Módulo I2C
El módulo I2C habla con aparatos conectados por I2C (también llamado TWI): sensores, memorias EEPROM, pantallas OLED, acelerómetros, relojes… Abres un bus con dos pines, SDA (datos) y SCL (reloj), y en él cada aparato tiene su dirección. Las placas tienen dos buses, I2C.PORT0 y I2C.PORT1, que se pueden usar a la vez con pines distintos.
use I2C

Para no escribir a mano los registros de cada chip, JARU IDE trae una biblioteca de drivers: lo cuenta Drivers I2C.
Plataformas
| Windows | ESP32 / ESP32-S3 | ESP32-P4 | ESP32-S31 | |
|---|---|---|---|---|
| El bus | No hay: el módulo hace como que todo va bien (ver abajo) | Los dos del chip | Los dos del chip | Los dos del chip |
| Códigos que devuelven las escrituras | Siempre 0 | 0, 2, 4 o 5 | 0, 2 o 5 | 0, 2 o 5 |
| Argumentos que no valen | Devuelven un valor de error | Lanzan una excepción | Igual | Igual |
| Tiempo de espera de serie | — | 50 ms | 1000 ms | 1000 ms |
| Bytes de una vez | — | Hasta 128 | Sin un límite fijo | Sin un límite fijo |
init en un bus que ya está abierto | — | No cambia nada: sigue con los pines y la frecuencia de antes | Lo abre otra vez con los nuevos; si lo abrió la placa, lo comparte | Igual |
En Windows no hay bus I2C: el programa corre, pero ningún aparato contesta de verdad. init, setTimeOut y close devuelven true; las escrituras, 0; read y transfer, una lista de ceros del tamaño que pides; readReg y readReg16, 0; y scan encuentra siempre un aparato en la dirección 0x50. Cada llamada escribe además una línea [I2C STUB] … en la consola. Los programas con I2C se prueban en la placa.
Funciones
| Función | Qué hace | Devuelve |
|---|---|---|
I2C.init(puerto, sda, scl, frecuencia) | Abre un bus | true o false |
I2C.scan(puerto) | Busca los aparatos del bus | Una lista de direcciones |
I2C.write(puerto, direccion, bytes) | Manda bytes a un aparato | Un código: 0 si fue bien |
I2C.read(puerto, direccion, n) | Lee n bytes de un aparato | Una lista de bytes |
I2C.transfer(puerto, direccion, bytes, n) | Manda bytes y lee la respuesta, sin soltar el bus | Una lista de bytes, o nil |
I2C.transfer(puerto, direccion, bytes, n, espera) | Lo mismo, con una espera entre la escritura y la lectura | Una lista de bytes, o nil |
I2C.writeReg(puerto, direccion, registro, valor) | Escribe un byte en un registro | Un código: 0 si fue bien |
I2C.readReg(puerto, direccion, registro) | Lee un byte de un registro | De 0 a 255, o nil |
I2C.readReg16(puerto, direccion, registro) | Lee 16 bits de un registro | De 0 a 65535, o nil |
I2C.writeReg16(puerto, direccion, registro, valor) | Escribe 16 bits en un registro | Un código: 0 si fue bien |
I2C.setTimeOut(puerto, ms) | Cambia el tiempo de espera del bus | true |
I2C.close(puerto) | Cierra el bus | true o false |
puerto es I2C.PORT0 o I2C.PORT1. direccion es la dirección de 7 bits del aparato, de 0x00 a 0x7F.
En las placas, si algo no vale, lanzan una excepción, que se captura con try (ver Errores y excepciones):
ERR_INVALID_VALUEsi el puerto no es 0 ni 1.ERR_TYPE_MISMATCHsi un número no es un entero, o si una lista de bytes no es una lista o lleva algo que no es un entero.ERR_INVALID_ARG_NUMBERsi no les pasas los argumentos que piden.
Un aparato que no contesta no es una excepción: lo dicen el código, el nil o la lista vacía que devuelven.
Los códigos de las escrituras
write, writeReg y writeReg16 devuelven un código:
| Código | Qué pasó |
|---|---|
0 | Fue bien. |
2 | El aparato no contestó: no hay ninguno en esa dirección, o no aceptó los datos. |
4 | Otro error. En ESP32 y ESP32-S3, también si el bus no está abierto. |
5 | Se acabó el tiempo de espera. |
En el P4 y el S31, un bus sin abrir y cualquier otro error dan 2.
init(puerto, sda, scl, frecuencia)
Abre el bus puerto en los pines sda y scl, a frecuencia hercios, y devuelve true si lo consigue. Las frecuencias más usadas son 100000 (100 kHz, el modo normal, que entienden todos los aparatos) y 400000 (400 kHz, el modo rápido).
En ESP32 y ESP32-S3, si el bus ya está abierto, init no cambia nada y devuelve true: para cambiar sus pines o su frecuencia, ciérralo antes con close. En el P4 y el S31, si la placa ya tenía abierto ese bus, init lo comparte, con los pines con que lo abrió la placa.
use I2C
// Puerto 0, SDA en el GPIO 21, SCL en el 22, a 100 kHz
if (I2C.init(I2C.PORT0, 21, 22, 100000)) then
println("Bus abierto")
end
Los pines I2C de cada placa están en su perfil (ver Perfiles de placa).
scan(puerto)
Prueba todas las direcciones del bus y devuelve una lista con las que contestan, de 1 a 126. Sirve para saber qué hay conectado y en qué dirección. Si el bus no está abierto, devuelve una lista vacía.
use I2C
I2C.init(I2C.PORT0, 21, 22, 100000)
var aparatos = I2C.scan(I2C.PORT0)
println("Aparatos: ", len(aparatos))
foreach (var dir in aparatos)
println(" 0x", dir.toHex())
end
write(puerto, direccion, bytes)
Manda a direccion los bytes de la lista bytes y devuelve un código. De cada número se manda el byte bajo.
use I2C
// Apagar una pantalla OLED (dirección 0x3C)
var codigo = I2C.write(I2C.PORT0, 0x3C, [0x00, 0xAE])
if (codigo == 0) then
println("Comando enviado")
end
read(puerto, direccion, n)
Lee n bytes de direccion y los devuelve en una lista. Nunca devuelve nil: si el aparato no contesta, la lista llega vacía o más corta. Comprueba su tamaño. En el ESP32 y el ESP32-S3, n va de 0 a 128: con más, o con un número negativo, lanza ERR_INVALID_VALUE, también en transfer.
use I2C
// Leer 6 bytes de un acelerómetro
var datos = I2C.read(I2C.PORT0, 0x68, 6)
if (len(datos) == 6) then
foreach (var b in datos)
println(" ", b)
end
else
println("El aparato no contesta")
end
transfer(puerto, direccion, bytes, n, espera)
Manda los bytes de la lista bytes y, sin soltar el bus (con un repeated start), lee n bytes de la respuesta. Es lo más habitual para leer los registros de un sensor: mandas el número de registro y lees su valor, sin que otro aparato pueda meterse en medio.
Devuelve la lista de bytes leídos, o nil si falla la escritura.
use I2C
// Leer el registro WHO_AM_I (0x75) de un MPU6050
var respuesta = I2C.transfer(I2C.PORT0, 0x68, [0x75], 1)
if (respuesta != nil) then
println("WHO_AM_I: 0x", respuesta[0].toHex())
end
espera, el quinto argumento, es opcional. Algunos chips no contestan dentro de un repeated start: preparan la respuesta aparte y necesitan que la escritura termine con STOP y que pase un tiempo antes de leer (los módulos seesaw de Adafruit, los sensores con tiempo de conversión…). Con espera, la escritura termina con STOP, la VM espera esos microsegundos exactos y lee en una transacción nueva. Con 0, STOP y lee en seguida.
use I2C
// Botones de un mando seesaw: módulo GPIO (0x01), registro BULK (0x04),
// 4 bytes, con STOP y 250 µs de espera, como hace la biblioteca de Adafruit
var botones = I2C.transfer(I2C.PORT0, 0x50, [0x01, 0x04], 4, 250)
La espera es un bucle ocupado dentro de la VM: vale para décimas de milisegundo, no para esperas largas. Va de 0 a 100000: un valor negativo cuenta como 0, y uno mayor da ERR_INVALID_VALUE. Para esperar milisegundos entre dos operaciones, usa pause() entre write() y read().
writeReg(puerto, direccion, registro, valor)
Escribe el byte valor (de 0 a 255) en el registro registro (de 0 a 255) y devuelve un código. Es más rápido que write, porque no crea una lista.
use I2C
// Despertar un MPU6050: 0 en su registro 0x6B
var codigo = I2C.writeReg(I2C.PORT0, 0x68, 0x6B, 0x00)
if (codigo == 0) then
println("Registro escrito")
end
readReg(puerto, direccion, registro)
Lee el byte del registro registro y lo devuelve, de 0 a 255, o nil si el aparato no contesta.
use I2C
var temperatura = I2C.readReg(I2C.PORT0, 0x48, 0x00)
if (temperatura != nil) then
println("Temperatura en bruto: ", temperatura)
end
readReg16(puerto, direccion, registro)
Lee los dos bytes que empiezan en el registro registro y los devuelve juntos como un valor de 16 bits, de 0 a 65535, o nil si el aparato no contesta.
El cuarto argumento, opcional, dice en qué orden vienen los bytes: sin él (o con false), el primero es el alto (big endian), como en el MPU6050 o el BMP280; con true, el primero es el bajo (little endian).
use I2C
var valor = I2C.readReg16(I2C.PORT0, 0x68, 0x3B) // el byte alto primero
var valorLE = I2C.readReg16(I2C.PORT0, 0x48, 0x00, true) // el byte bajo primero
writeReg16(puerto, direccion, registro, valor)
Escribe valor (de 0 a 65535) en dos bytes a partir del registro registro y devuelve un código. Como en readReg16, un quinto argumento true manda primero el byte bajo; sin él, el alto.
use I2C
var codigo = I2C.writeReg16(I2C.PORT0, 0x50, 0x00, 0x1234) // 0x12 y después 0x34
var codigoLE = I2C.writeReg16(I2C.PORT0, 0x50, 0x00, 0x1234, true) // 0x34 y después 0x12
setTimeOut(puerto, ms)
Cambia el tiempo que espera el bus a un aparato antes de darlo por perdido, en milisegundos, y devuelve true. De serie es de 50 ms en ESP32 y ESP32-S3, y de 1000 ms en el P4 y el S31. Un valor negativo da ERR_TYPE_MISMATCH.
use I2C
I2C.init(I2C.PORT0, 21, 22, 100000)
I2C.setTimeOut(I2C.PORT0, 100) // 100 ms
close(puerto)
Cierra el bus y libera sus pines. En ESP32 y ESP32-S3 devuelve true con un puerto válido; en el P4 y el S31, true si el bus estaba abierto y false si no.
use I2C
I2C.close(I2C.PORT0)
Constantes
| Constante | Valor | Qué es |
|---|---|---|
I2C.PORT0 | 0 | El primer bus |
I2C.PORT1 | 1 | El segundo bus |
Ejemplo completo
Lee la aceleración de un MPU6050 cada décima de segundo:
use I2C
const MPU = 0x68
const PWR_MGMT = 0x6B
const ACCEL_XOUT = 0x3B
if (!I2C.init(I2C.PORT0, 21, 22, 400000)) then
println("No se ha podido abrir el bus")
exit(1)
end
// ¿Está el sensor?
var encontrado = false
foreach (var dir in I2C.scan(I2C.PORT0))
if (dir == MPU) then
encontrado = true
end
end
if (!encontrado) then
println("No encuentro el MPU6050")
exit(1)
end
// Despertarlo
I2C.writeReg(I2C.PORT0, MPU, PWR_MGMT, 0x00)
pause(100)
while (true)
// 6 bytes: X, Y y Z, cada uno con el byte alto primero
var datos = I2C.transfer(I2C.PORT0, MPU, [ACCEL_XOUT], 6)
if (datos != nil and len(datos) == 6) then
var ax = (datos[0] << 8) | datos[1]
var ay = (datos[2] << 8) | datos[3]
var az = (datos[4] << 8) | datos[5]
// Son valores con signo
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
Una EEPROM 24C32
Esta memoria usa direcciones de 16 bits: se mandan los dos bytes de la dirección y después el dato.
use I2C
const EEPROM = 0x50
I2C.init(I2C.PORT0, 21, 22, 100000)
// Escribir 0x42 en la dirección 0x0000
var codigo = I2C.write(I2C.PORT0, EEPROM, [0x00, 0x00, 0x42])
if (codigo == 0) then
pause(10) // la EEPROM tarda en grabar
// Leerlo: primero la dirección, después el dato
I2C.write(I2C.PORT0, EEPROM, [0x00, 0x00])
var leido = I2C.read(I2C.PORT0, EEPROM, 1)
if (len(leido) == 1) then
println("Dato: 0x", leido[0].toHex())
end
end
Trampas y problemas conocidos
- Resistencias pull-up. El bus necesita resistencias pull-up en SDA y SCL (de 4,7 kΩ a 10 kΩ, normalmente). Muchos módulos ya las llevan; si conectas los chips directamente, ponlas tú.
- Direcciones de 7 bits. Algunas hojas de datos dan la dirección en 8 bits, con el bit de lectura o escritura. Si es así, divídela entre 2.
readno devuelvenil. Si el aparato no contesta, la lista llega vacía o más corta: mira su tamaño.- 128 bytes de una vez en ESP32 y ESP32-S3. En una escritura, lo que pase de 128 bytes no se manda, sin aviso, y una lectura de más de 128, o de una longitud negativa, lanza
ERR_INVALID_VALUE. Pártelo en trozos. - Un bus abierto no se reabre en ESP32 y ESP32-S3. Un segundo
initen el mismo puerto deja los pines y la frecuencia de antes: llama antes aclose. - En Windows, todo va bien. El simulacro de Windows contesta siempre: un programa que funciona en Windows puede no encontrar el aparato en la placa.
Todos los problemas conocidos de JARU, juntos, están en Problemas conocidos.