溫溼度感測是目前很常見的需求及技術,而 DHT11 是相當容易控制及撰寫的溫溼度感測器, DHT11 為數位信號輸出溫濕度複合感測器,包括一個電阻式感濕元件和一個NTC測溫元件,為 4 Pin*2.54的DIP封裝,使用及安裝相當方便。
該實例以STM32L031F6Px進行開發,配合STM32CubeMX及Keil 5,使用HAL函式庫。關於感測器更詳細的內容及資料可以參考資料手冊:AOSONG DHT11 Datasheet
如上圖及資料手冊所示, DHT11 僅透過Pin2─DATA進行感測資料的存取,一共有40個bits,分別是:濕度整數、濕度小數、溫度整數、溫度小數、資料檢驗碼,各8個bits。在資料手冊第5頁提及將前32個bits相加若等於檢驗碼,則是正確資料,若不是則放棄該筆資料,重新存取即可。
上圖可以看到整體的存取時序,且將時序用黑色及灰色呈現,分別是由MCU輸出給DHT11及MCU讀取 DHT11 的輸出訊號,因此在稍後的接腳設定在輸出輸入皆可,因實際上仍是由軟體進行切換控制。整體時序可以看到有一個起始訊號由MCU送給 DHT11 (Host the start of signal),接著Pull-up等待 DHT11 回應給MCU,收到Ready訊號後,則由 DHT11 輸出給MCU,透過輸出的High與Low時間長短表示資料”0″或是資料”1″。
在資料手冊中分為5個步驟,依序來了解。
1. 電源啟動需等待1秒,並有提升電阻於DATA Pin。
2. 須由MCU輸出至少18ms的Low訊號為起始訊號至 DHT11
3. DHT11 收到起始訊號後,會輸出80us的Low與80us的High訊號至MCU,表示為Ready
4. 存取溫溼度資料,起始皆為50us的Low,若接著是26~28us的HIGH則代表Data”0″,若為70us則是Data”1″,連續存取40個Bits。
STM32CubeMX專案設定
透過上述的資料手冊介紹,可以了解到需要1隻GPIO與 DHT11 溝通及1個TIMER進行時間的量測,同時使用USART列印至PC端觀看。因此CubeMX的設定如下,PA0為GPIO Out,不須做內部的設定,因為該腳會輸出輸入切換,實際由軟體進行控制,而TIM2為計數的TIMER,除頻為31,Period為65535(最大計數量),因為Clock的設定為32Mhz,因此TIMER2以1us的周期進行計數最符合DHT11的需求。
Keil
首先創立一個.c檔及.h檔,名稱皆為 dht11 ,用於撰寫 DHT11 的存取函式。
在.h檔先建立一個 DHT11 的結構體為 dht11_device,在後續可以用於存取所宣告的 DHT11 資料及TIMER設定。
※ .h檔記得引用”stm32l0xx.h”及”main.h”
struct dht11{
TIM_HandleTypeDef *htim;
GPIO_TypeDef* port;
uint16_t pin;
float temperature;
uint8_t humidty;
};
typedef struct dht11 dht11_device;
接著在.c檔先寫該結構體的初始化設定副程式。
將所要設定的 dht11_deivce 傳入,並將該結構體中所定義的TIMER、GPIO PORT、GPIO PIN與當初在Cube設定的傳入。
void dht11_init(dht11_device *dht, TIM_HandleTypeDef *htim, GPIO_TypeDef* port, uint16_t pin){
dht->htim = htim;
dht->port = port;
dht->pin = pin;
}
在前方有提及DATA Pin會切換輸出及輸入,並需要Pull-up的設定,因此直接寫一個針對GPIO Mode設定的副程式,當傳入的Mode為1,則設定 dht11_device 傳入結構體中的DATA Pin為輸出,反之若為0,則是輸入。
void set_pin_mode(dht11_device *dht, uint8_t Mode)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(Mode) //OUTPUT
{
GPIO_InitStruct.Pin = dht->pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(dht->port, &GPIO_InitStruct);
}else //INPUT
{
GPIO_InitStruct.Pin = dht->pin;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(dht->port, &GPIO_InitStruct);
}
}
接著以防 DHT11 死機,因此透過TIMER計數,若200us都沒有反應,則回傳0,致能所有中斷,以避免因為 DHT11 存取時的問題導致程式皆卡死。
uint8_t dht11_timeout(dht11_device *dht)
{
if((uint16_t)dht->htim->Instance->CNT > 200){
__enable_irq();
return 0;
}
return 1;
}
最後則是資料存取的函式為uint8_t get_dht11_data(dht11_device *dht)。
變數描述如下:
t1、t2用來計算訊號的時間、Bit為讀取到的”0″ or “1”
humVal、tempVal為濕度、溫度的整數資料
temp_float為溫度的小數資料(濕度在此沒有存取小數)
buffer[40] 為40個Bits的完整資料。
在此因小數的計算使用Pow函式,因此.c檔最前方需引入math.h。
依序為Start Signal、Ready Signal(80us Low、80us High)、資料存取。
※ 在此無做資料驗證判斷及實際的溫溼度校準,雖DHT11出廠已有初步校正,實際仍需透過精準的儀器比較後針對DHT11撰寫演算法或以線性回歸方式校正數值。
uint8_t get_dht11_data(dht11_device *dht)
{
uint16_t t1 = 0, t2 = 0, Bit = 0;
uint8_t humVal = 0, tempVal = 0;
float temp_float = 0;
uint8_t buffer[40];
//Start Signal
set_pin_mode(dht, 1);
dht->port->ODR&=~(1<<(dht->pin));
HAL_Delay(18);
dht->port->ODR|=(1<<(dht->pin));
__disable_irq(); //disable all interupts to do only read dht otherwise miss timer
HAL_TIM_Base_Start(dht->htim); //start timer
//set pin mode to input & check the DATA pin is high
set_pin_mode(dht, 0);
dht->htim->Instance->CNT=0;
while(HAL_GPIO_ReadPin(dht->port, dht->pin) == 1){
if(!dht11_timeout(dht))return 0;
}
//80us of low
dht->htim->Instance->CNT=0;
while(HAL_GPIO_ReadPin(dht->port, dht->pin) == 0){
if(!dht11_timeout(dht))return 0;
}
t1 = (uint16_t)dht->htim->Instance->CNT;
//80us of high
dht->htim->Instance->CNT=0;
while(HAL_GPIO_ReadPin(dht->port, dht->pin) == 1){
if(!dht11_timeout(dht))return 0;
}
t2 = dht->htim->Instance->CNT;
//if something wrong,the answer is not 80us.
if(t1 < 75 && t1 > 85 && t2 < 75 && t2 > 85)
{
__enable_irq();
return 0;
}
//Get Data
for(int j = 0; j < 40; j++)
{
dht->htim->Instance->CNT=0;
while(HAL_GPIO_ReadPin(dht->port, dht->pin) == 0){
if(!dht11_timeout(dht))return 0;
}
dht->htim->Instance->CNT=0;
while(HAL_GPIO_ReadPin(dht->port, dht->pin) == 1){
if(!dht11_timeout(dht))return 0;
}
t1 = dht->htim->Instance->CNT;
//if pass time 25us set as LOW
if(t1 > 20 && t1 < 30)
{
Bit = 0;
}
//if pass time 70us set as HIGH
else if(t1 > 60 && t1 < 80)
{
Bit = 1;
}
buffer[j] = Bit;
}
HAL_TIM_Base_Stop(dht->htim); //stop timer
__enable_irq(); //enable all interrupts
//get hum value from data buffer
for(int i = 0; i < 8; i++)
{
humVal += buffer[i];
humVal = humVal << 1;
}
//get temp value from data buffer
for(int i = 16; i < 24; i++)
{
tempVal += buffer[i];
tempVal = tempVal << 1;
}
for(int i = 24; i < 30; i++)
{
temp_float +=(float)(buffer[i]*pow(2,-8+(i-24)));
}
humVal = humVal >> 1;
tempVal = tempVal >> 1;
dht->temperature =(float)(temp_float);
dht->temperature+=tempVal;
dht->humidty=humVal;
return 1;
}
完成後須到.h檔完成副程式的宣告如下
void dht11_init(dht11_device *dht, TIM_HandleTypeDef *htim, GPIO_TypeDef* port, uint16_t pin);
void set_pin_mode(dht11_device *dht, uint8_t Mode);
uint8_t dht11_timeout(dht11_device *dht);
uint8_t get_dht11_data(dht11_device *dht);
main.c
在while前方需先宣告dht11_device及初始化結構體。
dht11_device dht;
dht11_init(&dht, &htim2, GPIOA, 1<<0);
在while當中get_dht11_data進行溫溼度資料的存取。
使用全域變數tempData、humData存取則可以在Debug模式透過Watch視窗查看變數變化,並以1000ms的頻率進行資料採樣。
在此附上該DHT11的專案於Github:STM32L0_DHT11