【前言】
在嵌入式软件开发,包括单片机开发中,软件架构对于开发人员是一个必须认真考虑的问题。软件架构对于系统整体的稳定性和可靠性是非常重要的,一个合适的软件架构不仅结构清晰,并且便于开发。在嵌入式或单片机软件开发的初期大多数开发者采用的都是简单的前后台顺序执行架构。仔细考虑下来,其实在嵌入式软件开发中,程序架构主要分为三种:顺序执行的前后台系统、时间片轮询系统和多任务操作系统。
顺序执行的前后台系统架构的优点是使用简单易于理解,而缺点是每个任务所占的CPU时间过长的话,会导致程序的实时性能差。
操作系统的本身是一个比较复杂的东西,任务的管理和调度实现的底层是很复杂和困难的。实际上真正能使用操作系统的人并不多,反而是跑裸机的占大多数,这也和产品的具体要求有关,很多简单的系统只需要裸机即可满足。
相较而言,简单的时间片轮调(轻量多任务非抢占式系统)架构相比顺序执行有很大优势,既有顺序执行法的优点,也有操作系统的部分优点且没有操作系统那么复杂。
【任务管理器-MillisTaskManager】
MillisTaskManager,是一个非常简单的、超轻量级分时合作式任务调度器,多用于Arduino或其他基本级别循环系统的应用中,可以替代旧的millis()轮询方案,不依赖ArduinoAPI。它在loop()函数中封装了使用millis() (或自启动以来返回毫秒数的其他函数millis()来跟踪定时任务迭代的方法。 这个想法是实例化任务管理器,添加要在各个时间段定期调用的任务(函数),然后在每次调用loop()都调用任务管理器调度程序来运行您的任务,而不是单独处理它们的任务。自己的时序变量和延迟。 使用 MillisTaskManager ,您可以:配置任务以使其定期且无限期地运行将任务配置为定期运行,但运行次数有限,或者在以后的一段时间内仅运行一次 链接一系列任务以按规定的时间间隔执行,并定期运行该链接 MillisTaskManager 对于定时运行以10毫秒或更长的时间(例如,您想每125ms或每5s运行一个特定任务)的多个任务很有用。 它不可以处理短于此周期的短周期执行任务。
它有以下一些 feature :
在核心层面它需要提供一个精确到毫秒级的系统时钟,然后周期调用Running函数即可正常驱动运行起来该任务调度器。
class MillisTaskManager
{
public:
typedef void(*TaskFunction_t)(void);//任务回调函数
struct Task
{
bool State; //任务状态
TaskFunction_t Function; //任务函数指针
uint32_t Time; //任务时间
uint32_t TimePrev; //任务上一次触发时间
uint32_t TimeCost; //任务时间开销(us)
uint32_t TimeError; //误差时间
struct Task* Next; //下一个节点
};
typedef struct Task Task_t;//任务类型定义
MillisTaskManager(bool priorityEnable = true);//false
~MillisTaskManager();
Task_t* Register(TaskFunction_t func, uint32_t timeMs, bool state = true);
Task_t* Find(TaskFunction_t func);
Task_t* GetPrev(Task_t* task);
bool Logout(TaskFunction_t func);
bool SetState(TaskFunction_t func, bool state);
bool SetIntervalTime(TaskFunction_t func, uint32_t timeMs);
uint32_t GetTimeCost(TaskFunction_t func);
uint32_t GetTickElaps(uint32_t nowTick, uint32_t prevTick);
#if (MTM_USE_CPU_USAGE == 1)
float GetCPU_Usage();
#endif
void Running(uint32_t tick);
private:
Task_t* Head; //任务链表头
Task_t* Tail; //任务链表尾
bool PriorityEnable; //优先级使能
};
【移植MillisTaskManager】
Step 1. 获取到 MillisTaskManager源码包,往之前的模板工程中添加源码 MillisTaskManager.cpp,且添加头文件路径:
Step 2. 由于 MillisTaskManager 源码为 c++代码,对于 main函数的处理有以下2种方法:
沿用.c方式,需要将文件属性设置为 c++ source file
第二种方法是直接将文件后缀名直接改为 .cpp ,使用 c++文件。
Step 3. 由于 MillisTaskManager 调度器需要由一个硬件ms定时器驱动,这里使用SysTick中断形式
#include "delay.h"
/* Private typedef ----------------------------------------------------------*/
/* Private define -----------------------------------------------------------*/
/* Private macro ------------------------------------------------------------*/
/* Private variables --------------------------------------------------------*/
volatile uint32_t SysTick_DelayTicks = 0;
/* Private function prototypes ----------------------------------------------*/
/* Private functions --------------------------------------------------------*/
/* Exported variables -------------------------------------------------------*/
/* Exported function prototypes ---------------------------------------------*/
extern uint32_t SystemCoreClock ;
#define SysTick_LoadValue (SystemCoreClock / 1000U)
#define CYCLES_PER_MICROSECOND (SystemCoreClock / 1000000U)
volatile static uint32_t System_ms = 0;
/**
* [url=home.php?mod=space&uid=159083]@brief[/url] 获取单片机自上电以来经过的毫秒数
* @param 无
* @retval 当前系统时钟毫秒数
*/
uint32_t millis(void)
{
return System_ms;
}
/**
* @brief 获取单片机自上电以来经过的微秒数
* @param 无
* @retval 当前系统时钟微秒数
*/
uint32_t micros(void)
{
return (System_ms * 1000 + (SysTick_LoadValue - SysTick->VAL) / CYCLES_PER_MICROSECOND);
}
/******************************************************************************
* @brief
* @param
* @retval
* [url=home.php?mod=space&uid=1020061]@attention[/url]
******************************************************************************/
void SysTick_Init(uint32_t ticks)
{
if(SysTick_Config(SystemCoreClock / ticks))
{
/* Capture error */
while(1);
}
NVIC_SetPriority(SysTick_IRQn, 0);
}
/******************************************************************************
* @brief
* @param
* @retval
* @attention
******************************************************************************/
void SysTick_Handler(void)
{
System_ms ++ ;
if(SysTick_DelayTicks)
{
SysTick_DelayTicks--;
}
}
Step 4. 初始化ms时基并且注册闪灯任务
/* Init SysTick timer 1ms for SysTick_DelayMs */
SysTick_Init(1000);
/*Register task*/
mtmMain.Register(Display_Update, 500); //每500ms闪烁LED灯的任务
Step 5. 在主循环中调用Running函数实现任务调度
while (1)
{
mtmMain.Running(millis());
}
Step 6. 至此任务调度器已经移植完成,为了方便观看结果以验证是否正常工作,另外再移植了 Segger RTT组件
Segger RTT 参考链接:
https://www.freesion.com/article/20081233930/
http://www.elecfans.com/emb/danpianji/202012021403622.html
Step 7. 编译运行,通过串口上位机工具或者 J-Link RTT Viewer 工具查看打印信息,并且可以看到板载LED 每500ms闪烁一次
该多任务管理器同样能被其它平台移植使用,且非常适合用于资源有限但又比较注重程序框架的单片机工程中,在这分享给大家