第5章 硬件描述语言(用代码描述电路)
一、导读
5.1 本章学习目标
理解硬件描述语言(HDL)的基本概念,认识HDL程序与软件程序的根本区别
掌握Verilog HDL的基本语法结构,包括模块定义、端口声明和数据类型
学会使用assign语句描述组合逻辑,使用always块描述时序逻辑
理解阻塞赋值(=)与非阻塞赋值(<=)的区别及正确使用方法
掌握模块化设计方法,包括层次化实例化和参数传递
学会编写测试平台(Testbench)进行功能仿真验证
了解Verilog与SystemVerilog的演进关系
能够用HDL实现前几章学到的组合逻辑和时序逻辑电路
5.2 为什么本章如此重要
在现代数字设计中,几乎没有人再用手工绘制逻辑门的方式来设计电路了。硬件描述语言(HDL)已经成为数字电路设计的标准工具。HDL允许工程师用文本代码的方式描述电路的结构和行为,然后通过EDA工具自动完成逻辑综合、布局布线和仿真验证。这种基于HDL的设计方法极大地提高了设计效率和可维护性。
HDL的核心价值在于它将"描述"与"实现"分离开来。设计者只需描述电路的功能和结构,具体的物理实现由综合工具完成。这使得设计者可以专注于系统架构和算法层面的决策,而不必陷入底层电路细节。同时,HDL代码的可移植性意味着同一份设计描述可以针对不同工艺库进行综合,生成不同技术节点的物理电路。
更重要的是,HDL使得设计的验证和复用成为可能。通过编写测试平台,可以在制造芯片之前发现设计中的错误,大幅降低开发成本和风险。模块化的HDL代码可以在不同项目中重复使用,积累形成IP库。掌握HDL是进入现代数字设计领域的必备技能,也是理解后续处理器设计章节的前提。
5.3 核心问题引导
HDL程序与C语言等软件程序有什么本质区别?为什么不能用软件编程的思维来写HDL?
Verilog中的wire和reg有什么区别?它们分别对应什么硬件结构?
assign语句和always块各自适合描述什么类型的电路?
阻塞赋值和非阻塞赋值在语义上有什么不同?为什么时序逻辑必须使用非阻塞赋值?
如何实现参数化的模块设计?parameter在实际工程中有什么作用?
测试平台的作用是什么?如何生成时钟信号和激励信号?
行为级描述和结构级描述有什么区别?各自适用于什么场景?
二、核心概念详解
5.1 Verilog模块与端口
Verilog的基本设计单元是模块(Module)。一个模块描述了一个数字电路的接口和内部实现。模块定义包括模块名、端口列表和模块体。端口分为三种类型:input(输入)、output(输出)和inout(双向)。在Verilog-2001标准中,端口声明和数据类型声明可以合并书写,使代码更加简洁。
module mux2to1 (
input wire a,
input wire b,
input wire sel,
output wire y
);
assign y = sel ? a : b;
endmodule模块内部可以使用两种描述方式:数据流描述(通过assign语句)和行为描述(通过always块)。assign语句用于描述组合逻辑,表示输出信号与输入信号之间的连续关系。always块用于描述在特定条件下执行的操作,可以描述组合逻辑和时序逻辑。
Verilog中的数据类型主要有两种:wire和reg。wire类型表示物理连线,其值由驱动它的源(如assign语句或模块输出)持续决定。reg类型表示变量,其值在assign或always块中被赋值时保持不变,直到下一次赋值。需要注意的是,reg类型并不一定对应硬件中的寄存器(Flip-Flop)——它只是Verilog中的一种数据类型。一个always块中的组合逻辑赋值使用的reg变量,综合后可能只是连线。
5.2 组合逻辑的HDL描述
组合逻辑可以用assign语句或always块来描述。assign语句适合简单的数据流描述,如算术运算、逻辑运算和多路选择:
assign sum = a + b;
assign y = (sel & a) | (~sel & b);
assign eq = (a == b);always块适合使用if-else和case语句描述更复杂的组合逻辑。描述组合逻辑的always块需要敏感列表包含所有输入信号(Verilog-2001中使用*自动推断):
always @(*) begin
if (sel)
y = a;
else
y = b;
end使用always块描述组合逻辑时,必须确保所有可能的输入组合都有对应的输出赋值。如果if-else或case语句没有覆盖所有情况,综合工具会推断出锁存器(Latch),这通常不是设计者的本意。解决方法是为输出指定默认值,或者确保if-else和case语句完整覆盖所有分支。
case语句在描述多路选择器和译码器时特别有用:
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
default: y = 1'b0;
endcase
end5.3 时序逻辑的HDL描述
时序逻辑使用always块描述,敏感列表为时钟边沿(如posedge clk)。D触发器的Verilog描述如下:
always @(posedge clk) begin
q <= d;
end带异步复位的D触发器:
always @(posedge clk or posedge rst) begin
if (rst)
q <= 1'b0;
else
q <= d;
end带同步使能的寄存器:
always @(posedge clk) begin
if (en)
q <= d;
end时序逻辑中必须使用非阻塞赋值(<=)。非阻塞赋值的语义是:在always块执行时,所有非阻塞赋值语句的右侧表达式先被计算,然后在always块结束时同时更新左侧变量。这模拟了硬件中所有触发器在同一时钟边沿同时更新的行为。
阻塞赋值(=)的语义是立即更新:语句执行完毕后,左侧变量立即变为新值。如果在时序逻辑中使用阻塞赋值,可能导致赋值顺序影响结果,产生与硬件行为不一致的仿真结果。因此,一个重要的设计规则是:时序逻辑使用非阻塞赋值,组合逻辑使用阻塞赋值。
5.4 模块化设计与层次化实例化
模块化设计是HDL的核心思想。复杂系统被分解为多个功能模块,每个模块独立编写和测试,然后通过层次化实例化组合成完整系统。模块实例化有两种方式:按位置关联和按名称关联。
按名称关联(推荐):
mux2to1 u_mux (
.a(data_a),
.b(data_b),
.sel(selector),
.y(result)
);按位置关联:
mux2to1 u_mux (data_a, data_b, selector, result);按名称关联更加清晰且不易出错,是推荐的实例化方式。
参数化模块是模块化设计的重要特性。通过parameter关键字,可以创建可配置的模块:
module adder #(parameter WIDTH = 8) (
input wire [WIDTH-1:0] a, b,
output wire [WIDTH:0] sum
);
assign sum = a + b;
endmodule实例化时可以覆盖参数值:
adder #(.WIDTH(32)) u_add32 (.a(x), .b(y), .sum(z));参数化设计使得同一个模块可以在不同位宽和配置下复用,极大地提高了代码的可重用性。
5.5 测试平台与仿真验证
测试平台(Testbench)是用于验证设计正确性的HDL代码。测试平台不需要综合为硬件,它的作用是生成激励信号、施加到被测设计(DUT,Design Under Test)的输入端,并观察输出是否符合预期。
一个基本的测试平台结构如下:
module tb_mux2to1;
reg a, b, sel;
wire y;
mux2to1 dut (.a(a), .b(b), .sel(sel), .y(y));
initial begin
a = 0; b = 0; sel = 0;
#10 a = 1;
#10 sel = 1;
#10 b = 1;
#10 $finish;
end
initial begin
$monitor("Time=%0t a=%b b=%b sel=%b y=%b",
$time, a, b, sel, y);
end
endmodule时钟信号生成:
initial clk = 0;
always #5 clk = ~clk;测试平台应该覆盖所有可能的输入组合和边界条件。对于复杂的 designs,可以使用任务(Task)和函数(Function)来组织测试代码,使用文件读取($readmemh)加载测试向量,使用波形显示($dumpfile/$dumpvars)记录仿真结果。
三、深入分析与讨论
5.1 行为级描述与结构级描述
HDL支持多个抽象层次的描述。行为级(Behavioral Level)描述关注电路"做什么",而不关心"怎么做"。例如,assign y = a + b只描述了加法功能,不指定使用什么类型的加法器电路。行为级描述简洁高效,适合算法探索和高层设计。
结构级(Structural Level)描述关注电路"怎么连接",通过实例化和连接已有模块来构建系统。例如,用全加器模块级联构成行波进位加法器。结构级描述更接近实际硬件,适合底层实现和精确控制。
RTL(Register Transfer Level)是介于行为级和门级之间的抽象层次,它用寄存器之间的数据传送和运算来描述电路。RTL是综合工具能够处理的最常用抽象层次,也是工业界最广泛使用的HDL描述层次。在RTL级,设计者描述数据在寄存器之间的流动和变换,综合工具将其转化为门级网表。
5.2 HDL设计中的可综合性问题
并非所有合法的Verilog代码都可以被综合为硬件。可综合性(Synthesizability)是HDL设计中的一个重要概念。一些常见的不可综合结构包括:initial块(仅用于仿真)、#延迟语句(硬件中无法精确控制延迟)、$display等系统任务、以及某些复杂的动态结构。
初学者常犯的一个错误是在HDL代码中混入不可综合的仿真语句。另一个常见错误是用软件编程的思维写HDL——例如使用for循环来"遍历"数据,但实际上HDL中的for循环在综合时会被展开为并行硬件结构,而不是软件中的顺序执行。理解HDL代码与硬件结构之间的对应关系,是写好可综合HDL代码的关键。
四、实践案例
5.1 案例一:用Verilog实现4位ALU
module alu_4bit #(parameter WIDTH = 4) (
input wire [WIDTH-1:0] a, b,
input wire [2:0] op,
output reg [WIDTH-1:0] y,
output wire z
);
assign z = (y == 0);
always @(*) begin
case (op)
3'b000: y = a + b;
3'b001: y = a - b;
3'b010: y = a & b;
3'b011: y = a | b;
3'b100: y = a ^ b;
3'b101: y = (a < b);
3'b110: y = ~(a | b);
3'b111: y = ~(a & b);
default: y = 0;
endcase
end
endmodule这个案例展示了参数化模块、case语句和组合逻辑always块的用法。ALU的位宽通过parameter配置,操作选择通过case语句实现,零标志通过连续赋值语句产生。
5.2 案例二:用Verilog实现序列检测器
module seq_detect (
input wire clk, rst, x,
output reg z
);
reg [1:0] state, next_state;
localparam S0 = 2'b00, S1 = 2'b01,
S2 = 2'b10, S3 = 2'b11;
always @(posedge clk or posedge rst) begin
if (rst)
state <= S0;
else
state <= next_state;
end
always @(*) begin
next_state = state;
z = 1'b0;
case (state)
S0: if (x) next_state = S1;
S1: if (x) next_state = S2;
else next_state = S0;
S2: if (~x) next_state = S3;
S3: begin
if (x) begin
next_state = S1;
z = 1'b1;
end else
next_state = S0;
end
endcase
end
endmodule这个案例展示了Mealy型FSM的Verilog实现。两个always块分别描述状态寄存器(时序逻辑)和下一状态/输出逻辑(组合逻辑)。使用localparam定义状态常量,提高代码可读性。
五、常见误区与注意事项
5.1 误区一:用软件编程思维写HDL
HDL描述的是硬件结构,不是软件算法。a = b + c在C语言中意味着"计算b+c并将结果存入变量a",但在Verilog中意味着"a和b+c之间有一根物理连线"。HDL中的并发执行(多个always块同时工作)与软件的顺序执行有本质区别。理解"代码即硬件"的思维方式是学习HDL的最大挑战。
5.2 误区二:在时序逻辑中使用阻塞赋值
在always @(posedge clk)块中使用阻塞赋值(=)会导致赋值顺序影响结果。例如:
always @(posedge clk) begin
a = b;
c = a;
end这里c获得的是b的新值(因为a已经被更新),而不是a的旧值。如果使用非阻塞赋值(<=),a和c会在同一时刻更新,c获得a的旧值。后者才正确反映了硬件中触发器的并行更新行为。
5.3 误区三:组合逻辑always块中遗漏分支
如果if-else或case语句没有覆盖所有输入组合,综合工具会推断出锁存器。例如:
always @(*) begin
if (sel)
y = a;
end当sel=0时,y没有赋值,综合工具会认为y需要保持之前的值,从而生成锁存器。解决方法是添加else分支或为y指定默认值:always @(*) begin y = 0; if (sel) y = a; end。
5.4 误区四:忽视初始状态
FPGA中的触发器在上电时处于随机状态,必须通过复位信号初始化。ASIC中某些工艺库可能支持初始值,但依赖初始值的可移植性很差。良好的HDL设计应该总是包含复位逻辑,确保系统从确定的初始状态开始工作。
六、本章小结
本章系统介绍了硬件描述语言的基本概念和使用方法。我们学习了Verilog的模块结构、端口声明和数据类型,掌握了用assign和always块描述组合逻辑与时序逻辑的方法。理解了阻塞赋值与非阻塞赋值的关键区别,学习了模块化设计和参数化模块的实现方式。测试平台的编写方法使得我们能够在仿真环境中验证设计的正确性。HDL是现代数字设计的标准工具,熟练掌握它将使你能够高效地设计和验证复杂的数字系统。
七、思考题与练习
用Verilog实现一个8位多路选择器(8选1),使用case语句描述。编写完整的测试平台验证所有输入组合。
解释为什么在时序逻辑的always块中必须使用非阻塞赋值。给出一个使用阻塞赋值导致错误结果的例子。
用Verilog实现一个带同步复位的4位计数器,计数范围0-15,到达15后回绕到0。
设计一个参数化的N位寄存器(N默认为32),带使能和异步复位功能。编写测试平台验证其功能。
用结构级描述方式,使用全加器模块实例化实现一个4位行波进位加法器。
分析以下代码综合后的硬件结构,画出对应的电路图:
module mystery(input a, b, c, output reg y);
always @(*) begin
case({a,b})
2'b00: y = c;
2'b01: y = ~c;
2'b10: y = 1'b1;
2'b11: y = 1'b0;
endcase
end
endmodule编写一个测试平台,自动生成随机激励信号并检查DUT输出是否符合预期(使用assert或条件判断)。
比较Verilog和VHDL在语法特性、设计哲学和应用领域方面的主要区别。