资讯动态

基于FPGA的地铁售票系统设计:从状态机到数码管驱动的完整实现

发布时间:2026/9/2 7:37:45 来源:尧图企业网站定制
简介本资源是一套完整的FPGA课程设计项目——地铁售票模拟系统面向计算机、软件工程、电子信息、自动化等专业的本科生及初学者用于实践数字逻辑设计、Verilog开发与FPGA工程实现能力。压缩包共18个文件涵盖核心Verilog源码.v、Vivado工程文件.xpr、.xdc、.lpr、仿真测试脚本.py、需求与设计说明文档.pdf、.md、票价配置表.xlsx及开源协议LICENSE总大小994KB结构清晰、模块完整便于理解顶层架构与各功能模块如按键输入、票价计算、票务状态机、数码管显示的协同逻辑。已有139人学习下载项目已通过导师评审并获95分高分所有代码在Windows/macOS/Linux多平台实测运行稳定配套资料齐全既可直接用于课程设计交付也支持二次开发与功能拓展是FPGA数字系统实践的优质入门范例。1. 项目概述与核心价值最近在整理大学时期的项目资料翻出了一个当年花了不少心思的课程设计——“基于FPGA的地铁售票模拟系统”。这个项目用Verilog HDL在Nexys4 DDR开发板上实现从状态机设计到外设驱动完整走了一遍FPGA开发流程。现在回头看它不仅仅是一个应付作业的“玩具”更是一个绝佳的FPGA入门综合实践案例涵盖了数字系统设计的核心思想从需求分析、模块划分、代码编写、仿真验证到最终的上板调试。如果你正在学习FPGA或者对如何将理论知识转化为一个可运行的实际系统感到困惑那么这个项目的设计思路和实现细节或许能给你带来不少启发。它模拟了地铁售票的基本流程包括线路选择、票价计算、投币找零、票卡打印模拟等虽然界面是简单的七段数码管和LED但背后的状态控制和数据流处理正是FPGA应用的典型场景。2. 系统整体架构与设计思路拆解2.1 需求分析与功能定义一个地铁售票机核心功能其实很明确人机交互、计费逻辑、交易执行。在我们的FPGA实现中需要将这些抽象功能映射到具体的硬件资源和操作流程上。人机交互由于开发板资源限制我们无法使用复杂的触摸屏。因此采用按键作为输入如线路选择、确认、取消七段数码管作为信息显示显示票价、投币金额、找零金额LED作为状态指示如交易成功、找零中。这要求设计清晰、简洁的状态提示逻辑。计费逻辑这是系统的核心算法。我们需要一个“票价表”通常基于简化的地铁网络模型例如按站数区间计费或固定线路票价。FPGA内部需要用存储器如查找表LUT或例化的ROM来存储票价信息并根据输入的选择实时计算出应付金额。交易执行模拟投币和找零过程。我们需要一个“投币金额累加器”和一个“找零计算模块”。当投币金额大于等于票价时触发出票用LED或特定数码管显示模拟并计算找零若取消交易则退还已投金额。2.2 硬件平台选型为什么是Nexys4 DDR当时选择Nexys4 DDR开发板是基于教学和项目需求的综合考虑资源充足其搭载的Xilinx Artix-7 FPGAXC7A100T拥有丰富的逻辑单元、Block RAM和DSP切片足以应对一个中型数字系统设计为状态机、计数器、票价存储和运算单元提供了充裕的空间。外设丰富板载了16个拨码开关、16个LED、5个按键、8个七段数码管。这正好完美匹配了我们项目的输入输出需求按键和拨码开关用于输入LED和数码管用于输出。无需额外购买模块降低了成本和复杂度。开发环境成熟配套的Vivado设计套件是业界标准学习资料多仿真、综合、实现、调试工具链完整。板载的USB-JTAG编程器也使得上板调试非常方便。DDR内存虽然我们这个项目没有用到其DDR2内存但它意味着该板卡能力上限高项目后续若有扩展如存储更多线路信息、更复杂的图形界面缓存留有空间。2.3 顶层模块划分与互联根据自顶向下的设计方法我们将系统划分为几个功能明确的子模块并通过顶层模块Top Module进行例化和连接。这是保证代码可读性、可维护性和可调试性的关键。输入去抖模块 (debounce)机械按键在按下和释放时会产生物理抖动导致单次按压被误识别为多次。这个模块用于对按键输入进行消抖处理产生干净的脉冲信号。通常采用一个约20ms的移位寄存器或计数器来实现。票价计算模块 (fare_calculator)该模块接收用户选择的“起点站”和“终点站”编码来自按键或拨码开关查询内部ROM中预存的票价表输出对应的票价金额。票价表在Verilog中可以用case语句或初始化一个ROM IP核来实现。投币与找零控制模块 (coin_handler)这是系统的“财务中心”。它包含一个累加器用于累加用户模拟投入的“硬币”通过按键模拟不同面额如1元、5元。实时比较累加金额与票价计算模块输出的票价。当金额足够时产生“交易有效”信号并计算找零金额累加金额-票价。它还需要处理“取消”信号清空累加器。显示驱动模块 (display_driver)负责将内部处理的数字信息票价、投币金额、找零金额、状态码转换为七段数码管和LED能够显示的信号。这包括二进制到BCD码的转换、数码管动态扫描驱动多位数码管共用驱动引脚通过快速轮流点亮实现稳定显示以及根据状态机控制显示内容。主控状态机 (main_fsm)这是系统的大脑一个有限状态机FSM。它定义了售票流程的所有状态例如IDLE空闲等待选择、SELECTING选择线路/站点、WAITING_FOR_COIN等待投币、CALCULATING计算找零、DISPENSING_TICKET出票、DISPENSING_CHANGE找零、CANCELLED取消。状态机接收来自去抖后按键的事件如“确认”、“取消”、“投币”结合其他模块的状态如金额是否足够产生状态跳转并输出控制信号给其他所有模块。注意模块划分的粒度很重要。过粗则模块内部逻辑复杂不利于调试和复用过细则顶层连线繁琐增加时序收敛难度。通常将具有明确独立功能的部分单独成模块。3. 核心模块的Verilog实现细节与难点解析3.1 有限状态机FSM的设计与编码规范状态机是本系统的控制核心。一个健壮的状态机设计应清晰区分现态、次态和输出逻辑。// 状态定义使用独热码One-Hot或二进制编码。对于状态数不多的情况独热码更直观且转换逻辑简单。 localparam S_IDLE 7b0000001; localparam S_SELECT_STATION 7b0000010; localparam S_WAIT_COIN 7b0000100; localparam S_CALC_FARE 7b0001000; localparam S_DISPENSE_TICKET 7b0010000; localparam S_DISPENSE_CHANGE 7b0100000; localparam S_CANCELLED 7b1000000; reg [6:0] current_state, next_state; // 第一段时序逻辑状态寄存器更新 always (posedge clk or posedge rst) begin if (rst) current_state S_IDLE; else current_state next_state; end // 第二段组合逻辑次态判断 always (*) begin next_state current_state; // 默认保持现态避免锁存器 case (current_state) S_IDLE: if (start_btn) next_state S_SELECT_STATION; S_SELECT_STATION: if (confirm_btn) next_state S_CALC_FARE; else if (cancel_btn) next_state S_CANCELLED; S_CALC_FARE: begin // 票价计算完成是一个瞬间信号计算完成后自动跳转 if (fare_valid) next_state S_WAIT_COIN; end S_WAIT_COIN: if (coin_inserted total_coin fare) next_state S_DISPENSE_TICKET; else if (cancel_btn) next_state S_CANCELLED; // ... 其他状态转移 endcase end // 第三段组合逻辑或时序逻辑输出控制信号 // 根据current_state产生控制其他模块的信号如enable_display, ticket_ready, change_ready等。 always (posedge clk) begin ticket_ready 1b0; // 默认值 case (current_state) S_DISPENSE_TICKET: begin ticket_ready 1b1; // 可以在此启动一个计时器模拟出票过程 end // ... endcase end实操心得三段式状态机是强烈推荐的编码风格。它将时序、次态逻辑和输出逻辑分离结构清晰综合工具优化效果好能有效避免组合逻辑环路和毛刺。状态编码选择状态少于8个时用二进制码状态较多且转换简单时用独热码。独热码的每个状态位驱动一个简单的逻辑在FPGA中利用丰富的触发器资源有时能获得更好的时序性能。默认赋值在组合逻辑的always (*)块中务必给next_state一个默认值通常是保持现态否则会生成不期望的锁存器Latch。在时序逻辑的输出段也建议对每个输出信号在case语句前赋默认值。3.2 票价存储与查询ROM的使用票价表是只读数据非常适合用ROM实现。在Vivado中我们可以用Core Generator生成一个ROM IP核也可以直接用Verilog数组初始化一个寄存器组来模拟。方法一使用Verilog数组适用于小容量、简单数据// 假设有4条线路票价固定 reg [7:0] fare_rom [0:3]; // 深度4宽度8位最大票价255 initial begin fare_rom[0] 8d3; // 线路1票价3元 fare_rom[1] 8d4; // 线路2票价4元 fare_rom[2] 8d5; // 线路3票价5元 fare_rom[3] 8d6; // 线路4票价6元 end // 查询 assign fare fare_rom[station_index];方法二使用Vivado的Distributed Memory Generator或Block Memory Generator IP核对于更大的票价表例如基于站点的矩阵票价使用IP核更专业。在Vivado中创建ROM IP选择单端口ROM设置数据宽度和深度并加载一个COE格式的初始化文件。COE文件包含了所有地址对应的数据。难点与解决票价计算可能需要根据“起点”和“终点”两个索引来查询。一种方法是将二维票价表展平为一维ROM地址由{start_station, end_station}拼接而成。但这样ROM深度会呈平方增长。对于课程设计更实用的方法是简化模型比如只预设几条固定线路的票价或者按“区间数”通过一个简单的计算单元实时计算而非查表。3.3 数码管动态扫描驱动Nexys4 DDR的8个数码管是共阳极的并且段选信号a-g, dp是共享的位选信号AN0-AN7独立。要同时显示不同数字必须采用动态扫描。module display_driver ( input clk, // 扫描时钟建议几百Hz到1kHz input [31:0] data_in, // 要显示的8位BCD码数据每4位一个数字 output reg [6:0] seg, output reg [7:0] an ); reg [2:0] scan_cnt; // 扫描计数器 reg [3:0] digit_val; // 当前扫描位的数值 always (posedge clk) begin scan_cnt scan_cnt 1; // 循环计数0-7 end always (*) begin an 8b1111_1111; // 默认全灭 an[scan_cnt] 1b0; // 选中当前位低电平有效 // 根据scan_cnt从data_in中取出对应4位BCD码 digit_val data_in[ (scan_cnt*4) : 4 ]; end // BCD码转七段码译码器 always (*) begin case (digit_val) 4h0: seg 7b1000000; // 0 4h1: seg 7b1111001; // 1 // ... 2-9 default: seg 7b1111111; // 全灭 endcase end endmodule注意事项扫描频率频率太低如低于60Hz会看到闪烁频率太高则会增加功耗且可能因LED余辉不足导致亮度不均。通常选择200Hz至1kHz。消隐处理在切换位选信号an和段选信号seg时最好插入一个极短的消隐时间所有数码管关闭可以消除切换时的“鬼影”现象。这可以通过在an变化前先将seg设置为全灭不显示任何段来实现。亮度调节可以通过PWM控制每个数码管点亮的时间占空比来调节整体亮度。4. 系统集成、仿真与上板调试全流程4.1 顶层模块集成与引脚约束将所有子模块在顶层模块中实例化并正确连接信号线。然后创建XDCXilinx Design Constraints约束文件将顶层模块的端口映射到Nexys4 DDR开发板的具体物理引脚上。module top_system ( input clk100mhz, // 板载100MHz时钟 input [4:0] btn, // 按键假设btn[0]为复位btn[1]为确认... input [3:0] sw, // 拨码开关用于选择线路 output [6:0] seg, output [7:0] an, output [15:0] led // LED用于状态指示 ); // 时钟分频产生较低频率的时钟供不同模块使用 wire clk_1khz; // 用于数码管扫描 wire clk_debounce; // 用于按键去抖 // 去抖模块实例化 debounce u_debounce_confirm (.clk(clk_debounce), .btn_in(btn[1]), .btn_out(btn_confirm_clean)); // 主状态机实例化 main_fsm u_fsm (.clk(clk100mhz), .rst(rst), .confirm(btn_confirm_clean), ...); // 票价计算、投币处理、显示驱动等模块实例化... // 信号互联... endmodule引脚约束文件.xdc示例片段# 时钟 set_property PACKAGE_PIN E3 [get_ports clk100mhz] set_property IOSTANDARD LVCMOS33 [get_ports clk100mhz] create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk100mhz] # 按键 set_property PACKAGE_PIN P17 [get_ports {btn[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {btn[0]}] # ... 其他按键和拨码开关 # 七段数码管 set_property PACKAGE_PIN T10 [get_ports {seg[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {seg[0]}] # ... 其他段选和位选引脚 # LED set_property PACKAGE_PIN H17 [get_ports {led[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {led[0]}]4.2 功能仿真Testbench的编写在综合和上板之前必须进行充分的仿真。编写一个全面的Testbench模拟用户操作序列。timescale 1ns / 1ps module tb_top_system(); reg clk; reg rst; reg [3:0] sw; reg [4:0] btn; wire [6:0] seg; wire [7:0] an; wire [15:0] led; top_system uut (.*); // 实例化待测系统 initial begin clk 0; forever #5 clk ~clk; // 100MHz时钟周期10ns end initial begin // 初始化 rst 1; sw 0; btn 5b11111; // 按键默认高电平未按下 #100; rst 0; // 释放复位 #200; // 测试用例1正常购票流程 $display( Test Case 1: Normal Purchase ); sw 4b0010; // 选择线路2 #100; btn[1] 0; // 按下确认键 #20; btn[1] 1; // 释放确认键 #500; // 等待状态跳转和票价显示 // 模拟投币投入两个2元假设有对应按键 btn[3] 0; #20 btn[3] 1; // 按一次2元键 #100; btn[3] 0; #20 btn[3] 1; // 再按一次2元键 #500; // 检查LED和数码管输出可通过查看波形图或添加$display语句 // ... // 测试用例2取消交易 $display( Test Case 2: Cancel Transaction ); rst 1; #100; rst 0; // 复位系统 // ... 模拟选择后取消 #2000; $finish; end endmodule在Vivado中运行仿真观察波形图中各内部信号如状态机状态current_state、票价fare、投币总额total_coin、显示数据data_in等的变化是否符合预期。这是排查逻辑错误最有效的手段。4.3 上板调试与问题排查实录仿真通过后生成比特流文件并下载到FPGA。上板调试常会遇到仿真中未发现的问题。问题按键操作不灵敏或连击现象按一次键状态却跳转了多次。排查首先检查去抖模块。确保去抖时钟频率合适约10-20ms检测周期。用示波器或逻辑分析仪如果板子支持抓取去抖前后的信号波形看抖动是否被滤除。如果硬件不支持一个土办法是在代码里添加一个计数器每次按键有效时让一个LED翻转观察LED的翻转频率是否与按压次数一致。解决调整去抖计数器的阈值。确保去抖模块的输出是干净的、一个时钟周期的脉冲信号而不是一个持续的电平。可以让去抖模块在检测到稳定按下后输出一个单周期的高脉冲状态机用边沿检测来捕获这个脉冲。问题数码管显示乱码或闪烁现象显示的数字不对或者亮度不稳定、闪烁。排查乱码检查BCD码到七段码的译码表是否正确。检查digit_val的取值是否超出了0-9的范围可能由于数据通路错误导致。可以暂时将digit_val固定为一个值如4‘h5看是否稳定显示“5”。闪烁检查动态扫描的时钟频率。频率太低会肉眼可见闪烁。用示波器测量位选信号an的波形看其切换频率是否在200Hz左右。同时检查消隐处理是否做好。解决调整扫描时钟分频系数。在切换an和更新seg之间插入几个时钟周期的延迟消隐。问题状态机“卡死”在某个状态现象操作流程进行到某一步后系统再无反应。排查这是最典型的状态机问题。将状态机当前状态current_state用LED或数码管显示出来例如用LED二进制显示状态编码的低几位。操作时观察状态变化看是否在预期状态跳转。如果卡在某个状态检查进入该状态的转移条件是否过于苛刻或者离开该状态的转移条件是否永远无法满足。解决在状态机的default分支中将next_state强制设为S_IDLE作为一个安全恢复机制。仔细检查状态转移条件中的信号是否已经有效例如fare_valid信号是否在计算完成后正确拉高了一个周期。问题时序违例Timing Violation现象综合实现后报告建立时间Setup Time或保持时间Hold Time违例系统上板后行为不稳定。排查查看Vivado的时序报告找到违例的路径Path。通常是两个寄存器之间的组合逻辑路径太长延迟大在时钟沿到来时数据还未稳定。解决流水线Pipeline在长组合逻辑路径中插入寄存器将计算拆分成多个时钟周期完成。降低时钟频率如果性能要求不高可以降低系统主时钟频率。优化代码检查是否有优先级过高的长链if-else或case语句尝试用查找表或并行逻辑替代。对于本项目关键路径可能在票价计算或找零计算模块确保这些计算是纯组合逻辑且不过于复杂。5. 项目扩展思考与进阶方向完成基础功能后这个项目还有很大的扩展空间可以深化对FPGA和数字系统的理解。增加支付方式模拟支持“交通卡”支付。可以增加一个模拟的RFID或NFC读卡器模块用另一个按键模拟刷卡并设计一个简单的“电子钱包”逻辑存储在FPGA的Block RAM中实现扣款和余额查询。引入更复杂的人机交互使用VGA或HDMI接口连接显示器用FPGA实现一个简单的图形界面GUI显示地铁线路图、票价、操作提示等。这需要学习视频时序生成和图形渲染基础。网络化与多机互联如果有多块开发板可以尝试通过UART、SPI或以太网如果板子支持实现简单的通信模拟“中心服务器”与多个“售票终端”的数据同步比如同步票价表、统计销售数据。加入音频提示利用板载的音频编解码器或PWM驱动一个蜂鸣器在出票、找零、错误时发出不同的提示音提升用户体验。软核处理器集成对于非常复杂的业务逻辑如动态票价计算、交易记录存储可以考虑在FPGA中实例化一个软核处理器如Xilinx MicroBlaze将控制逻辑用C语言编写而FPGA逻辑专注于高速、并行的外设驱动和数据处理。这体现了FPGA作为“可编程SoC”的优势。这个课程设计麻雀虽小五脏俱全。它强迫你从系统角度思考问题将软件算法思维转化为硬件并行思维。调试过程中与波形图打交道、分析时序问题、解决硬件的不确定性这些经验远比最终那个闪烁的数码管显示结果来得宝贵。当你看到自己设计的系统按照预想的流程稳定运行时那种成就感是单纯写软件程序难以比拟的。这或许就是硬件设计的魅力所在。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价