目录
[1. 修改DMA模式为Circular](#1. 修改DMA模式为Circular)
采用DMA(Direct Memory Access,直接存储器访问)控制器实现A/D采样。采用这种方式时,一旦配置好ADC参数及所使用的DMA通道,DMA控制器就会自动将A/D转换结果送至指定的存储器空间中(数组)。在使用A/D转换数据时,只需要在主程序中读取相应的数组变量就可以了,无需再调用HAL_ADC_GetValue()等函数来获取A/D转换结果。
采用DMA的方式可以不占用CPU的资源,直接由DMA控制器来实现外设(或存储器与存储器之间的数据交互。所以,这种方式在实际中是比较实用的,并且可以极大地提高CPU的工作效率。
一、建立工程
本文项目以来的硬件工程及配置参考本文作者的下述文章,工程配置基本一致。本文只描述不一样的地方。细说MCU用定时器控制ADC采样频率的实现方法并通过Simulink查看串口输出波形-CSDN博客 https://wenchm.blog.csdn.net/article/details/140523545https://wenchm.blog.csdn.net/article/details/140523545
1.相同的配置
- 配置串口;
- 配置TIM3,TIM4;
- 选择时钟源和Debug模式,配置系统时钟和ADC时钟;
- 配置GPIO,LED;
2.配置ADC
在硬件配置界面中打开Analog→ADC1,在其模式Mode区中,通道1(IN1)选择IN1 Single-ended;在下面的配置(Configuration)区中,需要对几个参数进行调整:
首先,在ADC设置(ADC_Settings)参数栏,依然可以不对ADC的时钟进行分频,还将预分频参数(Clock Prescaler)选择为Asynchronous clock mode divided by 1。本例中用定时器实现对采样频率的控制。
随后,依然将ADC设置(ADC_Settings)参数栏中连续转换模式(Continuous Conversion Mode)设置为Disabled,由于要用DMA,所以需要使能DMA连续请求(DAM Continuous Requests)参数。
3.配置DMA
打开DMA设置(DMA Settings)选项卡,先添加一个ADC1的DMA请求。DMA有多个可选通道,这里随便选择一个即可(共有两个DMA,每个都有8个通道)。此外,优先级有四级,从低(Low)到很高(Very High),可以先保持默认值Low。
DMA请求设置(DMA Request Settings)栏,可以设置DMA的模式;模式有两种:常规(Normal)和循环(Circular)。如果是Normal模式,仅会执行一次DMA,若要继续执行,则要重新启动。在Circular模式下,可以连续执行DMA。此例中,先将DMA模式设置为Normal。此外,在增量地址(Increment Address)中,勾选上存储器(Memory),这样就可以将数据顺次存储到一个数组中。因为A/D的转换结果需要一个16位的数,所以将数据宽度(Data Width)设置为半字(Half Word),一个字为32位。
ADC1的DMA请求设置完毕后,设置DMA连续请求(DAM Continuous Requests)参数为Enabled。
在ADC规则转换模式(ADC_Regular_ConversionMode)栏,还是将外部触发转换源(External Trigger Conversion Source)选择为Timer 3 Trigger Out event。在ADC规则转换模式参数栏中,将Rank下的采样时间选择为2.5个周期。
由于是使用DMA来实现将A/D采样结果传递到存储器(数组)的,所以无需配置ADC的中断。不过,因为配置了ADC的DMA功能,所以会用到DMA的中断。由于上面配置的是DMA1的通道1,所以会自动开启DMA1的通道1中断。打开ADC配置界面中的NVIC设置(NVIC Settings),可以看到DMA1 channel 1 global interrupt已经自动被使能了,并且不能取消。另外一个ADC1的中断(ADC1 and AD2 global interrupt)由于用不到,所以无需开启。
二、代码修改
1.定义存储ADC采样结果的数组
首先定义存储ADC采样结果的数组,本例中用数组变量ADC1ConvertedData。将存储ADC采样结果的数组定义为全局变量,同时定义一个后面会用到的变量ADCDMAFlag,将它们一并放到主程序中的注释对中:
cpp
/* USER CODE BEGIN PV */
uint16_t ADC1ConvertedData[ADC_CONVERTED_DATA_BUFFER_SIZE] = {0};
uint8_t ADCDMAFlag = 0;
/* USER CODE END PV */
其中,数组长度ADC_CONVERTED_DATA_BUFFER_SIZE可以定义到main.h中:
cpp
/* USER CODE BEGIN Private defines */
#define ADC_CONVERTED_DATA_BUFFER_SIZE (uint16_t) 60
/* USER CODE END Private defines */
2.启动ADC与定时器
本例中无需开启ADC1的中断。不过,要在主函数的初始化代码中调用ADC校验函数HAL_ADCEx_Calibration_Start,启动DMA方式的ADC转换(通过调用HAL_ADC_Start_DMA函数),并开启TIM3(通过调用HAL_TIM_Base_Start)。将上述三个函数的调用放到while(1)之前、MX_ADC1_Init()之后的注释对中:
cpp
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1,ADC_SINGLE_ENDED);
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC1ConvertedData,ADC_CONVERTED_DATA_BUFFER_SIZE);
HAL_TIM_Base_Start(&htim3);
HAL_TIM_Base_Start_IT(&htim4);
HAL_TIM_PWM_Start(&htim4,TIM_CHANNEL_1);
/* USER CODE END 2 */
ADC的采样是由TIM3控制的,采样值存入存储器(数组)的过程是通过DMA完成的,即ADC采样值在DMA控制器的控制下直接传送到数组ADC1ConvertedData中。虽然没有开启ADC1的中断,但在DMA完成设定长度的ADC采样数据传递后,也会调用一次回调函数HAL_ADC_ConvCpltCallback()。这里所谓的"设定长度",就是函数HAL_ADC_Start_DMA()中的第三个参数。该参数在前面的代码中被设定为60。
TIM4用来产生信号源。
3.编写主程序代码
如果要通过串口送出采样值数据,可以在本次DMA传送完毕后进行。如果DMA还在更新时就进行串口数据发送,可能会出现数据不连续的情况。所以,可以在回调函数HAL_ADC_ConvCpltCallback()中将一个标志变量置位(可使用前面定义的变量ADCDMAFlag),置位就表示DMA传送完毕。然后,在while(1)循环中,以此标志位为条件,实现一段完整的采样值数据发送。串口数据发送,可以通过在主程序中调用串口发送函数来实现。
cpp
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if(ADCDMAFlag == 1)
{
ADCDMAFlag = 0;
HAL_ADC_Stop_DMA(&hadc1);
HAL_UART_Transmit(&huart2,(uint8_t*)&ADC1ConvertedData,ADC_CONVERTED_DATA_BUFFER_SIZE*2,0xFFFF);
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC1ConvertedData,ADC_CONVERTED_DATA_BUFFER_SIZE);
HAL_Delay(1000);
}
}
/* USER CODE END 3 */
上面这段代码中有两个是控制DMA的函数,有一个是串口发送数据的函数。第一个函数是让DMA停止工作,暂停数据搬运,然后用函数HAL_UART_Transmit发送A/D采样数据。注意,在HAL_UART_Transmit的参数中,设置发送数据的长度为ADC采样数据的2倍,这是因为串口每次只能发送1个字节的数据,而一个A/D采样值会占用2个字节。数据发送完毕后,再重新启动ADC的DMA传输。
4.重定义回调函数
此外,在main.c中重新定义回调函数HAL_ADC_ConvCpltCallback():
cpp
/* USER CODE BEGIN 4 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* AdcHandle)
{
ADCDMAFlag =1;
}
//信号源
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
}
/* USER CODE END 4 */
5.查看结果
施加信号源(可以是PA5)到ADC输入端PA0上,打开串口接收的Simulink模型,即可看到通过串口送来的信号波形。
三、修改DMA模式
1. 修改DMA模式为Circular
上面例子中,主程序每间隔1000 ms发送一组数据;每次发送前要关闭DMA,发送后再重启。这种方式送来的两组数据其实并非连续的数据。那么,如何让串口实时向外连续发送A/D采样的数据呢?
在前面配置ADC1的DMA、设置ADC1的DMA请求的模式时,选择的是Normal。如果选择Circular,DMA就会持续传送ADC采样数据到数组中,不过会循环覆盖;如果能够在下次DMA数据传递完成前将数据发送出去,就不会有影响。假如还是设置ADC采集缓冲区长度为60,则DMA一次会传送60个采样值数据;因为采样频率为1 kHz,所以完成这些数据的采样需要60 ms的时间。加上DMA的处理时间,DMA完成这些数据的传递至少需要60 ms。这60个ADC采样值,占120个字节。串口发送1个字节的数,至少要发送10个二进制位(8个数据位、1个停止位和1个起始位),所以发送120个字节的数据,对应的二进制位数为1200,而设置的串口波特率为115200 bit/s发送1200位需要的时间为(1200/115200)s,约为10.4 ms。这个时间小于DMA搬运一次数据所需的60 ms。所以完全可以实现通过串口的数据实时发送。
修改while(1)循环中的代码如下:
cpp
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
//DMA标准模式
/*if(ADCDMAFlag == 1)
{
ADCDMAFlag = 0;
HAL_ADC_Stop_DMA(&hadc1);
HAL_UART_Transmit(&huart2,(uint8_t*)&ADC1ConvertedData,ADC_CONVERTED_DATA_BUFFER_SIZE*2,0xFFFF);
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&ADC1ConvertedData,ADC_CONVERTED_DATA_BUFFER_SIZE);
HAL_Delay(1000);
} */
//DMA循环模式
if(ADCDMAFlag ==1)
{
ADCDMAFlag =0;
HAL_UART_Transmit(&huart2,(uint8_t *)&ADC1ConvertedData,ADC_CONVERTED_DATA_BUFFER_SIZE*2,0xFFFF);
}
}
/* USER CODE END 3 */
2.查看结果
DMA模式修改为循环后,串口的工作效率更高,不丢数据,达到了实时传递数据的效果。