Skip to Content

How to use SPI on an ESP32 based PLC

SPI pins, modes and Arduino code for the ESP32 PLC, and how to share the bus with Ethernet and the µSD card
March 29, 2023 by
How to use SPI on an ESP32 based PLC
Boot & Work Corp. S.L, Ricard Franch Argullol

To use SPI on an ESP32 PLC, wire the device to the SO (MISO, GPIO 19), SI (MOSI, GPIO 23) and SCK (GPIO 18) terminals, use a free logic pin such as GPIO 0 as chip select, and send data with the Arduino SPI library inside SPI.beginTransaction() and SPI.endTransaction(). The external SPI port of the ESP32 PLC is the VSPI bus (SPI3) of the ESP32 and works at 3.3 V.

The same bus also serves the Ethernet controller and the µSD card of the PLC. Because of this, each device needs its own chip select, and your code must not change the bus settings outside a transaction. This guide covers the SPI signals and modes, the SPI controllers and default pins of the ESP32, the SPI terminals of the ESP32 PLC, the rules for a shared bus, Arduino code and a checklist for the first transfer. The sections on SPI modes and ESP32 pins apply to any ESP32 board.

SPI on the ESP32 PLCValue
ESP32 busVSPI (SPI3), the default SPI object in Arduino
MISO (master in)Terminal SO, GPIO 19
MOSI (master out)Terminal SI, GPIO 23
ClockTerminal SCK, GPIO 18
Chip selectNo fixed pin. Use a free logic pin, for example the GPIO 0 terminal
Logic level3.3 V, direct from the ESP32
RoleMaster. The Arduino SPI library doesn't support slave mode
Shared withEthernet controller and µSD card
Industrial Shields ESP32 industrial PLC
ESP32 PLC from Industrial Shields: an industrial controller based on the ESP32, programmable with the Arduino IDE.

How does SPI communication work?

SPI (Serial Peripheral Interface) is a synchronous, full duplex serial bus with one master and one or more slaves. The master generates the clock. On every clock pulse, one bit goes out on MOSI and one bit comes in on MISO. A transfer always sends and receives the same number of bits, so to read a byte the master sends a byte, often a dummy 0x00.

  • SCK (also SCLK): the clock from the master. The slaves never drive it.
  • MOSI (Master Out Slave In): data from the master to the slave.
  • MISO (Master In Slave Out): data from the slave to the master. Only the selected slave drives this line.
  • CS (Chip Select, also SS for Slave Select): one line per slave, active low. The slave listens only while its CS is low.

Some newer datasheets use other names for the same lines: COPI and CIPO, or SDO and SDI seen from each device.

SPI communication diagram
One master, one slave: the master selects the slave with CS, drives the clock and exchanges data on MOSI and MISO.

SPI modes: clock polarity and clock phase

The master and the slave must agree on when the data is valid. Two settings define it. CPOL is the level of the clock when the bus is idle. CPHA says if the data is sampled on the first or on the second clock edge. Together they give four SPI modes. The datasheet of the slave tells you the mode, as a mode number, as CPOL and CPHA values, or as a timing diagram.

ModeCPOLCPHAClock when idleData sampled onArduino constant
000LowRising edgeSPI_MODE0
101LowFalling edgeSPI_MODE1
210HighFalling edgeSPI_MODE2
311HighRising edgeSPI_MODE3

The other two settings are the clock frequency and the bit order. Most devices send the most significant bit first (MSBFIRST). In Arduino, SPISettings keeps the three values together: clock, bit order and mode.

ESP32 SPI controllers: SPI0 and SPI1 for the flash, HSPI and VSPI for your devices

The original ESP32 has four SPI controllers. SPI0 and SPI1 connect the chip to its flash memory (and PSRAM, if there is one), so you can't use them for your devices. SPI2 and SPI3 are general purpose SPI controllers. In Arduino they are called HSPI and VSPI.

ControllerArduino nameUseSCKMISOMOSICS
SPI0, SPI1noneInternal flash and PSRAMNot available for your devices
SPI2HSPIGeneral purposeGPIO 14GPIO 12GPIO 13GPIO 15
SPI3VSPIGeneral purpose, default SPI objectGPIO 18GPIO 19GPIO 23GPIO 5

Default pins of SPI2 and SPI3 (IO_MUX pins), from the ESP-IDF SPI Master documentation.

These default pins connect to the controller directly through the IO_MUX. Espressif gives up to 80 MHz on these pins. The ESP32 can also route SPI to other pins through the GPIO matrix, but then the limit is 40 MHz, and about 26 MHz for full duplex transfers. In practice, the slave and the wiring set a much lower limit than the ESP32. Other chips of the family, such as the ESP32-S3, use different pins and call the general purpose bus FSPI. See the ESP-IDF SPI Master documentation for the details.

SPI terminals on the ESP32 PLC

The ESP32 PLC family (models 19R to 58 in the ESP32 PLC family datasheet) has an external SPI port connected to VSPI (SPI3). The datasheet gives the SPI port as 3.3 V, direct from the ESP32 CPU. You don't need to change any DIP switch to use it. The PLC needs its normal 12 to 24 Vdc supply.

FunctionPLC terminalESP32 pinLevel
MISOSOGPIO 193.3 V
MOSISIGPIO 233.3 V
ClockSCKGPIO 183.3 V
Chip selectYour choice, for example GPIO 0Set in your codeDepends on the pin (GPIO 0 is 5 V)

There is no terminal for chip select. You choose a free logic pin and drive it from your code, as in the examples below. Don't use a 24 V digital output (a Q terminal) as chip select: its level and its switching time don't match an SPI device.

Check the chip select level. In our ESP32 PLC pinout time analysis, the GPIO 0 terminal works at 5 V, with rise and fall times of about 5 µs. The SI, SO and SCK lines work at 3.3 V. If your device has a 3.3 V chip select input that is not 5 V tolerant, add a level shifter on CS. The ESP32 pins are not 5 V tolerant either: a 5 V device needs a level shifter on its MISO output.

When you don't use SPI or Ethernet, the SO, SI and SCK terminals can also work as general digital inputs or outputs. The same pinout analysis gives their rise and fall times (about 100 ns).

ESP32 controller - Industrial Shields
The ESP32 PLC family brings the VSPI bus of the ESP32 to the SO, SI and SCK terminals.

Ethernet, µSD card and your SPI device share one bus

Inside the ESP32 PLC, the Ethernet controller, the µSD card and the external SPI port all use the same SPI bus (SPI3). Communication boards in the expansion slots can also use SPI with their own chip select: the CAN SPI board guide uses CS 32 in expansion slot 1 and CS 2 in slot 2. A shared bus works well if every device follows the same rules.

  1. 1 · Take the busbeginTransaction()Loads the clock, bit order and mode of your device. In the ESP32 Arduino core it also locks the bus until endTransaction().
  2. 2 · SelectCS lowOnly your device listens. The CS lines of Ethernet and the µSD card stay high.
  3. 3 · Exchangetransfer()Send and receive the bytes of one command, as the device datasheet describes.
  4. 4 · ReleaseCS high, endTransaction()Your device leaves the bus, and Ethernet and the µSD card can use it again.

Rule Keep CS low only between beginTransaction() and endTransaction().

  • Set CS high before anything else. In setup(), configure your CS pin as an output and set it high before you start Ethernet, the µSD card or SPI. A floating CS can make your device answer during Ethernet traffic.
  • Use a transaction for every access. Ethernet and the µSD card use their own clock and mode. If your code changes the settings outside a transaction, the next Ethernet or µSD access can fail.
  • Don't call SPI.end() and don't call SPI.begin() with other pins while Ethernet or the µSD card are in use. It stops the bus for all devices.
  • Check that your device releases MISO. When its CS is high, the device must put MISO in high impedance. Some devices keep driving MISO. On a shared bus they block Ethernet and the µSD card, and need a tri-state buffer with an enable input.
  • Keep transfers short. While your device holds the bus, Ethernet and the µSD card wait. Read one value per transaction instead of a long block.

The µSD card is a typical case: the PLC writes data to the card while the network is down, and Ethernet or MQTT sends it later. The Solution Library shows this pattern in SD card datalogging on an ESP32 PLC with daily files.

Arduino code for an SPI device on the ESP32 PLC

Install the Industrial Shields ESP32 boards package in the Arduino IDE (see how to install Industrial Shields boards in the Arduino IDE) and select your ESP32 PLC model in Tools. The SPI.h library of the package is the Arduino SPI library for the ESP32. It works as master only.

Basic master example: send one byte and print the answer

This sketch sends the character '0' every 100 ms and prints the byte that the slave returns. The library doesn't control CS, so the sketch drives it. Replace the clock, bit order and mode with the values of your device datasheet.

// SPI master example for the ESP32 PLC family
#include <SPI.h>

const int csPin = GPIO_0;              // chip select: GPIO 0 terminal (5 V)
const uint32_t spiClock = 1000000;     // 1 MHz: start low, raise it after the first tests

SPISettings deviceSettings(spiClock, MSBFIRST, SPI_MODE0);

uint8_t spiExchange(uint8_t out) {
  SPI.beginTransaction(deviceSettings); // take the bus with the settings of this device
  digitalWrite(csPin, LOW);             // select the device
  delayMicroseconds(10);                // GPIO 0 edges take about 5 us
  uint8_t in = SPI.transfer(out);       // send one byte and receive one byte
  digitalWrite(csPin, HIGH);            // release the device
  delayMicroseconds(10);                // let CS reach the high level
  SPI.endTransaction();                 // free the bus for Ethernet and the uSD card
  return in;
}

void setup() {
  Serial.begin(115200);
  pinMode(csPin, OUTPUT);
  digitalWrite(csPin, HIGH);            // keep the device off the bus during start-up
  SPI.begin();                          // VSPI: SCK GPIO 18, MISO GPIO 19, MOSI GPIO 23
}

void loop() {
  uint8_t answer = spiExchange('0');
  Serial.print("Received: 0x");
  Serial.println(answer, HEX);
  delay(100);
}

If Ethernet or the µSD card already started the bus, SPI.begin() doesn't start it again, so the order in setup() is not critical. What matters is that CS is high before the first transfer on the bus.

Read a 16-bit register

Most SPI devices work with commands: the master sends a command or register address, then sends dummy bytes to clock the answer in. The command byte and the length of the answer come from the device datasheet. This function reuses the settings of the example above.

uint16_t readRegister16(uint8_t command) {
  SPI.beginTransaction(deviceSettings);
  digitalWrite(csPin, LOW);
  delayMicroseconds(10);
  SPI.transfer(command);                // send the command or register address
  uint8_t high = SPI.transfer(0x00);    // dummy byte: clock in the first byte
  uint8_t low = SPI.transfer(0x00);     // dummy byte: clock in the second byte
  digitalWrite(csPin, HIGH);
  delayMicroseconds(10);
  SPI.endTransaction();
  return (uint16_t(high) << 8) | low;
}

Some devices need CS to stay low for the whole command, others need a pulse on CS between commands. Follow the timing diagram of the device. It is the most common source of wrong readings.

A second SPI bus with SPIClass on other ESP32 boards

On the ESP32 PLC, the external SPI port is VSPI, so you use the default SPI object. On an ESP32 board where HSPI is free, you can create a second bus with its own instance of SPIClass. Each bus then has its own clock and data lines.

#include <SPI.h>

SPIClass hspi(HSPI);                   // second SPI bus (SPI2)
const int hspiCs = 15;

void setup() {
  Serial.begin(115200);
  pinMode(hspiCs, OUTPUT);
  digitalWrite(hspiCs, HIGH);
  hspi.begin(14, 12, 13, hspiCs);      // SCK, MISO, MOSI, SS
}

void loop() {
  hspi.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
  digitalWrite(hspiCs, LOW);
  uint8_t in = hspi.transfer(0x00);
  digitalWrite(hspiCs, HIGH);
  hspi.endTransaction();
  Serial.println(in, HEX);
  delay(100);
}

Check the boot pins before you use HSPI: on the ESP32, GPIO 12 and GPIO 15 are strapping pins, and a device that pulls them to the wrong level during reset can stop the board from booting.

SPI wiring in a control cabinet

SPI is a bus for short connections between chips. It has no differential signals and no error check in the protocol. On a PLC, it is the right choice for a device that sits next to the controller, such as an external ADC, a thermocouple or RTD converter, a display or a memory chip.

  • Keep the device close. Mount it in the same cabinet as the PLC, with short wires. Long wires add capacitance and reflections, and the clock edges get worse.
  • Lower the clock when the wires get longer. Start at 1 MHz or less, check the data, then raise the clock step by step.
  • Share the ground. Connect the GND of the device to the GND of the PLC, and run it next to the signal wires.
  • Keep SPI away from noise. Route the wires away from relay outputs, contactors and motor cables.
  • Use a field bus for long distances. If the signal leaves the cabinet, use RS-485 with Modbus RTU, Ethernet or a 4-20 mA loop instead of SPI.

First SPI transfer: what to check when the data is wrong

SymptomLikely causeWhat to check
You always read 0xFFNobody drives MISOCS wiring and CS pin in the code, device power, MISO wire on the SO terminal
You always read 0x00MISO held low, or the device is in reset or not configuredMISO wiring, reset or enable pins of the device, start-up time after power on
Values are almost right, but shifted by one bitWrong SPI modeCPOL and CPHA in the datasheet, then the mode in SPISettings
Values are wrong at randomClock too fast for the wiring, or noiseLower the clock, shorten the wires, check the ground
Your device works, but Ethernet or the µSD card failThe device answers when it is not selected, or the settings change outside a transactionCS high in setup, transactions around every access, MISO released when CS is high, no SPI.end()
Your device works alone, but fails when Ethernet is activeCS low outside the transaction, or no lock between tasksCS low only between beginTransaction() and endTransaction(), all SPI accesses inside transactions

A logic analyser or an oscilloscope on SCK, MOSI, MISO and CS shows most of these problems in one capture. In the Ethernet connection test, you can check that Ethernet still works after you add your device.

Summary of SPI on the ESP32 PLC

Conclusion
  • The external SPI port of the ESP32 PLC is VSPI (SPI3): SO is MISO (GPIO 19), SI is MOSI (GPIO 23) and SCK is the clock (GPIO 18), all at 3.3 V.
  • Chip select is a pin you choose and drive in your code. Check its voltage against the device.
  • Ethernet, the µSD card and your device share the bus: CS high at start-up, one transaction per access, no SPI.end().
  • SPI is for devices inside the cabinet. For signals in the field, use RS-485, Ethernet or 4-20 mA.

From SPI to a working station. The railway carriage monitoring case uses an ESP32 PLC 57R that keeps the data on board when the 4G link drops and sends it when the link returns.

Read the railway carriage case study

To compare models and I/O, see the ESP32 PLC family.

FAQ about SPI on the ESP32 and the ESP32 PLC

What are the default SPI pins of the ESP32?

For VSPI (SPI3), the bus of the default SPI object in Arduino: SCK GPIO 18, MISO GPIO 19, MOSI GPIO 23 and CS GPIO 5. For HSPI (SPI2): SCK GPIO 14, MISO GPIO 12, MOSI GPIO 13 and CS GPIO 15. SPI0 and SPI1 are for the internal flash.

Which SPI bus does the ESP32 PLC use?

The external SPI port of the ESP32 PLC family is VSPI (SPI3): terminal SO is MISO (GPIO 19), SI is MOSI (GPIO 23) and SCK is the clock (GPIO 18). The Ethernet controller and the µSD card use the same bus.

Can the ESP32 PLC work as an SPI slave?

Not with the Arduino SPI library, which works as master only. On the ESP32 PLC the ESP32 is also the master of Ethernet and the µSD card on the same bus. To connect two PLCs, use RS-485, Ethernet or CAN.

Can I connect a 5 V SPI device to the ESP32 PLC?

Not directly. The SPI lines of the PLC work at 3.3 V and the ESP32 pins are not 5 V tolerant, so a 5 V MISO signal needs a level shifter. Some 5 V devices also don't read 3.3 V as a valid high level on their inputs.

Why does Ethernet stop working when I add an SPI device?

The usual causes are a CS that floats or stays low, a device that keeps driving MISO when it is not selected, a call to SPI.end(), or settings changed outside SPI.beginTransaction(). Set CS high in setup() and put every access inside a transaction.

What is the difference between SPI and I2C on a PLC?

SPI uses separate data lines in each direction, a clock and one CS line per device, and runs at higher clock rates. I2C uses two shared lines and selects each device by address. On the ESP32 PLC, I2C works at 5 V and SPI at 3.3 V. Both are for devices inside the cabinet.

How fast can SPI run on the ESP32?

Espressif gives up to 80 MHz on the default IO_MUX pins. Through the GPIO matrix the limit is 40 MHz, and about 26 MHz for full duplex transfers, which is how the Arduino SPI library works. On a PLC, the slave datasheet and the wiring set the real limit. Start at 1 MHz or less and raise the clock while the data stays correct.

​Search in our Blog

How to use SPI on an ESP32 based PLC
Boot & Work Corp. S.L, Ricard Franch Argullol March 29, 2023
Share this post

Looking for your ideal Programmable Logic Controller?

Take a look at this product comparison with other industrial controllers Arduino-based. 

We are comparing inputs, outputs, communications and other features with the ones of the relevant brands.

PLC Comparison