OneWireRPC — RPC Over a Single GPIO for Tiny Devices
Why OneWireRPC?
Sometimes you have exactly one spare pin and a burning need to talk to a tiny device: read a sensor, trigger a calibration, or push a tiny config change. You could wire up an entire UART or add a fancy transceiver, but that increases BOM and power. OneWireRPC is a pragmatic, low-footprint protocol that uses a single open-drain GPIO and a pull-up to implement a reliable request/response RPC link. Low complexity, low power, and easy to bit-bang on 8-bit MCUs.
How it works (high level)
The master and slave share one line pulled up to VCC. Both sides can pull the line low; the master initiates each transaction. Bits are sent with simple pulse-width encoding. Transactions are framed with a start sequence, a length, payload, and a CRC-8. The slave responds in a defined reply window so collisions don't happen.
Framing and encoding
Keep timings conservative so weak MCUs can sample reliably. Use a base slot of 250 µs (adjust for clock accuracy). Encode a '0' as a low pulse of one slot, and a '1' as a low pulse of three slots. A bit period is four slots (low then high). Frames use little-endian byte order for simplicity.
- Idle: line high (pulled up)
- Start: master pulls low for 8 slots, then releases for 4 slots (start delimiter)
- Length byte (1 byte): number of payload bytes
- Payload (N bytes)
- CRC-8 of length+payload (1 byte)
- End: master holds line high and begins a response timer (see reply timing)
Reply timing: slave must begin pulling the line low within a fixed window after the master's end-of-frame, e.g. 2–5 ms, and must finish its response inside an allowed reply timeout (say 50 ms). If the master sees no reply, it can retry after a backoff.
Example timings (base slot = 250 µs):
- Start low: 8 slots = 2 ms
- Start high: 4 slots = 1 ms
- Bit '0': low 1 slot, high 3 slots (250 µs low, 750 µs high)
- Bit '1': low 3 slots, high 1 slot (750 µs low, 250 µs high)
- Reply window: begin between 2 ms and 5 ms after end-of-frame
CRC and robustness
Use a tiny CRC-8 (e.g. poly 0x07, init 0x00). CRC covers length and payload so a single-byte header corruption is detected. Keep payloads small (16–32 bytes recommended) to bound reply time and keep power low.
Why not Manchester or UART? Because we want a minimal implementation with obvious timing that survives slow MCUs and variable clock sources. Pulse-width encoding is simple to sample with a timer or even a tight loop.
Sample pseudo-code
Master send (high-level):
// pull line low for start
pull_low(line); delay(2000); // 8 slots
release(line); delay(1000); // 4 slots
send_byte(length);
send_bytes(payload, length);
send_byte(crc8(length, payload));
// watch for reply in 2-5 ms window
if (wait_for_low_within(5_ms)) { reply = read_frame(); }
Slave receive (high-level):
if (detect_start()) {
length = read_byte();
payload = read_bytes(length);
crc = read_byte();
if (crc8_matches(length, payload, crc)) {
process_request(payload);
// prepare reply
wait_until_reply_window();
send_reply(reply_payload);
}
}
Bit read/write primitives are tiny: measure low pulse duration in slot units and decode 1/3 thresholds, or generate pulses by holding low and delaying for 1 or 3 slots.
Wiring and electrical notes
- Use an open-drain (or open-collector) output on both ends. If the MCU lacks open-drain, configure as output-low and switch to input (high-Z) to release the line.
- Pull-up: 10–47 kΩ is usually fine. Lower value helps speed and noise immunity, but increases leakage in sleep.
- Line capacitance: keep wiring short (<1 m ideally). If you must go longer, use a stronger pull-up and increase slot length to tolerate RC rise times.
- Levels: both sides must share VCC reference. For mixed-voltage systems, use an open-drain MOSFET level-translator or ensure 3.3V devices only.
Power and low-power strategies
OneWireRPC works well for battery sensors: keep the slave asleep until it detects the start pattern (long low followed by short high). Implement a wake-on-edge using an interrupt on the GPIO (falling edge) and verify the start sequence in software. If your MCU supports deep sleep with GPIO wake, you can achieve sub‑µA standby.
Collision avoidance and multi-slave setups
The protocol assumes a single master and optional multiple slaves. To avoid collisions, let the master address slaves in the payload (1 byte ID) and require slaves to only reply if the ID matches. Alternatively assign each slave a time slot to reply (slot = ID & mask) to keep things deterministic without complicated arbitration.
Troubleshooting
- If the slave never wakes: confirm pull-up is present and that falling edges generate an interrupt. Check that your wake ISR debounces and validates the start sequence timing before exiting sleep state.
- Noisy line or spurious bits: increase slot length and add a small RC filter (e.g. 10 kΩ and 100 pF) if you can afford the extra latency.
- CRC failures: verify both sides use the same CRC polynomial and byte order. Log raw captured pulses on a scope to verify timing assumptions.
- Reply collisions: check that replies start inside the allowed window and that slave reply timing uses a deterministic delay after the start-of-reply window opens.
Where this fits and when not to use it
OneWireRPC is perfect when pins and power are limited and you need simple command/response behavior: configuration, sensor reads, small bootloader commands, or recovery consoles on production boards. It's not suitable for high‑speed telemetry, large firmware transfers, or environments with heavy EMI—use UART, SPI, or a proper bus for that.
If you like minimalism and hate the noise of unnecessary parts, OneWireRPC gives you a practical, low-cost way to talk to tiny devices without adding hardware complexity. It's not magical—it's just useful.