← 返回首页

从零搭一套 STM32 数据采集系统:一个下午踩了五个坑

2026-08-28 19:30

读 GPIO、组帧、串口发送、ESP32 桥接……听起来简单,真正上手才发现,"能跑"和"跑得对"之间,隔着五个想骂人的坑。

写在前面

事情的起因很简单:手上有一块 STM32F103C8T6(Blue Pill 最小系统板),想让它读一堆 GPIO 状态,编码成数据帧,通过串口发出来。

听起来是个"hello world"级别的任务,对吧?读引脚、拼字符串、发串口,三件事。

结果我花了一个下午,从下午五点干到晚上七点,中间踩了五个坑。每一个坑都不大,但每一个都能让你盯着乱码和飘忽的数据怀疑人生。

这篇文章把这五个坑记下来,给后来的自己,也给同样在折腾的朋友。

起因:为什么要做这个

需求很简单:

  1. STM32 读 24 个 GPIO 输入状态
  2. 累加器 count 从 0 开始无限加
  3. 组装成 count=xxx,io=111100001111... 这样的数据帧
  4. 通过串口发出去

而且,我手上没有 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

时好时坏,乱码随机出现。我一度以为是波特率问题,调了半天。

最后做个实验才破案:

  1. 把 STM32 拔了 ST-LINK,插到充电宝(脱离电脑 USB)
  2. GND 跳线还在 → 正常
  3. 拔掉 GND 跳线 → 乱码
  4. 插回 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)

教训

复盘这五个坑,其实都有一个共同点:"能跑"和"跑得对"之间,隔着一堆寄存器级、物理级的细节。

  1. 共地——物理层面,不接就是乱码
  2. ODR 上拉——寄存器层面,配了 CNF 不够
  3. PLL 倍频——时钟层面,晶振频率不是系统时钟
  4. SysTick——时序层面,软件延时不靠谱
  5. 非阻塞——架构层面,别让显示功能拖垮采集

这些东西,看教程是"哦,原来如此",亲手踩一遍才是"妈的,记住了"。

后续

这套还只是个原型。要走向真正的产品,还有一堆事:

  • 软件去抖(机械开关输入要消抖)
  • 提高波特率和采样频率
  • 加 RS-485 转换 + Modbus RTU 协议
  • 重新规划引脚 MAP,画 PCB
  • 光耦隔离、宽压电源、看门狗

不过那是后话了。今天先把"能跑"这件事做扎实,五个坑,值了。


设备:STM32F103C8T6 + ESP32-D0WD-V3 + ST-LINK/V2 工具链:arm-none-eabi-gcc + ESP-IDF v5.1.5

← 查看上一篇:致萍宝 · 二十三岁的风
已经是最新文章了