06 定时器和PWM(1)
前言
前面介绍了一下外部中断,这一节主要介绍一下内部定时器和PWM,这两个知识还是比较重要的。
一、定时器
1.什么是定时器
定时器其实和计数器一样,我们通过设置一个值,当计数器运行一个计数寄存器向上加1或者向下减1达到这个值后,会发送一个事件,以此运行即可。
这个就是定时器。
2.在stm32中的定时器
在stm32中定时器是以TIMx来命名的,在stm32中有三类定时器,分别是基本定时器、通用定时器和高级定时器。
2.1 基本定时器
基本定时器的编号为TIM6
和TIM7
,它们是属于APB1
总线中的外设。
它们的功能有:定时中断、主模式触发DAC。
基本定时器属于最基本的定时器,在stm32f103中没有基本定时器。
2.2 通用定时器
通用定时器的编号为TIM2
、TIM3
、TIM4
、TIM5
,它们是属于APB1
总线中的外设。
通用定时器有基本定时器的全部功能,并且在此功能上还添加了内外时钟源选择、输入捕获、输出比较、编码器接口和主从模式等功能。
我们使用通用定时器可以做PWM的输出和捕获。
2.3 高级定时器
高级定时器的编号为TIM1
和TIM8
,它们是属于APB2
总线中的外设。
高级定时器有通用定时器的全部功能,并且增加了重复计数器、死区生成、互补输出、刹车输入等功能。
其中死区生成、互补输出、刹车输入是针对与三项无刷电机的功能。
在stm32f103c8t6中有一个高级定时器和三个通用定时器,编号分别是TIM1
、TIM2
、TIM3
和TIM4
。
接下来我们来了解一下定时器的内部结构。
3.定时器的内部结构
这里的图我借用了江科大的图
我们可以设置定时器是使用内部时钟还是外部时钟,也可以捕获外部的PWM信号,然后选择模式是内部模式还是外部模式或者是编码器模式,通过选择模式后进入到时基单元,时基单元主要是用来运行定时器的,预分频器是将输入的时钟再次进行分频,然后根据这个再次分好的频率来运行计数器CNT
,当计数器达到自动重装器中的值后会发出一个信号给中断输出控制,中断输出控制发送中断信号给NVIC
,这样就可以完成一次定时器中断了。
对于在运行过程中,我们通过代码修改自动重装值ARR
中的值时,stm32有两种模式,一种是无预装时许,另一种是有预装时许。
3.1 无预装时许
无预装时许的意思是,你在某一个时刻修改ARR
中的值后,当前计数器比较到当前值后就会立马响应信号,就比如下图:
原来的计数值是FF,当修改为36后,计数器寄存器达到36后就会发送一个信号出来。
3.2 有预装时许
有预装时许和无预装时许的区别在与有预装时许有一个影子寄存器,当你在计时的过程中修改ARR
的值,这个ARR
的值不会立刻响应,而是要等当前的ARR
值响应后下一次计数才可以响应:
可以看到,当修改值后,影子寄存器中的值是不会改变,只有ARR
中的值改变,当当前计时完成后,产生更新事件后,才会使用修改后的值。
4.开始写代码
我们已经了解了定时器的内部结构后我们就知道了如何写好这个代码。
逻辑其实就是,打开TIM的时钟,设置TIM使用内部时钟,配置TIM,打开NVIC中断,书写中断处理函数即可。
那下面就一步一步的进行实现。
4.1 打开TIM时钟
这一步很简单,只需要先确定TIM的编号,然后打开对应的APB
总线即可,这里我选择的是通用定时器TIM2
,TIM2
是在APB1
总线上的,所以代码就使用开启APB1
的函数即可开启:
1 | RCC_APB1PeriphClockCmd(RCC_APB2Periph_TIM2, ENABLE); |
再比如我开启高级定时器TIM1
,那高级定时器对应的是APB2
总线,那这里的开启时钟代码就是使用APB2
的函数即可开启:
1 | RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); |
这样就开启了定时器的时钟。
4.2 选择定时器的时钟
这里在之前内部结构可以了解到,我们可以选择外部时钟输入或者是内部时钟,配置内部时钟使用的函数是:
1 | void TIM_InternalClockConfig(TIM_TypeDef* TIMx); |
在TIMx
这填写对应的定时器编号,比如说这TIM2
使用内部时钟作为输入,那就填写为:
1 | TIM_InternalClockConfig(TIM2); |
这样就可以让TIM2
使用内部RCC
时钟了。
配置外部时钟输入的函数是:
1 | void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter); |
这个函数是通过设置ETR外部时钟模式2来作为时钟的输入,这里的传输比较多。
第一个参数是定时器的编号,第二个参数TIM_ExtTRGPrescaler
是外部时钟预分频器,这个参数可以填写下面的这些参数:
参数名 | 解释 |
---|---|
TIM_ExtTRGPSC_OFF | 不分频 |
TIM_ExtTRGPSC_DIV2 | 2分频 |
TIM_ExtTRGPSC_DIV4 | 4分频 |
TIM_ExtTRGPSC_DIV8 | 8分频 |
第三个参数TIM_ExtTRGPolarity
是外部触发极性,可以填写下面这些参数:
参数名 | 解释 |
---|---|
TIM_ExtTRGPolarity_Inverted | 低电平有效 |
TIM_ExtTRGPolarity_NonInverted | 高电平有效 |
第四个参数ExtTRGFilter
是外部触发器过滤器,这个东西是设置一个值,这个值要在0x00到0x0F之间才可以。过滤器就是在一个f频率中采样N个点,如果N个点的电平一致,那就代表有效,这个函数就是设置f和N的。
这里使用的是内部时钟,所以这里使用TIM_InternalClockConfig
来进行配置,代码如下:
1 | TIM_InternalClockConfig(TIM2); |
这样就可以使用内部RCC
时钟了。
4.3 配置定时器
这个配置方法很简单,和配置GPIO口一样,创建结构体然后配置里面的内容,最后初始化一下即可,这样就配置好TIM定时器了,使用的结构体是:
1 | typedef struct |
可以看到这个结构体中有一个TIM_RepetitionCounter
参数只有高级定时器才能使用,所以我们可以不配置,给个0即可,也可以使用TIM_TimeBaseStructInit
函数对结构体一个默认值。
这里就介绍一下这两种写法,第一种全部配置:
1 | TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0}; |
这样就可以配置TIM了,第二种方法,实现给这个结构体一个初始值,然后再配置需要用到的一些参数:
1 | TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct = {0}; |
这样就可以配置好了,其实还是比较简单的。
4.4 打开TIM中断
这个使用TIM_ITConfig()
函数进行打开即可,函数原型如下:
1 | void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState); |
TIMx
是TIM的编号。
TIM_IT
是填写触发模式,这里大家可以去标准库中进行查看。
NewState
这个是填写使能ENABLE
或者失能DISABLE
的。
这里我要配置TIM2定时器,更新事件,那么代码如下:
1 | TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); |
这样就可以使能定时器中断了。
4.5 配置NVIC
这个就很简单了,直接上代码:
1 | NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); |
4.6 启动TIM时钟
这里需要使用TIM_Cmd()
进行使能才可以让定时器运行,这里的代码如下:
1 | TIM_Cmd(TIM2, ENABLE); |
这样就可以启动时钟开始定时了。
4.7 书写中断服务函数
这个地方的中断服务函数需要到启动文件中去找,我这使用的是TIM2
,所以中断服务函数是:TIM2_IRQHandler
,然后写里面的内容即可,这里的话不是清除外部中断标志位和判断外部中断标志位了,而是清除定时器的中断标志位和判断定时器中断的标志位。
代码如下:
1 | void TIM2_IRQHandler() |
这样内部定时器中断和实现就写完了,接下来来介绍一下如何计算定时的时间。
5.定时时间的计算
这个是讲公式了,我们前面了解到一个内部时钟,这个是CLK,预分频器是PSC,重装寄存器是RCC,那定时频率的公式为:P_CLK = CLK / (PSC + 1) / (RCC + 1)。
根据这个公式就可以计算出来计时时间了,比如说我们要实现1s的定时,那PSC我们可以给7200 - 1,RCC可以给10000 - 1,这样就是1s发生一次中断。
当然你也可以给PSC 10000 - 1,RCC给7200 - 1。
这些都是可以的,这个是自己经过计算可以得到的。
二、外部时钟模式定时器
上面介绍了使用RCC
作为时钟的定时器,这里介绍一下用外部的时钟作为定时器的方式。
其实内部的思想是一样的,在时钟选择那不用TIM_InternalClockConfig()
函数进行设置,而是使用TIM_ETRClockMode2Config()
函数进行设置即可,上面详细解释了这个函数的原型和参数,这里就不再介绍了,然后配置个GPIO口,因为需要外部输入,外部输入需要使用到GPIO口,所以要进行配置,手册上对于作为时钟输入的GPIO口配置为浮空输入,这里也用浮空输入,代码如下:
1 | RCC_APB1PeriphClockCmd(RCC_APB2Periph_TIM2, ENABLE); |
然后就是书写中断服务函数:
1 | void TIM2_IRQHandler() |
中断服务函数的位置不固定。
三、PWM
终于到PWM脉冲调制控制了,这个功能的实现其实也是基于定时器的,这个功能用来控制电机的。
PWM有三个关键参数,一个是占空比,一个是分辨率,另一个是频率。
占空比是在一个时钟内,高电平的时间和一个时钟时间的比值。
Ts是一个时钟的时间,Ton是高电平的时间。这个就是占空比。
分辨率是占空比的变化步距。
频率是Ts时钟时间的倒数。
1.PWM工作过程
在PWM中有着定时器的重装寄存器ARR,还增加了一个捕获/比较寄存器CCR
,这个CCR
的作用是,当计数器到达这个CCR
设定的值后就会发生一个电平的翻转,例如下图:
黄色的线代表着ARR
中的值,红色的线代表CCR
中的值,可以看到当计数器到CCR
那时,电平就发生了变化,当到达ARR
后就会变回来,以此工作下去。
2.PWM内部结构
可以看到这个内部结构,CRR
就是对计数器中的值进行一个比较,当比较成功就会根据设置好的模式来进行输出,其实还是比较简单的,设置方法也是先设置TIM,然后配置GPIO口,最后配置PWM的模式就可以了,过程比较简单,前面见过的TIM设置这里就不再重复了。
因为PWM是通用定时器和高级定时器的一个功能,所以不需要额外开启时钟,直接开启对应的定时器的时钟即可。
主要要注意,基本定时器是没有PWM功能的。
2.1 配置GPIO口
这里需要使用到GPIO口来作为一个PWM的输出,翻看手册,可以看到
对于输出比较通道,GPIO口的配置是使用推挽复用输出GPIO_Mode_AF_PP
,所以这里就明白了,配置方法很简单:
1 | GPIO_InitTypeDef GPIO_InitStruct = {0}; |
2.2 配置PWM
上面有一步是配置TIM,那个之前就已经说了的,配置的内容一样,所以就直接省略,这里直接上PWM配置,使用到的函数比较多,其实我们这使用的是定时器的输出比较功能,而在输出比较功能中就有一个功能是PWM功能。
这里的输出比较有这么几个模式,这里还是借用江科大的图:
这里可以看到有这些功能,解释也比较清楚,所以这里就不介绍了。
这里配置PWM也是使用一个结构体来进行配置,结构体的类型为:
1 | typedef struct |
这里的话有一些可以不用配置,表明着仅对 TIM1 和 TIM8 有效的可以省略,这里可以使用TIM_OCStructInit
函数对创建好的结构体进行一个初始化,避免初始化时因为参数没有配置导致出现错误。
首先创建一个结构体:
1 | TIM_OCInitTypeDef TIM_OCInitStruct = {0}; |
然后使用TIM_OCStructInit
进行初始化配置:
1 | TIM_OCStructInit(&TIM_OCInitStruct); |
初始化完成后就可以开始对结构体中的成员开始配置了:
1 | TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; // 这个是配置模式,这里配置的模式是PWM1 |
这样就配置好了结构体了,下面就需要进行初始化,在初始化的时候需要选择对应的输出比较函数,在c8t6中有4个输出比较通道可以进行设置,并不是每个通道都可以初始化相应的定时器,这个需要在看GPIO引脚的默认复用功能是哪一个通道,然后再选择相应的通道,这四个函数为:
1 | void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct); |
比如说上面还是使用的是TIM2定时器,并且是使用PA3作为PWM的输出,那么这里的初始化通道就得选择4通道,初始化代码如下:
1 | TIM_OC4Init(TIM2, &TIM_OCInitStruct); |
这样就可以配置好TIM2_CH4了。
2.3 使能输出比较通道
这个使用一个函数就可以进行使能了:
1 | TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable); |
当然这也是有4个函数的,这4的函数要对应对应的通道来选择,不能乱选择。
2.4 使能定时器
设置完成后我们就可以加定时器进行设置了,设置的函数和前面的一样:
1 | TIM_Cmd(TIM2, ENABLE); |
2.5 计算出占空比
这样PWM就设置完成了,现在然后我们就可以了解一下占空比的计算方法了,我们之前设置了PSC预分频器和重装寄存器ARR,这个可以算出运行一次需要多少时间,那我们用设置的CCR捕获/比较寄存器来除以(ARR+1)了。
比如说在1s的定时器中输出0.5s的高电平和0.5s的低电平,那PSC设置为7200 - 1,ARR设置为10000 - 1,CCR设置为5000 - 1即可实现。
四、LED呼吸灯
这个项目太经典了,每个教程在讲PWM的时候都会讲一个呼吸灯的项目,这里也一样,跟随潮流,我也写一个。
其实这个项目很简单,就是不断的调整PWM的占空比就可以实现了。
初始化代码如下:
1 | void PWM_Init() |
然后主函数的实现代码如下:
1 | int main(){ |
总结
TIM定时器中断和PWM的基础功能还是比较简单的,大家可以多使用一下就可以慢慢的熟悉了。