# Troubleshooting SPI Communication on ESP32-Bit-Pirate: A Complete Guide

> Troubleshoot SPI communication on ESP32-Bit-Pirate by understanding its architecture, pinout, and bus release requirements. Solve your SPI issues with this guide.

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

---

**ESP32-Bit-Pirate users can diagnose SPI communication issues by understanding the layered architecture, proper pin configuration, and the critical requirement to release the SPI bus before switching between master and slave modes.**

The ESP32-Bit-Pirate open-source firmware provides a versatile tool for debugging SPI, I2C, and UART interfaces. This guide walks through the complete SPI stack implemented in the [geo-tp/ESP32-Bit-Pirate](https://github.com/geo-tp/ESP32-Bit-Pirate) repository, with concrete troubleshooting steps derived from the source code.

## Understanding the ESP32-Bit-Pirate SPI Architecture

The firmware organizes SPI functionality into four distinct layers:

- **Global State** — stores pin assignments, frequency, and mode flags via `GlobalState::getInstance()`
- **Low-Level Driver** — `SpiService` wraps the Arduino `SPI` API and `ESP32SPISlave` library
- **Controller** — `SpiController` parses commands and coordinates all SPI operations
- **Shells & Views** — interactive UIs for SD card, flash, and EEPROM operations

Understanding this separation is essential when troubleshooting SPI communication on ESP32-Bit-Pirate, as failures typically occur at layer boundaries—particularly when transitioning between master and slave modes.

## Configuring SPI Pins and Frequency

The `spi config` command triggers `SpiController::handleConfig()`, which validates and stores settings in `GlobalState` before calling `SpiService::configure()`:

```cpp
// src/Services/SpiService.cpp
void SpiService::configure(uint8_t mosi, uint8_t miso,
                          uint8_t sclk, uint8_t cs,
                          uint32_t frequency) {
    end();                         // Shut down any previous SPI session
    csPin = cs;
    spiFrequency = frequency;
    SPI.begin(sclk, miso, mosi, cs);
    pinMode(cs, OUTPUT);
    digitalWrite(cs, HIGH);
}

```

Notice the critical `end()` call at line 2. This prevents hardware conflicts when reconfiguring.

### Common Configuration Pitfalls

| Symptom | Root Cause | Solution |
|---------|-----------|----------|
| No data after sniff/slave session | Pins not released | Always call `spiService.end()` before mode switches |
| Wrong sniffing direction | `sniffMosi` flag mishandled | Verify choice index from `userInputManager.readValidatedChoiceIndex` |
| Frequency rejection | Value exceeds 80 MHz | Valid range is 1-80 MHz per `readValidatedUint8` constraints |

## Diagnosing Sniff Mode Failures

The `spi sniff` command enables passive capture on either MOSI or MISO. The implementation in `SpiController::handleSniff()` reveals two common failure modes:

```cpp
// src/Controllers/SpiController.cpp (excerpt)
void SpiController::handleSniff() {
    int choice = userInputManager.readValidatedChoiceIndex(
        "Select line to sniff", {" MOSI"," MISO"}, 0);
    bool sniffMosi = (choice == 0);
    
    // Pin mapping logic
    int slaveMisoPin = sniffMosi ? miso : -1;
    int slaveMosiPin = sniffMosi ? mosi : miso;
    
    spiService.end();  // CRITICAL: release master mode
    spiService.startSlave(sclk, slaveMisoPin, slaveMosiPin, cs);
    
    while (true) {
        char c = terminalInput.readChar();
        if (c == '\n' || c == '\r') break;
        auto packets = spiService.getSlaveData();
        // ... display hex data
    }
    // ... restore master configuration
}

```

### Sniff Mode Troubleshooting Checklist

1. **Verify physical wiring** — The `slaveMosiPin` and `slaveMisoPin` assignments swap based on `sniffMosi`. When capturing MISO (choice 2), `slaveMosiPin` receives the original MISO pin value.

2. **Confirm CS activation** — Data logging only occurs when CS is pulled low (see `handleSniff` lines 78-83 in the source).

3. **Check buffer retrieval** — `SpiService::getSlaveData()` reads from `ESP32SPISlave` buffer and re-queues transactions automatically.

## Fixing Slave Mode Transition Errors

The `spi slave` command requires full-duplex capture, making the mode transition even more sensitive:

```cpp
// src/Controllers/SpiController.cpp (excerpt)
void SpiController::handleSlave() {
    spiService.end();                         // Stop master mode
    spiService.startSlave(sclk, miso, mosi, cs);
    
    while (!terminalInput.available()) {
        delay(10);
    }
    
    spiService.stopSlave(sclk, miso, mosi, cs);
    spiService.configure(mosi, miso, sclk, cs, 
                         state.getSpiFrequency());  // Restore master
}

```

The `startSlave()` method contains a guard clause: `if (slave) return;`. This silent failure occurs if `end()` was not called—a frequent source of "slave mode not working" reports.

## SD Card, Flash, and EEPROM Interactions

Peripheral operations follow a strict save-restore pattern. For SD card mounting (`spi sdcard`):

```cpp
spiService.end();                     // Release the bus
sdService.configure(clk, miso, mosi, cs);
sdCardShell.run();                    // Interactive filesystem UI
spiService.configure(mosi, miso, sclk, cs, freq); // Restore configuration

```

The `ensureConfigured()` helper guarantees SPI bus restoration even when operations fail. Unit test `test_sdcard_mount_failure_does_not_run_shell_or_reconfigure` validates this error handling.

## Executing Byte-Code Sequences

Low-level SPI operations use a byte-code abstraction for portability. The JEDEC ID read demonstrates the pattern:

```cpp
#include "ByteCode.h"
#include <vector>

std::vector<ByteCode> cmds = {
    ByteCode(ByteCodeEnum::Write, 0x9F),   // JEDEC ID command
    ByteCode(ByteCodeEnum::Read,  3)       // Read 3 bytes
};

std::string id = spi.executeByteCode(cmds);
// Returns manufacturer and device ID as hex string

```

The controller implementation only prints non-empty results:

```cpp
// src/Controllers/SpiController.cpp
auto result = spiService.executeByteCode(bytecodes);
if (!result.empty()) {
    terminalView.println("SPI Read:\n");
    terminalView.println(result);
}

```

Empty responses indicate either bus contention or incorrect command sequencing.

## Leveraging the Test Suite for Diagnosis

The repository's unit tests in [`test/Controllers/test_SpiController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/test/Controllers/test_SpiController.cpp) codify expected behaviors:

- `test_config_updates_state_and_configures_spi_service` — validates pin/frequency propagation
- `test_slave_captures_packets_then_restores_spi_configuration` — confirms mode transition integrity
- `test_instruction_delegates_bytecodes_and_prints_result` — verifies byte-code execution

When troubleshooting SPI communication on ESP32-Bit-Pirate, referencing these test cases provides authoritative behavioral specifications.

## Summary

- **Always call `spiService.end()`** before switching between master and slave modes to prevent driver rejection
- **Verify pin mapping logic** in sniff mode—the MOSI/MISO assignment depends on the `sniffMosi` boolean
- **Trust the test suite**—unit tests document correct behavior for all SPI command paths
- **Use `ensureConfigured()`** after peripheral operations to guarantee bus restoration
- **Check choice indices carefully**—off-by-one errors in `readValidatedChoiceIndex` cause silent misconfiguration

## Frequently Asked Questions

### Why does SPI sniff mode show no data?

The most likely cause is CS line inactivity. The ESP32-Bit-Pirate firmware only logs bytes when CS is pulled low during transactions. Verify physical CS connectivity and confirm the target device drives CS correctly. Also ensure you selected the correct line (MOSI vs MISO) at the prompt—choice index 0 is MOSI, 1 is MISO.

### How do I switch from slave mode back to master mode?

The `SpiController::handleSlave()` method demonstrates the proper sequence: call `spiService.stopSlave()`, then re-invoke `spiService.configure()` with your original pin assignments and frequency. The `GlobalState` singleton retains these values via `getSpiFrequency()` and related getters.

### What is the maximum SPI frequency supported?

The firmware constrains frequency to 1-80 MHz through `readValidatedUint8` validation in `handleConfig()`. Exceeding 80 MHz causes immediate rejection at the configuration prompt. For reliable communication with most flash and EEPROM devices, 20-40 MHz is recommended.

### Why does SD card mounting fail to restore my SPI settings?

This should not occur in normal operation—the `ensureConfigured()` method guards restoration. If observed, check for stack overflow or memory corruption. The unit test `test_sdcard_mount_failure_does_not_run_shell_or_reconfigure` specifically validates that early abort leaves pins untouched, so persistent issues likely indicate hardware instability rather than firmware bugs.