I2C module
The I2C module talks to devices connected over I2C (also called TWI): sensors, EEPROM memories, OLED screens, accelerometers, clocks… You open a bus with two pins, SDA (data) and SCL (clock), and on it each device has its address. The boards have two buses, I2C.PORT0 and I2C.PORT1, which can be used at the same time with different pins.
use I2C

So that you don't have to write each chip's registers by hand, JARU IDE comes with a library of drivers: it's all in I2C drivers.
Platforms
| Windows | ESP32 / ESP32-S3 | ESP32-P4 | ESP32-S31 | |
|---|---|---|---|---|
| The bus | There isn't one: the module pretends everything goes well (see below) | The chip's two | The chip's two | The chip's two |
| Codes returned by writes | Always 0 | 0, 2, 4 or 5 | 0, 2 or 5 | 0, 2 or 5 |
| Arguments that aren't valid | Return an error value | Throw an exception | Same | Same |
| Default waiting time | — | 50 ms | 1000 ms | 1000 ms |
| Bytes at once | — | Up to 128 | No fixed limit | No fixed limit |
init on a bus that's already open | — | Changes nothing: it keeps the earlier pins and frequency | Opens it again with the new ones; if the board opened it, it shares it | Same |
On Windows there's no I2C bus: the program runs, but no device really answers. init, setTimeOut and close return true; writes, 0; read and transfer, a list of zeros of the size you ask for; readReg and readReg16, 0; and scan always finds a device at address 0x50. Each call also writes an [I2C STUB] … line to the console. Programs with I2C are tested on the board.
Functions
| Function | What it does | Returns |
|---|---|---|
I2C.init(port, sda, scl, frequency) | Opens a bus | true or false |
I2C.scan(port) | Looks for the devices on the bus | A list of addresses |
I2C.write(port, address, bytes) | Sends bytes to a device | A code: 0 if it went well |
I2C.read(port, address, n) | Reads n bytes from a device | A list of bytes |
I2C.transfer(port, address, bytes, n) | Sends bytes and reads the answer, without releasing the bus | A list of bytes, or nil |
I2C.transfer(port, address, bytes, n, wait) | The same, with a wait between the write and the read | A list of bytes, or nil |
I2C.writeReg(port, address, register, value) | Writes a byte to a register | A code: 0 if it went well |
I2C.readReg(port, address, register) | Reads a byte from a register | From 0 to 255, or nil |
I2C.readReg16(port, address, register) | Reads 16 bits from a register | From 0 to 65535, or nil |
I2C.writeReg16(port, address, register, value) | Writes 16 bits to a register | A code: 0 if it went well |
I2C.setTimeOut(port, ms) | Changes the bus waiting time | true |
I2C.close(port) | Closes the bus | true or false |
port is I2C.PORT0 or I2C.PORT1. address is the device's 7-bit address, from 0x00 to 0x7F.
On the boards, if something isn't valid, they throw an exception, which you catch with try (see Errors and exceptions):
ERR_INVALID_VALUEif the port isn't 0 or 1.ERR_TYPE_MISMATCHif a number isn't an integer, or if a list of bytes isn't a list or holds something that isn't an integer.ERR_INVALID_ARG_NUMBERif you don't pass the arguments they ask for.
A device that doesn't answer isn't an exception: the code, the nil or the empty list they return tell you so.
The write codes
write, writeReg and writeReg16 return a code:
| Code | What happened |
|---|---|
0 | It went well. |
2 | The device didn't answer: there's none at that address, or it didn't accept the data. |
4 | Another error. On the ESP32 and ESP32-S3, also if the bus isn't open. |
5 | The waiting time ran out. |
On the P4 and the S31, a bus that isn't open and any other error give 2.
init(port, sda, scl, frequency)
Opens the bus port on pins sda and scl, at frequency hertz, and returns true if it succeeds. The most used frequencies are 100000 (100 kHz, standard mode, which every device understands) and 400000 (400 kHz, fast mode).
On the ESP32 and ESP32-S3, if the bus is already open, init changes nothing and returns true: to change its pins or its frequency, close it first with close. On the P4 and the S31, if the board already had that bus open, init shares it, with the pins the board opened it with.
use I2C
// Port 0, SDA on GPIO 21, SCL on 22, at 100 kHz
if (I2C.init(I2C.PORT0, 21, 22, 100000)) then
println("Bus open")
end
Each board's I2C pins are in its profile (see Board profiles).
scan(port)
Tries every address on the bus and returns a list with the ones that answer, from 1 to 126. Use it to find out what's connected and at which address. If the bus isn't open, it returns an empty list.
use I2C
I2C.init(I2C.PORT0, 21, 22, 100000)
var devices = I2C.scan(I2C.PORT0)
println("Devices: ", len(devices))
foreach (var addr in devices)
println(" 0x", addr.toHex())
end
write(port, address, bytes)
Sends the bytes of the list bytes to address and returns a code. The low byte of each number is sent.
use I2C
// Turn off an OLED screen (address 0x3C)
var code = I2C.write(I2C.PORT0, 0x3C, [0x00, 0xAE])
if (code == 0) then
println("Command sent")
end
read(port, address, n)
Reads n bytes from address and returns them in a list. It never returns nil: if the device doesn't answer, the list arrives empty or shorter. Check its size. On the ESP32 and the ESP32-S3, n goes from 0 to 128: with more, or with a negative number, it throws ERR_INVALID_VALUE, also in transfer.
use I2C
// Read 6 bytes from an accelerometer
var data = I2C.read(I2C.PORT0, 0x68, 6)
if (len(data) == 6) then
foreach (var b in data)
println(" ", b)
end
else
println("The device doesn't answer")
end
transfer(port, address, bytes, n, wait)
Sends the bytes of the list bytes and, without releasing the bus (with a repeated start), reads n bytes of the answer. It's the usual way to read a sensor's registers: you send the register number and read its value, without another device being able to get in between.
It returns the list of bytes read, or nil if the write fails.
use I2C
// Read the WHO_AM_I register (0x75) of an MPU6050
var answer = I2C.transfer(I2C.PORT0, 0x68, [0x75], 1)
if (answer != nil) then
println("WHO_AM_I: 0x", answer[0].toHex())
end
wait, the fifth argument, is optional. Some chips don't answer inside a repeated start: they prepare the answer separately and need the write to end with STOP and some time to pass before the read (Adafruit's seesaw modules, sensors with a conversion time…). With wait, the write ends with STOP, the VM waits exactly that many microseconds and reads in a new transaction. With 0, STOP and read right away.
use I2C
// Buttons of a seesaw gamepad: GPIO module (0x01), BULK register (0x04),
// 4 bytes, with STOP and a 250 µs wait, as Adafruit's library does
var buttons = I2C.transfer(I2C.PORT0, 0x50, [0x01, 0x04], 4, 250)
The wait is a busy loop inside the VM: it's meant for tenths of a millisecond, not for long waits. It goes from 0 to 100000: a negative value counts as 0, and a larger one gives ERR_INVALID_VALUE. To wait milliseconds between two operations, use pause() between write() and read().
writeReg(port, address, register, value)
Writes the byte value (from 0 to 255) to the register register (from 0 to 255) and returns a code. It's faster than write, because it doesn't create a list.
use I2C
// Wake up an MPU6050: 0 in its register 0x6B
var code = I2C.writeReg(I2C.PORT0, 0x68, 0x6B, 0x00)
if (code == 0) then
println("Register written")
end
readReg(port, address, register)
Reads the byte of register register and returns it, from 0 to 255, or nil if the device doesn't answer.
use I2C
var temperature = I2C.readReg(I2C.PORT0, 0x48, 0x00)
if (temperature != nil) then
println("Raw temperature: ", temperature)
end
readReg16(port, address, register)
Reads the two bytes starting at register register and returns them together as a 16-bit value, from 0 to 65535, or nil if the device doesn't answer.
The fourth argument, optional, says in which order the bytes come: without it (or with false), the first one is the high one (big endian), as in the MPU6050 or the BMP280; with true, the first one is the low one (little endian).
use I2C
var value = I2C.readReg16(I2C.PORT0, 0x68, 0x3B) // high byte first
var valueLE = I2C.readReg16(I2C.PORT0, 0x48, 0x00, true) // low byte first
writeReg16(port, address, register, value)
Writes value (from 0 to 65535) in two bytes from register register on, and returns a code. As in readReg16, a fifth argument true sends the low byte first; without it, the high one.
use I2C
var code = I2C.writeReg16(I2C.PORT0, 0x50, 0x00, 0x1234) // 0x12 and then 0x34
var codeLE = I2C.writeReg16(I2C.PORT0, 0x50, 0x00, 0x1234, true) // 0x34 and then 0x12
setTimeOut(port, ms)
Changes how long the bus waits for a device before giving it up, in milliseconds, and returns true. By default it's 50 ms on the ESP32 and ESP32-S3, and 1000 ms on the P4 and the S31. A negative value gives ERR_TYPE_MISMATCH.
use I2C
I2C.init(I2C.PORT0, 21, 22, 100000)
I2C.setTimeOut(I2C.PORT0, 100) // 100 ms
close(port)
Closes the bus and frees its pins. On the ESP32 and ESP32-S3 it returns true with a valid port; on the P4 and the S31, true if the bus was open and false if not.
use I2C
I2C.close(I2C.PORT0)
Constants
| Constant | Value | What it is |
|---|---|---|
I2C.PORT0 | 0 | The first bus |
I2C.PORT1 | 1 | The second bus |
Complete example
It reads the acceleration of an MPU6050 every tenth of a second:
use I2C
const MPU = 0x68
const PWR_MGMT = 0x6B
const ACCEL_XOUT = 0x3B
if (!I2C.init(I2C.PORT0, 21, 22, 400000)) then
println("The bus couldn't be opened")
exit(1)
end
// Is the sensor there?
var found = false
foreach (var addr in I2C.scan(I2C.PORT0))
if (addr == MPU) then
found = true
end
end
if (!found) then
println("I can't find the MPU6050")
exit(1)
end
// Wake it up
I2C.writeReg(I2C.PORT0, MPU, PWR_MGMT, 0x00)
pause(100)
while (true)
// 6 bytes: X, Y and Z, each with the high byte first
var data = I2C.transfer(I2C.PORT0, MPU, [ACCEL_XOUT], 6)
if (data != nil and len(data) == 6) then
var ax = (data[0] << 8) | data[1]
var ay = (data[2] << 8) | data[3]
var az = (data[4] << 8) | data[5]
// They are signed values
if (ax > 32767) then ax = ax - 65536 end
if (ay > 32767) then ay = ay - 65536 end
if (az > 32767) then az = az - 65536 end
println("X: ", ax, " Y: ", ay, " Z: ", az)
end
pause(100)
end
A 24C32 EEPROM
This memory uses 16-bit addresses: you send the two bytes of the address and then the data.
use I2C
const EEPROM = 0x50
I2C.init(I2C.PORT0, 21, 22, 100000)
// Write 0x42 at address 0x0000
var code = I2C.write(I2C.PORT0, EEPROM, [0x00, 0x00, 0x42])
if (code == 0) then
pause(10) // the EEPROM takes a while to store it
// Read it: first the address, then the data
I2C.write(I2C.PORT0, EEPROM, [0x00, 0x00])
var stored = I2C.read(I2C.PORT0, EEPROM, 1)
if (len(stored) == 1) then
println("Data: 0x", stored[0].toHex())
end
end
Pitfalls and known issues
- Pull-up resistors. The bus needs pull-up resistors on SDA and SCL (usually from 4.7 kΩ to 10 kΩ). Many modules already have them; if you connect the chips directly, add them yourself.
- 7-bit addresses. Some datasheets give the address in 8 bits, with the read or write bit. If so, divide it by 2.
readdoesn't returnnil. If the device doesn't answer, the list arrives empty or shorter: check its size.- 128 bytes at once on the ESP32 and ESP32-S3. In a write, anything beyond 128 bytes isn't sent, without warning, and a read of more than 128, or of a negative length, throws
ERR_INVALID_VALUE. Split it into chunks. - An open bus isn't reopened on the ESP32 and ESP32-S3. A second
initon the same port keeps the earlier pins and frequency: callclosefirst. - On Windows, everything goes well. The Windows stand-in always answers: a program that works on Windows may not find the device on the board.
All the known issues of JARU, together, are in Known issues.