第一次接触DHT11温湿度传感器时,我也被官方驱动代码的复杂度震惊了。不仅需要引入多个头文件,代码耦合度还特别高,随便改个引脚定义就得翻遍整个工程。更头疼的是,传统轮询方式会占用大量CPU资源,在需要实时响应的系统中简直就是灾难。
DHT11采用单总线协议,每次通信需要精确控制18-30ms的低电平起始信号,然后等待传感器返回40位数据(包含温湿度及校验值)。传统做法是用延时函数配合GPIO读取,但这种方案存在三个致命缺陷:
实测发现,用标准轮询方式读取一次DHT11会阻塞主程序约4ms,这在需要频繁采集的场景下根本无法接受。于是我开始探索硬件自动化的解决方案,最终确定了"定时器输入捕获+DMA自动搬运"的技术路线。
STM32的定时器输入捕获功能简直就是为DHT11这类单总线设备量身定制的。其核心原理是利用定时器记录信号边沿变化的精确时刻,我们选择TIM1的通道1配置为PWMI模式(PWM输入模式),这样能同时捕获上升沿和下降沿时间戳。
具体工作流程:
这种硬件级测量完全不受中断影响,实测时序精度可达±0.5μs,远超市面大多数DHT11模块的精度需求。
光有精确测量还不够,我们需要解决数据搬运的CPU占用问题。这里祭出STM32的另一大杀器——DMA控制器。配置DMA1的通道2和通道3分别搬运CCR1和CCR2寄存器的值到内存数组,整个过程无需CPU参与。
关键配置要点:
实际测试中,DMA搬运40位数据仅消耗不到2μs的总线时间,相比轮询方案提升了2000倍效率。更重要的是,整个采集期间CPU可以正常执行其他任务。
c复制void IC_Init(void) {
// 开启TIM1时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
// 时基单元配置
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
TIM_TimeBaseStruct.TIM_Prescaler = 71; // 1μs计时精度
TIM_TimeBaseStruct.TIM_Period = 65535; // 最大计数值
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStruct);
// 输入捕获配置
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Falling;
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStruct.TIM_ICFilter = 0x05; // 适度滤波防抖动
TIM_ICInit(TIM1, &TIM_ICInitStruct);
// 配置为PWMI模式
TIM_PWMIConfig(TIM1, &TIM_ICInitStruct);
TIM_SelectInputTrigger(TIM1, TIM_TS_TI1FP1);
TIM_SelectSlaveMode(TIM1, TIM_SlaveMode_Reset);
}
这段代码有几个关键点需要注意:
c复制uint16_t edgeTime[41]; // 存储边沿时间戳
uint16_t pulseWidth[40]; // 存储脉冲宽度
void DMA_Config(void) {
DMA_InitTypeDef DMA_InitStruct;
// 通道2配置(CCR1值搬运)
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&TIM1->CCR1;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)edgeTime;
DMA_InitStruct.DMA_BufferSize = 41;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralToMemory;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_Init(DMA1_Channel2, &DMA_InitStruct);
// 通道3配置(CCR2值搬运)
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&TIM1->CCR2;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)pulseWidth;
DMA_InitStruct.DMA_BufferSize = 40;
DMA_Init(DMA1_Channel3, &DMA_InitStruct);
}
这里采用双DMA通道设计,主要考虑两点:
c复制void SendStartSignal(void) {
GPIO_Init(GPIOA, GPIO_Pin_8, GPIO_Mode_Out_PP);
GPIO_ResetBits(GPIOA, GPIO_Pin_8);
Delay_ms(20); // 保持20ms低电平
GPIO_SetBits(GPIOA, GPIO_Pin_8);
Delay_us(30); // 30μs高电平
GPIO_Init(GPIOA, GPIO_Pin_8, GPIO_Mode_IPU); // 切换为上拉输入
}
c复制void StartCapture(void) {
TIM_Cmd(TIM1, DISABLE);
TIM_SetCounter(TIM1, 0);
DMA_Cmd(DMA1_Channel2, ENABLE);
DMA_Cmd(DMA1_Channel3, ENABLE);
TIM_DMACmd(TIM1, TIM_DMA_CC1, ENABLE);
TIM_DMACmd(TIM1, TIM_DMA_CC2, ENABLE);
TIM_Cmd(TIM1, ENABLE);
}
c复制uint8_t ParseData(void) {
uint8_t data[5] = {0};
for(int i=0; i<40; i++) {
if(pulseWidth[i] > 40) { // 高电平>40μs判为1
data[i/8] |= (1 << (7-(i%8)));
}
}
// 校验数据
if(data[0]+data[1]+data[2]+data[3] == data[4]) {
humidity = data[0];
temperature = data[2];
return 1; // 成功
}
return 0; // 校验失败
}
在实际部署中可能会遇到以下问题:
这个方案在我负责的智能农业项目中稳定运行超过6个月,200多个节点日均采集超过50万次数据,故障率低于0.01%。最关键的是,CPU占用率从原来的15%降低到不足0.1%,系统响应速度提升明显。