简单译码器
在学习完上一节的内容之后,实际上已经可以搭出所有的组合逻辑了。但应该没有人会觉得利用最简单的逻辑门来组合电路很容易——这个跟我直接在模块上连线有什么区别?
在本节当中,我们将通过简单译码器的例子学习新的语法,简化未来复杂电路设计的难度。
简单译码器的功能
3-8 译码器的真值表如下:
输入 A | 输出 B |
---|---|
000 | 0000_0001 |
001 | 0000_0010 |
010 | 0000_0100 |
011 | 0000_1000 |
100 | 0001_0000 |
101 | 0010_0000 |
110 | 0100_0000 |
111 | 1000_0000 |
实现
向量信号和 reg
module decoder (
input wire [2:0] a_i,
output reg [7:0] b_o
);
//TODO
endmodule
我们来观察一下这种新的信号定义:
- 符号仍然是 c++ 中的 []
- 内部是由 : 分隔开的两个数,而不像 c++ 中用长度来定义数组
- 出现了新的信号类型 reg
在 system verilog 中,向量信号的定义需要给出信号的最高位和最低位,因此在这里使用 [2:0] 表示宽度为 3 的信号,[7:0] 表示宽度为 8 的信号。
另一方面,请注意在 system verilog 的语义上:wire 和 reg 并没有明显区别,两种信号的区别仅在于赋值方式和使用位置的不同,下面会做详细说明。
模块体中,译码器的逻辑实现如下:
always_comb begin
b_o = 8'h0; // (*)
if(a_i == 3'b000) begin
b_o[0] = 1;
end else if(a_i == 3'b001) begin
b_o[1] = 1;
end else if(a_i == 3'b010) begin
b_o[2] = 1;
end else if(a_i == 3'b011) begin
b_o[3] = 1;
end else if(a_i == 3'b100) begin
b_o[4] = 1;
end else if(a_i == 3'b101) begin
b_o[5] = 1;
end else if(a_i == 3'b110) begin
b_o[6] = 1;
end else begin
b_o[7] = 1;
end
end
字面信号量
字面信号量的基本结构如下:<signal-width>'<base><value>
<signal-length>
为信号量的宽度<base>
为<value>
的进制,常见的有 b (二进制), d (十进制), h (十六进制)<value>
为字面信号量的数值,不支持负数,可以使用下划线分割便于阅读,如 (32'h0000_0001)
always_comb
always_comb 表示这是一个组合逻辑块,与 always_ff 区分,表示其内部赋值的 reg 均不是寄存器。代码块由 begin 和 end 包围。
我们之前说过,wire 信号必须在模块体内部使用 assign 进行赋值,这里我们将其细化一下:
- wire 信号必须在模块体内部,always 块外部使用 assign 进行赋值(这意味着 wire 信号不能使用 if)
- reg 信号必须在 always 块中赋值
- 所有 input 信号必须为 wire
if-else 结构除需要 begin / end 对以外,以及必须在 always 块中使用以外,与 c++ 中的 if 没有本质区别
always_comb 块的使用规范
关于阻塞赋值号 =
我们知道,硬件描述语言描述的是逻辑电路。而在电路中的信号,并不像软件中的变量,跟一段内存绑定。信号是跟电路中的物理线绑定的。
当一个信号以阻塞赋值方式被赋值后,这个信号名将与该右值的输出信号线 重新绑定 供后续使用。
而 system verilog 中,除阻塞赋值外,还有非阻塞赋值 <=
当一个信号以非阻塞赋值方式被赋值后,在 always 块内,这个信号名将仍然与赋值前的信号线绑定,离开 always 块后变为其最后一次绑定的右值对应的信号线。
举个例子:
reg [3:0] a;
reg [3:0] b;
always_comb begin
a = 2; // (1)
b = 2; // (2)
b <= a + 1; // (3) b <= 3, but b is still 2 here
a = b + 1; // (4) a = 3
b <= a + 1; // (5) b <= 4, but b is still 2 here
a = b + 2; // (6) a = 2 + 2
end // here b changes to 4
中间向量信号
向量信号的语法在中间信号的位置同样可用,如上。
不要尝试按照软件方式理解这个概念,请将上面这个代码与下面的示意图对应进行理解:
flowchart LR
ainput[a=2] -- 2 --> adder1[+]
const1[1] -- 1 --> adder1
const2[1] -- 1 --> adder2
binput[b=2] -- 2 --> adder2[+]
adder2 -- a'=3 --> adder3[+]
const3[1] -- 1 --> adder3
binput -- 2 --> adder4[+]
const4[2] -- 2 --> adder4
adder4 -- a''=4 --> aoutput[output a=4]
adder3 -- 4 --> boutput[output b=4]
在 system verilog 中,为了防止出现混乱,请不要混合使用阻塞赋值和非阻塞赋值,并在 always_comb 中,使用语义更加清晰的阻塞赋值,毕竟你应该也不希望过几天之后就看不懂自己的代码吧。
杜绝在多个 always 块中对同一个 reg 信号进行赋值
reg a;
always_comb begin
a = 1;
end
always_comb begin
a = 0; // incorrect
end
这样从语义上理解,你会将 0 和 1 短接在一起形成短路。
实际上上面这段代码不可综合,最后生成的电路不确定,因此请严格杜绝这种行为。
请保证在赋值的 always_comb 块中完整讨论了所有的情况
reg [3:0] a;
reg [3:0] b;
always_comb begin // incorrect
if (a == 4'h0) begin
b = 4'h1;
end
end
在 system verilog 中,真值表中没有赋值的位置的默认值为保持原始值,这样会生成锁存器,导致电路不稳定。
因此请保证 always_comb 块覆盖了 所有可能分支
这里推荐一个比较麻烦的写法杜绝这个问题,像下面这样:
reg [3:0] a;
reg [3:0] b;
always_comb begin
b = 4'hx; // (*)
if (a == 4'h0) begin
b = 4'h1;
end
end
(*)这行代码给 b 信号设置了默认值 x,保证了所有分支被覆盖,从而杜绝了锁存器问题。
有关 X 的补充
实际上 X 表示无关,也就是你在卡诺图中使用的 X。
因此像下面这样的写法也完全是可以的:
reg [3:0] a;
always_comb begin
if(a == 4'bXX1X) begin
// do something
end
end
这样可以简化 if 中条件的表达式,降低出错概率。
小结
向量信号:
[<input / output>] <wire / reg> [<upper-limit-close> : <lower-limit-close>] <signal-name>;
reg 信号:
- wire 信号必须在模块体内部,always 块外部使用 assign 进行赋值(这意味着 wire 信号不能使用 if)
- reg 信号必须在 always 块中赋值
- 所有 input 信号必须为 wire
if 语句块:
always_comb begin
if (<expression>) begin
// do something
end else if (<expression>) begin
// do something
end else begin
// do something
end
end
always_comb 语句块:
-
请不要混合使用阻塞赋值和非阻塞赋值,并在 always_comb 中,使用语义更加清晰的阻塞赋值
-
请严格杜绝在多个 always 块中对同一个 reg 信号进行赋值
-
请保证在赋值的 always_comb 块中完整讨论了所有的情况
注
到这里完成了 System Verilog 1 的讲述,可以完成点亮数字人生和 4 位加法器 CPLD 实验。