ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FPGA出租车计价器设计:Verilog状态机与数码管动态扫描

FPGA出租车计价器设计:Verilog状态机与数码管动态扫描 简介基于Verilog的出租车计价系统是一套完整的数字逻辑设计实践工程参照上海市出租车计价规则实现起步价、公里费与低速等候费的自动计算。工程覆盖Verilog模块化设计、仿真验证与FPGA实现流程适合正在学习硬件描述语言及数字系统设计、FPGA开发的读者参考。压缩包共416个文件约10.95MB核心包含10个Verilog源程序文件、78个文本设计文件同时提供项目工程文件、仿真波形文件、编译报告以及FPGA配置文件等可完整支撑从源码编写、功能仿真到综合配置的每个环节。目前已有655人浏览学习。通过该工程读者可直接在Quartus环境中打开项目研读里程计数、计时器、费用计算等子模块的编写思路理解不同行驶状态下的计费切换逻辑并通过仿真波形观察验证结果工程内另外保留了多个备份文件便于对照调试和二次开发。这是一个兼具教学演示与设计参考价值的实用资源。 出租车计价系统几乎是每个玩Verilog的人都会碰到的“标配级”练手项目。很多教程喜欢把它包装成一个简单的计数器加显示模块实际上你把需求掰开揉碎之后会发现起步价怎么判定、等待时间怎么计费、里程脉冲怎么和费用累加关联、数码管动态扫描怎么才能不闪不串位每一处都比表面上复杂。这篇博文我打算从一个实际做过的FPGA课设出发把计价系统的完整设计思路、关键代码、仿真验证和上板调试踩过的坑全部过一遍。不管是正在写课设、准备电子竞赛还是第一次接触状态机和分频器的读者都可以把这篇当成一份可复用的工程笔记。1. 计价规则拆解先把“怎么收钱”变成可测的逻辑1.1 起步价、里程单价、等待费三项计费参数的界定做任何系统第一件事不是写代码而是把业务规则说清楚。出租车计价看起来简单但如果没有明确的参数代码根本没法落地。我这里用的是一套典型规则你可以根据题目要求自行修改起步价3公里内收费10元起步里程内含8分钟的免费等待时间。里程费超过起步里程后每公里收费2元。等待费行驶途中停车等待超过8分钟后按每2分钟1元计费。在这套规则里“起步价”不是简单的一次性加价而是一个分段判断逻辑在起步里程内不管怎么跑费用都保持10元一旦超过3公里超出的部分开始按里程累加。等待费类似它有一个免费额度免费时间内不跳字超过后才开始计入费用。这些细节看起来不起眼但恰恰是课程设计里最容易丢分的点。很多人的计价器只是“按脉冲数乘单价”完全没有考虑起步价包含的免费等待时间导致仿真波形和实际出租车行为对不上。所以第一步我建议你把规则画成一张表格把每个参数的取值范围、单位、触发条件写清楚再去定状态机和计数器。1.2 起步价截止线为什么状态机比“连续计数”更能表达真实业务有了规则下一个问题是计价逻辑到底怎么组织我见过有人用一个全局计数器从头数到尾在某个计数值到达时切换费用算法。这种做法在简单场景下能跑但一旦加入“停车等待”和“继续行驶”的切换代码会变得非常混乱因为你要用一堆if-else去判断当前到底处于什么业务阶段。更贴近真实业务的做法是用状态机把计价过程分成几个明确的阶段空闲空车待客、行驶载客运行、等待中途停车。状态机的核心价值在于它把“系统当前处于什么业务场景”这个问题显式化了后续的费用计算、里程计数、等待计时都只需要关心当前状态是什么再决定要不要加钱逻辑会清晰很多。实际设计时状态机可以用一段式、两段式或三段式。我习惯用两段式——第一段时序逻辑做状态跳转第二段组合逻辑做下一状态判断这样代码容易维护仿真排查问题也更直观。localparam IDLE 2d0; localparam RUN 2d1; localparam WAIT 2d2; reg [1:0] current_state; reg [1:0] next_state; always (posedge clk or negedge rst_n) begin if (!rst_n) current_state IDLE; else current_state next_state; end always (*) begin next_state current_state; case (current_state) IDLE: begin if (start_signal) next_state RUN; end RUN: begin if (wait_signal) next_state WAIT; else if (stop_signal) next_state IDLE; end WAIT: begin if (!wait_signal) next_state RUN; else if (stop_signal) next_state IDLE; end default: next_state IDLE; endcase end这套状态机里wait_signal来自车辆怠速检测信号stop_signal来自“结束计费”按键。状态切换时同步清理对应的计时标志后续计费逻辑就会非常顺。1.3 一条规则表把计价策略统一成参数化配置状态机解决了“处在什么阶段”的问题但费用计算本身还需要一套参数化配置。我用parameter把计价规则集中定义在模块顶部这样换一套计价规则不需要到处改代码。参数名含义默认值START_FARE起步价10元START_DIST_PULSE起步里程对应的脉冲数3000个脉冲FARE_PER_KM超起步里程后每公里单价2元/公里FREE_WAIT_TIME起步价内含等待秒数8秒WAIT_FARE_PER_MIN等待计费周期120秒/元这里的里程单位我建议直接用“脉冲数”来表示不要在FPGA内部做浮点除法。因为真实的出租车计价器通过轮速传感器产生脉冲一个脉冲对应固定的距离脉冲计数在Verilog里是天然适配的。你只需要在写需求时换算好“多少个脉冲等于一公里”然后把价格表里的公里数全部换成脉冲数。这套参数表还有一个好处——写报告和答辩的时候可以直接把它当作系统设计方案的一部分老师一眼就能看出你做了需求分析而不是拿到题目就闷头写代码。2. 模块划分与状态机落地从脉冲到费用的完整链路2.1 顶层结构分频、计数、计价、显示四个模块的分工很多初学者喜欢把全部逻辑写在一个always块里仿真跑通就完事但上板之后问题非常多。模块化设计不只是为了好看它能让每个独立的时钟域和逻辑边界变得可控。我的做法是把系统拆成四个模块时钟分频模块把板载50MHz时钟分频成1秒脉冲作为等待计时的基准。里程计数模块采集轮速脉冲做消抖和计数。计价控制模块状态机加费用累加是整个系统的核心。显示扫描模块把费用和里程的二进制数转换成BCD码再动态扫描到数码管。顶层模块只做例化把各个模块的端口连起来。这样做的好处是某个模块出问题时你能直接定位到波形文件里对应的那一层信号不用在几百行的代码里翻来找去。2.2 里程脉冲捕获与计数器的实现细节里程脉冲是计价系统里最关键的输入信号。在实际装置中它来自霍尔传感器或编码器在课设里我们通常用一个按键来模拟脉冲。但按键和真实脉冲有个重要区别——按键有机械抖动直接接进计数器会导致一次按下被计数多次。所以里程计数模块首先要解决“输入信号的有效性”问题。通常我在采集脉冲前加一个两级同步器把异步输入同步到系统时钟域再做边沿检测和消抖处理。同步器的本质是打两拍第一拍消除亚稳态第二拍得到稳定值。reg pulse_d1; reg pulse_d2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pulse_d1 1b0; pulse_d2 1b0; end else begin pulse_d1 pulse_in; pulse_d2 pulse_d1; end end wire pulse_pos pulse_d1 !pulse_d2; // 上升沿有效有了这个边沿信号里程计数器就可以安全地累加了。这里还有一个容易忽略的点计数器到达起步里程脉冲数之后超出的部分才开始计里程费所以需要把总脉冲数和起步脉冲数做一次减法而不是把总脉冲数直接乘以单价。2.3 费用累加逻辑一个always块里处理多通道加价费用累加是计价模块的核心。我的做法是用一个大位宽寄存器保存当前总费用每次秒脉冲或里程脉冲到来时根据当前状态决定是否需要自增。一个比较稳妥的设计思路是在同一个always块里用当前状态作为判断条件分别处理“里程计费”和“等待计费”两个通道。这两个通道互斥因为同一时刻要么在行驶要么在等待不会同时加钱。always (posedge clk or negedge rst_n) begin if (!rst_n) total_fare 0; else begin case (current_state) RUN: begin if (dist_pulse_pos dist_count START_DIST_PULSE) total_fare total_fare FARE_PER_KM; end WAIT: begin if (wait_time FREE_WAIT_TIME wait_pulse_pos) total_fare total_fare WAIT_FARE_STEP; end default: ; // IDLE不增加费用 endcase end end这里注意一个细节FARE_PER_KM和WAIT_FARE_STEP的数值不一定是1。如果你用BCD码保存费用可以让每次累加的单位是“角”或“分”显示的时候再做小数点定位。这样计价精度更高也不会因为浮点运算产生误差。3. 动态扫描显示与仿真验证板子上的每一分钱都要看得清3.1 时钟分频从板载时钟得到1秒基准的计算过程FPGA开发板通常自带50MHz或100MHz的有源晶振这个频率太快直接用来做计时没有任何意义。我们需要先分频得到1秒的时基。分频的本质是数脉冲。50MHz时钟意味着每秒有50,000,000个上升沿设计一个计数器从0数到49,999,999每数满一个周期就翻转一次分频输出就能得到一个周期为1秒的方波信号。parameter CLK_FREQ 50_000_000; parameter CNT_1S CLK_FREQ - 1; reg [31:0] cnt; reg clk_1s; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 0; clk_1s 1b0; end else if (cnt CNT_1S) begin cnt 0; clk_1s ~clk_1s; end else begin cnt cnt 1b1; end end很多人在写这个分频器时会忽略计数器位宽。50MHz的计数最大值接近5000万二进制表示需要26位如果只声明成16位计数器会溢出分频结果完全不对。所以这里我直接用reg [31:0]宁可多给几个位也不在这上面翻车。如果你不希望得到50%占空比的方波而是一次性的1秒脉冲也可以把clk_1s改成“计数到最大值时拉高一个周期”。对于等待计费来说这种单脉冲时基更好用可以直接作为wait_time的计数使能信号。3.2 数码管动态扫描的刷新率与毛刺回避费用和里程数据最终要显示在数码管上。如果每个数码管单独占一组I/O那开发板引脚不够用所以几乎都是动态扫描方案——一位一位地轮流点亮靠视觉暂留让所有位看起来同时亮。动态扫描的关键是刷新率。人眼舒适不闪烁的刷新率一般在50Hz以上如果你用4位数码管每位显示时间取4ms整个扫描周期16ms刷新率约60Hz实际效果稳定。刷新率太低会看到明显的闪烁太高又会造成某些位亮度不均匀。动态扫描还有一个隐藏的毛刺问题如果你在扫描位选切换的同时更新段码数据数码管可能短暂串位显示上一位的数字肉眼看起来就是数字“飘”了一下。解决办法是让段码更新和位选切换错开一个时钟周期或者对段码输出做一级寄存器锁存确保位选切换时段码数据已经稳定。always (posedge clk) begin seg_data_reg seg_data; // 段码寄存器锁定数据 seg_sel seg_sel_next; // 位选切换 end这个方法成本极低但能显著提升显示效果。上板测试时你会发现数字切换干净利落不再有那种“拖影”感。3.3 用Icarus Verilog做波形验证仿真方案怎么写写FPGA项目最忌讳的就是直接综合下载仿真验证这一步无论如何不能省。Icarus Verilog搭配GTKWave是一个非常轻量的仿真方案——免费、无需完整IDE、脚本化运行特别适合课程设计场景。仿真时需要写一个testbench我一般这样设计激励生成周期20ns的半周期周期时钟模拟50MHz时钟。给出异步复位信号拉低后再释放。模拟start_signal按键启动计价。连续产生若干次pulse_in脉冲模拟车辆行驶。再拉高wait_signal一段时间模拟中途停车。initial begin clk 1b0; forever #10 clk ~clk; end initial begin rst_n 1b0; #100 rst_n 1b1; #100 start_signal 1b1; #20 start_signal 1b0; repeat (50) begin #40 pulse_in 1b1; #40 pulse_in 1b0; end // 添加等待信号 #100 wait_signal 1b1; #2000 wait_signal 1b0; #100 $finish; end运行完仿真后把波形文件导入GTKWave重点观察current_state、dist_count、total_fare、wait_time这几个信号。如果total_fare在起步里程内没有变化超过起步里程后按预期增加等待超过免费时间后才开始计费那基本就说明核心逻辑正确可以上板了。4. 上板调试高频问题与自检清单4.1 “仿真过了上板乱跑”的根源排查这是所有FPGA初学者都会碰到的问题testbench里波形完美下载到开发板之后数码管乱跳、计费不稳定。根源通常出在输入信号处理上。仿真里的按键是理想的零抖动电平变化真实按键是机械触点抖动一次按下会产生几十毫秒的连续电平跳变直接进状态机就等于按了好几次。解决办法是在顶层对start_signal、stop_signal、wait_signal这些外部输入统一做“同步加消抖”。先打两拍同步到系统时钟域再用计数器消抖确认按键电平稳定超过10ms才认为是有效按键。消抖计数器本身很简单但它是上板稳定的关键。4.2 按键消抖、异步同步与复位设计我设计了一个通用按键消抖模块思路是检测到按键电平变化后启动一个20ms左右的计数器计数结束后重新采样电平如果和前一次采样一致就输出一个单脉冲。reg [19:0] debounce_cnt; reg key_sync1, key_sync2; reg key_stable; always (posedge clk) begin key_sync1 key_in; key_sync2 key_sync1; end always (posedge clk) begin if (key_sync2 ! key_stable) begin debounce_cnt 0; key_stable key_sync2; end end复位设计同样不能马虎。建议使用异步复位、同步释放的方式避免复位信号释放时产生亚稳态。也就是rst_n直接进always块的敏感列表但在内部先用两级触发器同步复位信号再释放。4.3 位宽、溢出与边界情况的自检清单上板前我习惯对照这份清单自检一遍很多问题能在综合前就被发现费用寄存器的位宽是否足够如果计价上限是999.9元用二进制需要至少14位用BCD码需要16位。位宽不够的直接后果是费用跳变清零。分频计数器的位宽是否满足最大值50MHz分频至少要26位建议直接声明32位。起步里程脉冲数和计数器位宽是否匹配如果计数器只有8位而起步里程是3000个脉冲计数会直接溢出。等待时间的计时基准是否正确是用分频后的1秒信号使能还是直接数时钟周期两者代码差别很大。状态机的default分支写了吗如果状态变量受到干扰进入未定义状态系统会卡死。显示模块里费用、里程的BCD转换结果和位选是否同步不同步会导致显示串位。这些项目里最隐蔽的是位宽问题。我自己第一次调试时费用计数到一定值就回零检查了半天才发现是一个位宽不够的中间信号在累加过程中发生了溢出。如果你也遇到类似症状第一反应应该是检查所有计数器的位宽而不是去怀疑显示模块。最后分享一个我后来一直沿用的习惯把所有通用模块比如按键消抖、时钟分频、数码管扫描都整理成独立文件存好。计价系统做完之后这些模块在后面的串口通信、信号采集项目里都能直接复用。调试这种设计类项目最花时间的从来不是写代码而是定位那些“差一点就对了”的边界问题。把一个系统完整调通之后你对状态机的理解会比看十遍教程都深刻。本文还有配套的精品资源点击获取
返回列表