4. 传感器数据采集基础:I2C/SPI接口传感器驱动开发、定时器触发采样、DMA传输数据、数据缓存与环形缓冲区设计
好,咱们进入第四章。这一章可以说是整个故障预测系统的「感官神经」。你想想看,没有准确、及时的数据,后面再牛的AI算法也是白搭。我刚开始做嵌入式AI那会儿,就吃过数据采集的亏——采样率不够,数据还丢包,模型训练出来就是个「睁眼瞎」。
这一章,我会带你搞定四个核心环节:传感器驱动(怎么跟传感器「说话」)、定时器触发(什么时候采样)、DMA传输(怎么不占CPU地把数据搬回来)、以及环形缓冲区(数据来了往哪儿放)。
4.1 I2C与SPI接口传感器驱动开发
先说接口。STM32上最常见的传感器接口就是I2C和SPI。我个人习惯是:低速、数据量小的传感器用I2C,高速、数据量大的用SPI。比如温度传感器、加速度计,I2C就够了;要是接摄像头或者高采样率的麦克风,那必须上SPI。
4.1.1 I2C驱动开发要点
I2C就两根线:SCL(时钟)和SDA(数据)。驱动开发说白了就是按照时序图,把「开始信号→设备地址→寄存器地址→数据读写→停止信号」这一套流程走通。
我在项目中遇到过一个问题:I2C总线挂死。排查了半天,发现是某个从设备没释放总线。后来我养成了一个习惯——每次通信前先发一个「停止信号」复位总线状态。
核心代码片段:I2C读取传感器数据
// 读取MPU6050加速度计数据
HAL_StatusTypeDef MPU6050_ReadAccel(MPU6050_HandleTypeDef *dev, int16_t *accel_data)
{
uint8_t buf[6];
uint8_t reg = MPU6050_ACCEL_XOUT_H;
// 写寄存器地址
HAL_I2C_Master_Transmit(&dev->hi2c, dev->addr, ®, 1, 100);
// 读6字节数据
HAL_I2C_Master_Receive(&dev->hi2c, dev->addr, buf, 6, 100);
// 组合成16位有符号数
accel_data[0] = (int16_t)((buf[0] << 8) | buf[1]);
accel_data[1] = (int16_t)((buf[2] << 8) | buf[3]);
accel_data[2] = (int16_t)((buf[4] << 8) | buf[5]);
return HAL_OK;
}
避坑指南:我曾经在I2C通信时忘了检查HAL函数的返回值,结果传感器偶尔不响应,程序就卡死了。后来我加了超时重试机制,问题解决。记住:任何通信都要加超时处理。
4.1.2 SPI驱动开发要点
SPI比I2C快,但线也多:SCK、MOSI、MISO、CS(片选)。每个从设备一根CS线,所以设备多了IO口不够用。
SPI驱动开发有个关键点:时钟极性(CPOL)和时钟相位(CPHA)必须和传感器匹配。我见过太多人在这上面栽跟头——读出来的数据全是0xFF或者0x00,其实就是模式没配对。
| SPI模式 | CPOL | CPHA | 数据采样边沿 |
|---|---|---|---|
| 模式0 | 0 | 0 | 上升沿采样 |
| 模式1 | 0 | 1 | 下降沿采样 |
| 模式2 | 1 | 0 | 下降沿采样 |
| 模式3 | 1 | 1 | 上升沿采样 |
嗯,这里要注意:大多数传感器默认用模式0或模式3。你拿到数据手册后,先看时序图,确认数据是在SCK的哪个边沿采样的。
4.2 定时器触发采样
传感器驱动写好了,接下来就是「什么时候采样」的问题。你总不能在主循环里一直轮询吧?那样CPU啥也别干了。
我的做法是:用定时器产生固定频率的中断,在中断服务函数里触发采样。比如故障预测需要100Hz的采样率,那就把定时器配成10ms中断一次。
定时器配置示例(100Hz采样)
// 定时器句柄
TIM_HandleTypeDef htim2;
// 定时器初始化函数
void MX_TIM2_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7200 - 1; // 72MHz / 7200 = 10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 100 - 1; // 10kHz / 100 = 100Hz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 使能更新中断
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
}
// 中断服务函数
void TIM2_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim2);
}
// 回调函数——在这里触发采样
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2->Instance)
{
// 触发ADC采样或读取传感器
Sensor_TriggerSampling();
}
}
注意:中断服务函数里不要做耗时操作!我曾经在中断里直接调用HAL_I2C_Master_Receive,结果因为I2C通信太慢,导致中断嵌套,系统直接崩了。正确的做法是:中断里只置一个标志位,主循环里处理数据。
4.3 DMA传输数据
说到DMA,说白了就是「数据搬运工」。CPU告诉DMA:「你去把传感器数据搬到内存里,搬完了告诉我一声」,然后CPU就可以去干别的事了。
我为什么强调DMA?因为故障预测系统往往需要连续采集数据,如果每次都用CPU去读,那CPU的利用率会非常低。用DMA的话,CPU可以腾出手来做FFT、特征提取这些计算密集型任务。
4.3.1 SPI+DMA传输
SPI配合DMA是最常见的组合。配置起来也不复杂:
// SPI DMA接收配置
void SPI_DMA_Receive(uint8_t *buffer, uint16_t size)
{
// 启动DMA接收
HAL_SPI_Receive_DMA(&hspi1, buffer, size);
}
// DMA传输完成回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1->Instance)
{
// 数据接收完成,处理数据
ProcessSensorData();
}
}
这里有个小技巧:使用DMA循环模式。比如你要连续采集1000个点,可以配成循环模式,DMA会自动把数据填满缓冲区,满了就从头开始覆盖。配合环形缓冲区使用,效果绝佳。
4.4 数据缓存与环形缓冲区设计
数据采集进来了,往哪儿放?直接放全局数组?那数据覆盖问题怎么处理?
我的答案是:环形缓冲区。它就像一个循环队列,有头指针和尾指针。数据从尾指针写入,从头指针读出。当缓冲区满了,新数据会覆盖最旧的数据。
环形缓冲区核心实现
// 环形缓冲区结构体
typedef struct {
float buffer[RING_BUFFER_SIZE];
uint16_t head; // 读指针
uint16_t tail; // 写指针
uint16_t count; // 当前数据个数
} RingBuffer_t;
// 初始化
void RingBuffer_Init(RingBuffer_t *rb)
{
rb->head = 0;
rb->tail = 0;
rb->count = 0;
}
// 写入数据
uint8_t RingBuffer_Write(RingBuffer_t *rb, float data)
{
rb->buffer[rb->tail] = data;
rb->tail = (rb->tail + 1) % RING_BUFFER_SIZE;
if (rb->count < RING_BUFFER_SIZE)
{
rb->count++;
}
else
{
// 缓冲区满了,覆盖最旧数据,头指针也要移动
rb->head = (rb->head + 1) % RING_BUFFER_SIZE;
}
return 1;
}
// 读取数据
uint8_t RingBuffer_Read(RingBuffer_t *rb, float *data)
{
if (rb->count == 0)
return 0; // 缓冲区空
*data = rb->buffer[rb->head];
rb->head = (rb->head + 1) % RING_BUFFER_SIZE;
rb->count--;
return 1;
}
经验之谈:我曾经在环形缓冲区上踩过一个坑——读写指针的原子性问题。在中断里写数据,在主循环里读数据,如果不加保护,可能会出现「读到了半新半旧的数据」。我的解决方案是:在临界区操作时关中断,或者使用无锁环形缓冲区(单生产者单消费者场景)。
4.5 完整数据采集链路
好了,把上面四个环节串起来,就是一个完整的数据采集链路:
- 定时器产生100Hz中断
- 中断服务函数里触发SPI DMA传输
- DMA自动把传感器数据搬进环形缓冲区
- 主循环从环形缓冲区取出数据,进行预处理和特征提取
你想想看,整个过程中CPU几乎不参与数据搬运,效率非常高。我之前的项目里,用这套方案实现了8通道、1kHz采样率的振动信号采集,CPU占用率才不到20%。
总结一下本章要点:
- I2C适合低速传感器,注意总线挂死问题
- SPI适合高速传感器,注意时钟模式匹配
- 定时器触发采样,中断里只做轻量操作
- DMA解放CPU,循环模式配合环形缓冲区
- 环形缓冲区解决数据生产者和消费者速度不匹配问题
下一章,我们会基于这一章采集到的数据,开始做信号预处理和特征提取。到时候你就知道,好的数据采集基础有多重要了。