Introduction
Communication protocols are the backbone of embedded systems. Whether you’re reading a sensor, talking to an SD card, or printing debug messages, you’re using one of the three major protocols: Serial (UART), I2C (TWI), or SPI. In this tutorial, we’ll run all three simultaneously on an Arduino Uno using the Velxio in-browser simulator. You’ll see how to scan the I2C bus, read/write an EEPROM, perform SPI transfers, and log everything over Serial.

Circuit Walkthrough
This example uses only the Arduino Uno board itself—no external components are required. The Arduino’s built-in peripherals provide everything we need:
- Serial (UART): Uses pins 0 (RX) and 1 (TX) for communication with the Serial Monitor.
- I2C (TWI): Uses pins A4 (SDA) and A5 (SCL). The internal pull‑up resistors are enabled by the Wire library.
- SPI: Uses pins 10 (SS), 11 (MOSI), 12 (MISO), and 13 (SCK). Pin 10 is configured as the slave select output.
No wiring is needed because all connections are internal to the Arduino. The Velxio canvas shows the Uno with its pin labels, and you can inspect the virtual components by opening the component picker.

Code Walkthrough
Let’s break down the sketch step by step.
Libraries and Definitions
#include <Wire.h>
#include <SPI.h>
#define DS1307_ADDR 0x68
#define EEPROM_ADDR 0x50
#define SS_PIN 10
We include the Wire library for I2C and the SPI library for SPI. Two I2C addresses are defined: 0x68 for a DS1307 RTC (though not physically present, we’ll scan for it) and 0x50 for a 24Cxx EEPROM. The SPI slave select pin is set to digital pin 10.
Helper Functions
byte bcdToDec(byte val) {
return ((val >> 4) * 10) + (val & 0x0F);
}
void readRTC(byte &hr, byte &min, byte &sec) {
Wire.beginTransmission(DS1307_ADDR);
Wire.write(0x00);
Wire.endTransmission();
Wire.requestFrom(DS1307_ADDR, 3);
sec = bcdToDec(Wire.read() & 0x7F);
min = bcdToDec(Wire.read());
hr = bcdToDec(Wire.read() & 0x3F);
}
The bcdToDec function converts Binary‑Coded Decimal to decimal. readRTC reads the seconds, minutes, and hours from a DS1307 RTC. The & 0x7F and & 0x3F masks clear unused bits.
void writeEEPROM(byte reg, byte value) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.write(value);
Wire.endTransmission();
delay(5);
}
byte readEEPROM(byte reg) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write(reg);
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
return Wire.available() ? Wire.read() : 0xFF;
}
These functions write and read a single byte from an I2C EEPROM at address 0x50. The delay(5) ensures the EEPROM has time to complete the write cycle.
byte spiTransfer(byte data) {
digitalWrite(SS_PIN, LOW);
byte result = SPI.transfer(data);
digitalWrite(SS_PIN, HIGH);
return result;
}
This function performs a full‑duplex SPI transfer: it pulls SS low, sends a byte, receives the response, and pulls SS high.
Setup
void setup() {
Serial.begin(9600);
Wire.begin();
pinMode(SS_PIN, OUTPUT);
digitalWrite(SS_PIN, HIGH);
SPI.begin();
Serial.println("===================================");
Serial.println(" Multi-Protocol Demo");
Serial.println(" Serial (USART) + I2C (TWI) + SPI");
Serial.println("===================================");
Serial.println();
// ── I2C: Scan bus ──
Serial.println("[I2C] Scanning bus...");
int found = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
Serial.print(" Found device at 0x");
if (addr < 16) Serial.print("0");
Serial.println(addr, HEX);
found++;
}
}
Serial.print(" ");
Serial.print(found);
Serial.println(" device(s) on I2C bus.");
Serial.println();
// ── I2C: Write/read EEPROM ──
Serial.println("[I2C] EEPROM write/read test:");
writeEEPROM(0, 42);
writeEEPROM(1, 99);
byte v0 = readEEPROM(0);
byte v1 = readEEPROM(1);
Serial.print(" Wrote 42, read ");
Serial.print(v0);
Serial.println(v0 == 42 ? " [OK]" : " [FAIL]");
Serial.print(" Wrote 99, read ");
Serial.print(v1);
Serial.println(v1 == 99 ? " [OK]" : " [FAIL]");
Serial.println();
// ── SPI: Transfer test ──
Serial.println("[SPI] Transfer test:");
byte spiData[] = {0xAA, 0x55, 0x42};
for (int i = 0; i < 3; i++) {
byte rx = spiTransfer(spiData[i]);
Serial.print(" TX=0x");
if (spiData[i] < 16) Serial.print("0");
Serial.print(spiData[i], HEX);
Serial.print(" RX=0x");
if (rx < 16) Serial.print("0");
Serial.println(rx, HEX);
}
Serial.println();
Serial.println("Setup complete. Reading RTC...");
Serial.println();
}
In setup(), we initialize all three protocols. Then we:
- Scan the I2C bus for devices from address 1 to 126.
- Test EEPROM by writing values 42 and 99 to addresses 0 and 1, then reading them back and verifying.
- Test SPI by sending three bytes (
0xAA,0x55,0x42) and printing the received bytes.
Loop
void loop() {
// ── Serial: Print RTC time every 2 seconds ──
byte hr, min, sec;
readRTC(hr, min, sec);
Serial.print("[RTC] ");
if (hr < 10) Serial.print("0");
Serial.print(hr);
Serial.print(":");
if (min < 10) Serial.print("0");
Serial.print(min);
Serial.print(":");
if (sec < 10) Serial.print("0");
Serial.print(sec);
Serial.print(" | Uptime: ");
Serial.print(millis() / 1000);
Serial.println("s");
delay(2000);
}
Every two seconds, the loop reads the RTC time and prints it along with the system uptime. This demonstrates continuous Serial output while I2C and SPI are idle.
Key Concepts
- Serial (UART): Asynchronous, point‑to‑point. Used for debug output and communication with PCs or modules like GPS.
- I2C: Synchronous, multi‑master, uses two wires (SDA, SCL). Each device has a unique address. Great for sensors and EEPROMs.
- SPI: Synchronous, full‑duplex, uses four wires (MOSI, MISO, SCK, SS). Faster than I2C but requires more pins.
- Bus Scanning: Iterating through I2C addresses to discover connected devices.
- EEPROM Read/Write: Non‑volatile memory that retains data after power loss.
- SPI Transfer: Simultaneous send and receive of data.
Common Pitfalls & Debugging Tips
- I2C Pull‑up Resistors: The Wire library enables internal pull‑ups, but for longer buses external resistors (4.7kΩ) are recommended.
- SPI SS Pin: Always set SS as output and keep it high when not in use to avoid accidentally entering slave mode.
- EEPROM Write Timing: After a write, wait at least 5 ms before the next operation.
- Serial Monitor Baud Rate: Ensure the Serial Monitor is set to 9600 baud.
- No External Devices: In this simulation, the I2C scan will find no devices (except possibly the Arduino’s internal TWI). The EEPROM test uses a virtual EEPROM built into the simulator.
Suggested Extensions
- Add Real Sensors: Connect a DS1307 RTC and a 24Cxx EEPROM to the I2C bus, and an SD card module via SPI.
- Log Data to EEPROM: Store sensor readings and retrieve them after reset.
- Use Interrupts: Trigger an SPI transfer on a button press.
- Implement a Command Interface: Parse Serial commands to control I2C and SPI operations.
Try It Yourself
Ready to see all three protocols in action? Open the live example in Velxio, compile, and run. Watch the Serial Monitor output as the Arduino scans the I2C bus, tests the EEPROM, performs SPI transfers, and then continuously prints the RTC time.
Launch the Multi-Protocol Demo on Velxio

Happy hacking!