☰
8选1数据选择器实现血型输血判断:编码、香农展开与LUT思维
2026/11/3 6:44:21 网站建设 项目流程

简介:面向数字逻辑课程学习者的一份仿真实验报告,来自桂林电子科技大学计算机与信息安全学院,围绕教材习题4.22的血型输血判断问题展开:用两个逻辑变量编码A、B、AB、O四种血型,分别表示输血者与受血者,再由八选一数据选择器搭建组合逻辑电路,判断输血组合是否符合箭头所示的授受关系。报告完整记录了实验目的、实验准备、实验内容、仿真真值表与问题记录,并说明LogicState作数字输入源、LogicProbe作输出端检测、GROUND接地等仿真环境的基本用法,以及如何判断数据选择器哪些端口接1、哪些接0。全包仅1个docx文档,约1.92MB,篇幅紧凑,适合实验前预习、实验后对照复盘,或作为组合逻辑设计的参考范例。目前已有3227人学习,对初次接触数据选择器设计方法的学生较有借鉴价值。

1. 数据选择器:一片 8 选 1 就是一张 8 行的真值表

数字逻辑课讲组合电路设计,教科书流程是列真值表、填卡诺图、圈 1、写最简与或式、再用与门或门搭出来。可血型输血判断这道题真做下来,卡人的不是化简,而是化简完项数依然不少,线一多就乱。反直觉的地方在于:四变量、九个 1 的函数,最省事的做法是干脆不化简,直接把 8 选 1 数据选择器当查找表用——三根地址线送三个变量,剩下那个变量接到数据端,八根数据线分别接 0、1、Q 或 Q 非,电路就出来了。这一点想通,FPGA 里 LUT 的原始形态也就通了。

2. 血型规则的布尔化:那条“子集”规则藏在编码里

输血相容性这件事,看着像医学常识,实际是一条非常干净的集合关系:供血者红细胞表面带的抗原,必须是受血者免疫系统认得的那几种的子集。A 型血带 A 抗原,B 型带 B 抗原,AB 型两种都带,O 型一种都不带。O 型之所以是万能供血者,因为它交出去的是空集,空集是任何集合的子集;AB 型之所以只能给 AB,因为它交出的是全集,全集只被全集包含。这条规则一句话就能说完,但落到电路上,它的长相完全取决于你怎么给四种血型编码。

2.1 从箭头图翻译成 16 行真值表

实验给出的提示是:用两个逻辑变量表示供血者,另两个表示受血者。四个变量记作 M、N(供血者)和 P、Q(受血者),每个血型对应一个两位二进制码。实验报告采用的编码是 A=00、B=01、AB=11、O=10——注意这不是自然二进制顺序,而是一个格雷码环路(00→01→11→10),相邻血型只差一位。这个选择在后面会显出代价。

把箭头图逐条展开,四个变量一共 2⁴=16 种组合,每一种要么合法(输出 1),要么不合法(输出 0):

M N供血者P Q受血者F
0 0A0 0A1
0 0A0 1B0
0 0A1 0O0
0 0A1 1AB1
0 1B0 0A0
0 1B0 1B1
0 1B1 0O0
0 1B1 1AB1
1 0O0 0A1
1 0O0 1B1
1 0O1 0O1
1 0O1 1AB1
1 1AB0 0A0
1 1AB0 1B0
1 1AB1 0O0
1 1AB1 1AB1

16 行里 1 的个数是 2+2+4+1=9。这个数字很关键:9 个最小项意味着如果硬走与或式,最简结果大概率是四五项,每项三到四个文字,光与门就得摆一排。而 9 个 1 也意味着这张表的信息量并不小,不是靠一两个门能凑出来的。

还有一点容易被忽略:16 行必须全部填满。有些报告只画了 9 行合法的组合,剩下 7 行空着。在纸面上这没毛病,因为不合法的组合本来就不关心;但在仿真里,真值表的每一行都是电路必须给出确定电平的输入,空着的行等于让数据端悬空,输出会是不确定态。这条后面会作为踩坑单独拎出来说。

2.2 编码决定了表达式长相:自然编码与格雷编码的差别

如果把编码换成自然顺序 O=00、A=01、B=10、AB=11,整件事会突然变得非常漂亮。此时两位码的含义就是“A 抗原位”和“B 抗原位”,相容条件退化成一条位运算:

合法 ⟺ (供血者 & ~受血者) == 0

也就是供血者为 1 的那一位,受血者必须也是 1。展开成布尔式就是F = (¬M + P)(¬N + Q)(M、N 为供血者两位,P、Q 为受血者两位),一个与门加两个或门就够了,连卡诺图都不用画。

而实验报告选的 A=00、B=01、AB=11、O=10 破坏了这个结构:O 型是 10 而不是 00,于是“空集”这个身份在编码上不再对应全零,子集关系没法用位运算直接表达。这不是错误,只是代价——真值表看起来“随机”,化简不出漂亮的式子,最后只能老老实实上数据选择器。这里其实就是整道题的设计取舍点:编码选得好,门电路省;编码选得随意,就得靠器件的通用性兜底。数据选择器的价值恰恰在于它不在乎你的编码长什么样,它只认地址和数据的对应关系。

2.3 用卡诺图交叉验证那 9 个 1

手工排一张四变量卡诺图,行用 MN(00、01、11、10),列用 PQ(00、01、11、10),把上面表格的 9 个 1 填进去,会看到几块很规整的形状:MN=10 那一整行全是 1(O 型万能供血),PQ=11 那一整列也全是 1(AB 型万能受血),剩下的 1 集中在主对角附近。

圈图的时候会发现,MN=10 整行可以圈成M·N̄,PQ=11 整列可以圈成P·Q,但这两块和主对角线上的 A→A、B→B、AB→AB 三个孤立 1 没法合并——因为格雷编码把 A 和 O 放在了对角位置,它们在物理意义上“相邻”(都是不含 B 抗原),在编码上却隔得最远。这正是 2.2 节说的问题在卡诺图上的投影:相邻性没被编码保住,化简就无从下手。

所以到了这一步,正确的判断不是继续硬凑最简式,而是换工具。数据选择器不是“化简的替代品”,它是在化简不划算时,用存储换逻辑的另一条路。

3. 四变量压进三根地址线:余一变量法怎么把数据端变成 Q

8 选 1 数据选择器只有三根地址线,要处理四个变量,直觉上会卡住。突破点在一个很朴素的事实上:MUX 的输出方程是地址最小项的加权和,Y = Σ mᵢ·Dᵢ,其中 mᵢ 是地址变量的最小项。这个式子和香农展开式长得一模一样,而香农展开式恰好允许你把一个变量“提”到外面。

3.1 原理:香农展开把第四个变量挤到数据端

对四变量函数 F(M,N,P,Q) 按 Q 展开:

F = Q̄·F(M,N,P,0) + Q·F(M,N,P,1)

括号里的两个东西都是三变量函数,各自由 8 个最小项组成。把这两个三变量函数再按 MNP 展开,整理后得到:

F = Σᵢ mᵢ · [ Q̄·Fᵢ(0) + Q·Fᵢ(1) ] i = 0..7

方括号里那一项,就是第 i 个数据输入端 Dᵢ 该接什么。它只有四种可能:Fᵢ(0) 和 Fᵢ(1) 都是 0 就接常量 0;都是 1 就接常量 1;是 0 和 1 就接 Q;是 1 和 0 就接 Q 非。换句话说,把三个变量交给地址线,第四个变量就被“折叠”进了数据端,数据端从 8 个常量变成了 8 个关于 Q 的函数。这就是所谓的余一变量法,也有人叫折叠法。

这套推导推广开:一片 2ᵏ 选 1 的 MUX 可以直接实现 k+1 个变量的任意组合函数,不需要任何附加门;变量再多,就得靠多片级联或者额外的逻辑门做前级压缩。16 选 1 能直接吃下四个变量,8 选 1 差一口气,差的这口气就是一个反相器。

3.2 逐行反推 D0 到 D7

按地址 MNP 从 000 数到 111,每一组地址固定了供血者(MN)和受血者编码的高位(P),只剩 Q 在 0 和 1 之间变。对照 2.1 节的真值表两两取值,就得到下面这张映射表:

地址 MNP供血者Q=0 时受血者Q=1 时受血者F(Q=0)F(Q=1)数据端 Dᵢ
000AAB10Q̄
001AOAB01Q
010BAB01Q
011BOAB01Q
100OAB111
101OOAB111
110ABAB000
111ABOAB01Q

八根数据线的结论是:D0 接 Q 非,D1、D2、D3、D7 接 Q,D4、D5 接常量 1,D6 接常量 0。全电路只需要一个非门加一片 MUX,不需要任何与门或门。

值得多看一眼的是 D4 和 D5 为什么是常量 1:地址 100 和 101 对应 MN=10,也就是 O 型供血者,无论受血者是谁都合法,所以这两行跟 Q 无关,恒为 1。同理 D6 是常量 0:地址 110 对应 MN=11 的 AB 型供血者配 P=0 的受血者(A 或 B),两种情况都不合法。这两个极端行是全表的锚点,推导时先定它们,剩下六行就不容易错。

3.3 换一组地址线,代价会变

地址线不一定非得选 MNP。如果改成用 N、P、Q 做地址、把 M 放到数据端,重新推一遍会得到 D = [1, M, M, 1, 0, M̄, 0, 1],同样需要四种不同的数据源(1、0、M、M̄),同样一个非门。这两种分组在门数上打平手。

但换成 16 选 1 就完全不同:四根地址线直接吃掉 MNPQ,八个数据端全变成常量,D = [1,0,0,1, 0,1,0,1, 1,1,1,1, 0,0,0,1],一个非门都不需要,代价是芯片大一号、地址线多一根、布线复杂度上去。这是个典型的面积换简洁的取舍,选哪个没有标准答案,取决于你手上有什么器件、以及这根反相器会不会成为时序或布局上的麻烦。

顺带说一句,如果非要用 4 选 1,两个变量做地址、剩下两个变量的四种组合做数据端,数据端会变成 QP、Q+P 之类的二变量函数,还得再补门,反而更绕。降维的层数越多,外围逻辑越重,这是组合逻辑设计里一条很硬的规律。

4. 仿真接线:从 LogicState 到 LogicProbe 的电平链路

推导对了,接错线一样白搭。仿真环境里的元件是虚拟的,但电平规则和真实芯片没有区别:LogicState 提供可点击切换的 0/1 数字源,LogicProbe 检测某点电平并给出亮/灭指示,GROUND 提供参考地。这三个东西构成了一个最小的激励—观测闭环。

4.1 74LS151 的引脚与使能端

以常见的 16 脚封装为例,引脚分布是这样的:

引脚名称作用
1、2、3、4D3 D2 D1 D0数据输入(低四位)
15、14、13、12D4 D5 D6 D7数据输入(高四位)
11、10、9A B C地址输入,A 是最低位
7Ē(G)使能,低电平有效
5Y高有效输出
6W低有效输出(Y 的反相)
16 / 8VCC / GND电源与地

接线时要盯死三件事:使能脚必须接 GROUND,否则芯片整片不工作,Y 恒为低;地址位序是 A 最低、C 最高,所以 M 接 C、N 接 B、P 接 A,接反了真值表会整体错位;Q 先过一个非门再分线,非门的输出接 D0,非门的输入接 D1、D2、D3、D7,这样八根数据线只有两个来源,不容易接串。

输出端挂 LogicProbe,观察 Y 就行。W 脚可以悬空,也可以接一个反相的探针做对照——两个探针永远一亮一灭,是判断芯片有没有正常工作的最快手段:如果两个都不亮,说明使能没接或者电源没上;如果两个同时亮,多半是探针接到了悬空节点上。

4.2 用穷举脚本替代手工对表

16 行输入,手工拨 16 次开关再抄 16 次结果,出错概率不低,而且错了很难定位。用一段 Python 把规则和 MUX 行为都建模出来,逐行比对,几十毫秒就能给出结论:

# 血型输血相容性:8 选 1 数据选择器方案的穷举验证 # 编码约定与实验报告一致:A=00 B=01 AB=11 O=10 A, B, AB, O = 0b00, 0b01, 0b11, 0b10 # 规则表:键是供血者,值是可以接受它的受血者集合 RULE = { O: {A, B, AB, O}, # O 型不带抗原,谁都能接 A: {A, AB}, B: {B, AB}, AB: {AB}, # AB 型带全抗原,只有 AB 能接 } def legal(m, n, p, q): donor = (m << 1) | n # 高两位是供血者 rec = (p << 1) | q # 低两位是受血者 return int(rec in RULE[donor]) # 1) 按规则直接展开 16 行真值表(不依赖任何化简) table = {(m, n, p, q): legal(m, n, p, q) for m in (0, 1) for n in (0, 1) for p in (0, 1) for q in (0, 1)} print("1 的个数 =", sum(table.values())) # 期望 9

这段代码的价值不在计算结果,而在于它把“规则”和“实现”彻底解耦了:RULE是医学事实的直译,table是它的布尔投影,中间没有任何人的判断。后面所有推导都以这张表为准,一旦哪里对不上,一定是推导错了,不会是表错了。

接着按 3.1 节的香农展开,把每个地址下 Q=0 和 Q=1 的两种结果归类成 0、1、Q、Q̄:

# 2) 以 MNP 为地址、Q 为数据端,反推 D0..D7 该接什么 def data_func(addr): m, n, p = addr f0, f1 = table[(m, n, p, 0)], table[(m, n, p, 1)] if (f0, f1) == (0, 0): return "0" if (f0, f1) == (1, 1): return "1" if (f0, f1) == (0, 1): return "Q" # Q 为 1 时才合法 return "~Q" # Q 为 0 时才合法 addrs = [(m, n, p) for m in (0, 1) for n in (0, 1) for p in (0, 1)] D = [data_func(a) for a in addrs] print("D0..D7 =", D) # 期望 ['~Q', 'Q', 'Q', 'Q', '1', '1', '0', 'Q']

最后一步才是真正的自证:把 D 表塞回一个 8 选 1 的行为模型,重新跑一遍 16 行,和规则表逐项对比。

# 3) 用推导出的 D 表模拟 MUX,逐行与规则表比对 def mux_out(m, n, p, q): addr = (m << 2) | (n << 1) | p # MNP 拼成 0..7 的地址 d = D[addr] return {"0": 0, "1": 1, "Q": q, "~Q": 1 - q}[d] mismatch = [k for k in table if mux_out(*k) != table[k]] print("不一致的行:", mismatch or "无")

mismatch为空,说明 D0 到 D7 的接法在全部 16 种输入下都等价于原始规则,这个方案就站得住了。这段脚本的意义在于,它检验的是“电路”而不是“我的算术”——人会在第 11 行抄错一个 0,脚本不会。

4.3 用一段行为级描述固化接线关系

把接线关系写成 Verilog,相当于给这张电路图做了一次可执行存档,后面换仿真环境、换芯片型号,照着改端口就行:

// 与仿真接线一一对应:d 的下标就是地址 MNP 的十进制值 module blood_check( input m, n, p, q, output y ); wire qn; assign qn = ~q; // 拼接顺序是高位在前:{D7, D6, D5, D4, D3, D2, D1, D0} wire [7:0] d = {q, 1'b0, 1'b1, 1'b1, q, q, q, qn}; wire [2:0] sel = {m, n, p}; // 注意 m 是最高位 assign y = d[sel]; // 8 选 1:地址译码后选中一路 endmodule

d[sel]这一行看着像数组索引,实际综合出来就是标准的“地址译码 + 与或”结构,和 74LS151 内部那八个三输入与门加一个八输入或门是同一回事。拼接顺序是最容易写反的地方:{q, 1'b0, ...}从左到右对应 D7 到 D0,和引脚表的顺序相反,改的时候一定要对着 3.2 节的表改,不要凭印象。

5. 避坑:这类实验最容易翻车的五个地方

5.1 使能端悬空,整片芯片装死

现象:所有输入都接好了,探针一动不动,输出恒为低电平,换芯片也没用。

原因:74LS151 的使能脚是低电平有效,悬空在真实电路里会被 TTL 输入特性拉成高电平,在仿真里则可能被判为不确定态,两种情况的结局都是芯片被关掉。

解决:使能脚直接接 GROUND,中间不要串电阻也不要接 LogicState。如果确实需要动态使能,就接一个确定的下拉,别让它浮着。

5.2 地址位序接反,真值表整体错位

现象:大部分行对得上,但某些行输出的值和推导表对不上,而且错得很“有规律”——像是把 001 和 100 的结果对调了。

原因:芯片的 A 脚是最低位、C 脚是最高位,而纸面上习惯把 M 写在最左边当最高位。如果按书写顺序 M→A、N→B、P→C 接,地址就反了。

解决:接之前先在图上标一遍权重,M 接 C(权 4)、N 接 B(权 2)、P 接 A(权 1)。或者干脆把真值表的地址列按 A、B、C 的顺序重排一遍再接线,让纸面和芯片用同一套位序。

5.3 Q 和 Q 非接反,一半的行判错

现象:D4、D5 那两行(O 型供血)正常,其他行有一半反了。

原因:D0 接的是 Q 非,而 D1 到 D3、D7 接的是 Q,八根线里只有一根需要反相。如果图省事从 Q 直接拉线到 D0,非门就白装了。

解决:把非门放在最前面,Q 进非门,非门的输出单独走一根线到 D0,其余四根从 Q 上引。这样“哪根线是反的”在图上一目了然,不用顺着线一根根追。

5.4 真值表只填合法行,非法组合输出不确定

现象:仿真跑起来有几行输出是高阻或者中间电平,探针半亮不亮。

原因:纸面真值表只画了 9 行合法组合,剩下 7 行没定义。在仿真里,没被驱动的数据输入端就是悬空,输出自然不确定。

解决:16 行必须全部填满,不合法的行明确写 0。填完之后再核对一遍 1 的总数是不是 9,这个数字是很好的自检位。

5.5 血型编码记混,O 和 AB 对调

现象:O→O 那一行判成 0,AB→AB 那一行也判成 0,但 O→AB 是 1。

原因:四选二编码不止一套约定,A=00、B=01、AB=11、O=10 和 O=00、A=01、B=10、AB=11 都有人用,抄表时串了。

解决:把编码表直接画在电路图旁边,别只写在报告正文里。更稳的做法是跑一遍 4.2 节那个脚本,让机器去查,别靠眼睛。

6. 设计取舍:仿真跑通只是最低门槛

这套方案在仿真里点亮探针,只说明它在这 16 组输入下行为正确。真正决定一个设计好不好,是另外三件事。

第一件是编码的选择权在你手里,但代价不对等。自然编码 O=00、A=01、B=10、AB=11 能把相容规则压成(donor & ~rec) == 0,两个或门一个与门就完事;格雷编码把 O 挪到 10,规则就散成了九个最小项。教材选格雷码的理由通常是“相邻血型只差一位”,便于观察,但在这个具体问题上,它把最该相邻的 A 和 O 推到了对角。如果目标是最小化门数,就该换编码;如果目标是让学生看清 MUX 的通用性,就保留格雷码——实验的目的和工程的目的不总是一致,这件事得自己想明白。

第二件是器件的规模和外围的复杂度是可以互相兑换的。8 选 1 加一个非门,或者 16 选 1 加零个门,两条路都通。前者省芯片、费布线,后者费引脚、省心思。我自己的习惯是:超过两片级联就走 16 选 1,只有一片的时候宁可多接一个非门,因为一根反相器出错的概率远小于多焊八根线。

第三件是验证方式决定了你敢不敢改。手工对表能验证一次,但对完就忘了;脚本能验证无数次,改一个接法重跑一遍就知道有没有回归。上面那三段 Python 加起来不到三十行,却覆盖了 16 行输入的全部组合,这种“写完就再也不用担心”的资产,比多化简出一项有用得多。这也是 FPGA 里 LUT 的哲学:与其为每个函数设计专用门阵列,不如准备一堆通用查找表,让工具去填内容。

最后补一句关于卡诺图的。它在四变量以内确实好用,能让人直观看到相邻性;但一旦编码选得不配合,卡诺图上的“相邻”和物理意义上的“相邻”就对不上,圈出来的项看着漂亮,实际含义是乱的。我后来带人做这类题,第一步永远是先把编码和物理含义的对应关系写在纸上,再动笔化简——不然化简得越顺,错得越远。

从那以后我每次做组合逻辑的设计,不管题目多简单,都会先用脚本把真值表和实现各建一遍、互相对照,再动手接线。那一次在 D0 上少接一个非门、对着探针找了两个小时的血泪经验,让我再也不敢跳过这一步。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询