ADC_DMA_双buffer传输
线程A
- 切换buffer地址
- 开启ADC转换,并使用DMA传输
- 等待获取DMA中断的信号量,获取到信号量,表示上一次DMA传输已完成
- 将地址通过消息队列传输给线程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
- 通过消息队列接受到传来的buffer的地址
- 通过地址读取数据并进行转换
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的步骤
- 电压输入范围:ADC的本质是对电压信号的采集。
- 确定输入通道:
- 有外部的:即IO口;有内部的,如芯片内部的温度传感器。
- 外部的16个通道对应着不同的IO口,分为规则通道和注入通道,注入通道可以插队规则通道。
- 设置转换顺序:通过设置规则序列寄存器和注入序列寄存器设置两个通道分别的顺序。
- 设置触发源:
- 写寄存器来控制开启转换和停止转换。
- 设置为内部定时器触发,或外部IO触发。
- 设置转换时间:
- ADC的时钟最高是14M,但是通过分频最高只能12M
- 采样周期最小是1.5个周期,周期是1/ADC_CLK
- ADC 的转换时间跟 ADC 的输入时钟和采样时间有关,公式为:Tconv = 采样时间 + 12.5 个周期。当 ADCLK = 14MHZ (最高),采样时间设置为 1.5 周期(最快),那么总的转换时间(最短)Tconv = 1.5 周期 + 12.5 周期 = 14 周期 = 1us。
- 数据寄存器:
- 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个。
- 中断:
- 数据转换结束后,可以产生中断,中断分为三种:规则通道转换结束中断,注入转换通道转换结束中断,模拟看门狗中断。
- DMA 请求:规则和注入通道转换结束后,除了产生中断外,还可以产生 DMA 请求
- 电压转换:
- 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 转换通道数目,根据实际设置即可。