2、系统时钟节拍:时钟节拍的概念、如何产生SysTick中断、配置时钟节拍频率
各位同学,咱们今天聊一个调度器里最基础、也最容易被忽视的东西——系统时钟节拍。
我刚开始做嵌入式那会儿,总觉得调度器嘛,不就是把任务排排队、轮流跑吗?后来被现实狠狠教育了一顿。有一次做多路数据采集,任务切换忽快忽慢,串口打印出来的时间戳完全对不上。查了两天才发现——时钟节拍根本没配准。
从那以后,我养成了一个习惯:写调度器之前,先把时钟节拍吃透。
2.1 时钟节拍是什么?
说白了,时钟节拍就是系统的心跳。就像人的心脏每秒钟跳那么多次,单片机也需要一个稳定的“心跳”来驱动任务切换。
你想想看,如果没有这个心跳,调度器怎么知道什么时候该切换任务?靠死循环数指令?那不同主频的芯片跑出来完全不一样,移植性直接归零。
时钟节拍的本质是:一个固定频率的周期性中断。每次中断到来,调度器就“醒”一次,检查要不要切换任务、要不要处理延时、要不要触发事件。
核心概念:时钟节拍 = 系统的心跳频率,单位通常是 Hz(每秒中断次数)。
常见节拍频率:100 Hz(10ms)、1000 Hz(1ms)、500 Hz(2ms)。
我个人习惯把节拍频率叫做 Tick Rate。这个值一旦定下来,整个调度器的行为就跟着它走了。
2.2 SysTick 定时器——Cortex-M 的专属心跳源
如果你用的是 Cortex-M 内核的单片机(STM32、GD32、NXP 的 LPC 系列等),那恭喜你——SysTick 定时器是硬件自带的,不需要额外配置外设定时器。
SysTick 是一个 24 位递减计数器,挂在内核时钟上。它从你设定的重装载值开始,每个时钟周期减 1,减到 0 的时候触发一次中断,然后自动重新装载,继续下一轮。
嗯,这里要注意:SysTick 的中断优先级是可以调的。我见过有人把 SysTick 优先级设得特别低,结果被其他中断一堵,调度器直接“断气”。
避坑指南:我曾经在一个项目中把 SysTick 优先级设成了最低,结果高频率的外设中断(比如 SPI 传输)一进来,SysTick 被无限推迟,任务调度直接瘫痪。后来我改成 SysTick 优先级设为最高或次高,问题才解决。
2.3 如何产生 SysTick 中断?
产生 SysTick 中断只需要三步:
- 设置重装载值(决定节拍频率)
- 清除当前计数值(防止一启动就触发)
- 使能 SysTick 中断 + 使能计数器
代码长这样:
// 假设系统时钟为 72 MHz,目标节拍频率为 1000 Hz(1ms)
// SysTick 重装载值 = 系统时钟 / 节拍频率 - 1
// 72,000,000 / 1000 - 1 = 71,999
#define SYSTICK_LOAD_VALUE 71999
void SysTick_Init(void)
{
// 1. 设置重装载值
SysTick->LOAD = SYSTICK_LOAD_VALUE;
// 2. 清除当前计数值
SysTick->VAL = 0;
// 3. 使能 SysTick 中断 + 使能计数器
// Bit 1: TICKINT(中断使能)
// Bit 0: ENABLE(计数器使能)
SysTick->CTRL |= (1 << 1) | (1 << 0);
}
中断服务函数怎么写?
void SysTick_Handler(void)
{
// 每次节拍到来,调用调度器的节拍处理函数
// 这个函数里会处理任务延时、时间片轮转等
Scheduler_TickHandler();
}
我个人习惯在 Scheduler_TickHandler() 里只做最轻量的事——递增一个全局 tick 计数器,真正的任务切换逻辑放到主循环或 PendSV 里做。为什么?因为中断里不能待太久,否则其他中断会饿死。
2.4 配置时钟节拍频率——选多少合适?
这个问题没有标准答案,但有几个经验法则:
| 节拍频率 | 周期 | 适用场景 | 注意事项 |
|---|---|---|---|
| 100 Hz | 10 ms | 低功耗、慢速传感器采集 | 任务切换粒度粗,延时精度低 |
| 500 Hz | 2 ms | 通用控制、按键扫描 | 平衡功耗与响应 |
| 1000 Hz | 1 ms | 电机控制、音频处理 | 中断开销明显增加 |
| 2000 Hz+ | 0.5 ms 以下 | 高速数据采集、实时性要求极高 | CPU 大部分时间在跑中断 |
我个人的建议是:从 1000 Hz 起步。这个频率在大多数 MCU 上都能跑,1ms 的精度也够用。如果发现 CPU 负载太高,再降到 500 Hz 试试。
小技巧:如果你不确定选多少,可以先在代码里定义一个宏:
#define TICK_RATE_HZ 1000u
所有跟节拍相关的计算都用这个宏。后期改频率,只需要改这一个地方。
2.5 时钟节拍与系统时钟的关系
这里有个容易搞混的点:系统时钟(SYSCLK)和时钟节拍(Tick)不是一回事。
- 系统时钟:CPU 的主频,比如 72 MHz、168 MHz。它决定了指令执行速度。
- 时钟节拍:调度器的心跳频率,比如 1000 Hz。它决定了任务切换的粒度。
SysTick 的重装载值 = 系统时钟频率 / 节拍频率 - 1。所以如果你改了系统时钟(比如从 72 MHz 切到 48 MHz 省电),一定要重新计算 SysTick 的重装载值,否则节拍频率就变了。
我曾经踩过这个坑:产品休眠后切到低速时钟,忘了更新 SysTick,结果 1ms 的节拍变成了 2ms,所有延时都翻倍了。
2.6 本章知识结构图
下面这张图把时钟节拍、SysTick 和调度器的关系画清楚了:
2.7 配置 SysTick 的完整流程
最后,我把配置 SysTick 的完整步骤总结一下,方便你直接照着做:
- 确定系统时钟频率——从时钟树配置里拿到 HCLK 的值。
- 确定目标节拍频率——根据应用场景选 100 Hz ~ 2000 Hz。
- 计算重装载值——
LOAD = SYSCLK / TICK_RATE - 1。 - 检查范围——LOAD 不能超过 0xFFFFFF(24 位最大值)。
- 初始化 SysTick——设置 LOAD、清 VAL、使能中断和计数器。
- 编写中断服务函数——调用调度器的 Tick 处理函数。
- 验证——用逻辑分析仪或 GPIO 翻转测一下实际中断频率。
验证方法:在 SysTick_Handler 里翻转一个 GPIO,用示波器或逻辑分析仪看波形。如果周期是 1ms,说明配置正确。我曾经用这个方法抓出过时钟源选错导致节拍翻倍的 bug。