UCOSIII 时间片轮转调度
在UCOS中,任务调度可以理解为任务切换,当心跳时钟到来时,系统就会把当前任务的现场保存起来,放到本任务的堆栈中,这个步骤和进中断前的push压栈很相似,不过现场状态是保存在本任务的堆栈中而不是中断堆栈中。然后CPU去执行高优先级的就绪任务。比如有两个任务,任务A和任务B。B的优先级比A的高,现在正在运行任务A,而任务B在等待延时结束。心跳时钟来了,系统会更新各个延时时间参数,更新完延时参数后发现B的延时到了,就把B进入就绪太。因为任务B的优先级比任务A的优先级高,所以系统会切换任务,这个切换在心跳时钟到来了后在心跳时钟中断里切换。切换任务的大概过程还是挺简单的,首先系统会把任务A的现场保存到任务A的堆栈中,也就是保存各个CPU寄存器的值,然后从任务B的堆栈中把数据拷贝到CPU寄存器中,切换到任务B执行。任务B堆栈中的数据从哪里来?其实在任务B调用延时函数进入挂起态的时候,系统已经把任务B的现场(也就是各个CPU寄存器的值)拷贝到任务B的堆栈中了。这也是为什么每个任务都需要有自己专用堆栈的原因。
系统会给每个任务运行一定的时间,时间的长度由心跳频率决定,比如心跳时钟为200hz,那么就是5ms来一个心跳时钟,然后系统在心跳时钟到来时切换任务,所以很明显,一个任务运行的时间就是5ms,也是一个任务运行一次可用的时间。一个时间片(5ms)用完后,如果没有更高优先级 的任务出现,那本任务还会在获得一个(5ms)的时间片,一直到更高优先级的任务出现为止。
任务切换过程中的保存现场和恢复现场肯定会占用CPU的时间,一个心跳时钟切换一次任务,所以任务切换不要太频繁,不然系统整天忙于切换任务,哪有多少时间去处理任务中的事情。但是也不能太慢,如果太慢的话系统的实时性就不高了。比如心跳时钟是50hz的话,那么20ms才会切换一次任务,延时函数的误差最大为20ms,如果是时间要求比较严格的程序的话,那还怎么玩。所以。感觉系统切换有点像动态扫描数码管吧,扫描太快的时候虽然数码管看起来舒服但是会占用CPU太多资源,而扫描太慢又会导致数码管闪烁,这个例子并不恰当,也就将就着看吧。在原子的栗子中,用的是72Mhz的CPU,系统心跳为200hz,也就是5ms切换一次任务,系统切换任务占用多少CPU时间我也不知道,反正72Mhz下系统心跳频率为200hz是没问题的。
任务调度分为任务级和中断级,上面所说的是任务级的任务调度,也就是除了心跳中断外没有其他的中断到来的情况下的任务切换,在心跳时钟到来时由系统正常的切换任务。还有一种是中断级的任务调度。
可剥夺型内核,当一个优先级高的任务就绪时,就算心跳时钟没到来,那么高优先级任务可以直接从低优先级任务手中把CPU使用权抢过来,而不需要管低优先级任务时间片是否已经用完。还是举个栗子吧,比如有两个任务,任务A和任务B。B的优先级比A的高,现在正在运行任务A,而任务B正在等待一个外部中断,所以现在任务B不是就绪太。突然间任务B所等待的外部中断来了,那么任务B就会进入就绪太,而现在心跳时钟还没来,任务A的时间片也没用完,那么问题来了,任务A是运行态,任务B是就绪太,而任务B的优先级比任务A要高,系统该运行任务A呢还是该运行任务B呢,当然是运行任务B呀,所以系统会在退出中断时切换到任务B运行,而任务A的时间片虽然没用完,但是也只能眼巴巴地看着任务B把自己的时间片抢走,任务B剥夺了任务A的CPU使用权,这就是可剥夺型内核。这样做的好处就是系统实时响应比较好,如果等到心跳时钟来的时候才切换到任务B运行的话,估计黄花菜都凉了。
时间片轮转调度,据说这是UCOSIII新增的功能,UCOSII是没有的,本人并没有用过UCOSII,也就不纠结UCOSII的功能了。要使用时间片轮转调度得打开宏OS_CFG_SCHED_ROUND_ROBIN_EN。
时间片轮转调度,就是允许不同的任务拥有相同的优先级,既然优先级一样,那相同优先级的任务怎么运行,只能是把系统分配给本优先级的时间轮流用了。老规矩举栗子,有三个任务,A、B和C,A的优先级是5,B和C的优先级都是6,任务B的时间片是,3,任务C的时间片是1,系统心跳时钟是200hz,时间片长度为1个系统时钟节拍,也就是1*5=5ms。系统并不会因为优先级6上面的任务多而会给这个优先级更长的时间片,在没有中断级任务调度的情况下,系统会轮流运行第5和第6优先级的任务,也就是每个优先级的任务各运行5ms。比如在40ms的时间内,任务会切换8次,在8次的任务切换当中,优先级5的任务运行4次,因为优先级5只有一个任务,所以任务A运行了4次,一共运行20ms。而优先级6的任务运行就比较奇葩了,优先级6的任务也有4个时间片,假设在优先级6里先运行任务B吧,因为任务B占用了3个时间片,所以任务C就只有一个时间片了。也就是说在40ms时间内,任务A一共运行了4次,而任务B和任务C只是各运行了一次而已。所以我们在使用时间片轮转调度(不同任务使用相同优先级)的时候,每个任务的时间片尽量不要太多,比如上面的栗子中,如果把任务B的时间片分配为20,那么任务C要等任务B运行完了之后才轮到自己运行,那任务C运行的频率会非常低。UCOS作者给这个问题留了个解决方案,就是一个任务可以放弃自己剩余的时间片,把时间送给本优先级下其他的任务,注意是给同优先级的其他任务,而不是给其他优先级的任务,比如上面栗子的任务B拥有3个时间片,在任务B使用的第一个时间片的时候,时间片本来有5个ms,但是B只运行2ms就不想运行了,那么B可以放弃本时间片剩余的3个ms,包括剩余的未使用的2个时间片也放弃了。在B放弃剩余时间的时候会切换到任务C而不是任务A运行,因为任务C和任务B同优先级,而A不是。当然,当下一个心跳时钟到来时还是会切换到任务A运行滴。这个放弃时间片是手动放弃时间片切换任务的,而不是系统正常的任务切换。
反正我是不喜欢使用时间片轮转调度这个功能,如果系统不是非常庞大的话,一个任务分配一个优先级是没问题的,而且不使用时间片轮转调度的话还可以减少代码量,节省芯片空间。
任务切换相关函数
- OSSched (void)
- OSSchedRoundRobinCfg()
- OSSchedRoundRobinYield()
各个函数用法及参数说明
- OSSched ()
任务切换函数,如果手动调用这个函数的话会直接引起任务切换,其实很多函数都会调用这个函数从而进行任务切换,比如系统延时函数、任务挂起函数等。
函数原型 | void OSSched (void) | |
使能开关 | 永远有效 | |
入口参数 | 无 |
|
注意事项 | 如果调度器已经上锁的话,这个函数是不起作用的 |
- OSSchedRoundRobinCfg()
时间片轮转调度初始化配置
函数原型 | void OSSchedRoundRobinCfg (CPU_BOOLEAN en, OS_TICK dflt_time_quanta, OS_ERR *p_err) | |
使能开关 | OS_CFG_SCHED_ROUND_ROBIN_EN | |
入口参数 | CPU_BOOLEAN en, | 其实就是时间片轮转调度的使能 DEF_ENABLED 使能 DEF_DISABLED 不使能 |
| OS_TICK dflt_time_quanta, | 默认的时间片长度,如果系统创建任务时初值为0,那么将会用这个值代替。如果这个值也被设置为0,则系统会用系统节拍的1/10作为时间轮转片节拍。比如系统心跳是1000hz,而dflt_time_quanta=0,那么时间片的频率就是200hz,每个时间片就是5ms |
| OS_ERR *p_err | 返回的错误码 |
注意事项 | 时间轮转片的频率和系统心跳频率是两回事,可以相同也可以不同 |
- OSSchedRoundRobinYield()
该任务放弃剩余时间片
函数原型 | void OSSchedRoundRobinYield (OS_ERR *p_err) | |
使能开关 | OS_CFG_SCHED_ROUND_ROBIN_EN | |
入口参数 | OS_ERR *p_err | 返回的错误码 |
注意事项 | 这个函数是要手动调用的,只有任务中才能调用本函数 |
写了个代码,功能很简单,还是新建三个任务,第一个任务用来创建第二和第三个任务,然后第一个任务就删除自己,系统里剩下任务2和任务3在运行。任务2 是LED闪烁任务,优先级是5;任务3是串口输出任务,优先级也是5,然后让这两个优先级相同的任务一起运行,这个工程只是让两个优先级相同的任务同时运行而已,并没有使用放弃时间片等功能。
代码如下
Main.c文件
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "includes.h"
#include "task.h"
/************************************************
ALIENTEK战舰STM32开发板UCOS实验
技术支持:www.openedv.com
淘宝店铺:http://eboard.taobao.com
关注微信公众平台微信号:"正点原子",免费获取STM32资料。
广州市星翼电子科技有限公司
作者:正点原子 @ALIENTEK
************************************************/
//UCOSIII中以下优先级用户程序不能使用,ALIENTEK
//将这些优先级分配给了UCOSIII的5个系统内部任务
//优先级0:中断服务服务管理任务 OS_IntQTask()
//优先级1:时钟节拍任务 OS_TickTask()
//优先级2:定时任务 OS_TmrTask()
//优先级OS_CFG_PRIO_MAX-2:统计任务 OS_StatTask()
//优先级OS_CFG_PRIO_MAX-1:空闲任务 OS_IdleTask()
//技术支持:www.openedv.com
//淘宝店铺:http://eboard.taobao.com
//广州市星翼电子科技有限公司
//作者:正点原子 @ALIENTEK
//*****************************************************************
//开始任务
OS_TCB starTaskTCB; //任务控制块
#define STAR_TASK_PRIO 3 //任务优先级
#define STAR_TASK_STK_SIZE 128 //任务堆栈总大小
CPU_STK STAR_TASK_STK[ STAR_TASK_STK_SIZE ]; //任务堆栈数组
int main()
{
OS_ERR myErr; //os的错误码
CPU_SR_ALLOC(); //使用临街保护就得添加这玩意,不然会报错
delay_init(); //时钟初始化
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组配置
uart_init(115200); //串口初始化
LED_Init(); //LED初始化
OSInit(&myErr); //os初始化
//创建任务的时候进入临街保护比较好,但是这个函数非用户的API函数,用户使用是否合适以后再讨论
OS_CRITICAL_ENTER();
//创建开始任务,在开始任务里创建其他任务,为每个函数入口参数都强制转换类型吧,以防万一
OSTaskCreate ( (OS_TCB *)&starTaskTCB, //任务控制块,
(CPU_CHAR *)"star task", //任务名字
(OS_TASK_PTR )starTaskFunc, //任务函数
(void *)0, //任务函数入口参数
(OS_PRIO )STAR_TASK_PRIO, //任务优先级
(CPU_STK *)&STAR_TASK_STK[0], //堆栈数组基地址
(CPU_STK_SIZE ) STAR_TASK_STK_SIZE/10, //堆栈溢出限位
(CPU_STK_SIZE ) STAR_TASK_STK_SIZE, //堆栈大小
(OS_MSG_QTY ) 0, //本任务消息长度
(OS_TICK ) 0, //时间片个数
(void *) 0, //任务块补充参数
(OS_OPT ) OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR, //选项
(OS_ERR *)&myErr); //错误码
OS_CRITICAL_EXIT(); //任务创建完了就该退出临界保护了
OSStart(&myErr); //任务创建完成后就该运行了
while(1); //运行系统后就一直在系统里面跑了,应该运行不到这里
}
Task.c文件
#include "task.h"
#include "sys.h"
#include "led.h"
#include "includes.h" //想要使用UCOS系统,得包含这个头文件
//LED秒闪任务,提示系统正在运行
OS_TCB ledTaskTCB;
#define LED_TASK_PRIO 5
#define LED_TASK_STK_SIZE 128
CPU_STK LED_TASK_STK[LED_TASK_STK_SIZE];
//串口输出任务1
OS_TCB uartTask_1_TCB;
#define UART_TASK_1_PRIO 5
#define UART_TASK_1_STK_SIZE 128
CPU_STK UART_TASK_1_STK[ UART_TASK_1_STK_SIZE ];
void starTaskFunc()
{
OS_ERR myErr;
//时间片轮转初始化,时间片长度为一个心跳节拍,1*5ms=5ms
OSSchedRoundRobinCfg(DEF_ENABLED,1,&myErr);
OSTaskCreate ( (OS_TCB *)&ledTaskTCB,
(CPU_CHAR *)"led task",
(OS_TASK_PTR )ledTaskFunc,
(void *)0,
(OS_PRIO )LED_TASK_PRIO,
(CPU_STK *)&LED_TASK_STK[0],
(CPU_STK_SIZE ) LED_TASK_STK_SIZE/10,
(CPU_STK_SIZE ) LED_TASK_STK_SIZE,
(OS_MSG_QTY ) 0,
(OS_TICK ) 0,
(void *) 0,
(OS_OPT ) OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR *)&myErr);
OSTaskCreate ( (OS_TCB *)&uartTask_1_TCB,
(CPU_CHAR *)"uart task 1 ",
(OS_TASK_PTR )uartTask_1_Func,
(void *)0,
(OS_PRIO )UART_TASK_1_PRIO,
(CPU_STK *)&UART_TASK_1_STK[0],
(CPU_STK_SIZE ) UART_TASK_1_STK_SIZE/10,
(CPU_STK_SIZE ) UART_TASK_1_STK_SIZE,
(OS_MSG_QTY ) 0,
(OS_TICK ) 0,
(void *) 0,
(OS_OPT ) OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
(OS_ERR *)&myErr);
//这个任务只是用来创建其他任务的,其他任务创建完成了这个任务也就没用了,删除掉
OSTaskDel((OS_TCB*)0,&myErr);
}
//LED任务的任务函数
//LED每隔500ms翻转一次,提示系统正在运行
void ledTaskFunc()
{
OS_ERR myErr;
while(1) //注意,每个任务都是一个死循环
{
OSTimeDlyHMSM ( (CPU_INT16U ) 0, //时
(CPU_INT16U ) 0, //分
(CPU_INT16U ) 0, //秒
(CPU_INT32U ) 100, //毫秒
(OS_OPT ) OS_OPT_TIME_HMSM_STRICT, //选项
(OS_ERR *)&myErr); //错误码
LED = !LED; //LED状态翻转
}
}
//串口任务1的任务函数
//功能;每隔1秒向电脑串口助手发送一次数据
void uartTask_1_Func()
{
OS_ERR myErr;
static u16 uartTaskCount=0;
while(1)
{
uartTaskCount++;
if(uartTaskCount > 65534)
{
uartTaskCount=0;
}
printf("uartTask 运行次数 :%d \r\n",uartTaskCount);
OSTimeDlyHMSM(0,0,1,0,OS_OPT_TIME_HMSM_STRICT,&myErr);//延时1秒
}
}
Task.h文件
#ifndef __TASK_H
#define __TASK_H
void starTaskFunc();
void ledTaskFunc();
void uartTask_1_Func();
#endif