简介这是一份基于C语言的单片机电梯程序控制系统源码包面向单片机学习、嵌入式开发及课程设计人群核心解决多楼层乘客接送与高效调度问题。程序按照各楼层间隔4.5米的物理结构设计通过判断不同楼层乘客需求在整体运行时间最短条件下完成运输并涵盖开关门、极限保护、超重检测等实用电梯逻辑。压缩包共53个文件约21.49MB主要包含C源文件、Keil工程文件、Proteus仿真电路、Hex烧写文件以及多段测试视频其中主、从单片机程序双套代码配合6线4相步进电机驱动文件便于理解整体控制流程和电机运动控制。已有453人学习下载适合需要掌握电梯控制算法、单片机编程或仿真联调的读者。包内测试视频覆盖警报、复位、开关门、极限开关、下行优先级、超重信号等关键功能场景可配合工程代码快速验证各模块行为是较完整的嵌入式综合实训参考。1. 电梯控制程序难的不是电机是“记住自己要去哪”很多第一次拿到“C语言单片机的电梯程序控制系统源码”的人会理所当然地以为难点在电机驱动上——正转反转继电器一搭电梯不就能跑了等真正把程序烧进板子才发现电梯要么到了楼层不停要么停完就忘了自己该干嘛要么开关门和升降逻辑在同一个循环里打架。反直觉的结论是电梯程序的核心不是电机控制而是状态机。你必须在内存里维护一套“电梯现在在哪、要往哪去、门开没开、超载没超载”的完整记忆哪怕只有一片STC89C52这样的51单片机RAM只有几百字节也能把这部电梯的脑子装完。这套源码最典型的落地场景是三类人做课程设计/毕业设计的本科生、参加蓝桥杯或电子设计竞赛准备电梯题的学生以及想用单片机复刻一套小型电梯逻辑、给PLC方案做个低成本验证的工程师。它能解决的问题是按钮响应、楼层判决、顺向调度、开关门时序这些整套控制逻辑而不是一颗电机怎么转。对新手来说跟着改一遍代码比对着书背一百遍状态机概念都管用。2. 先搞清电梯要控什么IO分配、传感器选型与四路核心反馈2.1 从机械到IO单片机眼里电梯是哪几路信号在你写第一行C代码之前要把电梯的机械动作翻译成单片机认识的IO信号。一台最简单的教学型电梯机械侧大概就是这些部件一个可以上下走的轿厢、一根导轨、每层一个停靠检测点、一台直流减速电机或者步进电机、一套开关门机构通常也是一颗小电机加两个限位开关再加上轿厢内的楼层按键和每层楼外面的上下呼叫按键。把这些部件映射到单片机的引脚上我一般会分成四路核心反馈楼层位置反馈、平层信号、门限位信号、按键输入。你可以用P0口接数码管段码、P2口接位选P1口的一部分接干簧管和按键P3口接电机和门的控制信号。这个分配没有唯一标准但有一个原则带中断能力的引脚留给“必须马上响应”的信号比如平层传感器和急停按钮。选择STC89C52作为主控是目前课程设计里最常见的选择原因很朴素51内核资料多到溢出来烧录器便宜Keil环境人人会配。如果你手头是STC15系列或者STC8系列程序结构完全不用改只需要注意定时器初值可能要重算因为它们的时钟频率往往不是12MHz。2.2 楼层与平层检测干簧管方案为什么是课程设计首选楼层检测的传感器方案各家用得不一样。工业电梯会用旋转编码器加绝对值定位但那东西对单片机课程设计来说成本偏高、接线复杂调试时还得对着波形数脉冲。教学场景下我用得最顺手的方案是每层楼固定一个干簧管或者霍尔传感器轿厢侧面装一块小磁铁轿厢路过哪层哪层的干簧管就被磁铁吸合输出一个低电平信号。这个方案的优点在结构上信号是开关量不是模拟量51单片机直接读引脚就能判断“电梯现在在第几层”。缺点也很明显——干簧管是机械触点轿厢经过时触点会抖而且如果磁铁装得离干簧管太近一次经过可能会反复吸合、断开好几次软件上不做消抖楼层显示就会乱跳。霍尔传感器方案和干簧管方案的代码逻辑几乎一模一样因为输出都是电平跳变只是霍尔传感器没有机械触点寿命更长、抖动更小价格贵几块钱。我的建议是仿真或者课程设计用干簧管就够了样机要长期跑的用霍尔。2.3 电机驱动与IO分配表直流减速电机加继电器换向驱动电机这块新手最容易犯的错误是拿单片机引脚直接去驱动电机——烧引脚是小事烧完不知道换芯片才是麻烦。常见的可靠做法是两级隔离单片机引脚先经过ULN2003或者光耦再去控制继电器或者驱动芯片。直流减速电机的换向靠两个继电器交叉导通实现方向控制只需要两个IO一个控制正转、一个控制反转两个都断开就是停止。如果用的是步进电机那驱动逻辑就是另外一回事了需要PWM脉冲序列去控制转动角度和方向代码量和IO占用都会增加。单纯为了“电梯控制逻辑”这个主题直流减速电机加继电器的组合是性价比最高的选择。下面这张表是我做过的项目里比较顺手的一套分配方案照着接基本不会打架你完全可以改成自己的板子模块引脚电平含义楼层干簧管1-4P1.0-P1.3低电平有效对应层有磁铁吸合上行按键/下行按键P1.4-P1.5按键按下为低电平轿厢内呼按键1-4P2.0-P2.3按下申请到某层电机正转P3.0高电平为正转电机反转P3.1高电平为反转开门继电器P3.2高电平开门关门继电器P3.3高电平关门平层传感器上/下P3.4-P3.5双传感器都触发表示电梯已平层3. 电梯程序的心脏是状态机方向优先调度与状态流转设计3.1 状态机拆分待机、运行、到站、开关门的状态定义把电梯的全部行为定义成一张状态图这是整个源码的地基。我在代码里会这样定义状态typedef enum { ST_IDLE 0, // 待机无请求门关 ST_MOVE_UP, // 向上运行 ST_MOVE_DOWN, // 向下运行 ST_STOPPING, // 到站减速正在平层 ST_DOOR_OPEN, // 门已开等待乘客 ST_DOOR_CLOSE, // 正在关门 ST_OVERLOAD // 超载保护 } LiftState; LiftState current_state ST_IDLE;这里的状态不是拿来好看的它决定主循环里哪段代码能执行。比如电梯处于ST_MOVE_UP时你只响应“上行方向的楼层请求”和“急停”下行按钮即使按了也不会立刻改变方向。这就是状态机的价值它把“电梯接下来干嘛”约束成一张明确的表而不是散落在if-else里的零散判断。3.2 顺向就近调度方向优先级与请求队列的实现思路电梯调度算法是个数学问题但单片机课程设计用最经典的“方向优先”算法就完全够用了。什么意思电梯当前向上运行时只响应上行方向的请求如果上行方向没有请求了才掉头响应下行请求。这个逻辑比“绝对最近楼层优先”简单得多也符合真实乘客的直觉。每层楼的请求我建议用位图存而不是数组。四个楼层就一个unsigned char变量bit0到bit3分别代表1到4楼是否有请求。按下楼层按键时把对应bit置1电梯到达该层时清0。这样判断“上行方向有没有请求”只需要一次移位加一次逻辑运算unsigned char req_flag 0; // bit0~bit3对应1~4楼1表示有请求 // 按2楼内呼键 req_flag | 0x02; // 置bit1 // 电梯在2楼平层后清除请求 req_flag ~0x02;判断顺向是否能停靠的代码长这样电梯当前在cur_floor向上运行时扫描cur_floor以上的楼层有没有置位的bit有就继续走没有就掉头。这里的扫描用循环比用位运算直观代码量也差不多unsigned char scan_up_request(unsigned char cur_floor) { unsigned char i; for (i cur_floor 1; i FLOOR_NUM; i) { if (req_flag (1 (i - 1))) return i; } return 0; // 上方无请求 }同理可以写scan_down_request向下扫描。0表示没有请求非0则表示目标楼层。这套逻辑放在状态机里就是“下一步跳转的唯一依据”。3.3 时间轴怎么来定时器节拍做1ms系统心跳电梯程序里到处都需要“延迟一段时间”——开门保持5秒、关门动作2秒、按键消抖20毫秒。新手最容易写出的问题是用delay_ms(5000)死等这会把整个单片机卡死在延时循环里按键、传感器、数码管全部失去响应。我的做法是定时器0配置成1ms中断维护一个系统节拍变量所有需要定时的地方都基于节拍去判断“时间到了没有”而不是阻塞等待。这样主循环永远有空闲去扫描IO、刷新状态机。volatile unsigned int sys_tick 0; // 1ms递增 void Timer0_Init() { TMOD 0xF0; TMOD | 0x01; // 定时器0模式116位定时 TH0 0xFC; // 12MHz晶振下1ms初值 TL0 0x18; ET0 1; TR0 1; EA 1; } void Timer0_ISR() interrupt 1 { TH0 0xFC; TL0 0x18; sys_tick; }这里的定时器初值0xFC18是对应12MHz晶振的。换晶振频率时初值要用65536 - 晶振频率 / 12 / 1000重新算不要生搬硬套。比如11.0592MHz晶振1ms初值就是65536 - 11059200 / 12 / 1000 65536 - 921 64615即0xFC67。很多程序跑飞或者时间不准根源就是换了晶振没换初值还以为是代码逻辑错了。4. 从零组装C代码主循环、楼层检测与数码管显示的落地写法4.1 主循环与状态机跃迁一个精简的switch-case骨架状态机的主循环一般长这样先刷按键请求再读传感器最后根据当前状态决定要不要切换状态。下面这段代码可以直接当骨架用void main(void) { Timer0_Init(); while (1) { key_scan(); // 扫描按键登记请求 update_current_floor(); // 根据干簧管更新当前楼层 state_machine_run(); // 状态机跃迁逻辑 display_refresh(); // 数码管显示非阻塞 } }void state_machine_run(void) { switch (current_state) { case ST_IDLE: if (req_flag) { if (get_target_floor() current_floor) { set_motor(MOTOR_UP); current_state ST_MOVE_UP; } else if (get_target_floor() current_floor) { set_motor(MOTOR_DOWN); current_state ST_MOVE_DOWN; } } break; case ST_MOVE_UP: if (is_at_floor(current_floor 1) is_leveled()) { set_motor(MOTOR_STOP); // 到站先停 open_door(); current_state ST_DOOR_OPEN; clear_request(current_floor); } if (!scan_up_request(current_floor)) { set_motor(MOTOR_STOP); // 上方无请求掉头准备 current_state ST_IDLE; } break; case ST_DOOR_OPEN: if (door_open_timeout()) { // 开门已到设定时间 close_door(); current_state ST_DOOR_CLOSE; } break; default: break; } }这段代码里is_leveled()是平层检测函数它必须被严格执行——电梯不是一检测到楼层信号就停而是要在平层窗口中才停否则轿厢会卡在两层楼之间。这也是很多源码里被一笔带过的地方实际调试最容易栽跟头。4.2 楼层检测干簧管抖动用“连续N次有效”过滤干簧管的抖动不会有按键那么剧烈但轿厢经过层站时如果磁铁位置不精确检测点会反复吸合断开好几次。我用的消抖方式和按键消抖类似但阈值不同按键消抖一般10到20毫秒干簧管我用连续5次扫描都检测到同一楼层有效每次扫描间隔5毫秒也就是总共25毫秒的稳定时间。#define FLOOR_SAMPLE_COUNT 5 unsigned char current_floor 1; // 当前楼层 unsigned char sample_counter 0; // 连续采样计数 void update_current_floor(void) { unsigned char i; unsigned char floor_now 0; // 扫描4层干簧管低电平有效 for (i 0; i FLOOR_NUM; i) { if (floor_sensor_read(i) 0) { floor_now i 1; break; } } if (floor_now 0) { sample_counter 0; // 电梯在两层之间不在任何楼层 return; } if (floor_now current_floor) { sample_counter; } else { sample_counter 1; // 检测到新楼层开始重新计数 } if (sample_counter FLOOR_SAMPLE_COUNT) { current_floor floor_now; } }这个设计的参数含义是FLOOR_SAMPLE_COUNT决定了电梯要多“确定”才认定自己到了某个楼层。值设大了电梯会冲过层站值设小了可能被一次抖动骗到。我一般从5开始调板子抖动大就往上加到8或者10。4.3 按键扫描与请求登记内呼外呼共用一个楼层请求表电梯的门控逻辑里内呼和外呼不一样——内呼是你进了轿厢按楼层外呼是在楼道里按上行/下行。但到了单片机这里它们可以统一处理最终都是“向某个楼层申请服务”。区别只在方向标志上。我一般用一个字节的内呼请求和一个字节的外呼请求或者干脆合一看题目的复杂度。#define KEY_PORT P1 void key_scan(void) { static unsigned char key_last 0xFF; unsigned char key_now KEY_PORT 0x3C; // 只取P1.2-P1.5的按键 if (key_now ! key_last) { if ((key_now 0x08) 0) // 2楼内呼键按下沿 req_flag | 0x02; if ((key_now 0x10) 0) // 3楼内呼键按下沿 req_flag | 0x04; key_last key_now; } }这里最关键的是只处理“按下沿”——按键按下是一个跳变如果不在key_last里比较程序每循环一遍就会登记一次电梯会跑到天荒地老。加上这个沿检测按一次只置位一次。如果你有键盘消抖的要求在按键扫描前加一个20毫秒的延时判断即可但不要用阻塞式delay要在系统节拍里做。4.4 数码管显示定时器中断里做动态扫描电梯楼层显示通常用两位数码管显示当前楼层或者轿厢方向箭头。动态扫描必须在定时器中断里做不能在主循环里做原因是主循环的时间不确定——按键扫描耗时、状态机判断耗时这些都会让显示间隔变长然后数码管就会出现肉眼可见的闪烁。unsigned char code seg_code[10] { 0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8, 0x80, 0x90 }; // 共阳极数码管段码表0-9 void display_refresh(void) { static unsigned char disp_index 0; P2 0xFF; // 先关闭所有位选消隐 if (disp_index 0) P0 seg_code[current_floor / 10]; else P0 seg_code[current_floor % 10]; disp_index (disp_index 1) % 2; P2 (disp_index 0) ? 0x01 : 0x02; // 打开当前位选 }P0口输出段码前必须先关闭位选这就是经典的“消隐”步骤。不然你在切换显示位时前一位的数据会残留在下一位上产生“鬼影”。另外要留意STC89C52的P0口是开漏输出直接输出高电平驱动数码管的段码能力很弱正确做法是段码接上拉电阻或者用灌电流方式驱动共阳数码管。这也是51单片机驱动LED最常见的翻车点——输出高电平点不亮不是程序写错了是电气特性使然。5. 电梯控制系统的五个常见坑现象、原因与解决办法5.1 楼层信号乱跳电梯显示在几层之间反复横跳现象电梯明明停在2楼数码管却时而显示2时而跳成3万用表量干簧管通路也是好的。原因干簧管是机械触点轿厢带磁铁经过时的吸合过程本身就伴随多次通断这不是短路是正常抖动。万用表测量只反映最终状态测不出高频抖动。解决按上一节的“连续N次采样有效”方案处理。注意这里不能用简单的电平判断必须计数而且要在主循环里利用定时器节拍保证每次扫描间隔是稳定的5ms而不是跟着主循环时长跑。另外检查一下磁铁和干簧管的安装距离我吃过一个亏——磁铁离干簧管只有5毫米吸合后磁性过强触点释放都变慢了实测拧开螺丝把距离调到15毫米问题消失。5.2 数码管亮度不均匀还有拖影现象楼层数码管显示时亮时暗快速切换时数字边缘有残影。原因动态扫描频率太低或者没有消隐。很多人的代码是在主循环里delay一下再刷新下一位延迟时间不准扫描频率就飘了。解决把扫描搬进定时器中断1ms刷新一位两位就是2ms一个完整轮询刷新率500Hz人眼完全看不出闪烁。消隐的细节是切位选之前先关闭所有位选也就是把P2全部置1再切换段码最后才打开目标位选。顺序不能反反了就是残影。5.3 继电器一动作单片机就复位程序重启现象电梯刚开始运行或者到站停靠时继电器发出咔哒声的瞬间数码管熄了过了一会儿又亮了感觉像程序跑飞重启。原因继电器线圈是感性负载断电瞬间会产生反向电动势几十伏甚至上百伏的尖峰脉冲直接串回电源轨把单片机的VCC砸出毛刺复位电路误判成掉电重启。我用示波器遇到过毛刺最高到48V无论单片机扛不扛得住这种都不该出现在MCU电源上。解决继电器线圈两端并联续流二极管方向是负极接电源正极、正极接驱动管集电极——这是续流的标准接法。另外电机两根线之间也要并电容我一般用104瓷片电容加100uF电解电容组合。如果条件允许用光耦把控制侧和驱动侧的电源彻底隔开这是最干净的方案。5.4 电梯到层不停直接冲过楼层现象电梯明明已到达目标楼层传感器也触发了但电机不停继续往前冲了几层才突然停住。原因这个最玄学。排查下来一半以上的原因是主循环里某个函数卡住了比如有人在状态机里用了一个延时500ms的消抖函数电梯在高速运行时500ms足够冲过一整层了。另外一部分原因是is_leveled()平层判断有问题——只检测了“到了楼层”没检测“平层窗口”电梯在下一层的干簧管还没触发时就被判定为“还没到”。解决把主循环里所有阻塞式延时全部替换成基于sys_tick的非阻塞判断。平层检测要用双信号上平层传感器和下平层传感器都进入触发区才算平层这样电梯停的位置才精准。5.5 单片机下载失败烧录不进去现象程序编译通过点击下载软件后提示“仍在连接中请给MCU上电”反复操作无效。原因STC单片机走的是串口冷启动下载协议——先让单片机断电点击下载后瞬间上电芯片才能进入下载模式。如果你点了下载才开始断电上电时序可能已经错了。另外就是波特率设太高、下载线太长太软导致信号失真。解决养成好习惯在下载软件点“下载/编程”按钮之后再给板子断电上电。下载电路板上确保有串口的电平转换芯片不要直接用USB转TTL线去连没有转换的板子。我个人的经验是把下载波特率从115200降到57600在长线缆环境下一把过。6. 验证这套电梯程序的三层手段外加一个血泪教训6.1 第一层串口打印状态机切换日志电梯程序是典型的“黑匣子”——它在跑你肉眼看不见内部状态单看电机转不转根本判断不了逻辑对不对。我的习惯是在每个状态切换时打一行串口日志当前时间、状态名、当前楼层、请求标志。跑一遍测试场景然后把串口日志拉出来对表。void log_state_change(LiftState old_state, LiftState new_state) { printf(t%u state:%d-%d floor%d req0x%02X\n, sys_tick, old_state, new_state, current_floor, req_flag); }串口日志比LED灯管用得多因为你把八个LED全接上也就只能看到八个bit而串口能把100个变量的值同时吐出来。不需要真接硬件在Proteus仿真里用虚拟串口配合“串口助手”就能调完一大半逻辑。6.2 第二层Proteus里搭虚拟电梯环境仿真最怕的是环境跟实物偏差太大但电梯这种低速控制场景仿真验证状态机的正确性是完全够用的。在Proteus里画好电机、传感器、按键这些虚拟元件把编译好的hex文件烧进仿真芯片然后跑测试场景从1楼出发分别按下2、3、4楼的内呼键观察电梯是否按照方向优先的规则一个一个响应。不过要记住一条边界Proteus仿的是逻辑不是物理。干簧管的机械抖动、继电器的反向电动势、数码管驱动电流不足这些在仿真里都看不到。所以我坚持有两个独立验证步骤仿真只验证状态机和调度逻辑硬件再验证电气可靠性。仿真过了只能说“逻辑对”不能说“板子行”。6.3 第三层硬件点位用万用表和示波器复查硬件调试我一般拿万用表先量三处VCC与GND之间电压是否稳定在5V量干簧管两端在磁铁靠近时是否确实导通量继电器线圈在动作时是否每路都有续流二极管。如果板子有异常复位就把示波器夹在VCC上看毛刺如果楼层信号乱跳把示波器探头夹在干簧管输出脚上看实际波形。硬件调试出问题后要改电路很麻烦——这是我最想早点明白的事情。后来我养成了一个习惯把所有可以软件调整的参数全部宏定义包括消抖次数、开门保持时间、关门超时时间这样电路板上哪怕有些电气参数不理想软件也能先兜住。这算是给自己的板子留的后悔药。6.4 最后讲一个我踩过的真坑有一版程序我偷懒没有做“方向优先”的完整判断只在ST_MOVE_UP里判断了“当前楼层上方有没有请求”却没判断“如果上方有请求但还没到那一层此时下方有人按下行——要不要掉头”。结果我的电梯在3楼向上2楼有人按下行程序立刻掉头向下跑而4楼的内呼请求已经被登记了但没被响应。乘客在4楼按了内呼电梯却往下走了这要是真实电梯投诉单能堆到天花板。这个坑的根因是我把“响应请求”和“响应方向”混在了一起。正确的逻辑是电梯一旦确定上行方向在方向不变的前提下先把上方所有请求都服务完然后才考虑掉头。方向优先级的所有判断都应该集中在一个函数里而不是散落在各个状态里。希望这篇关于单片机电梯程序控制系统的拆解能帮到你。如果你也卡在状态机设计或者调度逻辑上可以试着把电梯的运行轨迹画出来、把每层的请求写成一串二进制然后对着纸推演一遍这个步骤能省下你至少三个小时的烧录调试时间。本文还有配套的精品资源点击获取