STM32学习笔记——四、存储器和寄存器映射
1 存储器映射
STM32系统框图
- 在图中,被控单元的FLASH、RAM、FSMC 和AHB 到APB 的桥(即片上外设),这些功能部件共同排列在一个4GB 的地址空间内。
- 编程时我们通过地址操作他们
1.1 存储器映射
- 存储器映射:
存储器本身不具有地址信息,芯片厂商或用户,给存储器分配地址的过程。
- 存储器重映射:
给存储器再分配一个地址
1.2 存储区域功能划分
在这4GB 的地址空间中,ARM 已经粗线条的平均分成了8 个块,每块512MB。
- Block0 用来设计成内部FLASH
- Block1 用来设计成内部RAM
- Block2 用来设计成片上的外设
1.2.1 存储器Block0内部区域功能划分
Block0 主要用于设计片内的FLASH
:STM32F103ZET6的FLASH是512KB,属于大容量。
1.2.3 储存器Block1内部区域功能划分
Block1 用于设计片内的SRAM
:STM32F103ZET6的SRAM 都是64KB,
1.2.4 储存器Block2内部区域功能划分
Block2 用于设计片内的外设,根据外设的总线速度不同,Block 被分成了APB 和AHB 两部分,其中APB 又被分为APB1 和APB2
2 寄存器映射
- 使用寄存器原由:
片上外设每个单元对应一个起始地址,可以通过C语言指针的操作方式来访问这些单元。
但是每次通过地址方式访问,不好记忆且容易出错
// GPIOB 端口全部输出高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
//在编译器看来,这只是一个普通的变量,是一个立即数,得进行强制类型转换,转换成指针,让编译器认为是指针
- 寄存器(内存单元的别名):
根据每个单元功能不同,以功能为名给这个内存单元取一个别名,这个别名就是寄存器
- 寄存器映射(过程):
给已经分配好地址的有特定功能的内存单元取别名的过程
// GPIOB 端口全部输出高电平
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;
把指针操作“*”也定义到寄存器别名里面
// GPIOB 端口全部输出高电平
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;
3 STM32外设地址映射
片上外设区:分为三条总线
- APB1:低速外设
- APB2、AHB:高速外设
3.1 总线基地址
总线的基地址:总线的最低地址,挂载在该总线上的首个外设的地址
相对外设基地址偏移:该总线地址与“片上外设”基地址0x4000 0000 的差值
3.2 外设基地址
外设基地址:APB1总线地址最低,片上外设从这里开始
- 以GPIO外设来为例了解外设的基地址,GPIO 属于高速的外设,挂载到APB2 总线上
3.3外设寄存器
在XX 外设的地址范围内,分布着的就是该外设的寄存器
3.3.1 了解外设寄存器(以GPIO外设为例)(通用输入输出端口,STM32可控制的引脚,控制引脚输出高低电平)
- GPIO 有很多个寄存器,每一个都有特定的功能。
- 每个寄存器为32bit,占四个字节。
- 在该外设的基地址上按照顺序排列,寄存器的位置都以相对该外设基地址的偏移地址来描述。
- 编程时,需要反复的查阅外设的寄存器说明,避免用错。
3.3.2 如何理解寄存器的说明(以“GPIO 端口置位/复位寄存器”为例)
- 名称:
寄存器的名称“(GPIOx_BSRR)(x=A⋯E)”
- 指寄存器名为“GPIOx_BSRR”其中的“x”可以为A-E,
- 这个寄存器说明适用于GPIOA、GPIOB 至GPIOE
- 这些GPIO 端口都有这样的一个寄存器。
- 偏移地址:
本寄存器相对于这个外设的基地址的偏移
- 本寄存器的偏移地址是0x10,
- 从参考手册中查到GPIOA 外设的基地址为0x4001 0800
- 算出GPIOA 的这个GPIOA_BSRR 寄存器的地址为:0x4001 0800+0x10;
- 寄存器位表:
表中列出它的0-31 位的名称及权限
- 表上方的数字为位编号
- 表中间为位名称
- 表最下方为读写权限(w只写,r只读,rw可读写)
- 位功能说明:
介绍了寄存器每一位的功能
例图中:
- 本寄存器中有两种寄存器位(BRy 、BSy)
- y 数值可以是0-15,表示端口的引脚号
- BR1、BS1 就是控制GPIOA第1 个引脚。
- 其他内容说明、
- “复位”是将该位设置为0 的意思,而“置位”表示
将该位设置为1 - 0时引脚输出低电平,1时引脚输出高电平
4 C语言对寄存器的封装
以下内容着重理解,记不住查表看得懂就好
4.1 封装总线和外设基地址
把总线基地址和外设基地址都以相应的宏定义起来
总线或者外设都以他们的名字作为宏名
- 1、首先,定义了“片上外设”基地址:PERIPH_BASE
- 2、接着,在PERIPH_BASE 上加入各个总线的地址偏移:得到APB1、APB2 总线的地址APB1PERIPH_BASE、APB2PERIPH_BASE
- 3、接着,在APB1、APB2 总线的地址之上加入外设地址的偏移,得到GPIOA-G 的外设地址
- 在GPIOx(A-G) 外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址
有了具体地址,就可以用指针读写
使用(unsigned int ) 把GPIOB_BSRR 宏的数值强制转换成了地址,然后再用“”号做取指针操作,对该地址的赋值
4.2 封装寄存器列表
出发点:用上面的方法去定义地址,还是稍显繁琐,例如GPIOA-GPIOE 都各有一组功能相同的寄存器,如GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。
改进办法:引入C 语言中的结构体语法对寄存器进行封装
- 用typedef 关键字声明了名为GPIO_TypeDef 的结构体类型
- 结构体内有7 个成员变量,变量名正好对应寄存器的名字。
- C语言中,结构体内变量的存储空间是连续的,其中32位的变量占用4 个字节,16 位的变量占用2 个字节
假如这个结构体的首地址为0x4001 0C00(这也是第一个成员变量CRL 的地址),那么结构体中第二个成员变量CRH 的地址即为0x4001 0C00 +0x04,加上的这个0x04,正是代表CRL 所占用的4 个字节地址的偏移量,其它成员变量相对于结构
- 只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器
用GPIO_TypeDef 类型定义一个结构体指针GPIOx, 并让指针指向地址GPIOB_BASE(0x4001 0C00),使用地址确定下来,然后根据C 语言访问结构体的语法,用GPIOx->ODR 及GPIOx->IDR 等方式读写寄存器。
- 更进一步,直接使用宏定义好GPIO_TypeDef 类型的指针,而且指针指向各个GPIO端口的首地址,使用时我们直接用该宏访问寄存器即可
其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。
5 修改即存在的位操作方法
使用C 语言对寄存器赋值时,要求只修改该寄存器的某几位的值,且其它的寄存器位不变,需要用到C 语言的位操作方法。
5.1 把变量的某位清零
变量a代表寄存器,假设寄存器本来已有数值,需要把a的某一位清零,其他位不变
//定义一个变量a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;
//对bit2 清零
a &= ~(1<<2);
//括号中的1 左移两位,(1<<2) 得二进制数:0000 0100 b
//按位取反,~(1<<2) 得1111 1011 b
//假如a 中原来的值为二进制数: a = 1001 1111 b
//所得的数与a 作”位与&”运算,a = (1001 1111 b)&(1111 1011 b),
//经过运算后,a 的值a=1001 1011 b
// a 的bit2 位被清零,而其它位不变。
5.2 对变量的某几位进行赋值
寄存器位经过清零操作后,可以对某几位写入所需要的数值,且其它位不变
写入的数值一般就是需要设置寄存器的位参数。
//a = 1000 0011 b
//此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”
a |= (1<<2*2);
//a = 1001 0011 b,成功设置了第2组的值,其它组不变
5.3 对变量的某位取反
需要对寄存器的某个位进行取反操作,即1变0,0变1,直接如下操作,其它位不变
//a = 1001 0011 b
//把bit6 取反,其它位不变
a ^=(1<<6);
//a = 1101 0011 b
5.4 操作符总结
操作符 | 操作符作用 | 操作符示例 |
---|---|---|
>> | 8>>3 | 8右移3位 |
<< | 1<<6 | 1左移6位 |
&= | a &= ~(1<<2) | 按位与后赋值(两个二进制的对应位都为1时,结果为1,否则结果等于0) |
^= | x = 0x02, x ^= 0x01, x = 0x03 | 按位异或后赋值(两个二进制的对应位相同,结果为0,否则结果为1) |
|= | x = 0x02, x ^= 0x01, x = 0x03 | 按位或后赋值(两个二进制对应位都为0时,结果等于0,否则结果等于1) |