本实验是基于STM32 407的板子,使用的是通用定时器TIM14,相关的博文其实有很多,本文主要是基于他们结合个人学习理解总结整理,并非完全原创,只希望能帮助更好理解PWM输出的过程尤其是TIM_ARRPreloadConfig(TIM14, ENABLE)和TIM_ARRPreloadConfig()函数的的作用。
1 TIM14主要特性
关于TIM14的相关介绍可以在中文参考手册的16.3章节查看,该定时器只有一个独立通道,可用于输出捕获、输出比较、PWM生成(边沿对齐模式)、单脉冲模式输出。
下图为高级定时器和通用定时器的引脚分布图。
STM32 PWM工作过程(通道1为例)
2 相关寄存器
首先是基础的时基单元包含的:计数器寄存器(TIMx-CNT)、预分频器寄存器(TIMxPSC)、自动重载寄存器(TIMx_ARR)这三个,除此之外还会用到 4 个寄存器(通用定时器则只需要 3 个),来控制 PWM 的输出。这四个寄存器分别是:捕获/比较模式寄存器( TIMx_CCMR1/2)、捕获/比较使能寄存器( TIMx_CCER)、捕获/比较寄存器( TIMx_CCR1~4) 以及刹车和死区寄存器( TIMx_BDTR)。其中刹车和死区寄存器( TIMx_BDTR)只有当使用高级定时器进行PWM输出时才会用到
1. 捕获/比较模式寄存器( TIMx_CCMR1/2)
该寄存器总共有 2 个, TIMx _CCMR1和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。该寄存器的各位描述如下:
寄存器分了 2层,上面一层对应输出时的设置而下面的则对应输入时的设置。模式设置位 OCxM,此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,这 3 位必须设置为110/111。这两种 PWM 模式的区别就是输出电平的极性相反。 另外 CCxS 用于设置通道的方向(输入/输出)默认设置为 0,就是设置通道作为输出使用。
- 110:PWM模式1在,向上计数时,一旦TIMx_CNT <TIMx_CCR1时通道1为有效电平,否则为无效电平;在向下计数时,一旦TIMx_CNT >TIMx_CCR1时通道1为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。
- 111:PWM模式2-在向上计数时,一旦 TIMx_CNT <TIMx_CCR1时通道1为无效电平,否则为有效电平;在向下计数时,一旦TIMx_CNT >TIMx_CCR1时通道1为有效电平,否则为无效电平。
该寄存器的OC1PE位输出比较 1 预装载使能()也同样值得注意,该位描述如下,看不懂没关系,后面有详细讲解,先有个印象就行。
- 0:禁止与 TIMx_CCR1 相关的预装载寄存器。可随时向 TIMx_CCR1 写入数据,写入后将立即使用新值。
- 1:使能与 TIMx_CCR1 相关的预装载寄存器。可读/写访问预装载寄存器。TIMx_CCR1 预装
载值在每次生成更新事件时都会装载到活动寄存器中。
2. 捕获/比较使能寄存器(TIMx_CCER)
PWM输出实验只用到了 CC1E 位,该位决定了输入捕获/比较寄存器 1 (TIMx_CCR1) 是否能够实际捕获到计数器的值,是输入/捕获 1 输出使能位,要想PWM 从 IO 口输出,这个位必须设置为 1。
3. 捕获比较寄存器(TIMx-CCR1-4)
该寄存器总共有 4 个,对应 4 个输通道 CH1~4。
当通过CCMR1寄存器的CCxS 位将通道CC1设置输出使用时:
CCR1 为要装载到实际捕获/比较 1 寄存器的值(预装载值),即用来与TIMx_CNT 作比较的那个值。
如果没有通过 TIMx_CCMR 寄存器中的 OC1PE 位来使能预装载功能,写入的数值会被直接传
输至当前寄存器中。否则只在发生更新事件时生效(拷贝到实际起作用的捕获/比较寄存器1)
实际捕获/比较寄存器中包含要与计数器 TIMx_CNT 进行比较并在 OC1 输出上发出信号的值。
4. 刹车和死区寄存器(TIMx_BDTR)
该寄存器总共有 2 个, TIMx _CCMR1和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。该寄存器的各位描述如下:
该寄存器,我们只需要关注最高位: MOE 位,要想高级定时器的 PWM 正常输出,则必须设置 MOE 位为 1,否则不会有输出。注意:通用定时器不需要配置这个。
3 PWM输出实验步骤
使用定时器14的PWM功能,输出占空比可变的PWM波,用来驱动LED灯,从而达到LED【PF9]亮度由暗变亮,又从亮变暗,如此循环。
- 当TIM14的通道TIM_CH1选择到PF9时,GPIOF9与DS0刚好硬件相连,可以直接控制等亮灭。
因此此次实验主要用到TIM14的通道TIM_CH1和GPIOF9,首先使能TIM14和F9的时钟。
使能TIM14时钟:RCC_APB1PeriphClockCmd();
使能GPIOF时钟:RCC_AHB1PeriphClockCmd (); - 初始化PF9口为复用功能输出。函数:GPIO_Init();
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; - GPIOF9复用映射到TIM14
GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14); - 初始化定时器:设置TIM14的ARR,PSC等:TIM_TimeBaseInit();
- 设置 TIM14_CH1 的 PWM 模式及通道方向, 使能 TIM1 的 CH1 输出;
在库函数中, PWM 通道设置是通过函TIM_OC1Init()~TIM_OC4Init()来设置的,不同的通道的设置函数不一样,这里我们使用的是通道 1,所以使用的函数是 TIM_OC1Init()。
结构体 TIM_OCInitTypeDef的定义:
typedef struct
{
uint16_t TIM_OCMode; //设置模式是 PWM 还是输出比较;
uint16_t TIM_OutputState;//设置比较输出使能,也就是使能 PWM 输出到端口;
uint16_t TIM_OutputNState; //高级定时器 TIM1 和 TIM8 才用到的
uint16_t TIM_Pulse; //比较值,写CCRx
uint16_t TIM_OCPolarity; //设置极性是高还是低;
uint16_t TIM_OCNPolarity; //高级定时器 TIM1 和 TIM8 才用到的
uint16_t TIM_OCIdleState; //高级定时器 TIM1 和 TIM8 才用到的
uint16_t TIM_OCNIdleState; //高级定时器 TIM1 和 TIM8 才用到的
} TIM_OCInitTypeDef;
- 使能预装载寄存器: TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable);
- 使能自动重装载的预装载寄存器允许位,TIM_ARRPreloadConfig(TIM14,ENABLE);
- 使能定时器,TIM_Cmd(TIM1, ENABLE);
- 普通定时器在完成以上设置了之后, 就可以输出 PWM 了,但是高级定时器,我们还需要使能刹车和死区寄存器( TIM1_BDTR)的 MOE 位,以使能整个 OCx(即 PWM)输出。 库数的设置函数为:
TIM_CtrlPWMOutputs(TIM1,ENABLE); - 不断改变比较值CCRx,达到不同的占空比最后,在经过以上设置之后, PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIM1_CCR1 则可以控制 CH1 的输出占空比。继而控制 DS0 的亮度。
在库函数中,修改 TIM1_CCR1 占空比的函数是:
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
********************************* pwm.c *********************************
//TIM14 PWM部分初始化
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void TIM14_PWM_Init(u32 arr,u32 psc)
{
//此部分需手动修改IO口设置
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM14,ENABLE); //TIM14时钟使能
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE); //使能GPIOF时钟
GPIO_PinAFConfig(GPIOF,GPIO_PinSource9,GPIO_AF_TIM14); //GPIOF9复用为定时器14
//设置该引脚为复用输出功能,输出TIM1_CH1的PWM脉冲波形
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //GPIOF9为TIM_CH1
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF; //复用功能
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_100MHz; //速度100MHz
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //推挽复用输出
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOF,&GPIO_InitStructure); //初始化PF9
TIM_TimeBaseStructure.TIM_Prescaler=psc; //定时器分频
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseStructure.TIM_Period=arr; //自动重装载值
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseInit(TIM14,&TIM_TimeBaseStructure);//初始化定时器14
//初始化TIM14 Channel1 PWM模式
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性低
TIM_OC1Init(TIM14, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
TIM_OC1PreloadConfig(TIM14, TIM_OCPreload_Enable); //使能TIM14在CCR1上的预装载寄存器
TIM_ARRPreloadConfig(TIM14,DISABLE);//ARPE使能
TIM_Cmd(TIM14, ENABLE); //使能TIM14
}
********************************* main.c *********************************
int main(void)
{
u16 led0pwmval=0;
u8 dir=1;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置系统中断优先级分组2
delay_init(168); //初始化延时函数
uart_init(115200);//初始化串口波特率为115200
TIM14_PWM_Init(500-1,84-1); //84M/84=1Mhz的计数频率,重装载值500,所以PWM频率为 1M/500=2Khz.
while(1) //实现比较值从0-300递增,到300后从300-0递减,循环
{
delay_ms(10);
if(dir)led0pwmval++;//dir==1 led0pwmval递增
else led0pwmval--; //dir==0 led0pwmval递减
if(led0pwmval>300)dir=0;//led0pwmval到达300后,方向为递减
if(led0pwmval==0)dir=1; //led0pwmval递减到0后,方向改为递增
TIM_SetCompare1(TIM14,led0pwmval); //修改比较值,修改占空比
}
}
4 影子寄存器、预装载寄存器,TIM_OC1PreloadConfig和TIM_ARRPreloadConfig的作用
针对pwm.c中的TIM_OC1PreloadConfig()和TIM_ARRPreloadConfig()函数可能有的人跟我一样刚开始存在疑惑,这里参考这篇博客总结了下,可以对照着理解下。
首先看定时器的框图:
图中有阴影的小方框,代表该功能对应的寄存器有影子寄存器,也就是:PSC预分频器、自动重装载寄存器、REP寄存器和4个通道的捕获/比较寄存器。
可以看到这几个寄存器都是经常用到的,而且存在定时器工作过程中修改他们的可能性。在定时器工作过程中修改他们的值,就会出现一个问题了:如果上次ARR的值是200,通道1的比较寄存器CCR1值是100,产生占空比为50%的PWM。这个时候我要改变PWM的频率,我把ARR的值改为100,CCR1的值还没来得及更改,那么占空比肯定就会出问题,所以我就需要让他们同步修改。以前ARR=200,CCR1=100,提高频率后ARR=100,CCR1=50,我需要这两个寄存器的值同步修改,最好还是让他们计数完一个周期后再修改,那么进入下一个周期ARR、CCR1同步修改过去,对PWM的占空比就没有一点影响了。
为了达到这个目的,就得先用一个寄存器A把修改的值保存好(ARR_A=100,CCR1_A=50),一旦上一个周期结束,给一个信号,立即就把寄存器A的值赋值过去,立即生效,这样就完成了最理想的在定时器运行中修改寄存器的过程。下面对应到stm32中:
有影子寄存器的寄存器实际上对应了两个寄存器:一个是用户可以写入或读出数据的寄存器,称为preload register(预装载寄存器),另一个是用户看不见的、但在操作中真正起作用的寄存器称为shadow register(影子寄存器)。我们修改的定时器周期、预分频系数、通道的比较值等都是修改的表面那个预装载寄存器,要让这个修改起作用,就还要把预装载寄存器的值赋给影子寄存器才行。
从ARR预装载寄存器传送到影子寄存器,有两种方式,一种是立刻更新,一种是等触发事件之后更新;这两种方式主要取决于寄存器TIMx->CR1中的“APRE”位:
APRE=0,当ARR值被修改时,同时马上更新影子寄存器的值;
APRE=1,当ARR值被修改时,必须在下一次事件UEV发生后才能更新影子寄存器的值;
这就是TIM_ARRPreloadConfig(TIM14, ENABLE);函数的作用,同理TIM_ARRPreloadConfig()的作用就是通过TIMX_CCMR寄存器的OC1PE位实现在下一个周期将写入预装载寄存器的值写入实际起作用的影子寄存器中。