找BUG

HDLBits链接


前言

欢迎来到HDLBits系列的第23篇!今天的主题非常有意思——找BUG。这一章的题目会给我们一些有问题的代码,让我们找出错误并修正。

为什么我们要专门练习找BUG呢?因为在实际的工程开发中,调试代码的时间往往比写代码的时间还要长。能够快速定位和修复问题,是一个优秀工程师的必备技能。

生活实例类比:找BUG就像给汽车做故障诊断。汽车出问题了,我们需要通过各种现象(比如异响、仪表报警)来判断哪里出了问题,然后修复它。同样,代码有BUG了,我们需要通过仿真波形、错误提示来定位问题,然后修正代码。

这一章虽然相对简单,但每一个题目都代表了一类非常经典的错误。让我们一起来看看吧!


题库

8bit_2_1_Mux

问题分析

我们先来看第一个题目:一个8位的2选1多路选择器。

原代码:

1
2
3
4
5
6
7
8
9
module top_module (
input sel,
input [7:0] a,
input [7:0] b,
output out );

assign out = (~sel & a) | (sel & b);

endmodule

让我们来找找这里有什么问题:

问题1:输出位宽不匹配

  • 输出out没有指定位宽,默认是1位
  • 但输入ab都是8位的,这明显不对

问题2:逻辑表达式有误

  • &|来实现多路选择器,对于1位信号是可以的
  • 但对于多位信号,应该使用条件运算符? :或者case语句
  • 原代码中的写法实际上是按位运算,而不是多路选择

修正后的代码

1
2
3
4
5
6
7
8
9
10
11
12
module top_module (
input sel, // 选择信号
input [7:0] a, // 输入数据a
input [7:0] b, // 输入数据b
output [7:0] out // 输出数据
);

// 使用条件运算符实现2选1多路选择器
// sel=1时选择a,sel=0时选择b
assign out = sel ? a : b;

endmodule

要点小结

  • 位宽匹配非常重要!输入输出的位宽一定要对应
  • 对于多位信号的多路选择,条件运算符? :是最简洁清晰的方式
  • 代码写完后,要仔细检查信号的位宽是否正确

NAND

问题分析

第二个题目是用5输入与门来实现3输入与非门。

可供调用的5输入与模块:

1
module andgate ( output out, input a, input b, input c, input d, input e );

原代码:

1
2
3
4
5
module top_module (input a, input b, input c, output out);//

andgate inst1 ( a, b, c, out );

endmodule

让我们来分析一下问题:

问题1:端口顺序错误

  • andgate模块的端口顺序是:输出out在前,然后是输入a、b、c、d、e
  • 原代码把输入放在前面,输出放在后面了

问题2:端口数量不够

  • andgate有5个输入(a-e),但原代码只连接了3个
  • 未使用的输入应该接1(因为与门只要有一个输入是0,输出就是0)

问题3:缺少非门

  • 题目要求的是与非门,但andgate与门
  • 需要在与门的输出上加一个反相器

修正后的代码

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
module top_module (
input a, // 输入a
input b, // 输入b
input c, // 输入c
output out // 输出(与非)
);

// 内部信号:与门的输出
wire and_out;

// 实例化5输入与门
// 注意:端口顺序要正确,未使用的输入接1
andgate inst1 (
.out(and_out), // 输出连接到内部信号
.a(a),
.b(b),
.c(c),
.d(1'b1), // 未使用的输入接1
.e(1'b1)
);

// 与非门:与门输出取反
assign out = ~and_out;

endmodule

要点小结

  • 实例化模块时,建议使用命名端口连接.port(signal)),这样不容易出错
  • 未使用的输入要合理处理:与门/与非门的 unused 输入接1,或门/或非门的 unused 输入接0
  • 使用内部信号来连接不同的模块,提高代码可读性

8bit_4_1_Mux

问题分析

第三个题目稍微复杂一点:我们需要用两个2选1多路选择器来实现一个4选1多路选择器。

首先,题目提供了一个无BUG的2选1多路选择器:

1
2
3
4
5
6
module mux2 (
input sel,
input [7:0] a,
input [7:0] b,
output [7:0] out
);

待修改的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top_module (
input [1:0] sel,
input [7:0] a,
input [7:0] b,
input [7:0] c,
input [7:0] d,
output [7:0] out ); //

wire mux0, mux1;
mux2 mux0 ( sel[0], a, b, mux0 );
mux2 mux1 ( sel[1], c, d, mux1 );
mux2 mux2 ( sel[1], mux0, mux1, out );

endmodule

让我们来找找问题:

问题1:wire信号位宽不对

  • mux0mux1定义成了1位,但它们应该是8位的

问题2:例化名和信号名冲突

  • mux2 mux2 (...):例化名和模块名一样了,这会造成混淆
  • mux2 mux0 (...):例化名和信号名mux0一样了,这是不允许的

问题3:选择信号错误

  • 第二个mux2的选择信号用了sel[1],但应该用sel[0]
  • 第三个mux2的选择信号应该用sel[1],来选择前两个mux的输出

修正后的代码

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
29
30
31
32
33
34
35
36
37
38
module top_module (
input [1:0] sel, // 2位选择信号
input [7:0] a, // 输入数据a
input [7:0] b, // 输入数据b
input [7:0] c, // 输入数据c
input [7:0] d, // 输入数据d
output [7:0] out // 输出数据
);

// 内部信号:位宽8位,避免命名冲突
wire [7:0] mux0_out;
wire [7:0] mux1_out;

// 第一个mux2:在a和b之间选择(sel[0])
mux2 mux0_inst (
.sel(sel[0]),
.a(a),
.b(b),
.out(mux0_out)
);

// 第二个mux2:在c和d之间选择(sel[0])
mux2 mux1_inst (
.sel(sel[0]),
.a(c),
.b(d),
.out(mux1_out)
);

// 第三个mux2:在前两个mux的输出之间选择(sel[1])
mux2 mux2_inst (
.sel(sel[1]),
.a(mux0_out),
.b(mux1_out),
.out(out)
);

endmodule

要点小结

  • 信号命名要清晰,避免使用和模块名、例化名相同的名字
  • 层次化设计:用多个小模块组成大模块时,要理清信号的连接关系
  • 画一个简单的结构图有助于理解:
    • 第一级:用sel[0]选择a/b和c/d
    • 第二级:用sel[1]选择前两级的输出

Add/Sub

问题分析

第四个题目是一个加法器/减法器,根据do_sub信号来决定是做加法还是减法。

原代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// synthesis verilog_input_version verilog_2001
module top_module (
input do_sub,
input [7:0] a,
input [7:0] b,
output reg [7:0] out,
output reg result_is_zero
);//

always @(*) begin
case (do_sub)
0: out = a+b;
1: out = a-b;
endcase

if (~out)
result_is_zero = 1;
end

endmodule

让我们来分析问题:

问题1:逻辑取反符号使用错误

  • ~out是按位取反,不是逻辑非
  • 应该用out == 0或者!out来判断是否为0

问题2:if语句不完整,产生锁存器

  • result_is_zero是reg型,在if语句中只给它赋值了1,但没有给它赋值0的情况
  • 这会导致综合器推断出锁存器(latch),这在组合逻辑中是不希望看到的
  • out不为0时,result_is_zero会保持上一次的值,而不是变为0

修正后的代码

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
29
30
31
// synthesis verilog_input_version verilog_2001
module top_module (
input do_sub, // 1: 减法, 0: 加法
input [7:0] a, // 输入数据a
input [7:0] b, // 输入数据b
output reg [7:0] out, // 运算结果
output reg result_is_zero // 结果是否为0标志
);

always @(*) begin
// 先给输出一个默认值,防止产生锁存器
out = 8'd0;
result_is_zero = 1'b0;

// 根据do_sub选择加法或减法
case (do_sub)
1'b0: out = a + b; // 加法
1'b1: out = a - b; // 减法
default: out = a + b;
endcase

// 判断结果是否为0
if (out == 8'd0) begin
result_is_zero = 1'b1;
end
else begin
result_is_zero = 1'b0;
end
end

endmodule

要点小结

  • 组合逻辑中,一定要确保所有变量在所有分支都有赋值,避免产生锁存器
  • 一个好的习惯是:在always块的开头先给所有输出变量赋默认值
  • 按位取反~逻辑非!是不同的:
    • ~out是把每一位都取反
    • !out是逻辑非,只有当out全为0时结果才是1
  • 判断信号是否为0,最好用out == 0,这样最直观

Case statement

问题分析

最后一个题目是一个case语句的例子,根据code输入来输出对应的数值和有效标志。

原代码:

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 [7:0] code,
output reg [3:0] out,
output reg valid=1 );//

always @(*)
case (code)
8'h45: out = 0;
8'h16: out = 1;
8'h1e: out = 2;
8'd26: out = 3;
8'h25: out = 4;
8'h2e: out = 5;
8'h36: out = 6;
8'h3d: out = 7;
8'h3e: out = 8;
6'h46: out = 9;
default: valid = 0;
endcase

endmodule

让我们来找找问题:

问题1:valid信号的默认值错误

  • valid在声明时赋了初值1,但这只在仿真开始时有效
  • 在always块中,只有default分支给valid赋值0,其他分支没有给valid赋值
  • 这会导致valid产生锁存器,行为不符合预期
  • 正确的逻辑应该是:匹配到有效code时valid=1,否则valid=0

问题2:进制不一致

  • 大部分用的是十六进制(8'hxx),但有一个用的是十进制(8'd26
  • 还有一个用的是6位(6'h46),应该统一用8位

问题3:default分支的处理不完整

  • default分支只给valid赋值,没有给out赋值
  • 这会导致out在default情况下产生锁存器

修正后的代码

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
module top_module (
input [7:0] code, // 输入编码
output reg [3:0] out, // 输出数值
output reg valid // 有效标志
);

always @(*) begin
// 默认值
out = 4'd0;
valid = 1'b0; // 默认无效

case (code)
8'h45: begin
out = 4'd0;
valid = 1'b1;
end
8'h16: begin
out = 4'd1;
valid = 1'b1;
end
8'h1e: begin
out = 4'd2;
valid = 1'b1;
end
8'h26: begin // 修正:统一用十六进制
out = 4'd3;
valid = 1'b1;
end
8'h25: begin
out = 4'd4;
valid = 1'b1;
end
8'h2e: begin
out = 4'd5;
valid = 1'b1;
end
8'h36: begin
out = 4'd6;
valid = 1'b1;
end
8'h3d: begin
out = 4'd7;
valid = 1'b1;
end
8'h3e: begin
out = 4'd8;
valid = 1'b1;
end
8'h46: begin // 修正:统一用8位
out = 4'd9;
valid = 1'b1;
end
default: begin
out = 4'd0; // default情况下也给out赋值
valid = 1'b0;
end
endcase
end

endmodule

要点小结

  • case语句中,每个分支都应该给所有输出变量赋值,或者在开头给默认值
  • 代码风格要一致:所有常量的进制、位宽要统一
  • 在组合逻辑中,不要在声明变量时赋初值,这可能会误导综合器
  • 每个case分支都使用begin-end块,虽然会多写几行,但代码更清晰,也不容易出错

入门者避坑指南

这一章的题目虽然都是找BUG,但这些BUG都是非常经典的,初学者很容易犯。下面我总结一下最常见的5类错误:

坑点1:位宽不匹配

错误表现

1
2
3
4
// 错误示例
input [7:0] a;
output out; // 忘记指定位宽,默认是1位
assign out = a; // 位宽不匹配!

错误原因

  • 输出信号没有指定位宽,默认是1位
  • 输入和输出的位宽不一致

正确做法

1
2
3
4
// 正确示例
input [7:0] a;
output [7:0] out; // 指定位宽
assign out = a;

调试技巧

  • 写完代码后,检查所有信号的位宽是否匹配
  • 查看综合报告,看是否有位宽不匹配的警告

坑点2:组合逻辑产生锁存器

错误表现

1
2
3
4
5
6
// 错误示例
always @(*) begin
if (sel)
out = a; // 只有sel=1时给out赋值
// sel=0时out没有赋值,会产生锁存器!
end

错误原因

  • 在组合逻辑的always块中,没有给所有变量在所有分支都赋值
  • 综合器会推断出锁存器来保持未赋值时的值

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 正确示例1:给默认值
always @(*) begin
out = 8'd0; // 先给默认值
if (sel)
out = a;
end

// 正确示例2:if-else完整
always @(*) begin
if (sel)
out = a;
else
out = b;
end

调试技巧

  • 查看综合报告,搜索”latch”关键字
  • 在always块开头给所有输出变量赋默认值,这是最简单安全的方法

坑点3:端口连接错误

错误表现

1
2
// 错误示例:位置连接容易出错
andgate inst1 (a, b, c, out); // 端口顺序搞反了

错误原因

  • 使用位置连接,容易记错端口顺序
  • 模块的端口列表可能修改过,但实例化的地方没有更新

正确做法

1
2
3
4
5
6
7
8
9
// 正确示例:命名连接
andgate inst1 (
.out(out),
.a(a),
.b(b),
.c(c),
.d(1'b1), // 未使用的输入接1
.e(1'b1)
);

调试技巧

  • 实例化模块时,始终使用命名连接.port(signal)
  • 这样即使模块的端口顺序改变,也不会影响连接

坑点4:命名冲突

错误表现

1
2
3
// 错误示例
wire mux0;
mux2 mux0 ( ... ); // 信号名和例化名相同!

错误原因

  • 信号名和例化名、模块名相同
  • 综合器无法区分

正确做法

1
2
3
// 正确示例
wire mux0_out;
mux2 mux0_inst ( ... ); // 加上_inst后缀

调试技巧

  • 例化名加上_inst后缀
  • 内部信号名加上_out_tmp等后缀
  • 避免使用和模块名相同的信号名

坑点5:操作符使用错误

错误表现

1
2
3
// 错误示例
if (~out) // 按位取反,不是逻辑非
result_is_zero = 1;

错误原因

  • 混淆了按位操作符和逻辑操作符
  • ~是按位取反,!是逻辑非

正确做法

1
2
3
4
5
6
7
// 正确示例
if (out == 8'd0) // 最直观的方式
result_is_zero = 1;

// 或者
if (!out) // 逻辑非
result_is_zero = 1;

调试技巧

  • 判断是否为0,最好用== 0,这样代码最清晰
  • 记住几个常用操作符的区别:
    • ~:按位取反(每一位都取反)
    • !:逻辑非(只有全0时结果为1)
    • &:按位与
    • &&:逻辑与

结语

这一章的找BUG练习非常有价值!通过分析这些常见错误,我们可以:

  1. 避免自己犯同样的错误
  2. 提高调试代码的能力
  3. 养成良好的编码习惯

笔者认为,找BUG的能力是在实践中不断积累的。写得多了,调试得多了,自然就能快速定位问题。但通过学习这些经典错误,我们可以少走很多弯路。

下一章我们将学习如何从波形图反推电路,这也是一个非常有趣且实用的技能。敬请期待!