jueves, 30 de enero de 2020

Capítulo 29: arquitectura embebida límpia - parte 2 - Clean Arquitecture


Una arquitectura embebida limpia es una arquitectura embebida probable
Veremos cómo aplicar alguno de los principios de arquitectura a software embebido y firmware para ayudarte a eliminar el cuello de botella del hardware.
Capas
Las capas viene en muchos sabores. Empezamos con 3 capas. 
En la parte inferior está el Hardware. Como Doug nos advierte, debido a los avances tecnológicos y la ley de Moore el hardware cambiará. Partes se vuelven obsoletas y nuevas partes usan menos energía o mejoran el rendimiento o son más baratas. Cualquier cosa que sea, como un ingeniero de embebidos, yo no quiero tener un trabajo mayor del necesario cuando el cambio inevitable finalmente ocurra.

La separación entre el hardware y el resto del sistema es un hecho, al menos una vez que el hardware está definido. 
Aquí es donde los problemas comienzan cuando estás tratando de pasar el test de App-titude. No hay nada que guarde el conocimiento hardware de extenderse por todo el código. Si no tienes cuidado donde pones las cosas o qué módulo se le permite saber de otro módulo el código será muy difícil de cambiar. No estoy hablando simplemente cuando el hardware cambia si no cuando el usuario pregunta por un cambio o cuando se necesita arreglar un bug.

La mezcla entre el software y el hardware es un antipatrón. El código que exhibe este antipatrón resistirá cambios. Además, los cambios serán peligrosos, a menudo liderando consecuencias involuntarias. Se necesitarán pruebas de regresión del sistema completo para cambios menores. Si no has creado pruebas instrumentadas externas, espera aburrirte  con pruebas manuales y entonces tú puedes esperar nuevos bugs.
El hardware es un detalle
La línea entre el software y el firmware no está típicamente bien definida como lo puede estar la línea entre el código y el hardware.
Uno de los trabajos como desarrollador de software embebido es definir esa línea. El nombre de la línea entre el software y hardware es la capa de abstracción hardware o “Hardware Abstracción Layer” (HAL). Esto no es una nueva idea: esta ha estado en los PCs desde los días de windows.
HAL existe para el software que se asienta encima de éste y su API debería estar cosida a las necesidades del software. Como por ejemplo, el firmware puede almacenar bytes y arrays de bytes en una memoria flash. En contraste, la aplicación necesita almacenar y leer los pares de nombre valor para algún mecanismo de persistencia. El software no debería preocuparse de si los pares de nombre/valor  están almacenados en una memoria flash, en un disco, la nube o núcleo de memoria. El HAL provee un servicio, y este no revela el software como hace esto esto. La implementación flash es un detalle que debería estar oculto al software.

Como otro ejemplo, un LED está vinculado a un bit GPIO. El firmware podría proveer acceso al bit GPIO donde un HAL podría proveer “Led_TurnOn(5)”. Esto es una bonita capa de abstracción hardware de bajo nivel. Consideremos elevar el nivel de abstracción desde una perspectiva hardware a la perspectiva software/producto. Qué está indicando el LED? Supongamos que este indicó batería baja. En algún nivel, el firmware (o un paquete soporta la placa) podría proveer “Led_TurnOn(5)” mientras que HAL provee “Indicate_LowBattery()”. Se puede apreciar la HAL expresando servicios necesarios por la aplicación. También se pueden ver que las capas pueden contener capas. Esto es más que un patrón fractal repetitivo que un conjunto limitado de capas predefinidas. Las asignaciones GPIO son detalles que deben ocultarse al software.
No reveles los detalles del hardware al usuario del HAL
El software de una arquitectura embebida limpia es comprobable desde el hardware de destino. Un HAL exitoso provee esa costura o conjunto de puntos de sustitución que facilitan pruebas fuera del objetivo.
El procesador es un detalle
Cuando tu aplicación integrada usa un cadena de herramientas especializada esta ofrecerá a menudo archivos de encabezado para ayudarte. Estos compiladores a menudo se toman libertades con el lenguaje C, agregando nuevas palabras clave para acceder a sus funciones de procesador. El código se verá como C, pero ya no es C.

A veces compiladores de C suministrador por un vendedor proveen que se vean como variables globales para dar acceso directamente  a registros, puertos IO, relojes, etc. Es útil tener un fácil acceso  a estas cosas pero tenga en cuenta que cualquiera de sus códigos que utilizan estas útiles herramientas ya no es C. No se compilará para otro procesador, o tal vez incluso con un compilador diferente para el mismo procesador.

Le daremos al proveedor el beneficio de la duda asumiendo que es verdad que está tratando de ayudar. Pero ahora es hasta que tu usas esa ayuda en la manera que no duela en el futuro. Tendrá que limitar qué archivos pueden saber sobre las extensiones de C.

Echemos un vistazo a este archivo de encabezado diseñado para la familia de DSP de ACME, ya sabes, los utilizados por Wile E. Coyote:

#ifndef _ACME_STD_TYPES
#define _ACME_STD_TYPES
#if defined(_ACME_X42)
typedef unsigned int Uint_32;
typedef unsigned short Uint_16;
typedef unsigned char Uint_8;
typedef int Int_32;
typedef short Int_16;
typedef char Int_8;
#elif defined(_ACME_A42)
typedef unsigned long Uint_32;
typedef unsigned int Uint_16;
typedef unsigned char Uint_8;
typedef long Int_32;
typedef int Int_16;
typedef char Int_8;
#else
#error <acmetypes.h> is not supported for this environment
#endif
#endif

El fichero cabecera “acmetypes.h” no se debería usar directamente. Si se hace, tu código se vincula a una de los ACME DSPs. Tú estás usando un ACME DSP, tú dices, así que ¿cuál es el problema? Tú no puedes compilar el código a menos que incluyas esta cabecera. Si tú usas la cabecera y defines “_ACME_X42” o “_ACME_A42” tus integers tendrán un tamaño incorrecto si tú tratas de probar tu código fuera del objetivo. Si esto no es suficientemente malo, un día querrás portar tu aplicación  a otro procesador y tendrás que hacer esa tarea mucho más complicada por no elegir portabilidad y no limitar qué archivos saben sobre ACME.

En vez de usar “acmetypes.h” deberías intentar seguir un camino más estandarizado y usar “stdin.h”. Pero ¿qué ocurre si el compilador objetivo no utiliza “stdint.h”? Puedes escribir este archivo cabecera. El “stdint.h” que tú escribes para las compilaciones de destino usa “acmetypes.h” para compilaciones destino como esta:

#ifndef _STDINT_H_
#define _STDINT_H_
#include <acmetypes.h>
typedef Uint_32 uint32_t;
typedef Uint_16 uint16_t;
typedef Uint_8 uint8_t;
typedef Int_32 int32_t;
typedef Int_16 int16_t;
typedef Int_8 int8_t;
#endif

Haber usado “stdint.h” en tu software embebido y firmware ayuda a mantener el código limpio y portable. Ciertamente, todo el software debería ser procesado independientemente, pero no todo el firmware puede serlo. El siguiente fragmento de código toma ventaja de una extensión especial de C que da al código acceso a los periféricos en el micro-controlador. Probablemente tu producto utilice ese micro-controlador así que tú puedes usar sus periféricos integrados. Esta función saca una línea que dice “hi” al puerto serie de salida.

void say_hi()
{
IE = 0b11000000;
SBUF0 = (0x68);
while(TI_0 == 0);
TI_0 = 0;
SBUF0 = (0x69);
while(TI_0 == 0);
TI_0 = 0;
SBUF0 = (0x0a);
while(TI_0 == 0);
TI_0 = 0;
SBUF0 = (0x0d);
while(TI_0 == 0);
TI_0 = 0;
IE = 0b11010000;
}

Hay montones de problemas con esta pequeña función. Algo que puede alertarte es la presencia de ‘ob11000000. Esta notación binaria es chula pero ¿puede hacer C esto? Desafortunadamente, no. Hay varios problemas relativos a este código directamente usando las extensiones personalizadas de C:

IE: interrumpe bits habilitados.
SBUF0: buffer de salida serie.

Las variables en mayúscula acceden a micro-controladores en periféricos. Si tú quieres controlar las interrupciones y los caracteres de salida debes utilizar esos periférico. Sí, esto es conveniente pero no es C.

Una arquitectura embebida limpia usaría estos registros de acceso a dispositivos directamente en unos poco lugares y confinaría estos totalmente al firmware. Cualquier cosa que conozca sobre estos registros se convierte en firmware y está consecuentemente vinculado al sílicio. Vincular el código al procesador hará que duela cuando quieras ejecutar código antes de tener hardware estable. También dolerá cuando muevas tu aplicación embebida a un nuevo procesador.

Si tú usas micro-controladores como este, tu firmware podría aislar estas funciones de bajo nivel con alguna forma de capa de abstracción de procesador o «processor abstraction layer» (PAL). Firmware sobre el PAL podría ser comprobado fuera del objetivo lo que hace este un poco menos firme.

El sistema operativo es un detalle
El HAL es necesario pero ¿es suficiente? En sistemas embebidos de metal puro, el HAL puede ser todo lo que necesitas para mantener el código de ser muy adicto al entorno operativo. Pero qué hay sobre los sistemas embebidos que usan sistemas operando en tiempo real (RTOS) o alguna versión embebida de linux o windows.

Para dar a tu código embebido una buena oportunidad y una larga vida, tienes que tratar el sistema operativo como un detalle y protegerlo contra las dependencias del SO.

El software accede a los servicios del entorno operativo a través del SO. El SO es una capa que separa el software del hardware. Usar directamente un SO puede causar problemas. Por ejemplo, ¿qué ocurre si tu proveedor de RTOS lo ha comprado otra compañía y los royalties se disparan o la calidad baja? ¿Qué ocurre si tus necesidades cambian o tu  RTOS no tienen las capacidades que ahora necesitas? Tú tendrás que hacer un montón de cambios en el código. Estos no serán simples cambios sintácticos debido a la nueva API del OS pero probablemente tendrá que adaptarse semánticamente a las nuevas capacidad y primitivas del nuevo SO.

Una arquitectura embebida limpia aísla el software de los sistemas operativos, a través de la capa de abstracción del sistema operativo (OSAL). En algunos casos, implementar esta capa podría ser tan simple como cambiar el nombre de una función mientra que en otros podría conllevar envolver varias funciones juntas.

Si tu has movido tu software desde un RTOS a otro, sabes que esto es doloroso. Si tu software depende de un OSAL en vez de un OS directamente, estarías escribiendo un nuevo OSAL que es compatible con el viejo OSAL. ¿Qué preferirías hacer: modificar un montón de código complejo existente o escribir una nueva interfaz y comportamiento definidos? Esta pregunta no tiene truco. Yo elijo la última.

Tú podrías empezar a preocuparte por la hinchazón de código ahora. Sin embargo, en realidad, la capa se convierta en el lugar donde se aísla  gran parte de la duplicación en torno al uso de un sistema operativo. Esta duplicación no tiene que imponer una gran sobrecarga. Si define un OSAL, también puede alentar a sus aplicaciones a que tengan una estructura común. Puede proporcionar mecanismos de paso de mensajes, en lugar de hacer que cada hilo trabaje su modelo de concurrencia.

El OSAL puede ayudar a proporcionar puntos de prueba para que el valioso código de aplicación en la capa de software se pueda probar fuera del objetivo y fuera del sistema operativo. El software de una arquitectura embebida limpia es comprobable fuera del sistema operativo de destino. Un OSAL exitoso proporciona esa costura o conjunto de puntos de sustitución que facilitan las pruebas fuera del objetivo.
Programando interfaces y sustituibilidad
Además de añadir un HAL y potencialmente un OSAL dentro de cada una de las capas mayores (software, OS, firmware y hardware) tú puedes, y deberías, aplicar estos principios descritos a lo largo del libro. Estos principios animan a la separación de aspecto, programar interfaces y sustituibilidad. 

La idea de una arquitectura por capas es construir sobre la de idea de programar interfaces. Cuando un módulo interactúa con otro a través de interfaces puedes sustituir un proveedor de servicio por otro. Muchos lectores habrán escrito su propia versión de “printf” para desplegar en el objetivo. Tan pronto como la interfaz de tu “printf” es el mismo que la versión estándar de “printf” puedes anular un servicio por otro.

Una regla básica es usar archivos de encabezado como definiciones de interfaz. Cuando lo haces así, sin embargo, tienes que tener el cuidado  sobre qué va a ir en el archivo de cabecera. Limite el contenido del archivo de encabezado a las declaraciones de funciones, así como a las constantes y los nombres de estructura que necesita la función.

No desordene los archivos de encabezado de la interfaz con estructuras de datos, constantes y definiciones de tipos que solo necesita la implementación. No es solo una cuestión de desorden: ese desorden conducirá a dependencias no deseadas. Limite la visibilidad de los detalles de implementación. Espere que los detalles de implementación cambien. Cuantos menos lugares donde el código conozca los detalles, menos lugares donde el código tendrá que ser rastreado y modificado.

Una arquitectura embebida limpia es comprobable dentro de las capas porque los módulos interactúan a través de interfaces. Cada interfaz proporciona ese punto de costura o sustitución que facilita las pruebas fuera del objetivo.
DIRECTIVAS CONDICIONALES SECAS DE COMPILACIÓN
Un uso de la sustituibilidad que a menudo se pasa por alto se relaciona con la forma en que los programas integrados C y C ++ manejan diferentes objetivos o sistemas operativos. Existe una tendencia a utilizar la compilación condicional para activar y desactivar segmentos de código. Recuerdo un caso especialmente problemático donde la declaración #ifdef BOARD_V2 fue mencionada miles de veces en una aplicación de telecomunicaciones.

Esta repetición de código viola el principio No te repitas (DRY) .5 Si veo #ifdef BOARD_V2 una vez, no es realmente un problema. Seis mil veces es un problema extremo. La compilación condicional que identifica el tipo de hardware de destino a menudo se repite en los sistemas integrados. Pero, ¿Qué más podemos hacer?

¿Qué pasa si hay una capa de abstracción de hardware? El tipo de hardware se convertiría en un detalle oculto debajo del HAL. Si el HAL proporciona un conjunto de interfaces, en lugar de usar la compilación condicional, podríamos usar el enlazador o alguna forma de enlace de tiempo de ejecución para conectar el software al hardware.
Conclusión
Las personas que desarrollan software embebido tienen mucho que aprender de experiencias externas al software embebido. Si usted es un desarrollador integrado que ha elegido este libro, encontrará una gran cantidad de sabiduría para el desarrollo de software en las palabras e ideas.

Permitir que todo el código se convierta en firmware no es bueno para la salud a largo plazo de su producto. Poder realizar pruebas solo en el hardware de destino no es bueno para la salud a largo plazo de su producto. Una arquitectura embebida limpia es buena para la salud a largo plazo de su producto.


miércoles, 29 de enero de 2020

Capítulo 29: arquitectura embebida límpia - parte 1 - Clean Arquitecture


Hace tiempo leí un artículo titulado “The Growing Importance of Sustaining Software for de Dod” de Doug Schmidt’s blog. Doug hice la siguiente afirmación:

“Aunque el software no se desgasta, el firmware y el hardware se convierte en obsoleto y por lo tanto requiere de modificaciones software.”

Este fue un momento un momento esclarecedor para mi. Doug mencionó dos términos que yo habría pensado que eran obvios pero no. Software es esa cosa que puede tener una vida útil pero firmware se convertirá en obsoleto a medida que evolucione el hardware. Si has gastado algo de tiempo en desarrollo de sistemas embebidos, sabes que el hardware está continuamente evolucionando y mejorando. Al mismo tiempo, las características que se añadieron al nuevo software y este crece continuamente en complejidad.

Me gustaría añadir a la frase de Doug:

“Aunque el software no se desgasta, este se puede destruir desde el interior por dependencias no administradas del firmware y el hardware.”

No es raro que al software embebido se le niegue una vida potencialmente larga debido a estar infectado con dependencias del hardware.

Me gusta la definición de Doug del firmware, pero hay que ver otras definiciones:


La definición de Doug me hace darme cuenta que estas definiciones de firmware están mal o al menos están obsoletas. Firmware no significa que el código vive en un ROM. No es firmware por donde esté almacenado, más que eso, es firmware porque de lo que depende y de cómo de difícil es de cambiar a medida de cómo evoluciona el hardware. El hardware evoluciona así que estructuraríamos nuestro código embebido con esa realidad en mente.

No tengo nada contra el firmware o lo ingenieros firmware pero lo que se necesita es menos firmware y más software. Actualmente estoy en desacuerdo de que los ingenieros de firmware escriban tanto firmware.

Los ingenieros no firmware también escriben firmware! Los desarrolladores no firmware escriben esencialmente firmware cuando entierran una consulta SQL en el código o cuando difunden dependencias de la plataforma por todo su código. Los desarrolladores de Android escriben firmware cuando ellos no separan su lógica de negocio de la API de Android.

He estado involucrado en mucho esfuerzo donde la línea entre el código del producto (el software) y el código que interactúa con el hardware del producto (el firmware) es confuso hasta el punto de no existir. Por ejemplo, al final de los 90 tuve la diversión de ayudar a rediseñar un subsistema de comunicaciones que estaba en transición de time division multiplexing (TDM) a voice over IP (VOIP). VOIP es como se hacen las cosas ahora pero TDM fue considera estado del arte desde los 50 hasta los 60 y se desarrolló ampliamente en los 80 y 90.

En cualquier momento que teníamos una pregunta para el ingenieros de sistemas sobre como una llamada debería reaccionar a una situación dada, él desaparecería y un poco más tarde vendría con una respuesta detallada. ¿De dónde sacó la respuesta? La respuesta del ingeniero fue: “del código.” O sea, que el código enredado ¡fue la especificación para el nuevo producto! La implementación existente no tenía separación entre TDM y la lógica de negocio de hacer llamadas. Todo el producto fue hardware/tecnológicamente dependiente de arriba a abajo y no se podía desenredar. Todo el producto se había convertido esencialmente en firmware.

Considera otro ejemplo: Los mensajes comando que llegan a este sistema vía puerto serie. Como era de esperar, hay un procesador de mensajes. El procesador de mensajes conoce el formato de los mensajes, es capaz de decodificarlos y, entonces, puede enviar el mensaje al código que puede tratar la respuesta. Nada de esto es sorprendente, excepto que el procesador de mensajes reside en el mismo fichero que el código que interactúa con el hardware UART. EL procesador de mensajes estaba contaminado con detalles de UART. El procesador de mensajes podría haber sido software con una potencialmente larga vida útil pero en vez de eso es firmware. Al procesador de mensajes se le denegó la oportunidad de convertirse en software y esto no es lo correcto.

Yo he conocido y entendido la necesidad de separar el software del hardware desde hace mucho tiempo pero las palabras de Doug me aclararon cómo usar los términos “software” y “firmware” en relación uno con el otro.

Para ingenieros y programadores, el mensaje es claro; para de escribir tanto firmware y dar la oportunidad al código de tener vida útil larga. Por supuesto, exigirlo no lo hará. Veamos como se puede mantener limpia una arquitectura de software incrustado para dar al software oportunidad de tener una vida larga y útil. 
Prueba APP-TITUDE
¿Por qué tanto potencial software embebido se convierte en firmware? Esto parece que la mayoría del énfasis es que el código embebido funcione y no tanto en cómo estructurarlo para que tenga una vida útil. Kent Beck describe tres actividades en la construcción software (el texto entre comillas son las palabras de Kent y entre comillas las mías):

  1. “Primero haz que funcione”. Estás fuera del negocio si no funciona.
  2. “Después hazlo bien”. Refactorizar el código de manera que tú y otros puedan entenderlo y evolucionarlo  a medida que las necesidades cambian y están mejor entendidas.
  3. “Entonces haz esto rápido.” Refactorizar para las necesidades de rendimiento.

Muchos de los sistemas software embebidos que veo parecen haber sido escritos con “Haz que funcione” en mente, y quizá también por la obsesión por el “hazlo rápido” logrado por añadir micro-optimizaciones en cada oportunidad. En el libro “The Mythical Man-Month”, Fred Brooks nos sugiere “Cómo planear uno”. Kent y Fred están dando, virtualmente, el mismo consejo: aprende cómo funciona, entonces haz una mejor solución.

El software embebido no es especial cuando se trata de estos problemas. La mayoría de las aplicaciones no embebidas se construyen simplemente para trabajar, con poca consideración de hacer un código correcto para una larga vida útil.

Hacer que una aplicación funcione es lo que yo llamo la prueba “App-titude” para un programador. Los programadores que sólo se preocupan por hacer que sus aplicaciones funcionen están perjudicando a sus productos y empleadores. La programación es mucho más que sólo hacer que una aplicación funcione.

Como un ejemplo de código producido mientras se pasa la prueba “App-titude”, verifica estas funciones localizadas en un archivo de un pequeño sistema embebido:

ISR(TIMER1_vect) { ... }
ISR(INT2_vect) { ... }
void btn_Handler(void) { ... }
float calc_RPM(void) { ... }
static char Read_RawData(void) { ... }
void Do_Average(void) { ... }
void Get_Next_Measurement(void) { ... }
void Zero_Sensor_1(void) { ... }
void Zero_Sensor_2(void) { ... }
void Dev_Control(char Activation) { ... }
char Load_FLASH_Setup(void) { ... }
void Save_FLASH_Setup(void) { ... }
void Store_DataSet(void) { ... }
float bytes2float(char bytes[4]) { ... }
void Recall_DataSet(void) { ... }
void Sensor_init(void) { ... }
void uC_Sleep(void) { ... }

Esta lista de funciones está en el orden que se encuentra en el archivo fuente. Ahora, voy  a separarlas agrupadas por conceptos.

Funciones que tienen lógica de dominio.
float calc_RPM(void) { ... }
void Do_Average(void) { ... }
void Get_Next_Measurement(void) { ... }
void Zero_Sensor_1(void) { ... }
void Zero_Sensor_2(void) { ... }

Funciones que tienen configuración de la plataforma de software.
ISR(TIMER1_vect) { ... }*
ISR(INT2_vect) { ... }
void uC_Sleep(void) { ... }
Functions that react to the on off button press
void btn_Handler(void) { ... }
void Dev_Control(char Activation) { ... }
A Function that can get A/D input readings from the hardware
static char Read_RawData(void) { ... }

Funciones que almacenan valores para el almacenamiento persistente.
char Load_FLASH_Setup(void) { ... }
void Save_FLASH_Setup(void) { ... }
void Store_DataSet(void) { ... }
float bytes2float(char bytes[4]) { ... }
void Recall_DataSet(void) { ... }

Funciones que no hacen lo que su nombre implica.
void Sensor_init(void) { ... }

Mirando en otros ficheros de esta aplicación encontré muchos impedimentos para entender el código. También encontré una estructura de fichero que implicó que la única manera de comprobar cualquier cosa de este código es en el objetivo embebido. Virtualmente cada bit de este código sabe que está en una arquitectura especial de microprocesador, usando construcciones de C extendidas “extended” que vinculan el código a una cadena de herramientas y microprocesador en particular.  No hay manera para este código tener una vída larga útil a menos que el producto nunca necesite moverse a un entorno de hardware diferente.

Esta aplicación trabaja: el ingeniero pasó los test de App-titude pero la aplicación no puede decir que tenga una arquitectura limpia embebida.
El cuello de botella del hardware
Hay muchos aspectos especiales que los desarrolladores embebidos tienen que tratar que los desarrolladores de no embebidos no. Por ejemplo, el espacio de memoria limitada, las restricciones de tiempo o las líneas límite, la limitación IO, interfaces de usuario no convencionales y sensores y conexiones al mundo real. La mayoría del tiempo el  hardware está desarrollado de forma concurrente con el hardware y el firmware. Como un ingeniero desarrollando código para este tipo de sistemas, tú puedes no tener un lugar donde ejecutar ese código. Si esto no es suficientemente malo, una vez que obtienes el hardware, hay probabilidades que ese hardware tenga sus propios defectos, haciendo el  progreso de desarrollo de software incluso más lento de lo habitual.

Sí, embebido es especial. Los ingenieros embebidos son especiales pero el desarrollo de embebido no es especial de manera que los principio de este libro se puedan aplicar a los sistemas embebidos.

Uno de los problemas especiales es el cuello de botella del hardware. Cuando el código embebido se estructura sin aplicar los principios y prácticas de arquitectura limpia, tendrás que afrontar el escenario en el cual tú puedes probar tu código sólo en el objetivo. Si el objetivo es el único lugar donde son posibles las pruebas, el cuello de botella hardware lo ralentizará.