前言
玩 Arduino 的,第一個會接觸到的溫度跟濕度感測器十之八九,大概是 DHT 系列的感測器。
這一系列的感測器,因為已經一堆人幫它寫好底層溝通的函式囉,所以真的是使用上超級簡單的。真的不誇張,五分鐘搞定。所以當初筆者也用同樣的方法,很快的開發了一個露點溫度濕度計。
但是,現在玩 ESP32FORTH 這種少數人口在玩的語言,自然就沒這麼好命囉,這種特殊協定,可是沒人會理你的。雖然說 ESP32FORTH 是以 C語言開發的,所以要把別人以C語言所開發的DHT系列底層的溝通函式「黏」進來 FORTH 系統裡面並不困難。
可是啊,其實從頭打造起,來建構程式碼來跟 DHT 系列很特殊的 1-wire 通訊協定來溝通,老實說也不是非常困難的事情啊。讓我們來練練功,根據規格書來享受一下實作一些特殊通訊協定的樂趣吧。把他當飯後的消遣,就像玩數獨一樣啊! 😬
所以來用 ESP32FORTH 實作一下 DHT11, DHT22 這兩個 DHT 系列的溫度濕度雙感測器的溝通讀取吧。
DHT11 溫度濕度感測器
關於 DHT11,開門見山,一切從這份規格書談起!
DHT系列,這種感測器以往我們都稱呼它為 Smart I/O ,因為它本身已經內建 MCU 了,所以類比的溫濕度感測器的控制就不用我們來操心。只要透過適當的觸發,立即就是透過它們特殊的 1-wire 介面,將溫度跟濕度的數值以「數位」的方式回報給我們的程式中。
所以只要照規格書,把這個硬體層的 1-wire 通訊協定給實作出來,再根據數據的解碼格式解出資料,這樣就完全OK囉!
DHT11 跟 DHT22 是同一系列的溫度濕度感測器。而 DHT11 是 DHT22 的廉價版本,規格跟精確度差 DHT22 蠻多的。個人的經驗,是極不建議買 DHT11 的,因為真的很不準確。之前為了製作露點溫度濕度計,一開始貪便宜,買了4, 5顆 DHT11。可是很驚訝地發現,這麼多顆,每一顆讀出來的讀值都有很大的差異,真是不曉得哪一顆才是正確的。然後是濕度的讀值,跟家裡的濕度計誤差超過40%以上。真是讓人信心全無, DHT11 從此列入拒絕往來戶。而改用 DHT22 後,所有準確度問題迎刃而解。果然,一分錢一分貨,貴,還是品質的保證。
DHT11 溫度 Sensor
先來看 DHT11 的規格
量測範圍 | 準確度 | 數值解析度 | 反應時間 | |
溫度 | 0 - 50 C | +/- 2C | 1C | 6s - 30s |
濕度 | 20 - 90% RH | +/- 5% RH | 1% | 6s - 15s |
就說嘛, +/-2C 跟 +/-5%RH 的準確度,很爛的規格厚! (跟玩具差不多...) 😅
硬體接線, DHT系列特殊的 1-wire 接法如下面所示
主要三個 Pin, 電源跟接地兩個 Pin. 另外一個是 Data 資料接腳。 ESP32 用 digital I/O (DIO) 接上這個資料接腳,同時必須接上一個 5K 的上拉電阻。
這種 1-wire 的戲法,看久了其實都差不多。都是利用上拉電阻來達到雙向,多個 Slave 跟 Master 一起溝通的目的。要訣也很簡單,平常大家DIO都是在傾聽的狀態,這時候 DIO 必須是高阻抗。上拉電阻的結果,只要線上所有的人都是高阻抗的傾聽態下,自然電位是 3.3V (HIGH) 準位。
當某個人從高阻抗的傾聽態,轉變成輸出訊號的輸出態時, 當這個 DIO - HIGH (3.3V) 時候,大家都會還是看到 HIGH (3.3V) 。但是當這個 DIO 拉 LOW (0V) 時,電流流動,上拉電阻限流。這時候所有的人都看到 LOW。(其他的人都還是在高阻抗傾聽態)
所以只要維持永遠只有一位開啟 DIO 輸出,其他所有人都維持住高阻抗的傾聽態,這樣多方的交談就可以持續。
所以只要一條線,就可以進行多人的訊息交換。
硬體層的訊息溝通,我們在後面跟程式碼一起解說,這樣才會比較清楚。
DHT22 溫度濕度感測器
詳細的規格書在此啊, DHT22 也被稱為 AM2302。
來看 DHT22 的規格
量測範圍 | 重複性 | 數值解析準確度 | 反應時間 | |
溫度 | -40 - 80 C | +/- 0.2C | 0.1C | 2s |
濕度 | 20 - 90% RH | +/- 1% RH | 0.1%RH | 2s |
可以量到 -40C 喲,解析度,準確性,跟重複性也都不錯喲。 筆者實際使用後的經驗是很滿意的!(但是,價格也是不便宜的啦!😅 )
DHT11 跟 DHT22 的接線都是一樣的。硬體層的通訊格式也是一樣的。唯一的差異在資料編碼的格式有差異。一樣,細節後面跟程式碼一起搭配解說會比較清楚些。
硬體接線
因為買到的已經是模組囉,所以連上拉電阻都幫你弄好囉。只要簡單的接線就好。
這裡很簡單的把 ESP32 的 Pin14 接到 DHT 的 Data Pin。
Vcc/GND 接一接,這樣就搞定惹。超級簡單的吧!
FORTH 程式碼解說
DHT的 1-wire 硬體層通訊協定
如下所示,分兩大部分
(1) MCU Start 訊號:
首先, MCU 將 DIO 切成輸出狀態,將訊號線拉 LOW 製造 Start 訊號,要求正在傾聽的 DHT 裝置,請傳送資料給我。傳完 Start 訊號後 MCU 立刻將 DIO 切回高阻抗的傾聽狀態,避免干擾訊號線後續其他人的訊號傳輸。
(2) DHT 傳送資料:
DHT 傾聽到 Start 訊號,確認訊號符合規定也接受了,立刻將 DIO 從傾聽的高阻抗狀態轉變成輸出狀態。透過協議好的訊號規則,依序的將訊號線拉High 跟 拉 Low,傳送後續的交握確認跟 40 bits (5 bytes) 的資料。 MCU 傾聽訊號線的訊號,依序的將這些訊號根據協議好的規則重組回濕度跟溫度的資訊。
(1) MCU 的 Start 訊號
如下圖所示,
很簡單,當 MCU 開啟 DIO 輸出,將訊號線持續拉 LOW (0V) 至少 18 mS (毫秒)以上, 當DHT 傾聽到這樣的訊號,就會接受觸發,準備接續將 DIO 從高阻抗的傾聽態轉變成電壓的輸出態,開始輸出資料。
送完 Start 訊號後, MCU 必須把 DIO 放回 HIGH (5V) 約 20 - 40 uS (微秒),然後立刻將 DIO 切為高阻抗的傾聽態。開始傾聽由 DHT 所傳送過來的訊息。
這段程式碼如下,首先定義個 delay 指令,用來控制時間用。
: delay for next ;
這個指令利用迴圈來測試 (指令 t2),它一次 delay 所花費的時間會是 96.5ms / 1000000 = 0.0965 uS
製造 Start 訊號
: start! ( --)
DHTPin >OUTPUT
DHTPin ->Low
20 ms
DHTPin ->High
145 delay ( ~ 14uS)
DHTPin <INPUT
;
看圖說故事,先將 DHTPin 這個資料 DIO 腳位切成 OUTPUT 狀態(>OUTPUT)。 然後拉 Low,持續 20 mS 後將它拉 High,然後給它持續個 14uS 後 ( 145 delay) ,將 DIO 切回高阻抗的傾聽態 (<INPUT),準備傾聽從 DHT傳來的訊息。
(2) DHT 傳送資料:
DHT 傳送資料前的前導訊號
為了怕有問題,所以 DHT 不會立刻傳資料,而是會先傳送如上圖所示的前導訊號來交握。MCU 透過這個訊號(暗號)來確認是否是正在跟外星人交談勒,還是跟貨真價實的 DHT 來交談。如果是貨真價實的 DHT,這時候它會把訊號線拉 Low 約 80uS 後再拉 High 80uS。假如不是這樣,那肯定的現在的那個跟你傳送訊息的不是 DHT 本人囉! 🤷🏻♀️
先定義一個等待訊號線拉 High 的傾聽指令
: wait ( --) \ wait until pulse-high
begin DHTPin Pin@ until ;
就不斷傾聽 DHTPin ,直到訊號不為零 (Low)
然後就是完整的傾聽 DHT 送出所有資料的程式碼
: DHT@ ( -- n1 n2 n3 n4 CheckSum)
start!
207 delay ( ~ 20uS)
wait
850 delay ( ~ 82uS)
40bits@
;
先送 Start 訊號,然後我們先等個 20uS ( 207 delay) ,確定已經進到 DHT 傳送 80uS 的前導訊號 Low 之中後,利用 wait 等待前導訊號進入 80uS 的 High 後。精準的再等待 82uS (850 delay) 讓訊號進入 DHT 傳送 40bits 的資料傳送階段,再交由 40bits@ 接力,將 5 bytes ( 40bits) 的濕度跟溫度的資料逐一讀出。
DHT 送完前導訊號後,接下來就是要傳送 5 個 bytes 的資料。5個 bytes 的資料總共 40 bits。這五個 bytes 的資料,前面兩個 bytes 是濕度的資料,接續兩個 bytes 是溫度的資料。最後一個 byte 是 CheckSum 查驗檢查碼,用來確認這4個 bytes 的資料有無錯誤。但要注意的, 雖然硬體層同樣都是傳送 5個 bytes,但 DHT11 跟 DHT22 的濕度,溫度資料編碼並不相同。所以接收完畢後要用不同的編碼方式還原資料。
每一 byte 的資料總共 8 bits,其硬體層傳送的訊息格式如下
因為資料線只有一根線,所以利用脈波High的長短來表示 0 跟 1 bit 位元訊號。 脈波比較短的就是 0,脈波比較長的就是1。
短脈波,表示 0
首先會先拉 Low 長度 50 uS 的 Start 訊號,來告知並同步資料 bit 要傳送了。當接續的 High 訊號是 26 - 28 uS 的短訊號時,代表這個 bit 是 0
長脈波,表示 1
首先會先拉 Low 長度 50 uS 的 Start 訊號,來告知並同步資料 bit 要傳送了。當接續的 High 訊號是 70 uS 的長訊號時,代表這個 bit 是 1
所以,判斷單一 bit 脈波長短的程式碼
: signal@ ( -- true=1/false=0)
174 ( ~112uS)
for
DHTPin Pin@ 0= ( pulse low?)
if r> 104 ( ~ 67.175uS) < exit ( length > 70 = 44.825 uS)
then
next
." Error! Signal not match with expectation!" cr
abort
;
用 175 次的迴圈來判斷跟量測 High訊號的長度。
要注意的,這個迴圈每次的執行時間有量測過 (利用 t1指令),每次會花費 642ms / 1000000 = 0.642 uS。
所以 175次 起碼會跑 112 uS (=175*0.642 uS) 遠超過 70uS 的長訊號。所以當迴圈執行完畢,還沒看到 Low 訊號的出現時。這肯定是有問題囉,顯示訊號與預期不一致的錯誤訊息並停止執行(Abort)。
而迴圈裡面 DHTPin Pin@ 0= 來判斷是否訊號已經變成 Low 囉,如果是 Low 就根據長度來判斷是 0 還是 1 囉。
關鍵在迴圈的計數, r> 會把迴圈目前的數字取回。 for-next 是倒數的。所以當這個迴圈的數目小於 104 時,代表迴圈至少執行了 175 - 104 = 71次。71次的執行時間是 71*0.642 = 45.582 uS > 28 uS 的短脈波時間。
所以 當發現已經收到脈波從High變成Low囉,此時 r> 104 < 成立的話就是長脈波,代表 1 ; 否則就是短脈波,代表0。 得到長短脈波 bit 訊號的最終結果後 exit 跳出指令執行。
整個 8 bits (1 byte) 的資料讀取
: 8bits@ ( -- Data)
0 ( data)
7 for
wait
signal@ if 1 r@ lshift or then
next
;
用個 8次的 for-next 迴圈。每次先用 wait 來等待前導訊號 Low 的結束,轉到 High 的瞬間交給 signal@ 把這個接續的 High 脈波長度判斷出來,看是0還是1。
DHT的資料有說明,它們是高位元先傳送的。剛好我們的 for-next 是倒數的,所以依序計數是 7, 6, 5 ... 0。所以當判斷出來是1的時候,就把 1 左移 for-next 當時計數的位數,利用 or 把這位元的資料合併進去 data 裡面。依序完成8次,這個 byte 的資料就解出來囉。
因為總共有 5 bytes,所以
: 40bits@ ( -- n1 n2 n3 n4 n5)
4 for 8bits@ next
;
依序接收並解碼 5 bytes 的資料逐一放入堆疊中,完成。
所以執行 DHT@ 指令就可以從 1-wire 中要求 DHT 傳送並接收協議好的,5 bytes 的資料。
至此,硬體層資料溝通跟讀取就完成囉。剩下如何解讀這 5 bytes 的資料。
順便送上示波器所量測到的照片,可以很清楚地看到,一堆短長脈波所組合成的訊號。長脈波,代表 1 ; 短脈波,代表0。
DHT11 的濕度,溫度資料格式
接下來就是這5個 Bytes 資料的解碼囉。前面有提到了, DHT11 跟 DHT22 硬體層的傳送協定是一樣的,最終都會送5個 bytes 出來。但這5個 bytes 的「意思」不一樣。
DHT11 的格式是: [濕度整數位數字] [濕度小數位數字] [溫度整數位數字] [溫度小數位數字] [CheckSum檢查碼] 共五個數字
其中 [CheckSum檢查碼] = [濕度整數位數字] + [濕度小數位數字] + [溫度整數位數字] + [溫度小數位數字],用來查核前面四個數字有無傳輸錯誤。如果不一致,那就代表傳送不穩定,有資訊於傳送過程中丟失囉。
所以,當接收到 67 0 31 0 98 這五組數字,代表
濕度: 67.00 RH%
溫度: 31.00 C
CheckSum = 67 + 0 + 31 + 0 = 98 和第五個數字一致,所以傳輸成功。
前面也有提過, DHT11 的解析度只有 1% RH 跟 1C 的整數解析度的,所以第二個代表濕度的小數位的數字跟第四個代表溫度的小數位的數字,將永遠為零。
所以,解碼的程式碼
: >DHT11 ( RHint RHdec Tint Tdec Checksum -- RH Temp)
nip over - >r ( RHint RHdec Tint | R: checksum')
nip over r> -
abort" Error: CheckSum not match!!"
;
Tdec 是溫度小數位,直接 nip 丟掉,留下整數位的溫度 Tint。 然後 CheckSum 跟 Tint 的整數位相減。
RHdec 是濕度小數位,直接 nip 丟掉,留下整數位的濕度 RHint。然後 CheckSum 繼續跟 RHint 的整數位相減。
這時候如果傳輸都正確的話,經過兩次相減 CheckSum 應該變成零囉,如果不是零代表傳輸有錯誤,發出錯誤訊息並 Abort 指令的執行。
DHT22 的濕度,溫度資料格式
接下來是 DHT22 的資料格式,比較複雜些。所以直接用規格書的範例來解說囉。
一樣是5個 Bytes: [RH-MSB] [RH-LSB] [T-MSB] [T-LSB] [CheckSum]
CheckSum 依舊為 [CheckSum] = [RH-MSB] + [RH-LSB] + [T-MSB] + [T-LSB] ,用來查核傳送過程中有無錯誤發生。
不過要留意的喲, CheckSum 只有 1 byte,所以最多只到 255 喲,超過 255 進位的位元資料是會被捨去的。
濕度 = 1/10 * [RH-MSB]*256 + [RH-LSB] ,就是兩個 byte 形成一個 16 bits 的單整數啦,然後一位隱形的小數點。
所以 [0000 0010] [1000 1100] --> 652 意思為 65.2 % RH 的相對濕度
溫度 = 1/10 * [T-MSB]*256 + [T-LSB] ,就是兩個 byte 形成一個 16 bits 的單整數啦,然後一位隱形的小數點。
但是,但是, DHT22 可以感測到 -40 - 0 之間的零下溫度喲。所以 MSB 的最左邊那個 bit 代表正負號。所以當是負的溫度時,要把最左的那個 bit 先弄掉,這樣轉出來的整數才會是對的。
所以 [1000 0000] [0110 0101] = - [0000 0000] [0110 0101] = -101 --> -10.1 C 的溫度
所以,解碼的程式碼
: >DHT22 ( RH.H RH.L T.H T.L Checksum -- RH Temp)
>r 2dup + >r
swap 8 lshift or >r ( RH.H RH.L R: T CS1 CS)
2dup + >r ( RH.H RH.L R: CS2 T CS1 CS)
swap 8 lshift or ( RH R: CS2 T CS1 CS)
r> r> swap ( RH T CS2 R: CS1 CS)
r> + ( RH T CS3 R: CS)
256 u/mod drop
r> <>
abort" Error: CheckSum not match!!"
$7fff over and swap ?negate
;
這段程式碼比較多一些堆疊操作,可讀性比較低些。這時候 FORTH 程式設計員常用的技巧就是放上大量的堆疊前後變化的註解。透過這些註解後,其實可讀性也沒那麼糟,日後還是非常容易維護你的程式碼。
就逐步的將四個 bytes 的數字加起來,用來查驗 CheckSum 用。中間會透過 >r 暫時,暫存到返回堆疊。需要的時候再 r> 從返回堆疊搬回來運算。
CheckSum 最後階段都加好,計算好囉, 256 u/mod drop 來讓它不要超過 256,才來好跟收到的 CheckSum 相比較,不一致的話就發出錯誤訊息並 Abort 指令的執行。
8 lshift or 則是將 [MSB] 往左移八位元後跟 [LSB] 用 or 運算進行合併成一個單整數。
最後一個重點是,因為溫度可能有負的,這時候單整數的最左邊 bit 會是 1。所以 $7fff over and 將這個礙眼的 bit 去掉。 當這個 bit 是 1 的時候,2的補數系統會認為是負數。所以 swap ?negate ,當這個數字是負數的時候,把剛剛數字正確的正整數變成正確2的補數的「負整數」,這樣就完工啦。
所以,完整 DHT11 取值程式碼
: DHT11@ ( -- RH T)
DHT@ >DHT11
10 * >r 10 * r>
;
DHT@ 來跟 DHT感測器透過 1-wire 通訊要資料,然後將資料轉成正確濕度跟溫度順便檢查 CheckSum 有無錯誤。然後為了跟後續 DHT22 格式一致,將溫濕度都乘上10倍。(一位小數的意思)
完整 DHT22 取值程式碼
: DHT22@ ( -- RH T)
DHT@ >DHT22
;
DHT@ 來跟 DHT感測器透過 1-wire 通訊要資料,然後將資料轉成正確濕度跟溫度順便檢查 CheckSum 有無錯誤。
列印有帶正負號,一位小數的整數
: .[xx.x] dup <# # [char] . hold #s swap sign #> type ;
要注意的, ESP32FORTH 是 32位元的系統。 1 cell 的整數是 32位元的,所以 ESP32FORTH 這裡 <# 有點不合標準,不是採用 2cell 的雙整數。所以無需單整數轉雙整數 s>d
最後,不斷取值列印。
: DHT11
cr
begin
DHT11@
." Temperature = " .[xx.x] ." C , "
." Relative Humidity = " .[xx.x] ." %" cr
2000 ms
again
;
: DHT22
cr
begin
DHT22@
." Temperature = " .[xx.x] ." C , "
." Relative Humidity = " .[xx.x] ." %" cr
2000 ms
again
;
執行結果
筆者的三顆 DHT11 跑出來的結果,溫度是 15度,濕度是 18% 這種很離譜的值。應該都壞掉囉。
但 DHT22 就很棒,很穩定的,夏季悶熱的夜晚 31.6C 跟相對濕度 62% 左右。
然後只要故意一拔掉感測器,立刻因為脈波不對而 Abort 掉。真的是運作完美! 😭
最後,完整原始程式碼列表
\
\ DHT11, DHT22 1-wire Control Code - ESP32FORTH
\ Frank Lin 2022.7.20
\
\
\ Digital I/O Access Codes
\
: >OUTPUT ( pin --) \ set the direction of digital I/O to output
output pinMode
;
: <INPUT ( pin --) \ set the direction of digital I/O to input
input pinMode
;
: PULLUP ; immediate \ dummy for syntax sweeter
: ->High ( pin --) \ put digital I/O to High
high digitalWrite
;
: ->Low ( pin --) \ put digital I/O to Low
low digitalWrite
;
: Pin@ ( pin -- status) \ read the state of digital I/O, 0=low, 1=high
digitalRead
;
: ticks ( -- ticks)
ms-ticks
;
\
\ extension for ESP32FORTH
\ ESP32FORTH only has catch/throw, no standard ABORT, ABORT"
\
: abort ( --) -1 throw ;
: abort" ( flag "text" --)
state @
if
postpone if postpone s" postpone type postpone cr
postpone abort
postpone then
else [char] " parse type cr abort then
; immediate
\
\ DHT Sensor, 1-wire data Pin
\
14 constant DHTPin \ Pin14 as DHT data Pin
: delay ( n--) for next ; \ used as the delay timer
\
\ DIO and delay speed test
\
: t1 ticks DHTPin 1000000 for DHTPin Pin@ 0= if then next drop ticks swap - . ;
\
\ result: 642 ms / 1000000 = 0.642 uS per loop
\
: t2 ticks 1000000 delay ticks swap - . ;
\
\ result: 96.5 ms / 1000000 = 0.0965 uS for 1 delay
\
: wait ( --) \ wait until pulse-high
begin DHTPin Pin@ until
;
\
\ DHT 1-wire signal:
\ start: 50uS Low
\ signal 1: 70 uS Pulse High
\ signal 0: 26 - 28 uS Pulse High
\
\ 112uS / 0.642uS = 174
\ 67.175uS / 0.642uS = 104
\
: signal@ ( -- true=1/false=0)
174 ( ~112uS)
for
DHTPin Pin@ 0= ( pulse low?)
if r> 104 ( ~ 67.175uS) < exit ( length > 70 = 44.825 uS)
then
next
." Error! Signal not match with expectation!" cr
abort
;
: 8bits@ ( -- Data)
0 ( data)
7 for
wait
signal@ if 1 r@ lshift or then
next
;
: 40bits@ ( -- n1 n2 n3 n4 n5)
4 for 8bits@ next
;
\
\ Start Signal
\ 18mS Low, to active communication
\ then 20 - 40uS High
\ then wait DHT sends 80uS Low, 80uS High
\ then receive 40bits data transmition from DHT
\
\
\ 20uS = 20/0.0965 ~ 207 delay
\ 82uS = 82/0.0965 ~ 850 delay
\ 14uS = 14/0.0965 ~ 145 delay
\
: start! ( --)
DHTPin >OUTPUT
DHTPin ->Low
20 ms
DHTPin ->High
145 delay ( ~ 14uS)
DHTPin <INPUT
;
: DHT@ ( -- n1 n2 n3 n4 CheckSum)
start!
207 delay ( ~ 20uS)
wait
850 delay ( ~ 82uS)
40bits@
;
: >DHT11 ( RHint RHdec Tint Tdec Checksum -- RH Temp)
nip over - >r ( RHint RHdec Tint | R: checksum')
nip over r> -
abort" Error: CheckSum not match!!"
;
: ?negate ( n1 n2 -- n3)
$80 and if negate then
;
: >DHT22 ( RH.H RH.L T.H T.L Checksum -- RH Temp)
>r 2dup + >r
swap 8 lshift or >r ( RH.H RH.L R: T CS1 CS)
2dup + >r ( RH.H RH.L R: CS2 T CS1 CS)
swap 8 lshift or ( RH R: CS2 T CS1 CS)
r> r> swap ( RH T CS2 R: CS1 CS)
r> + ( RH T CS3 R: CS)
256 u/mod drop
r> <>
abort" Error: CheckSum not match!!"
$7fff over and swap ?negate
;
: DHT11@ ( -- RH T)
DHT@ >DHT11
10 * >r 10 * r>
;
: DHT22@ ( -- RH T)
DHT@ >DHT22
;
: .[xx.x] dup <# # [char] . hold #s swap sign #> type ;
: DHT11
cr
begin
DHT11@
." Temperature = " .[xx.x] ." C , "
." Relative Humidity = " .[xx.x] ." %" cr
2000 ms
again
;
: DHT22
cr
begin
DHT22@
." Temperature = " .[xx.x] ." C , "
." Relative Humidity = " .[xx.x] ." %" cr
2000 ms
again
;
xxx