公司动态
手搓EDA原理图编辑器:从零实现画布、连线与网表导出
这次“手搓EDA软件”系列来到第二期主题很聚焦原理图编辑器。上一期如果把整体框架和设计输入流程讲清楚了这一期就要落到真正动手画图的环节。用一句话概括这一期要验证的问题不靠成熟的第三方EDA内核从零手搓一个能画、能改、能导网表的原理图编辑器到底要解决多少问题。只看“画布上放几个元件拉几条线”这个表面效果原理图编辑器看起来不难。但它真正的工作量集中在几块看不见的地方元件符号库的组织方式、引脚与封装的一一映射、导线的电气语义、层次化图纸之间的跨页信号传递以及最终能不能生成一个下游PCB编辑器认的网表。这些功能没有一项能靠“画线工具”糊弄过去。这篇文章会从原理图编辑器在EDA链路中的位置讲起拆解画布交互、元件管理、连线与电气规则、网表导出这几个核心模块的设计思路再用表格对比手搓方案和嘉立创EDA、KiCad这类成熟工具的差距。最后给出开发过程中最容易踩的坑和排查清单。适合想了解EDA软件内部结构、或者打算自己折腾一个轻量原理图工具的朋友阅读。1. 手搓EDA原理图编辑器的核心能力速览能力项说明项目定位从零自研的轻量级EDA工具本篇聚焦原理图编辑器核心功能元件符号放置、导线连接、属性编辑、网格吸附、网表导出设计输入原理图绘制保存为自定义工程格式输出产物元件连接关系、引脚网络、层级设计、网表文件面向用户电子设计学习、教学演示、轻量级设计验证与PCB衔接通过网表和封装映射进入布局布线环节成熟度功能覆盖基础流程与商用工具仍有明显差距适合场景教学演示、工具链自主可控验证、轻量场景从材料来看这是一系列连续的技术汇报第二期把重心放在原理图编辑器的实现与功能展示上。为了避免夸大表格里没有写具体版本号、性能数字和安装包体积这些以实际发布内容为准。唯一可以确定的是这一期已经在“能跑起来”的层面上展示了原理图编辑器的完整交互流程。2. 原理图编辑器在EDA工具链中的位置先把链路说清楚。一套完整的EDA设计流程通常包含原理图设计、仿真验证、PCB布局布线、制造文件输出。原理图编辑器处于最前端负责把“电路功能意图”转成“机器可识别的连接关系”。具体拆开看原理图编辑器承担三件事以图形化方式表达电路用户放置电阻、电容、芯片等元件符号用导线把引脚连起来形成一张人可读的图纸。建立电气连接数据每条导线的两个端点、每个引脚的网络名、元件的位号与参数这些是后续一切流程的基础。输出通用网表把原理图转成网表文件。网表里有元件列表、封装名称、网络连接关系。PCB编辑器拿到网表之后才知道要摆放哪些元件、哪些引脚要连在一起。所以原理图编辑器不是孤立存在的。它的核心资产是“电气连接信息”不是绘图文件。很多自研EDA工具早期都能在画布上画得很好看但一导出网表就露馅引脚编号对不上、封装映射缺失、导线悬空导致网络断开。这些都是原理图编辑器必须解决的数据一致性问题。这个项目目前没有公开完整的源码级文档所以下面给出的实现复盘会按“常见设计思路 数据结构示例”的方式展开。具体到项目里如果改了存储格式就按自己的工程结构对号入座。3. 手搓原理图编辑器的技术选型与架构设计先谈技术选型。一个原理图编辑器通常有两个方案路线桌面端应用C/Qt 或 C#/WinForms适合做专业工具性能强但开发和打包成本高。Web 端应用Canvas/SVG加TypeScript部署容易、界面现代适合做跨平台工具核心难点在浏览器渲染性能和快捷键交互细节。从近两年开源社区自研EDA项目的趋势看Web 方案越来越常见。原因很直接Electron 或纯浏览器方案可以快速做出图形界面元件库用 JSON 维护网表导出用模板字符串拼整个闭环不需要重型的 C 工程。嘉立创EDA这类工具在浏览器端能跑出接近桌面软件的体验也说明Web技术栈在二维图形编辑场景下已经足够成熟。架构上要把数据层和渲染层分开。数据层负责保存元件、引脚、导线、网络这些对象渲染层只负责把数据画到画布上。这样做的好处是检查电气规则时不用关心画布坐标导出网表时也不用碰界面逻辑。一个典型的面向对象模型可以这么拆数据对象职责CircuitDocument整个原理图文档包含页面列表Sheet一页图纸包含图元列表ComponentInstance元件实例引用符号库符号包含位号、值、坐标、旋转角度PinRef引脚引用指向符号上的引脚IDWireSegment导线段包含起点和终点的网络终端Net网络聚合多个引脚和导线连接点用 JSON 保存的工程文件大概长这样{ version: 1.0, sheets: [ { id: sheet1, title: Power Supply, components: [ { id: C1, symbol: resistor, designator: R1, value: 10k, x: 120, y: 80, rotation: 0 } ], wires: [ { id: w1, from_pin: R1.pin1, to_pin: C1.pin2, points: [[120, 80], [200, 80], [200, 160]] } ] } ] }这个结构的好处很直接数据层和渲染层完全解耦。要支持撤销重做只需要对文档对象做命令模式要支持跨页网络只要在网络表里维护全局网络名。UI 层建议用分层 Canvas 绘制绘制顺序一般是背景网格层、导线层、元件符号层、标注层、交互层。这样缩放平移时只需要重绘可视区域性能基本可控。交互层单独放一层可以避免鼠标提示和临时高亮污染底图数据。4. 核心功能拆解与实现思路原理图编辑器的功能模块可以拆成六块画布交互、元件库、符号编辑、导线连接、属性编辑、网表导出。下面逐块展开。4.1 画布交互缩放、平移、网格吸附画布是所有交互的基础。原理图编辑最常用的操作就是放元件、找引脚、连线。没有缩放手感后面都白搭。实现时要注意几个点缩放围绕鼠标锚点进行而不是围绕画布中心否则用户操作时视角会跳。网格吸附建议做两级放置元件时吸附大网格画导线端点时吸附更细的网格两者可以独立配置。平移时保持元件和导线的相对坐标稳定不要在缩放过程中累积浮点误差。画布交互层保存一个 view 对象每次重绘都用同一个变换矩阵interface ViewTransform { scale: number; tx: number; ty: number; } function screenToWorld(sx: number, sy: number, view: ViewTransform) { return { x: (sx - view.tx) / view.scale, y: (sy - view.ty) / view.scale }; }如果开发过程中发现“鼠标在缩放后点不到导线端点”这种问题先别急着调吸附逻辑第一步应该检查 screenToWorld 的变换是否正确。坐标变换错误会引发连锁问题而且往往只在缩放比例接近整数倍时露馅。网格吸附的细节在于“吸附优先级”。当鼠标附近同时有元件引脚和网格点时引脚应该优先吸附。否则用户明明拖到引脚附近最终落在网格点上连线时电气连接就断了。4.2 元件符号库与放置元件符号库是原理图编辑器最先要补的“地基”。没有库用户连一个电阻都得手动画边框、画引脚、填引脚名体验非常差。一个最小可用的符号库每个符号至少要包括三部分符号图形线段、矩形、圆形、文本用图元数组表达。引脚定义引脚名、引脚号、电气类型例如输入、输出、电源、地。封装映射每个引脚对应的PCB封装焊盘编号例如原理图引脚2对应SOT-23封装焊盘2。用 JSON 表达电阻符号{ symbol: resistor, name: Resistor, pins: [ { name: 1, number: 1, type: passive }, { name: 2, number: 2, type: passive } ], body: [ { type: rect, x: -30, y: -10, w: 60, h: 20 }, { type: pin, name: 1, number: 1, x: -40, y: 0, direction: left }, { type: pin, name: 2, number: 2, x: 40, y: 0, direction: right } ], footprint: R_0805 }放置元件时用户从库面板拖拽符号进入画布此时创建的 ComponentInstance 引用符号ID并复制一份实例数据。这里要强调实例和符号必须分离。全局改符号定义时已放置的实例可以选择同步更新或保持旧样子这套机制和 KiCad 的 symbol library 与 schematic 元件之间的关系类似。元件库管理是个长期工作。嘉立创EDA、立创EDA这类工具之所以上手快很大程度是因为自带海量元件库用户不用自己画符号、填封装。手搓工具前期可以内置一个几十个常用元件的精简库先把流程跑通再逐步扩充。扩充时优先补常用无源器件和常见单片机、电源芯片这部分覆盖了大多数教学和原型验证场景。如果需要导入外部符号可以考虑支持标准格式的Symbol文件或者做一个从 CSV/JSON 批量导入的小工具。这能缓解自建库的工程量但要注意导入数据的字段映射不能只导图形不导引脚。4.3 导线连接与网络语义画线是原理图编辑器里电气语义最重的操作。表面上是连一条线实际上是建立一个网络连接。连接判断的核心逻辑有这几个导线端点在另一个元件引脚附近时自动生成连接点。导线端点在导线中间位置时自动把导线拆成两段并成为一个电气节点。网络名标注与导线关联同一网络名下所有端点逻辑上是连通的。这里最容易出问题的是“导线悬空”。用户在视觉上点了一下导线端点看起来接上了但数据层没有做引脚吸附电气上并没有连通。所以在实现导线工具时必须实时检测端点附近有没有可连接的引脚或线而不是只记录鼠标坐标。可以用一个简单的“最近连接点”检测function findClosestConnectionPoint(point: Point, sheet: Sheet, tolerance: number) { let best null; for (const pin of sheet.pins) { const dist distance(point, pin.position); if (dist tolerance (!best || dist best.dist)) { best { dist, target: pin }; } } for (const wire of sheet.wires) { for (const pt of wire.points) { const dist distance(point, pt); if (dist tolerance (!best || dist best.dist)) { best { dist, target: { wire, pt } }; } } } return best; }连接点命中之后再做“合并到同一网络”的数据操作。数据模型上网络用一个 Net 对象聚合多个 PinRef 和 WireSegment。每次画完线就扫描一次把相连的元素合并进同一个 Net。这一步做完还应该给用户一个可视反馈同一个网络的高亮颜色一致悬空引脚变成警告色。没有这个反馈用户在复杂图纸里很难发现漏连的地方。4.4 属性编辑与层次化图纸属性编辑模块决定软件好不好用。用户点一个元件要能修改位号、阻值、型号、描述点一根导线要能改网络名。实现属性面板并不复杂但需要数据模型支持“每个对象都有可配置的键值对”。建议 ComponentInstance 上直接挂一个 attributes 字典{ id: C1, symbol: capacitor, designator: C1, attributes: { Value: 10uF, Voltage: 50V, Footprint: C_0805, Datasheet: https://example.com/datasheet.pdf, Manufacturer: Example } }这样即使用户增加自定义属性也不会破坏数据结构。属性面板里还可以区分“符号自带属性”和“实例覆盖属性”后者优先级更高这样既能继承库里的默认参数又允许单实例调整。层次化设计是原理图编辑器的进阶功能。大板子不可能一页画完需要多页图纸、子图调用。常见做法是支持 SheetSymbol把一张子图作为一个“元件”放置到父图中子图的输入输出网络通过端口符号映射到父图导线。层次化实现的核心是端口匹配机制。子图里的端口符号定义网络名父图里引用子图时端口名必须与子图一致。不一致时要有明显的DRC报错不然跨页网络很容易变成悬空网络。4.5 网表生成原理图编辑器的最终输出是网表。网表格式有通用格式和工具私有格式自研工具一般先支持Spice格式或者自定义简化格式。一个简化网表可以这样组织[components] R1 resistor R_0805 10k C1 capacitor C_0805 10uF U1 ic SOP-8 MyChip [nets] VCC U1.pin1 R1.pin1 GND U1.pin2 C1.pin2 NET_1 R1.pin2 C1.pin1生成网表的算法核心是图遍历从某个引脚出发沿导线走找到所有同网络引脚。伪代码def collect_net(start_pin): net [] visited_pins set() stack [start_pin] while stack: pin stack.pop() if pin.id in visited_pins: continue visited_pins.add(pin.id) net.append(pin) for wire in pin.connected_wires: for other_pin in wire.other_pins(pin): stack.append(other_pin) return net生成网表后要做两项检查每个元件至少有两个引脚连接没有未命名的悬空网络。如果条件满足网表就可以交给PCB编辑器或者仿真工具。这里还有个容易被忽略的问题元件排序稳定性。同一个原理图多次导出的网表如果元件顺序每次都不同会给对比审核带来麻烦。建议在导出前对元件ID做一次稳定排序。4.6 撤销重做与命令模式画图工具最怕误操作。一个成熟的原理图编辑器必须有不小于50步的撤销栈。实现上建议用命令模式把每个操作封装成命令对象放置元件PlaceComponentCommand删除元件DeleteComponentCommand移动元件MoveComponentCommand画导线AddWireCommand删除导线DeleteWireCommand修改属性ChangeAttributesCommand每条命令实现 execute 和 undo 两个方法。工程文档的每次修改都通过命令对象执行而不是直接改数据。这样撤销重做功能在早期就具备不需要后期重构。命令模式还能顺手解决“批量操作回滚”的问题。例如用户导入一个包含20个元件的库中途失败时可以整体回滚不会留下半个导入的脏数据。4.7 快捷键与交互细节原理图编辑器最常用的操作集中在鼠标和键盘上快捷键设计直接影响使用效率。至少要有这些基础操作V/W切换选择工具和导线工具R旋转元件X/Y水平或垂直翻转Delete删除选中对象CtrlD复制并粘贴到当前位置CtrlZ / CtrlY撤销/重做F适应窗口缩放到全图CtrlG切换网格吸附元件旋转时要注意旋转的不仅是图形还包括引脚连接点坐标。如果只旋转图形不旋转引脚导线上已经建立的连接关系就会错位这也是一个常见Bug来源。5. 从原理图到PCB设计的衔接原理图编辑器做完整个工具链才刚开始。后面的PCB编辑器要消费网表所以封装映射必须从原理图阶段就管起来。一个典型流程原理图里每个元件都要绑定 Footprint 字段。网表导出时输出位号、值、封装和网络连接关系。PCB编辑器读取网表后按封装名从PCB封装库中实例化封装。用户进行布局布线再根据网络关系检查连通性。手搓EDA在这个环节最容易出现一个问题原理图引脚名和封装焊盘名对不上。比如原理图里引脚叫A、K封装焊盘叫1、2虽然网表能显示两个网络都连到了元件上但PCB阶段就分不清哪个网络应该连到哪个焊盘。解法是在元件符号的引脚定义里增加 footprintPin 字段显式写清每个引脚对应封装上的哪个焊盘{ name: A, number: 1, type: passive, footprintPin: 1 }这个字段如果从一开始就进数据模型后面做DRC、做仿真、做BOM都会省事很多。实际开发中这个字段可以在符号库创建时维护也可以在导入第三方库时自动映射但一定要确保导出网表前已经完整校验过。6. 原理图编辑器日常使用流程这部分从用户视角看一个原理图编辑器怎么用。手搓工具的核心使用流程可以分成五步第一步新建工程设置图纸尺寸。建议默认使用标准A4或A3纵向图纸方便打印和PDF导出。图纸尺寸在后续排版时不一定固定编辑器要支持随时调整。第二步从元件库拖入元件。这一步需要库面板支持搜索和分类过滤。没有搜索功能的库面板在元件数量超过200个以后基本不可用。第三步摆放元件。拖到画布后用空格或R旋转方向X/Y翻转然后按网格位置摆放。摆位时要留出足够的连线空间避免之后导线交叉过多。第四步连线。选择导线工具后从元件引脚点击开始拉线经由拐点连接到另一引脚。双击结束连线结束点必须落在引脚或导线连接点上。第五步填写属性并导出。给元件填位号和值给关键网络命名然后执行网表导出和DRC检查。检查通过流程结束。这五步看起来简单但每一步背后都有数据结构支撑。一个能顺畅完成这五步的编辑器已经具备作为轻量教学工具的基本条件。7. 对比手搓方案与嘉立创EDA、KiCad 的差距先说明手搓EDA的定位和嘉立创EDA、KiCad不同。后两者是成熟的工具产品覆盖从元件库、原理图、仿真、PCB到制造输出的完整链路。手搓方案的价值在于链路自主可控、代码可定制、教学演示直观。维度嘉立创EDA / KiCad手搓原理图编辑器元件库海量自带库在线搜索一键放置前期库少需自建或导入原理图绘制交互细节完善快捷键丰富基础绘制体验打磨需要时间仿真支持电路仿真通常没有或接入第三方仿真器PCB设计成熟的多层板设计规则检查完善需配套开发或暂不支持网表生成格式标准兼容性高内部格式能满足自用流程协作与版本管理支持团队协作、云端工程多数是单机文件格式成本闭源工具部分收费开源工具免费开发成本集中在人力一个很现实的经验是手搓工具想要达到日常可用的水平工程量比很多人想象的大。画布不难难的是元件库维护、规则检查、数据兼容这几块。如果一个项目只是想快速完成一块PCB直接用成熟EDA才是最稳妥的选择。嘉立创EDA覆盖了从原理图绘制、PCB布局布线、仿真到制造输出的完整流程很多工程师已经在用它做实际项目。比如在嘉立创EDA里画PCB时做开窗处理、布等长线、利用STP模型生成PCB封装、下载3D模型、配置2.54间距杜邦焊盘孔这些都是非常具体且高频的设计操作。手搓工具要想替代这部分工作工作量远超“画个原理图编辑器”本身。但如果目标是研究EDA数据结构、做一个教学演示工具、或者在公司内部做深度定制自研原理图编辑器完全值得。成熟的商业EDA产品通常不支持用户深度修改内部数据流而自研工具可以把元件库、网表格式、规则检查逻辑都开放出来。嘉立创EDA支持从原理图到PCB的完整流程也支持导入导出标准格式自研时候可以先把自己内部格式定义好再做一层导入导出的适配器而不是一开始就去支持复杂的企业级格式。8. 开发过程中的常见问题与排查方法以下清单按开发原理图编辑器时最高频的问题排列。问题现象可能原因排查方式解决方案元件拖入画布后消失坐标变换错误元件画在可视区域外打印元件世界坐标和视口坐标检查 screenToWorld 的缩放和平移计算导线看起来连上但网表缺失网络端点没有生成电气连接点导出网表前检查悬空引脚在导线端点做最近连接点吸附未命中时提示引脚与封装焊盘编号对不上符号引脚缺少 footprintPin 字段比对网表和封装焊盘信息在符号库数据中显式维护映射关系元件库中修改符号后已放置实例变形实例直接引用符号图形未保存快照检查实例数据是否包含图形拷贝实例与符号分离支持按ID同步或保留旧版缩放时定位偏差越来越大变换矩阵中浮点误差累积观察多次缩放后鼠标所在位置使用 double 精度避免反复累加缩放系数网络名重复但未合并网络合并逻辑只在连线时触发在所有编辑操作后重新构建网络表做一次全局网络重建按导线连通性分组撤销操作后连接点失效撤销命令未处理导线拆分段回放操作日志用命令模式统一撤销合并拆分导出网表时元件顺序不稳定集合遍历顺序随机多次导出对比对元件ID排序后再导出排查思路永远是从数据层验证不要只看图形层。图形看起来正确不代表电气数据正确。开发时可以在调试工具里直接打印每个 Net 包含的引脚列表和用户视觉上的连线做比对快速确认电气语义是否正确。9. 后续扩展方向建议原理图编辑器做到能画、能存、能导网表只是第一步。后续有价值的方向有这几个。第一个是DRC设计规则检查。原理图阶段可以做基础检查比如未连接引脚、重复位号、网络名冲突、元件缺少封装。这套检查规则不复杂但非常实用能大幅减少流到PCB阶段的低级错误。规则可以做成插件式用户自己写规则脚本工具内置默认规则集。第二个是仿真对接。原理图里包含完整元件参数和连接关系天然可以生成Spice子电路。如果元件库里能配置Spice模型就能从“画完验证不了”进阶到“画完先仿真”。仿真结果能反馈接线和数据模型问题是验证原理图正确性的一个重要手段。第三个是BOM导出。从原理图元件实例的 attributes 中提取位号、值、封装、品牌、采购编号生成BOM表格是一件高回报低成本的工程。很多自研工具优先做网表却忽略BOM实际上BOM在企业生产场景中同样刚需。BOM还可以和采购系统对接直接输出可编辑的Excel或CSV。第四个是外部格式兼容。如果有能力做 KiCad 格式导入导出等于获得了一套庞大的元件库生态。直接用开源项目的格式定义比自己从零维护元件库要快得多。但要注意开源项目有许可证要求集成前要做合规评估不能直接把有传染性许可证的代码搬进闭源项目。第五个是原理图与PCB的交互选择。成熟EDA里用户点击原理图元件PCB上会高亮同一个元件。这个功能在自研工具中实现需要打通两侧数据模型属于工具链成熟的标志之一。10. 小结这套手搓EDA适合谁说一点更实际的判断。手搓一张原理图编辑器如果你只是想快速做设计那直接用嘉立创EDA这类工具效率高得多。嘉立创EDA的安装、封装生成、3D模型下载、开窗、等长线布线等操作都有现成方案社区教程也多完全可以覆盖日常设计任务。如果你是对EDA工具内部机制感兴趣想理解原理图数据是怎么组织、网表怎么生成、从原理图到PCB这个链路是怎么衔接的那手搓这样一个小工具比看十篇理论文章都有效。自己亲手实现一次符号库、引脚、导线网络这几层数据结构后再回头用任何EDA软件理解都会不一样。这一点是这个系列最值得坚持的地方。这一期“看看原理图编辑器”的核心收获可以浓缩成三句话图形只是表象电气数据才是原理图的本质元件库、引脚映射、网表生成这三块决定工具能不能用与成熟EDA相比手搓方案短在生态长在可控。下一期的重点自然会落到PCB编辑器如何承接这份网表继续把手搓EDA的链路补完整。