最新项目中需要使用 STM32L476 的片子。在选择片子时,资源的多少成为了一个比较重要的考量。在斟酌一番之后,我决定采用 LL 库来实现本次的功能。下面就以 STM32L476 为例来介绍一下 LL 库(low-layer drivers)。下面是ST 中文官网上一篇《关于ST库函数的代码性能对比》的文章中对比了各种库的性能的图示:
关于 ST 各种库的介绍,可以参见博文《STM32 之一 HAL库、标准外设库、LL库(STM32 Embedded Software)》
LL 库一直是与 Cube HAL 库捆绑发布的。我们可以自己从 ST 官网下载对应的 Cube 包 STM32CubeL4 ,也可以直接在 CubeMX 中下载。对应的文档也是和 HAL 库在同一个文档中。名为 UM1884:Description of STM32L4/L4+ HAL and low-layer drivers,这里就不演示如何下载了。本次我们只需要关系文档中的 LL 库相关的章节即可。
LL 库旨在提供快速轻巧的面向专家的层,其比 HAL 库更接近硬件。 与 HAL 相反,LL API 不是提供给优化访问不是关键功能的外围设备或需要繁重的软件配置和/或复杂的上层协议栈(例如 FSMC,USB 或 SDMMC)。
在设计上,LL 库的 API 旨在用于独立模式或与 HAL 库结合使用。 不过它们不能与 HAL库同时用于相同的外设实例。如果您将 LL API 用于特定的外设实例,那么您仍然可以将 HAL API 用于其他外设实例。注意,LL API 可能会覆盖一些寄存器,这些寄存器的内容被映射到 HAL 句柄中。
LL 库的一大特点就是巧妙的运用 C 语言的静态、内联函数来直接操作寄存器。你会在 LL 库 .h 文件中发现大量的类似的静态内联函数。也正是因此,在 LL 库中,只有少数函数接口是放在 .c 文件中的。
所有的外设接口都是一个模板,下面以 ADC 为例来简单说明一下。每个 .h 文件的内容从前到后,大都可以分为四大部分:
代表各寄存器位或者参数的常量值(#define),例如 ADC 通道的定义:
#define LL_ADC_CHANNEL_0 (ADC_CHANNEL_0_NUMBER | ADC_CHANNEL_0_SMP | ADC_CHANNEL_0_BITFIELD ) /*!< ADC external channel (channel connected to GPIO pin) ADCx_IN0 */
寄存器读写宏函数以及一些基本运算宏函数。其中,寄存器读写宏函数是所有内联函数的基础。
/**
* @brief Write a value in ADC register
* @param __INSTANCE__ ADC Instance
* @param __REG__ Register to be written
* @param __VALUE__ Value to be written in the register
* @retval None
*/
#define LL_ADC_WriteReg(__INSTANCE__, __REG__, __VALUE__) WRITE_REG(__INSTANCE__->__REG__, (__VALUE__))
/**
* @brief Read a value in ADC register
* @param __INSTANCE__ ADC Instance
* @param __REG__ Register to be read
* @retval Register value
*/
#define LL_ADC_ReadReg(__INSTANCE__, __REG__) READ_REG(__INSTANCE__->__REG__)
函数参数结构体以及静态内联函数。LL 绝大多数函数即可都是静态内联函数。使用 LL 库,我们绝大多数都是使用这些内联函数即可。例如:
typedef struct
{
uint32_t Resolution; /*!< Set ADC resolution.
This parameter can be a value of @ref ADC_LL_EC_RESOLUTION
This feature can be modified afterwards using unitary function @ref LL_ADC_SetResolution(). */
uint32_t DataAlignment; /*!< Set ADC conversion data alignment.
This parameter can be a value of @ref ADC_LL_EC_DATA_ALIGN
This feature can be modified afterwards using unitary function @ref LL_ADC_SetDataAlignment(). */
uint32_t LowPowerMode; /*!< Set ADC low power mode.
This parameter can be a value of @ref ADC_LL_EC_LP_MODE
This feature can be modified afterwards using unitary function @ref LL_ADC_SetLowPowerMode(). */
} LL_ADC_InitTypeDef;
/**
* @brief Set ADC resolution.
* Refer to reference manual for alignments formats
* dependencies to ADC resolutions.
* @note On this STM32 serie, setting of this feature is conditioned to
* ADC state:
* ADC must be disabled or enabled without conversion on going
* on either groups regular or injected.
* @rmtoll CFGR RES LL_ADC_SetResolution
* @param ADCx ADC instance
* @param Resolution This parameter can be one of the following values:
* @arg @ref LL_ADC_RESOLUTION_12B
* @arg @ref LL_ADC_RESOLUTION_10B
* @arg @ref LL_ADC_RESOLUTION_8B
* @arg @ref LL_ADC_RESOLUTION_6B
* @retval None
*/
__STATIC_INLINE void LL_ADC_SetResolution(ADC_TypeDef *ADCx, uint32_t Resolution)
{
MODIFY_REG(ADCx->CFGR, ADC_CFGR_RES, Resolution);
}
/**
* @brief Get ADC resolution.
* Refer to reference manual for alignments formats
* dependencies to ADC resolutions.
* @rmtoll CFGR RES LL_ADC_GetResolution
* @param ADCx ADC instance
* @retval Returned value can be one of the following values:
* @arg @ref LL_ADC_RESOLUTION_12B
* @arg @ref LL_ADC_RESOLUTION_10B
* @arg @ref LL_ADC_RESOLUTION_8B
* @arg @ref LL_ADC_RESOLUTION_6B
*/
__STATIC_INLINE uint32_t LL_ADC_GetResolution(ADC_TypeDef *ADCx)
{
return (uint32_t)(READ_BIT(ADCx->CFGR, ADC_CFGR_RES));
}
配合 .c 的函数。这部分主要是一些函数体较长不适合作为内联函数的函数接口。这部分接口是封装的一些常用的操作。例如,我们在操作 RTC 时的流程:
我们可以使用 stm32l4xx_ll_rtc.h
中的各内联函数实现以上流程来字节实现一个写时间或者日期的接口。同时,stm32l4xx_ll_rtc.c
ST也为我们提供封装好的接口ErrorStatus LL_RTC_DATE_Init(RTC_TypeDef *RTCx, uint32_t RTC_Format, LL_RTC_DateTypeDef *RTC_DateStruct)
和 ErrorStatus LL_RTC_TIME_Init(RTC_TypeDef *RTCx, uint32_t RTC_Format, LL_RTC_TimeTypeDef *RTC_TimeStruct)
。至于如何取舍就看自己了!
但是需要注意:这部分接口仅仅封装了很少的一部分! 大多数我们还是需要自己根据寄存器操作流程来封装自己的接口!例如,读取 RTC 时间或者日期,需要先检查并等待 RTC_ISR 寄存器中的 RSF 位置 1 时才可以读取(影子寄存器模式下)。
LL 库围绕 .h / .c 文件构建,每个受支持的外围设备一个独立的文件,外加上五个与某些系统和 Cortex 相关功能的头文件。具体如下:
除了以上这些文件以外,LL 库 和 HAL 库共享一部分文件。这部分文件位于 CMSIS 中。主要是对于 MCU 中寄存器的封装定义,如下图所示:
红色框中的文件是与 HAL 库共享的。上图中的其他几个文件原则上来说数据用户层文件,不属于库文件。下图显示了 LL 库文件的包含关系:
通常来说,其只会包含 CMSIS 中的两个文件。
LL 库的使用既可以独立使用也可以和 HAL 库混合使用。 但是统一外设不能即使用 HAL 库,又使用 LL 库。例如,不可使用 HAL 库初始化外设,然后使用 LL 库读写外设。配套文档中有专门的两个章节介绍:
总体来说,LL 库的手动移植与标准外设库基本一致,就时钟源配置有些区别!先来看看移植之后的最终结果图:
下面我们一步一步来介绍一下如何手动移植。
首先我们需要从 ST 给的对应的芯片软件包里提取出需要的库文件(如果你不在意项目中一堆无用的文件的话,可以不提取),具体需要的库文件如下:
当然我们只需要复制一些必须的文件,杜绝出现一堆无用的文件,全部库文件如下:
根据 ST 驱动开发架构,我们的用户层文件中必须包含几个特定的文件。由于这几个文件需要根据用户的功能而变化,因此他们并不属于库文件,而属于用户层文件。具体如下:
我们只需要从任意例子中查到这几个文件就可以,后面我们需要更加自己的需要修改里面的内容,首要的是删除里面的不需要的内容。复制到自己的用户层目录中后,我一般会根据我的程序架构,调整里面的内容,删除不需要的东西。基本我会根据我的程序架构,将这几个文件的内容全部重整。
其中需要注意的是 stm32_assert.h
这个文件。这个文件是库文件源码目录中的 stm32_assert_template.h
复制到用户目录中并更名的!这是 LL 库唯一一个需要用户处理的文件。
LL 库相比于 HAL 库 和 标准外设库,一个特点就是没有了库的配置文件。但是 CMSIS 中要求的配置以及 MCU 需要的配置还是必须的!
这里有个配置技巧,ST的文件中也都有说明,就是将配置项配置到自己的编译工具链中,从而避免修改各个配置文件导致移植时的麻烦。以 MDK-ARM 为例,我们可以把配置项如下配置:
其他 IDE 的配置基本类似,这里就不一一说明了。下面我们具体来说明一下 LL 库的使用需要进行哪些配置。
在 LL 库中,默认使用了断言(assert),断言的定义位于文件 stm32_assert.h
中。如果要使用默认的断言,则该文件要求,在自己的编译工具链中定义宏值 USE_FULL_ASSERT
。注意,如果与 HAL 库混合使用,可能会导致重定义(HAL 库中也存在默认断言的定义)。使用时自己注意一下!
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef STM32_ASSERT_H
#define STM32_ASSERT_H
#ifdef __cplusplus
extern "C" {
#endif
/* Exported types ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
/* Includes ------------------------------------------------------------------*/
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function
* which reports the name of the source file and the source
* line number of the call that failed.
* If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((char *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(char *file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
#ifdef __cplusplus
}
#endif
#endif /* STM32_ASSERT_H */
LL 库本身支持多个系列的 MCU,我们必须要配置使用的芯片型号。这是 CMSIS 中的 stm32l4xx.h
文件中要求的。如下是未配置之前的原文件部分内容:
/** @addtogroup Library_configuration_section
* @{
*/
/**
* @brief STM32 Family
*/
#if !defined (STM32L4)
#define STM32L4
#endif /* STM32L4 */
/* Uncomment the line below according to the target STM32L4 device used in your
application
*/
#if !defined (STM32L412xx) && !defined (STM32L422xx) && \
!defined (STM32L431xx) && !defined (STM32L432xx) && !defined (STM32L433xx) && !defined (STM32L442xx) && !defined (STM32L443xx) && \
!defined (STM32L451xx) && !defined (STM32L452xx) && !defined (STM32L462xx) && \
!defined (STM32L471xx) && !defined (STM32L475xx) && !defined (STM32L476xx) && !defined (STM32L485xx) && !defined (STM32L486xx) && \
!defined (STM32L496xx) && !defined (STM32L4A6xx) && \
!defined (STM32L4P5xx) && !defined (STM32L4Q5xx) && \
!defined (STM32L4R5xx) && !defined (STM32L4R7xx) && !defined (STM32L4R9xx) && !defined (STM32L4S5xx) && !defined (STM32L4S7xx) && !defined (STM32L4S9xx)
/* #define STM32L412xx */ /*!< STM32L412xx Devices */
/* #define STM32L422xx */ /*!< STM32L422xx Devices */
/* #define STM32L431xx */ /*!< STM32L431xx Devices */
/* #define STM32L432xx */ /*!< STM32L432xx Devices */
/* #define STM32L433xx */ /*!< STM32L433xx Devices */
/* #define STM32L442xx */ /*!< STM32L442xx Devices */
/* #define STM32L443xx */ /*!< STM32L443xx Devices */
/* #define STM32L451xx */ /*!< STM32L451xx Devices */
/* #define STM32L452xx */ /*!< STM32L452xx Devices */
/* #define STM32L462xx */ /*!< STM32L462xx Devices */
/* #define STM32L471xx */ /*!< STM32L471xx Devices */
/* #define STM32L475xx */ /*!< STM32L475xx Devices */
/* #define STM32L476xx */ /*!< STM32L476xx Devices */
/* #define STM32L485xx */ /*!< STM32L485xx Devices */
/* #define STM32L486xx */ /*!< STM32L486xx Devices */
/* #define STM32L496xx */ /*!< STM32L496xx Devices */
/* #define STM32L4A6xx */ /*!< STM32L4A6xx Devices */
/* #define STM32L4P5xx */ /*!< STM32L4Q5xx Devices */
/* #define STM32L4R5xx */ /*!< STM32L4R5xx Devices */
/* #define STM32L4R7xx */ /*!< STM32L4R7xx Devices */
/* #define STM32L4R9xx */ /*!< STM32L4R9xx Devices */
/* #define STM32L4S5xx */ /*!< STM32L4S5xx Devices */
/* #define STM32L4S7xx */ /*!< STM32L4S7xx Devices */
/* #define STM32L4S9xx */ /*!< STM32L4S9xx Devices */
#endif
/* Tip: To avoid modifying this file each time you need to switch between these
devices, you can define the device in your toolchain compiler preprocessor.
*/
通常,我们是把需要的芯片类型定义到自己的编译工具链中,而不是去修改该文件!
STM32 系列的 MCU 都有一个很丰富的时钟源配置功能,满足用户的各种需求。时钟的配置是用户层文件 system_stm32l4xx.c
必须的。该文件中有如下内容:
#if !defined (HSE_VALUE)
#define HSE_VALUE 8000000U /*!< Value of the External oscillator in Hz */
#endif /* HSE_VALUE */
#if !defined (MSI_VALUE)
#define MSI_VALUE 4000000U /*!< Value of the Internal oscillator in Hz*/
#endif /* MSI_VALUE */
#if !defined (HSI_VALUE)
#define HSI_VALUE 16000000U /*!< Value of the Internal oscillator in Hz*/
#endif /* HSI_VALUE */
通常,我们是把使用的时钟源类型及频率定义到自己的编译工具链中,而不是去修改该文件!
选择了时钟源之后,我们必须要对 MCU 进行配置,以启动切换到选择的时钟源。通常在 main
函数的一开始就必须要先配置时钟源。在 ST 给的例子中的 main
函数中,就有配置的函数
/**
* @brief System Clock Configuration
* The system Clock is configured as follows :
* System Clock source = PLL (MSI)
* SYSCLK(Hz) = 80000000
* HCLK(Hz) = 80000000
* AHB Prescaler = 1
* APB1 Prescaler = 1
* APB2 Prescaler = 1
* MSI Frequency(Hz) = 4000000
* PLL_M = 1
* PLL_N = 40
* PLL_R = 2
* Flash Latency(WS) = 4
* @param None
* @retval None
*/
void SystemClock_Config(void)
{
/* MSI configuration and activation */
LL_FLASH_SetLatency(LL_FLASH_LATENCY_4);
LL_RCC_MSI_Enable();
while(LL_RCC_MSI_IsReady() != 1)
{
};
/* Main PLL configuration and activation */
LL_RCC_PLL_ConfigDomain_SYS(LL_RCC_PLLSOURCE_MSI, LL_RCC_PLLM_DIV_1, 40, LL_RCC_PLLR_DIV_2);
LL_RCC_PLL_Enable();
LL_RCC_PLL_EnableDomain_SYS();
while(LL_RCC_PLL_IsReady() != 1)
{
};
/* Sysclk activation on the main PLL */
LL_RCC_SetAHBPrescaler(LL_RCC_SYSCLK_DIV_1);
LL_RCC_SetSysClkSource(LL_RCC_SYS_CLKSOURCE_PLL);
while(LL_RCC_GetSysClkSource() != LL_RCC_SYS_CLKSOURCE_STATUS_PLL)
{
};
/* Set APB1 & APB2 prescaler*/
LL_RCC_SetAPB1Prescaler(LL_RCC_APB1_DIV_1);
LL_RCC_SetAPB2Prescaler(LL_RCC_APB2_DIV_1);
/* Set systick to 1ms in using frequency set to 80MHz */
/* This frequency can be calculated through LL RCC macro */
/* ex: __LL_RCC_CALC_PLLCLK_FREQ(__LL_RCC_CALC_MSI_FREQ(LL_RCC_MSIRANGESEL_RUN, LL_RCC_MSIRANGE_6),
LL_RCC_PLLM_DIV_1, 40, LL_RCC_PLLR_DIV_2)*/
LL_Init1msTick(80000000);
/* Update CMSIS variable (which can be updated also through SystemCoreClockUpdate function) */
LL_SetSystemCoreClock(80000000);
}
我们必须要根据自己的需要修改该函数!!!
注意,在函数的最后,必须要调用一下函数 void SystemCoreClockUpdate(void)
,否则,必须要手动修改 system_stm32l4xx.c
文件中的如下这些全局变量:
/** @addtogroup STM32L4xx_System_Private_Variables
* @{
*/
/* The SystemCoreClock variable is updated in three ways:
1) by calling CMSIS function SystemCoreClockUpdate()
2) by calling HAL API function HAL_RCC_GetHCLKFreq()
3) each time HAL_RCC_ClockConfig() is called to configure the system clock frequency
Note: If you use this function to configure the system clock; then there
is no need to call the 2 first functions listed above, since SystemCoreClock
variable is updated automatically.
*/
uint32_t SystemCoreClock = 4000000U;
const uint8_t AHBPrescTable[16] = {0U, 0U, 0U, 0U, 0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U, 6U, 7U, 8U, 9U};
const uint8_t APBPrescTable[8] = {0U, 0U, 0U, 0U, 1U, 2U, 3U, 4U};
const uint32_t MSIRangeTable[12] = {100000U, 200000U, 400000U, 800000U, 1000000U, 2000000U, \
4000000U, 8000000U, 16000000U, 24000000U, 32000000U, 48000000U};
/**
* @}
*/
在使用了在线升级(IAP)时,通常我们的程序需要分为两部分,即在实际功能程序前必须有个额外的程序来处理升级。我们的实际功能程序就必须要有偏移。这个配置项也是用户层文件 system_stm32l4xx.c
必须的。该文件中有如下内容:
/************************* Miscellaneous Configuration ************************/
/*!< Uncomment the following line if you need to relocate your vector Table in
Internal SRAM. */
/* #define VECT_TAB_SRAM */
#define VECT_TAB_OFFSET 0x00 /*!< Vector Table base offset field.
This value must be a multiple of 0x200. */
/******************************************************************************/
这个项不能配置到编译工具链中,只能修改本文件!
最后,如果要完整使用 LL 库,LL 库要求必须要定义全局宏值 USE_FULL_LL_DRIVER
。
至此,使用LL库的全部文件已经整理完成,接下来就是根据自己的功能修改代码即可!
现在 CubeMX 生成代码时,可以直接选择 LL 库。但是根据我之前的测试,其生成的代码比较简单,多半还需要自己再进行完善!具体如下:
直接生成没啥意思,生成的 LL 库的代码,仍然需要大量自己修改!
通过 LL 库的基本架构,我们不难发现,LL 就是通过内联函数的形式封装了一下对于寄存器的读写。用户必须要自己掌握各外设的操作流程。例如,在 HAL 库或者 SPL 库中,外设的初始化都有专门的接口,用户甚至不需要关心外设寄存器中,哪个配置项在前哪个在后。但是到了 LL 库中,用户必须知道外设配置项的先后操作顺序。
举个例子来具体说明一下。RTC 时间或者日期的设置流程为:
在 HAL 库中,写 RTC 时间函数接口 HAL_StatusTypeDef HAL_RTC_SetTime(RTC_HandleTypeDef *hrtc, RTC_TimeTypeDef *sTime, uint32_t Format);
的实现中已经完整封装好了以上流程,我们直接调用即可!而使用 LL 库我们不得不自己实现以上流程(使用 stm32l4xx_ll_rtc.h
中的内联函数来实现):
/** 设置时间
*
* @param[in] t 要设置的参数指针
* @retval 是否成功
**/
bool STM32RTC_SetTime(RTC_TIME *t)
{
if(!RTC_IS_BCD(t->ss) || RTC_BCD2DEC(t->ss) > RTC_ss_MAX
|| !RTC_IS_BCD(t->mm) || RTC_BCD2DEC(t->mm) > RTC_mm_MAX
|| !RTC_IS_BCD(t->hh) || RTC_BCD2DEC(t->hh) > RTC_hh_MAX)
{
return false;
}
LL_RTC_DisableWriteProtection(RTC);
LL_RTC_EnableInitMode(RTC);
/* 轮询 RTC_ISR 寄存器中的 INITF 位。当 INITF 置 1 时进入初始化阶段模式。大约需要 2 个 RTCCLK 时钟周期(由于时钟同步)。 */
while (LL_RTC_IsActiveFlag_INIT(RTC) != 1);
LL_RTC_TIME_Config(RTC, LL_RTC_TIME_FORMAT_AM_OR_24, t->hh, t->mm, t->ss);
LL_RTC_DisableInitMode(RTC);
LL_RTC_EnableWriteProtection(RTC);
return true;
}
当然,对于某些外设的常用操作,ST 也封装了一些常用的操作接口。这些接口就是位于LL库文件对应的 .c 文件中(区别于.h文件中的各内联函数)的各接口,但是这部分接口很少,大多数还是需要我们自己来封装需要的接口。例如以上流程,ST 就提供了一个封装好的函数 LL_RTC_TIME_Init(RTC_TypeDef *RTCx, uint32_t RTC_Format, LL_RTC_TimeTypeDef *RTC_TimeStruct);
来直接设置时间。其他外设基本也都还是这样。
注意:这部分接口并不可靠!不可靠!不可靠! 举个例子,如下图:
右侧为 RTC 中 闹铃 A 的初始化函数。乍一看没啥问题。但是,如果终端复位,闹铃仍然是使能的。在使能情况下,无法写各个闹铃相关的寄存器,这就意味着,复位初始化闹铃不会成功!对比左侧 HAL库,会先关闭闹铃相关的配置。