STM32HAL 快速入门(三十六):SysTick 实现高精度计时
本文介绍了基于STM32 SysTick定时器实现高精度计时的方法。SysTick是ARM内核集成的24位向下计数器,默认配置为1ms中断。通过读取VAL寄存器实时值,结合溢出处理算法,可精确计算微秒级延迟。文章详细解析了SysTick的寄存器配置逻辑,并提供了三个核心函数实现:微秒延时(udelay)、毫秒延时(mdelay)和纳秒级系统时间获取(system_get_ns)。这些方法解决了HA
一、前言
大家好,这里是 Hello_Embed。在嵌入式开发中,高精度计时是诸多场景的核心需求 —— 比如驱动 DHT11 温湿度传感器、DS18B20 温度传感器时,需要精确到微秒(μs) 的时序控制;而 HAL 库自带的 HAL_Delay() 函数最小单位是 1 毫秒(ms),精度过于粗糙,无法满足这类需求。
本文将聚焦 SysTick 系统定时器(ARM 内核自带),通过底层逻辑解析与代码实现,完成 “微秒级延时” 和 “纳秒级系统上电时间获取”,解决高精度计时难题。
二、SysTick 概述:内核自带的 “通用定时器”
在编写代码前,需先明确 SysTick 的核心特性与设计意义,这是理解后续逻辑的基础。
2.1 SysTick 基本属性
- 本质:集成在 NVIC(嵌套向量中断控制器)中的 24 位向下计数器;
- 计数规则:每接收 1 个时钟脉冲,计数值(
SysTick->VAL寄存器)自减 1;当计数值减至 0 时,触发 SysTick 中断,同时自动将SysTick->LOAD寄存器的预设值载入VAL,重新开始计数; - 时钟来源:默认使用系统时钟(如 72MHz),也可配置为 “系统时钟 / 8”(本文用系统时钟以保证最高精度)。
2.2 为什么需要 SysTick?
不同厂家的 STM32 外部定时器(如 TIM2、TIM3)型号、寄存器定义可能存在差异,导致基于外部定时器的代码移植困难。而 SysTick 是 ARM 内核统一设计的定时器,无论外部定时器如何变化,其操作逻辑(寄存器、中断机制)完全一致 —— 这也是它被称为 “系统定时器” 的核心原因:保证代码跨芯片兼容。
三、CubeMX 配置与 SysTick 默认时序
SysTick 的配置极其简单,CubeMX 会自动完成初始化,我们只需理解其默认时序即可。
3.1 CubeMX 配置步骤
- 打开 CubeMX 工程,进入 SYS 配置界面;
- “Timebase Source”(时间基准源)默认选择 SysTick(无需手动修改),截图如下:

3.2 默认时序:1kHz 中断(1ms 一次)
生成工程后,跟踪代码初始化流程:HAL_Init() → HAL_InitTick() → 配置 uwTickFreq,关键代码如下:
// HAL_InitTick() 中定义的 SysTick 中断频率
HAL_TickFreqTypeDef uwTickFreq = HAL_TICK_FREQ_DEFAULT; /* 1KHz */
HAL_TICK_FREQ_DEFAULT对应 1kHz,即 SysTick 每 1 秒产生 1000 次中断,每次中断间隔 1ms;- 这个频率是 HAL 库的默认时间基准:
HAL_Delay()函数就是通过 “每 1ms 让uwTick变量加 1,延时 Nms 即等待uwTick增加 N” 实现的。
四、SysTick 底层初始化解析:寄存器配置逻辑
要实现高精度计时,必须理解 SysTick 的底层初始化 —— 核心是 4 个关键寄存器的配置,代码路径为:HAL_Init() → HAL_InitTick() → HAL_SYSTICK_Config() → SysTick_Config(),关键代码如下:
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* 1. 配置重装寄存器(LOAD) */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* 2. 配置中断优先级 */
SysTick->VAL = 0UL; /* 3. 清零当前计数值寄存器(VAL) */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | /* 4. 配置控制寄存器(CTRL) */
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0UL);
4.1 各寄存器功能详解
- 重装寄存器(SysTick->LOAD)
- 作用:存储 “自动重装值”—— 当
VAL减至 0 时,会自动将LOAD的值载入VAL,确保计数循环; - 示例:系统时钟 72MHz 下,要实现 1ms 中断,需 72000 个时钟脉冲(72MHz × 1ms = 72000),因此
LOAD = 72000 - 1 = 71999(计数从 71999 减至 0,共 72000 次)。
- 作用:存储 “自动重装值”—— 当
- 中断优先级配置(NVIC_SetPriority)
- 作用:设置 SysTick 中断的优先级,避免被低优先级中断阻塞,确保计时精度;
- 此处配置为最低优先级(
(1UL << __NVIC_PRIO_BITS) - 1UL),不影响核心业务逻辑。
- 当前计数值寄存器(SysTick->VAL)
- 作用:存储当前计数进度,初始化时清零,保证计数从预设的
LOAD值开始,避免初始值混乱。
- 作用:存储当前计数进度,初始化时清零,保证计数从预设的
- 控制寄存器(SysTick->CTRL)
- 作用:控制 SysTick 的使能与工作模式,各 bit 含义:
SysTick_CTRL_CLKSOURCE_Msk:选择系统时钟作为计数时钟;SysTick_CTRL_TICKINT_Msk:使能 SysTick 中断(计数值到 0 时触发);SysTick_CTRL_ENABLE_Msk:使能 SysTick 定时器,开始计数。
- 作用:控制 SysTick 的使能与工作模式,各 bit 含义:
五、高精度计时原理:从 1ms 到 1μs、1ns
SysTick 默认是 1ms 中断,但我们可通过 “实时读取 VAL 寄存器值”,计算流逝的时钟数,进而实现更细粒度的计时 —— 核心是解决 “计数溢出” 问题。
5.1 核心计算公式
基于 SysTick 默认配置(系统时钟 72MHz,LOAD = 71999):
- 1 个时钟周期 = 1/72 μs ≈ 0.0139 μs;
LOAD + 1 = 72000个时钟周期 = 1ms(72000 × 1/72 μs = 1000 μs);- 推导:
- 1 μs 对应的时钟数 = (LOAD + 1) / 1000 = 72;
- n μs 对应的时钟数 = n × 72;
- 纳秒级时间 = 毫秒级时间(
HAL_GetTick())× 1e6 + 当前剩余时钟对应的纳秒。
5.2 关键问题:计数溢出处理
SysTick 是 “向下计数”,当 VAL 从 0 重新载入 LOAD 时,会产生 “溢出”—— 此时读取的 VAL 新值会大于旧值(例如旧值 100,新值 71999),需分两种情况计算流逝的时钟数:
- 无溢出(旧值 ≥ 新值):流逝时钟数 = 旧值 - 新值;
- 有溢出(旧值 < 新值):流逝时钟数 = 旧值 + (LOAD + 1) - 新值(旧值到 0 的时钟数 + 0 到新值的时钟数)。
六、核心函数实现:微秒 / 毫秒延时 + 纳秒级时间获取
新建 driver_timer.c 和 driver_timer.h(文件添加到工程的步骤省略),实现三个核心函数,满足不同精度需求。
6.1 微秒延时函数(udelay)
功能:实现 n 微秒的精确延时,通过循环等待 “累计流逝时钟数 ≥ 目标时钟数”。
#include "stm32f1xx_hal.h"
/**
* @brief 微秒延时函数
* @param us:要延时的微秒数(范围:1~任意,需确保不溢出)
* @retval 无
*/
void udelay(int us)
{
uint32_t told = SysTick->VAL; // 记录延时开始时的 VAL 值(旧值)
uint32_t tnow; // 记录实时 VAL 值(新值)
uint32_t load = SysTick->LOAD; // 获取 LOAD 寄存器值
uint32_t ticks; // 目标:n μs 对应的时钟周期数
uint32_t cnt = 0; // 累计流逝的时钟周期数
// 计算 n μs 对应的时钟数:(LOAD+1) 个时钟 = 1ms → 1μs = (LOAD+1)/1000 个时钟
ticks = us * (load + 1) / 1000;
while (1)
{
tnow = SysTick->VAL; // 读取实时 VAL 值
if (told >= tnow) // 无溢出:旧值 ≥ 新值
{
cnt += told - tnow; // 累加流逝的时钟数
}
else // 有溢出:旧值 < 新值(VAL 从 0 重装了 LOAD)
{
cnt += told + (load + 1) - tnow; // 累加溢出后的总时钟数
}
if (cnt >= ticks) // 累计时钟数达到目标,退出循环
break;
told = tnow; // 更新旧值,准备下一次计算
}
}
6.2 毫秒延时函数(mdelay)
功能:基于微秒延时函数实现,1 毫秒 = 1000 微秒,循环调用 udelay(1000) 即可。
/**
* @brief 毫秒延时函数
* @param ms:要延时的毫秒数(范围:1~任意)
* @retval 无
*/
void mdelay(int ms)
{
for (int i = 0; i < ms; i++) // 循环 ms 次,每次延时 1000 μs(1ms)
udelay(1000);
}
6.3 纳秒级系统上电时间获取(system_get_ns)
功能:获取从系统上电到当前时刻的总时间,精度到纳秒(ns),核心是 “毫秒级时间 + 剩余时钟对应的纳秒”。
/**
* @brief 获取系统上电到当前的总时间(单位:ns)
* @param 无
* @retval 系统上电总时间(纳秒)
* @note 存在小bug:未处理 tnow=0 的极端情况,日常使用影响极小
*/
uint64_t system_get_ns(void)
{
uint64_t ns = HAL_GetTick(); // 1. 获取毫秒级时间(uwTick 的值,1ms 加 1)
ns = ns * 1000000; // 转换为微秒,后续再转纳秒
uint32_t tnow = SysTick->VAL; // 2. 获取当前 VAL 值(当前计数进度)
uint32_t load = SysTick->LOAD; // 获取 LOAD 值
// 计算当前毫秒内已流逝的时钟数:(LOAD+1) - tnow(从 LOAD 减到 tnow 的总次数)
uint64_t cnt = (load + 1) - tnow;
// 计算当前毫秒内已流逝的纳秒数:cnt 个时钟 × 每个时钟的纳秒数(1e9 / (load+1))
ns += cnt * 1000000000 / (load + 1);
return ns; // 返回总纳秒数
}
七、测试验证:LED 闪烁 + OLED 显示上电时间
在 main.c 中调用上述函数,验证高精度计时效果 —— 实现 “LED 每 500ms 闪烁” 和 “OLED 显示系统上电时间(纳秒级)”。
7.1 测试代码
#include "driver_timer.h"
#include "driver_oled.h"
#include "stm32f1xx_hal.h"
int main(void)
{
// 1. 初始化外设
HAL_Init(); // HAL 库初始化(含 SysTick 初始化)
MX_GPIO_Init(); // GPIO 初始化(配置 LED 引脚 PC13)
OLED_Init(); // OLED 初始化(用于显示时间)
OLED_Clear(); // OLED 清屏
// 2. 获取系统上电时间并显示
uint64_t power_on_time = system_get_ns();
OLED_PrintString(0, 0, "Power-on Time(ns):"); // 显示标题
OLED_PrintSignedVal(0, 2, power_on_time); // 打印纳秒级时间
// 3. LED 循环闪烁(500ms 亮/500ms 灭)
while (1)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); // LED 亮(PC13 低电平有效)
mdelay(500); // 延时 500ms
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); // LED 灭(PC13 高电平)
mdelay(500); // 延时 500ms
}
}
7.2 测试现象
- LED 状态:每 500ms 稳定闪烁一次,延时精度无明显偏差;
- OLED 显示:清晰显示系统上电时间,例如
805500000ns(即 805.5ms),与实际时间相差不大,截图如下:
八、结尾
本文通过 SysTick 系统定时器,成功实现了 “微秒级延时” 和 “纳秒级时间获取”,解决了 HAL 库自带延时精度不足的问题。SysTick 的核心优势是 无需依赖外部定时器、代码跨芯片兼容,适合作为通用高精度计时方案。
下一篇,我们将进一步学习 “外部通用定时器” 的高精度延时实现 —— 外部定时器支持更多时钟来源和计数模式,适用场景更灵活。
Hello_Embed 继续带你从内核定时器到外部定时器,逐步掌握 STM32 高精度计时的全场景应用,敬请期待~
更多推荐



所有评论(0)