EDA软件里的原理图编辑器听起来像是自带专业门槛的东西但真到动手自己写的时候你会发现它和普通图形编辑器没有本质区别只是要把“电气连接语义”同时塞进数据模型。这是手搓 EDA 项目的第二次汇报这次重点放在原理图编辑器上从创建原理图、摆放元件、连线到生成网表和基础检查整个闭环怎么搭起来。先说结论如果你想自己做一个原理图编辑器最值得先看的不是绘图算法而是数据模型和交互顺序。画布渲染是皮模型才是骨网表和检查是筋没有筋画出来的图只能叫装饰画。后面我按实际落地顺序拆数据模型、视图交互、网表验证、层次扩展、环境安装和踩坑。适合正在搭这类小工具的开发者也适合对 EDA 工具内部实现好奇的工程师。1. 这一版的定位不是画板是原理图编辑器1.1 先想清楚做的是“查看器”还是“编辑器”“手搓 EDA”这个系列本来就是从零写一套能在本地跑起来的电子设计自动化工具。上一版如果已经能加载原理图文件并渲染出来那这次最自然的增量就是让它能编辑。但“能编辑”这三个字容易误判。普通画板在画布上画几条线、拖几个矩形那叫图形编辑器原理图编辑器不一样它必须知道线是导线、矩形是元件方框、方块边缘的小点是引脚引脚和导线碰在一起代表电气连接。也就是说编辑操作不能只改图形坐标还要同步改数据模型里的连接关系。查看器可以只读取数据编辑器必须反过来用户画一笔模型变一次用户删一条线网表结果也要跟着变。这是查看器和编辑器最大的分界线。1.2 这次迭代要验证的功能清单我建议在实际动手前先把验收标准列出来不然很容易陷进画布性能里去。这次我只围绕一个闭环验证能否新建一张空白的原理图。能否从简单元件库中放置电阻、电容、芯片引脚等基础符号。能否选中元件并拖到指定网格位置。能否在引脚之间画导线并且系统识别出电气连接。能否用网络标签标记电源网络、信号网络。能否撤销和重做大部分编辑操作。能否从模型生成一个基础网表并发现未连接引脚和明显短路。能否在常见的中低配置环境下稳定启动和运行。光看这个清单重心就很清楚了渲染效果排在后面数据和交互语义排在最前面。如果你也希望项目有一个真正可用的核心我建议按同样的顺序做。2. 原理图数据模型先定地基再动鼠标2.1 模型和视图为什么要分开原理图编辑器最容易犯的错就是把画布上的图形对象直接当作数据对象来用。比如用户拖了一个电阻画布上多了一个矩形加两个引脚于是代码里就把矩形、引脚线段全部存成一个数组输出时再把这个数组写进文件。这个思路在 Demo 阶段能跑但继续往深做会非常难受。原因是同一份原理图数据往往需要多种表现方式编辑时用带网格、热点、高亮的视图。打印或导出图片时用纯渲染视图。生成网表时只关心引脚和网络不关心线段的颜色。电气检查时只关心引脚连接、网络标签、元件编号。如果模型和视图不分家这些逻辑就会全部混在一起。每次改一个功能可能都要处理画布坐标、颜色样式、命中检测容易出问题。更稳的做法是分层数据模型层保存元件、引脚、导线、网络标签等对象以及它们之间的连接关系。视图层负责把模型画出来管理缩放、平移、选中状态和绘制参数。命令层把用户的编辑操作封装成可撤销、可重做的动作。保存文件和生成网表时只从模型层拿数据画布只是模型的一份可视化展示。这样整个编辑器才不容易失控。2.2 元件、引脚、连线、网络的基本数据结构原理图里最核心的对象其实不多。我一般会简化成这几类# 示意结构用于讲清数据划分 class Schematic: components: list[Component] wires: list[Wire] net_labels: list[NetLabel] nets: list[Net] class Component: id: str # 元件唯一 ID refdes: str # 位号例如 R1、C2、U3 value: str # 值例如 10k、100nF symbol_name: str # 符号库里的符号名称 position: Point # 放置位置 rotation: float # 旋转角度 mirror: bool # 是否镜像 pins: list[Pin] # 这个元件实例上的引脚 class Pin: id: str # 引脚唯一 ID name: str # 信号名例如 CLK、A0 number: str # 引脚编号例如 1、2、14 symbol_position: Point # 引脚在符号本地坐标系中的位置 connected_net: Net # 当前引脚所属网络 class Wire: id: str points: list[Point] # 折线端点 width: float net: Net class NetLabel: id: str text: str # 网络名称 position: Point class Net: id: str name: str pins: list[Pin] wires: list[Wire]这里有个容易忽略的点元件符号定义里的引脚和元器件实例上的引脚不是同一个东西。符号库里的引脚是模板比如 74HC595 有 16 个引脚位置固定但放进原理图后每个实例都要有自己的引脚 ID才能记录这个引脚连到了哪个网络。也就是说模板引脚负责绘制外观实例引脚负责电气连接。网络也不一定要立刻生成完整列表。我的做法是编辑过程中允许网络对象处于“未命名”“暂未合并”的状态所有导联操作都只改连接关系真正整理网络列表放到网表生成阶段完成。2.3 撤销重做不能后面再补很多人搭画布交互时会把撤销重做放到最后结果越到后面越绝望。原理图编辑的撤销重做粒度不是“画了一条线”这么简单。用户会批量移动几个元件会连续删除一组导线会修改网络标签文字。如果只在画布上保存图形快照撤销时会出现画面恢复但网络关系没恢复的情况。我建议从第一版就做命令栈class Command: def execute(self): pass def undo(self): pass def redo(self): pass每个编辑动作都封装成一个命令放置元件对应 PlaceComponentCommand。移动元件对应 MoveComponentCommand记录移动前后坐标。画导线对应 AddWireCommand记录起点、终点、关联网络。删除导线对应 RemoveWireCommand。修改属性对应 SetPropertyCommand。命令执行时修改数据模型撤销时反向恢复。画布只是根据模型重新渲染。这样做的代价是前期代码量多一点但后面排查问题时省非常多时间。3. 画布交互按顺序做缩放、网格、命中、连线3.1 缩放平移和网格吸附是基础原理图编辑器的交互第一步不是连导线而是先保证画布能在不同缩放级别下稳定操作。常见原理图动辄几百个元件局部放大、全局浏览、平移画布是基本能力。缩放平移的实现本身不复杂关键是坐标转换要统一。我通常维护一个视口对象class Viewport: scale: float # 每像素对应的世界单位 offset_x: float offset_y: float鼠标事件传来屏幕坐标先转成世界坐标再去做命中检测和网格吸附。所有编辑操作都基于世界坐标画布上显示时再乘缩放比例。这里最重要的是不要在有缩放的情况下直接用屏幕坐标建数据模型否则保存的文件在不同机器上可能显示得完全不对。网格吸附要优先做。原理图不等同于流程图导线必须落在标准网格上后续生成网表、对接 PCB 才不会出现乱七八糟的偏差。常见的网络间距有 10、25、50、100 mil 等编辑器至少要支持其中一种作为默认网格。吸附逻辑可以做成在鼠标落点附近取最近网格点。def snap_to_grid(pos, grid25): x round(pos.x / grid) * grid y round(pos.y / grid) * grid return Point(x, y)我不建议把网格做成一堆硬编码常量。放到配置里让用户后续能改会灵活很多。3.2 用引脚热点做电气命中画原理图时鼠标点击最频繁的地方是引脚而引脚在画布上往往只是一个几像素的小方块或短线。如果只按图形边界做命中检测用户每次点引脚都要很小心体验会很差。所以我专门给引脚设置一个较大的热点半径。比如引脚视觉尺寸是 4 个像素但命中半径设为 8 到 12 像素或者在缩放后的世界坐标里设为固定容差。这样点引脚附近的区域都能准确选中引脚。命中顺序也很重要。我习惯按这个优先级引脚热点网络标签导线元件边框空白区域原因是引脚连接是电气语义最关键的部分网络标签次之导线再次元件整体拖动排到后面。如果引脚先被命中就能立即进入连线状态如果先命中元件边框则进入移动状态。3.3 布线和自动连接画线看似简单实际上要考虑的细节很多。我从最简单的路径开始用户点击引脚作为起点每点一次画出一段水平或垂直折线在终点再点引脚或双击结束。每次点击后代码要判断当前光标附近是否存在可连接的引脚或网络标签如果存在就把这段线归入对应网络。这里有个很关键的实现点导线和导线相交并不一定意味着电气连接。真实 EDA 工具里同一个坐标点上有导线相交还要看有没有 junction 节点或者是否在同一个网络里。做编辑器时我建议采用“线段端点连接”作为默认不要随便把两条交叉的线合并成同一个网络。也就是说用户把一根导线画到另一根导线的中间不应该自动认为二者连通。要么支持用户显式添加连接点要么要求用户必须从引脚或从已有导线的端点开始新的连线。这样处理能极大减少后面网表生成时的误判。自动连接则是一个加分项。当导线终点落在引脚热点范围内系统自动把导线吸附到引脚中心并建立连接当导线终点和网络标签重合就赋予这段导线对应网络名称。人画原理图时真正精确“点到引脚中心”的次数不多大部分依赖这种吸附。3.4 选中和属性修改画布上选中一个元件后右侧应该能显示可编辑属性比如位号、值、封装、旋转角度。这些属性直接写回数据模型同时触发视图刷新。我踩过的一个明显坑是修改属性后没有同步更新引脚网络。比如把某个电阻位号从 R1 改成 R2其实网络不会变但如果修改的是元件符号类型比如从普通电阻换成可变电阻引脚数量变了原来的导线连接关系就要重新检查。所以在实现属性面板时至少要做两件事修改普通属性只做刷新。修改符号或引脚数量后强制重新验证所有连接。不要等用户手动点保存才检查编辑过程中保持模型一致性才能避免保存后才发现连接丢失。4. 网表生成和基础检查画完不等于画对4.1 网表是什么为什么不能从画布直接读网表是原理图最核心的输出之一。它描述的是哪些元件、哪些引脚、连到了哪个网络是后续仿真、PCB 布局布线、生产制造的基础。网表生成必须从数据模型读取而不是从画布读取。如果从画布读取屏幕上的缩放模式、绘制状态、隐藏对象都可能影响结果甚至导致同样的原理图在不同视口下生成不同网表。一个常见网表长这样逻辑上是一组网络描述(net VCC (pin U1.14) (pin U2.8) (pin C1.1) ) (net GND (pin U1.7) (pin C1.2) )字段不用完全一致关键是网络和引脚之间要有稳定映射。4.2 网表生成的整体流程我习惯把网表生成分成四步从模型里收集所有元件实例引脚。收集所有导线和网络标签。把导线端点、引脚、网络标签按坐标和连接关系建立图结构。用并查集或深度优先遍历找出连通分量每个连通分量对应一个网络。如果某个网络贴了网络标签就用标签文本作为网络名没有标签就用自动生成的名称比如 Net1、Net2。这里有个细节容易出错当两条导线首尾相接但没有形成“闭口”时它们应该算同一网络当导线只是视觉上交叉时默认不算同一网络。所以在建图时我只会把导线端点作为连接点不会把“线段中点附近有另一条线跨过”作为连接依据。如果项目需要支持真正的交叉连接那就要显式建模 junction 节点或者要求用户使用网络标签。否则网表生成的误连率会很高。4.3 先做四类基础检查原理图检查在早期版本里不需要做特别多先做四类最影响可用性的就够了未连接引脚。有些引脚没有任何导线和网络标签需要提示用户。重复位号。两个元件都叫 R1会导致网表冲突。单端网络。一个网络里只有一个引脚大概率是虚连或悬空。网络标签悬空。有标签但没有连到任何引脚或导线。检查的顺序可以这样先检查元件位号是否重复。再检查引脚是否都有网络归属。然后检查每个网络的引脚数量。最后检查网络标签和导线是否指向有效对象。只要这四类检查能跑通基础原理图质量就有保障了。4.4 检查结果怎么展示检查结果如果没有可定位信息用户会很难用。我建议把结果写到界面侧边栏每条错误都带一个可点击字段点击后在画布上定位到对应元件或引脚。错误信息要尽量具体“R1 的引脚 2 未连接”比“存在未连接引脚”有用。“网络 5V 只有一个引脚 C1.2”比“网络连接异常”有用。如果暂时不想做复杂定位至少把错误列表按原理图对象 ID 排序并导出成文本日志。5. 层次原理图先做一个能用的扩展架子5.1 单页放不下时才需要层次结构原理图不大时一张纸能画完不需要层次。但工程一旦变复杂比如嵌入式主板、电源板、控制板混在一起单页就会变成蜘蛛网。层次原理图的核心思路是父原理图放若干个方框每个方框对应一张子原理图方框上的端口和子原理图里的端口一一对应。5.2 层次块的数据模型为了让已有代码尽量复用我建议把层次块也设计成一种特殊元件class HierarchicalBlock: id: str name: str # 块名例如 MCU_POWER child_schematic: str # 子原理图文件 ports: list[BlockPort] # 块上的端口块上的端口相当于引脚只是它连接的是子原理图里的网络。这样在父原理图里块可以和普通元件一样被拖动、旋转、删除在子原理图里端口像普通网络标签一样存在。模型统一之后画布交互不需要为层次块写特别多新逻辑。5.3 网表怎么展开层次生成网表时如果遇到层次块一般需要做实例路径展开。比如父原理图里有一个块叫 PWR子原理图里有一个网络叫 VCC那么导出网表时应该是 PWR.VCC 或者 PWR/VCC避免不同实例之间网络名冲突。这个过程可以递归处理从顶层原理图开始遍历元件。遇到普通元件时把它的引脚加入当前实例路径下的网络。遇到层次块时递归加载子原理图并把实例路径追加为前缀。最终把所有网络拼接成一个平铺网表或者保持层次网表格式。层次展开时最容易遇到的问题是同一个子原理图被两块父方框引用。比如两个电机驱动电路都用同一张子原理图但它们是两个不同实例。此时必须给每个实例一个唯一路径前缀否则电机 A 和电机 B 的网络会混在一起。6. 安装、运行和常见问题排查6.1 安装依赖顺序如果是在互联网上搜“EDA软件安装”通常搜到的都是商业工具的安装流程。我这里说的“安装”是指把自研项目在本地跑起来顺序不太一样。手搓项目的依赖一般不难常见组合可能是 Python 加图形库或者 Node 加 Web 渲染或者 C 加 Qt。无论哪种我都建议按这个顺序安装安装基础运行时比如 Python 或 Node。创建虚拟环境或独立目录避免污染全局环境。安装项目依赖看 requirements.txt、package.json 或 pyproject.toml。尝试运行示例文件或测试用例。确认图片资源、字体、配置文件路径能被正确读取。很多启动失败不是因为核心代码有 bug而是依赖版本不对或配置文件缺少。6.2 启动失败排查链路如果你的编辑器启动就报错或者界面白屏、按钮没反应我建议按这个顺序排查看命令行窗口有没有报错信息。确认依赖版本特别是图形库和底层视窗库。检查当前工作目录是不是项目根目录样本文件路径是否有效。确认系统缺少的字体、默认渲染驱动、OpenGL 支持是否正常。如果用了 Web 技术检查端口是否被占用服务是否真的启动。先把日志打开再改参数。不要一上来就怀疑关键模块。6.3 低配置机器能跑吗原理图编辑器本身不像视频渲染那么费资源低配置机器跑起来压力并不大。数据模型里大多只是几百到几千个对象普通 CPU 完全够用。但有两个点会影响性能渲染是否逐帧全量重绘。如果每帧都重新画几百条线、几百个元件会明显卡顿。是否做了无效的命中检测。命中检测跑全量对象和跑空间索引性能差别很大。如果低配置环境里拖动卡顿优先优化局部刷新和空间索引不要急着加昂贵特效。7. 边界、坑点和下一步7.1 这个版本还不能做什么自己手搓的原理图编辑器和商业 EDA 工具之间还是有明显差距的。这个版本能解决的核心问题是原理图编辑数据模型、基本交互、网表生成和基础检查。还没做的通常包括大规模元件库管理。从原理图到 PCB 布局布线的对接。仿真需要的模型和波形展示。团队协作和版本对比。大量线上丝印、BOM、ERP 集成。这些问题不是一个原理图编辑器阶段能解决的。把这一版做扎实后面补这些才靠谱。7.2 我的建议如果你也在做类似工具我的建议是先把单页原理图的编辑闭环跑通再做层次。先做网表生成再做复杂检查。先支持一种稳定的输出格式再考虑兼容多种格式。不要把大量时间花在花哨画布效果上数据准确性永远第一位。原理图编辑器真正落地时最该盯住的不是功能列表而是输入格式、资源占用和网表结果的一致性。只要模型稳定画布怎么换都行模型一旦混乱后面全是补丁。踩过几次之后我发现很多问题不是工具能力不够而是前置环境和数据模型没有整理干净。希望这个系列的记录能让你少走两个弯路。