Bus Pirate 6 · Volume 11

Bus Pirate 6 Volume 11 — Building from Source, Custom Firmware, and Operational Hygiene

Pico SDK + Docker build, custom modes, RP2350 errata E9 mitigation, debugger comparison (BMP/J-Link/picoprobe), bench discipline

Contents

SectionTopic
1About this volume
2Setting up the build environment
· 2.1Pico SDK version pinning
· 2.2ARM GCC version
· 2.3Docker compose hermetic build
· 2.4Windows: the pico-sdk submodule gotcha
3Building each target
· 3.1The bus_pirate6_rev2 CMake target
· 3.2Build artifacts and UF2 output
· 3.3Verifying the build before flashing
4Writing a custom mode
· 4.1The mode vtable contract
· 4.2Adding a .pio program
· 4.3Registering with the mode menu
· 4.4Per-mode commands
· 4.5Worked example: a hypothetical CAN-bus mode skeleton
5RP2350 errata E9 — GPIO pulldown latch
· 5.1The hardware-level bug
· 5.2The 100 kΩ external pulldown workaround on BP6
· 5.3When external resistors are needed for JTAG/SWD targets
6Debugger comparison
· 6.1Bus Pirate 6 vs Black Magic Probe
· 6.2Bus Pirate 6 vs Segger J-Link
· 6.3Bus Pirate 6 vs picoprobe / CMSIS-DAP
· 6.4When the Bus Pirate is the right answer
7Operational hygiene
· 7.1Start in HiZ
· 7.2Voltage-check the target
· 7.3Disconnect probes between mode changes
· 7.4When to suspect the level translator
· 7.5Bench-fixture practices
8Legal and ethical posture
· 8.1Inheriting from _shared/legal_ethics.md
· 8.2The chip-dumping legal landscape
9When NOT to use the Bus Pirate 6
10Cheatsheet updates for Vol 12

1. About this volume

This is the synthesis volume — bringing together everything from Vols 2-10 with a focus on (a) extending the BP6 (custom firmware), (b) using it well (operational hygiene), and (c) knowing when it’s the wrong tool. Read this when you’re going to write firmware modifications, when you’ve had a frustrating session and want to know what went wrong, or when you’re deciding between the BP6 and a dedicated debugger for a specific task.


2. Setting up the build environment

The Bus Pirate firmware (Vol 3 § 2) builds with the standard Raspberry Pi Pico SDK toolchain — CMake + ARM GCC. Nothing exotic, but the submodule and pinning details matter.

2.1 Pico SDK version pinning

The firmware repo pins a specific Pico SDK version via a git submodule. After cloning:

git clone --recurse-submodules https://github.com/DangerousPrototypes/BusPirate5-firmware
cd BusPirate5-firmware
git submodule status

You should see the pico-sdk submodule checked out at a specific commit. Don’t update it independently — the firmware tracks a known-good SDK version. The maintainer bumps it when needed.

If --recurse-submodules was missed:

git submodule update --init --recursive

This is essential — the pico-sdk has its own submodules (TinyUSB, mbedTLS, lwIP). All three layers must check out cleanly.

2.2 ARM GCC version

The firmware compiles cleanly against any recent arm-none-eabi-gcc:

  • Linux: apt install gcc-arm-none-eabi (Debian/Ubuntu) or dnf install arm-none-eabi-gcc-cs (Fedora).
  • macOS: brew install --cask gcc-arm-embedded (Homebrew) or directly from ARM’s website.
  • Windows: download installer from developer.arm.com/downloads/-/arm-gnu-toolchain-downloads. Tested versions: 10.x, 11.x, 12.x, 13.x.

The Pico SDK picks up arm-none-eabi-gcc from PATH. As long as the binary is on PATH and a recent version, you’re set.

CMake: 3.13 or newer. Any modern Linux/Mac install satisfies; Windows users may need to install separately from cmake.org.

2.3 Docker compose hermetic build

The hassle-free path. The firmware repo includes docker-compose.yml:

docker-compose run --rm build

Pre-built container with pinned Pico SDK + ARM GCC + CMake. The container mounts the repo, runs the build, drops the output .uf2 files in build/, and exits.

Build time: ~3-5 minutes on a modern laptop (first run downloads the container image; subsequent runs are cached).

Use this on Linux/macOS where Docker is installed. On Windows: same Docker Desktop, same command — works identically.

This is the recommended path for anyone who doesn’t already have ARM GCC + CMake + Pico SDK installed locally. Don’t fight the toolchain; let the container do its job.

2.4 Windows: the pico-sdk submodule gotcha

For Windows users not using Docker, there’s a long-standing pico-sdk submodule resolution issue. The pico-sdk/lib/tinyusb/ submodule sometimes fails to fetch on Windows due to Git’s handling of nested LFS-tracked files. Symptom: build fails with “tinyusb/src/tusb.h: file not found.”

Workarounds:

  1. Use Docker compose (§ 2.3) — the container has the submodules pre-resolved.
  2. WSL2 + Linux toolchain — same as Linux from inside WSL.
  3. Manual submodule fetch from Git Bash:
cd pico-sdk
git submodule update --init --recursive

If that fails, clone tinyusb separately and copy it into the expected path:

cd pico-sdk/lib
git clone https://github.com/hathach/tinyusb.git

This is a known Pico-SDK-on-Windows issue, not a Bus-Pirate-specific issue. The firmware build inherits whatever the pico-sdk team has fixed.


3. Building each target

3.1 The bus_pirate6_rev2 CMake target

From the repo root:

mkdir build_rp2350
cd build_rp2350
cmake .. -DBUILD_TARGET=bus_pirate6_rev2
make -j$(nproc)

That builds the BP6 firmware. The -DBUILD_TARGET flag picks which board variant (Vol 3 § 7) — bus_pirate6_rev2 is the current BP6.

For other targets:

  • bus_pirate5_rev10 — production BP5 (RP2040)
  • bus_pirate5_xl — BP5XL (RP2350A)
  • bus_pirate5_rev8 — engineering-sample BP5 (rare)

The build trees are kept separate (build_rp2040/ vs build_rp2350/) because the Pico SDK is configured differently per MCU family.

3.2 Build artifacts and UF2 output

After a successful build:

build_rp2350/
├── bus_pirate6_rev2.elf      ← linked binary with debug symbols
├── bus_pirate6_rev2.uf2      ← the file you flash
├── bus_pirate6_rev2.bin      ← raw binary (rarely needed)
├── bus_pirate6_rev2.hex      ← Intel HEX format (some tools want this)
└── bus_pirate6_rev2.dis      ← disassembly (debugging aid)

The .uf2 is what you drop onto the BP6’s BOOTSEL mass-storage drive (Vol 1 § 6.1 for entry, Vol 4 § 2.4 for the $ bootloader command).

3.3 Verifying the build before flashing

Quick sanity checks before flashing your custom build:

  1. .uf2 size sanity — should be approximately the same size as the official auto-build UF2 for the same target (off by no more than ~30% means something’s badly wrong).

  2. .elf symbol checkarm-none-eabi-nm to confirm key symbols exist:

    arm-none-eabi-nm build_rp2350/bus_pirate6_rev2.elf | grep -E "(syntax_run|hw_spi_init|main)"

    If any are missing, the build linked something wrong.

  3. Memory map — confirm .text size + RAM usage fits within RP2350 limits:

    arm-none-eabi-size build_rp2350/bus_pirate6_rev2.elf

    RP2350 has 520 KB SRAM; your .bss + .data should be well under that.

If the .uf2 looks sane, flash it. The BOOTSEL is mask-ROM (Vol 1 § 6.1) — a bad UF2 produces a red blink, not a bricked device. You can re-flash with the official auto-build UF2 to recover.


4. Writing a custom mode

If the BP6’s stock 12 modes don’t cover your target protocol, you can add a new one. The mode vtable contract (Vol 3 § 5) is small enough that custom modes are a weekend project, not a months-long undertaking.

4.1 The mode vtable contract

A new mode is a C file in src/mode/ that exposes:

struct mode_t {
    const char *name;          // "CAN", "FOO", etc.
    void (*setup)(void);       // entered mode; init hardware
    void (*cleanup)(void);     // exiting mode; tear down
    void (*start)(void);       // BC_START opcode
    void (*stop)(void);        // BC_STOP opcode
    uint32_t (*write)(uint32_t data, uint8_t bits);  // write n bits
    uint32_t (*read)(uint8_t bits);                  // read n bits
    void (*clkh)(void);        // explicit clock-high
    void (*clkl)(void);        // explicit clock-low
    void (*dath)(void);        // explicit data-high
    void (*datl)(void);        // explicit data-low
    int  (*period)(void);      // period query/set
    const command_t *commands; // per-mode commands array
};

You don’t have to implement every callback — the runner only calls those that match opcodes in the user’s syntax line. Implement what makes sense for your protocol.

4.2 Adding a .pio program

If your protocol benefits from PIO timing (most do — see Vol 6 § 5 for which BP modes use PIO), write a .pio file at src/pirate/:

.program my_protocol
    set pindirs, 1     ; set pin direction to output
    set pins, 1 [3]    ; drive high, 3-cycle delay
    set pins, 0 [3]    ; drive low, 3-cycle delay
    ; ... your protocol's bit pattern ...

The Pico SDK’s pioasm tool (invoked automatically from CMake) converts this to a C header with the opcode array. Your mode’s setup() then loads it:

void my_setup(void) {
    PIO pio = pio2;
    int sm = pio_claim_unused_sm(pio, true);
    uint offset = pio_add_program(pio, &my_protocol_program);

    pio_sm_config c = my_protocol_program_get_default_config(offset);
    // ... configure pins, clock divider, FIFO ...
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
}

The pio_claim_unused_sm ensures you grab an unused state machine slot — important since the BP6 may be running other PIO-driven features (look-behind sampler on PIO 2, LED chain on PIO 0, etc.).

4.3 Registering with the mode menu

In src/modes.c (or wherever the mode table lives — check the current source layout):

static const struct mode_t mode_my_protocol = {
    .name = "MY_PROTO",
    .setup = my_setup,
    .cleanup = my_cleanup,
    .start = my_start,
    // ... other callbacks ...
    .commands = my_protocol_commands,
};

const struct mode_t *available_modes[] = {
    &mode_hiz,
    &mode_1wire,
    &mode_uart,
    // ... existing modes ...
    &mode_my_protocol,    // ← your new mode
};

Now m at the prompt shows MY_PROTO in the list, and selecting it enters your mode.

4.4 Per-mode commands

Beyond the vtable callbacks, your mode can expose commands (the things users type at the mode-specific prompt, like flash or eeprom or bluetag):

static int cmd_my_action(int argc, char **argv) {
    // implement the command
    printf("Running my custom command...\n");
    return 0;
}

const command_t my_protocol_commands[] = {
    { .name = "myaction", .handler = cmd_my_action,
      .help = "do my custom thing" },
    { NULL, NULL, NULL }  // sentinel
};

Now myaction at your mode’s prompt invokes cmd_my_action.

4.5 Worked example: a hypothetical CAN-bus mode skeleton

CAN isn’t currently a stock BP6 mode (you’d need a CAN transceiver IC between the BP6’s TTL signals and an actual CAN bus, but the protocol-layer work would be a custom mode). Skeleton:

// src/mode/can.c
#include "can.pio.h"
#include "mode.h"

static PIO can_pio;
static int can_sm;

void can_setup(void) {
    can_pio = pio2;
    can_sm = pio_claim_unused_sm(can_pio, true);
    uint offset = pio_add_program(can_pio, &can_program);

    pio_sm_config c = can_program_get_default_config(offset);
    // pin config, clock divider for 500 kbps...
    pio_sm_init(can_pio, can_sm, offset, &c);
    pio_sm_set_enabled(can_pio, can_sm, true);
}

uint32_t can_write(uint32_t data, uint8_t bits) {
    // Build CAN frame: SOF, ID, control, data, CRC, ACK, EOF
    // Push bits into PIO TX FIFO
    pio_sm_put_blocking(can_pio, can_sm, data);
    return 0;  // CAN doesn't have a per-byte ACK in the bit-level sense
}

uint32_t can_read(uint8_t bits) {
    // Pull from PIO RX FIFO
    return pio_sm_get_blocking(can_pio, can_sm);
}

const struct mode_t mode_can = {
    .name = "CAN",
    .setup = can_setup,
    .cleanup = can_cleanup,
    .write = can_write,
    .read = can_read,
    .commands = can_commands,
};

Then register mode_can in available_modes[] (§ 4.3). Done.

In practice, CAN’s complex framing (bit stuffing, CRC, retry on collision) means the PIO program is substantial — a real CAN mode is a multi-week project. But the architecture for adding it is exactly the skeleton above.


5. RP2350 errata E9 — GPIO pulldown latch

A silicon-level bug in the RP2350 family that the BP6 hardware mitigates. Worth understanding because it affects how you reason about the BP6’s pin behavior.

5.1 The hardware-level bug

The RP2350’s internal GPIO pulldowns latch as bus-keepers under certain conditions. Specifically: when a pulldown is enabled and the pin is being driven, then released to high-impedance, the internal logic that should re-engage the pulldown sometimes doesn’t release — it acts as a weak bus-keeper instead, holding whatever level the pin was last driven to.

For most applications this is invisible. For some — JTAG / SWD targets that distinguish “no chip is connected” by sensing a floating-high SWDIO line — it matters.

Detail: Raspberry Pi’s RP2350 errata documentation, item E9. Affects all RP2350A and RP2350B silicon revisions through (at least) 2026; no respin has been announced.

5.2 The 100 kΩ external pulldown workaround on BP6

The BP6 design hardware-mitigates this with 100 kΩ external pulldowns on each IO pin. These external resistors don’t have the bus-keeper-latching behavior of the internal pulldowns — they’re plain passive components.

In firmware, the BP6 also avoids relying on the internal pulldowns for any timing-critical pin discipline. The internal pulldowns may still be enabled in some configurations (the firmware leaves them set per-mode config), but the BP6’s behavior doesn’t depend on their reliability — the external resistors do the real work.

5.3 When external resistors are needed for JTAG/SWD targets

A few JTAG / SWD targets expect stronger pulldowns than 100 kΩ — specifically, ARM Cortex-M parts with SWDIO float-detection that distinguishes “no chip is here” by SWDIO being held strongly low through an internal pulldown. Some chip families implement this with weaker internal pulldowns; the BP6’s external 100 kΩ may not be enough to overcome those.

Symptom: bluetag (Vol 5 § 7) returns “no SWD/JTAG found” on a board you’re confident has a debug interface.

Workaround: add an external 4.7-10 kΩ pulldown to GND on the suspect SWDIO pin, with a separate probe wire. Re-run bluetag.

This is rare — most modern Cortex-M parts work with the BP6’s defaults. The corner case is documented because chasing it without knowing is frustrating.


6. Debugger comparison

When the BP6 is the right tool vs when a dedicated debugger is.

6.1 Bus Pirate 6 vs Black Magic Probe

Black Magic Probe (BMP) is a popular open-source ARM debugger. STM32-based, ~$60.

BP6BMP
Form factorAll-purpose multi-protocol toolJTAG/SWD-only debugger
Protocols supportedUART, I²C, SPI, JTAG, SWD, 1-Wire, smart card, DDR5 SPD, etc.JTAG, SWD only
Speed (SWD flash program)100-400 kHz~10 MHz
GDB servervia OpenOCDbuilt-in (no host software needed)
SWO / RTT traceNoYes
Cost$82.50~$60

Verdict: BMP wins for dedicated ARM development. BP6 wins for everything else + occasional JTAG.

Segger J-Link is the commercial gold-standard. J-Link Base is ~$400; Edu is ~$60 (educational license, same hardware).

BP6J-Link
Form factorAll-purpose multi-protocol toolJTAG/SWD-only debugger
Speed (SWD flash program)100-400 kHz5-50 MHz
Supported targetsARM Cortex-M (via OpenOCD)ARM, MIPS, ESP32, RISC-V, Renesas, many more
SWO / RTT / ETM traceNoYes
Vendor IDEsOpenOCD-based (any GDB IDE works)First-class in every commercial IDE
Cost$82.50$60 (Edu) / $400 (Base) / $1000+ (Pro)

Verdict: J-Link is the pro tool. BP6 is the recon tool. Get a J-Link Edu for $60 if you do real embedded development; the BP6 is for pin-find / occasional work.

6.3 Bus Pirate 6 vs picoprobe / CMSIS-DAP

picoprobe is a Raspberry Pi Pico flashed with the picoprobe firmware — it acts as a CMSIS-DAP compatible debugger. ~$4 (just the Pico).

BP6picoprobe
Form factorAll-purpose multi-protocol toolSWD/JTAG-only debugger (custom firmware on a Pico)
Speed (SWD flash program)100-400 kHz1-5 MHz
Protocol supportManySWD/JTAG only
CMSIS-DAPNo (OpenOCD only)Yes — works with every CMSIS-DAP-aware tool
Cost$82.50$4

Verdict: picoprobe is the cheap-and-cheerful answer for “I just need an SWD debugger.” The BP6 has none of picoprobe’s debug-specific features (and is 5-10× slower at flash programming) but offers many other capabilities picoprobe doesn’t.

6.4 When the Bus Pirate is the right answer

Synthesizing the above: the BP6 is the right tool when you’re doing recon, not deep debug.

  • “I don’t know what protocol this board speaks” → BP6 with bluetag, then scan in I²C, etc.
  • “I want to dump a flash chip” → BP6 + flash adapters.
  • “I need to read an I²C EEPROM” → BP6.
  • “I need to identify the chip on this debug header” → BP6 + bluetag + IDCODE.

Dedicated debuggers are the right tool when you’re doing deep debug:

  • “I’m developing firmware for an STM32 / nRF52 / ESP32 / etc., and I need fast flash programming + SWO + RTT” → J-Link or BMP.
  • “I’m in a production setting flashing thousands of units” → J-Link with Flasher Pro.
  • “I want CMSIS-DAP for my IDE” → picoprobe.

The BP6’s value is its breadth. The dedicated debuggers’ value is their depth. They’re complementary, not competitive — a well-equipped bench has both.


7. Operational hygiene

Patterns that distinguish “working sessions” from “I just bricked something.”

7.1 Start in HiZ

After power-on, the BP6 is in HiZ mode — all outputs disabled, all level translators in high-impedance, no PSU, no pull-ups. This is the safe state.

Don’t change mode until target side is configured (target voltage known, probe connections planned). Switching from HiZ to a mode that drives 8 pins at 3.3 V into an unknown target is how chips die.

If returning to a board mid-session and want to be sure you’re safe: m → HiZ. Probe re-checks the rails. Then re-enter the working mode.

7.2 Voltage-check the target

Before enabling the PSU at 3.3 V on a target you think is 3.3 V (Vol 5 § 9.3):

  1. Connect just GND first.
  2. Connect IO0 to one of the target’s rails.
  3. v — read the rail voltage.
  4. Set PSU to match.

This 30-second check has saved many “wait, it’s actually a 1.8 V part” mistakes from killing a chip.

7.3 Disconnect probes between mode changes

When changing modes (I²C → SPI, for example), the pin assignments change. IO5 might have been SCL; now it’s CS. The target sees the same probe but with different signaling.

Disconnect probes (or go to HiZ first) when changing modes if the target is still powered. This avoids transient signals as the BP6 reconfigures its drivers.

7.4 When to suspect the level translator

If you’re seeing flaky readings — bytes corrupting, occasional NACKs, garbage scattered through good data — and the target is known-good, suspect the 74LVC1T45 level translator (Vol 2 § 4).

Symptoms that point at the translator:

  • Symptoms scale with VCCB voltage (worse at 5 V, better at 3.3 V, or vice versa)
  • Symptoms scale with probe cable length (worse with longer cables)
  • Symptoms scale with target capacitance (worse with capacitive loads)

Confirmation: the 74LVC1T45’s data sheet gives propagation delay vs voltage curves. Compare to your symptom; if it matches, the translator is the issue. Fix is usually reducing target capacitance (shorter cables, fewer probes attached) or running the bus slower.

7.5 Bench-fixture practices

For repeated work on the same board, build a bench fixture: a Blank Plank (Vol 8 § 10.1) with the target’s probe points wired permanently. Plug the fixture into the BP6, connect the target, work. No re-clipping cables every session.

The KF141 Quick Connector (Vol 8 § 9) is the canonical fixture connector — strip wire ends, clamp into the KF141, build a permanent fixture in 5 minutes.

7.6 The shutdown sequence

End-of-session order (Vol 5 § 9.5):

  1. Stop the protocol — any active transaction completes.
  2. Disable PWM, frequency counter — G, exit F.
  3. Disable PSU — w (lowercase).
  4. Mode → HiZ.
  5. Disconnect probes from the target.
  6. Then unplug USB-C from BP6.

Wrong order (e.g., yanking USB while probes still on a powered target) can put transient signals on the target. Habits in this order prevent half the symptoms you’d otherwise spend time debugging.


8.1 Inheriting from _shared/legal_ethics.md

The Hack Tools project’s cross-tool legal/ethics doc at _shared/legal_ethics.md applies to the BP6 directly:

  • Own the hardware or have written authorization.
  • Don’t tamper with third-party devices — credit cards, hotel keys, transit cards, neighbor’s IoT devices.
  • Don’t post extracted firmware publicly if it contains proprietary code.

These aren’t BP6-specific rules; they apply to every Hack Tools project.

Dumping a SPI flash chip from a device you own is legal in every jurisdiction we’re aware of. Dumping from a device you don’t own (your friend’s router, a discarded board from someone else’s company) is generally also legal but the downstream use of the dump can be problematic:

  • DMCA (US) — extracting copyrighted firmware is potentially a DMCA violation if you bypass technical protection. Most consumer firmware has no protection; some does.
  • CFAA (US) — accessing data on a device you don’t own can be Computer Fraud and Abuse Act territory if it involves “unauthorized access.”
  • GDPR (EU) — if the firmware contains user data (logs, credentials, configs), the data has privacy implications even if the firmware extraction is legal.
  • Trade secret law — extracting unpublished proprietary information from a vendor’s device, especially with intent to clone or compete, can be a trade-secret violation regardless of how the extraction was done.

Conservative rule of thumb: dump your own devices, study them, modify them. Anything beyond that requires careful thought about the specific jurisdiction and context.


9. When NOT to use the Bus Pirate 6

Times when the BP6 is the wrong tool:

  • High-speed digital work. SPI > 30 MHz, fast Ethernet, USB 3.0, anything requiring sub-µs jitter. Get a real protocol analyzer (Teledyne LeCroy, Agilent / Keysight).
  • Production flash programming. Dump 1000 chips quickly. Get a TL866 or dedicated programmer.
  • GDB-driven embedded development. Get a J-Link or BMP.
  • Multi-channel logic analysis beyond 8 channels. Get a Saleae 16 or DSLogic Pro.
  • Real-time signal-integrity work (rise-time measurement, eye diagrams). Get an oscilloscope.
  • Custom protocol prototyping that goes beyond the BP6’s modes. Custom firmware on a Pico (or any MCU) is the answer.

The BP6 is a general-purpose recon and bring-up tool. It’s the answer when you need many things acceptably; not when you need one thing extremely.


10. Cheatsheet updates for Vol 12

Items for the laminate cheatsheet:

  • Build from source:
    • Docker: docker-compose run --rm build
    • Native: mkdir build_rp2350 && cd build_rp2350 && cmake .. -DBUILD_TARGET=bus_pirate6_rev2 && make -j
    • Output: bus_pirate6_rev2.uf2
  • Wrong UF2 ≠ bricked — BOOTSEL is mask-ROM, re-flash works.
  • Custom mode skeleton: vtable in src/mode/, .pio program in src/pirate/, register in src/modes.c.
  • Errata E9 mitigation: BP6 ships 100 kΩ external pulldowns; some JTAG/SWD targets need additional 4.7-10 kΩ external pulldown on suspect SWDIO.
  • BP6 vs dedicated debuggers:
    • SWD flash speed: BP6 100-400 kHz vs J-Link 5-50 MHz vs picoprobe 1-5 MHz.
    • BP6 wins for recon + multi-protocol. Dedicated debugger wins for production debug.
  • Operational hygiene order: HiZ → configure → voltage-check → drive → shutdown-sequence.
  • Shutdown sequence: stop protocol → disable PWM/freq → w PSU → m HiZ → disconnect probes → unplug.

End of Volume 11. Volume 12 is the cheatsheet — synthesis of every per-volume “Cheatsheet Updates” section into laminate-ready one-pagers.