xjtuse版面向bzhang的verilog学习
感谢来自同学推荐的一个 verilog oj .
(真是一款非常好玩的oj,使我枯燥的verilog半日速通学习变得有趣了起来。欢迎大家来玩!!)
(md没有verilog高亮......typst都有,不亏是typst,库太强大了
assign
assign 赋值目标 = 表达式
赋值目标必须是wire型。
连续赋值语句 assign 描述的是事物之间的关联,而不是将值从一个事物复制到另一个事物的操作。因此当代码中存在多个 assign 时,它们出现的顺序无关紧要。
module top_module( output one );
assign one = 1;
endmodule
不赋值默认为0.
module top_module(
output zero
);
endmodule
运算
c
+ - * /
** // 幂 (同python
&& || ! // 与 或 非 (同c
& | ~ // 按位与 按位或 按位非 (同c
^ ~^ // 异或 同或
<< <<< >> >>> // 左移 算术左移 右移 算术右移
== != // 等于 不等于
=== !== // 恒等于 恒不等于
缩减运算符 & | ^ 的使用:
实现四位输入的与 或 异或门
module top_module(
input [3:0] in,
output out_and,
output out_or,
output out_xor
);
// &in 等价于 in[3] & in[2] & in[1] & in[0]
assign out_and = ∈
// |in 等价于 in[3] | in[2] | in[1] | in[0]
assign out_or = |in;
// ^in 等价于 in[3] ^ in[2] ^ in[1] ^ in[0]
assign out_xor = ^in;
endmodule
实用的连接运算符 {}:
{3'b111, 3'b000} => 6'b111000
{1'b1, 1'b0, 3'b101} => 5'b10101
{4'ha, 4'd10} => 8'b10101010 // 4'ha 和 4'd10 的二进制表示均为 4'b1010
{5{1'b1}} // 5'b11111(或 5'd31 或 5'h1f)
{2{a,b,c}} // 等同于 {a,b,c,a,b,c}
{3'd5, {2{3'd6}}} // 9'b101_110_110。它是 101 与 的连接。
// 第二个向量,即 3'b110 的两个副本。
assign {out[7:0], out[15:8]} = in; // 交换两个字节。左右两边都是 16 位向量。
assign out[15:0] = {in[7:0], in[15:8]}; // 这是同一件事。
assign out = {in[7:0], in[15:8]}; // 这有所不同。右侧的 16 位向量被扩展为
// 与左侧的 24 位向量匹配,因此 out[23:16] 为零。
// 在前两个示例中,out[23:16] 没有被赋值。
一些写法
Verilog 支持的 4 种进制简写
完整的格式是:<位宽>'<进制><数值>。
| 简写 | 英文全称 | 中文含义 | 允许的字符 | 示例 |
|---|---|---|---|---|
'b 或 'B |
Binary | 2 进制 | 0, 1, x, z | 4'b1010 |
'h 或 'H |
Hexadecimal | 16 进制 | 0-9, a-f, x, z | 4'ha (等同于 4'b1010) |
'd 或 'D |
Decimal | 10 进制 | 0-9 | 4'd10 (等同于 4'b1010) |
'o 或 'O |
Octal | 8 进制 | 0-7, x, z | 4'o12 (等同于 4'b1010) |
- 如果不写进制,默认是 10 进制 :
如果你写assign a = 10;,编译器会把它当作 32 位的十进制数 10 处理(32'd10)。 - 下划线
_增强可读性 :
可以随意在数字中间加下划线,编译器会忽略它。16'b1010_1010_1111_0000比16'b1010101011110000容易看多了。32'hdead_beef也是合法的。
- 特殊值 x 和 z :
z:高阻态(悬空,不连线)。x:未知状态(可能是0也可能是1)。- 比如:
4'b10xz或12'h3x0都是合法的写法。
逻辑值
- 1:逻辑1
- 0:逻辑0
- x:不确定
- z:高阻态
wire®
不声明默认为wire类型,reg需显式标出。
创建一个具有 3 个输入和 4 个输出的模块,使其行为类似于导线,并建立以下连接:
a -> w
b -> x
b -> y
c -> z
module top_module(
input a,b,c,
output w,x,y,z );
assign {w,x,y,z} = {a,b,b,c};
endmodule
可以通过 wire 声明一个中间变量。实现以下示例:

`default_nettype none
module top_module(
input a,
input b,
input c,
input d,
output out,
output out_n );
wire N1, N2, N3;
assign {N1, N2} = {a & b, c & d};
assign N3 = N1 | N2;
assign {out ,out_n} = {N3, !N3};
endmodule
`default_nettype none (禁止创建隐式网络)的核心作用是:强迫必须先定义每一个信号,才能使用它。(一般情况下,如果在代码里突然写了一个从来没定义过的名字,编译器默认会把它当作一个 1-bit 的 wire 自动创建出来。)

module top_module (
input p1a, p1b, p1c, p1d, p1e, p1f,
output p1y,
input p2a, p2b, p2c, p2d,
output p2y );
wire n1, n2, n3, n4;
assign {n1, n2, n3, n4} = {p1a&p1c&p1b, p2a&p2b, p1d&p1e&p1f, p2c&p2d};
assign {p1y, p2y} = {n1|n3, n2|n4};
endmodule
向量
向量用于将相关的信号分组,并使用同一个名称,以便于操作。例如,wire [7:0] w;声明了一个名为w的 8 位向量,其功能等同于 8 条独立的 wire 线。
向量的声明时将维度放在向量名之前,调用的时候放在向量名之后。可以通过下标连续调用一段。
wire [99:0] my_vector; //Declare a 100-element vector
assign out = my_vector[10]; // Part-select one bit out of the vector
assign out_10 = my_vector[9:0];
eg1.

module top_module (
input wire [2:0] vec,
output wire [2:0] outv,
output wire o2,
output wire o1,
output wire o0 );
assign outv[2:0] = vec[2:0];
assign {o2, o1, o0} = {vec[2], vec[1], vec[0]};
endmodule
assign outv[2:0] = vec[2:0]; 可以直接写成 assign outv = vec;
assign {o2, o1, o0} = {vec[2], vec[1], vec[0]}; 可以直接写成 assign {o2, o1, o0} = vec;
eg2. Build a combinational circuit that splits an input half-word (16 bits, [15:0] ) into lower [7:0] and upper [15:8] bytes.
module top_module(
input wire [15:0] in,
output wire [7:0] out_hi,
output wire [7:0] out_lo );
assign {out_hi, out_lo} = {in[15:8], in[7:0]};
endmodule
Module
简单的端口连接
将已有模块a 实例化,并连接端口到顶层模块
module mod_a ( input in1, input in2, output out );
// Module body
endmodule
写法一:按名称连接(推荐)
module top_module ( input a, input b, output out );
// 语法:模块名 实例名 ( .端口名(信号名), ... );
mod_a inst1 (
.in1(a), // 将外部信号 a 接到子模块端口 in1
.in2(b), // 将外部信号 b 接到子模块端口 in2
.out(out) // 将外部信号 out 接到子模块端口 out
);
endmodule
写法二:按位置连接(By Position)
这种写法类似 C 语言函数调用,必须严格按照 mod_a 定义时的端口顺序来写。
module top_module ( input a, input b, output out );
// mod_a 的定义顺序是: in1, in2, out
// 所以这里的括号里必须依次填入: a, b, out
mod_a inst1 ( a, b, out );
endmodule
多个模块的端口连接
在 Verilog 的可综合设计中,模块之间的连接必须通过显示定义的信号(wire/reg)来完成。不能像在 C++ 或 Java 类的成员访问那样,直接在一个模块的实例化中去"读取"另一个实例内部的端口信号。

module top_module ( input clk, input d, output q );
wire q1, q2;
my_dff ins1 (.clk(clk), .d(d), .q(q1));
my_dff ins2 (.clk(clk), .d(q1), .q(q2));
my_dff ins3 (.clk(clk), .d(q2), .q(q));
endmodule
always
always @(敏感条件信号表)
各类顺序语句
always中赋值目标必须为reg型。
例如

always @(posedge CLK)
Q=D;
当CLK的上升沿来临的时候,将D的值赋给Q。@代表等待。
posedge 代表上升沿,negedge代表下降沿。如果没写则默认为电平敏感。
case
always 内部可以使用case语句
case类似于c里面的switch
case (表达式)
取值1: 语句1;
取值2: 语句2;
取值3: 语句3;
...
...
default: 默认语句;
endcase
不要忘记写endcase!!!default可以不写,不过一般建议写。

module top_module (
input clk,
input [7:0] d,
input [1:0] sel,
output reg [7:0] q
);
wire [7:0] q1,q2,q3;
my_dff8 ins1 (.clk(clk), .d(d), .q(q1));
my_dff8 ins2 (.clk(clk), .d(q1), .q(q2));
my_dff8 ins3 (.clk(clk), .d(q2), .q(q3));
always @(*)
case(sel)
2'b00: q=d;
2'b01: q=q1;
2'b10: q=q2;
2'b11: q=q3;
endcase
endmodule
当sel的值分别为0,1,2,3的时候,q取对应的值。注意case后面没有冒号。
begin-end
当always中有多条赋值语句的时候,要用begin-end包裹起来,作用相当于c里的大括号
阻塞赋值与非阻塞赋值
组合电路常用阻塞赋值,时序电路常用非阻塞赋值,但不是绝对的。不可以在一个always块中同时使用阻塞赋值与非阻塞赋值。
阻塞赋值
always @ (A,B)
begin
M1=A;
M2=B&M1;
Q=M1|M2;
end
激活前 :M1=0,M2=0,Q=0
激活后 :
先计算A=1,马上赋值给M1
再计算B&M1=1,马上赋值给M2
再计算M1|M2=1,马上赋值给Q
非阻塞赋值
always @ (A,B)
begin
M1<=A;
M2<=B&M1;
Q<=M1|M2;
end
激活后执行过程 :
先计算A=1,(等待,不赋值)
再计算B&M1=0,(等待,不赋值)
再计算M1|M2=0,(等待,不赋值)
过程结束
先赋值给M1=1
再赋值给M2=0
再赋值给Q=0
一些例子
Dff D触发器

涉及到时序电路用非阻塞取值
module top_module (
input clk,
input d,
output reg q );
always @(posedge clk)
q<=d;
endmodule
带有同步复位的D触发器
module top_module (
input clk,
input reset, // Synchronous reset
input [7:0] d,
output [7:0] q
);
always @(posedge clk) begin
if (reset) q<=8'b0; // 当复位信号为1时 q复位取0 注意位宽8
else q<=d;
end
endmodule
带有异步复位的D触发器
(XJTUSE超纲了 不学异步 但是bzhang的皮皮踢上出现了)
同步复位:复位信号像是一个排队的人,必须等到时钟上升沿(posedge clk)到来时,才会被处理。
异步复位:复位信号像是一个特权插队者,只要它变为高电平(areset = 1),电路会立即把输出 q 清零,完全不管时钟在哪。
module top_module (
input clk,
input areset, // 高电平有效的异步复位
input [7:0] d,
output reg [7:0] q
);
// 关键点:敏感列表中包含了 clk 的上升沿 OR areset 的上升沿
always @(posedge clk or posedge areset) begin
if (areset) begin
// 只要 areset 变为 1,立即执行这里,不看 clk
q <= 8'b0;
end
else begin
// 只有当 clk 上升沿到达,且 areset 为 0 时,才执行这里
q <= d;
end
end
endmodule
计数器
Build a 4-bit binary counter that counts from 0 through 15, inclusive, with a period of 16. The reset input is synchronous, and should reset the counter to 0.
module top_module (
input clk,
input reset, // Synchronous active-high reset
output [3:0] q);
always @(posedge clk) begin
if (reset) q <= 4'b0;
else q <= q + 1'b1; // 加到16溢出了会自动复位 所以不用写额外逻辑
end
endmodule
用16位全加器实现32位全加器
add16按照下图所示连接各个模块。提供的模块add16具有以下声明:
module add16 ( input[15:0] a, input[15:0] b, input cin, output[15:0] sum, output cout );
在每个模块内部add16,会实例化 16 个全加器(模块add1未提供)来实际执行加法运算。您必须编写包含以下声明的全加器模块:
module add1 ( input a, input b, input cin, output sum, output cout );

注意wire的拆分与连接
module top_module (
input [31:0] a,
input [31:0] b,
output [31:0] sum
);
wire [15:0] s1, s2;
wire c;
add16 add16_1 (.a(a[15:0]), .b(b[15:0]), .sum(s1), .cout(c));
add16 add16_2 (.a(a[31:16]), .b(b[31:16]), .cin(c), .sum(s2));
assign sum = {s2,s1};
endmodule
module add1 ( input a, input b, input cin, output sum, output cout );
assign sum = a^b^cin;
assign cout = (a&b) | (a&cin) | (b&cin);
// 呃其实完全没必要这么写 直接 assign {cout, sum} = {a+b+cin}; 就好了
endmodule
以下例子为真题或ppt出现过的
实现一个四位两输入的加法器(前年真题)
这题verilog太容易了 正文一行解决
module top_module (
input [3:0] a, // 第一个 4 位输入
input [3:0] b, // 第二个 4 位输入
output [3:0] sum, // 4 位和输出
output cout // 进位输出
);
// 使用拼接运算符 {cout, sum} 来接收 5 位的加法结果
// 第 4 位(最高位)赋值给 cout,低 4 位赋值给 sum
assign {cout, sum} = a + b;
endmodule
用一些2-4译码器实现一个4-16译码器(前年真题)
感觉这题还是电路图画比较好
// 2-4 译码器子模块
module dec2_4 (
input [1:0] in,
input enable,
output reg [3:0] out
);
always @(*) begin
if (enable) begin
case (in)
2'b00: out = 4'b0001;
2'b01: out = 4'b0010;
2'b10: out = 4'b0100;
2'b11: out = 4'b1000;
default: out = 4'b0000;
endcase
end else begin
out = 4'b0000; // 不使能时,输出全 0
end
end
endmodule
// 顶层模块 4-16译码器
module dec4_16 (
input [3:0] in,
input enable, // 整个 4-16 译码器的总使能
output [15:0] out
);
wire [3:0] select; // 用于连接第一级和第二级的使能信号
// 第一级:控制层(处理高两位 in[3:2])
// 根据高两位决定哪一个第二级译码器被激活
dec2_4 master_dec (
.in(in[3:2]),
.enable(enable),
.out(select)
);
// 第二级:输出层(处理低两位 in[1:0])
// 译码输出 0-3
dec2_4 slave_dec0 (
.in(in[1:0]),
.enable(select[0]),
.out(out[3:0])
);
// 译码输出 4-7
dec2_4 slave_dec1 (
.in(in[1:0]),
.enable(select[1]),
.out(out[7:4])
);
// 译码输出 8-11
dec2_4 slave_dec2 (
.in(in[1:0]),
.enable(select[2]),
.out(out[11:8])
);
// 译码输出 12-15
dec2_4 slave_dec3 (
.in(in[1:0]),
.enable(select[3]),
.out(out[15:12])
);
endmodule
4位优先编码器
四个优先级:r[3]、r[2]、r[1]和r[0]作为一组4位输入信号,输出是最高优先级的二进制代码
module prio_encoder_if(
input [3:0] r,
output reg [2:0] y
);
always @*
if (r[3]==1'b1) // 可以写成(r[3])
y = 3'b100;
else if (r[2]==1'b1) // 可以写成(r[2])
y = 3'b011;
else if (r[1]==1'b1) // 可以写成(r[1])
y = 3'b010;
else if (r[0]==1'b1) // 可以写成(r[0])
y = 3'b001;
else
y = 3'b000;
endmodule
2-4译码器
module decoder_2_4_if(
input [1:0] a,
input en,
output reg [3:0] y
);
always @*
if (en == 1'b0)
y = 4'b0000;
else if (a == 2'b00)
y = 4'b0001;
else if (a == 2'b01)
y = 4'b0010;
else if (a == 2'b10)
y = 4'b0100;
else
y = 4'b1000;
endmodule
2个8位二进制数相乘
核心原理类似于列竖式乘法。
module mult_for (
input [8:1] op0,
input [8:1] op1,
output reg [16:1] result
);
integer i;
always @*
begin
result = 0;
for(i=1; i<=8; i=i+1)
if(op1[i])
result = result + (op0 << (i-1));
end
endmodule
4选1多路选择器
多路选择器其实就是进阶版的译码器。多一步将译码对应位的值传递输出而已。
module mux41_case
(
input in0, in1, in2, in3,
input s0, s1,
output reg out // out声明为reg类型
);
always @*
begin
case ({s1, s0})
2'b00: out = in0;
2'b01: out = in1;
2'b10: out = in2;
default: out = in0;
endcase
end
endmodule
1位二进制比较器
真值表
| in0 | in1 | gt(in0>in1) | eq(in0=in1) | lt(in0<in1) |
|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 0 | 1 |
| 1 | 0 | 1 | 0 | 0 |
| 1 | 1 | 0 | 1 | 0 |
module comp_1
(
input in0,in1,
output reg gt,eq,lt
);
always @*
begin
gt = 0;
eq = 0;
lt = 0;
if(in0>in1)
gt = 1;
if(in0==in1)
eq = 1;
if(in0<in1)
lt = 1;
end
endmodule
always块内if语句之前,对gt,eq和lt都初始化赋值为0 。
这样做的重要性是保证每个输出都被分配一个值。如果没有这样做,Verilog会认为你不想让它们的值改变,系统将会自动生成一个锁存器,那么得到的电路就不再是一个组合电路了。
8-3 优先编码器
module prio_encode_8_3
(
input [7:0] in,
output reg [2:0] encode_out
);
always @*
begin
casez (in)
8'b1???????: encode_out = 3'b000;
8'b01??????: encode_out = 3'b001;
8'b001?????: encode_out = 3'b010;
8'b0001????: encode_out = 3'b011;
8'b00001???: encode_out = 3'b100;
8'b000001??: encode_out = 3'b101;
8'b0000001?: encode_out = 3'b110;
8'b00000001: encode_out = 3'b111;
endcase
end
endmodule
注意这里用的是casez。? 可以表示无关状态,可以用 z` 表示。
串并转换 8bit -> 32bit (前年真题)
原题:实现一个将串行数据(8bit宽)转换为并行数据(32bit 宽)的电路,功能是将有效输入(en=1)的 8bit 数据合并成 32bit 宽数据后输出,同时输出有效信号1,该电路输入输出信号包括:clk(时钟),rst(复位),en(输入有效),datain(输入数据),dout_en(输出有效),dataout(输出数据)。
涉及移位寄存器。其实就是将输入保存在一个中间值(寄存器),存够32bit就输出(dout_en=1).
module s8_to_p32 (
input wire clk, // 时钟信号
input wire rst, // 复位信号 (高电平有效)
input wire en, // 输入有效信号
input wire [7:0] datain, // 输入数据 (8bit)
output reg dout_en, // 输出有效信号
output reg [31:0] dataout // 输出数据 (32bit)
);
// 内部寄存器
// 用于缓存前3次输入的8bit数据,共24bit。
// 当然也可以定义为32bit,但逻辑上只需保存前24bit等待最后一次拼接。
reg [23:0] data_buffer;
// 计数器,范围 0~3,需要2bit宽
reg [1:0] cnt;
always @(posedge clk or posedge rst) begin
if (rst) begin
// 复位状态
cnt <= 2'd0;
data_buffer <= 24'd0;
dout_en <= 1'b0;
dataout <= 32'd0;
end
else begin
// 默认拉低输出有效信号 (脉冲)
dout_en <= 1'b0;
if (en) begin
if (cnt == 2'd3) begin
// ---- 第4个字节到达 ----
// 此时 data_buffer 中已经存了前3个字节
// 现在的 datain 是第4个字节
dout_en <= 1'b1; // 输出有效
// 拼接输出:{第1字节, 第2字节, 第3字节, 当前第4字节}
dataout <= {data_buffer, datain};
// 计数器归零,准备下一轮
cnt <= 2'd0;
// data_buffer 在此时其实不需要更新,或者清零均可,
// 因为下一轮的第1个字节会覆盖写入最低8位,随后移位。
// 为了逻辑统一,这里可以不做操作,或者复位 buffer
data_buffer <= 24'd0;
end
else begin
// ---- 第1, 2, 3个字节到达 ----
// 计数器累加
cnt <= cnt + 1'b1;
// 移位逻辑:原有的数据左移8位,新数据补入低8位
// data_buffer[23:0] << 8 | datain
// 实际上只需保留 buffer 的低16位左移,并拼上新数据
data_buffer <= {data_buffer[15:0], datain};
end
end
end
end
endmodule
在oj上实现仿真
实现一个D触发器。呃写top_module有点麻烦,随便看看得了。
module top_module ();
//=========================================================
// 1. [修改] 信号定义
// 规则:输入给子模块的信号定义为 reg,从子模块输出的信号定义为 wire
//=========================================================
reg clk = 0;
reg rst_n = 0; // 复位信号 (举例)
reg data_in = 0; // 自定义输入 (举例)
wire data_out; // 自定义输出 (举例)
//=========================================================
// 2. 时钟生成与波形启动 (通常不需要改)
//=========================================================
initial `probe_start; // 启动波形工具
always #5 clk = ~clk; // 产生时钟 (周期=10个单位)
//=========================================================
// 3. [修改] 实例化你的模块 (DUT - Device Under Test)
// 把下面的 my_custom_module 换成你写的模块名字
// 把 .端口(信号) 对应连好
//=========================================================
my_custom_module u_dut (
.clk(clk),
.rst(rst_n),
.in(data_in),
.out(data_out)
);
//=========================================================
// 4. [修改] 添加探针 (Probes)
// 你想在波形图上看到哪个信号,就在这里加一行
//=========================================================
`probe(clk);
`probe(rst_n);
`probe(data_in);
`probe(data_out);
//=========================================================
// 5. [修改] 测试流程 (Stimulus)
// 在这里控制信号的变化,制造测试场景
//=========================================================
initial begin
// --- 初始状态 ---
rst_n = 0;
data_in = 0;
// --- 开始测试 ---
#10 rst_n = 1; // 10单位时间后,复位拉高
#10 data_in = 1; // 又过10单位,输入变1
#20 data_in = 0; // 又过20单位,输入变0
$display ("Current time: %0d ps", $time); // 打印时间看看
#50 $finish; // 结束仿真
end
endmodule
//---------------------------------------------------------
// 下面这里放你自己的模块代码
//---------------------------------------------------------
module my_custom_module(
input clk, rst, d,
output reg q
);
// 这里写你的逻辑
always @(posedge clk or negedge rst) begin
if(!rst) q <= 0;
else q <= d;
end
endmodule