基于CMSIS RTOS 2:ADC回调/ISR向线程传输数据的方案问询
我在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

