由波形图描述电路

HDLBits链接


前言

欢迎来到HDLBits系列的第24篇!今天的主题非常有意思——由波形图描述电路。顾名思义,就是给我们一个波形图,让我们根据波形图来写出对应的Verilog代码。

为什么我们需要学习这个技能呢?因为在实际的工程开发中,我们经常会遇到这样的场景:

  1. 设计文档中给出了时序波形图,需要我们根据波形图来实现电路
  2. 调试时发现波形不符合预期,需要根据波形反推电路哪里出了问题
  3. 逆向分析一个已有电路,需要先观察波形,再理解电路功能

生活实例类比:这就像侦探破案。现场留下了一些痕迹(波形图),我们需要根据这些痕迹来还原案发经过(电路功能),然后找出凶手(写出代码)。

这一章的题目分为两部分:组合逻辑电路和时序逻辑电路。让我们开始吧!


题库

Combinational circuit 1

波形分析

我们先来看第一个组合逻辑电路的波形图:

1

让我们来分析一下:

  • 输入:ab
  • 输出:q

观察波形:

  • ab都为1时,q为1
  • 其他情况,q都为0

所以逻辑关系很简单:q = a & b(与运算)

生活实例类比:这就像一扇需要两把钥匙同时打开的门,只有当ab都有钥匙(为1)时,门才会打开(q为1)。

代码实现

1
2
3
4
5
6
7
8
9
10
module top_module (
input a, // 输入a
input b, // 输入b
output q // 输出q
);

// 与运算:a和b都为1时,q才为1
assign q = a & b;

endmodule

要点小结

  • 对于组合逻辑电路,先列出真值表,然后写出逻辑表达式
  • 简单的逻辑关系可以直接观察波形得出
  • 组合逻辑使用assign连续赋值语句

Combinational circuit 2

波形分析

第二个组合逻辑电路稍微复杂一点:

2

输入有4个:abcd
输出有1个:q

这种情况下,我们可以列出真值表,然后写出最小项之和的形式(SOP)。

观察波形,找出所有q=1的情况:

  • a=0, b=0, c=0, d=0 → 1
  • a=0, b=0, c=1, d=1 → 1
  • a=0, b=1, c=0, d=1 → 1
  • a=0, b=1, c=1, d=0 → 1
  • a=1, b=0, c=0, d=1 → 1
  • a=1, b=0, c=1, d=0 → 1
  • a=1, b=1, c=0, d=0 → 1
  • a=1, b=1, c=1, d=1 → 1

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module top_module (
input a, // 输入a
input b, // 输入b
input c, // 输入c
input d, // 输入d
output q // 输出q
);

// 最小项之和形式(SOP)
assign q = ~a & ~b & ~c & ~d | // 0000
~a & ~b & c & d | // 0011
~a & b & ~c & d | // 0101
~a & b & c & ~d | // 0110
a & ~b & ~c & d | // 1001
a & ~b & c & ~d | // 1010
a & b & ~c & ~d | // 1100
a & b & c & d; // 1111

endmodule

要点小结

  • 对于多输入的组合逻辑,可以列出真值表,然后写出最小项之和
  • 这种方法虽然繁琐,但最直接,不容易出错
  • 注意操作符的优先级:&|高,所以不需要括号

Combinational circuit 3

波形分析

第三个组合逻辑电路:

3

还是4个输入,1个输出。如果我们直接写最小项之和,会很繁琐。这时候可以用卡诺图(Karnaugh Map)来化简。

通过卡诺图化简,我们可以得到更简洁的表达式:
q = b & d | b & c | a & d | a & c

生活实例类比:这就像一个多重条件的门禁系统,只要满足以下任一条件就可以进入:

  • 有b和d权限
  • 有b和c权限
  • 有a和d权限
  • 有a和c权限

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input a, // 输入a
input b, // 输入b
input c, // 输入c
input d, // 输入d
output q // 输出q
);

// 卡诺图化简后的表达式
assign q = (b & d) | (b & c) | (a & d) | (a & c);

endmodule

要点小结

  • 卡诺图是化简逻辑表达式的有力工具
  • 化简后的电路使用更少的门,面积更小,速度更快
  • 简单的电路可以直接写化简后的表达式,复杂的电路建议用工具自动综合

Combinational circuit 4

波形分析

第四个组合逻辑电路:

4

观察波形可以发现,这个电路的逻辑非常简单:

  • 只要bc有一个为1,q就为1
  • 只有当bc都为0时,q才为0

所以逻辑表达式是:q = b | c(或运算)

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input a, // 输入a(注意:这个输入在电路中没有用到!)
input b, // 输入b
input c, // 输入c
input d, // 输入d(这个输入也没有用到!)
output q // 输出q
);

// 或运算:b或c为1时,q就为1
assign q = b | c;

endmodule

要点小结

  • 不是所有的输入都会在电路中用到,要仔细观察波形
  • 未使用的输入在代码中保留即可,不用处理
  • 如果综合工具报警告说某个输入未使用,可以忽略,只要功能正确就行

Combinational circuit 5

波形分析

第五个组合逻辑电路很有趣:

5

6

从波形图可以看出,这是一个多路选择器(MUX)

  • 输入:5个4位数据abcde
  • 选择信号:c(注意这里名字有点混淆,选择信号也叫c)
  • 输出:q

通过观察波形,我们可以得到选择关系:

  • c=0时,选择b
  • c=1时,选择e
  • c=2时,选择a
  • c=3时,选择d
  • 其他情况,输出0xf

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module top_module (
input [3:0] a, // 输入数据a
input [3:0] b, // 输入数据b
input [3:0] c, // 输入数据c(同时也是选择信号!)
input [3:0] d, // 输入数据d
input [3:0] e, // 输入数据e
output [3:0] q // 输出数据q
);

// 组合逻辑always块,使用case语句实现多路选择器
always @(*) begin
case (c)
4'd0: q = b; // c=0,选择b
4'd1: q = e; // c=1,选择e
4'd2: q = a; // c=2,选择a
4'd3: q = d; // c=3,选择d
default: q = 4'hf; // 其他情况,输出0xf
endcase
end

endmodule

要点小结

  • 多路选择器使用case语句实现最清晰
  • 注意这道题中输入信号c同时也是选择信号,名字有点混淆
  • 一定要加上default分支,否则会产生锁存器

Combinational circuit 6

波形分析

第六个组合逻辑电路是一个查表电路:

7

功能很简单:

  • 输入:3位信号a
  • 输出:16位信号q
  • 根据a的值,输出对应的16位数据

这就像一个只读存储器(ROM),a是地址,q是对应地址的数据。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module top_module (
input [2:0] a, // 3位地址输入
output [15:0] q // 16位数据输出
);

// 使用case语句实现查表功能
always @(*) begin
case (a)
3'd0: q = 16'h1232;
3'd1: q = 16'haee0;
3'd2: q = 16'h27d4;
3'd3: q = 16'h5a0e;
3'd4: q = 16'h2066;
3'd5: q = 16'h64ce;
3'd6: q = 16'hc526;
default: q = 16'h2f19;
endcase
end

endmodule

要点小结

  • 查表电路是组合逻辑的常见应用
  • 使用case语句实现查表最直观
  • 这种电路在实际中常用于:
    • 波形生成
    • 字符显示
    • 数学函数查表(如正弦表)

Sequential circuit 7

波形分析

现在我们来看时序逻辑电路!第一个时序电路:

8

时序逻辑和组合逻辑最大的区别是:时序逻辑有时钟信号,输出不仅取决于当前输入,还取决于历史状态

让我们分析这个电路:

  • 时钟:clk(上升沿触发)
  • 输入:a
  • 输出:q

观察波形:

  • 在每个时钟上升沿,q会更新为~aa的反)
  • 这是一个D触发器,D端输入是~a

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input clk, // 时钟信号(上升沿触发)
input a, // 输入数据a
output q // 输出q
);

// 时序逻辑always块,在时钟上升沿更新
always @(posedge clk) begin
q <= ~a; // 非阻塞赋值
end

endmodule

要点小结

  • 时序逻辑使用always @(posedge clk)来描述
  • 时序逻辑中使用非阻塞赋值<=
  • 输出在时钟上升沿更新,其他时间保持不变

Sequential circuit 8

波形分析

第二个时序电路稍微复杂一点:

9

这个电路有两个输出:pq

让我们分别分析:

输出p

  • 这是一个锁存器:当clock为高电平时,p = a;当clock为低电平时,p保持原值
  • 注意:题目中时钟信号叫clock,不是clk

输出q

  • 这是一个D触发器,在clock下降沿触发
  • 它的输入是p,所以在时钟下降沿,q会更新为当前的p

生活实例类比

  • p就像一个透明的门锁:当clock高电平时,门是透明的,a可以直接传到p;当clock低电平时,门锁上,p保持不变
  • q就像一个照相机:在时钟下降沿时,给p拍一张照,然后保存下来

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module top_module (
input clock, // 时钟信号
input a, // 输入数据a
output p, // 输出p(锁存器)
output q // 输出q(下降沿触发的D触发器)
);

// p是锁存器:clock为高电平时,p=a;否则保持
assign p = clock ? a : p;

// q是下降沿触发的D触发器
always @(negedge clock) begin
q <= p;
end

endmodule

要点小结

  • 锁存器是电平敏感的,触发器是边沿敏感的
  • posedge是上升沿,negedge是下降沿
  • 锁存器在现代设计中通常不推荐使用,因为容易产生时序问题

Sequential circuit 9

波形分析

第三个时序电路是一个计数器:

10

让我们分析一下这个计数器的行为:

  • 正常情况下(a=0),计数器从0到6循环计数:0→1→2→3→4→5→6→0→…
  • a=1时,计数器保持在4不变
  • a变回0后,计数器继续从4开始往下计数

生活实例类比:这就像一个电梯,正常情况下从1楼到7楼循环运行。但当有人在4楼按下停止按钮(a=1),电梯就会一直停在4楼。直到松开按钮(a=0),电梯才继续运行。

代码实现

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 clk, // 时钟信号
input a, // 保持信号(a=1时保持在4)
output [3:0] q // 计数器输出
);

always @(posedge clk) begin
if (a) begin
// a=1时,保持在4
q <= 4'd4;
end
else if (q == 4'd6) begin
// 计数到6时,下一个回到0
q <= 4'd0;
end
else begin
// 正常情况,加1
q <= q + 4'd1;
end
end

endmodule

要点小结

  • 计数器的设计要注意:
    • 计数范围(这道题是0-6)
    • 计数条件(什么时候加1,什么时候保持)
    • 复位条件(如果有的话)
  • 条件判断的顺序很重要:先检查保持条件,再检查计数边界

Sequential circuit 10

波形分析

最后一个时序电路稍微复杂一点:

11

这个电路有两个输入(ab),两个输出(qstate)。

让我们先分析输出q

  • 观察波形可以发现,qabstate的异或:q = a ^ b ^ state

再分析状态state

  • state是一个D触发器,在时钟上升沿更新
  • 观察state的变化规律:
    • a=1b=1时,下一状态state=1
    • a=0b=0时,下一状态state=0
    • 其他情况,state保持不变

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module top_module (
input clk, // 时钟信号
input a, // 输入a
input b, // 输入b
output q, // 输出q
output state // 输出state(寄存器)
);

// 输出q:a、b、state的异或
assign q = a ^ b ^ state;

// state是D触发器,在时钟上升沿更新
always @(posedge clk) begin
if (a & b) begin
// a和b都为1时,state置1
state <= 1'b1;
end
else if (~a & ~b) begin
// a和b都为0时,state置0
state <= 1'b0;
end
else begin
// 其他情况,state保持不变
state <= state;
end
end

endmodule

要点小结

  • 对于复杂的时序电路,可以分开分析:
    • 先分析组合逻辑部分(输出q
    • 再分析时序逻辑部分(状态state
  • 列出真值表有助于分析逻辑关系
  • 保持状态的语句state <= state可以省略,写上只是为了更清晰

入门者避坑指南

从波形图反推电路时,初学者很容易犯一些错误。下面我总结了最常见的5个问题:

坑点1:混淆组合逻辑和时序逻辑

错误表现

1
2
3
// 错误示例:应该用时序逻辑,却用了组合逻辑
// 题目要求是D触发器,但写成了组合逻辑
assign q = ~a; // 应该在always @(posedge clk)中

错误原因

  • 没有仔细看波形图中是否有时钟信号
  • 没有注意输出是否是在时钟边沿更新的

正确做法

1
2
3
4
// 正确示例:时序逻辑
always @(posedge clk) begin
q <= ~a;
end

调试技巧

  • 先看波形图中有没有时钟信号
  • 看输出是立即变化(组合逻辑),还是在时钟边沿变化(时序逻辑)

坑点2:阻塞赋值和非阻塞赋值使用错误

错误表现

1
2
3
4
// 错误示例:在时序逻辑中使用阻塞赋值
always @(posedge clk) begin
q = ~a; // 应该用<=,不是=
end

错误原因

  • 没有记住:时序逻辑用非阻塞赋值<=,组合逻辑用阻塞赋值=

正确做法

1
2
3
4
5
6
7
8
9
10
// 正确示例
// 组合逻辑
always @(*) begin
q1 = a & b;
end

// 时序逻辑
always @(posedge clk) begin
q2 <= a & b;
end

调试技巧

  • 记住这个简单的规则:
    • 组合逻辑(always @(*)):用=
    • 时序逻辑(always @(posedge clk)):用<=

坑点3:漏掉时钟边沿类型

错误表现

1
2
3
4
// 错误示例:应该是下降沿触发,写成了上升沿
always @(posedge clock) begin // 应该是negedge
q <= p;
end

错误原因

  • 没有仔细观察波形图中输出是在哪个边沿变化的

正确做法

1
2
3
4
// 正确示例:下降沿触发
always @(negedge clock) begin
q <= p;
end

调试技巧

  • 在波形图中画一条垂直线,看输出是在上升沿变化还是下降沿变化
  • 注意时钟信号的名字可能是clkclock

坑点4:信号位宽错误

错误表现

1
2
3
4
// 错误示例:位宽不匹配
input [3:0] a;
output q; // 应该是[3:0]
assign q = a;

错误原因

  • 没有仔细看波形图中信号是几位的

正确做法

1
2
3
4
// 正确示例
input [3:0] a;
output [3:0] q;
assign q = a;

调试技巧

  • 看波形图中信号是几位的(比如有没有显示十六进制,或者多位一起变化)
  • 检查输入输出的位宽是否匹配

坑点5:状态机设计错误

错误表现

1
2
3
4
5
6
// 错误示例:没有考虑所有情况
always @(posedge clk) begin
if (a & b)
state <= 1;
// 漏掉了其他情况!
end

错误原因

  • 没有完整分析波形图中状态的变化规律
  • 没有列出所有可能的输入组合

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
// 正确示例
always @(posedge clk) begin
if (a & b) begin
state <= 1'b1;
end
else if (~a & ~b) begin
state <= 1'b0;
end
else begin
state <= state; // 保持不变
end
end

调试技巧

  • 先画出完整的状态转移图或真值表
  • 确保所有可能的输入组合都有对应的处理
  • 如果有保持状态的情况,要明确写出来(或者给默认值)

结语

这一章的内容非常有趣,也非常实用。从波形图反推电路,是数字电路设计工程师的一项基本功。

笔者认为,要掌握这项技能,关键在于多练习:

  1. 先从简单的组合逻辑开始,熟悉基本的逻辑门
  2. 然后学习时序逻辑,理解触发器和锁存器的区别
  3. 最后挑战复杂的状态机和计数器

另外,建议大家在学习时,自己也多画波形图,这样可以更好地理解电路的行为。

下一章是HDLBits的最后一章:编写Testbench。这也是非常重要的一章,因为只有通过仿真验证,我们才能确保电路的功能是正确的。敬请期待!