Verilog编程规范、示例
Verilog编程规范的详细介绍:
一、工程组织形式
工程的组织形式一般包括doc、par、rtl和sim四个部分:
- doc:存放工程相关的文档,包括该项目用到的数据手册(datasheet)、设计方案等。
- prj:存放工程文件和使用到的一些IP文件。
- rtl:存放工程的rtl代码,这是工程的核心,文件名与module名称应当一致,建议按照模块的层次分开存放。
- sim:存放工程的仿真代码,复杂的工程里仿真不可或缺,可以极大减少调试的工作量。
二、文件头声明
每个Verilog文件的开头,都必须有一段声明的文字,包括文件的版权、作者、创建日期以及内容介绍等。
三、输入输出定义
input
:定义模块的输入信号,可以是wire
类型或其他类型,表示外部输入的信号。output
:定义模块的输出信号,通常是wire
或reg
类型,表示模块输出到外部的信号。inout
:用于双向信号,可以同时作为输入和输出,根据外部控制信号的状态决定其行为。
示例1:逻辑功能:AND门
module and_gate (
input wire a, // 输入信号 a
input wire b, // 输入信号 b
output wire y // 输出信号 y
);
// 逻辑功能:AND 门
assign y = a & b; // y = a AND b
endmodule
说明:
input wire a, b
:声明了两个输入端口,类型为wire
,表示这两个信号是连接外部电路的信号。output wire y
:声明了一个输出端口,类型为wire
,表示它是一个由模块输出到外部电路的信号。
示例2:带时序逻辑的模块
module d_flip_flop (
input wire clk, // 时钟信号
input wire reset, // 异步复位信号
input wire d, // 数据输入
output reg q // 输出,使用 reg 类型,因为需要存储状态
);
// 异步复位,并在时钟的上升沿更新输出
always @(posedge clk or posedge reset) begin
if (reset) begin
q <= 0; // 复位时输出为 0
end else begin
q <= d; // 时钟信号有效时,输出跟随输入 d
end
end
endmodule
-
输入端口(input):
clk
、reset
、d
都是输入端口,分别代表时钟信号、复位信号和数据输入。
-
输出端口(output):
q
是一个时序输出,类型是reg
,因为它存储了时钟信号控制下的状态值。
示例3:加法
module adder (
input wire [3:0] a, // 4位输入端口 a
input wire [3:0] b, // 4位输入端口 b
output wire [3:0] sum, // 4位输出端口 sum
output wire carry_out // 输出进位信号
);
assign {carry_out, sum} = a + b; // 执行加法操作并将进位和和输出
endmodule
-
输入端口(input):
a
和b
是4位宽度的输入端口,表示两个4位二进制数。
-
输出端口(output):
sum
是一个4位宽度的输出端口,表示两个输入相加的结果。carry_out
是一个单比特的输出端口,表示加法操作的进位输出。
示例4:双向端口inout
module bidirectional_example (
inout wire data, // 双向端口,数据线
input wire enable // 启用信号
);
// 当 enable 为 1 时,data 输出;当 enable 为 0 时,data 输入
assign data = (enable) ? 1'b1 : 1'bz; // 1'bz 表示高阻态
endmodule
inout
端口:data
是一个双向端口,可以在不同的条件下作为输入或输出使用。它的信号状态是三态(z
)或由assign
语句控制。
示例5:乘法
module multiplier (
input wire [7:0] a, // 8位输入端口 a
input wire [7:0] b, // 8位输入端口 b
output wire [15:0] product // 16位输出端口
);
assign product = a * b; // 执行乘法操作
endmodule
-
输入端口(input):
a
和b
都是8位宽度的输入端口,表示两个8位的二进制数。
-
输出端口(output):
product
是一个16位宽度的输出端口,用来存储a
和b
相乘的结果。
四、parameter定义
parameter定义一个常量值,建议全部字母大写,并用来定义有实际意义的常数,如单位延时、版本号等。
示例1:定义常量宽度
module register (
input wire clk,
input wire reset,
input wire [WIDTH-1:0] data_in, // 输入数据,宽度由 parameter 决定
output reg [WIDTH-1:0] data_out // 输出数据,宽度由 parameter 决定
);
// 定义 parameter,用于指定寄存器宽度
parameter WIDTH = 8; // 默认宽度为 8 位
always @(posedge clk or posedge reset) begin
if (reset)
data_out <= 0;
else
data_out <= data_in;
end
endmodule
parameter WIDTH = 8
:定义了一个名为WIDTH
的参数,默认值为 8。这个参数决定了data_in
和data_out
的位宽。- 如果在实例化时没有传递
WIDTH
参数,默认值8
将会被使用。
示例2:实例化 register
模块传递parameter值
module top_module (
input wire clk,
input wire reset,
input wire [15:0] data_in_16, // 16位输入
input wire [31:0] data_in_32, // 32位输入
output wire [15:0] data_out_16,
output wire [31:0] data_out_32
);
// 实例化 register 模块,使用不同的 WIDTH 参数
register #(.WIDTH(16)) reg_16 (
.clk(clk),
.reset(reset),
.data_in(data_in_16),
.data_out(data_out_16)
);
register #(.WIDTH(32)) reg_32 (
.clk(clk),
.reset(reset),
.data_in(data_in_32),
.data_out(data_out_32)
);
register #(.WIDTH(16))
:在实例化register
模块时,通过#(.WIDTH(16))
为WIDTH
参数指定了 16,这样data_in
和data_out
的宽度将是 16 位。register #(.WIDTH(32))
:在另一个实例化中,指定WIDTH
为 32,从而使得data_in
和data_out
的宽度为 32 位。
五、wire/reg定义
- wire:代表逻辑单元的物理连线,用于描述信号连接,无存储功能,需要外部驱动源。
- reg:一种存储型变量,能保持数据,常用于行为级描述。在过程语句中,被赋值信号通常定义为reg类型。
特性 | wire | reg |
---|---|---|
用途 | 用于连接信号(组合逻辑的输入输出) | 用于存储值(时序逻辑、触发器) |
驱动方式 | 由其他信号(如 assign 或模块输出)驱动 | 由 always 块或时钟边沿驱动 |
更新时机 | 由外部模块或逻辑驱动,不能在 always 中更新 | 可以在 always 块中进行赋值更新 |
适用场景 | 组合逻辑电路、模块间的信号传递 | 寄存器、触发器、时序逻辑 |
赋值方式 | 使用 assign 语句赋值 | 使用 = 或 <= 赋值 |
示例1:使用wire连接多个模块的输入和输出
module and_gate (
input wire a, // 输入信号
input wire b, // 输入信号
output wire y // 输出信号
);
assign y = a & b; // 计算 a 和 b 的与操作
endmodule
module top_module (
input wire a, // 输入信号
input wire b, // 输入信号
output wire result // 输出信号
);
wire and_output; // 定义一个 wire 用于连接 AND 门的输出
// 实例化 and_gate 模块
and_gate u1 (
.a(a), // 连接输入信号 a
.b(b), // 连接输入信号 b
.y(and_output) // 将 AND 门输出连接到 wire and_output
);
// 将 and_output 传递给最终的结果
assign result = and_output;
endmodule
- 在上述例子中,
wire and_output;
定义了一个wire
类型的信号,它用于连接and_gate
模块的输出。 wire
信号必须由某个逻辑源(如assign
语句、组合逻辑门等)驱动。
示例2:使用reg存储值
module flip_flop (
input wire clk, // 时钟信号
input wire reset, // 复位信号
input wire d, // 数据输入
output reg q // 数据输出(寄存器类型)
);
always @(posedge clk or posedge reset) begin
if (reset)
q <= 0; // 复位时,q 输出为 0
else
q <= d; // 在时钟上升沿时,将 d 的值存入 q
end
endmodule
- 在上述例子中,
q
被定义为reg
类型,因为它存储数据值,并且在时钟的上升沿时更新其值。 reg
信号能够在always
块中通过赋值语句(<=
或=
)进行更新。
示例3:组合使用wire、reg
module counter (
input wire clk, // 时钟信号
input wire reset, // 复位信号
output wire [3:0] count // 4位计数器输出
);
reg [3:0] count_reg; // 使用 reg 类型存储计数值
wire reset_signal; // 使用 wire 类型作为复位信号
assign reset_signal = reset; // 将复位信号传递给 wire 类型信号
always @(posedge clk or posedge reset_signal) begin
if (reset_signal)
count_reg <= 4'b0000; // 复位时将计数器值清零
else
count_reg <= count_reg + 1; // 否则计数加 1
end
assign count = count_reg; // 将寄存器值传递给输出
endmodule
count_reg
是一个reg
类型的信号,用于存储计数值。reset_signal
是一个wire
类型的信号,用于传递外部复位信号。count
是输出信号,通过assign
将count_reg
的值传递出去。
六、信号命名
- 每个文件只包含一个module,module名要小写,并且与文件名保持一致。
- 除parameter外,信号名全部小写,名字中的两个词之间用下划线连接。
- 信号名长度建议不超过15至20个字符,并且避免使用Verilog和VHDL的保留字命令。
- 不允许两个连续的下划线出现在命名字符串中。
- 不允许使用大小写来区分模块名称、变量、信号。
七、always块描述方式
- begin/end要单独另起一行,配对的begin/end列对齐。
- 在时序逻辑语句块(always)中统一采用非阻塞型赋值。
- 同一always块中,非阻塞赋值和阻塞赋值不能混用。
- 避免使用latch,如组合逻辑里面的if不带else分支。
- always有且仅有一个的敏感事件列表,敏感事件列表要完整,否则可能会造成前后仿真的结果不一致。
示例1:时钟驱动
module d_flip_flop (
input wire clk, // 时钟信号
input wire reset, // 复位信号
input wire d, // 数据输入
output reg q // 输出
);
always @(posedge clk or posedge reset) begin
if (reset)
q <= 0; // 复位时将 q 输出置为 0
else
q <= d; // 时钟上升沿时,将 d 的值存入 q
end
endmodule
- 描述方式:
always @(posedge clk or posedge reset)
表示当clk
上升沿或reset
变为高电平时触发该块。 - 行为:当
reset
信号为高时,输出q
置为 0;否则,在clk
上升沿时将输入d
的值赋给输出q
。
1.
always
语句
always
语句块用于描述硬件行为的变化。always
后面的括号内是触发条件,指定了何时该块的代码会被执行。always
语句块会在触发条件满足时执行其中的代码。2.
@
符号
@
符号是事件控制符号,用来指定触发条件。例如,@posedge clk
表示在clk
信号的上升沿(从 0 到 1)时触发该语句块。3.
posedge clk
的含义
posedge
是 "positive edge"(上升沿)的缩写,表示信号由低电平(0)变为高电平(1)的瞬间。
clk
是时钟信号,通常用来驱动时序电路的操作。因此,
posedge clk
表示当时钟信号clk
由低电平变为高电平时,触发该always
语句块的执行。4.
posedge reset
的含义
reset
通常是一个复位信号,当该信号有效时(通常为高电平),可以强制模块进入一个初始状态。
posedge reset
表示当reset
信号从低电平变为高电平时,触发该always
语句块的执行。5.
always @(posedge clk or posedge reset)
的含义结合起来,
always @(posedge clk or posedge reset)
表示该always
块会在两种情况下被触发:
时钟上升沿触发:当时钟
clk
由低电平变为高电平时(posedge clk
),触发该块的执行。复位信号上升沿触发:当复位信号
reset
由低电平变为高电平时(posedge reset
),触发该块的执行。这种触发条件常用于设计时序电路中的触发器(如 D 触发器)或者带有复位功能的计数器等。
6. 复位和时钟的优先级
在这种触发条件下,复位信号和时钟信号是“或”关系。这意味着:
优先级:如果复位信号
reset
在时钟的上升沿之前变为高电平,那么复位信号会优先生效,因为复位通常是硬件电路中最优先的操作。即使时钟信号也在变化,复位信号一旦有效,模块会立即进入复位状态。复位条件:当
reset
为高电平时,always
语句块的逻辑会优先处理复位操作。只有当复位信号无效(低电平)时,时钟的上升沿才会触发后续的逻辑操作。
示例2:计数器
module counter (
input wire clk, // 时钟信号
input wire reset, // 复位信号
output reg [3:0] count // 计数器输出
);
always @(posedge clk or posedge reset) begin
if (reset)
count <= 4'b0000; // 复位时将计数器清零
else
count <= count + 1; // 时钟上升沿时,计数加 1
end
endmodule
- 描述方式:
always @(posedge clk or posedge reset)
,计数器在clk
上升沿时增加计数,或者在reset
信号为高时清零。 - 行为:计数器在每个时钟周期增加 1,或者在复位信号有效时将计数值清零。
示例3:组合逻辑:与门
module and_gate (
input wire a, // 输入信号
input wire b, // 输入信号
output reg y // 输出信号
);
always @(*) begin
y = a & b; // 计算 a 和 b 的与操作
end
endmodule
- 描述方式:
always @(*)
表示该块在任何输入信号变化时都会触发,适用于组合逻辑。 - 行为:当输入信号
a
或b
改变时,y
的值将立即更新为a & b
。
示例4:多路选择器MUX
module mux (
input wire [1:0] sel, // 选择信号
input wire a, // 输入信号 a
input wire b, // 输入信号 b
input wire c, // 输入信号 c
input wire d, // 输入信号 d
output reg y // 输出信号
);
always @(*) begin
case (sel)
2'b00: y = a; // 选择 a
2'b01: y = b; // 选择 b
2'b10: y = c; // 选择 c
2'b11: y = d; // 选择 d
default: y = 0; // 默认输出 0
endcase
end
endmodule
- 描述方式:
always @(*)
表示当输入信号sel
,a
,b
,c
,d
改变时,y
的值会根据sel
的值选择不同的输入信号。 - 行为:
sel
信号决定了输出y
的值是a
,b
,c
还是d
,这是一个典型的多路选择器。
示例5:时序逻辑与组合逻辑混合使用
module counter_with_reset (
input wire clk, // 时钟信号
input wire reset, // 复位信号
output reg [3:0] count // 计数器输出
);
always @(posedge clk) begin
if (reset)
count <= 4'b0000; // 复位时清零
else
count <= count + 1; // 时钟上升沿时计数
end
always @(*) begin
if (count == 4'b1000)
$display("Count reached 8"); // 当计数达到 8 时输出信息
end
endmodule
- 描述方式:第一个
always @(posedge clk)
块描述了时序逻辑,计数器在时钟上升沿时增加。第二个always @(*)
块是组合逻辑,当计数器值为 8 时输出信息。
八、assign块描述方式
- 使用assign关键字用于指定输出信号与输入信号之间的逻辑关系。
- 在组合逻辑语句块(always和assign)中统一采用阻塞型赋值。
- 在逻辑代码中,除了三态控制逻辑接口允许使用高阻Z状态进行信号赋值外,在其他信号赋值、条件表达式等逻辑中都不允许使用高阻Z状态。
assign
用于描述组合逻辑,其赋值操作是连续的,即输入信号发生变化时,输出信号会立即反映出来。always
语句用于描述时序逻辑,并且通常配合时钟信号(例如posedge clk
)进行触发。
基本语法:assign <信号名> = <逻辑表达式>;
示例1:基本语句
module simple_assign(
input wire a, // 输入信号a
input wire b, // 输入信号b
output wire y // 输出信号y
);
// 组合逻辑:y = a AND b
assign y = a & b;
endmodule
assign y = a & b;
表示y
的值是a
和b
的逻辑与(AND)运算的结果。- 一旦
a
或b
改变,y
会立即反映出新的结果。 - 这是一个组合逻辑电路的简单示例。
示例2:多输入逻辑表达式
module logic_example(
input wire a, // 输入信号a
input wire b, // 输入信号b
input wire c, // 输入信号c
output wire y // 输出信号y
);
// 组合逻辑:y = (a AND b) OR c
assign y = (a & b) | c;
endmodule
assign y = (a & b) | c;
表示y
是(a AND b)
和c
的逻辑或(OR)运算的结果。- 一旦
a
、b
或c
中任何一个信号变化,y
的值会立即更新。
示例3:反转器
module inverter(
input wire a, // 输入信号a
output wire y // 输出信号y
);
// 组合逻辑:y = NOT a
assign y = ~a;
endmodule
assign y = ~a;
表示y
是a
的逻辑非(NOT)运算的结果。- 当输入信号
a
变化时,输出信号y
会立即反映其反转值。
示例4:4位加法器
module adder(
input wire [3:0] a, // 4位输入a
input wire [3:0] b, // 4位输入b
output wire [3:0] sum, // 4位和
output wire carry_out // 进位输出
);
// 组合逻辑:4位加法器
assign {carry_out, sum} = a + b;
endmodule
assign {carry_out, sum} = a + b;
表示sum
是a
和b
的和,carry_out
是进位输出。- 使用大括号
{}
表示将多个信号连接成一个信号。
九、空格和TAB
由于不同的解释器对于TAB翻译不一致,因此建议使用空格而不是TAB进行缩进,一般所有缩进以4个空格为单位。
十、注释
- 注释描述需要清晰、简洁,避免冗余。
- 核心代码和信号定义之间需要增加注释。
- 用“//”做小于1行的注释,用“/* */”做多于1行的注释。
十一、模块例化
- module例化名可以用“u_xx_x”标示。
- 建议给每个模块加timescale。
十二、其他注意事项
- 不使用repeat等循环语句(仿真代码除外)。
- 避免使用太复杂和少见的语法,可能造成语法综合器优化力度较低。
- 不使用include语句。
- 不使用disable、initial等综合工具不支持的电路。
- 不使用specify模块,不使用“===”、“!==”等不可综合的操作符。
- 除仿真外,不使用fork-join、while、repeat、forever语句。
- 除仿真外,不使用系统任务($)。
- 除仿真外,不使用deassign、force、release语句。
- 不在连续赋值语句中引入驱动强度和延时。
- 设计中不使用macro_module。
- 不在RTL代码中实例门级单元,尤其是CMOS、NMOS、PMOS、trans等。
遵循这些Verilog编程规范,有助于提高代码的可读性、可维护性和可移植性,也有助于逻辑工程师之间的交流与合作。