(1)实验平台:
普中STM32精灵开发板https://item.taobao.com/item.htm?id=739076227953(2)资料下载:普中科技-各型号产品资料下载链接
上一章我们介绍了 STM32F1 的通用定时器,使用 TIM4 的更新溢出中断控制D1 指示灯闪烁。这一章我们来学习如何使用通用定时器产生PWM 输出。本章要实现的功能是:通过 TIM3 的通道 2 输出 PWM 信号,控制D8 指示灯的亮度。学习本章可以参考《STM32F10x 中文参考手册》-14 通用定时器章节。本章分为如下几部分内容:
23.1 PWM 简介
PWM 是 Pulse Width Modulation 的缩写,中文意思就是脉冲宽度调制,简称脉宽调制。它是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,其控制简单、灵活和动态响应好等优点而成为电力电子技术最广泛应用的控制方式,其应用领域包括测量,通信,功率控制与变换,电动机控制、伺服控制、调光、开关电源,甚至某些音频放大器,因此学习PWM 具有十分重要的现实意义。
其实我们也可以这样理解,PWM 是一种对模拟信号电平进行数字编码的方法。通过高分辨率计数器的使用,方波的占空比被调制用来对一个具体模拟信号的电平进行编码。PWM 信号仍然是数字的,因为在给定的任何时刻,满幅值的直流供电要么完全有(ON),要么完全无(OFF)。电压或电流源是以一种通(ON)或断(OFF)的重复脉冲序列被加到模拟负载上去的。通的时候即是直流供电被加到负载上的时候,断的时候即是供电被断开的时候。只要带宽足够,任何模拟值都可以使用 PWM 进行编码。
PWM 对应模拟信号的等效图,如下图所示:
从图中可以看到,上图 a 是一个正弦波即模拟信号,b 是一个数字脉冲波形即数字信号。我们知道在计算机系统中只能识别是 1 和0,对于STM32F1 芯片,要么输出高电平(3.3V),要么输出低电平(0),假如要输出1.5V 的电压,那么就必须通过相应的处理,比如本章所要讲解的 PWM 输出,其实从上图也可以看到,只要保证数字信号脉宽足够就可以使用 PWM 进行编码,从而输出1.5V 的电压。
23.1.1 STM32F1 PWM 介绍
STM32F1 除了基本定时器 TIM6 和 TIM7,其他定时器都可以产生PWM 输出。其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的PWM 输出。而通用定时器也能同时产生多达 4 路的 PWM 输出,这些在定时器中断章节中已经介绍过。
PWM 的输出其实就是对外输出脉宽可调(即占空比调节)的方波信号,信号频率是由自动重装寄存器 ARR 的值决定,占空比由比较寄存器CCR 的值决定。其示意图如下图所示:
从图中可以看到,PWM 输出频率是不变的,改变的是CCR 寄存器内的值,此值的改变将导致 PWM 输出信号占空比的改变。占空比其实就是一个周期内高电平时间与周期的比值。
PWM 输出比较模式总共有 8 种,具体由寄存器 CCMRx 的位OCxM[2:0]配置。我们这里只讲解最常用的两种 PWM 输出模式:PWM1 和PWM2,其他几种模式可以参考《STM32F10x 中文参考手册》13、14 定时器章节。
PWM1 和 PWM2 这两种模式用法差不多,区别之处就是输出电平的极性不同。如下图所示:
PWM 模式根据计数器 CNT 计数方式,可分为边沿对齐模式和中心对齐模式。
(1)PWM 边沿对齐模式
当 TIMx_CR1 寄存器中的 DIR 位为低时执行递增计数,计数器CNT 从0计数到自动重载值(TIMx_ARR 寄存器的内容),然后重新从0 开始计数并生成计数器上溢事件。
以 PWM 模式 1 为例。只要 TIMx_CNT < TIMx_CCRx,PWM 参考信号OCxREF便为有效的高电平,否则为无效的低电平。如果 TIMx_CCRx 中的比较值大于自动重载值(TIMx_ARR 中),则 OCxREF 保持为“ 1”。如果比较值为0,则OCxREF保持为“ 0”。如下图所示:
当 TIMx_CR1 寄存器中的 DIR 位为高时执行递减计数,计数器CNT 从自动重载值(TIMx_ARR 寄存器的内容)递减计数到 0,然后重新从TIMx_ARR 值开始计数并生成计数器下溢事件。
以 PWM 模式 1 为例。只要 TIMx_CNT >TIMx_CCRx,PWM 参考信号OCxREF便为无效的低电平,否则为有效的高电平。如果 TIMx_CCRx 中的比较值大于自动重载值(TIMx_ARR 中),则 OCxREF 保持为“ 1”。此模式下不能产生0%的PWM 波形。
(2)PWM 中心对齐模式
在中心对齐模式下,计数器 CNT 是工作在递增/递减模式下。开始的时候,计数器 CNT 从 0 开始计数到自动重载值减 1(ARR-1),生成计数器上溢事件;然后从自动重载值开始向下计数到 1 并生成计数器下溢事件。之后从0 开始重新计数。如下图所示:
我们以 ARR=8,CCRx=4 为例进行介绍。第一阶段计数器CNT 工作在递增计数方式,从 0 开始计数,当 TIMx_CNT < TIMx_CCRx 时,PWM 参考信号OCxREF为有效的高电平,当 TIMx_CNT >= TIMx_CCRx 时,PWM 参考信号OCxREF 为无效的低电平。第二阶段计数器 CNT 工作在递减计数方式,从ARR 开始递减计数,当TIMx_CNT > TIMx_CCRx 时,PWM 参考信号 OCxREF 为无效的低电平,当TIMx_CNT<= TIMx_CCRx 时,PWM 参考信号 OCxREF 为有效的高电平。
中心对齐模式又分为中心对齐模式 1/2/3 三种,具体由寄存器CR1位CMS[1:0]配置。具体的区别就是比较中断标志位 CCxIF 在何时置1:中心模式1在 CNT 递减计数的时候置 1,中心对齐模式 2 在 CNT 递增计数时置1,中心模式 3 在 CNT 递增和递减计数时都置 1。
上述涉及到的寄存器可以参考《STM32F10x 中文参考手册》-13、14、15定时器章节的寄存器部分,里面有详细寄存器功能介绍。如果看不懂的可以暂时放下,因为我们使用的是库函数开发。
23.2 通用定时器 PWM 输出配置步骤
接下来我们介绍下如何使用库函数对通用定时器的PWM 输出进行配置。这个也是在编写程序中必须要了解的。其实 PWM 输出和上一章一样也是通用定时器的一个功能,因此还是要用到定时器的相关配置函数,具体步骤如下:(定时器相关库函数在 stm32f10x_tim.c 和 stm32f10x_tim.h 文件中)
(1)使能定时器及端口时钟,并设置引脚复用器映射
因为 PWM 输出也是通用定时器的一个功能,所以需要使能相应定时器时钟。由于 PWM 输出通道是对应着 STM32F1 芯片的 IO 口,所以需要使能对应的端口时钟,并将对应 IO 口配置为复用输出功能。例如本章 PWM 呼吸灯实验,我们使用的是 TIM3 的 CH2 通道输出 PWM 信号,因此需要使能 TIM3 时钟,调用的库函数如下:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能 TIM3 时钟
而 TIM3 的 CH2 通道对应的管脚是 PA7,正好对应底板LED 模块的D8 指示灯。假如 LED 灯并没有接在 PA7 引脚上,如果要让这个通道映射到LED 所接的IO口上,则需要使用 GPIO 的复用功能重映射,在《STM32F1xx 中文参考手册》-8通用和复用功能 I/O(GPIO 和 AFIO)-8.3.7 定时器复用功能重映射章节都有介绍如下:
LED 模块的 D8 就是连接在 PA7 口的,所以可以无需重映射即可在PA7 输出PWM。如果使用到重映射功能,需要开启 AFIO 时钟,所以开启AFIO 时钟函数如下:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
最后还要记得将 PA7 管脚模式配置为复用推挽输出
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出
(2)初始化定时器参数,包含自动重装值,分频系数,计数方式等
要使用定时器功能,必须对定时器内相关参数初始化,其库函数如下:
void TIM_TimeBaseInit(TIM_TypeDef*TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
这个在定时器中断章节就已经介绍。
(3)初始化 PWM 输出参数,包含 PWM 模式、输出极性、使能等
初始化定时器后,需要设置对应通道 PWM 的输出参数,比如PWM 模式、输出极性、是否使能 PWM 输出等。PWM 通道设置函数如下:
void TIM_OCxInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
我们知道每个通用定时器有多达 4 路 PWM 输出通道,所以TIM_OCxInit 函数名中的 x 值可以为 1/2/3/4。函数的第一个参数相信大家一看就清楚,是用来选择定时器的。第二个参数是一个结构体指针变量,同样我们看下这个结构体TIM_OCInitTypeDef 成员变量:
typedef struct
{
uint16_t TIM_OCMode; //比较输出模式
uint16_t TIM_OutputState; //比较输出使能
uint16_t TIM_OutputNState; //比较互补输出使能
uint32_t TIM_Pulse; //脉冲宽度
uint16_t TIM_OCPolarity; //输出极性
uint16_t TIM_OCNPolarity; //互补比较输出极性
uint16_t TIM_OCIdleState; //空闲状态下比较输出状态
uint16_t TIM_OCNIdleState; //空闲状态下比较输出状态
}
这里我们就讲解下比较常用的 PWM 模式所需的成员变量:
TIM_OCMode:比较输出模式选择,总共有 8 种,最常用的是PWM1 和PWM2。
TIM_OutputState:比较输出使能,用来使能 PWM 输出到IO 口。
TIM_OCPolarity:输出极性,用来设定输出通道电平的极性,是高电平还是低电平。
结构体内其他的成员变量 TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState 是高级定时器才用到的。如大家使用到高级定时器,可以查看中文参考手册高级定时器章节。
所以如果我们要配置 TIM3 的 CH2 为 PWM1 模式,输出极性为低电平,并且使能 PWM 输出,可以如下配置:
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;
TIM_OC2Init(TIM3,&TIM_OCInitStructure); //输出比较通道 2 初始化
(4)开启定时器
前面几个步骤已经将定时器及 PWM 配置好,但 PWM 还不能正常使用,只有开启定时器了才能让它正常工作,开启定时器的库函数如下:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
第一个参数是用来选择定时器。
第二个参数是用来使能或者失能定时器,也就是开启或者关闭定时器功能。同样可以选择 ENABLE 和 DISABLE。
例如我们要开启 TIM3,那么调用此函数如下:
TIM_Cmd(TIM3,ENABLE); //开启定时器
(5)修改 TIMx_CCRx 的值控制占空比
其实经过前面几个步骤的配置,PWM 已经开始输出了,只是占空比和频率是固定的,例如本章要实现呼吸灯效果,那么就需要调节TIM3 通道2 的占空比,通过修改 TIM3_CCR2 值控制。调节占空比函数是:
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint32_t Compare1);
对于其他通道,分别有对应的函数名,函数格式是TIM_SetComparex(x=1/2/3/4)。
(6)使能 TIMx 在 CCRx 上的预装载寄存器
使能输出比较预装载库函数是:
void TIM_OCxPreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
第一个参数用于选择定时器,第二个参数用于选择使能还是失能输出比较预装载寄存器,可选择为 TIM_OCPreload_Enable、TIM_OCPreload_Disable。
(7)使能 TIMx 在 ARR 上的预装载寄存器允许位
使能 TIMx 在 ARR 上的预装载寄存器允许位库函数是:
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
第一个参数用于选择定时器,第二个参数用于选择使能还是失能。
将以上几步全部配置好后,我们就可以控制通用定时器相应的通道输出PWM波形了,这里要特别提醒下,虽然高级定时器和通用定时器类似,但是高级定时器要想输出 PWM 波形,必须要设置一个 MOE 位(TIMx_BDTR 的第15 位),以使能主输出,否则不会输出 PWM。库函数设置的函数为:
void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
23.3 硬件设计
本章硬件电路非常简单,只使用到开发板上的 LED(D8),因为D8 指示灯正好接在 PA7 管脚,而此管脚具有 TIM3_CH2 复用功能。所以可以通过TIM3 的CH2输出 PWM 信号,实现呼吸灯效果。
23.4 软件设计
本章所要实现的功能是:通过 TIM3 的 CH2 输出一个PWM 信号,控制D8 指示灯由暗变亮,再由亮变暗,类似呼吸效果。程序框架如下:
(1)初始化 PA7 管脚为 PWM 输出功能
(2)PWM 输出控制程序
在前面介绍通用定时器 PWM 配置步骤时,就已经讲解如何初始化PWM。下面我们打开“\4--实验程序\1--基础实验\18-PWM 呼吸灯实验”工程,在APP 工程组中可以看到添加了 pwm.c 文件,在 StdPeriph_Driver 工程组中添加了stm32f10x_tim.c 库文件。定时器操作的库函数都放在stm32f10x_tim.c和stm32f10x_tim.h 文件中,所以使用到定时器功能就必须加入stm32f10x_tim.c文件,同时还要包含对应的头文件路径。
23.4.1 TIM3 通道 2 的 PWM 初始化函数
要使用定时器的 PWM 输出功能,我们必须先对它进行配置。TIM3 通道2的PWM 初始化代码如下:
/*******************************************************************************
* 函 数 名 : TIM3_CH2_PWM_Init
* 函数功能 : TIM3通道2 PWM初始化函数
* 输 入 : per:重装载值
psc:分频系数
* 输 出 : 无
*******************************************************************************/
void TIM3_CH2_PWM_Init(u16 per,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
/* 开启时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);
/* 配置GPIO的模式和IO口 */
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AF_PP;//复用推挽输出
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_TimeBaseInitStructure.TIM_Period=per; //自动装载值
TIM_TimeBaseInitStructure.TIM_Prescaler=psc; //分频系数
TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; //设置向上计数模式
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);
TIM_OCInitStructure.TIM_OCMode=TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OCPolarity=TIM_OCPolarity_Low;
TIM_OCInitStructure.TIM_OutputState=TIM_OutputState_Enable;
TIM_OC2Init(TIM3,&TIM_OCInitStructure); //输出比较通道2初始化
TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable); //使能TIMx在 CCR2 上的预装载寄存器
TIM_ARRPreloadConfig(TIM3,ENABLE);//使能预装载寄存器
TIM_Cmd(TIM3,ENABLE); //使能定时器
}
在 TIM3_CH2_PWM_Init()函数中,首先使能 GPIOA 端口时钟和TIM3 时钟,其次将 PB5 管脚模式配置为复用推挽输出。然后配置定时器结构体TIM_TimeBaseInitStructure,初始化 PWM 输出参数,由于我们的LED 指示灯是低电平点亮,而我们希望当 CCR2 的值小的时候,LED 暗,CCR2 值大的时候,LED 亮,所以我们设置为 PWM1 模式,输出极性为低电平,使能PWM 输出。最后就是开启 TIM3。这一过程在前面步骤介绍中已经提了。程序中我们看到最后调用了 TIM_OC2PreloadConfig()和 TIM_ARRPreloadConfig,他们是用来使能TIM3在 CCR2 上的预装载寄存器和自动重装载寄存器,第一个库函数必须调用,第二个的话如果不调用也没有关系。
TIM3_CH2_PWM_Init()函数有两个参数,用来设置定时器的自动装载值和分频系数,方便大家修改 PWM 频率。
其实如果你会使用通用定时器 TIM3 的 CH2 输出 PWM,那么其他通用定时器通道都一样。
23.4.2 主函数
编写好 PWM 初始化函数后,接下来就可以编写主函数了,代码如下:
#include "system.h"
#include "SysTick.h"
#include "led.h"
#include "pwm.h"
int main()
{
u8 i=0;
u8 fx=0;
u16 j=0;
SysTick_Init(72);
LED_Init();
TIM3_CH2_PWM_Init(500,72-1); //频率是2Kh
while(1)
{
if(fx==0)
{
j++;
if(j==300)
fx=1;
}
else
{
j--;
if(j==0)
fx=0;
}
TIM_SetCompare2(TIM3,j); //i值最大可以取499,因为ARR最大值是499.
i++;
if(i%10==0)
LED0=!LED0;
delay_ms(10);
}
}
主函数实现的功能很简单,首先初始化对应的硬件端口时钟和IO 口,然后调用我们前面编写的 TIM3_CH2_PWM_Init 函数,这里我们设定定时器自动重装载值为 500,预分频系数为 72-1,定时周期即为 500us,频率即为2KHz,这里为什么减 1 在定时器中断已经介绍。
初始化后,定时器开始工作,PA7 开始输出 PWM 波形,频率为2K,你也可以修改这个频率值,但是要注意,不能将频率设置过大,否则会看到DS0 指示灯有明显的闪烁。通过变量 fx 控制 i 的方向,如果 fx=0,i 值累加,否则递减,然后将这个变化的 i 值传递给 TIM_SetCompare2 函数,这个函数功能是改变占空比的,因此可以实现 D8 指示灯亮度的调节,呈现呼吸灯的效果。程序中将i 值控制在 300 内,主要是因为 PWM 输出波形占空比达到这个值时,DS0 指示灯亮度变化就不明显了,而且我们在初始化定时器时,将自动重装载值设置为499,所以这个 i 值也不能超过。
23.5 实验现象
将工程程序编译后下载到开发板内,可以看到 D8 指示灯由暗变亮,再由亮变暗,呈现呼吸灯的效果。