Embedded HandbookПриложения
Все разделы › XVI. Печатная плата
STM32F103 · уровень средний · 19 мин чтения
Открыть в интерактивном справочнике

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 и сверловка.

Три настройки, на которых ошибаются:

Сверловка — 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.

Порядок работы:

  1. Пользователь выбирает Tools / Plugins / <имя>.
  2. DipTrace пишет plugin_exchange.xml во временную папку — что именно, задают ExpMode и фильтры в settings.xml.
  3. Запускает .exe с полным путём к этому файлу первым аргументом; рабочая папка — папка плагина.
  4. Ждёт, пока процесс завершится.
  5. Если файл изменился — читает его и вносит изменения в плату по ImpMode.

Плагин DipTrace: DipTrace пишет файл обмена, запускает плагин и ждёт; изменённый файл импортируется, нетронутый — нет

Пять правил этого договора определяют устройство плагина:


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>

Ссылки файла обмена: компонент U1 берёт корпус по строке PatternStyle, его площадка Pad Id 8 — вывод 9 корпуса, цепь задаёт NetId площадки, а список Pads у цепи лишь повторяет его

Правила формата, которые нужно знать до первой строки плагина:

И ловушка в самом начале: в 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 корпуса сначала отражается.

Площадка корпуса на плате: снизу X корпуса сначала отражается, затем поворот на угол компонента и сдвиг; (1; 0) при угле 90° и точке (10; 20) даёт (10; 21) сверху и (10; 19) снизу

Перед работой на настоящей плате плагин прогоняют на настоящем файле обмена: в комплекте для разработчиков есть 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 Итоги

Полные исходники

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.

Открыть в интерактивном справочнике