Écrire des couches d'abstraction matérielle (HAL) en C

Nouvelles

MaisonMaison / Nouvelles / Écrire des couches d'abstraction matérielle (HAL) en C

May 04, 2023

Écrire des couches d'abstraction matérielle (HAL) en C

Jacob Beningo | 19 mai 2023 Les couches d'abstraction matérielles (HAL) sont un élément important

Jacob du Bénin | 19 mai 2023

Les couches d'abstraction matérielles (HAL) sont une couche importante pour chaque application logicielle embarquée. Un HAL permet à un développeur d'abstraire ou de découpler les détails matériels du code d'application. Le découplage du matériel supprime la dépendance de l'application vis-à-vis du matériel, ce qui signifie qu'elle est dans une position idéale pour être écrite et testée hors cible, ou en d'autres termes, sur l'hôte. Les développeurs peuvent alors simuler, émuler et tester l'application beaucoup plus rapidement, en supprimant les bogues, en accélérant la mise sur le marché et en réduisant les coûts de développement globaux. Explorons comment les développeurs embarqués peuvent concevoir et utiliser des HAL écrits en C.

Il est relativement courant de trouver des modules d'application intégrés qui accèdent directement au matériel. Bien que cela simplifie l'écriture de l'application, il s'agit également d'une mauvaise pratique de programmation car l'application devient étroitement couplée au matériel. Vous pensez peut-être que ce n'est pas grave. Après tout, qui a vraiment besoin d'exécuter une application sur plusieurs ensembles de matériel ou de porter le code ? Dans ce cas, je vous dirigerais vers tous ceux qui ont récemment souffert de pénuries de puces et ont dû revenir en arrière et non seulement reconcevoir leur matériel, mais aussi réécrire tous leurs logiciels. Il existe un principe que de nombreux spécialistes de la programmation orientée objet (POO) connaissent sous le nom de principe d'inversion de dépendance qui peut aider à résoudre ce problème.

Le principe d'inversion de dépendance stipule que "les modules de haut niveau ne doivent pas dépendre des modules de bas niveau, mais les deux doivent dépendre d'abstractions". Le principe d'inversion de dépendance est souvent implémenté dans les langages de programmation utilisant des interfaces ou des classes abstraites. Par exemple, si je devais écrire une interface d'entrée/sortie numérique (dio) en C++ qui prend en charge une fonction de lecture et d'écriture, cela pourrait ressembler à ceci :

classe dio_base {

public:

virtuel ~dio_base() = par défaut ;

// Méthodes de classe

écriture vide virtuelle (port dioPort_t, broche dioPin_t, état dioState_t) = 0 ;

dioState_t virtuel lu (port dioPort_t, broche dioPin_t) = 0 ;

}

Pour ceux d'entre vous qui connaissent C++, vous pouvez voir que nous utilisons des fonctions virtuelles pour définir l'interface, ce qui nous oblige à fournir une classe dérivée qui implémente les détails. Avec ce type de classe abstraite, nous pouvons utiliser le polymorphisme dynamique dans notre application.

À partir du code, il est difficile de voir comment la dépendance a été inversée. Au lieu de cela, regardons un diagramme UML rapide. Dans le schéma ci-dessous, un module led_io est dépendant d'une interface dio via l'injection de dépendance. Lorsque l'objet led_io est créé, un pointeur vers l'implémentation des entrées/sorties numériques lui est fourni. L'implémentation de tout microcontrôleur dio doit également respecter l'interface dio définie par dio_base.

En regardant le diagramme de classes UML ci-dessus, vous pensez peut-être que même si c'est génial pour concevoir une application dans un langage OOP comme C++, cela ne s'applique pas à C. Cependant, vous pouvez en fait obtenir ce type de comportement en C qui inverse les dépendances. Il existe une astuce simple qui peut être utilisée en C en utilisant des structures.

Tout d'abord, concevez l'interface. Vous pouvez le faire en écrivant simplement les signatures de fonction que vous pensez que l'interface devrait prendre en charge. Par exemple, si vous avez décidé que l'interface doit prendre en charge l'initialisation, l'écriture et la lecture de l'entrée/sortie numérique, vous pouvez simplement lister les fonctions comme suit :

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

dioState_t lire (port const dioPort_t, broche const dioPin_t);

Notez que cela ressemble beaucoup aux fonctions que j'ai définies plus tôt dans ma classe abstraite C++, juste sans le mot-clé virtual et la définition de classe abstraite pure (= 0).

Ensuite, je peux regrouper ces fonctions dans une structure typedef. La structure agira comme un type personnalisé qui contient toute l'interface dio. Le code initial ressemblera à ceci :

typedef structure {

void init (DioConfig_t const * const Config);

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

dioState_t lire (port const dioPort_t, broche const dioPin_t);

} god_base ;

Le problème avec le code ci-dessus est qu'il ne compilera pas. Vous ne pouvez pas inclure une fonction dans une structure en C. Cependant, vous pouvez inclure un pointeur de fonction ! La dernière étape consiste à convertir les fonctions dio HAL dans la structure en pointeurs de fonction. La fonction peut être convertie en plaçant un * devant le nom de la fonction, puis en mettant () autour. Par exemple, la structure devient maintenant la suivante :

typedef structure {

void (*init) (DioConfig_t const * const Config);

void (*write) (port const dioPort_t, broche const dioPin_t, état const dioState_t);

dioState_t (*lire) (port const dioPort_t, broche const dioPin_t);

} god_base ;

Disons maintenant que vous voulez utiliser le Dio HAL dans un module led_io. Vous pouvez écrire une fonction d'initialisation led qui prend un pointeur vers le type dio_base. Ce faisant, vous injecterez la dépendance et supprimerez la dépendance vis-à-vis du matériel de bas niveau. Le code C du module led init ressemblerait à ceci :

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

dio = dioPtr ;

port = portInit ;

pin = pinHeat ;

}

Interne au module led, un développeur peut utiliser l'interface HAL sans rien connaître du matériel ! Par exemple, vous pouvez écrire sur le périphérique dio dans une fonction led_toggle comme suit :

void led_toggle(void){

bool state = (dio->read(port, pin) == dio->HIGH) ? dio->BAS : dio->HAUT);

dio->écrire(port, broche, état} ;

}

Le code led serait complètement portable, réutilisable et abstrait du matériel. Aucune dépendance réelle sur le matériel, juste sur l'interface. À ce stade, vous avez toujours besoin d'une implémentation pour le matériel qui implémente également l'interface pour que le code led soit utilisable. Pour ce faire, vous devez implémenter un module dio avec des fonctions qui correspondent à la signature de l'interface. Vous assignerez ensuite ces fonctions à l'interface à l'aide d'un code C similaire à ce qui suit :

god_base god_hal = {

Dio_Init,

Dio_Write,

Dio_Lire

}

Le module led serait alors initialisé en utilisant quelque chose comme ceci :

led_init(dio_hal, PORTA, PIN15);

C'est ça! Si vous suivez ce processus, vous pouvez découpler votre code d'application du matériel via une série de couches d'abstraction matérielles !

Les couches d'abstraction matérielles sont un composant essentiel que chaque développeur de logiciels embarqués doit exploiter pour minimiser le couplage au matériel. Nous avons exploré une technique simple pour définir une interface et l'implémenter en C. Il s'avère que vous n'avez pas besoin d'un langage OOP comme C++ pour bénéficier des avantages des interfaces et des couches d'abstraction. C a suffisamment de capacités pour que cela se produise. Un point à garder à l'esprit est qu'il y a un peu de coût dans cette technique du point de vue des performances et de la mémoire. Vous perdrez probablement un appel de fonction digne de performances et suffisamment de mémoire pour stocker les pointeurs de fonction de vos interfaces. En fin de compte, ce petit coût en vaut la peine !

Plus d'informations sur les formats de texte