硬件模块设计的思考方式

HDLBits链接


基本的逻辑门操作

让我们从最基础的逻辑门开始,逐步培养硬件设计的思维方式。


题目1: 简单的连线

题目描述1: 将输入端口 in 和输出端口 out 连接。

Solution1:

1
2
3
4
5
6
module top_module (
input in,
output out
);
assign out = in;
endmodule

题目2: 接地

题目描述2: 将输出 out 接地。

1

Solution2:

1
2
3
4
5
module top_module (
output out
);
assign out = 1'b0;
endmodule

题目3: 或非门

题目描述3: 实现或非门操作。

2

Solution3:

1
2
3
4
5
6
7
module top_module (
input in1,
input in2,
output out
);
assign out = ~(in1 | in2);
endmodule

题目4: 简单的组合逻辑

题目描述4: 实现下图所示的逻辑操作。

3

Solution4:

1
2
3
4
5
6
7
module top_module (
input in1,
input in2,
output out
);
assign out = in1 & (~in2);
endmodule

题目5: 稍复杂的逻辑

题目描述5: 实现下图所示的逻辑操作。

4

Solution5:

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module (
input in1,
input in2,
input in3,
output out
);
wire temp;

// 先算in1和in2的同或
assign temp = ~(in1 ^ in2);
// 再和in3异或
assign out = temp ^ in3;
endmodule

题目6: 多个逻辑门同时工作

题目描述6: 尝试同时建立几个逻辑门,建立一个两输入的组合电路。

共7个输出如下:

  • out_and: a and b
  • out_or: a or b
  • out_xor: a xor b
  • out_nand: a nand b
  • out_nor: a nor b
  • out_xnor: a xnor b
  • out_anotb: a and-not b

Solution6:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module top_module(
input a, b,
output out_and,
output out_or,
output out_xor,
output out_nand,
output out_nor,
output out_xnor,
output out_anotb
);
assign out_and = a & b;
assign out_or = a | b;
assign out_xor = a ^ b;
assign out_nand = ~(a & b);
assign out_nor = ~(a | b);
assign out_xnor = ~(a ^ b);
assign out_anotb = a & (~b);
endmodule

题目7: 7420芯片

题目描述7:

7400系列集成电路是一个数字芯片系列,每个都由几个基本的逻辑门构成。7420是一个带有两个4输入与非门的芯片。

实现一个具有与7420芯片相同功能的模块,共8个输入和2个输出。

5

Solution7:

1
2
3
4
5
6
7
8
9
module top_module (
input p1a, p1b, p1c, p1d,
output p1y,
input p2a, p2b, p2c, p2d,
output p2y
);
assign p1y = ~(p1a & p1b & p1c & p1d);
assign p2y = ~(p2a & p2b & p2c & p2d);
endmodule

真值表

在前面的练习中,我们使用简单的逻辑门和几个逻辑门的组合,这些电路是组合电路的例子。

组合电路的意思是电路的输出仅取决于输入,这意味着对于任何给定的输入值,只有一个可能的输出值。因此,描述组合函数行为的一种方法是明确地列出所有可能的输入所对应的输出值,即真值表

对一个有N个输入的布尔函数而言,有2^N种可能的输入组合。真值表的每一行都列出了一个输入组合,因此总有2^N行。output列显示了每个输入值对应的输出。

6


如何用逻辑门实现真值表?

那么我们如何只用标准逻辑门来实现查找表的功能呢?

一种简单的方法是将真值表中所有输出为1的项写成乘积之和(SOP)的形式。求和即为或操作,乘积即为与操作。先使用一个N输入与门判断输入向量是否与真值表中的某一项匹配,然后再用一个或门对所有满足匹配条件的结果进行输出。

打个比方:每个与门就像一个”检测器”,专门检查输入是否匹配某一行;然后或门把所有匹配的结果汇总起来,只要有一个检测器说”匹配”,输出就是1。


题目:用真值表设计电路

题目描述: 构建一个模块实现上述真值表的功能。

7

Solution:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module(
input x3,
input x2,
input x1, // 三个输入
output f // 一个输出
);
// 找出所有f=1的项:
// x3=0, x2=1, x1=1 → ~x3 & x2 & x1
// x3=1, x2=0, x1=1 → x3 & ~x2 & x1
// x3=1, x2=1, x1=0 → x3 & x2 & ~x1
// x3=1, x2=1, x1=1 → x3 & x2 & x1
// 然后化简一下...

// 这里是化简后的结果
assign f = (x1 & x3) | (x2 & ~x3);
endmodule

部分考题

让我们来看一些更有趣的题目,继续锻炼硬件设计思维。


题目1: 比较两个2位数是否相等

题目描述1:

创建一个有两个2位输入 A[1:0]B[1:0] 的电路,产生一个输出 z。若 A = B,则 z = 1,否则 z = 0

Solution1:

1
2
3
4
5
6
7
8
9
module top_module (
input [1:0] A,
input [1:0] B,
output z
);
// Verilog可以直接比较两个向量是否相等,很方便!
assign z = (A == B) ? 1'b1 : 1'b0;
// 或者更简单: assign z = (A == B);
endmodule

题目2: 实现一个简单的函数

题目描述2: 构建模块实现函数 z = (x^y) & x

Solution2:

1
2
3
4
5
6
7
module top_module (
input x,
input y,
output z
);
assign z = (x ^ y) & x;
endmodule

题目3: 从波形图反推逻辑

题目描述3: 构建模块实现如下波形图的输入输出关系。

8

Solution3:

1
2
3
4
5
6
7
8
9
module top_module (
input x,
input y,
output z
);
// 从波形可以看出:x和y相同时z=1,不同时z=0
// 这就是同或运算!
assign z = ~(x ^ y);
endmodule

题目4: 模块连接

题目描述4:

A模块实现的功能如上述题二所示,B模块实现的功能如题三所示。搭建模块实现下图所示功能:

9

Solution4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module (
input x,
input y,
output z
);
wire za;
wire zb;

// 模块A: za = (x^y) & x
assign za = (x ^ y) & x;
// 模块B: zb = ~(x^y)
assign zb = ~(x ^ y);
// 输出: (za | zb) ^ (za & zb)
assign z = (za | zb) ^ (za & zb);

endmodule

硬件工程师的思考方式

这是本章最重要的部分!让我们来谈谈硬件工程师独特的思考方式。

重要提示: 当进行模块设计时,我们最好反向思考问题: 如何从输出推导输入。这与我们平时顺序式思考编程问题不同,在编程时我们一般首先看输入如何决定输出,即输入为XX时输出为XX;对硬件工程师而言,通常的思路是当输出为XX时,输入应该是什么?

在硬件设计中,学会在两种思路间思考与切换是很重要的技能。

打个比方:

  • 软件工程师: 如果按下这个按钮,会发生什么?(输入→输出)
  • 硬件工程师: 如果要让灯亮起来,需要按下哪些按钮?(输出→输入)

题目1: 手机铃声和振动马达控制

题目描述1: 设计一种电路来控制手机的铃声和振动马达。当有来电输入信号时(输入 ring),电路必须打开铃声(输出 ringer = 1)或电机(输出 motor = 1),但不能同时打开。如果手机处于振动模式(输入 vibrate_mode = 1),打开电机。否则打开铃声。

10

Solution1:

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input ring,
input vibrate_mode,
output ringer, // 响铃
output motor // 振动
);
// 从输出思考:
// 什么时候ring会响? 有来电 + 不在振动模式
assign ringer = ring & (~vibrate_mode);
// 什么时候会振动? 有来电 + 在振动模式
assign motor = ring & vibrate_mode;
endmodule

题目2: 恒温控制器

题目描述2: 加热/冷却恒温器同时控制加热器(冬季)和空调(夏季)。设计一个电路,根据需要打开或关闭加热器、空调和鼓风机。

恒温器有两种模式:

  • 加热模式(mode = 1): 当温度过低时打开加热器(too_cold = 1),但是不要使用空调
  • 冷却模式(mode = 0): 当温度太高时打开空调(too_hot = 1),但不要打开加热器

当暖气或空调打开时,同时打开风扇让空气流通。此外,用户也可以仅要求风扇打开(fan_on = 1),即使加热器和空调都关闭。

Solution2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module (
input too_cold,
input too_hot,
input mode,
input fan_on,
output heater,
output aircon,
output fan
);
// 加热器: 加热模式 + 太冷
assign heater = mode & too_cold;
// 空调: 制冷模式 + 太热
assign aircon = (~mode) & too_hot;
// 风扇: 制热中 或 制冷中 或 用户要求开风扇
assign fan = (mode & too_cold) | ((~mode) & too_hot) | fan_on;
endmodule

对单向量的各bit进行操作

接下来,我们来看看如何方便地操作向量中的每一位。


题目1: 统计1的个数

题目描述1: 为3位输入向量构造一个计数1个数的电路。

Solution1:

1
2
3
4
5
6
7
8
9
10
11
12
13
module top_module(
input [2:0] in,
output [1:0] out
);
integer i;

always @(*) begin
out = 2'b0; // 先赋初值
for(i = 0; i < 3; i = i + 1) begin
out = out + in[i];
end
end
endmodule

题目2: 相邻位的关系

题目描述2: 输入一个4位的输入向量 in[3:0],输出每个比特和它相邻比特之间的一些关系:

  • out_both: 这个输出向量的每一位应该表示对应的输入位和它左边的比特位(左边比特具有更高的索引)是否均为1。举例说明,out_both[2]应该指示出in[2]in[3]是否均为1。
  • out_any: 这个输出向量的每一位都应该表示相应的输入位和它右边的比特位是否存在1
  • out_different: 这个输出向量的每一位都应该表明相应的输入位是否与其左边的比特位不同

Solution2:

思路一: for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module top_module(
input [3:0] in,
output [2:0] out_both,
output [3:1] out_any,
output [3:0] out_different
);
integer i;

always @(*) begin
for(i = 0; i < 3; i = i + 1) begin
// 第i位和第i+1位都是1
out_both[i] = in[i] & in[i + 1];
// 第i+1位或第i位有1
out_any[i + 1] = in[i + 1] | in[i];
// 第i位和第i+1位不同
out_different[i] = in[i] ^ in[i + 1];
end
// 最后一位: 和第0位比较
out_different[3] = in[0] ^ in[3];
end

endmodule

思路二: 向量操作,更简洁!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module(
input [3:0] in,
output [2:0] out_both,
output [3:1] out_any,
output [3:0] out_different
);
// in[2:0] & in[3:1]: 每一位和左边一位相与
assign out_both = in[2:0] & in[3:1];
// in[3:1] | in[2:0]: 每一位和右边一位相或
assign out_any = in[3:1] | in[2:0];
// in[3:0] ^ {in[0], in[3:1]}: 每一位和左边一位异或,最后一位和第0位异或
assign out_different = in[3:0] ^ {in[0], in[3:1]};

endmodule

小贴士: 看看第二种思路,是不是很优雅?充分利用Verilog的向量操作,可以让代码既简洁又清晰!


题目3: 扩大到100位

题目描述3: 题目同上,但输入向量变为100位。

Solution3:

思路一: for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
module top_module(
input [99:0] in,
output [98:0] out_both,
output [99:1] out_any,
output [99:0] out_different
);
integer i;

always @(*) begin
for(i = 0; i < 99; i = i + 1) begin
out_both[i] = in[i] & in[i + 1];
out_any[i + 1] = in[i + 1] | in[i];
out_different[i] = in[i] ^ in[i + 1];
end
out_different[99] = in[0] ^ in[99];
end

endmodule

思路二: 向量操作

1
2
3
4
5
6
7
8
9
10
11
12
module top_module(
input [99:0] in,
output [98:0] out_both,
output [99:1] out_any,
output [99:0] out_different
);
// 和4位的情况一样,代码完全不用改!
assign out_both = in[98:0] & in[99:1];
assign out_any = in[99:1] | in[98:0];
assign out_different = in[99:0] ^ {in[0], in[99:1]};

endmodule

对比一下: 用向量操作的方法,从4位扩展到100位,代码完全不需要修改!这就是硬件描述语言的魅力所在。


入门者避坑指南

在培养硬件设计思维的过程中,初学者容易犯以下错误:


错误1: 用软件思维写硬件

错误表现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module bad_example (
input clk,
input [3:0] data,
output reg [3:0] result
);
integer i;

always @(*) begin
// 错误!在组合逻辑里写这种类似软件的循环
for(i = 0; i < 4; i = i + 1) begin
result[i] = data[i] + 1'b1;
// 还想着result[i]会马上更新,并影响下一次循环
end
end
endmodule

错误原因:
硬件是并行工作的,不是顺序执行的!不要把C语言的思维直接带到Verilog里。

正确做法:

1
2
3
4
5
6
7
module good_example (
input [3:0] data,
output [3:0] result
);
// 直接用向量操作,简洁明了
assign result = data + 4'd1;
endmodule

调试技巧:

  • 时刻提醒自己:我在设计电路,不是在写软件
  • 多想想:这段代码会生成什么样的硬件?

错误2: 忘记硬件是并行的

错误表现:

1
2
3
4
5
6
7
8
9
10
11
module bad_example (
input a, b, c,
output reg y
);
always @(*) begin
// 以为这两句是顺序执行的
y = a & b;
y = y | c; // 错误!以为这里的y是上一行的结果
// 实际上这两句是并行的,第二句会覆盖第一句
end
endmodule

虽然这个例子的最终结果可能是对的,但这种思维方式是危险的!

正确理解:
在always块里,虽然语句是顺序写的,但从硬件角度看,它们是并行的。不过,在组合always块里,阻塞赋值=确实是顺序执行的。

正确做法:

1
2
3
4
5
6
7
module good_example (
input a, b, c,
output y
);
// 直接用assign,最清楚
assign y = (a & b) | c;
endmodule


错误3: 不会从输出反向推导

错误表现:
拿到一个题目,总是在想”输入是这样的话,输出会是什么”,而不是”要让输出是这样,需要输入是什么”。

正确做法:
对于硬件设计,特别是控制逻辑,从输出反向推导往往更清晰。

比如之前的手机铃声控制例子:

  • 不要想”如果有来电,并且不在振动模式,那么…”
  • 而是想”铃声什么时候响?需要两个条件同时满足:有来电 + 不在振动模式”

调试技巧:

  • 画个真值表,把所有输出为1的情况列出来
  • 对于每个输出,问自己:要让这个输出为1,输入需要满足什么条件?

错误4: 过度设计

错误表现:
明明用assign一句话就能解决的问题,非要写个always块加case语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module bad_example (
input [1:0] sel,
input [3:0] a, b, c, d,
output reg [3:0] y
);
always @(*) begin
case(sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
2'b11: y = d;
endcase
end
endmodule

虽然这没有错,但对于这种简单的多路选择器,其实可以更简洁。不过这个例子其实还好,case语句是合适的。

想表达的是: 不要把简单问题复杂化。选择最直观、最简洁的写法。

调试技巧:

  • 如果assign能写清楚,就用assign
  • 如果分支很多,用case
  • 如果是优先级逻辑,用if-else

本章小结

这一章我们学习了硬件模块设计的思考方式:

  1. 基础逻辑门: 从简单的连线开始,逐步建立硬件思维
  2. 真值表: 列出所有可能的输入输出,然后用乘积之和(SOP)实现
  3. 两种思考方向:
    • 正向: 输入→输出(软件思维)
    • 反向: 输出→输入(硬件思维,推荐!)
  4. 向量操作: 充分利用Verilog的向量特性,让代码更简洁
  5. 硬件不是软件: 记住硬件是并行工作的!

硬件设计思维的培养需要时间和练习,多做题、多思考、多画图,慢慢你就会像硬件工程师一样思考了!