← Índice del curso

Comunicación entre Dispositivos · Tema 15 · Taller

Taller: leer un conversor o una memoria por SPI

Cierre del bloque de SPI con un proyecto completo: un microcontrolador lee un conversor analógico-digital MCP3008 y una memoria flash W25Q en el mismo bus. Vamos a armar las transacciones byte a byte, decodificar el resultado, programar y borrar la flash respetando su protocolo, probar el enlace por etapas y reconocer las fallas típicas con solo mirar los bytes que llegan.

01 · Objetivo

Qué vamos a construir

En los tres temas anteriores estudiamos SPI por partes: las cuatro líneas y el anillo de registros, los modos y la frecuencia, el bus con varios esclavos. En este taller las juntamos. Un microcontrolador va a hablar con dos periféricos que comparten SCLK, MOSI y MISO, cada uno con su propio CS:

  • Un MCP3008, conversor analógico-digital de 10 bits y 8 canales, para leer un potenciómetro o un sensor de temperatura. Su transacción es de tres bytes y no tiene registros.
  • Una memoria flash W25Q, para leer su identificación, leer datos, programarlos y borrarlos. Tiene un protocolo de comandos con reglas propias: habilitar la escritura, esperar al bit de ocupado y no cruzar páginas.

Los dos dispositivos son opuestos a propósito. El conversor es el caso simple (pedir y recibir una muestra). La flash es el caso con estado: una escritura no es instantánea, hay que habilitarla, y programar solo puede pasar bits de 1 a 0. Entre los dos aparecen casi todas las situaciones que se encuentran con dispositivos SPI reales.

02 · Materiales y cableado

Materiales y cableado

Hace falta lo siguiente (el código funciona con cualquier placa que tenga SPI de hardware):

  • Una placa con SPI: el ejemplo usa un ESP32 a 3,3 V. Con un Arduino Uno (5 V) hay que adaptar los niveles hacia la flash (tema 5), que es un dispositivo de 3,3 V.
  • Un MCP3008 y un W25Q (por ejemplo W25Q32 o W25Q128), en módulo o en encapsulado DIP/SOIC con adaptador.
  • Un potenciómetro de 10 kΩ como fuente de tensión variable. Para medir temperatura, un TMP36 (funciona desde 2,7 V; el LM35 necesita al menos 4 V de alimentación y no sirve en una placa de 3,3 V).
  • Protoboard, cables cortos y, si es posible, un analizador lógico de 8 canales.
Cableado del taller Las líneas SCLK, MOSI y MISO del microcontrolador se derivan a un conversor MCP3008 y a una memoria flash. Cada uno tiene además su propia línea CS. El conversor tiene su canal cero conectado al cursor de un potenciómetro. Microcontrolador3,3 V MCP3008conversor 10 bits W25Qmemoria flash SCLKMOSI (DIN / DI)MISO (DOUT / DO) CS del conversorCS de la flash CH0 ← potenciómetro VDD y VREF a 3,3 V · masa común · MOSI con MOSI, MISO con MISO (no se cruzan)

Tres puntos del cableado. Primero, en SPI no se cruzan las líneas, como sí se hacía con la UART: los nombres coinciden entre maestro y esclavo (el módulo puede llamar DIN/DOUT o DI/DO a lo que para el maestro es MOSI y MISO). Segundo, el MCP3008 usa VREF como fondo de escala: conectado a 3,3 V, la entrada de 0 a 3,3 V se convierte en 0 a 1023. Tercero, los CS tienen resistencias pull-up de unos 10 kΩ a 3,3 V, para que ambos dispositivos arranquen deseleccionados.

Con la placa a 3,3 V no hay que adaptar niveles. Con un Uno a 5 V, SCLK, MOSI y los dos CS deben pasar por un divisor o un buffer 74LVC antes de entrar a la flash (el MCP3008 puede alimentarse a 5 V; la salida de 3,3 V de la flash llega al umbral de entrada de un microcontrolador de 5 V con poco margen, como vimos en el tema 5).

03 · Parte A

La transacción del conversor

El MCP3008 no tiene registros ni comandos de lectura y escritura. Cada transacción pide un canal y devuelve una muestra, en tres bytes con CS en bajo todo el tiempo:

Los tres bytes de una conversión del MCP3008.
ByteMaestro envía (MOSI)Maestro recibe (MISO)Qué significa
10x01sin significadoBit de inicio: el conversor espera ver un 1 después de los ceros previos
20x80 | (canal << 4)bits 1 y 0 = B9 y B8 del resultadoEl bit 7 elige entrada simple; los bits 6–4, el canal. Mientras se envían, el conversor muestrea y empieza a devolver el resultado
30x00B7 a B0 del resultadoByte de relleno: sirve para «prestar» los pulsos con los que llegan los 8 bits bajos

El resultado de 10 bits queda repartido entre los dos últimos bytes recibidos, y se arma enmascarando los bits que no pertenecen al dato:

cuenta = ((r2 & 0x03) << 8) | r3  Ventrada = cuenta / 1023 × VREF

Es el caso, mencionado en el tema 12, de un esclavo que responde mientras todavía recibe el comando: el dato no empieza en un límite de byte. Por eso el segundo byte recibido trae solo dos bits útiles y hay que enmascarar.

Algunos cálculos que conviene tener presentes:

  • Resolución. Con VREF = 3,3 V, un paso vale 3,3 / 1023 ≈ 3,23 mV. Con un TMP36 (10 mV/°C), eso son unos 0,32 °C por cuenta.
  • Tiempo. Son 24 pulsos: a 1 MHz una lectura tarda 24 µs, es decir, unas 42 000 muestras por segundo como máximo (sin contar el tiempo de CS ni el procesamiento).
  • Frecuencia de reloj. El MCP3008 admite hasta 3,6 MHz con 5 V, pero solo unos 1,35 MHz con 2,7 V: la conversión interna necesita tiempo. A 3,3 V conviene usar 1 MHz. Pasarse produce errores en los bits bajos, difíciles de ver.
  • Saturación. Si la entrada iguala o supera VREF, el resultado es 1023 sin más información.

04 · Parte B

La transacción de la flash

La flash W25Q es un dispositivo con comandos de un byte, direcciones de tres bytes y estado interno. Los comandos que usaremos:

Comandos de la flash que se usan en el taller.
OperaciónBytes enviadosQué devuelve o hace
Identificación0x9F + 3 de rellenoTres bytes: 0xEF (fabricante), 0x40 (tipo), 0x18 (capacidad de un W25Q128)
Leer datos0x03 + dirección (3 bytes) + n de rellenon bytes consecutivos; la dirección avanza sola
Habilitar escritura0x06Pone el bit WEL en 1; se consume con la operación siguiente
Programar0x02 + dirección + 1 a 256 datosEscribe dentro de una página de 256 bytes; la flash queda ocupada unos 0,4 ms
Borrar sector0x20 + direcciónPone en 0xFF un sector de 4 KB; la flash queda ocupada decenas de ms
Leer estado0x05 + rellenoBit 0 = BUSY (ocupada), bit 1 = WEL (escritura habilitada)

Cuatro reglas hacen que la flash sea distinta de una memoria común:

  1. Se escribe con AND. Programar solo puede pasar bits de 1 a 0. Una posición borrada vale 0xFF; si se programa 0x3C sobre una posición que ya vale 0x0F, el resultado es 0x3C & 0x0F = 0x0C, no 0x3C. Para volver a poner unos hay que borrar, y se borra un sector entero.
  2. Antes de cada programación o borrado hay que enviar 0x06, en una transacción propia. La habilitación se pierde apenas termina la operación; sin ella, el comando se ignora sin aviso.
  3. La operación empieza cuando CS sube. El comando completo (comando, dirección, datos) tiene que estar dentro de una única transacción con CS bajo; al subir CS, la flash comienza a escribir.
  4. Mientras está ocupada solo atiende la lectura de estado. Cualquier otro comando se ignora. Hay que consultar el bit BUSY en un bucle hasta que valga 0 antes de seguir.

Y una limitación de la programación: los datos deben caber en una página de 256 bytes. Si la escritura llega al final de la página, la dirección no pasa a la página siguiente sino que vuelve al principio de la misma y pisa lo que había (con AND). Un programa que guarda más de 256 bytes tiene que partir los datos en trozos que respeten el límite de cada página.

05 · Poner a andar el enlace

Plan de pruebas

Como en el taller de UART, el enlace se prueba por etapas, de lo más simple a lo más completo. Cada etapa descarta un grupo de causas.

Plan de pruebas del taller, en orden.
EtapaCómo se haceResultado esperadoSi falla, sospechá de…
1. LazoUnir MOSI con MISO del maestro y enviar SPI.transfer(0x5A)Devuelve 0x5ACódigo o periférico SPI de la placa
2. Alimentación y CSCon un multímetro: 3,3 V en los VDD y CS en alto (3,3 V) en reposoTensiones correctasAlimentación, pull-ups, cableado de CS
3. Conversor: extremosLeer el canal 0 con el cursor del potenciómetro a masa y luego a 3,3 VCerca de 0 y 1023MISO, canal, VREF, masa común
4. Conversor: intermedioMover el cursor y comparar con un multímetrocuenta ≈ V / 3,3 × 1023Referencia, ruido, frecuencia de reloj
5. Flash: identificaciónEnviar 0x9F y leer tres bytes0xEF 0x40 0x18CS, modo, orden de bits, alimentación de la flash
6. Flash: lecturaLeer 16 bytes desde la dirección 0 de una flash nuevaTodos 0xFF (borrada)Dirección, comando 0x03
7. Flash: escrituraBorrar el sector, programar «HOLA», leer y compararLa lectura devuelve «HOLA»Falta de 0x06, no esperar BUSY, CS subido antes
8. FrecuenciaSubir de 1 MHz hasta el máximo del cableado repitiendo las pruebas 5 y 7Todo correcto hasta el límiteCables largos, protoboard, adaptadores de nivel

Una regla práctica: cada etapa se supera antes de pasar a la siguiente. Si se prueba todo junto y falla, no se sabe si el problema es el cableado de uno, el modo del otro o el programa.

06 · Con el microcontrolador

El código completo

El programa contiene las funciones de bajo nivel de cada dispositivo (cada una abre su propia transacción con sus ajustes) y una rutina de prueba que verifica la flash escribiendo y leyendo. Los pines de SPI por defecto del ESP32 son SCK 18, MISO 19 y MOSI 23; en un Uno, 13, 12 y 11.

#include <SPI.h>

const byte CS_ADC = 5;
const byte CS_FLASH = 17;
const SPISettings AJ_ADC(1000000, MSBFIRST, SPI_MODE0);     // 3,3 V: máximo del MCP3008 ~1,35 MHz
const SPISettings AJ_FLASH(4000000, MSBFIRST, SPI_MODE0);   // protoboard: hasta unos 4 MHz

// ---------- Conversor MCP3008 ----------
uint16_t leerAdc(byte canal) {
    SPI.beginTransaction(AJ_ADC);
    digitalWrite(CS_ADC, LOW);
    SPI.transfer(0x01);                                    // bit de inicio
    byte alto = SPI.transfer(0x80 | (canal << 4));        // entrada simple + canal; llegan B9 y B8
    byte bajo = SPI.transfer(0x00);                        // relleno; llegan B7..B0
    digitalWrite(CS_ADC, HIGH);
    SPI.endTransaction();
    return ((alto & 0x03) << 8) | bajo;
}

float leerTension(byte canal, byte promedio = 16) {
    uint32_t suma = 0;
    for (byte i = 0; i < promedio; i++) suma += leerAdc(canal);
    return suma / (float)promedio / 1023.0 * 3.3;
}

// ---------- Flash W25Q ----------
void flashComandoSolo(byte cmd) {                          // comandos sin dirección ni datos (WREN)
    SPI.beginTransaction(AJ_FLASH);
    digitalWrite(CS_FLASH, LOW);
    SPI.transfer(cmd);
    digitalWrite(CS_FLASH, HIGH);                          // la operación se ejecuta al subir CS
    SPI.endTransaction();
}

void enviarDireccion(uint32_t dir) {
    SPI.transfer((dir >> 16) & 0xFF);
    SPI.transfer((dir >> 8) & 0xFF);
    SPI.transfer(dir & 0xFF);
}

uint32_t flashId() {
    SPI.beginTransaction(AJ_FLASH);
    digitalWrite(CS_FLASH, LOW);
    SPI.transfer(0x9F);
    uint32_t id = 0;
    for (byte i = 0; i < 3; i++) id = (id << 8) | SPI.transfer(0x00);
    digitalWrite(CS_FLASH, HIGH);
    SPI.endTransaction();
    return id;                                             // 0xEF4018
}

byte flashEstado() {
    SPI.beginTransaction(AJ_FLASH);
    digitalWrite(CS_FLASH, LOW);
    SPI.transfer(0x05);
    byte e = SPI.transfer(0x00);
    digitalWrite(CS_FLASH, HIGH);
    SPI.endTransaction();
    return e;
}

bool flashEsperar(unsigned long limiteMs) {                // consultar BUSY hasta que baje, con límite de tiempo
    unsigned long t0 = millis();
    while (flashEstado() & 0x01) {
        if (millis() - t0 > limiteMs) return false;
    }
    return true;
}

void flashLeer(uint32_t dir, byte *destino, uint16_t n) {
    SPI.beginTransaction(AJ_FLASH);
    digitalWrite(CS_FLASH, LOW);
    SPI.transfer(0x03);
    enviarDireccion(dir);
    for (uint16_t i = 0; i < n; i++) destino[i] = SPI.transfer(0x00);
    digitalWrite(CS_FLASH, HIGH);
    SPI.endTransaction();
}

bool flashBorrarSector(uint32_t dir) {
    flashComandoSolo(0x06);                                // WREN antes de cada borrado
    SPI.beginTransaction(AJ_FLASH);
    digitalWrite(CS_FLASH, LOW);
    SPI.transfer(0x20);
    enviarDireccion(dir);
    digitalWrite(CS_FLASH, HIGH);                          // aquí empieza el borrado
    SPI.endTransaction();
    return flashEsperar(500);
}

bool flashProgramar(uint32_t dir, const byte *datos, uint16_t n) {
    while (n > 0) {
        uint16_t espacio = 256 - (dir & 0xFF);             // lo que queda hasta el final de la página
        uint16_t trozo = (n < espacio) ? n : espacio;
        flashComandoSolo(0x06);                            // WREN antes de cada programación
        SPI.beginTransaction(AJ_FLASH);
        digitalWrite(CS_FLASH, LOW);
        SPI.transfer(0x02);
        enviarDireccion(dir);
        for (uint16_t i = 0; i < trozo; i++) SPI.transfer(datos[i]);
        digitalWrite(CS_FLASH, HIGH);                      // empieza a escribir al subir CS
        SPI.endTransaction();
        if (!flashEsperar(50)) return false;
        dir += trozo; datos += trozo; n -= trozo;
    }
    return true;
}

void setup() {
    Serial.begin(115200);
    pinMode(CS_ADC, OUTPUT);   digitalWrite(CS_ADC, HIGH);
    pinMode(CS_FLASH, OUTPUT); digitalWrite(CS_FLASH, HIGH);
    SPI.begin();

    uint32_t id = flashId();
    Serial.print("ID de la flash: 0x"); Serial.println(id, HEX);
    if (id != 0xEF4018) { Serial.println("ID inesperado: revisar cableado, modo y frecuencia"); return; }

    const byte texto[] = "HOLA";
    byte leido[4];
    if (!flashBorrarSector(0)) { Serial.println("Borrado: timeout"); return; }
    if (!flashProgramar(0, texto, 4)) { Serial.println("Programacion: timeout"); return; }
    flashLeer(0, leido, 4);
    Serial.println(memcmp(texto, leido, 4) == 0 ? "Escritura verificada: OK" : "Escritura verificada: ERROR");
}

void loop() {
    Serial.print("Canal 0: ");
    Serial.print(leerTension(0), 3);
    Serial.println(" V");
    delay(500);
}

Cuatro decisiones del código. Cada función abre y cierra su transacción con los ajustes de su dispositivo, así el conversor a 1 MHz y la flash a 4 MHz conviven en el mismo bus (tema 14). flashEsperar consulta el estado con un límite de tiempo: si la flash nunca termina (porque está mal conectada), el programa informa en lugar de colgarse. flashProgramar parte los datos en páginas para que ninguna escritura cruce el límite de 256 bytes. Y la rutina de setup no escribe nada si la identificación no es la esperada.

La función leerTension promedia 16 lecturas: el ruido de una conversión de 10 bits es de uno o dos pasos, y el promedio lo reduce unas cuatro veces, a costa de 16 × 24 µs ≈ 0,4 ms por medición.

07 · Ver lo que pasa en el cable

Verificar con el analizador

Si algo falla, un analizador lógico conectado a SCLK, MOSI, MISO y los dos CS resuelve casi todas las dudas. Con el decodificador de SPI del programa del analizador se ven los bytes en hexadecimal. Lo que hay que comprobar en cada transacción:

Qué mostrar una captura correcta de cada transacción (reloj de 1 MHz para el conversor y de 4 MHz para la flash).
TransacciónCSPulsos de SCLKMOSIMISODuración
Conversor, canal 0conversor bajo, flash alto2401 80 00xx 0N NN (N = dato)24 µs
Identificación de la flashflash bajo, conversor alto329F 00 00 00xx EF 40 188 µs
Habilitar escrituraflash bajo806—2 µs
Programar 4 bytesflash bajo6402 00 00 00 48 4F 4C 41—16 µs
Leer estado (ocupada)flash bajo1605 00xx 03 (BUSY y WEL)4 µs

Cinco cosas para revisar en la captura, en este orden: 1) que CS baje antes del primer pulso y suba después del último; 2) que nunca haya dos CS en bajo a la vez; 3) que la cantidad de pulsos sea un múltiplo de 8 y coincida con el esperado; 4) que MOSI lleve los bytes que se pretendían enviar; 5) que MISO cambie y no quede quieto en 0 o en 1. Con eso se separan los problemas de software (comandos mal armados), de cableado (MISO quieto) y de configuración (bytes desplazados o espejados).

En la identificación, MISO suele valer 0xFF o 0x00 durante el primer byte: es el byte «sin significado» mientras la flash recibe el comando. No es un error.

08 · Analogía con programación

El conversor y la flash en Python

Antes de comprobar el hardware conviene tener un modelo en software de los dos dispositivos. Sirve para entender sus reglas, para probar el código sin la placa y para escribir las pruebas de las secciones anteriores. El programa siguiente implementa el conversor y una flash de 4 KB con los mismos comandos del taller, incluyendo el AND de la programación, la habilitación de escritura, el estado ocupado y la vuelta de página.

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

VREF = 5.0


def leer_adc(canal, tension):
    """Transacción de tres bytes de un MCP3008 (10 bits, referencia de 5 V). Devuelve (enviados, recibidos)."""
    cuenta = max(0, min(1023, round(tension / VREF * 1023)))
    enviados = [0x01, 0x80 | (canal << 4), 0x00]
    recibidos = [0xFF, (cuenta >> 8) & 0x03, cuenta & 0xFF]
    return enviados, recibidos


def decodificar(recibidos):
    return ((recibidos[1] & 3) << 8) | recibidos[2]


class Flash:
    """Flash de práctica de 4 KB: un sector. Borrada = 0xFF; programar solo puede pasar bits de 1 a 0."""
    ID = [0xEF, 0x40, 0x18]

    def __init__(self):
        self.mem = bytearray([0xFF]) * 4096
        self.mem[0:10] = b"TALLER SPI"
        self.wel = False
        self.restantes = 0                            # consultas de estado hasta terminar

    def estado(self):
        return (1 if self.restantes else 0) | (2 if self.wel else 0)

    def transaccion(self, env):
        n, cmd = len(env), env[0]
        rec = [0xFF] * n
        if self.restantes and cmd != 0x05:
            return rec                                # ocupada: solo atiende la lectura de estado
        if cmd == 0x9F:
            rec[1:4] = self.ID
        elif cmd == 0x06:
            self.wel = True
        elif cmd == 0x05:
            for i in range(1, n):
                rec[i] = self.estado()
                if self.restantes:
                    self.restantes -= 1
                    if self.restantes == 0:
                        self.wel = False              # al terminar, la escritura queda deshabilitada de nuevo
        elif cmd == 0x03:
            direccion = (env[1] << 16 | env[2] << 8 | env[3]) % 4096
            for i in range(4, n):
                rec[i] = self.mem[(direccion + i - 4) % 4096]
        elif cmd == 0x02 and self.wel:
            direccion = env[1] << 16 | env[2] << 8 | env[3]
            pagina = direccion & ~0xFF
            for i, dato in enumerate(env[4:]):
                destino = pagina | ((direccion + i) & 0xFF)    # al llegar al final de la página, vuelve al inicio
                self.mem[destino % 4096] &= dato
            self.restantes = 2
        elif cmd == 0x20 and self.wel:
            self.mem[:] = bytes([0xFF]) * 4096
            self.restantes = 5
        return rec


def esperar(flash):
    consultas = 0
    while flash.transaccion([0x05, 0x00])[1] & 1:
        consultas += 1
    return consultas + 1


env, rec = leer_adc(0, 1.65)
print(f"ADC canal 0 con 1,65 V: enviados {[f'{b:#04x}' for b in env]}, recibidos {[f'{b:#04x}' for b in rec]} -> {decodificar(rec)}")
cuenta = decodificar(leer_adc(1, 0.25)[1])
print(f"LM35 con 0,25 V: cuenta {cuenta} -> {cuenta / 1023 * VREF * 100:.1f} °C (un paso = {VREF / 1023 * 100:.2f} °C)")
print()

f = Flash()
print("ID:", [f"{b:#04x}" for b in f.transaccion([0x9F, 0, 0, 0])[1:]])
print("Lectura inicial:", bytes(f.transaccion([0x03, 0, 0, 0] + [0] * 10)[4:]))
f.transaccion([0x02, 0, 0, 0, ord("A")])
print("Programar sin 0x06 (ignorado):", chr(f.transaccion([0x03, 0, 0, 0, 0])[4]))
f.transaccion([0x06])
f.transaccion([0x02, 0, 0, 0, ord("A")])
print(f"Programar 'A' sobre 'T' tras 0x06: {f.mem[0]:#04x} = {chr(f.mem[0])!r} (0x54 & 0x41); consultas de estado: {esperar(f)}")
f.transaccion([0x06])
f.transaccion([0x20, 0, 0, 0])
print(f"Borrar el sector: primer byte {f.mem[0]:#04x}; consultas de estado: {esperar(f)}")
f.transaccion([0x06])
f.transaccion([0x02, 0, 0, 0, ord("A")])
esperar(f)
print("Programar 'A' sobre borrado:", chr(f.mem[0]))
f.transaccion([0x06])
f.transaccion([0x02, 0, 0, 0xFE, 0x31, 0x32, 0x33, 0x34])
esperar(f)
print("Programar 4 bytes desde 0x0FE (cruza la página):", [f"{f.mem[i]:#04x}" for i in (0xFE, 0xFF, 0x00, 0x01)])

Resultado: con 1,65 V en el canal 0 el conversor recibe [0xff, 0x01, 0x52] y la cuenta es 338 (con una referencia de 5 V). Con 0,25 V la cuenta es 51, lo que equivale a 24,9 °C con un sensor de 10 mV/°C, con pasos de 0,49 °C. La flash devuelve la identificación 0xef 0x40 0x18 y su contenido inicial, «TALLER SPI». Programar sin habilitar la escritura no tiene efecto (sigue la «T»). Con 0x06, programar «A» sobre «T» no da «A» sino 0x40 ('@'), porque 0x54 AND 0x41 = 0x40, y toma 3 consultas de estado. Hay que borrar el sector (6 consultas) para volver a tener 0xFF, y recién entonces «A» se escribe bien. La última línea muestra la vuelta de página: al programar cuatro bytes desde 0x0FE, los dos últimos no pasan a la página siguiente sino que caen en 0x00 y 0x01. Como en 0x00 había una «A» (0x41), el resultado es 0x41 AND 0x33 = 0x01, no 0x33.

Las pruebas automáticas de un controlador de flash se escriben contra un modelo como este, y se repiten después con el hardware real: si coinciden, el enlace y la lógica son correctos.

09 · Exploración interactiva

Laboratorio: conversor, flash y fallas

El laboratorio tiene tres herramientas. La primera es un conversor MCP3008 virtual: elegís el canal, la tensión, la alimentación y el reloj, y ves los bytes de la transacción, la cuenta y el veredicto. La segunda es una flash virtual de 4 KB: ejecutás los comandos del taller y mirás la memoria cambiar. La tercera es un catálogo de fallas: elegís una falla del enlace y ves qué bytes devuelve la lectura de identificación.

1. Conversor MCP3008 virtual

MOSI (maestro envía)—
MISO (maestro recibe)—
Cuenta decodificada—
Tensión calculada—
Equivalente con un sensor de 10 mV/°C—
Duración · velocidad de muestreo—
Veredicto—

Elegí los parámetros y presioná «Leer».

2. Flash virtual de 4 KB

Escritura habilitada (WEL)—
Estado de la flash—
Bytes de la transacción—
Tiempo a 4 MHz—
Desde 0x000—
ASCII—
Bytes de la última transacción, con CS en bajo.
ByteMOSI (maestro envía)MISO (maestro recibe)Qué ocurre
Todavía no se ejecutó ninguna transacción.

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

3. Catálogo de fallas del enlace

MOSI (maestro envía)—
MISO (maestro recibe; el primer byte no cuenta)—
Diagnóstico—

Elegí una falla para ver su patrón.

En la herramienta 1, la duración cuenta solo los 24 pulsos; la velocidad de muestreo máxima real depende también del tiempo entre transacciones. En la 2, los tiempos internos (0,4 ms para programar, 45 ms para borrar) son típicos de la familia, y el modelo usa 2 y 5 consultas de estado para representarlos. En la 3, los bytes resaltados difieren de los esperados (0xEF 0x40 0x18).

Recorrido guiado

  1. Con los valores iniciales (canal 0, 1,65 V, 3,3 V, 1 MHz) presioná «Leer». Se envían 0x01 0x80 0x00 y llegan 0xFF 0x02 0x00: la cuenta es 512 (la mitad de 1023) y la tensión calculada, 1,652 V. La lectura dura 24 µs, unas 42 000 muestras por segundo.
  2. Cambiá al canal 5: el segundo byte enviado pasa a 0xD0 (0x80 | 5 << 4). Cambiá la alimentación a 5 V con 1,65 V: la cuenta es 338 y el segundo byte recibido, 0x01 (con 3,3 V eran 512).
  3. Con 3,3 V, llevá la tensión a 3,3 V o más: la cuenta es 1023 y el veredicto «Saturado»: no se puede saber cuánto vale la señal por encima de la referencia.
  4. Con 3,3 V y 1,65 V, subí el reloj a 2 MHz: «Reloj excesivo». Con 5 V el mismo reloj es válido, hasta 3,6 MHz.
  5. En la herramienta 2, ejecutá la identificación: 4 bytes enviados (0x9F y tres de relleno), y llegan 0xEF, 0x40 y 0x18. Dura 8,00 µs a 4 MHz.
  6. Leé 8 bytes desde la dirección 0: 54 41 4C 4C 45 52 20 53, que en ASCII es «TALLER S». Mirá el volcado debajo.
  7. Programá «A» en la dirección 0 sin habilitar la escritura: «ignorado», y el volcado no cambia. Ahora ejecutá «Habilitar escritura» (WEL pasa a «sí») y programá «A» otra vez: la posición 0 queda en 0x40 («@»), no en «A», porque 0x54 AND 0x41 = 0x40. La flash aparece ocupada.
  8. Intentá leer los datos con la flash ocupada: la transacción es ignorada. Presioná «Esperar a que termine»: tres consultas de estado, las dos primeras con 0x03 (ocupada y escritura habilitada) y la última 0x00. La escritura queda deshabilitada.
  9. Habilitá la escritura y borrá el sector: espera de 6 consultas, y todo el volcado queda en FF. Habilitá otra vez y programá «HOLA»: ahora sí se lee «HOLA» (no se puede reutilizar la habilitación anterior).
  10. Probá la vuelta de página: habilitá la escritura y programá «ABCDEFGH» en la dirección 252. Esperá, poné la dirección en 0 y leé 8 bytes: en la posición 0 al 3 hay «@FD@» en lugar de «HOLA», porque los cuatro últimos caracteres volvieron al principio de la página y se combinaron con AND con lo que había.
  11. En la herramienta 3, recorré las fallas. «CS que no baja» y «MISO sin conectar» dan 0xFF en todos los bytes; «MISO a masa» da ceros; «Modo SPI equivocado» da 0xDE 0x80 0x30 (cada byte corrido); «Orden de bits», 0xF7 0x02 0x18; «MOSI y MISO cruzados» devuelve 0x9F 0x00 0x00 0x00, el eco de lo enviado.
  12. Con «Frecuencia demasiado alta», presioná el botón varias veces: el resultado cambia de una lectura a otra. Un enlace que a veces anda es más difícil de diagnosticar que uno que nunca anda.
Desafío: guardar una cadena larga

Querés guardar 300 bytes empezando en la dirección 0x0F0. ¿Cuántas operaciones de programación hacen falta, y con cuántos datos cada una? La primera página tiene lugar para 256 − 0xF0 = 16 bytes (hasta la dirección 0xFF); la segunda, para 256; la tercera para los 28 restantes. Son tres operaciones: 16, 256 y 28 bytes, cada una precedida por su 0x06 y seguida de la espera de BUSY. Es exactamente lo que hace flashProgramar con su bucle.

10 · Qué viene después

Ampliaciones y buenas prácticas

El taller deja un enlace funcional con dos dispositivos reales. Se puede extender en varias direcciones:

  • Calibración del conversor. Medir con un multímetro la tensión real de VREF y usarla en la fórmula; leer dos puntos conocidos y corregir ganancia y desfase.
  • Más lecturas y filtrado. Promediar, descartar los valores extremos o usar un filtro de media móvil. Para varios canales, leerlos en secuencia dejando un respiro entre uno y otro.
  • Registro de datos en la flash. Guardar una muestra por segundo en páginas consecutivas, con un puntero que avance y sobreviva a un reinicio (se busca la primera posición en 0xFF). Para no gastar siempre el mismo sector, se reparte la escritura entre varios (nivelación de desgaste), ya que cada sector soporta unas 100 000 operaciones de borrado.
  • Integridad de los datos. Agregar un CRC a cada registro (temas 43 a 45) para detectar corrupción por un corte de energía a mitad de una escritura.
  • Transferencias grandes. Leer o escribir con SPI.transfer(buffer, n) o con DMA en vez de bucles byte a byte, para llenar una pantalla o leer un bloque entero a la velocidad del bus.
  • Una tarjeta SD en el mismo bus, siguiendo la secuencia de arranque del tema 14 y las reglas para compartir MISO.

Y tres hábitos que valen para cualquier dispositivo SPI: leer primero un registro de identificación, bajar la frecuencia hasta que todo funcione y subirla de a poco, y verificar cada escritura leyéndola de vuelta.

11 · Comprensión

Confusiones frecuentes

«Si programo un byte, queda exactamente como lo envié»

Solo si la posición estaba borrada (0xFF). La programación hace un AND con el contenido anterior: 0x3C sobre 0x0F da 0x0C. Para reescribir hay que borrar el sector entero antes.

«Con un solo 0x06 puedo programar todo lo que quiera»

La habilitación se consume en cada operación de programar o borrar. Cada página programada necesita su propio 0x06, en una transacción aparte.

«Subir CS a mitad de la escritura no importa»

La flash empieza a escribir apenas CS sube. Si se sube antes de enviar todos los datos, solo se programa lo que llegó, y si se sube antes de enviar el comando completo, la operación se descarta. Todo el comando va en una sola transacción.

«Después de escribir puedo leer enseguida»

Durante la escritura la flash solo atiende la lectura de estado y los otros comandos se ignoran sin aviso. La lectura da basura o 0xFF. Hay que esperar a que BUSY baje, con un límite de tiempo por si la flash no responde.

«El conversor devuelve el dato en el tercer byte»

Devuelve 10 bits repartidos entre el segundo y el tercer byte: dos bits en el segundo y ocho en el tercero. Si se ignora el segundo, el resultado queda limitado a 0–255; si no se enmascara, se mezcla con bits sin significado.

«Todos los 0xFF significan que el dispositivo está vacío»

Una lectura de 0xFF puede ser una flash borrada (correcto) o un CS caído y un MISO sin conectar (falla). Por eso se verifica la identificación antes de interpretar los datos.

«Cruzo MOSI y MISO como TX y RX»

En SPI las líneas se conectan con el mismo nombre: MOSI del maestro con la entrada de datos del esclavo, MISO del maestro con su salida. Cruzarlas hace que el maestro lea el eco de lo que envió.

12 · Práctica guiada

Predecí antes de comprobar

1. Armar la transacción

Querés leer el canal 5 del MCP3008. ¿Qué tres bytes enviás? Si recibís 0xFF 0x02 0xAB, ¿cuál es la cuenta y qué tensión es con una referencia de 3,3 V?

Ver solución

Enviás 0x01, 0x80 | (5 << 4) = 0xD0 y 0x00. La cuenta es ((0x02 & 3) << 8) | 0xAB = 512 + 171 = 683. La tensión es 683 / 1023 × 3,3 ≈ 2,20 V.

2. Resolución con un sensor

Con VREF = 3,3 V y un TMP36 (10 mV/°C), ¿cuánto vale un paso del conversor en tensión y en grados? ¿Se pueden distinguir 25,0 °C de 25,2 °C?

Ver solución

Un paso: 3,3 V / 1023 ≈ 3,23 mV, es decir, 0,32 °C. Una diferencia de 0,2 °C es menor que un paso: dos lecturas consecutivas pueden dar la misma cuenta. Para ver diferencias más finas hay que promediar o usar una referencia menor.

3. El AND de la programación

Una posición vale 0x0F. Se programa 0x3C sobre ella. ¿Cuánto vale después? ¿Qué hay que hacer para que valga 0x3C?

Ver solución

0x0F AND 0x3C = 0x0C (00001111 AND 00111100 = 00001100). Para tener 0x3C hay que borrar el sector (queda 0xFF) y recién entonces programar 0x3C.

4. Cruzar el límite de la página

Programás 10 bytes en una sola operación empezando en la dirección 0x0FC (en una flash borrada). ¿Dónde caen?

Ver solución

Los primeros 4 bytes caen en 0x0FC a 0x0FF (el final de la página). Los 6 restantes vuelven al principio de la misma página y caen en 0x000 a 0x005, no en 0x100 en adelante. Se evita partiendo los datos en dos operaciones: 4 bytes en 0x0FC y 6 bytes en 0x100.

5. Tiempo de una lectura

Leer 256 bytes de la flash con el comando 0x03 requiere 1 + 3 + 256 = 260 bytes. ¿Cuánto tarda a 4 MHz? ¿Y a 8 MHz?

Ver solución

260 × 8 = 2080 pulsos. A 4 MHz: 2080 / 4 000 000 = 520 µs. A 8 MHz: 260 µs. El tiempo es inversamente proporcional a la frecuencia, pero el cableado de una protoboard limita a unos 4 MHz.

6. Diagnóstico por los bytes

Al leer la identificación de la flash obtenés, en distintas pruebas: (a) FF FF FF FF; (b) FF DE 80 30; (c) 9F 00 00 00; (d) FF F7 02 18. ¿Qué falla sospechás en cada caso?

Ver solución

(a) La flash no responde: CS que no baja, MISO sin conectar o flash sin alimentación. (b) Modo SPI equivocado: los bytes correctos (EF 40 18) corridos un bit a la izquierda. (c) MOSI y MISO cruzados: el maestro lee el eco de lo que envió. (d) Orden de bits equivocado: EF, 40 y 18 con los bits espejados (F7, 02, 18).

13 · Síntesis

Ideas para recordar

  • Un enlace SPI se construye por capas: cableado sin cruzar (MOSI con MOSI), modo y frecuencia que soporte cada dispositivo, un CS por esclavo y un protocolo de bytes definido por la hoja de datos.
  • El MCP3008 se lee con tres bytes: 0x01, 0x80 | (canal << 4) y 0x00; el resultado de 10 bits se arma con ((r[1] & 3) << 8) | r[2], y el fondo de escala es la referencia.
  • La frecuencia del conversor depende de la alimentación (3,6 MHz con 5 V, 1,35 MHz con 2,7 V); pasarse produce errores en los bits bajos.
  • La flash se programa con AND (solo de 1 a 0), por lo que reescribir exige borrar el sector; cada programación o borrado necesita su propio 0x06.
  • La flash empieza a escribir cuando CS sube y, mientras está ocupada, solo atiende la lectura de estado: se consulta el bit BUSY en un bucle con límite de tiempo.
  • Una programación no puede cruzar el límite de la página de 256 bytes: la dirección vuelve al principio de la misma página, por lo que los datos largos se parten por páginas.
  • Un enlace nuevo se prueba por etapas (lazo, alimentación, conversor en los extremos, identificación de la flash, lectura, escritura, frecuencia) y la primera lectura es siempre un registro de identificación conocido.
  • Los bytes devueltos delatan la falla: todo 0xFF (no responde), todo 0x00 (MISO a masa), bytes corridos (modo), bytes espejados (orden de bits), eco de lo enviado (MOSI y MISO cruzados) y valores que varían (frecuencia excesiva).

¿Listo para el siguiente bloque?

Antes de avanzar, armá los tres bytes para leer el canal 3 del MCP3008 y calculá la tensión de la cuenta 300 con una referencia de 3,3 V; explicá por qué programar 0x55 sobre una posición que vale 0x0F no produce 0x55; y decidí qué falla sospechás si la identificación de una flash llega como FF F7 02 18. Si podés justificar las tres respuestas, tenés el bloque de SPI completo.

En el próximo tema empezamos el bloque de I²C, el bus de dos hilos: SDA y SCL, colector abierto, resistencias pull-up y direcciones en lugar de líneas de selección.