对STM32芯片架构及固件库的理解

一、STM32芯片的基本架构

1、STM32内部组成

STM32芯片,其实就是一台功能阉割的电脑,属于片上SOC的一种。里面包含有Cortex-M、RAM、FLASH等组件。图中所示为STM32F1芯片的内部架构。

以日常生活中使用的电脑为例:
(1)内核Cortex-M:相当于X86电脑的CPU;
(2)总线矩阵:内核和外部设备进行交互的时候需要总线矩阵进行通信,这个总线矩阵就像是台式机上的南 北桥,实现 数据交换的作用。
(3)FLASH:相当于电脑的硬盘;
(4)外设:相当于电脑外接的鼠标,网口,键盘,摄像头等;
(5)RAM:相当于电脑的内存条;
(6)DMA:相当于U盘向电脑拷入数据时,DMA可以直接进行数据搬运,此时不需要CPU参与。
如果对电脑不够了解的话,也可将STM32F芯片看作自己的身体。
(1)内核Cortex-M:相当于我们的大脑,思维、决策和协调各种活动;
(2)总线矩阵:相当于我们的神经系统,类似于神经系统连接身体的各个部分,传递和处理信息;
(3)FLASH:相当于我们的长期记忆;
(4)RAM:相当于我们的短期期记忆;
(5)外设:相当于我们的五官和四肢‌;
(6)DMA:想当与我们的血液循环,无需额外的介入而自主的运行。

2、STM32的工作流程

学过C语言的同学都知道,在执行C语言程序的时候,我们所使用的电脑是无法直接的理解C语言。我们能看懂C语言(也可能有的同学看不懂dog);但电脑看不懂。电脑只能读懂0和1。因此需要编译将C语言翻译成机械码。

一般可分为以下几个步骤:
(1)预处理‌:预处理器处理源代码中的预处理指令(如#include、#define),生成预处理后的代码;
(2)‌编译‌:编译器将预处理后的代码转换为汇编语言代码,进行语法检查和优化;
(3)‌汇编‌:汇编器将汇编语言代码转换为机器语言指令,生成目标文件(.o);
(4)‌链接‌:链接器将目标文件与库文件链接,解决符号引用,生成可执行文件(.exe);
(5)‌加载‌:操作系统将可执行文件加载到内存中;
(6)‌执行‌:处理器按照指令顺序执行内存中的机器语言代码,完成程序运行。

当我们在IDE(VScode 、Clion、IAR、Keil)上写完STM32程序后,可经过IDE自带的编译器将所写的程序编译成STM32所能识别的机械语言也就是.hex文件。之后,通过USB-TTL模块或者STLINK下载器将.hex文件烧录到STM32中去。
其中,STM32中RAM储存的是存储变量、堆栈和其他临时数据;FLASH存储器用于存储程序代码、常量数据以及部分需要在断电后保留的数据。
其实,在STM32运行main()函数之前会做一些工作,来确保main()能够正常的运行,就像我们在做数学试卷的时候,在写答案之前会准备好以下几样。
(1)准备好草稿纸:在内存中在开辟堆栈;初始化堆栈指针 SP=_initial_sp;
(2)准备好试卷:初始化程序计数器指针;初始化 PC 指针=Reset_Handler;
(3)正式启动:可通过拉高或者拉低外设引脚的电平来选择启动方式,一般默认为从FLASH启动。

图片里的主闪存存储器就是FLASH。

二、单片机的FLASH和RAM

一般来说单片机的内存指的是FLASH和RAM,当在程序中定义了全局变量、局部变量、只读变量等参数时都是会存放到对应的FLASH或者是RAM中。具体对单片机FLASH和RAM的介绍之后再写,这里只对单片机内存分配,对堆和栈以及变量的存储做一个梳理和记录。

图中分别为M3内核、RAM、FLASH

1、FLASH(0x0800 0000)

FLASH主要是存放程序代码、全局变量(不为零)及常量的。下面是将 FLASH内部进行细分之后的一张图。


STM32的FLASH是从地址0x0800 0000开始的,是向上增长的。FLASH又可以细分为这么几个部分:
(1)文本段 (Text),其中文本段中又包含可执行代码 (Executable Code)和常量 (Literal Value);例如:const int B=1中的B的地址以及我们自己写的程序代码的地址均储存在FLASH的文本段中;
(2)只读数据区域 (Read Only Data);
(3)数据复制段 (Copy of Data Section),这个段充当的作用是存放程序中初始化为非0值的全局变量的初始值;
数据复制段是一个特别的情况,就是初始值不为零的全局变量,他储存的地址是在FlASH中,但开始运行程序时,这个定义的全局变量就会被拷贝到RAM中,此时若要查询变量的地址,发现是在RAM中;可通过查看ST-LINK Utility查看全局变量的地址。

2、RAM(0x2000 0000)

相对与FLASH来说,RAM主要就是用来存储数据了,在RAM中值得关注的是堆和栈的空间,堆是向上增长的而栈是向下生长的,如果一个函数运行的时候有大量的局部变量(栈向下增长),同时程序在整个过程中malloc申请了大量的堆空间而没有释放(堆向上增长),造成堆和栈空间的冲突,一旦堆栈冲突,系统就崩溃了。
如下是STM32中RAM的分区:

RAM中包含了如下几个部分:
(1)data:存放初始化为非0值的全局变量;
(2)bss:存放未初始化或者是初始化为0的全局变量;
(3)堆(Heap) : 由malloc申请,由free释放;堆是程序在运行的时候用malloc或new申请任意大小的内存,程序员自己负责在适当的时候用free或delete释放内存。可以根据需要请求一段连续的内存空间,并在程序执行过程中随着数据的变化而增长或减小。
(4)栈(Stack) : 存放局部变量、函数调用时的返回地址以及中断入口等;函数执行结束时这些存储单元自动被释放,其大小主要看函数调用的深度。
这是一个特殊的内存区域,主要用于存储程序执行过程中的局部变量、函数参数和函数调用的返回地址。它是按照后进先出的原则工作的,类似于生活中的堆叠物品。

  • 总的来说,FLASH和RAM可分为以下几个部分:
    Code为程序代码部分
    RO_data 表示程序定义的常量(如:const temp等);
    RW_data 表示已初始化的全局变量
    ZI_data 表示未初始化的全局变量,以及初始化为0的变量
    FLASH=Code+RO_data+RW_data
    RAM=RW_data+ ZI_data
    初始化时RW_data从flash拷贝到RAM

堆和栈的空间可以由我们来自由设定,可在在STM32的启动文件(.s)中,刚开头就有对堆(Head_Size)和栈(Stack_Size)空间的定义描述。

三、固件库的本质

ST公司官方提供了三种库,分别为标准库,HAL库,LL库。三种库各有优劣。但这些库本质上就是对STM32中的各种寄存器进行操作。

1、寄存器理解

以SM32F1的GPIOC引脚口为例。若想将设置GPIOC的引脚为高电平,首先是要找到 GPIOC引脚口的端口位设置/清除寄存器BSRR的地址。其在芯片内部的映射地址为0x40011010

BSRR寄存器是32bit,高低16bit有效,对应着16个外部IO,基本功能如下:
(1)在低16位地址(0-15)写1对应的IO则输出高电平,;
(2)在高16位地址(16-31)写1对应的IO则输出低电平;
(3)若高低16位地址都写1,高16位地址起作用。
以PC13所在对应的的第12位和第28位为例:

12位 28位 引脚电平
0 1
1 0
1 1
0 0 对数据输出寄存器不产生影响

若现在我们通过C语言指针的操作方式,让GPIOC16个IO 都输出高电平。其代码如下:

 // GPIOC 端口全部输出高电平
*(unsigned int*)(0x4001 1010) = 0xFFFF;

0x4001100C在我们看来是GPIOC端口BSRR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4001 1010,然后再对这个指针进行 * 操作。 刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作,具体见代码:

// GPIOC端口全部输出高电平
#define GPIOC_BSRR (unsigned int*)(GPIOC_BASE+0x10)
* GPIOC_BSRR = 0xFFFF;

那么这个地址0x40011010是怎么来的呢,
可以看到0x4001100C=0x40010000(基地址)+ 0x00001010(偏移地址),之所以采用基地址+偏移地址的方式,是因为这么多的引脚,有的引脚是高速引脚,有的引脚是低速引脚。
STM32F103C8T6的外设寄存器通常在两个桥上,分别为APB1和APB2。APB1操作速度限于36MHz,APB2操作于全速(最高72MHz)。通过本文的第一张图可以看到,GPIOC所在的寄存器在APB2(0x40010000)上。

2、寄存器地址理解

在拿到了GPIOC这个外设的地址0x40011000,就可以操作GPIOC寄存器了。那么首先看看,控制GPIOC有那些寄存器。根据芯片手册,可以得到:

名称 简称 寄存器地址 偏移地址
端口配置低寄存器 GPIOC_CLR 0x40011000 0x00
端口配置高寄存器 GPIOC_CHR 0x40011004 0x04
端口输入数据寄存器 GPIOC_IDR 0x40011008 0x08
端口输出数据寄存器 GPIOC_ODR 0x4001100C 0x0C
端口位设置/清除寄存器 GPIOC_BSRR 0x40011010 0x10
端口位清除寄存器 GPIOC_BRR 0x40011014 0x14
端口配置锁定寄存器 GPIOC_LCKR 0x40011018 0x18

所以通过(unsignedint*)(0x40011000)这个指针就可以操作这些寄存器,比如要操作GPIOC_ODR这个寄存器,则(unsignedint*)(0x40011000+0x0C),就可以找到GPIOC_ODR的地址,再对其进行操作, * ((unsignedint*)(0x40011000
+0x0C),就可以修改/读取GPIOC_ODR寄存器里面的内容。

GPIOC_CLR GPIOC_CHR GPIOC_IDR GPIOC_ODR GPIOC_BSRR GPIOC_BRR GPIOC_LCKR
⬆*(unsignedint*)(0x40011000)

3、HAL库封装

可STM32有很多外设,每个外设都对应着多种的寄存器。若要写一个功能。要一一查寄存器的功能即地址,然后进行配置。显然,太浪费时间了。于是,ST厂家就对这些寄存器地址进行封装。根据封装的规则不同,于是就形成了标准库,HAL库,LL库。还是以GPIOC的HAL库为例。
(1)首先,对GPIOC所在的外设基地址进行封装。

/* 外设基地址:整个外设区的基地址 */
#define PERIPH_BASE           0x40000000UL /*!< Peripheral base address in the alias region */

(2)之后,对GPIOC所在的外设总线地址进行封装。

#define APB2PERIPH_BASE       (PERIPH_BASE + 0x00010000UL)

(3)根据GPIOC在总线中的偏移地址0x1000, 可以计算出GPIOC 外设基地址

#define GPIOC_BASE            (APB2PERIPH_BASE + 0x00001000UL)

(4)封装GPIOC_BSRR寄存器地址,GPIOC_BSRR寄存器地址为GPIOC_BASE+0x10

#define GPIOC_BSRR            (GPIOC_BASE + 0x00000010UL)

(5)让GPIOC的16个引脚全部输出高电平

//1 // GPIOC 端口全部输出 高电平
*(unsigned int*) GPIOC_BSRR=0xFFFF;

到这一步可以发现,我们只需要操作GPIOB_ODR而不再是0x4001 0C0C这个地址,会更加直观。但GPIOA-GPIOE 都各有一组功能相同的BSRR寄存器,它们只是地址不一样,同样的,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装:

/** 
  * @brief General Purpose I/O
  */

typedef struct
{
  __IO uint32_t CRL;    
  __IO uint32_t CRH;
  __IO uint32_t IDR;
  __IO uint32_t ODR;
  __IO uint32_t BSRR;
  __IO uint32_t BRR;
  __IO uint32_t LCKR;
} GPIO_TypeDef;

这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有7个成员变量,变量名对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中32位的变量占用4个字节。
所以只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器。使用宏定义GPIO_TypeDef 类型的指针指向各个 GPIO 端口的首地址,使用时直接用该宏访问寄存器即可:

#define GPIOA               ((GPIO_TypeDef *)GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *)GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *)GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *)GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *)GPIOE_BASE)

所以,要想给GPIOC引脚都设为高电平

GPIOC->BSRR=0xFFFF;

但要想单独将GPIOC口的第十三个引脚设置为高电平,代码可写为:

GPIOC->BSRR=0x2000;

还是有0x2000这样的值,可读性还是差,于是HAL库又增加了一个函数:

HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);

函数内容如下:

/**
  * @brief  Sets or clears the selected data port bit.
  *
  * @note   This function uses GPIOx_BSRR register to allow atomic read/modify
  *         accesses. In this way, there is no risk of an IRQ occurring between
  *         the read and the modify access.
  *
  * @param  GPIOx: where x can be (A..G depending on device used) to select the GPIO peripheral
  * @param  GPIO_Pin: specifies the port bit to be written.
  *          This parameter can be one of GPIO_PIN_x where x can be (0..15).
  * @param  PinState: specifies the value to be written to the selected bit.
  *          This parameter can be one of the GPIO_PinState enum values:
  *            @arg GPIO_PIN_RESET: to clear the port pin
  *            @arg GPIO_PIN_SET: to set the port pin
  * @retval None
  */
void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState)
{
  /* Check the parameters */
  assert_param(IS_GPIO_PIN(GPIO_Pin));
  assert_param(IS_GPIO_PIN_ACTION(PinState));

  if (PinState != GPIO_PIN_RESET)
  {
    GPIOx->BSRR = GPIO_Pin;
  }
  else
  {
    GPIOx->BSRR = (uint32_t)GPIO_Pin << 16u;
  }
}

这样STM32的使用会变得非常方便,其他所有的外设的寄存器都有对应的操作函数,这样就可以从复杂的寄存器操作中解脱出来,从而有更多的时间和更少的失误去在算法方面进行操作。

你可能感兴趣的:(STM32学习,stm32,架构,嵌入式硬件)