从沙子到车辙(3.1):组合逻辑——没有记忆的计算

3.1 组合逻辑:没有记忆的计算

📚 本文内容摘自本人的开源书《从沙子到车辙 - 一个工程师的理解》

🔗 在线阅读/下载:from-sand-to-ruts

bash 复制代码
git clone https://github.com/Lularible/from-sand-to-ruts

⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。

一排开关,一颗 LED

你面前有一排开关和一颗 LED。

规则是:LED 只有在某两个特定开关同时按下时才亮。这俩开关------比如第 2 个和第 5 个------它们不挨着。你要怎么设计这个电路?

最简单的想法:用 AND 门把这两个开关信号 AND 在一起。但 AND 门只能接收两个输入。你有 8 个开关。

你需要组合逻辑

把 8 个开关的信号,经过若干层逻辑门处理后,驱动最终的 LED。这个电路的输出只取决于当前的开关状态------和之前的开关状态无关。你松开第 2 个开关,LED 立刻灭。你再按下,它立刻亮。不存在"刚才 LED 是亮的所以现在还是亮的"这种说法。

输出 = f(当前输入)。这就是组合逻辑------没有记忆的计算

组合逻辑是"没有记忆的计算"。就像阳光下的影子------它是实时投射的,不会记住5秒前你的姿势。你挡住光 → 影子立刻变。没有历史,没有上下文。

从真值表到硅片

门级基础

CMOS 工艺下,最基础的门不是 AND 和 OR,而是 NAND (与非)和 NOR(或非)。原因很物理:PMOS 和 NMOS 天然就是反相输出的。一个 NAND 或 NOR 用 4 个 MOSFET 就搭出来了。如果做 AND,得先 NAND 再反相------6 个 MOSFET,多出三分之一。

反过来用这个事实理解现代芯片:为什么 NAND Flash 叫"NAND"?因为它的存储单元阵列使用了 NAND 门结构来组织------本质上是在 CMOS 最省晶体管的结构上构建存储。名字不是随便取的。

AND 与 NAND 的物理本质

如果你用 C 来模拟逻辑门的行为,AND 和 NAND 的区别不过是一行取反:

c 复制代码
int nand(int a, int b) { return !(a && b); }
int and_gate(int a, int b) { return nand(nand(a, b), nand(a, b)); }

注意------我们用两个 NAND 接出了一个 AND。这在硅上意味着什么?每个 NAND 是 4 个 MOSFET,两个 NAND 是 8 个 MOSFET,外加连接一个 NAND 输出到另一个 NAND 输入的金属线。而如果我们直接在 CMOS 工艺里做 AND,标准做法是一个 NAND(4 个 MOSFET)后面跟一个反相器(2 个 MOSFET),共 6 个。为什么不是 8 个?因为 CMOS 反相器比 NAND 省晶体管------NAND 是串联两个 NMOS,反相器只需要一个 NMOS 和一个 PMOS。

这个细节给你一个直觉:用 NAND 和 NOR 作为基本门库,综合出来的电路通常比用 AND/OR 省面积 。所有的 EDA 综合工具------Synopsys Design Compiler、Cadence Genus------在内部都是先把你写的 RTL 转成 NAND/NOR 门级网表,再做逻辑优化,最后映射到标准单元库。你写的是 assign y = a & b;,工具看到的是两个 NAND 加一个 NOR 的某种拓扑。

全加器:二进制计算的原子

二进制加法的基本单元是一位全加器(Full Adder)。它吃三个输入------A、B、进位输入 C_in------吐出两个输出:和(Sum)与进位输出(C_out)。

你要把两个32位数加起来。但芯片不认识32------它只认识0和1。怎么用0和1做加法?先从最简单的开始:两个1位数相加,再加一个进位。

A B C_in Sum C_out
0 0 0 0 0
0 0 1 1 0
0 1 0 1 0
0 1 1 0 1
1 0 0 1 0
1 0 1 0 1
1 1 0 0 1
1 1 1 1 1

用逻辑表达式:

复制代码
Sum   = A ⊕ B ⊕ C_in
C_out = (A AND B) OR (A AND C_in) OR (B AND C_in)

现在用 C 把它实现出来------不是为了在芯片上跑,是为了理解它的行为:

c 复制代码
void full_adder(int a, int b, int c_in, int *sum, int *c_out) {
    *sum   = (a ^ b) ^ c_in;   // XOR 两次
    *c_out = (a && b) || (a && c_in) || (b && c_in);
}

// 测试所有 8 种输入
void test_full_adder() {
    int sum, c_out;
    for (int i = 0; i < 8; i++) {
        int a = (i >> 2) & 1;
        int b = (i >> 1) & 1;
        int c_in = i & 1;
        full_adder(a, b, c_in, &sum, &c_out);
        printf("A=%d B=%d Cin=%d → Sum=%d Cout=%d\n",
               a, b, c_in, sum, c_out);
    }
}

在硅片上,一个全加器的关键路径是从C_in→Sum------经过两级XOR门,大约3级门延迟。在40nm工艺下,一级门延迟约15-20ps,所以一个全加器从进位输入到和输出需要约50ps。而32位行波进位加法器------32个全加器串联------最坏情况下进位要从bit 0一路传到bit 31,总延迟约32×50ps=1.6ns。这就是为什么CPU里有超前进位加法器------用更多的逻辑门换更短的路径。这也是"有限资源"的第一个具体例子:面积换速度。

在硅上,一个全加器的关键路径是从 C_in → Sum 的标准门延迟大约 3 级(XOR → XOR)。从 C_in → C_out 更快,约 2 级(AND → OR)。这意味着进位在串联的全加器中传播时,每一级的延迟约为 2 个门延迟------这是行波进位加法器慢的根本原因。

把 N 个全加器串联------低位的 C_out 接到高位的 C_in------你就得到了行波进位加法器(Ripple Carry Adder)。它工作,但有问题:高位必须等低位的进位像波浪一样传播过来。N 位加法,N 级门延迟。

用 C 实现一个 4 位行波进位加法器:

c 复制代码
void ripple_carry_adder(int a[4], int b[4], int c_in,
                        int sum[4], int *c_out) {
    int carry = c_in;
    for (int i = 0; i < 4; i++) {
        int s, c;
        full_adder(a[i], b[i], carry, &s, &c);
        sum[i] = s;
        carry = c;
    }
    *c_out = carry;
}

// 演示:3 + 5 = 8
void demo_4bit_add() {
    int a[4] = {1, 1, 0, 0};  // 3 (LSB first): 0011
    int b[4] = {1, 0, 1, 0};  // 5 (LSB first): 0101
    int sum[4], c_out;
    ripple_carry_adder(a, b, 0, sum, &c_out);
    // sum = 0001, c_out=0 → 8
}

LSB first 的意思是第 0 位是最低位------这是在模仿硬件里比特的排列方式。在硅片上,加法器的进位方向是固定的:从 LSB 向 MSB 传播。你不能倒过来。

这就是为什么现代 CPU 改用超前进位加法器(Carry Look-ahead Adder):提前并行计算每一级的进位,用更多逻辑门换取更短的路径。用面积换速度------这个 trade-off 你会在整个计算机体系结构里反复遇见。

MUX 和译码器:数据流动的路由器

多路选择器(MUX)做一件事:从多个输入中选一个。

复制代码
if (S == 0) OUT = A; else OUT = B;    // 2-1 MUX

用两个选择信号 S1、S0,从 A、B、C、D 四个输入中选一个------4-1 MUX。

MUX 在 CPU 里无处不在。ALU 的操作选择是 MUX,寄存器文件写回的源选择是 MUX,跳转地址的选择是 MUX。把 CPU 的结构拆散了看,大部分硅面积都在 MUX 和导线上。

用 C 实现一个 8-to-1 多路选择器:

c 复制代码
int mux_8to1(int d[8], int s[3]) {
    int sel = (s[2] << 2) | (s[1] << 1) | s[0];  // 3-bit select
    return d[sel];   // 这和硬件里的一级译码逻辑完全等价
}

你可能会说:"这不就是个数组下标吗?"对------但硬件里没有"数组"。硬件里的 8-to-1 MUX 是三级的 2-to-1 MUX 树:第一层把 8 路选成 4 路,第二层把 4 路选成 2 路,第三层把 2 路选成 1 路。每级 2-to-1 MUX 由两个 CMOS 传输门组成。整个 8-to-1 MUX 消耗约 30~40 个 MOSFET。而你上面那行 C 代码 d[sel] ------如果编译器不优化------实际上隐含了地址计算、内存访问、寄存器 load 等数十条机器指令。在硬件里,MUX 就是直通的导线------零延迟的"选择"在物理上就是一组开关网络。

译码器(Decoder)做反向的事情:n 位输入 → 2^n 位输出。3-8 译码器:输入是 3 位地址,输出是 8 根线中唯一一根被拉高。用在指令译码(操作码 → 使能对应的功能单元)、寄存器选择、内存芯片选择(chip select)。

c 复制代码
void decoder_3to8(int addr[3], int out[8]) {
    for (int i = 0; i < 8; i++)
        out[i] = 0;           // 全部清零

    int sel = (addr[2] << 2) | (addr[1] << 1) | addr[0];
    out[sel] = 1;             // 只有选中那根线拉高
}

译码器在物理上由一个 AND 门阵列实现:输入是 3 位地址信号及其反信号(共 6 根线),输出是 8 个 3 输入 AND 门。每个 AND 门选通不同的地址组合。比如输出线 5(二进制 101)的 AND 门取反相后的 addr[1] 和原样的 addr[2] 和 addr[0]------只有地址 = 101 时这三个条件同时满足。这是"查找表"在门级的最纯粹形态。你的控制器就是一个巨型译码器------操作码进去,控制信号出来。

从 RTL 到门级网表

你写 Verilog 的时候写的是这样的:

verilog 复制代码
module alu (
  input  [7:0] a, b,
  input  [2:0] op,
  output reg [7:0] y
);
  always @(*) begin
    case (op)
      3'b000: y = a + b;
      3'b001: y = a - b;
      3'b010: y = a & b;
      3'b011: y = a | b;
      3'b100: y = a ^ b;
      default: y = 0;
    endcase
  end
endmodule

综合工具(比如 Design Compiler)把这一段 RTL 变成什么?大体过程是:

  1. 解析 case 语句 → 识别为一个 5-to-1 MUX(选加/减/与/或/异或的结果)加一个默认情况。
  2. 加法器 → 展开为全加器阵列,然后用超前进位或行波进位结构实现。
  3. MUX → 用标准单元库中的 MUX 单元(一组传输门或 AND-OR 逻辑)替换。
  4. 最终输出:一个门级网表------一堆 NAND、NOR、INV、MUX、FA(全加器标准单元)的实例,以及它们之间用金属线(net)连接的拓扑。

y = a + b 这一行 RTL,到最终硅片上几千个 MOSFET 的门级网表------中间经过了逻辑综合、技术映射、布局布线。你写的那行代码从来没有直接出现在硅片上,但它的语义被完整保留了。

物理缺陷如何变成逻辑错误

在芯片物理层,一个 NAND 门由 4 个 MOSFET 组成------两个 PMOS 并联上拉,两个 NMOS 串联下拉。

假设在制造过程中,有一颗亚微米级颗粒落在某个 NMOS 的栅极和衬底之间。在测试向量运行到涉及这个 NAND 门的路径时,会发生什么?

这颗颗粒可能在栅极氧化层中形成一个阻抗路径------等效于在栅极和衬底之间并联了一个大电阻。低频时,栅极电容还能正常充放电,NAND 门行为正常。但频率升高后,充放电时间不够,栅极电压到不了阈值------这个 NMOS 可能永远不导通,或者导通电阻变大。

于是这个 NAND 门就"退化"了------不是完全坏掉,而是在特定条件下输出错误。更隐蔽的是,它可能在 25°C 正常、-40°C 正常、125°C 时出错;可能在 VCC=3.3V 正常、VCC=3.0V 时出错;可能在相邻信号线没有翻转时正常、翻转时因串扰而出错。

这就是为什么车规芯片要做三温测试(-40°C、25°C、125°C)、做电压拉偏测试(VCC ±5%)、做 MBIST(Memory Built-In Self-Test)和 LBIST(Logic BIST)。做这些不是因为"质量要求高所以多测几遍",而是因为物理缺陷的显现是有条件的------一个潜伏的颗粒污染可能在某个特定的频率、温度、电压组合下才暴露为逻辑错误。

组合逻辑没有记忆------但如果门本身坏了,"没有记忆"的计算也会算出错误的结果。

关键路径:那条最慢的路

回到行波进位加法器。假设每个 AND 门延迟 20ps,每个 OR 门延迟 20ps,每个 XOR 门延迟 40ps。

在 4 位加法器中,进位 C_out 从第 0 位传到第 3 位需要穿过 4 个全加器的进位链。每位进位链的延迟大约为 AND(20ps)+ OR(20ps)= 40ps。4 级串起来------160ps 的进位传播延迟。再加上最后一轮 XOR 产生最终的 Sum[3](40ps),总路径延迟 ≈ 200ps。

而 Sum[0] 只需要 XOR → XOR 两级(共 80ps)就稳定了。

200ps vs 80ps------同一个加法器的不同输出位,稳定的时间不同。最慢的那条路径------从 C_in 到 Sum[3]------决定了这个加法器可以跑的最高频率:1 / 200ps = 5GHz。理论上可以跑到5GHz------但实际芯片还要考虑驱动、连线、时钟偏斜,所以真实的加法器频率低得多。

这还只是一个 4 位加法器。如果是 32 位加法器,进位需要传播 32 级,纯行波进位的关键路径延迟将超过 1.2ns------最高频率不到 800MHz。这就是为什么没有人用 32 位行波进位加法器来跑 CPU。超前进位加法器把进位提前并行计算出来,将 32 位的关键路径压缩到大约 log2(32) = 5 级逻辑深度,延迟压缩到约 300ps------3GHz 以上可以跑。

关键路径(critical path)就是组合逻辑电路中从任意输入到任意输出最慢的那条路。整个电路的最高运行频率由这条最慢的路决定。

你的 ECU 上的 CPU 之所以标称 300MHz 但实际只跑 200MHz------不是厂商定价策略决定的,是物理决定的。最坏情况下(最高温度、最低电压、最慢工艺角),那条关键路径延迟会从典型值的 3ns 膨胀到 5ns。你要为最坏情况留出足够的时序裕度------否则在那条关键路径上的某个全加器就会在进入下一级寄存器之前没算出正确结果。

你的车速是怎么算出来的

让我们接地气一点。

你的车上 ABS/ESP 控制器怎么知道车速?每个轮子上有一个轮速传感器------通常是霍尔效应或磁阻传感器,面对一个带齿的音轮。轮子转一圈,传感器吐出 48 个脉冲(典型齿数)。

ECU 内部发生的是一连串事件:

  1. 边沿检测 :捕捉脉冲的上升沿和下降沿------纯组合逻辑
  2. 定时计数 :硬件定时器在两个相邻脉冲沿之间统计时钟周期个数------时序逻辑(后面会讲)。
  3. 速度计算:轮子周长已知,齿间角度已知。速度 = 齿间角度增量 / 两脉冲沿间时间。这是算术------全加器、乘法器在干活。
  4. 滤波 :单次测量有噪声、齿槽机械公差、路面振动。需要滑动平均或低通滤波------MAC 运算(Multiply-Accumulate)。在 DSP 核上,单周期 MAC 是直接由一个组合逻辑块完成的。

一个看似简单的"车速 = XX km/h",在 ECU 内部经历了一条完整流水线------从模拟脉冲,到边沿检测,到定时器捕获,到 MAC 运算,再到 CAN 总线上的一帧报文。

如果任何一个全加器的进位链超时了------轮速就算不对。轮速不对,ABS 就可能误触发。

这就是为什么车规 MCU 要跑在远低于标称频率的实际频率上(留 timing margin),要做最坏执行时间分析(WCET),要给你看门狗来兜底。你在数据手册上看到的那颗 300MHz 的 Cortex-R5,在实际的发动机控制器里可能只跑 200MHz------因为 ASIL-D 要求的是确定性,不是峰值性能。

没有记忆,就没有上下文

但真正的"智能"------判断一个脉冲序列中有没有缺齿、判断车速是否在异常波动、判断 CAN 报文是否有重发------都需要记忆。需要把过去的输入和当前的输入联合处理。

在进入"记忆"之前,先致敬一下:加法器、多路器、译码器------这些东西,在 1950-60 年代是被当成"巨型计算机"的核心组件来设计的。 当时用真空管或分立晶体管搭的加法器,一个就占好几块电路板。今天,你的 Cortex-M4 里有几十万个加法器、几十万个 MUX,缩在一颗 5mm × 5mm 的硅片上。

60 年,从一间房到一片指甲盖。

从真空管加法器到你的Cortex-M4里的几十万个逻辑门------这是60年的接力。每一代工程师把逻辑化简方案递交给下一代,下一代在这个基础上搭出更复杂的逻辑。你现在用Verilog写下的每一行RTL,手里握着的就是这根接力棒的前端。


本篇小结

今天我们做了一件事:理解了组合逻辑------输出只取决于当前的输入,没有记忆的计算。

关键结论:

  1. NAND/NOR才是物理基础:CMOS工艺下,最基础的门是NAND和NOR,AND/OR是由它们拼接出来的------这是物理对逻辑的反向塑造。
  2. 全加器是二进制计算的原子:一个全加器用5个逻辑门实现1位加法,32个串联构成行波进位加法器------最慢的那条进位路径决定了CPU能跑多快。
  3. 物理缺陷会变成逻辑错误:一颗亚微米的颗粒,在特定温度/电压/频率下,能把一个NAND门变成间歇性出错的"退化门"------组合逻辑没记忆,但门本身会"坏"。

下一节,时序逻辑。我们从D触发器开始,给计算加上"记忆"。

【下集预告】

你盯着示波器,通道 2 上的数据信号在疯狂翻转。你说:"在下一个时钟上升沿,把这个 bit 存下来。"

这一刻,你在要求电路具有记忆。而"记忆"最基本单元,不过是用两个反相器首尾相接做成的那个小环------触发器。

下一节,时序逻辑。我们从 D 触发器开始。

相关推荐
DogDaoDao9 小时前
【AI Agent 深度解析】OpenHuman 开源项目全面分析 — 打造你的个人 AI 超级智能助手
人工智能·深度学习·开源·大模型·ai agent·智能体·openhuman
前端白袍9 小时前
AI+:OpenClaw:开源 AI Agent 框架的定位与技术分析
人工智能·开源·openclaw
星栈9 小时前
Rust WASM 文件上传全链路:从浏览器到 S3,一个字节都不能少
前端·前端框架·开源
上海知从科技10 小时前
SENT传输协议:汽车传感器数字化通信的最优解决方案
科技·安全·汽车·软件工程·汽车电子
放下华子我只抽RuiKe511 小时前
React 从入门到生产(三):副作用与数据获取
前端·javascript·深度学习·react.js·开源·ecmascript·集成学习
魔乐社区12 小时前
基于昇腾 MindSpeed LLM 玩转 DeepSeek-V4-Flash
人工智能·开源·大模型
北京盟通科技官方账号12 小时前
工业 PC 平台 EtherCAT 主站协议栈选型探讨:开源方案与商业级实时架构的工程落地对比
架构·机器人·开源·工控·ethercat·盟通科技·ec-master
无心水12 小时前
【分布式利器:金融级】金融级分布式架构开源框架全景解读
人工智能·分布式·金融·架构·开源·wpf·金融级框架
科技快报12 小时前
openJiuwen开源社区发布JiuwenSwarm,开启群体智能“养蜂”新时代!
开源