Bus Pirate 6 · Volume 10

Bus Pirate 6 Volume 10 — Binary Mode and Python Automation: BBIO Legacy and BPIO2 (FlatBuffers + COBS)

Host-side scripting — when to use BBIO (flashrom/avrdude compat) vs BPIO2 (new work), Python bindings, automated test rigs

Contents

SectionTopic
1About this volume
2Why binary mode at all (vs terminal-mode automation)
3Legacy BBIO protocol
· 3.1Entering BBIO: the 0x00 × 20 sequence → BBIO1 banner
· 3.2Mode selection: 0x01 SPI, 0x02 I²C, 0x03 UART, 0x04 1-Wire, 0x05 raw
· 3.3Per-mode framing
· 3.4pyBusPirateLite — the canonical Python client
· 3.5flashrom + avrdude integration
· 3.6When to use BBIO (legacy-tool compatibility)
4BPIO2 protocol (modern, FlatBuffers + COBS)
· 4.1Entering BPIO2: binmode → option 2
· 4.2The secondary USB CDC port
· 4.3The FlatBuffers schema (bpio.fbs)
· 4.4COBS framing
· 4.5StatusRequest / ConfigurationRequest / DataRequest
· 4.6Multi-language bindings (Python, Rust, C/C++)
· 4.7When to use BPIO2 (new work)
5Python automation patterns
· 5.1Open serial / claim BPIO2 mode
· 5.2Bulk flash dump script
· 5.3I²C bus scanner
· 5.4Parametric voltage / current sweep
6Building automated test rigs
· 6.1Production-line GO/NO-GO patterns
· 6.2Continuous integration / unattended fixtures
7Comparison to direct USB-CDC scripting on a Pico
8SUMP, FALA, IR Toy — the other binary protocols
9Cheatsheet updates for Vol 12

1. About this volume

The BP6 has two parallel binary-protocol stacks for host-side automation: BBIO (legacy, 2008-era, single-byte commands) and BPIO2 (modern, 2024-era, FlatBuffers over COBS). Both are accessible via the binmode command at the CLI. This volume documents both, when to pick which, and walks the Python-binding workflow for each.

The choice between them is real and consequential: BBIO is compatibility-required for legacy tools (flashrom, avrdude, pyBusPirateLite client) — you can’t use BPIO2 with those. BPIO2 is the right path for new code — type-safe schema, multi-language bindings, clean error handling. If you’re writing scripts that will only ever be used in-house, start with BPIO2 and ignore BBIO.

The BP5 / BP5XL / BP6 all support both protocols identically — same firmware tree, same binmode menu. So Python scripts you write here run on any Bus Pirate 5+.


2. Why binary mode at all (vs terminal-mode automation)

You can drive the BP6 from a script by speaking its text CLI directly over USB-CDC — open /dev/ttyACM0 in your script, write [0x90 r:4]\n, read back WRITE: 0x90 ACK\nREAD: 0x12 0x34 0x56 0x78\n, parse the text. This works and is the path of least friction for one-off scripts.

It also breaks easily:

  • Text parsing is fragile. The output format changes between firmware versions (the o command tunes it; the user’s terminal palette affects color codes; new fields get added). Your scraper code is hostage to those changes.
  • No type safety. Everything is a string until you parse it; a missed delimiter or unexpected log line crashes your code.
  • Verbose over the wire. “WRITE: 0x90 ACK\n” is 15 bytes to convey “1 byte was written with acknowledgment” — fine for an interactive session, slow for thousands of operations per second.
  • No structured error handling. You learn the operation failed by not seeing the expected pattern in output — a timeout you have to handle manually.

Binary mode fixes all four: typed commands, typed responses, compact framing, explicit error codes. Worth the small upfront cost of switching to binary for any automation that runs more than once.


3. Legacy BBIO protocol

The original Bus Pirate binary protocol, designed in the late 2000s for the BP3 generation. Preserved in the BP6 firmware for compatibility with the long tail of tools that speak it.

3.1 Entering BBIO: the 0x00 × 20 sequence → BBIO1 banner

The BBIO entry sequence is a magic byte pattern: send 0x00 twenty or more times. The BP6 firmware’s USB-CDC handler watches for this pattern; on detection, it switches the CDC channel from text-CLI mode to BBIO binary mode and emits the banner string BBIO1 (5 bytes: ASCII ‘B’, ‘B’, ‘I’, ‘O’, ‘1’).

# Pseudocode
ser.write(b'\x00' * 20)
banner = ser.read(5)
assert banner == b'BBIO1'
# Now in BBIO binary mode.

This entry sequence is the same on BP5 / BP5XL / BP6 — it works on any Bus Pirate 5+ unchanged. Same as on BP3.6, even — the legacy compat layer is faithful.

3.2 Mode selection: 0x01 SPI, 0x02 I²C, 0x03 UART, 0x04 1-Wire, 0x05 raw

Once in BBIO root, single-byte commands select a mode:

ByteModeBanner returned
0x01SPISPI1
0x02I²CI2C1
0x03UARTART1
0x041-Wire1W01
0x05Raw (DIO)RAW1
0x0FExit BBIO, back to text-CLI(no banner; emits “ASCII\n” or similar)

Each mode has its own command space — sending 0x01 after BBIO1 enters SPI binary mode, where subsequent bytes are SPI-specific commands.

3.3 Per-mode framing

Inside each mode, single-byte commands trigger operations:

SPI mode (after 0x01):

  • 0x02 — chip-select low (CS asserted)
  • 0x03 — chip-select high
  • 0x04 — read 1 byte from MISO (then read response byte)
  • 0x05 — send bulk SPI (high nibble = 0001, low nibble = N-1 byte count; followed by N bytes; then read N response bytes)
  • Multi-byte commands for clock speed, mode (0/1/2/3), and other config

I²C mode (after 0x02):

  • 0x02 — START
  • 0x03 — STOP
  • 0x04 — read 1 byte (ACK)
  • 0x06 — bulk write (similar to SPI bulk)
  • Variants for repeat START, NACK after read, etc.

The per-mode framing is documented in detail at dangerousprototypes.com/docs/Bitbang (the original wiki, still authoritative for BBIO). It’s small and learnable — about 30 distinct commands across all modes.

3.4 pyBusPirateLite — the canonical Python client

The reference Python implementation: pyBusPirateLite (originally by Sébastien Bourdeauducq, maintained since 2010+ by various contributors).

Install: pip install pyBusPirateLite. Use:

from pyBusPirateLite.SPI import SPI

bp = SPI('/dev/ttyACM0', 115200)
bp.pins = SPI.PIN_POWER | SPI.PIN_CS
bp.config = SPI.CFG_PUSH_PULL | SPI.CFG_CLK_EDGE
bp.speed = '1MHz'

bp.cs_low()
response = bp.transfer([0x9F, 0x00, 0x00, 0x00])  # JEDEC ID read
bp.cs_high()

print(f"JEDEC ID: {response[1]:02X} {response[2]:02X} {response[3]:02X}")

The library handles the BBIO entry sequence, mode selection, and per-mode commands transparently — you write Python that looks like SPI library code, the library translates to BBIO bytes.

Works on BP3.6, BP4, BP5, BP5XL, BP6 unchanged — the BBIO compat layer is universal.

3.5 flashrom + avrdude integration

The two big legacy tools that speak BBIO:

flashrom uses BBIO via the buspirate_spi programmer:

flashrom --programmer buspirate_spi:dev=/dev/ttyACM0,spispeed=4M -r dump.bin

flashrom negotiates BBIO, switches to SPI mode, talks to the target. Works on every Bus Pirate from BP3 onward.

avrdude uses BBIO for ISP programming of AVR microcontrollers:

avrdude -p atmega328p -c buspirate -P /dev/ttyACM0 -U flash:r:dump.hex:i

Same pattern — avrdude knows the buspirate programmer driver internally.

These two tools alone are reason enough to keep BBIO in the BP6 firmware. The community spent 15+ years building tooling around it; deprecating BBIO would break that ecosystem.

3.6 When to use BBIO (legacy-tool compatibility)

BBIO is the right answer when:

  • You need flashrom. Specifically, flashrom’s broader chip database, coreboot integration, or write-protect-register tooling.
  • You need avrdude. ISP programming of classic AVRs.
  • You’re using pyBusPirateLite or another long-established BBIO Python library. Migrating those scripts to BPIO2 is sometimes more work than it’s worth for simple use cases.
  • You’re working with documentation written before 2024. Every BP tutorial older than the BP5 (2024) targets BBIO; copying example code is faster than rewriting.

For any of those: BBIO works fine, is well-tested, and is going nowhere.


4. BPIO2 protocol (modern, FlatBuffers + COBS)

The 2024-era protocol designed alongside the BP5 launch. Type-safe, multi-language, well-structured. The right path for new work.

4.1 Entering BPIO2: binmode → option 2

At the BP6 CLI:

HiZ> binmode
Binary mode menu:
1. BBIO (legacy)
2. BPIO2 (modern FlatBuffers + COBS)
3. SUMP (Sigrok logic-analyzer protocol)
4. FALA (FlashROM Asynchronous Logic Analyzer)
5. IR Toy compat

Select [1-5]: 2

BPIO2 mode active on USB-CDC port 2 (this port stays at CLI).

The BP6 enumerates two USB-CDC ports at the OS level (/dev/ttyACM0 and /dev/ttyACM1 on Linux; COM<N> and COM<N+1> on Windows). The first is the text CLI; the second is reserved for binary protocols. Entering BPIO2 mode makes the second port speak BPIO2.

This means you can have a terminal session on the first port AND a Python script speaking BPIO2 on the second port at the same time. They don’t conflict.

4.2 The secondary USB CDC port

The dual-port architecture is essential — it’s what allows the BP6’s CLI to stay responsive while a host-side automation script is running.

In practice:

  • Terminal session on /dev/ttyACM0 — interactive use, status display.
  • Python BPIO2 client on /dev/ttyACM1 — automation.

The BP6 firmware coordinates the two: when BPIO2 is active, the LCD and status bar reflect what the BPIO2 client is doing (active mode, current pin states, etc.) so you can watch the automation from the CLI side without disrupting it.

4.3 The FlatBuffers schema (bpio.fbs)

BPIO2 messages are FlatBuffers — a compact, schema-defined binary serialization format. The schema lives at bpio.fbs in the firmware repo:

github.com/DangerousPrototypes/BusPirate-BPIO2-flatbuffer-interface/blob/main/bpio.fbs

This separate repo holds:

  • The .fbs schema definition
  • Pre-generated bindings for Python, Rust, C/C++ (the FlatBuffers compiler produces these)
  • Example code

FlatBuffers’ value proposition over JSON: zero-copy parsing (the wire bytes ARE the data structure, no intermediate parse step), schema-enforced types (compile-time validation), and small wire size (no field name strings; types are positional).

For our purposes: when you receive a FlatBuffers message in Python, you access it as a Python object. The library handles the encoding/decoding transparently.

4.4 COBS framing

FlatBuffers messages have variable length, so over a byte-stream like USB-CDC, you need a framing protocol to delimit them. BPIO2 uses COBS (Consistent Overhead Byte Stuffing) — a classic algorithm that takes an arbitrary byte sequence and encodes it such that the byte 0x00 never appears in the encoded output. The 0x00 then serves as a frame delimiter.

Encoding overhead: roughly 1 byte per 254 bytes of payload — minimal.

In Python, the cobs-python library handles this transparently:

import cobs.cobs as cobs

# Encode for transmission
framed = cobs.encode(flatbuf_bytes) + b'\x00'

# Decode on receive
raw_flatbuf = cobs.decode(framed_without_terminator)

You don’t typically touch COBS directly — the BPIO2 Python bindings wrap it.

4.5 StatusRequest / ConfigurationRequest / DataRequest

BPIO2 has three request types (and matching response types):

  • StatusRequestStatusResponse — query firmware version, current mode, pin states, PSU state, etc. Read-only.
  • ConfigurationRequestConfigurationResponse — change settings. Enter / exit mode, set PSU voltage, enable pull-ups, configure protocol parameters.
  • DataRequestDataResponse — execute a protocol operation. Write bytes, read bytes, run a complete bracket transaction.

Each request type has fields for the relevant operation. For example, a DataRequest in SPI mode might be:

{
    operation: "transaction",
    chip_select: true,    // assert CS
    write_data: [0x9F, 0x00, 0x00, 0x00],
    read_count: 4,
    chip_select_after: false  // deassert CS at end
}

The response:

{
    success: true,
    read_data: [0xFF, 0xEF, 0x40, 0x18],   // JEDEC ID + dummy
    error: null
}

Structured. Type-safe. Easy to script.

4.6 Multi-language bindings (Python, Rust, C/C++)

The pre-generated bindings repo includes:

  • Python: bpio_py/ — uses the flatbuffers Python package. Install via pip.
  • Rust: bpio_rs/ — Cargo crate. Targets no_std for embedded use cases.
  • C/C++: bpio_cpp/ — header-only library. Includes COBS encoding helpers.

Other languages: any language with a FlatBuffers compiler (Java, C#, Go, JavaScript, Swift) can generate bindings from the bpio.fbs schema. The wire protocol is language-agnostic.

4.7 When to use BPIO2 (new work)

BPIO2 is the right path when:

  • You’re writing new automation from scratch. No legacy compat constraint.
  • You want type safety / compile-time error checking. FlatBuffers schemas catch field-name typos at compile time, not at runtime when the device returns “unexpected response.”
  • You’re writing in Rust or C/C++. The multi-language bindings are first-class.
  • You’re building a production test rig that’ll run thousands of times. The structured protocol is robust across firmware updates (the schema is versioned; breaking changes are explicit).
  • You want to read pin state in real time (the parallel-tap logic-analyzer feature; § Vol 9 § 3.2 walks this).

When BPIO2 is not the right answer:

  • One-off script you’ll never reuse. Just speak the text CLI — fewer dependencies, faster prototyping.
  • Existing legacy library you trust. Don’t rewrite working code.

5. Python automation patterns

The canonical idioms.

5.1 Open serial / claim BPIO2 mode

import serial
from bpio2 import BPIO2Client  # from the pre-generated bindings

# Open serial to the BP6's second CDC port
ser = serial.Serial('/dev/ttyACM1', 115200, timeout=1)

# Wrap in BPIO2 client
client = BPIO2Client(ser)

# Verify we're talking to a BP6
status = client.status()
print(f"Firmware: {status.firmware_version}")
print(f"Board: {status.board}")
assert status.board == 'BP6_REV2'

The client handles the COBS framing + FlatBuffers encoding/decoding internally. From the script’s perspective, you just call methods.

5.2 Bulk flash dump script

def dump_spi_flash(client, output_path, chip_size_bytes, page_size=256):
    """Dump a SPI flash chip via BPIO2."""

    # Enter SPI mode
    client.set_mode('SPI', cpol=0, cpha=0, speed='4MHz')

    # Power the chip
    client.set_psu(3.3, enabled=True)

    with open(output_path, 'wb') as f:
        bytes_read = 0
        while bytes_read < chip_size_bytes:
            # Each iteration reads up to 256 bytes (page size) starting
            # at bytes_read.
            addr_bytes = bytes_read.to_bytes(3, 'big')
            response = client.spi_transaction(
                write_data=[0x03] + list(addr_bytes),  # SPI READ command
                read_count=page_size,
                chip_select_pre=True,
                chip_select_post=False
            )
            f.write(response.read_data)
            bytes_read += len(response.read_data)
            print(f"\r{bytes_read}/{chip_size_bytes} bytes...", end='')

    print(f"\nDone. Wrote {bytes_read} bytes to {output_path}")
    client.set_psu(0, enabled=False)

# Use:
client = BPIO2Client(serial.Serial('/dev/ttyACM1', 115200))
dump_spi_flash(client, 'dump.bin', 16 * 1024 * 1024)  # 16 MB chip

A few hundred lines of equivalent BBIO code; ~30 lines of BPIO2 Python.

5.3 I²C bus scanner

def scan_i2c_bus(client):
    """Scan I²C addresses 0x00-0x7F, return list of ACKing addresses."""

    client.set_mode('I2C', speed='100kHz', pullups=True)
    found = []

    for addr in range(0x00, 0x80):
        response = client.i2c_transaction(
            address=(addr << 1) | 0,  # write
            write_data=[],
            read_count=0,
            stop_at_end=True
        )
        if response.success:
            found.append(addr)

    return found

# Use:
addresses = scan_i2c_bus(client)
print(f"Found {len(addresses)} I²C devices:")
for a in addresses:
    print(f"  0x{a:02X}")

The BP6 firmware has a scan CLI command (Vol 6 § 4.4); this is the same logic from a script.

5.4 Parametric voltage / current sweep

The kind of analysis a Bus Pirate is well-suited for: characterize a target’s voltage / current curve across a sweep.

def sweep_voltage(client, v_start, v_stop, v_step):
    """Sweep PSU voltage and record current draw of attached target."""
    results = []

    for v in [v_start + i*v_step for i in range(int((v_stop - v_start) / v_step) + 1)]:
        client.set_psu(v, enabled=True)
        time.sleep(0.1)  # let things settle

        status = client.status()
        results.append({
            'voltage_set': v,
            'voltage_measured': status.psu_voltage,
            'current_ma': status.psu_current_ma,
        })
        print(f"V={v:.2f} → measured {status.psu_voltage:.3f} V, "
              f"current {status.psu_current_ma:.1f} mA")

    return results

# Use:
sweep = sweep_voltage(client, 1.8, 5.0, 0.1)
# Save sweep to CSV, plot in matplotlib, characterize the target.

This is exactly the kind of “I want to know exactly how my circuit behaves” workflow that’s tedious to do by hand but trivial to script.


6. Building automated test rigs

6.1 Production-line GO/NO-GO patterns

A bench setup that tests every unit coming off an assembly line:

def test_unit():
    """Per-unit test sequence. Returns PASS/FAIL + log."""

    log = []

    # 1. Power the unit
    client.set_psu(3.3, enabled=True)
    time.sleep(0.5)

    # 2. Check current draw is in expected range
    status = client.status()
    if not 5 < status.psu_current_ma < 50:
        return ('FAIL', f"Current out of range: {status.psu_current_ma} mA")

    # 3. Read board's I²C config EEPROM
    client.set_mode('I2C', pullups=True)
    response = client.i2c_transaction(
        address=0xA0,
        write_data=[0x00, 0x00],
        read_count=4,
        stop_at_end=True
    )
    if not response.success:
        return ('FAIL', "EEPROM not responding")

    config = response.read_data
    if config[0] != 0xAA or config[1] != 0x55:
        return ('FAIL', f"Invalid magic bytes: {config[0]:02X} {config[1]:02X}")

    serial_num = (config[2] << 8) | config[3]

    # 4. Verify boot-firmware checksum via SPI flash
    client.set_mode('SPI', cpol=0, cpha=0, speed='1MHz')
    # ... read first page, verify magic, etc.

    # 5. Power down
    client.set_psu(0, enabled=False)

    return ('PASS', f"S/N {serial_num} verified")

# Loop:
while True:
    input("Press Enter when next unit is connected...")
    result, msg = test_unit()
    print(f"  {result}: {msg}")

This pattern scales: 50 tests per shift, 250 units per day, all logged to CSV. The BP6 is the right tool for small-batch / pilot-line test work where a dedicated programmer is overkill but manual testing is too slow.

6.2 Continuous integration / unattended fixtures

Long-running automated fixtures: a board sits permanently connected to the BP6, the BP6 connects to a build server, every new firmware commit triggers a flash + test sequence.

# CI pipeline pseudo-code (GitHub Actions or similar)
- name: Flash device
  run: |
    python3 -m bp6.flash_and_test \
        --binary build/firmware.bin \
        --serial /dev/ttyACM1

- name: Run integration tests
  run: |
    python3 -m bp6.integration_tests \
        --board /dev/ttyACM1 \
        --report-junit results.xml

- name: Publish results
  uses: junit-publisher@v1
  with:
    junit: results.xml

The CI workflow attaches a BP6 to the build server, mounts it as a tty, and integration tests run against real silicon. BP6 cost (~$80) is trivial compared to the value of catching firmware regressions on real hardware before they ship.


7. Comparison to direct USB-CDC scripting on a Pico

A reasonable question: why use a BP6 at all? Why not just connect a Raspberry Pi Pico (RP2040) directly to the target via USB-CDC, with custom firmware that exposes the same operations?

When the BP6 wins:

  • No firmware to write. The BP6 firmware already does SPI, I²C, JTAG, etc. with hardened code, battle-tested PIO programs, well-defined protocols.
  • Built-in PSU + voltage measurement + pull-ups + level shifters. A bare Pico has none of these — you’d need to add per-pin level shifters ($10 of parts) + a programmable buck regulator ($5) + current shunt + an op-amp (~$2) + ADC support.
  • Standard protocols (BBIO + BPIO2) the ecosystem already knows. Custom Pico firmware = custom client library.
  • The look-behind buffer for parallel logic-analyzer capture (BP6-only).

When a custom-firmware Pico wins:

  • Custom protocols the BP6 doesn’t support (CAN bus, EthernetCAT, USB-C PD, etc.).
  • High-speed I/O beyond what the BP6’s bit-banged drivers can do.
  • Specialty hardware (a Pico can also drive an SD card directly, drive a stepper motor, etc.).
  • Standalone deployment — a Pico can run untethered; a BP6 needs a host for anything beyond its on-board NAND scripts.

For embedded-protocol bring-up and reverse engineering, the BP6 is the answer. For purpose-built test fixtures with custom protocols, a Pico with bespoke firmware fits better.


8. SUMP, FALA, IR Toy — the other binary protocols

The binmode menu has three other entries beyond BBIO and BPIO2:

SUMP

The Sigrok SUMP protocol — the Open Workbench Logic Sniffer / Sigrok logic-analyzer wire protocol. Speaking SUMP lets the BP6 act as a logic analyzer for PulseView (Vol 9 § 3.2). 8 channels of capture at up to 100 Msps burst, exfiltrated as a SUMP-protocol byte stream.

Used in the parallel-logic-analyzer workflow (Vol 9 § 3).

FALA

FlashROM Asynchronous Logic Analyzer — an extension protocol that flashrom can use to capture SPI flash traffic while it’s actively reading. Mostly redundant with SUMP+PulseView but kept for tooling that specifically expects FALA.

IR Toy

The original Dangerous Prototypes IR Toy was a small USB device for IR send/receive. The BP6 firmware includes an IR Toy compat layer that lets it speak the same wire protocol — so LIRC and other IR Toy-aware software work with the BP6 unchanged.


9. Cheatsheet updates for Vol 12

Items for the laminate cheatsheet:

  • BBIO entry: send 0x00 × 20 on first USB-CDC port; expect BBIO1 banner.
  • BBIO mode selection: 0x01 SPI / 0x02 I²C / 0x03 UART / 0x04 1-Wire / 0x05 raw. 0x0F to exit.
  • flashrom: flashrom --programmer buspirate_spi:dev=/dev/ttyACM0,spispeed=4M -r dump.bin
  • avrdude: avrdude -p atmega328p -c buspirate -P /dev/ttyACM0 -U flash:r:dump.hex:i
  • BPIO2 entry: binmode at CLI → option 2; switches the second USB-CDC port to BPIO2.
  • BPIO2 = FlatBuffers + COBS over the second USB-CDC port.
  • BPIO2 schema: bpio.fbs at github.com/DangerousPrototypes/BusPirate-BPIO2-flatbuffer-interface.
  • Three BPIO2 request types: StatusRequest, ConfigurationRequest, DataRequest.
  • Two USB-CDC ports: first = CLI, second = binary protocols. They don’t conflict.
  • SUMP for PulseView: binmode → option 3.
  • Use BPIO2 for new work; BBIO for legacy tools (flashrom, avrdude, pyBusPirateLite).

End of Volume 10. Volume 11 picks up with building the firmware from source, writing custom modes, the RP2350 errata-E9 mitigation, the debugger comparison, and operational hygiene for the BP6 on the bench.