摘要:不知道大家有没有这种感觉,就是感觉可以玩单片机,各个功能模块都可以驱动。 但是,如果让你写一套完整的代码,就完全没有逻辑和框架了。 开始写作吧。 ! 从东抄到西,编程水平还处于较低水平。 那么,如何提高你的编程技能呢?
学习一个好的编程框架或者一个编程思想可能会受益终生! 比如模块化编程、框架编程、状态机编程等都是不错的框架。
我今天讲的是状态机编程。 由于文章比较长,大家慢慢欣赏吧。 那么,状态机是一个什么样的东西呢?
状态机有五个元素,即状态、转换、事件、动作和保护。
什么是状态机?
状态机是这样的:状态机有五个元素,即状态、转换、事件、动作和保护。
状态:系统在某一时刻的稳定工作状态。 系统在整个工作周期中可能有多种状态。 例如,电机具有正转、反转、堵转三种状态。
状态机需要从状态集中选择一个状态作为初始状态。
迁移:系统从一种状态迁移到另一种状态的过程称为迁移。 迁移不会自动发生,需要对系统施加外部影响。 堵转的电机无法自行启动,因此必须对其通电。
事件:在某个时刻发生对系统有意义的事情。 状态机之所以发生状态转换,是因为事件的发生。 对于电机来说,加正电压、加负电压、切断电源都是事件。
动作:在状态机的迁移过程中,状态机执行一些其他行为。 这些行为都是行动。 动作是状态机对事件的响应。 对堵转电机施加正电压,电机将从堵转状态转为正转状态,同时启动电机。 这个启动过程可以看作是一个动作,即对上电事件的响应。
条件:状态机不响应事件。 对于事件,状态机必须满足某些条件才能发生状态转换。 我们以电机处于堵转状态为例。 虽然关闭并通电,但如果供电线路出现问题,电机仍然无法启动。
仅仅谈论概念太空洞了。 最后一个小例子:一个单片机,一个按钮,两个LED灯(记为L1和L2),一个人就够了!
规则说明:
1、L1L2状态转换顺序OFF/OFF—>ON/OFF—>ON/ON—>OFF/ON—>OFF/OFF
2. 通过按键控制L1L2 的状态。 每次状态转换需要连续按下按钮5次。
3. L1L2 OFF/OFF 初始状态
图1
下面的程序是根据功能需求编写的代码。
节目列表List1:
void main(void)
{
sys_init();
led_off(LED1);
led_off(LED2);
g_stFSM.u8LedStat = LS_OFFOFF;
g_stFSM.u8KeyCnt = 0;
while(1)
{
if(test_key()==TRUE)
{
fsm_active();
}
else
{
; /*idle code*/
}
}
}
void fsm_active(void)
{
if(g_stFSM.u8KeyCnt > 3) /*击键是否满 5 次*/
{
switch(g_stFSM.u8LedStat)
{
case LS_OFFOFF:
led_on(LED1); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_ONOFF; /*状态迁移*/
break;
case LS_ONOFF:
led_on(LED2); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_ONON; /*状态迁移*/
break;
case LS_ONON:
led_off(LED1); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFON; /*状态迁移*/
break;
case LS_OFFON:
led_off(LED2); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFOFF; /*状态迁移*/
break;
default: /*非法状态*/
led_off(LED1);
led_off(LED2);
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFOFF; /*恢复初始状态*/
break;
}
}
else
{
g_stFSM.u8KeyCnt++; /*状态不迁移,仅记录击键次数*/
}
}
事实上,在状态机编程中,正确的顺序应该是先有状态转移图,然后才是程序。 应根据设计的状态图编写程序。 不过考虑到有些小朋友可能会觉得代码比转换图更友好,所以我还是把程序放在第一位。
该状态转换图是使用 UML(统一建模语言)的语法元素绘制的。 语法不是很标准,但是足以说明问题了。
图2 按钮控制流灯状态转换图
圆角矩形代表状态机的各个状态,里面标注了状态的名称。
带箭头的直线或弧线表示从初始状态开始到次要状态结束的状态转换。
图中的文字内容是对迁移的描述,格式为:事件[条件]/动作列表(后两项可选)。
“事件[条件]/动作列表”的含义是:如果在某个状态下发生“事件”,则状态机
如果满足“[条件]”,则必须执行此状态转移,并且必须生成一系列“动作”以响应该事件。 在这个例子中,我使用“KEY”来表示击键事件。
图中有一个实心黑点,代表状态机工作前所处的未知状态。 在运行之前,必须将状态机从该状态强制迁移到初始状态。 这个迁移可以有一个动作列表(如图1),但不需要事件触发。
图中还有一个包含实心黑点的圆圈,代表状态机生命周期的结束。 本例中的状态机是无限的,因此没有指向圆圈的状态。
这个状态转换图我就不多说了。 相信你根据上面的代码就很容易理解了。 现在我们来谈谈程序列表List1。
我们先看一下fsm_active()函数。 g_stFSM.u8KeyCnt = 0; 该语句在 switch-case 中出现 5 次。 前 4 次显示为每个状态迁移的操作。 从简化代码和提高效率的角度来看,我们完全可以将这5次合并为1次,放在switch-case语句之前。 两者的效果是完全一样的。 代码之所以如此冗长,是为了清楚地表明每个状态转换中的所有动作细节都与图2状态转换图所表达的意图完全一致。
再看一下 g_stFSM 状态机结构体变量。 它有两个成员:u8LedStat 和 u8KeyCnt。 用这种结构来做状态机显得有点麻烦。 我们可以只使用像 u8LedStat 这样的整数变量来制作状态机吗?
当然! 我们将图2中的4个状态分别拆成5个小状态,这样这个状态机也可以用20个状态来实现,并且只需要一个unsigned char类型变量就足够了,并且每次击键都会触发状态迁移可以改变状态LED 灯每 5 次亮一次。 从外观上看,两种方法的效果是完全一样的。
假设我将功能需求从连续 5 次击键改变 L1L2 的状态更改为连续 100 次击键改变 L1L2 的状态。 在这种情况下,第二种方法需要4X100=400个状态! 而且,函数fsm_active()中的switch-case语句必须有400个case。 有没有办法写这样的程序? !
同样的功能改变,如果使用g_stFSM结构体来实现状态机,函数fsm_active()只需要将if(g_stFSM.u8KeyCnt>3)改为if(g_stFSM.u8KeyCnt > 98)即可!
g_stFSM结构体的两个成员中,u8LedStat可以看作是质变因子,相当于主变量; u8KeyCnt可以看作是一个量变因子,相当于辅助变量。 量变因素的逐渐积累,会引发质变因素的变化。
像 g_stFSM 这样的状态机称为扩展状态机。 我不知道如何使用业内正式的中文术语,所以我不得不把英文短语移过来。
2.状态机编程的优点
说到这里,大家大概都明白了什么是状态机,以及如何编写基于状态机的程序了。 那么使用状态机方法来编写单片机程序有什么好处呢?
(1)提高CPU使用效率
换句话说,每当我看到一个充满delay_ms()的程序时,我就会感到恶心。 几十ms、几十ms的软件延迟是对CPU资源的巨大浪费。 宝贵的CPU时间被浪费在NOP上。 根据指示。 那种原地踏步只是等待引脚电平跳变或者串口数据的程序也让我很纠结。 如果这件事永远不会发生,你想等到世界末日吗?
通过将程序变成状态机,这种情况将会得到显着改善。 程序只需要使用全局变量来记录工作状态,然后就可以转身做其他工作了。 当然,完成这些任务后,你应该检查一下你的工作状态是否有变化。 只要目标事件(定时未到、电平未跳变、串口数据未完成)没有发生,工作状态就不会改变,程序就会不断重复“查询-做某事”其他-查询-做其他事情”。 循环,这样CPU就不会闲置。
程序列表List3中,if{}else{}语句中else下面的内容(代码中没有添加,只是注释表示/*空闲代码*/)就是上面提到的“其他工作”。
这种处理方式的本质是在程序等待事件的过程中每隔一段时间插入一些有意义的工作,让CPU不至于无谓的等待。
(2)逻辑完整性
我认为逻辑完备性是状态机编程的最大优势。
不知道大家有没有用C语言写过一个小小的计算器程序。 很早之前写的,测试的时候,很糟糕! 当我有规律地输入计算时,程序可以得到正确的计算结果,但如果我故意输入数字和运算符号的随机组合,程序总是得到莫名其妙的结果。
后来我尝试模拟一下程序的工作过程。 正确的计算公式清晰,过程顺利。 但当我遇到不规则的配方时,我走路就感觉头晕。 那么多的flag,那么多的变量不停地变化,最后就无法再进行分析了。
时间长了,我了解了状态机,然后我突然意识到,当时的程序存在逻辑漏洞。 如果把这个计算器程序看成是一个反应式系统,那么一个数字或运算符就可以看成是一个事件,一次计算就是一组事件组合。
对于逻辑完整的反应式系统,无论何种事件组合,系统都能正确处理事件,并且系统本身的工作状态始终处于可知可控状态。 另一方面,如果系统的逻辑功能不完整,在特定事件的某些组合的驱动下,系统将进入不可知、不可控的状态,这与设计者的意图背道而驰。
状态机可以解决逻辑完整性问题。
状态机是一种以系统状态为中心、以事件为变量的设计方法。 它侧重于各个状态的特点以及状态之间的相互转换关系。 状态转移恰恰是由事件引起的,因此在研究特定状态时,我们自然会考虑任何事件对该状态的影响。 这样,每一个状态发生的每一个事件都会被我们考虑到,不会留下任何逻辑漏洞。
这对每个人来说可能听起来很空洞,但实践会带来真知。 有一天,如果你真的想设计一个逻辑复杂的程序,
我保证你会说:哇! 状态机真的很有用!
(3)清晰的程序结构
使用状态机编写的程序结构非常清晰。
对于程序员来说最痛苦的事情就是阅读别人写的代码。 如果代码不是很规范,手头又没有流程图,读一遍代码就会让人头晕。 只有一遍又一遍地跟着程序走,多次之后才能模糊地了解程序的大致工作流程。 要是有流程图就更好了,但是如果程序比较大的话,流程图就不会画的很详细,很多详细的流程还是需要从代码中去理解。
相比之下,用状态机编写的程序要好得多。 拿一张标准的UML状态转换图,加上一些简洁的文字描述,你就能一目了然地看到程序中的所有元素。 程序中有什么状态,会发生什么事件,状态机如何响应,响应后跳转到哪个状态都非常清楚,甚至很多动作细节都可以在状态转移图中找到。 毫不夸张地说,有了UML状态转换图,就不需要再写程序流程图了。
套用一句广告语:谁用了就知道!