Hardware architecture
Power Manifold is a six-port managed USB Power Delivery (USB-PD) power supply. Six charger blades and a management card plug into a backplane. Each blade runs its own USB-C port. A Raspberry Pi RP2350 on the management card supervises all six through an I2C multiplexer, divides the chassis power budget between them by priority, drives the status lights, and serves the network interfaces.
The user guide and integrations pages are the reference for every operator-facing surface. Controller internals and the charger blade firmware pages cover the firmware in more depth.
1. Overview
Section titled “1. Overview”
- Measurement at the point of load: Every blade meters its own port and reports numbers over I2C. No analog sense line crosses the backplane. The blade’s STM32G071 measures VBUS, the port current and three temperatures and serves them from a register file.
- One I2C segment per blade: An 8-channel I2C switch (TCA9548A) on the backplane gives every blade its own bus segment. This isolates the trace capacitance and lets identical blades share one address (0x3A). Six channels serve the blades; two are spare.
- A fixed-function controller on each blade: The blade’s STM32G071 runs ST’s USB-PD stack over a TPS55288 buck-boost converter and a TCPP02-M18 port protector. An over-voltage comparator and an over-current trip open the port’s VBUS switch independently of the firmware. The blade MCU carries no network, settings or user interface; the management controller configures it over the backplane.
- Central supervision: A single dual-core RP2350 supervises all six ports, enforces the chassis power budget and drives the status lights. The production controller is the
hardware/controllercard: an RP2350A with a WIZnet W6100 wired-Ethernet controller and a Raspberry Pi RM2 radio (§4.3). For development, a Raspberry Pi Pico 2 W can sit in the same slot on a breakout carrier (§4.2).- Status lights on a separate strip: The status lights are one serial chain of addressable LEDs on a strip behind the faceplate, fed from the backplane. Lights stay with their slots whether or not a blade is seated, and no LED data line runs through a blade’s card edge.
2. Component selection
Section titled “2. Component selection”| Subsystem / Function | IC / Part | Location | Key Features & Justification |
|---|---|---|---|
| PD Controller & Supervisor | ST STM32G071C8 | Charger Blade (x6) | Cortex-M0+ with the UCPD PD PHY, running ST’s USB-PD stack as the port’s policy engine plus the blade’s supervisor, power sequencing and the backplane register file (I2C target at 0x3A, map). Runs from the slot’s 5 V, so it answers with EN low; its watchdog restarts a stuck port. |
| Buck-Boost Converter | TI TPS55288 | Charger Blade (x6) | 0.8–21 V output in 20 mV steps and a current limit in 50 mA steps over the blade’s private I2C. The slot’s EN line is its EN/UVLO pin, so EN low is shutdown whatever the firmware does. Forced PWM while the output has to come down. |
| Port Protector | ST TCPP02-M18 + TI TLV7022 | Charger Blade (x6) | CC over-voltage/ESD protection, VCONN switch, VBUS N-FET gate driver with a fixed-threshold over-current trip (≈6 A across 7 mΩ), VBUS discharge and a current monitor for the MCU’s ADC. The dual comparator opens the switch above 1.2 × the contract voltage (DAC-set) or a fixed 23 V, independently of the firmware; a blank or crashed MCU leaves the switch open. |
| I2C Multiplexer | TI TCA9548A | Backplane | 8-channel bidirectional I2C switch with hardware reset (MUX_RST#). Isolates bus capacitance and allows identical I2C addresses on all blades. Channel N carries slot N+1; channels 6–7 spare. 4.7 kΩ pull-up pairs per downstream segment. |
| GPIO Expander | TI TCA9539 | Backplane | 16-bit I2C GPIO expander with interrupt output and hardware reset (EXP_RST#). P00–P05 = blade EN1–6 (active high), P06 = fan switch, P10–P15 = PRES#1–6 (active low); P07/P16/P17 grounded spares. All pins through 330 Ω series networks. |
| Main Supervisor MCU | Raspberry Pi RP2350 | Management slot | Dual Cortex-M33 @ 150 MHz, 520 KB SRAM, PIO state machines. Production: RP2350A on the controller card (§4.3). Development: Pico 2 W (adds CYW43439 WiFi/BT) on the pcie-breakout carrier (§4.2). |
| Ethernet Controller | WIZnet W6100-L | Controller card (§4.3) | Hardwired dual-stack IPv4/IPv6 TCP/IP + 10/100 MAC/PHY over SPI, into a MagJack whose link and activity LEDs it drives directly. SPI0 GP16–19 + RSTn GP20 + INTn GP21, matching WIZnet’s EVB-Pico2 boards. |
| Radio Module | Raspberry Pi RM2 | Controller card (§4.3) | CYW43439, the same silicon as the Pico 2 W, on the same GPIOs (GP23 WL_ON/BT_ON, GP24 data, GP25 CS, GP29 clock), so one WiFi stack and Improv BLE provisioning serve both boards. |
| Status Lights | WS2812C-2020-V6 | Status strip (x7) | Addressable RGB LEDs in a single serial chain behind the faceplate: D1 is the chassis light, D2–D7 are ports 1–6. The strip plugs into the backplane at J9 (to its J1), which carries the backplane’s 5 V rail and LED_DATA from the management socket through the 220 Ω series resistor R1; DIN comes from the controller’s 3.3 V GPIO. |
| Backplane Power Path | LM74800-Q1 + 2 × BSC014N06NS, 40 A SMD fuse (Reomax STS2400, 63 V DC), SMCJ33CA | Backplane | Back-to-back FETs in common drain behind the fuse at the XT60 input. The first is the ideal diode for reverse-polarity and reverse-current blocking. The second is a load switch that cuts the bus above ≈34 V (100 kΩ / 3.74 kΩ on OV, fed through the SW disconnect so the ladder draws nothing in shutdown) and below ≈17.7 V (100 kΩ / 6.81 kΩ on EN/UVLO from the common-drain mid-point, re-closing at ≈19.3 V, §7.4), and ramps the bus up at power-on (1 kΩ + 220 nF on HGATE, ≈0.5 A inrush). TVS-clamped (§7.4). The clamp’s 33 V stand-off and the ≈34 V cut-off set the bus ceiling. The cut-off disconnects the bus; it does not clamp it. Everything ahead of the second FET (U2, Q1/Q2, the OV ladder, the raw-side capacitors) is protected by its own 60–100 V ratings. |
| Logic Rail | TI TPS16416 eFuse + Diodes AP64352 | Backplane | VIN → TPS16416 eFuse (0.40 A active current limit, ≈11.2 W ceiling at 28 V, UL 2367 recognised and IEC 62368-1 CB certified) → 5 V buck (5.6 µH, 2 × 22 µF out) feeding the mux and expander, every pull-up, the LED chain, the fan and the 5 V fingers of all seven slots. The eFuse makes the buck and the whole 5 V domain a PS1 circuit (§7.4). The buck’s enable divider (100 kΩ / 7.5 kΩ from the eFuse output: on ≈16.8 V, off ≈15.1 V) is the low-bus backstop behind the LM74800’s cut-out. The blades run their logic from it and the controller card regulates its own 3.3 V from it; there is no 3.3 V rail on the backplane. |
| Fan Switch | AO3400A | Backplane | Low-side MOSFET driven from expander P06 (100 kΩ gate pull-down, SS34 flyback across the fan). The fan runs from the 5 V rail, which keeps the 30 V part off the bus. |
3. Blade card edge (slots 1–6)
Section titled “3. Blade card edge (slots 1–6)”The standard 36-pin PCIe x1 card-edge form factor provides high current capacity, mechanical keying, and staggered presence-detection pins (a seating check; the system is not serviced live, see §7.5). The high-current DC rails (pins 1–11) and the low-voltage digital signals (pins 12–18) sit on opposite sides of the key notch.
| Pin | Side A (Component Side) | Side B (Solder Side) | Pin | Functional Description & Electrical Requirements |
|---|---|---|---|---|
| A1 | PRSNT1# (Short Pin) | PGND | B1 | Presence sense loop lead (A1, short pin; see §7.5). B1 joins the main power return and serves as the pin-1-end sacrificial ground. |
| A2 | VIN | PGND | B2 | Main DC input (24 V nominal, +20 %/−15 %; the hardware tolerates 32 V, with the clamp and the LM74800 cut-off setting that ceiling under the TPS55288’s 36 V input limit) and high-current power return; fused again on the blade. The opposed pairs keep the inductive loop area small. Total 6-pin continuous capacity: ~6.6 A; worst-case draw ~5.3 A at 100 W from a 20 V bus. |
| A3 | VIN | PGND | B3 | |
| A4 | VIN | PGND | B4 | |
| A5 | VIN | PGND | B5 | |
| A6 | VIN | PGND | B6 | |
| A7 | VIN | PGND | B7 | |
| A8 | PGND | PGND | B8 | Power ground plane extension and thermal dissipation fingers. |
| A9 | PGND | PGND | B9 | (B9 carries EXP_RST# at the management slot only; grounded at charger slots.) |
| A10 | PGND | GND | B10 | Power-to-signal ground transition boundary. (B10 carries LED_DATA at the management slot only.) |
| A11 | GND (Shield Guard) | GND (Shield Guard) | B11 | Guard pins flanking the key notch. |
| — MECHANICAL KEY / POLARIZING NOTCH — | ||||
| A12 | 5V | 5V | B12 | Regulated 5 V logic supply from the backplane’s AP64352 buck, powering the STM32 and the TCPP02 through an LP2985 3.3 V regulator. The blade carries no I2C or ALERT# pull-ups: the segment I2C pull-ups are on the backplane, the ALERT# pull-up on the controller. |
| A13 | GND | GND | B13 | Dedicated quiet logic ground reference plane. |
| A14 | SCL | SDA | B14 | I2C clock (A14) and data (B14) from the TCA9548A mux channel. SDA/SCL pull-ups to the 5 V rail are on the backplane (one 4.7 kΩ pair per downstream segment). |
| A15 | GND (Shield Guard) | ALERT# | B15 | Active-low open-drain line, held low by the STM32 while a fault is latched, wire-OR’d across all blades into GLOBAL_ALERT#; shielded by A15. Neither the blades nor the backplane pull it up; the controller does. |
| A16 | GND | EN | B16 | Hardware enable driven by the backplane GPIO expander; a 100 kΩ pull-down on the blade holds it off. It is the TPS55288’s EN/UVLO, while the STM32 stays up on the 5 V finger and reports the line’s state. |
| A17 | GND (Shield Guard) | PRSNT2# (Short Pin) | B17 | Presence sense loop return (B17). The shorter pin asserts PRSNT# only at full seating. |
| A18 | GND (End Guard) | GND (End Guard) | B18 | Outer edge ESD guard and termination reference. |
3.1. Slot numbering
Section titled “3.1. Slot numbering”Ports are numbered 1–6 by the backplane silkscreen, and every per-slot resource follows that number: the firmware’s port index n−1 selects TCA9548A channel n−1, drives EN from expander pin P0(n−1), reads PRSNT# on P1(n−1) and renders pixel n of the light chain, whose pixel 0 is the chassis light ahead of port 1. The socket reference designators do not run in slot order, hence the table.
| Slot | Socket | I²C segment | Mux channel (pins) | EN | PRSNT# | Pixel | Position |
|---|---|---|---|---|---|---|---|
| 1 | J3 | SCL1/SDA1 | 0 (SC0/SD0) | P00 | P10 | D2 (after the chassis light D1) | leftmost, next to the management socket J2 |
| 2 | J5 | SCL2/SDA2 | 1 | P01 | P11 | D3 | |
| 3 | J7 | SCL3/SDA3 | 2 | P02 | P12 | D4 | |
| 4 | J4 | SCL4/SDA4 | 3 | P03 | P13 | D5 | |
| 5 | J6 | SCL5/SDA5 | 4 | P04 | P14 | D6 | |
| 6 | J8 | SCL6/SDA6 | 5 | P05 | P15 | D7 (last) | rightmost, at the fuse and XT60 end |
EN and PRSNT# reach the expander through 330 Ω arrays (RN4/RN5 and RN6/RN7) whose elements are pinned 1–8, 2–7, 3–6, 4–5; the fan sits on P06 through RN5’s third element. Seen from the front, the backplane has the management socket at the left, the six charger sockets 17.75 mm apart to its right, and the power input at the far right. Facing the faceplate, port 1 is on the left and port 6 on the right. The status strip puts each port’s light above it and the chassis light to the left of port 1. The power-up sweep runs left to right, chassis light first.
4. Management slot
Section titled “4. Management slot”The controller rides a card in a seventh card-edge socket on the backplane, beside the six charger slots. There are no internal cables, the card uses the same guides as the blades, and the connector family is the same.
4.1. Management slot pinout
Section titled “4.1. Management slot pinout”The management socket reuses the blade connector and pin geography: power half before the key notch, logic half after it. Management-only signals occupy pins that are grounded at the charger slots, so the two socket types stay drop-in compatible at the PCB level.
| Pin | Side A (Component Side) | Side B (Solder Side) | Pin | Management Slot Functionality |
|---|---|---|---|---|
| A1 | GND | GND | B1 | No presence circuit on the management slot (see §7.5). |
| A2–A7 | VIN | PGND | B2–B7 | Main DC bus, available to the management card for its own regulation if needed; neither controller board uses it. |
| A8–A9 | PGND | PGND / EXP_RST# | B8–B9 | B9 = TCA9539 hardware reset line, 10 kΩ to the 5 V rail on the backplane; the controller pulls it low open-drain. |
| A10 | PGND | LED_DATA | B10 | Single-wire WS2812C chain data into the backplane (220 Ω series). |
| A11 | GND (Shield Guard) | GND (Shield Guard) | B11 | Notch guard ground shield. |
| — MECHANICAL KEY / POLARIZING NOTCH — | ||||
| A12 | 5V | 5V | B12 | Backplane-sourced 5 V (AP64352). The controller card makes its own 3.3 V from it (§4.3); the dev carrier leaves it unused. |
| A13 | GND | GND | B13 | Quiet digital ground plane reference. |
| A14 | SCL | SDA | B14 | Upstream I2C bus to the TCA9548A mux and TCA9539 expander. |
| A15 | EXP_INT# | GLOBAL_ALERT# | B15 | Expander interrupt (blade insertion/removal) and the wire-OR’d blade ALERT# line. Both open-drain with no backplane pull-up; the controller pulls them up to its 3.3 V. |
| A16 | GND | MUX_RST# | B16 | TCA9548A hardware reset, which clears a hung downstream I2C segment without a power cycle. 10 kΩ to the 5 V rail on the backplane; the controller pulls it low open-drain. |
| A17 | GND | GND | B17 | Grounded (no presence loop). |
| A18 | GND (End Guard) | GND (End Guard) | B18 | Outer edge ESD ground guard. |
Cross-insertion: the sockets are mechanically identical. A charger blade seated in the management slot stays dark, because its port never arms until a controller has configured it, and nothing on the management slot’s bus will.
4.2. Development carrier: pcie-breakout + Pico 2 W
Section titled “4.2. Development carrier: pcie-breakout + Pico 2 W”hardware/pcie-breakout is a passive adapter from the management-slot card edge to a 2.54 mm 1x10 header, wired point-to-point to a Raspberry Pi Pico 2 W per the firmware’s GPIO map for that board:
| Header Pin | Signal | Pico 2 W GPIO |
|---|---|---|
| 1 | VIN | — (unused) |
| 2 | 5V | — (unused; the Pico is USB-powered) |
| 3 | GND | GND |
| 4 | SCL | GP5 (I2C0) |
| 5 | SDA | GP4 (I2C0) |
| 6 | EXP_INT# | GP6 |
| 7 | EXP_RST# | GP8 |
| 8 | MUX_RST# | GP7 |
| 9 | LED_DATA | GP2 (PIO) |
| 10 | GLOBAL_ALERT# | GP3 |
GP16–21 stay reserved for the wired-Ethernet path (W6100 per the EVB-Pico2 mapping), matching the controller card.
4.3. Controller card: hardware/controller
Section titled “4.3. Controller card: hardware/controller”hardware/controller is the production management card. It carries an RP2350A (QFN-60, 12 MHz crystal) with the flash described below; a WIZnet W6100-L on SPI0 (GP16–19, RSTn GP20, INTn GP21, the EVB-Pico2 map) with its own 25 MHz crystal and a Hanrun HR913550A MagJack whose link and activity LEDs the W6100 drives directly; and a Raspberry Pi RM2 radio module on the Pico 2 W’s CYW43439 pin map (GP23 WL_ON/BT_ON, GP24 data, GP25 CS, GP29 clock). An AP7361C LDO makes the card’s 3.3 V from the 5 V rail, which the slot fingers feed in the chassis and the unpopulated USB header feeds on the bench. SWD is on a 3-pin JST-SH header; BOOTSEL and RUN are push buttons; a 3-pin JST SH header (J5) carries a 3.3 V UART (UART0 on GP12/GP13, 1 kΩ series) for a Mean Well LAD-xxxU UPS supply, whose serial ground is its V−, common with the chassis VIN−.
The backplane signals land on GP1 (EXP_RST#) and GP8 (MUX_RST#) through 2N7002 open-drain drivers working against the backplane’s 10 kΩ pull-ups, GP2 (LED_DATA), GP4/GP5 (SDA/SCL, 4.7 kΩ to 3.3 V on the card), GP6 (EXP_INT#, internal pull-up only) and GP7 (ALERT#, 4.7 kΩ to 3.3 V). ALERT# and both resets sit on different pins than the dev carrier’s §4.2 map, and the FET drivers invert the resets (GPIO high asserts reset), so the firmware carries a board header for the card (firmware/controller/boards/pwrman_controller_card.h, built with PICO_BOARD=pwrman_controller_card) that selects the card’s pin map, the inverted reset drive, the 16 MB flash and its partition layout. The front-panel button is on GP22, and a 120 kΩ / 10 kΩ divider brings VIN to GP28/ADC2 for bus-voltage measurement. GPIO 3, 9–15, 26 and 27 are free. Because the mux and expander run from the backplane’s 5 V, the card drives the two reset lines through N-channel FETs (the backplane’s 10 kΩ pull-ups set the high level, and the RP2350’s boot-time pull-down leaves the FETs off, so a controller reboot never resets the expander) and pulls EXP_INT# and GLOBAL_ALERT# up to 3.3 V itself.
Flash: RP2350A + external W25Q128 (16 MB) on the primary QSPI chip-select. The RP2354’s 2 MB in-package flash would leave about 960 KB per image slot under A/B updates, and the CYW43439 WiFi firmware alone takes 220 KB of that; in-package flash cannot be enlarged. A second chip on QMI CS1 beside an RP2354 does not help either: the RP2354 shares its QSPI pads with the stacked die, the SDK has no CS1 flash driver, and the image slots stay capped at 2 MB. Because the RP2350A and RP2354A share the QFN-60 footprint, the layout keeps a lean assembly variant open: populate the RP2354A and leave the external flash unfitted (never both: they share a chip-select). QMI CS1 (GPIO0 on this package) stays free for possible PSRAM.
5. Mechanical and thermal
Section titled “5. Mechanical and thermal”
- Status lights: The status strip sits behind the faceplate with each port’s light above its USB-C port, so a slot’s light shows whether or not a blade is seated.
- Management card at the faceplate: The controller card’s RJ45 MagJack aligns flush with the front faceplate to the left of ports 1–6.
- Ethernet grounding and ESD: The RJ45 shield bonds to the chassis through its EMI spring fingers; the magnetics’ center taps terminate Bob-Smith style into the chassis-bonded ground region (§7.3).
- Thermal distribution: Each blade uses copper pours with thermal via arrays under the TPS55288 and the output FET’s drain to carry heat into the card-edge ground planes (PGND pins A8–A10, B2–B9). Two NTCs, at the converter and at the receptacle, are read out by the controller, and the blade trips its own port on them (100 °C / 70 °C, held for 50 ms).
6. Firmware architecture and supervisory state machine
Section titled “6. Firmware architecture and supervisory state machine”Implemented in firmware/controller (native pico-sdk C).
6.1. Dual-core RP2350 execution model
Section titled “6.1. Dual-core RP2350 execution model”The firmware runs on two cores. Only core 1 accesses the backplane.
- Core 1, engine (real-time power supervision):
- 100 Hz supervisory tick: round-robins the TCA9548A channels reading each blade’s register file in one burst read, runs the per-port state machine and the budget arbiter.
- Sole owner of the I2C bus and the TCA9539 (blade EN, presence, fan). Presence is re-read every 100 ms and on EXP_INT#.
- Services GLOBAL_ALERT# (edge interrupt + level check every tick): sweeps powered ports and switches off offenders via EN.
- Renders the WS2812C chain via PIO and publishes a seqlock telemetry snapshot plus a heartbeat.
- Core 0, management plane:
- lwIP over CYW43 WiFi and/or the W6100 wired netif (DHCP or static addressing; the wired link holds the default route when both are up): MQTT client (optionally over TLS) with Home Assistant discovery and LWT availability, mDNS, SNTP, embedded web UI + JSON REST API, Prometheus
/metrics, a console log ring with UDP syslog forwarding, USB CDC (and RTT) maintenance CLI, Improv Wi-Fi provisioning over BLE (BTstack on the same CYW43).- Owns settings (ping-pong flash sectors, CRC-protected) and feeds the hardware watchdog, but only while the core-1 heartbeat stays fresh, so either core stalling reboots the system.
- Between them: a command queue (core 0 → 1), an event queue (1 → 0), and the telemetry seqlock. Every management surface is a thin transport over the same command/telemetry interface.
6.2. Per-port state machine
Section titled “6.2. Per-port state machine”The blade negotiates PD contracts on its own, so there is no in-line “negotiating” state: the engine constrains the advertised PDO set ahead of time (a current ceiling and a voltage cap) and reacts to the contracts it observes.
| State | Entry Condition / Trigger | Actions & Hardware Control | LED Indication | Next State Transitions |
|---|---|---|---|---|
| ABSENT | PRES# high (no blade in slot). | EN low, mux channel unused, budget released. | Dim Amber | → PROBE on PRES# low (→ DISABLED if administratively off). |
| PROBE | Blade seated (at boot, the seated blades are released into PROBE one at a time, 250 ms apart, in priority order). | One attempt per tick: select the mux channel, read the blade’s register file at 0x3A, clear its latched faults, write the current ceiling and voltage cap, enable the port, assert EN. 3 failures → FAULT. | Blinking Cyan (2 Hz) | → IDLE on success. → FAULT on repeated probe failure. |
| IDLE | Powered, advertising, no sink. | 15 W base budget reservation held. Poll the blade each tick. | Solid White | → ACTIVE on sink attach. → ABSENT on removal. |
| ACTIVE | Sink attached, contract in place. | Full contract wattage reserved. Telemetry at tick rate; contract changes tracked against the budget. Measured draw under charged_mw for charged_min marks the port charged (a like period of draw clears it again); the off when charged policy and the per-port sleep timer end in DISABLED from here. |
Solid Green (<11 V) / Solid Blue (≥11 V); Solid White, as IDLE, once charged | → IDLE on detach. → THROTTLED on budget denial. → FAULT on alert. → DISABLED by auto-off. |
| THROTTLED | Contract would exceed the chassis budget. | Advertisement clamped to the granted wattage (never below 15 W); the refused ask is remembered for recovery (§6.3). | Pulsing (1 Hz) in the port’s active colour (Solid White once charged) | → ACTIVE when freed budget covers the original ask. → IDLE on detach. → FAULT on alert. |
| FAULT | Any fault the blade latched: its own OVP/OCP trips, converter faults, an NTC over its limit, a part or the PD stack failing. | EN low immediately, budget released, fault latched for diagnostics, 5 s cooldown; the blade is told the fault has been recorded so its latch (and ALERT#) clear. | Blinking Red (5 Hz) | → PROBE after cooldown (auto-retry; a fault whose cause persists comes straight back and repeats the cycle). → ABSENT on removal. |
| DISABLED | Administrative off (CLI/MQTT/REST/web), an auto-off policy, or a power-up policy of off (or last with the port last switched off). |
EN low, budget released. | Off | → PROBE on enable. → ABSENT on removal. |
- Chassis light and chain-wide indications: the chassis light, first in the chain and left of port 1, blinks red at 5 Hz while the DC bus is past its low (19 V) or high (29 V) flag, the same as a faulted port, and keeps blinking at the fault floor when the lights are at 0. Otherwise it breathes on a 2 s cycle while a management condition needs attention: blue while the BLE provisioning window is open, amber while settings are open for first-time setup, red while no link holds an address (slower than the bus-fault blink, so the two red states differ by speed). For the first 15 s after power-up (
NET_START_GRACE_MS) having no address is not shown and does not open BLE, so a boot that is still waiting for DHCP or a Wi-Fi join flashes neither red nor blue. With nothing to report it glows green at the master brightness capped at 32, so it is dim on a bright chain and dims no further than the other lights on a dimmed one. The power-up sweep lights all seven pixels in chain order, chassis light first (white or rainbow,led boot), and doubles as a chain-order check. Improv identify alternates every other pixel in blue at 2 Hz and outranks everything. Master brightness (led, the web Settings panel, or the Home Assistant number) scales all of it; at 0 the chain is dark except a faulted port, which keeps blinking at a floor level. Two schedules dim the whole chain toled_dim(default 4): a night window in local time (SNTP plus a fixedtzoffset) and idle dimming after a period with no port event; any port event restores full brightness.
6.3. Power budget and priority allocation
Section titled “6.3. Power budget and priority allocation”To stay within a fixed supply rating (default budget 360 W, i.e. 15 A at 24 V; six 100 W ports = 600 W aggregate peak capacity), the supervisor accounts for power by contract:
- Reserved budget, not measured draw: When a device negotiates a contract, the supervisor reserves the full contract wattage; measured draw is telemetry, not accounting. Every powered port also holds a 15 W base reservation (5 V @ 3 A): P_headroom = P_chassis_max − SUM(P_reserved[i])
- Priority shedding: Each port carries a priority (0 = highest; default = port number;
port <n> priorityin the CLI). When a new contract would exceed the budget, strictly lower-priority powered ports are renegotiated downward first (worst priority first, largest reservation breaking ties, never below the 15 W floor) by lowering the blade’s current ceiling, after which the blade re-advertises without dropping VBUS. Equal priority is never shed: first come, first served among peers.- Self-clamp fallback: If shedding lower-priority ports cannot free enough, the requesting port itself is clamped to whatever headroom remains.
- Recovery: A throttled port remembers the ask that was refused. When the whole refused amount fits the freed budget (detach, renegotiation, shed elsewhere), the reservation is claimed first, then the full advertisement is restored and the sink renegotiates upward. When only part of it fits, the clamp steps up by the available headroom in ≥5 W increments, at most one step per second per port, so partially freed power is allocated immediately. Freed headroom is allocated in priority order: a lower-priority throttled port waits while any higher-priority throttled port exists, since that port can also take headroom partially (EVT_THROTTLE codes: 0 clamped, 1 restored, 2 partial step).
6.4. Implementation notes
Section titled “6.4. Implementation notes”
- Blade interface: The blade’s register file (
firmware/charger-module/include/blade_regs.h, shared verbatim with the blade firmware and versioned by aPROTOregister) is a snapshot the controller reads in one burst (status flags, the PD contract, VBUS, port current, converter output and three temperatures), plus a control register, a command register (re-advertise, hard reset, clear faults) and the two limits. Configuration writes take effect together at the end of the transfer, limits ahead of the enable, so the port never arms on limits it has not been given. Out of reset the port is off until configured; a blade whose MCU restarts (watchdog, a brown-out on the slot’s 5 V) reports its configuration gone, and the port goes back through PROBE with no EN cut and no fault recorded. Faults latch on the blade with ALERT# low until the controller clears them, so nothing is missed while the controller is away; the controller maps them onto its port fault codes, and the blade’s raw word travels with the fault event. The converter and receptacle temperatures surface in the status table, the JSON,/metricsand two Home Assistant sensors per port, and feed the fan policy.- Blade firmware: The controller manages the blade’s firmware. The STM32G0 ROM bootloader answers on the same bus at 0x51, the controller image carries the blade firmware it was built with, and a blade found in its bootloader (blank, reset into it, or set to boot through it, the option the controller programs once) is checked against that image, written when it differs and started. A running blade on another version is sent to the bootloader only when the port is not in use: before its port is first powered, or after the port has had nothing attached for ten seconds. It is never updated while charging (
port <n> updateoverrides this) or while the controller’s own image is on trial. The blade’s watch timeout (theWATCH_Sregister) recovers a blade whose firmware has stopped responding: with EN low and no controller access for that period, the blade resets into its bootloader. With EN high it never does, so the ports keep running while the controller is restarting, updating or absent.- TCA9539 initialization order (critical): The expander’s Output Port registers power up as 0xFF (all high) while every pin defaults to input (hi-Z); the blades’ 100 kΩ EN pull-downs hold all ports off in that state. Firmware MUST write the Output Port registers to 0x0000 first, and only then write the Configuration registers (P0 = 0x80: EN1–6 + fan as outputs, P07 input; P1 = 0xFF: all inputs) to make the EN pins outputs. Reversing the order drives every EN high the instant the config write lands, enabling all blades at once and bypassing the per-port probe/budget sequence. The same rule applies after any EXP_RST# assertion or brown-out re-init.
- Expander/bus failure recovery: Five consecutive expander read failures trigger a MUX_RST# hardware reset (frees a hung downstream segment) and a TCA9539 re-init, which drops every EN. This fails safe: the ports re-probe and renegotiate from scratch.
- Flash layout (partition table): An RP2350 partition table (per-board JSON in
partitions/, compiled topartition_table.uf2and flashed once) divides the flash into A/B image slots plus adatapartition; the 16 MB production map adds anassetspartition, and the 4 MB Pico 2 W map has 2 × 1536 KB slots and 1016 KB of data. The firmware never hardcodes offsets: partitions are located by 64-bit ID through the bootrom at boot, so one binary spans every layout, and raw-offset reads go through the untranslated XIP alias (0x1C000000) so they stay correct when the bootrom maps slot B at the XIP base. Each image carries an IMAGE_DEF version derived from FW_VERSION (major, minor×256+patch), so the bootrom boots the newer slot and BOOTSEL drag-drop updates land in the inactive slot. A TBYB-flagged image boots as a trial and self-commits (bootrom explicit-buy) only after 10 s of an unbroken engine heartbeat and, with WiFi configured, once the network has come up at least once since boot. Otherwise any reboot, watchdog reset, or the 10-minute deadline reverts to the previous image.- OTA:
POST /api/v1/updatestreams a firmware image (the signed .bin; a UF2 or an unsigned .bin only from the console, auto-detected; picotool’s E10 marker block skipped) into the inactive A/B slot, chosen viarom_pick_ab_partition_during_update. Writes go sector-by-sector throughflash_safe_execute; the image’s first sector, the only place the bootrom looks for an IMAGE_DEF, is erased up front and written last with the TBYB flag patched in (refused for hashed/signed images, which the flag edit would invalidate), so an interrupted transfer never leaves a bootable half-image. After CRC/read-back verification the controller reboots via a bootrom flash-update boot (the only path that boots a TBYB image), handing off to the trial/commit flow above. One transfer at a time; a stalled client is dropped after 30 s; an uncommitted trial refuses updates so the known-good fallback slot is never overwritten. Before the first sector is written the image must carry a valid Ed25519 signature from one of the public keys built into the firmware (a 176-byte trailer on the raw .bin: length, board, SHA-512, signature), be built for this board, and be no older than the running image; only the console’supdate --unsigned/--downgradewaive those, since reaching it takes the USB header or a debug probe. The controller also checks for a newer release itself: shortly after the network comes up and daily after that it fetches a version pointer from the update source (a setting;http://fw.powermanifold.ioby default, a plain-HTTP proxy in front of the GitHub releases; empty turns the check off). A newer release is reported to Home Assistant’s update entity and on the console, and installs only when requested.- Settings: Ping-pong pair of 4 KB sectors with sequence numbers and CRC32 (a power failure mid-write leaves the previous copy intact), in the first two sectors of the
datapartition. Records carry a layout version; an older record loads with explicit defaults for the fields it predates, so a firmware update never wipes settings.GET /api/v1/settings/exportandPOST /api/v1/settingsround-trip the whole set as JSON for backup and restore.- Boot reason: deliberate reboots and the HardFault handler stamp the watchdog scratch registers before resetting, every boot leaves a sentinel there, and the rest comes from the chip’s reset-cause bits (which a SYSRESETREQ does not update, hence the sentinel). The banner,
info, the status JSON, Home Assistant and a fault-log record name the cause of every start: power-on, brown-out, RUN pin, debugger, watchdog, requested reboot, firmware update, trial revert, warm reset, or a HardFault with core, PC, LR and CFSR.- Warm start: a controller reboot (update, requested, watchdog, HardFault) never cuts port power. The expander runs from the backplane’s 5 V and neither controller board asserts its reset when the RP2350 resets, so it keeps its registers. The engine reads the expander before touching it: one still holding this firmware’s configuration is adopted as it stands (a warm start); only the power-on state (a cold start) gets the reset-and-configure sequence, outputs low first. Powered blades are then re-supervised in priority order without an EN cut: identify the blade, treat any fault latched meanwhile as a fault now, and rewrite the blade’s limits only where what it holds differs from the settings. The boot policy still switches off what it would at a cold boot. A powered blade that does not answer, then or later, keeps its power: it is shown as silent and polled until it does, and only a fault, a removal or the operator takes a port down. There is no budget arbitration or firmware fault response for the seconds the controller is away; the blades’ own limits cover that window.
6.5. Management plane
Section titled “6.5. Management plane”Every surface is a thin transport over the same command queue and telemetry snapshot. The HTTP API, MQTT and Home Assistant pages carry the full reference (endpoint, topic, entity and console tables); this section is the architectural summary.
- MQTT (primary remote surface):
pwrman/<name>/status(retained chassis status) and.../port/<n>/telemetryat 1 Hz, engine events on.../event(each with akindin Home Assistant’s event vocabulary and a human-readabletextfor faults and auto-off), availability via LWT. Commands:.../port/<n>/set(ON/OFF/hard_reset/src_cap) plus per-portpriority,limit,volt,boot,autooffandsleepsetters, chassisbudget,fan(auto/on/off),led,charged_mw/charged_min,improv(open a BLE window) andupdate(install the retained.../update/latestpointer). Settings changed remotely persist via a debounced save.- Home Assistant discovery, per port: power/voltage/current/state/energy sensors (energy
total_increasing, usable in the energy dashboard; the state sensor carries the last fault as attributes), converter and receptacle temperatures, an enable switch, hard-reset/re-announce buttons, priority and current-limit numbers, a voltage-cap select (5–20 V), a power-up select (on/off/last), an off-when-charged switch and a sleep-timer number, a charging binary sensor, and an events entity fed from the event topic. Chassis: power/headroom/energy sensors, budget and LED-brightness numbers, the fan select, charge-complete threshold numbers, diagnostic sensors for boot reason and LED mode, a Problem binary sensor, an Open BLE provisioning button, and a firmwareupdateentity whose Install pulls the pointer’s URL over plain HTTP into the OTA core. Entity names carry the port’s label; ids stay put across renames.- Web UI + REST: broker-independent local surface: live status page with per-port graphs, fault-log and console-log panels, and a settings panel covering every setting (unlocked by the API token, or, while no token exists, by the one-shot setup secret an Improv redirect carries or by the first hour after power-up for a request arriving over Ethernet, an hour a short button press restarts).
GET /api/v1/status,/faults,/log,/settings,/settings/export;POST /api/v1/settings(import),/port/<n>,/fan,/budget,/faults/clear,/reboot,/update(OTA push);GET /metricsfor Prometheus. Changes take a Bearer token; with none stored they are refused until first-time setup saves one.- USB CLI: provisioning (WiFi/MQTT credentials, device name, token, addressing, DNS, syslog), status/info, budget, per-port control/priority/name/limit/power-up policy/auto-off/sleep, charge thresholds, fan policy, LED brightness and schedule, timezone, fault log, settings export,
update <http-url>OTA pull,simfault injection on fake-blade builds,bootsel.- Addressing: DHCP on every link by default; a static address goes on the wired link when a W6100 is fitted, else on WiFi (never both). A configured DNS server always wins over DHCP’s.
- Console log: everything printed is mirrored into a 4 KB ring, readable over the API and the page, and optionally forwarded line by line to a UDP syslog host as RFC 5424, including the boot messages printed before the network came up. Console input is never mirrored.
- Health: an aggregate problem flag (any port faulted, engine stalled, trial firmware uncommitted, wired link down while WiFi carries the traffic) with a text description, on every surface. The MQTT client drops and reconnects a link that stops acknowledging publishes, so a stalled broker path cannot hold lwIP’s buffers.
- Fan policy: auto mode (default) follows total chassis power with hysteresis (settings
fan_on_w/fan_off_w, defaults 80/60 W) and a 30 s anti-flap hold. It also runs while any port holds a contract overfan_on_ma(default 3 A), and while any blade’s converter reads 65 °C or more, until all of them read below 55 °C. Manual on/off overrides from any surface drop it out of auto.- Fault log: faults, probe failures and boot reasons persist in the data partition right after the settings pair: a 64 KB ring of one-record 256 B flash pages (~256 records, oldest sector recycled), each carrying event, port power/contract at that instant, uptime and SNTP wall-clock time. CLI
faults/faults clear,GET /api/v1/faults, the page’s panel, and the newest per port as Home Assistant attributes.- Provisioning: an unprovisioned controller (or one off the network for five minutes) advertises the Improv Wi-Fi BLE service; the hosted provisioner on this site, the Home Assistant app or any Improv client hands it credentials, and the redirect into the web UI carries the setup secret that unlocks the settings panel; a wired-only controller instead has its settings open for the first hour after power-up. The chassis light breathing amber shows that settings are open. Outside a window the Bluetooth controller is powered down.
6.6. Verification
Section titled “6.6. Verification”The engine core (state machine + budget arbiter) is hardware-free and runs host-side against simulated drivers (test/, in CI on every firmware change): probe/fault/recovery paths, contract accounting, priority shedding and recovery ordering, power-up policy and staggering, charge-complete and auto-off, plus the hardware-free management pieces: the settings JSON round trip, the log ring, LED scheduling, event naming, IPv4 text, and the W6100 driver against an emulated chip. The same simulated backplane compiles into the firmware (-DFAKE_BLADES=ON) and loops a scripted demo (attaches, budget contention, an over-current fault), so the full management plane runs on a bare board with no backplane. The console’s sim command pauses the script and injects seats, attaches, loads, blade faults and restarts, pinned thermometer readings, silent chips and mux/expander failures by hand, so the fault log, problem sensor and event entities can be exercised end to end. See Building.
7. Grounding, chassis bonding and ESD protection
Section titled “7. Grounding, chassis bonding and ESD protection”7.1. Two-tier grounding model
Section titled “7.1. Two-tier grounding model”A three-domain, star-grounded topology is not physically realizable in this architecture: every blade and the backplane’s buck regulator join the logic-rail return to the DC bus return, so the system contains seven or more domain ties and no single star point can exist. Splitting copper planes anyway would force every I2C, EN, ALERT#, and PRSNT# trace to cross a plane slit with a broken return path. The design therefore uses two tiers:
- Circuit Ground (GND): One electrically continuous ground net per board and across the card-edge interconnect. “PGND” is layout geography (named copper zones that confine high-current switching returns to defined regions, §7.2), not a separate net. All GND and PGND card-edge fingers land on the same net.
- Chassis (CHGND): The metal enclosure, faceplate, card guides, and connector shells, deliberately multipoint-bonded to circuit ground (§7.3). Chassis is an ESD/EMI spreading structure, never an intentional current path.
Measurement integrity comes from the architecture rather than plane splits: each blade measures its own port locally and reports numbers over I2C, so ground offset between boards never enters a measurement, and I2C noise margins are far larger than any achievable inter-board ground offset.
7.2. Board-level ground-zone layout
Section titled “7.2. Board-level ground-zone layout”Charger blade (4-layer, 1 oz outer / 0.5 oz inner):
- L2: solid, unbroken ground plane. No slots, no splits. It is the power return, the logic reference, and the thermal spreader that carries converter heat to the card-edge fingers (§5). Every ground via from every zone lands on it. L3 carries the 5 V and 3.3 V logic pours.
- L1 power spine (“PGND” zone): VIN entry from fingers A2–A7/B2–B7 → input filter and capacitors → TPS55288 power stage and inductor → output capacitors → current-sense shunt → VBUS switch → USB-C receptacle. Both hot loops (input-capacitor and output-capacitor) are minimum-area and strictly local.
- L1 logic (“GND” zones): Logic routing from the logic-half fingers (5V, SCL/SDA, ALERT#, EN) stays off the power spine. No logic trace passes under the inductor or switch-node copper.
- Kelvin sense routing: Shunt sense lines run as a tight differential pair over solid ground, laterally clear of the inductor and switch node.
Zones are drawn as separate outlines on the same net (multiple zones, one net). They connect through the L2 plane rather than by touching on L1, so pours cannot silently merge across the geographic boundary, and a stray trace over the wrong zone keeps its return path.
Backplane: One solid ground plane under the entire board; all seven sockets’ GND and PGND fingers land on it. VIN distributes on dedicated copper pours on the outer layers only (4-layer, 2 oz outer / 0.5 oz inner; the VIN corridor is pour-based). The DC input (−) terminal lands on the plane next to a chassis-bonded mounting hole, so the heavy-current corridor runs between the DC entry and the slots’ power-half fingers; the TCA9548A / TCA9539 / LED / management-slot region is placed outside that corridor. I2C, EN, ALERT#, and PRSNT# route over the unbroken plane, with no split for them to cross.
7.3. Chassis bonding and enclosure
Section titled “7.3. Chassis bonding and enclosure”The chassis and circuit ground form one deliberately multipoint-bonded ground system (the PC-chassis model):
- Bonded standoffs: Plated, unmasked mounting holes (annular ring stitched with vias) on the backplane, fastened with star washers into conductive metal standoffs.
- USB-C shells: Each receptacle’s four through-hole shell tabs solder into the blade ground pour with multiple vias; shell contact with the faceplate cutouts provides additional chassis bonds by design.
- RJ45 MagJack (controller card): 360° EMI spring fingers against the enclosure cutout are the shield’s chassis bond. The magnetics’ center taps terminate Bob-Smith style: 75 Ω per pair into a common node, then 1 nF / 2 kV into the chassis-bonded ground region at the jack.
- DC entry: The input (−) terminal, a bonded standoff, and the ground plane meet in one corner of the backplane, so the highest-current cable entry references chassis at its point of entry.
- No isolation barrier: With shells, spring fingers, and card guides bonding the chassis at many points, a capacitive CHGND-to-GND barrier could not exist in practice, so the design has none. An ESD strike to any shell or panel spreads through the chassis and returns via many bonds, with only a small fraction crossing any single board.
7.4. Port ESD and DC input transient suppression
Section titled “7.4. Port ESD and DC input transient suppression”- CC lines (each blade): The TCPP02-M18 provides CC over-voltage and ESD protection at the receptacle.
- Data line (each blade): A USBLC6-2SC6 array on the receptacle’s data line.
- VBUS clamp (each blade): An ESDA25P35-1U1M TVS between VBUS and ground at the connector. USB cables are hot-plugged, and abrupt disconnection of a 100 W load produces an inductive load dump.
- DC bus clamp (backplane input): Bidirectional SMCJ33CA TVS across VIN and ground, behind the 40 A fuse and ahead of the LM74800 FET stage: 33 V stand-off above the 28.8 V declared maximum (the hardware tolerates 32 V), breakdown from ~36.7 V. It is bidirectional so a reversed cord meets the ideal diode’s block rather than a forward-biased clamp behind the fuse. The 32 V hardware ceiling above the declared range is this stand-off and the LM74800’s ≈34 V over-voltage cut-off (the 40 A SMD fuse is rated 63 V DC, so it does not set the limit); the fan switch is on the 5 V rail, so its 30 V MOSFET is off the bus. The TPS55288’s 36 V input limit is the hard constraint that keeps the bus in the 24 V class (48 V operation is not feasible), so the clamp’s job is holding realistic supply-side transients within the converter’s absolute-maximum headroom: connection ring when the DC cord meets ~2 mF of aggregate chassis capacitance, and inductive kick if the cord or supply drops out under load. Switch the external supply on after the DC cord is connected (or fit an anti-spark/NTC element at the entry) to limit connection inrush.
- DC bus undervoltage cut-out (backplane input stage): The LM74800’s EN/UVLO pin sits on a 100 kΩ / 6.81 kΩ divider (R7 / R8) from the common-drain mid-point of the FET pair. Below ≈17.7 V falling (16.8–18.5 V over the 1.091–1.159 V reference spread and 1 % resistors) it opens the second FET and disconnects the entire bus from the input; above ≈19.3 V rising (18.4–20.25 V) it re-closes through the inrush ramp. The divider keeps the worst-case turn-on under 20.4 V, the lowest voltage at which IEC 62368-1 B.2.3 tests a 24 V DC mains input. Because the whole bus goes, not just the logic rail, the blades lose VIN outright, the bulk capacitors stop being topped up, and the draw on a sagging supply falls to the LM74800’s 2.87 µA shutdown current plus the divider (≈0.2 mA; the OV ladder hangs off the SW disconnect and draws nothing). Sensing the mid-point rather than the raw input is deliberate: the bus capacitors hold the mid-point up through the second FET while the ideal diode blocks, so a brief input dropout never reaches the pin and the bus rides through it (VSNS, which only feeds the OV ladder, is on the raw input as the datasheet draws it). Behind that cut, the AP64352 buck’s own EN divider (100 kΩ / 7.5 kΩ from the eFuse output: on ≈16.8 V, off ≈15.1 V, 14.3–16.0 V over tolerance) is the backstop. When the bus decays through it, the TCA9539 loses VCC and its port pins go high-impedance, and each blade’s EN (B16) is held low by the blade’s own 100 kΩ pull-down, which shuts the converter down. The management card reboots with the rail, and on recovery the expander powers up with its ports as inputs, so no blade re-enables until firmware applies the boot policy. This hardware cut-out, not the budget engine, is the final low-bus failsafe: a sagging supply cannot be dragged below ~17.7 V with the ports still loaded, which bounds the worst bus current at roughly 36 A (six ports at 100 W just before the cut), under the 40 A fuse and within the XT60’s burst rating. A supply that cannot hold 20 V under load causes a disconnect-and-restart loop, not a hazard. The controller firmware reduces the chance of that loop: under 20 V it caps every port at 3 A (60 W, 22 A worst case at the cut), its 19 V low flag fires ahead of the cut and is written to the fault log, and a chassis that restarts on a marginal bus comes up capped and staggered.
- Logic-rail power limit (PS1, backplane): A TI TPS16416 eFuse (U5) sits between VIN and the AP64352’s input. Its active current limit is set to ≈0.40 A (24.9 kΩ on ILIM), so the power it can deliver is ≈11.2 W at 28 V (≈12 W with the limit at its tolerance ceiling, ≈13.8 W if the bus were run at the 32 V hardware limit) and ≈8.6 W at 20 V, under the 15 W that IEC 62368-1 clause 6.2.2 allows a PS1 source measured 3 s after the worst-case load. The part is UL 2367 recognised (file E169910) and IEC 62368-1 CB certified, so it is evaluated as the overcurrent protective device rather than faulted internally, and every programming pin fails toward FET-off: ILIM or IOCP open or shorted turns the FET off, OVP is tied to ground, the enable pin sits on a 1 MΩ / 120 kΩ divider from VIN, IDLY is open so the limit engages without blanking, and 10 nF on dVdT holds start-up inrush into the buck’s 10 µF input capacitor to ≈0.1 A. IOCP is 0.80 A (20.5 kΩ); a sustained overload is limited for 155 ms and then auto-retried after 620 ms. Everything downstream of its OUT pin (the buck, the 5 V fingers of all seven slots, each blade’s logic, the pixel chain and the fan) is therefore a PS1 circuit; the buck’s enable divider is referenced to the eFuse output so the buck waits for its input before it starts switching.
7.5. Card-edge guarding and presence detection
Section titled “7.5. Card-edge guarding and presence detection”The system is not hot-pluggable: installed USB-C cables prevent removing the front plate in service, so blades are only inserted or removed with the chassis de-energized. The presence circuit is therefore a population and seating check, not a hot-swap mechanism. It needs no precharge path, no extra finger stagger beyond the standard PCIe scheme, and no extraction-race firmware.
- Finger heights: Standard PCIe CEM two-length geometry. All power, ground, and signal fingers are full height; only A1 and B17 are short (CEM short-pin height per the connector drawing). Because the loop closes last, PRSNT# asserts only at full seating; a half-inserted blade reads absent and is never enabled.
- Presence loop (A1 → B17): The blade connects A1 to B17 with a plain trace: no components, no power dependency. The backplane grounds A1 at every charger slot. The loop closes only when both ends of the connector are seated, so an angled or partial insertion cannot read as present. B17 has a 10 kΩ pull-up to the 5 V rail and enters a TCA9539 input through a 330 Ω series resistor; the expander INT output provides EXP_INT#.
- Management slot: No presence circuit; A1 and B17 are grounded (§4.1). The management card is installed with the chassis de-energized. The firmware, not the backplane, detects whether a controller is present.
- Firmware use: Presence is read at boot and re-read every 100 ms plus on EXP_INT#. Empty slots are never probed and their EN lines never assert. As a further safeguard (not a hot-plug feature), any runtime PRSNT# deassertion, from a seating or vibration fault, immediately drives that slot’s EN low.
- Edge guards: A18/B18 remain full-height sacrificial grounds at the outer end, discharging handling static before signal fingers seat during assembly; A11/B11 guard the key notch. A16 is tied to GND (no floating reserved finger next to EN), and B1 joins the main power return, so both ends of the connector row terminate in solid ground.