1. 概述
1.1 實驗目的
????????本實驗旨在利用 DHT11 溫濕度傳感器,每隔 5 秒采集一次環境的溫度與濕度數據,并通過串口將數據循環打印輸出。所使用的 DHT11 模塊硬件結構簡單,包含三個接口引腳:電源正極(VCC)、電源負極(GND)以及一根用于數據通信的單總線信號線。
? ? ? ? DHT11 采用的是一種單總線雙向串行通信協議傳輸。這種通信方式僅僅需要一個I/O 口,就能同時完成數據的發送和接收,在短距離、低速率的數據采集應用中具有成本低、接線簡潔的優勢,非常適合初學者進行嵌入式系統開發和溫濕度監測實驗。
1.2 DHT11指標介紹
指標類別 | DHT11 | DHT22 |
---|---|---|
溫度測量范圍 | 0°C ~ 50°C | -40°C ~ 80°C |
溫度精度 | ±2°C | ±0.5°C |
濕度測量范圍 | 20% ~ 90% RH | 0% ~ 100% RH |
濕度精度 | ±5% RH | ±2% RH |
響應時間 | ≈ 1 秒 | ≈ 0.5 秒 |
采樣間隔 | ≥ 1 秒(低頻) | ≥ 0.5 秒(更快) |
工作電壓 | 3V ~ 5V | 3.3V ~ 6V |
適用場景 | 常規室內監控,成本敏感應用 | 精度要求高、適用于室內外及極端環境 |
價格(通常) | 較便宜 | 相對較貴 |
1.3 硬件連接
?????????DATA接微控制器GPIO引腳,需通過4.7kΩ上拉電阻連接至VCC(確保總線空閑時為高電平),這種模塊已經做好了上拉電阻和工作指示燈的電路集成,只需把正負極通過杜邦線連接到單片機的正負極上,并把DATA 連接到單片機其中的一個GPIO引腳上即可,本文連接的引腳是PE0引腳。
? ? ? ? 如果買的不是模塊,僅僅是DHT11傳感器,是不帶上拉電阻的,而且一共有4個引腳,其中一個是NC(空腳),需要自己進行上拉電阻的焊接,方便在實際工程中使用,集成到已有的板子上。如果是學習或工程研發調試階段,建議買上面那種模塊化的。
1.4 DHT11數據結構?
原始數據 5字節(40bit)固定格式(高位優先輸出):
00101101 (Byte4) | 00000000 (Byte3) | 00011100 (Byte2) | 00000000 (Byte1) | 01001001 (Byte0)
字節序 | 字段名稱 | 數據類型 | 說明 |
---|---|---|---|
Byte4 | 濕度整數部分 | 8位無符號 | 范圍:0~99%RH |
Byte3 | 濕度小數部分 | 8位無符號 | 固定補零(實際分辨率1%RH) |
Byte2 | 溫度整數部分 | 8位有符號 | 范圍:-40~+80℃ |
Byte1 | 溫度小數部分 | 8位無符號 | 固定補零(實際分辨率1℃) |
Byte0 | 校驗和 | 8位無符號 | Byte4+Byte3+Byte2+Byte1 |
1.5 DHT11時序圖
????????主機發送開始信號后,延時等待 20us-40us 后讀取 DH11T 的回應信號,讀取總線為低電平,說明DHT11 發送響應信號,DHT11 發送響應信號后,再把總線拉高,準備發送數據,每一 bit 數據都以低電平開始,格式見下面圖示。如果讀取響應信號為高電平,則 DHT11 沒有響應,請檢查線路是否連接正常。?
1.5.1 請求數據時序圖? ? ? ??
????????上圖是一個總圖,描述了數據請求階段和數據接收階段,當為數據請求階段時,與HDT11的DATA口連接的單片機I/O口需要設置為推挽輸出模式,因為要寫數據。下圖為數據請求階段時序圖。
1.5.2 接收數據時序圖
????????當單片機向DHT11請求數據后在數據接收階段,需要把單片機I/O設置為浮空輸入模式,讀取數據時序圖如下所示:
數據位為 0 的時序圖
數據位為 1 的時序圖?
2. STM32CubeMX配置
2.1 SYS配置
本文使用JLink下載代碼,如果讀者用STLink同理,都是如下圖選擇
2.2 GPIO配置
2.3 USART1配置
選擇異步通信,波特率115200常用波特率,其他配置默認即可,影響不大,主要是為了打印程序內部信息到控制臺查看。
2.4 RCC配置
選擇外部晶振作為時鐘源,然后如下圖配置時鐘樹,萬年不變,記住配置即可。
最終目的就是要求最后時鐘樹的末端,高速時鐘和低速時鐘分別是72MHz和36MHz?
2.5 PROJECT配置
?3. keil MDK配置
完成上述配置后,點擊右上角的GENERATE CODE生成代碼,并打開工程,便進入到keil MDK界面。
3.1 創建驅動文件
3.1.1 創建目錄和文件
首先在工程目錄core下新建hardware文件夾,方便我們存放一些外部模塊的驅動代碼文件,并新建兩個文件在此文件夾下,分別是dht11.h 和 dht11.c,注意這兩個文件的后綴,不要隱藏文件后綴名,防止后綴錯誤。一般會在這個文件中寫一些模塊的驅動或者封裝功能函數,在主函數中只需調用這些驅動文件的函數即可使用模塊功能為我們的業務服務了。
3.1.2 配置路徑
3.1.3 配置文件
3.2 引入包配置
為了讓串口能夠重定向更方便的打印輸出內容,需要引入這個包
3.3 調試配置
根據自己的調試下載工具選擇,作者使用的JLink V8 所以選擇下圖所示選項,如果是STLink 則選擇對應選項即可。
防止每次下載按重啟按鍵,可選擇燒錄后自動跑新程序這個選項?
配置完后一定要記得關閉keil MDK軟件,再打開VSCode進行編碼工作,否則剛剛設置的內容并沒有生成配置文件,以上配置工作白搞了。
4. VSCode編碼
?打開對應工程后,一定要先編譯下,這樣才能在對應.c文件下找到對應.h文件
4.1 USART1編寫
重定向代碼編寫,這是固定的代碼,找到usart.h / usart.c文件放到對應的位置上即可
/* USER CODE BEGIN 1 */
int fputc(int ch, FILE * file){HAL_UART_Transmit(&huart1,(uint8_t *)&ch, 1, 1000);return ch;
}
/* USER CODE END 1 *//* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */
?完成以上兩個地方的代碼編寫后,就可以在業務程序中使用printf(" your context"); 進行串口打印了。
4.2 驅動文件編寫
#ifndef __DHT11_H
#define __DHT11_H#include "stm32f1xx_hal.h" // 根據實際MCU型號修改
#include <stdint.h>
#include <stdbool.h>// 錯誤碼定義
typedef enum {DHT11_OK = 0,DHT11_ERROR_TIMEOUT,DHT11_ERROR_CHECKSUM,DHT11_ERROR_NO_RESPONSE
} DHT11_Status;// 溫濕度數據結構體
typedef struct {float temperature; // 溫度(℃)float humidity; // 濕度(%RH)
} DHT11_Data;// 函數聲明
DHT11_Status DHT11_Write(void);
DHT11_Status DHT11_Read(DHT11_Data *data);
void DHT11_DelayUs(uint32_t us);
#endif
#include "dht11.h"
#include "gpio.h"
#include "usart.h"// 微秒級延時
void DHT11_DelayUs(uint32_t us)
{uint32_t delay = (HAL_RCC_GetHCLKFreq() / 4000000 * us);while (delay--){;}
}// 初始化DHT11(配置GPIO為開漏輸出)
DHT11_Status DHT11_Write(void) {// 1. 配置引腳為推挽輸出(發送起始信號)GPIO_InitTypeDef GPIO_InitStruct = {0};GPIO_InitStruct.Pin = DHT11_Pin;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;GPIO_InitStruct.Pull = GPIO_PULLUP;HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);// 1. 主機發送起始信號(拉低18ms)HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET);HAL_Delay(18); // 實際需精確到18ms// 2. 拉高20-40us,切換為輸入模式HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET);DHT11_DelayUs(30);return DHT11_OK;
}// 讀取DHT11數據
DHT11_Status DHT11_Read(DHT11_Data *data) {uint8_t buffer[5] = {0}; // 40位數據:濕度整數/小數,溫度整數/小數,校驗和uint8_t checksum = 0;uint16_t timeout = 100; // 最多等待 100us// 1. 配置引腳為上拉輸入(等待DHT11響應)GPIO_InitTypeDef GPIO_InitStruct = {0};GPIO_InitStruct.Pin = DHT11_Pin;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_PULLUP;HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);// 2. 檢測DHT11響應(40-50us低電平 + 40-50us高電平)timeout = 100; // 超時時間約100uswhile(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {DHT11_DelayUs(1);if (--timeout == 0) {return DHT11_ERROR_NO_RESPONSE; // 等待低電平響應超時}}// 檢測DHT11的響應低電平結束(40-50us低電平)timeout = 60;// 適當增加超時容限while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {DHT11_DelayUs(1);if (--timeout == 0) {return DHT11_ERROR_TIMEOUT; // 低電平持續時間過長}}// 檢測DHT11的響應高電平結束(40-50us高電平)timeout = 60;// 適當增加超時容限while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {DHT11_DelayUs(1);if (--timeout == 0) {return DHT11_ERROR_TIMEOUT; // 高電平持續時間過長}}// 3. 讀取40位數據(每位:12-14us低電平 + 高電平長度決定0/1: 26-28us / 116-118us)for (int i = 0; i < 40; i++) {// 等待每位開始的12-14us低電平timeout = 30; // 適當增加超時容限while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {if (--timeout == 0) {return DHT11_ERROR_TIMEOUT;}DHT11_DelayUs(1);}// 測量高電平持續時間DHT11_DelayUs(30); // 等待30us后采樣if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {buffer[i / 8] |= (1 << (7 - (i % 8))); // 高電平持續時間長,表示1// 等待高電平結束timeout = 100;while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {if (--timeout == 0) {return DHT11_ERROR_TIMEOUT;}DHT11_DelayUs(1);}}// 如果是0,高電平已經結束(26-28us),不需要額外等待}// 4. 校驗數據(前4字節之和 = 校驗和)checksum = buffer[0] + buffer[1] + buffer[2] + buffer[3];if (checksum != buffer[4]) {return DHT11_ERROR_CHECKSUM;}// 5. 填充結果(忽略小數部分)data->humidity = buffer[0];data->temperature = buffer[2];return DHT11_OK;
}
4.2.1 延時函數
因為HAL庫只提供毫秒級別的延時函數,而單總線雙向串行通訊協議的時序圖中有用到微妙級別的延時,所以需要自定義微妙延時函數。?
// 微秒級延時
void DHT11_DelayUs(uint32_t us)
{uint32_t delay = (HAL_RCC_GetHCLKFreq() / 4000000 * us);while (delay--){;}
}
4.2.2 數據請求函數
// 初始化DHT11(配置GPIO為開漏輸出)
DHT11_Status DHT11_Write(void) {// 1. 配置引腳為推挽輸出(發送起始信號)GPIO_InitTypeDef GPIO_InitStruct = {0};GPIO_InitStruct.Pin = DHT11_Pin;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;GPIO_InitStruct.Pull = GPIO_PULLUP;HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);// 1. 主機發送起始信號(拉低18ms)HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET);HAL_Delay(18); // 實際需精確到18ms// 2. 拉高20-40us,切換為輸入模式HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET);DHT11_DelayUs(30);return DHT11_OK;
}
4.2.3 數據接收函數?
// 讀取DHT11數據
DHT11_Status DHT11_Read(DHT11_Data *data) {uint8_t buffer[5] = {0}; // 40位數據:濕度整數/小數,溫度整數/小數,校驗和uint8_t checksum = 0;uint16_t timeout = 100; // 最多等待 100us// 1. 配置引腳為上拉輸入(等待DHT11響應)GPIO_InitTypeDef GPIO_InitStruct = {0};GPIO_InitStruct.Pin = DHT11_Pin;GPIO_InitStruct.Mode = GPIO_MODE_INPUT;GPIO_InitStruct.Pull = GPIO_PULLUP;HAL_GPIO_Init(DHT11_GPIO_Port, &GPIO_InitStruct);// 2. 檢測DHT11響應(40-50us低電平 + 40-50us高電平)timeout = 100; // 超時時間約100uswhile(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {DHT11_DelayUs(1);if (--timeout == 0) {return DHT11_ERROR_NO_RESPONSE; // 等待低電平響應超時}}// 檢測DHT11的響應低電平結束(40-50us低電平)timeout = 60;// 適當增加超時容限while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {DHT11_DelayUs(1);if (--timeout == 0) {return DHT11_ERROR_TIMEOUT; // 低電平持續時間過長}}// 檢測DHT11的響應高電平結束(40-50us高電平)timeout = 60;// 適當增加超時容限while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {DHT11_DelayUs(1);if (--timeout == 0) {return DHT11_ERROR_TIMEOUT; // 高電平持續時間過長}}// 3. 讀取40位數據(每位:12-14us低電平 + 高電平長度決定0/1: 26-28us / 116-118us)for (int i = 0; i < 40; i++) {// 等待每位開始的12-14us低電平timeout = 30; // 適當增加超時容限while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) {if (--timeout == 0) {return DHT11_ERROR_TIMEOUT;}DHT11_DelayUs(1);}// 測量高電平持續時間DHT11_DelayUs(30); // 等待30us后采樣if (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {buffer[i / 8] |= (1 << (7 - (i % 8))); // 高電平持續時間長,表示1// 等待高電平結束timeout = 100;while(HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) {if (--timeout == 0) {return DHT11_ERROR_TIMEOUT;}DHT11_DelayUs(1);}}// 如果是0,高電平已經結束(26-28us),不需要額外等待}// 4. 校驗數據(前4字節之和 = 校驗和)checksum = buffer[0] + buffer[1] + buffer[2] + buffer[3];if (checksum != buffer[4]) {return DHT11_ERROR_CHECKSUM;}// 5. 填充結果(忽略小數部分)data->humidity = buffer[0];data->temperature = buffer[2];return DHT11_OK;
}
4.3 主函數業務代碼
/* USER CODE BEGIN Includes */
#include "dht11.h"
/* USER CODE END Includes *//* USER CODE BEGIN 2 */DHT11_Data sensor_data;/* USER CODE END 2 *//* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */printf("hello world!!! \r\t\n");if(DHT11_Write() == DHT11_OK){if (DHT11_Read(&sensor_data) == DHT11_OK) {printf("Temperature: %.1f C, Humidity: %.1f%%RH\r\n", sensor_data.temperature, sensor_data.humidity);} else {printf("DHT11 Read Error! \r\n");}}else {printf("DHT11 Write Error!\r\n");}HAL_Delay(5000);}/* USER CODE END 3 */
?
5. 結果驗證
????????對代碼進行編譯燒錄后,通過連接串口,能夠看到對應的數據,因為DHT11溫濕度傳感器精度不高,所以沒有小數點,盡管程序有讀取,均為0,如果想要更高精度的測量,建議使用DHT22
?