Verilog中等难度部分设计实践(含ALU算术逻辑单元的设计)
verilog的中等部分根据Deepseek给出的大纲理应包括时序逻辑verilog设计实践和组合逻辑verilog组合设计实践两个部分。
时序逻辑verilog设计和实践
在 Verilog 中,时序逻辑通常通过 always
块实现,并依赖时钟信号和复位信号。
关键点如下:
- 敏感列表:必须包含时钟(如
posedge clk
)和可能的复位信号。 - 非阻塞赋值(
<=
):时序逻辑中使用<=
避免竞争条件,确保并行更新。 - 复位策略:
- 同步复位:复位信号仅在时钟边沿生效。
- 异步复位:复位信号立即生效(如
posedge rst
)。
时序逻辑的设计要点如下:
时序逻辑设计要点
(1) 时钟域(Clock Domain)
- 所有时序逻辑需明确归属某个时钟域。
- 避免跨时钟域直接传递信号(需同步器处理)。
(2) 复位策略
- 同步复位:更安全,但依赖时钟。
- 异步复位:立即生效,但可能产生毛刺。
(3) 时序约束
- 需在综合工具中指定时钟频率、建立时间(Setup Time)和保持时间(Hold Time)。
- 使用
#
延迟在仿真中模拟时序行为(仅用于验证)。
时序逻辑的设计实践与验证
寄存器的设计与验证
下面的时序逻辑实践中实现了一个数字寄存器的设计和验证:
首先是register.v中的代码:
// 文件名: register.v
module register(
input clk,
input rst,
input [7:0] d,
output reg [7:0] q
);
always @(posedge clk or posedge rst)begin
if (rst)begin
q<=8'h00;
end else begin
q<=d;
end
end
endmodule
其次是tb_register.v中的代码:
// 文件名: tb_register.v
`timescale 1ns/1ns // 定义仿真时间单位
module tb_register;
// 定义信号
reg clk;
reg rst;
reg [7:0] d;
wire [7:0] q;
// 实例化被测模块
register u_register (
.clk(clk),
.rst(rst),
.d(d),
.q(q)
);
// 生成时钟信号(周期10ns)
initial begin
clk = 0;
forever #5 clk = ~clk; // 每5ns翻转一次,形成50MHz时钟
end
// 生成测试激励
initial begin
// 初始化信号
rst = 0;
d = 8'h00;
// 测试步骤1:复位测试
rst = 1; // 复位有效
#10; // 等待10ns(1个时钟周期)
rst = 0; // 复位释放
d = 8'hAA; // 输入数据变化
// 测试步骤2:正常数据传输
#10; // 等待时钟上升沿
d = 8'h55; // 输入新数据
// 测试步骤3:继续观察
#10;
d = 8'hFF;
// 仿真结束
#20 $finish;
end
// 监控信号变化
initial begin
$monitor("Time=%t: rst=%b, d=0x%h, q=0x%h", $time, rst, d, q);
// 生成波形文件(用于Verdi/ModelSim等工具)
$dumpfile("wave.vcd");
$dumpvars(0, tb_register);
end
endmodule
验证的结果如下:
实现的寄存器原理是,在时钟的上升沿的时候,假如rst信号为1则使得q=8’h00(一个十六进制等于四位的二进制),否则为q=d.q是输出量,d是输入量。在实际测试所得的波形图中也确实满足。在WSL+iverilog测试环境中的命令如下:
iverilog -o register.out register.v tb_register.v
vvp register.out
gtkwave wave.vcd
计数器的设计与验证
实现的具体代码和test_bench代码分别如下:
// 文件名: counter.v
module counter(
input clk,
input rst_n,
input en,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n)begin
if (!rst_n)begin
count<=4'b0000;
end else if (en )begin
count<=count+1;
end
end
endmodule
/*
实现的功能是设计了一个计数器,假如rst_n为0,那么count为0,否则如果en为1,那么count会一直加1,直到count为15,然后count会重新从0开始计数。
*/
// 文件名: tb_counter.v
`timescale 1ns/1ns
module tb_counter;
// 定义信号
reg clk;
reg rst_n;
reg en;
wire [3:0] count;
// 实例化被测模块
counter u_counter (
.clk(clk),
.rst_n(rst_n),
.en(en),
.count(count)
);
// 生成时钟信号(周期20ns)
initial begin
clk = 0;
forever #10 clk = ~clk; // 50MHz时钟
end
// 生成测试激励
initial begin
// 初始化信号
rst_n = 1;
en = 0;
// 测试步骤1:复位测试
rst_n = 0; // 复位有效(低电平)
#20;
rst_n = 1; // 复位释放
en = 1; // 使能计数
// 测试步骤2:正常计数(观察从0到15)
#320; // 等待16个时钟周期(16*20ns=320ns)
// 测试步骤3:关闭使能,观察是否停止计数
en = 0;
#40;
// 测试步骤4:重新开启使能
en = 1;
#40;
// 仿真结束
#20 $finish;
end
// 监控信号变化
initial begin
$monitor("Time=%t: rst_n=%b, en=%b, count=%d", $time, rst_n, en, count);
// 生成波形文件
$dumpfile("wave.vcd");
$dumpvars(0, tb_counter);
end
endmodule
实测发现这是比hspice好用很多的硬件解释语言,假如用hspice来写一个计数器不知道要写多少个晶体管。当然了二者 是不同层级的设计工具。
验证结果如下所示:
编译与仿真和波形观察命令如下:
iverilog -o counter.out counter.v tb_counter.v
vvp counter.out
gtkwave wave.vcd
组合逻辑设计与实践
Verilog中的组合逻辑是实现数字电路的基础,其核心特点是输出仅由当前输入决定,无记忆功能。verilog组合逻辑实现的方法有assign 连续赋值实现和always逻辑块实现。多说无益,下面是几个例子。
四选一多路选择器的设计和验证
module mux_4to1 (
input [3:0] data_in, // 4位输入信号:data_in[3], data_in[2], ..., data_in[0]
input [1:0] sel, // 2位选择信号
output reg out // 输出信号
);
// 组合逻辑:根据 sel 选择对应的输入位
always @(*) begin
case (sel)//根据sel作case分支选择
2'b00: out = data_in[0]; // sel=00 时选择第0位
2'b01: out = data_in[1]; // sel=01 时选择第1位
2'b10: out = data_in[2]; // sel=10 时选择第2位
2'b11: out = data_in[3]; // sel=11 时选择第3位
default: out = 1'bx; // 处理未定义的 sel 值(避免锁存器)
endcase
end
endmodule
//命名为mux_4to1.v
`timescale 1ns/1ns // 定义仿真时间单位
module tb_mux_4to1();
// 定义测试信号
reg [3:0] data_in; // 输入数据
reg [1:0] sel; // 选择信号
wire out; // 输出信号
// 实例化被测模块
mux_4to1 u_mux (
.data_in(data_in),
.sel(sel),
.out(out)
);
// 生成测试激励
initial begin
// 初始化信号
data_in = 4'b0000;
sel = 2'b00;
// 遍历所有可能的 sel 值,并测试不同输入
// 测试用例 1:data_in = 4'b1010
data_in = 4'b1010;
sel = 2'b00; #10; // 等待10ns,预期 out = data_in[0] = 0
sel = 2'b01; #10; // 预期 out = data_in[1] = 1
sel = 2'b10; #10; // 预期 out = data_in[2] = 0
sel = 2'b11; #10; // 预期 out = data_in[3] = 1
// 测试用例 2:data_in = 4'b0101
data_in = 4'b0101;
sel = 2'b00; #10; // 预期 out = 1
sel = 2'b01; #10; // 预期 out = 0
sel = 2'b10; #10; // 预期 out = 1
sel = 2'b11; #10; // 预期 out = 0
// 结束仿真
$display("Simulation completed!");
$finish;
end
// 实时打印信号变化(可选)
initial begin
$monitor("Time = %t | sel = %b | data_in = %b | out = %b",
$time, sel, data_in, out);
end
endmodule
//命名为tb_mux_4to1.v
仿真所得结果如下,验证成功多路4选1功能,根据输入的sel值输出选择后的值。
Time = 0 | sel = 00 | data_in = 1010 | out = 0
Time = 10 | sel = 01 | data_in = 1010 | out = 1
Time = 20 | sel = 10 | data_in = 1010 | out = 0
Time = 30 | sel = 11 | data_in = 1010 | out = 1
Time = 40 | sel = 00 | data_in = 0101 | out = 1
Time = 50 | sel = 01 | data_in = 0101 | out = 0
Time = 60 | sel = 10 | data_in = 0101 | out = 1
Time = 70 | sel = 11 | data_in = 0101 | out = 0
Simulation completed!
编译仿真命令如下:
iverilog -o mux.out mux_4to1.v tb_mux_4to1.v
vvp mux.out
ALU模块的设计与验证
首先需要知道什么是ALU模块,ALU模块(Arithmetic Logic Unit,算术逻辑单元)是计算机CPU的核心部件之一,负责执行所有算术运算(如加减乘除)和逻辑运算(如与、或、非、移位等)。它是CPU的“计算大脑”,直接影响处理器的性能。
ALU的核心功能
运算类型 | 典型操作 | 示例 |
---|---|---|
算术运算 | 加法、减法、乘法、增量等 | A + B 、A - 1 |
逻辑运算 | 与、或、非、异或、移位等 | A & B 、A << 1 |
比较运算 | 相等判断、大小比较 | A == B 、A > B |
ALU的组成结构
- 算术单元
- 加法器、减法器、乘法器等
- 支持带进位(
Carry
)的运算
- 逻辑单元
- 与门(AND)、或门(OR)、异或门(XOR)等
- 移位器(左移/右移)
- 标志寄存器
- Zero(零标志):运算结果是否为0
- Carry(进位标志):加法溢出或减法借位
- Overflow(溢出标志):有符号运算溢出
- Sign(符号标志):结果的最高位(正负)
ALU的工作流程
- 输入操作数:从寄存器或内存中读取数据(如
A
和B
)。 - 接收控制信号:由控制单元(Control Unit)指定操作类型(如
ADD
或AND
)。 - 执行运算:根据操作码计算结果。
- 输出结果与标志位:将结果写回寄存器/内存,并更新标志位。
在CPU架构中可见算术逻辑单元处于核心位置,或者可以说算术逻辑单元本身就是计算机的核心构件。
ALU的verilog代码和ALU_tb的verilog代码如下所示:
module ALU (
input [3:0] A, // 操作数A
input [3:0] B, // 操作数B
input [2:0] opcode, // 操作码(定义见下方)
input cin, // 进位输入(用于加减法)
output reg [3:0] F, // 结果输出
output reg cout, // 进位/借位标志
output zero, // 零标志(F=0时置1)
output overflow, // 溢出标志(有符号运算溢出)
output sign // 符号位(F的最高位)
);
// 操作码定义
parameter
OP_ADD = 3'b000, // A + B + cin
OP_SUB = 3'b001, // A - B - cin
OP_AND = 3'b010, // A & B
OP_OR = 3'b011, // A | B
OP_XOR = 3'b100, // A ^ B
OP_NOT = 3'b101, // ~A
OP_SHL = 3'b110, // 逻辑左移(A << 1,最低位补0)
OP_SHR = 3'b111; // 逻辑右移(A >> 1,最高位补0)
// 中间信号
wire [4:0] add_result; // 加法器结果(包含进位)
wire [4:0] sub_result; // 减法器结果(包含借位)
// 零标志(组合逻辑)
assign zero = (F == 4'b0000);
// 符号位(直接取最高位)
assign sign = F[3];
// 溢出标志(有符号加减法溢出判断)
assign overflow = (opcode == OP_ADD) ? (A[3] & B[3] & ~F[3]) | (~A[3] & ~B[3] & F[3]) :
(opcode == OP_SUB) ? (A[3] & ~B[3] & ~F[3]) | (~A[3] & B[3] & F[3]) : 1'b0;
// 加法器(A + B + cin)
assign add_result = A + B + cin;
// 减法器(A - B - cin,转换为补码加法)
assign sub_result = A - B - cin;
// ALU核心逻辑
always @(*) begin
case (opcode)//根据设定的opcode来执行指令,opcode包括与,或等逻辑运算和加减等算术运算
OP_ADD: begin
F = add_result[3:0];
cout = add_result[4];
end
OP_SUB: begin
F = sub_result[3:0];
cout = sub_result[4]; // 借位标志(实际为~cout)
end
OP_AND: {cout, F} = {1'b0, A & B};
OP_OR: {cout, F} = {1'b0, A | B};
OP_XOR: {cout, F} = {1'b0, A ^ B};
OP_NOT: {cout, F} = {1'b0, ~A};
OP_SHL: {cout, F} = {A[3], A[2:0], 1'b0}; // 左移,cout为移出的最高位
OP_SHR: {cout, F} = {A[0], 1'b0, A[3:1]}; // 右移,cout为移出的最低位
default: {cout, F} = {1'b0, 4'b0000};
endcase
end
endmodule
module ALU_tb;
// 输入信号
reg [3:0] A;
reg [3:0] B;
reg [2:0] opcode;
reg cin;
// 输出信号
wire [3:0] F;
wire cout, zero, overflow, sign;
parameter
OP_ADD = 3'b000,
OP_SUB = 3'b001,
OP_AND = 3'b010,
OP_OR = 3'b011,
OP_XOR = 3'b100,
OP_NOT = 3'b101,
OP_SHL = 3'b110,
OP_SHR = 3'b111;
// 实例化ALU
ALU uut (
.A(A),
.B(B),
.opcode(opcode),
.cin(cin),
.F(F),
.cout(cout),
.zero(zero),
.overflow(overflow),
.sign(sign)
);
// 测试用例
initial begin
// 初始化输入
A = 4'b0000;
B = 4'b0000;
opcode = 3'b000;
cin = 0;
// 测试加法:5 + 3 + 0 = 8(无进位)
#10 A = 4'b0101; B = 4'b0011; opcode = OP_ADD; cin = 0;
#10 $display("ADD: 5 + 3 = %d, Cout=%b, OF=%b", F, cout, overflow);
// 测试带进位加法:5 + 3 + 1 = 9(无溢出)
#10 cin = 1;
#10 $display("ADD+C: 5 + 3 +1 = %d, Cout=%b", F, cout);
// 测试减法:8 - 3 - 0 = 5
#10 A = 4'b1000; B = 4'b0011; opcode = OP_SUB; cin = 0;
#10 $display("SUB: 8 -3 = %d, Cout=%b", F, cout);
// 测试溢出:7 + 7 = 14(有符号溢出)
#10 A = 4'b0111; B = 4'b0111; opcode = OP_ADD; cin = 0;
#10 $display("ADD Overflow: F=%b, OF=%b", F, overflow);
// 测试逻辑操作:A=1010, B=1100
#10 A = 4'b1010; B = 4'b1100;
#10 opcode = OP_AND; #10 $display("AND: %b & %b = %b", A, B, F);
#10 opcode = OP_OR; #10 $display("OR : %b | %b = %b", A, B, F);
#10 opcode = OP_XOR; #10 $display("XOR: %b ^ %b = %b", A, B, F);
#10 opcode = OP_NOT; #10 $display("NOT: ~%b = %b", A, F);
// 测试移位
#10 A = 4'b1011;
#10 opcode = OP_SHL; #10 $display("SHL: 1011 << 1 = %b, Cout=%b", F, cout);
#10 opcode = OP_SHR; #10 $display("SHR: 1011 >> 1 = %b, Cout=%b", F, cout);
// 结束仿真
#10 $finish;
end
// 注意:OP_ADD等参数需在测试平台中重新声明或通过`include引入
// 此处简化处理,实际需补充参数定义
endmodule
验证结果如下:
ADD: 5 + 3 = 8, Cout=0, OF=1
ADD+C: 5 + 3 +1 = 9, Cout=0
SUB: 8 -3 = 5, Cout=0
ADD Overflow: F=1110, OF=1
AND: 1010 & 1100 = 1000
OR : 1010 | 1100 = 1110
XOR: 1010 ^ 1100 = 0110
NOT: ~1010 = 0101
SHL: 1011 << 1 = 0110, Cout=1
SHR: 1011 >> 1 = 0101, Cout=1
编译和仿真命令如下所示:
iverilog -o ALU.out ALU.v ALU_tb.v
vvp ALU.out
我其实本人是比较好奇现代数字系统设计的流程的,经过查询得知在verilog代码写好后并综合后是有专门的工具可以将其映射到物理版图构造并制作的。
大体流程如下:
1. 逻辑综合(Synthesis)
-
工具作用
:综合工具(如Synopsys Design Compiler)将Verilog代码转换为门级网表(Gate-level Netlist)。
- 例如,
assign F = A + B
可能被转换为4位行波进位加法器(Ripple Carry Adder)的AND/OR门组合。
- 例如,
-
优化目标:根据约束(速度、面积、功耗)优化电路结构。
2. 布局布线(Place & Route)
-
物理实现
:将门级网表映射到具体的硬件单元(如FPGA的查找表LUT或ASIC的标准单元库)。
- FPGA:配置可编程逻辑块(CLB)和连线资源。
- ASIC:生成掩膜版图,定义晶体管位置和金属层连线。
3. 硬件生成
- FPGA:通过比特流(Bitstream)文件配置芯片。
- ASIC:经过光刻、蚀刻等半导体工艺制造。