目录
裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。
-
轮询:周期性地查询各个模块或函数的状态,适用于简单且响应时间要求不高的系统,但无法有效解决复杂函数之间的相互影响问题。
-
前后台:通过将关键任务放在前台处理,而非关键任务放在后台,以确保关键任务的优先执行。这种方式对优先级管理有效,但复杂函数之间的相互影响仍可能存在。
-
定时器驱动:通过定时器周期性地触发任务执行,适合需要定期执行任务的场景,但不能解决函数间相互影响的问题。
-
基于状态机:将系统的各种状态和状态转移定义清晰,通过状态切换来控制系统行为。这种方法理论上可以避免复杂函数之间的直接影响,但在实践中确实需要良好的设计和调试
前面三种方法都无法解决一个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。第4种方法可以解决这个问题,但是实践起来有难度。
1. 轮询模式
可以理解为我们stm32的正常顺序编程,一步一步来,若是前面的条件有延时或者执行时间过长,就会导致后面的条件执行往后推迟。
在main函数中是一个while循环,里面依次调用2个函数,这两个函数相互之间有影响:如果“喂一口饭”太花时间,就会导致迟迟无法“回一个信息”;如果“回一个信息”太花时间,就会导致迟迟无法“喂下一口饭”。
使用轮询模式编写程序看起来很简单,但是要求while循环里调用到的函数要执行得非常快,在复杂场景里反而增加了编程难度。
void main()
{
while (1)
{
喂一口饭();
回一个信息();
}
}
2. 前后台
所谓“前后台”就是使用中断程序。当主程序在执行过程中,若是另一程序触发中断,优先中断的程序进行执行。
会出现问题:中断程序要是执行过长,将会导致迟迟离不开中断,并且要是创建多个中断还会出现抢占优先级的情况。
// 前后台程序
void main()
{
while (1)
{
// 后台程序
喂一口饭();
}
}
// 前台程序
void 滴_中断()
{
回一个信息();
}
在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭”无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致 “喂一口饭”迟迟无法执行。
继续改进,假设小孩吞下饭菜后会发出“啊”的一声,妈妈听到后才会喂下一口饭。喂饭、回复信息都是使用中断函数来处理。示例程序如下:
// 前后台程序
void main()
{
while (1)
{
// 后台程序
}
}
// 前台程序
void 滴_中断()
{
回一个信息();
}
// 前台程序
void 啊_中断()
{
喂一口饭();
}
main函数中的while循环是空的,程序的运行靠中断来驱使。如果电脑声音“滴”、小孩声音“啊”不会同时、相近发出,那么“回一个信息”、“喂一口饭”相互之间没有影响。在不能满足这个前提的情况下,比如“滴”、“啊”同时响起,先“回一个信息”时就会耽误“喂一口饭”,这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。(中断之间抢占优先级)
3. 定时器驱动
定时器驱动模式,是前后台模式的一种,可以按照不用的频率执行各种函数。比如需要每2分钟给小孩喂一口饭,需要每5分钟给同事回复信息。那么就可以启动一个定时器,让它每1分钟产生一次中断,让中断函数在合适的时间调用对应函数。示例代码如下:
// 前后台程序: 定时器驱动
void main()
{
while (1)
{
// 后台程序
}
}
// 前台程序: 每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分钟,那么就会耽误“回一个信息”;反过来也是一样的,如果“回一个信息”很花时间也会影响到“喂一口饭”;这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。
4. 基于状态机
当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式,都会退化到轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:
// 状态机
void main()
{
while (1)
{
喂一口饭();
回一个信息();
}
}
在main函数里,还是使用轮询模式依次调用2个函数。
关键在于这2个函数的内部实现:使用状态机,每次只执行一个状态的代码,减少每次执行的时间,代码如下:
void 喂一口饭(void)
{
static int state = 0;
switch (state)
{
case 0:
{
/* 舀饭 */
/* 进入下一个状态 */
state++;
break;
}
case 1:
{
/* 喂饭 */
/* 进入下一个状态 */
state++;
break;
}
case 2:
{
/* 舀菜 */
/* 进入下一个状态 */
state++;
break;
}
case 3:
{
/* 喂菜 */
/* 恢复到初始状态 */
state = 0;
break;
}
}
}
void 回一个信息(void)
{
static int state = 0;
switch (state)
{
case 0:
{
/* 查看信息 */
/* 进入下一个状态 */
state++;
break;
}
case 1:
{
/* 打字 */
/* 进入下一个状态 */
state++;
break;
}
case 2:
{
/* 发送 */
/* 恢复到初始状态 */
state = 0;
break;
}
}
}
以“喂一口饭”为例,函数内部拆分为4个状态:舀饭、喂饭、舀菜、喂菜。每次执行“喂一口饭”函数时,都只会执行其中的某一状态对应的代码。以前执行一次“喂一口饭”函数可能需要4秒钟,现在可能只需要1秒钟,就降低了对后面“回一个信息”的影响。
同样的,“回一个信息”函数内部也被拆分为3个状态:查看信息、打字、发送。每次执行这个函数时,都只是执行其中一小部分代码,降低了对“喂一口饭”的影响。
使用状态机模式,可以解决裸机程序的难题:假设有A、B两个都很耗时的函数,怎样降低它们相互之间的影响。但是很多场景里,函数A、B并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。所以这并不是最优的解决方法,需要使用多任务系统。