ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

HAL库实战:DHT11温湿度传感器+OLED显示完整工程

HAL库实战:DHT11温湿度传感器+OLED显示完整工程

简介:基于STM32 HAL库的DHT11温湿度采集与OLED显示工程,以STM32F0系列为硬件平台,适合物联网方向学生与嵌入式入门开发者,用于掌握环境监测节点的完整实现思路。工程围绕DHT11单总线通信和OLED显示两大核心,覆盖HAL库的GPIO、I2C/SPI接口配置、传感器40位数据帧解析、显示驱动初始化等关键环节,可直接从工程文件了解引脚分配与时钟配置。压缩包含193个文件,以59个.h、29个.c源码为主,另有Keil工程配置(.uvprojx)、CubeMX图形化配置(.ioc)、编译产物(.hex/.axf)及.o、.d、.map和.lst等中间文件,整体约6.9MB,便于对照源码检查构建过程。目前已有772人学习下载。参考该工程可以快速搭建温湿度实时显示环境,掌握DHT11时序读取与校验思路,理解OLED驱动的文本坐标设置与刷新逻辑,并复用到后续物联网设备的驱动层开发中,尤其适合入门阶段对照实物调试。

1. 肉眼可见的数据才有意义:HAL库DHT11温湿度+OLED显示小工程

做物联网节点会被DHT11“教育”一次,才算真正碰过单总线时序。手里这套基于HAL库的温湿度传感器+OLED显示的小项目,是一个能直接抄的本地传感器面板:MCU兼任“读数据的人”和“画屏幕的人”,按微秒级时序把DHT11的40 bit数据咬出来,再解析校验后刷到0.96寸I2C OLED上。它适合刚把图形化配置工具跑通、准备往物联网方向走的开发者,也适合给产品原型做本地调试屏。项目不大,却把HAL库的极限延时、单总线读时序、I2C驱动、OLED页面刷新这四件事串在了一起;看完不仅能跑通,还能把这套“读-解析-显示”框架换个传感器继续用。

2. 硬件连接与工程框架:单总线读数据、I2C出画面的基础

2.1 硬件清单与接线:四根线把数据链搭起来

这个项目的物料非常少。一块带HAL库支持的Cortex-M内核开发板、一个DHT11模块、一块0.96寸I2C接口的OLED屏幕,加上面包板和杜邦线就能开工。整个系统就两个传感器设备加一颗主控芯片,没有多余的外围电路,非常适合当第一个亲手调通的物联网本地节点。

| 物料 | 数量 | 说明 | | 开发板 | 1 | 任意Cortex-M0+/M3内核MCU开发板,HAL库支持,工作电压3.3V | | DHT11模块 | 1 | 三引脚(VCC/GND/Data),部分模块四引脚带一个空脚NC | | OLED 0.96寸 | 1 | SSD1306驱动,I2C接口,默认地址0x3C | | 上拉电阻 | 1 | 4.7k~10k,接在Data与VCC之间;若模块板载则免 | | 面包板/杜邦线 | 若干 | 调试期快速改线用 |

接线关系也很固定:DHT11的VCC接3.3V,GND接GND,Data接MCU的一个GPIO(比如PA0);OLED的SCL接I2C时钟线(比如PB8),SDA接I2C数据线(比如PB9),VCC和GND同样并到3.3V电源轨上。两块器件共用同一条3.3V电源轨,注意别接到5V,DHT11虽然标称3.3到5V都能跑,但OLED模块多数只支持3.3V,电压不一致很容易把屏幕烧掉,这种翻车经历我已经听过不止一次了。

关于Data线上的上拉电阻,这是单总线协议的物理基础。总线空闲时靠上拉电阻保持高电平,DHT11靠拉低电平来写0和应答;如果不加上拉,读回来的bit边沿会变得很钝,轻则偶尔校验错,重则整包数据全是0xFF。我一般建议用10k,功耗低,时序也够稳。模块如果板载了上拉电阻就不必再外接,但你要确认板子上那颗电阻真的焊上了,有些便宜的模块会省掉这颗料。

2.2 技术栈与工程结构:为什么HAL库要配手工微秒延时

软件层面用的就是HAL库加一套自己写的驱动。HAL库最常用的延时是阻塞式的毫秒延时,但DHT11的时序要求是微秒级:启动信号拉低要维持18ms以上,应答等待是20到40微秒,每一位数据的高电平持续时间也只有几十微秒。毫秒级延时没法精确表达这些时间窗口,所以驱动里必须自带一个微秒延时函数,这是这个项目里最容易被忽视的技术选型。

常见做法是用一个通用定时器做阻塞延时:开启定时器后读计数器值,达到目标微秒数就退出。也可以利用内核的DWT单元做周期计数,不额外占用定时器。我一般用定时器方案,因为图形化配置工具里勾选时钟源后就能生成初始化代码,逻辑直观,换芯片时改动也小。生成工程时记得勾选“每个外设独立生成.c/.h文件”的选项,后面加传感器驱动时不用翻大文件找初始化代码。

工程结构上,我把HAL库工程分成两层。底层驱动文件是自建的,放在用户代码目录里,刻意不塞进配置工具管理的目录,这样重新生成工程时不会被覆盖。

project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── dht11.h │ │ └── oled.h │ └── Src/ │ ├── main.c │ ├── dht11.c │ └── oled.c ├── Drivers/ │ ├── CMSIS/ │ └── Vendor_HAL_Driver/ └── user/ └── delay.c

Core里是配置工具生成的入口和中断文件,Drivers里是HAL库源码,user目录放自己写的微秒延时和通用小工具。dht11.c和oled.c放在Core/Src下的原因是编译链默认会搜这个目录,少改一处工程配置。HAL库版本之间有些宏定义差异,比如GPIO方向切换的接口在不同版本里长得不一样,但整体写法一致,照着现有API改就行。

2.3 这套组合为什么适合做物联网节点的本地面板

物联网节点最常见的问题,就是云端收到一个异常数据,但分不清是传感器坏了、协议解析错了还是网络传输丢了包。给节点配一块OLED本地屏,等于给数据链加了一个旁路观测点:传感器读出来什么,屏幕立刻显示什么,无线模块只负责转发屏幕上的同一份数据。这样排查问题时,一眼就能看出故障出在哪一段。

这个项目就是典型的“传感器+显示”最小闭环。DHT11负责采集,HAL库跑在MCU里负责解析,OLED负责呈现。后续如果接Wi-Fi模组或4G模组,只需把解析好的温湿度结构体序列化成JSON发出去,本地显示逻辑完全不用动。这也是我推荐先把显示环节做扎实的原因——它让传感器数据从“黑匣子”变成了看得见的东西,在物联网项目里,可视化永远是第一块拼图。

3. 核心代码拆解:从DHT11的微秒时序到OLED的页面刷新

3.1 单总线启动时序:先把DHT11从睡眠里叫醒

DHT11的读操作以一次明确的握手开始。主机先把数据线拉低至少18ms,DHT11检测到这个持续低电平后才会准备好数据;随后主机拉高20到40微秒释放总线,DHT11会主动把总线拉低约80微秒表示应答,再拉高80微秒后开始输出40位数据。整个过程里,主机在拉高后必须把GPIO从输出模式切换到输入模式,否则读不到DHT11发来的电平。

HAL库里切换GPIO方向并不像标准库那样直接改寄存器,而是重新初始化GPIO。下面这段启动函数放在每一次读数据之前:

void DHT11_Start(void) { // 配置为开漏输出,低电平有效 GPIO_InitTypeDef gpio_out = {0}; gpio_out.Pin = DHT11_Pin; gpio_out.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 gpio_out.Pull = GPIO_PULLUP; // 外部上拉为主,内部上拉作兜底 gpio_out.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_Port, &gpio_out); HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_RESET); Delay_us(20000); // 拉低20ms,超过18ms的最低要求 HAL_GPIO_WritePin(DHT11_GPIO_Port, DHT11_Pin, GPIO_PIN_SET); Delay_us(30); // 拉高30us后释放总线 // 切回输入模式,准备读DHT11的应答信号 GPIO_InitTypeDef gpio_in = {0}; gpio_in.Pin = DHT11_Pin; gpio_in.Mode = GPIO_MODE_INPUT; gpio_in.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT11_GPIO_Port, &gpio_in); }

这里有几个参数值得说清楚。拉低延时20ms而不是刚好18ms,是给DHT11的启动检测留余量;拉高延时30us落在20到40us窗口内。开漏输出模式配合外部上拉电阻实现“线与”,是多设备共享总线的标准接法,在单总线上既当发送方又当接收方时必须用这个模式。切换输入模式后立刻开始读应答,不要浪费时间在日志输出上,否则会错过DHT11拉低的那80us窗口。

3.2 逐位读取:用高电平持续时间区分0和1

DHT11输出的每一位都以50us低电平开始,然后是高电平。逻辑0的高电平持续26到28us,逻辑1的高电平持续约70us。所以读取策略很简单:跳过低电平,测量高电平的时间长度,超过阈值就判定为1,否则为0。这套判定方式本质上是在数电平的宽度,而不是捕捉边沿,对时序抖动有比较好的容忍度。

uint8_t DHT11_ReadBit(void) { uint32_t timeout = 0; // 等待低电平结束:每一位数据都以50us低电平打头 while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) { if (++timeout > 300) return 0xFF; // 超时保护,避免死等 Delay_us(1); } // 测量高电平持续时间 uint32_t high_time = 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { high_time++; Delay_us(1); if (high_time > 100) break; // 超过100us说明时序异常 } return (high_time > 50) ? 1 : 0; }

阈值的选取是这段代码的关键。0的高电平约28us,1的高电平约70us,取中间值50us能把两类信号干净地分开。延时1us的函数本身有误差,但引脚电平的边沿抖动和延时误差叠加后仍远小于50us的判定窗口,所以实际跑起来很稳。两个while循环都加了超时保护,一旦DHT11没接好或总线被卡死,函数返回0xFF而不是让整个系统死循环,这对嵌入式代码来说是最低限度的自我保护。

3.3 数据解析与校验:40位数据拼出温度和湿度

一次完整读取得到40位数据,按顺序排列:湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验字节8位。校验字节等于前四个字节相加后取低8位。这个校验能挡住大部分时序错乱产生的坏数据,是DHT11驱动里最后一道防线。

typedef struct { uint8_t humi_int; uint8_t humi_dec; uint8_t temp_int; uint8_t temp_dec; } DHT11_Data; uint8_t DHT11_ReadData(DHT11_Data *data) { uint8_t buf[5] = {0}; DHT11_Start(); // 应答信号:DHT11拉低80us后拉高80us,跳过这段 uint32_t wait = 0; while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_RESET) { if (++wait > 300) return 1; // 无应答,检查接线 Delay_us(1); } while (HAL_GPIO_ReadPin(DHT11_GPIO_Port, DHT11_Pin) == GPIO_PIN_SET) { if (++wait > 300) return 1; Delay_us(1); } for (int i = 0; i < 40; i++) { uint8_t bit = DHT11_ReadBit(); if (bit == 0xFF) return 3; // 读位超时,放弃本次数据 buf[i / 8] <<= 1; buf[i / 8] |= (bit & 0x01); } if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) return 2; // 校验失败,数据不可信 >void OLED_WritePage(uint8_t page, uint8_t *buf, uint8_t len) { uint8_t cmd[4]; cmd[0] = 0x00; // 控制字节:接下来是命令 cmd[1] = 0xB0 + page; // 页地址0~7 cmd[2] = 0x00; // 列地址低4位 cmd[3] = 0x10; // 列地址高4位 HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, cmd, 4, 100); // 写数据:控制字节0x40,后面是显示内容 uint8_t data[129]; data[0] = 0x40; memcpy(&data[1], buf, len); HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR, data, len + 1, 100); }

为什么要先发4字节命令再发数据?因为在页地址模式下,SSD1306的列地址不会自动回到0,必须每次先指定页地址和列起始地址,否则连续刷新时内容会依次错位。参数page范围是0到7,对应屏幕从上到下的8页;len最大128字节,正好是每页的宽度。函数内部临时开了129字节的栈缓冲,在内存紧张的MCU上也能接受,但如果要刷大面积画面,建议把缓冲提到函数外复用。

3.5 刷新策略与主循环:读-解析-显示的调度节奏

显示驱动的另一个坑是刷新闪烁。0.96寸OLED的GRAM只有128x64字节,但I2C在400kHz下传一屏数据也要约20ms。如果每秒刷新一次,数字每秒跳变时屏幕会隐约闪动,这在室内仪表上很刺眼。所以我在刷新前加了变化检测:温湿度没变化就不重新写GRAM。

void OLED_ShowDHT11(DHT11_Data *d) { static uint8_t last_temp = 0xFF, last_humi = 0xFF; // 数据没变化就不刷新,避免整屏闪烁 if (d->temp_int == last_temp && d->humi_int == last_humi) return; last_temp = d->temp_int; last_humi = d->humi_int; char line[20]; snprintf(line, sizeof(line), "Temp:%d.%dC", d->temp_int, d->temp_dec); OLED_ShowString(0, 0, line); snprintf(line, sizeof(line), "Humi:%d.%d%%", d->humi_int, d->humi_dec); OLED_ShowString(2, 0, line); }

static局部变量在函数退出后仍保留数值,适合做这种轻量级变化检测。这里只比较整数部分,因为DHT11的温度分辨率是1度、湿度分辨率是1%,小数位基本恒为0,只比较整数位可以减少无意义的屏幕刷新。OLED_ShowString的第一个参数是页码,0和2分别对应屏幕的上两行,这样只重写两页而不动整个GRAM。

主循环的调度节奏是整条数据链的节拍器:

while (1) { DHT11_Data d; if (DHT11_ReadData(&d) == 0) { OLED_ShowDHT11(&d); printf("temp=%d.%d humi=%d.%d\r\n", d.temp_int, d.temp_dec, d.humi_int, d.humi_dec); } HAL_Delay(1000); // DHT11采样周期最短约1秒,读太频繁反而失败 }

读失败时不显示也不打印,直接等下一个周期。这样做的好处是OLED上永远保留最后一次成功的数据,不会因为一次偶发校验失败而清屏或显示乱码。HAL_Delay(1000)保证DHT11有充足的采样间隔,同时让串口日志保持每秒一行的可读节奏。

4. 编译烧录与运行验证:从配置工具引脚到串口日志

4.1 引脚规划与外设初始化:先分配好硬件资源

在配置工具里把工程初始化搭好,比手写寄存器快得多。需要配置的外设有四个:一个GPIO用于DHT11的数据线,一组I2C用于OLED,一个通用定时器用于微秒延时,一个USART用于串口日志。下表是我常用的分配方案,引脚编号只是示例,换别的引脚要同步改代码里的宏定义。

| 外设 | 引脚 | 关键配置 | | GPIOA Pin0 | PA0 | 开漏输出、上拉、高速,DHT11 Data | | I2C1 | PB8/PB9 | 标准模式或快速模式,目标400kHz | | TIM1 | 内部时钟 | 预分频72-1,计数器向上计数 | | USART1 | PA9/PA10 | 115200, 8N1 |

GPIO初始化为开漏输出的原因在上一章说过:单总线需要线与能力。I2C的速率建议直接上400kHz,OLED的SSD1306完全支持快速模式,用的还是阻塞式传输,速度越快留给主循环的时间越多。定时器预分频按主频计算,如果MCU主频是72MHz,预分频72-1后计数器每加1正好是1微秒,延时函数里读到的计数器值就是已经过去的微秒数。

4.2 微秒延时与printf重定向:让日志开口说话

配置好定时器后,微秒延时的实现就是把计数器清零,然后阻塞等待计数到达目标值。这个函数简单直接,但要注意它会让CPU空转,所以只用于传感器时序这类短延时场景。

void Delay_us(uint32_t us) { __HAL_TIM_SET_COUNTER(&htim1, 0); HAL_TIM_Base_Start(&htim1); while (__HAL_TIM_GET_COUNTER(&htim1) < us); HAL_TIM_Base_Stop(&htim1); }

HAL_TIM_Base_Start和Stop放在延时的首尾,是因为这个定时器专用于延时,不承载其他功能;每次调用都启动和停止,不会有计数溢出干扰。如果us超过65535,计数器会回绕,所以调用方要保证传参不超过这个范围,DHT11时序里最大的延时是20ms,远低于上限。

printf重定向在HAL库里有现成套路:重写fputc函数,把字符通过串口发出去。这样整个工程里的printf都能往串口打,调试传感器时序时特别方便。

int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 100); return ch; }

重定向之后,串口助手里看到的每一行都对应一次完整的读-解析-显示调度。如果printf输出乱码,先查波特率是不是115200,再查串口助手的编码格式,这两个问题出现的频率远高于配置错误。

4.3 烧录与运行验证:从串口数据到屏幕显示的完整流程

焊好线、编译、下载、复位之后,按下面几步走,每一步都有明确的预期结果:

  1. 打开串口助手,设115200波特率,观察输出。正常情况下每秒钟应该有一行temp和humi数据,数值稳定不跳动。
  2. 用手捏住DHT11外壳,体温会让温度在几秒内上升1到2度,湿度也会因手汗明显变化。
  3. 用嘴对DHT11哈气,湿度应该快速上升,数据里能够直接看到变化。
  4. 观察OLED是否跟随串口同步变化,OLED显示与串口一致,说明读-解析-显示链路完整。

如果串口有输出但OLED不亮,先检查I2C地址是0x3C还是0x3D;如果OLED亮但数据不动,大概率是DHT11的GPIO方向切换或上拉电阻没接对。这两类问题占了新手调试时间的八成。

4.4 串口日志如何帮我们定位故障

运行稳定的日志长这样:

temp=25.0 humi=45.0 temp=25.0 humi=45.0 temp=25.3 humi=46.0

每行间隔1秒,数值在小范围内波动。如果出现下面的情况,对应关系很明确:日志完全没有输出的,查串口重定向和USART引脚是否被占;输出全是“temp=0.0 humi=0.0”的,查DHT11应答和GPIO方向切换;间歇性缺行的,说明上次读取校验失败,查上拉电阻和读取间隔。这些规律可以直接写进自己的调试清单里,下次遇到类似问题不用重新猜。

5. 避坑指南:单总线时序、上拉电阻与OLED刷新的那些坑

5.1 读出来全是0或255:先量上拉,别急着改代码

现象:串口稳定输出temp=0.0 humi=0.0,OLED跟随显示0.0,接线检查了好几遍还是没变化。

原因:DHT11的数据线没有上拉电阻。开发板内部上拉默认可能是关闭或弱上拉,DHT11驱动时靠外部上拉维持空闲高电平。没有上拉,读回来的电平被引脚寄生电容拉着走,时序全乱。

解决:在Data和VCC之间接一个4.7k到10k的电阻。如果是洞洞板模块,先确认模块背面有没有贴片上拉,有的模块省了这颗电阻,必须自己补。接上之后复位再读,数值立刻正常。从那以后我拿到任何单总线传感器,第一步永远是看原理图确认上拉,而不是先写代码。

5.2 第二次读取就开始失败:20ms启动时序被毫秒延时吞掉了

现象:运行时第一个数据100%正确,之后校验错误率飙升,甚至完全读不到应答,重启又正常几秒钟。

原因:把HAL_Delay当成万能延时用在启动信号里。HAL_Delay的最小粒度是1ms,而DHT11要求拉高20到40us后立刻读应答,毫秒级延时实际delay出来的时间可能超过1ms,DHT11早已输出完应答信号。

解决:所有微秒级时序一律走定时器延时函数,毫秒级的调度才用HAL_Delay。在主循环的1000ms等待里用HAL_Delay没问题,但在DHT11_Start和逐位读取里必须用Delay_us。给代码加注释提醒后来者不要“优化”成HAL_Delay,这个坑我已经踩过两回,每次都是同样的表现。

5.3 OLED花屏或显示一半:I2C地址和写命令时序背锅

现象:屏幕能亮,但数字和单位花掉,或者只显示上半屏,下半屏是噪点,动态刷新时错位更明显。

原因:第一,SSD1306模块的I2C地址有两种,0x3C和0x3D,由模块背面的电阻选择,地址不对会连设备都扫描不到。第二,写命令时没有区分命令和控制字节,SSD1306把数据误当成命令执行,GRAM内容全乱。

解决:先用I2C扫描程序确认设备地址,再核对OLED_WritePage里的控制字节。命令控制字节是0x00,数据是0x40。另外每次写GRAM前要重新设置页地址和列地址,不能靠上一次的指针位置,否则连续刷新会出现内容错位。

5.4 屏幕像水波纹一样闪:整屏刷新的代价

现象:OLED每秒刷新一次,数字变化时整个屏有肉眼可见的闪烁和拖影,看久了眼睛难受。

原因:每次刷新都顺序写完8页、128列,I2C传一整屏数据需要约20毫秒。在这20毫秒里,屏幕不同区域被先后改写,人眼看到的就是从上到下的滚动刷新,传感器数据本身没问题,纯粹是显示节奏没控制好。

解决:引入变化检测,数据没变就不刷;需要更新时只刷变化的页面。OLED的显示内容固定在屏幕上方两行时,只写第0页和第2页。如果一定要全屏刷新,先关显示、更新GRAM、再开显示,能明显减轻闪烁。这个局部刷新策略尤其适合仪表类界面,数据变化越快收益越大。

5.5 数值突然跳变又恢复:读取间隔低于DHT11的采样周期

现象:日志里偶尔出现一次温度从25跳到80再跳回来,间隔几十秒一次,没有规律,校验和也没有报错。

原因:DHT11内部采样周期约1到2秒,如果主循环读得太快,会读到传感器上一次数据的残留电平或不完整数据。校验和虽然挡掉了大部分坏包,但个别情况下坏包正好通过了校验和,坏数据就漏到了显示层。

解决:严格控制读间隔,每秒最多读一次。校验失败不要立即重试,直接放弃本次结果,等1秒后的下一个周期。数值跳变时,在显示层加一阶滤波:新值与前值偏差超过5度就连续读两次确认。这套处置之后,日志里再没出现过跳变,关键是让调度节奏尊重传感器本身的物理特性。

6. 二次开发思路:把温湿度计改装成物联网上报节点

6.1 先给MCU加一个“会说话的嘴”

改造的第一步是把解析好的数据搬出MCU。常见做法是在串口上接一个Wi-Fi模块或4G模组,把DHT11读到的温湿度组成JSON字符串发到云端。OLED继续留在本地,做调试期的观测窗。数据流变成“DHT11→MCU→OLED和通信模组”,显示和上报共用同一份解析结果。

void SensorPublishTask(void) { DHT11_Data d; if (DHT11_ReadData(&d) == 0) { char json[64]; snprintf(json, sizeof(json), "{\"temp\":%d.%d,\"humi\":%d.%d}", d.temp_int, d.temp_dec, d.humi_int, d.humi_dec); MQTT_Publish("sensor/device01", json); // 本地显示与上报使用同一份数据,云端错了本地能对账 OLED_ShowDHT11(&d); } }

这个函数把读、显、传三件事放在一起,数据只在一个地方解析,杜绝了两套代码各自读一遍导致数值不一致的问题。MQTT_Publish是一个很薄的封装,底层对接哪个模组取决于你手上的硬件,但上层接口不变,后续换通信方式不用动业务代码。

6.2 低功耗改造:读一次睡一次

如果节点用电池供电,HAL_Delay阻塞等待的1000ms就成了浪费。改法是把调度权交还给RTC:定时器唤醒后读一次数据、刷一次屏、发一次包,然后进Stop模式。DHT11上电初始化约1秒后才能读,所以唤醒后先等DHT11稳定再启动读取时序。

void RTC_WakeupCallback(void) { HAL_GPIO_TogglePin(LED_Pin); DHT11_Data d; if (DHT11_ReadData(&d) == 0) { OLED_ShowDHT11(&d); MQTT_Publish("sensor/device01", data_to_json(&d)); } // 回到Stop模式,等下一个RTC周期 }

低功耗改造时有个容易被忽略的点:OLED的SSD1306即使不显示也在耗电,进去Stop模式前要把I2C总线释放、给OLED发一条关显示命令,能省下几毫安电流。我在一次改造中实测,关显示后整机待机电流降了一半,这个收益比优化DHT11时序大得多。

6.3 换DHT22的适配思路

这套框架换DHT22时,单片机部分几乎不用改,要改的是时序阈值和数据结构。DHT22同样是单总线,但0和1的高电平持续时间更短,启动拉低至少1ms即可,读取间隔可以缩短到2秒。把DHT11_ReadBit里的50us阈值调整为DHT22对应的阈值,再把数据解析从整数小数拆成16位带符号温度即可。这也是当初我把驱动单独拆成.c/.h文件的原因——传感器换了,HAL库的I2C和GPIO那部分完全不动,只动数据处理这一层。

做到这里,一个能显示、能上报、能低功耗运行的温湿度节点就成型了。我后来做每个传感器节点都强制先搭本地屏加串口日志,再上无线,不然云端数据错了根本分不清是传感器坏了还是协议写错了。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进