Implementing Custom Protocols with ESP32-Bit-Pirate Bit-Banging: A Complete Guide

The ESP32-Bit-Pirate enables custom protocol implementation through reusable bit-bang primitives exposed by its service-oriented architecture, allowing arbitrary protocols to be composed via GPIO-level operations without modifying core firmware.

The ESP32-Bit-Pirate firmware implements a modular service-oriented architecture where each digital protocol operates as an independent service. These services expose low-level primitives—writeBit, readBit, togglePin, and bulk operations—that you can combine to implement implementing custom protocols with ESP32-Bit-Pirate bit-banging for virtually any digital signaling scheme.

Architecture Overview

The firmware organizes functionality into four distinct layers. Understanding these layers is essential for effective custom protocol development.

Layer Files Responsibility
Hardware abstraction src/Boards/* Board-specific GPIO mappings and pin configurations
Service layer src/Services/* Protocol-specific bit-bang API implementations
Model layer src/Models/* Generic data structures for bytecode and bitstreams
Command parser src/Commands/* CLI command routing and registration

The service layer is where custom protocol implementation primarily occurs. In src/Services/JtagService.h, you'll find generic bit-bang primitives including writeBit, shiftArray, and sendData. Similarly, src/Services/TwoWireService.h provides I²C-style operations through i2cBitBangWriteBit that work on arbitrary GPIO pairs.

Bit-Bang Primitives Available

Each service exposes timing-accurate GPIO controls suitable for custom protocol construction:

  • writeBit(bool) — Single bit output with configurable timing
  • writeBits(uint32_t, int) — Bulk bit transfer for efficient transmission
  • togglePin(uint8_t) — Raw GPIO pin manipulation
  • i2cBitBangWriteBit(scl, sda, level, delay_us) — Clock-data paired output
  • sendBinRaw_(vector<uint8_t>, te_us, bits, msb_first, invert) — High-level raw transmission

These primitives operate directly on GPIO pins, enabling any protocol expressible as precise 0/1 level sequences.

Implementing a Manchester-Encoded Protocol

Manchester encoding represents each data bit as a transition rather than a level. Here's a complete implementation using the I2cService bit-bang primitives.

Encoding Helper

// File: src/Commands/CustomProtocolCommands.cpp
#include "Services/I2cService.h"
#include "Models/Bpio2.h"
#include <vector>
#include <string>

// Manchester encode: 0 → low-to-high, 1 → high-to-low
static std::vector<bool> manchesterEncode(uint8_t byte) {
    std::vector<bool> bits;
    for (int i = 0; i < 8; ++i) {
        bool b = (byte >> i) & 0x01;
        bits.push_back(b ? true : false);   // first half
        bits.push_back(b ? false : true);   // second half (inverted)
    }
    return bits;
}

Command Implementation

void cmdManTx(const std::vector<std::string>& args) {
    if (args.size() != 2) {
        Serial.println(F("Usage: mantx <hex-byte>"));
        return;
    }

    uint8_t payload = strtoul(args[1].c_str(), nullptr, 16);
    auto encoded = manchesterEncode(payload);

    auto& i2c = I2cService::instance();
    i2c.configurePins(GPIO_NUM_22, GPIO_NUM_21);  // SCL, SDA
    i2c.setBitBangMode(true);                      // enable raw bit-bang

    const uint32_t halfPeriodUs = 200;
    for (bool level : encoded) {
        i2c.i2cBitBangWriteBit(
            GPIO_NUM_22,      // clock pin
            GPIO_NUM_21,      // data pin
            level,            // level to output
            halfPeriodUs      // timing per half-bit
        );
    }
    Serial.println(F("Manchester packet sent"));
}

The i2cBitBangWriteBit function in src/Services/TwoWireService.h handles the microsecond-precise timing while you supply the logical bit sequence.

Raw Sub-GHz Protocol Implementation

For radio-frequency protocols, SubGhzService provides sendBinRaw_—a high-level wrapper that abstracts the underlying bit-bang operations.

// File: src/Commands/CustomProtocolCommands.cpp
#include "Services/SubGhzService.h"

void cmdRawSubGhz(const std::vector<std::string>& args) {
    if (args.size() != 2) {
        Serial.println(F("Usage: rawsub <hex-bitstream>"));
        return;
    }

    std::vector<uint8_t> rawBytes;
    const std::string& hex = args[1];
    for (size_t i = 0; i < hex.length(); i += 2) {
        uint8_t byte = strtoul(hex.substr(i, 2).c_str(), nullptr, 16);
        rawBytes.push_back(byte);
    }

    const int te_us = 250;                           // bit period
    const int bits = 8 * rawBytes.size();            // total bits
    bool ok = SubGhzService::instance().sendBinRaw_(
        rawBytes,    // payload
        te_us,       // microseconds per bit
        bits,        // bit count
        true,        // MSB-first
        false        // no inversion
    );
    Serial.println(ok ? F("Raw Sub-GHz sent") : F("Failed"));
}

The sendBinRaw_ method in src/Services/SubGhzService.h internally calls low-level bit-bang routines, allowing custom OOK, FSK, or proprietary modulations through timing and polarity adjustments.

Registering Custom Commands

New protocols must be exposed through the command parser in src/Commands/ProtocolCommands.cpp or a dedicated file:

// File: src/Commands/CommandParser.cpp
void registerCustomCommands() {
    CommandParser::instance().addCommand(
        "mantx",
        "Transmit Manchester-encoded byte (hex)",
        cmdManTx);

    CommandParser::instance().addCommand(
        "rawsub",
        "Send raw Sub-GHz bitstream (hex)",
        cmdRawSubGhz);
}

Once registered, commands are available across all three interfaces: serial console, web CLI, and standalone mode.

Key Source Files for Custom Protocol Development

File Purpose
src/Services/JtagService.h Generic bit-bang: writeBit, shiftArray, sendData
src/Services/TwoWireService.h I²C-style bit-bang: i2cBitBangWriteBit
src/Services/SubGhzService.h RF transmission: sendBinRaw_
src/Models/Bpio2.h Bytecode/bitstream data structures
src/Commands/ProtocolCommands.cpp Command registration patterns

Timing Considerations for Bit-Banging

The ESP32-Bit-Pirate achieves precise timing through:

  1. Busy-wait delays in service implementations rather than RTOS task delays
  2. Configurable te_us parameters per bit or per transition
  3. GPIO direct register access for sub-microsecond switching where hardware permits

For protocols requiring sub-10µs precision, verify your ESP32 variant's GPIO performance and consider the GPIO_NUM_* constants defined in src/Boards/Common/Views/St7789SpiDeviceView.h for valid pin assignments.

Summary

  • ESP32-Bit-Pirate bit-banging relies on service-layer primitives that abstract hardware-specific GPIO operations
  • I2cService and TwoWireService provide clocked and unclocked bit-bang modes for wired protocols
  • SubGhzService enables custom RF protocols through sendBinRaw_ with configurable timing
  • Command registration in ProtocolCommands.cpp exposes new protocols to all user interfaces
  • No core firmware modification is required—custom protocols compose existing primitives

Frequently Asked Questions

What is the minimum bit period achievable with ESP32-Bit-Pirate bit-banging?

Practical limits depend on the ESP32 variant and GPIO configuration. The firmware uses microsecond-precision delays (te_us parameters), so sub-10µs bit periods are achievable on supported hardware. For exact limits, profile i2cBitBangWriteBit or sendBinRaw_ calls on your specific board.

Can I implement protocols requiring synchronous bidirectional communication?

Yes. The service primitives include readBit operations alongside writeBit. Implementations in src/Services/JtagService.h demonstrate bidirectional patterns—examine shiftArray for simultaneous read-write operations suitable for SPI-like protocols.

Do I need to reflash firmware for every protocol change?

Protocol logic resides in the service and command layers. If you modify only ProtocolCommands.cpp or add modular command files, you rebuild and reflash. For rapid prototyping, consider using the bytecode interpreter referenced in src/Models/Bpio2.h to define protocols without C++ recompilation.

How do I select GPIO pins for custom protocols?

Consult src/Boards/Common/Views/St7789SpiDeviceView.h for board-specific pin mappings. The configurePins() method on services accepts GPIO_NUM_* constants. Ensure selected pins support your required direction (input/output) and electrical characteristics for the target protocol.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →