STM32入门笔记(7):STM32CubeMX配置DAC+DMA生成正弦波及任意波形
本次实验实现了DAC+DMA生成正弦波,实际上任意波形都可以通过这种方式生成,只要你有波形的解析式,可以通过傅里叶变换得到,大家可以自行探索
鸽王归来。。。
这个系列已经很久没更新了,最近一位粉丝朋友在STM32生成正弦波方面遇到点问题,于是整理了一下写了这篇文章。
老规矩,源码会放在Q群中
交流Q群:659512171
实验平台:STM32F407ZGT6(所有带DAC的MCU都类似)
实验环境:STM32CubeMX v6.13.0
Keil MDK5
目录
一,基础知识:
DAC:
DAC即Digital Analog Converter,数模转换器的缩写。顾名思义就是将数字信号转换为模拟信号的一个外设,其原理是将电平划分为许多小块,近似输出模拟信号。有如下几个重要参数
DAC位数:
DAC位数表示了能将电平划分为多少等级,如12位DAC能将电平划分为2^15块。DAC位数越高,相应的精度也就越高。
DAC转换速率:
DAC的转换速率表示的是1s内DAC能进行多少次转换,实际应用中要使DAC的转换速率尽可能的大于输出信号频率以保证输出信号的平滑性,否则就会出现下面的情况:

上图是用100kHz的转换速率转换10kHz的正弦波,将转换速率提升到1MHz后大致波形就正常了,当然,如果放大看波形依旧是一个个小台阶,这是DAC原理限制,解决方法是后续电路滤波或者使用更高速率和精度的DAC。
DMA:
DMA即Direct Memory Access,直接存储器访问的缩写。DMA允许数据在内存中直接传输,该功能通过DMA控制器实现,不需要CPU的介入,因此可以很大的节约CPU资源,在今天的实验中,由于生成波形需要的速率较高,直接使用CPU会造成很大的资源浪费,因此选用DAC+DMA的方式。
二,基础配置:
在STM32CubeMX中新建一个项目,选择你的MCU,进入配置界面

1,时钟,调试配置:
选择时钟源,配置调试


配置一下时钟树

这里考虑到大部分朋友用的是F1系列,主频我设置为了72MHz
2,外设配置:
DAC配置:

来讲一下这几个参数:
Output Buffer:输出缓冲,可以提升带载能力,不过在本次实验中用不到
Trigger:触发方式,即通过何种方式触发DAC的一次转换,这里配置的是TIM2计时器触发DAC转换,这样就可以通过计时器实现频率同步
Wave generation mode:波形生成模式,有三角波和噪声波两种使能模式,本次实验不需要,所以不使能
DMA配置:
在DAC配置界面的DMA Setting一栏,点击Add添加一个DMA,配置为循环模式

关于DMA循环模式和正常模式:
DMA的循环模式会在DMA传输完成后将地址重新移至初始地址,自动开启下一次传输,正常模式则会在一次传输结束后停止DMA传输
由于本次生成的是周期性的正弦波,所以选择循环模式
注意,DMA传输过程中不允许对传输区域内存的更改,所以想要改变波形需要先停止DMA传输。
定时器配置:
由于选择的DAC触发源是TIM2,所以对TIM2进行配置

由于后续会在Keil中编写定时器配置的函数,所以这里就没有配置PSC和ARR。如果你的项目已经确定好要用多少的触发频率,在这里直接配置好就可以了
下面来讲一下 Trigger Output Parameters中的两个参数:
MSM:主从模式,值得注意的是,这个参数并不是配置该定时器的主从模式的,而是配置TRGI事件延时以实现几个定时器之间的完美同步
Trigger Event Selection:触发事件选择,在这里选择了更新事件
3,项目配置:
进入Project Manager页面,设置项目属性


设置完成后点击右上角GENERATE CODE生成代码,在弹出的对话框中点击Open Project打开项目。
三,代码编写:
首先确定程序逻辑:
1,通过用户编写的函数生成一个具有一个周期内DAC值的采样数组
2,将该数组通过DMA送入DAC,开启TIM2触发DAC转换
可以发现逻辑框架还是很简单的
1,数组生成:
使用了 math.h 和 stdlib.h 两个头文件,记得先引用
使用C语言 math库中的sin函数生成采样数组,程序说实话有点难以描述,先上代码再逐行讲解

/* USER CODE BEGIN 0 */
#define PI 3.14159265358979
#define TIME_INTERVAL_US 10
#define TIM_FREQUENCY_MHZ 72
typedef struct SinWavePar {
int length;
uint16_t *DACbuf;
}SinWavePar;
double linearmapping(double as,double ae,double bs,double be,double val){
return(val*(be-bs)/(ae-as)+bs-as*(be-bs)/(ae-as));
}
void TIM_Config(void){
__HAL_TIM_SET_PRESCALER(&htim2,0);
__HAL_TIM_SET_AUTORELOAD(&htim2,(uint32_t)((TIME_INTERVAL_US)*(TIM_FREQUENCY_MHZ)-1));
}
SinWavePar GenerateSinWave(int f){
SinWavePar Par;
Par.length = 1000000/(TIME_INTERVAL_US)/f;
Par.DACbuf = malloc(sizeof(uint16_t)*Par.length);
for(int i=0;i<Par.length;i++){
Par.DACbuf[i]=(uint16_t)linearmapping(-1,1,0,0xFFF,sin(linearmapping(0,Par.length,0,2*PI,i)));
}
return Par;
}
/* USER CODE END 0 */
先简单讲一下不那么重要的几个
宏定义 PI:π的近似值
宏定义 TIME_INTERVAL_US:两次转换之间的时间间隔,单位是us
结构体 SinWavePar:正弦波的结构体,用于储存采样点数量,数组地址
linearmapping:线性映射,用于将时基映射到0~2π,其本质是求坐标系中过 (as,bs),(ae,be)两个点的直线解析式,然后将val代入求值
TIM_Config:定时器参数的配置,单独写出来是为了方便后续修改正弦波参数
宏定义 TIM_FREQUENCY_MHZ:定时器的频率,单位是MHz
这个频率通过数据手册和时钟树找到:

从数据手册可以看到TIM2挂在APB1总线下

再看一下时钟树,可以发现定时器的频率是72
如果没有使用STM32CubeMX,可以使用 RCC_GetClocksFreq(固件库开发)或者
HAL_RCC_GetSysClockFreq (HAL库开发)先获取APB1的频率,然后乘2就是TIM2的频率
下面逐行讲解 int GenerateSinWave 函数
SinWavePar Par;
该语句用于定义结构体
Par.length = 1000000/(TIME_INTERVAL_US)/f;
该语句用于计算一个周期内采样点数量,1,000,000代表1,000,000us,计算原理如下:
根据数学知识,周期 T=1/f,但由于单位要用us,所以就乘了 1,000,000,变成1,000,000/f,这样就得到了一个周期需要多少个us,然后除以转换的时间间隔,就得到了一个周期内采样点的数量
Attention:如果除不尽,请不要改 1,000,000这个数,因为这是与计时器对应的,只改动该值会导致频率跑偏,除不尽就改动 TIME_INTERVAL_US这个宏定义,只要确保TIME_INTERVAL_US*TIM_FREQUENCY_MHZ 为整数,这个宏定义是小数也无所谓(如果不是对精度特别敏感,两个宏定义积不是整数也没关系)。1000000/TIME_INTERVAL_US/f的值最好是整数,如果不是整数会导致周期不完整。当然,上述都是对精度有要求下的注意事项,对精度要求不高就不用管了
Par.DACbuf = malloc(sizeof(uint16_t)*Par.length);
该语句用于为DACbuf分配空间,注意这个是储存波形数据的,后面不要把数组回收了
for(int i=0;i<Par.length;i++){
Par.DACbuf[i]=(uint16_t)linearmapping(-1,1,0,0xFFF,sin(linearmapping(0,Par.length,0,2*PI,i)));
}
由于每个采样点之间的时间间隔固定,所以以一个周期的采样点为时基,映射到0~2π计算对应正弦值,同时要将计算出的值再次映射到0~0xFFF区间内。很明显这两次映射都是线性关系,所以用线性映射,这里不作出线性关系的证明
return Par;
该语句用于返回数据结构体
至此,恭喜你已经攻克了本次实验难点,下面就是调用HAL库函数输出了
2,输出正弦波:
在main中调用函数输出:

/* USER CODE BEGIN 2 */
SinWavePar Par = GenerateSinWave(1000);
TIM_Config();
HAL_TIM_Base_Start_IT(&htim2);
HAL_DAC_Start_DMA(&hdac,DAC1_CHANNEL_1,(uint32_t*)Par.DACbuf,Par.length,DAC_ALIGN_12B_R);
/* USER CODE END 2 */
在用户初始化区域调用一次就可以连续输出了
编译烧录后把DAC输出引脚连接到示波器上,看一下波形

可以看到正弦波形还是比较完美的
放大后的几幅图像:

还是很平滑的,说明现阶段的转换速度还够用
四,总结:
本次实验实现了DAC+DMA生成正弦波,实际上任意波形都可以通过这种方式生成,只要你有波形的解析式,可以通过傅里叶变换得到,大家可以自行探索
--------------------------------------------------------------完---------------------------------------------------------------
都看到这里了不给个关注吗?
完整程序会放在Q群
交流Q群:659512171
更多推荐



所有评论(0)