分享一种嵌入式系统的设计思路,保证实时性和可抢占的任务调度模式,可用于代替RTOS系统,适用于各种32位单片机,方便搭建或移植,可读性强,易拓展和维护,占用更少的资源。
核心目标:
1、减少甚至取消硬延迟,以判断状态和倒计时的方式开启任务作为前台;
2、减少中断,尽量只保留串口中断和一个定时器中断作为后台。
基础思路:
举例,按功能需求,前台需要运行TaskA、TaskB、TaskC三个任务,其中C为优先级更高的任务,在A或B运行时,C可以抢占运行,C运行结束后继续运行A或B。(前提:C任务运行用时要短,否则需要对C进行优化,避免影响实时性)
- 创建任务状态变量,用于跟进每个任务的状态信息,其中至少需要包括:
uint8_t flag;//任务状态,开启还是关闭
uint32_t delay;//任务运行倒计时,用于暂时阻塞任务,这段时间可以运行其他任务
uint8_t step;// 减少甚至取消硬延迟的情况下,对单个任务需要分段,用switch语句的方式只运行任务的一小部分,中间的过度使用delay进行阻塞,step指示任务当前处于哪一段或将要跳转到哪一段
- 在main的while循环内处理前台:
依次判断A、B任务的flag和delay,确定是否运行该任务;
在A、B任务内,为flag、delay、step赋值,进而实现任务的开关,阻塞和跳转的功能;
- 在定时器中断(以滴答定时器或创建的1ms定时器为例)内处理后台
判断每个任务的delay或另外声明的delay变量是否大于零,是则--,确保每1毫秒声明的阻塞用delay大于零时都可以减1,实现倒计时;
判断C任务的flag和delay,确定是否运行该任务,此时A或B未运行完也可以先运行C任务;
- 原先用于其他周期性触发的各种定时器任务,可以新增倒计时变量和任务delay一样在中断内减1,并在前台任务内,判断该变量和重新赋值的情况下代替原先的定时器任务。
基础思路已经能够满足两个核心目标,但还可以从下面几个方面进行改进:
- 如何在需求增加或变更的情况下提升可拓展性,如新增的任务、switch语句更密集的分段;
- 如何更系统性的管理因达成核心目的而新增的各种变量,如switch语句状态、各种倒计时变量等等;
- 如何在多任务多分段流程非线性运行而是频繁跳转的情况下提升可读性;
- 如何在现有的项目上更效率的实现核心目标的思路。
进阶思路:
- 有系统的任务管理,以结构体的形式封装任务状态、以函数传参返回的形式封装任务状态的设置和查看;
- 任务调度,使用函数指针封装任务,以for循环的方式判断以及调度任务;
- 避免switch语句的臃肿,以switch语句嵌套switch语句的形式按逻辑或习惯进行多层嵌套;
具体的进阶思路就不具体展开描述了,本文档进入正题;如果将核心目标、基础思路以及进阶思路的逻辑部分剥离出来以工具的形式直接调用,保证系统的实时性并且还能实现方便搭建或移植,可读性强,易拓展和维护,占用更少的资源这些优点的难处将主要集中在对功能需求的理解、实现以及工具的运用上了。(提前说明:工具的入门使用相对简单,但熟练的运用需要更多的耐心来理解)以下将工具暂命名为easyTask OS
静态库:task_lib.lib
头文件:task_lib.h
头文件内容介绍:
//1、任务级别枚举
enum
{
TASKLEV = 0,
PPTASKLEV
};
//用于传参时指代任务的级别:在前台调度的普通任务、在后台调度的可抢占式任务
//任务状态枚举
enum
{
TASK_DISABLE = 0,
TASK_ENABLE,
TASK_TIMELY,
TASK_BUSY
};
//用于传参时需要设置的任务状态
//3、 倒计时级别枚举
enum
{
COUNTMSLEV = 0,
COUNTSECLEV
};
//用于传参时指代倒计时的级别:ms级倒计时、sec级倒计时(使用us级定时器作为后台时则相应变为us级和ms级倒计时,此时可用自定义的枚举进行表示,但对应的0和1不能改变)
//4、初始化
bool TaskInit(uint8_t * infos);
/*
uint8_t infos[7]={普通任务数量,抢占式任务数量,ms级倒计时数量,sec级倒计时数量,正向累计数量,任务复杂度,允许的switch语句嵌套层数上限};
//任务复杂度
ms级倒计时数量和sec级倒计时数量累加不能超过254;
允许的switch语句嵌套层数上限不能超过254;
允许的switch语句嵌套层数上限设置为0时则自动改为1;
*/
//5、任务调度判断
bool TaskisAllow(uint8_t lev,uint8_t point);
void TaskDusted(void);
/*
if(TaskisAllow(lev,point))//判断lev级别point位置的任务是否允许调度
{
(*TaskFnc[point])();//函数指针的运用,执行任务
TaskDusted();//执行完后的收尾工作,执行抢占式任务或需要对任务限制运行时一定要调用(在使用技巧补充部分会展开描述)
}
*/
//6、任务状态返回
uint8_t TaskFlag(uint8_t lev,uint8_t point);
//返回lev级别point位置的任务状态,一般用于查询其他任务的状态
uint8_t TaskStep(uint8_t steplev);
//返回当前任务在steplev级别下的step值(steplev:switch语句的嵌套级别 step:switch语句的expression值,在使用技巧补充部分会展开描述)
uint32_t TaskDelay(uint8_t lev,uint8_t point);
//返回lev级别point位置的任务阻塞delay值,一般用于查询其他任务的阻塞
uint32_t TaskOutDelay(void);
//返回当前任务的超时延迟outdelay值
//7、任务管理
bool TaskCtrl(uint8_t lev,uint8_t point,uint8_t flag,uint8_t step,uint32_t delay);
//设置lev级别point位置的任务的状态、steplev为0时的step值、阻塞值
bool TaskSet(uint8_t flag);
//设置当前任务的状态
bool TaskJump(uint8_t steplev,uint8_t step,uint32_t delay,uint32_t outdelay);
//设备当前任务在steplev级别下的step值、阻塞延迟、超时延迟
bool TaskOutCheck(uint32_t outdelay);
//当前任务的超时判断,超时延迟<=outdelay时返回真,否则返回假
//8、倒计时管理
bool CountSet(uint8_t countlev,uint8_t point,uint32_t delay);
//设置倒计时级别为countlev时,point位置的倒计时的延迟delay
bool CountCheck(uint8_t countlev,uint8_t point,uint32_t delay);
//判断倒计时级别为countlev时,point位置的倒计时,延迟<=delay时返回真,否则返回假
uint32_t CountDelay(uint8_t countlev,uint8_t point);
//返回倒计时级别为countlev时,point位置的倒计时延迟值
//9、计数管理
uint32_t CountAdd(uint8_t point);
//point位置的正向累计加1,并返回加1后的累计值
bool CountClear(uint8_t point);
//point位置的正向累计清零
//10、阻塞管理
void DelayFnc(void);
//在1ms定时器中断内调用,系统的实时性精度将控制在1ms
//或在1us定时器中断内调用,系统的实时性精度将控制在1us
//11、其他
char *TaskHelp(void);//以 “%s”, TaskHelp()的形式使用,打印easyTask OS信息
char *TaskShow_h(void);//以 “%s”, TaskShow_h ()的形式使用,打印整个.h文件内容
//打印时uint8_t buf[]容量建议为2048
使用技巧实例:
以下实际项目为例,使用easyTask OS实现功能需求;
外设:三个LED灯(从左至右依次为LED1、LED2、LED3),两个按键(KEY1,KEY2)
需求:
- 跑马灯,默认为从左到右依次点亮LED1、LED2、LED3、LED1…间隔为1秒,点亮1秒后熄灭紧接着点亮下一个LED;
- 按下KEY1(按下后并松开),改变点亮顺序为从右至左,从此刻点亮的LED处,熄灭后将点亮其左侧LED(LED1熄灭后点亮LED3),再次点按后改变点亮顺序为从左至右,以此类推;
- 短按KEY2(2秒内松开),改变跑马灯间隔为2秒,再次点按后改变间隔为3秒,再次点按恢复为1秒,以此类推;
- 长按KEY2超过2秒后,改变跑马灯间隔为0.5秒,长按松开后恢复间隔为长按之前的间隔(1秒、2秒或3秒)
到这里建议暂停浏览,可以思考自己会如何实现,或许会有更多的收获。
实现:
1、声明宏定义
LED*_H;//LED*点亮
LED*_L;//LED*熄灭
LED*_T;//LED*翻转
KEY*_R;//KEY*按下返回1否则返回0
2、声明函数、函数指针、全局变量
void LedTask(void);
void KeyTask(void);
void (*TaskFnc[])(void) =
{
LedTask,
KeyTask,
};
uint8_t led_flags[2][3]={{1,2,0},{2,0,1}};
uint8_t led_flag = 0;//点亮顺序
uint16_t led_delay[4]={1000,2000,3000,500};
uint8_t led_state[2]={0};//间隔(当前和上一次)
uint8_t infos[7]={2,0,1,0,0,0,1};
3、LED任务
void LedTask(void)
{
switch(TaskStep(0))
{
case 0:
LED1_H;
LED2_L;
LED3_L;
break;
case 1:
LED1_L;
LED2_H;
LED3_L;
break;
case 2:
LED1_L;
LED2_L;
LED3_H;
break;
}
TaskJump(0,led_flags[led_flag][TaskStep(0)],led_delay[led_state[0]],0);
}
4、KEY任务
void KeyTask(void)
{
switch(TaskStep(0))
{
case 0:
TaskSet(TASK_BUSY);//运行限制取消
if(KEY1_R)
{
if(CountCheck(COUNTMSLEV,0,0))//按键刚按下
CountSet(COUNTMSLEV,0,10+10)
else if(CountCheck(COUNTMSLEV,0,10))//完成消抖
CountSet(COUNTMSLEV,0,10)//保持按下的状态
}
else
{
if(CountCheck(COUNTMSLEV,0,0)){}
else if(CountCheck(COUNTMSLEV,0,10))
{
CountSet(COUNTMSLEV,0,0);
if(led_flag)led_flag = 0;
else led_flag = 1;
}
else CountSet(COUNTMSLEV,0,0); //消抖,倒计时清零
}
TaskJump(0,1,0, TaskOutDelay());//超时延迟继承,KEY2使用
break;
case 1:
TaskSet(TASK_ENABLE);//运行限制开启
if(KEY2_R)
{
if(TaskOutCheck(0))TaskJump(0,0,0,2000+10);//启用超时延迟,做消抖和长按判断
else if(TaskOutCheck(10))//长按中
{
if(led_state[0] != 3)
{
led_state[1] = led_state[0];//保存长按前的间隔
led_state[0] = 3;
}
TaskJump(0,0,0,10);//保持长按状态
}
else TaskJump(0,0,0,TaskOutDelay());//超时延迟继承,长按未持续两秒,未松开
}
else
{
if(TaskOutCheck(0)){};
else if(TaskOutCheck(10))//长按结束
{
if(led_state[0] == 3)
led_state[0] = led_state[1];
}
else if(TaskOutCheck(2000))//触发短按
[
led_state[0]++;
if(led_state[0] >= 3)led_state[0] = 0;
]
TaskJump(0,0,0,0);//超时延迟清零
}
break;
}
}
5、main函数内
/*
时钟、IO等初始化
*/
uint8_t i;
TaskInit(infos);
TaskCtrl(0,0,1,0,0);
TaskCtrl(0,1,1,0,0);
while
{
for(i=0;i<2;i++)
{
if(TaskisAllow(0,i))
{
(*TaskFnc[i])();
TaskDusted();
}
}
}
6、定时器中断内
DelayFnc();
关于easyTask OS的lib、h文件以及更详细的说明pdf,请见下面的链接:
easyTask OS: https://url97.ctfile.com/d/29116897-62189287-72ed38?p=4269 (访问密码: 4269)
版权归原作者 OliverF_ 所有, 如有侵权,请联系我们删除。