一.任务切换
RTOS 系统的核心是任务管理,而任务管理的核心是任务切换,任务切换决定了任务的执行顺序,任务切换效率的高低也决定了一款系统的性能,尤其是对于实时操作系统。
在具有嵌入式 OS 的典型系统中,处理时间被划分为了多个时间片。若系统中只有两个任务,这两个任务会交替执行。
上下文切换的两个场合:
1.执行系统调用
执行系统调用就是执行 FreeRTOS 系统提供的相关 API 函数。
1.如任务切换函数 taskYIELD()
2.中断级的任务切换函数为 portYIELD_FROM_ISR()
2.系统滴答定时器(SysTick)中断
void SysTick_Handler(void)
{
if(xTaskGetSchedulerState()!=taskSCHEDULER_NOT_STARTED)//系统已经运行
{
xPortSysTickHandler();
}
}
/*在滴答定时器中断服务函数中调用了 FreeRTOS 的 API 函数 xPortSysTickHandler(),此函数
源码如下:*/
void xPortSysTickHandler( void )
{
vPortRaiseBASEPRI(); (1)
{
if( xTaskIncrementTick() != pdFALSE ) //增加时钟计数器 xTickCount 的值
{
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (2)
}
}
vPortClearBASEPRIFromISR(); (3)
}
通过向中断控制和壮态寄存器 ICSR 的 bit28 写入 1 挂起 PendSV 来启动 PendSV 中断。这样就可以在 PendSV 中断服务函数中进行任务切换。
在 PendSV 中断服务程序中有调用函数vTaskSwitchContext()来获取下一个要运行的任务,
也就是查找已经就绪了的优先级最高的任务,如果优先级相同,则进行时间片调度,即在 FreeRTOS 中允许一个任务运行一个时间片(一个时钟节拍的长度)后让出 CPU 的使用权,让拥有同优先级的下一个任务运行。
要使用时间片调度的话宏 configUSE_PREEMPTION 和宏 configUSE_TIME_SLICING 必须为 1。时间片的长度由宏 configTICK_RATE_HZ 来确定,一个时间片的长度就是滴答定时器的中断周期。
我们打开CUBEMX看,
将频率设置成20,那每个任务运行50ms,再自己创建两个任务之后,生成代码。
void StartTask02(void const * argument)
{
/* USER CODE BEGIN StartTask02 */
uint8_t task2_num;
/* Infinite loop */
for(;;)
{
task2_num++; //任务 1 执行次数加 1 注意 task1_num1 加到 255 的时候会清零!!
//taskENTER_CRITICAL(); //进入临界区
printf("任务 2 已经执行:%d 次\r\n",task2_num);
//taskEXIT_CRITICAL(); //退出临界区
//延时 10ms,模拟任务运行 10ms,此函数不会引起任务调度
HAL_Delay(10);///延时10ms
//osDelay(1);///任务阻塞后,RTOS系统调用其它处于就绪状态的优先级最高的任务来执行
}
/* USER CODE END StartTask02 */
}
/* USER CODE BEGIN Header_StartTask03 */
/**
* @brief Function implementing the myTask03 thread.
* @param argument: Not used
* @retval None
*/
/* USER CODE END Header_StartTask03 */
void StartTask03(void const * argument)
{
/* USER CODE BEGIN StartTask03 */
uint8_t task3_num;
/* Infinite loop */
for(;;)
{
task3_num++; //任务 1 执行次数加 1 注意 task1_num1 加到 255 的时候会清零!!
//taskENTER_CRITICAL(); //进入临界区
printf("任务 3 已经执行:%d 次\r\n",task3_num);
//taskEXIT_CRITICAL(); //退出临界区
//延时 10ms,模拟任务运行 10ms,此函数不会引起任务调度
HAL_Delay(10);///延时10ms
//osDelay(1);
}
/* USER CODE END StartTask03 */
}
50ms的时间片内,大概每个执行四次,然后切换到下一个优先级相同的任务。
二.中断管理和临界段
中断是微控制器一个很常见的特性,中断由硬件产生,当中断产生以后 CPU 就会中断当前的流程转而去处理中断服务,Cortex-M 内核的 MCU 提供了一个用于中断管理的嵌套向量中断控制器(NVIC)。
Cotex-M3 的 NVIC 最多支持 240 个 IRQ(中断请求)、1 个不可屏蔽中断(NMI)、1 个 Systick(滴答定时器)定时器中断和多个系统异常。
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
这个宏用来设置freertos管理中断最大优先级,在cubemx此处设置
STM32 有 16 个优先级,0 为最高优先级,15 为最低优先级,宏configMAX_SYSCALL_INTERRUPT_PRIORITY设置为5之后,代表freertos只能管比5优先级低的优先级,即6,7,8,9…(数字越小,优先级越高),而比5优先级高的,0,1,2,3,4不受freertos管制。
低于此优先级(6,7,8,9…)的中断可以安全的调用 FreeRTOS 的 API 函数,高于此优先级(0,1,2,3,4)的中断 FreeRTOS 是不能调用的,中断服务函数也不能调用 FreeRTOS 的 API 函数。
在cubemx中可见
如图配置好两个定时器,在配置一个任务
void StartTask03(void const * argument)
{
/* USER CODE BEGIN StartTask03 */
uint8_t task3_num;
/* Infinite loop */
for(;;)
{
task3_num++; //任务 1 执行次数加 1 注意
if(task3_num==5)
{
printf("关闭中断.............\r\n");
portDISABLE_INTERRUPTS(); //关闭中断
delay_ms(1000);
printf("打开中断.............\r\n"); //打开中断
portENABLE_INTERRUPTS();
}
osDelay(1000);
}
/* USER CODE END StartTask03 */
}
任务级临界段代码保护
taskENTER_CRITICAL()和 taskEXIT_CRITICAL()是任务级的临界代码保护,一个是进入临
界段,一个是退出临界段,这两个函数是成对使用的。
在进入函数 vPortEnterCritical()以后会首先关闭中断,然后给变量 uxCriticalNesting
加一,uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数 vPortExitCritical()是退出临界段调用的,函数每次将 uxCriticalNesting 减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断。
==临界区代码一定要精简!==因为进入临界区会关闭中断,这样会导致优先级低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断得不到及时的响应!
函数 taskENTER_CRITICAL_FROM_ISR()和 taskEXIT_CRITICAL_FROM_ISR()中断级别
临界段代码保护,是用在中断服务程序中的,而且这个中断的优先级一定要低于
configMAX_SYSCALL_INTERRUPT_PRIORITY
taskENTER_CRITICAL(); //进入临界
total_num+=0.01f;
printf("total_num 的值为: %.4f\r\n",total_num);
taskEXIT_CRITICAL(); //退出临界
vTaskDelay(1000);
欢迎指正