1. 系统时钟节拍(Tick)的硬件原理与配置
各位同学,咱们今天正式开讲。第一个话题,就是整个任务调度系统的“心跳”——系统时钟节拍,也就是 Tick。
你想想看,一个操作系统也好,一个轻量级的调度器也好,它怎么知道什么时候该切换任务?靠的就是这个有规律的 Tick 中断。没有它,调度器就是个“瞎子”。
1.1 SysTick 定时器:Cortex-M 内核的“标配”
大部分现代单片机,尤其是 Cortex-M0/M3/M4/M7 内核,都内置了一个叫 SysTick 的定时器。这玩意儿是内核自带的,不依赖任何外设厂商的库。说白了,只要你用的是 Cortex-M 核,SysTick 就一定在。
它的核心作用就一个:产生一个周期性的中断。这个中断就是我们调度器的“节拍器”。
我个人习惯,拿到一个新板子,第一件事就是先把 SysTick 配好。因为后面所有的延时、超时、任务切换,全指着它呢。
SysTick 的核心寄存器(就三个):
- CTRL (控制和状态寄存器):启动/停止定时器,选择时钟源,查看计数是否到 0。
- LOAD (重装载值寄存器):设置你要计数的次数。计到 0 就触发中断,然后自动重装这个值。
- VAL (当前值寄存器):当前计数值,可读可写。写任意值会清空它并清除 COUNTFLAG 标志。
SysTick 是一个 24 位的递减计数器。什么意思?就是它从你设定的 LOAD 值开始,每个时钟周期减 1,减到 0 的时候,做两件事:
- 把 COUNTFLAG 位置 1(你可以轮询这个位,也可以不使能中断)。
- 如果使能了中断,就触发 SysTick_Handler 中断服务函数。
然后,硬件自动把 LOAD 值重新加载到 VAL 里,继续下一轮递减。整个过程不需要软件干预,非常省心。
1.2 如何配置 Tick 中断频率(1ms / 10ms)
配置频率,其实就是算 LOAD 值。公式很简单:
LOAD = (系统时钟频率 / Tick 频率) - 1
举个例子,假设你的系统时钟是 72MHz(STM32F103 的典型配置),你想要 1ms 的 Tick:
LOAD = (72,000,000 / 1000) - 1 = 72000 - 1 = 71999
如果你想要 10ms 的 Tick:
LOAD = (72,000,000 / 100) - 1 = 720000 - 1 = 719999
这里有个坑,我必须要提醒你。LOAD 寄存器只有 24 位,最大值是 16,777,215(0xFFFFFF)。你算出来的 LOAD 值不能超过这个数。如果系统时钟太高,比如 400MHz,你算 10ms 的 LOAD 值就是 3,999,999,还在范围内。但如果你算 100ms,LOAD 值就是 39,999,999,远超 24 位上限了。这时候你就得考虑用别的定时器,或者降低 Tick 频率。
我曾经踩过的坑: 有一次我把系统时钟超频到了 216MHz,然后想配一个 1ms 的 Tick,算出来 LOAD 值是 215,999。结果发现系统跑起来调度特别不稳定。查了半天,发现 SysTick 的时钟源我选错了,选成了 HCLK/8,实际频率只有 27MHz。所以配置前,一定要确认 SysTick 的时钟源是哪个。
下面是一个标准的初始化代码示例,以 STM32 标准库为例:
void SysTick_Init(uint32_t ticks)
{
// 1. 关闭 SysTick,防止配置过程中产生意外中断
SysTick->CTRL = 0;
// 2. 设置重装载值
SysTick->LOAD = ticks - 1;
// 3. 清空当前计数值
SysTick->VAL = 0;
// 4. 配置时钟源(通常选择处理器时钟,即 AHB 时钟)
// 并使能中断,使能定时器
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// 调用示例:系统时钟 72MHz,Tick 1ms
SysTick_Init(72000);
嗯,这里要注意,不同的 MCU 厂商的库函数封装可能不一样,但底层操作的都是那三个寄存器。你只要理解了原理,换什么芯片都能很快上手。
1.3 Tick 中断服务函数的设计要点
Tick 中断服务函数(ISR),是整个调度器的“心脏起搏点”。它的设计质量,直接决定了系统的实时性和稳定性。
我见过很多新手,把所有的活儿都往 Tick ISR 里塞,结果系统卡死。记住一个原则:Tick ISR 里只做“最轻量级”的事。
通常,Tick ISR 只做三件事:
- 递增一个全局的系统节拍计数器(Tick Count)。这个计数器是调度器进行时间片轮转、延时等待的基础。
- 检查是否有就绪的任务需要切换。如果有,设置一个标志位,或者直接调用 PendSV 来触发上下文切换。
- 递减软件定时器的计数值。如果某个软件定时器计数值减到 0,就设置对应的超时标志。
看一个典型的 Tick ISR 骨架:
volatile uint32_t g_ulTickCount = 0;
void SysTick_Handler(void)
{
// 1. 递增节拍计数器
g_ulTickCount++;
// 2. 递减软件定时器(假设有 10 个软件定时器)
for (int i = 0; i < 10; i++)
{
if (g_swTimer[i] > 0)
{
g_swTimer[i]--;
}
}
// 3. 通知调度器进行时间片轮转(如果有必要)
// 这里通常只是设置一个标志,真正的切换在 PendSV 里做
g_bNeedSched = 1;
}
我的个人建议: 不要在 Tick ISR 里做任何阻塞操作,比如等待某个外设标志位、调用 printf 打印调试信息。这些操作的时间是不可控的。如果一定要调试,可以用一个 GPIO 翻转来测量 ISR 的执行时间。
还有一个容易被忽略的点:中断优先级。SysTick 的中断优先级通常设置为最低(数值最大),或者至少比任何外设中断的优先级都低。为什么?因为 Tick 中断是周期性的,它不要求立即响应。而外设中断(比如 UART 接收、ADC 转换完成)往往有实时性要求。如果 Tick 中断优先级太高,它会打断外设中断,增加外设中断的延迟。
下面这张图,展示了 Tick 中断在整个系统中的位置和作用:
最后,关于 Tick 频率的选择,我给出一个参考:
| Tick 频率 | 典型应用场景 | 优点 | 缺点 |
|---|---|---|---|
| 1ms (1000Hz) | 高精度控制、音频处理、快速响应系统 | 时间分辨率高,延时精度好 | 中断频繁,CPU 开销大 |
| 10ms (100Hz) | 通用嵌入式系统、传感器采集、人机交互 | CPU 开销小,系统稳定 | 时间分辨率低,响应较慢 |
| 1s (1Hz) | 低功耗设备、环境监测、长时间记录 | 极低功耗,几乎不占用 CPU | 实时性极差,不适合任务调度 |
我个人习惯,对于大多数通用项目,先用 1ms 的 Tick。如果发现 CPU 负载过高,再考虑降低到 5ms 或 10ms。不要一开始就选 10ms,因为后面你会发现很多地方需要 1ms 的精度,改起来很麻烦。