基于FPGA与Verilog的智能计价系统设计:从模块化到Basys3硬件实现

张开发
2026/5/18 21:34:20 15 分钟阅读
基于FPGA与Verilog的智能计价系统设计:从模块化到Basys3硬件实现
1. 从零开始理解FPGA智能计价系统第一次接触FPGA设计时我被Verilog代码和硬件描述语言搞得一头雾水。直到用Basys3开发板完成这个出租车计价器项目才真正理解模块化设计的精妙。这个系统就像搭积木每个功能模块各司其职最后组合成完整作品。为什么选择出租车计价器作为入门项目因为它包含了FPGA开发的典型要素时钟分频、状态控制、数值计算、外设驱动。通过这个案例你能掌握从仿真到硬件部署的全流程。我当年做这个项目时最头疼的是数码管显示抖动问题后来发现是扫描频率设置不当导致的这个坑后面会详细讲。Basys3开发板是理想的实验平台它自带100MHz主时钟4位共阳极数码管16个拨码开关5个按钮足够的IO接口计价器的核心逻辑其实很简单行驶时按里程计费等待时按时间计费。但要用硬件实现就需要拆解成七个关键模块就像组装汽车要先造发动机、变速箱等部件。2. 模块化设计像拼乐高一样写Verilog2.1 时钟分频模块系统的心跳所有数字系统都需要时钟信号就像人的心跳。Basys3的100MHz时钟太快我们需要分频得到1Hz信号。这是我的分频代码加了详细注释module div( input clk_100M, // 100MHz主时钟 input reset, // 复位信号 output reg clk // 输出的1Hz时钟 ); reg [31:0] count; // 32位计数器 always(posedge clk_100M, negedge reset) begin if(!reset) begin // 复位时清零 clk 1d0; count 32d0; end else if(count 32d50_000000) begin // 计数到50,000,000 count 32d0; clk !clk; // 翻转时钟信号 end else count count 1d1; end endmodule实际调试时发现直接计数50,000,000次会导致时序违例。后来改为两级分频先用计数器降到1kHz再降到1Hz稳定性大幅提升。这是硬件设计的特点——理论简单实现时需要考滤实际电路特性。2.2 里程计算模块车轮转动的数字映射里程计算需要将轮胎脉冲转化为公里数。假设每个脉冲代表100米10个脉冲1公里最大里程9999公里满足实际需求module distanceCount( input clk, // 1Hz时钟 input work, // 载客状态 input start, // 行驶状态 input reset, // 复位 output reg [15:0] distance, // 里程值BCD码 output reg distance_enable // 超起步里程标志 ); always(posedge clk or negedge reset) begin if(!reset) begin distance 16d0; end else if(start work) begin // 行驶且载客时计数 if(distance[3:0] 9) begin // 个位满9进位 distance[3:0] 4d0; if(distance[7:4] 9) begin // 十位满9进位 distance[7:4] 4d0; // 百位、千位同理... end // 其他位处理... end else distance[3:0] distance[3:0] 1d1; end end // 超过3公里时使能计费 always(posedge clk or negedge reset) begin if(!reset) distance_enable 1d0; else if(distance 16d3) distance_enable 1d1; end endmodule调试时发现个问题直接使用16位二进制计数会导致数码管显示解码复杂。后来改用BCD码每4位表示1个十进制数显示模块处理起来更方便。这是硬件设计的经验——前期规划好数据格式能省去后期很多麻烦。3. 计时与计费硬件中的时间管理3.1 精确到秒的等待计时等待计时的难点在于既要计秒也要计分还要产生分钟脉冲信号。这是我的实现方案module timeCount( input clk, // 1Hz时钟 input reset, input start, // 0等待状态 output reg [7:0] s, // 秒计数(BCD) output reg [7:0] m, // 分计数(BCD) output wire time_enable // 分钟脉冲 ); always(posedge clk or negedge reset) begin if(!reset) begin s 8d0; m 8d0; end else if(!start) begin // 等待状态才计时 if(s[3:0] 9) begin // 秒个位 s[3:0] 4d0; if(s[7:4] 5) begin // 秒十位 s[7:4] 4d0; if(m[3:0] 9) begin // 分个位 m[3:0] 4d0; m[7:4] m[7:4] 1d1; // 分十位 end else m[3:0] m[3:0] 1d1; end else s[7:4] s[7:4] 1d1; end else s[3:0] s[3:0] 1d1; end end // 每分钟产生一个脉冲 assign time_enable (m[7:0] 8d1 s[7:0] 8d0) ? 1d1 : 1d0; endmodule实际测试发现机械开关会产生抖动导致误触发计时。后来在开关输入加了防抖模块检测到变化后延时20ms再采样问题解决。3.2 智能计费逻辑实现计费规则起步价5元3公里内超过3公里每公里加2元等待每分钟1元module fee( input work, // 载客状态 input start, // 行驶状态 input select_clk, // 计费使能 input reset, input clk, // 1Hz时钟 output reg [15:0] fee // 费用(BCD) ); reg [3:0] cnt 0; always(posedge clk or negedge reset or negedge work) begin if(!reset !work) fee 16d5; // 复位且无客时显示起步价 else if(select_clk !start work) // 等待计费每分钟1元 fee[3:0] fee[3:0] 1d1; else if(select_clk start work) begin // 行驶计费每公里2元 cnt cnt 1; if(cnt 10) begin // 10脉冲1公里 cnt 0; fee[3:0] fee[3:0] 2d2; // 加2元 end end // BCD进位处理... end endmodule在实验室测试时有位同学的费用显示总是跳变。排查发现是多个always块同时修改fee寄存器导致竞争。解决方法将所有对fee的修改合并到一个always块中。这是Verilog设计的重要原则避免多驱动源。4. 硬件实现从仿真到Basys3部署4.1 数码管显示动态扫描的艺术四位共阳数码管需要动态扫描显示要点扫描频率100Hz避免闪烁每位显示时间均等消隐处理防止鬼影module smg_ip_model( input clk, input work, // 显示里程或费用 input start, input reset, input [15:0] fee, // 费用 input [15:0] dis, // 里程 output [3:0] sm_wei, // 位选 output [7:0] sm_duan // 段选 ); // 生成400Hz扫描时钟 reg clk_400Hz; integer clk_cnt; always(posedge clk, negedge reset) begin if(!reset) begin clk_400Hz 1d0; clk_cnt 32d0; end else if(clk_cnt 32d125000) begin // 100MHz-400Hz clk_cnt 32b0; clk_400Hz !clk_400Hz; end else clk_cnt clk_cnt 1d1; end // 位选循环(1110-1101-1011-0111) reg [3:0] wei_ctrl 4b1110; always(posedge clk_400Hz) wei_ctrl {wei_ctrl[2:0], wei_ctrl[3]}; // 根据位选决定显示数据 reg [3:0] duan_ctrl; always(wei_ctrl) begin if(work) // 显示里程 case(wei_ctrl) 4b1110: duan_ctrl dis[3:0]; // 个位 4b1101: duan_ctrl dis[7:4]; // 十位 // 其他位... endcase else // 显示费用 case(wei_ctrl) 4b1110: duan_ctrl fee[3:0]; // 其他位... endcase end // BCD转7段码 reg [7:0] duan; always(duan_ctrl) case(duan_ctrl) 4h0: duan 8b1100_0000; // 0 4h1: duan 8b1111_1001; // 1 // 其他数字... default: duan 8b1100_0000; endcase assign sm_wei wei_ctrl; assign sm_duan duan; endmodule调试时发现显示有残影原因是段选信号切换比位选信号慢了。解决方法在位选变化前先关闭所有段选待位选稳定后再打开新段选。4.2 引脚约束硬件连接的桥梁Basys3的约束文件(.xdc)关键配置# 时钟 set_property PACKAGE_PIN W5 [get_ports clk_100M] set_property IOSTANDARD LVCMOS33 [get_ports clk_100M] # 数码管 set_property PACKAGE_PIN W7 [get_ports {sm_duan[0]}] # CA set_property PACKAGE_PIN W6 [get_ports {sm_duan[1]}] # CB # ...其他段选 # 位选 set_property PACKAGE_PIN W4 [get_ports {sm_wei[0]}] set_property PACKAGE_PIN V4 [get_ports {sm_wei[1]}] # ...其他位选 # 控制开关 set_property PACKAGE_PIN U9 [get_ports reset] # 车钥匙 set_property PACKAGE_PIN V9 [get_ports work] # 载客状态 set_property PACKAGE_PIN T5 [get_ports start] # 行驶状态有个常见错误是引脚分配冲突比如把两个输出信号分配到同一个引脚。建议做法先规划好所有使用的引脚在原理图上标注编写约束文件时逐项核对4.3 硬件测试验证系统功能完整的测试流程应该包括复位测试拨动reset开关数码管应显示0000所有计数器清零里程计费测试设置work1, start1模拟脉冲输入(每10脉冲1公里)3公里内显示起步价5元超过3公里后每公里增加2元等待计费测试设置work1, start0观察每分钟费用增加1元检查最大等待时间显示(99分钟)显示切换测试work1时显示里程work0时显示费用切换时应无闪烁或乱码边界测试最大里程9999公里最大费用9999元长时间运行稳定性在实验室测试时发现一个有趣现象当快速切换start状态时偶尔会出现计费错误。分析发现是机械开关抖动导致多次触发。最终解决方案在Verilog代码中加入状态锁存只有稳定20ms的状态变化才被识别。

更多文章