ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于STM32的图书馆环境监测系统:代码、原理图与仿真全开源

基于STM32的图书馆环境监测系统:代码、原理图与仿真全开源 图书馆这种地方人流密度变化大、纸质书籍和木质书架多、空调和新风系统又不一定全天开温湿度一旦失控书页发霉、胶装开裂、读者投诉闷热都是很现实的问题。这套基于 STM32 的开源图书馆环境监测系统说白了就是拿一块几十块钱的核心板配几路传感器把温度、湿度、光照、空气质量这些指标实时采上来超阈值就本地报警、屏幕显示、必要时往上一级上报。整套东西我把它拆成了三块代码、原理图、仿真三个都给了目的就是让不管是刚学完 GPIO 点灯的新手还是想找个完整项目练手的同学都能直接复现。我自己把这个项目从画板子到跑仿真、再到实物联调完整走了一遍踩的坑不算少下面把设计思路、电路细节、代码实现、仿真验证和排查经验一次性讲清楚。1. 项目整体设计与选型思路拆解1.1 图书馆场景到底该监测哪些量很多人一上手就想着把所有传感器都堆上去温湿度、光照、PM2.5、CO2、甲醛、噪声、人体红外……结果板子做得又大又乱代码里十几个驱动互相抢时序最后调试到崩溃。我的建议是先按图书馆的真实痛点排优先级。排在第一梯队的是温度和湿度。纸质文献最适宜的保存环境大致在 18~22℃、相对湿度 45%~60% 这个区间湿度长期高于 65% 就容易长霉低于 40% 纸张会变脆。这两个量直接决定书库能不能长期存放而且 DHT11 这类传感器便宜到离谱必须放。第二梯队是光照和空气质量。阅览区的照度按阅览需求一般要求 300lx 以上靠窗位置夏天可能飙到 2000lx 以上太强刺眼、太弱伤眼空气质量则关系到读者久坐的舒适度CO2 浓度超过 1000ppm 人就会开始犯困、注意力下降。这两项用 BH1750 光照模块加一个模拟式气体传感器就能解决。第三梯队才是噪声、人流这些锦上添花的东西。我这次开源版本先把前两梯队做扎实第三梯队留了接口后面想加随时加。这个取舍很关键项目能不能跑通八成取决于你有没有克制住我全都要的冲动。1.2 主控为什么选 STM32F103C8T6主控这块我几乎没有犹豫直接上STM32F103C8T6核心板。理由有三条都很实在。第一是资源刚好够用。这颗芯片是 Cortex-M3 内核主频 72MHz64KB Flash、20KB SRAM。本项目代码量大概在 20~30KB 左右RAM 峰值占用不到 8KB留了足够的余量给后续加功能。你要是换成 STM32F030F4 那种 16KB Flash 的加个 OLED 字库就顶到天花板了。第二是外设匹配度高。它自带 2 个 12 位 ADC、2 个 I2C、3 个 USART、多个定时器DHT11 用普通 GPIO 加微秒延时就能读BH1750 走 I2C1OLED 走 I2C1 的另一路地址或者软件 I2C气体传感器走 ADC1 的通道串口留给 ESP8266 上报和调试打印各司其职互不打架。第三是生态和价格。这块板子满大街都是十几块钱能买到资料、例程、社区问答铺天盖地出了问题搜一下基本都有答案。做项目最怕的就是选了个冷门芯片卡在一个寄存器配置上三天没人能问。注意市面上 F103C8T6 核心板有的标称 64KB Flash实际是 128KB 的扩容片这在正常使用中不影响但如果你要烧比较大的字库最好先用 ST-Link Utility 读一下实际容量免得烧录时莫名报错。1.3 系统分层架构与模块划分我把整个系统拆成四层从下往上依次是感知层、处理层、交互层、通信层。这样分层不是为了好看是为了让代码能改、能换、能复用。感知层就是各类传感器驱动每个传感器一个独立的 .c/.h 文件对外只暴露初始化和读取两个函数内部时序再乱也关在文件里。处理层负责数据滤波、单位换算、阈值判定和状态机管理这一层不碰任何硬件寄存器纯逻辑方便单独测试。交互层管 OLED 显示和蜂鸣器/LED 报警属于输出。通信层管串口协议打包和 ESP8266 上报如果哪天要换 4G 模块只动这一层就行。这么分层之后你会发现调试效率完全不一样。比如屏幕不亮我只要怀疑交互层数据全是 0我只看感知层。而不是一锅粥地在 main 函数里从上翻到下。后面讲代码结构的时候我会细说每个文件干什么。2. 硬件原理图设计与关键电路细节2.1 主控最小系统与电源树设计最小系统这部分是基础中的基础但恰恰是最容易画错的地方。STM32F103C8T6 的最小系统需要电源、复位、晶振、BOOT 配置、调试接口五块。电源方面外部输入我设计成 5VUSB 或 DC 座都行然后通过AMS1117-3.3稳压到 3.3V 给主控和传感器供电。这里有个细节必须说清楚AMS1117 的压差大约在 1.1V 左右也就是说输入 5V 的时候输出 3.3V 完全没问题但如果你想用 3.7V 锂电池直接供电输出电压会掉到 2.6V 左右STM32 可能工作不稳定。所以要电池供电的话得换低压差的 LDO比如 RT9013 之类。输入端和输出端各放一个 10μF 的钽电容加一个 100nF 的陶瓷电容前者滤低频纹波后者滤高频噪声两个位置不要省。每个 VDD 引脚旁边再单独挂一个 100nF这是 ST 官方手册反复强调的去耦我见过太多人省掉这个导致 ADC 采样值乱跳。复位电路用经典的 10kΩ 上拉加 100nF 电容到地再加一个复位按键。注意 NRST 引脚内部已经有 40kΩ 左右的上拉外部这个 10kΩ 是为了增强抗干扰不是可有可无。BOOT0 用一个 10kΩ 下拉到地平时从 Flash 启动想进 ISP 下载模式就把它短接到 3.3V。BOOT1也就是 PB2直接下拉不用管。晶振我用了 8MHz 的无源晶振配两个 20pF 负载电容接在 PD0/PD1OSC_IN/OSC_OUT上。负载电容的具体值跟晶振本身的 CL 参数有关公式是 CL (C1×C2)/(C1C2) Cstray其中 Cstray 一般估 3~5pF。晶振规格书上 CL 标 10pF 的话两个电容选 20pF 左右比较合适。选错了会起振困难表现为程序卡在 HAL_RCC_OscConfig 里出不来。调试接口我保留了 SWD 四线VCC、GND、SWDIO、SWCLK只占 PA13/PA14 两个引脚比 JTAG 省了三个脚。这两根线走线尽量短、尽量等长旁边不要走大电流的线。2.2 传感器接口电路逐个拆解先说DHT11。它只有一个数据脚接在 PA0 上这是一个单总线器件所以数据线必须接一个 4.7kΩ~10kΩ 的上拉电阻到 3.3V。为什么必须上拉因为 DHT11 内部是开漏输出只能主动拉低拉高要靠外部上拉电阻。没有这个电阻读出来的永远是 0。供电 3.3V 就够但注意 DHT11 对上电时序有要求上电后要等至少 1 秒再发第一次读取命令且两次读取间隔不能小于 1 秒有的资料说 2 秒更稳否则读出来的数据会不准甚至全是 0xFF。再看BH1750 光照模块。它走 I2CSCL 接 PB6、SDA 接 PB7两根线各自要接一个 4.7kΩ 上拉到 3.3V。这个上拉电阻值不是随便选的太小功耗大、灌电流大太大上升沿变缓、高速通信会出错。标准模式 100kHz 用 4.7k 是通用做法快速模式 400kHz 可以降到 2.2k。BH1750 的 ADDR 引脚决定地址接地是 0x23接高是 0x5C我的板子上直接接地。它的 VCC 范围是 2.4~3.6V接 3.3V 刚好。气体传感器我用的是MQ-135输出模拟电压接 PA1ADC1_IN1。这类传感器内部是一个加热丝加一个气敏电阻构成分压电路所以需要一个负载电阻 RL 把电流变化转成电压变化。模块上一般已经带了可调电位器用来调 RL 阻值。这里必须强调MQ 系列传感器一定要预热刚上电时读数完全不可信通常要预热 24 小时以上才能达到稳定状态短期使用至少要预热 3~5 分钟并且用软件做基线校准。ADC 输入前我加了一级 RC 低通10kΩ 串一个 100nF 到地截止频率约 160Hz能把高频干扰压下去。注意这个 10kΩ 会增大源阻抗所以 ADC 的采样时间要设置得长一些我用的是 71.5 个 ADC 周期约 1μs 级别确保采样电容能充满。OLED 我选的是SSD1306 0.96 寸 I2C 屏地址 0x3C和 BH1750 挂在同一路 I2C 上。这里有个小坑两个器件地址不同0x23 和 0x3C理论上可以共用一条总线但 OLED 刷新比较耗时如果和 BH1750 读数在同一时刻发起会出现总线忙等待。我的处理是把 OLED 刷新放在主循环的低优先级任务里BH1750 读取放在定时器触发的采样任务里错开执行。2.3 报警与执行电路报警部分我做了声光双路。声音用有源蜂鸣器通过一个 S8050 NPN 三极管驱动基极串 1kΩ 限流电阻发射极接地集电极接蜂鸣器负极蜂鸣器正极接 5V有源蜂鸣器电流一般在 30mA 左右超过 GPIO 的 20mA 驱动能力所以必须用三极管。基极和发射极之间再并一个 10kΩ 下拉防止悬空误触发。光报警用一颗红色 LED 加 1kΩ 限流电阻接在 PB0 上。限流电阻的算法是(3.3V - 2.0V) / 5mA 260Ω取 1kΩ 的话电流只有 1.3mA 左右亮度偏暗但够看。如果你想要亮一点可以降到 470Ω电流约 2.8mA仍然在 GPIO 安全范围内。提示驱动感性负载比如继电器、大功率蜂鸣器时一定要在线圈两端反向并一个续流二极管比如 1N4148否则断电瞬间的反向电动势会打穿三极管。如果需要联动新风系统或者加湿器可以在执行电路上加一个继电器模块由 PB1 控制。继电器我用的是 5V 小型继电器同样用三极管驱动注意继电器线圈和主控共地。2.4 PCB 布局与走线要点虽然我在开源包里给的主要是原理图但实际做板子的时候有几个点必须提前考虑。ADC 走线要远离晶振和数字信号线尽量走短、走直最好两侧包地。I2C 的两根线尽量等长并排走长度差控制在 5mm 以内。晶振下面的地要完整不要被其他信号割裂。去耦电容必须靠近对应引脚距离控制在 5mm 以内这一点很多人画板子时随手一放结果离引脚十万八千里等于没放。DHT11 的信号线如果是长距离比如引到书架深处建议改成屏蔽线或者在靠近 MCU 一侧加 100Ω 串联电阻加 100pF 到地做简单滤波否则单总线的时序很容易被干扰吞掉。3. 固件代码实现与核心逻辑3.1 工程目录结构与模块职责工程基于Keil MDK STM32CubeMX生成目录结构是这样的LibraryEnvMonitor/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── app_config.h │ │ ├── dht11.h │ │ ├── bh1750.h │ │ ├── gas_sensor.h │ │ ├── oled_ssd1306.h │ │ ── filter.h │ ── Src/ │ ├── main.c │ ├── dht11.c │ ├── bh1750.c │ ├── gas_sensor.c │ ├── oled_ssd1306.c │ ├── filter.c │ ├── app_task.c │ └── stm32f1xx_it.c ├── Drivers/ └── MDK-ARM/app_task.c是整个逻辑的调度中心里面维护一个状态机和一个基于 SysTick 的软件定时器数组。每个传感器的采样周期不同DHT11 是 2 秒一次BH1750 是 1 秒一次气体传感器是 500ms 一次OLED 刷新是 200ms 一次报警判定是 500ms 一次。这些周期不是拍脑袋定的而是跟传感器本身的响应速度匹配DHT11 本身就规定最短 1 秒你 100ms 读一次只会读到脏数据而 OLED 刷新太快没意义人眼也看不出来还白白占用 I2C 带宽。app_config.h里放所有阈值和周期常量比如#define TEMP_HIGH_TH 30.0f // 温度上限摄氏度 #define TEMP_HIGH_CLR 28.5f // 回差下限 #define HUMI_HIGH_TH 65.0f // 湿度上限百分比 #define HUMI_LOW_TH 40.0f // 湿度下限 #define LUX_LOW_TH 300.0f // 照度下限 #define GAS_HIGH_TH 2200 // 气体 ADC 原始值上限 #define DHT11_PERIOD_MS 2000 #define BH1750_PERIOD_MS 1000 #define OLED_PERIOD_MS 200把所有可调参数集中在一个头文件里好处是后面不管是换房间、换季节还是换传感器改这一个文件就够了不用满工程搜索if (temp 30)。3.2 DHT11 单总线驱动的实现要点DHT11 是整个项目里时序最苛刻的器件它的通信全靠微秒级的高低电平长短来区分 0 和 1。这就带来一个问题HAL 库里最细的延时是毫秒级HAL_Delay根本不够用。我的做法是用DWT 内核周期计数器做微秒延时。static void dht_delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks) { } }用之前要先使能 DWTCoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;为什么不用空循环 for(i0;in;i)因为编译器优化等级一变循环次数就变了你今天在 -O0 下调好的延时明天改成 -O2 就全乱了。DWT 是硬件计数器只跟主频有关跟编译器无关稳定得多。读一位数据的核心逻辑static uint8_t dht_read_byte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { while (DHT_PIN_READ() 0); // 等 50us 低电平结束 dht_delay_us(40); // 40us 后采样避开上升沿抖动 byte 1; if (DHT_PIN_READ()) byte | 1; while (DHT_PIN_READ() 1); // 等高电平结束 } return byte; }这里的 40μs 是关键。DHT11 用高电平持续 26~28μs 表示 070μs 表示 1。我在 50μs 低电平结束之后延时 40μs 再采样此时如果还是高电平那就是 1已经拉低了那就是 0。这个采样点的选择是有讲究的太早会落在上升沿的不稳定区太晚可能超过 0 的 28μs 窗口。40μs 正好卡在中间容错空间最大。完整的读取流程是这样的主机先拉低总线至少 18ms然后拉高 20~40μs 再释放DHT11 检测到之后拉低 80μs 作为响应再拉高 80μs接着连续输出 40 位数据湿度整数 8 位、湿度小数 8 位、温度整数 8 位、温度小数 8 位、校验和 8 位。校验方式是前四个字节相加取低 8 位跟第五个字节比较。注意整个读取过程必须关中断用__disable_irq()和__enable_irq()包起来。因为 SysTick 中断默认 1ms 一次如果它恰好在读位的过程中插进来就会打乱时序读出来全是 0。我第一版就是忘了关中断现象是偶尔能读到、大部分时候失败找了半天才发现是这个原因。读取失败的时候不要慌直接返回错误码然后在任务层做重试。我的策略是连续失败 3 次才认为传感器故障报错并把该路数据标记为无效避免单次干扰导致误报警。3.3 BH1750 与气体传感器驱动BH1750 走标准 I2C初始化时发送上电指令 0x01然后发送测量模式指令 0x10连续高分辨率模式1lx 精度。这里有个细节连续模式下传感器会自动周期性测量你不用每次都发指令单次模式 0x20 则测完就进省电模式每次都要重新触发。我用的是连续模式省事。#define BH1750_ADDR (0x23 1) void bh1750_init(I2C_HandleTypeDef *hi2c) { uint8_t cmd 0x01; // power on HAL_I2C_Master_Transmit(hi2c, BH1750_ADDR, cmd, 1, 100); cmd 0x10; // continuous H-res mode HAL_I2C_Master_Transmit(hi2c, BH1750_ADDR, cmd, 1, 100); } float bh1750_read(I2C_HandleTypeDef *hi2c) { uint8_t buf[2]; if (HAL_I2C_Master_Receive(hi2c, BH1750_ADDR, buf, 2, 100) ! HAL_OK) return -1.0f; return ((buf[0] 8) | buf[1]) / 1.2f; }那个除以 1.2 不是随便写的是 BH1750 手册里给的换算系数原始寄存器值除以 1.2 得到 lux。比如读出来 120实际就是 100lx。高分辨率模式下测量时间典型值 120ms所以两次读取之间至少要隔 120ms否则读到的是上一次的结果。气体传感器这块要老实说MQ-135 的输出跟具体气体浓度之间没有线性关系而且受温湿度影响很大。开源项目里我用的是相对变化趋势 固定阈值的简化方案上电预热 3 分钟后在洁净空气里采集 60 个样本取平均作为基线 R0之后每次读到的 Rs 跟 R0 比较比值超过设定阈值就判为异常。float gas_read_voltage(void) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); uint16_t raw HAL_ADC_GetValue(hadc1); return raw * 3.3f / 4095.0f; } /* 分压公式反推传感器电阻 */ float gas_calc_rs(float vout) { const float RL 10.0f; // 负载电阻单位 kΩ const float VC 5.0f; // 传感器回路供电 if (vout 0.01f) return 9999.0f; // 防止除零 return (VC - vout) / vout * RL; }提示如果你需要比较准确的气体浓度数值MQ-135 这种方案是不够的得换 CCS811 或 SGP30 这类带标定的数字传感器。但它们价格贵好几倍还要走 I2C 和预热。图书馆项目里判断空气闷不闷趋势法其实够用了。3.4 数据滤波与阈值判定原始数据直接用是会出问题的。DHT11 偶尔会跳一个明显错误的数ADC 采样也会因为电源波动抖几下。所以我在处理层加了两级滤波中值滤波去脉冲再加上滑动平均平滑。中值滤波的思路是取 N 个样本排序后取中间值能把孤立的野点直接干掉。滑动平均则是把最近 N 次的平均值输出让曲线更平滑。uint16_t median_filter(uint16_t *buf, uint8_t n) { uint16_t tmp[16]; memcpy(tmp, buf, n * sizeof(uint16_t)); for (uint8_t i 0; i n - 1; i) { for (uint8_t j 0; j n - 1 - i; j) { if (tmp[j] tmp[j 1]) { uint16_t t tmp[j]; tmp[j] tmp[j 1]; tmp[j 1] t; } } } return tmp[n / 2]; }N 取多少合适我试过 3、5、10 三档。N3 响应快但去噪效果一般N10 平滑但滞后明显温度变化要好几秒才能反映出来。最后选了N5兼顾两者。窗口再大就要考虑数组开销了嵌入式里 SRAM 是要省的。阈值判定这块有个非常实用的技巧叫滞回回差控制。假设温度阈值设 30℃如果不做处理温度在 29.9 和 30.1 之间来回跳蜂鸣器就会滴滴滴响个不停特别烦人。做法是设两个阈值升到 30℃ 触发报警但要等降到 28.5℃ 才解除。这样中间有 1.5℃ 的缓冲带抖动就不会引起反复动作。static uint8_t temp_alarm 0; void check_temp_alarm(float temp) { if (!temp_alarm temp TEMP_HIGH_TH) { temp_alarm 1; } else if (temp_alarm temp TEMP_HIGH_CLR) { temp_alarm 0; } }湿度我做了双向判定既有上限也有下限高于 65% 报警过湿低于 40% 报警过干两种情况的处理建议不一样一个要除湿一个要加湿所以报警标志位分开设置。3.5 显示与上报逻辑OLED 显示我做了三屏轮播每 4 秒切换一屏第一屏显示温湿度第二屏显示光照和气体第三屏显示报警状态和系统运行时间。为什么要轮播而不是全塞一屏因为 0.96 寸屏幕只有 128×64 像素中文字库一个 16×16 字模就占 32 字节全塞进去字太小看不清。显示驱动我用的是自己写的轻量级 SSD1306 库没有用现成的 U8g2因为 U8g2 编译进来要占十几 KB Flash对 F103C8T6 有点重。自己写的版本只支持 6×8 和 8×16 两种字模Flash 占用不到 4KB。上报部分通过 USART1 接 ESP8266用的是简单的自定义文本协议一行一条数据$DATA,T24.5,H55.2,L420,G1850,A0\r\n其中 T 是温度、H 是湿度、L 是照度、G 是气体原始值、A 是报警标志。为什么用文本协议不用二进制因为调试的时候可以直接拿串口助手看不用写解析工具出问题一眼就能看出来。带宽方面一条数据大概 40 字节2 秒发一次9600 波特率完全够用。如果用 ESP8266 上报到服务器建议加一个本地缓存 断线重连的机制网络断了先把数据存到环形缓冲区里恢复之后补发。环形缓冲区用一个 512 字节的数组加两个指针就能实现不需要动态内存。4. 仿真验证从 Proteus 到实物联调4.1 仿真环境搭建与元件准备先说清楚仿真的定位它能验证逻辑不能验证全部硬件。我见过有人以为仿真跑通了实物就一定没问题结果板子一打回来各种翻车。所以正确的心态是拿仿真做快速迭代把逻辑和大部分连接关系先跑对再用实物验证电源、时序、抗干扰这些仿真根本模拟不了的东西。Proteus 里搭这个仿真需要的元件清单是STM32F103C8注意要装 STM32 的 VSM 仿真模型不然放上去也没法仿真、DHT11、LCD 或 OLEDProteus 里 OLED 模型少我一般先用 LM016L 字符屏代替验证逻辑、电位器模拟气体传感器输出、LED 和 BUZZER。晶振和复位电路在仿真里可以简化因为 Proteus 的 MCU 模型内部有时钟设置直接在属性里填 8MHz 就行。流程是CubeMX 配好时钟和引脚生成 Keil 工程编译出 .hex 文件然后在 Proteus 里双击 MCU把 Program File 指向这个 hexClock Frequency 填 8000000。这里有个坑CubeMX 里如果开了 HSE 外部晶振Proteus 里就要把时钟频率对应填对否则仿真跑出来的时间尺度是错的你会看到 DHT11 怎么都读不出来其实是因为主频不对导致微秒延时全乱了。4.2 仿真阶段能验证和不能验证的东西能验证的引脚分配对不对、传感器读取逻辑对不对、数据换算公式对不对、报警阈值判定对不对、显示内容和格式对不对、串口协议打包对不对。这些占了整个项目逻辑的绝大部分用仿真跑一遍能省下大量反复烧录的时间。不能验证的真实电源的纹波、长线传输的信号完整性、传感器的实际精度和漂移、蜂鸣器的实际音量、I2C 总线在多设备下的实际负载。特别是 DHT11Proteus 的模型时序跟真实器件有偏差仿真里能读出来不代表实物就能读出来我实测下来仿真里的容错窗口比实物宽松不少。4.3 仿真跑不起来和发散的常见原因很多人反馈仿真一运行就卡死或者数值乱跳我总结了几类原因。第一类是模型缺失。Proteus 里如果某个元件没有仿真模型整个仿真会直接报错或者卡在初始化。判断方法是右键元件看有没有 Simulator Model 选项。DHT11 有些版本的库只有原理图符号没有仿真模型这种情况只能换成用信号发生器模拟数据或者干脆跳过这一路。第二类是时钟配置不匹配。前面说的主频填错是最常见的。还有一种是 CubeMX 里用了 HSE 但 Proteus 里晶振电路没接对导致 MCU 起不来。最简单的排查方法是在 main 里点一个 LED看仿真里闪不闪不闪就是时钟或者启动配置的问题。第三类是除零和越界。仿真环境对这类错误的反应有时候比实物还激烈会直接跑飞或者显示奇怪的值。比如前面气体传感器计算 Rs 的时候如果 vout 是 0就会得到无穷大然后显示异常。一定要在代码里加保护。第四类是仿真时间步长设置不合理。Proteus 的仿真时间步长如果设得太粗微秒级的单总线时序就会被完全忽略表现为 DHT11 永远读不到数据。可以在 System - Set Animation Options 里把时间步长调细或者改用实时仿真模式。提示仿真跑不通的时候先别急着怀疑自己的代码八成是配置问题。我的排查顺序是主频对不对、引脚对不对、有没有模型、有没有除零。按这个顺序走一遍基本都能定位。5. 常见问题与排查技巧实录5.1 硬件与电路层问题速查现象可能原因排查方法处理方式上电后 MCU 不工作电源电压不对、复位脚被拉低万用表量 VDD 和 NRST检查 LDO 输出确认复位电路程序下载不进去SWD 线序错、BOOT0 配置错查 SWDIO/SWCLK/GND 三根线BOOT0 拉低重新上电再连ADC 读数乱跳缺去耦电容、地线干扰、源阻抗大示波器看 VDD 纹波补去耦加 RC 滤波延长采样时间DHT11 一直读失败缺上拉电阻、间隔太短、时序被中断打断示波器看数据线波形加 4.7k 上拉间隔 2s读时关中断I2C 通信时通时断上拉阻值不合适、总线电容过大看 SCL/SDA 上升沿减小上拉到 2.2k缩短走线蜂鸣器不响或声音小GPIO 驱动能力不足量基极电压加三极管驱动改用有源蜂鸣器这张表基本覆盖了我调试过程中遇到的大部分问题。逐条说几个重点。去耦电容缺失导致的 ADC 抖动特别隐蔽因为在实验室用 USB 供电的时候可能完全正常一旦换成质量差一点的电源适配器就开始飘。判断方法是拿示波器看 VDD 上的纹波峰峰值超过 50mV 就要想办法了。DHT11 那块我要再强调一遍关中断。我第一版代码没关用逻辑分析仪抓波形明明时序都对数据就是错的查了很久才反应过来是 SysTick 打断。加上__disable_irq()之后一次就通了。I2C 上拉这个问题也很典型。一开始我用的是 10k结果 OLED 刷屏有明显的残影和拖尾示波器一看 SCL 上升沿是缓慢的斜坡。换成 4.7k 之后波形方方正正问题解决。5.2 软件与逻辑层问题速查现象可能原因处理方式程序卡在某处不动死循环等待标志位、时钟未就绪检查 while 循环有没有超时退出数值单位换算错误公式中的系数或量纲写错用已知值反推校验比如光照除以 1.2报警反复触发没有做滞回设置回差阈值屏幕花屏显存越界、刷新频率过高检查数组下标降低刷新率串口收到乱码波特率不一致、时钟源不对两边都设 115200确认主频定时不准SysTick 重装载值算错检查是否按主频正确计算死循环等待标志位这个问题本质上是硬件没响应但代码没设超时。HAL 库现在大多带超时参数但自己写的 while 循环很容易忘。我的习惯是任何等待都加一个计数器超过 10000 次就跳出并返回错误码宁可报错也不要卡死。单位换算这块BH1750 除以 1.2 我一开始写成了乘 1.2结果读数直接大了一个数量级还以为传感器坏了。这种低级错误在调试紧张的时候特别容易犯建议每一步换算都写个注释说明来源。5.3 我个人踩过的几个坑第一个坑是上电顺序。我的板子上传感器和 MCU 共用 3.3V但没有做缓启动。某个批次的上电瞬间电流冲击比较大导致 MCU 复位的时候 DHT11 也一起复位然后 DHT11 要等 1 秒才稳定代码里却在上电 200ms 就开始读结果第一次读数永远失败。后来我在初始化里加了一个 1500ms 的等待问题消失。第二个坑是字体和显示方向。SSD1306 有 128×64 和 128×32 两种卷屏方向也有四种。我照着别人的例程改忘了改分辨率结果下半屏全是噪点。这种错误看现象很难想到但只要核对一下初始化指令里的复用率和显示模式就能定位。第三个坑是仿真和实物的差异。仿真里 DHT11 我延时 30μs 采样就能读对实物上必须延时 40μs 才稳。这说明不管仿真跑得多顺实物上一定要重新测一遍时序余量。我现在养成的习惯是但凡涉及微秒级时序的地方都在实物上用逻辑分析仪抓一遍看到波形对了才放心。第四个坑是阈值设太死。我一开始把湿度上限卡在 60%结果梅雨季节几乎每天都在报警读者投诉铃声吵。后来放宽到 65%并且加了持续超过 5 分钟才报警的时间窗口判定误报率大幅下降。这个经验告诉我环境监测的阈值一定要留余量还要考虑持续时间不能一超就报。6. 后续可扩展的方向与二次开发建议如果你把这个项目跑通了想继续往上加东西我有几个方向可以推荐都是投入产出比比较高的。第一是加数据存储。现在数据只显示和上报断电就没了。加一片 AT24C02 或者 W25Q64把每小时的温湿度平均值存下来可以做出日曲线和周曲线。AT24C02 走 I2C容量 2Kbit够存几百条记录W25Q64 走 SPI8MB能存长期的原始数据。存储这块要注意磨损均衡EEPROM 的擦写寿命是 100 万次如果每 2 秒写一次几个月就到寿命了所以一定要按小时或者更长的周期落盘。第二是加本地控制联动。既然能测量就能控制。加一路继电器控制加湿器或除湿机加一路控制新风或者净化器做成一个闭环。这时候代码上要引入简单的 PID 或者开关控制逻辑并且注意执行器和传感器不能装在同一个位置否则会形成正反馈振荡加湿器一开传感器附近的湿度马上上去然后停机实际房间还是干的。第三是换通信方式做远程监控。现在的串口 ESP8266 方案适合小范围用。如果要做多个阅览室统一管理可以考虑换成带以太网的方案或者用 LoRa 做低功耗远距离传输。图书馆场景其实挺适合 LoRa 的一层楼放一个节点汇聚到一个网关。第四是加人体存在检测。用红外或者毫米波雷达检测某个区域有没有人有人的时候提高采样频率和报警灵敏度没人的时候进入低功耗模式只记录。这个对图书馆这种大部分时间没人、但需要长期监测的场所很有价值。最后分享一个小经验这套系统我建议在正式部署前先空跑一周把每天的数据都记录下来看看峰谷值到底在哪然后再定阈值。我当初拍脑袋定的阈值实际用起来有一半不合适。数据驱动地调参数比任何理论值都靠谱。代码和原理图我都整理在开源包里了CubeMX 工程、Keil 工程、Proteus 仿真文件都有接口定义和参数说明写在 README 里照着复现遇到问题回头看第五章那张速查表基本都能自己解决。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进