本文我們利用樹莓派的GPIO口來跟數(shù)字濕溫度計模塊DHT11進行通信取得溫度和濕度數(shù)據(jù)并顯示在數(shù)碼管上,通過按鈕來切換顯示溫度或濕度。
最終效果
樹莓派GPIO入門06-跟數(shù)字濕溫度計DHT11通信
\
硬件
- 數(shù)碼管
- 杜邦線
- 面包板
- 按鈕1只
- 數(shù)字濕溫度計DHT11模塊。(我們這個教程里用到的所有電子元件均可在淘寶購買到)
硬件圖
原理說明
原始的DHT11模塊有4根引腳,長成這個樣子:
原始DHT11模塊
由圖可以看出4根引腳里除了VCC,GND,DATA以外,還有一個引腳是N/A,也就是不使用。(不使用引出來干嘛?好看嗎?不解)
本文使用的是又被封裝了一次的模塊,去掉了無用的引腳。其他3個引腳保留。功能完全一樣,所以如果你手頭上的DHT11是有4根引腳的請忽略N/A針腳,其他的跟我使用的這種完全通用。3個針腳分別連接到3.3v電源,GND和任意GPIO口上。根據(jù)數(shù)據(jù)手冊(文末提供下載)的說明,總線(DATA引腳)在空閑狀態(tài)需要保持高電平狀態(tài),所以我們除了將DATA引腳接到一個GPIO口上,還要通過一個4.7K(經實測2K左右的就夠了)的電阻將DATA引腳并聯(lián)到VCC上。這個電阻也稱上拉電阻,電阻就是一般的電阻,只是在這里起的作用是上拉電平的作用所以稱之為上拉電阻。
與DHT11通信時,發(fā)送和接收信息都在一根DATA口上,這種只用1根總線的數(shù)據(jù)傳輸方式稱為單總線模式。
向DHT11發(fā)送數(shù)據(jù)時,GPIO口需要設置為OUTPUT模式,從DHT11接收數(shù)據(jù)時GPIO口需要切換成INPUT模式。
具體通信的時序如下:
8bit濕度整數(shù)數(shù)據(jù) + 8bit濕度小數(shù)數(shù)據(jù) + 8bi溫度整數(shù)數(shù)據(jù) + 8bit溫度小數(shù)數(shù)據(jù) + 8bit校驗和。而校驗和數(shù)據(jù)應該等于“濕度整數(shù)數(shù)據(jù)+濕度小數(shù)數(shù)據(jù)+溫度整數(shù)數(shù)據(jù)+溫度小數(shù)數(shù)據(jù)”所得結果的末8位。
我們的程序就應該遵循上述時序來與DHT11進行數(shù)據(jù)通信。
硬件連接
下面的連接圖只標出了DHT11的連線和上拉電阻的連線方法。數(shù)碼管和按鈕的連線請參考上一篇。
硬件連接圖
關鍵代碼
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
void readDHT11() { int i,j,cnt = 0; for (j = 0; j < RETRY; ++j) { for (i = 0; i < 5; ++i) { data[i] = 0; } // GPIO口模式設置為輸出模式 pinMode (DATA, OUTPUT) ; // 先拉高DATA一段時間,準備發(fā)送開始指令 digitalWrite (DATA, HIGH); usleep(500000); // 500 ms // 拉低DATA口,輸出開始指令(至少持續(xù)18ms) digitalWrite (DATA, LOW); usleep(TIME_START); digitalWrite (DATA, HIGH); // 開始指令輸出完畢,切換到輸入模式,等待DHT11輸出信號。 // 由于有上拉電阻的存在,所以DATA口會維持高電平。 pinMode (DATA, INPUT); // 在DATA口被拉回至高電平通知DHT11主機已經準備好接受數(shù)據(jù)以后, // DHT11還會繼續(xù)等待20-40us左右以后才會開始發(fā)送反饋信號,所以我們把這段時間跳過去 // 如果長時間(1000us以上)沒有低電平的反饋表示有問題,結束程序 cnt=0; while (digitalRead(DATA) == HIGH) { cnt++; if (cnt > MAXCNT) { printf("DHT11未響應,請檢查連線是否正確,元件是否正常工作。\n"); exit(1); } } // 這個反饋響應信號的低電平會持續(xù)80us左右,但我們不需要精確計算這個時間 // 只要一直循環(huán)檢查DATA口的電平有沒有恢復成高電平即可 cnt=0; while (digitalRead(DATA) == LOW) { cnt++; if (cnt > MAXCNT) { printf("DHT11未響應,請檢查連線是否正確,元件是否正常工作。\n"); exit(1); } } // 這個持續(xù)了80us左右的低電平的反饋信號結束以后,DHT11又會將DATA口拉回高電平并再次持續(xù)80us左右 // 然后才會開始發(fā)送真正的數(shù)據(jù)。所以跟上面一樣,我們再做一個循環(huán)來檢測這一段高電平的結束。 cnt=0; while (digitalRead(DATA) == HIGH) { cnt++; if (cnt > MAXCNT) { printf("DHT11未響應,請檢查連線是否正確,元件是否正常工作。\n"); exit(1); } } // ##################### 40bit的數(shù)據(jù)傳輸開始 ###################### for (i = 0; i < 40; i++) { // 每一個bit的數(shù)據(jù)(0或者1)總是由一段持續(xù)50us的低電平信號開始 // 跟上面一樣我們用循環(huán)檢測的方式跳過這一段 while (digitalRead(DATA) == LOW) { } // 接下來的高電平持續(xù)的時間是判斷該bit是0還是1的關鍵。 // 根據(jù)DHT11的說明文檔,我們知道 這段高電平持續(xù)26us-28us左右的話表示這是數(shù)據(jù)0。 // 如果這段高電平持續(xù)時間為70us左右表示這是數(shù)據(jù)1。 // 方法1:在高電平開始的時候記下時間,在高電平結束的時候再記一個時間, // 通過計算兩個時間的間隔就能得知是數(shù)據(jù)0還是數(shù)據(jù)1了。 // 方法2:在高電平開始的以后我們延時40us,然后再次檢測DATA口: // (a) 如果此時DATA口是低電平,表示當前位的數(shù)據(jù)已經發(fā)送完并進入下一位數(shù)據(jù)的傳輸準備階段(低電平50us)了。 // 由于數(shù)據(jù)1的高電平持續(xù)時間是70us,所以如果是數(shù)據(jù)1,此時DATA口應該還是高電平才對, // 據(jù)此我們可以斷言剛才傳輸?shù)倪@一位數(shù)據(jù)是0。 // (b) 如果延時40us以后DATA口仍然是高電平,那么我們可以斷言這一位數(shù)據(jù)一定是1了,因為數(shù)據(jù)0只會持續(xù)26us。 // 方法3:循環(huán)檢測電平狀態(tài)并計數(shù),每檢查一次如果電平狀態(tài)沒變就讓計數(shù)器加一,一直到電平狀態(tài)變成低電平為止。 // 數(shù)據(jù)0的高電平持續(xù)時間短,所以計數(shù)一定比數(shù)據(jù)1的計數(shù)少,由于微秒級別的延時太短,這個計數(shù)會有一定誤差。 // 我們需要先在自己的樹莓派上用printf打印出每一位數(shù)據(jù)計數(shù)的結果,然后觀察計數(shù)結果來設定一個閾值, // 高于這個閾值的就認為的數(shù)據(jù)1,低于這個值的就認為是數(shù)據(jù)0。 // 我們這里采用簡單易行的方法3。 // 實際上,由于要求的時序太短,在樹莓派上很難通過方法1和方法2實現(xiàn)。這也是我使用c而不是python的原因。 // 特別是方法2,因為linux里有一個系統(tǒng)級的延時函數(shù)usleep,單位確實是微秒。 // 貌似用usleep(40)就可以了,實際測試的結果是延時遠遠超過40us。其原因是usleep這個函數(shù)的延時方法是 // 暫停當前進程并放開cpu權限讓cpu可以在這段時間里去處理其他任務。這樣做的好處是不會浪費cpu資源, // 但問題是當系統(tǒng)將cpu權限交還給我們的進程的這個過程本身就要耗費若干ms(毫秒哦) // 所以導致usleep這個函數(shù)實際上沒有辦法做到延時幾十微秒。 cnt=0; while (digitalRead(DATA) == HIGH) { // 當所有數(shù)據(jù)傳輸完以后,DHT11會放開總線,DATA口就會被上拉電阻一直拉高。 // 所以如果超過一定時間電平還沒有被拉低就表示所有的數(shù)據(jù)已經傳輸完畢,停止檢測。 cnt++; if (cnt > MAXCNT) { break; } } if (cnt > MAXCNT) { break; } // 將當前位的計數(shù)保存起來 bits[i] = cnt; } // 整理數(shù)據(jù),將位數(shù)據(jù)轉成5個數(shù)字 for (i = 0; i < 40; ++i) { data[i/8] <<= 1; if (bits[i] > VAL) { data[i/8] |= 1; } //下面這句話就是用來測試自己的設備應該設定多少閾值的測試代碼 //printf("bits[%d] = %d (%d) \n", i, bits[i], bits[i]>200?1:0 ); } // 往屏幕上輸出取得的數(shù)據(jù) for (i = 0; i < 5; ++i) { printf("data[%d] = %d \n", i, data[i] ); } // 用校驗和來檢查接收數(shù)據(jù)是否完整 if (data[4] == (data[0] + data[1] + data[2] + data[3]) & 0xFF ) { printf("校驗成功! \n"); // 將濕度,溫度數(shù)據(jù)賦值給顯示用的濕度,溫度變量 shidu = data[0]; wendu = data[2]; break; } else { printf("校驗不成功,重新取值! \n"); // 校驗不成功,重新取值,連續(xù)10次取值不成功就放棄。一般連線和邏輯正確的話連續(xù)10次取值出錯是不可能的。 continue; } }}
校驗和
下圖是實際運行時利用校驗和檢測到數(shù)據(jù)出現(xiàn)了接收錯誤的情況利用校驗和來檢查接收數(shù)據(jù)的正確性
評論