133. Altium Designer: проект, правила, выпуск
Altium Designer — самая крупная из трёх программ: схема, плата, библиотеки, выпуск документации и файлов для завода живут в одном проекте. На машине — Altium Designer Agile 26.0.0.14.
Здесь разобран проект из комплекта примеров — Bluetooth Sentinel с микроконтроллером STM32F101 — и разобран без самого Altium: проект, правила и задание на выпуск прочитаны Python'ом, а плату для сверки открыл KiCad. Так смотрят чужой проект, когда Altium под рукой нет, — и так проверяют, что проект выдержит перенос в другую программу.
Когда Altium под рукой, то же самое находят в меню редактора платы (названия — из установленной версии 26):
| Что | Где в Altium |
|---|---|
| правила (133.3) | Design / Rules |
| стек слоёв (133.4) | Design / Layer Stack Manager |
| проверка правил | Tools / Design Rule Check |
| выпуск по заданию (133.5) | открыть .OutJob, у контейнера выходов — Generate content |
| чужой проект из другой программы | File / Import Wizard |
Как это называется в документации
Оригинал Перевод PCB project ( .PrjPcb)проект платы schematic document ( .SchDoc)лист схемы PCB document ( .PcbDoc)документ платы Output Job ( .OutJob)задание на выпуск design rule правило проектирования query, scope запрос: к каким объектам относится правило layer stack стек слоёв overlay шелкография solder mask, paste mask маска, трафарет пасты NC Drill файл сверловки device sheet готовый лист-блок из библиотеки compound file составной документ: файловая система внутри файла
133.1 Проект — текстовый файл
.PrjPcb — текст в формате INI: разделы в квадратных скобках, строки
«ключ=значение». В примере:
| Раздел | Сколько | Что это |
|---|---|---|
[DocumentN] |
12 | документы проекта |
[DeviceSheetN] |
17 | готовые листы-блоки из библиотеки |
[ConfigurationN] |
3 | конфигурации выпуска: Documentation, Assembly, Fabrication |
[OutputGroupN] |
10 | группы выходных файлов |
Документов 12: пять листов схемы .SchDoc, одна плата .PcbDoc, три
задания на выпуск .OutJob, два .Harness и один .Annotation. Проект
удобно хранить в git: он текстовый, и правка в нём видна строкой.
Ловушка — в первых трёх байтах: файл начинается с метки порядка байтов
UTF-8, и стандартный разбор INI в Python на ней спотыкается. Читать надо
в кодировке utf-8-sig:
def ini(path):
cp = configparser.ConfigParser(interpolation=None, strict=False)
cp.optionxform = str
cp.read(path, encoding="utf-8-sig") # файл начинается с метки порядка байтов UTF-8
return cp
133.2 Схема и плата — составные документы
.SchDoc и .PcbDoc начинаются с одних и тех же восьми байтов
D0 CF 11 E0 A1 B1 1A E1 — это составной документ OLE, файловая система
внутри файла, как у старых документов Word. У платы внутри 47 потоков
данных — по одному на вид объектов: Rules6/Data — правила,
Components6/Data — компоненты, Nets6/Data — цепи, Board6/Data —
настройки и стек слоёв, Tracks6/Data — дорожки.
В потоках правил, компонентов, цепей и схемы — текстовые записи вида
|КЛЮЧ=значение|…, каждая оканчивается нулевым байтом. Вытащить их — десять
строк:
def kv(rec):
d = {}
for part in rec.split("|"):
if "=" in part:
k, v = part.split("=", 1)
d.setdefault(k.upper(), v)
return d
def stream(ole, name):
s = ole.openstream(name).read().decode("latin-1")
return [kv(m.group(0)) for m in re.finditer(r"\|[^\x00]+", s)]
Открывает составной документ пакет olefile; здесь он в образе stm-ole:1.
Лист схемы с микроконтроллером начинается записью-заголовком
«Protel for Windows - Schematic Capture Binary File Version 5.0», за ней
948 записей. Вид записи — число RECORD, а что за объект, видно по ключам:
RECORD |
Сколько | Ключи | Что это |
|---|---|---|---|
| 1 | 16 | LIBREFERENCE, PARTCOUNT |
компонент |
| 2 | 130 | NAME, DESIGNATOR, ELECTRICAL |
вывод |
| 27 | 85 | LOCATIONCOUNT, X1, Y1 |
провод |
| 25 | 53 | TEXT |
метка цепи |
| 34 | 16 | TEXT, NAME |
обозначение: U2, C15, R9 … |
| 41 | 331 | NAME, TEXT |
параметр |
Микроконтроллер — MC-ARM-STM32F101-SQ37: в описании «36-Pin VFQFPN»
и «32 kB Flash, 6 kB Internal RAM». На листе он нарисован двумя частями —
обозначение U2 встречается дважды. На плате у него посадочное место
VFQFPN36_N.
133.3 Правила платы
Правил в примере 38. У каждого — имя, вид (RULEKIND), приоритет и
область действия. Правила с числами:
| Правило | В файле | В миллиметрах |
|---|---|---|
Clearance — зазор |
3.937mil | 0,1 |
Width — ширина дорожки |
3.937–19.685mil, предпочтительная 3.937mil | 0,1–0,5, предпочтительная 0,1 |
RoutingVias — переходное отверстие |
19.685mil, отверстие 11.811mil | 0,5, отверстие 0,3 |
HoleToHoleClearance — между отверстиями |
3.937mil | 0,1 |
ComponentClearance — между корпусами |
7.874mil | 0,2 |
DiffPairsRouting — дифференциальная пара |
10mil | 0,254 |
Длины Altium хранит в милах, даже когда плату разводили в миллиметрах: 3,937 мил — это 0,1 мм, записанные в другой единице. Перевод — мил × 0,0254.
Область действия задаётся запросом. Общие правила относятся ко всем
объектам (All), частные — к выбранным: (CompPinCount < 5) — корпуса
меньше чем с пятью выводами, IsSOIC, IsBGA, IsVia,
InComponentClass('PCBModule'). Среди правил одного вида действует
старшее — с меньшим числом приоритета: у разводки выводов частные
правила стоят с приоритетами 1–4, общее Fanout_Default — с приоритетом 5.
133.4 Стек слоёв
Плата четырёхслойная. Стек из потока Board6/Data:
| Слой | Толщина | εr |
|---|---|---|
Top Solder — маска |
0,3937 мил = 0,01 мм | 3,5 |
Top Layer — медь |
1,4173 мил = 0,036 мм | |
Dielectric 1 |
12,5984 мил = 0,32 мм | 4,8 |
Mid-Layer 1 — медь |
0,036 мм | |
Dielectric 2 |
0,32 мм | 4,8 |
Mid-Layer 2 — медь |
0,036 мм | |
Dielectric 3 |
0,4921 мил = 0,0125 мм | 4,8 |
Bottom Layer — медь |
0,036 мм | |
Bottom Solder — маска |
0,01 мм | 3,5 |
Стек примера несимметричный: третий диэлектрик всего 12,5 мкм. Для заказа так не делают — стек берут из стандартных у завода, симметричный относительно середины платы, иначе её выгнет при пайке.
Сумма — 32,1455 мил, 0,8165 мм:
out["thickness_mm"] = round(sum((l["copper_mil"] or 0) + (l["diel_mil"] or 0) for l in stack) * MIL, 4)
KiCad, открыв эту плату, получил ту же толщину — 0,8165 мм (133.6).
133.5 Выпуск: задание .OutJob
Файлы для завода Altium выпускает по заданию — .OutJob, тоже текст INI.
В Fabrication.OutJob примера три выхода: ODB++, Gerber и NC Drill.
Настройки, которые должен знать завод:
| Выход | Настройка | Значение |
|---|---|---|
| Gerber | единицы | Imperial — дюймы |
| знаков после запятой | 5 | |
| нули | SuppressLeadingZeroes — ведущие опущены |
|
| NC Drill | единицы | Imperial — дюймы |
| формат | 2 знака до запятой, 5 после — «2:5» | |
| нули | SuppressTrailingZeroes — хвостовые опущены |
|
| металлизированные отдельно | False — все отверстия в одном файле |
Если завод прочитает дюймы как миллиметры или возьмёт не тот формат, отверстия уйдут мимо площадок. Поэтому единицы и формат сверловки пишут ещё и в сопроводительном письме.
Сам проект знает больше выходов, чем использует это задание: в группе Fabrication Outputs их 12 — среди них Gerber X2 и IPC-2581, в группе Netlist Outputs — 21 формат списка соединений, в Export Outputs — 11, от STEP до Specctra.
133.6 Проверка чужого проекта без Altium
Скрипт altium_read.py читает проект целиком — документы, правила, стек,
компоненты, цепи, лист с микроконтроллером, задание на выпуск — и отдаёт
JSON. Для сверки ту же плату открывает KiCad 9: у него есть импорт Altium.
С первой попытки KiCad плату не открыл: LoadBoard вернул пустой результат —
по файлу формат не угадывается. Формат надо назвать:
b = pcbnew.LoadBoard(path, pcbnew.PCB_IO_MGR.ALTIUM_DESIGNER)
| Что | Разбор файла | KiCad после импорта |
|---|---|---|
| компонентов | 73 | 73 |
| цепей | 57 | 57 и служебная «без цепи» |
| медных слоёв | 4 | 4 |
| толщина | 0,8165 мм | 0,8165 мм |
| зазор, наименьший | 0,1 мм | 0,1 мм |
| дорожка, наименьшая | 0,1 мм | 0,1 мм |
| переходное, наименьшее | 0,5 мм, отверстие 0,3 мм | 0,5 мм, отверстие 0,3 мм |
| между отверстиями | 0,1 мм | 0,1 мм |
Два независимых чтения сошлись. Но перенос не бесплатен: 28 слоёв Altium
KiCad сопоставить не смог и пропустил, три шрифта заменил своими. А классы
цепей — All Nets из Altium и свой Default — получили настройки самого
KiCad: 0,2 мм зазор и 0,2 мм дорожка, а не 0,1 мм из правил примера. Минимумы
платы перенеслись, классы — нет; после импорта их задают заново.
133.7 Правила примера против профиля из раздела 130
Профиль из раздела 130 — правила, с которыми KiCad создаёт новую плату. Правила примера тоньше в четырёх местах:
| Правило | Пример | Профиль 130 |
|---|---|---|
| зазор | 0,1 мм | 0,2 мм |
| дорожка | 0,1 мм | 0,2 мм |
| переходное | 0,5 мм | 0,6 мм |
| между отверстиями | 0,1 мм | 0,25 мм |
Это не ошибка примера: на плате мелкие корпуса — посадочные места
VFQFPN36_N, CSR-CSR1010A05-IQQM-R-QFN32_M, LGA16-3X3X1_L. Но это
требование к заводу: такую плату заказывают там, где 0,1 мм — обычная
возможность, а не предел. Сверяют до разводки, не после.
133.8 Грабли
| Симптом | Причина |
|---|---|
configparser: «File contains no section headers» на .PrjPcb |
файл начинается с метки порядка байтов UTF-8 — читать utf-8-sig |
| в правилах «3.937mil» вместо 0,1 мм | Altium хранит длины в милах, даже у метрической платы |
KiCad не открывает .PcbDoc, LoadBoard вернул пустое |
формат не угадан — указать PCB_IO_MGR.ALTIUM_DESIGNER |
| после импорта в KiCad пропали слои | 28 слоёв не сопоставлены — сверять стек после импорта |
| классы цепей в KiCad — 0,2 мм, а в Altium было 0,1 | в классы перешли настройки KiCad, не правила Altium |
| завод сверлит мимо | формат сверловки 2:5 в дюймах не передан заводу |
| завод не берёт заказ | правила тоньше его возможностей — сверять до разводки |
133.9 Итоги
- Проект
.PrjPcbи задание.OutJob— текст INI с меткой UTF-8 в начале; схема и плата — составные документы OLE с текстовыми записями|КЛЮЧ=значение|. - В примере: 12 документов, 38 правил, 4 медных слоя, 73 компонента, 57 цепей, толщина 0,8165 мм — разбор файла и импорт KiCad сошлись.
- Длины — в милах: 3,937 мил = 0,1 мм.
- Сверловка примера — 2:5 в дюймах; единицы и формат сообщают заводу.
- При переносе в KiCad минимумы остаются, классы цепей — нет.
- Правила примера тоньше профиля 130 в четырёх местах — это требование к заводу.
Полные исходники
altium_read.py — чтение проекта Altium без Altium: документы, правила, стек, выпуск.
# -*- coding: utf-8 -*-
"""Раздел 133: что лежит в проекте Altium Designer — без самого Altium. Запуск в образе stm-ole:1.
python altium_read.py <папка проекта> <имя>.PrjPcb → JSON в stdout
Проект .PrjPcb и задание на выпуск .OutJob — текст в формате INI. Схема .SchDoc и плата .PcbDoc —
составные документы OLE: внутри потоки по видам объектов (Rules6/Data, Components6/Data, …),
в потоках — текстовые записи «|КЛЮЧ=значение|…», каждая оканчивается нулевым байтом. Длины
в записях — с единицами: «3.937mil»."""
import configparser, json, os, re, sys
import olefile
MIL = 0.0254 # мм в одном миле
def kv(rec):
d = {}
for part in rec.split("|"):
if "=" in part:
k, v = part.split("=", 1)
d.setdefault(k.upper(), v)
return d
def stream(ole, name):
s = ole.openstream(name).read().decode("latin-1")
return [kv(m.group(0)) for m in re.finditer(r"\|[^\x00]+", s)]
def mil(v):
m = re.fullmatch(r"(-?[\d.]+)mil", (v or "").strip())
return float(m.group(1)) if m else None
def ini(path):
cp = configparser.ConfigParser(interpolation=None, strict=False)
cp.optionxform = str
cp.read(path, encoding="utf-8-sig") # файл начинается с метки порядка байтов UTF-8
return cp
def sections(cp, prefix):
return [cp[s] for s in cp.sections() if re.fullmatch(prefix + r"\d+", s)]
def main(folder, prj):
cp = ini(os.path.join(folder, prj))
docs = [s["DocumentPath"] for s in sections(cp, "Document")]
out = {"documents": docs,
"device_sheets": len(sections(cp, "DeviceSheet")),
"configurations": [s["Name"] for s in sections(cp, "Configuration")],
"output_groups": {s.get("Name"): sum(1 for k in s if re.fullmatch(r"OutputType\d+", k))
for s in sections(cp, "OutputGroup")}}
# ── плата ──
pcb = os.path.join(folder, next(d for d in docs if d.lower().endswith(".pcbdoc")))
out["pcb_magic"] = open(pcb, "rb").read(8).hex()
ole = olefile.OleFileIO(pcb)
out["pcb_streams"] = len([s for s in ole.listdir() if s[-1] == "Data"])
rules = []
for r in stream(ole, "Rules6/Data"):
if "RULEKIND" in r:
rules.append({"name": r.get("NAME"), "kind": r["RULEKIND"], "priority": int(r.get("PRIORITY", "0")),
"scope1": r.get("SCOPE1EXPRESSION"), "scope2": r.get("SCOPE2EXPRESSION"),
**{k.lower(): mil(r[k]) for k in ("GAP", "MINLIMIT", "MAXLIMIT", "PREFEREDWIDTH", "MINWIDTH",
"MAXWIDTH", "MINHOLEWIDTH", "MAXHOLEWIDTH") if k in r}})
out["rules"] = rules
board = stream(ole, "Board6/Data")[0]
stack, i = [], 0
while "V9_STACK_LAYER%d_NAME" % i in board:
p = "V9_STACK_LAYER%d_" % i
stack.append({"name": board[p + "NAME"], "copper_mil": mil(board.get(p + "COPTHICK")),
"diel_mil": mil(board.get(p + "DIELHEIGHT")), "er": float(board.get(p + "DIELCONST") or 0) or None})
i += 1
out["stack"] = stack
out["copper_layers"] = [l["name"] for l in stack if l["copper_mil"]]
out["thickness_mm"] = round(sum((l["copper_mil"] or 0) + (l["diel_mil"] or 0) for l in stack) * MIL, 4)
comps = [c for c in stream(ole, "Components6/Data") if "PATTERN" in c]
out["components"] = len(comps)
out["patterns"] = sorted({c["PATTERN"] for c in comps})
out["nets"] = len([n for n in stream(ole, "Nets6/Data") if "NAME" in n])
# ── лист схемы с микроконтроллером ──
for d in docs:
if not d.lower().endswith(".schdoc"):
continue
recs = stream(olefile.OleFileIO(os.path.join(folder, d)), "FileHeader")
mcu = [r for r in recs if r.get("RECORD") == "1" and "STM32" in r.get("LIBREFERENCE", "")]
if not mcu:
continue
kinds = {}
for r in recs[1:]:
kinds[r.get("RECORD")] = kinds.get(r.get("RECORD"), 0) + 1
out["sheet"] = {"file": d, "header": recs[0].get("HEADER"), "records": len(recs) - 1,
"kinds": {k: kinds.get(k, 0) for k in ("1", "2", "25", "27", "34", "41")},
"keys": {k: sorted(next(r for r in recs if r.get("RECORD") == k)) for k in ("1", "2", "25", "27", "34", "41")},
"magic": open(os.path.join(folder, d), "rb").read(8).hex(),
"mcu": mcu[0]["LIBREFERENCE"], "mcu_parts_drawn": len(mcu),
"mcu_description": mcu[0].get("COMPONENTDESCRIPTION", ""),
"designators": sorted(r.get("TEXT") for r in recs if r.get("RECORD") == "34")}
# ── выпуск для завода ──
oj = ini(os.path.join(folder, "Fabrication.OutJob"))
g = oj["OutputGroup1"]
outs = []
for i in range(1, 20):
t = g.get("OutputType%d" % i)
if t:
item = dict(p.split("=", 1) for p in g.get("Configuration%d_Item1" % i, "").split("|") if "=" in p)
outs.append({"type": t, "name": g.get("OutputName%d" % i), "settings": item})
out["fabrication"] = outs
json.dump(out, sys.stdout, ensure_ascii=False, indent=1)
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
Дальше: раздел 134 — другие программы и перенос плат между ними.