01 · El mapa
Qué probar y dónde
Probar es ejecutar el código con entradas elegidas y comparar el resultado con lo esperado, de forma automática y repetible. Se distinguen niveles según cuánto del sistema participa, y cada uno tiene su costo y su alcance:
| Nivel | Qué ejercita | Dónde corre | Qué no puede ver |
|---|---|---|---|
| Unitaria | Una función o un módulo aislado: cálculos, filtros, protocolos, máquinas de estados | En la PC, en milisegundos | El hardware, los tiempos reales, las interrupciones |
| Integración | Varios módulos juntos, con el hardware simulado o falso | En la PC o en un simulador del procesador | El comportamiento eléctrico y el tiempo real |
| Hardware en el lazo (HIL) | El firmware real en la placa real, con estímulos y mediciones automáticas | En un banco de pruebas | Lo que no se llegó a estimular (el campo es más variado) |
| Sistema y aceptación | El producto completo en condiciones de uso: ambientales, de larga duración, con usuarios | Laboratorio y campo | — |
La forma de pirámide expresa una economía: cuanto más abajo se encuentra un error, más barato es diagnosticarlo y corregirlo. La Parte 3 del programa lo cuantifica con horas supuestas: si cada defecto cuesta 0,5 h en una prueba unitaria, 2 h en la integración, 8 h en el banco de hardware y 80 h si llega al campo, un equipo que encuentra el 60 % de sus 100 defectos mensuales en el banco y el 40 % en el campo gasta 3680 horas; con una pirámide que detecta el 50 % en pruebas unitarias, el 20 % en la integración, el 20 % en el banco y deja pasar solo el 10 %, gasta 1025 horas, un 72 % menos. Y fijarse en esto: aun con la pirámide, los 10 defectos que llegan al campo son el 78 % del costo restante: es el campo el que sigue pesando.
02 · Antes de probar
Diseñar para poder probar
Un código se puede probar en la PC solo si no depende del hardware. Si una función de cálculo incluye el archivo de cabecera del fabricante y escribe un registro, no compila en la PC. La capa de acceso al hardware (el HAL, tema 10) y la lógica de la aplicación deben estar separadas, y la lógica debe recibir por parámetro lo que necesita del exterior. Cuatro hábitos lo logran:
- Funciones puras donde se pueda. Una función que solo calcula una salida a partir de sus entradas, sin leer registros ni variables globales, se prueba con una línea (
nivel(mv)del apartado siguiente). - El hardware detrás de una interfaz (una estructura de punteros a función, o funciones declaradas en una cabecera cuya implementación cambia: la real en el equipo, una falsa en la PC).
- El tiempo como una entrada. La lógica no llama a
ahora_ms()directamente: recibe el instante o lo pide a una interfaz, y la prueba lo controla. Así se verifica un plazo de 10 minutos en un microsegundo, y el desborde de 49,7 días (tema 46). - Las máquinas de estados como funciones.
(estado, evento) → (estado nuevo, acciones)sin efectos ocultos: se prueba cada transición con una tabla (tema 24).
/* sensor.h: la lógica de la aplicación solo ve esta interfaz, nunca los registros */
typedef struct {
bool (*leer_adc)(uint16_t *crudo); /* false si el convertidor falló */
uint32_t (*ahora_ms)(void); /* el tiempo se pide, no se asume */
void (*alarma)(bool encendida);
} SensorHal;
/* bateria.c: lógica pura de la aplicación; compila igual en el microcontrolador y en la PC */
typedef enum { BAT_INVALIDA = -1, BAT_CRITICA, BAT_BAJA, BAT_MEDIA, BAT_LLENA } NivelBat;
NivelBat bateria_nivel(uint16_t mv) {
if (mv > 4200u) return BAT_INVALIDA; /* lectura imposible: error del sensor, no de la pila */
if (mv < 3000u) return BAT_CRITICA;
if (mv < 3300u) return BAT_BAJA;
if (mv < 3600u) return BAT_MEDIA;
return BAT_LLENA;
}
void bateria_vigilar(const SensorHal *hal, uint32_t *t_ultimo, NivelBat *nivel) {
uint16_t mv;
if ((int32_t)(hal->ahora_ms() - *t_ultimo) < 1000) return; /* una vez por segundo */
*t_ultimo = hal->ahora_ms();
*nivel = hal->leer_adc(&mv) ? bateria_nivel(mv) : BAT_INVALIDA;
hal->alarma(*nivel == BAT_CRITICA);
}
En el equipo, SensorHal se llena con las funciones reales (que leen el ADC y mueven un pin); en la prueba, con dobles de prueba (test doubles): un stub que devuelve valores fijos, un fake con un comportamiento simplificado (un reloj que avanza lo que se le ordena), o un mock que además anota cómo lo llamaron para verificar que la alarma se encendió una sola vez. La separación cuesta una indirección (un puntero a función por llamada, unos ciclos) y paga todo lo demás. Cuando ese costo importa en un camino crítico, se puede resolver en tiempo de compilación, con funciones static inline o con el enlazador eligiendo qué implementación incluir.
03 · La base de la pirámide
Pruebas unitarias en la PC
Con la lógica separada, las pruebas se escriben en C, se compilan con el compilador de la PC (gcc o clang) y se ejecutan en milisegundos. Existen marcos que ahorran el trabajo repetitivo: Unity (pequeño, hecho para sistemas embebidos), CppUTest y Ceedling (que automatiza Unity con generación de ejecutores y mocks). Un marco aporta lo mismo: una forma de declarar casos, aserciones con mensajes útiles, y un informe de cuáles pasaron.
/* test_bateria.c (con Unity; los nombres exactos de las macros conviene cotejarlos con la versión que se use) */
#include "unity.h"
#include "bateria.h"
void setUp(void) {} void tearDown(void) {}
/* Valores en los dos lados de cada límite */
void test_limites_de_cada_nivel(void) {
TEST_ASSERT_EQUAL_INT(BAT_CRITICA, bateria_nivel(2999));
TEST_ASSERT_EQUAL_INT(BAT_BAJA, bateria_nivel(3000));
TEST_ASSERT_EQUAL_INT(BAT_BAJA, bateria_nivel(3299));
TEST_ASSERT_EQUAL_INT(BAT_MEDIA, bateria_nivel(3300));
TEST_ASSERT_EQUAL_INT(BAT_MEDIA, bateria_nivel(3599));
TEST_ASSERT_EQUAL_INT(BAT_LLENA, bateria_nivel(3600));
TEST_ASSERT_EQUAL_INT(BAT_LLENA, bateria_nivel(4200));
TEST_ASSERT_EQUAL_INT(BAT_INVALIDA,bateria_nivel(4201));
}
/* Con dobles de prueba: un reloj falso y un ADC falso */
static uint32_t reloj; static uint16_t adc_valor; static bool adc_ok, alarma_estado; static int alarma_cambios;
static bool fake_adc(uint16_t *c) { *c = adc_valor; return adc_ok; }
static uint32_t fake_ahora(void) { return reloj; }
static void fake_alarma(bool e) { if (e != alarma_estado) alarma_cambios++; alarma_estado = e; }
static const SensorHal HAL_FALSO = { fake_adc, fake_ahora, fake_alarma };
void test_alarma_solo_con_bateria_critica(void) {
uint32_t t = 0; NivelBat n = BAT_LLENA;
reloj = 5000; adc_ok = true; adc_valor = 2900; alarma_cambios = 0; alarma_estado = false;
bateria_vigilar(&HAL_FALSO, &t, &n);
TEST_ASSERT_EQUAL_INT(BAT_CRITICA, n); TEST_ASSERT_TRUE(alarma_estado); TEST_ASSERT_EQUAL_INT(1, alarma_cambios);
reloj += 500; adc_valor = 4000; /* menos de un segundo: no debe volver a medir */
bateria_vigilar(&HAL_FALSO, &t, &n);
TEST_ASSERT_EQUAL_INT(BAT_CRITICA, n);
}
void test_el_reloj_da_la_vuelta(void) { /* el desborde de 49,7 días, probado en un instante */
uint32_t t = 0xFFFFFF00u; NivelBat n = BAT_LLENA;
reloj = 0x00000200u; adc_ok = true; adc_valor = 3400; /* pasaron 0x300 = 768 ms: todavía no corresponde */
bateria_vigilar(&HAL_FALSO, &t, &n);
TEST_ASSERT_EQUAL_INT(BAT_LLENA, n);
}
int main(void) { UNITY_BEGIN(); RUN_TEST(test_limites_de_cada_nivel); RUN_TEST(test_alarma_solo_con_bateria_critica); RUN_TEST(test_el_reloj_da_la_vuelta); return UNITY_END(); }
La compilación para la PC incluye todas las advertencias y los sanitizers, que son verificaciones que el compilador agrega al ejecutable y que detectan desbordes de buffer, usos de memoria inválida y comportamiento indefinido (tema 46) en el momento en que ocurren. Una prueba unitaria con sanitizers revisa mucho más que su aserción:
# Compilar y ejecutar las pruebas en la PC (un objetivo de make típico)
gcc -std=c11 -Wall -Wextra -Wconversion -g -O0 -fsanitize=address,undefined \
-Isrc -Itest -Iunity src/bateria.c test/test_bateria.c unity/unity.c -o build/test_bateria
./build/test_bateria
# Cobertura de líneas y ramas con gcov (se compila con --coverage y después se corre el informe)
gcc --coverage -O0 -Isrc -Iunity src/bateria.c test/test_bateria.c unity/unity.c -o build/test_cov
./build/test_cov && gcov -b build/test_cov-bateria.gcda
Dos advertencias de realismo. Primera: el código de la PC no es idéntico al del microcontrolador: int tiene 32 bits en un Cortex-M y 32 o 64 en la PC según el sistema, long cambia, el orden de bytes y la alineación pueden diferir, y el compilador es otro. Por eso se usan tipos de ancho fijo (uint16_t, int32_t) y se corre, además, un subconjunto de pruebas en el microcontrolador o en un simulador. Segunda: las pruebas unitarias en la PC no prueban las interrupciones ni el tiempo: eso es del banco de hardware.
04 · Qué entradas probar
Elegir los casos de prueba
No se puede probar todo: para bateria_nivel, las entradas posibles de 0 a 5000 mV son 5001, y en una función con dos entradas de 16 bits son más de cuatro mil millones. La habilidad está en elegir pocos casos que representen a muchos. Las técnicas clásicas:
- Clases de equivalencia. Se agrupan las entradas que el código trata igual y se prueba un representante de cada grupo. Para
nivelhay cinco clases: crítica, baja, media, llena e inválida. - Valores límite. Los errores se concentran en los bordes: los operadores
<contra<=, los «uno de más o de menos». Se prueba a ambos lados de cada borde. Los ocho casos de la Parte 1 (2999, 3000, 3299, 3300, 3599, 3600, 4200, 4201) cubren los cuatro bordes en lugar de las 5001 entradas. - Casos de error y de borde del tipo: el valor mínimo y máximo del tipo (0 y 65 535 en un
uint16_t), los punteros nulos, los buffers vacíos o llenos, los largos 0, 1, N−1, N y N+1 (tema 46). - Tablas de transiciones para una máquina de estados: cada par (estado, evento) con su resultado esperado, incluidos los eventos «imposibles» (tema 24).
- Pruebas con entradas aleatorias y propiedades: en lugar de un caso concreto, se afirma una propiedad que debe cumplirse siempre («la salida de
nivelnunca decrece al crecer la tensión, salvo en el salto a inválida»; «decodificar(codificar(x)) == x») y se prueba con miles de entradas al azar. Encuentra lo que nadie pensó. - Pruebas de regresión. Cada vez que se encuentra un error, se escribe primero la prueba que lo reproduce (tema 46) y se corrige después; la prueba queda en la suite para siempre. Es la forma más económica de construir una suite útil: refleja los errores que realmente ocurren.
Y un criterio de estilo que ahorra mucho: una prueba pequeña y con un nombre que cuenta qué se verifica (test_alarma_solo_con_bateria_critica), con una sola razón para fallar, independiente de las demás (sin depender del orden) y determinista (sin tiempos ni azar sin semilla fija). Una prueba que falla a veces es peor que ninguna: se aprende a ignorarla.
05 · ¿Sirven las pruebas?
Cuán buenas son las pruebas
Que las pruebas pasen no dice nada si no pueden fallar. Hay dos formas de medir su calidad:
- Cobertura: qué parte del código se ejecutó durante las pruebas. Se mide por líneas, por ramas (cada
iftomado en sus dos sentidos) y, para el software crítico, por MC/DC (cada condición de una expresión demostrada independiente de las demás: por ejemploa && bexige tres casos, no dos). Es una medida necesaria, pero no suficiente: ejecutar una línea no es verificar su resultado, y una prueba sin aserciones da cobertura del 100 %. La cobertura baja delata lo que nadie probó; la cobertura alta no prueba que lo probado sea correcto. - Pruebas de mutación: se modifica el código con cambios pequeños (un
<pasa a<=, una constante cambia en uno), cada uno es un mutante, y se vuelve a correr la suite: si alguna prueba falla, el mutante «muere» (la suite lo detectó); si todo sigue pasando, sobrevive (la suite no habría visto ese error real). El porcentaje de mutantes detectados mide cuánto verifican las pruebas, no cuánto ejecutan.
La Parte 2 del programa lo muestra con nivel(mv) y 12 mutantes (para cada uno de los tres umbrales: cambiar < por <=, sumarle 1 y restarle 1; y las mismas tres variantes en el límite de 4200 mV). Tres suites:
| Suite | Entradas | Mutantes detectados | Sobreviven |
|---|---|---|---|
| Valores típicos (2800, 3100, 3450, 3900, 5000) | 5 | 0 de 12 (0 %) | Todos |
| Malla cada 100 mV, de 2500 a 4500 | 21 | 8 de 12 (66,7 %) | Los de «un milivoltio menos» en cada umbral y el de 4201 |
| Valores en los límites | 8 | 12 de 12 (100 %) | Ninguno |
La suite de los valores típicos cubre las cinco clases (y todas las líneas del programa) y no detecta ni un mutante: un umbral corrido en un milivoltio pasa sin que nadie lo note. La malla de 21 entradas, que parece exhaustiva, detecta solo dos terceras partes porque sus puntos caen justo sobre los umbrales (múltiplos de 100) pero nunca un milivoltio antes. Y las ocho entradas en los límites detectan los doce, con menos casos que la malla. La moraleja: la cantidad de pruebas no mide su calidad; lo que importa es dónde caen. El laboratorio de este tema permite jugar con esto.
Existen herramientas de mutación para C y C++ (por ejemplo, Mull y Dextool Mutate; conviene cotejar su estado actual), pero incluso sin ellas, la idea sirve como ejercicio mental: ante cada comparación del código, preguntarse «si cambio este < por <=, ¿alguna prueba falla?». Si la respuesta es no, falta un caso en ese borde.
06 · El nivel intermedio
Simulación
Entre la PC y el banco hay un escalón: ejecutar el firmware real, compilado para el microcontrolador, dentro de un simulador que imita el procesador y sus periféricos. Así se prueba el binario verdadero (con su arranque, sus interrupciones y su configuración de periféricos) sin tener hardware, y se puede lanzar en la nube en paralelo, miles de veces. Los más usados:
| Herramienta | Qué ofrece | Límite |
|---|---|---|
| QEMU | Emulación del procesador (y de algunas placas Cortex-M); código abierto; se conecta a gdb | Pocos periféricos de microcontroladores reales; la temporización es aproximada |
| Renode | Simula placas y redes de dispositivos; permite escribir los casos en un lenguaje de pruebas y conectar varios equipos | Hay que contar con un modelo del chip o escribirlo |
| Simuladores de los fabricantes | Incluidos en el IDE: ejecutan el código, miran los registros, simulan algunos periféricos | Cada uno cubre lo suyo; poco automatizables |
| Simulación de la «planta» | El modelo matemático del proceso controlado (un horno, un motor) conectado a la lógica de control | Es tan bueno como el modelo |
La simulación es excelente para verificar la lógica en contexto: que el arranque llega a main, que el controlador estabiliza un horno simulado, que una secuencia de comunicaciones produce las respuestas esperadas, que el cargador de arranque (tema 42) rechaza una imagen dañada. Y tiene un límite que hay que recordar: un simulador hace lo que su modelo dice, no lo que hace el silicio. No reproduce los tiempos reales de las interrupciones, las particularidades del ADC, el ruido, la alimentación ni los errores de la propia hoja de datos (errata). Un firmware que pasa en el simulador pasó una prueba de lógica, no una de hardware.
Una variante muy útil para los controles es la simulación de la planta: se escribe en la PC (en Python, por ejemplo) un modelo del sistema físico, y se conecta con el código real del controlador compilado para la PC. Se puede probar una regulación de temperatura con una taza que se enfría, un sensor que falla o una carga que cambia de golpe, en segundos y con resultados repetibles, antes de calentar un solo grado de verdad.
07 · El equipo real, automatizado
Hardware en el lazo y pruebas de producción
Lo que el simulador no ve se prueba en el hardware, pero automatizando el banco para que sea tan repetible como una prueba de la PC. En una prueba de hardware en el lazo (HIL, del inglés hardware in the loop), una computadora controla el equipo bajo prueba y todo lo que lo rodea: lo programa, lo alimenta, le aplica estímulos, mide sus salidas y decide si pasó.
| Componente | Para qué sirve |
|---|---|
| Computadora de control (a menudo, el propio servidor de integración continua con una placa conectada) | Ejecuta las pruebas, graba el firmware por SWD (tema 45) y recoge los resultados |
| Fuente programable y medidor de corriente | Alimentar con tensiones variables (el hundimiento y la descarga de la pila), cortar la energía a propósito (tema 40) y medir el consumo (tema 36) |
| Estímulos: entradas digitales, generadores de señal, simuladores de sensores | Aplicar a las entradas lo que en campo generaría el mundo |
| Medidores: analizador lógico, osciloscopio, ADC de referencia (tema 44) | Verificar los tiempos y las señales de salida, independientemente del firmware |
| Canal de pruebas en el firmware | Una interfaz (por UART, USB o SWD) para dar órdenes («simular un evento») y leer estados internos, solo en la configuración de pruebas |
| Relés o multiplexores | Conectar un mismo banco a varias placas, o inyectar fallas (un corto, un cable abierto) |
Un criterio práctico: una prueba va al banco de hardware solo si no se puede comprobar en un nivel inferior. Lo característico del HIL es la verificación de tiempos (la latencia de una interrupción medida con un pin y un analizador, como en el tema 44), de periféricos reales (que el ADC lee lo que debe con una tensión de referencia), de consumo en cada modo de energía, de comunicaciones con un equipo real, y de actualización completa por radio o cable (tema 42). Como el banco es escaso, se protege: las confirmaciones llegan a él solo si pasaron las etapas baratas (apartado de integración continua).
Una variante industrial es el banco de pruebas de producción (fixture, o «cama de clavos»): un dispositivo que contacta puntos de prueba de la placa terminada, la programa y verifica en segundos que cada fuente de alimentación, cada sensor y cada interfaz funcionan. No busca errores de diseño sino defectos de fabricación (una soldadura, un componente mal puesto) en cada unidad. El firmware ayuda con un modo de autoprueba de fábrica y con la prevista cantidad de puntos de prueba en la placa (tema 44), que se decide en el diseño del hardware, no después.
08 · Lo que el uso normal no hace
Pruebas de robustez
Las pruebas de «camino feliz» verifican que el sistema hace lo que debe cuando todo va bien. Los fallos de campo ocurren cuando algo sale mal. Por eso un plan de pruebas de firmware incluye una categoría de pruebas de robustez, que provocan a propósito lo que en el campo ocurre solo:
- Inyección de fallos. Se simula el error y se verifica la reacción: un sensor que no responde o devuelve un valor imposible, un bus I²C que se traba, una escritura en flash que falla, una trama con CRC incorrecto. En la PC se logra con dobles de prueba que fallan a pedido; en el banco, con relés y generadores.
- Corte de energía en cada punto. Para un sistema que guarda datos o se actualiza (temas 40 a 42), se corta la alimentación en cada instante posible de la operación y se verifica que al reiniciar el sistema recupera un estado válido, sea el anterior o el nuevo, nunca uno a medias. En un modelo de flash simulada se puede cortar tras cada escritura y borrado, probando exhaustivamente todos los puntos (ejercicio 5).
- Entradas malformadas y fuzzing. Un generador envía al analizador de protocolos tramas aleatorias, truncadas, de largo absurdo o con campos fuera de rango, y vigila que nunca se cuelgue ni corrompa la memoria (tema 46). Con sanitizers en la PC, encuentra errores que ninguna prueba escrita a mano habría pensado.
- Estrés. Todas las interrupciones a su tasa máxima, las comunicaciones saturadas, la memoria casi llena, la tensión de alimentación en los extremos del rango, para hacer visibles las carreras y los desbordes de pila de baja probabilidad (tema 46).
- Larga duración (soak tests): el equipo funcionando días o semanas con una carga realista, buscando lo que solo aparece con el tiempo: fugas de memoria (tema 31), contadores que dan la vuelta (49,7 días), deriva del reloj, desgaste de la flash (tema 40) y reinicios por watchdog (tema 38).
- Ambientales: temperatura, humedad, vibración, interferencia electromagnética, según el producto: son pruebas de hardware, pero el firmware debe estar presente y registrando para ver cómo reacciona.
- Pruebas de actualización: instalar la versión nueva sobre la vieja en todos los caminos soportados, con cortes y con imágenes dañadas (tema 42), y la vuelta atrás.
Una disciplina que ayuda: en el firmware se incluye un modo de prueba (activado solo en la configuración de pruebas, nunca en el producto, tema 43) que permite forzar eventos (un reinicio, un fallo del sensor, el desborde de un contador) sin esperar a que ocurran. Es lo que vuelve alcanzables, en una tarde, las condiciones que en el campo tardan meses en producirse.
09 · Todo, en cada cambio
Integración continua
Las pruebas valen solo si se ejecutan siempre. La integración continua (CI) es la práctica de que cada cambio de código, al subirse al repositorio, dispare automáticamente una cadena de etapas en un servidor, y que el cambio no se acepte si alguna falla. Las pruebas dejan de depender de la disciplina individual: se corren todas, en cada cambio, sin pedirle a nadie que se acuerde.
| Etapa | Qué hace | Falla si… |
|---|---|---|
| Compilar | Construye el firmware con el compilador cruzado, en una configuración limpia (-Werror) | Hay un error o una advertencia nueva |
| Análisis estático | cppcheck, clang-tidy, reglas MISRA; formato del código | Aparece un patrón peligroso |
| Pruebas unitarias | Se compilan y corren en la PC con sanitizers; se mide la cobertura | Una prueba falla, o la cobertura cae bajo un umbral |
| Presupuestos de recursos | Analiza el .map: tamaño de flash, de RAM y de pila (temas 28 y 46) | Se supera el presupuesto o crece más de lo acordado |
| Integración en la PC o en el simulador | Ejecuta el firmware real en QEMU o Renode con escenarios | Un escenario no da el resultado esperado |
| Hardware en el lazo | Graba una placa real y corre las pruebas del banco | Una verificación de tiempos, periféricos o consumo falla |
| Empaquetado | Genera el binario, lo firma (tema 43), le asigna versión (tema 42) y guarda el ELF y el mapa como artefactos | — |
# Esquema de una definición de integración continua (la sintaxis exacta depende del sistema: GitHub Actions, GitLab CI, Jenkins…)
etapas:
compilar:
ejecutar: [ "cmake -B build -DCMAKE_TOOLCHAIN_FILE=arm.cmake -DWERROR=ON", "cmake --build build" ]
estatico:
necesita: compilar
ejecutar: [ "cppcheck --enable=warning,performance --error-exitcode=1 src/", "clang-format --dry-run -Werror src/*.c" ]
unitarias:
necesita: compilar
ejecutar: [ "make -C test run SANITIZE=1", "gcovr --fail-under-line 85" ]
presupuestos:
necesita: compilar
ejecutar: [ "python tools/verificar_mapa.py build/firmware.map --flash-max 245760 --ram-max 18432 --pila-min 25" ]
simulador:
necesita: [ unitarias, presupuestos ]
ejecutar: [ "renode-test test/escenarios.robot" ]
hardware_en_el_lazo:
necesita: [ simulador, estatico ]
corredor: banco-1 # un servidor con una placa conectada
ejecutar: [ "tools/grabar.sh build/firmware.bin", "pytest test/hil --placa banco-1" ]
empaquetar:
necesita: hardware_en_el_lazo
ejecutar: [ "tools/firmar.sh build/firmware.bin", "tools/guardar_artefactos.sh build/firmware.elf build/firmware.map" ]
El orden de las etapas importa por una razón de costo. Una etapa solo corre si las anteriores pasaron, así que conviene poner primero las baratas y que más fallan, y reservar al final las lentas y escasas. La Parte 4 del programa lo calcula con cinco etapas: compilar (1 min, falla el 10 %), análisis estático (2 min, 8 %), pruebas unitarias (1 min, 12 %), integración en la PC (5 min, 5 %) y hardware en el lazo (30 min, 6 %). Ordenadas de lo barato a lo caro, cada confirmación consume en promedio 28,04 minutos y el banco de hardware trabaja 20,77 minutos; en el orden inverso, 37,89 minutos y el banco, 30 minutos (corre siempre, también para código que no compila). Entre las 120 órdenes posibles, el mejor es 27,87 minutos y el peor, 38,05 minutos (en la práctica, compilar debe ir primero, porque lo demás lo necesita). El banco se libera casi un tercio: los cambios defectuosos ni llegan a él.
Algunas costumbres que acompañan a la CI: compilaciones reproducibles (la misma entrada da el mismo binario: versiones del compilador fijadas, ideal en un contenedor), guardar el ELF y el mapa de cada versión liberada (tema 45), ramas cortas que se integran seguido en lugar de acumular cambios, y que un cambio que rompe la CI se arregla de inmediato: una CI en rojo que nadie atiende se convierte en una CI ignorada.
10 · Comprobar que funciona
Verificación y diagnóstico
También el sistema de pruebas se verifica: una suite que nunca falla, o que falla sin razón, pierde toda su utilidad.
| Qué verificar | Cómo | Resultado esperado |
|---|---|---|
| Que las pruebas pueden fallar | Introducir un error a propósito (un mutante manual) y ver que alguna prueba lo detecta | La suite se pone en rojo |
| Que los límites están probados de los dos lados | Revisar cada comparación del código: ¿hay un caso en el borde y otro justo al lado? | Sí, en cada una (la Parte 2) |
| Que la suite es determinista | Ejecutarla 50 veces seguidas, con orden aleatorio | El mismo resultado siempre |
| La cobertura del código nuevo | Medirla en cada cambio (líneas y ramas) | No baja; el código crítico, cerca del 100 % |
| Que se ejerce el binario real | Ejecutar un subconjunto en el simulador o en el banco con el firmware que se entrega | Mismos resultados que en la PC |
| Que cada error corregido tiene su prueba | Cada ficha de error cerrada enlaza con una prueba de regresión | Sí |
| Que el banco de hardware se mantiene | Una prueba «de salud» del banco (referencias de tensión, cables) antes de las pruebas | El banco está sano; si no, las fallas no se atribuyen al firmware |
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| La cobertura es del 100 % y los errores siguen llegando | Las pruebas ejecutan el código sin verificar los resultados, o no prueban los bordes | Medir con pruebas de mutación; revisar las aserciones y los casos límite |
| Una prueba falla a veces | Depende del tiempo, del orden, del azar sin semilla o de un recurso compartido | Hacerla determinista (reloj falso, semilla fija); si no se puede, aislarla y arreglarla de inmediato |
| Pasa en la PC y falla en el equipo | Diferencias de tamaño de tipos, alineación, orden de bytes, compilador o temporización | Tipos de ancho fijo; correr un subconjunto en el simulador y en el banco |
| El código no se puede probar sin el hardware | La lógica incluye registros y cabeceras del fabricante | Separar la capa de acceso a hardware; inyectar las dependencias |
| La CI tarda una hora y nadie la espera | Etapas lentas al principio, o todo en serie | Ordenar de lo barato a lo caro; paralelizar; correr el banco solo si pasó lo anterior |
| Todos ignoran la CI en rojo | Fallas falsas frecuentes, o nadie es responsable de arreglarla | Eliminar las pruebas inestables; la persona que rompe, arregla |
| El mismo error vuelve a aparecer | No se escribió una prueba de regresión | Cada error corregido deja una prueba (tema 46) |
11 · Analogía con programación
Probar el modelo en Python
El programa reúne cuatro piezas del tema: la función nivel(mv) con sus ocho casos en los límites; las pruebas de mutación, con 12 mutantes y tres suites; el modelo de costo de los defectos según el nivel en que se detectan; y el cálculo del tiempo esperado de una integración continua según el orden de sus etapas, con las 120 órdenes posibles.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
# Pruebas del firmware: valores límite, pruebas de mutación, costo de encontrar un defecto según el nivel y orden de las etapas de una integración continua.
import itertools
# ---- Parte 1: una función de lógica pura y sus casos de prueba -----------------------------------------------------------------------------
def nivel(mv, u=(3000, 3300, 3600), estrictos=(True, True, True), limite=4200, lim_estricto=True):
"""Clasifica la tensión de una pila: -1 inválida, 0 crítica, 1 baja, 2 media, 3 llena."""
if (mv > limite) if lim_estricto else (mv >= limite): return -1
for i, t in enumerate(u):
if (mv < t) if estrictos[i] else (mv <= t): return i
return 3
print("Parte 1: nivel(mv) clasifica la tensión de la pila; umbrales 3000, 3300 y 3600 mV; más de 4200 mV es una lectura inválida")
casos = [(2999, 0), (3000, 1), (3299, 1), (3300, 2), (3599, 2), (3600, 3), (4200, 3), (4201, -1)]
assert all(nivel(mv) == esperado for mv, esperado in casos)
print(" casos en los límites (entrada -> salida esperada):", ", ".join(f"{mv}->{e}" for mv, e in casos))
print(f" clases de equivalencia: 5 (críticas, bajas, medias, llenas, inválidas); casos de prueba en los límites: {len(casos)}; todas las entradas posibles de 0 a 5000: {5001}")
# ---- Parte 2: pruebas de mutación: ¿detecta la suite un cambio pequeño en el código? --------------------------------------------------------
MUTANTES = []
for i, t in enumerate((3000, 3300, 3600)):
est = [True, True, True]; est[i] = False
MUTANTES.append((f"umbral {t}: < pasa a <=", dict(estrictos=tuple(est))))
u = [3000, 3300, 3600]; u[i] = t + 1
MUTANTES.append((f"umbral {t}: {t} pasa a {t + 1}", dict(u=tuple(u))))
u = [3000, 3300, 3600]; u[i] = t - 1
MUTANTES.append((f"umbral {t}: {t} pasa a {t - 1}", dict(u=tuple(u))))
MUTANTES += [("límite: > pasa a >=", dict(lim_estricto=False)), ("límite: 4200 pasa a 4201", dict(limite=4201)), ("límite: 4200 pasa a 4199", dict(limite=4199))]
SUITES = {"valores típicos": [2800, 3100, 3450, 3900, 5000],
"malla cada 100 mV (2500 a 4500)": list(range(2500, 4501, 100)),
"valores en los límites": [mv for mv, _ in casos]}
print()
print(f"Parte 2: {len(MUTANTES)} mutantes del código (un cambio pequeño cada uno); una suite 'mata' un mutante si alguna entrada da una salida distinta")
for nombre, entradas in SUITES.items():
vivos = [m for m, kw in MUTANTES if all(nivel(mv, **kw) == nivel(mv) for mv in entradas)]
muertos = len(MUTANTES) - len(vivos)
print(f" {nombre}: {len(entradas)} entradas, {muertos} de {len(MUTANTES)} mutantes detectados ({muertos / len(MUTANTES) * 100:.1f} %)")
if vivos: print(" sobreviven:", "; ".join(vivos))
# ---- Parte 3: el costo de encontrar un defecto según el nivel en que se encuentra -----------------------------------------------------------------
HORAS = {"prueba unitaria": 0.5, "integración en la PC": 2, "hardware en el lazo": 8, "campo": 80} # SUPUESTOS: horas para diagnosticar y corregir
ESCENARIOS = {"sin pruebas unitarias ni simulación": {"hardware en el lazo": 0.60, "campo": 0.40},
"con pirámide de pruebas": {"prueba unitaria": 0.50, "integración en la PC": 0.20, "hardware en el lazo": 0.20, "campo": 0.10}}
DEFECTOS = 100 # SUPUESTO: defectos introducidos por mes
print()
print(f"Parte 3: costo mensual de {DEFECTOS} defectos según dónde se detectan (horas de trabajo supuestas: {HORAS})")
totales = {}
for nombre, reparto in ESCENARIOS.items():
assert abs(sum(reparto.values()) - 1) < 1e-9
horas = sum(DEFECTOS * f * HORAS[n] for n, f in reparto.items())
totales[nombre] = horas
print(f" {nombre}: " + ", ".join(f"{f * 100:.0f} % en {n}" for n, f in reparto.items()) + f" -> {horas:,.0f} h".replace(",", " "))
a, b = totales.values()
print(f" la pirámide ahorra {a - b:,.0f} h por mes ({(1 - b / a) * 100:.0f} %); los 10 defectos que llegan al campo pesan {10 * 80 / b * 100:.0f} % del costo restante".replace(",", " "))
# ---- Parte 4: el orden de las etapas de una integración continua ------------------------------------------------------------------------------------
ETAPAS = {"compilar": (1, 0.10), "análisis estático": (2, 0.08), "pruebas unitarias": (1, 0.12), "integración en la PC": (5, 0.05), "hardware en el lazo": (30, 0.06)}
def costo(orden):
pasa, minutos, hil = 1.0, 0.0, 0.0
for e in orden:
t, p = ETAPAS[e]
minutos += pasa * t # la etapa solo corre si todas las anteriores pasaron
if e == "hardware en el lazo": hil = pasa * t
pasa *= 1 - p
return minutos, hil, pasa
print()
print("Parte 4: cada confirmación de código recorre etapas; si una falla, se corta ahí (minutos esperados por confirmación)")
for e, (t, p) in ETAPAS.items(): print(f" {e:<22} {t:>2} min, falla {p * 100:.0f} % de las veces")
barato = list(ETAPAS)
inverso = barato[::-1]
mejor = min(itertools.permutations(ETAPAS), key=lambda o: costo(o)[0])
peor = max(itertools.permutations(ETAPAS), key=lambda o: costo(o)[0])
for nombre, o in (("de lo barato a lo caro", barato), ("de lo caro a lo barato", inverso), ("mejor de las 120 órdenes", mejor), ("peor de las 120 órdenes", peor)):
m, h, ok = costo(o)
print(f" {nombre:<26} {m:5.2f} min en promedio; el banco de hardware trabaja {h:5.2f} min por confirmación; llegan a producción {ok * 100:.1f} %")
print(" orden mejor:", " > ".join(mejor))
Resultado, parte 1: los ocho casos en los límites (2999→0, 3000→1, 3299→1, 3300→2, 3599→2, 3600→3, 4200→3, 4201→inválido) pasan; hay 5 clases de equivalencia y 5001 entradas posibles. Parte 2: con 12 mutantes, la suite de valores típicos (5 entradas) detecta 0 (0,0 %), la malla cada 100 mV (21 entradas) detecta 8 (66,7 %; sobreviven «3000 pasa a 2999», «3300 pasa a 3299», «3600 pasa a 3599» y «4200 pasa a 4201») y los valores en los límites (8 entradas) detectan los 12 (100 %). Parte 3: con 100 defectos por mes y horas supuestas (0,5; 2; 8 y 80), sin pruebas unitarias ni simulación el costo es 3680 h y con la pirámide, 1025 h: ahorra 2655 h (72 %), y los 10 defectos que llegan al campo son el 78 % del costo restante. Parte 4: de lo barato a lo caro, 28,04 min por confirmación y 20,77 min del banco; de lo caro a lo barato, 37,89 y 30 min; el mejor orden de las 120 da 27,87 min y el peor 38,05 min; llegan al final el 65,1 % de las confirmaciones.
Probá agregar a la suite de valores típicos un solo caso, 3299, y ver cuántos mutantes más detecta; cambiar las probabilidades de falla de las etapas de la Parte 4 y ver cómo cambia el mejor orden; o agregar mutantes (por ejemplo, devolver un nivel equivocado) y ver qué suite los detecta. La lección del programa es que la calidad de una suite se puede medir, y que esa medida no coincide con la cantidad de pruebas ni con la cobertura.
12 · Exploración interactiva
¿Detecta tu suite el cambio?
El laboratorio implementa nivel(mv) y sus doce mutantes, y ejecuta sobre ellos la suite que elijas. Cada mutante «muere» si alguna entrada da una salida distinta de la del código original, y «sobrevive» si ninguna lo distingue. Podés agregar un valor propio (en milivoltios) para ver qué mutante pasa a detectarse.
Pruebas de mutación de nivel(mv)
Los 12 mutantes
Elegí una suite.
Recorrido guiado
- Con los valores típicos: 5 entradas, una por cada clase (y una inválida), y 0 de 12 mutantes detectados. Todo pasa, y la suite cubre todas las líneas, pero un umbral corrido en un milivoltio, o un
<cambiado por<=, no despierta ninguna prueba. - Elegí la malla cada 100 mV: 21 entradas, 8 de 12 (66,7 %). Los puntos de la malla caen justo sobre los umbrales (3000, 3300, 3600, 4200), así que detecta los cambios de
<a<=y de «+1», pero sobreviven cuatro: «3000 pasa a 2999», «3300 pasa a 3299», «3600 pasa a 3599» y «4200 pasa a 4201», que solo se ven con una entrada un milivoltio al otro lado. - Agregá el valor 3299 a la malla: 9 de 12 (75 %); sobreviven tres. Cada valor justo al otro lado de un borde mata exactamente un mutante. Agregá también 2999 y 3599 para llegar a 11, y 4201 para el último.
- Elegí los valores en los límites: solo 8 entradas y 12 de 12 (100 %), con menos casos que la malla. Dos entradas por borde, una a cada lado: es la regla de los valores límite.
- Con los valores típicos, agregá solo el 3000: 2 de 12 (16,7 %). Un único valor en un borde detecta justo los mutantes que se mueven en ese borde (el
<=y el «+1» del umbral de 3000), y ninguno más.
Desafío: ¿qué cambio de la función no podría detectar ninguna suite?
Un mutante equivalente: un cambio que no altera el comportamiento observable, por lo que ninguna prueba puede distinguirlo. En nivel, por ejemplo, no hay ninguno entre los doce (cada cambio altera la salida para alguna entrada), pero si se cambiara el orden de dos comprobaciones que nunca pueden cumplirse a la vez, o se sumara una constante que luego se descarta, el resultado sería idéntico. En los programas reales, entre los mutantes que sobreviven hay que distinguir los equivalentes (no importan) de los que revelan una prueba que falta (los importantes), y esa revisión es manual. Por eso el objetivo no es el 100 % a cualquier costo sino que cada sobreviviente se pueda explicar.
13 · Método de trabajo
Método: probar con orden
- Separá la lógica del hardware desde el diseño: HAL detrás de una interfaz, funciones puras, el tiempo como entrada, las máquinas de estados como tablas.
- Escribí las pruebas unitarias en la PC con un marco (Unity, CppUTest), con tipos de ancho fijo, todas las advertencias y sanitizers.
- Elegí los casos con criterio: clases de equivalencia, valores a ambos lados de cada límite, casos de error y de borde del tipo, tablas de transiciones, propiedades con entradas aleatorias de semilla fija.
- Cada error corregido deja una prueba de regresión, escrita antes de la corrección.
- Medí la calidad de la suite: cobertura de líneas y ramas para encontrar lo no probado, y mutación (aunque sea mental) para saber si lo probado se verifica.
- Sumá simulación para ejercitar el binario real y la lógica en contexto, y una simulación de la planta para los controles.
- Reservá el hardware en el lazo para lo que solo él ve: tiempos, periféricos, consumo, comunicaciones, actualización; y automatizalo con fuente programable, estímulos y medición.
- Provocá lo que el campo provocará: fallos inyectados, cortes de energía en cada punto, entradas malformadas, estrés y larga duración, con un modo de prueba en el firmware.
- Automatizá todo en integración continua, de lo barato a lo caro, con presupuestos de recursos, empaquetado firmado y artefactos guardados.
- Mantené el sistema de pruebas: sin pruebas inestables, con la CI en verde y con quien la rompe a cargo de arreglarla.
14 · Comprensión
Confusiones frecuentes
«El firmware no se puede probar sin el hardware»
La mayor parte sí: la lógica (cálculos, protocolos, máquinas de estados, decisiones) no depende del hardware si se diseña bien, y se prueba en la PC miles de veces por segundo. Al hardware se reserva lo que solo él puede mostrar: tiempos, periféricos, consumo.
«Con el 100 % de cobertura el código está bien probado»
La cobertura mide qué se ejecutó, no qué se verificó. En el laboratorio, la suite de valores típicos ejecuta todo el código y no detecta ni uno de los 12 mutantes. Una cobertura baja delata lo no probado; una alta no garantiza nada por sí sola.
«Cuantas más pruebas, mejor»
Lo que importa es dónde caen. Ocho entradas en los límites detectan todo lo que 21 entradas de una malla no detectan. Muchas pruebas redundantes, además, se vuelven lentas y caras de mantener, y la gente deja de correrlas.
«Si pasa en el simulador, pasa en el equipo»
El simulador hace lo que su modelo dice, no lo que hace el silicio: no reproduce los tiempos reales, el ruido ni las erratas del chip. Pasar en el simulador es una prueba de lógica; la de hardware se hace en el banco.
«Una prueba que falla de vez en cuando se puede ignorar»
Una prueba inestable destruye la confianza en toda la suite: se aprende a ignorar el rojo, y entonces se ignora también el rojo verdadero. Se arregla de inmediato (reloj falso, semilla fija, aislamiento) o se retira hasta arreglarla.
«Las pruebas son un trabajo aparte, para el final del proyecto»
Un código que no se diseñó para probarse es difícil de probar después, y los errores encontrados al final cuestan mucho más. La pirámide de la sección 1 muestra la economía. Las pruebas se escriben junto con el código y se ejecutan en cada cambio.
«La integración continua es solo para equipos grandes»
Un solo desarrollador la aprovecha igual: compilar, analizar y correr las pruebas en cada cambio lo protege de sus propios descuidos y deja un registro de que todo pasó antes de cada versión. El esfuerzo inicial es de horas y se paga rápido.
«Probar el camino feliz alcanza»
Los fallos del campo ocurren en los caminos tristes: un sensor que falla, un corte de energía a mitad de una escritura, una trama malformada, un contador que da la vuelta. Si el plan de pruebas no los provoca a propósito, los provocará el campo.
15 · Práctica guiada
Ejercicios
1. Valores límite
Una función pwm_ciclo(porcentaje) acepta valores de 0 a 100 y debe limitar los valores fuera de ese rango (los negativos a 0, los mayores de 100 a 100). ¿Qué valores de entrada elegirías para probar los bordes?
Ver solución
Dos bordes, cada uno probado a ambos lados y sobre él: −1, 0, 1 en el borde inferior y 99, 100, 101 en el superior: seis casos. Además, los extremos del tipo si es un entero de 16 bits con signo (−32 768 y 32 767) para comprobar que el límite no se desborda al restar o sumar. Con estos casos, los mutantes típicos (< por <=, 100 por 99 o por 101) mueren todos.
2. Índice de mutación
Una suite de pruebas detecta 7 de 10 mutantes. ¿Qué porcentaje es? ¿Qué se hace con los tres que sobreviven?
Ver solución
7 / 10 = 70 %. Para cada sobreviviente se pregunta: ¿es equivalente (el cambio no altera el comportamiento observable, por ejemplo una variable temporal con otro nombre)? Si lo es, se descarta. Si no, revela una prueba que falta: se escribe una entrada que haga diferir la salida del mutante de la original (casi siempre un valor justo al otro lado de un borde) y se agrega a la suite. El objetivo es que cada sobreviviente quede explicado.
3. Costo según el nivel
Un equipo detecta 40 defectos en un mes. El 70 % se encuentra en pruebas unitarias (0,5 h cada uno) y el 30 % en el banco de hardware (8 h cada uno). ¿Cuánto trabajo es? ¿Y si todos se hubieran encontrado en el banco?
Ver solución
Unitarias: 40 × 0,7 = 28 defectos × 0,5 h = 14 h. Banco: 40 × 0,3 = 12 defectos × 8 h = 96 h. Total: 110 h. Todos en el banco: 40 × 8 = 320 h. La diferencia, 210 h (un 66 %), es el valor de contar con las pruebas unitarias, antes de siquiera considerar el costo de los que llegan al campo.
4. Orden de las etapas
Una CI tiene dos etapas: A (2 minutos, falla el 20 % de las veces) y B (10 minutos, falla el 10 %). ¿Qué orden tiene menor tiempo esperado por confirmación?
Ver solución
Orden A, B: 2 + 0,8 × 10 = 10 min (B corre solo si A pasó, el 80 % de las veces). Orden B, A: 10 + 0,9 × 2 = 11,8 min. Conviene A primero. La regla general: ordenar de menor a mayor tiempo dividido por probabilidad de falla: A: 2 / 0,2 = 10; B: 10 / 0,1 = 100; A va primero. Es lo que hace que lo barato y lo que más falla vaya al principio.
5. Corte de energía en cada punto
Guardar un registro en flash se hace en tres escrituras: los datos (1), el CRC (2) y la marca de validez (3). ¿En cuántos puntos distintos se puede probar un corte de energía entre escrituras, y qué se verifica en cada uno?
Ver solución
Se puede cortar antes de la 1, entre la 1 y la 2, entre la 2 y la 3 y después de la 3: 4 puntos (0, 1, 2 o 3 escrituras completadas). En los tres primeros se debe verificar que al reiniciar el sistema recupera el registro anterior (el nuevo está incompleto y su marca de validez no está); en el último, que recupera el nuevo. Nunca un registro a medias. Si además se simula el corte durante cada escritura (bits a medio programar), son 3 puntos más: 7 casos en total, que una flash simulada en la PC recorre en milisegundos.
6. Cobertura de condiciones
Una función tiene if (a && b) { accion(); }. Una suite ejecuta los casos (a=1, b=1) y (a=0, b=0). ¿Tiene cobertura de líneas del 100 %? ¿Y de condiciones independientes (MC/DC)? ¿Cuál es el mínimo para MC/DC?
Ver solución
Líneas: 100 % (con el primer caso se ejecuta accion(); el segundo pasa por la condición falsa). Ramas: también las dos, verdadera y falsa. Pero MC/DC no se alcanza: en el caso (0,0) no se ve que b importa (con a en 0 el resultado es falso sea cual sea b), ni que a importa de forma independiente. El mínimo son tres casos: (1,1) da verdadero; (0,1) muestra que cambiar solo a cambia el resultado; (1,0) muestra lo mismo para b. En general, para n condiciones hacen falta al menos n + 1 casos.
16 · Síntesis
Ideas para recordar
- El firmware se prueba en una pirámide: muchas pruebas unitarias en la PC (milisegundos), algunas de integración con simulación, pocas con el hardware en el lazo y el campo en la punta. Cuanto más abajo se encuentra un error, más barato es: en el modelo de ejemplo, la pirámide reduce 3680 h a 1025 h por mes, pero los defectos que llegan al campo siguen pesando casi el 80 % del costo restante.
- Para probar en la PC hay que diseñar para probar: lógica separada del hardware, el HAL detrás de una interfaz, el tiempo como entrada, funciones puras y máquinas de estados como tablas. Se usan dobles de prueba (stubs, fakes, mocks), marcos como Unity o CppUTest y sanitizers; el código de la PC no es idéntico al del microcontrolador (tamaños de tipos, alineación), por lo que se usan tipos de ancho fijo y se corre un subconjunto en el simulador y el banco.
- Se eligen pocos casos que representen a muchos: clases de equivalencia, valores a ambos lados de cada límite (8 casos en vez de 5001), casos de error, tablas de transiciones, propiedades con entradas aleatorias y, sobre todo, una prueba de regresión por cada error corregido. Las pruebas deben ser pequeñas, independientes y deterministas.
- La cobertura no mide la calidad de las pruebas: ejecutar no es verificar. Las pruebas de mutación sí: con 12 mutantes, cinco valores típicos detectan 0, una malla de 21 valores detecta 8 y ocho valores en los límites detectan los 12. Lo que importa es dónde caen las entradas, no cuántas son.
- La simulación (QEMU, Renode, simulación de la planta) ejecuta el binario real sin hardware y prueba la lógica en contexto, pero hace lo que su modelo dice, no lo que hace el silicio. El hardware en el lazo automatiza un banco (fuente programable, estímulos, medición, canal de pruebas) y se reserva para tiempos, periféricos, consumo, comunicaciones y actualización.
- Las pruebas de robustez provocan lo que el campo provocará: fallos inyectados, corte de energía en cada punto de una escritura, entradas malformadas y fuzzing, estrés, larga duración y actualización con cortes. Un modo de prueba en el firmware vuelve alcanzables en una tarde las condiciones que tardan meses en aparecer.
- La integración continua ejecuta todas las etapas en cada cambio: compilar con
-Werror, análisis estático, pruebas unitarias con sanitizers y cobertura, presupuestos de flash/RAM/pila, simulación, hardware en el lazo, empaquetado firmado con el ELF guardado. Se ordenan de lo barato a lo caro: de 37,9 a 28 minutos por cambio y el banco liberado casi un tercio. Una CI ignorada es una CI inútil: se mantiene en verde.
¿Listo para el tema siguiente?
Antes de avanzar, revisá un proyecto tuyo y respondé: ¿qué parte de tu lógica podrías compilar hoy en la PC sin tocar nada?, ¿en cuántas comparaciones de tu código hay un caso de prueba a cada lado del borde?, ¿tus pruebas se ejecutan solas en cada cambio?, ¿qué pasaría si se corta la energía a mitad de cada escritura de flash? Si la primera respuesta es «nada», el trabajo útil de esta semana es separar la capa de hardware.
Con esto cerramos el bloque de depuración y pruebas. En el próximo tema empezamos a diseñar un producto de principio a fin: los requisitos y la especificación, es decir, qué debe hacer el sistema, cómo se mide y qué restricciones tiene.