版权声明 · 版权所有
本文 © AUTHOR_NAME year。保留所有权利。
许可:LICENSE_NAME
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
作者说明 · 学习记录
本站作为作者的学习记录站,不保证文章内容严谨或完全正确。
作者:GoDm@
用途:本文为作者的学习记录与实践笔记,用于记录学习过程、思考与示例代码(如有)。
欢迎朋友指正文章中的错误,联系邮箱:god_mao@foxmail.com
版权信息
:::warning
本文章为博主原创文章。遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
:::
引入
在新手刚刚入门时,通常使用的程序架构为裸机、顺序执行。也就是说把所有的功能放在一个while(1)
死循环中然后单片机不断循环执行。有必要的话会加上一些中断用来处理一些紧急事件或者外部的信号,构成经典的前后台系统。但是这样做有什么弊端呢?这是我在我踩过坑后意识到的:
- 有些程序完全不需要频繁执行,比如LED的刷新,按键的检测等。放在死循环中执行对CPU比较浪费。
- 随着功能的增多或者代码变得复杂你会感觉到程序越写越困难,循环中的功能模块有时需要和中断联动,各个模块之间也有可能需要联动起来。即使使用状态机也力不从心…
- 勉勉强强完成了任务,后期的维护也变得相当的麻烦。
踩过坑后的我痛定思痛:准备以后写程序都请出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);
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
|
#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},
};
void OSInit(TIM_HandleTypeDef htim){
HAL_TIM_Base_Start_IT(&htim);
TaskCount = sizeof(TaskList)/sizeof(TaskList[0]);
}
void OS_IT_RUN(void){
uint8_t i;
for(i=0;i<TaskCount;i++){
if(TaskList[i].Status == wait){
if(++TaskList[i].TaskTimer >= TaskList[i].TaskRunTime){ TaskList[i].TaskTimer = 0;
TaskList[i].Status = run; }
}
}
}
void OS_Run(void){
uint8_t j=0;
while(1){
if(TaskList[j].Status == run){
TaskList[j].FC();
TaskList[j].Status = wait ;
}
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()-1ms)(B)-----
参考文章