Learning Goals 5 min
Yesterday was theory. Today you actually plug an I²C device in, scan the bus to find it, and read its address back. Wire is the Arduino library that hides every detail of START / ACK / STOP — you write 4-line transactions and the library handles the rest. By the end of this lesson you will:
- Wire one or more I²C breakout boards to the UNO's A4/A5 pins with shared GND.
- Run the canonical "I²C scanner" sketch and read off the address of every connected device.
- Use
Wire.beginTransmission,Wire.write,Wire.endTransmission,Wire.requestFrom, andWire.readto manually write one register and read one register from a device.
Warm-Up 10 min
Find any I²C breakout from your kit. Good candidates:
- SSD1306 OLED (we'll display on it tomorrow — for today, just scan).
- I²C LCD backpack (from L02-27 / L02-28).
- BMP280, BME280, MPU-6050, DS3231 if you have them.
- An I²C breakout you bought and haven't used yet — perfect, today is "just check that the wiring works".
Wire it to the UNO
A4 and A5 are not a free choice on an UNO — those two pins are the bus, and every device you add hangs off the same pair. That is the whole point of a bus, and it is why the scanner sketch below can find a board you have not been told the address of.| Breakout pin | UNO pin |
|---|---|
| VCC (often labelled +, 3V3, or VCC) | 5V (or 3V3 if the breakout requires 3.3 V — check first) |
| GND | GND |
| SDA | A4 |
| SCL | A5 |
Optional pull-ups
Most breakouts have built-in pull-ups. If yours doesn't (rare), add 4.7 kΩ from SDA→5V and SCL→5V on the breadboard.
New Concept · The Wire API 25 min
Step 1 · Include and begin
#include <Wire.h>
void setup() {
Wire.begin(); // join the bus as the controller
Serial.begin(9600);
}Wire.begin() with no arguments makes the UNO the I²C controller — driving the clock, initiating all transactions. With an argument, it becomes a peripheral with that address (very rarely needed for Arduino).
The five API methods you actually use
| Method | What it does |
|---|---|
Wire.beginTransmission(addr) | Queue up a write to addr. Doesn't actually send anything yet. |
Wire.write(byte) | Append one byte to the queue. |
Wire.endTransmission() | Send everything queued; returns a status code (0 = success, > 0 = error). |
Wire.requestFrom(addr, n) | Ask addr for n bytes. Bytes arrive into an internal buffer. |
Wire.read() | Read one byte from the buffer. |
A complete "set register, then read register" transaction
To read the temperature from a BMP280 (just an example):
const uint8_t BMP280_ADDR = 0x76;
const uint8_t REG_TEMP_MSB = 0xFA;
// Step 1: tell the device which register to read
Wire.beginTransmission(BMP280_ADDR);
Wire.write(REG_TEMP_MSB);
Wire.endTransmission();
// Step 2: ask for 3 bytes (temperature is 20 bits packed into 3 bytes)
Wire.requestFrom(BMP280_ADDR, (uint8_t)3);
uint32_t raw = 0;
raw |= ((uint32_t)Wire.read()) << 12;
raw |= ((uint32_t)Wire.read()) << 4;
raw |= ((uint32_t)Wire.read()) >> 4;You wouldn't normally write this — the Adafruit_BMP280 library does it for you. But knowing the pattern lets you read any I²C chip's datasheet and write minimal code without needing a library.
Error codes from endTransmission()
| Code | Meaning |
|---|---|
| 0 | Success — peripheral ACKed everything. |
| 1 | Data too long for the Wire library's 32-byte buffer. |
| 2 | NACK on the address byte. Device not present at that address. |
| 3 | NACK on a data byte. Device didn't want any more data. |
| 4 | Other error (bus error, timeout, etc.). |
The bus scanner uses code 0 vs 2 to distinguish "device exists" vs "nothing at this address".
The canonical I²C scanner
#include <Wire.h>
void setup() {
Serial.begin(9600);
Wire.begin();
delay(2000);
Serial.println("# I2C scanner");
}
void loop() {
byte found = 0;
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte err = Wire.endTransmission();
if (err == 0) {
Serial.print("Found device at 0x");
if (addr < 16) Serial.print('0');
Serial.println(addr, HEX);
found++;
}
}
if (!found) Serial.println("No devices found.");
Serial.println();
delay(5000);
}This loops every 5 seconds. Plug in a device → it shows up next scan. Unplug → it disappears. The fastest debugging tool in I²C land.
Worked Example · Scan, identify, manually poke 25 min
Step 1 — wire your device(s) to the UNO
One or more I²C breakouts: VCC, GND, SDA → A4, SCL → A5. Multiple devices share the same pins.
Step 2 — upload the scanner from §3 and check the output
Open Serial Monitor at 9600 baud. After a few seconds you should see something like:
# I2C scanner Found device at 0x3C Found device at 0x68
Match the addresses against your device table from L03-17. 0x3C = SSD1306 OLED; 0x68 = MPU-6050 or DS3231. If your scan shows a device whose address you don't recognise, that's either a chip you forgot about or an address change you set.
Step 3 — write a single byte to a real device
Let's send the "display off" command to an SSD1306 OLED. Even without an OLED library, this should make the display go dark (if it was on).
// L03-18 · Manual OLED command using Wire
#include <Wire.h>
const uint8_t OLED_ADDR = 0x3C;
const uint8_t CTRL_COMMAND = 0x00; // next byte is a command, not data
const uint8_t CMD_DISPLAY_OFF = 0xAE;
const uint8_t CMD_DISPLAY_ON = 0xAF;
void sendCommand(uint8_t cmd) {
Wire.beginTransmission(OLED_ADDR);
Wire.write(CTRL_COMMAND);
Wire.write(cmd);
byte err = Wire.endTransmission();
Serial.print("# sent cmd 0x");
Serial.print(cmd, HEX);
Serial.print(" err=");
Serial.println(err);
}
void setup() {
Serial.begin(9600);
Wire.begin();
delay(2000);
Serial.println("# OLED off in 2s, on in 4s, off in 6s...");
}
void loop() {
delay(2000);
sendCommand(CMD_DISPLAY_OFF);
delay(2000);
sendCommand(CMD_DISPLAY_ON);
}If your OLED is fresh-out-of-box, "display off" may have no visible effect (the OLED hasn't been initialised, so it's blank anyway). The point of this exercise is to confirm the WRITE works — check that err prints as 0. err = 2 means "address NACK" → no device responded.
Step 4 — read a byte back
Many I²C chips have a "chip ID" register at a fixed address. Reading it back is a self-test. For an MPU-6050, the WHO_AM_I register is 0x75 and should contain 0x68:
#include <Wire.h>
const uint8_t MPU_ADDR = 0x68;
const uint8_t WHO_AM_I = 0x75;
void setup() {
Serial.begin(9600);
Wire.begin();
delay(1000);
Wire.beginTransmission(MPU_ADDR);
Wire.write(WHO_AM_I);
Wire.endTransmission();
Wire.requestFrom(MPU_ADDR, (uint8_t)1);
if (Wire.available()) {
uint8_t id = Wire.read();
Serial.print("WHO_AM_I = 0x");
Serial.println(id, HEX);
} else {
Serial.println("# no reply");
}
}
void loop() { }If you have an MPU-6050, you should see WHO_AM_I = 0x68. That's your "hello" from the chip — and you got it without any motion library, just Wire.
Step 5 — break the wiring on purpose
Pull the SDA wire (A4) while the scanner is running. The scanner output stops showing devices — wires not connected, no ACK from anyone. Plug it back in → devices reappear. This is the "eyes on the bus" debugging that the scanner gives you. Save the scanner sketch with the rest of your reusable tools.
Basic 5 min
Goal: Modify the scanner to print the total count of devices found at the end of each scan: "3 devices found".
Challenge 1 5 min
Goal: Write a helper i2cAlive(addr) that returns true if a device at addr responds, false if not. Call it once per second on a known address; if the result changes (e.g. the device disappears), print a warning.
Challenge 2 5 min
Goal: Build a tiny "register dump" tool. Type an address in hex into the Serial Monitor (e.g. 0x68), then a register (e.g. 0x75), and the sketch reads back one byte. Useful for poking unknown chips when their library hasn't been written yet.
Challenge 3 · Fix the blind scanner 10 min
A classmate typed the scanner from memory and wired an OLED to test it. Their wiring:
- OLED VCC → 5V, OLED GND → GND.
- OLED SDA → A5, OLED SCL → A4.
#include <Wire.h>
void setup() {
Serial.begin(9600);
Wire.begin();
Serial.println("# I2C scanner");
}
void loop() {
byte found = 0;
for (byte addr = 0; addr < 127; addr++) {
Wire.beginTransmission(addr);
byte err = Wire.endTransmission();
if (err = 0) {
Serial.print("Found device at 0x");
if (addr < 16) {
Serial.print('0');
}
Serial.println(addr);
found++;
}
}
Serial.print(found);
Serial.println(" devices found.");
delay(5000);
}There are four mistakes: one in the wiring and three in the code. For each one, write what it causes and the fix. One code mistake only shows up once the others are fixed: the OLED is reported at 0x60.
It works if the scanner prints Found device at 0x3C and 1 devices found. Pull the SDA wire and it must print 0 devices found.
Recap 5 min
The Wire library is the I²C equivalent of Servo and Stepper — five method calls do all the wire-level work. Wire.begin(), then a transaction pattern: beginTransmission + write + endTransmission for sending; requestFrom + read for receiving. The address-scanner sketch is the most useful debugging tool in this cluster — keep it in your toolbox. Tomorrow we put a real graphical display on the bus: the SSD1306 OLED, 128×64 monochrome, no extra wires beyond the four you used today.
Wirelibrary- Arduino's built-in I²C library. Handles START, ACK, STOP and clock generation for you.
Wire.begin()- Join the I²C bus as the controller. With an argument, joins as a peripheral at the given address (rarely used in Arduino projects).
Wire.beginTransmission(addr)- Queue up a write to address
addr. No bytes are sent untilendTransmission(). Wire.write(byte)- Append one byte to the transmission queue. Can be called multiple times to queue up several bytes.
Wire.endTransmission()- Send the queued bytes. Returns a status code (0 = success, 2 = NACK on address, etc.). Always check the return value when debugging.
Wire.requestFrom(addr, n)- Ask address
addrto sendnbytes. Bytes arrive into the Wire library's internal buffer; read them out withWire.read(). Wire.read()- Pop one byte from the receive buffer.
- I²C scanner
- A diagnostic sketch that tries to ACK every address 1..126 and prints which ones respond. Standard first step for any I²C wiring problem.
- WHO_AM_I register
- A read-only register found on many sensors that returns a fixed identifier. Reading it back is a self-test that confirms both the wiring and the chip are alive.
Extra Mission 5 min
Part 1 — Design a gadget that notices a missing part
Good gadgets warn you when a part comes loose. Design a plug-in sensor station, where sensor boards can be swapped on and off one I²C bus. Plan how it notices.
Your design must include:
- The boards it expects, with their addresses.
- How often it checks the bus, and why that is often enough.
- What it shows when a board disappears, and when it comes back: a message, an LED, or both.
- What it does if an unexpected address appears.
Part 2 — Make it
Build a bus-health monitor. Every 2 seconds, use your i2cAlive() helper from Challenge 1 to check each address you expect. Light the LED on D13 only when every board is present. Print a message to the Serial Monitor whenever a board appears or disappears.
It works if pulling a board's SDA wire turns the LED off and prints its address. Plugging it back in must reverse both.
Bring back next class: your design, your sketch and a screenshot of a board going and returning. Also bring an SSD1306 OLED (or an I²C 16×2 LCD) for L03-19. If you can, install the Adafruit_SSD1306 and Adafruit_GFX libraries.