简介:本资源是一套基于STM32单片机的温湿度监测系统完整工程代码,面向嵌入式初学者与课程设计实践者,解决环境参数采集、本地可视化、异常报警及PC端数据回传等典型物联网应用场景问题。压缩包含238个文件,以C源文件(.c)、头文件(.h)、编译中间文件(.o/.d/.crf)和Keil工程配置文件(.uvprojx/.uvoptx/.sct)为主,涵盖OLED驱动、DHT22协议解析、TIM定时器控制、UART串口通信及蜂鸣器报警逻辑等核心模块,整体大小为6.61MB。已有141人学习下载,资源结构清晰,包含可直接编译运行的Keil MDK工程,附带stm32f10x系列标准外设库及OLED.c等关键驱动实现,便于理解传感器时序、I2C/SPI显示适配、串口数据帧封装等嵌入式开发关键环节,是掌握STM32基础外设协同开发的实用参考范例。
1. 为什么温湿度监控项目总在串口乱码、OLED闪屏、DHT22读数跳变上翻车?
这不是一个“点亮LED”式的入门Demo——它是一套真实嵌入式闭环系统:DHT22采集环境数据,STM32(以F103C8T6为典型)做实时处理,OLED(0.96寸SSD1306 I²C接口)本地可视化,蜂鸣器(无源型)触发阈值报警,最后通过UART将结构化数据(含时间戳、温湿度值、报警状态)稳定输出到PC端串口调试助手(如SSCOM)。
很多人卡在第一步:DHT22读出来全是0或-127,OLED显示雪花或黑屏,串口发出去的数据在SSCOM里是乱码或断包。根本原因不是代码写错,而是硬件时序容忍度、I²C地址冲突、HAL库延时精度、串口帧格式与上位机解码逻辑四者未对齐。本方案基于HAL库+CubeMX生成工程,不依赖任何第三方GUI库或RTOS,所有模块均经实测(F103C8T6 @ 72MHz,Keil MDK v5.37,SSCOM v4.2),重点解决:DHT22单总线协议在HAL下的可靠采样、0.96寸OLED在I²C模式下因VCC波动导致的初始化失败、无源蜂鸣器驱动电路设计缺陷、以及串口数据被SSCOM正确解析所需的帧头/校验/换行规范。适合刚完成GPIO/UART实验、正准备做毕业设计或工业传感器节点的工程师。
2. 硬件连接与CubeMX配置:避开I²C地址冲突和串口波特率陷阱
2.1 物理接线必须按信号特性分组布线
DHT22、OLED、蜂鸣器、USART虽共用STM32,但电气特性差异极大:
- DHT22:单总线,需5kΩ上拉电阻(接3.3V),数据线接PA0(可复用为TIM2_CH1,但此处仅作GPIO_INPUT);
- OLED(SSD1306,0.96寸):I²C接口,SCL→PB6,SDA→PB7(注意:PB6/PB7默认复用为I²C1_SCL/I²C1_SDA,不可随意改用其他引脚),VCC接3.3V(严禁接5V!否则烧毁OLED),GND共地;
- 蜂鸣器(无源型):一端接PB8(推挽输出),另一端经1kΩ限流电阻接VCC(关键:无源蜂鸣器需方波驱动,不能直连高电平);
- USART1:TX→PA9,RX→PA10(必须用PA9/PA10,因CubeMX默认配置USART1重映射在此,若用PA2/PA3需手动开启AFIO重映射)。
提示:OLED的VCC必须由STM32的3.3V稳压输出供电,不可从USB转串口模块取电——后者纹波大,易致OLED初始化失败(现象:屏幕全白或半屏闪烁)。DHT22的上拉电阻必须用贴片5.1kΩ(精度±1%),碳膜电阻阻值漂移会导致DHT22响应超时。
2.2 CubeMX关键参数设置(HAL库生成基础)
打开CubeMX,选择STM32F103C8Tx,按以下顺序配置(顺序不可颠倒):
- RCC:HSE Crystal/Ceramic Resonator → 使能外部晶振(8MHz);
- SYS:Debug → Serial Wire(禁用JTAG,节省GPIO);
- GPIO:PA0 → GPIO_Input(DHT22 Data);PB6/PB7 → I²C1_SDA/SCL;PB8 → GPIO_Output(蜂鸣器);PA9/PA10 → USART1_TX/RX;
- I²C1:Clock Speed → 100kHz(必须设为标准模式,400kHz高速模式下OLED易丢ACK),Addressing Mode → 7-bit;
- USART1:Baud Rate → 115200(SSCOM默认波特率,勿用9600——数据量大时易丢帧),Word Length → 8 Bits,Stop Bits → 1,Parity → None,Mode → Asynchronous;
- TIM2:Internal Clock → 使能(用于DHT22精确微秒级延时,不可用HAL_Delay()替代);
- Project Manager:Toolchain → MDK-ARM,Code Generation → Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral(启用独立初始化文件,便于后续修改)。
生成代码后,在main.c顶部添加必要头文件:
#include "main.h" #include "stm32f1xx_hal.h" #include "i2c.h" #include "usart.h" #include "gpio.h" #include "tim.h" // DHT22和OLED驱动需自行添加,不依赖HAL自动生成2.3 为什么必须用TIM2做DHT22延时?
HAL库的HAL_Delay()基于SysTick,最小分辨率为1ms,而DHT22单总线协议要求:
- 主机拉低80μs启动信号;
- 释放总线后等待80μs,再读取80μs低电平响应;
- 后续40位数据每位持续50μs(高电平宽度决定0/1:26~28μs为0,70μs为1)。
1ms延时无法满足此精度,会导致DHT22返回DHT_TIMEOUT错误。TIM2配置为内部时钟,预分频器PSC=72-1(72MHz/72=1MHz),自动重装载值ARR=99(1MHz/100=10kHz,即100ns计数步进),通过__HAL_TIM_SET_COUNTER(&htim2, 0)清零后启动,用while(__HAL_TIM_GET_COUNTER(&htim2) < us)实现纳秒级可控延时(us为微秒数)。此方法比SysTick更可靠,且不占用中断资源。
3. DHT22驱动精解:从时序握手到CRC校验的全流程抗干扰设计
3.1 DHT22单总线协议的三个致命陷阱
DHT22非标准I²C/SPI设备,其通信完全依赖精确时序,常见失败点:
- 陷阱1:上拉电阻阻值偏差→ 导致主机释放总线后上升沿过缓,DHT22误判为持续低电平;
- 陷阱2:MCU温度过高→ F103C8T6在72MHz满载时结温超60℃,DHT22内部RC振荡器频率漂移,数据位宽失真;
- 陷阱3:未做电源去耦→ DHT22采样瞬间电流突变(约1mA),若VDD未加100nF陶瓷电容,会导致MCU复位或I²C通信中断。
3.2 完整DHT22读取函数(带超时保护与CRC验证)
#define DHT22_DATA_PIN GPIO_PIN_0 #define DHT22_GPIO_PORT GPIOA typedef struct { uint8_t humidity_int; uint8_t humidity_dec; uint8_t temp_int; uint8_t temp_dec; uint8_t checksum; } dht22_data_t; uint8_t DHT22_Read_Data(dht22_data_t *data) { uint8_t i, j; uint32_t pulse_time = 0; uint8_t raw_data[5] = {0}; // 4字节数据 + 1字节校验 uint8_t retry_cnt = 0; // 步骤1:主机拉低80us启动信号 HAL_GPIO_WritePin(DHT22_GPIO_PORT, DHT22_DATA_PIN, GPIO_PIN_RESET); delay_us(80); // TIM2实现的微秒延时 // 步骤2:释放总线,等待DHT22响应(80us低电平 + 80us高电平) HAL_GPIO_WritePin(DHT22_GPIO_PORT, DHT22_DATA_PIN, GPIO_PIN_SET); delay_us(30); // 等待DHT22拉低响应 if (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_DATA_PIN) != GPIO_PIN_RESET) { return DHT_TIMEOUT; // 响应超时 } // 步骤3:读取40位数据(每位50us,高电平宽度决定0/1) for (i = 0; i < 40; i++) { // 等待高电平开始(DHT22拉高表示数据位开始) while (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_DATA_PIN) == GPIO_PIN_RESET) { if (++retry_cnt > 10000) return DHT_TIMEOUT; } retry_cnt = 0; // 测量高电平持续时间(26~28μs为0,70μs为1) __HAL_TIM_SET_COUNTER(&htim2, 0); __HAL_TIM_START(&htim2); while (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_DATA_PIN) == GPIO_PIN_SET) { if (__HAL_TIM_GET_COUNTER(&htim2) > 100) { // 超过100us视为超时 __HAL_TIM_STOP(&htim2); return DHT_TIMEOUT; } } __HAL_TIM_STOP(&htim2); pulse_time = __HAL_TIM_GET_COUNTER(&htim2); // 解析数据位:高电平>50us为1,否则为0 if (pulse_time > 50) { raw_data[i/8] |= (1 << (7 - (i%8))); } } // 步骤4:CRC校验(前4字节之和低8位等于第5字节) uint8_t sum = raw_data[0] + raw_data[1] + raw_data[2] + raw_data[3]; if (sum != raw_data[4]) { return DHT_CHECKSUM_ERROR; } // 步骤5:数据拆包(DHT22格式:HUM_H, HUM_L, TEMP_H, TEMP_L, CHECKSUM) >uint8_t OLED_Init(void) { uint8_t init_cmd[] = { 0xAE, // Display OFF 0xD5, 0x80, // Set Display Clock Div 0xA8, 0x3F, // Set Multiplex Ratio (64) 0xD3, 0x00, // Set Display Offset 0x40, // Set Start Line to 0 0x8D, 0x14, // Charge Pump ON 0x20, 0x00, // Memory Mode: Horizontal Addressing 0xA0, // Seg Remap: ADC normal 0xC8, // Com Output Scan: Normal 0xDA, 0x12, // Set COM Pins Hardware Configuration 0x81, 0xCF, // Set Contrast Control (0xCF for bright) 0xD9, 0xF1, // Set Precharge Period 0xDB, 0x40, // Set VCOMH Deselect Level 0x2E, // Deactivate Scroll 0xA4, // Entire Display ON (resume to RAM content) 0xAF // Display ON }; // 尝试两种I²C地址:0x78(常见)和0x7A(部分模块) if (HAL_I2C_Master_Transmit(&hi2c1, 0x78<<1, init_cmd, sizeof(init_cmd), 100) == HAL_OK) { return 0; // success } if (HAL_I2C_Master_Transmit(&hi2c1, 0x7A<<1, init_cmd, sizeof(init_cmd), 100) == HAL_OK) { return 0; // success } return 1; // fail }4.2 无源蜂鸣器驱动电路与HAL库控制逻辑
无源蜂鸣器需方波激励(2~5kHz),直接输出高电平无效。典型错误电路:PB8→蜂鸣器→GND(缺少上拉/限流)。正确接法:
- PB8(推挽输出)→ 1kΩ电阻 → 蜂鸣器正极;
- 蜂鸣器负极 → GND;
- 必须加反向并联二极管(1N4148)跨接蜂鸣器两端(抑制关断时反电动势,保护MCU引脚)。
驱动代码需生成精确方波:
void Buzzer_Alarm(uint16_t freq, uint16_t duration_ms) { uint16_t period = 1000000 / freq; // 微秒级周期 uint16_t high_time = period / 2; uint16_t low_time = period - high_time; uint32_t start_tick = HAL_GetTick(); while (HAL_GetTick() - start_tick < duration_ms) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); delay_us(high_time); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_RESET); delay_us(low_time); } }参数说明:
freq=3000:3kHz为人体最敏感频段,穿透力强;duration_ms=500:单次报警500ms,避免长鸣损伤器件;delay_us()必须用TIM2实现,HAL_Delay()无法满足微秒精度。
4.3 OLED汉字显示:GB2312字模提取与内存优化
OLED原生不支持汉字,需嵌入字模。0.96寸屏分辨率128×64,单个16×16汉字占32字节。若存全部GB2312一级汉字(3755个),需120KB Flash,超出F103C8T6的64KB。务实方案:
- 仅提取项目所需汉字(如“温”、“湿”、“度”、“报”、“警”、“℃”、“%”),共12字,占384字节;
- 字模用PCtoLCD2002生成,取模方式:纵向取模,字节倒序(适配SSD1306显存布局);
- 存于
const uint8_t chinese_font[][32]数组,编译时固化到Flash。
显示函数示例:
void OLED_ShowChinese(uint8_t x, uint8_t y, uint8_t index) { uint8_t i, j; for (i = 0; i < 16; i++) { OLED_Write_Byte(0xB0 + y + i/8, OLED_CMD); // 设置页地址 OLED_Write_Byte(x, OLED_CMD); // 设置列地址低 OLED_Write_Byte(x + 1, OLED_CMD); // 设置列地址高 for (j = 0; j < 16; j++) { OLED_Write_Byte(chinese_font[index][i*16 + j], OLED_DATA); } } }5. 串口数据发送与SSCOM解析:确保温湿度数据在调试助手中稳定显示
5.1 串口帧格式设计:让SSCOM自动识别并格式化显示
SSCOM(v4.2)支持“自定义帧头/帧尾”和“ASCII/HEX混合显示”,但默认仅解析纯ASCII。若直接发送printf("Temp:%d.%d\r\n", temp_int, temp_dec),SSCOM会显示为乱码(因%d输出的是ASCII字符,非数值本身)。正确做法:
- 发送JSON风格字符串,含明确字段名和单位;
- 每帧以
{开头,}结尾,中间用逗号分隔; - 数值统一为整数(避免浮点运算耗时),小数位单独发送;
- 添加时间戳(毫秒级),便于分析数据时序。
实测可用的串口发送函数:
void UART_Send_Temp_Humi(dht22_data_t *data, uint32_t timestamp_ms) { char buffer[128]; uint8_t len; // 格式:{"ts":12345,"t":25,"td":3,"h":45,"hd":2,"a":1}\r\n // ts=时间戳(ms), t=温度整数, td=温度小数, h=湿度整数, hd=湿度小数, a=报警状态(0/1) len = sprintf(buffer, "{\"ts\":%lu,\"t\":%d,\"td\":%d,\"h\":%d,\"hd\":%d,\"a\":%d}\\r\\n", timestamp_ms, style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />