1. 存储器映射
被控单元的 FLASH, RAM, FSMC 和 AHB 到 APB 的桥(即片上外设),这些功能部件共同排列在一个 4GB 的地址空间内。我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过 C 语言对它们进行数据的读和写)。
1.1 存储器映射
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见 图 6-5 。 如果给存储器再分配一个地址就叫存储器重映射。
图 6-5 存储器映射(摘自参考手册-存储器映射章节)
1.1.1 存储器区域功能划分
在这 4GB 的地址空间中, ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途,具体分类见 表格 6-1 。每个块的大小都有 512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。
表格 6-1 存储器功能分类
序号 | 用途 | 地址范围 |
---|
Block 0 | Code | 0x0000 0000 ~ 0x1FFF FFFF(512MB) | Block 1 | SRAM | 0x2000 0000 ~ 0x3FFF FFFF(512MB) | Block 2 | 片上外设 | 0x4000 0000 ~ 0x5FFF FFFF(512MB) | Block 3 | FSMC 的 bank1 ~ bank2 | 0x6000 0000 ~ 0x7FFF FFFF(512MB) | Block 4 | FSMC 的 bank3 ~ bank4 | 0x8000 0000 ~ 0x9FFF FFFF(512MB) | Block 5 | FSMC 寄存器 | 0xA000 0000 ~ 0xCFFF FFFF(512MB) | Block 6 | 没有使用 | 0xD000 0000 ~ 0xDFFF FFFF(512MB) | Block 7 | Cortex-M3 内部外设 | 0xD000 0000 ~ 0xDFFF FFFF(512MB) |
在这 8 个 Block 里面,有 3 个块非常重要,也是我们最关心的三个块。 Block0 用来设计成内部 FLASH, Block1 用来设计成内部 RAM, Block2 用来设计成片上的外设。
存储器 Block0 内部区域功能划分 Block0 主要用于设计片内的 FLASH,我们使用的 STM32F103ZET6(霸道)和STM32F103VET6(指南者)的 FLASH 都是 512KB,属于大容量。要在芯片内部集成更大的 FLASH 或者 SRAM 都意味着芯片成本的增加,往往片内集成的 FLASH 都不会太大, ST 能在追求性价比的同时做到 512KB,实乃良心之举。 Block 内部区域的功能划分具体见 表格 6-2。
表格 6-2 存储器 Block0 内部区域功能划分
块 | 用途说明 | 地址范围 |
---|
Block0 | 预留 | 0x1FFE C008 ~ 0x1FFF FFFF | Block0 | 选项字节:用于配置读写保护、BOR 级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。当芯片不小心被锁住之后,我们可以从 RAM 里面启动来修改这部分相应的寄存器位。 | 0x1FFF F800 - 0x1FFF F80F | Block0 | 系统存储器:里面存的是 ST 出厂时烧写好的 isp 自举程序( 即Bootloader),用户无法改动。串口下载的时候需要用到这部分程序。 | 0x1FFF F000- 0x1FFF F7FF | Block0 | 预留 | 0x0808 0000 ~ 0x1FFF EFFF | Block0 | FLASH:我们的程序就放在这里。 | 0x0800 0000 ~ 0x0807 FFFF (512KB) | Block0 | 预留 | 0x0008 0000 ~ 0x07FF FFFF | Block0 | 取决于 BOOT 引脚,为 FLASH、系统存储器、 SRAM 的别名。 | 0x0000 0000 ~ 0x0007 FFFF |
储存器 Block1 内部区域功能划分 Block1 用于设计片内的 SRAM 。 我们使用的 STM32F103ZET6( 霸 道 )和 STM32F103VET6(指南者)的 SRAM 都是 64KB, Block 内部区域的功能划分具体见 表格 6-3。
表格 6-3 存储器 Block1 内部区域功能划分
块 | 用途说明 | 地址范围 |
---|
Block1 | 预留 | 0x2001 0000 ~ 0x3FFF FFFF | Block1 | SRAM 64KB | 0x2000 0000 ~0x2000 FFFF |
储存器 Block2 内部区域功能划分 Block2 用于设计片内的外设,根据外设的总线速度不同, Block 被分成了 APB 和 AHB 两部分,其中 APB 又被分为 APB1 和 APB2,具体见 表格 6-4。
表格 6-4 存储器 Block2 内部区域功能划分
块 | 用途说明 | 地址范围 |
---|
Block2 | APB1 总线外设 | 0x4000 0000 ~ 0x4000 77FF | Block2 | APB2 总线外设 | 0x4001 0000 ~ 0x4001 3FFF | Block2 | AHB 总线外设 | 0x4001 8000 ~ 0x5003 FFFF |
2. 寄存器映射
存储器本身没有地址,给存储器分配地址的过程叫存储器映射。 在存储器 Block2 这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。 比如,我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x4001 0C0C(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解), ODR 寄存器是 32bit,低 16bit 有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOB 的 16 个 IO 都输出高电平,具体见 代码 6-1 。
代码 6-1 通过绝对地址访问内存单元
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
0x4001 0C0C 在我们看来是 GPIOB 端口 ODR 的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即 (unsigned int *)0x4001 0C0C,然后再对这个指针进行 * 操作。
刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作,具体见 代码 6-2。
代码 6-2 通过寄存器别名方式访问内存单元
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;
为了方便操作,我们干脆把指针操作 “ * ” 也定义到寄存器别名里面,具体见 代码 6-3。
代码 6-3 通过寄存器别名访问内存单元
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;
2.1 STM32 的外设地址映射
片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1 挂载低速外设, APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。
2.1.1 总线基地址
表格 6-5 总线基地址
总线名称 | 总线基地址 | 总线基地址 |
---|
APB1 | 0x4000 0000 | 0x0 | APB2 | 0x4001 0000 | 0x0001 0000 | AHB | 0x4001 8000 | 0x0001 8000 |
表格 6-5 的“相对外设基地址偏移”即该总线地址与“片上外设”基地址 0x4000 0000 的差值。
2.1.2 外设基地址
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为 “ XX 外设基地址 ” ,也叫 XX 外设的边界地址。 这里面我们以 GPIO 这个外设来讲解外设的基地址, GPIO 属于高速的外设 ,挂载到 APB2 总线上,具体见 表格 6-6。
表格 6-6 外设 GPIO 基地址
外设名称 | 外设基地址 | 相对 APB2 总线的地址偏移 |
---|
GPIOA | 0x4001 0800 | 0x0000 0800 | GPIOB | 0x4001 0C00 | 0x0000 0C00 | GPIOC | 0x4001 1000 | 0x0000 1000 | GPIOD | 0x4001 1400 | 0x0000 1400 | GPIOE | 0x4001 1800 | 0x0000 1800 | GPIOF | 0x4001 1C00 | 0x0000 1C00 | GPIOG | 0x4001 2000 | 0x0000 2000 |
2.1.3 外设寄存器
在 XX 外设的地址范围内,分布着的就是该外设的寄存器。以 GPIO 外设为例, GPIO是通用输入输出端口的简称,简单来说就是 STM32 可控制的引脚,基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把 GPIO 的引脚连接到 LED 灯的阴极, LED 灯的阳极接电源,然后通过 STM32 控制该引脚的电平,从而实现控制 LED 灯的亮灭。 GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为 32bit ,占四个字节,在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以 GPIOB 端口为例,来说明 GPIO 都有哪些寄存器,具体见 表格 6-7 。
表格 6-7 GPIOB 端口的 寄存器地址列表
寄存器名称 | 寄存器地址 | 相对 GPIOB 基址的偏移 |
---|
GPIOB_CRL | 0x4001 0C00 | 0x00 | GPIOB_CRH | 0x4001 0C04 | 0x04 | GPIOB_IDR | 0x4001 0C08 | 0x08 | GPIOB_ODR | 0x4001 0C0C | 0x0C | GPIOH_BSRR | 0x4001 0C10 | 0x10 | GPIOH_BRR | 0x4001 0C14 | 0x14 | GPIOH_LCKR | 0x4001 0C18 | 0x18 |
有关外设的寄存器说明可参考 《STM32F10xx 参考手册》中具体章节的寄存器描述部分,在编程的时候我们需要反复的查阅外设的寄存器说明。 这里我们以 “GPIO 端口置位/复位寄存器”为例,教大家如何理解寄存器的说明,具体见 图 6-6。
图 6-6 GPIO 端口置位/复位寄存器说明
①名称 寄存器说明中首先列出了该寄存器中的名称, “(GPIOx_BSRR)(x=A…E)” 这段的意思是该寄存器名为 “GPIOx_BSRR” 其中的 “x” 可以为 A-E ,也就是说这个寄存器说明适用于 GPIOA、 GPIOB 至 GPIOE ,这些 GPIO 端口都有这样的一个寄存器。
②偏移地址 偏移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是 0x18,从参考手册中我们可以查到 GPIOA 外设的基地址为 0x4001 0800 ,我们就可以算出 GPIOA 的这个 GPIOA_BSRR 寄存器的地址为: 0x4001 0800+0x18 ;同理,由于 GPIOB 的外设基地址为 0x4001 0C00,可算出 GPIOB_BSRR 寄存器的地址为: 0x4001 0C00+0x18 。其他 GPIO 端口以此类推即可。
(PS:个人理解) { 例如:GPIOB 为例 在《STM32F1xx中文参考手册.pdf》中第28页,可以找到 GPIOB 外设的基地址为 0X4001 0C00(个人理解图 6-1)
个人理解图 6-1
端口输出数据寄存器 (GPIOx_ODR) 寄存器的偏移地址是 0X0C(个人理解图 6-2) 个人理解图 6-2
可以算出 GPIOB 的这个 GPIOB_ODR 寄存器的地址为:0X4001 0C00 + 0X0C = 4001 0C0C? }
③寄存器位表 紧接着的是本寄存器的位表,表中列出它的 0-31 位的名称及权限。表上方的数字为位编号,中间为位名称,最下方为读写权限,其中 w 表示只写, r 表示只读, rw 表示可读写。本寄存器中的位权限都是 w,所以只能写,如果读本寄存器,是无法保证读取到它真正内容的。而有的寄存器位只读,一般是用于表示 STM32 外设的某种工作状态的,由 STM32 硬件自动更改,程序通过读取那些寄存器位来判断外设的工作状态。
④位功能说明 位功能是寄存器说明中最重要的部分,它详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为 BRy 及 BSy ,其中的 y 数值可以是 0-15 ,这里的 0-15 表示端口的引脚号,如 BR0、 BS0 用于控制 GPIOx 的第 0 个引脚,若 x 表示 GPIOA ,那就是控制 GPIOA 的第 0 引脚,而 BR1 、 BS1 就是控制 GPIOA 第 1 个引脚。 其中 BRy 引脚的说明是 “0:不会对相应的 ODRx 位执行任何操作; 1:对相应 ODRx 位进行复位”。这里的“复位”是将该位设置为 0 的意思,而“置位”表示将该位设置为 1 ;说明中的 ODRx 是另一个寄存器的寄存器位,我们只需要知道 ODRx 位为 1 的时候,对应的引脚 x 输出高电平,为 0 的时候对应的引脚输出低电平即可。所以,如果对 BR0 写入 “1” 的话,那么 GPIOx 的第 0 个引脚就会输出“低电平”,但是对 BR0 写入 “0” 的话,却不会影响 ODR0 位,所以引脚电平不会改变。要想该引脚输出“高电平”,就需要对 “BS0” 位写入 “1” ,寄存器位 BSy 与 BRy 是相反的操作。
2.2 C 语言对寄存器的封装
2.2.1 封装总线和外设基地址
在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名,具体见 代码 6-4 。
代码 6-4 总线和外设基址宏定义
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x00020000)
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000)
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400)
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800)
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00)
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000)
#define GPIOB_CRL (GPIOB_BASE+0x00)
#define GPIOB_CRH (GPIOB_BASE+0x04)
#define GPIOB_IDR (GPIOB_BASE+0x08)
#define GPIOB_ODR (GPIOB_BASE+0x0C)
#define GPIOB_BSRR (GPIOB_BASE+0x10)
#define GPIOB_BRR (GPIOB_BASE+0x14)
#define GPIOB_LCKR (GPIOB_BASE+0x18)
代码 6-4 首先定义了“片上外设”基地址 PERIPH_BASE ,接着在 PERIPH_BASE 上加入各个总线的地址偏移,得到 APB1 、 APB2 总线的地址 APB1PERIPH_BASE 、 APB2PERIPH_BASE ,在其之上加入外设地址的偏移,得到 GPIOA-G 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可以用指针读写,具体见 代码 6-5。
代码 6-5 使用指针控制 BSRR 寄存器
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+0));
*(unsigned int *)GPIOB_BSRR = 0x01<<0;
unsigned int temp;
temp = *(unsigned int *)GPIOB_IDR;
该代码使用 (unsigned int *) 把 GPIOB_BSRR 宏的数值强制转换成了地址,然后再用 “ * ” 号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32 外设的状态。
2.2.2 封装寄存器列表
用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOE 都各有一组功能相同的寄存器,如 GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装,具体见 代码 6-6。
代码 6-6 使用结构体对 GPIO 寄存器组的封装
typedef unsigned int uint32_t; /*无符号 32 位变量*/
typedef unsigned short int uint16_t; /*无符号 16 位变量*/
/* GPIO 寄存器列表 */
typedef struct {
uint32_t CRL; /*GPIO 端口配置低寄存器 地址偏移: 0x00 */
uint32_t CRH; /*GPIO 端口配置高寄存器 地址偏移: 0x04 */
uint32_t IDR; /*GPIO 数据输入寄存器 地址偏移: 0x08 */
uint32_t ODR; /*GPIO 数据输出寄存器 地址偏移: 0x0C */
uint32_t BSRR; /*GPIO 位设置/清除寄存器 地址偏移: 0x10 */
uint32_t BRR; /*GPIO 端口位清除寄存器 地址偏移: 0x14 */
uint16_t LCKR; /*GPIO 端口配置锁定寄存器 地址偏移: 0x18 */
} GPIO_TypeDef;
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量,变量名正好对应寄存器的名字。 C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节, 16 位的变量占用 2 个字节,具体见图 6-7。
图 6-7 GPIO_TypeDef 结构体成员的地址偏移
也就是说,我们定义的这个 GPIO_TypeDef ,假如这个结构体的首地址为 0x4001 0C00(这也是第一个成员变量 CRL 的地址),那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 + 0x04,加上的这个 0x04,正是代表 CRL 所占用的 4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。 这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器,具体见 代码 6-7。
代码 6-7 通过结构体指针访问寄存器
GPIO_TypeDef * GPIOx;
GPIOx = GPIOB_BASE;
GPIOx->IDR = 0xFFFF;
GPIOx->ODR = 0xFFFF;
uint32_t temp;
temp = GPIOx->IDR;
这段代码先用 GPIO_TypeDef 类型定义一个结构体指针 GPIOx ,并让指针指向地址 GPIOB_BASE(0x4001 0C00) ,使用地址确定下来,然后根据 C 语言访问结构体的语法,用 GPIOx->ODR 及 GPIOx->IDR 等方式读写寄存器。 最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 GPIO 端口的首地址,使用时我们直接用该宏访问寄存器即可,具体 代码 6-8。
代码 6-8 定义好 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)
#define GPIOF ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH ((GPIO_TypeDef *) GPIOH_BASE)
GPIOB->BSRR = 0xFFFF;
GPIOB->CRL = 0xFFFF;
GPIOB->ODR =0xFFFF;
uint32_t temp;
temp = GPIOB->IDR;
GPIOA->BSRR = 0xFFFF;
GPIOA->CRL = 0xFFFF;
GPIOA->ODR =0xFFFF;
uint32_t temp;
temp = GPIOA->IDR;
这里我们仅是以 GPIO 这个外设为例,给大家讲解了 C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。
2.3 修改寄存器的位操作方法
使用 C 语言对寄存器赋值时,我们常常要求只修改该寄存器的某几位的值,且其它的寄存器位不变,这个时候我们就需要用到 C 语言的位操作方法了。
2.3.1 把变量的某位清零
此处我们以变量 a 代表寄存器,并假设寄存器中本来已有数值,此时我们需要把变量 a 的某一位清零,且其它位不变,方法见 代码 6-1
代码 6-1 对某位清零
unsigned char a = 0x9f;
a &= ~(1<<2);
2.3.2 把变量的某几个连续位清零
由于寄存器中有时会有连续几个寄存器位用于控制某个功能,现假设我们需要把寄存器的某几个连续位清零,且其它位不变,方法见代码 清单 6-2 。
代码清单 6-2 对某几个连续位清零
a &= ~(3<<2*1);
a &= ~(3<<2*2);
2.3.3 对变量的某几位进行赋值
寄存器位经过上面的清零操作后,接下来就可以方便地对某几位写入所需要的数值了,且其它位不变,方法见 代码清单 6-3,这时候写入的数值一般就是需要设置寄存器的位参数。
代码清单 6-3 对某几位进行赋值
a |= (1<<2*2);
2.3.4 对变量的某位取反
某些情况下,我们需要对寄存器的某个位进行取反操作,即 1 变 0 , 0 变 1,这可以直接用如下操作,其它位不变,见 代码清单 6-4 。
代码清单 6-4 对某位进行取反操作
a ^=(1<<6);
|