文章目录[隐藏]
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_ENABLE
在stm32f1xx_hal_conf.h
配置头文件中定义的,定义如下:
#define PREFETCH_ENABLE 1U
说明条件为真,然后会判断下一层的条件编译语句。
defined(STM32F101x6)
乍一看好像是一个带参宏,但是 go to definition
后却发现,没有定义,如下图所示:
那到底怎么回事呢?
在
keil5
的IDE
中,有这么一项,见下图:第二个条件编译到底是什么含义呢?
就是判断你所使用的芯片类型,如果你是手动添加
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_param
在freeRTOS
中称为断言,刚开始接触时,感觉断言是什么东东,好抽象啊,后来,就这?就这?后来,我总结出一个办法,碰到一个稀奇古怪的计算机概念后,千万别顾名思义,那样很痛苦,我统一给这些概念分配编号,仅仅将其当作一个编号,然后去看代码,就能比较好的理解其内容。还是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通过
ss
与esp
指针(这里涉及到一点汇编的知识)会将B所用到的参数入栈,还会将B的下一条指令的地址(返回地址)入栈,等到B结束时,就是遇到 B 的 ‘}’ 时就等价于遇到ret
指令,此时将此时esp
所指向的内容弹出到eip
中,就是将返回地址弹出到eip
中,回到A中继续往下执行。如果有返回值,怎么办呢,会通过寄存器来传递返回值,或者通过栈
如果感觉到有点复杂,没有理解,没关系,我写这个的目的就是说,如果理解了自然更好,没理解的话,暂时可以不管,只需要知道正常情况下,一个C函数调用另一个C函数,有点复杂,所以引入内联函数,来解决这个问题。
具体内联函数是如何来绕过这个复杂的调用机制,又能在A中执行函数B的内容呢,很简单,就是跟宏展开是一个道理,就是在编译的过程中将B函数中的内容直接插到A中,单纯的文本替换。这样就A中既能执行B的内容,又避开了调用机制。
关于内联就说到这里,下面开始看具体的代码,其实就是配置寄存器的过程。
由AIRCR
的bit8
至bit10
控制优先级分组,代码很简单,是一些位与、位或的运算,这里主要说一下,优先级分组的概念。
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
暂无评论