资讯详情 STM32综合实战:DHT11温湿度、按键中断与LCD显示全解析
📅 2026/10/14 17:02:32
简介这套STM32工程面向嵌入式初学者与STM32F10x开发者以STM32F103ZET6为主控整合DHT11温湿度采集、按键外部中断、LCD实时显示与呼吸灯效果是学习裸机外设驱动和中断编程的完整实例。包内共209个文件以H/C源码、Keil工程配置(uvprojx/uvoptx)和启动汇编为主附带编译生成的axf/hex/map等产物、Python辅助脚本、批处理及说明文档压缩包仅7.11MB结构便于按模块查阅。目前已有296人学习适合对照工程理解单总线通信时序、GPIO/EXTI中断配置、LCD驱动初始化和定时器PWM控灯等关键环节。通过阅读LCD驱动(bsp_ili9341_lcd.c)、定时器与系统初始化代码可以快速掌握外设分层封装方法并在开发板上验证温湿度显示与按键交互效果。1. 这个zip到底是什么把DHT11、按键、中断和LCD揉进一块ZET6板子打开这个压缩包你大概率会看到一堆以main、gpio、dht11、lcd、key命名的源文件标准的裸机工程结构。它的本质是一个很典型的外设综合训练STM32F103ZET6作为主控用DHT11读温湿度用外部中断接按键再把结果刷到LCD上。看起来功能简单但这类工程真正的难点不在C语言而在DHT11那种娇气的单总线时序以及它和中断之间天然的冲突。适合谁刚把GPIO点灯跑顺、想串外设的初学者或者要做课程设计但不想只会抄代码的人。反直觉的地方在于整个系统里最慢的传感器反而逼着你学会处理中断优先级、临界区这类偏底层的功夫。本文按硬件接线、代码实现、中断博弈、踩坑清单的顺序把这个工程从能看懂讲到能改跑。2. 先搭硬件ZET6的外设分配、DHT11单总线原理与LCD接线2.1 一张表格理清引脚与中断线分配做这类工程第一步不是写代码是先把芯片的资源盘清楚。STM32F103ZET6是144脚的大容量型号外设多到用不完但这不代表可以随手乱接。我的习惯是先列一张外设分配表把GPIO、SPI、EXTI、上拉电阻全部定下来再接一根线看一眼原理图。下面这张表是这类工程最常见的接法你手里的板子如果引脚不同照着改宏定义就行。外设引脚模式说明DHT11 DATAPB11开漏输出 外部4.7k上拉单总线数据线读时不切换模式KEY1PA0上拉输入按下接地对应EXTI0KEY2PA1上拉输入按下接地对应EXTI1LCD SCKPA5复用推挽输出SPI1时钟LCD MOSIPA7复用推挽输出SPI1主机输出LCD CSPB0推挽输出片选低有效LCD DCPB1推挽输出数据/命令选择LCD RSTPB2推挽输出复位低有效这里有两个选择要解释。第一DHT11数据线为什么用开漏而不是推挽因为单总线在空闲时必须被上拉电阻拉高开漏输出天然支持线与主机释放总线时引脚自动被外部电阻拉高读数据时直接读输入寄存器不需要在输入和输出之间来回切换模式。推挽方案要写切换方向的代码切换瞬间还容易产生毛刺开漏明显更干净。第二按键接PA0和PA1是有讲究的这两个引脚对应EXTI0和EXTI1两条独立中断线两个按键可以分别进中断不用在同一个中断里多花判断逻辑。LCD的SPI1引脚复用是固定的PA5只能做SCKPA7只能做MOSI这是芯片内部AF映射决定的改不了。2.2 DHT11单总线协议为什么它比I2C、SPI都娇气DHT11用的是单总线协议一根线上既有主机发的起始信号又有从机回的响应和数据全部靠时序的宽窄区分。一次完整通信大概是这样的主机先把总线拉低至少18ms然后释放并拉高20到40usDHT11收到起始信号后先拉低80us响应再拉高80us之后开始发送40bit数据。这40bit里每一位都是50us的低电平开头关键区别在后面的高电平宽度高电平持续26到28us是逻辑0持续70us左右是逻辑1。问题就出在这个高电平宽度上。拿逻辑0来说高电平只有不到30us如果主机在读取过程中被一个高优先级中断打断哪怕只耽误几十个微秒位就判错了整个40bit全乱校验和永远过不了。相比之下I2C有SCL时钟线做同步SPI有主机时钟打拍子DHT11什么都没有它把时序的准确性完全托付给主机代码。这也是为什么这个工程会把中断单独拿出来做文章——不是按键需要中断而是DHT11在读数据期间必须想办法不被中断干扰。数据格式是固定的5字节湿度整数、湿度小数基本恒为0、温度整数、温度小数、校验和。校验和等于前四个字节相加比如两个整数位都是0x28校验和就该是0x50对不上就说明这帧数据坏了直接丢弃。读取间隔至少要1秒以上我习惯用2秒因为DHT11内部的采样周期本来就慢读太频繁不仅数据不变还容易把传感器累出毛病。2.3 LCD与按键的接线SPI分频、上拉电阻和供电LCD这块这类工程里最常见的是SPI接口的TFT彩屏驱动IC可能是ILI9341、ST7789或ST7735。三种IC的命令表有差异接线方式倒是统一的SCK、MOSI、CS、DC、RST五根线加电源。上电时要特别注意RST引脚屏幕驱动IC需要复位信号一般要求低电平脉冲宽度不低于10us但我习惯给到20ms宁多勿少。另一个容易翻车的是SPI速率ZET6的SPI1最高可以跑到18MHz但屏幕驱动IC未必撑得住很多屏在超过8MHz时就开始花屏我一般初始化时分频到2MHz左右确认显示正常后再慢慢提频。按键的接线相对简单两个按键一端接PA0和PA1另一端接GND内部上拉即可。注意不要用外部下拉因为STM32的GPIO默认状态不定内部上拉更可控。这里有个小坑如果板子上按键模块自带LED指示灯那个LED的正极如果直接接3.3V按键按下时LED的电流会串进IO口压降导致电平判断异常。遇到按键不灵的板子先查是不是模块上有跳线或独立供电。供电方面DHT11的工作电压是3.3V到5V都能跑但上拉电阻的值接入3.3V和5V时不一样。3.3V供电建议10k5V供电建议4.7k。有些现成的DHT11模块上已经焊了上拉电阻直接接就行了不需要再外接。如果自己买的是裸传感器务必补上那颗4.7k到10k的电阻否则数据线悬空读数会飘。3. 代码落到工程里DHT11读取、LCD刷新与按键EXTI的实现3.1 先写DHT11驱动起始信号、位读取与超时保护DHT11驱动是这套工程的心脏代码写得好不好直接决定数据可不可信。先看引脚初始化我推荐直接用开漏输出加外部上拉整个驱动里不用切换模式void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_WriteBit(GPIOB, GPIO_Pin_11, Bit_SET); // 初始释放总线 }开漏模式下写1就是释放总线让上拉拉高写0就是强拉低读的时候直接调用GPIO_ReadInputDataBit完全不用改模式。GPIO_Speed设50MHz是因为开漏输出的开关速度影响下降沿质量速度太慢会导致DHT11识别不出起始信号这是很多人忽略的点。然后是起始信号和单字节读取这两个函数是整个驱动最核心的部分static void DHT11_Start(void) { GPIO_WriteBit(GPIOB, GPIO_Pin_11, Bit_RESET); // 拉低总线 delay_ms(20); // 至少18ms给20ms更稳 GPIO_WriteBit(GPIOB, GPIO_Pin_11, Bit_SET); // 释放总线 delay_us(30); // 拉高20-40us取中间值30us } static uint8_t DHT11_ReadByte(void) { uint8_t val 0; uint32_t timeout; for (uint8_t i 0; i 8; i) { timeout 10000; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 0) // 等50us低电平结束 { if (--timeout 0) return 0xFF; // 超时保护防止死等 } delay_us(40); // 延时40us后采样 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 1) val (val 1) | 0x01; // 高电平结束仍为高判为1 else val (val 1); // 否则判为0 timeout 10000; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 1) // 等高电平结束 { if (--timeout 0) return 0xFF; } } return val; }这个读取逻辑的关键在延时40us后采样。DHT11每一位都是50us低电平开头逻辑0的高电平是26到28us逻辑1的高电平是70us左右。等低电平结束后延时40us此时如果是逻辑0高电平已经结束被拉低读到0如果是逻辑1高电平还在持续读到1。这个延时后采样法是单总线读取最经典的写法比测量高电平宽度更省指令但也更依赖delay_us的准确性。timeout超时保护必须加否则DHT11拔线后程序会死在while循环里整个系统瘫痪。完整的读取函数还需拼上响应检测和校验uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5]; uint32_t timeout; DHT11_Start(); timeout 10000; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 1) // 等DHT11拉低响应 { if (--timeout 0) return 1; } timeout 10000; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 0) // 等80us低电平结束 { if (--timeout 0) return 1; } timeout 10000; while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11) 1) // 等80us高电平结束 { if (--timeout 0) return 1; } for (uint8_t i 0; i 5; i) buf[i] DHT11_ReadByte(); if ((buf[0] buf[1] buf[2] buf[3]) buf[4]) // 校验和 { *humidity buf[0]; *temperature buf[2]; return 0; } return 1; }返回0表示读取成功返回1表示通信失败。调用方只需要判断返回值失败就显示上一次成功的数据或者画个问号不要因为一帧失败就清空屏幕。这里有一个工程细节值得注意DHT11的小数位buf[1]和buf[3]几乎永远是0但必须参与校验和计算这是数据格式的一部分不能因为反正都是0就跳过。3.2 LCD显示驱动写命令函数、开窗画点与字符刷新的取舍LCD驱动代码通常很长初始化命令表动辄几十条但核心的函数就三个写命令、写数据、开窗画点。其他所有显示函数都能由这三个组合出来。先看最基本的SPI字节发送和命令/数据区分static void LCD_SendByte(uint8_t dat) { SPI_I2S_SendData(SPI1, dat); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) SET); // 等发送完成 } static void LCD_WriteCmd(uint8_t cmd) { GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_RESET); // CS拉低 GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_RESET); // DC拉低命令 LCD_SendByte(cmd); GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET); // CS拉高 } static void LCD_WriteData(uint8_t dat) { GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_RESET); GPIO_WriteBit(GPIOB, GPIO_Pin_1, Bit_SET); // DC拉高数据 LCD_SendByte(dat); GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET); }DC引脚的高低电平决定了SPI线上传的是命令还是数据这是所有SPI彩屏的通用协议。注意SPI_I2S_GetFlagStatus等待的是BSY标志不是TXE。只用TXE的话最后一个字节可能还没从移位寄存器发完就被下一个操作打断某些屏会出现最后一个像素点花掉的问题这种玄学问题排查起来很费劲。开窗画点是显示的基础。TFT屏的GRAM是一个二维数组写入前要先告诉屏幕要往哪个矩形区域写再连续填充RGB565像素void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); // 列地址设置 LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); // 行地址设置 LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); // 开始写GRAM } void LCD_FillRect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color) { LCD_SetWindow(x0, y0, x1, y1); uint32_t count (x1 - x0 1) * (y1 - y0 1); for (uint32_t i 0; i count; i) { LCD_WriteData(color 8); // RGB565高字节 LCD_WriteData(color 0xFF); // RGB565低字节 } }命令0x2A和0x2B是ILI9341的窗口命令ST7789也兼容。如果屏幕驱动IC是ST7735命令一样但部分屏幕需要先设置偏移量否则显示区域会偏移。显示字符的思路是把ASCII字模按16行乘8列取模逐列扫描时每个bit对应一个像素点1就画前景色0就画背景色。这里最容易踩的坑是取模方式行列式和列行式搞反的话字会横着躺下。我一般用取模软件直接生成列行式数据配合驱动里逐行扫描、每字节低位在前的写法很少出问题。初始化屏幕时序有个细节上电后先延时至少100ms再拉RST低电平低电平保持20ms以上再拉高然后延时50ms才开始发初始化命令表。有些开发板为了省事把RST直接接到主控复位上这样屏幕会跟着主控一起复位也能跑但偶尔会出现屏幕初始化不彻底的白屏遇到就直接给RST单独接IO口。3.3 KEY中断EXTI配置、软件消抖与中断里只置标志位按键用中断还是轮询这个工程里既然写了中断就按EXTI来。配置分三部分GPIO、EXTI、NVIC缺一不可void KEY_EXTI_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; // KEY1接PA0 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); // 映射到EXTI0 EXTI_InitStructure.EXTI_Line 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 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 2; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }GPIO_EXTILineConfig这个函数容易忘。EXTI0可以映射到PA0、PB0、PC0等多个引脚必须事先告诉芯片选哪个端口否则中断永远不触发。按键按下接地是下降沿所以用EXTI_Trigger_Falling如果按键接法相反改成EXTI_Trigger_Rising。中断处理函数里只做一件事置标志位。volatile uint8_t g_key1_pressed 0; void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) ! RESET) { EXTI_ClearITPendingBit(EXTI_Line0); delay_ms(20); // 软件消抖 if (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) 0) { g_key1_pressed 1; // 只置标志位不做显示处理 } } }两个细节要说明。第一清中断标志位要放在最前面边沿触发的中断如果不清标志会不断重入死循环。有些写法先消抖再清标志抖动期间又来一次触发标志位被新事件覆盖按键就丢了。先清再消抖宁可按一次多等20ms也不能漏事件。第二中断里绝对不能直接调用LCD刷屏函数。LCD写一个全屏矩形要循环几千次每次都是SPI阻塞发送整套流程跑完要几十毫秒在中断里做这些等于把系统卡死。正确的做法是置标志位主循环里检测到标志位后再刷屏。关于软件消抖20ms的delay_ms在中断里其实是有争议的。SysTick在中断里依然会走不会卡死但会拉长中断占用时间。更好的消抖方案是用定时器做时间戳进中断时记录当前时刻主循环里判断两次边沿间隔是否超过20ms。这类工程里用delay_ms消抖属于最常见写法能跑但你要知道它的代价。4. 中断的优先级博弈DHT11的临界区保护与按键响应的平衡4.1 NVIC优先级分组抢占优先级和子优先级怎么分这个工程的微妙之处在于按键要用中断DHT11又怕中断。怎么让两者共存答案在NVIC优先级里。STM32的NVIC支持抢占优先级和子优先级抢占优先级高的中断可以打断抢占优先级低的子优先级只在同抢占优先级之间生效。首先要调用一次分组函数这个函数整个工程只能调用一次调用时机放在main函数最开头NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占 2位子优先级Group_2是4级抢占、4级子优先级对这类小型工程来说够用。分好组后按键中断的抢占优先级设为1主循环代码运行在优先级之外裸机主循环是没有任何优先级的所以按键中断能正常打断主循环。到这里还没有问题问题出在DHT11读取函数运行期间如果按键中断恰好来了DHT11的位时序就会被打出毛刺。解决思路有三个按推荐程度排序。4.2 临界区保护读DHT11时关中断的代价与替代方案最粗暴的方案是关全局中断DHT11整个读取流程大约4ms这4ms里把中断全部屏蔽__disable_irq(); // 关闭全局中断 ret DHT11_ReadData(hum, temp); // 4ms内禁止所有中断 __enable_irq(); // 恢复这个方案的优点是简单、可靠4ms内DHT11读到的数据基本不会错。代价是按键在这4ms内的边沿会被漏掉如果恰好按键在这4ms内按下这次触发就丢了。4ms对人不算什么但如果主循环里还有其他需要及时响应的外设比如串口接收这几毫秒就可能丢一个字节。第二种方案更精细只屏蔽按键的EXTI中断不影响其他中断。EXTI-IMR ~(EXTI_Line0 | EXTI_Line1); // 屏蔽两个按键中断 ret DHT11_ReadData(hum, temp); EXTI-IMR | (EXTI_Line0 | EXTI_Line1); // 恢复直接操作EXTI的IMR寄存器把按键对应的中断线屏蔽掉DHT11读取期间任何外设中断照常响应只有按键暂时失效。这个方案我比较推荐因为对这套系统来说串口、定时器、LCD的刷新都比按键那4ms重要。要注意的是操作IMR时要用和|不能直接赋值否则会把其他已经使能的中断线误关掉。第三种方案是把DHT11读取放到定时器中断里做并让定时器中断的抢占优先级高于按键中断这样DHT11读取期间按键中断来了也进不来但其他低优先级中断不受影响。本质上是把临界区伪装成了一个高优先级任务。这个方案代码复杂度高但对优先级编排的理解比前两种深入适合想把这套工程吃透的人。4.3 用定时器状态机读DHT11把4ms的阻塞拆散如果不想用临界区也不想在读取期间牺牲按键响应终极方案是把DHT11的时序拆成状态机交给一个定时器中断驱动。用1个10us的定时器中断每个tick推进一个状态起始信号拉低、拉高、等响应低电平、按位读取。每个状态只做一件事然后立即退出中断其他中断完全不受影响。// 伪代码示意每10us进入一次驱动DHT11状态机 void TIMx_IRQHandler(void) { switch (dht11_state) { case DHT11_START_LOW: DHT11_LOW(); delay_ticks 2000; // 拉低20ms dht11_state DHT11_START_HIGH; break; case DHT11_START_HIGH: DHT11_HIGH(); delay_ticks 3; // 拉高30us dht11_state DHT11_WAIT_RESP; break; case DHT11_WAIT_RESP: if (DHT11_READ() 0) dht11_state DHT11_SKIP_LOW; break; // ... 按位读取状态 } }状态机方案读取一颗完整的数据需要几十次中断每一次中断执行时间只有几微秒对系统的影响几乎可以忽略。但代价是代码量翻倍调试复杂度上升状态转换的边界条件很容易写错。这个工程如果只是课程设计或者入门练习用临界区方案就够了如果按键对你来说特别重要用屏蔽EXTI的方案状态机是进阶玩法不建议作为第一个跑通的版本。优先级分配的具体参数我建议按键中断抢占优先级设1如果系统里还有串口或定时器它们的抢占优先级设2这样DHT11区域屏蔽的是抢占优先级小于等于它的中断而读取期间用USE_EXTI_MASK方案时其他中断完全不受影响。这个配置不是唯一的但它能保证按键永远第一时间响应DHT11有临界区保护系统里其他外设按优先级排队。5. 避坑DHT11读数为0、LCD白屏、按键连跳的排查清单5.1 DHT11一直读0或者校验和总不过现象函数永远返回1或者读出来的温湿度是0串口打印原始数据发现五个字节全一样像卡在某个状态。原因排查顺序先查硬件用万用表量PB11电平空闲时应为3.3V如果为0说明上拉电阻没焊或者线短路。再查软件看起始信号拉低时间够不够18ms——很多移植过来的delay_ms精度有问题实际只有几毫秒。最后怀疑时序被打断把DHT11读取放到临界区试试。解决给PB11补一颗4.7k上拉电阻确认delay_ms(20)实际延时达标读取期间屏蔽按键EXTI。如果还是不行把杜邦线换短一点DHT11的时序对线长敏感超过20cm就很容易翻车。5.2 LCD白屏或者花屏不是屏坏了现象上电后屏幕白屏或者能显示但颜色不对、有条纹。原因白屏大概率是初始化命令表没执行完常见具体原因包括RST拉低时间不够、SPI速率太快、DC引脚接错。花屏多半是窗口命令和像素数据格式不匹配比如屏幕是18位色却按16位写。解决RST低电平保持20ms以上SPI分频调到2MHz确认DC引脚拉高时是数据、拉低时是命令。花屏的话把开窗函数的坐标高低字节对调试试或者检查取模方向。还有一个常被忽略的点屏幕供电3.3V供电不足时背光亮但显示乱码量一下屏的VCC对地电压。5.3 按键按一下触发两次或者一按就卡死现象按键按一次LCD上计数器跳两个数或者按键按下后整个程序卡住不动。原因触发两次是消抖没做好机械按键按下瞬间有十几毫秒的抖动每次抖动都触发下降沿中断。卡死多半是在中断里调用了delay_ms且SysTick没初始化或者中断里死等一个永远不会来的标志。解决消抖用20ms延时重读电平或者用定时器做时间戳。中断里除了清标志和置位其他什么都别干。如果按键是接在PA0上检查GPIO_EXTILineConfig是否正确配置没配置的话中断会一直进不去表现为按键完全无效。5.4 第一次上电读不到数据复位后又正常现象每次上电后第一次读DHT11必然失败按复位键后再读就正常。原因DHT11上电后需要1秒左右完成自检期间不接受任何通信请求。如果主循环上电后立刻发起始信号相当于对着一台还没启动的设备喊话自然没人理。解决上电后先延时至少1秒再读或者把首次读取放在一个1秒定时器回调里。这个坑几乎每个做DHT11的人都会踩一次我一般习惯在系统初始化最后加一句delay_ms(1500)一劳永逸。5.5 关中断读DHT11后后续中断全部失效现象用了__disable_irq()后程序运行正常但按键再也不响应串口也收不到数据。原因__disable_irq()和__enable_irq()必须配对使用。如果DHT11读取函数里有任何一个分支提前返回比如超时返回1中断就永远关着。更隐蔽的情况是关中断期间调用了delay_ms而delay依赖SysTick中断SysTick被关之后delay死等程序卡在延时里出不来表面看像死机实际是中断被自己关了。解决用临界区时务必在函数体内统一出口或者用EXTI-IMR屏蔽方案代替全局关断。我写DHT11驱动时会在__enable_irq()后面加一行__NOP()调试断点看能不能执行到执行不到就说明中间有提前返回路径。6. 验证这套系统可信再做三个进阶改造先把验证做完再谈进阶。第一个土办法连续采集50组数据串口打印原始五字节加校验结果看有没有间歇性失败。如果失败率超过1%先别怀疑传感器回头查时序。第二个办法把DHT11放进塑料袋对着传感器哈气湿度应该快速上升10%以上温度缓慢升高如果湿度纹丝不动说明驱动读的根本不是DHT11。第三个办法拿一个标准温湿度计放旁边对比DHT11湿度允许误差正负5%RH温度正负2摄氏度在这个范围内波动就是正常的别拿实验室仪器的精度去要求一颗几块钱的传感器。验证通过后改造方向我建议三个。第一把DHT11的小数位用起来它虽然大多时候是0但某些型号在特定湿度区间会输出非零值直接把buf[1]和buf[3]拼进显示逻辑显示精度会好一点点。第二把最近一次成功的数据存入Flash上电后先显示旧数据再刷新这样传感器故障时屏幕不会一片空白用户感知会好很多。第三LCD刷新加个局部刷新策略只在温湿度变化超过1个单位时才重画数值区域避免整屏清空再重写的闪烁感也让SPI总线没那么忙。我印象最深的一次翻车是在一个类似工程里DHT11死活读不对最后发现是一根杜邦线内部快断了接触电阻导致信号幅度不对。从那以后我养成了一个习惯任何传感器数据不稳定先拿万用表量线再怀疑代码。这套工程值得做它把嵌入式最基础的几块硬骨头一次性凑齐了跑通它你对GPIO、中断、时序、驱动的理解会扎实很多。希望这篇能帮到你。本文还有配套的精品资源点击获取