techmannih/nema34-mounted-stepper-controller
A 48 V USB‑C PD 3.1 EPR NEMA‑34 stepper controller PCB combining STM32/TMC5160 control, external MOSFET motor bridges, protected power and regenerative braking, USB data, telemetry, diagnostics, status LEDs, and buzzer alarms.
- Version
- 0.3.92
- License
- unset
- Stars
- 1
scripts/check-usb-reference.py
"""Check the USB bottom-layer trunks against native filled In2 ground copper."""
import argparse
import hashlib
import json
import math
from pathlib import Path
import pcbnew
import wx
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('board', type=Path)
parser.add_argument('output', type=Path)
args = parser.parse_args()
app = wx.App(False)
board = pcbnew.LoadBoard(str(args.board.resolve()))
nets = ['USB_DP', 'USB_DM', 'USB_DP_CONN', 'USB_DM_CONN']
xy = lambda p: (pcbnew.ToMM(p.x), pcbnew.ToMM(p.y))
tracks = list(board.GetTracks())
vias = [t for t in tracks if isinstance(t, pcbnew.PCB_VIA)]
ground_vias = [xy(v.GetPosition()) for v in vias if v.GetNetname() == 'GND']
planes = [z.GetFilledPolysList(pcbnew.In2_Cu) for z in board.Zones()
if not z.GetIsRuleArea() and z.GetNetname() == 'GND' and z.IsOnLayer(pcbnew.In2_Cu)]
errors, results = [], []
for net in nets:
transfers = [xy(v.GetPosition()) for v in vias if v.GetNetname() == net]
lengths, uncovered, samples = {}, [], 0
for t in tracks:
if isinstance(t, pcbnew.PCB_VIA) or t.GetNetname() != net:
continue
layer = board.GetLayerName(t.GetLayer())
a, b = xy(t.GetStart()), xy(t.GetEnd())
length = math.dist(a, b)
lengths[layer] = lengths.get(layer, 0) + length
if layer != 'B.Cu':
continue
for i in range(math.ceil(length / .05) + 1):
ratio = min(1, i * .05 / length) if length else 0
p = (a[0] + (b[0] - a[0]) * ratio, a[1] + (b[1] - a[1]) * ratio)
# A through signal via necessarily has an antipad in the ground plane.
if any(math.dist(p, v) <= .45 for v in transfers):
continue
samples += 1
point = pcbnew.VECTOR2I(pcbnew.FromMM(p[0]), pcbnew.FromMM(p[1]))
if not any(plane.Contains(point) for plane in planes):
uncovered.append([round(v, 4) for v in p])
distances = [min(math.dist(v, g) for g in ground_vias) for v in transfers]
if not lengths.get('B.Cu') or any(layer not in ['F.Cu', 'B.Cu'] for layer in lengths):
errors.append(f'{net}: expected bottom trunks and top fanouts only')
if uncovered:
errors.append(f'{net}: {len(uncovered)} samples lack In2 ground reference')
if not transfers or max(distances, default=0) > 2:
errors.append(f'{net}: a signal transition lacks ground stitching within 2 mm')
results.append(dict(net=net, length_mm={k: round(v, 3) for k, v in lengths.items()},
checked_samples=samples, uncovered_samples=uncovered,
maximum_ground_stitch_distance_mm=round(max(distances, default=0), 3)))
report = dict(board_sha256=hashlib.sha256(args.board.read_bytes()).hexdigest(),
checker_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
sample_pitch_mm=.05, signal_via_antipad_exclusion_radius_mm=.45,
maximum_ground_stitch_distance_mm=2, nets=results, errors=errors,
limitation='Geometric return-path check for the full-speed interface; not an impedance, eye-diagram or USB compliance measurement.')
args.output.write_text(json.dumps(report, indent=2) + '\n')
print(json.dumps(report, indent=2))
raise SystemExit(bool(errors))