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