zoukankan      html  css  js  c++  java
  • 【.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} ℃
    气压:{prs.Hectopascals:0.00} hPa";
                    Console.Clear();
                    Console.WriteLine(outstr);
                    Thread.Sleep(1000);
                }
            }
        }
    }

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

    运行结果如下:

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

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

  • 相关阅读:
    这可能是全网最全的流媒体知识科普文章
    JavaCV实战专栏文章目录(JavaCV速查手册)
    JavaCV复杂滤镜filter特效处理入门教程和常用案例汇总
    【PC桌面软件的末日,手机移动端App称王】写在windows11支持安卓,macOS支持ios,龙芯支持x86和arm指令翻译
    如何在龙芯架构和国产化操作系统平台上运行javacv
    如何在国产龙芯架构平台上运行c/c++、java、nodejs等编程语言
    JavaCV开发详解之35:使用filter滤镜实现画中画,以屏幕画面叠加摄像头画面为例
    JavaCV开发详解之34:使用filter滤镜实现字符滚动和无限循环滚动字符叠加,跑马灯特效制作
    JavaCV开发详解之33:使用filter滤镜实现动态日期时间叠加
    JavaCV开发详解之32:使用filter滤镜实现中文字符叠加
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/15359206.html
Copyright © 2011-2022 走看看