2 Level HAL (Hardware Abstraction Layer) Design

I’ve been working on different brands of MCU for over 20 years and have to reinvent how to use UART, I2C, SPI hardware again and again. 8 years ago I said “enough reinventing” and started to write my own set of libraries that I would port on any MCU I would use..  It evolved from very simple, cooperative tasker, simple drivers to premptive operating system, synchronization primitives to complex drivers. Name and naming conventions also have been changing while the libraries evolve. Nowadays I call it Art. It may even change until I ship it.

When I first started Art, my first aim was to create a simple HAL layer.  On any MCU, developer uses UART, SPI, GPIO, timers most of the time.. If I define what an UART is and write the components that suits the definition, that’s it, I could reuse what I wrote for project A in project B. And that worked really well..

However, that task wasn’t that easy. The different needs for different uses contradict each other.. For instance, serial port uses rx and tx buffer; Modbus uses single buffer to hold received and transmitted as Modbus is unidirectional at a time.. One can use two buffers but that will use unnecessary RAM.

With GPIO pins, you can drive a led, or you can wait for a change on the pin. You can connect many SPI slaves to SPI port, and also that SPI might be slave of other SPI master. The same applies to I2C.

The abstraction of the port should allow those configurations, also there should be a naming convention. With the evolution of the Art, my final design is the following:

  1. Each HAL implementation class has Port suffix: UartPort, SpiPort, I2cPort, PinPort. This is platform specific. Art should supply its implementation as UartPort* uart0(); UartPort* uart1(); SpiPort* spi0(); I2cPort* i2c0(); etc..
  2. Each actual implementation uses the ports as property: Uart port has setUartPort(UartPort*) and UartPort* uartPort(). Similarly SpiMaster, SpiSlave has setSpiPort(SpiPort*) and SpiPort* spiPort() etc.. I2C has similar pattern. This implementation is portable. There is no need to rewrite.
  3. The classes given in 2, are the actual implementations that one should use.

That way, with two level of abstraction, the platform specific implementation becomes very minimal. The HAL component of xxxPort only does very specific job. For instance SpiPort::write(void* buffer, Word length) writes given length of Spi Words to the hardware port. If the length is short enough it uses registers directly. If the data is longer than certain amount it uses DMA. If the hardware has FIFO, it pushes the data into FIFO and if all the data is pushed into the FIFO it does not block the CPU as the hardware may continue the job by itself.. All the management code is moved into SpiMaster.

SpiSlave is a little different story. If you code it blocking, the complation of the execution depens on the behaviour of the master. That will effect all the other codes in the MCU as execution of them will be deferred. To overcome this one can use a dedicated Thread however, that will use resources and also requires synchronization which will add unnecessary complexity to the project.

My approach is using asynchronous writes and reads. That way the you gave the data to SpiSlave and tell it which function to call when the job is done. The thread may handle other jobs at mean time.. Although I talked about asynchronous reads and writes for SpiSlave it shares the same methods with SpiMaster. You can do the same in that class as well but as the control belongs to master and Spi is fast, you usually do not need asynchronicity..

A few sample use case would be the following:

Gpio

Make pin 0 of port A output and clear the pin for STM32F series:

pa0()->configure(PinFunctionOutput0);

Similary, one may hold a pointer to pin and use it:

PinPort* led = pa0();
led->configure(PinFunctionOutput0);
led->toggle();

But there is a better way, with Pin class using PinPort as a port:

Pin led;
led.configure(pa1(), PinFunctionOutput);
led = 1;
led = 0;

This leads to being able to seperate definition and usage:

Pin led(pa1());

int main()
{
  led.configure(PinFunctionOutput0);
  led = 0;
  led = 1;
  led.toggle();
}

Spi

SpiMaster spiMaster;
spiMaster.setPort(ssp0());
spiMaster.setSelectPin(p1_0);
spiMaster.open();

spiMaster.start();
spiMaster.writeRead(dataA, dataB, 8);
spiMaster.stop();

Edge Detection

This code calls handlePinEvent when pin 0 of port 2 of LPC series changes from 0 to 1:

EdgeDetector edgeDetector;
edgeDetector.onEvent().connect(handlePinEvent);
edgeDetector.setPin(p2_0());
edgeDetector.setEdge(EdgeRising);
edgeDetector.start();

The EdgeDetector class is the most easy one to describe why 2 level HAL is beneficial. If all the abstraction is implemented in the PinPort class, we had to put necessary data into it. That means two pointers (one for callback list, one for thread pointer) into every PinPort instance. If one uses 30 pins in a 32 bit environment that means 240 bytes on that platform are wasted just for unused callback pointer and thread pointer. With two level abstraction only those pointers are used in EdgeDetector class..

Thread pointer on the EdgeDetector or similar class is used to denote in which thread the callbacks are run.. With this design, PinPort class uses 0 RAM. Literally.. The tip: The content of it is stored in ROM. p2_0() returns a pointer to ROM..

Leave a Reply

Your email address will not be published. Required fields are marked *