一、Systick介绍

        从原理上来说,Systick定时器和开发板上的通用定时器没有区别。从功能上来说,Systick定时器主要是用来用来进行延时的,而通用或者高级定时器往往用来进行PWM输出、输入捕获等功能。至于为什么不用通用定时器或者高级定时器来完成延时功能,则是考虑到节省MCU的资源来做更重要的事。

        SysTick系统定时器是属于内核中的一个外设,内嵌在NVIC中。该定时器是一个24位的向下递减的计数器。在裸机编程中常用做延时函数,而在FreeRTOS中则用来给系统提供时钟的,因此非常重要。

        SysTick 是 Cortex-M 系列内核中普遍存在的系统定时器,共 24 位(高 8 位保留),通过不断递减进行计数,到达 0 时将 SYST_CSR 的 COUNTFLAG 标志位置 1 并根据 TICKINT 标志位决定是否产生内核中断。因此,利用 SysTick 实现延时函数就分为中断式和非中断式两种。前者容易产生优先级冲突,也不适合在中断函数中调用;这里选择的是后者,直接对寄存器进行操作和查询。需要注意的是,使用 FreeRTOS 等实时操作系统时,修改 SysTick 会影响系统时钟建议采用软件延时或使用其他定时器。

         SysTick共有4个相关寄存器,通常只用到3个(校准寄存器少用)。

寄存器名称寄存器描述
CTRLSysTick控制及状态寄存器
LOADSysTick重装载数值寄存器
VALSysTick当前数值寄存器
CALIBSysTick校准数值寄存器

二、systick优先级介绍

(1)裸机中SysTick中断优先级最高

        SysTick定时器中断是一个周期性的计时器,在实时操作系统中可以用于提供系统的时钟节拍。SysTick中断具有较高的中断优先级,通常为0(或最低的数值)。由于SysTick定时器的重要性,它通常需要以较高的优先级运行,以确保准确地产生定时中断。

(注意:中断优先级是通过中断优先级寄存器(NVIC_IPR)来配置的。在ARM Cortex-M处理器中,中断优先级的数值越小表示优先级越高,因此PendSV中断的中断优先级数值为最大值,而SysTick中断的中断优先级数值为最小值。)

(2)FreeRTOS中SysTick中断优先级最低

FreeRTOS 在启动任务调度器的函数中设置了 PendSV 和 SysTick 的中断优先级

BaseType_t xPortStartScheduler( void )
{
/* 忽略其他代码 */

    /* 设置 PendSV 和 SysTick 的中断优先级为最低中断优先级 */
    portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI;
    portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI;

/* 忽略其他代码 */
}

三、SysTick_Config(X)的作用

void systick_config(void)
{
    /* setup systick timer for 1000Hz interrupts */
    if (SysTick_Config(SystemCoreClock / 1000U)){
        /* capture error */
        while (1){
        }
    }
    /* configure the systick handler priority */
    NVIC_SetPriority(SysTick_IRQn, 0x00U);
}

 SysTick_Config的参数,其实就是一个时钟次数,叫systick重装定时器的值。已知预装载值和时钟频率,那么可以求出进入一次中断的时间

预装载值/时钟频率
(SystemCoreClock / 1000U)/ (SystemCoreClock) S 进入一次中断,也就是1ms进入一次

四、裸机使用systick中断延时1ms

void delay_1ms(uint32_t count)
{
    delay = count;
    while(0U != delay){
    }
}

void delay_decrement(void)
{
    if (0U != delay){
        delay--;
    }
}

void SysTick_Handler(void)
{
    delay_decrement();
}

五、裸机使用systick非中断延时1ms

void delay_1ms(uint32_t count)
{
	uint32_t ticks;
	uint32_t told,tnow,reload,tcnt=0;

	reload = SysTick->LOAD;                     //获取重装载寄存器值
	ticks = count * (SystemCoreClock / 1000);   //计算定时1ms滴答定时器要计数的时间值
	told=SysTick->VAL;                          //获取当前数值寄存器值
	while(1)
	{
		tnow = SysTick->VAL;          			//获取当前数值寄存器值
		if(tnow!=told)              			//当前值不等于开始值说明已在计数。切记:滴答定时器是倒计数
		{         
			if(tnow<told)             			//当前值小于开始数值,说明未计到0
				tcnt += told - tnow;     		//计数值=开始值-当前值
			else                      			//当前值大于开始数值,说明已计到0并重新计数
				tcnt += reload-( tnow - told ); //told tnow reload  (相当于整段事件中只有told到tnow这段时间没有计时
			told = tnow;                		//更新开始值
			if(tcnt >= ticks)break;     		//计数值大于等于要延迟的计数值,则计数完毕
		} 
	} 	
}

六、nop延时

通过使用__NOP()函数,因为使用了8M晶振9倍频,72MHz,所以一个nop约等于1/72us,所以使用72个nop函数为一个us,然后根据需要的定时时间进行计算。

void delay_us(u32 nTimer)
{
	u32 i=0;
	for(i=0;i<nTimer;i++){
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
	}
}

stm32F407,主频为168Mhz 

void delay_u(uint16_t micro)
{
     uint32_t Delay = micro * 168/4;
    do
    {
        __NOP();
    }
    while (Delay --);
}

七、freeRTOS中的systick

(1)在FreeRTOS中SysTick定时器尤为重要,因为它是给FreeRTOS系统提供时钟的。在FreeRTOS中任务的切换即每个任务运行的时间是由SysTick定时器提供的。

(2) 在Cortex-M内核上,FreeRTOS使用Systick定时器作为心跳时钟,一般默认心跳时钟为1ms,进入Systick中断后,内核会进入处理模式进行处理,在Systick中断处理中,系统会在 ReadList 就绪链表从高优先级到低优先找需要执行的任务,进行调度,如果有任务的状态发生了变化,改变了状态链表,就会产生一个pendSV异常,进入pendSV异常,通过改变进程栈指针(PSP)切换到不同的任务。对于相同优先级的任务,每隔一个Systick,运行过的任务被自动排放至该优先级链表的尾部(时间片调度),用户也可以在线程模式下主动触发PendSV,进行任务切换。freeRTOS采取先运行优先级高的任务(优先级优先),然后同等优先级的任务采用时间片轮转

(3)freeRTOS不使用systick进行延时的原因?

        delay_ms()这个函数主要是靠不断读取SysTick计数器的值来实现延迟。但是加入了RTOS之后,RTOS强制将systick的中断设置为最低,假设在一个中断优先级比systick高的中断interrupt中调delay_ms()来进行延时,那么由于中断优先级高于systick,从而导致systick无法抢占,也就无法增加计数器的值,就会导致interrupt中断服务函数死等delay_ms()延时,无法退出,从而造成比systick优先级低的中断服务都无法使用,系统也无法调度。

(4)freeRTOS中如何使用systick进行延时

void delay_us(u32 nus)
{ 
       u32 ticks;
       u32 told,tnow,reload,tcnt=0;
       if((0x0001&(SysTick->CTRL)) ==0)    //定时器未工作
              vPortSetupTimerInterrupt();  //初始化定时器
 
       reload = SysTick->LOAD;                     //获取重装载寄存器值
       ticks = nus * (SystemCoreClock / 1000000);  //计数时间值
       
       vTaskSuspendAll();//阻止OS调度,防止打断us延时
       told=SysTick->VAL;  //获取当前数值寄存器值(开始时数值)
       while(1)
       {
              tnow=SysTick->VAL; //获取当前数值寄存器值
              if(tnow!=told)  //当前值不等于开始值说明已在计数
              {         
                     if(tnow<told)  //当前值小于开始数值,说明未计到0
                          tcnt+=told-tnow; //计数值=开始值-当前值
 
                     else     //当前值大于开始数值,说明已计到0并重新计数
                            tcnt+=reload-tnow+told;   //计数值=重装载值-当前值+开始值  (
                                                      //已从开始值计到0) 
 
                     told=tnow;   //更新开始值
                     if(tcnt>=ticks)break;  //时间超过/等于要延迟的时间,则退出.
              } 
       }  
       xTaskResumeAll();	//恢复OS调度		   
} 

        

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐