1. 项目概述
这个基于Verilog HDL和Quartus II的自动售货机控制系统,是我在数字逻辑设计课程中的期末实践项目。它完整实现了投币识别、商品选择、找零计算和倒计时显示等核心功能模块。整个系统采用状态机架构,通过FPGA开发板上的按键、LED和数码管完成人机交互。
在实际开发过程中,我遇到了状态机跳转异常、按键消抖不稳定、数码管显示闪烁等多个典型问题。通过这个项目,不仅巩固了Verilog的编码规范,更深入理解了时序逻辑设计中的关键要点。下面将详细解析各模块的设计思路和实现细节。
2. 系统架构设计
2.1 整体状态机设计
系统采用Moore型状态机作为控制核心,共定义6个主要状态:
- IDLE:待机状态,显示欢迎语
- INSERT_COIN:投币状态,累计金额
- SELECT_ITEM:商品选择状态
- DISPENSE:出货状态
- CHANGE:找零状态
- TIMEOUT:操作超时状态
状态转移条件由投币信号、选择信号和30秒倒计时共同决定。以下是关键状态转移代码片段:
verilog复制always @(posedge clk or posedge rst) begin
if (rst)
current_state <= IDLE;
else
case(current_state)
IDLE: if (coin_in) current_state <= INSERT_COIN;
INSERT_COIN: if (no_coin_for_2s) current_state <= TIMEOUT;
else if (confirm) current_state <= SELECT_ITEM;
// 其他状态转移...
endcase
end
2.2 时钟与复位设计
系统使用50MHz主时钟,通过分频模块产生:
- 1kHz数码管扫描时钟
- 100Hz按键检测时钟
- 1Hz倒计时时钟
异步复位电路采用施密特触发器进行消抖处理,确保复位信号稳定可靠。实际测试发现,简单的RC滤波电路在FPGA板上会产生复位不可靠的问题,改进方案如下:
verilog复制// 改进后的复位电路
reg [3:0] reset_debounce;
always @(posedge clk) begin
reset_debounce <= {reset_debounce[2:0], ~KEY0}; // KEY0为复位按键
if (&reset_debounce) reset_reg <= 1'b1;
else if (~|reset_debounce) reset_reg <= 1'b0;
end
3. 核心模块实现
3.1 投币识别模块
支持1元、5元、10元三种面额识别,通过三个独立按键模拟。采用脉冲边沿检测技术避免长按重复计数:
verilog复制reg [1:0] coin_1r_prev;
always @(posedge clk) begin
coin_1r_prev <= {coin_1r_prev[0], KEY1}; // KEY1模拟1元投币
if (coin_1r_prev == 2'b01) // 检测上升沿
total_money <= total_money + 3'd1;
end
金额累计使用BCD码格式,便于后续数码管显示和金额比较。设计中需要注意:
- 最大投币金额限制为99元
- 投币时数码管显示当前累计金额
- 30秒无操作自动清零
3.2 商品选择与库存管理
定义4种商品及其价格:
- A商品:3元
- B商品:5元
- C商品:7元
- D商品:10元
库存管理使用4个4位寄存器,上电初始化后可通过管理员模式补货。商品选择逻辑包含以下保护机制:
- 余额不足时对应商品LED闪烁提示
- 库存为零时禁用选择
- 选择确认后立即扣除库存
verilog复制// 商品选择判断逻辑
always @(*) begin
case(sel_item)
2'b00: item_valid = (total_money >= 3) && (stock_A != 0);
2'b01: item_valid = (total_money >= 5) && (stock_B != 0);
// 其他商品判断...
endcase
end
3.3 找零计算模块
找零计算采用BCD码减法器实现,支持最大找零金额为9元(假设面额为1元)。关键算法如下:
verilog复制// BCD减法器
always @(*) begin
if (total_money >= item_price) begin
change = total_money - item_price;
enough_money = 1'b1;
end else begin
change = 4'd0;
enough_money = 1'b0;
end
end
实际测试发现,当找零金额超过9元时,普通BCD减法会出现错误。解决方案是采用双BCD码处理十位数:
verilog复制// 改进的BCD减法器
reg [3:0] change_units, change_tens;
always @(*) begin
if (total_money >= item_price) begin
{change_tens, change_units} = total_money - item_price;
end
// ...
end
3.4 倒计时显示模块
30秒倒计时使用1Hz时钟驱动,数码管动态扫描显示剩余时间。显示格式为"S-XX",其中XX为剩余秒数:
verilog复制reg [5:0] countdown;
always @(posedge clk_1hz or posedge rst) begin
if (rst)
countdown <= 6'd30;
else if (current_state != IDLE)
countdown <= (countdown == 0) ? 0 : countdown - 1;
end
// 数码管显示控制
always @(posedge clk_1khz) begin
case(scan_cnt)
2'b00: {seg_sel, seg_data} = {2'b01, 8'b01001001}; // 显示"S"
2'b01: {seg_sel, seg_data} = {2'b10, 8'b01111111}; // 显示"-"
2'b10: {seg_sel, seg_data} = {2'b11, bin2bcd(countdown/10)}; // 十位
2'b11: {seg_sel, seg_data} = {2'b00, bin2bcd(countdown%10)}; // 个位
endcase
end
4. 关键问题与解决方案
4.1 按键消抖处理
最初使用简单的延时消抖法,在快速连续操作时会出现误触发。改进方案采用状态机消抖:
verilog复制parameter DEBOUNCE_IDLE = 2'b00;
parameter DEBOUNCE_WAIT = 2'b01;
parameter DEBOUNCE_CONFIRM = 2'b10;
always @(posedge clk_100hz) begin
case(debounce_state)
DEBOUNCE_IDLE:
if (key_in != key_state) begin
debounce_state <= DEBOUNCE_WAIT;
debounce_cnt <= 8'd0;
end
DEBOUNCE_WAIT:
if (debounce_cnt == 8'd20) begin
debounce_state <= DEBOUNCE_CONFIRM;
key_state <= key_in;
end else
debounce_cnt <= debounce_cnt + 1;
DEBOUNCE_CONFIRM:
debounce_state <= DEBOUNCE_IDLE;
endcase
end
4.2 数码管显示闪烁
动态扫描时出现闪烁问题,原因分析:
- 扫描频率不稳定
- 段选和位选信号不同步
- 刷新率低于视觉暂留阈值
解决方案:
- 严格使用1kHz扫描时钟
- 段选/位选信号同步更新
- 增加消隐处理
verilog复制// 改进的数码管驱动
always @(posedge clk_1khz) begin
seg_sel <= next_sel;
seg_data <= next_data;
// 增加1ms消隐
if (blank_cnt != 0) begin
seg_data <= 8'hFF;
blank_cnt <= blank_cnt - 1;
end
end
4.3 状态机异常跳转
调试中发现状态机偶尔会跳转到非法状态,原因:
- 组合逻辑产生毛刺
- 异步信号未同步处理
- 状态编码未用parameter明确定义
改进措施:
- 采用独热码(one-hot)状态编码
- 对异步输入信号进行两级寄存器同步
- 增加default状态处理
verilog复制parameter [5:0]
IDLE = 6'b000001,
INSERT_COIN = 6'b000010,
// 其他状态...
TIMEOUT = 6'b100000;
always @(posedge clk) begin
async_signal_reg <= {async_signal_reg[0], async_signal_raw};
end
always @(posedge clk) begin
case(current_state)
// 正常状态转移
default: current_state <= IDLE; // 异常状态自动复位
endcase
end
5. Quartus II实现细节
5.1 工程设置要点
- 器件选择:根据开发板型号正确选择FPGA器件(如Cyclone IV EP4CE6E22C8)
- 引脚分配:严格按照开发板原理图分配
- 按键:设置为带内部上拉的输入
- 数码管:设置为推挽输出
- 编译选项:
- 开启状态机安全综合(Safe State Machine)
- 设置优化策略为平衡(Balanced)
5.2 时序约束设置
关键时序约束:
- 创建50MHz主时钟约束
- 设置输入延迟(Input Delay)约束按键信号
- 输出延迟(Output Delay)约束数码管信号
tcl复制# SDC时序约束示例
create_clock -name clk -period 20 [get_ports clk]
set_input_delay -clock clk 5 [get_ports {KEY[*]}]
set_output_delay -clock clk 3 [get_ports {SEG[*]}]
5.3 仿真验证方法
- 编写Testbench模拟投币、选择操作序列
- 使用ModelSim进行功能仿真
- 关键测试场景:
- 连续快速投币
- 余额不足时选择商品
- 倒计时超时恢复
- 找零金额计算
verilog复制// 测试用例示例
initial begin
// 初始化
rst = 1; #100; rst = 0;
// 测试1元投币
KEY1 = 0; #50; KEY1 = 1; #100000; // 模拟按键按下
// 检查金额显示
if (SEG !== 8'b00000110) $display("Error: 1元显示错误");
end
6. 硬件调试技巧
6.1 信号观测方法
- 使用SignalTap II逻辑分析仪抓取内部信号
- 配置采样深度为1K
- 触发条件设置为状态机跳转
- 临时将内部信号引出到LED观察
- 例如:assign LEDR[0] = (current_state == IDLE);
6.2 常见硬件问题
- 数码管显示不全:
- 检查限流电阻值(通常220Ω)
- 确认共阴/共阳类型匹配
- 按键响应异常:
- 测量按键按下时的电压值
- 调整消抖时间参数
- FPGA配置失败:
- 检查JTAG连接
- 确认供电电压稳定
6.3 功耗优化建议
- 不使用的IO口设置为三态输入
- 低频模块使用时钟门控
- 数码管扫描间隔可适当延长到2ms
- 使用Quartus PowerPlay分析功耗热点
verilog复制// 时钟门控示例
reg gated_clock;
always @(*) begin
gated_clock = clk_1hz & (current_state != IDLE);
end
7. 项目扩展方向
- 增加RFID支付功能:
- 集成MFRC522模块
- 设计SPI接口控制器
- 网络库存监控:
- 添加UART接口
- 与上位机通信
- 销售数据分析:
- 使用FPGA片内RAM存储交易记录
- 实现简单统计功能
- 温度监控:
- 集成DS18B20温度传感器
- 商品冷藏提醒
verilog复制// SPI主机接口示例
reg [7:0] spi_tx_data;
wire [7:0] spi_rx_data;
spi_master spi_inst (
.clk(clk),
.rst(rst),
.tx_data(spi_tx_data),
.rx_data(spi_rx_data),
.sck(SPI_SCK),
.mosi(SPI_MOSI),
.miso(SPI_MISO),
.cs(SPI_CS)
);
通过这个项目,我深刻体会到数字系统设计需要综合考虑功能正确性、时序稳定性和硬件资源利用率。特别是在状态机设计和异步信号处理方面,任何疏忽都可能导致难以调试的随机故障。建议初学者在实现基本功能后,一定要进行边界条件测试和长时间稳定性测试。