You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于CMSIS RTOS 2:ADC回调/ISR向线程传输数据的方案问询

STM32 ADC DMA乒乓缓冲+CMSIS RTOS2 数据处理问题解答

我在STM32平台使用带DMA的ADC采集模拟值,传输完成时触发HAL回调。由于数据处理耗时较长,为避免阻塞回调函数,将数据传输至线程处理。采用长度200的缓冲(分两个100长度的半缓冲,乒乓机制),在ADC半完成和完成事件触发回调,避免同一缓冲数据重叠。当前基于CMSIS RTOS 2的实现代码如下:

#define BUFFER_SIZE 100
static int16_t buffer[BUFFER_SIZE*2] = {0};

static volatile int16_t *p_buf[2] = {&(buffer[0]), &(buffer[BUFFER_SIZE])};

typedef struct
{
    void *addr;
    uint32_t len;
} msg_t;

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    msg_t msg;
    msg.addr = (void *)p_buf[0];
    msg.len = BUFFER_SIZE;

    osMessageQueuePut(queue, &msg, 0, 0);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    msg_t msg;
    msg.addr = (void *)p_buf[1];
    msg.len = BUFFER_SIZE;

    osMessageQueuePut(queue, &msg, 0, 0);
}

static void process_thread(void *argument)
{
    msg_t msg;

    queue = osMessageQueueNew(4, sizeof(msg_t), NULL);

    while (1)
    {
        osMessageQueueGet(queue, &msg, 0, osWaitForever);
        // process data
    }
}

问题1:CMSIS RTOS 2中从回调/ISR向线程传输半缓冲数据的推荐方式

当前使用消息队列是合理且推荐的方案,它能直接传递缓冲地址、长度等上下文信息,适配多缓冲场景。需要注意几个关键细节:

  • 队列初始化要提前:不能在处理线程内创建队列(如当前代码),否则回调可能在队列就绪前触发,导致数据丢失。建议在全局初始化函数或main线程中创建队列,确保回调触发前队列已就绪。
  • ISR环境调用规范:在HAL回调(本质是ISR上下文)中调用osMessageQueuePut时,超时参数必须设为0,因为ISR不能阻塞等待队列空闲。如果队列已满,当前调用会直接失败,这时候需要配合问题2的流量控制方案处理。
  • 保持volatile修饰:缓冲指针p_buf已用volatile修饰,确保ISR和线程间的内存可见性,这个做法正确,需保留。

除此之外,也可以用二进制信号量+共享缓冲数组的方式:每个缓冲对应一个信号量,ISR完成后释放对应信号量,线程获取信号量后处理对应缓冲。但这种方式需要额外维护缓冲状态,灵活性不如消息队列,因此优先推荐消息队列方案。


问题2:处理线程耗时过长导致队列满、缓冲被覆盖的解决办法

当前问题核心是采集速度 > 处理速度,且缓冲数量(2个)与队列大小(4)不匹配,队列满后新采集会覆盖旧缓冲的未处理数据。可从以下维度解决:

1. 匹配缓冲数量与队列大小,扩展缓冲池

把乒乓缓冲扩展为多缓冲池,比如增加到4个缓冲,对应队列大小设为4。这样每个消息对应独立缓冲,即使队列满,新采集也有未使用的缓冲可用,不会覆盖正在处理的旧数据。示例修改:

#define BUFFER_COUNT 4
#define BUFFER_SIZE 100
static int16_t buffer[BUFFER_COUNT * BUFFER_SIZE] = {0};
static volatile int16_t *p_buf[BUFFER_COUNT];

// 初始化缓冲指针
void buffer_init(void)
{
    for(int i=0; i<BUFFER_COUNT; i++)
    {
        p_buf[i] = &buffer[i * BUFFER_SIZE];
    }
}

同时需调整ADC DMA传输配置:若用循环DMA,需将缓冲长度设为BUFFER_COUNT*BUFFER_SIZE,并在回调中循环切换缓冲指针;若用非循环DMA,每次完成后重新启动DMA并切换缓冲。

2. 提升处理线程优先级

创建处理线程时设置更高优先级(高于系统中其他非实时线程),确保线程能及时被调度,减少数据堆积。示例:

osThreadAttr_t thread_attr = {
    .priority = osPriorityHigh // 设为高优先级
};
osThreadNew(process_thread, NULL, &thread_attr);

3. 优化数据处理逻辑

分析处理代码耗时点,针对性优化:

  • 用查表法替代复杂运算,减少不必要的计算;
  • 把批量处理拆分为小块,每处理部分数据后主动调用osThreadYield(),避免长时间独占CPU;
  • 若业务允许,降低数据精度或采样率,减少单缓冲数据量。

4. 加入流量控制(极端场景)

若以上方法无法满足需求,可在回调中检查队列是否已满,若满则暂时停止ADC采集,直到线程处理完部分数据后再恢复。示例:

static volatile uint8_t need_resume_adc = 0;

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    msg_t msg;
    msg.addr = (void *)p_buf[0];
    msg.len = BUFFER_SIZE;

    // 队列无空间则停止ADC,标记需要恢复
    if(osMessageQueuePut(queue, &msg, 0, 0) != osOK)
    {
        HAL_ADC_Stop_DMA(hadc);
        need_resume_adc = 1;
    }
}

处理线程中补充恢复逻辑:

while(1)
{
    osMessageQueueGet(queue, &msg, 0, osWaitForever);
    // process data
    
    // 检查并恢复ADC采集
    if(need_resume_adc)
    {
        HAL_ADC_Start_DMA(&hadc, (uint32_t*)buffer, BUFFER_SIZE*2);
        need_resume_adc = 0;
    }
}

内容的提问来源于stack exchange,提问作者Android_dev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 23:45:34