跳转至

6.2 STM32模块开发-PWM

前言

这里主要讲解 Breeze Mini 的PWM输出功能模块,完整代码在工程目录下的 Modules 子目录下的 stm32f10x_module_motor.cstm32f10x_module_motor.h 中,该代码主要用于产生具有不同占空比的PWM信号来控制空心杯电机的速度大小。

相关知识

PWM简介

PWM(Pulse Width Modulation) 叫做 脉冲宽度调制,是一种利用数字输出来控制模拟电路的方法,简单来说就是对脉冲的宽度进行控制。PWM的一个优点是从处理器到被控系统信号都是数字形式的,无需进行 数模转换。让信号保持为数字形式可将噪声影响降到最小。噪声只有在强到足以将逻辑1改变为逻辑0或将逻辑0改变为逻辑1时,也才能对数字信号产生影响。其具体波形如下所示:

breeze_embedded_pwm_wave

占空比 是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,即下图中红框框选部分:

breeze_embedded_pwm_ccmr1_ocm

设置成 110111 的差别仅在于 会使得输出电平的极性相反。从上图还可以看出 存在 有效电平无效电平 两种电平标准,此时通过设置输出极性,来确定有效电平是高电平还是低电平。如下图所示:

breeze_embedded_pwm_ccer_cc1

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信号的,先看下图:

breeze_embedded_pwm_wave_theory

定时器重装载值为ARR,比较值CCRx,t时刻对计数器值和比较值进行比较,如果计数器值小于CCRx值,则输出低电平;如果计数器值大于CCRx值,则输出高电平。结合上图分析可知,在PWM的一个周期内:

  1. 定时器从0开始向上计数

  2. 在0-t1段,定时器计数器TIMx_CNT值小于CCRx值,输出低电平

  3. 在t1-t2段,定时器计数器TIMx_CNT值大于CCRx值,输出高电平

  4. 当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.cstm32f10x_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.0GPIOA.1GPIOB.0GPIOB.1 上,该部分原理图如下所示:

breeze_embedded_motor_sch

软件设计

完整代码在工程目录下的 Modules 子目录下的 stm32f10x_module_motor.cstm32f10x_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,且有:

CCRx = (TIM_{Period} + 1) * D

D为占空比,易知当D = 1时,CCRx取最大值1000,由上式可得到:

TIM_{Period} = 999

而PWM波形周期T的计算公式为:

T = \frac{(TIM_{Period} + 1) * (TIM_{Prescaler} + 1)}{TIM_{CLK}}

且当TIM_{Period} < 65535时,设置TIM_{Prescaler} = 2,而TIM_{CLK}表示定时器的时钟频率,由于设置不分频,则TIM2和TIM3的时钟频率和系统时钟频率相同,均为72MHz,也即:

TIM_{CLK} = 72000000

将得到的TIM_{Period} = 999TIM_{Prescaler} = 2TIM_{CLK} = 72000000代入到PWM波形计算公式解得:

T = 41.67us

故代码如下:

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);

参考