【.NET 與樹莓派】氣壓感測器——BMP180

東邪獨孤發表於2021-10-01

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

好了,今天的博文就水到這裡了。

 

相關文章