Skip to main content

I2C Module

The I2C module enables communication with devices using the I2C (Inter-Integrated Circuit) protocol, also known as TWI (Two-Wire Interface). It's ideal for connecting sensors, EEPROM memories, OLED displays, accelerometers, and other peripherals to the ESP32.

JARU I2C module screenshot for scanning buses and communicating with sensors and peripherals

The ESP32 has two independent I2C buses (PORT0 and PORT1), allowing multiple devices to be connected simultaneously with different pin configurations.

Usage

use I2C

Constants

ConstantValueDescription
I2C.PORT00Primary I2C bus
I2C.PORT11Secondary I2C bus

Functions

init

The I2C.init(port, sda, scl, frequency) function initializes an I2C bus with the specified pins and frequency.

use I2C

// Initialize I2C on port 0, SDA=21, SCL=22, 100kHz
var ok = I2C.init(I2C.PORT0, 21, 22, 100000)

if (ok) then
println("I2C initialized successfully")
end
ParameterTypeDescription
portintegerI2C port (0 or 1)
sdaintegerPin for the data line (SDA)
sclintegerPin for the clock line (SCL)
frequencyintegerBus frequency in Hz (typically 100000 or 400000)

Return

Returns true if initialization was successful, false otherwise.

Common frequencies
  • 100000 (100 kHz): Standard mode, compatible with all devices
  • 400000 (400 kHz): Fast mode, for devices that support it

scan

The I2C.scan(port) function scans the I2C bus for connected devices. Useful for diagnostics and address discovery.

use I2C

I2C.init(I2C.PORT0, 21, 22, 100000)

var devices = I2C.scan(I2C.PORT0)

println("Devices found: ", len(devices))
foreach (var addr in devices)
println(" - 0x", hex(addr))
end
ParameterTypeDescription
portintegerI2C port to scan

Return

Returns a list with the addresses (1-127) of devices that responded.

write

The I2C.write(port, address, data) function sends a list of bytes to an I2C device.

use I2C

// Send command to an OLED display (address 0x3C)
var command = [0x00, 0xAE] // Display OFF
var error = I2C.write(I2C.PORT0, 0x3C, command)

if (error == 0) then
println("Command sent successfully")
end
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address (7 bits, 0x00-0x7F)
datalistList of bytes to send

Return

Returns an error code:

CodeMeaning
0Success
1Data too long for buffer
2NACK received on address
3NACK received on data
4Other error

read

The I2C.read(port, address, length) function reads bytes from an I2C device.

use I2C

// Read 6 bytes from an accelerometer
var data = I2C.read(I2C.PORT0, 0x68, 6)

if (data != nil) then
println("Bytes received: ", len(data))
foreach (var b in data)
println(" ", b)
end
end
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address
lengthintegerNumber of bytes to read

Return

Returns a list with the bytes read.

transfer

The I2C.transfer(port, address, write_data, read_length) function performs a write followed by a read without releasing the bus (Repeated Start). This is the most common pattern for reading sensor registers.

use I2C

// Read the WHO_AM_I register (0x75) from an MPU6050
var register = [0x75]
var response = I2C.transfer(I2C.PORT0, 0x68, register, 1)

if (response != nil) then
println("WHO_AM_I: 0x", hex(response[0]))
end
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address
write_datalistBytes to write (typically register address)
read_lengthintegerNumber of bytes to read afterwards

Return

Returns a list with the bytes read, or nil if the operation failed.

Repeated Start

The Repeated Start is essential for many I2C sensors. It allows writing the register address and reading its value without another device being able to interrupt the communication.

writeReg

The I2C.writeReg(port, address, register, value) function writes a byte to a specific register. This is an optimized version that avoids creating lists.

use I2C

// Configure a sensor's control register
var error = I2C.writeReg(I2C.PORT0, 0x68, 0x6B, 0x00) // Wake up MPU6050

if (error == 0) then
println("Register configured")
end
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address
registerintegerRegister address (0-255)
valueintegerValue to write (0-255)

Return

Returns the error code (0 = success).

readReg

The I2C.readReg(port, address, register) function reads a byte from a specific register. This is an optimized version that returns the value directly.

use I2C

// Read temperature from a sensor
var temp = I2C.readReg(I2C.PORT0, 0x48, 0x00)

if (temp != nil) then
println("Raw temperature: ", temp)
end
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address
registerintegerRegister address to read

Return

Returns the register value (0-255) or nil if the read failed.

readReg16

The I2C.readReg16(port, address, register [, littleEndian]) function reads a 16-bit value (2 bytes) from a register.

use I2C

// Read 16-bit value in Big Endian (default)
var value = I2C.readReg16(I2C.PORT0, 0x68, 0x3B)

// Read in Little Endian
var valueLE = I2C.readReg16(I2C.PORT0, 0x48, 0x00, true)
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address
registerintegerRegister address
littleEndianboolean(Optional) If true, interprets as Little Endian. Default: false (Big Endian)

Return

Returns the 16-bit value (0-65535) or nil if the read failed.

Byte order
  • Big Endian (default): The most significant byte (MSB) is read first. Common in sensors like MPU6050, BMP280.
  • Little Endian: The least significant byte (LSB) is read first. Used by some temperature sensors.

writeReg16

The I2C.writeReg16(port, address, register, value [, littleEndian]) function writes a 16-bit value to a register.

use I2C

// Write 16-bit value in Big Endian
var error = I2C.writeReg16(I2C.PORT0, 0x50, 0x00, 0x1234)

// Write in Little Endian
var errorLE = I2C.writeReg16(I2C.PORT0, 0x50, 0x00, 0x1234, true)
ParameterTypeDescription
portintegerI2C port
addressintegerDevice address
registerintegerRegister address
valueinteger16-bit value to write (0-65535)
littleEndianboolean(Optional) If true, writes in Little Endian

Return

Returns the error code (0 = success).

setTimeOut

The I2C.setTimeOut(port, ms) function sets the maximum wait time for I2C operations. Useful to prevent hangs if a device doesn't respond.

use I2C

I2C.init(I2C.PORT0, 21, 22, 100000)
I2C.setTimeOut(I2C.PORT0, 100) // 100ms timeout
ParameterTypeDescription
portintegerI2C port
msintegerTimeout in milliseconds

Return

Returns true if configured successfully.

close

The I2C.close(port) function releases the I2C bus resources.

use I2C

// When done using the bus
I2C.close(I2C.PORT0)
ParameterTypeDescription
portintegerI2C port to close

Return

Returns true if closed successfully, false if the port was invalid.

Complete Example: Reading MPU6050 Sensor

use I2C

// Configuration
var MPU_ADDR = 0x68
var PWR_MGMT = 0x6B
var ACCEL_XOUT = 0x3B

// Initialize I2C
if (!I2C.init(I2C.PORT0, 21, 22, 400000)) then
println("Error initializing I2C")
exit(1)
end

// Verify sensor is connected
var devices = I2C.scan(I2C.PORT0)
var found = false
foreach (var addr in devices)
if (addr == MPU_ADDR) then
found = true
end
end

if (!found) then
println("MPU6050 not found")
exit(1)
end

// Wake up the sensor (exit sleep mode)
I2C.writeReg(I2C.PORT0, MPU_ADDR, PWR_MGMT, 0x00)
pause(100)

// Read acceleration in a loop
while (true)
// Read 6 bytes: ACCEL_X (2), ACCEL_Y (2), ACCEL_Z (2)
var data = I2C.transfer(I2C.PORT0, MPU_ADDR, [ACCEL_XOUT], 6)

if (data != nil and len(data) == 6) then
// Combine bytes (Big Endian, signed)
var ax = (data[0] << 8) | data[1]
var ay = (data[2] << 8) | data[3]
var az = (data[4] << 8) | data[5]

// Convert to signed if necessary
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("Accel X:", ax, " Y:", ay, " Z:", az)
end

pause(100)
end

Example: Writing to 24C32 EEPROM

use I2C

var EEPROM_ADDR = 0x50

I2C.init(I2C.PORT0, 21, 22, 100000)

// Write byte at address 0x0000
// The 24C32 EEPROM uses 16-bit addresses
var addressHigh = 0x00
var addressLow = 0x00
var data = 0x42

var error = I2C.write(I2C.PORT0, EEPROM_ADDR, [addressHigh, addressLow, data])

if (error == 0) then
println("Data written successfully")
pause(10) // Wait for EEPROM write cycle

// Read the data back
I2C.write(I2C.PORT0, EEPROM_ADDR, [addressHigh, addressLow])
var readData = I2C.read(I2C.PORT0, EEPROM_ADDR, 1)

if (readData != nil) then
println("Data read: 0x", hex(readData[0]))
end
end

Considerations

Pull-up Resistors

The I2C bus requires pull-up resistors on the SDA and SCL lines (typically 4.7kΩ to 10kΩ). Many modules already include them, but if you connect devices directly, make sure to add them.

I2C Addresses

I2C addresses are 7 bits (0x00-0x7F). Some datasheets show 8-bit addresses (including the R/W bit). In that case, divide the address by 2 to get the 7-bit address.

Supported Platforms

PlatformSupportNotes
ESP32Full support with two independent buses
Windows (SDL2)🔶Simulation (stub) for development
Emscripten (Web)🔶Simulation (stub) for development