先说在开头啊,我们学习定时器总感觉它是很难的,这里我就不说他的编程难度,而是对于它的理解难度。学习定时器你就必须了解他的来龙去脉。
比如说你现在要使用一个定时器,那么先要决定你要用哪一个定时器,是高级定时器还是通用定时器呢?假如你要用高级定时器(TIM1 、TIM8),那他和通用定时器有啥区别呢?在程序中我们如何来体现他们的区别呢?第二个你要关心的就是你使用的定时器是多少位的,一般有16位和32位区分。比如TIM1就是16位,那么他的最大计数个数就是2^16=65536-1,
也就是说你的TIM_TimeBaseStructure.TIM_Period = X ;其中X最大值为65535,你不能高于它。但是如果你使用TIM2就是32位,那么他的最大计数个数就是2^32=4294967296-1,也就是说你的TIM_TimeBaseStructure.TIM_Period = X ;其中X最大值为4294967295,你不能高于它。 第三就是你要清楚你使用的这个定时器的时钟来源是哪里,一般来说我们高级定时器(TIM1、TIM8)的时钟来源是来自AHB2,你可以这样记:高级定时器的时钟频率肯定高于通用定时器的时钟频率对吧,又因为2>1,所以高级定时器的时钟来源是来自AHB2对吧,那么通用定时器的时钟源就来自AHB1了对吧。
在这里还要注意的是STM32中除非APB1的时钟分频数设置为1,否则通用定时器TIMx的时钟是 APB1时钟的2倍,当 APB1 的时钟不分频的时候,通用定时器 TIMx 的时钟就等于APB1 的时钟。这也就解释了下面我使用TIM3是通用定时器,挂载在 APB1上,而 APB1是36MHz,通用定时器TIMx的时钟是 APB1时钟的2倍,所以TIM3的定时器是72MHZ 计算。只有把这些该清楚了才能续的下去,当然远不止这一下。
上一章我们说到了定时器,这一章称热打铁热说一说脉冲宽度调制-PWM。
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度进行控制。
看到这个图,我先想聪明的你马上会想到,这个玩意一定有两个寄存器ARR和CRRx,而这两个值一定是需要自己设置的,图上都标出来了,对吧。
我们假定定时器工作在向上计数 PWM模式,且当 CNT<CCRx 时,输出 0,当 CNT>=CCRx 时输出 1。那么就可以得到如上的 PWM示意图:
- 当 CNT 值小于 CCRx 的时候,IO 输出低电平(0),
- 当 CNT 值大于等于 CCRx 的时候,IO 输出高电平(1),
- 当 CNT 达到 ARR 值的时候,重新归零,然后重新向上计数,依次循环。
改变 CCRx 的值,就可以改变 PWM 输出的占空比,改变 ARR 的值,就可以改变 PWM 输出的频率,这就是 PWM 输出的原理。所以说对于PWM我们只要在程序中修改这两个值就可以了,其他的都一样。
STM32F429 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。
直接开搞,来看一下定时器的分类,这是F4的定时器分配图
这里还要知道一个概念就是通道:
通道是什么,我们换个说法,通道=路=引脚,这样是不是好理解一点,也就是说TIM2有4路定时器。TIM5也有4路,TIM3也有4路,TIM4也有4路。为什么要这么多路呢?
比如我们要产生8路周期,占空比都不同的PWM信号输出,那我们可以选TIM2的 CH1/CH2/CH3/CH4 还有TIM3 的CH1,CH2,CH3,CH4 这8路进行输出,需要这么多路,就是为了可以输出/输入 更多的信号。比如你用小舵机做一个小机器人就需要很多的定时器对应很多的通道也就是对应很多引脚。输出的信号分别是哪些管脚呢。很明显对F4来说就是
TIM2的 CH1/CH2/CH3/CH4 对应 PA5 /PA1/PA2/PA3 这4个管脚。
TIM5的 CH1/CH2/CH3/CH4 对应 PH10/PH11/PH12/PIO 这4个管脚。
要使 STM32F429 的通用定时器 TIMx 产生 PWM 输出,除了上一章介绍的寄存器外,我们还会用到4个寄存器,来控制 PWM 的。这三个寄存器分别是:
捕获/比较模式寄存器(TIMx_CCMR1/2)
捕获/比较使能寄存器(TIMx_CCER)
捕获/比较寄存器(TIMx_CCR1~4)
刹车和死区寄存器(TIMx_BDTR)这个寄存器一般在高级定时器中使用,就是TIM1和TIM8。如果你没用到这两个定时器就不要管这个寄存器了。脉冲宽度调制模式可以生成一个信号,该信号频率由 TIMx_ARR 寄存器值决定,其占空比则由 TIMx_CCRx 寄存器值决定。
寄存器讲解
捕获/比较模式寄存器(TIMx_CCMR1/2)
该寄存器一般有 2 个:TIMx _CCMR1和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和CH2,而 TIMx_CCMR2 控制 CH3 和 CH4
这个寄存器我们关心的是 位14 13 12 和 位 9 8,其他的默认就可以了。
模式设置位 OC4M,此部分由 3位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,所以这 3 位必须设置为 110/111。这两种 PWM 模式的区别就是输出电平的极性相反。另外 CC4S 用于设置通道的方向(输入/输出)默认设置为00,就是设置通道作为输出使用。
捕获/比较使能寄存器(TIM3_CCER)
使能寄存器见名知意,当然是使能TIM3定时器的啊。使CC4E置位为1就可以了。
捕获/比较寄存器(TIMx_CCR1~4)
该寄存器总共有 4 个,对应 4 个通道 CH1~4。我们使用的是通道3。在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。
程序代码配置
我们文本的例子是定时器3的通道3,对应PB0管脚。
1.开启TIM3时钟,配置 PB0
要使用 TIM3,我们必须先开启 TIM3的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置 PB0 为复用输出(当然还要时能 GPIOB的时钟),这是因为 TIM3_CH3通道将使用 PB0的复用功能作为输出,我们配置 PB0为复用输出,才可以实现 TIM3_CH3的 PWM 经过 PB0输出。
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能TIM外设时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE); //使能GPIO外设时钟
//设置该引脚为复用输出功能,输出TIM3 CH3的PWM脉冲波形
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //TIM3_CH3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
2.设置 TIM3的ARR和PSC
在开启了 TIM3的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来控制输出 PWM 的周期。这在库函数是通过 TIM_TimeBaseInit 函数实现的,在上一节定时器中断章节已经有讲解过,这里就不详细讲解,调用的格式为:
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 100;
TIM_TimeBaseStructure.TIM_Prescaler =360-1;
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
对于上面的参数解释一下:
①频率:我们使用的APB1时钟源是72MHz的,在此我们不做分频,通过配置相关的参数来设置输入频率,计算方法:输入频率=APB1时钟/(预分频系数+1)=72 000 000Hz/360=200 000Hz =200KHZ。
②TIM_TImeBaseStructure.TIM_Period参数决定了输出PWM波形的频率,输出PWM波形的频率=定时器的输入频率/TIM_TImeBaseStructure.TIM_Period,这里我们设置设置的周期为200 000Hz/100=2000Hz,即0.5ms一个周期。
如果你想修改你的输出PWM波形的频率按我上面自己配置即可。
3.设置TIM3_CH3的PWM模式及通道方向, 使能 TIM3的CH3输出
接下来,我们要设置 TIM3_CH3为 PWM 模式,我们要通过配置 TIM3_CCMR2的相关位来控制 TIM3_CH3 的模式。在库函数中,PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的,不同的通道的设置函数不一样,这里我们使用的是通道 3,所以使用的函数是 TIM_OC3Init()。这里的结构体我就不多少了,自己看一下就可以明白,对于普通定时器我们只需要设置下面这四个参数就可以了。
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = 500;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC3Init(TIM3, &TIM_OCInitStructure); //根据TIM_OCInitStruct中指定的参数初始化外设TIMx
③配置占空比:占空比=配置占空比的值/ TIM_TImeBaseStructure.TIM_Period,这里的占空比为50/100=50% 。50%的占空比那么波形也一定是一半高一半低对吧,一会我们仿真看一看。
4使能TIM3
所有的都配置完了,最后能一下TIM3。
TIM_Cmd(TIM3, ENABLE);//使能TIM3
5.使能基本定时器3的预装载值。
使能TIM3在CCR3上的预装载寄存器
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); ////使能TIM3在CCR3上的预装载寄存器,即TIM3_CCR3的预装载值在更新事件到来时才能被传送至当前寄存器中。
TIM_ARRPreloadConfig(TIM3, ENABLE); //使能TIM3在ARR上的预装载寄存器 (影子寄存器)
这里我们来讲一下影子寄存器:
1.有影子寄存器的有3个:分频寄存器PSC,自动重装载寄存器 ARR,自动捕获CCRx(x是对应的通道),注意,PSC,ARR,CCRx不是影子寄存器,而是它们对应的“预装载寄存器”;
2、影子寄存器才是真正起作用的寄存器,但是ST没有提供这个寄存器出来,只是提供出与之相对应的预装载寄存器,分别为“PSC,ARR,CCRx”
3、我们用户能接触到,能修改或读取的都是预装载寄存器,ST只是把它们开放出来(影子寄存器并没有开放给用户),其实就是ARR寄存器,如:TIM3->ARR
4、从预装载寄存器ARR传送到影子寄存器,有两种方式,一种是立刻更新,一种是等触发事件之后更新;这两种方式主要取决于寄存器TIMx->CR1中的“APRE”位;
- APRE=0,当ARR值被修改时,同时马上更新影子寄存器的值;
- APRE=1,当ARR值被修改时,必须在下一次事件发生后才能更新影子寄存器的值;
5、怎么样马上立刻更改影子寄存器的值,而不是下一个事件;方法如下:
- 将ARPE=0,TIM_ARRPreloadConfig(TIM3, DISABLE );
- 在ARPE=1,TIM_ARRPreloadConfig(TIM3, ENABLE);
TIM_ARRPreloadConfig设置为DISABLE 和ENABLE的问题,他的作用只是允许或禁止在定时器工作时向ARR的缓冲器中写入新值,以便在更新事件发生时载入覆盖以前的值。在开始初始化的时候你已经把" TIM_TimeBaseStructure.TIM_Period=100; //ARR的值 ",后来也一直是这个值,原因是你没有编写中断服务函数或者你在中断服务函数中根本就没有给ARR缓冲器重新写入新值,所以设置为DISABLE 和ENABLE都没有影响,但是最保险的方法就是是能一下。
汇总一下代码:
void TIM3_PWM_Init()
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能TIM时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB , ENABLE);//使能GPIO外设时钟
//设置该引脚为复用输出功能,输出TIM3 CH3的PWM脉冲波形
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //TIM3_CH3
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
TIM_TimeBaseStructure.TIM_Period = 100; //输出PWM波形的频率=定时器的输入频率/TIM_TImeBaseStructure.TIM_Period,200 000Hz/100=2000Hz,即0.5ms一个周期
TIM_TimeBaseStructure.TIM_Prescaler =360-1;//不分频 输入频率=APB1时钟/(预分频系数+1)=72 000 000Hz/360=200 000Hz =200K
TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
TIM_OCInitStructure.TIM_Pulse = 50; //占空比=配置占空比的值/TIM_TImeBaseStructure.TIM_Period,500/1000=50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //输出极性:TIM输出比较极性高
TIM_OC3Init(TIM3, &TIM_OCInitStructure);
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); //CH3预装载使能
TIM_ARRPreloadConfig(TIM3, ENABLE); //使能TIM3在ARR上的预装载寄存器 (影子寄存器)
TIM_Cmd(TIM3, ENABLE); //使能TIM3
}
6.主函数的编写
主函数就很简单,一个while函数就可以解决。
int main(void)
{
SystemInit ();
TIM3_PWM_Init();
while(1)
{
}
}
根据而上一章讲到的方法keil中调试一下,可以清楚地看到周期是0.5ms,跟我们上面设置的一样。占空比也是50%,就是1/2。
当然我们这里设置的PWM波是死的,也就是说是固定的50%。那么这实际项目中我们一般是要求可变的PWM,最典型的就是控制电机的转速。举一个例子,下面是电机的驱动芯片就是TB6612的管脚分配图。
TB6612引脚分配:
VM PWMA--------->TIM3_CH3(PBO)
VCC AIN2--------->GPIOB_12
GND AIN1--------->GPIOB_13
AO1 STBY--------->GPIOB_14
AO2 BIN1--------->GPIOB_15
BO2 BIN2--------->GPIOA_12
BO1 PWMB--------->TIM3_CH4(PB1)
在这里我们用GPIOB_12和GPIOB_13的电平高低来控制电机的前进后退,具体的电平高低要看真值表。用TIM3_CH3(PB0)来控制电机的转速。设置不同的值,电机的转速就是有快有慢。那么用哪一个函数来控制呢?
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
理所当然,对于其他通道,分别有一个函数名字,函数格式为 TIM_SetComparex(x=1,2,3,4)。现在我们就可以控制 TIM3 的 CH3 输出 PWM 波了。也就是一个轮子的转速快慢了。这里我用一个小小的if语句来使定时器3的通道3的值发生变化。
int main(void)
{
u16 i=0;
SystemInit ();
TIM3_PWM_Init();
while(1)
{
i++;
TIM_SetCompare3(TIM3,i); //定时器3的通道3
if(i==100) i=0;
}
}
下面看一下效果:
可以明显的看到PWM波在不断地变化,那就对应电机转速的变化或者小灯的明暗程度。
至此我们就明白是STM32是如何输出PWM的。这里我们来总结一下:
我们输出PWM用到了定时器3,用TIM_Period 和TIM_Prescaler来确定输出PWM的频率和周期.用TIM_Pulse来确定占空比。
如果你单纯的使用定时器3做定时功能那么TIM_Period 和TIM_Prescaler来确定需要定时的时间,也就是多长时间进入一次中断服务函数执行相应的指令。只是在不同的地方叫法不同,本质还是一样的。
聪明的你会了吗?