嵌入式C语言开发---存储器与寄存器

时间:2024-03-01 11:02:08

概述:

讲述如何使用C语言来对底层寄存器进行封装

内容:

  1. 存储器映射
  2. 寄存器与寄存器映射
  3. C语言访问寄存器
  1. 存储器映射

程序存储器、数据存储器、寄存器和I/O 端口排列在同一个顺序的4 GB 地
址空间内

存储器映射:

存储器本身不具有地址信息,它的地址是由芯片厂商或用户分配,给存储器
分配地址的过程称为存储器映射,如果再分配一个地址就叫重映射。

存储器区域划分

ARM 将这4GB 的存储器空间,平均分成了8 块区
域,每块区域的大小是512MB,这个容量是非常大的,因此芯片厂商就在每块容
量范围内设计各自特色的外设,要注意一点每块区域容量占用越大,芯片成本就
越高,所以说我们使用的STM32 芯片都是只用了其中一部分。

 

 

在这8 个Block 里面,Block0、Block1 和Block2 这3 个块是我们最为关
心的。因为它包含了STM32 芯片的内部Flash、RAM 和片上外设。

Block0 内部又划分了好多个功能块,我们按地址从低到高顺序依次
介绍。
0x0000 0000-0x0007 FFFF:取决于BOOT 引脚,为FLASH、系统存储器、
SRAM 的别名。
0x0008 0000-0x07FF FFFF:预留。
0x0800 0000-0x0807 FFFF:片内FLASH,我们编写的程序就放在这一区域
(512KB)。

0x0808 0000-0x1FFF EFFF:预留。
0x1FFF F000-0x1FFF F7FF:系统存储器,里面存放的是ST 出厂时烧写好的
isp 自举程序,用户无法改动。使用串口下载的时候需要用到这部分程序。
0x1FFF F800-0x1FFF F80F:选项字节,用于配置读写保护、
BOR 级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。当芯片不
小心被锁住之后,我们可以从RAM 里面启动来修改这部分相应的寄存器位。
0x1FFF F810-0x1FFF FFFF:预留。
(2)Block1 内部区域功能划分
Block1 用于设计片内的SRAM,我们使用的STM32F103ZET6 的SRAM 是64KB。
从图5.1.1 中可以看到Block1 内部又划分了几个功能块,我们按地址从低到高
顺序依次介绍。
0x2000 0000-0x2000 FFFF:SRAM,容量为64KB。
0x2001 0000-0x3FFF FFFF:预留。
(3)Block2 内部区域功能划分
Block2 用于设计片内外设,根据外设总线速度的不同,Block2 被划分为AHB
和APB 两部分,APB 又被分成APB1 和APB2 总线。这些都可以在图5.1.1 中看到,
我们按地址从低到高顺序依次介绍。
0x4000 0000-0x4000 77FF:APB1 总线外设。
0x4000 7800-0x4000 FFFF:预留。
0x4001 0000-0x4001 3FFF:APB2 总线外设。
0x4001 4000-0x4001 7FFF:预留。
0x4001 8000-0x4002 33FF:AHB 总线外设。
0x4002 4400-0x5FFF FFFF:预留。
在Block3/4/5 中还包含了FSMC 扩展区域,这3 个块可用于扩展外部存储器,
比如SRAM,NORFLASH 和NANDFLASH 等。

寄存器和寄存器映射

lock2 这片区域是用来设计片上外设的,
由于Cortex-M3 内核是32 位的,所以存储器内部是以四个字节为一个单元,每
一个单元对应不同的功能,当我们控制这些单元时也就可以控制外设。每一个单
元还对应一个地址,我们要操作这些单元,也就是通过对应的地址来访问。由于
STM32 外设非常多而且复杂,如果每操作一个外设就要写一大串对应的存储单元
地址,显然是非常麻烦的而且还极容易出错。因此我们就把每个单元的功能作为
名,给这个内存取一个别名,这个别名就是我们经常说的寄存器。

然后通过C
语言指针来操作这些寄存器即可。那什么是寄存器映射呢?给已经分配好地址的
有特定功能的内存单元取别名的过程就叫寄存器映射。

比方说我们找到0x4001 1010 这个单元地址,那么可以通过查阅相关资料了
解到此单元具有GPIOC 端口置位/复位功能(至于此地址如何查找这个功能我们
后面会具体介绍)。因此为了更好区分此单元的功能和方便后续的程序开发,可
以给这个单元取一个别名GPIOC_BSRR,那么这个GPIOC_BSRR 就是寄存器,并且
这个寄存器地址就是0x4001 1010。这个过程就是寄存器映射。

如何访问STM32 寄存器内容:

我们知道寄存器就是一些有特定功能的内存单元,所以要访问STM32 寄存器
也就是操作STM32 的内存单元,根据C 语言指针的特点,可以使用指针来操作
STM32 的内存单元。假如我们要让STM32 的GPIOC 的第0 管脚输出低电平,我们
怎么使用C 语言来处理?

片上外设区分为四条总线,根据外设速度的不同,不同总线挂载着不同的外
设, APB1 挂载低速外设,APB2 和AHB 挂载高速外设。相应总线的最低地址我
们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。
APB1 总线的地址最低,因此片上外设就从这这个地址开始,也称外设基地址。

外设基地址
每条总线上都会挂接着很多的外设,这些外设也会有自己的地址范围,
XXX 外设的首个地址即最低地址就是XXX 外设的基地址,也称作XXX 边界地
址。有关STM32F1xx 外设的具体边界地址可以参考《STM32F1xx 中文参考手
册》P28 页,里面有详细的介绍。这里我们就以GPIO 外设来讲解外设基地址。
其他的外设也是同样分析

 

 

外设GPIOx 都是挂接在APB2 总线上,属于高速的外
设,而APB2 总线的基地址是0x4001 0000,故GPIOA 的相对APB2 总线的地址偏
移是800。
(3)外设寄存器地址
XXX 外设的寄存器就分布在其对应的外设地址范围内。这里我们以GPIO 外
设为例,GPIO 是通用输入输出端口的简称,可以通过软件来控制其输入和输出。
GPIO 有很多个寄存器,每一个都有特定的功能。每个寄存器为32bit,占四个
字节,这些寄存器都是按顺序依次排列在外设的基地址上。寄存器的位置都以相
普中STM32F1xx 开发攻略
www.prechin.cn
39
对该外设基地址的偏移地址来描述。这里我们以GPIOC 端口为例,来说明GPIO
都有哪些寄存器,

这里我们就以GPIOC_BSRR 寄存器来教大家如何看《STM32F1xx 中文参考手
册》内寄存器的说明。大家如果想要了解更多的寄存器内容,可以参考《STM32F1xx
中文参考手册》相应寄存器外设部分。
首先我们需要打开STM32 中文参考手册,然后找到GPIO 外设章节,里面会
有一个GPIO 寄存器,只要找到我们所要查找的寄存器即可

A.红色框4 表示的我们所查找寄存器的名称,寄存器GPIOx_BSRR 内的x 表
示的是STM32GPIO 端口,范围是A-E,也就是说在GPIOA、GPIOB 等端口中都有
这个寄存器。
B.红色框5 表示的是相对GPIOx 地址的偏移值,比如现在我们使用的是
GPIOC 外设,其基地址是0x4001 1000,那么本寄存器GPIOx_BSRR 地址=0x4001
1000+0x10=0x4001 1010。对于其他的GPIO 外设也是一个原理。

C.红色框6 和7 表示的是寄存器的位表。其中6 表示寄存器编号,因为一个
寄存器是32bit,所以范围是0-31。7 表示的是相应位的权限,w:只写,r:只
读,rw:可读可写。本寄存器位权限是w,所以只能写,如果试图读本寄存器,
是无法保证读取到它真正内容的。而有的寄存器位权限为只读,一般是用于表示
STM32 外设的某种工作状态的,由STM32 硬件自动更改,通过读取那些寄存器位
来判断外设的工作状态。
D.红色框8 是寄存器位功能说明。这个也是寄存器说明中最重要的部分,它
详细介绍了寄存器每一个位的功能。例如本寄存器中有两种寄存器位,分别为
BRy 及BSy,其中的y 数值表示的是管脚号,可以是0-15。如BR0、BS0 用于
控制GPIOx 的第0 个引脚,若x 表示GPIOC,那就是控制GPIOC 的第0 引脚,
而BR1、BS1 就是控制GPIOC 第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 是相反的操作。

使用C 语言封装寄存器:

实例1:控制GPIOC 端口的第0 管脚输出一个低电平。首先我们需要知道
GPIOC 端口外设是挂接在哪个总线上的,然后根据总线基地址和本身的偏移地址
得到GPIOC 外设基地址,最后通过这个外设基地址得到里面各种寄存器基地址。
(1)总线和外设基地址封装
普中STM32F1xx 开发攻略
www.prechin.cn
41
根据寄存器的概念,我们可以使用C 语言中的宏定义对寄存器进行定义。具
体代码如下:
//定义外设基地址
#define PERIPH_BASE ((unsigned int)0x40000000) 1)
//定义APB2 总线基地址
#define APB2PERIPH_BASE (PERIPH_BASE + 0x00010000) 2)
//定义GPIOC 外设基地址
#define GPIOC_BASE (AHB1PERIPH_BASE + 0x0800) 3)
//定义寄存器基地址这里以GPIOC 为例
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00) 4)
#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)
上述代码中我们在后面备注了数字,下面对其进行简单介绍下其功能:
1)定义外设的基地址,这个地址也是Block2 的基地址。
2)定义APB2 总线基地址,因为Block2 的第一个总线是APB1,而APB2 总
线地址只需要加上对应的地址偏移量即可。
3)定义GPIO 外设基地址,因为GPIOC 是挂接在APB2 总线上的,所以找到
对应的端口地址偏移量即可知道GPIOC 端口基地址。
4)定义GPIO 外设寄存器基地址,这里以GPIOC 端口为例,因为GPIOC_CRL
是GPIOC 外设的第一个寄存器,所以基地址就是GPIOC 地址,其他寄存器地址只
需要在GPIOC 基地址上加上相应的偏移量即可。

我们得到了寄存器具体的地址,那么就可以使用C 语言指针来操作读写。例
如我们需要GPIOC0 输出一个低电平或者高电平,可以使用下面语句来操作。
//控制GPIOC 第0 管脚输出一个低电平


GPIOC_BSRR = (0x01<<(16+0));
//控制GPIOC 第0 管脚输出一个高电平
GPIOC_BSRR = (0x01<<0);

 

我们知道GPIOC_BSRR 的值是这个寄存器的地址,但是编译器不知道它是地
址,而是把它当做立即数,所以我们必须要强制转换为(unsigned int *)指针
类型才可以对其操作,这一点特别要注意。然后再在前面加上一个“*”作取指
针操作,表示对该地址内内容进行写,读操作也同样使用“*”取指针操作。如
下:
unsigned int temp;
temp =GPIOC_IDR;
将寄存器内的数据保存在变量temp 中,使用到变量时一定要进行定义。

寄存器封装
通过前面讲解,我们已经可以对寄存器进行操作,但是还稍有不足,因为
STM32 的GPIO 比较多,我们不可能每使用一个GPIO 都做前面一样的一大堆定义。
根据GPIO 寄存器的特点,我们知道不论GPIOA 还是GPIOB 等都拥有一组功能相
同的寄存器,如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 */
普中STM32F1xx 开发攻略
www.prechin.cn
43
uint16_t LCKR; /*GPIO 端口配置锁定寄存器地址偏移: 0x18 */
}GPIO_TypeDef;
这段代码用typedef 关键字声明了名为GPIO_TypeDef 的结构体类型,结
构体内有7 个成员变量,变量名正好对应寄存器的名字。C 语言的语法规定,
结构体内变量的存储空间是连续的,其中32 位的变量占用4 个字节,16 位的变
量占用2 个字节。
也就是说,我们定义的这个GPIO_TypeDef ,假如这个结构体的首地址为
0x4001 1000(这也是第一个成员变量CRL 的地址),那么结构体中第二个成员
变量CRH 的地址即为0x4001 1000 +0x04 ,加上的这个0x04 ,正是代表CRH
所占用的4 个字节地址的偏移量,其它成员变量相对于结构体首地址的偏移,
在上述代码右侧注释已给出。
这样的地址偏移与STM32 GPIO 外设定义的寄存器地址偏移一一对应,只要
给结构体设置好首地址,就能把结构体内成员的地址确定下来,然后就能以结构
体的形式访问寄存器了,比如我们还是将GPIOC0 输出低电平,具体代码如下:
GPIO_TypeDef * GPIOx; //定义一个GPIO_TypeDef 型结构体指针GPIOx
GPIOx = GPIOC_BASE; //把指针地址设置为宏GPIOC_BASE 地址
GPIOx->BSRR =(1<<(16+0)); //通过指针访问并修改GPIOC_BSRR 寄存器
这段代码先用GPIO_TypeDef 类型定义一个结构体指针GPIOx,并让指针指
向GPIOC 基地址GPIOC_BASE,地址确定下来,然后根据C 语言访问结构体的内
容,用GPIOx->BSRR 写寄存器。为了操作更简便灵活,我们直接使用宏定义好
GPIO_TypeDef 类型的指针,而且指针指向各个GPIO 端口的首地址,使用时我
们直接用该宏访问寄存器即可。具体代码如下:
#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)
GPIOC->BSRR = (1<<(16+0));

我们这里仅仅以GPIO 这个外设为例,给大家讲解了如何使用C 语言对寄存
器封装,对于其他的外设也是使用同样方法。其实到了后面的实验程序的编写,
我们都是使用ST 公司提供的固件库,他们把STM32 所有外设都已经封装好了,
我们只需要调用即可。我们这里分析这个封装过程只是想让大家更加清楚理解如
何使用C 来封装寄存器的。