Implementing a New Bus Pirate Mode

A step-by-step guide to creating a protocol mode for Bus Pirate 5/6/7 firmware.
Reference implementation: src/mode/dummy1.c


Overview

A mode is a protocol handler (UART, SPI, I2C, etc.) that plugs into the Bus Pirate’s syntax engine. Each mode provides:

  • Setup — configuration UI (interactive wizard and CLI flags)
  • Saved settings — persist/reload config to flash
  • Hardware init/teardown — pin claiming, peripheral setup, cleanup
  • Syntax handlers — write, read, start, stop (driven by the bytecode pipeline)
  • Macros — numbered shortcuts like (0) menu, (1) action
  • Periodic service — async polling (e.g. UART RX buffer)
  • Settings display — shown by the i (info) command
  • Help — mode-specific command listing
  • Mode commands — extra commands available only in this mode

How the Syntax Pipeline Works

The Bus Pirate uses a three-step pipeline for tight timing:

  1. Pre-process — User input is compiled into simple bytecodes
  2. Execute — Each bytecode is handed to your mode function for actual IO
  3. Post-process — Results are formatted and printed to the terminal

Important: You cannot printf() during step 2. Instead, set fields on the result struct (data_message, error, in_data, etc.) and the pipeline will display them.


File Structure

A mode consists of three touch points:

FilePurpose
src/mode/mymode.cAll mode logic — setup, handlers, constraints
src/mode/mymode.hFunction declarations, extern for def and commands
src/modes.cRegistration in the modes[] dispatch table

You also need a #define BP_USE_MYMODE guard in pirate.h and a mode enum entry.


Step 1: Includes and Config Struct

Start with the required headers and a file-static configuration struct.

#include <stdio.h>
#include "pico/stdlib.h"
#include "pirate.h"
#include "system_config.h"
#include "command_struct.h"
#include "bytecode.h"           // Bytecode structure for data IO
#include "pirate/bio.h"         // Buffered pin IO functions
#include "pirate/storage.h"     // storage_load_mode / storage_save_mode
#include "ui/ui_help.h"         // ui_help_mode_commands
#include "ui/ui_prompt.h"       // ui_prompt_bool, ui_prompt_mode_settings_*
#include "ui/ui_term.h"         // ui_term_color_info, ui_term_color_reset
#include "dummy1.h"
#include "lib/bp_args/bp_cmd.h" // Constraint-based setup: flags, prompts, help, hints

Each mode keeps its own file-static config struct. All fields must be uint32_t so they work with the storage_load_mode() / storage_save_mode() helpers:

static struct {
    uint32_t speed;     // Example numeric parameter (1..1000)
    uint32_t output;    // Example choice parameter (0=push-pull, 1=open-drain)
} mode_config;

Step 2: Pin Labels and Mode Commands

// Pin labels shown on the display and in the terminal status bar.
// No more than 4 characters long.
static const char pin_labels[][5] = { "OUT1", "OUT2", "OUT3", "IN1" };

// Mode-specific commands. If your mode has special commands
// (like UART's bridge, monitor, etc), add them here.
// The table MUST be { 0 } terminated.
const struct _mode_command_struct dummy1_commands[] = { 0 };
const uint32_t dummy1_commands_count = count_of(dummy1_commands);

Step 3: Define Setup Constraints

The bp_cmd system uses a single bp_command_def_t to drive five concerns from one definition:

  1. CLI flag parsingm dummy1 -s 500 -o od
  2. Interactive prompting — wizard menus with validation
  3. Help displaym dummy1 -h
  4. Linenoise hints — ghost text as you type
  5. Tab completion — complete flag names

Build it bottom-up: constraints → opts → def.

3a: Value Constraints

Each configurable parameter gets a bp_val_constraint_t that defines its type, valid range (or choices), default value, and prompt text.

Integer range (BP_VAL_UINT32):

static const bp_val_constraint_t dummy1_speed_range = {
    .type = BP_VAL_UINT32,
    .u = { .min = 1, .max = 1000, .def = 100 },
    .prompt = 0, // Use a T_ key here for translation, e.g. T_DUMMY1_SPEED_MENU
    .hint = 0,   // Use a T_ key here for a hint subtitle
};
FieldPurpose
.u.min, .u.maxValid range (inclusive)
.u.defDefault value when flag is absent on CLI
.promptT_ translation key for interactive menu title (0 = placeholder)
.hintT_ translation key for subtitle below prompt (0 = placeholder)

Named choices (BP_VAL_CHOICE):

static const bp_val_choice_t dummy1_output_choices[] = {
    { "push-pull",  "pp", 0, 0 }, // value=0 → push-pull
    { "open-drain", "od", 0, 1 }, // value=1 → open-drain
};
static const bp_val_constraint_t dummy1_output_choice = {
    .type = BP_VAL_CHOICE,
    .choice = { .choices = dummy1_output_choices, .count = 2, .def = 0 },
    .prompt = 0, // e.g. T_DUMMY1_OUTPUT_MENU
};

Each bp_val_choice_t entry:

FieldPurpose
.nameCLI string the user types (e.g. "push-pull")
.aliasShort alias (e.g. "pp")
.labelT_ key for interactive menu label (0 = placeholder)
.valueInteger stored in config when selected

3b: Flag/Option Table

Maps CLI flags to constraints. The array must end with a { 0 } sentinel:

static const bp_command_opt_t dummy1_setup_opts[] = {
    { "speed",  's', BP_ARG_REQUIRED, "1-1000",               0, &dummy1_speed_range },
    { "output", 'o', BP_ARG_REQUIRED, "push-pull/open-drain", 0, &dummy1_output_choice },
    { 0 }, // ← sentinel — always required
};
FieldPurpose
long_name--speed on the command line
short_name-s on the command line
arg_typeBP_ARG_REQUIRED (takes a value) or BP_ARG_NONE (boolean flag)
arg_hintShown in help text: -s <1-1000>
descriptionT_ key for help text (0 = placeholder)
constraintPointer to a bp_val_constraint_t (NULL = no auto-validation)

3c: Command Definition

The master struct that ties everything together. Must be non-static so it can be exported via the header and wired into modes[] as .setup_def:

const bp_command_def_t dummy1_setup_def = {
    .name = "dummy1",
    .description = 0,
    .opts = dummy1_setup_opts,
};
FieldPurpose
.nameMatches the protocol name (lowercase) used with the m command
.descriptionT_ key (0 = placeholder)
.optsPointer to the flag table
.positionalsNULL — modes use flags, not positional args
.actionsNULL — modes don’t have sub-commands
.usageNULL — auto-generated from opts

Step 4: The Setup Function

This is the most complex function in a mode. It handles two paths:

  • Interactive (m dummy1 with no flags) — load saved settings, offer wizard
  • CLI (m dummy1 -s 500 -o od) — parse flags directly

Returns 1 on success (proceed to setup_exc), 0 on cancel or error.

4a: Storage Descriptor

Map each config field to a JSON tag for flash persistence:

const char config_file[] = "bpdummy1.bp";
const mode_config_t config_t[] = {
    // clang-format off
    { "$.speed",  &mode_config.speed,  MODE_CONFIG_FORMAT_DECIMAL },
    { "$.output", &mode_config.output, MODE_CONFIG_FORMAT_DECIMAL },
    // clang-format on
};

Each mode_config_t entry:

FieldPurpose
.tagJSON path in the config file (e.g. "$.speed")
.configPointer to the uint32_t field in mode_config
.formatted_asMODE_CONFIG_FORMAT_DECIMAL or MODE_CONFIG_FORMAT_HEXSTRING

4b: Detect Interactive vs CLI

Check the “primary” flag to determine which path to take:

bp_cmd_status_t st = bp_cmd_flag(&dummy1_setup_def, 's', &mode_config.speed);
if (st == BP_CMD_INVALID) return 0;
bool interactive = (st == BP_CMD_MISSING);

bp_cmd_flag() return values:

StatusMeaning
BP_CMD_OKFlag found and valid — value written to output
BP_CMD_MISSINGFlag not on command line — constraint default written to output
BP_CMD_INVALIDFlag present but failed validation — error already printed

4c: Interactive Path — Saved Settings

When no CLI flags are given, first try to load previously saved settings:

if (interactive) {
    if (storage_load_mode(config_file, config_t, count_of(config_t))) {
        printf("\r\n\r\n%s%s%s\r\n", ui_term_color_info(),
               GET_T(T_USE_PREVIOUS_SETTINGS), ui_term_color_reset());
        dummy1_settings(); // Display the loaded values

        prompt_result result;
        bool user_value;
        if (!ui_prompt_bool(&result, true, true, true, &user_value)) {
            return 0; // User pressed 'x' to exit
        }
        if (user_value) {
            return 1; // User accepted saved settings — skip wizard
        }
    }

The flow is:

  1. storage_load_mode() reads the config file and populates mode_config.*
  2. Display the loaded values so the user can review them
  3. ui_prompt_bool() asks “use previous settings? (y/n)”
  4. If yes → return immediately, skip the wizard
  5. If no → fall through to the interactive wizard

4d: Interactive Path — Prompt Wizard

If no saved settings exist, or the user declined them, run the full wizard:

    if (bp_cmd_prompt(&dummy1_speed_range, &mode_config.speed) != BP_CMD_OK) return 0;
    if (bp_cmd_prompt(&dummy1_output_choice, &mode_config.output) != BP_CMD_OK) return 0;

bp_cmd_prompt() drives an interactive prompt from a bp_val_constraint_t:

  • BP_VAL_UINT32: shows "min-max (default)" and validates input
  • BP_VAL_CHOICE: shows a numbered menu of named options, accepts name/alias/number
  • Returns BP_CMD_OK on success, BP_CMD_EXIT if user cancels

4e: CLI Path

When flags are present, the primary flag is already parsed. Collect remaining flags — missing ones automatically get their constraint default:

} else {
    st = bp_cmd_flag(&dummy1_setup_def, 'o', &mode_config.output);
    if (st == BP_CMD_INVALID) return 0;
}

4f: Save and Finish

Always save after a successful setup and display the final configuration:

storage_save_mode(config_file, config_t, count_of(config_t));
dummy1_settings();
return 1;

Step 5: Hardware Setup and Cleanup

setup_exc — Hardware Init

Called after setup() returns successfully. This is where you configure peripherals and claim pins:

uint32_t dummy1_setup_exc(void) {
    // 1. Configure hardware pins / peripherals.
    bio_output(BIO4);
    bio_output(BIO5);
    bio_output(BIO6);
    bio_input(BIO7);

    // 2. Claim IO pins so the Bus Pirate won't let the user manipulate them.
    system_bio_update_purpose_and_label(true, BIO4, BP_PIN_MODE, pin_labels[0]);
    system_bio_update_purpose_and_label(true, BIO5, BP_PIN_MODE, pin_labels[1]);
    system_bio_update_purpose_and_label(true, BIO6, BP_PIN_MODE, pin_labels[2]);
    system_bio_update_purpose_and_label(true, BIO7, BP_PIN_MODE, pin_labels[3]);
    return 1;
}

Claimed pins are blocked from PWM/FREQ/etc while the mode is active.

cleanup — Teardown

Called when the user exits the mode (returns to HiZ):

void dummy1_cleanup(void) {
    // 1. Disable/deinit any hardware you configured.
    bio_init();

    // 2. Release IO pins and clear labels.
    system_bio_update_purpose_and_label(false, BIO4, BP_PIN_MODE, 0);
    system_bio_update_purpose_and_label(false, BIO5, BP_PIN_MODE, 0);
    system_bio_update_purpose_and_label(false, BIO6, BP_PIN_MODE, 0);
    system_bio_update_purpose_and_label(false, BIO7, BP_PIN_MODE, 0);
}

Step 6: Settings Display

Shown by the i (info) command. Use the standardized display functions so output format matches all other modes:

void dummy1_settings(void) {
    // Numeric setting: label, value, units string (0 for no units)
    ui_prompt_mode_settings_int(
        "Speed",                    // label — use GET_T(T_xxx) for translation
        mode_config.speed,          // current value
        0                           // units string (e.g. GET_T(T_KHZ)) or 0
    );
    // Choice setting: label, selected choice name, units
    const char* output_name = "push-pull";
    for (uint32_t i = 0; i < count_of(dummy1_output_choices); i++) {
        if (dummy1_output_choices[i].value == mode_config.output) {
            output_name = dummy1_output_choices[i].name;
            break;
        }
    }
    ui_prompt_mode_settings_string(
        "Output type",              // label
        output_name,                // current choice name
        0                           // units
    );
}
FunctionWhen to use
ui_prompt_mode_settings_int()Numeric values (baud rate, speed, data bits)
ui_prompt_mode_settings_string()Choice/enum values (parity, output type, flow control)

Step 7: Syntax Handlers

These are the bytecode pipeline handlers. Do not printf() in these functions — set fields on the result struct instead.

Write — User enters a number or string

void dummy1_write(struct _bytecode* result, struct _bytecode* next) {
    static const char message[] = "--DUMMY1- write()";

    // your code — user data is in result->out_data
    for (uint8_t i = 0; i < 8; i++) {
        bio_put(BIO5, result->out_data & (0b1 << i));
    }

    // example error handling
    static const char err[] = "Halting: 0xff entered";
    if (result->out_data == 0xff) {
        result->error = SERR_ERROR;
        result->error_message = err;
        return;
    }

    result->data_message = message;
}

Result struct fields

FieldPurpose
result->out_dataData value the user entered (up to 32 bits)
result->in_dataValue to show the user (e.g. SPI read-back)
result->bitsBit count config (e.g. 0xff.4 = 4 bits)
result->number_formatFormat used: df_bin, df_hex, df_dec, df_ascii
result->data_messageText decoration to display (e.g. “ACK”, “NACK”)
result->errorError level (see table below)
result->error_messageError text to display
result->repeatHandled by the layer above — do not implement

Error codes

CodeBehavior
SERR_NONENo error
SERR_DEBUGDisplay message, continue
SERR_INFODisplay message, continue
SERR_WARNDisplay message, continue
SERR_ERRORDisplay message, halt execution

Read — User enters r

void dummy1_read(struct _bytecode* result, struct _bytecode* next) {
    static const char message[] = "--DUMMY1- read()";

    uint32_t data = bio_get(BIO7);

    result->in_data = data;         // put the read value in in_data
    result->data_message = message; // optional text decoration
}

Start / Stop — [ and ] keys

void dummy1_start(struct _bytecode* result, struct _bytecode* next) {
    static const char message[] = "-DUMMY1- start()";
    bio_put(BIO4, 1);
    result->data_message = message;
}

void dummy1_stop(struct _bytecode* result, struct _bytecode* next) {
    static const char message[] = "-DUMMY1- stop()";
    bio_put(BIO4, 0);
    result->data_message = message;
}

Full Duplex Start/Stop — { and } keys

Used by SPI for simultaneous read/write. Most modes leave these as stubs or point protocol_start_alt / protocol_stop_alt at the regular start/stop in the modes.c registration:

void dummy1_startr(struct _bytecode* result, struct _bytecode* next) {
    (void)result; (void)next;
}

Step 8: Macros

Macros are passed from the command line directly (not through the syntax system). Macro (0) is always a menu listing available macros:

void dummy1_macro(uint32_t macro) {
    printf("-DUMMY1- macro(%d)\r\n", macro);
    switch (macro) {
        case 0:
            printf(" 0. This menu\r\n 1. Print \"Hello World!\"\r\n");
            break;
        case 1:
            printf("Never gonna give you up\r\n");
            break;
    }
}

Step 9: Periodic Service

Called regularly by the main loop. Useful for async polling (e.g. checking for bytes in a UART buffer). Link via .protocol_periodic in modes.c — use noperiodic if not needed:

void dummy1_periodic(void) {
    static uint32_t cnt;
    if (cnt > 0xffffff) {
        printf("\r\n-DUMMY1- periodic\r\n");
        cnt = 0;
    }
    cnt++;
}

Step 10: Bitwise Handlers

Legacy bitbang support for clock/data line manipulation. Most hardware modes don’t need these — use nullfunc1_temp in your modes.c registration instead.

Important: The signature must match the _mode struct: void(struct _bytecode*, struct _bytecode*).

void dummy1_clkh(struct _bytecode* result, struct _bytecode* next) {
    (void)result; (void)next;
    // set clock high
}

Step 11: Help

Show mode-specific commands when the user presses ?:

void dummy1_help(void) {
    ui_help_mode_commands(dummy1_commands, dummy1_commands_count);
}

Step 12: The Header File

Export everything that modes.c needs:

void dummy1_write(struct _bytecode* result, struct _bytecode* next);
void dummy1_read(struct _bytecode* result, struct _bytecode* next);
void dummy1_start(struct _bytecode* result, struct _bytecode* next);
void dummy1_stop(struct _bytecode* result, struct _bytecode* next);

// full duplex start/stop — signature matches _mode struct
void dummy1_startr(struct _bytecode* result, struct _bytecode* next);
void dummy1_stopr(struct _bytecode* result, struct _bytecode* next);

void dummy1_macro(uint32_t macro);
void dummy1_periodic(void);

uint32_t dummy1_setup(void);
uint32_t dummy1_setup_exc(void);
void dummy1_cleanup(void);
void dummy1_settings(void);

// bitwise handlers — signature must match _mode struct
void dummy1_clkh(struct _bytecode* result, struct _bytecode* next);
void dummy1_clkl(struct _bytecode* result, struct _bytecode* next);
void dummy1_dath(struct _bytecode* result, struct _bytecode* next);
void dummy1_datl(struct _bytecode* result, struct _bytecode* next);
void dummy1_dats(struct _bytecode* result, struct _bytecode* next);
void dummy1_clk(struct _bytecode* result, struct _bytecode* next);
void dummy1_bitr(struct _bytecode* result, struct _bytecode* next);

void dummy1_help(void);

extern const struct _mode_command_struct dummy1_commands[];
extern const uint32_t dummy1_commands_count;
extern const struct bp_command_def dummy1_setup_def;

Key points:

  • All bitwise and syntax handler signatures must be void(struct _bytecode*, struct _bytecode*)
  • dummy1_setup_def must be extern const so modes.c can reference it
  • dummy1_commands[] and dummy1_commands_count must be extern for the dispatch table

Step 13: Register in modes.c

Add the include guard

#ifdef BP_USE_DUMMY1
#include "mode/dummy1.h"
#endif

Add the mode entry

Wire every function pointer in the modes[] array:

#ifdef BP_USE_DUMMY1
    [DUMMY1] = {
        .protocol_name = "DUMMY1",                       // friendly name (promptname)
        .protocol_start = dummy1_start,                  // start
        .protocol_start_alt = dummy1_start,              // start with read
        .protocol_stop = dummy1_stop,                    // stop
        .protocol_stop_alt = dummy1_stop,                // stop with read
        .protocol_write = dummy1_write,                  // send(/read) max 32 bit
        .protocol_read = dummy1_read,                    // read max 32 bit
        .protocol_clkh = dummy1_clkh,                    // set clk high
        .protocol_clkl = dummy1_clkl,                    // set clk low
        .protocol_dath = dummy1_dath,                    // set dat hi
        .protocol_datl = dummy1_datl,                    // set dat lo
        .protocol_dats = dummy1_dats,                    // toggle dat
        .protocol_tick_clock = dummy1_clk,               // tick clk
        .protocol_bitr = dummy1_bitr,                    // read 1 bit
        .protocol_periodic = dummy1_periodic,            // async polling
        .protocol_macro = dummy1_macro,                  // macro handler
        .protocol_setup = dummy1_setup,                  // setup UI
        .protocol_setup_exc = dummy1_setup_exc,          // hardware init
        .protocol_cleanup = dummy1_cleanup,              // teardown
        .protocol_settings = dummy1_settings,            // display settings
        .protocol_help = dummy1_help,                    // help
        .mode_commands = dummy1_commands,                 // mode-specific commands
        .mode_commands_count = &dummy1_commands_count,    // command count
        .protocol_get_speed = nullfunc7_no_error,        // speed (or implement)
        .setup_def = &dummy1_setup_def,                  // ← enables CLI flags & hints
    },
#endif

Key fields

FieldRequired?Purpose
.protocol_setupYesYour setup UI function
.protocol_setup_excYesHardware init (called after setup succeeds)
.protocol_cleanupYesTeardown on mode exit
.protocol_settingsYesDisplay config for i command
.setup_defYesEnables m mymode -h help and m mymode -<Tab> completion
.protocol_write / .protocol_readYesCore IO handlers
.protocol_start / .protocol_stopYes[ / ] handlers
.protocol_start_alt / .protocol_stop_altUse start/stop{ / } full duplex (point at start/stop if unused)
.protocol_periodicUse noperiodicAsync polling (or noperiodic if not needed)
.protocol_macroUse nullfunc4Macro handler (or nullfunc4 if not needed)
.protocol_clkh etc.Use nullfunc1_tempBitwise ops (or nullfunc1_temp stubs)
.mode_commands{ 0 } tableMode-specific commands
.protocol_get_speedUse nullfunc7_no_errorReturn current protocol speed

Quick Reference: bp_cmd API

Constraint-Aware Functions (used in mode setup)

FunctionSignatureReturns
bp_cmd_flag(def, char, &out)bp_cmd_status_t
bp_cmd_prompt(constraint, &out)bp_cmd_status_t

Status Codes

StatusMeaning
BP_CMD_OKValue obtained and valid
BP_CMD_MISSINGNot on command line (default written for flags)
BP_CMD_INVALIDPresent but failed validation (error printed)
BP_CMD_EXITUser cancelled interactive prompt

Constraint Types

TypeUnionFields
BP_VAL_UINT32.u.min, .max, .def
BP_VAL_INT32.i.min, .max, .def
BP_VAL_FLOAT.f.min, .max, .def
BP_VAL_CHOICE.choice.choices, .count, .def

Checklist

  • Create src/mode/mymode.c with config struct and all handlers
  • Define bp_val_constraint_t for each setup parameter
  • Define bp_command_opt_t[] flag table (sentinel-terminated with { 0 })
  • Define bp_command_def_t (non-static, exported)
  • Implement setup() with dual-path (interactive + CLI)
  • Add saved settings: storage_load_mode() / storage_save_mode()
  • Implement setup_exc() — hardware init and pin claiming
  • Implement cleanup() — hardware deinit and pin release
  • Implement settings()ui_prompt_mode_settings_int/string()
  • Implement syntax handlers: write, read, start, stop
  • Implement macro handler (at minimum macro (0) menu)
  • Create src/mode/mymode.h with all declarations and extern for def
  • Add #define BP_USE_MYMODE in pirate.h and mode enum entry
  • Register in modes.c — include guard + modes[] entry with .setup_def
  • Build and verify