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 inI2cService.cppwith microsecond resolution viamicros(). - SPI benchmarks use equivalent methods in
SpiService.cpp, accessed throughSpiController.cppcommands. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →