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

概述:

讲述如何使用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 来封装寄存器的。

原文地址:https://www.cnblogs.com/IotI/p/8427164.html

时间: 2024-08-29 19:52:27

嵌入式C语言开发---存储器与寄存器的相关文章

嵌入式C语言常用关键字

1.static关键字 这个关键字前面也有提到,它的作用是强大的. 要对static关键字深入了解,首先需要掌握标准C程序的组成. 标准C程序一直由下列部分组成: 1)正文段--CPU执行的机器指令部分,也就是你的程序.一个程序只有一个副本:只读,这是为了防止程序由于意外事故而修改自身指令: 2)初始化数据段(数据段)--在程序中所有赋了初值的全局变量,存放在这里. 3)非初始化数据段(bss段)--在程序中没有初始化的全局变量:内核将此段初始化为0. 注意:只有全局变量被分配到数据段中. 4)

嵌入式Linux裸机开发(五)——SDRAM初始化

嵌入式Linux裸机开发(五)--SDRAM初始化 一.SDRAM初始化流程 S5PV210有两个独立的DRAM控制器,一个最大支持512MB,一个最大支持1024MB,但两个控制器必须支持相同类型的内存. 根据三星S5PV210文档可知,DDR2类型内存的初始化流程如下: 1.提供稳压电源给内存控制器和内存芯片,内存控制器必须保持CLE在低电平,此时就会提供稳压电源.注:当CKE引脚为低电平时,XDDR2SEL应该处于高电平 2.根据时钟频率正确配置PhyControl0.ctrl_start

嵌入式Linux裸机开发(十一)——Nandflash

嵌入式Linux裸机开发(十一)--Nandflash 一.Nand Flash简介 NandFlash是Flash的一种,具有容量较大,改写速度快等优点,适用于大量数据的存储.NandFlash没有专门的地址线,发送指令.地址和数据都通过8/16位宽的总线(I/O接口)到内部的寄存器. NandFlash分为SLC和MLC两类.SLC全称为Single-Level Cell,单层单元闪存,MLC全称为Multi-Level Cell,多层单元闪存.SLC每一个单元储存一位数据,而MLC通过使用

嵌入式Linux裸机开发(四)——重定位relocate

嵌入式Linux裸机开发(四)--重定位relocate 一.位置有关编码 汇编源文件被编译成二进制可执行程序时编码方式可能与内存地址有关,也可能与内存地址无关.与内存地址有关的为位置有关编码,与内存地址无关的为位置无关编码. 程序在设计时需要规划一个程序运行时的地址(链接地址),编译连接器在链接时必须指定这个链接地址,得到的二进制程序的程序理论规划的运行时地址和编译连接器指定的链接地址才相同,程序才能正常运行.位置无关编码程序则无需设计程序时规划运行时地址,编译链接器链接时同样无需指定链接地址

嵌入式C语言全套视频教程云盘下载!

了解过嵌入式开发的朋友们都有一定的了解,语言是学习嵌入式开发必须具备的工具语言,学好C语言基础可以更好的学习嵌入式开发.今天在这里给大家分享一个嵌入式C语言全套视频教程,需要的朋友可以下载来看看! 课程目录部分截图: 百度云盘下载:http://pan.baidu.com/s/1c1OETIo 密码:cgcj

嵌入式Linux裸机开发(七)——UART串口通信

嵌入式Linux裸机开发(七)--UART串口通信 一.UART串口通信简介 通用异步收发器简称UART,即UNIVERSAL ASYNCHRONOUS RECEIVER AND TRANSMITTER, 它用来传输串行数据.发送数据时, CPU 将并行数据写入UART,UAR按照一定的格式在一根电线上串 行发出:接收数据时, UART检测另一根电线的信号,将串行收集在缓冲区中, CPU 即可读取 UART 获得这些数据. 在 S5PV210中, UART提供了 4 对独立的异步串口I/O端口,

如何使用eclipse进行嵌入式Linux的开发

如何使用eclipse进行嵌入式Linux的开发 作者:曾宏安,华清远见嵌入式学院高级讲师. 如何使用eclipse进行嵌入式Linux的开发 习惯了在windows环境下开发的程序员在转到Linux平台时经常会抱怨没有一个好用的集成开发环境.和windows下常用的一些开发软件相比,Linux自带的一些开发环境使用起来要么界面和操作不够方便.友好,要么功能不全.其实我们完全可以在开源社区上找到操作方便,功能强大的开发环境.下面就向大家介绍一个能在Linux上运行,界面和功能与windows上同

嵌入式linux QT开发(一)——QT简介

嵌入式linux QT开发(一)--QT简介 一.QT简介 1.QT简介 QT是一个跨平台的C++图形用户界面库,由挪威TrollTech公司出品,目前包括Qt Creator, QtEmbedded,Qt Designer快速开发工具,Qt Linguist国际化工具等部分,Qt支持所有Linux/Unix系统,还支持Windows平台. 2.QT优点 Qt是一个跨平台的C++图形用户界面应用程序框架,提供给应用程序开发者建立艺术级的图形用户界面所需的所用功能.Qt很容易扩展,并且允许真正地组

嵌入式Linux裸机开发(一)——点亮Led

嵌入式Linux裸机开发(一)--点亮Led 开发板:友善之臂smart210 一.电路图查阅 1.底板电路图 查阅开发板底板电路图,查阅LED相关部分 LED电路工作原理: LED的正极接3.3V,负极接地时导通,LED发光. 开发板共有四颗LED,正极接3.3V,负极接开发板的LED1_LED4引脚,如果LED要点亮则需要输入低电平. 2.核心板电路图 查阅核心板电路图可知,LED1-LED4接在SoC的GPJ2_0-GPJ2_3,通过控制GPJ2_0-GPJ2_3的寄存器使对应GPIO引脚