How to Bit-Bang GPIO Pins with ESP32-Bit-Pirate: A Complete Guide

Bit‑bang GPIO pins on ESP32‑Bit‑Pirate using the built‑in toggle, pulse, and jam CLI commands, or call PinService methods directly for custom firmware extensions. The firmware implements a clean three‑layer architecture—CLI parsing, controller logic, and hardware abstraction—letting you control any GPIO pin at runtime without recompiling.

This guide covers both interactive shell usage and programmatic access. All examples reference the actual source implementation in geo‑tp/ESP32‑Bit‑Pirate.


Architecture Overview

ESP32‑Bit‑Pirate organizes GPIO operations into distinct layers. Understanding this structure helps you choose the right integration point for your use case.

Layer Responsibility Key File
CLI / Shell Parses user input into TerminalCommand objects DioController::handleTogglePin
Controller Validates arguments, checks pin permissions, executes timing loops src/Controllers/DioController.cpp
Service Wraps ESP‑IDF GPIO API (gpio_set_level, gpio_get_level, etc.) src/Services/PinService.cpp
Utility Provides millisecond timing and non‑blocking input detection UtilityService

This separation means you can bit‑bang GPIO from the console for quick tests, or import the same service classes into custom commands without duplicating hardware logic.


Bit-Banging GPIO Pins via CLI Commands

The fastest way to start is through the serial shell. Three commands cover most bit‑banging needs: toggle, pulse, and jam.

Toggle Command: Continuous Square Wave

> toggle 5 250
  • GPIO 5 toggles every 250 ms (4 Hz square wave)
  • Press Enter to stop the loop

The implementation in [DioController.cpp lines 90‑119](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Controllers/DioController.cpp#L90-L119) enters a tight loop calling pinService.setHigh() and pinService.setLow() while checking utilityService.readChar() for the stop signal.

Pulse Command: Single Timed Pulse

> pulse 12 5000
  • Drives GPIO 12 HIGH for 5 ms, then returns to LOW
  • Useful for triggering external logic analyzers or reset lines

Jam Command: Random Noise Generation

> jam 4 10 50
  • Rapidly toggles GPIO 4 with random pulse widths between 10 µs and 50 µs
  • Helpful for EMI testing or protocol fuzzing

All three commands are documented in [HelpShell.cpp lines 244‑255](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Shells/HelpShell.cpp#L244-L255) and validate pin numbers against the board's allowed GPIO mask before execution.


Bit-Banging GPIO Pins Programmatically

For custom firmware features, import PinService and UtilityService directly. This matches the pattern used by the built‑in controllers.

Basic Toggle Loop Implementation

#include "PinService.h"
#include "UtilityService.h"

void simpleToggle(uint8_t pin, uint32_t intervalMs) {
    PinService pinService;
    UtilityService util;

    pinService.setOutput(pin);          // Configure GPIO as OUTPUT
    bool state = false;

    uint32_t lastToggle = util.nowMs();
    while (true) {
        uint32_t now = util.nowMs();
        if (now - lastToggle >= intervalMs) {
            lastToggle = now;
            state = !state;
            state ? pinService.setHigh(pin) : pinService.setLow(pin);
        }
        // Break on custom condition or use util.readChar() for keypress
        if (util.readChar() == '\r') break;
    }
}

Key methods from PinService.cpp:

  • setOutput(uint8_t pin) — Configures pin mode to GPIO_MODE_OUTPUT
  • setHigh(uint8_t pin) — Calls gpio_set_level(pin, 1) and forces output mode
  • setLow(uint8_t pin) — Calls gpio_set_level(pin, 0) and forces output mode
  • read(uint8_t pin) — Returns current level via gpio_get_level
  • setInput(uint8_t pin) / setInputPullup(uint8_t pin) / setInputPullDown(uint8_t pin) — Input configuration variants

Advanced: Applying the Same Pattern to Other Protocols

The bit‑banging architecture extends beyond basic GPIO. The I2cService and SubGhzService demonstrate identical layering for complex protocols.

I²C Bit-Bang Example

#include "I2cService.h"

void sendByte(uint8_t sclPin, uint8_t sdaPin, uint8_t data) {
    I2cService i2c;
    bool ackReceived;
    
    i2c.i2cBitBangWriteByte(
        sclPin,      // SCL GPIO
        sdaPin,      // SDA GPIO
        data,        // Byte to transmit
        5,           // Delay between edges (µs)
        ackReceived  // Out: ACK status
    );
}

The i2cBitBangWriteByte function in I2cService.cpp manually clocks SCL while shifting bits onto SDA—pure software I²C using the same PinService primitives.

Sub-GHz Radio Transmission

SubGhzController and SubGhzService apply bit‑banging to RF: startTxBitBang() and stopTxBitBang() toggle the radio's data pin at precise intervals to modulate ASK/OOK signals. This proves the architecture scales from microseconds (I²C) to milliseconds (GPIO toggle) to sub‑carrier timing (RF).


Critical Source Files Reference

File Purpose Direct Link
src/Controllers/DioController.cpp toggle, pulse, jam command handlers; toggle loop timing View source
src/Services/PinService.cpp ESP‑IDF GPIO wrapper (setOutput, setHigh, setLow, read) View source
src/Shells/HelpShell.cpp Command documentation for discoverability Lines 244‑255
src/Services/I2cService.cpp I²C bit‑bang implementation using same service pattern Same directory structure
src/Controllers/SubGhzController.cpp / src/Services/SubGhzService.cpp RF bit‑bang TX control Controller, Service

Summary

  • CLI commands (toggle, pulse, jam) provide immediate GPIO bit‑banging without code changes
  • PinService encapsulates all ESP‑IDF GPIO operations for safe, reusable hardware access
  • DioController implements millisecond‑accurate timing loops with non‑blocking stop detection
  • The same three‑layer pattern (CLI → Controller → Service) applies to I²C, Sub‑GHz, and custom protocols
  • All bit‑banging operations validate pins against board‑specific masks and restore safe states on exit

Frequently Asked Questions

What GPIO pins can I use with ESP32-Bit-Pirate bit-banging commands?

Only pins permitted by the board's GPIO mask are accepted. The firmware validates pin numbers in DioController before calling PinService. Consult your specific board definition or check the allowed mask at runtime—attempting to use reserved pins (flash, PSRAM, or strapping pins) returns an error without modifying hardware state.

How accurate is the timing for GPIO bit-banging?

Millisecond‑scale accuracy is reliable for toggle and pulse commands using utilityService.nowMs(). Microsecond precision requires busy‑waiting or ESP‑IDF ets_delay_us(); the jam command and I2cService implement tighter loops for sub‑millisecond edges. For critical timing, disable Wi‑Fi/BT or use the RMT peripheral instead of software loops.

Can I bit-bang GPIO from my own custom command?

Yes. Include PinService.h and UtilityService.h, instantiate the services, and call setOutput, setHigh, and setLow directly. Follow the pattern in DioController::handleTogglePin lines 68‑85 for argument parsing and permission checks, then implement your timing logic. The service layer ensures your code remains portable across ESP32 variants.

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 →