STM32F7学习笔记4:寄存器
在学习STM32编程时,通常有两种编程方式,一种是寄存器编程,另外一种是函数库编程,其中寄存器编程是基础, 而函数库编程是在寄存器编程的基础上升级而来的一种易于学习和开发的编程方式,是我们学习STM32编程的时候需要重点掌握的一种编程方法。
1. STM32
STM实物如图所示:
外形介绍如下:
- 正面:正面是丝印,ARM应该是表示该芯片使用的是ARM的内核,STM32F767IGT6是芯片型号,后面的字应该是跟生产批次相关,最下面的是ST的LOGO。
- 四周:四周是引脚,左下角的小圆点表示1脚,然后从1脚起按照逆时针的顺序排列(所有芯片的引脚顺序都是逆时针排列的)。 开发板中把芯片的引脚引出来,连接到其他各种芯片上(比如传感器芯片), 然后在STM32上编程(实际就是通过程序控制这些引脚输出高电平或者低电平)来控制其他芯片工作,通过做实验的方式来学习STM32芯片的各个资源。
正面引脚图如下
2. 芯片内部结构
STM32F7采用Cortex-M7内核,内核即CPU,由ARM公司设计。ARM公司并不生产芯片,只出售其芯片技术授权。
芯片生产厂商(SOC)如ST、TI、NXP等,在内核之外设计部件并生产整个芯片。这些外设如CPIO、USART、I2C、SPI等叫做片上外设。
架构如下:
芯片主系统架构基于两个子系统,一个是AXI转多层AHB桥,多层AHB总线矩阵。
AXI转多层AHB桥,从AXI4协议转成AHB-Lite协议, 其中包含3个AXI转32-bit AHB桥通过32-bit的AHB总线矩阵连接到外部存储器FMC接口、 外部存储器Quad SPI接口、内部SRAM(SRAM1 and SRAM2)。 还包含一个AXI转64-bit AHB桥通过64-bit总线矩阵连接到内部FLASH。
多层AHB总线矩阵,其中32-bit多层AHB总线矩阵互联11个主设备和8个从设备,64-bit多层AHB总线矩阵则是CPU通过AXI转AHB桥通过这个64-bit多层AHB总线矩阵连接到内部Flash。 DMA主设备通过32-bit AHB总线矩阵通过这个64-bit多层AHB总线矩阵连接到内部Flash。
架构如下图所示:
主控总线通过一个总线矩阵来连接被控总线,总线矩阵用于主控总线之间的访问仲裁管理,仲裁采用循环调度算法。 总线之间交叉的时候如果有个圆圈则表示可以通信,没有圆圈则表示不可以通信。
3. 存储器映射
连接被控总线的是FLASH,RAM和片上外设,这些功能部件共同排列在一个4GB的地址空间内。 我们在编程的时候,操作的也正是这些功能部件。
3.1 存储器映射
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射,具体见下图。如果给存储器再分配一个地址就叫存储器重映射。
3.2 存储器区域功能划分
在这4GB的地址空间中,ARM已经粗线条的平均分成了8个块,每块512MB,每个块也都规定了用途,具体分类见表格 5‑1。每个块的大小都有512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。具体分类如下表:
序号 | 用途 | 地址范围 |
---|---|---|
Block0 | SRAM | 0x0000 0000~ 0x1FFF FFFF(512MB) |
Block1 | SRAM | 0x2000 0000~ 0x3FFF FFFF(512MB) |
Block2 | 片上外设 | 0x4000 0000~ 0x5FFF FFFF(512MB) |
Block3 | FMC的bank1~ bank2 | 0x6000 0000~ 0x7FFF FFFF(512MB) |
Block4 | FMC的bank3~ bank4 | 0x8000 0000~ 0x9FFF FFFF(512MB) |
Block5 | FMC | 0xA000 0000~ 0xCFFF FFFF(512MB) |
Block6 | FMC | 0xD000 0000~ 0xDFFF FFFF(512MB |
Block7 | Cortex-M7内部外设 | 0xE000 0000~ 0xFFFF FFFF(512MB) |
在这8个Block里面,有3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部FLASH,Block1用来设计成内部RAM,Block2用来设计成片上的外设。
3.2.1 存储器Block0内部区域功能划分
Block0是存放程序代码的区域。 用户也可以在此处放置数据。通过ITCM或AXIM接口执行指令提取和数据访问。
具体功能如下表:
地址 | 说明 |
---|---|
0x1FFF 0020~ 0x1FFF FFFF | 预留 |
0x1FFF 0000~ 0x1FFF 001F | 16个字节用于锁定对应的OTP数据块。 |
0x0820 0000~ 0x1FFE FFFF | 预留 |
0x0800 0000~ 0x081F FFFF | FLASH:我们的程序就放在这里。 |
0x0030 0000~ 0x07FF FFFF | 预留 |
0x0020 0000~ 0x003F FFFF | FLASH存储基于ITCM总线接口,不支 持写操作,即只读。 |
0x0011 0000~ 0x001F FFFF | 预留 |
0x0010 0000~ 0x0010 EDBF | 系统存储器:里面存的是ST出厂时烧写好的 ISP自举程序,用户无法改动。串口下载的 时候需要用到这部分程序。 |
0x0000 4000~ 0x000F FFFF | 预留 |
0x0000 0000~ 0x0000 3FFF | ITCM RAM,只能被CPU访问,不用经过总线矩 阵,属于高速的RAM。 |
3.2.2 存储器Block1内部区域功能划分
Block1用于设计片内的SRAM。用户也可以在这里进行编码。通过DTCM或AXIM接口执行指令提取和数据访问。
F767 内部SRAM的大小为512KB,分SRAM1 368KB,SRAM2 16KB,DTCM 128KB。
具体划分如下表
地址 | 说明 |
---|---|
0x2008 0000~ 0x3FFF FFFF | 预留 |
0x2007 C000~ 0x2007 FFFF | SRAM2 16KB |
0x2002 0000~ 0x2007 BFFF | SRAM1 368KB |
0x2000 0000~ 0x2001 FFFF | DTCM 128KB |
3.2.3 存储器Block2内部区域功能划分
Block2用于设计片内的外设,根据外设的总线速度不同,Block被分成了APB和AHB两部分,其中APB又被分为APB1和APB2,AHB分为AHB1和AHB2。
地址 | 说明 |
---|---|
0x4000 0000~ 0x4000 7FFF | APB1 总线外设 |
0x4000 7800~ 0x4000 FFFF | 预留 |
0x4001 0000~ 0x4001 6BFF | APB2 总线外设 |
0x4001 5800~ 0x4001 FFFF | 预留 |
0x4002 0000~ 0x4007 FFFF | AHB1 总线外设 |
0x4008 0000~ 0x4FFF FFFF | 预留 |
0x5000 0000~ 0x5006 0BFF | AHB2 总线外设 |
0x5006 0C00~ 0x5FFF FFFF | 预留 |
4. 寄存器映射
在存储器Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。
我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
4.1 STM32 的外设地址映射
片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB挂载低速外设,AHB挂载高速外设。
相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。
其中APB1总线的地址最低,片上外设从这里开始,也叫外设基地址。
4.1.1 总线基地址
F767的总线基地址如下表:
总线名称 | 总线基地址 | 相对外设基地址的偏移 |
---|---|---|
APB1 | 0x4000 0000 | 0x0 |
APB2 | 0x4001 0000 | 0x0001 0000 |
AHB1 | 0x4002 0000 | 0x0002 0000 |
AHB2 | 0x4802 0000 | 0x0802 0000 |
APB3 | 0x5000 0000 | 0x1000 0000 |
AHB3 | 0x5100 0000 | 0x1100 0000 |
APB4 | 0x5800 0000 | 0x1800 0000 |
AHB4 | 0x5802 0000 | 0x1802 0000 |
4.1.2 外设基地址
总线上挂载着各种外设,这些外设也有自己的地址范围,特定外设的首个地址称为“XX外设基地址”。
F767外设GPIO基地址
外设名称 | 外设基地址 | 相对AHB1总线的地址便宜 |
---|---|---|
GPIOA | 0x4002 0000 | 0x0 |
GPIOB | 0x4002 0400 | 0x0000 0400 |
GPIOC | 0x4002 0800 | 0x0000 0800 |
GPIOD | 0x4002 0C00 | 0x0000 0C00 |
GPIOE | 0x4002 1000 | 0x0000 1000 |
GPIOF | 0x4002 1400 | 0x0000 1400 |
GPIOG | 0x4002 1800 | 0x0000 1800 |
GPIOH | 0x4002 1C00 | 0x0000 1C00 |
4.1.3 外设寄存器
在XX外设的地址范围内,分布着的就是该外设的寄存器。以GPIO外设为例,GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚, 基本功能是控制引脚输出高电平或者低电平。最简单的应用就是把GPIO的引脚连接到LED灯的阴极,LED灯的阳极接电源,然后通过STM32控制该引脚的电平,从而实现控制LED灯的亮灭。
GPIO有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个字节,在该外设的基地址上按照顺序排列, 寄存器的位置都以相对该外设基地址的偏移地址来描述。这里我们以GPIOF端口为例,来说明GPIO都有哪些寄存器,
GPIOF端口的 寄存器地址列表
寄存器名称 | 寄存器地址 | 相对GPIOB基址的偏移 |
---|---|---|
GPIOF_MODER | 0x4002 1400 | 0x00 |
GPIOF_OTYPER | 0x4002 1404 | 0x04 |
GPIOF_OSPEEDR | 0x4002 1408 | 0x08 |
GPIOF_PUPDR | 0x4002 140C | 0x0C |
GPIOF_IDR | 0x4002 1410 | 0x10 |
GPIOF_ODR | 0x4002 1414 | 0x14 |
GPIOF_BSRR | 0x4002 1418 | 0x18 |
GPIOF_LCKR | 0x4002 141C | 0x1C |
GPIOF_AFRL | 0x4002 1420 | 0x20 |
GPIOF_AFRH | 0x4002 1424 | 0x24 |
4.1.4 GPIO示例讲解
示例如下图:
名称:F767的寄存器说明中首先列出了该寄存器中的名称,“(GPIOx_BSRR)(x=A…I)”这段的意思是该寄存器名为“GPIOx_BSRR”其中的“x”可以为A-I, 也就是说这个寄存器说明适用于GPIOA、GPIOB至GPIOI,这些GPIO端口都有这样的一个寄存器。
偏移地址:移地址是指本寄存器相对于这个外设的基地址的偏移。本寄存器的偏移地址是0x18,从参考手册中我们可以查到GPIOA外设的基地址为0x4002 0000。我们就可以算出GPIOA的这个GPIOA_BSRR寄存器的地址为:0x4002 0000+0x18 ; 同理,由于GPIOB的外设基地址为0x4002 0400, 可算出GPIOB_BSRR寄存器的地址为:0x4002 0400+0x18 。其他GPIO端口以此类推即可。
寄存器位表:紧接着的是本寄存器的位表,表中列出它的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的时候对应的引脚输出低电平即可(感兴趣的读者可以查询该寄存器GPIOx_ODR的说明了解)。 所以,如果对BR0写入“1”的话,那么GPIOx的第0个引脚就会输出“低电平”,但是对BR0写入“0”的话,却不会影响ODR0位,所以引脚电平不会改变。 要想该引脚输出“高电平”,就需要对“BS0”位写入“1”,寄存器位BSy与BRy是相反的操作。
5. C语言对寄存器的封装
5.1 封装总线和外设基地址
宏定义如下:
/* 外设基地址 */
#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)
/* 寄存器基地址,以GPIOF为例 */
#define GPIOF_MODER (GPIOF_BASE+0x00)
#define GPIOF_OTYPER (GPIOF_BASE+0x04)
#define GPIOF_OSPEEDR (GPIOF_BASE+0x08)
#define GPIOF_PUPDR (GPIOF_BASE+0x0C)
#define GPIOF_IDR (GPIOF_BASE+0x10)
#define GPIOF_ODR (GPIOF_BASE+0x14)
#define GPIOF_BSRR (GPIOF_BASE+0x18)
#define GPIOF_LCKR (GPIOF_BASE+0x1C)
#define GPIOF_AFRL (GPIOF_BASE+0x20)
#define GPIOF_AFRH (GPIOF_BASE+0x24)
使用指针控制BSRR寄存器
/* 控制GPIOH 引脚10输出低电平(BSRR寄存器的BR10置0) */
*(unsigned int *)GPIOH_BSRR = (0x01<<(16+10));
/* 控制GPIOH 引脚10输出高电平(BSRR寄存器的BS10置1) */
*(unsigned int *)GPIOH_BSRR = 0x01<<10;
unsigned int temp;
/* 控制GPIOH 端口所有引脚的电平(读IDR寄存器) */
temp = *(unsigned int *)GPIOH_IDR;
该代码使用 (unsigned int *)把GPIOH_BSRR宏的数值强制转换成了地址,然后再用“*”号做取指针操作,对该地址的赋值, 从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取STM32外设的状态。
5.2 封装寄存器列表
使用结构体对GPIO寄存器组的封装
typedef unsigned int uint32_t; /*无符号32位变量*/
typedef unsigned short int uint16_t; /*无符号16位变量*/
/* GPIO寄存器列表 */
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端口首地址:
/*使用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)
/*使用定义好的宏直接访问*/
/*访问GPIOH端口的寄存器*/
GPIOH->BSRR = 0xFFFF; //通过指针访问并修改GPIOH_BSRR寄存器
GPIOH->MODER = 0xFFFFFFF; //修改GPIOH_MODER寄存器
GPIOH->OTYPER =0xFFFFFFF; //修改GPIOH_OTYPER寄存器
uint32_t temp;
temp = GPIOH->IDR; //读取GPIOH_IDR寄存器的值到变量temp中
/*访问GPIOA端口的寄存器*/
GPIOA->BSRR = 0xFFFF; //通过指针访问并修改GPIOA_BSRR寄存器
GPIOA->MODER = 0xFFFFFFF; //修改GPIOA_MODER寄存器
GPIOA->OTYPER =0xFFFFFFF; //修改GPIOA_OTYPER寄存器
//uint32_t temp; //该变量前面已定义
temp = GPIOA->IDR; //读取GPIOA_IDR寄存器的值到变量temp中
5.3 寄存器操作
5.3.1 寄存器清零
代码如下:
/* 对某位清零 */
//定义一个变量 a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;
//对 bit2 清零
a &= ~(1<<2);
//上面一行代码右边括号中的1左移两位,(1<<2)得二进制数:0000 0100 b(这个便是位2的掩码)
//然后按位取反,~(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 位就被清零,而其它位不变。
/* 下面是对某几个连续位清零 */
//同样我们首先定义一个变量 a = 1001 1111 b (二进制数)
unsigned char a = 0x9f;
//若把a中的二进制位分成2个一组
//即 bit0、bit1为第0组,bit2、bit3为第1组,
// bit4、bit5为第2组,bit6、bit7为第3组
//现在,我们要对第1组的bit2、bit3清零
a &= ~(3<<2*1);
//括号中的3左移两位,(3<<2*1)得二进制数:0000 1100 b(这个是位3、位2的掩码)
//然后按位取反,~(3<<2*1)得 1111 0011 b
//假如a中原来的值为二进制数: a = 1001 1111 b
//所得的数与a作”位与&”运算: a = (1001 1111 b) & (1111 0011 b)
//经过运算后,a的值 a=1001 0011 b
//最后 a的第1组的bit2、bit3就被清零了,而其它位不变。
//上述 (~(3<<2*1)) 中的 1 即为组编号; 如清零第3组bit6、bit7此处应为3,即要左移6位
//括号中的 2 为每组的位数,每组有2个二进制位; 若分成4个一组,此处即为 4
//括号中的 3 是组内所有位都为1时的值; 若分成4个一组,此处即为二进制数“1111 b”
//例如对第2组bit4、bit5清零,3就要左移4位
a &= ~(3<<2*2);
5.3.2 寄存器置位
代码如下:
/* 对某位赋值 */
//假设 a = 1000 0011 b
a |= (1<<4);
//此时对变量 a 的 bit4 置1
//置1后,即 a = 1001 0011 b
/* 对某几位进行赋值 */
//假设 a = 1000 0011 b
a |= (1<<2*2);
//此时对清零后的第2组bit4、bit5设置成二进制数“01 b ”(也就是“01 b”左移4位)
//即 a = 1001 0011 b,成功设置了第2组的值,其它组不变
5.3.3 寄存器取反
//a = 1001 0011 b
//把bit6取反,其它位不变
a ^=(1<<6);
//a = 1101 0011 b