ADC_DMA_双buffer传输

news/2024/11/17 21:38:35/文章来源:https://www.cnblogs.com/huajchen/p/18414214

ADC_DMA_双buffer传输

image-20240913091300573

线程A

  1. 切换buffer地址
  2. 开启ADC转换,并使用DMA传输
  3. 等待获取DMA中断的信号量,获取到信号量,表示上一次DMA传输已完成
  4. 将地址通过消息队列传输给线程B
uint32_t *adc_value = NULL;/* USER CODE END Header_adc_dma_task_function */
void adc_dma_task_function(void *argument)
{/* USER CODE BEGIN adc_dma_task_function */uint32_t *addr = NULL;uint8_t i = 0;//系数adc_value = pvPortMalloc(2 * sizeof(uint32_t));	//申请两块buffer	/* Infinite loop */for(;;){i++;if(2 <= i){i = 0;}        addr = &adc_value[i];//对addr切换地址HAL_ADC_Start_DMA(&hadc1, &adc_value[i], 1);//开启ADC转换,并使用DMA传输osSemaphoreAcquire(dma_semaphoreHandle, osWaitForever);//获取DMA中断的信号量osMessageQueuePut(adc_queueHandle, &addr, NULL, osWaitForever);//将buffer地址传输给线程B
//				osDelay(500);}/* USER CODE END adc_dma_task_function */
}

线程B

  1. 通过消息队列接受到传来的buffer的地址
  2. 通过地址读取数据并进行转换
void conversion_taskFun(void *argument)
{/* USER CODE BEGIN conversion_taskFun */uint32_t *addr = NULL;uint32_t adc_value = 0;float vlotage = 0;float temperate = 0;/* Infinite loop */for(;;){osMessageQueueGet(adc_queueHandle, &addr, NULL, osWaitForever);adc_value = *addr;vlotage = ((float)adc_value / 4096) * 3.3;temperate = (1.43 - vlotage) / 0.0043 + 25.0;    // 转换为温度值,转换公式:T(℃)= ((V25 - Vsense) / Avg_Slope) + 25log_i("adc buffer address: %X", addr);log_i("adc value: %d", adc_value);log_i("temperate: %0.2f", temperate); }/* USER CODE END conversion_taskFun */
}

DMA中断

extern osSemaphoreId_t dma_semaphoreHandle;
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{if(hadc == &hadc1){osSemaphoreRelease(dma_semaphoreHandle);}
}

为什么需要两个线程

答:之所以有两个线程(一个处理adc+dma的数据流,另一个打印DMA的),是为了实时性,如果只有一个线程且线程在打印过程中,如果数据准备好了,那么这个线程要等这个线程打印完数据,才回去响应发过来的信号量,这就不满足实时性要求。

存在的风险

当Buffer1被DMA填充完数据,线程A启动新的DMA传输至新的Buffer2时,有可能会出现Buffer2正在被线程B使用 转换电压和打印。因此在线程A启动传输至Buffer2时,需要确认线程B已经处于非Busy状态(已经转换完电压),线程A才能启动向Buffer2填充ADC值的新的传输,因为,线程B很可能此时正在尝试读取Buffer2的ADC值,若是在此时,线程A直接启动传输,那Buffer2很可能被DMA新的数据覆盖就很有可能丢失原先ADC的数据,造成ADC数据的不连续或总线错误。

升级做法

在线程A和线程B之间添加互斥锁

在线程A开始启动向Buffer2进行DMA传输之前获取互斥锁,开启传输之后释放互斥锁。

线程B在读取消息队列时,先获取互斥锁,打印结束,释放互斥锁。

线程A

当DMA输出到Buffer1后,触发DMA中断(中断回调做的事:发送消息队列给线程A)。线程A接收到消息队列(或二值信号量)后,第一步,先检查线程B的接收队列中是否有消息队列还未处理(queue_peek),若线程B的消息接收队列中已经为空,说明线程B已经开始处理,但不确定线程B已经处理完,此时,线程A继续尝试获取互斥锁,若成功获得互斥锁,则确认线程B已经处理完Buffer2中的数据并且打印完,此时,线程A开始启动向Buffer2进行DMA传输,释放和B线程的互斥锁,并发送邮箱给线程B。

线程B

当线程B接收到消息队列(邮箱)时(没接收到消息时,均为queue_peek函数阻塞),先尝试获取互斥锁,若获得互斥锁,则使用queue_receive拿走消息队列(邮箱)中的数据,并开始解析消息队列(邮箱)所指示的Buffer,并将解析出来的电压值通过串口打印,这一切结束后,释放互斥锁,接着等待新的消息队列(邮箱)。

#define WAITMAX 0xffffffff
SemaphoreHandle_t Send_to_taskxQueue;
SemaphoreHandle_t mutex;
QueueHandle_t dcxQueue;
BaseType_t TaskWoken = pdFALSE;
uint8_t flag = 0;
uint8_t addressflag;//用于切换buffer地址//线程A,生产者线程
void adc_thread(void *para)
{//变量的生命周期只和它所定义的地址有关//buffer2它是局部变量,它的生命周期和线程是绑定的,存在栈里//malloc是在堆区创建一块内存,这块内存只要没回收,就是永生的。uint32_t *buffer2 = malloc(sizeof(uint32_t));//先申请uint32_t *buffer1 = malloc(sizeof(uint32_t));if ( (NULL == buffer2)  &&                   //判空(NULL == buffer1)    ){uartprintf("invalid memory \r\n");while(1);}memset(buffer2,0,sizeof(uint32_t));				// 初始化内存memset(buffer1,0,sizeof(uint32_t));Send_to_taskxQueue = xSemaphoreCreateBinary();//创建二值信号量mutex = xSemaphoreCreateMutex();//创建互斥量dcxQueue = xQueueGenericCreate( 1, sizeof(uint32_t),queueQUEUE_TYPE_BASE);//创建队列uint32_t temp = 0;HAL_ADC_Start_DMA(&hadc1,buffer1,sizeof(uint32_t)); //开启DMAwhile(1){xSemaphoreTake(Send_to_taskxQueue,WAITMAX);//获取信号量,该信号量来自中断if(1 == flag){xQueuePeek(dcxQueue,&temp,0);//先窥探队列if(temp == 0) //判断是否数据没被拿走{//数据被线程adc_information_dispose拿走了xSemaphoreTake(mutex,0xffffffff); //获取互斥量,虽然拿走了但是还需要检查是否处理完数据了	xSemaphoreGive(mutex); //确定线程adc_information_dispose处理完数据处于空闲状态了if(0 == addressflag)//用于切换buffer地址{xQueueSendToBack(dcxQueue,buffer1,WAITMAX);HAL_ADC_Start_DMA(&hadc1,buffer2,sizeof(uint32_t));addressflag = 1;}else{xQueueSendToBack(dcxQueue,buffer2,WAITMAX);HAL_ADC_Start_DMA(&hadc1,buffer1,sizeof(uint32_t));addressflag = 0;}	}else{				uartprintf("adc_information_dispose data no\r\n");	}}else//第一次采集,队列中还没有数据,不用获取互斥量,先使用buffer1{flag = 1;xQueueSendToBack(dcxQueue,buffer1,WAITMAX);HAL_ADC_Start_DMA(&hadc1,buffer2,sizeof(uint32_t));addressflag = 1;}}
}

配置ADC的步骤

  1. 电压输入范围:ADC的本质是对电压信号的采集。
  2. 确定输入通道:
    • 外部的:即IO口;有内部的,如芯片内部的温度传感器。
    • 外部的16个通道对应着不同的IO口,分为规则通道注入通道,注入通道可以插队规则通道。
  3. 设置转换顺序:通过设置规则序列寄存器和注入序列寄存器设置两个通道分别的顺序。
  4. 设置触发源:
    • 写寄存器来控制开启转换和停止转换。
    • 设置为内部定时器触发,或外部IO触发。
  5. 设置转换时间:
    • ADC的时钟最高是14M,但是通过分频最高只能12M
    • 采样周期最小是1.5个周期,周期是1/ADC_CLK
    • ADC 的转换时间跟 ADC 的输入时钟和采样时间有关,公式为:Tconv = 采样时间 + 12.5 个周期。当 ADCLK = 14MHZ (最高),采样时间设置为 1.5 周期(最快),那么总的转换时间(最短)Tconv = 1.5 周期 + 12.5 周期 = 14 周期 = 1us。
  6. 数据寄存器:
    • ADC 转换后的数据根据转换组的不同,规则组的数据放在 规则数据寄存器寄存器,注入组的数据放在 注入数据寄存器
    • ADC 规则组数据寄存器 ADC_DR 只有一个,是一个 32 位的寄存器,低 16 位在单 ADC时使用,高 16 位是在 ADC1 中双模式下保存 ADC2 转换的规则数据,双模式就是 ADC1 和ADC2 同时使用。在单模式下,ADC1/2/3 都不使用高 16 位。因为 ADC 的精度是 12 位,无论 ADC_DR 的高 16 或者低 16 位都放不满,只能左对齐或者右对齐
    • 规则通道可以有 16 个这么多,可规则数据寄存器只有一个,如果使用多通道转换,很容易被下一个时间点的另外一个通道转换的数据覆盖掉,最常用的做法就是开启 DMA 传输
    • 注入数据寄存器有四个,对应通道4个。
  7. 中断:
    • 数据转换结束后,可以产生中断,中断分为三种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断。
    • DMA 请求:规则和注入通道转换结束后,除了产生中断外,还可以产生 DMA 请求
  8. 电压转换:
    • 12 位满量程对应的就是 3.3V,即12 位满量程对应的数字值是:2^12 = 4096。
    • 我们要求的是转换过后的数字值对应的电压值:求的电压值/数字值 = 3.3/4096,所以电压值 = (3.3/4096)* 数字值
ADC_InitTypeDef 结构体
typedef struct
{uint32_t ADC_Mode; // ADC 工作模式选择FunctionalState ADC_ScanConvMode; /* ADC 扫描(多通道)或者单次(单通道)模式选择 */FunctionalState ADC_ContinuousConvMode; // ADC 单次转换或者连续转换选择uint32_t ADC_ExternalTrigConv; // ADC 转换触发信号选择uint32_t ADC_DataAlign; // ADC 数据寄存器对齐格式uint8_t ADC_NbrOfChannel; // ADC 采集通道数
} ADC_InitTypeDef;
  • ADC_Mode:配置 ADC 的模式,当使用一个 ADC 时是独立模式,使用两个 ADC 时是双模式,在双模式下还有很多细分模式可选,我们一般使用一个 ADC 的独立模式。
  • ScanConvMode:可选参数为 ENABLE 和 DISABLE,配置是否使用扫描。如果是单通道 AD 转换使用 DISABLE,如果是多通道 AD 转换使用 ENABLE
  • ADC_ContinuousConvMode:可选参数为 ENABLE 和 DISABLE,配置是启动自动连续转换还是单次转换。使用 ENABLE 配置为使能自动连续转换;使用 DISABLE 配置为单次转换,转换一次后停止需要手动控制才重新启动转换。一般设置为连续转换。
  • ADC_ExternalTrigConv:外部触发选择,图 30-1 中列举了很多外部触发条件,可根据项目需求配置触发来源。实际上,我们一般使用软件自动触发
  • ADC_DataAlign:转换结果数据对齐模式,可选右对齐 ADC_DataAlign_Right 或者左对齐 ADC_DataAlign_Left。一般我们选择右对齐模式。
  • ADC_NbrOfChannel:AD 转换通道数目,根据实际设置即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/797070.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ubuntu下stlink烧录stm32代码

ubuntu下stlink烧录stm32代码ubuntu下stlink烧录stm32代码,记录备忘 0、环境一、下载stlink驱动 二、编译 三、 安装stlink驱动 四、验证安装成功 usb口接stlink后,查到设备五、 烧录 六、其它

高等数学 2.2 函数的求导法则

目录1、常数和基本初等函数的导数公式2、函数的和、差、积、商的求导法则3、反函数的求导法则4、复合函数的求导法则 1、常数和基本初等函数的导数公式公式 公式(1) \((C) = 0\) (2)\((x^{\mu}) = \mu x^{\mu - 1}\)(3)\((\sin x) = \cos x\) (4)\((\cos x) = - \sin x…

自尽氚气出题人+rui 之 氚荠甲苯二酸 代码

运输计划 显然我们可以处理出每个区间正方向和反方向走的代价,那么最后的问题可以转化为每个点选择 \(0/1\) 之一,要求区间的选择两两不冲突,在这个基础上最小化代价之和。 则,可以参考 \(2-SAT\) 的思路,处理出每个点选择 \(0/1\) 两两的限制状况,不难发现这种限制应该是…

十一,Spring Boot 当中配置拦截器的“两”种方式

十一,Spring Boot 当中配置拦截器的“两”种方式 @目录十一,Spring Boot 当中配置拦截器的“两”种方式1. 准备工作:2. Spring Boot当中配置拦截器的第一种方式:通过配置类的方式3. Spring Boot 当中配置拦截器的第二种方式:4. 补充:URI 和 URL 的区别5. 总结:6. 最后:…

PbootCMS常用公司信息标签调用

以下是 PbootCMS 常用公司信息标签的表格形式,方便查阅和使用:标签名 描述 示例代码{pboot:companyname} 公司名称 {pboot:companyname}{pboot:companyaddress} 公司地址 {pboot:companyaddress}{pboot:companypostcode} 邮政编码 {pboot:companypostcode}{pboot:companycont…

Electric Power

Power How Batteries Work电池提供给外面稳定的电压氧化反应,电压会逐渐减少,知道不能给设备供电。USB PD(Power Delivery) ref:https://www.usbzh.com/article/detail-479.html USB Types Type A, Type B vs Type CType C:reversible bi-directional power capabilities bet…

记忆力训练:解锁大脑潜能的钥匙

记忆力训练:解锁大脑潜能的钥匙 在快节奏的现代生活中,良好的记忆力成为了我们学习、工作乃至日常生活中不可或缺的能力。无论是背诵长篇课文、记忆复杂数据,还是快速回顾过往经历,强大的记忆力都能让我们事半功倍。然而,随着年龄的增长和生活压力的增加,许多人发现自己的…

PbootCMS做英文站面包屑“首页”怎么处理

在使用 PbootCMS 构建英文站点时,需要将面包屑中的“首页”文字改为英文“Home”。可以通过设置面包屑标签的参数来实现这一需求。 面包屑标签 标签格式:html{pboot:position}参数说明:separator=*:分隔符,默认为 >>。 separatoricon=*:分割图标,默认为空,如使用…

PbootCMS栏目页如何调用当前栏目的文章

要在栏目页调用当前栏目的文章,可以使用 PbootCMS 提供的 {pboot:list} 标签。以下是如何在栏目页调用当前栏目的文章的具体方法。 1. 栏目页调用当前栏目的文章 假设你需要在栏目页调用当前栏目的文章,可以使用以下代码:{pboot:list num=10 scode={sort:scode} page=0}<…

运行PbootCMS系统有哪些环境要求?

为了确保 PbootCMS 系统能够顺利安装和运行,以下列出了 PbootCMS 的基本运行环境要求: 1. PHP 版本要求最低要求:PHP 5.4+ 推荐版本:支持最新的 PHP 7.0、7.1、7.2 兼容性:由于 PbootCMS 支持 SQLite 和 MySQL 数据库,因此即使空间没有配置 MySQL,也可以使用 SQLite 方式…

PbootCMS配置留言发送到QQ邮箱教程

要在 PbootCMS 中配置留言发送到 QQ 邮箱,可以按照以下步骤进行操作: 1. 登陆 QQ 邮箱,找到设置 > 账户登录 QQ 邮箱:打开 QQ 邮箱。进入设置 > 账户:在 QQ 邮箱首页右上角点击“设置”,然后选择“账户”。2. 开启 SMTP 服务找到 SMTP 服务设置:在账户设置页面向下…