首页 > 其他分享 >ADC_DMA_双buffer传输

ADC_DMA_双buffer传输

时间:2024-09-14 16:02:06浏览次数:10  
标签:DMA 转换 buffer 线程 ADC uint32 adc

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) + 25
    log_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)); //开启DMA
	while(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 转换通道数目,根据实际设置即可。

标签:DMA,转换,buffer,线程,ADC,uint32,adc
From: https://www.cnblogs.com/huajchen/p/18414214

相关文章

  • 实战中学习:CMA和DMA_CMA导致安装大游戏失败的问题
     在实际遇到的问题中,遇到Android虚机中安装超过1.8G以上的大游戏APK文件时,就会出现安装失败的现象,通过分析,发现在/data/目录下进行大文件拷贝时,就会出现数据不一致,发现时内核打开了CMA和DMA_CMA,解决方法目前主要回退,将CMA和DMA_CMA的配置关闭现象: 拷贝5次:数据不一致/data/app......
  • CString类的ReleaseBuffer的用处是什么
    用了好多年了,才发现这个问题,写个小程序测试了,终于搞明白了:CStringstr; str=_T("1234567890");TCHAR*p=str.GetBuffer(100); //TCHAR*p=str.GetBufferSetLength(100);_tcscpy(p,_T("12345678111111190"));str.ReleaseBuffer(); //测试str+="aaa"; m_......
  • ADC入门准备(六):信号与系统知识回顾
    目录3.2周期信号的傅里叶级数分析3.2.1 傅里叶级数的三角形式狄里赫利(Dirichlet)条件方波的傅里叶级数展开 三角波的傅里叶级数展开锯齿波的傅里叶级数展开3.2.2 傅里叶有限级数与最小方均误差正余弦积分公式奇谐函数与偶谐函数吉伯斯现象3.2.3傅里叶级数的......
  • 5G Multicast/Broadcast Services(MBS) (三)Broadcast
    这篇是Broadcast的overview,正文开始。值得注意的是,对于5MBSbroadcast,UE处于RRCidle/RRC connected/Inactive时,网络侧都可以通过MRB将MBS广播数据传输到UE。对于Broadcast涉及的RNTI有G-RNTI以及MCCH-RNTI。1SessionManagement对于特定服务,将执行以下阶段:(1)MBS......
  • DMA直接存储器存取
    DMA直接存储器存取DMA简介存储器映像DMA框图DMA基本结构DMA硬件请求通道数据宽度与对齐DMA相关库函数DMA_InitType函数案例DMA转运存储器数据用到的函数接线图示例代码DMA与ADC多通道的扫描模式配合接线图示例代码DMA简介DMA(DirectMemoryAccess)直接存储器存取......
  • FrameBuffer
    一、基本概念        FrameBuffer:可以译作"帧缓冲、帧缓存",有时简称为fbdrv。这是一种独立于硬件的抽象图形设备。是Linux为显示设备提供的一个接口,把显存抽象后的一种设备,允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作控制fb。        对于......
  • startFromBuffer
    1.Buffer类的底层实现以IntBuffer和HeapIntBuffer为例讲解Buffer的实现机制核心内容publicabstractclassBuffer{//这四个变量的关系:mark<=position<=limit<=capacity //这些变量就是Buffer操作的核心了,之后我们学习的过程中可以看源码是如何操作这些......
  • 零基础国产GD32单片机编程入门(十六)DMA详解及ADC-DMA方式采集含源码
    文章目录一.概要二.GD32F103C8T6单片机DMA外设特点三.GD32单片机DMA内部结构图四.DMA各通道请求五.GD32F103C8T6单片机ADC-DMA采集例程六.工程源代码下载七.小结一.概要基本概念:DMA是DirectMemoryAccess的首字母缩写,是一种完全由硬件执行数据交换的工作方式。DM......
  • framebuffer帧缓存
    1.framebufferFramebuffer(帧缓冲区)是用于存储图像数据的一块内存区域。我们可以将我们想要显示的图像数据写到framebuffer中,驱动程序每隔一段时间会自动的去读取Framebuffer中的图像数据,并根据读取到的图像数据在屏幕上显示对应的图像。2.颜色的表示 我们知道一幅图像其......
  • 帧缓冲 framebuffer
    一、基本概念framebuffer:帧缓存、帧缓存(显示设备)Linux内核为显示提供的一套应用程序接口。(驱动内核支持)分辨率:像素点显示屏:800*600(横向有800个像素点,纵向有600个像素点)显卡(显存(保存像素点的值))RGB888(8个bitR,8个bitG,8个bitB)PC,4412(RGB888)RGB565(S3C2440)......