基于时间片的单片机任务管理组件-TaskProc
基于时间片的单片机任务管理组件-TaskProc

基于时间片的单片机任务管理组件-TaskProc

TaskProc 组件的移植与使用。

简介

TaskProc 组件使用 C 语言编写,可以很方便移植到其它平台。该组件提供了创建任务、删除任务、挂起任务和将任务从挂起态恢复等操作,轻量简约,十分适用于单片机平台。TaskProc 组件使用系统中任一硬件定时器做为时钟源,在定时器中断里更新各个任务的时间,然后在主线程中轮询确认任务是否到时,若是时间到了则执行该任务。TaskProc 组件中所有任务均为周期任务,任务周期由创建任务时确定,并支持动态修改任务周期。TaskProc 组件支持系统时基与任务周期独立配置,例如硬件定时器周期可以设置到 10ms,任务周期可以设置到 9ms、11ms 等任意整数值,最小任务周期为 1ms。

头文件

TaskProc 组件头文件如下所示。TASK_PROC_DEBUG_ENABLE 宏定义仅用于调试,会自动输出一些信息,方便调试。

/*********************************************************************************************************
* 模块名称: TaskProc.h
* 摘    要: 时间片轮询模块
* 当前版本: 1.0.0
* 作    者: SZLY(COPYRIGHT 2019 SZLY. All rights reserved.)
* 完成日期: 2023年09月04日
* 内    容: 
* 注    意: 一个任务函数只能对应一个任务,不可共用
**********************************************************************************************************
* 取代版本: 1.0.0
* 作    者: HRZ
* 完成日期: 2023年09月04日
* 修改内容: 添加动态注册、删除和修改任务周期 API 函数
* 修改文件: TaskProc.c/TaskProc.c
*********************************************************************************************************/
#ifndef _TASK_PROC_H_
#define _TASK_PROC_H_

/*********************************************************************************************************
*                                              包含头文件
*********************************************************************************************************/

/*********************************************************************************************************
*                                              宏定义
*********************************************************************************************************/
#define TASK_PROC_DEBUG_ENABLE (0)

/*********************************************************************************************************
*                                              枚举结构体定义
*********************************************************************************************************/

/*********************************************************************************************************
*                                              API函数声明
*********************************************************************************************************/
void InitTaskProc(void);                                                        //初始化时间片轮询模块
void TaskTimerProc(unsigned int decTime);                                       //时间片定时器中断任务标记处理
void TaskProcess(void);                                                         //时间片主程序任务处理
void TaskRegister(void (*taskHook)(void), unsigned int period, char* taskName); //任务注册
void TaskDelete(void (*taskHook)(void));                                        //删除任务
void TaskModifyPeriod(void (*taskHook)(void), unsigned int period);             //修改任务周期
void TaskSuspend(void (*taskHook)(void));                                       //挂起任务
void TaskResume(void (*taskHook)(void));                                        //唤醒任务

#if TASK_PROC_DEBUG_ENABLE
void TaskPrintParam(void);                                                      //打印所有任务参数
#endif

#endif

源文件

TaskProc 组件源文件如下所示。为了避免使用 malloc 动态内存分配,以及方便用户使用(用户无需定义任务控制块),TaskProc 组件预制了任务控制块,因此有最大任务数量限制,可以通过 MAX_TASK_NUM 宏修改。MAX_TASK_NUM 宏要根据实际项目做适当修改。

/*********************************************************************************************************
* 模块名称: TaskProc.c
* 摘    要: 时间片轮询模块
* 当前版本: 1.0.1
* 作    者: SZLY(COPYRIGHT 2019 SZLY. All rights reserved.)
* 完成日期: 2023年09月04日
* 内    容: 
* 注    意: 
**********************************************************************************************************
* 取代版本: 1.0.0
* 作    者: HRZ
* 完成日期: 2023年09月04日
* 修改内容: 添加动态注册、删除和修改任务周期 API 函数
* 修改文件: TaskProc.c/TaskProc.c
*********************************************************************************************************/

/*********************************************************************************************************
*                                              包含头文件
*********************************************************************************************************/
#include "TaskProc.h"

#if TASK_PROC_DEBUG_ENABLE
  #include "stdio.h"
#endif

/*********************************************************************************************************
*                                              宏定义
*********************************************************************************************************/
//最大任务数量
#define MAX_TASK_NUM (10)

//空指针定义
#ifndef NULL
  #define NULL 0
#endif

/*********************************************************************************************************
 *                                              枚举结构体定义
 *********************************************************************************************************/
//任务结构体
typedef struct
{
  unsigned int  run;      //程序运行计数,为 1 就代表要运行一次,2 则代表两次,以此类推
  unsigned int  timer;    //计时器,单位是 ms。向下递减,递减到零则表示将 run 加一
  unsigned int  period;   //任务运行周期,单位是 ms
  void (*taskHook)(void); //要运行的任务函数,单位是 ms
  char* taskName;         //任务名
  void* next;             //下一项
}StructTaskCtr;           //任务控制块

/*********************************************************************************************************
*                                              内部变量
*********************************************************************************************************/
//定时器轮询使能标志位,0-定时器中断可以执行轮询操作,其它-定时器禁止执行轮询操作
volatile static unsigned int s_iTimerPollFlag = 0;

//定时器向下递减总数,增加、删除任务的时候会无法计时,此变量用来记录中的向下递减的时间
volatile static unsigned int s_iTimerDecCnt = 0;

//是否初始化过标志位
volatile static unsigned char s_iHasInitFlag = 0;

//链表是否有修改过标志位
volatile static unsigned char s_iLinkUpdateFag = 0;

//任务链表以及链表项
static StructTaskCtr* s_pTaskHeader = NULL;
static StructTaskCtr  s_arrTaskItem[MAX_TASK_NUM] = {0};

/*********************************************************************************************************
*                                              内部函数声明
*********************************************************************************************************/
static void TaskCallBack(void);                  //任务执行函数
static void TaskRemarks(unsigned int decTime);   //任务标记

/*********************************************************************************************************
*                                              内部函数实现
*********************************************************************************************************/
/*********************************************************************************************************
* 函数名称: TaskCallBack
* 函数功能: 任务执行函数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 由主函数执行,负责做任务回调
*********************************************************************************************************/
static void TaskCallBack(void)
{
  StructTaskCtr* node;
TASK_CALLBACK_BEGIN_MARK:                //函数起始位置标记
  s_iLinkUpdateFag = 0;                  //清空链表修改标志位
  node = s_pTaskHeader;                  //获取链表头
  while(NULL != node)                    //循环遍历整张链表
  {
    if(0 != node->run)                   //该任务需要运行
    {
      node->taskHook();                  //执行任务回调函数
      if(0 != s_iLinkUpdateFag)          //任务回调函数中执行了修改任务链表相关操作,例如注册、删除任务
      {
#if TASK_PROC_DEBUG_ENABLE
        if(NULL != node->taskName){printf("TaskCallBack: Task(%s) update the link\r\n", node->taskName);}
        else{printf("TaskCallBack: Task(?) update the link\r\n");}
#endif
        goto TASK_CALLBACK_BEGIN_MARK;   //返回该函数最开始位置,重新遍历一遍
      }
	  s_iTimerPollFlag++;                //禁止定时器更新
      node->run--;                       //执行计数减一
	  s_iTimerPollFlag--;                //恢复定时器更新
    }
    node = node->next;                   //递归到下一项
  }
}

/*********************************************************************************************************
* 函数名称: TaskRemarks
* 函数功能: 任务标记
* 输入参数: decTime:递减的时间,ms
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 由定时器中断服务函数执行,负责时钟计数
*********************************************************************************************************/
static void TaskRemarks(unsigned int decTime)
{
  StructTaskCtr* node;
  unsigned int time;
  node = s_pTaskHeader;
  while(NULL != node)
  {
    //剩余时间大于需要递减的时间,直接减去响应的时间即可
    if(node->timer > decTime)
    {
      node->timer = node->timer - decTime;
    }

    //剩余时间小于等于需要递减的时间,需要考虑递减时间大于剩余时间的情况
    else
    {
      time = decTime - node->timer;
      node->run += 1 + (time / node->period);
      node->timer = node->period - (time % node->period);
    }

    //迭代到下一项
    node = node->next;
  }
}

/*********************************************************************************************************
*                                              API函数实现
*********************************************************************************************************/
/*********************************************************************************************************
 * 函数名称: InitTaskProc
 * 函数功能: 初始化时间片轮询模块
 * 输入参数: void
 * 输出参数: void
 * 返 回 值: void
 * 创建日期: 2023年09月04日
 * 注    意:
 *********************************************************************************************************/
void InitTaskProc(void)
{
  int i;

  //标记已经初始化过
  if(0 != s_iHasInitFlag)
  {
    return;
  }
  s_iHasInitFlag = 1;

  //静态变量清零
  s_iTimerPollFlag = 0;
  s_iTimerDecCnt = 0;
  s_iLinkUpdateFag = 0;
  for(i = 0; i < MAX_TASK_NUM; i++)
  {
    s_arrTaskItem[i].run = 0;
    s_arrTaskItem[i].timer = 0;
    s_arrTaskItem[i].period = 0;
    s_arrTaskItem[i].taskHook = NULL;
    s_arrTaskItem[i].next = NULL;
  }
}

/*********************************************************************************************************
 * 函数名称: TaskTimerProc
 * 函数功能: 任务标记
 * 输入参数: decTime:向下递减的时间片数量,如果定时器中断服务函数周期为 1ms,那么 decTime 输入为 1;如果是周期是
 *                   10ms,那么  decTime 输入为 10。以此类推
 * 输出参数: void
 * 返 回 值: void
 * 创建日期: 2023年09月04日
 * 注    意: 1、需要放到定时器中断服务函数中周期执行。
 *           2、在注册、删除任务时,因为任务链表是全局变量,所以需要用一个标志位提示中断服务函数任务链表正在被编辑,
 *              请勿执行递减操作,此时中断服务函数要直接返回,不能对任务链表执行任何操作。
 *              为了保证周期回调的精确性,在这里需要将总的递减时间记录下来,在下一次进入执行 TaskTimerProc 函数时,
 *              一并减掉,这样就能保证其它任务的周期不被严重干扰。
 *********************************************************************************************************/
void TaskTimerProc(unsigned int decTime)
{
  //统计总的递减时间
  s_iTimerDecCnt += decTime;

  //任务链表为空、正在被编辑或未被初始化,直接返回
  if((0 != s_iTimerPollFlag) || (NULL == s_pTaskHeader) || (0 == s_iHasInitFlag))
  {
    return;
  }

  //时间递减
  TaskRemarks(s_iTimerDecCnt);

  //清空递减时间
  s_iTimerDecCnt = 0;
}

/*********************************************************************************************************
* 函数名称: TaskProcess
* 函数功能: 任务处理
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 需要放到主函数的死循环中不停调用
*********************************************************************************************************/
void TaskProcess(void)
{
  TaskCallBack();
}

/*********************************************************************************************************
* 函数名称: TaskRegister
* 函数功能: 任务注册
* 输入参数: taskHook:任务回调函数,period:任务执行周期(ms)
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 禁止将任务周期设定为 0
*********************************************************************************************************/
void TaskRegister(void (*taskHook)(void), unsigned int period, char* taskName)
{
  int i;
  StructTaskCtr* node;

  //尚未初始化
  if(0 == s_iHasInitFlag)
  {
    InitTaskProc();
  }

  //周期为 0,直接返回
  if(0 == period)
  {
    return;
  }

  //校验该任务是否已经注册,若已经注册过则直接返回
  node = s_pTaskHeader;
  while(NULL != node)
  {
    if(taskHook == node->taskHook)
    {
      return;
    }
    node = node->next;
  }

  //寻找空的任务项
  node = NULL;
  for(i = 0; i < MAX_TASK_NUM; i++)
  {
    if(NULL == s_arrTaskItem[i].taskHook)
    {
      node = &s_arrTaskItem[i];
      break;
    }
  }

  //查找失败,说明系统中任务数量大于预设值,直接卡死
  if(NULL == node)
  {
    while(1){}
  }

  //记录任务信息
  node->run = 0;             //注册后不执行
  node->timer = period;      //设置计时器
  node->period = period;     //设置预装载值
  node->taskHook = taskHook; //记录任务回调函数
  node->next = NULL;         //下一项默认为空
  node->taskName = taskName; //记录任务名

  //禁止定时器更新
  s_iTimerPollFlag++;

  //直接添加到表头
  node->next = s_pTaskHeader;
  s_pTaskHeader = node;

  //恢复定时器更新
  s_iTimerPollFlag--;

  //标记任务链表又被修改过
  s_iLinkUpdateFag = 1;
}

/*********************************************************************************************************
* 函数名称: TaskDelete
* 函数功能: 删除任务
* 输入参数: taskHook:任务回调函数
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 
*********************************************************************************************************/
void TaskDelete(void (*taskHook)(void))
{
  StructTaskCtr* node;
  StructTaskCtr* next;

  //尚未初始化
  if(0 == s_iHasInitFlag)
  {
    InitTaskProc();
  }

  //禁止定时器更新
  s_iTimerPollFlag++;

  //获取表头指针
  node = s_pTaskHeader;

  //需要删除的任务就在表头
  if(taskHook == s_pTaskHeader->taskHook)
  {
    s_pTaskHeader = s_pTaskHeader->next;
    node->run = 0;
    node->timer = 0; 
    node->period = 0;
    node->taskHook = 0;
    node->next = 0;
  }

  //递归查找任务所在位置并删除
  else
  {
    while(NULL != node->next)
    {
      next = node->next;
      if(taskHook == next->taskHook)
      {
        node->next = next->next;
        next->run = 0;
        next->timer = 0; 
        next->period = 0;
        next->taskHook = 0;
        next->next = 0;
        break;
      }
      node = node->next;
    }
  }

  //恢复定时器更新
  s_iTimerPollFlag--;

  //标记任务链表又被修改过
  s_iLinkUpdateFag = 1;
}

/*********************************************************************************************************
* 函数名称: TaskModifyPeriod
* 函数功能: 修改任务周期
* 输入参数: taskHook:任务回调函数,period:任务执行周期(ms)
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 禁止将任务周期设定为 0
*********************************************************************************************************/
void TaskModifyPeriod(void (*taskHook)(void), unsigned int period)
{
  int i;

  //尚未初始化
  if(0 == s_iHasInitFlag)
  {
    InitTaskProc();
  }

  //周期为 0,直接返回
  if(0 == period)
  {
    return;
  }

  //禁止定时器更新
  s_iTimerPollFlag++;

  //查找目标位置并更新时间
  for(i = 0; i < MAX_TASK_NUM; i++)
  {
    if(taskHook == s_arrTaskItem[i].taskHook)
    {
      s_arrTaskItem[i].run = 0;
      s_arrTaskItem[i].timer = period;
      s_arrTaskItem[i].period = period;
      break;
    }
  }

  //恢复定时器更新
  s_iTimerPollFlag--;

  //标记任务链表又被修改过
  s_iLinkUpdateFag = 1;
}

/*********************************************************************************************************
* 函数名称: TaskSuspend
* 函数功能: 挂起任务
* 输入参数: taskHook:任务回调函数
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: TaskSuspend 与 TaskResume 配对使用
*********************************************************************************************************/
void TaskSuspend(void (*taskHook)(void))
{
  StructTaskCtr* node;
  StructTaskCtr* next;

  //尚未初始化
  if(0 == s_iHasInitFlag)
  {
    InitTaskProc();
  }

  //禁止定时器更新
  s_iTimerPollFlag++;

  //获取表头指针
  node = s_pTaskHeader;

  //需要挂起的任务就在表头
  if(taskHook == s_pTaskHeader->taskHook)
  {
    s_pTaskHeader = s_pTaskHeader->next;
    node->next = 0;
  }

  //递归查找任务所在位置并移除
  else
  {
    while(NULL != node->next)
    {
      next = node->next;
      if(taskHook == next->taskHook)
      {
        node->next = next->next;
        next->next = 0;
        break;
      }
      node = node->next;
    }
  }

  //恢复定时器更新
  s_iTimerPollFlag--;

  //标记任务链表又被修改过
  s_iLinkUpdateFag = 1;
}

/*********************************************************************************************************
* 函数名称: TaskResume
* 函数功能: 唤醒任务
* 输入参数: taskHook:任务回调函数
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 
*          1、TaskSuspend 与 TaskResume 配对使用
*          2、任务唤醒后将会被立即运行
*********************************************************************************************************/
void TaskResume(void (*taskHook)(void))
{
  int i;
  StructTaskCtr* node;

  //尚未初始化
  if(0 == s_iHasInitFlag)
  {
    InitTaskProc();
  }

  //校验该任务是否已经处在活动链表,若已经已存在过则直接返回
  node = s_pTaskHeader;
  while(NULL != node)
  {
    if(taskHook == node->taskHook)
    {
      return;
    }
    node = node->next;
  }

  //寻找对应的任务项
  node = NULL;
  for(i = 0; i < MAX_TASK_NUM; i++)
  {
    if(taskHook == s_arrTaskItem[i].taskHook)
    {
      node = &s_arrTaskItem[i];
      break;
    }
  }

  //查找失败,说明该任务尚未注册,直接返回
  if(NULL == node)
  {
    return;
  }

  //记录任务信息
  node->run = 1;              //唤醒后立即执行
  node->timer = node->period; //设置计时器
  node->next = NULL;          //下一项默认为空

  //禁止定时器更新
  s_iTimerPollFlag++;

  //直接添加到表头
  node->next = s_pTaskHeader;
  s_pTaskHeader = node;

  //恢复定时器更新
  s_iTimerPollFlag--;

  //标记任务链表又被修改过
  s_iLinkUpdateFag = 1;
}

/*********************************************************************************************************
* 函数名称: TaskPrintParam
* 函数功能: 打印所有任务参数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 空的任务项不会被打印
*********************************************************************************************************/
#if TASK_PROC_DEBUG_ENABLE
void TaskPrintParam(void)
{
  int i;
  StructTaskCtr* next;
  StructTaskCtr* node;

  //打印所有任务的参数
  printf("\r\nTaskPrintParam: All Task Param:\r\n");
  for(i = 0; i < MAX_TASK_NUM; i++)
  {
    if(NULL != s_arrTaskItem[i].taskHook)
    {
      printf("\r\n");
      if(NULL != s_arrTaskItem[i].taskName){printf("Task name: %s\r\n", s_arrTaskItem[i].taskName);}
      else{printf("Task name: ?\r\n");}
      printf("Task time: %d, %d, %d\r\n", s_arrTaskItem[i].period, s_arrTaskItem[i].timer, s_arrTaskItem[i].run);
      printf("Task addr: 0x%08X\r\n", (unsigned int)s_arrTaskItem[i].taskHook);
      if(NULL == s_arrTaskItem[i].next)
      {
        printf("Task next: NULL\r\n");
      }
      else
      {
        next = s_arrTaskItem[i].next;
        if(NULL != next->taskName){printf("Task next: %s\r\n", next->taskName);}
        else{printf("Task next: ?\r\n");}
      }
    }
  }

  //打印活动状态下的任务参数
  printf("\r\nTaskPrintParam: Task Link\r\n");
  node = s_pTaskHeader;
  while(NULL != node)
  {
    if(NULL != node->next)
    {
      if(NULL != node->taskName){printf("%s->", node->taskName);}
      else{printf("?->");}
    }
    else
    {
      if(NULL != node->taskName){printf("%s\r\n", node->taskName);}
      else{printf("\r\n");}
    }
    node = node->next;
  }
  printf("\r\n");
}
#endif

移植

时钟源方面可以使用 SysTick 驱动,也可以使用任一定时器。移植代码如下所示。TaskTimerProc 函数的参数用于告知 TaskProc 模块定时器的周期,单位是 ms。此处 SysTick 中断服务函数执行周期为 1ms,因此 TaskTimerProc 参数为 1。如果 SysTick 中断服务函数执行周期为 10ms,那么 TaskTimerProc 的参数要修改为 10,以此类推。

#include "TaskProc.h"

/*********************************************************************************************************
* 函数名称: SysTick_Handler
* 函数功能: SysTick中断服务函数
* 输入参数: void
* 输出参数: void
* 返 回 值: void
* 创建日期: 2023年09月04日
* 注    意: 
*********************************************************************************************************/
void  SysTick_Handler(void)
{
  TaskTimerProc(1);
}

主函数中要需要先初始化 TaskProc 组件,再注册相关任务,最后再不停的调用 TaskProcess 函数即可,如下所示。

#include "TaskProc.h"
#include "LED.h"

int main(void)
{
  //初始化软硬件
  InitHardware();
  InitSoftware();
  
  //初始化 TaskProc 组件
  InitTaskProc();

  //注册任务
  TaskRegister(LED1Task, 500, "LED1 Task");
  TaskRegister(LED2Task, 750, "LED2 Task");

  //主循环
  while(1)
  {
    TaskProcess();
  }
}

任务注册与删除

任务注册与删除通过 TaskProc 组件提供的 TaskRegister 和 TaskDelete 函数实现。

任务的注册如下所示。任务函数的形参固定为 void (*)(void),与 FreeRTOS 和 μC/OS-III 等操作系统的任务函数不同,它并非一个死循环,且任务函数不能复用。注册函数时,需要提供任务函数入口地址,任务周期(以 ms 为单位)和任务名(字符串)。任务名仅用于调试输出,实际使用时可以简单的使用 NULL 空指针代替字符串。任务本身需要的控制块(控制结构体)已经在 TaskProc 组件中内置了,所以用户无需额外定义一个任务控制块。

注意:如果注册的任务超过了规定的最大任务数量,TaskRegister 函数会直接卡死。

#include "TaskProc.h"

//任务函数
void LED1Task(void)
{
  GPIO_WriteBit(GPIOC, GPIO_Pin_4, (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_4)));
}

//主程序
int main(void)
{
  //注册任务
  TaskRegister(LED1Task, 500, "LED1 Task");

  ...
}

任务删除可以通过 TaskDelete 函数轻松实现,如下所示。

//删除 LED1 任务
TaskDelete(LED1Task);

任务的挂起和恢复

任务挂起与恢复通过 TaskProc 组件提供的 TaskSuspend 和 TaskResume 函数实现,具体使用如下所示。挂起任务时,TaskProc 组件会将该任务从活动链表中删除,但任务周期、任务名等信息会被保存下来。唤醒任务后,任务将按照之前设定的周期运行。

注意:通过 TaskResume 函数唤醒任务后,任务将会被立即执行。

//挂起 LED1 任务
TaskSuspend(LED1Task);

//恢复 LED1 任务
TaskResume(LED1Task);

修改任务周期

修改任务周期可以通过 TaskProc 组件提供的 TaskModifyPeriod 函数实现,具体如下所示。处在挂起状态的任务也可以通过 TaskModifyPeriod 修改周期。

//修改 LED1 任务的周期为 1s
TaskModifyPeriod(LED1Task, 1000);

中断相关

若要在中断里执行 TaskProc 组件的 API 函数,则要确保该中断的优先级小于等于定时器中断的优先级。最好是系统初始化时,直接将定时器中断优先级设定为最高。因为定时器中断服务程序中需要轮询所有任务,并更新时间,比较耗资源,因此不宜在系统中部署太多的任务。

源码下载