ESP32, STM32 y nRF52: conectividad, consumo y criterios para elegir microcontrolador
ESP32, STM32 y nRF52: conectividad, consumo y criterios para elegir microcontrolador
En el capítulo 7 recorrimos las dos placas con las que casi todo el mundo empieza: Arduino, un microcontrolador de 8 bits con un entorno amable y una comunidad enorme, y Raspberry Pi, un computador Linux completo capaz de correr Nerves y hablar con el mundo por red. Ese par cubre los extremos del espectro: lo muy simple y lo muy capaz. El problema aparece cuando el proyecto deja de ser un experimento y necesita algo del medio: un nodo que dure meses con una pila de botón, un controlador de motor que responda en microsegundos sin jitter, o un sensor que se conecte solo a la red WiFi de la casa.
Ese hueco lo llenan tres familias de silicio que hoy concentran la mayor parte de los diseños reales de robótica e IoT: ESP32 de Espressif, STM32 de STMicroelectronics y nRF52 de Nordic Semiconductor. No compiten exactamente en lo mismo. Cada una nació resolviendo un problema distinto, y esa diferencia de origen sigue marcando qué tan bien se comportan cuando les pides algo que no está en su zona natural.
Este capítulo es un análisis comparado de las tres. Vamos a ver qué hay dentro de cada chip, qué periféricos traen, cuánta corriente consumen en cada estado, con qué herramientas se compilan y se graban, y cómo se conectan con Elixir. Al final vas a tener un método concreto —no una intuición— para decidir cuál va en tu próximo proyecto, y un modelo de cálculo para estimar la vida de la batería antes de comprar nada.
Tres filosofías de silicio
Antes de mirar hojas de datos conviene entender la intención de diseño de cada familia, porque explica casi todas sus decisiones técnicas. El ESP32 nació como el sucesor del ESP8266, un chip que se hizo famoso por una razón brutal: era un módulo WiFi completo que costaba menos que el conector de red que reemplazaba. Espressif diseñó el ESP32 poniendo la conectividad en el centro: radios WiFi y Bluetooth integradas en el mismo die que la CPU, con toda la pila de protocolos ya escrita y probada. El resto del chip —GPIO, ADC, timers— es competente, pero es acompañamiento. Si tu problema empieza con “necesito que esto se conecte a internet”, el ESP32 es la respuesta por defecto.
El STM32 es lo opuesto. ST viene del mundo del control industrial y del automotriz, y su prioridad es el determinismo: que una interrupción llegue en un número exacto de ciclos, que un timer genere PWM complementario con tiempo muerto configurable para un puente en H de un motor trifásico, que el ADC dispare por hardware sincronizado con ese timer y guarde la muestra por DMA sin que la CPU intervenga. La radio no viene incluida (salvo en las series WB y WL) porque en una fábrica el cable es más confiable. Si tu problema empieza con “esto tiene que responder siempre en menos de X microsegundos”, el STM32 es la respuesta.
El nRF52 es un especialista en radio de bajo consumo. Nordic construyó la familia alrededor de un transceptor 2,4 GHz extremadamente eficiente y una CPU que sabe dormir de verdad. Su métrica de orgullo no son los MHz sino los microamperes: cuánta carga gasta un ciclo completo de despertar, medir un sensor, transmitir un paquete BLE y volver a dormir. Si tu problema empieza con “esto tiene que durar dos años con una CR2032”, el nRF52 es la respuesta.
flowchart TD
P[Requisito dominante del proyecto] --> C{¿Qué manda?}
C -->|Conectividad IP,<br/>throughput, cloud| E[ESP32<br/>WiFi + BT integrados<br/>240 MHz dual-core]
C -->|Timing duro,<br/>control de motores,<br/>certificación industrial| S[STM32<br/>Cortex-M0 a M7<br/>timers avanzados + DMA]
C -->|Años de batería,<br/>enlace BLE, wearable| N[nRF52<br/>Cortex-M4F 64 MHz<br/>radio de ultrabajo consumo]
E --> E1[Costo por nodo bajo<br/>Consumo activo alto]
S --> S1[Consumo controlable<br/>Sin radio salvo WB/WL]
N --> N1[Consumo mínimo<br/>Sin IP nativo, sin WiFi]
E1 --> H[Nodo hub con Linux/Nerves<br/>agrega, decide y expone]
S1 --> H
N1 --> H
Ese diagrama ya adelanta la conclusión práctica del capítulo: en un sistema real rara vez eliges uno. Eliges una topología donde cada silicio hace lo que sabe hacer, y un nodo con Linux —típicamente Nerves sobre Raspberry Pi o similar— coordina el conjunto.
ESP32: la conectividad como punto de partida
El ESP32 no es un chip, es una familia
Cuando alguien dice “ESP32” a secas casi siempre se refiere al chip original de 2016, el ESP32-D0WD, pero desde entonces Espressif publicó una línea completa que se divide en dos ramas según la arquitectura del núcleo.
| Modelo | Arquitectura | Núcleos / Frecuencia | WiFi | Bluetooth | 802.15.4 | Rasgo distintivo |
|---|---|---|---|---|---|---|
| ESP32 | Xtensa LX6 | 2 × 240 MHz | 802.11 b/g/n | BR/EDR + BLE 4.2 | No | El clásico; Ethernet MAC, 2 DAC, touch |
| ESP32-S2 | Xtensa LX7 | 1 × 240 MHz | 802.11 b/g/n | No | No | USB OTG, muchos GPIO, sin Bluetooth |
| ESP32-S3 | Xtensa LX7 | 2 × 240 MHz | 802.11 b/g/n | BLE 5 | No | Instrucciones vectoriales para IA, USB OTG, LCD/camera |
| ESP32-C3 | RISC-V | 1 × 160 MHz | 802.11 b/g/n | BLE 5 | No | Reemplazo económico del ESP8266 |
| ESP32-C6 | RISC-V (HP + LP) | 160 MHz + 20 MHz | 802.11 ax (WiFi 6) | BLE 5.3 | Sí | Thread y Zigbee; núcleo de bajo consumo separado |
| ESP32-H2 | RISC-V | 1 × 96 MHz | No | BLE 5.3 | Sí | Solo radios de bajo consumo, sin WiFi |
| ESP32-P4 | RISC-V | 2 × 400 MHz | No | No | No | Alto rendimiento sin radio; se acompaña de un ESP32-C6 |
Dos lecturas importantes de esa tabla. La primera: la rama RISC-V es la dirección de futuro, porque los chips nuevos (C3, C6, H2, P4) abandonan el núcleo propietario Xtensa por una arquitectura abierta con soporte creciente en LLVM, lo que facilita el mantenimiento de toolchains. La segunda: la rama Xtensa todavía tiene más periféricos exóticos, y el S3 sigue siendo el chip con más capacidades de interfaz (controlador LCD paralelo, interfaz de cámara, PSRAM octal) y con aceleración vectorial para inferencia de modelos pequeños.
Un detalle que confunde a mucha gente: chip, módulo y placa son cosas distintas. El chip es el ESP32-D0WD. El módulo es el ESP32-WROOM-32, que mete el chip, el cristal, la memoria flash SPI, la antena y un blindaje metálico en un paquete certificado. La placa es la DevKitC, que agrega el módulo, un conversor USB-serie, un regulador de 3,3 V, botones de BOOT y EN, y pines. Cuando compras “un ESP32” estás comprando la placa; cuando diseñas un producto compras el módulo (porque viene con la certificación de radio ya hecha, que es cara y lenta de obtener por cuenta propia).
Qué hay dentro del ESP32 original
El chip clásico se fabrica en 40 nm y organiza sus recursos así:
- CPU: dos núcleos Tensilica Xtensa LX6 de 32 bits, configurables entre 80 y 240 MHz. Se llaman PRO_CPU (núcleo 0) y APP_CPU (núcleo 1). Por convención, la pila de red corre en el núcleo 0 y la aplicación en el núcleo 1, aunque no es obligatorio.
- Memoria: 520 KB de SRAM interna, 448 KB de ROM con rutinas de arranque, y 16 KB de SRAM en el dominio RTC (que sobrevive al deep sleep). La flash es externa, típicamente 4 MB en un módulo WROOM, mapeada en el espacio de direcciones por un caché.
- Coprocesador ULP: una máquina de estados minúscula que puede leer ADC y GPIO mientras los núcleos principales duermen, consumiendo microamperes. Es la pieza que permite usar el ESP32 en aplicaciones con batería sin que sea un desastre.
- Radios: WiFi 802.11 b/g/n en 2,4 GHz y Bluetooth de doble modo (clásico BR/EDR y BLE). Comparten una sola antena y un solo front-end de RF, lo que significa que WiFi y Bluetooth se turnan el aire.
- Rango de alimentación: 2,3 V a 3,6 V. Los GPIO no son tolerantes a 5 V; conectar un sensor de 5 V directo a un pin es una de las formas más comunes de matar el chip.
Los periféricos digitales relevantes para robótica:
| Periférico | Cantidad | Nota práctica |
|---|---|---|
| GPIO | 34 (no todos usables) | GPIO 6–11 están cableados a la flash; GPIO 34–39 son solo entrada y sin pull-up interno |
| ADC SAR de 12 bits | 2 unidades, 18 canales | ADC2 queda inutilizable mientras el WiFi está activo |
| DAC de 8 bits | 2 | GPIO 25 y 26; útiles para audio simple o referencias |
| Sensores táctiles capacitivos | 10 | Detectan contacto sin componentes externos |
| UART | 3 | UART0 se usa para el log de arranque y el flasheo |
| I2C | 2 | Ambos con soporte de maestro y esclavo |
| SPI | 4 (2 disponibles) | SPI0 y SPI1 quedan tomados por la flash |
| I2S | 2 | Audio digital, también micrófonos PDM |
| PWM (LEDC) | 16 canales | Resolución configurable hasta 20 bits |
| MCPWM | 2 unidades | Orientado a control de motores, con tiempo muerto |
| Pulse counter | 8 unidades | Cuenta encoders por hardware sin cargar la CPU |
| TWAI (compatible CAN 2.0) | 1 | Necesita un transceptor externo tipo SN65HVD230 |
| Ethernet MAC | 1 | Necesita PHY externo (LAN8720 es el típico) |
La trampa clásica del ADC2 merece una línea aparte: el segundo conversor comparte recursos con el subsistema de radio, así que cualquier lectura analógica en un canal de ADC2 falla mientras el WiFi está conectado. Si tu diseño necesita entradas analógicas y red al mismo tiempo, todas las señales deben ir a canales del ADC1 (GPIO 32 a 39). Es una restricción de silicio, no un bug de software, y hay que resolverla en el esquemático.
Los modos de energía del ESP32
El ESP32 tiene un consumo activo alto comparado con sus competidores, porque una radio WiFi transmitiendo es cara en energía. La estrategia de diseño no es reducir el consumo activo sino minimizar el tiempo que se pasa activo.
stateDiagram-v2
[*] --> Activo
Activo: Activo (CPU + radio)<br/>95-260 mA
ModemSleep: Modem-sleep<br/>CPU activa, radio apagada<br/>20-68 mA
LightSleep: Light-sleep<br/>CPU en pausa, RAM retenida<br/>~0.8 mA
DeepSleep: Deep-sleep<br/>solo dominio RTC + ULP<br/>~10 uA
Hibernacion: Hibernación<br/>solo timer RTC<br/>~5 uA
Activo --> ModemSleep: apagar radio entre transmisiones
ModemSleep --> Activo: DTIM beacon / envío
ModemSleep --> LightSleep: sin trabajo pendiente
LightSleep --> ModemSleep: interrupción GPIO / timer
LightSleep --> DeepSleep: dormir largo
DeepSleep --> Activo: reinicio desde ROM<br/>(pierde la RAM, no el RTC)
DeepSleep --> Hibernacion: apagar también la RAM RTC
Hibernacion --> Activo: solo timer o EXT0
Las cifras aproximadas —las exactas dependen del modelo, la tensión y la temperatura, y hay que confirmarlas en la hoja de datos del chip concreto— son del orden de:
| Estado | Corriente típica | Qué sigue vivo | Tiempo de salida |
|---|---|---|---|
| WiFi transmitiendo (802.11n, 20 dBm) | 180–260 mA en picos | Todo | — |
| WiFi recibiendo / conectado ocioso | 95–110 mA | Todo | — |
| Modem-sleep (radio apagada, CPU 240 MHz) | 30–68 mA | CPU, RAM, periféricos | inmediato |
| Modem-sleep (CPU 80 MHz) | 20–25 mA | CPU, RAM, periféricos | inmediato |
| Light-sleep | ~0,8 mA | RAM, periféricos configurados | ~1 ms |
| Deep-sleep con ULP activo | ~150 µA | Dominio RTC + ULP | ~10 ms |
| Deep-sleep sin ULP | ~10 µA | Dominio RTC, RAM RTC | ~10 ms |
| Hibernación | ~5 µA | Solo timer RTC | ~10 ms |
La consecuencia de diseño es directa: el ESP32 con batería solo funciona si duerme la mayor parte del tiempo. Un sensor que despierta cada 15 minutos, se conecta al WiFi, publica una medición por MQTT y vuelve a dormir puede promediar menos de 100 µA; el mismo sensor con la conexión permanente promediará 100 mA y vaciará una batería de 2000 mAh en menos de un día. Y hay un costo escondido en ese ciclo: la asociación WiFi consume energía. Reconectarse a un punto de acceso, negociar DHCP y establecer TLS puede tomar entre 1 y 5 segundos a 100 mA o más. En un ciclo de despertar de 3 segundos, la conexión es el 90 % del gasto. Optimizar eso —guardando el canal y el BSSID en la RAM RTC, usando IP estática, reutilizando sesión TLS— es lo que separa un prototipo de un producto.
Toolchains del ESP32
| Toolchain | Lenguaje | Cuándo tiene sentido |
|---|---|---|
| ESP-IDF | C / C++ | Es el SDK oficial, basado en FreeRTOS. Acceso completo a todo el hardware. Obligatorio para producto serio. |
| Arduino-ESP32 | C++ | Capa sobre ESP-IDF con la API de Arduino. Prototipado rápido, enorme cantidad de librerías. |
| PlatformIO | C / C++ | Gestor de proyectos y dependencias que orquesta ESP-IDF o Arduino desde VS Code, con builds reproducibles. |
| MicroPython | Python | Iteración interactiva por REPL. Cómodo para explorar sensores, lento para lazos de control. |
| AtomVM | Elixir / Erlang | Máquina virtual BEAM reducida corriendo en el propio ESP32. Procesos, mensajes y supervisión en el microcontrolador. |
| Rust (esp-rs) | Rust | Soporte oficial de Espressif, tanto bare-metal como sobre ESP-IDF. |
Instalar ESP-IDF y compilar un proyecto vacío se ve así:
# Clonar el SDK oficial en una versión fija (nunca trabajes contra master)
git clone -b v5.3 --recursive https://github.com/espressif/esp-idf.git ~/esp/esp-idf
# Instalar los compiladores (xtensa-esp32-elf o riscv32-esp-elf según el target)
cd ~/esp/esp-idf && ./install.sh esp32,esp32c3
. ~/esp/esp-idf/export.sh # carga el entorno en la shell actual
# Crear un proyecto desde una plantilla y compilarlo
cp -r ~/esp/esp-idf/examples/get-started/hello_world ~/proyectos/hola
cd ~/proyectos/hola && idf.py set-target esp32 && idf.py build
# Grabar y abrir el monitor serie (Ctrl+] para salir)
idf.py -p /dev/ttyUSB0 flash monitor
Si el flasheo falla con un timeout, casi siempre es porque la placa no entró en modo bootloader. En las DevKit con conversor CP2102 el autoreset funciona solo; en clones con CH340 a veces hay que mantener presionado BOOT, tocar EN y soltar BOOT.
ESP32 y Elixir: AtomVM
Aquí aparece la pieza que hace que este capítulo pertenezca a un curso de Elixir. Nerves no corre en un ESP32: Nerves construye un sistema Linux embebido, y Linux necesita una MMU y decenas de megabytes de RAM que el ESP32 no tiene. Confundir esto es el malentendido más frecuente de quien llega desde Elixir al mundo del microcontrolador.
Lo que sí corre en el ESP32 es AtomVM, una implementación reducida de la máquina virtual BEAM escrita en C, pensada para dispositivos con cientos de kilobytes de RAM. AtomVM ejecuta bytecode BEAM real —el mismo que produce elixirc— e implementa procesos ligeros, paso de mensajes, receive, monitores y una porción del OTP. No es Elixir completo: no hay compilación en el dispositivo, la biblioteca estándar está recortada y hay que cuidar la memoria. Pero el modelo de concurrencia es el mismo.
AtomVM soporta hoy ESP32 (Xtensa y RISC-V), STM32, Raspberry Pi Pico y una versión para escritorio útil para probar lógica.
Un proyecto Elixir para AtomVM se organiza con el plugin ExAtomVM:
# mix.exs
defmodule Sensor.MixProject do
use Mix.Project
def project do
[
app: :sensor,
version: "0.1.0",
elixir: "~> 1.16",
deps: [{:exatomvm, github: "atomvm/ExAtomVM", runtime: false}],
atomvm: [start: Sensor, flash_offset: 0x250000]
]
end
def application, do: [extra_applications: []]
end
Y el módulo de entrada, un parpadeo con supervisión mínima escrito con el estilo de procesos que ya conoces:
defmodule Sensor do
@moduledoc """
Punto de entrada para AtomVM. La función start/0 es la que arranca
cuando el firmware bootea; no hay Application ni supervisor OTP completo,
así que la estructura de procesos se levanta a mano.
"""
@led_pin 2
@periodo_ms 500
def start do
GPIO.set_pin_mode(@led_pin, :output)
parpadeo = spawn(fn -> parpadear(:low) end)
latido = spawn(fn -> latir() end)
# El proceso principal no puede terminar: si retorna, la VM se apaga.
supervisar([parpadeo, latido])
end
defp parpadear(nivel) do
GPIO.digital_write(@led_pin, nivel)
Process.sleep(@periodo_ms)
parpadear(invertir(nivel))
end
defp invertir(:low), do: :high
defp invertir(:high), do: :low
# Testigo periódico: cuántos procesos vivos hay en la VM.
defp latir do
Process.sleep(5_000)
:erlang.display({:vivo, :erlang.system_info(:process_count)})
latir()
end
# Monitorea a los hijos y se queda bloqueado en receive para siempre.
defp supervisar(hijos) do
Enum.each(hijos, &Process.monitor/1)
receive do
{:DOWN, _ref, :process, pid, motivo} ->
:erlang.display({:hijo_caido, pid, motivo})
supervisar([])
end
end
end
El ciclo de compilación y grabado:
mix deps.get
mix atomvm.packbeam # empaqueta los .beam en un archivo .avm
mix atomvm.esp32.flash # graba el .avm en la partición de datos
Nota importante: el .avm con tu aplicación se graba encima de un firmware AtomVM previamente instalado. La secuencia completa la primera vez es grabar la imagen de AtomVM con esptool.py y después subir el .avm con la tarea de Mix; actualizar la aplicación no requiere volver a grabar la VM. Para conectividad, AtomVM expone el módulo :network, que envuelve la pila WiFi de ESP-IDF: se entrega una configuración de estación con SSID y clave, y se espera a que el chip obtenga IP.
defmodule Sensor.Wifi do
@moduledoc """
Conexión WiFi en modo estación bajo AtomVM. El módulo :network es parte
de la biblioteca estándar de AtomVM y recibe listas de propiedades.
"""
def conectar(ssid, clave) do
config = [ssid: ssid, psk: clave]
case :network.wait_for_sta(config, 30_000) do
{:ok, {ip, mascara, gateway}} ->
:erlang.display({:conectado, formatear_ip(ip), formatear_ip(mascara), formatear_ip(gateway)})
{:ok, ip}
{:error, motivo} ->
:erlang.display({:sin_red, motivo})
{:error, motivo}
end
end
defp formatear_ip({a, b, c, d}), do: "#{a}.#{b}.#{c}.#{d}"
end
Si el detalle de la API cambia entre versiones de AtomVM, el concepto no: se configura una interfaz en modo estación, se bloquea hasta tener dirección IP y desde ahí se usan sockets. Conviene siempre contrastar con la documentación de la versión que tengas instalada antes de dar por buena una firma de función.
El ESP32 como periférico de un nodo Nerves
Hay un patrón que resuelve el 80 % de los sistemas reales sin pelear con las limitaciones de AtomVM: el ESP32 hace de radio y de front-end de sensores, y un nodo Nerves hace de cerebro. La comunicación entre ambos puede ser un cable serie, SPI, o directamente MQTT sobre WiFi.
sequenceDiagram
participant S as Sensor (I2C)
participant E as ESP32 (ESP-IDF)
participant B as Broker MQTT
participant N as Nodo Nerves (Elixir/OTP)
participant U as Interfaz Phoenix
Note over E: deep-sleep 15 min
E->>E: despierta por timer RTC
E->>S: lectura I2C de temperatura
S-->>E: 23.4 °C
E->>E: asociación WiFi (canal cacheado en RAM RTC)
E->>B: PUBLISH sensores/sala/temp {"v":23.4,"bat":3.71}
B->>N: entrega al suscriptor
N->>N: GenServer valida, promedia y persiste
N->>U: broadcast por PubSub
U-->>U: el gráfico se actualiza en vivo
E->>E: vuelve a deep-sleep
Del lado Elixir, el consumidor es un GenServer común y corriente. Este ejemplo usa Tortoise311, un cliente MQTT mantenido por la comunidad Elixir, pero la estructura sirve para cualquier transporte:
defmodule Hub.Ingesta do
@moduledoc """
Recibe mediciones publicadas por los nodos ESP32, valida el payload,
descarta lecturas fuera de rango y guarda el último valor por nodo.
"""
use GenServer
require Logger
@bateria_critica_v 3.4
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def ultimo(nodo), do: GenServer.call(__MODULE__, {:ultimo, nodo})
def registrar(nodo, json), do: GenServer.cast(__MODULE__, {:medicion, nodo, json})
@impl true
def init(_opts), do: {:ok, %{lecturas: %{}, descartes: 0}}
@impl true
def handle_cast({:medicion, nodo, json}, estado) do
case decodificar(json) do
{:ok, %{temp: t, bateria: v}} when t >= -40.0 and t <= 85.0 ->
if v < @bateria_critica_v, do: Logger.warning("nodo #{nodo} con batería baja: #{v} V")
lectura = %{temp: t, bateria: v, recibida_en: System.system_time(:second)}
{:noreply, put_in(estado.lecturas[nodo], lectura)}
{:ok, fuera_de_rango} ->
Logger.warning("lectura descartada de #{nodo}: #{inspect(fuera_de_rango)}")
{:noreply, %{estado | descartes: estado.descartes + 1}}
{:error, motivo} ->
Logger.error("payload inválido de #{nodo}: #{inspect(motivo)}")
{:noreply, %{estado | descartes: estado.descartes + 1}}
end
end
@impl true
def handle_call({:ultimo, nodo}, _from, estado),
do: {:reply, Map.fetch(estado.lecturas, nodo), estado}
defp decodificar(json) do
case Jason.decode(json) do
{:ok, %{"v" => t, "bat" => v}} when is_number(t) and is_number(v) ->
{:ok, %{temp: t / 1.0, bateria: v / 1.0}}
{:ok, otro} -> {:error, {:campos_faltantes, Map.keys(otro)}}
error -> error
end
end
end
El valor de este patrón es que el rango de fallas del microcontrolador queda contenido: si un ESP32 se cuelga o se queda sin batería, el sistema pierde un sensor, no el control. Toda la lógica que necesita evolucionar rápido vive en el nodo Elixir, donde puedes desplegar sin tocar hardware.
STM32: control determinista y consumo bajo control
Cómo leer un número de parte
La familia STM32 tiene más de mil referencias, y el nombre codifica todo lo relevante. Tomemos STM32L476RGT6:
| Segmento | Valor | Significado |
|---|---|---|
STM32 | — | Familia de 32 bits de ST |
L | Serie | L = ultra low power (también F, G, H, U, WB, WL, C) |
4 | Subfamilia | Nivel de rendimiento dentro de la serie |
76 | Variante | Combinación de periféricos |
R | Encapsulado / pines | R = 64 pines (C=48, V=100, Z=144) |
G | Flash | G = 1 MB (8=64 KB, B=128 KB, C=256 KB, E=512 KB) |
T | Paquete | T = LQFP |
6 | Temperatura | 6 = −40 a 85 °C (7 = −40 a 105 °C) |
Saber leer esto ahorra horas: cuando un tutorial dice “usa un STM32F4” y tu placa dice STM32F401CCU6, ya sabes que tienes 256 KB de flash, 48 pines y encapsulado UFQFPN.
Las series y para qué sirve cada una
| Serie | Núcleo | Perfil | Uso típico |
|---|---|---|---|
| C0 | Cortex-M0+ | Costo mínimo | Reemplazo de 8 bits, electrodomésticos simples |
| F0 / F1 / F3 | M0 / M3 / M4 | Propósito general | Placas Blue Pill, control básico, aprendizaje |
| F4 | M4F @ 84–180 MHz | Rendimiento equilibrado | El caballo de batalla de la robótica hobby y semi-profesional |
| F7 / H7 | M7 @ 216–550 MHz | Alto rendimiento | Visión, audio, control multieje, HMI gráfica |
| G0 / G4 | M0+ / M4F | General y control analógico | G4 tiene comparadores, op-amps y ADC rápidos para motores |
| L0 / L1 / L4 / L5 | M0+ / M3 / M4F / M33 | Bajo consumo | Sensores con batería, medidores, wearables |
| U0 / U5 | M0+ / M33 | Bajo consumo de nueva generación | Igual que la L pero con TrustZone y mejores números |
| WB | M4F + M0+ | Inalámbrico | BLE 5, Zigbee, Thread; el M0+ corre la pila de radio |
| WL | M4F + M0+ | Sub-GHz | LoRa y (G)FSK integrados en el chip |
| H5 / N6 | M33 / M55 | Seguridad y edge AI | Certificación, aceleración de redes neuronales |
Para robótica, dos series concentran casi todo: F4 cuando quieres potencia y no te importa el consumo (controladoras de vuelo, drivers de motores paso a paso, impresoras 3D), y G4 cuando el objetivo es control de motores BLDC con lazo de corriente, porque integra amplificadores operacionales y comparadores en el mismo silicio, eliminando componentes externos.
Los periféricos que hacen la diferencia
Lo que distingue a un STM32 de un ESP32 no es la CPU: es la calidad de los periféricos y su capacidad de operar sin la CPU.
Timers avanzados. Un TIM1 o TIM8 genera hasta tres pares de salidas PWM complementarias, con tiempo muerto (dead-time) programable entre el flanco de apagado de una y el de encendido de la otra. Eso es exactamente lo que necesita un inversor trifásico para no cortocircuitar la rama alta y baja de un puente. También tiene una entrada de break que apaga todas las salidas por hardware en un ciclo si se dispara una protección de sobrecorriente, sin que ningún software intervenga. En un ESP32 esa función existe en el MCPWM pero con menos granularidad; en un Arduino sencillamente no existe.
DMA con triggers de hardware. El controlador DMA puede ser disparado por un evento de timer para leer el ADC y depositar el resultado en un buffer circular en RAM, generando una interrupción solo cuando el buffer está a la mitad y al llenarse. El resultado: puedes muestrear a 1 Msps con la CPU casi ociosa. Este patrón —timer dispara ADC, ADC alimenta DMA, DMA notifica en half/full— es la columna vertebral de cualquier lazo de control serio.
ADC de calidad. Los ADC de la serie G4 llegan a 4 Msps con 12 bits, con modo inyectado (una conversión prioritaria que interrumpe la secuencia regular) y watchdog analógico (una interrupción automática si la señal sale de una ventana). Comparado con el ADC del ESP32, que es notoriamente no lineal y necesita calibración por punto, la diferencia es de otra categoría. CAN y CAN-FD, el bus estándar del mundo automotriz e industrial, viene nativo en muchos STM32: sigue necesitando un transceptor externo, pero el controlador de protocolo con filtros de aceptación por hardware está en el chip.
flowchart LR
subgraph SinCPU["Lazo que corre sin intervención de la CPU"]
T[TIM1<br/>PWM complementario<br/>+ dead-time] -->|trigger TRGO| A[ADC1<br/>conversión inyectada<br/>sincronizada al centro del PWM]
A -->|petición DMA| D[DMA1<br/>buffer circular en SRAM]
D -->|IRQ half / full| C[CPU Cortex-M4F<br/>ejecuta el lazo PI<br/>y actualiza CCR]
C -->|escribe registros CCR| T
BRK[Comparador<br/>de sobrecorriente] -->|BREAK| T
end
T --> M[Puente en H<br/>Motor BLDC]
M --> SH[Resistencia shunt] --> A
M --> SH2[Sobrecorriente] --> BRK
Ese diagrama muestra lo que significa “determinista”: el camino crítico de protección (sobrecorriente apaga las salidas) no pasa por ningún software, y el muestreo del ADC ocurre siempre en el mismo instante del ciclo PWM, no cuando el planificador de tareas se acuerda.
Consumo en las series de bajo consumo
Las series L y U son la respuesta de ST al nRF52 en la métrica de energía. Los números de referencia, siempre a confirmar en la hoja de datos del modelo exacto, son estos:
| Modo | STM32L4 (orden de magnitud) | Qué se retiene |
|---|---|---|
| Run a 80 MHz | ~100 µA/MHz desde flash | Todo |
| Low-power run (2 MHz) | ~120 µA total | CPU lenta, periféricos limitados |
| Sleep | ~30 µA/MHz | Periféricos y RAM; la CPU se detiene |
| Stop 2 con RTC | ~1 µA | RAM completa, RTC, algunos periféricos despertables |
| Standby con RTC | ~300 nA | Solo backup registers y RTC |
| Shutdown | ~8 nA | Prácticamente nada; despierta solo por pin o RTC |
Comparado con el ESP32, la escala es otra: el estado de espera más profundo del ESP32 (≈5 µA) es más caro que el modo Stop del STM32L4 con toda la RAM viva. Esto no es defecto del ESP32; es la consecuencia de llevar dos radios y 520 KB de SRAM en el mismo die.
La contrapartida: para tener conectividad hay que agregar un chip. Un STM32L4 con un módulo WiFi externo consume más, cuesta más y ocupa más área que un ESP32-C3 solo. La serie WB y WL existen justamente para cerrar ese hueco con radio integrada.
Toolchain del STM32
| Herramienta | Rol |
|---|---|
| STM32CubeMX | Configurador gráfico: eliges pines, relojes y periféricos, y genera el código de inicialización |
| STM32CubeIDE | IDE basado en Eclipse que integra CubeMX, el compilador y el depurador |
| HAL / LL | Dos capas de biblioteca: HAL es portable y verbosa; LL es delgada y cercana a los registros |
| arm-none-eabi-gcc | El compilador real; también existe la opción de Clang/LLVM |
| OpenOCD / ST-LINK | Puente entre el PC y el chip vía SWD, para grabar y depurar |
| GDB | Depuración paso a paso con breakpoints reales en hardware |
| libopencm3 | Alternativa libre a HAL, más compacta y sin generador de código |
| Zephyr RTOS | Sistema operativo con drivers portables y devicetree |
| embedded-hal (Rust) | Ecosistema Rust con abstracciones de periféricos con verificación en tiempo de compilación |
| AtomVM | Sí, también en STM32: la VM de BEAM corre sobre placas Discovery y Nucleo con suficiente RAM |
Un ciclo de compilación y grabado sin IDE se ve así:
make -j$(nproc) # compilar con el Makefile generado por CubeMX
# Grabar por SWD con OpenOCD (o con st-flash como alternativa)
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
-c "program build/firmware.elf verify reset exit"
st-flash write build/firmware.bin 0x08000000
# Depurar: servidor en una terminal, cliente GDB en otra
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg
arm-none-eabi-gdb build/firmware.elf -ex "target extended-remote :3333"
Los flags de compilación importan más de lo que parece. Un Cortex-M4F requiere decirle al compilador que tiene FPU de precisión simple, o generará llamadas a rutinas de software para cada operación con flotantes:
-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard
Olvidar -mfloat-abi=hard en un lazo de control con PID en punto flotante puede multiplicar por diez el tiempo de ejecución sin ningún error visible.
STM32 y Elixir
Hay tres caminos, en orden de cuán común es cada uno:
- STM32 como esclavo de un nodo Nerves. El STM32 corre el lazo de control duro y expone un protocolo simple por UART o SPI; el nodo Elixir manda consignas y lee telemetría. Es el patrón dominante en robots móviles: el STM32 cierra el lazo de velocidad de las ruedas a 1 kHz, y Elixir planifica la trayectoria a 20 Hz.
- AtomVM sobre STM32. Viable en placas con RAM suficiente (una Discovery F407 o F429 con SDRAM externa). Sirve para lógica de coordinación, no para lazos de microsegundos.
- Generación de código o de configuración desde Elixir. Menos glamoroso pero muy útil: usar Elixir para generar tablas de calibración, mapas de motor o firmware de configuración que después se compilan en el proyecto C.
El protocolo del punto 1 es donde se juega la robustez. Un marco simple, con delimitador, longitud y checksum, evita el 90 % de los problemas de sincronización:
defmodule Puente.Trama do
@moduledoc """
Codificación y decodificación de tramas hacia un STM32 por UART.
Formato:
0xA5 0x5A | LEN (1 byte) | CMD (1 byte) | PAYLOAD (LEN-1 bytes) | CRC8 (1 byte)
El CRC cubre desde LEN hasta el último byte de PAYLOAD.
"""
@sof <<0xA5, 0x5A>>
@polinomio 0x07
@doc "Arma una trama lista para escribir en el puerto serie."
def codificar(cmd, payload) when is_integer(cmd) and is_binary(payload) do
len = byte_size(payload) + 1
cuerpo = <<len::8, cmd::8, payload::binary>>
@sof <> cuerpo <> <<crc8(cuerpo)::8>>
end
@doc """
Extrae todas las tramas completas de un buffer acumulado.
Devuelve {tramas, resto} para que el llamador conserve el remanente.
"""
def decodificar(buffer), do: decodificar(buffer, [])
defp decodificar(<<0xA5, 0x5A, len::8, resto::binary>> = completo, acc) do
if byte_size(resto) >= len + 1 do
<<cuerpo::binary-size(len), crc::8, cola::binary>> = resto
<<cmd::8, payload::binary>> = cuerpo
calculado = crc8(<<len::8>> <> cuerpo)
if calculado == crc do
decodificar(cola, [{:ok, cmd, payload} | acc])
else
# CRC malo: descarta un byte y resincroniza
<<_descartado::8, siguiente::binary>> = completo
decodificar(siguiente, [{:error, :crc} | acc])
end
else
{Enum.reverse(acc), completo}
end
end
defp decodificar(<<0xA5, 0x5A>> = parcial, acc), do: {Enum.reverse(acc), parcial}
defp decodificar(<<0xA5>> = parcial, acc), do: {Enum.reverse(acc), parcial}
defp decodificar(<<_byte::8, resto::binary>>, acc), do: decodificar(resto, acc)
defp decodificar(<<>>, acc), do: {Enum.reverse(acc), <<>>}
@doc "CRC-8 con polinomio 0x07, valor inicial 0."
def crc8(datos), do: crc8(datos, 0)
defp crc8(<<>>, acc), do: acc
defp crc8(<<byte::8, resto::binary>>, acc) do
nuevo =
Enum.reduce(0..7, Bitwise.bxor(acc, byte), fn _, c ->
desplazado = Bitwise.band(Bitwise.bsl(c, 1), 0xFF)
if Bitwise.band(c, 0x80) != 0, do: Bitwise.bxor(desplazado, @polinomio), else: desplazado
end)
crc8(resto, nuevo)
end
end
Este módulo se puede probar en iex sin ningún hardware:
iex> trama = Puente.Trama.codificar(0x10, <<250::16>>)
<<165, 90, 3, 16, 0, 250, 112>>
iex> Puente.Trama.decodificar(trama)
{[{:ok, 16, <<0, 250>>}], ""}
iex> Puente.Trama.decodificar(<<0, 0>> <> trama <> <<165, 90>>)
{[{:ok, 16, <<0, 250>>}], <<165, 90>>}
Fíjate en el tercer caso: basura al inicio y una trama incompleta al final. El decodificador descarta el ruido, entrega lo válido y devuelve el remanente para el próximo bloque de bytes. Ese comportamiento es lo que hace que un enlace serie sobreviva a un reinicio del microcontrolador en medio de una transmisión.
El GenServer que lo usa con Circuits.UART:
defmodule Puente.Serial do
@moduledoc "Enlace UART con el STM32, con reintento de apertura y buffer de resincronización."
use GenServer
require Logger
@reintento_ms 2_000
def start_link(opts), do: GenServer.start_link(__MODULE__, opts, name: __MODULE__)
def enviar(cmd, payload), do: GenServer.cast(__MODULE__, {:enviar, cmd, payload})
@impl true
def init(opts) do
estado = %{
dispositivo: Keyword.fetch!(opts, :dispositivo),
velocidad: Keyword.get(opts, :velocidad, 115_200),
uart: nil,
buffer: <<>>
}
{:ok, estado, {:continue, :abrir}}
end
@impl true
def handle_continue(:abrir, estado) do
{:ok, uart} = Circuits.UART.start_link()
opciones = [speed: estado.velocidad, active: true, framing: Circuits.UART.Framing.None]
case Circuits.UART.open(uart, estado.dispositivo, opciones) do
:ok ->
Logger.info("UART abierto en #{estado.dispositivo}")
{:noreply, %{estado | uart: uart}}
{:error, motivo} ->
Logger.warning("no se pudo abrir #{estado.dispositivo}: #{inspect(motivo)}")
Process.send_after(self(), :reintentar, @reintento_ms)
{:noreply, estado}
end
end
@impl true
def handle_info(:reintentar, estado), do: {:noreply, estado, {:continue, :abrir}}
def handle_info({:circuits_uart, _puerto, datos}, estado) when is_binary(datos) do
{tramas, resto} = Puente.Trama.decodificar(estado.buffer <> datos)
Enum.each(tramas, &procesar/1)
{:noreply, %{estado | buffer: resto}}
end
def handle_info({:circuits_uart, _puerto, {:error, motivo}}, estado) do
Logger.error("error de UART: #{inspect(motivo)}")
Process.send_after(self(), :reintentar, @reintento_ms)
{:noreply, %{estado | uart: nil, buffer: <<>>}}
end
@impl true
def handle_cast({:enviar, _cmd, _payload}, %{uart: nil} = estado) do
Logger.warning("descartando envío: UART cerrado")
{:noreply, estado}
end
def handle_cast({:enviar, cmd, payload}, estado) do
:ok = Circuits.UART.write(estado.uart, Puente.Trama.codificar(cmd, payload))
{:noreply, estado}
end
defp procesar({:ok, 0x20, <<izq::signed-16, der::signed-16>>}),
do: Logger.debug("velocidades: izq=#{izq} der=#{der}")
defp procesar({:ok, cmd, payload}),
do: Logger.debug("comando #{cmd} sin manejador, #{byte_size(payload)} bytes")
defp procesar({:error, :crc}), do: Logger.warning("trama con CRC inválido descartada")
end
nRF52 DK: radio de bajo consumo y la placa para aprenderla
Qué trae la placa de desarrollo
El nRF52 DK (referencia de fábrica PCA10040) es la placa oficial de Nordic para los SoC nRF52832, nRF52810 y nRF52805. Incluye:
- El SoC principal con su antena de 2,4 GHz y el circuito de adaptación ya diseñado.
- Un depurador SEGGER J-Link OB integrado. No necesitas comprar una sonda: el mismo cable USB graba, depura y entrega una consola serie virtual.
- Conectores compatibles con Arduino Uno Revisión 3 para usar shields de terceros, más 4 botones y 4 LEDs para experimentar sin cablear nada.
- Antena NFC con conector, para habilitar la función de etiqueta NFC-A (útil para el emparejamiento por toque).
- Alimentación por USB o por pila CR2032, más pines dedicados para medir corriente insertando un amperímetro en serie. Ese detalle es más importante de lo que parece: sin un punto de medición, caracterizar el consumo real de un firmware es adivinanza.
- Cabecera para depurar un chip externo, convirtiendo la placa en programador de tu propio diseño.
Ojo con una confusión frecuente: nRF52 DK y nRF52840 DK son placas distintas. La segunda (PCA10056) lleva el nRF52840, que tiene más memoria, USB nativo, radio 802.15.4 y BLE Long Range. Si un tutorial usa Thread o Zigbee, necesitas la 52840.
Los SoC de la familia nRF52
| SoC | Núcleo | Flash / RAM | Radio | Rasgo |
|---|---|---|---|---|
| nRF52805 | Cortex-M4 @ 64 MHz | 192 KB / 24 KB | BLE 5 | El más pequeño y económico |
| nRF52810 | Cortex-M4 @ 64 MHz | 192 KB / 24 KB | BLE 5 | Sin FPU, sin NFC |
| nRF52811 | Cortex-M4 @ 64 MHz | 192 KB / 24 KB | BLE 5 + 802.15.4 | Direction finding |
| nRF52820 | Cortex-M4 @ 64 MHz | 256 KB / 32 KB | BLE 5.2 | USB device |
| nRF52832 | Cortex-M4F @ 64 MHz | 512 KB / 64 KB | BLE 5, ANT, NFC-A | El equilibrado; el que trae el DK |
| nRF52833 | Cortex-M4F @ 64 MHz | 512 KB / 128 KB | BLE 5.1 + 802.15.4 | Rango extendido de temperatura (105 °C) |
| nRF52840 | Cortex-M4F @ 64 MHz | 1 MB / 256 KB | BLE 5 Long Range + 802.15.4 | USB 2.0, CryptoCell-310 |
Todos comparten el mismo núcleo y la misma frecuencia. Lo que cambia es memoria, radios adicionales y periféricos. Esto es deliberado: el código que escribes para un 52832 migra casi sin cambios a un 52840 si el proyecto crece.
Dos periféricos exclusivos del ecosistema Nordic merecen atención porque son la razón técnica de su eficiencia:
- PPI (Programmable Peripheral Interconnect): una matriz que conecta el evento de un periférico con la tarea de otro, en hardware. Ejemplo: el evento “comparación del timer” dispara directamente la tarea “iniciar conversión del ADC”, sin que la CPU despierte. Con PPI se pueden construir cadenas completas de adquisición que funcionan con el procesador dormido.
- EasyDMA: casi todos los periféricos (SPI, UART, ADC, I2S) tienen acceso directo a la RAM. Combinado con PPI, permite un ciclo de “muestrear 100 valores por SPI y guardarlos en RAM” con la CPU apagada todo el tiempo.
Un ESP32 haría lo mismo con la CPU despierta a 20 mA. El nRF52 lo hace a decenas de microamperes. Ahí está la diferencia de dos órdenes de magnitud.
Cómo funciona BLE, en el nivel que necesitas
Bluetooth Low Energy no es “Bluetooth más lento”. Es un protocolo distinto que comparte la banda de 2,4 GHz. Su modelo mental tiene dos capas que hay que separar.
GAP (Generic Access Profile) define quién habla con quién: un dispositivo periférico (tu sensor) emite paquetes de advertising cada cierto intervalo; un dispositivo central (tu teléfono o tu nodo Nerves) escanea, encuentra el anuncio y decide conectarse.
GATT (Generic Attribute Profile) define qué se dice una vez conectados: los datos se organizan en servicios, y cada servicio contiene características, que son valores con un UUID, permisos y propiedades (leer, escribir, notificar).
sequenceDiagram
participant P as nRF52 (periférico)
participant C as Central (Nerves + BLE)
Note over P: dormido, radio apagada (~1,9 uA)
loop cada intervalo de advertising (p. ej. 1 s)
P->>P: despierta ~1 ms
P-->>C: ADV_IND en canales 37, 38, 39
P->>P: vuelve a dormir
end
C->>P: CONNECT_IND (solicita conexión)
Note over P,C: se negocia connection interval,<br/>slave latency y supervision timeout
C->>P: descubrimiento de servicios GATT
P-->>C: servicio 0x181A (Environmental Sensing)
C->>P: escribe 0x0001 en el CCCD de la característica
Note over C: activa notificaciones
loop cada connection interval
P->>P: despierta, escucha el anchor point
alt hay dato nuevo
P-->>C: NOTIFY temperatura = 23.4 C
else sin datos
P-->>C: paquete vacío (o se salta por slave latency)
end
P->>P: vuelve a dormir
end
C->>P: desconexión
Note over P: vuelve a advertising
Tres parámetros gobiernan el consumo de un enlace BLE, y conviene entenderlos porque son la palanca principal de diseño:
- Connection interval: cada cuánto se encuentran central y periférico. Va de 7,5 ms a 4 s en pasos de 1,25 ms. Más corto significa menor latencia y más consumo.
- Slave latency: cuántos eventos de conexión puede saltarse el periférico si no tiene nada que decir. Con un intervalo de 100 ms y una latencia de 9, el sensor solo despierta cada segundo, pero si necesita enviar algo urgente puede hacerlo en el próximo evento.
- Supervision timeout: cuánto se espera sin comunicación antes de declarar la conexión perdida.
Un sensor de temperatura bien configurado usa intervalo largo y slave latency alta: el enlace se siente instantáneo cuando hay datos y cuesta casi nada cuando no los hay.
El consumo del nRF52 con números
| Estado | Corriente típica (nRF52832, 3 V, DC/DC activo) |
|---|---|
| CPU ejecutando desde flash | ~52 µA/MHz (≈3,3 mA a 64 MHz) |
| Radio transmitiendo a 0 dBm | ~5,3 mA |
| Radio transmitiendo a +4 dBm | ~7,5 mA |
| Radio recibiendo (1 Mbps) | ~5,4 mA |
| System ON, idle, con RAM retenida y RTC | ~1,9 µA |
| System OFF con retención de RAM | ~0,7 µA |
| System OFF sin retención | ~0,3 µA |
Lo relevante no es ninguna cifra aislada sino la carga por evento. Un evento de advertising completo —despertar, encender el oscilador, transmitir en tres canales, apagar— consume del orden de decenas de microcoulombs. Multiplicado por la frecuencia de eventos, da la corriente media.
Ese cálculo se hace mejor con código que a mano. El siguiente módulo es Elixir puro y corre en cualquier iex:
defmodule Energia.Presupuesto do
@moduledoc """
Estima la corriente media y la vida de una batería a partir de un ciclo
de trabajo descrito como lista de fases. Cada fase aporta carga
(corriente x tiempo) al ciclo completo.
"""
defstruct nombre: "", corriente_ma: 0.0, duracion_ms: 0.0
@doc "Crea una fase del ciclo."
def fase(nombre, corriente_ma, duracion_ms) do
%__MODULE__{nombre: nombre, corriente_ma: corriente_ma / 1.0, duracion_ms: duracion_ms / 1.0}
end
@doc """
Carga total del ciclo en microcoulombs, corriente media en microamperes
y desglose ordenado por peso. `periodo_s` incluye el tiempo dormido.
"""
def analizar(fases, periodo_s) when is_list(fases) and periodo_s > 0 do
periodo_ms = periodo_s * 1000.0
tiempo_activo = Enum.reduce(fases, 0.0, &(&1.duracion_ms + &2))
# mA x ms = microcoulombs
carga_uc = Enum.reduce(fases, 0.0, &(&1.corriente_ma * &1.duracion_ms + &2))
if tiempo_activo > periodo_ms do
{:error, {:ciclo_imposible, tiempo_activo, periodo_ms}}
else
desglose =
fases
|> Enum.map(fn f ->
aporte = f.corriente_ma * f.duracion_ms
%{fase: f.nombre, carga_uc: redondear(aporte, 2), porcentaje: redondear(aporte / carga_uc * 100.0, 1)}
end)
|> Enum.sort_by(& &1.porcentaje, :desc)
{:ok,
%{
carga_por_ciclo_uc: redondear(carga_uc, 2),
corriente_media_ua: redondear(carga_uc / periodo_ms * 1000.0, 2),
ciclo_util_pct: redondear(tiempo_activo / periodo_ms * 100.0, 3),
desglose: desglose
}}
end
end
@doc """
Vida estimada en días. `factor_util` descuenta autodescarga y el hecho
de que no se aprovecha el 100 % de la carga nominal (0,7 es conservador).
"""
def vida_dias(corriente_media_ua, capacidad_mah, factor_util \\ 0.7)
def vida_dias(ua, _cap, _f) when ua <= 0.0, do: {:error, :corriente_invalida}
def vida_dias(corriente_media_ua, capacidad_mah, factor_util) do
horas = capacidad_mah * factor_util / (corriente_media_ua / 1000.0)
{:ok, redondear(horas / 24.0, 1)}
end
defp redondear(valor, decimales), do: Float.round(valor * 1.0, decimales)
end
Ahora comparemos los tres candidatos para el mismo requisito: publicar una temperatura una vez por minuto, alimentado por batería.
alias Energia.Presupuesto, as: P
# --- nRF52832 anunciando por BLE, sin conexión permanente ---
nrf = [
P.fase("despertar + oscilador", 3.0, 0.4),
P.fase("lectura del sensor I2C", 1.2, 5.0),
P.fase("advertising en 3 canales", 5.3, 1.5),
P.fase("dormido (System ON idle)", 0.0019, 59_992.0)
]
{:ok, r_nrf} = P.analizar(nrf, 60) # corriente_media_ua ≈ 2,1
{:ok, dias_nrf} = P.vida_dias(r_nrf.corriente_media_ua, 220)
# CR2032 de 220 mAh: miles de días; el límite pasa a ser la autodescarga de la pila
# --- ESP32 despertando, conectándose a WiFi y publicando por MQTT ---
esp = [
P.fase("arranque desde deep-sleep", 60.0, 300.0),
P.fase("lectura del sensor I2C", 25.0, 5.0),
P.fase("asociación WiFi + DHCP", 120.0, 1_800.0),
P.fase("handshake TLS + PUBLISH", 160.0, 700.0),
P.fase("deep-sleep", 0.010, 57_195.0)
]
{:ok, r_esp} = P.analizar(esp, 60) # corriente_media_ua ≈ 5.900 (unos 5,9 mA)
{:ok, dias_esp} = P.vida_dias(r_esp.corriente_media_ua, 2000)
# 18650 de 2000 mAh: del orden de 9 a 10 días
# --- STM32L4 con radio LoRa externa ---
stm = [
P.fase("despertar desde Stop 2", 2.0, 0.1),
P.fase("lectura del sensor I2C", 1.5, 5.0),
P.fase("transmisión LoRa SF7", 45.0, 60.0),
P.fase("Stop 2 con RTC", 0.001, 59_934.9)
]
{:ok, r_stm} = P.analizar(stm, 60) # corriente_media_ua ≈ 45
El resultado no es sutil: para el mismo requisito funcional, el consumo medio varía en un factor de casi 3000 entre el nRF52 y el ESP32. La conclusión práctica es que la elección de radio determina la vida de la batería mucho más que la elección de CPU, y que el ESP32 con WiFi y batería solo es viable si el intervalo de reporte es largo o si hay energía disponible (panel solar, USB, red). El desglose que devuelve el módulo agrega el matiz importante: en el caso del ESP32, la asociación WiFi es el rubro dominante, así que antes de cambiar de chip conviene atacar ese número —guardar el canal y el BSSID en la RAM RTC, usar IP estática, reanudar sesión TLS—, porque ahí se puede recortar más de la mitad.
Toolchain del nRF52
Nordic estandarizó todo en el nRF Connect SDK, que es Zephyr RTOS más los componentes propietarios de Nordic (pila BLE, gestión de energía, actualización por aire). La gestión del árbol de repositorios se hace con west.
# Instalar la herramienta de gestión de workspaces
pip install --user west
# Inicializar el workspace del SDK en una versión fija
west init -m https://github.com/nrfconnect/sdk-nrf --mr v2.6.0 ~/ncs
cd ~/ncs && west update
west zephyr-export
# Compilar un ejemplo de periférico BLE para la placa nRF52 DK
cd ~/ncs/nrf/samples/bluetooth/peripheral_lbs
west build -b nrf52dk_nrf52832
# Grabar por el J-Link integrado en la placa
west flash
# Consola serie del dispositivo
minicom -D /dev/ttyACM0 -b 115200
El nombre de la placa (nrf52dk_nrf52832) cambió en versiones recientes de Zephyr al formato nrf52dk/nrf52832; si el build falla diciendo que no encuentra la placa, ese suele ser el motivo. Zephyr configura el hardware mediante devicetree: un archivo declarativo que describe qué periféricos existen, en qué pines y con qué parámetros. Un overlay para agregar un sensor I2C se ve así:
/* boards/nrf52dk_nrf52832.overlay */
&i2c0 {
status = "okay";
clock-frequency = <I2C_BITRATE_STANDARD>;
sht3xd: sht3xd@44 {
compatible = "sensirion,sht3xd";
reg = <0x44>;
};
};
Las funcionalidades del firmware se activan aparte, con Kconfig, en el archivo prj.conf: CONFIG_BT=y y CONFIG_BT_PERIPHERAL=y habilitan la pila BLE en rol periférico, CONFIG_BT_DEVICE_NAME="SensorSala" define el nombre que se anuncia, CONFIG_SENSOR=y y CONFIG_I2C=y incorporan los drivers, y CONFIG_PM_DEVICE=y habilita la gestión de energía por dispositivo.
Esta separación —hardware en devicetree, funcionalidades en Kconfig, lógica en C— es el modelo de Zephyr y es lo que hace que un mismo código funcione en un nRF52 y en un STM32 sin cambios.
nRF52 y Elixir: el central corre en Nerves
No hay una VM de BEAM para nRF52 y probablemente no la haya: 64 KB de RAM no alcanzan. La integración con Elixir es siempre desde el otro lado del enlace: el nRF52 es el periférico BLE, y un nodo Nerves con un adaptador Bluetooth es el central.
En el ecosistema Elixir existe BlueHeron, una implementación de la pila Bluetooth Host que habla HCI directamente con un controlador (por UART o USB) sin depender de BlueZ; también es posible usar BlueZ vía D-Bus si el sistema Nerves lo incluye. La estructura de la aplicación no depende de cuál elijas. Lo importante es aislar la parte inestable —el enlace de radio— detrás de un proceso supervisado que pueda morir y renacer sin arrastrar al resto:
defmodule Sala.Supervisor do
@moduledoc "Árbol de supervisión del nodo central."
use Supervisor
def start_link(opts), do: Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
@impl true
def init(_opts) do
hijos = [
# Almacena el último valor de cada sensor; sobrevive a caídas del enlace.
Sala.Registro,
# El enlace BLE: si el adaptador desaparece, este proceso muere y se reinicia.
{Sala.EnlaceBle, dispositivos: ["EA:1F:0C:9A:22:31"]},
# Publica al exterior; no depende del enlace.
Sala.ApiWeb
]
# rest_for_one: si el Registro cae, se reinician también los que dependen de él.
Supervisor.init(hijos, strategy: :rest_for_one)
end
end
Sala.Registro puede ser un Agent simple que guarde la última medición por dirección BLE junto con su marca de tiempo; con eso, detectar sensores caídos es filtrar los que llevan más de N segundos sin reportar. Lo relevante del árbol es el orden: el registro va primero porque los demás dependen de él, y :rest_for_one garantiza que una caída del enlace de radio no borre el estado acumulado.
La decodificación del payload BLE sí es responsabilidad tuya y sí es puro Elixir verificable. Las características estándar de Bluetooth SIG tienen formatos definidos; por ejemplo, la característica Temperature (UUID 0x2A6E) del servicio Environmental Sensing es un entero con signo de 16 bits en centésimas de grado, little-endian:
defmodule Sala.Gatt do
@moduledoc "Decodificadores de características GATT estándar de Bluetooth SIG."
@doc """
Característica Temperature (0x2A6E): sint16, unidad 0,01 °C, little-endian.
iex> Sala.Gatt.temperatura(<<0x26, 0x09>>)
{:ok, 23.42}
iex> Sala.Gatt.temperatura(<<0xD8, 0xFC>>)
{:ok, -8.4}
"""
def temperatura(<<centesimas::little-signed-16>>), do: {:ok, centesimas / 100.0}
def temperatura(otro), do: {:error, {:longitud_invalida, byte_size(otro)}}
@doc """
Característica Humidity (0x2A6F): uint16, unidad 0,01 %.
iex> Sala.Gatt.humedad(<<0x10, 0x17>>)
{:ok, 59.36}
"""
def humedad(<<centesimas::little-unsigned-16>>), do: {:ok, centesimas / 100.0}
def humedad(otro), do: {:error, {:longitud_invalida, byte_size(otro)}}
@doc """
Característica Battery Level (0x2A19): uint8, porcentaje entero de 0 a 100.
iex> Sala.Gatt.bateria(<<87>>)
{:ok, 87}
iex> Sala.Gatt.bateria(<<200>>)
{:error, {:fuera_de_rango, 200}}
"""
def bateria(<<nivel::unsigned-8>>) when nivel <= 100, do: {:ok, nivel}
def bateria(<<nivel::unsigned-8>>), do: {:error, {:fuera_de_rango, nivel}}
def bateria(otro), do: {:error, {:longitud_invalida, byte_size(otro)}}
end
Estos decodificadores son la parte del sistema que más se rompe en la práctica —un firmware nuevo cambia el formato y todo deja de funcionar— y son también la más fácil de testear: son funciones puras sobre binarios, sin hardware de por medio.
Tabla comparativa maestra
| Dimensión | ESP32 (clásico / S3) | STM32 (F4 / L4) | nRF52832 |
|---|---|---|---|
| Núcleo | Xtensa LX6/LX7, 2 × 240 MHz | Cortex-M4F, 80–180 MHz | Cortex-M4F, 64 MHz |
| RAM interna | 520 KB | 96–320 KB según modelo | 64 KB |
| Flash | Externa, típicamente 4–16 MB | Interna, 64 KB – 2 MB | Interna, 512 KB |
| WiFi | Sí, integrado | No (salvo módulo externo) | No |
| Bluetooth | BR/EDR + BLE (BLE 5 en S3/C3) | Solo serie WB | BLE 5 + ANT + NFC-A |
| Otras radios | 802.15.4 en C6/H2 | Sub-GHz en serie WL | 2,4 GHz propietario |
| Consumo dormido | ~10 µA (deep sleep) | ~1 µA (Stop 2, L4) | ~1,9 µA (System ON idle) |
| Consumo activo típico | 30–260 mA | 3–40 mA | 3,3 mA + 5,3 mA de radio |
| Determinismo de timing | Medio (FreeRTOS, radio interrumpe) | Alto (timers avanzados, DMA) | Alto (PPI, EasyDMA) |
| Calidad del ADC | Baja, requiere calibración | Alta, hasta 4 Msps en G4 | Media-alta, SAADC 12 bits |
| Control de motores | MCPWM con dead-time | El mejor de los tres (TIM1/TIM8, break) | Básico |
| Depurador | JTAG con sonda externa | SWD, sondas baratas y ubicuas | J-Link OB en la placa |
| Toolchain principal | ESP-IDF (FreeRTOS) | CubeIDE / HAL / Zephyr | nRF Connect SDK (Zephyr) |
| Elixir en el chip | Sí, con AtomVM | Sí, con AtomVM en placas con RAM | No |
| Elixir como central/host | Nerves + MQTT | Nerves + UART/SPI/CAN | Nerves + BlueHeron/BlueZ |
| Costo típico del módulo | Bajo | Medio | Medio-alto |
| Certificación de radio | Módulos precertificados | No aplica (sin radio) | Módulos precertificados |
| Donde brilla | Nodo conectado a la nube | Lazo de control industrial | Sensor a pila que dura años |
Un método para elegir, en vez de una corazonada
La elección de microcontrolador se hace mal cuando se decide por familiaridad (“siempre uso ESP32”) o por precio de la placa. Un método reproducible pesa los requisitos reales y hace explícitas las restricciones que descalifican.
flowchart TD
A[Requisitos del nodo] --> B{¿Necesita IP nativo<br/>WiFi o Ethernet?}
B -->|Sí| C{¿Alimentado por red<br/>o batería grande?}
C -->|Sí| D[ESP32-S3 o C6]
C -->|No, pila pequeña| E{¿Puede reportar<br/>cada varios minutos?}
E -->|Sí| F[ESP32-C3 con deep-sleep<br/>y conexión optimizada]
E -->|No| G[Replantear: gateway BLE/Thread<br/>+ nRF52 en el sensor]
B -->|No| H{¿Timing duro<br/>menor a 100 us<br/>o control de motor?}
H -->|Sí| I{¿Bajo consumo<br/>también crítico?}
I -->|No| J[STM32F4 o G4]
I -->|Sí| K[STM32L4 o U5]
H -->|No| L{¿Enlace inalámbrico<br/>de bajo consumo?}
L -->|BLE / Thread / Zigbee| M[nRF52832 o nRF52840]
L -->|LoRa / sub-GHz| N[STM32WL]
L -->|No, solo cableado| O{¿Restricción de costo<br/>por unidad?}
O -->|Alta| P[STM32C0 o G0]
O -->|Media| Q[STM32F4 o ESP32-C3]
D --> Z[Verificar: certificación,<br/>disponibilidad, ciclo de vida,<br/>toolchain que el equipo domina]
F --> Z
G --> Z
J --> Z
K --> Z
M --> Z
N --> Z
P --> Z
Q --> Z
El diagrama ordena la conversación, pero los proyectos reales tienen más de un eje en tensión. Para eso sirve una puntuación ponderada, que además obliga a escribir los pesos y así discutirlos:
defmodule Seleccion.Mcu do
@moduledoc """
Selección de microcontrolador por puntuación ponderada con restricciones duras.
Los criterios se puntúan de 0 a 10 por candidato. Los pesos expresan
cuánto importa cada criterio en ESTE proyecto y deben sumar 1.0.
Las restricciones duras descalifican candidatos sin importar su puntaje.
"""
@candidatos %{
esp32_s3: %{
nombre: "ESP32-S3",
criterios: %{conectividad_ip: 10, consumo: 3, determinismo: 5, costo: 8, ecosistema: 9, control_motor: 6},
capacidades: [:wifi, :ble, :usb, :atomvm, :dual_core]
},
esp32_c6: %{
nombre: "ESP32-C6",
criterios: %{conectividad_ip: 10, consumo: 5, determinismo: 5, costo: 8, ecosistema: 7, control_motor: 5},
capacidades: [:wifi, :ble, :thread, :zigbee, :atomvm]
},
stm32g4: %{
nombre: "STM32G4",
criterios: %{conectividad_ip: 1, consumo: 6, determinismo: 10, costo: 6, ecosistema: 8, control_motor: 10},
capacidades: [:can, :timers_avanzados, :adc_rapido, :opamp]
},
stm32l4: %{
nombre: "STM32L4",
criterios: %{conectividad_ip: 1, consumo: 9, determinismo: 9, costo: 6, ecosistema: 8, control_motor: 7},
capacidades: [:can, :timers_avanzados, :bajo_consumo_profundo]
},
nrf52832: %{
nombre: "nRF52832",
criterios: %{conectividad_ip: 2, consumo: 10, determinismo: 8, costo: 5, ecosistema: 7, control_motor: 3},
capacidades: [:ble, :nfc, :ppi, :bajo_consumo_profundo]
},
nrf52840: %{
nombre: "nRF52840",
criterios: %{conectividad_ip: 3, consumo: 9, determinismo: 8, costo: 4, ecosistema: 7, control_motor: 3},
capacidades: [:ble, :thread, :zigbee, :usb, :nfc, :ppi, :bajo_consumo_profundo]
}
}
@doc """
Evalúa los candidatos. Opciones: `:pesos` (mapa criterio => peso, deben
sumar 1.0) y `:requiere` (lista de capacidades obligatorias).
iex> {:ok, r} = Seleccion.Mcu.evaluar(
...> pesos: %{conectividad_ip: 0.1, consumo: 0.45, determinismo: 0.1,
...> costo: 0.15, ecosistema: 0.1, control_motor: 0.1},
...> requiere: [:ble])
iex> hd(r).nombre
"nRF52832"
"""
def evaluar(opts \\ []) do
pesos = Keyword.get(opts, :pesos, pesos_por_defecto())
requeridas = Keyword.get(opts, :requiere, [])
with :ok <- validar_pesos(pesos) do
resultado =
@candidatos
|> Enum.filter(fn {_k, c} -> Enum.all?(requeridas, &(&1 in c.capacidades)) end)
|> Enum.map(fn {clave, c} ->
puntaje =
Enum.reduce(pesos, 0.0, fn {criterio, peso}, acc ->
acc + Map.get(c.criterios, criterio, 0) * peso
end)
%{clave: clave, nombre: c.nombre, puntaje: Float.round(puntaje, 2)}
end)
|> Enum.sort_by(& &1.puntaje, :desc)
case resultado do
[] -> {:error, {:sin_candidatos, requeridas}}
lista -> {:ok, lista}
end
end
end
@doc "Imprime la evaluación como ranking con barras."
def informe(opts \\ []) do
case evaluar(opts) do
{:ok, lista} ->
Enum.each(lista, fn r ->
IO.puts(String.pad_trailing(r.nombre, 12) <> String.pad_leading("#{r.puntaje}", 6) <> " " <> String.duplicate("#", round(r.puntaje)))
end)
{:error, motivo} ->
IO.puts("sin resultados: #{inspect(motivo)}")
end
end
defp pesos_por_defecto do
%{conectividad_ip: 0.2, consumo: 0.2, determinismo: 0.2, costo: 0.15, ecosistema: 0.15, control_motor: 0.1}
end
defp validar_pesos(pesos) do
suma = pesos |> Map.values() |> Enum.sum()
if abs(suma - 1.0) < 0.001, do: :ok, else: {:error, {:pesos_no_suman_uno, suma}}
end
end
Tres escenarios distintos con el mismo módulo:
# Escenario A: sensor de temperatura a pila que debe durar dos años
Seleccion.Mcu.informe(
pesos: %{conectividad_ip: 0.05, consumo: 0.5, determinismo: 0.05, costo: 0.2, ecosistema: 0.1, control_motor: 0.1},
requiere: [:bajo_consumo_profundo])
# Escenario B: controlador de un robot móvil con dos motores BLDC
Seleccion.Mcu.informe(
pesos: %{conectividad_ip: 0.05, consumo: 0.05, determinismo: 0.35, costo: 0.15, ecosistema: 0.1, control_motor: 0.3},
requiere: [:timers_avanzados])
# Escenario C: nodo de domótica que habla con la nube y con una malla Thread
Seleccion.Mcu.informe(
pesos: %{conectividad_ip: 0.4, consumo: 0.15, determinismo: 0.1, costo: 0.2, ecosistema: 0.15, control_motor: 0.0},
requiere: [:wifi, :thread])
El escenario C es interesante porque la restricción [:wifi, :thread] deja un solo candidato en la lista: el ESP32-C6. Cuando una lista de requisitos colapsa a un candidato, eso es información valiosa: significa que la decisión ya estaba tomada por los requisitos, no por preferencia. Con todo, hay criterios que ninguna tabla de puntaje captura y que deciden proyectos reales:
- Disponibilidad y ciclo de vida. Un chip excelente que tiene 40 semanas de plazo de entrega no sirve. ST y Nordic publican compromisos de longevidad de 10 o 15 años; para un producto industrial eso es un requisito, no un lujo.
- Certificación de radio. Diseñar tu propia antena implica certificar el equipo completo. Usar un módulo precertificado (ESP32-WROOM, un módulo Nordic de Fanstel o Insight) traslada gran parte de ese costo al proveedor.
- Qué domina el equipo. Un STM32 mal usado por alguien que aprende Zephyr desde cero rinde peor que un ESP32 bien usado por alguien con tres años de ESP-IDF.
- Herramientas de depuración. El J-Link OB del nRF52 DK y el ST-LINK del STM32 permiten breakpoints reales en hardware. En un ESP32 la depuración por JTAG existe pero es menos habitual: mucha gente depura con
printf, lo cual es aceptable hasta que deja de serlo.
Errores comunes
| Error | Causa | Solución |
|---|---|---|
| El ESP32 se reinicia al activar el WiFi | El pico de corriente de transmisión (hasta 500 mA instantáneos) hace caer la tensión del regulador o del USB del PC | Alimentar con una fuente de al menos 1 A y poner un condensador electrolítico de 470 µF cerca del módulo |
| Las lecturas del ADC del ESP32 dan 0 o basura con red activa | El canal usado pertenece al ADC2, que queda inhabilitado mientras el WiFi está en uso | Mover todas las señales analógicas a canales del ADC1 (GPIO 32 a 39) en el esquemático |
| El ESP32 no entra en modo de grabación | El circuito de autoreset del conversor USB-serie no funciona en clones con CH340 | Mantener BOOT presionado, tocar EN, soltar BOOT; o soldar los condensadores de autoreset |
| Un GPIO del ESP32 no responde | Se usó un pin del rango 6–11 (conectado a la flash) o 34–39 (solo entrada, sin pull-up) | Consultar el mapa de pines del módulo antes de asignar funciones |
| Un sensor de 5 V mata el ESP32 o el nRF52 | Los GPIO no son tolerantes a 5 V | Usar un conversor de nivel bidireccional o un divisor resistivo en las entradas |
| El PID del STM32 corre mucho más lento de lo esperado | Se compiló sin -mfloat-abi=hard, así que la FPU no se usa y las operaciones flotantes van por software | Agregar -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard y verificar que las bibliotecas enlazadas usen la misma ABI |
| El STM32 arranca a una frecuencia mucho menor que la nominal | El PLL no se configuró y el chip sigue corriendo desde el oscilador interno (HSI) | Configurar el árbol de relojes en CubeMX y verificar el valor real leyendo SystemCoreClock |
| El DMA del STM32 no transfiere nada | El buffer está en una región de RAM que ese controlador DMA no puede alcanzar (típico en H7 con DTCM) | Ubicar el buffer en una sección de RAM accesible mediante el script del linker |
| El PWM de motor produce cortocircuitos en el puente | Se usó PWM simple sin tiempo muerto entre las ramas alta y baja | Usar un timer avanzado (TIM1/TIM8) con salidas complementarias y dead-time configurado |
| El nRF52 no aparece al escanear | El intervalo de advertising es muy largo o la potencia de transmisión está en el mínimo | Reducir el intervalo durante el emparejamiento y volver a subirlo después de conectar |
| La conexión BLE se cae de forma intermitente | El supervision timeout es demasiado corto para el intervalo y la slave latency configurados | Verificar la relación: el timeout debe superar (1 + latency) × interval × 2 con margen |
| El consumo medido del nRF52 es diez veces el esperado | Un periférico quedó habilitado (UART, un pin con pull-up contra tierra, el regulador LDO en vez del DC/DC) | Deshabilitar periféricos no usados, revisar el estado de cada GPIO al dormir y activar el convertidor DC/DC |
west build falla porque no encuentra la placa | El identificador de placa cambió de formato entre versiones de Zephyr | Usar la nomenclatura de la versión instalada (nrf52dk_nrf52832 o nrf52dk/nrf52832) |
| Nerves no compila para ESP32 | Nerves construye Linux embebido y el ESP32 no tiene MMU ni RAM suficiente | Usar AtomVM en el ESP32, o el ESP32 como periférico de un nodo Nerves sobre hardware con Linux |
| La aplicación AtomVM se queda sin memoria | Se usaron estructuras de datos grandes o binarios que no caben en la RAM disponible | Procesar por trozos, evitar acumular listas y medir con :erlang.system_info/1 |
| El enlace UART entre Elixir y el microcontrolador se desincroniza | Se está leyendo por líneas o sin delimitadores, y un reinicio deja bytes a medias | Usar tramas con byte de inicio, longitud y checksum, y descartar bytes hasta resincronizar |
Ejercicios propuestos
-
Presupuesto energético comparado. Usando el módulo
Energia.Presupuesto, modela un nodo que reporta cada 5 minutos en tres variantes: ESP32 con WiFi, nRF52 con BLE, y STM32L4 con LoRa. Calcula la vida de la batería para una CR2032 (220 mAh), una AA de litio (3000 mAh) y una 18650 (2500 mAh). Presenta el resultado como tabla y explica en qué punto cada combinación deja de tener sentido. -
Optimización del ciclo de conexión. Toma el caso del ESP32 del ejemplo y crea variantes que reduzcan la fase de asociación WiFi: IP estática (elimina DHCP, ≈300 ms), BSSID y canal cacheados (≈600 ms menos de escaneo), sesión TLS reanudada (≈400 ms menos). Calcula cuánto mejora la vida de la batería con cada optimización acumulada y en cuál conviene invertir primero.
-
Extender el selector. Agrega al módulo
Seleccion.Mculos candidatosstm32wb(BLE integrado) ystm32wl(LoRa integrado), con sus criterios y capacidades. Después agrega un criterio nuevo,certificacion, y verifica cómo cambia el ranking del escenario de domótica cuando ese criterio pesa 0,25. -
Decodificadores GATT con propiedades. Escribe pruebas basadas en propiedades para
Sala.Gattusando StreamData: para cualquier entero de 16 bits con signo, codificarlo y decodificarlo debe devolver el valor original dividido por 100. Agrega el decodificador de la característica Pressure (0x2A6D), que es un uint32 en unidades de 0,1 pascal. -
Protocolo robusto a fallas. Extiende
Puente.Tramacon un número de secuencia de 8 bits y un mecanismo de confirmación. Escribe una prueba que simule pérdida de tramas al azar y verifique que el emisor reintenta hasta tres veces antes de reportar el error hacia arriba. -
Elección justificada. Toma un proyecto propio y escribe una página con: los requisitos duros, los pesos de cada criterio con su justificación, la salida de
Seleccion.Mcu.informe/1, y una explicación de por qué el ganador del puntaje es o no es la elección final. Si la elección final difiere del puntaje, identifica qué criterio no estaba modelado. -
Topología híbrida. Diseña en un diagrama Mermaid un sistema de invernadero con: cuatro nodos nRF52 midiendo humedad de suelo a pila, un STM32 controlando bombas y válvulas con lazo cerrado, un ESP32 como gateway BLE a WiFi, y un nodo Nerves ejecutando la lógica de riego y la interfaz web. Indica el protocolo de cada enlace y qué pasa cuando cada uno de los nodos falla.
Lo que viene
Este capítulo cubrió el rango de los microcontroladores: dispositivos sin sistema operativo de propósito general, con memoria medida en kilobytes, capaces de responder en microsegundos y de dormir con corrientes de microamperes. Vimos que la elección entre ESP32, STM32 y nRF52 se reduce a qué requisito domina —conectividad, determinismo o energía— y que en un sistema real suelen convivir los tres, con un nodo Elixir coordinándolos. Pero hay problemas que ningún microcontrolador resuelve. Ejecutar una red neuronal sobre video en tiempo real, procesar una nube de puntos de un LiDAR, o implementar un lazo de control con miles de canales simultáneos requiere otra categoría de hardware. En el capítulo 9 subimos al siguiente escalón: los módulos NVIDIA Jetson con su GPU para inferencia, las plataformas AMD Kria que combinan procesador y lógica programable, las FPGA como recurso de cómputo paralelo reconfigurable, y los ASIC como destino final cuando el volumen lo justifica. Ahí también cerraremos el mapa completo de decisión, desde un microcontrolador de un dólar hasta un chip diseñado a medida, para que sepas exactamente en qué punto de esa escala vive cada uno de tus proyectos. El índice completo del curso está en /tecnologias/elixir-robotics/00-indice/.