Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Comments on What exactly is a HAL and how would I write my own?

Parent

What exactly is a HAL and how would I write my own?

+2
−0

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?

History

0 comment threads

Post
+1
−0

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.

History

3 comment threads

Purpose of opaque type (4 comments)
Inferring registers from port number - not MISRA compliant (3 comments)
Organizing and VCS (2 comments)
Organizing and VCS
Carl‭ wrote 27 days ago

Thank you for your Answer Lundin. In terms of organizing and VCS, do you have a something like a separate git repository for your all your *hal.c and *hal.h, and then another git repository with all the driver code, with separate branches for separate MCUs?

Lundin‭ wrote 24 days ago

Carl‭ I don't use git but yes separate repo for all "company standard" files like HALs shared between multiple projects and one repo for drivers. Checked out hard copies of the files then get added to each project, you don't want to be in a position where updates to standard/driver code leads to automatic updates in each and every project. Rather, you now and then take your time with a project to check diff between project repo and the others; that’s easy to do with programs like Winmerge.