欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

第二部分 基础篇 - 第5章 PWM

程序员文章站 2022-06-08 20:39:32
...

5.1 PWM_输出

5.1.1 PWM输出的工作原理

脉冲宽度调制(PWM),是英文“ Pulse Width Modulation” 的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。
STM32 的定时器除了 TIM6 和 7。其他的定时器都可以用来产生 PWM 输出。其中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产生多达 4路的 PWM 输出,这样, STM32 最多可以同时产生 30 路 PWM 输出。这里我们仅利用 TIM3的 CH2 产生一路 PWM 输出。如果要产生多路输出,大家可以根据我们的代码稍作修改即可。

5.1.2 PWM输出的寄存器描述

同样,我们首先通过对 PWM 相关的寄存器进行讲解,大家了解了定时器 TIM3 的 PWM原理之后,我们再讲解怎么使用库函数产生 PWM 输出。
要使 STM32 的通用定时器 TIMx 产生 PWM 输出,除了上一章介绍的寄存器外,我们还会用到 3 个寄存器,来控制 PWM 的。这三个寄存器分别是:捕获 /比较模式寄存器( TIMx_CCMR1/2)、捕获/比较使能寄存器( TIMx_CCER)、捕获/比较寄存器( TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。
首先是捕获/比较模式寄存器( TIMx_CCMR1/2),该寄存器总共有 2 个, TIMx _CCMR1和 TIMx _CCMR2。 TIMx_CCMR1 控制 CH1 和 2,而 TIMx_CCMR2 控制 CH3 和 4。该寄存器的各位描述如图5-1。

第二部分 基础篇 - 第5章 PWM

图5-1 TIMx_CCMR1 寄存器各位描述

该寄存器的有些位在不同模式下,功能不一样,所以在图5-1 中,我们把寄存器分了 2层,上面一层对应输出而下面的则对应输入。关于该寄存器的详细说明,请参考《 STM32 参考手册》的14.4.7 一节。这里我们需要说明的是模式设置位 OCxM,此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是 PWM 模式,所以这 3 位必须设置为 110/111。这两种PWM 模式的区别就是输出电平的极性相反。
接下来,我们介绍捕获/比较使能寄存器( TIMx_CCER),该寄存器控制着各个输入输出通道的开关。该寄存器的各位描述如图5-2。
第二部分 基础篇 - 第5章 PWM

图5-2 TIMx_ CCER 寄存器各位描述

该寄存器比较简单, 我们这里只用到了 CC2E 位,该位是输入/捕获 2 输出使能位,要想PWM 从 IO 口输出,这个位必须设置为 1,所以我们需要设置该位为 1。该寄存器更详细的介绍了,请参考《 STM32 参考手册》的14.4.9 这一节。
最后,我们介绍一下捕获/比较寄存器( TIMx_CCR1~4),该寄存器总共有 4 个,对应 4 个输通道 CH1~4。因为这 4 个寄存器都差不多,我们仅以 TIMx_CCR1 为例介绍,该寄存器的各位描述如图5-3。
第二部分 基础篇 - 第5章 PWM

图5-3 寄存器 TIMx_ CCR1 各位描述

在输出模式下,该寄存器的值与 CNT 的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的输出脉宽了。 本章,我们使用的是 TIM3的通道 2,所以我们需要修改 TIM3_CCR2 以实现脉宽控制 DS0 的亮度。
我们要利用 TIM3 的 CH2 输出 PWM 来控制 DS0 的亮度,但是 TIM3_CH2 默认是接在 PA7上面的,而我们的 DS0 接在 PB5 上面,如果普通 MCU,可能就只能用飞线把 PA7 飞到 PB5上来实现了,不过,我们用的是 STM32,它比较高级,可以通过重映射功能,把 TIM3_CH2映射到 PB5 上。

图5-4 寄存器 AFIO_MAPR 各位描述

STM32 的重映射控制是由复用重映射和调试 IO 配置寄存器( AFIO_MAPR)控制的,该寄存器的各位描述如图5-4。
我们这里用到的是 TIM3 的重映射,从上图可以看出, TIM3_REMAP 是由[11:10]这 2 个位控制的。 TIM3_REMAP[1:0]重映射控制表如表5-1。
表5-1 TIM3_REMAP 重映射控制表

第二部分 基础篇 - 第5章 PWM

【注】重映像只适用于 64、 100 和 144 脚的封装。
默认条件下, TIM3_REMAP[1:0]为 00,是没有重映射的,所以 TIM3_CH1~TIM3_CH4 分别是接在 PA6、 PA7、 PB0 和 PB1 上的,而我们想让 TIM3_CH2 映射到 PB5 上, 则需要设置TIM3_REMAP[1:0]=10,即部分重映射,这里需要注意,此时 TIM3_CH1 也被映射到 PB4 上了。

5.1.3 PWM输出的具体代码分析

本章要实现通过TIM3实现四路方波的输出,由 以TIM3_CH1 输出 PWM 为例进行讲解。下面我们介绍通过库函数来配置该功能的步骤。
首先要提到的是,PWM 相关的函数设置在库函数文件 stm32f10x_tim.h和 stm32f10x_tim.c文件中。
1) 开启 TIM3 时钟以及GPIO的时钟,配置 PA6为复用输出。
要使用 TIM3,我们必须先开启 TIM3 的时钟,这点相信大家看了这么多代码,应该明白了。库函数使能 TIM3 及PA6时钟的方法是:

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); //使能定时器 3 时钟

这在前面一章已经提到过。 库函数设置 AFIO 时钟的方法是:
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //复用时钟使能
这两行代码很容易组织,这里不做过多重复的讲解。 设置 PA6为复用功能输出的方法在前面的几个实验都有类似的讲解,相信大家很明白,这里简单列出 GPIO 初始化的一行代码即可:

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出

2) 初始化 TIM3,设置 TIM3 的 ARR 和 PSC。
在开启了 TIM3 的时钟之后,我们要设置 ARR 和 PSC 两个寄存器的值来控制输出 PWM 的周期。当 PWM 周期太慢(低于 50Hz)的时候,我们就会明显感觉到闪烁了。因此, PWM 周期在这里不宜设置的太小。 这在库函数是通过 TIM_TimeBaseInit 函数实现的,在上一节定时器中断章节我们已经有讲解,这里就不详细讲解,调用的格式为:

TIM_TimeBaseStructure.TIM_Period = Arr; //设置自动重装载值
TIM_TimeBaseStructure.TIM_Prescaler =Psc; //设置预分频值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure); //根据指定的参数初始化 TIMx

3) 设置 TIM3_CH1的 PWM 模式,使能 TIM3 的 CH1 输出。
接下来,我们要设置 TIM3_CH1为 PWM 模式(默认是冻结的),在库函数中, PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的, 不同的通道的设置函数不一样, 这里我们使用的是通道1,我们直接来看看结构体 TIM_OCInitTypeDef的定义:

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;

这里我们讲解一下与我们要求相关的几个成员变量:
参数 TIM_OCMode 设置模式是 PWM 还是输出比较,这里我们是 PWM 模式。
参数 TIM_OutputState 用来设置比较输出使能,也就是使能 PWM 输出到端口。
参数 TIM_OCPolarity 用来设置极性是高还是低。
其他的参数 TIM_OutputNState, TIM_OCNPolarity, TIM_OCIdleState 和 TIM_OCNIdleState 是高级定时器 TIM1 和 TIM8 才用到的。
要实现我们上面提到的场景, 方法是:

    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;       //配置为PWM模式1
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;   
    //设置跳变值,当计数器计数到这个值时,电平发生跳变
TIM_OCInitStructure.TIM_Pulse = CCR1_Val; 
//当定时器计数值小于CCR1_Val时为高电平
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;  
    TIM_OC1Init(TIM3, &TIM_OCInitStructure);     //使能通道1
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);5) 使能 TIM3。
TIM_ARRPreloadConfig(TIM3, ENABLE);          // 使能TIM3重载寄存器

在完成以上设置了之后,我们需要使能 TIM3。 使能 TIM3 的方法前面已经讲解过:
TIM_Cmd(TIM3, ENABLE); //使能 TIM3
其余的代码笔者就不在这里列出来了,请参看本书的配套代码。

5.1.4 PWM输出的实验现象

现在,TIM3 的通道 1(PA.06)、2(PA.07)、3(PB.00)、4(PB.01)就会输出不同占空比的 PWM 信号了。PWM 信号可以通过示波器看到。考虑到并不是每个用户手头上都有示波器,我们在这里采用软件仿真的方式来验证我们的程序。
以前我们都是通过 J_LINK 直接将我们的代码烧到开发板的 Flash 中去调试,现在要换成软件仿真,得首先设置一下我们的开发环境,按照如下步骤所示。
1)点击 Target Options 选项图标,选中 Debug 选项卡,选中 Use Simulator 选项,按图中所示进行设置,然后点击“OK”按钮,见图5-5所示。

第二部分 基础篇 - 第5章 PWM

图5-5 模拟调试设置

2)点击 Start/Stop Debug Session 选项图标,点击 Analysis Windows ,弹出窗口后点击Setop…选项卡,在弹出的 Setup Logic Analysis 串口中点击 New(Insert)按钮,然后在文本栏里面分别输入:PORTA.6 、PORTA7、PORTB.0、PORTB.1,记住,是 New 一个就输入一个信号的 IO,输完之后需要再 New。对应的相关设置见图5-6。
第二部分 基础篇 - 第5章 PWM

图5-6端口输出配置

需要注意的是Display Range的设置,默认的设置是看不到波形的。
3)设置完信号源之后,点击 RUN 按钮,仿真信号即出来了,当信号出来之后,可点击STOP 按钮,让信号不再变化,方便观察。其中 In、Out、All 这三个按钮可以调节显示信号的疏密程度,见图5-7。
第二部分 基础篇 - 第5章 PWM

图5-7波形输出

其中 Cursor 选项可以帮助我们测量信号的时间差,Amplitute 则可以帮助我们测量信号的幅值。
有条件的朋友还可以用示波器查看波形。

5.2呼吸灯

5.2.1呼吸灯的工作原理

呼吸灯,就是指灯光设备的亮度随着时间由暗到亮逐渐增强,再由亮到暗逐渐衰减,很有节奏感地一起一伏,就像是在呼吸一样,因而被广泛应用于手机、电脑等电子设备的指示灯中。冰冷的电子设备应用呼吸灯后,顿时增添了几分温暖。
要使用数字器件控制灯光的强弱,我们很自然就想到 PWM(脉冲宽度调制)技术。假如以LED 作为灯光设备,且由控制器输出的 PWM 信号可以直接驱动 LED,PWM 信号中的低电平可点亮 LED 灯。当 LED 以较高的频率进行开关(亮灭)切换时,由于视觉暂留效应,人眼是看不到 LED 灯的闪烁现象的,反映到人眼中能感觉到的是亮度的差别。即以一定的时间长度为周期,LED 灯亮的平均时间越长,亮度就越高,反之越暗。因此,我们可以使用高频率的 PWM 信号,通过调制信号的占空比,控制 LED 灯的亮度。
那么具体我们应该控制 LED 灯以怎样的亮度曲线变化能够达到最好的效果呢?亮度随着时间逐渐变强再衰减,可以用两种常见的数学函数表示,分别是半个周期的正弦函数与指数上升曲线及其对称得到的下降曲线。

第二部分 基础篇 - 第5章 PWM

图5-8正弦曲线与指数曲线

相对来说,使用下凹函数曲线灯光处于暗的状态更长,所以指数函数的曲线更符合我们呼吸灯的亮度变化要求。
接下来就要确定呼吸灯的呼吸频率(即一个亮度起伏过程)。据统计,成人的一个呼吸周期为 3 秒钟,即吸气时间(亮度上升时间) 1.5 秒,呼气时间(亮度衰减时间)1.5 秒。我们使用定时器即可精确控制它的呼吸频率,当然,读者想把呼吸灯的频率调快或慢一点都是可以的,呼吸周期为 3 秒钟只是一个参考值。

5.2.2呼吸灯的具体代码分析

  1. 生成指数曲线 PWM 数据
    要实现 LED 亮度随着指数曲线变化,我们需要使用占空比呈指数曲线变化的 PWM 信号,而这样的信号由定时器经过查表产生。这个表的数据存储在程序中的数组 indexWave中。
    第二部分 基础篇 - 第5章 PWM

    图5-9 LED 变化曲线
uint8_t indexWave[] = {1,1,2,2,3,4,6,8,10,14,19,25,33,44,59,80,
    107,143,191,255,255,191,143,107,80,59,44,33,25,19,14,10,8,6,4,3,2,2,1,1};

这个表有 40 个数字,从图中可以看到这些数字呈指数上升再衰减,正好是呼吸灯的一个控制周期。数字的大小范围是 0~255,即把 LED 的亮度分为了 0~255 个等级。
假如我们把定时器的脉冲计数器 TIMx_CNT 上限设置为 255,把这个表的数据一个一个地赋值到定时器的比较寄存器 TIMx_CCR 中,那么在每个 PWM 周期中,当 TIMx_CNT的计数值小于比较寄存器 TIMx_CCR 的值时, 就会在通道中输出低电平,点亮 LED,而随着 TIMx_CCR 的值由 LED 亮度表得来,所以 LED 点亮的时间就会呈图中的曲线变化,实现呼吸灯的功能。
这个表的数据是使用 matlab 软件生成的。该代码运行后会生成一个“index_wave.c”的文件,用户把该文件中的数据复制到工程中的数组中即可。

%本代码用于产生呼吸灯使用的指数函数数据
clear;

x = [0 : 8/19 : 8];       %设置序列 ,指数上升
up = 2.^x ;               %求上升指数序列  
up = uint8(up);           %化8位数据

y = [8: -8/19 :0];       %设置序列 ,指数下降
down = 2.^y ;            %求下降指数序列
down = uint8(down);      %化8位数据

line = [[0:8/19:8],[8:8/19:16]]         %拼接序列
val = [up , down]                       %拼接输出序列

dlmwrite('index_wave.c',val);       %输出到文件index_wave.c
plot(line,val,'.');                 %显示波形图
  1. 初始化 GPIO
    下面分析具体的定时器配置代码。本实验使用 PB0 作为定时器 PWM 输出通道,先对它进行初始化。作 PWM 输出通道的引脚需要被配置为复用推挽输出模式。
static void TIM3_GPIO_Config(void) 
{
  GPIO_InitTypeDef GPIO_InitStructure;

  /* GPIOB clock enable */
  RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); 
  /* 配置呼吸灯用到的PB0引脚 */
  GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_0 ;
  GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;           // 复用推挽输出
  GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  GPIO_Init(GPIOB, &GPIO_InitStructure);
}
  1. 配置定时器模式
    在 TIM3_Mode_Config 函数中,完成了呼吸灯所需要的定时器 PWM 输出模式配置。
static void TIM3_Mode_Config(void)
{
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
    TIM_OCInitTypeDef  TIM_OCInitStructure;                                                                             

    /* 设置TIM3CLK 时钟为72MHZ */
  RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);  //使能TIM3时钟

  /* 基本定时器配置 */      
  TIM_TimeBaseStructure.TIM_Period = 255; //当定时器从0计数到255,即为266次,为一个定时周期
  TIM_TimeBaseStructure.TIM_Prescaler = 1999;       //设置预分频:
//设置时钟分频系数:不分频(这里用不到)
  TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1 ;          
  TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;   //向上计数模式
  TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);

  /* PWM模式配置 */
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;    //配置为PWM模式1
  TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //使能输出
  TIM_OCInitStructure.TIM_Pulse = 0;                                                

//设置初始PWM脉冲宽度为0 
//当定时器计数值小于CCR1_Val时为低电平
  TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;        

  TIM_OC3Init(TIM3, &TIM_OCInitStructure);          //使能通道3
  TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);     //使能预装载 
  TIM_ARRPreloadConfig(TIM3, ENABLE);                                                   //使能TIM3重载寄存器ARR

  /* TIM3 enable counter */
  TIM_Cmd(TIM3, ENABLE);                                                       
//使能定时器3    
TIM_ITConfig(TIM3,TIM_IT_Update, ENABLE);       //使能update中断    
NVIC_Config_PWM();  //配置中断优先级       
}

这个定时器的模式配置主要分为三个部分,分别为时基初始化,输出模式初始化和中断配置。
 时基初始化
代码中前10行是定时器的时基初始化,这部分主要负责配置定时器的定时周期、时钟频率、计数方式等。它使用到库函数 TIM_TimeBaseInit,利用结构体TIM_TimeBaseInitTypeDef 进行配置,该结构体有以下成员:
1) TIM_Period
定时周期,实质是存储到重载寄存器 TIMx_ARR 的数值,脉冲计数器从 0 累加到这个值上溢或从这个值自减至 0 下溢。这个数值加 1 然后乘以时钟源周期就是实际定时周期。
本实验中向该成员赋值为 255,即定时周期为(255+1)* T ,T 为定时器的时钟周期。
2) TIM_Prescaler
对定时器时钟 TIMxCLK 的预分频值,分频后作为脉冲计数器 TIMx_CNT 的驱动时钟,得到脉冲计数器的时钟频率为:fCK_CNT=fTIMx_CLK/(N+1),其中 N 为即为赋给本成员的时钟分频值。
本实验给 TIM_Prescaler 成员赋值为 1999,即对时钟 2000 分频,所以定时器的时钟周期 T 为 2000/72000000。
注:本实验中的 TIM_Prescaler 的值在下一小节给出具体说明。
3) TIM_ClockDivision
时钟分频因子。怎么又出现一个配置时钟分频的呢?要注意这个TIM_ClockDivision 和上面的 TIM_Prescaler 是不一样的。TIM_Prescaler 预分频配置是对TIMxCLK进行分频,分频后的时钟被输出到脉冲计数器TIMx_CNT ,而TIM_ClockDivision 虽然也是对 TIMxCLK 进行分频,但它分频后的时钟频率为 fDTS,是被输出到定时器的 ETRP 数字滤波器部分,会影响滤波器的采样频率。TIM_ClockDivision 可以被配置为 1 分频(fDTS=fTIMxCLK)、2 分频及 4 分频。ETRP 数字滤波器的作用是对外部时钟 TIMxETR 进行滤波。
本实验中是使用内部时钟 TIMxCLK 作为定时器时钟源的,没有进行滤波所以配置TIM_ClockDivision 为任何数值都没有影响。
4) TIM_CounterMode
本成员配置的为脉冲计数器 TIMx_CNT 的计数模式,分别为向上计数,向下计数,及*对齐模式。向上计数即 TIMxCNT 从 0 向上累加到 TIM_Period 中的值,(重载寄存器 TIMx_ARR 的值),产生上溢事件;向下计数则 TIMxCNT 从 TIM_Period 的值累减至0,产生下溢事件。而*对齐模式则为向上、向下计数的合体,TIMxCNT 从 0 累加到TIM_Period 的值减 1 时,产生一个上溢事件,然后向下计数到 1 时,产生一个计数器下溢事件,再从 0 开始重新计数。
本实验中 TIM_CounterMode 成员被赋值为 TIM_CounterMode_Up(向上计数模式)。填充完配置参数后,调用库函数 TIM_TimeBaseInit()把这些控制参数写到寄存器中,定时器的时基配置就完成了。
 输出模式配置
在本函数代码的后面是关于定时器的输出模式配置的。通用定时器的输出模式由 TIM_OCInitTypeDef 类型结构体的以下几个成员来设置:
1) TIM_OCMode
输出模式配置,主要使用的为 PWM1 和 PWM2 模式。PWM1 模式是:在向上计数时,当 TIMx_CNT

static void NVIC_Config_PWM(void)
{
  NVIC_InitTypeDef NVIC_InitStructure;

  /* Configure one bit for preemption priority */
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);

  /* 配置TIM3_IRQ中断为中断源 */
  NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

配置好中断,下面就要编写中断服务函数。

void TIM3_IRQHandler(void)
{   
    static uint8_t pwm_index = 0;           //用于PWM查表
    static uint8_t period_cnt = 0;      //用于计算周期数

    if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  //TIM_IT_Update
    {           
            period_cnt++;
            if(period_cnt >= 10)    //若输出的周期数大于10,输出下一种脉冲宽的PWM波
            {
                //根据PWM表修改定时器的比较寄存器值
                TIM3->CCR3 = indexWave[pwm_index];  
                pwm_index++;        //标志PWM表的下一个元素
               //若PWM脉冲表已经输出完成一遍,重置PWM查表标志
                if( pwm_index >=  40)   
                {
                    pwm_index=0;                                
                }

                period_cnt=0;       //重置周期计数标志
        }
        TIM_ClearITPendingBit (TIM3, TIM_IT_Update);    //必须要清除中断标志位
    }
}

本中断服务函数在每次定时器更新事件发生时执行一次(即 256 个定时器时钟周期)。函数中使用了静态变量 pwm_index 和 period_cnt,它们分别用来查找 PWM 表元素和记录同样占空比的脉冲输出了多少次。
本代码的目的是每 10 次定时器中断更新一次 PWM 表中的数据到比较寄存器TIMx_CCR 中,当遍历完 PWM 表的 40 个元素时,再重头开始遍历 PWM 表,周而复始,重复 LED 的呼吸过程。

整个呼吸过程的时间计算方法如下:
因为定时器的 TIM_Prescaler 设置为 1999;
所以定时器的时钟频率:fTIM = 72000000/(TIM_Prescaler+1) = 36000 Hz
即定时器的时钟周期为:tTIM = 1/fTIM = 1/36000 s
因为定时器的 TIM_Period 设置为 255;
所以定时器的中断周期为:tint= tTIM * (TIM_Period+1) =0.0071 s
因为 PWM 表有 pwm_index = 40 个亮度占空比数据,同种占空比信号输出 period_cnt =10 次
所以一个呼吸周期 T = tint *40 *10 = 2.844 s
这个呼吸周期长度和前面提到的 3s 近似,下载测试,实验效果很好。
有读者问,为什么要在中断服务函数中设置一个变量 period_cnt 让同种占空比数据输出10 次呢?如果把定时器的 TIM_Prescaler 设置成 19999(原来的 10 倍),然后每种占空比信号由原来的 10 次改为 1 次,那么总的呼吸周期还是一样的 2.844s ,为什么不采用这样看起来更简单的方案? 野火曾测试过,后一种方案的实验现象是 LED 会出现闪烁,原因就让读者亲自去思考吧。
以上是最基本的PWM输出调制实现呼吸灯,笔者在者例还要讲解一下重映射的输出配置。在这里讲解的是通过重映射 TIM3_CH2到 PB5 上,由 TIM3_CH2 输出 PWM 来控制LED的亮度。下面我们介绍通过库函数来配置该功能的步骤。
1) 开启 TIM3 时钟以及复用功能时钟,配置 PB5 为复用输出。
要使用 TIM3,我们必须先开启 TIM3 的时钟,这点相信大家看了这么多代码,应该明白了。这里我们还要配置 PB5 为复用输出,这是因为 TIM3_CH2 通道将重映射到 PB5 上,此时, PB5属于复用功能输出。在此只列出库函数设置 AFIO 时钟的方法。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //复用时钟使能
其余的和前面的配置一样,就不再列出了。
2) 设置 TIM3_CH2 重映射到 PB5 上。
因为 TIM3_CH2 默认是接在 PA7 上的,所以我们需要设置 TIM3_REMAP 为部分重映射(通过 AFIO_MAPR 配置),让 TIM3_CH2 重映射到 PB5 上面。 在库函数函数里面设置重映射的函数是:
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);
在前面已经讲解过, STM32 重映射只能重映射到特定的端口。 第一个入口参数可以理解为设置重映射的类型,比如 TIM3 部分重映射入口参数为GPIO_PartialRemap_TIM3,这点可以顾名思义了。 所以 TIM3 部分重映射的库函数实现方法是:
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3, ENABLE);
3) 初始化 TIM3,设置 TIM3 的 ARR 和 PSC。
4) 设置 TIM3_CH2 的 PWM 模式,使能 TIM3 的 CH2 输出。
在库函数中, PWM 通道设置是通过函数 TIM_OC1Init()~TIM_OC4Init()来设置的, 不同的通道的设置函数不一样, 这里我们使用的是通道 2,所以使用的函数是 TIM_OC2Init()。
void TIM_OC2Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
5) 使能 TIM3。
在完成以上设置了之后,我们需要使能 TIM3。 使能 TIM3 的方法前面已经讲解过:
TIM_Cmd(TIM3, ENABLE); //使能 TIM3
6) 修改 TIM3_CCR2 来控制占空比。
最后,在经过以上设置之后, PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改 TIM3_CCR2 则可以控制 CH2 的输出占空比。继而控制LED的亮度。在库函数中,修改 TIM3_CCR2 占空比的函数是:
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
理所当然,对于其他通道,分别有一个函数名字, 函数格式为 TIM_SetComparex(x=1,2,3,4)。通过以上 6 个步骤,我们就可以控制 TIM3 的 CH2 输出 PWM 波了。笔者在这里指列出了关键的代码,与前文中PWM输出配置相同的笔者就没有在这里列出。

本章参考代码

IAR版本完整代码
KEIL版本完整代码

相关标签: PWM STM32