2026嵌赛--基于沁恒 CH32V307 的智能衣物管理衣柜系统与多维度安全监测实现(已开源)
项目总体展示
基于沁恒 CH32V307 的智能衣物管理衣柜系统与多维度安全监测实现_哔哩哔哩_bilibili
项目简介
用沁恒 CH32V307 做了个智能衣柜,前后调了挺久,踩了一堆坑,这篇把整个过程记录下来,同时开源分享给大家(附文章末尾)。
主要功能:温湿度监测、空气质量检测(MQ-135)、自动通风除湿、紫外线消毒、加热烘干、补光照明、语音控制,通过 ESP8266 接 OneNET 云平台,用微信小程序远程控制。还加了个 ESP32-S3-Cam,结合 YOLOv8 做衣物识别(这部分还在收尾中)。
关键词:CH32V307 · 智能衣柜 · RISC-V · OneNET MQTT · ESP8266 · 嵌入式裸机调度
整个项目的定位是低成本 + 高集成度,所有控制逻辑跑在裸机上,没用 RTOS,MCU 是 CH32V307VCT6,288KB Flash / 32KB RAM,够用但不富裕。
项目背景
南方宿舍的衣柜很容易发霉,尤其梅雨季节,放进去的衣服过几天就能闻到霉味。手动开窗通风容易忘,买了除湿盒又懒得换,紫外消毒灯更是摆设——没人定期开过。另外换季翻衣服也很烦,柜子里塞了一堆,根本不知道哪件放哪里。
所以就想做一个能自动检测环境、自动通风、还能云端管理的衣柜:
-
湿度超标自动开风扇
-
空气质量异常(烟雾/酒精)自动蜂鸣报警
-
光线暗了自动开补光灯
-
小程序随时查状态、远程控制
-
语音控制("开风扇"这种)
技术选型
| 模块 | 选型 | 选型理由 |
|---|---|---|
| 主控 MCU | 沁恒 CH32V307VCT6 | RISC-V 内核,144MHz,288KB Flash/32KB RAM,外设够用,价格便宜 |
| 温湿度 | DHT11 | 单总线协议,±2°C 精度,满足衣柜环境监测需求 |
| 空气质量 | MQ-135 | 模拟 + 数字双输出,检测酒精、烟雾等有害气体 |
| 人体感应 | YX55692 红外测距 | 判断门开关状态 |
| 光照检测 | YX55690 光敏电阻 | ADC 采样,给补光灯自动控制用 |
| WiFi | ESP8266 (AT 固件) | UART 驱动 AT 指令,接 OneNET MQTT,成本低 |
| 视觉识别 | ESP32-S3-Cam + YOLOv8 | 识别衣物类型,上报分类结果 |
| 语音控制 | SU-03T 离线语音 | UART 串口,自定义指令词,不用联网 |
| 云平台 | OneNET MQTT | 设备属性上报、命令下发、小程序集成 |
| 显示 | 128×128 LCD 彩屏 | SPI 接口,实时显示环境数据 |
系统架构
硬件架构
以 CH32V307 为核心,通过 GPIO、UART、SPI、ADC 连接各类传感器和执行器。
传感器组:
-
DHT11 温湿度 —— GPIO 单总线(PE3)
-
YX55690 光敏电阻 —— ADC 通道 0(PA0)
-
YX55692 红外测距 —— GPIO 输入(PE6)
-
MQ-135 空气质量 —— ADC 通道 1(PA1)+ GPIO 数字输出(PE5)
-
ESP32-S3-Cam 视觉模块 —— 独立子系统,HTTP API 通信
执行器组:
-
雾化器(DF-HE-30)—— GPIO(PE10)
-
补光 LED 灯带 —— GPIO(PE0)
-
紫外 LED 消毒灯带 —— GPIO(PE2)
-
蜂鸣器 —— GPIO(PE4)
-
7015 风扇 ×2(继电器)—— GPIO(PE1)
-
加热线(继电器)—— GPIO(PE8)
通信模块:
| 接口 | 外设 | 波特率 | 引脚 | 用途 |
|---|---|---|---|---|
| UART1 | USART1 | 115200 | PA9/PA10 | 调试 printf |
| UART4 | UART4 | 9600 | PC10/PC11 | SU-03T 语音模块 |
| UART6 | UART6 | 115200 | PC0/PC1 | ESP8266 WiFi |

系统整体框图:左侧传感器组、右侧执行器组,中间 CH32V307 协调控制,顶部通过 ESP8266 上云
衣柜的机械结构按照内嵌式一体化设计,控制板、传感器、执行器全部内置:

固件架构
系统用的是裸机协作式调度器,没有 RTOS,所有任务按固定周期顺序跑,一个任务执行时其他任务都等着。
task_t scheduler_task[] = {
{Light_Proc, 300, 0}, // 光照检测 + 补光灯自动控制
{Fan_Task, 1500, 0}, // 温湿度读取 + 风扇自动控制
{MQ135_task, 500, 0}, // 空气质量检测 + LCD 显示
{Key_Proc, 10, 0}, // 矩阵键盘扫描
{Door_Detect_Task, 100, 0}, // 红外门状态检测
{Speech_Control, 50, 0}, // 语音指令处理
};
系统时钟由 TIM3 提供 1ms 基准(uwTick),所有调度周期基于此计时。

软件分为五层:微信小程序(客户端)→ 业务数据层(微信云开发)→ 物联网中台(OneNET)→ 底层硬件端(CH32V307),另外还有外部服务层(天气 API、YOLOv8)
通信架构
┌─────────────────────────────────────────────────────┐
│ 微信小程序 (WXML/WXSS/JS) │
│ ↕ HTTP API │
│ OneNET 云平台 │
│ ↕ MQTT │
├─────────────────────────────────────────────────────┤
│ CH32V307 + ESP8266 (AT+MQTTPUBRAW / SUB) │
│ ┌───────────────┬──────────────────────┐ │
│ │ UART6 (ESP8266) │ UART4 (SU-03T) │ │
│ │ MQTT ↔ OneNET │ 语音命令 → action │ │
│ └───────────────┴──────────────────────┘ │
└─────────────────────────────────────────────────────┘
完整通信链路:微信小程序通过 HTTPS 访问 OneNET,OneNET 通过 MQTT 下发指令到 ESP8266,ESP8266 通过 UART 通知 CH32V307 执行对应操作
数据上传格式(OneJSON)
{
"id": "123",
"version": "1.0",
"params": {
"Temperature": {"value": 26},
"Humidity": {"value": 60},
"fan": {"value": 1},
"lock": {"value": 0},
"light": {"value": 2048},
"heating": {"value": false},
"UVLamp": {"value": false},
"Humidify": {"value": false},
"Alcohol": {"value": 512},
"FillLamp": {"value": true}
}
}
核心功能实现
1. 环境监测
1.1 温湿度监测(DHT11)
-
通信协议:单总线(One-Wire),PE3(GPIOE Pin 3)
-
采样周期:1500ms(原来是 800ms,但 DHT11 最小采样间隔是 1s,读太快会出问题,见 Q1)
-
分辨率:温度 1°C,湿度 1%RH;精度 ±2°C、±5%RH
-
显示:LCD 第 2 行显示温度,第 3 行显示湿度
-
上报:每次读取成功后 MQTT 上报,字段名
Temperature、Humidity -
驱动流程:GPIO 模式切换(输出→拉低 20ms 复位→输入→等待应答→读取 40bit→校验和验证)
DHT11 的单总线时序比较严格,每次读取需要 20~30ms(含复位拉低的 18ms),这也是协作式调度器里任务阻塞的主要来源之一。
1.2 空气质量监测(MQ-135)
-
模拟输出(AO):PA1(ADC 通道 1),12 位 ADC,值域 0~4095
-
数字输出(DO):PE5(GPIO 上拉输入),低电平表示超标
-
报警:DO 低时蜂鸣器长响,LCD 显示「超标」;正常时显示「正常」
-
上报字段:
Alcohol(模拟量原始值) -
通道管理:每次读取前切换 ADC 通道到 1,读完恢复通道 0,避免和光敏电阻冲突(见 Q2)
1.3 光照检测(YX55690)
-
接口:PA0(ADC 通道 0),12 位 ADC
-
值域:0(全暗)~ 4095(全亮),日光灯下约 3500~4000,全黑约 500
-
上报字段:
light -
平均滤波:连续采样 10 次取平均,消除瞬时波动
1.4 门状态检测(YX55692 红外测距)
-
接口:PE6(GPIO 上拉输入)
-
传感器逻辑:0 = 有遮挡(门关),1 = 无遮挡(门开)
-
消抖:关门需连续遮挡 1 秒以上才判定(防止门板晃动误触发),开门立即响应
-
上报字段:
lock(0=关,1=开) -
LCD 显示:第 4 行显示「有人」或「无人」,状态变化时立即刷新
2. 环境自动调节
2.1 风扇自动通风除湿
-
控制引脚:PE1,驱动继电器 → 2×7015 风扇
-
自动逻辑:湿度 > 60%RH 时自动开风扇,湿度回落后自动关
-
远程接管:小程序下发
fan指令后置fan_manual=1,不再自动控制 -
上报字段:
fan(0/1) -
控制方式:自动(湿度触发)+ 语音 + 远程 + 键盘
原来阈值设的 45%RH(代码注释里还留着),改成 60%RH 之后风扇不会频繁启停了。
2.2 补光灯自动控制
-
控制引脚:PE0,驱动高亮 LED 灯带
-
自动逻辑:门开(有人)且光强低于 3800 时自动开补光灯
-
远程接管:小程序下发
FillLamp后置fill_manual=1,自动控制让位 -
上报字段:
FillLamp(true/false)
2.3 紫外线消毒
-
控制引脚:PE2,驱动紫外 LED 灯带
-
控制方式:仅支持语音/远程/键盘,没有自动定时逻辑
-
上报字段:
UVLamp(true/false)
规划了 24 小时定时消毒(每次 8 分钟),当前版本没做,打算后面在调度器里加个 UV_Timer_Task。
2.4 加热烘干
-
控制引脚:PE8,驱动加热线,继电器隔离
-
控制方式:键盘 + 语音 + 远程,没有自动逻辑
-
上报字段:
heating(true/false)
2.5 雾化加湿
-
控制引脚:PE10,驱动 DF-HE-30 雾化器
-
控制方式:键盘 + 语音 + 远程,没有自动逻辑
-
上报字段:
Humidify(true/false)
3. 多维度安全监测
3.1 MQ-135 气体超标报警
-
检测对象:酒精蒸汽、烟雾、甲醛、氨气等有害气体
-
报警方式:蜂鸣器持续鸣响(PE4,低电平有效)
-
显示:LCD 第 3 行右侧显示「正常」/「超标」
-
上报字段:
Alcohol(模拟量原始值,数字量仅本地报警) -
阈值调节:通过模块上的电位器旋钮设置 DO 触发阈值
3.2 红外门状态监测
-
检测方式:YX55692 红外测距传感器,检测门板与传感器间的距离变化
-
防误报:关门需连续遮挡 >1 秒才判定(防止门板晃动误触发)
-
开门响应:无遮挡时立即判定
-
联动上报:门状态变化时立即上传到 OneNET,不等 10s 定时周期
4. 三端控制系统
系统有三种独立的控制通路:
4.1 矩阵键盘(本地物理按键)
-
规格:4×4 矩阵,16 个按键
-
引脚分配:
-
行(输出):PD11(R4), PD9(R3), PE15(R2), PE13(R1)
-
列(输入、上拉):PE11(C1), PE9(C2), PE7(C3), PC5(C4)
-
-
扫描方式:逐行拉低,读列电平,每行切换后
Delay_Us(5)等待电平稳定 -
调度周期:10ms,最高频任务
-
边沿检测:
key_down = key_val & (key_val ^ key_old)识别按键下降沿,避免重复触发
| 按键 | 功能 | 按键 | 功能 |
|---|---|---|---|
| 1 | 开紫外灯 | 2 | 关紫外灯 |
| 3 | 开加湿器 | 4 | 关加湿器 |
| 5 | 开加热器 | 6 | 关加热器 |
4.2 SU-03T 离线语音控制
-
接口:UART4(PC10-TX, PC11-RX),9600bps
-
数据格式:单字节 HEX 模式(非 ASCII 文本),每字节对应一条指令
| 指令值(HEX) | 语音指令词 | 执行动作 |
|---|---|---|
| 0x01 | 打开风扇 | Fan_Set(1) |
| 0x02 | 打开加热 | Warm_Task(1) |
| 0x03 | 打开雾化 | Humidifier_Task(1) |
| 0x04 | 打开紫外 | Led_Task(1) |
| 0x06 | 打开补光 | FillLamp_Set(1) |
| 0x11 | 关闭风扇 | Fan_Set(0) |
| 0x12 | 关闭加热 | Warm_Task(0) |
| 0x13 | 关闭雾化 | Humidifier_Task(0) |
| 0x14 | 关闭紫外 | Led_Task(0) |
| 0x16 | 关闭补光 | FillLamp_Set(0) |
| 0x21 | 查室温 | Send_Temperature_Reply() |
-
中断处理:UART4 RXNE 中断 → else-if 链匹配 → 设置
action变量 →Speech_Control()消费(调度周期 50ms)
语音模块曾有 ASCII/HEX 模式配置问题(见 Q4),修复后各指令稳定触发。
4.3 微信小程序远程控制
-
通信链路:微信小程序 → OneNET HTTP API → MQTT → ESP8266 AT → CH32V307 UART6
-
下发格式:
{"id":"xxx","version":"1.0","params":{"fan":{"value":1}}} -
处理流程:
-
mqtt_cmd_poll()逐行扫描 UART6 接收缓冲区 -
匹配
+MQTTSUBRECV前缀 → 提取 JSON payload →mqtt_handle() -
mqtt_handle()调用json_bool()解析各指令 -
执行对应控制函数(带远程接管锁存,如
Fan_Set()会置fan_manual=1) -
回复
set_reply(code:200)+ 立即上报最新状态
-
4.4 执行器远程接管锁存
| 执行器 | 变量 | 自动条件 | 远程指令覆盖 | 键盘/语音覆盖 |
|---|---|---|---|---|
| 风扇(PE1) | fan_manual |
湿度>60%自动 | ✅ 置 fan_manual=1 |
不影响自动 |
| 补光灯(PE0) | fill_manual |
门开+光暗自动 | ✅ 置 fill_manual=1 |
不影响自动 |
| 紫外灯(PE2) | — | 无自动逻辑 | ✅ 直接控制 | ✅ 直接控制 |
| 加热器(PE8) | — | 无自动逻辑 | ✅ 直接控制 | ✅ 直接控制 |
| 加湿器(PE10) | — | 无自动逻辑 | ✅ 直接控制 | ✅ 直接控制 |
5. LCD 本地显示(128×128 SPI 彩屏)
-
接口:SPI(PB3-SCLK, PB5-MOSI),GPIO(PD3-DC, PB4-RES)
-
显示布局(4 行,16×16 中文字体):
-
第 1 行:光强度(
光强度: XXXX) -
第 2 行:温度(
温度: XX) -
第 3 行:湿度 + 酒精状态(
湿度: XX+酒精状态: 正常/超标) -
第 4 行:门状态 + 执行器状态
-
-
刷新策略:各任务独立刷新自己对应的区域,非全屏刷新
6. 视觉衣物识别(ESP32-S3-Cam + YOLOv8)
-
架构:独立子系统,不占用 CH32V307 计算资源
-
通信:HTTP API(ESP32-S3-Cam 发起请求,访问 YOLOv8 推理服务)
-
识别目标:T 恤、外套、裤子、裙子等衣物分类
-
数据通路:ESP32-S3-Cam 拍照 → HTTP POST → YOLOv8 推理 → 识别结果 → 通过 ESP8266 MQTT 上报到 OneNET
-
当前状态:代码框架已有(
case 7/case 17预留),拍照→识别→结果回传的完整链路还没打通。
7. MQTT 云平台通信
-
平台:OneNET 物联网开放平台(MQTT 3.1.1)
-
硬件:ESP8266 (AT 固件) + UART6(115200bps,PC0/PC1)
-
连接流程:
-
AT+RST复位 ESP8266 -
AT+CWMODE=1Station 模式 -
AT+CWJAP="SSID","PWD"连接 WiFi -
AT+MQTTUSERCFG配置 MQTT 客户端(ID、用户名、密码——OneNET token) -
AT+MQTTCONN连接 Broker -
AT+MQTTSUB订阅property/set主题
-
-
属性上报:通过
AT+MQTTPUBRAW发布 OneJSON payload,含 10 个数据字段 -
自动重连:检测到
+MQTTDISCONNECTED后,主循环每 20 秒尝试一次完整重连 -
断线保护:
upload_temp_humi()入口检查mqtt_online,未连接时跳过发布
8. 执行器 GPIO 引脚分配
| 功能 | 引脚 | 有效电平 | 驱动对象 |
|---|---|---|---|
| 补光灯 | PE0 | 高=开,低=关 | 高亮 LED 灯带 |
| 风扇(继电器) | PE1 | 高=开,低=关 | 7015 风扇 ×2 |
| 紫外灯 | PE2 | 高=开,低=关 | 紫外 LED 灯带 |
| 蜂鸣器 | PE4 | 低=响,高=静 | 有源蜂鸣器 |
| 加热线(继电器) | PE8 | 高=开,低=关 | 加热线缆 |
| 雾化器 | PE10 | 高=开,低=关 | DF-HE-30 |
问题与优化
这部分是整篇文章的重点,记录了开发过程中踩过的坑。每个问题按「发现 → 查找 → 确认 → 解决」四步展开,有些当时真的找了挺久才搞清楚原因。
Q1:DHT11 温湿度数据显示粘滞,变化不明显
发现: 温度始终在 26°C 附近,湿度几乎不动,不管对着传感器吹气还是拿湿手靠近,读数基本没什么变化。LCD 和 OneNET 云平台上看到的数据一致——都像是"冻住了"。
查找:
-
先排除云平台缓存——LCD 本地显示和 OneNET 上报数据一样,说明问题在传感器侧
-
在
DHT11_Read_Data()前后加printf打印返回值和temp/humi,观察每次读取结果 -
发现日志里连续多次读取返回值都是 0,但
temp没变——函数说"成功"但数据没更新
确认: 翻了 DHT11_Read_Data() 的源码,找到了问题:
if(校验通过) {
*temp = buf[2]; // 更新数据
}
else {
// 校验失败:什么都没做!但还是走到了 return 0
}
return 0; // ← 不管校验过没过,都返回"成功"
校验和失败时 *temp 没有更新,但函数还是返回 0,调用者以为读成功了,实际用的是上一轮的旧值。
进一步排查发现两个叠加的原因:
-
Fan_Task调度周期 800ms,DHT11 最小采样间隔是 1s,读太频繁导致部分读取失败 -
DHT11 精度 ±2°C、分辨率 1°C,室内稳定环境下读数天然变化小
解决:
-
校验和失败时返回 2(新增错误码),不再和成功混用返回值 0
-
Fan_Task调度周期从 800ms 改为 1500ms,满足 DHT11 最小间隔 -
所有调用点增加对返回值 2 的处理
教训: 错误码设计要清晰——"通信成功但数据无效"和"完全成功"是两种不同的状态,不能用同一个返回值混淆。传感器驱动必须考虑数据手册里的时序约束。
Q2:MQ-135 空气质量与光敏传感器读数完全一致
发现: OneNET 云平台上 Alcohol(MQ-135)和 Light(光敏电阻)两个数据点数值完全相同,本地 printf 打印也是一样的。
查找: 两个传感器用不同 ADC 通道——光敏电阻在 PA0(通道 0),MQ-135 在 PA1(通道 1)。硬件接线没问题的话不可能读数一样,重点查 ADC 通道配置。
-
检查
Light_Init()和MQ135_Init()的初始化代码,发现两个函数都调用了ADC_Init()和RCC_ADCCLKConfig(),后者覆盖前者 -
Light_Init设置连续转换模式,MQ135_Init设置单次模式 -
MQ135_Init最后调用,ADC 最终运行在单次模式
确认: 连续模式下,ADC_SoftwareStartConvCmd() 是空操作——ADC 已经在持续转换,不需要重新启动。MQ-135 读完切回通道 0 后,ADC 可能还在转换通道 1,通道切换要等下一轮才生效。Get_ADC_Average() 依赖别人把通道恢复到 0,但在连续模式下这个恢复不可靠。
解决:
-
Get_ADC_Average()函数头部显式切换到通道 0 -
MQ135_Get_Analog()每次读取前显式切换到通道 1 -
两个函数各管自己的通道,互不依赖
-
MQ135_Init不再重复初始化 ADC,由Light_Init统一配置
uint16_t Get_ADC_Average(uint8_t times) {
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_28Cycles5);
// ← 主动切通道
uint32_t sum = 0;
for(uint8_t i=0; i<times; i++) {
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC));
sum += ADC_GetConversionValue(ADC1);
Delay_Ms(1);
}
return sum / times;
}
修改后两边读数不再相同,各自符合物理量的正常范围。
教训: 共享硬件资源时,每个使用者要主动配置自己需要的状态,不要依赖别人"恢复"。连续转换模式下的通道切换时序特别坑——寄存器值改了,ADC 硬件还没开始用新配置。
Q3:系统上电后约 10 秒调度器才开始工作
发现: MCU 上电后,按键无反应、语音无响应、LCD 不更新,等大约 10 秒后才恢复正常。每次上电都这样,时间基本固定。
查找: 在 main() 里逐个位置加时间戳 printf,发现时间全被 ESP8266/MQTT 初始化吃掉了。每个 Delay_Ms() 和 uart6_wait_for() 都是同步阻塞的,加起来将近 14 秒:
Delay_Ms(3000) —— 等待 ESP8266 上电启动 → 3s
Delay_Ms(2000) —— AT+RST 复位后等待 → 2s
uart6_wait_for("ready", 5000) —— 等待模块就绪 → ~1s
uart6_wait_for("OK", 15000) —— AT+CWJAP WiFi 连接 → ~3s
uart6_wait_for("OK", 10000) —— AT+MQTTCONN MQTT 连接 → ~3s
uart6_wait_for("OK", 5000) —— AT+MQTTSUB 订阅 → ~1s
uart6_wait_for("OK", 5000) —— 初始数据上报 → ~1s
确认: 所有初始化都在 while(1) 之前,调度器没启动之前这些代码在同步跑。这是架构层面的问题——初始化流程的设计没有考虑不能让用户等。
解决思路: ESP8266/MQTT 初始化改造为状态机,在调度器中分步执行:
typedef enum { WIFI_RESET, WIFI_CONNECT, MQTT_CONFIG, MQTT_CONN, MQTT_SUB, MQTT_READY } init_state_t;
init_state_t net_state = WIFI_RESET;
void network_init_task() {
switch(net_state) {
case WIFI_RESET:
uart6_send("AT+RST\r\n"); net_state = WIFI_CONNECT; start_timeout(5000);
break;
case WIFI_CONNECT:
if(received("ready")) { net_state = WIFI_CONFIG; }
else if(timeout) { retry_or_skip(); }
break;
// ...
}
}
每次 network_init_task() 执行只需几毫秒,不阻塞其他任务。这个改造对通信架构改动较大,计划在下一版本实现。
教训: 裸机里任何阻塞延时都是在浪费 CPU。ESP8266 AT 指令的交互方式很容易写出线性阻塞的初始化流程——正确做法是状态机 + 超时机制,没有 RTOS 也能实现非阻塞初始化。
Q4:语音模块关闭指令完全无效
发现: 说"开风扇"(指令 1)有时候能触发,说"关风扇"(指令 11)一次都没成功过。两个指令的代码路径完全对称,不应该出现这种差异。
查找:
-
用按键逐一测试各执行器,确认硬件本身没问题,问题在语音接收侧
-
用 USB-TTL 连接 SU-03T 的 UART 输出,在电脑上直接看模块发的数据
-
串口助手文本模式下看到模块发的是字符
"1"、"2"——看起来正常 -
切换到 HEX 模式后发现,模块实际发的是
0x31(ASCII'1'),不是预期的0x01
确认:
if(data==0x01) action=1; // 期望 0x01,实际收到 0x31,匹配失败
if(data==0x11) action=11; // 期望 0x0B,实际收到两个字节 0x31 0x31,更不可能匹配
SU-03T 的串口输出格式可配置为 HEX 模式或 ASCII 文本模式,模块当时配的是文本模式,发的是 ASCII 字符,而代码期望的是二进制值。
"开风扇"偶尔能触发,是因为 UART4 波特率之前配置有误,偶尔误码碰巧匹配上了。
解决:
-
把 SU-03T 语音模块的串口输出格式改成「十六进制/二进制模式」
-
改完后用 USB-TTL HEX 模式逐一验证每个指令词发的字节值
-
确认 UART4 波特率与模块配置一致
教训: 串口调试必须用 HEX 模式看原始字节。文本模式显示的 "1" 可能是 0x01、0x31 甚至 0x81,肉眼看起来一样,实际差很多。HEX 模式是串口调试的第一选择。
Q5:按键控制不可靠
发现: 矩阵键盘按下后时灵时不灵,在温湿度读取周期附近按键基本触发不了。边沿检测逻辑(key_down = key_val & (key_val ^ key_old))已经有了,但还是不灵。
查找:
-
在
Key_Proc入口加printf打印每次扫描的key_val——按键按下时有时读到键值,有时读到 0xFF(无按键) -
时间戳打印发现
Fan_Task执行时Key_Proc被推迟了 20~30ms -
Fan_Task内部调用DHT11_Read_Data(),里面有Delay_Ms(20)拉低总线 + 等待应答
确认: 两个问题叠加:
问题 A——调度阻塞:Fan_Task 每次执行约 20~30ms(被 DHT11 卡住),Key_Proc 这段时间内完全无法运行。如果按键在这 20~30ms 窗口内按下并释放,边沿检测永远捕不到。
问题 B——消抖缺失:Key_Read() 切换行线后立即读列线,没有延时。机械按键弹跳(通常 5~10ms)导致读到的列电平不稳定。
// 原始代码
GPIO_ResetBits(GPIOD, GPIO_Pin_11); // 切换行
// ← 没有任何延时!
if(GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_11)==0) temp=13; // 立即读列
解决:
-
Fan_Task周期从 800ms 改为 1500ms(Q1 已同步处理) -
Key_Read()每次切换行线后加Delay_Us(5),等电平稳定再读列
教训: 协作式调度器里任务执行时间是关键约束。DHT11 的 20ms 阻塞延时和机械按键弹跳叠加,把按键的有效窗口压得很短。根本解法是把 DHT11 改为异步状态机,治标的方法是延长扫描间隔。
Q6:红外门状态检测逻辑反且延时过长
发现: 衣柜门关上时,LCD 显示「有人」;门打开时显示「无人」——和实际完全相反。
查找:
-
手动测试 YX55692:用手挡住传感器 → 输出低电平(0),移开 → 高电平(1)
-
查
infrared.h文档:"0: 触发(有遮挡), 1: 未触发(无遮挡)"——传感器电平定义是清楚的 -
查
Door_Detect_Task():有遮挡 →Door_State=1(开门),无遮挡 →Door_State=0(关门)
确认: 传感器安装在衣柜内侧朝向柜门:
-
柜门关上 → 门板靠近传感器 → 有遮挡 → 应该是"关门"
-
代码里有遮挡映射为"开门"——逻辑反了
同时发现消抖延时设的是 10000ms(10 秒),关门后要等 10 秒 LCD 才更新,体验很差。
解决:
// 修改后
if(有遮挡) { 计数++;
if(计数>=1000ms) Door_State=0; // 关门(1秒消抖)
}
else { Door_State=1; } // 开门(立即响应)
-
映射关系翻转:有遮挡 → 关门,无遮挡 → 开门
-
消抖从 10 秒缩到 1 秒
教训: 传感器逻辑映射必须在物理安装后做验证。"遮挡"本身没有业务含义,取决于安装位置和检测方向。安装完第一件事:用手挡住传感器,看代码里的变量是否对应了正确的业务状态。
Q7:ADC 初始化配置冲突
发现: 查代码时发现 Light_Init 和 MQ135_Init 都对 ADC1 做了完整初始化——两个模块共享同一个外设,各自初始化一次肯定有问题。
查找:
| 配置项 | LIGHT_INIT |
MQ135_INIT |
|---|---|---|
| ADC 时钟分频 | RCC_PCLK2_Div8(12MHz) |
RCC_PCLK2_Div6(16MHz) |
| 转换模式 | ContinuousConvMode = ENABLE |
ContinuousConvMode = DISABLE |
| 通道配置 | 通道 0,28.5 周期采样 | 通道 1,239.5 周期采样 |
调用顺序是 Light_Init() → MQ135_Init(),后者会完整覆盖前者的配置。
确认: 特别严重的是时钟分频:RCC_PCLK2_Div6 在 96MHz 系统时钟下把 ADC 时钟设为 96/6 = 16MHz,而 CH32V30x 的 ADC 时钟上限是 14MHz,时钟超标可能导致 ADC 转换结果不稳定。
解决: MQ135_Init 不再调用 ADC_Init() 和 RCC_ADCCLKConfig(),只初始化 GPIO。ADC 由 Light_Init 统一配置,通道切换由各模块运行时自己管理。
// 修改后的 MQ135_Init——只配 GPIO,不碰 ADC 公共配置
void MQ135_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure = {0};
RCC_APB2PeriphClockCmd(MQ135_AO_RCC | MQ135_DO_RCC | MQ135_ADC_RCC, ENABLE);
GPIO_InitStructure.GPIO_Pin = MQ135_AO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(MQ135_AO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = MQ135_DO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_Init(MQ135_DO_PORT, &GPIO_InitStructure);
}
教训: 外设初始化应由一个模块统一管理,其他模块只配置自己用的部分(通道号、采样时间)。两个模块各自完整初始化同一个外设,最终配置取决于调用顺序,很脆弱——"代码复制粘贴"的典型后果。
补充优化
| 问题 | 说明 |
|---|---|
| UART4 ISR 效率 | if 链改为 else if,匹配后不再继续判断剩余条件 |
volatile 缺失 |
中断与主循环共享变量添加 volatile |
| 类型不一致 | 混用 u8 与 uint8_t,统一为 uint8_t |
| 死代码清理 | 移除空的 light.c / light.h |
| 代码重复 | _uart_ZZ1~_uart_ZZ4 四个相同结构函数合并为参数化函数 |
未来可优化方向
这部分是当前版本没做完或还可以改进的地方,也是给自己留的路线图。
1. ESP32-S3-Cam 视觉识别管线
现状:CH32V307 固件里预留了触发摄像头的占位代码(action 7/17),ESP32-S3-Cam 硬件已接入,但拍照→HTTP POST→YOLOv8 推理→结果回传的完整链路还没打通。
待做:
-
ESP32-S3-Cam 固件(拍照、WiFi 传输、HTTP 通信)
-
YOLOv8 轻量化部署(TFLite/ONNX)或云端推理服务
-
CH32V307 与 ESP32-S3-Cam 通信协议定义
-
识别结果的本地 LCD 显示
2. 裸机协作式调度器的固有限制
-
任务相互阻塞:DHT11 的 20ms 阻塞让所有低周期任务(如 Key_Proc 10ms)都可能延迟
-
无抢占:高优先级事件必须等当前任务跑完才能处理
-
无优先级反转保护:
mqtt_cmd_poll()解析 JSON 时如果耗时过长,会阻塞所有其他任务
可选路径:
-
迁移到 FreeRTOS / RT-Thread Nano,任务按优先级抢占 + 信号量同步
-
在裸机框架内把阻塞任务改成有限状态机,每次只执行非阻塞片段
3. ESP8266/MQTT 初始化阻塞
将初始化流程改为状态机(Q3 已给出伪代码),分步执行不阻塞,同时引入启动超时跳过机制。
4. DHT11 阻塞式驱动
用硬件定时器辅助的异步状态机,把 DHT11 时序拆成多个阶段,彻底解放 CPU。或者直接换 I2C 接口的 SHT30/AHT20,从根本上消除阻塞问题。
5. ADC 扫描模式 + DMA
当前单次转换 + 软件触发多通道存在时序不可靠问题。可改为扫描模式 + DMA,ADC1 自动扫描通道 0 和通道 1,DMA 搬运结果到缓冲区,CPU 只需读缓冲区,大幅降低 ADC 读取的 CPU 占用。
6. 看门狗(IWDG)
当前代码没有启用独立看门狗。DHT11 单总线偶尔总线锁死(拉低后传感器无应答),没有超时硬复位机制。建议在主循环末尾喂狗,确保任意任务卡死时 MCU 能自动复位。
7. 阈值控制加迟滞
当前单一固定阈值,传感器在阈值附近波动时执行器会频繁启停:
// 有迟滞的风扇控制
#define FAN_ON_HUMI 65 // ≥65% 开启
#define FAN_OFF_HUMI 55 // ≤55% 关闭
if(humi >= FAN_ON_HUMI && !fan_on) { fan_on = 1; GPIO_SetBits(...); }
if(humi <= FAN_OFF_HUMI && fan_on) { fan_on = 0; GPIO_ResetBits(...); }
65% 开启、55% 才关闭,10% 的迟滞带消除反复启停。
8. 其他
| 方向 | 说明 | 优先级 |
|---|---|---|
| 低功耗模式 | 空闲时关闭 WiFi、风扇等大功率器件,RTC 定时唤醒或红外中断唤醒 | 中 |
| UART DMA | UART6 和 UART4 改用 DMA + IDLE 中断,帧完成后统一处理,降低 ISR 开销 | 中 |
| 紫外定时消毒 | 调度器加 UV_Timer_Task,利用 uwTick 实现每日定时消毒(设计报告规划了但未实现) | 中 |
| 数据存储 | 断电后历史数据丢失,可外接 Flash 或用 OneNET 云端存储历史数据 | 低 |
| WiFi 配置 | SSID 密码硬编码在源码,可改为 SmartConfig 或小程序下发配网 | 低 |
| 单元测试 | 整个项目无单元测试,建议为 JSON 解析、调度器逻辑等添加 PC 端可运行的测试用例 | 中 |
| 报警历史 | 酒精超标报警仅本地蜂鸣器响应,无云端报警事件记录和推送通知 | 低 |
项目成果
实物

衣柜打开状态,顶部是控制板和各模块走线,内部挂着衣服,右侧有 LED 灯带

衣柜关闭状态,前方是运行中的小程序界面,展示实时温湿度和设备控制
PCB
PCB 渲染图:上方三个继电器,左下是 SU-03T 语音模块,中间是 CH32V307 主控,右侧是 ESP8266 WiFi 模块和降压模块

PCB 实物:绿色板,ESP8266 在顶部,底部三个蓝色继电器

微信小程序
小程序做了几个核心页面:首页总览、衣柜环境详情(含设备控制)、我的衣橱(衣物管理)、天气预报。

首页:今日穿搭推荐、衣柜环境概览(温湿度 + 空气质量 + 光照)、快捷操作、待清洗提醒

衣柜环境详情:实时温湿度、柜门状态、酒精检测、光强度,以及各执行器的开关控制

我的衣橱:衣物列表,支持按类型筛选,记录穿着次数和清洗状态

天气预报:实时天气 + 穿搭建议,调用和风天气 API
软件成果
-
裸机协作式调度器框架(可复用到其他 CH32/STM32 项目)
-
MQTT over ESP8266 AT 指令通信驱动
-
OneJSON 数据上传与属性同步协议实现
-
问题排查与优化记录(即本文)
总结
| 维度 | 技术点 |
|---|---|
| MCU 底层 | 寄存器级外设配置、中断服务、时钟树、ADC/DMA/UART/SPI |
| 传感器驱动 | DHT11 单总线协议、MQ-135 ADC 采样、红外 IO 检测 |
| 通信协议 | MQTT 3.1.1、TCP/IP over AT、OneJSON 数据格式 |
| 云平台 | OneNET 设备接入、属性上报/设置、设备影子 |
| 软件架构 | 裸机协作式调度器、有限状态机、环形缓冲区 |
| 问题排查 | 调试串口 printf、USB-TTL 抓包、二分法定位、逻辑分析 |
(这个项目最大的价值不是某个单一技术有多深,而是把多个嵌入式技术栈整合在一起——在有限的硬件资源(288KB Flash/32KB RAM、无 RTOS)上实现了一个能用的物联网终端系统。
踩的坑基本都记在上面的 Q&A 里了,大部分都是传感器时序、ADC 共享资源、串口通信格式这些比较实际的问题,希望对类似项目有参考价值。)
引脚分配表
下表列出本系统所有 GPIO 引脚的功能分配,按端口分组。
| 引脚 | 功能 | 方向 | GPIO模式 | 连接对象 | 驱动文件 |
|---|---|---|---|---|---|
| PA0 | 光照检测 ADC | 输入 | AIN | YX55690 光敏电阻 | led.c |
| PA1 | 酒精检测 ADC | 输入 | AIN | MQ-135 AO (模拟量输出) | mq135.c |
| PA9 | 调试串口 TX | 输出 | AF_PP | USB-TTL (调试 printf) | debug.c |
| PA10 | 调试串口 RX | 输入 | IPU | USB-TTL (调试) | debug.c |
| PA15 | LCD 复位 | 输出 | PP | LCD RESET | lcd.c |
| PB3 | LCD SPI 时钟 | 输出 | AF_PP | LCD SCK (SPI3) | lcd.c |
| PB4 | LCD SPI MISO | 输出 | PP | LCD MISO (未使用) | lcd.c |
| PB5 | LCD SPI 数据 | 输出 | AF_PP | LCD MOSI (SPI3) | lcd.c |
| PC0 | ESP8266 UART TX | 输出 | AF_PP | ESP8266 RXD (UART6) | uart.c |
| PC1 | ESP8266 UART RX | 输入 | IPU | ESP8266 TXD (UART6) | uart.c |
| PC5 | 矩阵键盘 列4 | 输入 | IPU | 4×4 薄膜键盘 | key.c |
| PC10 | 语音模块 UART TX | 输出 | AF_PP | SU-03T RXD (UART4) | uart.c |
| PC11 | 语音模块 UART RX | 输入 | IPU | SU-03T TXD (UART4) | uart.c |
| PD3 | LCD 数据/命令 | 输出 | PP | LCD DC | lcd.c |
| PD4 | LCD 片选 | 输出 | PP | LCD CS | lcd.c |
| PD5 | LCD 背光 | 输出 | PP | LCD BL | lcd.c |
| PD9 | 矩阵键盘 行2 | 输出 | PP | 4×4 薄膜键盘 | key.c |
| PD11 | 矩阵键盘 行1 | 输出 | PP | 4×4 薄膜键盘 | key.c |
| PE0 | 补光灯控制 | 输出 | PP | 高亮 LED 灯带 (继电器) | led.c |
| PE1 | 风扇控制 | 输出 | PP | 7015 风扇 ×2 (继电器) | fan.c |
| PE2 | 紫外灯控制 | 输出 | PP | 紫外 LED 消毒灯带 | led.c |
| PE3 | DHT11 数据总线 | 双向 | PP/IPU | DHT11 温湿度传感器 (单总线) | dht11.c |
| PE4 | 蜂鸣器控制 | 输出 | PP | 有源蜂鸣器 (低电平有效) | Beep.c |
| PE5 | 酒精阈值数字输出 | 输入 | IPU | MQ-135 DO (数字量输出) | mq135.c |
| PE6 | 门状态检测 | 输入 | IPU | YX55692 红外测距传感器 | infrared.c |
| PE7 | 矩阵键盘 列3 | 输入 | IPU | 4×4 薄膜键盘 | key.c |
| PE8 | 加热控制 | 输出 | PP | 加热线缆 (继电器) | warm.c |
| PE9 | 矩阵键盘 列2 | 输入 | IPU | 4×4 薄膜键盘 | key.c |
| PE10 | 加湿器控制 | 输出 | PP | DF-HE-30 雾化器 | humidifier.c |
| PE11 | 矩阵键盘 列1 | 输入 | IPU | 4×4 薄膜键盘 | key.c |
| PE13 | 矩阵键盘 行4 | 输出 | PP | 4×4 薄膜键盘 | key.c |
| PE15 | 矩阵键盘 行3 | 输出 | PP | 4×4 薄膜键盘 | key.c |
引脚资源统计: 本系统共使用 32 个 GPIO 引脚,分布在 GPIOA(4)、GPIOB(3)、GPIOC(4)、GPIOD(5)、GPIOE(16) 五个端口。其中模数转换占用 PA0/PA1(ADC1 通道 0/1),三路 UART 占用 6 个引脚(PA9/PA10、PC0/PC1、PC10/PC11),SPI LCD 占用 6 个引脚(含控制 GPIO),4×4 矩阵键盘占用 8 个引脚,其余 6 个引脚为独立执行器控制。
参考资料
[1] 王磊, 张强. 基于STM32的智能衣柜控制系统设计[J]. 电子技术应用, 2022(10):25-27.
[2] 李华, 赵明. 基于物联网的智能家居环境监测系统设计[J]. 计算机与数字工程, 2026, 16(1):132-134.
[3] 陈晓, 刘洋. RISC-V 处理器在物联网终端中的应用[J]. 单片机与嵌入式系统应用, 2021(5):42-43,34.
[4] 吴峰, 周杰. MQTT 协议在物联网中的性能分析[J]. 计算机工程, 2017, 43(3):84-87,91.
[5] 张伟, 李娜. 基于STM32和ESP8266的物联网网关设计[J]. 电子设计工程, 2024, 13(1):116-124. [6] 沁恒微电子. CH32FV2x_V3x 参考手册[EB/OL]. (2025-12-01)[2026-07-05]. Nanjing Qinheng Microelectronics Co., Ltd.
[7] 中移物联网. OneNET 平台 MQTT 接入文档[EB/OL]. (2024-09-10)[2026-07-05]. OneNET物联网平台_开发者文档_OneNET
[8] 微信. 小程序开发指南[EB/OL]. (2025-08-15)[2026-07-05]. 微信开放文档
驱动层附件:
更多推荐



所有评论(0)