Arduino y Raspberry Pi: microcontrolador contra computador de placa única

Por: Artiko
elixirroboticaiotelectronicaarduinoraspberry-pigpionervesmicrocontroladores

Arduino y Raspberry Pi: microcontrolador contra computador de placa única

En el capítulo 6 recorrimos la generación anterior de control embebido: el IC 555 generando temporizaciones con resistencias y condensadores, el PIC16F877A programado en ensamblador o en C con MPLAB, y el BASIC Stamp interpretando tokens desde una EEPROM externa. Ese recorrido dejó claro el patrón: cada plataforma resolvía un problema de su época, y cada una imponía una barrera de entrada distinta —cálculo de componentes, programadores dedicados, herramientas propietarias.

Arduino y Raspberry Pi son las dos respuestas modernas a esa barrera, y son respuestas de naturaleza radicalmente distinta. Arduino tomó un microcontrolador que ya existía (el ATmega328P de Atmel) y lo envolvió en un bootloader, un conector USB, una biblioteca de funciones legibles y un IDE de un solo botón. Raspberry Pi tomó el camino opuesto: en lugar de simplificar el microcontrolador, puso un computador Linux completo en una placa del tamaño de una tarjeta de crédito y expuso pines de entrada/salida en un conector lateral.

Confundir ambas cosas es el error conceptual más caro de este curso. No son competidoras: son categorías distintas de hardware que resuelven mitades distintas de un sistema. Este capítulo desarma las dos, pin por pin y capa por capa, para que cuando llegues a diseñar un robot o un nodo IoT sepas exactamente cuál va en cada lugar —y por qué muchas veces van las dos juntas.

Las dos categorías: MCU y SBC

Antes de entrar en modelos concretos hay que fijar la distinción que organiza todo el capítulo.

Un microcontrolador (MCU) es un chip único que contiene CPU, memoria de programa, memoria de datos y periféricos de entrada/salida en el mismo silicio. Ejecuta un solo programa, que empieza a correr milisegundos después de aplicar alimentación y no termina nunca. No hay sistema operativo, no hay procesos, no hay sistema de archivos. Tu código es el sistema.

Un computador de placa única (SBC, single-board computer) es un computador completo miniaturizado: SoC con CPU multinúcleo, RAM externa en chips separados, almacenamiento en microSD o eMMC, controladores de USB, red y video. Arranca un kernel Linux, planifica procesos, monta sistemas de archivos y tarda decenas de segundos en estar listo.

graph TB
    subgraph MCU["Microcontrolador — ATmega328P"]
        direction TB
        A1["Núcleo AVR 8 bits<br/>16 MHz"]
        A2["Flash 32 KB<br/>(programa)"]
        A3["SRAM 2 KB<br/>(variables)"]
        A4["EEPROM 1 KB<br/>(persistencia)"]
        A5["Periféricos:<br/>ADC · Timers · UART · SPI · I2C"]
        A1 --- A2
        A1 --- A3
        A1 --- A4
        A1 --- A5
    end

    subgraph SBC["Computador de placa única — Raspberry Pi 5"]
        direction TB
        B1["SoC BCM2712<br/>4x Cortex-A76 2.4 GHz"]
        B2["LPDDR4X externa<br/>4 a 16 GB"]
        B3["microSD / NVMe<br/>rootfs Linux"]
        B4["Southbridge RP1<br/>USB · Ethernet · GPIO"]
        B5["Kernel Linux<br/>procesos · drivers · red"]
        B1 --- B2
        B1 --- B3
        B1 --- B4
        B1 --- B5
    end

    MCU -->|"Determinismo de microsegundos<br/>Arranque instantáneo<br/>Consumo en miliamperios"| R["Sensores, actuadores,<br/>lazos de control"]
    SBC -->|"Red, criptografía, almacenamiento<br/>Concurrencia, visión, bases de datos"| S["Coordinación, interfaz,<br/>nube, decisiones"]

La consecuencia práctica: un microcontrolador puede garantizarte que va a leer un encoder cada 100 microsegundos sin fallar nunca; un Linux de propósito general no puede prometer eso, porque el planificador puede darle la CPU a otra cosa. A cambio, el SBC te da TLS, sistema de archivos, actualizaciones remotas y un runtime como la BEAM de Erlang.

Arduino: qué es exactamente

Arduino es una empresa italiana que diseña y fabrica placas basadas en microcontroladores, junto con el software para programarlas. Todo su ecosistema es de código abierto: los esquemáticos, los archivos de PCB, el IDE y las bibliotecas. Esa apertura es la razón de que existan miles de clones compatibles y de que el formato de conector de la UNO se haya convertido en un estándar de facto para shields.

Lo importante es entender que “Arduino” nombra tres cosas a la vez, y conviene separarlas:

  1. El hardware: placas con un microcontrolador, un regulador de voltaje, un conversor USB-serie y un conector de pines estandarizado.
  2. El framework de software: un conjunto de bibliotecas en C++ (pinMode, digitalWrite, analogRead, Serial) que abstraen los registros del microcontrolador.
  3. El flujo de trabajo: escribir un sketch, compilarlo, y cargarlo por USB gracias a un bootloader que ya viene grabado en el chip.

Puedes usar el framework Arduino en hardware que no es Arduino (ESP32, STM32, RP2040), y puedes programar una placa Arduino sin usar el framework, escribiendo directamente a los registros del AVR. Las tres capas son separables.

La arquitectura interna del ATmega328P

El corazón de la Arduino UNO R3 es un ATmega328P: un microcontrolador AVR de 8 bits con arquitectura Harvard modificada. Harvard significa que la memoria de programa y la memoria de datos están en espacios de direcciones separados, con buses separados. Eso permite que el procesador busque la siguiente instrucción mientras todavía está accediendo a datos de la instrucción actual.

flowchart LR
    subgraph Chip["ATmega328P"]
        direction TB
        PC["Contador de programa"] --> FLASH["Flash 32 KB<br/>instrucciones de 16 bits"]
        FLASH --> IR["Registro de instrucción"]
        IR --> DEC["Decodificador"]
        DEC --> ALU["ALU 8 bits"]
        REG["32 registros<br/>de propósito general<br/>R0 a R31"] <--> ALU
        ALU <--> SRAM["SRAM 2 KB<br/>pila y variables"]
        ALU <--> IO["Registros de E/S<br/>DDRx · PORTx · PINx"]
        IO <--> PINES["Pines físicos<br/>PB0-PB7 · PC0-PC6 · PD0-PD7"]
        ALU <--> EE["EEPROM 1 KB"]
        CLK["Cristal 16 MHz"] --> DEC
        subgraph PERIF["Periféricos con acceso directo a los pines"]
            T0["Timer0 8 bits"]
            T1["Timer1 16 bits"]
            T2["Timer2 8 bits"]
            ADC["ADC 10 bits<br/>6 canales"]
            USART["USART"]
            SPI["SPI"]
            TWI["TWI / I2C"]
        end
        PERIF <--> IO
    end

Los números concretos del ATmega328P:

  • 32 KB de Flash para el programa, de los cuales aproximadamente 0,5 KB están ocupados por el bootloader.
  • 2 KB de SRAM para variables, pila y buffers. Esta es la limitación que más rápido se siente: una cadena larga o un buffer de 512 bytes ya consume una cuarta parte.
  • 1 KB de EEPROM para datos que deben sobrevivir a cortes de energía (calibraciones, contadores, configuración).
  • 16 MHz de reloj, con la mayoría de instrucciones ejecutándose en un solo ciclo. Eso da del orden de 16 millones de operaciones por segundo, sin sistema operativo compitiendo.
  • Lógica de 5 V: un pin en alto entrega 5 V y un pin de entrada espera 0 V o 5 V.

Los tres puertos de E/S (B, C, D) se controlan con tres registros cada uno:

  • DDRx (Data Direction Register): un bit por pin. En 1, el pin es salida; en 0, entrada.
  • PORTx: si el pin es salida, define el nivel. Si es entrada, activa o desactiva el pull-up interno.
  • PINx: leído, devuelve el estado eléctrico real de los pines del puerto.

Cuando escribes pinMode(13, OUTPUT) la biblioteca traduce eso a poner a 1 el bit 5 de DDRB, porque el pin digital 13 de la UNO es físicamente PB5. Esta traducción es la que hace que el mismo sketch funcione en placas con distinta distribución de pines.

El GPIO de Arduino, pin por pin

En una UNO R3 el conector expone:

GrupoPinesCapacidad
DigitalesD0 a D13 (14 pines)Entrada o salida, 5 V
PWMD3, D5, D6, D9, D10, D11Salida analógica simulada de 8 bits
AnalógicosA0 a A5 (6 pines)Entrada al ADC de 10 bits, o E/S digital
Serie (UART)D0 (RX), D1 (TX)Compartidos con el puerto USB
SPID10 (SS), D11 (MOSI), D12 (MISO), D13 (SCK)Bus síncrono de alta velocidad
I2CA4 (SDA), A5 (SCL)Bus de dos hilos, multidispositivo
Interrupciones externasD2, D3Disparo por flanco sin sondeo
Alimentación5V, 3V3, GND, VIN3V3 limitado a unos 150 mA

Los límites eléctricos importan más que la lista de funciones:

  • Cada pin entrega o absorbe cómodamente 20 mA; el máximo absoluto del datasheet es 40 mA, y llegar ahí acorta la vida del chip.
  • El total sumado de todos los pines no debe superar aproximadamente 200 mA, y hay límites por puerto además del global.
  • Un LED con su resistencia consume entre 5 y 15 mA: se puede conectar directo. Un motor, un relé o una tira de LEDs no: necesitan un transistor, un MOSFET o un driver, alimentados desde una fuente aparte.

El PWM merece una nota. analogWrite(pin, 128) no genera 2,5 V: genera una onda cuadrada de 0 y 5 V con 50% de ciclo de trabajo. Un LED lo percibe como brillo medio y un motor como velocidad media porque ambos integran la señal. Un multímetro también lo promedia. Pero si conectas eso a la entrada de un circuito que espera un voltaje real, necesitas un filtro RC o un DAC.

Las frecuencias por defecto de PWM en la UNO son de aproximadamente 490 Hz en los pines gobernados por Timer1 y Timer2 (D3, D9, D10, D11), y de aproximadamente 980 Hz en los pines de Timer0 (D5 y D6). Timer0 también sostiene millis() y delay(), así que cambiarle la frecuencia rompe la medición de tiempo: es una de las trampas clásicas.

La familia Arduino

PlacaMicrocontroladorBits / RelojFlash / SRAMLógicaRasgo distintivo
UNO R3ATmega328P8 bits / 16 MHz32 KB / 2 KB5 VLa referencia del ecosistema
NanoATmega328P8 bits / 16 MHz32 KB / 2 KB5 VMismo chip en formato protoboard
Mega 2560ATmega25608 bits / 16 MHz256 KB / 8 KB5 V54 digitales, 16 analógicos, 4 UART
Leonardo / MicroATmega32u48 bits / 16 MHz32 KB / 2,5 KB5 VUSB nativo: se presenta como teclado o mouse
UNO R4 MinimaRenesas RA4M1 (Cortex-M4)32 bits / 48 MHz256 KB / 32 KB5 VDAC real, ADC de mayor resolución, CAN
UNO R4 WiFiRA4M1 + ESP32-S332 bits / 48 MHz256 KB / 32 KB5 VWi-Fi, Bluetooth y matriz de LEDs integrada
MKR (familia)SAMD21 (Cortex-M0+)32 bits / 48 MHz256 KB / 32 KB3,3 VVariantes con Wi-Fi, LoRa, Sigfox, NB-IoT
Nano 33 BLE SensenRF52840 (Cortex-M4F)32 bits / 64 MHz1 MB / 256 KB3,3 VSensores integrados y Bluetooth LE
Nano RP2040 ConnectRP2040 + NINA-W10232 bits / 133 MHz16 MB / 264 KB3,3 VDoble núcleo y Wi-Fi

Tres lecturas de esta tabla:

El salto de 8 a 32 bits ya ocurrió. Las placas nuevas de Arduino usan núcleos ARM Cortex-M. La UNO R3 sigue viva por inercia del ecosistema y porque su tolerancia a 5 V la hace robusta frente a errores de cableado, no porque su chip sea competitivo.

La familia Nano son placas compactas, varias de ellas con sensores ya montados en la placa: temperatura, humedad, presión barométrica, micrófono, acelerómetro. Sirven para prototipar sin cablear nada.

La familia MKR está pensada para IoT: cada variante trae una radio distinta y todas comparten el mismo formato de placa y de conector de batería LiPo, con circuito de carga incluido.

El entorno de desarrollo de Arduino

El Arduino IDE compila y carga programas para todo ese rango de dispositivos. Es un editor con un compilador cruzado detrás (avr-gcc para las placas AVR, arm-none-eabi-gcc para las ARM) y una herramienta de carga (avrdude para AVR). El flujo completo es este:

sequenceDiagram
    participant Dev as Editor / IDE
    participant GCC as Compilador cruzado
    participant AVRD as avrdude
    participant BOOT as Bootloader en Flash
    participant APP as Aplicación en Flash

    Dev->>GCC: sketch.ino + bibliotecas
    Note over GCC: Se le antepone Arduino.h<br/>y se le añade un main() que llama<br/>a setup() una vez y a loop() siempre
    GCC->>GCC: Compila y enlaza el core de la placa
    GCC->>AVRD: firmware.hex (formato Intel HEX)
    AVRD->>BOOT: Pulso DTR sobre el puerto serie = reset
    Note over BOOT: Tras el reset el bootloader<br/>escucha el puerto un par de segundos
    AVRD->>BOOT: Protocolo STK500, página por página
    BOOT->>APP: Escribe la Flash y verifica
    BOOT->>APP: Salta a la dirección 0x0000
    APP->>APP: setup() y luego loop() infinito

El detalle del pulso DTR explica un comportamiento que confunde a mucha gente: cada vez que abres el monitor serie, la placa se reinicia. No es un fallo, es el mismo mecanismo que usa avrdude para entrar al bootloader.

Un sketch mínimo, completo y funcional:

// blink.ino — parpadeo del LED integrado
#define LED 13

void setup() {
  pinMode(LED, OUTPUT);
}

void loop() {
  digitalWrite(LED, HIGH);
  delay(500);
  digitalWrite(LED, LOW);
  delay(500);
}

Dos funciones, ningún main. El core de Arduino aporta el main real, que llama a setup() una vez y luego entra en un bucle infinito llamando a loop(). Es la forma más simple de un superloop, el patrón de arquitectura dominante en microcontroladores sin sistema operativo.

El problema del delay() es que bloquea: durante esos 500 ms el procesador no puede hacer nada más. La versión no bloqueante del mismo comportamiento, que es la que se usa en cualquier programa real:

// blink_no_bloqueante.ino
#define LED 13
const unsigned long INTERVALO = 500;

unsigned long ultimoCambio = 0;
bool estadoLed = false;

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(115200);
}

void loop() {
  unsigned long ahora = millis();

  if (ahora - ultimoCambio >= INTERVALO) {
    ultimoCambio = ahora;
    estadoLed = !estadoLed;
    digitalWrite(LED, estadoLed ? HIGH : LOW);
  }

  // El loop sigue libre para otras tareas
  if (Serial.available() > 0) {
    char comando = Serial.read();
    if (comando == 'p') {
      Serial.println(estadoLed ? "encendido" : "apagado");
    }
  }
}

La resta ahora - ultimoCambio está escrita así a propósito: millis() desborda a los ~49,7 días, y con aritmética de enteros sin signo la resta sigue dando el intervalo correcto incluso a través del desbordamiento. Comparar ahora >= ultimoCambio + INTERVALO sí se rompe.

PlatformIO: el otro entorno

PlatformIO IDE es la alternativa profesional al IDE oficial. Se instala como extensión de VS Code y aporta lo que el IDE de Arduino no tiene: gestión de dependencias declarativa, múltiples entornos de compilación en un mismo proyecto, integración con depuradores por hardware y estructura de proyecto compatible con control de versiones.

La configuración vive en un archivo platformio.ini en la raíz del proyecto:

; platformio.ini — el mismo código para dos placas distintas
[env:uno]
platform = atmelavr
board = uno
framework = arduino
monitor_speed = 115200
lib_deps =
    adafruit/Adafruit BME280 Library@^2.2.4
    bblanchon/ArduinoJson@^7.0.4

[env:mkrwifi1010]
platform = atmelsam
board = mkrwifi1010
framework = arduino
monitor_speed = 115200
build_flags = -D TIENE_WIFI
lib_deps =
    adafruit/Adafruit BME280 Library@^2.2.4
    bblanchon/ArduinoJson@^7.0.4

Con eso, pio run -e uno compila para la UNO y pio run -e mkrwifi1010 para la MKR, resolviendo las bibliotecas en cada caso. Las dependencias quedan fijadas por versión en el repositorio, no en una carpeta global del usuario.

El ecosistema de Arduino se completa con Arduino IoT Cloud, un servicio para conectar placas a internet, visualizar variables en paneles y compartir proyectos sin escribir el backend. Es útil para prototipos y demostraciones; para un producto propio, en este curso vamos a construir esa capa nosotros con Elixir.

Raspberry Pi: qué es exactamente

Raspberry Pi es un proyecto de la fundación británica del mismo nombre, desarrollado en colaboración con Broadcom, que empezó con un objetivo educativo: poner un computador programable en manos de estudiantes por el precio de un libro de texto. Terminó convirtiéndose en la base de una enorme cantidad de productos industriales, robots y dispositivos IoT.

El catálogo se organiza en cuatro categorías, y mezclarlas es fuente de confusión:

CategoríaQué esEjemplosSistema operativo
Computadores de placa únicaPlaca completa con conectores de usuarioPi 4, Pi 5Linux completo
Módulos de cómputo (CM)El SoC y la RAM en un formato para soldar o encastrar en una placa base propiaCM4, CM5Linux completo
Serie ZeroPlacas mínimas, económicas, de bajo consumoZero 2 WLinux completo
Serie PicoMicrocontroladores, no computadoresPico, Pico 2, Pico WSin Linux: firmware directo

La serie Pico es la excepción que confirma la regla: es un microcontrolador de la misma familia que un Arduino, no un SBC. Se programa con C/C++ mediante el SDK de Raspberry Pi, con MicroPython o con el framework Arduino, y arranca en milisegundos. Comparte el nombre de marca, no la categoría.

ModeloSoCCPURAMNotas
Pi 5BCM27124x Cortex-A76 a 2,4 GHz4 a 16 GB LPDDR4XSouthbridge RP1, PCIe, RTC, botón de encendido
Pi 4 Model BBCM27114x Cortex-A72 a 1,5 a 1,8 GHz1 a 8 GB LPDDR4USB 3.0, Gigabit Ethernet real, doble micro-HDMI
Pi 3 Model B+BCM2837B04x Cortex-A53 a 1,4 GHz1 GB LPDDR2Wi-Fi de doble banda, PoE por cabezal
Zero 2 WRP3A04x Cortex-A53 a 1 GHz512 MBConsumo muy bajo, formato mínimo
Compute Module 4BCM27114x Cortex-A721 a 8 GBeMMC opcional, para placas base propias
PicoRP20402x Cortex-M0+ a 133 MHz264 KB SRAMMicrocontrolador; 2 MB de Flash externa; PIO
Pico 2RP23502x Cortex-M33 o 2x Hazard3 (RISC-V)520 KB SRAMMicrocontrolador; núcleos seleccionables

El RP1 del Pi 5 es un cambio arquitectónico que conviene conocer: es un chip aparte, diseñado por la propia fundación, que concentra los controladores de USB, Ethernet, GPIO, SPI, I2C, UART y las cámaras. En modelos anteriores todo eso estaba dentro del SoC de Broadcom. La consecuencia visible para nosotros es que la numeración de los gpiochip en Linux cambió respecto a las generaciones previas, así que nunca hay que asumirla: se descubre en tiempo de ejecución.

El cabezal de 40 pines

Todos los modelos modernos de Raspberry Pi exponen un conector de 40 pines con la misma distribución. Los pines físicos se numeran del 1 al 40 en zigzag, pero el software casi siempre usa la numeración BCM, que es el nombre de la línea GPIO dentro del SoC. Son dos sistemas distintos para los mismos agujeros: el pin físico 11 es GPIO17 en BCM. Mezclarlos es la causa número uno de “conecté todo bien y no pasa nada”.

FunciónLíneas BCMComentario
Alimentación 3,3 VPines físicos 1 y 17Total disponible del orden de 50 mA
Alimentación 5 VPines físicos 2 y 4Viene directo de la fuente, sin regular
Tierra8 pines repartidosUsar el más cercano al dispositivo
I2C (bus 1)GPIO2 (SDA), GPIO3 (SCL)Con pull-ups de 1,8 kΩ ya montados en la placa
SPI0GPIO10 (MOSI), GPIO9 (MISO), GPIO11 (SCLK), GPIO8 (CE0), GPIO7 (CE1)Dos líneas de selección de chip
UART principalGPIO14 (TXD), GPIO15 (RXD)Consola de arranque por defecto
PWM por hardwareGPIO12, GPIO13, GPIO18, GPIO19El resto solo admite PWM por software
Propósito generalEl resto de las 26 líneas del cabezalEntrada o salida, con pull-up o pull-down configurable

Y ahora los límites, que son mucho más estrechos que en Arduino:

  • La lógica es de 3,3 V. No hay tolerancia a 5 V. Aplicar 5 V a un pin de entrada puede destruir la línea o el SoC completo.
  • Cada pin entrega del orden de 16 mA, y el consumo total sumado del riel de 3,3 V ronda los 50 mA.
  • No hay conversor analógico-digital. Ninguno. Ni un solo canal. Para leer un potenciómetro, un LDR o un sensor de humedad de suelo analógico necesitas un chip externo: un MCP3008 por SPI (8 canales, 10 bits) o un ADS1115 por I2C (4 canales, 16 bits). Esta ausencia sorprende a todo el mundo la primera vez.

La comparación directa con Arduino en el plano eléctrico:

AspectoArduino UNO R3Raspberry Pi 4/5
Voltaje lógico5 V3,3 V, sin tolerancia a 5 V
Corriente por pin~20 mA cómodos~16 mA
ADC integradoSí, 6 canales de 10 bitsNo, requiere chip externo
DAC integradoNo (sí en UNO R4)No
PWM por hardware6 pines4 líneas
Consecuencia de un error de cableadoSuele sobrevivirSuele ser fatal

La regla que se deduce: nunca conectes una salida de un Arduino de 5 V directamente a un pin de un Raspberry Pi. Entre ambos va un divisor resistivo (para una señal unidireccional lenta) o un conversor de nivel bidireccional (para I2C o SPI). En el sentido inverso, del Pi al Arduino, normalmente funciona sin adaptador porque el Arduino reconoce 3,3 V como nivel alto, pero eso es un margen ajustado y depende del chip.

Cómo se ve el GPIO desde Linux

En un microcontrolador escribes a un registro y el pin cambia. En Linux hay un kernel de por medio, y esa indirección tiene una historia.

Durante años se usó la interfaz sysfs: exportabas un pin escribiendo su número en /sys/class/gpio/export y luego manipulabas archivos de texto. Esa interfaz está obsoleta desde hace tiempo y tiene defectos serios: no hay propiedad del recurso (cualquier proceso puede pisar el pin de otro), los pines quedan exportados si el proceso muere, y es lenta.

La interfaz actual es el carácter de dispositivo GPIO: /dev/gpiochip0, /dev/gpiochip1, etc. Cada archivo representa un controlador de GPIO con sus líneas. Un proceso abre el chip, pide líneas concretas mediante llamadas ioctl, y el kernel se las asigna en exclusiva mientras el descriptor esté abierto. Si el proceso muere, el kernel libera las líneas automáticamente. La biblioteca de espacio de usuario que envuelve esto es libgpiod.

flowchart TB
    APP["Proceso de usuario<br/>(BEAM, Python, C)"] -->|"open + ioctl"| CDEV["/dev/gpiochipN<br/>carácter de dispositivo"]
    CDEV --> DRV["Driver de GPIO en el kernel"]
    DRV --> HW["Controlador físico<br/>(RP1 en Pi 5, SoC en Pi 4)"]
    HW --> PIN["Pin del cabezal de 40"]

    APP -.->|"obsoleto, evitar"| SYSFS["/sys/class/gpio<br/>interfaz sysfs"]
    SYSFS -.-> DRV

    DRV -->|"evento de flanco"| CDEV
    CDEV -->|"lectura del descriptor<br/>con marca de tiempo del kernel"| APP

    style SYSFS stroke-dasharray: 5 5

Las herramientas de línea de comandos de libgpiod permiten explorar todo esto antes de escribir una línea de código:

# Qué controladores de GPIO existen en esta placa
gpiodetect
# Salida típica en un Raspberry Pi:
# gpiochip0 [pinctrl-rp1] (54 lines)
# gpiochip1 [rp1-gpio-brcmstb] (32 lines)

# Qué líneas tiene un chip, con sus nombres y quién las usa
gpioinfo gpiochip0

# Poner en alto la línea 18 y mantenerla mientras el comando siga vivo
gpioset --mode=wait gpiochip0 18=1

# Leer el estado de la línea 17
gpioget gpiochip0 17

# Esperar flancos de subida en la línea 17 e imprimir cada evento
gpiomon --rising-edge gpiochip0 17

El nombre y el número del gpiochip que corresponde al cabezal cambia entre modelos y entre versiones de kernel, especialmente con la llegada del RP1 en el Pi 5. Por eso gpiodetect y gpioinfo son el primer paso de cualquier diagnóstico: descubren la topología real de la placa que tienes delante en lugar de asumirla. Las bibliotecas modernas, incluida la de Elixir que veremos a continuación, resuelven esto permitiendo referirse a las líneas por su nombre lógico.

El entorno de desarrollo del Raspberry Pi

Aquí hay dos caminos, y la elección determina cómo se ve el proyecto entero.

Camino 1: Raspberry Pi OS. Se graba la imagen en una microSD con Raspberry Pi Imager, se arranca, y se tiene una distribución Debian completa con apt, systemd, SSH y todo lo demás. Instalas Erlang y Elixir, escribes tu aplicación, la ejecutas como servicio. Es el camino familiar para cualquiera que venga del desarrollo en Linux, y es el correcto para prototipar y para dispositivos de los que tienes acceso físico.

Sus costos aparecen en producción: la microSD se corrompe con cortes de energía, el arranque tarda decenas de segundos, actualizar cien dispositivos en terreno significa scripts de SSH, y la superficie de ataque incluye todo lo que Debian trae instalado.

Camino 2: Nerves. Nerves es el framework de Elixir para sistemas embebidos. Compila tu aplicación en una imagen de firmware completa: un kernel Linux mínimo, un sistema de archivos raíz de solo lectura y la BEAM ejecutándose como proceso de inicio. No hay shell de usuario, no hay apt, no hay systemd. La aplicación es el sistema.

stateDiagram-v2
    [*] --> BootLoader: energía aplicada
    BootLoader --> Kernel: carga el kernel Linux
    Kernel --> InitBEAM: monta rootfs de solo lectura
    InitBEAM --> Supervisores: erlinit lanza la BEAM como PID 1
    Supervisores --> Corriendo: el árbol de supervisión levanta<br/>GPIO, red, aplicación
    Corriendo --> Corriendo: un proceso falla y el supervisor lo reinicia
    Corriendo --> FirmwareNuevo: mix upload por la red
    FirmwareNuevo --> ParticionB: escribe en la partición inactiva
    ParticionB --> Reinicio: marca la partición B como activa
    Reinicio --> Kernel: arranca desde B
    Reinicio --> ParticionA: si B falla, revierte a A
    ParticionA --> Kernel

El mecanismo de doble partición A/B es la razón principal para elegir Nerves en producción: una actualización fallida no deja el dispositivo inutilizable, porque el firmware anterior sigue intacto en la otra partición.

El flujo de trabajo completo:

# Instalar el generador de proyectos, una sola vez
mix archive.install hex nerves_bootstrap

# Crear el proyecto
mix nerves.new estacion_sensores
cd estacion_sensores

# Elegir el objetivo: rpi0, rpi3, rpi4, rpi5, bbb, x86_64...
export MIX_TARGET=rpi4

# Resolver dependencias para ese objetivo
mix deps.get

# Compilar la imagen de firmware completa
mix firmware

# Grabar la microSD (la primera vez, con la tarjeta en el computador)
mix burn

# A partir de ahí, actualizar por la red sin sacar la tarjeta
mix upload estacion_sensores.local

La variable MIX_TARGET es la pieza central: con MIX_TARGET=host el mismo proyecto compila y corre en tu máquina de desarrollo con dependencias simuladas, y con MIX_TARGET=rpi4 produce firmware real. Eso permite escribir y probar la lógica sin tener la placa encendida.

GPIO desde Elixir: Circuits

La biblioteca circuits_gpio del proyecto Elixir Circuits es la que usaremos en todo el curso para hablar con pines. Funciona igual en Raspberry Pi OS y en Nerves, porque en ambos casos habla con /dev/gpiochipN a través de libgpiod.

Las dependencias en mix.exs:

defmodule EstacionSensores.MixProject do
  use Mix.Project

  def project do
    [
      app: :estacion_sensores,
      version: "0.1.0",
      elixir: "~> 1.16",
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {EstacionSensores.Application, []}
    ]
  end

  defp deps do
    [
      {:circuits_gpio, "~> 2.1"},
      {:circuits_i2c, "~> 2.0"},
      {:circuits_spi, "~> 2.0"},
      {:circuits_uart, "~> 1.5"}
    ]
  end
end

Un GenServer que hace parpadear un LED sin bloquear a nadie:

defmodule EstacionSensores.Latido do
  @moduledoc """
  Parpadea un LED conectado a GPIO18 usando mensajes diferidos,
  sin bloquear el proceso ni el planificador de la BEAM.
  """
  use GenServer
  require Logger

  alias Circuits.GPIO

  @intervalo_ms 500

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @impl true
  def init(opts) do
    linea = Keyword.get(opts, :linea, "GPIO18")

    # En circuits_gpio 2.x la línea puede identificarse por su nombre
    # lógico ("GPIO18"), por una tupla {"gpiochip0", 18} o por su
    # número, y la biblioteca resuelve el chip correcto en la placa.
    {:ok, gpio} = GPIO.open(linea, :output, initial_value: 0)

    Process.send_after(self(), :alternar, @intervalo_ms)
    {:ok, %{gpio: gpio, encendido: false}}
  end

  @impl true
  def handle_info(:alternar, %{gpio: gpio, encendido: encendido} = estado) do
    nuevo = not encendido
    GPIO.write(gpio, if(nuevo, do: 1, else: 0))

    Process.send_after(self(), :alternar, @intervalo_ms)
    {:noreply, %{estado | encendido: nuevo}}
  end

  @impl true
  def terminate(_razon, %{gpio: gpio}) do
    GPIO.write(gpio, 0)
    GPIO.close(gpio)
    :ok
  end
end

Lo que distingue esto de un sketch de Arduino: si este proceso falla, su supervisor lo reinicia, init/1 vuelve a abrir la línea y el LED sigue parpadeando. Mientras tanto, miles de otros procesos siguen corriendo sin enterarse. Es el modelo de tolerancia a fallos de la BEAM aplicado a hardware.

Leer un botón por interrupciones, sin sondeo, con antirrebote:

defmodule EstacionSensores.Boton do
  @moduledoc """
  Detecta pulsaciones en GPIO17 por interrupción de flanco.
  El pull-up interno mantiene la línea en alto; el botón la lleva a tierra.
  """
  use GenServer
  require Logger

  alias Circuits.GPIO

  @rebote_ms 40

  def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)

  @impl true
  def init(opts) do
    linea = Keyword.get(opts, :linea, "GPIO17")

    {:ok, gpio} = GPIO.open(linea, :input, pull_mode: :pullup)
    :ok = GPIO.set_interrupts(gpio, :both)

    {:ok, %{gpio: gpio, ultimo_evento_ns: 0}}
  end

  # circuits_gpio entrega los eventos como mensajes normales al proceso
  # dueño de la línea: {:circuits_gpio, identificador, marca_ns, valor}
  @impl true
  def handle_info({:circuits_gpio, _linea, marca_ns, valor}, estado) do
    delta_ms = (marca_ns - estado.ultimo_evento_ns) / 1_000_000

    cond do
      delta_ms < @rebote_ms ->
        # Rebote mecánico del contacto: se ignora
        {:noreply, estado}

      valor == 0 ->
        Logger.info("Botón presionado")
        {:noreply, %{estado | ultimo_evento_ns: marca_ns}}

      true ->
        Logger.info("Botón liberado")
        {:noreply, %{estado | ultimo_evento_ns: marca_ns}}
    end
  end
end

Dos cosas que vale la pena subrayar. Primero, la marca de tiempo viene del kernel, no de Elixir: es mucho más precisa que medir el momento en que el proceso recibió el mensaje. Segundo, el rebote mecánico es real: un pulsador barato genera decenas de transiciones en los primeros milisegundos de contacto, y sin el filtro contarías una pulsación como veinte.

Nota de compatibilidad: en la versión 1.x de circuits_gpio se abría el pin con GPIO.open(18, :output), pasando el número directamente. La versión 2.x introdujo los identificadores por nombre y por tupla chip/desplazamiento, precisamente porque la numeración cruda dejó de ser estable entre placas y kernels. Si encuentras código antiguo con números sueltos, esa es la razón.

Usar las dos juntas: la arquitectura híbrida

En la práctica, muchos sistemas serios no eligen: ponen un SBC y un microcontrolador en la misma máquina y les reparten el trabajo según lo que cada uno hace bien.

flowchart LR
    subgraph Campo["Capa de tiempo real"]
        S1["Encoders de rueda"] --> MCU["Arduino / microcontrolador"]
        S2["Sensores analógicos"] --> MCU
        S3["Finales de carrera"] --> MCU
        MCU --> A1["Driver de motores"]
        MCU --> A2["Servos"]
    end

    MCU <-->|"USB-serie 115200 bps<br/>protocolo de líneas"| SBC

    subgraph Cerebro["Capa de coordinación"]
        SBC["Raspberry Pi<br/>BEAM / Nerves"]
        SBC --> V["Visión por computador"]
        SBC --> W["API web y panel"]
        SBC --> DB["Almacenamiento local"]
        SBC --> NET["MQTT / TLS hacia la nube"]
    end

    NET --> Nube["Servidor Phoenix"]

El microcontrolador se encarga del lazo de control: leer el encoder cada milisegundo, ajustar el PWM del motor, cortar todo si se activa un final de carrera. Esas garantías temporales no dependen de que Linux esté ocupado. El SBC se encarga de todo lo que necesita CPU, memoria, red o almacenamiento.

El puente entre ambos suele ser el puerto serie. Del lado del Arduino, un protocolo de texto simple y completo:

// puente_serial.ino
// Protocolo de líneas terminadas en \n:
//   PING          -> PONG
//   LED <0|1>     -> OK LED <estado>
//   READ <canal>  -> VAL <canal> <lectura 0-1023>
//   PWM <pin> <0-255> -> OK PWM <pin> <valor>
// Cualquier otra cosa -> ERR <comando>

#define LED 13
const size_t MAX_LINEA = 48;

char buffer[MAX_LINEA];
size_t largo = 0;

void setup() {
  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, LOW);
}

void procesar(char *linea) {
  if (strcmp(linea, "PING") == 0) {
    Serial.println("PONG");
    return;
  }

  if (strncmp(linea, "LED ", 4) == 0) {
    int valor = atoi(linea + 4);
    digitalWrite(LED, valor ? HIGH : LOW);
    Serial.print("OK LED ");
    Serial.println(valor ? 1 : 0);
    return;
  }

  if (strncmp(linea, "READ ", 5) == 0) {
    int canal = atoi(linea + 5);
    if (canal < 0 || canal > 5) {
      Serial.println("ERR canal fuera de rango");
      return;
    }
    int lectura = analogRead(A0 + canal);
    Serial.print("VAL ");
    Serial.print(canal);
    Serial.print(' ');
    Serial.println(lectura);
    return;
  }

  if (strncmp(linea, "PWM ", 4) == 0) {
    int pin, valor;
    if (sscanf(linea + 4, "%d %d", &pin, &valor) == 2) {
      valor = constrain(valor, 0, 255);
      pinMode(pin, OUTPUT);
      analogWrite(pin, valor);
      Serial.print("OK PWM ");
      Serial.print(pin);
      Serial.print(' ');
      Serial.println(valor);
      return;
    }
  }

  Serial.print("ERR ");
  Serial.println(linea);
}

void loop() {
  while (Serial.available() > 0) {
    char c = Serial.read();

    if (c == '\n') {
      buffer[largo] = '\0';
      if (largo > 0) procesar(buffer);
      largo = 0;
    } else if (c != '\r' && largo < MAX_LINEA - 1) {
      buffer[largo++] = c;
    }
  }
}

Y del lado del Raspberry Pi, el cliente en Elixir usando circuits_uart:

defmodule EstacionSensores.PuenteArduino do
  @moduledoc """
  Cliente del protocolo de líneas expuesto por el Arduino conectado por USB.
  """
  use GenServer
  require Logger

  alias Circuits.UART

  @velocidad 115_200
  @timeout_ms 2_000

  # ---- API pública ----

  def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)

  def ping, do: GenServer.call(__MODULE__, {:comando, "PING"}, @timeout_ms)

  def led(encendido?) when is_boolean(encendido?) do
    GenServer.call(__MODULE__, {:comando, "LED #{if encendido?, do: 1, else: 0}"}, @timeout_ms)
  end

  def leer_analogico(canal) when canal in 0..5 do
    case GenServer.call(__MODULE__, {:comando, "READ #{canal}"}, @timeout_ms) do
      {:ok, "VAL " <> resto} ->
        [_canal, valor] = String.split(resto, " ", parts: 2)
        {:ok, String.to_integer(valor)}

      otro ->
        otro
    end
  end

  def pwm(pin, valor) when valor in 0..255 do
    GenServer.call(__MODULE__, {:comando, "PWM #{pin} #{valor}"}, @timeout_ms)
  end

  # ---- Implementación ----

  @impl true
  def init(opts) do
    puerto = Keyword.get(opts, :puerto) || detectar_puerto()
    {:ok, uart} = UART.start_link()

    :ok =
      UART.open(uart, puerto,
        speed: @velocidad,
        active: true,
        framing: {UART.Framing.Line, separator: "\n"}
      )

    # El bootloader del Arduino reinicia la placa al abrir el puerto:
    # hay que darle tiempo antes del primer comando.
    Process.send_after(self(), :listo, 2_000)

    {:ok, %{uart: uart, puerto: puerto, esperando: nil, listo: false}}
  end

  @impl true
  def handle_info(:listo, estado), do: {:noreply, %{estado | listo: true}}

  # UART en modo activo entrega cada línea como un mensaje al proceso
  @impl true
  def handle_info({:circuits_uart, _puerto, linea}, %{esperando: nil} = estado)
      when is_binary(linea) do
    Logger.debug("Línea no solicitada del Arduino: #{linea}")
    {:noreply, estado}
  end

  def handle_info({:circuits_uart, _puerto, linea}, %{esperando: desde} = estado)
      when is_binary(linea) do
    GenServer.reply(desde, {:ok, String.trim(linea)})
    {:noreply, %{estado | esperando: nil}}
  end

  def handle_info({:circuits_uart, _puerto, {:error, razon}}, estado) do
    Logger.error("Error de puerto serie: #{inspect(razon)}")
    {:stop, {:uart, razon}, estado}
  end

  @impl true
  def handle_call({:comando, _texto}, _desde, %{listo: false} = estado) do
    {:reply, {:error, :arduino_reiniciando}, estado}
  end

  def handle_call({:comando, texto}, desde, %{uart: uart} = estado) do
    case UART.write(uart, texto <> "\n") do
      :ok -> {:noreply, %{estado | esperando: desde}}
      {:error, razon} -> {:reply, {:error, razon}, estado}
    end
  end

  # Busca el primer puerto que parezca un adaptador USB-serie
  defp detectar_puerto do
    UART.enumerate()
    |> Map.keys()
    |> Enum.find(fn nombre ->
      String.starts_with?(nombre, "ttyUSB") or String.starts_with?(nombre, "ttyACM")
    end)
    |> case do
      nil -> raise "No se encontró ningún adaptador serie conectado"
      puerto -> puerto
    end
  end
end

Detalles que hacen que este código funcione y no solo compile:

  • UART.enumerate() devuelve un mapa de los puertos disponibles con su descripción; permite descubrir la placa sin cablear un nombre fijo. Los clones con chip CH340 aparecen como ttyUSB0, las placas con USB nativo como ttyACM0.
  • El framing de líneas hace que circuits_uart entregue mensajes completos en lugar de fragmentos arbitrarios de bytes. Sin él tendrías que reensamblar las líneas a mano.
  • La espera de 2 segundos tras abrir el puerto no es supersticiosa: es el tiempo que el bootloader del Arduino escucha antes de saltar a la aplicación. Comandos enviados antes se pierden.
  • El GenServer.reply/2 diferido convierte una operación de ida y vuelta sobre un medio asíncrono en una llamada síncrona limpia para quien consume la API.

Cuándo usar cada uno

El criterio de decisión, en forma de árbol:

flowchart TD
    Inicio["¿Qué tiene que hacer el dispositivo?"] --> Q1{"¿Necesita garantías<br/>temporales duras<br/>(microsegundos)?"}

    Q1 -->|Sí| Q2{"¿Además necesita red,<br/>almacenamiento o cómputo pesado?"}
    Q1 -->|No| Q3{"¿Necesita TLS, base de datos,<br/>visión o interfaz web?"}

    Q2 -->|No| MCU["Solo microcontrolador<br/>Arduino, ESP32, STM32"]
    Q2 -->|Sí| HIB["Arquitectura híbrida<br/>SBC + MCU por serie"]

    Q3 -->|Sí| Q4{"¿Alimentado por batería<br/>durante semanas?"}
    Q3 -->|No| Q5{"¿Hay entradas analógicas?"}

    Q4 -->|Sí| MCU2["Microcontrolador con radio<br/>y modos de bajo consumo"]
    Q4 -->|No| SBC["SBC con Nerves<br/>Raspberry Pi"]

    Q5 -->|Sí, muchas| MCU
    Q5 -->|Pocas o ninguna| SBC

    MCU --> Fin["Diseño definido"]
    MCU2 --> Fin
    SBC --> Fin
    HIB --> Fin

Y la comparación desarrollada, dimensión por dimensión:

DimensiónMicrocontrolador (Arduino)Computador de placa única (Raspberry Pi)
Tiempo de arranqueMilisegundos15 a 40 segundos con Linux completo
Determinismo temporalCiclo de reloj predecibleSujeto al planificador del kernel
Consumo típico activo20 a 50 mA; microamperios dormido500 mA a 3 A según modelo y carga
Memoria de trabajoKilobytesGigabytes
Entradas analógicasIntegradasRequieren chip externo
Tolerancia a corte de energíaAlta: no hay sistema de archivos que corromperBaja con microSD sin precauciones
ConcurrenciaUn solo hilo más interrupcionesProcesos, hilos, y en la BEAM millones de procesos ligeros
Red y criptografíaLimitada, requiere bibliotecas ajustadasNativa y completa
Actualización remotaPosible pero hay que construirlaNerves la trae con particiones A/B
DepuraciónImpresiones por serie o JTAGShell remoto, IEx, trazas, observador
Costo típicoBajoMedio
Lenguaje habitual en este cursoC++ del framework ArduinoElixir sobre la BEAM

Tres escenarios concretos para aterrizarlo:

Un sensor de humedad de suelo alimentado por batería que reporta una vez por hora. Microcontrolador, sin discusión. Necesita ADC (que el Pi no tiene), necesita dormir en microamperios entre reportes (que Linux no hace), y no necesita nada de lo que aporta un SBC. Un Arduino MKR con radio LoRa o un ESP32 resuelven el caso completo.

Un panel de control con cámara que clasifica piezas en una cinta transportadora. SBC. Hay visión por computador, hay una interfaz web, hay que guardar histórico. Un Raspberry Pi 5 con Nerves y Phoenix. Si además hay que controlar el motor de la cinta con precisión, se le agrega un microcontrolador.

Un robot móvil con cuatro motores, encoders, sensores de distancia y navegación autónoma. Híbrido. El microcontrolador cierra el lazo de velocidad de cada rueda y lee los encoders sin perder pulsos; el Raspberry Pi hace la planificación de trayectoria, la fusión de sensores y expone la telemetría. El puente serie que escribimos arriba es exactamente esa frontera.

Errores comunes

SíntomaCausaSolución
El Raspberry Pi deja de responder o el pin queda muerto tras conectar un ArduinoSe aplicaron 5 V a un GPIO que solo tolera 3,3 VIntercalar un divisor resistivo para señales unidireccionales o un conversor de nivel bidireccional para I2C y SPI. Verificar con multímetro antes de conectar
analogRead() en el Raspberry Pi no existeEl Pi no tiene ningún conversor analógico-digitalAgregar un MCP3008 por SPI o un ADS1115 por I2C, y leerlos con circuits_spi o circuits_i2c
El LED conectado al pin 11 no respondeSe confundió la numeración física del cabezal con la numeración BCMFijar una convención y documentarla. En circuits_gpio 2.x, usar nombres como "GPIO17" en vez de números sueltos
El sketch compila pero la placa se reinicia sola o se comporta de forma erráticaDesbordamiento de la SRAM de 2 KB: buffers grandes, cadenas dinámicas o recursión profundaMover las cadenas constantes a Flash con F("texto") en las impresiones, reducir buffers, revisar el aviso de memoria disponible que muestra el compilador
El monitor serie muestra caracteres ilegiblesLa velocidad configurada no coincide con la del Serial.begin()Igualar ambos valores. En PlatformIO se fija con monitor_speed; en Elixir con la opción speed: de UART.open
Los primeros comandos enviados al Arduino desde Elixir se pierdenAbrir el puerto serie dispara un reset por DTR y el bootloader tarda ~2 s en ceder el controlEsperar antes del primer comando, o implementar un reintento con un PING hasta recibir PONG
GPIO.open devuelve un error de permisosEl usuario no pertenece al grupo que posee /dev/gpiochipNAgregar el usuario al grupo gpio y volver a iniciar sesión. En Nerves no aplica: la BEAM corre con privilegios
El motor funciona un momento y luego el Arduino se reiniciaEl motor se alimenta desde el pin de 5 V de la placa y su corriente de arranque hunde la tensiónAlimentar el motor desde una fuente independiente, unir las tierras, y controlarlo con un MOSFET o un driver dedicado
Un pulsador registra decenas de eventos por cada pulsaciónRebote mecánico del contactoFiltrar por tiempo en software (como en el ejemplo de Boton) o agregar un condensador de 100 nF en paralelo al pulsador
El Raspberry Pi no arranca después de un corte de energíaEl sistema de archivos de la microSD se corrompió por una escritura interrumpidaUsar Nerves con rootfs de solo lectura, o montar en solo lectura las particiones críticas y aislar los datos mutables
gpioset funciona pero el pin vuelve a su estado anterior de inmediatoEl carácter de dispositivo libera la línea cuando el proceso terminaUsar gpioset --mode=wait para mantener el proceso vivo, o gestionar la línea desde un proceso persistente
Cambiar la frecuencia de PWM rompió millis() y delay()Se reconfiguró Timer0, que además de los pines 5 y 6 sostiene la base de tiempo del frameworkUsar Timer1 o Timer2 para PWM personalizado y dejar Timer0 intacto
mix firmware compila para la máquina de desarrollo en vez de la placaLa variable MIX_TARGET no está exportada en esa terminalExportar MIX_TARGET=rpi4 antes de mix deps.get y mix firmware, y volver a resolver dependencias tras cambiarla

Ejercicios propuestos

  1. Inventario del cabezal. En un Raspberry Pi, ejecuta gpiodetect y gpioinfo y produce una tabla propia con las líneas del cabezal de 40, indicando cuáles están ocupadas por un driver y cuáles están libres. Compara el resultado con el diagrama oficial de pines y anota cualquier diferencia.

  2. Traducción de abstracciones. Reescribe el sketch blink.ino sin usar pinMode ni digitalWrite, manipulando directamente los registros DDRB y PORTB. Mide con micros() cuántos ciclos toma cada versión y explica la diferencia.

  3. Semáforo con dos plataformas. Implementa el mismo semáforo de tres LEDs dos veces: una en Arduino con millis() y máquina de estados, otra en Elixir sobre Raspberry Pi con un GenServer y Process.send_after/3. Compara la cantidad de código, la legibilidad y qué pasa en cada una cuando el proceso o el programa falla.

  4. El ADC que falta. Conecta un potenciómetro a un MCP3008 y lee su posición desde Elixir con circuits_spi. Documenta el protocolo de tres bytes del MCP3008 y cómo se arma la petición del canal.

  5. Extender el puente. Agrega al protocolo serie del ejemplo un comando STATUS que devuelva en una sola línea el estado del LED, la lectura de A0 y el tiempo de actividad en milisegundos. Del lado de Elixir, expón una función estado/0 que devuelva un mapa ya parseado.

  6. Robustez del puente. Modifica PuenteArduino para que, ante un {:error, :ebadf} o una desconexión del USB, un supervisor lo reinicie y vuelva a detectar el puerto automáticamente. Prueba desconectando el cable en caliente.

  7. Presupuesto de corriente. Diseña en papel un nodo que use tres LEDs, un servo, un sensor DHT22 y un módulo de relé. Calcula el consumo total, decide qué se alimenta desde la placa y qué desde una fuente externa, y justifica cada decisión con los límites de corriente de este capítulo.

  8. Decidir con criterio. Para cada uno de estos casos, elige microcontrolador, SBC o híbrido, y escribe tres líneas de justificación: un contador de personas en una puerta con reporte diario por celular; un timelapse con cámara y subida a la nube; una impresora 3D; un sensor de calidad del aire en una sala con panel web en tiempo real.

  9. Nerves de punta a punta. Crea un proyecto con mix nerves.new, hazlo parpadear un LED, grábalo en una microSD y luego cambia el intervalo de parpadeo y actualízalo con mix upload sin sacar la tarjeta. Cronometra ambos ciclos de despliegue.

  10. Comparación de arranque. Mide con un cronómetro el tiempo que pasa entre aplicar energía y ver el primer parpadeo, en tres configuraciones: Arduino UNO, Raspberry Pi con Raspberry Pi OS y un servicio de systemd, y Raspberry Pi con Nerves. Explica de dónde sale cada diferencia.

Lo que viene

Con este capítulo quedan cubiertas las dos plataformas de entrada: el microcontrolador de 8 bits que arranca al instante y el computador Linux que trae la BEAM y la red. Ya sabes leer un cabezal de pines, respetar los límites eléctricos, abrir una línea GPIO desde Elixir y tender un puente serie entre ambos mundos.

Pero el mapa tiene un hueco evidente. El Arduino UNO no tiene radio, y el Raspberry Pi consume demasiado para funcionar con batería durante meses. Entre ambos extremos vive la generación de microcontroladores que domina hoy el IoT real: el ESP32 con Wi-Fi y Bluetooth integrados y modos de sueño profundo, el STM32 con su enorme catálogo de variantes y periféricos industriales, y el nRF52 diseñado desde el silicio para Bluetooth Low Energy y consumo mínimo. En el capítulo 8 los desarmamos con el mismo nivel de detalle: arquitectura, periféricos, entornos de desarrollo y cómo se integran con un backend en Elixir. El índice completo del curso está en la portada.