Computo avanzado en el borde: Jetson, Kria, FPGA y ASIC
Computo avanzado en el borde: Jetson, Kria, FPGA y ASIC
En el capitulo 8 recorrimos las tres familias de microcontroladores que resuelven la mayoria de los proyectos reales: ESP32 cuando hace falta conectividad inalambrica barata, STM32 cuando hace falta un catalogo profesional con soporte a diez años, nRF52 cuando la bateria manda. Los tres comparten una caracteristica: son maquinas secuenciales pequeñas. Tienen un nucleo (o dos), unos cientos de kilobytes de RAM y ejecutan una instruccion a la vez, muy rapido, pero una a la vez.
Esa arquitectura falla ante cierta clase de problemas. Un ESP32 puede leer diez sensores, hablar MQTT y mover dos servos sin despeinarse. No puede segmentar una imagen de 1920x1080 a treinta cuadros por segundo, ni correr un filtro de Kalman extendido sobre una nube de puntos LiDAR, ni demodular ocho canales de radio simultaneos con latencia de microsegundos. Para eso hace falta otra cosa: paralelismo real, en hardware, no simulado por un planificador de tareas.
Este capitulo cubre las cuatro plataformas que aportan ese paralelismo, ordenadas de mas flexible a mas rigida: los modulos Nvidia Jetson, que traen una GPU con miles de nucleos junto a la CPU; los AMD Kria, sistemas en modulo que ponen procesadores ARM y logica programable en el mismo chip; las FPGA puras, donde uno describe circuitos en lugar de programas; y los ASIC, silicio fabricado a medida que no se puede cambiar nunca mas. Al final construimos algo mas util que la suma de las descripciones: un metodo de decision con rubrica ponderada, implementado en Elixir y ejecutable, para responder con criterio la pregunta que abre todo proyecto: ¿que controlador uso aqui?
El continuo del computo: de la logica fija al software puro
Antes de mirar cada plataforma conviene ubicarlas en un eje. Hay una tension fundamental en electronica digital entre flexibilidad y eficiencia, y todas las tecnologias de este capitulo son puntos distintos de ese mismo compromiso.
En un extremo esta el ASIC: el circuito esta grabado fisicamente en el silicio, cada transistor existe para hacer una cosa, no sobra nada. Es lo mas rapido y lo que menos consume por operacion util, y es absolutamente inmutable. En el otro extremo esta un procesador de proposito general corriendo software interpretado: puede hacer cualquier cosa, se cambia en un segundo, y desperdicia enormes cantidades de energia decodificando instrucciones, moviendo datos entre cachés y prediciendo saltos.
flowchart LR
A["ASIC<br/>Silicio fijo"] --> B["FPGA<br/>Logica reconfigurable"]
B --> C["SoC + FPGA<br/>Kria, Zynq"]
C --> D["SoC + GPU/NPU<br/>Jetson"]
D --> E["SoC Linux<br/>Raspberry Pi"]
E --> F["Microcontrolador<br/>ESP32, STM32"]
F --> G["MCU + VM<br/>AtomVM, MicroPython"]
A -.->|"maxima eficiencia<br/>por operacion"| A
G -.->|"maxima flexibilidad<br/>de cambio"| G
style A fill:#7c2d12,color:#fff
style B fill:#9a3412,color:#fff
style C fill:#b45309,color:#fff
style D fill:#0369a1,color:#fff
style E fill:#0284c7,color:#fff
style F fill:#0891b2,color:#fff
style G fill:#0d9488,color:#fff
Leer ese diagrama de izquierda a derecha es leer una escala de costo de cambiar de opinion. Modificar un ASIC cuesta millones y meses. Modificar el bitstream de una FPGA cuesta una compilacion de veinte minutos. Modificar el firmware de un microcontrolador cuesta tres segundos de grabado. Modificar un modulo de Elixir corriendo sobre la BEAM cuesta, literalmente, nada: se recarga en caliente sin detener el sistema.
Leerlo de derecha a izquierda es leer una escala de energia por operacion. Ese es el eje que importa en el borde, donde la bateria o el disipador son finitos.
Por que el computo se mueve al borde
Durante una decada la respuesta por defecto fue “manda los datos a la nube y que alla decidan”. Para robotica eso no funciona, y las razones son concretas:
- Latencia. Un robot movil a 2 m/s recorre 20 cm en 100 ms. Un viaje de ida y vuelta a un centro de datos, con red movil, ronda esos 100 ms en el mejor caso y varios cientos en el peor. Un lazo de control de evasion de obstaculos no tolera esa ventana.
- Ancho de banda. Una camara 1080p a 30 fps sin comprimir genera cerca de 1.5 Gbit/s. Comprimida a H.264 con calidad utilizable, unos 8 Mbit/s sostenidos por camara. Multiplicalo por cuatro camaras y por cien robots.
- Disponibilidad. El robot tiene que seguir funcionando cuando se cae el enlace. Una planta industrial con el techo lleno de metal es un ambiente hostil para el WiFi.
- Privacidad y regulacion. Video de personas, audio de conversaciones, telemetria medica. Procesarlo en el dispositivo y transmitir solo metadatos cambia por completo el perfil de cumplimiento.
- Costo operativo. Inferencia en la nube se paga por invocacion, para siempre. Inferencia en el borde se paga una vez, en la compra del modulo.
La consecuencia practica: en el robot vive una computadora capaz de correr modelos de vision y sensor fusion, y la nube queda para lo que solo la nube puede hacer, que es agregar datos de toda la flota y entrenar.
Nvidia Jetson: GPU y aceleradores dentro del robot
Un modulo Jetson no es una tarjeta grafica pequeña. Es un sistema en modulo (SoM) completo, del tamaño de una tarjeta de credito o menos, que integra en un solo paquete varios procesadores especializados que comparten memoria.
Que hay adentro de un Jetson
Los modulos de la familia comparten una estructura conceptual, aunque cambien las cantidades:
- Un cluster de CPU ARM de 64 bits (Cortex-A78AE en la generacion Orin, nucleos Carmel propios en Xavier). Aqui corre Linux, tus servicios, tu logica de negocio, tu aplicacion de Elixir.
- Una GPU con arquitectura CUDA (Ampere en Orin, Volta en Xavier, Blackwell en la generacion Thor). No es solo para dibujar: es el motor de computo paralelo de proposito general que ejecuta las multiplicaciones de matrices de una red neuronal.
- Nucleos Tensor, unidades dentro de la GPU dedicadas a operaciones de matriz en precision reducida (FP16, INT8, FP8, FP4 segun generacion). Son la razon por la que un modulo de 25 W hace inferencia comparable a una GPU de escritorio de hace pocos años.
- Aceleradores de aprendizaje profundo (DLA) en los modulos mayores: bloques de funcion fija para capas convolucionales. Descargan trabajo de la GPU y consumen menos por inferencia.
- ISP (Image Signal Processor), que convierte los datos crudos del sensor de camara en imagen utilizable sin gastar CPU.
- Codificadores y decodificadores de video por hardware, para H.264, H.265 y AV1 segun modelo.
- Memoria unificada: CPU y GPU comparten el mismo banco de LPDDR. Esto elimina las copias explicitas de host a dispositivo que dominan la programacion CUDA en PC, y es una de las particularidades importantes al portar codigo.
flowchart TB
subgraph SoM["Modulo Jetson"]
CPU["CPU ARM 64-bit<br/>Linux, servicios, Elixir/BEAM"]
GPU["GPU CUDA<br/>+ nucleos Tensor"]
DLA["DLA<br/>acelerador de inferencia"]
ISP["ISP<br/>procesado de imagen"]
NVENC["Codec de video<br/>NVENC / NVDEC"]
MEM[("Memoria LPDDR<br/>unificada")]
CPU <--> MEM
GPU <--> MEM
DLA <--> MEM
ISP --> MEM
NVENC <--> MEM
end
CAM["Camaras MIPI CSI-2"] --> ISP
SENS["LiDAR, IMU, USB, Ethernet"] --> CPU
CPU --> ACT["Actuadores via GPIO,<br/>UART, CAN, I2C"]
style SoM fill:#0c4a6e,color:#fff
style MEM fill:#155e75,color:#fff
Las familias del catalogo
Nvidia organiza los Jetson en series que escalan en computo, memoria y consumo. La forma correcta de compararlos no es por “TOPS” a secas sino por TOPS por vatio y por memoria disponible, porque en la practica lo que te bloquea es que el modelo no quepa en RAM.
| Serie | Rol tipico | Perfil de consumo | Uso representativo |
|---|---|---|---|
| Jetson Orin Nano | Entrada a IA en el borde | Decenas de vatios en el extremo bajo | Robot educativo, camara inteligente, un modelo de vision a la vez |
| Jetson Orin NX | Punto medio de densidad | Intermedio | Drone de inspeccion, AMR pequeño, varios flujos de camara |
| Jetson AGX Xavier | Generacion previa, aun vigente | Configurable en modos de potencia | Sistemas industriales desplegados, soporte de largo plazo |
| Jetson AGX Orin | Alta densidad de computo | El mas alto de la generacion Orin | Vehiculo autonomo, robot movil con multiples sensores y modelos concurrentes |
| Jetson AGX Thor | Robotica humanoide y fisica | Clase estacion de trabajo embebida | Modelos grandes de vision-lenguaje-accion en el robot |
Un detalle que sorprende a quien viene del mundo del microcontrolador: el consumo de un Jetson es configurable por software. El modulo expone perfiles de potencia que limitan frecuencias de CPU y GPU y cantidad de nucleos activos.
# Consultar el perfil de potencia activo y los disponibles
sudo nvpmodel -q --verbose
# Cambiar al perfil 0, que normalmente es el de maximo rendimiento
sudo nvpmodel -m 0
# Fijar los relojes al maximo del perfil actual (desactiva el escalado dinamico)
sudo jetson_clocks
# Monitorear en vivo CPU, GPU, memoria, temperaturas y consumo
tegrastats
Esos cuatro comandos son parte del sistema base y son la herramienta principal para caracterizar un diseño: se mide el consumo real con tegrastats mientras corre la carga de trabajo, y se elige el perfil mas bajo que aun cumple la tasa de cuadros objetivo. Un robot a bateria puede duplicar su autonomia solo con esta decision.
La pila de software: JetPack, TensorRT e Isaac
El hardware sin la pila de software no sirve de mucho. Nvidia distribuye JetPack, que instala sobre el modulo:
- Linux for Tegra (L4T), una distribucion basada en Ubuntu con el kernel y los controladores del SoC.
- CUDA, el modelo de programacion paralela.
- cuDNN, primitivas optimizadas de redes neuronales.
- TensorRT, el compilador y motor de inferencia. Este es el componente decisivo: toma un modelo entrenado (normalmente en formato ONNX), fusiona capas, elige kernels optimizados para ese chip exacto, cuantiza a INT8 o FP16 y produce un motor serializado. La ganancia frente a correr el modelo con el framework de entrenamiento suele ser de varias veces en latencia.
- VPI, biblioteca de vision con implementaciones que corren en CPU, GPU o aceleradores segun se pida.
- DeepStream, para tuberias de video multi-camara.
Sobre eso vive Isaac, la plataforma de robotica: Isaac Sim para simulacion fisica y generacion de datos sinteticos, Isaac Lab para entrenamiento de politicas por aprendizaje por refuerzo, e Isaac ROS como conjunto de paquetes ROS 2 acelerados por GPU para odometria visual, estimacion de pose, segmentacion y navegacion.
El flujo tipico de un modelo de vision desde el entrenamiento hasta el robot:
sequenceDiagram
participant D as Estacion de entrenamiento
participant O as Modelo ONNX
participant T as trtexec / TensorRT
participant J as Jetson en el robot
participant E as Supervisor Elixir
D->>O: Exporta el modelo entrenado
O->>T: Se copia al Jetson destino
Note over T: La compilacion se hace EN el<br/>mismo modelo de Jetson que<br/>ejecutara, no en el PC
T->>T: Fusion de capas, calibracion INT8,<br/>seleccion de kernels
T->>J: Motor .engine serializado
E->>J: Inicia el proceso de inferencia
loop Cada cuadro
J->>J: Captura, preprocesa, infiere
J-->>E: Detecciones (JSON por stdout)
E->>E: Decide, publica, actua
end
Note over E,J: Si el proceso muere,<br/>el supervisor lo reinicia
El punto marcado en el diagrama merece enfasis: el motor de TensorRT se compila en el hardware de destino. Un .engine generado en un AGX Orin no es valido en un Orin Nano. Esto rompe la intuicion de “compilo una vez y despliego a toda la flota” y hay que planificarlo en la canalizacion de construccion.
Donde encaja Elixir en un Jetson
Un Jetson corre Ubuntu sobre ARM64. Eso significa que Erlang y Elixir corren nativamente, sin adaptaciones: se instala el runtime, se compila el proyecto y funciona. La pregunta interesante no es si Elixir corre, sino que rol le conviene.
Elixir no va a competir con CUDA en multiplicar matrices. Lo que aporta es exactamente lo que le falta a una pila de vision escrita en C++ y Python: supervision, tolerancia a fallos, concurrencia ordenada y distribucion. El patron que mejor funciona es tratar cada componente pesado como un proceso del sistema operativo bajo un Port, con Elixir como capa de coordinacion.
Este es un ejemplo completo y ejecutable de ese patron. Primero el proceso de inferencia, en Python, que simula el ciclo de captura y deteccion emitiendo una linea JSON por cuadro:
#!/usr/bin/env python3
"""detector.py - Proceso de inferencia. Emite una linea JSON por cuadro.
En un sistema real, aqui se abriria la camara, se pasaria el cuadro por
el motor TensorRT y se publicarian las detecciones. La estructura del
bucle y del protocolo de salida es la misma.
"""
import json
import math
import sys
import time
def main():
cuadro = 0
inicio = time.monotonic()
while True:
cuadro += 1
t = time.monotonic() - inicio
# Sustituto de la inferencia real: un objeto que oscila en X.
confianza = 0.55 + 0.4 * abs(math.sin(t / 3.0))
detecciones = [{
"clase": "persona",
"confianza": round(confianza, 3),
"caja": [round(320 + 200 * math.sin(t), 1), 240.0, 120.0, 300.0],
}]
salida = {
"cuadro": cuadro,
"t_ms": round(t * 1000, 1),
"detecciones": detecciones,
}
sys.stdout.write(json.dumps(salida) + "\n")
sys.stdout.flush() # imprescindible: sin esto Elixir no recibe nada
time.sleep(1 / 30) # 30 cuadros por segundo
if __name__ == "__main__":
try:
main()
except (BrokenPipeError, KeyboardInterrupt):
sys.exit(0)
Y ahora el supervisor en Elixir que lo arranca, lo vigila, interpreta su salida y lo reinicia si muere:
defmodule Vision.Detector do
@moduledoc """
Envuelve el proceso externo de inferencia en un GenServer.
Responsabilidades:
- lanzar el proceso y mantenerlo vivo
- decodificar cada linea JSON en un evento del sistema
- aplicar el umbral de confianza y publicar detecciones utiles
- medir la tasa efectiva de cuadros
"""
use GenServer
require Logger
@umbral 0.75
# --- API publica ---
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc "Ultima deteccion aceptada, o nil."
def ultima, do: GenServer.call(__MODULE__, :ultima)
@doc "Cuadros por segundo medidos sobre la ventana reciente."
def fps, do: GenServer.call(__MODULE__, :fps)
# --- Callbacks ---
@impl true
def init(opts) do
script = Keyword.get(opts, :script, "detector.py")
Process.flag(:trap_exit, true)
{:ok, abrir_puerto(%{puerto: nil, script: script, ultima: nil, marcas: []})}
end
defp abrir_puerto(estado) do
puerto =
Port.open({:spawn_executable, System.find_executable("python3")}, [
:binary,
:exit_status,
{:line, 8192},
{:args, [estado.script]}
])
Logger.info("detector iniciado: #{estado.script}")
%{estado | puerto: puerto}
end
@impl true
def handle_info({puerto, {:data, {:eol, linea}}}, %{puerto: puerto} = estado) do
ahora = System.monotonic_time(:millisecond)
marcas = [ahora | estado.marcas] |> Enum.take_while(&(ahora - &1 < 1000))
case Jason.decode(linea) do
{:ok, %{"detecciones" => dets}} ->
aceptadas = Enum.filter(dets, &(&1["confianza"] >= @umbral))
estado = %{estado | marcas: marcas}
case aceptadas do
[] ->
{:noreply, estado}
[mejor | _] ->
Phoenix.PubSub.broadcast(Vision.PubSub, "detecciones", {:deteccion, mejor})
{:noreply, %{estado | ultima: mejor}}
end
{:error, _} ->
Logger.warning("linea no decodificable del detector")
{:noreply, %{estado | marcas: marcas}}
end
end
# El proceso externo termino: se reinicia tras una pausa corta.
def handle_info({puerto, {:exit_status, codigo}}, %{puerto: puerto} = estado) do
Logger.error("detector termino con codigo #{codigo}, reiniciando")
Process.sleep(500)
{:noreply, abrir_puerto(%{estado | puerto: nil})}
end
def handle_info(_otro, estado), do: {:noreply, estado}
@impl true
def handle_call(:ultima, _from, estado), do: {:reply, estado.ultima, estado}
def handle_call(:fps, _from, estado), do: {:reply, length(estado.marcas), estado}
@impl true
def terminate(_motivo, %{puerto: puerto}) when is_port(puerto) do
Port.close(puerto)
:ok
end
def terminate(_motivo, _estado), do: :ok
end
Lo que este codigo compra es concreto. Si el proceso de inferencia se cae por un error de memoria de CUDA, vuelve solo en medio segundo, y el resto del robot —el control de motores, la telemetria, la interfaz web— nunca se entera. Si mañana hay dos camaras, se lanzan dos instancias bajo el mismo supervisor. Si el robot necesita reportar a una estacion base, la BEAM ya sabe distribuirse entre nodos.
Para computo numerico en el propio Elixir, el ecosistema tiene Nx con el backend EXLA, que compila expresiones tensoriales a codigo nativo y puede usar la GPU:
# mix.exs
# {:nx, "~> 0.7"},
# {:exla, "~> 0.7"}
defmodule Fusion do
import Nx.Defn
@doc """
Paso de prediccion de un filtro de Kalman lineal.
x: vector de estado, P: covarianza, F: transicion, Q: ruido de proceso.
"""
defn predecir(x, p, f, q) do
x_pred = Nx.dot(f, x)
p_pred = Nx.add(Nx.dot(Nx.dot(f, p), Nx.transpose(f)), q)
{x_pred, p_pred}
end
end
# Uso:
# x = Nx.tensor([[0.0], [0.0]])
# p = Nx.eye(2)
# f = Nx.tensor([[1.0, 0.1], [0.0, 1.0]])
# q = Nx.multiply(Nx.eye(2), 0.01)
# {x1, p1} = Fusion.predecir(x, p, f, q)
Para inferencia directa desde Elixir sin proceso externo, Ortex expone ONNX Runtime como NIF y Evision expone OpenCV. Ambas rutas son validas; la eleccion depende de si el modelo ya esta optimizado con TensorRT (entonces conviene el Port a un proceso que use la API de Nvidia) o si basta ONNX Runtime.
AMD Kria: procesador y logica programable en el mismo silicio
Los Kria son sistemas en modulo de AMD, y su interes esta en que resuelven un problema distinto al del Jetson. Un Jetson acelera cargas que se expresan bien como algebra lineal masiva: redes neuronales, principalmente. Un Kria acelera cargas que necesitan latencia deterministica y protocolos a medida: control de motores a decenas de kilohertz, adquisicion sincrona de multiples sensores, interfaces de camara no estandar, procesamiento de señal con jitter acotado.
La arquitectura de dos mitades
Un Kria K26 esta construido alrededor de un dispositivo de la familia Zynq UltraScale+ MPSoC. La idea central de esa familia es que el chip tiene dos regiones interconectadas:
- El Processing System (PS): procesadores ARM reales, grabados en silicio fijo. Un cluster de aplicacion Cortex-A53 de 64 bits que corre Linux, y un cluster de tiempo real Cortex-R5F pensado para lazos de control con latencia garantizada. Ademas GPU Mali para grafica basica, controladores de memoria, Ethernet, USB, SD.
- La Programmable Logic (PL): la FPGA propiamente tal. Cientos de miles de celdas logicas, bloques de RAM, y bloques DSP para multiplicar y acumular.
Entre ambas mitades corre un bus AXI de alto ancho de banda. Eso permite un reparto natural: Linux en el PS gestiona la red, el sistema de archivos y la logica de aplicacion; la PL implementa los bloques que necesitan responder en nanosegundos; y ambos se comunican por registros mapeados en memoria y transferencias DMA.
flowchart TB
subgraph K26["AMD Kria K26 SOM"]
subgraph PS["Processing System (silicio fijo)"]
A53["4x Cortex-A53<br/>Linux, aplicacion, red"]
R5["2x Cortex-R5F<br/>tiempo real duro"]
DDR[("DDR4 en el modulo")]
end
AXI{{"Interconexion AXI"}}
subgraph PL["Programmable Logic (FPGA)"]
PIPE["Tuberia de vision<br/>o control de motor"]
DSP["Bloques DSP<br/>MAC paralelos"]
BRAM[("Block RAM")]
IO["I/O a medida<br/>(protocolos propios)"]
end
A53 <--> AXI
R5 <--> AXI
AXI <--> PIPE
PIPE <--> DSP
PIPE <--> BRAM
PIPE <--> IO
AXI <--> DDR
end
SENS["Encoders, camaras,<br/>ADC rapidos"] --> IO
A53 --> NET["Ethernet / aplicacion"]
style PS fill:#166534,color:#fff
style PL fill:#854d0e,color:#fff
style AXI fill:#1e3a8a,color:#fff
K24 y K26: dos perfiles
| Aspecto | Kria K24 SOM | Kria K26 SOM |
|---|---|---|
| Orientacion | Control de motores y procesamiento de señal digital | Vision con IA, robotica, industria |
| Perfil de logica | Menor, optimizado para lazos de control y DSP | Mayor, para tuberias de vision completas |
| Consumo tipico | Mas bajo, apunta a accionamientos compactos | Mas alto, permite mas paralelismo simultaneo |
| Kit de evaluacion asociado | Kit orientado a accionamiento de motor | KV260 (vision) y KR260 (robotica) |
| Caso emblematico | Servoaccionamiento con control de corriente a alta frecuencia | Camara multiflujo con deteccion y seguimiento |
Los kits de evaluacion importan mas de lo habitual aqui. El KV260 Vision AI Starter Kit y el KR260 Robotics Starter Kit traen el SoM montado en una placa portadora con conectores utiles (camara, Ethernet, Pmod, y en el KR260 tambien interfaces pensadas para accionamientos y sensores industriales), y permiten empezar sin diseñar la placa base.
El modelo de aplicaciones aceleradas
La barrera historica de la FPGA es su curva de aprendizaje. AMD ataca ese problema en Kria con un modelo de aplicaciones aceleradas empaquetadas: en lugar de sintetizar un diseño desde cero, uno instala una aplicacion que trae su bitstream y su software, y la carga en tiempo de ejecucion. La herramienta de linea de comandos del sistema Kria gestiona ese ciclo:
# Listar las aplicaciones aceleradas instaladas en el sistema
sudo xmutil listapps
# Descargar la aplicacion actualmente cargada en la logica programable
sudo xmutil unloadapp
# Cargar una aplicacion por su nombre
sudo xmutil loadapp kv260-smartcam
# Ver el estado de la plataforma
sudo xmutil boardid
Esto convierte a la logica programable en algo casi tan dinamico como el software: se descarga un diseño y se carga otro sin reiniciar. Un robot podria tener un bitstream para “modo navegacion” con una tuberia de vision estereo, y otro para “modo manipulacion” con controladores de servo de alta frecuencia, e intercambiarlos segun la tarea.
Para desarrollar sin escribir HDL, la ruta habitual es PYNQ, que expone los bloques de la logica programable como objetos de Python:
#!/usr/bin/env python3
"""Uso de un overlay PYNQ: la logica programable se manipula como objetos.
El overlay (.bit + .hwh) describe que bloques hay en la FPGA. PYNQ los
descubre y crea atributos para cada uno.
"""
from pynq import Overlay, allocate
import numpy as np
# Carga el bitstream en la logica programable
overlay = Overlay("/home/root/overlays/filtro.bit")
# Los IP del diseño aparecen como atributos; se listan asi:
print(overlay.ip_dict.keys())
# Buffers accesibles por el DMA: memoria fisicamente contigua
entrada = allocate(shape=(1024,), dtype=np.int32)
salida = allocate(shape=(1024,), dtype=np.int32)
entrada[:] = np.arange(1024, dtype=np.int32)
dma = overlay.axi_dma_0
dma.sendchannel.transfer(entrada)
dma.recvchannel.transfer(salida)
dma.sendchannel.wait()
dma.recvchannel.wait()
print("primeros resultados:", salida[:8])
entrada.freebuffer()
salida.freebuffer()
Ese allocate no es un detalle menor: la FPGA accede a memoria por direccion fisica, sin pasar por la MMU del procesador, asi que el buffer tiene que ser contiguo y no paginable. Es el tipo de detalle que separa el mundo del software del mundo del hardware, y aparece constantemente al trabajar con logica programable.
Desde Elixir, un Kria se trata igual que un Jetson: Linux sobre ARM64, la BEAM corre nativamente, y la aceleracion se alcanza por Port o NIF hacia el codigo que toca la logica programable.
FPGA: describir circuitos en lugar de escribir programas
Vale la pena detenerse en la FPGA por si misma, porque es la tecnologia que mas cambia la forma de pensar.
De que esta hecha
Una FPGA (Field Programmable Gate Array, matriz de compuertas programable en campo) es un circuito integrado que contiene una malla de recursos genericos que uno interconecta a voluntad:
- LUT (Look-Up Table): una tablita de verdad programable, tipicamente de 6 entradas y 1 o 2 salidas. Con ella se implementa cualquier funcion booleana de esas entradas. Es el ladrillo basico de la logica combinacional.
- Flip-flops: elementos de memoria de un bit sincronizados con el reloj. Son los que dan estado al circuito.
- Bloques DSP: multiplicadores-acumuladores dedicados. Sintetizar una multiplicacion con LUTs es caro; los bloques DSP la hacen en un ciclo y son el recurso que limita cuantos filtros o cuantas capas convolucionales caben.
- Block RAM (BRAM): bloques de memoria de doble puerto distribuidos por el chip, de unos pocos kilobits cada uno.
- Interconexion programable: la malla de rutas y conmutadores que decide que se conecta con que. Esta es, casi siempre, el recurso que se agota primero y el que determina la frecuencia maxima alcanzable.
- Recursos de reloj: PLL y MMCM que generan y distribuyen relojes limpios a todo el chip, y redes de reloj dedicadas de bajo sesgo.
- Bloques de E/S: pines configurables en distintos estandares electricos, con serializadores para señales rapidas.
Cuando uno “programa” una FPGA no carga instrucciones: carga un bitstream, un archivo que configura el estado de cada LUT, cada flip-flop y cada punto de la interconexion. Al terminar la carga, el chip es el circuito descrito.
La diferencia mental respecto de un microcontrolador
Esta tabla resume el cambio de modelo:
| Dimension | Microcontrolador | FPGA |
|---|---|---|
| Unidad de trabajo | Instruccion ejecutada en el tiempo | Compuerta existente en el espacio |
| Concurrencia | Simulada por interrupciones o RTOS | Fisica: todo ocurre a la vez, siempre |
| Agregar funcionalidad | Cuesta tiempo de CPU | Cuesta area del chip |
| Latencia | Variable, depende del planificador | Fija, contada en ciclos de reloj |
| Lenguaje | C, C++, Rust, Python, Elixir | VHDL, Verilog, SystemVerilog, Chisel |
| Depuracion | Puntos de ruptura, printf | Analizador logico integrado, simulacion de formas de onda |
| Ciclo de compilacion | Segundos | Minutos a horas |
| Errores tipicos | Desbordamiento de pila, puntero nulo | Violacion de temporizacion, latch inferido, cruce de dominios de reloj |
La frase clave: en una FPGA, agregar un segundo canal de procesamiento no hace mas lento al primero, siempre que quede area. Ese es el motivo por el que un radar, una tarjeta de adquisicion de 64 canales o un conmutador de red usan FPGA y no un procesador.
Verilog: un ejemplo completo
Este es un generador de PWM sintetizable, con resolucion y frecuencia parametrizables. Cumple las reglas de estilo que evitan los errores clasicos: reset sincrono, asignacion no bloqueante en logica secuencial, todo dentro de un unico dominio de reloj.
// pwm.v - Generador PWM parametrizable.
// La salida esta en alto mientras el contador es menor que el ciclo de trabajo.
module pwm #(
parameter integer ANCHO = 8 // resolucion en bits
) (
input wire clk, // reloj del sistema
input wire rst_n, // reset activo en bajo, sincrono
input wire enable, // habilita la salida
input wire [ANCHO-1:0] ciclo, // ciclo de trabajo, 0 .. 2^ANCHO-1
output reg salida
);
reg [ANCHO-1:0] contador;
always @(posedge clk) begin
if (!rst_n) begin
contador <= {ANCHO{1'b0}};
salida <= 1'b0;
end else if (!enable) begin
contador <= {ANCHO{1'b0}};
salida <= 1'b0;
end else begin
contador <= contador + 1'b1; // envuelve solo
salida <= (contador < ciclo) ? 1'b1 : 1'b0;
end
end
endmodule
Un banco de pruebas para simularlo, ejecutable con Icarus Verilog, que es libre y se instala en cualquier maquina:
// pwm_tb.v - Banco de pruebas del generador PWM.
`timescale 1ns/1ps
module pwm_tb;
localparam integer ANCHO = 8;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg enable = 1'b0;
reg [ANCHO-1:0] ciclo = 8'd0;
wire salida;
integer altos = 0;
integer total = 0;
pwm #(.ANCHO(ANCHO)) dut (
.clk(clk), .rst_n(rst_n), .enable(enable),
.ciclo(ciclo), .salida(salida)
);
always #5 clk = ~clk; // reloj de 100 MHz (periodo 10 ns)
// Medicion del ciclo de trabajo efectivo
always @(posedge clk) begin
if (enable) begin
total <= total + 1;
if (salida) altos <= altos + 1;
end
end
initial begin
$dumpfile("pwm_tb.vcd");
$dumpvars(0, pwm_tb);
repeat (4) @(posedge clk);
rst_n = 1'b1;
ciclo = 8'd64; // 64/256 = 25 %
enable = 1'b1;
repeat (256 * 4) @(posedge clk); // cuatro periodos completos
$display("ciclos altos = %0d de %0d -> %0.2f %%",
altos, total, (100.0 * altos) / total);
if (altos * 100 / total > 20 && altos * 100 / total < 30)
$display("RESULTADO: correcto");
else
$display("RESULTADO: fuera de rango");
$finish;
end
endmodule
Se compila y ejecuta asi:
# Instalacion en Debian/Ubuntu: sudo apt install iverilog gtkwave
iverilog -o pwm_sim pwm.v pwm_tb.v
vvp pwm_sim
# Para inspeccionar las formas de onda generadas:
gtkwave pwm_tb.vcd
Poder simular sin hardware es central en este mundo. El ciclo de sintesis en una FPGA grande tarda de minutos a horas; el ciclo de simulacion tarda segundos. Todo el desarrollo funcional ocurre en simulacion, y la sintesis solo se corre cuando el diseño ya se comporta bien.
Chisel: describir hardware desde un lenguaje moderno
Escribir Verilog directamente es tedioso para diseños parametricos. Chisel es un lenguaje especifico de dominio embebido en Scala: uno escribe programas en Scala que, al ejecutarse, construyen un grafo de circuito, y ese grafo se emite como Verilog. La generacion del hardware ocurre en tiempo de elaboracion, con todo el poder del lenguaje anfitrion disponible.
// Contador.scala - Contador parametrizable descrito en Chisel.
import chisel3._
import chisel3.util._
class Contador(ancho: Int, tope: Int) extends Module {
require(ancho > 0, "el ancho debe ser positivo")
require(tope < (1 << ancho), s"tope $tope no cabe en $ancho bits")
val io = IO(new Bundle {
val habilitar = Input(Bool())
val borrar = Input(Bool())
val valor = Output(UInt(ancho.W))
val desborde = Output(Bool())
})
val reg = RegInit(0.U(ancho.W))
val fin = reg === tope.U
when(io.borrar) {
reg := 0.U
}.elsewhen(io.habilitar) {
reg := Mux(fin, 0.U, reg + 1.U)
}
io.valor := reg
io.desborde := io.habilitar && fin
}
La ventaja aparece cuando se generan familias de modulos: un bucle de Scala puede instanciar dieciseis filtros con coeficientes distintos calculados en tiempo de elaboracion, algo que en Verilog exigiria macros o generadores externos. La desventaja es una capa mas de herramientas entre uno y el Verilog final, y mensajes de error que a veces hablan de Scala cuando el problema es de hardware.
El flujo de herramientas de una FPGA
Este es el camino desde el codigo hasta el chip configurado, y donde se rompe cada cosa:
flowchart TD
HDL["Codigo HDL<br/>Verilog / VHDL / Chisel"] --> SIM["Simulacion funcional<br/>banco de pruebas"]
SIM -->|"comportamiento incorrecto"| HDL
SIM --> SYN["Sintesis<br/>HDL a netlist de LUT y FF"]
SYN -->|"latch inferido,<br/>logica no sintetizable"| HDL
SYN --> CONS["Restricciones<br/>reloj, pines, temporizacion"]
CONS --> PNR["Emplazamiento y ruteo<br/>place and route"]
PNR -->|"no cabe: se agotaron<br/>LUT, DSP o rutas"| HDL
PNR --> STA["Analisis estatico<br/>de temporizacion"]
STA -->|"slack negativo:<br/>no llega a la frecuencia"| HDL
STA --> BIT["Generacion del bitstream"]
BIT --> CARGA["Carga en la FPGA<br/>JTAG o flash de arranque"]
CARGA --> LAB["Verificacion en hardware<br/>analizador logico integrado"]
LAB -->|"falla solo en el chip:<br/>cruce de dominios de reloj"| HDL
style HDL fill:#1e3a8a,color:#fff
style STA fill:#7c2d12,color:#fff
style LAB fill:#166534,color:#fff
Las tres flechas de retorno señalan los tres puntos donde se pierde tiempo de verdad:
- La sintesis rechaza el codigo porque describe algo que no es un circuito. El caso clasico es el latch inferido: un bloque combinacional donde no todas las ramas asignan la salida, asi que la herramienta infiere un elemento de memoria transparente que nadie pidio.
- El emplazamiento y ruteo no converge porque el diseño no cabe o no se puede rutear. La solucion casi nunca es “comprar una FPGA mas grande”: es rediseñar para compartir recursos, por ejemplo multiplexar un solo bloque DSP entre varios canales en lugar de instanciar uno por canal.
- El analisis de temporizacion da slack negativo: existe un camino combinacional tan largo que la señal no alcanza a estabilizarse antes del siguiente flanco de reloj. Se arregla con pipelining, insertando registros que parten el camino en tramos mas cortos, a costa de latencia en ciclos.
Y el error que solo aparece en hardware, nunca en simulacion: el cruce de dominios de reloj. Cuando una señal generada con un reloj se muestrea con otro reloj no relacionado, el flip-flop puede quedar en un estado metaestable, ni alto ni bajo, por un tiempo indefinido. Se manifiesta como fallas aleatorias cada varias horas. La solucion estandar son sincronizadores de dos flip-flops para señales de un bit, y FIFO asincronos con codigo Gray para buses.
Un caso ilustrativo: MiSTer FPGA
MiSTer es un proyecto de codigo abierto que reimplementa consolas y maquinas arcade completas dentro de una FPGA. No es un emulador: no hay software interpretando instrucciones de otro procesador. Hay una descripcion del hardware original —el procesador, los chips de video, los de sonido, la logica de bus— sintetizada como circuito real.
Sirve como ejemplo pedagogico porque muestra exactamente para que sirve la reconfigurabilidad: la misma FPGA carga un bitstream distinto por sistema, y el comportamiento a nivel de ciclo es indistinguible del original, incluyendo la latencia de entrada. Un emulador por software introduce buffers y variabilidad; el circuito no.
ASIC: cuando el diseño se convierte en silicio
Un ASIC (Application-Specific Integrated Circuit) es un chip fabricado para hacer una cosa. Su diseño se graba fisicamente en las mascaras de fotolitografia, y una vez fabricado no cambia jamas.
Que se gana y que se pierde
Frente a la misma funcion implementada en FPGA, un ASIC en un nodo comparable ofrece de manera consistente:
- Frecuencias de reloj significativamente mas altas, porque no hay interconexion programable en el camino: los cables van directo de una compuerta a la siguiente.
- Consumo mucho menor, por el mismo motivo y porque no hay recursos configurados que no se usen.
- Area mucho menor, y por tanto costo unitario menor a volumen alto.
- Costo unitario minimo, una vez amortizada la inversion inicial.
Y pierde todo lo demas:
- NRE (Non-Recurring Engineering) enorme: el juego de mascaras, las licencias de herramientas, la propiedad intelectual de terceros, el equipo de diseño y verificacion.
- Plazos largos entre el cierre del diseño y el silicio funcional.
- Cero flexibilidad: un error de logica descubierto despues de fabricar se corrige, en el mejor caso, en la siguiente revision del chip.
- Riesgo concentrado: si el mercado cambia mientras el chip se fabrica, el chip nace obsoleto.
El flujo de diseño completo
flowchart TD
ESPEC["Especificacion funcional<br/>y de rendimiento"] --> RTL["Diseño RTL<br/>Verilog / VHDL"]
RTL --> VER["Verificacion funcional<br/>simulacion, UVM, formal"]
VER -->|"errores de logica"| RTL
VER --> SINT["Sintesis logica<br/>RTL a compuertas de la biblioteca"]
SINT --> DFT["Insercion de estructuras<br/>de testeo (scan chains)"]
DFT --> FLOOR["Floorplanning<br/>ubicacion de bloques y pines"]
FLOOR --> PLACE["Emplazamiento de celdas"]
PLACE --> CTS["Sintesis del arbol de reloj"]
CTS --> ROUTE["Ruteo de interconexiones"]
ROUTE --> STA2["Analisis de temporizacion<br/>con parasitos extraidos"]
STA2 -->|"no cumple"| FLOOR
STA2 --> DRC["Verificacion fisica<br/>DRC y LVS"]
DRC --> TAPE["Tapeout<br/>entrega de mascaras GDSII"]
TAPE --> FAB["Fabricacion en la foundry"]
FAB --> PKG["Encapsulado"]
PKG --> TEST["Prueba de produccion<br/>y binning"]
TEST --> PROD["Producto"]
style ESPEC fill:#1e3a8a,color:#fff
style TAPE fill:#7c2d12,color:#fff
style PROD fill:#166534,color:#fff
Vale la pena nombrar las etapas menos evidentes:
- DFT (Design for Test): se insertan cadenas de registros que permiten inyectar patrones y leer resultados en fabrica. Sin esto no hay forma economica de saber si un chip salio bueno.
- Sintesis del arbol de reloj (CTS): distribuir un reloj a millones de flip-flops de modo que llegue a todos casi al mismo tiempo es un problema en si mismo; el sesgo de reloj se come el margen de temporizacion.
- DRC (Design Rule Check): verifica que la geometria respeta las reglas de fabricacion del proceso, anchos minimos, separaciones, densidades.
- LVS (Layout Versus Schematic): verifica que el dibujo fisico corresponde exactamente al circuito que se diseño.
- Tapeout: el momento en que se entrega el archivo GDSII a la foundry. El nombre viene de cuando el diseño se entregaba en cinta magnetica.
Que se fabrica como ASIC
Todo lo que uno usa a diario y no es programable: procesadores de aplicacion de telefonos, controladores de memoria flash, chips de gestion de energia, transceptores de radio, aceleradores de IA de centro de datos, codificadores de video de camaras, chips de mineria de criptomonedas, controladores Ethernet y USB. Tambien los propios microcontroladores del capitulo 8: un STM32 es un ASIC, uno cuya funcion especifica resulta ser “ejecutar el codigo que le cargues”.
Puertas de entrada accesibles
Historicamente el ASIC era inalcanzable fuera de la industria. Eso cambio parcialmente con dos desarrollos:
- PDK abiertos: kits de diseño de proceso publicados con licencia libre para nodos maduros, que permiten usar herramientas de codigo abierto de sintesis y layout automatizado sin firmar acuerdos de confidencialidad.
- Obleas compartidas (MPW, multi-project wafer): varios diseños pequeños comparten un mismo juego de mascaras y una misma corrida de fabricacion, repartiendo el costo. Programas educativos como Tiny Tapeout llevan esto al extremo: uno envia un diseño diminuto, en el orden de unos pocos cientos de compuertas, y recibe silicio real por un costo comparable al de un curso.
Para un curso de robotica el valor de estas puertas no es fabricar el chip del proyecto, sino entender el flujo completo hasta el final, lo que cambia por completo la forma de leer una hoja de datos.
Tabla comparativa de las plataformas
Esta es la referencia consolidada. Los ordenes de magnitud son indicativos y sirven para comparar entre columnas, no como especificacion.
| Criterio | MCU (ESP32/STM32) | SoC Linux (Raspberry Pi) | Jetson | Kria / SoC+FPGA | FPGA pura | ASIC |
|---|---|---|---|---|---|---|
| Modelo de ejecucion | Secuencial, un hilo por nucleo | Secuencial, multinucleo con SO | Secuencial + paralelismo masivo SIMT | Secuencial + paralelismo espacial | Paralelismo espacial puro | Circuito fijo |
| Lenguaje principal | C, C++, Rust, Elixir/AtomVM | Cualquiera, incluido Elixir/Nerves | C++, Python, CUDA, Elixir de orquestador | C/C++, Python, HDL | VHDL, Verilog, Chisel | HDL + flujo fisico |
| Latencia de respuesta | Microsegundos, con jitter de RTOS | Milisegundos, jitter del planificador | Milisegundos por inferencia | Nanosegundos en la parte PL | Nanosegundos, deterministica | Minima posible |
| Consumo tipico | Miliwatts a pocos vatios | Pocos vatios | Decenas de vatios | Unidades a decenas de vatios | Variable, alto en reposo | Minimo por funcion |
| Costo unitario | Muy bajo | Bajo | Alto | Alto | Medio a muy alto | Muy bajo a volumen |
| NRE / inversion inicial | Casi nula | Casi nula | Baja | Media | Media | Muy alta |
| Tiempo hasta el primer prototipo | Horas | Horas | Dias | Semanas | Semanas | Meses a años |
| Cambiar la funcionalidad | Trivial, se regraba | Trivial | Trivial en software | Recompilar bitstream | Recompilar bitstream | Imposible |
| Punto fuerte | Costo, consumo, simplicidad | Ecosistema y facilidad | Redes neuronales y vision | Determinismo + protocolos a medida | Paralelismo y latencia fija | Eficiencia absoluta |
| Punto debil | Sin computo pesado | Sin determinismo duro | Consumo y costo | Complejidad de herramientas | Curva de aprendizaje | Rigidez y riesgo |
| Volumen donde conviene | 1 a millones | 1 a decenas de miles | 1 a decenas de miles | 1 a miles | 1 a miles | Cientos de miles en adelante |
Un metodo para elegir el controlador correcto
El error mas comun al elegir plataforma es decidir por familiaridad (“siempre uso ESP32”) o por deslumbramiento (“este trae IA”). Un metodo reproducible evita ambas trampas. Lo que sigue es un procedimiento de siete pasos.
Paso 1: escribir el requisito duro
Antes de mirar catalogos, se escriben las restricciones no negociables del proyecto. Cada una en una linea, con un numero:
- Tasa de muestreo o de cuadros minima.
- Latencia maxima aceptable de extremo a extremo, y si esa latencia debe ser garantizada o basta con que sea tipica.
- Presupuesto de energia: vatios disponibles, o autonomia en horas con una bateria de capacidad dada.
- Presupuesto de costo unitario, y volumen de produccion esperado al año.
- Restricciones fisicas: dimensiones, temperatura de operacion, vibracion, certificaciones exigidas.
- Vida util del producto y horizonte de disponibilidad del componente.
Si alguno de estos numeros no se puede escribir todavia, ese es el trabajo pendiente, no la eleccion del chip.
Paso 2: clasificar la carga de trabajo
Toda carga cae en una de cuatro categorias, y cada una apunta a una plataforma distinta:
| Categoria de carga | Caracteristica | Plataforma natural |
|---|---|---|
| Control y E/S | Lee sensores, decide, mueve actuadores, comunica | Microcontrolador |
| Computo secuencial complejo | Logica de aplicacion, base de datos, red, interfaz | SoC con Linux |
| Paralelismo de datos | La misma operacion sobre muchisimos datos: convoluciones, algebra lineal | GPU (Jetson) |
| Paralelismo de tuberia con determinismo | Flujo continuo, latencia fija, protocolos a medida | FPGA o SoC+FPGA |
Un robot real casi siempre tiene las cuatro. La respuesta correcta rara vez es una plataforma: es una arquitectura heterogenea donde cada carga vive donde le corresponde.
Paso 3: aplicar el filtro eliminatorio
Con los numeros del paso 1, se descartan plataformas por incumplimiento absoluto. Este arbol resume las eliminaciones mas frecuentes:
flowchart TD
INICIO["Requisitos escritos<br/>y carga clasificada"] --> Q1{"¿Latencia garantizada<br/>menor a 1 microsegundo?"}
Q1 -->|Si| FPGA1["FPGA o SoC+FPGA<br/>Un SO no lo garantiza"]
Q1 -->|No| Q2{"¿Necesita inferencia de<br/>redes neuronales en tiempo real?"}
Q2 -->|Si| Q3{"¿El modelo cabe en<br/>una NPU de microcontrolador?"}
Q3 -->|Si| MCU1["MCU con acelerador<br/>de IA integrado"]
Q3 -->|No| JET["Jetson u otro modulo<br/>con GPU/NPU"]
Q2 -->|No| Q4{"¿Presupuesto de energia<br/>menor a 100 mW promedio?"}
Q4 -->|Si| MCU2["Microcontrolador<br/>de bajo consumo"]
Q4 -->|No| Q5{"¿Necesita sistema de archivos,<br/>red completa, interfaz grafica?"}
Q5 -->|Si| Q6{"¿Ademas necesita lazos<br/>de control deterministicos?"}
Q6 -->|Si| KRIA["SoC + FPGA (Kria)<br/>o SoC Linux + MCU dedicado"]
Q6 -->|No| SOC["SoC Linux<br/>Raspberry Pi, BeagleBone"]
Q5 -->|No| Q7{"¿Volumen anual mayor<br/>a cientos de miles?"}
Q7 -->|Si| ASIC1["Evaluar ASIC<br/>contra MCU a volumen"]
Q7 -->|No| MCU3["Microcontrolador"]
style FPGA1 fill:#854d0e,color:#fff
style JET fill:#0c4a6e,color:#fff
style KRIA fill:#166534,color:#fff
style ASIC1 fill:#7c2d12,color:#fff
style MCU1 fill:#0d9488,color:#fff
style MCU2 fill:#0d9488,color:#fff
style MCU3 fill:#0d9488,color:#fff
style SOC fill:#0284c7,color:#fff
Este arbol no da la respuesta final: reduce el conjunto de candidatos a dos o tres. La decision entre esos candidatos es el paso siguiente.
Paso 4: puntuar con pesos explicitos
Aqui entra la parte que evita las discusiones circulares en el equipo. Se definen criterios, se les asignan pesos que suman 1.0, se puntua cada candidato de 0 a 10 en cada criterio, y se calcula el puntaje ponderado. Lo valioso no es el numero final sino que la discusion se traslada a los pesos, que es donde realmente esta el desacuerdo.
Este modulo de Elixir implementa la rubrica y se ejecuta directamente:
defmodule Selector do
@moduledoc """
Rubrica ponderada para elegir plataforma de computo.
Los pesos representan la importancia relativa de cada criterio en
ESTE proyecto. Los puntajes (0 a 10) representan que tan bien
cumple cada candidato ese criterio.
Ejecutar con: elixir selector.exs
"""
@criterios [
:computo_paralelo,
:determinismo,
:consumo,
:costo_unitario,
:tiempo_de_desarrollo,
:ecosistema,
:disponibilidad_larga
]
@candidatos %{
"MCU (STM32G0)" => %{
computo_paralelo: 1,
determinismo: 9,
consumo: 10,
costo_unitario: 10,
tiempo_de_desarrollo: 8,
ecosistema: 9,
disponibilidad_larga: 9
},
"SoC Linux (Raspberry Pi CM)" => %{
computo_paralelo: 4,
determinismo: 3,
consumo: 6,
costo_unitario: 7,
tiempo_de_desarrollo: 9,
ecosistema: 10,
disponibilidad_larga: 7
},
"Jetson Orin Nano" => %{
computo_paralelo: 9,
determinismo: 4,
consumo: 3,
costo_unitario: 3,
tiempo_de_desarrollo: 7,
ecosistema: 8,
disponibilidad_larga: 7
},
"Kria K26 (SoC + FPGA)" => %{
computo_paralelo: 8,
determinismo: 10,
consumo: 4,
costo_unitario: 2,
tiempo_de_desarrollo: 3,
ecosistema: 5,
disponibilidad_larga: 8
},
"FPGA pura" => %{
computo_paralelo: 9,
determinismo: 10,
consumo: 4,
costo_unitario: 3,
tiempo_de_desarrollo: 2,
ecosistema: 4,
disponibilidad_larga: 8
}
}
@doc """
Valida que los pesos cubran todos los criterios y sumen 1.0.
"""
def validar_pesos(pesos) do
faltantes = @criterios -- Map.keys(pesos)
suma = pesos |> Map.values() |> Enum.sum()
cond do
faltantes != [] ->
{:error, "faltan criterios: #{inspect(faltantes)}"}
abs(suma - 1.0) > 0.001 ->
{:error, "los pesos suman #{Float.round(suma, 3)}, deben sumar 1.0"}
true ->
:ok
end
end
@doc """
Devuelve la lista de {candidato, puntaje} ordenada de mayor a menor.
"""
def evaluar(pesos, candidatos \\ @candidatos) do
with :ok <- validar_pesos(pesos) do
resultado =
candidatos
|> Enum.map(fn {nombre, puntajes} ->
total =
Enum.reduce(pesos, 0.0, fn {criterio, peso}, acc ->
acc + peso * Map.fetch!(puntajes, criterio)
end)
{nombre, Float.round(total, 2)}
end)
|> Enum.sort_by(fn {_n, p} -> p end, :desc)
{:ok, resultado}
end
end
@doc """
Muestra que criterio aporta mas al puntaje de un candidato.
Sirve para explicar POR QUE gano, no solo que gano.
"""
def desglose(nombre, pesos, candidatos \\ @candidatos) do
puntajes = Map.fetch!(candidatos, nombre)
pesos
|> Enum.map(fn {criterio, peso} ->
{criterio, Float.round(peso * Map.fetch!(puntajes, criterio), 2)}
end)
|> Enum.sort_by(fn {_c, aporte} -> aporte end, :desc)
end
def imprimir(titulo, pesos) do
IO.puts("\n=== #{titulo} ===")
case evaluar(pesos) do
{:ok, resultados} ->
Enum.each(resultados, fn {nombre, puntaje} ->
barra = String.duplicate("#", round(puntaje))
IO.puts(:io_lib.format("~-30s ~5.2f ~s", [nombre, puntaje, barra]))
end)
{ganador, _} = hd(resultados)
IO.puts("\nAporte principal en #{ganador}:")
ganador
|> desglose(pesos)
|> Enum.take(3)
|> Enum.each(fn {criterio, aporte} ->
IO.puts(" #{criterio}: #{aporte}")
end)
{:error, motivo} ->
IO.puts("Error: #{motivo}")
end
end
end
# --- Tres proyectos distintos, los mismos candidatos ---
Selector.imprimir("Sensor de humedad a bateria, 5 años, 50.000 unidades", %{
computo_paralelo: 0.02,
determinismo: 0.10,
consumo: 0.38,
costo_unitario: 0.28,
tiempo_de_desarrollo: 0.10,
ecosistema: 0.06,
disponibilidad_larga: 0.06
})
Selector.imprimir("Robot movil de inventario con vision, 200 unidades", %{
computo_paralelo: 0.34,
determinismo: 0.10,
consumo: 0.14,
costo_unitario: 0.10,
tiempo_de_desarrollo: 0.16,
ecosistema: 0.12,
disponibilidad_larga: 0.04
})
Selector.imprimir("Servoaccionamiento industrial de alta frecuencia, 2.000 unidades", %{
computo_paralelo: 0.12,
determinismo: 0.36,
consumo: 0.08,
costo_unitario: 0.12,
tiempo_de_desarrollo: 0.14,
ecosistema: 0.08,
disponibilidad_larga: 0.10
})
Guardalo como selector.exs y corre elixir selector.exs. Vas a ver que el ganador cambia con los pesos, no con el hardware. Ese es exactamente el punto: no existe la mejor plataforma, existe la mejor plataforma para un conjunto de pesos. Cuando dos personas discuten sobre que chip usar, casi siempre estan discutiendo sobre pesos sin saberlo.
Paso 5: verificar con un prototipo de riesgo
El puntaje es una hipotesis. Antes de comprometerse hay que probar el supuesto mas fragil, y solo ese. Ejemplos de prototipos de riesgo bien enfocados:
- Si el supuesto es “el modelo corre a 30 fps en este modulo”, se compila el motor de inferencia en el modulo real y se mide con
tegrastats. Nada mas. - Si el supuesto es “el lazo de control cierra en 20 microsegundos”, se implementa solo el lazo y se mide con un osciloscopio, sin el resto del sistema.
- Si el supuesto es “el diseño cabe en esta FPGA”, se sintetiza el bloque mas grande y se lee el reporte de utilizacion.
- Si el supuesto es “la bateria dura ocho horas”, se mide el consumo promedio real con la carga real durante una hora y se extrapola.
Un prototipo de riesgo no es un producto minimo: es un experimento con una sola variable.
Paso 6: diseñar la arquitectura heterogenea
Una vez elegido, el trabajo es repartir responsabilidades. La arquitectura que se repite en robots serios tiene tres capas de computo:
flowchart LR
subgraph BORDE["Capa de tiempo real"]
MCU["Microcontrolador o<br/>region PL de FPGA"]
MCU --> M1["Lazos de corriente<br/>y velocidad"]
MCU --> M2["Seguridad: paro de<br/>emergencia, limites"]
MCU --> M3["Muestreo sincronico<br/>de sensores"]
end
subgraph COMPUTO["Capa de computo"]
SOC["SoC con Linux<br/>Jetson o Kria PS"]
SOC --> C1["Percepcion:<br/>vision, LiDAR"]
SOC --> C2["Planificacion<br/>y navegacion"]
SOC --> C3["Supervision Elixir:<br/>estado, reinicio, telemetria"]
end
subgraph NUBE["Capa de flota"]
SRV["Servidor Elixir/Phoenix"]
SRV --> N1["Agregacion de<br/>telemetria"]
SRV --> N2["Despliegue de<br/>firmware y modelos"]
SRV --> N3["Reentrenamiento<br/>de modelos"]
end
MCU <-->|"CAN, SPI,<br/>memoria compartida"| SOC
SOC <-->|"MQTT, gRPC,<br/>distribucion BEAM"| SRV
style BORDE fill:#7c2d12,color:#fff
style COMPUTO fill:#0c4a6e,color:#fff
style NUBE fill:#166534,color:#fff
La regla que ordena este reparto: lo que mata o rompe si falla vive en la capa de tiempo real, con el codigo mas simple y auditable posible. Lo que solo degrada la experiencia si falla vive en la capa de computo, donde puede reiniciarse. Elixir brilla justamente en la segunda capa, porque su modelo de supervision convierte los fallos en reinicios acotados en lugar de en caidas totales.
Paso 7: documentar la decision y su fecha de revision
Toda decision de plataforma debe quedar escrita con tres elementos: los pesos usados, los candidatos descartados con su motivo, y la condicion que obligaria a revisarla. Ejemplos de condicion de revision: “si el volumen anual supera las 100.000 unidades, reevaluar ASIC”; “si aparece un microcontrolador con NPU capaz de correr este modelo, reevaluar bajar del Jetson”; “si el proveedor anuncia fin de vida del modulo”.
Sin ese tercer elemento, la decision se fosiliza y el proyecto arrastra durante años una eleccion que dejo de ser correcta.
Un ejemplo completo del metodo aplicado
Proyecto: robot movil autonomo para conteo de inventario en bodega. Debe navegar pasillos, leer codigos de barras en estanterias a tres alturas, evitar personas, operar ocho horas por turno, y se planean 200 unidades.
Paso 1, requisitos duros: navegacion con actualizacion de pose a 20 Hz; deteccion de personas con latencia menor a 150 ms; lectura de codigos a 15 fps sobre tres camaras; autonomia de 8 horas con recarga en base; costo objetivo del subsistema de computo acotado; vida del producto 7 años.
Paso 2, clasificacion: hay control (motores de traccion, sensores de seguridad), hay computo secuencial (planificacion, gestion de tareas, sincronizacion con el sistema de inventario), hay paralelismo de datos (tres flujos de vision con deteccion), y no hay requisito de latencia sub-microsegundo.
Paso 3, filtro: la pregunta de latencia sub-microsegundo da “no”. La pregunta de inferencia en tiempo real da “si” y el modelo de deteccion sobre tres camaras no cabe en la NPU de un microcontrolador. El arbol conduce a la rama Jetson.
Paso 4, rubrica: con los pesos del segundo ejemplo del codigo, gana el Jetson por su puntaje en computo paralelo, con el SoC Linux como segundo cercano.
Paso 5, prototipo de riesgo: el supuesto fragil es “tres camaras a 15 fps con el modelo de deteccion mas el de codigos de barras, dentro del presupuesto de energia de 8 horas”. Se monta un modulo, se compila el motor con TensorRT, se corre la carga completa y se mide con tegrastats en cada perfil de nvpmodel. Si el perfil de menor consumo que cumple los 15 fps deja el consumo dentro del presupuesto, la hipotesis se sostiene.
Paso 6, arquitectura: un STM32 en la base movil cierra los lazos de motor y gestiona el paro de emergencia y los bumpers, comunicandose por CAN. El Jetson corre Linux con la percepcion en GPU y una aplicacion Elixir que supervisa los procesos de vision, mantiene la maquina de estados de la mision, publica telemetria y expone una interfaz web local para diagnostico. Un servidor Phoenix agrega la flota.
Paso 7, revision: reevaluar si el numero de camaras sube a seis, si el volumen anual supera 2.000 unidades, o si aparece un modulo con mejor relacion de computo por vatio en el mismo rango de precio.
Elixir a traves de todas las capas
Conviene cerrar el mapa de donde puede vivir Elixir en cada plataforma de este capitulo, porque no es obvio y cambia segun el nivel:
| Plataforma | Como corre Elixir | Rol adecuado |
|---|---|---|
| Microcontrolador (ESP32) | AtomVM, una VM de BEAM reducida sobre el chip | Logica de aplicacion concurrente, maquinas de estado, protocolo |
| SoC Linux (Raspberry Pi, BeagleBone) | Nerves: firmware completo con la BEAM como sistema | Todo el dispositivo, incluidas actualizaciones y red |
| Jetson | Runtime nativo de Elixir sobre Ubuntu ARM64 | Supervision de procesos de inferencia, orquestacion, telemetria, interfaz |
| Kria (lado PS) | Runtime nativo sobre la distribucion Linux del modulo | Igual que Jetson, mas control de carga de bitstreams |
| FPGA pura | No corre; se le habla desde el procesador anfitrion | Elixir como capa de control sobre el bus |
| ASIC | No corre, salvo que el ASIC sea un procesador | Elixir en el sistema que lo integra |
El patron transversal: Elixir rara vez hace el trabajo pesado, y casi siempre decide cuando y como se hace. Esa division es deliberada. El codigo que multiplica matrices o cierra un lazo de corriente debe ser rapido y predecible; el codigo que decide que modelo cargar, que hacer si un sensor deja de responder y como reportar el estado debe ser resiliente y facil de razonar. Son virtudes distintas y conviene no pedirle las dos al mismo lenguaje.
Errores comunes y como resolverlos
| Error | Sintoma | Causa | Solucion |
|---|---|---|---|
| Elegir la plataforma antes de escribir los requisitos | Rediseño a mitad de proyecto | La decision se tomo por familiaridad o por moda | Aplicar el paso 1 del metodo: numeros escritos antes de mirar catalogos |
| Comparar plataformas solo por TOPS o por megahercios | El modulo elegido no rinde lo prometido | Las cifras de pico no reflejan la carga real ni el limite de memoria | Medir con la carga propia en el hardware real antes de comprometerse |
| Compilar el motor de TensorRT en un modulo distinto al de destino | El motor no carga o falla al inicializar | El motor se optimiza para un chip y una version de biblioteca especificos | Generar el .engine en el mismo modelo de Jetson que lo va a ejecutar, dentro de la canalizacion de construccion |
| Dejar el Jetson en el perfil de potencia por defecto | Autonomia mucho menor a la estimada | El perfil por defecto no es el mas eficiente para la carga | Caracterizar con nvpmodel y tegrastats, elegir el perfil minimo que cumple |
| Asumir que Linux garantiza latencia | Fallos esporadicos en el lazo de control | Un SO de proposito general no ofrece garantias de tiempo real duro | Mover el lazo critico a un microcontrolador dedicado, a los nucleos R5F o a la logica programable |
| Bloque combinacional sin asignar todas las ramas en HDL | La sintesis reporta un latch inferido | La herramienta debe conservar el valor anterior, y para eso crea memoria | Asignar valor por defecto al inicio del bloque, o usar logica secuencial explicita |
| Muestrear una señal de otro dominio de reloj sin sincronizar | Fallos aleatorios cada varias horas, imposibles de reproducir | Metaestabilidad al capturar una señal asincrona | Sincronizador de dos flip-flops para un bit, FIFO asincrono con codigo Gray para buses |
| Perseguir la frecuencia agregando logica | El slack se vuelve mas negativo con cada intento | El camino critico se alarga en lugar de acortarse | Insertar registros de pipeline para partir el camino, aceptando mas latencia en ciclos |
| Instanciar un multiplicador por canal en FPGA | El emplazamiento falla por falta de bloques DSP | Se copio la estructura del codigo secuencial al hardware | Multiplexar un bloque DSP en el tiempo entre varios canales |
Olvidar el flush en el proceso externo bajo Port | El GenServer de Elixir nunca recibe datos | La salida queda en el buffer del proceso hijo | Vaciar explicitamente la salida tras cada mensaje, o desactivar el buffering |
No cerrar el Port al terminar el GenServer | Procesos huerfanos acumulandose | El proceso del sistema operativo sobrevive a su dueño | Implementar terminate/2 cerrando el puerto, y activar trap_exit |
| Evaluar un ASIC por costo unitario a volumen bajo | El proyecto se vuelve inviable | Se ignoro el NRE, que domina completamente a bajo volumen | Calcular costo total dividido por unidades esperadas, no costo marginal |
| Usar buffers normales con DMA de FPGA | Datos corruptos o fallo de acceso a memoria | La FPGA usa direcciones fisicas y el buffer no es contiguo | Reservar memoria contigua con el mecanismo de la plataforma antes de transferir |
| Ignorar la disponibilidad a largo plazo del modulo | Rediseño forzado a los tres años | El componente entro en fin de vida antes que el producto | Verificar el compromiso de longevidad del fabricante antes de elegir |
Ejercicios propuestos
-
Rubrica propia. Toma el codigo de
Selectory agrega dos criterios que importen en un proyecto tuyo (por ejemplo, resistencia a temperatura o facilidad de certificacion). Agrega tambien un candidato nuevo con sus puntajes. Corre la evaluacion con tres juegos de pesos distintos y explica por escrito por que cambia el ganador. -
Simulacion de PWM. Instala Icarus Verilog, compila el modulo
pwm.vcon su banco de pruebas y verifica que el ciclo de trabajo medido corresponde al programado. Despues modifica el parametroANCHOa 10 bits y ajusta el banco de pruebas para que siga midiendo cuatro periodos completos. -
Contar recursos. Toma el mismo modulo PWM e instancialo 4, 16 y 64 veces en un modulo superior. Si tienes acceso a una herramienta de sintesis, anota cuantas LUT y cuantos flip-flops consume cada version y grafica la relacion. Si no tienes la herramienta, estima a mano cuantos flip-flops necesita cada contador y verifica que la relacion sea lineal.
-
Presupuesto de energia. Elige un robot imaginario con una bateria de 5000 mAh a 14.8 V. Calcula cuantas horas duraria alimentando: un STM32 mas actuadores, un Raspberry Pi mas actuadores, y un Jetson mas actuadores. Usa cifras de consumo de hojas de datos reales y documenta tus supuestos sobre el consumo de los motores.
-
Detector supervisado. Levanta un proyecto Mix, agrega
jasoncomo dependencia, incorpora el moduloVision.Detectory el scriptdetector.pyde este capitulo, y ponlo bajo un supervisor. Mata el proceso de Python desde otra terminal y comprueba en los registros que vuelve solo. Despues modifica el script para que termine con codigo de error tras diez segundos, y observa el comportamiento del reinicio. -
Medicion de latencia comparada. Escribe un programa que mida el tiempo entre un flanco de entrada y una respuesta de salida, primero en un microcontrolador y despues en un SoC con Linux, ambos con la misma logica trivial. Registra 10.000 mediciones en cada uno, calcula el promedio, el percentil 99 y el maximo, y explica la diferencia entre ambos histogramas.
-
Arbol de decision aplicado. Toma tres proyectos distintos —uno propio, uno de un compañero y uno inventado con requisitos extremos— y recorre el arbol de decision del paso 3 para cada uno. Documenta en que nodo se separaron los caminos.
-
Flujo de ASIC en papel. Elige una funcion simple, por ejemplo un decodificador de un encoder en cuadratura, y describe por escrito que ocurriria en cada una de las etapas del flujo de ASIC del diagrama. Identifica en cual de esas etapas seria mas caro descubrir un error y por que.
-
Reparto heterogeneo. Dibuja en Mermaid la arquitectura de tres capas para un proyecto tuyo, asignando cada funcion a la capa que le corresponde segun la regla de “lo que rompe si falla va abajo”. Justifica cada asignacion en una linea.
-
Aceleracion en Nx. Extiende el modulo
Fusioncon el paso de correccion del filtro de Kalman y verifica numericamente el resultado contra un calculo hecho a mano con matrices de 2x2. Compara el tiempo de ejecucion con y sin el backend EXLA activado.
Que viene despues
Con este capitulo se cierra el recorrido por los controladores: desde el temporizador 555 y el PIC de los capitulos iniciales, pasando por los microcontroladores modernos, hasta las plataformas de computo intensivo y el silicio hecho a medida. Tienes ahora el mapa completo y, mas importante, un metodo reproducible para ubicarte en el: escribir los requisitos duros, clasificar la carga, filtrar candidatos, puntuar con pesos explicitos, verificar el supuesto mas fragil, repartir en capas y dejar escrita la condicion de revision.
Elegido el controlador, aparece de inmediato el siguiente problema, y es el que ocupa el resto del curso: ese controlador tiene que hablar con el mundo. Sensores, expansores de puertos, memorias, pantallas, convertidores analogico-digitales, controladores de motor. Cada uno tiene su protocolo, y hay uno que aparece mas que ningun otro porque resuelve el caso mas comun: conectar muchos dispositivos lentos a un mismo par de cables.
En el capitulo 10 entramos de lleno en IoT y en el protocolo I2C: como funcionan sus dos lineas, por que necesitan resistencias de pull-up y como se calculan, que es una condicion de inicio y de parada, como se direcciona un esclavo y que hacer cuando dos dispositivos comparten direccion, como se leen y escriben registros, y como se depura un bus con un analizador logico cuando deja de responder. Lo veremos con codigo Elixir concreto leyendo sensores reales, que es donde toda la teoria de estos nueve capitulos empieza a producir mediciones.
El indice completo del curso esta en /tecnologias/elixir-robotics/00-indice/.