一、接线布局以及开发环境
硬件:ESP32-S3,DHT11模块(三线和四线一样,这个模块自带上拉电阻10kΩ)
接线:DHT11模块正负极连电源正负极,最好是3.3v,因为模块自带了上拉电阻。ESP32上随便找个引脚(我用引脚15)连DHT11的S脚。
开发环境:ESP-IDF,VSCODE。
二、DHT11模块原理
单总线通讯原理,非常好理解。主从双方都是既输出又输入,不过启动信号是主机发起。识别0、1靠高电平时间长短。一次固定传输40bits,总用时大概4.5ms。
模块难点是时间单位用us,微秒,并且时间容错较低,2us左右出入都有可能通信失败。
模块能检测湿度和温度信号,使用3.3v或5v电源都行。
ESP32引脚开启上拉电阻和OD(开漏)输出。
三、上传源代码
#include <string.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include "driver/gpio.h"
#include <esp_log.h>
#include "esp_timer.h"
#include "esp_rom_sys.h"
#define DHT11_GPIO 15 // DHT11引脚定义
const static char *TAG = "DHT11";
// 温度 湿度buffer
uint8_t buffer[5];
int64_t phase_duration[3]={0};
int64_t bit_duration_low[40]={0};
int64_t bit_duration_high[40]={0};
// DHT11 初始化引脚,等待1s上电时间
void DHT11_Init()
{
gpio_config_t cnf={
.mode = GPIO_MODE_OUTPUT_OD,
.pin_bit_mask = 1ULL<<DHT11_GPIO,
.pull_up_en=1,
};
gpio_config(&cnf);
vTaskDelay(1200/portTICK_PERIOD_MS);
}
/*
timeout单位是us
*/
esp_err_t wait_pin_state(uint32_t timeout, int expected_pin_state)
{
/*用这段固定时间的代码也可以*/
// esp_rom_delay_us(timeout);
// if(gpio_get_level(DHT11_GPIO)==expected_pin_state)
// return ESP_OK;
// else
// return ESP_FAIL;
/*建议用这段代码*/
int64_t start_time;
start_time=esp_timer_get_time();
while(esp_timer_get_time()-start_time<=timeout)
{
if(gpio_get_level(DHT11_GPIO)==expected_pin_state)
return ESP_OK;
esp_rom_delay_us(1);
}
return ESP_FAIL;
}
esp_err_t DataRead()
{
int64_t time_since_waiting_start;
esp_err_t result=ESP_FAIL;
memset(buffer,0,sizeof(buffer));
gpio_set_direction(DHT11_GPIO,GPIO_MODE_OUTPUT_OD);
gpio_set_level(DHT11_GPIO,1);
vTaskDelay(2000/portTICK_PERIOD_MS);
gpio_set_level(DHT11_GPIO,0);
vTaskDelay(25/portTICK_PERIOD_MS);
gpio_set_level(DHT11_GPIO,1);
time_since_waiting_start=esp_timer_get_time();
gpio_set_direction(DHT11_GPIO,GPIO_MODE_INPUT);
result=wait_pin_state(19,0); //
if(result == ESP_FAIL)
{
ESP_LOGE(TAG, "Phase A Fail, slave not set LOW.");
return ESP_FAIL;
}
phase_duration[0]=esp_timer_get_time()-time_since_waiting_start;
time_since_waiting_start=esp_timer_get_time();
/*等从机拉低总线83us,再拉高87us*/
result=wait_pin_state(80,1);
if (result == ESP_FAIL)
{
ESP_LOGE(TAG, "Phase B Fail, slave not set HIGH.");
return ESP_FAIL;
}
phase_duration[1]=esp_timer_get_time()-time_since_waiting_start;
time_since_waiting_start=esp_timer_get_time();
result = wait_pin_state(80, 0);
if (result == ESP_FAIL)
{
ESP_LOGE(TAG, "Phase C Fail, slave not set LOW to start sending.");
return ESP_FAIL;
}
phase_duration[2]=esp_timer_get_time()-time_since_waiting_start;
time_since_waiting_start=esp_timer_get_time();
for(int j=0;j<5;j++)
{
for(int i =0;i<8;i++)
{
/*数位低电平时间*/
while(gpio_get_level(DHT11_GPIO)==0)
{
esp_rom_delay_us(1);
}
bit_duration_low[j*8+i]=esp_timer_get_time()-time_since_waiting_start;
/*检测数字高位的时间长度来判断是1或0*/
time_since_waiting_start=esp_timer_get_time();
do
{
if (gpio_get_level(DHT11_GPIO) == 0)
{
if (esp_timer_get_time() - time_since_waiting_start > 40)
{
/*数字1*/
buffer[j] = buffer[j] | (1U << (7 - i));
/*数字0不用处理*/
}
bit_duration_high[j*8+i]=esp_timer_get_time()-
time_since_waiting_start;
time_since_waiting_start=esp_timer_get_time();
break;
}
} while (esp_timer_get_time() - time_since_waiting_start < 74);
}
}
result=wait_pin_state(56,1);
if (result == ESP_FAIL)
{
ESP_LOGE(TAG, "Data is all read.But CAN not set high.");
return ESP_FAIL;
}
return ESP_OK;
}
// 主函数
void app_main(void)
{
esp_err_t result;
uint8_t i,j;
DHT11_Init();
while(1)
{
memset(phase_duration,0,sizeof(phase_duration));
memset(bit_duration_low,0,sizeof(bit_duration_low));
memset(bit_duration_high,0,sizeof(bit_duration_high));
result = DataRead();
if (result==ESP_OK)
{
ESP_LOGI(TAG,"Reading data succeed.");
if(((buffer[0]+buffer[1]+buffer[2]+buffer[3])&0xFF) != buffer[4])
ESP_LOGE(TAG, "But checksum error.");
ESP_LOGI(TAG, "Temperature is:%d.%d, Humidity is:%d.%d", buffer[2], buffer[3], buffer[0],buffer[1]);
}
ESP_LOGI(TAG,"PhaseA duration is:%lld",phase_duration[0]);
ESP_LOGI(TAG,"PhaseB duration is:%lld",phase_duration[1]);
ESP_LOGI(TAG,"PhaseC duration is:%lld",phase_duration[2]);
ESP_LOGI(TAG,"Bit duration is as follows:");
for(j=0;j<5;j++)
{
for(i=0;i<8;i++)
{
printf("%lld,%lld-",bit_duration_low[j*8+i],bit_duration_high[j*8+i]);
}
printf("\n");
}
}
}
每隔2秒左右输出结果如下:
2024-04-26 16:37:44 I (926424) DHT11: Reading data succeed.
2024-04-26 16:37:44 I (926424) DHT11: Temperature is:26.1, Humidity is:71.2
2024-04-26 16:37:44 I (926424) DHT11: PhaseA duration is:19
2024-04-26 16:37:44 I (926424) DHT11: PhaseB duration is:79
2024-04-26 16:37:44 I (926434) DHT11: PhaseC duration is:79
2024-04-26 16:37:44 I (926434) DHT11: Bit duration is as follows:
2024-04-26 16:37:44 51,27-49,72-50,25-51,25-51,25-50,72-50,71-51,73-
2024-04-26 16:37:44 51,25-50,25-51,25-51,25-50,26-50,25-51,70-51,28-
2024-04-26 16:37:44 51,25-50,26-50,25-51,72-49,72-50,25-51,70-51,28-
2024-04-26 16:37:44 51,25-50,25-51,25-51,25-51,25-49,25-50,26-50,74-
2024-04-26 16:37:44 51,25-51,70-51,71-51,25-49,26-49,71-49,25-51,25-
我将40位bit的时间高低都打印出来了,51,27表示低位持续51us,高位持续27us,这两段表示一个bit数字0。
四、聊一些体会
这个DHT11难点是调用ESP32的微秒级计时器。在通讯过程千万不能用ESP_LOGI()这类打印函数,安插了这类打印函数在通信中途试图看时间点,试了很多次都通信失败,发现调用一次这个打印函数耗时2000多us.....
vTaskDelay()这种是Freertos控制的定时器,依赖软系统,而Freertos的sys_tick默认是100ms,最低也只能设置到1ms,意思是Freertos对后台控制切换的精度最高是1毫秒一次,而我们要求精度去到微秒,因此不能用vtaskdelay(),要用硬件定时器的esp_timer_get_time()。
试了很多次都是无法检测Phase A下降沿、读取40bit过程无法检测上拉电位等等,怀疑过是硬件线路搭建错误比如上拉电位达不到,也怀疑过是系统定时不够精确。经实验,系统硬件定时足够精确到1us。用示波器检查结果如下:
主机拉高保持18us,从机第一次拉低。
从机低电平保持80us,拉高。
从机高电平保持80us,拉低。
***下面开始发送数据bit***
从机低电平50us,拉高
从机高电平26us,拉低 信号0
从机低电平50us,拉高
从机高电平70us,拉低 信号1
从机低电平52us,拉高
从机高电平26us,拉低 信号0
从机低电平50us,拉高
从机高电平24us,拉低 信号0
从机低电平50us,拉高
从机高电平26us,拉低 信号0
***数据发送完毕***
从机低电平保持52us,拉高,完成一次传输
原来这个模块的时间间隔与芯片手册描述的间隔有出入。