4、传感器数据采集任务:常见传感器(IMU、编码器、超声波、摄像头)的驱动与DMA/中断方式采集
在FreeRTOS机器人架构中,传感器数据采集任务位于系统的最底层,直接决定了上层推理、控制与通信任务的数据质量与实时性。本章聚焦于四类最具代表性的传感器——IMU(惯性测量单元)、编码器、超声波测距模块和摄像头,深入剖析其驱动设计要点,并重点讲解如何利用STM32的DMA(直接存储器访问)与中断机制,在FreeRTOS多任务环境下实现高效、低延迟、不阻塞CPU的数据采集。我们将从传感器特性出发,对比轮询、中断与DMA三种采集模式的适用场景,并给出可落地的任务设计与代码框架。
上图展示了传感器数据采集的整体架构:底层为四类传感器,中间层为三种采集方式,上层为数据缓冲与通知机制,最终服务于FreeRTOS中的各类处理任务。在实际工程中,我们需要根据传感器的数据特性(如数据率、数据量、实时性要求)选择最合适的采集方式,并设计合理的任务间通信机制。
4.1 传感器特性与采集方式选型
在开始编写驱动之前,必须深入理解每类传感器的物理特性和接口协议,这直接决定了采集方式的选择。下表对比了四类传感器的关键参数与推荐采集方式:
| 传感器类型 | 接口协议 | 数据率/更新率 | 数据量 | 推荐采集方式 | 关键考量 |
|---|---|---|---|---|---|
| IMU (MPU6050/ICM20602) | I2C / SPI | 100Hz ~ 1kHz | 6轴原始数据,每次约14字节 | SPI + DMA 或 I2C + 中断 | 需要高频率、低延迟;SPI+DMA可大幅降低CPU占用 |
| 编码器 (增量式AB相) | GPIO + TIM (编码器模式) | 取决于电机转速,可达数十kHz | 脉冲计数,4字节/次 | 硬件定时器编码器模式 + 定时中断 | 硬件自动计数,无需CPU干预;定时读取计数值即可 |
| 超声波 (HC-SR04) | GPIO (TRIG/ECHO) | 10Hz ~ 50Hz | 单次测距,2字节 | GPIO中断 + 输入捕获 (ICU) | 测量脉宽,需精确计时;中断方式避免阻塞 |
| 摄像头 (OV2640/OV7725) | DCMI (数字摄像头接口) | 15 ~ 60 FPS | QVGA (320x240) 约150KB/帧 | DCMI + DMA 双缓冲 | 数据量大,必须DMA;双缓冲避免帧撕裂 |
核心选型原则:对于数据率高、数据量大的传感器(如摄像头),必须使用DMA;对于数据率中等但实时性要求高的传感器(如IMU),推荐使用中断或DMA;对于数据率低、事件驱动的传感器(如超声波),使用中断+输入捕获即可。轮询方式仅适用于数据率极低且CPU负载不高的场景,在FreeRTOS多任务系统中应尽量避免。
4.2 IMU驱动与DMA采集实现
IMU是机器人姿态感知的核心传感器,通常通过SPI或I2C接口与STM32连接。以SPI接口的ICM20602为例,其数据率可达1kHz,若使用轮询方式读取,将严重占用CPU时间。推荐采用SPI+DMA的方式,将数据读取操作交给DMA控制器完成。
操作流程:
- 初始化SPI外设:配置为DMA模式,设置数据宽度为8位或16位,使能TX和RX DMA请求。
- 配置DMA通道:为SPI的RX分配一个DMA通道,设置传输方向为外设到内存,内存地址为IMU数据缓冲区。
- 启动DMA传输:在FreeRTOS任务中,通过调用
HAL_SPI_Receive_DMA()启动一次异步读取。 - 注册DMA完成中断:在DMA传输完成中断中,使用
BaseType_t xHigherPriorityTaskWoken = pdFALSE;和vTaskNotifyGiveFromISR()通知采集任务数据已就绪。 - 任务等待通知:采集任务调用
ulTaskNotifyTake()阻塞等待,收到通知后处理数据。
// IMU DMA采集任务伪代码
void IMU_Task(void *arg) {
uint8_t imu_rx_buf[14];
// 初始化SPI DMA
HAL_SPI_Receive_DMA(&hspi1, imu_rx_buf, 14);
while(1) {
// 等待DMA完成通知
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
// 解析IMU数据
parse_imu_data(imu_rx_buf);
// 将数据发送到姿态解算队列
xQueueSend(attitude_queue, &imu_data, 0);
// 启动下一次DMA读取
HAL_SPI_Receive_DMA(&hspi1, imu_rx_buf, 14);
}
}
// DMA完成中断回调
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(imu_task_handle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
经验提醒:IMU数据通常包含陀螺仪和加速度计的原始值,建议在DMA中断中只做数据就绪通知,将数据解析和滤波放在任务上下文中执行,避免在ISR中做浮点运算或复杂处理。同时,SPI的时钟频率不宜过高,一般控制在10MHz以内,以保证信号完整性。
4.3 编码器采集:硬件定时器模式与中断
增量式编码器输出两路相位差90°的方波信号(A相和B相),STM32的定时器具有专门的编码器接口模式,可以自动对脉冲进行计数,并根据相位关系判断旋转方向。这种硬件计数方式完全不需要CPU干预,是最理想的采集方案。
实现步骤:
- 配置定时器为编码器模式:将TIM的CH1和CH2分别映射到A相和B相,设置计数模式为TI1和TI2的边沿计数。
- 设置计数范围:根据编码器线数设置ARR值,例如500线编码器,4倍频后每转2000个脉冲,ARR设为1999。
- 使能更新中断:当计数器溢出或下溢时触发中断,用于记录圈数。
- 周期性读取计数值:在FreeRTOS中创建一个周期任务(如10ms周期),读取TIM->CNT寄存器,计算速度。
// 编码器速度计算任务
void Encoder_Task(void *arg) {
int16_t last_count = 0;
TickType_t last_tick = xTaskGetTickCount();
while(1) {
vTaskDelay(pdMS_TO_TICKS(10)); // 10ms周期
int16_t current_count = (int16_t)TIM2->CNT;
int16_t delta = current_count - last_count;
// 计算速度:delta / 10ms / 2000 * 60 = RPM
float rpm = (float)delta / 2000.0f * 100.0f * 60.0f;
last_count = current_count;
// 发送速度数据到控制任务
xQueueSend(speed_queue, &rpm, 0);
}
}
风险提示:编码器计数器的数据类型必须使用有符号数(int16_t或int32_t),否则在正反转切换时会出现计数错误。同时,要注意定时器的溢出中断处理,防止计数回绕导致的速度计算错误。建议在溢出中断中维护一个圈数变量,与CNT寄存器组合成32位完整计数。
4.4 超声波传感器:中断+输入捕获方案
HC-SR04超声波传感器通过TRIG引脚发送10μs以上的高电平触发信号,然后ECHO引脚会输出一个与距离成正比的高电平脉宽。测量这个脉宽的时间即可计算距离。由于脉宽测量需要微秒级精度,且等待期间不能阻塞CPU,因此推荐使用输入捕获(ICU)中断方式。
操作流程:
- 配置定时器输入捕获:将定时器通道配置为上升沿捕获,并使能捕获中断。
- 发送触发信号:在任务中通过GPIO输出10μs高电平到TRIG引脚。
- 捕获上升沿:在捕获中断中记录当前计数值,并将捕获通道切换为下降沿捕获。
- 捕获下降沿:在捕获中断中记录第二个计数值,计算脉宽。
- 计算距离:距离 = 脉宽时间 × 声速 / 2。
// 输入捕获中断回调
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) {
static uint32_t rising_time = 0;
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1) {
if(__HAL_TIM_GET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1) == TIM_INPUTCHANNELPOLARITY_RISING) {
rising_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
// 切换为下降沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_FALLING);
} else {
uint32_t falling_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
uint32_t pulse_width = falling_time - rising_time;
// 计算距离并发送到避障队列
float distance = pulse_width * 0.034f / 2.0f; // 单位cm
xQueueSendFromISR(ultrasonic_queue, &distance, NULL);
// 切换回上升沿捕获
__HAL_TIM_SET_CAPTUREPOLARITY(htim, TIM_CHANNEL_1, TIM_INPUTCHANNELPOLARITY_RISING);
}
}
}
实战要点:超声波传感器存在测量盲区(通常2cm以内)和最大量程限制(通常4m)。在任务中应加入数据有效性检查,过滤掉超出量程的异常值。同时,由于声波传播速度受温度影响,在精度要求高的场景下应加入温度补偿。
4.5 摄像头DMA双缓冲采集
摄像头是数据量最大的传感器,以OV2640为例,输出QVGA(320×240)RGB565格式图像,每帧约150KB。若使用CPU逐像素搬运,将完全占用CPU资源。必须使用DCMI接口+DMA双缓冲方案。
双缓冲机制:
- 分配两个帧缓冲区:Buffer A和Buffer B,大小均为150KB。
- DMA交替传输:当DMA正在填充Buffer A时,CPU可以处理Buffer B中的数据;反之亦然。
- 帧完成中断:每帧传输完成触发DMA中断,在中断中切换缓冲区指针,并通知视觉处理任务。
// 摄像头DMA双缓冲配置
#define FRAME_SIZE 320 * 240 * 2 // RGB565
static uint8_t frame_buffer[2][FRAME_SIZE] __attribute__((aligned(32)));
static uint8_t active_buffer = 0;
void Camera_Init(void) {
// 配置DCMI接口
// 配置DMA为循环模式,双缓冲
HAL_DMAEx_MultiBufferStart(&hdma_dcmi, (uint32_t)&DCMI->DR,
(uint32_t)frame_buffer[0],
(uint32_t)frame_buffer[1], FRAME_SIZE);
// 使能DCMI捕获
HAL_DCMI_Start_DMA(&hdcmi, DCMI_MODE_CONTINUOUS, 0, 0);
}
// DMA传输完成中断
void DMA_IRQHandler(void) {
if(__HAL_DMA_GET_FLAG(&hdma_dcmi, DMA_FLAG_TC)) {
__HAL_DMA_CLEAR_FLAG(&hdma_dcmi, DMA_FLAG_TC);
// 切换缓冲区
active_buffer ^= 1;
// 通知视觉任务处理已完成的帧
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
vTaskNotifyGiveFromISR(vision_task_handle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
内存风险:两个150KB的缓冲区共需300KB内存,对于STM32F4系列(通常192KB RAM)可能不够。解决方案包括:使用SDRAM外部内存、降低分辨率(如QQVGA 160×120)、或使用JPEG压缩输出模式。在设计阶段必须评估内存预算。
4.6 案例拆解:四传感器融合采集系统
下面通过一个完整的案例,展示如何在FreeRTOS中整合上述四种传感器的采集任务。假设我们正在设计一个两轮差速机器人,需要同时采集IMU、左右轮编码器、前方超声波和摄像头图像。
系统约束:
- MCU:STM32F407VET6(192KB RAM,1MB Flash)
- IMU:ICM20602,SPI接口,1kHz输出
- 编码器:500线增量式,左右各一个
- 超声波:HC-SR04,10Hz采样
- 摄像头:OV2640,QQVGA(160×120)RGB565,30FPS
任务划分与优先级:
| 任务名称 | 优先级 | 周期/触发方式 | 功能 |
|---|---|---|---|
| IMU采集任务 | 高 (5) | DMA完成中断触发 | 读取IMU数据,发送到姿态解算队列 |
| 编码器采集任务 | 中 (4) | 10ms周期 | 读取左右编码器计数,计算速度 |
| 超声波采集任务 | 中 (3) | 100ms周期 | 触发测距,等待中断结果 |
| 摄像头采集任务 | 高 (6) | DMA帧完成中断触发 | 切换缓冲区,通知视觉处理 |
| 视觉处理任务 | 低 (2) | 摄像头帧通知触发 | 图像处理(如颜色识别) |
实施过程与问题排查:
在初次调试时,发现IMU数据偶尔出现跳变。通过逻辑分析仪抓取SPI波形,发现是DMA传输与SPI配置冲突导致。解决方案是在DMA传输完成中断中,先关闭SPI,再重新配置DMA,最后重新使能SPI。另一个问题是摄像头DMA双缓冲的内存对齐问题,将缓冲区声明为32字节对齐后解决。
性能对比:
- 使用轮询方式采集IMU(1kHz):CPU占用率约35%
- 使用DMA方式采集IMU:CPU占用率降至5%以下
- 摄像头使用DMA双缓冲后,CPU占用率从80%降至15%
复盘经验:在FreeRTOS多传感器系统中,建议为每个传感器创建独立的采集任务,并通过队列或任务通知与上层处理任务解耦。这样既保证了数据采集的实时性,又避免了任务间的相互阻塞。同时,要充分利用STM32的硬件外设(DMA、定时器、输入捕获),将CPU从繁重的数据搬运中解放出来,专注于算法和控制逻辑。
4.7 常见误区与总结
在传感器采集任务设计中,以下误区需要特别注意:
- 误区一:所有传感器都使用轮询。在FreeRTOS中,轮询会阻塞任务,导致高优先级任务无法及时响应。应优先使用中断和DMA。
- 误区二:在ISR中做复杂处理。ISR应保持短小精悍,只做必要的标志设置和数据通知,将耗时处理放在任务中。
- 误区三:忽略DMA缓冲区对齐。STM32的DMA要求缓冲区地址按数据宽度对齐,否则可能导致总线错误或数据错乱。
- 误区四:未考虑数据一致性。当DMA正在写入缓冲区时,如果任务同时读取该缓冲区,会产生数据撕裂。必须使用双缓冲或互斥机制。
- 误区五:中断优先级配置不当。FreeRTOS要求所有使用ISR中FreeRTOS API的中断优先级不得高于
configMAX_SYSCALL_INTERRUPT_PRIORITY,否则会导致系统崩溃。
总结而言,传感器数据采集任务的设计核心在于:理解传感器特性、选择合适的采集方式、合理利用STM32硬件资源、设计高效的任务间通信机制。通过DMA和中断的合理组合,可以在FreeRTOS中构建一个低延迟、高吞吐、CPU友好的传感器数据采集系统,为上层推理和控制任务提供稳定可靠的数据基础。