OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 1951|回复: 2

freertos ADC数据被多个任务访问

[复制链接]

2

主题

5

帖子

0

精华

初级会员

Rank: 2

积分
107
金钱
107
注册时间
2018-4-23
在线时间
24 小时
发表于 2026-6-2 14:57:37 | 显示全部楼层 |阅读模式
1金钱
实际项目中,经常会出现,ADC数据被多个模块访问的情况,在freertos中有一些疑问。
场景如下,
1. ADC1 采用DMA对多个通道数据进行采样,采样数据有 CH1:温度   CH2: 电流  CH3:湿度,
2. 在freertos 中有3个任务,对应 温度采集模块,电流控制模块,温湿度采集模块,
3. 每个任务都需要访问 ADC采样数据,进行内部运算或者控制,

问题:这3个任务应该如何与ADC进行数据通讯?

1. 消息队列
由于ADC转换是不会停止的,可以新建一个ADC任务,每100us发送一次结果到消息队列中,
会有几个问题,
1.消息队列会被写满,设置超时时间位0,可以退出,保证100us 发送数据不变
2.消息队列的深度,设置1,保证数据发送就覆盖,保证实时性
3. 多任务接受消息队列后,无论那个任务先读,其余2个任务将丢失了,那一时刻的数据,
4. 由于任务优先级问题,优先级高的任务可以实时读取到数据,优先级低的任务可能会被多次阻塞

有没有能像全局变量那样的方式,每个任务可以按自己的任务节奏,想什么时候去读都可以,且在实时性较高





回复

使用道具 举报

2

主题

5

帖子

0

精华

初级会员

Rank: 2

积分
107
金钱
107
注册时间
2018-4-23
在线时间
24 小时
 楼主| 发表于 2026-6-2 15:14:02 | 显示全部楼层
之前用全局变量,统筹各个模块的输入输出,效率和实时性都比较好,将其转换成freeRtos后,代码复杂和操作难度,提升好多,本想一个模块建一个任务,但是会出现对任务访问的情况不知道怎么处理
回复

使用道具 举报

0

主题

107

帖子

0

精华

高级会员

Rank: 4

积分
545
金钱
545
注册时间
2026-1-29
在线时间
37 小时
发表于 2026-7-28 09:30:08 | 显示全部楼层
在 FreeRTOS 中实现多任务共享 ADC 数据的核心目标是:既要保证数据访问的实时性,又要避免竞态条件(Race Condition)。以下是针对你的场景的详细分析和解决方案:

一、核心矛盾分析
需求        挑战
多任务独立读取 ADC 数据(无需严格同步)        需避免数据覆盖/撕裂(Tearing)
高实时性(低延迟访问)        不能因锁机制阻塞关键任务
数据完整性(每个任务获取完整采样周期数据)        需解决并发读写冲突
二、推荐方案对比
方案 1:双缓冲 + 互斥锁(推荐)
原理
双缓冲结构:定义两个相同的数据缓冲区 adc_buf[2],其中一个用于 DMA 写入(后台),另一个供任务读取(前台)。
状态标记:用 volatile uint8_t adc_ready 标识当前有效缓冲区编号(0 或 1)。
互斥锁保护:仅在切换缓冲区时短暂持有锁,确保原子操作。
代码示例

// ADC 数据结构体
typedef struct {
    uint16_t ch1; // 温度
    uint16_t ch2; // 电流
    uint16_t ch3; // 湿度
} ADC_DataTypeDef;

// 双缓冲区
static ADC_DataTypeDef adc_buf[2];
volatile uint8_t adc_active_buf = 0; // 当前 DMA 写入的缓冲区索引
SemaphoreHandle_t adc_mutex;         // 互斥锁句柄

// DMA 完成回调函数(ISR 中调用)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
   
    // 切换缓冲区并释放锁(唤醒等待的任务)
    xSemaphoreTakeFromISR(adc_mutex, &xHigherPriorityTaskWoken);
    adc_active_buf ^= 1; // 翻转缓冲区索引 (0<->1)
    xSemaphoreGiveFromISR(adc_mutex, &xHigherPriorityTaskWoken);
   
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

// 任务获取最新数据(非阻塞)
bool get_adc_data(ADC_DataTypeDef* data) {
    if (xSemaphoreTake(adc_mutex, 0)) { // 尝试获取锁(不阻塞)
        uint8_t current_buf = adc_active_buf ^ 1; // 读取旧缓冲区(上次已准备好的)
        memcpy(data, &adc_buf[current_buf], sizeof(ADC_DataTypeDef));
        xSemaphoreGive(adc_mutex);
        return true;
    }
    return false; // 锁被占用,放弃本次读取
}

      
优点
零拷贝:任务直接读取最新完整数据,无需复制。
低延迟:仅在缓冲区切换时短暂持锁(微秒级)。
公平性:所有任务平等竞争锁,无优先级反转风险(若使用 configUSE_MUTEXES)。
适用场景
任务读取频率不高(如每 1ms 读一次)。
对数据实时性要求极高(如电机控制中的电流环)。
方案 2:原子操作 + 单缓冲(轻量级)
原理
单缓冲区:只有一个 adc_buf,但通过 __atomic 系列函数实现无锁读写。
强制顺序化:确保 CH1/CH2/CH3 数据要么全部更新完成,要么全部不可见。
代码示例

// 使用 C11 标准原子操作(需编译器支持)
static _Atomic ADC_DataTypeDef adc_buf;

// DMA 完成回调(ISR 中)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // 一次性更新所有通道数据(保证原子性)
    __atomic_store_n(&adc_buf.ch1, new_ch1_value, __ATOMIC_RELEASE);
    __atomic_store_n(&adc_buf.ch2, new_ch2_value, __ATOMIC_RELEASE);
    __atomic_store_n(&adc_buf.ch3, new_ch3_value, __ATOMIC_RELEASE);
}

// 任务读取数据(任何地方调用)
void read_adc_task(void *arg) {
    ADC_DataTypeDef local_copy;
    while (1) {
        // 按相同顺序读取所有字段(防止ABA问题)
        local_copy.ch1 = __atomic_load_n(&adc_buf.ch1, __ATOMIC_ACQUIRE);
        local_copy.ch2 = __atomic_load_n(&adc_buf.ch2, __ATOMIC_ACQUIRE);
        local_copy.ch3 = __atomic_load_n(&adc_buf.ch3, __ATOMIC_ACQUIRE);
        
        // 处理数据...
        osDelay(10);
    }
}

      
优点
完全无锁:消除了互斥锁带来的上下文切换开销。
极致性能:适合超高频率采样(如 >10kHz)。
缺点
内存占用翻倍:_Atomic 类型通常会导致结构体大小增加(对齐填充)。
调试困难:难以追踪数据变更历史。
方案 3:发布-订阅模式(解耦扩展性强)
原理
中央发布者:创建一个专用 adc_publisher_task(),负责从 DMA 接收数据并向订阅者广播。
订阅队列:每个任务创建独立的队列,接收自己关心的数据子集。
代码框架

// 定义主题类型
typedef enum {
    TOPIC_TEMP,
    TOPIC_CURRENT,
    TOPIC_HUMIDITY
} TopicTypeDef;

// 发布者任务
void adc_publisher_task(void *arg) {
    ADC_DataTypeDef data;
    while (1) {
        // 等待 DMA 完成信号(可通过事件组或信号量触发)
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        
        // 分发数据到各主题队列
        xQueueSend(queue_temp,   &data.ch1, 0);
        xQueueSend(queue_current,&data.ch2, 0);
        xQueueSend(queue_humidity,&data.ch3, 0);
    }
}

// 订阅者任务示例(温度监控)
void temp_monitor_task(void *arg) {
    uint16_t temp;
    while (1) {
        if (xQueueReceive(queue_temp, &temp, portMAX_DELAY)) {
            // 处理温度数据...
        }
    }
}

      
优点
模块化设计:新增传感器通道无需修改现有任务。
流量控制:可针对不同主题设置不同队列深度。
缺点
额外RAM消耗:每个主题需要一个独立队列。
调度开销:频繁的消息传递可能成为瓶颈(尤其在高频场景)。
三、决策树:如何选择最优方案?
查看代码
≤1kHz
>1kHz


开始
采样频率?
方案1: 双缓冲+互斥锁
是否需要历史数据?
方案2: 原子操作
方案3: 发布-订阅
结束
四、最佳实践建议
优先采用方案1:它在实时性和实现复杂度之间取得了最佳平衡。
优化中断优先级:确保 ADC DMA 完成中断优先级高于所有任务优先级,防止数据滞后。
添加看门狗保护:在长时间无法获取锁的情况下触发复位,避免系统挂起。
性能测试指标:
最大可持续采样率(Hz)
95%分位数的任务响应延迟(μs)
CPU利用率占比
通过合理选择并发控制机制,你可以构建出一个既高效又稳定的多任务数据采集系统。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /1 下一条

正点原子公众号

如发现本坛存在违规或侵权内容, 请点击这里发送邮件举报 (或致电020-38271790)。请提供侵权说明和联系方式。我们将及时审核依法处理,感谢配合。

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2026-8-5 00:17

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表