STM32-TIM定时器

本内容基于江协科技STM32视频内容,整理而得。

1. TIM

1.1 TIM定时器

  • 定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断
  • 16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时。1/(72/65536/65536)(计数器就是用来执行计数定时的一个寄存器,每来一个时钟,计数器加1。预分频器可以对计数器的时钟进行分频,让计数更加灵活。自动重装寄存器就是计数的目标值,就是想要计多少个时钟申请中断。这些寄存器构成了定时器最核心的部分,把这一块电路称为时基单元)
  • 不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。
  • 根据复杂度和应用场景分为高级定时器、通用定时器、基本定时器三种类型。

1.2 定时器类型

类型 编号 总线 功能
高级定时器 TIM1、TIM8 APB2 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能
通用定时器 TIM2、TIM3、TIM4、TIM5 APB1 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能
基本定时器 TIM6、TIM7 APB1 拥有定时中断、主模式触发DAC的功能
  • STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4
  • DAC:数模转换器

1.3 基本定时器

主要功能:

  • 16位自动重装载累加计数器
  • 16位可编程预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频
  • 触发DAC的同步电路
  • 在更新事件(计数器溢出)时产生中断/DMA请求

image.png

  • 预分频器PSC
    预分频器之前连接的就是基准计数时钟的输入。由于基本定时器只能选择内部时钟,因此可以认为预分频器的输入线是连接到内部时钟(CK_INT)。
    内部时钟的来源是RCC的TIMxCLK,这里的频率值一般都是系统的主频72MHz。
    预分频器写0,就是不分频;写1就是2分频,输出频率=输入频率/2=36MHz。
    写2就是3分频,输出= 输入/3。所以预分频器的值和实际的分频系数相差了1,实际分频系数=预分频器的值 + 1。预分频器是16位的,所以最大值可以写65535,也就是65536分频。

  • 计数器CNT
    计数器可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加1。计数器也是16位的,所以里面的值可以从0一直加到65535。如果再加的话,计数器就会回到0重新开始。所以计数器的值在计数过程中会不断自增运行,当自增运行到目标值时,会产生中断,那就完成了定时的任务。所以还需要一个存储目标值的寄存器,那就是自动重装寄存器了。

  • 自动重装寄存器
    自动重装寄存器也是16位的,它存的就是要写入的计数目标。在运行的过程中,计数值不断自增,自动重装值是固定的目标,当计数值等于自动重装值时,也就是计时时间到了。那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。

  • U和UI箭头
    图上画的一个向上的折现箭头,就代表这里会产生中断信号,像这种计数值等于自动重装值产生的中断,一般把它叫做“更新中断”。这个更新中断之后就会通往NVIC,我们再配置好NVIC的定时器通道,那定时器的更新中断就能够得到CPU的响应了。向下的箭头,代表的是会产生一个事件,这里对应的事件就叫做“更新事件”。更新事件不会触发中断,但可以触发内部其他电路的工作。

1.4 通用定时器

主要功能:

  • 16位向上、向下、向上/向上 自动重装载累加计数器

  • 16位可编程预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频

  • 4个独立通道:

    • 输入捕获
    • 输出比较
    • PWM生成(边缘或中间对齐模式)
    • 单脉冲模式输出
  • 使用外部信号控制定时器和定时器互连的同步电路

  • 如下事件发生时产生中断/DMA:

    • 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或内部/外部触发)
    • 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
    • 输入捕获
    • 输出比较
  • 支持针对定位的增量(正交)编码器和霍尔传感器电路

  • 触发输入作为外部时钟或者按周期的电流管理
    image.png

  • CNT计数器
    CNT计数器支持向上计数模式、向下计数模式、中央对齐模式。向下计数模式就是从重装值开始,向下自减,减到0之后,回到重装值同时申请中断。中央对齐模式是从0开始,先向上自增,计到重装值,申请中断,然后再向下自减,减到0,再申请中断。

  • 时钟源
    通用定时器的时钟源不仅可以选择内部72MHz时钟,还可以选择外部时钟。
    (1)第一个外部时钟就是来自TIMx_ETR引脚上的外部时钟,也就是可以在TIM2的ETR引脚即PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,以及输入滤波电路,这两块电路可以对外部时钟进行一定的整形。因为是外部引脚的时钟,所以难免有一些毛刺,那这些电路就可以对输入的波形进行滤波。滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了。这一路也叫做“外部时钟模式2”。
    image.png
    (2)TRGI也可以提供外部时钟,主要用作触发输入使用的,这个触发输入可以触发定时器的从模式。当TRGI当作外部时钟来使用时,这一路就叫做“外部时钟模式1”。通过这一路的外部时钟有哪些呢?第一个就是ETR引脚的信号。第二个是ITR信号,这一部分的时钟信号是来自其他定时器的。主模式的TRGO可以通向其他定时器,通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了,ITR0到ITR3分别来自其他4个定时器的TRGO输出。也可以选择TI1F_ED,这里连接的是输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思。还可以通过TI1FP1和TI2FP2获得,TI1FP1是连接到了CH1引脚的时钟,TI2FP2连接到了CH2引脚的时钟。
    总结:外部时钟模式1的输入可以是ETR引脚、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚。

  • 输出比较电路
    下面的右边部分是输出比较电路,总共有四个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形,驱动电机。

  • 输入捕获电路
    左边是输入捕获电路,也是有四个通道,对应的也是CH1到CH4的引脚,可以用于测输入方波的频率等。

  • 捕获/比较寄存器
    中间的寄存器是捕获/比较寄存器,是输入捕获和输出比较电路共用的,因为输入捕获和输出比较不能同时使用,所以这里寄存器是共用的,引脚也是共用的。

1.4 高级定时器

主要功能:

  • 16位向上、向下、向上/向上 自动重装载累加计数器
  • 16位可编程预分频器,用于对输入的时钟按系数为1~65536之间的任意数值分频
  • 4个独立通道:
    • 输入捕获
    • 输出比较
    • PWM生成(边缘或中间对齐模式)
    • 单脉冲模式输出
  • 死区时间可编程的互补输出
  • 使用外部信号控制定时器和定时器互连的同步电路
  • 允许在指定数据的计数器周期之后更新定时器寄存器的重复计数器
  • 刹车输入信号可以将定时器输出信号置于复位状态或者一个已知状态
  • 如下事件发生时产生中断/DMA:
    • 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或内部/外部触发)
    • 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)
    • 输入捕获
    • 输出比较
    • 刹车信号输入
  • 支持针对定位的增量(正交)编码器和霍尔传感器电路
  • 触发输入作为外部时钟或者按周期的电流管理

image.png

  • 重复次数计数器
    和通用定时器的区别:第一个是在申请中断的地方增加了一个重复次数计数器,有了这个计数器之后,就可以实现每隔几个计数周期才发生一次更新事件和更新中断。原来的结构是每个计数周期完成后就都会发生更新,这就相当于对输出的更新信号又做了一次分频。

  • DTG死区生成器和互补输出
    下面是高级定时器对输出比较模块的升级了,DTG(dead time generate)是死区生成电路。右边的输出引脚由原来的一个变为了两个互补的输出,可以输出一对互补的PWM波,这些电路是为了驱动三相无刷电机的,比如四轴飞行器、电动车的后轮、电钻等,里面都可能是三相无刷电机。因为三相无刷电机的驱动电路一般需要3个桥臂,每个桥臂2个大功率开关管来控制,所以总共需要6个大功率开关管。因此这里的输出PWM引脚的前三路就变为了互补的输出。另外,为了防止互补输出的PWM驱动桥臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,所以前面就加上了死区生成电路。在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全都关断,防止直通现象。

  • 刹车输入
    最后一部分就是刹车输入的功能了,这个是为了给电机驱动提供安全保障的。如果外部引脚BKIN(Break IN)产生了刹车信号,或内部时钟失效,产生了故障,那么控制电路就会自动切断电机的输出,防止意外的发生。

1.5 定时中断基本结构

image.png
运行控制:控制寄存器中的一些位,如启动停止、向上或向下计数等。
右边就是计时时间到,产生更新中断后的信号去向,如果是高级定时器的话,还会多一个重复计数器。中断信号会先在状态寄存器里置一个中断标志位,这个标志位会通过中断输出控制,到NVIC申请中断。中断输出控制就是一个中断输出的允许位,如果需要某个中断,就记得允许一下。
image.png
image.png

1.6 预分频器时序

image.png

  • 计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)
  • CK_PSC:预分频器时钟,内部时钟就是72MHz。
  • CNT_EN:计数器使能,高电平计数器正常运行,低电平计数器停止。
  • CK_CNT:计数器时钟,它既是预分频器的时钟输出,也是计数器的时钟输入。
  • 在开始时,计数器未使能,计数器时钟不运行。使能后,前半段,实际分频系数为1(PSC=0),计数器的时钟等于预分频器前的时钟;后半段,实际分频系数为2(PSC=1)了,计数器的时钟也就变为预分频器前时钟的一半了。在计数器时钟的驱动下,下面的计数器寄存器也跟随时钟的上升沿不断自增,在中间的这个位置FC之后,计数值变为0了,从这里可以推断出ARR自动重装值就是FC。当计数值计到和重装值相等,并且下一个时钟来临时,计数值才清零,同时下面产生一个更新事件,这就是一个计数周期的工作流程。
  • 下面的三行描述的是预分频寄存器的一种缓冲机制,也就是这个预分频寄存器实际上是有两个,一个是预分频控制寄存器,是供我们读写用的,它并不直接决定分频系数。另外还有一个缓冲寄存器或者说是影子寄存器:预分频缓冲器,这个缓冲寄存器才是真正起作用的寄存器,比如我们在某个时刻,把预分频寄存器由0改成了1,如果在此时立刻改变时钟的分频系数,那么就会导致在一个计数周期内,前半部分和后半部分的频率不一样。因此设计了缓冲器,当计数计到一半的时候改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频寄存器的值才会被传递到缓冲寄存器里,才会生效。
  • 最后一行可以看出:预分频器内部也是靠计数来分频的,当预分频值为0时,计数器就一直为0,直接输出原频率;当预分频值为1时,计数器就0、1、0、1、0、1这样计数,在回到0时,输出1个脉冲,这样输出频率就是输入频率的2分频,预分频器的值和实际的分频系数之间有一个数的偏移。

1.7 计数器时序

image.png

  • 计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)= CK_PSC / (PSC + 1) / (ARR + 1)
  • CK_INT:内部时钟72MHz;
  • CNT_EN:计数器使能,上升沿有效;
  • CK_CNT:计数器时钟,因为分频系数为2,所以这个频率是CK_INT除2。然后计数器在这个时钟每个上升沿自增,当增到0036的时候,发生溢出。计到36之后,再来一个上升沿,计数器清零,计数器溢出,产生一个更新事件脉冲,另外还会置一个更新中断标志(UIF),更新中断标志(UIF)置1了,就会申请中断,中断响应后,需要在中断程序中手动清零。

1.8 计数器无预装时序

image.png
更改了自动加载寄存器,由FF改成了36,那计数值的目标值就由FF变成了36,所以这里计到36之后,就直接更新开始下一轮的计数,

1.9 计数器有预装时序

image.png
在计数的中途,把计数目标值由F5改成了36。下面有个影子寄存器,这个影子寄存器才是真正起作用的,它还是F5,所以现在计数的目标还是计到F5,产生更新事件,同时,要更改的36才被传递到影子寄存器,在下一个计数周期这个更改的36才有效,所以这个引入影子寄存器的目的实际上是为了同步,就是让值的变化和更新事件同步发生,防止在运行途中,更改造成错误。

1.10 RCC时钟树

image.png

  • 时钟源
    在时钟产生电路,有四个震荡源:
    (1)HSI:内部的8MHz高速RC振荡器;
    (2)HSE:外部的4~16MHz高速石英晶体振荡器,也就是晶振,一般都是接8MHz;
    (3)LSE:外部的32.768KHz低速晶振,这个一般是给RTC提供时钟的;
    (4)LSI:最后是内部的40KHz低速RC振荡器,这个可以给看门狗提供时钟。
    上面两个高速晶振是用提供系统时钟的,AHB、APB2、APB1的时钟都是来源于这两个高速晶振,只不过是外部的石英振荡器比内部的RC振荡器更加稳定,所以一般都是用外部晶振。

  • ST配置时钟
    在SystemInit函数里,ST配置时钟:首先会启动内部时钟HSI,选择内部8MHz为系统时钟,暂时以内部8MHz的时钟运行。然后再启动外部时钟,配置外部时钟进入PLL锁相环进行倍频,8MHz倍频9倍,就得到72MHz,等到锁相环输出稳定后,选择锁相环输出为系统时钟,这样就把系统时钟由8MHz切换为了72MHz。

  • CSS时钟安全系统
    CSS(clock security system):时钟安全系统,也是负责切换时钟的,可以监测外部时钟的运行状态,一旦外部时钟失效,就会自动把外部时钟切换回内部时钟,保证系统时钟的运行,防止程序卡死造成事故。

  • 时钟分配电路

    • AHB总线: 首先系统时钟72MHz进入AHB总线,AHB总线有个预分频器,在SystemInit里配置的分配系数为1,那AHB的时钟就是72MHz。
    • APB1总线:这里配置的分配系数为2,所以APB1总线的时钟为72MHz/2=36MHz。下面的如果APB1预分频系数=1,则频率不变,否则频率*2,然后右边,是单独为定时器2-7开通的,因为这里预分频系数,我们给的是2,所以这里频率要再*2,所以通向定时器2~7的时钟为72MHz。因此无论是高级定时器、通用定时器还是基本定时器,它们的内部基准时钟都是72MHz。
    • APB2总线:APB2的分频系数为1,所以时钟为72MHz。然后接在APB2上的时钟也单开了一路,即如果APB2预分频系数=1,则频率不变,否则频率*2。因为分频系数给的是1,所以定时器1和8的时钟就是72MHz。
    • 时钟输出部分都有一个与门进行输出控制,控制位写的是外部时钟使能,这就是我们再在程序中写RCC_APB2/1PeriphClockCmd作用的地方,打开时钟,就是在这个位置写1,让左边的时钟能够通过与门输出给外设。

2. TIM库函数及代码

2.1 TIM库函数

// 恢复缺省配置
void TIM_DeInit(TIM_TypeDef* TIMx);

// 时基单元初始化
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

// 结构体变量赋一个默认值
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

// 使能计数器---运行控制
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);

// 使能中断输出---中断输出控制
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

/*-----------------时基单元的时钟选择---------*/
// 选择内部时钟
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);

// 选择ITRx其他定时器的时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);

// 选择TIx捕获通道的时钟
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
                                uint16_t TIM_ICPolarity, uint16_t ICFilter);

// 选择ETR通过外部时钟模式1输入的时钟
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
                             uint16_t ExtTRGFilter);

// 选择ETR通过外部时钟模式2输入的时钟
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, 
                             uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);

/*--------------------------------------------------------*/
// 不是用来选择时钟的,单独用来配置ETR引脚的预分频器、极性、滤波参数的
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
                   uint16_t ExtTRGFilter);

// 用来单独写预分频值的
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);

// 用来改变计数器的计数模式
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);

// 自动重装器预装功能配置
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);

// 给计数器写入一个值
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);

// 给自动重装器写入一个值
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);

// 获取当前计数器的值
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);

// 获取当前的预分频器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);

/*-------------获取标志位和清除标志位的------------*/
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);


2.2 6-1定时器中断代码

2.2.1 硬件电路:

实现功能:定时1s,并在OLED上显示Num值(中断触发次数,也即定时时间)
image.png

2.2.2 代码流程

  1. 定时器代码

    1. 开启时钟RCC,定时器的基准时钟和整个外设的工作时钟都会同时打开了;
    2. 选择时基单元的时钟源,对于定时中断,选择内部时钟源;
    3. 配置时基单元,包括预分频器、自动重装器、计数模式等–用结构体;
    4. 配置中断输出控制,允许更新中断输出到NVIC;
    5. 配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级;
    6. 运行控制;
      整个模块配置完成后,还需要使能一下计数器,要不然计数器是不会运行的。当定时器使能后,计数器就开始计数了,当计数器更新时,触发中断,最后再写一个定时器的中断函数,这样这个中断函数就每隔一段时间就能自动执行一次了。
  2. 中断函数

    1. 判断是否进入定时器TIM2的中断,然后清除中断标志位。
    2. 使定时器每秒自动加一下Num变量
  3. ARR和PSC设置

    1. ARR=10000 - 1;PSC = 7200 - 1;
    2. 定时器时钟CK_CNT =72M / (PSC + 1) = 10000 ;
    3. 定时频率 = CK_CNT / (ARR + 1) = 1。定时1s,也就是定时频率为1Hz。

预分频是对72M进行7200分频,得到的就是10K的计数频率,在10K的频率下,计10000个数,就是1s的时间。(在1s的时间内计10000个数,当计到10000个数后,自动清0,同时申请中断,在OLED显示屏上就是显示Num值每1s加1)。也可以更改ARR和PSC的值。

  1. main函数
    1. OLED显示Num值。

2.2.3 代码

  • Timer.c代码
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:定时中断初始化
  * 参    数:无
  * 返 回 值:无
  */
void Timer_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;		//时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;	//计数器模式,选择向上计数
	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_TimeBaseInit,配置TIM2的时基单元	
	
	/*中断输出配置*/
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
																//TIM_TimeBaseInit函数末尾,手动产生了更新事件
																//若不清除此标志位,则开启中断后,会立刻进入一次中断
																//如果不介意此问题,则不清除此标志位也可
	
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

  • main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"

uint16_t Num;			//定义在定时器中断里自增的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Timer_Init();		//定时中断初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Num:");			//1行1列显示字符串Num:
	
	while (1)
	{
		OLED_ShowNum(1, 5, Num, 5);			//不断刷新显示Num变量
	}
}

/**
  * 函    数:TIM2中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)		//判断是否是TIM2的更新事件触发的中断
	{
		Num ++;												//Num变量自增,用于测试定时中断
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);			//清除TIM2更新事件的中断标志位
															//中断标志位必须清除
															//否则中断将连续不断地触发,导致主程序卡死
	}
}

2.3 6-2定时器外部时钟代码

2.3.1 硬件电路

  • 将对射式红外传感器接在PA0引脚,即TIM2_CH1_ETR,时钟配置为外部时钟模式2。
  • 每次对传感器进行遮挡时,计数值CNT(TIM_GetCounter(TIM2))加1,当加到ARR时,触发中断,使Num值加1。
  • 在OLED显示屏上显示Num和CNT的值。

image.png

2.3.2 代码流程

  1. 定时器代码
    1. 开启时钟RCC,TIM2和GPIOA。
    2. 配置GPIO,为上拉输入。
    3. 选择时基单元的时钟源,外部时钟模式2,时钟从TIM2_ETR引脚输入;
    4. 配置时基单元,包括预分频器、自动重装器、计数模式等–用结构体;
    5. 配置中断输出控制,允许更新中断输出到NVIC;
    6. 配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级;
    7. 运行控制;
  2. ARR和PSC
    1. ARR = 10 - 1; PSC = 1 - 1;
    2. 因为没有进行分频,所以对射式红外传感器每遮挡一次,计数值CNT加1,当计数值加到9后,自动清零,同时申请中断,Num++。
  3. main函数
    1. 实现在OLED显示屏上显示Num和CNT的值。

2.3.3 代码

  • Timer.c
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:定时中断初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数配置为外部时钟,定时器相当于计数器
  */
void Timer_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);						//将PA0引脚初始化为上拉输入
	
	/*外部时钟配置*/
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
																//选择外部时钟模式2,时钟从TIM_ETR引脚输入
																//注意TIM2的ETR引脚固定为PA0,无法随意更改
																//最后一个滤波器参数加到最大0x0F,可滤除时钟信号抖动
	
	/*时基单元初始化*/
	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的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;			//重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);				//将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元	
	
	/*中断输出配置*/
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
																//TIM_TimeBaseInit函数末尾,手动产生了更新事件
																//若不清除此标志位,则开启中断后,会立刻进入一次中断
																//如果不介意此问题,则不清除此标志位也可
																
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:返回定时器CNT的值
  * 参    数:无
  * 返 回 值:定时器CNT的值,范围:0~65535
  */
uint16_t Timer_GetCounter(void)
{
	return TIM_GetCounter(TIM2);	//返回定时器TIM2的CNT
}

  • main.c
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"

uint16_t Num;			//定义在定时器中断里自增的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	Timer_Init();		//定时中断初始化
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "Num:");			//1行1列显示字符串Num:
	OLED_ShowString(2, 1, "CNT:");			//2行1列显示字符串CNT:
	
	while (1)
	{
		OLED_ShowNum(1, 5, Num, 5);			//不断刷新显示Num变量
		OLED_ShowNum(2, 5, Timer_GetCounter(), 5);		//不断刷新显示CNT的值
	}
}

/**
  * 函    数:TIM2中断函数
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数为中断函数,无需调用,中断触发后自动执行
  *           函数名为预留的指定名称,可以从启动文件复制
  *           请确保函数名正确,不能有任何差异,否则中断函数将不能进入
  */
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)		//判断是否是TIM2的更新事件触发的中断
	{
		Num ++;												//Num变量自增,用于测试定时中断
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);			//清除TIM2更新事件的中断标志位
															//中断标志位必须清除
															//否则中断将连续不断地触发,导致主程序卡死
	}
}

相关推荐

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-10 22:32:02       5 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-10 22:32:02       5 阅读
  3. 在Django里面运行非项目文件

    2024-07-10 22:32:02       4 阅读
  4. Python语言-面向对象

    2024-07-10 22:32:02       7 阅读

热门阅读

  1. EventBus原理分析

    2024-07-10 22:32:02       10 阅读
  2. Modelsim中使用tcl命令导出仿真数据到txt文件

    2024-07-10 22:32:02       11 阅读
  3. Spring中@Transactional的实现和原理

    2024-07-10 22:32:02       9 阅读
  4. H5小游戏开发,广告游戏开发制作

    2024-07-10 22:32:02       10 阅读
  5. 电脑多开卡顿的所有原因汇总

    2024-07-10 22:32:02       13 阅读
  6. 【C语言】通过fgets和fscanf了解读写文件流的概念

    2024-07-10 22:32:02       10 阅读