目录
[SysTick 系统定时器完整流程](#SysTick 系统定时器完整流程)
[1. 硬件层 --- Cortex-M4 内核内置 SysTick](#1. 硬件层 — Cortex-M4 内核内置 SysTick)
[24 位寄存器限制](#24 位寄存器限制)
[2. 配置层 --- 决定多久中断一次](#2. 配置层 — 决定多久中断一次)
[3. 中断服务层 --- 到点后干什么](#3. 中断服务层 — 到点后干什么)
[HAL_IncTick() --- stm32f4xx_hal.c](#HAL_IncTick() — stm32f4xx_hal.c)
[TimingDelay_Decrement() --- bsp_SysTick.c](#TimingDelay_Decrement() — bsp_SysTick.c)
[4. 应用层 --- 两个延时函数的实现](#4. 应用层 — 两个延时函数的实现)
[HAL_Delay(1ms 精度)--- stm32f4xx_hal.c](#HAL_Delay(1ms 精度)— stm32f4xx_hal.c)
[Delay_us / Delay_ms(10us 精度)--- bsp_SysTick.c](#Delay_us / Delay_ms(10us 精度)— bsp_SysTick.c)
[5. 两种延时的对比](#5. 两种延时的对比)
[6. 时间单位换算](#6. 时间单位换算)
[7. 本质一句话](#7. 本质一句话)
[8. 纯 HAL 路线 --- myproject 的实际配置](#8. 纯 HAL 路线 — myproject 的实际配置)
[调用链(三级跳水,从 main 到寄存器)](#调用链(三级跳水,从 main 到寄存器))
[最终落寄存器的代码 --- core_cm4.h](#最终落寄存器的代码 — core_cm4.h)
[纯净 HAL 的中断服务 --- stm32f4xx_it.c](#纯净 HAL 的中断服务 — stm32f4xx_it.c)
[谁在调用 HAL_InitTick](#谁在调用 HAL_InitTick)
[9. 总结:SysTick 全流程一张图](#9. 总结:SysTick 全流程一张图)
[ISR 层(软件分频)](#ISR 层(软件分频))
[应用层:减法 vs 加法](#应用层:减法 vs 加法)
SysTick 系统定时器完整流程
1. 硬件层 --- Cortex-M4 内核内置 SysTick
| 寄存器 | 作用 |
|---|---|
| STK_LOAD | 重装载值 |
| STK_VAL | 当前计数值(递减) |
| STK_CTRL | 控制寄存器(开关、时钟源、中断使能) |
24 位寄存器限制
SysTick 的 LOAD 和 VAL 寄存器是 24 位的,最大装载值:
STK_LOAD_max = 2²⁴ - 1 = 0xFFFFFF = 16,777,215
这对中断周期有两个硬约束:
最长中断周期(LOAD 取最大):
T_max = (16,777,215 + 1) ÷ 180,000,000 ≈ 93.2ms
LOAD=16,777,215 → 一次中断要等约 93ms,再大装不下了
→ SysTick 单次定时无法超过 ~93ms(F429@180MHz 下)。更长的延时必须靠中断里软件重新装填 或多次中断累积计数 ------这就是 HAL 用 uwTick++ 累积、野火用 TimingDelay-- 倒数的原因。
最短中断周期(LOAD 取最小 1):
T_min = (1 + 1) ÷ 180,000,000 ≈ 11ns
LOAD=1 → 每 2 个时钟周期中断一次,理论上可达 90MHz 中断频率,实际不可能(CPU 当场卡死)
→ 实际工程中 LOAD 至少几十到几百,否则中断开销会吞掉全部 CPU 时间。
LOAD 值与最大单次延时的对应(180MHz):
| LOAD | 周期 | 最大单次延时 |
|---|---|---|
| 180 | 1μs | 16.7s(用 TimingDelay 倒到 0) |
| 1,800 | 10μs | 167s |
| 180,000 | 1ms | 16,777s(≈ 4.6 小时) |
| 16,777,215 | 93.2ms | 约 26 分钟(HAL_Delay 最大值 0xFFFFFFFFms ≈ 49 天,跨过 49 天后 uwTick 翻转,有专门处理) |
总结:24 位决定了硬件单次最长约 93ms,但软件通过 硬件计数器 + 中断累加 的方式把延时扩展到了任意时长。硬件提供节拍,软件负责数拍子。
每个 CPU 时钟周期,硬件自动做的事:
每个时钟周期:
STK_VAL = STK_VAL - 1 ← 硬件自动减 1
当 STK_VAL 减到 0 时:
├─ 硬件自动:STK_VAL = STK_LOAD ← 重装载
├─ 硬件自动:COUNTFLAG = 1 ← 置标志位
└─ 硬件自动:跳转 SysTick_Handler() ← 触发中断
硅片电路全自动循环------不减慢 CPU、不占执行指令。上电默认关闭(STK_CTRL=0),必须软件使能后才跑。硬件没有"默认中断周期",LOAD 值完全由你决定。
2. 配置层 --- 决定多久中断一次
HAL_SYSTICK_Config(SystemCoreClock / N);
// LOAD = SystemCoreClock / N
//
// N = 1000 → LOAD = 180000 → 1ms 中断(HAL 默认)
// N = 100000 → LOAD = 1800 → 10us 中断(野火 BSP)
// N = 1000000 → LOAD = 180 → 1us 中断
中断周期 = (LOAD + 1) ÷ 系统时钟频率。
LOAD 越小 → 中断越频繁 → 精度越高(但中断开销也越大)。
HAL 用 /1000(1ms)是 ST 工程师写死的一个约定------方便编程、外设超时用毫秒计算、1ms 是"够细又不过于频繁"的权衡点。
3. 中断服务层 --- 到点后干什么
stm32f4xx_it.c 里的 SysTick_Handler():
void SysTick_Handler(void)
{
HAL_IncTick(); // uwTick += 1(HAL 的 1ms 计数器)
TimingDelay_Decrement(); // TimingDelay--(野火 BSP 的 10us 计数器)
}
中断在后台静默执行,修改全局变量,主程序完全无感知。
HAL_IncTick() --- stm32f4xx_hal.c
__IO uint32_t uwTick; // 全局滴答计数器
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT; // 默认 1(= 1ms/次)
__weak void HAL_IncTick(void)
{
uwTick += uwTickFreq; // 每次中断 uwTick 加 1ms
}
TimingDelay_Decrement() --- bsp_SysTick.c
static __IO u32 TimingDelay; // 野火 BSP 的私有计数器
void TimingDelay_Decrement(void)
{
if (TimingDelay != 0x00)
TimingDelay--; // 每次中断减 1(= 10us)
}
4. 应用层 --- 两个延时函数的实现
HAL_Delay(1ms 精度)--- stm32f4xx_hal.c
__weak void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick(); // 记下起点 uwTick
uint32_t wait = Delay + uwTickFreq; // 目标差值(+1 保证不小于)
while ((HAL_GetTick() - tickstart) < wait) { } // 死等 uwTick 涨到位
}
模式:递增计数器 + 差值比较。 uwTick 不断涨,主循环算"涨了多少",够数了就退出。
Delay_us / Delay_ms(10us 精度)--- bsp_SysTick.c
void Delay_us(__IO u32 nTime) // nTime = 10us 的倍数
{
TimingDelay = nTime; // 设初值
while (TimingDelay != 0); // 死等减到 0
}
#define Delay_ms(x) Delay_us(100*x) // 1ms = 100 × 10us
模式:递减计数器 + 归零判断。 TimingDelay 每次中断减 1,主循环等到 0 就退出。
5. 两种延时的对比
| HAL_Delay | 野火 Delay_us | |
|---|---|---|
| 计数器 | uwTick(递增) |
TimingDelay(递减) |
| 分辨率 | 1ms | 10μs |
| 比较方式 | (当前值 - 起点) >= 目标 |
值 == 0 |
__weak |
✅ 可覆盖 | ❌ 固定 |
| 额外用途 | HAL 内部超时判断 | 用户精确时序 |
6. 时间单位换算
1s = 1000ms (1秒 = 一千毫秒)
1ms = 1000us (1毫秒 = 一千微秒)
1s = 1,000,000us (1秒 = 一百万微秒)
s → ms → us 都是 1000 进制,千倍关系。
7. 本质一句话
硬件到点敲门 → 中断函数改个变量 → 主循环等变量到位 → 延时结束。
硬件只管"到没到",变量改多少、怎么改、主循环怎么等------全部是几行 C 代码决定的。
8. 纯 HAL 路线 --- myproject 的实际配置
以 hal_first_project 工程为例,不走野火 BSP,纯 HAL 体系。
调用链(三级跳水,从 main 到寄存器)
main.c: HAL_Init()
└─ stm32f4xx_hal.c:176 HAL_InitTick(TICK_INT_PRIORITY)
└─ stm32f4xx_hal.c:256 HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq))
└─ stm32f4xx_hal_cortex.c:227-230 return SysTick_Config(TicksNumb);
└─ core_cm4.h:2022-2036 __STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
↓ 最终写寄存器
最终落寄存器的代码 --- core_cm4.h
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks) // ticks = 180000
{
// ① 检查 24 位上限
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk) // SysTick_LOAD_RELOAD_Msk = 0xFFFFFF
return (1UL);
// ② 写三个寄存器
SysTick->LOAD = (uint32_t)(ticks - 1UL); // STK_LOAD = 179999
NVIC_SetPriority(SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); // 优先级 = 最低(15)
SysTick->VAL = 0UL; // STK_VAL = 0(清零)
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | // 时钟源 = HCLK(180MHz)
SysTick_CTRL_TICKINT_Msk | // 使能中断
SysTick_CTRL_ENABLE_Msk; // 启动定时器
return (0UL);
}
三个寄存器位的来源:
| 宏 | 值 | 含义 |
|---|---|---|
SysTick_CTRL_CLKSOURCE_Msk |
(1 << 2) | 时钟源 = 系统时钟 / 8 |
SysTick_CTRL_TICKINT_Msk |
(1 << 1) | 减到 0 时产生中断 |
SysTick_CTRL_ENABLE_Msk |
(1 << 0) | 启动计数器 |
实际配置数值(180MHz)
| 项目 | 值 | 来源 |
|---|---|---|
| SystemCoreClock | 180MHz=(180000000)hz | SystemClock_Config() 配置 |
| uwTickFreq | 1(HAL_TICK_FREQ_1KHZ) |
stm32f4xx_hal.c:112 |
| Reload 值 | 180,000 - 1 = 179,999 | 180M / (1000/1) - 1 |
| 中断周期 | 1ms溢出一次 | (179999 + 1) / 180M |
| 中断优先级 | 15(最低) | (1 << 4) - 1 |
纯净 HAL 的中断服务 --- stm32f4xx_it.c
void SysTick_Handler(void)
{
HAL_IncTick(); // uwTick += 1,不包含野火的 TimingDelay_Decrement()
}
这个工程不用野火 BSP,所以中断里只有 HAL 的 uwTick 累加,没有 TimingDelay 那套 10us 的减法逻辑。
谁在调用 HAL_InitTick
不止 HAL_Init() 会调,时钟频率改变时也会重新调:
| 调用位置 | 时机 |
|---|---|
HAL_Init() --- stm32f4xx_hal.c:176 |
上电初始化 |
HAL_RCC_ClockConfig() --- stm32f4xx_hal_rcc.c:722 |
系统时钟变更 |
HAL_RCCEx_PeriphCLKConfig() --- stm32f4xx_hal_rcc_ex.c:3305 |
外设时钟变更 |
每次时钟变了,LOAD 值都要重新算------频率越高,SystemCoreClock / 1000 越大,中断还是 1ms;频率越低,LOAD 自动变小,保持 1ms 不变。这就是用公式而不是写死数字的好处。
9. 总结:SysTick 全流程一张图
硬件层
Cortex-M4 内核内置 SysTick(24 位递减计数器)
CTRL ENABLE 位写 1 瞬间:
VAL = LOAD(硬件自动装填)
每个 HCLK 周期:VAL = VAL - 1
减到 0:→ 触发中断 + 硬件自动重装 VAL = LOAD + 循环往复
上电默认关闭,没有默认周期——LOAD 值完全由你决定
配置层
SysTick_Config(180000) → LOAD=179999 → 1ms 中断(HAL 默认)
SysTick_Config(1800) → LOAD=1799 → 10us 中断(野火 BSP)
公式:中断周期 = (LOAD + 1) / 系统时钟频率
时序:软件写 0 进 VAL → 硬件瞬间自动装填 → 从 LOAD 开始往下减
ISR 层(软件分频)
SysTick 每 10us 中断一次 →
├─ TimingDelay-- (每次,给 Delay_us 用)
└─ HAL_TickCounter++
if (>= 100) HAL_IncTick()(每 100 次 = 1ms,给 HAL_Delay 用)
应用层:减法 vs 加法
| 减法模式 | 加法模式 | |
|---|---|---|
| 函数 | Delay_us(n) / Delay_ms(n) |
HAL_Delay(n) |
| 变量 | TimingDelay(私有) |
uwTick(全局共享) |
| 模式 | 设初值 → 死等归零 | 记起点 → 死等差值到位 |
| 精度 | 10us | 1ms |
| 适合 | 一次性精确延时 | 全局时基,多消费者共用 |
同一个硬件,两个延时体系,纯软件分道,互不影响。
核心理解
硬件只是节拍器------到点了通知你(中断),怎么用是你的事
24 位寄存器单次最长 ~93ms,长延时靠"硬件发节拍 + 软件计数"
一个 10us 硬件中断,喂出两套延时体系:10us 减法 + 1ms 加法
主循环做的事永远是:设个初值 → 死等 ISR 把条件改到成立 → 走人