HAL_Init()函数阅读记录

HAL_Init()函数阅读记录

1)代码展示:

HAL_StatusTypeDef HAL_Init(void)
{
  /* Configure Flash prefetch */
#if (PREFETCH_ENABLE != 0)
#if defined(STM32F101x6) || defined(STM32F101xB) || defined(STM32F101xE) || defined(STM32F101xG) || \
    defined(STM32F102x6) || defined(STM32F102xB) || \
    defined(STM32F103x6) || defined(STM32F103xB) || defined(STM32F103xE) || defined(STM32F103xG) || \
    defined(STM32F105xC) || defined(STM32F107xC)

  /* Prefetch buffer is not available on value line devices */
  __HAL_FLASH_PREFETCH_BUFFER_ENABLE();
#endif
#endif /* PREFETCH_ENABLE */

  /* Set Interrupt Group Priority */
  HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

  /* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
  HAL_InitTick(TICK_INT_PRIORITY);

  /* Init the low level hardware */
  HAL_MspInit();

  /* Return function status */
  return HAL_OK;
}

2)分析过程

首先是两层条件编译,宏PREFETCH_ENABLEstm32f1xx_hal_conf.h配置头文件中定义的,定义如下:

#define  PREFETCH_ENABLE              1U

说明条件为真,然后会判断下一层的条件编译语句。

defined(STM32F101x6)乍一看好像是一个带参宏,但是 go to definition后却发现,没有定义,如下图所示:

那到底怎么回事呢?

keil5IDE中,有这么一项,见下图:

第二个条件编译到底是什么含义呢?

就是判断你所使用的芯片类型,如果你是手动添加hal库到工程项目中,那么肯定需要将你所使用的芯片对应的宏放到上面的框框中。

使用CubeMX工具生成的代码,选择好所使用的芯片后,生成的工程中自动就有了,很是方便。

在正常情况下,条件编译为真。就会执行宏__HAL_FLASH_PREFETCH_BUFFER_ENABLE();

#define __HAL_FLASH_PREFETCH_BUFFER_ENABLE()    (FLASH->ACR |= FLASH_ACR_PRFTBE)
#define FLASH_ACR_PRFTBE_Pos                (4U) 
#define FLASH_ACR_PRFTBE_Msk                (0x1UL << FLASH_ACR_PRFTBE_Pos)     /*!< 0x00000010 */
#define FLASH_ACR_PRFTBE                    FLASH_ACR_PRFTBE_Msk               /*!< Prefetch Buffer Enable */

FLASH_ACR_PRFTBE定义在stm32f103xe.h中。

就是操作flash中的ACR寄存器的bit4为1.

对应的含义是什么呢?对应的含义是使能预取缓冲区,但是复位后,默认是开启的,这里不知道为什么再次打开了。这个预取缓冲区根据我看的资料,应该类似于8086中的指令队列。能加快指令的执行。

那么为什么FLASH->ACR就操作了对应的寄存器呢,这是我一开始觉得最巧妙的地方。

#define FLASH               ((FLASH_TypeDef *)FLASH_R_BASE)//FLASH_R_BASE就是一个地址,或者叫指针
//stm32是32位的,能访问4GB的线性地址空间,而芯片的设计者会将flash中用到的寄存器映射到该连续的地址空间中的一部分。
//FLASH_R_BASE 就是flash寄存器的所占空间的起始地址。这种映射方式叫做统一编址。
#define FLASH_R_BASE          (AHBPERIPH_BASE + 0x00002000UL) /*!< Flash registers base address */
typedef struct
{
  __IO uint32_t ACR;
  __IO uint32_t KEYR;
  __IO uint32_t OPTKEYR;
  __IO uint32_t SR;
  __IO uint32_t CR;
  __IO uint32_t AR;
  __IO uint32_t RESERVED;
  __IO uint32_t OBR;
  __IO uint32_t WRPR;
} FLASH_TypeDef;

这里为什么要加__IO呢?__IO 的原型是:

#define     __IO    volatile             /*!< Defines 'read / write' permissions */

volatile是一种类型限定符,说明所修饰的对象是易变的,每次应该从变量对应的地址处访问,而不应该从高速缓存中访问。

如果不修饰会怎么样呢?

因为该结构体中的成员变量对应的是寄存器,而寄存器中在程序运行过程中,可能某个状态改变了,对应的bit位就会发生改变。不修饰的话,CPU可能会从高速缓存中取成员变量,这样就造成了实际的值与取到的值不一致。所以要用volatile限定符,来告诉CPU不要走捷径,每次老老实实的从我诞生的地方取。

有人可能要问,高速缓存是什么东西?

首先他是一种CPU提高执行速度的一种方式,是由硬件实现的,对程序员来说是不可见的。一般来说是没有寄存器来控制的。我们程序员,哪怕是系统程序的编写者,他所能看见的也只是寄存器层面。所以非要说高速缓存是什么,我也讲不清楚,只知道CPU用他来干什么。具体怎么实现的东东,我觉得不用太过纠结。

当然,我还见过另外一种,它的概念叫做高速缓冲区 ,是在linux中与磁盘管理相关的。这个高速缓冲区是可见的,是在物理内存中划分处一块内存空间,用一种双向循环链表的结构来管理这段空间。比如,当写磁盘的时候,实际上是向该内存空间写,然后是利用磁盘中断,来读取缓冲区的内容。

上面的两者都是有缓冲、缓存的意思,第一种是硬件实现的,第二种是软件实现的。为什么讲这个呢,就是因为笔者本身也不是科班出身的,一开始碰到很多概念很抽象,很难受,所以希望别人能吸取教训,哪些东西必须搞清楚,哪些东西不用搞的那么清楚,特别是底层,很多东西都是硬件实现的,对于硬件实现的,更多关注实现了什么功能,如何控制;软件实现的,多看代码。

linus大名鼎鼎的一句话:

/*  read  the  fucking  source code    */

很多不理解的东西,看完代码后,就不会觉得那么抽象了。好了,扯远了,继续下面的代码分析。

接下来是设置中断优先级分组 HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4)

void HAL_NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
  /* Check the parameters */
  assert_param(IS_NVIC_PRIORITY_GROUP(PriorityGroup));
  
  /* Set the PRIGROUP[10:8] bits according to the PriorityGroup parameter value */
  NVIC_SetPriorityGrouping(PriorityGroup);
}

最开始的assert_param是一个宏,具体定义如下:

#ifdef  USE_FULL_ASSERT
#define assert_param(expr) ((expr) ? (void)0U : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0U)
#endif /* USE_FULL_ASSERT */
#ifdef  USE_FULL_ASSERT
void assert_failed(uint8_t *file, uint32_t line)
{

}
#endif /* USE_FULL_ASSERT */

上面的代码块在配置文件stm32f1xx_hal_conf.h中,如果定义了宏USE_FULL_ASSERT,则表达式expr为真,就为0,为假的话,就调用函数assert_failed,该函数在main.c中,并没有实现,有需要的话,自己实现。

这个assert_paramfreeRTOS中称为断言,刚开始接触时,感觉断言是什么东东,好抽象啊,后来,就这?就这?后来,我总结出一个办法,碰到一个稀奇古怪的计算机概念后,千万别顾名思义,那样很痛苦,我统一给这些概念分配编号,仅仅将其当作一个编号,然后去看代码,就能比较好的理解其内容。还是linus的那句话,大佬就是大佬,越学就越觉得大佬说话是对的。

NVIC_SetPriorityGrouping(PriorityGroup)的代码如下:

  #define NVIC_SetPriorityGrouping    __NVIC_SetPriorityGrouping
__STATIC_INLINE void __NVIC_SetPriorityGrouping(uint32_t PriorityGroup)
{
  uint32_t reg_value;
  uint32_t PriorityGroupTmp = (PriorityGroup & (uint32_t)0x07UL);             /* 0x07=00000111,取     PriorityGroup低三位        */

  reg_value  =  SCB->AIRCR;              /* 读取SCB中的AIRCR寄存器    */
  reg_value &= ~((uint32_t)(SCB_AIRCR_VECTKEY_Msk | SCB_AIRCR_PRIGROUP_Msk)); 
    /* SCB_AIRCR_VECTKEY_Pos              16U  
      SCB_AIRCR_VECTKEY_Msk              (0xFFFFUL << SCB_AIRCR_VECTKEY_Pos) 
      #define SCB_AIRCR_PRIGROUP_Pos              8U   
      #define SCB_AIRCR_PRIGROUP_Msk             (7UL << SCB_AIRCR_PRIGROUP_Pos) 
      reg_value的值为16~31位清零,8~10位清零,其余位保持不变
    */
  reg_value  =  (reg_value                                   |
                ((uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos) |
                (PriorityGroupTmp << SCB_AIRCR_PRIGROUP_Pos) );               /* Insert write key and priority group */
  SCB->AIRCR =  reg_value;
}

首先解释一下:__STATIC_INLINE

  #define __STATIC_INLINE                        static __inline

static修饰函数,就是限定函数的使用范围,只能在其定义的文件范围内使用,一般用于不希望其他文件的程序调用该函数。

后面的__inline是内联函数的意思

内联函数是一个什么概念呢

在一个函数A中调用另一个函数B时,等B执行完后,再返回A中继续执行。看样子很简单,但是要实现上面这种调用,就还是有点麻烦,需要通过一种栈机制来实现。具体如何实现的呢?

首先在调用B前,A通过ssesp指针(这里涉及到一点汇编的知识)会将B所用到的参数入栈,还会将B的下一条指令的地址(返回地址)入栈,等到B结束时,就是遇到 B 的 ‘}’ 时就等价于遇到ret指令,此时将此时esp所指向的内容弹出到eip中,就是将返回地址弹出到eip中,回到A中继续往下执行。如果有返回值,怎么办呢,会通过寄存器来传递返回值,或者通过栈

如果感觉到有点复杂,没有理解,没关系,我写这个的目的就是说,如果理解了自然更好,没理解的话,暂时可以不管,只需要知道正常情况下,一个C函数调用另一个C函数,有点复杂,所以引入内联函数,来解决这个问题。

具体内联函数是如何来绕过这个复杂的调用机制,又能在A中执行函数B的内容呢,很简单,就是跟宏展开是一个道理,就是在编译的过程中将B函数中的内容直接插到A中,单纯的文本替换。这样就A中既能执行B的内容,又避开了调用机制。

关于内联就说到这里,下面开始看具体的代码,其实就是配置寄存器的过程。

AIRCRbit8bit10控制优先级分组,代码很简单,是一些位与、位或的运算,这里主要说一下,优先级分组的概念。

ARM公司推出的CORTEX-M3优先级寄存器IPR是8位的。经过ST裁剪后,只使用其中高4位。然后又将这4位划分,比如2位代表抢占优先级,2位代表子优先级。至于为什么要这样,那我也不清楚,对于这种硬件层面的东西,只需要了解怎么使用即可。

代码(uint32_t)0x5FAUL << SCB_AIRCR_VECTKEY_Pos将寄存器高16位设置为0x5FA的原因是:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SyAvc3w7-1638172431073)(HAL_Init()]函数源码分析与追踪.assets/5FA.png)

默认情况下,这个由CubeMX生成的代码,中断优先级分组为NVIC_PRIORITYGROUP_4,4位全是抢占。


接下来的函数是HAL_InitTick(TICK_INT_PRIORITY);初始化systick

__weak HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
  /* Configure the SysTick to have interrupt in 1ms time basis*/
  if (HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq)) > 0U)
  {
    return HAL_ERROR;
  }

  /* Configure the SysTick IRQ priority */
  if (TickPriority < (1UL << __NVIC_PRIO_BITS))
  {
    HAL_NVIC_SetPriority(SysTick_IRQn, TickPriority, 0U);
    uwTickPrio = TickPriority;
  }
  else
  {
    return HAL_ERROR;
  }

  /* Return function status */
  return HAL_OK;
}

首先还是说__weak,__weak修饰一个函数,表示这个函数是弱定义的,具体的含义是,如果在其他文件中重新实现这个函数,那么编译器会就认其他文件中实现的函数,这个__weak修饰的函数就相当于无效了,没有在其他文件中实现的话,那就以这个__weak修饰的函数为准。

在启动文件中也实现了所有的中断函数,也是弱定义的,然后想在中断中实现自己的功能,只需要自己定义同名的函数就可以了。

函数HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq))systick的配置函数。

uint32_t HAL_SYSTICK_Config(uint32_t TicksNumb)
{
   return SysTick_Config(TicksNumb);
}
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
  {
    return (1UL);                                 /* #define SysTick_LOAD_RELOAD_Msk (0xFFFFFFUL )  */
  }

  SysTick->LOAD  = (uint32_t)(ticks - 1UL);                         /* set load寄存器 */
  NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL); /* set Priority for Systick Interrupt */
  SysTick->VAL   = 0UL;                                             /* Load the SysTick Counter Value */
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;                         /* Enable SysTick IRQ and SysTick Timer */
  return (0UL);                                                     /* Function successful */
}

SystemCoreClock / (1000U / uwTickFreq)SystemCoreClock是一个全局变量,系统初始化为16M,代表的是HCLK时钟。

uwTickFreq默认代表的是systick频率为1khz

这里为什么要减1呢ticks - 1UL,原因是在内核编程手册中,有这么一段话:

内核手册中教你寄存器值的设置方式,如果希望systick interrupt是每隔100时钟脉冲发生的话,就设置成99。如果是希望100时钟脉冲后的话就设置成100。

这里是希望每隔1ms发生一次,就是72000,所以需要减一。

为什么这里有个判断语句呢,因为systick是24位的。所以最大的值为0xFFFFFF,正好是SysTick_LOAD_RELOAD_Msk.肯定不能超过最大允许值啊。

然后是设置优先级NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL)

优先级大小为1UL << __NVIC_PRIO_BITS) - 1UL,(1<<4)-1,即为0xFF

#define NVIC_SetPriority            __NVIC_SetPriority
__STATIC_INLINE void __NVIC_SetPriority(IRQn_Type IRQn, uint32_t priority)
{
  if ((int32_t)(IRQn) >= 0)
  {
    NVIC->IP[((uint32_t)IRQn)] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & (uint32_t)0xFFUL);
  }
  else
  {
    SCB->SHP[(((uint32_t)IRQn) & 0xFUL)-4UL] = (uint8_t)((priority << (8U - __NVIC_PRIO_BITS)) & \     (uint32_t)0xFFUL);
  }//__IOM uint8_t  SHP[12U]; 
}

这里说明一下,NVIC只是管理只是16个系统异常(说是16个,但是实际上只有9个,而且9个中的3个优先级是固定死的,优先级可编程的只有6个)之外的中断。所以这里有个判断,如果(IRQn) >= 0为真,说明不是系统异常,那么优先级的设置就在NVIC中的中断优先级寄存器,即IPR,系统异常的优先级并不是在IPR中设置的。而是在系统控制块中。

优先级数值越小,优先级越高。systick优先级为15,是最低的优先级。

(((uint32_t)IRQn) & 0xFUL)-4UL,这里为什么这么做呢?以systick为例,SysTick_IRQn = -1,-1``-1在计算机中的存储为补码,即为0xfffffff,与上0xF后就为0xF,即为15,15-4=11,正好对应到SHP[12U]`数组的最后一个字节。

这里也说明了系统异常的优先级并不是一定大于中断的优先级,很多书中并不区分异常与中断,但是这里是区分的。

val寄存器设置为0,因为val寄存器保存的是当前的计数值。当然为0。

后面就是设置CTRL寄存器中相应的bit位,具体含义是:时钟源选择AHB、使能中断、使能计数器。最后插一句,这个systick是循环的计数器,计数值为0时,会自动重新装载计数值到LOAD寄存器。

总结HAL_SYSTICK_Config这个函数,配置了定时时间,默认配置systick的优先级为15,并使能了一些控制位。在HAL_InitTick函数中,先是通过上面的函数默认配置SYSTICK,然后根据HAL_InitTick传进来的参数配置systick的优先级。

然后全局变量uwTickPrio被设置成SYSTICK的优先级。


下面一个函数是HAL_MspInit(),该函数的含义是初始化低水平的硬件,那么来看看到底实现了什么。

void HAL_MspInit(void)
{

__HAL_RCC_AFIO_CLK_ENABLE();
__HAL_RCC_PWR_CLK_ENABLE();

__HAL_AFIO_REMAP_SWJ_NOJTAG();


}
#define __HAL_RCC_AFIO_CLK_ENABLE()   do { \
                                   __IO uint32_t tmpreg; \
                                   SET_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);\
                                   /* Delay after an RCC peripheral clock enabling */\
                                   tmpreg = READ_BIT(RCC->APB2ENR, RCC_APB2ENR_AFIOEN);\
                                   UNUSED(tmpreg); \
                                 } while(0U)
#define SET_BIT(REG, BIT)     ((REG) |= (BIT))

第一个宏,使能AFIO时钟。

第二个宏是电源接口时钟使能.

基本上就是用哪个模块,哪个模块的时钟就要使能。

第三个宏就是

#define __HAL_AFIO_REMAP_SWJ_NOJTAG()  AFIO_DBGAFR_CONFIG(AFIO_MAPR_SWJ_CFG_JTAGDISABLE)
#define AFIO_DBGAFR_CONFIG(DBGAFR_SWJCFG)  do{ uint32_t tmpreg = AFIO->MAPR;     \
                                             tmpreg &= ~AFIO_MAPR_SWJ_CFG_Msk; \
                                             tmpreg |= DBGAFR_SWJCFG;          \
                                             AFIO->MAPR = tmpreg;              \
                                             }while(0u)
                                             ```

这个宏就是配置AFIO_MAPR的位24~26,代表的是启动SWD

3)总结

说了一些乱其八糟的东西,一方面是追根究地,一方面是为了加深自己的印象。

1)打开了指令的预取。

2)设置了 优先级分组

3)初始化SYSTICK,初始化为1ms产生中断。并且使能了。中断的优先级默认为15,如果说想修改优先级的话,那么在stm32f1xx_hal_conf.h文件中修改宏TICK_INT_PRIORITY

cubemx中都没有看到修改systick的界面,只是勾选后,就初始化成这个样子。

4)底层硬件的初始化,打开了AFIO时钟,电源接口时钟,并且根据选择的串行线在AFIO_MAPR中完成重映射。

版权声明:本文为CSDN博主「xiaoqa11」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xiaoqa11/article/details/121612051

生成海报
点赞 0

xiaoqa11

我还没有学会写个人说明!

暂无评论

发表评论

相关推荐

RT-Thread Studio移植LAN8720A驱动

RTT网络协议栈驱动移植(霸天虎) 1、新建工程 ​ 工程路径不含中文路径名,工程名用纯英文不含任何符号。 2、用CubeMx配置板子外设 2.1、配置时钟 ​ 按照自己板子配置相应时钟。

Lin总线通信在STM32作为主机代码以及从机程序

距离上次做资料准备已经过去六天了。最近在学车,上周末就没有开电脑。这周开始进行了Lin通信的代码整理,目前是可以正常通信的了,采用的是增强型校验方式。后期再进一步跟进研究。。。更新一博,留

4路红外循迹模块使用教程

4路红外循迹模块使用教程 个人原创博客:点击浏览模块详细信息: 工作电压:DC 3.3V~5V 工作电流:尽量选择1A以上电源供电 工作温度:-10℃~50℃ 安装孔

HAL库串口中断

一,配置串口初始化 void MX_USART1_UART_Init(void) {huart1.Instance USART1;huart1.Init.BaudRate 115200;huart1.Init.WordLen