在STM32CubeMX启用FreeRTOS后,在导出代码时会出现一个如图4所示的对话框。提示在使用FreeRTOS时,强烈建议将HAL的基础时钟设置为非SysTick定时器。在前面的示例中,我们都是将HAL的基础时钟设置为定时器TIM6,但并未详细说明这么做的原因。
在前一节已经介绍了HAL基础时钟的作用,以及使用SysTick定时器或TIM6定时器作为HAL基础时钟时的工作原理。通过前面章节对FreeRTOS的介绍,也知道了FreeRTOS需要使用SysTick定时器作为其基础时钟,以产生FreeRTOS的嘀嗒信号,在SysTick的定时中断里进行任务状态检查,需要时发出任务调度申请。
图4 在使用FreeRTOS时提示需要使用非SysTick定时器作为HAL基础时钟源
那么,在使用FreeRTOS时,如果在图4的对话框中点击“Yes”,执意使用SysTick作为HAL的基础时钟,生成的代码编译后能否正常运行呢?如果使用了TIM6作为HAL的基础时钟,FreeRTOS是如何对SysTick进行初始化,如何对SysTick的中断进行处理的呢?
对于第一个问题,如果在图4的对话框中点击“Yes”,执意使用SysTick作为HAL的基础时钟,生成的代码编译后是无法正常运行的,即使FreeRTOS只有一个非常简单的任务。这种情况下FreeRTOS就没有基础时钟,无法产生嘀嗒信号,所以无法正常运行,其代码方面的原因在后面解释。
所以,在使用FreeRTOS时,必须为HAL设置一个非SysTick定时器作为HAL的基础时钟,SysTick将自动作为FreeRTOS的基础时钟。这是FreeRTOS的移植决定的,因为SysTick是Cortex-M内核的一个定时器,在整个STM32系列中都是存在的,使用SysTick作为滴答时钟进行移植显然适用性更强,针对不同系列的STM32处理器移植时需要的改动可以最小化。
在STM32CubeMX中基于STM32F407ZG创建一个项目,使用TIM6作为HAL的基础时钟,启用FreeRTOS后NVIC的自动设置结果如图5所示。定时器TIM6的抢占优先级为0,SysTick和PendSV中断的优先级都设置为15,而且这3个中断都不能被关闭,不能修改优先级。
图5 启用FreeRTOS,并使用TIM6作为HAL的基础时钟后的NVIC设置
在FreeRTOS中,系统嘀嗒信号的频率由参数configTICK_RATE_HZ决定,默认值是1000Hz。SysTick通过定时中断产生嘀嗒信号,SysTick默认定时周期是1ms。
在main()函数中,执行函数osKernelStart()启动内核时对SysTick定时器进行初始化设置。跟踪函数osKernelStart()的代码,发现最终设置SysTick定时器的是文件port.c中的函数xPortStartScheduler()和vPortSetupTimerInterrupt()。函数xPortStartScheduler()中设置SysTick和PendSV中断的中断优先级,函数vPortSetupTimerInterrupt()设置SysTick的定时周期,代码如下:
// 设置systick 定时器,产生需要频率的嘀嗒中断
__attribute__(( weak )) void vPortSetupTimerInterrupt( void )
{
/* 计算用于配置嘀嗒中断的需要的常数 */
#if( configUSE_TICKLESS_IDLE == 1 ) //Tickless低功耗模式,正常情况下为0
{
ulTimerCountsForOneTick = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ );
xMaximumPossibleSuppressedTicks = portMAX_24_BIT_NUMBER / ulTimerCountsForOneTick;
ulStoppedTimerCompensation = portMISSED_COUNTS_FACTOR / ( configCPU_CLOCK_HZ / configSYSTICK_CLOCK_HZ );
}
#endif /* configUSE_TICKLESS_IDLE */
/*停止和清除SysTick的控制寄存器和计数值寄存器 */
portNVIC_SYSTICK_CTRL_REG = 0UL;
portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL;
/* 配置SysTick,使其以设定的频率产生中断*/
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL;
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
}
函数vPortSetupTimerInterrupt()的功能是配置SysTick定时器相关的寄存器,使其以设定的频率产生中断。当参数configUSE_TICKLESS_IDLE的值等于1的时候,也就是使用了Tickless低功耗模式的时候会计算几个常数,这几个常数会在Tickless低功耗模式的时候用于嘀嗒计数值的补偿。
函数中用到的portNVIC_SYSTICK_CTRL_REG、portNVIC_SYSTICK_LOAD_REG等宏是SysTick相关寄存器的移植定义,在文件port.c中的定义时:
#define portNVIC_SYSTICK_CTRL_REG ( * ( ( volatile uint32_t * ) 0xe000e010 ) )
#define portNVIC_SYSTICK_LOAD_REG ( * ( ( volatile uint32_t * ) 0xe000e014 ) )
#define portNVIC_SYSTICK_CURRENT_VALUE_REG ( * ( ( volatile uint32_t * ) 0xe000e018 ) )
查阅Cortex-M4内核技术手册会发现,这3个宏对应的就是SysTick的控制和状态寄存器SYST_CSR、重载值寄存器SYST_RVR和当前值寄存器SYST_CVR。
在使用TIM6作为HAL基础时钟,启用了FreeRTOS的项目中,会发现在文件stm32f4xx_it.c中没有SysTick中断ISR函数SysTick_Handler()的代码框架。而FreeRTOS要使用SysTick的定时中断产生嘀嗒信号,必然是要定义ISR函数的。搜索关键字SysTick_Handler,发现在文件FreeRTOSConfig.h中有如下的宏定义:
/* 将FreeRTOS 移植的中断处理函数映射到CMSIS 的标准ISR函数名 */
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
/* 重要提示: 在使用STM32Cube时,如果HAL的基础时钟被设置为SysTick,下面的定义会被注释,以免覆盖HAL定义的ISR函数SysTick_Handler */
#define xPortSysTickHandler SysTick_Handler
通过这3个宏定义,FreeRTOS将自己移植的3个中断的处理函数与CMSIS的标准ISR函数名关联起来。例如,SysTick中断的标准ISR函数名是SysTick_Handler,FreeRTOS移植的函数名就是xPortSysTickHandler。
源代码中有英文注释特别强调,如果将HAL的基础时钟设置为SysTick,那么第3个宏定义是会被注释掉的,以免覆盖HAL定义的ISR函数SysTick_Handler。但是,这种情况下FreeRTOS就没有基础时钟了,不会产生嘀嗒信号,所以FreeRTOS就无法正常运行了。
FreeRTOS移植的SysTick中断的处理函数xPortSysTickHandler()是在文件port.c中定义的,其功能是调用函数xTaskIncrementTick()使嘀嗒信号计数值递增,并且检查是否需要进行上下文切换。如果需要进行上下文切换,就将PendSV中断挂起,实际的上下文切换是在PendSV的中断处理程序里执行的。函数xTaskIncrementTick()的代码如下。
void xPortSysTickHandler( void )
{
portDISABLE_INTERRUPTS();
{
/* 将RTOS嘀嗒计数值递增,并检查是否需要进行上下文切换 */
if( xTaskIncrementTick() != pdFALSE )
{
/*使PendSV中断挂起,请求上下文切换,上下文切换在PendSV中断里执行 */
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT;
}
}
portENABLE_INTERRUPTS();
}
其中调用的函数xTaskIncrementTick()是在文件task.c中实现的,文件task.c中定义了一个表示嘀嗒信号当前计数值的全局变量xTickCount,函数xTaskIncrementTick()的一个功能就是每次使xTickCount的值加1,在计算延时的时候就会用到全局变量xTickCount。