4、传感器数据采集任务:常见传感器(IMU、编码器、超声波、摄像头)的驱动与DMA/中断方式采集

在FreeRTOS机器人架构中,传感器数据采集任务位于系统的最底层,直接决定了上层推理、控制与通信任务的数据质量与实时性。本章聚焦于四类最具代表性的传感器——IMU(惯性测量单元)、编码器、超声波测距模块和摄像头,深入剖析其驱动设计要点,并重点讲解如何利用STM32的DMA(直接存储器访问)与中断机制,在FreeRTOS多任务环境下实现高效、低延迟、不阻塞CPU的数据采集。我们将从传感器特性出发,对比轮询、中断与DMA三种采集模式的适用场景,并给出可落地的任务设计与代码框架。

传感器层 IMU (SPI/I2C) 陀螺仪+加速度计 编码器 (TIM/EXTI) AB相正交脉冲 超声波 (GPIO+ICU) TRIG/ECHO 脉宽 摄像头 (DCMI) OV2640/OV7725 采集方式 轮询 (Polling) CPU阻塞,低效 中断 (EXTI/TIM) 事件触发,实时响应 DMA 传输 硬件搬运,零CPU干预 DMA+空闲中断 数据缓冲与通知 DMA 双缓冲 Ping-Pong 切换 环形队列 (RingBuf) 生产者-消费者 任务通知/信号量 ISR 中解除阻塞 FreeRTOS 队列 任务间数据传递 上层任务 姿态解算任务 互补/卡尔曼滤波 里程计任务 速度/位置积分 避障规划任务 超声波融合 视觉处理任务 AI/图像处理 采集方式选择依据:数据率、实时性、CPU负载

上图展示了传感器数据采集的整体架构:底层为四类传感器,中间层为三种采集方式,上层为数据缓冲与通知机制,最终服务于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控制器完成。

操作流程:

  1. 初始化SPI外设:配置为DMA模式,设置数据宽度为8位或16位,使能TX和RX DMA请求。
  2. 配置DMA通道:为SPI的RX分配一个DMA通道,设置传输方向为外设到内存,内存地址为IMU数据缓冲区。
  3. 启动DMA传输:在FreeRTOS任务中,通过调用HAL_SPI_Receive_DMA()启动一次异步读取。
  4. 注册DMA完成中断:在DMA传输完成中断中,使用BaseType_t xHigherPriorityTaskWoken = pdFALSE;和vTaskNotifyGiveFromISR()通知采集任务数据已就绪。
  5. 任务等待通知:采集任务调用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)中断方式。

操作流程:

  1. 配置定时器输入捕获:将定时器通道配置为上升沿捕获,并使能捕获中断。
  2. 发送触发信号:在任务中通过GPIO输出10μs高电平到TRIG引脚。
  3. 捕获上升沿:在捕获中断中记录当前计数值,并将捕获通道切换为下降沿捕获。
  4. 捕获下降沿:在捕获中断中记录第二个计数值,计算脉宽。
  5. 计算距离:距离 = 脉宽时间 × 声速 / 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友好的传感器数据采集系统,为上层推理和控制任务提供稳定可靠的数据基础。