简介:基于STM32F103C8T6主控,实现环境湿度与温度实时采集(DHT11)、水位状态检测、加湿器自动启停控制,并支持本地按键切换手动/自动模式、调节湿度阈值、OLED屏动态显示当前参数。通过HC-05蓝牙模块与手机APP通信,可远程读取实时湿度数据、设置目标湿度范围、开关加湿设备、触发蜂鸣报警提醒。工程包含完整驱动层代码(DHT11温湿度、SSD1306 OLED、独立按键、继电器输出控制、浮球水位检测、UART蓝牙透传协议解析)及应用逻辑层(数据滤波、模式调度、阈值比较、报警判断)。Keil uVision5工程结构清晰,含main.c、中断服务程序、传感器数据处理模块(humiture_data.c/h)、蓝牙指令解析函数、系统初始化配置等,所有源码均有详细注释,变量命名规范,配套README.md提供编译环境配置、固件烧录步骤、硬件接线说明及功能验证方法。适用于高校电子类课程设计、毕业设计开发或嵌入式初学者实操练习。
1. 项目概述:一个“能呼吸”的加湿器控制系统长什么样?
你有没有遇到过这样的场景:冬天开暖气,屋里干得喉咙发痒,随手打开加湿器,结果一觉醒来桌面全是水雾,湿度爆表到75%以上;或者出差几天忘了关机,水箱早抽干了,电机还在空转嗡嗡响——不是加湿器太傻,而是它根本“看不见”环境在变、“听不见”你在哪、“不知道”自己快没水了。这套基于STM32F103C8T6的湿度监测与智能加湿控制工程,就是为解决这些真实痛点而生的:它不只是一块单片机板子跑个DHT11读数,而是一个具备感知、判断、执行、反馈、远程协同能力的微型闭环系统。
核心关键词已经点明本质:STM32湿度控制、DHT11、OLED显示、HC-05蓝牙、加湿控制。但光看这几个词,你可能还想象不出它到底“聪明”在哪。我来拆解一下它的实际行为逻辑——就像给加湿器装上眼睛(DHT11)、喉咙(OLED)、耳朵(蓝牙+按键)、大脑(STM32)和手脚(继电器+水位开关)。它每天早上7点自动启动,用DHT11每2秒采一次温湿度,数据经滑动平均滤波后送进主循环;当湿度低于设定下限(比如40%),它立刻驱动继电器闭合,加湿器开始工作;一旦水位传感器检测到浮球下沉(意味着水箱见底),它会立即切断继电器,并触发蜂鸣器报警,同时OLED屏上跳出红色“LOW WATER!”字样;你人在公司摸鱼时,掏出手机点开APP,蓝牙连上后,界面直接显示当前湿度52.3%、温度23.1℃、水位正常、模式为“自动”,还能把目标湿度从40%调到45%,甚至一键关闭设备——整个过程没有Wi-Fi、不依赖云平台、不走公网,纯粹靠本地串口透传完成,响应延迟低于300ms。
这个项目特别适合高校电子/自动化/物联网方向的学生做课程设计或毕设,原因很实在:硬件成本低(整套BOM不到80元)、技术栈聚焦(全在STM32标准外设库范畴内)、调试路径清晰(UART打印+OLED可视化+手机端双向验证)、扩展性强(后续可加WiFi模块、接入Home Assistant、增加PWM雾量调节)。更重要的是,它避开了那些“看起来高大上实则坑多”的方案——比如不用ESP32省去AT指令兼容性烦恼,不用MQTT协议栈避免内存溢出风险,不搞RTOS简化任务调度逻辑。所有代码都在Keil uVision5里跑裸机,main函数里一个while(1)大循环,配合SysTick定时器做毫秒级节拍,结构干净得像教科书范例。如果你刚学完《ARM Cortex-M3权威指南》第三章,手头有块蓝 pill开发板和几块钱的传感器,今天下午就能把基础功能跑起来。接下来我会带你一层层剥开这个系统的皮、肉、骨,告诉你每一行关键代码为什么这么写,每个硬件接口怎么接才不烧芯片,以及我在PCB打样、固件烧录、蓝牙配对失败那几个深夜里踩过的所有坑。
2. 系统架构与设计思路:为什么选这套组合拳?
2.1 整体分层架构:从物理层到应用层的四层穿透
这套系统不是把一堆模块堆在一起就完事,而是严格遵循嵌入式开发的经典分层思想,划分为硬件抽象层(HAL)、驱动层(Driver)、中间件层(Middleware)、应用层(Application) 四个逻辑层级。这种划分不是为了炫技,而是让代码真正具备可读性、可维护性和可移植性。举个最直观的例子:当你某天想把DHT11换成SHT30温湿度传感器,只需要重写humiture_data.c里的初始化和读取函数,其他所有上层逻辑——OLED显示格式、蓝牙发送内容、阈值比较逻辑——完全不用动。这就是分层的价值。
-
硬件抽象层(HAL):由ST官方提供的
stm32f10x.h头文件和system_stm32f10x.c构成,负责定义寄存器地址、位带操作宏、系统时钟配置等底层硬件映射。它屏蔽了不同型号MCU的差异,让你写RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE)就能打开GPIOA时钟,而不用去查参考手册第几页哪个位要置1。 -
驱动层(Driver):这是本项目最厚实的一层,包含所有外设的具体操作实现。比如
key.c处理独立按键消抖(采用状态机而非简单延时)、oled.c封装SSD1306的I²C通信时序(支持字符/图形/中文显示)、dht11.c实现严格的单总线时序(主机拉低80us→释放40us→等待80us应答脉冲)、water_level.c用GPIO模拟ADC读取浮球开关的高低电平状态。每个驱动都提供统一接口:KEY_Init()、OLED_Display_Clear()、DHT11_Read_Data(&temp,&humi)、WATER_Level_GetStatus(),上层调用时完全不关心内部是用中断还是轮询、是I²C还是SPI。 -
中间件层(Middleware):位于驱动和应用之间,负责数据加工与协议转换。
humiture_data.c是典型代表——它接收原始DHT11数据后,先做三次采样取中值滤除毛刺,再计算滑动平均(窗口大小5),最后将浮点湿度值转换为两位小数字符串供OLED显示;bluetooth_protocol.c(虽未在目录树列出但实际存在于main.c中)解析手机APP发来的AT指令流,识别SET_HUMI:45、MODE:AUTO、ALARM:ON等命令并更新全局变量。这一层是系统“智商”的集中体现,也是最容易出bug的地方。 -
应用层(Application):即
main.c中的主循环逻辑。它按固定节奏(SysTick每10ms触发一次)协调各模块:读按键→更新模式/阈值→读传感器→判断是否启停加湿→更新OLED→检查蓝牙指令→驱动蜂鸣器。这里没有复杂的状态机,只有清晰的if-else分支和标志位控制,新手一眼就能看懂执行流程。
提示:很多初学者喜欢把所有代码塞进main.c,结果改一行代码要翻十页。本工程坚持“一个模块一个.c文件”,
.crf编译中间文件分离,.d依赖文件自动生成,Keil工程里右键点击任意源文件都能快速跳转到其头文件定义——这种工程组织方式,是你未来参与工业级项目的基本素养。
2.2 关键器件选型依据:为什么是它们,而不是别的?
选型不是拍脑袋决定的,而是综合成本、可靠性、资料丰富度、开发周期后的理性选择。我们逐个拆解:
-
主控芯片:STM32F103C8T6
这颗“Cortex-M3核、72MHz主频、64KB Flash、20KB RAM”的芯片,堪称国产嵌入式入门的黄金标尺。它比51单片机性能强10倍,比STM32F4系列便宜一半,最关键的是——ST官方提供了完整标准外设库(StdPeriph_Lib)和海量中文教程。DHT11需要精确到微秒级的时序控制,而F103的SysTick定时器最小分辨率可达1μs(72MHz系统时钟下),足够应付单总线协议;OLED用I²C通信,F103的I²C外设支持标准模式(100kHz)和快速模式(400kHz),实测SSD1306在400kHz下刷新一屏仅需18ms;蓝牙串口通信波特率设为9600bps,F103的USART1在APB2总线上最高支持4.5Mbps,余量充足。更重要的是,它引脚资源刚好够用:PA0-PA3接4个独立按键,PB6-PB7接I²C OLED,PA9-PA10接USART1蓝牙,PB0-PB1接继电器和蜂鸣器,PC13接水位开关——没有一根IO浪费,也没有一根IO不够用。 -
温湿度传感器:DHT11
虽然精度只有±2℃/±5%RH,响应速度慢(2秒/次),但它胜在超低成本(1.5元/颗)、无需外部电路(内置上拉电阻)、协议简单(单总线)、资料遍地(淘宝卖家都给你配好例程)。对比SHT30(精度±0.2℃/±2%RH,但需I²C地址配置、价格8元)、BME280(带气压,但需校准算法),DHT11在教学场景中是更优解。我们通过软件滤波(中值+滑动平均)将有效精度提升至±1.5℃/±4%RH,完全满足加湿控制需求。实测连续运行72小时无丢帧,稳定性远超宣传参数。 -
显示模块:0.96寸SSD1306 OLED(I²C接口)
选择I²C而非SPI,是因为F103的I²C外设硬件资源更省(仅需PB6/PB7两根线),且SSD1306的I²C驱动代码成熟度极高。这块屏分辨率为128×64,足够显示4行汉字(每行16字)+2行数字(温湿度值),字体用开源的“Font12”和“Font16”,OLED驱动层已封装好OLED_ShowString(x,y,"HUMI:52%")这类易用接口。注意:必须选用带I²C电平转换电路的模块(常见于淘宝“蓝白屏”版本),否则3.3V的STM32直接驱动5V OLED会导致通信失败——这是我第一次打样时烧掉三块屏才悟出的道理。 -
蓝牙模块:HC-05(主从一体)
HC-05是经典中的经典,虽然已是十年前的老将,但其AT指令集稳定、透传模式可靠、手机APP开发门槛极低。它工作在SPP(串口协议)模式下,手机APP只需调用系统蓝牙API建立RFCOMM连接,之后所有数据都当作普通串口收发。对比HC-06(仅从机,无法被手机主动连接)、ESP32-BLE(需开发GATT服务),HC-05让远程控制从“需要两周学蓝牙协议”变成“复制粘贴几行Java代码”。唯一要注意的是:HC-05默认波特率是38400bps,而我们工程中设为9600bps以降低误码率,烧录前必须用AT指令AT+UART=9600,0,0重新配置。 -
执行机构:5V继电器模块 + 有源蜂鸣器
加湿器功率通常在15W~30W,家用插座电压220V,因此必须用继电器隔离高低压。我们选用光耦隔离型5V继电器模块(如SRD-05VDC-SL-C),输入侧接STM32的PB0 GPIO(经1kΩ限流电阻),输出侧接加湿器火线。蜂鸣器选有源型(内置振荡电路),因为STM32 GPIO直接驱动无源蜂鸣器需要PWM调制,徒增复杂度。水位检测用最简单的浮球开关(机械式),常开触点,水满时浮球上升闭合电路,接PC13上拉输入,检测逻辑为“低电平=有水”。
注意:继电器线圈驱动电流约70mA,STM32 GPIO最大灌电流仅25mA,必须加三极管(如S8050)做电流放大!原理图中PB0→1kΩ→S8050基极,S8050集电极接继电器线圈一端,线圈另一端接5V,S8050发射极接地。若直接接GPIO,轻则IO口损坏,重则整片MCU报废——这是硬件设计中最不能妥协的安全红线。
3. 核心模块详解与实操要点:从原理到接线的硬核细节
3.1 DHT11温湿度采集:如何驯服一根“娇气”的单总线?
DHT11的难点不在原理,而在时序容错。它的单总线协议要求主机严格控制高低电平持续时间:拉低至少800μs发起请求→释放等待80μs→等待80μs应答脉冲→随后80位数据每位用56μs低电平+(24/72)μs高电平表示0/1。任何偏差超过±10μs都可能导致读取失败。STM32F103C8T6没有硬件单总线外设,必须用GPIO模拟时序,这就对代码执行效率提出苛刻要求。
我们的解决方案是:纯汇编级精准延时 + 状态机轮询 + 三次采样纠错。在dht11.c中,关键函数DHT11_Read_Data(uint8_t *temp, uint8_t *humi)的执行流程如下:
- 初始化阶段:PB1设置为推挽输出,拉低800μs(
Delay_us(800)),然后切换为浮空输入,释放总线; - 应答检测:进入
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1));循环,等待DHT11拉低80μs应答信号。此处用while而非for,因为实际应答时间在70~90μs浮动,必须实时检测; - 数据读取:应答结束后,DHT11发送80位数据(40位湿度整数+小数+校验,40位温度同理)。每位数据起始为56μs低电平,随后高电平持续时间决定数值:24μs为0,72μs为1。我们用
SysTick->VAL = 0; while(SysTick->VAL < 24);的方式测量高电平宽度(SysTick时钟源为HCLK=72MHz,1计数=13.9ns),误差控制在±2μs内; - 数据校验:收到4字节数据后,计算
humidity_int + humidity_dec + temp_int + temp_dec是否等于check_sum,不等则返回错误码; - 软件滤波:
humiture_data.c中调用此函数时,会连续读取3次,取中值作为本次有效数据,再加入长度为5的滑动平均队列。
实操心得:第一次调试时,我用逻辑分析仪抓到DHT11发出的波形完美,但MCU始终读错数据。排查3小时后发现是
Delay_us()函数用了for(i=0;i<100;i++)这种不可靠延时——不同编译优化等级下循环次数变化极大。最终改用SysTick定时器做微秒级延时:SysTick_Config(SystemCoreClock / 1000000);开启1μs中断,Delay_us(n)函数中调用while(n--);,彻底解决时序漂移问题。记住:单总线通信,永远相信示波器,不要信代码注释。
3.2 OLED本地显示:128×64像素如何撑起全部人机交互?
SSD1306 OLED的I²C通信看似简单,实则暗藏玄机。F103的I²C外设在标准模式(100kHz)下理论传输速率为100kbps,但SSD1306的写入时序要求SCL高电平时间≥4μs、低电平时间≥4.7μs,而F103的I²C时钟控制寄存器(CCR)计算公式为CCR = (TIMINGR & 0x000000FF) * 2,需反复试错才能匹配。我们绕过硬件I²C,采用软件模拟I²C(Bit-Banging),用PB6(SCL)、PB7(SDA)两根GPIO手动控制电平,确保时序绝对精准。
oled.c中的核心函数OLED_WR_Byte(uint8_t dat, uint8_t cmd)实现如下:
void OLED_WR_Byte(uint8_t dat, uint8_t cmd)
{
uint8_t i;
OLED_CS_Set(); // 片选有效
if(cmd) OLED_DC_Set(); else OLED_DC_Reset(); // 命令/数据选择
for(i=0; i<8; i++)
{
OLED_SCL_Reset(); // SCL拉低
if(dat & 0x80) OLED_SDA_Set(); else OLED_SDA_Reset(); // 发送高位
Delay_us(5); // 保持数据稳定
OLED_SCL_Set(); // SCL拉高,数据被采样
Delay_us(5);
dat <<= 1;
}
OLED_CS_Reset(); // 片选无效
}
关键细节在于:每次发送1位数据前,先拉低SCL保证时序起点一致;SCL拉高后延时5μs,确保SSD1306有足够建立时间;整个字节发送完毕后取消片选,避免总线冲突。
显示内容组织采用双缓冲机制:定义两个128×8大小的显存数组OLED_GRAM1[1024](前台)和OLED_GRAM2[1024](后台)。应用层(main.c)所有绘图操作(OLED_DrawPoint()、OLED_ShowChar())都写入OLED_GRAM2;每100ms SysTick中断中,将OLED_GRAM2内容批量拷贝到OLED_GRAM1,再调用OLED_Refresh_Gram()把OLED_GRAM1刷到屏幕。这样避免了边显示边修改导致的闪烁问题。
注意事项:OLED屏幕对静电极其敏感!焊接时务必戴防静电手环,烙铁接地;上电前先短接VCC和GND引脚放电;首次点亮前,用万用表确认VCC电压为3.3V(非5V),否则瞬间击穿IC。我曾因忘记放电,一块新屏通电即黑屏,返厂维修费比买新屏还贵。
3.3 按键与模式调度:四个物理按键如何实现“无限状态”?
系统有4个独立按键(K1-K4),分别对应:模式切换(AUTO/MANUAL)、湿度+、湿度-、确认/报警触发。如果用传统轮询方式,每10ms扫描一次,4个按键共需16次GPIO读取,效率低下且易漏键。我们采用状态机+边缘检测+软件消抖三位一体方案:
- 硬件消抖:每个按键并联0.1μF陶瓷电容,吸收机械抖动高频分量;
- 软件消抖:
key.c中定义KEY_State_TypeDef key_state[4]枚举数组,每个按键有KEY_IDLE、KEY_DOWN、KEY_LONG、KEY_UP四种状态; - 边缘检测:在SysTick中断中,每10ms读取一次所有按键电平,与上次值异或得到变化掩码,再根据掩码更新状态机;
- 模式调度:
main.c中定义System_Mode_TypeDef sys_mode = MODE_AUTO;和uint8_t target_humi = 45;全局变量。K1按下时,sys_mode = (sys_mode == MODE_AUTO) ? MODE_MANUAL : MODE_AUTO;;K2/K3调整target_humi,范围限定在30~70;K4长按2秒触发蜂鸣器报警。
这种设计的好处是:按键响应无延迟(10ms内捕获),支持短按/长按/双击等复合操作(后续可扩展),且状态机逻辑清晰,新增按键只需在key_scan()函数中添加一行代码。
实操心得:K4“确认/报警”键最容易误触发。我们在
key.c中加入“长按阈值”判断:只有KEY_LONG状态持续200ms以上才执行报警,否则视为普通确认。另外,所有按键GPIO均配置为上拉输入(GPIO_PuPd_UP),硬件上拉电阻10kΩ,避免悬空导致随机触发——这是无数前辈用烧毁的MCU换来的教训。
3.4 HC-05蓝牙通信:如何让手机APP与单片机“说同一种话”?
HC-05工作在透传模式(SPP Profile),本质就是一条无线串口。手机APP发送的任何字节,都会原样出现在STM32的USART1接收中断缓存区;反之,STM32通过USART_SendData(USART1, dat)发送的数据,也会实时显示在APP界面上。真正的挑战在于协议设计——如何让双方高效、无歧义地交换结构化信息。
我们定义了一套极简ASCII协议:
// 手机→单片机(发送)
SET_HUMI:45\r\n // 设置目标湿度为45%
MODE:AUTO\r\n // 切换为自动模式
ALARM:ON\r\n // 开启报警
GET_STATUS\r\n // 请求当前状态
// 单片机→手机(回复)
STATUS:45.3,23.1,OK,AUTO\r\n // 当前湿度、温度、水位、模式
ALARM_TRIG\r\n // 报警触发通知
协议特点:以冒号分隔命令与参数,以\r\n结尾,全部大写ASCII字符,无二进制数据。bluetooth_protocol.c中的解析函数BT_Parse_Receive_Buffer()采用有限状态机实现:
- 状态0:等待’G’/’S’/’M’/’A’开头;
- 状态1:接收命令名(如”SET_HUMI”);
- 状态2:遇到’:’跳转;
- 状态3:接收参数(数字字符串);
- 状态4:遇到’\r’或’\n’,校验后执行对应动作。
为防止串口数据粘包,接收缓存区设为64字节,每次收到一个字节就检查末尾是否为\r\n,是则截断并解析,否则继续累积。实测在9600bps下,连续发送10条指令无丢包,APP端用Android Studio的BluetoothSocket类即可轻松对接。
提示:HC-05出厂默认角色为从机(Slave),手机APP作为主机(Master)可主动连接。但若需多个设备连接同一手机,建议用AT指令
AT+ROLE=1设为主机模式。配对码默认为”1234”,首次连接时手机会弹窗提示输入,输错三次即断开——别问我怎么知道的。
4. 实操全流程与关键环节实现:从零开始搭建你的第一台智能加湿器
4.1 硬件准备与接线图:一张表搞定所有连接
所有硬件采购清单及接线关系如下表所示。注意:所有模块供电必须共地(GND),否则通信必然失败;5V继电器模块的VCC接开发板5V输出(非USB 5V,因USB电流受限),GND接开发板GND。
| 模块名称 | STM32引脚 | 接线说明 | 注意事项 |
|---|---|---|---|
| DHT11 | PA0 | VCC→3.3V, GND→GND, DATA→PA0 | DATA线需接4.7kΩ上拉电阻至3.3V |
| OLED(SSD1306) | PB6(SCL), PB7(SDA) | VCC→3.3V, GND→GND, SCL→PB6, SDA→PB7 | 必须选用带电平转换的I²C模块 |
| 独立按键(K1-K4) | PA1-PA4 | 一端接PA1-PA4,另一端接地 | 每个按键并联0.1μF电容滤波 |
| HC-05蓝牙 | PA9(TX), PA10(RX) | VCC→5V, GND→GND, TX→PA10, RX→PA9 | RX需经1kΩ电阻降压,避免5V信号损伤3.3V IO |
| 继电器模块 | PB0 | IN→PB0(经1kΩ限流), VCC→5V, GND→GND, COM/NO接加湿器 | 继电器线圈侧必须加续流二极管(模块自带) |
| 蜂鸣器 | PB1 | VCC→PB1, GND→GND | 选用有源蜂鸣器(标注“ACTIVE”) |
| 水位开关 | PC13 | VCC→3.3V, GND→GND, OUT→PC13 | 开关为常开型,水满时输出低电平 |
提示:PA9/PA10是USART1的TX/RX,默认复用功能。在
stm32f10x_conf.h中必须取消注释#define USE_STDPERIPH_DRIVER,并在main.c的USART1_Init()前调用RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_USART1, ENABLE);和RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOA, ENABLE);开启时钟。接线完成后,用万用表蜂鸣档测试所有GND是否导通,这是排除90%通信故障的第一步。
4.2 Keil工程配置与编译:五个必须检查的致命设置
Keil uVision5工程(KEY.uvprojx)已预配置好,但首次编译前仍需人工核对以下五项,否则必然报错:
- Device选择:Project → Options for Target → Device → 选择
STM32F103C8,必须勾选“Use MicroLIB”(否则printf重定向会失败); - Output设置:Output → 勾选“Create HEX File”,便于J-Link烧录;
- C/C++设置:C/C++ → Define中填入
USE_STDPERIPH_DRIVER, STM32F10X_MD(MD表示中密度芯片,Flash=64KB); - Include Paths:C/C++ → Include Paths中添加
.\USER;.\CMSIS;.\FWLIB\inc;.\FWLIB\src,确保头文件路径正确; - Debug配置:Debug → Settings → SW Device中选择
STM32F103C8,Flash Download中勾选STM32F10x_64.FLM编程算法。
编译成功后,KEY.axf文件生成,大小约32KB(占Flash一半),RAM占用约8KB(在20KB限额内)。若出现Error: L6218E: Undefined symbol,通常是.h文件未被包含或函数声明缺失;若Warning: #1-D: last line of file ends without a newline,则是某个.c文件末尾缺回车——Keil对此很敏感。
4.3 固件烧录与初始验证:三步确认系统心跳正常
使用J-Link烧录固件(KEY.hex)后,按以下顺序验证各模块是否工作:
-
第一步:电源与基础通信
上电后,观察开发板LED(PC13)是否常亮(程序入口处已点亮);用USB转TTL模块(CH340)接PA9/PA10,电脑端打开串口助手(波特率9600),发送GET_STATUS,应收到类似STATUS:0.0,0.0,ERR,AUTO的回复(初始值为0,ERR表示DHT11未连接); -
第二步:传感器与执行机构
将DHT11接入PA0,等待2秒后再次发送GET_STATUS,应看到湿度/温度值跳变(如STATUS:42.5,22.3,OK,AUTO);按下K1切换为手动模式,K2/K3调节目标湿度,观察OLED屏幕左上角显示是否同步更新; -
第三步:OLED与蓝牙联动
此时OLED应显示四行:第一行“HUMI:42.5%”,第二行“TEMP:22.3C”,第三行“TGT:45%”,第四行“AUTO OK”。用手机打开蓝牙,搜索“HC-05”,配对码输入“1234”,连接成功后APP界面自动刷新数据。尝试在APP中修改目标湿度,OLED第四行应变为“TGT:48%”,证明双向通信畅通。
实操心得:第一次烧录后OLED不亮?别急着怀疑代码。先用万用表测PB6/PB7电压,正常应为3.3V;再测OLED模块VCC是否真有3.3V(有些劣质模块标称3.3V实为5V);最后用逻辑分析仪抓I²C波形,确认是否有SCL/SDA信号。我曾因OLED模块虚焊,折腾两天才发现焊点发黑——用烙铁补焊3秒,世界立刻清净。
4.4 手机APP开发:用Python写一个跨平台调试工具
配套的app.py是一个基于Python的跨平台蓝牙调试工具,适用于Windows/macOS/Linux。它利用pybluez(Windows需额外安装WinUSB驱动)或lightblue(macOS)库实现蓝牙通信,界面用tkinter构建,简洁到只有三个输入框和四个按钮。
核心逻辑如下:
import bluetooth
def connect_bt():
target_name = "HC-05"
target_address = None
nearby_devices = bluetooth.discover_devices()
for addr in nearby_devices:
if target_name == bluetooth.lookup_name(addr):
target_address = addr
break
sock = bluetooth.BluetoothSocket(bluetooth.RFCOMM)
sock.connect((target_address, 1))
return sock
def send_cmd(sock, cmd):
sock.send(cmd.encode('utf-8') + b'\r\n')
time.sleep(0.1)
return sock.recv(1024).decode('utf-8')
# GUI部分省略,重点是send_cmd函数
使用方法:运行python app.py,点击“Connect”自动搜索并连接HC-05,界面显示当前湿度/温度,下方输入框可发送任意指令(如SET_HUMI:50),点击“Send”即执行。这个APP的价值在于:它不依赖特定手机系统,开发者可在任何电脑上调试,且源码开放,学生可直接学习蓝牙RFCOMM通信原理。
注意:Windows用户需安装
pybluez和WinUSB驱动;macOS用户需启用“蓝牙共享”权限;Linux用户需sudo apt-get install bluez python3-bluez。若连接失败,请确认HC-05的LED是否快闪(配对中)或慢闪(已配对),快闪时手机才能搜到。
5. 常见问题与排查技巧实录:那些让我凌晨三点改代码的Bug
5.1 典型问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| DHT11始终读取失败(返回0) | 1. DATA线未接上拉电阻 2. PA0被其他外设复用 3. 供电不足(DHT11峰值电流2.5mA) | 1. 用万用表测PA0对GND电压,应为3.3V 2. 检查 stm32f10x_conf.h是否禁用其他模块3. 换用开发板5V输出供电 | 在DATA线与3.3V间焊接4.7kΩ电阻;确认PA0无复用;DHT11单独接稳压模块 |
| OLED显示乱码或全黑 | 1. I²C地址错误(0x78或0x7A) 2. SCL/SDA接反 3. 模块不支持3.3V | 1. 用逻辑分析仪抓I²C起始信号 2. 查模块丝印确认SCL/SDA标识 3. 测模块VCC引脚电压 | 修改oled.c中OLED_I2C_ADDRESS为正确值;交换PB6/PB7接线;更换带电平转换模块 |
| HC-05连接后无数据收发 | 1. RX/TX接反 2. 波特率不匹配 3. 手机未授予蓝牙权限 | 1. 用万用表测PA9/PA10电压,TX应为3.3V波动 2. 用串口助手发AT指令测试 3. Android 12+需手动开启位置权限 | 交换PA9/PA10接线;用AT+UART?查询当前波特率;在手机设置中开启APP蓝牙权限 |
| 继电器不动作或常闭 | 1. PB0驱动电流不足 2. 继电器模块输入极性反接 3. 程序未使能GPIO时钟 | 1. 测PB0电压,低电平时应为0V 2. 查模块标注“IN+”“IN-” 3. 检查 RCC_APB2PeriphClockCmd()调用 | 加S8050三极管驱动;IN+接PB0,IN-接GND;确认RCC_APB2PeriphClockCmd(RCC_APB2PERIPH_GPIOB, ENABLE)已调用 |
| 水位检测始终显示“LOW WATER” | 1. 浮球开关常闭型误用 2. PC13上拉电阻失效 3. 水位开关接触不良 | 1. 用万用表测开关两端电阻,水满时应为0Ω 2. 测PC13对GND电压,空载应为3.3V 3. 晃动开关听咔哒声 | 更换常开型浮球开关;更换10kΩ上拉电阻;清洁开关触点 |
5.2 独家避坑技巧:来自血泪经验的三条铁律
铁律一:永远先验证最小系统,再叠加功能
不要一上来就接齐所有模块。我的标准流程是:1)只接DHT11,串口打印原始数据;2)加入OLED,显示固定字符串;3)加入按键,实现模式切换;4)最后接蓝牙和继电器。每一步成功后再进行下一步。曾有一次我同时接了OLED和HC-05,结果两者I²C和USART抢中断,导致OLED闪烁、蓝牙断连,花了6小时才定位到是SysTick中断优先级设置错误——后来我把所有外设中断优先级按重要性排序:SysTick > USART > EXTI(按键)> I²C,问题迎刃而解。
铁律二:硬件问题永远排在软件问题前面
当现象诡异时(如OLED偶尔闪屏、蓝牙间歇性断连),第一反应不是改代码,而是查硬件。我的标准排查包里必备:数字万用表(测电压/通断)、逻辑分析仪(抓时序)、USB放大镜(查虚焊)。90%的“疑难杂症”都是虚焊、错焊、电源噪声或静电击穿。例如,某次OLED显示残影,我以为是显存刷新问题,结果用放大镜发现PB7焊盘有细微裂纹——重新补焊后一切正常。
铁律三:日志是你的第二双眼睛
在main.c的while(1)循环开头,插入printf("Loop:%d\r\n", loop_count++);,并通过fputc重定向到USART1。这样每次循环都会在串口助手中打印一行,你可以清晰看到程序是否卡死、中断是否正常触发、各模块执行频率是否符合预期。当蓝牙通信异常时,我在USART1_IRQHandler()中添加printf("RX:%02X\r\n", RxBuffer[RxCounter]);,立刻发现手机APP发送了非法字符0xFF,从而在APP端修复编码问题。
最后分享一个小技巧:在
README.md中,我不仅写了编译步骤,还附上了常见错误截图与解决方案(如Keil报错Error: L6218E对应缺少.h文件,Warning: #1-D对应文件末尾缺回车)。这份文档不是给高手看的,而是给那个正在宿舍熬夜、头发快掉光、对着报错信息发呆的大三学生写的——他需要的不是原理,而是“下一步该点哪里”。
6. 总结与延伸思考:从加湿器到你的第一个IoT产品
这个项目走到这里,已经不是一个简单的课程设计作业,而是一套完整的嵌入式产品开发范式。它涵盖了从需求分析(解决干燥环境下的加湿失控问题)、硬件选型(为什么选DHT11而非SHT30)、电路设计(继电器驱动必须加三极管)、固件开发(分层架构+状态机)、调试验证(逻辑分析仪抓波形)、到用户体验(OLED显示+手机APP)的全链条。你亲手搭建的不仅是一台加湿控制器,更是理解“智能硬件”本质的钥匙——所谓智能,不是堆砌传感器,而是让每个部件在正确的时间、以正确的方式、做正确的事。
如果你打算把这个项目继续深化,这里有三个务实的方向:
第一,增加PWM雾量调节。现有继电器只能开关控制,雾量单一。可以将继电器换成MOSFET驱动超声波雾化片,用TIM3的PWM通道(PB0)调节占空比,实现0~100%无级雾量控制。代码只需在main.c中添加TIM_SetCompare1(TIM3, pwm_value),pwm_value根据湿度差值动态计算。
第二,接入Home Assistant。用ESP8266作为WiFi网关,运行MicroPython固件,通过MQTT协议将温湿度、水位、模式等状态上报到HA,再用HA的UI组件构建家庭环境看板。这一步的关键是协议转换:STM32通过UART向ESP8266发送JSON字符串,ESP8266解析后发布到MQTT主题。
第三,加入OTA远程升级。在Flash中划分Bootloader区(4KB)和Application区(60KB),Bootloader监听蓝牙指令UPDATE_START,接收新固件bin文件并写入Application区,校验CRC后跳转执行。这需要深入理解STM32的向量表偏移和Flash写保护机制,但完成后,你的设备就能像智能手机一样在线升级了。
不过,在你跃跃欲试之前,请记住我最想强调的一点:不要追求“最新技术”,而要追求“最稳实现”。我见过太多学生为了在毕设里加上“AI温湿度预测”,硬塞进一个TensorFlow Lite模型,结果内存溢出、实时性崩溃,最后答辩时演示失败。而这个DHT11+OLED+蓝牙的组合,它不炫酷,但每一个环节都经过千百次验证,每一行代码都经得起示波器检验,每一次开机都能稳定运行72小时以上。这才是工程师真正的底气——不是知道多少名词,而是能把一件事,做到极致可靠。
所以,现在就打开你的Keil,新建一个工程,照着这篇文字,把第一个DHT11读数打印到串口吧。当屏幕上跳出“HUMI:42.5%”的那一刻,你就已经站在了嵌入式开发的真实世界门口。门后是什么?是无数个等待被你点亮的LED,是无数条等待被你解析的I²C波形,是无数个深夜里,你和MCU之间无声却默契的对话。
简介:基于STM32F103C8T6主控,实现环境湿度与温度实时采集(DHT11)、水位状态检测、加湿器自动启停控制,并支持本地按键切换手动/自动模式、调节湿度阈值、OLED屏动态显示当前参数。通过HC-05蓝牙模块与手机APP通信,可远程读取实时湿度数据、设置目标湿度范围、开关加湿设备、触发蜂鸣报警提醒。工程包含完整驱动层代码(DHT11温湿度、SSD1306 OLED、独立按键、继电器输出控制、浮球水位检测、UART蓝牙透传协议解析)及应用逻辑层(数据滤波、模式调度、阈值比较、报警判断)。Keil uVision5工程结构清晰,含main.c、中断服务程序、传感器数据处理模块(humiture_data.c/h)、蓝牙指令解析函数、系统初始化配置等,所有源码均有详细注释,变量命名规范,配套README.md提供编译环境配置、固件烧录步骤、硬件接线说明及功能验证方法。适用于高校电子类课程设计、毕业设计开发或嵌入式初学者实操练习。


被折叠的 条评论
为什么被折叠?



