LL drivers

Overview

The Low Layer (LL) drivers are part of the STM32Cube firmware HAL, providing a basic set of optimized, one-shot services. Unlike the HAL drivers, the LL drivers are not fully portable across all STM32 families; the availability of certain functions depends on the physical presence of the corresponding features in the specific product.

The Low Layer (LL) drivers are designed to offer the following features:

  • A set of inline functions for direct and atomic register access.

  • One-shot operations that can be used by the HAL drivers or at the application level.

  • Fully independent of HAL, allowing standalone usage without HAL drivers.

  • Comprehensive feature coverage for all supported peripherals.

Example Image

Note

The LL initialization functions (C file) are no longer available on HAL2. If a user needs to initialize a given peripheral using the LL layer, they rely on code generation with the LL layer. This generates the required sequence based on the LL Low, Middle, and High-level APIs available in the header (.h) files. The generated sequence provides better footprint optimization compared to the previous LL Init functions.

From an architecture point of view, the low layer drivers are at the same level as core drivers. Core drivers are called internally by the HAL drivers to provide common intrinsic services for several HAL drivers that share the same peripheral hardware block, while the low layer drivers provide low-level services to both HAL drivers and the application.

The LL drivers are designed to provide hardware services based on the available features of the STM32 peripherals. These services are reflecting exactly the hardware capabilities and provide One-shot operations that are called following the programming model of the reference manual document. The LL services are not based on standalone processes and do not need any additional memory resources to save their state, counters, or data pointers; all operations must be performed by changing the associated peripheral registers content.

All the LL drivers are called through their physical instance (peripheral register structures mapped on the peripheral base registers). Only system peripherals do not need to be called through their instance. The LL drivers are given in one module for each physical peripheral located in a separate header file. These files are called within the HAL drivers. When using the LL drivers at the application side, the LL header files are included through the LL file inclusion model with no additional project setting.

Low Layer files

File

Description

Common (for all series)

stm32tnxx_ll_utils.h

The LL UTILS driver contains a set of generic APIs that can be used for:

  • Device electronic signature

  • Timing functions

stm32tnxx_ll_system.h

The LL SYSTEM driver contains a set of generic APIs that can be used to:

  • Get the device unique ID

  • Get the package device type

Peripheral drivers

stm32tnxx_ll_bus.h

This is the h-source file for core bus control and peripheral clock activation and deactivation

Example: LL_AHB2_GRP1_EnableClock

stm32tnxx_ll_ppp.h

This is the h-source file of the PPP low layer driver. The low layer PPP driver is a standalone module. To be used, the application includes the stm32tnxx_ll_ppp.h Note: When the low layer is used with the HAL drivers, there is no need to include the given LL header files in the application.

Unlike the HAL drivers, the low-layer ones are not built on a process model but rather on simple register access operations. Thus, the low-layer drivers have no configuration file.

For the common drivers:

LL driver

APIs

LL system

__STATIC_INLINE uint32_t LL_GetPackageType(void)

__STATIC_INLINE uint32_t LL_GetUID_Word0(void)

__STATIC_INLINE uint32_t LL_GetUID_Word1(void)

__STATIC_INLINE uint32_t LL_GetUID_Word2(void)

LL util

__STATIC_INLINE void LL_InitTick(uint32_t cpuclk_frequency, uint32_t ticks)

__STATIC_INLINE void LL_Delay_NoISR(uint32_t delay_ms)

APIs definition levels

The LL drivers’ scope is to provide an abstraction API level that covers snippet functions and some of the standard peripheral library functionalities. The low layer drivers provide a complementary set of basic functions for the HAL drivers to allow the customization or the replacement of some high-level processes. The following figure summarizes the function coverage level for all the ST firmware offerings:

Example Image

Each LL peripheral driver provides the following three API levels:

Low level

This level includes the LL_PPP_WRITE_REG() / LL_PPP_READ_REG() (redirection of CMSIS register operations) and the LL_PPP_DMA_GET_REG_ADDR :

  • Multi-instance peripherals:

/**
 * @brief  Write a value in PPP register.
 * @param  instance PPP Instance.
 * @param  reg      Register to be written.
 * @param  value    Value to be written in the register.
 * @retval None.
 */
#define LL_PPP_WRITE_REG(instance, reg, value) WRITE_REG(((instance)->reg), (value))
/**
 * @brief  Read a value in PPP register.
 * @param  instance PPP Instance.
 * @param  reg      Register to be read.
 * @retval Register value.
 */
#define LL_PPP_READ_REG(instance, reg) READ_REG(instance->reg)
  • Single instance peripherals (system peripheral):

/**
  * @brief  Write a value in PPP register
  * @param  reg Register to be written
  * @param  value Value to be written in the register
  */
#define LL_PPP_WRITE_REG(reg, value) WRITE_REG(PPP->reg, (value))

/**
  * @brief  Read a value in PPP register
  * @param  reg Register to be read
  * @retval Register value
  */
#define LL_PPP_READ_REG(reg) READ_REG(PPP->reg)

Note

The LL_PPP_DMA_GET_REG_ADDR() API is a common interface for DMA-related registers for all peripherals to prevent using direct register access in DMA configuration APIs. This API provides only the address of the register used for DMA transfer. It is used only for configuration, thus, conditional checks are allowed. For optimal use, direct register access can always be used. The LL_PPP_DMA_GetRegAddr() prototype is defined as follows:

__STATIC_INLINE uint32_t LL_PPP_DMA_GET_REG_ADDR(PPP_TypeDef *p_ppp /* Sub Instance if any, e.g., channel */, uint32_t property);

Middle level

This level includes one-shot operation APIs (atomic) and the elementary LL_PPP_SetItem() and LL_PPP_Action() functions, such as LL_ADC_Enable() , LL_LPTIM_SetCounterMode() , and LL_LPTIM_StartCounter() :

LL_ADC_Enable();
LL_LPTIM_SetCounterMode();
LL_LPTIM_StartCounter();

High level

This level includes global configuration and initialization functions that cover full standalone operations on relative peripheral registers, such as LL_RCC_PLL_ConfigDomain_SAI() and LL_RTC_TIME_Config() :

LL_RCC_PLL_ConfigDomain_SAI(uint32_t source, uint32_t pll_m, uint32_t pll_n, uint32_t pll_p);
LL_RTC_TIME_Config(uint32_t hour, uint32_t minute, uint32_t second);

APIs Categories

For Low-Layer driver API visibility and in order to explicitly differentiate common actions associated with different modes and features, some APIs are categorized. The category notion can refer to the hardware mode, feature, and physical sub-block, depending on the peripheral.

The following table shows categories associated with some supported peripherals:

Peripheral

Associated categories

ALL

DMA: DMA Get Data address APIs

ADC

INJ: injected mode

REG: Regular mode

RTC

TIME

DATE

ALARMA

ALARMB

TIMESTAMP

TAMPER

WAKEUP

BAK: backup

CAL: calibration

TIM

CC: capture compare

OC: output compare

IC: Input capture

RCC

HSE

HSI

HSI48

LSI

LSCO

MSI

PLLx (with x depending on the given STM32 series)

Functions coding rules

Low-layer functions are defined to have direct or indirect access (register base + offset from LL function parameter) and unconditional register access.

__STATIC_INLINE void LL_PPP_SetConfigItem(PPPx_TypeDef *p_ppp, uint32_t value)
{
    WRITE_REG(p_ppp->REG, ADJUST(value));
}

Thus, if-else statements are not allowed. However, an exception is made when register naming, or bit definition or sequence might differ across ⁽¹⁾.

__STATIC_INLINE void LL_PPP_SetConfigItem (PPPx_TypeDef *p_ppp, uint32_t value)
{
  if(p_ppp == PPPA)
  {
    WRITE_REG (p_ppp->REGISTERA, ADJUST (value));
  }
  else
  {
    WRITE_REG (p_ppp->REGISTERB, ADJUST (value));
  }
}

⁽¹⁾ this exception does not include Specific Interrupt and status flags management rule. ⁽²⁾ ADJUST macro refers to the mathematical processing (shift, masking…etc) made on the value.

The compile-time conditional checks are allowed to limit function scope according to physical availability of the peripheral or specific features:

#if defined (PPP)
 (specific feature)
#endif
  • Example

#if defined(QUADSPI)
__STATIC_INLINE void LL_RCC_SetQSPIClockSource(uint32_t clk_source) { }
#endif /* QUADSPI */
#if defined (FEATURE_BIT_DEFINITION)
(specific feature)
#endif

Example:

#if defined(PWR_CR1_SRAM3PD)
__STATIC_INLINE void LL_PWR_SetSRAM3RunRetention(uint32_t sram3_retention)
{
  MODIFY_REG(PWR->CR1, LL_PWR_SRAM3_RUN_FULL_RETENTION, ((~sram3_retention) & LL_PWR_SRAM3_RUN_FULL_RETENTION));
}

__STATIC_INLINE uint32_t LL_PWR_GetSRAM3RunRetention(void)
{
  return ((~(READ_BIT(PWR->CR1, LL_PWR_SRAM3_RUN_FULL_RETENTION))) & LL_PWR_SRAM3_RUN_FULL_RETENTION);
}
#endif /* defined(PWR_CR1_SRAM3PD) */

The compile-time conditional checks are also allowed to customize a function’s implementation according to physical availability of the peripheral or specific features:

  • Example:

__STATIC_INLINE void LL_COMP_ConfigInputs(COMP_TypeDef *p_comp, uint32_t input_minus, uint32_t input_plus)
{
#if defined (COMP_CFGRX_INP2SEL)
  MODIFY_REG(p_comp->CFGR, COMP_CFGRx_INMSEL | COMP_CFGRx_INPSEL |
                          COMP_CFGRx_INP2SEL | COMP_CFGRx_SCALEN | COMP_CFGRx_BRGEN,
                          input_minus | input_plus);
#else
  MODIFY_REG(COMPx->CFGR, COMP_CFGRx_INMSEL | COMP_CFGRx_INPSEL |
                          COMP_CFGRx_SCALEN | COMP_CFGRx_BRGEN,
                          Input_minus | input_plus);
#endif
}

Important

For both HAL and LL drivers, it is prohibited to use the device RPN define as compile-time conditional checks (e.g., #if defined(STM32C562xx)). Instead, the implementation relies on dedicated defines for peripherals, features, or feature bits, as described above.

This approach simplifies the maintenance of HAL and LL drivers when new derivatives of the same series are introduced. In such cases, the Device Family Pack (DFP) should be updated with the new device header file, which includes the necessary defines for peripherals and features.

As a result, HAL and LL drivers will automatically support the new device derivatives without requiring modifications, provided the peripherals and features remain consistent. Updates to HAL and LL drivers are only necessary if the new derivative introduces new use cases or features.

Indirect access is recommended when a status flag, interrupt, or activation field could be located in different registers following the sub-instance (channel, stream, etc.). It is recommended to calculate the addressed register offset through the sub-instance index.

  • Example:

__STATIC_INLINE void LL_PPP_ClearIT_XXX (PPP_TypeDef *p_ppp, uint32_t sub_index)
{
      STM32_WRITE_REG (p_ppp->REG + CALC_OFFSET (sub_index), PPP_REG_XXXX)
}

Function prototypes

All the low layer APIs for a PPP peripheral are defined in stm32yxxx_ll_ppp.h with the following rule:

__STATIC_INLINE return_type LL_PPP_Function (PPPx_TypeDef *p_ppp, args)
{
  (Function body)
}

Inlining is a form of extra effort to call the function faster than it would otherwise, generally by substituting the code of the function into its caller. As well as eliminating the need for a call and return sequence, it might allow the compiler to perform certain optimizations between the bodies of both functions.

Sometimes it is necessary for the compiler to emit a standalone copy of the object code for a function even though it is an inline function, for instance if it is necessary to take the address of the function, if it cannot be inlined in some particular context, or if optimization has been turned off.

Note

There are various ways to define inline functions; any given kind of definition might definitely emit stand-alone object code, definitely not emit stand-alone object code, or only emit stand-alone object code if it is known to be needed. Sometimes this can lead to duplication of object code.

Low layer functions are defined as static inline. Thus, local definition may be emitted if required. Multiple definitions are possible in the application, in different translation units, and it will still work. Just dropping the inline reduces the program to a portable one (again, all other things being equal).

Functions arguments

The low layer APIs are defined as static inline , except the helper services, which are to be defined as macros. These functions might have no arguments or multiple arguments. The maximum number of arguments is fixed to 4 to allow high code optimization and full usage of core registers without using the stack. An exception is made when the arguments are to be loaded in the same register; in this case, more than 4 arguments could be used.

__STATIC_INLINE return_type LL_PPP_Function (PPPx_TypeDef *p_ppp, args)
{
  (Function body)
}

Item

Description

Possible Values

return_type

return value type

  • void

  • {u}intx_t where x could be 8/16/32

p_ppp

peripheral instance (not required for system peripherals)

  • Depend on product Part number (ex: ADC1)

args

functions’ arguments (can be omitted or with

more arguments)

  • void

  • {u}intx_t where x could be 8/16/32

Data pointers are not allowed; the low layer handles only single data transfers, and it is up to the application to manage multiple data transfers. Memory addresses are to be defined as uintx_t (where x could be 8/16/32) following the transfer mode, and it is up to the application to use the right transfer API or cast from a different user pointer type.

LL_PPP_TransmitData8 (PPPx, data8);
LL_PPP_TransmitData16 (PPPx, data16);

Example: USART

LL_USART_TransmitData8 (PPPx, data8);
LL_USART_TransmitData9 (PPPx, data16);

Note

  1. In the LL layer, enumerated types are not allowed to minimize ORed definition conflict.

  2. Defines are aligned to the HAL literals and enumerated types to ease redefinition. Examples:
    1. In the HAL, HAL_GPIO_PIN_0 is a define redirected to the equivalent define in the low layer GPIO driver.

    2. In the HAL, the GPIO mode is an enumeration, the possible values are redirected to their equivalent in the low layer GPIO driver.

  3. Defines are inherited from bit definition in CMSIS files. Example:

Examples

#define LL_GPIO_PIN_0          GPIO_BSRR_BS0   /*!< Select pin 0 */
#define LL_GPIO_PIN_1          GPIO_BSRR_BS1   /*!< Select pin 1 */
#define HAL_GPIO_PIN_0         LL_GPIO_PIN_0   /*!< GPIO pin 0  */
#define HAL_GPIO_PIN_1         LL_GPIO_PIN_1   /*!< GPIO pin 1  */
#define LL_GPIO_MODE_INPUT       0x00000000U         /*!< Select input mode              */
#define LL_GPIO_MODE_OUTPUT      GPIO_MODER_MODE0_0  /*!< Select output mode             */
#define LL_GPIO_MODE_ALTERNATE   GPIO_MODER_MODE0_1  /*!< Select alternate function mode */
#define LL_GPIO_MODE_ANALOG      GPIO_MODER_MODE0    /*!< Select analog mode             */
typedef enum
{
    HAL_GPIO_MODE_INPUT     =  LL_GPIO_MODE_INPUT,      /*!< Input Floating mode */
    HAL_GPIO_MODE_OUTPUT    =  LL_GPIO_MODE_OUTPUT,     /*!< Output mode         */
    HAL_GPIO_MODE_ALTERNATE =  LL_GPIO_MODE_ALTERNATE,  /*!< Alternate mode      */
    HAL_GPIO_MODE_ANALOG    =  LL_GPIO_MODE_ANALOG      /*!< Analog mode         */
} hal_gpio_mode_t;
  1. The following peripherals do not require to be instantiated:

Peripherals (IPs)

System

FLASH/NVM

DBGMCU

EXTI

PWR

RCC

SYSCFG/SBS

RTC

TAMP

  1. By default, the returned value is uint32_t, even for Boolean values. However, for data read and reception, depending on the data width, the returned value can be {u}intx_t where x could be 8/16/32. Examples:

__STATIC_INLINE uint32_t LL_PPP_IsEnabledIT_XX(PPP_TypeDef *p_ppp);
__STATIC_INLINE uint8_t LL_PPP_ReceiveData8(PPPx_TypeDef *p_ppp);
__STATIC_INLINE uint16_t LL_PPP_ReceiveData9(PPPx_TypeDef *p_ppp);

Functions implementation

Local variables

The Low layer functions are not use additional resources than core and peripheral registers. Both global variables and objects (structure) are not allowed. The low layer drivers can use only local variables. Local variables used to make logical operations before loading them into register, are always 32-bit variables. Typically, local variables are placed in registers by the compiler, instead of on the (stack) frame of the function. Using the core registers is very efficient because they do not require memory accesses, so the compiler can use shorter/faster instructions when working with them.

__STATIC_INLINE void LL_PPP_SetConfigItem(PPPx_TypeDef *p_ppp, uint32_t value)
{
    uint32_t tmp = 0;
    tmp = (value) << 4;
    tmp |= CONVERT(value);
    WRITE_REG(p_ppp->REG, tmp);
}

Local variables usage are limited and, if possible, use internal core registers for register bits handling. Therefore, the function above can be defined as follows:

__STATIC_INLINE void LL_PPP_SetConfigItem(PPPx_TypeDef *p_ppp, uint32_t value)
{
    WRITE_REG(p_ppp->REG, ADJUST(value));
}

Operations on registers

It is recommended to handle register operations using the following exported macros from the CMSIS files. Examples:

STM32_SET_BIT(REG, BIT)
STM32_CLEAR_BIT(REG, BIT)
STM32_READ_BIT(REG, BIT)
STM32_CLEAR_REG(REG)
STM32_WRITE_REG(REG, VAL)
STM32_READ_REG(REG)
STM32_MODIFY_REG(REG, CLEARMASK, SETMASK)

Boolean operations

The returned Boolean value is always uint32_t . Example:

__STATIC_INLINE uint32_t LL_PPP_IsEnabledIT_XX(PPP_TypeDef *p_ppp);

Internal implementation are as follows:

__STATIC_INLINE uint32_t LL_PPP_IsItem_XXX(PPP_TypeDef *PPPx)
{
  return ((READ_BIT(PPPx->REG, BIT_DEFINITION) == (BIT_DEFINITION)) ? 1UL : 0UL);
}

Naming rules

Naming convention

The following tables summarize the common words and phrases to be used for some standard operations:

Class

Category

Operation

Word / Phrase

Communication

Send data

TransmitDataFormat

Receive data

ReceiveDataFormat

Fix data alignment

SetDataWidth

Set Transfer length

SetDataLength

Abandon current transfer

Abort

Storage

Write Data Read Data

WriteDataFormat ReadDataFormat

Generic

Activation and

deactivation

Activate a feature or a mode

EnableItem

Deactivate a feature or a mode

DisableItem

Return activation state

IsEnabled

Bit handling

Clear a bit

ClearItem

Set a bit

SetItem

Read a bit (Boolean)

IsEnabledItem

IsActiveItem

Configuration

Set a single configuration value

SetItem

Set multiple configuration values

ConfigItem

Get a configuration value

GetItem

Specific action

Perform a Specific action on an item

ActionItem

Check whether an operation is done

IsOperation(verb in past tense)

Note

  1. Enable/Disable actions have no extra parameters beyond the instance, e.g., LL_ADC_Enable(ADCx).

  2. The Set action keyword is used to handle only one item at once, e.g., LL_SPI_SetMemDataWidth(SPIx, 32).

  3. For SetAction, additional indirect arguments could be used, but they are not configuration items, e.g., LL_USART_SetBaudRate(USART2, HCLK, LL_USART_BAUDRATE_9600); HCLK is used to calculate the BRR value but the configuration item is only LL_USART_BAUDRATE_9600.

  4. The Config keyword is used when several items are needed to make a full configuration, e.g., LL_RCC_PLL_Config(SRC, M, N, R).

Definitions:

Item

Description

Example

Item

Refer to the action relative to the configuration item to be updated.

LL_USART_SetBaudRate

Action/Operation

Refer to a specific operation to handle a specific feature:

  • Force

  • Release

  • Start

  • Stop

  • Reload

  • Request

  • Acknowledge

  • Generate

  • Update

  • Insert

  • Select

  • Ongoing

LL_ADC_StartConversion

Format

Refer to the data width, it is an integer value depending on the physical data width to be handled

LL_USART_TransmitData8

Operation

Refer to a specific operation state for a specific feature

LL_ADC_IsCalibrated

Names/identifiers have no more than 31 non-unique significant characters (MISRA C: 2012 Rule-5.1). This does not mean that names cannot be longer than 31 characters, just that any two or more identifiers are not identical in the first 31 characters. Any names/identifiers that exceed 31 significant characters are abbreviated.

When appropriate, all name/identifier words and tokens are abbreviated using the STM32Cube firmware abbreviation table (See STM32Cube Firmware Specification). Note that this rule covers only the second phrase part and not the action word; for example, for “EnableItem” only the “Item” part follows the abbreviation rule.

General naming rules

Item

Name

Example

File name

stm32tnxx_ll_ppp (h)

stm32tnxx_ll_adc.h

Function name format

LL_PPP{_CATEGORY}_Function (PPPx, ARGS)

LL_USART_SetBaudRate (USART2, HCLK, LL_USART_BAUDRATE_9600);

LL_RCC_HSI_Enable ()

MACRO

__LL_PPP{_CATEGORY}_ITEM (ARG1, ARG2….ARGN)

__LL_RTC_YEAR (RTC)

Define Format

LL_PPP{_CATEGORY}_ITEM{_VALUE}

LL_USART_BAUDRATE_9600

  • The PPP prefix refers to the peripheral name as defined in stm32tnxx reference manuals, e.g., ADC.

  • Registers are considered as constants. In most cases, their names are in uppercase and use the same acronyms as in the stm32tnxx reference manuals.

  • Peripheral registers are declared in the PPP_TypeDef structure (e.g., ADC_TypeDef) in the stm32tnxx.h header file. stm32tnxx.h corresponds to device-specific header files such as stm32f401xc.h, stm32f401xe.h, stm32f405xx.h, stm32f415xx.h, stm32f407xx.h, stm32f417xx.h, stm32f427xx.h, stm32f437xx.h, stm32f429xx.h, or stm32f439xx.h.

Functions naming rules

The format of the low layer functions is defined according to the following format:

__STATIC_INLINE return_type LL_PPP_Function (PPPx_TypeDef *PPPx, args)

The Function naming part is one of the following according to the action category:

  • Specific Interrupt, DMA request and status flags management:

Set/Get/Clear/Enable/Disable flags on interrupt and status registers

Name

Examples

LL_PPP_{_CATEGORY}_ActionItem_BITNAME LL_PPP{_CATEGORY}_IsItem_BITNAME_Action

LL_RCC_ClearFlag_FWRST()

LL_ADC_ClearFlag_EOC(ADC1)

LL_RCC_IsActiveFlag_FWRST()

LL_DMA_ClearFlag_TC(DMAx, channel)

The possible function formats are listed in the following table:

Item

Action

Format

Flag

Get

LL_PPP_IsActiveFlag_BITNAME

Clear

LL_PPP_ClearFlag_BITNAME

Interrupt

Enable

LL_PPP_EnableIT_BITNAME

Disable

LL_PPP_DisableIT_BITNAME

Get

LL_PPP_IsEnabledIT_BITNAME

DMA

Enable

LL_PPP_EnableDMAReq_BITNAME

Disable

LL_PPP_DisableDMAReq_BITNAME

Get

LL_PPP_IsEnabledDMAReq_BITNAME

Note

  • The BITNAME matches the naming as defined in the STM32 product documentation.

  • Remove the letter corresponding to Interrupt or Flag bit handling actions from the BITNAME, as specified in the STM32 documentation (e.g., rename LSIRDYF to LSIRDY).

  • If removing the letter results in a non-significant or unclear short name, replace the flag BITNAME with an explicit abbreviated name (e.g., use UPDATE instead of UIE).

  • When handling multiple interrupt and status flags, it is recommended to use the exported macros from the CMSIS files, such as:

    • STM32_SET_BIT(REG, BIT)

    • STM32_CLEAR_BIT(REG, BIT)

    • STM32_READ_BIT(REG, BIT)

    • STM32_CLEAR_REG(REG)

    • STM32_WRITE_REG(REG, VAL)

    • STM32_READ_REG(REG)

    • STM32_MODIFY_REG(REG, CLEARMASK, SETMASK)

    • STM32_POSITION_VAL(VAL)

  • Peripherals clock activation/deactivation management: Enable/Disable/Reset a peripheral clock

Name

Example

LL_BUS_GRPx_ActionClock{Mode}

LL_AHB2_EnableClock (LL_AHB2_GPIOA | LL_AHB2_GPIOB)

LL_APB1_EnableClockSleep (LL_APB1_DAC1)

x: corresponding to group index, it refers to the index of modified register on a bus.

  • Peripherals activation/deactivation management: Enable/Disable a peripheral or activate/Deactivate specific peripheral features

Name

Example

LL_PPP{_CATEGORY}_Action{Item}

LL_PPP{_CATEGORY}_IsItemAction

LL_ADC_Enable()

LL_ADC_EnableCalibration()

LL_ADC_IsCalibrated()

LL_RCC_HSI_Enable()

LL_RCC_HSI_IsReady()

  • Peripheral configuration management: Set/Get a peripheral configuration value

Name

Example

LL_PPP{_CATEGORY}_Set{ or Get}ConfigItem

LL_USART_SetBaudRate (USART2, Clock, LL_USART_BAUDRATE_9600)

  • Peripheral register’s management: Write/Read a register or return DMA relative register address

Name

LL_PPP_WRITE_REG(instance, reg, value)

LL_PPP_READ_REG(instance, reg)

LL_PPP_DMA_GET_REG_ADDR( PPP_TypeDef *p_ppp, {Sub Instance if any, e.g., Channel}, {uint32_t Property})

Property: Variable used to identify a register. It can be direction, data register type, etc.

Macros naming rules

  • Helper operations: formatting of an argument to fit register bitfields values

Name

Example

LL_PPP{_CATEGORY}_ITEM (ARG1, ARG2….ARGN)

LL_RTC_YEAR(RTC)

Note

Helper macros are only used for values computation or conversion or register offset calculation for internal driver use (for more information refer to the “Functions definition rules” section).

Literal naming rules

Name

Example

LL_PPP{_CATEGORY}_ITEM_VALUE

LL_RCC_HSE_ADCCLKSOURCE_DIVx

LL_USART_BAUDRATE_9600

LL_PPP{_CATEGORY}_ITEM

LL_I2C_OVRF

Functions definition rules

The Low Layer drivers are designed to provide a basic set of optimized, one-shot services. In the application, these services are called following the programming model of the reference manual. The function scope, unlike the HAL, is not based on high-level processes but on basic operations on core and peripheral registers. The definition of the low-layer APIs targets both a friendly user API concept and optimized register access.

The following rules provide the necessary recommendations to define the APIs for a specific peripheral:

  1. Interrupts, DMA requests and status flags are always coded with only one (or two, in case of modify operation) register operation and defined according to the following format:

Item

Action

Format

Flag

Get

LL_PPP_IsActiveFlag_BITNAME

Clear

LL_PPP_ClearFlag_BITNAME

Interrupt

Enable

LL_PPP_EnableIT_BITNAME

Disable

LL_PPP_DisableIT_BITNAME

Get

LL_PPP_IsEnabledIT_BITNAME

DMA

Enable

LL_PPP_EnableDMAReq_BITNAME

Disable

LL_PPP_DisableDMAReq_BITNAME

Get

LL_PPP_IsEnabledDMAReq_BITNAME

Examples:

__STATIC_INLINE void LL_USART_ClearFlag_PE(USART_TypeDef *p_usart)
{
  WRITE_REG(p_usart->ICR, USART_ICR_PECF);
}

__STATIC_INLINE uint32_t LL_USART_IsEnabledIT_PE(USART_TypeDef *p_usart)
{
  return ((READ_BIT(p_usart->CR1, USART_CR1_PEIE) == (USART_CR1_PEIE)) ? 1UL : 0UL);
}

2. Identify peripheral functional blocks and related services and features. A service provides a standalone action on a feature even if it can be used or modified by another block. The service is then associated with unitary “Get” operations to be able to restore already set items when only configuration has to be updated or changed.

Standalone services:

LL_RCC_PLL_ConfigDomain_SYS(uint32_t source, uint32_t pll_m, uint32_t pll_n, uint32_t pll_r)
LL_RCC_PLL_ConfigDomain_SAI(uint32_t source, uint32_t pll_m, uint32_t pll_n, uint32_t pll_p)
LL_RCC_PLL_ConfigDomain_48M(uint32_t source, uint32_t pll_m, uint32_t pll_n, uint32_t pll_q)

Unitary services:

LL_RCC_PLL_GetDivider(void)
LL_RCC_PLL_GetMainSource(void)
LL_RCC_PLL_GetN(void)
LL_RCC_PLL_GetQ(void)
// etc.

In user code, when an item is updated, for instance SRC field in the example below, the unitary “Get” operations could be used to restore already set values:

LL_RCC_PLL_ConfigDomain_SYS(src, LL_RCC_PLL_GetDivider(), pll_n, LL_RCC_PLL_GetPLLR())

3. Same scope low layer APIs are defined in a homogeneous way and have the same action/item naming, even if the two peripherals do not belong to the same peripheral class (for instance, Communication peripheral). Example: In the DMA LL driver, the LL_DMA_SetMemDataWidth() function is used to set the DMA data width, and the same feature is present in the SPI peripheral, so it is recommended to use the same naming as follows: LL_SPI_SetMemDataWidth()

  1. When a status flag or Interrupt or an activation field could be located in different registers following the sub-instance (channel, stream, etc.), it is recommended to calculate the addressed register offset through the sub-instance index.

Example:

__STATIC_INLINE void LL_PPP_ClearIT_XXX(PPP_TypeDef *p_ppp, uint32_t sub_index)
{
    WRITE_REG(p_ppp->REG + CALC_OFFSET(sub_index), PPP_REG_XXXX)
}

5. When there is no direct mathematic formula to calculate the offset, or no additional timing due to the offset computations, it is recommended to define a static constant table (LUT) to minimize the processing time in the LL functions. Note that MISRA C rules are applied to prevent cast issues.

__STATIC_INLINE void LL_PPP_Action(PPP_TypeDef *p_ppp, uint32_t sub_index)
{
    WRITE_REG(p_ppp->REG + OFFSET_TAB[sub_index], PPP_REG_XXXX)
}

Example:

__STATIC_INLINE void LL_DMA_DisableStream(DMA_TypeDef *p_dma, uint32_t Stream)
{
    uint32_t dma_base_addr = (uint32_t)p_dma;
    CLEAR_BIT(((DMA_Stream_TypeDef *)(dma_base_addr + LL_DMA_STR_OFFSET_TAB[Stream]))->CR, DMA_SxCR_EN);
}
  1. Rule 5 could be applied as well on the status flag or Interrupt or an activation field bit definition:

__STATIC_INLINE void LL_PPP_ClearIT_XXX(PPP_TypeDef *p_ppp, uint32_t sub_index)
{
    WRITE_REG(p_ppp->REG, LL_PPP_OFFSET_TAB[sub_index])
}
  1. A complete register access sequence required to perform a standalone PPP peripheral functionality by referring to the PPP peripheral associated registers is to be always defined in a single LL function, e.g., LL_GPIO_LockPin().

__STATIC_INLINE void LL_GPIO_LockPin(GPIO_TypeDef* p_gpio, uint32_t pin_msk)
{
    WRITE_REG(p_gpio->LCKR, GPIO_LCKR_LCKK | pin_msk);
    WRITE_REG(p_gpio->LCKR, pin_msk);
    WRITE_REG(p_gpio->LCKR, GPIO_LCKR_LCKK | pin_msk);
    READ_REG(p_gpio->LCKR);
}

8. When a complete register access sequence is required to perform a standalone PPP1 peripheral functionality by referring to another PPP2 peripheral associated registers, the relative PPP1 Low Layer function is only cover the functionality part covered by the PPP1 registers only.

LL_PWR_EnableBkUpAccess();
(...)
LL_RTC_EnableInitMode();
  1. Timeout, error management, and flag querying within loops are always to be implemented at application level. The low-layer functions do not embed such statements internally.

// Ex: App.c
LL_RCC_HSE_Enable();
LL_RCC_HSE_EnableBypass();

while (true) /* Infinite loop */
{
    if (LL_RCC_GetSysClockSource() == LL_RCC_SYS_CLKSOURCE_STATUS_HSE)
    {
        /* Timeout management */
        /* Error management */
    }
}

10. A low-layer function A does not use statements that might be used by another function B when the statement can be used once within a global process. Example: The low layer function implementation below is wrong:

__STATIC_INLINE void LL_PPP_ActionA(void)
{
    SET_BIT(PPP->REG1, PPP_REG1_BITx);
    SET_BIT(PPP->REG2, PPP_REG1_BITy);
}

__STATIC_INLINE void LL_PPP_ActionB(void)
{
    SET_BIT(PPP->REG1, PPP_REG1_BITz);
    SET_BIT(PPP->REG2, PPP_REG1_BITy);
}

/* complete configuration in application */
LL_PPP_ActionA();
LL_PPP_ActionB();

The correct implementation is to split LL_PPP_ActionA() and LL_PPP_ActionB() into three functions as follows:

__STATIC_INLINE void LL_PPP_ActionA(void)
{
    SET_BIT(PPP->REG1, PPP_REG1_BITx);
}

__STATIC_INLINE void LL_PPP_ActionB(void)
{
    SET_BIT(PPP->REG1, PPP_REG1_BITz);
}

__STATIC_INLINE void LL_PPP_ActionC(void)
{
    SET_BIT(PPP->REG2, PPP_REG1_BITy);
}

/* complete configuration in application */
LL_PPP_ActionA();
LL_PPP_ActionB();
LL_PPP_ActionC();

Usage scenarios

The low layer is designed to be used in standalone mode or combined with HAL. It cannot be automatically used with the HAL for the same peripheral instance. When using the LL APIs for a specific instance, users can still use the HAL APIs for other instances. The main constraint is when the LL overwrites some registers that might not be mirrored in the HAL handles.

User application based on LL only

The low layer can be used without calling the HAL drivers’ services by simply including stm32tnxx_ll_ppp.h in the application files.

The LL APIs for a peripheral are called following the sequence recommended by the programming model in the reference manual of the used product. In this case, the HAL drivers associated with the peripheral in use can be removed from the workspace; however, the STM32Cube framework is still used as for the HAL drivers case (i.e., system file, startup file, and CMSIS is always included).

LL inclusion model

Note

The stm32_ll.h is provided as a template and includes all the LL driver header files of the given series. In the case of code generation, this file will contain only the selected LL driver components. Users can utilize the copied file as is, where all LL drivers of the given STM32 family are included, or customize it to keep only the required LL driver’s inclusion. This flexibility allows users to optimize their application by including only the necessary LL drivers, thereby reducing the code footprint and improving maintainability.

User application with mixed HAL and LL

Mixed use is allowed; however, some considerations are taken into account:

  • Configuration APIs:
    • It is not allowed to use the HAL configuration APIs and the combination of low-layer APIs for the same instance simultaneously. Some configuration parameters could impact the handle’s internal parameters, which are not visible to the LL APIs.

    • It is recommended to ensure the full configuration of the peripheral instance using the HAL by calling:
      • The HAL global configuration function.

      • Additional HAL sub-block configuration functions, if required.

      • Additional HAL feature configuration functions, if required.

  • Process APIs:
    • It is not allowed to use the HAL process APIs and the combination of low-layer APIs for the same instance simultaneously. Some HAL process functions could impact the handle’s internal parameters, which are not visible to the LL APIs.

  • Summary:
    • The recommendation is to use the HAL to configure the given peripheral instance without mixing it with the LL functions. If more granularity is needed in the process starting and control, use the LL functions. However, after starting to use the LL layer, it is not allowed to revert to using the HAL, as the LL actions may have an impact that is not represented or synchronized in the HAL PPP handle.

Note

  1. When process APIs are not used and the functionality is performed through the low layer, callbacks are not called. Post-processing or error management are handled by the user.

  2. When using the LL APIs for process operations, the IRQ handler HAL APIs cannot be called. The IRQ is implemented by the user. Each LL driver implements the required macros to read and clear associated interrupt flags.

Cohabitation with HAL

The low layer can be used without any constraint with all the HAL drivers that are not based on handle objects (such as RCC, Cortex, common HAL, flash, and GPIO). However, when using both HAL and Low Layer (LL) drivers together, users should be aware of the following considerations:

  • The Low Layer is intended for expert users with a strong understanding of hardware aspects.

  • HAL drivers provide a high-level abstraction and are based on standalone processes that do not require in-depth hardware knowledge of the peripheral.

Several HAL and LL cohabitation schemes are possible and are described below.

Initializing a peripheral using the Low layer

By default, when the HAL PPP initialization and configuration APIs need to be replaced by the LL APIs, the application is implemented entirely with LL APIs for the addressed instance, as the HAL processes cannot be used in conjunction with LL APIs.

In this scenario, the peripheral handle is no longer used, and therefore, it does not need to be allocated. Additionally, the HAL PPP IRQ handler should not be called within the PPP Interrupt Service Routines (PPPi_IRQHandler). It is the responsibility of the user application to write the required ISR based on the various LL functions that handle the interrupts and flags. Furthermore, the HAL callbacks are no longer useful, and synchronization with the application is ensured by the user’s ISR.

Control functions using the Low layer

The control operations generally aim to change, at run-time, the content of some registers that were initially set using the HAL_PPP_SetConfig or various HAL PPP feature APIs. If the HAL_PPP initialization and configuration APIs are used, and the user only needs to call LL APIs to update some features, it is recommended to use the HAL_PPP_SetItem , HAL_PPP_SetFeature , HAL_PPP_EnableFeature , or HAL_PPP_DisableFeature functions instead. These HAL unitary functions are built using the LL APIs and ensure proper synchronization with the handles (when needed).

Example:

  • To change the counter mode of the TIM, the following unitary function can be called:

hal_status_t HAL_TIM_SetCounterMode(const hal_tim_handle_t *htim,
                                    hal_tim_counter_mode_t counter_mode);

While the HAL_TIM_SetCounterMode() is defined as follows:

hal_status_t HAL_TIM_SetCounterMode(const hal_tim_handle_t *htim,
                                    hal_tim_counter_mode_t counter_mode)
{
  ASSERT_DBG_PARAM((htim != NULL));

  ASSERT_DBG_STATE(htim->global_state,
                   (HAL_TIM_STATE_INIT | HAL_TIM_STATE_IDLE));

  tim_t *p_tim = TIM_INSTANCE(htim);

  /* Ensure that the instance supports mode selection */
  ASSERT_DBG_PARAM(IS_TIM_COUNTER_MODE_SELECT_INSTANCE(p_tim));

  /* Check counter mode validity */
  ASSERT_DBG_PARAM(IS_TIM_COUNTER_MODE(counter_mode));

  LL_TIM_SetCounterMode(p_tim, (uint32_t)counter_mode);

  return HAL_OK;
}

And the LL_TIM_SetCounterMode() is defined as follows:

__STATIC_INLINE void LL_TIM_SetCounterMode(TIM_TypeDef *timx, uint32_t mode)
{
  MODIFY_REG(timx->CR1, (TIM_CR1_DIR | TIM_CR1_CMS), mode);
}

The HAL_TIM_SetCounterMode function ultimately redirects to the LL_TIM_SetCounterMode function after performing assertion checks. These assertion checks are entirely bypassed if the compilation flags USE_ASSERT_DBG_PARAM or USE_ASSERT_DBG_STATE are not set.

  • To disable the UART FIFO mode the following function can be called:

hal_status_t HAL_UART_DisableFifoMode(hal_uart_handle_t *huart)

While the HAL_UART_DisableFifoMode() is defined as follows:

hal_status_t HAL_UART_DisableFifoMode(hal_uart_handle_t *huart)
{
  USART_TypeDef *p_uartx;

  ASSERT_DBG_PARAM(huart != NULL);
  p_uartx = (USART_TypeDef *)((uint32_t)huart->instance);
  ASSERT_DBG_PARAM(IS_UART_FIFO_INSTANCE((USART_TypeDef *)(uint32_t)huart->instance));
  ASSERT_DBG_STATE(huart->global_state, HAL_UART_STATE_CONFIGURED);
  ASSERT_DBG_STATE(huart->rx_state, HAL_UART_RX_STATE_IDLE);
  ASSERT_DBG_STATE(huart->tx_state, HAL_UART_TX_STATE_IDLE);

  UART_ENSURE_INSTANCE_DISABLED(p_uartx);

  LL_USART_DisableFIFO(p_uartx);

  huart->fifo_mode = HAL_UART_FIFO_MODE_DISABLED;

  /* Update Tx and Rx numbers of data to process */
  UART_SetNbDataToProcess(huart);

  UART_ENSURE_INSTANCE_ENABLED(p_uartx);

  return HAL_OK;
}

And the LL_USART_DisableFIFO() is defined as follows:

__STATIC_INLINE void LL_USART_DisableFIFO(USART_TypeDef *p_usart)
{
  CLEAR_BIT(p_usart->CR1, USART_CR1_FIFOEN);
}

The HAL_UART_DisableFifoMode function ensures that the LL_USART_DisableFIFO function is called to effectively disable the FIFO at the register level. Additionally, it updates the handle’s impacted parameters, such as fifo_mode, and adjusts the number of data items to process through the internal function UART_SetNbDataToProcess .

Using HAL operation APIs with the Low layer

The HAL I/O operations APIs are generally provided in three models: - Blocking Model (Polling) - Interrupt Model - DMA Model

If one of the above model APIs needs to be replaced by the LL APIs, it is not necessary to replace the other model APIs. Customized I/O operations APIs do not require the use of LL APIs for the initialization and configuration provided by HAL.

For DMA and Interrupt model APIs, when customized by the LL, the associated HAL callbacks cannot be used for the addressed instance.

If I/O operations are customized and the user wants to continue using the HAL_PPP_GetState() and HAL_PPP_GetLastErrorCodes() APIs, the customized processes update the state and error fields accordingly.

This approach can be used to customize all or some of the HAL processes, particularly to improve the performance of time-critical operations. The processes to be customized should have no dependency on other processes. For example, you can customize the Transmit APIs with the LL while keeping the Receive APIs with the HAL.