
简介面向FPGA课程设计、毕业设计及数字系统竞赛准备这套基于VHDL语言的出租车计费系统设计资料提供了从源码到硬件验证的完整闭环。系统以单片FPGA为控制核心包含计程、计费、计时、译码动态扫描、按键控制及蜂鸣器提示等典型模块涵盖里程按键模拟、等待计时与金额累计逻辑适合数字逻辑、EDA技术相关课程的进阶实践。压缩包共253个文件约13.32MB主要类型包括Quartus工程配置.qpf/.qsf、VHDL源码.vhd、原理图文件.bdf/.bsf、设计文档.doc/.docx/.pdf以及仿真波形.vwf等可直接打开工程查看模块划分、编译下载并对照文档理解状态机与动态扫描的时序设计。资源已有309人学习配套FPGA板原理图和设计说明辅以文档中的设计流程与模块分析能帮助读者快速搭建实验环境、复现计费逻辑并学习如何用VHDL完成状态控制、分频计数与数码管扫描显示是工程实践性较强的完整参考方案。1. 出租车计费系统的正确打开方式它其实是一道数字电路综合题拿到“基于FPGA设计出租车计费系统”这个标题很多人的第一反应是“这不就是做个加法器吗”。实际上它比按键点灯复杂也比图像处理简单刚好卡在FPGA入门最舒服的位置要处理外部脉冲、要做状态机、要做参数化设计、要面对分频与显示扫描这些真实工程问题。VHDL语言负责把计费规则描述成硬件逻辑Quartus负责完成综合与布局布线FPGA板原理图则决定信号怎么落到真实引脚上。整份资料的价值不在于“能算出钱”而在于它演示了一条从计费规则到硬件时序的完整转换路径。适合正在做EDA课程设计、FPGA毕业设计或者想从语法学习转向完整工程的开发者。规则里有一个容易踩的认知误区计费不是“里程乘单价”这么简单而是事件驱动的累加逻辑。传感器每转一圈产生脉冲脉冲触发里程计数等待时间用秒脉冲累计金额在任意时刻都可能被查询显示。理解了这一点整个系统就是一台由脉冲控制的状态机而不是一堆数学公式。2. 系统架构拆解计费规则、模块划分与VHDL信号约定2.1 计费系统在FPGA上到底要拆成几个模块我一般会按数据流把系统拆成五个部分时钟分频模块、按键消抖模块、计费核心模块、里程脉冲计数模块、动态数码管显示模块。最顶层用VHDL写一个taxi_top.vhd把这些子模块例化进来对外暴露的引脚只有时钟、复位、启动/停止按键、里程脉冲输入、数码管位选和段选。这种拆法遵循一个原则每个模块只解决一类时序问题。分频模块把50MHz系统时钟变成1Hz的秒脉冲计费核心在这个秒脉冲上累加等待时间里程脉冲属于外部异步事件不能直接进计费核心要先通过边沿检测或消抖电路做一次同步显示模块自己维护一个扫描时钟频率在1kHz左右即可因为人的视觉对数码管闪烁不敏感但扫描频率太低会明显闪动。顶层例化的好处是仿真方便。写testbench时只需要对taxi_top这个顶层实体做时序激励不需要单独去拉内部信号。Quartus自带的RTL Viewer还能帮你检查例化关系是否正确如果某个端口没连上一眼就能看出来。2.2 计费规则的VHDL常量定义与参数选择计费规则是整个工程的“需求文档”在VHDL里最先落地的不是逻辑而是一组常量。这个做法对应了VHDL中常量的定义语法常量在声明时必须赋初值仿真过程中不能修改综合时会被优化成固定的逻辑值。package fee_pkg is constant START_FEE : integer : 80; -- 起步价8.0元单位是角 constant MILE_FREE : integer : 6; -- 起步里程3公里按0.5km为一个计费单位 constant MILE_STEP : integer : 12; -- 每超0.5公里加1.2元即12角 constant WAIT_STEP : integer : 24; -- 每等待5分钟加2.4元即24角 constant WAIT_PERIOD : integer : 300; -- 等待计费间隔300秒即5分钟 end fee_pkg;金额统一用“角”做整数单位是为了避开浮点运算。FPGA做浮点加法会消耗大量DSP资源而整数加法只需要几个LUT。显示时再通过取模和除法把角拆成元、角、分。里程单位设定为0.5公里意味着每收到一个里程脉冲就代表车走了0.5公里前6个脉冲落在起步价覆盖范围内从第7个开始才按里程加价。这组常量放到package里而不是写在主文件顶部是因为testbench也要引用。仿真时可以通过修改包内常量的方式把WAIT_PERIOD从300秒压到3秒让等待计费的验证不需要真的等五分钟。价格规则各家不同但用常量把业务和逻辑分离的思路是通用的。2.3 原理图里需要核对的信号与网络标号FPGA板原理图不是用来画着好看的它决定了引脚分配和外部电路连接。拿到原理图先找三个部分电源树、时钟网络、按键与数码管接口。常见做法是根据原理图建立一份“信号-引脚”对照表例如时钟引脚接在E1按键接在J15数码管段选接在AB10到AB16。这份表后面在Quartus的Pin Planner里会逐行填写。看原理图有个高频坑多页原理图复制页面后PAGE NUMBER仍然保持1ORCAD会报 ORCAP-11010 页码重复。这个错误不影响FPGA工程编译但会导致PCB工程师无法确认图纸顺序。拿到别人发的原理图时先检查图纸属性里的页码有没有按1、2、3递增排列省得后面返工。FPGA核心板的原理图通常还包含JTAG下载接口和配置芯片JTAG的TDI、TDO、TCK、TMS四根线必须和FPGA对应引脚逐一核对接反了下载器会识别不到器件。3. VHDL核心代码实现分频器、计费状态机、数码管扫描3.1 用分频使能信号替代分频时钟避免跨时钟域问题分频模块最忌讳的做法是直接把分频后的信号当时钟用比如拿一个1Hz的信号去驱动rising_edge。这会制造多个时钟域布局布线阶段容易产生时序违规仿真时倒是看不出来。正确做法是产生一个宽度为一个系统时钟周期的使能脉冲所有逻辑继续使用原始50MHz时钟。library ieee; use ieee.std_logic_1164.all; entity clk_div_en is generic ( DIV_RATIO : integer : 50_000_000 ); port ( clk : in std_logic; rst_n : in std_logic; clk_en : out std_logic ); end clk_div_en; architecture rtl of clk_div_en is signal cnt : integer range 0 to DIV_RATIO - 1; begin process(clk, rst_n) begin if rst_n 0 then cnt 0; clk_en 0; elsif rising_edge(clk) then clk_en 0; if cnt DIV_RATIO - 1 then clk_en 1; cnt 0; else cnt cnt 1; end if; end if; end process; end rtl;这段代码里clk_en在计数到最大值时拉高一个时钟周期然后立即恢复低电平。后面所有模块的写法都是if rising_edge(clk) then if clk_en 1 then ...相当于在同一个时钟域里通过门控使能信号来降低事件频率。DIV_RATIO是generic参数仿真时改小到5就能快速观察秒级逻辑变化下板时保持50000000对应50MHz晶振的1秒分频。3.2 计费状态机的三段式处理IDLE、RUN、STOP计费核心是典型的状态机应用场景。三个状态分别是待机、运行、停止。待机时金额清零运行时累加里程和等待费用停止时金额冻结不再变化。状态机的输出——当前金额——不应该通过组合逻辑直接修改而是注册在触发器上保证任何时刻输出的都是稳定值。library ieee; use ieee.std_logic_1164.all; use work.fee_pkg.all; entity fee_calc is port ( clk : in std_logic; rst_n : in std_logic; start_pulse : in std_logic; stop_pulse : in std_logic; mile_pulse : in std_logic; clk_1hz_en : in std_logic; fee_out : out integer range 0 to 9999 ); end fee_calc; architecture rtl of fee_calc is type state_t is (IDLE, RUN, STOP); signal state : state_t; signal mile_cnt : integer range 0 to 255; signal wait_cnt : integer range 0 to WAIT_PERIOD; signal fee_ang : integer range 0 to 9999; begin process(clk, rst_n) begin if rst_n 0 then state IDLE; mile_cnt 0; wait_cnt 0; fee_ang 0; elsif rising_edge(clk) then case state is when IDLE mile_cnt 0; wait_cnt 0; fee_ang 0; if start_pulse 1 then fee_ang START_FEE; state RUN; end if; when RUN if stop_pulse 1 then state STOP; end if; if mile_pulse 1 then if mile_cnt MILE_FREE - 1 then fee_ang fee_ang MILE_STEP; end if; mile_cnt mile_cnt 1; end if; if clk_1hz_en 1 then if wait_cnt WAIT_PERIOD - 1 then fee_ang fee_ang WAIT_STEP; wait_cnt 0; else wait_cnt wait_cnt 1; end if; end if; when STOP null; end case; end if; end process; fee_out fee_ang; end rtl;进入RUN时先把起步价写入金额状态机就位。里程脉冲到来时已走过的半公里数mile_cnt小于6说明在3公里起步范围内只计数不加价从第7个脉冲开始每次累加12角。等待计费逻辑独立于里程计数clk_1hz_en每秒拉高一次累计到300秒时加一次等待费用并清零重新开始。等待计费和里程计费互不干扰保证了堵车时金额持续增长而里程不跳变。仿真时需要注意一个细节start_pulse和stop_pulse都是单周期脉冲不是电平信号。如果实际按键给的是持续高电平必须在顶层模块里先做一次按键消抖加边沿检测把按键按下转换成单周期脉冲否则状态机会在多个时钟周期里反复触发金额会出现不可控的跳变。3.3 数码管动态扫描与费用的十进制拆分费用以“角”为单位存储显示时必须拆成十进制位。由于金额范围限制在0到9999角之间直接用整数除法和取模是最简洁的方案。综合工具遇到除常数10时会优化成移位和加法电路不会真正生成除法器资源开销可以接受。signal fee_bcd3 : integer range 0 to 9; signal fee_bcd2 : integer range 0 to 9; signal fee_bcd1 : integer range 0 to 9; signal fee_bcd0 : integer range 0 to 9; fee_bcd3 fee_ang / 1000; fee_bcd2 (fee_ang / 100) mod 10; fee_bcd1 (fee_ang / 10) mod 10; fee_bcd0 fee_ang mod 10;fee_bcd3是金额的千位fee_bcd2是百位fee_bcd1是十位fee_bcd0是个位。出租车金额显示只需要三位数加一位小数所以实际工程中通常只使用fee_bcd2、fee_bcd1和fee_bcd0其中fee_bcd1是元位fee_bcd0是角位。动态扫描的原理是让所有数码管共用一组段选线通过快速轮流点亮每一位来制造“同时亮”的视觉假象。位选切换频率大约1kHz每一路点亮时间在1毫秒左右亮度足够且看不到闪烁。process(clk) variable scan_cnt : integer range 0 to 999; variable pos : integer range 0 to 3; begin if rising_edge(clk) then if scan_cnt 999 then scan_cnt : 0; if pos 3 then pos : 0; else pos : pos 1; end if; else scan_cnt : scan_cnt 1; end if; end if; case pos is when 0 seg_sel 1110; seg_data seg_code(fee_bcd1); when 1 seg_sel 1101; seg_data seg_code(fee_bcd0); when others seg_sel 1011; seg_data seg_code(10); -- 熄灭 end case; end process;这个循环里pos决定当前点亮哪一位数码管seg_sel是位选信号低电平有效。小数点的处理要看板子原理图如果最小显示单位是角那么个位和角位之间需要点亮小数点通常单独留一个IO控制。seg_code是一个case语句构成的段码查表函数输入数字0到9输出共阳数码管的七段编码这部分代码和电路板的段选顺序强相关实际使用时需要比对原理图上段选线连接的顺序来调整。4. Quartus工程配置与板级下载从建工程到写进FPGA4.1 新建Quartus工程的器件选择与文件组织Quartus工程的第一个步骤是选择正确的FPGA型号。标题里写的是Quartus工程常见配套平台是Altera Cyclone IV系列比如EP4CE10F17C8。启动Quartus II 13.1或新版Quartus Prime后通过File菜单选择New Project Wizard工程名和顶层实体名保持一致建议叫taxi_top。器件型号按实际板子选择选错型号会导致引脚分配无法保存编译时报错提示器件不匹配。工程目录内部文件组织推荐按功能分目录而不是所有VHDL文件堆在根目录taxi/ ├── src/ │ ├── clk_div_en.vhd │ ├── debounce.vhd │ ├── fee_calc.vhd │ ├── seg_display.vhd │ └── taxi_top.vhd ├── sim/ │ └── tb_taxi_top.vhd └── quartus/ └── taxi.qpf把仿真文件和源码文件分开后续使用ModelSim做行为仿真时Quartus的Compile Order列表会清晰很多。台面工作流一般是先写VHDL文件在Quartus中逐个添加到工程然后执行全编译。编译完成后看Analysis Synthesis报告里的逻辑单元使用量如果使用率超过90%就需要检查是不是有冗余逻辑或者超出目标器件的容量。4.2 Pin Planner引脚分配与程序下载引脚分配是FPGA工程从虚拟设计走向物理硬件的桥梁。Quartus中Assignments菜单选择Pin Planner根据原理图的网络标号填入引脚位置。关键引脚包括时钟、复位、里程脉冲、数码管位选和段选。时钟引脚必须使用板上的专用时钟输入比如Cyclone IV的CLK0引脚普通IO不能接晶振信号。信号名称方向引脚位置示例说明sys_clk输入E1板载50MHz晶振rst_n输入F1低电平复位start_btn输入J15按键启动计费stop_btn输入J16按键停止计费mile_pulse输入M1里程传感器脉冲seg_sel[3:0]输出AB10 AB11 AB12 AB13数码管位选seg_data[6:0]输出AB14 AB15 AB16数码管段选引脚分配完毕后重新编译Quartus会生成.sof文件。下载时Tools菜单打开Programmer硬件设置为USB-BlasterMode选择JTAG勾选Write File后点击Start。如果Programmer打开文件时明显卡顿通常是USB-Blaster驱动异常或者JTAG链路中某个器件处于繁忙状态断开重插一次即可。4.3 把代码固化到配置芯片工程完成前的最后一步JTAG下载的.sof文件断电即失课程设计答辩时经常出现“拔电源再插回去程序没了”的情况。解决办法是生成.pof文件并烧写到板载EPCS配置芯片。Quartus中Assignments菜单选择Device进入Device and Pin Options勾选Configuration页面中的Active Serial配置模式。重新编译后Programmer窗口里的文件类型从.sof切换成.pofMode选择Active Serial Programming勾选Program/Configure执行烧写。生成pof之前需要确认电路板上确实有EPCS配置芯片的焊盘。有些精简版FPGA开发板只保留了JTAG接口没有焊EPCS器件这时候.pof烧写会失败。判断方法很简单看原理图里FPGA配置引脚附近有没有一个型号以EPCS开头的八脚Flash芯片没有就只能用JTAG下载每次上电重新加载。5. 仿真验证与高频踩坑ModelSim启动失败后先看波形5.1 编写testbench验证计费规则的正确性仿真不是编译没报错就结束了计费规则的验证必须写testbench驱动输入信号并观察输出是否随时间变化符合预期。testbench属于仿真专用代码不需要综合因此可以用wait for这类不可综合的语句来生成时钟波形。library ieee; use ieee.std_logic_1164.all; entity tb_taxi_top is end tb_taxi_top; architecture sim of tb_taxi_top is signal clk : std_logic : 0; signal rst_n : std_logic : 0; signal start_pulse : std_logic : 0; signal stop_pulse : std_logic : 0; signal mile_pulse : std_logic : 0; signal clk_1hz_en : std_logic : 1; signal fee_out : integer range 0 to 9999; constant CLK_PERIOD : time : 20 ns; begin clk not clk after CLK_PERIOD / 2; uut : entity work.fee_calc port map ( clk clk, rst_n rst_n, start_pulse start_pulse, stop_pulse stop_pulse, mile_pulse mile_pulse, clk_1hz_en clk_1hz_en, fee_out fee_out ); stimulus : process begin rst_n 0; wait for 100 ns; rst_n 1; start_pulse 1; wait for 20 ns; start_pulse 0; wait for 100 ns; assert fee_out 80 report 起步价错误 severity error; for i in 1 to 6 loop mile_pulse 1; wait for 20 ns; mile_pulse 0; wait for 80 ns; end loop; assert fee_out 80 report 起步里程内不应加价 severity error; mile_pulse 1; wait for 20 ns; mile_pulse 0; wait for 100 ns; assert fee_out 92 report 超出起步里程后第一次加价错误 severity error; stop_pulse 1; wait for 20 ns; stop_pulse 0; wait; end process; end sim;测试流程分成四步复位后启动计费验证起步价80角连发六个里程脉冲验证金额不变化第七个脉冲验证金额跳到92角停止计费验证状态切换。testbench里使用了assert语句断言失败会打印错误信息仿真不需要盯着波形肉眼判断对错。这个激励文件直接挂在计费核心模块上覆盖了核心状态机的关键路径。5.2 Quartus II cannot launch ModelSim的常见原因Quartus和ModelSim的联调是EDA工具链里最容易出问题的一环。启动仿真时报错 Cannot launch ModelSim七成原因是仿真工具的调用路径没有配置对。在Quartus的Tools菜单打开Options进入EDA Tool Options把ModelSim路径指向实际安装目录下的win32aloem文件夹。注意新版Quartus Prime只能调用特定版本的ModelSim版本相差过大时会报版本不匹配错误。另一个容易忽略的问题是VHDL文件的编译顺序。Quartus工程中的Compile Order逻辑决定了ModelSim自动编译的顺序fee_pkg里定义了常量包其他文件都依赖它如果这个包没有被排在最前面ModelSim会在编译时提示找不到包名fee_pkg。手动调整Compile Order把包文件拖到第一位重新编译后再启动仿真。遇到仿真库损坏时直接在工程目录下删除simulation文件夹重建比反复修改设置更省时间。5.3 仿真的波形检查技巧与初始化阶段处理ModelSim波形窗口里看到的异常通常集中在仿真初始阶段。复位释放后的第一个时钟沿所有信号应该进入确定状态。如果波形图上出现紫色高阻线说明信号没有初始化需要检查复位逻辑是否覆盖了所有分支。VHDL里integer类型默认初始值为左边界即最小值比如integer range 0 to 9999初始化为0一般没问题但std_logic类型初始值是U必须通过复位赋值。观察等待计费逻辑时直接跑完整300秒太慢通用做法是把fee_pkg里的WAIT_PERIOD临时改成3验证等待计费每次加24角的逻辑成立后再改回来。写testbench时用force命令直接修改内部信号也可以但会绕过状态机控制可能会把系统置入不存在的非法状态不如改常量参数来得干净。6. 从能跑变成好验收板级验证技巧与防抖参数化改造6.1 按键消抖与边沿检测的代码实现计费系统的启动和停止都是按键触发机械按键按下瞬间会产生10到20毫秒的抖动如果直接接到状态机的start_pulse端口一次按键可能触发多次启动脉冲状态机会在IDLE和RUN之间反复横跳。所有外部按键信号进FPGA后必须先做消抖和边沿检测这是板级验证正常工作的前提。entity debounce is generic ( DELAY_CNT : integer : 500_000 ); port ( clk : in std_logic; rst_n : in std_logic; key_in : in std_logic; key_out : out std_logic ); end debounce; architecture rtl of debounce is signal cnt : integer range 0 to DELAY_CNT - 1; signal reg : std_logic; begin process(clk, rst_n) begin if rst_n 0 then cnt 0; reg 1; elsif rising_edge(clk) then if key_in reg then cnt 0; else if cnt DELAY_CNT - 1 then reg key_in; cnt 0; else cnt cnt 1; end if; end if; end if; end process; key_out reg; end rtl;DELAY_CNT在50MHz时钟下取500000对应10毫秒的消抖窗口。按键电平稳定10毫秒后才更新输出抖动期间的毛刺全部被滤除。这个模块输出的是稳定电平后面还需要接一个上升沿检测电路用一拍寄存器延迟产生start_pulse单周期脉冲。卡顿的地方在于消抖延时和用户体验的权衡延时太短滤不干净延时太长按键感觉迟钝10毫秒是常用值。6.2 通过generic参数化解耦计费规则与仿真速度计费规则中的起步价、里程单价、等待时间都是工程常量但测试时想快速看到结果又不想改完再改回来。把常量从package升级为顶层模块的generic是更灵活的做法。顶层实体声明类似generic(START_FEE : integer : 80; WAIT_PERIOD : integer : 300)仿真时通过generic map指定为3下板验证时用默认值300。参数化设计让同一份源码在仿真和板级验证之间无缝切换不需要维护两套代码。验收时检查三个关键点出租车计费金额从启动到停止的变化曲线是否符合预设规则按下停止键后金额保持不变等待计费的时间间隔符合配置值。用SignalTap嵌入式逻辑分析仪抓状态机的状态位可以看到每次按键的完整迁移路径。上电后不按键时数码管应该显示0启动后立即显示起步价里程脉冲输入后逐次加价停止后所有输入失效。6.3 用VHDL设计文档反向检查代码完整性设计文档说明通常是课程设计资料里最容易被忽视的部分但它恰好是答辩提问的高频区。一份合格的FPGA计费系统设计文档要包含状态转换图、引脚绑定表、模块例化关系、仿真截图和板级验证照片。建议在完成板级验证后对照文档里的功能列表逐条打勾把仿真波形图截下来标出关键时间点答辩时这几张图比文字描述有说服力得多。如果文档里的引脚绑定表和实际Quartus工程不一致以工程为准修正文档否则答辩现场会被老师抓到明显破绽。本文还有配套的精品资源点击获取