3、时钟源选择:SysTick定时器原理、外部定时器作为时钟源、低功耗模式下的时钟选择

好,咱们接着聊Tick配置。前面讲了怎么算、怎么配,但有个关键问题我一直没细说——时钟源从哪来?

你可能觉得,这还用问?直接用SysTick呗。没错,大部分时候确实是这样。但我在项目里遇到过几次,默认的SysTick在某些场景下就是不好使。比如低功耗模式下,它直接罢工了。或者你的系统时钟频率变了,Tick也跟着乱跳。

所以这一节,咱们把时钟源这件事彻底讲透。

3.1 SysTick定时器原理

先说最常用的——SysTick。它是Cortex-M内核自带的一个24位递减计数器。说白了,就是一个硬件定时器,专门给操作系统用的。

它的工作流程很简单:

  1. 从你设定的重装载值开始,每个时钟周期减1
  2. 减到0,触发一次中断
  3. 自动重新装载,继续下一轮

嗯,就这么朴实无华。但有几个细节,我建议你留意:

  • 24位计数器:最大值是2^24 - 1 = 16777215。如果你的时钟频率太高,这个值可能不够用。比如168MHz的主频,你算算,一个Tick最多能计多久?
  • 时钟源可选:SysTick可以选处理器时钟(HCLK),也可以选HCLK/8。我个人习惯用HCLK/8,这样重装载值可以设得大一些,减少中断频率。
  • 校准寄存器:有些芯片有校准值,但说实话,我很少用它。因为不同芯片的校准精度参差不齐。

重要提醒:SysTick的中断优先级,我建议设成最低。为什么?因为Tick中断太频繁了,如果优先级太高,会抢占其他关键任务的中断。我曾经在一个项目中,把SysTick优先级设成了0,结果串口中断一直被抢,数据都收不全。

3.2 外部定时器作为时钟源

那什么时候需要用外部定时器呢?我遇到过两种情况:

  • SysTick被占用了:比如你在用某些RTOS调试组件,它可能自己用了SysTick
  • 需要更灵活的时钟控制:比如你想在Tick中断里做点别的事,或者需要多个不同频率的定时器

用外部定时器替换SysTick,其实不复杂。核心思路就一句话:找一个通用定时器,让它产生固定频率的中断,然后在中断服务函数里调用FreeRTOS的Tick处理函数。

代码大概长这样:

// 假设用TIM2,配置成1ms中断一次
void TIM2_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
    {
        TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
        
        // 关键:调用FreeRTOS的Tick处理函数
        if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
        {
            xPortSysTickHandler();
        }
    }
}

这里有个坑,我踩过——中断优先级。外部定时器的中断优先级,必须和SysTick原本的优先级一致。否则调度器会出问题。FreeRTOS内部有个宏叫configKERNEL_INTERRUPT_PRIORITY,你配外部定时器时,得用这个值。

警告:用外部定时器时,别忘了把SysTick关掉。不然两个定时器都在触发Tick中断,系统时间会乱套。我曾经调试了一下午,才发现SysTick没关,两个定时器在打架。

3.3 低功耗模式下的时钟选择

低功耗,这是个大话题。很多产品要求省电,比如电池供电的物联网设备。但FreeRTOS的Tick机制,天然和低功耗有冲突。

为什么?你想想看,系统想睡觉,但Tick定时器每隔1ms就把它叫醒一次。这还怎么省电?

所以,低功耗模式下,时钟源的选择就变得很关键了。我总结了几种常见做法:

方案 原理 适用场景 我踩过的坑
SysTick + 自动唤醒 进入低功耗前,设置一个RTC或LPTIM定时器,在下一个任务到期时唤醒 任务周期较长,可以容忍一定延迟 唤醒时间计算不准,任务被延迟执行
无Tick模式 完全关闭Tick中断,只在任务切换或外部事件时更新系统时间 极低功耗场景,任务很少 时间精度很差,不适合需要精确延时的应用
低频时钟源 用32.768kHz的RTC时钟代替高频SysTick 需要保持基本的时间计数,同时省电 Tick周期变长,系统响应变慢

我个人比较推荐第一种方案——SysTick + 自动唤醒。FreeRTOS其实内置了低功耗支持,叫configUSE_TICKLESS_IDLE。你把它设为1,系统就会在空闲时自动进入低功耗模式,并且计算下一次任务到期的时间,提前设置好唤醒定时器。

配置起来也不复杂:

// 在FreeRTOSConfig.h中
#define configUSE_TICKLESS_IDLE         1

// 然后实现这两个函数
void vPortSuppressTicksAndSleep(TickType_t xExpectedIdleTime)
{
    // 1. 计算可以睡多久
    // 2. 设置唤醒定时器
    // 3. 进入低功耗模式
    // 4. 醒来后,调整系统Tick计数
}

TickType_t xPortGetTickRateHz(void)
{
    // 返回当前的Tick频率
    return configTICK_RATE_HZ;
}

小技巧:实现vPortSuppressTicksAndSleep时,记得处理一种边界情况——在进入低功耗的瞬间,有中断发生了。这时候不能睡,得立即返回。我刚开始没注意这个,结果系统偶尔会卡死,查了好久才发现是这里的问题。

嗯,关于时钟源的选择,差不多就这些了。总结一下我的建议:

  • 普通项目:直接用SysTick,省事
  • 需要灵活控制:换外部定时器,注意优先级一致
  • 低功耗需求:开启Tickless模式,配合RTC或LPTIM

下一节,咱们聊聊Tick在实际项目中可能会遇到的那些坑,以及怎么调试Tick相关的问题。到时候我会分享几个我亲手解决的案例,保证让你少走弯路。