公告

大中华汽车电子生态圈社区并入开发者社区- 更多资讯点击此

Tip / 登入 to post questions, reply, level up, and achieve exciting badges. Know more

cross mob

TC275调试日志——时间片调度系统

TC275调试日志——时间片调度系统

凌风2018_版主_
Level 4

今天我们来回想一下我们学习单片机的历程,从大三开始学习单片机到现在有四年了吧。编写的程序还只是一个大while循环。。。也就是所谓的前后台程序,对于多任务的处理,要不就是顺序执行(if、switch等等),要不就是放在中断。虽然做法很low,但是应付大多数的程序都够了。

随着程序复杂度的提升,使用前后台程序就显得有些混乱了。假如说一个ECU的底层,ECU要采集实时踏板、水温、油压、轨压、空气压力、转速、进气量等等各种信息,还要实时计算这些信息控制燃油的喷射,还要能提供整车控制任务,甚至还要能输出诊断信息。可以想象这任务量是很庞大的,单单使用顺序执行,中断显然是不够的。这种层次的任务量才是真正的多任务,能够处理这种多任务才算是真正入了门。

操作系统是很多人的选择,操作系统就是为了多任务的调度而生的,调度也是操作系统的核心。我们今天讲一个比操作系统第一个层次的多任务调度方法,就是时间片轮询法。我之前虽然用到了,但是一直没有系统性的规划这种方法。今天我们就尝试用这种方法搭建一个系统性质的运行环境。

1、首先来说明定时器的复用

上一贴我们使用STM搭建了1ms定时的底层,并且开了匹配中断。我们现在需要在此基础上得到10ms、20ms、100ms的定时,需要怎么做呢? 不难想,我们只需要设定一个定时值,假如10,每次中断减一,判断标志位为0就得到了10ms的定时。这就是定时器的复用。代码上,我们可以参考以下的方式:

#define TASK_NUM   (3) //  这里定义的任务数为3,表示有三个任务会使用此定时器定时。 

uint16 TaskCount[TASK_NUM] //  这里为三个任务定义三个变量来存放定时值

uint8  TaskMark[TASK_NUM]; //  同样对应三个标志位,为0表示时间没到,为1表示定时时间到。

/* FunctionName : TimerInterrupt() * Description : 定时中断服务函数 * EntryParameter : None * ReturnValue : None*/

 

/*代码解释:定时中断服务函数,在中断中逐个判断,如果定时值为0了,表示没有使用此定时器或此定时器已经完成定时,不做处理。否则定时器减一,知道为零时,相应标志位值1,表示此任务的定时值到了。*/

 

 void TimerInterrupt(void)

{    

       uint8 i;     

      for (i=0; i<TASKS_NUM; i++)    

      {        

             if (TaskCount)        

           {              

                 TaskCount--;              

                if (TaskCount == 0)              

                {                    

                     TaskMark = 0x01;              

                }        

            }   

}

 

 TaskCount[0] = 20;       // 延时20ms

 TaskMark[0]  = 0x00;     // 启动此任务的定时器

大家可以体会一下以上代码,我们可以在1ms的基础定时上,得到20ms的定时。

2、时间片轮询法

时间片其实和以上是一个道理,也是利用定时器的复用,增加的一点就是任务调度。就是在相应的定时时间到了调度相应的任务,而其他任务则仍然处于延时当中。相当于是在其他任务空闲的时候运行调度任务,这样就能充分利用CPU时间,我们的操作系统就是这样的,当然操作系统要更复杂。接下来我们用代码来实现:

首先定义一个任务结构体,该结构体包含一个运行任务的所有信息。

typedef struct _TASK_COMPONENTS

{    

      uint8 Run;                 // 程序运行标记:0-不运行,1运行    

     uint8 Timer;               // 计时器    

     uint8 ItvTime;             // 任务运行间隔时间    

     void (*TaskHook)(void);    // 要运行的任务函数

} TASK_COMPONENTS;               // 任务定义

 

接下来定义一个任务标志处理函数,类似于中断服务函数。作用是处理任务标志位:

void TaskRemarks(void)

 {    

      uint8 i;    

     for (i=0; i<TASKS_MAX; i++)          // 逐个任务时间处理    

     {         

          if (TaskComps.Timer)          // 时间不为0        

          {            

               TaskComps.Timer--;         // 减去一个节拍            

              if (TaskComps.Timer == 0)       // 时间减完了            

              {                 

                   TaskComps.Timer = TaskComps.ItvTime;       // 恢复计时器值,从新下一次                 

                  TaskComps.Run = 1;           // 任务可以运行            

              }        

          }   

     }

}

这个函数要放在定时中断里面,和我们定时复用里面讲的一样,这里就是各个任务定时的核心。

接下来是任务处理函数,这个函数可以说是系统任务处理的核心。只需放在while循环里面,随着时间片的流转,该函数不断调用任务函数,完成轮询调度的工作。

void TaskProcess(void)

{    

    uint8 i;    

   for (i=0; i<TASKS_MAX; i++)           // 逐个任务时间处理    

   {         

        if (TaskComps.Run)           // 时间不为0        

        {             

            TaskComps.TaskHook();         // 运行任务             

           TaskComps.Run = 0;          // 标志清0        

        }    

   }   

}

说到这里,整个系统结构就很清楚了。在定时里面完成任务定时的复用,然后在主循环里面不停的轮询调用任务函数,这样已完成多任务的调度。

接下来我们给出一个例子:

static TASK_COMPONENTS TaskComps[] =

{    

  {0, 1000, 1000, TaskDisplayClock},          // 显示时钟    

  {0, 20, 20, TaskKeySan},                // 按键扫描    

  {0, 30, 30, TaskDispStatus},            // 显示工作状态     

                                          // 这里添加你的任务。。。。

  };

这里我们有三个任务,显示当前时钟,1s显示一次,因为我们基础定时为1ms,所以定时值为1000;按键扫描为了消抖,所以定为20ms的任务;最后一个是显示工作状态,定为30ms的任务。

typedef enum _TASK_LIST

{    

   TAST_DISP_CLOCK,          // 显示时钟    

   TAST_KEY_SAN,             // 按键扫描    

   TASK_DISP_WS,             // 工作状态显示     

                             // 这里添加你的任务。。。。     

   TASKS_MAX                 // 总的可供分配的定时任务数目

 } TASK_LIST; 

定义任务列表为枚举类型。

接下来则是各个任务的定义;

/* FunctionName   : TaskDisplayClock() * Description    : 显示任务 * EntryParameter : None * ReturnValue    : None */

void TaskDisplayClock(void)

 

}

 

/* FunctionName   : TaskKeySan() * Description    : 扫描任务 * EntryParameter : None * ReturnValue    : None */

void TaskKeySan(void)

 

 

}

 

/* FunctionName   : TaskDispStatus() * Description    : 工作状态显示 * EntryParameter : None * ReturnValue    : None*/

void TaskDispStatus(void)

 

 

}

最后是主函数部分:

/* FunctionName   : main() * Description    : 主函数 * EntryParameter : None * ReturnValue    : None */

int main(void)

{    

    InitSys();                  // 初始化    

   while (1)    

   {        

      TaskProcess();             // 任务处理    

   }

再添加好系统初始化之后,就可以在while循环放入任务调出函数。当然不要忘了在定时中断里面放入标志位处理函数。

有没有发现这个系统都很清晰,定义好这些以后,我们只需要定义任务的信息和任务函数就完了,更本不用管主函数。只要定义好任务时间,一切都会按部就班的工作。一瞬间世界安静了。。。。。。

大家可以在实际中应用,体会这种调度方法的妙处。

好了今天的帖子就到这里,我们下次再唠。

(参考文献:Killoser. 牛人分享: 单片机应用程序架构的经验总结. https://forum.mianbaoban.cn/topic/60374_1_1.html?from=timeline.2017)


0 点赞
3927 次查看
14 评论