4. 多传感器数据采集:I2C/SPI/UART总线协议、多传感器时序同步、DMA传输与缓存管理
各位同学,欢迎来到第四章。这一章我们聊聊数据怎么从传感器“流”进MCU。说白了,就是物理世界和数字世界的桥梁怎么搭。
我做了这么多年医疗贴片,最深的体会就是:传感器选型决定了性能上限,但总线协议和采集策略决定了你能不能摸到这个上限。很多项目死在半路,不是因为传感器不好,而是数据没采对。
4.1 三大总线协议:I2C、SPI、UART
医疗贴片里,传感器接口无非这三种。我一个个说。
4.1.1 I2C:两线制,省引脚但慢
I2C只有两根线:SCL(时钟)和SDA(数据)。优点是省引脚,一个MCU可以挂一堆传感器。缺点是速度慢,标准模式100kHz,快速模式400kHz。
我在项目中遇到过一个血氧传感器,要求100Hz采样率,每次传5字节数据。算一下:100Hz × 5字节 × 8位 = 4000bps。I2C 400kHz模式理论带宽400kbps,但实际要算上地址、应答、起始停止位,利用率也就60%左右。400k × 60% = 240kbps,够用。但如果传感器数量多了,总线就挤了。
避坑指南:我曾经在一个贴片项目里挂了4个I2C传感器,结果发现总线电容太大,波形都变形了。后来加了电平转换芯片才解决。记住:I2C总线电容不要超过400pF,否则信号完整性会出问题。
4.1.2 SPI:四线制,快但占引脚
SPI有SCLK、MOSI、MISO、CS四根线。速度可以跑到几十MHz,适合高带宽传感器,比如加速度计、陀螺仪。
SPI的优点是全双工,收发同时进行。缺点是每个设备需要一根片选线,传感器多了,MCU引脚不够用。
我个人习惯:如果传感器数量≤3个,优先用SPI。超过3个,考虑I2C或者用SPI菊花链。
4.1.3 UART:点对点,简单可靠
UART只有TX和RX两根线,异步通信,不需要时钟线。医疗贴片里,UART常用于GPS模块、蓝牙模块这类“黑盒子”设备。
UART的波特率一般115200或921600。注意:UART是点对点,不能像I2C那样挂多个设备。
| 协议 | 线数 | 速度 | 适用场景 |
|---|---|---|---|
| I2C | 2 | 100k-3.4MHz | 温度、湿度、压力等低速传感器 |
| SPI | 4+ | 10-80MHz | 加速度计、陀螺仪、ADC等高速传感器 |
| UART | 2 | 115200-921600 | GPS、蓝牙、串口屏等模块 |
4.2 多传感器时序同步
医疗贴片最怕什么?数据时间戳对不上。比如心电和血氧,如果时间差超过10ms,算法算出来的结果就是错的。
为什么会这样?因为每个传感器有自己的采样时钟,MCU轮询读取时,天然存在时间偏移。
4.2.1 硬件同步方案
最可靠的办法:用传感器的同步引脚。很多高端传感器(比如BMI160、MPU9250)都有SYNC引脚,可以接收外部触发信号。
我建议的做法:MCU输出一个PWM信号,同时触发所有传感器的采样。这样所有传感器在同一时刻开始采集,时间差可以控制在微秒级。
4.2.2 软件同步方案
如果传感器没有同步引脚,那就只能靠软件了。核心思路:用一个高精度定时器(比如TIM1)产生中断,在中断里依次读取所有传感器。
注意:读取顺序要固定,并且记录每个传感器的读取时间戳。算法端根据时间戳做插值对齐。
小技巧:我曾经在一个项目中,用DMA+定时器实现了“伪同步”。定时器触发DMA,DMA自动从多个SPI接口读取数据,CPU全程不参与。这样时间抖动可以控制在1个DMA时钟周期内。
4.3 DMA传输与缓存管理
医疗贴片的数据量其实不小。一个三轴加速度计,100Hz采样率,每个轴16位,每秒就是 3 × 2 × 100 = 600字节。再加上心电、血氧、温度... 每秒几KB很正常。
如果CPU逐字节读取,那它啥也别干了,光伺候传感器了。这时候就需要DMA。
4.3.1 DMA的基本原理
DMA就是硬件搬运工。CPU告诉DMA:“从外设寄存器搬100个字节到内存地址0x20001000”,然后CPU就可以去干别的事了。DMA搬完数据,发个中断通知CPU。
说白了,DMA让CPU从“搬运工”变成了“管理者”。
4.3.2 双缓冲(Ping-Pong Buffer)
这是我最常用的技巧。两个缓冲区,一个给DMA写,一个给CPU读。DMA写满Buffer A后,自动切换到Buffer B,同时触发中断通知CPU处理Buffer A的数据。
这样CPU和DMA各干各的,互不干扰。数据不会丢,也不会被覆盖。
// 伪代码示例:双缓冲初始化
#define BUF_SIZE 256
uint8_t buffer_a[BUF_SIZE];
uint8_t buffer_b[BUF_SIZE];
void dma_init(void) {
// 配置DMA通道,目标地址为buffer_a
// 传输完成中断触发后,切换目标地址到buffer_b
// 同时置位标志位,通知主循环处理buffer_a
}
void main_loop(void) {
while(1) {
if(buffer_a_ready) {
process_data(buffer_a, BUF_SIZE);
buffer_a_ready = 0;
}
if(buffer_b_ready) {
process_data(buffer_b, BUF_SIZE);
buffer_b_ready = 0;
}
// 其他任务...
}
}
4.3.3 环形缓冲区(Ring Buffer)
如果数据流是连续的,双缓冲可能不够用。这时候用环形缓冲区。DMA持续往环形缓冲区写数据,CPU从缓冲区读数据。只要读的速度大于写的速度,就不会丢数据。
嗯,这里要注意:环形缓冲区要处理好“写指针追尾读指针”的问题。我一般留一个空位作为“满标志”,避免读写指针相等时无法区分空和满。
警告:我曾经在一个项目中,环形缓冲区写指针追上了读指针,导致新数据覆盖了未处理的老数据。算法算出来的心率直接跳到了200bpm。后来加了“半满中断”才解决——当缓冲区使用超过50%时,强制提高CPU处理优先级。
4.4 实战建议:医疗贴片的数据采集架构
最后,我分享一下个人常用的架构:
- 传感器层:每个传感器独立配置,开启DMA传输模式
- DMA层:每个传感器对应一个DMA通道,使用双缓冲
- 缓存层:DMA中断里,将数据从双缓冲搬到环形缓冲区
- 处理层:主循环或RTOS任务,从环形缓冲区取数据,做滤波、融合、打包
- 输出层:通过UART或BLE将数据发送出去
这个架构我用了好几年,在多个医疗贴片项目里验证过。数据不丢、时间戳准确、CPU占用率低。你想想看,如果CPU大部分时间都在睡觉,那电池续航自然就长了。
好了,这一章就到这里。下一章我们聊聊传感器数据怎么滤波——别小看这一步,有时候一个卡尔曼滤波器能救活整个项目。