记录一下,方便以后翻阅~
主要内容:
1) PWM原理复习;
2) DAC PWM数模转换原理;
3) 相关实验代码解读。
实验功能:系统启动后,PA8输出PWM波形,经二阶RC滤波后转化为DAC输出,按WK_UP键输出电压变大,按KEY1键,输出电压降低。每次按键,ADC采集输出电压值并传至串口调试助手上。
官方资料:《STM32中文参考手册V10》第12章——数字模拟转换DAC和第14章——通用定时器
硬件连接
定时器1通道1输出PWM,通过PA8输出,经过二阶RC滤波后输出电压。
1. PWM复习
也可参考《STM32学习心得十八:通用定时器基本原理及相关实验代码解读》实验二部分知识,基本一样。
1.1 PWM工作原理复习
脉冲宽度调制(PWM——Pulse Width Modulation)简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种技术。简单一点,就是对脉冲宽度的控制。
PWM工作逻辑可参考下图理解:
上图中,
ARR为自动重装载寄存器(TIMx_ARR)的位[0:15]设定值,
CCRx为某个捕获比较寄存器(TIMx_CRRx)的位[0:15]设定值,
黑线为计数器寄存器(TIMx_CNT)的位[0:15]实时值,记CNT。
当CNT小于CCRx时,可设输出低电平,反之输出高电平。因此可以认为:脉宽调制(PWM)信号的周期由ARR决定,占空比由CCRx决定。
以通道1为例,寄存器主要包括:
1) 捕获比较(值)寄存器(TIMx_CCRx(x=1,2,3,4)),用来设置比较值;
2) 捕获/比较模式寄存器 1(TIMx_CCMR1),位[4:6],输出比较1模式(OC1M):
在PWM方式下,设置PWM模式1(110),在向上计数时,一旦TIMx_CNT
或在PWM方式下,设PWM模式2(111),在向上计数时,一旦TIMx_CNT
3) 捕获/比较使能寄存器(TIMx_CCER),位1,输入/捕获1输出极性CC1P:
当CC1通道配置为输出时,0:高电平有效,1:低电平有效;
4) 捕获/比较使能寄存器(TIMx_CCER),位0,输入/捕获1输出使能CC1E:
当CC1通道配置为输出时,0:关闭,1:打开。
1.2 计数模式有哪些?
1) 向上计数模式:计数器从0计数到自动加载值(TIMx_ARR计数器的值),然后重新从0开始计数并且产生一个计数器溢出事件;
2) 向下计数模式:计数器从自动装入的值 (TIMx_ARR计数器的值)开始向下计数到0,然后从自动装入的值重新开始并且产生一个计数器向下溢出的事件;
3) 中央对齐模式(向上/向下计数) :计数器从0开始计数到自动加载的值(TIMx_ARR寄存器的值−1),产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器下溢事件,然后再从0开始重新计数。
1.3 STM32哪些定时器可用于PWM输出?
由上图可知,除了基本定时器TIM6和TIM7,其他的定时器都可以用来产生PWM输出。其中,高级定时器TIM1和TIM8可以同时产生多达7路的PWM输出。而通用定时器也能同时产生多达4路的PWM输出。
1.4 STM32 定时器输出通道引脚整理(可参考数据手册)
1.5 对自动重载的预装载寄存器的理解
自动装载寄存器是预先装载的,写或读自动重装载寄存器将访问预装载寄存器。根据在TIMx_CR1寄存器中的自动装载预装载使能位(ARPE)的设置,预装载寄存器的内容被立即或在每次的更新事件UEV时传送到影子寄存器。
影子寄存器保存的是定时器当前的计数值(或者溢出值),这个值是立即生效的值,这个计数值是从预装载寄存器(ARR)传过来的,但ARR什么时候把计数值传给影子寄存器呢?这儿就有个预装载使能位(ARPE):当ARPE=0的时候,你写入ARR的值马上就传到影子寄存器,也就立即生效当ARPE=1的时候,ARR的值就是直接传过去了,而是等到定时器更新事件发生,才把这个值传到影子寄存器,也就起到一个缓冲作用。
举例说明:
如上图所示,当ARPE=1时,自动加载寄存器值从F5改为36时,计数器寄存器值从F0增加至F5时产生更新事件,说明自动加载寄存器值从F5改为36时就已生效。
如上图所示,当ARPE=0时,自动加载寄存器值从FF改为36时,计数器寄存器值从31增加至36时产生更新事件,说明自动加载寄存器值从F5改为36时未即使生效,要等下个比较周期生效。
简单的说:
ARPE=1,ARR立即生效;
APRE=0,ARR下个比较周期生效。
1.6 PWM输出配置一般步骤(以TIM3_CH2为例)
1.6.1 使能定时器3和相关IO口时钟:
使能定时器3时钟:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
使能GPIOB时钟:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
1.6.2 初始化IO口为复用推挽输出。函数:GPIO_Init();
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
1.6.3 开启AFIO时钟(因为要把PB5(对应LED0)作定时器的PWM输出引脚,所以要重映射配置),同时设置重映射。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE); //部分重映射,即TIM3_CH2对应PB5引脚//
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitTypeStruct);
//TIM_TimeBaseInitTypeStruct结构体包括5个参数://
//uint16_t TIM_Prescaler; 针对预分频器寄存器(TIMx_PSC),值范围在0x0000到0xFFFF之间//
//uint16_t TIM_CounterMode; 计数模式,前面已讲//
//uint16_t TIM_Period; 针对自动重装载寄存器(TIMx_ARR),确定脉宽调制信号的周期,前面已讲//
//uint16_t TIM_ClockDivision; 针对控制寄存器1(TIMx_CR1)的位[8:9],设定时钟分频因子CKD,输入捕获用//
//uint8_t TIM_RepetitionCounter。 重复计数寄存器(TIMx_RCR),只有高级定时器才有//
1.6.4.1 uint16_tTIM_Prescaler参数解读
用来确定最后计数器的时钟频率,其值与CK_PSC频率相关。
而CK_PSC频率与选择哪个时钟源有关,如下图所示,一般选择内部时钟CK_INT为时钟源,即CK_INT=CK_PSC。
CK_INT时钟计算方法如下图所示:
当APB1的分频系数是1时,通用定时器的时钟等于APB1时钟的1倍,否则是2倍。
举例:默认调用SystemInit函数情况下:
SYSCLK=72M
AHB时钟=72M
APB1时钟=36M
所以APB1的分频系数=AHB/APB1时钟=2;
所以,通用定时器时钟CK_INT=2*36M=72M。
1.6.4.2 uint16_tTIM_ClockDivision参数解读
针对控制寄存器1(TIM3_CR1)的位[8:9],设定时钟分频因子CKD。若其值为01,则tDTS=2tCK_INT,说明要连续采样2次电平,且都是预期电平值,才能说明是一次有效触发,产生一次触发输入捕获中断(起滤波作用)。
1.6.5 初始化输出比较参数:
TIM_OC2Init(TIM3,&TIM_OCInitTypeStruct);
//TIM_OCInitTypeStruct结构体主要涉及4个参数://
//uint16_t TIM_OCMode; 选择PWM模式1或者模式2//
//uint16_t TIM_OutputState; 输出使能或失能//
//uint16_t TIM_Pulse; 设比较值//
//uint16_tTIM_OCPolarity; 比较输出极性//
1.6.5.1 uint16_t TIM_OCMode参数解读
在TIMx_CCMR3寄存器中的OCxM位选择PWM模式1或模式2。
设为110时为PWM模式1,在向上计数时,一旦TIMx_CNT
设为111时为PWM模式2,在向上计数时,一旦TIMx_CNT
1.6.5.2 uint16_t TIM_Pulse参数解读
针对捕获/比较寄存器 1(TIM3_CCR1),若CC1通道配置为输出,CCR1包含了装入当前捕获/比较1寄存器的值(预装载值);若CC1通道配置为输入,CCR1包含了由上一次输入捕获1事件(IC1)传输的计数器值。
1.6.5.3 uint16_t TIM_OCPolarity参数解读
针对捕获/比较使能寄存器(TIM3_CCER),设置极性,即高电平有效或低电平有效。
1.6.6 使能预装载寄存器:
TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable);
//针对捕获/比较模式寄存器1(TIM3_CCMR1),位[11],设置OC2PE值//
设计preload register和shadow register的好处是,所有真正需要起作用的寄存器(shadow register)可以在同一个时间(发生更新事件时)被更新为所对应的preload register的内容,这样可以保证多个通道的操作能够准确地同步。如果没有shadow register,或者preload register和shadow register是直通的,即软件更新preload register时,同时更新了shadow register,因为软件不可能在一个相同的时刻同时更新多个寄存器,结果造成多个通道的时序不能同步,如果再加上其它因素(例如中断),多个通道的时序关系有可能是不可预知的。
1.6.7 使能定时器:
TIM_Cmd(TIM3, Enable);
1.6.8 不断改变比较值CCRx,达到不同的占空比效果:
TIM_SetCompare2(TIM3, uint16_t Compare2);
//针对捕获/比较寄存器2(TIM3_CCR2),位[0:15],设置捕获/比较2(CCR2)的值//
2. DAC PWM数模转换原理
PWM本质上其实就是是一种周期一定,而高低电平占空比可调的方波。
上图中,N为ARR-1个计数,n为CCR寄存器的值,T为周期时间,
PWM波形可以用分段函数表示为式:
其中:
T是单片机中计数脉冲的基本周期,也就是STM32定时器的计数频率的倒数;
N是PWM波一个周期的计数脉冲个数,也就是STM32的ARR-1的值;
n是PWM波一个周期中高电平的计数脉冲个数,也就是STM32的CCRx的值;
VH和VL分别是PWM波的高低电平电压值,k为谐波次数,t为时间。
将上式展开成傅里叶级数,得到以下公式(不用理解如何推导):
上式,第1项为直流分量,第2项为1次谐波分量,第3项为大于1次的高次谐波分量。直流分量与n成线性关系,并随着n从0到N,直流分量从VL到VL+VH之间变化。这是电压输出DAC所需要的。
因此,如果能把上式中除直流分量外的谐波过滤掉,则可以得到从PWM波到电压输出DAC的转换,即:PWM波可以通过一个低通滤波器进行解调。上式中的第2项的幅度和相角与n有关,频率为1/(NT),其实就是PWM的输出频率。该频率是设计低通滤波器的依据。如果能把1次谐波很好过滤掉,则高次谐波就应该基本不存在了。
通过上面的了解,我们可以得到PWM DAC的分辨率,计算公式如下:
分辨率=log2(N)
这里假设n的最小变化为1,当N=256的时候,分辨率就是8位。而STM32的定时器都是16位的,可以很容易得到更高的分辨率,分辨率越高,速度就越慢。不过我们在本章要设计的DAC分辨率为8位。
在8位分辨条件下,我们一般要求1次谐波对输出电压的影响不要超过1个位的精度,也就是3.3/256=0.01289V,那么1次谐波的值不能大于0.01289V。
假设VH为3.3V,VL为0V,那么一次谐波的最大值是2*3.3/π=2.1V,这就要求我们的RC滤波电路提供至少-20lg(2.1/0.01289)=-44dB的衰减。(这里,推测衰减量公式为:-20lg(谐波最大幅值/最小精度)=衰减值,暂没有更好的解释)
STM32的定时器最快的计数频率是72Mhz,8位分辨率的时候,PWM频率为72M/256=281.25Khz。如果是1阶RC滤波,则要求截止频率为1.77Khz,如果为2阶RC滤波,则要求截止频率为22.34Khz。
参考网上的解释:
1)PWM频率为328.125Khz,那么一次谐波频率就是328.125Khz;
2)1阶RC滤波,幅频特性为:-10lg[1+(f/fp)^2];fp为截止频率。
所以对一阶滤波来说,要达到-44dB的衰减,必须-10lg[1+(f/fp)^2]=-44; 得到f/fp=158.486,即fp=328.125/158.486=2.07Khz。
3)2阶RC滤波,幅频特性为:-20lg[1+(f/fp)^2];fp为截止频率。
所以对二阶滤波来说,要达到-44dB的衰减,必须-20lg[1+(f/fp)^2]=-44; 得到f/fp=12.549,即fp=328.125/12.549=26.14Khz。
3. 相关实验代码解读
这里会将PWM输出实验和PWM DAC输出实验代码进行对比,其实两个实验的代码基本一样。
3.1 timer.h头文件代码解读
PWM DAC输出实验
#ifndef __TIMER_H
#define __TIMER_H
#include "sys.h"
//PWM DAC输出实验是让GPIOA,引脚8输出PWM波形,经二阶RC滤波后,输出稳定DAC值,对应配置是选择复用,高级定时器TIM1_CH1//
void TIM1_PWM_Init(u16 arr,u16 psc);
#endif
PWM输出实验
#ifndef __TIMER_H
#define __TIMER_H
#include "sys.h"
//PWM输出实验是让GPIOB,引脚5输出PWM波形,从而控制LED灯呼吸,对应配置是选择重映射,通用定时器TIM3_CH2//
void TIMER3_PWM_Init(u16 arr, u16 psc);
#endif
3.2 timer.c文件代码解读
PWM DAC输出实验
#include "timer.h"
#include "led.h"
//PWM DAC,定时器1通道1输出PWM波形,通过PA8输出,经过二阶RC滤波后输出电压//
void TIM1_PWM_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
//第1步,时钟使能//
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); //使能TIM1外设时钟使能//
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA外设时钟使能//
//第2步,初始化GPIOA,引脚8,推挽复用输出功能,输出TIM1 CH1的PWM脉冲波形//
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8; //GPIOA, PA8//
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出//
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
//第3步,初始化定时器//
TIM_TimeBaseStructure.TIM_Period = arr; //设置自动重装载周期值//
TIM_TimeBaseStructure.TIM_Prescaler =psc; //设置预分频值//
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式//
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim//
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
//第4步,初始化输出比较参数//
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //CH1 PWM2模式//
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能//
TIM_OCInitStructure.TIM_Pulse = 100; //设置待装入捕获比较寄存器的脉冲值//
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //OC1 低电平有效//
TIM_OC1Init(TIM1, &TIM_OCInitStructure);
//第5步,使能预装载寄存器//
TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);
//*****下面两行,在定时器PWM输出实验里没有*****//
TIM_ARRPreloadConfig(TIM1, ENABLE); //使能TIMx在ARR上的预装载寄存器//
TIM_CtrlPWMOutputs(TIM1,ENABLE); //主输出使能,高级定时器必须开启//
//*****上述两行,在定时器PWM输出实验里没有*****//
//最后一步,使能TIM1//
TIM_Cmd(TIM1, ENABLE);
}
PWM输出实验
#include "timer.h"
#include "led.h"
//PWM实验,定时器3通道2输出PWM波形,从而控制LED灯//
void TIMER3_PWM_Init(u16 arr, u16 psc)
{
GPIO_InitTypeDef GPIO_InitTypeStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitTypeStruct;
TIM_OCInitTypeDef TIM_OCInitTypeStruct;
//第1步,使能TIM3时钟,使能GPIOB时钟,使能AFIO时钟(PWM DAC实验不用)//
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO,ENABLE);
//第2步,初始化GPIOB5为复用推挽输出,50MHz//
GPIO_InitTypeStruct.GPIO_Pin=GPIO_Pin_5;
GPIO_InitTypeStruct.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_InitTypeStruct.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitTypeStruct);
//*****开启部分使能重映射******//
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);
//第3步,定时器参数初始化//
TIM_TimeBaseInitTypeStruct.TIM_Period=arr;
TIM_TimeBaseInitTypeStruct.TIM_Prescaler=psc;
TIM_TimeBaseInitTypeStruct.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInitTypeStruct.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitTypeStruct);
//第4步,初始化输出比较参数//
TIM_OCInitTypeStruct.TIM_OCMode=TIM_OCMode_PWM2; //CH2 PWM2模式//
TIM_OCInitTypeStruct.TIM_OutputState=TIM_OutputState_Enable;
TIM_OCInitTypeStruct.TIM_Pulse=100; //设置待装入捕获比较寄存器的脉冲值//
TIM_OCInitTypeStruct.TIM_OCPolarity=TIM_OCPolarity_Low; //OC2 低电平有效//
TIM_OC2Init(TIM3,&TIM_OCInitTypeStruct);
//第5步,使能预装载寄存器//
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
//最后一步,使能TIM3//
TIM_Cmd(TIM3,ENABLE);
}
3.3 main.c文件代码解读
PWM DAC输出实验
#include "led.h"
#include "key.h"
#include "delay.h"
#include "sys.h"
#include "usart.h"
#include "adc.h"
#include "timer.h"
int main(void)
{
u16 adcx;
float temp;
u8 t=0;
u16 pwmval=0;
u8 key;
delay_init(); //延时函数初始化//
uart_init(115200); //串口初始化为115200//
KEY_Init(); //KEY初始化//
LED_Init(); //LED端口初始化//
Adc_Init(); //ADC初始化//
TIM1_PWM_Init(255,0); //TIM1 PWM初始化, Fpwm=72M/256=281.25Khz//
TIM_SetCompare1(TIM1,pwmval);
while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)
{
if(pwmval<250)pwmval+=10;
TIM_SetCompare1(TIM1,pwmval);
}else if(key==KEY1_PRES)
{
if(pwmval>10)pwmval-=10;
else pwmval=0;
TIM_SetCompare1(TIM1,pwmval);
}
if(t==10||key==KEY1_PRES||key==WKUP_PRES) //手动按键改变占空比,从而改变DAC值//
{
adcx=TIM_GetCapture1(TIM1);
printf("TIM1_CCR1值:%d\r\n",adcx);
temp=(float)adcx*(3.3/256);
printf("TIM1_CCR1对应电压值:%f\r\n",temp);
adcx=Get_Adc_Average(ADC_Channel_1,10);
temp=(float)adcx*(3.3/4096);
printf("ADC读取值:%d\r\n",adcx);
printf("ADC对应电压大小:%f\r\n",temp);
}
delay_ms(200);
LED0=!LED0;
}
}
PWM输出实验
#include "sys.h"
#include "delay.h"
#include "led.h"
#include "timer.h"
int main(void)
{
u16 led0pwmval=0;
u8 dir=1; //方向,1时led0pwmval加,0时led0pwmval减//
delay_init(); //延时函数初始化//
LED_Init(); //LED端口初始化//
TIMER3_PWM_Init(899,0); //不分频。PWM频率=72000000/900=80Khz//
while(1)
{
delay_ms(1); //每隔1ms改变一次占空比//
if(dir)
led0pwmval++;
else
led0pwmval--;
if(led0pwmval>1000)
dir=0;
if(led0pwmval==0)
dir=1;
TIM_SetCompare2(TIM3,led0pwmval);
}
}
PWM DAC和PWM输出实验的代码基本一样,不同之处为,PWM DAC输出实验利用高级定时器TIM1的通道1输出(即GPIOA,PA8),而PWM输出实验利用通用定时器TIM3的通道2输出(即GPIOB,PB5),因此相关配置会略有不同。
再者,PWM DAC输出实验,DAC主要靠硬件设计,非代码,因此该实验案例无需stm32f10x_dac.c相关文件。
实验结果
旧知识点
1)复习如何新建工程模板,可参考STM32学习心得二:新建工程模板;
2)复习基于库函数的初始化函数的一般格式,可参考STM32学习心得三:GPIO实验-基于库函数;
3)复习寄存器地址,可参考STM32学习心得四:GPIO实验-基于寄存器;
4)复习位操作,可参考STM32学习心得五:GPIO实验-基于位操作;
5)复习寄存器地址名称映射,可参考STM32学习心得六:相关C语言学习及寄存器地址名称映射解读;
6)复习时钟系统框图,可参考STM32学习心得七:STM32时钟系统框图解读及相关函数;
7)复习延迟函数,可参考STM32学习心得九:Systick滴答定时器和延时函数解读;
8)复习ST-LINK仿真器的参数配置,可参考STM32学习心得十:在Keil MDK软件中配置ST-LINK仿真器;
9)复习ST-LINK调试方法,可参考STM32学习心得十一:ST-LINK调试原理+软硬件仿真调试方法;
10)复习如何对GPIO进行复用,可参考STM32学习心得十二:端口复用和重映射;
11)复习串口通信相关知识,可参考STM32学习心得十四:串口通信相关知识及配置方法;
12)复习ADC原理及一般配置步骤,可参考STM32学习心得二十三:ADC转换原理及模数转换实验和STM32学习心得二十四:内部温度传感器原理及实验和STM32学习心得二十五:光敏传感器原理及实验。