
简介面向电子、通信及计算机类专业学生的电梯控制器Verilog语言课程设计说明书以6层电梯控制器为实例系统梳理从需求分析、设计原理到实现的完整流程重点包括上升/下降请求处理、楼层指示数码管、开关门时序控制等设计要点并给出基本状态设计、模块设计、波形仿真等核心内容对电梯升降控制、开关门状态转换等关键逻辑也做了详细说明有助于理解Verilog HDL有限状态机设计及Quartus II仿真调试方法。压缩包内共1个doc文件大小约282KB排版清晰包含摘要、目录、正文、流程图、参考文献与附录可直接用作课程设计报告蓝本。目前已有2230人学习适合需要快速完成同类课题或深入掌握电梯控制器设计思路的人群使用。 做硬件课程设计有个很现实的问题题目看似简单写起来特别容易翻车。比如“电梯控制器 Verilog语言课程设计”这种经典题目网上代码一抓一大把但很多同学下了工程、跑完仿真就交差了结果一到综合、上板全是问题。我这些年带过不少学生做这块也踩过不少坑想把这些经验写出来给正在做或者准备做的同学一个能直接照抄的思路。这个题目选得其实很聪明。电梯控制器非常适合拿来练手它既有典型的时序逻辑楼层计数、按键消抖又有状态机思想电梯的运行控制还涉及模块化设计按键处理、请求仲裁、显示译码、控制器本身可以拆得很干净覆盖了Verilog课程设计的大部分考点。做完这一个项目你基本就把FPGA开发最核心的那套流程走了一遍后面的项目只会越来越顺手。接下来说说我是怎么把它从思路到代码、再到仿真和排坑一步步实现的内容会比较实在代码风格可以直接参考。1. 项目概述与设计思路拆解1.1 为什么课程设计偏爱电梯控制器很多同学问我随便做个计数器、流水灯不也一样能过吗说实话能过但学不到东西。电梯控制器是少有的能在“小规模FPGA”上体现完整控制逻辑的题目。它要求你处理多个输入各楼层呼叫按钮、轿厢内选层按钮、多个输出电机正反转、开关门、楼层显示还要处理状态切换的时序。更重要的是它有明确的“业务规则”电梯在向上运行的时候只响应同方向的呼梯请求到了最顶层必须折返优先响应当前运行方向上的最近楼层。这种业务逻辑非常适合用有限状态机FSM来表达也是面试时数字IC岗位经常挂出来的“电梯调度”简化版。换句话说这题做好了你简历上还真能写一句“基于状态机的电梯控制器设计与实现”比流水灯有说服力得多。1.2 模块划分与状态机初构我的建议是拿到这个题先别急着写代码先画模块框图。我把整个系统拆成了四个核心部分按键消抖模块处理所有按键输入防抖后再给控制器楼层计数/位置模块实时记录电梯当前所在楼层请求仲裁模块根据电梯运行方向选出下一次要停的楼层主控状态机控制电梯运行、停止、开关门每个模块可以单独写testbench验证最后再进行顶层例化。电梯的主控状态机我一般定义这样几个状态localparam IDLE 3d0; // 空闲 localparam UP_START 3d1; // 上行加速/匀速 localparam DOWN_START 3d2; // 下行 localparam DOOR_OPEN 3d3; // 开门 localparam DOOR_CLOSE 3d4; // 关门 localparam WAIT 3d5; // 等待新指令为什么不用一条if-else链硬怼因为电梯的运行逻辑天然是分阶段的启动、运行、停靠、开关门、等待。状态机能让你把每个阶段的行为独立描述出来后期加功能比如超时强制关门、紧急制动非常方便只需要新增状态和转移条件不会把逻辑改成一团乱麻。2. 核心模块实现与代码细节2.1 按键消抖与楼层计数先解决“物理层”问题一定要把按键消抖放在第一位做。这句话我强调多少次都不为过。机械按键在按下和松开的瞬间电平会抖动几十毫秒如果不处理一次按下去可能被控制器识别成七八次电梯会直接疯掉。消抖的思路很简单检测到按键电平变化后持续采样如果稳定了比如20ms再输出有效电平。实际实现用的是计数器module key_debounce #(parameter N 20) ( input wire clk, input wire rst_n, input wire key_in, output reg key_out ); reg [19:0] cnt; reg key_reg1, key_reg2; // 两拍同步消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin key_reg1 1b1; key_reg2 1b1; end else begin key_reg1 key_in; key_reg2 key_reg1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 20d0; key_out 1b1; end else if (key_reg2 ! key_out) begin // 电平不稳定开始计数 if (cnt 20d999999) begin // 20ms 50MHz key_out key_reg2; cnt 20d0; end else begin cnt cnt 1b1; end end else begin cnt 20d0; end end endmodule注意代码里先做了两拍同步这一步很多教程不会重点讲但在真实工程里非常重要——按键输入来自板子外部和时钟域不同直接采进来可能有亚稳态风险。楼层计数相对简单但有一个坑电梯上行和下行时楼层计数方向是相反的。我习惯用两个计数器或者一个带方向的计数器。这里给出一个带方向控制的楼层计数器module floor_counter ( input wire clk, input wire rst_n, input wire up, input wire down, input wire [2:0] floor_max, output reg [2:0] floor ); always (posedge clk or negedge rst_n) begin if (!rst_n) floor 3d1; else if (up) begin floor (floor floor_max) ? floor : floor 1b1; end else if (down) begin floor (floor 3d1) ? floor : floor - 1b1; end end endmodule这里floor_max是顶层参数比如6层楼就传6。为什么用参数而不是写死因为以后想扩展成8层、10层只改参数就行了不用动逻辑。2.2 请求仲裁与运行控制状态机是核心电梯的请求分楼层呼叫和轿厢内选层。这里最需要想清楚的问题是电梯当前上行、已有请求楼层集合怎么确定下一个停靠目标简单做法是遍历所有楼层请求找出“在运行方向上最靠近当前位置的那个楼层”。这个逻辑用Verilog写可能稍绕但思路并不复杂电梯上行时优先找比当前楼层高的、最小楼层作为目标电梯下行时优先找比当前楼层低的、最大楼层作为目标都找不到则改变方向两个方向都找不到进入空闲等待等请求到达后再启动。这部分我贴一段状态机控制的核心代码跑仿真时会特别直观。always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; motor_up 1b0; motor_down 1b0; door_open 1b0; end else begin case (state) IDLE: begin door_open 1b1; if (req_in) state DOOR_CLOSE; end DOOR_CLOSE: begin door_open 1b0; // 关门定时器满后判断目标方向 if (target current_floor) begin motor_up 1b1; motor_down 1b0; state UP_START; end else if (target current_floor) begin motor_down 1b1; motor_up 1b0; state DOWN_START; end else state DOOR_OPEN; end UP_START: begin if (current_floor target) state DOOR_OPEN; end DOWN_START: begin if (current_floor target) state DOOR_OPEN; end DOOR_OPEN: begin // 开门定时器时间到后若有新请求则关门否则继续等 if (door_timer_done new_request) state DOOR_CLOSE; end default: state IDLE; endcase end enddefault分支一定不能省。状态机综合出来如果没有default遇上未定义状态寄存器状态会维持在未知值上板后就容易“死机”。这是很多同学仿真没问题、一跑板子就挂掉的重要原因之一。2.3 数码管显示与顶层例化显示模块通常用动态扫描给定的时钟比如50MHz扫描速度要大于人眼视觉暂留频率大概超过60Hz一般用1kHz到2kHz逐个数码管刷新就够用。扫描时注意切换位选信号后要等一小段时间再把段选数据送出去否则肉眼看会有很明显的拖影。顶层模块的主要工作就是例化所有子模块连线。这里最容易出的问题是端口方向搞反编译报错一串“port width mismatch”或者“cannot be driven”之类。我的习惯是写顶层之前先把每个子模块的端口列成一个表格标清楚方向、位宽、连接信号再开始写代码基本能避免一半以上的连线错误。3. 实操记录Modelsim仿真与波形验证3.1 仿真环境的搭建与激励编写我用Modelsim的主要流程是新建工程、添加所有.v源文件、再添加testbench也叫tb文件、编译、仿真、看波形。Testbench不属于可综合代码一般建议和源文件分开放最好在文件名里加个_tb后缀后期找起来方便。Testbench里要做的几件事比较固定生成时钟信号生成复位时序按输入场景产生按键信号检查输出结果是否符合预期时钟我一般这么产生parameter CLK_PERIOD 20; // 50MHz周期20ns always #(CLK_PERIOD/2) clk ~clk;复位时序基本是initial begin clk 1b0; rst_n 1b0; #100 rst_n 1b1; end注意复位时间要足够长让状态机的初始状态稳定建立。有些同学复位只给一个时钟周期仿真也能跑但这代表不了真实板子的上电情况建议至少给100ns以上。3.2 激励场景的设计仿真不是随便按几个按键要有目的性。我的建议是至少覆盖这几类场景单层呼梯电梯从1层到3层过程简单重点看楼层计数是否逐层加1同向顺路停靠电梯上行1→3过程中又有人在2层按下行请求此时电梯应该先停2层还是直接到3层再返回我的实现是若2层请求方向与当前运行方向一致则2层顺路停靠若方向相反则等电梯折返后再响应反向请求电梯上行时收到下行请求应该等上行任务结束后再响应满楼层覆盖连续请求1到6层看状态机是否稳定不出现跳变或者漏停签名注意一定要在仿真波形里观察current_floor是否精确等于目标楼层很多隐藏bug会在这个环节暴露出来。3.3 用task简化复杂激励当场景越来越多直接用initial块写会变得混乱。建议用task封装操作。比如task press_key; input [2:0] floor; begin key_in floor; #1000; key_in 3b000; #500; end endtask这样在initial里调用非常直观initial begin press_key(3d3); press_key(3d1); #5000; $stop; end代码更干净后面想加场景也只是多两行调用。3.4 波形分析的正确姿势波形出来以后先不要急着截图先看几个关键信号复位信号是否正常拉高状态机的状态变化是否符合设计预期楼层计数是否按照时钟沿逐层递增或递减目标楼层请求是否在合适的时间点被锁存如果状态机在某个状态卡住先把对应时刻的输入信号全部列出来看看是不是某个条件没满足。比如常见的卡死是电梯停在某个楼层后door_timer计数到了但new_request一直为0状态机就永远停在DOOR_OPEN。仿真看起来像“命令执行完了没反应”本质就是请求信号没拉高需要在激励里补上。看到有不确定值“X”也先不要慌习惯用鼠标拖拽时间轴定位到第一次出现X的时间点往前倒推是哪个信号导致的。一般就是复位没做好或者某个变量在初始时刻没有被赋初值。4. 常见问题与排查技巧实录4.1 这些问题我见过太多次了现象可能原因解决手段仿真波形全是红X信号没初始化检查rst_n是否拉高变量是否有初值状态机顺序乱跳缺少default分支补全case的default回IDLE按键按一次楼层加两次按键未消抖消抖模块没生效或消抖时间太短电梯上下行方向反了目标楼层判断的“大于小于”写反核对target和current_floor比较逻辑上板后显示乱码或闪烁动态扫描时序有问题位选切换和段选送数之间加延时有些楼层呼叫不响应请求锁存丢失确认请求信号在运行过程中是否保持有效4.2 我踩过的几个深坑第一个坑是楼层数位宽不够。比如设计是6层楼current_floor定义成3bit够用。但有人图省事把目标楼层也定义成3bit在做减法判断方向时比如3 - 5在无符号数里就变成了6导致电梯判断方向反了。这里两个办法一个是把变量定义成signed或者直接改用if (target current_floor)这样的大于小于比较而不用减法。第二个坑是异步复位释放。如果rst_n和时钟不同步有概率出现复位释放时正好器件采到亚稳态。推荐在复位模块里先使用内部同步复位逻辑把外部复位打两拍再使用这样在项目答辩时也能说出一些自己的考虑。第三个坑是modelsim里波形出来了但特别毛刺各种杂散脉冲。这个大概率是全组合逻辑输出的毛刺不是真实板子上的问题但如果你用来判断状态会出错。所以我的习惯是所有跨模块的输出信号尽量打一拍寄存后再使用既增强时序可靠性又让仿真波形清爽很多。4.3 关于仿真和上板的差异仿真通过只是第一步上板以后你会发现有些问题仿真根本看不出来。比如板子上的晶振频率可能和modelsim仿真时用的时钟不一样如果你的消抖计数器上限是按50MHz算的结果板上实际是12MHz那消抖时间就完全不对。我的建议是顶层把时钟频率定义成parameter所有分频或计数的计算都基于这个参数这样换板子只改一处。再比如LED数码管显示仿真里你只看得到各个位选和段选信号的电平变化但人眼感受不到亮度、拖影、扫描噪声这些只能靠实际看板子才能调。扫描不过关时有人会看到重影这时优先检查位选信号是否在段选数据稳定后才切换以及扫描周期是否太长。5. 这个项目做完我建议你再做两步扩展如果还想让课程设计更完整一点或者想为下一份实习准备点项目经验有两个改动成本低但效果很好的扩展方向。第一加入电梯内开关门超时报警。给开门状态加一个定时器超过设定时间蜂鸣器响这在状态机里加一个定时分支就行难度不大但对时序的理解会更进一步。第二把单台电梯的控制扩展到双电梯联动。这是很多大厂数字逻辑面试的简化版场景。你需要考虑两台电梯之间如何分配请求最笨的办法是固定一个负责单数层、一个负责双数层简单但能展示模块间通信能力。我在实际调试这个项目时最大的感受是Verilog语法本身只占三成精力剩下七成都在和各种“没想到”做斗争——没想到方向判断会反没想到按键会连跳没想到仿真好好的上板却乱闪。这些问题恰恰是这个课程设计最值钱的地方因为它们是真实硬件工程的一部分你在教科书上见不到只有亲手调过才知道。最后再说一个小技巧把每个模块的验证场景写成注释放在testbench的头部下次回来看代码时你真的会感谢那个把过程记清楚的自己。本文还有配套的精品资源点击获取