Escritura de capas de abstracción de hardware (HAL) en C
HogarHogar > Blog > Escritura de capas de abstracción de hardware (HAL) en C

Escritura de capas de abstracción de hardware (HAL) en C

Nov 22, 2023

Jacob de Benín | 19 de mayo de 2023

Las capas de abstracción de hardware (HAL) son una capa importante para todas las aplicaciones de software integradas. Una HAL permite a un desarrollador abstraer o desacoplar los detalles del hardware del código de la aplicación. El desacoplamiento del hardware elimina la dependencia de la aplicación del hardware, lo que significa que está en una posición perfecta para escribirse y probarse fuera del objetivo, o en otras palabras, en el host. Luego, los desarrolladores pueden simular, emular y probar la aplicación mucho más rápido, eliminando errores, llegando al mercado más rápido y disminuyendo los costos generales de desarrollo. Exploremos cómo los desarrolladores integrados pueden diseñar y usar HAL escritos en C.

Es relativamente común encontrar módulos de aplicaciones integrados que acceden directamente al hardware. Si bien esto simplifica la escritura de la aplicación, también es una mala práctica de programación porque la aplicación se acopla estrechamente al hardware. Podría pensar que esto no es gran cosa; después de todo, ¿quién necesita realmente ejecutar una aplicación en más de un conjunto de hardware o portar el código? En ese caso, lo remitiría a todos los que sufrieron escasez de chips recientemente y tuvieron que regresar y no solo rediseñar su hardware, sino también reescribir todo su software. Hay un principio que muchos en la programación orientada a objetos (POO) conocen como el principio de inversión de dependencia que puede ayudar a resolver este problema.

El principio de inversión de dependencia establece que "los módulos de alto nivel no deberían depender de los módulos de bajo nivel, sino que ambos deberían depender de abstracciones". El principio de inversión de dependencia a menudo se implementa en lenguajes de programación utilizando interfaces o clases abstractas. Por ejemplo, si tuviera que escribir una interfaz de entrada/salida digital (dio) en C++ que admita una función de lectura y escritura, podría parecerse a lo siguiente:

clase dio_base {

público:

virtual ~dio_base() = predeterminado;

// métodos de clase

escritura de vacío virtual (puerto dioPort_t, pin dioPin_t, estado dioState_t) = 0;

virtual dioState_t read (puerto dioPort_t, pin dioPin_t) = 0;

}

Para aquellos que estén familiarizados con C++, pueden ver que estamos usando funciones virtuales para definir la interfaz, lo que requiere que proporcionemos una clase derivada que implemente los detalles. Con este tipo de clase abstracta, podemos usar polimorfismo dinámico en nuestra aplicación.

A partir del código, es difícil ver cómo se ha invertido la dependencia. En cambio, veamos un diagrama UML rápido. En el siguiente diagrama, un módulo led_io depende de una interfaz dio a través de la inyección de dependencia. Cuando se crea el objeto led_io, se proporciona un puntero a la implementación de entradas/salidas digitales. La implementación de cualquier microcontrolador dio también debe cumplir con la interfaz dio definida por dio_base.

Mirando el diagrama de clases UML anterior, podría estar pensando que si bien esto es excelente para diseñar una aplicación en un lenguaje OOP como C ++, esto no se aplica a C. Sin embargo, de hecho puede obtener este tipo de comportamiento en C que invierte las dependencias. Hay un truco simple que se puede usar en C usando estructuras.

Primero, diseñe la interfaz. Puede hacer esto simplemente escribiendo las firmas de función que cree que la interfaz debería admitir. Por ejemplo, si ha decidido que la interfaz debería admitir la inicialización, escritura y lectura de la entrada/salida digital, podría enumerar las funciones de la siguiente manera:

void write(dioPort_t const port, dioPin_t const pin, dioState_t const state);

dioState_t read(dioPort_t const port, dioPin_t const pin);

Tenga en cuenta que esto se parece mucho a las funciones que definí anteriormente en mi clase abstracta de C++, solo que sin la palabra clave virtual y la definición de clase abstracta pura (= 0).

A continuación, puedo empaquetar estas funciones en una estructura typedef. La estructura actuará como un tipo personalizado que contiene toda la interfaz dio. El código inicial será algo como lo siguiente:

estructura typedef {

void init (DioConfig_t const * const Config);

escritura nula (dioPort_t const port, dioPin_t const pin, dioState_t const state);

dioState_t lectura (dioPort_t const puerto, dioPin_t const pin);

} dios_base;

El problema con el código anterior es que no compilará. No puede incluir una función en una estructura en C. Sin embargo, ¡puede incluir un puntero de función! El último paso es convertir las funciones dio HAL en la estructura a punteros de función. La función se puede convertir colocando un * delante del nombre de la función y luego poniendo () alrededor. Por ejemplo, la estructura ahora se convierte en la siguiente:

estructura typedef {

vacío (*init) (DioConfig_t const * const Config);

void (*escribir) (dioPort_t const port, dioPin_t const pin, dioState_t const state);

dioState_t (*lectura) (dioPort_t const port, dioPin_t const pin);

} dios_base;

Supongamos ahora que desea utilizar Dio HAL en un módulo led_io. Podría escribir una función de inicio de led que lleve un puntero al tipo dio_base. Al hacerlo, estaría inyectando la dependencia y eliminando la dependencia del hardware de bajo nivel. El código C para el módulo de inicio de LED sería similar al siguiente:

void led_init(dio_base * const dioPtr, dioPort_t const portInit, dioPin_t const pinInit){

dio = dioPtr;

puerto = portInit;

alfiler = alfilerCalor;

}

¡Interno al módulo LED, un desarrollador puede usar la interfaz HAL sin saber nada sobre el hardware! Por ejemplo, podría escribir en el periférico dio en una función led_toggle de la siguiente manera:

vacío led_toggle(vacío){

estado booleano = (dio->leer(puerto, pin) == dio->ALTO) ? dio->BAJO : dio->ALTO);

dio->escribir(puerto, pin, estado};

}

El código led sería completamente portátil, reutilizable y abstraído del hardware. No hay dependencias reales en el hardware, solo en la interfaz. En este punto, aún necesita una implementación para el hardware que también implemente la interfaz para que el código LED sea utilizable. Para que esto suceda, implementaría un módulo dio con funciones que coincidan con la firma de la interfaz. Luego asignaría esas funciones a la interfaz usando un código C similar al siguiente:

dios_base dios_hal = {

dio_iniciar,

dio_escritura,

dio_leer

}

Luego, el módulo LED se inicializaría usando algo como lo siguiente:

led_init(dio_hal, PORTA, PIN15);

¡Eso es todo! Si sigue este proceso, puede desacoplar el código de su aplicación del hardware a través de una serie de capas de abstracción de hardware.

Las capas de abstracción de hardware son un componente fundamental que todo desarrollador de software integrado debe aprovechar para minimizar el acoplamiento con el hardware. Hemos explorado una técnica simple para definir una interfaz e implementarla en C. Resulta que no necesita un lenguaje OOP como C++ para obtener los beneficios de las interfaces y las capas de abstracción. C tiene suficientes capacidades para que esto suceda. Un punto a tener en cuenta es que hay un poco de costo en esta técnica desde el punto de vista del rendimiento y la memoria. Lo más probable es que pierda el rendimiento de una llamada de función y la memoria suficiente para almacenar los punteros de función de sus interfaces. ¡Al final del día, este pequeño costo bien vale la pena!

Más información sobre formatos de texto