ESP32-Bit-Pirate for Controlling External Devices via SPI: A Complete Guide

The ESP32-Bit-Pirate firmware provides a menu-driven interface and C++ API for probing, reading, writing, and analyzing SPI flash chips, EEPROMs, and other peripherals through the ESP32 SPI master peripheral.

The ESP32-Bit-Pirate repository (by geo-tp) implements a versatile, open-source firmware that transforms ESP32-based boards into a universal SPI bus analyzer and programmer. Whether you need to dump firmware from a flash chip, debug an SPI EEPROM, or prototype with new peripherals, this project offers both interactive shell commands and reusable library components. The architecture cleanly separates hardware abstraction from user interface, making it straightforward to embed SPI control logic into your own Arduino or PlatformIO projects.

Core Architecture of the SPI Stack

The firmware organizes SPI functionality across five distinct layers. Understanding this structure helps you navigate the source code and extend it for custom devices.

Layer Responsibility Key Source Files
Entry point Hardware setup, TerminalView creation, interactive shell startup [src/main.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/main.cpp)
Service layer Low-level hardware drivers exposing clean C++ APIs [src/Services/SpiService.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Services/SpiService.cpp), I2cService.cpp, UartService.cpp
Shell layer Interactive menus translating user commands to service calls [SpiFlashShell.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Shells/SpiFlashShell.cpp), [SpiEepromShell.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Shells/SpiEepromShell.cpp)
Transformer layer Text-to-binary parsing and formatting ArgTransformer.cpp, Bpio2Transformer.cpp
Analyzer layer Binary scanning for strings, signatures, secrets [BinaryAnalyzer.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Analyzers/BinaryAnalyzer.cpp)

The SpiService Hardware Abstraction

SpiService acts as the central gateway for controlling external devices via SPI. It wraps the ESP-IDF SPI driver in a thin, stateless C++ class that multiple shells can share.

Key Methods for SPI Control

  • configure(mosi, miso, sclk, cs, frequency) — Sets pin assignments and SPI clock speed (Hz)
  • beginTransaction() / endTransaction() — Manages CS line assertion and SPISettings
  • transfer(uint8_t data) — Full-duplex byte exchange
  • Flash commands: readFlashIdRaw(), readFlashData(), writeFlashPage(), eraseFlashSector()
  • EEPROM commands: initEeprom(), readEeprom(), writeEeprom()

The service deliberately maintains minimal state (only CS pin and frequency), allowing rapid context switching between different SPI devices.

Probing and Identifying SPI Flash Chips

The JEDEC ID command (0x9F) is the standard mechanism for identifying SPI flash manufacturers and memory capacities. In SpiService::readFlashIdRaw within [SpiService.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Services/SpiService.cpp), the implementation follows this sequence:

#include "Services/SpiService.h"

SpiService spi;
spi.configure(/*mosi=*/23, /*miso=*/19, /*sclk=*/18, /*cs=*/5, /*freq=*/4'000'000);

spi.beginTransaction();                // CS driven low
spi.transfer(0x9F);                    // JEDEC ID command
uint8_t id[3];
for (auto &b : id) b = spi.transfer(0x00);  // Read manufacturer + device ID
spi.endTransaction();                  // CS driven high

Serial.printf("Flash ID: %02X %02X %02X\r\n", id[0], id[1], id[2]);
// Typical output: 20 BA 18 = Winbond, 4MB W25Q32JV

The three-byte response decodes as:

  • Byte 0: Manufacturer ID (e.g., 0x20 = Winbond, 0xEF = Winbond alternate, 0x01 = Cypress/Spansion)
  • Bytes 1-2: Device ID including memory type and capacity

Reading Data from SPI Flash Memory

The READ DATA command (0x03) supports sequential reads across the entire address space. The ESP32-Bit-Pirate implementation in SpiService::readFlashData handles the 24-bit address transmission:

uint32_t address = 0x001000;               // Start address (must align to needs)
uint8_t buffer[256];
spi.beginTransaction();
spi.transfer(0x03);                         // READ DATA opcode
spi.transfer((address >> 16) & 0xFF);       // Address[23:16]
spi.transfer((address >> 8) & 0xFF);        // Address[15:8]
spi.transfer(address & 0xFF);               // Address[7:0]

for (size_t i = 0; i < sizeof(buffer); ++i)
    buffer[i] = spi.transfer(0x00);         // Continuous read, MOSI = don't care
spi.endTransaction();

// Hex dump output
for (size_t i = 0; i < sizeof(buffer); ++i) {
    Serial.printf("%02X ", buffer[i]);
    if ((i + 1) % 16 == 0) Serial.println();
}

Performance note: The 0x03 command has no frequency limitation on most modern flashes, but fast read commands (0x0B) add a dummy byte latency cycle and are required above ~50 MHz.

Writing to SPI Flash: Page Programming

Flash writes require enabling write access, page-aligned addressing, and status polling. The ESP32-Bit-Pirate handles this sequence in SpiService::writeFlashPage:

std::vector<uint8_t> page = {0xDE, 0xAD, 0xBE, 0xEF};  // Max 256 bytes per page
uint32_t address = 0x000100;                           // Page start address

// Step 1: Write Enable
spiService.enableFlashWrite(4'000'000);                // Sends 0x06 command

// Step 2: Page Program
spi.beginTransaction();
spi.transfer(0x02);                                    // PAGE PROGRAM opcode
spi.transfer((address >> 16) & 0xFF);
spi.transfer((address >> 8) & 0xFF);
spi.transfer(address & 0xFF);
for (auto b : page) spi.transfer(b);                   // Write payload
spi.endTransaction();

// Step 3: Wait for completion (polling WIP bit in status register)
spiService.waitForFlashWriteComplete(4'000'000);

Critical constraints when controlling external devices via SPI for flash writes:

  • Pages are typically 256 bytes; crossing page boundaries wraps to page start
  • Erase before write: Flash bits can only be cleared (1→0) in pages; erasing (0→1) requires separate sector/block erase commands
  • Typical sector erase: 0x20 command for 4KB sectors, ~100-400ms duration

Interactive SPI Flash Shell Commands

The [SpiFlashShell.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Shells/SpiFlashShell.cpp) module provides a terminal-based interface eliminating manual command sequencing:


> flash
=== SPI Flash Shell ===
Select a SPI Flash action:
0) Probe          - Auto-detect JEDEC ID and capacity
1) Analyze        - Heuristic analysis of flash content
2) Search         - Pattern search across address range
3) Strings        - Extract printable ASCII strings
4) Read           - Read arbitrary address range to console
5) Write          - Write hex/ASCII data to address
6) Dump           - Pretty-formatted hex dump
7) Raw Dump       - Binary blob output
8) Erase          - Sector or chip erase
🚪 Exit Shell

Shell selection 0 (Probe) automatically executes readFlashIdRaw() and cross-references against known flash database entries. Selection 3 (Strings) leverages BinaryAnalyzer to locate human-readable content—useful for recovering configuration data or firmware version strings.

SPI EEPROM Control with SpiEepromShell

SPI EEPROMs (Microchip 25xx series, STMicro M95xxx, etc.) use simpler protocols without erase-before-write requirements. The [SpiEepromShell.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Shells/SpiEepromShell.cpp) implementation handles page-size detection and write-cycle timing:


> eeprom
Select EEPROM type:
0) 25X010 | 1 KB    (16-byte pages)
1) 25X020 | 2 KB    (16-byte pages)
2) 25X040 | 4 KB    (32-byte pages)
3) 25X080 | 8 KB    (32-byte pages)
...

Select EEPROM action:
0) Probe            - Read status register, detect write-protect
1) Analyze          - Content heuristics
2) Read             - Hex/ASCII read with address range
3) Write            - Interactive hex/ASCII editor
4) Dump             - Pretty-formatted output
5) Raw Dump         - Binary stream
6) Erase            - Fill with 0xFF
🚪 Exit Shell

The spiService.initEeprom(...) method configures page boundaries and maximum address based on the selected device type, preventing wrap-around errors during multi-page writes.

Binary Analysis and Data Extraction

The [BinaryAnalyzer.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Analyzers/BinaryAnalyzer.cpp) module operates on buffers retrieved via SpiService, providing:

  • String extraction: Minimum length thresholds, printable character ranges
  • File signature detection: Magic bytes for common firmware formats (ELF, ZIP, JPEG, etc.)
  • Entropy analysis: Identifying encrypted or compressed regions
  • Pattern matching: Custom hex string searches

This analysis runs entirely on the ESP32, enabling field diagnostics without transferring large binary dumps to a host computer.

Integrating ESP32-Bit-Pirate into Custom Projects

To use the SPI control capabilities in your own firmware:

  1. Copy service files: Add SpiService.h/cpp to your project
  2. Include header: #include "Services/SpiService.h"
  3. Instantiate and configure: Call configure() with your pin mapping
  4. Execute commands: Use beginTransaction()/endTransaction() wrappers or high-level methods

PlatformIO configuration from the original project:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps = 
    SPI
    ; additional vendor libraries as needed

Summary

  • ESP32-Bit-Pirate provides both interactive shells and reusable C++ libraries for controlling external devices via SPI on ESP32 hardware
  • SpiService abstracts ESP-IDF SPI master with stateless, reusable design in [SpiService.cpp](https://github.com/geoip/ESP32-Bit-Pirate/blob/pioarduino/pioarduino/src/Services/SpiService.cpp)
  • SpiFlashShell and SpiEepromShell offer menu-driven access to JEDEC probing, read/write operations, and binary analysis
  • Flash operations require write-enable, page programming, and status polling sequences handled automatically by the service layer
  • BinaryAnalyzer enables on-device content inspection without host transfer overhead
  • Modular architecture supports easy extension for new SPI peripheral types

Frequently Asked Questions

How do I identify an unknown SPI flash chip with ESP32-Bit-Pirate?

Connect the chip to GPIO pins 18 (SCK), 19 (MISO), 23 (MOSI), and your chosen CS pin. Flash the firmware, open a serial terminal at 115200 baud, type flash, and select 0) Probe. The shell sends JEDEC ID command 0x9F and displays manufacturer code, memory type, and capacity. Cross-reference the three-byte ID against manufacturer datasheets for exact part numbers.

What is the maximum SPI clock speed supported?

The SpiService::configure() method accepts any frequency in Hz, but practical limits depend on your wiring. The default 4'000'000 (4 MHz) is reliable for breadboard setups. For PCB designs with short traces, speeds up to 26 MHz typically work without signal integrity issues. The ESP32 SPI peripheral supports up to 80 MHz, but most SPI flash chips require the fast-read command (0x0B) above ~50 MHz.

Can I use ESP32-Bit-Pirate to recover a bricked device?

Yes, if the bricked device exposes SPI flash pins and the flash chip is not write-protected via WP pin or status register. Use 4) Read to dump the current firmware, 8) Erase to clear sectors, and 5) Write to program a known-good binary. For devices with locked bootloaders, you may need to pull the flash chip's HOLD or WP pins to appropriate logic levels first.

Is it possible to control non-memory SPI devices like sensors or displays?

Absolutely. The core SpiService class provides generic transfer() methods suitable for any SPI device. Create a new shell in src/Shells/ following the pattern in SpiFlashShell.cpp, or use beginTransaction()/endTransaction() directly in your application code. The stateless design means you can interleave transactions to multiple devices on the same bus by calling configure() with different CS pins between operations.

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 →