May 04, 2023
Scrittura di livelli di astrazione hardware (HAL) in C
Jacob Beningo | May 19, 2023 Hardware abstraction layers (HALs) are an important
Giacobbe del Benin | 19 maggio 2023
I livelli di astrazione hardware (HAL) sono un livello importante per ogni applicazione software incorporata. Un HAL consente allo sviluppatore di astrarre o disaccoppiare i dettagli hardware dal codice dell'applicazione. Il disaccoppiamento dell'hardware rimuove la dipendenza dell'applicazione dall'hardware, il che significa che è in una posizione perfetta per essere scritta e testata fuori destinazione o, in altre parole, sull'host. Gli sviluppatori possono quindi simulare, emulare e testare l'applicazione molto più velocemente, rimuovendo i bug, arrivando sul mercato più velocemente e diminuendo i costi di sviluppo complessivi. Esploriamo come gli sviluppatori embedded possono progettare e utilizzare HAL scritti in C.
È relativamente comune trovare moduli applicativi incorporati che accedono direttamente all'hardware. Sebbene ciò semplifichi la scrittura dell'applicazione, è anche una pratica di programmazione inadeguata perché l'applicazione diventa strettamente accoppiata all'hardware. Potresti pensare che questo non sia un grosso problema: dopo tutto, chi ha davvero bisogno di eseguire un'applicazione su più di un set di hardware o di trasferire il codice? In tal caso, mi rivolgo a tutti coloro che recentemente hanno sofferto di carenza di chip e sono dovuti tornare indietro e non solo riprogettare l'hardware, ma anche riscrivere tutto il software. Esiste un principio che molti nella programmazione orientata agli oggetti (OOP) conoscono come principio di inversione delle dipendenze che può aiutare a risolvere questo problema.
Il principio di inversione delle dipendenze afferma che "i moduli di alto livello non dovrebbero dipendere da moduli di basso livello, ma entrambi dovrebbero dipendere da astrazioni". Il principio dell'inversione delle dipendenze è spesso implementato nei linguaggi di programmazione utilizzando interfacce o classi astratte. Ad esempio, se dovessi scrivere un'interfaccia di input/output digitale (dio) in C++ che supporti una funzione di lettura e scrittura, potrebbe essere simile alla seguente:
classe dio_base {
pubblico:
virtuale ~dio_base() = predefinito;
// Metodi di classe
scrittura virtuale void (porta dioPort_t, pin dioPin_t, stato dioState_t) = 0;
virtual dioState_t read(dioPort_t port, dioPin_t pin) = 0;
}
Chi di voi ha familiarità con C++, può vedere che stiamo utilizzando funzioni virtuali per definire l'interfaccia, il che ci richiede di fornire una classe derivata che ne implementi i dettagli. Con questo tipo di classe astratta possiamo utilizzare il polimorfismo dinamico nella nostra applicazione.
Dal codice è difficile vedere come sia stata invertita la dipendenza. Consideriamo invece un rapido diagramma UML. Nello schema seguente, un modulo led_io dipende da un'interfaccia dio tramite l'iniezione di dipendenza. Quando viene creato l'oggetto led_io, viene fornito un puntatore all'implementazione per ingressi/uscite digitali. L'implementazione per qualsiasi microcontrollore dio deve anche soddisfare l'interfaccia dio definita da dio_base.
Osservando il diagramma delle classi UML riportato sopra, potresti pensare che, sebbene sia ottimo per progettare un'applicazione in un linguaggio OOP come C++, questo non si applica a C. Tuttavia, puoi in effetti ottenere questo tipo di comportamento in C che inverte le dipendenze. C'è un semplice trucco che può essere usato in C usando le strutture.
Innanzitutto, progetta l'interfaccia. Puoi farlo semplicemente scrivendo le firme delle funzioni che ritieni che l'interfaccia dovrebbe supportare. Ad esempio, se hai deciso che l'interfaccia debba supportare l'inizializzazione, la scrittura e la lettura dell'ingresso/uscita digitale, potresti semplicemente elencare le funzioni in modo che siano simili alle seguenti:
void write(dioPort_t const porta, dioPin_t const pin, dioState_t const stato);
dioState_t read(dioPort_t porta const, dioPin_t pin const);
Si noti che assomiglia molto alle funzioni che ho definito in precedenza nella mia classe astratta C++, solo senza la parola chiave virtual e la definizione di classe astratta pura (= 0).
Successivamente, posso impacchettare queste funzioni in una struttura typedef. La struttura agirà come un tipo personalizzato che contiene l'intera interfaccia dio. Il codice iniziale sarà simile al seguente:
read(port, pin) == dio->HIGH) ? dio->LOW : dio->HIGH);/p> write(port, pin, state};/p>