What exactly is a HAL and how would I write my own?
When I program STM32 microcontrollers I use ST's toolchain which provides all the tools necessary in order to build (and flash) the application binary. The toolchain also comes with ST's own library (source and header files) that allows me to control the micro's peripherals by using their functions. They group these files under a common name, "HAL", which stands for Hardware Abstraction Layer.
Looking into, for example, the gpio hal I see code like this.
/* stm32f4xx_hal_gpio.c */
void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
uint32_t odr;
assert_param(IS_GPIO_PIN(GPIO_Pin));
/* get current Output Data Register value */
odr = GPIOx->ODR;
/* Set selected pins that were at low level, and reset ones that were high */
GPIOx->BSRR = ((odr & GPIO_Pin) << GPIO_NUMBER) | (~odr & GPIO_Pin);
}
So in this function they set/reset some register bits (alter the hardware) and the result of setting these bits is that a pin is toggled (on a higher abstraction layer).
So is a HAL really just a collection of functions that when called set/reset specific register bits for my specific microcontroller? If so, is it really necessary to call it a HAL?
3 answers
So is a HAL really just a collection of functions that when called set/reset specific register bits for my specific microcontroller?
The HAL created by ST for the STM32 is a relatively thin wrapper around registers. Most functions access a register (or a few related registers) and perform some useful low-level operation for the caller.
The STM32 HAL library (plus the STM32cubeMX) is for the entire STM32 family, not just a specific microcontroller. If you need to move the calling code to a different STM32 microcontroller (say from an F4 to a G4), your calling code should be able to call the HAL generated for that other microcontroller, generally. (There may be exceptions, for example the ADC code for F0 isn't compatible with F4, G4, and most of the rest STM32.)
If so, is it really necessary to call it a HAL?
If you want to access registers directly, you can do that without a HAL.
In my case STM32 HAL and CubeMX saved a lot of time, accounting for the time which I've spent learning the HAL.
p.s.
At some point you'll also come across the term Board Support Package (BSP).
0 comment threads
A proper HAL is exactly what it sounds like - a Hardware Abstraction Layer. Meaning code which can execute no matter what underlying hardware that's sitting below it. Hardware includes the MCU itself, naturally.
ST's bloat libs is a bad example since they aren't a proper HAL but just a thin wrapper around register access on STM32 families. Please note that unlike us users of MCUs, ST have absolutely zero interest in making it easy for us to port away from their MCU to a competitor brand. They just want to make it easy to port between ST products and so their lib is intentionally designed that way - it is actually non-portable in the true sense of the word. You can't even use it to port between ST products like STM32, STM8 and SPC5. And their competitors do the same thing too.
GPIO_TypeDef contains super-specific hardware stuff related to STM32 specifically. Likely it is just a pointer pointing directly into the memory-mapped registers. That's not anywhere near a HAL. I don't know why ST calls it such, either it's a marketing trick or they are just incompetent, or maybe both.
A proper HAL:
- Requires the user to know how a certain type of hardware peripheral works. Not how the hardware peripheral on some particular MCU family works.
- Does not expose any hardware-related driver code such as raw registers, raw register settings, interrupt handling etc etc.
- Can be ported cleanly to another MCU by simply swapping out the underlying driver.
In terms of OO design, a HAL is an abstract base class which may be implemented by writing the driver through inheritance. The simplest form of inheritance in C is just to place all function declarations in a header then call those - and if the driver didn't implement them you'll get a linker error.
I can give an example from a CAN bus HAL I've written in C. It's proprietary code so I can't share it as whole, but I can share fragments. You could write a HAL for similar serial buses like UART, SPI, I2C etc in the same manner. It's just that CAN driver is fairly complicated compared to the others and so a HAL is definitely justified, especially when you want portability of application-tier code like protocol stacks etc.
The HAL is located in a header called canhal.h. It is just a header. It is unlikely that it needs to be acompanied with a canhal.c but sometimes you add a .c file in case there's some internal definition that needs to be included. The driver that actually implements all functions will be named something like mcu_xyz_can.h/mcu_xyz_can.c where xyz is the specific MCU. I've ported implementations of this HAL to various Cortex M and PowerPC targets including some STM32 and SPC5, so there's some 6-7 different drivers implementing it and not just for ST but also various MCUs from Microchip, NXP and so on.
The header first defines some common types used by the HAL API:
typedef struct can_msg_t can_msg_t;
/*
can_msg_t is an opaque type defined by driver implementation.
Users and application code only declare pointers of this type.
Allocation is handled by the driver. A copy function is provided for copying the contents of
one buffer to the other.
*/
#define CAN_MSG_EXTENDED_BIT (1u << 30) // used by can_id_t
typedef uint32_t can_id_t;
/*
A plain number corresponding to the 11 or 29 bit identifier, in bits 0 to 28.
Bit CAN_MSG_EXTENDED_BIT will be be set to indicate extended identifiers.
The application need not access this type directly: the can_set_id() and can_get_id() helper
functions can be used for converting to/from plain 32-bit integers.
See function definitions at the bottom of this header file.
*/
typedef enum
{
// a lot of status codes are defined here, related to generic CAN bus errors
} can_status_t;
// Maximum number of data bytes in CAN and CAN FD respectively:
#define CAN_DATA_BYTES_N 8
#define CANFD_DATA_BYTES_N 64
Then there's a init function template for the whole driver:
can_status_t can_init_hw (void);
This initializes the CAN driver and general hardware, such as pin/clock routing registers etc, if applicable. The driver implementation of this will initialize any internal variables here and also set routing registers if needed.
Then a separate init function for each CAN bus peripheral that's present on the MCU - often there's more than one and the HAL needs to cover that:
can_status_t can_init_port (volatile void* port,
uint32_t sysclk_khz,
uint32_t baudrate_khz,
const can_id_t* mailbox_can_id,
size_t mailbox_can_id_n);
Here I've chosen to include a pointer to the start of the register map as the means to tell different CAN peripherals apart. It does point straight into the register map but it's a void pointer so nothing meaningful can be done with it in itself. The driver can then define something like CAN0 as a pointer to the first hardware register and it need not be void* since volatile uint32_t* or whatever type it actually got is implicitly convertible. It can be a pointer to some register struct defined by the manufacturer too.
sysclk_khz and baudrate_khz are quite self-explanatory: you provide the MCU clock for pre-scaler calculation purposes and you provide the baudrate which the peripheral should run at, since it will be fixed once up and running.
The "mailbox" parameters are optional for CAN hardware peripherals that support mailbox functionality - not every hardware does and then you'd just set these to NULL/0.
The driver implementation of this function will be pretty big and super-specific to the given hardware, with all the pre-scale clocking and timing setup, as well as generic register setups. I can share the start of one such implementation, from mcu_xyz_can.c:
/* Check which port that was selected */
volatile Can* can_port = port;
if(can_port != CAN0 && can_port != CAN1)
{
return CAN_ERR_INIT_HW; // port parameter is wrong
}
In this case Can* is a register struct similar to the ST ones but in this case by Microchip's equivalent bloat lib, which is not a HAL either. It points straight into the register map too. A struct which shouldn't be exposed outside the specific driver. CAN0 will be such a volatile struct* defined in some register map by the manfacturer (and incompetently not defined as volatile there even though it contains nothing but volatile members).
What's nice from here is that I may use can_port->register_name to setup all registers so that can_init_port sets up the relevant port only and can be called several times for each existing port. This is a common trick when writing any form of driver code when more than one specific peripheral should be supported.
Other functions in the canhal.h will be more straight-forward, they'll look something like:
(in the real proprietary code they obviously come with lots of source code documentation of all parameters and possible return values)
can_status_t can_get_status (volatile void* port);
void can_msg_copy (can_msg_t* restrict dst, can_msg_t* restrict src);
void can_msg_set_id (can_msg_t* msg, can_id_t id);
can_id_t can_msg_get_id (const can_msg_t* msg);
void can_msg_set_data (can_msg_t* msg, const uint8_t* data, size_t data_n);
size_t can_msg_get_data (const can_msg_t* msg, uint8_t* data);
can_status_t can_send (volatile void* port, const can_msg_t* msg);
can_status_t can_receive (volatile void* port, can_msg_t* msg);
So everything is made abstract: the port itself, the can messages, the identifier which is a special thing in CAN, and so on. Access to the various types is provided by setters/getters.
In the application I then have setup calling code like this:
status = can_init_hw();
if(status != CAN_OK) { appname_error_handler(err_from_can_status(status)); return ; }
status = can_init_port (port, APPNAME_CPU_CLK, APPNAME_CAN_BAUD, NULL, 0);
if(status != CAN_OK) { appname_error_handler(err_from_can_status(status)); return ;}
status = can_msg_create(&tx_msg, 0, NULL, 0, false);
if(status != CAN_OK) { appname_error_handler(err_from_can_status(status)); return ;}
status = can_msg_create(&rx_msg, 0, NULL, 0, false);
if(status != CAN_OK) { appname_error_handler(err_from_can_status(status)); return ;}
And a typical send sequence might look like this (from a CANopen-esque CAN application layer):
const uint8_t BOOTUP [CAN_DATA_BYTES_N] = { ... };
can_msg_set_id(tx_msg, CANNAME_MSG_BOOTUP);
can_msg_set_data(tx_msg, BOOTUP, 8);
can_status_t status = can_send(can_port, tx_msg);
if(status != CAN_OK)
{
/* error handling */
}
In this case the caller doesn't know or care which MCU that the code is running on, how CAN messages are handled in memory on this MCU, how the internal identifier format is laid out, how exactly the message is sent and so on. (This code actually got a HAL for the CAN application tier too, so it can run multiple different CAN protocols on multiple MCU targets.)
All in all this HAL including source code comments and the driver implementation is just around a thousand LoC, so it's not a huge library or anything.
A HAL (hardware abstraction layer) tries to present a common software interface to different underlying hardware. This is done by re-writing the HAL to each specific hardware it supports. In a sense, a HAL is really a specification for a software interface applications can use to manipulate hardware.
The advantage to the application developer is that you can write your application to call HAL functions instead of directly accessing the hardware, which then allows the app to be easily ported to different hardware also supported by the same HAL. The disadvantage is that the common interface presented by the HAL may not provide access to every detailed feature of all underlying hardware. The application interface to the HAL has to be generalized somewhat, so may not always allow applications to use the most efficient method, or not access all hardware capabilities.
No, a HAL is never required, but it can be a good idea to use one because:
- If you ever want to port your app to different hardware that is also supported by the HAL you used, then in theory, no changes to the app are required other than linking to the HAL version specific to the new target hardware.
- Some peripherals can be rather complicated to use. Presumably the HAL designers are intimately familiar with the specific hardware versions that are targeted, and have already done the detailed setting up and managing of those target hardwares.
On the other hand, if you want to use an exotic feature of a particular hardware version, are sure you will never want to port the app, or are interested in learning the hardware details, then you can write directly to the hardware registers.
Added in response to comments to Lundin's answer
This is really a response to Carl's comment to Lundin's answer, but ended up being too much for a comment. Besides, this is real content that shouldn't be buried in a comment.
Carl wrote:
With regards to the init function, there is a way to infer all the necessary registers from just the port parameter. For the microcontroller I'm using, the USART registers for port 0 are UCSR0A (0xC0), UCSR0B (0xC1), UCSR0C (0xC2). USART port 1 shows same 1 byte offset in memory, so I thought of something like this for the init function:usart_status_t usart_init_port(volatile void* port, ...) { volatile uint8_t* usart_port = port;
if(usart_port != &UCSR0A && usart_port != &UCSR1A) { return USART_ERR; } volatile uint8_t* ucsrna_ptr = usart_port; volatile uint8_t* ucsrnb_ptr = ucsrna_ptr + 1u; volatile uint8_t* ucsrnc_ptr = ucsrna_ptr + 2u; uint8_t ucsrna = *ucsrna_ptr; uint8_t ucsrnb = *ucsrnb_ptr; uint8_t ucsrnc = *ucsrnc_ptr; /* Set up temporary "register" variables below ... */ //Assign values to hardware registers *ucsrna_ptr = ucsrna; *ucsrnb_ptr = ucsrnb; *ucsrnc_ptr = ucsrnc;}
Pointer arithmetic is not MISRA compliant though. Is this a good idea?
No, it is not. This does at run time what should be done at build time. You know which UART you are using at build time. The run time code shouldn't be determining which registers it uses. All the information is available at build time. That's when the registers should be resolved, with the runtime code writing directly to those registers.
This is what preprocessors are for. Take a look at my UART_MODBUS.INS.DSPIC file (driver or HAL for a dsPIC UART with additional capabilities to support Modbus). in the Embed dspic repository on GitHub. Up to line 311 is all preprocessor computations. In particular look at the PICK block starting on line 226. This does exactly what you are asking for, which is to find which registers are used by the specific UART. It also identifies fields in other system registers, like the interrupt mask bit, enable bit, and priority.
See also the file QQQ_UART_MODBUS.DSPIC. That is the template file that is copied and modified for each application. The application writer selects all the features by setting the appropriate constant values, then the previous file is included which derives everything else from those constants.
This example is implemented with my PIC and dsPIC assembler preprocessor, PREPIC. PREPIC is in turn implemented on the Embed scripter, ESCR. However, the concept applies to pre-processing in general.

0 comment threads