开始学习之前,你手上需要准备好以下三样物品:STM32原理图、STM32的中文参考手册以及一份从网上下载好的开发指南。
在第一篇学习笔记中还有以前学习51单片机的过程中,我们已经学习过通过操作寄存器来实现某种操作。事实上不管是什么单片机,我们在写程序控制他们的时候,本质上都是在操作寄存器。51也好,32也好,我们都会提到寄存器,在32中更是存在大量的寄存器映射(即之前使用的宏定义)。那么,寄存器到底是什么?寄存器映射又是什么呢?让我们带着问题开始学习。
在单片机的存储器中,设计的是片上外设,它们以四个字节为一个单元,共32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过C语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
下面我们之间去工程文件中学习,到底什么是寄存器映射。
打开一个STM32工程,打开头文件stm32f10x.h,可以看到下面也是大量的宏定义,
按下组合键Ctrl+F,进入搜索,输入GPIOE,选择Match Case 和Match Whole Word,点击Find Next,定位到GPIOE的宏定义:
#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)
发现他们是由GPIO_TypeDef 类型的指针变量重新定义为别名,从英文中可以看出GPIOG_BASE也就是GPIOG的基地址,宏定义成别名。
让我们定位到GPIOG_BASE(之后要定位到其他变量或者宏定义都可以这样做):
Go To Definition Of 'GPIOG_BASE’
可以看到这些宏定义:
#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)
依旧是宏定义重新定义为别名,从英文中可以看出GPIOx_BASE是APB2PERIPH_BASE(APB2总线基地址)+ 一个十六进制的数值。
我们之前说过,要操作GPIO,就要先进入APB2总线,因为从系统构架图我们得知,GPIO是挂载在APB2总线下的。
我们可以用树木的结构来理解,所有的GPIO(树枝)都是从APB2总线(树干)中扩展开来,每一个GPIO(树枝)在APB2总线上(树干)的位置都不同,而这个位置就是那个十六进制数,我们称之为相对于总线基地址的偏移量。
我们可以得出这样一个公式:总线基地址 + 偏移量 = 外设基地址。
上面代码的宏定义也就是用这样一个公式来定义外设别名的。同时各寄存器的偏移量都可以在中文参考手册中查找到。
用上文提到的方法定位到APB2PERIPH_BASE,可以发现,APB2PERIPH_BASE也是类似的由系统总线基地址 + 偏移量,来定义APB2的别名的。
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
至此,我们大概了解,寄存器的概念还有寄存器的映射(又或者称为寄存器的封装)。
用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOE 都各有一组功能相同的寄存器,如 GPIOA_ODR、GPIOB_ODR和GPIOC_ODR等等,它们都是功能类似却地址不同,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,官方库使用了 C 语言中的结构体语法对寄存器进行封装,让我们一起来进入工程看看。
还是打开头文件stm32f10x.h,查找ODR,发现有这样一个结构体:
typedef struct
{
__IO uint32_t CRL; //GPIO_CRL 端口配置低寄存器 地址偏移: 0x00
__IO uint32_t CRH; //GPIO_CRH 端口配置高寄存器 地址偏移: 0x04
__IO uint32_t IDR; //GPIO_IDR 数据输入寄存器 地址偏移: 0x08
__IO uint32_t ODR; //GPIO_ODR 数据输出寄存器 地址偏移: 0x0C
__IO uint32_t BSRR; //GPIO_BSRR 位设置/清除寄存器 地址偏移: 0x10
__IO uint32_t BRR; //GPIO_BRR 端口位清除寄存器 地址偏移: 0x14
__IO uint32_t LCKR; //GPIO_LCKR 端口配置锁定寄存器 地址偏移: 0x18
} GPIO_TypeDef;
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef 的结构体类型,结构体内有 7 个成员变量,变量名正好对应寄存器的名字。 C 语言的语法规定,结构体内变量的存储空间是连续的,其中 32 位的变量占用 4 个字节,16 位的变量占用 2 个字节, 也就是说,我们定义的这个 GPIO_TypeDef , 假如这个结构体的首地址为 0x40010C00( 这也是第一个成员变量 CRL 的地址) , 那么结构体中第二个成员变量 CRH 的地址即为 0x4001 0C00 +0x04 , 加上的这个 0x04 ,正是代表 CRL 所占用的 4 个字节地址的偏移量, 其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器。
说了那么多,那我们为什么到底要这么做???
OK,让我们来看看,如果不使用大量的宏定义,而是直接操作寄存器,代码会是怎样的?
有可能是这样:
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
你看得出这是GPIOB端口,全部输出高电平吗?
又或者是这样:
((((uint32_t)0x40000000) + 0x10000) + 0x1800)+ 0x10 |=((uint16_t)0x0020);
通过操作GPIOE的BSRR寄存器,来控制GPIOE_Pin_5输出高电平
而这仅仅是一两行代码而已,如果是一个工程呢?你能想象得出整个工程是多么的繁琐以及可读性多么差吗?
所以,为了在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名,用操作宏名的方式,来简化那些复杂的操作。
一起努力。共勉。: )