sync: HP_BOWEI 2026-09-09
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
# Altium → KiCad 迁移 Playbook
|
||||
|
||||
两次真实迁移(XMOD_V1.0:10层/1191元件/15页;GQ_EXCT7G_V1:ADRV9009 板)踩出来的东西。
|
||||
SKILL.md 只写「怎么走」,这里写「为什么」和「踩了会怎样」。遇到卡壳先回来查这里。
|
||||
|
||||
---
|
||||
|
||||
## 1. 为什么必须先转 ASCII
|
||||
|
||||
KiCad 10.0.4 的 Altium **原理图**解析器不支持 AD26 的二进制 `.SchDoc`。直接
|
||||
`File → Import Non-KiCad Project → Altium Project` 会报:
|
||||
|
||||
```
|
||||
Header already parsed
|
||||
```
|
||||
|
||||
PCB 解析器是好的,只有原理图这一侧不行。二进制 `.SchDoc` 是 CFB 复合文档
|
||||
(文件头 `D0 CF 11 E0`),ASCII 版则以 `|HEADER=Protel for Windows` 开头。
|
||||
|
||||
Altium 侧的出路是 `IServerDocument.DoFileSave('ASCII')`,没有 CLI,只能跑
|
||||
DelphiScript。两个坑:
|
||||
|
||||
- **不要经 MCP 驱动这个脚本**。`obj_run_process(ScriptingSystem:RunScript)` 会与
|
||||
桥接自己的 `StartMCPServer` 脚本冲突挂起 —— Altium 一次只能跑一个脚本。
|
||||
正确姿势:**先停掉 eda-agent**,再在 Altium GUI 里 `File → Run Script` 手动跑。
|
||||
- **DelphiScript 的 `FindFirst` 枚举不可靠**,脚本里的文件名必须是硬编码列表。
|
||||
`stage_ascii.py stage` 就是干这个的:用 Python 枚举出实际的 `.SchDoc`,把名字
|
||||
灌进生成的 `.pas` 里。
|
||||
|
||||
转换只对 staging 副本做,原始工程一个字节都不能动。
|
||||
|
||||
---
|
||||
|
||||
## 2. eda-agent(altium MCP)桥接的脾气
|
||||
|
||||
桥接不是常驻服务,是 Altium GUI 里跑的一个轮询 DelphiScript:
|
||||
|
||||
- **约 10 分钟空闲会自己停**。跑长任务中途回来发现 `app_ping` 不通是常态,
|
||||
重跑 `Dispatcher.pas → StartMCPServer` 即可,图里 `check_altium_bridge` 的
|
||||
回环就是为这个留的。
|
||||
- **一次只能跑一个脚本**,所以任何别的 DelphiScript 都要先停桥接。
|
||||
- 开启路径:`DXP/Preferences → Scripting System → Global Projects → Install`
|
||||
选 `Altium_API.PrjScr`,然后 `File → Run Script → Dispatcher.pas →
|
||||
StartMCPServer → Run`,状态窗出现 `MCP: idle` 就是通了。
|
||||
- **eda-agent 生不了 Gerber**。`proj_run_output(gerber)` 在没有 OutJob 的工程上
|
||||
是空操作。Altium 侧的 Gerber 只能让用户在 GUI 里
|
||||
`File → Fabrication Outputs → Gerber Files` 手动导。
|
||||
|
||||
---
|
||||
|
||||
## 3. KiCad 导入这一步没有自动化通道
|
||||
|
||||
整工程导入必须在 **KiCad 主程序(工程管理器)** 里点:
|
||||
`File → Import Non-KiCad Project → Altium Project...`
|
||||
源选 **staging 里的 `_ascii` 版 `.PrjPCB`**,目标选 target_dir。
|
||||
|
||||
`kicad-cli pcb import --format altium` 只能转 PCB 单板,**不要拿它替换主板** ——
|
||||
那样会切断原理图↔PCB 的关联。它有用的地方是另转一份到 `_migration/cli_pcb/`
|
||||
当几何参考/对照。
|
||||
|
||||
导入完成后常见的两个结构问题:
|
||||
|
||||
- **扁平层次**:导入器不生成 sheet 符号,所有页平铺;还可能把某一页当成根图。
|
||||
Save All 时可能生成重复的 `.kicad_sch`(例如 `12.XC7Z100_HP.kicad_sch` 多一份)。
|
||||
确认无引用后删掉重复文件即可,页数要能对回基线。
|
||||
- **`IEEE_SYMBOL` 未处理记录**:IEEE 装饰符号被跳过,无害,不用管。
|
||||
|
||||
文件数核对就是拿「Altium 原理图页数 + 1 块 PCB」对
|
||||
`target_dir` 下的 `*.kicad_sch` + `*.kicad_pcb` 计数。
|
||||
|
||||
---
|
||||
|
||||
## 4. 导入后 PCB 的两个必修项
|
||||
|
||||
### 4.1 BGA / 平面焊盘丢网络(假短路的真凶)
|
||||
|
||||
Altium 用**负片内电层**隐式连接电源和地,焊盘本身不带网络信息。KiCad 没有这套
|
||||
机制,导入时这些焊盘就空着网络。XMOD 实测 **527 个 BGA 电源/地焊盘丢网络**,
|
||||
DRC 里表现为 103 个「GND vs 空网络」短路。
|
||||
|
||||
修法:拿阶段 0 导出的 Altium 网表真值(`netlist_tabular.csv` 的
|
||||
component/pin/net)程序化回填。`fix_pcb.py` 就是干这个的,先 `--dry-run` 看量。
|
||||
|
||||
回填后还会剩一批查不到真值的空网络焊盘 —— 多是 RJ45 屏蔽壳、机械/安装孔焊盘,
|
||||
按实际情况单独指派 GND(XMOD 是 18 个)。
|
||||
|
||||
### 4.2 覆铜 zone 的本地 clearance(异 net 避让偏大的真凶)
|
||||
|
||||
这是最容易白费力气的一个坑。Altium 导入器把**每一块 zone 的本地 Clearance
|
||||
写成 KiCad 默认的 0.5mm**,落在 `.kicad_pcb` 里长这样:
|
||||
|
||||
```
|
||||
(zone ... (connect_pads (clearance 0.5)) ...)
|
||||
```
|
||||
|
||||
而 **zone 的本地值会盖过网络类 clearance**。所以只改 `.kicad_pro` 的网络类
|
||||
(比如 0.2mm)或板级规则,铺铜避让一点都不会变 —— 必须逐块 zone 改。
|
||||
|
||||
目标值取 Altium 的 **polygon clearance**(InPolygon 规则,去基线
|
||||
`pcb_design_rules.json` 查;XMOD 和 GQ 实测都是 **0.254mm**,但别照抄)。
|
||||
|
||||
**热焊盘不用动**:KiCad 的 `thermal_gap` 0.1651mm 已经匹配 Altium
|
||||
`PolygonConnect` 的 AirGap 0.165mm。
|
||||
|
||||
改完必须重填铺铜。大板上 **SWIG/pcbnew 的 refill 容易超时甚至段错误**,
|
||||
去 KiCad GUI 里按 `B`,或者走 IPC 后端。
|
||||
|
||||
### 4.3 DRC 数字的正常轨迹
|
||||
|
||||
导入只搬几何,规则用的是 KiCad 默认(比 Altium 严),所以初始 DRC 会很吓人。
|
||||
XMOD 的轨迹供参考:按基线改完 `.kicad_pro` 规则 **2878 → 1481**(错误
|
||||
1810→414,track/via/drill 三类规则误报各 199 清零);再回填 527 个焊盘网络 +
|
||||
重填铺铜后 clearance 282→0、via_dangling 199→15、shorting 103→92。
|
||||
残留的 92 是板角铺铜畸变和连接器,不是真短路。
|
||||
|
||||
---
|
||||
|
||||
## 5. 网表比对:比 partition,不比网络名
|
||||
|
||||
两个工具的命名规则不同,逐名比对必然一片红。要比的是**等价类**:把网表归约成
|
||||
「每个引脚 → 它所在网络的全部节点集合」,两侧集合相等即电气一致。
|
||||
|
||||
三个归一化,少一个就出假差异:
|
||||
|
||||
1. **KiCad 网名有前导 `/`**(层次路径),Altium 没有 → 剥掉。
|
||||
XMOD 初比出的 16 个差异全是 `R_I-` vs `/R_I-` 这种。
|
||||
2. **Altium tabular 导出有 `net=?` 的重复行**(隐藏的电源/平面引脚被重复列出)。
|
||||
XMOD 原始 9511 行里有 4500 行是 `?`,去重后才是 5011 个真实节点。
|
||||
3. **KiCad 扁平结构下整体导出网表会漏页** —— XMOD 整体导出只拿到根图的 98 个
|
||||
节点。必须**逐页** `kicad-cli sch export netlist --format kicadxml` 再按网名合并
|
||||
(5011 节点 / 1186 元件 / 1477 网)。格式必须是 **kicadxml**;PADS 格式会漏掉
|
||||
单节点网(只得 4154)。
|
||||
|
||||
XMOD 终判:5011=5011 节点全同、0 单边节点、等价类集合完全相等(1473=1473)、
|
||||
0 实质差异 → **网表 100% 一致**。
|
||||
|
||||
`netcmp.py` 把上面全部实现了,包括逐页导出。
|
||||
|
||||
---
|
||||
|
||||
## 6. Gerber / 几何比对:什么叫「100% 一致」
|
||||
|
||||
**跨工具 byte/像素级一致不可能**,这不是没做好,是物理上做不到:aperture、
|
||||
坐标格式、填充算法、字体引擎、正负片全都不同。所以判定基准必须先界定清楚:
|
||||
|
||||
**判定 = 逐层制造几何等价。** 离散铜(走线/焊盘/过孔/孔/板框)要零差异;
|
||||
铺铜填充纹理、丝印字形、负片内电层属于**工具固有容差项**。
|
||||
|
||||
### 6.1 决定性证据是矢量比对,不是栅格 XOR
|
||||
|
||||
栅格 XOR(pygerber 渲染后逐像素)只能给「像什么程度」的百分比,说服力有限。
|
||||
矢量比对是逐条逐个匹配走线和过孔,对上就是同一块板。
|
||||
|
||||
XMOD 的矢量结论:**走线 12144=12144、过孔 4808=4808 逐条逐个相同,
|
||||
`TOTAL_DISCRETE_DIFF=0`**。
|
||||
|
||||
坐标变换只有 2 个自由参数(平移),由两侧过孔质心解出;用 2 个参数去对上
|
||||
几千个图元,对上了就是实证而不是曲线拟合。Altium 与 KiCad 之间是**纯平移,
|
||||
无翻转无镜像**(XMOD 实测 `KiCad = Altium + (38.501, −180.004)`)。
|
||||
注意 Altium 的 y 轴向上、KiCad 向下,变换里 y 要取负。
|
||||
|
||||
容差取 **30um**:eda-agent 读出的 Altium 坐标是整数 mil,读数精度本身
|
||||
±12.7um,两端各一次就接近 25um;KiCad 侧是精确的。
|
||||
|
||||
`dump_kicad_vec.py` + `cmp_vec.py` 实现这一套,Altium 侧的 JSON 格式见
|
||||
`vector-compare.md`。
|
||||
|
||||
### 6.2 栅格比对的参考数值(XMOD 逐层)
|
||||
|
||||
| 层 | 差异 | 判定 |
|
||||
|----|------|------|
|
||||
| 焊盘 Paste | 0.035% | 完全一致(也用来验证平移变换的精度) |
|
||||
| 板框 Profile | 0.34% | 一致 |
|
||||
| 外层铜 F_Cu | 1.04% | 基本一致 |
|
||||
| 阻焊 Mask | ~2% | 扩展量微差 |
|
||||
| 丝印 Silk | 42–70% | 字体引擎不同,固有 |
|
||||
| 内电层 GND/POWER ×5 | 反相后 ~12% | Altium 负片 vs KiCad 正片 |
|
||||
| 内层信号 ×3 | 高 | 铺铜覆盖/填充差异 |
|
||||
|
||||
看到丝印 70% 不要慌,那是字体,不是错。负片内电层必须**先反相**才能比。
|
||||
|
||||
### 6.3 三个不可消除的工具固有差异(已查明根因,不是缺陷)
|
||||
|
||||
1. **负片内电层**:KiCad 架构里没有「内电层」这种层类型,也没有负片输出能力。
|
||||
与 Altium 的负片格式无法一致,但**物理铜等价**——制板厂显影结果相同。
|
||||
2. **铺铜填充纹理**:填充算法不同(热焊盘辐条/网格/反焊盘),即使轮廓和网络
|
||||
完全相同也不可能逐像素一致。
|
||||
3. **丝印字体**:字体引擎不同。
|
||||
|
||||
验收报告里要把这三条明确列为容差项,别当缺陷去修 —— 修不掉。
|
||||
|
||||
---
|
||||
|
||||
## 7. Altium 侧 Gerber 手动导出参数
|
||||
|
||||
eda-agent 生不了,让用户在 Altium 里导,参数要对齐 KiCad 侧:
|
||||
|
||||
- `File → Fabrication Outputs → Gerber Files`:单位 mm、精度 4:6、
|
||||
Embedded RS274X、**不镜像**、**绝对原点**
|
||||
- `File → Fabrication Outputs → NC Drill Files`
|
||||
- 输出落在 `Project Outputs for <工程名>\`,复制到 `_migration/gerber/altium/`
|
||||
|
||||
KiCad 侧对应:`kicad-cli pcb export gerbers`(精度 6、绝对原点)+
|
||||
`kicad-cli pcb export drill`(Excellon、mm)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 证据目录约定
|
||||
|
||||
全部落在 `<target_dir>\_migration\`:
|
||||
|
||||
```
|
||||
altium_baseline/ 阶段0 基线真值:stats / nets / bom / stackup / pcb_design_rules.json
|
||||
reference/ 参考网表(netlist_tabular.csv) + 原理图 PDF
|
||||
altium_source_snapshot/ 只读源副本
|
||||
convert_to_ascii.pas 由 stage_ascii.py 生成
|
||||
ascii_convert_log.txt DelphiScript 跑完的日志
|
||||
cli_pcb/ kicad-cli 忠实导入的 PCB(仅作几何参考,不替换主板)
|
||||
netcmp/ xml_*.xml 逐页网表 + diff.json + 报告
|
||||
gerber/ altium_vec/ kicad_vec/ layer_map.json + 比对报告
|
||||
backup/ 每次改板前的 .kicad_pcb 快照
|
||||
verification_report.md 最终验收报告
|
||||
```
|
||||
|
||||
XMOD 的基线真值(作为「基线该长什么样」的样例):15 页 / 元件 1191(PCB 1186)/
|
||||
管脚 9511 / 网络 1474 / 10 层 / 走线 21296 · 过孔 4808 · 焊盘 5154 /
|
||||
未布线 0 / 224×150mm / 41 条设计规则。
|
||||
@@ -0,0 +1,125 @@
|
||||
# 矢量几何比对:Altium 侧 JSON 怎么导
|
||||
|
||||
`cmp_vec.py` 要两个目录:`kicad_vec/` 由 `dump_kicad_vec.py` 生成;
|
||||
`altium_vec/` 得你在 Claude 会话里用 eda-agent 的 MCP 工具查出来再写盘 ——
|
||||
LangGraph 进程里没有 MCP,这一步必须由会话来做。
|
||||
|
||||
---
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
_migration\gerber\
|
||||
├── altium_vec\ meta.json vias.json pads.json tracks.json
|
||||
├── kicad_vec\ meta.json vias.json pads.json tracks.json (dump_kicad_vec.py 生成)
|
||||
└── layer_map.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## altium_vec 各文件的格式
|
||||
|
||||
`cmp_vec.py` 的 `load()` 容忍三种外壳:裸 list、`{"vias": [...]}`、`{"objects": [...]}`,
|
||||
所以直接把 MCP 返回的结果落盘一般就能用。字段名**区分大小写**,按下表来。
|
||||
|
||||
### meta.json
|
||||
|
||||
```json
|
||||
{
|
||||
"unit_note": "eda-agent 返回的是整数 mil,读数精度 ±0.5mil = 12.7um",
|
||||
"scale_to_mm": 0.0254,
|
||||
"counts": {"tracks": 10206, "arcs": 108, "pads": 3010, "vias": 2209}
|
||||
}
|
||||
```
|
||||
|
||||
`scale_to_mm` 是 `cmp_vec.py` 唯一会读的字段(也可以用 `--scale-to-mm` 覆盖)。
|
||||
eda-agent 返回整数 mil,所以是 `0.0254`。
|
||||
|
||||
### vias.json — 来自 `pcb_get_vias`
|
||||
|
||||
小写字段:
|
||||
|
||||
```json
|
||||
{"vias": [{"x": 22046, "y": 33005, "size": 20, "hole_size": 12,
|
||||
"net": "VCC3V3", "low_layer": "TopLayer", "high_layer": "BottomLayer"}]}
|
||||
```
|
||||
|
||||
`cmp_vec.py` 只用 `x` / `y`,其余留着供人核对。
|
||||
|
||||
### tracks.json — 来自 `obj_query`(PCB 文档,Track 对象)
|
||||
|
||||
**大写**字段(obj_query 原样返回 Altium 的属性名),值可以是字符串:
|
||||
|
||||
```json
|
||||
{"objects": [{"Layer": "TopLayer", "X1": "21739", "Y1": "33141",
|
||||
"X2": "21859", "Y2": "33141", "Width": "6", "Net": "PS232_RX"}]}
|
||||
```
|
||||
|
||||
`cmp_vec.py` 用 `Layer` / `X1,Y1` / `X2,Y2`,按中点逐层匹配。
|
||||
|
||||
### pads.json — 来自 `obj_query`(Pad 对象)
|
||||
|
||||
```json
|
||||
{"objects": [{"Name": "", "X": "22666", "Y": "32072", "Layer": "TopLayer",
|
||||
"Net": "GND", "HoleSize": "0", "TopXSize": "59",
|
||||
"TopYSize": "118", "Rotation": "90"}]}
|
||||
```
|
||||
|
||||
用 `X` / `Y`。
|
||||
|
||||
---
|
||||
|
||||
## layer_map.json
|
||||
|
||||
Altium 层名 → KiCad 层名。**必须逐工程写,不能套公式** —— `MidLayerN` 到
|
||||
`InN.Cu` 的对应取决于叠层顺序,想当然会全错。
|
||||
|
||||
```json
|
||||
{
|
||||
"TopLayer": "F.Cu",
|
||||
"MidLayer1": "In2.Cu",
|
||||
"MidLayer3": "In5.Cu",
|
||||
"MidLayer2": "In7.Cu",
|
||||
"BottomLayer": "B.Cu"
|
||||
}
|
||||
```
|
||||
|
||||
(上面是 GQ_EXCT7G_V1 的真实映射,注意 MidLayer2/3 是反的。)
|
||||
|
||||
怎么定:不带 `--layer-map` 跑一次 `cmp_vec.py`,它会打印两侧每层的走线条数,
|
||||
按计数配对;再用 `pcb_get_layer_stackup`(Altium 侧)和 `get_layer_list`
|
||||
(KiCad 侧)核对叠层顺序。内电层(负片平面)通常没有走线,不进 map。
|
||||
|
||||
---
|
||||
|
||||
## 跑一遍
|
||||
|
||||
```powershell
|
||||
# 1) KiCad 侧(下面的 KiCad python 路径取自 ~/skills_env.md 的 KICAD_PYTHON)
|
||||
& "D:\KiCad\10.0\bin\python.exe" dump_kicad_vec.py `
|
||||
--board "E:\Kicad_design\FOO\FOO.kicad_pcb" `
|
||||
--out "E:\Kicad_design\FOO\_migration\gerber\kicad_vec"
|
||||
|
||||
# 2) Altium 侧:在会话里用 eda-agent 查,按上面的格式写进 altium_vec\
|
||||
|
||||
# 3) 先不给 layer_map,看两侧每层走线数,定映射
|
||||
python cmp_vec.py --altium-vec ...\altium_vec --kicad-vec ...\kicad_vec
|
||||
|
||||
# 4) 写好 layer_map.json 再跑完整比对
|
||||
python cmp_vec.py --altium-vec ...\altium_vec --kicad-vec ...\kicad_vec `
|
||||
--layer-map ...\layer_map.json --report ...\vec_result.json
|
||||
```
|
||||
|
||||
退出码 0 = 全部图元匹配。
|
||||
|
||||
---
|
||||
|
||||
## 结果怎么读
|
||||
|
||||
- `unmatched_altium` 和 `unmatched_kicad` **都要是 0**。只看一边会漏掉
|
||||
「KiCad 多出来的图元」。
|
||||
- `residual mm` 的 max 应该在容差内且远小于容差(XMOD/GQ 实测中位数在 1um 量级)。
|
||||
如果 max 贴着容差上限,多半是平移标定被拉偏了 —— 先查两侧过孔数是否相等。
|
||||
- 两侧过孔数不同 → 质心标定必然偏。先查是导入丢了过孔,还是 Altium 侧
|
||||
`obj_query` 漏了层,别硬比。
|
||||
- 某一层大面积 unmatched,而其它层是 0 → 十有八九是 `layer_map.json` 配错了。
|
||||
Reference in New Issue
Block a user