Skip to content
Rodeo

The satellite chain

Running on real satellites since 2 November 2026

A ring of a Rev B motherboard, a pot bank and an encoder bank ran about 24,600 scans in two minutes with none bad, every control read and both LEDs driven from the Seed. The driver is also host-tested against a simulated ring — discovery, a live break found and repaired, a satellite added while running — and those paths have not all been reproduced on hardware: breaks within a ring and chains of three or more are still untried. API: sat_chain.h, sat_chain_port.h, sat_frame.h. The wire protocol is hardware/rodeo-motherboard/docs/RODEO_SATELLITE_CHAIN_CONTRACT.md; this page is the app’s side of it.

Every control beyond the I/O connectors — pot banks, encoder banks, buttons — is on a chain of small satellite boards, each with a CH32V003 that reads its controls and passes frames along a UART ring. A satellite’s position in the chain is its address. The last satellite closes the ring by itself when nothing is plugged into its OUT.

Two parts

  • sat_frame.h — the frame and slot layout, CRC-8/SMBUS, slot kinds, build and parse. Pure logic, no hardware. The SDK’s driver and the satellite firmware include this one header, and host tests exercise it from both sides, so the two ends cannot drift apart.
  • sat_chain.* — the Seed’s driver: USART3 at 1 Mbaud on DMA, a scan paced by a timer (4 ms by default), discovery at start-up and whenever the chain changes, and fault and break reporting.

Using it

rodeo::SatChain chain; // the engine: pure logic
rodeo::SatChainPort port; // USART3 and its DMA
int main()
{
hw.Init();
chain.Init(rodeo::SatChain::Config(), System::GetUs());
port.Init();
for(;;)
{
port.Process(chain); // never blocks: call every time round
if(chain.Changed())
Rebuild(); // chain.Satellites(), chain.Satellite(i): what is where
float cutoff = chain.Pot(0, 2); // satellite 0, slot 2: 0..1
int detents = chain.EncoderSteps(1, 0); // since the last call, + or -
chain.SetLed(1, 255, SAT_LED_SLOW_BLINK);
chain.SetRgb(2, 0, 255, 96, 0); // a button bank's first LED: amber
}
}

The engine waits 50 ms for the satellites to boot, discovers, then scans every 4 ms. Three bad scans in a row and it rediscovers; three failed discoveries and it finds the break by closing the ring one satellite further at a time, then keeps scanning everything before the break so the instrument stays playable, and retries the whole ring every two seconds.

Controls are addressed by satellite and slot, in chain order. Mapping a physical control to a parameter is the instrument’s job, in its own control map — the SDK never names a knob.

Open

  • How pot conditioning splits between satellite and Seed (planning/REV_B_PLAN.md, “The pot pipeline”). It decides whether Pot() returns a calibrated value or a raw one.
  • What an app sees when the ring breaks mid-performance: last good values held, plus a fault it can show.