我们看到的 STM32 芯片是已经封装好的成品,主要由内核和片上外设组成。若与电脑类比,内核与外设就如同电脑上的 CPU与主板、内存、显卡、硬盘的关系。
STM32F103采用的是 Cortex-M3内核,内核即 CPU,由 ARM公司设计。ARM公司并不生产芯片,而是出售其芯片技术授权。芯片生产厂商(SOC)如 ST、TI、Freescale,负责在内核之外设计部件并生产整个芯片,这些内核之外的部件被称为核外外设或片上外设。如 GPIO、USART(串口)、I2C、SPI等都叫做片上外设。(采用野火官方的介绍)
芯片(这里指内核,或者叫 CPU)和外设之间通过各种总线连接,其中驱动单元有 4个,被动单元也有 4 个。为了方便理解,我们都可以把驱动单元理解成是CPU 部分,被动单元都理解成外设。下面我们简单介绍下驱动单元和被动单元的各个部件。
被控单元的FLASH,RAM,FSMC和AHB到APB的桥(即片上外设),这些功能部件共同排列在一个 4GB 的地址空间内。我们在编程的时候,可以通过他们的地址找到他们,然后来操作他们(通过 C 语言对它们进行数据的读和写)。
存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就称为存储器映射。如果给存储器再分配一个地址就叫存储器重映射。
存储器区域功能划分
在这 4GB 的地址空间中,ARM 已经粗线条的平均分成了 8 个块,每块 512MB,每个块也都规定了用途,每个块的大小都有 512MB,显然这是非常大的,芯片厂商在每个块的范围内设计各具特色的外设时并不一定都用得完,都是只用了其中的一部分而已。
在这 8个Block里面,有 3个块非常重要,也是我们最关心的三个块。Block0用来设计成内部 FLASH,Block1 用来设计成内部 RAM,Block2 用来设计成片上的外设,下面我们简单的介绍下这三个 Block 里面的具体区域的功能划分。
我们知道,存储器本身没有地址,给存储器分配地址的过程叫存储器映射。
在存储器 Block2这块区域,设计的是片上外设,它们以四个字节为一个单元,共 32bit,每一个单元对应不同的功能,当我们控制这些单元时就可以驱动外设工作。我们可以找到每个单元的起始地址,然后通过 C 语言指针的操作方式来访问这些单元,如果每次都是通过这种地址的方式来访问,不仅不好记忆还容易出错,这时我们可以根据每个单元功能的不同,以功能为名给这个内存单元取一个别名,这个别名就是我们经常说的寄存器,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射。
比如,我们找到 GPIOB 端口的输出数据寄存器 ODR 的地址是 0x4001 0C0C(至于这个地址如何找到可以先跳过,后面我们会有详细的讲解),ODR 寄存器是 32bit,低 16bit有效,对应着 16 个外部 IO,写 0/1 对应的的 IO 则输出低/高电平。现在我们通过 C 语言指针的操作方式,让 GPIOB 的 16 个 IO都输出高电平。
// GPIOB 端口全部输出 高电平
*(unsigned int*)(0x4001 0C0C) = 0xFFFF;
0x4001 0C0C在我们看来是 GPIOB端口ODR的地址,但是在编译器看来,这只是一个普通的变量,是一个立即数,要想让编译器也认为是指针,我们得进行强制类型转换,把它转换成指针,即(unsigned int *)0x4001 0C0C,然后再对这个指针进行 * 操作。
刚刚我们说了,通过绝对地址访问内存单元不好记忆且容易出错,我们可以通过寄存器的方式来操作。
// GPIOB 端口全部输出 高电平
#define GPIOB_ODR (unsigned int*)(GPIOB_BASE+0x0C)
* GPIOB_ODR = 0xFF;
为了方便操作,我们干脆把指针操作“*”也定义到寄存器别名里面
// GPIOB 端口全部输出 高电平
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
GPIOB_ODR = 0xFF;
片上外设区分为三条总线,根据外设速度的不同,不同总线挂载着不同的外设,APB1挂载低速外设,APB2 和 AHB 挂载高速外设。相应总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。其中 APB1 总线的地址最低,片上外设从这里开始,也叫外设基地址。
总线名称 | 总线基地址 | 相对外设基地址的偏移 |
---|---|---|
APB1 | 0x4000 0000 | 0x0 |
APB2 | 0x4001 0000 | 0x0001 0000 |
AHB | 0x4001 8000 | 0x0001 8000 |
以上所有的关于存储器映射的内容,最终都是为大家更好地理解如何用 C 语言控制读写外设寄存器做准备,此处是本章的重点内容。
在编程上为了方便理解和记忆,我们把总线基地址和外设基地址都以相应的宏定义起来,总线或者外设都以他们的名字作为宏名。
/* 外设基地址 */
#define PERIPH_BASE ((unsigned int)0x40000000)
/* 总线基地址 */
#define APB1PERIPH_BASE PERIPH_BASE
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define AHBPERIPH_BASE (PERIPH_BASE + 0x20000)
/* GPIO 外设基地址 */
#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)
/* 寄存器基地址,以 GPIOB 为例 */
#define GPIOA_CRL *(unsigned int*)(GPIOA_BASE+0x00)
#define GPIOA_CRH *(unsigned int*)(GPIOA_BASE+0x04)
#define GPIOA_IDR *(unsigned int*)(GPIOA_BASE+0x08)
#define GPIOA_ODR *(unsigned int*)(GPIOA_BASE+0x0C)
#define GPIOA_BSRR *(unsigned int*)(GPIOA_BASE+0x10)
#define GPIOA_BRR *(unsigned int*)(GPIOA_BASE+0x14)
#define GPIOA_LCKR *(unsigned int*)(GPIOA_BASE+0x18)
#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)
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00)
#define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04)
#define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08)
#define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C)
#define GPIOC_BSRR *(unsigned int*)(GPIOC_BASE+0x10)
#define GPIOC_BRR *(unsigned int*)(GPIOC_BASE+0x14)
#define GPIOC_LCKR *(unsigned int*)(GPIOC_BASE+0x18)
#define GPIOD_CRL *(unsigned int*)(GPIOD_BASE+0x00)
#define GPIOD_CRH *(unsigned int*)(GPIOD_BASE+0x04)
#define GPIOD_IDR *(unsigned int*)(GPIOD_BASE+0x08)
#define GPIOD_ODR *(unsigned int*)(GPIOD_BASE+0x0C)
#define GPIOD_BSRR *(unsigned int*)(GPIOD_BASE+0x10)
#define GPIOD_BRR *(unsigned int*)(GPIOD_BASE+0x14)
#define GPIOD_LCKR *(unsigned int*)(GPIOD_BASE+0x18)
#define GPIOE_CRL *(unsigned int*)(GPIOE_BASE+0x00)
#define GPIOE_CRH *(unsigned int*)(GPIOE_BASE+0x04)
#define GPIOE_IDR *(unsigned int*)(GPIOE_BASE+0x08)
#define GPIOE_ODR *(unsigned int*)(GPIOE_BASE+0x0C)
#define GPIOE_BSRR *(unsigned int*)(GPIOE_BASE+0x10)
#define GPIOE_BRR *(unsigned int*)(GPIOE_BASE+0x14)
#define GPIOE_LCKR *(unsigned int*)(GPIOE_BASE+0x18)
首先定义了 “片上外设”基地址 PERIPH_BASE,接着在 PERIPH_BASE上加入各个总线的地址偏移,得到 APB1、APB2 总线的地址 APB1PERIPH_BASE、APB2PERIPH_BASE,在其之上加入外设地址的偏移,得到 GPIOA-G 的外设地址,最后在外设地址上加入各寄存器的地址偏移,得到特定寄存器的地址。一旦有了具体地址,就可
以用指针读写。
/* 控制 GPIOB 引脚 0 输出低电平(BSRR 寄存器的 BR0 置 1) */
*(unsigned int *)GPIOB_BSRR = (0x01<<(16+0));
/* 控制 GPIOB 引脚 0 输出高电平(BSRR 寄存器的 BS0 置 1) */
*(unsigned int *)GPIOB_BSRR = 0x01<<0;
unsigned int temp;
/* 读取 GPIOB 端口所有引脚的电平(读 IDR 寄存器) */
temp = *(unsigned int *)GPIOB_IDR;
该代码使用 (unsigned int *) 把 GPIOB_BSRR宏的数值强制转换成了地址,然后再用“ * ”号做取指针操作,对该地址的赋值,从而实现了写寄存器的功能。同样,读寄存器也是用取指针操作,把寄存器中的数据取到变量里,从而获取 STM32外设的状态。
用上面的方法去定义地址,还是稍显繁琐,例如 GPIOA-GPIOE 都各有一组功能相同的寄存器,如 GPIOA_ODR/GPIOB_ODR/GPIOC_ODR 等等,它们只是地址不一样,但却要为每个寄存器都定义它的地址。为了更方便地访问寄存器,我们引入 C 语言中的结构体语法对寄存器进行封装。
typedef unsigned int uint32_t; /*无符号 32 位变量*/
typedef unsigned short int uint16_t; /*无符号 16 位变量*/
/* GPIO 寄存器列表 */
typedef struct {
uint32_t CRL; /*GPIO 端口配置低寄存器 地址偏移: 0x00 */
uint32_t CRH; /*GPIO 端口配置高寄存器 地址偏移: 0x04 */
uint32_t IDR; /*GPIO 数据输入寄存器 地址偏移: 0x08 */
uint32_t ODR; /*GPIO 数据输出寄存器 地址偏移: 0x0C */
uint32_t BSRR; /*GPIO 位设置/清除寄存器 地址偏移: 0x10 */
uint32_t BRR; /*GPIO 端口位清除寄存器 地址偏移: 0x14 */
uint16_t LCKR; /*GPIO 端口配置锁定寄存器 地址偏移: 0x18 */
} GPIO_TypeDef;
这段代码用 typedef 关键字声明了名为 GPIO_TypeDef的结构体类型,结构体内有 7个成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,结构体内变量的存储空间
是连续的,其中 32 位的变量占用 4个字节,16位的变量占用 2 个字节。
也就是说,我们定义的这个 GPIO_TypeDef ,假如这个结构体的首地址为 0x4001 0C00(这也是第一个成员变量 CRL 的地址), 那么结构体中第二个成员变量 CRH 的地址即为
0x4001 0C00 +0x04 ,加上的这个 0x04 ,正是代表 CRL所占用的 4个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,在上述代码右侧注释已给。
这样的地址偏移与 STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构体的形式访问寄存器
GPIO_TypeDef * GPIOx; //定义一个 GPIO_TypeDef 型结构体指针 GPIOx
GPIOx = GPIOB_BASE; //把指针地址设置为宏 GPIOH_BASE 地址
GPIOx->IDR = 0xFFFF;
GPIOx->ODR = 0xFFFF;
int32_t temp;
temp = GPIOx->IDR; //读取 GPIOB_IDR 寄存器的值到变量 temp 中
这段代码先用 GPIO_TypeDef 类型定义一个结构体指针GPIOx,并让指针指向地址GPIOB_BASE(0x4001 0C00),使用地址确定下来,然后根据 C 语言访问结构体的语法,用
GPIOx->ODR及 GPIOx->IDR 等方式读写寄存器。
最后,我们更进一步,直接使用宏定义好 GPIO_TypeDef 类型的指针,而且指针指向各个 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)
使用定义好的宏直接访问*/
/*访问 GPIOB 端口的寄存器*/
GPIOB->BSRR = 0xFFFF; //通过指针访问并修改 GPIOB_BSRR 寄存器
1GPIOB->CRL = 0xFFFF; //修改 GPIOB_CRL 寄存器
GPIOB->ODR =0xFFFF; //修改 GPIOB_ODR 寄存器
int32_t temp;
temp = GPIOB->IDR; //读取 GPIOB_IDR 寄存器的值到变量 temp 中
*访问 GPIOA 端口的寄存器*/
GPIOA->BSRR = 0xFFFF;
GPIOA->CRL = 0xFFFF;
GPIOA->ODR =0xFFFF;
uint32_t temp;
temp = GPIOA->IDR; //读取 GPIOA_IDR 寄存器的值到变量 temp 中
这里我们仅是以 GPIO 这个外设为例,给大家讲解了 C 语言对寄存器的封装。以此类推,其他外设也同样可以用这种方法来封装。好消息是,这部分工作都由固件库帮我们完成了,这里我们只是分析了下这个封装的过程,让大家知其然,也只其所以然。
int main()
{
*(unsigned int*)0x40021018 |=(1<<4);//打开GPIO时钟
*(unsigned int*)0x40011004 &=~(0x0F<<(4*5));//配置输出模式
*(unsigned int*)0x40011004 |=(1<<(4*5));
*(unsigned int*)0x4001100C &=~(1<<13);//配置输出低电平
}
想深入学习可以订阅我的项目实战专栏