前言
國中上物理課的時候,大家耳熟能詳的一個題目一定是:請問外面下雨打雷中,突然一道閃電劃空而過,然後你手上剛好有個碼錶開始計時。最後經過 4.7秒後你聽到雷的聲音了。今日室外的溫度計刻度是 23.7度C,請問跟估計一下閃電發生的距離離你多遠啊??
這時候老師會叫你背一下聲音在空氣中的速度公式,聲速 (m/sec) = 331 + 0.6 * T (攝氏溫度)
所以目前聲速為 331+ 0.6 * 23.7 = 345.22 m/s
然後因為 4.7秒後聽到雷聲,所以落雷處跟你的距離為 4.7*345.22 = 1622.534 公尺 = 1.622534 公里 (答案)
所以 331 + 0.6T 這個聲速公式,大家應該再熟悉也不過了!
關於超音波,就是超過 24kHz 頻率以上的聲波,因為頻率超出人耳所能感受的程度,人耳是聽不到的,所以叫超聲波。
最有名關於超聲波常聽到的是蝙蝠這個動物。因為它可以發出超聲波,然後藉由聲波撞擊物體回彈後,由這個時間差來得知物體的遠近。所以即使在暗無天日的山洞裡,無需眼睛,蝙蝠藉由超聲波來探知環境,依舊可以自由自在的飛行。
聲速如前面所示跟溫度有關,溫度越高,聲音的速度越快。假如我們像蝙蝠一樣用超音波來做距離的量測,超音波往返的時間除以二乘上當時聲音的速度就是量測的距離。假如我們的距離永遠是固定的,那量測到的時間就只跟當時聲音的速度有關,聲音速度越快的時候時間越短。但溫度越高的時候,聲音的速度越快。慢著,「溫度越高的時候,聲音的速度越快」,這不是溫度計了嗎。
量測到的時間越短,代表聲音的速度越快,意味著此時空氣的溫度越高。所以量測到的時間,就是空氣的溫度表現囉。
哈,讓我們來用這個原理,來製作一個靠量測聲音速度來得知溫度,很有趣的,超音波溫度計吧。
HC-SR04 超音波距離感測器
有玩過 Arduino 的一定會玩過這個感測器啦。HC-SR04是個超音波發射跟接收的一個雙重感測器,它可以像蝙蝠一樣,向周圍環境發射40kHz的超音波。超音波撞擊到物體後會反彈回來,會被HC-SR04 的另一個接收器接收到。藉由計算超音波往返之間的時間差,最後可以得知物體的距離。 (Datasheet 在這裏)
所以原來的 HC-SR04 是被設計來利用超音波來量測距離的。
像一隻 ET 有兩個眼睛,很可愛的 HC-SR04。 標示 T 的那顆眼睛負責發射超音波,標示 R 的那顆眼睛負責接收超音波。
一共有四根接腳,分別是 Vcc電源, Trig, Echo, 跟 GND接地
使用方式很簡單,
Trig 腳位送上 10uS 寬度的 TTL High 訊號後, HC-SR04 的發射端會開始發送8個 40kHz頻率的超音波脈波。
當最後一個脈波送出去後, Echo 的腳位會被拉 High。直到接收到最後一個超音波脈波回來後, Echo 的腳位訊號才會被拉 Low。所以藉由量測 Echo腳位拉High的時間,即可得知超音波往返的時間,進而得知物體的距離遠近 L。
讓我們來玩一玩這個感測器,下圖為接上 Arduino Uno 的狀況
Trig 給他接到 Arduino Uno Pin2
Echo 給他接到 Arduino Uno Pin3
FORTH 程式解說
這個部落格是要盡可能的來推廣 FORTH 語言的,所以這次我們不用 Arduino IDE 來撰寫程式,而是採用 Flash FORTH。
1. 距離量測
前面提到,要觸發 HC-SR04 開始運作,需要先有一個 10uS High 的 DIO 訊號。 uS 微秒可是 1E-6 次方秒耶,時間非常非常的短,已經開始接近機械語言原生的執行速度囉。 Flash FORTH 只有提供標準 mS 毫秒 1E-3 次方秒的指令, uS 微秒是沒有提供的。 (嚴格來說,其實是有的,開發者有提供一個版本 uS 的程式碼放在外部檔案給有需要的人。)
沒有提供也沒關係,我們可以很簡單的寫一個類似的,就
: delay ( u --) for next ;
很簡單的一個空的 for-loop 就可以拿來做 delay 啦。
不過要測試一下, 量測一下它真實的執行秒數。
: t ticks $ffff delay ticks swap - . ;
ticks 會把現在的毫秒(mS)為單位的 tick 數目取出來,然後執行 0xFFFF = 65535 次的 delay,然後再取一次現在的毫秒(mS)為單位的 tick 數目,兩個相減就是 65535 次的 delay 執行所花的毫秒(mS)時間。
經過多次執行 t 後,數據是 94.5 mS。所以每次的迴圈時間是
94.5 ms / 65535 = 1.44198 uS
所以假如需要 10 uS 的 delay
10 uS / 1.44198 uS = 6.934 大約等於 7
所以 7 delay ,就會得到 10 uS 的延遲時間。
要啟動 HC-SR04 開始量測,需要在 Trig 腳位,送上 10 uS 的 High 訊號,程式碼如下
: trig ( --) \ trigger HCSR04 to work
trigPin ->High
7 delay \ 10us/1.44198us = 6.934 ~ 7
trigPin ->Low
;
Trigger HC-SR04 開始工作後,接下來 Echo 腳位的回傳 High,表示8個 40kHz 的超聲波已經傳送出去囉,等待接收反射回來的超聲波訊號。
: wait ( --) \ wait echo signal to high
begin echoPin Pin@ until
;
很間單,begin-until 迴圈讀 echoPin 直到它不為零. (0=Low, 其他為High)
EchoPin為 High 的時候,就要開始計時囉。因為 EchoPin 為 Low 的時候代表接收到回傳回來的脈波。所以 High 所維持的時間,就是超聲波傳送到待側物,然後反彈回來的旅行時間。
計時的程式碼如下
: echo@ ( -- width) \ measure echo pulse width, 0 = failed
$ffff
for echoPin Pin@ 0= \ per loop time = 747 ms/65535 = 11.3985 us
if $ffff r> - exit then
next 0
;
用個 0xFFFF (65535) 的最大總數的 for-next 迴圈來計時。
首先讀取 echoPin ,來看說是不是已經 Low 了。如果不是 Low,就繼續 for-loop 計數。最後,假如所有 65535 都數完了,都還沒有 Low, 那肯定是有問題的囉。放個 0 回傳,表示有問題。
假如遇到 echoPin 變成 Low 了,表示有收到回傳波囉。 然後這個 for-next 的計數數目,就是這個 EchoPin 脈波的寬度跟時間囉。
for-next 不是很標準 ANSI FORTH 的指令,然後一般來說 for-next 的計數器應該都是從 0 開始計數的。 Flash FORTH 為了速度,結果它的 for-next 是倒數的。 😅
例如 10 for next 的結果,會是 9, 8, 7, ..., 0 。所以要得到目前的計數數,還得用原來的總數再減一次才會是正確的數字。
所以當 echoPin 變成 Low 了,表示有收到回傳波囉。先 r> 得到目前倒數的數目,然後用 $ffff (65535) 去減,才會得到正確的計數數目。 r> 順便把 for-next 在返回堆疊所建立的結構拆掉清乾淨 ,然後 exit 的強迫停止執行並跳出這個指令。
就這樣, echoPin High 的寬度就量出來囉。但這是 for-next 執行的計數,不是真實的時間。要想辦法化成真正的時間。
所以,定義 tt 來測試一下
: tt \ test the loop running time on unused Pin5
Pin5 <INPUT
Pin5 PULLUP ->High
ticks
$ffff \ $ffff full loop will take 747ms on 16Mhz Uno
for Pin5 Pin@ 0=
if $ffff r> - exit then
next
ticks swap - . cr
;
tt 會開啟 Pin5,接上內建上拉電阻,所以永遠是 High。
然後一模一樣的迴圈程式碼,測試一下執行 0xFFFF (65535) 下所需要花費的時間為多少。
經過測量後,會是 747 mS
所以一個 for-next 迴圈所花費的時間等於 747 mS / 65535 = 11.3985 uS
例如當執行 echo@,結果得到 143 這樣數值的迴圈執行數時,這代表 echo 脈波 High 的寬度為 143 * 11.3985 uS = 1629.9885 uS = 1.63 mS.
所以觸發一次完整的超音波距離量測,程式碼
: hcsr04@ ( -- width)
trig wait echo@
;
就 trig 產生 10uS 的觸發脈波, wait 等待 echo pin腳回傳 High, echo@ 將量測到的,脈波 High 的寬度給傳回來。
接下來要算距離囉,大家朗朗上口的,聲音的速度 speed = 331 + 0.6*T (攝氏溫度) m/sec
現在是夏天,所以室內溫度 30 C 很正常, speed = 331 + 0.6*30 = 349 m/sec
距離/速度 = 時間。再來,聲音一來一回,所以跑了兩倍的距離。所以 真實的距離(公尺) = 1/2 * 速度 * 時間 = 1/2 * 349 m/sec * 時間(秒)
我們 hcsr04@ 量到的是 for-next 的 cycle 數。又,我們剛剛有量過了,1個 for-next 的 cycle 花費 11.3985 uS
所以 時間(秒) = cycle * 11.3985E-6
全部合起來就是, 真實的距離(公釐) = 1/2 * 349 * cycle * 11.3985E-6 *1.0E3 = 1.98903845 cycle
我們把 1.9890 設為一個比例常數,所以
dist (mm) = cycle * 19890 / 10000
19890 constant ratio
: >dist ( width -- mm )
ratio 10000 u*/mod nip
;
>dist 給定 cycle 數後,利用比例算數 u*/mod 乘上 ratio 後除以 10000 得到距離的結果。
所以,完整的距離量測
: dist@ ( -- u)
hcsr04@ >dist
;
列印的指令,距離是 mm,但我們比較熟悉 cm。所以給它加上一個小數點就是囉。
: .[xxx.x] ( dist --)
0 <# # [char] . hold #s #> type
;
最後,連續的量測跟列印結果
: measure
init
begin ." dist = " dist@ .[xxx.x] ." cm" cr
again
;
2. 超音波溫度計
這時候,我們要倒過來算了。距離是以已知且固定不變的。利用量測聲音來回的時間算出此時聲音的速度,再利用這個聲音的速度來算出得知此時空氣分子的溫度囉。
公式推導,假設距離 L,超音波來回的時間為 t,空氣溫度為 T
speed = 2*L / t = 331 + 0.6 T
所以 T = 1/0.6 *(2*L/t - 331) = 5/3*(2*L/t -331)
L 用 1公尺,是非常合理的距離。所以
溫度 T = 5/3 * (2/t - 331) = 5/3 * (2/(cycle*11.3985E-6) - 331) = 5/3*(175461.68/cycle-331) = 292436.139/cycle - 551.667
取一位小數點的話
10*T = 2924361./cycle - 5517
這段化成程式碼為
: >temp ( width -- Celsius)
2924361. rot um/mod nip 5517 -
;
2924361 這個數字超過 65535 囉,所以用兩個 word 的雙整數來處理它。
FORTH 語言的規則,只要數字上面有小數點,就會被視為雙整數。 所以給它加上ㄧ點 2924361. 就會是佔兩個堆疊空間的雙整數囉。
rot 把 width (cycle) 翻進來, um/mod 是個雙整數除以單整數的混合除法指令。除完之後,nip 把餘數丟掉,再跟 5517 相減,這樣就完成時間轉成攝氏溫度的計算囉。
所以,完整的溫度量測
: temp@ ( -- Celsius)
hcsr04@ >temp
;
連續的溫度量測
: thermometer
init
temp@ ( init value)
begin 49 * temp@ + 50 u/ ( 50 points average)
." temperature = " dup .[xxx.x] ." C" cr
again
;
一開始測試的時候只量測單一值,可是發現因為距離一公尺,這個超音波量測比較不穩定,太敏感,變異太大了。
所以採用 50次的平均來降低誤差。
進 begin-again 迴圈前先 temp@ 量個初值,進迴圈之後每次的量測值都用 1/50 的加權平均的方式被合併進來總平均值。這樣就可以降低不穩定所帶來的誤差,整體會逐漸收斂到一個群體的平均值。
測試結果
1. 距離量測,
典型的 HC-SR04 的應用,超音波距離量測儀。測試運作OK
測試影片
2. 超音波溫度計,
拿一個溫度計在旁邊當校正源,適當調整發射距離(在一公尺附近)直到溫度跟校正源所顯示一致時就OK喔!
正常應該做個支架來固定,可以做個很酷的一公尺高的支架,然後讓感測器往地上發送超音波,這樣一定很酷啊,完全用聲音來量測空氣溫度的溫度計啊。放在客廳當擺飾,一定酷呆啦!有客人來問,就可以很臭屁地跟他說,哦,這個是靠發出人耳聽不到的聲音來量溫度的東東哦!
不過這裡只是簡單測試用,好玩就好啦!
測試影片
原始程式列表
\
\ Ultra Sonic Sensor HC-SR04
\ Frank Lin 2021.7.23
\
marker --uno_dio--
\
\ bit masks
\
%00000001 constant bit0
%00000010 constant bit1
%00000100 constant bit2
%00001000 constant bit3
%00010000 constant bit4
%00100000 constant bit5
%01000000 constant bit6
%10000000 constant bit7
\
\ Atmega328 Digital I/O Registers
\
#37 constant PortB
#40 constant PortC
#43 constant PortD
: Pin_Def ( mask Port "Name" --)
create swap c, c,
does> dup c@ ( mask)
swap char+ c@ ( Port)
;
\
\ Arduino Uno with ATmega328 Pin definations
\
flash
bit0 PortD Pin_Def Pin0
bit1 PortD Pin_Def Pin1
bit2 PortD Pin_Def Pin2
bit3 PortD Pin_Def Pin3
bit4 PortD Pin_Def Pin4
bit5 PortD Pin_Def Pin5
bit6 PortD Pin_Def Pin6
bit7 PortD Pin_Def Pin7
bit0 PortB Pin_Def Pin8
bit1 PortB Pin_Def Pin9
bit2 PortB Pin_Def Pin10
bit3 PortB Pin_Def Pin11
bit4 PortB Pin_Def Pin12
bit5 PortB Pin_Def Pin13
bit0 PortC Pin_Def Pin14
bit1 PortC Pin_Def Pin15
bit2 PortC Pin_Def Pin16
bit3 PortC Pin_Def Pin17
bit4 PortC Pin_Def Pin18
bit5 PortC Pin_Def Pin19
\
\ Digital I/O Access Codes
\
: >OUTPUT ( mask port --) \ set the direction of digital I/O to output
1- ( DDR register)
mset ( set to High = OUTPUT)
;
: <INPUT ( mask port --) \ set the direction of digital I/O to input
1- ( DDR register)
mclr ( set to Low = INPUT)
;
: PULLUP ; immediate ( --) \ dummy word
: ->High ( mask port --) \ put digital I/O to High
mset
;
: ->Low ( mask port --) \ put digital I/O to Low
mclr
;
: Pin@ ( mask port -- status) \ read the state of digital I/O, 0=Low
2- ( Pin register)
mtst ( read the state)
;
\
\ Ultra Sonic HC-SR04 Control
\
\
\ Pin2 = Trig
\ Pin3 = Echo
\
marker --sr04--
flash
Pin2 Pin_Def trigPin
Pin3 Pin_Def echoPin
: delay ( u --) for next ;
: t ticks $ffff delay ticks swap - . ;
\
\ after tested on my 16MHz Uno, $ffff delay will take
\ about 94.5 ms
\ 1 loop will take 94.5ms / 65535 = 1.44198us
\
: trig ( --) \ trigger HCSR04 to work
trigPin ->High
7 delay \ 10us/1.44198us = 6.934 ~ 7
trigPin ->Low
;
: wait ( --) \ wait echo signal to high
begin echoPin Pin@ until
;
: tt \ test the loop running time on unused Pin5
Pin5 <INPUT
Pin5 PULLUP ->High
ticks
$ffff \ $ffff full loop will take 747ms on 16Mhz Uno
for Pin5 Pin@ 0=
if $ffff r> - exit then
next
ticks swap - . cr
;
: echo@ ( -- width) \ measure echo pulse width, 0 = failed
$ffff
for echoPin Pin@ 0= \ per loop time = 747 ms/65535 = 11.3985 us
if $ffff r> - exit then
next 0
;
\
\ speed of sound is 0.0349 meters/ms @ 30C
\ 34.9 mm/ms
\ dist = width * 0.0113985 * 34.9 / 2 = width * 0.198904
\
19890 constant ratio
: >dist ( width -- mm )
ratio 10000 u*/mod nip
;
: init
trigPin >OUTPUT
echoPin <INPUT
;
: hcsr04@ ( -- width)
trig wait echo@
;
: dist@ ( -- u)
hcsr04@ >dist
;
: .[xxx.x] ( dist --)
0 <# # [char] . hold #s #> type
;
: measure
init
begin ." dist = " dist@ .[xxx.x] ." cm" cr
again
;
\
\ Ultra Sonic Thermometer
\
\
\ 2L/speed = t, speed = 331 + 0.6 T
\ 2L/t = speed = 331 + 0.6 T
\
\ So, T = 1/0.6* (2L/t -331) = 5/3*(2L/t-331)
\ if L = 1 meter
\ T = 5/3*(2/t-331)
\
\ 1 cycle on loop = 11.3985 uS
\ So,
\ T = 5/3*(2/(c*11.3985E-6)-331)
\ = 5/3*(175461.683/c-331)
\ = 292436.139/c - 551.666
\
\ 10*T =2924361/c - 5517
\
: >temp ( width -- Celsius)
2924361. rot um/mod nip 5517 -
;
: temp@ ( -- Celsius)
hcsr04@ >temp
;
: thermometer
init
temp@ ( init value)
begin 49 * temp@ + 50 u/ ( 50 points average)
." temperature = " dup .[xxx.x] ." C" cr
again
;