项目总体展示

基于沁恒 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 上报,字段名 TemperatureHumidity

  • 驱动流程: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}}}

  • 处理流程

    1. mqtt_cmd_poll() 逐行扫描 UART6 接收缓冲区

    2. 匹配 +MQTTSUBRECV 前缀 → 提取 JSON payload → mqtt_handle()

    3. mqtt_handle() 调用 json_bool() 解析各指令

    4. 执行对应控制函数(带远程接管锁存,如 Fan_Set() 会置 fan_manual=1

    5. 回复 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)

  • 连接流程

    1. AT+RST 复位 ESP8266

    2. AT+CWMODE=1 Station 模式

    3. AT+CWJAP="SSID","PWD" 连接 WiFi

    4. AT+MQTTUSERCFG 配置 MQTT 客户端(ID、用户名、密码——OneNET token)

    5. AT+MQTTCONN 连接 Broker

    6. 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 云平台上看到的数据一致——都像是"冻住了"。

查找:

  1. 先排除云平台缓存——LCD 本地显示和 OneNET 上报数据一样,说明问题在传感器侧

  2. DHT11_Read_Data() 前后加 printf 打印返回值和 temp/humi,观察每次读取结果

  3. 发现日志里连续多次读取返回值都是 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,室内稳定环境下读数天然变化小

解决:

  1. 校验和失败时返回 2(新增错误码),不再和成功混用返回值 0

  2. Fan_Task 调度周期从 800ms 改为 1500ms,满足 DHT11 最小间隔

  3. 所有调用点增加对返回值 2 的处理

教训: 错误码设计要清晰——"通信成功但数据无效"和"完全成功"是两种不同的状态,不能用同一个返回值混淆。传感器驱动必须考虑数据手册里的时序约束。


Q2:MQ-135 空气质量与光敏传感器读数完全一致

发现: OneNET 云平台上 Alcohol(MQ-135)和 Light(光敏电阻)两个数据点数值完全相同,本地 printf 打印也是一样的。

查找: 两个传感器用不同 ADC 通道——光敏电阻在 PA0(通道 0),MQ-135 在 PA1(通道 1)。硬件接线没问题的话不可能读数一样,重点查 ADC 通道配置。

  1. 检查 Light_Init()MQ135_Init() 的初始化代码,发现两个函数都调用了 ADC_Init()RCC_ADCCLKConfig(),后者覆盖前者

  2. Light_Init 设置连续转换模式,MQ135_Init 设置单次模式

  3. MQ135_Init 最后调用,ADC 最终运行在单次模式

确认: 连续模式下,ADC_SoftwareStartConvCmd() 是空操作——ADC 已经在持续转换,不需要重新启动。MQ-135 读完切回通道 0 后,ADC 可能还在转换通道 1,通道切换要等下一轮才生效。Get_ADC_Average() 依赖别人把通道恢复到 0,但在连续模式下这个恢复不可靠。

解决:

  1. Get_ADC_Average() 函数头部显式切换到通道 0

  2. MQ135_Get_Analog() 每次读取前显式切换到通道 1

  3. 两个函数各管自己的通道,互不依赖

  4. 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)一次都没成功过。两个指令的代码路径完全对称,不应该出现这种差异。

查找:

  1. 用按键逐一测试各执行器,确认硬件本身没问题,问题在语音接收侧

  2. 用 USB-TTL 连接 SU-03T 的 UART 输出,在电脑上直接看模块发的数据

  3. 串口助手文本模式下看到模块发的是字符 "1""2"——看起来正常

  4. 切换到 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 波特率之前配置有误,偶尔误码碰巧匹配上了。

解决:

  1. 把 SU-03T 语音模块的串口输出格式改成「十六进制/二进制模式」

  2. 改完后用 USB-TTL HEX 模式逐一验证每个指令词发的字节值

  3. 确认 UART4 波特率与模块配置一致

教训: 串口调试必须用 HEX 模式看原始字节。文本模式显示的 "1" 可能是 0x01、0x31 甚至 0x81,肉眼看起来一样,实际差很多。HEX 模式是串口调试的第一选择。


Q5:按键控制不可靠

发现: 矩阵键盘按下后时灵时不灵,在温湿度读取周期附近按键基本触发不了。边沿检测逻辑(key_down = key_val & (key_val ^ key_old))已经有了,但还是不灵。

查找:

  1. Key_Proc 入口加 printf 打印每次扫描的 key_val——按键按下时有时读到键值,有时读到 0xFF(无按键)

  2. 时间戳打印发现 Fan_Task 执行时 Key_Proc 被推迟了 20~30ms

  3. 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;  // 立即读列

解决:

  1. Fan_Task 周期从 800ms 改为 1500ms(Q1 已同步处理)

  2. Key_Read() 每次切换行线后加 Delay_Us(5),等电平稳定再读列

教训: 协作式调度器里任务执行时间是关键约束。DHT11 的 20ms 阻塞延时和机械按键弹跳叠加,把按键的有效窗口压得很短。根本解法是把 DHT11 改为异步状态机,治标的方法是延长扫描间隔。


Q6:红外门状态检测逻辑反且延时过长

发现: 衣柜门关上时,LCD 显示「有人」;门打开时显示「无人」——和实际完全相反。

查找:

  1. 手动测试 YX55692:用手挡住传感器 → 输出低电平(0),移开 → 高电平(1)

  2. infrared.h 文档:"0: 触发(有遮挡), 1: 未触发(无遮挡)"——传感器电平定义是清楚的

  3. 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_InitMQ135_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
类型不一致 混用 u8uint8_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]. 微信开放文档

驱动层附件:

GitHub - yanweih67-hash/The-Smart-Wardrobe-Based-On-CH32: 基于沁恒 CH32V307 的智能衣物管理衣柜系统与多维度安全监测实现/Implementation of Intelligent Clothing Management Wardrobe System and Multi-dimensional Safety Monitoring Based on WCH CH32V307 · GitHub

更多推荐