☰
C语言单片机电梯控制程序核心:状态机与调度逻辑深度解析
2026/10/27 23:56:24 网站建设 项目流程

简介:这是一份基于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,模式1(16位定时) 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 req=0x%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楼按了内呼,电梯却往下走了,这要是真实电梯,投诉单能堆到天花板。

这个坑的根因是:我把“响应请求”和“响应方向”混在了一起。正确的逻辑是:电梯一旦确定上行方向,在方向不变的前提下,先把上方所有请求都服务完,然后才考虑掉头。方向优先级的所有判断都应该集中在一个函数里,而不是散落在各个状态里。

希望这篇关于单片机电梯程序控制系统的拆解能帮到你。如果你也卡在状态机设计或者调度逻辑上,可以试着把电梯的运行轨迹画出来、把每层的请求写成一串二进制,然后对着纸推演一遍,这个步骤能省下你至少三个小时的烧录调试时间。

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

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

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

立即咨询