← Índice del curso

Sistemas Embebidos · Tema 50 · Bloque 11

Calidad, documentación y mantenimiento

Un firmware no termina el día en que se entrega: empieza a vivir. Se instala en miles de equipos, hay que corregirlo cuando aparece un error, adaptarlo cuando cambia un componente, actualizarlo cuando se descubre una vulnerabilidad, y entregárselo a alguien que no lo escribió. Una gran parte del costo total de un producto embebido está después del primer envío, y lo que decide si ese costo es razonable se sembró antes: en el estilo del código, en cómo se revisó, en cómo se numeraron las versiones, en lo que quedó escrito. Este tema reúne las prácticas que mantienen sano a un producto durante años: estándares de código y análisis estático, revisiones, versionado, documentación que se mantiene viva, trazabilidad, mantenimiento de la flota y el ciclo de vida completo, incluido el día en que un componente deja de fabricarse.

01 · De qué hablamos

Qué es la calidad en un firmware

La calidad no es una sola cosa. En un producto embebido se reparte en cuatro propiedades que a menudo compiten entre sí, y cada proyecto decide cuánto pesa cada una:

Propiedades de la calidad de un firmware.
PropiedadPreguntaCómo se logra
Corrección¿Hace lo que debe?Requisitos verificables (tema 48), pruebas (tema 47), revisiones
Robustez y confiabilidad¿Sigue funcionando ante entradas raras y fallas?Programación defensiva, watchdog, pruebas de robustez (temas 37 a 39 y 47)
Mantenibilidad¿Se puede entender, corregir y extender con poco riesgo?Arquitectura clara (tema 49), estilo uniforme, documentación, pruebas que protegen los cambios
Portabilidad¿Se puede llevar a otro microcontrolador o compilador?Capas, tipos de ancho fijo, evitar comportamiento indefinido (tema 46)

La que más se descuida es la mantenibilidad, porque su costo se paga después y por otra persona (a menudo, uno mismo seis meses más tarde). Una idea útil es la deuda técnica: cada atajo (un número mágico, una función de 200 líneas, un módulo sin documentar, una prueba que se salteó) es un préstamo que acelera hoy y cobra intereses mañana, en forma de cambios más lentos y riesgosos. Un poco de deuda deliberada y anotada es una herramienta legítima (sacar un prototipo a tiempo); la deuda que se acumula sin que nadie la registre termina frenando todo.

Y un dato del contexto embebido que cambia las prioridades: corregir en el campo es caro o imposible. Un error de una aplicación web se corrige en una hora para todos; uno de un equipo instalado en 17 000 contenedores exige una actualización por radio (tema 42), con riesgo propio, o una visita técnica. Por eso en este mundo la calidad se construye antes de entregar, con prácticas baratas y repetidas, más que con parches después.

02 · Reglas compartidas

Estándares de código y análisis estático

Un estándar de código es un conjunto de reglas que todo el equipo sigue. Tiene dos capas, con propósitos distintos:

  • Estilo: formato, nombres, estructura de archivos. Su objetivo es que el código de cualquiera se lea igual. Se resuelve con un formateador automático (por ejemplo clang-format) y un archivo de configuración en el repositorio: nadie discute las llaves en una revisión.
  • Seguridad y portabilidad: reglas que evitan construcciones que el lenguaje deja indefinidas o propensas a error. Los conjuntos más conocidos son MISRA C (nacido en la industria automotriz y muy usado en sistemas críticos) y CERT C (orientado a la seguridad informática). Se verifican con análisis estático; conviene cotejar la versión vigente de cada norma, y que MISRA es un documento de acceso pago.
Reglas típicas de un estándar para firmware, con el motivo de cada una.
ReglaMotivoTema relacionado
Tipos de ancho fijo (uint16_t, int32_t) en lugar de int, long, unsignedEl tamaño de int depende de la plataforma: portabilidad y pruebas en la PC47
Llaves en todo if, for y whileUna segunda línea agregada después «parece» estar dentro y no lo está—
Sin memoria dinámica después de la inicializaciónFragmentación y fallas impredecibles31
Sin goto, sin recursiónFlujo difícil de seguir; la recursión consume pila sin límite46, 49
Sin strcpy, sprintf, gets; usar versiones con límiteDesbordes de buffer46
Constantes con nombre en lugar de «números mágicos»Un 4095 suelto no dice si es el ADC, un tamaño o un plazo—
Complejidad ciclomática de cada función de 10 o menosCada camino independiente es un caso a probar47
Todas las advertencias activadas y tratadas como erroresEl compilador detecta variables sin inicializar y conversiones peligrosas46
Datos compartidos con interrupciones: volatile y protección documentadaCondiciones de carrera16, 46

Dos reglas de método importan tanto como las de contenido. Primera: una regla que no se verifica automáticamente no existe: se la olvida en la tercera semana. Los formateadores, los compiladores con -Wall -Wextra -Werror y los analizadores estáticos (cppcheck, clang-tidy, las herramientas comerciales de MISRA) se ejecutan en la integración continua (tema 47) y rompen la compilación si hay una violación nueva. Segunda: las excepciones se documentan. A veces una regla no puede cumplirse (acceder a un registro exige un cast de entero a puntero); en ese caso se marca la línea con el motivo y la regla desviada, para que una persona la revise una vez y la herramienta no vuelva a avisar.

La complejidad ciclomática es la medida más usada de cuán enredada es una función: el número de caminos linealmente independientes, que se calcula sumando uno al número de puntos de decisión (if, for, while, case, &&, ||, ?:). Una función de complejidad 18 necesita al menos 18 casos de prueba para recorrer todos sus caminos (tema 47), y es difícil de razonar. La Parte 1 del programa y el laboratorio de este tema implementan un revisor sencillo: aplicado a cuatro fragmentos, el conforme da 0 violaciones y V = 1; el de defectos de estilo da 10 violaciones (tipos básicos, llaves, números mágicos) y V = 3; la máquina de estados con cinco case y once condiciones da 0 violaciones pero V = 18; y el inseguro da 9 (memoria dinámica, strcpy, sprintf, goto). Es un revisor superficial, de expresiones regulares, y ninguno reemplaza a una herramienta seria, pero muestra con qué poco se detectan problemas reales.

/* Una desviación documentada: se marca el motivo y la regla, para que se revise una vez */
#define REG_USART1  ((volatile uint32_t *)0x40013800u)   /* MISRA: cast de entero a puntero (desviación D-03: acceso a registros, revisada 2026-02-10) */

/* La máquina de estados de 18 caminos del laboratorio, reducida con una tabla (tema 24): cada transición es un dato, no un if */
typedef struct { uint8_t estado; uint8_t evento; uint8_t siguiente; } Transicion;
static const Transicion TABLA[] = {
    { ST_ALMACENADO, EV_BOTON,       ST_ACTIVO  },
    { ST_ACTIVO,     EV_FUERA_RANGO, ST_ALARMA  },
    { ST_ACTIVO,     EV_FALLA,       ST_FALLA   },
    { ST_ALARMA,     EV_EN_RANGO,    ST_ACTIVO  },
    { ST_ALARMA,     EV_FALLA,       ST_FALLA   },
    { ST_DESCARGA,   EV_BOTON,       ST_ACTIVO  },
    { ST_FALLA,      EV_BOTON,       ST_ACTIVO  },
};
uint8_t siguiente_estado(uint8_t estado, uint8_t evento) {
    for (uint8_t i = 0u; i < (uint8_t)(sizeof TABLA / sizeof TABLA[0]); i++) {
        if ((TABLA[i].estado == estado) && (TABLA[i].evento == evento)) { return TABLA[i].siguiente; }
    }
    return estado;                                        /* evento sin transición: se queda donde está */
}

La tabla tiene complejidad 4 (el for, el if y el && suman 3 decisiones, más uno), sin importar cuántas transiciones tenga: la lógica se movió a los datos, que se pueden revisar de un vistazo y probar con una tabla de casos. Es la misma idea de las pruebas del tema 47: la complejidad baja, y baja el costo de verificar.

03 · Otro par de ojos

Revisiones de código

La revisión de código consiste en que otra persona lea un cambio antes de que se incorpore. Encuentra errores que quien escribió no ve (leyó lo que quiso escribir), difunde el conocimiento del sistema entre varias personas (evita que solo una sepa cómo funciona el módulo de radio) y mejora el código sin esfuerzo extra. Funciona mejor con algunas condiciones:

  • Cambios pequeños. La atención se agota rápido: un cambio de 50 a 200 líneas se revisa bien; uno de 2000 se «aprueba» sin leerlo. Los cambios chicos y frecuentes se revisan, integran y revierten mejor.
  • Las herramientas primero. Antes de que una persona mire, la integración continua ya pasó el formateador, el compilador con -Werror, el análisis estático, las pruebas y la medida de tamaño (tema 47). Así la revisión humana no gasta tiempo en comas y se concentra en lo que solo una persona puede juzgar.
  • Una lista de comprobación que recuerde los puntos específicos del dominio embebido.
  • Tono constructivo: se revisa el código, no a la persona; se pregunta («¿qué pasa si el sensor no responde?») en lugar de afirmar; se separa lo obligatorio de lo opcional.
Lista de comprobación para una revisión de firmware.
PreguntaQué se busca
¿Cumple el requisito y tiene prueba?El cambio enlaza con un requisito o un defecto (trazabilidad); trae su prueba de regresión (tema 46)
¿Qué pasa si falla?Cada llamada que puede fallar tiene su manejo; no hay errores ignorados; hay límite en los reintentos
¿Datos compartidos con interrupciones?volatile, sección crítica o atómico; lecturas de varias palabras consistentes (tema 46)
¿Límites y tamaños?Buffers con largo comprobado, índices en rango, pila y RAM dentro del presupuesto (temas 46 y 49)
¿Tiempo?Nada bloqueante dentro de una ISR; secciones críticas cortas; esperas con límite
¿Energía?No impide el sueño profundo; no deja periféricos encendidos (tema 35)
¿Interfaces y arquitectura?Las dependencias van hacia abajo; el contrato del módulo está actualizado (tema 49)
¿Es legible?Nombres que dicen lo que son; comentarios que explican el porqué; funciones cortas
¿Seguridad?Sin claves ni datos sensibles; entradas externas validadas (tema 43)

Dos advertencias de realismo. La revisión no reemplaza a las pruebas: un lector humano no ejecuta el código ni mide tiempos. Y no escala sola: si la mayoría de los comentarios de una revisión son sobre estilo, es una señal de que falta automatizar el estilo; si son sobre diseño, de que la discusión debió ocurrir antes de escribir el código (en un ADR, tema 49).

04 · Saber qué hay en cada equipo

Control de versiones y versionado

Todo el código, y también los scripts de compilación, la configuración del enlazador, los datos de calibración, la documentación y las pruebas, vive en un sistema de control de versiones (en la práctica, Git). Aporta tres cosas que en un producto embebido son indispensables: el historial de qué cambió, cuándo y por qué; la posibilidad de reproducir exactamente el firmware de cualquier versión liberada; y las ramas para trabajar en paralelo (una corrección urgente sobre la versión de campo mientras se desarrolla la siguiente).

Cada firmware que sale de la fábrica debe poder responder: «¿exactamente qué código y con qué compilador y opciones se construyó esto?». Para eso se hacen tres cosas: se marca (tag) cada versión liberada; se incrusta la versión en el binario (para que el equipo la informe y el cargador de arranque la compare, tema 42); y se guarda el ELF y el mapa de esa versión (tema 45).

# Marcar la versión liberada y generar el archivo de versión que se compila dentro del firmware
git tag -a v1.5.0 -m "Versión 1.5.0: modo de alarma con medida cada 2 s"
git describe --tags --dirty --always              # p. ej. v1.5.0, o v1.5.0-3-g9f2c1ab-dirty si hay cambios sin confirmar

# version.h generado en cada compilación (no se escribe a mano)
cat > build/version.h <<EOF
#define FW_VERSION_MAYOR  1
#define FW_VERSION_MENOR  5
#define FW_VERSION_PARCHE 0
#define FW_COMMIT         "$(git rev-parse --short HEAD)"
#define FW_LIMPIO         $(git diff --quiet && echo 1 || echo 0)    /* 0 si había cambios sin confirmar: esa compilación no se libera */
EOF

La numeración de las versiones sigue de forma habitual el versionado semántico (MAYOR.MENOR.PARCHE), que le da significado a cada número:

Versionado semántico aplicado a un firmware.
NúmeroCuándo subeEjemplo
MAYORUn cambio incompatible: el formato de la trama, la estructura de los datos guardados, la interfaz con otros equipos1.5.0 → 2.0.0 al cambiar el formato de la trama de radio
MENORUna función nueva compatible con lo anterior1.4.2 → 1.5.0 al agregar el modo de alarma
PARCHESolo correcciones de errores, sin cambio de funcionamiento1.4.2 → 1.4.3 al corregir el desborde del contador
PrelanzamientoUn sufijo para versiones de prueba1.5.0-rc1 es anterior a 1.5.0

La Parte 2 del programa lo ejerce. Dos lecciones concretas. Primera: las versiones se comparan numéricamente, no como texto: ordenadas alfabéticamente, 1.4.10 queda antes que 1.4.2; el orden correcto es 1.4.2, 1.4.10, 1.5.0-rc1, 1.5.0, 2.0.0. Es un error clásico en cargadores de arranque y en servidores de actualización (tema 42), y causa actualizaciones rechazadas o, peor, retrocesos. Segunda: la versión siguiente se puede derivar de los mensajes de las confirmaciones con un convenio de prefijos (fix:, feat:, feat!: o la palabra BREAKING para los incompatibles): desde 1.4.2, con dos correcciones, una función nueva y un cambio de documentación, la siguiente es 1.5.0; si además hay un cambio incompatible, 2.0.0; con solo correcciones, 1.4.3. Y el cargador con la versión 1.5.0 instalada rechaza la 1.4.2 (retroceso) y la 1.5.0 (no es más nueva) y acepta la 1.5.1 y la 2.0.0 (anti-retroceso, tema 43).

En un producto embebido hay varios números de versión que conviene no confundir: la del firmware, la del hardware (revisión de la placa), la del protocolo de comunicaciones, la del formato de los datos guardados y la del cargador de arranque. Cada una evoluciona a su ritmo, y las combinaciones compatibles se documentan: «el firmware 2.x requiere la revisión de placa C o posterior y el protocolo 3». El equipo informa todas en su reporte, para que el servidor sepa con qué está hablando.

05 · Lo que no está en el código

Documentación que se mantiene

La documentación que nadie mantiene es peor que ninguna: engaña. La clave es documentar lo que el código no puede decir y mantenerlo cerca del código, en el mismo repositorio y revisado en las mismas revisiones. Lo que el código ya dice (i++; // incrementa i) es ruido; lo que no dice es el porqué.

Qué documentar y dónde.
DocumentoContenidoDónde vive
Comentarios en el códigoEl porqué, las restricciones y las trampas; las unidades; las referencias a la hoja de datosEn el archivo fuente
Contratos de los módulosPrecondiciones, errores, contexto, propiedad de la memoria (tema 49)En los encabezados, con un formato que una herramienta (Doxygen) convierte en referencia
README del proyectoCómo compilar, grabar, probar y depurar desde cero (en pocos comandos)En la raíz del repositorio
Arquitectura y decisionesLas vistas del tema 49, el presupuesto y los ADREn el repositorio, como texto
Especificación y trazabilidadRequisitos, su verificación y la matriz (tema 48)En el repositorio o en una herramienta de gestión de requisitos
Manual de usuario y de instalaciónCómo se instala, se configura, qué significan los indicadoresDocumento aparte, con versión propia
Registro de cambios (changelog)Qué cambió en cada versión, en lenguaje de quien lo usaEn el repositorio, generado en parte desde las confirmaciones
ProcedimientosCómo liberar una versión, cómo atender un defecto de campo, cómo se hace la compra final de un componenteEn el repositorio o en el sistema de calidad
/**
 * @brief  Convierte la lectura cruda del ADC a décimas de grado Celsius.
 * @param  crudo  Lectura de 12 bits (0 a 4095) del canal del termistor.
 * @return Temperatura en décimas de °C (por ejemplo, 253 es 25,3 °C); TEMP_INVALIDA si el valor es imposible.
 * @note   La curva sale de la tabla 7 de la hoja de datos del sensor, con la corrección del tema de calibración.
 *         NO llamar desde una interrupción: usa una tabla en flash y demora hasta 40 µs.
 */
int16_t temperatura_convertir(uint16_t crudo);

/* El sensor necesita 15 ms desde el reinicio antes de responder (hoja de datos, tabla 3). No acortar esta espera. */
#define SENSOR_ESPERA_ARRANQUE_MS  15u

/* NO reordenar: la radio debe apagarse antes de entrar en reposo o queda consumiendo 8 mA (medido, tema 36). */

Los comentarios del ejemplo dicen lo que el código no puede: la unidad, el rango, la fuente de los datos, la restricción del contexto y la razón de un orden aparentemente arbitrario. Son los que le ahorran a alguien un día de depuración. Las prácticas que mantienen viva la documentación son pocas: escribirla junto con el cambio (la revisión comprueba que se actualizó), generar lo que se pueda (referencia de la API, versión, tamaño de memoria, registro de cambios), mantenerla como texto bajo control de versiones, y borrar lo que ya no es cierto: un documento viejo con apariencia oficial hace daño.

06 · Reproducir y responder

Trazabilidad y gestión de la configuración

La trazabilidad de los requisitos (tema 48) sigue después: de cada requisito al diseño, al código, a la prueba y, ahora, a la versión que lo implementó. Y la gestión de la configuración es la disciplina de saber, en todo momento, qué componentes forman cada versión liberada y poder reconstruirla. Su herramienta central es la línea base (baseline): el conjunto exacto de elementos de una liberación: el código en un commit determinado, el compilador y sus opciones, las bibliotecas de terceros con su versión, el cargador de arranque, los datos de configuración y la documentación.

La Parte 3 del programa muestra una manera simple de hacerlo cumplir: un manifiesto con todo lo que influye en el binario (el commit, la versión del compilador, las opciones, las versiones de las bibliotecas) y una huella (un hash de ese manifiesto). Con el mismo manifiesto, dos compilaciones dan la misma huella (ae8f26b0ca40 en el ejemplo); al cambiar solo la opción de optimización de -Os a -O2, la huella cambia (ef8ad64bc367): un cambio que a nadie le parecía un cambio de versión queda a la vista. Una compilación reproducible (la misma entrada da exactamente el mismo binario, con el compilador fijado, en un contenedor) permite además verificar, meses después, que el firmware instalado corresponde a lo que se aprobó.

La lista de componentes de terceros, con sus versiones y licencias, es la lista de materiales de software (SBOM, del inglés software bill of materials). Cuesta poco mantenerla y responde la pregunta de la que depende el mantenimiento de seguridad (tema 43): «se publicó una vulnerabilidad en la pila de radio 3.4.1: ¿qué equipos de la flota la tienen?». En el ejemplo de la Parte 3, el aviso afecta a la pila anterior a 3.4.2, y la flota tiene tres versiones: la 1.4.2 (pila 3.4.1, 12 000 equipos), la 1.5.0 (pila 3.4.2, 5000) y la 2.0.0 (pila 3.5.0, 800). Están expuestos los de la 1.4.2: 12 000 de 17 800 equipos, el 67,4 %. Sin la lista, esa respuesta lleva días de investigación; con ella, un minuto, y el plan de acción (publicar la 1.4.3 con la pila corregida, priorizar esa parte de la flota) se puede empezar el mismo día.

Otras buenas costumbres de configuración: fijar las versiones de las herramientas y bibliotecas (nada de «la última»), guardar el ELF, el mapa y la huella de cada versión liberada (sin ellos, un HardFault en el campo solo muestra direcciones, tema 45), registrar qué versión tiene cada equipo (el informe de estado de cada equipo la incluye, tema 37), y firmar lo que se libera (tema 43). Quien liberó la versión 1.5.0 hace tres años tiene que poder volver a compilarla hoy y obtener el mismo binario.

07 · Después de la entrega

Mantenimiento de un producto en servicio

El mantenimiento es todo el trabajo sobre un producto ya entregado. Se reconocen cuatro tipos, con motivos distintos:

Tipos de mantenimiento.
TipoMotivoEjemplo en el registrador
CorrectivoCorregir un defectoEl contador de milisegundos da la vuelta a los 49,7 días (tema 46)
AdaptativoAdaptarse a un cambio del entorno: un componente nuevo, una norma, un protocoloReemplazar el sensor que deja de fabricarse por otro
PerfectivoMejorar o agregar funcionesEl modo de alarma con medida cada 2 s
PreventivoReducir riesgos futuros: reordenar, documentar, agregar pruebas, pagar deuda técnicaPartir la función de 18 caminos en una tabla de transiciones

Un proceso mínimo de gestión de defectos evita que los problemas se pierdan y que lo urgente desplace a lo importante. Cada defecto se registra en un sistema de seguimiento con los datos para reproducirlo (versión, configuración, pasos, registros del equipo; tema 44), se clasifica y se prioriza. Conviene distinguir la severidad (qué tan grave es el daño: seguridad de personas, pérdida de datos, molestia) de la prioridad (qué tan pronto se corrige: depende también de cuántos equipos afecta y de si hay un rodeo):

Una clasificación simple de defectos.
NivelDescripciónRespuesta típica
CríticoRiesgo para personas o seguridad, pérdida de datos, equipos que dejan de funcionar o vulnerabilidad explotableCorrección inmediata y actualización de emergencia (tema 42)
AltoUna función principal falla, con rodeo difícilEn la próxima versión de parche
MedioUna función secundaria falla o hay un rodeo razonableSe planifica para una versión menor
BajoMolestia, texto, caso muy raroCuando haya capacidad

El mantenimiento de una flota tiene problemas propios. Compatibilidad: una corrección no debe romper lo que ya funciona (por eso el versionado semántico y las pruebas de regresión), y los equipos de campo tienen versiones distintas, así que hay que saber cuáles se admiten y desde cuál se puede actualizar a cuál. Despliegue gradual: se actualiza primero un grupo chico, se mira que funcione (los contadores de salud y reinicios, tema 37), y recién entonces el resto (tema 42). Soporte de versiones: no se puede mantener todas las versiones para siempre; se define una política («se corrigen errores críticos de las dos últimas versiones mayores») y se comunica. Seguridad: la pila de radio, la biblioteca criptográfica y el sistema operativo reciben avisos de vulnerabilidades durante toda la vida del producto, y alguien tiene que leerlos, evaluarlos y actuar (tema 43): un producto sin ese proceso se vuelve vulnerable aunque su código no haya cambiado.

Y un criterio práctico que ahorra mucho dolor: cada corrección entra con su prueba de regresión (tema 46) y cada cambio, con su análisis de impacto sobre la trazabilidad: qué requisitos, módulos y pruebas toca. Una corrección «de una línea» en una rutina de interrupción puede romper un plazo; la matriz de trazabilidad y la integración continua existen para que se descubra antes de liberar.

08 · De la idea al retiro

El ciclo de vida del producto

Un producto embebido atraviesa fases, y las decisiones de cada una condicionan las siguientes. Lo que distingue a los sistemas embebidos de otros es la larga vida: es común que un equipo industrial o médico se fabrique durante cinco años y se mantenga en servicio diez más.

Ciclo de vida de un producto embebido Una línea de tiempo con cuatro fases: desarrollo (años cero a uno), producción (años uno a seis), soporte (años seis a dieciséis) y fin de vida. Debajo, tres barras de actividades: el desarrollo ocupa la primera fase; la producción con compras de componentes ocupa la segunda; el mantenimiento con parches y actualizaciones cubre desde la producción hasta el fin del soporte. Una marca indica el aviso de fin de fabricación de un componente y la última compra antes del fin del suministro. DesarrolloProducciónSoporteFin de vida año 0año 1año 6año 16 requisitos, diseño, pruebasfabricación, compras, calidadparches, actualizaciones, repuestos, avisos de seguridad aviso de fin de fabricación de un componente:compra final (Parte 4)

Cada fase deja tareas que si se postergan salen caras. Desarrollo: diseñar para el mantenimiento (arquitectura, pruebas, documentación) y elegir componentes con garantía de suministro a largo plazo (tema 49). Producción: compras, calidad de fábrica y pruebas de producción (tema 47), y control de la configuración. Soporte: los cuatro tipos de mantenimiento, la gestión de flota y el aprovisionamiento de repuestos. Fin de vida: avisar con anticipación, ofrecer un reemplazo, definir qué ocurre con los datos del cliente (borrado seguro, tema 43), y qué versión final de firmware se deja y se documenta.

El caso más característico del ciclo de vida es la obsolescencia de componentes: un fabricante anuncia que un chip deja de producirse. Las opciones son comprar una cantidad suficiente de una vez (compra final, last time buy), rediseñar el producto con un sustituto (con la revalidación y el posible cambio de firmware que implica) o dejar de vender el producto. La Parte 4 hace la cuenta de la primera: un componente anuncia su fin de fabricación con la última compra posible al final del año 3; se fabrican 20 000 equipos por año durante 5 años, y se estima que el 3 % de la base instalada necesita un repuesto de ese componente cada año hasta el año 10. Después del año 3 hacen falta 40 000 unidades para la producción de los años 4 y 5 y 19 200 para repuestos de los años 4 a 10, en total 59 200; con un margen del 10 %, 65 120 unidades, 58 608 USD a 0,90 USD cada una. Es una inversión que hay que decidir con tiempo, comparándola con el costo de rediseñar, y la cuenta depende de supuestos (la tasa de repuestos, la vida útil de almacenamiento del componente) que conviene revisar cada año. Por eso conviene que el diseño tolere alternativas desde el principio: una capa HAL y controladores que aíslen el componente (tema 49) vuelven el cambio de sensor un trabajo de semanas y no de meses.

Una última idea sobre el final: la retirada también se diseña. Qué se hace con las claves y los datos que quedan en el equipo, cómo se desactiva la conectividad, cómo se reciclan las baterías, qué ocurre con la nube que atiende a los equipos viejos. Un producto abandonado sin plan sigue estando conectado y siendo vulnerable años después de que el fabricante dejó de mirarlo.

09 · Comprobar que funciona

Verificación y diagnóstico

Comprobaciones sobre la calidad y el mantenimiento.
Qué verificarCómoResultado esperado
El estándar se cumpleFormateador, compilador con -Werror y análisis estático en la integración continuaCero violaciones nuevas; desviaciones documentadas
Complejidad bajo controlMedir la complejidad ciclomática de cada función en cada cambioNinguna por encima de 10 sin justificación
Todo cambio se revisaReglas del repositorio: no se integra sin revisión y sin la integración continua en verdeCada cambio tiene revisor y trazabilidad al requisito o defecto
Cada versión liberada se puede reproducirRecompilar desde la marca en un entorno limpio y comparar la huella o el binarioEl mismo binario, o la misma huella
El equipo informa su versiónRevisar el reporte de estado: firmware, hardware, protocolo, arranqueTodas las versiones presentes y consistentes
La documentación está al díaInstalar y compilar siguiendo solo el README en una máquina nuevaFunciona sin preguntar a nadie
Se sabe qué hay en la flotaConsulta de la lista de componentes por versión: ¿cuántos equipos tienen cada biblioteca?Respuesta en minutos
Hay un plan para los componentesRevisar el estado de ciclo de vida de cada componente crítico, cada añoNinguno sin alternativa o compra planificada
Síntomas frecuentes y qué revisar.
SíntomaCausa probableQué hacer
Nadie se anima a tocar un móduloDeuda técnica: sin pruebas, sin documentación, complejidad altaPruebas de caracterización primero; refactorizar en pasos pequeños; documentar el porqué
No se puede reproducir un binario de hace dos añosSin línea base: compilador, bibliotecas o configuración sin fijarManifiesto, versiones fijas, contenedor de compilación y archivo de artefactos
Un defecto de campo no se puede diagnosticarSin ELF de esa versión o sin registro en el equipoGuardar ELF y mapa de cada liberación; informe de falla (temas 44 y 45)
Las revisiones son discusiones de estiloEl estilo no está automatizadoFormateador y análisis estático en la integración continua
Se rechazan actualizaciones válidas o se aceptan viejasVersiones comparadas como textoComparación numérica de MAYOR, MENOR, PARCHE (Parte 2)
Un aviso de seguridad lleva una semana de análisisNo hay lista de componentes por versiónSBOM por versión y registro de la versión de cada equipo
Un componente deja de fabricarse y no hay planSin seguimiento del ciclo de vida ni aislamiento del componenteRevisión anual; HAL y controladores que aíslen; compra final calculada

10 · Analogía con programación

Probar el modelo en Python

El programa reúne cuatro piezas del tema: un revisor de estilo y de complejidad ciclomática para fragmentos de C (el mismo del laboratorio); el versionado semántico, con el orden correcto de versiones, la versión siguiente derivada de las confirmaciones y la decisión de un cargador de arranque; la huella de una compilación y la exposición de una flota a una vulnerabilidad; y el cálculo de la compra final de un componente que deja de fabricarse.

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

# Calidad, documentación y mantenimiento: revisor de estilo y complejidad, versionado, huella de una compilación y compra final de un componente.
import re, json, hashlib

# ---- Parte 1: un revisor de estilo y de complejidad ciclomática para código C --------------------------------------------------------------------
EJEMPLOS = [
("Conforme", """/* Promedio móvil de 8 muestras (sin memoria dinámica) */
#define N_MUESTRAS 8u
static int32_t suma_total = 0;
int32_t promedio_agregar(int16_t nueva, int16_t *ventana, uint8_t *indice) {
    suma_total -= ventana[*indice];
    ventana[*indice] = nueva;
    suma_total += nueva;
    *indice = (uint8_t)((*indice + 1u) % N_MUESTRAS);
    return suma_total / (int32_t)N_MUESTRAS;
}"""),
("Defectos de estilo", """int leer_temp(int raw)
{
    int celsius;
    if (raw > 4095) return -1;
    celsius = raw * 125 / 1000 - 40;
    for (int i = 0; i < 10; i++) celsius += 0;
    return celsius;
}"""),
("Complejidad alta", """uint8_t decidir_modo(uint8_t estado, int16_t temp, uint8_t bateria, bool radio_ok, bool boton) {
    switch (estado) {
    case ST_ALMACENADO:
        if (boton) { return ST_ACTIVO; }
        break;
    case ST_ACTIVO:
        if (temp > LIMITE_ALTO || temp < LIMITE_BAJO) { return ST_ALARMA; }
        if (bateria < BAT_MIN && !radio_ok) { return ST_FALLA; }
        break;
    case ST_ALARMA:
        if (temp <= LIMITE_ALTO && temp >= LIMITE_BAJO) { return ST_ACTIVO; }
        if (!radio_ok && bateria < BAT_MIN) { return ST_FALLA; }
        break;
    case ST_DESCARGA:
        if (boton || !radio_ok) { return ST_ACTIVO; }
        break;
    case ST_FALLA:
        return boton ? ST_ACTIVO : ST_FALLA;
    default:
        return ST_FALLA;
    }
    return estado;
}"""),
("Inseguro", """char nombre[16];
void guardar_nombre(const char *entrada) {
    char *copia = malloc(32);
    strcpy(nombre, entrada);
    sprintf(copia, "id=%d", 4242);
    if (!copia) goto salir;
salir:
    free(copia);
}""")]
def limpiar(codigo):
    codigo = re.sub(r"/\*.*?\*/", lambda m: re.sub(r"[^\n]", " ", m.group()), codigo, flags=re.S)
    codigo = re.sub(r"//[^\n]*", "", codigo)
    return re.sub(r'"[^"\n]*"', '""', codigo)
def analizar(codigo):
    limpio = limpiar(codigo)
    viol = []
    for n, (orig, l) in enumerate(zip(codigo.split("\n"), limpio.split("\n")), 1):
        if len(orig) > 100: viol.append((n, "línea de más de 100 caracteres"))
        if re.search(r"\b(int|short|long|unsigned)\b", l) and not re.search(r"\bmain\s*\(", l): viol.append((n, "tipo básico: usar tipos de ancho fijo (uint16_t, int32_t)"))
        if re.search(r"^\s*(if|for|while)\s*\(.*\)\s*[^{\s;][^{]*;\s*$", l): viol.append((n, "if/for/while sin llaves"))
        if re.search(r"\bgoto\b", l): viol.append((n, "uso de goto"))
        if re.search(r"\b(malloc|calloc|realloc|free)\s*\(", l): viol.append((n, "memoria dinámica"))
        if re.search(r"\b(strcpy|strcat|sprintf|gets)\s*\(", l): viol.append((n, "función insegura (sin comprobar el largo)"))
        if not re.search(r"#define|\bconst\b|\benum\b", l):
            for tok in re.findall(r"\b0[xX][0-9A-Fa-f]+\b|\b\d+\b", l):
                if len(tok) >= 2: viol.append((n, f"número mágico {tok}: usar una constante con nombre"))
    puntos = len(re.findall(r"\b(if|for|while|case)\b", limpio)) + limpio.count("&&") + limpio.count("||") + limpio.count("?")
    return viol, puntos + 1
print("Parte 1: revisor de estilo y complejidad ciclomática (V = decisiones + 1; se sugiere no pasar de 10 por función)")
for nombre, codigo in EJEMPLOS:
    viol, v = analizar(codigo)
    print(f"  {nombre}: {len(viol)} violaciones de estilo, complejidad V = {v}{' (SUPERA 10)' if v > 10 else ''}")
    for n, m in viol: print(f"      línea {n}: {m}")

# ---- Parte 2: versionado semántico y orden de versiones -----------------------------------------------------------------------------------------
def analizar_version(v):
    m = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.]+))?", v)
    mayor, menor, parche, pre = int(m[1]), int(m[2]), int(m[3]), m[4]
    return (mayor, menor, parche, 1 if pre is None else 0, pre or "")        # una versión previa va antes que la definitiva
def siguiente(v, commits):
    mayor, menor, parche = analizar_version(v)[:3]
    if any("BREAKING" in c or re.match(r"\w+!:", c) for c in commits): return f"{mayor + 1}.0.0"
    if any(c.startswith("feat") for c in commits): return f"{mayor}.{menor + 1}.0"
    if any(c.startswith("fix") for c in commits): return f"{mayor}.{menor}.{parche + 1}"
    return v
print()
print("Parte 2: versionado semántico (MAYOR.MENOR.PARCHE)")
imagenes = ["1.5.0-rc1", "1.4.10", "2.0.0", "1.4.2", "1.5.0"]
print("  orden alfabético (incorrecto):", sorted(imagenes))
print("  orden por versión (correcto): ", sorted(imagenes, key=analizar_version))
commits = ["fix: corrige el desborde del contador de 49 días", "feat: agrega modo de alarma con medida cada 2 s", "fix: CRC del registro", "docs: actualiza el ADR-004"]
print(f"  desde 1.4.2 con {len(commits)} cambios (2 fix, 1 feat, 1 docs): la siguiente versión es {siguiente('1.4.2', commits)}")
print(f"  si además hay un 'feat!: cambia el formato de la trama': {siguiente('1.4.2', commits + ['feat!: cambia el formato de la trama'])}")
print(f"  solo correcciones: {siguiente('1.4.2', [commits[0], commits[2]])}")
actual = analizar_version("1.5.0")
for cand in ("1.4.2", "1.5.0", "1.5.1", "2.0.0"):
    c = analizar_version(cand)
    print(f"  el cargador con la versión 1.5.0 instalada y el contador de seguridad al día: {cand} -> {'acepta' if c > actual else 'rechaza (no es más nueva: evita el retroceso)'}")

# ---- Parte 3: huella de una compilación y exposición a una vulnerabilidad -----------------------------------------------------------------------------
def huella(m): return hashlib.sha256(json.dumps(m, sort_keys=True).encode()).hexdigest()[:12]
manifiesto = {"commit": "a1b2c3d", "compilador": "arm-gcc 12.2", "opciones": "-Os -Wall -Werror", "bibliotecas": {"crc": "1.2.0", "pila_radio": "3.4.1"}}
copia = json.loads(json.dumps(manifiesto))
otro = json.loads(json.dumps(manifiesto)); otro["opciones"] = "-O2 -Wall -Werror"
print()
print("Parte 3: la huella de la configuración de una compilación")
print(f"  manifiesto igual -> huella {huella(manifiesto)} y {huella(copia)} (iguales: {huella(manifiesto) == huella(copia)})")
print(f"  cambiando -Os por -O2 -> huella {huella(otro)} (distinta)")
FLOTA = {"1.4.2": ("3.4.1", 12_000), "1.5.0": ("3.4.2", 5_000), "2.0.0": ("3.5.0", 800)}
expuestos = {v: n for v, (lib, n) in FLOTA.items() if analizar_version(lib) < analizar_version("3.4.2")}
total = sum(n for _, n in FLOTA.values())
print(f"  aviso: la pila de radio anterior a 3.4.2 tiene una vulnerabilidad; versiones de equipos expuestas: {', '.join(expuestos)} ({sum(expuestos.values()):,} de {total:,} equipos; {sum(expuestos.values()) / total * 100:.1f} %)".replace(",", " "))

# ---- Parte 4: ciclo de vida: compra final de un componente que deja de fabricarse ------------------------------------------------------------------
PROD, ANIOS_PROD, SOPORTE, TASA, ULTIMA, MARGEN, PRECIO = 20_000, 5, 10, 0.03, 3, 0.10, 0.90   # SUPUESTOS: unidades por año, años de producción, años de soporte, repuestos, año de la última compra
produccion = sum(PROD for y in range(ULTIMA + 1, ANIOS_PROD + 1))
repuestos = sum(round(TASA * PROD * min(y - 1, ANIOS_PROD)) for y in range(ULTIMA + 1, SOPORTE + 1))
demanda = produccion + repuestos
print()
print("Parte 4: un componente anuncia su fin de fabricación; la última compra es al final del año", ULTIMA)
print(f"  producción de los años {ULTIMA + 1} a {ANIOS_PROD}: {produccion:,} unidades; repuestos de los años {ULTIMA + 1} a {SOPORTE} ({TASA * 100:.0f} % de la base instalada por año): {repuestos:,}".replace(",", " "))
print(f"  demanda total {demanda:,}; con margen del {MARGEN * 100:.0f} %: {round(demanda * (1 + MARGEN)):,} unidades = {demanda * (1 + MARGEN) * PRECIO:,.0f} USD a {PRECIO} USD cada una".replace(",", " "))

Resultado, parte 1: el fragmento conforme tiene 0 violaciones y V = 1; el de defectos de estilo, 10 violaciones (tipos básicos en las líneas 1, 3 y 6; falta de llaves en las líneas 4 y 6; números mágicos 4095, 125, 1000, 40 y 10) y V = 3; la máquina de estados, 0 violaciones y V = 18 (supera 10); el inseguro, 9 violaciones (memoria dinámica en las líneas 3 y 8, strcpy y sprintf, goto, falta de llaves y números mágicos 16, 32 y 4242) y V = 2. Parte 2: el orden alfabético da 1.4.10, 1.4.2, 1.5.0, 1.5.0-rc1, 2.0.0 y el correcto 1.4.2, 1.4.10, 1.5.0-rc1, 1.5.0, 2.0.0; desde 1.4.2 la versión siguiente es 1.5.0 (con una función nueva), 2.0.0 (con un cambio incompatible) o 1.4.3 (solo correcciones); con 1.5.0 instalada, el cargador rechaza 1.4.2 y 1.5.0 y acepta 1.5.1 y 2.0.0. Parte 3: el mismo manifiesto da la huella ae8f26b0ca40 y, al cambiar -Os por -O2, ef8ad64bc367; con la pila de radio anterior a 3.4.2 vulnerable, quedan expuestos los equipos de la versión 1.4.2: 12 000 de 17 800 (67,4 %). Parte 4: hacen falta 40 000 unidades de producción y 19 200 de repuestos después del año 3: 59 200 en total, 65 120 con el 10 % de margen y 58 608 USD.

Probá agregar al revisor una regla propia (por ejemplo, detectar printf o líneas con TODO), sumar las versiones «1.5.0-rc10» y «1.5.0-rc2» y ver cómo se ordenan (el programa compara el sufijo como texto, una simplificación: el estándar compara por identificadores separados por puntos, y por eso conviene escribir rc.2 y rc.10), cambiar la tasa de repuestos de la Parte 4 al 6 % y ver cuánto sube la compra final, o editar el manifiesto de la Parte 3 y ver cambiar la huella. La lección del programa es que buena parte de la calidad se puede medir y verificar con unas pocas líneas de código, y que esas verificaciones se pueden automatizar en cada cambio.

11 · Exploración interactiva

El revisor de código

El laboratorio es el revisor de la Parte 1: pegás o editás un fragmento de C y aplica reglas de un estándar simplificado (tipos de ancho fijo, llaves, números mágicos, goto, memoria dinámica, funciones inseguras, líneas largas) y mide la complejidad ciclomática. Elegís uno de los cuatro ejemplos o escribís el tuyo. Es un revisor superficial (de expresiones regulares, sin entender el código), pensado para ver con qué poco se detectan problemas reales y qué cosas escapan.

¿Cumple el estándar este código?

Complejidad ciclomática—
Violaciones de estilo—

Detalle

Elegí un ejemplo o escribí código.

Recorrido guiado

  1. El ejemplo 2 («defectos de estilo») muestra 10 violaciones y V = 3: tipos básicos (int) en las líneas 1, 3 y 6, falta de llaves en la 4 y la 6, y los números mágicos 4095, 125, 1000, 40 y 10. Cada una se arregla con una constante con nombre (ADC_MAX, MV_POR_CUENTA) y un tipo de ancho fijo.
  2. El ejemplo 1 («conforme») da 0 violaciones y V = 1: usa tipos de ancho fijo, una constante con nombre y ni memoria dinámica ni funciones inseguras. Compará su aspecto con el anterior.
  3. El ejemplo 3 (la máquina de estados) da 0 violaciones pero V = 18 y la etiqueta «supera 10». El estilo es impecable y el código es difícil de probar: 18 caminos independientes. El revisor mide dos cosas distintas, y la segunda no se arregla con un formateador sino con diseño (la tabla de transiciones de la sección 2).
  4. El ejemplo 4 («inseguro») da 9 violaciones: malloc y free, strcpy y sprintf (desbordes de buffer, tema 46), goto, un if sin llaves y los números mágicos 16, 32 y 4242. Cada una apunta a un fallo del tema 46.
  5. Probá editar el ejemplo 2: cambiá int por int32_t y agregá las llaves; el contador baja violación por violación. Después reemplazá 4095 por una constante definida con #define: el revisor ignora las líneas de definición de constantes, que son justamente el lugar donde el número debe aparecer.
  6. Escribí un fragmento propio con un if (a && b || c) más un for: la complejidad sube a 5 (las dos decisiones lógicas, el if y el for, más uno). Y escribí uno que use printf: el revisor no lo marca, porque no tiene esa regla. Un revisor solo detecta lo que se le enseñó a buscar, y por eso las reglas de un estándar crecen con los errores que el equipo va sufriendo.
Desafío: ¿qué reglas agregarías, y qué problemas no puede detectar ninguna regla de este tipo?

Se pueden agregar reglas sintácticas: funciones de más de 40 líneas, más de cuatro parámetros, variables globales sin static, printf fuera de un módulo de registro, comparaciones de flotantes con ==, switch sin default, y comentarios TODO sin identificador de tarea. Un analizador real, que entiende el código, agrega flujo de datos (variables sin inicializar, punteros nulos, accesos fuera de límites). Lo que ninguna regla puede detectar es lo que depende del significado: que el umbral sea el correcto, que el plazo se cumpla, que la fórmula de calibración sea la de la hoja de datos, que el diseño sea el adecuado. Esos son trabajo de las pruebas (tema 47) y de la revisión humana. Las herramientas liberan a las personas de lo mecánico para que dediquen su atención a eso.

12 · Método de trabajo

Método: cuidar un producto

  1. Elegí un estándar de código (estilo y reglas de seguridad y portabilidad) y automatizalo: formateador, -Wall -Wextra -Werror y análisis estático en la integración continua. Documentá las desviaciones.
  2. Medí la complejidad de cada función y mantenela por debajo de 10; reemplazá cadenas de if y case por tablas.
  3. Revisá todo cambio antes de integrarlo: cambios pequeños, herramientas primero, lista de comprobación del dominio embebido, tono constructivo.
  4. Poné todo en control de versiones (código, scripts, configuración, documentación, pruebas), marcá cada liberación y generá el archivo de versión en cada compilación.
  5. Numerá con versionado semántico, comparando las versiones numéricamente, y llevá por separado las versiones de firmware, hardware, protocolo, datos y arranque con su tabla de compatibilidad.
  6. Documentá el porqué, cerca del código y como texto versionado: contratos en los encabezados, README, arquitectura y ADR, changelog. Generá lo que se pueda y borrá lo que ya no es cierto.
  7. Definí la línea base de cada liberación: manifiesto con commit, compilador, opciones y bibliotecas; compilación reproducible; ELF, mapa, huella y lista de componentes archivados.
  8. Gestioná los defectos y los cambios: registro con datos para reproducir, severidad y prioridad, prueba de regresión con cada corrección y análisis de impacto sobre la trazabilidad.
  9. Mantené la flota: política de versiones soportadas, despliegue gradual, seguimiento de avisos de seguridad de cada componente y un registro de qué versión tiene cada equipo.
  10. Planificá la vida completa: componentes con garantía de suministro, revisión anual de obsolescencia, compra final calculada o rediseño, y un plan de retirada.

13 · Comprensión

Confusiones frecuentes

«Las reglas de estilo son una cuestión de gusto y hacen perder tiempo»

El estilo se automatiza y deja de discutirse; lo que cuenta son las reglas de seguridad y portabilidad (tipos de ancho fijo, funciones con límite, sin memoria dinámica), cada una con un motivo concreto del tema 46. Un estándar verificado por herramientas ahorra revisiones y errores.

«Si el código cumple el estándar, es correcto»

Cumplir un estándar cierra una familia de problemas conocidos, nada más. El ejemplo 3 del laboratorio tiene cero violaciones y una complejidad de 18; ninguna regla dice que sus umbrales sean los correctos. La corrección la dan los requisitos, las pruebas y las revisiones.

«Una revisión de código es solo buscar errores»

También difunde el conocimiento (que más de una persona entienda cada módulo), mantiene la uniformidad y obliga a explicar el cambio. Su valor sobrevive aunque casi no encuentre errores, y es la mejor protección contra el «solo una persona sabe cómo funciona».

«El número de versión es un asunto de marketing»

Es información técnica: de él depende que un cargador acepte o rechace una actualización, que se sepa qué compatibilidad tiene un equipo y que se pueda reproducir su binario. Comparar versiones como texto (1.4.10 menor que 1.4.2) es un error real y frecuente.

«La documentación se escribe al final, cuando todo esté estable»

Al final se olvida el porqué de las decisiones y nunca queda tiempo. La que se mantiene es la que se escribe con el cambio, cerca del código y revisada con él. Y la que no se actualiza hay que borrarla: una documentación vieja con apariencia oficial engaña.

«Con el repositorio ya puedo reproducir cualquier versión»

El código sí, pero el binario depende también del compilador, sus opciones, las bibliotecas de terceros y los scripts. Sin fijarlos (manifiesto, contenedor, versiones exactas), recompilar la versión de hace dos años puede dar un binario distinto.

«El mantenimiento es corregir errores»

Esa es solo una de sus cuatro formas (correctivo). Una parte grande es adaptativa (componentes y normas que cambian), perfectiva (nuevas funciones) y preventiva (reducir la deuda técnica), y en un producto con avisos de seguridad continuos, una parte no depende de que el código tenga errores.

«Si un componente deja de fabricarse, se busca otro y listo»

Cambiar un componente puede implicar rediseñar la placa, revalidar el producto, ajustar el firmware y recertificar. Por eso se calcula la compra final como alternativa, se aísla el componente detrás del HAL y se revisa el ciclo de vida de los componentes críticos todos los años, no el día en que llega el aviso.

14 · Práctica guiada

Ejercicios

1. Complejidad ciclomática

Una función tiene 4 if, un for, 3 case, 2 operadores && y un ||. ¿Cuál es su complejidad ciclomática y qué se recomienda hacer?

Ver solución

Puntos de decisión: 4 + 1 + 3 + 2 + 1 = 11. Complejidad: 11 + 1 = 12, por encima del umbral de 10. Se recomienda dividirla: extraer funciones auxiliares con un solo propósito, o reemplazar el switch y los if encadenados por una tabla de transiciones o de acciones. Con 12 caminos independientes, probarla exige al menos 12 casos, y razonar sobre ella es difícil.

2. Versionado

La versión actual es 2.3.7. Calculá la siguiente si los cambios son (a) solo correcciones, (b) una función nueva compatible y (c) un cambio incompatible en el formato de los datos guardados. Además, ¿cuál es mayor, 2.3.10 o 2.3.9? ¿Y 2.4.0-rc2 frente a 2.4.0?

Ver solución

(a) 2.3.8; (b) 2.4.0 (sube MENOR y vuelve PARCHE a cero); (c) 3.0.0. Numéricamente, 2.3.10 es mayor que 2.3.9 (10 > 9; como texto saldría al revés). Y 2.4.0-rc2 es anterior a 2.4.0: una versión de prelanzamiento precede a la definitiva. Un cargador de arranque que comparara como texto aceptaría una actualización equivocada o rechazaría una válida.

3. Compra final

Un componente deja de fabricarse y la última compra es al final del año 2. Se fabrican 10 000 equipos por año durante 4 años; el soporte llega al año 8; cada año se reemplaza el 2 % de la base instalada al comienzo del año (a partir del año 3). Con un margen del 15 % y un precio de 1,20 USD, ¿cuánto hay que comprar y cuánto cuesta?

Ver solución

Producción de los años 3 y 4: 2 × 10 000 = 20 000. Repuestos: la base instalada al comienzo de cada año es 10 000 × (año − 1) hasta un máximo de 40 000: año 3: 20 000 × 2 % = 400; año 4: 30 000 × 2 % = 600; años 5 a 8: 40 000 × 2 % = 800 cada uno, 3200. Total de repuestos: 400 + 600 + 3200 = 4200. Demanda: 20 000 + 4200 = 24 200. Con el 15 % de margen: 24 200 × 1,15 = 27 830 unidades, por 27 830 × 1,20 = 33 396 USD. Es una inversión que hay que comparar con el costo de rediseñar con otro componente.

4. Exposición de la flota

Una flota tiene 6000 equipos con la biblioteca 2.1, 3000 con la 2.2 y 1000 con la 2.3. Un aviso indica que las versiones anteriores a la 2.3 son vulnerables. ¿Cuántos equipos y qué porcentaje están expuestos? ¿Qué se necesita tener para contestarlo rápido?

Ver solución

Expuestos: 6000 + 3000 = 9000 de 10 000, el 90 %. Para contestar en minutos hacen falta la lista de componentes de cada versión de firmware (SBOM) y el registro de qué versión tiene cada equipo. Con eso, el plan sigue: corrección en una versión de parche, despliegue gradual priorizando la parte expuesta (tema 42) y comunicación a los clientes si corresponde.

5. ¿Máquina o persona?

Clasificá cada hallazgo según lo detecte mejor una herramienta automática o una persona en la revisión: (a) un int donde debía haber uint16_t; (b) un umbral de temperatura que no coincide con el requisito; (c) un strcpy; (d) una ISR que llama a una función bloqueante; (e) una llave que falta.

Ver solución

Herramienta: (a), (c) y (e): son patrones sintácticos que un analizador detecta siempre y sin cansarse. Persona: (b), porque exige conocer el requisito; (d) es intermedio: un analizador con el grafo de llamadas puede detectarlo, pero lo habitual es que lo vea el revisor que conoce qué es una ISR y qué bloquea. La regla práctica: automatizar todo lo mecánico, para que la revisión humana se dedique a lo que requiere criterio.

6. Comentarios que valen la pena

De estos tres comentarios, ¿cuál está de más y por qué? (a) i++; // incrementa i; (b) /* el sensor necesita 15 ms tras el reinicio (hoja de datos, tabla 3) */; (c) /* NO reordenar: la radio debe apagarse antes del reposo */.

Ver solución

El (a) sobra: repite lo que el código ya dice y se queda desactualizado si alguien lo cambia. Los otros dos registran lo que el código no dice: (b) la fuente y la razón de un valor, y (c) una dependencia de orden oculta, cuya violación se paga en consumo. Un buen comentario responde «¿por qué?», no «¿qué?»; cuando hace falta explicar el qué, suele ser una señal de que conviene renombrar la variable o partir la función.

15 · Síntesis

Ideas para recordar

  • La calidad de un firmware es corrección, robustez, mantenibilidad y portabilidad. La mantenibilidad es la más descuidada y se paga más tarde, como deuda técnica. En lo embebido, corregir en el campo es caro, de modo que la calidad se construye antes de entregar con hábitos baratos y automáticos.
  • Un estándar de código tiene estilo (que se automatiza con un formateador) y reglas de seguridad y portabilidad (MISRA C, CERT C: tipos de ancho fijo, llaves, sin memoria dinámica ni funciones inseguras). Una regla que no se verifica automáticamente no existe; las excepciones se documentan. La complejidad ciclomática (decisiones + 1, máximo 10) mide cuántos caminos hay que probar: una máquina de estados de V = 18 se reduce con una tabla.
  • Las revisiones encuentran errores, difunden conocimiento y mejoran el código; funcionan con cambios pequeños, las herramientas primero, una lista de comprobación del dominio y tono constructivo. No reemplazan a las pruebas.
  • Todo vive en control de versiones; cada liberación se marca y su versión se incrusta en el binario. El versionado semántico (MAYOR.MENOR.PARCHE) se compara numéricamente (1.4.2 es anterior a 1.4.10), y las versiones de firmware, hardware, protocolo, datos y arranque se llevan por separado con su tabla de compatibilidad.
  • La documentación registra lo que el código no dice (el porqué, las unidades, las restricciones, las decisiones) cerca del código, como texto versionado y revisado con cada cambio; lo generable se genera y lo falso se borra.
  • La gestión de la configuración define una línea base por liberación (manifiesto con commit, compilador, opciones y bibliotecas; huella; compilación reproducible; ELF y mapa archivados) y mantiene la lista de componentes (SBOM): con ella, un aviso de vulnerabilidad se contesta en minutos (12 000 de 17 800 equipos expuestos, 67,4 %).
  • El mantenimiento es correctivo, adaptativo, perfectivo y preventivo; los defectos se registran con severidad y prioridad y cada corrección entra con su prueba. El ciclo de vida es largo (desarrollo, producción, soporte, fin de vida): la obsolescencia de componentes se afronta con una compra final calculada (65 120 unidades y 58 608 USD en el ejemplo), un diseño que aísle el componente y un plan de retirada.

¿Listo para el tema siguiente?

Antes de avanzar, revisá un proyecto tuyo y respondé: ¿qué reglas de tu estándar se verifican solas en cada cambio?, ¿cuál es la función más compleja y cuánto vale su V?, ¿podrías recompilar hoy el binario que entregaste hace un año y obtener el mismo?, ¿qué componentes críticos tienen fecha de fin de fabricación? Si alguna respuesta es «no sé», ahí está el trabajo de mantenimiento más rentable.

En el próximo tema juntamos todo el curso en un proyecto integrador: un dispositivo completo con sensores, estados, tareas, bajo consumo, watchdog y actualización, del requisito al producto.