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.


No hay comentarios:

Publicar un comentario