引言
在嵌入式单片机编程的广阔领域中,状态机宛如一颗璀璨的明星,占据着举足轻重的地位。它不仅是一 种强大的编程工具,更是一种独特的编程思想,广泛应用于各种复杂系统的开发中。对于51单片机而 言,状态机的运用能够极大地提升程序的效率、可读性和可维护性,使开发者能够更加轻松地应对各种 复杂的任务。 想象一下,当你面对一个需要处理多个复杂状态和事件的项目时,传统的编程方式可能会让你陷入代码 的迷宫,难以理清头绪。而状态机则像是一张清晰的地图,能够帮助你有条不紊地规划程序的流程,让 每一个状态和事件都有明确的处理方式。无论是简单的按键控制,还是复杂的通信协议实现,状态机都 能发挥出它的强大威力。
状态机基本概念
状态机的定义与要素
状态机,全称为有限状态机(Finite State Machine,FSM),是一种用于描述系统在有限个状态之间 进行转换的数学模型。它主要由五个关键要素构成:状态(state)、迁移(transition)、事件 (event)、动作(action)和条件(guard)。
状态(state):指系统在某一时刻所存在的稳定工作情况。一个系统在整个工作周期中可能拥有多个 不同的状态。例如,一部电动机通常具有正转、反转和停转这三种状态。在状态机的设计中,需要从 所有可能的状态中选取一个作为初始状态。
迁移(transition):表示系统从一个状态转移到另一个状态的过程。这种转移并非自动发生,而是需 要外界对系统施加一定的影响。以停转的电动机为例,它自己不会自动转动起来,必须通过上电这一 外部操作才能使其开始运转。
事件(event):是指某一时刻发生的对系统有意义的事情。状态机之所以会发生状态迁移,正是因为 出现了特定的事件。对于电动机来说,加正电压、加负电压和断电等操作就是引发状态迁移的事件。
动作(action):在状态机的迁移过程中,系统会做出一些其他的行为,这些行为就是动作。动作是状 态机对事件的响应。当给停转的电动机加上正电压时,电动机从停转状态迁移到正转状态,同时启动 电机的这个过程就可以看作是动作,它是对上电事件的具体响应。
条件(guard):状态机对事件并非有求必应,即使出现了事件,状态机还需要满足一定的条件才能发 生状态迁移。还是以停转状态的电动机为例,即使合闸上电了,但如果供电线路存在问题,电动机仍 然无法转动起来。
状态机的表示方法
状态机可以通过多种方式进行表示,其中最常见的是状态转移图和二维表格。
状态转移图:使用圆圈来表示状态,圆圈中的文字或数字代表该状态的名称或编码。状态转移的方向 用箭头表示,箭头旁边标注的文字则是转移条件。对于梅里状态图,在箭头旁通常用“输入/输出”的 格式来表示转移条件以及满足该条件下的输出;而对于摩尔状态机,常将输出放置在状态圆圈中。例 如,当 k = 0 时,状态从 a0 转移到 a1 ;若 k0 = 1 ,则从状态 a1 转移到 a2 ;若 reset = 0 ,无论处于何种状态,都将转移到 a0 状态。
二维表格:一般将当前状态号写在横行上,将事件写在纵列上。表格中的交点表示系统在某个状态下 对特定事件的响应,包括输出动作和状态迁移。例如,在 s0 状态下,如果发生 e0 事件,那么就执 行 a0 动作,并保持状态不变;如果发生 e1 事件,那么就执行 a1 动作,并将状态转移到 s1 态。
梅里状态机与摩尔状态机
根据状态机的输出信号是否与电路的输入有关,可以将状态机分为梅里(Mealy)型状态机和摩尔 (Moore)型状态机。
梅里型状态机:电路的输出信号不仅与电路当前状态有关,还与电路的输入有关,即次态 = f(现状,输 入),输出 = f(现状,输入)。这种状态机对输入的反应更为迅速,能够在相同的周期内做出响应,无需 等待时钟。例如,在状态 B 时,如果输入为 1 ,则直接以组合电路输出 z = 1 ,不需要等到下一 个有效沿到来。
摩尔型状态机:电路的输出仅仅与各触发器的状态有关,不受电路输入信号的影响或无输入,即次态 = f(现状,输入),输出 = f(现状)。摩尔状态机的输出在时钟边沿变化,总是在一个周期后才会改变,因 此相对更加安全。例如,要想输出 z = 1 ,必须在寄存器中的两个 1 都输入进去后才可以,并且输 出 z = 1 会在下一个有效沿到来的时候被赋值。
51单片机状态机实现步骤
定义状态
在51单片机中实现状态机,首先需要确定系统可能存在的所有状态,并为每个状态赋予唯一的标识。这 些状态可以是简单的基本状态,也可以是包含多个子状态序列的复合状态。例如,在一个按键处理程序 中,按键可能存在释放、抖动、闭合、抖动和重新释放等状态。我们可以使用枚举类型来定义这些状 态,如下所示:
// 按键状态枚举
typedef enum {
RELEASE = 0, // 松开状态
PRESS = 1, // 短按状态
LONG_PRESS = 2, // 长按状态
PRESS_WAIT = 3, // 按下等待状态(消抖)
RELEASE_WAIT = 4, // 松开等待状态(消抖)
START = 5
} KeyState;
定义输入
识别影响状态转移的外部信号,这些信号通常是各种传感器的输入或者用户的操作。例如,在按键控制 程序中,按键的按下和松开就是影响状态转移的重要输入信号。我们可以通过读取相应的引脚电平来获 取这些输入信息。以下是一个简单的按键读取函数示例:
// 假设按键连接到P3.2引脚
sbit KEY = P3^2;
// 读取按键状态
bit read_key() {
return KEY;
}
设计状态转移逻辑
根据输入信号和当前状态,确定何时从一个状态转移到另一个状态。这需要仔细分析系统的需求和行 为,确保状态转移的逻辑正确无误。例如,在按键处理程序中,当检测到按键按下时,需要进行消抖处 理,确认按键确实被按下后,再将状态转移到相应的按下状态;当检测到按键松开时,同样需要进行消 抖处理,然后将状态转移到松开状态。以下是一个简单的按键状态转移逻辑示例:
// 当前按键状态
KeyState current_state = START;
// 处理按键状态转移
void handle_key_state() {
bit key_value = read_key();
switch (current_state) {
case START:
if (key_value == 0) {
current_state = PRESS_WAIT; // 进入按下等待状态(消抖)
}
break;
case PRESS_WAIT:
// 消抖处理
// 假设消抖时间为20ms
if (key_value == 0) {
current_state = PRESS; // 消抖完成,进入短按状态
} else {
current_state = START; // 消抖失败,回到起始状态
}
break;
case PRESS:
if (key_value == 1) {
current_state = RELEASE_WAIT; // 进入松开等待状态(消抖)
} else {
// 检测是否长按
// 假设长按时间为1000ms
// 如果按键按下时间超过1000ms,则进入长按状态
// 这里省略具体的计时代码
if (is_long_press()) {
current_state = LONG_PRESS; // 进入长按状态
}
}
break;
case RELEASE_WAIT:
// 消抖处理
if (key_value == 1) {
current_state = START; // 消抖完成,回到起始状态
} else {
current_state = PRESS; // 消抖失败,回到短按状态
}
break;
case LONG_PRESS:
if (key_value == 1) {
current_state = RELEASE_WAIT; // 进入松开等待状态(消抖)
}
break;
}
}
编写状态转移函数
在51单片机的程序中,状态转移函数通常是一个循环,不断检查输入信号并更新当前状态。以下是一个 简单的主循环示例:
void main() {
while (1) {
handle_key_state(); // 处理按键状态转移
// 其他任务
}
}
输出函数
定义每个状态下系统的输出行为,这些行为可能与状态转移同步,也可能根据当前状态独立产生。例 如,在按键处理程序中,当按键处于不同状态时,可以通过控制LED灯的亮灭来直观地显示当前状态。以 下是一个简单的LED控制函数示例:
// 假设LED连接到P1.0引脚
sbit LED = P1^0;
// 根据按键状态控制LED灯
void control_led() {
switch (current_state) {
case START:
LED = 0; // 熄灭LED
break;
case PRESS:
LED = 1; // 点亮LED
break;
case LONG_PRESS:
// 长按状态下LED闪烁
// 这里省略具体的闪烁代码
break;
case RELEASE:
LED = 0; // 熄灭LED
break;
}
}
然后在主循环中调用该函数:
void main() {
while (1) {
handle_key_state(); // 处理按键状态转移
control_led(); // 根据按键状态控制LED灯
// 其他任务
}
}
51单片机状态机实现的代码示例
简单按键状态机示例
以下是一个完整的简单按键状态机示例,实现了按键的未按状态、短按状态和长按状态的检测,并通过 LED灯显示当前状态。简单按键状态机示例 以下是一个完整的简单按键状态机示例,实现了按键的未按状态、短按状态和长按状态的检测,并通过 LED灯显示当前状态。
#include <reg52.h>
// 按键状态枚举
typedef enum {
RELEASE = 0, // 松开状态
PRESS = 1, // 短按状态
LONG_PRESS = 2, // 长按状态
PRESS_WAIT = 3, // 按下等待状态(消抖)
RELEASE_WAIT = 4, // 松开等待状态(消抖)
START = 5
} KeyState;
// 按键事件枚举
typedef enum {
IDLE_EVENT, // 未按下事件
PRESS_DOWN_EVENT, // 短按按下事件
PRESS_UP_EVENT, // 短按弹起事件
LONG_PRESS_DOWN_EVENT, // 长按按下事件
LONG_PRESS_UP_EVENT, // 长按弹起事件
} Key_Event;
// 定义引脚的属性结构体
typedef struct {
bit pinlevel; // 引脚电平
KeyState state; // 当前状态
Key_Event event; // 当前事件
unsigned int first_time; // 记录第一次检测到电平的时刻
unsigned int second_time; // 记录第二次检测到电平的时刻(消抖)
unsigned int third_time; // 记录第三次检测到电平的时刻(是否长按)
} Key;
// 按键连接到P3.2引脚
sbit KEY = P3^2;
// LED连接到P1.0引脚
sbit LED = P1^0;
// 按键结构体数组
Key keys = {0, START, IDLE_EVENT, 0, 0, 0};
// 消抖等待时间,初始值20ms
unsigned int KEY_debounce_confine = 20;
// 按键长按限制时间,初始值1000ms
unsigned int KEY_long_press = 1000;
// 读取按键电平
bit read_key() {
return KEY;
}
// 处理按键状态转移
void handle_key_state() {
bit key_value = read_key();
switch (keys.state) {
case START:
if (key_value == 0) {
keys.state = PRESS_WAIT; // 进入按下等待状态(消抖)
keys.first_time = get_time(); // 记录第一次检测到电平的时刻
}
break;
case PRESS_WAIT:
// 消抖处理
if (get_time() - keys.first_time >= KEY_debounce_confine) {
if (key_value == 0) {
keys.state = PRESS; // 消抖完成,进入短按状态
keys.event = PRESS_DOWN_EVENT; // 短按按下事件
} else {
keys.state = START; // 消抖失败,回到起始状态
}
}
break;
case PRESS:
if (key_value == 1) {
keys.state = RELEASE_WAIT; // 进入松开等待状态(消抖)
keys.second_time = get_time(); // 记录第二次检测到电平的时刻
} else {
// 检测是否长按
if (get_time() - keys.first_time >= KEY_long_press) {
keys.state = LONG_PRESS; // 进入长按状态
keys.event = LONG_PRESS_DOWN_EVENT; // 长按按下事件
}
}
break;
case RELEASE_WAIT:
// 消抖处理
if (get_time() - keys.second_time >= KEY_debounce_confine) {
if (key_value == 1) {
keys.state = START; // 消抖完成,回到起始状态
if (keys.state == PRESS) {
keys.event = PRESS_UP_EVENT; // 短按弹起事件
} else if (keys.state == LONG_PRESS) {
keys.event = LONG_PRESS_UP_EVENT; // 长按弹起事件
}
} else {
keys.state = PRESS; // 消抖失败,回到短按状态
}
}
break;
case LONG_PRESS:
if (key_value == 1) {
keys.state = RELEASE_WAIT; // 进入松开等待状态(消抖)
keys.second_time = get_time(); // 记录第二次检测到电平的时刻
}
break;
}
}
// 根据按键状态控制LED灯
void control_led() {
switch (keys.state) {
case START:
LED = 0; // 熄灭LED
break;
case PRESS:
LED = 1; // 点亮LED
break;
case LONG_PRESS:
// 长按状态下LED闪烁
// 这里省略具体的闪烁代码
break;
case RELEASE:
LED = 0; // 熄灭LED
break;
}
}
// 获取当前时间
unsigned int get_time() {
// 这里省略具体的时间获取代码
// 可以使用定时器来实现
return 0;
}
void main() {
while (1) {
handle_key_state(); // 处理按键状态转移
control_led(); // 根据按键状态控制LED灯
// 其他任务
}
}
代码解释
按键状态枚举:定义了按键可能的所有状态,包括松开状态、短按状态、长按状态、按下等待状态 (消抖)、松开等待状态(消抖)和起始状态。
按键事件枚举:定义了按键在不同状态下可能发生的事件,如未按下事件、短按按下事件、短按弹起 事件、长按按下事件和长按弹起事件。
按键属性结构体:用于存储按键的相关信息,包括引脚电平、当前状态、当前事件以及第一次、第二 次和第三次检测到电平的时刻。
状态转移逻辑:在 handle_key_state 函数中,根据按键的输入信号和当前状态,判断是否需要进行 状态转移。在状态转移过程中,会进行消抖处理和长按检测。
输出函数:在 control_led 函数中,根据按键的当前状态控制LED灯的亮灭,直观地显示当前按键状 态。
案例分析
按键控制流水灯
假设有一个单片机系统,包含一个按键和两个LED灯(记为L1和L2)。
要求实现以下功能:
- L1和L2的状态转换顺序为:OFF/OFF --> ON/OFF --> ON/ON --> OFF/ON --> OFF/OFF。
- 通过按键控制L1和L2的状态,每次状态转换需要连续按键5次。
- L1和L2的初始状态为OFF/OFF。
以下是实现该功能的代码示例:
#include <reg52.h>
// 定义LED引脚
sbit L1 = P1^0;
sbit L2 = P1^1;
// 定义按键引脚
sbit KEY = P3^2;
// 定义LED状态枚举
typedef enum {
LS_OFFOFF = 0,
LS_ONOFF = 1,
LS_ONON = 2,
LS_OFFON = 3
} LedState;
// 定义状态机结构体
typedef struct {
LedState u8LedStat; // LED状态
unsigned char u8KeyCnt; // 按键计数
} FSM;
FSM g_stFSM;
// 初始化系统
void sys_init() {
// 初始化LED为熄灭状态
L1 = 0;
L2 = 0;
// 初始化状态机
g_stFSM.u8LedStat = LS_OFFOFF;
g_stFSM.u8KeyCnt = 0;
}
// 检测按键是否按下
bit test_key() {
return KEY == 0;
}
// 处理状态机事件
void fsm_active() {
if (g_stFSM.u8KeyCnt > 3) { // 击键是否满5次
switch (g_stFSM.u8LedStat) {
case LS_OFFOFF:
L1 = 1; // 点亮L1
g_stFSM.u8KeyCnt = 0; // 重置按键计数
g_stFSM.u8LedStat = LS_ONOFF; // 状态迁移
break;
case LS_ONOFF:
L2 = 1; // 点亮L2
g_stFSM.u8KeyCnt = 0; // 重置按键计数
g_stFSM.u8LedStat = LS_ONON; // 状态迁移
break;
case LS_ONON:
L1 = 0; // 熄灭L1
g_stFSM.u8KeyCnt = 0; // 重置按键计数
g_stFSM.u8LedStat = LS_OFFON; // 状态迁移
break;
case LS_OFFON:
L2 = 0; // 熄灭L2
g_stFSM.u8KeyCnt = 0; // 重置按键计数
g_stFSM.u8LedStat = LS_OFFOFF; // 状态迁移
break;
default: // 非法状态
L1 = 0; // 熄灭L1
L2 = 0; // 熄灭L2
g_stFSM.u8KeyCnt = 0; // 重置按键计数
g_stFSM.u8LedStat = LS_OFFOFF; // 恢复初始状态
break;
}
} else {
g_stFSM.u8KeyCnt++; // 状态不迁移,仅记录击键次数
}
}
void main() {
sys_init();
while (1) {
if (test_key()) {
fsm_active();
} else {
// 空闲代码
}
}
}
案例解释
状态定义:通过枚举类型定义了LED灯的四种状态:OFF/OFF、ON/OFF、ON/ON和OFF/ON。 状态机结构体:使用结构体 FSM 来存储当前LED状态和按键计数。
状态转移逻辑:在 fsm_active 函数中,根据按键计数和当前LED状态,判断是否需要进行状态迁 移。当按键计数达到5次时,根据当前状态进行相应的LED控制和状态更新。
按键检测:在 main 函数中,不断检测按键是否按下,如果按下则调用 fsm_active 函数处理状态机 事件。
总结
通过本文的详细介绍,我们深入了解了51单片机状态机的基本概念、实现步骤、代码示例和实际应用案 例。状态机作为一种强大的编程工具,能够帮助我们更加高效地处理复杂的系统任务,提高程序的可读 性和可维护性。
在实际开发中,我们可以根据具体的需求灵活设计状态机的状态和转移逻辑,结合各种传感器和外设, 实现更加丰富多样的功能。同时,通过合理运用状态机的思想,我们可以更好地应对系统中的各种复杂 情况,使程序的设计更加严谨和可靠。
希望读者能够通过本文的学习,掌握51单片机状态机的实现方法,并将其应用到实际项目中。在实践过 程中,不断探索和创新,充分发挥状态机的优势,为嵌入式系统的开发带来更多的可能性。