Skip to content

Thermal relief - #10

Open
Boernsman wants to merge 6 commits into
mainfrom
thermal-relief
Open

Boernsman wants to merge 6 commits into
mainfrom
thermal-relief

Conversation

@Boernsman

Copy link
Copy Markdown
Contributor

What this changes

Checks

  • scripts/lint.sh && scripts/test.sh pass.
  • New or changed behaviour has a test.
  • CHANGELOG.md has an entry under [Unreleased], if this is user-visible.

CONTRIBUTING.md asks two questions that CI cannot answer for you. Answer the
ones that apply, and delete the rest.

  • Touched anything under src/pinside/kicad/? The KiCad tests run in
    CI in a container, but a real KiCad install is the only place project
    output gets opened. Say which KiCad version you ran against.
  • Touched anything under src/pinside/firmware/templates/? CI compiles
    the host tests with the mock HAL and cross-compiles against the Pico SDK.
    Say whether you flashed it and what the fixture did.
  • Added or changed a target in targets.py? Name the datasheet and the
    table you checked the function map against. A wrong map produces a config
    that validates and does not work.
  • Changed a probe in pogo.py? Cite the supplier drawing. These numbers
    end up as drill sizes on a board somebody orders.

The thermal gap and bridge were KiCad's generic 0.5 mm, which assume a 2.54 mm
header on an uncrowded board. A fixture is denser than that, and a relief too
wide starves the pad it is relieving: KiCad wants two spokes and says
starved_thermal when it cannot get them.

The arithmetic is the whole of it. A spoke is copper that has to survive between
two neighbouring pads, and a void of gap around each one leaves free - 2 * gap,
so the gap is (free - bridge) / 2 -- floored at the design rules' own minimum
clearance, capped where KiCad's default sits, with the bridge taken from the net
class's track width because a spoke is a trace.

The connector counts even though pinside does not place it. A DD-78's rows are
2.08 mm apart on 1.6 mm pads, which is 0.48 mm of room for a 1 mm void, and a
direct fixture's board exists to have one soldered into it. Its dimensions are
in the catalogue, so the pour can suit a part that arrives later.

Found by routing rather than by reading: the 78-way example came back from
fastroute with three starved_thermal errors on exactly those ground ways, on a
board that had passed every check pinside had. It routes clean now -- 43
connections, nothing unrouted, DRC silent before and after -- which is the first
time that example has been clean end to end.

563 tests, lint and mypy clean, DRC silent on four other generated boards.
The rule in ARCHITECTURE.md named fixture_config.c and the roster assertions as
the generated surface. It had been wider than that for some time: the emitter
also writes fixture_config.h, CMakeLists.txt, test/run.sh, openrpc.json and the
README -- the config in C, as a build, as a contract and as prose. Eight of the
twelve templates are copied byte-for-byte, which is the half that was true and
is the half worth keeping, because a template is reviewed once and a generator
is re-reviewed every time its input changes.

Prose could not stop that drift, so the rule is enforced now. Every file in a
generated firmware project is either byte-identical to a template or named in a
list in the test, and a new generated file fails until somebody adds it -- which
is where the question "should this be generated at all?" gets asked out loud
rather than answered by omission. Verified by inventing a file: the guard names
it.

The KiCad side needed a different rule. It generates everything, because there
is no template for a board whose every coordinate comes from the DUT, so the
list itself is the reviewable artefact and it is pinned: a direct project with a
guide plate writes fifteen files, and the fifteen are named. That list has grown
by five this release -- 3D models, a connector footprint, a ground post -- each
of which should have been a decision, and from here each one is.

565 tests, lint and mypy clean.
A fixture carries what its carrier board brings out and nothing else.
Anything above that -- a 24 V loop supply, a 12 V driver rail -- is now
declared in `fixture.supplies` and arrives on a two-way inlet header
placed beside the ground post: pin 1 the rail, pin 2 its return, a
PWR_FLAG on each net because a connector pin is passive and the driver
is on the bench. No regulator is generated anywhere, and a test asserts
it: a power supply on a probe plate is one nobody has reviewed, feeding
a board whose whole purpose is to touch another one.

`isolated` is the half nothing can derive. A shared supply returns to
GND; an isolated one keeps its own net and pinside ties it to nothing,
which is the difference between a loop that floats and one quietly
referred to the DUT's ground through the fixture.

PF057 is `requires.supply_mv` finally meaning something at the fixture
level: a module asking for a rail the fixture does not carry, naming
what it does carry. Within ten per cent above and not merely something
bigger -- a 5 V output stage on the 24 V loop supply is not satisfied
by it.

What a carrier brings out was two tables disagreeing: `_wire_module`
knew 3V3 and GND, `power_pins` also had VBUS and VSYS and the schematic
left them spare. One table in modules.py answers both now, so +5V is a
rail the fixture has and wires. VSYS stays out: it is a power input,
anywhere from the regulator's floor to 5.5 V depending on what feeds it.

ERC and DRC are clean on a project carrying an isolated 24 V inlet and
a shared 12 V one, and on the demo fixture with its new +5V rail.
`pinside project --base` writes the third plate: the board on the DUT's
unprobed face, which is what the closing force lands on and the one
everybody has been cutting by hand.

Nothing on that face and it is the outline and the mounting holes --
the cheapest board a fab will quote. Something on that face and a
window is cut through the plate for every part that does not clear
`fixture.base_gap_mm`, the gap the standoffs on that side leave. A part
whose height nobody declared is cut for anyway: a part of unknown
height is not a part of no height, and a window costs a hole in a plate
while a wrong assumption costs the test run. Windows closer together
than a 2 mm web are merged, because a sliver of FR-4 between two
cutouts arrives broken and a bigger window cannot hold the board off
the pins.

Where the probes bow the board, a pillar. The probe plate already
stands itself up at the corners of the probe field the DUT's holes do
not reach; this asks the same question about the DUT, and places a post
only where at least 2 N of probes are pushing on one. Heaviest corner
first, recomputed after each placement: taking them in the order the
geometry listed put a pillar at each end of one cluster, the second
carrying nothing, which is a hole in a plate. A pillar holds the board
around itself like a screw does, so corners within its reach stop being
candidates. What each one ends up carrying is reported rather than
asserted -- 2 N is a rule of thumb and the note says so.

The windows go in the fixture frame, not the DUT's. `mirror = x` lays
the board face-down, so a part at the DUT's own (30, 30) is at
(width - 30, 30) once it is on the pins; cut at the file's coordinates
and the plate is a perfect mirror image of the one needed. That is the
mistake this exists to prevent, and it has a test in both settings.

PK007 refuses a base plate on a DUT probed from both faces. PK008 warns
where a window was clipped at the board edge: a window to the edge is a
notch rather than a closed shape, and routing one out is a decision
somebody makes rather than a shape pinside guesses at.

DRC reports nothing on a generated plate carrying a merged window, a
clipped one and a pillar.
The base plate's silkscreen read "1 props" on a plate with one pillar,
which is the kind of thing that gets ordered. It counts properly now.

The picture is the plate from a DUT with three parts on its unprobed
face: one window merged from a pair 6 mm apart, one clipped at the
board edge, two of the DUT's own mounting holes and one pillar where
the probe cluster has nothing holding it down.
The base was a board: an outline, the mounting holes, and a window
through it for every part on the DUT's unprobed face. That was the
wrong thing to make. A board cannot carry a pin that passes *through*
the DUT, and that pin is the point -- it is what the probe plate comes
down to find its own position.

So `--base` now writes an `.stl`. A nest the board drops into: corner
brackets around its outline, a seat under it, a pocket under every part
on that face that would otherwise hold it up, and a mandrel at each of
the DUT's own mounting holes that passes through the board and carries
on up through the plates' matching holes. Both coordinates come from
the same transform, so the plate cannot land anywhere but where the
probes meet their pads.

Three numbers shape it and all three are declared. `base_fit_mm` is the
clearance on every sliding surface and belongs to a printer rather than
a drawing -- an FDM pin is as fat as the flow is set to. `base_gap_mm`
is how far the board stands off the seat: nought lies it flat, which is
the stiffest arrangement and the one where every part on that face
needs a pocket. `mandrel_height_mm` is the gap, the board, a plate, the
probe's travel, and enough pin still engaged at the bottom of the
stroke.

Printing changes three rules. A pocket may run out to the side of the
part, because a printed well is open-sided as happily as closed -- the
board version held its windows off the edge only because an outline has
to be a closed curve, and a part overhanging the DUT's edge was never
properly cleared. Bosses exist only when a gap is declared: with the
board on the seat, the seat is the support. And a bracket that would
stand over a pocket is not placed.

The mesh is tested as a mesh: every edge shared by exactly two faces
and traversed once in each direction, no zero-area triangles, positive
enclosed volume, and the seat and every pocket floor measured off the
emitted STL. That found three defects while it was being written -- a
T-junction where a tall wall met a short one, an L-shaped bracket
fanned from its own centre so the triangulation webbed across the
notch, and two bracket legs sharing one edge instead of overlapping.

PK008 changes meaning with the part: it was a window clipped at the
board edge, and is now fewer than two mounting holes to put a mandrel
through. One pin lets the board swing, and the plate is then being
aligned by eye.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant