3

【.NET 与树莓派】气压传感器——BMP180

 2 years ago
source link: https://www.cnblogs.com/tcjiaan/p/15359206.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【.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);
        }

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

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

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

367389-20211001121331055-1066673109.png

运算的代码如下:

        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);
            }
        }

结果如下图所示。

367389-20211001122146262-17069311.png

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

367389-20211001122515404-2138994497.png

 实际上这是个平方运算,但是,用  (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。

运行结果如下:

367389-20211001171927785-543479547.png

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK