一、SysTick(系统滴答定时器)概述
操作系统需要一个滴答定时器周期性产生中断,以产生系统运行的节拍。在中断服务程序里,基于优先级调度的操作系统会根据进程优先级切换任务,基于时间片轮转系统会根据时间片切换任务。总之,滴答定时器是一个操作系统的“心跳”。
Cortex-M3在内核部分封装了一个滴答定时器--SysTick,在之前的ARM内核通常是不会把定时器做进内核,定时器都是SOC厂商自己制作的外设。显然,Cortex-M3封装了这么一个定时器,对于将操作系统移植到不同SOC厂商生产的Cortex-M3系类MCU上,带来了极大的方便。Cortex-M3内核统一了这样的一个系统滴答定时器,移植操作系统的时候可以使用内核的定时器,而忽略掉不同厂商生产定时器带来的分歧。
二、SysTick control and status register(STK_CTRL)
SysTick的控制是极其简单的,它的控制和状态都汇聚在同一个寄存器STK_CTRL上。
STK_CTRL的每一位的含义英文解释都是很清晰的,不必多说。需要额外讨论的是COUNTFLAG标志位,这个标志位代表的含义是:当计数为0时,也即STK_VAL计数至0时,此标志位置1。
经过笔者一番摸索,对此位有更多的看法。
COUNTFLAG:
1、此位与SysTick的中断无关,不是中断标志位,可以算作事件标志位(计数至0事件)。
2、此位可以用作软件查询
3、读写此寄存器都会硬件自动更新COUNTFLAG的值,当然此位的值软件也是可以改写的。也就是说,倘若我们轮训查看COUNTFLAG是否置1(也就是计数是否结束)。当SysTick硬件上计数为0,COUNTFLAG因此硬件自动置1。在我们软件读取STK_CTRL的时候,其实SysTick的STK_VAL的值已经不是0(因为自动重装,并且可能计时几个CLK了)。这个时候我们读取到了COUNTFLAG的标志位的1,同时也将COUNTFLAG自动清零。
三、滴答定时器应用之精准延时函数
1、函数实现思路
函数实现使用“轮询状态位COUNTFLAG”实现精准延时节拍10us。
在使用的时候,首先调用函数SysTick_Init配置SysTick的定时周期为10us。在延时函数中,当启动定时器后,就调用函数SysTick_GetFlagStatus轮询是否定时10us结束,如果结束就更新一下延时节拍变量nTime。
由于SysTick定时器自动重装计数器初值,而且SysTick_GetFlagStatus在检测到SET的时候,COUNTFLAG也自动清理。所以软件不必装定时器初值,也不必手动清除标志位COUNTFLAG。
2、函数实现代码
#include "bsp_sysTick.h" /** * @brief 读取SysTick的状态位COUNTFLAG * @param 无 * @retval The new state of USART_FLAG (SET or RESET). */ static FlagStatus SysTick_GetFlagStatus(void) { if(SysTick->CTRL&SysTick_CTRL_COUNTFLAG_Msk) { return SET; } else { return RESET; } } /** * @brief 清除SysTick的状态位COUNTFLAG * @param 无 * @retval 无 */ static void SysTick_ClearFlag(void) { SysTick->CTRL &= ~ SysTick_CTRL_COUNTFLAG_Msk; } /** * @brief 配置系统滴答定时器 SysTick * @param 无 * @retval 1 = failed, 0 = successful */ uint32_t SysTick_Init(void) { /* SystemFrequency / 1000 1ms中断一次 * SystemFrequency / 100000 10us中断一次 * SystemFrequency / 1000000 1us中断一次 */ /* 设置定时周期为10us */ if (SysTick_Config(SystemCoreClock / 100000)) { /* Capture error */ return (1); } /* 关闭滴答定时器且禁止中断 */ SysTick->CTRL &= ~ (SysTick_CTRL_ENABLE_Msk | SysTick_CTRL_TICKINT_Msk); return (0); } /** * @brief us延时程序,10us为一个单位 * @param * @arg nTime: Delay_us( 1 ) 则实现的延时为 1 * 10us = 10us * @retval 无 */ void Delay_us(__IO uint32_t nTime) { /* 清零计数器并使能滴答定时器 */ SysTick->VAL = 0; SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; for( ; nTime > 0 ; nTime--) { /* 等待一个延时单位的结束 */ while(SysTick_GetFlagStatus() != SET); } /* 关闭滴答定时器 */ SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk; }
3、函数的优点和缺陷
优点:
使用轮询法实现精准延时是可靠的,因为硬件自动重装定时器初值,只要我们在下一次计数结束(为0)之前,将节拍计数变量nTime更新,那么这个延时函数就是可靠的。
使用轮询法的另一个好处是没有用到全局变量,完全是局部变量搞定了所需功能。倘若使用中断延时,必须利用全局变量给精准延时函数传递参数。
缺陷:
由于使用的是轮询法,有可能被其他的中断打断,假设其他的中断的服务时间有很长,使得“在下一次计数结束(为0)之前,没有将节拍计数变量nTime更新”,那么延时的时间将要增长。
4、注意
此延时函数的最小分辨率不能设置为1us,最好设置为>=10us,这是因为轮训的周期和1us相比具有可比性,时间误差太大。
四、滴答定时器应用之程序段计时
1、函数实现思路
首先对滴答定时器初始化,计时节拍数是计数器的最大值。在感兴趣的程序段开始处,启动定时器,在程序段的结束处关闭定时器。倘若这段时间很长,超过了计数器的计数最大值,就会在中断函数中对溢出次数进行计数。最终的程序段时间决定于计数器的数据寄存器SysTick->VAL 中的剩余值和中断溢出次数。
另外为了使程序能够对不同的程序段或者不同情况下的程序段进行计时,使用了一个结构体定义保存计时数据的结构体类型。在对程序段进行计时的时候,通过一个运行指针指向所要保存的变量中。
2、函数代码
① User_SysTick.c
/** ****************************************************************************** *计时最小单位:1/72M s *计时最大长度:2^32/72M = 59.65 s *使用方法: *(1) 定义一个保存计时数据的TimingVarTypeDef类型变量Time *(2) 初始化 * SysTick_Time_Init(&Time); *(3) 在while循环中放置启动/停止函数 * while(1){ * SysTick_Time_Start(); * 测试运行时间的代码 * SysTick_Time_Stop(); * } ****************************************************************************** */ /* 定义保存未使用DMA时测试程序段运行时间的变量 */ TimingVarTypeDef Time; /* 指针指向当前保存时间的变量 */ TimingVarTypeDef * CurrentTimingVar; /* 系统滴答定时器的中断次数 */ uint32_t TimeupTimes; /** * @brief 配置系统滴答定时器 SysTick * @param 无 * @retval 1 = failed, 0 = successful */ uint32_t SysTick_Init(void) { /* 设置定时周期为最大定时数SysTick_LOAD_RELOAD_Msk */ if (SysTick_Config(SysTick_LOAD_RELOAD_Msk)) { /* Capture error */ return (1); } /* 关闭滴答定时器且禁止中断 */ SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk; return (0); } /** * @brief 滴答定时器 SysTick 计时初始化 * @param 初始化计时变量的成员--计时次数 * @retval 无 */ void SysTick_Time_Init(TimingVarTypeDef * TimingVar) { /* 指针指向当前保存时间的变量 */ CurrentTimingVar = TimingVar; /* 计时次数初始化 */ CurrentTimingVar->SetSaveTimesNum = SaveTimesBufNum - 2; } /** * @brief 滴答定时器 SysTick 计时启动 * @param 无 * @retval 无 */ void SysTick_Time_Start(void) { /* 判断已经计时次数是否达到设置的计时次数 */ if(CurrentTimingVar->SaveTimesTemp < CurrentTimingVar->SetSaveTimesNum){ /* 滴答定时器的数据寄存器清零 */ SysTick->VAL = 0; /* 滴答定时器中断次数清零 */ TimeupTimes = 0; /* 启动滴答定时器 */ SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; } } /** * @brief 滴答定时器 SysTick 计时停止并保存处理数据 * @param 无 * @retval 无 */ void SysTick_Time_Stop(void) { /* 保存已经计时次数 */ uint32_t TimesTemp = CurrentTimingVar->SaveTimesTemp; /* 保存设置计时总次数 */ uint32_t SetSaveTimesNum = CurrentTimingVar->SetSaveTimesNum; uint32_t i,TimeWidthAverageTemp = 0; /* 保存设置计时总次数 */ if(SysTick->CTRL & SysTick_CTRL_ENABLE_Msk) { /* 关闭滴答定时器 */ SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk; /* 计算计时总时间 */ CurrentTimingVar->TimeWidth[TimesTemp] = SysTick_LOAD_RELOAD_Msk * TimeupTimes \ + (SysTick_LOAD_RELOAD_Msk - SysTick->VAL + 1); /* 判断计时次数是否满 */ if((++TimesTemp) == SetSaveTimesNum) { /* 计算平均值 */ for(i = 0;i < SetSaveTimesNum; i++) { TimeWidthAverageTemp += CurrentTimingVar->TimeWidth[i]; } CurrentTimingVar->TimeWidthAvrage = TimeWidthAverageTemp/SetSaveTimesNum; } /* 已经计时次数变量加1 */ CurrentTimingVar->SaveTimesTemp++; } }
② User_SysTick.h
#define SaveTimesBufNum 4 /* 计时存储区的大小 */ typedef struct { uint32_t SetSaveTimesNum; /* 设置计时总次数 */ uint32_t SaveTimesTemp; /* 已经计时的次数 */ uint32_t TimeWidth[SaveTimesBufNum]; /* 计时存储区 */ uint32_t TimeWidthAvrage; /* 平均计时长度 */ } TimingVarTypeDef; /* 计时变量类型 */ extern TimingVarTypeDef Time; extern uint32_t TimeupTimes; extern uint32_t SysTick_Init(void); extern void SysTick_Time_Init(TimingVarTypeDef * TimingVar); extern void SysTick_Time_Start(void); extern void SysTick_Time_Stop(void);
③ stm32f10x_it.c
/** * @brief This function handles SysTick Handler. * @param None * @retval None */ void SysTick_Handler(void) { TimeupTimes++; }
参考资料:《STM32F10xxx Cortex-M3 programming manual.pdf》
《STM32库开发实战指南》