Two Wires, One Kill Switch: Controlling Several Modules From a Pin-Starved Carrier

Two Wires, One Kill Switch: Controlling Several Modules From a Pin-Starved Carrier

Every so often, a constraint on a board is so tight it stops being a nuisance and becomes the whole design. This was one of those. We had a carrier board that needed to control the power state of several system-on-modules — up to six of them from one carrier — and we had exactly two general-purpose pins to do it with. Not two pins per module. Two pins, total, for the lot. And the pins couldn't be pressed into service as a named communication protocol like I2C or SPI; they had to stay plain, dumb, toggle-them-yourself GPIO.

On top of that, the signal doing the work here is the master power enable for each module. That's not a status LED or a nice-to-have. It's the line that turns a module on and off, and by extension it's the kill switch. When something goes wrong with a module, this is the wire you reach for. So the requirement wasn't just "control several modules with two pins." It was "control several modules with two pins, and make the control path so reliable that it behaves predictably even when everything around it is misbehaving." That second clause is the one that shaped every decision.

Two wires can't hold four independent bits — unless you add memory

The first thing to be honest about is the information-theory floor. Two wires, at any instant, can represent one of four states. If all you ever need is "one module on at a time" — say, for sequential bring-up or test — you don't need anything clever. A small combinational decoder turns your two levels directly into a one-hot selection, with no memory and therefore no way to glitch: the output is a pure function of the two inputs, and it re-settles correctly the instant power or logic stabilizes. That's genuinely the most robust option if the use case allows it.

Ours didn't. We needed arbitrary, independent, simultaneous on/off across all the modules — this one on, that one off, two others on — and held that way. The moment you require independent states to persist across all channels at once, two wires are no longer enough on their own, because you're asking four (or six) bits to be remembered while your two wires are busy carrying the next instruction. That means memory. Somewhere in the fan-out there has to be a storage element per channel.

The clean way to get that is a shift register with a separate storage stage — two internal registers, not one. Data ripples in bit by bit as you toggle the two pins, but the outputs only ever reflect the storage register, never the shifting one. So the half-formed, mid-update garbage that exists while you're clocking bits in never reaches the enable lines. The outputs change only at the instant you deliberately transfer the shifted value into storage. That single property — outputs isolated from the shift in progress — is what buys you "no failure in logic." The lines can't momentarily flicker a module on or off while you're partway through an update.

Spending zero pins on the latch

There's a catch, and it's exactly the kind of thing the two-pin budget punishes. A shift register with a separate storage stage normally needs a third control line to trigger the transfer from shift to storage. We didn't have a third pin. Spending one wasn't an option.

So we moved that job out of firmware and into hardware. A small counter watches the clock line and, after a fixed number of pulses — one full update frame — fires the latch automatically, then resets itself for the next frame. Firmware never sees this line and never manages it. It just clocks in a fixed-length frame, and the hardware does the transfer at the end on its own. The third "pin" effectively exists, but it lives on the board as a few cents of logic instead of on the connector as a wire we couldn't spare. This is the sort of trade that only makes sense once you accept the pin budget as immovable: you buy back capability with parts, not pins.

The state at power-up is not optional

For a kill switch, the most dangerous moment is the one before anyone is in control — the instant power comes up and the MCU is still booting, GPIOs half-defined, firmware not yet running. If the enable lines float or come up in a random state during that window, you can get modules powering on before anything is ready for them. That's unacceptable for a power-control path.

So the storage element is held cleared for the first few tens of milliseconds after power-up by a small power-on-reset, forcing every enable line to a known "all off" state regardless of what the processor is doing during its own boot. As a second layer, each physical enable trace gets a pull-down at the connector, so even in the brief window before the reset releases, the line reads a defined low rather than floating. The register's output-enable is tied permanently active, because the reset already guarantees a safe startup value and we didn't want to spend logic — or a pin — solving a problem twice. The principle underneath all of this: the safe state must be the default, achieved by hardware, never something firmware has to hurry to establish.

We also made the update frame stateless on purpose. Every update sends the full desired state of all channels, not an incremental "toggle bit three" instruction. That makes the scheme self-correcting: a corrupted frame is simply overwritten cleanly by the next full frame, and no error accumulates over time. It costs nothing and removes a whole class of drift bugs.

Scaling fell out for free. The storage element has more channels than we currently use, and the two wires already carry all of them — going from four modules to the original six is just a longer frame and moving where the counter taps its "frame complete" signal. No new routing back to the processor, no new pins.

A wrinkle: a shared enable can only ever be a broadcast

Partway through, a real problem surfaced. When modules sit in parallel and their enable pins land on a common net, that pin becomes a broadcast — one physical wire, one value, seen by everyone at once. No amount of firmware cleverness lets a single wire carry different values to different modules simultaneously. As wired, the shared enable could only be a group-wide master, not per-module control.

The fix keeps both behaviours instead of choosing between them. The shared enable stays as a genuine master kill for the whole group. Then each module gets its own individually-routed select line from the two-pin fan-out, and per slot a simple AND combines the two: a module is actually enabled only when the master enable is asserted and that slot is individually selected. Better still, that combined result drives a real high-side load switch on the slot's power feed rather than just holding a logic pin low — so a de-selected module has its power physically removed, and there's no chance of its enable pin backfeeding or loading the shared net.

Why we kept the kill path off any shared bus

The obvious question is why not just put small I2C expanders at each module and address them individually — it scales cleanly and sidesteps the pin count entirely. We considered it and deliberately declined, for the safety-critical path.

An I2C bus is shared, and a shared bus can hang: one device holding the data line low during a brownout, crash, or ESD event jams the bus for every device on it. Now think about when you most need to power-cycle a module — precisely when it's misbehaving, which is exactly when its bus segment is most likely to hang. An I2C-based kill switch can therefore become unavailable at the one moment you need it. That's a single point of failure sitting on top of your last-resort control. So the deterministic, protocol-free fan-out stays on the power and kill path, where its failure modes are fully predictable, and I2C expanders are reserved for things that aren't safety-critical — status indicators, board-ID straps — where a temporary hang is an inconvenience, not the loss of your ability to cut power.

That's the whole theme, really. When one signal has to work when nothing else does, you keep it out of every shared, hangable, stateful thing you can — and you pay for that in a handful of small parts rather than in reliability.