一、存储器映射
- 存储器是不具备地址信息的,地址是由芯片厂商和用户进行分配,给存储器分配地址的过程称为存储器映射,再分配一个地址就叫重映射。
- STM32芯片存储器映射图
- 从图中可以看出ARM将4GB分为了8块,block0——block7,每一块512Mb
- 其中block0:包含了STM32芯片内部的Flash。这一块内存主要用来设计芯片的Flash,ZET6为512K的Flash,要集成更大的Flash和SRAM,意味着芯片的成本就要增加,所以往往片内集成的Flash不会太大
- block1:包含SRAM(64KB)
- block2:包含了STM32的一些片内外设资源
- 存储器区域划分
二、寄存器和寄存器映射
- 存储器内部4个字节为1个单元,每个单元对应不同的功能,当我们要控制对应的单元的时候,就要控制对应的外设,每一个单元还对应一个地址,操作这些单元就是要对这些地址进行访问
- 由于STM32的外设非常多,而且复杂,如果每操作一个外设,就要写一大串对应的存储单元的地址,非常麻烦,容易写错。
- 因此,我们把每个单元的功能作为名,给这个内存取一个别名,这个别名就是我们经常说的寄存器。通过C语言指针操作这个寄存器,就可以实现访问它的内容。寄存器即有特定功能的内存单元。
- 寄存器映射:给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
三、基地址
- 访问STM32的寄存器就是操作这些内存单元,通常会用指针操作STM32的内存单元
- 片上外设区分为4条总线,根据外设速度的不同,不同总线上面挂着不同的外设
- APB1挂载着低速的外设
- APB2和AHB挂载着高速的外设
- 相应总线的最低地址称之为总线的基地址
- 圈住部分为APB1总线的基地址
- 0x40000400比基地址增加的值称为偏移地址(相对于基地址的偏移地址)
- 偏移量:两个地址相减
- STM32总线地址如下:
- GPIO外设地址如下:
四、外设寄存器地址
- 我们以 GPIOB 端口为例,来说明GPIO都有哪些寄存器:
- 手册怎么使用
- 1为寄存器名称
- 2为数字序号,代表寄存器的每一个位,一个寄存器占用4个字节,32位,所以从0-31,一共32位
- 3为方框中的内容,可以参考下面表格找到对应的功能
- 4是方框下方的字母,代表权限,w表示只写,r代表只能读,rw表示可读可写
- 5是表格中的内容,是介绍每一个位它具体的含义
五、使用C语言指针操作寄存器
总线和外设基地址封装
#define PERIPH_BASE ((unsigned int)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000)
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00)
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0x00)
#define GPIOB_CRH *(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR *(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
#define GPIOB_BSRR *(unsigned int*)(GPIOB_BASE+0x10)
#define GPIOB_BRR *(unsigned int*)(GPIOB_BASE+0x14)
#define GPIOB_LCKR *(unsigned int*)(GPIOB_BASE+0x18)
- 上述第一行为外设基地址
- 定义宏的时候会使用大写的字母,使用有意义的字母去定义
- (GPIOB_BASE+0x00),编译器是不认识的,只认为这是一个值加上一个值,得到的还是一个值,并不认为这是地址
- 我们要操作地址,才能访问对应的单元,那么怎么才能操作地址呢?
- 所以,要通过指针
- 首先,要强制定义为无符号整型的一个指针(unsigned int*),这个指针代表的是一个地址,要访问它的内容,就要再前面再加一个指针
- 指针的指针就是它地址里面的值
- 得到了这些地址之后,我们就可以使用C语言的指针操作读写了
- 举例
- 比如要控制GPIOB5输出一个低电平或者高电平:
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+5));
- BSRR:端口位设置/清除寄存器
- 解释:对于BSRR这个寄存器来说,低16位输出高电平,高16位是用来清0,也就是输出低电平
- +5是因为要操作第5个管脚,加16是因为高16位用来输出低电平,加16以后再加5,即操作第5个管脚,控制它位低电平
- 0x01对应的是赋值,赋一位为1,当赋1的时候才会进行操作
*(unsigned int *)GPIOB_BSRR = (0x01<<5);
- 比如要读取GPIOB管脚所有电平:
unsigned int temp;
- 定义一个变量,用来管脚保存的电平,
temp = *(unsigned int *)GPIOB_IDR;
- IDR:端口输入数据寄存器,会把16个管脚的电平全部反馈出来
- 比如要控制GPIOB5输出一个低电平或者高电平:
六、使用C语言结构体操作寄存器
寄存器封装
- GPIOA,GPIOB,GPIOC等等,他们都拥有一组功能相同的寄存器,只是地址不同,为了更方便的访问寄存器,就引入了C语言当中的结构体,来对寄存器进行封装
typedef unsigned int uint32_t; typedef unsigned short int uint16_t; typedef struct { uint32_t CRL; uint32_t CRH; uint32_t IDR; uint32_t ODR; uint32_t BSRR; uint32_t BRR; uint16_t LCKR; }GPIO_TypeDef;
- 结构体类型中定义了7个成员变量,变量名称和寄存器名称一致
- C语言当中的语法规定,结构体内的成员变量存储在一片连续的存储空间,所以只需要知道结构体的首地址,依次可以类推其他的地址。CRL的地址与首地址一致,其余顺序往下加,多几个字节加几个,如:CRH是多4个字节。
- 定义成员变量的时候,需要注意:根据寄存器的位数定义它的类型
- 例如:CRL4个字节,所以定义的uint32_t(无符号整型)的一个类型,LCKR是两个字节的一个变量
- 例如:使用GPIOB的第5个引脚来访问BSRR寄存器来设置一个低电平
// 定义一个指针的变量(该变量是结构体类型)指向对应的地址 GPIO_TypeDef * GPIOx; // 指向GPIOB端口的一个基地址 GPIOx = GPIOB_BASE; // 用结构体的语法访问BSRR的成员,设置低电平 GPIOx->BSRR =(1<<(16+5));
// 直接使用宏定义来定义GPIO的端口 // 通过GPIO结构体类型的指针,强制转化成一个指针变量 #define GPIOB ((GPIO_TypeDef *) GPIOB_BASE) // 用结构体的语法访问BSRR的成员,设置低电平 GPIOB->BSRR = (1<<(16+5));