南昌航空大学实验报告25201517-段为晨

一、概览:从逻辑门到组合电路
三次作业一路做下来,感触颇多。最开始看到题目的时候,我以为就是个简单的门电路模拟,写几个if-else判断一下与或非就完事了。结果做到第二题才发现,事情远没有那么简单——三态门、译码器、数据选择器、数据分配器,这些名字听起来就头大,更别说还要处理什么控制引脚、使能条件、多路输出。现在回过头看,这套题目设计得确实有水平,从五種基本门到九种复合元件,难度阶梯爬升得很自然,每一步都逼着你去思考更复杂的电路结构。

知识层面上,这活儿把数字电路和编程揉在了一起。你得真懂逻辑门的真值表,也得会写面向对象的代码。与门或门还好说,到了译码器那块儿,S1=1且S2+S3=0才能正常工作,否则所有输出无效——这种使能逻辑要是理解不到位,代码写出来全是坑。编程方面更不用说,继承多态、哈希映射、图遍历,能用的基本都招呼上了。代码量从第一题的三四百行涨到第二题的五百多行,看似没多多少,但逻辑复杂度翻了一倍不止。

难度这事儿得分阶段看。第一题算是热身,只要搞清楚每个门怎么算,再做个拓扑排序把信号传播理顺,基本就能跑通。第二题才是真正的考验,多输出元件的引脚映射规则特别绕,比如说译码器,控制引脚占0/1/2,输入从3开始,输出从6往后排,一开始我老记混。再加上输出格式五花八门——普通门输出"元件-0:值",译码器输出"元件:编号",分配器输出一串带横杠的字符串——光是对齐这些格式就折腾了好几回。

二、设计与实现:把电路装进代码里
我选择用面向对象的方式来搭这个架子。Gate是个抽象类,管着每个元件的名字和输入引脚映射表,子类各自实现计算逻辑。这么做的好处很明显,新增一种元件只需要继承Gate然后重写两个方法就行,不用动原来的代码。后来第二题加四个新元件的时候,确实省了不少事。

类的继承层次大致是这么个结构:Gate底下分了AndGate、OrGate、NotGate这些基础门,然后XorGate和XnorGate处理异或同或,TriGate管三态门。比较特殊的是Decoder、Mux和Demux,它们不光有输入输出,还多了控制引脚,计算逻辑也复杂得多。说实话,最开始设计的时候没想过会扩展到九种元件,所以基类里只考虑了单输出情况。等做到译码器和分配器的时候,发现compute方法返回int不够用了——有的元件得返回字符串。最后只好改成返回Object,在输出的时候再判断类型。这是设计上的一处妥协,但也算灵活应对了。

模拟计算这块儿,我采用的是反复扫描的方式。把所有元件放在一个集合里,循环遍历,谁的条件满足了就计算谁,然后把计算结果塞回信号表里,供其他元件使用。直到某一次遍历下来没有任何新元件被计算,循环结束。这种方法简单粗暴,对付小规模电路完全够用。但说句实话,要是电路规模再大一点,几十上百个元件的话,这个O(n²)的算法估计会慢得让人抓狂。更好的做法应该是先做拓扑排序,按照依赖关系一次性算完,效率能提升不少。不过当时图省事,也就这么交了。

信号传播用的是HashMap,键是信号源的名字,值是电平值。每个元件的inputs映射表里存的是引脚号到信号源的对应关系。计算的时候从信号表里取值,算完了再把输出引脚作为新的信号源塞回去。这中间有个坑:一开始我没考虑到多级传播的情况,只扫描了一遍就以为完事了,结果好几个样例都输出不全。后来改成while循环才解决问题。

三、那些让人头疼的坑
写这套代码的过程中踩了不少坑,挑几个印象深的说说。

第一个坑是引脚编号。译码器的控制引脚,题目明确说了0/1/2对应S1/S2/S3,输入从3号开始。我一开始自作聪明,觉得控制引脚应该从1开始编号,结果样例8死活跑不出来。输入是A-0 B-0 C-1 D-0 E-0,连接关系里C接到M(2)1-0,D接到-1,E接到-2,这明显就是控制引脚从0开始的证据。我愣是调了半天才反应过来。后来仔细翻题目,发现人家早就写明白了,只是我没仔细看。从那以后,凡是遇到新元件,我第一件事就是去查引脚排序规则,绝不凭感觉猜。
第二个坑是数据选择器的映射方向。Mux的控制端选择数据输入,按理说控制信号00选第一个数据端,01选第二个,依次类推。但我一开始理解反了,以为00选最后一个,结果样例9输出死活不对。后来对照着题目里的描述"Z(1)1有1个控制端、2个数据输入端,S=0选择D0"才改过来。这事儿给我的教训是:但凡涉及编码对应关系的,一定要先写出真值表,不然很容易搞反。

第三个坑是三态门的无效状态处理。控制端为0的时候,三态门处于高阻态,输出无效,这个元件就不应该出现在结果里。我一开始图省事,控制端为0就返回0,结果样例7直接报错。后来才弄明白,无效和输出低电平是两码事,必须返回null让上层逻辑忽略它。同样的道理也适用于译码器,控制条件不满足的时候所有输出无效,不能随便给个默认值。

第四个坑是数据分配器的输出顺序。题目要求按引脚编号从小到大输出,比如F(3)1有8个输出引脚,从W0排到W7。但我一开始按选择顺序排,选择W3的时候输出"1------"(1在最前面),正确应该是"------1"(1在最后面)。这个问题其实不难,就是构建输出字符串的时候别搞反了方向。后来统一改成从高位往低位遍历,依次填入对应的值或横杠。

总结了一下调试过程中遇到的各种问题,引脚编号错误出现三次,每次平均花半小时;逻辑运算错误两次,每次二十分钟;输出格式错误四次,每次十五分钟;循环依赖导致计算顺序出问题一次,折腾了四十分钟。这些数据说明一个道理:大部分bug其实都源于对题目理解不够透彻,真正代码写错的情况反而不多。

四、还能怎么改进
虽然代码交上去跑通了,但回过头看,可以改进的地方还真不少。

架构层面最该改的就是那个反复扫描的模拟算法。现在这种方式虽然简单,但不够优雅。如果改成拓扑排序,先根据连接关系构建依赖图,然后从输入信号出发逐级计算,效率会高很多。特别是对于大规模电路,性能差距会很明显。我后来想了一下,其实可以给每个元件增加一个入度计数器,表示还有多少个依赖的输入没算出来,每算完一个元件就减少下游元件的入度,入度归零就可以计算了。这样一次遍历就能完成全部计算。

另外就是职责拆分的问题。现在的Main类简直是个大杂烩,解析输入、创建元件、运行模拟、格式化输出,全挤在一起。按照单一职责原则,应该拆成InputParser、GateFactory、CircuitSimulator、OutputFormatter四个独立的类,各管一摊。这样代码可读性会好很多,以后修改某个环节也不至于牵一发而动全身。

代码细节方面也有改进空间。比如Gate类里的canCompute方法在每个子类里都有类似的实现,完全可以用模板方法模式提炼到基类里,子类只需要提供引脚数量等参数就行。还有那些散落在各处的字符串常量,像元件类型"A"、"O"这些,应该统一定义为枚举类型或者常量类,免得写错了查半天。

异常处理这块基本是空白。题目说了默认输入正确,所以我就没加任何校验。但如果真要做一个能实际使用的模拟器,空指针异常、重复连接、格式错误这些情况都得考虑到。至少得做到程序不会莫名其妙崩溃,能给出清晰的错误提示。

五、这趟旅程教会我的事
做完这三次作业,收获最大的其实不是学会了怎么模拟数字电路,而是对"怎么设计一个可扩展的系统"有了切身的体会。最开始我只考虑了五种基本门,基类设计得很简单,输入输出都是单端的。到了第二题,新元件一来,发现原来的设计不够用了——多输出、控制引脚、多种返回类型,这些在最初完全没有预料到。如果一开始就能把变化的部分识别出来,用更灵活的方式去设计,后来的扩展会顺畅得多。

另一个让我感触很深的是细节的重要性。引脚编号差一位,输出结果就全变了;控制信号的使能条件少判断一个,整个元件就废了。这些看似琐碎的地方,恰恰决定了程序能不能跑对。写代码的时候马虎一点,调试的时候就得加倍偿还。

测试意识也是在这个过程中建立起来的。以前写代码习惯了一次性写完再跑,结果一堆bug挤在一起,根本不知道从哪下手。后来学乖了,每写完一个元件就单独测试,确认无误了再往下写。特别是那些复杂的元件比如译码器和分配器,我先用几个简单的输入验证逻辑正确,再集成到整个电路里去跑。

当然也有做得不够的地方。算法效率这块是明显的短板,反复扫描的方法在学术上不够漂亮,实际应用中也不够实用。还有就是设计模式的知识储备不足,很多问题明明有更优雅的解法,但因为不熟悉那些模式,只能硬着头皮写重复代码。这两块是今后需要重点补的。

后续打算深入学习一下事件驱动的仿真模型,这种思路在电路模拟领域应用很广,比现在这种静态拓扑排序要灵活得多。设计模式方面,工厂模式和策略模式是接下来要啃的重点,争取下次遇到类似的需求能写出更健壮的代码。另外还想抽空看看硬件描述语言,把软件模拟和硬件设计串起来理解,或许对数字电路的认识会更通透。

最后想说,这三道题从难易程度到知识覆盖都安排得很合理,既有编码的锻炼,也有设计的思考。如果说有什么建议的话,希望后续能增加一些异常输入的测试用例,比如引脚重复连接、信号值超出范围之类的,让学生在正常逻辑之外也练练容错处理。毕竟真实世界里的输入从来不会那么规规矩矩。

posted @ 2026-06-24 00:40  dwcicx0420  阅读(6)  评论(0)    收藏  举报