Physical Layer: Wiring the Arduino SPI Port
Before writing a single line of code, you must establish a clean physical layer. The Serial Peripheral Interface (SPI) is a synchronous, full-duplex bus that relies on four primary wires. Unlike I2C, which multiplexes data on a single line, SPI separates data into two distinct paths (MISO and MOSI), allowing simultaneous transmission and reception.
On the classic Arduino Uno R3 (ATmega328P), the hardware SPI port is mapped to specific digital pins, but it is also broken out on the 2x3 ICSP header. Using the ICSP header is the most robust method because it bypasses the digital pin multiplexer and remains consistent across different AVR board layouts.
| Signal | Digital Pin (Uno/Nano) | ICSP Header Pin | Direction (from Master) |
|---|---|---|---|
| MOSI (Master Out Slave In) | 11 | 4 | Output |
| MISO (Master In Slave Out) | 12 | 1 | Input |
| SCK (Serial Clock) | 13 | 3 | Output |
| SS / CS (Slave Select / Chip Select) | 10 (Hardware default) | N/A (Any GPIO) | Output |
Pull-Up and Pull-Down Requirements
A common misconception is that SPI requires the 4.7kΩ pull-up resistors used in I2C. SPI data lines (MOSI, MISO, SCK) do not require pull-up resistors. They are actively driven push-pull outputs. However, the Chip Select (CS) line has a critical physical layer requirement:
- CS Pull-Up (10kΩ to VCC): When the Arduino resets or boots, its GPIO pins temporarily float. If the CS line floats low, the slave device will wake up and attempt to drive the MISO line. If multiple devices are on the bus, this causes a bus contention short. A 10kΩ pull-up on every CS line keeps slaves dormant during microcontroller boot.
- MISO Pull-Up (10kΩ to VCC): Only required if you have multiple SPI devices on the same bus and one of the slave devices might be unpowered while the master is running. This prevents the unpowered chip from parasitically dragging the MISO line low through its internal ESD diodes.
Bus Mechanics: SPI vs I2C vs UART
Choosing the right protocol depends entirely on your constraints regarding distance, speed, and device count. SPI trades wiring complexity for raw throughput and simplicity of hardware implementation.
| Feature | SPI | I2C | UART |
|---|---|---|---|
| Wires Required | 4 (plus 1 CS per device) | 2 (SDA, SCL) | 2 (TX, RX) |
| Max Speed (Typical) | 10 MHz - 50+ MHz | 100 kHz - 3.4 MHz | 115.2 kbps - 3 Mbps |
| Addressing | None (Hardware CS routing) | 7-bit or 10-bit software | None (Point-to-point) |
| Max Distance | Short (< 30 cm on breadboard) | Short-Medium (< 1 m) | Long (meters, or km via RS-485) |
| Topology | Master-Slave (Multi-slave via CS) | Multi-Master Bus | Point-to-Point |
When to choose which: Use SPI when you need high bandwidth (e.g., TFT displays, SD cards, external flash) and have enough GPIO pins for individual Chip Selects. Use I2C for low-speed sensor networks (temperature, IMUs) where you want to minimize wiring and only have two pins available. Use UART for point-to-point communication over longer distances, or when talking to PCs and GPS modules.
Minimal Working Exchange: Reading an SPI Sensor
Let's wire and code a minimal exchange. We will read the "WHO_AM_I" device ID register from an ADXL345 accelerometer configured in SPI mode. The WHO_AM_I register address is 0x00, and it should return 0xE5.
Wiring Table
| Arduino Uno Pin | ADXL345 SPI Module Pin |
|---|---|
| 5V | VCC |
| GND | GND |
| D13 (SCK) | SCL |
| D11 (MOSI) | SDA (or SDI) |
| D12 (MISO) | SDO (or ALT ADDRESS) |
| D10 (CS) | CS |
Note: Ensure the CS pin on the ADXL345 module has a 10kΩ pull-up to 3.3V/5V if not already present on the breakout board.
Arduino SPI Code
Modern Arduino cores use SPI.beginTransaction() to safely configure the bus without interfering with other libraries (like an SD card library) that might share the port. You can view the official Arduino SPI Reference for deeper API details.
#include <SPI.h>
const int CS_PIN = 10;
const byte READ_FLAG = 0x80; // Bit 7 high indicates a read operation
const byte WHO_AM_I_REG = 0x00;
void setup() {
Serial.begin(115200);
while (!Serial); // Wait for serial monitor
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH); // Deselect device immediately
SPI.begin();
Serial.println("SPI Bus Initialized.");
}
void loop() {
// Configure bus: 1MHz, Most Significant Bit First, SPI Mode 3 (CPOL=1, CPHA=1 for ADXL345)
SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE3));
digitalWrite(CS_PIN, LOW); // Assert Chip Select
// Send register address with read flag
SPI.transfer(WHO_AM_I_REG | READ_FLAG);
// Send dummy byte (0x00) to clock out the response from MISO
byte deviceId = SPI.transfer(0x00);
digitalWrite(CS_PIN, HIGH); // Deassert Chip Select
SPI.endTransaction();
Serial.print("Device ID: 0x");
Serial.println(deviceId, HEX);
if (deviceId == 0xE5) {
Serial.println("Success: ADXL345 detected on SPI bus.");
} else {
Serial.println("Error: Unexpected Device ID. Check wiring and SPI Mode.");
}
delay(2000);
}
Debugging the SPI Bus: Sniffing and Fixing Classic Failures
When your SPI device returns garbage data, 0xFF, or 0x00, guessing is a waste of time. You need to see the physical layer. The SparkFun SPI Tutorial provides excellent oscilloscope captures of what these signals should look like, but for bench debugging, a logic analyzer is mandatory.
How to Sniff the Bus
Purchase a generic 24MHz 8-channel USB logic analyzer (often based on the Cypress CY7C68013A chip, costing around $12-$15). Download PulseView (Sigrok), an open-source logic analyzer GUI. Connect the probes to MOSI, MISO, SCK, and CS, and set the ground clip to the Arduino GND. In PulseView, add the SPI protocol decoder, assign the channels, and trigger on the CS falling edge. You will instantly see if the master is clocking data and if the slave is responding.
The Classic Failures
- Baud and Mode Mismatch (CPOL/CPHA): SPI has four modes (0, 1, 2, 3) dictated by Clock Polarity (CPOL) and Clock Phase (CPHA). If your master uses Mode 0 but the sensor requires Mode 3, the data will be sampled on the wrong clock edge, resulting in bit-shifted garbage. Always check the sensor datasheet for the required SPI Mode and set it in
SPISettings(). - Chip Select (CS) Clashes: Unlike I2C, SPI doesn't suffer from software address clashes. Instead, it suffers from CS clashes. If you wire two devices to the same CS pin, or forget to set an unused device's CS pin HIGH before initializing a new one, both devices will drive the MISO line simultaneously, causing a short and corrupting data.
- Missing Pull-Up on CS: If your code works perfectly but the bus locks up every time you press the Arduino reset button, your CS line is floating during boot. Add the 10kΩ pull-up resistor.
- Wire Length and Capacitance: SPI is not designed for long cables. At 8 MHz, a 50cm ribbon cable will introduce enough parasitic capacitance to round off the square clock waves into sine waves, causing the slave to miss clock edges. Keep SPI traces/wires under 30cm. If you must go further, drop the clock speed to 1 MHz or use differential SPI transceivers.
Arduino SPI Port FAQ
Can I use any digital pin for the Arduino SPI port CS?
Yes. While the hardware SS pin (D10 on the Uno) must be kept as an OUTPUT to prevent the ATmega328P from accidentally switching into SPI Slave mode, you can use any available digital GPIO pin for the actual Chip Select signal. In fact, when managing multiple SPI devices, you will assign a unique digital pin to each device's CS line. Just remember to initialize them all as OUTPUT and set them HIGH in your setup() function.
Why is my SPI device returning 0xFF or 0x00 on every read?
A solid 0xFF usually means the MISO line is being pulled high (or floating high) and the slave device is not responding. This is almost always caused by the CS line never going LOW, a broken MISO wire, or the slave device lacking power. A solid 0x00 usually means the MISO line is shorted to ground, or you are reading the bus before the slave has had time to power up and initialize. Use your logic analyzer to verify that CS actually drops to 0V during the transaction.
What is the maximum cable length for an Arduino SPI port?
For standard 5V or 3.3V single-ended SPI running at 8 MHz to 16 MHz, the practical limit on a breadboard or ribbon cable is about 30 cm (12 inches). If you lower the clock speed to 1 MHz or 500 kHz using SPISettings, you can reliably push the distance to 1 meter. For distances beyond 1 meter, you must abandon standard single-ended SPI and use RS-422/RS-485 differential line drivers to transmit the SPI signals over twisted pair cables.
Does the Arduino SPI port need pull-up resistors like I2C?
No. The MOSI, MISO, and SCK lines are push-pull outputs that actively drive the lines HIGH and LOW; they do not rely on external pull-up resistors to achieve a HIGH state. Adding pull-ups to the data and clock lines will only increase the current draw and slow down the rising edge times due to the RC time constant formed with the wire capacitance. The only line that typically requires a pull-up is the CS line, to keep the slave device deselected during microcontroller resets.






