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:
| Dirección | Nombre | Acceso | Contenido |
|---|---|---|---|
0x75 | WHO_AM_I | lectura | Identificación: siempre 0x68 |
0x6B | PWR_MGMT_1 | lectura y escritura | Administración de energía. Valor de arranque 0x40: el bit 6 (SLEEP) está en 1, el sensor duerme |
0x1C | ACCEL_CONFIG | lectura y escritura | Bits 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 0x40 | ACCEL_XOUT_H … ACCEL_ZOUT_L | lectura | Aceleración en X, Y y Z: tres enteros de 16 bits con signo, byte alto primero |
0x41 y 0x42 | TEMP_OUT_H y _L | lectura | Temperatura: 16 bits con signo |
0x43 a 0x48 | GYRO_XOUT_H … GYRO_ZOUT_L | lectura | Velocidad 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
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:
- 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. - 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 Pescribe 0x00 en el registro 0x6B. - 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.
| Función | Qué hace | En 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 bits | nada todavía |
Wire.write(byte) | Agrega un byte (o un arreglo) al buffer de envío | nada 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 repetido | S 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 llegaron | Sr (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:
| Resultado | Interpretación |
|---|---|
| Aparecen exactamente las direcciones esperadas | El 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ón | Ese dispositivo no contesta: alimentación, cableado, dirección distinta de la supuesta (cambió un pin) o dispositivo ocupado |
| Aparece una dirección que no esperaba | Otro dispositivo (un módulo con varios chips: un EEPROM junto al reloj, por ejemplo), o la dirección alternativa de un dispositivo |
| No aparece nada | Pull-ups ausentes, SDA y SCL cruzadas, o dispositivos sin alimentación |
| Contestan todas las direcciones | SDA 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 dispositivos | El 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:
| Dirección | Dispositivos habituales |
|---|---|
0x20 a 0x27 | Expansor PCF8574 (y las mochilas de pantallas LCD, a menudo en 0x27) |
0x23 / 0x5C | Sensor de luz BH1750 (según el pin ADDR) |
0x29 | Sensor de distancia VL53L0X |
0x38 a 0x3F | Expansor PCF8574A |
0x3C / 0x3D | Pantalla OLED SSD1306 |
0x40 | Sensor de corriente INA219, sensores de humedad HTU21D / Si7021, controlador de servos PCA9685 |
0x48 a 0x4B | Conversor ADS1115, sensor de temperatura TMP102 |
0x50 a 0x57 | Memorias EEPROM de la familia 24Cxx |
0x53 / 0x1D | Acelerómetro ADXL345 (según su pin ALT) |
0x68 / 0x69 | Reloj DS3231 o DS1307 (0x68), acelerómetro MPU6050 (0x68 o 0x69) |
0x70 | Dirección de «llamada a todos» del PCA9685, multiplexores TCA9548A (0x70 a 0x77) |
0x76 / 0x77 | Sensores 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
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:
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:
| Formato | Ejemplo de bytes | Cómo se decodifica |
|---|---|---|
| 16 bits con signo, big-endian | 0xFC 0xE0 | (int16_t)((b1 << 8) | b2) = −800 |
| 16 bits con signo, little-endian | 0xE0 0xFC | (int16_t)((b2 << 8) | b1) = −800 |
| 12 bits justificados a la izquierda (TMP102) | 0x19 0x00 | entero 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) | 0x59 | una 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:
- Se lee el registro (
actual). - Con
& ~máscarase ponen en cero los bits del campo a cambiar y se conservan los demás. - Con
| (valor & máscara)se ponen los bits nuevos del campo. - 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 Pescribed1enregyd2enreg + 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
Elegí los dispositivos y presioná «Barrer el bus».
2. Consola de registros de un MPU6050 (0x68)
Elegí una operación y presioná «Ejecutar».
3. Decodificador de bytes
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
- 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.
- 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.
- 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.
- 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. - 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. - 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 Py PWR_MGMT_1 pasa a 0x00. - 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.
- Elegí «Modificar bits» con registro 1C, máscara 18 y valor nuevo 10 (±8 g): se lee
0x00y se escribe0x10. Repetí la ráfaga de 14 bytes: como la aceleración física no cambió pero la sensibilidad sí, las cuentas de Z pasan a0x10 0x00(4096) y las de X a0xFF 0x38(−200). La interpretación del laboratorio usa la nueva sensibilidad (4096 cuentas por g) y sigue dando 1,000 g en Z. - 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.
- 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:
- Buscar la dirección y sus variantes por pines. Anotarla en 7 bits.
- Hacer un barrido. ¿Aparece la dirección? Si no, arreglar eso antes de seguir.
- Leer el registro de identificación. ¿Da el valor de la hoja? Si no, es otro dispositivo o hay algo mal con la lectura.
- 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.
- Seguir la secuencia de arranque (salir del reposo, configurar rango y velocidad, esperar el tiempo indicado). Verificar cada escritura leyéndola de vuelta.
- 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).
- Agregar control de errores: revisar los códigos de Wire y la cantidad de bytes recibidos, con mensajes que digan qué falló.
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| El barrido encuentra el dispositivo pero el WHO_AM_I da 0x00 o 0xFF | Se leyó sin fijar bien el puntero, o el dispositivo está dormido | Revisar endTransmission(false) y el START repetido; mirar la captura |
| Todos los datos valen 0 | El sensor sigue en reposo o sin configurar | Salir del modo de reposo y esperar el tiempo de arranque |
| Los datos cambian con valores absurdos o saltos | Orden de bytes invertido, falta el signo o lecturas no coherentes | Revisar la combinación y usar ráfaga |
| El valor es correcto pero su escala no | Se cambió el rango y no la sensibilidad | Calcular la escala a partir del registro de configuración |
| Una configuración se pierde al cambiar otra | Se escribió el registro completo en lugar de modificar bits | Usar leer–modificar–escribir |
| La lectura devuelve menos bytes de los pedidos | Se superó el buffer, el dispositivo dejó de contestar o se cortó el mensaje | Partir la lectura; mirar el resultado de requestFrom |
| Funciona unos minutos y deja de responder | Un bus que se bloquea (tema 19) o alimentación inestable | Revisar 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 esS 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.