1. 引入
  2. 时间片轮询法
    1. Talk is cheap,Show me the code.
    2. 什么意思
  3. 这样的方法有什么不足?
  4. 参考文章

单片机(伪)多任务处理:时间片轮询法

版权信息

本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。


引入

在新手刚刚入门时,通常使用的程序架构为裸机、顺序执行。也就是说把所有的功能放在一个while(1)死循环中然后单片机不断循环执行。有必要的话会加上一些中断用来处理一些紧急事件或者外部的信号,构成经典的前后台系统。但是这样做有什么弊端呢?这是我在我踩过坑后意识到的:

  1. 有些程序完全不需要频繁执行,比如LED的刷新,按键的检测等。放在死循环中执行对CPU比较浪费。
  2. 随着功能的增多或者代码变得复杂你会感觉到程序越写越困难,循环中的功能模块有时需要和中断联动,各个模块之间也有可能需要联动起来。即使使用状态机也力不从心…
  3. 勉勉强强完成了任务,后期的维护也变得相当的麻烦。

踩过坑后的我痛定思痛:准备以后写程序都请出RTOS这个大手子。但是后来我又发现 有时你想要实现的功能刚好介于复杂与不复杂之间…咋理解呢,就是说用顺序执行,复杂了点,用RTOS吧好像也没必要…毕竟移植还是挺麻烦的。当然还有就是RTOS体量过大,有些单片机吃不下,或者吃下了但是也撑的吃不下自己写的代码了属于是小鸟胃这一块,不过目前我还没遇到过,我用的单片机基本上属于大卫戴这一块

时间片轮询法

为了优化上面这些问题,大佬们于是提出了一种基于时间片的裸机开发架构,我们可以利用一个定时器提供心跳,不断的进行计数。然后当定时时间一到,那么就可以开始执行相应的任务了。

Talk is cheap,Show me the code.

首先是.h头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**

  ******************************************************************************

  * @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那样把空闲时间给空闲任务。
注意当前延时了多少时间是一个函数执行完就直接开始计算的,我之前就理解为了一个时间片只有一个函数执行。实际上不是的。

这样的方法有什么不足?

  1. 首先,像这样的丐版RTOS,实时性并没有真正的RTOS高,不是说执行就执行,会有一些延迟。
  2. 如果某个任务运行时间超过一个时间片,它可能会一直占用CPU,导致后面的任务无法及时执行,从而影响系统的响应时间。然后一整个就乱了。 但是个人感觉如果实时性要求不高,也无伤大雅🤔

比如时间片为1ms。A运行时间为2ms,每隔4ms执行一次。B运行时间不计,每隔3ms运行一次。

(A1ms-1ms)-1ms(B)-1ms(A1ms-1ms)(B)-1ms-1ms(A1ms([1])-1ms)(B[2])-----

参考文章


  1. 这里该执行B了,但由于A抢占CPU,无法执行。 ↩︎

  2. 这次B实际上隔了4ms才执行 ↩︎