分享一个状态机实现类操作系统延时风格的算法

2019-07-20 14:30发布

本帖最后由 FreeRTOS 于 2016-11-22 00:05 编辑

今天看到论坛里的一个求助帖子:
http://www.openedv.com/forum.php ... 39&page=1#pid505654
让我想起了之前一直想仿照contiki来解决传统状态机碰到多个延时不好处理的情况,今天恰好有心情说干就干!
相关资料请查阅contiki源码,本测试程序完全仿照contiki的算法实现,不过是简化版的,大神勿喷
关于状态机的基础知识我这里就不作详细讲解了,好了直接上源码:
[mw_shl_code=applescript,true]#define LINE_NUM_GET(num)   num = __LINE__; case __LINE__:
#define PROCESS_BEGIN(s)    switch(s) { case 0:
#define PROCESS_END()       }

/* 任务结构体 */
typedef struct _TaskStruct
{
    void (*TaskHook)(void);     // 要运行的任务函数
    uint32_t Counter;           // 任务全局节拍
    uint16_t LineNum;           // 行号
    uint8_t Run;                // 程序运行标记:0-不运行,1运行
} TaskStruct;

TaskStruct Tasks[3];


#define PROCESS_DELAY(task, n)      do                                             
                                    {                                               
                                        task.Counter = GlobalTimerCnt;              
                                        LINE_NUM_GET(task.LineNum);                 
                                        if( (GlobalTimerCnt - task.Counter) > n )   
                                        {                                          
                                            break;                                 
                                        }                                          
                                        return;                                    
                                    } while(0)[/mw_shl_code]
如果对contiki没了解过的话,看起来会有些吃力,这里我大概说明下。
在每个任务的开头与结尾必须是 PROCESS_BEGIN(s)和PROCESS_END(),至于这两个宏是什么等下再解释
然后就是仿照我们平时使用OS时最常用方式:在while(1)里完成对应的任务
好了,接下来就是最关键的部分PROCESS_DELAY(task, n),这部分代码就是以状态机来实现类操作系统延时!
说太多口水都干了,直接贴几个任务看看使用方法,使用非常简单:
[mw_shl_code=applescript,true]/**********************************************/
/*                                            */
/*                  任务1                     */
/*                                            */
/**********************************************/
void test_process1(void)
{
    PROCESS_BEGIN(Tasks[0].LineNum);
   
    while (1)
    {
        PROCESS_DELAY(Tasks[0], 1000);
        Uart1SendString("rocess1 Delay1 Test! ");
        
        PROCESS_DELAY(Tasks[0], 2000);
        Uart1SendString("rocess1 Delay2 Test! ");
        
        PROCESS_DELAY(Tasks[0], 1500);
        Uart1SendString("rocess1 Delay3 Test! ");
    }
   
    PROCESS_END();
}


/**********************************************/
/*                                            */
/*                  任务2                     */
/*                                            */
/**********************************************/
void test_process2(void)
{
    PROCESS_BEGIN(Tasks[1].LineNum);
   
    while (1)
    {
        PROCESS_DELAY(Tasks[1], 700);
        Uart1SendString("rocess2 Delay1 Test! ");
        
        PROCESS_DELAY(Tasks[1], 1500);
        Uart1SendString("rocess2 Delay2 Test! ");
    }
   
    PROCESS_END();
}


/**********************************************/
/*                                            */
/*                  任务3                     */
/*                                            */
/**********************************************/
void test_process3(void)
{
    PROCESS_BEGIN(Tasks[2].LineNum);
   
    while (1)
    {
        PROCESS_DELAY(Tasks[2], 500);
        Led_Toggle(LED1);
    }
   
    PROCESS_END();
}[/mw_shl_code]
任务1与任务2是串口每隔一段时间打印出测试字符串,任务3是小灯最喜欢的跑灯任务,没有之一!!!

下面就是创建任务,主要是对结构体成员LineNum清零(必须)和设置任务回调函数:
[mw_shl_code=applescript,true]/* 任务初始化 */
    memset((void*)Tasks, 0, sizeof(Tasks));
    Tasks[0].TaskHook = test_process1;
    Tasks[1].TaskHook = test_process2;
    Tasks[2].TaskHook = test_process3;[/mw_shl_code]

好了接下来看执行,执行跟状态机一样,就是各个任务对应的函数轮着来,值得注意的是并没有任务在原地死等,所有任务都以查询的方式进入和退出!!!
[mw_shl_code=applescript,true]while(1)
    {
        /* 任务执行,本质还是状态机,但类似操作系统风格 */
        for(i=0; i<3; i++)
        {
            Tasks.TaskHook();
        }
    }[/mw_shl_code]

到这里就完成了以状态机的方式来实现类操作系统风格的延时!可能各位对代码一时摸不着头脑,我大概说下算法的思想。
把宏PROCESS_BEGIN()与PROCESS_END()展开可以发现其实就是一个switch结构,任务里面的延时调用了PROCESS_DELAY()之后
会把行号记录在任务结构成员LineNum,而算法的精髓也是在于这里,每次调用任务的回调函数时switch结构会跳转到记录行号的位置
然后查询任务结构体成员Counter的值是否已达到延时的节拍,如果还没延时完毕就直接return,任务不需要在原地死等。由于所有任务共用一个堆栈,因此任务中不能出现临时变量,如果实在需要使用变量,那么就遵从contiki的建议使用静态变量,
个人不建议使用全局变量,没别的原因,主要是看起来太TM烦!

算法的大概思想就是这样,小灯近段时间烦心事太多了,所以没有来得及检查代码,只是粗略测试了好像没问题,希望各位大神
能帮忙检查下然后把问题报告给我,我会第一时间修改!

小灯采用阿波罗STM32F7开发板做的测试,如果你们手头上的板子不是的话,要么光顾下原子网店,否则就自行修改下吧。
Process_Delay.rar (1.08 MB, 下载次数: 1266) 2016-11-21 23:51 上传 点击文件名下载附件


友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。
该问题目前已经被作者或者管理员关闭, 无法添加新回复
37条回答
小小速
1楼-- · 2019-07-21 18:20
东北小辉辉 发表于 2016-11-22 09:20
阿mo上的那个小小调度器其实就是仿照CONTIKI的调度机制PT协程来写的,这种方式看起来很巧妙,但是不知道 ...

我才看了一天,是个十足的新人,脑子里还有很多问题
lxj19901115
2楼-- · 2019-07-21 23:34
 精彩回答 2  元偷偷看……
lxj19901115
3楼-- · 2019-07-22 04:59
原理很简单,就是维护一个定时器,定时事件组,有互斥类型的,也有周期运行的,
更新和维护这个定时器事件组,则可以将
task_timer_list_running
放入主循环或者直接放入TICK中断函数执行,至于具体放在哪里,则要看这个事件组里面,执行时间最长的事件所需要花费的时间,
lxj19901115
4楼-- · 2019-07-22 07:17
另外对于楼主说的这个方法,有这个缺陷,如果在DELAY之前执行的动作时间过长的话,则会影响整个系统的反应速度,如下面代码
void test_process1(void)
{
    PROCESS_BEGIN(Tasks[0].LineNum);
   
    while (1)
    {
       code1...
       delay(x);
      code2...
      delay(y);
    }
   
    PROCESS_END();
}
如此调度会导致在延时前面的某写代码,和某个动作会反复得到执行的机会,大大降低反应速度
FreeRTOS
5楼-- · 2019-07-22 08:18
 精彩回答 2  元偷偷看……
FreeRTOS
6楼-- · 2019-07-22 10:05
止天 发表于 2016-11-22 09:34
最早看到是在蝇量级协程的文章

最大的问题就是在里面嵌套Switch case容易出错

switch case是否容易出错我没研究过,求科普

一周热门 更多>