鸣涧 发表于 2022-7-20 22:50:46

学习STM32,从HAL库的框架设计开始

学习STM32,从HAL库的框架设计开始

文章出处:armfly


1 重要提示

学习使用HAL库前,有必要对他们的基本设计框架有所了解,然后深入学习,效果更好。

为了方便调用,HAL库为各种外设基本都配了三套API,查询,中断和DMA。

2 HAL库的配置文件

HAL库有一个专门的配置文件叫stm32h7xx_hal_conf.h,这个文件里面有一个很重要的参数,就是HSE_VALUE,大家所设计板子使用的实际晶振大小一定要与这个数值一致。比如V7的外置晶振是25MHz,那么这里就务必配置宏定义为:

#define HSE_VALUE    ((uint32_t)25000000)


完整的代码如下:

.    /* ########################## Module Selection ############################## */
.    /**
.      * @brief This is the list of modules to be used in the HAL driver
.      */
.    #define HAL_MODULE_ENABLED
.    #define HAL_ADC_MODULE_ENABLED
.    #define HAL_CEC_MODULE_ENABLED
.
.   /* 省略未写 */
.
.    /* ########################## Oscillator Values adaptation ####################*/
.    /**
.      * @brief Adjust the value of External High Speed oscillator (HSE) used in your application.
.      *      This value is used by the RCC HAL module to compute the system frequency
.      *      (when HSE is used as system clock source, directly or through the PLL).
.      */
.    #if !defined(HSE_VALUE)
.    #define HSE_VALUE    ((uint32_t)25000000) /*!< Value of the External oscillator in Hz */
.    #endif /* HSE_VALUE */
.
.    #if !defined(HSE_STARTUP_TIMEOUT)
.      #define HSE_STARTUP_TIMEOUT    ((uint32_t)5000)   /*!< Time out for HSE start up, in ms */
.    #endif /* HSE_STARTUP_TIMEOUT */
.
.    /**
.      * @brief Internaloscillator (CSI) default value.
.      *      This value is the default CSI value after Reset.
.      */
.    #if !defined(CSI_VALUE)
.      #define CSI_VALUE    ((uint32_t)4000000) /*!< Value of the Internal oscillator in Hz*/
.    #endif /* CSI_VALUE */
.
.    /**
.      * @brief Internal High Speed oscillator (HSI) value.
.      *      This value is used by the RCC HAL module to compute the system frequency
.      *      (when HSI is used as system clock source, directly or through the PLL).
.      */
.    #if !defined(HSI_VALUE)
.      #define HSI_VALUE    ((uint32_t)64000000) /*!< Value of the Internal oscillator in Hz*/
.    #endif /* HSI_VALUE */
.
.    /**
.      * @brief External Low Speed oscillator (LSE) value.
.      *      This value is used by the UART, RTC HAL module to compute the system frequency
.      */
.    #if !defined(LSE_VALUE)
.      #define LSE_VALUE    ((uint32_t)32768) /*!< Value of the External oscillator in Hz*/
.    #endif /* LSE_VALUE */
.
.
.    #if !defined(LSE_STARTUP_TIMEOUT)
.      #define LSE_STARTUP_TIMEOUT    ((uint32_t)5000)   /*!< Time out for LSE start up, in ms */
.    #endif /* LSE_STARTUP_TIMEOUT */
.
.    /**
.      * @brief External clock source for I2S peripheral
.      *      This value is used by the I2S HAL module to compute the I2S clock source
.      *      frequency, this source is inserted directly through I2S_CKIN pad.
.      */
.    #if !defined(EXTERNAL_CLOCK_VALUE)
.      #define EXTERNAL_CLOCK_VALUE    12288000U /*!< Value of the External clock in Hz*/
.    #endif /* EXTERNAL_CLOCK_VALUE */
.
.    /* Tip: To avoid modifying this file each time you need to use different HSE,
.       ===you can define the HSE value in your toolchain compiler preprocessor. */
.
.    /* ########################### System Configuration ######################### */
.    /**
.      * @brief This is the HAL system configuration section
.      */
.    #defineVDD_VALUE                  ((uint32_t)3300) /*!< Value of VDD in mv */
.    #defineTICK_INT_PRIORITY            ((uint32_t)0x0F) /*!< tick interrupt priority */
.    #defineUSE_RTOS                     0
.    /* #defineUSE_SD_TRANSCEIVER         1U   */            /*!< use uSD Transceiver */
.
.    /* ########################### Ethernet Configuration ######################### */
.    #define ETH_TX_DESC_CNT         4/* number of Ethernet Tx DMA descriptors */
.    #define ETH_RX_DESC_CNT         4/* number of Ethernet Rx DMA descriptors */
.
.    #define ETH_MAC_ADDR0    ((uint8_t)0x02)
.    #define ETH_MAC_ADDR1    ((uint8_t)0x00)
.    #define ETH_MAC_ADDR2    ((uint8_t)0x00)
.    #define ETH_MAC_ADDR3    ((uint8_t)0x00)
.    #define ETH_MAC_ADDR4    ((uint8_t)0x00)
.    #define ETH_MAC_ADDR5    ((uint8_t)0x00)
.
.    /* ########################## Assert Selection ############################## */
.    /**
.      * @brief Uncomment the line below to expanse the "assert_param" macro in the
.      *      HAL drivers code
.      */
.    /* #define USE_FULL_ASSERT    1 */
.
.
.    /* ################## SPI peripheral configuration ########################## */
.    /**
.      * @brief Used to activate CRC feature inside HAL SPI Driver
.      *      Activated   (1U): CRC code is compiled within HAL SPI driver
.      *      Deactivated (0U): CRC code excluded from HAL SPI driver
.      */
.
.    #define USE_SPI_CRC                   1U
.
.
.    /* Includes ------------------------------------------------------------------*/
.    /**
.      * @brief Include module's header file
.      */
.
.    #ifdef HAL_RCC_MODULE_ENABLED
.      #include "stm32h7xx_hal_rcc.h"
.    #endif /* HAL_RCC_MODULE_ENABLED */
.    /* 省略未写 */
.
.
.    /* Exported macro ------------------------------------------------------------*/
.    #ifdefUSE_FULL_ASSERT
.    /**
.      * @briefThe assert_param macro is used for function's parameters check.
.      * @paramexpr: 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)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
.    /* Exported functions ------------------------------------------------------- */
.      void assert_failed(uint8_t* file, uint32_t line);
.    #else
.      #define assert_param(expr) ((void)0)
.    #endif /* USE_FULL_ASSERT */
除了HSE_VALUE,下面几个也要作为了解:

第72行,滴答定时器的优先级设置。这个优先级的设置至关重要。因为HAL库中各个外设驱动里面的延迟实现是基于此文件提供的时间基准。

如果在中断服务程序里面调用基于此时间基准的延迟函数HAL_Delay要特别注意,因为这个函数的时间基准是基于滴答定时器或者其他通用定时器实现,实现方式是滴答定时器或者其他通用定时器里面对变量计数。如此以来,结果是显而易见的,如果其他中断服务程序调用了此函数,且中断优先级高于滴答定时器,会导致滴答定时器中断服务程序一直得不到执行,从而卡死在里面。所以滴答定时器的中断优先级一定要比他们高。

另外这个时间基准既可以使用滴答定时器实现也可以使用通用的定时器实现,默认情况下是用的滴答定时器。

第73行,当前HAL库还不支持RTOS方式。

第74行,STM32H7的SDIO外接支持UHS-I 模式 (SDR12, SDR25, SDR50, SDR104和DDR50)的SD卡,需要1.8的电平转换器,此选项就是来使能此功能用的。

第92行,用于使能断言功能,在HAL库的API里面都有用到,用来判断函数形参是否有效。默认情况下是关闭的。

第126行,使能断言功能后,实际对应的代码位置。这里没有对函数void assert_failed(uint8_t* file, uint32_t line)做具体实现,大家可以根据自己的需求去实现,这里提供一个参考:/*
    ST库函数使用了C编译器的断言功能,如果定义了USE_FULL_ASSERT,那么所有的ST库函数将检查函数形参
    是否正确。如果不正确将调用 assert_failed() 函数,这个函数是一个死循环,便于用户检查代码。
   
    关键字 __LINE__ 表示源代码行号。
    关键字__FILE__表示源代码文件名。
   
    断言功能使能后将增大代码大小,推荐用户仅在调试时使能,在正式发布软件是禁止。

    用户可以选择是否使能ST固件库的断言供能。使能断言的方法有两种:
    (1) 在C编译器的预定义宏选项中定义USE_FULL_ASSERT。
    (2) 在本文件取消"#define USE_FULL_ASSERT    1"行的注释。   
*/

/*
*********************************************************************************************************
*    函 数 名: assert_failed
*    形    参:file : 源代码文件名称。关键字__FILE__表示源代码文件名。
*            line :代码行号。关键字 __LINE__ 表示源代码行号
*    返 回 值: 无
*********************************************************************************************************
*/
void assert_failed(uint8_t* file, uint32_t line)
{
    /*
      用户可以添加自己的代码报告源代码文件名和代码行号,比如将错误文件和行号打印到串口
      printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
    */
   
    /* 这是一个死循环,断言失败时程序会在此处死机,以便于用户查错 */
    while (1)
    {
    }
}
3 HAL库的时间基准

为了方便各种外设延迟的实现,HAL库专门搞了一个时间基准,默认来源是滴答定时器,也可以通过重定向使用其他定时器实现。相关函数全部集中在stm32h7xx_hal.c文件里面实现,关于这些函数在本教程的第16章有详细讲解。

4 HAL库的启动流程

这里通过V7板子初始化流程做个说明:

.    /*
.    ******************************************************************************************************
.    *    函 数 名: bsp_Init
.    *    功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。
.    *            只需要调用一次
.    *    形    参:无
.    *    返 回 值: 无
.    ******************************************************************************************************
.    */
.    void bsp_Init(void)
.    {
.      /* 配置MPU */
.      MPU_Config();
.
.      /* 使能L1 Cache */
.      CPU_CACHE_Enable();
.
.      /*
.         STM32H7xx HAL 库初始化,此时系统用的还是H7自带的64MHz,HSI时钟:
.         - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。
.         - 设置NVIV优先级分组为4。
.         */
.      HAL_Init();
.
.      /*
.         配置系统时钟到400MHz
.         - 切换使用HSE。
.         - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。
.      */
.      SystemClock_Config();
.
.
.      bsp_InitKey();      /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */
.      bsp_InitTimer();      /* 初始化滴答定时器 */
.      bsp_InitUart();    /* 初始化串口 */
.      bsp_InitExtIO();    /* 初始化FMC总线74HC574扩展IO. 必须在 bsp_InitLed()前执行 */
.      bsp_InitLed();      /* 初始化LED */
.
.      bsp_InitLPTIMOutPWM(); /* 低功耗定时器PWM输出 */
.    }
第13 – 16行,配置MPU和使能Cache,对于H7而已,这两个函数要优先执行,因为Flash速度,SRAM速度跟CPU和TCM有些差距的,所以要使能Cache。而不同存储区的Cache策略要通过MPU分别进行配置。

第23行,调用函数HAL_Init时,系统依然使用的64MHz HSI时钟,这点要特别注意。此函数会调用函数HAL_InitTick,初始化滴答时钟中断1ms,并设置NVIV优先级分组为4。

这里就隐含了一个知识点,就是它会开启滴答定时器中断,如果用户也要使用滴答定时器中断,此问题就要引起注意,我们的bsp_timer.C文件解决办法如下:
/*
*********************************************************************************************************
*    函 数 名: SysTick_Handler
*    功能说明: 系统嘀嗒定时器中断服务程序。启动文件中引用了该函数。
*    形    参:无
*    返 回 值: 无
*********************************************************************************************************
*/
void SysTick_Handler(void)
{
    HAL_IncTick();    /* ST HAL库的滴答定时中断服务程序 */
   
    if (g_ucEnableSystickISR == 0) /* 做了一个变量标志,调用了函数bsp_InitTimer才置位此变量 */
    {
      return;
    }
   
    SysTick_ISR();    /* 安富莱bsp库的滴答定时中断服务程序 */
}
第30行,通过此函数切换HSI到外部高速时钟HSE,并配置系统时钟到400MHz。调用这个函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。

前面几步执行完毕后就可以初始化外设了。

5 HAL库初始化外设

HAL库为外设初始化提供了一套框架,这里以串口为例进行说明,调用函数HAL_UART_Init初始化串口,此函数就会调用HAL_UART_MspInit,这个函数是弱定义的,在stm32h7xx_hal_uart.c文件里面:
/**
* @brief Initialize the UART MSP.
* @param huart: UART handle.
* @retval None
*/

__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);

/* NOTE : This function should not be modified, when the callback is needed,
            the HAL_UART_MspInit can be implemented in the user file
   */
}
如果要初始化,直接将此函数在其它源文件里面实现即可,如果用到了中断和DMA,也是直接在这里填写。
void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
   
    if(huart->Instance==USART1)
    {
      __HAL_RCC_USART1_CLK_ENABLE();

      __HAL_RCC_GPIOB_CLK_ENABLE();

      GPIO_InitStruct.Pin = GPIO_PIN_7;
      GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
      GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
      HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

      GPIO_InitStruct.Pin = GPIO_PIN_14;
      GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
      GPIO_InitStruct.Alternate = GPIO_AF4_USART1;
      HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
    else if(huart->Instance==USART2)
    {
      __HAL_RCC_USART2_CLK_ENABLE();

      __HAL_RCC_GPIOD_CLK_ENABLE();
      __HAL_RCC_GPIOA_CLK_ENABLE();

      GPIO_InitStruct.Pin = GPIO_PIN_6;
      GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
      GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
      HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);

      GPIO_InitStruct.Pin = GPIO_PIN_2;
      GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
      GPIO_InitStruct.Pull = GPIO_NOPULL;
      GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
      GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
      HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    }
}
由于所有串口都是通过函数HAL_UART_Init做初始化,所以函数HAL_UART_MspInit也是共用的。因此需要大家在这个里面区分是配置的哪个串口。

6 HAL库的中断处理思路

为了加强对中断的管理,HAL库也为各种外设中断也配套一个函数,比如串口就是函数HAL_UART_IRQHandler。

void USART1_IRQHandler(void)
{
    /* USER CODE BEGIN USART1_IRQn 0 */
         此处可以添加用户代码
    /* USER CODE END USART1_IRQn 0 */

   /* 参数是串口句柄 */
    HAL_UART_IRQHandler(&huart1);

    /* USER CODE BEGIN USART1_IRQn 1 */
         此处可以添加用户代码
    /* USER CODE END USART1_IRQn 1 */
}
那么问题来了,如果要实现功能,用户的应用程序怎么写入中断?用户可以直接在函数HAL_UART_IRQHandler的前面或者后面添加新代码,也可以直接在HAL_UART_IRQHandler调用的各种回调函数里面执行,这些回调都是弱定义的,方便用户直接在其它文件里面重定义(下面回调主要是用于串口DMA时调用的):
HAL_UART_TxHalfCpltCallback()
HAL_UART_TxCpltCallback()
HAL_UART_RxHalfCpltCallback()
HAL_UART_RxCpltCallback()
HAL_UART_ErrorCallback()
7 HAL库的DMA处理思路

为了方便各种外设直接启动DMA,HAL库专门为支持DMA操作的外设都提供了对应的DMA函数,比如串口的:
HAL_UART_Transmit_DMA()
HAL_UART_Receive_DMA()
HAL_UART_DMAPause()
HAL_UART_DMAResume()
HAL_UART_DMAStop()
这里特别注意一点,针对外设的DMA函数基本都有开启中断,如果用户使能此外设的NVIC,使用中务必别忘了写DMA的中断服务程序,比如使用DMA1_Stream1:
void DMA1_Stream1_IRQHandler(void)
{
    /* USER CODE BEGIN USART1_IRQn 0 */
         此处可以添加用户代码
    /* USER CODE END USART1_IRQn 0 */

      /* 参数是DMA句柄 */
   HAL_DMA_IRQHandler(&hdma_usart1_tx);

    /* USER CODE BEGIN USART1_IRQn 1 */
         此处可以添加用户代码
    /* USER CODE END USART1_IRQn 1 */
}
如果要在DMA传输完成,半传输完成等中断里面执行功能,也是通过HAL_DMA_IRQHandler调用的各种回调函数里面实现,这些回调都是弱定义的,方便用户直接在其它文件里面重定义:
HAL_UART_TxHalfCpltCallback()
HAL_UART_TxCpltCallback()
HAL_UART_RxHalfCpltCallback()
HAL_UART_RxCpltCallback()
8 总结

先从整体框架上把握HAL库,然后具体外设具体学习,后面做应用会得心应手些。



页: [1]
查看完整版本: 学习STM32,从HAL库的框架设计开始