
第一次看到这道题的时候我差点直接按老套路写了一个q q 1的计数器结果题目要求里明晃晃写着“use four d-type flip-flops”和“use assign statements for the next-state logic”让我意识到这不是一个普通的模12计数器。它是 HDLbits 里 Exam/ece241 2014 q7a 这道题表面是“Counter 1-12”实际上考的是状态表、卡诺图化简、手动推导次态逻辑这一整套基本功。这道题很适合正在刷 HDLbits 的初学者尤其是已经会写基础计数器、但想搞明白“硬件里组合逻辑到底怎么来的”的人。如果你只会用 always 块让综合器替你做逻辑那做完这道题你会对数字电路底层多一层理解。本文我会从头还原题目拆解题意给你完整的卡诺图推导过程再附上一份可以直接跑的 Verilog 实现最后聊聊我踩过的坑和调试心得。1. 题目还原与解题目标1.1 题干到底在要求什么先说原题。Exam/ece241 2014 q7a 的题干大意是设计一个计数器从 1 计数到 12然后回转再次从 1 开始使用同步复位复位时计数器回到 1输出 q 就是当前计数值电路必须使用四个 D 触发器次态逻辑必须用 assign 语句和组合逻辑实现。很多人在这一步就卡住了因为它的限制条件比普通计数器多。第一计数范围不是 0 到 11而是 1 到 12虽然都是 12 个状态但状态编码完全不同复位值也不是 0。第二它明确要求“four d-type flip-flops”其实就是让你在顶层模块用四个触发器构成一个 4 位寄存器而不是直接写一个always (posedge clk)然后让综合器自己去推断 D 触发器和加法器。第三要求“use assign statements for the next-state logic”也就是说你需要显式推导出每个触发器的 D 输入表达式再用 assign 把它赋给寄存器的次态信号。这意味着你不能用always *case 语句也不能让加法器自动产生次态而是要亲手把“当前状态 q 到下一状态 next_q 的映射”转换成布尔表达式。这道题核心是状态表与卡诺图的应用。1.2 为什么不能无脑写计数器如果你直接写下面这种代码功能上完全正确也能在波形仿真里跑出 1 到 12 的循环但它不满足教学目的always (posedge clk) begin if (reset) q 4d1; else if (q 4d12) q 4d1; else q q 1b1; end这个写法的问题在于综合工具会自己生成一个 4 位加法器然后通过比较器判断是否为 12再通过多路选择器决定下一次装载什么值。这些组合逻辑是工具自动推断的不是你自己设计出来的。题目要求你手动推导“next-state logic”本质上是要你把这张表用布尔代数写出来。换句话说这道题考的不是你会不会写 Verilog 语法而是你会不会做数字逻辑设计。它把“编码”“状态转换”“卡诺图化简”“触发器建模”这几件事全部串在一起。这个思路迁移到实际项目中就是你遇到一个非标准序列计数器或状态机时能自己用手工画状态表、推表达式而不是只会依赖综合器的操作。2. 状态编码与设计思路2.1 计数范围与复位边界计数范围是 1 到 12这意味着我们使用 4 位输出就够4 位二进制能表示 0 到 1512 个有效状态分别对应十进制q3q2q1q0100012001030011401005010160110701118100091001101010111011121100这里必须注意两个边界。一是复位值不是 0000而是 0001如果你习惯性把复位写成q 4d0那你一开始就会比正确答案差一个节拍。二是计数到 12 之后的下一状态是 1而不是 13 或 0所以状态 12 的次态比较特殊需要单独处理。有效状态之外还剩下 0、13、14、15 四个未使用状态。题目没有规定这些状态的行为我们在卡诺图化简时可以把它们当作 dont care 处理这样能圈出更大的组得到更简单的表达式当然如果你担心上电乱飞也可以让它们导向复位值但那会让表达式变长很多。2.2 为什么用状态表加卡诺图从 1 到 12 的计数器如果只是描述功能写一个 always 块加判断就够了但题目硬性要求手动推导组合逻辑。这时候状态表是最直观的工具把当前状态作为输入把下一状态作为输出列出 12 个有效状态对应的 4 位次态。然后问题就从“写代码”变成“求贝尔函数”。卡诺图的价值是把你从琐碎的代数化简里解放出来。4 个变量的卡诺图是 4x4 的格子只要把每个次态位为 1 的最小项标进去就可以通过画圈合并相邻项。这个方法比直接做布尔代数化简快很多而且不容易出错。我在实际做题时是先画状态转换表然后为 d3、d2、d1、d0 各画一张卡诺图最后再翻译成 assign 表达式。这里有一点很关键卡诺图化简的时候行列变量顺序必须按格雷码排列也就是 00、01、11、10这样相邻格子才只有一个变量变化圈出来的 2、4、8 个格子才能正确消去变量。如果你画成普通的二进制顺序 00、01、10、11那卡诺图就废了圈出来的项大概率是错的。2.3 同步复位与异步复位的选择题干明确写的是 synchronous reset也就是复位信号只对时钟上升沿敏感。所以在 always 块里复位判断必须在posedge clk之内写成always (posedge clk) begin if (reset) q 4d1; else q next_q; end同步复位的好处是电路结构简单不容易产生亚稳态设计时序约束也方便缺点是复位信号的有效宽度必须至少保持一个时钟周期否则可能采不到。异步复位在 HDLbits 其他题目里也常出现写法是always (posedge clk or posedge reset)但这道题不要这么写。如果搞混同步和异步在仿真时复位行为可能看起来差不多但综合后的电路完全不同。异步复位会额外给每个触发器加入复位引脚连接同步复位则是把复位信号当作普通数据输入的一部分通过多路选择器接入 D 端。这道题要求同步复位我建议每次都先读题干中的英文关键词“synchronous reset”还是“asynchronous reset”再决定怎么写。3. 从状态表到 assign 逻辑3.1 建立完整的状态转换表设当前状态为 q[3:0]下一状态为 next_q[3:0]其中 next_q 的每一位就是一个 D 触发器的输入。我们把 12 个有效状态全部展开当前状态下一状态d3d2d1d0000100100010001000110011001101000100010001010101010101100110011001110111011110001000100010011001100110101010101010111011101111001100110000010001注意最后一行状态 12 的下一状态是 1所以 d3 从 1 变 0d0 从 0 变 1其他位也都是 0。这一行最容易写错尤其是 d0如果不小心把 next_q 直接写成q 1那状态 12 会跑到 13整个计数器就崩了。接下来就是分别针对 d3、d2、d1、d0 写卡诺图化简。下面我会把过程完整列出来你可以拿着笔自己画一遍再对照我的结果。3.2 d3 与 d0 的化简d3 是下一状态的最高位。从状态转换表看d3 在以下当前状态时取 10111、1000、1001、1010、1011在 1100 时取 0。画出卡诺图后可以圈出两组第一组1000、1001、1010、1011对应条件是q[3] ~q[2]也就是 q3 为 1 且 q2 为 0这四项合并为一项第二组0111单独一项对应条件是~q[3] q[2] q[1] q[0]。所以d3 (q[3] ~q[2]) | (~q[3] q[2] q[1] q[0]);验证一下状态 111? 只有 1100 时 q31、q21第一项为 0第二项因为 q31 也为 0所以 d30正确。状态 0111 时第一项为 0第二项为 1d31正确。这个表达式不复杂而且只用了 2 个与门和 1 个或门。d0 是下一状态的最低位。只看状态转换表会发现一个规律在有效状态 1 到 12 里d0 恰好等于 q0 取反。也就是说当前状态 q0 如果是 1下一次 q0 一定是 0当前状态 q0 如果是 0下一次 q0 一定是 1。你可以自己核一圈从 0001 到 1100 基本每两个状态跳一次只有 0000、1101、1110、1111 这些无效状态不在讨论范围内。所以 d0 直接写成d0 ~q[0];这个结论看起来简单到像作弊但推导过程是严谨的。如果你不想依赖观察规律也可以照常画卡诺图最后同样会得到这个结果。3.3 d2 与 d1 的化简d2 是 4 个表达式里最麻烦的一个。从状态表看d2 在以下当前状态时取 10010、0101、0110、1001、1010。把这几个最小项标到卡诺图上如果只圈存在的 1表达式会很碎。这里可以用一个技巧在这 5 个状态里除了 0010 是 q11、q00 外其余 4 个状态其实都可以按 q1 和 q0 的组合拆成两组。0101 和 1001 都是 q10、q010110 和 1010 都是 q11、q00。再结合 q3、q2 做化简并利用无效状态 1101、1110、1111 作为 dont care我最后得到d2 (q[1] ~q[0] (~q[3] | ~q[2])) | (~q[1] q[0] (q[3] | q[2]));这个表达式可能不是唯一解因为 dont care 的取法会影响最终化简结果。你只要保证有效状态全部正确就完全能通过题目验证。d1 也可以找出规律。观察 d1 为 1 的状态0001、0010、0101、0110、1001、1010。你会发现这 6 个状态都有个共同特点就是 q0 和 q1 不相同也就是q0 ^ q1 1。再检查那些 q0^q11 但不是 d11 的状态唯一例外是 0001? 不对0001 本身 d11再往下看0001、0010、0101、0110、1001、1010 全部满足 xor1而没有其他有效状态满足 xor1。所以 d1 可以先写成d1 (q[0] ^ q[1]);但别忘了q0^q11 的无效状态还有 13、14、15 之类如果我们不约束它们综合时这些 dont care 可能乱跳。再检查有效状态中 q0^q10 时 d1 确实都是 0所以这个表达式功能上没问题。不过为了让无效状态也稳定一些或者让卡诺图化简更规范我最终写成d1 (q[0] ^ q[1]) (~q[3] | ~q[2]);加上(~q[3] | ~q[2])后状态 13、14、15 在这些位组合下会被过滤掉逻辑更保守。当然如果你追求最小化也可以直接用q[0] ^ q[1]仿真波形是一样的因为测试向量不会进入无效状态。3.4 完整可综合 Verilog 代码把上面四个表达式组合起来得到一份完整的答案。我习惯把次态逻辑命名为 next_q并用 wire 声明让组合逻辑和时序逻辑彻底分开module top_module ( input clk, input reset, output [3:0] q ); wire [3:0] next_q; assign next_q[3] (q[3] ~q[2]) | (~q[3] q[2] q[1] q[0]); assign next_q[2] (q[1] ~q[0] (~q[3] | ~q[2])) | (~q[1] q[0] (q[3] | q[2])); assign next_q[1] (q[0] ^ q[1]) (~q[3] | ~q[2]); assign next_q[0] ~q[0]; always (posedge clk) begin if (reset) q 4d1; else q next_q; end endmodule这段代码里always块只负责把 next_q 锁存进四个触发器复位时装载 0001其他时刻每个时钟沿都从 next_q 更新。组合逻辑全部由 assign 完成完全满足题干要求。如果你想让输出 q 不直接暴露内部寄存器也可以多写一个输出寄存器但按本题要求output [3:0] q直接接触发器输出即可没有必要再打一拍。4. 常见错误与调试技巧4.1 复位值写成 0 导致差一拍这是最典型的错误。普通计数器几乎都是从 0 开始但本题明确要求复位到 1。如果你写成q 4d0仿真波形会从 0、1、2 一路数到 12然后从 12 回到 0和正确波形的相位差一个周期。更难受的是计数器本身功能看起来是循环的很多人会误以为只是初始相位问题结果在后续状态判断上越改越乱。解决办法是在读题时圈出 reset value。HDLbits 的题目描述一般都会有一句类似“Reset should set the counter to 1”的话不要凭惯性写。4.2 状态 12 的次态处理错误很多人在写行为级代码时会用if (q 4d12) q 4d1;这个逻辑没问题。但当你改成 assign 次态表达式后最容易忘记 d0 在状态 12 时应该输出 1。因为状态 12 的编码是 1100按普通二进制加法12 加 1 是 13也就是 1101但我们需要回到 0001。也就是说从 1100 到 0001 的变化并不是单纯的 1而是一个“环绕跳转”。我在推导 d0 时也是反复确认了这个点才敢写下d0 ~q[0]。你如果手推状态表务必把最后一行 1100 - 0001 单独标注出来四个 d 触发器每一位都要重新核对。4.3 无效状态引发的仿真差异卡诺图化简时如果利用了 dont care 圈项得到的表达式只能在有效状态上保证正确。比如我上面 d2 的写法就默认把 1101、1110、1111 当作无关项处理。如果你在仿真里手动把 q 置成这些无效状态计数器行为可能不符合预期但这并不影响题目得分因为测试平台不会自动进入无效状态。不过在实际工程项目中没人敢这么赌。更稳妥的做法是给无效状态加一个统一的转移目标比如让它们全部回到 0001。代价是表达式会明显变长卡诺图化简时也不能再利用那些 dont care。这属于“功能正确”和“逻辑最简”之间的权衡在 HDLbits 里优先追求最简表达即可在真实芯片设计里建议加上安全处理。4.4 仿真验证时怎么测写完代码后我强烈建议在 HDLbits 自带的 testbench 之外自己额外做一个时序检查。关键观测点是复位后第一个时钟上升沿q 是否为 0001q 从 0001 数到 1100 是否连续、无跳变在 q1100 的下一个时钟沿q 是否直接回到 0001持续跑几十个周期q 是否始终只在 1 到 12 之间循环从不出 0、13、14、15。如果你是本地用 Verilog 仿真可以在 testbench 里用一个$display判断非法状态always (posedge clk) begin if (q 4d0 || q 4d12) $display(ERROR: invalid state %d at time %t, q, $time); end这样只要计数器一进无效状态仿真器立刻报错比盯波形图高效得多。4.5 一份 HDLbits 答案的使用建议很多人在刷题时会搜“hdlbits 答案”或者“hdlbits 答案及思路”我也干过。但我建议拿到答案后别直接抄先把答案里的 assign 表达式重新推导一遍对照状态表的每一行验证。因为这道题真正的收获不是记住那四行表达式而是学会“从状态表到卡诺图再到布尔表达式”的方法。一旦你掌握了往后遇到计数范围不同的、计数序列不连续的题目都能快速设计出来。5. 把思路迁移到其他计数器设计5.1 如果改成 Counter 0-12 怎么改假设题目改成从 0 数到 12其他条件不变。那状态就从 0000 到 1100一共 13 个状态复位值也变成 0000。这时状态 12 的次态是 0000状态 0 的次态是 0001整体状态表会更“自然”一些但 d3、d2、d1、d0 的表达式会重新变。你依然要画四张卡诺图只是原来 0001 是起始状态、0000 是无效状态现在反过来。所以不要背表达式要背流程定状态编码、列状态表、画卡诺图、圈项化简、翻译成 assign。这套流程对所有计数器都通用。5.2 如果计数序列不是连续递增比如设计一个 1、3、5、7、9、11 的奇数计数器状态之间的跳变不再是 1那状态表会更散。这时候卡诺图的作用会更大因为每个次态位的 1 分布更稀疏你必须通过画圈去找公共项。实际操作中我还会先用一个always *配合 case 语句把 next_q 算出来然后让综合器综合再用公式化简工具去验证手工推导结果。考试场景下没有工具就只能老老实实画卡诺图。5.3 用相同方法设计序列状态机这个思路也不只用于计数器。任何“当前状态到下一状态”的映射理论上都能套用“状态表 卡诺图 assign”的方式实现。你甚至可以不用 always 块写状态机完全用 assign 加触发器组实现。虽然实际工程中大家更习惯写always *的 case 状态机但偶尔遇到 ASIC 综合或某些低功耗约束场景手动展开组合逻辑反而能更精细地控制电路结构。6. 我对这道题的实际体会刷 HDLbits 练到这道题之前我一直觉得计数器的次态逻辑是综合器顺手就生成了动手推卡诺图的次数屈指可数。但这道题逼着我把状态表、格雷码卡诺图、最小项合并这些“古老技能”重新捡起来做完之后最大的感受是你看代码的方式变了。以前看别人写的assign next ...只会觉得语法奇怪现在一眼就能看出它是哪几个状态的合并项能判断哪个地方可能踩了无效状态的坑。最后分享一个我自己的验证技巧把所有 assign 表达式写完后先不要急着跑全量仿真而是只跑 q 从 0001 到 1100 再回到 0001 这一圈。把每个时钟沿前后的 next_q 打印出来和手画的状态表逐行比对。这一步能帮你迅速定位到底是哪个 d 触发器的表达式错了也能让你对“为什么这个表达式长这样”有更清楚的认识。