# 时间片轮询框架 **Repository Path**: coding_everything/timeslice-polling ## Basic Information - **Project Name**: 时间片轮询框架 - **Description**: 这是一个跨平台的ETP(Ecbm-Timeslice-Polling)框架。本框架基于时间片轮询法,任务之间不具备有抢占性,优先级由安装任务的顺序决定。 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 23 - **Created**: 2022-11-26 - **Last Updated**: 2022-11-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ETP简介 ETP是**E**cbm-**T**imeslice-**P**olling的缩写,是ECBM工作室推出的时间片轮询程序框架。具有如下几个特点: - 任务的优先级固定。所有任务按安装时的顺序执行,不可在运行时更改。 - 任务是静态的,不能在运行时创建、删除。但是可以开启和停止任务。 - 代码不和硬件关联,方便移植。 - 任务之间不会抢占CPU,如果某个任务阻塞,那么整体都会卡主。 - 由于任务按顺序执行,但如果某一任务的执行时间太长,会影响到其他任务的正常运行。 时间片轮询是一种基础框架,可以通过合理地分割任务来充分利用CPU。但ETP并不是实时操作系统,在实际应用中也会出现阻塞,请根据实际需求来决定如何应用。 # 如何移植 ETP是硬件无关的框架,但因为是基于时间片的框架,所以移植的目标平台至少要有一个定时器用来产生时间片。 ## 1.确认数据类型 为了实现跨平台,ETP使用了自定义的数据类型,根据实际硬件的情况修改。在etp.h中的宏定义专栏: ```c #define etp_u1 bit //1位无符号变量 #define etp_u8 unsigned char //8位无符号变量 #define etp_u16 unsigned int //16位无符号变量 #define etp_u32 unsigned long //32位无符号变量 ``` ## 2.设置固定周期的中断 时间片的概念和定时器并不挂钩,理论上一串外来的脉冲+外部中断也能实现时间片,但是大多数情况下还是用定时器产生一个固定周期的中断。比如51单片机的定时器中断。 ## 3.加入工程 以keil为例,在Project窗口选择合适地方,比如DEVICE。 ![](https://gitee.com/ecbm/document-image-material/raw/master/ETP%E6%89%8B%E5%86%8C/%E5%8F%8C%E5%87%BB%E5%B7%A5%E7%A8%8B.png) 双击DEVICE,弹出添加文件的窗口,添加etp.c到工程中。 ![](https://gitee.com/ecbm/document-image-material/raw/master/ETP%E6%89%8B%E5%86%8C/%E6%B7%BB%E5%8A%A0etp.c.png) 接下来在main.c中加载头文件: ```c #include "etp.h" ``` 至此,ETP已经完整的移植到工程中了,就差一步就可以开始使用了。 ## 4.添加代码 以ECBM库的工程为例,在定时器0的中断中添加etp_time_tick函数。 ```c void fun1(void)TIMER0_IT_NUM{ etp_time_tick(); } ``` 在main函数里的主循环里添加etp_main函数。 ```c void main(void){ //主函数。 system_init();//ECBM库的初始化。 timer_init(); //定时器初始化。 timer_set_timer_mode(0,1000);//设置定时器0为定时器模式,定时时间为1000uS。 timer_start(0);//打开定时器0。 while(1){ etp_main();//ETP的执行函数。 } } ``` 然后编译,不出错的话说明ETP已经移植成功。根据下面的例程来一起使用ETP吧。 # 如何运行 通过上面的移植之后,ETP框架已经搭建好了。但是只有框架,没有内容的话是没有意义的。下面告诉大家如何来基于ETP框架来安排任务。 ## 函数API ### etp_install 函数原型:void etp_install(etp16 tim,void (* fun)(void)); #### 描述 时间片任务的安装函数。 #### 输入 - tim:该任务的执行周期,单位是“时间片个数”。 - fun:任务函数。 #### 输出 无 #### 返回值 - 0~254:该任务的编号。 - 255:任务列表已满,任务安装失败。 #### 参数设置 ETP框架的特点决定了任务的数量是静态的,因此需要手动设置任务列表的大小。 打开etp.h,在第38行的宏定义处填写一个1~254的值: ```c #define ETP_TASK_MAX 10 ``` 如上所示就是设置任务列表的容量为10,可以安装10个任务。 #### 调用例程 ```c void test(void){//定义一个函数。 P11=!P11;//内容根据实际需要,这里只做测试,用LED闪烁来看效果。 } ...//其他无关代码 void main(void){ ...//其他无关代码 etp_install(100,test);//test函数每100个时间片执行一次。 while(1){ etp_main();//ETP的执行函数。 } } ``` #### 注意事项 1. 安装的函数一定得先定义再用etp_install安装。如果只声明没有定义的话,在执行的时候一定会跑飞。 2. 假如一个时间片是1mS,100个时间片就是100mS。同理一个时间片是10mS,100个时间片就是1S。 3. 本函数仅将任务函数排放进任务列表内,此时任务还不会执行,还需要配合etp_start来启动任务。 4. 先安装的任务的优先级比后安装的任务优先级高。每次轮询都是先执行优先级高的任务。 5. 本函数会返回任务编号,只要不是255就说明任务安装成功。 ### etp_main 函数原型:void etp_main(void); #### 描述 时间片轮询的主函数。 #### 输入 无 #### 输出 无 #### 返回值 无 #### 调用例程 ```c ...//其他无关代码 void main(void){ ...//其他无关代码 while(1){ etp_main();//ETP的执行函数。 } } ``` #### 注意事项 1. 为了方便管理任务,建议主循环只存放etp_main一个函数就行。其他需要执行的工作打包成任务函数来让ETP来执行。 ### etp_time_tick 函数原型:void etp_time_tick(void); #### 描述 任务时间函数。 #### 输入 无 #### 输出 无 #### 返回值 无 #### 调用例程 ```c void fun1(void)TIMER0_IT_NUM{//这里是定时器0的中断函数。 etp_time_tick();//每次中断执行一次。 } ``` #### 注意事项 1. 把本函数放入定时器中断或者其他周期性的中断中。 ### etp_stop 函数原型:void etp_stop(etp8 id); #### 描述 任务停止函数。 #### 输入 - id:需要停止的任务的编号。 #### 输出 无 #### 返回值 无 #### 调用例程 ```c void key_scan(void){//这是按键扫描函数。 if(key_stop==0){//当停止键按下时, etp_stop(0);//停止编号为0的任务。 } } ``` #### 注意事项 1. 任务的编号由etp_install函数在安装该任务的时候生成。可以用一个变量储存起来: ```c u8 led_task_id; ...//其他无关代码 void main(void){ ...//其他无关代码 led_task_id=etp_install(100,test);//test函数每100个时间片执行一次,并把编号给变量led_task_id。 while(1){ ...//其他无关代码 } } ``` ### etp_start 函数原型:void etp_start(etp8 id); #### 描述 任务开启函数。 #### 输入 - id:需要开启的任务的编号。 #### 输出 无 #### 返回值 无 #### 调用例程 ```c u8 led_task_id; ...//其他无关代码 void main(void){ ...//其他无关代码 led_task_id=etp_install(100,test);//test函数每100个时间片执行一次,并把编号给变量led_task_id。 etp_start(led_task_id);//开启test任务。 while(1){ ...//其他无关代码 } } ``` #### 注意事项 1. 由于所有任务在安装的时候都不会自动开启,所以在安装之后需要执行本函数来开启任务。当然也可以在其他时机开启,或者在某种条件触发后开启任务。 ### etp_get_status 函数原型:etp8 etp_get_status(etp8 id); #### 描述 任务状态获取函数,用于获取某个任务的状态,通过状态可知道任务是否正常运行。 #### 输入 - id:需要查询的任务编号。 #### 输出 无 #### 返回值 - 任务的状态。 #### 调用例程 ```c u8 led_task_id; ...//其他无关代码 void main(void){ ...//其他无关代码 led_task_id=etp_install(100,test);//test函数每100个时间片执行一次,并把编号给变量led_task_id。 etp_start(led_task_id);//开启test任务。 while(1){ ...//其他无关代码 etp_main();//主要执行函数。 if(etp_get_status(led_task_id)&ETP_TASK_DELAY){//检查任务的延时标志位,如果任务的执行时间片超过设定的100。该标志位会被置位。 uart_printf(1,"Task Delay!\r\n");//向串口发送任务执行时间过长的警告。 } } } ``` #### 注意事项 无 ### 完整例程代码 ```c #include "ecbm_core.h"//加载库函数的头文件。 #include "etp.h"//加载ETP的头文件。 void tog_p11(void){//P1.1脚翻转函数。 P11=!P11; } void tog_p12(void){//P1.2脚翻转函数。 P12=!P12; } void main(void){ //主函数。 system_init();//ECBM库的初始化。 timer_init(); //定时器初始化。 timer_set_timer_mode(0,1000);//设置定时器0为定时器模式,定时时间为1000uS。 timer_start(0);//打开定时器0。 etp_install(10,tog_p11);//每10mS执行一次tog_p11。 etp_install(20,tog_p12);//每20mS执行一次tog_p12。 etp_start(0);//开启任务tog_p11。 etp_start(1);//开启任务tog_p12。 while(1){ etp_main();//ETP的执行函数。 } } void fun1(void)TIMER0_IT_NUM{//这里是定时器0的中断函数。 P10=1;//这句话只是为了逻辑分析仪看时序的。 etp_time_tick();//每次中断执行一次。 P10=0;//这句话只是为了逻辑分析仪看时序的。 } ``` 运行之后,逻辑分析仪的波形图如下: ![](https://gitee.com/ecbm/document-image-material/raw/master/ETP%E6%89%8B%E5%86%8C/%E6%B3%A2%E5%BD%A2%E5%AF%B9%E6%AF%94.png) 通道0是定时器的中断波形,上升沿意味着进入了定时器中断,下降沿意味着定时器中断结束。 通道1对应着任务tog_p11,所以P1.1脚每次翻转电平的间隔都是10mS。 通道2对应着任务tog_p12,所以P1.2脚每次翻转电平的间隔都是20mS。 # 运行特性 为了更好的理解ETP,我选了几个比较常见的问题进行了测试。如有补充,欢迎加入778916610QQ群提出。 ## 1.任务阻塞 假如有一个任务内部使用了while(1)来阻塞进程的话,那么其他任务将无法再获得运行的机会。因此严禁单个任务阻塞。 ## 2.任务延后 假如一个任务在运行的时候,超出了它的时间片,那么会导致所有任务失去预设的周期。下面用例子进行说明。 ### 正常情况 ```c #include "ecbm_core.h"//加载库函数的头文件。 #include "etp.h"//加载ETP的头文件。 void task1(void){//任务1。 P11=1;//置一代表任务开始。 delay_ms(5);//假设这个任务执行需要5mS。 P11=0;//置零代表任务结束。 } void task2(void){//任务2。 P12=1;//置一代表任务开始。 delay_ms(3);//假设这个任务执行需要3mS。 P12=0;//置零代表任务结束。 } void main(void){ //主函数。 system_init();//ECBM库的初始化。 timer_init(); //定时器初始化。 timer_set_timer_mode(0,1000);//设置定时器0为定时器模式,定时时间为1000uS。 timer_start(0);//打开定时器0。 etp_install(50,task1);//每50mS执行一次任务1。 etp_install(25,task2);//每25mS执行一次任务2。 etp_start(0);//开启任务1。 etp_start(1);//开启任务2。 while(1){ etp_main();//ETP的执行函数。 } } void fun1(void)TIMER0_IT_NUM{//这里是定时器0的中断函数。 P10=1;//这句话只是为了逻辑分析仪看时序的。 etp_time();//每次中断执行一次。 P10=0;//这句话只是为了逻辑分析仪看时序的。 } ``` 假设每个任务的执行时间远小于任务间隔,那么两个任务的执行情况是符合预期的: ![](https://gitee.com/ecbm/document-image-material/raw/master/ETP%E6%89%8B%E5%86%8C/%E4%BB%BB%E5%8A%A1%E6%AD%A3%E5%B8%B8%E6%97%B6%E9%97%B4.png) 由上图可知,任务1每50mS执行一次,任务2每25mS执行一次。同时因为执行任务1需要5mS,所以当任务1和任务2需要同时执行的时候,任务2总会延后5mS(就是在等任务1执行结束)才能执行,这就是因为在ETP框架中任务不能抢占的结果。也正是因为任务执行需要时间,所以一部分间隔不是严格的50mS和25mS。 ### 任务执行时间超过间隔时间 现在假定这种情况,任务1实际的执行时间是60mS,然而任务1的间隔时间却是50mS。其他条件不变。 ```c #include "ecbm_core.h"//加载库函数的头文件。 #include "etp.h"//加载ETP的头文件。 void task1(void){//任务1。 P11=1;//置一代表任务开始。 delay_ms(60);//假设这个任务执行需要60mS。 P11=0;//置零代表任务结束。 } void task2(void){//任务2。 P12=1;//置一代表任务开始。 delay_ms(3);//假设这个任务执行需要3mS。 P12=0;//置零代表任务结束。 } void main(void){ //主函数。 system_init();//ECBM库的初始化。 timer_init(); //定时器初始化。 timer_set_timer_mode(0,1000);//设置定时器0为定时器模式,定时时间为1000uS。 timer_start(0);//打开定时器0。 etp_install(50,task1);//每50mS执行一次任务1。 etp_install(25,task2);//每25mS执行一次任务2。 etp_start(0);//开启任务1。 etp_start(1);//开启任务2。 while(1){ etp_main();//ETP的执行函数。 } } void fun1(void)TIMER0_IT_NUM{//这里是定时器0的中断函数。 P10=1;//这句话只是为了逻辑分析仪看时序的。 etp_time_tick();//每次中断执行一次。 P10=0;//这句话只是为了逻辑分析仪看时序的。 } ``` 那么将会出现如下情况: ![](https://gitee.com/ecbm/document-image-material/raw/master/ETP%E6%89%8B%E5%86%8C/%E4%BB%BB%E5%8A%A11%E6%8B%96%E5%BB%B6.png) 可以看到,因为任务1执行完消耗60mS,任务2为了等待任务1结束也必须等待60mS才能执行。然后因为任务1的执行标志位每50mS就置一,也就意味着任务1还没执行完就获得了下一次轮询的执行允许。于是两个任务的执行关系就完全脱离了设置的50mS和25mS了。 出现了以上的现象的时候,受影响的任务都会被标记上“延时”标志位。可以通过读取任务状态得知当前任务有没有延时。 ## 3.任务执行卡点 代码就不列举了。任务1和任务2的执行间隔是一致的,但是执行的时间不一致。 ![](https://gitee.com/ecbm/document-image-material/raw/master/ETP%E6%89%8B%E5%86%8C/%E4%BB%BB%E5%8A%A1%E5%8D%A1%E7%82%B91.png) 按理来说,任务1和任务2的执行时间都小于其间隔,那么他们都应该是按优先级来执行的,且顺序不会变。但是实际情况确实任务1有时先执行,有时候后执行。这是因为在etp_time里置一任务1的运行状态时,etp_main恰好已经过了判断任务1的时机,于是本来应该先执行的任务1只能等到下一次轮询才能执行。 **因此,如果两个任务要求连续执行且执行先后顺序固定的话,还是要把两个任务合并成一个才行。** # 占用空间 | 内容 | 消耗空间 | | :-------------: | :-----------------: | | 任务消耗 | 每个任务增加 8 Byte | | 框架消耗 | 3.1 Byte | | etp_install函数 | 99 Byte | | etp_main函数 | 118 Byte | |etp_time_tick函数| 216 Byte | | etp_stop函数 | 17 Byte | | etp_start函数 | 24 Byte | |etp_get_status函数| 26 Byte | 函数的占用空间和KEIL的优化等级和编译器版本有关,上表仅做参考。