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

> Learn to bit-bang GPIO pins with ESP32-Bit-Pirate using CLI commands or direct method calls. Control pins at runtime without recompiling thanks to its clean architecture.

- Repository: [Geo/ESP32-Bit-Pirate](https://github.com/geo-tp/ESP32-Bit-Pirate)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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](https://github.com/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/DioController.cpp) |
| **Service** | Wraps ESP‑IDF GPIO API (`gpio_set_level`, `gpio_get_level`, etc.) | [`src/Services/PinService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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

```text
> toggle 5 250

```

- **GPIO 5** toggles every **250 ms** (4 Hz square wave)
- Press **Enter** to stop the loop

The implementation in [[`DioController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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

```text
> 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

```text
> 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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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

```cpp
#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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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

```cpp
#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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/DioController.cpp) | `toggle`, `pulse`, `jam` command handlers; toggle loop timing | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Controllers/DioController.cpp) |
| [`src/Services/PinService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/PinService.cpp) | ESP‑IDF GPIO wrapper (`setOutput`, `setHigh`, `setLow`, `read`) | [View source](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Services/PinService.cpp) |
| [`src/Shells/HelpShell.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Shells/HelpShell.cpp) | Command documentation for discoverability | [Lines 244‑255](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Shells/HelpShell.cpp#L244-L255) |
| [`src/Services/I2cService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.cpp) | I²C bit‑bang implementation using same service pattern | Same directory structure |
| [`src/Controllers/SubGhzController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/SubGhzController.cpp) / [`src/Services/SubGhzService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/SubGhzService.cpp) | RF bit‑bang TX control | [Controller](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Controllers/SubGhzController.cpp), [Service](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/pioarduino/src/Services/SubGhzService.cpp) |

---

## 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`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/PinService.h) and [`UtilityService.h`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/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.