ARM单片机滴答定时器理解与应用(一)(详细解析)

发布于:2025-07-10 ⋅ 阅读:(23) ⋅ 点赞:(0)


一、滴答定时器与定时器1区别

以中微芯片BAT32G137为例进行相关说明。

SysTick:滴答定时器
TIME1:定时器1

1.1 SysTick:滴答定时器(System Tick Timer)

其核心功能是周期性产生中断信号​(称为“滴答”),为系统提供稳定的时间基准,类似于钟表的“滴答”声,因此得名“滴答定时器”,滴答定时器(SysTick)作为ARM Cortex-M内核内置的24位递减计数器。

时钟源选择​:
可配置为内核时钟(如 HCLK,与 CPU 同频)或分频后的外部时钟(如 HCLK/8)
芯片手册:系统定时器SysTick是一个24位倒计时定时器,可选择fCLK或fIL计数时钟

![[Pasted image 20250708125842.png]]

自动重载​:
计数器从预设值(LOAD 寄存器)开始递减至 0 后,自动重载初值并触发中断(若使能)

在数字电路和嵌入式系统中,​计数器(如SysTick定时器)的计数行为主要由时钟上升沿驱动,这是同步时序逻辑设计的通用标准。因此我们一般就认为计数器的计数行为是时钟上升沿驱动。

原因​:

  • 抗干扰性​:上升沿通常避开信号建立/保持时间(Setup/Hold Time)的敏感区,减少亚稳态风险。
  • 时序一致性​:所有寄存器在同一上升沿同步更新,避免组合逻辑竞争冒险

SysTick的硬件实现

  • ARM Cortex-M内核的递减计数器,其工作流程为:
    • 时钟上升沿到来​ → 检测到边沿 → 计数器值减1(VAL寄存器更新)。
    • VAL从1→0时,在下一个上升沿置位COUNTFLAG标志并重载LOAD

​![[Pasted image 20250709101910.png]]
由于自动重装载值理解瑕疵,查阅资料和检索视频以及利用大模型并未能完全解惑,因此在这一块,我们就先死记硬背实际产生的周期是重装载值加1。

寄存器结构​:
通过 CTRL(控制状态)、LOAD(重载值)、VAL(当前值)、CALIB(校准)四个寄存器管理

小结:

SysTick的计数行为严格依赖配置的时钟源边沿

VAL=0时,在下一个时钟上升沿自动完成三件事:

  • 重载LOAD值到VAL
  • 置位COUNTFLAG标志;
  • TICKINT=1,触发中断;
因素 上升沿触发 下降沿触发
稳定性 高(避开亚稳态窗口) 中(易受噪声干扰)
同步性 所有寄存器统一更新 可能导致时序偏移
功耗 常规设计无显著差异 低功耗场景可能优化唤醒时机
典型应用 SysTick、CPU指令执行、寄存器更新 DDR内存、异步信号同步、特定通信协议

1.2 关于滴答定时器代码理解

1.2.1 系统时钟获取函数(内核时钟)

SystemCoreClockUpdate();

SystemCoreClockUpdate() 是 STM32 微控制器 HAL 库中的核心函数,主要用于动态更新系统核心时钟频率​(存储于全局变量 SystemCoreClock 中)。
刷新 SystemCoreClock 变量​:该函数通过读取 RCC(复位和时钟控制)寄存器的当前配置(如时钟源选择、PLL 倍频/分频系数、预分频器等),实时计算当前 CPU 核心时钟频率(SYSCLK),并将结果更新至全局变量 SystemCoreClock(单位:Hz)
注意单位是:HZ

确保时钟数据准确性​:当系统时钟配置发生变动(如切换 HSI/HSE/PLL 时钟源、修改分频系数或进入低功耗模式),必须调用此函数以同步最新频率值,避免依赖时钟的模块(如延时、串口波特率)因数据滞后而出错;

关于低功耗模式唤醒,一定需要重新调用,更新最新的系统时钟。

应用场景:

  • 初始化阶段​:在系统时钟初始化(如 SystemInit())后调用一次,确保 SystemCoreClock 初始值正确

  • 动态时钟配置​:若程序运行时修改时钟树(如切换时钟源、调整 PLL 参数),需立即调用以刷新频率值

  • 非自动调用​:HAL 库不会自动调用此函数,开发者需在时钟变更后显式调用

  • 实时性要求​:在低功耗模式切换(如唤醒后恢复主时钟)后必须调用,确保外设时钟配置正确

SystemCoreClockUpdate() 是 STM32 开发中的时钟同步枢纽,它通过实时解析 RCC 寄存器,确保 SystemCoreClock 变量始终反映真实的 CPU 频率。正确调用该函数是保障延时精度、外设时序及系统稳定性的关键步骤。

1.2.2 时钟配置

SysTick_Config(SYSTICK_LOAD);

#if defined (__Vendor_SysTickConfig) && (__Vendor_SysTickConfig == 0U)

__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
  {
    return (1UL);                                                   /* Reload value impossible */
  }

  SysTick->LOAD  = (uint32_t)(ticks - 1UL);                         /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
  SysTick->VAL   = 0UL;                                             /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;                         /* Enable SysTick IRQ and SysTick Timer */
  return (0UL);                                                     /* Function successful */
}

#endif

1UL​ 是一个无符号长整型(unsigned long)的数值常量,表示数值1,但通过后缀 UL 明确指定了其数据类型。

1UL 的核心意义是:​显式定义一个无符号长整型的数值1。它在嵌入式开发、位操作、大数值运算中至关重要,能避免类型歧义、保证计算安全性和跨平台一致性。在涉及硬件操作或宏定义时,务必优先使用明确的后缀(如 ULULL)而非依赖默认类型。

#if defined (__Vendor_SysTickConfig) && (__Vendor_SysTickConfig == 0U)

#endif

检查宏 __Vendor_SysTickConfig是否被定义。若未定义,整个表达式为假(0);若已定义,继续判断后续条件。
判断该宏的值是否等于无符号整型的0(0U)。若等于0,则条件成立;否则不成立。

SysTick_CTRL_CLKSOURCE_Msk
  • 该宏对应 CTRL 寄存器的 ​Bit 2(CLKSOURCE)​,用于选择时钟源。
  • CLKSOURCE = 1 时:选择 ​内核时钟(HCLK)​​ 作为 SysTick 的时钟源。
  • CLKSOURCE = 0 时:选择 ​外部时钟(HCLK/8)​​ 作为时钟源
    在赋值语句中,SysTick_CTRL_CLKSOURCE_Msk 被显式启用(即置为 1),因此 ​默认选择 HCLK 时钟源。

若需使用 ​HCLK/8​ 作为时钟源,需额外调用库函数:

SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);  // 手动切换为 HCLK/8

编译器会启用ARM CMSIS库提供的标准SysTick初始化函数SysTick_Config()。该函数自动配置SysTick的重载值、中断优先级和时钟源。
SysTick_Config() 是 CMSIS 库的标准函数,其设计即默认使用 HCLK 时钟源,以提供最高精度。

自定义行为(条件不成立时)​ 暂时还未用到。后续更新FreeRTOS可能会进一步说明。
若表达式为假(如 __Vendor_SysTickConfig 定义为 1U),开发者需提供自定义的SysTick配置函数,覆盖默认实现。这在以下场景需:

  • 特定硬件优化(如特殊时钟源分频)。
  • 操作系统需接管SysTick(如FreeRTOS)。
  • 需规避CMSIS库的兼容性问题。

并且可以看到

  SysTick->LOAD  = (uint32_t)(ticks - 1UL); 

我们的重载值默认会减一,那么说明在传递ticks的时候就不需要提前减一。如果需要300个时钟周期,我们就直接写入300即可。

![[Pasted image 20250709110703.png]]

这是参考ARM Cortex-M3与Cortex-M4权威指南(第三版) 关于滴答定时器(SysTick)CTRL 寄存器(控制和状态寄存器)的说明。

1.2.3 重装载值确定

#define SYSTICK_LOAD (SystemCoreClock /(unsigned long int)(SYSTICK_CYCLE_1000MS/SYSTICK_CYCLE_TIME))

详细分析如何设计重装载值

SystemCoreClock :系统主频(HCLK),即CPU内核时钟频率(单位:Hz),例如:72,000,000 (72MHz)

SYSTICK_CYCLE_1000MS :表示基准时间周期(1000毫秒)对应的计数值,通常为1000(表示1秒 = 1000ms)

SYSTICK_CYCLE_TIME目标定时周期​(单位:毫秒),即期望SysTick中断的间隔时间,例如1 (1ms) 或 10 (10ms)

(SYSTICK_CYCLE_1000MS / SYSTICK_CYCLE_TIME) :将目标周期转换为相对于1000ms的倍数,表示 ​1秒内需触发的中断次数,例如1000 (若目标为1ms) 或 100 (若目标为10ms)。

在说这个重载值之前还需要进行一些概念性的理解:

SystemCoreClock
  • 这是一个软件变量​(通常由CMSIS定义并提供),代表当前处理器内核(Core)所运行的时钟频率
  • 单位是赫兹
  • 它是SysTick定时器的主要时钟源(虽然在Cortex-M设备上SysTick的时钟源可以通过寄存器选择内部时钟或内核时钟分频后的时钟)。
  • 例如,一个典型的基于ARM Cortex-M内核的STM32微控制器运行在72MHz时,SystemCoreClock = 72,000,000 Hz

时钟周期 (T_时钟):​

  • 定义:这是处理器(核心)运行的一个最基本的、不可分割的时间单位

(这个地方很重要,这是理解ARM时间的基石,我们的时钟周期就是一个时间基石,如果这里不准,那么整个系统我们需要的时间都是不准的(不借助外部),因此这个地方也是衡量一个单片机性能优劣的一个最直观的标准。).

  • 物理意义:它是SystemCoreClock频率所对应的周期时间。
  • 计算公式: T_时钟 = 1 / SystemCoreClock
  • 单位:​
  • 例如:如果SystemCoreClock = 72 MHz = 72,000,000 Hz,那么 T_时钟 = 1 / 72,000,000 s ≈ 13.89 纳秒

频率与时间的关系:​

  • 频率 (f):​​ 表示每秒发生的事件数量(这里是时钟周期的数量)。
  • 周期 (T):​​ 表示一个完整波形(或一个时钟事件)所持续的时间。T = 1 / f
  • ​“经过72MHz次时钟周期,实际表示1S时间”:​​ 这正是对频率定义的应用。如果处理器时钟以72MHz运行:
    • 每秒(1s)内发生的时钟周期数 = 72,000,000
    • 要测量1秒的时间,程序只需要计算经过了72,000,000个时钟周期即可。
    • 同样地,要测量一个精确的时间间隔(如t秒),程序需要计算t * SystemCoreClock个时钟周期。
  • SystemCoreClock核心的运行频率
  • T_时钟 = 1 / SystemCoreClock核心的基础节拍​(一个时钟周期的时间)。
  • 计数时钟周期是测量时间的基础方式。SysTick定时器本质上就是这样一个硬件计数器(通常为24位或32位递减计数器)。通过正确配置SysTick的加载值(LOAD),让它每递减N个时钟周期就产生一次中断或置位一个标志位,就可以精确地获得所需的时间基准(比如每毫秒中断一次)。

基于以上可以认为一个时钟周期的时间是 T_时钟 = 1 / SystemCoreClock,如果我想到1ms的中断,那么就是使用 (1ms转换为s)/T_时钟 得到的就是重装载值,这是最基本的理解,

![[Pasted image 20250709143608.png]]

但是不太容易封装,因为我们通过变量 SystemCoreClock 获取的是频率,那么公式就可以暂时的转换为 (1ms转换为s)* SystemCoreClock,需要注意的是我们的单位是在秒计算的。此外我们产生中断的时间需求也是不一样的,因此还是不能更好的封装。
但是上述的计算重装载的底层思路就是如此。

根据上述思路我们重新整理,重新封装。

1、首先我们需要把单个的时钟周期给表示出来,使用我们能获取到的变量,

T_时钟 = 1 / SystemCoreClock 计算重装载值的核心参数之一。

2、我们需要封装的是一个最基本的中断时基,例如我需要最小中断是125us、250us、1ms

根据上述我们还需要明确一个中断的时间周期,因为只有有了中断时间周期,我们使用中断时间周期除以我们的时钟周期就会得到需要时钟周期的个数,为什么要这样?这是因为中断时间周期其实就是我们需要的中断时间,也就是说我需要1ms或者125us时间产生一个中断,但是这个1ms怎么得到或者说以什么为基准才能知道现在到了1ms或者125us,那么基石就是时钟周期,换句话说就是我们的主频频率,因为主频频率会决定我们时钟周期也就是下面这个公式:

T_时钟 = 1 / SystemCoreClock

3、接下来就是封装时基

中断时间周期来源于对应中断时间频率,
而中断时间频率也就是1S产生的中断次数,
我们明确的是中断时间周期,但是我们需要转换成中断时间频率,

为什么需要引入中断频率这个概念?

首先我们需要明白的是我们需要的中断时间是不一样的,可能是us、可能是ms,从这里就可以看出时间是不统一的,并且如果想转换为s,就需要引入小数,这在单片机中无疑会增加大量的计算。
另一方面我们的获取到SystemCoreClock 单位是HZ,那么对应的时间单位也是S,从计算角度我们也需要使用S相关的变量或者是见解变量,这样在计算中才能保证数据的准确性。

基于第一个方面我们有理由引入中断频率这个概念,也就是1S时间内我们的中断次数,这样就可以把我们的中断时间us、ms嵌套到中断频率里面,或者说是使用中断频率来表示我们想要的中断时间(这样表达其实是不准确的,这是因为我们预先是不知道中断频率的,只有确定了中断时间,才能进一步确定中断频率。)

那么公式如下:​
中断频率​(单位:Hz)

![[Pasted image 20250709151608.png]]

假设我们现在需求的中断时间是125us,那么我们就能计算出频率是多少,

但是我们在对外暴漏的时候需要把这个中断时间给暴露出来,这样就能达到封装的目的。(在本例中我们只需要使用滴答定时器产生一个最小的中断时间本例使用的是125us,当然在实际使用中可能是1ms。),并且在封装的过程中我们要把时间统一,因为在单片机中最小的时间一般使用的都是us,那么我们就以us为单位计算频率,这是没有影响的。

#define SYSTICK_CYCLE_1000MS 				((unsigned long int) 1000000)
#define SYSTICK_CYCLE_100MS 				((unsigned long int) 100000)
#define SYSTICK_CYCLE_10MS 					((unsigned long int) 10000)
#define SYSTICK_CYCLE_5MS 					((unsigned long int) 5000)
#define SYSTICK_CYCLE_1MS 					((unsigned long int) 1000)
#define SYSTICK_CYCLE_500US 				((unsigned long int) 500)
#define SYSTICK_CYCLE_250US 				((unsigned long int) 250)
#define SYSTUCK_CYCLE_125US 				((unsigned long int) 125)
#define SYSTICK_CYCLE_50US 					((unsigned long int) 50)
#define SYSTICK_CYCLE_10US 					((unsigned long int) 10)
#define SYSTICK_CYCLE_1US 					((unsigned long int) 1)

#define SYSTICK_CYCLE_TIME                  SYSTUCK_CYCLE_125US 

(unsigned long int)(SYSTICK_CYCLE_1000MS/SYSTICK_CYCLE_TIME)
这个公式就是使用的我上述描述的思路。

这样就能满足我们对外暴露出需要的最小中断时间函数以后,就能方便进一步计算出LOAD(重装载值)了。

将上述公式继续变换:

![[Pasted image 20250709153409.png]]

这么变换,看起来是多此一举,但是在封装过程是必不可缺少的。结合下面这个公式:

![[Pasted image 20250709153349.png]]

正如上面所说这是从LOAD的数学意义出发,明明在上面我已经得到了中断最小时间,为什么还需要使用 1/中断频率 这是因为单位不统一,也就是上述我分析的,单位不统一导致的以及直接使用us、ms计算会给单片机带来巨大运算量。因为这一步的核心目的就是将时间转换为HZ,从而使两端的单位是匹配的,最后得出计算重装载值的方法。

4、直接接公式的角度去理解

#define SYSTICK_LOAD (SystemCoreClock /(unsigned long int)(SYSTICK_CYCLE_1000MS/SYSTICK_CYCLE_TIME))

首先我们知道通过时钟树的操作,我们的滴答定时器可以完全利用主频时钟,也就是说对于滴答定时器是可控的,也就是说原本的时钟是1s,包含72M个时钟时钟周期,那如果是计数最大值,能表示多少时间?
SysTick从最大值递减至0需经历16,777,215个时钟周期。在72MHz频率下,每个周期耗时 172,000,000\frac{1}{72,000,000}72,000,0001​ 秒,总耗时约为233ms

那我们站在另外一个角度,我们原本一秒的具有72M个时钟周期,我们一秒的时间又需要100个10ms中断,也就是说72M个时钟周期要平均分配在100个10ms中断里面,那么只要使用主频除以中断频率,就能得到重装载值。 写了这么多,我觉得这才是最核心最精简的理解。

本质是 ​将总时钟周期按中断次数均分

此外我们还需要明白就是,时钟频率是基石,是一切的基石,在之前我是把时钟频率和定时器是割裂的,导致没有第一时间想到: ​将总时钟周期按中断次数均分

可以这么说,在某个定时器或者计数器在某个时钟频率下工作,你就可以理解为这个在此时此刻这个时钟频率就是完全为这个定时器或者计数器工作,至于其他地方也会使用这个时钟频率,那就是时钟树操心的了,不管我们的事,我们只要这个此时此刻记住这个时钟频率就干我们这一件事,我们就能明白 ” 将总时钟周期按中断次数均分“ 这句话的含义,以及最后一段话的含义。

1.3 定时器1

任何一个芯片手册都会讲的很清楚,这里就不班门弄斧了。

不同的芯片直接参考不同的芯片手册,我觉得更好。


文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。

【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。

感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言。


网站公告

今日签到

点亮在社区的每一天
去签到