(本文针对电子设计大赛的训练题目进行总结归纳,以记录STM32的学习)
使用STM32单片机制作一低频信号发生器和简易示波器,要求使用STM32的ADC,DAC和DMA。
本题目的制作采用正点原子STM32F407探索者开发板作为硬件平台,具体要求如下:
1,用TFTLCD屏幕制作交互界面,显示信号波形和测量的频率和峰峰值
2,使用STM32片内ADC对输入的信号进行处理
3,用STM32片内DAC输出正弦波与方波(频率范围为100Hz到10kHz,电压峰峰值范围为0.1-3.3V,且均可调)
4,输出信号的频率的误差不超过3%,测量的频率和峰峰值的误差不超过2%
----------------------------------------------------------------------分割线---------------------------------------------------------------------------
使用芯片:STM32F407ZGT6(标准库)
本题目所使用的STM32的外设主要有ADC,DAC和DMA(至于TFTLCD使用的FSMC等外设在此不做赘述)
STM32F4xx系列一般都有3个ADC
STM32F4的ADC是12位逐次趋近型ADC(分辨率可配置:12位,10位,8位,6位),有19个复用通道,可测量16个外部源,2个内部源和VBAT通道的信号,这些通道的AD转换可在单次,连续,扫描或不连续采样模式下进行
ADC的结果储存在一个左对齐或者右对齐的16位数据寄存器中
所有ADC共用时钟ADCCLK,这一时钟经APB2时钟(PCLK2)分频而来,分频系数为/2,/4,/6,/8
ADC的时钟不能超过36MHz,否则结果将不准确
ADC有16个外部通道,对应的引脚如下图所示:
可见ADC1和ADC2的16个通道使用的引脚是一样的,ADC3使用的引脚基本不一样
事实上,还可将多个ADC分为两组----规则转换和注入转换,规则通道组最多由16个转换组成,相当于程序的正常的运行;注入通道组最多由4个转换组成,相当于程序中的中断。由于本题目只使用一个ADC通道,关于这两个组的知识,在这里不做详述。
本次题目中的ADC使用定时器触发的方式,而不是在某个循环里用读取数值的函数读取ADC的值
STM32的ADC可以通过外部事件触发转换,比如定时器,EXTI等(采用外部事件的触发的好处是可以控制采样频率),且在F4当中可以配置触发的极性,如下图所示:
外部触发的事件如下图所示(仅给出规则通道):
ADC会在数个ADCCLK周期内对输入电压进行采样
总转换时间Tconv=采样时间+12周期
【例】ADCCLK=30MHz,采样时间=3周期
Tconv=3+12=15周期=15/30 us=0.5us
ADC配置步骤如下(采用定时器触发,使能DMA):
1,开启ADC(APB2)和GPIO(AHB1)的时钟
2,配置IO口
注意事项:模式要为模拟输入(GPIO_Mode_AN),上下拉配置要不带上下拉(GPIO_PuPd_NOPULL)
3,ADC通用初始化
void ADC_CommonInit(ADC_CommonInitTypeDef* ADC_CommonInitStruct);
4,ADC初始化
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);
5,ADC使能
6,配置相应的外部触发(比如定时器)
7,配置DMA
DMA:直接存储器访问,通俗的讲,就是将一个地址空间的数据和另一个地址空间的数据之间建立一条直接传输的数据通道,且这条通道最大的好处就是当CPU初始化传输动作之后,传输动作本身由DMA控制器实现,即DMA传输方式无需CPU直接控制传输。
STM32F4最多有两个DMA控制器(DMA1和DMA2),共16个数据流,每个控制器8个,每一个DMA控制器都用于管理一个或多个外设的存储器访问请求。每个数据流总共可以有多达8个通道(或称请求),每个数据流通道都有一个仲裁器,用于处理DMA请求间的优先级。
每个数据流可配置为:
1,外设到存储器,存储器到外设,存储器到存储器传输
2,存储器方双缓冲的双缓冲区通道
看一下参考手册的这两张表就很清楚了:
DMA使能函数:
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);
对于单ADC模式,要使能最后一次传输仍保持DMA请求
void ADC_DMARequestAfterLastTransferCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
配置好DMA的结构体,然后用DMA_Cmd(DMAx_Streamy, ENABLE);使能即可
STM32F4的DAC是12位数字输入,电压输出型DAC,DAC可以按照8位或12位进行配置(12位模式下可配置左对齐或右对齐),有两个DAC转换器,各对应一个输出通道
PA4:DAC_OUT1 PA5:DAC_OUT2
DAC可由外部事件触发
在这些触发方式中,TIM6和TIM7是基本定时器,配置简单,因此可以选择这两种触发方式。且在如下的参考手册中也可以看到,TIM6,7可以专门用于驱动DAC:
TIM6,7可输出多种类型的TRGO信号,可以通过如下函数选择:
TIM_SelectOutputTrigger(TIM6,TIM_TRGOSource_Update); //更新事件,即定时器溢出产生的更新信号
每个DAC通道都具有DMA功能,发生外部触发(而不是软件触发时),将产生DMA请求
DACDMA的配置方法和ADC类似,在这里不做详述
VDACout=Vref*(DOR/4095)
Vref一般为3.3V
----------------------------------------------------------------------分割线---------------------------------------------------------------------------
本次题目选择**ADC1通道5(PA5)**作为输入端口
(1)12位ADC单次转换,数据右对齐
(2)预分频4分频(即ADCCLK=PCLK2/4=84/4=21MHz)
(3)两个采样阶段之间延迟5个时钟
(4)ADC采样时间为周期
总转换时间=1/(2110^6)(+12)=
(5)定时器触发ADC采样
ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_T2_CC2;//使用定时器触发
ADC_InitStructure.ADC_ExternalTrigConvEdge = ADC_ExternalTrigConvEdge_Rising;//上升沿触发
注:正点原子的ADC例程的程序中是禁止触发检测,使用软件触发的,即ADC_ExternalTrigConvEdge_None
然后调用库函数ADC_SoftwareStartConv(ADC1);使能软件触发,可以去学习一下区别。
其实,ADC的触发采样还有多种方式,可以去阅读一下参考手册看看其他方式,比如定时器的TRGO信号
ADC的触发方式如下图所示:
(6)DMA使能
在这里要注意一些F1和F4的小区别:
F1:当外部触发信号被选为ADC规则或注入转换时,只有上升沿可以启动转换
F4:F4的触发边沿在ADC_Init的结构体中可配置
(1)触发选择:定时器2通道2,上升沿触发
(2)由于上升沿触发,因而要使用定时器的输出比较功能(也就是输出PWM功能)
所以我们要初始化两个结构体:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
(3)定时器的参数配置
TIM_TimeBaseStructure.TIM_Period=arr; //自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler=psc; //定时器分频
定时时间Tout=((arr+1)*(psc+1))/Tclk
使用定时器的时候有个非常重要的点要注意:定时器的时钟
STM32F4的定时器由APB1时钟得来(除非APB1分频系数为1,否则为APB1时钟的两倍)
PS:系统在执行完SystemInit后(默认),AHB为168Hz,APB1=42MHz,AHB/APB1=4≠1,因此定时器时钟为84MHz
(4)使用定时器触发ADC的一些小问题
是否需要初始化定时器通道对应的引脚
本题目选择使用ADC1,因此根据上面的表使用DMA2数据流4通道0
配置细节:
1,DMA_DeInit(DMA2_Stream4); //复位
2,DMA结构体配置
重点:DMA_InitStructure.DMA_Channel = DMA_Channel_0; //通道选择
DMA_InitStructure.DMA_PeripheralBaseAddr = (u32)&(ADC1->DR); //ADC外设地址
DMA_InitStructure.DMA_Memory0BaseAddr =(u32)DATA; //DMA 存储器0地址,DATA是创建的数组,用于存储数据
DMA_InitStructure.DMA_BufferSize = 1024; //数据传输量
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; //地址从外设到内存
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器增量模式
3,DMA使能:DMA_Cmd(DMA2_Stream4, ENABLE);
当然,如果使用DMA的相关中断函数也是很方便的,在这里不做详述。
本次题目选择**DAC_OUT1(PA4)**作为DAC输出端口
(1)IO口配置
IO口模式要为模拟输入GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AN;//模拟输入
(2)DAC配置
要选择DAC的触发方式(正点原子的DAC例程中这里的参数设置为DAC_Trigger_None,即不使用触发,可以进行比较学习)
DAC_InitType.DAC_Trigger=DAC_Trigger_T6_TRGO; //定时器6
(3)DAC与DMA使能
DAC_Cmd(DAC_Channel_1, ENABLE); //使能DAC通道1
DAC_DMACmd(DAC_Channel_1,ENABLE);
(4)另外一些常用的函数
DAC_SetChannel1Data(DAC_Align_12b_R,temp); //设置数据位数和对齐方式以及DAC的值
DAC触发使用TIM6触发,TIM6作为基本定时器与TIM2配置类似,甚至还更加简单,在这里不做详述,只需要注意一个重点,那就是要根据想要输出信号的频率修改自动重装载值或定时器分频系数。
在末尾加上这个:TIM_SelectOutputTrigger(TIM6,TIM_TRGOSource_Update);
本次题目采用DAC的通道1输出,根据上面的表可知使用DMA1数据流5通道 7
配置细节:
#define DAC_DHR12R1 (u32)&(DAC->DHR12R1)
DMA_InitStructure.DMA_PeripheralBaseAddr = DAC_DHR12R1; //DAC外设地址
DMA_InitStructure.DMA_DIR = DMA_DIR_MemoryToPeripheral; //内存到外设
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设非增量模式
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器增量模式
ADC的采样率是使用ADC时非常要注意的一个地方。所谓采样,就是将一个信号(例如时间或空间上连续的函数 )转换为数字序列(时间或空间上离散的函数)的过程;而采样率表示每秒ADC可以采样的次数,从几赫兹到几G赫兹不等
根据奈奎斯特-香农采样定理,采样频率最小要为信号最高频率的两倍,由于本次题目要求的输出信号的最高频率10kHz,因此采样频率应该设为20kHz
影响采样率的几个因素:
(1)ADC时钟:21MHz
(2)ADC_RegularChannelConfig(ADC1, ADC_Channel_5, 1, ADC_SampleTime_28Cycles); 采样周期为28
(3)ADC_CommonInitStructure.ADC_TwoSamplingDelay = ADC_TwoSamplingDelay_5Cycles; //两个采样阶段之间的延迟5个时钟
(4)定时器的触发周期:TIM2_PWM_Init(9,20); (定时器时钟为21MHz)则定时器的触发一次的时间为:(99+1)*(20+1)/21M=10^(-4)s
根据前面的分析可知:ADC会在数个ADCCLK周期内对输入电压进行采样
总转换时间Tconv=采样时间+12周期
Tconv=28+12+5=45周期=45/21us≈2.14us 也就是
【例】ADCCLK=30MHz,采样时间=3周期
Tconv=3+12=15周期=15/30 us=0.5us
DAC的数据寄存器的数值的改变就可以改变输出的电压值,因此我们控制DAC输出一系列值就可以近似的输出想要的信号以及信号的峰峰值,修改DAC的触发频率就可以改变输出信号的频率。
这一系列值在程序中的体现就是用数组存储一系列值,可以发现,如果想要输出方波,那存储的值就只有两个(当然,方波的输出也可以用STM32定时器输出PWM的功能,只不过峰峰值不可调,但效果应该会好一点。不过本次题目就使用DAC输出方波);但是想要输出正弦波,显然就要很多个值,那这些值如何得到呢,有三个方法:
(1)在网上直接找一个正弦表
(2)利用matlab生成正弦表
(3)直接在程序中利用math.h的函数生成正弦表
在本题目中我们采用第三种方法(生成的数组大小为256),程序如下(关于这段程序的讲解,请翻到下面设置峰峰值部分):
u16 SineWave_Value[256];
/********正弦波输出表***********/
//Um :输出电压的峰值(1.65-3.3)
/*******************************/
void SineWave_Data(u16 *D,float VPP)
{
u16 i;
float VP=VPP/2;
for( i=0;i<256;i++ )
{
D[i]=(u16) ( ( (VP*sin( ( 1.0*i/255 ) *2*PI ))+1.65 )*4095/3.3 ); //Um* ( sin( 1.0*i/255*2*PI )+1.65 ) *4095/3.3
}
}
当然不同的输出也要在DMA中修改:
#define Square 2
#define Sin 256
if(sign) //sign代表选择方波或正弦波
{
DMA_InitStructure.DMA_Memory0BaseAddr =(u32)SineWave_Value;//DMA 存储器0地址
DMA_InitStructure.DMA_BufferSize = Sin;//数据传输量
}else{
DMA_InitStructure.DMA_Memory0BaseAddr =(u32)dac_out_square;//DMA 存储器0地址
DMA_InitStructure.DMA_BufferSize = Square;//数据传输量
}
根据上面的初始化,可知定时器6的时钟为84MHz,定时器分频值设为41
if(sign) //sign代表选择方波或正弦波
{ //正弦波
f=(u32)(2000000/sizeof(SineWave_Value)/f); //f为自动重装载值和频率,也是想要输出信号的,SineWave_Value是存放着正弦值的数组
}else{
//方波
f=(u32)(1000000/f);
}
程序的式子的计算过程如下:
假定想要输出信号的频率为f
(1)正弦波
可知生成的正弦表的大小为256,也就是说在(f/1)的时间内,要将这256个值全部输出,即DAC要在(1/f)的时间内触发256次。根据前面设置的DAC触发条件,也就是要定时器溢出256次。知道了这些就可以列出等式了:
(arr+1)(psc+1)/Tclk=Tout=1/(f256)
所以解得:arr≈Tclk/(256f(psc+1))=2000000/(256f)
调试中遇到的问题:定时器结构体里有一个参数:TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; 要注意这个时钟分频
(2)方波
方波的数组只有两个值,计算过程和正弦波的一模一样,在这里不做详述。
结果arr≈1000000/f
方波的峰峰值比较好设置,改变一个值即可,程序如下:
void SquareWave_Data(u16 *D,float VPP)
{
D[0]=(u16)(VPP*4095/3.3);
D[1]=0;
}
难点在于正弦波的峰峰值的设置。
程序当中设置正弦波的峰峰值就在生成正弦表的函数内:
u16 SineWave_Value[256];
/********正弦波输出表***********/
//Um :输出电压的峰值(1.65-3.3)
/*******************************/
void SineWave_Data(u16 *D,float VPP)
{
u16 i;
float VP=VPP/2;
for( i=0;i<256;i++ )
{
D[i]=(u16) ( ( (VP*sin( ( 1.0*i/255 ) *2*PI ))+1.65 )*4095/3.3 ); //Um* ( sin( 1.0*i/255*2*PI )+1.65 ) *4095/3.3
}
}
再讲解前要注意一点,那就是STM32既不能生成负电压,也不能利用ADC读取负电压值。因此输出的正弦波必须带有直流偏置(如果想输出负电压,则必须设计外围电路),为了方便起见,在本题目中就将直流偏置设置为DAC最大输出电压3.3V的一半------1.65V,正弦波的幅值要大于1.65V而小于3.3V
( 1.0i/255 ) 2PI 比较好理解,就是按照256个点步进,转换为弧度值
(VPsin( ( 1.0i/255 ) 2PI ))+1.65 就是将弧度值转化为正弦函数值并加上直流偏置
(VPsin( ( 1.0*i/255 ) 2PI ))+1.65 )*4095/3.3 将想输出的电压值转换为DAC的数据寄存器值
交互界面主要的功能如下:
(1)显示程序的功能信息以及基本的欢迎语句
(2)设置建议信号发生器的信号种类和输出频率和峰峰值
本次题目的交互界面利用开发板自带的4.3寸TFTLCD触摸屏来进行设计,显然这种设计的驱动分为两个部分,一是LCD屏幕,二是触摸屏的使用。LCD由于之前做的东西已经使用过,在这里不做详述,本次题目只对第一次使用的触摸屏以及设计用户界面的一些问题做一下记录。
我使用的屏幕是正点原子的4.3寸电容触摸屏,触摸屏的驱动IC是GT9147
在这里只记录一下设计时踩得坑:
(1)要时刻注意画笔的颜色要不然就有可能看不到显示
(2)在横屏时,触摸屏能感应到的区域似乎只有上面的一部分,这个在之后要研究一下
(3)直接使用正点原子给的屏幕扫描函数时,要注意这个函数给出的坐标的方向,是以屏幕的竖屏为起始状态来确定x轴和y轴的
(4)在使用触摸屏时,常会因为程序运行的过快导致程序只会读取一个数字。比如说,想通过触摸屏输入4个数字,但是当按到1时,就直接输入了1111
解决方法:检查是否松开
while(tp_dev.sta&TP_PRES_DOWN)
{
tp_dev.scan(0);
}
欢迎界面
选择输出信号种类(这里以正弦波为例)
设置参数一:频率(这里以1kHz为例)
设置参数二:峰峰值(这里以2.56V为例)
直接进入示波器模式,显示信号波形:
测量信号的频率和峰峰值是本次题目最重要的部分,尤其是频率的测量,涉及到信号与系统的相关知识,而且要使用STM32的DSP库
----------------------------------------------------------------------分割线---------------------------------------------------------------------------
在改进程序时使用了DMA中断,将画出信号波形和用FFT测量的程序放在了DMA的中断里,这样就可以对采样到了1024个点直接进行测量,但是在实际调试时却发现DMA中断进不去,经过排查发现这是DMA的中断位的问题
在DMA初始化中,使能DMA中断的函数是这个样子的:
DMA_ITConfig(DMA2_Stream4,DMA_IT_TC,ENABLE); //使能DMA传输中断
这里的DMA_IT_TC用于使能相关的DMA中断源,就是传输完成中断
但在中断服务函数中是这个样子写的:
void DMA2_Stream4_IRQHandler(void)
{
if(DMA_GetITStatus(DMA2_Stream4,DMA_IT_TCIF4)!=RESET){
DMA_ClearITPendingBit(DMA2_Stream4,DMA_IT_TCIF4);
}
}
可以看到这里又是DMA_IT_TCIF4和上面不同
DMA中断发生标志位和使能的标志位是不一样的,在写DMA中断的时候要特别注意这一点
在中文参考手册当中也有提到这一点:
在标准库中也可以找到这个:
----------------------------------------------------------------------分割线---------------------------------------------------------------------------
图形界面由于是自己画的,做的比较粗糙,以后可以考虑用emwin来改进
鉴于有挺多人评论要的,我直接把百度网盘的链接放在这把,需要的可以自己拿
链接:https://pan.baidu.com/s/1pb5jmrp-vq2xL6FflbkRUA
提取码:1234