6.2 STM32模块开发-PWM
前言¶
这里主要讲解 Breeze Mini 的PWM输出功能模块,完整代码在工程目录下的 Modules 子目录下的 stm32f10x_module_motor.c 和 stm32f10x_module_motor.h 中,该代码主要用于产生具有不同占空比的PWM信号来控制空心杯电机的速度大小。
相关知识¶
PWM简介¶
PWM(Pulse Width Modulation) 叫做 脉冲宽度调制,是一种利用数字输出来控制模拟电路的方法,简单来说就是对脉冲的宽度进行控制。PWM的一个优点是从处理器到被控系统信号都是数字形式的,无需进行 数模转换。让信号保持为数字形式可将噪声影响降到最小。噪声只有在强到足以将逻辑1改变为逻辑0或将逻辑0改变为逻辑1时,也才能对数字信号产生影响。其具体波形如下所示:
占空比 是PWM中非常重要的参数,其定义为上图中 "可变的脉冲宽度" 与 "恒定的脉冲周期" 的比值。例如占空比为50%即高电平占整个周期时间的一半。本质上我们所调节的PWM的占空比,在非常短的时间(一个周期)内,让IO口一段时间输出高电平,一段时间输出低电平,由于时间非常短,当输出低电平的时候电机还没来得及停止转动,这时候高电平又来了,结果就是电机比正常(占空比为1)转的时候要慢,同理如果让占空比越大电机速度就越快,占空比为1时速度最快,为0时电机停止转动。
在PWM调速系统中,一般可以采用 定宽调频、调宽调频 和 定频调宽 3种方法来改变脉冲的占空比,但是前两种方法在调速时改变了控制脉宽的周期,从而引起控制脉冲频率的改变,当该频率与系统的固有频率接近时将会引起系统振荡。所以常采用 定频调宽法 来改变占空比。
定频调宽调速 是在脉冲波形的频率不变的前提下(脉冲波形的周期不变),通过改变一个周期波形内高电平的时间从而改变占空比,进而由 等效原理 可知在一个脉冲周期中的平均电压也会发生改变,使得电机转速发生变化。假定电机始终接通电源时,电机最大转速为Vmax, 占空比为D = t / T, 则电机的平均速度Vd = D * Vmax。由公式可知当改变占空比D = t /T 时,就可以得到不同的电机平均速度Vd,从而达到调速的目的。
STM32的PWM外设¶
STM32的PWM输出信号都是通过定时器产生的,除了TIM6和TIM7,其他的定时器都能够产生PWM输出。其中高级定时器TIM1能够产生最多7路的PWM信号输出,通用定时器能够产生最多4路的PWM信号。为了能够对STM32的PWM信号的产生有更深入地理解,这里先介绍与PWM有关的寄存器。
由于STM32的PWM信号是通过定时器产生的,所以如果要使用通用定时器TIM来产生PWM输出,除了要了解 STM32驱动开发-通用与高级定时器 中所讲解的寄存器外,还要介绍3个寄存器,这3个寄存器分别是 捕获/比较模式寄存器(TIMx_CCMR[1...2])、捕获/比较使能寄存器(TIMx_CCER) 和 捕获/比较寄存器(TIMx_CCR[1...4])。
TIMx_CCMR寄存器¶
该寄存器共有2个,分别为TIMx_CCMR1和TIMx_CCMR2。其中TIMx_CCMR1控制通道1和2(CH1, CH2),TIMx_CCMR2控制通道3和4(CH3, CH4),下图为该寄存器的各位描述:
该寄存器的有些位在不同模式下的功能是不一样的,对该寄存器的详细说明,可以参考 STM32寄存器手册 第288页,14.4.7一节。这里要额外说明的是 模式设置位OCxM,该设置位由3个二进制位组成,所以一共可以配置成7种模式,我们这里使用的是PWM模式,所以需要设置成 110 或者 111,即下图中红框框选部分:
设置成 110 和 111 的差别仅在于 会使得输出电平的极性相反。从上图还可以看出 存在 有效电平 和 无效电平 两种电平标准,此时通过设置输出极性,来确定有效电平是高电平还是低电平。如下图所示:
当 CC1 位0时,有效电平为 高电平,否则为 低电平。
TIMx_CCER寄存器¶
该寄存器控制各个输入输出通道的开关,其各位描述如下图所示:
该寄存器使用起来比较简单,想要定时器从第几个通道输出PWM信号,就置第几个CC[1...4]E为1,比如想要从通道2输出PWM信号,就需要将 CC2E 置为1,更加详细的用法可以参考 STM32寄存器手册 第292页,14.4.9一节。
TIMx_CCR寄存器¶
TIMx_CCR一共有4个,分别对应4路输出通道,该寄存器的各位描述为:
在输出模式下,该寄存器的值会与CNT的值进行比较,根据比较结果来产生相应的动作。修改该寄存器的值就可以控制PWM的输出脉宽。
STM32产生PWM过程¶
在前面对每个相关寄存器进行单独讲解后,现以 "向上计数模式" 来讲解上述寄存器是如何相互配合进而产生出PWM信号的,先看下图:
定时器重装载值为ARR,比较值CCRx,t时刻对计数器值和比较值进行比较,如果计数器值小于CCRx值,则输出低电平;如果计数器值大于CCRx值,则输出高电平。结合上图分析可知,在PWM的一个周期内:
-
定时器从0开始向上计数
-
在0-t1段,定时器计数器TIMx_CNT值小于CCRx值,输出低电平
-
在t1-t2段,定时器计数器TIMx_CNT值大于CCRx值,输出高电平
-
当TIMx_CNT值达到ARR时,定时器溢出,重新向上计数...循环此过程
至此一个PWM周期完成,同时也可以知道ARR的值决定 PWM周期(在时钟频率一定的情况下,当前为默认内部时钟CK_INT) ,CCRx的值决定 PWM占空比(高低电平所占整个周期比例)。
另外若定时器配置成 PWM1模式(即当CNT<CCRx时输出有效电平),此时若要产生上图这种波形,需要设置 有效电平 为低电平,即置CCER寄存器的CCx为1。若配置成 PWM2模式(即当CNT<CCRx时输出无效电平),此时若要产生上图这种波形,需要设置 有效电平 为高电平,即置CCER寄存器的CCx为0。总结来说就是 工作模式 和 输出极性 决定了波形输出是 "前高后低" 型的,还是 "前低后高" 型的。
库函数配置PWM¶
PWM相关的库函数在 FWLib 目录下的 stm32f10x_tim.c 和 stm32f10x_tim.h 中。这里以定时器的TIM3通道3输出PWM信号为例介绍使用固件库产生PWM的方法。首先像前面介绍的代码一样要先使能对应APB总线下的外设时钟(TIM3是APB1总线下的外设):
1 | RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); |
然后设置输出的GPIO口,由于TIM3的通道3在默认GPIOB的第0口输出,故代码如下:
1 2 3 4 5 6 | GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); |
接着是设置TIM3的ARR和PSC两个寄存器的值来控制生成的PWM信号的周期,这一步实际上就是配置产生PWM信号的TIM3定时器的定时溢出周期,这个在前面介绍定时器时已经介绍过了,这里就直接给出代码:
1 2 3 4 5 | TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); |
接着需要设置TIM3_CH3为PWM输出模式(默认),这里官方库提供 TIM_OC1Init()~TIM_OC4Init() 4个函数来分别独立设置4个通道的工作模式,这里使用的是通道3,所以使用的函数是:
1 | void TIM_OC3Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); |
这种初始化外设函数的声明结构已经多次出现了,想必读者已经非常熟悉了,TIM_OCInitTypeDef 的类型定义为:
1 2 3 4 5 6 7 8 9 10 11 | typedef struct { uint16_t TIM_OCMode; uint16_t TIM_OutputState; uint16_t TIM_OutputNState; uint16_t TIM_Pulse; uint16_t TIM_OCPolarity; uint16_t TIM_OCNPolarity; uint16_t TIM_OCIdleState; uint16_t TIM_OCNIdleState; } TIM_OCInitTypeDef; |
这里主要介绍3个会用到的成员变量:TIM_OCMode 用来设置工作模式是 PWM 还是 输出比较。TIM_OutputState 用来使能PWM输出到端口,TIM_OCPolarity 用来设置输出极性的高低。其他的成员变量只有高级定时器才会用到(TIM3是通用定时器)。这样要实现功能的初始化代码为:
1 2 3 4 5 | TIM_OCStructInit(&TIM_OCInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; |
最后只需要使能TIM3即可:
1 | TIM_Cmd(TIM3, ENABLE); |
虽然此时PWM已经开始输出了,但是信号的占空比和频率都是固定的,如果想要在初始化之后修改PWM的占空比,可以通过修改TIM3_CCR3寄存器的值来实现。官方库提供修改TIM_CCR3的函数为:
1 | void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3); |
这样就可以控制TIM3的通道3输出PWM信号了。
硬件连接¶
Breeze Mini 硬件上4个电机控制信号线分别连接在 GPIOA.0、GPIOA.1、GPIOB.0 和 GPIOB.1 上,该部分原理图如下所示:
软件设计¶
完整代码在工程目录下的 Modules 子目录下的 stm32f10x_module_motor.c 和 stm32f10x_module_motor.h 中。
PWM初始化¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | void Motor_Init(void) { u16 prescaler = (u16)(SystemCoreClock / 24000000) - 1; GPIO_InitTypeDef GPIO_InitStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOB, &GPIO_InitStructure); TIM_DeInit(TIM2); TIM_DeInit(TIM3); TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = prescaler; TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); TIM_OCStructInit(&TIM_OCInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC2Init(TIM2, &TIM_OCInitStructure); TIM_OC3Init(TIM3, &TIM_OCInitStructure); TIM_OC4Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_Cmd(TIM2, ENABLE); TIM_Cmd(TIM3, ENABLE); } |
现在对上述代码进行分拆讲解,首先该代码定义了相关外设初始化结构体变量:
1 2 3 4 | u16 prescaler = (u16)(SystemCoreClock / 24000000) - 1; GPIO_InitTypeDef GPIO_InitStructure; //PWM外设输出口初始化变量 TIM_OCInitTypeDef TIM_OCInitStructure; //定时器输出比较初始化变量 TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; //定时器定时初始化变量 |
接着由于 Breeze Mini 的4路PWM输出分别使用的是TIM2的通道1和2,TIM3的通道3和4,GPIO口使用了GPIOA和GPIOB的各两个端口,所以使能APB总线外设时钟的代码为:
1 2 | RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); |
按照PWM输出要求,GPIO口的输出模式要设置成 GPIO_Mode_AF_PP(复用推挽输出)模式,另外输出的GPIO口分别为PA0、PA1、PB0和PB1,所以代码如下:
1 2 3 4 5 | GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_Init(GPIOB, &GPIO_InitStructure); |
为了能够重新配置定时器,先复位TIM2和TIM3的配置:
1 2 | TIM_DeInit(TIM2); TIM_DeInit(TIM3); |
接下来设置定时器产生PWM波形的周期和计数模式,由于设计的 Breeze Mini PWM的设置值,也就是CCRx值的范围为100~1000,且有:
D为占空比,易知当D = 1时,CCRx取最大值1000,由上式可得到:
而PWM波形周期T的计算公式为:
且当TIM_{Period} < 65535时,设置TIM_{Prescaler} = 2,而TIM_{CLK}表示定时器的时钟频率,由于设置不分频,则TIM2和TIM3的时钟频率和系统时钟频率相同,均为72MHz,也即:
将得到的TIM_{Period} = 999、TIM_{Prescaler} = 2、TIM_{CLK} = 72000000代入到PWM波形计算公式解得:
故代码如下:
1 2 3 4 5 6 | TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = prescaler; //prescaler=2 TIM_TimeBaseStructure.TIM_ClockDivision = 0; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数 TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); |
接着设置比较输出的模式,由于要求生成的PWM波形为 "前高后低" 型,采用的是 PWM1模式(TIM_OCMode_PWM1) 的话,需要设置输出极性为 有效电平为高电平(TIM_OCPolarity_High),故代码如下:
1 2 3 4 5 | TIM_OCStructInit(&TIM_OCInitStructure); TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; |
接着初始化各通道的 输出比较模式 并 使能输出比较的预加载功能:
1 2 3 4 5 6 7 8 9 | TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC2Init(TIM2, &TIM_OCInitStructure); TIM_OC3Init(TIM3, &TIM_OCInitStructure); TIM_OC4Init(TIM3, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable); TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable); |
最后使能TIM2和TIM3来产生PWM波形:
1 2 | TIM_Cmd(TIM2, ENABLE); TIM_Cmd(TIM3, ENABLE); |