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 度。
好了,今天的博文就水到這裡了。