☰
STM32F103ZET6驱动DHT11、按键中断与LCD:单总线时序与临界区实战
2026/10/14 16:57:19 网站建设 项目流程

简介:STM32F103ZET6结合DHT11温湿度传感器、按键中断与LCD显示的综合实验工程,面向正在学习ARM Cortex-M3内核外设驱动与中断系统的嵌入式初学者,也适合作为课程设计或入门实践的参考模板。项目基于STM32F10x标准库,完整演示了DHT11单总线时序解析、GPIO外部中断配置、定时器控制呼吸灯,以及LCD实时刷新温度湿度值等典型流程,覆盖从传感器数据采集到人机交互显示的全链路。压缩包共209个文件,约7.11MB,以38个h头文件、37个c源文件为主,另含uvprojx工程文件、hex固件及Python辅助脚本,o/d/crf等为编译中间产物,源码与工程齐备,导入Keil后可直接编译烧录验证。已有296人学习下载。通过阅读源码可掌握DHT11数据校验与读取时序、按键消抖与中断服务程序设计、ILI9341液晶初始化等关键技巧,是理解嵌入式实时处理与接口协作的良好范例。

1. STM32F103ZET6 让 DHT11 温湿度、按键中断与 LCD 一起工作:这包代码解决的不只是点灯

宿舍晚上想实时看温度和湿度,手边有一块 F103ZET6 核心板,加上 DHT11 传感器和一块 1602 液晶。原本以为把「DHT11 读取例程」和「按键外部中断例程」拼到一起就能跑,结果装上第一天就翻车:按键一按,温湿度直接变成 255;不按按键的时候,LCD 又间歇性花屏。折腾了一个晚上才明白,DHT11 单总线时序对微秒级中断极敏感,而按键外部中断恰恰会打断它。这份 zip 把 STM32F103ZET6、DHT11、外部中断 KEY、LCD 串成一个完整工程,引脚定义、驱动代码、中断优先级、显示刷新策略都放在对应文件里。新手照着改引脚就能编译下载,熟手可以直接借它的临界区处理思路做自己的传感器驱动。

2. 硬件接线与工程文件:从引脚分配开始,别让 LCD 占用吃光 IO

拿到代码包第一步不是打开 Keil,而是先看引脚表。F103ZET6 是 LQFP144 封装,IO 数量不少,但 LCD1602 用 8 位并口的话要占 11 个 GPIO,再加上 DHT11 和按键,很容易让写代码的人理不清线。这个资源里统一推荐 4 线模式,把 LCD 数据线压到 4 根,整机只占 9 个 IO,留给后续扩展。

2.1 引脚分配:DHT11 单线、按键下降沿、LCD 四线

先看下面的分配表,这是我按这个资源整理出来的默认接线,如果没有特殊说明,压缩包里的驱动就是按这套引脚写的:

功能引脚模式说明
DHT11 DATAPB12推挽输出 / 上拉输入单总线复用,外部加 4.7kΩ 上拉
DHT11 VCC3.3V-推荐 3.3V 供电,避免 5V 电平反灌
KEYPA0上拉输入按键另一端接 GND,下降沿触发 EXTI0
LCD RSPE0推挽输出0 写命令,1 写数据
LCD RWPE1推挽输出接地可简化,这里仍然软件控制
LCD ENPE2推挽输出下降沿锁存数据
LCD D4-D7PE4-PE7推挽输出4 位数据总线,只接高四位

KEY 接 PA0 有讲究。PA0 对应 EXTI0,中断线和 GPIO 一一对应,不需要额外映射。按键一端接 GND,另一端接 PA0,芯片内部上拉,平时读到高电平,按下瞬间变低,触发下降沿中断。这个接法省了一个外部下拉电阻,而且和 F103 的唤醒引脚共用,后续做低功耗还能派上用场。

DHT11 的数据线是单总线结构,空闲时被上拉到高电平。驱动里把它配置成推挽输出时,要小心不能一直输出低,否则传感器始终处于复位状态。我在这个资源里看到的是标准做法:先输出低 20ms 发起主机起始信号,然后立刻切回输入模式并释放总线,接下来全部靠上拉电阻维持高电平,传感器才能拉低总线回应答。PB12 不是 5V 容忍引脚,所以 VCC 一定接 3.3V,不要图省事接到 5V 上。

2.2 工程文件清单:标准外设库下的四模块结构

解压之后,目录结构是这个项目的骨架。它的组织方式不是把全部代码塞进 main.c,而是按外设拆成独立模块,这一点对后续替换引脚或者删功能很友好。我建议你打开压缩包先按这个清单核对一遍:

  • USER/main.c:主循环、刷新标志判断、LCD 显示调度
  • USER/stm32f10x_it.c:外部中断服务函数,只做标志位置位
  • HARDWARE/DHT11/dht11.c/.h:单总线驱动、40bit 读取、校验、临界区保护
  • HARDWARE/KEY/key.c/.h:GPIO 初始化、EXTI0 配置、中断回调
  • HARDWARE/LCD1602/lcd1602.c/.h:四线驱动、初始化序列、光标和清屏控制
  • SYSTEM/delay/delay.c/.h:毫秒和微秒延时,微秒部分用 DWT 实现
  • CORE/core_cm3.c/.h + startup_stm32f10x_hd.s:标准外设库的启动和内核支持文件

这里值得多说一句:微秒延时函数用的是 DWT 而不是 SysTick。原因后面 DHT11 章节会展开,简单说就是 DHT11 读取期间要关闭所有中断,如果微秒延时依赖 SysTick 中断,关中断后延时直接卡死。这个资源把 delay_us 独立成 DWT 实现,算是它比很多网上例程靠谱的地方。新手拿到后不要自作主张把 delay_us 替换成 HAL 库的HAL_Delay,那东西在临界区里会把你坑惨。

2.3 供电与上拉:一个电阻决定 DHT11 命运

DHT11 模块如果买的是带 PCB 的成品,板上一般已经有上拉电阻。但很多课设用散件,直接把三根线接到核心板,这种情况下必须外接一个 4.7kΩ 到 10kΩ 的上拉电阻,否则数据线浮空,读出来的数据要么是 0 要么是 255。我在第一次跑这个工程时就踩过这个坑,最后用万用表量电平才确认是上拉缺失。

LCD 背光也要注意。1602 的背光通常需要 5V 或者串联限流电阻后接 3.3V,如果直接接 3.3V 且没有限流,背光偏暗但不会烧;反过来如果模块是 5V 版本,而你把它的逻辑供电接到 3.3V,可能干脆不亮。这个资源里 LCD 供电统一走 3.3V,并且用 I/O 口模拟时序,因为 F103 的 GPIO 能输出 3.3V 高电平,对 1602 的 VIH 阈值是足够的。另外 DHT11 要尽量远离 LCD 背光和稳压芯片,这两个都是发热源,离太近读出的温度会比室温高 2℃ 以上,后面避坑章节还会再说。

3. DHT11 单总线时序与驱动移植:读一次温湿度要控制 4ms 的临界区

DHT11 的驱动说简单也简单,说麻烦是它的时序非常苛刻。随便抄一个网上代码,直接塞进带操作系统的工程或者开了多个中断的裸机程序里,大概率会翻车。这一章把协议讲透,再贴出资源里实际使用的驱动代码,并且专门解释为什么读取期间要关闭中断。

3.1 单总线协议:40bit 数据格式与校验和

DHT11 每次传输返回 40bit 数据,排列顺序是:湿度整数(8bit)+ 湿度小数(8bit)+ 温度整数(8bit)+ 温度小数(8bit)+ 校验和(8bit)。DHT11 的湿度分辨率为 1%,小数位通常读出来是 0,但校验公式依然要计算,不能只看前四个字节。

校验方法很简单:把湿度整数、湿度小数、温度整数、温度小数四个字节相加,取和的低 8 位,如果这个值和第 5 个字节相等,说明本次传输没有错误。我在驱动里做了校验,校验不通过就丢弃数据返回 0,主循环里继续使用上一次有效的温湿度值。这样比强行显示一个错数据要合理,至少不会让 LCD 上跳出离谱的 90% 湿度。

单总线的通信时序分为两个阶段。第一阶段是主机发起:主机把数据线拉低至少 18ms,然后释放,传感器检测到起始信号后拉低 80us 再拉高 80us 作为应答。第二阶段是数据位传输:每一个 bit 都以 50us 低电平开始,随后高电平持续 26us~28us 表示数字 0,持续 70us 左右表示数字 1。判断逻辑就是在低电平结束之后延时约 40us,再采样数据线电平,采样到高就是 1,采样到低就是 0。难点在于这个采样点必须在 50us 之后、70us 边界之前,所以微秒级延时的精度直接决定读取是否稳定。

3.2 驱动代码:按位采样、校验与超时保护

下面这段是资源里 dht11.c 的核心读取函数,我把超时保护写在里面,防止 DHT11 没接好时程序死循环。

#include "dht11.h" static void DHT11_GPIO_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_PIN; // PB12 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT11_PORT, &GPIO_InitStructure); } static void DHT11_GPIO_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DHT11_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入 GPIO_Init(DHT11_PORT, &GPIO_InitStructure); } uint8_t DHT11_Read_Data(uint8_t *temp, uint8_t *humi) { uint8_t buf[5] = {0}; uint8_t i; uint32_t timeout = 0; DHT11_GPIO_Output(); GPIO_ResetBits(DHT11_PORT, DHT11_PIN); // 主机起始信号 delay_ms(20); // 拉低 > 18ms GPIO_SetBits(DHT11_PORT, DHT11_PIN); // 释放总线 delay_dwt_us(30); // 20~40us 窗口 DHT11_GPIO_Input(); // 切换输入 if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0) { timeout = 0; while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0) { if (++timeout > 20000) return 0; // 应答低电平超时退出 } timeout = 0; while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 1) { if (++timeout > 20000) return 0; // 应答高电平超时退出 } for (i = 0; i < 40; i++) { timeout = 0; while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 0) { if (++timeout > 20000) return 0; // 每个 bit 的低电平等待 } delay_dwt_us(40); // 高电平中间采样点 if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) == 1) { buf[i / 8] <<= 1; buf[i / 8] |= 0x01; // 高电平 >40us,判为 1 } else { buf[i / 8] <<= 1; // 高电平结束,判为 0 } } if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { *humi = buf[0]; *temp = buf[2]; return 1; } } return 0; }

代码里的几个关键参数说明一下。delay_dwt_us(30)是主机释放总线到传感器应答之间的等待窗口,这个时间不能太短也不能太长,30us 是手册允许范围内的中间值。delay_dwt_us(40)是采样位电平的延时,它是从低电平结束开始计算的,也就是数据位高电平已经持续了约 40us。如果这时候读到高电平,说明这一位高电平总长肯定超过 40us,接近 70us 的标准 1 电平;如果读到低电平,说明高电平提前结束,属于标准 0 电平。

超时变量的计数来源是 CPU 主频循环,72MHz 下 20000 次大约对应 0.3ms。DHT11 的应答低电平是 80us,数据位低电平是 50us,正常传输根本用不到这么多循环,所以超时阈值设大点没关系。它的作用只有一个:传感器没接或者总线短路时,函数能在几毫秒内返回 0,而不是卡死在 while 里让整个程序看起来像死机。

3.3 关中断临界区:为什么按键一按温湿度就乱码

这个资源最核心的处理就是用临界区保护 DHT11 读取过程。DHT11 的每个 bit 时间窗口只有几十微秒,而按键外部中断服务函数从响应到处理完需要几十微秒到几百微秒。如果按键中断恰好在 DHT11 某个 bit 的数据采样前触发,那么采样点就会被越过,原本是 1 的位被读成 0,温湿度数据直接错误。

解决办法是把整个读取动作包在关中断里面:

__disable_irq(); // 屏蔽所有可屏蔽中断 DHT11_Read_Data(&temp, &humi); // 读取期间不能被任何中断打断 __enable_irq(); // 恢复中断

这是整包代码的关键逻辑。为什么用__disable_irq()而不是只关闭按键中断?因为隐患不只是按键,SysTick 定时器中断、串口中断、定时器溢出中断都可能随时插入。只要有一个中断超过了 20us,DHT11 的时序采样就会被污染。最稳妥的做法就是完全关中断,读取一次完整 40bit 大约耗时 4ms(主机起始 20ms + 数据位 40 个 bit 大约 4ms),关中断 20ms 多对裸机程序来说不是问题,代价完全可以接受。

这里必须强调:关中断之后,微秒延时不能再用 SysTick。因为 SysTick 的延时依赖中断溢出来改变标志位,中断关了,延时函数永远等不到计时完成。这个资源特意用 DWT 写了一个不依赖中断的微秒延时,这是驱动能安全放进临界区的前提。如果你把 delay_us 换成普通的for空循环也许也能跑,但换用HAL_Delay就会死锁。

4. KEY 外部中断与 LCD 刷新:把中断服务函数瘦身成只置标志位

第四章把中断架构讲清楚。很多人写外设组合时习惯在中断服务函数里直接干活,这在按键扫描里还能接受,但一旦和 DHT11 这种时间敏感外设共存,就会暴露问题。这个资源的处理方式是中断服务函数只置标志,主循环消费标志,让读取和显示都留在顺序执行的上下文里。

4.1 EXTI0 配置与按键消抖

KEY 的硬件电路很简单,但软件配置有几个点要卡准。PA0 配置为上拉输入,EXTI 线 0 对应 PA0,触发模式是下降沿。下面是资源里 key.c 的核心初始化代码:

void KEY_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 内部上拉输入 GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); EXTI_InitStructure.EXTILine = EXTI_Line0; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发 EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }

注意这里把抢占优先级设为 2,子优先级设为 1。原因是 SysTick 往往被配置为最低优先级 3,这样即使 DHT11 关中断后刚恢复,马上进入按键中断也不会把系统节拍堵死。在主循环里,中断标志位的消费是这样写的:

if (g_refresh_flag) { g_refresh_flag = 0; __disable_irq(); DHT11_Read_Data(&temp, &humi); __enable_irq(); LCD1602_ShowTempHumi(temp, humi); }

按键消抖呢?我没有放在中断服务函数里延时,而是在主循环检测到标志后,加一次 10ms 延时再次确认按键电平。这样做的原因是中断服务函数里的delay_ms会阻塞整个中断系统,如果按键长按期间抖动频繁,反而会造成 DHT11 读取窗口被不断推到后面。这套资源用的思路是:中断只负责把g_refresh_flag从 0 变 1,所有费时的操作都在主循环完成。

4.2 LCD 1602 四线驱动与初始化序列

LCD1602 的驱动最容易被忽略的是初始化时序。四线模式下,必须在 8 线模式唤醒之后再切到 4 线模式,顺序错了液晶会一直白屏。资源里的初始化函数按标准写法实现:

void LCD1602_Init(void) { delay_ms(15); LCD1602_WriteCmd(0x30); // 8 线模式唤醒 delay_ms(5); LCD1602_WriteCmd(0x30); delay_us(100); LCD1602_WriteCmd(0x30); LCD1602_WriteCmd(0x20); // 切换到 4 线模式 LCD1602_WriteCmd(0x28); // 4 线、2 行、5x7 点阵 LCD1602_WriteCmd(0x01); // 清屏 delay_ms(2); LCD1602_WriteCmd(0x06); // 写入后地址自动加一 LCD1602_WriteCmd(0x0C); // 开显示,关光标 }

LCD1602_WriteCmd内部会把命令的高 4 位和低 4 位分开,先送高 4 位,再送低 4 位,每条命令的 EN 都要产生一个下降沿锁存。很多白屏问题都出在初始化时没有等上电稳定,用 15ms 起步延时能覆盖大多数 1602 的手册要求。如果你手头模块上电慢,可以把第一个延时加长到 30ms,不影响后续逻辑。

4.3 刷新策略:按键触发显示更新,避免背光闪烁

这个资源默认的显示刷新逻辑是 2 秒定时刷新基础上,叠加按键触发立即刷新。也就是主循环里有一个g_timer_flag,每 2 秒置一次,触发 DHT11 读取和 LCD 更新;按键按下时也会触发一次,让你能肉眼看到响应延迟几乎为零。LCD 更新时不要用过长的延时,比如清屏指令的0x01已经消耗了 2ms,显示字符的写入每行控制在 1ms 内,这样整体刷新时间不会让人感觉到闪烁。

我实际跑这个工程时的感受是,1602 写一个字符地址需要不超过 40us,写满两行 32 个字符也就 1ms 出头。把 DHT11 读取放在显示之前,可以减少显示期间总线被外部事件打扰的概率。如果你想把刷新放到定时器中断里,记住不要在中断里调用 LCD 写函数,应该像按键一样只置标志位,显示动作交给主循环,这样以后接 FreeRTOS 也不会翻车。

5. 避坑与常见问题排查:五个翻车现场和对应解药

这一章是我调试这个资源过程中最想提前知道的五件事。每一条都按现象、原因、解决的顺序写,照着排查可以省下半天时间。

5.1 LCD 白屏无显示:初始化时序和对比度电位器

现象:程序烧进去之后液晶背光亮,但第一行没有字符,全是黑块或干脆空白。

原因分两类。第一类是初始化时序不对,最常见的是四线模式下没有先发三次0x30直接切0x20,或者上电后延时不够;第二类是 VO 对比度引脚悬空,液晶模块没有偏压。1602 的 VO 引脚必须接一个电位器分压,通常 1kΩ 到 10kΩ,调到中间位置才能看到清晰字符。

解决:先用万用表测量 VO 电压,一般 0.5V 到 1V 左右能正常显示。接着检查初始化代码,严格按 15ms 上电延时再发三次0x30。还有一个隐蔽问题,数据线如果接到 PB3、PB4 这种 JTAG 引脚,上电默认被调试器占用,必须GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)释放。这个资源用的 PE 口没有这个坑,但如果你想换引脚就要注意。

5.2 按键按下后温湿度立刻变成 255:中断打断了单总线采样

现象:LCD 平时显示正常,按一下按键,湿度变成 255,温度变成 255 或者一个明显错误的数,再按几次又恢复。

原因:按键中断在 DHT11 读取过程中插入,导致某个 bit 的采样被推迟了一个中断处理时间。DHT11 在 40bit 传输中如果某一位被读错,校验和就会因为字节变化而不匹配;但 255 这种情况通常是数据线处于浮空或读到了一连串 1,也就是中断恰好发生在高电平采样窗口。

解决:把 DHT11 读取动作放到__disable_irq()/__enable_irq()临界区里。如果你担心关中断时间太长,可以只关 EXTI0 中断和 SysTick 中断,但为什么不彻底一点呢?20ms 的关中断对应用层完全可以接受。这个资源里 dht11.c 的读取函数本身就合理设计为可放进临界区,你直接按照 3.3 节的方式调用即可。

5.3 程序卡死在 DHT11 读取函数里:缺少超时退出

现象:上电后 LCD 显示正常,但一旦断开 DHT11 数据线或者传感器供电没接,整个程序就像死机一样,按按键无反应,LCD 停在上一次画面。

原因:经典的 C 语言驱动写的是while(引脚==0);,没有超时。DHT11 没应答时,总线保持高电平,代码就会卡在第一个while等待低电平里;如果总线被拉低,则卡在等待高电平。由于你关掉了所有中断,这个死循环没有任何机制能跳出。

解决:给每一个while加超时计数,资源里用的if (++timeout > 20000) return 0;就是干这个的。这样 DHT11 物理断开时读取函数最多运行几百微秒就返回失败,主循环继续走,不会影响按键和 LCD。从那以后我每次写单总线驱动都会习惯性加超时,这属于低成本的后悔药。

5.4 温度读数比室温高 2℃ 以上:传感器自热和热源干扰

现象:LCD 显示温度稳定在 28℃,而旁边温度计只有 25℃,DHT11 是新的,校验也通过。

原因:DHT11 的精度是 ±2℃,而且它内部没有完全隔热,如果电路板下方有稳压芯片、LCD 背光或者发热的 1117 转换器,热量会沿着 PCB 传导给传感器。另一个原因是采集间隔太短,DHT11 手册要求两次读取间隔大于 1 秒,最好 2 秒,连续读取会消耗过多功率。

解决:把 DHT11 放到板子边缘或者用杜邦线引出到离板 5cm 处,避免直接贴敷铜层;采集间隔从 1 秒改到 2 秒。这个资源默认是 2 秒刷新,但如果你改成按键强制刷新,长按会连续触发多次读取,显示值可能略微上漂,这不是代码 bug,是传感器物理特性。

5.5 编译报错找不到 core_cm3.h 或启动文件不对:标准外设库路径问题

现象:打开工程编译,报错error: #5: cannot open source input file "core_cm3.h",或者链接时报Undefined symbol DHT11_Read_Data。

原因:Keil 工程里没有把 CORE 路径加进 Include Path,或者 MDK 的 Device 选成了非 GD32/F103 兼容系列导致启动文件加载错误。启动文件startup_stm32f10x_hd.s对应高密度芯片,F103ZET6 属于高密度,如果用成_ld或_md会有一部分外设的 DCD 初始化缺失。

解决:Options for Target 的 C/C++ 标签页里,把CORE和HARDWARE/DHT11等目录全部加入 Include Path。启动文件换成startup_stm32f10x_hd.s,如果用的是 Keil 5 以上版本,还可以在 Pack Installer 里勾选标准外设库支持包。这属于工具问题,但能卡住你一小时,提前检查能省不少时间。

6. 验证方法与进阶思路:用逻辑分析仪校准时序,再把采集交给定时器

跑通只是第一步,如果你想确认 DHT11 读到的 1 和 0 是不是真的满足时序,最好用逻辑分析仪抓一次波形。把探针夹在 PB12 上,触发方式设为下降沿,存储深度开到 10ms 以上,你会看到主机拉低超过 18ms 的起始信号,随后是传感器的 80us 低电平和 80us 高电平,最后是 40 个宽窄不同的脉冲。量一下每个数据位的高电平宽度,小于 30us 的是 0,大于 60us 的是 1。如果发现所有位都偏短,说明采样点延时 40us 应该在 DHT11 实际输出上微调,毕竟不同批次传感器时序离散性还是存在的。

第二个验证方法是连续采集统计。在 main 循环里连续读 50 次,每次间隔 500ms,打印出温度最大值和最小值。稳定合格的 DHT11 测试环境下温度波动应在 ±1℃ 内,湿度波动在 ±2% 内。如果数值跳动过大,优先检查上拉电阻是否接触良好,再检查临界区是否真的包住了整个读取函数。

进阶用法是把这次按键中断触发的刷新逻辑改成定时器触发。比如用 TIM2 产生 2 秒的更新中断,在定时器中断里置g_timer_flag,主循环消费标志并从 DHT11 读取数据。注意定时器中断优先级要低于 DHT11 的临界区需求,因为临界区本身会屏蔽它,所以中断优先级设置不影响正确性,只影响响应延迟。我还建议在读取失败时保留上次有效数据,并且用清屏加0xFE光标归位的方式刷新,这样即使偶发失败也不会让 LCD 画面闪烁。

这个资源里还预留了一个小功能接口:在main.c的循环里,你可以用同一个 KEY 按键切换摄氏度和华氏度显示,只需要在按键标志生效后多判断一个模式变量。华氏度换算公式是F = C * 9 / 5 + 32,注意用整数运算时先乘后除避免精度丢失。

从那以后我每次做 DHT11 相关的工程,都会强制自己走一遍逻辑分析仪抓波形,哪怕不开机也得先量一下上拉电阻。芯片手册里的时间参数是死的,但你的信号线上的实际波形才是唯一真相。希望这个资源里的临界区处理、超时保护和中断瘦身思路能帮到你,少踩我当初踩过的那些坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询