通用定时器_输出比较介绍及案例实践
引言
经过前面对通用定时器的整体介绍,我们对其功能和结构有了一个初步的了解,接下来,我们深入学习一下通用定时器相对基本定时器新增的几个功能的原理,并通过基础案例进行练习。
首先,我们来学习通用定时器的输出比较功能,并以LED呼吸灯的案例进行练习。
一、PWM介绍
1.1 PWM概念
由于实际输出比较功能主要就是用来生成pwm波形的,因此在学习输出比较之前,我们先了解一下什么是PWM。
PWM,即脉冲宽度调制(Pulse-Width Modulation),简单来说就是可以产生一段高低电平占比不同的周期性信号,也就是方波,然后通过调整高电平所占一个周期的比例对模拟电路进行控制的技术。
为什么说是对模拟电路进行控制?因为PWM信号实际上他是个数字信号0或1构成的一段方波信号,对于模拟电路或者单片机中的电压来说最高就是3.3V或者5V,最低就是0V,而通过PWM波可以实现控制单片机或模拟电路对外输出0-3.3V或5V之间的电压,因此我们说:PWM是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。
而其中的原理其实就是面积等效原理。比如我现在有一段pwm信号,也就是一段方波信号,如下图所示。
如上图,假如现在输出了一段PWM方波信号如上图所示,然后一个周期中处于高电平的部分大约只有整个周期的1/3,而高电平时输出的电压是3.3V左右,低电平输出的认为是0V,每一个周期的波形都长这样,那么我们可以认为:这一段连续输出的方波信号就可以等效看成1.1V左右的电压输出,换句话说,每个周期的高电平与低电平进行了“面积”上的等效一样,因为整个输出的不是一直都为3.3V,也不是一直是0V,因此输出的电压近似就是处于0~3.3V之间的,只要输出的频率够高,就可以等效近似为一段稳定的处于0到3.3V之间的电压输出了。
因此,按照上述解释,我们可以感受到,适合使用PWM去控制的电路中的负载一定是具有“惯性”的,换句话说,负载上电以后再突然断电时不会瞬间停止工作,而是会慢慢的停止,这才适合使用PWM控制。
就像钨丝灯泡那样,由于钨丝灯泡发光是利用了钨丝发热产生的光亮,而温度不可能断电马上下去,因此我们看见断电后灯泡的光亮会逐渐暗下去,就是这个意思。
因此PWM这个技术常常用在控制电机以及LED亮度调节上,因为电机在转动过程中突然断电的时候一般不会马上停下来,而需要一个缓冲,这就可以利用PWM来控制输给电机的电压,进而控制其转速;再者,那控制LED亮度调节难道LED断电也不会马上熄灭吗?实际上并不是,LED点亮后断电确实会马上熄灭,但是LED的亮灭是需要人眼来分辨的,因此实际调节亮度时利用了人眼的视觉暂留效应。
所谓视觉暂留效应,是指光刺激停止后视觉形象仍短暂保留的生理现象,这是由英国伦敦大学教授彼得·马克·罗杰特于1824年在《移动物体的视觉暂留现象》中首次提出的。 其原理就是光信号传入大脑神经存在延迟,视觉印象保留时间约为0.1秒至0.4秒。简单来说就是出现光后突然灭掉时,我们的视觉上一般还会保留0.1s左右的画面,即从熄灭到眼睛彻底看不见还会有0.1s左右的时间。因此人们其实就利用了这个特点,产生了动画,动画实际上就是一帧一帧的静态画面播放出来形成的,只不过每一帧间隔的时间非常短,使得前一个画面消失、出现了下一个画面时,人眼还停留在上一个画面上从而无法分辨,因此看着就好像是段连续的动画一样。
我们这里也是一样的,只要让LED每次点亮后熄灭的时间小于0.1s甚至更短,人眼看不出来,然后调整输出给LED的电压就能给人一种LED亮度在变化的感觉了。
总结一下:
PWM可以使用数字信号产生近似模拟量的输出去控制模拟电路中的负载,使用PWM控制的电路中的负载要求其状态不会发生突变,通常应用于控制电机、LED亮度调节等场景。
1.2 PWM的3个主要参数
经过前面的介绍,相信大家对于PWM已经有了一个相对清晰地认识。在实际使用时,我们生成的PWM波形本质上就是生成了一个方波信号,只不过其中的高低电平比例不同罢了。
对于PWM波形,我们需要着重认识的还有其3个主要参数,分别是周期、频率以及占空比,如下图所示。
这是一段PWM方波,其中所谓的周期就是指这段PWM波中连续两个上升沿或下降沿之间间隔的时间,一般用大写字母T表示;所谓的频率就是指周期的倒数,一般用小写字母f表示,即f = 1 / T;而占空比表示的就是PWM波的一个周期中高电平时间t(或者是高电平宽度)所占整个周期的比例,即t / T。
在后续案例和输出比较学习中都会涉及到相关概念,因此希望大家能够理解这三个参数。
1.3 PWM应用的注意事项
值得注意的是,在使用PWM驱动惯性电器时,一般不改变频率和周期,主要是通过更改占空比达到对外输出的有效电压的值。
换句话说,无论是控制电机转速还是控制LED亮度,本质上都是修改了输入的PWM波的占空比,而不是频率和周期;修改频率和周期影响的是能够动态调整电机转速和LED亮度的区间。
二、通用定时器的输出比较
接下来,我们正式开始介绍通用定时器的输出比较功能的作用以及基本原理。
2.1 输出比较的作用
通用定时器中的输出比较功能实际上就是用来控制输出方波的,因此绝大多数情况下就是用于输出前面介绍的PWM波形,当然也可以输出其他的波形,但是只能是方波,原因在于定时器本质输出的还是0或者1的数字信号,不是高电平就是低电平,因此能够产生的只能是这种方波信号,不可能产生锯齿波、正弦波等波形。
2.2 输出比较功能的实现结构
然后,我们再次回到通用定时器框图,来看看输出比较功能的实现对应的框图内容,如下图所示。
图中所框的部分就是输出比较功能实现的构成部分。由图可以看出,其主要包括了三大部分,分别是计数模块、比较模块(捕获/比较寄存器)以及输出模块。
首先是计数部分。大家应该很熟悉了,就是自动重装载寄存器和CNT计数器构成,用来进行定时器的计数的;
其次是比较模块,也就是捕获/比较寄存器部分。看图可知,每个通用定时器含有四个捕获/比较寄存器,也就是说一个定时器可以同时进行4路的输出比较;
最后是输出模块。同样的有4路输出,伴随着比较部分一起。
2.3 输出比较的实现原理
了解了输出比较的基本构成,接下来就聊聊它是如何工作,实现比较的。前面说过,输出比较功能简单说就是通过设定一个基准值,然后与计数器的值进行比较,然后确定不同时机输出的高低电平,最后输出最后生成的方波波形。
因此,输出比较实现必然是有一个地方记录我们设置的比较值的,也有一个地方确定我们如何比较,也就是设置比较的模式。
实际上,STM32中是使用的CCMR寄存器中的OCxM[2:0](x=1、2、3、4)来设置输出比较的模式、CCR寄存器用来设置比较值,其中CCMR寄存器就是Capture Compare Mode Register捕获/比较模式寄存器,CCR寄存器就是Capture Compare Register捕获/比较寄存器,看名字也很好理解了。
我们举例说明一下,以框图中的通道1为例:
假设此时设置的自动重装载值为99,计数方式为向上计数,也就是说实际计数100次会产生一次溢出事件,然后如果捕获/比较寄存器CCR中设置的值为60,那么其核心思想就是CNT计数器中的值会与捕获/比较寄存器中设置的值进行比较,根据比较结果(如大于、等于或小于),来产生不同的输出信号OCxREF(如高电平1或者低电平0)。
那么具体会产生什么样的输出就得看我们所设置的输出比较模式了。STM32中通用定时器可设置的输出比较模式一共有8种,也就是用CCMR寄存器中的OCxM[2:0]所占的3位进行模式控制。相关介绍可在参考手册中的通用定时器相关寄存器部分的CCMR寄存器介绍中找到,如下图所示
现在,我们就假设使用通道1,计数器的值为CNT,比较寄存器CCR的值为60,则
1、OC1M[2:0] = 000时,比较模式为输出冻结,此时CNT和CCR的比较结果不会影响输出,也就是不使用这个功能,即默认不开启输出比较;
2、OC1M[2:0] = 001时,强制输出高电平。当出现CNT=CCR时,直接强制输出高电平,再不改变,如下图所示。
即原本输出的是低电平,然后计数值与比较值相同了,此时输出的电平被强制拉高,随后不再改变,一直持续输出高电平。
3、OC1M[2:0] = 010时,强制输出低电平。当出现CNT=CCR时,直接强制输出低电平,再不改变,如下图所示。
与001模式相反,即原本输出的是高电平的话,随着计数值增大,然后计数值与比较值相同了,此时输出的电平被强制拉低,随后不再改变,一直持续输出低电平。
4、OC1M[2:0] = 011时,为输出翻转模式,此时出现一次CNT=CCR,就会翻转一下输出电平状态,如高->低、低->高...,如下图所示。
可以看出,一开始为低电平,随后当计数值等于比较值时输出电平被翻转为高电平,随后计数器溢出,重新从0开始计数,此时仍为高电平,随后再次出现计数值等于比较值时输出电平再次翻转变成低电平,以此循环。可以发现,此模式下输出的是一个很标准的方波,其中高电平和低电平宽度或者是持续时间就是我们计数器一轮计数的时间,因此此时输出的方波频率为计数器溢出频率的一半,占空比为50%。这种看起来有些像PWM了,但实际并不是,我们所设置的CCR更像相位的变化。
5、OC1M[2:0] = 100时,直接强制输出低电平。这就很强制了,在该模式下,比较值和计数值也是一点影响都没有,只能输出低电平,非常简单。
6、OC1M[2:0] = 101时,直接强制输出高电平。与100相反,该模式下,比较值和计数值也是一点影响都没有,只能输出高电平。
7、OC1M[2:0] = 110时,为PWM1模式,当CNT<CCR时,输出高电平(有效电平);当CNT>=CCR时,输出低电平。显然,此时产生的方波的频率与计数器溢出频率是对应的,如下图所示。
因此,PWM1模式下,这个波形显然就是PWM波形,其中的占空比 = CCR / (设置的重装载值 + 1),即60/100 = 60%。
8、OC1M[2:0] = 111时,为PWM2模式,当CNT<CCR时,输出低电平(有效电平);当CNT>=CCR时,输出高电平。显然该模式与PWM1恰好相反,如下图所示。
那么同理,在PWM2模式下,其占空比 = (设置的重装载值 + 1 - CCR) / (设置的重装载值 + 1),即40/100 = 40%。
当然,可能有人注意到,为什么CNT与CCR比较时,偏偏是大于等于和小于而不是小于等于和大于?
仔细想想,CNT小于CCR时,实际就是计数CCR次以后翻转了电平,而若是小于等于CCR,那么实际是计数了CCR+1次才翻转电平,此时计算占空比的话我们还需要多一步计算,更显麻烦,选大于等于而不是大于也是同理。因此这里规定的是小于CCR以及大于等于CCR其实还帮我们省事了。
总结一下,后面我们使用通用定时器去生成PWM波形的时候,一般采用的就是PWM1模式,也就是OC1M = 110的情况,因为咱们习惯性认为占空比表示的就是高电平宽度,因此选择PWM1模式更符合往常的习惯。
三、输出比较相关寄存器介绍
接下来,我们再简单梳理一下使用通用定时器的输出比较功能时,会涉及到的主要的寄存器,因此,我们需要查看参考手册中相关的寄存器介绍了。
3.1 控制寄存器TIMx_CR1
首先是控制寄存器1,由于通用定时器技术模式多样,因此我们需要专门配置一下计数方向,所以要在CR1中对DIR位进行配置,如下图所示
我们一般就使用默认的向上计数模式就行,因此DIR位要配置为0即可,参考代码如下。
/* 计数器计数方向: 0:向上计数 1:向下计数 */
TIM5->CR1 &= ~TIM_CR1_DIR;
3.2 捕获/比较模式寄存器TIMx_CCMR1
然后是TIMx_CCMR1寄存器,这是捕获/比较模式寄存器,用来配置输入捕获或者输出比较功能的模式的,如下图所示。
从图中可以看出,这个寄存器和之前见过的有些不一样,他有两行位,但是只有16位,原因在于这一个寄存器要负责两种功能的配置,所以使用两行列出来了。然后一个CCMR寄存器负责两个通道的配置,因此由图可以发现其中低8位负责的通道1的相关配置、高8位负责的通道2的相关配置,其位名字中分别是1或2,也很好区分。
其次,CCxS是用来配置通道方向的,所谓的通道方向就是指这个通道是用来进行输出还是输入,其介绍如下图所示。
以通道1为例,我们本次介绍的是输出比较,因此若要配置,则是将CC1S位配置为00即可。参考代码如下
/* CH2通道配置为输出 00表示输出 */
TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;
而另外三种情况就全是输入的情况,等我们介绍输入捕获的时候再详细说明。
既然选择了输出的通道,那么接着就要配置一下输出比较相关的内容,也同样是在CCMR寄存器中进行配置,如下图所示。
以通道1为例,由于输出比较有8种模式可选,因此我们必然需要对其进行配置,而配置的方法就是如上图所示的OC1M位,我们希望生成PWM信号,那么一般就会将OC1M为配置为PWM1模式,即110,参考代码如下
/* 配置CH2的输出比较模式(8种模式,选择PWM1模式)110 */
TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;
TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;
TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;
值得注意的是,我们可以发现在PWM模式的介绍中,所写的是CNT<CCR时通道为“有效电平”,与前面我们介绍的所述不一致。实际上这里的有效电平可以是高电平,也可以是低电平,主要由我们所需要控制的负载的特性相关,当然也是由我们自己配置。比如说我们的LED是使用低电平点亮,那么此时要使用PWM控制LED的话,一般认为低电平为PWM输出的有效电平,大概就是这个意思。
同时这个“有效电平”有专门的位进行配置的,在手册上被称为通道极性。
3.3 捕获/比较使能寄存器TIMx_CCER
CCER寄存器是捕获/比较使能寄存器,顾名思义主要是用来开启或关闭一些功能的,如下图所示。
可以发现,CCER寄存器直接负责4个通道的配置,每个通道刚好有两个配置的位,分别是CCxP位和CCxE位。从图中介绍可以知道:CCxP位表示通道极性,通道为输出时用来设置有效电平为高还是为低,通道为输入时用来设置捕获产生的时机,是反相还是不反相;CCxE位表示的是通道使能,通道为输出时就是开启输出还是禁止输出,通道为输入时就是使能捕获还是禁止捕获。
本次介绍的是输出比较功能,因此主要配置的是通道为输出情况下的内容,前面说到输出比较模式的时候提到配置PWM模式时涉及到的有效电平,也就是这里需要用CCxP进行配置的,如置0则有效电平设置为高电平,此时CNT<CCR时就会输出高电平了,若置1则有效电平为低电平,显然CNT<CCR时就会输出低电平了。同时CCxE位就是使能通道输出或者禁止通道输出的位了。参考代码如下
/* 设置CH2通道的极性: 0高电平有效 or 1低电平有效 */
TIM5->CCER &= ~TIM_CCER_CC2P;
/* 使能CH2通道 0: 关闭 1:开启*/
TIM5->CCER |= TIM_CCER_CC2E;
3.4 捕获/比较寄存器TIMx_CCR1
CCR寄存器,也就是捕获/比较寄存器,在通道配置为输出时用于设置比较值,通道配置为输入时用于存放计数器值,如下图所示。
值得一提的是,由上图介绍中可以发现,他提到了“预装载”特性,也就是说,实际上捕获/比较寄存器也存在预装载功能,换句话说,捕获/比较寄存器也有影子寄存器,也是有个预装载寄存器,因此会有相应的位来决定是否开启预装载功能。同时在通用定时器框图中捕获/比较寄存器的图应该也有阴影,如下图所示
由图可知,这只是通道1对应的捕获/比较寄存器,也就是说一个通道对应一个,因此一个有四个CCR寄存器,从通用定时器的寄存器映射图可以清楚看见,如下图所示
因此,以通道2为例,若我希望设置的比较值为60,那么需要向CCR2寄存器中给60,参考代码如下
TIM5->CCR2 = 60;
以上就是本次输出比较功能的配置所涉及到的几个寄存器了,当然还有比较基础的需要配置的寄存器,比如设置预分频数值、自动重装载值啥的。
至此,对于输出比较功能强相关的几个配置位在上面基本介绍完成。当然了,实际上通用定时器还有许多寄存器中的内容并没有作深入介绍,大家感兴趣可以去自己了解一下,后续的学习也会逐渐介绍到。
四、案例实践
接下来,就进入实操环节了,我们通过一个案例来使用通用定时器的输出比较功能,熟悉其操作流程。
4.1 LED呼吸灯控制
本次的案例叫做LED呼吸灯,也就是呈现出一种呼吸效果,在LED上的体现就是亮度从暗逐渐变亮、然后从亮逐渐变暗的往复效果。
4.1.1 需求描述
输出占空比可调的PWM波形,作用到二极管,使二极管(LED-2)呈现呼吸灯的效果。
4.1.2 硬件电路设计
由原理图可知,LED2与GPIOA的PA1相连,LED2另一端接3V3高电平,因此只需要PA1输出低电平即可点亮LED-2,反之输出高电平即可熄灭LED-2。
4.1.3 需求分析
了解需求和相关原理图后,我们来分析一下该需求,需求描述道:“输出占空比可调的PWM波形”,所以第一个问题是我们需要输出PWM波形;同时“作用到二极管(LED-2)”,由原理图可知LED-2是接在PA1上的,所以我们看看PA1引脚能够进行哪些功能,所以查一下数据手册,搜PA1看看,如下图所示
可以发现,PA1除了其标准的GPIO功能,还复用了串口、ADC以及定时器功能,其中定时器恰好是本次介绍的通用定时器中的两种(TIM2~5的TIM2和TIM5),因此这就好办了。可以很方便地利用本次介绍的通用定时器的输出比较功能的PWM模式,直接生成PWM波形经过PA1输出到LED-2上,这样就实现描述的前半句话的需求了。
然后需求描述后半句说“使二极管(LED-2)呈现呼吸灯的效果”,前面说了呼吸灯效果就是一种暗到亮、亮到暗的效果,就像LED自动缓慢来回调节亮度一样的,给人就感觉LED在一呼一吸一样的。由于前面介绍PWM时恰好说过,PWM常用于LED的亮度调节,只需要调整设置的占空比即可,因此这里所谓的自动来回调节亮度也就是不断地增大占空比或者减小占空比,对应到输出比较功能生成PWM波形就是不断调节设定的比较值,因此这样后半段需求就能够实现了。
简单整理一下,要实现该需求,
首先,让PA1复用TIM5通道,利用TIM5的输出比较功能生成PWM波形经过PA1输出给LED-1实现其亮度可调;其次,要不断修改生成的PWM波形的占空比,即改变TIM5的输出比较值,如0->1->2->...->99->98->...->2->1->0->1...类似这样循环往复,实现LED-2亮度变化如呼吸一般,即呼吸灯。
4.1.4 软件设计(寄存器方式)
按照前面的分析,接下来就可以正式开始编写代码了。
既然要使用PA1的复用功能,那么咱对GPIO的配置就属于通用定时器TIM5中的内容,与之前单独使用GPIO控制LED的代码就没啥关系了,因为纯LED控制只是利用了GPIO的标准功能,而这里本质上是利用了定时器输出比较功能来输出的信号控制LED,希望大家能够理解。
因此我们主要编写的就是TIM5的相关配置了,本次工程目录结构大致如下图所示。
如图,为方便,其实是复制的之前的工程进行的修改,因此本次我们实际用到的只是新增的TIM5的文件以及延时功能和主程序。那么首先我们编写的就是TIM5的相关内容。
4.1.4.1 TIM5相关代码
首先要定义的函数一般就是对TIM5的初始化,其中主要做的配置就是基础的计数配置以及当然要使用的输出比较功能的配置;
其次上一次基本定时器案例的HAL库实现时,我们发现其将定时器的开启和关闭单独使用一个函数进行封装了,因此我们也可以学着来试试,所以还要定义定时器开启和关闭的函数;
最后由于我们需要不断修改“LED的亮度”,而亮度控制就是输出给LED的PWM波形的占空比不同,按照前面的分析本质上就是需要修改输出比较的比较值,因此为了方便,我们还可以专门定义一个设置比较值的函数,用于修改占空比。
因此总结一下,本次案例关于TIM5的代码编写会定义的函数如下:
1、TIM5的初始化函数:TIM5_Init
2、TIM5定时器的使能函数:TIM5_Start
3、TIM5定时器的关闭函数:TIM5_Close
4、TIM5定时器的设置比较值的函数:TIM5_SetDutyCycle
所以TIM5头文件代码参考如下:
#ifndef __TIM5_H
#define __TIM5_H#include "stm32f10x.h"void TIM5_Init(void); // 初始化
void TIM5_Start(void); // 开启定时器
void TIM5_Close(void); // 关闭定时器
void TIM5_SetDutyCycle(uint8_t dutyCycle); // 设置pwm占空比#endif
接着,我们就要对这些函数依次实现。
首先是TIM5的初始化函数的编写。
因为PA1复用功能,所以对于PA1的配置也需要在这里进行,那么首先需要完成的就是PA1的相关配置了。第一步就是开启时钟,此时要用GPIOA,同时要使用TIM5,所以需要开启两个时钟;其次PA1使用复用功能,同时需要进行输出,因此PA1的工作模式要设置为复用推挽输出,速率给个50MHz就行,若不记得如何配了,可以去查看一下参考手册的RCC时钟那一章以及GPIO寄存器那一部分,参照一下,如下图所示。
其次,对于TIM5定时器的配置,首先肯定是对于时基单元的配置,即预分频数值、自动重装载值的设置。那么如何设置,本次案例是要实现呼吸灯效果,本质上就是亮度不断变化,根据前面的分析,这需要利用人眼的视觉暂留效应才能够实现这种效果,也就是LED点亮后熄灭的时间不能高于0.1s也就是100ms,我们使用的时钟默认是内部时钟源72MHz,所以设置的预分频数值和自动重装载值要满足一个条件,即一轮计数的时间最好不要高于100ms,也就是(预分频数值+1)/72000000 * (自动重装载值+1) 要小于0.1s,因此(预分频数值+1)*(自动重装载值+1) 要小于7200000。
而我们知道,输出比较的时候比较值是与计数值比较,而计数值最大就是自动重装载值,因此为了计算方便,我们可以将自动重装载值设置为100-1,也就是计数100次,这样我没设置的比较值正好就对应占空比,看着更舒服。那么此时(预分频数值+1)*(自动重装载值+1)就变成了(预分频数值+1)*100需要小于7200000,也就是(预分频数值+1)<72000即可,所以我们为了换算方便,预分频数值直接设置为7200-1即可,这样我们一轮计数的时间就是7200/72000000*100=10ms,完全可以满足人眼无法分辨的情况。
因此总结一下:
对于时基单元的配置,预分频数值设置为7199,自动重装载数值设置为99即可。
接着就可以进行输出比较相关内容的配置了,本次使用的PA1是复用的TIM5的通道2。根据前面对寄存器的介绍,应该能想到我们需要先设置计数模式,这个默认使用向上计数就行;其次是设置通道方向,也就是是输出还是输入,显然为通道2的输出;接着设置输出比较模式,要生成PWM,那么最常使用的是其中的PWM1模式;接着还要设置一下通道极性,也就是通道2的有效电平为高电平,低电平也可,这对需求本质上没有什么影响,我就默认设置高电平;还要设置比较值,其实这里设不设置都行,但是给个初始值还是好点,相当于初始化;最后使能通道2就行。本来还要开启定时器,但是我们单独封装了函数来设置,所以这里就不用开启了。
那么总结一下输出比较功能的配置步骤:
1、设置计数模式:向上计数;
2、设置通道方向:通道2为输出;
3、设置输出比较模式:PWM1;
4、设置通道极性:通道2的有效电平为高;
5、设置比较值:随便给一个就行,不影响;
6、使能通道2。
其中的比较值的设置所在先后顺序不影响,毕竟后面我们还会不断修改它。同时由于前面已经详细上述涉及到的寄存器的配置方法,所以不记得的可以自己去查手册或者翻到前面再看一下,这里就不再赘述了。
至此,关于TIM5的初始化就完成了。参考代码如下:
void TIM5_Init(void)
{// 1. 开启时钟RCC->APB1ENR |= RCC_APB1ENR_TIM5EN;RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;// 2. 配置GPIO工作模式 PA1:复用推挽输出模式 MODE-11 CNF-10GPIOA->CRL |= GPIO_CRL_MODE1;GPIOA->CRL |= GPIO_CRL_CNF1_1;GPIOA->CRL &= ~GPIO_CRL_CNF1_0;// 3. 设置定时器// 3.1 设置预分频值 7200-1TIM5->PSC = 7200 - 1;// 3.2 设置重装载值 每10ms溢出一次 99TIM5->ARR = 100 - 1;// 3.3 设置计数方向TIM5->CR1 &= ~TIM_CR1_DIR;// 3.4 设置CCR2TIM5->CCR2 = 60;// 3.5 配置通道模式为输出 CC2S-00TIM5->CCMR1 &= ~TIM_CCMR1_CC2S;// 3.6 设置pwm模式1 OC2M-110TIM5->CCMR1 |= TIM_CCMR1_OC2M_2;TIM5->CCMR1 |= TIM_CCMR1_OC2M_1;TIM5->CCMR1 &= ~TIM_CCMR1_OC2M_0;// 3.7 设置通道极性 低电平有效TIM5->CCER |= TIM_CCER_CC2P;// 3.8 使能通道2TIM5->CCER |= TIM_CCER_CC2E;
}
接着是TIM5的开启和关闭函数的实现。这就很简单了,只有一条语句,其中设置的寄存器位和前面基本定时器一模一样,不记得的可以自己查一下参考手册,如下图所示。
参考代码如下:
// 开启定时器
void TIM5_Start(void)
{TIM5->CR1 |= TIM_CR1_CEN;
}// 关闭定时器
void TIM5_Close(void)
{TIM5->CR1 &= ~TIM_CR1_CEN;
}
最后是设置占空比的函数,实际上就是单独设置一下前面初始化时设置的比较值,也比较简单,这里不再赘述。参考代码如下
// 设置pwm占空比
void TIM5_SetDutyCycle(uint8_t dutyCycle)
{TIM5->CCR2 = dutyCycle;
}
至此,TIM5的相关配置就完成了。
这个时候,当我们开启TIM5时,就会自动开始计时,并执行输出比较功能,将生成的默认占空比的PWM波从PA1输出给到LED-2,因此到这里我们一半的需求已经实现完成了。
接下来就是在主函数直接完成后半段需求,呼吸灯效果。
4.1.4.2 main.c相关代码
按照执行顺序,我们首先引入相关头文件,然后进行定时器的初始化,并打开TIM5定时器,为了方便设置占空比以及调整占空比增大或减小的方向,因此再定义两个变量,用来表示占空比大小dutyCycle和增减方向dir。
接着就是在while中不断调整占空比了。根据前面分析,就是让占空比先逐渐增大、再逐渐减小,其中范围肯定就是所设置的自动重装载值100-1=99咯,而判断增到最大或者减到最小后开始做相反操作的标志就是前面定时的dir控制,逻辑其实挺简单的,就不再赘述。
参考代码如下
#include "tim5.h"
#include "Delay.h"int main(void)
{// 1. 初始化TIM5_Init();// 2. 开启定时器TIM5_Start();// 3. 初始化占空比及变化方向uint8_t dutyCycle = 0;uint8_t dir = 0; // 0-增 1-减while (1){if (dir == 0){// 增大占空比dutyCycle += 1;// 达到最小亮度,改变变化方向if (dutyCycle >= 99){dir = 1;}}else{// 减小占空比dutyCycle -= 1;// 达到最大亮度,改变变化方向if (dutyCycle <= 1){dir = 0;}}// 设置占空比TIM5_SetDutyCycle(dutyCycle);// 增加少量延时,降低“呼吸”速度Delay_ms(15);}
}
值得注意的是,由于我们设置的有效电平为高电平,因此随着占空比的增大,实际LED-2的亮度会不断降低,反之则升高。
至此,本次LED呼吸灯的寄存器实现案例就完成了。
4.1.5 软件设计(HAL库方式)
接下来,我们再使用HAL库方式实现一下该案例。
4.1.5.1 STM32CubeMX配置
首先打开STM32CubeMX,点击【ACCESS TO MCU SELECTOR】进行芯片选择。
然后,搜索自己使用的芯片(我使用的是STM32F103ZET6),双击芯片创建工程。
工程创建好后,会进入该工程的图形化配置界面。然后先进行系统配置,单击【System Core】,选择【SYS】,将其中的调试器【Debug】选择【Serial Wire】串行单线调试模式,也就是使用STlink使用的调试模式,时基源默认系统定时器,如下图所示。
然后配置一下时钟。选择【RCC】,然后选择HSE和LSE均为石英晶体震荡【Crystal/Ceramic Resonator】。如下图所示。
接着点击【Clock Configuration】配置一下时钟树,选择HSE,9倍频,然后APB1部分进行2分频,如下图所示。
这里我们也可以发现,图形化配置中的时钟树自动将输入定时器的时候找那个频率x2给到了定时器2~7了。
接着,自然应该是要配置PA1相关的内容,由于此时PA1是复用定时器功能,因此我们直接配置TIM5即可配置好PA1了,因此接着配置TIM5。
所以选择【Timers】,使用TIM5定时器,所以点击TIM6,然后可以发现这里出现的配置项比前面基本定时器多很多,因为通用定时器功能多了不少。如下图所示,每一个表示什么都已经标注好了。
所谓的从模式【Slave Mode】,就是当进行定时器级联时,如果当前定时器要通过其他定时器输出信号驱动的话,此时当前定时器就属于从模式下,就是主机从机的概念。本次我们不需要,所以默认不开启就行;
接着的触发源【Trigger Source】就是作从模式下用来驱动当前定时器的信号源,当然本次也不需要,所以默认不开启;
然后是内部时钟【Internal Clock】,本次使用的就是内部时钟源,所以一定要勾选上。
接着是4个channel通道,PA1复用的TIM5的通道2,所以显然我们需要配置通道2为PWM生成模式。
然后模式配置就差不多可以了,配置的方式如上述及图中所示。
接着配置TIM5的一些参数。有了寄存器配置经验,这里就不再赘述。简单来说主要就是对TIM5进行初始化的内容,如下图所示。
不过这里也多了不少参数,我们过一遍。
首先是【Counter Settings】计数器部分参数配置:
PreScaler,即对应PSC预分频数值,我们给的7199,也就是作7200分频;
Count Mode,即对应DIR计数方向,我们通常使用向上计数,即UP;
Counter Period(AutoReload Register),即对应自动重装载寄存器值ARR,我们配置为99,即一轮计数100次;
Internal Clock Division(CKD),即对应控制寄存器中的CKD位设置的内部时钟分频,当然我们不需要使用,所以不开启Disable即可;
auto-reload preload,即对应控制寄存器的APRE位的预装载使能,当然,我们可以不用开启,所以选Disable即可。
其次是【Trigger Output】触发输出部分参数。
由于我们本次不涉及触发相关内容,因此暂时不做说明,后面介绍到再说。
最后是【PWM Generation Channel 2】PWM生成通道2的参数配置。
Mode,即PWM1模式和PWM2模式,我们通常使用PWM1模式即可;
Pluse,即输出比较值,对应寄存器CCR,由于后面我们会不断修改,因此这里给0即可;
Output Compare Preload,即输出比较预装载使能,前面说过捕获/比较寄存器也存在预装载功能,也就是有预装载寄存器和影子寄存器,这其实对应寄存器CCMR中的OCEP位。当然开不开都可以,HAL库是默认开的,所以不影响,即Enable/Disable都可;
Fast Mode,即快速模式,对应CCMR寄存器中的OCFE位,用于加快输出对事件的响应,当然本次不需要使用,所以选择Disable即可。
CH Polarity,对应通道极性,也就是有效电平设置,本次直接设置为高电平High即可。
因此对于TIM5参数的相关配置即上述及上面HAL库配置图所示。
最后,我们配置一下工程管理,点击进入【Project Manager】,在【Project】中配置一下,如下图所示。
然后进入【Code Generator】,勾选内容如下图所示。
最后点击生成代码,并打开项目即可。
然后就会自动在keil中打开工程了。接着我们对keil中的Debug做简单配置就好了。
4.1.5.2 Keil中的配置
keil中主要是简单设置一下debug的内容。进入【魔法棒】,然后选择【Debug】,进入【Settings】,然后选择【Flash Download】,勾选上【Reset and Run】;然后进入【Pack】,取消勾选【Enable】即可。
最后不要直接叉掉,点击确认/Ok后退出。
4.1.5.3 Vscode完善代码
接着,我们就可以使用VScode打开工程,进行代码分析并且补充一下代码了。
首先,我们可以看看tim.c中的配置,检查一下进行图形化配置时有没有设置错,若有问题可以直接在代码中修改,如下图所示。
如上图,TIM5的初始化配置也和基本定时器一样,用一个结构体进行一些参数配置,重点看看预分频数值和自动重装载值有没有搞错。
那么现在,定时器配置也都做好了,因此接下来我们需要完善的就是去主程序不断修改比较值了。当然,比较值因为算是比较底层的实现,即便是在HAL库也是一样,因此我们也干脆封装成一个函数去调用,与寄存器实现时的逻辑一样。
所以,我们直接在tim.c中定义一个设置占空比的函数,并在tim.h中进行声明,参考代码如下:
接着直接在设置占空比函数中调用HAL库的函数设置比较值就行。当然,目前还不知道HAL库用啥函数获取的,所以得利用Vscode的代码联想功能区试一下,这里我就直接列出来吧。
__HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__)
可以发现,这个函数还有点特殊,是个宏定义函数,不过进去一看如上图所示,其中有三个参数,看名字还是容易理解是什么的,__HANDLE__就是定时器的那个结构体指针、__CHANNEL__就是使用的通道、__COMPARE__就是设置的比较值。
因此我们直接传入相应内容即可,参考代码如下
/* USER CODE BEGIN 1 */
void TIM5_SetDutyCycle(uint8_t dutyCycle)
{__HAL_TIM_SetCompare(&htim5, TIM_CHANNEL_2, dutyCycle);
}
/* USER CODE END 1 */
最后,我们就可以直接在main.c中进行初始化以及呼吸灯逻辑的补充了。
主函数中,相关的初始化已经完成了,需要注意的是,我们还需要手动开启一下TIM5定时器才行。
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);
然后就是定义设置的占空比的变量dutyCycle,然后我们稍微优化一下代码。因为之前寄存器方式的代码还是显得有些重复,如下图所示
可以发现,我们使用dir来控制增减方向时,这俩部分看着还是非常类似,所以我们思考一下能不能优化一下逻辑,不要显得赘余。
实际上是可行的,涉及到的问题也就是:怎么样实现占空比减少到最小或增大到最大时自动修改增减方向,只要解决这个问题,就基本可以优化了。首先“占空比减少到最小或增大到最大时自动修改”这句话完全可以变换成判断语句的代码,也就是如下所示
然后,我们就想,在这个语句里面,要写什么?当出现这种情况时我们实际上就是需要修改增减方向了,也就是此时dutyCycle要从+变成-、或者从-变成+了,而无论加减,那个加减数都是一样的,所以这就好办了。我们可以直接将那个数变成原来的相反数,然后直接与dutyCycle进行加法,这样就能实现增减方向的控制了。所以此时我们还需要定义一个变量记录每次加减的数值,就相当于占空比一次变化的步长step,一般我们设置为1,这样思路就有了。参考代码如下
当然,虽然思路已经形成代码,但是上述代码逻辑其实还有一些小问题。
比如我初始占空比为0,此时已经不满足判断条件,因此会变成-1,此时占空比就会变成负数,显然不符合逻辑,所以自然可以想到让step初始为-1。但是这时候其实仍不能解决问题,因为占空比为0时,进入判断将step变成1后会0+1=1,此时再次判断结果又进去将step变成-1了,此时占空比又会-1变成0,然后就这样循环卡在0和1里面,这显然不行。所以最好的办法就是让占空比初始值处于1~99之间,这样就能解决该问题了。所以直接将dutyCycle初始值改为1即可。
最后再增加一个10ms左右的延时减慢“呼吸”速度即可。最终main.c补充的完整参考代码如下
HAL_TIM_PWM_Start(&htim5, TIM_CHANNEL_2);/* USER CODE END 2 */uint8_t dutyCycle = 0;int8_t step = 1;/* Infinite loop *//* USER CODE BEGIN WHILE */while (1){/* USER CODE END WHILE *//* USER CODE BEGIN 3 */if (dutyCycle <= 1 || dutyCycle >= 99){step = -step;}dutyCycle += step;TIM5_SetDutyCycle(dutyCycle);}HAL_Delay(10);/* USER CODE END 3 */
}
至此,HAL库实现的代码就完成了,最后编译烧录即可看见实验现象,如下。
五、小结
本文介绍了STM32通用定时器的输出比较功能及其PWM波生成原理,并以LED呼吸灯为例介绍了如何利用通用定时器的输出比较生成PWM波形。主要内容包括:
1. PWM概念与原理,解释了占空比、频率和周期三个关键参数;
2. 通用定时器输出比较功能的结构与实现原理,包括8种输出模式;
3. 通过寄存器配置和HAL库两种方式实现LED呼吸灯的具体步骤,包括定时器初始化、PWM参数设置及动态调整占空比的逻辑。
以上便是本次文章的所有内容,欢迎各位朋友在评论区讨论,本人也是一名初学小白,愿大家共同努力,一起进步吧!
鉴于笔者能力有限,难免出现一些纰漏和不足,望大家在评论区批评指正,谢谢!