直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。 两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自 于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。
本次实验使用DMA功能搬运ADC数据(从外设到内存)和DAC数据(从内存到外设)
ADC需要配置为连续转换模式,因为是通过DMA搬运转换后的数据到存储器,ADC每转换一次,DMA搬运一次;之前实现ADC功能配置成了单次转换模式,是只完成一次转换,然后在程序中循环调用函数控制ADC再次转换
DMA模式设置为循环模式Circular,因为ADC是循环转换的,所以DMA也要设为循环模式,循环搬运ADC数据,如果设置为普通模式Normal,则DMA只会搬运一次
因为每次搬运的都是同一个ADC通道数据,到同一个内存地址,所以增加地址的选项都不用勾选;如果数据是放在一个数组里的,则内存的增加地址选项需要勾上
数据长度有字节Byte,半字Half Word,字Word,分别是8位,16位,32位,一般CubeMX软件会给出外设默认的数据类型,例如串口是Byte,ADC就是Half Word
使能地址递增的功能,DMA控制器在进行数据搬运的时候,会对搬运的目标地址或源地址进行递增操作,每次地址递增多少取决于搬运数据宽度。
如果勾选地址增加选项的话,则DMA每搬运1次数据,外设或者内存的地址就加1,下一次搬运来的数据就不是放在一开始设置的地址了,而是加1后的地址
1、如果外设地址和内存地址都不变,则DMA每次都从同一个外设地址搬运数据到同一块内存地址,内存里的数据会被覆盖
2、如果外设地址增加,内存地址不变,则DMA搬运一次外设地址的数据,外设地址就会加1,下次DMA搬运的就是加1后的地址数据
3、如果外设地址不变,内存地址增加,则DMA每次从同一个外设地址搬运数据,放到连续增加的内存地址中
4、如果外设地址和内存地址都增加,则相当于将连续的外设地址的数据复制到连续的内存地址中
使用DAC输出一个正弦波,使用DMA后,DMA就会自动将要输出的数据搬运到DAC输出寄存器,因为产生正弦波需要一定的周期,则这个周期就用定时器来设定,当定时器计数溢出时,就驱动DMA搬运内存数据到DAC,形成正弦波,整个过程不用CPU干预
开启DAC通道1,使能外部触发
因为要用定时器产生一个可控的周期,所以这里选择定时器5触发事件
数据方向是从内存到外设,也是循环模式,因为要将内存中的一组数据传输到DAC,所以内存要开启增加地址模式
因为在DAC实验中输出正弦波是将一个波形分成了32份,所以是32个采样点,要让每1个采样点的采样间隔是1/32ms,所以定时器每隔1/32ms溢出一次
所以定时器的时钟先设置为64MHz,因为64刚好整除32,如果用72MHz的话,会有点误差
既然规定了要定时1/32ms,定时器时钟设置为了64MHz,所以定时器计数一次是1/64us,所以计数值 = 1/32ms / 1/64us = 1000/32us / 1/64us = 2000
CubeMX配置中设置不分频,计数值为2000,在64MHz的时钟频率下,定时器计数2000次就等于1/32ms,使能自动重载,触发事件选择更新事件,这里定时器就配置好了
因为DMA中断是在DMA完成一次搬运后去通知CPU的,本次实验ADC的转换是很快速的,如果每次搬运完一个数据都去中断CPU,则CPU的工作效率就会减低,而DAC也是用定时器控制周期输出一个正弦波,也不需要中断CPU;所以把DMA的中断去掉
CubeMX生成的初始化代码中要确保DMA的初始化是在ADC和DAC初始化之前的,因为如果在ADC中使用了DMA的函数,但DMA的初始化是在ADC后面的,则DMA功能不起作用
本次生成的初始化代码DMA默认在ADC和DAC的前面
MyInit.c
初始化函数中打印调式信息,初始化数码管,然后启动ADC的DMA模式,将值放到NTC.usADC_Value中
启动DAC输出正弦波,延时一会,给DMA搬运的时间
/*
* @name Peripheral_Set
* @brief 外设设置
* @param None
* @retval None
*/
static void Peripheral_Set()
{
printf("----此程序用DMA功能完成ADC与DAC----\r\n");
printf("Initialization completed,system startup!\r\n");
printf("Software version is V%.1f\r\n\r\n",SoftWare_Version);
//初始化数码管
Display.TM1620_Init();
//启动AD转换,DMA模式
HAL_ADC_Start_DMA(&hadc1,(uint32_t*)&NTC.usADC_Value,(uint32_t)1);
//启动DAC,DMA模式输出正弦波
DAC_Apply.Output_Sine_Wave();
HAL_Delay(1); //延时
}
NTC.c
NTC.usADC_Value的值会由DMA自动搬运,所以该值是实时更新的,在函数中并不需要调用函数软件读取ADC转换值,由硬件DMA来完成这些工作
/*
* @name Get_NTC_Voltage
* @brief 获取NTC的电压
* @param None
* @retval None
*/
static void Get_NTC_Voltage()
{
//因为NTC.usADC_Value的值已经由DMA得到,所以这里只需要计数即可
NTC.fNTC_Voltage = (NTC.usADC_Value*3.3)/4095; //反向计算出输入ADC引脚的电压
//打印AD转换后的信息
printf("ADC转换的原始值 = %d\r\n",NTC.usADC_Value);
printf("计算得出的电压值 = %.2fV\r\n",NTC.fNTC_Voltage);
}
DAC_Apply.c
在输出正弦波函数中启动定时器5,也以DMA模式启动DAC功能,DMA自动将CH_value数组的值搬运到DAC的DAC_DHRx寄存器中,
因为在CubeMX中配置的是内存地址增加,所以DMA会自动将数组的值全部搬运出去
//分成32份的正弦波的值
const uint16_t CH_value[32] =
{
2448,2832,3186,3496,3751,3940,4057,4095,4057,3940,
3751,3496,3186,2832,2448,2048,1648,1264,910,600,345,
156,39,0,39,156,345,600,910,1264,1648,2048
};
/*
* @name Output_Sine_Wave
* @brief 输出正弦波
* @param None
* @retval None
*/
static void Output_Sine_Wave()
{
//启动定时器5
HAL_TIM_Base_Start(&htim5);
//启动DAC,DMA模式
HAL_DAC_Start_DMA(&hdac,DAC_CHANNEL_1,(uint32_t*)CH_value,1,DAC_ALIGN_12B_R);
}
CallBack.c
外部中断回调函数中通过按键改变定时器5的重装载值,从而改变DMA的触发周期,那正弦波的频率也会由于搬运频率的改变而改变
/*
* @name HAL_GPIO_EXTI_Callback
* @brief 外部中断回调函数
* @param GPIO_Pin:触发外部中断的引脚
* @retval None
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY1_Pin)
{
LED.LED_Fun(LED1,LED_Flip);
//调整正弦波周期
switch (TIM5->ARR)
{
case 2: TIM5->ARR = 20000; printf("正弦波周期调整为100Hz\r\n");break;
case 20000: TIM5->ARR = 2000; printf("正弦波周期调整为1KHz\r\n");break;
case 2000: TIM5->ARR = 200; printf("正弦波周期调整为10KHz\r\n");break;
case 200: TIM5->ARR = 20; printf("正弦波周期调整为100KHz\r\n");break;
case 20: TIM5->ARR = 2; printf("正弦波周期调整为1MHz\r\n");break;
default: TIM5->ARR = 2000; printf("重装载寄存器错误,正弦波频率校准为1KHz\r\n");break;
}
}
}