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 的时候,做两件事:

  1. 把 COUNTFLAG 位置 1(你可以轮询这个位,也可以不使能中断)。
  2. 如果使能了中断,就触发 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 只做三件事:

  1. 递增一个全局的系统节拍计数器(Tick Count)。这个计数器是调度器进行时间片轮转、延时等待的基础。
  2. 检查是否有就绪的任务需要切换。如果有,设置一个标志位,或者直接调用 PendSV 来触发上下文切换。
  3. 递减软件定时器的计数值。如果某个软件定时器计数值减到 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 中断在整个系统中的位置和作用:

系统时钟节拍与调度器协同关系图 系统时钟节拍与调度器协同关系 系统时钟 (HCLK) SysTick 定时器 (24位递减计数器) 配置 LOAD 值,产生周期性中断 Tick 中断服务函数 (SysTick_Handler) 递增 Tick Count | 递减软件定时器 | 设置调度标志 系统节拍计数器 软件定时器管理 任务调度标志 核心原则:Tick ISR 只做轻量级工作,真正的任务切换由 PendSV 处理

最后,关于 Tick 频率的选择,我给出一个参考:

Tick 频率 典型应用场景 优点 缺点
1ms (1000Hz) 高精度控制、音频处理、快速响应系统 时间分辨率高,延时精度好 中断频繁,CPU 开销大
10ms (100Hz) 通用嵌入式系统、传感器采集、人机交互 CPU 开销小,系统稳定 时间分辨率低,响应较慢
1s (1Hz) 低功耗设备、环境监测、长时间记录 极低功耗,几乎不占用 CPU 实时性极差,不适合任务调度

我个人习惯,对于大多数通用项目,先用 1ms 的 Tick。如果发现 CPU 负载过高,再考虑降低到 5ms 或 10ms。不要一开始就选 10ms,因为后面你会发现很多地方需要 1ms 的精度,改起来很麻烦。