1. 寄存器基本原理
寄存器是单片机内部一种特殊的内存,可以实现对单片机各个功能的控制,我们编写程序最终就是去控制寄存器
下面的举例平台为STM32F407ZG
1.1 STM32寄存器分类
大类 | 小类 | 说明 |
---|---|---|
内核寄存器 | 内核相关寄存器 | 包含R0~R15、xPSR、特殊功能寄存器等 |
中断控制寄存器 | 包含NVIC和SCB相关寄存器,NVIC有:ISER、ICER、ISPR、IP等;SCB有:VTOR、AIRCR、SCR等 | |
SysTick寄存器 | 包含CTRL、LOAD、VAL和CALIB四个寄存器 | |
内存保护寄存器 | 可选功能,STM32F103没有 | |
调试系统寄存器 | ETM、ITM、DWT、IPIU等相关寄存器 | |
外设寄存器 | 包含GPIO、UART、IIC、SPI、TIM、DMA、ADC、DAC、RTC、I/WWDG、PWR、CAN、USB等各种外设寄存器 |
2. 寄存器映射
给寄存器地址命名的过程就叫寄存器映射
在 STM32F4 中 0x4002 000C 是 GPIOA_PUPDR 的地址,但是我们直接看 0x4002 000C 并不知道他是谁的地址,虽然我们可以通过查找手册的方式找到,但是这样的方式实在的过于繁琐而且不利于我们后续开发程序,所以我们需要对寄存器地址命名,以便于我们看到新的名字就知道他的作用是什么
2.1 地址映射基本概念
我们首先要知道下面的概念:
- 总线基地址(BUS_ BASE_ ADDR)
- 外设基于总线基地址的偏移量(PERIPH_OFFSET)
- 寄存器相对外设基地址的偏移量(REG_OFFSET)
- 寄存器地址= BUS_BASE_ADDR + PERIPH_OFFSET + REG_OFFSET
而上面的地址可以很轻易的在手册中找到,下面以寻找 STM32F4 中 GPIOA_PUPDR
的地址为例
2.2 如何通过手册查找寄存器地址
首先我们要明确要寻找的地址是什么东西(总线?外设?寄存器?)。GPIOA_PUPDR
是 GPIOA_PUPDR
的一个寄存器,所以我们在这里要寻找的是一个寄存器,而寄存器是基于外设偏移的,而外设是基于总线偏移的,所以我们需要先找到 GPIOA
是挂载到哪一个总线的哪一个外设上的。
打开《STM32F4xx参考手册》,此手册可以在正点原子资料中免费获取,也可以去 ST 官方下载英文版或者中文社区寻找中文版资料,
在2.3 存储器映射
中的表2.STM32F4xx
寄存器边界地址,找到外设为 GPIOA
的地方,如下图所示:
①:即是我们要寻找的外设
②:可以通过表格看到 GPIOA
是挂载在 AHB1
上
③:GPIOA
的地址刚好是 AHB1
最低的地址,也就是说相对于 AHB1
的起始地址没有偏移,也即 BUS_ BASE_ ADDR = 0x4002 0000,PERIPH_OFFSET=0
④:点击这个超链接我们可以看到 GPIOA
其他寄存器的详细情况
点击④会跳转到 7.4.11 GPIO 寄存器映射
,在表32.GPIO 寄存器映射和复位值
中,寻找 GPIOA_PUPDR
如下图所示:
①:即是我们要寻找的寄存器
②:相对于 GPIOA
基地址的偏移量,也即是 REG_OFFSET=0x0C
③:可以看到这是 32 位的寄存器,也就是每一个寄存器的大小是 4B(后续会用到)
④:注意到 GPIOB_PUPDR
和 GPIOA_PUPDR
甚至 GPIOx_PUPDR
(x为A…I)都是同一个偏移量,这就为我们使用一个结构体来初始化所有的GPIOx提供了可能(往后继续看)
到这里我们已经得到了所有的地址了
BUS_ BASE_ ADDR | PERIPH_OFFSET | REG_OFFSET |
---|---|---|
0x4002 0000 | 0x00 | 0x0C |
把他们拼接起来就得到了 GPIOA_PUPDR = 0x4002 000C
那么我们如何对这个地址的正确性进行验证呢?
理论上可以使用 keil5 的 debug 功能来调试查看寄存器的地址,但是我试过好像有点问题,所以换一种方式来验证。使用正点原子的串口例程,我们使用串口把地址发送到电脑上进行查看,我编写了如下的程序
int main(void)
{
uint8_t len;
uint16_t times = 0;
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(336, 8, 2, 7); /* 设置时钟,168Mhz */
delay_init(168); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
while(1)
{
printf("%p\r\n",&GPIOA->PUPDR);
delay_ms(1000);
}
}
收到的结果为
与我们计算的结果一致,说明我们的计算方式是正确的,下面将会从代码层面寻找他是如何拼接地址的
2.3 寄存器命名的方式
2.3.1 直接操作寄存器
由于 GPIOA_PUPDR
是 32 位的,所以我们需要使用 int
类型的指针来拿他的地址,使用(unsigned int *)
把地址强转为指针类型,然后使用 *
访问地址所指向的值,来对寄存器进行赋值(需要注意寄存器是否可读、可写等)
*(unsigned int *)(0x4002 000C)=0XFFFF;
2.3.2 命名后再操作寄存器
使用宏定义对地址进行了一次命名,相较于第一种方式可读性更好
#define GPIOA_PUPDR *(unsigned int *)(0x4002 000C)
GPIOA_PUPDR = 0XFFFF;
2.3.3 使用结构体命名寄存器
下面是 stm32f407xx.h
里面的内容,他使用了结构体的方式来完成寄存器的映射,下面将会讲解为什么可以通过结构体进行映射
typedef struct
{
__IO uint32_t MODER; /*!< GPIO port mode register, Address offset: 0x00 */
__IO uint32_t OTYPER; /*!< GPIO port output type register, Address offset: 0x04 */
__IO uint32_t OSPEEDR; /*!< GPIO port output speed register, Address offset: 0x08 */
__IO uint32_t PUPDR; /*!< GPIO port pull-up/pull-down register, Address offset: 0x0C */
__IO uint32_t IDR; /*!< GPIO port input data register, Address offset: 0x10 */
__IO uint32_t ODR; /*!< GPIO port output data register, Address offset: 0x14 */
__IO uint32_t BSRR; /*!< GPIO port bit set/reset register, Address offset: 0x18 */
__IO uint32_t LCKR; /*!< GPIO port configuration lock register, Address offset: 0x1C */
__IO uint32_t AFR[2]; /*!< GPIO alternate function registers, Address offset: 0x20-0x24 */
} GPIO_TypeDef;
通过 c 语言的基础知识我们知道,结构体变量的名字实际代表的是结构体的首地址,而结构体内部成员是顺序排列的,在上面的手册中可以看到 GPIOA
的寄存器(除了 AFR
)都是 32 位寄存器,意味着我们可以在结构体中依次按照寄存器的名字进行成员的定义。例如 GPIOA_PUPDR
,我们在手册中看到基于 GPIOA
的偏移量为 0x0C
,根据上面代码的定义 __IO uint32_t PUPDR;
位于第 4 行,前面有 3 行其他成员的定义,一共占用空间为 32bit x 3 =12B,正好对应了在手册中的偏移量,同时我们也可以在代码的注释中看到地址偏移为0x0C
。上一节提到了 GPIOx_PUPDR
(x为A…I)都是同一个偏移量,所以当 GPIO_TypeDef
的基地址不同的时候,便可以定义不同组的 IO 口了,这便是使用结构体的好处。
接下来通过源码的方式来寻找 GPIOx_PUPDR
的地址。
打开 stm32f407xx.h
,搜索 GPIOA
,可以看到下图的宏定义,这里的宏定义和上一小节讲的是一样的,但是他还套娃了很多层,
我们双击 GPIOA_BASE
,按照下图方式操作
可以得到这个
我们继续重复上面的步骤
继续重复
直到这里我们知道了他的地址结构为
PERIPH_BASE | AHB1PERIPH_BASE | GPIOA_BASE |
---|---|---|
0x40000000UL | PERIPH_BASE + 0x00020000UL | AHB1PERIPH_BASE + 0x0000UL |
计算得到 GPIOA_BASE = 0x4002 0000
,这个地址通过 #define GPIOA ((GPIO_TypeDef *) GPIOA_BASE)
就转化为了 GPIOA
结构体的基地址了,然后我们再加上 GPIOx_PUPDR
相对于 GPIOA
偏移量就得到了 GPIOx_PUPDR = 0x4002 000C
至此寄存器映射就结束了,可以通过类似的方式查找到所有的寄存器地址,多看手册和程序源码。