基于单片机的电梯控制系统设计:状态机建模与C语言实现
2026/9/15 22:05:40 网站建设 项目流程

简介:面向单片机初学者的课程设计资料,以电梯程序控制系统为完整案例,涵盖8051/STM32微控制器的楼层请求接收、路径规划、电机PWM控制、限位/超速/超重等安全保护逻辑,适合电子、自动化类学生用于课程设计或综合实训。压缩包共35个文件、约30.32MB,主要包括C语言源程序、Keil工程文件(uvproj/uvopt/plg)、编译生成的hex/obj/lst、仿真设计文件(DSN/DBK)以及7段测试录屏视频,覆盖开关门、复位、警报、上行/下行优先级、极限开关与超重引脚等运行场景,便于对照仿真和实物理解电梯调度流程。已有95人学习下载。资源除主从单片机程序外,还提供README说明与完整Proteus仿真工程,可帮助学习者快速上手、按模块完成从程序编写到联调测试的闭环实践。

1. 电梯课程设计为什么卡在“状态”而不是“电机”

不少人在做基于单片机的电梯程序控制系统课程设计时,第一反应是去研究电机驱动、继电器控制或 PWM 调速,结果程序写到一半发现真正麻烦的并不是让电机转起来,而是让电梯在 1 楼到 5 楼之间不出错地来回跑。电梯控制的本质是“状态转换”:什么时候开门、什么时候关门、什么条件下响应上行召唤、什么时候忽略反向召唤,这比单纯控制一个直流电机要复杂得多。如果一上来就画原理图、接按键、写延时函数,后面大概率会陷入逻辑混乱。

本设计要解决的核心问题,是如何用一颗常见的 STC 或 51 单片机,把电梯的楼层检测、内外呼梯、电机正反转、开关门动作串成一个可靠的有限状态机。适合正在做单片机课程设计、电子设计竞赛入门、以及想弄明白“状态机在实际项目中到底怎么落地”的读者。我默认你手里有一块 STC89C52 或兼容的最小系统板,配一个 4 位数码管、几个按键、一个直流减速电机,以及一颗用来看波形的逻辑分析仪或示波器。没有硬件也没关系,Proteus 仿真一样可以跑通全文代码,但文中涉及的电气细节只有实物调试时才会暴露。

2. 电梯控制系统的硬件架构与单片机选型

2.1 用 STC89C52 搭建电梯控制的最小硬件清单

课程设计里最稳妥的单片机选择是 STC89C52RC,原因有三个:5V 供电与绝大多数传感器和电机驱动模块兼容;IO 口足够覆盖 12 个按键、4 位数码管段选位选和 2 路电机方向控制;Keil + STC-ISP 的烧录链路简单,宿舍里一根 USB-TTL 就能搞定。如果你手头只有 STM32,也没问题,把本文代码里的sbit定义和延时函数替换成 HAL 库的引脚操作即可,状态机逻辑完全通用。

最小硬件清单如下:STC89C52 单片机最小系统板一块;4 位共阴数码管一个,段选接 P0 口,位选接 P2 口的低 4 位,中间串 220Ω 限流电阻;独立按键 4 个,接 P1.0 到 P1.3,分别代表 1 楼内呼、5 楼内呼、开门按钮、关门按钮;一楼和五楼限位开关各一个,接 P3.2 和 P3.3(外部中断引脚,稍后解释为什么);电机驱动模块一个,推荐 L298N 或 TB6612,IN1、IN2 接 P2.6、P2.7,ENA 接 P2.5 用于使能;蜂鸣器一个,接 P2.4,用于到站提示。

2.2 楼层检测用限位开关还是霍尔传感器

楼层检测有两条路线:在每层安装限位开关,或者用安装在电机轴上的霍尔测速模块计算脉冲数。课程设计建议选限位开关,理由很直接:电梯轿厢机械结构简单,三个楼层装三个微动开关,每层停靠时撞到对应的开关,单片机读到低电平就知道当前楼层。这种方式不需要额外编写脉冲计数程序,也不存在累积误差。

霍尔测速路线的问题在于,你不仅要写外部中断计数,还要处理电机惯性带来的过冲:电机断电后轿厢还会滑行一小段距离,脉冲数可能多计。限位开关则天然免疫这个问题,因为撞到开关的位置就是机械停靠位置。唯一要注意的是,轿厢在两层之间晃动时可能出现瞬时接触,所以程序中要加去抖判断。去抖不一定要用delay(10)阻塞延时,更好的做法是放到定时器中断里做 10ms 的连续电平扫描。

// 楼层检测引脚定义(STC89C52) sbit FLOOR_1 = P3^2; // 1楼限位开关,低电平有效 sbit FLOOR_2 = P3^3; // 2楼限位开关 sbit FLOOR_3 = P3^4; // 3楼限位开关

这段定义表明楼层输入全部接在 P3 口,并且 1 楼接到了外部中断 0(P3.2)上。这样做的用意是:如果电梯正在运行途中,某层限位开关被触发,可以立即通过中断标志位记录楼层信息,而不需要主循环轮询到下一次才处理。2 楼和 3 楼用普通 IO 轮询即可,因为电梯到站时主循环本来就会执行楼层判断逻辑。参数上,所有开关按下时接地,单片机内部上拉电阻保证空闲时为高电平,这是共地接法最简单可靠的形式。

2.3 电机驱动与继电器的取舍

电梯模型常用的驱动电机有两种:直流减速电机和 28BYJ-48 步进电机。直流减速电机配合 L298N 驱动板是最省事的方案,因为 L298N 自带使能脚和方向脚,单片机只需要给两个逻辑电平就能控制正反转和停止。步进电机的好处是定位精确,但你需要写环形脉冲分配表,程序复杂度会上升一个档次。做课程设计时如果只追求“能跑、能停、能演示”,直流减速电机够用;如果指导老师明确要求“精确平层”,那就用步进电机。

实际接线时有个容易踩的坑:L298N 的 GND 必须和单片机共地,否则 IN1、IN2 的逻辑电平没有参考地,电机要么不转要么乱转。另外一个坑是 L298N 的 5V 输出口可以给单片机供电,但前提是输入电源在 7V 到 12V 之间,而且电机启动瞬间的电压跌落会导致单片机复位。我的建议是电机电源和单片机电源完全分开,只用 L298N 的逻辑电平部分和单片机通信,这样最稳。

// 电机方向控制(L298N 驱动模块) sbit MOTOR_IN1 = P2^6; // 方向控制1 sbit MOTOR_IN2 = P2^7; // 方向控制2 sbit MOTOR_ENA = P2^5; // 使能控制,PWM可调速 void Motor_Up(void) { MOTOR_IN1 = 1; MOTOR_IN2 = 0; MOTOR_ENA = 1; // 全速运行 } void Motor_Down(void) { MOTOR_IN1 = 0; MOTOR_IN2 = 1; MOTOR_ENA = 1; } void Motor_Stop(void) { MOTOR_IN1 = 0; MOTOR_IN2 = 0; MOTOR_ENA = 0; // 失能,电机自由滑行 }

逻辑说明:Motor_UpMotor_Down通过改变 IN1、IN2 的差动电平来换向,ENA 置 1 让 H 桥全通。Motor_Stop里两个输入都是 0,ENA 为 0,此时电机处于滑行状态,惯性会带着轿厢继续移动。如果发现停层不准,可以在停止时序里加一段短暂的反向制动:先让电机反向转 100ms 再失能,这在代码实现上就是Motor_Down(); delay(100); Motor_Stop();,效果比单纯断电好得多。

3. 电梯程序控制系统的状态机建模

3.1 把电梯行为拆成 5 个稳定状态

电梯控制程序的复杂度不在代码量,而在逻辑分支的组合爆炸。如果你用“当前楼层 + 目标楼层 + 运行方向”的平铺方式写if,那代码会越写越长,最后自己都改不动。正确做法是用有限状态机把电梯的行为收敛成 5 个稳定状态:空闲、开门、关门、上行、下行。任何时刻电梯必处且仅处在这 5 个状态中的一个,状态之间的跳变由按键事件、限位开关事件和定时器事件驱动。

为什么这 5 个状态足够?因为电梯在真实世界中只会做这几件事:停着等人(空闲)、开门放人进来(开门)、等人上完关门(关门)、往高处走(上行)、往低处走(下行)。什么“超载报警”“消防联动”“检修模式”在课程设计阶段都是加分项,不进入基础状态机。你只需要把这 5 个状态的迁移条件理清,程序自然就稳了。蓝桥杯单片机国赛客观题里不少关于状态机的题目,本质考察的就是这种迁移条件的完备性。

状态迁移的触发事件可以归纳为四类:内呼键(楼层按键)、外呼键(上下行召唤)、限位开关触发、超时事件。这四类事件不是所有状态都必须响应,比如电梯正在关门时,门外的人拍开门按钮,门要重新打开;但电梯正在上行时按 1 楼内呼键,这个请求只能记录到待办列表里,等电梯下行时才能响应。这里的“什么时候响应”就是状态机迁移条件要回答的问题。

typedef enum { IDLE, // 空闲:电梯停在某层,等待指令 DOOR_OPEN, // 开门:正在开门或门一直开着等待 DOOR_CLOSE,// 关门:正在关门,防夹检测可放这里 RUN_UP, // 上行:电机正转,轿厢向上移动 RUN_DOWN // 下行:电机反转,轿厢向下移动 } ElevatorState; ElevatorState currentState = IDLE;

这段枚举定义了状态机的全部 5 个状态,currentState是全局状态变量。很多教材会把“开门”和“门完全打开等待”分成两个状态,但在单片机上合并成DOOR_OPEN就够了:进入这个状态时启动一个 5 秒定时器,定时结束自动切到DOOR_CLOSE。课程设计的时间有限,状态分得太细只会增加状态迁移表的维护成本。我见过一个过于极端的例子,把“开门中”“门已开”“超时等待”“关门中”“防夹暂停”拆成 5 个状态,结果状态迁移图画了满满一页 A4 纸,程序却只跑了 3 层楼。

3.2 状态迁移表与优先级仲裁

状态机的关键不在状态定义,在迁移条件。下面这张表是我在做课程设计时总结的对照表,它把每个状态的输入事件和输出动作写在一行里,程序实现时就是一个switch加事件判断。注意同一状态下的多个事件有优先级,比如“开门”状态下同时有“5 楼内呼”和“关门超时”,内呼优先,因为乘客还没上完。

当前状态触发事件迁移条件下一状态执行动作
IDLE内呼楼层 xx != 当前楼层RUN_UP 或 RUN_DOWN记录目标楼层,电机启动
IDLE外呼上行/下行召唤楼层 != 当前楼层RUN_UP 或 RUN_DOWN记录召唤方向和楼层
DOOR_OPEN5 秒超时无其它请求DOOR_CLOSE启动关门时序,蜂鸣器提示
DOOR_OPEN门内呼按键任意楼层键保持 DOOR_OPEN重置 5 秒定时器,继续等待
RUN_UP限位开关 K 触发K == 目标楼层DOOR_OPEN电机停止,刷新当前楼层
RUN_UP内呼楼层 xx < 当前楼层保持 RUN_UPx 存入反向队列,暂不响应
RUN_DOWN限位开关 K 触发K == 目标楼层DOOR_OPEN电机停止,刷新当前楼层
RUN_DOWN内呼楼层 xx > 当前楼层保持 RUN_DOWNx 存入反向队列,暂不响应

这张表重点在于“反向队列”的设计。电梯在上行过程中收到低楼层内呼,不能立刻掉头,而是把请求存进队列,等当前方向跑完后再处理。单片机课程设计里最常见的 bug 就是这里:电梯上行到 3 楼时,1 楼有人按了上行外呼,程序直接让电梯掉头下行,导致 5 楼的人永远等不到电梯。正确的仲裁逻辑是:同方向的请求立即响应,反方向的请求挂起,等当前方向目标完成后再切换方向。

3.3 用定时器中断驱动状态机而非阻塞延时

51 单片机入门教程里最常见的写法是while循环里嵌套delay(5000)实现 5 秒开门等待,这在电梯程序里是非常糟糕的做法。阻塞延时会让 CPU 在延时期间无法响应按键中断和限位开关中断,如果有人在延时期间按了急停按钮,程序毫无反应。正确做法是使用定时器中断产生一个 10ms 的时基信号,主循环每 10ms 扫描一次所有事件源,状态机在一个固定节拍下运行。

// 定时器0初始化:16位模式,10ms中断一次 void Timer0_Init(void) { TMOD &= 0xF0; // 清空定时器0的模式位 TMOD |= 0x01; // 模式1:16位定时器 TH0 = 0xDC; // 10ms @ 11.0592MHz TL0 = 0x00; ET0 = 1; // 使能定时器0中断 EA = 1; // 总中断使能 TR0 = 1; // 启动定时器 }

这里的关键参数是 11.0592MHz 晶振下的重装值:TH0 = 0xDC; TL0 = 0x00计算出 10ms 的中断周期。计算过程是:定时器从 0xDC00 计数到 0xFFFF,计数 9216 个脉冲,每个脉冲 12 个时钟周期,即 9216 × 12 / 11.0592MHz ≈ 10ms。如果你用的晶振是 12MHz,重装值要改成0xD8F0,否则时间基准偏离会导致开门等待时间误差明显。每 10ms 中断一次后,在中断里递增三个软计数器:开门超时计数器、按键消抖计数器、关门超时计数器。主循环只做状态刷新和事件扫描,不做任何延时,这样按键响应延迟控制在一个时基内。

4. 电梯程序控制系统的 C 语言实现与关键代码

4.1 主循环架构:每 10ms 扫描一次事件

主程序的结构可以分为三个层次:定时器中断里维护时基和软计数器;主循环里先扫描楼层限位开关,再扫描按键,最后执行状态更新;状态更新函数内部根据当前状态决定是否响应事件。这三件事严格分开的好处是调试容易:发现楼层检测不对就查扫描函数,发现状态不跳变就查状态更新函数,职责单一,不需要在几百行代码里大海捞针。

void main(void) { Timer0_Init(); // 初始化定时器 Motor_Stop(); // 电机初始为停止 currentFloor = 1; // 电梯默认停在 1 楼 currentState = IDLE; // 初始状态为空闲 while (1) { Scan_FloorSwitch(); // 每 10ms 扫描一次楼层开关 Scan_Key(); // 扫描按键输入 Update_StateMachine(); // 根据事件更新状态机 } }

逻辑说明:Scan_FloorSwitch每次只读取三个限位开关的当前电平,有变化时更新currentFloor并置位楼层事件标志;Scan_Key读取四个按键,消抖后置位对应的请求标志;Update_StateMachine是核心函数,它读取当前状态和事件标志,查状态迁移表决定是否跳转。主循环没有任何延时函数,执行一轮的时间远小于 10ms,因此实际效果是每 10ms 完整扫描一轮事件。这个架构的好处是无论按键在第几毫秒按下,最多等待 10ms 就会被捕获,比轮询方式灵敏得多。

主循环里有一个容易被忽视的细节:Scan_FloorSwitchScan_Key必须是无阻塞的。也就是说这两个函数内部不能用while (key == 0)这种等待释放的写法,否则一个按键按住不放就会卡死整个循环。正确的消抖做法是:检测到电平变化后记录时间戳,直到连续 2 次扫描都是同一电平才确认有效。下面给出的按键扫描代码就是这种思路。

// 按键消抖扫描(非阻塞版本) #define KEY_PRESSED 0 // 按键按下为低电平 void Scan_Key(void) { static unsigned char lastKeyState = 0xFF; unsigned char currentKeyState = 0xFF; // 读取四个按键,分别为:开门、关门、1楼内呼、5楼内呼 if (KEY_OPEN == KEY_PRESSED) currentKeyState &= 0xFE; if (KEY_CLOSE == KEY_PRESSED) currentKeyState &= 0xFD; if (KEY_FLOOR1 == KEY_PRESSED) currentKeyState &= 0xFB; if (KEY_FLOOR2 == KEY_PRESSED) currentKeyState &= 0xF7; if (currentKeyState != lastKeyState) { debounceCnt = 0; // 电平变化,重置计数 lastKeyState = currentKeyState; return; // 等待下一次扫描确认 } if (debounceCnt < 2) { debounceCnt++; // 连续两次扫描电平一致 return; } // 电平稳定且为按下状态,触发事件 if (!(currentKeyState & 0x01) && !keyOpenEvent) { keyOpenEvent = 1; // 开门按键事件 } // 其余按键事件同理省略 }

参数说明:这是状态机里的防抖增强版,不阻塞主循环。根据“当单片机遇上状态机”的思路,防抖不是延时的理由,因为这里核心是实时性。debounceCnt记录连续一致电平的扫描次数,达到 2 次说明消抖完成,对应 20ms 的硬件防抖时间。不同厂商、不同批次的按键抖动时间差异较大,视具体情况可以将阈值改为 5 次,对应 50ms,这通常能覆盖绝大多数机械按键的抖动范围。每次只记录事件标志而不是直接执行动作,目的是让状态机统一处理事件,避免在扫描函数里直接改变状态导致状态迁移混乱。

// 按键事件标志定义 bit keyOpenEvent; // 开门请求 bit keyCloseEvent; // 关门请求 bit keyFloor1Event; // 1楼内呼 bit keyFloor5Event; // 5楼内呼

这组bit变量就是状态机的事件输入。bit类型是 Keil C51 的扩展语法,占用 1 位存储空间,如果移植到 STM32 上要改成uint8_t。事件标志被读取后必须及时清零,否则状态机会反复执行同一个事件,导致电梯开门后一直重置定时器。常见做法是事件标志在进入对应状态时清零,比如DOOR_OPEN状态里读取keyFloor5Event并判断是否重置超时定时器后,立即将keyFloor5Event清零。

4.2 状态迁移函数:从 IDLE 到 RUN_UP 的完整路径

状态迁移函数是整个程序的心脏。它的结构是外层switch按当前状态分发,内层if判断事件标志。核心原则是:每个事件在特定状态下只有一条处理路径,不存在“既是 A 又是 B”的模糊逻辑。以下代码实现了IDLERUN_UP两个状态的核心逻辑,其余状态可按相同的模式补全。

void Update_StateMachine(void) { switch (currentState) { case IDLE: // 空闲状态:响应内呼和开门按键 if (keyFloor1Event && currentFloor != 1) { keyFloor1Event = 0; targetFloor = 1; currentState = (currentFloor > 1) ? RUN_DOWN : RUN_UP; Motor_Down(); // 实际方向由目标楼层决定,这里省略判断 } else if (keyFloor5Event && currentFloor != 5) { keyFloor5Event = 0; targetFloor = 5; currentState = (currentFloor < 5) ? RUN_UP : RUN_DOWN; Motor_Up(); } else if (keyOpenEvent) { keyOpenEvent = 0; currentState = DOOR_OPEN; openDoorTimer = 500; // 5秒开门计时 } break; case RUN_UP: // 上行状态:检测是否到达目标楼层 if (floorSwitchEvent && currentFloor == targetFloor) { floorSwitchEvent = 0; Motor_Stop(); currentState = DOOR_OPEN; openDoorTimer = 500; } // 反向请求只记录,不响应 if (keyFloor1Event) { keyFloor1Event = 0; pendingFloor = 1; // 存入反向待办 } break; // 其余状态省略,按相同模式编写 default: break; } }

这段代码里需要注意两个关键设计:第一,IDLE状态里判断了currentFloor != targetFloor,这是防止电梯已经在该楼层时还执行启动动作;第二,RUN_UP状态里对keyFloor1Event的处理是“记录到pendingFloor但不改变运行方向”,这正对应状态迁移表里的“反向队列”设计。等电梯到达目标楼层开门上下客后,IDLE状态下会检测到pendingFloor有值,再执行新的方向切换。

4.3 数码管显示与楼层刷新逻辑

4 位共阴数码管在电梯系统里通常用来显示当前楼层和运行方向。显示刷新需要动态扫描:每次只点亮一位,通过位选切换依次点亮 4 位,利用人眼视觉暂留形成完整显示效果。动态扫描的刷新频率不能低于 50Hz,否则会看到闪烁。主循环的执行速度远高于此,因此直接在主循环里加一个软件计数器,每 5ms 切换一个显示位即可。

// 数码管段码表:共阴数码管,0-F 的段码 unsigned char code segCode[10] = { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 };

这里的段码是共阴数码管搭配 74HC573 锁存器的典型接法下的值。0xC0表示数字 0:a 到 g 段中只有 g 段不亮,对应二进制 11000000。如果你用的是共阳数码管,段码要取反,比如 0 的共阳段码是 0x3F。这个细节经常导致第一次焊接的同学发现数码管显示乱码,排查半天最后发现是共阴共阳搞反了。

void Display_Process(void) { static unsigned char dispIndex = 0; // 位选全部关闭,防止段选数据串位 P2 &= 0x8F; // P2.4~P2.7 清零,关闭所有位选 switch (dispIndex) { case 0: P0 = segCode[currentFloor / 10]; break; // 十位 case 1: P0 = segCode[currentFloor % 10]; break; // 个位 case 2: P0 = 0xBF; break; // 显示 "-" 表示方向标识 case 3: P0 = segCode[(currentState == RUN_UP) ? 1 : 0]; break; } // 打开当前位选 P2 |= (0x10 << dispIndex); dispIndex++; if (dispIndex >= 4) dispIndex = 0; }

参数说明:P2 &= 0x8F这句是为了在切换显示位之前关闭所有位选,防止上一位的显示数据残留在这一位。0xBF是 “-” 符号的段码,用于指示电梯当前方向。P2 |= (0x10 << dispIndex)依次点亮第 1 到第 4 位。注意P2口的低 4 位之前被用于楼层限位开关输入,高 4 位用于位选和电机控制,两者不能冲突。这就是为什么在硬件设计时要把数码管位选安排在 P2.4 到 P2.7 的原因。

5. Proteus 仿真与实物调试中的边界条件

5.1 用 Proteus 搭建仿真环境时怎么处理限位开关

Proteus 仿真 51 单片机是课程设计验证阶段效率最高的方式,它不需要任何实体硬件,在宿舍就能把状态机逻辑调通。在 Proteus 里搭建电梯模型时,限位开关可以用 BUTTON 元件代替,关键是模拟“电梯到达楼层”这个事件:你需要手动按下对应楼层的按钮,模拟轿厢撞到开关的机械接触。

仿真时有个容易误导人的地方:Proteus 里的 BUTTON 按下时是瞬间跳变的,没有真实微动开关的抖动过程。这意味着你在真实硬件上写的 20ms 消抖代码在 Proteus 里会正常工作,但反过来不行——在 Proteus 上不加消抖的程序也能跑通,一旦烧到实机就会出现按键一次触发多次的故障。所以课程设计说明书里一定要强调“消抖逻辑在实物测试中验证过”,而不是“Proteus 仿真验证过”。仿真只验证逻辑正确性,实机验证才验证电气可靠性。

另一个仿真相较于实物的差异是电机响应。如果用的是 Proteus 自带的 MOTOR-PWM 元件,它的响应是理想的,没有惯性。实机上的直流减速电机在断电后还会继续转一小段,这段惯性会让电梯冲过停靠位置。仿真时你看不出这个问题,因此要在实机调试时预留一个提前减速区间:距离目标楼层还剩 1 层时切换到低占空比 PWM,用 50% 的功率爬完最后一段,再完全断电。

5.2 联动调试的四步走

从 Proteus 仿真通过到实物稳定运行,我的经验是按功能模块逐个验证,而不是一次性把所有硬件接好跑总程序。以下是我在调试自己的课程设计时用的流程:第一步,只烧数码管显示程序,验证楼层显示和方向指示正常,排除段选位选接线错误;第二步,只接按键和蜂鸣器,按下按键时蜂鸣器响,松手停,验证按键扫描和消抖逻辑;第三步,接电机驱动模块,用最简单的“按下上行按键电机正转”程序验证 L298N 接线和供电;第四步,把所有模块合在一起跑完整状态机。

每一步的具体验证方法要能用眼睛看到结果,不能靠“推测”。比如验证消抖逻辑时,连续快速按同一个按键 100 次,如果数码管显示的楼层值不变,说明消抖可靠;如果偶尔出现跳变一次,说明消抖时间不够,把debounceCnt从 2 增大到 4 再试。验证限位开关时,用杜邦线直接短接对应引脚到 GND,模拟开关触发,观察数码管楼层值是否切换。如果短接后楼层没变,用万用表量引脚电压,确认是否真正拉低。

// 调试辅助:楼层开关扫描带标志位 void Scan_FloorSwitch(void) { if (FLOOR_1 == 0 && lastFloor1State != 0) { lastFloor1State = 0; currentFloor = 1; floorSwitchEvent = 1; // 楼层变化事件 } else if (FLOOR_1 == 1 && lastFloor1State != 1) { lastFloor1State = 1; // 离开1楼 } // FLOOR_2、FLOOR_3 的判断逻辑相同 }

参数说明:floorSwitchEvent是楼层限位触发事件,它只在限位开关从高电平变为低电平的瞬间置位一次,持续按住的阶段不会重复置位。这个“边沿触发”语义对电梯系统很重要——电梯到达某层时只应该触发一次停靠动作,如果在电平触发模式下写代码,电梯可能会在开门期间反复触发楼层事件,导致状态机抖动。lastFloor1State作为上一个扫描周期的电平记忆,用于检测边沿变化。

5.3 时序检查:用逻辑分析仪看按键消抖和定时器中断

时序类问题在 Proteus 仿真里几乎不可能暴露,但在实机上非常常见。比如定时器中断重装值算错,导致开门等待时间从 5 秒变成 8 秒或 3 秒;或者按键消抖程序和主循环的节拍不匹配,出现按键响应延迟忽长忽短。这类问题用逻辑分析仪是效率最高的定位方式。

把逻辑分析仪的通道 1 接到定时器中断里翻转的测试引脚 P1.7,通道 2 接到按键引脚,通道 3 接到电机使能脚。启动采集后按下开门按键,观察三个通道的时序关系:按键信号出现下降沿后,测试引脚是否在 10ms 内出现了对应的中断处理标记;电机使能脚的电平变化是否和程序里设置的事件时序一致。如果按键采样后状态机反应延迟了 20ms 或 30ms,说明消抖计数的扫描节拍和定时器中断不同步,需要检查TMOD配置是否遮掉了定时器 1 的设置。

这里有一个多数教材没写清楚的细节:51 的定时器初始化代码里TMOD &= 0xF0这句至关重要。它先把 TMOD 的高 4 位(定时器 1 的模式)保留不动,只改低 4 位(定时器 0 的模式),防止初始化定时器 0 时把定时器 1 的配置冲掉。很多同学的代码里直接写TMOD = 0x01,如果后面又初始化了定时器 1 做波特率,就会把波特率配置清零,串口乱码。同理,TCON的位操作也要用|=&=而不是整体赋值。

6. 用状态切换日志快速定位逻辑 Bug

程序写完后不要急着上电测试,先在代码里埋一个调试状态输出函数。这个函数在每次状态迁移时把“旧状态、触发事件、新状态”通过串口发送到上位机,波特率 9600,数据格式用可读的逗号分隔文本。12 位 ADC 和 LCD1602 在电梯这种低复杂度系统里用不上,但一串可读的状态切换日志能让你在两分钟内看出电梯行为是否符合预期。

// 状态切换日志输出(串口调试) void Log_StateChange(ElevatorState oldState, ElevatorState newState, unsigned char event) { UART_SendString("OLD:"); UART_SendHex(oldState); // 发送十六进制状态值 UART_SendString(" EVT:"); UART_SendHex(event); UART_SendString(" NEW:"); UART_SendHex(newState); UART_SendString("\r\n"); }

实际排查故障的例子:某次测试中电梯到达 5 楼后开门,5 秒后关门,但门完全关闭后电梯直接下行到 1 楼,没有停。看日志发现DOOR_CLOSE状态结束后直接跳到了RUN_DOWN,触发事件是pendingFloor,说明在DOOR_OPEN状态里误把 1 楼内呼事件同时记录到了pendingFloor。修掉事件标志和pendingFloor赋值语句的粘连后故障消失。这个定位过程如果靠肉眼盯数码管,一两分钟看不出来,但看日志一眼就能找到。

最后一章收一个逆向调试技巧。把状态机跑一遍正常流程后,故意制造异常情况来验证系统的稳定性,比如在电梯正在运行时连续快速按多次反向楼层键,或者同时在 1 楼和 5 楼都按下内呼键。正确的行为是:电梯完成当前方向的所有请求后才切换方向,并且两个请求都不丢失。用串口日志观察这个过程,你应该看到RUN_UP -> DOOR_OPEN -> RUN_DOWN的完整链路,中间不会有重复开门或跳过楼层的情况。如果出现电梯在某层反复开门而不继续走,打开状态切换日志,看是否有未清零的事件标志触发了异常的DOOR_OPEN -> DOOR_OPEN自循环。每个状态入口处的日志记录时间戳,能直接算出各状态的停留时长是否符合定时器配置,这在验证 5 秒开门超时、4 秒关门超时这类参数时比示波器更好用。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询