系列文章目录
FreeRTOS实时操作系统(一)RTOS的基本概念
FreeRTOS实时操作系统(二)任务创建与任务删除(HAL库)
FreeRTOS实时操作系统(三)任务挂起与恢复
FreeRTOS实时操作系统(四)中断任务管理
FreeRTOS实时操作系统(五)进入临界区、任务调度器挂起与恢复
FreeRTOS实时操作系统(六)列表与列表项
文章目录
- 系列文章目录
- 前言
- 时间片调度概念
- 滴答定时器
- 未带RTOS的HAL库
- 带RTOS的HAL库
- 时间片调度实验
- 改变时间片大小
前言
在学习正点原子的时间片调度的教程中,突然要改变滴答定时器的中断频率,而我之前对这方面没有一点点了解,所以需要详细补充一下这个知识点。
时间片调度概念
在第一片文章里面提到过时间片调度的概念,这里重复一下:
同等优先级任务轮流地享有相同的 CPU 时间(可设置), 叫时间片,在FreeRTOS中,一个时间片就等于SysTick 中断周期。
首先每个任务只执行一个时间片,不管任务里面的while循环运行了几次,时间到达之后就要切换到下一个任务。
时间片的大小取决于滴答定时器的中断频率。
像Task3这样被阻塞的任务(系统延时或等待信号等),将直接切换到下一个任务,时间片没执行到也不管,剩下的时间片直接丢掉了。
滴答定时器
像上面的就涉及到了滴答定时器的中断设置,影响了时间片的长度,之前学习HAL库或者蓝桥杯等等,最多是对1ms的滴答定时器利用,从没想过更改,如下图:
滴答定时器有四个寄存器:
CTRL | SysTick控制及状态寄存器 |
---|---|
LOAD | SysTick重装载数值寄存器 |
VAL | SysTick当前数值寄存器 |
CALIB | SysTick校准数值寄存器 |
未带RTOS的HAL库
参考手册中说:RCC通过AHB时钟(HCLK)8分频后作为Cortex系统定时器(SysTick)的外部时钟。通过对SysTick控制与状态寄存器的设置,可选择上述时钟或Cortex(HCLK)时钟作为SysTick时钟。
也就是是说可以选择AHB时钟8分频或HCLK(内核)时钟。
网上介绍说:HAL库是无法配置这个1分频和8分频,生成的都是一样的,由下图所示,默认选择的是HCLK时钟。
至于解决方案:stm32cubemx配置systick滴答定时器有bug
我这里虽然灰了,但是我看了看条件,确实定义了__Vendor_SysTickConfig,同时它的值也是0,这里可能是编译器有问题吧。
带RTOS的HAL库
在生成的RTOS的HAL库代码中,与标准库有一些区别,在这里重新定义了这个函数。
HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
RCC_ClkInitTypeDef clkconfig;
uint32_t uwTimclock = 0;
uint32_t uwPrescalerValue = 0;
uint32_t pFLatency;
/*Configure the TIM1 IRQ priority */
HAL_NVIC_SetPriority(TIM1_UP_IRQn, TickPriority ,0);
/* Enable the TIM1 global Interrupt */
HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);
/* Enable TIM1 clock */
__HAL_RCC_TIM1_CLK_ENABLE();
/* Get clock configuration */
HAL_RCC_GetClockConfig(&clkconfig, &pFLatency);
/* Compute TIM1 clock */
uwTimclock = HAL_RCC_GetPCLK2Freq();
/* Compute the prescaler value to have TIM1 counter clock equal to 1MHz */
uwPrescalerValue = (uint32_t) ((uwTimclock / 1000000U) - 1U);
/* Initialize TIM1 */
htim1.Instance = TIM1;
/* Initialize TIMx peripheral as follow:
+ Period = [(TIM1CLK/1000) - 1]. to have a (1/1000) s time base.
+ Prescaler = (uwTimclock/1000000 - 1) to have a 1MHz counter clock.
+ ClockDivision = 0
+ Counter direction = Up
*/
htim1.Init.Period = (1000000U / 1000U) - 1U;
htim1.Init.Prescaler = uwPrescalerValue;
htim1.Init.ClockDivision = 0;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
if(HAL_TIM_Base_Init(&htim1) == HAL_OK)
{
/* Start the TIM time Base generation in interrupt mode */
return HAL_TIM_Base_Start_IT(&htim1);
}
/* Return function status */
return HAL_ERROR;
}
这是因为在HAL库中,我把TIM1定时器作为基础时钟源,TIM1就不能在作为其他用途。在STM32CubeMX中不能再对TIM1做任何设置。在NVIC中,TIM1的中断被自动启用,可以修改TIM1的中断优先级,但是不能关闭TIM1的中断。同时,SysTick定时器的中断也还是被自动启用的,且不能关闭,
所以在这里,我用TIM1替代了滴答定时器,完全代替了SysTick的功能。
原因是:在使用FreeRTOS时,必须为HAL设置一个非SysTick定时器作为HAL的基础时钟,SysTick将自动作为FreeRTOS的基础时钟。也是就强制滴答定时器作为FreeRTOS的心跳,关系到任务调度和时间片,但是我们之前了解过HAL_Delay,原来其内部实现靠的是滴答定时器,现在自然变成你设置的其他定时器提供了。为啥HAL和FreeRTOS不都使用一个SysTick,是因为内部会发生冲突
参考:FreeRTOS的基础时钟
参考:FreeRTOS的Systick和HAL时基
SysTick定时器:
文件port.c中的函数xPortStartScheduler()和vPortSetupTimerInterrupt()。函数xPortStartScheduler()中设置SysTick和PendSV中断的中断优先级,函数vPortSetupTimerInterrupt()设置SysTick的定时周期,
portNVIC_SYSTICK_CTRL_REG、portNVIC_SYSTICK_LOAD_REG等宏就是相关寄存器。更改这些能改变滴答定时器的时间。
综合上面这些程序代码,就可以分析出来,改变上图的宏定义就可以实现对时间片大小的调整。
时间片调度实验
设计目标:将设计三个任务:start_task、task1、task2,其中task1和task2优先级相同均为2。为了使现象明显,将滴答定时器的中断频率设置为50ms中断一次,即一个时间片50ms。
将宏configUSE_TIME_SLICING 和 configUSE_PREEMPTION 置1。
抢占式调度器宏:
时间片调度宏:
改变时间片大小
/* Configure SysTick to interrupt at the requested rate. */
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 );
现在详细介绍一下这个portNVIC_SYSTICK_LOAD_REG :
configSYSTICK_CLOCK_HZ :
一步一步追踪:configSYSTICK_CLOCK_HZ=16M;
configTICK_RATE_HZ=1000;
所以计算可得:portNVIC_SYSTICK_LOAD_REG=16k-1;
在16M的时钟下,每16k次计数就中断一次,所以滴答定时器的中断时间就是1ms。
所以一般我们改变这个configTICK_RATE_HZ值就可以改变滴答定时器的时间。
要改成50ms的滴答定时器,就把这个configTICK_RATE_HZ值改为20即可。
之后就可以进行时间片调度的测试:
TaskHandle_t task1_handler;
#define TASK1_PRIO 2
#define TASK1_STACK_SIZE 128
TaskHandle_t task1_handler;
void task1( void * pvParameters );
/* TASK2 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小 创建任务
*/
#define TASK2_PRIO 2
#define TASK2_STACK_SIZE 128
TaskHandle_t task2_handler;
void task2( void * pvParameters );
void vTaskCode( void * pvParameters )
{
taskENTER_CRITICAL(); /* 进入临界区 */
xTaskCreate((TaskFunction_t ) task1,
(char * ) "task1",
(configSTACK_DEPTH_TYPE ) TASK1_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK1_PRIO,
(TaskHandle_t * ) &task1_handler );
xTaskCreate((TaskFunction_t ) task2,
(char * ) "task2",
(configSTACK_DEPTH_TYPE ) TASK2_STACK_SIZE,
(void * ) NULL,
(UBaseType_t ) TASK2_PRIO,
(TaskHandle_t * ) &task2_handler );
vTaskDelete(NULL);
taskEXIT_CRITICAL(); /* 退出临界区 */
}
// Function that creates a task.
void vOtherFunction( void )
{
xTaskCreate( vTaskCode, "tak1", 128, NULL, 1, &task1_handler );
vTaskStartScheduler();
}
void task1( void * pvParameters )
{
uint32_t task1_num = 0;
while(1)
{
taskENTER_CRITICAL(); /* 进入临界区 */
printf("task1运行次数:%d\r\n",++task1_num);
taskEXIT_CRITICAL(); /* 退出临界区 */
HAL_Delay(10);
}
}
void task2( void * pvParameters )
{
uint32_t task2_num = 0;
while(1)
{
taskENTER_CRITICAL(); /* 进入临界区 */
printf("task2运行次数:%d\r\n",++task2_num);
taskEXIT_CRITICAL(); /* 退出临界区 */
HAL_Delay(10);
}
}
设置两个串口发送任务的优先级一样,之后就可以测试结果:
可以看到最后在50ms的时间片内,每个任务的while循环大概执行5次后进行了任务的切换。
在上面代码中,我们在串口发送函数上下加入了临界区代码保护,主要是防止数据发送了一半,切换了任务,数据没发完。
如果上面代码的两个任务优先级不同,就会出现一直执行优先级高的任务的现象,因为它不会出现阻塞了,就一直是就绪态。
这里能使用HAL_Delay()函数,但不能使用vTaskDelay(),都相当于阻塞任务,会出现时间片还没用完就进行了任务切换,而且因为滴答定时器的时间变化了,vTaskDelay的单位就不再是1ms了,是50ms单位了