I²C
I²C, said "eye-squared-see", connects many parts over just two wires. Every part has its own address, so the Pi can call one by name while the rest ignore it.
SCL is the clock and SDA is the data. The Pi sets the pace, at 100,000 ticks a second. That's slower than SPI, but it stays at two wires however many parts you add.
Every message starts with an address. All the parts hear it, and only the one with that address answers, by pulling SDA low on the last tick. That answer is called an acknowledge. If nothing answers, nothing is there.
The wires rest high. Weak pull-ups hold both wires at 3.3 volts, and a part sends a 0 by pulling the wire low, then letting go. Because nothing ever drives a wire high, two parts can't fight over it.
Parts keep their data in registers: small numbered slots inside the chip. To read one, you send its number and read back what's in it. To change a setting, you write to one.
Power I²C parts from 3.3 V, not 5 V. Some boards tie SDA and SCL to their own supply, so a 5 V feed can put 5 volts on the Pi's pins, and that can damage them.
What uses it
These parts all speak I²C:
- Motion sensors, such as the MPU-6050
- Temperature and pressure sensors, such as the BME280
- Small OLED displays, such as the SSD1306
- Analog-to-digital converters, such as the ADS1115
- Servo driver boards, such as the PCA9685, which runs 16 servos from these two wires
I²C only moves the bytes. What each register means is up to the part, and its data sheet lists them.
Read a motion sensor
A GY-521 is a cheap board with a motion sensor on it. It measures the pull of gravity, so it can tell which way up it is. Leave its XDA, XCL and INT pins unconnected.
- Pin 13.3 VTo the board's VCC
- Pin 3GPIO 2SDA, to the board's SDA
- Pin 5GPIO 3SCL, to the board's SCL
- Pin 6GNDTo the board's GND
Pi and its devices come from a library, not from the language, so this code won't build
without it. In IntelliJ, choose Tools | Nylon | Add Nylon Library… and pick raspberry-pi. That adds
one line to your project's build.markup.code:
(+dependencies : RepositoryDependency coordinate={"com.grandtheorem:raspberry-pi:0.1.0"} /)
Every file starts with its namespace. Pi is the board, nylon.time gives us sleep, and
0x68 is the sensor's address on the bus.
namespace motion
import grandtheorem.rpi.Pi
import namespace nylon.time
let SENSOR: u8 = u8(0x68)
Each reading arrives as two bytes, high byte first, and can be negative. This turns them back into one number.
let signed(high: u8, low: u8): i32 {
let value = i32(high) * 256 + i32(low)
if (value >= 32768) {
return value - 65536
}
return value
}
Ask the board for its I²C bus. Register 0x75 holds the chip's ID, which proves it's there and
answering.
entrypoint.default main() {
let i2c = Pi.i2c()
println("Chip ID ${i2c.readRegister(SENSOR, u8(0x75))}")
The chip starts asleep, and reads as zero until you wake it. Writing 0 to register 0x6B does
that, and it needs a moment to get going.
i2c.writeRegister(SENSOR, u8(0x6B), u8(0))
sleep(100.milliseconds())
The readings start at register 0x3B. Send that number once, and transfer reads the next six
bytes in one go, so all three come from the same moment.
let send: u8[] = [u8(0x3B)]
let data = mut u8[6]
for (20 times) {
i2c.transfer(SENSOR, send, data)
Three pairs of bytes make the three directions. Then wait half a second and read again.
let x = signed(data[0], data[1])
let y = signed(data[2], data[3])
let z = signed(data[4], data[5])
println("x ${x}, y ${y}, z ${z}")
sleep(500.milliseconds())
}
}
The run window shows something like this, with your own numbers:
Chip ID 112
x 412, y -88, z 16204
Lying flat, z reads about 16,000, because that's what gravity looks like to this chip. Upside down it goes negative, and a cheap chip can sit a few hundred off. Tilt the board and watch the pull move between x, y and z.
The chip ID says what you really have. 104 means a real MPU-6050, and 112 means an MPU-6500, which many boards sold as MPU-6050s carry. Clones and sister chips report other numbers, such as 113 for an MPU-9250. None of those are faults, and this program works the same on all of them.
If the run stops with "did not acknowledge", nothing answered at that address. The message
gives the address in decimal, so 104 means 0x68 and 118 means 0x76. Check the wiring first,
then the address. If you have more than one board, check the run window too: its first line, Running ... on,
names the board the program went to, and it should be the one you wired.
Add a second part
Now for the point of I²C. A BME280 sensor goes on the same four wires, side by side with the GY-521 on a breadboard. It needs no new pins, just its own address.
Two addresses on one bus. A BME280 sits at 0x76 or 0x77, depending on the board, so if the
run stops at this address, change it to the other.
namespace both
import grandtheorem.rpi.Pi
let MOTION: u8 = u8(0x68)
let CLIMATE: u8 = u8(0x76)
One bus, and each call names its part. The BME280 keeps its ID in register 0xD0.
entrypoint.default main() {
let i2c = Pi.i2c()
println("Motion sensor: ${i2c.readRegister(MOTION, u8(0x75))}")
println("Climate sensor: ${i2c.readRegister(CLIMATE, u8(0xD0))}")
}
Both parts answer on the same two wires:
Motion sensor: 112
Climate sensor: 96
96 means a BME280. 88 means a BMP280, a common mislabel on cheap boards, which works the same way but has no humidity sensor.
Good to know
Two parts can't share an address. Many boards have a pin or a pad that changes theirs. On a
GY-521, connecting AD0 to 3.3 V moves it from 0x68 to 0x69.
A missing part stops the program. V0 can't catch a part that doesn't answer, so a program can't scan the bus or try one address and then another. Your code has to name each address.
You don't need to add pull-ups. The Pi has its own on these pins. Keep the wires short.
A BME280's raw readings aren't degrees. Turning them into temperature takes the calibration values stored in each chip and the maker's formula, which is a job for a library.
These programs ran on a Raspberry Pi Zero 2 W and a Raspberry Pi 4, as this page shows them.