数字电路模拟程序三次迭代总结性Blog

一、前言
(1)作业概述
三次迭代式编程作业围绕数字电路模拟程序展开,逐步从基础门电路模拟过渡到复杂组合逻辑器件,最终扩展到时序逻辑系统。三次作业难度递进,功能需求递增:
第4次作业:实现基本门电路(与、或、非、异或、同或)的模拟,支持信号传播和结果输出。
第5次作业:增加三态门、译码器、多路选择器、数据分配器等组合逻辑器件,丰富元件库并改进传播算法。
第6次作业:引入时序逻辑元件(如D触发器、寄存器、计数器等),实现带时钟和状态保持的电路模拟。
整个过程中,不仅巩固了Java集合框架、正则表达式、面向对象设计等核心知识,更让我深刻体会到软件抽象和迭代开发在复杂系统构建中的关键作用。
(2)题量与难度
迭代 代码量 难度 核心挑战
第4次 200行 低 基本门逻辑实现、信号传播循环、输入解析
第5次 450行 中 多类型器件抽象、统一传播框架、复杂输出格式
第6次 650行 高 时序逻辑建模、状态管理、时钟边沿检测
三次作业难度递增合理,从简单布尔运算逐步上升到具有时序状态的实际硬件描述,让我对数字电路底层原理和软件模拟技术都有了更深刻的理解。
二、设计与分析
2.1 第4次作业:基本门电路模拟
2.1.1 作业要求
实现一个数字电路模拟器,支持以下五种基本门:
A:与门(可多输入)
O:或门(可多输入)
N:非门(单输入)
X:异或门(二输入)
Y:同或门(二输入)
输入格式为 INPUT: A-1 B-0 ...,连接关系通过 [源引脚 目标引脚1 目标引脚2 ...] 定义,其中引脚格式为 门名-引脚号,输出引脚固定为 门名-0。程序需按拓扑顺序传播信号,并输出所有门的计算结果。
2.1.2 类图
image
2.1.3 核心代码分析
信号传播算法:
采用迭代法,每次循环先根据已有信号更新各门输入,再计算所有输入完备的门,将其输出加入信号表,重复直到无新信号产生。这种方式简单有效,但可能因循环依赖导致死循环,故设置最大迭代次数作为保护。
2.1.4 复杂度分析
image

时间复杂度:O(n * m),n为门数,m为连接边数,最坏情况需迭代n次。
空间复杂度:O(n + e),存储门实例和连接关系。
2.1.5 设计心得
1.数据结构选择:使用 Map<String, Integer> 存储信号值,便于通过引脚名快速访问;使用 Map<String, List> 存储连接关系,方便源到目标的遍历。
2.迭代传播的局限:遇到组合反馈时可能无法收敛,但作业中均为无环组合电路,因此可行。
3.统一输出排序:按门类型(AONXY)和编号排序,便于结果对比,体现了良好的输出规范意识。
2.2 第5次作业:组合逻辑扩展
2.2.1 迭代新增内容
在第4次基础上,新增以下器件:
三态门 (S):控制端为0时输出高阻(无效),为1时输出数据端值。
译码器 (M):n位输入,2^n个输出,低电平有效(选中输出0,其余1),并带有3个控制端(使能、片选等)。
多路选择器 (Z):n位选择信号,2^n个数据输入,输出选中的一路。
数据分配器 (F):n位选择信号,1个数据输入,将数据输出到选中的一路,其余输出为'-'(表示高阻或未连接)。
同时引入抽象类 Component 统一管理所有器件,并实现更复杂的输入引脚分配(不同器件引脚编号不同)和输出格式(译码器输出激活索引,数据分配器输出字符串等)。

2.2.2 类图关系
image
每个子类重写 setInput、isReady、evaluate 和 isInputPin 方法,实现了多态的信号接收和计算。
2.2.3 核心代码分析
多态传播:
主循环遍历所有组件,调用 isReady() 判断输入是否完整,若完整则调用 evaluate() 计算输出并存入信号表。这种设计使新增组件只需实现接口,无需修改传播逻辑,体现了开闭原则。
2.2.4 复杂度分析
image

由于引入了更多组件类型和更复杂的引脚映射,时间复杂度和空间复杂度均有所增加,但整体仍为O(n*m)量级。主要开销在于正则表达式解析组件名称和动态创建实例。
2.2.5 设计心得
抽象的魅力:通过 Component 抽象类,将不同器件的共性(名称、类型、编号、输入状态、求值能力)提取出来,使得传播引擎与具体器件解耦,大大提升了扩展性。
引脚编号规范化:不同器件的引脚编号规则各异(如译码器控制引脚02,数据引脚3n+2),必须在 isInputPin 和 setInput 中精确处理,否则会导致信号错乱。这一阶段让我深刻体会到“硬件引脚定义”的严谨性。

输出格式多样化:译码器输出低电平索引,数据分配器输出带'-'的字符串,这要求程序根据不同器件类型定制输出。使用 instanceof 判断类型并分别处理,虽然不够优雅,但在当时是务实的选择。
2.3 第六次作业:子电路与分层设计
2.3.1 作业要求
在第五次作业基础上,本次迭代的核心需求是支持 子电路(Subcircuit) 的定义与实例化。主要新增特性包括:
通过 C: 和 endc 关键字定义子电路模板,内部包含独立的输入(INPUT:)、 输出(OUT:)和连接关系。
顶层可引用子电路实例,如 C1-A 表示子电路 C1 的引脚 A。
子电路支持嵌套,即子电路内部可再引用其他子电路。
信号传播需跨越层次边界,子电路的输出作为顶层或其他子电路的输入。
输出时,子电路内部所有门和嵌套子电路的输出引脚均需按统一格式输出。
2.3.2 核心代码分析
(1)信号引脚的统一管理(Pin 类)
Pin 既可作为元件的输入引脚(sink),也可作为输出引脚(source)。通过 source 指针形成信号链,计算时从源引脚递归取值,实现了信号的前向传播。
(2)子电路的模板与实例分离
SubCircuitTemplate 存储子电路的静态结构(输入、输出、连接),而 CompositeCircuit 是动态实例,包含实际引脚和内部元件。这种设计使得同一模板可被多次实例化,且每个实例独立维护状态。
(3)分层次构建与连接处理
CircuitBuilder.build() 首先构建顶层输入引脚,然后处理顶层连接。遇到子电路引用时,调用 getSubCircuitInstance() 创建实例(若已存在则复用),并递归调用 buildSubCircuitInternal() 构建其内部结构。构建过程中,通过 ConnectionContext 接口统一处理引脚查找和源判断,避免大量 if-else 分支。
(4)递归计算与输出收集
CompositeCircuit.computeAllOutputs() 先确保所有输入引脚有值,然后递归计算嵌套子电路和内部门,最后将输出值映射到外部输出引脚。CircuitBuilder.computeAndOutput() 执行顶层计算,并收集所有门和子电路内部门的输出,按门类型(AONXY)和编号排序输出。
2.3.3类图
image
image
2.3.4 复杂度分析
image

时间复杂度:计算复杂度为 O(N),其中 N 为所有元件(门和子电路)总数,因为每个元件仅计算一次(computed 标记)。
空间复杂度:存储所有模板、实例和引脚,与输入规模成正比。
代码规模:约 600 行,是三次作业中结构最复杂、抽象程度最高的一次。
2.3.5 设计心得
模板与实例模式的价值:将子电路的“定义”与“使用”分离,避免了重复解析,支持复用和嵌套,这几乎是任何复杂系统建模的必备思想。
引脚抽象至关重要:将信号从元件中独立出来,使得连接关系更清晰,也方便处理跨层次的信号转发。
递归构建与计算:采用递归方式处理嵌套结构,代码简洁且易于理解,但需要注意避免循环引用(作业未要求处理)。
接口隔离(ConnectionContext):在构建过程中引入内部接口,使得顶层和子电路的构建逻辑可以复用,降低了 CircuitBuilder 的复杂度。
本次迭代的最大收获:从“单层组合电路”到“层次化子电路”的演进,让我深刻理解了软件工程中分治和抽象的重要性。同时,这次设计也为未来引入时序逻辑(如时钟、寄存器)预留了接口,体现了良好的扩展性。
三、采坑心得
(1)引脚编号解析错误
第5次作业中,译码器的引脚编号既有控制引脚(0,1,2)又有数据引脚(3,4,...),我在 setInput 中误将数据引脚从0开始索引,导致译码器输入错位,所有输出全反。后来仔细阅读题目引脚定义,才修正了映射关系。

(2)信号传播死循环
第4次作业使用 do-while 直接循环传播,未设置最大迭代次数。当电路中存在组合反馈(如输出直接连回输入)时,程序会无限循环。虽然题目未要求处理反馈,但安全起见,我增加了 maxIter 限制,超过则退出并标记未计算元件。

(3)正则表达式匹配失败
第5次作业中,组件命名如 A(2)1、M(3)2,我最初使用简单的 split 解析,遇到括号和数字组合时频频出错。后来改用 Pattern.compile("([AONXYSMZF])(?:\((\d+)\))?(\d+)") 统一提取类型、参数和编号,才彻底解决了解析问题。正则表达式的威力在此体现得淋漓尽致。

(4)输出排序不一致
两次作业的输出排序要求不同:第4次按类型顺序(AONXY)和编号排序;第5次按更复杂的顺序(AONXYSMZF)。我一开始硬编码了排序字符串,但发现类型顺序会变化,于是统一使用 typeOrderStr 变量,方便调整。避免魔鬼数字/字符串的习惯从此养成。

(5)数据分配器输出格式错误
第5次数据分配器要求输出形如 F1:0--(表示数据输出到第0路)。我最初直接输出数字,不符合格式,测试点全挂。后来才明白需要用字符数组填充 '-' 并设置选中位为数据值。这类格式细节虽然繁琐,但严格遵守题目要求是得分的关键。

四、改进建议

  1. 使用枚举代替字符串类型
    门类型、组件类型用 String 表示容易拼写错误,且 switch 语句缺乏类型安全。可定义 GateType 枚举,包含计算逻辑,使代码更健壮。

  2. 引入拓扑排序传播
    当前迭代传播法在元件数多时效率较低,且无法处理反馈。可以采用拓扑排序(若电路无环)或事件驱动队列,提高效率并支持更复杂电路。

  3. 分离解析、建模、仿真阶段
    目前所有逻辑混在 Main 中,可拆分为:
    Parser:解析输入和连接,生成元件对象和网络。
    Simulator:执行信号传播。
    Reporter:格式化输出结果。
    这样符合单一职责原则,便于测试和扩展

  4. 增加日志和调试支持
    调试时若能输出每个元件输入变化过程,将极大帮助定位问题。可加入 debug 开关,打印关键状态。

  5. 单元测试自动化
    当前测试完全依赖手动输入和人工比对,可改用 JUnit 编写单元测试,对每个元件独立验证,提高开发效率.

五、总结——这三次作业我到底学到了啥
(1)从“搬砖”到“设计”的蜕变
第一次作业,我只会用 Map 和 List 硬编码传播,代码像流水账。第二次作业,我开始抽象出 Component,发现原来“多态”和“继承”真的能让代码更干净。第三次虽然只有设计,但我知道了“时序”和“状态”的重要性——软件不只是处理当前输入,有时还要记住过去。

(2)Java技能实打实提升
集合框架:HashMap、ArrayList、LinkedHashMap 用得炉火纯青。
正则表达式:从畏惧到熟练,现在能快速写出匹配复杂命名的模式。
异常处理:学会了用 try-catch 处理解析异常,避免程序崩溃。
面向对象设计:抽象类、继承、多态、接口(虽然没用到)都有了实践。

(3)硬件知识不再是天书
以前觉得“译码器”“多路选择器”是硬件工程师的事,现在自己用代码实现了它们,明白了其工作原理,也理解了计算机组成原理的基础。

(4)踩过的坑都是财富
排序方向、引脚编号、格式输出……每一个坑都让我在下次更小心。现在写代码前会先画引脚定义表,输出前会对照格式检查三遍。

(5)未来还要学什么
设计模式:工厂模式创建元件,访问者模式遍历电路,这些都会让代码更优雅。
并发仿真:真实的硬件仿真需要并行事件处理,Java并发包值得深入学习。
图形化界面:若能画出电路图并交互调试,不仅有趣,更能加深理解。

posted @ 2026-06-24 21:36  MXY0531  阅读(3)  评论(0)    收藏  举报