作为一名FPGA开发工程师,我经常遇到初学者对Verilog语法感到困惑的情况。今天我想通过一个自动售货机的实际项目,带大家从零开始理解FPGA开发的基础知识。这个项目非常适合刚接触FPGA的朋友,因为它涵盖了数字电路设计的核心概念,同时又足够直观有趣。
自动售货机看似简单,但它包含了状态机、数据处理、用户交互等FPGA开发的典型要素。在DAY1的内容中,我们将重点解析项目的Verilog代码基础部分,特别是数据类型和控制结构这些看似基础但极其重要的概念。
提示:如果你是第一次接触FPGA开发,建议先准备好Quartus或Vivado开发环境,这样可以在阅读的同时动手实践代码示例。
wire类型是Verilog中最基础的数据类型,它直接对应硬件中的物理连线。在自动售货机项目中,所有的输入输出端口都默认使用wire类型:
verilog复制input [0:2] KEY; // 3位按键输入
input [0:9] SW; // 10位拨码开关输入
output [0:9] LEDR; // 10位LED输出
output [0:6] HEX5; // 7段数码管输出
wire类型有几个重要特性:
在实际项目中,我习惯将所有显式连接的信号都声明为wire,即使它默认就是wire类型。这样可以提高代码的可读性:
verilog复制wire [3:0] product_selected; // 明确声明为wire类型
与wire不同,reg类型可以存储数值,相当于硬件中的寄存器。在自动售货机项目中,我们需要用reg来保存各种状态信息:
verilog复制reg[1:0] escolher; // 2位选择状态
reg[7:0] produto_escolhido; // 8位商品编码
reg[7:0] dinheiro_inserido; // 8位投入金额
reg[3:0] estado_atual; // 4位状态机状态
reg类型的关键特点:
常见误区:很多初学者认为reg一定会被综合成寄存器,实际上在组合逻辑中使用的reg并不会生成触发器,它只是语法上的寄存器。
parameter是Verilog中定义常量的方式,它提高了代码的可读性和可维护性。在自动售货机项目中,我们用parameter来定义状态机的各个状态:
verilog复制parameter espera=0, select=1, insert_money=2, give_change=3, show=4;
parameter的优势:
在实际工程中,我习惯将所有的魔法数字都用parameter替代,比如:
verilog复制parameter PRICE_COLA = 50; // 可乐价格
parameter PRICE_CHIP = 75; // 薯片价格
Verilog的语法借鉴了C语言,但也有自己的特点。在自动售货机项目中,我们可以看到几个关键语法元素:
verilog复制dinheiro_inserido = dinheiro_inserido + 25;
verilog复制begin
statement1;
statement2;
end
verilog复制module vending_machine(
input [0:2] KEY,
output [0:6] HEX5
);
// 模块内容
endmodule
编码规范建议:我个人的习惯是begin与关键字同行,end单独一行并缩进对齐,这样代码层次更清晰。
Verilog的if-else与C语言非常相似,但要注意它是在描述硬件电路:
verilog复制if(SW[0]==1) begin
dinheiro_inserido = dinheiro_inserido + 25;
moedas_inseridas_25 = moedas_inseridas_25+1;
end
重要注意事项:
case语句是状态机设计的利器,自动售货机项目中用它来实现商品选择和状态转换:
verilog复制case(produto_escolhido)
1: price = 50;
2: price = 75;
3: price = 100;
endcase
case语句的特点:
经验分享:在状态机设计中,我习惯为case语句添加default分支,即使理论上不会执行到,这是一个良好的防御性编程习惯。
Verilog支持多种循环结构,但在可综合代码中要谨慎使用:
verilog复制for (i = 0; i < 8; i = i+1) begin
bcd = {bcd[10:0], bin[7-i]};
end
verilog复制repeat (10) begin
if( i*100 < valor_troco) i = i + 1;
end
verilog复制while (condition) begin
statement;
end
verilog复制forever #5 clock = ~clock; // 生成时钟信号
重要提示:在可综合代码中,循环次数必须是编译时可确定的常数,否则会导致不可预测的硬件行为。
自动售货机的核心是一个有限状态机(FSM),在Verilog中我们使用parameter和case语句来实现:
verilog复制parameter IDLE=0, SELECT=1, PAYMENT=2, DELIVERY=3;
reg [1:0] current_state, next_state;
always @(posedge clk or posedge reset) begin
if(reset) current_state <= IDLE;
else current_state <= next_state;
end
always @(*) begin
case(current_state)
IDLE: if(any_key_pressed) next_state = SELECT;
SELECT: if(selection_valid) next_state = PAYMENT;
PAYMENT: if(payment_done) next_state = DELIVERY;
DELIVERY: next_state = IDLE;
default: next_state = IDLE;
endcase
end
状态机设计要点:
自动售货机需要处理金额计算和找零,这里使用BCD码转换作为示例:
verilog复制reg [3:0] bcd1, bcd2, bcd3; // 个十百位BCD码
always @(posedge clk) begin
if(reset) begin
bcd1 <= 0; bcd2 <= 0; bcd3 <= 0;
end
else begin
// 二进制转BCD算法
for (i = 0; i < 8; i = i+1) begin
bcd1 = (bcd1 > 4) ? bcd1 + 3 : bcd1;
bcd2 = (bcd2 > 4) ? bcd2 + 3 : bcd2;
bcd3 = (bcd3 > 4) ? bcd3 + 3 : bcd3;
{bcd3, bcd2, bcd1} = {bcd3, bcd2, bcd1} << 1;
{bcd1[0], bin} = {bin, 1'b0};
end
end
end
金额处理注意事项:
自动售货机需要驱动LED和数码管显示状态信息:
verilog复制// LED显示投入金额
assign LEDR = dinheiro_inserido[7:0];
// 数码管显示价格
always @(*) begin
case(produto_escolhido)
1: HEX5 = 7'b100_1110; // 显示"5"
2: HEX5 = 7'b001_0010; // 显示"7"
3: HEX5 = 7'b000_0001; // 显示"0"
default: HEX5 = 7'b111_1111; // 全灭
endcase
end
显示驱动设计技巧:
在实现自动售货机项目时,我遇到过几个典型问题:
有效的仿真可以节省大量调试时间:
verilog复制initial begin
reset = 1;
#20 reset = 0;
SW = 1; // 选择商品1
#50 KEY = 1; // 确认选择
// 更多测试场景...
end
verilog复制always @(posedge clk) begin
$display("State=%d, Money=%d", current_state, dinheiro_inserido);
end
当代码下载到FPGA后出现问题:
verilog复制assign LEDR[9] = (current_state == SELECT); // 选择状态指示
在DAY1的实现中,我们重点构建了自动售货机的基础框架。掌握了这些Verilog基础知识后,在后续的DAY2中,我们将进一步完善商品选择、货币识别和出货控制等高级功能。