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.
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) |
|
The LL UTILS driver contains a set of generic APIs that can be used for:
|
|
|
The LL SYSTEM driver contains a set of generic APIs that can be used to:
|
|
|
Peripheral drivers |
|
This is the h-source file for core bus control and peripheral clock activation and deactivation Example: LL_AHB2_GRP1_EnableClock |
|
|
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
|
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:
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 |
|
|
p_ppp |
peripheral instance (not required for system peripherals) |
|
|
args |
functions’ arguments (can be omitted or with more arguments) |
|
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
In the LL layer, enumerated types are not allowed to minimize ORed definition conflict.
- Defines are aligned to the HAL literals and enumerated types to ease redefinition. Examples:
In the HAL, HAL_GPIO_PIN_0 is a define redirected to the equivalent define in the low layer GPIO driver.
In the HAL, the GPIO mode is an enumeration, the possible values are redirected to their equivalent in the low layer GPIO driver.
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;
The following peripherals do not require to be instantiated:
|
Peripherals (IPs) |
|
|---|---|
|
System |
FLASH/NVM DBGMCU EXTI PWR RCC SYSCFG/SBS RTC TAMP |
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
Enable/Disable actions have no extra parameters beyond the instance, e.g., LL_ADC_Enable(ADCx).
The Set action keyword is used to handle only one item at once, e.g., LL_SPI_SetMemDataWidth(SPIx, 32).
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.
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:
|
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.hheader file.stm32tnxx.hcorresponds to device-specific header files such asstm32f401xc.h,stm32f401xe.h,stm32f405xx.h,stm32f415xx.h,stm32f407xx.h,stm32f417xx.h,stm32f427xx.h,stm32f437xx.h,stm32f429xx.h, orstm32f439xx.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
LSIRDYFtoLSIRDY).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
UPDATEinstead ofUIE).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:
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()
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);
}
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])
}
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();
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).
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
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.
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.