132. DipTrace: схема, плата и плагины
DipTrace — пакет из четырёх программ с общими библиотеками: схема, плата, редакторы компонентов и посадочных мест. Автоматизируют его плагинами: внешняя программа на любом языке получает от DipTrace файл обмена в XML, правит его и отдаёт обратно. Никакого API внутри процесса — только файл.
Здесь — путь платы в DipTrace от схемы до файлов для завода и плагин «Развязка»: он проверяет, что у каждого вывода питания микроконтроллера свой конденсатор не дальше 3 мм. Плагин проверен на плате из раздела 131: файл обмена собран из неё по спецификации, а положения площадок сверены с тем, что посчитал KiCad. На машине — DipTrace 5.3.0.3.
Как это называется в документации
Оригинал Перевод schematic capture ввод схемы PCB layout разводка платы pattern посадочное место — так в DipTrace называют footprint component компонент: обозначение на схеме вместе с посадочным местом net class класс цепей ratline неразведённое соединение, «резинка» real-time DRC проверка правил на ходу N/C drill, Excellon файл сверловки tented via переходное отверстие, закрытое маской plug-in, exchange file плагин, файл обмена back annotation обратная аннотация: изменения платы — в схему
132.1 Четыре программы и их файлы
| Программа | Что в ней | Файл |
|---|---|---|
| Schematic | схема | .dch |
| PCB Layout | плата | .dip |
| Component Editor | библиотека компонентов: обозначение и посадочное место | .eli |
| Pattern Editor | библиотека посадочных мест | .lib |
Все четыре программы — версии 5.3.0.3. Файлы схемы и платы двоичные: пример
платы из комплекта начинается с метки DTBOARD, схемы — с DTSCHEM. Хранить
их в git можно, сравнивать — нет. Текстовая форма — XML: у каждой программы
есть сохранение в XML, и ровно этот XML DipTrace пишет плагинам.
132.2 От схемы к плате
В схеме — File / Convert to PCB (Ctrl+B): схема открывается в PCB Layout, правила берутся из схемы или из любой другой платы. Детали сначала лежат кучей; Placement / Arrange Components раскладывает их у начала координат, дальше — расстановка руками или автоматическая.
Правила задают до разводки. Route / Net Classes — классы цепей: ширина дорожки, зазор, виды переходных отверстий. Каждая цепь принадлежит какому-то классу, по умолчанию — Default. Зазор между классами (Class to Class) важнее зазора внутри класса. Автотрассировщик — Route / Run Autorouter (Ctrl+F9) — классы учитывает.
Если схема изменилась после разводки, плату обновляют по схеме (Renew layout by schematic), а изменения, сделанные на плате, возвращают в схему обратной аннотацией (Back annotate).
132.3 Проверки
Три проверки — в меню Verification:
| Команда | Что проверяет |
|---|---|
| Check Design Rules (F9) | зазоры, размеры, дифференциальные пары — по правилам |
| Check Net Connectivity | оборванные и слитые цепи |
| Compare to Schematic | плата соответствует схеме |
Правила — Verification / Design Rules, четыре вкладки: Clearances, Sizes, Real-time DRC, Options. Проверка на ходу (Real-time DRC) рисует красный круг, как только дорожка подошла ближе допустимого. Но проверяет она только то, что включено в её настройках, — поэтому перед выпуском файлов полную проверку F9 запускают хотя бы раз.
132.4 Файлы для завода
File / Export / Gerber — каждый слой отдельным файлом. Кнопка Export All предлагает три варианта: архив только с Gerber, архив с Gerber и сверловкой, файлы по одному. Заводу нужны оба — Gerber и сверловка.
Три настройки, на которых ошибаются:
- в слое маски снять галочку Vias — переходные отверстия закрываются маской;
- в слое пасты — только площадки SMD;
- Pad/Via Holes не отмечать: отверстия в Gerber нужны, только если сверлить самим, завод берёт их из файла сверловки.
Сверловка — File / Export / N/C Drill в формате Excellon, кнопка Auto подбирает инструменты. Gerber X2 умеет класть отверстия в свои файлы, но большинство заводов по-прежнему ждут Excellon. Есть и ODB++ — File / Export / ODB++.
132.5 Лицензия
Бесплатная версия (Freeware) — только для некоммерческого использования: так прямо сказано в её лицензии. Для работы за деньги нужна одна из платных версий — Starter, Lite, Standard, Extended или Full. Ограничения версий по выводам и слоям в тексте лицензии не названы: их смотрят в описании версий перед покупкой.
132.6 Плагин: как DipTrace его запускает
Плагин — обычная программа .exe на любом языке. Лежит в своей папке внутри
папки DipTrace: для платы — Plugins\Pcb\<имя>\, для остальных программ —
Plugins\Schematic\, Plugins\CompEdit\, Plugins\PattEdit\. В папке —
сама программа и файл описания settings.xml.
Порядок работы:
- Пользователь выбирает Tools / Plugins / <имя>.
- DipTrace пишет
plugin_exchange.xmlво временную папку — что именно, задаютExpModeи фильтры вsettings.xml. - Запускает
.exeс полным путём к этому файлу первым аргументом; рабочая папка — папка плагина. - Ждёт, пока процесс завершится.
- Если файл изменился — читает его и вносит изменения в плату по
ImpMode.
Пять правил этого договора определяют устройство плагина:
- Код возврата не читается. Успех или отказ DipTrace определяет только по тому, изменён ли файл. Плагин, который ничего не нашёл или упал, файл не трогает.
- Канала для сообщений нет — ни консоли, ни файла ошибок. Всё, что плагин хочет сказать, — в свой журнал или своё окно.
- Тайм-аута нет. Зависший плагин держит DipTrace, пока пользователь не нажмёт Cancel; тогда процесс убивают и ничего не импортируют.
- Плагины ищутся один раз, при запуске программы. После установки
или правки
settings.xmlDipTrace перезапускают. - Пути — короткие и в кодовой странице Windows. Командная строка передаётся через старый ANSI-буфер примерно на 255 байт.
132.7 Файл обмена
Файл обмена — тот же XML, что DipTrace сохраняет в текстовом виде. У платы
корень <Source Type="DipTrace-PCB">, внутри две части: библиотека корпусов,
которыми пользуется плата, и сама плата <Board>. Вот вывод 9 микроконтроллера
на плате из раздела 131 — в корпусе, у компонента и в цепи:
<Source Type="DipTrace-PCB" Version="5.3.0.3" Units="mm">
<Library Type="DipTrace-ComponentLibrary" Name="Design cache" Units="mm">
<Library Type="DipTrace-PatternLibrary" Name="Design cache" Units="mm">
<Patterns>
<Pattern Id="6" PatternStyle="PatType6" Mounting="SMD">
<Name>LQFP-48_7x7mm_P0.5mm</Name>
<Pads>
<Pad Id="8" Style="PadT6" X="-4.1625" Y="-1.25" Angle="0" Side="Top">
<Number>9</Number>
</Pad>
</Pads>
</Pattern>
</Patterns>
</Library>
</Library>
<Board>
<Components>
<Component Id="12" PatternStyle="PatType6" X="24" Y="-15" Angle="0" Side="Top" Flip="N" HorzFlip="N" Group="-1" Locked="N" Selected="N">
<RefDes>U1</RefDes>
<Pads>
<Pad Id="8" NetId="0" InternalConnection="-1" />
</Pads>
</Component>
</Components>
<Nets>
<Net Id="0" NetClass="0" RouteMode="Ratlines" Locked="N">
<Name>+3V3</Name>
<Pads>
<Item Comp="12" Pad="8" />
</Pads>
</Net>
</Nets>
</Board>
</Source>
Правила формата, которые нужно знать до первой строки плагина:
- Ссылки — по номерам
Id: компонент ссылается на корпус черезPatternStyle, площадка — на цепь черезNetId. Цепь площадки задаёт именноNetId; список<Pads>у цепи — лишь его отражение. - Удалить объект — не вычеркнуть узел, а поставить
Enabled="N": узла, которого нет в файле, DipTrace просто не касается. - Новому объекту
Idне придумывают — его назначит DipTrace; ставятSelected="Y". - Длины — в единицах из
Units(mm,inchилиmil), разделитель — точка, углы — в радианах против часовой стрелки. - Параметры отрезка дорожки (слой, ширина, переходное отверстие) записаны на его втором конце.
- Виды переходных отверстий и классы цепей связаны по месту в списке,
а не по
Id: переставить список — молча переназначить правила. - Площадки корпуса записаны от его центра. На плату их переносят так: у нижней стороны отражают X, затем поворачивают на угол компонента и сдвигают в его точку.
И ловушка в самом начале: в settings.xml тип пишется через подчёркивания —
DipTrace_Pcb_Plugin, а в файле обмена через дефис — DipTrace-PCB.
132.8 Плагин «Развязка»
Правило из раздела 130: у каждого вывода питания — свой конденсатор, вплотную. Плагин проверяет его на плате DipTrace: для каждого вывода питания микросхемы ищет ближайшую площадку конденсатора на той же цепи и сравнивает расстояние с пределом 3 мм.
Файл описания:
<?xml version="1.0" encoding="utf-8"?>
<Source Type="DipTrace_Pcb_Plugin" Name="Decoupling check" ExeFile="DecapCheck.exe"
Hint="Power pin to its capacitor distance">
<Settings>
<ExpMode>Partial</ExpMode>
<ImpMode>Edit</ImpMode>
<Comp>All</Comp>
<Net>All</Net>
<Shape>Selected</Shape>
<Plane>None</Plane>
<Table>None</Table>
<Dim>None</Dim>
<Diff>None</Diff>
<Board>None</Board>
<Trace>None</Trace>
<Stamp>No</Stamp>
</Settings>
</Source>
Для анализа нужны компоненты и цепи целиком: Comp и Net — All. Назад
принимаются только выделенные графические объекты — Shape в режиме
Selected, и метки плагина выделены. Строки — все двенадцать, какие положены
плате: пропущенная строка не означает «по умолчанию».
Площадка корпуса → плата:
def board_point(comp, px, py):
"""Площадка корпуса → плата: у нижней стороны X корпуса отражается, затем поворот и сдвиг."""
if comp["side"] == "Bottom":
px = -px
a = comp["angle"]
return (comp["x"] + px * math.cos(a) - py * math.sin(a),
comp["y"] + px * math.sin(a) + py * math.cos(a))
Ближайший конденсатор к каждому выводу питания:
for p in ic["pads"]:
if not POWER.match(p["net"]) or on_net[p["net"]] < 2:
continue # не питание или вывод ни с чем не соединён
best = None
for c in caps:
for q in c["pads"]:
if q["net"] == p["net"]:
d = math.hypot(q["x"] - p["x"], q["y"] - p["y"])
if best is None or d < best[0]:
best = (d, c["ref"], q)
rows.append((ic["ref"], p, best))
Микросхема — корпус с наибольшим числом выводов; питание — цепи с именами
вида +3V3, VDD…, VCC…; вывод, цепь которого из одной площадки, ни с чем
не соединён и пропускается. Конденсатор, уже отданный более близкому выводу,
второму не засчитывается.
Результат — узкий. Компоненты и цепи нужны были только для анализа, назад уходят одни метки:
board = root.find("./Board")
for tag in ("Components", "Nets", "Shapes"):
for e in board.findall(tag):
board.remove(e)
Метка — линия от вывода до конденсатора и надпись на сборочном слое
Top Assy, не на меди и не на шелкографии.
Нарушений нет — файл не трогают. Есть — пишут во временный файл рядом, перечитывают его и заменяют одним переименованием: оборванная запись не оставит DipTrace половину файла.
if not bad:
return False # файл не тронут — DipTrace ничего не импортирует
mark(root, units, bad)
atomic_write(tree, path)
return True
fd, tmp = tempfile.mkstemp(prefix="plugin_exchange.", suffix=".tmp", dir=path.parent)
os.close(fd)
tmp = Path(tmp)
try:
tree.write(tmp, encoding="utf-8", xml_declaration=True)
ET.parse(tmp) # перечитать до замены
os.replace(tmp, path)
finally:
tmp.unlink(missing_ok=True)
Отчёт — в журнал %LOCALAPPDATA%\DipTrace\PluginLogs\DecapCheck.log,
по строке на вывод питания.
132.9 Проверка без DipTrace
DipTrace запускает только .exe. Плагин на Python собирают в него программой
PyInstaller — сценарий build_exe.ps1 лежит в комплекте для разработчиков
(Docs\plugin_sdk). Здесь .exe не собирался: проверена логика — на файлах
обмена, собранных по спецификации из платы раздела 131.
Сначала — перенос координат. Для всех 80 площадок 15 корпусов плагин посчитал положение на плате сам — из точки и угла компонента и смещения площадки в корпусе — и сравнил с тем, что посчитал KiCad. Расхождение меньше 0,00001 мм: координаты площадок кратны 0,0001 мм, а детали повёрнуты на 0 или 90°, так что при записи четырёх знаков округлять нечего. При произвольных углах ошибка доходила бы до 0,00005 мм — половины последнего знака. (У KiCad ось Y смотрит вниз, у файла обмена — вверх; при сборке файла y заменён на −y.)
Потом — шесть файлов обмена:
| Файл обмена | Что сказал плагин | Файл после плагина |
|---|---|---|
| плата 131, миллиметры | 4 вывода питания, у каждого свой конденсатор в 2,025 мм | не тронут |
| та же плата в милах | те же 2,025 мм | не тронут |
| та же плата в дюймах | 2,024–2,027 мм | не тронут |
| C4 отодвинут на 4 мм | вывод 9: 6,025 мм — одно нарушение | линия и надпись |
C3 удалён, Enabled="N" |
вывод 48: ближайший — C4, 6,412 мм | линия и надпись |
| корпуса нет в библиотеке | код 1, причина — в журнале | не тронут |
В изменённом файле нет ни компонентов, ни цепей, а библиотека осталась прежней — ровно тот узкий результат, который описан выше.
Дюймы — отдельный урок: четыре знака в дюймах — это шаг 2,54 мкм, и расстояния плывут уже в третьем знаке миллиметра. Для развязки неважно, для зазоров в сотые доли миллиметра — важно.
Нижней стороны на плате 131 нет, и с самой программой формула для неё не
сверена. Взята она из комплекта для разработчиков: X корпуса отражают только
по Side="Bottom", а Flip и HorzFlip второй раз не применяют — они уже
учтены в корпусе, который DipTrace кладёт в файл обмена. Числа руками формулу
лишь повторяют: площадка корпуса в точке (1; 0), компонент в (10; 20),
угол 90°. Сверху площадка оказывается в (10; 21), снизу — в (10; 19): X корпуса
сначала отражается.
Перед работой на настоящей плате плагин прогоняют на настоящем файле обмена:
в комплекте для разработчиков есть capture_exchange.py — он сохраняет файл,
который DipTrace передал плагину, ничего в нём не меняя.
Для нижней стороны это и есть проверка: плата с одной деталью снизу под
углом 90°, файл обмена снят capture_exchange.py, и координаты площадок,
которые насчитал плагин, сравнивают с их положением в Gerber нижней меди.
До такой проверки результат для деталей снизу — под вопросом.
132.10 Грабли
| Симптом | Причина |
|---|---|
| плагин отработал, а плата не изменилась | файл обмена не перезаписан: DipTrace смотрит только на его изменение |
| объект «удалён» в XML, а на плате остался | узел вычеркнули; удаляют Enabled="N" |
| плагина нет в меню | тип в settings.xml записан через дефис, а нужно DipTrace_Pcb_Plugin; или DipTrace не перезапущен |
| DipTrace «завис» после запуска плагина | плагин ждёт ввода с консоли, которой у него нет, — так делает Console.ReadLine() в примере из комплекта |
| переставили виды переходных — поменялись отверстия по всей плате | ссылки на виды переходных и классы цепей — по месту в списке |
| расстояния в дюймовом файле не сходятся в третьем знаке | четыре знака в дюймах — шаг 2,54 мкм |
| завод просит сверловку | отверстия — отдельным файлом N/C Drill в формате Excellon |
| коммерческий заказ сделан на Freeware | бесплатная версия — только для некоммерческого использования |
132.11 Итоги
- Четыре программы DipTrace 5.3.0.3; схема и плата — двоичные файлы, текстовая форма — XML.
- Путь платы: Convert to PCB → классы цепей → разводка → F9 → Gerber по слою в файле и сверловка N/C Drill.
- Плагин —
.exeиsettings.xml; разговор с DipTrace — один файл, код возврата не читается. - Удаляют
Enabled="N"; новые объекты — безId, сSelected="Y". - «Развязка» на плате 131: 4 вывода питания по 2,025 мм; 80 площадок сошлись с KiCad.
- Freeware — только некоммерческое использование.
Полные исходники
decap_check.py — плагин PCB Layout «Развязка»: свой конденсатор у каждого вывода питания.
# -*- coding: utf-8 -*-
"""Плагин PCB Layout DipTrace «Развязка»: у каждого вывода питания микросхемы — свой конденсатор
не дальше LIMIT_MM. Протокол SDK: DipTrace передаёт путь к файлу обмена первым аргументом и после
выхода читает этот же файл. Нарушений нет — файл не трогаем; есть — дописываем метки на сборочный
слой и заменяем файл целиком одним переименованием. Отчёт — в журнал: другого канала назад нет."""
import math, os, re, sys, tempfile, traceback
import xml.etree.ElementTree as ET
from pathlib import Path
NAME, VERSION = "DecapCheck", "1.0"
LIMIT_MM = 3.0
TO_MM = {"mm": 1.0, "inch": 25.4, "mil": 0.0254}
DIGITS = {"mm": 4, "inch": 4, "mil": 2}
POWER = re.compile(r"^(\+?\d+V\d*|VDD\w*|VCC\w*|VBAT\w*|VREF\w*)$", re.I)
def real(el, attr, default=None):
raw = el.get(attr)
if raw is None and default is not None:
return default
try:
v = float(raw.replace(",", "."))
except (AttributeError, ValueError):
raise ValueError("%s: %s=%r" % (el.tag, attr, raw))
if not math.isfinite(v):
raise ValueError("%s: %s=%r" % (el.tag, attr, raw))
return v
def fmt(v, units):
d = DIGITS[units]
r = round(v / TO_MM[units], d) or 0.0
return ("%.*f" % (d, r)).rstrip("0").rstrip(".") or "0"
def board_point(comp, px, py):
"""Площадка корпуса → плата: у нижней стороны X корпуса отражается, затем поворот и сдвиг."""
if comp["side"] == "Bottom":
px = -px
a = comp["angle"]
return (comp["x"] + px * math.cos(a) - py * math.sin(a),
comp["y"] + px * math.sin(a) + py * math.cos(a))
def read(root):
units = root.get("Units")
if root.get("Type") != "DipTrace-PCB" or units not in TO_MM:
raise ValueError("не плата DipTrace или неизвестные единицы: %r, %r" % (root.get("Type"), units))
k = TO_MM[units]
patterns = {}
for pat in root.iterfind("./Library//Library[@Type='DipTrace-PatternLibrary']/Patterns/Pattern"):
patterns[pat.get("PatternStyle")] = {
p.get("Id"): (real(p, "X") * k, real(p, "Y") * k, p.findtext("Number") or p.get("Id"))
for p in pat.iterfind("./Pads/Pad")}
nets = {n.get("Id"): n.findtext("Name") or "" for n in root.iterfind("./Board/Nets/Net")
if n.get("Enabled") != "N"}
comps = []
for c in root.iterfind("./Board/Components/Component"):
if c.get("Enabled") == "N":
continue
ref, style = c.findtext("RefDes") or "?", c.get("PatternStyle")
if style not in patterns:
raise ValueError("%s: корпуса %s нет в библиотеке файла" % (ref, style))
comp = {"ref": ref, "x": real(c, "X") * k, "y": real(c, "Y") * k,
"angle": real(c, "Angle", 0.0), "side": c.get("Side", "Top"), "pads": []}
for p in c.iterfind("./Pads/Pad"):
if p.get("Id") not in patterns[style]:
raise ValueError("%s: площадки %s нет в корпусе %s" % (ref, p.get("Id"), style))
px, py, num = patterns[style][p.get("Id")]
bx, by = board_point(comp, px, py)
comp["pads"].append({"num": num, "net": nets.get(p.get("NetId"), ""), "x": bx, "y": by})
comps.append(comp)
return units, comps
def check(comps):
"""Вывод питания микросхемы → ближайшая площадка конденсатора на той же цепи."""
ic = max(comps, key=lambda c: len(c["pads"])) # микросхема — корпус с наибольшим числом выводов
on_net = {}
for c in comps:
for p in c["pads"]:
on_net[p["net"]] = on_net.get(p["net"], 0) + 1
caps = [c for c in comps if c["ref"].upper().startswith("C")]
rows = []
for p in ic["pads"]:
if not POWER.match(p["net"]) or on_net[p["net"]] < 2:
continue # не питание или вывод ни с чем не соединён
best = None
for c in caps:
for q in c["pads"]:
if q["net"] == p["net"]:
d = math.hypot(q["x"] - p["x"], q["y"] - p["y"])
if best is None or d < best[0]:
best = (d, c["ref"], q)
rows.append((ic["ref"], p, best))
bad, taken = [], {}
for row in sorted((r for r in rows if r[2]), key=lambda r: r[2][0]):
taken.setdefault(row[2][1], []).append(row)
for ref, p, best in rows:
if best is None:
bad.append((ref, p, best, "no cap"))
elif best[0] > LIMIT_MM:
bad.append((ref, p, best, "far"))
elif taken[best[1]][0][1] is not p: # конденсатор уже отдан более близкому выводу
bad.append((ref, p, best, "shared"))
return rows, bad
def mark(root, units, bad):
"""Узкий результат для ImpMode=Edit: только новые метки; компоненты и цепи были нужны для анализа."""
board = root.find("./Board")
for tag in ("Components", "Nets", "Shapes"):
for e in board.findall(tag):
board.remove(e)
shapes = ET.SubElement(board, "Shapes")
for ref, p, best, why in bad:
text = "%s.%s %s" % (ref, p["num"], why)
if best:
q = best[2]
text += " %s %.3fmm" % (best[1], best[0])
s = ET.SubElement(shapes, "Shape", Type="Line", Layer="Top Assy", LineWidth=fmt(0.2, units), Selected="Y")
pts = ET.SubElement(s, "Points")
ET.SubElement(pts, "Point", X=fmt(p["x"], units), Y=fmt(p["y"], units))
ET.SubElement(pts, "Point", X=fmt(q["x"], units), Y=fmt(q["y"], units))
t = ET.SubElement(shapes, "Shape", Type="Text", Layer="Top Assy", Selected="Y")
ET.SubElement(ET.SubElement(t, "Points"), "Point", X=fmt(p["x"], units), Y=fmt(p["y"], units))
ET.SubElement(ET.SubElement(t, "TextLines"), "TextLine").text = text
def write_log(text):
for base in (os.environ.get("LOCALAPPDATA"), tempfile.gettempdir()):
if not base:
continue
try:
folder = Path(base) / "DipTrace" / "PluginLogs"
folder.mkdir(parents=True, exist_ok=True)
(folder / (NAME + ".log")).write_text(text, encoding="utf-8")
return
except OSError:
continue
def atomic_write(tree, path):
fd, tmp = tempfile.mkstemp(prefix="plugin_exchange.", suffix=".tmp", dir=path.parent)
os.close(fd)
tmp = Path(tmp)
try:
tree.write(tmp, encoding="utf-8", xml_declaration=True)
ET.parse(tmp) # перечитать до замены
os.replace(tmp, path)
finally:
tmp.unlink(missing_ok=True)
def run(path):
tree = ET.parse(path)
root = tree.getroot()
units, comps = read(root)
rows, bad = check(comps)
log = ["%s %s, limit %.2f mm" % (NAME, VERSION, LIMIT_MM)]
for ref, p, best in rows:
log.append("%s.%s %s -> %s" % (ref, p["num"], p["net"],
"%s %.3f mm" % (best[1], best[0]) if best else "none"))
log.append("violations: %d" % len(bad))
write_log("\n".join(log) + "\n")
if not bad:
return False # файл не тронут — DipTrace ничего не импортирует
mark(root, units, bad)
atomic_write(tree, path)
return True
def main(argv):
if len(argv) != 2:
write_log("%s %s: expected one exchange-file argument\n" % (NAME, VERSION))
return 2
path = Path(argv[1]).resolve()
try:
run(path)
return 0
except Exception:
write_log("%s %s\n%s\n\n%s" % (NAME, VERSION, path, traceback.format_exc()))
return 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Дальше: раздел 132а «DipTrace: свои корпуса и компоненты» — посадочное место, символ, 3D-модель и имена корпусов по IPC-7351.