100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

I2C and SPI Communication

How Arduino talks to sensors, displays, and other chips over two dominant synchronous serial buses: I2C's two-wire shared bus and SPI's fast four-wire full-duplex link.

Communication & LibrariesIntermediate10 min readJul 10, 2026
Analogies

Why Arduino Needs Dedicated Buses

A single Arduino has only a handful of I/O pins, yet a real project may need a temperature sensor, an OLED screen, a real-time clock, and an SD card all at once. Wiring each device with its own dedicated parallel lines would exhaust the pins instantly. I2C (Inter-Integrated Circuit) and SPI (Serial Peripheral Interface) solve this by letting many chips share the same small set of wires, each device addressed or selected in turn. Both are synchronous serial buses, meaning a shared clock line keeps sender and receiver in lockstep so no timing guesswork is required.

🏏

Cricket analogy: Like a single set of stumps shared by both batting ends: rather than building a fresh pitch per delivery, one shared strip serves every ball, just as one bus serves every chip.

I2C: Two Wires, Many Devices

I2C uses just two lines: SDA (serial data) and SCL (serial clock), plus a shared ground. On an Arduino Uno these are pins A4 (SDA) and A5 (SCL). Every device on the bus has a unique 7-bit address, so the master (the Arduino) begins a transaction by broadcasting the target address, and only the matching chip responds. Because the lines are open-drain, they need pull-up resistors (commonly 4.7 kΩ) to a positive rail; most breakout boards include these already. I2C is slower than SPI, typically 100 kHz standard or 400 kHz fast mode, but its two-wire simplicity makes it ideal for connecting many low-bandwidth sensors.

🏏

Cricket analogy: The umpire calling a specific batter's name before a review is like the master broadcasting a 7-bit address so only the addressed chip answers.

cpp
#include <Wire.h>

void setup() {
  Wire.begin();           // join the I2C bus as master
  Serial.begin(9600);
}

void loop() {
  // Request 1 byte from a DS3231 RTC at address 0x68 (seconds register)
  Wire.beginTransmission(0x68);
  Wire.write(0x00);       // point to register 0
  Wire.endTransmission();

  Wire.requestFrom(0x68, 1);
  if (Wire.available()) {
    byte seconds = Wire.read();
    Serial.print("Seconds (BCD): 0x");
    Serial.println(seconds, HEX);
  }
  delay(1000);
}

Stuck bus? Upload the classic I2C scanner sketch: loop addresses 1–126, call Wire.beginTransmission(addr) and Wire.endTransmission(), and print any address that returns 0. It confirms your wiring and reveals each device's real address before you write driver code.

SPI: Four Wires, Full Speed

SPI trades I2C's minimal wiring for raw speed and full-duplex data flow. It uses four signals: SCK (clock), MOSI (master out, slave in), MISO (master in, slave out), and one SS/CS (slave select) line per device. On an Uno these live on pins 13, 11, and 12, with any spare pin acting as a chip select. Instead of addressing by number, the master pulls a device's CS line low to activate it, then shifts bits out on MOSI while simultaneously reading bits back on MISO in the same clock cycles. Clock speeds of several megahertz make SPI the natural choice for SD cards, TFT displays, and high-rate sensors, at the cost of one extra pin per additional device.

🏏

Cricket analogy: Full-duplex SPI is like a quick single where both batters run in opposite directions at once, sending and receiving in the same instant.

I2C and SPI use different logic conventions and can conflict on voltage. Many modern breakouts run at 3.3 V; feeding them the Uno's 5 V logic can permanently damage them. Check each device's datasheet and add a level shifter when mixing 5 V and 3.3 V parts, especially on SPI lines like MOSI and SCK.

cpp
#include <SPI.h>

const int CS_PIN = 10;

void setup() {
  pinMode(CS_PIN, OUTPUT);
  digitalWrite(CS_PIN, HIGH);   // deselect device
  SPI.begin();
}

byte readRegister(byte reg) {
  SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
  digitalWrite(CS_PIN, LOW);    // select device
  SPI.transfer(reg | 0x80);     // 0x80 = read bit for many sensors
  byte value = SPI.transfer(0x00); // send dummy, receive data
  digitalWrite(CS_PIN, HIGH);   // deselect
  SPI.endTransaction();
  return value;
}

Choosing Between Them

Reach for I2C when pin count matters more than speed: multiple slow sensors, an RTC, and a small OLED can all coexist on the same two wires. Reach for SPI when throughput dominates, such as streaming to an SD card or refreshing a color display, and you can spare a chip-select pin per device. Many projects blend both: an ESP32 might drive a fast SPI display while polling a bank of I2C environmental sensors. Understanding each bus's trade-offs lets you allocate the microcontroller's limited pins deliberately instead of running out halfway through a build.

🏏

Cricket analogy: Choosing I2C vs SPI is like picking a spinner for control or a pace bowler for speed: match the tool to what the situation demands.

  • I2C uses two wires (SDA, SCL) with 7-bit addressing and open-drain pull-ups, ideal for many slow devices.
  • SPI uses four wires (SCK, MOSI, MISO, CS) with one chip-select per device and runs at megahertz speeds full-duplex.
  • On an Uno, I2C is on A4/A5 and hardware SPI is on pins 11/12/13 plus a chosen CS pin.
  • The Wire library drives I2C; the SPI library drives SPI, using beginTransaction to set speed and mode.
  • Watch voltage levels: many breakouts are 3.3 V and need level shifting from the Uno's 5 V logic.
  • Use an I2C scanner to discover device addresses and confirm wiring before writing drivers.
  • Blend both buses in one project: SPI for high-bandwidth peripherals, I2C for banks of low-rate sensors.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ArduinoProgrammingStudyNotes#I2CAndSPICommunication#I2C#SPI#Communication#Arduino#StudyNotes#SkillVeris#ExamPrep