社团作业=_=

任务一:波形生成。1.使用STM32的DAC功能,生成0~10kHz的方波,频率精确到1%以内;  2.能够生成三角波、正弦波;  3.实现频率设置,可以实现一定步进数的调节,最高为100Hz

一、相关内容简介

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输出使用时需要先将端口配置为模拟输入模式以避免干扰。


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

void Dac1_Init()
{GPIO_InitTypeDef GPIO_InitStructure;DAC_InitTypeDef DAC_InitType;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA时钟使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // DAC时钟使能DAC_DeInit(); // 初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 配置为模拟输入GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_SetBits(GPIOA, GPIO_Pin_4);DAC_StructInit(&DAC_InitType); // 初始化DAC_InitType.DAC_Trigger = DAC_Trigger_None; // 不使用触发功能,对应寄存器中TEN1=0,TSEL1[2:0] = 000DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None; // 不使用自带的波形生成功能DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; // 通过线性反馈移位寄存器生成伪噪声,仅当DAC_WaveGeneration配置为DAC_WaveGeneration_Noise时有效DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Disable; // 禁用输出缓存DAC_Init(DAC_Channel_1, &DAC_InitType); // 初始化DACDAC_Cmd(DAC_Channel_1, ENABLE); // 使能DACDAC_SetChannel1Data(DAC_Align_12b_R, 0); // 输出0V
}

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

2.配置定时器

2.1.初始化

因为这里只是使用定时器的中断,所以随便用哪个定时器都可以
以配置TIM3为例,代码如下:

void TIM_Config()
{u16 arr = 1; // 自动重装载寄存器(Auto Reload Register)u16 psc = 0; // 预分频器(Prescaler)TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;NVIC_InitTypeDef NVIC_InitStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // TIM3时钟使能TIM_DeInit(TIM3); // 初始化TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); // 初始化TIM_TimeBaseStructure.TIM_Period = arr;TIM_TimeBaseStructure.TIM_Prescaler = psc;TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 似乎是在数字滤波器当中才会用到,平时一般设为0(TIM_CKD_DIV1)TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 允许更新中断NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_Init(&NVIC_InitStructure);
}

因为这里要用的是中断,所以中间调用了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分频(不分频)的状态。


以两个中断的间隔时常作为时钟周期(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寄存器值

void TIM3_IRQHandler() // 中断函数是靠函数签名来确认的,因此不能改变这里的函数名,参数和返回类型
{u16 vals = 0;if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET){TIM_ClearITPendingBit(TIM3, TIM_IT_Update); // 清零中断标志位if (mode == 0) // 生成方波{vals = counter * 4000; // 此时mod为2。则counter将会在0,1之间不断循环,从而达到生成方波的目的}else if (mode == 1) // 生成锯齿波,{vals = ((float)counter / mod) * 4000; // 此时mod由我们自己定义。counter从0加到mod-1再回到0并循环}else if (mode == 2) // 生成三角波{//vals = (u16)(sin(counter / (double)mod) * 2000 + 2000); //这是实时计算寄存器值的代码,会占用较多的CPU时间导致无法生成相对高频的波形,所以这里采用先一次性计算完成,后逐个读取,以空间换时间的方法。vals = sin_wave[counter]; // 此时mod刚好等于sin_wave[]数组的长度。}DAC_SetChannel1Data(DAC_Align_12b_R, vals);counter++;counter %= mod;}
}

这里的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运行就好了

from math import *ARRAY_LEN = 300output = ""
output += "u16 sin_wave[" + str(ARRAY_LEN) + "] = {"
for i in range(ARRAY_LEN):# 这里的+2000是为了保证能够输出sin函数为负值的部分val = sin(i / float(ARRAY_LEN) * 2 * pi) * 2000 + 2000 output += str(int(val))output += ", "
output = output[:-2]
output += "};"
print(output)

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

3.主函数

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

#include "delay.h"
#include "key.h"
#include "sys.h"u8 mode = 0;
u16 mod = 2;
u16 counter = 0;
u16 sin_wave[300]; // 具体数据由之前的脚本生成int main(void)
{u8 key;delay_init();NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);TIM_Config(); Dac1_Init();KEY_Init();TIM_Cmd(TIM3, ENABLE);while (1){key = KEY_Scan(0);if (key == KEY0_PRES) // 改变波形种类{mode++;mode %= 3;if (mode == 0)mod = 2; // 方波elsemod = 300; // 正弦波和锯齿波}else if (key == KEY1_PRES) // 频率变为原来的0.5倍{TIM3->ARR = (TIM3->ARR + 1) * 2 - 1;}else if (key == WKUP_PRES) // 频率变为原来的2倍{TIM3->ARR = (TIM3->ARR + 1) / 2 - 1;}}
}

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

虽然可以通过减少点数的方法来增加频率,但这是以损失波形精度为代价的。设想每个周期只含有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

代码如下:

void Dac1_Init()
{GPIO_InitTypeDef GPIO_InitStructure;DAC_InitTypeDef DAC_InitType;RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIOA时钟使能RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // DAC时钟使能DAC_DeInit(); // 初始化GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 配置为模拟输入GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_SetBits(GPIOA, GPIO_Pin_4);DAC_StructInit(&DAC_InitType); // 初始化DAC_InitType.DAC_Trigger = DAC_Trigger_T2_TRGO; // **与上一方案不同**DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None; // 不使用自带的波形生成功能DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0; // 通过线性反馈移位寄存器生成伪噪声,仅当DAC_WaveGeneration配置为DAC_WaveGeneration_Noise时有效DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Disable; // 禁用输出缓存DAC_Init(DAC_Channel_1, &DAC_InitType); // 初始化DACDAC_Cmd(DAC_Channel_1, ENABLE); // 使能DACDAC_DMACmd(DAC_Channel_1, ENABLE); // **与上一方案不同**
}

这段代码与方案一有两处不同,第一处是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,也不用编写中断函数
代码如下:

void TIM_Config()
{u16 arr = 1; // 自动重装载寄存器(Auto Reload Register)u16 psc = 0; // 预分频器(Prescaler)TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // TIM3时钟使能TIM_DeInit(TIM2); // 初始化TIM_TimeBaseStructInit(&TIM_TimeBaseStructure); // 初始化TIM_TimeBaseStructure.TIM_Period = arr;TIM_TimeBaseStructure.TIM_Prescaler = psc;TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 似乎是在数字滤波器当中才会用到,平时一般设为0(TIM_CKD_DIV1)TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update); // **与上一方案不同**
}

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

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

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

3.配置DMA

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


由此编写的DMA初始化代码如下:

void MYDMA_Config(u32 cpar, u32 cmar, u16 cndtr) // cpar为外设地址,cmar为内存地址,cndtr为搬运的数据个数(对应于数组长度)
{DMA_InitTypeDef DMA_InitStructure;RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE); // 重要,很多时候只顾着改DMA通道而忘记使能对应时钟,导致DMA不工作DMA_DeInit(DMA2_Channel3); // 初始化DMA_StructInit(&DMA_InitStructure); // 初始化DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 方向:从内存到外设DMA_InitStructure.DMA_BufferSize = cndtr; // 指定每轮DMA需要搬运的数据个数DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不变(固定为DAC寄存器地址)DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增(遍历数组)DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据长度为16bitDMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据长度为16bitDMA_InitStructure.DMA_Priority = DMA_Priority_High; // 设置为高优先级(使用单个DMA时无影响)DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 非内存到内存模式DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; // 外设起始地址DMA_InitStructure.DMA_MemoryBaseAddr = cmar; // 内存起始地址DMA_Init(DMA2_Channel3, &DMA_InitStructure);DMA_Cmd(DMA2_Channel3, ENABLE); // 使能
}

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

4.其它逻辑

4.1.计算波形

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

typedef enum _waveType
{WAVE_SQUARE, // 方波WAVE_SINE, // 正弦波WAVE_RAMP, // 锯齿波WAVE_TRIANGLE, // 三角波
} waveType;

之后是生成波形的代码

void wave_gen(waveType type, u16 len, u16 vpp)
{u16 base = 2048; // 偏置,保证负电压可以正常输出double amp; u16 i; // 循环变量if (vpp > 3300)vpp = 3300; // 若输入的vpp超过范围,则限制在最大值amp = vpp / 3300.0 * 2047.0; // 根据峰峰值进行放缩if (type == WAVE_SINE) // 正弦波{for (i = 0; i < len; i++){data[i] = (u16)(base + sin((double)i / len * 6.283185307) * amp);}}else if (type == WAVE_RAMP) // 锯齿波{for (i = 0; i < len; i++){data[i] = (u16)(base + ((double)i / len - 0.5) * 2 * amp);}}else if (type == WAVE_SQUARE) // 方波{for (i = 0; i < len / 2; i++){data[i] = base + amp;}for (i = len / 2; i < len; i++){data[i] = base - amp;}}else if (type == WAVE_TRIANGLE) // 三角波{for (i = 0; i < len / 2; i++){data[i] = (u16)(base + ((double)i / len - 0.25) * 4 * amp);}for (i = len / 2; i < len; i++){data[i] = (u16)(base + ((double)(len - i) / len - 0.25) * 4 * amp);}}
}
4.2.重置波形

因为每一次改变波形后都需要重新设置DAC,定时器和DMA,所以将这些外设的配置函数写到了一个子函数中方便调用

void reset_all() // 里面的type变量是一个waveType型的全局变量,相当于上一方案的mode;len是一个全局变量,表示当前data[]数组中有效数据的个数
{wave_gen(type, len, data_vpp);DAC_Cmd(DAC_Channel_1, DISABLE);DAC_DMACmd(DAC_Channel_1, DISABLE);DMA_Cmd(DMA2_Channel3, DISABLE);TIM_Cmd(TIM2, DISABLE);TIM2_Int_Init(1, 0);Dac1_Init();MYDMA_Config((u32) & (DAC->DHR12R1), (u32)data, len);TIM_Cmd(TIM2, ENABLE);
}
4.3.主函数
#include "delay.h"
#include "math.h"
#include "key.h"
#include "sys.h"u16 data[512];
u16 type = WAVE_SQUARE, len = 300, data_vpp = 3300;int main(void)
{u8 key = 0;delay_init();KEY_Init();reset_all();while (1){key = KEY_Scan(0);if (key != 0){if (key == KEY0_PRES){type++;type %= 4;}else if (key == KEY1_PRES){len -= 50;}else if (key == WKUP_PRES){len += 50;}reset_all();}}
}

这里改变频率有两种方式,第一种是len不变,改变定时器的arr和psc;第二种是定时器的参数不变,改变数组的有效数据个数len。使用后者可以保证定时器的利用率最高,也就是频率越低输出越精准。
如果需要按照一定的频率步进来修改len,则需要一个从freq到len换算的函数,这里不再赘述。

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

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


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

1.配置DAC

在上一方案的基础上,删掉最后的

DAC_DMACmd(DAC_Channel_1, ENABLE);

2.配置定时器

还是基于上一方案,将最后的

TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_Update);

删除,之后添加

TIM_DMACmd(TIM2, TIM_DMA_Update, ENABLE);

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

3.配置DMA

此时DMA请求的来源是TIM2而不是DAC1,查表得知对应的DMA通道为DMA1_Channel2。
将上一方案中的DMA2_Channel3全部替换为DMA1_Channel2,并且将

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE);

替换为

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

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

2020.06.01

用STM32的内置DAC制作一个波形生成器(发生器)相关推荐

  1. U盘GPIO文件系统映射-STM32利用内置FLASH做U盘

    受到linux对一切设备的控制都当成文件对待的启发 于是便有了这个将GPIO映射到U盘中的想法,这样一来便可以在任何支持U盘的设备中扩展系统的硬件功能了 我的QQ是243786753,这属于原创作品, ...

  2. 带内部参考电压(VREFINT)校正的STM32 DMA 内置温度采集

    笔者今天来介绍一下STM32ADC内置温度的采集,重点是通过内置参考电压来避免ADC参考电压VDDA对温度ADC采集的影响. 1.STM32ADC简介   stm32F4系列ADC,逐次趋近型AD.1 ...

  3. 用APPinventor制作一个密码生成器

    今天,小编教大家制作一个密码生成器. 材料:一台装有APPinventor2018(最低版本)电脑. 启动APPinventor 参照小编的<离线版AppInventor搭建服务教程> A ...

  4. day4 高阶函数 嵌套函数 装饰器 内置函数 列表生成式 迭代器 生成器

    一.函数即变量 1.赋值效果图 a = 1  b = a def func(): print('hello') func 是函数名,相当于变量名,print('hello')是函数体,相当于变量的值, ...

  5. BH1750的一些使用心得(STM32,内置工程)

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 前言 一.BH1750是什么? 二.如何使用BH1750 2.1 BH1750的通信方式 2.2 BH1750的指令集 2.3 ...

  6. 春节倒计时,让我来秀一手:用Python制作一个对联生成器

    前言 跨年跨完了,马上就要迎来春节了,这不得秀一手? 那就直接开始春节的表演呗 勉勉强强来用python制作对联生成器吧 效果展示 这里的话,你自己想要啥春联主题是可以搜索滴,有些地方也是可以看着改的 ...

  7. 从此不怕被盗号:教你如何用 Python 制作一个密码生成器

    原由: 定期更换密码是一种非常重要的安全措施,这种做法可以有效地保护你的账户和个人信息不受黑客和网络攻击者的侵害. 密码泄露是一个非常普遍的问题,许多人的账户和密码经常会被泄露出来,导致个人信息被盗用 ...

  8. android apk 提取,android APK提取内置软件odex转dex

    android APK提取内置软件odex转dex 细心的网友可能发现android的ROM中有很多odex文件,相对于APK中的dex文件而言这个odex有什么作 用呢? android123提示大 ...

  9. python内置函数中的 IO文件系列 open和os

    本篇介绍 IO 文件中的 open 和 os基础用法. 本次用一个游戏登陆 基础界面做引子,来介绍. 实现存储的话,方式是很多的. 比如 存到字典 和列表了,可是字典.列表是临时的,玩网页游戏一次还是 ...

最新文章

  1. 两机五节点电力系统的潮流仿真计算_南科大杨再跃课题组在电力系统、机器学习等领域取得重要研究成果...
  2. bat修改文件内容_在win10系统中一键修改MapGIS67系统库背景色
  3. sendmail邮件服务器配置
  4. python apktool_【转】利用apktool反编译apk,并且重新签名打包
  5. VTK:相交线用法实战
  6. 『Go 语言底层原理剖析』文末送书
  7. json qbytearray 串 转_JSON数据采集网关,json转Modbus RTU串IO口RS485转4~20mA边缘计算智能终端...
  8. Struts2的Action访问Session对象的两种方式及原理
  9. 如何在excel中打钩
  10. JSTL核心标签超详细
  11. MYSQL UPDATE使用子查询
  12. 软件测试工程师简历项目经验怎么写?
  13. cif t t操作流程图_cif流程(cif贸易术语流程图)
  14. SpringBoot将数据放入Excel里面通过浏览器直接下载到本地
  15. jzoj P1285 奶酪厂
  16. List、Map、Set集合的特点及常用方法
  17. 如何用VBA保护工作表
  18. c学前儿童语言教育试卷,学前儿童语言教育期中试卷
  19. ENVI统计值异常问题
  20. VMware未来二十年,打开数字化转型的无限可能

热门文章

  1. U盘格式化后数据恢复【图文教程】
  2. 小程序Promise用法
  3. 《Web前端黑客技术揭 秘》解决部分示例无效的问题
  4. php四舍五入代码,PHP四舍五入函数代码详解
  5. 做运营2年,总结运营应具备的2种思维方式和3个习惯
  6. ChatGPT | Word文档如何更好地提取表格内容给ChatGPT
  7. python 添加半透明水印_超简单Python安全批量加水印教程!
  8. 夏普大屏显示服务器,夏普展示4.1寸2K屏幕:2016年大规模应用
  9. swift问题集--未完待续
  10. GDC2011: Fast and Efficient Facial Rigging