# ESP32-Bit-Pirate Performance Benchmarks for I2C and SPI: Measuring Bus Speed and Latency

> Explore ESP32-Bit-Pirate performance benchmarks for I2C and SPI. Measure bus speed and latency accurately using built-in timing functions for optimal embedded system design.

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

---

**The ESP32-Bit-Pirate firmware provides built-in timing functions for I2C and SPI that measure round-trip latency in microseconds and calculate sustained throughput in kilobytes per second using the `micros()` timer.**

This guide explains how to run accurate performance benchmarks on the ESP32-Bit-Pirate open-source firmware. The repository implements dedicated service layers for each bus protocol, exposing timing-aware methods that return elapsed microseconds for every transaction.

## I2C Performance Benchmarking Architecture

The I2C subsystem centers on **[`src/Services/I2cService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.cpp)**, which wraps the Arduino Wire API with high-precision timing instrumentation.

### Core Timing Methods

| Method | Signature | Measurement Purpose |
|--------|-----------|-------------------|
| `ping()` | `bool ping(uint8_t addr, bool sendStop, uint32_t* outDtUs)` | Single-byte round-trip latency |
| `readReg()` | `bool readReg(uint8_t addr, uint8_t reg, uint8_t* outVal, uint32_t* outDtUs)` | Register read latency with addressing overhead |
| `probeRegRW()` | `uint8_t probeRegRW(...)` | Read-modify-write composite timing |
| `i2cBitBang*` family | Multiple variants | Software-timed bus at custom frequencies |

All methods accept an optional `outDtUs` pointer that receives the elapsed time from `micros()`. When `nullptr` is passed, the operation executes without timing overhead.

### I2C Throughput Testing

For sustained transfer rates, the service provides EEPROM-oriented bulk operations:

```cpp
// Measure sustained read throughput
i2cService.eepromErase(0xFF);
uint32_t start = micros();

for (uint16_t i = 0; i < 256; ++i) {
    uint8_t value = i2cService.eepromReadByte(i);
}

uint32_t elapsed = micros() - start;
float kbps = 256.0 / (elapsed / 1000.0);

```

The **`probeReadableReg()`** and **`probeRegRW()`** methods execute standardized byte sequences, making them ideal for reproducible benchmark suites across different target devices.

### I2C Stress Testing Modes

The bit-bang implementation (`i2cBitBangWriteByte`, `i2cBitBangReadByte`, `i2cBitBangPing`) permits:

- **Frequency scaling** beyond hardware limits (tested to 1 MHz+)
- **Glitch injection** for noise immunity validation
- **Bus recovery sequences** for lockup detection

These routines bypass the hardware TWI peripheral, enabling direct GPIO timing measurements.

## SPI Performance Benchmarking Architecture

SPI benchmarks are coordinated through **[`src/Controllers/SpiController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/SpiController.cpp)** with the underlying service implementation in **[`src/Services/SpiService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/SpiService.cpp)**.

### SPI Timing Methods

| Command | Function | Measurement |
|---------|----------|-------------|
| `spi ping <cs>` | Chip-select toggle latency | Round-trip CS assertion time |
| `spi xfer <hex>` | Raw byte transfer | Per-byte and bulk throughput |
| `spi speed <hz>` | Clock configuration | Verified frequency setting |

The controller parses CLI input and delegates to service methods that bracket `SPI.transfer()` calls with `micros()` timestamps.

### SPI Throughput Calculation

```cpp
// 128-byte SPI transfer benchmark
uint8_t txBuffer[128];
uint8_t rxBuffer[128];
memset(txBuffer, 0x55, sizeof(txBuffer));

uint32_t start = micros();
spiService.transfer(txBuffer, rxBuffer, sizeof(txBuffer));
uint32_t elapsed = micros() - start;

float throughput = 128.0 / (elapsed / 1000.0);  // kB/s

```

SPI transactions use `SPI.beginTransaction()` with configurable settings from **`spi speed`**, ensuring measurements reflect actual operating conditions rather than default frequencies.

## Running Benchmarks from the Command Line

Both buses expose consistent interfaces through the terminal layer in **[`src/Views/WebTerminalView.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Views/WebTerminalView.cpp)** and the serial console.

### I2C Benchmark Session

```

> mode i2c
> i2c config 21 22 400000    # SDA, SCL, 400 kHz

> i2c ping 0x50
ACK received! Device is present. (Δt = 45 µs)

> i2c eeprom write 0x00 0xAA
> i2c eeprom read 0x00 256
[hex dump]
Read completed in 18432 µs (≈ 13.9 kB/s)

```

### SPI Benchmark Session

```

> mode spi
> spi config 18 23 19 5 8000000   # CLK, MOSI, MISO, CS, 8 MHz

> spi ping 0
CS toggled successfully (Δt = 12 µs)

> spi xfer 55:55:55:55:55:55:55:55
Transferred 8 bytes in 14 µs (≈ 571 kB/s)

```

## Interpreting Benchmark Results

### Latency Metrics

| Bus Type | Typical Range | Factors |
|----------|-------------|---------|
| I2C ping | 40–120 µs | Pull-up strength, bus capacitance, clock stretch |
| SPI ping | 8–25 µs | CS line capacitance, trace length |

The `ping` operations measure **protocol overhead only**—no data payload—making them reliable indicators of electrical and timing health.

### Throughput Metrics

| Operation | Expected Rate | Bottleneck |
|-----------|-------------|------------|
| I2C EEPROM page write | 5–15 kB/s | ACK polling, page boundaries |
| I2C EEPROM sequential read | 30–50 kB/s | Clock frequency, device latency |
| SPI burst transfer | 80–95% of raw clock | Software loop overhead, `micros()` calls |

For maximum SPI throughput, reduce the measurement granularity by transferring larger blocks and sampling `micros()` less frequently.

## Code Reference: Minimal Benchmark Implementation

This complete example demonstrates the service-layer API for programmatic benchmarking:

```cpp
#include "I2cService.h"
#include "SpiService.h"

void runI2CBenchmark(I2cService& i2c) {
    // Latency test
    uint32_t latencyUs = 0;
    bool present = i2c.ping(0x50, true, &latencyUs);
    
    if (present) {
        Serial.printf("I2C device 0x50: %lu µs ping\n", latencyUs);
        
        // Throughput test via register reads
        uint32_t totalUs = 0;
        for (int i = 0; i < 100; i++) {
            uint8_t val;
            uint32_t dt;
            i2c.readReg(0x50, i, &val, &dt);
            totalUs += dt;
        }
        Serial.printf("Average readReg: %lu µs\n", totalUs / 100);
    }
}

void runSPIBenchmark(SpiService& spi) {
    // CS latency
    uint32_t csLatency = spi.measureCsToggle(0);
    Serial.printf("SPI CS0 toggle: %lu µs\n", csLatency);
    
    // Block transfer
    uint8_t block[256];
    uint32_t start = micros();
    spi.transfer(block, block, sizeof(block));
    uint32_t elapsed = micros() - start;
    
    Serial.printf("SPI 256-byte transfer: %lu µs (%.2f kB/s)\n",
                  elapsed, 256.0 / (elapsed / 1000.0));
}

```

## Key Source Files

| File Path | Purpose |
|-----------|---------|
| [`src/Services/I2cService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.cpp) | Core I2C driver with `ping()`, `readReg()`, `i2cBitBang*()` |
| [`src/Controllers/I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/I2cController.cpp) | CLI dispatcher for `i2c ping`, `i2c eeprom` commands |
| [`src/Controllers/SpiController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Controllers/SpiController.cpp) | CLI dispatcher for `spi ping`, `spi xfer`, `spi speed` |
| [`test/Controllers/test_I2cController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/test/Controllers/test_I2cController.cpp) | Unit tests validating I2C timing APIs |
| [`test/Controllers/test_SpiController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/test/Controllers/test_SpiController.cpp) | Unit tests validating SPI timing APIs |
| [`src/Views/WebTerminalView.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Views/WebTerminalView.cpp) | Web terminal integration for benchmark commands |

## Summary

- **I2C benchmarks** use `ping()`, `readReg()`, and EEPROM methods in [`I2cService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/I2cService.cpp) with microsecond resolution via `micros()`.
- **SPI benchmarks** use equivalent methods in [`SpiService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/SpiService.cpp), accessed through [`SpiController.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/SpiController.cpp) commands.
- **Throughput calculations** require dividing byte counts by elapsed milliseconds to yield kB/s figures.
- **Bit-bang modes** enable frequency stress testing and glitch injection beyond hardware peripheral limits.
- **Consistent CLI syntax** across both buses permits scripted benchmark automation through the web or serial terminal.

## Frequently Asked Questions

### How accurate are the ESP32-Bit-Pirate timing measurements?

The firmware uses the ESP32's native `micros()` function, which provides **1 µs resolution** with negligible overhead. For sub-microsecond precision, the bit-bang implementations can be instrumented with `esp_timer_get_time()` instead, though this requires modifying [`src/Services/I2cService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.cpp) or [`SpiService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/SpiService.cpp) directly.

### Can I benchmark I2C speeds above 1 MHz?

Yes. While the hardware TWI peripheral typically limits I2C to 1 MHz, the **`i2cBitBang*()`** methods in [`src/Services/I2cService.cpp`](https://github.com/geo-tp/ESP32-Bit-Pirate/blob/main/src/Services/I2cService.cpp) implement pure software signaling. These allow clock stretching experiments and custom timing patterns up to several MHz, limited only by GPIO toggle speed and bus capacitance.

### What is the difference between `ping` and `xfer` benchmarks?

**`ping`** measures **control path latency**—the time to assert a device and receive acknowledgment without data transfer. **`xfer`** measures **data path throughput**—the time to move actual payload bytes. For I2C, `ping` uses START-STOP sequences; for SPI, it toggles chip-select. Both report elapsed microseconds via `outDtUs` parameters or CLI output parsing.