全部例程

SysTick - 系统定时器

W55MH32

更新于 2026年8月13日

本篇总结

使用 SysTick,实现 us/ms 级延时。

🔑 KEIL MDK、UART调试、SysTick

本示例演示两种使用 SysTick 实现精准延时的方式:中断计数法(每次计数到 0 触发中断,在中断中递减计数变量)与 轮询 COUNTFLAG 法(查询 CTRL 寄存器 bit16 标志位)。

SysTick 简介

SysTick 是一个 24bit 的向下递减计数器,计数器每计数一次的时间为 1/SYSCLK(一般 SYSCLK=72MHz)。当重装载数值寄存器的值递减到 0 时,系统定时器产生一次中断,以此循环往复。

SysTick 共有 4 个寄存器,实际使用时只需配置前三个:

寄存器描述关键位
CTRL控制及状态寄存器bit16 COUNTFLAG / bit2 CLKSOURCE / bit1 TICKINT / bit0 ENABLE
LOAD重装载数值寄存器bit23:0 RELOAD(递减到 0 时重装载)
VAL当前数值寄存器bit23:0 CURRENT(读返回当前值,写清零)
CALIB校准数值寄存器本例程未使用

SysTick 属于内核外设,寄存器定义和库函数都在 core_cm3.h 中。标准库提供了 SysTick_Config() 函数封装了 LOAD、VAL、CTRL 三个寄存器的配置,一个函数即可完成初始化。

前置工具准备

开始本例程前,请确认以下工具已安装:

  • WIZ UartTool V1.0:串口日志观察工具:点击下载

硬件连接

SysTick 为内核内置外设,无需外部引脚。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC,用于观察串口打印的延时验证日志。

工程结构

工程位于 SDK 内 1.SDK\ModuleDemo\SysTick\,核心文件如下:

SysTick/
├── USER/
│   ├── main.c            ← SysTick 初始化 + 两种延时实现 + 主流程
│   ├── w55mh32_conf.h    ← 外设头文件集中包含
│   ├── w55mh32_it.c/h    ← 中断服务函数(SysTick_Handler)
│   ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│   └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
    └── delay/
        └── delay.c/h      ← SysTick 延时函数封装

步骤1:SysTick 初始化(中断方式)

文件:USER/main.c | 函数:SysTick_Init() — 调用标准库 SysTick_Config() 配置中断周期为 10us。


/**
 * @brief  启动系统滴答定时器 SysTick(中断方式)
 * @note   中断周期 = SystemCoreClock / 100000 = 72M / 100000 = 720 个计数周期 = 10us
 * @param  无
 * @retval 无
 */
void SysTick_Init(void)
{
    /* SystemFrequency / 1000    1ms 中断一次
     * SystemFrequency / 100000  10us 中断一次
     * SystemFrequency / 1000000 1us  中断一次
     */
    if (SysTick_Config(SystemCoreClock / 100000)) {
        /* Capture error */
        while (1);
    }
}

SysTick_Config() 形参 ticks 决定中断周期:ticks = SystemCoreClock / N,则中断一次的时间 T = 1us × (N/72),本例 N=100000 对应 10us 中断一次。函数内部完成三件事:写 LOAD 重装载值 → 配置中断优先级为 15(最低)→ 使能定时器 + 中断 + AHB 时钟源。

如果你想自定义

  • 改中断周期 → 修改 SysTick_Config() 的入参,如 SystemCoreClock / 1000 对应 1ms 中断
  • 改中断优先级 → 在 SysTick_Config() 后调用 NVIC_SetPriority(SysTick_IRQn, n),n 范围 0~15,数值越小优先级越高

步骤2:中断方式延时函数

函数:Delay_us() + 中断服务 SysTick_Handler() + TimingDelay_Decrement() — 主循环等待变量递减到 0,中断每 10us 递减一次。


__IO uint32_t TimingDelay = 0;

/**
 * @brief   us 延时程序,10us 为一个单位(中断方式)
 * @param   nTime: Delay_us(1) 实现延时 1 × 10us = 10us
 * @retval  无
 */
void Delay_us(__IO u32 nTime)
{
    TimingDelay = nTime;
    while (TimingDelay != 0);
}

/**
 * @brief  SysTick 中断服务函数(每 10us 触发一次)
 */
void SysTick_Handler(void)
{
    TimingDelay_Decrement();
}

/**
 * @brief  获取节拍程序,在 SysTick_Handler 中调用
 */
void TimingDelay_Decrement(void)
{
    if (TimingDelay != 0x00) {
        TimingDelay--;
    }
}

工作流程:Delay_us(n) 把 n 赋给全局变量 TimingDelay,主循环空等其归零;SysTick 每 10us 进一次中断递减 TimingDelay。例如 Delay_us(100000) → 100000 × 10us = 1s。

步骤3:轮询方式延时函数(替代方案)

函数:SysTick_Delay_Us() / SysTick_Delay_Ms() — 查询 CTRL 寄存器 bit16 的 COUNTFLAG 标志位,计数器递减到 0 时硬件自动置 1,读取后自动清 0。


/**
 * @brief  SysTick 微秒级延时(轮询 COUNTFLAG 方式)
 * @param  us: 延时微秒数
 */
void SysTick_Delay_Us(__IO uint32_t us)
{
    uint32_t i;
    SysTick_Config(SystemCoreClock / 1000000);   /* 1us 中断一次 */

    for (i = 0; i < us; i++) {
        while (!((SysTick->CTRL) & (1 << 16)));  /* 等 COUNTFLAG 置 1 */
    }
    /* 关闭 SysTick 定时器 */
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}

/**
 * @brief  SysTick 毫秒级延时(轮询 COUNTFLAG 方式)
 * @param  ms: 延时毫秒数
 */
void SysTick_Delay_Ms(__IO uint32_t ms)
{
    uint32_t i;
    SysTick_Config(SystemCoreClock / 1000);      /* 1ms 中断一次 */

    for (i = 0; i < ms; i++) {
        while (!((SysTick->CTRL) & (1 << 16)));  /* 等 COUNTFLAG 置 1 */
    }
    /* 关闭 SysTick 定时器 */
    SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;
}

轮询法无需中断和全局变量,直接读 CTRL 的 bit16(COUNTFLAG):计数器从 RELOAD 递减到 0 时硬件置 1,读取后自动清 0。每次循环对应一次完整递减周期,所以 SysTick_Config(SystemCoreClock/1000) 配置为 1ms 周期,循环 ms 次即延时 ms 毫秒。

两种方式对比

  • 中断方式:延时期间 CPU 阻塞在 while 循环,但 SysTick 持续运行,可同时被更高优先级中断抢占;适合需要后台时基的场景
  • 轮询方式:不进中断,代码更直观;每次调用都重新 SysTick_Config,调用结束关闭定时器;适合简单的阻塞延时

步骤4:主函数逻辑

文件:USER/main.c | 函数:main() — 初始化 UART 与 SysTick,循环打印时间戳验证两种延时方式。


int main(void)
{
    uint32_t          cnt = 0;
    RCC_ClocksTypeDef clocks;

    delay_init();
    UART_Configuration(115200);
    RCC_GetClocksFreq(&clocks);

    printf("\n");
    printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, "
           "PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
           (float)clocks.SYSCLK_Frequency / 1000000,
           (float)clocks.HCLK_Frequency / 1000000,
           (float)clocks.PCLK1_Frequency / 1000000,
           (float)clocks.PCLK2_Frequency / 1000000,
           (float)clocks.ADCCLK_Frequency / 1000000);
    printf("SysTick Delay Test.\n");

    /* 初始化 SysTick(中断方式,10us 中断一次) */
    SysTick_Init();

    while (1)
    {
        /* 方式1:中断方式延时 1s(100000 × 10us) */
        printf("[%lu] interrupt delay 1s ...\n", cnt++);
        Delay_us(100000);

        /* 方式2:轮询方式延时 500ms */
        printf("[%lu] polling delay 500ms ...\n", cnt++);
        SysTick_Delay_Ms(500);
    }
}

主循环交替演示两种延时方式:中断方式 Delay_us(100000) 延时 1s,轮询方式 SysTick_Delay_Ms(500) 延时 500ms。通过串口打印时间戳可观察实际延时效果。

注意

轮询方式每次调用都会重新 SysTick_Config,会覆盖中断方式的配置。两种方式混合使用时需注意:调用 SysTick_Delay_Ms/Us 后,中断方式的 TimingDelay 递减周期已被改变,需重新调用 SysTick_Init() 恢复。

步骤5:printf 重定向

重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。


int fputc(int c, FILE *f)
{
    if (c == '\n')
        SER_PutChar('\r');         // 补回车,确保串口工具正确换行
    return (SER_PutChar(c));
}

int SER_PutChar(int ch)
{
    while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
    USART_SendData(USART_TEST, (uint8_t)ch);
    return ch;
}

编译与下载

Keil MDK 中打开 SysTick.uvprojxF7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。下载成功后按 RESET 键启动新固件。

运行验证

打开 WIZ UartTool V1.0 ,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。按 RESET 启动后,预期每隔约 1.5s 打印一条日志(中断延时 1s + 轮询延时 500ms):

SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
SysTick Delay Test.
[0] interrupt delay 1s ...
[0] polling delay 500ms ...
[1] interrupt delay 1s ...
[1] polling delay 500ms ...

可看到两条日志间隔约 1s(中断方式),随后约 500ms(轮询方式),依次交替。若时钟频率异常(SYSCLK 不是 72.0Mhz),延时时间会等比例偏差。

常见问题

Q: 程序卡在 SysTick_Init 的 while(1) 死循环

SysTick_Config() 返回 1 表示 ticks 参数超过 24bit 最大值(2²⁴ = 16777216)。检查 SystemCoreClock / N 是否超限,例如 SystemCoreClock / 1 = 72000000 就超了。

Q: Delay_us 卡在 while(TimingDelay != 0) 不退出

SysTick 中断未正常触发。检查:
1) SysTick_Handler 是否已在 w55mh32_it.cmain.c 中定义,且与启动文件向量表名称一致;
2) 是否被 SysTick_Delay_Ms/Us 覆盖配置后未恢复 SysTick_Init()

Q: 延时时间明显不准确(偏快或偏慢)

先看串口打印的 SYSCLK 是否为 72.0Mhz。若频率异常,检查 system_w55mh32.c 中 HSE_VALUE 是否与实际晶振一致。SysTick_Config 依赖 SystemCoreClock 宏,时钟配置错误会直接传导到延时周期。

Q: 轮询方式 SysTick_Delay_Ms 延时为 0 或极短

COUNTFLAG (bit16) 读取逻辑错误。正确写法:while (!((SysTick->CTRL) & (1<<16)));。注意 SysTick_Config 会把 TICKINT 位置 1(使能中断),若不希望进中断需在配置后手动清除:SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;

Q: 中断方式与轮询方式如何选择

中断方式适合需要持续后台时基的场景(如操作系统心跳、软定时器),延时期间可被高优先级中断抢占;轮询方式不进中断、代码简单,适合单纯的阻塞延时。两者都会阻塞当前任务,差异在于 SysTick 是否同时承担其他时基职责。本例程 delay.c 中的 delay_ms/delay_us 通常用轮询方式实现。

总结

本文基于 W55MH32L 的内核 SysTick 定时器实现两种精准延时方案:

  • SysTick 简介:CM3 内核 24bit 递减计数器,4 个寄存器(CTRL/LOAD/VAL/CALIB),标准库 SysTick_Config() 一行完成初始化
  • 中断方式SysTick_Init() 配置 10us 中断周期 → Delay_us(n) 等待 TimingDelay 归零 → SysTick_Handler 中递减
  • 轮询方式SysTick_Delay_Us/Ms() 查询 CTRL 的 bit16 COUNTFLAG,每次循环对应一个完整递减周期
  • 关键点:中断周期 = ticks / SYSCLK;SysTick_Config 形参最大 2²⁴;两种方式混用时注意轮询法会覆盖中断配置

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板

Andy

W5500 技术支持专家