常见程序框架
注意,本章中的代码大部分都是不完整的,类似于伪代码,如果直接运行是无法运行的,需要自行添加其他内容,关键是用于核心知识讲解。
1 int main()
2 {
3 sys_init();
4 while(1)
5 {
6 update_sensors();
7 make_decision();
8 execute_decision();
9 }
10 return 0;
11 }
这是自动控制系统的经典框架,一般任务已经可以胜任了。
while循环执行一次需要多少时间?
如果其中某个环节耗时非常久会怎么样?
如果需要执行多个对时间有精确时序要求的任务应该怎么办?
任务挑战
控制2个LED灯,其中一个每亮0 . 1s ,灭0 .9秒交替,另一个每亮0 .5s ,灭0 .5s交 替?
第一个灯我们可以这样
1 void f1()
2 {
3 digitalWrite(led1,HIGH);
4 delay(100);
5 digitalWrite(led1,LOW);
6 delay(900);
7 }
第二个我们这样
1 void f2()
2 {
3 digitalWrite(led2,HIGH);
4 delay(500);
5 digitalWrite(led2,LOW);
6 delay(500);
7 }
综合起来怎么办呢?
也不是没有办法,经过一段时间的思考,我们可以想到这样的方式:
1 void f3()
2 {
3 digitalWrite(led1,HIGH);
4 digitalWrite(led2,HIGH);
5 delay(100);
6 digitalWrite(led1,LOW);
7 delay(400);
8 digitalWrite(led2,LOW);
9 delay(500);
10 }
我们分别将led1和led2的时序图画出来,发现是对的,但是上面的f3函数存在 很大的问题:
1. 改一发而动全身,可能因为某些原因,需要led2的运行规律发生变 化,那么所有的led的延时都要重新计算。
2. 如果要控制的对象更多的话,这个延时就很难把握了,函数会异常 的复杂。
经过我们认真的思考,会发现问题出在delay()函数上,因为每个任务的delay 之间会影响,这样就导致了整个运行时间发生变化,能否减少这种影响呢?我 们来看另外一种不同的实现办法:
1 void f4(int frame)
2 {
3 int time_in_seconds=frame%1000;
4 if(time_in_seconds<100)
5 {
6 digitalWrite(led1,HIGH);
7 }
8 else
9 {
10 digitalWrite(led1,LOW);
11 }
12 }
13 void f5(int frame)
14 {
15 int time_in_seconds=frame%1000;
16 if(time_in_seconds<500)
17 {
18 digitalWrite(led2,HIGH);
19 }
20 else
21 {
22 digitalWrite(led2,LOW);
23 }
24 }
25 long frame=0;
26 void loop()
27 {
28 frame++;
29 f4(frame);
30 f5(frame);
31 delay(1);
32 }
上面的代码中,除了loop函数外,其他地方没有delay ,所以几乎是微秒级完 成,那么我们的全局变量frame实际上就实现了每1毫秒自增1 ,有了绝对时间 的概念,f4和f5就可以解耦了。而且我们的loop的响应速度几乎能达到1毫秒。
在上面的代码中,f4 ,f5实现功能简单,但是代码却不少,如果需要,我们也 可以优化如下,但这这样代码可读性就不如上面高了。
1 long frame=0;
2 void loop()
3 {
4 frame++;
5 frame%1000<100?digitalWrite(led1,HIGH):digitalWrite(led1,LOW);
6 frame%1000<500?digitalWrite(led2,HIGH):digitalWrite(led2,LOW);
7 delay(1);
8 }
上面的代码利用了条件运算符来进行代码的缩减,代码量迅速减小,但是是否 已经达到最小代码量了呢?我们再深入思考,会发现digitalWrite函数的第二个 参数实际上就是前面表达式的值,所以还能优化如下:
1 long frame=0;
2 void loop()
3 {
4 frame++;
5 digitalWrite(led1,frame%1000<100);
6 digitalWrite(led2,frame%1000<500);
7 delay(1);
8 }
代码明显比上面的要更精简,但是可读性下降了,但是随着自己见识的增加, 这些小tricks可以说是非常容易在其他人代码里面看到的。通过上面的迭代例 子,我是想给大家传达一个思想,要写好代码不仅仅是实现功能就行,还 要思考这样是否是最佳的解决方案了,通用性、 易读性、编码速度、运 行速度、 内存占用等的权衡是否达到了比较优的方案,通过自己不断的思 考以及网络上的学习, 自己水平就会慢慢提升,每过段时间再回顾以前自己的 代码,就能发现自己的变化。慢慢也能够分辨出来网络上各种代码编写者的水平高低。
最大的就是通用性问题,因为点灯这个任务比较简单,如果任务比较复杂,而 且有很多种的话,就会出现问题。比如我们考虑下面的代码:
1 long frame=0;
2 void loop()
3 {
4 frame++;
5 f1(frame);
6 f2(frame);
7 f3(frame);
8 f4(frame);
9 f5(frame);
10 delay(1);
11 }
这个框架中,代码中f1-f5可以自己根据时间来实现不同的功能,扩展起来也比 较方便,唯独的问题就是整个loop的时间实际上是f1-f5全部执行时间加上1毫 秒还有frame++的时间,因为任务较多情况下,f1-f5的总执行时间跟1毫秒比起 来就可能不是那么容易忽略了,这样就造成frame的自增的时间实际上并不是1 毫秒,就会导致系统并未达到我们当初的设计要求。
当然有,我们分几个部分来考虑这个问题:
1. 将delay(1)改为delay(10)
这样frame的自增时间变成10毫秒,导致任务所占的比例减少,及时 准确性就会提高。
2. 不用delay ,将frame++放到硬件定时器当中
因为硬件计数器是采用硬件中断来完成的,并不受到f1-f5的影响,
这样loop代码中就只有f1-f5的函数体而已,将frame声明成全局变量
3. 让f1-f5依此执行,每次loop只执行其中一个,下次就换成下一个执 行
这样loop的总时间就不是f1-f5的和,而是其中最长时间,代码如下:
1 long frame=0;
2 void timer_10ms()
3 {
4 frame++;
5 }
6 int state=0;
7 void loop()
8 {
9 switch(state)
10 {
11 case 0:f1(frame);break;
12 case 1:f2(frame);break;
13 case 2:f3(frame);break;
14 case 3:f4(frame);break;
15 case 4:f5(frame);break;
16 }
17 state++;
18 if(state>4)
19 {
20 state=0;
21 }
22 }
经过上面的优化,我们的代码框架实际上就已经具备调度器的雏形了,因为系 统能够实现在多个任务中切换,因为每个任务执行的很快,在外部看来就像是 多个任务同时在运行。
上面的代码在一般的项目上使用已经足够了,可能的问题就是如果添加任务, 要修改loop中的switch语句,还要修改下面的state的极限值,当工程比较小的 时候,这个影响是比较小的,还有的就是每个任务执行频率是相同的,要实现 不同频率执行就要在函数内部通过if语句来实现。
1 void msg_update() {
2 do_something();
3 }
4 void screen_update() {
5 do_something();
6 }
7 void key_scan() {
8 do_something();
9 }
10 sch_init_task();
11 sch_add_task(msg_update, 0, 11); // 110ms
12 sch_add_task(screen_update, 0, 50);
13 sch_add_task(key_scan, 0, 1);
14 while (1) {
15 sch_run_tasks();
16 }
main .c 伪代码
可以看到上面的代码非常的容易理解,而且扩展起来很方便,如何才能实现上 面的效果呢?
xxx .h 部分头文件
1 #define SCH_MAX_TASKS (4)
2 struct __attribute__ ((__packed__)) sTask
3 {
4 void (* pTask)(void);
5 u16 Delay;
6 u16 Period;
7 u8 RunMe;
8 };
xxx .c 合作式调度器伪代码
1 /*
2 * schedule.c, for system control.
3 * change log
4 * first commit
5 */
6 #include "public.h"
7 struct sTask sch_tasks[SCH_MAX_TASKS];
8 void sch_run_tasks(void) {
9 s16 index=SCH_MAX_TASKS;
10 while(index >0)
11 {
12 index-- ;
13 if (sch_tasks[index ].RunMe > 0)
14 {
15 (*sch_tasks[index ].pTask)(); // Run the task
16 sch_tasks[index ].RunMe = 0; // Reset /reduce RunMe flag
17 if (sch_tasks[index].Period) {
18 sch_tasks[index ].Delay =sch_tasks[index ].Period;
19 }
20 else if (sch_tasks[index ].Period == 0)
21 {
22 sch_delete_task(index);
23 }
24 break;
25 }
26 }
27 sch_goto_sleep();
28 }
29 u8 sch_add_task(void (*pFunction)(), const u16 delay, const u16 period) {
30 u8 index = 0;
31 while ((sch_tasks[index].pTask != NULL) &&(index < SCH_MAX_TASKS)) {
32 index++;
33 }
34 if (index == SCH_MAX_TASKS) {
35 return SCH_MAX_TASKS;
36 }
37 sch_tasks[index].pTask = pFunction;
38 sch_tasks[index].Delay = delay;
39 sch_tasks[index].Period = period;
40 sch_tasks[index].RunMe = 0;
41 return index; // return position of task (to allow later deletion)
42 }
43 u8 sch_delete_task(const u8 index) {
44 u8 ret;
45 if (sch_tasks[index].pTask == 0) {
46 ret = ERROR;
47 } else {
48 ret = SUCCESS;
49 }
50 sch_tasks[index].pTask = 0x0000;
51 sch_tasks[index].Delay = 0;
52 sch_tasks[index].Period = 0;
53 sch_tasks[index].RunMe = 0;
54 return ret;
55 }
56 u8 sch_init_task(void) {
57 s16 index = SCH_MAX_TASKS;
58 while(index > 0)
59 {
60 index-- ;
61 sch_tasks[index].pTask = 0x0000;
62 sch_tasks[index].Delay = 0;
63 sch_tasks[index].Period = 0;
64 sch_tasks[index].RunMe = 0;
65 }
66 return SUCCESS;
67 }
68
69 inline void sch_update(void) {
70 s16 index = SCH_MAX_TASKS;
71 while(index > 0)
72 {
73 index-- ;
74 if (sch_tasks[index ].pTask) {
75 if (sch_tasks[index].Delay == 0) {
76 sch_tasks[index].RunMe = 1; // Set the run flag
77 } else {
78 sch_tasks[index].Delay -= 1;
79 }
80 }
81 }
82 }
yyy.c 定时器中断10ms触发
定时器中断
__interrupt void Timer(void) {
sch_update();
}
上面代码中用到了函数指针,这样在使用的时候就可以运行我们自定义的函数 了,函数指针算是c语言中比较高级的内容了,不熟悉的请自行查阅。上面实 际上已经是一个完整的合作式调度器的代码了,可以非常方便的移植到各种 微控制器上。
上面的代码就是个完整的合作式调度器代码,调度器分成合作式和抢占式,
合作式就是各个任务不会打断其它任务,依次调度,抢占式就是各个任务可能会被其它任务中断。其中合作式调度器实现起来简单,内存占用小,甚至可以 运行在89c51这类资源非常少的微控制器上,应该是以后项目上的首选。但是 也有一些局限性,就是每个单独任务的执行时间不能超过调度最大时间,不 然系统响应时间就没法保证,解决办法就是将耗时的任务拆分成一系列子任务 来运行。详细的情况要自己查阅相关书籍来进行更深入的学习。