ARM Cortex-M架构中有一个位于核内的标准系统节拍时钟(Systick)。系统节拍时钟的寄存器地址由ARM标准规定,在所有芯片中都是相同的。开发者可通过ARM CMISIS库标准函数进行配置。相对于芯片自身提供的计时器,使用系统节拍时钟的优势在于其初始化和中断函数程序可以运行在任何ARM Cortex-M核的芯片上。而芯片计时器的初始化和中断函数只适用于特定芯片,在进行程序移植时需要重新研读芯片数据手册编写程序。
系统节拍时钟是一个倒数计时器。系统节拍时钟的SYST_RVR
寄存器用来设置倒数计时器的初始值。硬件首先会将SYST_RVR
中的初始值装载进SYST_CVR
寄存器中。在每个时钟上升或者下降沿,SYST_CVR
寄存器值会减1。倒数到0后芯片自动将SYST_RVR
寄存器值重新装载到SYST_CVR
寄存器中重新开始倒计时。在倒数到0时,系统节拍时钟还、会将SYST_CSR
寄存器中的COUNTFLAG
位置1。如果在SYST_CSR
寄存器中设置了TICKINT
位,同时还会触发SysTick
中断。SysTick
中断位于ARM Cortex-M定义的16个中断位中,在所有芯片上具有相同的中断号。
以下是core_cm4.h
中SysTick
的定义。
/**
\brief Structure type to access the System Timer (SysTick).
*/
typedef struct
{
__IOM uint32_t CTRL; /*!< Offset: 0x000 (R/W) SysTick Control and Status Register */
__IOM uint32_t LOAD; /*!< Offset: 0x004 (R/W) SysTick Reload Value Register */
__IOM uint32_t VAL; /*!< Offset: 0x008 (R/W) SysTick Current Value Register */
__IM uint32_t CALIB; /*!< Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;
...
#define SysTick ((SysTick_Type *) SysTick_BASE ) /*!< SysTick configuration struct */
以下是利用ARM CMSIS标准库提供的系统节拍时钟初始化函数,开发者程序#include "core_cm4.h"
后可直接调用。需要注意的是
SYST_RVR
寄存器有效值只有24bit。所以在设置倒数计时器初始值时要确保其不大于0xFFFFFF
。ticks
值取决于倒是计时器周期和驱动系统节拍时钟的时钟频率。如果驱动时钟频率为24MHz
,开发者希望系统节拍时钟中断频率为1KHz(1ms周期)。那么tick
的值为24*1000000/1000 = 24000
。SysTick_CTRL_TICKINT_Msk
。/**
\brief System Tick Configuration
\details Initializes the System Timer and its interrupt, and starts the System Tick Timer.
Counter is in free running mode to generate periodic interrupts.
\param [in] ticks Number of ticks between two interrupts.
\return 0 Function succeeded.
\return 1 Function failed.
\note When the variable __Vendor_SysTickConfig is set to 1, then the
function SysTick_Config is not included. In this case, the file device.h
must contain a vendor-specific implementation of this function.
*/
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL); /* Reload value impossible */
}
SysTick->LOAD = (uint32_t)(ticks - 1UL); /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
SysTick->VAL = 0UL; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0UL); /* Function successful */
}
ARM Cortex M系统节拍时钟有四个寄存器SYST_CSR(SysTick Control and Status Register),SYST_RVR(SysTick Reload Value),SYST_CVR(SysTick Current Value),SYST_CALIB(SysTick Calibration Value Register)。具体寄存器定义可以参见ARM Cortex-M的官方文档:ARM Cortex-M4 System timer, SysTick/ARM Cortex-M3 Systimer/ARM Cortex-M0/M0+ optional systimer, SysTick。
系统节拍时钟可以用于很多场合。这里简单列出几种。
在写底层驱动时经常需要进行延时(delay)或者超时判定(timeout)。例如在初始化外设时需要先复位外设然后等待复位结束约几百毫秒后开始进行配置。抑或是在通信驱动中常会使用轮询方式发送和接受数据,轮询超时后报错。这些功能都可以通过系统节拍时钟实现。
以下是系统节拍时钟中断的示例程序。tick
提供了一个系统上电后的单调时间 monotonic time
。如果系统节拍时钟中断的周期是1ms
,那tick
也可以作为毫秒精度的时间戳timestamp_ms
。
volatile uint32_t tick = 0;
void SysTick_Handler(void)
{
tick++;
}
uint32_t get_tick(void)
{
return tick;
}
利用上述的中断函数并假设系统节拍时钟中断周期为1ms
,我们可以实现如下的延时函数。
#define TICK_RATE_MS 1U // 1毫秒的tick数
void delay_ms(uint32_t t)
{
uint32_t start_ts = get_tick() * TICK_RATE_MS;
while ((get_tick() * TICK_RATE_MS) - start_ts < t)
{
}
}
类似的我们可以用tick
来实现超时判定。一下是一个通讯驱动的发送函数示例。
enum error_code
{
SUCCESS,
ERROR_TIMEOUT
};
enum error_code send_data(void *data, size_t len, uint32_t timeout)
{
enum erro_code e = ERROR_TIMEOUT;
uint32_t start_tick = get_tick();
send_low_level(data, len); // 底层发送函数
while(get_tick() - start_tick < timeout)
{
if (send_low_level_done()) // 查询发送是否成功直至超时
{
e = SUCCESS;
break;
}
}
return e;
}
嵌入式实时操作系统通常需要按一定频率呼叫操作系统调度器(scheduler)来实现切换任务。在移植操作系统到ARM Cortex-M架构的MCU上时,一般使用系统节拍器产生周期中断,在系统节拍器的中断函数内呼叫操作系统调度器。这样做的优势是提升代码可移植性,不依赖特定芯片的时钟。
这里以FreeRTOS的ARM Cortex-M4移植代码作为范本分析。FreeRTOS中ARM Cortex-M4的Systick_Handler()
实现位于FreeRTOS-Kernel/portable/GCC/ARM_CM4_MPU/port.c中。如果开发者不需要在Systick_Handler()
中增加自定义功能,可以通过在FreeRTOSConfig.h
中直接定义#define xPortSysTickHandler SysTick_Handler
来把xPortSysTickHandler()
直接作为系统节拍器中断函数。这个中断函数中最重要的是xTaskIncrementTick()
。
void xPortSysTickHandler( void )
{
uint32_t ulDummy;
ulDummy = portSET_INTERRUPT_MASK_FROM_ISR();
{
/* Increment the RTOS tick. */
if( xTaskIncrementTick() != pdFALSE )
{
/* Pend a context switch. */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portCLEAR_INTERRUPT_MASK_FROM_ISR( ulDummy );
}
xTaskIncrementTick()
的实现位于FreeRTOS-Kernel/task.c中。其实现较为复杂,有兴趣可以去研读完整的代码。这里摘录其中的注释来解释其主要功能。
/* Called by the portable layer each time a tick interrupt occurs.
Increments the tick then checks to see if the new tick value will cause any
tasks to be unblocked. */
注释中指明xTaskIncrementTick()
在芯片的节拍中断函数中使用。每次被呼叫时,节拍值(tick)增1来为系统提供一个单调时间并且查询是否有延时函数到期或者有新任务可以解冻运行。
SysTick是ARM Cortex-M芯片中非常有用的功能。只需要进行简单的配置就可以给整个系统增加一个单调时间。这个单调时间可用于延时,可用于超时判定和提供时间戳。而且SysTick的代码不加修改就可以运行在任何基于ARM Cortex M架构的芯片上,可以减少系统移植时的开发时间。