Let’s use the classic LED blink exercise to understand the code STM32CubeMX creates. At the same time, I’ll compare the code when using HAL and LL libraries. I commented on the differences between HAL and LL in a previous post.

The project structure generated by STM32CubeMX looks close to what I expected. The relevant sources are inside the Core folder, divided into Src and Inc. Other software packages, such as HAL and BSP libraries, live in the parallel folder Drivers. But I find the project’s structure in STM32CubeIDE messy; it reorganizes the code into different folders. The main.c file and some high-level library files are in the Application/User/Core folder, which doesn’t match the hard drive’s file structure one-to-one. A similar situation happens with the Drivers folder. Well, this is not a real problem after learning it.

To build the HAL and LL examples, I started a new project in STM32CubeMX from the Board Selector. When the Board Options window appears, I choose USER LED GREEN (LD2) only. For the HAL example, I used all the default options. For the LL case, I deselected Human Machine Interface in the BSP configuration and set PA5 to GPIO_Output in the Pinout view. Later, I configured the pin in Output Push Pull mode with a Pull-up resistor. On the Project Manager > Advanced settings page, I changed the drivers for all peripherals from HAL to LL.

Blinky at high level, using HAL and BSP

STM32CubeMX uses the HAL and BSP libraries by default. This case is the simplest; you don’t really need to know details about the hardware. Just remember that BSP names the green LED on the board as LED_GREEN.

Then, the project’s configuration in STM32CubeMX is straightforward, just set a project name and select the right toolchain. After generating the code, we import it into STM32CubeIDE.

The skimmed main file looks like this:

#include "main.h"

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void)
{
    /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
    HAL_Init();

    /* Configure the system clock */
    SystemClock_Config();

     /* Initialize all configured peripherals */
    MX_GPIO_Init();

    /* Initialize led */
    BSP_LED_Init(LED_GREEN);

    /* Turn led on*/
    BSP_LED_On(LED_GREEN);

    while (1)
    {
        BSP_LED_Toggle(LED_GREEN);
        HAL_Delay(500); //ms
    }
}

void SystemClock_Config(void)
{
    ...
}

static void MX_GPIO_Init(void)
{
    ...
}

void Error_Handler(void)
{
    ...
}

...

The HAL and BSP libraries are included by main.h as:

#include "stm32g4xx_hal.h"
#include "stm32g4xx_nucleo.h"

Avoiding details, it is clear what is happening. HAL takes the complete task of initialization and basic system configuration. The next instruction configures the system clock. It sets the oscillator, PLL, etc. It also initializes the CPU and bus clocks. GPIO pins and UARTs are configured in the function MX_GPIO_Init(), but the actual configuration of the LED pin is done with BSP_LED_Init(LED_GREEN), which is defined in stm32g4xx_nucleo.c, and it overwrites the values set before. The BSP library is used again to turn the LED on at program startup.

Inside the while loop, we program the actions we want. In this case, BSP provides a function to toggle a digital output directly, and HAL provides a delay function. So, Blinky reduces to these two lines of code. Without them, the LED will remain on continuously.

In this example, the use of HAL is explicit; it is used everywhere. The functions SystemClock_Config() and MX_GPIO_Init() also use it internally to complete the initialization. Not surprisingly, all the HAL functions are implemented in the stm32g4xx_hal.c file. BSP, on the other hand, is responsible for providing the handler LED_GREEN and the functions to operate with this handler, including BSP_LED_Init(), BSP_LED_On(), and BSP_LED_Toggle(). These definitions are set in stm32g4xx_nucleo.c and its header file. In many cases, the BSP functions warp a similar HAL function, but operate with a different input variable type. For example, BSP_LED_Toggle(Led_TypeDef) internally calls the function HAL_GPIO_TogglePin(GPIO_TypeDef, uint16_t). In that way, BSP raises the abstraction level over HAL.

Blinky using LL

The Low Level (LL) drivers provide access to MCU registers and implement basic hardware access. This version of the Blinky example was created in STM32CubeMX, disabling the HAL and BSP drivers. The original code had a couple of extra sentences, which I removed to match the HAL version’s structure. It looks like the following:

#include "main.h"

void SystemClock_Config(void);
static void MX_GPIO_Init(void);

int main(void)
{
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_SYSCFG);

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();

  while (1)
  {
    LL_GPIO_TogglePin(GPIOA, LL_GPIO_PIN_5);
    LL_mDelay(1000); // ms
  }
}

void SystemClock_Config(void)
{
    ...
}

static void MX_GPIO_Init(void)
{
    ...
}

void Error_Handler(void)
{
    ...
}

...

Here, the functions and parameter names are more esoteric. You have to go to the LL documentation to understand their meaning. But here we have only a few of them.

The first difference appears when resetting the peripherals. Instead of the HAL_Init() function we have LL_APB2_GRP1_EnableClock() with one argument. The functions SystemClock_Config() and MX_GPIO_Init() are implemented using only LL instructions (no HAL).

LL also has a function to toggle an output pin, but it requires two input parameters, the GPIO Port and pin masks to address the pin to control. In this board, the pin controlling the LED is the PA5. You can check that in the board’s schematic, or in the pinout view of STM32CubeMX with BSP enabled. I guess that the pin name PA5 comes from the Pin number 5 in the GPIOA port. It matches, but I haven’t found the definition in the documentation.

Finally, LL also has a delay function LL_mDelay() similar to HAL_Delay() which accepts an argument representing the period to wait, in milliseconds.

Conclusion

The objective of this simple example was to show the basic differences between HAL and LL, how the code differs, and to become familiar with the project structure created by STM32CubeMX and STM32CubeIDE.

HAL and BSP work together to implement a high-level driver to control the MCU hardware. Those drivers are more intuitive, but also increase the overhead. On the other hand, with LL, we can program the same application with more control. Which one to choose?. It depends on the application, how efficient the code needs to be, and if we expect to port the firmware to a different MCU. Because of its highest abstraction level, HAL provides the easiest portability between MCUs on the STM32 platform.

So, why make things complicated when you can make them easy? Then go for HAL.