1.知识理论基础
什么是呼吸灯:
顾名思义,它是一盏灯。灯的亮度由亮到暗、由暗到亮的变化,有逐渐的、有规律的变化,就像人的呼吸或灯的呼吸一样,所以称为呼吸灯。为了让灯实现这样的变化,我们需要让stm32的IO口输出一个可调的电平。这时候我们就需要用到PWM了。那么什么是脉宽调制呢?我们继续往下看。
什么是定时器:
要谈PWM,首先要了解stm32定时器。 PWM是定时器的功能之一。 STM32F103有TIME1和TIME8高级定时器,TIME2TIME5通用定时器,TIME6和TIME7基本定时器。我们要使用的STM32F103C8T6只有4个定时器,TIME1TIME4。
那么定时器的作用是什么呢?定时、输出比较、输入捕获、互补输出。其中,基本定时器只有定时功能,通用定时器只有互补输出,高级定时器则全部都有。我们这里使用的是通用定时器。 TIM2。
通用定时器的具体功能是:
这里我们需要用到TIM2_CH2的PWM输出功能。
那么什么是脉宽调制呢?
脉宽调制(PWM),英文“Pulse Width Modulation”的缩写,简称脉冲宽度调制,是一种利用微处理器的数字输出来控制模拟电路的非常有效的技术。简单来说,就是脉冲宽度的控制。
简单来说,就是一个可调脉冲,控制在一个周期内,控制高电平持续多长时间和低电平持续多长时间(占空比),从而实现电平输出。常用于舵机、电机控制等。
两个重要的概念,频率和占空比:
频率是指信号每秒从高电平到低电平再回到高电平的次数,是一个PWM波周期的倒数。
占空比是指高电平持续时间与一个周期持续时间之比。因此,可以通过控制占空比(我们要编程的“数字”)来控制输出的等效电压。
对于方波(PWM输出是方波),频率和占空比决定了波。
为了不让理解太困难,我们就不深入了,但是建议大家去CSDN、百度等平台有一个更全面的了解,为我们下学期的智能汽车比赛准备基础知识。
**二、**硬件连接
引脚:具有定时器功能
LED 连接:
我们使用TIM2_CH2,您可以在自己的练习中更改它以达到更好的学习效果。通过图2,默认情况下(即没有进行端口映射),TIM2_CH2对应的IO端口为PA1。我们将PWM输出极性设置为高,然后将LED的正极连接到PA1,负极连接到GND。 (如果设置输出极性为低,则反接,负极接IO口,正极接5V)
3、软件编程
首先我们在工程的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-分别为bit、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,其中我们使用的TIM2 Tclk乘以系统内部APB1时钟,(固件库的SystemInit函数已经将APB1的时钟初始化为2分频,所以APB1的时钟为36M。从STM32的内部时钟树图我们知道:当APB1 的时钟分频为1 时,TIM27 的时钟为APB1 时钟,如果APB1 的时钟分频数不为1,则TIM27 的时钟频率将是APB1 时钟的两倍,因此,TIM2 的时钟为72M,即Tclk=72M)
接下来我们先来说说PWM模式。 PWM 有两种模式:PWM1 和PWM2。当我们设置的值小于arr值时,PWM1输出高电平。当我们设置的值大于arr值时,PWM2输出高电平。输出高电平。下图为PWM2模式。
从图中我们可以看出为什么ARR值决定了周期。定时器从0开始计数(这里是向上计数方式,向下计数则相反,这也是上面公式需要+1的原因)。当ARR计数时,发生溢出(Update)事件(可以从这里设置中断,这次没有使用中断,不再解释)。返回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_ APB1PeriphClockC md(RCC_APB1Periph_TIM2,启用); //使能APB1上挂载的TIM2的时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //使能GPIOA和复用功能时钟AFIO //TIM2定时器初始化TIM_TimeBaseInitStruct.TIM_Period=arr; //重新加载值arrTIM_TimeBaseInitStruct.TIM_Prescaler=psc; //预分频值pscTIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式TIM_TimeBaseInitStruct.TIM_ClockDivision=0; //时钟分频为0,TDTS=Tck_timTIM_TimeBaseInit(TIM2,TIM_TimeBaseInitStruct); //TIM2定时器使能TIM_Cm d( TIM2,ENABLE);//TIM2通道2初始化TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM1; //PWM模式1TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High; //高电平有效TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable; //输出比较使能TIM_OC 2Init(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.1GPIO_InitStruct. GPIO_速度=GPIO_速度_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); //使能APB1上挂载的TIM2时钟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; //重新加载值arrTIM_TimeBaseInitStruct.TIM_Prescaler=psc; //预分频值pscTIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up; //向上计数模式TIM_TimeBaseInitStruct.TIM_ClockDivision=0; //时钟分频为0,TDTS=Tck_timARR 我们以value和psc值作为参数,调用时设置。这样就完成了初始化函数
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct; //定义结构体变量TIM_TimeBaseInitStruct.TIM_Period=arr; //重新加载值arrTIM_TimeBaseInitStruct.TIM_Prescaler=psc; //预分频值pscTIM_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,启用);到这里步骤2就完成了。
初始化定时器通道。通用定时器有4 个通道,如图3 所示。这里我们将使用通道2,TIM2_CH2;每个定时器通道都有一个单独的初始化函数。
有两个参数,一个是定时器TIMx(同样只适用于通用定时器2 3 4),另一个是结构体变量。我们来看看结构体变量中的成员。
typedef 结构{ 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;第四是比较值。后面我们会用另一个函数直接在main函数中设置。这个数字就是我们图9中的CCRx对应的值。它也可以称为占空比。我们这里不使用它。设置;
所以我们的通道2初始化结构体的参数设置为:
Tim_ocinitStruct.tim_ocmode=tim_ocmode_pwm1; tim_ocinitstructructructrucstructructure=tim_ocpolity_high; tim_ocinitstrut.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模式1TIM_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.1GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz; //50MHz速度GPIO_In it(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=0delay_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 ;初始化delay函数,然后通过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 只需要写在这三个文件中即可。编写完成后,进行编译和编程。自己做完后,建议更换定时器和频道再操作。你会更加熟练。
4.烧写验证
话不多说,上图(我家里没有示波器,所以用软件调试来查看输出波形)
可以看到波形由小变大,然后又变小(可以看到图片底部的时间结合波形宽度),然后看LED的变化:
可以看到LED逐渐由亮到暗再亮,说明我们的实验结果完全符合标准。