寄存器介绍
存储器映射
存储器本身是不具有地址的,是一块具有特定功能的内存单元,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就叫做存储区映射。给内存单元分配地址之后,就可以通过指针去操作内存地址。
存储器映射表
我们知道,GD32是一个32位的单片机,它的地址范围为2的32次方,也就是4GB的地址空间。为了降低不同客户在相同应用时的软件复杂度,存储映射是按 RISC-V 处理器提供的规则预先定义的。在存储器映射表中,一部分地址空间由 RISC-V 的系统外设所占用,且不可更改。此外,其余部分地址空间可由芯片供应商定义使用。 因为存储映射表的内容过长,大家可以去查看用户手册的第25页。 暂时无法在飞书文档外展示此内容 用户手册
什么是寄存器
寄存器是具有特定功能的内存单元,通过操作这些内存单元可以驱动外设工作。寄存器按功能又可分为指令寄存器、地址寄存器和数据寄存器,处理器可以使用相互独立的总线来读取指令和加载/存储数据。
寄存器映射
程序存储器,数据存储器,寄存器和I / O端口都在同一个线性的 4 GB的地址空间之内。每一个寄存器都对应不同的功能,操作相应的寄存器就可以配置不同的功能。如果我们要控制某个外设工作,那我们可以找到这个单元的起始地址,然后通过c语言指针的方式来访问这些内存单元。但通常我们会给这个特殊的内存单元取一个名字,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射,这个别名就是我们所说的寄存器。
寄存器重映射
给寄存器再分配一个地址的过程叫做寄存器重映射。
总线基地址
片上外设区域分为三条总线,分别为AHB总线、APB1总线和APB2总线。AHB总线最高时钟可达160MHZ,APB1总线最高时钟可达80MHZ,APB2总线时钟最高可达160MHZ。
根据外设速度的不同,不同的总线挂载着不同的外设。总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。关于总线上挂载的外设更多详细信息请查看数据手册的第11页。
关于总线基地址和地址范围:
总线名称 | 总线基地址 | 总线地址范围 |
---|---|---|
APB1 | 0x4000 0000 | 0x4000 0000-0x4000 FFFF |
APB2 | 0x4001 0000 | 0x4001 0000-0x4001 FFFF |
AHB1 | 0x4002 0000 | 0x4002 0000-0x4BFF FFFF |
AHB2 | 0x4C00 0000 | 0x4C00 0000-0x4FFF FFFF |
外设基地址
每个总线上都挂载着很多外设,这些外设也都有自己的地址范围。
关于GPIO这个外设的地址映射信息:
外设名称 | 外设基地址 | 相对于AHB1总线的地址偏移 |
---|---|---|
GPIOA | 0x4002 0000 | 0x0000 0000U |
GPIOB | 0x4002 0400 | 0x0000 0400U |
GPIOC | 0x4002 0800 | 0x0000 0800U |
外设寄存器地址
在外设的地址范围内,分布着该外设的寄存器。
以GPIO外设为例,GPIO外设地址范围内有很多个寄存器,每一个都有特定的功能,通过操作对应的寄存器来配置GPIO的功能。每个寄存器都为32位,占4个字节,这里我们以GPIOA端口的寄存器进行介绍。
关于GPIOA端口寄存器地址映信息如表1-8-1所示。其中GPIOA_BASE
表示GPIOA的基地址,为0x4002 0000
GPIOA端口各寄存器地址:
寄存器名称 | 地址偏移 | 相对于GPIOA的地址 |
---|---|---|
GPIOx_CTL(端口控制寄存器) | 0x00 | GPIOA_BASE + 0x00U |
GPIOx_OMODE(端口输出模式寄存器) | 0x04 | GPIOA_BASE + 0x04U |
GPIOx_OSPD(端口输出速度寄存器) | 0x08 | GPIOA_BASE + 0x08U |
GPIOx_PUD(端口上/下拉寄存器) | 0x0C | GPIOA_BASE + 0x0CU |
GPIOx_ISTAT(端口输入状态寄存器) | 0x10 | GPIOA_BASE + 0x10U |
GPIOx_OCTL(端口输出控制寄存器) | 0x14 | GPIOA_BASE + 0x14U |
GPIOx_BOP(端口位操作寄存器) | 0x18 | GPIOA_BASE + 0x18U |
GPIOx_LOCK(端口配置锁定寄存器) | 0x1C | GPIOA_BASE + 0x1CU |
GPIOx_AFSEL0(备用功能选择寄存器0) | 0x20 | GPIOA_BASE + 0x20U |
GPIOx_AFSEL1(备用功能选择寄存器1) | 0x24 | GPIOA_BASE + 0x24U |
GPIOx_BC(位清除寄存器) | 0x28 | GPIOA_BASE + 0x28U |
GPIOx_TG(端口位翻转寄存器) | 0x2C | GPIOA_BASE + 0x2CU |
如何操作寄存器
上面我们了解到了寄存器映射和GPIO外设寄存器的相关地址,那我们怎么去编写代码操作寄存器呢?下面我们就来简单介绍一下。
比如我们想让GPIOA端口的16个引脚都置1。我们需要去配置端口输出控制寄存器GPIOx_OCTL,通过查找用户手册148页可以知道这个寄存器的地址偏移量为0x14,GPIOA端口的基地址为0x4002 0000,所以GPIOA_OCTL寄存器的地址为
0x4002 0000 + 0x14 = 0x4002 0014
那我们就是对这个地址进行操作。那如何操作呢?
关于端口位操作寄存器说明:
通过图可以了解到,要想使能所有引脚配置为1, 只需要将OCTLy(y=0..15)对应的位置1即可。也就是配置GPIOA_OCTL寄存器的高16位为0,低16位为1,换成十六进制就是0x0000FFFF。
通过绝对地址访问内存单元
/* GPIOA 端口的16个引脚全部输出高电平 */
*(unsigned int*)(0x40020014) = 0xFFFF;
2
说明
(unsigned int*)
的作用是将0x40020014这个数字量 强制类型转化为 无符号整形的 地址,告诉编译器这是一个地址。
*(unsigned int)
是相当于控制地址里的内容,也就是对这个地址对应的内存空间的值,
*(unsigned int*)(0x40020014) = 0xFFFF;
就是相当于对0x40020014这个内存空间赋值为0xFFFF。
通过别名访问内存单元
通过上面的方式确实可以对寄存器地址进行操作,但是操作起来很麻烦,用户也不能清晰的明白这个地址对应的功能。今日看懂了,明日就不知道这一串数字代表的是什么。而如果我们给每个地址都起一个名字,这样看到名字是不是就知道这个地址对应什么功能了呢。
/* GPIOA 端口的16个引脚全部输出高电平 */
#define GPIOA_OCTL (unsigned int*)(0x40020014)
*GPIOA_OCTL = 0xFFFF;
2
3
或者为了方便我们干脆将 *
和 地址
写到一个,如下。
/* GPIOA 端口的16个引脚全部输出高电平 */
#define GPIOA_OCTL *(unsigned int*)(0x40020014)
GPIOA_OCTL = 0xFFFF;
2
3
库函数介绍
为什么要使用库函数
从上一节我们了解到如何去用寄存器驱动外设,但我们也同时了解到GD32的寄存器数量非常多,这么多的寄存器光是定义就需要花费很多的时间,更不用说还要去查找对应的功能,找到对应的地址,然后配置需要的值,这在难度和时间上都是不可取的。为此,库函数就在这种情况下应运而生,库函数能使我们的开发效率大大提高。**关于GD32的标准外设库函数,GD32的官方已经给我们开发好了,**我们只需要移植到我们的工程使用即可。库函数的使用不需要让我们去了解硬件的机制,只需要根据需要的功能去查找对应的函数,然后调用即可,大大降低了开发要求。
库函数简单介绍
从前面的环境搭建章节可以知道,创建的工程模板里面有GD32VW553的标准外设库,我们可以打开其中的gd32vw55x_gpio.h和gd32vw55x_gpio.c文件进行查看。
如何操作库函数
我们还是让GPIOA端口的16个引脚都置1,那么使用库函数该如何配置呢?首先,我们知道我们操作的功能是关于GPIO的,那么首先要从GPIO外设库中去寻找。 打开gd32vw55x_gpio.h
头文件,拉到最下面可以看到所有声明的有关GPIO的API函数。
我们是让GPIOA的16个端口都置1,也就是对端口进行操作,我们寻找对应的功能函数,从函数名可以知道gpio_port_write
这个函数是对端口进行写数据操作,我们对16个引脚全部置1不就是相当于对GPIOA这个端口写入数据0xFFFF吗。可见这个功能函数是我们需要的。
/* GPIOA 端口的16个引脚全部输出高电平 */
gpio_port_write(GPIOA,0xFFFF);
2
通过上面简单的一行代码,我们就实现了GPIOA端口的16个引脚全部输出高电平,那具体是怎么实现的呢,我们来分析一下。 首先是调用gpio_port_write(GPIOA,0xFFFF);
我们进入 gpio_port_write
函数看一下,这个函数下只写了一行代码
GPIO_OCTL(gpio_periph) = (uint32_t)data;
把我们的参数带进去,即为:
GPIO_OCTL(GPIOA) = (uint32_t)0xFFFF;
那我们再看一下GPIO_OCTL(GPIOA)
是个什么东西,通过跳转可以知道有这么一个宏定义:
#define GPIO_OCTL(gpiox) REG32((gpiox) + 0x14U)
带入GPIOA参数,即为
#define GPIO_OCTL(GPIOA) REG32((GPIOA) + 0x14U)
把GPIOA+0x14的地址带入,替换为原始内容,即为
//GPIO_OCTL(GPIOA) = (uint32_t)0xFFFF;
REG32(0x40020014) = (uint32_t)0xFFFF
2
在gd32F4xx.h头文件中有这么一个宏定义
#define REG32(addr) (*(volatile uint32_t )(uint32_t)(addr))
把REG32(addr) 替换掉就是
* (*(volatile uint32_t *)(uint32_t)(0x40020014)) = (uint32_t)0xFFFF;
看到这个是不是很熟悉了,这不就是我们上一节用寄存器去配置GPIOA端口16个引脚全部置1的例子吗。 说白了,其实库函数就是在寄存器的基础上又封装了一层,使操作起来更简单,最后还是通过寄存器来实现的。
寄存器和库函数的区别
从上面的章节中,我们了解到使用寄存器和库函数最终都是对地址进行操作,那它们之间有什么区别呢?什么时候用寄存器什么时候用库函数呢?
寄存器更能理解原理,更直观,库函数相对来说屏蔽底层,直接面向应用。
使用库函数较寄存器代码量会增大,库函数会把所有情况都考虑到函数里,有时会造成代码的冗余。
库函数使用起来相对简单,容易上手,可快速开发应用,大大提高效率。
寄存器占用内存少,速度快,在资源有限或者要求执行速度的情况下寄存器是一个不错的选择。