Skip to content

Latest commit

 

History

History
174 lines (117 loc) · 5.47 KB

File metadata and controls

174 lines (117 loc) · 5.47 KB

💻 Chapter 3: ESP32 SPI Integration (Code and Performance)

The hardware is ready, the protocol is understood. Now it's time to flex the ESP32's silicon muscles.

Using SPI on ESP32 is not like using an Arduino Uno. Factors like IO Matrix (Pin Remapping), DMA (Direct Memory Access), and Dual Core come into play.


3.1 What are VSPI and HSPI? (Hardware Blocks)

There are 4 hardware SPI units inside the ESP32:

  1. SPI0: Used for Internal Flash memory. (Do not touch!)
  2. SPI1: Used for Internal Flash memory. (Do not touch!)
  3. SPI2 (HSPI): Open to the user.
  4. SPI3 (VSPI): Open to the user.

Confusion: In the Arduino world, the default SPI object usually uses the VSPI (SPI3) block. If you want to open a second SPI line, you use HSPI (SPI2). The letters "V" or "H" at the beginning have no effect on speed; they are just naming conventions.

Default Pin Map (ESP32 WROOM)

Pin Name VSPI (Default) HSPI (Second Port)
SCK GPIO 18 GPIO 14
MISO GPIO 19 GPIO 12
MOSI GPIO 23 GPIO 13
CS GPIO 5 GPIO 15

3.2 Pin Matrix: Pick Any Pin You Want! 🗺️

ESP32's greatest power is its GPIO Matrix structure. The table above is just the "default". You can route SPI signals (SCK, MOSI, MISO) to almost any GPIO pin.

#include <SPI.h>

// Let's define our own pins
#define MY_SCK  25
#define MY_MISO 26
#define MY_MOSI 27
#define MY_CS   32

SPIClass mySPI(VSPI); // Use VSPI hardware block

void setup() {
  // begin(SCK, MISO, MOSI, CS);
  mySPI.begin(MY_SCK, MY_MISO, MY_MOSI, MY_CS); 
  pinMode(MY_CS, OUTPUT);
}

⚠️ Field Note: Do not use "Input only" pins (GPIO 34, 35, 36, 39) for anything other than MISO!


3.3 SPI Transaction: Protecting Settings 🛡️

What happens if there are two sensors on the same bus, one running at 1 MHz Mode 0 and the other at 10 MHz Mode 3?

In the old way, you would change settings globally every time. This is dangerous. ESP32 uses the Transaction structure. This means: "Load settings right before sending data, remove protection when done."

Why is SPI.beginTransaction() Mandatory?

This function does 3 things:

  1. Sets the SPI bus speed, Bit order (MSB/LSB), and Mode (CPOL/CPHA).
  2. Prevents other tasks (RTOS tasks) from interrupting and messing up settings.
  3. Creates a critical "Atomic" operation area.
graph TD
    A[Start] --> B{Lock Bus<br>beginTransaction}
    B --> C[Set CS Pin LOW]
    C --> D[Data Transfer<br>transfer/transfer16]
    D --> E[Set CS Pin HIGH]
    E --> F{Release Bus<br>endTransaction}
    F --> G[Finish]

    style B fill:#fff9c4,stroke:#fbc02d
    style F fill:#fff9c4,stroke:#fbc02d
    style D fill:#e3f2fd,stroke:#1565c0
Loading

3.4 Performance: DMA (Direct Memory Access) 🚀

Does the processor (CPU) have to wait for every byte when driving a TFT screen or writing large data to an SD card? No.

DMA bypasses the CPU, takes data from RAM, and pumps it directly to the SPI block.

  • CPU: "DMA, push this image sequence to the screen, I'll read sensors in the meantime."
  • Result: CPU load decreases, SPI speed hits maximum.

Library Recommendation: The standard SPI.h library has partial DMA support. For high-performance displays, use DMA-optimized libraries like TFT_eSPI or LovyanGFX.


3.5 Best Practice Code Example (Professional)

The following code is a secure and standard-compliant SPI read/write function.

#include <SPI.h>

const int PIN_CS = 5;

// Define settings as an object (1 MHz, MSB First, MODE 0)
SPISettings sensorSettings(1000000, MSBFIRST, SPI_MODE0);

void setup() {
  Serial.begin(115200);
  
  // Start SPI (Default Pins)
  SPI.begin();
  
  // CS pin is managed manually
  pinMode(PIN_CS, OUTPUT);
  digitalWrite(PIN_CS, HIGH); // Passive at start
}

void loop() {
  readSensor();
  delay(100);
}

void readSensor() {
  // STEP 1: Apply settings and protect the bus
  SPI.beginTransaction(sensorSettings);
  
  // STEP 2: Select Slave
  digitalWrite(PIN_CS, LOW);
  
  // STEP 3: Data Exchange (Send and Receive simultaneously)
  // Send "0x01" command to sensor, receive the response
  byte response = SPI.transfer(0x01); 
  
  // STEP 4: Deselect Slave
  digitalWrite(PIN_CS, HIGH);
  
  // STEP 5: Release the bus (For other devices)
  SPI.endTransaction();
  
  Serial.print("Received Data: ");
  Serial.println(response, HEX);
}

✅ Summary

  1. Pin Freedom: You can use any pin with SPI.begin(sck, miso, mosi, cs).
  2. Transaction Essential: Never drive SPI without beginTransaction and endTransaction. The system will crash, especially with multiple slaves.
  3. Speed Limit: Lower the speed if the cable is long (via SPISettings). 10MHz is good for 10cm cable, drop to 1MHz for 50cm cable.


Chapter 2: Protocol Back to Menu Chapter 4: Troubleshooting