1. 实验背景与工程目标
在嵌入式物联网系统中,环境参数采集与云端上报构成典型的数据闭环。本实验聚焦于 STM32 平台下 DHT22 温湿度传感器数据的精确读取与定时触发机制构建,为后续 MQTT 协议报文(PUBLISH)上传至阿里云 IoT 平台奠定坚实的数据源基础。
本实验并非孤立功能验证,而是承接第6讲实验11的完整项目演进:在已实现 Wi-Fi 连接、MQTT 客户端初始化及 SUBSCRIBE 订阅功能的基础上,新增两个关键模块——定时器2(TIM2)驱动的周期性任务调度机制与DHT22 传感器数据采集模块。二者协同工作,形成“定时唤醒→传感器采样→数据校验→准备上报”的最小可行数据链路。
需明确的是,该设计严格遵循嵌入式系统资源约束原则:
- 不引入额外 RTOS 依赖,全部基于 HAL 库 + 中断 + 主循环协作模型;
- 定时器选择非随意行为,其选型直接受限于系统已有外设占用状态与中断优先级拓扑;
- DHT22 驱动不采用阻塞式轮询,而通过 GPIO 输入捕获+精确延时组合实现单总线协议解析,兼顾精度与 CPU 占用率。
整个流程最终服务于一个明确工程目的:确保每 30 秒可稳定获取一组包含小数位的温湿度原始值,并将其无损传递至后续 MQTT 数据封装环节。任何环节的偏差都将导致 PUBLISH 报文内容失真或发送时机失控。
2. 定时器2(TIM2)模块移植与配置
2.1 外设选型依据与硬件约束分析
STM32F103 系列 MCU 共提供 4 个通用定时器(TIM2–TIM5)与 2 个高级控制定时器(TIM1、TIM8)。在本项目中,TIM3 已被用于实现系统心跳或 LED 闪烁等基础时序任务,其 NVIC 中断通道(IRQn = TIM3_IRQn)已被占用且优先级已固定。
选择 TIM2 而非 TIM4 或 TIM5 的核心原因在于:
-中断向量独立性:TIM2 使用独立 IRQn(TIM2_IRQn),与 TIM3 无冲突,避免中断嵌套复杂度;
-时钟树路径一致性:TIM2 与 TIM3 同属 APB1 总线,预分频器与计数器配置逻辑完全复用,降低移植风险;
-引脚复用自由度高:TIM2_CH1 默认映射至 PA0,当前未被其他外设(如 UART、SPI)占用,保留扩展可能性。
若强行选用 TIM4,则需重新评估其在 CubeMX 中的时钟使能状态、NVIC 分组设置是否与现有 TIM3 配置兼容——这将引入非必要调试成本。因此,“参照 TIM3 移植 TIM2”是符合工程最小改动原则的理性决策。
2.2 源码级移植操作规范
移植过程本质是 HAL 库驱动层的符号替换,需覆盖全部代码上下文,杜绝遗漏:
- 头文件引用修正
在tim.h中,将原#include "stm32f1xx_hal_tim.h"后续所有 TIM3 相关宏定义替换为 TIM2:
```c
// 原 TIM3 定义
#define htim3 htim2 // 关键:重命名句柄指针
extern TIM_HandleTypeDef htim3;
// 替换后
extern TIM_HandleTypeDef htim2;
```
初始化结构体参数同步
在tim.c的MX_TIM2_Init()函数中,确保htim2.Instance指向TIM2,而非TIM3:c htim2.Instance = TIM2; // 必须显式指定 htim2.Init.Prescaler = 7199; // 72MHz / (7199+1) = 10kHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 29999; // 10kHz / 30000 = 333.33Hz → 30s溢出需软件计数注:此处 Period 设置为 29999 是为实现 30 秒定时的折中方案。因 HAL_TIM_Base_Start_IT() 仅支持单次溢出中断,故需在中断服务函数中维护一个静态计数器,累计 30 次溢出(每次 1 秒)后触发传感器采集。此设计规避了超大 Period 值导致的计数器溢出风险。
中断服务函数重定向
在stm32f1xx_it.c中,必须删除原TIM3_IRQHandler,新建TIM2_IRQHandler:c void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(&htim2); // 调用 HAL 标准中断处理 }
同时,在tim.c中实现对应的回调函数:c void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { static uint8_t sec_counter = 0; sec_counter++; if (sec_counter >= 30) { sec_counter = 0; dht22_trigger_read(); // 触发 DHT22 采集 } } }NVIC 中断使能配置
在main.c的SystemClock_Config()后添加:c HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); // 抢占优先级1,子优先级0 HAL_NVIC_EnableIRQ(TIM2_IRQn);
优先级设定需高于 Wi-Fi 事件处理(通常为 2~3),但低于 SysTick(0),确保定时中断不被低优先级任务阻塞。
2.3 启动时机控制:订阅成功后启动
TIM2 不应在main()开始即启动,而应严格绑定 MQTT 连接生命周期。在 MQTT 客户端完成MQTT_Connect()并收到CONACK响应后,才调用:
HAL_TIM_Base_Start_IT(&htim2); // 启动 TIM2 中断此设计保证:
- 若 Wi-Fi 连接失败或 MQTT 服务器不可达,TIM2 不会空转消耗功耗;
- 避免传感器在未建立通信链路前盲目采集,防止数据积压丢失;
- 符合物联网设备“按需唤醒”的低功耗设计范式。
编译验证阶段必须确认:
-htim2句柄全局可见性(extern 声明位置正确);
- 所有htim3字符串被彻底替换(包括注释中的示例);
-TIM2_IRQHandler与HAL_TIM_PeriodElapsedCallback链接无误;
- NVIC 配置未与其他外设冲突。
3. DHT22 传感器驱动模块集成与增强
3.1 模块导入与工程结构整合
DHT22 驱动采用标准三层架构:
-硬件抽象层(HAL):dht22_hal.c/h—— 封装 GPIO 初始化、输入捕获、微秒级延时;
-协议解析层(Protocol):dht22_parser.c/h—— 实现单总线时序解析、CRC 校验、数据提取;
-应用接口层(API):dht22.c/h—— 提供dht22_read_float()等用户调用函数。
集成步骤如下:
1. 将dht22.c、dht22.h、dht22_hal.c、dht22_hal.h复制至工程Core/Src与Core/Inc目录;
2. 在main.c顶部添加#include "dht22.h";
3. 在main.c的MX_GPIO_Init()后调用DHT22_GPIO_Init(),指定数据引脚(如 PA1);
4. 在main.c的while(1)循环前,添加DHT22_Init()完成模块初始化。
关键检查点:
DHT22_GPIO_Init()中必须将 PA1 配置为推挽输出模式(初始高电平),并在进入输入模式前执行HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, GPIO_PIN_SET),确保总线空闲态为高。
3.2 数据精度增强:整数→浮点数解析重构
原始 DHT22 驱动仅返回整数型温湿度值(uint8_t temp_int,uint8_t humi_int),无法满足工业级监测需求。增强核心在于重构dht22_parser.c中的数据解包逻辑:
DHT22 原始数据帧结构为 40 位:
| 字节 | 含义 | 原始值(示例) |
|------|------|----------------|
| 0–1 | 湿度整数+小数 |0x230x00→ 35.0% |
| 2–3 | 温度整数+小数 |0x1A0x00→ 26.0℃ |
| 4 | 校验和 |0x3D|
增强后解析逻辑(dht22_parser.c):
typedef struct { float temperature; // 单位:℃,精度0.1℃ float humidity; // 单位:%RH,精度0.1%RH } DHT22_Data_t; DHT22_Status_t DHT22_ParseData(uint8_t raw_data[5], DHT22_Data_t *out) { uint16_t raw_humi = (raw_data[0] << 8) | raw_data[1]; // 湿度原始值 uint16_t raw_temp = (raw_data[2] << 8) | raw_data[3]; // 温度原始值 // DHT22 协议规定:高字节为整数,低字节为小数(BCD编码) // 实际硬件返回为二进制值,需按位分离 out->humidity = (raw_humi >> 8) + (raw_humi & 0xFF) * 0.1f; out->temperature = (raw_temp >> 8) + (raw_temp & 0xFF) * 0.1f; // CRC 校验:前4字节和应等于第5字节 uint8_t crc = raw_data[0] + raw_data[1] + raw_data[2] + raw_data[3]; return (crc == raw_data[4]) ? DHT22_OK : DHT22_CRC_ERROR; }对应头文件dht22.h中需声明新结构体与函数:
typedef enum { DHT22_OK, DHT22_TIMEOUT, DHT22_CRC_ERROR } DHT22_Status_t; DHT22_Status_t DHT22_Read_Float(DHT22_Data_t *data);3.3 采集触发与错误处理机制
dht22_trigger_read()函数实现在dht22.c中,其职责是:
- 拉低总线 20ms 发送启动信号;
- 释放总线并等待 DHT22 响应;
- 读取 40 位数据流;
- 调用DHT22_ParseData()解析;
- 返回状态码供上层判断。
关键错误处理策略:
-超时检测:每个电平变化均设置HAL_Delay(1)保护,若超过 100μs 未检测到跳变,立即返回DHT22_TIMEOUT;
-CRC 强制校验:即使数据读取成功,CRC 错误亦视为无效数据,拒绝上报;
-状态缓存:定义静态变量static DHT22_Data_t last_valid_data,当本次读取失败时,仍可返回上次有效值,避免数据断崖式丢失。
调用示例(位于TIM2_IRQHandler触发的采集入口):
void dht22_trigger_read(void) { DHT22_Data_t sensor_data; DHT22_Status_t status = DHT22_Read_Float(&sensor_data); if (status == DHT22_OK) { // 数据有效,存入全局缓冲区供 MQTT 封装使用 g_mqtt_payload.temp = sensor_data.temperature; g_mqtt_payload.humi = sensor_data.humidity; g_mqtt_payload.timestamp = HAL_GetTick(); } else { // 记录错误类型,便于调试 printf("DHT22 Error: %d\r\n", status); } }4. 硬件连接与物理层调试要点
4.1 DHT22 与 STM32 的电气连接规范
DHT22 为 3.3V 逻辑器件,严禁直接接入 5V 系统。本实验采用以下连接方式:
| DHT22 引脚 | STM32 引脚 | 连接说明 |
|------------|-------------|-----------|
| VDD | 3.3V | 接 MCU 的 3.3V 电源轨,需加 100nF 陶瓷电容滤波 |
| GND | GND | 共地,走线尽量短 |
| DATA | PA1 | 串联 5.1kΩ 上拉电阻至 3.3V(内部上拉不足) |
| NC | — | 悬空 |
致命陷阱警示:若省略外部上拉电阻,DHT22 在释放总线后无法可靠恢复高电平,导致后续所有读取失败。实测表明,仅依赖 STM32 内部 40kΩ 上拉时,信号上升时间长达 15μs,超出 DHT22 协议要求的 5μs 上升沿阈值。
4.2 温度响应验证方法论
视频中出现“初始读数全为 0”现象,本质是 DHT22 的固有特性:
-首次上电需 1s 稳定时间:DHT22 内部 RC 电路需完成充电,此期间读取必返回 0x0000;
-热惯性延迟:环境温度变化需通过封装材料传导至内部热敏元件,响应时间约 2~5 秒;
-手指加热验证法:用手紧握传感器 3 秒后读取,温度应上升 1~2℃,证明硬件链路与驱动逻辑均正常。
更严谨的验证应使用红外测温仪比对,而非依赖人手感知。在量产测试中,建议增加上电自检流程:
// 上电后连续读取3次,丢弃首次结果 for (int i = 0; i < 3; i++) { HAL_Delay(1000); DHT22_Read_Float(&dummy); }5. 软件集成验证与调试技巧
5.1 编译零警告实践指南
常见警告来源及消除方法:
-隐式函数声明:在dht22.h中未声明DHT22_Read_Float(),而在main.c中直接调用 → 补全头文件包含与函数声明;
-未使用变量:static uint8_t unused_var未被引用 → 删除或添加__attribute__((unused));
-格式化字符串不匹配:printf("%d", float_val)→ 改用printf("%.1f", float_val);
-指针类型不兼容:&temp_int传给uint8_t*参数 → 确保参数类型与函数原型严格一致。
启用编译器警告等级:
- GCC 添加-Wall -Wextra -Werror,将警告视为错误强制修复;
- 在 CubeIDE 中勾选 “Treat warnings as errors”。
5.2 串口调试信息设计原则
printf()输出必须满足:
-非阻塞:重定向至HAL_UART_Transmit()并启用 DMA,避免主循环卡死;
-分级控制:定义DEBUG_LEVEL宏,#if DEBUG_LEVEL >= 2控制详细寄存器值输出;
-时间戳关联:每条日志前置HAL_GetTick(),便于分析时序关系。
典型调试输出格式:
[1245] DHT22 OK: T=25.3°C, H=48.7%RH [1275] MQTT Publish queued: topic=/sys/xxx/thing/event/property/post5.3 实际项目踩坑记录
- Wi-Fi 模块复位干扰:ESP8266 复位时产生 >100mA 电流尖峰,导致 STM32 电源跌落,DHT22 通信异常。解决方案:在 ESP8266 VCC 与 GND 间并联 470μF 钽电容,并将 DHT22 供电从 STM32 独立改为 LDO 稳压输出。
- HAL_Delay 精度陷阱:
HAL_Delay(1)实际耗时约 1.05ms(SysTick 配置误差),导致 DHT22 时序偏移。改用HAL_Delay_us(80)(自定义微秒延时)后问题解决。 - 浮点运算性能瓶颈:
printf("%.1f")调用sprintf导致栈溢出。改用整数运算:temp_int = (int)(data.temperature * 10); printf("%d.%d", temp_int/10, temp_int%10);。
6. 下一阶段衔接:PUBLISH 报文构造基础
本实验产出的g_mqtt_payload结构体,即为后续 MQTT PUBLISH 报文的有效载荷(Payload)源头。其字段将直接映射至阿里云 IoT 物模型 JSON:
{ "id": "12345", "version": "1.0", "params": { "temperature": 25.3, "humidity": 48.7, "timestamp": 1712345678 } }而id字段需由HAL_GetTick()或 RTC 获取唯一序列号,version固定为 “1.0”。这些细节将在下一讲中展开,但必须意识到:当前所有传感器数据采集的精度、稳定性、时效性,直接决定最终上云报文的业务价值。一个返回 0 值的传感器,无论 MQTT 协议栈多么健壮,都无法传递真实世界的状态。
我在实际产线部署中曾遇到过类似问题:DHT22 在 -10℃ 环境下持续返回 0,最终发现是 PCB 上未预留传感器安装孔位,导致外壳冷凝水渗入引脚造成短路。因此,硬件可靠性永远是软件功能的前提——这比任何优化技巧都重要。