← Índice del curso

Sistemas Embebidos · Tema 49 · Bloque 11

Diseño de la arquitectura

Con los requisitos del tema anterior en la mano, llega el momento de decidir cómo se construirá el sistema. La arquitectura es el conjunto de decisiones que más cuesta cambiar una vez tomadas: qué microcontrolador, cómo se divide el firmware en módulos, cómo se hablan entre sí, qué corre en una interrupción y qué en una tarea. Un buen diseño se reconoce porque esas decisiones salen de los requisitos, se comparan con números y quedan registradas con su motivo; uno malo, porque «se fue armando» y a los seis meses nadie se anima a tocarlo. Este tema sigue con el registrador de temperatura para la cadena de frío y recorre las decisiones en el orden en que se toman: la arquitectura de hardware, la elección del microcontrolador, la división en módulos y capas, las interfaces, el modelo de ejecución y la comprobación de plazos.

01 · El puente

De los requisitos a la arquitectura

Una definición práctica: la arquitectura de un sistema es el conjunto de decisiones de diseño que son costosas de revertir y que determinan si se cumplen los requisitos no funcionales. Cambiar el nombre de una variable es barato; cambiar de microcontrolador cuando el firmware ya está escrito, o descubrir que el diseño de tareas no cumple el plazo de la alarma, significa rehacer meses de trabajo. Por eso estas decisiones se toman al principio, con cuentas, y se revisan con los interesados.

Las decisiones no se toman «por gusto»: cada una se justifica con un atributo de calidad de la especificación (tema 48). Una buena manera de pensar la arquitectura es como una cadena de preguntas y respuestas con números:

Del requisito a la decisión de arquitectura, en el registrador.
Requisito (tema 48)Pregunta de arquitecturaDecisión que toma
2 años con 2 pilas AA: 53,28 µA de objetivo de diseño¿Qué procesador y qué modelo de ejecución caben en esa corriente?Microcontrolador de muy bajo consumo; ejecución por eventos con sueño profundo; sin tick fijo
Alarma en menos de 5 s¿Qué ocurre en una interrupción y qué en una tarea, y con qué prioridades?Medición periódica de 2 s en la zona de alerta; la radio con plazo propio
30 días de datos, 345 600 B¿Dónde se guardan y cómo?Flash externa con registro circular (tema 41)
Actualización segura por radio¿Cuánta flash hace falta y dónde está la imagen de respaldo?Cargador de arranque y dos imágenes; el respaldo, en el chip o en la flash externa
Mantenimiento por 10 años¿Cómo se organiza el código para que lo entienda quien no lo escribió?Módulos con interfaces claras y dependencias en un solo sentido

Una arquitectura se describe con varias vistas, cada una responde a una pregunta distinta: la de hardware (qué componentes y cómo se conectan), la de módulos (cómo se divide el código y quién depende de quién), la de ejecución (qué se ejecuta en cada interrupción y en cada tarea, y cuándo), la de datos (qué información fluye y dónde se guarda) y la de memoria (el mapa de flash y RAM, tema 28). No hay una vista «completa»; hay un conjunto que, junto, permite razonar sobre el sistema.

El proceso para cada decisión importante es el mismo, y es la mejor protección contra las decisiones por costumbre: (1) enumerar alternativas (al menos dos, aunque una sea «lo que hicimos la última vez»), (2) evaluarlas contra los requisitos con números, no con opiniones, (3) decidir, y (4) registrar la decisión con sus alternativas descartadas y el motivo, para que dentro de un año se pueda saber si el motivo sigue valiendo (sección 8).

02 · Los bloques físicos

La arquitectura de hardware

El primer boceto es un diagrama de bloques del hardware: qué componentes hay y cómo se conectan. Para el registrador, con las decisiones de los temas anteriores, queda así:

Diagrama de bloques del registrador de temperatura Las dos pilas AA alimentan un regulador de bajo consumo que da 3,3 voltios al microcontrolador, al sensor de temperatura, a la memoria flash externa y a la radio. El microcontrolador se comunica con el sensor por I2C, con la memoria flash y con la radio por SPI, y tiene un botón, un LED y un conector de depuración SWD. 2 pilas AA3 V nominales Reguladorbajo consumo en reposo Microcontroladornúcleo de bajo consumoRTC · ADC · watchdog Sensor de temperaturaFlash externa (datos)Radio Botón · LEDConector SWD I²CSPISPI3,3 V

Detrás de cada bloque hay decisiones de partición hardware y software: qué se resuelve con un componente y qué con código. Cada una tiene un costo en dinero, energía, tamaño y complejidad del firmware:

Decisiones de partición de hardware y software en el registrador.
DecisiónAlternativasCriterio
Sensor de temperaturaDigital integrado (I²C) o analógico (termistor) leído con el ADC del chipEl digital ahorra calibración y código y consume poco; el analógico cuesta menos pero exige calibrar y un ADC bien referenciado. Requisito de ±0,5 °C: el digital lo cumple de fábrica
Almacenamiento de datosFlash interna, flash externa, FRAM345 600 B no caben junto a dos imágenes de 73 KB en una flash interna chica: se necesita una externa (tema 41); la FRAM evita el desgaste pero cuesta más
Reloj de tiempo realEl RTC integrado en el chip o uno externoEl integrado ahorra un componente; el externo da más exactitud con la temperatura. Alcanza si la deriva admitida lo permite (tema 34)
WatchdogEl del chip o uno externoUno externo sobrevive a una falla del reloj del chip (tema 38); para el producto puede alcanzar el interno con ventana
RadioMódulo con certificación o diseño propio; radio integrada en el microcontroladorUn módulo certificado ahorra meses de homologación; la radio integrada ahorra espacio y costo pero fija el microcontrolador (y su consumo en reposo)
AlimentaciónRegulador lineal, conmutado o conexión directa a la pilaLa corriente de reposo del regulador cuenta entera dentro de los 53 µA (tema 33); a veces conviene alimentar directo si el rango del chip lo admite
Depuración y pruebasConector SWD y puntos de prueba, o ningunoPocos centavos que valen meses de depuración (temas 44 y 45); se deciden ahora, porque después no se puede soldar

Estas decisiones conviene tomarlas junto con quien diseña el hardware, antes de enviar la placa a fabricar: una resistencia que falta o un pin mal elegido se corrige en el firmware con trucos o en la placa con una nueva fabricación de semanas. Una norma útil es dejar margen en el hardware donde es barato: un pin de sobra, una huella para un componente opcional, un conector de depuración, puntos de prueba en las líneas de comunicación, y un microcontrolador de la misma familia con más memoria, de huella compatible, por si el firmware crece.

03 · La decisión más visible

Elegir el microcontrolador

La elección del microcontrolador debería ser una consecuencia de los requisitos, y no el punto de partida. El método tiene dos pasos, y el orden importa: primero se descartan los candidatos que no cumplen las condiciones obligatorias, y luego se comparan los que quedan con un puntaje ponderado. Mezclar los dos pasos (poniéndole puntos a un chip al que le falta la memoria) lleva a elegir un chip imposible por un buen puntaje en lo demás.

Criterios habituales para elegir un microcontrolador.
CriterioQué se miraCómo se relaciona con los requisitos
Memoria (flash y RAM)Capacidad, con margen para crecer; flash con borrado por páginas; ¿doble banco?Presupuesto por módulo (abajo) con margen; actualización A/B (tema 42)
ConsumoCorriente en sueño con el reloj de tiempo real activo, corriente en funcionamiento por MHz, tiempo y energía de despertarPresupuesto de corriente media (tema 48)
RendimientoNúcleo y frecuencia, unidad de coma flotante, aceleradoresTiempo de cálculo de las tareas; suele sobrar en un sensor
PeriféricosADC (bits, canales), interfaces (I²C, SPI, UART), temporizadores, RTC, DMA, comparadoresInterfaces de la especificación; uso de DMA para dormir más (tema 35)
Rango de alimentaciónTensión mínima y máxima; comportamiento con la pila casi agotadaCurva de descarga de las pilas (tema 36)
SeguridadArranque seguro, aceleración criptográfica, protección de lectura, almacenamiento de clavesRequisitos de seguridad (tema 43)
CostoPrecio por volumen, no a la unidad, más los componentes que ahorra o exigeCosto de materiales máximo
Herramientas y ecosistemaCompilador, depurador (tema 45), bibliotecas, ejemplos, comunidad, experiencia del equipoPlazo y riesgo del proyecto
Disponibilidad y ciclo de vidaPlazos de entrega, proveedores alternativos, años de producción garantizados, familia compatible hacia arriba y hacia abajoVida del producto de 10 años; riesgo de faltantes
Erratas y madurezLista de errores conocidos del silicio, versión del chipConfiabilidad (tema 37)

La Parte 1 del programa lo hace con el registrador y cinco candidatos ficticios (para no apoyarnos en datos de un fabricante concreto: en un proyecto real se llenan con la hoja de datos). Primero, el presupuesto de memoria de los módulos, que da lo que debe cumplir el chip:

  • Flash: la aplicación suma 14 + 3 + 8 + 38 + 4 + 6 = 73 KB. Con un margen del 30 % y el requisito de actualización A/B (dos imágenes en el chip, tema 42) más 16 KB del cargador de arranque, hacen falta 73 × 1,3 × 2 + 16 = 205,8 KB.
  • RAM: 6324 B de los módulos más 1536 B de pila son 7860 B; con el margen, 10,0 KB.
  • Pero una decisión de arquitectura cambia la cuenta: si la imagen de respaldo se guarda en la flash externa, solo hace falta un espacio de aplicación en el chip: 73 × 1,3 + 16 = 110,9 KB.
Candidatos ficticios y resultado con A/B en el chip (sueño de 2,0 µA como máximo, perfil equilibrado).
CandidatoFlashRAMSueñoCostoResultado
A (M0+, bajo consumo)128 KB16 KB0,8 µA1,40 USDDescartado: flash
B (M4, bajo consumo)512 KB128 KB1,5 µA3,10 USD6,49 puntos (elegido)
C (M0, económico)64 KB8 KB3,0 µA0,80 USDDescartado: flash, RAM y sueño
D (M33, con seguridad)1024 KB256 KB2,0 µA4,60 USD5,66 puntos
E (M4 con radio)512 KB96 KB2,5 µA4,20 USDDescartado: sueño

Dos cosas llaman la atención. Primera: el candidato B gana con los cuatro perfiles de pesos (costo, consumo, equilibrado y riesgo), así que la elección es robusta: no depende de cómo se pondere. Segunda, y más importante: si la imagen de respaldo va a la flash externa, el candidato A (el más barato y de menor consumo) pasa a cumplir y gana con los cuatro perfiles (7,69 puntos en el equilibrado, contra 6,49 de B): una decisión de arquitectura de un componente de unos centavos cambió el microcontrolador elegido y ahorró 1,70 USD por unidad, y casi la mitad de la corriente de sueño. Esa interacción entre decisiones es la razón de evaluar la arquitectura como un conjunto y no pieza por pieza.

Algunas precauciones al elegir: los números de la hoja de datos son «típicos», y el consumo real depende de qué periféricos estén encendidos (tema 36), por lo que conviene medir en una placa de evaluación antes de decidir; la elección frágil se detecta variando los pesos (si cambiar 10 puntos de peso invierte el resultado, no hay un ganador claro y la decisión se debe tomar por otro criterio); se prefiere una familia con variantes compatibles, para pasar a más memoria sin rediseñar; y se verifica el abastecimiento (plazos, alternativas): un chip perfecto que no se consigue no es una opción.

04 · Partir el firmware

Dividir el firmware en módulos

Un firmware de decenas de miles de líneas no se puede entender entero. Se divide en módulos, cada uno con una responsabilidad clara, y se organizan en capas con una regla simple: los módulos de una capa solo dependen de los de las capas inferiores, nunca al revés. Las capas típicas de un firmware (que ya aparecieron en el tema 10) son:

Capas de un firmware y sus módulos en el registrador.
CapaFunciónMódulos del registrador
3. AplicaciónLa lógica del producto: estados, políticas, secuenciasapp_estado (modos de operación, decisiones de alarma y de reporte)
2. ServiciosFunciones reutilizables independientes del productoregistro (registro circular con CRC), protocolo (tramas de radio), energia (modos de sueño y presupuesto), comandos (consola y configuración)
1. ControladoresHablan con un componente concretodrv_sensor, drv_flash, drv_radio
0. HALAcceso al hardware del microcontroladorhal_gpio, hal_i2c, hal_spi, hal_uart, hal_timer

Las capas dan dos beneficios que ya vimos en el tema 47. Si el microcontrolador cambia (como en la elección anterior), solo se reescribe la capa 0, y si cambia el sensor, solo el controlador. Y la lógica de las capas superiores se puede probar en la PC con el HAL falso. Dos ideas guían el reparto:

  • Alta cohesión: todo lo que hay dentro de un módulo tiene que ver con una sola responsabilidad. Un módulo «utilidades» con funciones sin relación es la señal contraria.
  • Bajo acoplamiento: los módulos se conocen lo mínimo, solo a través de su interfaz pública (sección siguiente). Si cambiar un módulo obliga a tocar otros cinco, el acoplamiento es alto. Y la ocultación de información es la herramienta: cada módulo esconde sus decisiones internas (cómo guarda los datos, qué registros usa) y expone solo lo que otros necesitan.

La Parte 2 del programa analiza las dependencias de 13 módulos con esas capas y encuentra dos violaciones de la regla: drv_sensor → registro (un controlador de capa 1 que depende de un servicio de capa 2: sube) y registro → app_estado (un servicio que depende de la aplicación). Juntas forman ciclos (drv_sensor → registro → app_estado → drv_sensor y registro → app_estado → registro): ningún módulo del ciclo se puede probar, entender ni reutilizar sin los demás. La solución es invertir esas dependencias: el sensor entrega su dato a la aplicación, que decide qué hacer con él, y el registro recibe los datos por parámetro en lugar de pedirlos a la aplicación; con esos dos cambios, las violaciones bajan a 0.

Una medida útil es la inestabilidad de un módulo, I = dependencias salientes / (salientes + entrantes): cerca de 0 es un módulo del que todos dependen y que no depende de nadie (estable, difícil de cambiar sin romper a otros: el HAL, por ejemplo, hal_spi da 0,00); cerca de 1, un módulo que depende de muchos y del que nadie depende (inestable, fácil de cambiar: la aplicación, app_estado da 0,83). La regla sana es que las dependencias apunten hacia los módulos más estables: lo que cambia seguido (la política del producto) depende de lo que casi no cambia (el HAL), nunca al revés.

05 · Los contratos entre módulos

Interfaces entre módulos

La interfaz de un módulo es su archivo de cabecera: las funciones, los tipos y las constantes que otros pueden usar. Una buena interfaz es un contrato: dice qué se necesita para llamar a cada función (precondiciones), qué garantiza al volver (postcondiciones), cómo informa los errores, a quién pertenecen los buffers y si se puede llamar desde una interrupción. Lo que no dice el contrato, el que llama lo adivina, y adivina distinto.

/* registro.h: registro circular de mediciones en la flash externa (tema 41).
 * Responsabilidad: guardar y recuperar muestras; nada sabe de la radio ni del sensor.
 * Contexto: NO se puede llamar desde una interrupción (puede bloquear hasta 50 ms en un borrado). */
#ifndef REGISTRO_H
#define REGISTRO_H
#include <stdint.h>
#include <stdbool.h>

typedef struct { uint32_t instante_s; int16_t temp_centesimas; uint16_t estado; } Muestra;    /* 8 bytes */

typedef enum { REG_OK = 0, REG_LLENO_SOBRESCRITO, REG_ERROR_FLASH, REG_VACIO, REG_PARAM_INVALIDO } RegEstado;

/* Inicia el registro: recupera el estado tras un corte de energía (la última muestra válida). */
RegEstado registro_iniciar(void);

/* Agrega una muestra. Postcondición: si devuelve REG_OK o REG_LLENO_SOBRESCRITO, la muestra
 * sobrevive a un corte de energía inmediato. Con REG_LLENO_SOBRESCRITO se perdió la más antigua. */
RegEstado registro_agregar(const Muestra *m);

/* Lee la muestra número 'k' contando desde la más antigua (0 = la más antigua). 'out' es propiedad
 * del que llama y no puede ser NULL. Devuelve REG_VACIO si k es mayor que la cantidad guardada. */
RegEstado registro_leer(uint32_t k, Muestra *out);

uint32_t registro_cantidad(void);
#endif

Ese archivo contesta las preguntas que, sin contrato, se descubren depurando: ¿qué pasa si falla la flash? ¿se puede llamar desde la ISR de la radio? (No: lo dice el encabezado.) ¿quién libera out? ¿el dato sobrevive a un corte? Un detalle útil: los tipos de error forman parte de la interfaz, y las funciones que pueden fallar devuelven un estado explícito en lugar de fallar en silencio. Y lo que el módulo guarda por dentro (el formato de página de la flash, el índice) queda en el archivo .c y static: si mañana se cambia el formato, nadie más se entera.

La otra decisión es cómo se comunican los módulos. Hay cuatro estilos, con costos distintos:

Formas de comunicar módulos y cuándo conviene cada una.
EstiloCómoVentajasCostos y riesgos
Llamada directaUn módulo llama a las funciones de otroSimple, rápido, fácil de seguir en el depuradorAcoplamiento al módulo concreto; difícil de sustituir en pruebas sin un HAL falso
Callback (puntero a función registrado)El módulo de bajo nivel avisa al de alto nivel sin conocerloInvierte la dependencia (el sensor avisa a la aplicación)Hay que definir en qué contexto se ejecuta (¿ISR?) y qué puede hacer
Cola de eventosLos módulos depositan eventos y la aplicación los procesa uno a unoDesacopla en el tiempo; las ISR solo encolan; un único hilo de lógica sin carreras (tema 46)Memoria de la cola; hay que dimensionarla y decidir qué se pierde si se llena
Datos compartidosVariables globales con acceso protegidoEficiente para datos que cambian y se leen seguidoLas carreras del tema 46; debe haber un propietario por dato
/* Cola de eventos de la aplicación: las ISR y los módulos de bajo nivel solo encolan; app_estado procesa en el bucle principal */
typedef enum { EV_MEDIDA_LISTA, EV_BOTON, EV_RADIO_ENVIADO, EV_RADIO_FALLO, EV_COMANDO, EV_ALARMA_SENSOR } TipoEvento;
typedef struct { TipoEvento tipo; int32_t dato; } Evento;

#define COLA_N 16u                                        /* potencia de dos */
static Evento cola[COLA_N];  static volatile uint8_t cab, col;  static volatile uint8_t eventos_perdidos;

bool evento_publicar(TipoEvento t, int32_t d) {          /* segura desde una ISR: sección crítica muy corta */
    uint32_t pm = __get_PRIMASK();  __disable_irq();
    bool ok = (uint8_t)(cab - col) < COLA_N;
    if (ok) { cola[cab++ & (COLA_N - 1)] = (Evento){ t, d }; } else { eventos_perdidos++; }
    __set_PRIMASK(pm);
    return ok;
}

void app_bucle(void) {                                    /* un solo contexto que ejecuta la lógica: no hay carreras en los datos de la aplicación */
    for (;;) {
        Evento e;
        if (evento_tomar(&e)) app_procesar(&e);       /* máquina de estados (tema 24) */
        else                  energia_dormir_hasta_proxima_interrupcion();   /* sin eventos: a dormir (tema 35) */
    }
}

El estilo de la cola de eventos con una sola aplicación que procesa de a un evento tiene una ventaja que se paga sola: la lógica del producto corre en un solo contexto, así que casi no tiene condiciones de carrera (tema 46); las interrupciones, que son el único lugar donde hay concurrencia real, hacen lo mínimo (leer el dato, encolar un evento) y terminan. Es la base de la arquitectura recomendada para el registrador.

06 · Cómo se reparte el tiempo

El modelo de ejecución

Otra decisión de fondo: cómo se organiza el tiempo dentro del firmware. Hay tres modelos habituales y no hay uno «mejor»: cada uno responde a requisitos distintos.

Modelos de ejecución de un firmware.
ModeloCómo funcionaCuándo convieneLímites
Superbucle con interrupcionesUn bucle infinito en main que llama a cada módulo por turno; las ISR atienden lo urgente y levantan banderasSistemas simples, con pocas tareas y plazos holgados; mínima memoriaUna tarea larga retrasa a todas las demás (latencia = suma de las más largas); difícil de escalar
Dirigido por eventos (cola de eventos, máquina de estados)El programa duerme hasta que ocurre un evento; la aplicación lo procesa y vuelve a dormirEquipos a batería, con mucho reposo (el caso del registrador)Cada manejador debe ser corto y no bloquear; las esperas largas se modelan como estados
Sistema operativo de tiempo real (RTOS)Varias tareas con prioridades; el núcleo cambia de una a otra y gestiona colas, semáforos y temporizadoresMuchas actividades concurrentes con plazos distintos, bloqueos largos (una pila de comunicaciones)Memoria (núcleo y una pila por tarea), tick de reloj, carreras y prioridades por razonar (tema 22)

Un RTOS no es gratis. La Parte 4 del programa cuantifica lo que cuesta, con los valores supuestos de un núcleo pequeño: 6 KB de flash y 3648 B de RAM (1200 B del núcleo y cuatro tareas de 512 B de pila más 100 B de bloque de control), es decir 2112 B de RAM más que el superbucle con su única pila. Más serio, para un equipo a batería, es el tick: la interrupción periódica del núcleo que lo despierta del sueño para decidir si hay algo que hacer. Con un supuesto de 8 µs a 4 mA por cada despertar:

Costo de energía del tick del sistema operativo (resultado de la Parte 4).
ConfiguraciónCorriente mediaFrente al margen de 37,50 µA
Tick de 1 kHz32,00 µAEl 85 % del margen: peligroso
Tick de 100 Hz3,20 µAEl 8,5 % del margen
Sin tick (despierta solo por eventos reales, 10,5 por segundo)0,34 µAMenos del 1 %

Un tick de 1 kHz consume más corriente que todo el resto del sistema junto (el circuito entero consumía 15,78 µA). Por eso los sistemas operativos de bajo consumo ofrecen un modo sin tick (tickless), que programa el temporizador para despertar solo en el próximo evento real. Y por eso, para el registrador, la arquitectura recomendada es la dirigida por eventos: pocas actividades, mucho reposo, memoria justa, y todo el consumo de despertar se debe a eventos verdaderos. Si más adelante apareciera una pila de comunicaciones que exigiera un RTOS, se elegiría uno con modo sin tick, y esta decisión de arquitectura se revisaría con números nuevos.

07 · ¿Llegamos a tiempo?

Tiempo real: tareas y plazos

Cuando hay varias actividades con plazos, hay que comprobar que todas los cumplen, con el peor caso de cada una, antes de escribir código (el tema 22 dio las bases de las tareas y las prioridades). El modelo mínimo: cada tarea tiene un tiempo de cómputo en el peor caso C, un período T y un plazo D. Con prioridades fijas, el tiempo de respuesta de la tarea i se calcula iterando Ri = Ci + Bi + Σ ⌈Ri / Tj⌉ · Cj sobre las tareas j de mayor prioridad, donde Bi es el bloqueo: el tiempo que puede retrasarla una tarea de menor prioridad que tiene ocupado un recurso o que no se puede interrumpir. La tarea cumple si Ri ≤ Di.

Las prioridades se asignan por el plazo más corto (deadline monotonic; si el plazo es igual al período, esto equivale a la asignación por período más corto, rate monotonic). Una trampa frecuente es dar la prioridad más baja a la tarea de la radio por tener el período más largo (una transmisión cada 15 minutos), cuando su plazo, cuando transmite una alarma, es corto: se asigna por el plazo, no por el período. La Parte 3 analiza cuatro tareas del registrador:

Tareas del registrador y su respuesta en el peor caso (borrado de flash no interrumpible de 40 ms).
TareaCTDBloqueoRespuesta
comandos (consola)2 ms100 ms50 ms40 ms42 ms: cumple
medición y alarma25 ms2000 ms1000 ms40 ms67 ms: cumple
radio (transmitir)400 ms900 000 ms2900 ms40 ms475 ms: cumple
guardar (flash)30 ms60 000 ms60 000 ms0 ms465 ms: cumple

La utilización total es de solo el 3,34 %, muy por debajo de la cota de Liu y Layland para cuatro tareas (75,7 %, válida cuando los plazos igualan los períodos): hay mucho margen de cómputo. Pero la utilización no es el problema; el problema es el bloqueo. El borrado de una página de flash no se puede interrumpir y en muchos microcontroladores detiene también la captura de instrucciones desde esa flash (conviene cotejarlo en el manual del chip). Con un borrado de 40 ms, la tarea de comandos tarda 2 + 40 = 42 ms y llega a su plazo de 50 ms. Con un borrado de 60 ms, la respuesta es de 62 ms y no cumple. No hubo que escribir una línea de código para descubrir que la consola puede dejar de responder durante un borrado largo, y que o bien el plazo se relaja a 100 ms, o bien el borrado se reparte en pasos cortos, o bien se usa una flash externa para los datos, que no bloquea al núcleo.

Algunas reglas que salen de esta forma de analizar: las interrupciones hacen lo mínimo (el bloqueo que causan es su duración, tema 44); las secciones críticas son cortas (tema 46); los recursos compartidos se protegen con exclusión mutua con herencia de prioridad si hay un RTOS, para acotar la inversión de prioridad (tema 22); no se duerme dentro de una sección crítica; y el peor caso, no el promedio: se mide con el pin de prueba y el analizador lógico (tema 44) y se reemplaza el supuesto del análisis por el valor medido.

08 · Que no se pierda el porqué

Presupuestos y decisiones registradas

La arquitectura se consolida en dos documentos vivos. El primero es el presupuesto: la tabla que reparte los recursos limitados (flash, RAM, pila, CPU, energía, costo) entre los módulos, con un margen explícito. Cada módulo responde por lo que gasta, se mide en cada versión (la integración continua del tema 47 vigila los límites) y cuando un módulo se pasa, se renegocia el reparto, no se ignora. Márgenes razonables al empezar: 30 % de flash y de RAM, 25 % de pila sobre el peor caso medido, CPU por debajo del 50 % de utilización y 50 % de energía (como en el tema 48); se reducen a medida que el producto madura y las medidas reemplazan a los supuestos.

El segundo es el registro de decisiones de arquitectura (ADR, del inglés architecture decision record): una nota corta por cada decisión importante, que dice qué se decidió, por qué y qué se descartó. Su valor aparece un año después, cuando alguien pregunta «¿por qué no usamos un RTOS?» y la respuesta está escrita, con los números, y se puede revisar si el motivo sigue valiendo.

ADR-004: La imagen de respaldo del firmware se guarda en la flash externa
Estado: aceptada (2026-03-12)

Contexto: la actualización segura (R-17) exige una imagen de respaldo (tema 42). Con dos imágenes en el chip
  hacen falta 205,8 KB de flash (73 KB de aplicación x 1,3 x 2 + 16 KB de arranque); con una sola, 110,9 KB.
Decisión: la imagen de respaldo se guarda en la flash externa (que ya existe para los datos). En el chip
  queda una sola aplicación y el cargador de arranque, que copia la imagen de respaldo si la aplicación falla.
Alternativas descartadas:
  - Dos imágenes en el chip (A/B): exige un microcontrolador de 512 KB (3,10 USD, 1,5 uA de sueño).
  - Sin imagen de respaldo: no cumple R-17.
Consecuencias: permite un microcontrolador de 128 KB (1,40 USD, 0,8 uA); el arranque es más lento ante una falla
  (copia de la imagen, unos 3 s); la flash externa pasa a ser un componente crítico (su falla impide recuperar).
Cuándo revisarla: si la aplicación supera 100 KB, o si falla la verificación del respaldo en las pruebas de corte de energía.

Dos hábitos mantienen vivo todo esto: se revisan las decisiones cuando cambian sus supuestos (una medida de consumo que contradice el presupuesto, un nuevo requisito) y nunca se borra una decisión: se marca como reemplazada y se enlaza con la nueva. Y una advertencia final sobre el alcance: no hay que decidir todo al principio. Las decisiones caras (microcontrolador, modelo de ejecución, división en módulos, partición de hardware) se toman temprano; las baratas (el nombre de una función, el tamaño de un buffer) se toman cuando hacen falta. Una arquitectura que quiere resolverlo todo antes de escribir código se parece a la que no resuelve nada: las dos se equivocan, solo que una tarda más en enterarse.

09 · Comprobar que funciona

Verificación y diagnóstico

Comprobaciones sobre la arquitectura.
Qué verificarCómoResultado esperado
Cada requisito no funcional tiene una decisión que lo cumpleRevisar la tabla requisito–decisión y los presupuestos con númerosNingún requisito de energía, tiempo o memoria queda sin cuenta
La elección del microcontrolador es robustaVariar los pesos y las condiciones; mirar si cambia el ganadorEl ganador resiste, o se documenta el criterio de desempate
Las dependencias van en un solo sentidoGenerar el grafo de inclusión (#include) y buscar subidas y ciclos (Parte 2); incluirlo en la integración continuaCero violaciones y cero ciclos
Cada módulo tiene un contratoRevisar los encabezados: precondiciones, errores, propiedad de la memoria, contexto (¿ISR?)Nada que el que llama tenga que adivinar
Los plazos se cumplen en el peor casoAnálisis de tiempo de respuesta con los tiempos medidos (pin y analizador)R ≤ D en todas las tareas, con bloqueo incluido
Los presupuestos se respetanAnalizar el .map en cada compilación (tema 47)Cada módulo dentro de su parte, margen vigente
Las decisiones están registradasUn ADR por cada decisión cara, con alternativas y motivoSe puede responder «¿por qué?» sin preguntar a la persona que decidió
Síntomas frecuentes y qué revisar.
SíntomaCausa probableQué hacer
Cambiar un módulo rompe otros tresAcoplamiento alto, interfaz que expone detalles internosOcultar la información; llevar las dependencias a interfaces
No se puede probar un módulo sin el restoDependencias circulares o hacia arribaInvertir la dependencia (callbacks, datos por parámetro)
Se eligió un microcontrolador y falta memoria a mitad del proyectoSin presupuesto por módulo ni margenPresupuestar antes; elegir una familia con variantes compatibles
La consola se traba durante un borrado de flashUn bloqueo no interrumpible excede el plazoAnálisis de plazos con bloqueo; borrado en pasos, o flash externa
El consumo en reposo es mucho mayor que lo calculadoUn tick fijo del sistema operativo o interrupciones periódicas innecesariasModo sin tick, dirigido por eventos (tema 35)
Nadie recuerda por qué se hizo asíDecisiones sin registrarADR para cada decisión cara; revisarlo al cambiar un supuesto
El diseño se rehace en cada versiónLa arquitectura nunca se escribió o se rehízo por modaDocumentar vistas y decisiones; cambiar con motivos medidos

10 · Analogía con programación

Probar el modelo en Python

El programa reúne cuatro piezas del tema: el presupuesto de memoria por módulo y la elección del microcontrolador entre cinco candidatos con condiciones obligatorias y cuatro perfiles de pesos, con y sin la imagen de respaldo en la flash externa; el análisis de dependencias de 13 módulos (violaciones de capas, ciclos y la medida de inestabilidad); el análisis del tiempo de respuesta de cuatro tareas con bloqueo; y el costo de memoria y de energía del superbucle frente a un sistema operativo con distintos ticks.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

# Diseño de la arquitectura: presupuesto por módulo y elección del microcontrolador, análisis de dependencias, planificabilidad de las tareas y costo del modelo de ejecución.
import math

# ---- Parte 1: presupuesto de memoria por módulo y elección del microcontrolador ----------------------------------------------------------------
MODULOS = {"HAL y controladores": (14, 1200), "sensor": (3, 300), "almacenamiento (registro circular y CRC)": (8, 1024),
           "radio y protocolo": (38, 2800), "energía y estado": (4, 400), "comandos y configuración": (6, 600)}   # (flash KB, RAM B): SUPUESTOS de diseño
PILA_B, MARGEN, ARRANQUE_KB = 1536, 1.3, 16
app_kb = sum(f for f, _ in MODULOS.values())
ram_b = sum(r for _, r in MODULOS.values()) + PILA_B
print("Parte 1: presupuesto de memoria de los módulos del registrador y elección del microcontrolador")
for k, (f, r) in MODULOS.items(): print(f"  {k:<42} {f:>3} KB de flash, {r:>5} B de RAM")
print(f"  aplicación: {app_kb} KB de flash; RAM de módulos más pila de {PILA_B} B: {ram_b} B")
def necesidad(ext):                                               # con la imagen de respaldo en flash externa se necesita un solo espacio de aplicación en el chip
    flash = app_kb * MARGEN * (1 if ext else 2) + ARRANQUE_KB
    return flash, ram_b * MARGEN / 1024
for ext in (False, True):
    f, r = necesidad(ext)
    print(f"  {'segunda imagen en flash externa' if ext else 'dos imágenes A/B en el chip'}: hace falta {f:.1f} KB de flash y {r:.1f} KB de RAM (con margen de {MARGEN})")
# (flash KB, RAM KB, sueño µA con RTC, activo µA/MHz, costo USD, ecosistema 1-5, disponibilidad 1-5): MICROCONTROLADORES FICTICIOS, solo para el ejemplo
CAND = {"A (M0+, bajo consumo)": (128, 16, 0.8, 35, 1.40, 4, 4), "B (M4, bajo consumo)": (512, 128, 1.5, 60, 3.10, 5, 4),
        "C (M0, económico)": (64, 8, 3.0, 110, 0.80, 3, 5), "D (M33, con seguridad)": (1024, 256, 2.0, 40, 4.60, 4, 3),
        "E (M4 con radio)": (512, 96, 2.5, 70, 4.20, 4, 4)}
PERFILES = {"costo": (0.40, 0.20, 0.15, 0.15, 0.10), "consumo": (0.15, 0.45, 0.15, 0.10, 0.15),
            "equilibrado": (0.25, 0.30, 0.15, 0.15, 0.15), "riesgo": (0.15, 0.20, 0.30, 0.25, 0.10)}      # pesos: costo, consumo, ecosistema, disponibilidad, margen
def evaluar(ext, sueno_max, perfil):
    fl_n, ram_n = necesidad(ext)
    filas = {}
    min_sue = min(v[2] for v in CAND.values()); min_run = min(v[3] for v in CAND.values()); min_cos = min(v[4] for v in CAND.values())
    for n, (fl, ram, sue, run, cos, eco, disp) in CAND.items():
        motivos = [m for m, mal in (("flash", fl < fl_n), ("RAM", ram < ram_n), ("sueño", sue > sueno_max)) if mal]
        consumo = 0.7 * min_sue / sue * 10 + 0.3 * min_run / run * 10
        margen = min(10, 5 * fl / fl_n)
        pesos = PERFILES[perfil]
        puntos = pesos[0] * min_cos / cos * 10 + pesos[1] * consumo + pesos[2] * eco * 2 + pesos[3] * disp * 2 + pesos[4] * margen
        filas[n] = (motivos, puntos)
    return filas
print("  condiciones obligatorias (flash, RAM y sueño de 2,0 µA como máximo), después puntaje de 0 a 10 según el perfil de pesos")
for ext in (False, True):
    for perfil in PERFILES:
        filas = evaluar(ext, 2.0, perfil)
        validos = {n: p for n, (m, p) in filas.items() if not m}
        ganador = max(validos, key=validos.get) if validos else "ninguno"
        print(f"    {'externa ' if ext else 'A/B     '} perfil {perfil:<12} -> " + ", ".join(f"{n[0]}={'descartado' if filas[n][0] else f'{filas[n][1]:.2f}'}" for n in CAND) + f"; elegido: {ganador[0] if validos else ganador}")
print("  motivos de descarte con A/B en el chip:", "; ".join(f"{n[0]}: {', '.join(m)}" for n, (m, _) in evaluar(False, 2.0, 'equilibrado').items() if m))

# ---- Parte 2: análisis de dependencias entre módulos -------------------------------------------------------------------------------------------
CAPA = {"hal_gpio": 0, "hal_i2c": 0, "hal_spi": 0, "hal_uart": 0, "hal_timer": 0, "drv_sensor": 1, "drv_flash": 1, "drv_radio": 1,
        "registro": 2, "protocolo": 2, "energia": 2, "comandos": 2, "app_estado": 3}
DEPS = {"hal_gpio": [], "hal_i2c": [], "hal_spi": [], "hal_uart": [], "hal_timer": [], "drv_sensor": ["hal_i2c", "registro"], "drv_flash": ["hal_spi"],
        "drv_radio": ["hal_spi", "hal_gpio"], "registro": ["drv_flash", "app_estado"], "protocolo": ["drv_radio"], "energia": ["hal_timer", "hal_gpio"],
        "comandos": ["hal_uart"], "app_estado": ["drv_sensor", "registro", "protocolo", "energia", "comandos"]}
print()
print("Parte 2: dependencias entre los 13 módulos (capas: 0 HAL, 1 controladores, 2 servicios, 3 aplicación)")
saltos = [(a, b) for a, bs in DEPS.items() for b in bs if CAPA[b] >= CAPA[a]]
print("  dependencias hacia arriba o dentro de la misma capa (violan la regla):", ", ".join(f"{a}->{b}" for a, b in saltos))
def ciclos():
    hallados, pila, visto = [], [], set()
    def dfs(n):
        if n in pila:
            hallados.append(pila[pila.index(n):] + [n])
            return
        if n in visto: return
        visto.add(n); pila.append(n)
        for m in DEPS[n]: dfs(m)
        pila.pop()
    for n in DEPS: dfs(n)
    return hallados
cic = ciclos()
print("  ciclos:", " ; ".join(" -> ".join(c) for c in cic) if cic else "ninguno")
entrada = {n: sum(n in bs for bs in DEPS.values()) for n in DEPS}
print("  los más usados (dependen de ellos):", ", ".join(f"{n} ({entrada[n]})" for n in sorted(entrada, key=lambda n: (-entrada[n], n))[:3]))
inestab = {n: (len(DEPS[n]) / (len(DEPS[n]) + entrada[n]) if (len(DEPS[n]) + entrada[n]) else 0) for n in DEPS}
print(f"  inestabilidad I = salientes / (salientes + entrantes): app_estado {inestab['app_estado']:.2f}, registro {inestab['registro']:.2f}, hal_spi {inestab['hal_spi']:.2f}")
DEPS_ARREGLADO = {k: [d for d in v if (k, d) not in saltos] for k, v in DEPS.items()}
DEPS_ARREGLADO["app_estado"] = DEPS["app_estado"]
DEPS_ARREGLADO["drv_sensor"] = ["hal_i2c"]; DEPS_ARREGLADO["registro"] = ["drv_flash"]
print(f"  tras invertir las dos dependencias (el sensor entrega su dato a la aplicación y el registro recibe los datos por parámetro): violaciones {sum(1 for a, bs in DEPS_ARREGLADO.items() for b in bs if CAPA[b] >= CAPA[a])}")

# ---- Parte 3: planificabilidad de las tareas (análisis del tiempo de respuesta con prioridades fijas) -------------------------------------------------
TAREAS = [("comandos", 2, 100, 50), ("medición y alarma", 25, 2000, 1000), ("radio", 400, 900_000, 2900), ("guardar", 30, 60_000, 60_000)]   # (nombre, C ms, T ms, D ms)
u = sum(c / t for _, c, t, _ in TAREAS)
print()
print("Parte 3: cuatro tareas periódicas, prioridades por plazo más corto (deadline monotonic)")
print(f"  utilización U = {u * 100:.2f} %; cota de Liu y Layland para n = {len(TAREAS)}: {len(TAREAS) * (2 ** (1 / len(TAREAS)) - 1) * 100:.1f} % (para plazos iguales al período)")
def respuesta(i, bloqueo):
    c, d = TAREAS[i][1], TAREAS[i][3]
    r = c + bloqueo
    while True:
        nuevo = c + bloqueo + sum(math.ceil(r / TAREAS[j][2]) * TAREAS[j][1] for j in range(i))
        if nuevo == r or nuevo > d: return nuevo
        r = nuevo
for borrado_ms in (40, 60):
    print(f"  con un borrado de flash no interrumpible de {borrado_ms} ms en la tarea 'guardar':")
    for i, (n, c, t, d) in enumerate(TAREAS):
        b = borrado_ms if n != "guardar" else 0
        r = respuesta(i, b)
        print(f"    {n:<18} C={c:>3} ms T={t:>7} ms D={d:>6} ms bloqueo={b:>2} ms -> respuesta {r:>4} ms {'cumple' if r <= d else 'NO CUMPLE'}")

# ---- Parte 4: el costo del modelo de ejecución: superbucle, sistema operativo con tick fijo o sin tick ------------------------------------------------
RTOS_FLASH_KB, RTOS_RAM_B, TAREAS_N, PILA_TAREA, TCB = 6, 1200, 4, 512, 100                  # SUPUESTOS de un sistema operativo de tiempo real pequeño
ram_so = RTOS_RAM_B + TAREAS_N * (PILA_TAREA + TCB)
print()
print("Parte 4: superbucle con interrupciones frente a un sistema operativo de tiempo real")
print(f"  memoria: superbucle 0 KB de kernel y una pila de {PILA_B} B; con sistema operativo {RTOS_FLASH_KB} KB de flash y {ram_so} B de RAM (kernel {RTOS_RAM_B} B + {TAREAS_N} tareas de {PILA_TAREA} B de pila y {TCB} B de bloque); RAM adicional {ram_so - PILA_B} B")
I_MA, T_DESPERTAR = 4, 8e-6                                      # SUPUESTO: cada despertar (salir del sueño, atender la interrupción, volver) cuesta 8 µs a 4 mA
for nombre, hz in (("tick de 1 kHz", 1000), ("tick de 100 Hz", 100), ("sin tick (solo eventos reales, 10,5 despertares por segundo)", 10.5)):
    print(f"    {nombre:<62} {I_MA * T_DESPERTAR * hz * 1000:6.2f} µA")
print("  margen disponible en el presupuesto de corriente: 53,28 - 15,78 = 37,50 µA")

Resultado, parte 1: la aplicación suma 73 KB de flash y 7860 B de RAM con la pila; con dos imágenes A/B en el chip hacen falta 205,8 KB de flash y 10,0 KB de RAM, y con la segunda imagen en flash externa, 110,9 KB. Con A/B en el chip quedan descartados A (flash), C (flash, RAM y sueño) y E (sueño) y gana B con los cuatro perfiles (5,83 costo, 6,65 consumo, 6,49 equilibrado y 7,48 riesgo; D queda con 4,88, 6,00, 5,66 y 6,25); con la imagen externa pasa también A y gana con los cuatro perfiles (7,26; 8,22; 7,69; 7,83). Parte 2: dos violaciones (drv_sensor→registro y registro→app_estado), dos ciclos, los módulos más usados son hal_gpio, hal_spi y registro (2 cada uno), inestabilidad de app_estado 0,83, registro 0,50 y hal_spi 0,00; tras invertir las dos dependencias, 0 violaciones. Parte 3: utilización de 3,34 % (cota de 75,7 %); con un bloqueo de 40 ms las respuestas son 42, 67, 475 y 465 ms y todas cumplen; con 60 ms, comandos responde en 62 ms y no cumple su plazo de 50 ms. Parte 4: el sistema operativo agrega 6 KB de flash y 2112 B de RAM; el tick de 1 kHz consume 32,00 µA, el de 100 Hz, 3,20 µA y sin tick, 0,34 µA, frente a un margen de 37,50 µA.

Probá cambiar en la Parte 1 el margen (MARGEN) o el límite de sueño y ver cómo se reordenan los candidatos, subir el módulo de radio a 60 KB y ver quién queda; agregar en la Parte 2 una dependencia nueva y ver si genera una violación; cambiar en la Parte 3 el borrado de flash a 45 ms para encontrar el valor exacto en que la consola deja de cumplir; o pasar el tick de la Parte 4 a 250 Hz. La lección del programa es que las decisiones de arquitectura se pueden evaluar con cuentas antes de escribir código, y que esas cuentas se pueden repetir cada vez que cambia un supuesto.

11 · Exploración interactiva

Elegir el microcontrolador

El laboratorio implementa la Parte 1: cinco microcontroladores ficticios, las condiciones obligatorias (flash, RAM y corriente de sueño) y un puntaje ponderado. Elegís el perfil de pesos, el límite de corriente de sueño y, sobre todo, una decisión de arquitectura: si la imagen de respaldo del firmware va en la flash externa o dos imágenes A/B en el chip. La tabla muestra quién queda descartado y por qué, y quién gana.

¿Qué microcontrolador cumple y cuál conviene?

Memoria necesaria en el chip—
Elegido—

Candidatos

Elegí las condiciones.

Recorrido guiado

  1. Con las condiciones iniciales (A/B en el chip, sueño de 2,0 µA, perfil equilibrado) hacen falta 205,8 KB de flash y 10,0 KB de RAM. Quedan descartados A (flash: 128 < 205,8 KB), C (flash, RAM y sueño) y E (sueño: 2,5 > 2,0 µA); B saca 6,49 y D, 5,66. Gana B.
  2. Recorré los cuatro perfiles de pesos: B gana siempre (costo 5,83, consumo 6,65, riesgo 7,48). Es una elección robusta: no depende de cómo se pondere.
  3. Marcá «Imagen de respaldo en la flash externa»: la necesidad de flash baja a 110,9 KB y A pasa las condiciones, con 7,69 puntos en el perfil equilibrado contra 6,49 de B. Gana A con los cuatro perfiles. Una decisión de arquitectura cambió el microcontrolador y se ahorran 1,70 USD por unidad.
  4. Con la imagen externa y la corriente de sueño en 1,0 µA, solo A (0,8 µA) cumple. Sin la imagen externa y con el mismo límite, la respuesta es «ninguno cumple»: B tiene 1,5 µA y A no tiene flash. Hay que relajar un requisito o cambiar la arquitectura, y el laboratorio muestra qué decisión lo desbloquea.
  5. Subí la corriente de sueño a 3,0 µA sin la imagen externa: E pasa a cumplir con 5,50 puntos, pero B sigue ganando con 6,49 y D con 5,66. Relajar un requisito no siempre cambia la elección, y a veces sí: conviene saberlo antes de negociarlo.
  6. Probá con el perfil «consumo» y la imagen externa: A saca 8,22 contra 6,65 de B, la diferencia más grande. El perfil que se elige es una decisión del proyecto y debe estar escrito, no ser una preferencia oculta de quien hace la tabla.
Desafío: ¿cuándo es mala idea elegir por puntaje ponderado?

Cuando los criterios no son intercambiables. El puntaje promedia, y un promedio permite que un candidato con una falla grave en un criterio la compense con puntos en otros. Por eso las condiciones obligatorias se aplican antes y fuera del puntaje (un chip sin memoria suficiente no es una opción por barato que sea). También falla cuando los pesos se eligen al revés, para justificar una preferencia previa («el que usamos siempre»): el remedio es fijar los pesos antes de mirar los candidatos y registrar la tabla en el ADR. Y es engañoso cuando las diferencias de puntaje son menores que la incertidumbre de los datos: entre 6,49 y 6,35, con consumos «típicos» de una hoja de datos, no hay un ganador, hay un empate. En esos casos se decide por criterios de riesgo (la experiencia del equipo, la disponibilidad) o se mide en una placa de evaluación.

12 · Método de trabajo

Método: diseñar con orden

  1. Partí de los requisitos y sus números (tema 48): lista las decisiones de arquitectura que cada requisito exige, con su presupuesto.
  2. Dibujá el diagrama de bloques de hardware y decidí la partición hardware y software con quien diseña la placa, antes de fabricarla. Dejá margen barato: pines, huellas, conector de depuración y puntos de prueba.
  3. Presupuestá la memoria por módulo, con margen, y derivá de allí lo que debe cumplir el microcontrolador.
  4. Elegí el microcontrolador en dos pasos: primero, condiciones obligatorias; después, puntaje ponderado con pesos fijados de antemano. Variá los pesos para comprobar que la elección resiste, y medí el consumo real en una placa de evaluación.
  5. Evaluá las decisiones como un conjunto: una decisión de arquitectura puede cambiar la elección del chip (la imagen de respaldo externa). Compará alternativas completas, no piezas sueltas.
  6. Dividí el firmware en capas y módulos con una responsabilidad clara, dependencias solo hacia abajo y sin ciclos. Comprobalo con el grafo de inclusión y que la integración continua lo vigile.
  7. Escribí el contrato de cada módulo: precondiciones, postcondiciones, errores, propiedad de la memoria y contexto (¿ISR?). Ocultá lo interno.
  8. Elegí el modelo de ejecución con números: superbucle, dirigido por eventos o RTOS, según los requisitos, la memoria y el costo de energía del tick. En un equipo a batería, preferí lo dirigido por eventos, o un RTOS sin tick.
  9. Comprobá los plazos en el peor caso, con bloqueos, antes de escribir código: tiempo de respuesta de cada tarea, prioridades por plazo, ISR cortas, secciones críticas cortas. Reemplazá los supuestos por mediciones apenas puedas.
  10. Registrá las decisiones (ADR) con alternativas y motivo, mantené el presupuesto vivo y revisá todo cuando cambie un supuesto. Decidí temprano lo caro, y lo barato cuando haga falta.

13 · Comprensión

Confusiones frecuentes

«Se elige el microcontrolador primero y después se ve qué se puede hacer»

Es el camino más común y el más riesgoso. El microcontrolador debe salir de los requisitos y el presupuesto. Elegirlo antes lleva a descubrir a mitad del proyecto que falta memoria, que el consumo no cierra o que falta un periférico, cuando cambiarlo ya es carísimo.

«El chip más potente es la opción segura»

Más potencia suele significar más consumo, más costo y más complejidad, y en un equipo a batería puede ser lo que impide cumplir el requisito de autonomía. Se busca el chip suficiente, con margen, y de una familia que permita crecer.

«Un RTOS siempre es mejor que un superbucle»

Cuesta memoria (en el ejemplo, 6 KB de flash y 2112 B de RAM más) y, con un tick fijo de 1 kHz, 32 µA de corriente, más que todo el resto del circuito. Un RTOS conviene cuando hay muchas actividades concurrentes con plazos distintos; para un sensor que duerme casi todo el tiempo, lo dirigido por eventos suele ser mejor.

«Con una utilización baja de la CPU, los plazos se cumplen seguro»

La utilización era del 3,34 % y la consola igual perdía su plazo cuando un borrado de flash de 60 ms la bloqueaba. Lo que decide los plazos es el peor caso de cada cadena, incluidos los bloqueos y las secciones no interrumpibles, no la carga promedio.

«Las capas son burocracia que ralentiza el desarrollo»

Cuestan una indirección y ahorran mucho más: permiten cambiar el microcontrolador o un sensor tocando una sola capa y probar la lógica en la PC (tema 47). Los ciclos y las dependencias hacia arriba, en cambio, hacen que ningún módulo se pueda entender o probar por separado.

«La interfaz de un módulo son solo los nombres de las funciones»

También son sus precondiciones, errores, propiedad de la memoria y contexto de llamada (¿se puede usar en una ISR?). Lo que no está escrito en el contrato, cada usuario lo interpreta a su manera, y esas diferencias son una fuente de errores que aparecen lejos del módulo.

«Documentar las decisiones es perder tiempo»

Una nota de diez líneas ahorra horas de discusión cuando alguien, un año después, quiere revertir una decisión sin conocer el motivo. Cuesta poco hacerla en el momento, y casi nada recuperarla después si no se hizo.

«La arquitectura se define toda al principio y no cambia»

Las decisiones caras se toman temprano, pero se revisan cuando cambian sus supuestos (una medición que contradice el presupuesto, un requisito nuevo). Lo que debe evitarse es decidir sin números y cambiar sin registrar: el cambio con motivos es mantenimiento; el cambio sin ellos, deriva.

14 · Práctica guiada

Ejercicios

1. Memoria necesaria

Una aplicación suma módulos de 20 KB, 35 KB y 12 KB de flash. Se usa un margen del 30 %, un cargador de arranque de 16 KB y actualización A/B. ¿Cuánta flash hace falta en el chip? ¿Y si la imagen de respaldo se guarda en una flash externa? ¿Qué candidatos de la tabla del tema cumplen en cada caso?

Ver solución

Aplicación: 20 + 35 + 12 = 67 KB; con margen, 67 × 1,3 = 87,1 KB. Con A/B en el chip: 87,1 × 2 + 16 = 190,2 KB: cumplen B (512 KB), D (1024 KB) y E (512 KB, salvo por su sueño); A (128 KB) no. Con la imagen de respaldo externa: 87,1 + 16 = 103,1 KB: pasa también A (128 KB), a condición de que su RAM y su sueño cumplan. La decisión de la flash externa cambia el conjunto de candidatos.

2. Una elección frágil

Dos candidatos X e Y se puntúan (de 0 a 10) en costo, consumo y ecosistema: X = 8, 5, 9; Y = 6, 9, 6. Con pesos 0,4, 0,4 y 0,2, ¿quién gana? ¿Y con 0,5, 0,3 y 0,2? ¿Qué se concluye?

Ver solución

Con 0,4 / 0,4 / 0,2: X = 3,2 + 2,0 + 1,8 = 7,0; Y = 2,4 + 3,6 + 1,2 = 7,2: gana Y. Con 0,5 / 0,3 / 0,2: X = 4,0 + 1,5 + 1,8 = 7,3; Y = 3,0 + 2,7 + 1,2 = 6,9: gana X. Un cambio de 10 puntos de peso invierte el resultado: la elección es frágil y el puntaje no alcanza para decidir. Hay que decidir con otro criterio (riesgo, experiencia, un prototipo) o precisar mejor los pesos con el cliente antes de mirar los candidatos.

3. Ciclo de dependencias

Los módulos A, B y C se llaman así: A usa B, B usa C y C usa A. ¿Qué problema hay? ¿Cómo se rompe? ¿Cuál es la inestabilidad de un módulo con 3 dependencias salientes y 1 entrante?

Ver solución

Es un ciclo: ninguno de los tres se puede compilar, probar ni entender sin los otros dos. Se rompe invirtiendo una dependencia: la que va del módulo de nivel más bajo hacia el más alto (aquí, C → A) se reemplaza por un callback que A registra en C, o por datos pasados por parámetro, de modo que C ya no conozca a A. La inestabilidad es I = 3 / (3 + 1) = 0,75: es un módulo bastante inestable (depende de mucho y casi nadie depende de él), apto para estar en las capas altas.

4. Tiempo de respuesta

Tres tareas con prioridades por período: T1 (C=1, T=10, D=10), T2 (C=3, T=20, D=20) y T3 (C=5, T=50, D=50), en ms. Calculá la respuesta de cada una. Después agregá un bloqueo de 8 ms y de 10 ms a las dos primeras y decidí si cumplen.

Ver solución

R1 = 1. R2: se parte de 3; 3 + ⌈3/10⌉·1 = 4; con 4: 3 + 1 = 4: R2 = 4. R3: se parte de 5; 5 + ⌈5/10⌉·1 + ⌈5/20⌉·3 = 5 + 1 + 3 = 9; con 9: 5 + 1 + 3 = 9: R3 = 9. Todas cumplen (U = 0,10 + 0,15 + 0,10 = 35 %). Con un bloqueo de 8 ms: R1 = 1 + 8 = 9 ≤ 10 (cumple); R2: 3 + 8 = 11 → 3 + 8 + ⌈11/10⌉ = 13 → con 13: 3 + 8 + ⌈13/10⌉ = 13: R2 = 13 ≤ 20 (cumple). Con un bloqueo de 10 ms: R1 = 11 > 10: T1 no cumple. La utilización era baja y el bloqueo igual rompe el plazo de la tarea más rápida.

5. El costo del tick

Un sistema operativo usa un tick de 250 Hz y cada despertar cuesta 10 µs a 5 mA. ¿Qué corriente media agrega? ¿Qué parte representa del margen de 37,5 µA del registrador?

Ver solución

5 mA × 10 µs × 250 por segundo = 5 mA × 0,0025 = 0,0125 mA = 12,5 µA. Frente al margen: 12,5 / 37,5 = 33 %. Sigue cumpliendo, pero consume una tercera parte del margen solo en despertar sin hacer nada útil. Con un modo sin tick, esa corriente casi desaparece, y por eso es la opción para los equipos a batería con un RTOS.

6. Un registro de decisión

Redactá los elementos de un ADR para la decisión «el registrador usa una arquitectura dirigida por eventos en lugar de un RTOS».

Ver solución

Contexto: requisito de 2 años con 2 pilas AA (objetivo de 53,28 µA); el circuito consume 15,78 µA y deja 37,5 µA de margen; hay cuatro actividades con plazos holgados. Decisión: un único bucle de aplicación que procesa una cola de eventos y duerme cuando no hay ninguno; las interrupciones solo encolan. Alternativas: RTOS con tick de 1 kHz (32 µA, el 85 % del margen y 2112 B de RAM y 6 KB de flash más); RTOS sin tick (viable, pero agrega memoria y complejidad sin necesidad); superbucle con sondeo (despierta sin necesidad y retrasa la alarma). Consecuencias: sin carreras en la lógica (un solo contexto), memoria mínima, manejadores cortos y estados explícitos; las operaciones largas (borrado de flash) hay que repartirlas. Cuándo revisar: si se agrega una pila de comunicaciones que bloquee, o si los plazos se acortan por debajo de lo que garantiza la medición del peor caso.

15 · Síntesis

Ideas para recordar

  • La arquitectura es el conjunto de decisiones caras de revertir que determinan si se cumplen los requisitos no funcionales. Cada una se toma enumerando alternativas, evaluándolas con números, decidiendo y registrando el porqué (ADR). Se describe con varias vistas: hardware, módulos, ejecución, datos y memoria.
  • En la arquitectura de hardware se decide la partición hardware y software (sensor digital o analógico, flash externa, RTC, watchdog, radio, alimentación) junto con quien diseña la placa, y se deja margen barato: pines, huellas, conector de depuración y puntos de prueba.
  • El microcontrolador se elige de los requisitos: presupuesto de memoria por módulo (73 KB de flash y 7860 B de RAM en el ejemplo), condiciones obligatorias primero (205,8 KB con A/B, 110,9 KB con el respaldo externo) y puntaje ponderado después, con pesos fijados de antemano. La elección debe resistir al variar los pesos, y una decisión de arquitectura (la imagen externa) cambió el ganador de B a A, con el chip más barato y de menor consumo.
  • El firmware se divide en capas y módulos (HAL, controladores, servicios, aplicación) con alta cohesión, bajo acoplamiento y dependencias solo hacia abajo y sin ciclos; la inestabilidad (0 para el HAL, 0,83 para la aplicación) muestra que lo que cambia depende de lo estable. Las dos violaciones del ejemplo se corrigen invirtiendo dependencias con callbacks y datos por parámetro.
  • La interfaz es un contrato (precondiciones, postcondiciones, errores, propiedad de la memoria, contexto de ISR) que oculta lo interno. Los módulos se comunican por llamadas, callbacks, cola de eventos o datos compartidos con propietario; la cola de eventos con una sola aplicación evita casi todas las carreras.
  • El modelo de ejecución (superbucle, dirigido por eventos, RTOS) se elige con números: un RTOS suma 6 KB de flash y 2112 B de RAM, y un tick de 1 kHz consume 32 µA, más que todo el circuito. En un equipo a batería, dirigido por eventos o RTOS sin tick (0,34 µA).
  • Los plazos se comprueban antes de codificar con el tiempo de respuesta en el peor caso (R = C + B + interferencia), con prioridades por plazo y no por período. La utilización era del 3,34 % y un borrado de flash de 60 ms bloqueaba a la consola más allá de su plazo: manda el bloqueo, no la carga. Los presupuestos con margen (30 % de memoria, 25 % de pila, 50 % de energía) y el registro de decisiones mantienen vivo el diseño.

¿Listo para el tema siguiente?

Antes de avanzar, tomá un proyecto tuyo y respondé: ¿podés explicar por qué se eligió ese microcontrolador, con números y con las alternativas descartadas?, ¿el grafo de dependencias de tus módulos tiene ciclos o subidas?, ¿qué tarea tiene el peor bloqueo y cuánto vale?, ¿dónde está escrita cada decisión importante? Si no, el trabajo útil de esta semana es escribir tres ADR y dibujar el grafo.

En el próximo tema nos ocupamos de lo que mantiene vivo al producto durante años: calidad, documentación y mantenimiento, es decir, los estándares de código, las revisiones, el versionado, la trazabilidad y el ciclo de vida.