ARTICLE DETAIL

资讯详情

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

基于FPGA的出租车计费器:VHDL状态机与按键消抖实战

基于FPGA的出租车计费器:VHDL状态机与按键消抖实战 简介基于FPGA的出租车计费系统毕业设计资料包面向电子、通信及计算机相关专业的学生和FPGA初学者是一套从HDL代码到原理图、说明文档的完整工程方案。资源共253个文件压缩包约12.75MB核心包括tdf、vhd等FPGA逻辑设计源码bdf、bsf等原理图文件以及doc、pdf、readme等说明文档此外还有qsf、sof等Quartus工程配置与下载文件便于直接打开和参考。已有105人学习适合正在准备毕业设计或想系统掌握数字系统设计流程的读者。资料覆盖出租车计费算法的HDL实现、GPS与传感器接口设计、时序控制、仿真验证等关键环节并附有仿真波形与报告文件可帮助理解FPGA并行处理、硬件描述语言以及从综合到配置的完整开发链路对提升工程实践能力很有价值。1. 为什么用FPGA做出租车计费一个被低估的并行时序样本出租车计费这种活儿听起来用单片机加个霍尔传感器就能搞定但实际上它比表面看起来更适合放到FPGA上做。原因在于计费系统并不是一个简单累加器它要同时处理里程脉冲计数、等待时间计时、白天夜间费率切换、数码管动态刷新、按键消抖等多个任务而且这些任务之间还共享同一组计费寄存器。用MCU写你要操心中断优先级、定时器分时复用用FPGA写每个模块天然独立运行时序关系在综合前就能确定下来。这套毕业设计资源最值钱的地方不在“出租车计费”本身而在于它把计数器、按键消抖、动态扫描显示、蜂鸣器驱动这几块最典型的数字电路知识点捏成了一个有业务含义的完整系统。适合刚学完VHDL想找一个综合项目练手的人也适合准备答辩想快速复现一套可演示硬件的同学。资源包里能看到time_counter.vhd、seg.vhd、key.vhd、control.vhd、bell.vhd这些源码备份整个设计的模块划分一目了然。2. 计费模型与硬件量化车费是怎么映射成寄存器的2.1 计费规则的三种基本状态出租车计费在逻辑上可以拆成三件事起步价判断、里程费累加、等待时间费累加。起步价是一个固定金额打车后先进入“起步价区间”这个区间内无论跑多远都只收起步价超出起步里程后每增加一个计费单位比如每公里累加一笔里程费当车速低于某个阈值或者停车时进入等待计费状态按时间累加。这三件事在FPGA里全部落到“某个条件满足时对某个寄存器做加法”这个基本操作上。起步价区间用里程脉冲计数器判断当里程计数小于起步里程对应的脉冲数时显示固定金额大于后每累计一个脉冲就加一次里程单价。等待时间费则需要一个基准时钟常见做法是用一个10ms或1s的时钟信号作为时间片在“低速/停车”标志有效时对这个时间片计数攒够一个计费单位就加一次等待费。由于FPGA的加法操作是并行发生的里程脉冲计数和等待时间计时可以各自跑各自的计数器互不阻塞这是FPGA方案相比MCU方案在结构上的天然优势。2.2 里程脉冲和时间基准两个输入信号的约定系统正常运行前必须先定清楚两个输入信号的含义。第一个是里程脉冲实际项目中它来自霍尔传感器或光电编码器轮子每转一圈产生一个脉冲每个脉冲对应固定的距离。设计资源里没有车辆硬件常见做法是用一个按键或信号发生器模拟这个脉冲每按一次代表行驶了一个固定距离。代码里time_counter.vhd这个文件就是干这个事的。第二个是基准时钟FPGA开发板上的晶振通常是50MHz需要分频成适合计费用的低频时钟比如10ms或1s。这两个信号的频率决定了计费精度。假设每个里程脉冲代表0.01公里10米那1000个脉冲就是10公里。如果想让显示金额以0.1元为单位跳动起步价3公里内10元、超出部分每公里2.5元那每0.1公里也就是10个脉冲加一次0.25元。这个换算关系需要写进说明文档的表格里代码里最好把“每多少个脉冲加多少钱”设计成可修改的常量方便根据题目要求调整。2.3 计费参数表起步价、单价、等待时间的寄存器映射典型的计费参数可以这样归类参数含义硬件表示示例值START_FEE起步价固定金额寄存器8.0元START_DIST起步里程起始里程脉冲阈值300个脉冲3公里MILE_FEE每公里单价每N个脉冲加一次的费用2.5元/公里WAIT_FEE等待费每M个时间片加一次的费用0.5元/分钟WAIT_THRESHOLD进入等待计费的延迟低速持续时长阈值5分钟在VHDL里这些参数不建议直接写成magic number散落在代码中。比较好维护的做法是在control.vhd的顶层用一个包package定义所有常量或者把费率配置成拨码开关输入——但作为毕业设计用常量定义已经足够。需要注意金额在FPGA里是整数运算把“元”换算成“角”或者“分”来算避免浮点。比如起步价8元存成80单位角里程单价2.5元存成25角/次最后显示时把结果除以10得到元模10得到角。2.4 可运行的里程计数模块示例下面是基于工程里time_counter.vhd的设计思路重新组织的一段可综合VHDL代码实现了里程脉冲累加和起步里程阈值判断library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity mile_counter is port ( clk_10ms : in std_logic; -- 10ms基准时钟 rst_n : in std_logic; -- 异步复位低有效 pulse_in : in std_logic; -- 里程脉冲输入 start_flg : out std_logic; -- 起步里程标志1已超出起步里程 mile_cnt : out std_logic_vector(19 downto 0) -- 里程脉冲计数 ); end mile_counter; architecture rtl of mile_counter is signal cnt_r : std_logic_vector(19 downto 0) : (others 0); signal pulse_ff : std_logic_vector(1 downto 0) : (others 0); constant START_DIST : std_logic_vector(19 downto 0) : 00000000010010110000; -- 300个脉冲 begin process (clk_10ms, rst_n) begin if rst_n 0 then cnt_r (others 0); pulse_ff (others 0); elsif rising_edge(clk_10ms) then pulse_ff pulse_ff(0) pulse_in; -- 上升沿检测前一拍为0、当前拍为1时产生单周期脉冲 if (pulse_ff(0) 0 and pulse_in 1) then if cnt_r 11111111111111111111 then cnt_r cnt_r 1; end if; end if; end if; end process; start_flg 1 when cnt_r START_DIST else 0; mile_cnt cnt_r; end rtl;代码里pulse_ff这个两级寄存器是关键它完成对pulse_in的跨时钟域同步并形成边沿检测避免外部按键或传感器信号直接进入计数逻辑时产生亚稳态。cnt_r在达到最大值后饱和而不是回卷防止计费金额异常跳变。START_DIST以二进制常量形式给出格式是“00000000010010110000”对应十进制的300。实际工程中如果想调整起步里程只改这一个常量就够了不需要动任何逻辑结构。注意这里的clk_10ms是已经分频后的低频时钟如果直接用50MHz时钟跑这个模块counter的位宽和功耗都会不划算。3. VHDL模块划分读透源码包里的文件结构3.1 五个核心.vhd文件各管哪一块解开资源包之后第一眼看到的是大量带.bak后缀的文件。.bak是Quartus自动生成的源码备份工程真正编译时用的是对应无后缀的.vhd文件比如time_counter.vhd和time_counter.vhd.bak内容一致只存在一个文件时说明作者习惯直接以.bak作为留存副本。从文件名基本可以推断出整个工程的模块拓扑结构time_counter.vhd负责里程脉冲计数和时间基准分频seg.vhd负责数码管段码译码key.vhd负责按键消抖和按键事件提取control.vhd是顶层控制模块bell.vhd负责蜂鸣器驱动。这种划分方式在FPGA入门项目里非常典型。它是按“外设控制”来切分的显示、输入、发声各自独立顶层控制模块负责把它们粘起来。这样设计的直接好处是每一个模块都可以单独仿真。比如seg.vhd是组合逻辑译码输入BCD码、输出七段码这种模块用testbench验证特别快bell.vhd则是把某个计费事件比如起步价切换、结算完成映射为一段持续时间的方波输出控制蜂鸣器鸣叫。3.2 那些.cmp、.map.atm文件是什么和源码有什么关系工程里还有一类文件非常显眼带.atm后缀、.cmp后缀和.sldhu后缀的文件。这些不是源码而是Quartus编译过程中生成的中间产物。.cmp是Compilation Output文件保存了编译警告和错误信息的汇总。.map.atm是Quartus II Map阶段逻辑综合后的适配阶段自动生成的临时文件内容主要是资源利用率和映射结果。.sldhu是SignalTap嵌入式逻辑分析仪相关文件说明工程里可能配置了SignalTap在线调试。看到time.autos这个文件可以确认工程使用的是Altera系的Quartus工具链因为autos文件是Quartus自动生成的增量编译信息。对读者来说理解这些文件的含义比删除它们更重要。很多人在网上下载到FPGA工程后直接用Quartus打开根目录却发现找不到.qpf工程文件而抓狂。这类毕业设计资源包的实际用法是新建一个空工程把.vhd文件添加进去然后重新编译那些中间文件是原作者的编译环境信息不一定能直接在你的电脑上还原。原理图部分同理如果是Quartus原理图输入方式生成的.bdf文件需要对应版本的Quartus才能打开。3.3 模块之间的信号连接与时钟域约定control.vhd作为顶层模块负责例化所有子模块并连接信号。一个合理的信号连接关系是这样的外部时钟经分频后得到clk_10ms基准同时送给time_counter用作时间片、送给key模块用作消抖基准、送给control模块用作状态机节拍里程脉冲pulse_in经key模块同步边沿检测后进入time_countertime_counter输出里程计数和起步标志control模块根据起步标志和等待标志计算金额BCD码seg模块把BCD码转成七段码驱动数码管结算事件触发bell模块输出短音。整条链路是单向的从输入到输出中间没有跨时钟域问题全部运行在同一个分频时钟域里这在设计上是最省事的方案。常见误区是一上来就想在顶层做复杂的流水线交互。实际上这个项目的控制流非常简单核心是“事件驱动累加”。不需要握手机制不涉及异步FIFO只要保证所有寄存器都使用同一个时钟沿更新时序分析就非常好过。控制模块里如果写了复杂的状态机反而要注意每个状态之间的转移条件是否覆盖了边界情况。3.4 费用计算的流水线思路先算后显示金额显示部分值得单独说一下。数码管显示需要的是十进制BCD码而计费累加是在二进制或二进制编码的十进制BCD空间里进行的。如果直接把二进制里程计数值送去译码seg模块只能显示十六进制字符观感很差。推荐的做法是在control模块内部用两个独立的BCD加法器一个累加“起步费”一个累加“里程费”每次触发累加时做一次BCD加法超过9自动进位。这是教科书里标准的BCD加法器结构用VHDL实现只需要在二进制加法之后加一个大于9则加6调整的判断。宏模块部分工程里如果用到分频器通常直接用Quartus的PLL IP核生成低抖动时钟也可以自己写异步分频。IP核的好处是Quartus会自动做时钟约束优化和布线规划手工分频虽然代码少但综合后时钟到各个寄存器的偏斜skew要靠时序分析器帮你检查初学者容易忽略这一点。4. 按键消抖与计费状态机把业务规则写进时序逻辑4.1 从业务需求推导FSM状态转移出租车计费器必须解释清楚“开始计费、等待计费、结算清零”这几个状态怎么切。一个具备工程意义的状态机至少包含四个状态IDLE待机、RUN正常行驶计费、WAIT等待计费、STOP结算暂停。复位后进入IDLE按下“开始键”进入RUN此时起步费生效里程脉冲开始累加车速低于阈值超过设定时间进入WAIT里程停止累加但等待时间开始累加再按一次“暂停键”进入STOP所有计数暂停但数据保留最后按“结算键”输出总费用并回到IDLE。这个状态机跟交通灯控制系统的状态机结构非常相似都是典型的摩尔状态机输出只取决于当前状态。4.2 按键消抖的VHDL写法与时间参数选择机械按键在按下和释放的瞬间会产生约5~20ms的电平抖动一个不被处理的上升沿会让计数器一次跳好几个数。消抖有两个层次用硬件RC滤波器或者用逻辑延时采样。FPGA里资源充足推荐用延时重采样法。下面这段代码是一个标准消抖模块采样周期10ms连续两次采样电平一致才认为按键状态稳定library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity key_debounce is port ( clk_10ms : in std_logic; -- 10ms采样时钟 rst_n : in std_logic; key_in : in std_logic; -- 原始按键电平按下为0 key_pulse : out std_logic -- 单周期脉冲按下瞬间产生 ); end key_debounce; architecture rtl of key_debounce is signal key_sync : std_logic_vector(1 downto 0) : 11; signal stable : std_logic : 1; begin process (clk_10ms, rst_n) begin if rst_n 0 then key_sync 11; stable 1; elsif rising_edge(clk_10ms) then key_sync key_sync(0) key_in; -- 连续两次采样都为低电平认为进入稳定按下状态 if key_sync(1) 0 and key_sync(0) 0 then stable 0; elsif key_sync(1) 1 and key_sync(0) 1 then stable 1; end if; end if; end process; -- 检测稳定信号由高到低产生单周期按键脉冲 key_pulse 1 when (stable 1 and key_sync(1) 0 and key_sync(0) 0) else 0; end rtl;代码里key_sync是两级同步寄存器先消除亚稳态再做消抖判定。稳定电平stable只有在连续10ms采样为同一个值时才翻转这能把抖动段的毛刺全部滤掉。key_pulse的输出逻辑实际上是在stable为高、同步后的按键为低的那一拍产生脉冲也就是按下动作发生的时刻。消抖时间参数10ms是经验值如果实际按键抖动更严重可以适当把采样时钟周期加大到20ms代价是按键响应变慢这在出租车计费场景里完全可接受。4.3 白天/夜间模式切换的实现计费系统通常要求白天和夜间执行不同单价夜间上浮比例常见在20%到50%之间。硬件上一般设计一个额外拨码开关或按键控制mode信号control模块根据mode选择不同的里程费常量和等待费常量。因为常量本身就是VHDL的常数定义模式切换只是在顶层选择一个“常数使能”比如夜间模式把里程单价从25角改成30角即可。所有涉及的BCD加法器精度都保留到角切换模式时直接把当前BCD累加寄存器切换为另一组初始值会存在既有计费不连续的问题。所以比较严谨的做法是模式切换只影响切换之后的累加不会追溯之前已经跑过的费用写说明文档时把这个约束写清楚属于合理的人为约定。5. Quartus综合流程与仿真验证从源码到能跑起来的硬件5.1 拿到源码后怎么在Quartus里重建工程第一步不是打开文件而是新建工程File - New Project Wizard指定工程名称、目标芯片型号。Altera系的FPGA用Quartus II开发Xilinx系用Vivado这套资源的文件特征指向Quartus。工程建立后把time_counter.vhd、seg.vhd、key.vhd、control.vhd、bell.vhd全部添加进工程设置control.vhd为顶层实体然后直接启动Analysis Synthesis。如果原工程是多个版本混合的代码首次编译会报一些端口不匹配的警告这通常是顶层例化时端口名不一致导致打开Messages窗口逐条核对即可。5.2 写testbench验证里程脉冲计数与费用累加仿真验证是整个项目里信噪比最高的部分因为代码是不是真的逻辑正确跑一遍Modelsim或Quartus自带的Simulation Waveform就知道。以time_counter模块为例写一个最简testbenchlibrary ieee; use ieee.std_logic_1164.all; entity tb_mile_counter is end tb_mile_counter; architecture sim of tb_mile_counter is signal clk_10ms : std_logic : 0; signal rst_n : std_logic : 0; signal pulse_in : std_logic : 0; signal start_flg : std_logic; signal mile_cnt : std_logic_vector(19 downto 0); constant CLK_PERIOD : time : 10 ms; -- 10ms时钟周期 begin uut : entity work.mile_counter port map ( clk_10ms clk_10ms, rst_n rst_n, pulse_in pulse_in, start_flg start_flg, mile_cnt mile_cnt ); clk_10ms not clk_10ms after CLK_PERIOD / 2; stim_proc : process begin rst_n 0; wait for 30 ms; rst_n 1; wait for 20 ms; -- 模拟10个里程脉冲每个间隔30ms for i in 1 to 10 loop pulse_in 1; wait for 1 ms; pulse_in 0; wait for 29 ms; end loop; wait for 100 ms; std.env.stop; end process; end sim;testbench的核心思路还是那句话手动把输入信号按业务规则排出来然后观察输出是否符合预期。这段仿真里pulse_in每次拉高1ms、拉低29ms模拟一个周期性脉冲每30ms一个脉冲。运行仿真的预期结果是mile_cnt依次递增到10start_flg保持为0因为300脉冲的起步阈值远未达到。如果把循环次数改成320次就能验证start_flg从0变1的时刻。仿真输出波形里还要检查一个容易被忽略的点mile_cnt是否在clk_10ms上升沿之后稳定更新中间有没有毛刺。理论上边沿检测加同步寄存器以后不该有毛刺这也是衡量代码风格是否干净的一个直观标准。5.3 引脚约束与上板常见的坑仿真通过之后最花时间的环节是引脚分配。Quartus里通过Assignments - Pin Planner完成需要对照开发板原理图确定按键接到哪个引脚、数码管是共阳还是共阴、段码位选分别对应bank几的哪个引脚。常见的三个坑第一数码管共阳共用阴的判断错误导致段码全部取反显示乱码第二按键按下是低电平有效而FPGA引脚内部默认上拉能力有限导致按键电平不稳定第三蜂鸣器输出引脚直接驱动有源蜂鸣器时电流不足需要通过三极管或MOS管驱动。这些在资源包自带的原理图里都能查到指导意义比看文字大得多。5.4 用SignalTap观察内部寄存器上板之后最实用的调试工具是SignalTap嵌入式逻辑分析仪。把clk_10ms设为采样时钟把mile_cnt、cnt_r、state状态机当前状态设为观察信号在SignalTap里设置触发条件为state等于RUN然后运行设备。启动后手动模拟几个里程脉冲波形窗口里能看到mile_cnt逐次加1state在几个状态间正确迁移。SignalTap文件本身就是工程里.sldhu后缀文件的来源配置SignalTap之后Quartus会重新编译并生成新的.sof配置文件把新的配置文件下载进FPGASignalTap窗口才能正常抓取信号。6. 进阶技巧把毕业设计模板改造成可扩展的计费平台6.1 把单价定义从常量改成运行时配置寄存器原始的VHDL代码里起步价、里程单价、等待单价全部是constant这就意味着每次调价都要重新综合、重新下载比特流。稍微进阶一点的做法是把这些参数改成寄存器由顶层模块通过拨码开关或UART写入初始化值。拨码开关方案最简单用8位拨码做单价设定BCD格式直接输入control模块在每个计费周期读取一次。这样做的好处是现场演示时不用重新编译就可以调整费率你可以在答辩现场当着评委的面按题目要求改单价演示效果会好很多。6.2 动态扫描数码管的刷新时序安排数码管多位显示几乎必用动态扫描每一位轮流点亮1~2ms利用人眼视觉暂留形成连续显示。刷新频率太低会闪烁太高会带来位选切换时的拖影。4位数码管用1ms扫描周期也就是1kHz刷新率效果最好。扫描时序建议放在time_counter模块里用一个独立计数器产生扫描周期与计费时钟同步但分频比不同二者在同一时钟域内互不干扰。位选切换时先关闭段选再切换位选最后打开段选可以避免“拖尾”现象。这段逻辑放在seg.vhd里实现它本质上是一个小状态机加组合译码跟计费主状态机并行运行正好体现了FPGA并行性的优势。6.3 里程计数器溢出与累加精度问题里程计数和费用累加的位宽设计属于容易被忽视的问题。假设每个脉冲代表10米跑1000公里需要的脉冲数是100000个也就是17位二进制数。如果time_counter用的是16位计数器溢出上限是65535大约655公里就会回卷归零费用必然出错。所以计算位宽的公式是计数器最大计数值乘以每脉冲代表里程必须大于设计里程上限。20位计数器对应最大1048575个脉冲按10米一个脉冲折算超过一万公里足够覆盖出租车全寿命里程这也是示例代码用20位的原因。同理费用累加用BCD后位宽由“十进制位数”决定显示4位金额最大999.9元就用16位BCD加法器每4位一组共4组。给这个模块写注释时把位宽计算公式写进去后续换传感器参数时会省很多事。6.4 里程脉冲异常时的硬件保护实际车辆上里程传感器可能接触不良或断线在FPGA里表现为长时间没有脉冲且没有按下暂停键。一个实用技巧是用time_counter里的基准时钟额外统计“无脉冲持续时长”若超过30秒就自动把系统切到等待计费状态并在蜂鸣器上鸣叫两声提示司机检查传感器。这里的“30秒”参数同样做成常量挂在control模块顶部。这个设计并不复杂但能体现你对系统鲁棒性的理解在毕业设计答辩里属于一个免费加分的可靠性细节。本文还有配套的精品资源点击获取
返回列表