Escrevendo Camadas de Abstração de Hardware (HALs) em C
LarLar > blog > Escrevendo Camadas de Abstração de Hardware (HALs) em C

Escrevendo Camadas de Abstração de Hardware (HALs) em C

Nov 22, 2023

Jacó de Benin | 19 de maio de 2023

As camadas de abstração de hardware (HALs) são uma camada importante para todos os aplicativos de software incorporados. Um HAL permite que um desenvolvedor abstraia ou separe os detalhes de hardware do código do aplicativo. Desacoplar o hardware remove a dependência do aplicativo do hardware, o que significa que ele está em uma posição perfeita para ser gravado e testado fora do destino ou, em outras palavras, no host. Os desenvolvedores podem simular, emular e testar o aplicativo com muito mais rapidez, removendo bugs, chegando ao mercado mais rapidamente e diminuindo os custos gerais de desenvolvimento. Vamos explorar como os desenvolvedores embarcados podem projetar e usar HALs escritos em C.

É relativamente comum encontrar módulos de aplicativos embarcados que acessam diretamente o hardware. Embora isso torne a escrita do aplicativo mais simples, também é uma prática de programação insatisfatória porque o aplicativo fica fortemente acoplado ao hardware. Você pode pensar que isso não é grande coisa - afinal, quem realmente precisa executar um aplicativo em mais de um conjunto de hardware ou portar o código? Nesse caso, eu o direcionaria a todos que sofreram escassez de chips recentemente e tiveram que voltar e não apenas redesenhar seu hardware, mas também reescrever todo o seu software. Existe um princípio que muitos em programação orientada a objetos (OOP) conhecem como princípio de inversão de dependência que pode ajudar a resolver esse problema.

O princípio de inversão de dependência afirma que "módulos de alto nível não devem depender de módulos de baixo nível, mas ambos devem depender de abstrações". O princípio de inversão de dependência é frequentemente implementado em linguagens de programação usando interfaces ou classes abstratas. Por exemplo, se eu fosse escrever uma interface de entrada/saída digital (dio) em C++ que suporta uma função de leitura e gravação, poderia ser algo como o seguinte:

classe dio_base {

público:

virtual ~dio_base() = padrão;

// métodos de classe

virtual void write(dioPort_t port, dioPin_t pin, dioState_t state) = 0;

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

}

Para aqueles que estão familiarizados com C++, podem ver que estamos usando funções virtuais para definir a interface, o que exige que forneçamos uma classe derivada que implemente os detalhes. Com esse tipo de classe abstrata, podemos usar polimorfismo dinâmico em nosso aplicativo.

Pelo código, é difícil ver como a dependência foi invertida. Em vez disso, vamos ver um diagrama UML rápido. No diagrama abaixo, um módulo led_io é dependente de uma interface dio por meio de injeção de dependência. Quando o objeto led_io é criado, é fornecido um ponteiro para a implementação de entradas/saídas digitais. A implementação para qualquer microcontrolador dio também deve atender a interface dio que é definida por dio_base.

Observando o diagrama de classes UML acima, você pode estar pensando que, embora isso seja ótimo para projetar um aplicativo em uma linguagem OOP como C++, isso não se aplica a C. No entanto, você pode de fato obter esse tipo de comportamento em C que inverte as dependências. Existe um truque simples que pode ser usado em C usando estruturas.

Primeiro, projete a interface. Você pode fazer isso apenas escrevendo as assinaturas de função que você acredita que a interface deve suportar. Por exemplo, se você decidiu que a interface deve oferecer suporte à inicialização, gravação e leitura da entrada/saída digital, basta listar as funções como as seguintes:

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);

Observe que isso se parece muito com as funções que defini anteriormente em minha classe abstrata C++, apenas sem a palavra-chave virtual e a definição de classe abstrata pura (= 0).

Em seguida, posso empacotar essas funções em uma estrutura typedef. A struct agirá como um tipo personalizado que contém toda a interface dio. O código inicial será algo como o seguinte:

read(port, pin) == dio->HIGH) ? dio->LOW : dio->HIGH);/p>

write(port, pin, state};/p>