Écrire des couches d'abstraction matérielle (HAL) en C
Jacob Beningo | 19 mai 2023
Les couches d’abstraction matérielle (HAL) sont une couche importante pour chaque application logicielle embarquée. Une couche HAL permet à un développeur d’abstraire ou de découpler les détails matériels du code de l’application. Le découplage du matériel supprime la dépendance de l’application au 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 ensuite 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. Voyons comment les développeurs embarqués peuvent concevoir et utiliser des HAL écrites en C.
Il est relativement courant de trouver des modules d’application embarqués qui accèdent directement au matériel. Bien que cela simplifie l’écriture de l’application, c’est aussi une mauvaise pratique de programmation car l’application devient étroitement couplée au matériel. Vous pensez peut-être que ce n’est pas un gros problème - après tout, qui a vraiment besoin d’exécuter une application sur plus d’un ensemble 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 qui 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 beaucoup de gens dans la programmation orientée objet (POO) connaissent comme le 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 devraient pas dépendre de modules de bas niveau, mais les deux devraient 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 à ce qui suit :
Classe dio_base {
public:
virtual ~dio_base() = valeur par défaut;
Méthodes de classe
virtual void write(dioPort_t port, dioPin_t pin, dioState_t state) = 0;
port dioState_t lecture(dioPort_t virtuel, dioPin_t broche) = 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.
D’après le 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 diagramme ci-dessous, un module led_io dépend d’une interface dio par injection de dépendance. Lorsque l’objet led_io est créé, il reçoit un pointeur vers l’implémentation pour les entrées/sorties numériques. L’implémentation pour tout microcontrôleur dio doit également répondre à 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 cela est idéal pour concevoir une application dans un langage POO 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 énumérer les fonctions comme suit :
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);
Notez que cela ressemble beaucoup aux fonctions que j’ai définies précédemment dans ma classe abstraite C++, mais sans le mot-clé virtuel et la définition de classe abstraite pure (= 0).
Ensuite, je peux empaqueter ces fonctions dans une structure typedef. La structure agira comme un type personnalisé qui contient l’ensemble de l’interface dio. Le code initial ressemblera à ce qui suit :
typedef struct {
void init (DioConfig_t const * const Config);
void write (dioPort_t port const, dioPin_t broche const dioState_t état const);
dioState_t lire (dioPort_t port de const, dioPin_t broche de const);
} dio_base;
Le problème avec le code ci-dessus est qu’il ne se 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 de la structure en pointeurs de fonction. La fonction peut être convertie en plaçant un * devant le nom de la fonction, puis en plaçant () autour de lui. Par exemple, la structure devient maintenant la suivante :
typedef struct {
void (*init) (DioConfig_t const * const Config);
void (*write) (dioPort_t port const, dioPin_t broche const dioState_t état const);
dioState_t (*lire) (dioPort_t port de const, dioPin_t broche de const);
} dio_base;
Disons maintenant que vous souhaitez 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 injecteriez la dépendance et supprimeriez la dépendance sur le matériel de bas niveau. Le code C du module d’initialisation LED ressemblerait à ce qui suit :
void led_init(dio_base * const dioPtr, dioPort_t const portInit, dioPin_t const pinInit){
dio = dioPtr;
port = portInit;
pin = pinInit;
}
Interne au module led, un développeur peut utiliser l’interface HAL sans rien connaître au matériel ! Par exemple, vous pouvez écrire dans 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->LOW : dio->HIGH);
dio->write(port, pin, état};
}
Le code LED serait entièrement portable, réutilisable et extrait du matériel. Pas de réelles dépendances sur le matériel, juste sur l’interface. À ce stade, vous avez encore 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 affecteriez ensuite ces fonctions à l’interface à l’aide d’un code C semblable au suivant :
dio_base dio_hal = {
Dio_Init,
Dio_Write,
Dio_Read
}
Le module led serait ensuite initialisé à l’aide de quelque chose comme ceci:
led_init(dio_hal, PORT, NIP 15);
Voilà! 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érielle !
Les couches d’abstraction matérielle sont un composant essentiel que tout 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 POO comme C ++ pour bénéficier des avantages des interfaces et des couches d’abstraction. C a suffisamment de capacités pour y arriver. 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