Sistemas incrustados y los lenguajes que los programan
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
| Sector | Ejemplo tipico | Restriccion dominante |
|---|---|---|
| Automotriz | Unidad de control de motor, ABS, airbag | Determinismo duro, certificacion ISO 26262 |
| Dispositivos medicos | Bomba de infusion, oximetro, marcapasos | Seguridad del paciente, trazabilidad |
| IoT de consumo | Sensor de temperatura con WiFi, enchufe inteligente | Costo por unidad y consumo energetico |
| Industria 4.0 | PLC, sensor de vibracion, gateway Modbus | Robustez electrica, disponibilidad continua |
| Robotica | Controlador de motores, IMU, planificador local | Latencia de lazo de control |
| Aeroespacial | Aviones, satelites, cohetes | Tolerancia a radiacion y a fallos |
| Electronica de consumo | Audifonos, camaras, electrodomesticos | Costo, 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
| Placa | Nucleo | Frecuencia | RAM | Flash | Nota |
|---|---|---|---|---|---|
| Arduino Uno (ATmega328P) | AVR 8 bits | 16 MHz | 2 KB SRAM | 32 KB | El clasico didactico |
| Raspberry Pi Pico (RP2040) | 2x Cortex-M0+ | 133 MHz | 264 KB SRAM | 2 MB externa | Muy barata, sin radio |
| ESP32 (clasico) | 2x Xtensa LX6 | 240 MHz | ~520 KB SRAM | 4 MB tipico | WiFi + Bluetooth |
| ESP32-C3 | RISC-V 32 bits | 160 MHz | ~400 KB SRAM | 4 MB tipico | WiFi + BLE, mas barato |
| STM32F4 (gama media) | Cortex-M4F | 84-180 MHz | 96-192 KB | 512 KB - 1 MB | Industrial, muchos perifericos |
| Raspberry Pi 5 | 4x Cortex-A76 + MMU | 2.4 GHz | 4-16 GB | microSD | Ya 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:
-
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-gccoxtensa-esp32-elf-gcc. -
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.
-
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
| Lenguaje | Base | Donde brilla | Limitacion principal |
|---|---|---|---|
| Ada / SPARK | Compilado nativo | Aeronautica, ferrocarriles, medicina; verificacion formal, aritmetica de punto fijo para chips sin FPU | Ecosistema pequeño, herramientas comerciales |
| TinyGo | Compilador Go sobre LLVM | Codigo Go con goroutines en MCU, buen soporte WASM | Recoleccion de basura, subconjunto de la biblioteca de Go |
| Espruino | Interprete JavaScript | Prototipado rapido, gente que ya sabe JS | Rendimiento y huella del interprete |
| PicoRuby / mruby | Interprete Ruby compacto | Sintaxis Ruby en MCU, usado en el mundo educativo japones | Comunidad reducida fuera de nichos |
| Zig | Compilado nativo | Interoperabilidad con C sin fricciones, control de asignadores | Lenguaje joven, API en movimiento |
| Erlang / Elixir | Bytecode BEAM sobre AtomVM | Concurrencia y tolerancia a fallos | VM 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:
- 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.
- 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.
- 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.
- 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.
| Aspecto | AtomVM | Nerves |
|---|---|---|
| Hardware objetivo | Microcontroladores: ESP32, STM32, RP2040 | Sistemas con Linux: Raspberry Pi, BeagleBone, x86 |
| Sistema operativo | Ninguno o RTOS por debajo | Linux minimo construido con Buildroot |
| Maquina virtual | AtomVM (reimplementacion compacta en C) | La BEAM completa de Erlang/OTP |
| Memoria tipica | Cientos de KB de RAM | Cientos de MB a GB |
| OTP disponible | Subconjunto | OTP completo, Phoenix, Ecto, todo hex.pm compatible |
| Arranque | Milisegundos | Segundos |
| Consumo | Milivatios, apto para bateria | Vatios, normalmente alimentado de red |
| Caso tipico | Nodo sensor, actuador, dispositivo a bateria | Gateway, 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.
| Criterio | Assembler | C | C++ | Rust | MicroPython | Elixir / AtomVM |
|---|---|---|---|---|---|---|
| Huella en flash | minima | muy baja | baja a media | baja | media a alta (interprete) | media (VM + app) |
| Uso de RAM | minimo | muy bajo | bajo si evitas heap | bajo | alto (heap del interprete) | medio (heaps por proceso) |
| Velocidad de ejecucion | maxima | maxima | maxima | maxima | 10x a 100x mas lenta | interpretada, mas lenta que nativa |
| Determinismo temporal | total | alto | alto | alto | bajo (recoleccion de basura) | medio (GC por proceso, pausas cortas) |
| Seguridad de memoria | ninguna | ninguna | escasa | verificada en compilacion | gestionada por el interprete | aislamiento por proceso |
| Modelo de concurrencia | manual | tareas de RTOS + mutex | igual que C | tareas + async (embassy, RTIC) | asyncio cooperativo | procesos + mensajes, apropiativo |
| Tolerancia a fallos | nula | manual | manual | manual | excepciones, sin aislamiento | supervisores, let it crash |
| Velocidad de desarrollo | muy lenta | media | media | media, empinada al principio | muy rapida (REPL) | rapida |
| Depuracion en vivo | JTAG | JTAG, printf | JTAG, printf | probe-rs, defmt | REPL interactivo | consola, trazas |
| Soporte de fabricantes | total | total | amplio | creciente, mayormente comunidad | bueno en chips populares | acotado a plataformas soportadas |
| Curva de aprendizaje | muy alta | media | alta | alta | muy baja | media 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.
| Norma | Que cubre | Cuando te la piden |
|---|---|---|
| IPC-A-610 | Aceptabilidad de ensamblajes electronicos | Inspeccion visual de placas ensambladas |
| IPC J-STD-001 | Requisitos de soldadura | Criterios de aceptacion de uniones soldadas |
| IPC/WHMA-A-620 | Ensamblajes de cables y arneses | Terminacion de conectores y mazos |
| IPC-A-600 | Aceptabilidad de PCB desnudas | Calidad del circuito impreso antes de montar |
Errores comunes
| Error | Sintoma que ves | Causa real | Solucion |
|---|---|---|---|
| Elegir el lenguaje antes que el chip | El proyecto se atasca al no haber soporte | MicroPython o AtomVM necesitan RAM y un port existente | Elegir primero el chip por requisitos electricos y de consumo, despues el lenguaje entre los que lo soportan |
Usar delay() bloqueante en todo | El dispositivo “no responde” mientras espera | delay ocupa la CPU sin ceder | Con RTOS usar vTaskDelay; en AtomVM Process.sleep cede el planificador; en bare metal usar maquinas de estado con timers |
| Desbordar la pila | Reinicios aleatorios, valores corruptos sin patron | Buffers grandes en variables locales o recursion sin control en C | Mover buffers a estaticos o al heap, aumentar el tamaño de pila de la tarea, activar deteccion de desbordamiento |
| Asignar memoria dentro de una interrupcion | Cuelgue o error de “memory allocation in interrupt” | Los asignadores no son seguros en contexto de ISR | Preasignar buffers, en la ISR solo levantar banderas o enviar a una cola |
| Suponer que la recoleccion de basura es gratis | Un lazo de control pierde muestras esporadicamente | Pausas del recolector en MicroPython o en la VM | Sacar el lazo critico del lenguaje gestionado, o preasignar y minimizar la generacion de basura |
| Confundir MCU con MPU | Se compra una Raspberry Pi para un dispositivo a bateria | El MPU consume vatios y arranca en segundos | Usar MCU para el borde a bateria y MPU solo donde haga falta Linux |
| Portar codigo de servidor Elixir tal cual a AtomVM | Errores de modulo o funcion no encontrada | AtomVM implementa un subconjunto de OTP | Consultar la documentacion de modulos soportados de la version en uso antes de diseñar |
| Ignorar el watchdog | El equipo funciona en el escritorio y se congela en campo | El watchdog no se alimenta o esta deshabilitado | Habilitarlo y alimentarlo desde el punto que demuestre que el sistema esta vivo, no desde un timer ciego |
| No fijar los pines flotantes | Lecturas erraticas de un boton | Sin pull-up ni pull-down la entrada capta ruido | Configurar la resistencia interna (:up o :down) o poner una externa |
Arrastrar std::string y std::vector a un MCU | La flash se llena, el heap se fragmenta | Contenedores dinamicos de C++ asignan memoria constantemente | Usar contenedores de capacidad fija o buffers estaticos |
| Grabar la aplicacion sin el firmware de la VM | El chip no arranca o reinicia en bucle | AtomVM y MicroPython requieren su firmware grabado primero | Grabar firmware una vez, despues iterar solo con la imagen de la aplicacion |
| Programar sin medir | ”Deberia funcionar” pero no funciona | No se verifico voltaje ni señal reales | Multimetro para continuidad y voltaje, analizador logico para I2C/SPI antes de culpar al codigo |
Ejercicios propuestos
-
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)?
-
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.
-
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.
-
Rompe la memoria a proposito. Compila el ejemplo de
strcpysin verificacion en C que aparece en este capitulo, primero en tu computador y despues en un microcontrolador. Documenta la diferencia de comportamiento. Despues reescribelo usandosnprintfcon limite y explica por que ahora es seguro. -
Concurrencia sin mutex. Extiende el ejemplo
Estacionde 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. -
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.
-
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.
-
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/.