Saltar al contenido principal

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

Captura del módulo I2C de JARU para escanear buses y comunicarse con sensores y periféricos

Para no escribir a mano los registros de cada chip, JARU IDE trae una biblioteca de drivers: lo cuenta Drivers I2C.

Plataformas​

WindowsESP32 / ESP32-S3ESP32-P4ESP32-S31
El busNo hay: el módulo hace como que todo va bien (ver abajo)Los dos del chipLos dos del chipLos dos del chip
Códigos que devuelven las escriturasSiempre 00, 2, 4 o 50, 2 o 50, 2 o 5
Argumentos que no valenDevuelven un valor de errorLanzan una excepciónIgualIgual
Tiempo de espera de serie—50 ms1000 ms1000 ms
Bytes de una vez—Hasta 128Sin un límite fijoSin un límite fijo
init en un bus que ya está abierto—No cambia nada: sigue con los pines y la frecuencia de antesLo abre otra vez con los nuevos; si lo abrió la placa, lo comparteIgual

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ónQué haceDevuelve
I2C.init(puerto, sda, scl, frecuencia)Abre un bustrue o false
I2C.scan(puerto)Busca los aparatos del busUna lista de direcciones
I2C.write(puerto, direccion, bytes)Manda bytes a un aparatoUn código: 0 si fue bien
I2C.read(puerto, direccion, n)Lee n bytes de un aparatoUna lista de bytes
I2C.transfer(puerto, direccion, bytes, n)Manda bytes y lee la respuesta, sin soltar el busUna lista de bytes, o nil
I2C.transfer(puerto, direccion, bytes, n, espera)Lo mismo, con una espera entre la escritura y la lecturaUna lista de bytes, o nil
I2C.writeReg(puerto, direccion, registro, valor)Escribe un byte en un registroUn código: 0 si fue bien
I2C.readReg(puerto, direccion, registro)Lee un byte de un registroDe 0 a 255, o nil
I2C.readReg16(puerto, direccion, registro)Lee 16 bits de un registroDe 0 a 65535, o nil
I2C.writeReg16(puerto, direccion, registro, valor)Escribe 16 bits en un registroUn código: 0 si fue bien
I2C.setTimeOut(puerto, ms)Cambia el tiempo de espera del bustrue
I2C.close(puerto)Cierra el bustrue 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_VALUE si el puerto no es 0 ni 1.
  • ERR_TYPE_MISMATCH si 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_NUMBER si 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ódigoQué pasó
0Fue bien.
2El aparato no contestó: no hay ninguno en esa dirección, o no aceptó los datos.
4Otro error. En ESP32 y ESP32-S3, también si el bus no está abierto.
5Se 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​

ConstanteValorQué es
I2C.PORT00El primer bus
I2C.PORT11El 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.
  • read no devuelve nil. 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 init en el mismo puerto deja los pines y la frecuencia de antes: llama antes a close.
  • 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.