• 注册 / 登录
  • 切换到窄版
  • 查看: 2044|回复: 0

    用STM32内置DAC制作一个波形发生器

    [复制链接]

    665

    主题

    679

    帖子

    6481

    积分

    版主

    Rank: 7Rank: 7Rank: 7

    积分
    6481
    发表于 2022-9-4 14:58:12 | 显示全部楼层 |阅读模式

    路线栈欢迎您!

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    一、相关内容简介

    1.DAC
    DAC指数模转换器,指的是将数字量转为模拟量的一类元件。以此项目中的DAC为例,通过向DAC的寄存器写入0 ~ 4095之间的一个值,就能输出0 ~ 3.3V的一个电压。

    2.STM32的内置DAC
    此次使用的STM32F103ZET6芯片自带一个12位数字输入,电压输出的数模转换器。这个DAC模块具有两个支持独立转换的通道,还可以配置成两个通道同时转换。DAC可以配置为12位(4096档)或者8位(256档)。

    3.定时器
    定时器的功能挺多的,这里主要是利用定时器在每个计数周期计数器溢出产生中断/触发输出来达到定时输出波形的目的

    4.DMA
    DMA的中文名是直接存储器访问,相比于通过CPU来控制传输数据,DMA的速度更快,并且可以节省CPU资源。

    二、用定时器中断+DAC实现

    这是最容易想到的办法。因为波形可以看作是电压关于时间的函数,而涉及到在指定时刻(指定周期内)进行操作时,很容易就会想到用定时器,所以只需要在定时器的中断函数当中计算此时的电压值并写入DAC寄存器,就能达到输出波形的目的。

    具体实现思路:用一个全局变量mode来存储当前需要输出的波形类型,主函数中用while循环扫描板载按键是否被按下,从而对应改变mode的值。而定时器中断函数中根据当前mode的值进行对应波形的计算,将计算出来的值写入DAC寄存器。

    1.配置DAC
    查开发手册得知DAC的两个通道分别对应PA4和PA5,在作为DAC输出使用时需要先将端口配置为模拟输入模式以避免干扰。

    1.png

    这里直接用了开发板例程的代码,以使用DAC通道1,PA4作为模拟输出为例,代码如下:
    1. void Dac1_Init()

    2. {

    3.     GPIO_InitTypeDef GPIO_InitStructure;

    4.     DAC_InitTypeDef DAC_InitType;



    5.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA时钟使能

    6.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // DAC时钟使能



    7.     DAC_DeInit(); // 初始化

    8.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;

    9.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 配置为模拟输入

    10.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    11.     GPIO_Init(GPIOA, &GPIO_InitStructure);

    12.     GPIO_SetBits(GPIOA, GPIO_Pin_4);



    13.     DAC_StructInit(&DAC_InitType); // 初始化

    14.     DAC_InitType.DAC_Trigger = DAC_Trigger_None; // 不使用触发功能,对应寄存器中TEN1=0,TSEL1[2:0] = 000

    15.     DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None; // 不使用自带的波形生成功能

    16.     DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; // 通过线性反馈移位寄存器生成伪噪声,仅当DAC_WaveGeneration配置为DAC_WaveGeneration_Noise时有效

    17.     DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Disable; // 禁用输出缓存

    18.     DAC_Init(DAC_Channel_1, &DAC_InitType); // 初始化DAC



    19.     DAC_Cmd(DAC_Channel_1, ENABLE); // 使能DAC



    20.     DAC_SetChannel1Data(DAC_Align_12b_R, 0); // 输出0V

    21. }
    复制代码

    以通道1为例,查阅开发手册可得以8位右对齐,12位左对齐,12位右对齐三种模式操作的时候其实是在写入DHR8R1,DHR12L1,DHR12R1三个不同位置的寄存器,这些值经过自动移位写入内部的DHR1寄存器,之后被转存至DOR1寄存器。从DHRx转存到DORx所需要的时钟周期取决于触发功能的配置,详见数据手册。

    2.配置定时器

    2.1.初始化;因为这里只是使用定时器的中断,所以随便用哪个定时器都可以。

    以配置TIM3为例,代码如下:
    1. void TIM_Config()

    2. {

    3.     u16 arr = 1; // 自动重装载寄存器(Auto Reload Register)

    4.     u16 psc = 0; // 预分频器(Prescaler)

    5.     TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

    6.     NVIC_InitTypeDef NVIC_InitStructure;



    7.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3时钟使能



    8.     TIM_DeInit(TIM3); // 初始化

    9.         TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); // 初始化

    10.     TIM_TimeBaseStructure.TIM_Period = arr;

    11.     TIM_TimeBaseStructure.TIM_Prescaler = psc;

    12.     TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 似乎是在数字滤波器当中才会用到,平时一般设为0(TIM_CKD_DIV1)

    13.     TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式

    14.     TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);



    15.     TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 允许更新中断



    16.     NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;

    17.     NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

    18.     NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;

    19.     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

    20.     NVIC_Init(&NVIC_InitStructure);

    21. }
    复制代码

    因为这里要用的是中断,所以中间调用了TIM_ITConfig()。下一个方案中将用触发输出TIM_SelectOutputTrigger()来代替中断。

    影响定时器时钟周期的是TIM_Period,TIM_Prescaler两个量。分别对应TIMx_ARR,TIMx_PSC两个寄存器。
    TIMx_ARR:自动重装寄存器。在向上计数模式中,计数器从0数到arr后产生溢出事件(可用于触发中断/DMA);在向下计数模式中,计数器从arr数到0后产生溢出事件;在中央对齐模式中,计数器从0数到arr-1产生溢出事件,再从arr数到1产生溢出事件。
    TIMx_PSC:预分频器,通过一个16位的计数器来达到分频的效果。要注意的是写入寄存器的值+1才是这个计数器的模值,例如当设定psc=0时,计数器的模值为1,处于1分频(不分频)的状态。

    2.png

    以两个中断的间隔时常作为时钟周期(Tout),则有以下公式:
    Tout=(arr+1) * (psc+1)/Fclk
    其中Fclk为提供给定时器的时钟频率,本项目中为72MHz
    查手册可得当arr为0时定时器不工作,所以arr的最小值为1。在这种情况下,所能够配置的最短时钟周期为(1+1) * (0+1)/72MHz=2 * 1/72MHz=27.77μs
    因为涉及到中断,所以后面配置了一下NVIC中断控制器。这一方案中只涉及到定时器这一个中断,所以不用考虑各种优先级,随便配置下就好。

    2.2.中断函数
    之后是写TIM3的中断函数,当中用到了一些全局变量:
    mode:当前波形类型
    counter:用于计数的变量,在每个时钟周期后+1
    mod:最大计数值。这个值会影响生成波形的最高频率和波形的精度
    sin_wave[]:一个预先计算好的数组,里面包含了一个周期的正弦波对应的DAC寄存器值
    1. void TIM3_IRQHandler() // 中断函数是靠函数签名来确认的,因此不能改变这里的函数名,参数和返回类型

    2. {

    3.     u16 vals = 0;

    4.     if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)

    5.     {

    6.         TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 清零中断标志位

    7.         if (mode == 0) // 生成方波

    8.         {

    9.             vals = counter * 4000; // 此时mod为2。则counter将会在0,1之间不断循环,从而达到生成方波的目的

    10.         }

    11.         else if (mode == 1) // 生成锯齿波,

    12.         {

    13.             vals = ((float)counter / mod) * 4000; // 此时mod由我们自己定义。counter从0加到mod-1再回到0并循环

    14.         }

    15.         else if (mode == 2) // 生成三角波

    16.         {

    17.             //vals = (u16)(sin(counter / (double)mod) * 2000 + 2000); //这是实时计算寄存器值的代码,会占用较多的CPU时间导致无法生成相对高频的波形,所以这里采用先一次性计算完成,后逐个读取,以空间换时间的方法。

    18.             vals = sin_wave[counter]; // 此时mod刚好等于sin_wave[]数组的长度。

    19.         }

    20.         DAC_SetChannel1Data(DAC_Align_12b_R, vals);

    21.         counter++;

    22.         counter %= mod;

    23.     }

    24. }
    复制代码

    这里的mod值需要在主函数中修改mode值的时候对应修改。
    当mode=0(方波)时,mod应设为2(使counter在0,1之间循环)
    当mode=1(锯齿波)时,mod应按需求设置(mod越大波形的小锯齿越不明显,但是波形的单个周期会变长,导致能达到的最高频率变小)
    当mode=2(正弦波)时,mod不仅要按需设置,还要保证mod与sin_wave[]数组的长度一致(mod大了数组会越界,mod小了正弦波的单个周期不完整)

    附上一段简单的生成sin_wave[]数组的python代码,改一下ARRAY_LEN运行就好了
    1. from math import *

    2. ARRAY_LEN = 300

    3. output = ""

    4. output += "u16 sin_wave[" + str(ARRAY_LEN) + "] = {"

    5. for i in range(ARRAY_LEN):

    6.     # 这里的+2000是为了保证能够输出sin函数为负值的部分

    7.     val = sin(i / float(ARRAY_LEN) * 2 * pi) * 2000 + 2000

    8.     output += str(int(val))

    9.     output += ", "

    10. output = output[:-2]

    11. output += "};"

    12. print(output)
    复制代码

    要注意的是,这里使用的是u16类型的变量而不是int类型,否则会出现正负转换的问题

    3.主函数

    主函数主要负责初始化各个模块并循环读取按键,当中用到的delay.h, key.h和sys.h都是随开发板提供的库,分别实现了延时,按键扫描和位带操作的功能,这些库在网上很容易找到

    主函数代码如下
    1. #include "delay.h"
    2. #include "key.h"
    3. #include "sys.h"

    4. u8 mode = 0;
    5. u16 mod = 2;
    6. u16 counter = 0;
    7. u16 sin_wave[300]; // 具体数据由之前的脚本生成

    8. int main(void)

    9. {

    10.     u8 key;

    11.     delay_init();

    12.     NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);

    13.     TIM_Config();

    14.     Dac1_Init();

    15.     KEY_Init();

    16.     TIM_Cmd(TIM3, ENABLE);

    17.     while (1)

    18.     {

    19.         key = KEY_Scan(0);

    20.         if (key == KEY0_PRES) // 改变波形种类

    21.         {

    22.             mode++;

    23.             mode %= 3;

    24.             if (mode == 0)

    25.                 mod = 2; // 方波

    26.             else

    27.                 mod = 300; // 正弦波和锯齿波

    28.         }

    29.         else if (key == KEY1_PRES) // 频率变为原来的0.5倍

    30.         {

    31.             TIM3->ARR = (TIM3->ARR + 1) * 2 - 1;

    32.         }

    33.         else if (key == WKUP_PRES) // 频率变为原来的2倍

    34.         {

    35.             TIM3->ARR = (TIM3->ARR + 1) / 2 - 1;

    36.         }

    37.     }

    38. }
    复制代码

    上述代码稍微拼凑调整一下即可得到一个最简单的波形生成器。
    然而,上述方法无法产生较高频率的波形。经测试,这段代码可以生成频率在10KHz左右,形状还算过得去的方波,但是由于设计的正弦波和锯齿波每个周期内就有300个点,因此这两种波形的频率最高只能达到1KHz左右

    3.jpg

    4.jpg

    5.jpg

    虽然可以通过减少点数的方法来增加频率,但这是以损失波形精度为代价的。设想每个周期只含有5个点的电压,那么频率可以轻松上10KHz,但是得到的波形将会是一堆锯齿或者折线。

    因此,我们需要找到一个本质上输出速度更快的方案。

    三、用定时器+DMA+DAC实现

    上一个方案中最耗时的部分是输出锯齿波时对于电压值的计算,因为实时运算会占用较多的CPU时间(尤其是对于正弦函数这种更为复杂的运算),而且整个计算的结果其实是可以复用的,所以在生成正弦波的时候,我们使用了预先写入数组中的值,这样每次输出一个电压时只涉及到读数组的操作。类似地,我们可以在切换波形的时候算出一个周期内各个点对应的寄存器值并存入数组中,之后每次中断读取数组中的值并写入寄存器,这样可以减少重复运算量,缩短输出一个电压的时间。

    然而,中断是由CPU执行的,虽然定时器产生中断的时钟周期很短,但是从发生中断到调用中断的整个过程中还是会占用若干个个时钟周期。而DMA可以不通过CPU直接在内存和外设之间交换数据,其耗时相比处理中断会更短。

    使用DMA与使用中断函数不同,单个DMA只能够处理外设到内存、内存到外设和内存到内存之间的数据转移,无法进行其它的操作。然而,通过配置DMA,我们可以使得DMA每次搬运数据之后源地址(或目标地址)增加8bit,16bit或32bit。正是通过每次搬运之后的地址偏移,我们能够将整个数组中的内容依次写入到DAC寄存器当中。

    具体来说,每次写DAC寄存器的过程都是将一个长度为16bit,低12位有效,高4位置零的无符号整数写入到DAC->DHR12R1寄存器当中,因此我们可以新建一个u16型的数组存放一个周期内的数据,并配置DMA每次传输后目标地址不变,源地址增加16bit,传输完成后源地址回到初始位置并循环,就能达到输出波形的目的了。

    具体实现思路:用一个全局变量mode来存储当前需要输出的波形类型,再用一个u16类型的全局数组data存储波形一个周期内对应的寄存器值。主函数中用while循环扫描板载按键是否被按下,从而对应改变mode的值,并根据此时的mode计算data中的值,而定时器不再产生中断,而是产生DMA请求,由DMA来完成将data中的数据写入DAC寄存器的任务。

    1.配置DAC

    代码如下:
    1. void Dac1_Init()

    2. {

    3.     GPIO_InitTypeDef GPIO_InitStructure;

    4.     DAC_InitTypeDef DAC_InitType;



    5.     RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA时钟使能

    6.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // DAC时钟使能



    7.     DAC_DeInit(); // 初始化

    8.     GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;

    9.     GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 配置为模拟输入

    10.     GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;

    11.     GPIO_Init(GPIOA, &GPIO_InitStructure);

    12.     GPIO_SetBits(GPIOA, GPIO_Pin_4);



    13.     DAC_StructInit(&DAC_InitType); // 初始化

    14.     DAC_InitType.DAC_Trigger = DAC_Trigger_T2_TRGO; // **与上一方案不同**

    15.     DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None; // 不使用自带的波形生成功能

    16.     DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; // 通过线性反馈移位寄存器生成伪噪声,仅当DAC_WaveGeneration配置为DAC_WaveGeneration_Noise时有效

    17.     DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Disable; // 禁用输出缓存

    18.     DAC_Init(DAC_Channel_1, &DAC_InitType); // 初始化DAC



    19.     DAC_Cmd(DAC_Channel_1, ENABLE); // 使能DAC

    20.     DAC_DMACmd(DAC_Channel_1, ENABLE); // **与上一方案不同**

    21. }
    复制代码

    这段代码与方案一有两处不同,第一处是DAC_Trigger由DAC_Trigger_None改成了DAC_Trigger_T2_TRGO,第二处是DAC_SetChannel1Data()被改成了DAC_DMACmd()
    方案一中的DAC与定时器的关联度较弱,因为定时器中断当中除了写DAC寄存器也可以做其它的事情,而写DAC寄存器的操作也可以在任何一个函数当中进行。然而,在此方案中,定时器、DAC与DMA之间有着紧密的联系。定时器在每个时钟周期后产生触发输出,该触发输出引起DAC根据寄存器内的值产生电压,同时DAC产生DMA请求使其将新的数据写入DAC的寄存器中。这种情况下,写入DAC寄存器的值并不会立即生效,而是在定时器的触发输出触发DAC后电压值才会更新。

    因此,由于DAC是接受到定时器2(不是上一方案的定时器3,原因之后会讲)的触发输出后才会更新,所以Trigger被设为了T2_TRGO(后面配置定时器的时候也会进行相应的配置),而DMA请求是由DAC产生的,并且此时直接设置DAC寄存器的值并不会改变DAC输出,所以最后用DAC_DMACmd()替换了DAC_SetChannel1Data()。

    2.配置定时器

    这里不使用中断,所以不需要配置NVIC,也不用编写中断函数
    代码如下:
    1. void TIM_Config()

    2. {

    3.     u16 arr = 1; // 自动重装载寄存器(Auto Reload Register)

    4.     u16 psc = 0; // 预分频器(Prescaler)

    5.     TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;



    6.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // TIM3时钟使能



    7.     TIM_DeInit(TIM2); // 初始化

    8.         TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); // 初始化

    9.     TIM_TimeBaseStructure.TIM_Period = arr;

    10.     TIM_TimeBaseStructure.TIM_Prescaler = psc;

    11.     TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 似乎是在数字滤波器当中才会用到,平时一般设为0(TIM_CKD_DIV1)

    12.     TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式



    13.     TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);

    14.     TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update); // **与上一方案不同**

    15. }
    复制代码

    这里用TIM_SelectOutputTrigger来使TIM2产生触发输出以触发DAC更新,所以不需要用TIM_ITConfig()来使能中断

    这里为什么不用TIM3呢,因为查开发手册发现TIM3的触发输出传不到DAC这儿

    6.png

    上一个方案为什么不用TIM2呢,因为最初的程序是各种东拼西凑而成的,当时引入的代码使用的是TIM3

    3.配置DMA

    有许多外设都可以产生DMA请求,使得DMA搬运一次数据。不同的DMA通道用于接收不同的外设产生的请求,在这里我们使用的是DAC1产生的DMA请求,查表可得对应的DMA通道为DMA2_Channel3

    7.png

    8.png

    由此编写的DMA初始化代码如下:
    1. void MYDMA_Config(u32 cpar, u32 cmar, u16 cndtr) // cpar为外设地址,cmar为内存地址,cndtr为搬运的数据个数(对应于数组长度)

    2. {

    3.     DMA_InitTypeDef DMA_InitStructure;



    4.     RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE); // 重要,很多时候只顾着改DMA通道而忘记使能对应时钟,导致DMA不工作



    5.     DMA_DeInit(DMA2_Channel3); // 初始化



    6.     DMA_StructInit(&DMA_InitStructure); // 初始化

    7.     DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:从内存到外设

    8.     DMA_InitStructure.DMA_BufferSize = cndtr; // 指定每轮DMA需要搬运的数据个数

    9.     DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变(固定为DAC寄存器地址)

    10.     DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增(遍历数组)

    11.     DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据长度为16bit

    12.     DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据长度为16bit

    13.     DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 设置为高优先级(使用单个DMA时无影响)

    14.     DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存模式

    15.     DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环

    16.     DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; // 外设起始地址

    17.     DMA_InitStructure.DMA_MemoryBaseAddr = cmar; // 内存起始地址



    18.     DMA_Init(DMA2_Channel3, &DMA_InitStructure);

    19.     DMA_Cmd(DMA2_Channel3, ENABLE); // 使能

    20. }
    复制代码

    每条语句的具体解释见注释。

    4.其它逻辑

    4.1.计算波形
    用于在改变输出波形后计算寄存器内的值并填入全局数组data[]中。
    为了提高代码可读性,预先声明了一个枚举类型
    1. typedef enum _waveType

    2. {

    3.     WAVE_SQUARE, // 方波

    4.     WAVE_SINE, // 正弦波

    5.     WAVE_RAMP, // 锯齿波

    6.     WAVE_TRIANGLE, // 三角波

    7. } waveType;
    复制代码

    之后是生成波形的代码
    1. void wave_gen(waveType type, u16 len, u16 vpp)

    2. {

    3.     u16 base = 2048; // 偏置,保证负电压可以正常输出

    4.     double amp;

    5.     u16 i; // 循环变量

    6.     if (vpp > 3300)

    7.         vpp = 3300; // 若输入的vpp超过范围,则限制在最大值

    8.     amp = vpp / 3300.0 * 2047.0; // 根据峰峰值进行放缩



    9.     if (type == WAVE_SINE) // 正弦波

    10.     {

    11.         for (i = 0; i < len; i++)

    12.         {

    13.             data[i] = (u16)(base + sin((double)i / len * 6.283185307) * amp);

    14.         }

    15.     }

    16.     else if (type == WAVE_RAMP) // 锯齿波

    17.     {

    18.         for (i = 0; i < len; i++)

    19.         {

    20.             data[i] = (u16)(base + ((double)i / len - 0.5) * 2 * amp);

    21.         }

    22.     }

    23.     else if (type == WAVE_SQUARE) // 方波

    24.     {

    25.         for (i = 0; i < len / 2; i++)

    26.         {

    27.             data[i] = base + amp;

    28.         }

    29.         for (i = len / 2; i < len; i++)

    30.         {

    31.             data[i] = base - amp;

    32.         }

    33.     }

    34.     else if (type == WAVE_TRIANGLE) // 三角波

    35.     {

    36.         for (i = 0; i < len / 2; i++)

    37.         {

    38.             data[i] = (u16)(base + ((double)i / len - 0.25) * 4 * amp);

    39.         }

    40.         for (i = len / 2; i < len; i++)

    41.         {

    42.             data[i] = (u16)(base + ((double)(len - i) / len - 0.25) * 4 * amp);

    43.         }

    44.     }

    45. }
    复制代码

    4.2.重置波形
    因为每一次改变波形后都需要重新设置DAC,定时器和DMA,所以将这些外设的配置函数写到了一个子函数中方便调用
    1. void reset_all() // 里面的type变量是一个waveType型的全局变量,相当于上一方案的mode;len是一个全局变量,表示当前data[]数组中有效数据的个数

    2. {

    3.     wave_gen(type, len, data_vpp);

    4.     DAC_Cmd(DAC_Channel_1, DISABLE);

    5.     DAC_DMACmd(DAC_Channel_1, DISABLE);

    6.     DMA_Cmd(DMA2_Channel3, DISABLE);

    7.     TIM_Cmd(TIM2, DISABLE);

    8.     TIM2_Int_Init(1, 0);

    9.     Dac1_Init();

    10.     MYDMA_Config((u32) & (DAC->DHR12R1), (u32)data, len);

    11.     TIM_Cmd(TIM2, ENABLE);

    12. }
    复制代码

    4.3.主函数
    1. #include "delay.h"

    2. #include "math.h"

    3. #include "key.h"

    4. #include "sys.h"



    5. u16 data[512];

    6. u16 type = WAVE_SQUARE, len = 300, data_vpp = 3300;



    7. int main(void)

    8. {

    9.     u8 key = 0;

    10.     delay_init();

    11.     KEY_Init();

    12.     reset_all();



    13.     while (1)

    14.     {

    15.         key = KEY_Scan(0);

    16.         if (key != 0)

    17.         {

    18.             if (key == KEY0_PRES)

    19.             {

    20.                 type++;

    21.                 type %= 4;

    22.             }

    23.             else if (key == KEY1_PRES)

    24.             {

    25.                 len -= 50;

    26.             }

    27.             else if (key == WKUP_PRES)

    28.             {

    29.                 len += 50;

    30.             }

    31.             reset_all();

    32.         }

    33.     }

    34. }
    复制代码

    这里改变频率有两种方式,第一种是len不变,改变定时器的arr和psc;第二种是定时器的参数不变,改变数组的有效数据个数len。使用后者可以保证定时器的利用率最高,也就是频率越低输出越精准。

    如果需要按照一定的频率步进来修改len,则需要一个从freq到len换算的函数,这里不再赘述。

    四、另一种用定时器+DMA+DAC实现的思路

    上一方案当中的触发流程是定时器→DAC→DMA,看似环环相扣,但是DAC是通过外部触发的方式来更新的,查阅手册得知该模式下DAC每隔3个APB1时钟周期才将DHRx寄存器的数据搬到DORx寄存器,而当不使用硬件触发功能时,DHRx寄存器到DORx寄存器只需要一个APB1时钟周期,因此不使用触发功能可以提高DAC的转换速度(经后期测试此方案虽然能提高波形的频率,但这种速度提升并不是由于DAC转换时间缩短导致的)。

    9.png

    上一方案中DMA请求是由DAC产生的,而此时我们可以把DAC剥离开来,仅让定时器产生DMA请求更新DAC寄存器,而DAC则按照自己的时钟周期去根据寄存器的值更新输出电压。

    1.配置DAC
    在上一方案的基础上,删掉最后的
    1. DAC_DMACmd(DAC_Channel_1, ENABLE);
    复制代码

    2.配置定时器
    还是基于上一方案,将最后的
    1. TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update);
    复制代码

    删除,之后添加
    1. TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);
    复制代码

    使得定时器直接产生DMA请求

    3.配置DMA
    此时DMA请求的来源是TIM2而不是DAC1,查表得知对应的DMA通道为DMA1_Channel2。
    将上一方案中的DMA2_Channel3全部替换为DMA1_Channel2,并且将
    1. RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);
    复制代码

    替换为
    1. RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    复制代码

    经测试,同样在arr=1,psc=0,len=300的情况下,第二种方案输出的正弦波频率能达到14.66KHz且其它功能正常。第一种方案输出的正弦波只能到1.03KHz,且由于CPU需要处理大量中断,此时通过按键无法更改波形(主函数无响应)。第三种方案可以达到16KHz,输出频率略有提升。

    10.jpg

    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    小黑屋|路丝栈 ( 粤ICP备2021053448号 )

    GMT+8, 2024-9-19 09:44 , Processed in 0.049414 second(s), 22 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表