引入
在新手刚刚入门时,通常使用的程序架构为裸机、顺序执行。也就是说把所有的功能放在一个while(1)死循环中然后单片机不断循环执行。有必要的话会加上一些中断用来处理一些紧急事件或者外部的信号,构成经典的前后台系统。但是这样做有什么弊端呢?这是我在我踩过坑后意识到的:
- 有些程序完全不需要频繁执行,比如LED的刷新,按键的检测等。放在死循环中执行对CPU比较浪费。
- 随着功能的增多或者代码变得复杂你会感觉到程序越写越困难,循环中的功能模块有时需要和中断联动,各个模块之间也有可能需要联动起来。即使使用状态机也力不从心…
- 勉勉强强完成了任务,后期的维护也变得相当的麻烦。
踩过坑后的我痛定思痛:准备以后写程序都请出RTOS这个大手子。但是后来我又发现 有时你想要实现的功能刚好介于复杂与不复杂之间…咋理解呢,就是说用顺序执行,复杂了点,用RTOS吧好像也没必要…毕竟移植还是挺麻烦的。当然还有就是RTOS体量过大,有些单片机吃不下,或者吃下了但是也撑的吃不下自己写的代码了属于是小鸟胃这一块,不过目前我还没遇到过,我用的单片机基本上属于大卫戴这一块
时间片轮询法
为了优化上面这些问题,大佬们于是提出了一种基于时间片的裸机开发架构,我们可以利用一个定时器提供心跳,不断的进行计数。然后当定时时间一到,那么就可以开始执行相应的任务了。
Talk is cheap,Show me the code.
首先是.h头文件
#ifndef __OS_H__
#define __OS_H__
//任务状态
typedef enum TaskStatus
{
wait,run,stop
}TaskStatus;//等待运行、正在运行、停止运行
//任务信息块
typedef struct
{
uint16_t TaskTimer; //用于计时
uint16_t TaskRunTime; //每隔多少时间运行一次
TaskStatus Status; //任务状态
void (*FC)(); //任务函数指针
}TaskStruct;
void OSInit(TIM_HandleTypeDef htim);//初始化设置,这里是用的STM32hal库
void OS_IT_RUN(void);//放在提供心跳的定时器中断里运行
void OS_Run(void);//主函数里运行的
#endif
然后是.c
/**
******************************************************************************
* @file : OS.c
* @brief : 任务函数、OS内核
******************************************************************************
*/
#include "OS.h"
/* 任务声明------------------------------------------------------------------*/
void A(void);
void B(void);
void C(void);
/* -------------------------------------------------------------------------*/
/* 任务清单配置---------------------------------------------------------------*/
uint8_t TaskCount = 0;//记录任务数量
TaskStruct TaskList[] = {
{0,5,run,A},
{0,10,wait,B},
{0,3,wait,C},
};//任务清单
/* -------------------------------------------------------------------------*/
/* 任务同步所需公共变量--------------------------------------------------------*/
/* -------------------------------------------------------------------------*/
/* OS 主功能代码--------------------------------------------------------------*/
void OSInit(TIM_HandleTypeDef htim){//可以根据自己的需求重新写
HAL_TIM_Base_Start_IT(&htim); //打开定时器
TaskCount = sizeof(TaskList)/sizeof(TaskList[0]); //利用sizeof计算数组长度的方法可得到任务数量
}
//放在中断中执行
void OS_IT_RUN(void){
uint8_t i;
for(i=0;i<TaskCount;i++){ //遍历所有循环
if(TaskList[i].Status == wait){ //当任务wait状态,执行以下步骤
if(++TaskList[i].TaskTimer >= TaskList[i].TaskRunTime){
//计时,并判断是否到达定时时间
//如果到了定时时间,则将任务挂起,并复位计时(也可以执行完后复位,效果一样)
TaskList[i].TaskTimer = 0;
TaskList[i].Status = run;
}
}
}
}
//放在main函数中执行,自带死循环,用于执行任务
void OS_Run(void){
uint8_t j=0;
while(1){
if(TaskList[j].Status == run){ //判断一个任务是否为run状态
TaskList[j].FC(); //执行该任务函数
TaskList[j].Status = wait ; //取消任务的run状态
}
if(++j>=TaskCount)j=0; //不断循环遍历所有任务
/* 这里可以定义空闲任务 */
}
}
/* -------------------------------------------------------------------------*/
按照分而治之的思想,完全可以把任务函数重新写在一个task.c文件中,这样更加简洁美观。
采用上面的代码,个人认为比为每个功能函数提供一个flag,时间到达后将任务标志为置位。然后在main函数的循环中检查标志位状态(类似状态机)那种方法要方便。避免了一些重复的工作。
什么意思
比如我有三个功能:
- A:5ms执行一次
- B:10ms执行一次
- C:3ms执行一次
我们随机提拔一个定时器作为心跳时钟,说白了就是掐表的嘛。这个定时器一般使用基本定时器性价比高一点。这个定时器每隔1ms就叫一下,我们可以决定ABC在上电时候时是否执行,或者上电后延迟一个自己的任务周期再执行。比如我们设定上电时:仅A执行。如果我们忽略代码的执行时间,那么程序就是这么运行的:
A-1ms-1ms-1ms(C)-1ms-1ms(A)-1ms(C)-1ms-1ms-1ms(C)-1ms(A、B)--------
如果上一个功能模块已经执行完了,但下一个功能模块的定时时间还没到,便会产生空闲时间,这那些1ms后没有括号的就是CPU的空闲段。同样可以像RTOS那样把空闲时间给空闲任务。
注意当前延时了多少时间是一个函数执行完就直接开始计算的,我之前就理解为了一个时间片只有一个函数执行。实际上不是的。
这样的方法有什么不足?
- 首先,像这样的丐版RTOS,实时性并没有真正的RTOS高,不是说执行就执行,会有一些延迟。
- 如果某个任务运行时间超过一个时间片,它可能会一直占用CPU,导致后面的任务无法及时执行,从而影响系统的响应时间。然后一整个就乱了。
但是个人感觉如果实时性要求不高,也无伤大雅🤔
比如时间片为1ms。A运行时间为2ms,每隔4ms执行一次。B运行时间不计,每隔3ms运行一次。
(A1ms-1ms)-1ms(B)-1ms(A1ms-1ms)(B)-1ms-1ms(A1ms([1])-1ms)(B[2])-----
参考文章
- 嵌入式裸机设计思想——时间片轮裸机开发架构+状态机+定时器调度机制_基于状态机制 定时器-CSDN博客遵循 CC 4.0 BY-SA 版权协议
- STM32裸机-时间片任务轮询_时间片轮询-CSDN博客遵循 CC 4.0 BY-SA 版权协议