STM32入门教程(定时器篇)
重要的内容写在前面:
- 该系列是以up主江协科技的STM32视频教程为基础写下去的,大部分内容都参考了老师的课件,对于一些个人认为比较重要但是老师仅口述的部分,笔者都有用文字的方式记录并标出了重点。
- 文中的图片基本都来源于老师的课件以及开发板和芯片的手册,粘贴过来是为了方便阅读。
- 如果有条件的可以先学习一些相关课程再去看STM32的教程,学起来会更加轻松(不太建议零基础开始直接STM32,听起来可能会有点困难,可以先学51单片机),相关课程有数字电路(强烈推荐先学数电,不然可能会有很多地方理解起来很困难)、模拟电路、计算机组成原理(像寄存器、存储器、中断等在这门课里有很详细的介绍)、计算机网络等。
- 如有错漏欢迎指出。
视频链接:[6-1] TIM定时中断_哔哩哔哩_bilibili
一、STM32中的定时器
1、定时器TIM概述
(1)定时器TIM(Timer)可以对输入的时钟脉冲进行计数,并在计数值达到设定值时触发中断。它由16位计数器、预分频器、自动重装寄存器的时基单元组成,在72MHz计数时钟下可以实现最大59.65s的定时。
(2)TIM不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。
(3)根据复杂度和应用场景,定时器可分为高级定时器、通用定时器、基本定时器三类:
|
类型 |
编号 |
总线 |
功能 |
|
高级定时器 |
TIM1、TIM8 |
APB2 |
拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
|
通用定时器 |
TIM2、TIM3、TIM4、TIM5 |
APB1 |
拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
|
基本定时器 |
TIM6、TIM7 |
APB1 |
拥有定时中断、主模式触发DAC的功能(内部硬件在不受程序的控制下实现自动运行) |
(4)STM32F103C8T6提供的定时器资源:TIM1、TIM2、TIM3、TIM4。
2、三种定时器框图
(1)基本定时器框图:

①计数器、预分频器和自动重装载寄存器构成最基本的计数计时电路(时基单元)。
②来自RCC的TIMxCLK提供时钟信号(内部时钟),频率值一般都是系统的主频——72MHz。
③预分频器PSC可以对72MHz的计数时钟信号进行预分频:
[1]如果该寄存器的值为0,就表示不分频(也叫一分频),这时输出频率等于输入频率(72MHz)。
[2]如果该寄存器的值为1,就表示二分频,输出频率等于输入频率的二分之一(36MHz)。
[3]如果该寄存器的值为2,就表示三分频,以此类推。预分频器的长度为16位,所以最大值可以写65535。

④计数器可以对预分频后的计数时钟信号进行计数,计数时钟信号每有一个上升沿,计数器的值+1,计数器的长度也为16位,计数最大值为65535,计数超过最大值后计数器的值归零并产生中断信号。
⑤自动重装载寄存器的长度也为16位,它存储的是用户写入的计数目标值(自动重装值),在定时器运行的过程中,计数器中的值不断自增,而自动重装值是固定的目标值,当计数器中的值等于自动重装值时,定时器产生中断信号,并且计数器中的值清零,同时开始下一次的计数计时。

⑥折线箭头UI代表产生中断信号,对应的中断称为“更新中断”,计数值等于自动重装值时,更新中断信号会进入NVIC,如果有配置NVIC的定时器通道,那么NVIC能把定时器的更新中断信号传给CPU。
⑦U折线箭头代表产生一个事件,对应的事件称为“更新事件”,更新事件不会触发中断,但可以触发内部其它电路进行相关的工作。
⑧主模式触发DAC(数字信号转模拟信号)的功能:
在使用DAC时,可能需要用DAC会输出一段波形,这需要每隔一段时间触发一次DAC,让它输出下一个电压点,这个功能可以用定时中断实现,但是会增大CPU的负担
为此,定时器设计了一个主模式,使用主模式可以将定时器的更新事件映射到触发输出TRGO的位置,而TRGO直接连接DAC的触发转换引脚,这样就不需要通过定时中断触发DAC转换,实现硬件自动化
(2)通用定时器框图:

①计数器、预分频器和自动重装载寄存器构成最基本的计数计时电路(时基单元),它们的工作流程和基本定时器基本一样。
②对于通用定时器和高级定时器而言,计数器的计数模式不止向上计数(从0自增到自动重装值)1种,还有向下计数模式和中央对齐计数模式。
[1]向下计数模式:从自动重装载值开始计数器的值向下自减,直到计数为0后,计数器的值回到重装载值,同时申请中断。

[2]中央对齐计数模式:从0开始计数器的值向上自增,待达到自动重装值后申请中断,然后计数器的值开始向下自减,直到计数为0后再次申请中断,以此往复。

③时基单元上面的部分是内外时钟源选择和主从触发模式的结构,对于基本定时器而言,定时只能选择内部时钟(也就是系统频率为72MHz),而通用定时器的时钟源不仅可以选择内部的72MHz时钟,还可以选择外部时钟:
[1]一个是来自TIMx_ETR引脚上的外部时钟,对于TIM2,TIM2_ETR引脚复用在PA0上(即PA0的波形就是TIM2_ETR的时钟信号)。可以在PA0上接一个外部方波时钟信号,然后配置内部的极性选择、边沿检测和预分频器电路,再配置输入滤波电路。经过输入滤波的时钟信号有两个去向,第一个去向是ETRF,经过触发控制器的选择后流向时基单元;第二个去向是TRGI,占用触发输入通道,经过触发控制器的选择后流向时基单元。
[2]第二个外部时钟是ETR下面的ITR信号,它是由来自其它4个定时器的时钟信号(由其它定时器的TRGO输出)经过4选1得到的。通过TRGO和ITR可以实现定时器的级联,具体流程为,主定时器产生一次更新事件,其TRGO输出一个时钟脉冲到从定时器的ITR,从定时器收到一个时钟脉冲,从定时器的计数器值+1。

[3]第三个外部时钟是ITR下面的TI1F_ED,TI1F_ED连接输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟信号,通过这一路输入的时钟信号,上升沿和下降沿均有效。
[4]触发控制器还能选择TI1FP1和TI2FP2提供的时钟信号,其中TI1FP1就是CH1引脚的时钟信号,TI2FP2就是CH2引脚的时钟信号。
④触发控制器下的编码器接口可以读取正交编码器的输出波形,后续会有这方面的介绍,此处不过多赘述。
⑤时基单元下面的部分包含两块电路(以预分频器为轴线进行划分),两块电路同时包含捕获/比较寄存器。
[1]右边一块是输出比较电路,总共有4个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形。
[2]左边一块是输入捕获电路,总共有4个通道,也分别对应CH1到CH4的引脚,可以用于测量输入方波的频率。
[3]对于同一个定时器,输入捕获和输出比较不能同时使用。
(3)高级定时器框图:
相比于通用定时器,高级定时器在申请中断的地方增加了一个重复次数计数器,它可以实现每隔几个计数周期才发生一次更新事件和更新中断(相当于对输出的更新信号又做了一次分频)
对于高级定时器,最大定时时间可以设定得非常大

3、定时中断基本结构
(1)时钟输入源选择(给计数器提供计数脉冲):
①内部时钟模式(RCC内部时钟):最常用的定时模式,系统RCC外设时钟直接供给定时器。
②外部时钟模式2(ETR引脚,GPIO输入):外部引脚输入脉冲,每来一个脉冲,CNT就+1,用于外部脉冲计数。
③外部时钟模式1(ITRx,其它定时器输入):由别的定时器输出触发当前定时器,实现定时器级联。
④编码器模式(TIx 捕获通道,GPIO 输入):通常接正交编码器的A/B相,自动根据电平增减CNT,常用于电机位置及转速测量。
(2)时基单元:
①输入的时钟脉冲先经过PSC分频,公式为
计数脉冲频率=定时器原始时钟频率 / (PSC+1)
②CNT计数器接收PSC分频后的脉冲信号,每来1个脉冲计数值就+1(假设为向上计数模式)。
③ARR自动重装寄存器存储定时周期阈值,它决定多久触发一次中断。
(3)中断输出控制:
当CNT计数达到ARR设定的重装载值,会生成更新中断标志位,如果有配置允许更新中断,模块将向外输出中断请求信号
(4)NVIC:
①定时器的中断信号被送入内核NVIC,NVIC根据提前配置的中断优先级,仲裁是否打断主程序。
②允许中断时,CPU暂停main主循环,跳转到定时器中断服务函数执行,中断处理完毕后,返回主程序继续运行。

4、预分频器时序

(1)CK_PSC是输入预分频器的时钟信号。
(2)CNT_EN是计数器使能信号,高电平代表计数器能正常运行,低电平代表计数器停止。
(3)CK_CNT是计数器时钟脉冲(输入计数器的时钟脉冲,由预分频器输出):
①CNT_EN为低电平时,CK_CNT不会产生计数脉冲。
②CNT_EN为高电平时,时序图前半段预分频器的参数为1,计数器的时钟频率CK_CNT等于输入预分频器的时钟CK_PSC,时序图后半段预分频器的参数为2,计数器的时钟频率CK_CNT等于输入预分频器的时钟CK_PSC二分之一。
(4)在定时器时钟信号CK_CNT的驱动下,计数器寄存器的计数值随CK_CNT上升沿的出现不断自增,当计数值达到重装载值FC,且下一个CK_CNT上升沿来临时,计数值回到0,同时产生一个更新事件脉冲。
(5)用户可读写预分频控制寄存器,当用户改变预分频控制寄存器的值时,预分频器的参数不会马上改变,待一个计数周期结束后,缓冲寄存器(又称为影子寄存器)会将预分频控制寄存器的值读入,这时预分频器的参数才会真正被改变。
(6)预分频器的内部实际上是靠计数进行分频的,当参数为1时,预分频器不分频,当参数为2时,预分频计数器会进行计数,每2个计数输出一个计数脉冲到计数器,结果就是输出频率等于输入频率的二分之一。(计数脉冲频率:CK_CNT_f = CK_PSC_f / (PSC + 1))
5、计数器时序

(1)CK_INT是内部时钟,频率为72MHz。
(2)CNT_EN是时钟使能信号,高电平代表计数器时钟CK_CNT能正常运行,低电平代表计数器时钟CK_CNT关闭。
(3)由于分频因子为2,所以CK_CNT的频率是CK_INT的二分之一。
(4)CK_CNT产生上升沿时,计数器寄存器的计数值+1。当增加到ARR设定的自动重装值且下一个CK_CNT上升沿来临时,产生一个更新事件脉冲,同时将更新中断标志位UIF置为1(UIF置为1,会向CPU申请中断,中断响应后中断程序需要将该标志位清零),并且计数器中的值清零,同时开始下一次的计数计时。
(5)计数器溢出频率(中断产生频率):
CK_CNT_OV_f = CK_CNT_f / (ARR + 1) = CK_PSC_f / (PSC + 1) / (ARR + 1)
(6)计数器的ARR寄存器(自动加载寄存器)也设计了缓冲寄存器,是否使用该寄存器可以由用户自行选择:
①计数器无预装时序:

②计数器有预装时序:

6、RCC时钟树

(1)图中左边一部分是时钟的产生电路,右边一部分(以AHB预分频器为界)是时钟的分配电路。
(2)时钟产生电路有4个震荡源,其中前两个高速振荡器负责提供系统时钟(AHB、APB2、APB1的时钟都来源于这两个高速晶振)。
①内部的8MHz高速RC振荡器(8MHz HSI RC)。
②外部的4-16MHz高速石英晶体振荡器(4-16MHz HSE OSC,也就是晶振,一般接8MHz,负责提供系统时钟)。
③外部的32.768KHz低速晶振(LSE OSC 32.768 kHz,一般负责给RTC提供时钟)。
④内部的40kHz低速RC振荡器(LSI RC 40kHz,给看门狗提供时钟)。
(3)ST芯片中,SystemInit函数配置时钟的方式:
①首先启动内部时钟,选择内部的8MHz高速RC振荡器提供系统时钟,暂时以内部8MHz的时钟运行。
②接着再启动外部时钟(4-16MHz高速石英晶体振荡器),配置外部时钟进入PLL锁相环进行倍频,8MHz倍频9倍得到72MHz的时钟信号。
③待锁相环输出稳定后,选择锁相环输出为系统时钟SYSCLK,这样就把系统时钟的频率由8MHz切换为72MHz。
(4)72MHz的系统时钟信号进入AHB总线,AHB总线有一个预分频器,在SystemInit中配置的分配系数为1,那么AHB的时钟频率就是72MHz。
①AHB时钟信号进入APB1总线,APB1总线的预分频器系数被配置为2,所以APB1总线的时钟频率为36MHz,不过APB1总线的预分频器后面除了直接通向APB1外设的线路外,还有一条通向定时器的线路,因为APB1总线的预分频器系数为2,所以这里时钟的频率倍频(系数不为1就倍频),传到定时器的时钟信号频率就是72MHz。
②AHB时钟信号进入APB2总线,APB2总线的预分频器系数被配置为1,所以APB2总线的时钟频率为72MHz,APB2总线的预分频器后面除了直接通向APB2外设的线路外,也有一条通向定时器的线路,因为APB2总线的预分频器系数为1,所以这里时钟的频率不倍频,传到定时器的时钟信号频率就是72MHz。
③无论是高级定时器、通用定时器还是基本定时器,它们的内部基准时钟频率都被保证是72MHz,前提是不要随便修改SystemInit中的默认配置。
④在时钟信号进入外设前都必须经过一个与门,这时时钟信号是否能留下外设就由使能端控制,是否使能时钟,由程序员在代码中决定(不需要工作的外设不需要使能时钟,以此尽可能地减少能耗)。
7、定时器定时中断(示例程序)
(1)按照下图所示接好线路,并将使用OLED屏进行显示的工程文件夹作为模板复制一份使用。

(2)定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,下面先介绍比较常用的几个。
[1] TIM_DeInit函数:将指定的定时器外设(TIMx)的所有寄存器恢复为芯片上电复位后的默认值。
函数原型:void TIM_DeInit(TIM_TypeDef* TIMx);
参数解释:
TIM_TypeDef* TIMx:指定要进行复位的定时器外设
返回值:无返回值
[2] TIM_TimeBaseInit函数:根据用户指定的参数初始化定时器(TIMx)的时基单元,包括配置预分频器、计数模式、自动重载寄存器和时钟分频因子等参数。
函数原型:void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
参数解释:
TIM_TypeDef* TIMx:指定要初始化的定时器外设
TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct:指向时基初始化结构体的指针,该结构体包含了定时器时基单元的所有配置参数
返回值:无返回值
[3] TIM_TimeBaseStructInit函数:将TIM_TimeBaseInitTypeDef 结构体中的所有成员参数填充为它们的默认值(缺省值)。
函数原型:void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
参数解释:
TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct:指向要被填充默认值的TIM_TimeBaseInitTypeDef结构体的指针
返回值:无返回值
[4] TIM_Cmd函数:用于使能(Enable)或禁用(Disable)指定定时器的计数器,即控制定时器的计数核心是否开始运行。
函数原型:void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
FunctionalState NewState:ENABLE表示使能计数器,定时器开始计数;DISABLE表示禁用计数器,定时器停止计数
返回值:无返回值
[5] TIM_ITConfig函数:用于使能(Enable)或禁用(Disable)指定定时器的特定中断源,简单来说,它决定了定时器内部发生某个事件(如计数溢出)时,是否向CPU发送中断请求。
函数原型:void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_IT:指定要配置的中断源类型;这是一个位掩码,可使用按位或“|”一次性组合多个中断源
FunctionalState NewState:ENABLE表示使能指定的中断源;DISABLE表示禁用指定的中断源
返回值:无返回值
[6] TIM_InternalClockConfig函数:将指定定时器(TIMx)的时钟源配置为内部时钟(CK_INT),即由RCC(复位与时钟控制器)提供的APB总线时钟。
函数原型:void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
参数解释:
TIM_TypeDef* TIMx:指定要配置时钟源的定时器外设
返回值:无返回值
[7] TIM_ITRxExternalClockConfig函数:将指定的从定时器(Slave Timer)的时钟源配置为来自其它主定时器(Master Timer)的内部触发输入(ITRx,Internal Trigger x),从而实现定时器之间的级联。
函数原型:void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
参数解释:
TIM_TypeDef* TIMx:指定从定时器,即被配置为使用外部时钟的定时器
uint16_t TIM_InputTriggerSource:选择从定时器从哪个主定时器接收内部触发信号
返回值:无返回值
[8] TIM_TIxExternalClockConfig函数:将定时器的外部输入捕获通道引脚(TIx,即TIMx_CH1~CH4)配置为定时器的外部时钟源(外部时钟模式1),从而使计数器在每个选定输入引脚的有效边沿(上升沿、下降沿或双边沿)上进行计数。
函数原型:void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource, uint16_t TIM_ICPolarity, uint16_t ICFilter);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_TIxExternalCLKSource:选择用作时钟源的TIx输入源,它决定了时钟信号来自于哪个输入通道以及采用何种边沿检测方式
uint16_t TIM_ICPolarity:指定输入捕获的极性,即选择外部信号的有效边沿为上升沿还是下降沿
uint16_t ICFilter:设置输入捕获滤波器的采样频率和采样次数,用于对外部输入信号进行数字滤波,滤除高频噪声,有效取值范围为0x0~0x0F(即0~15)
返回值:无返回值
[9] TIM_ETRClockMode1Config函数:将定时器的ETR(External Trigger,外部触发输入)引脚配置为定时器的外部时钟源(外部时钟模式1),从而使计数器在每个ETR输入信号的有效边沿(上升沿或下降沿)上进行计数。
函数原型:void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_ExtTRGPrescaler:设置ETR输入信号的预分频系数,用于对过高频率的外部信号进行降频处理
uint16_t TIM_ExtTRGPolarity:指定ETR输入信号的有效边沿极性,决定计数器是在ETR引脚的上升沿还是下降沿进行计数
uint16_t ExtTRGFilter:设置ETR输入信号的数字滤波器带宽,可滤除外部信号中的高频噪声,防止计数器错误累加,该参数的有效范围为0x00~0x0F
返回值:无返回值
[10] TIM_ETRClockMode2Config函数:将定时器的ETR(External Trigger,外部触发输入)引脚配置为定时器的外部时钟源(外部时钟模式2),从而使得定时器的计数器在每个ETR输入信号的有效边沿(上升沿或下降沿)上进行计数。
函数原型:void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_ExtTRGPrescaler:设置ETR输入信号的预分频系数,用于对过高频率的外部信号进行降频处理
uint16_t TIM_ExtTRGPolarity:指定ETR输入信号的有效边沿极性,决定计数器是在ETR引脚的上升沿还是下降沿进行计数
uint16_t ExtTRGFilter:设置ETR输入信号的数字滤波器带宽,可滤除外部信号中的高频噪声,防止计数器错误累加,该参数的有效范围为0x00~0x0F
返回值:无返回值
[11] TIM_ETRConfig函数:配置定时器外部触发输入的四个核心参数——预分频系数、极性与边沿、滤波器带宽以及是否反相。
函数原型:void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_ExtTRGPrescaler:设置ETR输入信号的预分频系数,用于对过高频率的外部信号进行降频处理
uint16_t TIM_ExtTRGPolarity:指定ETR输入信号的有效边沿极性,并决定是否反相
uint16_t ExtTRGFilter:设置ETR输入信号的数字滤波器带宽,可滤除外部信号中的高频噪声,防止计数器错误累加,该参数的有效范围为0x00~0x0F
返回值:无返回值
[12] TIM_PrescalerConfig函数:在运行时(尤其是在定时器已经开始工作后)动态修改预分频器的值。
函数原型:void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t Prescaler:需要设置的目标预分频值
uint16_t TIM_PSCReloadMode:可选立即生效模式(新的分频值会立刻被装入预分频器的影子寄存器,并从下一个计数脉冲开始生效)和更新事件生效模式(新的分频值会在下一个更新事件发生时,才被装入影子寄存器)
返回值:无返回值
[13] TIM_CounterModeConfig函数:在运行时(尤其是在定时器已经开始工作后)动态修改计数器的计数模式。
函数原型:void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_CounterMode:需要设置的计数模式
返回值:无返回值
[14] TIM_ARRPreloadConfig函数:配置自动重载寄存器(ARR) 的影子寄存器更新方式。
函数原型:void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
FunctionalState NewState:ENABLE,ARR的新值被写入预装载寄存器,将在下一次更新事件发生时才被同步到真正起作用的影子寄存器中,在PWM占空比同步更新时能获得更平滑的波形;DISABLE,ARR的新值会立即更新到影子寄存器中,新的计数周期从下一个时钟开始生效
返回值:无返回值
[15] TIM_SetCount函数:将指定的定时器计数器寄存器(TIMx_CNT)设置为用户指定的任意值。
函数原型:void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t Counter:要写入计数器寄存器的新值
返回值:无返回值
[16] TIM_SetAutoreload函数:设置指定定时器的自动重载寄存器(TIMx_ARR)的值。
函数原型:void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t Autoreload:要写入ARR寄存器的自动重载值
返回值:无返回值
[17] TIM_GetCounter函数:读取指定定时器当前计数器寄存器(TIMx_CNT)的值。
函数原型:uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
参数解释:
TIM_TypeDef* TIMx:指定要读取计数器值的定时器外设
返回值:指定定时器当前计数器的瞬时值
[18] TIM_GetPrescaler函数:读取指定定时器当前预分频器寄存器(TIMx_PSC)的值。
函数原型:uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
参数解释:
TIM_TypeDef* TIMx:指定要读取预分频器值的定时器外设
返回值:指定定时器的当前预分频器值
[19] TIM_GetFlagStatus函数:读取定时器状态寄存器(TIMx_SR)中指定标志位的当前值。
函数原型:FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
参数解释:
TIM_TypeDef* TIMx:指定要查询标志位的定时器外设
uint16_t TIM_FLAG:指定要查询的状态标志位类型;该参数是一个位掩码,一次只能传入一个标志位(不能使用按位或组合),函数会检查状态寄存器中该标志位是否被置位
返回值:SET表示指定的状态标志位已被置位(硬件事件已发生);RESET表示指定的状态标志位未被置位(硬件事件未发生)
[20] TIM_ClearFlag函数:在主程序中清除指定定时器状态寄存器(TIMx_SR)中的一个或多个待处理状态标志位(置0)。
函数原型:void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
参数解释:
TIM_TypeDef* TIMx:指定要清除标志位的定时器外设
uint16_t TIM_FLAG:指定需要清除的标志位类型;该参数是一个位掩码,可使用按位或组合多个标志位,一次性清除多个待处理标志位
返回值:无返回值
[21] TIM_GetITStatus函数:检查指定定时器的特定中断源是否已发生且被使能,即判断是否有有效的中断请求等待处理。
函数原型:ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
参数解释:
TIM_TypeDef* TIMx:指定要查询中断状态的定时器外设
uint16_t TIM_IT:指定要检查的中断源类型;该参数是一个位掩码,一次只能检查一个中断源,不能使用按位或组合
返回值:SET表示指定的中断源已发生且已被使能——即存在有效的中断请求;RESET表示指定的中断源未发生,或虽已发生但中断未被使能
[22] TIM_ClearITPendingBit函数:在中断函数中清除指定定时器状态寄存器(TIMx_SR)中一个或多个待处理的中断标志位(置0)。
函数原型:void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
参数解释:
TIM_TypeDef* TIMx:指定要清除中断挂起位的定时器外设
uint16_t TIM_FLAG:指定需要清除的中断挂起位类型;该参数是一个位掩码,可以使用按位或“|”组合多个标志位,一次性清除多个中断挂起位
返回值:无返回值
(3)在项目的System组中添加Timer.h文件和Timer.c文件,用于封装定时器模块的代码。
①Timer.h文件:
#ifndef __Timer_H
#define __Timer_H
void Timer_Init(void);
#endif
②Timer.c文件:
#include "stm32f10x.h" // Device header
extern uint16_t Num; //声明外部变量,允许该文件使用/更改main.c中的Num
void Timer_Init(void)
{
//开启TIM2的RCC内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//选择时基单元的时钟源(对于定时中断,选择内部时钟源)
TIM_InternalClockConfig(TIM2);
//配置时基单元(预分频器参数、自动重装值、计数模式等)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//定时频率 = 72MHz / (PSC + 1) / (ARR + 1) = 1Hz(定时1秒)=> PSC = 7199,ARR = 9999
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_IT_Update);
//TIM_TimeBaseInit会将中断标志位置为1,这里需要清除一下
//否则定时器2初始化完成时会立马发生一次中断(导致计数会从1而不是0开始)
//配置输出中断控制,允许更新中断输出到NVIC
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//配置NVIC,在NVIC中打开定时器2中断的通道并分配优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void TIM2_IRQHandler(void) //中断函数可以写在main.c中
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断这个中断是不是由定时器2触发
{
Num++; //计数值+1
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志位
}
}
(4)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,OLED屏显示的计数应该是从0开始逐秒+1。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
}
}
8、定时器外部时钟(示例程序)
(1)按照下图所示接好线路,并将定时器定时中断的工程文件夹作为模板复制一份使用。

(2)修改项目的System组中添加Timer.h文件和Timer.c文件。
①Timer.h文件:
#ifndef __Timer_H
#define __Timer_H
void Timer_Init(void);
uint16_t Timer_GetCount(void);
#endif
②Timer.c文件:
#include "stm32f10x.h" // Device header
extern uint16_t Num; //声明外部变量,允许该文件使用/更改main.c中的Num
void Timer_Init(void)
{
//开启TIM2的RCC内部时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
//选择时基单元的时钟源(选择通过ETR的外部时钟源)
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x00);
//TIM_ExtTRGPSC_OFF:不需要分频
//TIM_ExtTRGPolarity_NonInverted:高电平/上升沿有效
//0x00:不使用滤波器(采样点个数为0)
//TIM2_ETR引脚和PA0在一起复用,需要配置GPIOA(PA0的波形就是输入TIM2的时钟)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元(预分频器参数、自动重装值、计数模式等)
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1; //ARR自动重装器的值(10个脉冲产生一次中断,计数值会归零)
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //PSC预分频器的值(不分频)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_IT_Update);
//TIM_TimeBaseInit会将中断标志位置为1,这里需要清除一下
//否则定时器2初始化完成时会立马发生一次中断(计数会从1而不是0开始)
//配置输出中断控制,允许更新中断输出到NVIC
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
//配置NVIC,在NVIC中打开定时器2中断的通道并分配优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
uint16_t Timer_GetCount(void)
{
return TIM_GetCounter(TIM2); //返回定时器2计数器的值
}
void TIM2_IRQHandler(void) //中断函数可以写在main.c中
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断这个中断是不是由定时器2触发
{
Num++; //计数值+1
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志位
}
}
(3)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,按照主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
OLED_ShowString(2,1,"CNT:");
while(1)
{
OLED_ShowNum(1,5,Num,5); //TIM2每产生一次中断(遮挡传感器10次),Num值+1
OLED_ShowNum(2,5,Timer_GetCount(),5); //显示当前遮挡次数(达到10后会清零)
}
}
二、使用定时器输出PWM信号
1、定时器的输出比较
(1)OC(Output Compare,输出比较)可以通过比较CNT(时基单元中的计数器)与CCR寄存器(捕获/比较寄存器)值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形。
(2)PWM(Pulse Width Modulation)即脉冲宽度调制,在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速、开关电源等领域。

(3)每个高级定时器和通用定时器都拥有4个输出比较通道,高级定时器的前3个通道额外拥有死区生成和互补输出的功能。
①通用定时器的输出部分(以通道1为例):

[1]CNT是计数器的值(4个通道共用一个CNT,所以4个通道的频率相同),CCR1是第1路的捕获/比较寄存器的值(决定PWM占空比),两个值进行比较,将CNT≥CCR1的判断结果送入输出模式控制器,输出模式控制器根据判断结果和OC1M(OC1M输出比较的模式)生成基准电平信号oc1ref。
|
输出比较的模式 |
描述 |
|
冻结 |
CNT=CCR时,REF保持为原状态 |
|
匹配时置有效电平 |
CNT=CCR时,REF置有效电平 |
|
匹配时置无效电平 |
CNT=CCR时,REF置无效电平 |
|
匹配时电平翻转 |
CNT=CCR时,REF电平翻转 |
|
强制为无效电平 |
CNT与CCR无效,REF强制为无效电平 |
|
强制为有效电平 |
CNT与CCR无效,REF强制为有效电平 |
|
PWM模式1 |
向上计数:CNT<CCR时,REF置有效电平,CNT≥CCR时,REF置无效电平(如下图所示,有效电平为高电平) 向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平 |
|
PWM模式2 |
向上计数:CNT<CCR时,REF置无效电平,CNT≥CCR时,REF置有效电平 向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平 |

[2]oc1ref有两个去向,第一个去向是主模式控制器,可以将oc1ref映射到主模式的TRGO输出上,用来触发其它定时器/ADC/DAC;第二个去向是右侧的极性选择器,CC1P为0时,oc1ref信号的电平不翻转直接输出,CC1P为1时,oc1ref信号的电平翻转后输出。
[3]输出使能电路决定经过电平翻转器的信号能不能输出到OC1引脚(CH1通道的引脚),CC1E为0时,通道关闭,OC1引脚不受定时器控制;CC1E为1时,通道打开,内部波形输出到引脚OC1。
[4]PWM模式的参数计算:
PWM频率:Freq = CK_PSC_f / (PSC + 1) / (ARR + 1)
PWM占空比:Duty = CCR / (ARR + 1)
PWM分辨率:Reso = 1 / (ARR + 1)(比如分辨率为1%,那么占空比的可调单位就为1%)
(4)高级定时器前三个通道的输出部分(以通道1为例):

2、LED呼吸灯(示例程序)
(1)理论上给LED灯一个正弦模拟信号,LED灯的亮度可以从低慢慢变高,再从高慢慢变低,但是这在单片机中难以实现,单片机只能直接输出数字信号,但借助PWM调制,给LED灯一个占空比不断变化的方波脉冲,其实就相当于给LED灯一个电压不断变化的模拟信号。

(2)按下图所示接好电路,并将使用OLED屏进行显示的工程文件夹作为模板复制一份使用。(本次LED灯的正极连接PA0,负极连接GND,使用的是高电平驱动方法)

(3)定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,这里再介绍几个。
[1] TIM_OCxInit函数:初始化指定定时器的特定输出比较通道,配置其输出模式、极性、使能状态和初始脉冲宽度等参数。(x可取1~4,是4个通道的编号)
函数原型:void TIM_OCxInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
TIM_OCInitTypeDef* TIM_OCInitStruct:包含输出比较通道所有配置参数的结构体
返回值:无返回值
[2] TIM_ForcedOCxConfig函数:强制将指定定时器的输出比较通道的输出电平设置为有效(Active)或无效(Inactive)状态,而不依赖于计数器与捕获/比较寄存器的比较结果,不过这只是接管了输出电平的控制权,定时器的其它功能(如中断请求)不受影响。(x可取1~4,是4个通道的编号)
函数原型:void TIM_ForcedOCxConfig(TIM_TypeDef* TIMx, uint16_t TIM_ForcedAction);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_ForcedAction:指定强制输出的电平状态,TIM_ForcedAction_Active代表有效电平,TIM_ForcedAction_InActive代表无效电平(有效/无效和高/低的对应关系,取决于极性配置CC1P)
返回值:无返回值
[3] TIM_OCxPreloadConfig函数:使能或禁用指定定时器输出比较通道的预装载寄存器(Preload Register)。(x可取1~4,是4个通道的编号)
函数原型:void TIM_OCxPreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_OCPreload:指定预装载功能的使能或禁用状态,TIM_OCPreload_Enable表示使能(新装载值在更新事件时生效,也就是一个PWM周期结束时生效),TIM_OCPreload_Disable表示禁用(新装载值立即生效)
返回值:无返回值
[4] TIM_OCxFastConfig函数:使能或禁用指定定时器输出比较通道的快速使能功能(Output Compare Fast Enable),该功能仅在使用单脉冲模式(One Pulse Mode)时有效。(x可取1~4,是4个通道的编号)
函数原型:void TIM_OCxFastConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCFast);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_OCFast:指定输出比较快速使能功能的使能或禁用状态,TIM_OCFast_Enable表示使能,TIM_OCFast_Disable表示禁用
返回值:无返回值
[5] TIM_ClearOCxRef函数:使能或禁用指定定时器输出比较通道的OCxREF信号清除功能。(x可取1~4,是4个通道的编号)
函数原型:void TIM_ClearOCxRef(TIM_TypeDef* TIMx, uint16_t TIM_OCClear);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_OCClear:指定OCxREF清除功能的使能或禁用状态,TIM_OCClear_Enable表示使能,TIM_OCClear_Disable表示禁用
返回值:无返回值
[6] TIM_OCxPolarityConfig函数:单独配置指定定时器输出比较通道的输出极性(输出极性决定了OCx引脚上有效电平的定义是高电平还是低电平)及输入捕获通道的捕获边沿,即配置CCER寄存器的CCxP位。(x可取1~4,是4个通道的编号)
函数原型:void TIM_OCxPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_OCPolarity:指定通道的输出极性(或输入捕获边沿),TIM_OCPolarity_High表示高电平有效/上升沿捕获,TIM_OCPolarity_Low表示低电平有效/下降沿捕获
返回值:无返回值
[7] TIM_OCxNPolarityConfig函数:在运行时单独配置高级定时器互补输出通道(OCxN)输出极性的函数。(x可取1~3)
函数原型:void TIM_OCxNPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCNPolarity);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_OCNPolarity:指定互补通道的输出极性,TIM_OCNPolarity_High表示高电平有效,TIM_OCNPolarity_Low表示低电平有效
返回值:无返回值
[8] TIM_CCxCmd函数:独立地使能或禁用(开启或关闭)一个指定的定时器捕获/比较通道。(此处的x是函数名的一部分,不是通道编号)
函数原型:void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_Channel:指定要操作的通道
uint16_t TIM_CCx:指定通道的新状态,TIM_CCx_Enable表示使能,TIM_CCx_Disable表示禁用
返回值:无返回值
[9] TIM_CCxNCmd函数:独立使能或禁用(开启或关闭)高级定时器指定通道的互补输出(OCxN)。(此处的x是函数名的一部分,不是通道编号)
函数原型:void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_Channel:指定要操作的通道
uint16_t TIM_CCxN:指定通道的新状态,TIM_CCxN_Enable表示使能,TIM_CCxN_Disable表示禁用
返回值:无返回值
[10] TIM_SelectOCxM函数:在运行时动态修改指定定时器输出比较通道的输出比较模式(OCxM)。(此处的x是函数名的一部分,不是通道编号)
函数原型:void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t TIM_Channel:指定要操作的通道
uint16_t TIM_OCMode:指定要设置的输出比较模式
返回值:无返回值
[11] TIM_SetComparex函数:设置指定定时器特定通道的捕获/比较寄存器(CCRx)的值,即写入新的比较值。(x可取1~4,是4个通道的编号)
函数原型:void TIM_SetComparex(TIM_TypeDef* TIMx, uint16_t Comparex);
参数解释:
TIM_TypeDef* TIMx:指定要操作的定时器外设
uint16_t Comparex:要写入捕获/比较寄存器(CCRx)的新比较值
返回值:无返回值
[12] TIM_CtrlPWMOutputs函数:使能或禁用高级定时器的主输出(Main Output Enable, MOE),即PWM输出的“总开关”。
函数原型:void TIM_CtrlPWMOutputs(TIM_TypeDef* TIMx, FunctionalState NewState);
参数解释:
TIM_TypeDef* TIMx:指定要操作的高级定时器
FunctionalState NewState:指定主输出的使能或禁用状态,ENABLE表示使能,DISABLE表示禁用
返回值:无返回值
[13] TIM_OCStructInit函数:将输出比较初始化结构体TIM_OCInitTypeDef的所有成员填充为默认值。
函数原型:void TIM_OCStructInit(TIM_OCInitTypeDef* TIM_OCInitStruct);
参数解释:
TIM_OCInitTypeDef* TIM_OCInitStruct:指向TIM_OCInitTypeDef结构体的指针,该结构体包含了定时器输出比较的所有配置参数
返回值:无返回值
(4)TIM2_CH1_ETR复用于PA0引脚,也就是说,TIM2的比较输出信号会来到PA0,那么在配置GPIOA的模式时,需要选择复用推挽输出而不能选择普通推挽输出,因为PA0的信号来源是TIM2(片上外设),而不是输出数据寄存器。

(5)引脚重映射配置:
①要进行引脚重映射,首先打开AFIO的时钟(AFIO相当于一个数据选择器,可以选择引脚连接到哪个外设上,供该外设使用)。
②如果想把PA15、PB3、PB4三个引脚(调试端口)当作普通GPIO口引脚使用,需要将JTAG复用解除,执行“GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);”即可。
③如果想重映射定时器或其它外设的复用引脚,比如将TIM2_CH1_ETR映射到PA15上,执行“GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENABLE);”语句即可。
④如果重映射的引脚正好是调试端口,那么需要将JTAG复用解除,然后进行映射。
⑤TIM2复用功能引脚重映射关系如下所示,本例需要将TIM2_CH1_ETR的信号输出到PA0,不需要重映射。

(6)在项目的Hardware组中添加PWM.h文件和PWM.c文件,用于封装PWM模式相关的代码。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere1(uint16_t Compare);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH1_ETR与PA0复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //选择复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 99
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 719
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)(占空比越大LED灯越亮)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的值
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare); //修改CCR的值(修改占空比)
}
(7)编译整个工程,将程序下载到开发板中,可以看到LED灯有呼吸灯的现象。
3、驱动舵机(示例程序)
(1)舵机概述:
①舵机可简单认为是一种根据输入PWM信号占空比来控制输出角度的装置。(实际的舵机控制远比这复杂得多)

②输入PWM信号要求:周期为20ms(频率为50Hz),高电平宽度为0.5ms~2.5ms。

(2)按下图所示接好电路,并将PWM驱动LED呼吸灯的工程文件夹作为模板复制一份使用。(这次使用的是TIM2的通道2,舵机的输入信号连接PA1)

(3)修改PWM.h文件和PWM.c文件。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere2(uint16_t Compare);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH2与PA1复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //选择复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 19999
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 50Hz => PSC = 71
TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)(ARR取值范围500~2500)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的初始值
TIM_OC2Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere2(uint16_t Compare)
{
TIM_SetCompare2(TIM2, Compare); //修改CCR的值(修改占空比)
}
(4)在项目的Hardware组中添加Servo.h文件和Servo.c文件,用于封装舵机模块的代码。
①Servo.h文件:
#ifndef __Servo_H
#define __Servo_H
void Servo_Init(void);
void Servo_SetAngle(float Angle);
#endif
②Servo.c文件:
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Servo_Init(void)
{
PWM_Init();
}
void Servo_SetAngle(float Angle)
{
PWM_SetCompere2(Angle /180 *2000 + 500);
//Angle为旋转角度,这里采取的角度范围是0°~180°
//当Angle为90°时,输入信号中的一个脉冲要有1.5ms处于高电平
//PWM_SetCompere2会将CCR的值修改为1500
//占空比Duty = CCR / (ARR + 1) = 1500 / 20000(也就是1.5ms/20ms)
}
(5)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,根据主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Key.h"
#include "Servo.h"
uint8_t KeyNum = 0;
float Angle;
int main()
{
OLED_Init();
OLED_ShowString(1,1,"Angle:");
Servo_Init(); //初始化舵机(舵机包含PWM模块,PWM也会在其中初始化)
Key_Init();
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1) //按下按键1,舵机旋转30°
{
Angle += 30;
if(Angle > 180) //如果已经旋转到最大值,回到初始状态0°
Angle = 0;
}
Servo_SetAngle(Angle); //更改当前旋转角度
OLED_ShowNum(1,7,Angle,3);
}
}
4、驱动直流电机(示例程序)
(1)直流电机概述:
①直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转。
②直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作,TB6612(下左图)是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。



(2)按下图所示接好电路,并将PWM驱动LED呼吸灯的工程文件夹作为模板复制一份使用。(这次使用的是TIM2的通道3,直流电机的输入信号连接PA2)

(3)修改PWM.h文件和PWM.c文件。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere3(uint16_t Compare);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH3与PA2复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //需要选择复用推挽输出,具体可看GPIO的位结构图
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 99
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 719
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)(占空比越大电机速度越快)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的值
TIM_OC3Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere3(uint16_t Compare)
{
TIM_SetCompare3(TIM2, Compare); //修改CCR的值(修改占空比)
}
(4)在项目的Hardware组中添加Motor.h文件和Motor.c文件,用于封装直流电机模块的代码。
①Motor.h文件:
#ifndef __Motor_H
#define __Motor_H
void Motor_Init(void);
void Motor_SetSpeed(int8_t Speed);
#endif
②Motor.c文件:
#include "stm32f10x.h" // Device header
#include "PWM.h"
void Motor_Init(void)
{
PWM_Init();
//使能GPIOA的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置端口模式
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5; //电机控制模式输入端
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
}
void Motor_SetSpeed(int8_t Speed)
{
if(Speed >= 0) //正转
{
GPIO_SetBits(GPIOA, GPIO_Pin_4);
GPIO_ResetBits(GPIOA, GPIO_Pin_5);
PWM_SetCompere3(Speed); //修改速度(修改占空比)
}
else //反转
{
GPIO_SetBits(GPIOA, GPIO_Pin_5);
GPIO_ResetBits(GPIOA, GPIO_Pin_4);
PWM_SetCompere3(Speed); //修改速度(修改占空比)
}
}
(5)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,根据主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Motor.h"
#include "Key.h"
uint8_t KeyNum;
int8_t Speed;
int main()
{
OLED_Init();
Motor_Init();
Key_Init();
OLED_ShowString(1,1,"Speed:");
while(1)
{
KeyNum = Key_GetNum();
if(KeyNum == 1) //按下按键1,电机转速+20
{
Speed += 20;
if(Speed > 100) //电机转速上限100(占空比最大100%)
Speed = -100; //切换为反向满速转动
}
Motor_SetSpeed(Speed); //修改当前转速
OLED_ShowNum(1,7,Speed,3);
}
}
三、使用定时器捕获输入信号
1、定时器的输入捕获
(1)IC(Input Capture)输入捕获可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。
①输入捕获可配置为PWMI模式,同时测量频率和占空比。
②输入捕获可配合主从触发模式,实现硬件全自动测量。
(2)频率测量方法有测频法和测周法两种,测频法适合测量高频信号,测周法适合测量低频信号。(N越大,误差越小)

(3)每个高级定时器和通用定时器都拥有4个输入捕获通道(以通道1为例)。

①输入信号从TIMx_CH1进入定时器的输入通道1,首先信号经过滤波器处理(CCMR1寄存器的ICF位可以控制滤波器的参数,参数越大,滤波效果越好),接着信号TI1F进入边沿检测器,边沿检测器可以选择上升沿触发或下降沿触发,当出现指定的电平跳变时,边沿检测电路就会触发后续电路执行相应的动作。
②边沿检测器的输出信号TI1FP1经过极性选择器(极性选择由CCER寄存器的CC1P位决定),然后来到下一个数据选择器,该选择器选择TIMx_CH1、TIMx_CH2和TRC(来自从模式控制器)中的一个信号(通道3和通道4也是这样的结构,信号选择由CCMR1寄存器的CC1S位决定)通过,这样做有两个目的:
[1]可以灵活切换后续捕获电路的输入。
[2]可以把一个引脚的输入同时映射到两个捕获单元。
③经过数据选择器后,信号进入(预)分频器(分频参数由CCMR1寄存器的ICPS位决定),经过分频后的触发信号可以触发捕获电路进行工作。
④每有一个触发信号,CNT的值就会向CCR转运一次(CNT可以记录两个上升沿的时间间隔,它由定时器内部时钟驱动,充当计时器的角色),转运的同时会发生一个捕获事件,这个事件会在状态寄存器置标志位,同时也可以产生中断(捕获中断)。每捕获一次触发信号,都要将CNT清零,这一步可以使用主从触发模式完成。
⑤TI1FP1除了前往数据选择器,还可以前往从模式控制器,如果配置从模式触发源为TI1FP1,触发后执行Reset动作,则可以自动完成TI1FP1信号自动触发CNT清零的操作。
(4)主从触发模式:
①主模式可以将定时器内部的信号映射到TRGO引脚,用于触发其它外设。
②从模式可以接收其它外设或者自身外设的一些信号用于控制自身定时器的运行,需要选择指定的一个信号得到TRGI,TRGI触发从模式,从模式可以选择一项操作自动执行。

2、输入捕获模式测频率(示例程序)
(1)这里使用测周法测量PWM信号的频率,两个上升沿的时间间隔正好为一个周期,CNT负责计时,每有一个上升沿,CNT的值就会向CCR1转运一次并且自动清零,CCR1接收到的就是测得的周期值。

(2)按照下图所示接好线路,并将PWM驱动LED呼吸灯的工程文件夹作为模板复制一份使用。(由定时器自己产生信号,再让另一个定时器捕获,进行测量)

(3)修改PWM.h文件和PWM.c文件。
①PWM.h文件:
#ifndef __PWM_H
#define __PWM_H
void PWM_Init(void);
void PWM_SetCompere1(uint16_t Compare);
void PWM_SetPrescaler(uint16_t Prescaler);
#endif
②PWM.c文件:
#include "stm32f10x.h" // Device header
void PWM_Init(void)
{
//打开TIM2外设的时钟和GPIO外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
//配置GPIO(TIM2_CH1_ETR与PA0复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //选择复用推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置TIM2的时钟源和时基单元
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
//分辨率Reso = 1 / (ARR + 1) = 1% => ARR = 99
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 719
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; //ARR自动重装器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 720 - 1; //PSC预分频器的值
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
//配置输出比较单元
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure); //给结构体赋一个初始值,便于代码移植
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; //PWM模式1
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; //电平不翻转
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //输出使能
//占空比Duty = CCR / (ARR + 1)
TIM_OCInitStructure.TIM_Pulse = 0; //CCR中的值
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
//使能定时器2
TIM_Cmd(TIM2, ENABLE);
}
void PWM_SetCompere1(uint16_t Compare)
{
TIM_SetCompare1(TIM2, Compare); //修改CCR的值(修改占空比)
}
void PWM_SetPrescaler(uint16_t Prescaler)
{
TIM_PrescalerConfig(TIM2, Prescaler, TIM_PSCReloadMode_Immediate); //修改PSC的值(修改频率)
}
(4)定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,这里再介绍几个(仅简单介绍,需要使用时转去看函数上方的注释即可)。
[1] TIM_ICInit函数:根据 TIM_ICInitTypeDef 结构体中指定的参数,初始化指定定时器的某个输入捕获通道,配置参数有捕获通道、通道的捕获极性、输入映射关系、预分频器和数字滤波器。
函数原型:void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
TIM_ICInitTypeDef* TIM_ICInitStruct:包含了输入捕获通道所有配置参数的结构体
返回值:无返回值
[2] TIM_PWMIConfig函数:将定时器配置为PWM输入模式(PWMI),以测量外部PWM信号的频率和占空比。
函数原型:void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
TIM_ICInitTypeDef* TIM_ICInitStruct:包含输入捕获通道配置参数的结构体;用户只需配置一个通道的参数,函数会自动配置另一个通道(1和2为一对,3和4为一对)
返回值:无返回值
[3] TIM_ICStructInit函数:将输入捕获初始化结构体TIM_ICInitTypeDef的所有成员填充为安全、确定的默认值。
函数原型:void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);
参数解释:
TIM_ICInitTypeDef* TIM_ICInitStruct:指向TIM_ICInitTypeDef结构体的指针
返回值:无返回值
[4] TIM_SelectInputTrigger函数:选择指定定时器的输入触发源,即决定定时器的触发信号(TRGI, Trigger Input)来自哪个内部或外部信号。
函数原型:void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_InputTriggerSource:选择输入触发源的类型
返回值:无返回值
[5] TIM_SelectOutputTrigger函数:选择指定定时器(作为主设备)的触发输出(TRGO, Trigger Output)信号来源,即决定该定时器通过内部连接向其它外设发送何种同步信号。
函数原型:void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_TRGOSource:指定触发输出(TRGO)信号的来源
返回值:无返回值
[6] TIM_SelectSlaveMode函数:配置指定定时器的从模式(Slave Mode),决定定时器如何响应其输入触发信号(TRGI)。
函数原型:void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_SlaveMode:指定从模式类型,决定定时器对触发信号(TRGI)的响应方式
返回值:无返回值
[7] TIM_SetICxPrescaler函数:独立设置每个输入捕获通道的预分频器。(x可取1~4,是4个通道的编号)
函数原型:void TIM_SetICxPrescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_ICPSC:指定输入捕获的预分频值
返回值:无返回值
[8] TIM_GetCapturex函数:读取指定定时器输入捕获通道的捕获/比较寄存器(TIMx_CCRx)的值,即获取在捕获事件发生瞬间被锁存的计数器值。(x可取1~4,是4个通道的编号)
函数原型:uint16_t TIM_GetCapturex(TIM_TypeDef* TIMx);
参数解释:
TIM_TypeDef* TIMx:指定要读取捕获值的定时器外设
返回值:指定通道的捕获/比较寄存器(CCRx)的当前值
(5)在项目的Hardware组中添加IC.h文件和IC.c文件,用于封装输入捕获相关的代码。
①IC.h文件:
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
#endif
②IC.c文件:
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
//RCC开启时钟,将GPIO和TIM3的时钟打开
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//GPIO初始化,将GPIO配置成输入模式(TIM3_CH1与PA6复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR自动重装器的值(填最大值防止溢出)
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 71
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器的值(设置测周法标准频率为1MHz)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //直连通道输入(通道1输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
//选择从模式触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
//选择从模式触发执行的操作
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
//开启定时器
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //读取CCR的值并用它计算TIM2产生信号的频率(单位Hz)
}
(6)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,OLED屏应显示测得TIM2产生的信号频率为1000Hz。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main()
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
//TIM2比较输出信号的参数设置
PWM_SetPrescaler(720 - 1); //频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz
PWM_SetCompere1(50); //占空比Duty = CCR / (ARR + 1)
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
}
}
3、PWMI模式测频率和占空比(示例程序)
(1)PWMI基本结构:
①TL1FP1负责捕获周期(用于计算频率,频率即周期的倒数),两个上升沿的时间间隔正好为一个周期,CNT负责计时,每有一个上升沿,CNT的值就会向CCR1转运一次并且自动清零(将从模式触发源配置为TL1FP1,从模式执行Reset,也就是CNT清零操作)。
②TL1FP2负责捕获脉宽(用于计算占空比,占空比=脉宽/周期×100%),在一个周期开始时上升沿将CNT清零,CNT开始计时,当捕获到下降沿时,CNT的值就是脉宽,CNT的值向CCR2转运,但是不清零。

(2)按照下图所示接好线路,并将输入捕获模式测频率的工程文件夹作为模板复制一份使用。(由定时器自己产生信号,再让另一个定时器捕获,进行测量)

(3)修改IC.h文件和IC.c文件:
①IC.h文件:
#ifndef __IC_H
#define __IC_H
void IC_Init(void);
uint32_t IC_GetFreq(void);
uint32_t IC_GerDuty(void);
#endif
②IC.c文件:
#include "stm32f10x.h" // Device header
void IC_Init(void)
{
//RCC开启时钟,将GPIO和TIM3的时钟打开
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//GPIO初始化,将GPIO配置成输入模式(TIM3_CH1与PA6复用)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR自动重装器的值(填最大值防止溢出)
//频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz => PSC = 71
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //PSC预分频器的值(设置测周法标准频率为1MHz)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //直连通道输入(通道1输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_PWMIConfig(TIM3, &TIM_ICInitStructure); //仅支持通道1和通道2
/*配置输入捕获单元等效代码
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //直连通道输入(通道1输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
TIM_ICInitStructure.TIM_Channel = TIM_Channel_2; //选择通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Falling; //下降沿触发
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //不分频
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_IndirectTI; //交叉通道输入(通道2输入通道1)
TIM_ICInit(TIM3, &TIM_ICInitStructure);
*/
//选择从模式触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);
//选择从模式触发执行的操作
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);
//开启定时器
TIM_Cmd(TIM3, ENABLE);
}
uint32_t IC_GetFreq(void)
{
return 1000000 / (TIM_GetCapture1(TIM3) + 1); //读取CCR的值并用它计算TIM2产生信号的频率(单位Hz)
}
uint32_t IC_GerDuty(void)
{
return (TIM_GetCapture2(TIM3) + 1) * 100 / (TIM_GetCapture1(TIM3) + 1); //计算占空比
}
(4)在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,OLED屏应显示测得TIM2产生的信号频率为1000Hz、占空比为50%。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "PWM.h"
#include "IC.h"
int main()
{
OLED_Init();
PWM_Init();
IC_Init();
OLED_ShowString(1,1,"Freq:00000Hz");
OLED_ShowString(2,1,"Duty:000%");
//TIM2比较输出信号的参数设置
PWM_SetPrescaler(720 - 1); //频率Freq = CK_PSC / (PSC + 1) / (ARR + 1) = 1000Hz
PWM_SetCompere1(50); //占空比Duty = CCR / (ARR + 1)
while(1)
{
OLED_ShowNum(1,6,IC_GetFreq(),5);
OLED_ShowNum(2,6,IC_GerDuty(),3);
}
}
4、编码器接口测速(示例程序)
(1)Encoder Interface编码器接口:
①编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度。

②每个高级定时器和通用定时器都拥有1个编码器接口。
③两个输入引脚借用了输入捕获的通道1和通道2。

(2)编码器接口基本结构:
ARR中的值应设置为最大值65535,CNT从0开始计数,当编码器正向旋转时,CNT的值递增,当编码器反向旋转时,CNT的值递减
如果CNT的值为0,那么CNT再往下递减就会从65535开始递减,如果以补码形式读取计数器中的值,那么这时读取CNT得到的恰好是-1

(3)计数方向与编码器信号的关系:

(4)两个计数实例:(在TI1和TI2上计数)
①TI1和TI2均不反相:

②TI1反相:

(5)编码器接口测速示例程序:
①按照下图所示接好线路,并将定时器定时中断的工程文件夹作为模板复制一份使用。(定时器的编码器一般给电机测速,本例用旋转编码器模拟电机旋转)

②定时器相关的库函数声明在stm32f10x_tim.h文件的底部,与定时器有关的库函数有非常多,这里再介绍一个函数——TIM_EncoderInterfaceConfig函数,用于将指定的定时器配置为编码器接口模式。
函数原型:void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx, uint16_t TIM_EncoderMode, uint16_t TIM_IC1Polarity, uint16_t TIM_IC2Polarity);
参数解释:
TIM_TypeDef* TIMx:指定要配置的定时器外设
uint16_t TIM_EncoderMode:指定编码器接口的工作模式,决定计数器在哪些边沿进行计数
uint16_t TIM_IC1Polarity:指定通道1(TI1)的输入捕获极性
uint16_t TIM_IC2Polarity:指定通道2(TI2)的输入捕获极性
返回值:无返回值
③项目的Hardware组中添加Encoder.h文件和Encoder.c文件用于封装旋转编码器的代码。
[1]Encoder.h文件:
#ifndef __Encoder_H
#define __Encoder_H
void Encoder_Init(void);
int16_t Encoder_Get(void);
#endif
[2]Encoder.c文件:
#include "stm32f10x.h" // Device header
void Encoder_Init(void)
{
//开启GPIO和定时器的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
//配置GPIO(TIM3通道1和通道2的输入引脚分别为PA6和PA7)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
//配置时基单元
TIM_InternalClockConfig(TIM3);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; //这个参数跟时基单元关系不大
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up ; //向上计数模式
TIM_TimeBaseInitStructure.TIM_Period = 65536 - 1; //ARR自动重装器的值(填最大值)
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1; //不分频,编码器的时钟直接驱动计数器
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; //重复计数器的值(高级定时器特有)
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
//配置输入捕获单元
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICStructInit(&TIM_ICInitStructure); //给结构体中的所有参数赋一个默认值
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道1
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInit(TIM3, &TIM_ICInitStructure); //配置通道1
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; //选择通道2
TIM_ICInitStructure.TIM_ICFilter = 0xF; //滤波器参数
TIM_ICInit(TIM3, &TIM_ICInitStructure); //配置通道2
//配置编码器接口模式
TIM_EncoderInterfaceConfig(TIM3, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
//TIM_EncoderMode_TI12:在TI1和TI2上计数
//TIM_ICPolarity_Rising:通道不反向(第三个参数配置通道1,第四个参数配置通道2)
//启动定时器
TIM_Cmd(TIM3, ENABLE);
}
int16_t Encoder_Get(void)
{
int16_t Temp = TIM_GetCounter(TIM3);
TIM_SetCounter(TIM3, 0); //每返回一次速度值(在TIM2定时时间内旋转多少次)CNT就清零,为下次测速做准备
return Temp; //获取TIM3的计数器值(并将其中的二进制数强制转换为有符号数)
}
④在main.c文件中粘贴以下代码,然后编译,将程序下载到开发板中,根据主函数中的注释进行调试。
#include "stm32f10x.h" // Device headerCmd
#include "OLED.h"
#include "Encoder.h"
#include "Delay.h"
#include "Timer.h"
int16_t Speed;
int main()
{
OLED_Init();
Encoder_Init();
Timer_Init();
OLED_ShowString(1,1,"Speed:");
while(1)
{
OLED_ShowSignedNum(1,7,Speed,5); //更新显示当前旋转速度(有符号数)
//Delay_ms(1000); Delay会阻塞主程序运行,不建议使用
}
}
void TIM2_IRQHandler(void) //使用TIM2的更新中断读取当前旋转速度
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) //判断这个中断是不是由定时器2触发
{
Speed = Encoder_Get(); //每隔1秒(TIM2的定时时间)读取一次旋转速度
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除中断标志位
}
}更多推荐


所有评论(0)