spi-bus-nanoframework

SPI Bus in nanoFramework: Fast Communication

  • 4 min

The SPI bus is a synchronous communication protocol designed for high-speed data transfer.

If we compare I2C to a country road (reliable, two lanes, moderate speed), SPI is a German autobahn with no speed limit.

While I2C typically runs at 100kHz or 400kHz, SPI can easily reach 10MHz, 40MHz, or even 80MHz. That’s why it’s the standard choice for devices that handle large volumes of data, such as:

  • LCD/TFT/OLED color displays.
  • SD card readers.
  • Ethernet modules (like the ENC28J60).
  • RFID/NFC readers (like the RC522).

In this post, we will learn how to tame this speed using the SpiDevice class.

For this tutorial, you need to install the NuGet package: nanoFramework.System.Device.Spi

The SPI Bus Lines

Speed comes at a price: cables. SPI requires 4 lines to operate (compared to 2 for I2C).

  • SCK (Serial Clock): The clock that sets the pace (generated by the Master).
  • MOSI (Master Out Slave In): Data flowing from the microcontroller to the sensor.
  • MISO (Master In Slave Out): Data flowing from the sensor to the microcontroller.
  • CS / SS (Chip Select): The most important one! It’s an individual pin for each device. If you connect 3 SPI sensors, they will share SCK, MOSI, and MISO, but you will need 3 separate CS pins.

Full-Duplex Communication

A unique feature of SPI is that it is Full-Duplex. Imagine two people exchanging business cards at the same time. In the same clock cycle, the Master sends a bit to the Slave AND the Slave sends a bit to the Master. Data is always exchanged, even if one of them sends “junk” (dummy bytes) just to push the other’s data through.

Configuration in nanoFramework

On the ESP32, we have two hardware SPI buses available for the user: VSPI (Bus 1) and HSPI (Bus 2).

By default, on most ESP32 DevKits, VSPI (which we often call SPI1) is on:

  • MOSI: GPIO 23
  • MISO: GPIO 19
  • SCK: GPIO 18
  • CS: an available GPIO, usually pin 5

The main class is SpiDevice. First, we configure the connection with SpiConnectionSettings.

using System.Device.Spi;

// Configuration
// 1. Bus ID: check the target mapping
// 2. Chip Select Pin: GPIO 5
SpiConnectionSettings settings = new SpiConnectionSettings(1, 5);

// Advanced parameters (Crucial to read from the sensor's Datasheet)
settings.ClockFrequency = 1000000; // 1 MHz (Speed)
settings.Mode = SpiMode.Mode0;     // Clock polarity and phase (Mode0 is the most common)

// Create the device
SpiDevice dispositivo = SpiDevice.Create(settings);
Copied!

SPI Mode (0, 1, 2, 3) This defines when data is read (whether on the rising or falling edge of the clock). If you configure the mode incorrectly, you will read garbage. 90% of devices use Mode0, but always check the datasheet.

Transferring Data: Write, Read, and TransferFullDuplex

We have three ways to communicate:

Write Only with Write

Used to send commands or configure registers (e.g., sending pixels to a display).

byte[] command = new byte[] { 0x01, 0xFF };
device.Write(command);
Copied!

Read Only with Read

Used to read data when we don’t need to send anything specific (though internally, SPI always sends zeros to be able to read).

byte[] buffer = new byte[4];
device.Read(buffer);
Copied!

Exchange with TransferFullDuplex

The “pure” form of SPI. You send one array and receive another of the same size simultaneously.

byte[] writeBuffer = new byte[] { 0xAA, 0xBB };
byte[] readBuffer = new byte[writeBuffer.Length];

device.TransferFullDuplex(writeBuffer, readBuffer);

// Now 'readBuffer' contains what the sensor replied WHILE we were sending AA and BB.
Copied!

Example: A Dummy Byte

Let’s say we want to read a value from an RFID sensor. It typically works like this:

  1. We pull the CS line low (automatic with SpiDevice).
  2. We send the address of the register we want to read (e.g., 0x15).
  3. The sensor needs clock cycles to respond, so we send an “empty” byte (0x00) to “push” the response towards us.
// We want to read register 0x15
// Byte 0: Address 0x15
// Byte 1: 0x00 (Dummy to give time to receive the data)
byte[] writeBuffer = new byte[] { 0x15, 0x00 };
byte[] readBuffer = new byte[2];

device.TransferFullDuplex(writeBuffer, readBuffer);

// readBuffer[0] -> Garbage (what we received while sending the address)
// readBuffer[1] -> The actual data from register 0x15!
byte validData = readBuffer[1];
Copied!

This byte dance is what’s confusing at first, but it’s the foundation of SPI’s speed.

Drivers and Graphic Devices

As with I2C, the great advantage of nanoFramework is its ecosystem.

If you want to use a TFT ILI9341 display or an RC522 reader, do not write the SPI protocol by hand. Use the NuGet drivers.

For example, for a card reader:

  1. Install nanoFramework.Iot.Device.Mfrc522.
  2. Initialize it by passing the already configured SpiDevice.

The driver will handle all the register, command, and timing management, taking advantage of the SPI bus speed we configured.