STM32单片机入门学习笔记——定时器TIM第一部分

笔记整理自B站UP主江科大自化协教程《STM32入门教程-2023持续更新中》,所用单片机也为教程推荐单片机。

大致内容

第一部分:定时器基本定时的功能,定时器每隔这个时间产生一个中断,来实现每隔一个固定时间执行一段程序的目的,比如要做一个时钟、秒表或者使用一些程序算法的时候都需要用到定时中断这个功能

第二部分:定时器输出比较的功能,最常见的用途就是产生PWM波形,用于驱动电机等设备

第三部分:定时器输入捕获的功能,使用输入buhuo这个模块来实现测量方波频率的例子

第四部分:定时器的编码器接口,使用编码器接口能够更加方便地读取正交编码器的输出波形,在编码电机测速中,应用广泛

使用定时器的外部时钟,可以提供一个更加精准的时钟来计时或者也可以把外部时钟当做一个计数器,用来统计引脚上电平翻转的次数。

TIM简介

  • TIM(Timer)定时器,定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断

当计数器的输入时一个准确可靠的基准时钟(STM32中一般都是主频72MHz)的时候,在对这个基准时钟进行计数的过程,实际上就是计时的过程。

如果对72MHz计72个数,那就是1MHz即1us的时间;如果计72000个数,那就是1KHz即1ms的时间

  • 16位的计数器、预分频器、自动重装寄存器时基单元(最核心的部分),在72MHz计数时钟下可以实现最大59.65s的定时

计数器:用来执行计数定时的一个寄存器,每来一个时钟,计数器加1

预分频器:可以对计数器的时钟进行分频,让这个计数更加灵活

自动重装寄存器:计数的目标值,即想要计多少个时钟申请中断

72M/65536/65536得到中断频率,取到数即为59.65s

STM32定时器支持级联的模式,也就是一个定时器的输出当做另一个定时器的输入,这样最大定时时间:59.65*65536*65536

  • 不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。根据复杂度和应用场景分为了高级定时器(最复杂)、通用定时器基本定时器三种类型。

定时器类型

  • STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4(没有基本定时器)

基本定时器

基本定时器只能选择内部时钟

基本定时器框图

预分频器

写0:不分频或者一分频,输出频率=输入频率=72MHz;

写1:二分频,输出频率=输入频率/2=36MHz;

写2:三分频,输出频率=输入频率/3=24MHz;

概括归纳:实际分频系数 = 预分频器的值 + 1

预分频器是16位的,所以最大值可以写65535,也就是65536分频

计数器

可以对预分频后的计数时钟进行计数,计数时钟每来一个上升沿计数器的值就+1,计数器也是16位的,里面的值可以从0~65535

UI(向上箭头)

计数值等于自动重装值产生的中断——更新中断;

U(向下箭头)

会产生一个事件——更新事件

更新事件不会触发中断,但可以触发内部其他电路的工作

主模式触发DAC功能

它能让内部的硬件在不受程序的控制下实现自动运行

使用DAC的时候,可能会用DAC输出一段波形,就需要每隔一段时间来触发一次DAC,让它输出下一个电压点,通过把定时器的更新事件映射到这个触发输出TRGO(Trigger Out)的位置,这样定时器的更新就不需要通过中断来触发DAC转换了,整个过程不需要软件的参与,实现了硬件的自动化。

通用定时器

对于通用定时器而言,计数器的计数模式就不止向上计数一种,还有向下计数模式中央对齐模式

中央对齐模式:先向上自增,计到重装值,申请中断,然后再向下自减,减到0,申请中断,一次循环...

ITR和定时器的连接关系

ITR0~3:可用于实现定时器的级联,比如可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,再初始化TIM2,选择TIR2,对应就是TIM3的TRGO,再选择时钟为外部时钟模式1(TRGI),这样TIM3的更新事件就可以驱动TIM2的时基单元

TI1F_ED:ED(edge边沿)

外部时钟模式1

输入可以是ETR引脚(外部时钟)、其他定时器、CH1引脚的边沿、CH1引脚和CH2引脚

外部时钟模式2

定时器主模式输出

输出比较电路

可以用于输出PWM波形,驱动电机

输入捕获电路

可以用于测量输入方波的频率

输入捕获和输出比较共用的寄存器

高级定时器

相对于通用定时器增加的部分

可以实现每隔几个计数周期,才发生一次更新事件和更新中断(相当于对输出的更新信号又做了一次分频),59.65*65536s

DTG(Dead Time Generate)死区生成电路

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

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

定时中断基本结构图

PSC:Prescaler预分频器

CNT:Counter计数器

ARR:AutoReloadRegister自动重装定时器

中断输出控制:中断输出的允许位,如果需要某个中断就允许一下

预分频器时序

CK_PSC:预分频器的输入时钟

CNT_EN:计数器使能,高电平计数器正常运行,低电平计数器停止

CN_CNT:计数器时钟输入,也是预分频器的时钟输出

根据上图可以推断ARR自动重装值就是FC

预分频缓冲器:影子寄存器(可以看通用定时器高级定时器那张图,方框带有阴影的都是影子寄存器),这是真正起作用的寄存器

比如在某个时刻,把预分频控制寄存器由0改成1,如果在此时立刻改变时钟的分频系数,会导致在一个计数周期内,前半部分和后半部分的频率不一致,不太严谨;这个预分频缓冲器,就是当在计数计到一半的时候改变了分频值,这个变化并不会立刻生效,而是会等到本次计数周期结束时,产生了更新事件,预分频控制寄存器的值才会被传送到预分频缓冲器中,才会生效。

  • 计数器计数频率:CK_CNT = CK_PSC / (PSC + 1)

计数器时序

CK_INT:内部时钟72MHz

  • 计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)

CK_PSC=72MHz

取倒数即为溢出时间

计数器无预装时序

没有缓冲寄存器的情况

计数器有预装时序

有缓冲寄存器的情况

引入影子寄存器的目的:为了同步,让值的变化和更新事件同步,防止在运行途中更改造成错误

RCC时钟树

用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统

程序中主函数之前还会执行一个SystemInit函数,这个函数就是用来配置整个时钟树的

左半边是时钟产生电路;右半边是时钟分配电路

SYSCLK:系统时钟72MHz

时钟产生电路有四个震荡源,分别是内部8MHz高速RC振荡器、外部的4~16MHz高速石英晶体振荡器(晶振一般都是接8MHz)、外部的32.768KHz的低速晶振(一般是给RTC提供时钟)、内部的40KHz低速RC振荡器(给看门狗提供时钟)

两个高速晶振是用来提供系统时钟,AHB、APB1、APB2的时钟都是来源于这两个高速晶振

PLL:锁相环,8MHz倍频9倍,得到72MHz,等到锁相环输出稳定后选择锁相环输出为系统时钟

一开始是内部8MHz提供系统时钟,后锁相环输出为系统时钟,由8MHz切换为72MHz,这是ST配置的流程。

CSS:Clock Security System 时钟安全系统,负责切换时钟,可以监测外部时钟的运行状态,一旦外部时钟失效,就会自动把外部时钟切换回内部时钟,保证系统时钟的运行,防止程序卡死造成事故,高级定时器中也有CSS。

APB1总线的时钟:72MHz/2=36MHz

无论基本定时器、通用定时器、高级定时器的内部基准时钟都是72MHz

外设时钟配置就是指我们程序中配置时钟

定时中断代码讲解(定时器定时中断变量++为例)

第一步:RCC开启时钟

第二步:选择时基单元的时钟源

第三步:配置时基单元

第四步:配置输出中断控制,允许更新中断输出到NVIC

第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级

第六步:运行控制,使能计数器

上述步骤可参考定时器中断基本结构图:

定时器中断基本结构图

先来看一下定时器的库函数,stm32f10x_tim.h

TIM_DeInit——恢复缺省配置

TIM_TimeBaseInit——时基单元初始化

TIM_TimeBaseStructInit——给结构体变量赋一个默认值

TIM_Cmd——使能计数器,对应图中的运行控制

TIM_ITConfig——用来使能中断输出信号,对应图中的中断输出控制

TIM_InternalClockConfig——选择内部时钟

TIM_ITRxExternalClockConfig——选择ITRx其他定时器的时钟

TIM_TIxExternalClockConfig——选择TIx捕获通道的时钟

TIM_ETRClockMode1Config——选择ETR通过外部时钟模式1输入的时钟

Polarity——极性、Filter——滤波器

TIM_ETRClockMode2Config——选择ETR通过外部时钟模式2输入的时钟

TIM_ETRConfig——单独用来配置ETR引脚的预分频器、极性、滤波器这些参数的

TIM_PrescalerConfig——单独用来写预分频值的

TIM_CounterModeConfig——用来改变计数器的计数模式

TIM_ARRPreloadConfig——自动重装器预装功能配置,前面提到过有预装和无预装

TIM_SetCounter——给计数器写入一个值

TIM_SetAutoreload——给自动重装器写入一个值

TIM_GetCounter——获取当前计数器的值,如果想看这个计数器计到哪里了可以调用这个函数

TIM_GetPrescaler——获取当前预分频器的值

TIM_GetFlagStatus、TIM_ClearFlag、TIM_GetITStatus、TIM_ClearITPendingBit——获取标志位和清除标志位,讲外部中断的时候也提到过,不知道的可以参考那一节

第一步——开启时钟

主体代码

注意这里是APB1

第二步——选择时基单元的时钟

选择为内部时钟,当然定时器上电后默认使用内部时钟,不写这一行也行

主体代码

第三步——配置时基单元

TIM_ClockDivision

  • 之前有提到过滤波器可以滤掉信号抖动干扰,如果工作呢?

就是在一个固定的是时钟频率f下对信号进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定,把采样值输出,如果这N个采样值不全相同,那就说明信号有抖动,这时就保持上一次的输出或者直接输出低电平,这样就能保证输出信号在一定程度上的滤波,这里的采样频率f和采样点数N都是滤波器的参数,频率越低采样点数越多,那滤波效果就越好,不过相应的信号延迟就越大。

  • 采样频率f从哪来?

内部时钟直接而来,也可以是由内部时钟加一个时钟分频而来,那分频多少由TIM_ClockDivision函数决定

TIM_CounterMode

向上计数、向下计数、三种中央对齐的计数模式

TIM_Period

周期,ARR自动重装器的值

TIM_Prescaler

PSC,预分频器的值

TIM_RepetitionCounter

重复计数器的值,这个是高级定时器才有的,这里不需要用,直接给0

定1s的时间如何配置参数呢?

公式:计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)

定时1s->定时频率1Hz

套用公式,比如PSC = 7200 - 1,ARR = 10000 - 1

72MHz / 7200 / 10000 = 1Hz

注意PSC和ARR的取值在0~65535之间

PSC和ARR的取值不是唯一的,可以预分频给少点,自动重装给多点,这样就是以一个比较高的频率(时间短)计比较多的数;也可以预分频给多点,自动重装给少点,这样就是以一个比较低的频率(时间长)计比较少的数

主体代码

第四步——使能更新中断

主体代码

开启了更新中断到NVIC的通路

第五步——配置NVIC

主体代码

具体解释可以参考外部中断那一节

第六步——启动定时器

主体代码

代码分析

可以通过这个函数看一下计数器的值,我们这里是0~9999变化的,对应了之前代码中的TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;

中断函数代码部分

中断函数名都是规定好的,启动文件中找,startup_stm32f10x_md.s

主体代码

定时中断代码讲解(定时器外部时钟端口PA0为例)

上一部分代码理解了,这一部分就可以在上一部分的代码上稍作更改

第一步——开启时钟

主体代码

第二部——配置GPIO

表格中给的是浮空输入,这里建议用上拉输入

如果外部的输入信号功率很小,内部的上拉电阻可能会影响到这个输入信号,这时可用浮空输入,防止影响外部输入的电平。

主体代码

第三步——选择时基单元的时钟

这和之前不一样,这里就是来源于外部时钟了

主体代码

第二个参数:外部触发预分频器,TIM_ExtTRGPSC_OFF不需要分频

第三个参数:外部触发的极性,TIM_ExtTRGPolarity_Inverted反向(低电平或者下降沿有效);TIM_ExtTRGPolarity_NonInverted不反向(高电平或者上升沿有效)

第四个参数:外部触发滤波器,之前第三步讲时基单元的时候有介绍过滤波器,就是以采用频率f采样N个点,具体怎样的对应关系看手册:

写0x00不用滤波器

第四步——配置时基单元

主体代码

公式:计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)

计数器溢出频率 = 72MHz / 1 / 10 = 7200000Hz

第五步——使能更新中断

主体代码

第六步——配置NVIC

主体代码

第七步——启动定时器

主体代码

编程小技巧

跨文件使用变量,extern声明变量就是告诉编译器,这个变量在别的文件中也定义了,这个变量就是另外文件中同一个变量的引用,这个过程并没有定义新的变量,其实头文件中的函数声明也是用extern实现的,这个extern可以省略,一般不写。

中断函数可以放在main.c文件中,也可以放在其他模块化的文件中

其他问题

在TIM_TimeBaseInit函数中最后有这么一句

生成一个更新事件,立刻来重新装载预分频器的值和重复计数器的值

预分频器是有一个缓冲寄存器的,我们写的值只有在更新事件时才会起作用,所以这里为了让值立刻起作用,在这最后手动生成了一个更新事件,这样预分频器的值就有效了,但同时它导致了更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,当我们之后一旦初始化完了,更新中断就会立刻进入,这就是我们刚一上电就立刻进中断的原因。

解决方法

感谢抽出宝贵时间阅读的各位小读者们,创作不易,如果感觉有帮助的话,帮忙点个赞再走吧!你们的支持是我创作的动力,希望能带给大家更多优质的文章。

  • 16
    点赞
  • 53
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
如果您的STM32单片机屏幕一直闪烁,可能是因为您的程序中使用了循环延时的方式,导致CPU一直忙碌而无法及时刷新屏幕。为了解决这个问题,可以使用STM32定时器中断来实现LED的闪烁,从而释放CPU资源,让它能够及时地刷新屏幕。 以下是使用TIM中断方式实现LED闪烁的示例代码: ```c #include "stm32f10x.h" void TIM2_IRQHandler(void) // 定时器2中断服务函数 { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) // 检查是否发生了更新事件 { static uint8_t led_state = 0; if (led_state == 0) { GPIO_SetBits(GPIOC, GPIO_Pin_13); // PC13输出高电平 led_state = 1; } else { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // PC13输出低电平 led_state = 0; } TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 清除中断标志位 } } int main(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); // 使能GPIOC时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 使能TIM2时钟 // 配置GPIOC.13为推挽输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOC, &GPIO_InitStructure); // 配置TIM2 TIM_TimeBaseStructure.TIM_Period = 999; // 自动重载值 TIM_TimeBaseStructure.TIM_Prescaler = 7199; // 预分频器 TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 使能更新中断 TIM_Cmd(TIM2, ENABLE); // 启动定时器 // 配置NVIC NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); while (1) { // 空闲循环 } } ``` 在上面的代码中,我们使用了定时器TIM2,并把它配置为1ms的定时器。在定时器中断服务函数中,我们通过改变LED的状态来实现LED的闪烁。由于使用了中断方式,CPU不会一直忙碌,从而可以释放CPU资源,让它能够及时地刷新屏幕。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AoXin_TechJZ.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值