从零搭一套 STM32 数据采集系统:一个下午踩了五个坑
读 GPIO、组帧、串口发送、ESP32 桥接……听起来简单,真正上手才发现,"能跑"和"跑得对"之间,隔着五个想骂人的坑。
写在前面
事情的起因很简单:手上有一块 STM32F103C8T6(Blue Pill 最小系统板),想让它读一堆 GPIO 状态,编码成数据帧,通过串口发出来。
听起来是个"hello world"级别的任务,对吧?读引脚、拼字符串、发串口,三件事。
结果我花了一个下午,从下午五点干到晚上七点,中间踩了五个坑。每一个坑都不大,但每一个都能让你盯着乱码和飘忽的数据怀疑人生。
这篇文章把这五个坑记下来,给后来的自己,也给同样在折腾的朋友。
起因:为什么要做这个
需求很简单:
- STM32 读 24 个 GPIO 输入状态
- 累加器 count 从 0 开始无限加
- 组装成
count=xxx,io=111100001111...这样的数据帧 - 通过串口发出去
而且,我手上没有 CH340(USB 转串口模块),快递要等很久。手头正好有一块 ESP32 开发板,于是决定:让 ESP32 临时当 CH340 用,把 STM32 的串口数据桥接到 USB 上。
架构长这样:
STM32 ──TTL串口──► ESP32(当桥)──► USB ──► 电脑
用一颗 240MHz 双核、带 Wi-Fi 蓝牙的芯片,去干一颗几毛钱 CH340 的活——杀鸡用牛刀,但架不住它就在手边。
第一个坑:串口不共地,乱码到怀疑人生
一切从搭环境开始。ESP-IDF 装完,STM32 的 arm-none-eabi 工具链装完,两边都能单独跑。
然后接线,STM32 的 TX 接 ESP32 的 RX,RX 接 TX……等等,GND 呢?
我一开始没接 GND。因为两个板子的 USB 都插在同一台电脑上,心想"USB 不是都共地了吗"。
结果——能通,但时不时乱码:
count=515,io=000001000000000000000000
count=516,io=0000010��=�e�5�UR�
count=517,io=000001000000000000000000
时好时坏,乱码随机出现。我一度以为是波特率问题,调了半天。
最后做个实验才破案:
- 把 STM32 拔了 ST-LINK,插到充电宝(脱离电脑 USB)
- GND 跳线还在 → 正常
- 拔掉 GND 跳线 → 乱码
- 插回 GND → 恢复
真相大白:之前"没连 GND 也能通",是因为两个 USB 插在同一台电脑上,通过电脑主板间接共地了。一旦脱离这个前提,立刻乱码。
教训一:串口通信必须共地,别指望"USB 都插一台电脑"这种巧合。 电流要形成回路,信号才有参考电位。
第二个坑:上拉输入不设 ODR,悬空读到 0
解决了共地,开始写正式的采集程序。24 个 GPIO 配成上拉输入(悬空=1,接 GND=0)。
写完烧录,看数据:
io=000001000000000000000000
等等,悬空不应该是全 1 吗?怎么全 0?
查了半天,发现是 STM32 的一个经典细节:
CNF=10, MODE=00是"上拉/下拉输入"模式- 但到底是上拉还是下拉,由 ODR 寄存器决定
- 我只配了 CNF/MODE,没设 ODR,默认 ODR=0 → 实际是下拉,所以悬空读到 0
修复:把输入脚的 ODR 置 1,上拉生效,悬空终于读到 1。
教训二:STM32 的"上拉/下拉输入"是二选一,由 ODR 决定,不是配了 CNF 就完事。 这种寄存器级的细节,不看手册根本想不到。
第三个坑:波特率依赖系统时钟,8M 晶振不能直接算
数据对了,但接下来发现发送频率不对——理论 100ms 一帧(10 帧/秒),实际只有 5 帧/秒,慢了一半。
一开始以为是软件延时不准(确实是,后面会讲),但顺带发现一个更基础的坑:
STM32F103 上电默认跑 8MHz HSI 内部时钟,而串口波特率 115200 是按 72MHz 算的 BRR 值。
如果不配置 PLL 把 8MHz 倍频到 72MHz,直接按 72MHz 算 BRR,波特率就是错的,发出去全是乱码。
正确做法:先 clock_init() 配置 PLL(8M → 72M),再按 72MHz 算 BRR。
教训三:晶振频率 ≠ 系统时钟。 8M 晶振要经过 PLL 倍频到 72MHz,串口波特率要按倍频后的系统时钟算。
第四个坑:软件延时不靠谱,慢了一半
回到"100ms 变 200ms"的问题。
我一开始写的延时是"软件延时":
static void delay_ms(unsigned int ms) {
for (i = 0; i < ms; i++) {
for (j = 0; j < 18000; j++) { nop; }
}
}
这个 18000 是拍脑袋估的——72MHz 下,多少次空循环约等于 1ms。
结果就是不准:实际的 100ms 变成了 200ms,帧率慢一半。
修复:改用 SysTick 硬件定时器(Cortex-M3 内核自带,精确到 1ms):
// 72MHz / 1000 = 72000 周期 = 1ms
SYSTICK_LOAD = 72000 - 1;
改完,帧率精确回到 10 帧/秒。
教训四:软件延时依赖 CPU 频率和编译优化,不准且会漂。 要精确计时,用硬件定时器(SysTick),别用 for 循环数数。
第五个坑:阻塞闪灯拖慢采集
需求里有个"事件闪灯":检测到任意 GPIO 状态变化,板载 LED 闪一下(50ms)。
我一开始这么写:
if (cur_map != prev_map) { // 检测到变化
LED亮;
delay_ms(50); // 阻塞 50ms!
LED灭;
}
结果:只要 IO 状态一变,帧率就掉(100ms + 50ms = 150ms 一帧)。
因为 delay_ms(50) 是忙等,CPU 在闪灯期间啥也不干,采集发送被推迟了。
修复:改成非阻塞。用 millis() 时间戳记录点灯时刻,主循环照常采集发送,到时间了自动灭灯:
if (cur_map != prev_map) {
LED亮;
led_on_time = millis(); // 记录时刻,不阻塞
prev_map = cur_map;
}
if (led_state && millis() - led_on_time >= 50) {
LED灭; // 后台灭灯
}
改完,采集频率稳定 10 帧/秒,不受闪灯影响。
教训五:阻塞和采集要解耦。 闪灯归闪灯,采集归采集。用时间戳 + 状态机,别让一个延时把整个主循环卡住。
修复
五个坑填完,系统终于稳定了:
STM32 sensor ready
count=0,io=111111111111111111111111
count=1,io=111111111111111111111111
count=2,io=111111111111111111111111
...
24 个 IO 全 1(上拉悬空),count 稳定递增,10 帧/秒,接 GND 触发闪灯也不影响频率。
完整链路:
STM32(采集+组帧) ──TTL── ESP32(纯桥接) ──USB── 电脑(monitor)
教训
复盘这五个坑,其实都有一个共同点:"能跑"和"跑得对"之间,隔着一堆寄存器级、物理级的细节。
- 共地——物理层面,不接就是乱码
- ODR 上拉——寄存器层面,配了 CNF 不够
- PLL 倍频——时钟层面,晶振频率不是系统时钟
- SysTick——时序层面,软件延时不靠谱
- 非阻塞——架构层面,别让显示功能拖垮采集
这些东西,看教程是"哦,原来如此",亲手踩一遍才是"妈的,记住了"。
后续
这套还只是个原型。要走向真正的产品,还有一堆事:
- 软件去抖(机械开关输入要消抖)
- 提高波特率和采样频率
- 加 RS-485 转换 + Modbus RTU 协议
- 重新规划引脚 MAP,画 PCB
- 光耦隔离、宽压电源、看门狗
不过那是后话了。今天先把"能跑"这件事做扎实,五个坑,值了。
设备:STM32F103C8T6 + ESP32-D0WD-V3 + ST-LINK/V2 工具链:arm-none-eabi-gcc + ESP-IDF v5.1.5