1. 项目概述:为什么SysTick是嵌入式开发的“心跳”
在嵌入式开发领域,尤其是基于ARM Cortex-M内核的项目里,SysTick系统定时器是一个你绕不开的核心模块。它不像GPIO、UART那样直接与外部世界交互,却像人体的心跳一样,为整个系统提供着最基础、最稳定的节拍。无论是实现一个简单的delay_ms()函数,还是支撑起一个完整的实时操作系统(RTOS)内核,SysTick都是那个幕后功臣。
我接触过不少从单片机转过来的开发者,初次面对ARM Cortex-M的SysTick时,常常会把它和普通的外设定时器(如TIMx)混淆。实际上,SysTick是ARM内核的一部分,是Cortex-M处理器标准配备的“官方”定时器。它的设计初衷就是为了给操作系统或任何需要精确时间基准的软件提供一个简单、可靠、且与具体芯片型号无关的时钟源。这意味着,只要你写的代码是针对Cortex-M内核的,那么关于SysTick的操作和寄存器配置,在不同厂商(TI、ST、NXP等)的芯片上几乎是完全一致的,移植性极强。
在德州仪器(TI)的CC27xx系列无线MCU中,这颗基于Cortex-M33内核的芯片,其SysTick模块同样遵循ARM的标准架构。但CC27xx作为一款面向低功耗无线应用的芯片,SysTick的角色更为关键。它不仅负责产生系统节拍,还深度参与到芯片的低功耗管理策略中。例如,当CPU进入睡眠(Sleep)模式时,许多外设时钟会被门控以节省功耗,但SysTick往往需要保持运行,以便在预定时间唤醒系统,或者为低功耗定时任务提供时间基准。理解并正确配置SysTick,是确保CC27xx这类低功耗MCU既能“睡得香”又能“准时醒”的关键。
本文将以CC27xx的参考手册资料为蓝本,结合我多年的嵌入式实战经验,为你深入拆解ARM Cortex-M33 SysTick的每一个寄存器,从位域定义到配置技巧,从基础应用到高级场景,让你彻底掌握这颗“系统心跳”的驾驭之道。
2. SysTick寄存器全景与内存映射解析
在动手配置之前,我们必须先搞清楚SysTick在芯片内存地图中的“住址”。这对于直接操作寄存器或者调试时查看内存状态至关重要。
2.1 系统控制空间与SysTick的“家”
ARM Cortex-M架构定义了一个名为“系统控制空间”(System Control Space, SCS)的特殊内存区域,地址范围从0xE000E000到0xE000EFFF。这个区域是内核的“后花园”,里面存放着NVIC(嵌套向量中断控制器)、SCB(系统控制块)、MPU(内存保护单元)以及我们关注的SysTick等核心模块的寄存器。
在CC27xx的内存映射表中,我们可以清晰地找到:
- SYSTICK的基地址是
0xE000E010。
这意味着,SysTick的四个寄存器——控制与状态寄存器(SYST_CSR)、重载值寄存器(SYST_RVR)、当前值寄存器(SYST_CVR)和校准值寄存器(SYST_CALIB)——都从这个基地址开始,以固定的偏移量排列。
注意:在TI的文档中,你可能会看到
SYSTIMER和SYSTICK两个相似的寄存器描述表。这通常是文档排版或版本差异导致的,它们描述的是同一个硬件模块——即Cortex-M内核的SysTick定时器。在实际编程中,我们始终使用0xE000E010这个SCS空间内的地址进行访问。
2.2 寄存器访问类型解读
在阅读芯片手册时,每个寄存器位域都会标注访问类型(Access Type),这是硬件对软件操作的“许可协议”,必须严格遵守:
- R (Read): 只读。软件只能读取该位的值,任何写入操作都不会改变其硬件状态,通常用于反映状态标志。
- W (Write): 只写。软件写入操作有效,但读取该位可能返回未定义的值或固定值。SysTick的CURRENT字段就是一个典型例子,写入任何值用于清零计数器。
- R/W (Read/Write): 可读可写。软件可以自由读取和修改该位的值。
- Reserved: 保留位。这是最容易踩坑的地方!手册中标记为
RES的位,软件绝不能依赖其读出的值(可能是0,可能是1,也可能是随机值),更严禁向其写入非复位值。向保留位写入可能导致不可预测的行为,从功能异常到系统锁死都有可能。安全的做法是,在修改寄存器时,使用“读-修改-写”操作,只改变我们关心的位,保留其他位(包括保留位)不变。
3. 核心寄存器深度剖析与配置实战
了解了SysTick的“住址”和操作规则后,我们来逐一拆解它的四个核心寄存器。我会结合代码示例和配置逻辑,让你不仅知道每个位是干什么的,更明白为什么要这么配置。
3.1 SYST_CSR:控制与状态寄存器(偏移 0x00)
这个寄存器是SysTick的“大脑”,负责启停定时器、选择时钟源、控制中断使能,并告诉我们定时器是否已经“走完了一轮”。
位域详解与配置策略:
Bit 0 - ENABLE: SysTick计数器使能位。
- 0:计数器关闭。
- 1:计数器开启,开始从
SYST_CVR的当前值递减计数(如果SYST_CVR为0,则会立即从SYST_RVR重载值开始)。 - 实战要点:在初始化时,标准的启动顺序是:先配置重载值(
SYST_RVR),再清空当前值(SYST_CVR),最后才置位ENABLE。避免在计数器处于随机值时启动。
Bit 1 - TICKINT: SysTick异常(中断)使能位。
- 0:计数器递减到0时,不产生SysTick异常(中断)。
- 1:计数器递减到0时,产生SysTick异常。此时,如果中断在NVIC中已启用且优先级足够高,CPU将跳转到SysTick中断服务程序(ISR)执行。
- 实战要点:如果你使用SysTick仅为RTOS提供时基,那么此位必须置1。如果仅用于简单的延时(轮询方式),则可以置0,通过查询
COUNTFLAG来判断是否超时。
Bit 2 - CLKSOURCE: 时钟源选择位。这是影响定时精度的关键配置。
- 0:使用外部参考时钟。在CC27xx中,这通常是指来自芯片时钟树分频后的内核时钟(
HCLK)或一个专用的低速时钟。具体是哪一路,需要查阅CC27xx的时钟树文档。重要提示:在低功耗睡眠模式下,这个时钟可能会被关闭,导致SysTick停止! - 1:使用处理器时钟(
FCLK)。对于Cortex-M33,这就是内核的自由运行时钟,与HCLK通常同源(除非有特殊配置)。选择此时钟源,只要CPU在运行(即使处于Sleep模式),SysTick就能工作,精度最高。 - 配置建议:在绝大多数应用场景下,特别是需要SysTick在睡眠模式下保持计时以唤醒系统时,必须将
CLKSOURCE置为1,选择处理器时钟。只有当你明确需要一个与CPU主频异步的、更慢的、且可能在深度睡眠下仍能运行的时钟时,才考虑使用外部时钟(并需确认该时钟在目标功耗模式下是否有效)。
- 0:使用外部参考时钟。在CC27xx中,这通常是指来自芯片时钟树分频后的内核时钟(
Bit 16 - COUNTFLAG: 计数到零标志位。
- 这是一个状态位。当SysTick计数器从1递减到0时,此位被硬件自动置1。
- 读取
SYST_CSR寄存器时,会返回该位的当前值。特别需要注意的是:读取该寄存器后,此位会被自动清零。此外,向SYST_CVR寄存器写入任何值,也会清零此位。 - 应用场景:在无中断(轮询)的延时函数中,可以通过循环查询此位是否被置1来判断延时是否结束。这是实现
delay()函数的一种经典方法。
一个典型的SYST_CSR初始化代码片段(C语言,基于CMSIS-Core)如下:
// 假设我们使用处理器时钟,并启用SysTick中断 SysTick->CTRL = (1 << 2) | // CLKSOURCE = 1, 选择处理器时钟 (1 << 1) | // TICKINT = 1, 启用中断 (0 << 0); // ENABLE = 0, 先不启动,等待其他配置完成 // 注意:此处直接赋值,实际中更安全的做法是使用位操作,避免影响保留位。 // 例如:SysTick->CTRL |= (1 << 2) | (1 << 1); SysTick->CTRL &= ~(1 << 0);3.2 SYST_RVR:重载值寄存器(偏移 0x04)
这个寄存器决定了SysTick的“心跳”周期。它定义了计数器递减到0后,自动重新装载的初始值。
- Bit [23:0] - RELOAD: 重载值。这是一个24位寄存器,因此可配置的最大值为
2^24 - 1 = 16,777,215。 - 重要限制:重载值不能为0。如果设置为0,SysTick计数器在使能后不会产生周期性的中断或计数到零事件(尽管一次从0到0的“计数”可能会触发一次动作)。这通常不是我们期望的行为。
- 周期计算:定时周期(秒) =
(RELOAD + 1) / Fclk。其中Fclk是SysTick的输入时钟频率(由CLKSOURCE决定)。- 举例:若
Fclk = 48 MHz,想要产生1ms(0.001秒)的中断,则RELOAD = (0.001 * 48,000,000) - 1 = 47999。 - 计算过程:
RELOAD是计数器从开始递减到0所经历的时钟周期数。从RELOAD值递减到0,总共是RELOAD + 1个时钟周期(因为计数了RELOAD, RELOAD-1, ..., 1, 0)。所以公式是周期 = (计数值) / 频率 = (RELOAD + 1) / Fclk。
- 举例:若
配置示例:
// 目标:在48MHz系统时钟下,配置SysTick为1ms中断一次 #define SYSTEM_CLOCK_FREQ_HZ 48000000UL #define SYSTICK_INTERRUPT_PERIOD_MS 1UL // 计算重载值,并确保不超过24位最大值 uint32_t reload_value = (SYSTEM_CLOCK_FREQ_HZ / 1000UL) * SYSTICK_INTERRUPT_PERIOD_MS - 1; if (reload_value > 0xFFFFFFUL) { // 错误处理:计算出的重载值超出范围,需要降低中断频率或提高时钟频率 while(1); // 或进行其他错误处理 } SysTick->LOAD = reload_value & 0xFFFFFFUL; // 明确地只写入低24位3.3 SYST_CVR:当前值寄存器(偏移 0x08)
这个寄存器反映了SysTick计数器当前的计数值。
- Bit [23:0] - CURRENT: 当前计数值。这是一个24位的递减计数器。
- 读写特性:
- 读操作:返回计数器当前的瞬时值。由于计数器可能在读取过程中正在递减,为了获得准确的时间间隔测量,建议先读取一次,再读取一次,如果第二次值比第一次大(因为可能发生了翻转),则需要进行处理。更常见的做法是,如果需要精确计时,最好在计数器使能前将其清零,然后通过重载值和时钟频率来计算时间。
- 写操作:向该寄存器写入任何值,都会立即将CURRENT字段清零。同时,这个写操作也会清除SYST_CSR中的COUNTFLAG标志位。
- 初始化关键步骤:在配置完
SYST_RVR后,必须向SYST_CVR写入一个值(通常写0),以将计数器清零并清除可能存在的旧COUNTFLAG标志。否则,如果计数器使能时CURRENT是一个随机值,第一个定时周期将是不可预测的长度。
标准初始化流程中的使用:
// 1. 配置重载值(可选,如果之前没配过) SysTick->LOAD = desired_reload_value; // 2. 清空当前计数器,并清除可能的COUNTFLAG标志 SysTick->VAL = 0; // 写入任何值均可,通常写0 // 3. 配置控制寄存器并启动 SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk;3.4 SYST_CALIB:校准值寄存器(偏移 0x0C)
这个寄存器提供了芯片出厂时对10ms定时周期的硬件校准值,主要用于在不知道精确系统时钟频率时,获得一个相对准确的时间基准。注意:这个寄存器是只读的。
Bit [23:0] - TENMS: 10毫秒校准值。
- 这个值表示,当SysTick时钟源以标称频率运行时,为了产生10ms(100Hz)的定时中断,需要向
SYST_RVR写入的RELOAD值。 - 重要警告:
TENMS的值是针对一个特定的、理想的参考时钟频率(例如,某些Cortex-M芯片是固定的内部RC时钟)给出的。它不一定等于你当前系统实际使用的时钟频率下的10ms计数值! - 如果该字段读数为0,则表示该芯片未提供校准值,不能依赖此功能。
- 这个值表示,当SysTick时钟源以标称频率运行时,为了产生10ms(100Hz)的定时中断,需要向
Bit 30 - SKEW: 精度指示位。
- 0:表示
TENMS值是精确的(在标称时钟下)。 - 1:表示
TENMS值不是精确的10ms,可能因工艺、电压、温度(PVT)变化而有最大±1%的误差。软件需要将此误差考虑在内。
- 0:表示
如何使用校准值?校准值的核心用途是估算系统时钟频率,尤其是在启动初期,时钟树尚未完全按预期配置时。
uint32_t tenms_value = SysTick->CALIB & SysTick_CALIB_TENMS_Msk; if (tenms_value != 0) { // 假设芯片标称的校准时钟频率是 X Hz (例如 1MHz)。 // 那么,实际的系统时钟频率 F_actual 可以估算为: // F_actual = (tenms_value * X) / (10ms对应的标称计数值) // 但这需要你知道X是多少,通常手册会说明。 // 更常见的做法是:如果你需要一个大致准确的延时,可以直接使用这个值。 // 例如,配置 SysTick->LOAD = tenms_value / 10; 来获得大约1ms的中断(误差取决于SKEW位和时钟差异)。 } else { // 芯片未提供校准值,必须通过其他方式(如外部晶振、PLL锁定)获知精确时钟频率。 }实操心得:在现代MCU开发中,尤其是使用HSE(外部高速晶振)和PLL(锁相环)产生系统时钟的场合,我们通常能精确知道
SYSCLK的频率。因此,SYST_CALIB寄存器的直接使用场景并不多。它的存在更多是为了兼容性,或在时钟源非常不确定的极端情况下提供一个“兜底”的、大致可用的时间基准。对于CC27xx这类有复杂时钟管理单元的芯片,强烈建议通过配置和读取时钟树相关寄存器来获取精确的时钟频率,而不是依赖TENMS。
4. SysTick在嵌入式系统中的典型应用模式
理解了寄存器,我们来看看SysTick在真实项目中如何大显身手。它绝不仅仅是一个简单的定时器。
4.1 模式一:裸机环境下的精准延时(轮询法)
在没有操作系统的简单项目中,我们经常需要微秒或毫秒级的延时。利用SysTick的COUNTFLAG标志,可以实现不占用CPU中断的忙等待延时。
实现一个delay_ms()函数:
/** * @brief 基于SysTick的毫秒级延时(轮询方式) * @param ms: 要延时的毫秒数 * @note 此函数会阻塞CPU。使用前必须正确初始化SysTick时钟源和重载值。 */ void delay_ms(uint32_t ms) { // 假设SysTick已配置为1ms产生一次COUNTFLAG // 即 SysTick->LOAD = (SystemCoreClock / 1000) - 1; // 且 CLKSOURCE = 1, TICKINT = 0, ENABLE = 1 for(uint32_t i = 0; i < ms; i++) { // 等待COUNTFLAG被置位 while((SysTick->CTRL & SysTick_CTRL_COUNTFLAG_Msk) == 0) { // 空循环,等待 } // 读取CTRL寄存器会清除COUNTFLAG,为下一次等待做准备 // 也可以显式地清空CURRENT寄存器来清除COUNTFLAG:SysTick->VAL = 0; } } // 初始化函数 void systick_delay_init(void) { // 停止SysTick SysTick->CTRL = 0; // 配置为1ms周期(假设系统时钟是48MHz) SysTick->LOAD = (48000000UL / 1000UL) - 1; // 清空计数器 SysTick->VAL = 0; // 选择处理器时钟,不使能中断,启动计数器 SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk; }注意事项:这种轮询延时会完全占用CPU,在延时期间CPU无法执行其他任务。它适用于对时序要求非常严格、且系统任务简单的场景。在复杂的或低功耗应用中,应避免长时间使用此类忙等待延时。
4.2 模式二:RTOS的时钟节拍(Tick)源
这是SysTick最经典、最重要的用途。几乎所有的Cortex-M RTOS(如FreeRTOS, RT-Thread, μC/OS)都默认使用SysTick作为其心跳时钟源,用于任务调度、时间片轮转和定时器管理。
RTOS中SysTick中断服务例程(ISR)的典型职责:
- 递增系统时钟计数器:一个全局变量
xTickCount加1,记录系统启动以来的Tick数。 - 检查任务延时列表:遍历所有被挂起(延时等待)的任务,如果某个任务的唤醒时间已到,则将其置为就绪状态。
- 执行任务调度:如果本次Tick导致更高优先级的任务就绪,则触发一次任务上下文切换(PendSV异常)。
- 处理软件定时器:如果RTOS支持软件定时器,则检查并触发到期的定时器回调函数。
以FreeRTOS的vPortSetupTimerInterrupt()函数为例(简化):
void vPortSetupTimerInterrupt( void ) { /* 计算所需的重载值,产生固定频率的Tick中断(例如1kHz,即1ms) */ uint32_t ulReloadValue = ( configCPU_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL; /* 检查重载值是否在24位范围内 */ configASSERT( ( ulReloadValue & ~( 0xFFFFFFUL ) ) == 0UL ); /* 配置重载值寄存器 */ portNVIC_SYSTICK_LOAD_REG = ulReloadValue; /* 清空当前值寄存器 */ portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL; /* 配置控制与状态寄存器: - 使用处理器时钟 (CLKSOURCE=1) - 使能SysTick中断 (TICKINT=1) - 启动计数器 (ENABLE=1) */ portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT ); }经验分享:在RTOS中,SysTick中断的频率(
configTICK_RATE_HZ)需要仔细权衡。频率太高(如10kHz),中断开销大,消耗更多CPU资源;频率太低(如100Hz),任务调度和定时器的粒度变粗,系统响应变慢。对于大多数应用,1ms(1kHz)是一个经验上的甜点值,在响应速度和开销之间取得了良好平衡。
4.3 模式三:低功耗应用中的周期性唤醒
在CC27xx这类无线MCU中,低功耗是核心设计目标。CPU大部分时间可能处于睡眠(Sleep)或深度睡眠(Deep Sleep)模式。SysTick可以配置为在CPU睡眠时继续运行(前提是CLKSOURCE选择处理器时钟,且该时钟在睡眠模式下仍有效),并在计数到零时产生中断,将CPU从睡眠中唤醒,执行一些周期性的任务(如传感器采样、无线协议栈处理等),然后再次进入睡眠。
低功耗定时唤醒流程:
- 配置SysTick为所需的唤醒周期(例如,100ms),并使能中断。
- 在SysTick中断服务程序(ISR)中,执行必要的周期性任务。
- 在中断服务程序末尾,调用进入低功耗模式的函数(如
__WFI()或__WFE()指令)。 - CPU进入睡眠,SysTick继续递减计数。
- 当SysTick再次计数到零,触发中断,CPU被唤醒,跳转到ISR,周而复始。
关键配置点:必须确认在目标低功耗模式下,SysTick所使用的时钟源(FCLK)是否仍然活跃。在Cortex-M33中,通常Sleep模式下FCLK是保持运行的,因此SysTick可以正常工作。但在Deep Sleep模式下,主时钟可能被关闭,此时SysTick也会停止。需要查阅CC27xx具体的电源模式说明。
5. 高级主题与疑难杂症排查
掌握了基本应用后,我们来看看一些更深入的话题和实际开发中容易遇到的问题。
5.1 SysTick中断优先级与嵌套
SysTick异常在ARM异常模型中编号为15(IRQ号为-1),其优先级是可配置的。在RTOS中,SysTick中断的优先级通常被设置为最低的可编程优先级(即优先级数值最大)。例如,如果使用8位优先级中的3位用于抢占优先级,那么最低优先级可能是0xE0或0xFF(取决于具体实现)。
为什么设置成最低?这是为了系统的实时性和稳定性。SysTick中断作为系统的“心跳”,它执行的任务(更新时基、检查延时)通常不是最紧急的。如果设置成高优先级,它可能会打断正在处理关键硬件事件(如UART接收、ADC转换完成)的中断服务程序,导致这些实时性要求更高的事件响应延迟,甚至丢失数据。让SysTick中断可以被其他更紧急的中断抢占,是更合理的设计。
配置示例(使用CMSIS-NVIC函数):
// 将SysTick中断的优先级设置为最低(假设优先级分组为3,即抢占优先级占高3位) NVIC_SetPriority(SysTick_IRQn, (1 << (8 - __NVIC_PRIO_BITS)) - 1); // 或者更直接地,如果优先级位数为3,最低优先级为7 // NVIC_SetPriority(SysTick_IRQn, 7);5.2 重载值计算中的溢出与精度问题
这是一个常见的陷阱。RELOAD是24位的,最大值约为1677万。如果你的系统时钟频率很高,而想要的定时周期很长,就可能发生溢出。
举例:系统时钟Fclk = 200 MHz,想要配置1秒的SysTick中断。 所需重载值 =200,000,000 - 1 = 199,999,999。 这个值远大于0xFFFFFF(16,777,215),无法直接实现。
解决方案:
- 降低SysTick中断频率:改为配置10ms中断,然后在中断服务程序中用一个软件计数器累加,每100次中断视为1秒。
volatile uint32_t soft_timer_1s = 0; void SysTick_Handler(void) { static uint32_t tick_count = 0; tick_count++; if(tick_count >= 100) { // 10ms * 100 = 1s tick_count = 0; soft_timer_1s++; // 执行你的1秒任务 } // ... RTOS或其他节拍处理 } - 使用更低频率的时钟源:如果
CLKSOURCE可以选择外部时钟,且该时钟频率较低(如32.768kHz),则可以轻松实现长定时。但需权衡精度和功耗。 - 使用其他定时器外设:对于超长定时需求,可以考虑使用芯片的其他通用定时器(如CC27xx的LGPT),它们通常是32位或16位但带预分频的,更适合长定时。
5.3 调试技巧与常见问题排查表
当你发现SysTick不工作、中断不触发或者定时不准时,可以按照以下思路排查:
| 现象 | 可能原因 | 排查步骤与解决方法 |
|---|---|---|
| SysTick中断完全不触发 | 1. SysTick未使能中断(TICKINT=0)。2. SysTick计数器未启动( ENABLE=0)。3. 重载值( RELOAD)为0。4.NVIC中SysTick中断未使能(最常见疏忽!)。 5. 中断服务函数(ISR)名称或向量表配置错误。 | 1. 检查SYST_CSR的TICKINT和ENABLE位。2. 检查 SYST_RVR是否被正确写入非零值。3.关键:使用 NVIC_EnableIRQ(SysTick_IRQn);(CMSIS)或在SCB中设置相关使能位。4. 确认启动文件或链接脚本中,向量表里 SysTick_Handler的地址指向了你的C函数。 |
| 第一次中断时间异常长 | 使能SysTick前未清空SYST_CVR。 | 在设置LOAD后,使能ENABLE前,务必先向VAL(CVR)寄存器写入0。 |
| 定时周期不准 | 1.CLKSOURCE选择错误,使用了在当前功耗模式下会停止的时钟。2. 重载值计算错误,忽略了 +1。3. 系统时钟频率( Fclk)与预期不符(如PLL未锁定、分频比配错)。4. 中断服务程序执行时间过长,影响了下次中断的准时性。 | 1. 确认CLKSOURCE=1(使用处理器时钟)。2. 复核计算公式: 周期 = (RELOAD + 1) / Fclk。3. 通过读取芯片时钟树寄存器,验证实际的系统时钟频率。 4. 优化中断服务程序代码,确保其执行时间远小于SysTick中断周期。 |
| 在低功耗模式下SysTick停止 | 进入的低功耗模式关闭了SysTick所使用的时钟源(FCLK)。 | 1. 查阅芯片数据手册,确认目标低功耗模式下FCLK的状态。2. 如果 FCLK确实会关闭,考虑使用在低功耗模式下仍运行的独立时钟源(如低频RC振荡器)来驱动另一个定时器(如RTC或低功耗定时器LGPT)实现唤醒。 |
| SysTick中断导致其他高优先级任务延迟 | SysTick中断优先级设置过高。 | 按照5.1节的建议,将SysTick中断优先级设置为系统最低(数值最大)。 |
5.4 安全状态(TrustZone)下的SysTick
对于Cortex-M33这类支持TrustZone安全扩展的处理器,SysTick在安全(Secure)和非安全(Non-secure)世界是银行化的。这意味着安全世界和非安全世界各有自己独立的一套SysTick寄存器副本(SYST_CSR_S,SYST_CSR_NS等)和独立的中断向量(SysTick_S_Handler,SysTick_NS_Handler)。
这对开发的影响:
- 安全世界的软件可以配置和使用自己的SysTick,完全不受非安全世界的影响。
- 非安全世界的软件也可以配置和使用自己的SysTick,但通常需要安全世界的软件进行初始化的授权或委托。
- 一个常见的RTOS运行模式是:安全世界运行一个简单的安全监控程序或可信服务,使用自己的SysTick;非安全世界运行一个功能丰富的通用RTOS(如FreeRTOS),也使用自己的SysTick。两者互不干扰。
在CC27xx的编程中,你需要根据你的TrustZone分区方案,明确当前代码运行在哪个安全状态,并访问对应的寄存器组。CMSIS-Core提供了相应的安全属性宏来帮助区分,例如在安全代码中直接使用SysTick指针访问的就是安全世界的寄存器。
6. 在CC27xx工程中的具体实现与代码参考
最后,我们结合CC27xx的SDK(软件开发工具包),看看如何优雅、正确地初始化和使用SysTick。TI的SDK通常提供了高度封装的驱动库,但理解其底层实现依然非常重要。
6.1 使用TI DriverLib进行初始化
TI的CC27xx SDK通常包含一个DriverLib,提供了硬件抽象层的API。对于SysTick,初始化可能如下所示:
#include <ti/devices/cc27xx/driverlib/sys_ctrl.h> #include <ti/devices/cc27xx/driverlib/interrupt.h> // 假设系统时钟已配置为48MHz #define SYSTEM_CLOCK_HZ 48000000UL #define SYSTICK_PERIOD_MS 1UL // 1ms中断 void systick_init_with_driverlib(void) { // 1. 计算重载值 uint32_t reloadValue = (SYSTEM_CLOCK_HZ / 1000UL) * SYSTICK_PERIOD_MS - 1; // 2. 使用DriverLib API配置SysTick // 此函数内部会处理重载值、清空当前值、配置控制寄存器 SysTickPeriodSet(reloadValue); // 设置重载值并清空当前值 // 3. 注册中断处理函数(需要你先实现SysTick_Handler函数) // 在启动文件或中断管理器中,通常已经将SysTick_Handler关联好了。 // 这里确保你的C函数实现了 void SysTick_Handler(void) {} // 4. 在NVIC中使能SysTick中断(DriverLib可能已封装) IntEnable(INT_SYSTICK); // INT_SYSTICK是DriverLib定义的中断号 // 5. 使能SysTick计数器(选择处理器时钟、使能中断) SysTickEnable(); // 这个API通常会设置CLKSOURCE=1, TICKINT=1, ENABLE=1 }6.2 直接寄存器操作(备选方案)
在某些对代码尺寸或时序有极端要求的场合,或者你想完全掌控底层,可以直接操作寄存器:
#define SYSTICK_BASE 0xE000E010UL #define SYST_CSR (*((volatile uint32_t *)(SYSTICK_BASE + 0x0))) #define SYST_RVR (*((volatile uint32_t *)(SYSTICK_BASE + 0x4))) #define SYST_CVR (*((volatile uint32_t *)(SYSTICK_BASE + 0x8))) #define SYST_CALIB (*((volatile uint32_t *)(SYSTICK_BASE + 0xC))) // CSR位定义 #define SYST_CSR_ENABLE (1UL << 0) #define SYST_CSR_TICKINT (1UL << 1) #define SYST_CSR_CLKSOURCE (1UL << 2) #define SYST_CSR_COUNTFLAG (1UL << 16) void systick_init_direct(void) { // 1. 禁用SysTick SYST_CSR &= ~SYST_CSR_ENABLE; // 2. 设置重载值 (1ms @ 48MHz) uint32_t reload_val = (48000000UL / 1000UL) - 1; SYST_RVR = (reload_val & 0x00FFFFFFUL); // 确保只写入24位 // 3. 清空当前计数器(同时清除COUNTFLAG) SYST_CVR = 0; // 4. 配置控制寄存器:使用处理器时钟、使能中断、先不启动 SYST_CSR = SYST_CSR_CLKSOURCE | SYST_CSR_TICKINT; // 注意:这里没有设置ENABLE位 // 5. 在NVIC中设置优先级并使能中断(需要操作NVIC寄存器,此处略) // NVIC_SetPriority(SysTick_IRQn, ...); // NVIC_EnableIRQ(SysTick_IRQn); // 6. 最后,启动SysTick计数器 SYST_CSR |= SYST_CSR_ENABLE; }6.3 一个完整的SysTick应用框架示例
下面是一个在CC27xx裸机环境中,使用SysTick实现多组软件定时器的框架思路,这在很多物联网传感器轮询场景中非常实用:
#include <stdint.h> #include <stdbool.h> // 软件定时器结构体 typedef struct { uint32_t timeout_ticks; // 超时的SysTick节拍数 uint32_t start_tick; // 启动时的SysTick节拍数 bool is_active; // 定时器是否激活 void (*callback)(void); // 超时回调函数 } soft_timer_t; #define MAX_TIMERS 5 soft_timer_t timer_list[MAX_TIMERS]; volatile uint32_t systick_counter = 0; // SysTick中断累加的全局节拍计数器 void SysTick_Handler(void) { systick_counter++; // 更新全局时基 // 检查所有软件定时器 for(int i = 0; i < MAX_TIMERS; i++) { if(timer_list[i].is_active) { // 计算经过的节拍数(注意处理计数器回绕) uint32_t elapsed_ticks = systick_counter - timer_list[i].start_tick; if(elapsed_ticks >= timer_list[i].timeout_ticks) { timer_list[i].is_active = false; // 标记为完成 if(timer_list[i].callback != NULL) { timer_list[i].callback(); // 执行回调 } } } } } // 启动一个软件定时器 bool start_soft_timer(uint8_t id, uint32_t timeout_ms, void (*cb)(void)) { if(id >= MAX_TIMERS) return false; if(timeout_ms == 0) return false; // 将毫秒转换为SysTick节拍数(假设SysTick配置为1ms中断) uint32_t timeout_ticks = timeout_ms; timer_list[id].timeout_ticks = timeout_ticks; timer_list[id].start_tick = systick_counter; timer_list[id].is_active = true; timer_list[id].callback = cb; return true; } // 主函数初始化 int main(void) { // 初始化硬件、时钟... systick_init_with_driverlib(); // 初始化SysTick为1ms中断 // 启动几个软件定时器 start_soft_timer(0, 1000, timer0_callback); // 1秒后执行 start_soft_timer(1, 500, timer1_callback); // 500毫秒后执行 while(1) { // 主循环处理其他任务 // SysTick中断会在后台自动更新并检查定时器 if(/* 某个条件 */) { start_soft_timer(2, 200, timer2_callback); // 动态启动新定时器 } // 进入低功耗模式,等待中断唤醒 __WFI(); } return 0; }这个框架展示了如何基于SysTick这个稳定的硬件时基,构建一个灵活的软件定时器系统,从而轻松管理多个不同周期的异步任务,这是嵌入式系统开发中非常经典的模式。通过深入理解SysTick的每一个寄存器位,你就能在项目中游刃有余地驾驭时间,构建出稳定、高效且低功耗的嵌入式应用。