news 2026/10/16 1:06:58

手搭计算机基础门:XOR、Mux与DMux的实现与调试指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手搭计算机基础门:XOR、Mux与DMux的实现与调试指南

刚把非门、与门、或门搭完,很多人的第一反应是“就这?不就是三个基本门嘛”。但紧接着进入“计算机系统要素”第一章第二节,我指着任务列表说了句“不至于吧”——这三个新门分别是异或门(XOR)、多路选择器(Mux)和多路分配器(DMux)。名字一个比一个抽象,真值表一个比一个绕,但等你真正动手写完 HDL、跑完测试,你会意识到这仨才是后面加法器、寄存器、内存、CPU 的地基。这篇文章就是我在这一节从一脸懵到跑通全过程的完整记录:三个门为什么存在、怎么用 HDL 实现、测试怎么跑、报错怎么排查,以及做完之后目光能看到多远。

这一节适合两类人:一类是刚开始接触数字电路,想用动手方式把布尔代数变成长的电路的人;另一类是学过一点计算机组成原理,但只知道“Mux 是选择器,XOR 是异或”,没亲手实现过,脑子里始终差一层实感的人。看完这一篇,你至少能从“背结论”进化到“自己能把结论搭出来”。

1. 拆解目标:三个新门到底在解决什么问题

1.1 异或门:真正意义上的“第一个会运算的门”

异或门的规则很简单:两个输入相同输出 0,不同输出 1。真值表只有四行,但你别小看它。生活里最常见的例子是楼梯双控开关:楼上楼下两个开关控制同一盏灯,两个开关状态一致时灯灭,不一致时灯亮。你去按开关,本质上就是在做一次异或运算。

在数字系统里,异或门是加法器的地基。半加器的“和”位就是两个数的异或——你不需要知道进位怎么处理,先把“两数相加的结果本位”用异或表达出来。除此之外,奇偶校验、模式匹配、伪随机数发生器,全都绕不开 XOR。所以在第一章就让你实现它,不是为了凑任务数量,是因为再往下走,你第一个真正意义上“算算术”的电路,就是从这个门长出来的。

1.2 Mux:CPU 里的“铁路道岔”

多路选择器 Mux 的抽象程度比 XOR 高一些。它有三个输入:两路数据 a、b,一个选择信号 sel;输出就一路,out。sel 为 0 时输出 a,sel 为 1 时输出 b。

你可以把它想象成铁路道岔:两条轨道进来,到底让哪辆车开到主干道上,由道岔位置决定。CPU 内部全是这种东西。寄存器要从 A 通路还是 B 通路取数?ALU 的操作数来自内存还是来自寄存器?下一条指令的地址是顺序加一还是跳转?这些“二选一”全部要靠 Mux 来实现。没有 Mux,处理器就变成了一条只能单向流动、无法做任何决策的管道。

这也是为什么很多讲体系结构的书,会把 Mux 称为“数据通路的闸门”。你这次亲手实现它,后面看到 CPU 数据通路图里一排排 Mux,就不会觉得陌生了。

1.3 DMux:把一条路拆成多条路的“分拣员”

多路分配器 DMux 和 Mux 刚好相反:一个输入 in、一个选择信号 sel、两个输出 a 和 b。sel 为 0 时,in 被送到 a,b 保持 0;sel 为 1 时,in 被送到 b,a 保持 0。

这就像快递分拣线:一条传送带把包裹运过来,前面的分拣员根据标签把包裹推到左边还是右边。数字系统里,内存寻址就靠这个:一条数据线上的内容,要根据地址信号决定到底写入哪个存储单元。DMux 是你以后理解“译码器”“多路寻址”的第一级台阶。

2. 动手之前:工具链、语法与“能用的零件”

2.1 工具链:不需要示波器,一套 HDL 仿真器足够

这一节所有东西都可以在纯软件环境里完成。我实际使用的流程是:在项目文件夹里放好之前已经通过的 Not.hdl、And.hdl、Or.hdl,然后新建 Xor.hdl、Mux.hdl、DMux.hdl,再用配套的测试脚本逐个加载、编译、跑对比。

课程配套的 HDL 语言比 Verilog 简化很多,没有复杂的时序控制,也没有模块接口的额外语义。你只需要写清楚输入、输出、内部接线,剩下的事交给仿真器。它会把你的芯片实例化,按测试脚本逐行设置输入、执行一次“模拟时钟上升沿”、然后把输出结果和期望值文件对比。整个过程全是黑盒验证:仿真器不关心你中间是怎么接的线,只看输出是否符合预期。

这种“黑盒验证”的思路正是现代芯片开发里最常见的做法。你在这次用仿真器学的这套流程,将来进到更正式的 EDA 工具里,核心逻辑是一样的:写 RTL、写 testbench、跑仿真、对比波形。

2.2 HDL 入门:CHIP、PARTS 和那条“看不见的导线”

如果你对 HDL 还不熟,先记一个最小骨架:

CHIP 名字 { IN x, y; OUT out; PARTS: 子门实例; }

PARTS 区里每一行都是一个子门调用,比如Not(in=a, out=na)意味着:把 Not 这个已经实现好的芯片接进去,输入引脚 in 连到当前芯片的输入 a,输出引脚 out 连到一根叫 na 的内部线。

HDL 最核心的动作就是“给线起名字”。你声明的输入输出是外部可见的引脚,而 PARTS 里那些中间的 out=xxx,本质上就是在当前芯片内部新增一根导线。你可以让这根线被多个子门同时读取,这叫扇出;但要记住,一根线只能有一个来源,不能有多个东西同时往这根线上写值,否则就冲突了。这个点后面聊报错时还会踩到。

2.3 手头零件箱:Nand、Not、And、Or 全都在

按照课程路线,底层基元只有 Nand,其余门都用 Nand 搭出来。到这里,我的“零件箱”里已经有:

芯片说明当前状态
Nand基元直接可用
Not非门已实现并测试通过
And与门已实现并测试通过
Or或门已实现并测试通过

我个人的习惯是:只要有 And、Or、Not 能解决的问题,就不拆回 Nand,因为高层门加起来可读性更好。等到了追求最少门数、或者需要验证底层逻辑时,再单独拆开。后面实现 XOR 时我会把两种风格都写一遍,正好对比一下“好读”和“最省门”之间的取舍。

3. 实现异或门:从真值表到两份可用的 HDL 代码

3.1 真值表推公式:先把 XOR 拆成一句人话

XOR 的定义式是“相同为 0,不同为 1”。但我们需要的不是这句话,而是能用已有门表达的逻辑式。把真值表完整写出来:

abXOR
000
011
101
110

看“什么时候输出 1”:两种情况,a=0 且 b=1,或者 a=1 且 b=0。第一种写成NOT(a) AND b,第二种写成a AND NOT(b),这两个结果再求或,就是 XOR。于是:

XOR(a, b) = (NOT(a) AND b) OR (a AND NOT(b))

这个推导其实是在教你一个通用方法:输出为 1 的行,把每个输入按“0 取反,1 保留”写成子条件,再把所有子条件用 OR 连起来,这就是所谓积之和形式。后面遇到再复杂的逻辑门,这套记法都能用。

3.2 方案 A:用 And、Or、Not 组合,代码直观好读

按照公式,我用已实现的三个门直接搭,HDL 长这样:

CHIP Xor { IN a, b; OUT out; PARTS: Not(in=a, out=na); Not(in=b, out=nb); And(a=a, b=nb, out=anb); And(a=na, b=b, out=nab); Or(a=anb, b=nab, out=out); }

逐行过一遍。两个 Not 先把 a、b 分别取反,得到 na、nb。第一个 And 做a AND nb,对应真值表里“a=1 且 b=0”的情况;第二个 And 做na AND b,对应“a=0 且 b=1”的情况。最后 Or 把两种情况的输出合并成最终结果。这个写法可以说完全照着公式抄,几乎没有理解成本。

很多人在这一步会担心:“同一个 a 既连到了 Not,又连到了 And,没问题吗?”没问题,这就是扇出。一个信号被多处读取,在数字电路里是常规操作,只要不出现“多个信号驱动同一个线”就行。

3.3 方案 B:只用四个 Nand,探究一下“最少门数”

方案 A 总共用了 2 个 Not、2 个 And、1 个 Or,文件层是 5 个子门。如果你把 Not、And、Or 再拆成 Nand,实际上后台用的是十几个 Nand。但在只能使用基元 Nand 的条件下,XOR 有更精简的经典实现——四个 Nand:

CHIP Xor { IN a, b; OUT out; PARTS: Nand(a=a, b=b, out=c); Nand(a=a, b=c, out=d); Nand(a=b, b=c, out=e); Nand(a=d, b=e, out=out); }

这条电路我第一次看的时候也没反应过来,直接盯着真值表代了一轮才明白。这里 c 是NOT(a AND b),也就是 a 与 b 的与非值;d 是a NAND c,e 是b NAND c;最后 out 等于d NAND e。你把四种输入代一遍就能看到,它确实满足 XOR 的真值表。

方桵 A 和方案 B 没有绝对优劣:方案 A 可读性更好,方案 B 在只允许 Nand 的终极约束下更省门。我个人的建议是,做这节时两种都动手写一次。你在方案 B 里获得的“同一功能可以有完全不同的电路拓扑”的感觉,后面设计 ALU、优化电路时会非常值钱。

3.4 踩坑提醒:中间信号不能和输入引脚混用

第一次写 XOR 时,容易犯一个错:想在同一个内部线名上反复赋值,比如先Not(in=a, out=na),后面又想复用 na 存另一个结果。这在 HDL 里是不允许的。一根线只能有一个驱动源,你需要另起名字,比如 na2、n_b 这种。

另一个常见问题是大小写不规范。HDL 对引脚名和线名大小写敏感,a、A 是两个标识符。我的习惯是全部用小写引脚名、小写线名,只在 CHIP 名字上保持首字母大写。文件名还必须和 CHIP 名完全一致,否则加载时会报找不到芯片。这三个细节看着小,实操里能拦住一大半报错。

4. 实现 Mux 和 DMux:一对互逆的控制门

4.1 Mux 的真值表与选择信号:sel 等于 0 走 a,等于 1 走 b

Mux 的输入是 a、b、sel,输出 out。把 8 种输入组合全部列出来:

selabout
0000
0010
0101
0111
1000
1011
1100
1111

观察这张表,sel=0 时,out 完全跟着 a 走;sel=1 时,out 完全跟着 b 走。于是表达式非常直接:

out = (a AND NOT(sel)) OR (b AND sel)

这个式子的物理含义是:sel 先用 Not 变成互补信号,分别去“开门”。sel=0 时,第一条路 a 经过与门放行;sel=1 时,NOT(sel) 为 0,第一条路被关死,第二条路 b 放行。两个结果再 OR 到一起,就实现了二选一。

4.2 Mux 的 HDL 实现:分两路“与”,再汇成“或”

对应上面公式的 HDL 代码:

CHIP Mux { IN a, b, sel; OUT out; PARTS: Not(in=sel, out=nsel); And(a=a, b=nsel, out=la); And(a=b, b=sel, out=lb); Or(a=la, b=lb, out=out); }

我给中间线起了名字 la、lb,语义是“左臂”和“右臂”。别人看代码时,扫一眼就知道左边这路是和 sel 的反相信号做与,右边这路是和 sel 本身做与。命名是否清晰,在门数多了以后会直接影响排查效率。

实际测试时我还观察到一件事:很多人会把And(a=a, b=nsel, out=la)里的b=nsel写成b=not(sel),这当然不合法。HDL 只接受已经存在的线名,不会帮你内联计算。凡是需要取反,必须先单独拉一个 Not 出来接线,记住这个习惯,写复杂芯片时才不会乱。

4.3 DMux 的真值表与 HDL 实现:把一路分发到两条路

DMux 的输入是 in 和 sel,输出是 a 和 b。真值表里,sel 决定 in 去哪一路:

selinab
0000
0110
1000
1101

可以看到,sel=0 时,a 等于 in;sel=1 时,b 等于 in。用公式写就是:

a = in AND NOT(sel) b = in AND sel

HDL 实现和 Mux 几乎是对称的:

CHIP DMux { IN in, sel; OUT a, b; PARTS: Not(in=sel, out=nsel); And(a=in, b=nsel, out=a); And(a=in, b=sel, out=b); }

这个实现里四个子门,两路 And 共用同一个输入 in,另一条腿上分别接 sel 和 nsel。所以 DMux 从本质上就是“把 in 分别送往两个与门,再靠 sel 的互补信号决定哪一扇门开着”。真值表全部行都满足,测试就能一次过。

4.4 从 1 位到多路:DMux4Way 和 Mux4Way 的扩展思路

这一节的任务只到 DMux 为止,但我建议你顺手想一下:如果输入有 4 路,选择信号有 2 位,该怎么办?这就是 4-way Mux 和 4-way DMux 的雏形。

4-way DMux 的做法是级联:先用高位选择信号把输入分成“上半组”和“下半组”,再用低位信号在组内细分。比如输入 in,sel[1] 先把 in 送到 A/B 这半边还是 C/D 这半边,然后 sel[0] 再决定具体落到哪一条。你只需要三个 DMux 按层级接起来就完成了。Mux4Way 也是同理,只是把两个 Mux 先各自二选一,再用一个 Mux 从两个中间结果里选最终输出。

这层“递归式拆解”的思路以后会在内存地址译码器里反复出现。你现在觉得它多余,等做 16KB 的内存时,地址译码就是从单个 DMux 这样一层层“分”下去的结果。

5. 测试脚本与调试实录:仿真器上最容易翻车的三个地方

5.1 测试脚本怎么写:逐行枚举所有输入组合

课程配套的仿真脚本写法比较固定。拿 XOR 举例,一个完整测试脚本会包含这些段落:

load Xor.hdl, output-file Xor.out, compare-to Xor.cmp, output-list a%B3.1.3 b%B3.1.3 out%B3.1.3; set a 0, set b 0, eval, output; set a 0, set b 1, eval, output; set a 1, set b 0, eval, output; set a 1, set b 1, eval, output;

这里eval表示对当前输入组合执行一次仿真,output表示把结果写入输出文件,compare-to对应的 .cmp 文件里存的是期望值。仿真器跑完,屏幕上会告诉你每个用例是否与期望一致。

初学时最容易犯的懒病是只挑两三种输入跑一下,看到没有报错就当通过了。对于 XOR 来说,四个组合全跑也花不了几秒;但 Mux 有 8 个组合,DMux 有 4 个组合,一旦漏掉某个边界情况,错误当时看不见,后面做加法器时会突然跳出来咬你一口。

5.2 未声明内部线:编译报错最常见的原因

我在这三个门上遇到的报错,有一半都在说“某个线没声明”。典型场景是:

Not(in=sel, out=nsel); And(a=a, b=nsel, out=a1); And(a=b, b=sel, out=b1); Or(a=a1, b=b1, out=out);

我第一遍写 Mux 时,把线名写成了 nsel、a1、b1,结果在 And 里引用了 a1,但 a1 是在后一行才声明出来的——HDL 不像某些顺序执行的编程语言,它不管先后顺序,但是要求名字拼写必须完全一致。你如果写a1和al这种长得像但不同的名字,仿真器直接拒绝编译。

建议所有内部线名都用简短、一致、不易混淆的字母组合,比如 la、lb、na、nb。当你开始用temp1、temp2、tmpA这种名字时,排查报错成本就会直线上升。

5.3 悬空与冲突:输出管脚重复驱动

HDL 里每个子门的每个输入都必须接到一个确定的信号,不允许悬空。另一个更容易踩的坑是“输出冲突”:两个子门试图把结果写到同一条内部线,或者你试图往芯片的最终输出引脚 out 上接两次。

比如有段时间我想省事,直接把中间结果接到 out,后面又用另一个 And 再接到 out,仿真器就会报 out 已经被驱动。记住规则:最终输出引脚 out 只能被赋值一次;中间线也只能有一个来源,但可以被多个门读取。这就是“一源多扇出,禁止多驱动”。

5.4 功能全对却不稳定:别忘了边界全 0 和全 1 用例

有一种情况特别隐蔽:你的测试脚本里每个组合都写了,其中大部分结果也对,但“in 等于 0”或者“sel 等于 1 且 a/b 都等于 0”这种极端组合没有覆盖,导致某个输出错误没暴露。

以 DMux 为例,真值表里有一个很容易写错的地方:sel=0、in=0 时,a 应该是 0,b 也应该是 0。有些初学版本会把 b 写成And(a=in, b=sel, out=b)之外的形式,导致 in=0 时 b 跟着 a 一起输出 1。只有把 in=0 的所有组合都跑一遍,这个 bug 才会现形。

我的习惯是:每个门都把自己的完整真值表整理成测试脚本,四行、八行的枚举不嫌麻烦。跑测试不是走形式,是真值表在机器上的复述。

5.5 调试方法论:拿真值表一行一行对着排

编译错误和逻辑错误要分开处理。编译错误看提示里的行号和线名;逻辑错误如果不严重,可以靠测试报告里失败的用例定位。

更高效的定位方式是在仿真器里单步执行。你可以把某个失败用例的输入设置好,然后让仿真器逐条执行 PARTS 里的子门,一边执行一边观察每条内部线当前是什么值,把中间结果和真值表预期一对,通常一两分钟就能找到错在哪。我后来写多路选择器遇到 bug,都是这样直接“看线”,比盲改代码快得多。

还有一点:如果报错信息写着“chip not found”,先检查文件名。课程仿真器对文件名和 CHIP 名是否一致比较严格,大小写错了也会直接报找不到门。别问我是怎么知道的。

6. 仅仅三个门,怎么就成“现代计算机”的一部分了

6.1 异或门怎么变成加法器

拿到 XOR 之后,半加器就是一行公式的事:和的低位是 XOR,进位是 AND。全加器则需要处理来自低位的进位,多出来的一个半加器加上一个 OR 就能组合出来。所以你现在写的这个 XOR,其实已经是未来 ALU 的一部分。

这也是为什么我说 XOR 是第一扇“会运算”的门。Not、And、Or 都只是在做逻辑判断,到了 XOR,电路开始表达“两个数相加的结果”。从这里往上走,你会搭出 16 位加法器、加/减法器,最后把它们塞进 ALU 里,完成计算机最核心的算术逻辑单元。

6.2 Mux 怎么变成可控存储电路

Mux 的能力是“选路”,这个能力在存储单元里非常关键。寄存器往往需要从多个来源接收数据:可能来自输入端口、来自 ALU 计算结果、来自内存读取结果。每个来源都是一路数据,用 Mux 按控制信号决定最终存入哪一路。再往上,程序计数器的下一个地址是 PC+1 还是跳转目标,也是靠 Mux 选择。

没有 Mux,CPU 就没有办法“根据当前状态决定下一步动作”,整个机器就只能机械地一路跑到底。所以你现在做的这个三输入两行表达式的小门,将来在 CPU 数据通路图里会以几十个、上百个的形式反复出现。

6.3 DMux 怎么变成解码器的起点

DMux 扮演的是“分发”的角色。内存里要写数据到的某个地址,本质上就是用地址线去控制多个 DMux,把数据总线上的内容分发到对应的存储单元。指令译码器也类似:操作码经过一层层选择,最终变成某一条控制线上的有效信号,驱动后续硬件工作。

你可以把 DMux 看作“最简译码器”。单个 DMux 只是 1 位地址分两路;把多个 DMux 组合起来,就能形成 n 位地址分 2 的 n 次方路的完整译码结构。后面做内存、做指令解码,你都会反复看到这个熟悉的影子。

6.4 回头看:“从零开始”这条路的节奏感

《计算机系统要素》第一章的精髓,不是让你记住某个门的真值表,而是让你习惯一种工作循环:看真值表,写逻辑表达式,用已有组件搭 HDL,跑测试,过。循环往复,每一轮的手段都一样,但搭出来的东西却越来越靠近一个完整的处理器。

我做完这三个门之后,最大的感受是“原来复杂系统不是一步到位的,而是像搭积木一样一层层垒上去”。每次拿到新的零件清单,前面那些已经跑通的门就是新的积木,你不用关心它们内部有多少个 Nand,只要把它们当作黑盒去用。这个“黑盒思维”比任何一行 HDL 代码都值钱。

最后分享一个实操小技巧:写完 HDL,先别急着点 run。花两分钟看一眼自己用了哪些中间线,确认每一根线都有且只有一个驱动源,确认每个子门的输入都写全了。这套“眼读”能帮你把一半的编译报错消灭在按下回车之前。我前几次偷懒跳过这一步,结果反复在同一个地方花了几倍时间。老老实实眼读一遍,后面跑得才顺。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 1:06:13

双节锂电池主动均衡设计:MP2642GR-Z与PIC18F46K42实战方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 1:06:07

JOS bootloader详解:从实模式到保护模式的内核加载全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 1:05:58

RAPID:从人类示范生成可执行机器人程序的新范式

1. 项目概述:当机器人开始“看懂”人类动作并自动生成可执行程序RAPID——全称 Robot Agentic Programming from Demonstrations,不是某个新出的OCR工具,也不是云端文档扫描服务,更和“rapid ocr onnx是云端还是本地”这种纯视觉识…

作者头像 李华
网站建设 2026/10/16 1:05:28

51单片机按键与数码管协同实战:消抖、动态扫描与时间片轮询

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 1:05:25

易语言封装CreateThread线程API模块:句柄管理与参数传递实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 1:05:01

Kibana 7.8.0 Dashboard实战:从环境准备到面板共享与排错

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华