【.NET 与树莓派】气压传感器——BMP180
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)。可以使用一个辅助类——
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 度。
好了,今天的博文就水到这里了。