前言
其實是從零件盒翻到一顆一年前買的 BMP180,這個很普遍的大氣壓力感測器。想說,好吧,來玩玩這顆感測器吧,順便豐富一下我們 Flash FORTH 的函式庫吧。
利用人家已經寫好的函式庫,然後 #include 進來用,這太 Low 了啦。來練練功,試看看只靠 Data Sheet 裡的資料,能否順利寫出底層可以跟 BMP180 溝通的程式碼呢?就這樣抱著試看看的心態,(鍵盤)東敲敲,西打打的,最後還是順利寫出來囉,蠻有趣的。
其實,以前就對那種利用氣壓計來做天氣預測的電子晴雨計非常著迷。只要有機會出國的話,總是會買個帶回來,放在客廳。出門前總是會多看幾眼,瞧瞧它的預測,看是要下雨了,還是會是個大晴天來當個參考。當然,現在網路時代,即時氣象預測應該都比這種老古董準確多囉。但,客廳擺個電子溫度計晴雨計的個人小氣象台,瞬間質感就增加很多。☺️
沒想到現在可以自己做了呢, DIY Maker 萬歲!
BMP180
BMP180 溫度大氣壓力感測器,是最新 BOSCH 所推出的溫度氣壓傳感器,用來代替舊型的 BMP085。
其性能非常高,主要鎖定在智慧手機,平板電腦和運動設備等高級移動設備的應用。它主要遵循舊型的 BMP085 但增加許多改進,如較小的尺寸和數位匯流排的擴展。
超低功耗低至3μA,絕對精度最低可以達到0.03hPa,使BMP180成為移動設備節能的領導者。BMP180的獨特之處在於其獨立於電源電壓的非常穩定的行為(性能)。其採用強大的8-pin陶瓷無引線晶片承載(LCC)超薄封裝,可以通過I2C匯流排直接與各種微處理器相連接。
主要特點:
- 壓力範圍:300~1100hPa(海拔9000米~-500米)
- 電源電壓:1.8V~3.6V(VDDA),1.62V~3.6V(VDDD)
- LCC8封裝:無鉛陶瓷載體封裝(LCC)
- 尺寸:3.6mmx3.8×0.93mm
- 低功耗:5μA,在標準模式
- 高精度:低功耗模式下,解析度為0.06hPa(0.5米)
- 高線性模式下,解析度為0.03hPa(0.25米),且具有溫度補償
- 含溫度輸出
- I2C介面
- MSL 1反應時間:7.5ms
- 待機電流:0.1μA
- 無需外部時鐘電路
典型應用:
- 室內室外導航,GPS 精確導航輔助
- 休閒、體育和醫療健康等監測
- 天氣預報
- 垂直速度指示(上升/下沉速度)
- 風扇功率控制
硬體接線
最近學到的技巧,兩台 I2C 裝置要對連的話,只要簡單 SDA 接 SDA,SCL 接 SCL 就可以囉,不需要上拉電阻。然後這個 BMP180,它的 I2C 的 SlaveID 是 0x77
要注意的 BMP180 是吃 3.3V 的電源的,所以 Vcc 要接到 Arduino Uno 3.3V 那裡,不要錯接到 5V 囉。
內部方塊圖
FORTH 程式解說
看圖說故事囉,看 Datasheet 寫程式碼!一切的一切,從這個原廠的 Datasheet 開始!
BMP180 很壞的地方是,它提供了很多校正常數。只讀出感測器的溫度和壓力的數值是不準確的,需要利用這些 BMP180 內部的校正常數,透過演算法的計算,才能算出正確的溫度和壓力的數值。 BMP180 內部並沒有 MCU,所以這麼複雜的計算它是無能為力的。也就是說,沒有免費的午餐,你要幫它在你自己的 MCU 裡實作這些演算法才能正確得到最終溫度跟壓力的數值。
Datasheet 提供了詳細的流程跟C語言整數運算的算式和範例。你要一步一步的照這些整數運算的算式,逐步演算後才會得到正確的溫度和壓力的數值。但是,詳細的校正常數的校正原理,Bosch 並沒有公開。所以所有的算式只能照著 Datasheet 去實做啦,而這些公式怎麼來的,這一切都是秘密!
所以,照 Data Sheet
第一步,讀出所有校正常數
校正常數一共 11 個單整數的數值,(AC1, AC2..., AC6, B1, B2, MB, MC, MD)
我們可以用之前所開發的 I2C 的指令 regsRead ( start-addr n -- data1 data2 ... datan) 一次讀完,送到 FORTH 的參數堆疊來。不過因為 Arduino Uno 的記憶體很有限,所以 Falsh FORTH 的堆疊無法太大,經過測試,這樣會垮掉的。
所以可以一次用 regRead ( addr -- data) 讀一個暫存器的值,這樣比較慢,但比較保險。或是依舊使用 regsRead,但一次讀少一點,避免參數堆疊跨掉。
這裡我們依舊採用 regsRead 一次讀多個參數,但分開兩次讀取。
FORTH 有兩種變數的使用方式,
第一種是經典的 variable [Name] ,使用的時候會回傳變數的位址,然後用 @ 取出數值,或 ! 存入數值。
第二種是比較新的,有點可變常數的意涵的 XXX value [Name],使用的時候會不回傳變數的位址了,而是直接是變數的值。然後可以用 XXX to [Name] 用 to 把新數值存入變數中。
這裡校正常數看不出來有需要得到它們變數位址的需要,所以採用第二種方法來定義校正常數。
為了測試用,所以一開始的值是 Datasheet 裡面的範例數值,這樣也好方便測試所編寫的數學公式程式碼有無錯誤。
408 value AC1
-72 value AC2
-14383 value AC3
32741 value AC4
32757 value AC5
23153 value AC6
6190 value B1
4 value B2
-32768 value MB
-8711 value MC
2868 value MD
I2C 一次只能傳一個 byte的數值,且Datasheet 寫了, MSB first。所以 regsRead 讀的時候, MSB 會先,然後才是 LSB。利用下列程式碼將這兩個 byte 變成完整 16bit 的 FORTH 單整數數值。
: >num ( msb lsb -- num)
swap 8 lshift or
;
就 msb 左移 8 bits 後跟 lsb OR運算一下就是囉。
前面說了,這裡我們依舊採用 regsRead 一次讀多個參數,但分開兩次讀取。
第一次從 0xAA 開始,總共讀 12 bytes。 (所以是 AC1, AC2, AC3..., AC6)
第二次從 0xB6 開始,總共讀 10 bytes。 (所以是 AC1, AC2, AC3..., AC6)
: calib! ( --) \ read calibration data
$aa 12 regsRead
>num to AC6
>num to AC5
>num to AC4
>num to AC3
>num to AC2
>num to AC1
$b6 10 regsRead
>num to MD
>num to MC
>num to MB
>num to B2
>num to B1
;
就讀出 2bytes 後, >num 合併成一個單整數,然後存入對應變數中,提供後面計算使用。
第二步,讀出未校正溫度值 UT
: rawT@ ( -- temp.raw) \ read raw temperature data
$2e $f4 regWrite
5 ms
$f6 2 regsRead
>num
;
看圖說故事,先在 0xF4 暫存器寫入 0x2E 觸發溫度開始轉換後,等 4.5 ms (這裡我們等 5 ms) 後於暫存器 0xF6 (MSB) 0xF7 (LSB) 讀出結果。
一樣,用 >num 將兩個 bytes 的結果合併成一個單整數後完成,這個數字 Data Sheet 稱它為 UT。
第三步,讀出未校正壓力值 UP
: rawP@ ( -- pressure.raw) \ read raw pressure data
$34 $f4 regWrite
50 ms
$f6 2 regsRead
>num 0
;
寫入 0x34 資料至暫存器 0xF4 觸發壓力感測器轉換。 要注意的,這裡有個 OSS 過取樣的參數,BMP180 支援過取樣(Over-Sampling) 的技術來增加數值的精確度。如果要使用的話, OSS 參數需左移6bits 後合併至 0x34 。
這裡為了簡單,都不使用 Over-Sampling,所以 OSS = 0 。
觸發壓力轉換後,需要等待轉換完畢後在 0xF6, 0xF7, 0xF8 取出轉換後的結果。 BMP180 為了彈性,設計了很多模式。每種模式得到的精確度跟轉換時間的不一樣。這裡為了保險,採用了最大的等待時間,不會超過 50 ms ,所以等待 50 ms 後來取值。
未校正的壓力值被稱為 UP ,為一個 24 bits 的數字,所以需要一個 32 bits 的雙整數來容納它。
所以是 0xF6 (MSB) 左移 16位元,0xF7 (LSB) 左移 8位元,再跟 0xF8 (XLSB) 合併成 24位元的數字,但因為我們不用 Over-Sampling,所以 OSS =0 , 8 - OSS = 8 - 0 = 8
所以24位元的數字要右移8位元。這結果就是 MSB.LSM 一個 16位元的單整數囉。 所以用 >num 來合併就可以囉。而 0xF8 (XLSB) 可以直接丟掉不用。
最後為了彈性,補個 0 讓 UP 還是一個 FORTH 系統裡面不帶符號的雙整數。
第四步,根據校正參數,由 UT 計算出正確的溫度值
呵呵,好玩的來囉。
來好好練習一下 FORTH 的固定整數運算的技巧囉。
這裡一切先求有,先不考慮最佳化。 Data Sheet 的這些公式都是以 C 語言為基礎的。C語言這類的語言都是以變數為主來運作的語言,跟我們 FORTH 是以堆疊為主的語言很不一樣,但也只能將就了。
所以這裡看圖說故事, Data Sheet 怎麼說,我們就怎麼做,可讀性就只能放棄囉。
所以
: X1 ( UT -- X1)
AC6 - AC5 32768 u*/mod nip
;
就 X1 = (UT - AC6) * AC5 / 2^15
給定 UT 把 X1 算出來。 2^15 = 32768 ,很直接的 UT 和 AC6 相減後 AC5/32768 的一個不帶符號的比例乘法 u*/mod 就搞定囉!
: X2 ( X1 -- X2)
MD + MC 2048 rot */
;
就 X2 = MC * 2^11 / (X1 + MD)
給定 X1 算出 X2 來。 2^11 = 2048 ,也是很直接的 MC 來跟 2048/(X1+MD) 來個帶符號的比例乘法 */ 就搞定囉!
: B5 ( UT -- B5)
X1 dup X2 +
;
就 B5 = X1 + X2
給定 UT 算出 X1,複製給定 X1 算出 X2,然後 X1 跟X2 相加!
: >Celsius ( UT -- Celsius) \ unit: in 0.1C
B5 8 + 16 /
;
就 T = (B5 + 8) / 2^4 ,最終結果,校正過後的真實攝氏溫度。
給定 UT 算出 B5 加 8 後除以 16 就是最終結果!
第五步,根據校正參數,由 UT 及 UP 計算出正確的壓力值
最複雜的來了,來根據 UT 及 UP 及校正常數,逐步算出正確的壓力值吧!
: B6 ( B5 -- B6)
4000 -
;
就 B6 = B5 - 4000 ,簡單明暸!
: X1.1 ( B6 -- X1)
dup 4096 */ B2 2048 */
;
就 X1 = (B6 * B6 / 2^12) * B2 / 2^11
給定 B6 ,複製一下跟 4096 來個 */ 的比例運算 ( 2^12 = 4096)
然後結果來跟 B2/2048 再來一次 */ 的比例運算就是囉 (2^11 = 2048)
這裡要先抱怨一下的,一般的 FORTH 系統,正常是允許重複定義指令的。也就是說,當已經有一個指令被定義在 FORTH 的字典中,這時候假如再重複定義一個相同名字的指令,這是可以被允許的。只是已經被編譯過的不受影響,但新的指令如果再被用到的話,新的會蓋過舊的指令。
然後我們 Flash FORTH 的開發者,不知道哪根筋不對了。這麼優良的傳統居然給它屏棄掉了,已經定義過的指令不允許再次重新定義過。
哎哎哎,只好用很憋扭的名字,將 X1 用 X1.1 來代替囉!
: X2.1 ( B6 -- X2)
AC2 2048 */
;
就 X2 = AC2 * B6 /2^11 ,一次 */ 比例運算搞定!
: X3.1 ( B6 -- X3)
dup X1.1 swap X2.1 +
;
就 X3 = X1 + X2
: B3 ( B6 -- B3)
X3.1 AC1 4 * + 2+ 4 /
;
就 B3 = (((AC1*4 + X3) <<OSS) + 2) / 4
這裡 OSS = 0 ,因為我們不使用 Over-Sampling。 所以不用左移任何位元。
所以公式為 B3 = (AC1*4 + X3 + 2) / 4
...
中間一堆算式,都是用類似的手法轉成 FORTH 語言程式碼,就不介紹囉。
挑比較重要的,
: B7 ( d.UP B6 -- d.p0)
>r r@ B3 0 d-
d2* 50000 ud*
r> B4 ud/mod rot drop
;
就看起來很複雜的 B7 = ((unsigned long)UP - B3) * (50000 >>OSS) if (B7<0x80000000) { p = (B7*2)/B4} else { p=(B7/B4)*2}
這段的 C語言程式碼,把 UP 轉型成不帶符號長整數 (等於FORTH 雙整數) 進行運算,後面的 if 判斷是怕數字太大而溢位,所以先乘後除,改成先除後乘,只是這樣而已。
這裡,主角 UP 開始出場了。
要注意啊, UP 是個雙整數(兩個單整數組成,共 32bits) 的喲。所以這裡是很好的機會,來練習一下經典 FORTH 裡面雙整數的算數運算。
首先堆疊裡有一個 d.UP 雙整數,跟 B6 單整數。 >r 先把礙眼的 B6 推到返回堆疊裡面暫存一下。
r@ B3 給定 B6 把 B3 算出來。 放個 0 把B3單整數變成不帶符號的雙整數。
d- ( d1 d2 -- d1-d2) 是個雙整數減法的指令, 所以就是 d.UP - d.B6
d2* ( d -- 2*d) 是個雙整數乘上2的指令,所以就是 (d.UP - d.B6)*2
ud* ( ud u -- ud*u) 這個在 FORTH 裡面,我們叫做混合算數的指令。就一個不帶符號雙整數ud和一個不帶符號單整數u的相乘,結果是不帶符號的雙整數ud'=ud*u
所以 50000 ud* 後就是 (d.UP - d.B6)*2*50000
r> B4 ,給定 B6 算出 B4
ud/mod ( ud u -- ur udq) 又是一個混合算數的指令,就一個不帶符號雙整數ud和一個不帶符號單整數u的相除,結果是不帶符號的單整數餘數 ur ,及一個依舊是不帶符號雙整數的商 udq
所以 r> B4 ud/mod rot drop 其實就是將前面雙整數的結果除以 B4,只留商,將餘數丟掉。所以就是 (d.UP - d.B6)*2*50000/B4,這個就是由B7算出來的 p 啦,是個雙整數的結果。
最後
: >Pressure ( d.UP UT -- d.Pressure)
B5 B6 B7 ( d.p0)
2dup X1.3 >r ( d.p0 R:x1)
2dup X2.3 r> ( d.p0 x2 x1)
swap - 3791 + 16 /
m+
;
串連全部啦,給定 UP UT 然後算出正確壓力值囉!
就 p = p + (X1 + X2 + 3791 ) / 2^4
相信前面的解說之下,現在這段你應該是看得懂的!要提醒的,原來前面X2是要乘上 -7357 的負的數值的。這裡用了小技巧,前面乘上 7357 正的值,而在這裡,用相減的方式而不是相加來彌補,這樣可以盡量用到不帶符號雙整數的最大位數。
就這樣,所有計算完畢,正確壓力值就出來囉。
: BMP180@ ( -- Pa Celsius)
rawT@ >r
rawP@ r@ >Pressure
r> >Celsius
;
透過 I2C 觸發 BMP180 工作,取出溫度跟壓力,然後透過校正常數計算出正確壓力跟溫度的數值。
: main ( --)
init
begin
BMP180@
." Temperature = " 0 <# # [char] . hold #s #> type ." C , "
." Pressure = " d. ." Pa" cr
again
;
主測試程式碼,不斷觸發 BMP180 工作,取出正確壓力,溫度數值後列印。
測試結果,
還不錯的啦!溫度 (29.5C) 跟小米的溫度計所量到的 (29.3C) 相比較,誤差只有 0.2C 左右。
壓力 (996.3hPa) 跟 Apple Watch 所量測到的 (992.5 hPa) 相比較,大概很穩定的相差約 4 hPa 左右。真的是不錯的呀,可以拿來進一步製作家庭小氣象台,晴雨計的囉!
測試影片
原始程式碼
\
\ BMP180 Pressure Sensor
\ Frank Lin 2021.08.08
\
marker --i2c--
decimal
\
\ ARduino I2C
\
\ Registers
$b8 constant TWBR
$b9 constant TWSR
$bb constant TWDR
$bc constant TWCR
\ Bits in the Control Register
%10000000 constant mTWINT \ Interrupt Flag
%01000000 constant mTWEA \ Enable Acknowledge
%00100000 constant mTWSTA \ Start Condition
%00010000 constant mTWSTO \ Stop Condition
%00001000 constant mTWWC \ Write Collition
%00000100 constant mTWEN \ Enable
%00000001 constant mTWIE \ Interrupt Enable
variable ACK?
: i2cInit ( -- ) \ Set frequency to 100kHz
%11 TWSR mclr \ prescale = 1
[ Fcy #100 / #16 - 2/ ] literal TWBR c!
mTWEN TWCR mset
;
: DoneWait ( -- ) \ Wait for operation to complete
begin TWCR c@ mTWINT and until
;
: <Start| ( -- ) \ Send start condition
[ mTWINT mTWEN or mTWSTA or ] literal TWCR c!
DoneWait
;
: |Restart| ( -- ) \ Send repeated start
<Start|
;
: |Stop> ( -- ) \ Send stop condition
[ mTWINT mTWEN or mTWSTO or ] literal TWCR c!
;
: |Tx| ( c--) \ send 1 byte to bus
DoneWait
TWDR c!
[ mTWINT mTWEN or ] literal TWCR c!
DoneWait
TWSR c@ $f8 and $18 xor 0= \ SLA + W
if true ACK? ! exit then \ true if ACK
TWSR c@ $f8 and $28 xor 0= \ data byte
if true ACK? ! exit then \ true if ACK
TWSR c@ $f8 and $40 xor 0= \ SLA + R
if true ACK? ! exit then \ true if ACK
false ACK? ! \ false if NACK
;
: |Rx-ACK| ( -- c)
[ mTWINT mTWEN or mTWEA or ] literal TWCR c!
DoneWait TWDR c@
;
: |Rx-NACK| ( -- c)
[ mTWINT mTWEN or ] literal TWCR c!
DoneWait TWDR c@
;
: |Addr-WR| ( 7-bit-addr --) \ ask slave for writing
1 lshift 1 invert and
|Tx|
;
: |Addr-RD| ( 7-bit-addr --) \ ask slve for reading
1 lshift 1 or
|Tx|
;
: ?Response ( --) \ ACK check
ACK? @ abort" I2C: No response from Slave!"
;
: i2cPing? ( 7-bit-addr -- f ) \ ping i2c slave
<Start| |Addr-RD|
ACK? @
if |Rx-NACK| drop true
else false then
;
: scanner ( --) \ scan all i2c slave
base @ hex i2cInit
cr #5 spaces $10 for r@ 2 u.r next
$80
for r@ $0f and $f =
if cr r@ $f0 and #2 u.r [char] : emit space
then
r@ $7 $78 within
if r@ i2cPing?
if r@ #2 u.r
else ." -- " then
else #3 spaces then
next
cr base !
;
\
\ I2C extension
\
variable slaveid \ i2c address
: regWrite ( data reg --)
<Start|
slaveid @ |Addr-WR| ?Response
|Tx| ( reg)
|Tx| ( data)
|Stop>
;
: regRead ( reg -- data)
<Start|
slaveid @ |Addr-WR| ?Response
|Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
|Rx-NACK|
|Stop>
;
: regsWrite ( d1 d2 d3 ... dn reg n --)
<Start|
slaveid @ |Addr-WR| ?Response
swap |Tx| ( reg)
for
|Tx| ( data)
next
|Stop>
;
: regsRead ( reg n -- d1 d2 ... dn)
<Start|
slaveid @ |Addr-WR| ?Response
swap |Tx| ( reg)
|Restart|
slaveid @ |Addr-RD|
1-
for
|Rx-ACK|
next
|Rx-NACK|
|Stop>
;
\ Math extension
: */ ( n1 n2 n3 --- n1*n2/n3)
>r 2dup xor >r ( n1 n2 R: sign1,2 n3)
abs swap abs swap
r> r> ( u1 u2 sign1,2 n3 )
tuck xor ( u1 u2 n3 sign )
>r
abs u*/mod nip r> ?negate
;
marker --b180--
\ BMP180
408 value AC1
-72 value AC2
-14383 value AC3
32741 value AC4
32757 value AC5
23153 value AC6
6190 value B1
4 value B2
-32768 value MB
-8711 value MC
2868 value MD
: >num ( msb lsb -- num)
swap 8 lshift or
;
: calib! ( --) \ read calibration data
$aa 12 regsRead
>num to AC6
>num to AC5
>num to AC4
>num to AC3
>num to AC2
>num is AC1
$b6 10 regsRead
>num to MD
>num to MC
>num to MB
>num to B2
>num to B1
;
: init
$77 slaveid !
i2cInit 10 ms
calib!
;
: rawT@ ( -- temp.raw) \ read raw temperature data
$2e $f4 regWrite
5 ms
$f6 2 regsRead
>num
;
: rawP@ ( -- pressure.raw) \ read raw pressure data
$34 $f4 regWrite
50 ms
$f6 2 regsRead
>num 0
;
\ Calculation for temperature
: X1 ( UT -- X1)
AC6 - AC5 32768 u*/mod nip
;
: X2 ( X1 -- X2)
MD + MC 2048 rot */
;
: B5 ( UT -- B5)
X1 dup X2 +
;
: >Celsius ( UT -- Celsius) \ unit: in 0.1C
B5 8 + 16 /
;
\ Calculation for pressure
: B6 ( B5 -- B6)
4000 -
;
: X1.1 ( B6 -- X1)
dup 4096 */ B2 2048 */
;
: X2.1 ( B6 -- X2)
AC2 2048 */
;
: X3.1 ( B6 -- X3)
dup X1.1 swap X2.1 +
;
: B3 ( B6 -- B3)
X3.1 AC1 4 * + 2+ 4 /
;
: X1.2 ( B6 -- X1)
AC3 8192 */
;
: X2.2 ( B6 -- X2)
dup 4096 */ B1 32767 */ 2/
;
: X3.2 ( B6 -- X3)
dup X1.2 swap X2.2 + 2+ 4 /
;
: B4 ( B6 -- B4)
X3.2 32768 + AC4 32768 u*/mod nip
;
: B7 ( d.UP B6 -- d.p0)
>r r@ B3 0 d-
d2* 50000 ud*
r> B4 ud/mod rot drop
;
: X1.3 ( d.p0 -- x1 )
256 um/mod nip 0 over ud*
3038 ud* 65535 um/mod nip
;
: X2.3 ( d.p0 -- -x2 )
7357 ud* 65535 um/mod nip
;
: >Pressure ( d.UP UT -- d.Pressure)
B5 B6 B7 ( d.p0)
2dup X1.3 >r ( d.p0 R:x1)
2dup X2.3 r> ( d.p0 x2 x1)
swap - 3791 + 16 /
m+
;
: BMP180@ ( -- Pa Celsius)
rawT@ >r
rawP@ r@ >Pressure
r> >Celsius
;
: main ( --)
init
begin
BMP180@
." Temperature = " 0 <# # [char] . hold #s #> type ." C , "
." Pressure = " d. ." Pa" cr
again
;