HDLBits答案(23)_找BUG
找BUG
前言
欢迎来到HDLBits系列的第23篇!今天的主题非常有意思——找BUG。这一章的题目会给我们一些有问题的代码,让我们找出错误并修正。
为什么我们要专门练习找BUG呢?因为在实际的工程开发中,调试代码的时间往往比写代码的时间还要长。能够快速定位和修复问题,是一个优秀工程师的必备技能。
生活实例类比:找BUG就像给汽车做故障诊断。汽车出问题了,我们需要通过各种现象(比如异响、仪表报警)来判断哪里出了问题,然后修复它。同样,代码有BUG了,我们需要通过仿真波形、错误提示来定位问题,然后修正代码。
这一章虽然相对简单,但每一个题目都代表了一类非常经典的错误。让我们一起来看看吧!
题库
8bit_2_1_Mux
问题分析
我们先来看第一个题目:一个8位的2选1多路选择器。
原代码:
1 | module top_module ( |
让我们来找找这里有什么问题:
问题1:输出位宽不匹配
- 输出
out没有指定位宽,默认是1位 - 但输入
a和b都是8位的,这明显不对
问题2:逻辑表达式有误
- 用
&和|来实现多路选择器,对于1位信号是可以的 - 但对于多位信号,应该使用条件运算符
? :或者case语句 - 原代码中的写法实际上是按位运算,而不是多路选择
修正后的代码
1 | module top_module ( |
要点小结
- 位宽匹配非常重要!输入输出的位宽一定要对应
- 对于多位信号的多路选择,条件运算符
? :是最简洁清晰的方式 - 代码写完后,要仔细检查信号的位宽是否正确
NAND
问题分析
第二个题目是用5输入与门来实现3输入与非门。
可供调用的5输入与模块:
1 | module andgate ( output out, input a, input b, input c, input d, input e ); |
原代码:
1 | module top_module (input a, input b, input c, output out);// |
让我们来分析一下问题:
问题1:端口顺序错误
andgate模块的端口顺序是:输出out在前,然后是输入a、b、c、d、e- 原代码把输入放在前面,输出放在后面了
问题2:端口数量不够
andgate有5个输入(a-e),但原代码只连接了3个- 未使用的输入应该接1(因为与门只要有一个输入是0,输出就是0)
问题3:缺少非门
- 题目要求的是与非门,但
andgate是与门 - 需要在与门的输出上加一个反相器
修正后的代码
1 | module top_module ( |
要点小结
- 实例化模块时,建议使用命名端口连接(
.port(signal)),这样不容易出错 - 未使用的输入要合理处理:与门/与非门的 unused 输入接1,或门/或非门的 unused 输入接0
- 使用内部信号来连接不同的模块,提高代码可读性
8bit_4_1_Mux
问题分析
第三个题目稍微复杂一点:我们需要用两个2选1多路选择器来实现一个4选1多路选择器。
首先,题目提供了一个无BUG的2选1多路选择器:
1 | module mux2 ( |
待修改的代码:
1 | module top_module ( |
让我们来找找问题:
问题1:wire信号位宽不对
mux0和mux1定义成了1位,但它们应该是8位的
问题2:例化名和信号名冲突
mux2 mux2 (...):例化名和模块名一样了,这会造成混淆mux2 mux0 (...):例化名和信号名mux0一样了,这是不允许的
问题3:选择信号错误
- 第二个mux2的选择信号用了
sel[1],但应该用sel[0] - 第三个mux2的选择信号应该用
sel[1],来选择前两个mux的输出
修正后的代码
1 | module top_module ( |
要点小结
- 信号命名要清晰,避免使用和模块名、例化名相同的名字
- 层次化设计:用多个小模块组成大模块时,要理清信号的连接关系
- 画一个简单的结构图有助于理解:
- 第一级:用sel[0]选择a/b和c/d
- 第二级:用sel[1]选择前两级的输出
Add/Sub
问题分析
第四个题目是一个加法器/减法器,根据do_sub信号来决定是做加法还是减法。
原代码:
1 | // synthesis verilog_input_version verilog_2001 |
让我们来分析问题:
问题1:逻辑取反符号使用错误
~out是按位取反,不是逻辑非- 应该用
out == 0或者!out来判断是否为0
问题2:if语句不完整,产生锁存器
result_is_zero是reg型,在if语句中只给它赋值了1,但没有给它赋值0的情况- 这会导致综合器推断出锁存器(latch),这在组合逻辑中是不希望看到的
- 当
out不为0时,result_is_zero会保持上一次的值,而不是变为0
修正后的代码
1 | // synthesis verilog_input_version verilog_2001 |
要点小结
- 组合逻辑中,一定要确保所有变量在所有分支都有赋值,避免产生锁存器
- 一个好的习惯是:在always块的开头先给所有输出变量赋默认值
- 按位取反
~和逻辑非!是不同的:~out是把每一位都取反!out是逻辑非,只有当out全为0时结果才是1
- 判断信号是否为0,最好用
out == 0,这样最直观
Case statement
问题分析
最后一个题目是一个case语句的例子,根据code输入来输出对应的数值和有效标志。
原代码:
1 | module top_module ( |
让我们来找找问题:
问题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 | module top_module ( |
要点小结
- 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练习非常有价值!通过分析这些常见错误,我们可以:
- 避免自己犯同样的错误
- 提高调试代码的能力
- 养成良好的编码习惯
笔者认为,找BUG的能力是在实践中不断积累的。写得多了,调试得多了,自然就能快速定位问题。但通过学习这些经典错误,我们可以少走很多弯路。
下一章我们将学习如何从波形图反推电路,这也是一个非常有趣且实用的技能。敬请期待!




