【.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)。可以使用一个辅助类——

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 度。

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