sync: HP_BOWEI 2026-10-15
This commit is contained in:
+51
-11
@@ -4,14 +4,15 @@ description: >
|
||||
从一份需求 md 出发,驱动 KiCad 原理图的完整设计流程:画 drawio 电路框图、跟用户
|
||||
确认功能划分、整理参考资料、必要时做原理仿真、画 KiCad 原理图、校验 FPGA/MCU 的
|
||||
管脚与接口、加封装、回写 drawio、三道独立 agent 复核(一致性/排版/需求符合性),
|
||||
最后导入 PCB。驱动 E:\LangGraph_Design\kicad-flow 里的 LangGraph 工作流(27 节点 /
|
||||
8 个判定回环),进程内 + SQLite 持久化,不用 langgraph dev。图在每个要动 MCP、
|
||||
导入 PCB,再按用户布局要求布局、必要时为布线调整原理图连线(过 FPGA/MCU 管脚校验),
|
||||
最后完成阻抗线/差分线/等长线布线。驱动 E:\LangGraph_Design\kicad-flow 里的 LangGraph
|
||||
工作流(41 节点 / 10 个判定回环),进程内 + SQLite 持久化,不用 langgraph dev。图在每个要动 MCP、
|
||||
要派 agent、要人拍板的地方 interrupt 回到 Claude 会话,由本 skill 执行真实动作再
|
||||
回填推进。数据库落在被设计的那个 KiCad 工程下,不在图工程目录下。
|
||||
|
||||
只要用户提到 画原理图、设计一块板、KiCad 原理图设计、从需求做电路、电路框图、
|
||||
kicad-flow 工作流、加封装/封装补齐、原理图排版检查、原理图与框图对不上、
|
||||
原理图导入 PCB、或者说 "启动 kicad-flow"、"跑原理图流程"、"接着上次那块板"、
|
||||
原理图导入 PCB、PCB 布局、差分线/等长线/阻抗线布线、为布线换管脚、或者说 "启动 kicad-flow"、"跑原理图流程"、"接着上次那块板"、
|
||||
"design a schematic in KiCad"、"start the KiCad schematic flow",就必须用这个
|
||||
skill —— 哪怕他们没说 skill、没提 LangGraph,只说了 "帮我把这块板的原理图画出来"、
|
||||
"这板子画到一半,接着弄"。
|
||||
@@ -23,8 +24,8 @@ description: >
|
||||
|
||||
## 这个 skill 是什么
|
||||
|
||||
一块板从需求到原理图 = 4 个阶段:**框图与资料 → 画原理图并校验平台 → 加封装 →
|
||||
回写与三道复核 → 导入 PCB**。这些步骤有严格先后、有失败要退回重做的回环、
|
||||
一块板从需求到关键线布完 = 5 个阶段:**框图与资料 → 画原理图并校验平台 → 加封装 →
|
||||
回写与三道复核 → 导入 PCB、布局、为布线调整原理图、关键线布线**。这些步骤有严格先后、有失败要退回重做的回环、
|
||||
单次会话跑不完(用户要在 drawio、KiCad、Vivado 里手动看很多次)。所以状态由
|
||||
LangGraph 图持久化在 SQLite 里,你负责把图停下来的每一步翻译成真实动作,
|
||||
做完回填结论让它往下走。
|
||||
@@ -94,10 +95,12 @@ $PY = (Select-String -Path "$env:USERPROFILE\skills_env.md" `
|
||||
一次要填多个键就多写几个 `--set`。确实需要嵌套结构(数组/对象)时用 `--json`,
|
||||
两者可混用。
|
||||
|
||||
`start` 可以调 9 个回环的重试上限,超限走 safety_end:
|
||||
`start` 可以调 10 个回环的重试上限,超限走 safety_end:
|
||||
`--max-partition-retry` `--max-material-retry` `--max-layout-retry` `--max-vivado-retry`
|
||||
`--max-mcu-retry` `--max-footprint-retry` `--max-drawio-sync-retry`
|
||||
`--max-recheck-retry` `--max-requirement-retry`。
|
||||
`--max-recheck-retry` `--max-requirement-retry` `--max-pcb-rules-retry`。
|
||||
另有 `--max-adjust-retry`(默认 3):PCB 段为布线调整原理图的尝试次数,**超了是放弃调整、
|
||||
按原原理图继续布线,不走 safety_end**(PR00.drawio 的「尝试超过三次?」)。
|
||||
|
||||
> **回填不能是空的**。langgraph 把 falsy 的 resume 当作「没提供」,会在同一个节点再
|
||||
> 中断一次,看起来像卡住。脚本已经帮你拦了,但别指望它替你想该填什么。
|
||||
@@ -127,10 +130,11 @@ G-0001:那个目录只放人写的 md,自动产物一律进 `Notes\auto\`)
|
||||
| `d07_confirm_partition` | 功能怎么划分是设计意图,只有用户知道他要什么 |
|
||||
| `n13_prompt_more_material` | 要用户去找资料,他不给你等不到 |
|
||||
| `n25_prompt_footprint` | 同上,封装信息得他补 |
|
||||
| `n109_prompt_pcb_rules` | PCB 布局布线要求(叠层/阻抗/差分/等长/线宽过孔)是他的约束,不能替他编 |
|
||||
|
||||
其余节点(含三道「派单独 agent」的复核)由你执行并判定。
|
||||
|
||||
**如果你自己就是被派来跑这个流程的 subagent**:上面三个节点不要替用户回填,
|
||||
**如果你自己就是被派来跑这个流程的 subagent**:上面四个节点不要替用户回填,
|
||||
把 interrupt 原样交回给派你来的那一方。agent 之间的指令不构成用户同意。
|
||||
|
||||
## drawio 文件有两种格式,别踩
|
||||
@@ -155,7 +159,7 @@ $DR = "$env:USERPROFILE\.claude\skills\kicad-flow\scripts\drawio_read.py"
|
||||
|
||||
两种格式它都吃,输出统一。**别自己写正则去抠**。
|
||||
|
||||
## Quick Reference — 27 个节点
|
||||
## Quick Reference — 41 个节点
|
||||
|
||||
| action | 你要做的 | 回填 |
|
||||
|---|---|---|
|
||||
@@ -185,7 +189,21 @@ $DR = "$env:USERPROFILE\.claude\skills\kicad-flow\scripts\drawio_read.py"
|
||||
| `d22_meets_requirement` | **派 agent** 比原始需求 | `meets_requirement` `gaps` |
|
||||
| `n98_collect_fixes` | 把差距整理成可执行修改方案 | `fix_plan` |
|
||||
| `n93_schematic_done` | 无 interrupt,图自己出报告 | — |
|
||||
| `n101_import_to_pcb` | `sync_schematic_to_board` 导入 PCB | `pcb_path` `imported` |
|
||||
| `n101_import_to_pcb` | `schgen.pcbimport` 导入 PCB(**不用** MCP 的 sync,G-0039) | `pcb_path` `imported` |
|
||||
| `n109_prompt_pcb_rules` | **要用户**给布局要求 md(叠层/阻抗差分等长/线宽过孔) | `pcb_rules_md` |
|
||||
| `d176_pcb_rules_complete` | 要求每项都有可落成规则的数值? | `pcb_rules_complete` `missing` |
|
||||
| `n110_place_components` | 评估难度,按最利于布线的方式布局(`schgen.pcbplace`) | `placement_report` `placed` |
|
||||
| `d140_need_sch_adjust` | 要不要为布线调整原理图连线?**只提方案,先不动图** | `need_sch_adjust` `adjust_plan` |
|
||||
| `d144_adjust_over_limit` | 无 interrupt,尝试次数计数,超 3 次放弃调整 | — |
|
||||
| `d116_need_fpga_pin_change` | 方案动了 FPGA 管脚? | `need_fpga_pin_change` |
|
||||
| `n118_rebuild_vivado_probe` | 按新管脚改验证 Vivado 工程 | `vivado_prj_dir` |
|
||||
| `d123_swap_bitstream_ok` | 新管脚出得来 bitstream?(false=方案被拒,不重试) | `bitstream_ok` |
|
||||
| `d128_need_mcu_pin_change` | 方案动了 MCU 管脚?(false=方案通过 → n183) | `need_mcu_pin_change` |
|
||||
| `d132_swap_mcu_io_ok` | 调整后 MCU IO 对吗?(false=方案被拒,**不退回 n10**) | `mcu_io_ok` |
|
||||
| `d148_have_new_adjust` | 被拒了,还有新方案吗? | `have_new_adjust` `adjust_plan` |
|
||||
| `n183_apply_sch_adjust` | 方案通过后改原理图并更新到 PCB(**别重跑 pcbimport**),逐焊盘核对 | `applied` |
|
||||
| `n159_collect_critical_nets` | 统计阻抗/差分/等长需求,建网络类/差分对规则 | `critical_nets` |
|
||||
| `n167_route_critical_nets` | 按用户原则布完全部关键线 + DRC,流程终点 | `routed` `route_report` |
|
||||
|
||||
排版判定 `d11_layout_issue` 与 `d20_layout_issue` **同极性**:
|
||||
**true = 有排版问题 → 回去重修**,false = 没问题 → 往前走。回填键都叫 `layout_issue`。
|
||||
@@ -198,11 +216,33 @@ $DR = "$env:USERPROFILE\.claude\skills\kicad-flow\scripts\drawio_read.py"
|
||||
| `Invalid \escape` | 你在用 `run.py '<JSON>'` 拼路径。改用 `kf_flow.py resume --set` |
|
||||
| 中文 hint 是乱码 | 你绕过了 kf_flow.py。它自己转 UTF-8,直接跑 run.py 才需要 `PYTHONIOENCODING=utf-8` |
|
||||
| 同一个节点反复中断 | 回填是 falsy(空 dict/`false`/`""`)。至少给一个非空键 |
|
||||
| `回填缺少本节点的主键 xxx` | interrupt 声明的第一个键(判定/主结论)没给。**缺判定键时节点会把它当 false、悄悄走另一条边**(d20 曾因此被跳到 d22),所以脚本默认拦下。确实只填部分键才加 `--allow-partial` |
|
||||
| `Read` drawio 只看到 base64 | SVG 外壳+压缩格式。用 `drawio_read.py` |
|
||||
| `拒绝启动:...会落在本工作流设计目录内` | `--prj-dir` 指到 `E:\LangGraph_Design\kicad-flow` 了。要指目标板子的工程目录 |
|
||||
| `KF_PRJ_DIR 指向的目录不存在` | 先把工程目录建出来 |
|
||||
| `[SAFETY-END]` | 某回环超上限。先解决报告里指出的卡点,再 `start --restart`,或 `--max-*-retry` 调高 |
|
||||
| 改了图工程的代码想验证 | 图工程根下 `python smoke_test.py`,6 项用例覆盖落点/闸门/回环/护栏 |
|
||||
| 改了图工程的代码想验证 | 图工程根下 `python smoke_test.py`,9 项用例覆盖落点/闸门/回环/护栏/PCB 段/drawio 逐边对账 |
|
||||
| n183 之后布局全没了 | 落地调整方案时又跑了 `schgen.pcbimport`,它会重建空板。布局后只改受影响焊盘的网络 |
|
||||
|
||||
## 画原理图 / 加封装用 schgen 脚本库(n10 / n23 / d11)
|
||||
|
||||
`scripts\schgen\` 是从两块真实板子(ipots_tunerX10 载板、DVBS2_dmod tuner 子板)通用化来的
|
||||
原理图生成库:**原理图由工程里的 python 脚本生成,不用 MCP 一个个点**。坐标由符号真实外框推出,
|
||||
自带重叠/越界检查、子电路画法检查(G-0035/37)、封装↔引脚一一对应检查、drawio→PNG 封面插图。
|
||||
用法、原语、踩过的坑见 [scripts/schgen/README.md](scripts/schgen/README.md);
|
||||
最小完整示例是 `E:\Kicad_design\DVBS2_dmod\carrier\gen\`(gen_sch.py + mkfp.py,4 页 78 器件)。
|
||||
|
||||
```powershell
|
||||
python <工程>\gen\gen_sch.py # 出图(有重叠不写文件)
|
||||
python -m schgen.check <工程>\<prj>.kicad_sch # 在 scripts 目录下跑:ERC/器件数/封装/子电路/接点
|
||||
& <KICAD_PYTHON> <工程>\gen\mkfp.py # n23:从参考 PCB 提取封装(要 pcbnew)
|
||||
& <KICAD_PYTHON> -m schgen.pcbimport <根图> <板.kicad_pcb> # n101:从网表建板 + 逐焊盘验收
|
||||
```
|
||||
|
||||
检查器不管"挤"只管"压":出完图一定 `kicad-cli sch export pdf` 渲染成 PNG 逐页看一遍。
|
||||
|
||||
> **n101 不要用 MCP 的 `sync_schematic_to_board`**(G-0039):它自报 success,DVBS2_dmod 上实际只写了
|
||||
> 26/60 个网(`Net-(...)` 无名网全丢、还冒出一个 PWR_FLAG 网)。用 `schgen.pcbimport`,以它的逐焊盘验收为准。
|
||||
|
||||
## 相关
|
||||
|
||||
|
||||
@@ -132,7 +132,9 @@ payload 的 `retry` 和 `sim_results` 告诉你上一轮发生了什么,**把
|
||||
> 典型应用电路或参考设计保持一致,便于逐项比对、便于照抄取值,也便于后面 `n101`
|
||||
> 导入 PCB 后直接沿用这层就近关系。
|
||||
|
||||
`batch_add_and_connect` / `batch_add_components` / `batch_connect` 比逐个加快得多。
|
||||
首选 `scripts\schgen` 脚本库生成(见 schgen/README.md,示例 `E:\Kicad_design\DVBS2_dmod\carrier\gen\gen_sch.py`):
|
||||
自带重叠/越界、子电路、ERC、网表器件数检查,封面框图由 `schgen.diagram` 渲染、`schgen.root` 嵌入。
|
||||
零星修改才用 MCP 的 `batch_add_and_connect` / `batch_add_components` / `batch_connect`。
|
||||
|
||||
这是三条回环的汇合点,payload 会告诉你这轮为什么回来:
|
||||
- `fix_plan` 非空 → `d22` 判了不符合需求,**逐条按方案返工**
|
||||
@@ -207,6 +209,10 @@ STM32 可用 cubemx MCP 的 `get_pin_functions` / `get_pinout` 交叉验证。
|
||||
2. 按型号或 datasheet 从网上下载并安装
|
||||
3. 按型号或 datasheet 自己画(`create_footprint`)
|
||||
|
||||
脚本化做法:`schgen.fplib.FpLib`(KiCad 自带 python 跑)按位号从参考 `.kicad_pcb` 提取封装,
|
||||
自画件直接写 `.kicad_mod`;原理图生成脚本里用 `fp_resolver` 按数值查表。
|
||||
验收:`python -m schgen.check <根图>` 的"封装"一行必须是"全部 N 个器件有封装且引脚/焊盘一一对应"。
|
||||
|
||||
`search_footprints` / `list_library_footprints` 找库内现成的,
|
||||
`search_jlcpcb_parts` 可顺带确认可采购性。
|
||||
|
||||
@@ -304,9 +310,139 @@ STM32 可用 cubemx MCP 的 `get_pin_functions` / `get_pinout` 交叉验证。
|
||||
|
||||
### `n101_import_to_pcb`(101)— 导入 PCB
|
||||
|
||||
`sync_schematic_to_board` 把原理图导入 PCB(器件带封装落板)。导入后确认器件数与
|
||||
网表数与原理图一致(`get_component_list`、`get_nets_list`)。
|
||||
**用 `schgen.pcbimport`,不要用 MCP 的 `sync_schematic_to_board`**(G-0039):后者自报 success,
|
||||
实测 ipots 载板 338/327 封装、191/426 网;DVBS2_dmod 26/60 网(`Net-(...)` 无名网全丢、
|
||||
多出一个 PWR_FLAG 网、U4 一串焊盘没网)。
|
||||
|
||||
PCB 的布局/布线/DRC **不属于本流程**,导入成功即收工。
|
||||
```powershell
|
||||
cd $env:USERPROFILE\.claude\skills\kicad-flow\scripts
|
||||
& <KICAD_PYTHON> -m schgen.pcbimport <根图.kicad_sch> <工程>\<prj>.kicad_pcb [--layers 4]
|
||||
```
|
||||
|
||||
它从 kicad-cli 网表建一块新板(封装按页排在停车场、挂网),再重新载入**逐焊盘**比对网表,
|
||||
末行"验收:… 问题 0"才算成功。每次都是重建整块板,布局开始后别再跑。
|
||||
|
||||
导入成功后直接进入阶段五(PCB)。
|
||||
|
||||
回填:`--set pcb_path="<.kicad_pcb 路径>" --set imported=true`
|
||||
|
||||
---
|
||||
|
||||
## 阶段五:PCB —— 布局要求 → 布局 → 为布线调整原理图 → 关键线布线
|
||||
|
||||
PCB 段在 PR00.drawio 右侧,cell id 形如 `T50IymjQljGLdgGnkx4X-109`,下面只写末尾数字。
|
||||
|
||||
**原理图调整是「先试后落」的**:`d140` 只提方案、不动原理图;方案依次过 FPGA
|
||||
(`d116`→`n118`→`d123`)和 MCU(`d128`→`d132`)两道校验,全过才算通过
|
||||
(`adjust_status=accepted`),**由 `n183` 真正改原理图并更新到 PCB**。任一道不过即
|
||||
「调整被拒绝」→ `d148` 想新方案;总尝试次数由 `d144` 卡在 `max_adjust_retry`(默认 3),
|
||||
超了或想不出新方案都是「放弃调整」(`adjust_status=abandoned`),**按原原理图继续布线,
|
||||
不是 safety_end**。
|
||||
|
||||
### `n109_prompt_pcb_rules`(109)— **要用户**给布局要求 md
|
||||
|
||||
**必须拿到用户本人的回答**,不能替他编要求。md 放 `<工程>\Notes\` 根目录(用户手写),
|
||||
至少覆盖:① 板层设置(层数/叠层/各层用途);② 阻抗线、差分线、等长线的要求;
|
||||
③ 线宽、过孔的要求。回到这里时 payload 的 `missing` 是上轮查出的缺项,照着问。
|
||||
|
||||
回填:`--set pcb_rules_md="<md 绝对路径>"`
|
||||
|
||||
### `d176_pcb_rules_complete`(176)— 布局要求完整吗
|
||||
|
||||
逐项核对,而且每项都要是**能直接落成设计规则的数值**(目标阻抗 + 按叠层算出的线宽/间距、
|
||||
差分对与等长组成员及容差、最小线宽/过孔孔径与焊环)。「按常规」「尽量短」不算。
|
||||
|
||||
> false 回 `n109` 再问用户,上限 `max_pcb_rules_retry`(默认 3),超限 safety_end。
|
||||
|
||||
回填:`--set pcb_rules_complete=true` 或 `--set pcb_rules_complete=false --json '{"missing":["等长容差"]}'`
|
||||
|
||||
### `n110_place_components`(110)— 评估难度并布局
|
||||
|
||||
先把板框、叠层、网络类设进 PCB,再评估布局布线难度(BGA/细间距扇出与层数、关键信号走向
|
||||
与长度、大电流路径、接口器件的板边约束),然后在满足用户要求的前提下**以最有利于布线**
|
||||
的方式摆件。用 `schgen.pcbplace`(KiCad 自带 python),`check()` 到 0 重叠/出板,
|
||||
`render()` 出正反面 PNG 目视。难度评估与布局理由写成一份 md。
|
||||
|
||||
回填:`--set placement_report="<md 路径>" --set placed=true`
|
||||
|
||||
### `d140_need_sch_adjust`(140)— 要不要为布线调整原理图连线
|
||||
|
||||
看飞线:FPGA 同 bank 内换脚、MCU 功能换到另一组可复用引脚、连接器/排阻换序、差分对 P/N 对调
|
||||
这类**只改连接、不改功能**就能明显省层/省过孔/解交叉的地方。**此时先别动原理图**。
|
||||
|
||||
回填:`--set need_sch_adjust=false`,或
|
||||
`--set need_sch_adjust=true --json '{"adjust_plan":[{"ref":"U1","change":"LED0: A5→B6","why":"省两个过孔"}]}'`
|
||||
|
||||
### `d144_adjust_over_limit`(144)— 无 interrupt
|
||||
|
||||
图自己计数:放行一次 `adjust_retry` +1;已达 `max_adjust_retry` 就判「已超过三次,放弃修改」
|
||||
直接去 `n159`。
|
||||
|
||||
### `d116_need_fpga_pin_change`(116)/ `d128_need_mcu_pin_change`(128)— 方案动没动管脚
|
||||
|
||||
只看当前 `adjust_plan`。板上没有 FPGA / MCU 直接回 false。
|
||||
**`d128` 回 false 就等于本轮方案通过**(FPGA 那道要么没涉及、要么已经过了),下一步 `n183` 落地。
|
||||
|
||||
回填:`--set need_fpga_pin_change=true` / `--set need_mcu_pin_change=false`
|
||||
|
||||
### `n118_rebuild_vivado_probe`(118)— 按新管脚重建验证工程
|
||||
|
||||
优先在 `n14` 建的工程上改 XDC。检查项与 `n14` 相同:管脚分配、供电 bank、时钟与 PLL
|
||||
(时钟仍在 CC 脚)、差分对;Zynq 另查 MIO/EMIO、QSPI/eMMC/JTAG。
|
||||
|
||||
回填:`--set vivado_prj_dir="<工程目录>"`
|
||||
|
||||
### `d123_swap_bitstream_ok`(123)— 新管脚下出得来 bitstream 吗
|
||||
|
||||
> **false 不回 `n118` 重试**,而是本轮方案被拒 → `d148`。别在这里反复改 Vivado。
|
||||
|
||||
回填:`--set bitstream_ok=true` 或 `--set bitstream_ok=false --json '{"errors":["时钟不在 CC 脚"]}'`
|
||||
|
||||
### `d132_swap_mcu_io_ok`(132)— 调整后的 MCU IO 对吗
|
||||
|
||||
判据同 `d17`(专用脚复用、上拉/开漏、电平域、启动引脚),STM32 可用 cubemx 交叉验证。
|
||||
**与 `d17` 不同:false 不退回 `n10`**,只是本轮方案被拒 → `d148`。
|
||||
|
||||
回填:`--set mcu_io_ok=true` 或 `--set mcu_io_ok=false --json '{"violations":["PB3:JTDO 被占"]}'`
|
||||
|
||||
### `d148_have_new_adjust`(148)— 被拒了,还有新方案吗
|
||||
|
||||
payload 的 `rejections` 最后一条是被拒原因。避开它能给新方案就回 true 并带新 `adjust_plan`;
|
||||
想不出就 false —— 放弃调整。
|
||||
|
||||
回填:`--set have_new_adjust=false`,或 `--set have_new_adjust=true --json '{"adjust_plan":[...]}'`
|
||||
|
||||
### `n183_apply_sch_adjust`(183)— 修改原理图并更新到 PCB 中
|
||||
|
||||
只有方案通过(`d128` 否 / `d132` 正确)才会到这里。
|
||||
|
||||
1. 按 `adjust_plan` 改原理图连线。schgen 生成的原理图就**改生成脚本重出图**,别手改
|
||||
`.kicad_sch` 之后又被脚本覆盖。
|
||||
2. 更新到 PCB:PCB 已经布局,**绝对不要重跑 `schgen.pcbimport`**(它重建空板,布局全丢),
|
||||
只改受影响焊盘的网络并保持器件位置。
|
||||
3. FPGA 的 XDC 以 `n118` 通过的那版为准。
|
||||
4. kicad-cli 导出网表与 PCB 逐焊盘核一遍,改过的网络全部一致才回 true。
|
||||
|
||||
回填:`--set applied=true --set note="<改了哪些网络>"`(路径变了再加 `schematic_path` / `pcb_path`)
|
||||
|
||||
### `n159_collect_critical_nets`(159)— 统计阻抗/差分/等长需求
|
||||
|
||||
payload 的 `adjust_status`:
|
||||
|
||||
- `accepted` → 方案已由 `n183` 落地,以更新后的网络为准。
|
||||
- `abandoned` → 原理图不动;若为验证改过 Vivado 工程的 XDC,退回 `n14` 那版。
|
||||
- `none` → 没调整,直接统计。
|
||||
|
||||
然后对照原理图、datasheet 与布局要求 md 统计**所有**阻抗线、差分线、等长线,并在 PCB
|
||||
里建好对应网络类/差分对规则。
|
||||
|
||||
回填:`--json '{"critical_nets":[{"group":"USB","kind":"diff","nets":["USB_DP","USB_DN"],"rule":"90R 5/5mil"}]}'`
|
||||
|
||||
### `n167_route_critical_nets`(167)— 关键线布线(流程终点)
|
||||
|
||||
按用户布线原则把 `critical_nets` 全部布完(kicad MCP 的 `route_differential_pair` /
|
||||
`route_trace` 等):差分对全程耦合、换层配回流过孔、等长组绕到容差内。布完跑 DRC,
|
||||
关键线相关违规清零,逐组记录实测长度/偏差写进布线报告。普通信号不在本步范围。
|
||||
图随后写最终报告并结束。
|
||||
|
||||
回填:`--set routed=true --set route_report="<md 路径>"`
|
||||
|
||||
@@ -58,6 +58,9 @@ DEFAULT_GRAPH_ROOT = _env_lookup("KICAD_FLOW_ROOT", r"E:\LangGraph_Design\kicad-
|
||||
RETRY_LIMITS = {
|
||||
"partition": 3, "material": 5, "layout": 5, "vivado": 3, "mcu": 3,
|
||||
"footprint": 5, "drawio-sync": 3, "recheck": 3, "requirement": 3,
|
||||
"pcb-rules": 3,
|
||||
# adjust 超限不是 safety_end,而是 d144「放弃调整原理图」,按原原理图继续布线
|
||||
"adjust": 3,
|
||||
}
|
||||
|
||||
|
||||
@@ -165,7 +168,7 @@ def report(graph, thread: dict, before_len: int) -> int:
|
||||
_out("先解决这个卡点,再用 `start --restart` 重跑,或用 --max-*-retry 调高上限。")
|
||||
return 0
|
||||
|
||||
_out("[DONE] 图已跑完:原理图设计完成并已导入 PCB。")
|
||||
_out("[DONE] 图已跑完:原理图完成、PCB 已布局、关键线(阻抗/差分/等长)已布完。")
|
||||
_out(f"[REPORT] {values.get('report_path', '(未生成)')}")
|
||||
_out(f"[PCB] {values.get('pcb_path', '(未回填)')}")
|
||||
return 0
|
||||
@@ -250,6 +253,17 @@ def cmd_resume(args):
|
||||
snap = graph.get_state(thread)
|
||||
if not snap.next:
|
||||
_fatal("当前没有挂起的 interrupt,无需回填。先跑 status 看状态。")
|
||||
# 判定键缺失时节点会把它当 False 处理、悄悄走另一条边(2026-09-23 DVBS2_dmod 的 d20
|
||||
# 就这样被跳到了 d22)。所以 interrupt 声明的第一个键(判定/主结论键)必须给。
|
||||
want = None
|
||||
for task in snap.tasks:
|
||||
if task.interrupts and isinstance(task.interrupts[0].value, dict):
|
||||
want = task.interrupts[0].value.get("resume")
|
||||
if isinstance(want, dict) and want and not args.allow_partial:
|
||||
key0 = next(iter(want))
|
||||
if key0 not in payload:
|
||||
_fatal(f"回填缺少本节点的主键 {key0}({want[key0]})。"
|
||||
f"本节点声明的键:{list(want)}。确实只想填部分键就加 --allow-partial")
|
||||
before = len((snap.values or {}).get("history", []))
|
||||
_out(f"[RESUME] {json.dumps(payload, ensure_ascii=False)}")
|
||||
graph.invoke(Command(resume=payload), thread)
|
||||
@@ -342,6 +356,8 @@ def main() -> int:
|
||||
p.add_argument("--set", action="append", metavar="KEY=VALUE",
|
||||
help="免转义回填;true/false→bool,纯数字→int,[ 或 { 开头→JSON")
|
||||
p.add_argument("--json", help="直接给 JSON 对象(与 --set 可混用)")
|
||||
p.add_argument("--allow-partial", action="store_true",
|
||||
help="允许不填 interrupt 声明的主键(默认缺主键直接报错)")
|
||||
p.set_defaults(func=cmd_resume)
|
||||
|
||||
p = sub.add_parser("list", help="列出该工程库里的所有线程")
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
# schgen —— kicad-flow 的原理图 / 封装生成库
|
||||
|
||||
`n10_draw_schematic`、`n23_assign_footprints`、`d11/d20` 排版检查用。由两块真实板子的生成脚本通用化而来:
|
||||
ipots_tunerX10 载板(`E:\Kicad_design\ipots_tunerX10\carrier\gen`,9 页 / 327 器件)与
|
||||
DVBS2_dmod tuner 子板(`E:\Kicad_design\DVBS2_dmod\carrier\gen`,4 页 / 78 器件,**最小完整示例,先看这个**)。
|
||||
|
||||
思路:**原理图由工程里的 python 脚本生成,不手画**。所有坐标由符号真实外框推出,
|
||||
画什么就同步登记一个占位矩形,最后逐对查重叠/越界,有问题不写文件。改电路改脚本重跑。
|
||||
|
||||
## 模块
|
||||
|
||||
| 模块 | 干什么 | 跑它用的 python |
|
||||
|---|---|---|
|
||||
| `env` | 读 `~/skills_env.md`:`KICAD_ROOT`(必需)、`CHROME_EXE`(可选) | 任意 |
|
||||
| `kicad_lib` | `.kicad_sch` 底层:符号提取(含 extends 展开)/ 外框 / 旋转 / `Sheet` 写文件;`register_lib()` 登记工程符号库 | 任意 |
|
||||
| `layout` | `Canvas`:占位登记 + 重叠/越界检查(中文按 1.3 字宽估,G-0034) | 任意 |
|
||||
| `page` | `Project` / `Page`:画图原语,见下 | 任意 |
|
||||
| `symgen` | `box_symbol()` 按引脚表生成方框符号(每边可不同间距);`grab_from_sch()` 从参考 `.kicad_sch` 抽符号 | 任意 |
|
||||
| `diagram` | drawio → PNG(SVG 外壳直接截;纯 XML 用 diagrams.net viewer 在 headless Chrome 渲染,需联网) | 任意 |
|
||||
| `root` | 根图:层次页符号 + 内嵌框图 PNG(300DPI 反算 scale,G-0022);`.kicad_pro`;`sym/fp-lib-table` | 任意 |
|
||||
| `check` | `python -m schgen.check <根图>`:ERC、网表器件数、**封装↔引脚一一对应**、子电路画法(G-0035/37) | 任意 |
|
||||
| `fplib` | `FpLib`:按位号从参考 `.kicad_pcb` 提取封装 / 从 KiCad 官方库拷贝 | **KiCad 自带 python**(要 pcbnew) |
|
||||
| `pcbimport` | n101:从网表建板(封装按页排停车场 + 挂网)并**逐焊盘验收**。代替不可信的 MCP `sync_schematic_to_board`(G-0039) | **KiCad 自带 python** |
|
||||
| `pcbplace` | 布局:圆角板框、按占地中心摆放、正反面、实物禁布区(护套等)、占地重叠/出板检查(跨面只比通孔焊盘)、**自动摆位号** `place_refs()`、JLC 4 层叠层、出正反面 PNG | **KiCad 自带 python**(出图转 PNG 会找装了 fitz 的系统 python) |
|
||||
|
||||
在工程脚本里:
|
||||
|
||||
```python
|
||||
import os, sys
|
||||
sys.path.insert(0, os.path.expandvars(r"%USERPROFILE%\.claude\skills\kicad-flow\scripts"))
|
||||
from schgen import Project, register_lib, R, C, L, GND, snap
|
||||
```
|
||||
|
||||
## 画图原语(`Page`)
|
||||
|
||||
两脚无源件**必须画进它服务的电路里**(G-0035/G-0037),别摆空地两头挂标签:
|
||||
|
||||
| 原语 | 用途 |
|
||||
|---|---|
|
||||
| `comp(lib, ref, val, x, y, rot, fields=...)` | 放件;位号撞车直接报错(G-0028)。`fields`:above/left/right/top/none 或 `("xy", rx, ry, vx, vy)` |
|
||||
| `inline(x0, y0, lib, ...)` / `vline(...)` | 在水平 / 竖直线上**串**一个件,返回线的另一端 |
|
||||
| `shunt(bx, by, lib, ..., net, down=True)` | 从母线一点挂下(上)一个件到电源/地:退耦、上下拉 |
|
||||
| `run_taps(px, py, ux, stops, net)` | 沿线打一串抽头挂 shunt,末端出标签 |
|
||||
| `fan_series(...)` | 一组引脚各串一个件、排成一列(引脚距与件距不同时用) |
|
||||
| `gnd_bus` / `gnd_row` | 多个地脚并一根母线只落一个地符号 |
|
||||
| `stub(cx, cy, pp, pin, net, stagger)` | 引脚拉线接标签/电源符号;排针上相邻电源脚用 stagger 错开 |
|
||||
| `dot` / `seg` / `path` / `nc_pin` / `frame` / `text` | 丁字接点(G-0036)/ 线段 / 折线 / 断路标记 / 虚线框(留位不贴)/ 注释 |
|
||||
| `finish()` | 有重叠或越界:打印并**不写文件** |
|
||||
|
||||
二极管等横放符号竖着挂时传 `rot=90/270`(`D_Schottky` 1 脚=K:`rot=270` 时 K 在上)。
|
||||
|
||||
## 标准流程(DVBS2_dmod 就是这么跑的)
|
||||
|
||||
1. `gen/gen_sch.py`:`make_lib()`(box_symbol / grab_from_sch → `lib/<prj>.kicad_sym`)→ 每页一个函数 →
|
||||
`render()` 框图 → `write_root()` → `write_project()` → `write_lib_tables()`
|
||||
2. `python -m schgen.check <根图>`:ERC 0、器件数对得上、子电路 A/B 级 0
|
||||
3. `kicad-cli sch export pdf` → 用 fitz 渲染 PNG **逐页看图**(检查器不管"挤",只管"压")
|
||||
4. n23:`<KICAD_PYTHON> gen/mkfp.py`(FpLib 从参考 PCB 按位号取件 + 自画件),
|
||||
`gen_sch.py` 里 `fp_resolver` 按**数值**查表、按位号覆盖;再跑一次 check 看封装一行
|
||||
5. n101:`<KICAD_PYTHON> -m schgen.pcbimport <根图> <板.kicad_pcb> [--layers 4]`(在 scripts 目录下跑),
|
||||
输出最后一行"问题 0"才算导入成功。它每次重建整块板(DNP/不进 BOM/MPN 等字段一起同步)
|
||||
6. 布局:工程里写 `gen/place.py`(示例见 DVBS2_dmod),用 `Placer` 按"板内坐标"摆件,
|
||||
`check()` 为 0 后再 `kicad-cli pcb drc --schematic-parity`。建板 → 布局两步都是脚本,
|
||||
改原理图后可以一路重跑;**一旦开始手工布线就别再跑 pcbimport**(它会重建空板)
|
||||
|
||||
布局经验(DVBS2_dmod):先按信号流分区再摆件——并行总线让源端引脚与连接器引脚同序对齐,
|
||||
串阻排一行;开关电源回路(LX→二极管→BOOST 电容→PGND)收在芯片一角;DNP 的留位件
|
||||
(如 2.x 的大电感)可以放背面;金属壳 NIM 的正下方背面可以放它自己电源脚的 0402 去耦
|
||||
|
||||
## 坑(已在代码里处理,改代码前先看)
|
||||
|
||||
- `justify center` 会让整页静默解析失败,网表空、ERC 0(G-0018)→ 判断读没读进去看**网表器件数**
|
||||
- 标签角度只有 0/90,180/270 会反向压元件;字段角度要补偿元件旋转(G-0019)
|
||||
- 取 unit 时要带 `NAME_0_x` 公共图形(G-0019)
|
||||
- KiCad 库的 `(extends ...)` 派生符号要展开,属性块有嵌套括号必须配平替换,否则 ERC 报 lib_symbol_mismatch
|
||||
- 连接器是 passive 脚,`PWR_FLAG` 放在电源进线连接器旁;稳压器 OUT 多脚时只留一个 power_out,
|
||||
其余 passive,否则 ERC 报 power_out 对 power_out
|
||||
- `run_taps` 的每个挂件元组必须带另一端网络(第 6 项);`run_taps(net=None)` 之后若又手动把线往外延,
|
||||
最后一个抽头点要自己补 `dot()`(check 的 C 级会报"丁字口缺接点")
|
||||
- 检查器的字宽按 1.2mm/字(ASCII)估:0.9、1.1 都在实板上漏报过数值文字压隔壁件;
|
||||
检查器只管"压"不管"挤",出图目视 + 独立 agent 复核仍然必要
|
||||
- 改 `sym-lib-table`/`fp-lib-table` 后要**整个退出 KiCad 再开**才生效(G-0020)
|
||||
- KiCad 通用连接器符号(`Connector_Generic:Conn_*`)自带 `ki_fp_filters`(如 `Connector*:*_1x??_*`),
|
||||
封装选择器默认勾着"Apply footprint filters",工程库里的座子(从参考设计提取的 VH3.96-2P、IDC20_2MM)
|
||||
会被藏掉,用户看到的是"这个件没有封装"。做法:`set_fp_filter(src, "库:名*")` 后
|
||||
`rename(symbol_def(src), src, 新名)` 拷进工程符号库再用(示例见 DVBS2_dmod 的 make_lib)。
|
||||
**不要**直接覆盖内嵌的通用符号——ERC 会报 lib_symbol_mismatch
|
||||
- `kicad-cli pcb drc --schematic-parity` 对**脚本生成的原理图 + pcbimport 建的板**,会在所有无名网络
|
||||
(`Net-(...)`)的焊盘上报 net_conflict "No corresponding pin found in schematic"(ipots 199 条、
|
||||
DVBS2 85 条),有名网络一条不报;pcbimport 的逐焊盘验收(按 kicad-cli 导出的网表)全部一致。
|
||||
根因未查清,按已知告警处理,在 KiCad 界面里按一次 F8 再确认
|
||||
- 圆角板框要用 `SetArcGeometry(起, 中, 止)` 三点法;只给圆心+起止点,弧可能绕远路穿过板内
|
||||
- pcbnew `SetField()` 新建的字段默认可见,会被印成丝印,必须 `GetField(k).SetVisible(False)`
|
||||
- **丝印(位号)**:`place_refs(size=0.8, thick=0.15, prefer={...})` 按"离器件近、横移小"的代价
|
||||
顺序找位置,要求不压任何器件占地(含自己)、焊盘、丝印图形、已摆位号、板边;数值统一挪到 Fab。
|
||||
用 KiCad 真实文字外框算碰撞——0.8mm 字高的位号外框实测 3.38 x 1.46mm(竖写 1.46 宽),
|
||||
所以**要竖写位号的缝至少留 1.8~1.9mm**,一排 0402 想统一竖写在正上方,间距要 ≥1.7mm。
|
||||
摆不下的会列出来:那是布局问题(器件之间没给位号留地方),去改布局,别缩字号。
|
||||
验收看 `kicad-cli pcb drc` 的 silk_over_copper / silk_overlap / silk_edge_clearance 三类都为 0
|
||||
- 判断工程封装库有没有加载:选择器标题里的总数 = KiCad 自带(10.0 为 15435)+ 全局额外库 + 工程库
|
||||
@@ -0,0 +1,22 @@
|
||||
"""kicad-flow 的原理图生成库(n10 画原理图用)。
|
||||
|
||||
由 ipots_tunerX10 载板生成器(E:\\Kicad_design\\ipots_tunerX10\\carrier\\gen)通用化而来,
|
||||
去掉了写死的工程路径;本机路径一律从 ~/skills_env.md 读(KICAD_ROOT、可选 CHROME_EXE)。
|
||||
|
||||
模块:
|
||||
env 读 skills_env
|
||||
kicad_lib .kicad_sch 底层:符号提取 / 外框 / 旋转 / Sheet 写文件
|
||||
layout Canvas:占位登记 + 重叠/越界检查
|
||||
page Project / Page:画图原语(inline / shunt / fan_series / run_taps / gnd_bus ...)
|
||||
symgen 工程符号库:从参考 .kicad_sch 抽符号 / 按引脚表生成方框符号
|
||||
diagram drawio(SVG 外壳或纯 XML)-> PNG
|
||||
root 根图(层次页 + 框图插图)、.kicad_pro、库表
|
||||
check ERC + 网表器件数 + 子电路画法检查
|
||||
|
||||
在工程脚本里:
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.expandvars(r"%USERPROFILE%\\.claude\\skills\\kicad-flow\\scripts"))
|
||||
from schgen import Project, register_lib, R, C, L, GND
|
||||
"""
|
||||
from .kicad_lib import register_lib, set_fp_filter, uid, snap, placed_pins, pin_dir, symbol_bbox, rot_bbox
|
||||
from .page import Project, Page, R, C, L, GND
|
||||
@@ -0,0 +1,331 @@
|
||||
"""原理图验收:ERC + 网表器件数 + 子电路画法检查。
|
||||
|
||||
python -m schgen.check <根图.kicad_sch> [--expect-refs 位号清单文件或逗号串]
|
||||
|
||||
四件事:
|
||||
1. kicad-cli ERC(错误/警告条数与分类);
|
||||
2. 网表里的器件数与网络数——判断一页有没有真被 KiCad 读进去**看这个**,
|
||||
不要看 ERC 的 0(G-0018:一页解析失败时 ERC 照样 0 条);位号重名会被静默合并(G-0028),
|
||||
给了 --expect-refs 就逐个比对缺了哪些;
|
||||
3. 子电路画法(G-0035/G-0037):
|
||||
A 级:两脚件两端都只挂标签、连通域里没有别的元件引脚——看不出它串在哪/退耦谁;
|
||||
B 级:连通关系(可穿过其它无源件)到不了任何芯片/连接器引脚,且离最近的芯片/连接器
|
||||
引脚也超过 NEAR mm——说明它没画在所服务的子电路里。
|
||||
连通性含丁字口(端点落在另一条线中段,G-0036)。引脚坐标取自图纸自带的 lib_symbols,
|
||||
不依赖库表。
|
||||
C 级:三条以上线端(或线端+引脚)汇于一点却没画接点——电气连通但图面看不出(G-0036 的反面)。
|
||||
4. 封装:每个器件的封装文件存在,符号引脚号与焊盘号一一对应。
|
||||
"""
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from .env import kicad_cli
|
||||
from .kicad_lib import balanced, rot_pt
|
||||
|
||||
TOL = 0.02
|
||||
NEAR = 55.0
|
||||
TWO_PIN_PREFIX = ("R", "C", "L", "FB", "JP", "D")
|
||||
|
||||
|
||||
def sexp(text):
|
||||
toks = re.findall(r'\(|\)|"(?:[^"\\]|\\.)*"|[^\s()]+', text)
|
||||
stack, cur = [], []
|
||||
for tk in toks:
|
||||
if tk == '(':
|
||||
stack.append(cur); cur = []
|
||||
elif tk == ')':
|
||||
done = cur; cur = stack.pop(); cur.append(done)
|
||||
else:
|
||||
cur.append(tk[1:-1].replace('\\"', '"') if tk.startswith('"') else tk)
|
||||
return cur
|
||||
|
||||
|
||||
def find(node, name):
|
||||
return [c for c in node if isinstance(c, list) and c and c[0] == name]
|
||||
|
||||
|
||||
def at(node):
|
||||
a = find(node, "at")
|
||||
if not a:
|
||||
return None
|
||||
v = a[0]
|
||||
return (float(v[1]), float(v[2]), float(v[3]) if len(v) > 3 else 0.0)
|
||||
|
||||
|
||||
def embedded_pins(text):
|
||||
"""{lib_id: {unit: {pin: (dx, dy)}}},取自 .kicad_sch 的 lib_symbols。"""
|
||||
i = text.find("(lib_symbols")
|
||||
if i < 0:
|
||||
return {}
|
||||
ls = balanced(text, i)
|
||||
res = {}
|
||||
for m in re.finditer(r'\n\s*\(symbol "([^"]+:[^"]+)"', ls):
|
||||
blk = balanced(ls, m.start() + ls[m.start():].find("("))
|
||||
lid = m.group(1)
|
||||
units = {}
|
||||
for sm in re.finditer(r'\(symbol "[^"]*_(\d+)_\d+"', blk):
|
||||
u = int(sm.group(1))
|
||||
sub = balanced(blk, sm.start())
|
||||
for pm in re.finditer(r'\(pin\s+\S+\s+\S+\s*\(at\s+([-\d.]+)\s+([-\d.]+)', sub):
|
||||
seg = sub[pm.end():pm.end() + 400]
|
||||
nm = re.search(r'\(number "([^"]*)"', seg)
|
||||
if nm:
|
||||
units.setdefault(u, {})[nm.group(1)] = (float(pm.group(1)), float(pm.group(2)))
|
||||
res[lid] = units
|
||||
return res
|
||||
|
||||
|
||||
class DS:
|
||||
def __init__(self):
|
||||
self.p = {}
|
||||
|
||||
def find(self, k):
|
||||
self.p.setdefault(k, k)
|
||||
while self.p[k] != k:
|
||||
self.p[k] = self.p[self.p[k]]
|
||||
k = self.p[k]
|
||||
return k
|
||||
|
||||
def union(self, a, b):
|
||||
ra, rb = self.find(a), self.find(b)
|
||||
if ra != rb:
|
||||
self.p[ra] = rb
|
||||
|
||||
|
||||
def key(x, y):
|
||||
return (round(x / TOL), round(y / TOL))
|
||||
|
||||
|
||||
def on_seg(px, py, x1, y1, x2, y2):
|
||||
if abs((x2 - x1) * (py - y1) - (y2 - y1) * (px - x1)) > 0.05:
|
||||
return False
|
||||
return (min(x1, x2) - TOL <= px <= max(x1, x2) + TOL
|
||||
and min(y1, y2) - TOL <= py <= max(y1, y2) + TOL)
|
||||
|
||||
|
||||
def subckt(path):
|
||||
text = io.open(path, encoding="utf-8").read()
|
||||
lib = embedded_pins(text)
|
||||
root = sexp(text)[0]
|
||||
wires = []
|
||||
for w in find(root, "wire"):
|
||||
xy = [(float(p[1]), float(p[2])) for p in find(find(w, "pts")[0], "xy")]
|
||||
if len(xy) == 2:
|
||||
wires.append((xy[0][0], xy[0][1], xy[1][0], xy[1][1]))
|
||||
labels = []
|
||||
for kind in ("global_label", "hierarchical_label", "label"):
|
||||
for lb in find(root, kind):
|
||||
p = at(lb)
|
||||
if p:
|
||||
labels.append((p[0], p[1], lb[1]))
|
||||
comps = []
|
||||
for s in find(root, "symbol"):
|
||||
lid = find(s, "lib_id")
|
||||
pos = at(s)
|
||||
if not lid or pos is None:
|
||||
continue
|
||||
ref = ""
|
||||
for pr in find(s, "property"):
|
||||
if len(pr) > 2 and pr[1] == "Reference":
|
||||
ref = pr[2]
|
||||
un = find(s, "unit")
|
||||
u = int(un[0][1]) if un else 1
|
||||
units = lib.get(lid[0][1], {})
|
||||
pins = dict(units.get(0, {}))
|
||||
pins.update(units.get(u, {}))
|
||||
ab = {}
|
||||
for n, (dx, dy) in pins.items():
|
||||
rx, ry = rot_pt(dx, dy, pos[2])
|
||||
ab[n] = (pos[0] + rx, pos[1] - ry)
|
||||
comps.append(dict(ref=ref, pins=ab))
|
||||
|
||||
ds = DS()
|
||||
nodes = [(px, py, "pin", "%s.%s" % (c["ref"], n)) for c in comps for n, (px, py) in c["pins"].items()]
|
||||
nodes += [(x, y, "label", nm) for x, y, nm in labels]
|
||||
for (x1, y1, x2, y2) in wires:
|
||||
ds.union(key(x1, y1), key(x2, y2))
|
||||
for w1 in wires:
|
||||
for w in wires:
|
||||
if w is w1:
|
||||
continue
|
||||
for (ex, ey) in ((w1[0], w1[1]), (w1[2], w1[3])):
|
||||
if on_seg(ex, ey, *w):
|
||||
ds.union(key(ex, ey), key(w[0], w[1]))
|
||||
for (px, py, _, _) in nodes:
|
||||
for w in wires:
|
||||
if on_seg(px, py, *w):
|
||||
ds.union(key(px, py), key(w[0], w[1]))
|
||||
groups = {}
|
||||
for (px, py, kind, nm) in nodes:
|
||||
groups.setdefault(ds.find(key(px, py)), []).append((kind, nm))
|
||||
|
||||
def is_passive(c):
|
||||
r = c["ref"]
|
||||
return r and not r.startswith("#") and r.startswith(TWO_PIN_PREFIX) and len(c["pins"]) == 2
|
||||
|
||||
bad_a = []
|
||||
for c in comps:
|
||||
if not is_passive(c):
|
||||
continue
|
||||
ends = []
|
||||
for n, (px, py) in c["pins"].items():
|
||||
g = groups.get(ds.find(key(px, py)), [])
|
||||
others = [m for m in g if m[0] == "pin" and not m[1].startswith(c["ref"] + ".")
|
||||
and not m[1].startswith("#")]
|
||||
ends.append((len(others), any(m[0] == "label" for m in g)))
|
||||
if all(n == 0 and lb for n, lb in ends):
|
||||
bad_a.append(c["ref"])
|
||||
|
||||
anchors, apts = set(), []
|
||||
for c in comps:
|
||||
if c["ref"] and not c["ref"].startswith("#") and not is_passive(c):
|
||||
for n, pt in c["pins"].items():
|
||||
anchors.add("%s.%s" % (c["ref"], n)); apts.append(pt)
|
||||
du = DS()
|
||||
passives = [c for c in comps if is_passive(c)]
|
||||
for c in passives:
|
||||
rs = [ds.find(key(px, py)) for px, py in c["pins"].values()]
|
||||
du.union(rs[0], rs[1])
|
||||
live = {du.find(r) for r, mem in groups.items() if any(k == "pin" and nm in anchors for k, nm in mem)}
|
||||
bad_b = []
|
||||
for c in passives:
|
||||
ok = any(du.find(ds.find(key(px, py))) in live for px, py in c["pins"].values())
|
||||
if not ok:
|
||||
ok = any(math.hypot(px - ax, py - ay) <= NEAR
|
||||
for px, py in c["pins"].values() for ax, ay in apts)
|
||||
if not ok:
|
||||
bad_b.append(c["ref"])
|
||||
# C 级:三条及以上线端(或线端+引脚)汇在一点却没有接点。KiCad 电气上算连通,
|
||||
# 但图面上看不出是连上的(DVBS2_dmod 第 3 轮 d20 抓到过两处)
|
||||
jset = {key(float(j[1][1]), float(j[1][2])) for j in
|
||||
[(x, find(x, "at")[0]) for x in find(root, "junction")]}
|
||||
ends = {}
|
||||
for (x1, y1, x2, y2) in wires:
|
||||
for p in ((x1, y1), (x2, y2)):
|
||||
ends.setdefault(key(*p), []).append(p)
|
||||
pinpts = {key(px, py) for c in comps for (px, py) in c["pins"].values()}
|
||||
bad_c = []
|
||||
for k, pts in ends.items():
|
||||
n = len(pts) + (1 if k in pinpts else 0)
|
||||
mid = sum(1 for w in wires if on_seg(pts[0][0], pts[0][1], *w)
|
||||
and key(w[0], w[1]) != k and key(w[2], w[3]) != k)
|
||||
if n + 2 * mid >= 3 and k not in jset:
|
||||
bad_c.append("(%.2f,%.2f)" % pts[0])
|
||||
return len(comps), bad_a, bad_b, bad_c
|
||||
|
||||
|
||||
def erc(root):
|
||||
out = os.path.join(tempfile.mkdtemp(prefix="kf_erc_"), "erc.rpt")
|
||||
subprocess.run([kicad_cli(), "sch", "erc", "--severity-error", "--severity-warning",
|
||||
"-o", out, root], capture_output=True)
|
||||
t = io.open(out, encoding="utf-8", errors="replace").read() if os.path.exists(out) else ""
|
||||
kinds = {}
|
||||
for m in re.finditer(r'\[(\w+)\]:\s*(.*)', t):
|
||||
kinds.setdefault(m.group(1), []).append(m.group(2))
|
||||
return t, kinds
|
||||
|
||||
|
||||
def netlist(root):
|
||||
out = os.path.join(tempfile.mkdtemp(prefix="kf_net_"), "n.xml")
|
||||
subprocess.run([kicad_cli(), "sch", "export", "netlist", "--format", "kicadxml", "-o", out, root],
|
||||
capture_output=True)
|
||||
r = ET.parse(out).getroot()
|
||||
comps = {c.get("ref"): (c.findtext("value"), c.findtext("footprint")) for c in r.iter("comp")}
|
||||
nets = {}
|
||||
for n in r.iter("net"):
|
||||
nets[n.get("name")] = [(x.get("ref"), x.get("pin")) for x in n.iter("node")]
|
||||
return comps, nets, out
|
||||
|
||||
|
||||
def _fp_dirs(prj_dir):
|
||||
"""库名 -> .pretty 目录:工程 fp-lib-table 优先,其余按 KiCad 自带库目录找。"""
|
||||
from .env import kicad_footprint_dir
|
||||
d = {}
|
||||
t = os.path.join(prj_dir, "fp-lib-table")
|
||||
if os.path.exists(t):
|
||||
for m in re.finditer(r'\(name "([^"]+)"\).*?\(uri "([^"]+)"\)', io.open(t, encoding="utf-8").read()):
|
||||
d[m.group(1)] = m.group(2).replace("${KIPRJMOD}", prj_dir)
|
||||
return d, kicad_footprint_dir()
|
||||
|
||||
|
||||
def fpcheck(net_xml, prj_dir):
|
||||
"""每个器件:封装文件存在;符号引脚号 ⊆ 焊盘号;有编号的焊盘都有对应引脚(否则它挂不上网)。
|
||||
返回问题清单。"""
|
||||
r = ET.parse(net_xml).getroot()
|
||||
pins = {}
|
||||
for lp in r.iter("libpart"):
|
||||
k = (lp.get("lib"), lp.get("part"))
|
||||
pins[k] = {p.get("num") for p in lp.iter("pin")}
|
||||
dirs, share = _fp_dirs(prj_dir)
|
||||
bad = []
|
||||
for c in r.iter("comp"):
|
||||
ref, fp = c.get("ref"), c.findtext("footprint") or ""
|
||||
if ref.startswith("#"):
|
||||
continue
|
||||
if not fp:
|
||||
bad.append("%s 没有封装" % ref)
|
||||
continue
|
||||
lib, name = fp.split(":", 1)
|
||||
base = dirs.get(lib) or os.path.join(share, lib + ".pretty")
|
||||
f = os.path.join(base, name + ".kicad_mod")
|
||||
if not os.path.exists(f):
|
||||
bad.append("%s 的封装 %s 找不到(%s)" % (ref, fp, f))
|
||||
continue
|
||||
pads = set(re.findall(r'\(pad\s+"([^"]+)"', io.open(f, encoding="utf-8").read()))
|
||||
ls = c.find("libsource")
|
||||
sp = pins.get((ls.get("lib"), ls.get("part")), set()) if ls is not None else set()
|
||||
miss = sorted(sp - pads)
|
||||
extra = sorted(pads - sp)
|
||||
if miss:
|
||||
bad.append("%s(%s) 符号引脚在封装里没有:%s" % (ref, name, ",".join(miss)))
|
||||
if extra and sp:
|
||||
bad.append("%s(%s) 焊盘没有对应符号引脚:%s" % (ref, name, ",".join(extra)))
|
||||
return bad
|
||||
|
||||
|
||||
def main(argv):
|
||||
root = argv[0]
|
||||
expect = None
|
||||
if "--expect-refs" in argv:
|
||||
v = argv[argv.index("--expect-refs") + 1]
|
||||
expect = set((io.open(v).read() if os.path.exists(v) else v).replace("\n", ",").split(","))
|
||||
expect.discard("")
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
rc = 0
|
||||
rpt, kinds = erc(root)
|
||||
print("ERC:", ", ".join("%s x%d" % (k, len(v)) for k, v in sorted(kinds.items())) or "0 条")
|
||||
comps, nets, npath = netlist(root)
|
||||
real = {k: v for k, v in comps.items() if not k.startswith("#")}
|
||||
print("网表: 器件 %d 个,网络 %d 条 (%s)" % (len(real), len(nets), npath))
|
||||
fb = fpcheck(npath, os.path.dirname(os.path.abspath(root)))
|
||||
print("封装: %s" % ("全部 %d 个器件有封装且引脚/焊盘一一对应" % len(real) if not fb
|
||||
else "%d 处问题" % len(fb)))
|
||||
for b in fb:
|
||||
print(" !! " + b)
|
||||
if fb:
|
||||
rc = 1
|
||||
if expect is not None:
|
||||
miss = sorted(expect - set(real))
|
||||
if miss:
|
||||
rc = 1
|
||||
print(" !! 网表里缺: %s" % " ".join(miss))
|
||||
d = os.path.dirname(os.path.abspath(root))
|
||||
files = sorted(set(re.findall(r'\(property "Sheetfile" "([^"]+)"', io.open(root, encoding="utf-8").read())))
|
||||
for f in files:
|
||||
n, a, b, cj = subckt(os.path.join(d, f))
|
||||
flag = "OK " if not a and not b and not cj else "!! "
|
||||
if a or b or cj:
|
||||
rc = 1
|
||||
print("%s%-28s 元件 %3d A:两端只挂标签 %d B:没进子电路 %d C:丁字口缺接点 %d %s"
|
||||
% (flag, f, n, len(a), len(b), len(cj), " ".join(sorted(set(a) | set(b)) + cj)))
|
||||
return rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,103 @@
|
||||
"""把 drawio 框图渲染成 PNG,供原理图第一页当插图(n10 硬要求)。
|
||||
|
||||
两种 drawio 格式都吃:
|
||||
* SVG 外壳(drawio MCP / 导出的 .drawio.svg):headless Chrome 直接截 SVG;
|
||||
* 纯 mxfile XML(drawio 桌面版存的 .drawio):套一层 HTML,用 diagrams.net 的
|
||||
viewer-static.min.js 在 headless Chrome 里渲染后截图(需要能上网取 JS)。
|
||||
|
||||
不用 draw.io.exe --export:G-0022 实测它在 Windows 上报 input file not found,还会挂起。
|
||||
|
||||
python -m schgen.diagram <in.drawio> <out.png> [--scale 2]
|
||||
"""
|
||||
import html
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from .env import chrome
|
||||
|
||||
VIEWER_JS = "https://viewer.diagrams.net/js/viewer-static.min.js"
|
||||
|
||||
|
||||
def _png_size(p):
|
||||
d = io.open(p, "rb").read(32)
|
||||
return struct.unpack(">II", d[16:24])
|
||||
|
||||
|
||||
def _page_size(xml):
|
||||
m = re.search(r'pageWidth="(\d+)"[^>]*pageHeight="(\d+)"', xml)
|
||||
if m:
|
||||
return int(m.group(1)), int(m.group(2))
|
||||
return 1600, 1200
|
||||
|
||||
|
||||
def _bounds(xml):
|
||||
"""所有顶点几何的外包框,用来裁掉页面空白。"""
|
||||
xs, ys = [], []
|
||||
for m in re.finditer(r'<mxGeometry\s+([^>]*?)/?>', xml):
|
||||
a = dict(re.findall(r'(\w+)="([^"]*)"', m.group(1)))
|
||||
if a.get("relative") == "1" or "width" not in a:
|
||||
continue
|
||||
x, y = float(a.get("x", 0)), float(a.get("y", 0))
|
||||
w, h = float(a["width"]), float(a["height"])
|
||||
xs += [x, x + w]; ys += [y, y + h]
|
||||
if not xs:
|
||||
return None
|
||||
return min(xs), min(ys), max(xs), max(ys)
|
||||
|
||||
|
||||
def render(src, png, scale=2, margin=20):
|
||||
raw = io.open(src, encoding="utf-8", errors="replace").read()
|
||||
tmpdir = tempfile.mkdtemp(prefix="kf_diag_")
|
||||
if raw.lstrip().startswith("<svg") or "<svg" in raw[:500]:
|
||||
m = re.search(r'<svg[^>]*\swidth="([\d.]+)(?:px)?"[^>]*\sheight="([\d.]+)', raw)
|
||||
w, h = (int(float(m.group(1))), int(float(m.group(2)))) if m else (1600, 1200)
|
||||
url = "file:///" + os.path.abspath(src).replace("\\", "/")
|
||||
budget = []
|
||||
else:
|
||||
xml = raw
|
||||
bb = _bounds(xml)
|
||||
if bb:
|
||||
w, h = int(bb[2] - bb[0] + 2 * margin), int(bb[3] - bb[1] + 2 * margin)
|
||||
else:
|
||||
w, h = _page_size(xml)
|
||||
cfg = '{"highlight":"#0000ff","nav":false,"resize":false,"toolbar":"","edit":null,' \
|
||||
'"lightbox":false,"border":%d,"xml":%s}' % (margin, _json_str(xml))
|
||||
page = ('<!DOCTYPE html><html><head><meta charset="utf-8">'
|
||||
'<style>html,body{margin:0;padding:0;background:#fff}</style></head><body>'
|
||||
'<div class="mxgraph" style="max-width:100%%" data-mxgraph="%s"></div>'
|
||||
'<script src="%s"></script></body></html>' % (html.escape(cfg, quote=True), VIEWER_JS))
|
||||
hp = os.path.join(tmpdir, "d.html")
|
||||
io.open(hp, "w", encoding="utf-8").write(page)
|
||||
url = "file:///" + hp.replace("\\", "/")
|
||||
budget = ["--virtual-time-budget=15000"]
|
||||
if os.path.exists(png):
|
||||
os.remove(png)
|
||||
os.makedirs(os.path.dirname(os.path.abspath(png)), exist_ok=True)
|
||||
cmd = [chrome(), "--headless", "--disable-gpu", "--hide-scrollbars",
|
||||
"--force-device-scale-factor=%d" % scale, "--default-background-color=FFFFFFFF",
|
||||
"--window-size=%d,%d" % (w, h), "--screenshot=%s" % os.path.abspath(png)] + budget + [url]
|
||||
subprocess.run(cmd, check=True, capture_output=True, timeout=120)
|
||||
if not os.path.exists(png):
|
||||
raise SystemExit("截图失败:%s" % png)
|
||||
pw, ph = _png_size(png)
|
||||
print("框图 -> %s %dx%d px(KiCad 按 300DPI 解释:scale=1 时 %.0fx%.0f mm)"
|
||||
% (png, pw, ph, pw / 300.0 * 25.4, ph / 300.0 * 25.4))
|
||||
return png
|
||||
|
||||
|
||||
def _json_str(s):
|
||||
import json
|
||||
return json.dumps(s)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
a = sys.argv[1:]
|
||||
sc = 2
|
||||
if "--scale" in a:
|
||||
i = a.index("--scale"); sc = int(a[i + 1]); del a[i:i + 2]
|
||||
render(a[0], a[1], scale=sc)
|
||||
@@ -0,0 +1,52 @@
|
||||
"""读 ~/skills_env.md 里的本机路径(KICAD_ROOT 等),不在脚本里写死。"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
_ENV = None
|
||||
|
||||
|
||||
def env(key, default=None):
|
||||
global _ENV
|
||||
if _ENV is None:
|
||||
_ENV = {}
|
||||
p = os.path.join(os.path.expanduser("~"), "skills_env.md")
|
||||
if os.path.exists(p):
|
||||
for line in io.open(p, encoding="utf-8", errors="replace"):
|
||||
m = re.match(r"^\s*([A-Z0-9_]+)\s*=\s*(.+?)\s*$", line)
|
||||
if m:
|
||||
_ENV.setdefault(m.group(1), re.split(r"\s+#", m.group(2))[0].strip())
|
||||
return os.environ.get(key) or _ENV.get(key) or default
|
||||
|
||||
|
||||
def kicad_root():
|
||||
r = env("KICAD_ROOT")
|
||||
if not r:
|
||||
raise SystemExit("~/skills_env.md 里没有 KICAD_ROOT")
|
||||
return r
|
||||
|
||||
|
||||
def kicad_cli():
|
||||
return os.path.join(kicad_root(), "bin", "kicad-cli.exe")
|
||||
|
||||
|
||||
def kicad_symbol_dir():
|
||||
return os.path.join(kicad_root(), "share", "kicad", "symbols")
|
||||
|
||||
|
||||
def kicad_footprint_dir():
|
||||
return os.path.join(kicad_root(), "share", "kicad", "footprints")
|
||||
|
||||
|
||||
def chrome():
|
||||
"""headless 渲染框图用。skills_env 里可配 CHROME_EXE;否则按常见位置找 Chrome / Edge。"""
|
||||
c = env("CHROME_EXE")
|
||||
cands = [c] if c else []
|
||||
cands += [r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
||||
r"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe",
|
||||
r"C:\Program Files\Microsoft\Edge\Application\msedge.exe"]
|
||||
for p in cands:
|
||||
if p and os.path.exists(p):
|
||||
return p
|
||||
raise SystemExit("找不到 Chrome/Edge,请在 ~/skills_env.md 里配 CHROME_EXE")
|
||||
@@ -0,0 +1,138 @@
|
||||
"""工程封装库:从参考设计的 .kicad_pcb 按位号提取封装 / 从 KiCad 官方库拷贝。
|
||||
|
||||
**必须用 KiCad 自带的 python 跑**(要 import pcbnew):~/skills_env.md 的 KICAD_PYTHON。
|
||||
|
||||
from schgen.fplib import FpLib
|
||||
lib = FpLib(r"E:\\...\\lib\\myboard.pretty")
|
||||
lib.from_pcb(r"E:\\...\\SMOD_V1.1.kicad_pcb", [("WSON-10-0R5", "U703", "TPS7A9201")])
|
||||
lib.from_kicad([("SOT-23", "Package_TO_SOT_SMD", "SOT-23", "Q")])
|
||||
lib.report()
|
||||
|
||||
取封装的顺序(G-0033):① 项目已有参考设计 ② KiCad 官方库(本地与线上)③ 第三方
|
||||
④ 按 datasheet 自己画。厂商私有 case style 往往就是某个标准封装,先看 datasheet 末页。
|
||||
|
||||
pcbnew swig 的坑(G-0038):便捷函数 FootprintLoad() 不能用,要走 PCB_IO_MGR 插件;
|
||||
对 BOARD Remove() 过 FOOTPRINT 之后 swig 类型注册会坏,所以这里只读板子、不改板子。
|
||||
"""
|
||||
import os
|
||||
|
||||
import pcbnew
|
||||
|
||||
MIN_TXT_H = pcbnew.FromMM(0.8) # 丝印下限(嘉立创):字高 0.8 / 笔宽 0.15
|
||||
MIN_TXT_W = pcbnew.FromMM(0.15)
|
||||
|
||||
|
||||
class FpLib:
|
||||
def __init__(self, pretty):
|
||||
self.out = pretty
|
||||
os.makedirs(pretty, exist_ok=True)
|
||||
self.io = pcbnew.PCB_IO_MGR.FindPlugin(pcbnew.PCB_IO_MGR.KICAD_SEXP)
|
||||
self.ok, self.miss = [], []
|
||||
self._boards = {}
|
||||
|
||||
@staticmethod
|
||||
def fixup(fp):
|
||||
# 无编号且孔环为 0 的"焊盘"其实是机械定位孔,改成 NPTH,否则 DRC 报 annular_width
|
||||
for pad in fp.Pads():
|
||||
if pad.GetNumber():
|
||||
continue
|
||||
d, s = pad.GetDrillSize(), pad.GetSize()
|
||||
if d.x and d.x >= s.x - 1 and d.y >= s.y - 1:
|
||||
pad.SetAttribute(pcbnew.PAD_ATTRIB_NPTH)
|
||||
pad.SetLayerSet(pcbnew.PAD.UnplatedHoleMask())
|
||||
texts = [fp.Reference(), fp.Value()]
|
||||
for it in fp.GraphicalItems():
|
||||
if it.GetClass() in ("PCB_TEXT", "FP_TEXT"):
|
||||
texts.append(it)
|
||||
for it in texts:
|
||||
if it.GetTextHeight() < MIN_TXT_H:
|
||||
it.SetTextSize(pcbnew.VECTOR2I(MIN_TXT_H, MIN_TXT_H))
|
||||
if it.GetTextThickness() < MIN_TXT_W:
|
||||
it.SetTextThickness(MIN_TXT_W)
|
||||
|
||||
def save(self, fp, name, fix=None):
|
||||
fp.SetOrientationDegrees(0) # 板上实例还原成库里的朝向
|
||||
if fp.IsFlipped():
|
||||
fp.Flip(fp.GetPosition(), pcbnew.FLIP_DIRECTION_TOP_BOTTOM
|
||||
if hasattr(pcbnew, "FLIP_DIRECTION_TOP_BOTTOM") else False)
|
||||
fp.SetPosition(pcbnew.VECTOR2I(0, 0))
|
||||
fp.SetFPID(pcbnew.LIB_ID("", name))
|
||||
fp.SetReference("REF**")
|
||||
fp.SetValue(name)
|
||||
fp.SetPath(pcbnew.KIID_PATH())
|
||||
self.fixup(fp)
|
||||
if fix:
|
||||
fix(fp)
|
||||
self.io.FootprintSave(self.out, fp)
|
||||
return sorted({p.GetNumber() for p in fp.Pads()}, key=lambda s: (len(s), s))
|
||||
|
||||
def from_pcb(self, pcb, items, fix=None):
|
||||
"""items: [(目标封装名, 参考板上的位号, 说明), ...]"""
|
||||
if pcb not in self._boards:
|
||||
print("载入 %s ..." % pcb)
|
||||
self._boards[pcb] = pcbnew.LoadBoard(pcb)
|
||||
brd = self._boards[pcb]
|
||||
for name, ref, note in items:
|
||||
fp = brd.FindFootprintByReference(ref)
|
||||
if fp is None:
|
||||
self.miss.append((name, "%s 里找不到位号 %s" % (os.path.basename(pcb), ref)))
|
||||
continue
|
||||
src = "%s:%s(%s)" % (os.path.basename(pcb), fp.GetFPID().GetUniStringLibItemName(), ref)
|
||||
self.ok.append((name, src, self.save(fp, name, fix), note))
|
||||
|
||||
def from_kicad(self, items, fp_dir=None, fix=None):
|
||||
"""items: [(目标封装名, 库名, 库内封装名, 说明), ...];目标名给 None 表示沿用原名。"""
|
||||
from .env import kicad_footprint_dir
|
||||
fp_dir = fp_dir or kicad_footprint_dir()
|
||||
for name, lib, src, note in items:
|
||||
try:
|
||||
fp = self.io.FootprintLoad(os.path.join(fp_dir, lib + ".pretty"), src)
|
||||
if fp is None:
|
||||
raise RuntimeError("FootprintLoad 返回 None")
|
||||
self.ok.append((name or src, "KiCad:%s/%s" % (lib, src),
|
||||
self.save(fp, name or src, fix), note))
|
||||
except Exception as e:
|
||||
self.miss.append((name or src, "%s: %s" % (type(e).__name__, e)))
|
||||
|
||||
def add_courtyard(self, name, rect=None, margin=0.25, silk_rect=False):
|
||||
"""给库里的封装补 F.CrtYd(参考设计提来的常常没有,DRC 就查不到它和别人重叠)。
|
||||
rect=(x0, y0, x1, y1) 给实物外形(封装本地坐标,mm,如简牛护套);不给就按焊盘+丝印外框。
|
||||
silk_rect=True 时同时在 F.SilkS 画出这个外形。已有 courtyard 的先清掉再画。"""
|
||||
fp = self.io.FootprintLoad(self.out, name)
|
||||
for g in list(fp.GraphicalItems()):
|
||||
if g.GetLayer() == pcbnew.F_CrtYd:
|
||||
fp.Remove(g)
|
||||
if rect is None:
|
||||
xs, ys = [], []
|
||||
for p in fp.Pads():
|
||||
bb = p.GetBoundingBox()
|
||||
xs += [bb.GetX(), bb.GetRight()]; ys += [bb.GetY(), bb.GetBottom()]
|
||||
for g in fp.GraphicalItems():
|
||||
if g.GetLayer() in (pcbnew.F_SilkS, pcbnew.F_Fab) and g.GetClass() != "PCB_TEXT":
|
||||
bb = g.GetBoundingBox()
|
||||
xs += [bb.GetX(), bb.GetRight()]; ys += [bb.GetY(), bb.GetBottom()]
|
||||
rect = (pcbnew.ToMM(min(xs)), pcbnew.ToMM(min(ys)), pcbnew.ToMM(max(xs)), pcbnew.ToMM(max(ys)))
|
||||
x0, y0, x1, y1 = rect[0] - margin, rect[1] - margin, rect[2] + margin, rect[3] + margin
|
||||
layers = [pcbnew.F_CrtYd] + ([pcbnew.F_SilkS] if silk_rect else [])
|
||||
for lay in layers:
|
||||
s = pcbnew.PCB_SHAPE(fp)
|
||||
s.SetShape(pcbnew.SHAPE_T_RECT)
|
||||
s.SetStart(pcbnew.VECTOR2I(pcbnew.FromMM(x0 if lay == pcbnew.F_CrtYd else rect[0]),
|
||||
pcbnew.FromMM(y0 if lay == pcbnew.F_CrtYd else rect[1])))
|
||||
s.SetEnd(pcbnew.VECTOR2I(pcbnew.FromMM(x1 if lay == pcbnew.F_CrtYd else rect[2]),
|
||||
pcbnew.FromMM(y1 if lay == pcbnew.F_CrtYd else rect[3])))
|
||||
s.SetLayer(lay)
|
||||
s.SetWidth(pcbnew.FromMM(0.05 if lay == pcbnew.F_CrtYd else 0.15))
|
||||
fp.Add(s)
|
||||
fp.Reference().SetTextAngleDegrees(0) # 参考设计里常是 180°,看着像镜像
|
||||
self.io.FootprintSave(self.out, fp)
|
||||
print(" courtyard %-20s (%.2f,%.2f)-(%.2f,%.2f)" % (name, x0, y0, x1, y1))
|
||||
|
||||
def report(self):
|
||||
print("\n写出 %s" % self.out)
|
||||
for name, src, pads, note in self.ok:
|
||||
ps = ",".join(pads) if len(pads) <= 12 else "%s..%s (%d)" % (pads[0], pads[-1], len(pads))
|
||||
print(" %-34s pads=%-22s <- %-40s %s" % (name, ps, src, note))
|
||||
for name, why in self.miss:
|
||||
print(" !! %-31s %s" % (name, why))
|
||||
return 0 if not self.miss else 1
|
||||
@@ -0,0 +1,335 @@
|
||||
"""写 .kicad_sch 的底层:符号提取、外框、旋转变换、页面拼装。
|
||||
|
||||
符号来源用注册制:KiCad 自带库按库名直接找;工程自己的库用 register_lib(name, path)
|
||||
登记(例如 register_lib("myboard", r"...\\lib\\myboard.kicad_sym"))。
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import math
|
||||
|
||||
from .env import kicad_symbol_dir
|
||||
|
||||
_LIBS = {} # 库名 -> .kicad_sym 路径(工程库)
|
||||
_cache = {}
|
||||
|
||||
|
||||
_FP_FILTER = {} # lib_id -> 覆盖后的 ki_fp_filters
|
||||
|
||||
|
||||
def set_fp_filter(lib_id, flt):
|
||||
"""覆盖某个符号的 ki_fp_filters(封装选择器的"Apply footprint filters"用它)。
|
||||
KiCad 通用连接器符号自带 Connector*:*_1x??_* 之类的过滤,工程自己的封装库
|
||||
(如从参考设计提取的 VH3.96-2P)会被它藏掉,看起来像"没有封装"。flt 用空格分隔多个模式。"""
|
||||
_FP_FILTER[lib_id] = flt
|
||||
|
||||
|
||||
def register_lib(name, path):
|
||||
_LIBS[name] = path
|
||||
_cache.pop(name, None)
|
||||
_bbox_cache.clear()
|
||||
|
||||
|
||||
def _read(libname):
|
||||
if libname in _cache:
|
||||
return _cache[libname]
|
||||
p = _LIBS.get(libname) or os.path.join(kicad_symbol_dir(), libname + ".kicad_sym")
|
||||
_cache[libname] = io.open(p, encoding="utf-8").read()
|
||||
return _cache[libname]
|
||||
|
||||
|
||||
def balanced(text, start):
|
||||
"""从 text[start] 的 '(' 起取配平的整段 s-expression(跳过字符串里的括号)。"""
|
||||
d, j = 0, start
|
||||
while j < len(text):
|
||||
c = text[j]
|
||||
if c == '"':
|
||||
j += 1
|
||||
while j < len(text) and not (text[j] == '"' and text[j - 1] != '\\'):
|
||||
j += 1
|
||||
elif c == '(':
|
||||
d += 1
|
||||
elif c == ')':
|
||||
d -= 1
|
||||
if d == 0:
|
||||
return text[start:j + 1]
|
||||
j += 1
|
||||
raise ValueError("unbalanced")
|
||||
|
||||
|
||||
def symbol_def(lib_id):
|
||||
"""取出 (symbol "Lib:Name" ...) 定义,名字改成 lib_id 形式供 lib_symbols 用。
|
||||
KiCad 自带库里的 (extends "X") 派生符号会被展开成完整定义。"""
|
||||
lib, name = lib_id.split(":", 1)
|
||||
t = _read(lib)
|
||||
i = t.find('(symbol "%s"' % name)
|
||||
if i < 0:
|
||||
raise KeyError(lib_id)
|
||||
blk = balanced(t, i)
|
||||
m = re.search(r'\(extends "([^"]+)"\)', blk)
|
||||
if m:
|
||||
# 派生符号:拿父符号的图形/引脚,属性用子符号的(属性块有嵌套括号,必须按配平取)
|
||||
base = balanced(t, t.find('(symbol "%s"' % m.group(1)))
|
||||
body = re.sub(r'\(symbol "%s_' % re.escape(m.group(1)), '(symbol "%s_' % name, base)
|
||||
body = body.replace('(symbol "%s"' % m.group(1), '(symbol "%s"' % name, 1)
|
||||
for pm in re.finditer(r'\(property "([^"]*)"', blk):
|
||||
pr = balanced(blk, pm.start())
|
||||
k = body.find('(property "%s"' % pm.group(1))
|
||||
if k >= 0:
|
||||
old = balanced(body, k)
|
||||
body = body[:k] + pr + body[k + len(old):]
|
||||
else: # 父符号没有的属性(如 ki_fp_filters)插在最后一个属性后
|
||||
k = body.rfind('(property "')
|
||||
old = balanced(body, k)
|
||||
body = body[:k + len(old)] + "\n\t\t" + pr + body[k + len(old):]
|
||||
blk = body
|
||||
blk = blk.replace('(symbol "%s"' % name, '(symbol "%s"' % lib_id, 1)
|
||||
if lib_id in _FP_FILTER:
|
||||
k = blk.find('(property "ki_fp_filters"')
|
||||
if k >= 0:
|
||||
old = balanced(blk, k)
|
||||
blk = blk[:k] + re.sub(r'\(property "ki_fp_filters" "[^"]*"',
|
||||
lambda _: '(property "ki_fp_filters" "%s"' % _FP_FILTER[lib_id],
|
||||
old, count=1) + blk[k + len(old):]
|
||||
return blk
|
||||
|
||||
|
||||
def uid(*parts):
|
||||
"""确定性 uuid:同样的输入每次生成同样的 uuid,重跑脚本不会让 git diff 满屏。"""
|
||||
h = hashlib.md5(("|".join(str(p) for p in parts)).encode()).hexdigest()
|
||||
return "%s-%s-%s-%s-%s" % (h[0:8], h[8:12], h[12:16], h[16:20], h[20:32])
|
||||
|
||||
|
||||
def _unit_blocks(blk, unit):
|
||||
"""取出属于某个 unit 的子符号。KiCad 把各 unit 共用图形放在 NAME_0_x,
|
||||
引脚放在 NAME_<unit>_x,两边都要,只取后者会丢掉整个本体图形(G-0019)。"""
|
||||
if unit is None:
|
||||
return blk
|
||||
subs = []
|
||||
for m in re.finditer(r'\(symbol "([^"]*_(\d+)_\d+)"', blk):
|
||||
if int(m.group(2)) in (0, unit):
|
||||
subs.append(balanced(blk, m.start()))
|
||||
return "\n".join(subs) if subs else blk
|
||||
|
||||
|
||||
def pin_positions(lib_id, unit=None):
|
||||
"""{pin_number: (dx, dy, rot)},相对符号原点;符号坐标 y 向上为正。"""
|
||||
blk = _unit_blocks(symbol_def(lib_id), unit)
|
||||
res = {}
|
||||
for m in re.finditer(r'\(pin\s+\S+\s+\S+\s*\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)', blk):
|
||||
x, y, rot = float(m.group(1)), float(m.group(2)), float(m.group(3))
|
||||
seg = blk[m.end():m.end() + 400]
|
||||
nm = re.search(r'\(number "([^"]*)"', seg)
|
||||
if nm:
|
||||
res[nm.group(1)] = (x, y, rot)
|
||||
return res
|
||||
|
||||
|
||||
GRID = 1.27
|
||||
|
||||
|
||||
def snap(v):
|
||||
"""吸附到 1.27mm 连接网格,否则 ERC 报 off-grid、线也连不上。"""
|
||||
return round(float(v) / GRID) * GRID
|
||||
|
||||
|
||||
_bbox_cache = {}
|
||||
|
||||
|
||||
def symbol_bbox(lib_id, unit=None):
|
||||
"""符号外框 (minx, miny, maxx, maxy),符号坐标系(y 向上),含引脚端点。"""
|
||||
key = (lib_id, unit)
|
||||
if key in _bbox_cache:
|
||||
return _bbox_cache[key]
|
||||
blk = _unit_blocks(symbol_def(lib_id), unit)
|
||||
xs, ys = [], []
|
||||
N = r'(-?\d+(?:\.\d+)?)'
|
||||
for m in re.finditer(r'\((rectangle|polyline|circle|arc|bezier)\b', blk):
|
||||
g = balanced(blk, m.start())
|
||||
for mm in re.finditer(r'\((?:start|end|mid|xy|center)\s+' + N + r'\s+' + N, g):
|
||||
xs.append(float(mm.group(1))); ys.append(float(mm.group(2)))
|
||||
cm = re.search(r'\(center\s+' + N + r'\s+' + N + r'\s*\)\s*\(radius\s+' + N, g)
|
||||
if cm:
|
||||
cx, cy, r = float(cm.group(1)), float(cm.group(2)), float(cm.group(3))
|
||||
xs += [cx - r, cx + r]; ys += [cy - r, cy + r]
|
||||
for m in re.finditer(r'\(pin\b', blk):
|
||||
g = balanced(blk, m.start())
|
||||
am = re.search(r'\(at\s+' + N + r'\s+' + N, g)
|
||||
if am:
|
||||
xs.append(float(am.group(1))); ys.append(float(am.group(2)))
|
||||
if not xs:
|
||||
xs, ys = [-2.54, 2.54], [-2.54, 2.54]
|
||||
bb = (min(xs), min(ys), max(xs), max(ys))
|
||||
_bbox_cache[key] = bb
|
||||
return bb
|
||||
|
||||
|
||||
def rot_pt(dx, dy, rot):
|
||||
a = math.radians(rot)
|
||||
c, s = math.cos(a), math.sin(a)
|
||||
return (dx * c - dy * s, dx * s + dy * c)
|
||||
|
||||
|
||||
def rot_bbox(bb, rot):
|
||||
xs, ys = [], []
|
||||
for x in (bb[0], bb[2]):
|
||||
for y in (bb[1], bb[3]):
|
||||
px, py = rot_pt(x, y, rot)
|
||||
xs.append(px); ys.append(py)
|
||||
return (min(xs), min(ys), max(xs), max(ys))
|
||||
|
||||
|
||||
def placed_pins(lib_id, unit=None, rot=0):
|
||||
"""{pin: (dx_sheet, dy_sheet, rot_sheet)}:已含旋转与 y 翻转,加到放置坐标上即图纸坐标。"""
|
||||
out = {}
|
||||
for k, (dx, dy, prot) in pin_positions(lib_id, unit).items():
|
||||
rx, ry = rot_pt(dx, dy, rot)
|
||||
out[k] = (rx, -ry, (prot + rot) % 360)
|
||||
return out
|
||||
|
||||
|
||||
def pin_dir(rot_sheet):
|
||||
"""引脚出线方向(图纸坐标,y 向下)。引脚 angle 指向本体内侧,出线取反向。"""
|
||||
a = math.radians(rot_sheet)
|
||||
return (-math.cos(a), math.sin(a))
|
||||
|
||||
|
||||
def esc(s):
|
||||
return s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
|
||||
|
||||
class Sheet:
|
||||
"""一页 .kicad_sch 的内容。子图不写 sheet_instances(那是根图独有的)。"""
|
||||
|
||||
def __init__(self, title, project, paper="A3", rev="A", company="",
|
||||
sheet_uuid=None, root_uuid=None):
|
||||
# sheet_uuid 必须等于根图里该页 sheet 符号的 uuid,否则 KiCad 判为批注错误
|
||||
self.sheet_uuid = sheet_uuid or uid("root", title)
|
||||
self.root_uuid = root_uuid
|
||||
self.title, self.project, self.paper = title, project, paper
|
||||
self.rev, self.company = rev, company
|
||||
self.items, self.libs, self.n = [], {}, 0
|
||||
|
||||
def _need(self, lib_id):
|
||||
if lib_id not in self.libs:
|
||||
self.libs[lib_id] = symbol_def(lib_id)
|
||||
|
||||
def _path(self):
|
||||
return "/%s/%s" % (self.root_uuid, self.sheet_uuid) if self.root_uuid \
|
||||
else "/%s" % self.sheet_uuid
|
||||
|
||||
def comp(self, lib_id, ref, value, x, y, rot=0, unit=1, footprint="",
|
||||
hide_ref=False, hide_val=False, ref_at=None, val_at=None, dnp=False,
|
||||
fields=None, in_bom=True):
|
||||
"""放置元件。ref_at/val_at = (绝对x, 绝对y, 对齐),由调用方按外框算好。
|
||||
fields: 额外属性 {名字: 值}(如 MPN),一律隐藏。"""
|
||||
x, y = snap(x), snap(y)
|
||||
self._need(lib_id)
|
||||
bb = rot_bbox(symbol_bbox(lib_id, unit), rot)
|
||||
if ref_at is None:
|
||||
ref_at = (x, y - bb[3] - 2.032, "center")
|
||||
if val_at is None:
|
||||
val_at = (x, y - bb[1] + 2.032, "center")
|
||||
rxa, rya, rj = ref_at
|
||||
vxa, vya, vj = val_at
|
||||
# justify 只认 left/right/top/bottom/mirror;写 center 会让整页静默解析失败(G-0018)
|
||||
rj = "" if rj == "center" else " (justify %s)" % rj
|
||||
vj = "" if vj == "center" else " (justify %s)" % vj
|
||||
hidr = " hide" if hide_ref else ""
|
||||
hidv = " hide" if hide_val else ""
|
||||
# 90/270 放置时 KiCad 把元件角度叠到字段上,反向补偿;180 走镜像(G-0019)
|
||||
fa = {0: 0, 90: 270, 180: 0, 270: 90}[rot % 360]
|
||||
self.n += 1
|
||||
u = uid(self.title, ref, unit, self.n)
|
||||
extra = ""
|
||||
for k, v in (fields or {}).items():
|
||||
extra += (' (property "%s" "%s" (at %.2f %.2f 0)'
|
||||
' (effects (font (size 1.27 1.27)) hide))\n' % (esc(k), esc(v), x, y))
|
||||
self.items.append(
|
||||
' (symbol (lib_id "%s") (at %.2f %.2f %d) (unit %d)\n'
|
||||
' (exclude_from_sim no) (in_bom %s) (on_board yes) (dnp %s)\n'
|
||||
' (uuid "%s")\n'
|
||||
' (property "Reference" "%s" (at %.2f %.2f %d)'
|
||||
' (effects (font (size 1.27 1.27))%s%s))\n'
|
||||
' (property "Value" "%s" (at %.2f %.2f %d)'
|
||||
' (effects (font (size 1.27 1.27))%s%s))\n'
|
||||
' (property "Footprint" "%s" (at %.2f %.2f 0) (effects (font (size 1.27 1.27)) hide))\n'
|
||||
' (property "Datasheet" "" (at %.2f %.2f 0) (effects (font (size 1.27 1.27)) hide))\n'
|
||||
'%s'
|
||||
' (instances (project "%s" (path "%s" (reference "%s") (unit %d))))\n'
|
||||
' )'
|
||||
% (lib_id, x, y, rot, unit, ("yes" if in_bom else "no"), ("yes" if dnp else "no"), u,
|
||||
ref, rxa, rya, fa, rj, hidr,
|
||||
esc(value), vxa, vya, fa, vj, hidv,
|
||||
footprint, x, y, x, y, extra, self.project, self._path(), ref, unit))
|
||||
return (x, y)
|
||||
|
||||
def wire(self, x1, y1, x2, y2):
|
||||
x1, y1, x2, y2 = snap(x1), snap(y1), snap(x2), snap(y2)
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (wire (pts (xy %.2f %.2f) (xy %.2f %.2f))\n'
|
||||
' (stroke (width 0) (type default)) (uuid "%s"))'
|
||||
% (x1, y1, x2, y2, uid(self.title, "w", x1, y1, x2, y2, self.n)))
|
||||
|
||||
# 标签文字角度只有 0/90,朝向由 justify 决定;写 180/270 会反向压在元件上(G-0019)
|
||||
GLROT = {0: (0, "left"), 180: (0, "right"), 90: (90, "left"), 270: (90, "right")}
|
||||
|
||||
def glabel(self, name, x, y, direction=0, shape="bidirectional"):
|
||||
"""direction: 标签体伸出方向,0=右 90=上 180=左 270=下。"""
|
||||
x, y = snap(x), snap(y)
|
||||
ang, just = self.GLROT[direction % 360]
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (global_label "%s" (shape %s) (at %.2f %.2f %d) (fields_autoplaced yes)\n'
|
||||
' (effects (font (size 1.27 1.27)) (justify %s)) (uuid "%s"))'
|
||||
% (name, shape, x, y, ang, just, uid(self.title, "gl", name, x, y, self.n)))
|
||||
|
||||
def text(self, s, x, y, size=1.6):
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (text "%s" (at %.2f %.2f 0)\n'
|
||||
' (effects (font (size %.2f %.2f)) (justify left bottom)) (uuid "%s"))'
|
||||
% (esc(s.replace('"', "'")), x, y, size, size, uid(self.title, "t", s, x, y, self.n)))
|
||||
|
||||
def rect(self, x0, y0, x1, y1, dash=True):
|
||||
"""虚线框:圈出一个子电路(例如"留位不贴"的一组件)。"""
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (rectangle (start %.2f %.2f) (end %.2f %.2f)\n'
|
||||
' (stroke (width 0.2) (type %s)) (fill (type none)) (uuid "%s"))'
|
||||
% (x0, y0, x1, y1, "dash" if dash else "default",
|
||||
uid(self.title, "rect", x0, y0, self.n)))
|
||||
|
||||
def junction(self, x, y):
|
||||
"""丁字接点:一条线的端点落在另一条线中段上,必须放 junction 才导通(G-0036)。"""
|
||||
x, y = snap(x), snap(y)
|
||||
self.n += 1
|
||||
self.items.append(
|
||||
' (junction (at %.2f %.2f) (diameter 0) (color 0 0 0 0)'
|
||||
' (uuid "%s"))' % (x, y, uid(self.title, "j", x, y, self.n)))
|
||||
|
||||
def nc(self, x, y):
|
||||
x, y = snap(x), snap(y)
|
||||
self.n += 1
|
||||
self.items.append(' (no_connect (at %.2f %.2f) (uuid "%s"))'
|
||||
% (x, y, uid(self.title, "nc", x, y, self.n)))
|
||||
|
||||
def raw(self, sexpr):
|
||||
self.items.append(sexpr)
|
||||
|
||||
def dump(self, path, generator="kicad_flow_schgen"):
|
||||
out = ['(kicad_sch (version 20231120) (generator "%s") (generator_version "8.0")' % generator,
|
||||
' (uuid "%s")' % uid("sheetfile", self.title),
|
||||
' (paper "%s")' % self.paper,
|
||||
' (title_block (title "%s") (rev "%s") (company "%s"))'
|
||||
% (esc(self.title), self.rev, esc(self.company)),
|
||||
' (lib_symbols']
|
||||
for k in sorted(self.libs):
|
||||
out.append(" " + self.libs[k].replace("\n", "\n "))
|
||||
out.append(' )')
|
||||
out.extend(self.items)
|
||||
out.append(')')
|
||||
io.open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
|
||||
return path
|
||||
@@ -0,0 +1,115 @@
|
||||
"""占位登记与重叠检测。
|
||||
|
||||
原理图上画的每一样东西都登记成一个矩形,最后逐对相交检查。
|
||||
要拦的是三类:文字压文字、文字压器件/走线、器件压器件。
|
||||
走线之间相交是正常的(交叉不等于连接),不算错。
|
||||
"""
|
||||
|
||||
PAPER = {"A4": (297.0, 210.0), "A3": (420.0, 297.0), "A2": (594.0, 420.0), "A1": (841.0, 594.0)}
|
||||
|
||||
CH_W = 1.20 # 1.27 号字下 ASCII 单字符宽度(mm)。原来用 0.90 会漏报:DVBS2_dmod 的
|
||||
# C405 "22u/35V" 实际压到隔壁 C406 的极板上而没报;1.10 又漏过 C412。宁可误报(G-0034)
|
||||
CJK_W = 1.30 # 中文/全角字符宽度。KiCad 的笔画字体里全角字宽约等于字号,
|
||||
# 按 ASCII 的 0.90 算会低估近 40% —— 09 RF 页就漏检过一处
|
||||
# 中文说明压住端接电阻的重叠,所以按字符类型分别估(G-0034)。
|
||||
|
||||
|
||||
def strw(s, size=1.27):
|
||||
"""按字符类型估算字符串宽度(mm)"""
|
||||
k = size / 1.27
|
||||
return sum((CJK_W if ord(c) > 0x2E80 else CH_W) for c in s) * k
|
||||
CH_H = 1.70 # 单行文字高度
|
||||
LBL_PAD = 3.0 # 全局标签外框比文字多出来的量(箭头/边框)
|
||||
|
||||
|
||||
def cat(what):
|
||||
return what.split(":", 1)[0]
|
||||
|
||||
|
||||
class Canvas:
|
||||
def __init__(self, paper="A3", name=""):
|
||||
w, h = PAPER[paper]
|
||||
# KiCad 图框内沿约在四边 10mm 处;左边还有行号栏,底部整条留给标题栏
|
||||
self.x0, self.y0 = 22.0, 14.0
|
||||
self.x1, self.y1 = w - 14.0, h - 36.0
|
||||
self.name, self.paper = name, paper
|
||||
self.boxes = []
|
||||
|
||||
# ---- 登记 ----
|
||||
def add(self, x0, y0, x1, y1, what):
|
||||
if x1 < x0: x0, x1 = x1, x0
|
||||
if y1 < y0: y0, y1 = y1, y0
|
||||
self.boxes.append((x0, y0, x1, y1, what))
|
||||
|
||||
def add_sym(self, cx, cy, bbox, what, pad=0.0):
|
||||
"""bbox 为符号坐标系(y 向上,已按放置角度旋转过)。"""
|
||||
bx0, by0, bx1, by1 = bbox
|
||||
self.add(cx + bx0 - pad, cy - by1 - pad, cx + bx1 + pad, cy - by0 + pad, what)
|
||||
|
||||
def add_text(self, x, y, s, justify="left", size=1.27, what=None):
|
||||
k = size / 1.27
|
||||
w = strw(s, size)
|
||||
h = CH_H * k
|
||||
if justify == "left":
|
||||
x0, x1 = x, x + w
|
||||
elif justify == "right":
|
||||
x0, x1 = x - w, x
|
||||
else:
|
||||
x0, x1 = x - w / 2.0, x + w / 2.0
|
||||
self.add(x0, y - h * 0.8, x1, y + h * 0.4, what or ("text:" + s[:18]))
|
||||
|
||||
def add_label(self, x, y, s, lrot, what=None):
|
||||
"""全局标签:锚点在 (x,y),标签体朝 lrot 指定的方向伸出。
|
||||
0=向右 180=向左 90=向上 270=向下(KiCad 文字角度约定)。"""
|
||||
w = strw(s) + LBL_PAD
|
||||
t = 1.15 # 标签体半高(1.27 号字 + 边框,实测约 2.1~2.4mm 高;排针 2.54 行距刚好放得下)
|
||||
d = 0.4 # 标签体把锚点包在里面,这一小段不算和自己的引线冲突
|
||||
if lrot == 0:
|
||||
self.add(x + d, y - t, x + w, y + t, what)
|
||||
elif lrot == 180:
|
||||
self.add(x - w, y - t, x - d, y + t, what)
|
||||
elif lrot == 90:
|
||||
self.add(x - t, y - w, x + t, y - d, what)
|
||||
else:
|
||||
self.add(x - t, y + d, x + t, y + w, what)
|
||||
|
||||
def add_wire(self, x1, y1, x2, y2, what="wire"):
|
||||
t = 0.25
|
||||
self.add(min(x1, x2) - t, min(y1, y2) - t, max(x1, x2) + t, max(y1, y2) + t, what)
|
||||
|
||||
# ---- 检查 ----
|
||||
def outside(self):
|
||||
bad = []
|
||||
for b in self.boxes:
|
||||
if b[0] < self.x0 - 0.01 or b[1] < self.y0 - 0.01 or \
|
||||
b[2] > self.x1 + 0.01 or b[3] > self.y1 + 0.01:
|
||||
bad.append(b)
|
||||
return bad
|
||||
|
||||
@staticmethod
|
||||
def _care(a, b):
|
||||
ca, cb = cat(a), cat(b)
|
||||
txt = ("text", "lbl", "ref")
|
||||
# 断路标记画在引脚端点上,本来就贴着符号和走线,只管它别压着文字
|
||||
if "nc" in (ca, cb) or "jct" in (ca, cb):
|
||||
return ca in txt or cb in txt
|
||||
if ca in txt or cb in txt:
|
||||
return True
|
||||
return ca == "sym" and cb == "sym"
|
||||
|
||||
def overlaps(self):
|
||||
res, bs = [], self.boxes
|
||||
for i in range(len(bs)):
|
||||
ax0, ay0, ax1, ay1, aw = bs[i]
|
||||
for j in range(i + 1, len(bs)):
|
||||
bx0, by0, bx1, by1, bw = bs[j]
|
||||
if ax1 <= bx0 + 0.01 or bx1 <= ax0 + 0.01 or \
|
||||
ay1 <= by0 + 0.01 or by1 <= ay0 + 0.01:
|
||||
continue
|
||||
if not self._care(aw, bw):
|
||||
continue
|
||||
res.append((aw, bw, round(ax0, 1), round(ay0, 1)))
|
||||
return res
|
||||
|
||||
def report(self):
|
||||
return self.outside(), self.overlaps()
|
||||
@@ -0,0 +1,384 @@
|
||||
"""画原理图页的高层原语:Sheet 负责写文件,Canvas 负责记账做重叠/越界检查,两者成对更新。
|
||||
|
||||
用法(一个工程一个 Project,多页共享位号登记表与封装解析):
|
||||
|
||||
from schgen import Project
|
||||
prj = Project("MyBoard", out_dir=r"E:\\...\\MyBoard",
|
||||
fp_resolver=lambda ref, val: FPMAP[val])
|
||||
pg = prj.page("02 Power", "02_Power.kicad_sch", paper="A3")
|
||||
x, y, pp = pg.comp("Device:R", "R1", "10K", 100, 80)
|
||||
...
|
||||
pg.finish() # 有重叠/越界 -> 打印并返回 False,不写文件
|
||||
|
||||
画法约定(G-0035 / G-0037):两脚无源件必须画进它服务的电路里——
|
||||
串联件用 inline()/fan_series(),退耦/上下拉用 shunt()/run_taps() 挂在母线上。
|
||||
不要 two_pin() 摆在空地两端各挂一个标签(只留给确实孤立的跳线位)。
|
||||
"""
|
||||
import os
|
||||
|
||||
from .kicad_lib import (Sheet, placed_pins, pin_dir, symbol_bbox, rot_bbox, uid, snap)
|
||||
from .layout import Canvas, LBL_PAD, strw
|
||||
|
||||
GND = "GND"
|
||||
DEFAULT_PWRSYM = {"GND": "power:GND", "+3V3": "power:+3V3", "+5V": "power:+5V",
|
||||
"+12V": "power:+12V", "+1V2": "power:+1V2", "+1V8": "power:+1V8",
|
||||
"+2V5": "power:+2V5", "+3V3A": "power:+3V3A"}
|
||||
R, C, L = "Device:R", "Device:C", "Device:L"
|
||||
|
||||
|
||||
class Project:
|
||||
def __init__(self, name, out_dir, fp_resolver=None, pwrsym=None, company="", rev="A",
|
||||
props_resolver=None):
|
||||
"""fp_resolver(ref, val) -> "库:封装";props_resolver(ref, val) -> {"MPN": ..., ...}
|
||||
(额外隐藏属性,BOM 用)。"""
|
||||
self.name, self.out_dir = name, out_dir
|
||||
self.fp_resolver = fp_resolver or (lambda ref, val: "")
|
||||
self.props_resolver = props_resolver or (lambda ref, val: {})
|
||||
self.pwrsym = dict(DEFAULT_PWRSYM, **(pwrsym or {}))
|
||||
self.company, self.rev = company, rev
|
||||
self.refs = {} # 位号登记表:KiCad 对重名位号静默合并(G-0028)
|
||||
self.npwr = 0
|
||||
self.pages = []
|
||||
|
||||
def page(self, title, fname, paper="A3"):
|
||||
p = Page(self, title, fname, paper)
|
||||
self.pages.append(p)
|
||||
return p
|
||||
|
||||
def footprint(self, ref, val):
|
||||
if ref.startswith("#"):
|
||||
return ""
|
||||
fp = self.fp_resolver(ref, val)
|
||||
if fp is None:
|
||||
raise KeyError("没有为 %s (%s) 指定封装" % (ref, val))
|
||||
return fp
|
||||
|
||||
|
||||
def pwr_rot(lib, ux, uy):
|
||||
"""选放置角度,让电源符号顺着出线方向伸出去,不倒压回走线。"""
|
||||
bb = symbol_bbox(lib)
|
||||
for rot in (0, 90, 180, 270):
|
||||
b = rot_bbox(bb, rot)
|
||||
sx = (b[0] + b[2]) / 2.0
|
||||
sy = -(b[1] + b[3]) / 2.0
|
||||
if ux * sx + uy * sy > 0.05:
|
||||
return rot
|
||||
return 0
|
||||
|
||||
|
||||
class Page:
|
||||
def __init__(self, prj, title, fname, paper="A3"):
|
||||
self.prj = prj
|
||||
self.sh = Sheet(title, prj.name, paper=paper, rev=prj.rev, company=prj.company,
|
||||
sheet_uuid=uid("sh", fname))
|
||||
self.cv = Canvas(paper=paper, name=fname)
|
||||
self.fname = fname
|
||||
self.frames = []
|
||||
|
||||
# ---------- 基本元素 ----------
|
||||
def text(self, s, x, y, size=1.6):
|
||||
"""多行用 \\n;每行都登记占位。"""
|
||||
self.sh.text(s, x, y, size)
|
||||
lines = s.split("\n")
|
||||
for k, ln in enumerate(lines):
|
||||
yy = y - (len(lines) - 1 - k) * size * 1.35
|
||||
self.cv.add_text(x, yy, ln, "left", size, what="text:" + ln[:20])
|
||||
|
||||
def wire(self, x1, y1, x2, y2):
|
||||
x1, y1, x2, y2 = snap(x1), snap(y1), snap(x2), snap(y2)
|
||||
self.sh.wire(x1, y1, x2, y2)
|
||||
self.cv.add_wire(x1, y1, x2, y2)
|
||||
|
||||
@staticmethod
|
||||
def _lrot(ux, uy):
|
||||
if ux > 0.5:
|
||||
return 0
|
||||
if ux < -0.5:
|
||||
return 180
|
||||
return 270 if uy > 0 else 90
|
||||
|
||||
def label(self, name, x, y, ux, uy):
|
||||
"""在 (x,y) 放全局标签,标签体朝 (ux,uy) 伸出。"""
|
||||
lrot = self._lrot(ux, uy)
|
||||
self.sh.glabel(name, x, y, lrot)
|
||||
self.cv.add_label(x, y, name, lrot, what="lbl:" + name)
|
||||
|
||||
def power(self, net, x, y, ux, uy):
|
||||
"""电源符号:藏 #PWRxxxx,但网名要显示。"""
|
||||
lib = self.prj.pwrsym[net]
|
||||
rot = pwr_rot(lib, ux, uy)
|
||||
bb = rot_bbox(symbol_bbox(lib), rot)
|
||||
self.prj.npwr += 1
|
||||
ref = "#PWR%04d" % self.prj.npwr
|
||||
lrot = self._lrot(ux, uy)
|
||||
if lrot == 0:
|
||||
vx, vy, vj = x + bb[2] + 1.0, y, "left"
|
||||
elif lrot == 180:
|
||||
vx, vy, vj = x + bb[0] - 1.0, y, "right"
|
||||
elif lrot == 90:
|
||||
vx, vy, vj = x, y - bb[3] - 1.6, "center"
|
||||
else:
|
||||
vx, vy, vj = x, y - bb[1] + 1.6, "center"
|
||||
self.sh.comp(lib, ref, net, x, y, rot=rot, hide_ref=True,
|
||||
ref_at=(x, y, "left"), val_at=(vx, vy, vj))
|
||||
self.cv.add_sym(x, y, bb, "sym:" + ref)
|
||||
self.cv.add_text(vx, vy, net, vj, what="ref:" + ref + "=")
|
||||
|
||||
def nc(self, x, y):
|
||||
self.sh.nc(x, y)
|
||||
self.cv.add(x - 1.0, y - 1.0, x + 1.0, y + 1.0, "nc:NC")
|
||||
|
||||
def frame(self, x0, y0, x1, y1, title=None, size=1.4):
|
||||
"""虚线框圈出一组件(如"留位不贴")。框线不参与重叠检查,标题文字参与。"""
|
||||
self.sh.rect(x0, y0, x1, y1)
|
||||
if title:
|
||||
self.text(title, x0 + 1.5, y0 - 1.0, size)
|
||||
|
||||
# ---------- 元件 ----------
|
||||
def comp(self, lib, ref, val, x, y, rot=0, unit=1, fields="above", show=True,
|
||||
dnp=False, props=None, in_bom=True):
|
||||
"""fields: 'above' 位号上/数值下;'left' 叠在左侧;'right' 叠在右侧;
|
||||
'top' 都叠在上方;'none' 不显示;("xy", rx, ry, vx, vy) 手动指定(大芯片常用)。
|
||||
返回 (x, y, 已旋转的引脚表)。"""
|
||||
x, y = snap(x), snap(y)
|
||||
key = (ref, unit)
|
||||
prev = self.prj.refs.get(ref)
|
||||
if key in self.prj.refs or (prev and prev[1] != val):
|
||||
o = self.prj.refs.get(key, prev)
|
||||
raise SystemExit("位号撞车 %s:%s 的 %s 与 %s 的 %s" % (ref, self.fname, val, o[0], o[1]))
|
||||
self.prj.refs[key] = self.prj.refs[ref] = (self.fname, val)
|
||||
bb = rot_bbox(symbol_bbox(lib, unit), rot)
|
||||
fp = self.prj.footprint(ref, val)
|
||||
if not ref.startswith("#"):
|
||||
props = dict(self.prj.props_resolver(ref, val) or {}, **(props or {}))
|
||||
dval = val # DNP 件 KiCad 只画灰色 + 红叉,数值字段原样,不加 "DNP" 字样
|
||||
if not show or fields == "none":
|
||||
self.sh.comp(lib, ref, val, x, y, rot=rot, unit=unit, hide_ref=True, hide_val=True,
|
||||
dnp=dnp, footprint=fp, fields=props, in_bom=in_bom)
|
||||
else:
|
||||
if isinstance(fields, tuple): # ("xy", 位号x, 位号y, 数值x, 数值y),左对齐
|
||||
ra, va = (fields[1], fields[2], "left"), (fields[3], fields[4], "left")
|
||||
elif fields == "left":
|
||||
fx = x + bb[0] - 1.6
|
||||
ra, va = (fx, y - 1.0, "right"), (fx, y + 2.0, "right")
|
||||
elif fields == "right":
|
||||
fx = x + bb[2] + 1.6
|
||||
ra, va = (fx, y - 1.0, "left"), (fx, y + 2.0, "left")
|
||||
elif fields == "top":
|
||||
ra = (x, y - bb[3] - 4.1, "center")
|
||||
va = (x, y - bb[3] - 2.032, "center")
|
||||
else:
|
||||
ra = (x, y - bb[3] - 2.032, "center")
|
||||
va = (x, y - bb[1] + 2.032, "center")
|
||||
self.cv.add_text(ra[0], ra[1], ref, ra[2], what="ref:" + ref)
|
||||
self.cv.add_text(va[0], va[1], dval, va[2], what="ref:" + ref + "=")
|
||||
self.sh.comp(lib, ref, val, x, y, rot=rot, unit=unit, ref_at=ra, val_at=va,
|
||||
dnp=dnp, footprint=fp, fields=props, in_bom=in_bom)
|
||||
self.cv.add_sym(x, y, bb, "sym:" + ref)
|
||||
pp = placed_pins(lib, unit, rot)
|
||||
return x, y, pp
|
||||
|
||||
def pin_xy(self, cx, cy, pp, pin):
|
||||
return cx + pp[pin][0], cy + pp[pin][1]
|
||||
|
||||
def stub(self, cx, cy, pp, pin, net, length=6.35, stagger=False):
|
||||
"""从引脚按其方向拉线,末端接全局标签或电源符号。
|
||||
stagger:相邻行电源引线一长一短错开,免得电源符号肩并肩顶在一起。"""
|
||||
if pin not in pp:
|
||||
raise KeyError("引脚 %s 不存在" % pin)
|
||||
dx, dy, prot = pp[pin]
|
||||
px, py = cx + dx, cy + dy
|
||||
ux, uy = pin_dir(prot)
|
||||
if stagger and net in self.prj.pwrsym:
|
||||
row = int(round((dy if abs(ux) > 0.5 else dx) / 2.54))
|
||||
if row % 2:
|
||||
length += 7.62
|
||||
ex, ey = snap(px + ux * length), snap(py + uy * length)
|
||||
self.wire(px, py, ex, ey)
|
||||
self.term(net, ex, ey, ux, uy)
|
||||
return ex, ey
|
||||
|
||||
def nc_pin(self, cx, cy, pp, pin):
|
||||
px, py = self.pin_xy(cx, cy, pp, pin)
|
||||
self.nc(px, py)
|
||||
|
||||
def two_pin(self, lib, ref, val, x, y, na, nb, rot=0, stub=6.35, dnp=False):
|
||||
"""两脚件两端各挂标签。只用于确实孤立的件;外围件请用 inline/shunt。"""
|
||||
pp = placed_pins(lib, 1, rot)
|
||||
d1, d2 = pp["1"], pp["2"]
|
||||
vertical = abs(d1[1] - d2[1]) > abs(d1[0] - d2[0])
|
||||
x, y, pp = self.comp(lib, ref, val, x, y, rot=rot, dnp=dnp,
|
||||
fields="left" if vertical else "above")
|
||||
for pin, net in (("1", na), ("2", nb)):
|
||||
self.stub(x, y, pp, pin, net, stub)
|
||||
return x, y
|
||||
|
||||
# ---------- 子电路原语 ----------
|
||||
def _ends(self, lib, rot, ux, uy):
|
||||
"""沿 (ux,uy) 方向返回 (近端脚号, 远端脚号)。"""
|
||||
p = placed_pins(lib, 1, rot)
|
||||
a, b = sorted(p.keys())[:2]
|
||||
ja = p[a][0] * ux + p[a][1] * uy
|
||||
jb = p[b][0] * ux + p[b][1] * uy
|
||||
return (a, b) if ja < jb else (b, a)
|
||||
|
||||
def dot(self, x, y):
|
||||
x, y = snap(x), snap(y)
|
||||
self.sh.junction(x, y)
|
||||
self.cv.add(x - 0.6, y - 0.6, x + 0.6, y + 0.6, "jct")
|
||||
|
||||
def term(self, net, x, y, ux, uy):
|
||||
"""线的末端:标准轨用电源符号,其余用全局标签;net=None 表示什么都不接。"""
|
||||
if net is None:
|
||||
return
|
||||
if net in self.prj.pwrsym:
|
||||
self.power(net, snap(x), snap(y), ux, uy)
|
||||
else:
|
||||
self.label(net, snap(x), snap(y), ux, uy)
|
||||
|
||||
def seg(self, x1, y1, x2, y2):
|
||||
if abs(x1 - x2) > 0.01 or abs(y1 - y2) > 0.01:
|
||||
self.wire(snap(x1), snap(y1), snap(x2), snap(y2))
|
||||
|
||||
def path(self, *pts):
|
||||
"""折线:path((x0,y0), (x1,y1), ...)"""
|
||||
for a, b in zip(pts, pts[1:]):
|
||||
self.seg(a[0], a[1], b[0], b[1])
|
||||
|
||||
def fan_series(self, cx, cy, pp, items, side=1, dist=40.0, pitch=12.7,
|
||||
tail=6.35, ch0=18.0, chstep=3.81, taps=None):
|
||||
"""一组引脚上的串联件:引脚 -L形走线-> 元件 -> 标签,元件排成一列。
|
||||
items: [(引脚号, lib, 位号, 数值, 对外网名), ...];side +1 右 / -1 左。
|
||||
taps: {引脚号: 抽头点相对 cx 的偏移},在串阻外侧留接点挂上下拉。"""
|
||||
ys = [cy + pp[p][1] for p, _, _, _, _ in items]
|
||||
y0 = sum(ys) / len(ys) - pitch * (len(items) - 1) / 2.0
|
||||
ex = snap(cx + side * dist)
|
||||
out = {}
|
||||
near, far = self._ends(items[0][1], 90, side, 0)
|
||||
for k, (pin, lib, ref, val, net) in enumerate(items):
|
||||
px, py = cx + pp[pin][0], cy + pp[pin][1]
|
||||
x, y, epp = self.comp(lib, ref, val, ex, snap(y0 + k * pitch), rot=90, fields="above")
|
||||
nx, ny = x + epp[near][0], y + epp[near][1]
|
||||
mx = snap(cx + side * (ch0 + k * chstep))
|
||||
self.seg(px, py, mx, py)
|
||||
self.seg(mx, py, mx, ny)
|
||||
self.seg(mx, ny, nx, ny)
|
||||
fx, fy = x + epp[far][0], y + epp[far][1]
|
||||
toff = (taps or {}).get(pin)
|
||||
if toff is not None:
|
||||
hx = snap(cx + side * toff)
|
||||
self.seg(fx, fy, hx, fy)
|
||||
self.dot(hx, fy)
|
||||
fx, fy = hx, fy
|
||||
out[pin] = (hx, fy)
|
||||
tx = snap(fx + side * tail)
|
||||
self.seg(fx, fy, tx, fy)
|
||||
self.term(net, tx, fy, side, 0)
|
||||
return out
|
||||
|
||||
def shunt(self, bx, by, lib, ref, val, net, down=True, tail=6.35, dnp=False,
|
||||
drop=9.0, fields="left", rot=0):
|
||||
"""从母线上一点往下(或往上)挂一个件,末端接电源符号或标签。
|
||||
退耦电容、上下拉电阻都用它。返回 (件中心x, y, 远端x, 远端y)。"""
|
||||
sgn = 1 if down else -1
|
||||
near, far = self._ends(lib, rot, 0, sgn)
|
||||
x, y, epp = self.comp(lib, ref, val, snap(bx), snap(by + sgn * drop), rot=rot,
|
||||
fields=fields, dnp=dnp)
|
||||
nx, ny = x + epp[near][0], y + epp[near][1]
|
||||
fx, fy = x + epp[far][0], y + epp[far][1]
|
||||
self.seg(bx, by, nx, ny)
|
||||
ty = snap(fy + sgn * tail)
|
||||
if tail > 0:
|
||||
self.seg(fx, fy, fx, ty)
|
||||
self.term(net, fx, ty, 0, sgn)
|
||||
return x, y, fx, fy
|
||||
|
||||
def gnd_bus(self, cx, cy, pp, pins, side=1, dist=13.0, drop=9.0, net=GND):
|
||||
"""一侧若干地脚并到一条竖直母线上,底端只放一个地符号。"""
|
||||
ys = []
|
||||
bx = snap(cx + side * dist)
|
||||
for p in pins:
|
||||
px, py = cx + pp[p][0], cy + pp[p][1]
|
||||
self.seg(px, py, bx, py)
|
||||
ys.append(snap(py))
|
||||
y0, y1 = min(ys), max(ys)
|
||||
self.seg(bx, y0, bx, snap(y1 + drop))
|
||||
for yy in ys:
|
||||
if abs(yy - y0) > 0.01:
|
||||
self.dot(bx, yy)
|
||||
self.power(net, bx, snap(y1 + drop), 0, 1)
|
||||
|
||||
def run_taps(self, px, py, ux, stops, net=None, end=8.0):
|
||||
"""从 (px,py) 沿 ±x 拉线,沿途在指定 x 上打抽头挂件,末端出标签。
|
||||
stops: [(抽头 x, 'up'|'dn', lib, 位号, 数值, 另一端的网[, dnp]), ...]"""
|
||||
cur = px
|
||||
for k, st in enumerate(stops):
|
||||
hx = snap(st[0])
|
||||
self.seg(cur, py, hx, py)
|
||||
self.shunt(hx, py, st[2], st[3], st[4], st[5], down=(st[1] == "dn"),
|
||||
dnp=(st[6] if len(st) > 6 else False))
|
||||
if k < len(stops) - 1 or net is not None:
|
||||
self.dot(hx, py)
|
||||
cur = hx
|
||||
if net is not None:
|
||||
tx = snap(cur + ux * end)
|
||||
self.seg(cur, py, tx, py)
|
||||
self.term(net, tx, py, ux, 0)
|
||||
return cur
|
||||
|
||||
def gnd_row(self, cx, cy, pp, pins, drop=6.0, tail=5.0, at=0, net=GND):
|
||||
"""一排朝下的地脚各自下拉一小段并到横母线,母线上只落一个地符号。"""
|
||||
xs = []
|
||||
for p in pins:
|
||||
px, py = cx + pp[p][0], cy + pp[p][1]
|
||||
self.seg(px, py, px, snap(py + drop))
|
||||
xs.append(snap(px))
|
||||
yb = snap(cy + pp[pins[0]][1] + drop)
|
||||
self.seg(min(xs), yb, max(xs), yb)
|
||||
gx = xs[at]
|
||||
self.seg(gx, yb, gx, snap(yb + tail))
|
||||
self.power(net, gx, snap(yb + tail), 0, 1)
|
||||
for k, xx in enumerate(xs):
|
||||
interior = 0.01 < abs(xx - min(xs)) and abs(xx - max(xs)) > 0.01
|
||||
if interior or k == at:
|
||||
self.dot(xx, yb)
|
||||
|
||||
def inline(self, x0, y0, lib, ref, val, side=1, length=12.7, dnp=False, fields="above",
|
||||
rot=90):
|
||||
"""在水平线上串一个件:从 (x0,y0) 出发,返回线另一端坐标。
|
||||
二极管这类本身横放的符号传 rot=0/180 来定正反向。"""
|
||||
near, far = self._ends(lib, rot, side, 0)
|
||||
cx = snap(x0 + side * length / 2.0)
|
||||
x, y, epp = self.comp(lib, ref, val, cx, snap(y0), rot=rot, fields=fields, dnp=dnp)
|
||||
nx, ny = x + epp[near][0], y + epp[near][1]
|
||||
fx, fy = x + epp[far][0], y + epp[far][1]
|
||||
self.seg(x0, y0, nx, ny)
|
||||
return fx, fy
|
||||
|
||||
def vline(self, x0, y0, lib, ref, val, down=True, length=12.7, dnp=False, fields="left",
|
||||
rot=0):
|
||||
"""在竖直线上串一个件:从 (x0,y0) 出发,返回线另一端坐标。"""
|
||||
sgn = 1 if down else -1
|
||||
near, far = self._ends(lib, rot, 0, sgn)
|
||||
cy = snap(y0 + sgn * length / 2.0)
|
||||
x, y, epp = self.comp(lib, ref, val, snap(x0), cy, rot=rot, fields=fields, dnp=dnp)
|
||||
nx, ny = x + epp[near][0], y + epp[near][1]
|
||||
fx, fy = x + epp[far][0], y + epp[far][1]
|
||||
self.seg(x0, y0, nx, ny)
|
||||
return fx, fy
|
||||
|
||||
def finish(self, force=False):
|
||||
out, ov = self.cv.report()
|
||||
if out or ov:
|
||||
print(" !! %s 越界 %d 处, 重叠 %d 处" % (self.fname, len(out), len(ov)))
|
||||
for b in out[:15]:
|
||||
print(" 越界 %-28s (%.1f,%.1f)-(%.1f,%.1f) 图框 (%.0f,%.0f)-(%.0f,%.0f)"
|
||||
% (b[4], b[0], b[1], b[2], b[3],
|
||||
self.cv.x0, self.cv.y0, self.cv.x1, self.cv.y1))
|
||||
for o in ov[:25]:
|
||||
print(" 重叠 %-28s x %-28s @(%.1f,%.1f)" % o)
|
||||
if not force:
|
||||
return False
|
||||
self.sh.dump(os.path.join(self.prj.out_dir, self.fname))
|
||||
print(" OK %-26s 元素 %4d 个" % (self.fname, len(self.cv.boxes)))
|
||||
return True
|
||||
@@ -0,0 +1,210 @@
|
||||
"""n101:从原理图网表确定性地建 PCB(封装 + 网络,封装按页排在"停车场"里),再逐焊盘验收。
|
||||
|
||||
**必须用 KiCad 自带的 python 跑**(要 pcbnew):
|
||||
|
||||
<KICAD_PYTHON> -m schgen.pcbimport <根图.kicad_sch> <输出.kicad_pcb> [--layers 2|4]
|
||||
|
||||
为什么不用 kicad MCP 的 sync_schematic_to_board(G-0039):它自报 success,实际
|
||||
ipots 载板写出 338 个封装(应 327)、191 个网(应 426);DVBS2_dmod 上 26 个网(应 60),
|
||||
还冒出一个叫 PWR_FLAG 的网、U4 一串焊盘没网——所有 "Net-(...)" 无名网全丢。
|
||||
kicad-cli 没有"从原理图更新 PCB"子命令,GUI 的 F8 要人点,所以这里自己建。
|
||||
|
||||
pcbnew swig 的坑(G-0038):FootprintLoad() 便捷函数坏的,走 PCB_IO_KICAD_SEXPR;
|
||||
对 BOARD Remove() 过 FOOTPRINT 后 swig 类型注册会坏,所以**每次都建一块新板**,
|
||||
不做"载入旧板再删"。已有板子会被覆盖——布局开始之后就别再跑它了。
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pcbnew
|
||||
|
||||
from .env import kicad_cli, kicad_footprint_dir
|
||||
|
||||
GAP, SHEET_GAP, FIELD_W = 2.0, 12.0, 250.0
|
||||
|
||||
|
||||
def mm(v):
|
||||
return pcbnew.FromMM(float(v))
|
||||
|
||||
|
||||
def netlist(sch):
|
||||
out = os.path.join(tempfile.mkdtemp(prefix="kf_pcb_"), "n.xml")
|
||||
subprocess.check_call([kicad_cli(), "sch", "export", "netlist", "--format", "kicadxml",
|
||||
"-o", out, sch], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
r = ET.parse(out).getroot()
|
||||
comps = []
|
||||
for c in r.iter("comp"):
|
||||
sh = c.find("sheetpath")
|
||||
flds = {f.get("name"): (f.text or "") for f in c.iter("field")}
|
||||
props = {pr.get("name") for pr in c.iter("property")}
|
||||
comps.append(dict(ref=c.get("ref"), val=c.findtext("value") or "", fields=flds,
|
||||
dnp="dnp" in props, no_bom="exclude_from_bom" in props,
|
||||
fp=c.findtext("footprint") or "",
|
||||
sheet=sh.get("names") if sh is not None else "",
|
||||
path=(sh.get("tstamps") if sh is not None else "") + (c.findtext("tstamps") or "")))
|
||||
nets = [(n.get("name"), [(x.get("ref"), x.get("pin")) for x in n.iter("node")])
|
||||
for n in r.iter("net")]
|
||||
return comps, nets
|
||||
|
||||
|
||||
def fp_dirs(prj_dir):
|
||||
d = {}
|
||||
t = os.path.join(prj_dir, "fp-lib-table")
|
||||
if os.path.exists(t):
|
||||
for m in re.finditer(r'\(name "([^"]+)"\).*?\(uri "([^"]+)"\)', io.open(t, encoding="utf-8").read()):
|
||||
d[m.group(1)] = m.group(2).replace("${KIPRJMOD}", prj_dir)
|
||||
return d
|
||||
|
||||
|
||||
_IO = pcbnew.PCB_IO_KICAD_SEXPR()
|
||||
|
||||
|
||||
def load_fp(libdir, name):
|
||||
fp = _IO.FootprintLoad(libdir, name)
|
||||
if fp is not None and not hasattr(fp, "SetReference"):
|
||||
fp = pcbnew.Cast_to_FOOTPRINT(fp)
|
||||
return fp
|
||||
|
||||
|
||||
def fp_box(fp):
|
||||
"""占地与包围盒相对原点的偏移——原点不一定在中心(连接器常在 1 脚)。"""
|
||||
fp.SetPosition(pcbnew.VECTOR2I(0, 0))
|
||||
try:
|
||||
bb = fp.GetBoundingBox(False, False)
|
||||
except TypeError:
|
||||
bb = fp.GetBoundingBox()
|
||||
return (pcbnew.ToMM(bb.GetWidth()), pcbnew.ToMM(bb.GetHeight()),
|
||||
pcbnew.ToMM(bb.GetX()), pcbnew.ToMM(bb.GetY()))
|
||||
|
||||
|
||||
def note(board, s, x, y, size=2.0):
|
||||
t = pcbnew.PCB_TEXT(board)
|
||||
t.SetText(s)
|
||||
t.SetPosition(pcbnew.VECTOR2I(mm(x), mm(y)))
|
||||
t.SetLayer(pcbnew.Cmts_User)
|
||||
t.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size)))
|
||||
t.SetTextThickness(mm(size / 6.0))
|
||||
t.SetHorizJustify(pcbnew.GR_TEXT_H_ALIGN_LEFT)
|
||||
board.Add(t)
|
||||
|
||||
|
||||
def build(sch, pcb, layers=2, x0=20.0, y0=20.0):
|
||||
prj_dir = os.path.dirname(os.path.abspath(sch))
|
||||
dirs = fp_dirs(prj_dir)
|
||||
share = kicad_footprint_dir()
|
||||
comps, nets = netlist(sch)
|
||||
board = pcbnew.CreateEmptyBoard()
|
||||
board.SetCopperLayerCount(layers)
|
||||
placed, y = {}, y0
|
||||
for sheet in sorted(set(c["sheet"] for c in comps)):
|
||||
grp = sorted((c for c in comps if c["sheet"] == sheet), key=lambda c: (c["fp"], c["ref"]))
|
||||
note(board, "%s (%d)" % (sheet, len(grp)), x0, y - 4)
|
||||
x, rowh = x0, 0.0
|
||||
for c in grp:
|
||||
if not c["fp"]:
|
||||
raise SystemExit("%s 没有封装" % c["ref"])
|
||||
lib, name = c["fp"].split(":", 1)
|
||||
fp = load_fp(dirs.get(lib) or os.path.join(share, lib + ".pretty"), name)
|
||||
if fp is None:
|
||||
raise SystemExit("载入不了封装 %s(%s)" % (c["fp"], c["ref"]))
|
||||
fp.SetFPID(pcbnew.LIB_ID(lib, name)) # 板上必须是 "库:名",否则以后无法从库更新
|
||||
fp.SetReference(c["ref"])
|
||||
fp.SetValue(c["val"])
|
||||
# 原理图上的附加字段(MPN 等)也写进封装,否则 DRC 原理图对照报 Missing symbol field
|
||||
for k, v in c.get("fields", {}).items():
|
||||
if k not in ("Reference", "Value", "Footprint", "Datasheet") and v:
|
||||
fp.SetField(k, v)
|
||||
fd = fp.GetField(k)
|
||||
if fd is not None:
|
||||
fd.SetVisible(False) # 新建字段默认可见,会被印成丝印
|
||||
# 不贴(DNP)/不进 BOM 跟着原理图走,否则 DRC 报 footprint_symbol_mismatch
|
||||
fp.SetDNP(c.get("dnp", False))
|
||||
fp.SetExcludedFromBOM(c.get("no_bom", False))
|
||||
if c["path"]:
|
||||
try:
|
||||
fp.SetPath(pcbnew.KIID_PATH(c["path"])) # F8 靠它认同一个器件
|
||||
except Exception:
|
||||
pass
|
||||
board.Add(fp)
|
||||
w, h, ox, oy = fp_box(fp)
|
||||
if x > x0 and x + w > x0 + FIELD_W:
|
||||
x, y, rowh = x0, y + rowh + GAP, 0.0
|
||||
fp.SetPosition(pcbnew.VECTOR2I(mm(x - ox), mm(y - oy)))
|
||||
placed[c["ref"]] = fp
|
||||
x += w + GAP
|
||||
rowh = max(rowh, h)
|
||||
y += rowh + SHEET_GAP
|
||||
miss = []
|
||||
for name, nodes in nets:
|
||||
ni = pcbnew.NETINFO_ITEM(board, name)
|
||||
board.Add(ni)
|
||||
for ref, pin in nodes:
|
||||
fp = placed.get(ref)
|
||||
pads = [p for p in fp.Pads() if p.GetNumber() == pin] if fp else []
|
||||
if not pads: # 同号多焊盘(散热盘/安装脚)要全挂上
|
||||
miss.append("%s.%s" % (ref, pin))
|
||||
for pad in pads:
|
||||
pad.SetNet(ni)
|
||||
board.BuildListOfNets()
|
||||
pcbnew.SaveBoard(pcb, board)
|
||||
print("写出 %s:封装 %d,网 %d" % (pcb, len(placed), len(nets)))
|
||||
if miss:
|
||||
print("!! 连不上的节点 %d 个:%s" % (len(miss), " ".join(miss[:20])))
|
||||
return comps, nets, miss
|
||||
|
||||
|
||||
def verify(sch, pcb):
|
||||
"""独立验收:重新载入板子,按网表逐焊盘比对(G-0039:别信导入工具的自报)。"""
|
||||
comps, nets = netlist(sch)
|
||||
b = pcbnew.LoadBoard(pcb)
|
||||
fps = {f.GetReference(): f for f in b.GetFootprints()}
|
||||
bad = []
|
||||
want = {c["ref"] for c in comps}
|
||||
if set(fps) != want:
|
||||
bad.append("器件不一致:多 %s 少 %s" % (sorted(set(fps) - want), sorted(want - set(fps))))
|
||||
for c in comps:
|
||||
f = fps.get(c["ref"])
|
||||
if f and f.GetFPIDAsString() != c["fp"]:
|
||||
bad.append("%s 封装 %s != %s" % (c["ref"], f.GetFPIDAsString(), c["fp"]))
|
||||
pinnet = {(r, p): n for n, nodes in nets for r, p in nodes}
|
||||
npads = 0
|
||||
for ref, f in fps.items():
|
||||
for pad in f.Pads():
|
||||
num = pad.GetNumber()
|
||||
if not num:
|
||||
continue # 无编号的机械孔
|
||||
npads += 1
|
||||
exp = pinnet.get((ref, num))
|
||||
got = str(pad.GetNetname())
|
||||
if exp is None and got:
|
||||
bad.append("%s.%s 板上挂了 %s,网表里没有" % (ref, num, got))
|
||||
elif exp is not None and got != exp:
|
||||
bad.append("%s.%s 应为 %s,板上是 %r" % (ref, num, exp, got))
|
||||
boardnets = {str(n) for n in b.GetNetsByName().keys() if str(n)} # swig 给的是 wxString
|
||||
wantnets = {n for n, _ in nets}
|
||||
if boardnets != wantnets:
|
||||
bad.append("网名不一致:多 %s 少 %s" % (sorted(boardnets - wantnets)[:10],
|
||||
sorted(wantnets - boardnets)[:10]))
|
||||
print("验收:器件 %d/%d,网 %d/%d,有编号焊盘 %d,问题 %d"
|
||||
% (len(fps), len(want), len(boardnets), len(wantnets), npads, len(bad)))
|
||||
for x in bad[:30]:
|
||||
print(" !! " + x)
|
||||
return bad
|
||||
|
||||
|
||||
def main(argv):
|
||||
sch, pcb = argv[0], argv[1]
|
||||
layers = int(argv[argv.index("--layers") + 1]) if "--layers" in argv else 2
|
||||
_, _, miss = build(sch, pcb, layers)
|
||||
bad = verify(sch, pcb)
|
||||
return 1 if (miss or bad) else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,375 @@
|
||||
"""PCB 布局辅助:板框 / 按占地中心摆放 / 正反面 / 叠层 / 占地重叠检查 / 出图。
|
||||
|
||||
**必须用 KiCad 自带的 python 跑**(要 pcbnew)。典型用法(见 DVBS2_dmod 的 carrier/gen/place.py):
|
||||
|
||||
from schgen.pcbplace import Placer
|
||||
pl = Placer(r"...\\board.kicad_pcb", origin=(100, 100)) # 以下坐标都是板内坐标(mm,左上为 0,0,y 向下)
|
||||
pl.outline(74, 40, r=1.0)
|
||||
pl.at("U1", 4.95, 7.0, rot=0) # 封装原点落在 (x, y)
|
||||
pl.center("C304", 61.5, 12.0, rot=90) # 封装占地(courtyard)中心落在 (x, y)
|
||||
pl.center("C303", 8.95, 11.6, rot=90, bottom=True)
|
||||
pl.keepout("J1-shroud", 33.5, 29.5, 58.5, 38.5) # 封装里没画、但实物要占的地方
|
||||
pl.allow_overhang("U1") # F 头之类本来就伸出板边的件
|
||||
pl.save(); bad = pl.check() # 重叠/出板清单,空 = 干净
|
||||
pl.render(png) # 出图目视
|
||||
|
||||
pcbnew 坑(G-0038):只改位置/翻面不删件,所以直接在现有板上操作是安全的;
|
||||
SetLayerType / 叠层写不进,叠层在 save() 之后按文本补(set_stackup)。
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import pcbnew
|
||||
|
||||
from .env import kicad_cli
|
||||
|
||||
|
||||
def sys_python():
|
||||
"""找一个 import 得了 fitz(pymupdf) 的 python:skills_env 的 SYS_PYTHON > 常见安装位置 > PATH。"""
|
||||
import shutil
|
||||
from .env import env
|
||||
cands = [env("SYS_PYTHON"), r"C:\Python314\python.exe", r"C:\Python313\python.exe",
|
||||
r"C:\Python312\python.exe", env("CONDA_LANGGRAPH_PYTHON"), shutil.which("python")]
|
||||
for c in cands:
|
||||
if c and os.path.exists(c) and subprocess.run([c, "-c", "import fitz"],
|
||||
capture_output=True).returncode == 0:
|
||||
return c
|
||||
raise SystemExit("找不到装了 pymupdf 的 python,请在 ~/skills_env.md 配 SYS_PYTHON")
|
||||
|
||||
|
||||
def mm(v):
|
||||
return pcbnew.FromMM(float(v))
|
||||
|
||||
|
||||
# 嘉立创 4 层 1.6mm(JLC7628)——ipots 载板同款
|
||||
JLC4 = """ (setup
|
||||
(stackup
|
||||
(layer "F.SilkS" (type "Top Silk Screen"))
|
||||
(layer "F.Paste" (type "Top Solder Paste"))
|
||||
(layer "F.Mask" (type "Top Solder Mask") (thickness 0.01))
|
||||
(layer "F.Cu" (type "copper") (thickness 0.035))
|
||||
(layer "dielectric 1" (type "prepreg") (thickness 0.2104) (material "FR4") (epsilon_r 4.4) (loss_tangent 0.02))
|
||||
(layer "In1.Cu" (type "copper") (thickness 0.0152))
|
||||
(layer "dielectric 2" (type "core") (thickness 1.065) (material "FR4") (epsilon_r 4.6) (loss_tangent 0.02))
|
||||
(layer "In2.Cu" (type "copper") (thickness 0.0152))
|
||||
(layer "dielectric 3" (type "prepreg") (thickness 0.2104) (material "FR4") (epsilon_r 4.4) (loss_tangent 0.02))
|
||||
(layer "B.Cu" (type "copper") (thickness 0.035))
|
||||
(layer "B.Mask" (type "Bottom Solder Mask") (thickness 0.01))
|
||||
(layer "B.Paste" (type "Bottom Solder Paste"))
|
||||
(layer "B.SilkS" (type "Bottom Silk Screen"))
|
||||
(copper_finish "ENIG")
|
||||
(dielectric_constraints no)
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
class Placer:
|
||||
def __init__(self, pcb, origin=(100.0, 100.0), strip_outline=False):
|
||||
"""strip_outline=True:载入前按文本删掉旧板框(Edge.Cuts 上的顶层图元),之后重画。
|
||||
不能载入后用 board.Remove() 删——删任何对象之后本版 swig 类型注册就坏了(G-0038,
|
||||
实测删板框线也一样:之后 GetCourtyard() 返回裸 SwigPyObject)。"""
|
||||
self.pcb = pcb
|
||||
self.ox, self.oy = origin
|
||||
if strip_outline:
|
||||
self._strip_edge_cuts()
|
||||
self.b = pcbnew.LoadBoard(pcb)
|
||||
self.fp = {f.GetReference(): f for f in self.b.GetFootprints()}
|
||||
self.w = self.h = None
|
||||
self.keepouts = [] # (名字, x0, y0, x1, y1, 面) 板内坐标
|
||||
self.overhang = set()
|
||||
self.placed = set()
|
||||
|
||||
# ---------- 坐标 ----------
|
||||
def P(self, x, y):
|
||||
return pcbnew.VECTOR2I(mm(self.ox + x), mm(self.oy + y))
|
||||
|
||||
def outline(self, w, h, r=1.0, width=0.1):
|
||||
"""圆角矩形板框(Edge.Cuts)。重复调用会叠加,只在新板上调一次。"""
|
||||
self.w, self.h = w, h
|
||||
L = pcbnew.Edge_Cuts
|
||||
segs = [((r, 0), (w - r, 0)), ((w, r), (w, h - r)), ((w - r, h), (r, h)), ((0, h - r), (0, r))]
|
||||
for a, c in segs:
|
||||
s = pcbnew.PCB_SHAPE(self.b)
|
||||
s.SetShape(pcbnew.SHAPE_T_SEGMENT)
|
||||
s.SetStart(self.P(*a)); s.SetEnd(self.P(*c))
|
||||
s.SetLayer(L); s.SetWidth(mm(width))
|
||||
self.b.Add(s)
|
||||
if r > 0:
|
||||
# 三点法画圆角:只给圆心+起止点时 KiCad 会按方向取弧,可能绕远路穿过板内
|
||||
# (DVBS2_dmod 第一版就这样,DRC 报孔到板边 0mm);中间点放在外侧 45° 处就唯一了
|
||||
k = r * (1 - 0.5 ** 0.5)
|
||||
for s, m, e in (((0, r), (k, k), (r, 0)), ((w - r, 0), (w - k, k), (w, r)),
|
||||
((w, h - r), (w - k, h - k), (w - r, h)), ((r, h), (k, h - k), (0, h - r))):
|
||||
a = pcbnew.PCB_SHAPE(self.b)
|
||||
a.SetShape(pcbnew.SHAPE_T_ARC)
|
||||
a.SetArcGeometry(self.P(*s), self.P(*m), self.P(*e))
|
||||
a.SetLayer(L); a.SetWidth(mm(width))
|
||||
self.b.Add(a)
|
||||
|
||||
def has_outline(self):
|
||||
return any(d.GetLayer() == pcbnew.Edge_Cuts for d in self.b.GetDrawings())
|
||||
|
||||
def _strip_edge_cuts(self):
|
||||
"""按文本删掉顶层 Edge.Cuts 图元(gr_line/gr_arc/...),返回删掉的个数。"""
|
||||
from .kicad_lib import balanced
|
||||
t = io.open(self.pcb, encoding="utf-8").read()
|
||||
pat = re.compile(r"\n\t\((gr_line|gr_arc|gr_rect|gr_circle|gr_poly)\b")
|
||||
out, i, n = [], 0, 0
|
||||
for m in pat.finditer(t):
|
||||
if m.start() < i:
|
||||
continue
|
||||
blk = balanced(t, m.start() + 2)
|
||||
if '(layer "Edge.Cuts")' in blk:
|
||||
out.append(t[i:m.start()])
|
||||
i = m.start() + 2 + len(blk)
|
||||
n += 1
|
||||
out.append(t[i:])
|
||||
io.open(self.pcb, "w", encoding="utf-8", newline="\n").write("".join(out))
|
||||
return n
|
||||
|
||||
# ---------- 摆放 ----------
|
||||
def _side(self, f, bottom):
|
||||
if f.IsFlipped() != bottom:
|
||||
try:
|
||||
f.Flip(f.GetPosition(), pcbnew.FLIP_DIRECTION_TOP_BOTTOM)
|
||||
except (AttributeError, TypeError):
|
||||
f.Flip(f.GetPosition(), False)
|
||||
|
||||
def at(self, ref, x, y, rot=0, bottom=False):
|
||||
"""封装原点落在板内坐标 (x, y)。rot 为俯视逆时针角度(底面件也按俯视给)。"""
|
||||
f = self.fp[ref]
|
||||
self._side(f, bottom)
|
||||
f.SetOrientationDegrees(rot)
|
||||
f.SetPosition(self.P(x, y))
|
||||
self.placed.add(ref)
|
||||
return f
|
||||
|
||||
def box(self, ref):
|
||||
"""封装占地(优先 courtyard,没有就用含焊盘/丝印的外框),板内坐标 (x0, y0, x1, y1)。"""
|
||||
f = self.fp[ref]
|
||||
cy = f.GetCourtyard(pcbnew.B_CrtYd if f.IsFlipped() else pcbnew.F_CrtYd)
|
||||
if cy.OutlineCount():
|
||||
bb = cy.BBox()
|
||||
x0, y0, w, h = bb.GetX(), bb.GetY(), bb.GetWidth(), bb.GetHeight()
|
||||
else:
|
||||
bb = f.GetBoundingBox(False, False)
|
||||
x0, y0, w, h = bb.GetX(), bb.GetY(), bb.GetWidth(), bb.GetHeight()
|
||||
X0, Y0 = pcbnew.ToMM(x0) - self.ox, pcbnew.ToMM(y0) - self.oy
|
||||
return (X0, Y0, X0 + pcbnew.ToMM(w), Y0 + pcbnew.ToMM(h))
|
||||
|
||||
def center(self, ref, x, y, rot=0, bottom=False):
|
||||
"""封装占地中心落在 (x, y)——比按原点摆直观(原点常在 1 脚)。"""
|
||||
self.at(ref, x, y, rot, bottom)
|
||||
x0, y0, x1, y1 = self.box(ref)
|
||||
dx, dy = x - (x0 + x1) / 2.0, y - (y0 + y1) / 2.0
|
||||
pos = self.fp[ref].GetPosition()
|
||||
self.fp[ref].SetPosition(pcbnew.VECTOR2I(pos.x + mm(dx), pos.y + mm(dy)))
|
||||
return self.box(ref)
|
||||
|
||||
def keepout(self, name, x0, y0, x1, y1, bottom=False):
|
||||
"""登记一块实物占地(封装里没画的护套、螺丝头、线缆出口……)参与重叠检查。"""
|
||||
self.keepouts.append((name, x0, y0, x1, y1, "B" if bottom else "F"))
|
||||
|
||||
def allow_overhang(self, *refs):
|
||||
self.overhang.update(refs)
|
||||
|
||||
# ---------- 检查 ----------
|
||||
def check(self, gap=0.0, edge=0.3):
|
||||
"""同面占地两两不重叠(留 gap),占地在板框内(离边 edge,allow_overhang 的除外),
|
||||
全部封装都摆过。返回问题清单。"""
|
||||
def hit(p, q, g):
|
||||
return not (p[2] + g <= q[0] or q[2] + g <= p[0] or p[3] + g <= q[1] or q[3] + g <= p[1])
|
||||
|
||||
items = [] # (名字, 占地框, 面, 通孔焊盘框列表)
|
||||
for ref, f in self.fp.items():
|
||||
side = "B" if f.IsFlipped() else "F"
|
||||
th = []
|
||||
for p in f.Pads():
|
||||
if p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH):
|
||||
bb = p.GetBoundingBox()
|
||||
x0, y0 = pcbnew.ToMM(bb.GetX()) - self.ox, pcbnew.ToMM(bb.GetY()) - self.oy
|
||||
th.append((x0, y0, x0 + pcbnew.ToMM(bb.GetWidth()), y0 + pcbnew.ToMM(bb.GetHeight())))
|
||||
items.append((ref, self.box(ref), side, th))
|
||||
for k in self.keepouts:
|
||||
items.append((k[0], k[1:5], k[5], []))
|
||||
bad = []
|
||||
for i in range(len(items)):
|
||||
a, ba, sa, ta = items[i]
|
||||
for j in range(i + 1, len(items)):
|
||||
c, bc, sc, tc = items[j]
|
||||
if a.split("-")[0] == c.split("-")[0]:
|
||||
continue # 器件和它自己登记的禁布区(如 J1-shroud)
|
||||
if sa == sc:
|
||||
if hit(ba, bc, gap):
|
||||
bad.append("重叠 %s x %s" % (a, c))
|
||||
continue
|
||||
# 不同面:只有通孔焊盘/引脚会穿过去,按焊盘比(金属壳 NIM 底下的背面去耦是合法的)
|
||||
if any(hit(pb, bc, gap) for pb in ta) or any(hit(pb, ba, gap) for pb in tc):
|
||||
bad.append("跨面 %s x %s(通孔焊盘)" % (a, c))
|
||||
if self.w:
|
||||
for ref, bx, side, _ in items:
|
||||
if ref in self.overhang:
|
||||
continue
|
||||
if bx[0] < edge or bx[1] < edge or bx[2] > self.w - edge or bx[3] > self.h - edge:
|
||||
bad.append("出板/贴边 %s (%.1f,%.1f)-(%.1f,%.1f)" % ((ref,) + tuple(bx)))
|
||||
miss = sorted(set(self.fp) - self.placed)
|
||||
if miss:
|
||||
bad.append("没摆的件:%s" % " ".join(miss))
|
||||
return bad
|
||||
|
||||
# ---------- 丝印 ----------
|
||||
def _bb(self, item):
|
||||
bb = item.GetBoundingBox()
|
||||
x0, y0 = pcbnew.ToMM(bb.GetX()) - self.ox, pcbnew.ToMM(bb.GetY()) - self.oy
|
||||
return (x0, y0, x0 + pcbnew.ToMM(bb.GetWidth()), y0 + pcbnew.ToMM(bb.GetHeight()))
|
||||
|
||||
def silk_obstacles(self, side):
|
||||
"""某一面丝印要避开的东西:该面所有器件占地、所有通孔焊盘、该面所有贴片焊盘、
|
||||
各器件画在该面丝印层上的图形(外框线)、登记的禁布区。返回 [(名字, 框)]。"""
|
||||
silk = pcbnew.B_SilkS if side == "B" else pcbnew.F_SilkS
|
||||
obs = []
|
||||
for ref, f in self.fp.items():
|
||||
fs = "B" if f.IsFlipped() else "F"
|
||||
if fs == side:
|
||||
obs.append((ref + "/body", self.box(ref)))
|
||||
for p in f.Pads():
|
||||
th = p.GetAttribute() in (pcbnew.PAD_ATTRIB_PTH, pcbnew.PAD_ATTRIB_NPTH)
|
||||
if th or fs == side:
|
||||
obs.append((ref + "/pad", self._bb(p)))
|
||||
for g in f.GraphicalItems():
|
||||
if g.GetLayer() == silk and g.GetClass() not in ("PCB_TEXT", "PCB_FIELD"):
|
||||
obs.append((ref + "/silk", self._bb(g)))
|
||||
for k in self.keepouts:
|
||||
if k[5] == side:
|
||||
obs.append((k[0], k[1:5]))
|
||||
return obs
|
||||
|
||||
def place_refs(self, size=0.8, thick=0.15, gap=0.15, edge=0.35, order=None, fixed=None,
|
||||
max_off=4.0, prefer=None):
|
||||
"""自动摆位号:字高 size / 笔宽 thick(嘉立创丝印下限 0.8/0.15),数值一律移到 Fab 层。
|
||||
每个位号在自己器件四周按"上/下/左/右 × 横/竖"逐步外移找空位,要求不压任何器件占地
|
||||
(含自己)、焊盘、丝印图形、已摆的位号、板边。fixed={位号: (x, y, 角度)} 手工指定。
|
||||
prefer={位号: (方位, 角度)},方位 u/d/l/r、角度 0/90:优先试这个组合(一排小件想要整齐
|
||||
地竖写在正上方时用)。返回摆不下的位号清单。"""
|
||||
fixed = fixed or {}
|
||||
prefer = prefer or {}
|
||||
placed = {"F": [], "B": []}
|
||||
obs_cache = {}
|
||||
fail = []
|
||||
refs = order or sorted(self.fp, key=lambda r: (self.box(r)[2] - self.box(r)[0]) *
|
||||
(self.box(r)[3] - self.box(r)[1]))
|
||||
for ref in refs:
|
||||
f = self.fp[ref]
|
||||
side = "B" if f.IsFlipped() else "F"
|
||||
v = f.Value()
|
||||
v.SetLayer(pcbnew.B_Fab if side == "B" else pcbnew.F_Fab)
|
||||
r = f.Reference()
|
||||
r.SetVisible(True)
|
||||
r.SetLayer(pcbnew.B_SilkS if side == "B" else pcbnew.F_SilkS)
|
||||
r.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size)))
|
||||
r.SetTextThickness(mm(thick))
|
||||
if side not in obs_cache:
|
||||
obs_cache[side] = self.silk_obstacles(side)
|
||||
obs = obs_cache[side]
|
||||
|
||||
def free(tb):
|
||||
if tb[0] < edge or tb[1] < edge or (self.w and tb[2] > self.w - edge) or \
|
||||
(self.h and tb[3] > self.h - edge):
|
||||
return False
|
||||
g = gap
|
||||
for _, o in obs:
|
||||
if not (tb[2] + g <= o[0] or o[2] + g <= tb[0] or tb[3] + g <= o[1] or o[3] + g <= tb[1]):
|
||||
return False
|
||||
for o in placed[side]:
|
||||
if not (tb[2] + g <= o[0] or o[2] + g <= tb[0] or tb[3] + g <= o[1] or o[3] + g <= tb[1]):
|
||||
return False
|
||||
return True
|
||||
|
||||
def put(x, y, ang):
|
||||
r.SetTextAngleDegrees(ang)
|
||||
r.SetPosition(self.P(x, y))
|
||||
return self._bb(r)
|
||||
|
||||
if ref in fixed:
|
||||
x, y, ang = fixed[ref]
|
||||
tb = put(x, y, ang)
|
||||
placed[side].append(tb)
|
||||
if not free(tb):
|
||||
fail.append(ref + "(指定位置仍冲突)")
|
||||
continue
|
||||
x0, y0, x1, y1 = self.box(ref)
|
||||
cx, cy = (x0 + x1) / 2.0, (y0 + y1) / 2.0
|
||||
done = False
|
||||
# 候选位置按代价排序:离器件近、横向偏移小的优先(位号要一眼看出属于谁),
|
||||
# 竖排略贵于横排;横向偏移不超过器件自身半宽/半高 + 1mm
|
||||
sizes = {}
|
||||
for ang in (0, 90):
|
||||
tb0 = put(0, 0, ang)
|
||||
sizes[ang] = (tb0[2] - tb0[0], tb0[3] - tb0[1])
|
||||
hx, hy = (x1 - x0) / 2.0 + 1.0, (y1 - y0) / 2.0 + 1.0
|
||||
cands = []
|
||||
for ang, (tw, th) in sizes.items():
|
||||
d = 0.0
|
||||
while d <= max_off + 1e-9:
|
||||
for k in range(0, 41):
|
||||
s = (k + 1) // 2 * 0.25 * (1 if k % 2 else -1)
|
||||
for side_, (x, y), lim in (("u", (cx + s, y0 - gap - d - th / 2), hx),
|
||||
("d", (cx + s, y1 + gap + d + th / 2), hx),
|
||||
("l", (x0 - gap - d - tw / 2, cy + s), hy),
|
||||
("r", (x1 + gap + d + tw / 2, cy + s), hy)):
|
||||
if abs(s) > lim:
|
||||
continue
|
||||
cost = d + 0.8 * abs(s) + (0.4 if ang == 90 else 0.0)
|
||||
if prefer.get(ref) == (side_, ang):
|
||||
cost -= 3.0
|
||||
cands.append((cost, x, y, ang))
|
||||
d += 0.25
|
||||
cands.sort(key=lambda c: c[0])
|
||||
for _, x, y, a in cands:
|
||||
tb = put(x, y, a)
|
||||
dx = x - (tb[0] + tb[2]) / 2.0 # 文字框中心校正到 (x, y)
|
||||
dy = y - (tb[1] + tb[3]) / 2.0
|
||||
tb = put(x + dx, y + dy, a)
|
||||
if free(tb):
|
||||
placed[side].append(tb)
|
||||
done = True
|
||||
break
|
||||
if not done:
|
||||
fail.append(ref)
|
||||
r.SetVisible(False)
|
||||
return fail
|
||||
|
||||
# ---------- 存盘 / 叠层 / 出图 ----------
|
||||
def layers(self, names=None, n=None):
|
||||
if n:
|
||||
self.b.SetCopperLayerCount(n)
|
||||
for lid, nm in (names or {}).items():
|
||||
self.b.SetLayerName(lid, nm)
|
||||
|
||||
def save(self, stackup=None, power_layers=()):
|
||||
pcbnew.SaveBoard(self.pcb, self.b)
|
||||
if stackup or power_layers:
|
||||
t = io.open(self.pcb, encoding="utf-8").read()
|
||||
head = "\t(setup\n"
|
||||
if stackup and "(stackup" not in t and head in t:
|
||||
t = t.replace(head, stackup, 1)
|
||||
for ln in power_layers: # SetLayerType 写不进,按文本改(G-0038)
|
||||
t = t.replace('"%s" signal' % ln, '"%s" power' % ln)
|
||||
io.open(self.pcb, "w", encoding="utf-8", newline="\n").write(t)
|
||||
|
||||
def render(self, png, layers="F.Cu,B.Cu,F.SilkS,B.SilkS,F.Fab,F.CrtYd,B.CrtYd,Edge.Cuts,Dwgs.User",
|
||||
dpi=200):
|
||||
"""出一张总览 PNG(kicad-cli 出 SVG → headless 浏览器截图太麻烦,直接走 PDF + fitz)。"""
|
||||
pdf = os.path.splitext(png)[0] + ".pdf"
|
||||
subprocess.run([kicad_cli(), "pcb", "export", "pdf", "--layers", layers, "--mode-single",
|
||||
"-o", pdf, self.pcb], capture_output=True)
|
||||
clip = ((self.ox - 20) / 25.4 * 72, (self.oy - 8) / 25.4 * 72,
|
||||
(self.ox + (self.w or 100) + 8) / 25.4 * 72, (self.oy + (self.h or 100) + 8) / 25.4 * 72)
|
||||
code = ("import fitz,sys; d=fitz.open(sys.argv[1]); p=d[0]; r=fitz.Rect(%r,%r,%r,%r); "
|
||||
"p.get_pixmap(dpi=%d, clip=r & p.rect).save(sys.argv[2])" % (clip + (dpi,)))
|
||||
# KiCad 自带 python 通常没有 pymupdf,交给装了 fitz 的系统 python 转 PNG。
|
||||
# 注意在 KiCad python 里 PATH 上的 "python" 仍是它自己,所以按候选路径找。
|
||||
subprocess.run([sys_python(), "-c", code, pdf, png], check=True)
|
||||
return png
|
||||
@@ -0,0 +1,117 @@
|
||||
"""根图(第 1 页):左栏层次页符号 + 右侧内嵌 drawio 框图 PNG。
|
||||
|
||||
跨页连接一律用全局标签,层次页不需要 sheet pin。子页的 sheet_uuid 由 uid("sh", 文件名)
|
||||
推出,与 page.Page 的约定一致,所以根图和子图可以各自单独重生成。
|
||||
|
||||
from schgen.root import write_root
|
||||
write_root(prj, [("02_Power.kicad_sch", "02 电源"), ...], png=r"...\\diagram.png",
|
||||
title="MyBoard", note="一段说明文字")
|
||||
|
||||
插图:KiCad 对没有 pHYs 的 PNG 按 300DPI 解释,scale=1 时宽 = 像素/300*25.4mm(G-0022),
|
||||
据此反算 scale;(image (at 中心)) 给的是中心不是左上角;base64 每行 76 字符。
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
import os
|
||||
import struct
|
||||
|
||||
from .kicad_lib import uid, esc
|
||||
from .layout import PAPER
|
||||
|
||||
KICAD_BITMAP_DPI = 300.0
|
||||
|
||||
|
||||
def image_block(png, x0, y0, max_w, max_h, key="img"):
|
||||
raw = io.open(png, "rb").read()
|
||||
px, py = struct.unpack(">II", raw[16:24])
|
||||
nat_w, nat_h = px / KICAD_BITMAP_DPI * 25.4, py / KICAD_BITMAP_DPI * 25.4
|
||||
scale = min(max_w / nat_w, max_h / nat_h)
|
||||
w, h = nat_w * scale, nat_h * scale
|
||||
cx, cy = x0 + w / 2.0, y0 + h / 2.0
|
||||
b64 = base64.b64encode(raw).decode("ascii")
|
||||
body = "\n\t\t\t".join('"%s"' % b64[i:i + 76] for i in range(0, len(b64), 76))
|
||||
blk = ('\t(image\n\t\t(at %.3f %.3f)\n\t\t(scale %.6f)\n\t\t(uuid "%s")\n'
|
||||
'\t\t(data %s\n\t\t)\n\t)' % (cx, cy, scale, uid("root", key, os.path.basename(png)), body)) # 只用文件名:工程搬目录后重生成结果不变
|
||||
return blk, (x0, y0, w, h)
|
||||
|
||||
|
||||
def write_root(prj, sheets, png=None, title=None, note="", paper="A2",
|
||||
col_w=110.0, sheet_h=18.0, gap=10.0, img_note=None):
|
||||
"""sheets: [(文件名, 显示名), ...],按顺序排在左栏(页码 2、3…)。"""
|
||||
PW, PH = PAPER[paper]
|
||||
title = title or prj.name
|
||||
root_u = uid("root", prj.name)
|
||||
out = ['(kicad_sch (version 20231120) (generator "kicad_flow_schgen") (generator_version "8.0")',
|
||||
' (uuid "%s")' % root_u,
|
||||
' (paper "%s")' % paper,
|
||||
' (title_block (title "%s") (rev "%s") (company "%s"))'
|
||||
% (esc(title), prj.rev, esc(prj.company)),
|
||||
' (lib_symbols)']
|
||||
x = 26.0
|
||||
y = 24.0
|
||||
if note:
|
||||
out.append(' (text "%s" (at %.1f %.1f 0) (effects (font (size 1.8 1.8)) (justify left top))'
|
||||
' (uuid "%s"))' % (esc(note), x, y, uid("root", "note")))
|
||||
y += 6.0 + 2.6 * (note.count("\n") + 1)
|
||||
for i, (fn, name) in enumerate(sheets):
|
||||
sy = y + 6.0 + i * (sheet_h + gap)
|
||||
out.append(
|
||||
' (sheet (at %.1f %.1f) (size %.1f %.1f) (fields_autoplaced yes)\n'
|
||||
' (stroke (width 0.1524) (type solid)) (fill (color 0 0 0 0.0))\n'
|
||||
' (uuid "%s")\n'
|
||||
' (property "Sheetname" "%s" (at %.1f %.2f 0)\n'
|
||||
' (effects (font (size 1.8 1.8)) (justify left bottom)))\n'
|
||||
' (property "Sheetfile" "%s" (at %.1f %.2f 0)\n'
|
||||
' (effects (font (size 1.4 1.4)) (justify left top)))\n'
|
||||
' (instances (project "%s" (path "/%s" (page "%d"))))\n'
|
||||
' )' % (x, sy, col_w, sheet_h, uid("sh", fn), esc(name), x, sy - 1.0,
|
||||
fn, x, sy + sheet_h + 1.0, prj.name, root_u, i + 2))
|
||||
info = None
|
||||
if png:
|
||||
ix0 = x + col_w + 14.0
|
||||
iy0 = 24.0
|
||||
blk, info = image_block(png, ix0, iy0, PW - 14.0 - ix0, PH - 40.0 - iy0 - 8.0)
|
||||
out.append(blk)
|
||||
if img_note:
|
||||
out.append(' (text "%s" (at %.1f %.1f 0) (effects (font (size 1.6 1.6)) (justify left top))'
|
||||
' (uuid "%s"))' % (esc(img_note), ix0, iy0 + info[3] + 3.0,
|
||||
uid("root", "imgnote")))
|
||||
out.append(' (sheet_instances (path "/" (page "1")))')
|
||||
out.append(')')
|
||||
p = os.path.join(prj.out_dir, prj.name + ".kicad_sch")
|
||||
io.open(p, "w", encoding="utf-8").write("\n".join(out) + "\n")
|
||||
print("根图 %s (%.0f KB)%s" % (p, os.path.getsize(p) / 1024.0,
|
||||
" 框图 %.0fx%.0f mm" % (info[2], info[3]) if info else ""))
|
||||
return p
|
||||
|
||||
|
||||
def write_project(prj):
|
||||
"""最小 .kicad_pro(没有它 KiCad 打不开工程);已存在就不动。"""
|
||||
p = os.path.join(prj.out_dir, prj.name + ".kicad_pro")
|
||||
if not os.path.exists(p):
|
||||
io.open(p, "w", encoding="utf-8").write(
|
||||
'{\n "meta": {"filename": "%s.kicad_pro", "version": 1},\n'
|
||||
' "schematic": {"legacy_lib_dir": "", "legacy_lib_list": []},\n'
|
||||
' "sheets": [],\n "boards": [],\n "libraries": {"pinned_footprint_libs": [],'
|
||||
' "pinned_symbol_libs": []}\n}\n' % prj.name)
|
||||
return p
|
||||
|
||||
|
||||
def write_lib_tables(prj, sym_libs=(), fp_libs=()):
|
||||
"""工程级 sym-lib-table / fp-lib-table,用 ${KIPRJMOD} 相对路径。
|
||||
改完必须**整个退出 KiCad 再重启**才生效(G-0020)。"""
|
||||
def table(kind, libs):
|
||||
rows = ['(%s_lib_table' % kind, ' (version 7)']
|
||||
for name, rel in libs:
|
||||
# 绝对路径原样写;ipots 工程里 ${KIPRJMOD} 版 fp-lib-table 曾没被认到(多半是没重启)
|
||||
uri = rel if os.path.isabs(rel) else "${KIPRJMOD}/" + rel
|
||||
rows.append(' (lib (name "%s") (type "KiCad") (uri "%s") (options "")'
|
||||
' (descr ""))' % (name, uri.replace("\\", "/")))
|
||||
rows.append(')')
|
||||
return "\n".join(rows) + "\n"
|
||||
if sym_libs:
|
||||
io.open(os.path.join(prj.out_dir, "sym-lib-table"), "w", encoding="utf-8").write(
|
||||
table("sym", sym_libs))
|
||||
if fp_libs:
|
||||
io.open(os.path.join(prj.out_dir, "fp-lib-table"), "w", encoding="utf-8").write(
|
||||
table("fp", fp_libs))
|
||||
@@ -0,0 +1,130 @@
|
||||
"""工程符号库:从参考设计的 .kicad_sch 抽符号,或按引脚表程序化生成方框符号。
|
||||
|
||||
from schgen.symgen import grab_from_sch, box_symbol, write_lib
|
||||
blocks = [grab_from_sch(r"...\\SMOD\\07_PWR.kicad_sch", "IC_SCH:root_0_TPS7A92_IC_SCH.SchLib",
|
||||
"TPS7A9201DSK"),
|
||||
box_symbol("TPS65235", ref="U", value="TPS65235",
|
||||
left=[("2", "VIN", "power_in"), ...], right=[...], top=[...], bottom=[...])]
|
||||
write_lib(r"...\\lib\\myboard.kicad_sym", blocks)
|
||||
|
||||
先查参考设计有没有现成符号(抄过来最省事、引脚最不容易错),没有才自己生成。
|
||||
"""
|
||||
import io
|
||||
import re
|
||||
|
||||
from .kicad_lib import balanced
|
||||
|
||||
PIN_TYPES = {"input", "output", "bidirectional", "tri_state", "passive", "free",
|
||||
"unspecified", "power_in", "power_out", "open_collector", "open_emitter",
|
||||
"no_connect"}
|
||||
|
||||
|
||||
def rename(block, old, new):
|
||||
"""把顶层符号名和子单元名 old_x_y 一并改掉。"""
|
||||
block = block.replace('(symbol "%s"' % old, '(symbol "%s"' % new, 1)
|
||||
for o in (old.split(':')[-1], old):
|
||||
block = re.sub(r'\(symbol "%s(_\d+_\d+)"' % re.escape(o),
|
||||
lambda m: '(symbol "%s%s"' % (new, m.group(1)), block)
|
||||
return block
|
||||
|
||||
|
||||
def grab_from_sch(sch_path, lib_id, new_name):
|
||||
"""从 .kicad_sch 的 lib_symbols 段里按 lib_id 抽出符号并改名。"""
|
||||
t = io.open(sch_path, encoding="utf-8").read()
|
||||
i = t.find('(symbol "%s"' % lib_id)
|
||||
if i < 0:
|
||||
raise KeyError("%s 里没有 %s" % (sch_path, lib_id))
|
||||
return rename(balanced(t, i), lib_id, new_name)
|
||||
|
||||
|
||||
def pin_count(block):
|
||||
return len(re.findall(r'\(pin\s+\w+\s+\w+', block))
|
||||
|
||||
|
||||
def box_symbol(name, ref="U", value=None, desc="", left=(), right=(), top=(), bottom=(),
|
||||
pitch=2.54, pin_len=5.08, min_w=15.24, min_h=10.16, hidden_num=False,
|
||||
footprint=""):
|
||||
"""方框符号。每边是 [(号, 名[, 类型]), ...],按顺序从上到下 / 从左到右排开;
|
||||
号为 None 表示那一格留空(用来把引脚分组)。宽高按引脚名长度与数量自动撑开。"""
|
||||
value = value or name
|
||||
|
||||
def norm(side):
|
||||
out = []
|
||||
for p in side:
|
||||
if p is None:
|
||||
out.append(None)
|
||||
continue
|
||||
num, nm = p[0], p[1]
|
||||
typ = p[2] if len(p) > 2 else "passive"
|
||||
assert typ in PIN_TYPES, typ
|
||||
out.append((num, nm, typ))
|
||||
return out
|
||||
|
||||
L, Rr, T, B = norm(left), norm(right), norm(top), norm(bottom)
|
||||
# pitch 可以是一个数,也可以按边分别给:{"left":7.62,"right":5.08,"top":12.7,"bottom":12.7}
|
||||
pd = pitch if isinstance(pitch, dict) else {}
|
||||
pl, pr_, pt, pb = (pd.get(k, 2.54) if pd else pitch for k in ("left", "right", "top", "bottom"))
|
||||
namew = lambda side: max([len(p[1]) for p in side if p] or [0]) * 1.1
|
||||
nh = max(len(T), len(B), 0)
|
||||
h = max(min_h, (len(L) + 1) * pl, (len(Rr) + 1) * pr_)
|
||||
w = max(min_w, namew(L) + namew(Rr) + 5.08, (len(T) + 1) * pt, (len(B) + 1) * pb,
|
||||
(max([len(p[1]) for p in T + B if p] or [0]) * 1.1 + 2.54) * 2 if nh else 0)
|
||||
w = round(w / 2.54 / 2 + 0.49) * 2.54 * 2 # 宽高取 2.54 的偶数倍,引脚落格点
|
||||
h = round(h / 2.54 / 2 + 0.49) * 2.54 * 2
|
||||
hw, hh = w / 2.0, h / 2.0
|
||||
lines = [' (symbol "%s"' % name,
|
||||
' (pin_names (offset 0.762))%s' % (' (pin_numbers hide)' if hidden_num else ''),
|
||||
' (exclude_from_sim no) (in_bom yes) (on_board yes)',
|
||||
' (property "Reference" "%s" (at %.2f %.2f 0)'
|
||||
' (effects (font (size 1.27 1.27)) (justify left)))' % (ref, -hw, hh + 1.27),
|
||||
' (property "Value" "%s" (at %.2f %.2f 0)'
|
||||
' (effects (font (size 1.27 1.27)) (justify left)))' % (value, -hw, -hh - 1.27),
|
||||
' (property "Footprint" "%s" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))'
|
||||
% footprint,
|
||||
' (property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))',
|
||||
' (property "Description" "%s" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))'
|
||||
% desc.replace('"', "'"),
|
||||
' (symbol "%s_0_1"' % name,
|
||||
' (rectangle (start %.2f %.2f) (end %.2f %.2f)'
|
||||
' (stroke (width 0.254) (type default)) (fill (type background)))' % (-hw, hh, hw, -hh),
|
||||
' )',
|
||||
' (symbol "%s_1_1"' % name]
|
||||
|
||||
def pin(num, nm, typ, x, y, ang):
|
||||
lines.append(' (pin %s line (at %.2f %.2f %d) (length %.2f)' % (typ, x, y, ang, pin_len))
|
||||
lines.append(' (name "%s" (effects (font (size 1.27 1.27))))' % nm)
|
||||
lines.append(' (number "%s" (effects (font (size 1.27 1.27)))))' % num)
|
||||
|
||||
def col(n, pitch): # 竖边:从上往下,居中
|
||||
top_y = (n - 1) * pitch / 2.0
|
||||
return [round((top_y - k * pitch) / 1.27) * 1.27 for k in range(n)]
|
||||
|
||||
def row(n, pitch):
|
||||
x0 = -(n - 1) * pitch / 2.0
|
||||
return [round((x0 + k * pitch) / 1.27) * 1.27 for k in range(n)]
|
||||
|
||||
for p, y in zip(L, col(len(L), pl)):
|
||||
if p:
|
||||
pin(p[0], p[1], p[2], -hw - pin_len, y, 0)
|
||||
for p, y in zip(Rr, col(len(Rr), pr_)):
|
||||
if p:
|
||||
pin(p[0], p[1], p[2], hw + pin_len, y, 180)
|
||||
for p, x in zip(T, row(len(T), pt)):
|
||||
if p:
|
||||
pin(p[0], p[1], p[2], x, hh + pin_len, 270)
|
||||
for p, x in zip(B, row(len(B), pb)):
|
||||
if p:
|
||||
pin(p[0], p[1], p[2], x, -hh - pin_len, 90)
|
||||
lines += [' )', ' )']
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def write_lib(path, blocks):
|
||||
out = ['(kicad_symbol_lib (version 20231120) (generator "kicad_flow_schgen") (generator_version "8.0")']
|
||||
out += blocks
|
||||
out.append(')')
|
||||
io.open(path, "w", encoding="utf-8").write("\n".join(out) + "\n")
|
||||
names = [re.match(r'\s*\(symbol "([^"]+)"', b).group(1) for b in blocks]
|
||||
for b, n in zip(blocks, names):
|
||||
print(" 符号 %-24s 引脚 %3d" % (n, pin_count(b)))
|
||||
return path
|
||||
Reference in New Issue
Block a user