01. 初识STM32
一、什么是STM32
STM32,从字面上来理解,ST 是意法半导体,M 是 Microelectronics 的缩写,32 表示 32 位,合起来理解,STM32 就是指 ST 公司开发的 32 位微控制器。
STM32 主要分两大块,MCU 和 MPU,MCU 就是我们常见的 STM32 微控制器,不能跑 Linux,而 MPU 则是 ST 在 19 年才推出的微处理器,可以跑 Linux。STM32 有很多系列,可以满足市场的各种需求,从内核上分有 Cortex-M0、M3、M4 和 M7 这几种,每个内核又大概分为主流、高性能和低功耗。其中,F1 代表了基础型,基于 Cortex-M3 内核,主频为 72MHZ,F4 代表了高性能,基于 Cortex-M4 内核,主频 180M。
二、STM32命令方式
这里,我们使用 STM32F407ZGT6 型号的单片机,从命名就可以知道如下信息:
三、STM32F407系统架构
STM32F407 是 ST 公司基于 ARM 授权 Cortex M4 内核而设计的一款芯片,而 Cortex M 内核使用的是 ARM v7-M 架构,是为了替代老旧的单片机而量身定做的一个内核,具有低成本、低功耗、实时性好、中断响应快、处理效率高等特点。
ARM 公司提供内核(如 Cortex M4,简称 CM4)授权,完整的 MCU 还需要很多其他组件。芯片公司(ST、NXP、TI、GD、华大等)在得到 CM4 内核授权后,就可以把 CM4 内核用在自己的硅片设计中,添加:存储器,外设,I/O 以及其它功能块。不同厂家设计出的单片机会有不同的配置,包括存储器容量、类型、外设等都各具特色,因此才会有市面上各种不同应用的 ARM 芯片。
可以看到,ARM 公司提供 CM4 内核和调试系统,其他的东西(外设(I2C、SPI、UART、TIM等)、存储器(SRAM、FLASH等)、I/O等)由芯片制造商设计开发。这里 ST 公司就是 STM32F407 芯片的制造商。
芯片内核和外设之间通过各种总线连接,其中主控总线有 8 条,被控总线有 7 条。主控总线通过一个总线矩阵来连接被控总线,总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。总线之间交叉的时候如果有个圆圈则表示可以通信,没有圆圈则表示不可以通信。
主控主线 | 被控主线 |
---|---|
Cortex M4 内核 I 总线 | 内部 FLASH Icode 总线 |
Cortex M4 内核 D 总线 | 内部 FLASH Dcode 总线 |
Cortex M4 内核 S 总线 | 主要内部 SRMA1(112KB) |
DMA1 存储器总线 | 辅助内部 SRMA1(16KB) |
DMA2 存储器总线 | 辅助内部 SRMA1(64KB)(适用于 F42xxx 和 F42xxx) |
DMA2 外设总线 | AHB1 外设(包含 AHB-APB 总线桥和 APB 外设) |
以太网 DMA 总线 | AHB2 外设 |
USB OTG HS DMA 总线 | FSMC |
【1】、I 总线(I - Bus)
此总线用于将 Cortex-M4 内核的指令总线连接到总线矩阵。内核通过此总线获取指令。此总线访问的对象是包含代码的存储器(内部 Flash/SRAM 或通过 FSMC 的外部存储器)。
【2】、D 总线(D - Bus)
此总线用于将 Cortex-M4 数据总线和 64 KB CCM 数据 RAM 连接到总线矩阵。内核通过此总线进行立即数加载和调试访问。此总线访问的对象是包含代码或数据的存储器(内部 Flash 或通过 FSMC 的外部存储器)。
【3】、S 总线(S - Bus)
此总线用于将 Cortex-M4 内核的系统总线连接到总线矩阵。此总线用于访问位于外设或 SRAM 中的数据。也可通过此总线获取指令(效率低于 ICode)。此总线访问的对象是 112KB、64KB 和 16KB 的内部 SRAM、包括 APB 外设在内的 AHB1 外设、AHB2 外设以及通过 FSMC 的外部存储器。
【4】、DMA 存储器总线
此总线用于将 DMA 存储器总线主接口连接到总线矩阵。DMA 通过此总线来执行存储器数据的传入和传出。此总线访问的对象是数据存储器:内部 SRAM(112KB、64KB、16KB)以及通过 FSMC 的外部存储器。
【5】、DMA 外设总线
此总线用于将 DMA 外设主总线接口连接到总线矩阵。DMA 通过此总线访问 AHB 外设或执行存储器间的数据传输。此总线访问的对象是 AHB 和 APB 外设以及数据存储器:内部SRAM 以及通过 FSMC 的外部存储器。
【6】、以太网 DMA 总线
此总线用于将以太网 DMA 主接口连接到总线矩阵。以太网 DMA 通过此总线向存储器存取数据。此总线访问的对象是数据存储器:内部 SRAM(112KB、64KB 和 16KB)以及通过 FSMC 的外部存储器。
【7】、USB OTG HS DMA 总线
此总线用于将 USB OTG HS DMA 主接口连接到总线矩阵。USB OTG DMA 通过此总线向存储器加载/存储数据。此总线访问的对象是数据存储器:内部 SRAM(112KB、64KB 和 16KB)以及通过 FSMC 的外部存储器。
四、什么是存储器映射
STM32 是一个 32 位单片机,他可以很方便的访问 4GB 以内的存储空间(2^32 = 4GB),因此 Cortex M4 内核将所有结构,包括:FLASH、SRAM、外设及相关寄存器等全部组织在同一个 4GB 的线性地址空间内,我们可以通过 C 语言来访问这些地址空间,从而操作相关外设(读/写)。数据字节以小端格式(小端模式)存放在存储器中,数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
存储器本身是没有地址信息的,我们对存储器分配地址的过程就叫存储器映射。这个分配一般由芯片厂商做好了,ST 将所有的存储器及外设资源都映射在一个 4GB 的地址空间上(8个块),从而可以通过访问对应的地址,访问具体的外设。
ST 将 4GB 空间分成 8 个块,每个块 512MB,从图中我们可以看出有很多保留区域(Reserved),这是因为一般的芯片制造厂家是不可能把 4GB 空间用完的,同时,为了方便后续型号升级,会将一些空间预留(Reserved)。
在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。Block0 用来设计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设。
第一个块是Block 0,用于存储代码,即 FLASH 空间。我们使用的 STM32F407ZGT6 的 FLASH 就是 1MB。
第二个块是 Block 1,用于存储数据,即 SRAM 空间。STM32F407 内部 SRAM 的大小为 128KB,其中 SRAM1 为 112KB,SRAM2 为 16KB。
第三个块是 Block 2,用于外设访问,STM32F407 内部大部分的外设都是放在这个块里面的。根据外设的总线速度不同,Block 被分成了 APB 和 AHB 两部分,其中 APB 又被分为 APB1 和 APB2,AHB 分为 AHB1 和 AHB2。还有一个 AHB3 包含了 Block3/4/5,AHB3 包含的 3 个 Block 用于扩展外部存储器,如 SRAM,NORFLASH 和 NANDFLASH 等。
五、什么是寄存器映射
给存储器分配地址的过程叫 存储器映射,寄存器是一类特殊的存储器,它的每个位都有特定的功能,可以实现对外设/功能的控制,给寄存器的地址命名的过程就叫 寄存器映射。
在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
【1】、寄存器名字
每个寄存器都有一个对应的名字,以简单表达其作用,并方便记忆,这里 GPIOx_ODR 表示寄存器英文名,x 可以从A~I,说明有 9 个这样的寄存器。
【2】、寄存器偏移量及复位值
地址偏移量表示相对该外设基地址的偏移,比如 GPIOB,我们参考数据手册可知其 GPIOB 外设基地址是:0x4002 0400。那么 GPIOB_ODR 寄存器的地址就是:0x4002 0414。知道了外设基地址和地址偏移量,我们就可以知道任何一个寄存器的实际地址。
复位值表示该寄存器在系统复位后的默认值,可以用于分析外设的默认状态。这里全部是 0。
【3】、寄存器位表
描述寄存器每一个位的作用(共 32bit),这里表示 ODR 寄存器的第 15 位(bit),位名字为 ODR15,rw 表示该寄存器可读写(r,可读取;w,可写入)。
【4】、位功能描述
描述寄存器每个位的功能,这里表示位 0 ~ 15,对应 ODR0 ~ ODR15,每个位控制一个 IO 口的输出状态。
参考数据手册,我们可以知道找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x4002 0C14,ODR 寄存器是 32bit,低 16bit 有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOF的 16 个 IO 都输出高电平。
// GPIOB 端口全部输出 高电平
*(unsigned int*)(0x40020C14) = 0xFFFF;
0x40010C14 在我们看来是 GPIOF 端口数据输出寄存器 ODR 的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即 (unsigned int *)0x40010C14,然后再对这个指针进行 * 操作。
通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过定义了一个 GPIOB_ODR 的宏,来替代数值操作,很明显,GPIOB_ODR 的可读性和可维护性,比直接使用数值操作来的直观和方便。这个宏定义过程就可以称之为寄存器的映射。
#define GPIOB_ODR *(unsigned int *)(0x40010C14)
GPIOB_ODR = 0XFFFF;
这里为了简便使用宏定义定义单个寄存器的映射,实际上对于大量寄存器的映射,使用结构体是最方便的方式,stm32f407xx.h 里面使用结构体方式对 STM32F407 的寄存器做了详细映射;
STM32F407 大部分外设寄存器地址都是在存储块 2 上面的。具体某个寄存器地址,由三个参数决定:
- 总线基地址(BUS_BASE_ADDR);
- 外设基于总线基地址的偏移量(PERIPH_OFFSET);
- 寄存器相对外设基地址的偏移量(REG_OFFSET)。
因此,某个具体某个寄存器地址可以表示为:
上表中 APB1 的基地址,也叫 外设基地址,表中的偏移量就是相对于外设基地址的偏移量。
外设基于总线基地址的偏移量(PERIPH_OFFSET),这个不同外设偏移量不一样,我们可以在 STM32F407 存储器映射图里面找到具体的偏移量,这里以 GPIO 为例,其偏移量如表所示:
知道了外设基地址,再在参考手册里面找到具体某个寄存器相对外设基地址的偏移量就可以知道该寄存器的实际地址了,这里以 GPIOB 的相关寄存器为例。
/* 外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)
/* 总线基地址 */
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHB1PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define AHB2PERIPH_BASE (PERIPH_BASE + 0x10000000)
/* GPIO 外设基地址 */
#define GPIOA_BASE (AHB1PERIPH_BASE + 0x0000)
#define GPIOB_BASE (AHB1PERIPH_BASE + 0x0400)
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800)
#define GPIOD_BASE (AHB1PERIPH_BASE + 0x0C00)
#define GPIOE_BASE (AHB1PERIPH_BASE + 0x1000)
#define GPIOF_BASE (AHB1PERIPH_BASE + 0x1400)
#define GPIOG_BASE (AHB1PERIPH_BASE + 0x1800)
#define GPIOH_BASE (AHB1PERIPH_BASE + 0x1C00)
#define GPIOI_BASE (AHB1PERIPH_BASE + 0x2000)
首先定义了 “片上外设” 基地址 PERIPH_BASE,接着在 PERIPH_BASE 上加入各个总线的地址偏移,得到 APB1、APB2、AHB1 和 AHB2 总线的基地址 APB1PERIPH_BASE、APB2PERIPH_BASE,AHB1PERIPH_BASE 和 AHB2PERIPH_BASE。然后在 AHB1 总线基地址上加上 GPIO 外设的地址偏移,得到 GPIOA~GPIOI 的外设基地址。
GPIOA ~ GPIOH 都各有一组功能相同的寄存器,如 GPIOA_MODER/GPIOB_MODER/GPIOC_MODER 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装。
typedef struct {
uint32_t MODER; /*GPIO 模式寄存器 地址偏移: 0x00 */
uint32_t OTYPER; /*GPIO 输出类型寄存器 地址偏移: 0x04 */
uint32_t OSPEEDR; /*GPIO 输出速度寄存器 地址偏移: 0x08 */
uint32_t PUPDR; /*GPIO 上拉/下拉寄存器 地址偏移: 0x0C */
uint32_t IDR; /*GPIO 输入数据寄存器 地址偏移: 0x10 */
uint32_t ODR; /*GPIO 输出数据寄存器 地址偏移: 0x14 */
uint16_t BSRRL; /*GPIO 置位/复位寄存器低 16 位部分 地址偏移: 0x18 */
uint16_t BSRRH; /*GPIO 置位/复位寄存器高 16 位部分 地址偏移: 0x1A */
uint32_t LCKR; /*GPIO 配置锁定寄存器 地址偏移: 0x1C */
uint32_t AFR[2]; /*GPIO 复用功能配置寄存器 地址偏移: 0x20-0x24 */
} GPIO_TypeDef;
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 8 个成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节。也就是说,假如我们定义一个 GPIO_TypeDef 类型的结构体,且结构体的首地址为 0x4002 0400(这也是第一个成员变量 MODER 的地址),那么结构体中第二个成员变量 OTYPER 的地址即为 0x4002 0400 +0x04 ,加上的这个 0x04 ,正是代表 MODER 所占用的 4 个字节地址的偏移量。其中的 BSRR 寄存器分成了低 16 位 BSRRL 和高 16 位 BSRRH,BSRRL 置 1 引脚输出高电平,BSRRH 置 1 引脚输出低电平,这里分开只是为了方便操作。
这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器了。
/* 使用 GPIO_TypeDef 把地址强制转换成指针 */
#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)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)