1. HDLBits到底是个什么存在,为什么我建议你从头刷到尾
先交代一下背景。HDLBits是亨利·王(Henry Wong)在2015年左右搞出来的一个在线Verilog练习平台,几乎涵盖了数字逻辑设计和Verilog语法的全部基础知识点,目前大概有290多个小题,从最简单的assign赋值一路干到有限状态机、移位寄存器、计数器、加法器、仲裁器、甚至是一整条可选的CPU通路。不用装任何EDA工具,浏览器打开就能写代码、点仿真、看波形,写完交上去立刻告诉你对还是不对,逻辑综合、仿真验证全部在云端做完。
这东西最开始只是在高校数字电路课程里作为课后练习流传,后来因为题目设计得实在太好了——每个模块对应一个明确的知识点,难度梯度极其合理——慢慢就成了国内外面试数字IC岗、FPGA岗之前公认的刷题题库。我见过不少应届生把HDLBits刷了两遍三遍,也有人一边刷一边整理自己的题库笔记,回头面试的时候直接拿里面的题目思路去套笔试题。
这篇内容不是把290道题的代码全部贴出来(那样你也记不住,平台也不鼓励),而是把题目按照类别拆分,讲解每一类题背后的破题思路、常见坑和答案的推导逻辑。你照着这条线自己写一遍,比抄一份答案有用得多。适合正在学数电/Verilog的本科生、准备数字IC校招的求职者,以及想快速验证自己对某个语法点理解是否到位的在职工程师。
2. 刷题之前必须弄明白的三件事:评分机制、语法习惯、状态机思维
2.1 HDLBits是怎么判你答案对错的
很多人上来就写代码,写完交上去发现红X,就开始怀疑语法、怀疑EDA工具抽风,实际上是没搞懂HDLBits的评分机制。它的检验方式是:给你写的模块实例化到它的testbench里,跑一组预定义的输入激励,然后比对输出波形。如果你的输出和标准答案在每一个时钟周期、每一个输出端口上都完全一致,才算通过。
这意味着两件事。第一,你写的模块必须严格匹配题目给的模块声明——端口名字可以随便起,但端口数量、方向、位宽必须完全一致,多一个端口、少一个位宽都直接编译报错。第二,仿真时序是严格对齐的,你的组合逻辑输出需要在一个delta cycle内稳定,时序逻辑输出必须在时钟沿之后的值与参考答案一致。所以不要指望用延迟或者初始值之类的偏门手段蒙混过关,它测试的不是"能不能跑",而是"行为是否完全一致"。
另外需要注意,HDLBits对某些模糊行为会判定为错误,比如未初始化的寄存器、未连接悬空的输入、多个驱动源的竞争等。它的仿真器相对严格,很多你感觉"应该能过"的写法,它可能直接标红。这也是为什么我建议你刷题时反过来把它当lint工具用——哪些写法报警告,你就知道哪些写法在真实项目里也容易出事。
2.2 写之前先统一你的代码风格
HDLBits题目虽然短小,但input/output声明、wire/reg声明、always块的书写习惯会直接影响你的调试效率。我自己刷下来总结出一套模板式开头,每道题都先照这个架子搭,再填逻辑:
- 组合逻辑统一用assign或者always@(*)块,能assign就assign;
- 时序逻辑统一用always@(posedge clk)块,内部用非阻塞赋值<=,复位统一用异步复位(题目里有同步复位要求除外);
- 不建议在组合逻辑块里用阻塞赋值写多段if-else,容易引入不必要的锁存器;
- 多位的信号命名建议带上位宽后缀,比如count_next、sum_wire,方便追波形;
- 临时连线要么单独起名,要么直接用内联表达式,不要复用模块端口名。
这套习惯在HDLBits里会直接反映在编译通过率上。很多初学者犯的典型错误是:在always块里写了一个组合逻辑分支但少了else,导致综合出latch,功能在某些输入组合下不对,而且波形极难排查。HDLBits的编译虽然不报latch错误,但你的答案会翻车。
2.3 建立"状态机视角",而不是"写代码视角"
刷到中后期你会发现,HDLBits的题目本质上不是代码题,而是逻辑设计题。比如摩尔型有限状态机,题目不会告诉你需要几个状态、状态怎么编码,它只会描述行为:某个序列检测到101时输出1。你必须先画出状态转移图,再决定状态编码、次态逻辑、输出逻辑,最后才落成代码。
所以从最简单的题目开始,就应该养成先在纸上画时序图、画状态图的习惯。我见过太多人一上来就开写always块,写完发现少了一个状态分支,或者输出时序差了一拍。HDLBits的仿真只能告诉你"错了",它不会告诉你"你的状态机少了一个状态"。所以刷题之前,先把"设计先行、代码后写"这个思维方式立起来,后面会很省事。
3. 从Verilog基础到组合逻辑:前三章真正想考你的不是语法
HDLBits前几章叫Getting Started和Verilog Language,很多人以为是简单的语法题,快速过一遍就完事了。实际上这几章的坑非常密集,尤其是向量操作、位拼接、缩减运算、复制运算这些平时不常用的语法点,每一题都藏着一个容易忽略的细节。
3.1 向量操作:赋值宽度不匹配是最容易踩的雷
向量(Vector)相关的题目,比如vector0、vectorr、vector2等等,核心考点就是位宽和索引顺序。HDLBits的向量声明通常是input [3:0] a这种从高到低声明,但索引顺序有讲究,有些题目会用input [0:3] in这种反向声明,这时候in[0]是最高位,和大多数人默认的[3:0]是反的,一不留意就写反了。
还有一些题目考的是部分赋值和位拼接。比如assign out = {a[1:0], b[2:1]}这种写法,你得清楚拼接的结果位宽是4位,高位在前。HDLBits比较容易考的一个点是:拼接运算的左侧只能是向量变量,不能是向量的一部分,比如{a[1:0]} = 2'b01;是合法的,但assign {a[1:0], b[2:1]} = 4'b0110这种用法必须确保两侧位宽完全一致。
我个人建议,刷vector章节时不要贪快,每一道题都把代码的位宽手工标一遍,把assign语句左右两边的位宽写在注释里,强制自己确认匹配。这个习惯在后续的加法器、乘法器题目里能省大量的debug时间。
3.2 缩减运算与复制运算:看似简单但容易糊
缩减运算(Reduction Operators)和复制运算(Replication Operator)是HDLBits的高频考点。缩减运算就是把向量的所有位做与、或、异或,得到一个1位的值,比如assign parity = ^a;就是求奇偶校验位。复制运算则是{n{vector}},把一个向量复制n份然后拼接。
这些语法本身不复杂,但HDLBits会把它们嵌套到实际场景里考。比如有一道题是让你判断一个8位数是不是2的幂次方,正确思路就是:这个数只有1位是1,所以用缩减异或判断奇数个1不行,得用(x != 0) && ((x & (x-1)) == 0)这种技巧。诸如此类的题目都在提醒你:HDLBits考的不是语法,而是你能否把数学逻辑和位操作结合起来。
3.3 组合逻辑模块:加法器、多路选择器的"标准答案"套路
从Combinational Logic章节开始,题目变得真正有工程味了。加法器是这一章的绝对主角:半加器、全加器、行波进位加法器、进位选择加法器、BCD加法器,一层层加难度。
我的体会是,这类题目不要自己闷头从门级搭,直接在RTL层写数据流描述。比如全加器一行搞定:assign {cout, sum} = a + b + cin;,这样写既简洁又不会出错。但是,HDLBits有一部分题目是故意限制你只能用门级原语的,比如限定使用module full_adder的子模块来实例化,这时候你必须清楚全加器的门级构成:sum = a ^ b ^ cin; cout = (a & b) | (cin & (a ^ b));。
多路选择器题目则经常把case语句、三元运算符和generate语句混在一起考。比如让你用generate语句生成一个参数化的多路选择器,这种题目在真实项目中确实会遇到,HDLBits考它的目的就是让你熟悉generate的语法和参数化设计思维。
3.4 卡诺图与最简逻辑:揪出冗余项的工程价值
HDLBits的Karnaugh Map章节被很多人跳过,我觉得这是最不应该跳过的部分。题目会给出真值表或者卡诺图,让你写出最简逻辑表达式。表面上看是数电课的复习,实际上它考的核心是:你能否识别出无关项(Don't care)并利用它们化简。
比如有一题标准答案是out = a | b | c这种最简形式,如果你写out = (~a & b & ~c) | (a & ~b & ~c) | ...这种未化简形式,HDLBits照样判定正确,因为功能等价。但如果你在笔试面试中这么写,面试官大概率会追问一句"能不能化简"。所以刷这些题时,我建议你逼自己写出最简表达式,甚至可以先用卡诺图画一遍再写代码。
4. 时序逻辑和计数器:时钟、复位、阻塞赋值的分水岭
进入Sequential Logic章节,HDLBits的难度会有一个明显的跳跃。这一章的核心考点不是语法本身,而是你对时序电路工作原理的理解:触发器何时采样数据、复位信号是同步还是异步、计数器计到最大值后如何回绕、移位寄存器怎么做到并行加载和串行输出。
4.1 同步复位与异步复位的选择逻辑
HDLBits的题目会明确要求这是同步复位还是异步复位,这是最容易答错的第一关。以异步复位为例,标准模板是:
always @(posedge clk or posedge areset) begin if (areset) q <= 0; else q <= d; end同步复位则是:
always @(posedge clk) begin if (sreset) q <= 0; else q <= d; end写着容易,但很多人在做"同步复位与异步释放"相关的题目时会踩坑。HDLBits里有专门一题叫"Synth"系列,要求你用门级电路实现某个同步/异步复位逻辑,那时候你就不能靠always块模板了,得老老实实画电路结构。
另外要注意,非阻塞赋值<=是时序逻辑的标配,这一点HDLBits的仿真器不会标错,但在综合和笔试里是红线。如果你在时序块里用阻塞赋值=,即使仿真结果碰巧是对的,也属于不规范写法,面试官看到会直接扣分。
4.2 计数器的回绕与限定范围
计数器是HDLBits的出题大户,比如让你设计一个0到11的计数器、一个0到999的BCD计数器、一个在特定使能信号下才自增的计数器。这类题的核心就一个:什么时候清零或回绕。
我的建议是每次写计数器之前,先在注释里写下最大值和模值。比如0到11的计数器,最大值是11,所以每到11并且使能拉高时,next_state就归0。如果你不写注释直接写归纳公式,很容易把边界条件写错——比如写成了if (count == 12)才清零,那这计数器就会在12这个状态待一个周期才回0,输出波形和标准答案就对不上了。
还有一类带使能和加载(load)的计数器,加载信号优先级通常高于使能,但低于复位。这个优先级的判断几乎每道题都会考,你只要记住一个原则:异步复位最高,同步加载次之,使能最低,基本不会错。
4.3 移位寄存器:串行进、并行出、环形移位的组合拳
移位寄存器题目会把你对bit操作的能力提升一个档次。HDLBits里有左移右移、算术移位、并行加载、串行输入并行输出、以及4位环形移位等一堆变体。核心语法就两个:拼接操作符和移位操作符。
比如一个典型的左移移位寄存器:"左移1位,新数据从低位进入",RTL写法是q <= {q[3:0], in};,注意这里q声明为4位,拼接后赋值给q会自动截断最高位,正好实现丢弃最高位的左移。
环形移位则不同,它要求q <= {q[0], q[3:1]}这种把最低位搬到最高位的做法。我见过不少人在环形移位题目上栽跟头,因为把拼接顺序写反了,导致移位方向反了。建议这类题目在写之前先在草稿纸上写一个4位数,手动按位挪一遍,明确目标位宽下最高位去了哪里,再写代码。
4.4 有限状态机章节:从Moore到Mealy的完整破题流程
HDLBits的FSM部分大概有20多道题,从简单的state diagram理解,到序列检测、复杂状态机、多周期状态机都有。我认为这是全题库最有价值的一部分,也是面试最常考的部分。
破题流程我总结为四步:
- 画状态转移图。明确每个状态代表什么、输入有哪些、输出有哪些;
- 确定状态编码。HDLBits没有强制要求,你可以用
localparam定义状态名,也可以用独热码,但推荐用参数化的localparam,可读性最好; - 写三段式状态机。第一段时序逻辑做状态转移,第二段组合逻辑算次态,第三段组合逻辑算输出;
- 检查输出是否和时钟对齐。Moore型输出在状态变化后一个周期更新,Mealy型输出在输入变化时可以直接变化,这两者的波形差异是HDLBits最容易判定你错误的地方。
以经典的"101序列检测器"为例,用Moore型设计你需要3个状态:
- S0:等待第一个1;
- S1:已收到1,等待0;
- S2:已收到10,等待1,此时输出拉高。
只有进入S2且下一个时钟沿到来时输出才拉高。如果你用Mealy型,在S2状态下输入为1的瞬间就可以拉高输出,不用等下一个时钟。HDLBits的有些题目会特别标注"输出要在状态A期间保持拉高",这时候就必须选Moore。
5. 再上一层:构建电路、仿真验证和可综合设计中的"暗坑"
从Building Larger Circuits开始,题目不再是单一模块,而是要求你组合多个子模块,涉及计数器级联、状态机+计数器协同、甚至完整的8位CPU数据通路。这一章是真正的"总复习",很多题目的难度已经逼近产业界初级工程师面试的coding题。
5.1 模块实例化的参数传递:genvar、parameter用对了吗
HDLBits在较大型电路题目里经常要求你生成多个子模块实例,比如生成32个全加器组成一个行波进位加法器。这时你必须掌握generate语法:
genvar i; generate for (i = 0; i < 32; i = i + 1) begin : adders full_adder fa_inst(...); end endgenerate注意genvar是生成变量,不能直接当普通变量用,for语句的循环变量必须是genvar类型。很多人在这一章翻车不是因为逻辑不对,而是generate语法写错——比如漏了begin标签或者把genvar写成了integer。
5.2 真值表的另一种解读:行为描述与结构描述的关系
HDLBits里有若干题要求你"仅使用真值表和MUX实现某个逻辑函数",这其实是引导你理解行为描述与结构描述的联系。这种题最稳妥的做法是把输入变量当作选择器的地址线,把真值表的输出列当作数据输入线的常数。比如4输入真值表,用4to1 MUX,把输入[1:0]接到选择端,真值表输出依次接到数据端,就完成了功能。如果你对这种题目感觉无从下手,说明你对"电路结构"和"行为描述"的对应关系还比较模糊,建议回头再复习一下卡诺图章节。
5.3 仿真波形的解读:HDLBits的唯一调试工具
HDLBits没有像Vivado那样强大的波形调试界面,但它每次测试完会提供仿真波形,你可以通过点击节点查看特定时刻的信号值。这道题不过的时候,我强烈建议你点开波形图,把输入信号和你的输出信号对齐逐周期看。
我自己常用的调试流程是这样的:先看什么时候开始出错,然后在出错时刻往前倒推三四个周期,看输入序列和你的状态机当前状态是否匹配。比如序列检测器题目,如果你在第三个1的时候输出了高电平,但标准答案是在第四个周期才输出,那你多半是把状态机的状态数量设计少了,提前进入了目标状态。
还有一点:HDLBits允许你自己写testbench(在仿真页面里有对应的tab),但大多数题目不需要你写。如果遇到极其棘手的bug,你可以把模块代码复制到本地Vivado或Verilator里,写一个简单testbench直接仿真看波形,比在网页上反复猜效率高得多。
5.4 多周期状态机的边界:缺少超时机制会怎样
Building Larger Circuits里面有一类"多周期计数器+状态机"协同的题目,例如"设计一个在一定周期后才给出有效输出的模块"。这种题目的典型坑是:状态机进入某个状态后,计数器何时清零、何时溢出,必须和状态机配合得天衣无缝。
我提醒一个点:状态机的所有状态都必须有明确的跳转条件,不允许出现未定义状态。HDLBits的仿真器不会显式告诉你"进入了未知状态",但你的输出会变成X(未知值),波形上一眼就能看出来。如果你的输出是X,先检查是不是有没赋初值的reg变量,或者case语句漏掉了default分支。
6. 一道完整题目的解题示例:从读题到通过的完整链路
前面讲了不少方法论,这里拿一道典型的题目拆开走一遍全流程。以"Countbcd"这道题为例(4位BCD计数器,0到9999循环),它要求实现一个4位十进制计数器,每位BCD码都是0到9,当低位进位时高位加1。
读题后我的第一反应是拆解成子模块:个位计数器、十位计数器、百位计数器、千位计数器。每一个计数器都是0到9,只是使能条件不同。个位始终在ena和reset条件下计数;十位只有在个位等于9并且ena拉高时才计数;百位只有在十位等于9、个位等于9并且ena才计数;千位以此类推。
在代码层面,我写了4个always块,每个都维护一个0到9的计数器,进位条件用组合逻辑计算出来:
wire h_en = (count[3:0] == 4'd9); wire t_en = (count[7:4] == 4'd9) & h_en; wire th_en = (count[11:8] == 4'd9) & t_en;然后每位计数器在对应的使能条件下加1,遇到9就清零。这种"层次化拆解+独立计数"的思路,是HDLBits在大型设计部分想让你掌握的工程化思维方式。如果你试图用一个巨大的状态机来模拟4位BCD计数器的状态转换,会非常痛苦,而且几乎必然出错。分层永远是解决复杂逻辑的第一原则。
这道题实际提交第一次没过,原因是我忽略了复位优先级。题目要求同步复位,但我把复位放在了if分支的最内层,导致复位信号被ena挡住。改成在最外层判断复位,内部再判断使能,就通过了。这类复位优先级错误极其常见,刷题时记住一个口诀:"复位最先,使能其次,计数最后。"
7. 关于刷题顺序和刷完后如何保持手感
7.1 推荐的刷题路线图:三轮递进法
第一轮:顺序刷完Verilog Language和Combinational Logic,目标是把语法基础打牢,不追求全对,拿到80%正确率就可以进入下一章。这一轮大概需要一到两周。
第二轮:认真刷Sequential Logic和Building Larger Circuits,每一道题都要画状态图或时序图,提交前先在纸上写一遍思路。这一轮最耗时,但也是收获最大的阶段。大概需要一个月左右。
第三轮:回头重刷全部题目,把之前因为运气好通过的题重新独立做一遍。这次每一题都要以"面试现场白板写代码"的标准要求自己,尽量不用localparam只看题目端口就直接写。这一轮大概两周能完成。
7.2 刷题时的错题整理方式
我建议每道题通过之后,在笔记里记录三样东西:题目考点、我的错误原因、标准解法的关键一步。
比如"Exams/review2015_fsmonehot"这道题,我的笔记写的是:"考点:独热码状态机的编码方式、输出逻辑直接连状态位。错误原因:写了额外的次态逻辑但没用上。关键一步:独热码的输出y[0] = state[0]直接由状态位驱动,不需要组合逻辑。"
这种整理方式比抄代码有用得多。到了面试前,你不需要重新刷一遍题,只需要翻笔记过一遍考点和易错点,效率非常高。
7.3 保持手感:每周挑3道旧题重做
刷完一轮之后你会发现,Verilog的语法和设计思路如果不持续使用,两三个星期就会生疏。我的做法是每周挑3道以前做过的题重写一遍,不限时,但要求一次提交通过。这比反复刷新题更有利于巩固记忆,因为旧题的重做能检验你是否真的理解了当时"为什么这么写"。
8. 刷题之外:HDLBits对你的真实收益上限在哪里
如果只是把HDLBits当成一个题库刷完就扔,那它给你的价值也就只是笔试加分。但如果你愿意把它当做一个设计思维的训练场,它的收益上限其实高得多。
我举一个例子。HDLBits里的"Exams/m2014 q4b"系列和"Exams/2014 q3c"系列,这些标着Exams的题目实际上是MIT、伯克利等高校数字电路课程的往届试题。这些题目的特点是:题干信息非常少,只给你一组输入输出关系,让你反推内部逻辑。这类题逼着你从行为倒推结构,这正是真实芯片设计中"推理他人模块接口行为"的雏形能力。
再比如"Legacy"章节里的题目,很多都是早年面试题或者经典教材习题,比如"Moore状态机输出滞后问题""Mealy状态机毛刺问题的避免",这些在教科书上只是概念描述,但你在HDLBits上写完代码、跑了仿真之后,才真正理解"为什么Mealy输出有毛刺、Moore相对安全"这类工程判断背后的逻辑。
我自己刷完HDLBits最直观的变化不是Verilog写得快了,而是写代码之前脑子里先有电路结构。以前看到一个需求,第一反应是"这个用if-else怎么写",现在第一反应是"这个用哪几种基本模块搭、状态怎么切、时序怎么对齐"。
所以我的最后一条建议是:不要把"答案汇总"当成最终目标,而是把每道题的标准答案当作一面镜子,照出你自己在电路设计思维上的盲区。这290道题刷完,你不是多背了290个模块,而是多建立了一套从行为到结构的翻译能力,这套能力在后面的笔试面试、乃至真实的工程开发中,都会持续地给你回报。