BMP180 是一款数字气压计传感器,实际可读出温度和气压值。此模块使用 IIC(i2c)协议。模块体积很小,比老周的大拇指指甲还小;也很便宜,一般是长这样的。螺丝孔只开一个,也有开两个孔的。

这货基本上没有焊接排针的,买回来得自己焊。以前提过,老周的焊工比较差,注定成不了焊武帝。所以在焊接的时候,第一次是温度没调高,280度居然化不了锡(锡丝说明上说180-254度均可),然后调到300度,OK。然而一时手残,有两个焊盘被我弄成“连锡”,于是很无奈地用烙铁头拼命地刮锡。总算焊好了,只是长相实在丑陋,看着像四抔鸡 Shi 在上面。也罢,反正自己用,管他呢,能导电就行。

做实验时其实不焊接也行,把它放在面包板上,然后用面包板线直接插在模块的接口上、这样做能用,只是容易接触不良。当然了,你找四根铁丝(或剥了皮的电线)穿过焊盘上的孔,用手拧紧也行,反正能让其导电就行。

-----------------------------------------------------------------------------------------------------------------------

BMP180 模块其实操作起来不算难,就是读出来的数据换算过程比较长。这个可以直接抄数据手册上的,只是抄的时候要专心,很容易抄错步骤。

首先,它的 IIC 从机地址是 0x77。

const int DEV_ADDR = 0x77;

它有四种工作方式,由过采样率(OSRS)表示,值分别为0,1,2,3。

1、超低功耗(ultra low power)= 0;

2、标准(standard) = 1;

3、高精度(high)= 2;

4、超高精度(ultra high resolution))= 3。

由于这些值是固定的,咱们可以用一个枚举类型来定义。

    public enum OSRS
{
UltraLowPower = 0,
Standard = 1,
High = 2,
UltraHighResolution = 3
}

一、初始化校准变量

在模块上电后,需要从一系列寄存器中读出一堆 16 位整数,用于模块自身的校准。因为每个寄存器中的值是 8 位,所以,每个校准变量都要用到两个寄存器,高字节先读,再读低字节。

下面是数据手册上的截图。

编程的时候,直接按这个来就是了。比如,AC1 变量,读的寄存器为 0xAA 和 0xAB。其中,只有 AC4、AC5、AC6 是无符号整数(ushort),其他都是有符号的(short)。

        short AC1, AC2, AC3;
ushort AC4, AC5,AC6;
short B1, B2;
short MB, MC, MD;

读寄存器的方法是先向 IIC 从机写入(发送)寄存器的地址,然后再读,这样就会返回对应寄存器的值。

1、write address ----->

2、read value <-------

        private byte ReadByteFromReg(byte regaddr)
{
byte r = 0;
// 1、写入要读的寄存器地址
_dev.WriteByte(regaddr);
// 2、读内容
r = _dev.ReadByte();
return r;
}

读16位整数就是读两个寄存器,然后把两个字节组成一个16位整数值,这里它采用的是“大端”格式(Big Endian)。可以使用一个辅助类——

BinaryPrimitives ,位于 System.Buffers.Binary 命名空间。
        private UInt16 ReadUint16(byte addr1, byte addr2)
{
UInt16 r = 0;
Span<byte> data = stackalloc byte[2];
// 读第一个字节
data[0] = ReadByteFromReg(addr1);
// 读第二个字节
data[1] = ReadByteFromReg(addr2);
// 字节顺序为“大端”(BE)
r = BinaryPrimitives.ReadUInt16BigEndian(data);
return r;
}

这个方法统一返回无符号整数,需要时可以强制转换为有符号的。比如,用下面代码来初始化校准变量。

            AC1 = (short)ReadUint16(0xaa, 0xab);
AC2 = (short)ReadUint16(0xac, 0xad);
AC3 = (short)ReadUint16(0xae, 0xaf);
AC4 = ReadUint16(0xb0, 0xb1);
AC5 = ReadUint16(0xb2, 0xb3);
AC6 = ReadUint16(0xb4, 0xb5);
B1 = (short)ReadUint16(0xb6, 0xb7);
B2 = (short)ReadUint16(0xb8, 0xb9);
MB = (short)ReadUint16(0xba, 0xbb);
MC = (short)ReadUint16(0xbc, 0xbd);
MD = (short)ReadUint16(0xbe, 0xbf);

二、读出温度和气压的原始数据(未经过OSRS补偿)

在读出数据后需要进行一堆运算,其中会用到这些变量。

        int B3, B5, B6;
uint B4, B7;
int X1, X2, X3; float _temper, _pressure;

最后一行的两个浮点数,表示经过运算后真实的温度和气压值。温度单位为摄氏度,气压单位为帕。温度精度是 0.1 摄氏度,即 290 表示 29.0 度;气压精度是帕,一般我们看天气预报用的是百帕(hPa),所以结果要乘以 0.01。

下面两个方法读出温度和气压的原始值,类型为整型。

        // 私有方法:读出未经OSRS补偿的温度
private int ReadUncompensatedTemper()
{
// 1、先向0xF4寄存器写入0x2e
WriteByteToReg(0xf4, 0x2e);
// 2、坐和等待
Thread.Sleep(5);
// 3、从两个寄存器中读出数据
return (int)ReadUint16(0xf6, 0xf7);
}
// 私有方法:读出未作补偿的气压
// 这个读出来是24位的,所以用int
private int ReadUncompensatedPressure()
{
// 写寄存器
byte wv = (byte)(0x34 + ((byte)_osrs << 6)); // 注意这里
WriteByteToReg(0xf4, wv);
// 等待时间由OSRS决定
// 精度越高,所需要的时间越长
switch(_osrs)
{
case OSRS.UltraLowPower:
Thread.Sleep(5);
break;
case OSRS.Standard:
Thread.Sleep(8);
break;
case OSRS.High:
Thread.Sleep(14);
break;
case OSRS.UltraHighResolution:
Thread.Sleep(26);
break;
}
// 读出
byte[] data = new byte[3];
data[0] = ReadByteFromReg(0xf6);
data[1] = ReadByteFromReg(0xf7);
data[2] = ReadByteFromReg(0xf8);
return ((data[0] << 16) + (data[1] << 8) + data[2]) >> (8 - (byte)_osrs);
}

在写完寄存器后,因为模块要采集数据,所以要等待十到几十毫秒,精度越高,等待的时间越长。这是数据手册上的表格。

三、补偿运算(得出真正的结果)

这个过程是连续的,先算出真实的温度,再算气压;计算气压时也会用到温度的计算结果,所以说这个过程其实是连起来的。这个过程没什么特殊技巧的,完全就是抄手册。流程如下

运算的代码如下:

        public void MeasureDatas()
{
int ut = ReadUncompensatedTemper();
int up = ReadUncompensatedPressure();
X1 = (ut - AC6) * AC5 / 32768;
X2 = MC * 2048 / (X1 + MD);
B5 = X1 + X2;
// 温度已算出
_temper = ((B5 + 8) / 16) * 0.1f;
B6 = B5 - 4000;
X1 = (B2 * (B6 * B6 / 4096)) / 2048;
X2 = AC2 * B6 / 2048;
X3 = X1 + X2;
B3 = (((AC1 * 4 + X3) << (byte)_osrs) + 2) / 4;
X1 = AC3 * B6 / 8192;
X2 = (B1 * (B6 * B6 / 4096)) / 65536;
X3 = ((X1 + X2) + 2) / 4;
B4 = AC4 * (uint)(X3 + 32768) / 32768;
B7 = (uint)(up - B3) * (uint)(50000 >> (byte)_osrs);
int p = B7 < 0x80000000 ? (int)((B7*2)/B4) : (int)((B7/B4)*2);
X1 = (p * p) / 65536;
X1 = (X1 * 3038) / 65536;
X2 = (-7357 * p) / 65536;
p = p + (X1 + X2 + 3791) / 16;
// 气压已算出
_pressure = p * 0.01f;
}

抄手册时要小心,因为太长,一不小心就会抄错。整个文件的代码如下:

using System;
using System.Device.I2c;
using System.Buffers.Binary;
using System.Threading; namespace Device
{
// 过采样率
public enum OSRS
{
UltraLowPower = 0,
Standard = 1,
High = 2,
UltraHighResolution = 3
} public class Bmp180 : IDisposable
{
// 默认地址
private const int DEV_ADDR = 0x77;
// 过采样系数
OSRS _osrs;
// IIC 设备引用
I2cDevice _dev = null; // 下面这一组变量都是根据数据手册定义的
short AC1, AC2, AC3;
ushort AC4, AC5,AC6;
short B1, B2;
short MB, MC, MD;
int B3, B5, B6;
uint B4, B7;
int X1, X2, X3; float _temper, _pressure; // 构造函数
public Bmp180(OSRS oss = OSRS.Standard)
{
_osrs = oss;
// 初始化IIC设备
// 总线ID(BUS ID)可以自己根据实际来改
// 我这里用的是4,一般默认是1
I2cConnectionSettings cs = new(4, DEV_ADDR);
_dev = I2cDevice.Create(cs);
// 读入校准数据
ReadCalibration();
} // 私有方法:向寄存器写入字节
private void WriteByteToReg(byte regaddr, byte val)
{
Span<byte> data = stackalloc byte[2];
data[0] = regaddr; //寄存器地址
data[1] = val; //要写的值
_dev.Write(data);
}
// 私有方法:从寄存器读出字节
private byte ReadByteFromReg(byte regaddr)
{
byte r = 0;
// 1、写入要读的寄存器地址
_dev.WriteByte(regaddr);
// 2、读内容
r = _dev.ReadByte();
return r;
} // 私有方法:从寄存器中读出16位整数
// 16位整数有两个字节,分布在两个寄存器中
private UInt16 ReadUint16(byte addr1, byte addr2)
{
UInt16 r = 0;
Span<byte> data = stackalloc byte[2];
// 读第一个字节
data[0] = ReadByteFromReg(addr1);
// 读第二个字节
data[1] = ReadByteFromReg(addr2);
// 字节顺序为“大端”(BE)
r = BinaryPrimitives.ReadUInt16BigEndian(data);
return r;
}
// 私有方法:读校准数据
// 这个没啥技术含量,完全按照手册上来
private void ReadCalibration()
{
AC1 = (short)ReadUint16(0xaa, 0xab);
AC2 = (short)ReadUint16(0xac, 0xad);
AC3 = (short)ReadUint16(0xae, 0xaf);
AC4 = ReadUint16(0xb0, 0xb1);
AC5 = ReadUint16(0xb2, 0xb3);
AC6 = ReadUint16(0xb4, 0xb5);
B1 = (short)ReadUint16(0xb6, 0xb7);
B2 = (short)ReadUint16(0xb8, 0xb9);
MB = (short)ReadUint16(0xba, 0xbb);
MC = (short)ReadUint16(0xbc, 0xbd);
MD = (short)ReadUint16(0xbe, 0xbf
);
}
// 私有方法:读出未经OSRS补偿的温度
private int ReadUncompensatedTemper()
{
// 1、先向0xF4寄存器写入0x2e
WriteByteToReg(0xf4, 0x2e);
// 2、坐和等待
Thread.Sleep(5);
// 3、从两个寄存器中读出数据
return (int)ReadUint16(0xf6, 0xf7);
}
// 私有方法:读出未作补偿的气压
// 这个读出来是24位的,所以用int
private int ReadUncompensatedPressure()
{
// 写寄存器
byte wv = (byte)(0x34 + ((byte)_osrs << 6)); // 注意这里
WriteByteToReg(0xf4, wv);
// 等待时间由OSRS决定
// 精度越高,所需要的时间越长
switch(_osrs)
{
case OSRS.UltraLowPower:
Thread.Sleep(5);
break;
case OSRS.Standard:
Thread.Sleep(8);
break;
case OSRS.High:
Thread.Sleep(14);
break;
case OSRS.UltraHighResolution:
Thread.Sleep(26);
break;
}
// 读出
byte[] data = new byte[3];
data[0] = ReadByteFromReg(0xf6);
data[1] = ReadByteFromReg(0xf7);
data[2] = ReadByteFromReg(0xf8);
return ((data[0] << 16) + (data[1] << 8) + data[2]) >> (8 - (byte)_osrs);
} // 公共方法:处理所有数据
public void MeasureDatas()
{
int ut = ReadUncompensatedTemper();
int up = ReadUncompensatedPressure();
X1 = (ut - AC6) * AC5 / 32768;
X2 = MC * 2048 / (X1 + MD);
B5 = X1 + X2;
// 温度已算出
_temper = ((B5 + 8) / 16) * 0.1f;
B6 = B5 - 4000;
X1 = (B2 * (B6 * B6 / 4096)) / 2048;
X2 = AC2 * B6 / 2048;
X3 = X1 + X2;
B3 = (((AC1 * 4 + X3) << (byte)_osrs) + 2) / 4;
X1 = AC3 * B6 / 8192;
X2 = (B1 * (B6 * B6 / 4096)) / 65536;
X3 = ((X1 + X2) + 2) / 4;
B4 = AC4 * (uint)(X3 + 32768) / 32768;
B7 = (uint)(up - B3) * (uint)(50000 >> (byte)_osrs);
int p = B7 < 0x80000000 ? (int)((B7*2)/B4) : (int)((B7/B4)*2);
X1 = (p * p) / 65536;
X1 = (X1 * 3038) / 65536;
X2 = (-7357 * p) / 65536;
p = p + (X1 + X2 + 3791) / 16;
// 气压已算出
_pressure = p * 0.01f;
} // 公共属性:获得真实的温度值
public float GetTemper() => _temper;
// 公共属性:获得真实的气压
public float GetPressure() => _pressure; public void Dispose()
{
_dev?.Dispose();
}
}
}

【注】在实例化 I2cConnectionSettings 时,bus id 一般是 1,因为老周在树莓派上开了 i2c-4,所以总线是 4(因为默认的GPIO被外接的风扇插头挡住,插不进杜邦线)。

测试一下。

        static void Main(string[] args)
{
Bmp180 dev = new Bmp180(); while(true)
{
dev.MeasureDatas();
Console.Clear();
float temp = dev.GetTemper();
float pres = dev.GetPressure();
Console.WriteLine("温度:{0:0.00} ℃,气压:{1:0.00} hPa", temp, pres);
System.Threading.Thread.Sleep(1000);
}
}

结果如下图所示。

这个运算过程有个地方比较蛋疼,那就是误差。怎么说呢,比如一个表达式中同时存在乘法和除法时,你会发现先除再乘,与先乘再除之间所产生的结果是有差距的,得到的气压会接近 1015 hPa 到 1020 hPa。比如,有行代码:

实际上这是个平方运算,但是,用  (p / 256) * (p / 256) 与  (p * p) / 65536 之间得到结果会有差距,这个真不好说哪个更准确了。

===========================================================================================

上面老周只是为了给大伙伴演示才自己动手写了个封装,其实微软团队已经在 Iot.Device.Bindings 库中提供了封装,可以直接拿来用。

在项目中添加 system.device.gpio 和 iot.device.bindings 这两个包包的引用。

dotnet add package System.Device.Gpio
dotnet add package Iot.Device.Bindings

然后就可以直接开局。

using System;
using System.Device.I2c;
using Iot.Device.Bmp180;
using System.Threading;
using UnitsNet; namespace MyApp
{
class Program
{
static void Main(string[] args)
{
// IIC 总线初始化
I2cConnectionSettings iicset = new I2cConnectionSettings(4, Bmp180.DefaultI2cAddress);
I2cDevice device= I2cDevice.Create(iicset);
// BMP180对象初始化
Bmp180 bmpobj = new Bmp180(device);
// 设置采样模式
bmpobj.SetSampling(Sampling.Standard); // 读数
while(1 == 1)
{
// 温度
Temperature tmp = bmpobj.ReadTemperature();
// 气压
Pressure prs = bmpobj.ReadPressure();
// 输出
string outstr = $"温度:{tmp.DegreesCelsius:0.00} ℃\n气压:{prs.Hectopascals:0.00} hPa";
Console.Clear();
Console.WriteLine(outstr);
Thread.Sleep(1000);
}
}
}
}

注意 I2cConnectionSettings 初始化时,总线ID我这里用的是4,前面说过原因,如果你没修改过树莓派的配置,那默认是 1。

运行结果如下:

因为刚刚下了一场大暴雨,所以温度比上午时低了 2 度。

好了,今天的博文就水到这里了。

【.NET 与树莓派】气压传感器——BMP180的更多相关文章

  1. 张高兴的 Windows 10 IoT 开发笔记:BMP180 气压传感器

    注意:海拔高度仅供参考 GitHub : https://github.com/ZhangGaoxing/windows-iot-demo/tree/master/BMP180Demo

  2. 基于树莓派的微型气象站设计与开发(Windows 10 IoT Core)

    前言 树莓派(Raspberry Pi,RPi)是专门为学生计算机编程教育而设计,只有信用卡大小的卡片式电脑,可以运行Linux或者Windows 10 IoT Core操作系统.本文将利用树莓派和U ...

  3. 树莓派 + Windows IoT Core 搭建环境监控系统

    前言:Windows IoT 是微软为嵌入式开发板设计的一种物联网操作系统,运行Windows UWP(C# 开发),可以设计出丰富的交互界面,驱动GPIO,连接一些传感器做有意思的事,本文详细介绍如 ...

  4. 「雕爷学编程」Arduino动手做(27)——BMP280气压传感器

    37款传感器与模块的提法,在网络上广泛流传,其实Arduino能够兼容的传感器模块肯定是不止37种的.鉴于本人手头积累了一些传感器和模块,依照实践出真知(一定要动手做)的理念,以学习和交流为目的,这里 ...

  5. 详解树莓派Model B+控制蜂鸣器演奏乐曲

    步进电机以及无源蜂鸣器这些都需要脉冲信号才能够驱动,这里将用GPIO的PWM接口驱动无源蜂鸣器弹奏乐曲,本文基于树莓派Mode B+,其他版本树莓派实现时需参照相关资料进行修改! 1 预备知识 1.1 ...

  6. Linux主机上使用交叉编译移植u-boot到树莓派

    0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...

  7. 树莓派 基于Web的温度计

    前言:家里的树莓派吃灰很久,于是拿出来做个室内温度展示也不错. 板子是model b型. 使用Python开发,web框架是flask,温度传感器是ds18b20 1 硬件连接 ds18b20的vcc ...

  8. Raspberry Pi(树莓派)上安装Raspbian(无路由器,无显示器)

    一. 准备工作 1. 树莓派主板 型号:树莓派3 B型 处理器:四核64位ARM Cortex-A53 CPU 内核架构:ARMv8 2. 一张大于8G的TF卡(本人用的是32G的,也作为PiLFS用 ...

  9. 树莓派3B的食用方法-1(装系统 网线ssh连接)

    首先要有一个树莓派3B , 在某宝买就行, 这东西基本上找到假货都难,另外国产和英国也没什么差别,差不多哪个便宜买哪个就行. 不要买店家的套餐,一个是配的东西有些不需要,有的质量也不好. 提示:除了G ...

随机推荐

  1. 【springcloud】Zuul高级配置(zuul--3)

    转自:https://blog.csdn.net/pengjunlee/article/details/87285673 为路由提供HystrixFallback 当Zuul中某一个路由的断路器被断开 ...

  2. 深入浅出Mybatis系列(八)---objectFactory、plugins、mappers

    1.objectFactory是干什么的? 需要配置吗? MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成.默认的对象工厂需要做的仅仅是实例化 ...

  3. 使用HttpURLConnection多线程下载

    1 import java.io.IOException; 2 import java.io.InputStream; 3 import java.io.RandomAccessFile; 4 imp ...

  4. jQuery中的文档操作处理(五):append()、prepend()、after()、before()、wrap()、wrapAll()、wrapInner()、clone()等

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <hea ...

  5. 读《深入理解java虚拟机》小结

    之所以学习 jvm ,是因为在学习多线程相关知识时,对 volatile 关键字理解的不够透彻,总有种似懂非懂的感觉.于是通过在网上各种资料的查阅,最终将 volatile 和 jvm 联系上了,本身 ...

  6. vue element-ui el-date-picker 数据可以更改,但是前端不显示的更改后的数据问题

    template: <el-form-item label="有效时间:" prop="validTime">                    ...

  7. python入门-变量与数据类型

    1.命名规则 变量名只能包含字母.数字和下划线.但不能以数字打头. 变量名不能包含空格 不能与关键字冲突 变量名应尽量简短且具有描述性 2.字符串 python中引号括起的内容,其中引号可以为单引号或 ...

  8. java 线程基础篇,看这一篇就够了。

    前言: Java三大基础框架:集合,线程,io基本是开发必用,面试必问的核心内容,今天我们讲讲线程. 想要把线程理解透彻,这需要具备很多方面的知识和经验,本篇主要是关于线程基础包括线程状态和常用方法. ...

  9. 前端搭建Linux云服务器,Nginx配置详解及部署自己项目到服务器上

    目录 搭建Linux云服务器 购买与基本配置 链接linux服务器 目录结构 基本命令 软件安装 Linux 系统启动 启动过程 运行级别 Nginx详解 1.安装 方式一:yum安装 方式二:自定义 ...

  10. java基础之ThreadLocal

    早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编写出优美的多线程程序.Thr ...