☰
FPGA数字系统设计实战:从状态机到地铁售票模拟系统
2026/9/28 7:25:49 网站建设 项目流程

简介:本资源是一套完整的基于FPGA的地铁售票模拟系统课程设计实现,面向计算机、电子信息、自动化、通信工程等专业的本科生及初学者,解决数字电路与嵌入式系统课程中综合实践能力训练问题,适用于课设开发、作业提交、答辩演示及Verilog逻辑设计进阶学习。压缩包共18个文件,涵盖核心Verilog源码(.v)、Vivado工程配置(.xpr/.xdc/.lpr)、仿真测试脚本(.py)、需求与设计说明文档(.pdf/.md)、票价配置表(.xlsx)及开源协议(LICENSE),总大小994KB,结构清晰、模块划分明确,便于理解状态机设计、按键消抖、数码管动态扫描与票务逻辑控制等关键知识点。已有139人学习下载,项目已通过导师评审并获95分高分,所有代码经Windows/macOS/Linux多平台实测运行稳定,配套资料齐全,可直接用于教学实践或在此基础上拓展多站换乘、余额显示等新功能。

1. 项目概述:从“地铁售票”到“FPGA系统设计”的跨越

最近在整理资料时,翻出了一个几年前带学生做的课程设计项目——“基于FPGA的地铁售票模拟系统”。这个项目听起来挺有意思,对吧?它不像一个纯粹的算法研究,也不像单纯的软件应用,而是把数字逻辑、状态机、人机交互和具体的应用场景(地铁售票)结合在了一起。对于电子工程、微电子或者计算机硬件方向的同学来说,这是一个绝佳的练手项目,能让你把书本上的Verilog语言、状态机设计、外设驱动等知识,串成一个完整的、能“动”起来的系统。

这个项目的核心,就是利用一块FPGA开发板(比如常用的Nexys4 DDR),模拟一个简化版的地铁售票机。用户可以通过板载的按键、拨码开关选择目的地和票数,通过数码管或VGA显示器看到票价和找零信息,最终通过“确认”按键完成一次虚拟的购票流程。麻雀虽小,五脏俱全。它涉及了输入消抖、状态机控制、票价计算逻辑、显示驱动等多个模块,是对数字系统设计能力的一次综合性考察。我之所以觉得这个项目值得拿出来细说,是因为它完美地诠释了如何将一个现实世界的需求(售票),用硬件描述语言(Verilog)在可编程逻辑器件(FPGA)上实现出来。这个过程里踩过的坑、总结的技巧,远比最后那个能跑通的“源码”更有价值。

2. 系统整体架构与设计思路拆解

2.1 需求分析与功能定义

在设计任何系统之前,明确需求是第一步。对于一个课程设计级别的“地铁售票模拟系统”,我们需要定义清楚它的边界和功能。这个系统不需要连接真实的纸币识别器或硬币找零机,我们的目标是模拟核心业务流程和逻辑。

基于此,我通常会将系统功能分解为以下几个核心部分:

  1. 线路与票价管理:模拟一个简单的地铁网络,例如包含4-6个站点。票价规则可以设定为固定票价,或者更复杂的按里程计费。这部分是系统的“数据核心”。
  2. 用户交互输入:用户如何选择目的地、票数?通常使用开发板上的拨码开关(Switches)来代表不同的站点编码,用另外的开关或按键(Buttons)来增加/减少票数。
  3. 交易逻辑处理:这是系统的大脑。它需要根据用户选择的站点和票数,计算出总票价。然后,模拟用户“投币”(通过按键模拟投入不同面额),实时计算已投金额、待付金额和应找零金额。
  4. 信息显示输出:将票价、已投金额、找零等信息实时显示给用户。可以用七段数码管(7-Segment Display)显示数字,或者用VGA接口连接显示器显示更丰富的图形界面(对于课程设计,数码管更常见,VGA可作为加分项)。
  5. 系统控制与状态流转:整个购票过程不是一个静态操作,而是一系列状态的切换。例如:IDLE(空闲等待选择)、CALCULATING(计算票价)、WAITING_FOR_COIN(等待投币)、PAYMENT_CONFIRM(支付确认)、DISPENSING_TICKET(出票/完成)。这需要一个清晰的状态机(Finite State Machine, FSM)来控制。

2.2 硬件平台选型:为什么是Nexys4 DDR?

在众多FPGA开发板中,Nexys4 DDR(以及其后续版本Nexys A7)是高校课程设计和入门FPGA开发的常客。选择它作为本项目平台,有以下几个非常实际的理由:

  1. 资源适中且明确:它搭载的Xilinx Artix-7 FPGA(XC7A100T)拥有约10万个逻辑单元,对于本设计来说绰绰有余。资源明确便于进行面积和时序评估。
  2. 外设丰富且典型:板载了16个拨码开关、5个按键(其中4个可作方向键)、16个LED、8个七段数码管。这些正好对应了我们需要的输入(选择站点、票数、投币)、输出(状态指示、金额显示)设备。无需额外连接模块,降低了硬件复杂度。
  3. 调试与编程便利:通过USB接口即可完成供电、编程和调试,集成度高。其配套的Vivado设计套件也是行业标准,学生能接触到工业级的开发流程。
  4. 社区与资料丰富:由于使用广泛,遇到问题时更容易在网上找到解决方案或参考设计,降低了学习门槛。

注意:虽然项目资料包可能直接提供了针对Nexys4 DDR的约束文件(.xdc),但理解每个外设(如按键、数码管)对应的FPGA引脚号及其电气特性(如上拉、下拉)至关重要。这是硬件设计区别于纯软件的第一课。

2.3 顶层模块划分与接口定义

根据功能需求,我们可以将整个系统划分为几个相对独立的模块,然后在顶层模块(Top Module)中将它们实例化并连接起来。这是一种典型的自顶向下(Top-Down)设计方法。

一个合理的模块划分可能如下:

  • top_system.v:顶层模块。定义所有对外的引脚(连接至按键、开关、数码管等),并实例化所有子模块。
  • debounce.v:按键消抖模块。机械按键在按下和弹起时会产生物理抖动,导致单次按压被误识别为多次。这个模块通过采样和延时滤波来消除抖动,产生干净的按键信号。
  • station_selector.v:站点选择器。读取拨码开关的状态,将其解码为对应的站点ID。
  • ticket_counter.v:票数计数器。通常用两个按键控制票数的增减,并设置上下限(如1-9张)。
  • fare_calculator.v:票价计算器。根据输入的站点ID和票数,查询预设的票价表,计算出总费用。这里可以集成简单的票价规则,比如固定票价,或者基于查找表(LUT)的里程票价。
  • coin_simulator.v:投币模拟器。用另外的按键模拟投入不同面额的硬币(如1元、5元)。它需要累加已投金额,并与总费用比较。
  • fsm_controller.v:有限状态机控制器。这是系统的核心控制单元,定义所有状态(IDLE, CAL, WAIT, PAY, DONE等),并根据输入(如选择完成信号、投币完成信号、确认键)控制状态转移,并输出控制其他模块的使能信号。
  • display_driver.v:显示驱动器。将需要显示的数值(如票价、已投金额、找零)转换为七段数码管或VGA显示器所需的段选和位选信号。对于数码管,通常还需要一个扫描模块来动态刷新。

各模块之间通过清晰的信号接口进行通信。例如,状态机控制器会输出一个calc_enable信号给票价计算器,当用户完成选择后,这个信号拉高,触发一次计算。计算完成后,票价计算器会拉高calc_done信号,并输出total_fare数据,状态机检测到calc_done后,便进入等待投币状态。

3. 核心模块的Verilog实现与难点解析

3.1 有限状态机(FSM)的设计与编码

状态机是本系统的“灵魂”,它决定了整个流程是否顺畅、逻辑是否严谨。一个健壮的状态机设计需要仔细定义状态、输入条件和输出行为。

状态定义: 我通常建议使用独热码(One-Hot Encoding)来定义状态,尽管它消耗的触发器(Flip-Flop)更多,但译码逻辑简单,在FPGA上利用丰富的查找表资源时,往往在速度和可靠性上更有优势。对于本项目,状态可以定义为:

localparam S_IDLE = 6'b000001; // 空闲,等待选择 localparam S_CAL_FARE = 6'b000010; // 计算票价 localparam S_WAIT_COIN = 6'b000100; // 等待投币 localparam S_PAY_CONFIRM = 6'b001000; // 支付确认(等待用户按确认键) localparam S_DISPENSE = 6'b010000; // 出票/交易完成 localparam S_ERROR = 6'b100000; // 错误状态(如投币不足时确认) reg [5:0] current_state, next_state;

状态转移逻辑: 这是状态机设计的核心,需要用always @(*)组合逻辑块来描述。每个现态(current_state)结合不同的输入信号,决定次态(next_state)。

always @(*) begin next_state = S_IDLE; // 默认状态,防止锁存器生成 case (current_state) S_IDLE: begin if (selection_done) next_state = S_CAL_FARE; else next_state = S_IDLE; end S_CAL_FARE: begin if (calc_done) next_state = S_WAIT_COIN; else next_state = S_CAL_FARE; end S_WAIT_COIN: begin if (coin_total >= total_fare) next_state = S_PAY_CONFIRM; else if (cancel_pressed) next_state = S_IDLE; else next_state = S_WAIT_COIN; end S_PAY_CONFIRM: begin if (confirm_pressed) next_state = S_DISPENSE; else if (cancel_pressed) next_state = S_IDLE; // 取消,退回等待投币状态?这里涉及找零逻辑,需要细化 else next_state = S_PAY_CONFIRM; end S_DISPENSE: begin // 出票完成后,延时一段时间自动回到空闲状态 if (dispense_timer_done) next_state = S_IDLE; else next_state = S_DISPENSE; end default: next_state = S_IDLE; endcase end

状态输出逻辑: 输出逻辑可以根据状态直接赋值,也可以采用Mealy型或Moore型。为了清晰,我常用Moore型(输出仅与当前状态有关)来产生控制信号。

always @(*) begin // 默认输出 calc_enable = 1'b0; display_mode = 2'b00; led_indicator = 6'b000000; case (current_state) S_IDLE: begin display_mode = 2'b00; led_indicator = 6'b000001; end // 显示欢迎信息或提示选择 S_CAL_FARE: begin calc_enable = 1'b1; display_mode = 2'b01; led_indicator = 6'b000010; end // 显示计算中 S_WAIT_COIN: begin display_mode = 2'b10; led_indicator = 6'b000100; end // 显示票价和已投金额 // ... 其他状态输出 endcase end

实操心得:状态机设计最容易出错的地方是状态覆盖不全和产生锁存器。务必为next_state设置默认值,并且在case语句中处理好所有可能的输入组合。使用default分支是一个好习惯。另外,将状态转移逻辑(组合)和状态寄存器更新逻辑(时序)分开写在两个always块中,是教科书式的标准做法,清晰且不易出错。

3.2 按键消抖与边缘检测

FPGA开发板上的按键是机械触点,其抖动时间通常在5-20ms。如果不处理,一次按压会被误认为是多次快速按压。

消抖原理:核心思想是延时采样。当检测到按键电平变化后,并不立即认为按键有效,而是等待一段时间(比如20ms),待抖动过去后再次采样,如果电平稳定为新状态,则判定为一次有效的按键动作。

一种简单可靠的Verilog实现方式是使用一个计数器:

module debounce ( input wire clk, // 系统时钟(如100MHz) input wire button_in, // 原始的按键输入 output reg button_out // 消抖后的稳定输出 ); parameter DEBOUNCE_TIME = 20_000_000; // 20ms @ 100MHz 时钟 reg [31:0] counter; reg button_sync; always @(posedge clk) begin button_sync <= button_in; // 同步化,避免亚稳态 if (button_sync != button_out) begin // 检测到输入与当前输出不同,开始计数 counter <= counter + 1; if (counter >= DEBOUNCE_TIME) begin // 计时达到消抖时间,更新输出 button_out <= button_sync; counter <= 0; end end else begin // 输入输出相同,重置计数器 counter <= 0; end end endmodule

边缘检测:消抖后得到的是稳定的电平信号。但我们往往需要的是“按下”或“释放”这个事件,也就是检测信号的上升沿或下降沿。这可以通过寄存一拍再比较来实现:

reg button_db_reg; wire button_pressed; // 按下事件(下降沿,假设低电平有效) always @(posedge clk) begin button_db_reg <= button_db; // button_db是消抖后的信号 end assign button_pressed = (~button_db) & button_db_reg; // 检测下降沿

button_db_reg保存了上一个时钟周期的按键状态,button_db是当前状态。当上一周期为高(未按下),当前周期为低(按下)时,button_pressed产生一个时钟周期的高脉冲,代表一次有效的按下事件。

注意事项:消抖模块的DEBOUNCE_TIME参数需要根据实际时钟频率计算。例如,100MHz时钟下,20ms对应2,000,000个时钟周期。如果参数设置过大,会导致按键响应迟钝;过小则可能无法滤除抖动。另外,对于多个按键,可以实例化多个消抖模块,或者设计一个参数化的、支持多通道的消抖模块。

3.3 票价计算与找零逻辑的实现

票价计算模块fare_calculator相对直接。它内部可以维护一个简单的票价表,例如用一个查找表(LUT)或case语句实现。

module fare_calculator ( input wire [2:0] station_id, // 假设有8个站点 input wire [3:0] ticket_num, // 票数,1-9 input wire calc_enable, output reg [11:0] total_fare, // 总票价,最大999? output reg calc_done ); reg [7:0] fare_per_ticket; // 单张票价 always @(*) begin case (station_id) 3'd0: fare_per_ticket = 8'd2; // 站点0票价2元 3'd1: fare_per_ticket = 8'd3; // ... 其他站点 default: fare_per_ticket = 8'd0; endcase end always @(posedge clk) begin if (!rst_n) begin total_fare <= 12'd0; calc_done <= 1'b0; end else if (calc_enable) begin // 使能有效时,计算总价。这里可以加入更复杂的规则,如分段计价 total_fare <= fare_per_ticket * ticket_num; // 注意位宽,防止溢出 calc_done <= 1'b1; // 计算完成信号拉高 end else begin calc_done <= 1'b0; // 使能无效时,完成信号拉低 end end endmodule

找零逻辑通常集成在投币模拟器或状态机中。核心是:change = coin_total - total_fare。但需要注意几个细节:

  1. 找零计算时机:应在用户按下“确认”键(S_PAY_CONFIRM->S_DISPENSE)时计算并锁存找零金额,因为在出票/完成状态,系统需要显示找零信息。
  2. 负数处理:如果coin_total < total_fare,change为负数。这代表投币不足。系统应禁止进入完成状态,并提示用户继续投币或取消交易。可以在状态机S_WAIT_COIN中判断,只有coin_total >= total_fare时才允许进入确认状态。
  3. 金额表示:为了便于显示,所有金额(票价、已投、找零)建议用整数表示,单位为“分”。例如,3.5元表示为350。这样可以避免使用浮点数,简化硬件逻辑。

3.4 数码管动态扫描显示驱动

Nexys4 DDR有8个四位七段数码管,但FPGA的引脚有限,不可能为每个数码管的每一段都单独分配引脚。因此采用了动态扫描技术:所有数码管的相同段(a, b, c, d, e, f, g, dp)是并联的,通过位选信号(AN0-AN7)来决定当前点亮哪一个数码管。通过快速轮流点亮各个数码管,利用人眼的视觉暂留效应,看起来就像是所有数码管同时显示。

驱动模块需要完成两件事:段码译码和动态扫描。

module seg_display ( input wire clk, input wire [31:0] bcd_data, // 要显示的8位BCD码数据,每4位代表一个数字 output reg [7:0] seg, // 段选信号(a-g, dp) output reg [7:0] an // 位选信号(AN0-AN7,低电平有效) ); reg [2:0] scan_cnt; // 扫描计数器 reg [3:0] digit; // 当前要显示的数字(0-9) // 分频产生扫描时钟(约1kHz) reg [15:0] div_cnt; wire scan_clk; always @(posedge clk) begin div_cnt <= div_cnt + 1; end assign scan_clk = div_cnt[15]; // 取高位作为扫描时钟 // 动态扫描 always @(posedge clk) begin if (scan_clk) begin scan_cnt <= scan_cnt + 1; case (scan_cnt) 3'd0: begin an <= 8'b11111110; digit <= bcd_data[3:0]; end // 显示最低位 3'd1: begin an <= 8'b11111101; digit <= bcd_data[7:4]; end // ... 依次类推 3'd7: begin an <= 8'b01111111; digit <= bcd_data[31:28]; end // 显示最高位 default: begin an <= 8'b11111111; digit <= 4'h0; end endcase end end // 段码译码(共阳极数码管,段信号低电平点亮) always @(*) begin case (digit) 4'h0: seg = 8'b11000000; // 0 4'h1: seg = 8'b11111001; // 1 // ... 2-9 4'ha: seg = 8'b10001000; // A (显示“A”表示错误或其他状态) default: seg = 8'b11111111; // 全灭 endcase end endmodule

踩坑记录:动态扫描最常见的两个问题是闪烁和重影。

  1. 闪烁:扫描频率太低。如果扫描周期大于人眼视觉暂留时间(约1/24秒),就会感觉到闪烁。通常扫描频率需要在60Hz以上(即每个数码管点亮周期小于16ms)。上面的例子用div_cnt[15]在100MHz下产生约762Hz的扫描时钟,远高于临界值。
  2. 重影:位选信号切换时,段码数据没有同步变化。在点亮下一个数码管之前,段码数据必须先更新为对应数码管要显示的内容。如果更新慢了,上一个数码管的段码会短暂地显示在下一个数码管上,造成重影。确保段码数据digit的赋值与位选信号an的赋值在同一个时钟沿下完成,可以避免此问题。

4. 系统集成、仿真与上板调试

4.1 顶层模块集成与引脚约束

当所有子模块都编写并单独测试通过后,需要在顶层模块top_system.v中将它们像搭积木一样连接起来。顶层模块主要做三件事:

  1. 声明输入输出端口:这些端口名称需要与约束文件(.xdc)中的引脚名称对应。
  2. 实例化子模块:将各个功能模块例化,并用线网(wire)或寄存器(reg)按设计好的接口连接起来。
  3. 生成全局时钟和复位:可能包含一个时钟分频模块,为不同部分提供所需频率的时钟。复位信号通常由板载的复位按键产生。

引脚约束(.xdc文件)是连接FPGA内部逻辑与外部物理引脚的关键。对于Nexys4 DDR,Digilent提供了参考约束文件。我们需要根据自己顶层模块的端口名进行修改。例如:

# 系统时钟 (100MHz) set_property PACKAGE_PIN E3 [get_ports clk] set_property IOSTANDARD LVCMOS33 [get_ports clk] create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports clk] # 复位按键(中间按钮,按下为低) set_property PACKAGE_PIN C12 [get_ports rst_n] set_property IOSTANDARD LVCMOS33 [get_ports rst_n] # 站点选择拨码开关 SW0-SW2 set_property PACKAGE_PIN J15 [get_ports {station_sel[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {station_sel[0]}] # ... 类似地约束 SW1, SW2 # 确认按键 BTNU set_property PACKAGE_PIN T18 [get_ports btn_confirm] set_property IOSTANDARD LVCMOS33 [get_ports btn_confirm] # ... 约束其他按键(增加票数、减少票数、模拟投币等) # 七段数码管位选 AN0-AN7 set_property PACKAGE_PIN U13 [get_ports {an[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {an[0]}] # ... 约束 AN1-AN7 # 七段数码管段选 CA-CG, DP set_property PACKAGE_PIN L18 [get_ports {seg[0]}] # CA set_property IOSTANDARD LVCMOS33 [get_ports {seg[0]}] # ... 约束其他段

务必仔细核对每个端口的引脚编号和电平标准(LVCMOS33)。一个错误的约束会导致整个系统无法工作。

4.2 功能仿真与Testbench编写

在将设计下载到板子之前,必须进行充分的仿真(Simulation)。仿真可以验证逻辑的正确性,比上板调试效率高得多。我们需要为顶层模块或关键子模块编写测试平台(Testbench)。

一个简单的Testbench结构如下:

`timescale 1ns / 1ps module tb_top_system(); // 1. 定义仿真时钟和复位 reg clk; reg rst_n; parameter CLK_PERIOD = 10; // 100MHz 时钟周期10ns always #(CLK_PERIOD/2) clk = ~clk; initial begin clk = 0; rst_n = 0; // 初始复位 #100; // 复位保持100ns rst_n = 1; // 释放复位 // ... 后续测试激励 end // 2. 实例化待测设计(DUT) top_system uut ( .clk(clk), .rst_n(rst_n), // ... 连接其他端口 ); // 3. 生成测试激励 initial begin // 等待复位结束 @(posedge rst_n); #100; // 测试用例1:选择站点1,购买2张票 station_sel = 3'b001; // 选择站点1 #20; btn_ticket_inc = 1; // 按一下增加票数 #(CLK_PERIOD*10); btn_ticket_inc = 0; #20; btn_ticket_inc = 1; // 再按一下,票数变为2 #(CLK_PERIOD*10); btn_ticket_inc = 0; #50; btn_selection_done = 1; // 确认选择 #(CLK_PERIOD*10); btn_selection_done = 0; // 观察状态机是否进入S_CAL_FARE,然后S_WAIT_COIN // 观察total_fare计算是否正确(假设站点1票价3元,总价应为6元) // 测试用例2:模拟投币 // 投入两个5元(模拟按键) btn_coin_5 = 1; #(CLK_PERIOD*10); btn_coin_5 = 0; #100; btn_coin_5 = 1; #(CLK_PERIOD*10); btn_coin_5 = 0; // 观察coin_total是否变为10,是否满足>=total_fare(6),状态机是否进入S_PAY_CONFIRM // 测试用例3:确认支付 #100; btn_confirm = 1; #(CLK_PERIOD*10); btn_confirm = 0; // 观察状态机是否进入S_DISPENSE,找零change是否计算正确(10-6=4) // 观察数码管显示是否正确 // 运行一段时间后结束仿真 #5000; $finish; end // 4. 可选:将信号变化记录到VCD文件,便于用波形查看器分析 initial begin $dumpfile("wave.vcd"); $dumpvars(0, tb_top_system); end endmodule

在Vivado中运行仿真后,可以在波形窗口查看所有信号的变化,验证状态跳转、数据计算、输出显示是否符合预期。

4.3 上板调试与问题排查实录

仿真通过后,就可以生成比特流文件并下载到FPGA开发板了。但上板后问题往往才真正开始出现。以下是我在带学生调试此类项目时遇到的几个典型问题及解决方法:

问题1:按键无反应,或反应异常(连按)

  • 可能原因A:未添加消抖模块。这是最常见的原因。直接读取按键电平会导致多次触发。
  • 排查:检查顶层设计是否实例化了debounce模块,并且所有按键输入都经过了消抖处理。
  • 可能原因B:按键电平理解错误。Nexys4 DDR的按键是低电平有效(按下为0,松开为1)。在代码中判断按键是否按下,应该是if (btn_pressed == 1'b0)或者用下降沿检测。
  • 排查:查阅板卡手册,确认按键电路是上拉还是下拉。在代码中打印(通过LED)按键消抖后的电平,观察按下和松开时的变化。

问题2:数码管显示乱码或不亮

  • 可能原因A:段码或位选信号极性弄反。数码管有共阳极和共阴极之分,段码表和位选有效电平需要匹配。
  • 排查:查阅Nexys4 DDR原理图。其数码管是共阳极的。这意味着:
    • 位选(AN):低电平有效(0点亮该位数码管)。
    • 段选(CA-CG, DP):低电平有效(0点亮该段)。
  • 可能原因B:扫描频率不对。频率太低会闪烁,频率太高可能因驱动能力导致亮度不均。
  • 排查:调整扫描时钟的分频系数。用示波器或逻辑分析仪测量an[0]信号的频率,确保在60Hz-1kHz范围内。
  • 可能原因C:约束文件错误。段选或位选信号连接到了错误的FPGA引脚。
  • 排查:在Vivado的“I/O Planning”视图或生成的引脚分配报告中,逐一核对seg和an信号是否分配到了正确的引脚号。

问题3:状态机“卡死”在某个状态

  • 可能原因A:状态转移条件不满足。例如,在S_WAIT_COIN状态等待coin_total >= total_fare,但投币模拟信号没有正确更新coin_total。
  • 排查:添加一些“调试输出”。例如,将当前状态current_state用二进制形式输出到LED灯上。通过观察LED的亮灭模式,可以判断系统停留在哪个状态。然后重点检查进入下一个状态的条件信号是否产生。
  • 可能原因B:异步复位或时钟域问题。如果状态机使用了异步复位,要确保复位信号干净,没有毛刺。如果系统中存在多个时钟,要小心处理跨时钟域的信号。
  • 排查:对于课程设计,建议全部使用同一个主时钟clk,并使用同步复位。检查复位信号是否在约束文件中正确分配给了板载的复位按键。

问题4:计算结果显示错误

  • 可能原因A:数据位宽不足导致溢出。例如,票价计算fare_per_ticket * ticket_num,如果fare_per_ticket是8位(最大255),ticket_num是4位(最大15),乘积可能超过8位。如果直接用8位寄存器接收,就会溢出。
  • 排查:检查所有算术运算(+, -, *)前后信号的位宽。使用$display在仿真中打印中间结果,或者将关键数据输出到LED上观察。
  • 可能原因B:BCD码转换错误。如果显示模块需要BCD码,而计算模块输出的是二进制,则需要一个二进制转BCD的模块。转换逻辑错误会导致显示数字不对。
  • 排查:单独测试二进制转BCD模块。输入一个已知的二进制数(如8'd123),看输出的BCD码(8'h01, 8'h02, 8'h03)是否正确。

调试技巧:LED是你的好朋友。在调试初期,不要急于让所有功能(尤其是显示)都完美。可以把关键内部信号(如current_state,total_fare[3:0],coin_total[3:0])直接赋值给LED灯。通过观察LED的二进制显示,可以快速定位问题大致在哪个模块。这是一种非常有效的“硬件调试打印”方法。

5. 项目扩展与进阶思考

完成基础功能后,这个项目还有很大的扩展空间,可以进一步提升复杂度和实用性:

  1. 增加VGA图形界面:用VGA接口在显示器上绘制更友好的界面,如地铁线路图、站点选择按钮、动画效果等。这需要学习VGA时序控制和图形绘制原理,会涉及帧缓冲(Framebuffer)和字符/图形生成模块。
  2. 实现更复杂的票价体系:从固定票价改为按里程计费。这需要系统存储一个“票价矩阵”,或者根据起点站和终点站的编号计算出一个“距离”,再根据距离查表得到票价。这会对状态机的设计(如需要选择“起点”和“终点”)和计算模块提出更高要求。
  3. 加入声音提示:利用板载的音频编解码器或PWM驱动一个蜂鸣器,在关键节点(如选择完成、投币不足、交易成功)发出不同音调的声音提示。
  4. 使用软核处理器:如果FPGA资源足够(如Nexys4 DDR的Artix-7),可以嵌入一个Xilinx MicroBlaze软核处理器。将复杂的业务逻辑(如票价计算、状态控制)用C语言在处理器上实现,而将时序要求严格的模块(如显示扫描、按键消抖)仍用Verilog实现为外设。这样可以将项目升级为一个简单的“软硬协同”系统,更贴近实际应用。
  5. 添加数据持久化:模拟“日结”功能。使用板载的EEPROM或FPGA内部的Block RAM,记录每天的交易次数、总金额等数据,并通过串口上传到电脑。这涉及到存储器的读写和通信协议(如I2C、UART)的实现。

这个“基于FPGA的地铁售票模拟系统”项目,从概念到实现,贯穿了数字系统设计的全流程:需求分析、模块划分、Verilog编码、功能仿真、约束设计、上板调试。它成功地将多个分散的知识点(组合逻辑、时序逻辑、状态机、外设驱动)融合在一个有趣的应用场景中。对于学习者而言,最终那个能正确运行的.bit文件固然令人欣喜,但更宝贵的财富是在调试过程中培养出的系统思维和解决问题的能力。当你看到数码管随着你的按键操作而跳动,最终显示出正确的找零信息时,那种对硬件系统的掌控感,是纯软件编程无法替代的。希望这份详细的拆解和记录,能为你实现自己的FPGA项目提供一份扎实的参考。

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

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

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

立即咨询