← Índice del curso

Comunicación entre Dispositivos · Tema 18 · I²C

Leer y escribir registros I²C

Ya sabemos qué viaja por el bus. Ahora lo usamos para lo que casi siempre se hace con I²C: hablar con un dispositivo a través de su mapa de registros. Vamos a ver cómo funciona el puntero de registro y su autoincremento, cómo se escriben las funciones básicas con la biblioteca Wire, cómo se barre el bus para descubrir qué hay conectado, cómo se reconstruyen valores de varios bytes con signo y cómo se cambian algunos bits de un registro sin pisar los demás.

01 · De los bytes a los dispositivos

Un dispositivo es un mapa de registros

El protocolo del tema 17 solo mueve bytes: no dice qué significan. El significado lo pone cada dispositivo, y casi todos lo organizan de la misma manera: como una pequeña memoria de registros numerados, de un byte cada uno. Algunos registros son de configuración (se escriben para encender el sensor, elegir su rango, su velocidad de medición), otros son de estado (dicen si hay una medición lista) y otros contienen los datos medidos. La hoja de datos del dispositivo trae una tabla con el número y el significado de cada uno: el mapa de registros.

Trabajar con un dispositivo I²C es, entonces, siempre lo mismo: escribir registros para configurarlo y leer registros para obtener sus datos. Lo único que cambia de un chip a otro es la tabla. Aprender el mecanismo una vez permite manejar un acelerómetro, un sensor de presión, un reloj de tiempo real o una memoria con las mismas cuatro funciones.

En este tema usamos como ejemplo recurrente un MPU6050, un acelerómetro y giroscopio muy común en módulos de bajo costo, en la dirección 0x68. Sus registros reales (los que usa el código de la sección 07) son:

Registros del MPU6050 que se usan en el tema.
DirecciónNombreAccesoContenido
0x75WHO_AM_IlecturaIdentificación: siempre 0x68
0x6BPWR_MGMT_1lectura y escrituraAdministración de energía. Valor de arranque 0x40: el bit 6 (SLEEP) está en 1, el sensor duerme
0x1CACCEL_CONFIGlectura y escrituraBits 4 y 3: rango del acelerómetro (00 = ±2 g, 01 = ±4 g, 10 = ±8 g, 11 = ±16 g). Bits 7 a 5: autotest
0x3B a 0x40ACCEL_XOUT_H … ACCEL_ZOUT_LlecturaAceleración en X, Y y Z: tres enteros de 16 bits con signo, byte alto primero
0x41 y 0x42TEMP_OUT_H y _LlecturaTemperatura: 16 bits con signo
0x43 a 0x48GYRO_XOUT_H … GYRO_ZOUT_LlecturaVelocidad angular en X, Y y Z: tres enteros de 16 bits con signo

02 · Cómo se elige un registro

El mapa y el puntero

Mapa de registros con puntero que avanza solo Una columna de registros numerados desde 0x3B con su nombre y su contenido. Una flecha marca el puntero de registro en 0x3B. Una lectura en ráfaga devuelve los bytes en orden y el puntero se incrementa después de cada uno. dirección registro contenido 0x3B ACCEL_XOUT_H 0xFC 0x3C ACCEL_XOUT_L 0xE0 0x3D ACCEL_YOUT_H 0x04 0x3E ACCEL_YOUT_L 0xCE 0x3F ACCEL_ZOUT_H 0x40 0x40 ACCEL_ZOUT_L 0x00 0x41 TEMP_OUT_H 0xF0 0x42 TEMP_OUT_L 0xB0 puntero = 0x3B cada byte leído avanza el puntero +1 Una lectura de N bytes devuelve N registros consecutivos. En naranja: los dos registros que se tocan con una lectura de 2 bytes desde 0x3B (valores del ejemplo).

El dispositivo mantiene internamente un puntero de registro: el número del registro al que se accederá a continuación. Se maneja con tres reglas:

  1. El primer byte de datos de una escritura es el puntero. Después de S dir+W A, el byte que envía el maestro es el número de registro; el dispositivo lo guarda como puntero.
  2. Cada byte que sigue, en una escritura, se guarda en el registro apuntado, y el puntero avanza un lugar. Por eso S dir+W A 0x6B A 0x00 A P escribe 0x00 en el registro 0x6B.
  3. Una lectura devuelve el registro apuntado y avanza el puntero. Si se piden varios bytes, llegan los registros consecutivos: es la lectura en ráfaga (burst). Con ella se obtienen en un mensaje los 14 bytes de aceleración, temperatura y giroscopio.

De ahí surge el mensaje de lectura del tema 17: no se puede indicar el registro dentro de un mensaje de lectura (en la lectura, el maestro solo envía el byte de dirección), por lo que primero se hace una escritura de un solo byte para fijar el puntero y, con un START repetido, se pasa a leer: S dir+W A reg A Sr dir+R A dato … N P.

Dos detalles sobre el puntero:

  • Algunos dispositivos no tienen registros. Un BH1750 (sensor de luz) o el conversor del tema 15 funcionan por comandos: un byte de comando inicia la medición, y la lectura devuelve el resultado. La estructura de mensajes es la misma, pero el primer byte no es un número de registro.
  • No todos los mapas son continuos ni avanzan igual. Algunos tienen huecos reservados; otros, como las memorias EEPROM, usan una dirección interna de 16 bits (dos bytes de puntero). Y algunos registros son puertos que no avanzan (leer repetidamente un registro FIFO devuelve los datos sucesivos de una cola). La hoja de datos lo dice.

Los registros tienen un tipo de acceso que hay que respetar: de solo lectura (identificación, datos medidos, estado), de lectura y escritura (configuración) y a veces de solo escritura (comandos). Escribir en uno de solo lectura normalmente no produce error: el dispositivo confirma con ACK y lo ignora, así que el ACK no prueba que algo se haya guardado.

03 · La herramienta

La biblioteca Wire

En Arduino y en el ESP32, la biblioteca Wire arma los mensajes que vimos con unas pocas funciones. Trabaja con buffers: primero se cargan los bytes que se quiere enviar y recién al llamar a endTransmission() viaja el mensaje por el bus.

Funciones principales de Wire (maestro).
FunciónQué haceEn el bus
Wire.begin()Inicializa el periférico como maestro (en el ESP32 admite los pines: Wire.begin(sda, scl))—
Wire.setClock(hz)Fija la frecuencia de SCL (100 000 por defecto; 400 000 para el modo rápido)—
Wire.beginTransmission(dir)Prepara un mensaje de escritura hacia la dirección de 7 bitsnada todavía
Wire.write(byte)Agrega un byte (o un arreglo) al buffer de envíonada todavía
Wire.endTransmission()Envía lo preparado y termina con STOP. Devuelve el código del tema 17 (0 = todo ACK)S dir+W A … P
Wire.endTransmission(false)Igual, pero sin STOP: queda pendiente un START repetidoS dir+W A …
Wire.requestFrom(dir, n)Lee n bytes, con ACK en todos menos el último, y deja los recibidos en el buffer. Devuelve cuántos llegaronSr (o S) dir+R A … N P
Wire.available() / Wire.read()Cuántos bytes quedan en el buffer de recepción, y retira uno (devuelve un int, -1 si no hay)—

El tamaño del buffer limita cuántos bytes caben en un mensaje: 32 bytes en las placas AVR (Uno, Nano, Mega) y más en el ESP32. Para una memoria o una pantalla que reciben bloques grandes hay que partirlos. Una lectura de más bytes que el buffer devuelve solo los que entran.

Con estas funciones se escriben las tres operaciones sobre registros que resuelven casi todo, con la forma de los mensajes del tema 17:

  • escribirReg(dir, reg, valor): S dir+W A reg A valor A P.
  • leerRegs(dir, reg, destino, n): S dir+W A reg A Sr dir+R A dato … N P. Con n = 1 lee un registro; con n > 1 es una ráfaga.
  • modificarBits(dir, reg, mascara, valor): leer el registro, cambiar solo los bits de la máscara y volver a escribirlo (sección 06).

Siempre hay que mirar el resultado: endTransmission() distinto de 0 indica un NACK (tema 17), y requestFrom() que devuelve menos de lo pedido indica que el dispositivo no contestó o se cortó el mensaje. Un programa que ignora esos valores sigue «funcionando» con datos viejos o con ceros.

04 · ¿Qué hay conectado?

El barrido de direcciones

Cuando se arma un circuito nuevo, la primera pregunta es qué direcciones contestan. La respuesta es un barrido (scanner): probar una por una las 112 direcciones válidas (0x08 a 0x77) con un mensaje de solo la dirección, S dir+W [A/N] P, y anotar las que devuelven ACK. Con Wire es un bucle de beginTransmission y endTransmission sin datos, mirando si el código es 0.

Es la herramienta de diagnóstico más útil de I²C, y cómo se lee el resultado dice mucho:

Qué indica el resultado de un barrido.
ResultadoInterpretación
Aparecen exactamente las direcciones esperadasEl bus y los dispositivos están bien. Falta confirmar (con el registro de identificación) que cada dirección es el dispositivo que se cree
Falta una direcciónEse dispositivo no contesta: alimentación, cableado, dirección distinta de la supuesta (cambió un pin) o dispositivo ocupado
Aparece una dirección que no esperabaOtro dispositivo (un módulo con varios chips: un EEPROM junto al reloj, por ejemplo), o la dirección alternativa de un dispositivo
No aparece nadaPull-ups ausentes, SDA y SCL cruzadas, o dispositivos sin alimentación
Contestan todas las direccionesSDA está atascada en bajo: en cada noveno pulso el maestro la ve baja y lo toma por un ACK. Un dispositivo trabado o un cortocircuito (tema 19)
Aparece una dirección donde hay dos dispositivosEl barrido no distingue dos chips con la misma dirección: se ve una sola. Si los datos salen mezclados, es esta la causa

Una vez que aparecen direcciones, la tabla siguiente ayuda a adivinar qué son. Muchas se repiten entre fabricantes, así que son pistas, no certezas:

Direcciones frecuentes de 7 bits (con sus variantes por pines).
DirecciónDispositivos habituales
0x20 a 0x27Expansor PCF8574 (y las mochilas de pantallas LCD, a menudo en 0x27)
0x23 / 0x5CSensor de luz BH1750 (según el pin ADDR)
0x29Sensor de distancia VL53L0X
0x38 a 0x3FExpansor PCF8574A
0x3C / 0x3DPantalla OLED SSD1306
0x40Sensor de corriente INA219, sensores de humedad HTU21D / Si7021, controlador de servos PCA9685
0x48 a 0x4BConversor ADS1115, sensor de temperatura TMP102
0x50 a 0x57Memorias EEPROM de la familia 24Cxx
0x53 / 0x1DAcelerómetro ADXL345 (según su pin ALT)
0x68 / 0x69Reloj DS3231 o DS1307 (0x68), acelerómetro MPU6050 (0x68 o 0x69)
0x70Dirección de «llamada a todos» del PCA9685, multiplexores TCA9548A (0x70 a 0x77)
0x76 / 0x77Sensores BME280 / BMP280 / BMP180

Dos advertencias. Un barrido envía una escritura de solo la dirección y es inofensivo para casi todos los dispositivos, pero unos pocos reaccionan mal a un mensaje sin datos (algunos lo toman como un comando). Y el barrido solo prueba que alguien contesta: la certeza de que es el dispositivo correcto la da leer su registro de identificación.

05 · De los bytes al valor

Datos de varios bytes

De dos bytes a una magnitud física El byte alto 0xFC y el byte bajo 0xE0 se combinan en un entero de 16 bits, 0xFCE0. Se interpreta con signo, lo que da menos 800. Se divide por la sensibilidad de 16384 cuentas por g y da menos 0,049 g. byte alto 0xFC byte bajo 0xE0 combinar (alto << 8) | bajo 16 bits 0xFCE0 = 64736 con signo (C2) 64736 − 65536 = −800 ÷ 16384 cuentas/g −0,049 g Si el dispositivo manda primero el byte bajo, el orden se invierte: (bajo, alto) en lugar de (alto, bajo).

Una magnitud medida casi nunca entra en un byte. Se reparte en dos o más registros consecutivos, y para recomponerla hay que resolver tres cuestiones que dependen del dispositivo y están en su hoja de datos:

1. El orden de los bytes

En el formato big-endian el primer registro tiene el byte alto (MPU6050, BME280 en los datos de medición, ADS1115, TMP102). En el little-endian el primero es el bajo (ADXL345, y los coeficientes de calibración del BME280). La combinación es distinta en cada caso:

big-endian: valor = (b1 << 8) | b2  little-endian: valor = (b2 << 8) | b1

Equivocarse de orden produce valores absurdos pero a veces razonables: con los bytes 0xFC 0xE0 (−800) un orden invertido da 0xE0FC, es decir, −7940. No hay error ni aviso: solo números que no cuadran.

2. El signo

Las magnitudes con signo (aceleración, velocidad angular, temperatura bajo cero) se guardan en complemento a dos. Al armar un valor de 16 bits en un tipo sin signo hay que reinterpretarlo: en C se hace con un int16_t, que convierte 0xFCE0 (64 736) en −800; en un lenguaje sin tipos de ancho fijo se resta 65 536 cuando el bit 15 vale 1. Si se usa un int de 16 bits sin cuidado, o un unsigned, la aceleración negativa aparece como un número enorme.

3. La escala y la forma

El entero es «cuentas del conversor», no una magnitud física. Hay que dividir por la sensibilidad, que depende de la configuración: el MPU6050 en ±2 g da 16 384 cuentas por g; en ±4 g, 8192; en ±8 g, 4096; en ±16 g, 2048. La temperatura es °C = cuentas / 340 + 36,53. Y no todos los datos son enteros binarios sencillos:

Formatos de datos frecuentes.
FormatoEjemplo de bytesCómo se decodifica
16 bits con signo, big-endian0xFC 0xE0(int16_t)((b1 << 8) | b2) = −800
16 bits con signo, little-endian0xE0 0xFC(int16_t)((b2 << 8) | b1) = −800
12 bits justificados a la izquierda (TMP102)0x19 0x00entero de 16 bits con signo, desplazado 4 bits a la derecha: 0x1900 >> 4 = 400; × 0,0625 °C = 25,0 °C
20 bits repartidos en 3 bytes (presión del BME280)msb lsb xlsb(msb << 12) | (lsb << 4) | (xlsb >> 4)
BCD (relojes: DS3231, DS1307)0x59una cifra decimal por nibble: 5 × 10 + 9 = 59

Una última cuestión es la coherencia. Si un valor de 16 bits se lee con dos lecturas de un byte, entre ellas el sensor puede terminar una nueva medición y se mezcla el byte alto de una muestra con el bajo de otra: un valor que nunca existió. Dos remedios: leer todos los bytes en una sola ráfaga (muchos sensores congelan los registros de datos mientras dura la lectura) o seguir las indicaciones de la hoja de datos sobre qué registro se lee primero. Además, la ráfaga es más rápida: un único mensaje con una dirección en lugar de varios.

06 · Escribir con cuidado

Escribir sin pisar

Un registro de configuración suele agrupar varios campos de pocos bits: el rango y el autotest de ACCEL_CONFIG, por ejemplo. Escribir el registro completo para cambiar un solo campo borra los demás. La solución es la operación leer–modificar–escribir:

nuevo = (actual & ~máscara) | (valor & máscara)
  1. Se lee el registro (actual).
  2. Con & ~máscara se ponen en cero los bits del campo a cambiar y se conservan los demás.
  3. Con | (valor & máscara) se ponen los bits nuevos del campo.
  4. Se escribe el resultado.

Ejemplo: elegir ±8 g en ACCEL_CONFIG (bits 4 y 3 = 10, máscara 0x18, valor 0x10) cuando el registro vale 0xE0 (autotest en los bits 7 a 5): (0xE0 & ~0x18) | 0x10 = 0xE0 | 0x10 = 0xF0. Los bits del autotest quedan como estaban. Este patrón se usa tanto que conviene tener la función hecha (sección 07).

Algunas reglas para escribir registros:

  • Verificar leyendo de vuelta. Un ACK solo dice que el byte llegó. Leer el registro después de escribirlo comprueba que se guardó (y detecta registros de solo lectura o bits reservados que el chip fuerza a un valor).
  • Respetar los bits reservados. La hoja de datos suele indicar que ciertos bits «deben escribirse como 0» o «se leen como 1». Conviene partir del valor leído y no de una constante.
  • Esperar lo que haga falta. Un reinicio por software, el encendido de un sensor o una conversión llevan tiempo. El MPU6050, por ejemplo, necesita unos 100 ms tras salir del modo de reposo antes de que los datos sean válidos. La hoja de datos da los tiempos.
  • El orden importa. Muchos dispositivos exigen una secuencia de inicio (salir del reposo, configurar, activar las mediciones). Hay que seguirla tal cual.
  • Una escritura en ráfaga también autoincrementa: S dir+W A reg A d1 A d2 A P escribe d1 en reg y d2 en reg + 1. Útil para configurar varios registros contiguos con un solo mensaje.

07 · Con el microcontrolador

Con el microcontrolador

El primer programa es el barrido. Recorre las 112 direcciones válidas y avisa de las que contestan, y de los códigos de error que no son un simple NACK:

#include <Wire.h>

void barrido() {
    byte encontrados = 0;
    for (byte dir = 0x08; dir <= 0x77; dir++) {
        Wire.beginTransmission(dir);
        byte codigo = Wire.endTransmission();          // solo la dirección: S dir+W [A/N] P
        if (codigo == 0) {
            Serial.print("Contesta 0x"); Serial.println(dir, HEX);
            encontrados++;
        } else if (codigo != 2) {                      // 2 = nadie contesta (lo normal); otros, algo raro
            Serial.print("Error "); Serial.print(codigo);
            Serial.print(" en 0x"); Serial.println(dir, HEX);
        }
    }
    Serial.print(encontrados); Serial.println(" dispositivo(s)");
    if (encontrados == 112) Serial.println("Contestaron todas: SDA atascada en bajo?");
    if (encontrados == 0)   Serial.println("Ninguna: revisar pull-ups, cableado y alimentación");
}

void setup() {
    Serial.begin(115200);
    Wire.begin();
    barrido();
}

void loop() {}

El segundo programa reúne las tres funciones básicas y las usa para despertar y leer un MPU6050: identifica el chip, sale del modo de reposo, elige el rango de ±2 g y lee en cada vuelta los 14 bytes de aceleración, temperatura y giroscopio en una sola ráfaga.

#include <Wire.h>

const byte MPU = 0x68;                               // AD0 a masa
const byte REG_ACCEL_CONFIG = 0x1C;
const byte REG_ACCEL_XOUT_H = 0x3B;
const byte REG_PWR_MGMT_1   = 0x6B;
const byte REG_WHO_AM_I     = 0x75;

// S dir+W A reg A valor A P
bool escribirReg(byte dir, byte reg, byte valor) {
    Wire.beginTransmission(dir);
    Wire.write(reg);
    Wire.write(valor);
    return Wire.endTransmission() == 0;
}

// S dir+W A reg A Sr dir+R A dato ... dato N P   (devuelve cuántos bytes llegaron, o -1 si falló el puntero)
int leerRegs(byte dir, byte reg, byte *destino, byte n) {
    Wire.beginTransmission(dir);
    Wire.write(reg);
    if (Wire.endTransmission(false) != 0) return -1;     // sin STOP: sigue un START repetido
    byte recibidos = Wire.requestFrom(dir, (uint8_t)n);
    for (byte i = 0; i < recibidos; i++) destino[i] = Wire.read();
    return recibidos;
}

// Cambia solo los bits de la máscara: leer - modificar - escribir.
bool modificarBits(byte dir, byte reg, byte mascara, byte valor) {
    byte actual;
    if (leerRegs(dir, reg, &actual, 1) != 1) return false;
    return escribirReg(dir, reg, (actual & ~mascara) | (valor & mascara));
}

int16_t combinar(byte alto, byte bajo) {             // big-endian con signo
    return (int16_t)((alto << 8) | bajo);
}

void setup() {
    Serial.begin(115200);
    Wire.begin();
    Wire.setClock(400000);

    byte id = 0;
    if (leerRegs(MPU, REG_WHO_AM_I, &id, 1) != 1 || id != 0x68) {
        Serial.print("No es un MPU6050 (WHO_AM_I = 0x"); Serial.print(id, HEX); Serial.println("). Revisar cableado y dirección.");
        while (true) delay(1000);
    }
    escribirReg(MPU, REG_PWR_MGMT_1, 0x00);          // sale del reposo (bit SLEEP en 0)
    delay(100);                                      // tiempo de arranque del sensor
    modificarBits(MPU, REG_ACCEL_CONFIG, 0x18, 0x00); // bits 4:3 = 00 -> ±2 g, sin tocar los demás
}

void loop() {
    byte b[14];
    if (leerRegs(MPU, REG_ACCEL_XOUT_H, b, 14) != 14) {
        Serial.println("Lectura incompleta");
        delay(500);
        return;
    }
    float ax = combinar(b[0], b[1]) / 16384.0;      // 16384 cuentas por g en ±2 g
    float ay = combinar(b[2], b[3]) / 16384.0;
    float az = combinar(b[4], b[5]) / 16384.0;
    float temp = combinar(b[6], b[7]) / 340.0 + 36.53;
    float gx = combinar(b[8], b[9]) / 131.0;        // 131 cuentas por °/s en ±250 °/s
    Serial.print("a = ("); Serial.print(ax, 3); Serial.print(", "); Serial.print(ay, 3); Serial.print(", "); Serial.print(az, 3);
    Serial.print(") g  T = "); Serial.print(temp, 1); Serial.print(" C  gx = "); Serial.print(gx, 2); Serial.println(" grados/s");
    delay(200);
}

Cuatro decisiones del código. El primer paso es leer WHO_AM_I y detener el programa si no coincide: es mejor un mensaje claro que una hora de datos sin sentido. Cada función devuelve un resultado que se compara: leerRegs da cuántos bytes llegaron, y -1 si ni siquiera se pudo escribir el puntero. La lectura es una sola ráfaga de 14 bytes, no siete lecturas de dos bytes, para que las muestras sean coherentes y para ahorrar tiempo. Y la temperatura y la aceleración se calculan en float recién al final, con el entero ya reconstruido con signo.

Un detalle del ESP32: si se usan pines distintos de los habituales, se inicializa con Wire.begin(sda, scl). Y si un módulo tiene el pin AD0 en alto, la dirección pasa a ser 0x69 y solo cambia la constante MPU.

08 · Analogía con programación

Un dispositivo I²C en Python

Un dispositivo I²C es, para el programador, un objeto con una memoria de 256 bytes, un puntero y dos operaciones. El modelo siguiente lo implementa, junto con un bus que contesta como Wire (0 si hay ACK, 2 si nadie contesta) y las funciones del tema: barrido, lectura en ráfaga, escritura, leer–modificar–escribir y la reconstrucción de enteros con signo. Sirve para probar la lógica de un controlador antes de tener el sensor.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

class Sensor:
    """Dispositivo con 256 registros, un puntero y autoincremento."""
    def __init__(self, direccion, nombre, registros, solo_lectura=()):
        self.direccion, self.nombre = direccion, nombre
        self.mem = bytearray(256)
        for reg, valor in registros.items():
            self.mem[reg] = valor
        self.solo_lectura = set(solo_lectura)
        self.puntero = 0

    def escribir(self, datos):
        """Bytes de un mensaje de escritura: el primero es el puntero, los demás se guardan."""
        self.puntero = datos[0]
        for d in datos[1:]:
            if self.puntero not in self.solo_lectura:    # en un registro de solo lectura se confirma pero no se guarda
                self.mem[self.puntero] = d
            self.puntero = (self.puntero + 1) % 256

    def leer(self, n):
        salida = []
        for _ in range(n):
            salida.append(self.mem[self.puntero])
            self.puntero = (self.puntero + 1) % 256     # autoincremento
        return salida


class Bus:
    def __init__(self, *dispositivos):
        self.disp = {d.direccion: d for d in dispositivos}

    def escribir(self, direccion, datos):                # equivale a endTransmission()
        if direccion not in self.disp:
            return 2
        self.disp[direccion].escribir(list(datos))
        return 0

    def leer(self, direccion, n):                        # equivale a requestFrom() + read()
        return self.disp[direccion].leer(n) if direccion in self.disp else []

    def barrido(self):
        return [d for d in range(0x08, 0x78) if d in self.disp]


def leer_regs(bus, direccion, reg, n):
    bus.escribir(direccion, [reg])                       # fija el puntero
    return bus.leer(direccion, n)                        # lee n bytes consecutivos


def escribir_reg(bus, direccion, reg, valor):
    return bus.escribir(direccion, [reg, valor])


def modificar(bus, direccion, reg, mascara, valor):
    actual = leer_regs(bus, direccion, reg, 1)[0]
    nuevo = (actual & ~mascara & 0xFF) | (valor & mascara)
    escribir_reg(bus, direccion, reg, nuevo)
    return actual, nuevo


def int16(alto, bajo):                                   # big-endian con signo
    v = (alto << 8) | bajo
    return v - 65536 if v & 0x8000 else v


def bcd(b):
    return (b >> 4) * 10 + (b & 0x0F)


hexa = lambda b: f"{b:#04x}"

solo_lectura = {0x75} | set(range(0x3B, 0x49))
mpu = Sensor(0x68, "MPU6050", {
    0x75: 0x68, 0x6B: 0x40, 0x1C: 0x00,
    0x3B: 0xFC, 0x3C: 0xE0, 0x3D: 0x04, 0x3E: 0xCE, 0x3F: 0x40, 0x40: 0x00,   # aceleración X, Y, Z
    0x41: 0xF0, 0x42: 0xB0,                                                    # temperatura
    0x43: 0x00, 0x44: 0x00, 0x45: 0xFF, 0x46: 0xFF, 0x47: 0x00, 0x48: 0x03,   # giroscopio
}, solo_lectura)
bme = Sensor(0x76, "BME280", {0xD0: 0x60}, {0xD0})
bus = Bus(mpu, bme)

print("Barrido:", [hexa(d) for d in bus.barrido()])
print("WHO_AM_I del 0x68:", hexa(leer_regs(bus, 0x68, 0x75, 1)[0]), "| ID del 0x76:", hexa(leer_regs(bus, 0x76, 0xD0, 1)[0]))

b = leer_regs(bus, 0x68, 0x3B, 14)
print("Ráfaga de 14 bytes:", " ".join(hexa(x) for x in b), "| puntero final:", hexa(mpu.puntero))
ax, ay, az = (int16(b[i], b[i + 1]) for i in (0, 2, 4))
temp = int16(b[6], b[7])
gx, gy, gz = (int16(b[i], b[i + 1]) for i in (8, 10, 12))
print(f"aceleración: {ax}, {ay}, {az} cuentas -> {ax / 16384:.3f}, {ay / 16384:.3f}, {az / 16384:.3f} g")
print(f"temperatura: {temp} cuentas -> {temp / 340 + 36.53:.1f} °C")
print(f"giroscopio: {gx}, {gy}, {gz} cuentas -> {gx / 131:.3f}, {gy / 131:.3f}, {gz / 131:.3f} °/s")

print()
print("PWR_MGMT_1 al arrancar:", hexa(leer_regs(bus, 0x68, 0x6B, 1)[0]))
print("Escribir 0x00 (código", escribir_reg(bus, 0x68, 0x6B, 0x00), ") ->", hexa(leer_regs(bus, 0x68, 0x6B, 1)[0]))
escribir_reg(bus, 0x68, 0x1C, 0xE0)
antes, despues = modificar(bus, 0x68, 0x1C, 0x18, 0x10)
print(f"ACCEL_CONFIG: {hexa(antes)} -> {hexa(despues)} (±8 g sin tocar el autotest); leído: {hexa(leer_regs(bus, 0x68, 0x1C, 1)[0])}")
escribir_reg(bus, 0x68, 0x75, 0x00)
print("Escribir 0x00 en WHO_AM_I (solo lectura):", hexa(leer_regs(bus, 0x68, 0x75, 1)[0]), "(sigue igual)")
print("Escribir en 0x69, donde no hay nadie: código", bus.escribir(0x69, [0x75]))
print(f"Orden de bytes invertido: int16(0xE0, 0xFC) = {int16(0xE0, 0xFC)}; BCD 0x59 = {bcd(0x59)}, 0x23 = {bcd(0x23)}")

Resultado: el barrido encuentra 0x68 y 0x76. El WHO_AM_I del 0x68 es 0x68 y el ID del BME280, 0x60. La ráfaga de 14 bytes devuelve 0xfc 0xe0 0x04 0xce 0x40 0x00 0xf0 0xb0 0x00 0x00 0xff 0xff 0x00 0x03 y el puntero termina en 0x49, un registro después del último leído (0x48): el autoincremento. Con los enteros con signo, la aceleración es −800, 1230 y 16 384 cuentas, o −0,049, 0,075 y 1,000 g (el eje Z mide la gravedad), la temperatura es −3920 cuentas, que son 25,0 °C, y el giroscopio da 0, −1 y 3 cuentas (0,000, −0,008 y 0,023 °/s). PWR_MGMT_1 arranca en 0x40 y tras escribir 0x00 vale 0x00. En ACCEL_CONFIG, con 0xe0 de partida, el rango de ±8 g da 0xf0: los bits de autotest se conservan. Escribir en WHO_AM_I no cambia nada (sigue en 0x68) y escribir en la dirección 0x69 devuelve el código 2. Por último, el orden invertido da −7940 en lugar de −800, y los BCD 0x59 y 0x23 valen 59 y 23.

Observá que el modelo no simula el reposo del sensor real (donde los registros de datos no se actualizan hasta salir de él), ni cambia las cuentas al cambiar el rango. El laboratorio de la sección siguiente sí recalcula las cuentas al cambiar el rango de ACCEL_CONFIG.

09 · Exploración interactiva

Laboratorio: barrido, registros y bytes

El laboratorio tiene tres herramientas. La primera es un barrido de direcciones sobre un bus virtual: elegís qué dispositivos hay conectados y en qué estado está el bus, y ves qué direcciones contestan. La segunda es una consola de registros de un MPU6050 virtual: leés, escribís y leés en ráfaga, y ves el mensaje, los bytes y el puntero. La tercera es un decodificador de bytes: elegís el formato y ves el valor y qué pasaría con el orden equivocado.

1. Barrido de direcciones

Direcciones que contestan—
Cantidad de respuestas—
Dispositivos reales—
Duración del barrido (100 kHz · 400 kHz)—
Veredicto—

Elegí los dispositivos y presioná «Barrer el bus».

2. Consola de registros de un MPU6050 (0x68)

Mensaje en el bus—
Bytes recibidos o enviados—
Puntero después—
Interpretación—
PWR_MGMT_1 · ACCEL_CONFIG—
Veredicto—

Elegí una operación y presioná «Ejecutar».

3. Decodificador de bytes

Valor entero—
Magnitud física—
Con el orden equivocado—

Escribí dos bytes y elegí el formato.

En la herramienta 1, un barrido son 112 mensajes de un byte (9 pulsos cada uno), sin contar los START, los STOP ni las pausas. En la 2, los registros de aceleración y giroscopio se recalculan según el rango de ACCEL_CONFIG (las cuentas son las que correspondería a la misma aceleración física: 1 g en Z); el giroscopio y la temperatura se interpretan con las escalas por defecto. En la 3, las magnitudes físicas se calculan con las sensibilidades del MPU6050 (16 384 cuentas por g) y del TMP102 (0,0625 °C por cuenta).

Recorrido guiado

  1. Con los valores iniciales (MPU6050, BME280 y SSD1306), presioná «Barrer el bus»: contestan 0x3C, 0x68 y 0x76, tres de 112, en 10,08 ms a 100 kHz y 2,52 ms a 400 kHz.
  2. Poné como dispositivo 4 un DS3231 (0x68): son cuatro dispositivos reales pero el barrido sigue mostrando tres direcciones, porque el MPU6050 y el reloj comparten la 0x68. Es el aviso de direcciones repetidas.
  3. Cambiá el estado del bus a «SDA atascada en bajo»: contestan las 112 direcciones. Con «Sin pull-ups» no contesta ninguna. Ambos síntomas apuntan al tema 16 y al 19, no a los dispositivos.
  4. En la herramienta 2, con los valores iniciales (leer el registro 75), ejecutá: S 0xD0(W) A 0x75 A Sr 0xD1(R) A 0x68 N P. El valor 0x68 es el WHO_AM_I y el puntero queda en 0x76.
  5. Elegí «Leer en ráfaga», registro 3B, 14 bytes: llegan FC E0 04 CE 40 00 F0 B0 00 00 FF FF 00 03, el puntero termina en 0x49 y la interpretación da X = −0,049 g, Y = 0,075 g, Z = 1,000 g y 25,0 °C.
  6. Leé el registro 6B: 0x40 (el sensor duerme). Elegí «Escribir un registro», registro 6B, valor 00: el mensaje es S 0xD0(W) A 0x6B A 0x00 A P y PWR_MGMT_1 pasa a 0x00.
  7. Escribí en el registro 75: el dispositivo confirma pero «ignorado: solo lectura». Es el caso de un ACK que no significa que se guardó algo.
  8. Elegí «Modificar bits» con registro 1C, máscara 18 y valor nuevo 10 (±8 g): se lee 0x00 y se escribe 0x10. Repetí la ráfaga de 14 bytes: como la aceleración física no cambió pero la sensibilidad sí, las cuentas de Z pasan a 0x10 0x00 (4096) y las de X a 0xFF 0x38 (−200). La interpretación del laboratorio usa la nueva sensibilidad (4096 cuentas por g) y sigue dando 1,000 g en Z.
  9. Hacé la cuenta de la manera equivocada: 4096 cuentas divididas por 16 384 darían 0,25 g. Es el error clásico de cambiar el rango y olvidar la escala.
  10. En la herramienta 3, con los bytes FC E0 y el formato big-endian: −800, o −0,049 g. «Con el orden equivocado»: −7940. Pasá al formato little-endian: ahora el resultado es −7940, y el que sería correcto con el otro orden, −800. Probá 19 00 en «12 bits justificados» (TMP102): 400 cuentas = 25,0 °C. Y 59 23 en BCD: 59 y 23.
Desafío: ráfaga contra lecturas separadas

Leer los 14 bytes de datos desde el registro 0x3B en una sola ráfaga, ¿cuántos pulsos cuesta, frente a siete lecturas separadas de dos bytes cada una (una por magnitud)? Una ráfaga: 0xD0, 0x3B, 0xD1 y 14 datos = 17 bytes = 153 pulsos (382,5 µs a 400 kHz). Cada lectura de dos bytes: 0xD0, registro, 0xD1 y 2 datos = 5 bytes = 45 pulsos; siete lecturas son 315 pulsos (787,5 µs a 400 kHz): más del doble. Y además, las siete lecturas pueden mezclar muestras de instantes distintos.

10 · Un procedimiento

Cómo atacar un dispositivo nuevo

Con un sensor que nunca usaste, la hoja de datos tiene decenas de páginas. Un recorrido en el mismo orden evita perderse:

  1. Buscar la dirección y sus variantes por pines. Anotarla en 7 bits.
  2. Hacer un barrido. ¿Aparece la dirección? Si no, arreglar eso antes de seguir.
  3. Leer el registro de identificación. ¿Da el valor de la hoja? Si no, es otro dispositivo o hay algo mal con la lectura.
  4. Leer el estado inicial de los registros de configuración y compararlos con los «valores de reinicio» de la hoja de datos: confirma que se entiende el mapa.
  5. Seguir la secuencia de arranque (salir del reposo, configurar rango y velocidad, esperar el tiempo indicado). Verificar cada escritura leyéndola de vuelta.
  6. Leer los datos en ráfaga y reconstruirlos: orden de bytes, signo, escala. Probar con una situación conocida (el eje Z de un acelerómetro quieto debe dar 1 g; un sensor de temperatura debe dar el ambiente).
  7. Agregar control de errores: revisar los códigos de Wire y la cantidad de bytes recibidos, con mensajes que digan qué falló.
Síntomas al trabajar con registros y su causa probable.
SíntomaCausa probableQué hacer
El barrido encuentra el dispositivo pero el WHO_AM_I da 0x00 o 0xFFSe leyó sin fijar bien el puntero, o el dispositivo está dormidoRevisar endTransmission(false) y el START repetido; mirar la captura
Todos los datos valen 0El sensor sigue en reposo o sin configurarSalir del modo de reposo y esperar el tiempo de arranque
Los datos cambian con valores absurdos o saltosOrden de bytes invertido, falta el signo o lecturas no coherentesRevisar la combinación y usar ráfaga
El valor es correcto pero su escala noSe cambió el rango y no la sensibilidadCalcular la escala a partir del registro de configuración
Una configuración se pierde al cambiar otraSe escribió el registro completo en lugar de modificar bitsUsar leer–modificar–escribir
La lectura devuelve menos bytes de los pedidosSe superó el buffer, el dispositivo dejó de contestar o se cortó el mensajePartir la lectura; mirar el resultado de requestFrom
Funciona unos minutos y deja de responderUn bus que se bloquea (tema 19) o alimentación inestableRevisar las líneas en reposo y agregar recuperación del bus

11 · Comprensión

Confusiones frecuentes

«Para leer un registro alcanza con Wire.requestFrom()»

No: la lectura devuelve el registro al que apunta el puntero, y eso depende de la última escritura. Hay que fijar el puntero antes con beginTransmission, write(reg) y endTransmission(false). Si se omite, se lee lo que quedó apuntado por la operación anterior (a veces, el registro correcto por casualidad).

«Si el barrido encuentra la dirección, el sensor funciona»

Solo hay alguien que contesta. Puede ser otro chip con la misma dirección, o el sensor correcto pero dormido o sin configurar. El registro de identificación es la comprobación de verdad.

«Para cambiar un bit escribo el registro con el valor que quiero»

Eso pisa los otros bits del registro (incluidos los reservados y los de otras funciones). Se lee, se modifica solo el campo con una máscara y se vuelve a escribir.

«Un ACK al escribir confirma que el valor se guardó»

Un ACK confirma la recepción del byte. Un registro de solo lectura o un valor inválido pueden confirmarse y descartarse. Se verifica leyendo de vuelta.

«Los valores de 16 bits siempre llegan con el byte alto primero»

Depende del dispositivo: el MPU6050 y el ADS1115 sí; el ADXL345 y la calibración del BME280, no. Se confirma en la hoja de datos, y se prueba con un valor conocido.

«Leer dos bytes por separado es lo mismo que leerlos juntos»

No: entre ambas lecturas puede actualizarse la medición y el resultado mezcla dos muestras. Además cuesta más del doble en tiempo de bus. Lo correcto es una única ráfaga.

«Wire.read() devuelve un byte»

Devuelve un int: el byte (0 a 255) o −1 si no hay nada que leer. Guardarlo directamente en un byte sin comprobar available() o el valor de requestFrom() convierte una lectura fallida en un 0xFF engañoso.

«Al encender, el dispositivo está listo para medir»

Muchos arrancan en reposo o en un modo de bajo consumo (el MPU6050 con el bit SLEEP en 1). Hay que configurarlos y esperar el tiempo de arranque antes de leer los datos.

12 · Práctica guiada

Predecí antes de comprobar

1. Leer el WHO_AM_I

Escribí la secuencia de símbolos para leer el registro 0x75 de un dispositivo en 0x68 y calculá cuántos pulsos y cuánto tiempo a 100 kHz y a 400 kHz.

Ver solución

S 0xD0(W) A 0x75 A Sr 0xD1(R) A 0x68 N P. Son 4 bytes (0xD0, 0x75, 0xD1 y el dato): 4 × 9 = 36 pulsos: 360 µs a 100 kHz y 90 µs a 400 kHz.

2. De cuentas a grados

La temperatura del MPU6050 llega como 0xF0 0xB0. ¿Qué temperatura es? (°C = cuentas / 340 + 36,53).

Ver solución

0xF0B0 = 61 616; como el bit 15 vale 1, es negativo: 61 616 − 65 536 = −3920. Entonces −3920 / 340 + 36,53 = −11,53 + 36,53 = 25,0 °C.

3. Cambiar un campo de bits

ACCEL_CONFIG vale 0xA8. Querés pasar a ±16 g (bits 4 y 3 = 11) sin tocar el resto. ¿Qué máscara y qué valor usás, y qué se escribe?

Ver solución

Máscara 0x18, valor 0x18. 0xA8 & ~0x18 = 0xA0 (1010 1000 con los bits 4 y 3 en cero) y 0xA0 | 0x18 = 0xB8. Los bits 7 y 5 (autotest) quedan como estaban.

4. Ráfaga o lecturas separadas

Comparar el costo de leer 14 bytes en una ráfaga (desde 0x3B) y de leer siete magnitudes de 2 bytes con lecturas independientes, a 400 kHz.

Ver solución

Ráfaga: 3 + 14 = 17 bytes = 153 pulsos = 382,5 µs. Siete lecturas: 7 × 5 bytes × 9 = 315 pulsos = 787,5 µs, más del doble, y con riesgo de mezclar muestras de instantes distintos.

5. La escala equivocada

Configurás ±8 g y la aceleración Z, con el sensor quieto, llega como 0x10 0x00. ¿Cuánto da si dividís por 16 384? ¿Cuál es el valor correcto?

Ver solución

0x1000 = 4096 cuentas. Con 16 384: 4096 / 16 384 = 0,25 g (incorrecto). En ±8 g la sensibilidad es de 4096 cuentas por g: 4096 / 4096 = 1,00 g, la gravedad.

6. Un barrido sospechoso

Un barrido informa «112 dispositivos». ¿Qué falla sospechás y qué mirarías?

Ver solución

No hay 112 chips: SDA está atascada en bajo, y el maestro ve un ACK en el noveno pulso de cualquier dirección. Se mide SDA en reposo (tema 16): se desconectan los dispositivos de a uno para ver cuál la retiene, y se busca un cortocircuito.

7. Un reloj en BCD

Un DS3231 devuelve minutos 0x59 y horas 0x23. ¿Qué hora es? ¿Qué leerías si lo tomaras como binario? ¿Y qué byte escribirías para poner 45 minutos?

Ver solución

BCD: 0x59 es 59 (nibbles 5 y 9) y 0x23 es 23: son las 23:59. Como binario, 0x59 = 89 y 0x23 = 35. Para escribir 45 se arma ((45 / 10) << 4) | (45 % 10) = 0x45.

13 · Síntesis

Ideas para recordar

  • Un dispositivo I²C es un mapa de registros: se lo configura escribiendo y se obtienen los datos leyendo. La hoja de datos trae la tabla; el mecanismo es siempre el mismo.
  • El primer byte de datos de una escritura es el puntero de registro. Cada byte leído o escrito avanza el puntero (autoincremento), de modo que una lectura de n bytes devuelve n registros consecutivos.
  • Leer un registro son dos pasos sin soltar el bus: escribir el puntero (endTransmission(false)) y leer con un START repetido (requestFrom). Escribir un registro es S dir+W A reg A valor A P.
  • Un barrido prueba las 112 direcciones con mensajes de solo la dirección. Que una dirección conteste no prueba qué es: eso lo da el registro de identificación. Contestan todas = SDA atascada; no contesta ninguna = faltan pull-ups o alimentación; una dirección repetida se ve como una sola.
  • Para recomponer un dato de varios bytes hay que conocer el orden de los bytes, el signo (complemento a dos, con int16_t), la escala (según el rango configurado) y el formato (BCD, justificado a la izquierda, 20 bits).
  • Los valores de varios bytes se leen en una sola ráfaga: es más rápido y evita mezclar muestras de instantes distintos.
  • Para cambiar un campo de bits se usa leer–modificar–escribir: nuevo = (actual & ~máscara) | (valor & máscara). Un ACK no prueba que se guardó: se lee de vuelta.
  • Para un dispositivo nuevo: dirección, barrido, registro de identificación, estado inicial, secuencia de arranque verificada, lectura en ráfaga y control de errores.

¿Listo para el siguiente tema?

Antes de avanzar, escribí la secuencia de símbolos para escribir 0x08 en el registro 0x1C de un dispositivo en 0x68; reconstruí el valor de los bytes 0xFF 0x38 como entero de 16 bits con signo, big-endian; y explicá por qué un barrido que informa 112 respuestas indica una falla del bus y no 112 dispositivos. (Para la segunda: 0xFF38 − 65 536 = −200.) Si podés justificar las tres respuestas, sabés manejar los registros de un dispositivo I²C.

En el próximo tema vemos los problemas reales de I²C: la capacidad del bus, el estiramiento del reloj, las direcciones repetidas y los multiplexores, y cómo recuperar un bus bloqueado.