基于Verilog与Quartus的FPGA出租车计价器设计:从状态机到数码管显示的实战解析
1. 项目概述与设计思路
大家好,今天我想和大家分享一个非常实用的FPGA项目——用Verilog在Quartus平台上实现出租车计价器。这个项目是我在实际教学中经常使用的案例,因为它涵盖了状态机设计、外设控制和实际应用场景,特别适合FPGA初学者练手。
出租车计价器的核心逻辑其实不难理解。起步价8元覆盖3公里,之后每500米增加1元费用。我们需要实时计算行驶距离和对应费用,并通过数码管显示出来。在实际项目中,车速设定为每秒200米,这样我们可以通过计算时间脉冲来模拟行驶距离。
我选择使用睿智FPGA开发板进行验证,这个板子的资源足够实现这个设计,而且性价比很高。整个设计包含了几个关键模块:按键消抖模块处理用户输入,状态机模块控制计价器的不同状态,速度脉冲生成模块模拟车辆行驶,最后是显示模块驱动数码管。这种模块化的设计思路在实际工程中非常实用,每个模块各司其职,便于调试和维护。
如果你刚开始接触FPGA,这个项目能帮你快速理解状态机的设计理念。状态机是数字逻辑设计的核心概念之一,用好了能让你的设计更加清晰可靠。我在实际项目中发现,很多初学者最大的问题不是不会写代码,而是不知道如何将实际问题转化为状态机的状态和转移条件。
2. 状态机设计详解
状态机是整个计价器系统的控制核心,它决定了系统在不同输入条件下的行为响应。在我的实现中,设计了三个主要状态:空闲状态(IDLE)、行驶状态(RUNNING)和暂停状态(PAUSED)。
当系统上电或复位时,状态机进入空闲状态,等待用户按下启动按键。一旦检测到启动信号,状态转移到行驶状态,此时计价器开始计算里程和费用。如果用户按下暂停键,状态转移到暂停状态,计价暂停但保持当前计费信息。当收到重新计费信号时,状态机重置所有数据并回到空闲状态。
状态转移的设计需要考虑各种边界情况。比如在暂停状态下,如果用户再次按下启动键,应该恢复到行驶状态而不是重新开始计费。这些细节决定了用户体验的好坏。在实际编码中,我使用了一个3位的状态寄存器来表示当前状态,这样既节省资源又便于调试。
// 状态定义
localparam IDLE = 3'b001;
localparam RUNNING = 3'b010;
localparam PAUSED = 3'b100;
// 状态寄存器
reg [2:0] current_state;
reg [2:0] next_state;
// 状态转移逻辑
always @(posedge clk_50M or negedge rst_n) begin
if (!rst_n)
current_state <= IDLE;
else
current_state <= next_state;
end
// 下一状态逻辑
always @(*) begin
case (current_state)
IDLE: begin
if (charge_begin_n)
next_state = RUNNING;
else
next_state = IDLE;
end
RUNNING: begin
if (charge_begin_n)
next_state = PAUSED;
else if (charge_end)
next_state = IDLE;
else
next_state = RUNNING;
end
PAUSED: begin
if (charge_begin_n)
next_state = RUNNING;
else if (charge_end)
next_state = IDLE;
else
next_state = PAUSED;
end
default: next_state = IDLE;
endcase
end
状态机的输出逻辑需要根据当前状态产生相应的控制信号。在行驶状态,使能距离计数和费用计算;在暂停状态,保持当前数值但停止计数;在空闲状态,重置所有寄存器为初始值。这种明确的状态划分让代码更加清晰,也降低了后期调试的难度。
3. 速度脉冲生成模块
速度脉冲模块负责模拟车辆行驶过程中产生的距离脉冲。根据需求,车速是每秒200米,这意味着每5毫秒车辆行驶1米。但我们的计价规则要求每500米增加1元费用,显示精度为100米,所以需要合理设计脉冲生成逻辑。
在我的实现中,使用50MHz的系统时钟作为基准,通过分频产生所需的时间间隔。50MHz的时钟周期是20纳秒,要产生5毫秒的定时需要250000个时钟周期。我使用一个24位的计数器来实现这个分频,当计数器达到249999时产生一个脉冲信号。
// 脉冲生成模块核心代码
reg [23:0] speed_counter;
reg metre_pulse;
// 每5ms产生一个脉冲(对应1米)
always @(posedge clk_50M or negedge rst_n) begin
if (!rst_n) begin
speed_counter <= 24'd0;
metre_pulse <= 1'b0;
end else if (current_state == RUNNING) begin
if (speed_counter == 24'd249999) begin
speed_counter <= 24'd0;
metre_pulse <= 1'b1;
end else begin
speed_counter <= speed_counter + 24'd1;
metre_pulse <= 1'b0;
end
end else begin
speed_counter <= 24'd0;
metre_pulse <= 1'b0;
end
end
每10个1米脉冲产生一个100米脉冲(用于里程显示),每500个1米脉冲产生一个500米脉冲(用于费用计算)。这种分层设计确保了不同精度要求的计时需求。在实际测试中,我发现需要添加使能信号控制,只有在行驶状态下才产生脉冲,这样能准确反映车辆的运行状态。
脉冲生成的准确性直接影响计费的精确度。我在睿智FPGA开发板上实测时,使用了逻辑分析仪检查脉冲波形,确保时间间隔准确。这种验证方式很重要,因为时钟分频容易受到复位逻辑和状态机控制的影响,需要仔细调试。
4. 计费逻辑实现
计费逻辑是项目的核心算法部分,需要准确实现起步价和超出里程的计算。根据需求,前3公里收取8元起步价,之后每500米增加1元费用。在数字逻辑中,我们需要用整数运算来模拟这种分段计费模式。
我使用两个主要寄存器:距离寄存器和费用寄存器。距离寄存器记录以米为单位的总行驶距离,费用寄存器记录以0.1元为单位的费用值。这样设计便于计算和显示,避免了浮点数运算的复杂性。
起步价的判断很关键。当距离小于3000米时,费用固定为800(代表80.0元)。超过3000米后,每500米增加100(代表10.0元)。这里的数学计算需要仔细处理,确保没有误差积累。
// 计费逻辑核心代码
reg [15:0] distance_reg; // 以米为单位
reg [15:0] charge_reg; // 以0.1元为单位
always @(posedge clk_50M or negedge rst_n) begin
if (!rst_n) begin
distance_reg <= 16'd0;
charge_reg <= 16'd800; // 起步价80.0元
end else if (charge_end) begin
distance_reg <= 16'd0;
charge_reg <= 16'd800;
end else if (metre_pulse && current_state == RUNNING) begin
distance_reg <= distance_reg + 16'd1;
if (distance_reg < 3000) begin
charge_reg <= 16'd800; // 保持起步价
end else begin
// 每500米增加1元
if ((distance_reg - 3000) % 500 == 0)
charge_reg <= charge_reg + 16'd10;
end
end
end
在实际编码中,我发现了一个常见问题:当距离刚好达到3000米时,计费应该立即切换为按500米递增模式。我使用取模运算来判断是否达到500米的整数倍,但需要确保在临界点处理正确。测试时特别验证了2999米、3000米、3001米、3500米等关键点的计费情况。
费用寄存器采用16位宽度,最大可以表示6553.5元,完全满足实际应用需求。距离寄存器也是16位,最大表示65535米,即65.535公里,对于市区出租车运营足够了。
5. 数码管显示驱动
数码管显示模块负责将距离和费用信息以人性化的方式展示出来。我们使用8位数码管,其中4位显示费用(格式XX.X元),4位显示距离(格式XX.X公里)。这种显示方式既符合日常习惯,又能满足精度要求。
数码管驱动采用动态扫描方式,通过快速切换位选信号和对应段选信号,利用人眼视觉暂留效应形成稳定显示。50MHz时钟需要适当分频产生扫描频率,一般200-1000Hz的扫描频率比较合适,频率太低会有闪烁感,太高则亮度会下降。
// 数码管扫描计数器
reg [19:0] scan_counter;
reg [7:0] bit_select;
reg [7:0] seg_select;
always @(posedge clk_50M or negedge rst_n) begin
if (!rst_n)
scan_counter <= 20'd0;
else
scan_counter <= scan_counter + 20'd1;
end
// 位选信号生成
always @(posedge clk_50M or negedge rst_n) begin
if (!rst_n)
bit_select <= 8'b00000001;
else if (scan_counter == 20'd99999) begin
bit_select <= {bit_select[6:0], bit_select[7]};
scan_counter <= 20'd0;
end
end
// 段选信号生成
always @(*) begin
case (bit_select)
8'b00000001: seg_select = digit0; // 费用个位
8'b00000010: seg_select = digit1; // 费用小数点后第一位
8'b00000100: seg_select = digit2; // 费用十位
8'b00001000: seg_select = digit3; // 费用百位
8'b00010000: seg_select = digit4; // 距离个位
8'b00100000: seg_select = digit5; // 距离小数点后第一位
8'b01000000: seg_select = digit6; // 距离十位
8'b10000000: seg_select = digit7; // 距离百位
default: seg_select = 8'b11111111;
endcase
end
数据显示需要将二进制数值转换为BCD码,然后再通过查表转换为七段数码管编码。费用显示需要特别处理小数点位置,在十位和个位之间显示小数点表示0.1元精度。距离显示同样需要处理小数点,在百位和十位之间显示小数点表示0.1公里精度。
我在实际调试中发现,数码管的亮度一致性是个需要注意的问题。不同位选信号的占空比应该相同,否则会出现亮度不均的现象。另外,段选信号需要根据具体的数码管类型(共阴或共阳)进行适当的逻辑调整。
6. 按键处理与消抖技术
机械按键的抖动问题是数字系统设计中常见的挑战。当按键按下或释放时,会产生持续10-20毫秒的机械抖动,如果不处理会导致多次误触发。在这个计价器设计中,按键的准确响应直接影响用户体验和计费准确性。
我采用的消抖方案是采样滤波法。通过定时采样按键状态,只有当连续多次采样值相同时才认为按键状态稳定。这种方法硬件资源消耗少,效果可靠。消抖时间一般设置为20毫秒左右,能够覆盖大多数机械按键的抖动时间。
module key_jitter(
input clkin,
input key_in,
output reg key_negedge
);
reg [19:0] counter;
reg key_reg;
reg key_stable;
// 20ms计时器
always @(posedge clkin) begin
if (counter == 20'd999999)
counter <= 20'd0;
else
counter <= counter + 20'd1;
end
// 定时采样
always @(posedge clkin) begin
if (counter == 20'd0)
key_reg <= key_in;
end
// 边沿检测
always @(posedge clkin) begin
if (counter == 20'd0) begin
key_stable <= key_reg;
key_negedge <= key_stable & ~key_reg;
end
end
endmodule
启动/暂停按键和重新计费按键需要不同的处理方式。启动/暂停按键是切换型按键,每次按下切换状态;重新计费按键是脉冲型按键,按下后产生一个复位脉冲。在实际设计中,我为每个按键单独实例化消抖模块,确保各自独立工作。
测试按键功能时,我使用了不同按压力度和速度进行验证,确保在各种操作情况下都能可靠响应。特别是快速连续按键的情况,需要确保状态切换的准确性。睿智FPGA开发板上的按键质量不错,机械抖动相对较小,但消抖处理仍然是必要的。
7. Quartus工程实现与调试
在Quartus II开发环境中创建工程时,需要注意一些设置细节。首先选择正确的器件型号,睿智FPGA开发板通常使用Cyclone IV系列器件。然后创建顶层模块和子模块,建议模块名称与文件名保持一致,避免混淆。
我习惯的工程目录结构是:prj目录存放工程文件,rtl目录存放Verilog源代码,sim目录存放仿真文件,doc目录存放设计文档。这种结构清晰规范,便于团队协作和版本管理。在Quartus中设置好库文件搜索路径,确保所有模块都能正确编译。
管脚分配是关键步骤,需要根据开发板的实际布局进行配置。睿智FPGA开发板的数码管通常连接在GPIO口上,按键连接到专用按键引脚,时钟信号连接到专用时钟引脚。在Assignment Editor中仔细分配每个信号的管脚编号和电气特性。
# 管脚分配示例
set_location_assignment PIN_E1 -to clk_50M
set_location_assignment PIN_M1 -to rst_n
set_location_assignment PIN_B8 -to charge_begin
set_location_assignment PIN_A8 -to charge_end
set_location_assignment PIN_F11 -to running_led
编译过程中需要关注Quartus的警告信息。有些警告可以忽略,但有些可能暗示潜在问题。特别是时序警告,如果建立时间或保持时间不满足,可能导致系统不稳定。我一般会启用TimeQuest时序分析器进行详细检查,确保时序收敛。
调试阶段我使用了SignalTap II逻辑分析仪,这是Quartus自带的强大工具。通过插入探测点,可以实时观察内部信号的波形,帮助定位问题。特别是在验证状态机转移和脉冲生成时,SignalTap提供了很大帮助。
8. 常见问题与优化建议
在实际实现过程中,可能会遇到一些典型问题。首先是资源使用问题,这个计价器设计相对简单,但如果添加更多功能可能会占用较多逻辑单元。建议定期查看编译报告中的资源使用情况,确保不超过器件容量。
时序问题也是常见的挑战。当系统时钟频率较高时,组合逻辑路径可能产生较大的延迟,导致时序违规。可以通过流水线设计、逻辑优化等方式改善时序性能。在Quartus中可以使用Optimization Technique设置来平衡性能和资源使用。
显示闪烁是另一个常见问题。如果数码管扫描频率不合适,会出现闪烁或亮度不均。建议调整扫描频率在200-500Hz范围内,这个频率范围既能避免闪烁,又能保证足够的亮度。也可以通过PWM调光技术进一步改善显示效果。
// PWM调光示例
reg [7:0] pwm_counter;
reg [7:0] brightness;
always @(posedge clk_50M) begin
pwm_counter <= pwm_counter + 8'd1;
end
assign seg_enable = (pwm_counter < brightness);
对于功耗优化,建议在非活动状态下关闭不必要的模块时钟。Quartus的PowerPlay Power Analyzer工具可以帮助分析功耗情况,指导优化方向。特别是电池供电的应用场景,功耗优化尤为重要。
代码可读性和可维护性也很重要。建议添加充分的注释,使用有意义的信号命名,采用一致的代码风格。模块化设计使得后期功能扩展更加容易,比如可以添加夜间计费模式、低速等待计费等功能。
最后推荐使用版本控制系统管理设计文件,即使是个人项目也是个好习惯。Git是常用的选择,可以跟踪设计变更,方便回溯和协作。定期备份工程文件,避免意外丢失工作成果。
更多推荐

所有评论(0)