Arduino y Raspberry Pi: microcontrolador contra computador de placa única
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:
- El hardware: placas con un microcontrolador, un regulador de voltaje, un conversor USB-serie y un conector de pines estandarizado.
- El framework de software: un conjunto de bibliotecas en C++ (
pinMode,digitalWrite,analogRead,Serial) que abstraen los registros del microcontrolador. - 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:
| Grupo | Pines | Capacidad |
|---|---|---|
| Digitales | D0 a D13 (14 pines) | Entrada o salida, 5 V |
| PWM | D3, D5, D6, D9, D10, D11 | Salida analógica simulada de 8 bits |
| Analógicos | A0 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 |
| SPI | D10 (SS), D11 (MOSI), D12 (MISO), D13 (SCK) | Bus síncrono de alta velocidad |
| I2C | A4 (SDA), A5 (SCL) | Bus de dos hilos, multidispositivo |
| Interrupciones externas | D2, D3 | Disparo por flanco sin sondeo |
| Alimentación | 5V, 3V3, GND, VIN | 3V3 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
| Placa | Microcontrolador | Bits / Reloj | Flash / SRAM | Lógica | Rasgo distintivo |
|---|---|---|---|---|---|
| UNO R3 | ATmega328P | 8 bits / 16 MHz | 32 KB / 2 KB | 5 V | La referencia del ecosistema |
| Nano | ATmega328P | 8 bits / 16 MHz | 32 KB / 2 KB | 5 V | Mismo chip en formato protoboard |
| Mega 2560 | ATmega2560 | 8 bits / 16 MHz | 256 KB / 8 KB | 5 V | 54 digitales, 16 analógicos, 4 UART |
| Leonardo / Micro | ATmega32u4 | 8 bits / 16 MHz | 32 KB / 2,5 KB | 5 V | USB nativo: se presenta como teclado o mouse |
| UNO R4 Minima | Renesas RA4M1 (Cortex-M4) | 32 bits / 48 MHz | 256 KB / 32 KB | 5 V | DAC real, ADC de mayor resolución, CAN |
| UNO R4 WiFi | RA4M1 + ESP32-S3 | 32 bits / 48 MHz | 256 KB / 32 KB | 5 V | Wi-Fi, Bluetooth y matriz de LEDs integrada |
| MKR (familia) | SAMD21 (Cortex-M0+) | 32 bits / 48 MHz | 256 KB / 32 KB | 3,3 V | Variantes con Wi-Fi, LoRa, Sigfox, NB-IoT |
| Nano 33 BLE Sense | nRF52840 (Cortex-M4F) | 32 bits / 64 MHz | 1 MB / 256 KB | 3,3 V | Sensores integrados y Bluetooth LE |
| Nano RP2040 Connect | RP2040 + NINA-W102 | 32 bits / 133 MHz | 16 MB / 264 KB | 3,3 V | Doble 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ía | Qué es | Ejemplos | Sistema operativo |
|---|---|---|---|
| Computadores de placa única | Placa completa con conectores de usuario | Pi 4, Pi 5 | Linux completo |
| Módulos de cómputo (CM) | El SoC y la RAM en un formato para soldar o encastrar en una placa base propia | CM4, CM5 | Linux completo |
| Serie Zero | Placas mínimas, económicas, de bajo consumo | Zero 2 W | Linux completo |
| Serie Pico | Microcontroladores, no computadores | Pico, Pico 2, Pico W | Sin 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.
| Modelo | SoC | CPU | RAM | Notas |
|---|---|---|---|---|
| Pi 5 | BCM2712 | 4x Cortex-A76 a 2,4 GHz | 4 a 16 GB LPDDR4X | Southbridge RP1, PCIe, RTC, botón de encendido |
| Pi 4 Model B | BCM2711 | 4x Cortex-A72 a 1,5 a 1,8 GHz | 1 a 8 GB LPDDR4 | USB 3.0, Gigabit Ethernet real, doble micro-HDMI |
| Pi 3 Model B+ | BCM2837B0 | 4x Cortex-A53 a 1,4 GHz | 1 GB LPDDR2 | Wi-Fi de doble banda, PoE por cabezal |
| Zero 2 W | RP3A0 | 4x Cortex-A53 a 1 GHz | 512 MB | Consumo muy bajo, formato mínimo |
| Compute Module 4 | BCM2711 | 4x Cortex-A72 | 1 a 8 GB | eMMC opcional, para placas base propias |
| Pico | RP2040 | 2x Cortex-M0+ a 133 MHz | 264 KB SRAM | Microcontrolador; 2 MB de Flash externa; PIO |
| Pico 2 | RP2350 | 2x Cortex-M33 o 2x Hazard3 (RISC-V) | 520 KB SRAM | Microcontrolador; 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ón | Líneas BCM | Comentario |
|---|---|---|
| Alimentación 3,3 V | Pines físicos 1 y 17 | Total disponible del orden de 50 mA |
| Alimentación 5 V | Pines físicos 2 y 4 | Viene directo de la fuente, sin regular |
| Tierra | 8 pines repartidos | Usar 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 |
| SPI0 | GPIO10 (MOSI), GPIO9 (MISO), GPIO11 (SCLK), GPIO8 (CE0), GPIO7 (CE1) | Dos líneas de selección de chip |
| UART principal | GPIO14 (TXD), GPIO15 (RXD) | Consola de arranque por defecto |
| PWM por hardware | GPIO12, GPIO13, GPIO18, GPIO19 | El resto solo admite PWM por software |
| Propósito general | El resto de las 26 líneas del cabezal | Entrada 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:
| Aspecto | Arduino UNO R3 | Raspberry Pi 4/5 |
|---|---|---|
| Voltaje lógico | 5 V | 3,3 V, sin tolerancia a 5 V |
| Corriente por pin | ~20 mA cómodos | ~16 mA |
| ADC integrado | Sí, 6 canales de 10 bits | No, requiere chip externo |
| DAC integrado | No (sí en UNO R4) | No |
| PWM por hardware | 6 pines | 4 líneas |
| Consecuencia de un error de cableado | Suele sobrevivir | Suele 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 comottyUSB0, las placas con USB nativo comottyACM0.- El framing de líneas hace que
circuits_uartentregue 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/2diferido 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ón | Microcontrolador (Arduino) | Computador de placa única (Raspberry Pi) |
|---|---|---|
| Tiempo de arranque | Milisegundos | 15 a 40 segundos con Linux completo |
| Determinismo temporal | Ciclo de reloj predecible | Sujeto al planificador del kernel |
| Consumo típico activo | 20 a 50 mA; microamperios dormido | 500 mA a 3 A según modelo y carga |
| Memoria de trabajo | Kilobytes | Gigabytes |
| Entradas analógicas | Integradas | Requieren chip externo |
| Tolerancia a corte de energía | Alta: no hay sistema de archivos que corromper | Baja con microSD sin precauciones |
| Concurrencia | Un solo hilo más interrupciones | Procesos, hilos, y en la BEAM millones de procesos ligeros |
| Red y criptografía | Limitada, requiere bibliotecas ajustadas | Nativa y completa |
| Actualización remota | Posible pero hay que construirla | Nerves la trae con particiones A/B |
| Depuración | Impresiones por serie o JTAG | Shell remoto, IEx, trazas, observador |
| Costo típico | Bajo | Medio |
| Lenguaje habitual en este curso | C++ del framework Arduino | Elixir 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íntoma | Causa | Solución |
|---|---|---|
| El Raspberry Pi deja de responder o el pin queda muerto tras conectar un Arduino | Se aplicaron 5 V a un GPIO que solo tolera 3,3 V | Intercalar 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 existe | El Pi no tiene ningún conversor analógico-digital | Agregar un MCP3008 por SPI o un ADS1115 por I2C, y leerlos con circuits_spi o circuits_i2c |
| El LED conectado al pin 11 no responde | Se confundió la numeración física del cabezal con la numeración BCM | Fijar 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ática | Desbordamiento de la SRAM de 2 KB: buffers grandes, cadenas dinámicas o recursión profunda | Mover 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 ilegibles | La 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 pierden | Abrir el puerto serie dispara un reset por DTR y el bootloader tarda ~2 s en ceder el control | Esperar antes del primer comando, o implementar un reintento con un PING hasta recibir PONG |
GPIO.open devuelve un error de permisos | El usuario no pertenece al grupo que posee /dev/gpiochipN | Agregar 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 reinicia | El motor se alimenta desde el pin de 5 V de la placa y su corriente de arranque hunde la tensión | Alimentar 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ón | Rebote mecánico del contacto | Filtrar 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ía | El sistema de archivos de la microSD se corrompió por una escritura interrumpida | Usar 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 inmediato | El carácter de dispositivo libera la línea cuando el proceso termina | Usar 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 framework | Usar Timer1 o Timer2 para PWM personalizado y dejar Timer0 intacto |
mix firmware compila para la máquina de desarrollo en vez de la placa | La variable MIX_TARGET no está exportada en esa terminal | Exportar MIX_TARGET=rpi4 antes de mix deps.get y mix firmware, y volver a resolver dependencias tras cambiarla |
Ejercicios propuestos
-
Inventario del cabezal. En un Raspberry Pi, ejecuta
gpiodetectygpioinfoy 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. -
Traducción de abstracciones. Reescribe el sketch
blink.inosin usarpinModenidigitalWrite, manipulando directamente los registrosDDRByPORTB. Mide conmicros()cuántos ciclos toma cada versión y explica la diferencia. -
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 yProcess.send_after/3. Compara la cantidad de código, la legibilidad y qué pasa en cada una cuando el proceso o el programa falla. -
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. -
Extender el puente. Agrega al protocolo serie del ejemplo un comando
STATUSque 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ónestado/0que devuelva un mapa ya parseado. -
Robustez del puente. Modifica
PuenteArduinopara 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. -
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.
-
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.
-
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 conmix uploadsin sacar la tarjeta. Cronometra ambos ciclos de despliegue. -
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.