BlinkSpeak — One‑LED, Multi‑Message Debugging for Minimal Microcontrollers
Why one‑LED debugging?
Many tiny projects have a single status LED and no serial port. When something goes wrong — boot failure, sensor misread, brownout — a single LED is often all you have to communicate state. BlinkSpeak is a deliberately simple protocol and accompanying implementation pattern that turns one LED into a low‑bandwidth, robust diagnostic channel: structured messages, error codes, and short text-like payloads encoded as blink patterns.
Design goals
- Work on minimal microcontrollers with one GPIO for an LED.
- Be human‑readable and machine parsable (for capture with a light sensor or photodiode).
- Survive brownouts and watchdog resets; restartable in the middle of a message.
- Keep power usage low and timing simple — a few constants and no floating point.
Protocol overview
BlinkSpeak encodes messages as a start sequence, a length byte, a sequence of data nibbles (4‑bit), and a checksum, all transmitted using on/off pulses. Each symbol (start, nibble, checksum) is framed with a short gap to allow resynchronization. Time is handled in multiples of a base slot (T).
- Start: 3 long pulses (3*T on, T off between) to signal a message is coming.
- Length: 1 byte sent as two nibbles.
- Data: N nibbles for N bytes of payload (high nibble then low nibble per byte).
- Checksum: 1 byte (xor of payload) sent as two nibbles.
- End: 1 long pulse to mark message end.
Each nibble is transmitted by a sequence of four slots. A bit value '1' is encoded as a pulse in a slot, '0' is silence. Example: nibble 0xB (1011) becomes on, off, on, on over four slots. Between nibbles insert a gap of 2*T.
Timing and practical values
Pick a base slot T between 80 ms and 200 ms. Faster is harder for human decoding and more fragile on cheap photodiodes; slower wastes time. Recommended default: T = 120 ms. That makes a nibble 4*T = 480 ms plus the inter‑nibble gap — you can transmit a 6‑byte payload in well under a minute.
Tip: For human troubleshooting use longer pulses (T ≈ 150–200 ms) so a human can count pulses. For automated capture use shorter T (≈ 80–120 ms) and a photodiode with a small microcontroller reader.
Implementation pattern
Keep the transmitter state machine tiny and restartable. The transmitter runs in three parts: prepare (fill a small ring buffer), send (driven by a periodic timer interrupt or polling), and fallback (watchdog or power fail). Use checksum to detect truncated messages.
Receiver for humans is simply counting start pulses and then reading groups of four blink slots. For an automated receiver, sample the LED state at 5–10× slot frequency and decode by thresholding.
Minimal pseudo code
Transmitter (high level):
// prepare
msg = [length, payload bytes..., checksum]
queue_nibbles(msg)
// sending loop (called every T/4)
if (next_slot_should_be_on()) set_led(1); else set_led(0);
Receiver (polling):
// sample at 5x slot rate, detect pulses, align to start
if (detect_start_sequence()) {
align_to_slot();
for (i = 0; i < total_nibbles; ++i) {
nibble = 0;
for (b = 0; b < 4; ++b) {
if (sample_slot()) nibble |= (1 << (3-b));
}
store_nibble(nibble);
}
validate_checksum();
}
Examples and use cases
- Boot failure: payload = {0x01, boot_step_code, retry_count}
- Sensor error: payload = {0x02, sensor_id, error_code}
- Config checksum: payload = {0x03, config_crc_high, config_crc_low}
Encode short human messages by mapping ASCII (subset) to nibbles: pack the upper 4 bits of a character, then the lower 4 bits. For a one‑LED debug during field service you can flash a 6‑character code (e.g., "bldg01") that an engineer can read or a smartphone photodiode can capture.
Power and reliability
LED toggling consumes current. To avoid draining a battery or preventing brownout recovery, limit BlinkSpeak activity to short windows: during boot, after a crash, or when a dedicated error flag is set. Use watchdogs: if power is unstable the transmitter will likely be interrupted; the start sequence and checksum allow receivers to ignore partial transmissions.
For ultra low power, use a 1kHz timer interrupt and only sample or drive the LED while sending; otherwise keep LED pin low and the MCU in sleep. If you expect a stuck‑on LED due to firmware bugs, add a hardware RC to pull the LED off unless actively driven.
Troubleshooting and tips
- If messages are not decoded, increase T by 25% — timing jitter and sampling latency are the usual suspects.
- Use a cheap photodiode + op amp as a capture front end for automated decoding. A fast ADC or interrupt on threshold makes decoding trivial.
- Watch for ambient flicker (LED mains lamps). Implement a short preamble with three identical start pulses to improve alignment.
- Log message types and frequencies in a small counter store so repeated errors can be summarized instead of flooding the LED.
Wrap up
BlinkSpeak gives you structured diagnostics without extra hardware or pins. It trades bandwidth for universality: one LED and a little timing discipline yields messages you can read by eye or capture automatically. The implementation fits into a few dozen lines of C, survives interrupted sends, and is low enough complexity to include in boot paths and watchdog handlers. Next time your tiny board refuses to boot, let it speak.