1. Verilog快速入门指南
作为一名FPGA开发者,掌握Verilog是必备技能。Verilog作为一种硬件描述语言(HDL),主要用于数字电路的设计和验证。与软件编程不同,Verilog描述的是硬件电路的行为和结构,因此需要采用完全不同的思维方式。
提示:学习Verilog时,最重要的是理解"硬件思维"——你写的每一行代码最终都会变成实际的硬件电路。
1.1 Verilog基础语法
Verilog的基本构建块是模块(module),每个模块代表一个具有特定功能的电路单元。模块通过端口(port)与其他模块或外部环境通信。
1.1.1 基本模块结构
一个最简单的Verilog模块如下:
verilog复制module my_module(
input wire a, // 输入端口
output reg b // 输出端口
);
// 模块内部逻辑
endmodule
input:声明输入端口output:声明输出端口wire:线网类型,用于连续赋值reg:寄存器类型,用于过程赋值
1.1.2 连续赋值与过程赋值
Verilog中有两种主要的赋值方式:
- 连续赋值(Continuous Assignment):使用
assign关键字
verilog复制assign out = a & b; // 描述一个与门
- 过程赋值(Procedural Assignment):在
always块中使用
verilog复制always @(posedge clk) begin
q <= d; // 描述一个D触发器
end
注意:
assign语句的目标必须是wire类型,而always块中的赋值目标必须是reg类型。
1.2 运算符与表达式
Verilog支持丰富的运算符,包括:
- 算术运算符:
+,-,*,/,%,**(幂) - 逻辑运算符:
&&,||,! - 位运算符:
&,|,~,^,~^ - 移位运算符:
<<,>>,<<<,>>> - 关系运算符:
==,!=,===,!==,>,<,>=,<=
1.2.1 缩减运算符
缩减运算符(Reduction Operators)可以对向量的所有位进行操作:
verilog复制wire [3:0] in;
wire out_and, out_or, out_xor;
assign out_and = ∈ // 等价于 in[3] & in[2] & in[1] & in[0]
assign out_or = |in; // 等价于 in[3] | in[2] | in[1] | in[0]
assign out_xor = ^in; // 等价于 in[3] ^ in[2] ^ in[1] ^ in[0]
1.2.2 连接运算符
连接运算符{}可以将多个信号或常量拼接起来:
verilog复制wire [7:0] byte1 = 8'hAB;
wire [7:0] byte2 = 8'hCD;
wire [15:0] word = {byte1, byte2}; // 结果为16'hABCD
1.3 向量与数组
Verilog中的向量(Vector)用于表示多位信号:
verilog复制wire [7:0] data_bus; // 8位总线
reg [31:0] counter; // 32位计数器
向量可以部分选择:
verilog复制wire [15:0] word = 16'h1234;
wire [7:0] high_byte = word[15:8]; // 获取高8位,值为8'h12
wire [7:0] low_byte = word[7:0]; // 获取低8位,值为8'h34
2. Verilog组合逻辑设计
组合逻辑电路的输出仅取决于当前输入,不包含任何存储元件。在Verilog中,可以使用assign语句或always @(*)块来描述组合逻辑。
2.1 基本门电路实现
2.1.1 简单门电路
verilog复制module basic_gates(
input a, b,
output and_out, or_out, not_out, xor_out
);
assign and_out = a & b;
assign or_out = a | b;
assign not_out = ~a;
assign xor_out = a ^ b;
endmodule
2.1.2 多路选择器
4选1多路选择器实现:
verilog复制module mux4_1(
input [3:0] in,
input [1:0] sel,
output reg out
);
always @(*) begin
case(sel)
2'b00: out = in[0];
2'b01: out = in[1];
2'b10: out = in[2];
2'b11: out = in[3];
endcase
end
endmodule
2.2 译码器与编码器
2.2.1 3-8译码器
verilog复制module decoder3_8(
input [2:0] in,
input enable,
output reg [7:0] out
);
always @(*) begin
if(enable) begin
case(in)
3'b000: out = 8'b00000001;
3'b001: out = 8'b00000010;
3'b010: out = 8'b00000100;
3'b011: out = 8'b00001000;
3'b100: out = 8'b00010000;
3'b101: out = 8'b00100000;
3'b110: out = 8'b01000000;
3'b111: out = 8'b10000000;
endcase
end else begin
out = 8'b00000000;
end
end
endmodule
2.2.2 8-3优先编码器
verilog复制module priority_encoder8_3(
input [7:0] in,
output reg [2:0] out
);
always @(*) begin
casez(in)
8'b1???????: out = 3'b000;
8'b01??????: out = 3'b001;
8'b001?????: out = 3'b010;
8'b0001????: out = 3'b011;
8'b00001???: out = 3'b100;
8'b000001??: out = 3'b101;
8'b0000001?: out = 3'b110;
8'b00000001: out = 3'b111;
default: out = 3'b000;
endcase
end
endmodule
提示:
casez语句中的?表示"不关心"位,可以匹配0、1或x(未知)。
2.3 算术运算电路
2.3.1 加法器实现
1位全加器:
verilog复制module full_adder(
input a, b, cin,
output sum, cout
);
assign sum = a ^ b ^ cin;
assign cout = (a & b) | (a & cin) | (b & cin);
endmodule
4位加法器:
verilog复制module adder4(
input [3:0] a, b,
input cin,
output [3:0] sum,
output cout
);
assign {cout, sum} = a + b + cin;
endmodule
2.3.2 乘法器实现
4位乘法器:
verilog复制module multiplier4(
input [3:0] a, b,
output [7:0] product
);
assign product = a * b;
endmodule
3. Verilog时序逻辑设计
时序逻辑电路包含存储元件,其输出不仅取决于当前输入,还取决于电路的历史状态。在Verilog中,时序逻辑通常使用always @(posedge clk)或always @(negedge clk)来描述。
3.1 触发器与寄存器
3.1.1 D触发器
基本D触发器:
verilog复制module d_flip_flop(
input clk, d,
output reg q
);
always @(posedge clk) begin
q <= d;
end
endmodule
带同步复位的D触发器:
verilog复制module dff_sync_reset(
input clk, reset, d,
output reg q
);
always @(posedge clk) begin
if(reset)
q <= 1'b0;
else
q <= d;
end
endmodule
带异步复位的D触发器:
verilog复制module dff_async_reset(
input clk, reset, d,
output reg q
);
always @(posedge clk or posedge reset) begin
if(reset)
q <= 1'b0;
else
q <= d;
end
endmodule
注意:同步复位和异步复位的区别在于敏感列表和复位条件判断的时机。
3.1.2 移位寄存器
8位串行输入并行输出移位寄存器:
verilog复制module shift_register_sipo(
input clk, reset, serial_in,
output reg [7:0] parallel_out
);
always @(posedge clk or posedge reset) begin
if(reset)
parallel_out <= 8'b0;
else
parallel_out <= {parallel_out[6:0], serial_in};
end
endmodule
3.2 计数器设计
3.2.1 二进制计数器
4位二进制计数器:
verilog复制module counter4(
input clk, reset,
output reg [3:0] count
);
always @(posedge clk or posedge reset) begin
if(reset)
count <= 4'b0;
else
count <= count + 1'b1;
end
endmodule
带使能和加载的计数器:
verilog复制module counter_load(
input clk, reset, enable, load,
input [3:0] load_data,
output reg [3:0] count
);
always @(posedge clk or posedge reset) begin
if(reset)
count <= 4'b0;
else if(load)
count <= load_data;
else if(enable)
count <= count + 1'b1;
end
endmodule
3.2.2 分频器
时钟分频器(将时钟频率除以N):
verilog复制module clock_divider(
input clk, reset,
output reg divided_clk
);
parameter DIVISOR = 10;
reg [31:0] counter;
always @(posedge clk or posedge reset) begin
if(reset) begin
counter <= 0;
divided_clk <= 0;
end else begin
if(counter == (DIVISOR/2 - 1)) begin
divided_clk <= ~divided_clk;
counter <= 0;
end else begin
counter <= counter + 1;
end
end
end
endmodule
3.3 有限状态机设计
有限状态机(FSM)是时序逻辑设计中的重要概念,常用于控制逻辑的实现。
3.3.1 Moore型状态机
简单的3状态Moore机:
verilog复制module moore_fsm(
input clk, reset, in,
output reg out
);
// 状态定义
parameter S0 = 2'b00;
parameter S1 = 2'b01;
parameter S2 = 2'b10;
reg [1:0] current_state, next_state;
// 状态寄存器
always @(posedge clk or posedge reset) begin
if(reset)
current_state <= S0;
else
current_state <= next_state;
end
// 下一状态逻辑
always @(*) begin
case(current_state)
S0: next_state = in ? S1 : S0;
S1: next_state = in ? S2 : S0;
S2: next_state = in ? S2 : S0;
default: next_state = S0;
endcase
end
// 输出逻辑(Moore型:输出仅取决于当前状态)
always @(*) begin
case(current_state)
S0: out = 1'b0;
S1: out = 1'b0;
S2: out = 1'b1;
default: out = 1'b0;
endcase
end
endmodule
3.3.2 Mealy型状态机
简单的2状态Mealy机:
verilog复制module mealy_fsm(
input clk, reset, in,
output reg out
);
// 状态定义
parameter S0 = 1'b0;
parameter S1 = 1'b1;
reg current_state, next_state;
// 状态寄存器
always @(posedge clk or posedge reset) begin
if(reset)
current_state <= S0;
else
current_state <= next_state;
end
// 下一状态逻辑
always @(*) begin
case(current_state)
S0: next_state = in ? S1 : S0;
S1: next_state = in ? S1 : S0;
default: next_state = S0;
endcase
end
// 输出逻辑(Mealy型:输出取决于当前状态和输入)
always @(*) begin
case(current_state)
S0: out = 1'b0;
S1: out = in ? 1'b1 : 1'b0;
default: out = 1'b0;
endcase
end
endmodule
4. Verilog高级技巧与实战经验
4.1 模块化设计与层次化实例
4.1.1 模块实例化
Verilog支持模块的层次化实例,这是构建复杂系统的基础:
verilog复制module top_module(
input [3:0] a, b,
input cin,
output [3:0] sum,
output cout
);
wire [3:0] carry;
// 实例化4个全加器
full_adder fa0(.a(a[0]), .b(b[0]), .cin(cin), .sum(sum[0]), .cout(carry[0]));
full_adder fa1(.a(a[1]), .b(b[1]), .cin(carry[0]), .sum(sum[1]), .cout(carry[1]));
full_adder fa2(.a(a[2]), .b(b[2]), .cin(carry[1]), .sum(sum[2]), .cout(carry[2]));
full_adder fa3(.a(a[3]), .b(b[3]), .cin(carry[2]), .sum(sum[3]), .cout(cout));
endmodule
4.1.2 参数化设计
使用parameter可以使模块更加灵活:
verilog复制module generic_adder #(
parameter WIDTH = 8
)(
input [WIDTH-1:0] a, b,
input cin,
output [WIDTH-1:0] sum,
output cout
);
assign {cout, sum} = a + b + cin;
endmodule
实例化时可以指定参数:
verilog复制generic_adder #(.WIDTH(16)) adder16(.*);
4.2 阻塞赋值与非阻塞赋值
Verilog中有两种赋值方式,理解它们的区别至关重要:
- 阻塞赋值(
=):顺序执行,立即赋值
verilog复制always @(*) begin
a = b; // 立即赋值
c = a; // 使用新的a值
end
- 非阻塞赋值(
<=):并行执行,在块结束时赋值
verilog复制always @(posedge clk) begin
a <= b; // 记录赋值操作
c <= a; // 使用旧的a值
end
黄金规则:
- 组合逻辑使用阻塞赋值(
=)- 时序逻辑使用非阻塞赋值(
<=)- 不要在同一个always块中混用两种赋值方式
4.3 测试平台编写
验证是数字设计的重要环节。下面是一个简单的测试平台示例:
verilog复制module testbench;
// 信号声明
reg clk = 0;
reg reset = 0;
reg [3:0] a, b;
wire [3:0] sum;
wire cout;
// 时钟生成
always #5 clk = ~clk;
// 实例化被测设计
adder4 uut(.a(a), .b(b), .cin(1'b0), .sum(sum), .cout(cout));
// 测试激励
initial begin
// 初始化
reset = 1;
a = 4'b0;
b = 4'b0;
// 复位
#10 reset = 0;
// 测试用例1
#10 a = 4'b0011; b = 4'b0101;
// 测试用例2
#10 a = 4'b1111; b = 4'b0001;
// 测试用例3
#10 a = 4'b1000; b = 4'b1000;
// 结束仿真
#10 $finish;
end
// 波形输出
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, testbench);
end
endmodule
4.4 常见问题与调试技巧
4.4.1 锁存器推断
当组合逻辑的always块没有为所有可能的输入组合指定输出时,综合工具会推断出锁存器:
verilog复制// 不好的写法:会产生锁存器
always @(*) begin
if(enable)
out = in;
end
// 好的写法:为所有情况指定输出
always @(*) begin
if(enable)
out = in;
else
out = 1'b0;
end
4.4.2 时序约束与亚稳态
在跨时钟域传输数据时,需要考虑亚稳态问题。常见的解决方案包括:
- 两级触发器同步:
verilog复制always @(posedge clk) begin
sync_reg1 <= async_signal;
sync_reg2 <= sync_reg1;
end
- 握手协议
- 异步FIFO
4.4.3 资源优化
- 使用
case而不是嵌套的if-else可以提高综合结果的质量 - 合理使用流水线可以提高系统时钟频率
- 共享算术运算单元可以减少面积
4.5 实战项目:串并转换器
结合前面学到的知识,实现一个8位串行输入到32位并行输出的转换器:
verilog复制module serial_to_parallel(
input clk,
input reset,
input en,
input [7:0] datain,
output reg dout_en,
output reg [31:0] dataout
);
reg [1:0] count;
reg [23:0] buffer;
always @(posedge clk or posedge reset) begin
if(reset) begin
count <= 2'b0;
buffer <= 24'b0;
dout_en <= 1'b0;
dataout <= 32'b0;
end else begin
dout_en <= 1'b0;
if(en) begin
if(count == 2'b11) begin
// 第4个字节到达,输出32位数据
dataout <= {buffer, datain};
dout_en <= 1'b1;
count <= 2'b0;
buffer <= 24'b0;
end else begin
// 存储前3个字节
buffer <= {buffer[15:0], datain};
count <= count + 1'b1;
end
end
end
end
endmodule
这个设计的关键点包括:
- 使用计数器跟踪接收到的字节数
- 使用缓冲区存储部分数据
- 在接收到第4个字节时输出完整32位数据
- 使用使能信号控制数据有效性
在实际项目中,可能还需要添加错误检测、数据对齐等功能,但这个基本框架已经展示了Verilog设计的核心思想。
