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.

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
| Constant | Value | Description |
|---|---|---|
I2C.PORT0 | 0 | Primary I2C bus |
I2C.PORT1 | 1 | Secondary 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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port (0 or 1) |
sda | integer | Pin for the data line (SDA) |
scl | integer | Pin for the clock line (SCL) |
frequency | integer | Bus frequency in Hz (typically 100000 or 400000) |
Return
Returns true if initialization was successful, false otherwise.
- 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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C 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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address (7 bits, 0x00-0x7F) |
data | list | List of bytes to send |
Return
Returns an error code:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Data too long for buffer |
| 2 | NACK received on address |
| 3 | NACK received on data |
| 4 | Other 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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address |
length | integer | Number 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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address |
write_data | list | Bytes to write (typically register address) |
read_length | integer | Number of bytes to read afterwards |
Return
Returns a list with the bytes read, or nil if the operation failed.
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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address |
register | integer | Register address (0-255) |
value | integer | Value 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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address |
register | integer | Register 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)
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address |
register | integer | Register address |
littleEndian | boolean | (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.
- 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)
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
address | integer | Device address |
register | integer | Register address |
value | integer | 16-bit value to write (0-65535) |
littleEndian | boolean | (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
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C port |
ms | integer | Timeout 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)
| Parameter | Type | Description |
|---|---|---|
port | integer | I2C 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
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 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
| Platform | Support | Notes |
|---|---|---|
| ESP32 | ✅ | Full support with two independent buses |
| Windows (SDL2) | 🔶 | Simulation (stub) for development |
| Emscripten (Web) | 🔶 | Simulation (stub) for development |