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

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, 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:

// 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 with the underlying service implementation in 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

// 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 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:

#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 Core I2C driver with ping(), readReg(), i2cBitBang*()
src/Controllers/I2cController.cpp CLI dispatcher for i2c ping, i2c eeprom commands
src/Controllers/SpiController.cpp CLI dispatcher for spi ping, spi xfer, spi speed
test/Controllers/test_I2cController.cpp Unit tests validating I2C timing APIs
test/Controllers/test_SpiController.cpp Unit tests validating SPI timing APIs
src/Views/WebTerminalView.cpp Web terminal integration for benchmark commands

Summary

  • I2C benchmarks use ping(), readReg(), and EEPROM methods in I2cService.cpp with microsecond resolution via micros().
  • SPI benchmarks use equivalent methods in SpiService.cpp, accessed through 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 or 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 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.

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 →