一,知识理论基础
什么是呼吸灯:
顾名思义,就是一个灯。灯的亮度的变化,由亮变暗,从暗变亮,有一个渐变,规律的变化,像是人的呼吸,是灯的呼吸,所以叫呼吸灯。而要让灯可以达到这样的变化,我们要让stm32的IO口上输出一个可调的电平,这时我们就要用到PWM,那什么是PWM呢,我们继续往下看。
什么是定时器:
讲PWM我们要先认识stm32的定时器,PWM是定时器的功能之一。STM32F103有TIME1和TIME8高级定时器,TIME2TIME5通用定时器,还有TIME6和TIME7基本定时器。我们要使用的STM32F103C8T6只具有4个定时器,TIME1TIME4.
那么定时器有什么功能呢?定时、输出比较、输入捕获、互补输出,其中,基本定时器就只有定时功能,通用定时器便除了互补输出没有其他都有,而高级定时器便是全都有啦,我们这里用到通用定时器TIM2。
通用定时器具体的功能有:
在这里我们要用到TIM2_CH2的PWM输出功能。
那么什么是PWM呢?
脉冲宽度调制(PWM),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制。
简单点说就是一个可调的脉冲,控制在一个周期内,控制高电平多长时间,低电平多长时间(占空比),从而实现电平的输出。经常用于舵机、电机控制等。。。
两个重要的概念,频率、占空比:
频率是指每秒钟信号从高电平到低电平再回到高电平的次数,为一个PWM波周期的倒数。
占空比是指高电平持续时间比一个周期持续的时间。所以可以通过控制占空比(我们要编程的“数”),来控制输出的等效电压。
对于方波(pwm输出的就是方形波)的话,频率和占空比就确定了一个波。
为了不至于太难理解,我们不进行深讲,但是建议大家可以去CSDN,百度等等平台进行全面一点的认知,对我们下学期的智能车比赛做基础知识储备。
**二,**硬件连接
具有定时器功能的引脚:
LED连接:
我们用到TIM2_CH2,自己实操时可以换一个以达到更好的学习效果。通过图二,我们在默认情况下(即不使用端口映射)TIM2_CH2对应的IO口是PA1,我们将PWM输出极性设置为高,便将LED的正极接到PA1上,负极接GND,(若将输出极性设置成低那就反过来接,将负极接到IO口,,正极接5V)
三,软件编程
首先我们在工程中HARDWARE文件夹下新建PWM文件夹并新建PWM.c PWM.h两个文件,导入mdk5,具体操作省略,可以看前边推文。我们将PWM的初始化函数写到PWM.c的文件中函数命名为“TIM2_PWM_Init”(可以随意命名)。
我们先从简单的讲起,PWM.h头文件没什么重点,如下:
#ifndef __PWM_H
#define __PWM_H
#include "sys.h" //导入头文件
void TIM2_PWM_Init(u16 arr,u16 psc); //函数声明
#endif
这里要说的是因为用到了u16 的数据类型定义我们要导入一个头文件“sys.h”(u8,u16,u32都是C语言数据类型,分别代表8位,16位,32位长度的数据类型,这里也可以直接调用"stm32f10x.h")
接下来是编写PWM.c文件,编写初始化 “void TIM2_PWM_Init(u16 arr,u16 psc);”函数,函数参数为arr重装载值决定pwm的频率周期,psc是时钟预分频数(主要用于计算时间范围为0-65534),这里有一条公式可以计算周期时间Tout= (arr+1)*(psc+1) /Tclk,其中Tclk我们用的TIM2是系统内部APB1时钟倍频来的,(固件库的SystemInit函数里面已经初始化APB1的时钟为2分频,所以APB1的时钟为36M,而从STM32的内部时钟树图得知:当APB1的时钟分频数为1的时候,TIM27的时钟为APB1的时钟,而如果APB1的时钟分频数不为1,那么TIM27的时钟频率将为APB1时钟的两倍。因此,TIM2的时钟为72M,即 Tclk=72M)
接下来我们先说说PWM的模式,PWM有两个模式,PWM1和PWM2,PWM1是当我们设定的值比arr值小时输出高电平,PWM2是当我们设定的值比arr值大时输出高电平。如下图就是PWM2模式。
我们从图出发,可以看到为什么说ARR值决定周期,定时器从0开始计数(这里是向上计数模式,向下计数则相反,也是上边公式为什么要+1),数到ARR时产生溢出(更新)事件(可以从这个地方设置中断,本次用不到中断,不进行讲解),重新回到0 ,这便是一个周期,我们要设置的值便是图中CCRx,这个值会跟ARR进行比较(所以叫输出比较),通过模式设定决定输出高低电平。(为了不至于太难理解请一定结合上图一起看)。我们先看看完整的代码,然后一个一个函数讲PWM.c
#include "PWM.h"
void TIM2_PWM_Init(u16 arr,u16 psc)
{
//结构体变量定义
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
GPIO_InitTypeDef GPIO_InitStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
//时钟使能 TIM2 、GPIOA、 AFIO ①
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //使能TIM2挂载在APB1上的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA、复用功能时钟AFIO
//TIM2定时器初始化 ②
TIM_TimeBaseInitStruct.TIM_Period=arr; //重装载值arr
TIM_TimeBaseInitStruct.TIM_Prescaler=psc; //预分频值psc
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStruct.TIM_ClockDivision=0; //时钟分割为0 ,TDTS = Tck_tim
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
//TIM2定时器使能
TIM_Cmd(TIM2,ENABLE);
//TIM2通道2初始化 ③
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High; //高电平有效
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable; //输出比较使能
TIM_OC2Init(TIM2,&TIM_OCInitStruct);
//TIM2通道2预装载寄存器使能
TIM_OC2PreloadConfig(TIM2,TIM_OCPreload_Enable);
//GPIO PA1初始化 ④
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1; //PA.1
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; //50MHz速度
GPIO_Init(GPIOA,&GPIO_InitStruct);
}
首先我们先总结一下初始化pwm输出的编程步骤:
步骤介绍
使能时钟
初始化定时器
初始化定时器通道
初始化GPIO
现在我们一个点一个点的讲解:
使能时钟,这里 GPIO挂载在APB2总线上,之前文章说过,而我们要用到的TIM2是挂载在APB1上的,所以我们要使能的时钟是RCC_APB1,这里要注意的是通用定时器是挂载在APB1上,高级定时器则是在APB2上。【补充:时钟函数的申明在stm32f10x_rcc.h,这里是上一讲写少了的】
这里我们要写的代码是:
//时钟使能 TIM2 、GPIOA、 AFIO ①
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //使能TIM2挂载在APB1上的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA
初始化定时器,初始化定时器跟初始化GPIO的操作类似,我们先看看要用到的函数【定时器相关函数申明在文件stm32f10x_time.h中】:
这里函数的两个参数一个是TIMx ,x可以是2,3,4,说明这个初始化函数只适用在通用定时器初始化上,第二个参数是一个结构体变量,里边的成员有:
typedef struct
{
uint16_t TIM_Prescaler; //预分频值
uint16_t TIM_CounterMode; //计数模式
uint16_t TIM_Period; // 重装载值
uint16_t TIM_ClockDivision; //时间分割
uint8_t TIM_RepetitionCounter; //重复计数,就是重复溢出多少次才给你来一个溢出中断,如果初始化为0的话,计数器溢出一次,中断一次!
} TIM_TimeBaseInitTypeDef;
其中预分频值跟重装载值上边讲过了,计数模式有向上计数、向下计数、中央对齐模式(中央对齐模式有模式1、2、3),这里我们用到向上计数模式,对于向上计数模式在上边有讲过了,便是从0计数到ARR重装载值,而向下计数的话便是从ARR计数到0。时间分割主要是用于数字滤波器相关,我们在此用不到只要设置为0就好了,重复计数模式,在这里我们用不到,上边注释有稍微讲了一下,可以自行再了解一下。所以这里我们要设置参数如下:
TIM_TimeBaseInitStruct.TIM_Period=arr; //重装载值arr
TIM_TimeBaseInitStruct.TIM_Prescaler=psc; //预分频值psc
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStruct.TIM_ClockDivision=0; //时钟分割为0 ,TDTS = Tck_tim
ARR值与psc值我们作为参数,在调用时再进行设置。所以完整的初始化函数
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //定义结构体变量
TIM_TimeBaseInitStruct.TIM_Period=arr; //重装载值arr
TIM_TimeBaseInitStruct.TIM_Prescaler=psc; //预分频值psc
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInitStruct.TIM_ClockDivision=0; //时钟分割为0 ,TDTS = Tck_tim
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStruct);
之后我们要对定时器使能,使用void TIM_Cmd();(省略参数)
同样,这个函数适用于通用定时器,使用比较简单,如下:
TIM_Cmd(TIM2,ENABLE);
这里第2步就写完了。
初始化定时器通道,通用定时器有4个通道上边图3有进行讲解,这里我们要用到是通道2即TIM2_CH2;每一个定时器通道都有单独的初始化函数。
一样是有两个参数,一个是定时器TIMx(同样是只适用通用定时器2 3 4),一个是结构体变量,我们看看结构体变量里的成员。
typedef struct
{
uint16_t TIM_OCMode; //输出模式
uint16_t TIM_OutputState; //输出比较使能位
uint16_t TIM_OutputNState; //高级定时器输出比较N状态
uint16_t TIM_Pulse; //比较值(图9 CCRx)
uint16_t TIM_OCPolarity; //输出比较极性
uint16_t TIM_OCNPolarity; //高级定时器输出比较N极性
uint16_t TIM_OCIdleState; //设置高级定时器空闲状态
uint16_t TIM_OCNIdleState; //设置高级定时器N空闲状态
} TIM_OCInitTypeDef;
我们用到的是通用定时器所以不用看那些高级定时器才能用的参数,所以这里我们只要设置4个参数就可以了。首先第一个输出模式。
这里我们用到PWM模式1,PWM模式2上边有讲到,至于其他的模式在此不叫讲述,可以自行百度。
TIM_OCMode= TIM_OCMode_PWM1;
第二个TIM_OutputState这个是使能位,我们选择使能就好了
TIM_OutputState=TIM_OutputState_Enable;
第三个是输出极性,也就是我们要的是高电平有效还是低电平有效,这个跟我们LED引脚连接相关,这里我们选择高电平有效,LED的连接上我们将正极接到GPIO口上;
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;
第四个是比较值,我们在后边主函数会用另一个函数直接设置,这个数也就是我们图9CCRx对应的那个值,也可以称之为占空比,这里我们不用设置;
所以我们通道2初始化结构体的参数设置是:
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;
这里我们还需要通过void TIM_OC2PreloadConfig();(省略参数)这个函数来使能通道2上的预装载寄存器
他有两个参数,一个设置是哪个通用定时器,一个是使能,比较简单,这里直接设置:
TIM_OC2PreloadConfig(TIM2,TIM_OCPreload_Enable);
那么我们通道2初始化步骤完整的代码如下:
TIM_OCInitTypeDef TIM_OCInitStruct; //定义结构体变量
//TIM2通道2初始化 ③
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High; //高电平有效
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable; //输出比较使能
TIM_OC2Init(TIM2,&TIM_OCInitStruct);
//TIM2通道2预装载寄存器使能
TIM_OC2PreloadConfig(TIM2,TIM_OCPreload_Enable);
GPIO初始化,这里上一篇已经讲过了,不过这里要注意的是我们使用的是复用推挽输出模式,这个是有固定要求的,可以查阅《stm32中文参考手册》
那么GPIO初始化代码如下【补充:GPIO系列函数申明在文件stm32f10x_gpio.h中】:
GPIO_InitTypeDef GPIO_InitStruct;
//GPIO PA1初始化 ④
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP; //复用推挽输出
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_1; //PA.1
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; //50MHz速度
GPIO_Init(GPIOA,&GPIO_InitStruct);
综上,PWM.c文件中的PWM初始化函数就写好了,我们接着写主函数main.c,先看完整代码:
#include "stm32f10x.h"
#include "delay.h"
#include "PWM.h"
int main(void)
{
int ledpwm=0; //定义占空比变量
TIM2_PWM_Init(899,0); //初始化PWM ARR=899;PSC=0
delay_init(); //初始化延时函数
while(1)
{
delay_ms(5); //稳定pwm波
for(ledpwm =0; ledpwm <=255; ledpwm ++) //从0到255一个个加
{
TIM_SetCompare2(TIM2, ledpwm); //设置TIM2_CH2占空比
delay_ms(10); //延时10ms
}
for(ledpwm =255; ledpwm >=0; ledpwm --) //从255到0,一个个减
{
TIM_SetCompare2(TIM2, ledpwm); //设置TIM2_CH2占空比
delay_ms(10); //延时10ms
}
}
}
导入PWM.h头文件,然后初始化pwm,arr=899,psc=0;初始化延时函数,然后通过for循环从0到255计数,这个相信有点C语言基础的都没问题,然后是一个新函数,void TIM_SetCompare2();设置通道2捕获比较寄存器的值。
两个参数,一个是哪个通用定时器,一个是比较寄存器的值,比较简单,如下
TIM_SetCompare2(TIM2, ledpwm);
然后这里为什么是255呢,这个值是可以计算的,LED的最大亮度对应的电压通过占空比计算出对应数值就好了,再大的数值对LED的亮度也就没用了,亮度最大了,还可能烧坏LED。
Stm32的高电平 是5v 我们设置的ARR值是899,那么最大就是899,假设我们设置的比较值是450,那没就是50%的输出电平也就是2.5v,以此计算。
完整文件PWM.h PWM.c main.c就这三个文件要写,写好了编译烧写就可以了,在自己动手实操一遍后建议换一个定时器和通道再操作一遍,会更加熟练。
四,烧写验证
话不多说,上图(家里没示波器,我用软件调试来查看输出的波形)
可以看到波形从小到大再到小(可以通过图片下边的时间结合波形宽度看出),再看看LED的变化:
可以看到LED渐渐从亮到暗再到亮,说明我们实验结果完美达标。