Introducción: El software cuando no hay servidor ni navegador
Hola devs, ustedes saben que como desarrolladores de software modernos, estamos acostumbrados a la abundancia de recursos: gigabytes de RAM, sistemas de archivos abstractos, garbage collectors, contenedores Docker y sistemas operativos que protegen nuestros procesos si cometemos un error de punteros o de memoria.
Sin embargo, existe un universo paralelo del desarrollo de software donde un único firmware se ejecuta directamente sobre un microcontrolador, controlando desde el sistema de frenos de un automóvil hasta un marcapasos o un termostato inteligente. Bienvenido al mundo de los sistemas embebidos y de los Sistemas Operativos de Tiempo Real (RTOS) de estos últimos te hablé en mis últimos artículos.
Lo interesante es que, aunque seguimos escribiendo software, muchas de las reglas que damos por sentadas en el desarrollo backend o de escritorio simplemente dejan de existir.
1. La arquitectura de memoria: sin memoria virtual ni "redes de seguridad"
En una aplicación convencional que corre sobre Linux, Windows o macOS, el sistema operativo utiliza memoria virtual. Si nuestro programa intenta escribir en una dirección de memoria que no le pertenece, el sistema operativo detecta el problema y finaliza el proceso mediante un Segmentation Fault (Linux) o una Access Violation (Windows). El resto del sistema continúa funcionando normalmente.
La mayoría de microcontroladores, en cambio, no poseen una Unidad de Gestión de Memoria (MMU). Algunos dispositivos modernos incorporan una MPU (Memory Protection Unit) con capacidades limitadas de protección, pero en general el firmware ejecuta código con acceso prácticamente directo a toda la memoria disponible.
Una distribución típica de memoria puede representarse así:

Nota: La ubicación exacta del heap y del stack depende del linker script y de la arquitectura del microcontrolador. El diagrama anterior representa una organización típica, no una regla universal.
¿Quién decide dónde vive cada sección de memoria?
Hasta ahora hemos visto una distribución típica de la memoria de un microcontrolador. Sin embargo, esa organización no ocurre por arte de magia.
A diferencia de una aplicación de escritorio, donde el sistema operativo administra automáticamente la memoria de cada proceso, en un firmware es el linker quien decide exactamente en qué dirección física residirá cada sección del programa.
Esa distribución se define mediante un linker script, un archivo que indica dónde deben ubicarse el código (
.text), las constantes (.rodata), las variables (.data,.bss), el heap y el stack dentro de la memoria del dispositivo.Gracias a este mecanismo, el desarrollador tiene un control mucho más directo sobre la organización física de la memoria que en cualquier aplicación convencional.
El riesgo del Stack Overflow sin aislamiento
En una aplicación .NET o Java, una recursión infinita suele provocar una excepción de desbordamiento de pila y el proceso termina de forma controlada.
En un microcontrolador, la situación puede ser mucho más delicada, ojito con esto dev.
Si el stack crece más allá del espacio reservado, normalmente no existe un mecanismo automático que detenga la ejecución antes de corromper la memoria. Dependiendo de la arquitectura, el resultado puede ser:
- Corrupción silenciosa de variables.
- Sobrescritura de otras regiones de memoria.
- Excepciones de hardware (HardFault, BusFault, etc.).
- Reinicios inesperados del dispositivo.
En un sistema crítico, cualquiera de estos escenarios puede producir un comportamiento impredecible.
2. Concurrencia y el Sistema Operativo de Tiempo Real (RTOS)
Como ya hemos analizado en los artículos anteriores: mientras que sistemas operativos como Linux o Windows buscan maximizar el throughput (el rendimiento global del sistema), un RTOS (Real-Time Operating System) tiene un objetivo diferente:
Garantizar que cada tarea crítica se ejecute antes de su tiempo límite (deadline).
¿Qué significa realmente "tiempo real"?
Una confusión muy común es pensar que "tiempo real" significa "muy rápido" pero nah!
No es así.
Un sistema de tiempo real es aquel capaz de responder dentro de un tiempo máximo conocido y garantizado.
Por ejemplo, si un sistema de frenado debe reaccionar en menos de 5 milisegundos, lo importante no es que normalmente responda en 500 microsegundos, sino que nunca supere esos 5 milisegundos.
Ese comportamiento predecible se conoce como determinismo temporal.
Interrupciones (ISR): cuando el hardware toma el control
Cuando ocurre un evento físico por ejemplo, la activación de un sensor de impacto el procesador ejecuta una Rutina de Servicio de Interrupción (ISR).

Es importante notar que una ISR no es una tarea del RTOS. Las interrupciones tienen su propio mecanismo de prioridad, independiente del planificador del sistema operativo.
Por esta razón, el código dentro de una ISR suele mantenerse extremadamente corto: únicamente registra el evento y despierta una tarea que realizará el trabajo pesado.
Tareas y planificación expropiativa
En un RTOS, el software suele organizarse en tareas (threads) con diferentes niveles de prioridad.
El modelo más común es la planificación expropiativa por prioridades.
Si una tarea de prioridad alta pasa al estado listo (Ready), el scheduler suspende inmediatamente la ejecución de cualquier tarea de prioridad inferior para darle el procesador.
Por ejemplo:
- Alta prioridad: control del sistema de frenos.
- Media prioridad: adquisición de sensores.
- Baja prioridad: registro de eventos o comunicación.
Esto permite garantizar tiempos de respuesta predecibles para las funciones críticas.
Memoria estática en las tareas
Aunque muchos RTOS modernos permiten crear tareas dinámicamente, en sistemas críticos suele preferirse la asignación estática de memoria.
Cada tarea dispone de un stack con un tamaño definido previamente.
Si ese tamaño se calcula incorrectamente, la tarea puede desbordar su pila y provocar corrupción de memoria, uno de los errores más difíciles de detectar en firmware.
3. Del mundo físico al código: ADC y PWM
Mientras un servidor procesa peticiones HTTP o mensajes JSON, un microcontrolador trabaja directamente con señales eléctricas.
ADC (Analog-to-Digital Converter)
Un convertidor analógico-digital transforma un voltaje físico en un valor numérico.
Por ejemplo, un sensor que entrega una señal entre 0 V y 5 V puede convertirse en un entero de:
- 10 bits → valores entre 0 y 1023.
- 12 bits → valores entre 0 y 4095.
Ese valor digital es el que finalmente procesa el software.
PWM (Pulse Width Modulation)
El PWM permite controlar actuadores utilizando únicamente señales digitales.
En lugar de variar directamente el voltaje, el microcontrolador genera pulsos muy rápidos cuyo ciclo de trabajo (duty cycle) determina la potencia efectiva entregada a la carga.
Gracias a esta técnica es posible controlar:
- Motores eléctricos.
- Servomotores.
- Válvulas.
- LEDs de intensidad variable.
- Resistencias calefactoras.
Todo ello utilizando simples salidas digitales.
4. El hardware también se programa: Memory-Mapped I/O
Una de las diferencias más sorprendentes para un desarrollador backend es que, en un microcontrolador, muchos periféricos se controlan escribiendo directamente en memoria.
No existe una API del sistema operativo ni una llamada al kernel.
Por ejemplo:
GPIOA->ODR |= (1 << 5);
Aunque parezca una variable, en realidad GPIOA->ODR representa un registro físico del microcontrolador.
Al escribir en esa dirección de memoria, el procesador modifica directamente el estado de un pin eléctrico.
Este mecanismo se conoce como Memory-Mapped I/O y es una de las bases de la programación embebida.
5. ¿Por qué C y las variables globales siguen dominando este entorno?
En aplicaciones empresariales solemos considerar las variables globales como un antipatrón.
Sin embargo, en firmware la situación es diferente.
Históricamente, C ha dominado este entorno porque ofrece un control extremadamente preciso sobre la memoria y el hardware.
Además, las variables globales siguen siendo comunes por varias razones:
- Compartir estado entre tareas e interrupciones.
- Evitar copias innecesarias de estructuras grandes.
- Representar registros de hardware mapeados en memoria.
- Trabajar con cantidades muy limitadas de RAM.
- Mantener compatibilidad con grandes bases de código heredadas.
Por la misma razón, muchos sistemas embebidos también evitan el uso de memoria dinámica durante la operación normal.
Aunque funciones como malloc() y free() existen y son perfectamente válidas en muchos entornos, en aplicaciones críticas suelen evitarse porque pueden introducir fragmentación de memoria, tiempos de ejecución impredecibles y fallos por falta de memoria. En su lugar, es habitual reservar toda la memoria necesaria durante el arranque del sistema y reutilizarla durante toda la vida del firmware.
Esto no significa que las variables globales deban utilizarse indiscriminadamente...
En conclusión...
Desarrollar software embebido implica cambiar completamente la forma de pensar, es otro paradigma.
Ya no contamos con memoria virtual, procesos aislados ni un sistema operativo dispuesto a protegernos de cada error. El software interactúa mucho más cerca del hardware y, en muchos casos, un simple desbordamiento de memoria o una mala planificación temporal puede traducirse en un fallo físico del dispositivo.
Comprender cómo se organiza la memoria, cómo funciona un RTOS y cómo el procesador interactúa directamente con el hardware es el primer paso para entender por qué los sistemas embebidos exigen estándares de diseño muy diferentes a los del desarrollo empresarial.
Estimado dev, en el próximo artículo de esta serie exploraremos algunos de los mecanismos que permiten que estos sistemas sean confiables incluso cuando un error parece inevitable: Watchdog Timers, tolerancia a fallos, supervisión del sistema y estándares como MISRA C, diseñados para reducir al mínimo la posibilidad de que un solo bug termine comprometiendo un sistema crítico, ¡No te lo pierdas, crack!
Y ya sabes qué hacer con este artículo, ¡compártelo!
Créditos de imagen de portada: Foto de Anne Nygård en Unsplash
