2.1 组合逻辑与数据通路


2.1 组合逻辑与数据通路

本节摘要:组合逻辑是"输入一变、输出立刻跟着变"的电路,不依赖时钟、不存储状态。本节围绕 Verilog 描述组合逻辑的两种方式——assign 连续赋值与 always 组合块展开,讲清位宽、阻塞赋值与并发语义,最后给出一条区分组合与时序的判据。读完你能独立写出多路选择器、译码器、比较器等数据通路构件。

一句话先立规矩

数字电路只有两种基本逻辑:组合逻辑——输出只由当前输入决定,像一台没有记忆的机器;时序逻辑——输出还取决于过去的状态,靠时钟和触发器记住历史。本章 2.1 管前者,2.2 管后者。这条分界线以后每次写代码都要先问自己:我这段代码需不需要"记住"?不需要,就是组合逻辑。

组合逻辑的物理形态,就是 1.2 讲过的 LUT 网络:输入经过几张 LUT 的级联,直接得到输出。Verilog 描述它有两种姿势,我们一一来过。

第一种姿势:assign 连续赋值

assign 是描述组合逻辑最直观的方式——它表达"这条线永远等于右边表达式的结果"。右边的输入一变,左边立刻更新,不需要时钟参与。

// 一个 4 位二选一多路选择器:sel 为 0 输出 a,为 1 输出 b module mux2( input [3:0] a, input [3:0] b, input sel, output [3:0] y ); assign y = sel ? a : b; // 条件表达式:sel 为真取 a,否则取 b endmodule

assign 的好处是一目了然:右侧表达式能写什么,几乎就等价于你能搭出什么组合电路。再来一个稍复杂的例子,把前面 1.2 的 LUT 概念接上:

// 4 位全加器的组合实现:sum 为和,cout 为进位输出 module adder4( input [3:0] a, input [3:0] b, input cin, // 低位的进位输入 output [3:0] sum, output cout ); wire [4:0] tmp; assign tmp = {1'b0, a} + b + cin; // 扩一位,防止进位被截断 assign sum = tmp[3:0]; assign cout = tmp[4]; endmodule

注意 {1'b0, a} 这个写法:它是位拼接,把 1 个 0 拼到 a 的高位,扩成 5 位再做加法。这是处理进位的标准动作。如果你直接 assign {cout, sum} = a + b + cin; 也可以,效果相同——左边拼接表示把结果拆成进位和低四位。

⚠️ 常见坑:位宽不匹配是静默杀手assign y = a + b; 如果 y 比 a+b 窄,超出部分直接丢弃且不报错。高位的进位说没就没了。设计时先估算结果位宽,或者用拼接显式声明进位位。

第二种姿势:always 组合块

组合逻辑更复杂的形态——比如需要分支、多条件判断——用 assign 写会绕,这时用 always @(*) 组合块。星号表示"敏感列表自动推导:所有被读到的信号",输入一变化就重新计算块内逻辑。

// 用 always 组合块写一个 4 位比较器:输出三种比较结果 module cmp4( input [3:0] a, input [3:0] b, output reg gt, // a 大于 b output reg eq, // a 等于 b output reg lt // a 小于 b ); always @(*) begin if (a > b) begin gt = 1'b1; eq = 1'b0; lt = 1'b0; end else if (a == b) begin gt = 1'b0; eq = 1'b1; lt = 1'b0; end else begin gt = 1'b0; eq = 1'b0; lt = 1'b1; end end endmodule

这个例子藏着组合逻辑的铁律always @(*) 里每个分支都必须覆盖所有输出变量的赋值。如果你漏写 lt = 1'b0,综合工具推断不出该输出在某个分支下的取值,会自动生成一个锁存器(电平敏感的存储单元)来"记住"上一次的值——这在组合逻辑里是要出事的,时序不可控、和后端实现行为不一致。

综合工具会给出 warning,但很多初学者当警告是噪音就忽略了。记住这条守则:组合 always 块里,要么每个分支赋全,要么开头给所有输出赋默认值。业界常用的防呆写法是先赋默认值再写分支:

always @(*) begin gt = 1'b0; eq = 1'b0; lt = 1'b0; // 默认全 0 if (a > b) gt = 1'b1; else if (a==b) eq = 1'b1; else lt = 1'b1; end

case 与 z 态:别把不定态当 bug

case 适合译码器这类多分支结构。先看一个 7 段数码管译码器(输入 4 位 BCD,输出 7 段):

// 7 段数码管译码器:输出 7 位,分别点亮对应段 module seg7( input [3:0] bcd, output reg [6:0] seg ); always @(*) begin case (bcd) 4'd0: seg = 7'b0111111; // 显示 0 4'd1: seg = 7'b0000110; // 显示 1 4'd2: seg = 7'b1011011; // 显示 2 4'd3: seg = 7'b1001111; // 显示 3 4'd4: seg = 7'b1100110; // 显示 4 4'd5: seg = 7'b1101101; // 显示 5 4'd6: seg = 7'b1111101; // 显示 6 4'd7: seg = 7'b0000111; // 显示 7 default: seg = 7'b0000000; // 其余输入灭灯 endcase end endmodule

case 里有两个易错点:一是分支必须覆盖所有可能输入,否则同样触发锁存器推断——所以 default 分支不是可选项;二是 4 位输入存在 4'd104'd15 这些"非法"值,default 把它们统一兜住。这和软件的 switch 不一样,硬件没有"没匹配就跳过"的说法。

再讲一个新手常被吓到的东西:仿真波形里出现的 x(未知)和 z(高阻)。x 表示仿真器不知道是 0 还是 1——通常是信号未初始化或时序没对好;z 是高阻,来自三态门 inout 端口,表示这根线当前没人驱动。组合逻辑里 z 是合法状态(总线仲裁时常见),x 几乎总是 bug 信号,见到先查初始化。

组合与时序:一张判定表

判据 组合逻辑 时序逻辑
是否依赖时钟 否,输入变立即变 是,只在时钟沿更新
是否记住状态 是(触发器)
典型代码 assign、always @(*) always @(posedge clk)
赋值方式 阻塞 = 非阻塞 <=
综合产物 LUT 网络 触发器 + 组合部分

为什么时序块必须用 <=?因为 <= 描述的是"这一拍采样、下一拍生效"——所有触发器在同一时钟沿各自采样,互不干扰。若用 =,块内语句会串行执行,第二个语句能读到第一个语句刚改的值,这描述的就不是寄存器更新,而是组合逻辑链,综合出来的电路和你的意图完全对不上。保命规则:时序块 <=,组合块 =,混用必出错。

💡 关键直觉:写 Verilog 时时刻想着"这段代码最终长成什么电路"。写组合逻辑就是搭数据通路,写时序逻辑就是安排记忆单元。脑海里没有电路图,代码就是无根浮萍。

本节要点回顾

  • 组合逻辑两特征:无时钟、无状态;输出只由当前输入决定。
  • 两种描述姿势assign 适合单表达式,always @(*) 适合多分支,二者综合结果等价。
  • 锁存器防呆:组合 always 块要么分支赋全,要么先写默认值;case 必有 default。
  • 位宽是责任:结果位宽要估算,进位要显式扩位,静默截断不报错最阴险。
  • 赋值符号分工:时序 <=、组合 =,这是"仿真行为等于综合电路"的保障。
  • x 与 z:x 几乎总是 bug,z 是三态合法状态,别混为一谈。

下一步进入 2.2:组合逻辑搭好了数据通路,但要"记住"状态还得靠时钟——时序逻辑与有限状态机正式登场。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U