Sistemas incrustados y los lenguajes que los programan

Por: Artiko
elixirroboticaiotelectronicaatomvmmicrocontroladoressistemas-embebidosmicropythonrust-embebido

Sistemas incrustados y los lenguajes que los programan

En el capitulo 4 armamos el banco de trabajo: resistencias, capacitores, transistores, LEDs, el multimetro, la protoboard y el cautin. Todo ese material es hardware pasivo o discreto: hace exactamente lo que la fisica del componente dicta y nada mas. Un divisor resistivo divide voltaje siempre igual, un transistor conmuta cuando la base recibe corriente, un capacitor se carga con una constante de tiempo fija. No hay decisiones, no hay logica, no hay “si pasa esto entonces aquello”.

Este capitulo introduce la pieza que convierte ese conjunto de componentes en un sistema que decide: el microcontrolador y el firmware que corre dentro de el. Vamos a definir con precision que es un sistema incrustado, como se organiza por dentro, que restricciones impone, y sobre todo que lenguajes se usan para programarlo y que gana y que pierde uno con cada eleccion. Al final del capitulo vas a haber visto el mismo programa —hacer parpadear un LED— escrito en Assembler, C, C++, Rust, MicroPython y Elixir sobre AtomVM, y vas a entender por que el ultimo, que parece el mas improbable, es el que hace mas facil el problema que domina la robotica y el IoT: hacer varias cosas a la vez sin volverse loco.

Que es exactamente un sistema incrustado

Un sistema incrustado (o embebido, ambos terminos se usan) es un sistema de computo construido alrededor de un microcontrolador o microprocesador, dedicado a una tarea especifica dentro de un producto mayor. La palabra clave es dedicado. Tu notebook es una computadora de proposito general: hoy edita video, manana compila codigo, pasado juega. El controlador dentro de tu lavadora hace una sola cosa durante los diez años de vida util del aparato: leer sensores de nivel y temperatura, mover un motor y una electrovalvula, y encender un LED.

De esa dedicacion se derivan casi todas sus caracteristicas:

  • No hay usuario frente a una pantalla. La interfaz suele ser un boton, un LED, un display de siete segmentos o directamente nada. Muchas veces la unica interfaz es un protocolo de red.
  • El programa se llama firmware y vive en memoria no volatil. No se “instala”: se graba (flashea) en la memoria flash del chip y arranca solo al energizar.
  • El sistema arranca y no se apaga. El firmware entra en un bucle infinito. Si el bucle termina, algo salio mal.
  • Los recursos son fijos y escasos. No puedes agregar RAM. Lo que trae el chip es todo lo que vas a tener, para siempre.
  • El fallo importa de verdad. Un servidor web que se cae se reinicia y pierde unas peticiones. Un controlador de frenos que se cae mata a alguien. Entre esos dos extremos hay un abanico enorme de criticidad.

Ejemplos que probablemente tienes a menos de tres metros: el control remoto del televisor, el mouse inalambrico, el cargador rapido del telefono (si, tiene un microcontrolador negociando voltaje), el reloj inteligente, el termostato, el router, el auto (que trae entre 50 y 150 unidades de control), el ascensor del edificio y la balanza del supermercado.

Sectores donde vive

SectorEjemplo tipicoRestriccion dominante
AutomotrizUnidad de control de motor, ABS, airbagDeterminismo duro, certificacion ISO 26262
Dispositivos medicosBomba de infusion, oximetro, marcapasosSeguridad del paciente, trazabilidad
IoT de consumoSensor de temperatura con WiFi, enchufe inteligenteCosto por unidad y consumo energetico
Industria 4.0PLC, sensor de vibracion, gateway ModbusRobustez electrica, disponibilidad continua
RoboticaControlador de motores, IMU, planificador localLatencia de lazo de control
AeroespacialAviones, satelites, cohetesTolerancia a radiacion y a fallos
Electronica de consumoAudifonos, camaras, electrodomesticosCosto, autonomia de bateria

La restriccion dominante es lo que decide el lenguaje. Un marcapasos no se programa en MicroPython. Un sensor de humedad de invernadero no necesita Ada certificado.

Microcontrolador y microprocesador: la diferencia que lo explica todo

La confusion mas comun al empezar es tratar un ESP32 y una Raspberry Pi como si fueran lo mismo. No lo son, y la diferencia determina que lenguajes puedes usar.

Un microprocesador (MPU) es solo la CPU. Necesita chips externos para memoria RAM, para almacenamiento y para periféricos. Corre un sistema operativo completo, normalmente Linux, con MMU (unidad de gestion de memoria), procesos aislados, sistema de archivos y planificador. Es lo que hay dentro de una Raspberry Pi, una BeagleBone o tu telefono.

Un microcontrolador (MCU) es un sistema completo en un solo encapsulado: CPU, RAM, memoria flash, y periféricos (GPIO, timers, ADC, UART, I2C, SPI) integrados en el mismo silicio. No tiene MMU. No corre Linux. Tu codigo es literalmente lo unico que se ejecuta, o casi.

flowchart TB
    subgraph MCU["Microcontrolador (ESP32, STM32, RP2040)"]
        direction TB
        C1[CPU 80-480 MHz]
        R1[SRAM interna<br/>decenas a cientos de KB]
        F1[Flash<br/>cientos de KB a pocos MB]
        P1[Perifericos integrados<br/>GPIO / ADC / PWM / I2C / SPI / UART]
        C1 --- R1
        C1 --- F1
        C1 --- P1
    end

    subgraph MPU["Microprocesador (Raspberry Pi, BeagleBone)"]
        direction TB
        C2[CPU multinucleo 1-2 GHz + MMU]
        R2[DRAM externa<br/>cientos de MB a GB]
        F2[microSD / eMMC<br/>GB]
        P2[Controladores externos<br/>USB / HDMI / Ethernet]
        C2 --- R2
        C2 --- F2
        C2 --- P2
    end

    MCU -->|"firmware directo, arranque en ms"| USO1[Control en tiempo real<br/>bateria, bajo costo]
    MPU -->|"Linux, arranque en segundos"| USO2[Vision, redes complejas,<br/>logica de alto nivel]

La consecuencia practica: en un MPU con Linux puedes correr cualquier cosa —Python, Node, la BEAM completa de Erlang/Elixir vía Nerves— porque tienes gigabytes y un sistema operativo. En un MCU tienes que elegir un lenguaje cuyo tiempo de ejecucion quepa en cientos de kilobytes, y esa es exactamente la conversacion de este capitulo.

Numeros concretos de placas que vas a usar

PlacaNucleoFrecuenciaRAMFlashNota
Arduino Uno (ATmega328P)AVR 8 bits16 MHz2 KB SRAM32 KBEl clasico didactico
Raspberry Pi Pico (RP2040)2x Cortex-M0+133 MHz264 KB SRAM2 MB externaMuy barata, sin radio
ESP32 (clasico)2x Xtensa LX6240 MHz~520 KB SRAM4 MB tipicoWiFi + Bluetooth
ESP32-C3RISC-V 32 bits160 MHz~400 KB SRAM4 MB tipicoWiFi + BLE, mas barato
STM32F4 (gama media)Cortex-M4F84-180 MHz96-192 KB512 KB - 1 MBIndustrial, muchos perifericos
Raspberry Pi 54x Cortex-A76 + MMU2.4 GHz4-16 GBmicroSDYa es Linux, no es MCU

Fija la vista en la columna de RAM. El ESP32 tiene aproximadamente medio megabyte. Tu navegador ahora mismo esta usando mil veces mas. Todo lo que discutimos sobre lenguajes cabe o no cabe en esa columna.

Como llega tu codigo al chip

Antes de comparar lenguajes conviene entender el camino que recorre el codigo, porque cada lenguaje interviene en un punto distinto de ese camino.

flowchart LR
    SRC[Codigo fuente<br/>.c / .cpp / .rs / .py / .ex]

    subgraph HOST["En tu computador (host)"]
        COMP[Compilador cruzado<br/>gcc-xtensa, arm-none-eabi-gcc, rustc]
        OBJ[Objetos .o]
        LINK[Enlazador + script de memoria<br/>define donde va cada seccion]
        BIN[Binario / imagen<br/>.elf -> .bin / .hex / .uf2 / .avm]
    end

    subgraph FLASHER["Grabado"]
        TOOL[Herramienta de flasheo<br/>esptool / probe-rs / openocd / picotool]
    end

    subgraph TARGET["En el microcontrolador (target)"]
        FLASH[(Memoria flash)]
        BOOT[Bootloader / reset vector]
        RAM[(SRAM: .data + .bss + stack + heap)]
        RUN[Tu programa corriendo]
    end

    SRC --> COMP --> OBJ --> LINK --> BIN --> TOOL --> FLASH
    FLASH --> BOOT --> RAM --> RUN
    RUN -->|"UART / USB CDC / JTAG"| LOG[Consola y depuracion]

Tres ideas importantes de este diagrama:

  1. Compilacion cruzada. Tu computador es x86 o ARM64; el chip destino es Xtensa, RISC-V o Cortex-M. El compilador que usas produce codigo para una arquitectura distinta de la que lo ejecuta. Por eso los toolchains se llaman arm-none-eabi-gcc o xtensa-esp32-elf-gcc.

  2. El enlazador y el mapa de memoria. En un PC el sistema operativo decide donde cargar tu programa. En un MCU lo decide un archivo de script de enlazado que dice literalmente “el codigo va en flash a partir de la direccion 0x08000000, las variables inicializadas se copian a SRAM en 0x20000000, la pila crece hacia abajo desde el final de la RAM”. Cuando algo se desborda, no hay swap: el sistema se cuelga o reinicia.

  3. El punto donde interviene el lenguaje. C, C++ y Rust compilan a instrucciones nativas y lo que se graba es tu programa. MicroPython y Elixir/AtomVM graban primero un interprete o maquina virtual escrito en C, y despues tu programa como datos que esa VM leera. Ese es el compromiso central que veremos.

Tres formas de ejecutar codigo en un microcontrolador

flowchart TB
    subgraph A["1. Bare metal (metal desnudo)"]
        A1[Tu codigo] --> A2[Registros del hardware]
        A2 --> A3[Silicio]
    end

    subgraph B["2. Con RTOS"]
        B1[Tus tareas] --> B2[Planificador RTOS<br/>FreeRTOS / Zephyr]
        B2 --> B3[Drivers / HAL]
        B3 --> B4[Silicio]
    end

    subgraph C["3. Sobre maquina virtual"]
        C1[Tu bytecode<br/>Python / BEAM] --> C2[VM escrita en C<br/>MicroPython / AtomVM]
        C2 --> C3[RTOS o bare metal]
        C3 --> C4[Silicio]
    end

    A -.->|"maximo control<br/>minima abstraccion"| ESC[Escala de abstraccion]
    B -.->|"concurrencia con hilos<br/>y prioridades"| ESC
    C -.->|"maxima productividad<br/>mayor huella"| ESC

Bare metal significa que tu main() es lo primero y lo unico que corre. Escribes directamente en registros de hardware: para encender un pin haces algo como GPIOA->ODR |= (1 << 5);. Es lo mas rapido y lo mas predecible, pero cada cosa que quieras hacer en paralelo la tienes que orquestar tu mismo con maquinas de estado y timers.

Un RTOS (sistema operativo de tiempo real) agrega un planificador que reparte la CPU entre tareas con prioridades, mas primitivas de sincronizacion: colas, semaforos, mutex. FreeRTOS y Zephyr son los dos mas comunes. El ESP-IDF, framework oficial de Espressif para el ESP32, esta construido sobre FreeRTOS. Aqui aparecen los problemas clasicos de concurrencia con memoria compartida: condiciones de carrera, interbloqueos, inversion de prioridad.

Sobre una maquina virtual ejecutas bytecode. Pagas un costo de velocidad e de memoria, y a cambio obtienes el modelo de programacion del lenguaje anfitrion completo: recoleccion de basura, tipos dinamicos o, en el caso de la BEAM, procesos aislados con paso de mensajes.

Este ultimo punto es la razon por la que este curso existe. Sostenlo en la cabeza: la maquina virtual no es un impuesto que pagas por comodidad, es la que trae el modelo de concurrencia.

Assembler: el punto de partida historico

El lenguaje ensamblador es la representacion textual de las instrucciones que la CPU ejecuta. Cada linea es una instruccion de maquina. No hay abstracciones: manejas registros, direcciones y banderas de estado a mano.

Este es un parpadeo escrito para un PIC16F877A, el microcontrolador que vamos a ver en el proximo capitulo, en ensamblador de Microchip:

; Parpadeo de un LED en RB0 del PIC16F877A
        LIST    P=16F877A
        #INCLUDE <P16F877A.INC>
        __CONFIG _XT_OSC & _WDT_OFF & _LVP_OFF

CONT1   EQU     0x20            ; variables para el retardo
CONT2   EQU     0x21

        ORG     0x0000
        GOTO    INICIO

        ORG     0x0005
INICIO
        BSF     STATUS, RP0     ; banco 1
        BCF     TRISB, 0        ; RB0 como salida
        BCF     STATUS, RP0     ; banco 0

BUCLE
        BSF     PORTB, 0        ; LED encendido
        CALL    RETARDO
        BCF     PORTB, 0        ; LED apagado
        CALL    RETARDO
        GOTO    BUCLE

RETARDO
        MOVLW   d'255'
        MOVWF   CONT1
LAZO1
        MOVLW   d'255'
        MOVWF   CONT2
LAZO2
        DECFSZ  CONT2, F
        GOTO    LAZO2
        DECFSZ  CONT1, F
        GOTO    LAZO1
        RETURN

        END

Observa lo que hace falta para algo tan simple: seleccionar el banco de memoria correcto antes de tocar TRISB, contar ciclos a mano para generar un retardo, saber que BSF significa bit set file. El retardo depende de la frecuencia del cristal: si cambias el oscilador, cambia el tiempo del parpadeo.

Hoy casi nadie escribe firmware completo en ensamblador. Se usa para el codigo de arranque antes de que exista una pila, para rutinas de interrupcion donde cada ciclo cuenta, y para leer lo que el compilador genero cuando algo no cuadra. Pero entenderlo importa porque explica de donde viene todo lo demas.

C: el idioma comun del firmware

C es un lenguaje de nivel intermedio: combina manipulacion de memoria de bajo nivel (punteros, aritmetica de direcciones, acceso directo a registros mapeados en memoria) con abstracciones de alto nivel para la epoca en que se creo (funciones, estructuras, tipos). Es el denominador comun absoluto de la industria embebida. Practicamente todo fabricante de silicio publica su SDK en C.

Este es un parpadeo con el framework de Arduino, que es la puerta de entrada mas comun:

// Parpadeo basico con el framework Arduino
const int LED_PIN = 2;   // GPIO2, LED integrado en muchas placas ESP32

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

void loop(void) {
  digitalWrite(LED_PIN, HIGH);
  Serial.println("encendido");
  delay(500);

  digitalWrite(LED_PIN, LOW);
  Serial.println("apagado");
  delay(500);
}

Y este es el mismo parpadeo en ESP-IDF, el SDK nativo del ESP32, donde ya aparece FreeRTOS:

// Parpadeo con ESP-IDF: el codigo corre dentro de una tarea de FreeRTOS
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"

#define LED_PIN GPIO_NUM_2

void app_main(void)
{
    gpio_reset_pin(LED_PIN);
    gpio_set_direction(LED_PIN, GPIO_MODE_OUTPUT);

    int nivel = 0;
    while (1) {
        nivel = !nivel;
        gpio_set_level(LED_PIN, nivel);
        printf("LED en %d\n", nivel);
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}

Nota vTaskDelay en vez de delay: no bloquea la CPU, cede el procesador a otras tareas de FreeRTOS durante ese tiempo. Esa distincion —bloquear versus ceder— es una de las fuentes de error mas comunes en embebidos.

Ahora el punto doloroso de C. Este programa compila sin advertencias y es un desastre:

#include <string.h>
#include <stdio.h>

void leer_nombre_sensor(const char *entrada) {
    char buffer[8];
    strcpy(buffer, entrada);   // sin verificar longitud
    printf("Sensor: %s\n", buffer);
}

int main(void) {
    leer_nombre_sensor("temperatura_exterior_norte");  // 26 caracteres en 8 bytes
    return 0;
}

En un PC esto probablemente aborte con un fallo de segmentacion. En un microcontrolador sin MMU no hay quien detecte la violacion: simplemente sobrescribes lo que hubiera despues de buffer en la pila —incluida la direccion de retorno— y el dispositivo hace algo impredecible. Puede colgarse, puede reiniciarse por watchdog, o puede seguir funcionando mal durante semanas. Esta clase de fallo, la corrupcion de memoria, es la razon principal por la que existe Rust en este espacio.

Ventajas de C: disponible para absolutamente todo el silicio, huella minima, control total, ecosistema y documentacion inagotables, el codigo generado es predecible.

Costos de C: sin seguridad de memoria, sin abstracciones para concurrencia (dependes del RTOS y de la disciplina), gestion manual de recursos, el compilador te deja hacer cosas que no deberias.

C++: abstracciones sin costo de ejecucion

C++ nacio en 1979 de la mano de Bjarne Stroustrup como una extension de C con clases, pensada para modelar problemas complejos sin renunciar al rendimiento. En embebidos se usa un subconjunto: normalmente se compila con excepciones y RTTI deshabilitados (-fno-exceptions -fno-rtti) porque ambos agregan tablas en flash y comportamiento no deterministico.

Lo que si se aprovecha mucho es el concepto de abstraccion sin costo: escribir codigo expresivo que el compilador reduce a lo mismo que habrias escrito a mano en C.

// Un driver de LED como clase, con la plantilla resuelta en tiempo de compilacion
#include <Arduino.h>

template <uint8_t PIN>
class Led {
public:
  void iniciar() const {
    pinMode(PIN, OUTPUT);
    apagar();
  }

  void encender() const { digitalWrite(PIN, HIGH); estado_ = true; }
  void apagar()   const { digitalWrite(PIN, LOW);  estado_ = false; }

  void alternar() const {
    if (estado_) { apagar(); } else { encender(); }
  }

private:
  mutable bool estado_ = false;
};

// Guardia RAII: enciende al construir, apaga al destruir el objeto
class LedEncendidoMientras {
public:
  explicit LedEncendidoMientras(uint8_t pin) : pin_(pin) {
    digitalWrite(pin_, HIGH);
  }
  ~LedEncendidoMientras() {
    digitalWrite(pin_, LOW);
  }
private:
  uint8_t pin_;
};

Led<2> ledEstado;

void setup() {
  ledEstado.iniciar();
  Serial.begin(115200);
}

void loop() {
  {
    LedEncendidoMientras luz(2);   // se enciende aqui
    Serial.println("procesando lectura...");
    delay(200);
  }                                 // se apaga solo al salir del bloque
  delay(800);
}

Led<2> es una plantilla: el numero de pin se conoce en compilacion, asi que el compilador puede generar el mismo codigo que un digitalWrite(2, HIGH) directo, sin sobrecarga de objeto. Y LedEncendidoMientras usa RAII: el destructor apaga el LED pase lo que pase al salir del bloque, incluso si sales por un return temprano. Ese patron es enormemente util para liberar buses I2C, deseleccionar chips SPI o soltar mutex.

El riesgo de C++ en embebidos es lo contrario: es facil arrastrar sin darse cuenta std::string, std::vector o std::function, que asignan memoria dinamica y hacen que la huella crezca y la fragmentacion del heap se vuelva un problema. Los equipos serios definen una guia de estilo restrictiva.

Rust: seguridad de memoria verificada en compilacion

Rust ataca directamente el problema que C no resuelve. Su sistema de propiedad (ownership) y prestamos (borrowing) verifica en tiempo de compilacion que no haya punteros colgantes, dobles liberaciones, desbordamientos de buffer ni carreras de datos, sin recolector de basura y sin costo de ejecucion.

En embebidos se usa en modo no_std: sin biblioteca estandar, sin asignador de memoria por defecto, sin sistema operativo. El esqueleto minimo de un binario no_std se ve asi:

#![no_std]
#![no_main]

use core::panic::PanicInfo;

// Sin sistema operativo hay que decidir que pasa ante un panic.
// Aqui: bucle infinito; en produccion suele reiniciarse por watchdog.
#[panic_handler]
fn on_panic(_info: &PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
pub extern "C" fn main() -> ! {
    loop {
        // logica del firmware
    }
}

El valor real aparece en las abstracciones. La comunidad definio embedded-hal, un conjunto de traits (interfaces) que describen periféricos de forma independiente del chip. Un driver escrito contra esos traits funciona igual sobre un STM32, un RP2040 o un ESP32:

#![no_std]

use embedded_hal::digital::OutputPin;
use embedded_hal::delay::DelayNs;

/// Driver de LED reutilizable: no sabe sobre que chip corre.
pub struct LedIntermitente<P> {
    pin: P,
    encendido: bool,
}

impl<P: OutputPin> LedIntermitente<P> {
    pub fn nuevo(pin: P) -> Self {
        Self { pin, encendido: false }
    }

    pub fn alternar(&mut self) -> Result<(), P::Error> {
        if self.encendido {
            self.pin.set_low()?;
        } else {
            self.pin.set_high()?;
        }
        self.encendido = !self.encendido;
        Ok(())
    }

    /// Parpadea `veces` con `ms` de periodo, usando cualquier fuente de retardo.
    pub fn parpadear<D: DelayNs>(&mut self, d: &mut D, veces: u32, ms: u32)
        -> Result<(), P::Error>
    {
        for _ in 0..veces {
            self.alternar()?;
            d.delay_ms(ms);
        }
        Ok(())
    }
}

Fijate en dos cosas. Primero, Result<(), P::Error>: en Rust escribir a un pin puede fallar y el tipo te obliga a decidir que hacer con ese fallo; en C digitalWrite simplemente devuelve void. Segundo, LedIntermitente toma posesion del pin. Si otro trozo de codigo intenta usar el mismo pin, el compilador lo rechaza. Ese patron —representar recursos de hardware como valores de los que solo puede haber un dueño— se llama type state y elimina en compilacion una clase entera de errores de configuracion de perifericos.

Los nombres concretos de los crates HAL (esp-hal, stm32f4xx-hal, rp2040-hal, embassy) y sus APIs cambian entre versiones, asi que al empezar un proyecto conviene mirar la documentacion de la version exacta que vas a usar en vez de copiar ejemplos antiguos.

Costos de Rust en embebidos: la curva de aprendizaje del comprobador de prestamos es real; el soporte de fabricante rara vez es oficial (los HAL suelen ser de la comunidad); las interrupciones y el estado global requieren patrones especificos (critical-section, celdas de acceso exclusivo) que al principio se sienten burocraticos; y el ecosistema, aunque crece rapido, es una fraccion del de C.

MicroPython: el interprete que cabe en el chip

MicroPython es una reimplementacion de Python 3 pensada para microcontroladores. Lo que se graba en el chip es el interprete completo, y tu codigo .py vive despues en un pequeño sistema de archivos dentro de la flash. Eso cambia el flujo de trabajo por completo: hay un REPL interactivo por el puerto serie, puedes escribir una linea y verla ejecutarse al instante, y editar el programa no requiere recompilar nada.

# main.py -- parpadeo y lectura de un potenciometro en ESP32 con MicroPython
from machine import Pin, ADC
import time

led = Pin(2, Pin.OUT)
pot = ADC(Pin(34))
pot.atten(ADC.ATTN_11DB)      # rango ampliado, ~0 a 3.3 V

def leer_voltaje():
    crudo = pot.read()        # 0..4095 en 12 bits
    return crudo * 3.3 / 4095

while True:
    led.value(not led.value())
    print("voltaje: {:.2f} V".format(leer_voltaje()))
    time.sleep_ms(500)

La productividad es notable: en dos minutos tienes un sensor leyendo. El costo aparece en tres frentes.

Memoria. El interprete ocupa flash y, sobre todo, el heap de Python vive en la SRAM que ya era escasa. En un ESP32 con medio megabyte se trabaja comodo; en un chip con 32 KB de RAM, MicroPython no entra.

Determinismo. Hay recoleccion de basura. Cuando el recolector corre, tu bucle de control se detiene por un tiempo que no controlas. Para parpadear un LED da igual; para un lazo de control de motor a 1 kHz, no.

Concurrencia. MicroPython ofrece uasyncio (cooperativa, un solo hilo) e interrupciones de hardware con restricciones fuertes: dentro de una rutina de interrupcion no puedes asignar memoria, asi que el patron habitual es preasignar buffers y solo levantar banderas. No hay procesos aislados: si una parte del programa lanza una excepcion no capturada, el programa entero cae.

Este es el patron correcto para una interrupcion en MicroPython, que ilustra bien la restriccion:

# Contar pulsos de un sensor con interrupcion, sin asignar memoria en la ISR
from machine import Pin
import micropython
import time

micropython.alloc_emergency_exception_buf(100)

pulsos = 0

def _isr(pin):
    global pulsos
    pulsos += 1          # solo operaciones sobre objetos ya existentes

sensor = Pin(4, Pin.IN, Pin.PULL_UP)
sensor.irq(trigger=Pin.IRQ_FALLING, handler=_isr)

anterior = 0
while True:
    time.sleep(1)
    actual = pulsos
    print("pulsos por segundo:", actual - anterior)
    anterior = actual

Otras opciones que conviene conocer

LenguajeBaseDonde brillaLimitacion principal
Ada / SPARKCompilado nativoAeronautica, ferrocarriles, medicina; verificacion formal, aritmetica de punto fijo para chips sin FPUEcosistema pequeño, herramientas comerciales
TinyGoCompilador Go sobre LLVMCodigo Go con goroutines en MCU, buen soporte WASMRecoleccion de basura, subconjunto de la biblioteca de Go
EspruinoInterprete JavaScriptPrototipado rapido, gente que ya sabe JSRendimiento y huella del interprete
PicoRuby / mrubyInterprete Ruby compactoSintaxis Ruby en MCU, usado en el mundo educativo japonesComunidad reducida fuera de nichos
ZigCompilado nativoInteroperabilidad con C sin fricciones, control de asignadoresLenguaje joven, API en movimiento
Erlang / ElixirBytecode BEAM sobre AtomVMConcurrencia y tolerancia a fallosVM adicional, subconjunto de OTP

Un lenguaje que aparece poco pero conviene tener presente: Verilog y Chisel no son lenguajes de software sino de descripcion de hardware; se usan para FPGA y diseño de silicio, no para firmware. Si te los cruzas en una oferta de trabajo de “embebido”, el puesto es de diseño digital, no de programacion.

Elixir sobre AtomVM: por que tiene sentido

Llegamos al lenguaje que da nombre al curso, y hay que justificarlo bien porque a primera vista es absurdo: un lenguaje funcional con recoleccion de basura, disenado para centrales telefonicas y servidores web, corriendo en un chip de medio megabyte.

La justificacion no esta en el lenguaje. Esta en la maquina virtual.

Que hace distinta a la BEAM

La BEAM es la maquina virtual de Erlang y Elixir. Su modelo tiene cuatro propiedades que casi ningun otro tiempo de ejecucion combina:

  1. Procesos ligeros. No son procesos del sistema operativo ni hilos. Son unidades de ejecucion de la VM que cuestan muy poco crear. Puedes tener cientos de ellos.
  2. Aislamiento total de memoria. Cada proceso tiene su propio heap. Ningun proceso puede corromper la memoria de otro, ni siquiera por accidente. No existen las carreras de datos porque no hay datos compartidos.
  3. Paso de mensajes asincrono. La unica forma de comunicarse es enviar mensajes a un buzon. No hay mutex, no hay semaforos, no hay secciones criticas.
  4. Planificacion apropiativa. La VM interrumpe procesos que llevan mucho tiempo ejecutando. Un proceso que entra en un bucle cerrado no congela a los demas.

Ahora compara eso con lo que significa hacer tres cosas a la vez en un ESP32 con C: leer un sensor por I2C, atender peticiones HTTP y mantener la hora sincronizada. En C creas tres tareas de FreeRTOS, decides prioridades, comparte estado con colas o con variables globales protegidas por mutex, y si una tarea toma dos mutex en distinto orden que otra, tienes un interbloqueo que aparecera un martes a las tres de la manana. En la BEAM son tres procesos que no comparten nada y se hablan por mensajes.

sequenceDiagram
    participant S as Proceso Sensor
    participant C as Proceso Coordinador
    participant R as Proceso Red
    participant L as Proceso LED

    Note over S,L: Cuatro procesos aislados, sin memoria compartida

    S->>C: {:lectura, :temperatura, 23.4}
    C->>L: {:parpadear, 1}
    C->>R: {:publicar, "temp", 23.4}
    R-->>C: {:ok, :publicado}

    S->>C: {:lectura, :temperatura, 41.9}
    C->>L: {:parpadear, 5}
    C->>R: {:publicar, "alerta", 41.9}

    Note over R: el WiFi se cae<br/>el proceso Red falla
    R--xC: {:EXIT, motivo}
    C->>R: el supervisor lo reinicia
    Note over S,L: Sensor y LED siguieron<br/>funcionando sin enterarse

Ese ultimo tramo es lo que en el mundo Erlang se llama let it crash: en vez de escribir codigo defensivo para cada fallo posible, dejas que el proceso que falla muera y que un supervisor lo reinicie en un estado limpio conocido. En un dispositivo que debe funcionar meses sin intervencion, esa estrategia vale mas que cualquier optimizacion.

Que es AtomVM

La BEAM completa no cabe en un microcontrolador: necesita decenas de megabytes. AtomVM es una implementacion alternativa de la maquina virtual de Erlang, escrita en C, disenada desde cero para dispositivos pequeños. Ejecuta el mismo bytecode .beam que produce el compilador de Erlang o de Elixir, pero con una huella de otro orden de magnitud.

Sus objetivos de plataforma incluyen ESP32, STM32, Raspberry Pi Pico (RP2040), WebAssembly y sistemas tipo UNIX genericos para desarrollo local. La disponibilidad exacta de perifericos y de modulos varia por plataforma, asi que antes de comprometerte con una placa conviene revisar la documentacion de la version que vas a usar.

flowchart TB
    subgraph DEV["En tu computador"]
        EX[".ex / .erl<br/>codigo fuente"]
        MIX["mix compile<br/>(compilador de Elixir)"]
        BEAM[".beam<br/>bytecode BEAM"]
        PACK["mix atomvm.packbeam<br/>empaqueta modulos + bibliotecas"]
        AVM["archivo .avm<br/>imagen de aplicacion"]
        EX --> MIX --> BEAM --> PACK --> AVM
    end

    subgraph FLASHSTEP["Grabado"]
        FW["Firmware AtomVM<br/>(la VM, escrita en C)"]
        APP["Tu .avm"]
    end

    AVM --> APP

    subgraph CHIP["En el ESP32"]
        VM["AtomVM<br/>interprete de opcodes BEAM"]
        SCHED["Planificador de procesos<br/>+ heaps por proceso + GC"]
        NIF["NIFs y puertos<br/>gpio / i2c / spi / uart / network / ledc"]
        HW["Perifericos del chip"]

        VM --> SCHED
        VM --> NIF --> HW
    end

    FW -->|"esptool, offset del bootloader"| VM
    APP -->|"esptool, offset de la particion de aplicacion"| VM

La pieza clave del diagrama son los NIFs y puertos: funciones implementadas en C que exponen el hardware al mundo Elixir. Cuando llamas a :gpio.digital_write/2 desde Elixir, esa llamada baja a codigo C que escribe el registro del periférico. La VM te da concurrencia y aislamiento; los NIFs te dan el hardware.

El parpadeo en Elixir sobre AtomVM

Primero el proyecto. Un mix.exs tipico para AtomVM:

defmodule Parpadeo.MixProject do
  use Mix.Project

  def project do
    [
      app: :parpadeo,
      version: "0.1.0",
      elixir: "~> 1.15",
      deps: deps(),
      atomvm: [
        start: Parpadeo,
        flash_offset: 0x250000
      ]
    ]
  end

  def application, do: []

  defp deps do
    [
      {:exatomvm, github: "atomvm/ExAtomVM", runtime: false}
    ]
  end
end

ExAtomVM es el complemento de Mix que agrega las tareas de empaquetado y grabado. La clave start: indica que modulo tiene la funcion start/0 que AtomVM invocara al arrancar.

Ahora el programa:

defmodule Parpadeo do
  @led 2

  def start do
    :gpio.set_pin_mode(@led, :output)
    ciclo(:low)
  end

  defp ciclo(nivel) do
    :gpio.digital_write(@led, nivel)
    Process.sleep(500)
    ciclo(opuesto(nivel))
  end

  defp opuesto(:low), do: :high
  defp opuesto(:high), do: :low
end

Un detalle que sorprende viniendo de C: no hay bucle while. ciclo/1 se llama a si misma indefinidamente. Como es una llamada en posicion de cola, la VM la convierte en un salto sin consumir pila. Los bucles infinitos por recursion de cola son la forma idiomatica de escribir el bucle principal en la BEAM, y no desbordan nada.

Para compilar y grabar:

# 1. Dependencias
mix deps.get

# 2. Empaquetar el bytecode en una imagen .avm
mix atomvm.packbeam

# 3. Grabar la aplicacion en el ESP32
mix atomvm.esp32.flash --port /dev/ttyUSB0

# 4. Ver la salida de la consola serie
#    (cualquier terminal serie sirve; picocom, screen o idf.py monitor)
picocom -b 115200 /dev/ttyUSB0

El firmware de AtomVM se graba una sola vez en la placa; despues, cada iteracion de desarrollo solo regraba tu .avm, que es pequeño y rapido de subir.

Donde se paga la diferencia: concurrencia

El parpadeo no demuestra nada; cualquier lenguaje parpadea. Lo que demuestra el valor de la BEAM es este programa, que hace tres cosas simultaneas con supervision:

defmodule Estacion do
  @led 2
  @boton 4

  def start do
    :gpio.set_pin_mode(@led, :output)
    :gpio.set_pin_mode(@boton, :input)
    :gpio.set_pin_pull(@boton, :up)

    coordinador = spawn(fn -> coordinar(0, 0) end)

    spawn(fn -> latir(coordinador) end)
    spawn(fn -> vigilar_boton(coordinador, :high) end)
    spawn(fn -> reportar(coordinador) end)

    # El proceso inicial se queda dormido; los demas trabajan.
    dormir_para_siempre()
  end

  defp dormir_para_siempre do
    Process.sleep(60_000)
    dormir_para_siempre()
  end

  # ---- Proceso 1: parpadeo de latido cada segundo ----
  defp latir(coordinador) do
    :gpio.digital_write(@led, :high)
    Process.sleep(50)
    :gpio.digital_write(@led, :low)
    send(coordinador, {:latido, self()})
    Process.sleep(950)
    latir(coordinador)
  end

  # ---- Proceso 2: muestreo del boton por sondeo ----
  defp vigilar_boton(coordinador, anterior) do
    Process.sleep(20)
    actual = :gpio.digital_read(@boton)

    if actual == :low and anterior == :high do
      send(coordinador, {:pulsacion, self()})
    end

    vigilar_boton(coordinador, actual)
  end

  # ---- Proceso 3: pide un resumen cada 5 segundos ----
  defp reportar(coordinador) do
    Process.sleep(5_000)
    send(coordinador, {:resumen, self()})
    reportar(coordinador)
  end

  # ---- Proceso coordinador: unico dueño del estado ----
  defp coordinar(latidos, pulsaciones) do
    receive do
      {:latido, _origen} ->
        coordinar(latidos + 1, pulsaciones)

      {:pulsacion, _origen} ->
        :erlang.display({:boton_presionado, pulsaciones + 1})
        coordinar(latidos, pulsaciones + 1)

      {:resumen, _origen} ->
        :erlang.display({:resumen, latidos, pulsaciones})
        coordinar(latidos, pulsaciones)
    end
  end
end

Lee ese codigo buscando un mutex. No hay. Buscando una variable global. No hay. El estado (latidos, pulsaciones) vive dentro de los argumentos de una funcion recursiva que solo un proceso ejecuta. Los otros tres procesos no pueden tocarlo ni por error: lo unico que pueden hacer es mandar un mensaje. Si mañana agregas un cuarto sensor, agregas un spawn y una clausula en el receive, y no tienes que revisar si rompiste la sincronizacion de lo anterior.

El equivalente en C con FreeRTOS existe y funciona, pero requiere declarar tareas con sus tamaños de pila, crear una cola, definir una estructura de mensaje, y ser disciplinado con quien escribe que. La diferencia no es de sintaxis; es de cuantas formas tienes de equivocarte.

stateDiagram-v2
    [*] --> Arranque

    Arranque --> Configurando: AtomVM carga el .avm
    Configurando --> Supervisando: start/0 lanza los procesos

    state Supervisando {
        [*] --> Operando
        Operando --> Operando: mensajes normales
        Operando --> ProcesoCaido: excepcion en un proceso
        ProcesoCaido --> Reiniciando: el supervisor detecta la salida
        Reiniciando --> Operando: proceso recreado en estado limpio
    }

    Supervisando --> Arranque: reinicio total del dispositivo
    Supervisando --> [*]: corte de energia

Un detalle importante y honesto: AtomVM implementa un subconjunto de la biblioteca estandar de Erlang/OTP y de Elixir. Tienes procesos, mensajes, spawn, receive, monitores, y las abstracciones de comportamiento mas usadas, pero no esperes que todo modulo que funciona en tu servidor Phoenix funcione tal cual en el chip. La lista concreta de modulos y funciones soportados esta en la documentacion de AtomVM y crece con cada version; la costumbre sana es verificarla antes de diseñar alrededor de una biblioteca.

AtomVM y Nerves no compiten

Es facil confundirlos porque ambos son “Elixir en hardware”, pero atacan capas distintas.

AspectoAtomVMNerves
Hardware objetivoMicrocontroladores: ESP32, STM32, RP2040Sistemas con Linux: Raspberry Pi, BeagleBone, x86
Sistema operativoNinguno o RTOS por debajoLinux minimo construido con Buildroot
Maquina virtualAtomVM (reimplementacion compacta en C)La BEAM completa de Erlang/OTP
Memoria tipicaCientos de KB de RAMCientos de MB a GB
OTP disponibleSubconjuntoOTP completo, Phoenix, Ecto, todo hex.pm compatible
ArranqueMilisegundosSegundos
ConsumoMilivatios, apto para bateriaVatios, normalmente alimentado de red
Caso tipicoNodo sensor, actuador, dispositivo a bateriaGateway, controlador con pantalla, vision, agregacion

La arquitectura habitual combina ambos: varios nodos AtomVM en el campo midiendo y actuando, hablando por MQTT o BLE con un gateway Nerves que agrega, decide y sube a la nube. Es la misma division que en cualquier sistema distribuido, solo que el borde tiene medio megabyte de RAM.

Tabla de compromisos

Esta es la tabla a la que vas a volver cuando tengas que elegir. Los valores de huella son ordenes de magnitud aproximados para un programa pequeño en un ESP32; varian mucho segun opciones de compilacion, version del SDK y que bibliotecas arrastres.

CriterioAssemblerCC++RustMicroPythonElixir / AtomVM
Huella en flashminimamuy bajabaja a mediabajamedia a alta (interprete)media (VM + app)
Uso de RAMminimomuy bajobajo si evitas heapbajoalto (heap del interprete)medio (heaps por proceso)
Velocidad de ejecucionmaximamaximamaximamaxima10x a 100x mas lentainterpretada, mas lenta que nativa
Determinismo temporaltotalaltoaltoaltobajo (recoleccion de basura)medio (GC por proceso, pausas cortas)
Seguridad de memorianingunaningunaescasaverificada en compilaciongestionada por el interpreteaislamiento por proceso
Modelo de concurrenciamanualtareas de RTOS + mutexigual que Ctareas + async (embassy, RTIC)asyncio cooperativoprocesos + mensajes, apropiativo
Tolerancia a fallosnulamanualmanualmanualexcepciones, sin aislamientosupervisores, let it crash
Velocidad de desarrollomuy lentamediamediamedia, empinada al principiomuy rapida (REPL)rapida
Depuracion en vivoJTAGJTAG, printfJTAG, printfprobe-rs, defmtREPL interactivoconsola, trazas
Soporte de fabricantestotaltotalampliocreciente, mayormente comunidadbueno en chips popularesacotado a plataformas soportadas
Curva de aprendizajemuy altamediaaltaaltamuy bajamedia si ya sabes Elixir

El arbol de decision

flowchart TD
    INICIO[Nuevo proyecto embebido] --> Q1{Requisito de tiempo real duro?<br/>lazos de control, seguridad critica}

    Q1 -->|Si| Q2{Certificacion obligatoria?<br/>DO-178C, IEC 62304, ISO 26262}
    Q2 -->|Si| ADA[Ada / SPARK o C con MISRA<br/>y proceso de certificacion]
    Q2 -->|No| Q3{Equipo con experiencia en Rust?}
    Q3 -->|Si| RUST[Rust no_std<br/>seguridad sin costo de ejecucion]
    Q3 -->|No| CBM[C o C++ bare metal / RTOS]

    Q1 -->|No| Q4{Cuanta RAM tiene el chip?}
    Q4 -->|"menos de 64 KB"| CBM
    Q4 -->|"64 KB a 256 KB"| Q5{Prioridad del proyecto?}
    Q4 -->|"mas de 256 KB"| Q6{El problema es<br/>principalmente de concurrencia<br/>y tolerancia a fallos?}

    Q5 -->|Huella minima| CBM
    Q5 -->|Prototipar rapido| MPY[MicroPython]

    Q6 -->|Si| ELIXIR[Elixir sobre AtomVM<br/>procesos, mensajes, supervisores]
    Q6 -->|No, es calculo o senales| Q7{Necesitas bibliotecas<br/>del fabricante?}
    Q7 -->|Si| CBM
    Q7 -->|No| MPY

    ELIXIR --> Q8{Necesitas Linux,<br/>vision o OTP completo?}
    Q8 -->|Si| NERVES[Nerves sobre Raspberry Pi]
    Q8 -->|No| OK[AtomVM en el MCU]

Nota que este arbol no tiene un ganador. Un proyecto real suele mezclar: el lazo de control de motores en C bare metal sobre un STM32, el nodo sensor con AtomVM sobre ESP32, el gateway con Nerves sobre Raspberry Pi. Elegir bien significa elegir por capa, no por preferencia.

Las competencias que sostienen todo esto

Independiente del lenguaje, hay un cuerpo de conocimiento que todo desarrollador de sistemas incrustados termina necesitando. Vale la pena tenerlo mapeado porque marca que estudiar despues.

Electronica de base: ley de Ohm y divisores resistivos (capitulo 3), conversion analogico-digital y su resolucion, comportamiento de capacitores e inductores, transistores como conmutadores, diodos de proteccion contra fuerza contraelectromotriz, y —la mas subestimada— saber leer un datasheet hasta el final.

Perifericos del microcontrolador: GPIO, temporizadores y PWM, watchdog (el perro guardian que reinicia el sistema si el firmware se cuelga), ADC y DAC, interrupciones y sus prioridades, DMA, y modos de bajo consumo.

Protocolos cableados: I2C (dos hilos, muchos dispositivos, direccionamiento por bus), SPI (mas rapido, un hilo de seleccion por dispositivo), UART (punto a punto, asincrono), CAN (robusto, tolerante a ruido, estandar automotriz).

Protocolos inalambricos: BLE, WiFi, LoRa, Zigbee para la capa fisica; MQTT y CoAP para la capa de aplicacion.

Herramientas: analizador logico, osciloscopio, multimetro, depurador JTAG/SWD, y del lado del software: control de versiones, integracion continua que compile el firmware en cada cambio, y pruebas automatizadas sobre hardware real.

Del lado del hardware, si vas a diseñar placas y no solo programarlas: esquematicos y PCB en KiCad (libre y gratuito), EasyEDA o Altium; y las normas IPC que la industria usa como criterio de aceptacion.

NormaQue cubreCuando te la piden
IPC-A-610Aceptabilidad de ensamblajes electronicosInspeccion visual de placas ensambladas
IPC J-STD-001Requisitos de soldaduraCriterios de aceptacion de uniones soldadas
IPC/WHMA-A-620Ensamblajes de cables y arnesesTerminacion de conectores y mazos
IPC-A-600Aceptabilidad de PCB desnudasCalidad del circuito impreso antes de montar

Errores comunes

ErrorSintoma que vesCausa realSolucion
Elegir el lenguaje antes que el chipEl proyecto se atasca al no haber soporteMicroPython o AtomVM necesitan RAM y un port existenteElegir primero el chip por requisitos electricos y de consumo, despues el lenguaje entre los que lo soportan
Usar delay() bloqueante en todoEl dispositivo “no responde” mientras esperadelay ocupa la CPU sin cederCon RTOS usar vTaskDelay; en AtomVM Process.sleep cede el planificador; en bare metal usar maquinas de estado con timers
Desbordar la pilaReinicios aleatorios, valores corruptos sin patronBuffers grandes en variables locales o recursion sin control en CMover buffers a estaticos o al heap, aumentar el tamaño de pila de la tarea, activar deteccion de desbordamiento
Asignar memoria dentro de una interrupcionCuelgue o error de “memory allocation in interrupt”Los asignadores no son seguros en contexto de ISRPreasignar buffers, en la ISR solo levantar banderas o enviar a una cola
Suponer que la recoleccion de basura es gratisUn lazo de control pierde muestras esporadicamentePausas del recolector en MicroPython o en la VMSacar el lazo critico del lenguaje gestionado, o preasignar y minimizar la generacion de basura
Confundir MCU con MPUSe compra una Raspberry Pi para un dispositivo a bateriaEl MPU consume vatios y arranca en segundosUsar MCU para el borde a bateria y MPU solo donde haga falta Linux
Portar codigo de servidor Elixir tal cual a AtomVMErrores de modulo o funcion no encontradaAtomVM implementa un subconjunto de OTPConsultar la documentacion de modulos soportados de la version en uso antes de diseñar
Ignorar el watchdogEl equipo funciona en el escritorio y se congela en campoEl watchdog no se alimenta o esta deshabilitadoHabilitarlo y alimentarlo desde el punto que demuestre que el sistema esta vivo, no desde un timer ciego
No fijar los pines flotantesLecturas erraticas de un botonSin pull-up ni pull-down la entrada capta ruidoConfigurar la resistencia interna (:up o :down) o poner una externa
Arrastrar std::string y std::vector a un MCULa flash se llena, el heap se fragmentaContenedores dinamicos de C++ asignan memoria constantementeUsar contenedores de capacidad fija o buffers estaticos
Grabar la aplicacion sin el firmware de la VMEl chip no arranca o reinicia en bucleAtomVM y MicroPython requieren su firmware grabado primeroGrabar firmware una vez, despues iterar solo con la imagen de la aplicacion
Programar sin medir”Deberia funcionar” pero no funcionaNo se verifico voltaje ni señal realesMultimetro para continuidad y voltaje, analizador logico para I2C/SPI antes de culpar al codigo

Ejercicios propuestos

  1. Inventario domestico. Recorre tu casa y anota diez dispositivos que contengan un sistema incrustado. Para cada uno responde: ¿que tarea especifica hace?, ¿que sensores y actuadores crees que tiene?, ¿cual seria su restriccion dominante (costo, consumo, determinismo, seguridad)?

  2. Clasificacion MCU vs MPU. Busca las hojas de datos de un ESP32-C3, un STM32F103 y un Raspberry Pi Zero 2W. Arma una tabla con nucleo, frecuencia, RAM, almacenamiento, consumo tipico y si corre Linux. Decide cual usarias para: un sensor de humedad de suelo a bateria durante seis meses, un panel de control con pantalla tactil, y un lazo de control de un motor paso a paso.

  3. El mismo parpadeo, tres veces. Escribe el parpadeo de un LED en C con Arduino, en MicroPython y en Elixir con AtomVM. Mide con cronometro cuanto tiempo te toma cada uno desde cero hasta ver el LED funcionando, incluyendo instalar herramientas. Anota tambien el tamaño del binario o imagen resultante.

  4. Rompe la memoria a proposito. Compila el ejemplo de strcpy sin verificacion en C que aparece en este capitulo, primero en tu computador y despues en un microcontrolador. Documenta la diferencia de comportamiento. Despues reescribelo usando snprintf con limite y explica por que ahora es seguro.

  5. Concurrencia sin mutex. Extiende el ejemplo Estacion de Elixir agregando un cuarto proceso que simule la lectura de un sensor de temperatura enviando un valor aleatorio cada dos segundos, y haz que el coordinador guarde el maximo historico. Cuenta cuantas lineas cambiaste en el codigo existente.

  6. Diagrama tu proyecto. Elige una idea propia de robotica o IoT y dibuja en Mermaid la arquitectura por capas: que corre en el borde, que en el gateway, que en la nube, y con que protocolo se comunican. Justifica el lenguaje elegido en cada capa usando la tabla de compromisos.

  7. Presupuesto de memoria. Para un ESP32 con 520 KB de SRAM, estima cuanta memoria quedaria libre para tu aplicacion despues del stack de WiFi, un buffer de 4 KB para lecturas y la VM. Busca en la documentacion de AtomVM o de MicroPython cifras reales y compara con tu estimacion.

  8. Lectura de datasheet. Toma la hoja de datos del PIC16F877A y localiza: cantidad de pines de E/S, tamaño de memoria de programa, tamaño de RAM, canales de ADC y su resolucion, y el voltaje de operacion. Vas a necesitar esos datos en el proximo capitulo.

Que viene despues

Ya sabes que es un sistema incrustado, como se organiza por dentro, como llega tu codigo al chip y que gana y que pierde cada lenguaje. Tienes tambien el criterio para no elegir por moda: primero el chip segun requisitos electricos y de consumo, despues el lenguaje entre los que ese chip soporta, y despues la arquitectura por capas.

En el capitulo 6 bajamos de la teoria a tres piezas de silicio concretas que marcaron epoca y que siguen siendo excelentes para aprender: el temporizador IC 555, que genera pulsos sin una sola linea de codigo y te obliga a pensar en constantes de tiempo RC; el PIC16F877A, el microcontrolador con el que se formaron generaciones enteras y cuyo ensamblador ya viste asomar en este capitulo; y el BASIC Stamp, el primer intento serio de programar un microcontrolador con un lenguaje de alto nivel, antecesor directo de la idea que AtomVM lleva al extremo. Entender de donde venimos hace mucho mas claro por que hoy podemos escribir Elixir sobre un chip de medio megabyte.

El indice completo del curso esta en /tecnologias/elixir-robotics/00-indice/.