跳到主要内容

裸机程序设计模式与缺陷

裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。第4种方法可以解决这个问题,但是实践起来有难度。

假设一位职场妈妈需要同时解决2个问题:给小孩喂饭、回复工作信息,场景如图2.1所示,后面将会演示各类模式下如何写程序:

. 使用职场妈妈类比裸机和RTOS

轮询模式​

示例代码如下:

// 经典单片机程序: 轮询

void main()

{

while (1)

{

喂一口饭();

回一个信息();

}

}

main函数中是一个while循环,里面依次调用2个函数,这两个函数相互之间有影响:如果“喂一口饭”太花时间,就会导致迟迟无法“回一个信息”;如果“回一个信息”太花时间,就会导致迟迟无法“喂下一口饭”。

使用轮询模式编写程序看起来很简单,但是要求while循环里调用到的函数要执行得非常快,在复杂场景里反而增加了编程难度。

前后台​

所谓“前后台”就是使用中断程序。假设收到同事发来的信息时,电脑会发出“滴”的一声,这时候妈妈才需要去回复信息。示例程序如下:

// 前后台程序

void main()

{

while (1)

{

// 后台程序

喂一口饭();

}

}

10

// 前台程序

void 滴_中断()

{

回一个信息();

}

第2行的main函数里while循环的代码是后台程序,平时都是while循环在运行;

当同事发来信息,电脑发出“滴”的一声,触发了中断。妈妈暂停喂饭,去执行“滴_中断”给同事回复信息;

在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭”无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致 “喂一口饭”迟迟无法执行。

继续改进,假设小孩吞下饭菜后会发出“啊”的一声,妈妈听到后才会喂下一口饭。喂饭、回复信息都是使用中断函数来处理。示例程序如下:

// 前后台程序

void main()

{

while (1)

{

// 后台程序

}

}

09

// 前台程序

void 滴_中断()

{

回一个信息();

}

15

// 前台程序

void 啊_中断()

{

喂一口饭();

}

main函数中的while循环是空的,程序的运行靠中断来驱使。如果电脑声音“滴”、小孩声音“啊”不会同时、相近发出,那么“回一个信息”、“喂一口饭”相互之间没有影响。在不能满足这个前提的情况下,比如“滴”、“啊”同时响起,先“回一个信息”时就会耽误“喂一口饭”,这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。

定时器驱动​

定时器驱动模式,是前后台模式的一种,可以按照不同的频率执行各种函数。比如需要每2分钟给小孩喂一口饭,需要每5分钟给同事回复信息。那么就可以启动一个定时器,让它每1分钟产生一次中断,让中断函数在合适的时间调用对应函数。示例代码如下:

// 前后台程序: 定时器驱动

void main()

{

while (1)

{

// 后台程序

}

}

09

// 前台程序: 每1分钟触发一次中断

void 定时器_中断()

{

static int cnt = 0;

cnt++;

if (cnt % 2 == 0)

{

喂一口饭();

}

else if (cnt % 5 == 0)

{

回一个信息();

}

}

main函数中的while循环是空的,程序的运行靠定时器中断来驱使。

定时器中断每1分钟发生一次,在中断函数里让cnt变量累加(代码第14行)。

第15行:进行求模运算,如果对2取模为0,就“喂一口饭”。这相当于每发生2次中断就“喂一口饭”。

第19行:进行求模运算,如果对5取模为0,就“回一个信息”。这相当于每发生5次中断就“回一个信息”。

这种模式适合调用周期性的函数,并且每一个函数执行的时间不能超过一个定时器周期。如果“喂一口饭”很花时间,比如长达10分钟,那么就会耽误“回一个信息”;反过来也是一样的,如果“回一个信息”很花时间也会影响到“喂一口饭”;这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。

基于状态机​

当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式,都会退化到轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:

// 状态机

void main()

{

while (1)

{

喂一口饭();

回一个信息();

}

}

在main函数里,还是使用轮询模式依次调用2个函数。

关键在于这2个函数的内部实现:使用状态机,每次只执行一个状态的代码,减少每次执行的时间,代码如下:

void 喂一口饭(void)

{

static int state = 0;

switch (state)

{

case 0:

{

08 /* 舀饭 */

09 /* 进入下一个状态 */

state++;

break;

}

case 1:

{

15 /* 喂饭 */

16 /* 进入下一个状态 */

state++;

break;

}

case 2:

{

22 /* 舀菜 */

23 /* 进入下一个状态 */

state++;

break;

}

case 3:

{

29 /* 喂菜 */

30 /* 恢复到初始状态 */

state = 0;

break;

}

}

}

36

void 回一个信息(void)

{

static int state = 0;

40

switch (state)

{

case 0:

{

45 /* 查看信息 */

46 /* 进入下一个状态 */

state++;

break;

}

case 1:

{

52 /* 打字 */

53 /* 进入下一个状态 */

state++;

break;

}

case 2:

{

59 /* 发送 */

60 /* 恢复到初始状态 */

state = 0;

break;

}

}

}

以“喂一口饭”为例,函数内部拆分为4个状态:舀饭、喂饭、舀菜、喂菜。每次执行“喂一口饭”函数时,都只会执行其中的某一状态对应的代码。以前执行一次“喂一口饭”函数可能需要4秒钟,现在可能只需要1秒钟,就降低了对后面“回一个信息”的影响。

同样的,“回一个信息”函数内部也被拆分为3个状态:查看信息、打字、发送。每次执行这个函数时,都只是执行其中一小部分代码,降低了对“喂一口饭”的影响。

裸机程序的缺陷​

使用裸机程序的缺陷,可以罗列如下:

① 无法彻底解决任务之间相互影响的问题

② 无法让高优先级的任务立刻运行

③ 效率问题

对于缺陷1,使用状态机模式可以降低任务相互之间的影响。但是很多场景里,函数A、B并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。

缺陷2的示例代码如下:

while (1)

{

if (A_Is_Ready())

A();

If (B_Is_Ready())

B();

}

假设B函数优先级更高,当B函数能运行时我们想让它“立刻”能运行。当函数A正在运行的过程中,B_Is_Ready的条件满足了,B函数也无法“立刻”运行。

缺陷3:对于上述代码,假设“A_Is_Ready”函数很少为真,你也需要次次进行判断。这导致效率降低。