第一次接触状态机是在大三的嵌入式系统课上,当时教授在黑板上画了几个圆圈和箭头,说这是"有限状态机"。说实话,那会儿完全没理解这玩意儿有什么用,直到后来参加蓝桥杯比赛,在调试数码管窗口切换时遇到各种按键冲突、显示错乱的问题,才真正体会到状态机的威力。
状态机本质上是一种编程思想,它把系统划分为有限的状态,每个状态下系统有特定的行为,并且只能在特定条件下转换到其他状态。拿我们常见的电梯举例:电梯有"上行"、"下行"、"停靠"、"故障"等状态,每个状态下对按钮的响应不同,状态转换也有严格条件(比如必须停靠后才能改变运行方向)。
在单片机开发中,状态机特别适合处理这类场景:
相比传统的标志位方法,状态机有三大优势:
工欲善其事,必先利其器。在开始编码前,我们需要先熟悉手中的开发板。蓝桥杯官方指定的国信天长开发板(无论是绿板还是蓝板)都采用了IAP15F2K61S2单片机,这是一颗增强型的51内核芯片,内置EEPROM和PWM等实用外设。
开发板左侧的S4-S7是四个独立按键,它们与矩阵键盘共享P3.0-P3.3引脚。使用时需要特别注意:
硬件连接检查清单:
开发板使用两个74HC138译码器控制8位数码管:
数码管是共阳型,意味着:
常见问题排查:
现在我们来设计一个包含计时器、日期显示和温度读取的三功能系统。使用S7切换计时器模式,S6切换日期模式,S5切换温度模式。
首先明确系统的状态:
c复制typedef enum {
STATE_IDLE, // 空闲状态
STATE_TIMER, // 计时器模式
STATE_DATE, // 日期显示
STATE_TEMP // 温度显示
} SystemState;
状态转换规则:
状态机的骨架代码结构如下:
c复制SystemState currentState = STATE_IDLE;
void stateMachineUpdate() {
static unsigned char key = getKey();
switch(currentState) {
case STATE_IDLE:
displayWelcome();
if(key == KEY_S7) currentState = STATE_TIMER;
else if(key == KEY_S6) currentState = STATE_DATE;
else if(key == KEY_S5) currentState = STATE_TEMP;
break;
case STATE_TIMER:
handleTimer();
if(key == KEY_S6) {
exitTimer();
currentState = STATE_DATE;
}
break;
// 其他状态处理类似
}
}
每个状态应包含三个部分:
在实际调试中,我发现两个棘手问题:按键抖动导致误触发,以及延时函数阻塞系统响应。经过多次实验,总结出以下解决方案。
传统延时消抖会阻塞系统,改进方案是使用状态机+定时器:
c复制typedef enum {
KEY_IDLE,
KEY_DETECTED,
KEY_CONFIRMED,
KEY_RELEASED
} KeyState;
KeyState keyState = KEY_IDLE;
unsigned int keyHoldTime = 0;
void checkKey() {
switch(keyState) {
case KEY_IDLE:
if(KEY_PIN == 0) {
keyState = KEY_DETECTED;
keyHoldTime = 0;
}
break;
case KEY_DETECTED:
if(++keyHoldTime > DEBOUNCE_TIME) {
if(KEY_PIN == 0) {
keyState = KEY_CONFIRMED;
triggerKeyAction();
} else {
keyState = KEY_IDLE;
}
}
break;
case KEY_CONFIRMED:
if(KEY_PIN == 1) {
keyState = KEY_RELEASED;
}
break;
case KEY_RELEASED:
keyState = KEY_IDLE;
break;
}
}
使用定时器0产生1ms中断,构建时间基准:
c复制void timer0Init() {
TMOD &= 0xF0;
TMOD |= 0x01; // 模式1,16位定时器
TH0 = 0xFC; // 1ms@12MHz
TL0 = 0x18;
ET0 = 1; // 允许定时器0中断
EA = 1; // 开总中断
TR0 = 1; // 启动定时器
}
void timer0Isr() interrupt 1 {
static unsigned int msCount = 0;
TH0 = 0xFC; // 重装初值
TL0 = 0x18;
keyScan(); // 每1ms扫描一次按键
if(++msCount >= 1000) {
msCount = 0;
secondTick(); // 每秒执行的任务
}
}
这样主循环只需处理状态机更新:
c复制void main() {
hardwareInit();
timer0Init();
while(1) {
stateMachineUpdate();
displayRefresh(); // 非阻塞式显示刷新
}
}
下面给出核心功能模块的完整实现,并分享调试过程中积累的经验。
c复制#include <reg52.h>
#include <intrins.h>
// 硬件定义
sbit HC173_A = P2^5;
sbit HC173_B = P2^6;
sbit HC173_C = P2^7;
sbit S7 = P3^0;
sbit S6 = P3^1;
sbit S5 = P3^2;
// 数码管段码表
unsigned char code SMG_duanma[19] = { /* 省略 */ };
// 状态定义
typedef enum {
STATE_IDLE,
STATE_TIMER,
STATE_DATE,
STATE_TEMP
} SystemState;
// 全局变量
SystemState currentState = STATE_IDLE;
unsigned char timerValue = 0;
unsigned char date[3] = {24, 2, 10};
char temperature = 25;
void selectHC173(unsigned char ch) {
HC173_A = ch & 0x01;
HC173_B = (ch >> 1) & 0x01;
HC173_C = (ch >> 2) & 0x01;
}
void displayNumber(unsigned char pos, unsigned char num) {
selectHC173(6);
P0 = 0x01 << pos;
selectHC173(7);
P0 = SMG_duanma[num];
}
void handleIdleState() {
// 显示横线表示待机
for(unsigned char i=0; i<8; i++) {
displayNumber(i, 16); // 显示"-"
}
}
void handleTimerState() {
// 显示格式: HH-MM-SS
displayNumber(0, timerValue/3600%24/10);
displayNumber(1, timerValue/3600%24%10);
displayNumber(2, 16); // "-"
displayNumber(3, timerValue%3600/60/10);
displayNumber(4, timerValue%3600/60%10);
displayNumber(5, 16); // "-"
displayNumber(6, timerValue%60/10);
displayNumber(7, timerValue%60%10);
}
// 其他状态处理函数类似...
void stateMachineUpdate() {
static unsigned char lastKey = 0;
unsigned char currentKey = getKey();
// 状态转换
if(currentKey != lastKey) {
switch(currentKey) {
case KEY_S7:
currentState = STATE_TIMER;
break;
case KEY_S6:
currentState = STATE_DATE;
break;
case KEY_S5:
currentState = STATE_TEMP;
break;
}
lastKey = currentKey;
}
// 状态执行
switch(currentState) {
case STATE_IDLE:
handleIdleState();
break;
case STATE_TIMER:
handleTimerState();
break;
// 其他状态...
}
}
状态跟踪技巧:
常见问题解决:
性能优化建议:
记得在最终版本中移除调试代码,并做好注释。状态机的优势在于,即使半年后回头看代码,也能快速理解各部分的逻辑关系。