文章目录
- 一、寄存器
- 1、存储器映射
- 2、存储器映射表
- 3、寄存器
- 4、寄存器映射
- 5、寄存器重映射
- 6、总线基地址、外设基地址、外设寄存器地址
- 7、操作寄存器(以操作一个GPIO口为例)
- 1. 寄存器地址定义部分
- 2. `GPIOD_Configuration` 函数部分
- 3. `main` 函数部分
- 二、库函数
- 1、为什么要使用库函数
- 2、什么是库函数
- 3、库函数的详解(以GPIO为例)
- 4、如何操作库函数(以点亮LED连接到PD7为例)
- 三、寄存器和库函数的区别
- 1、寄存器与库函数的区别
- 2、寄存器操作的优势和劣势
- 3、库函数的优势和劣势
- 四、GD32F4xx 系列器件的存储器映射表
一、寄存器
- GD32F450ZG 单片机的寄存器是用于控制和配置芯片内部各种硬件资源的存储单元。它们就像是一组开关和控制器,通过对这些寄存器进行读写操作,可以实现对单片机的功能配置、数据传输、中断处理等多种操作。
1、存储器映射
- 概念:存储器映射是将芯片中的各种存储器(如闪存、SRAM等)和外设(如定时器、GPIO等)统一编址,把它们看成一个连续的地址空间。就好像一个大仓库,不同的区域存放着不同的东西,这个大仓库的每个存储单元都有一个唯一的地址。这样CPU可以通过这个统一的地址来访问不同的资源。
- 举例:在GD32F450ZG单片机中,整个存储区域可能从0x00000000开始,一直到某个较大的地址,其中不同的段分配给了不同的用途。例如,一部分地址范围是闪存(程序存储区),用来存放程序代码;另一部分是SRAM(数据存储区),用于存放程序运行时的数据。
2、存储器映射表
- 概念:存储器映射表是一张详细的“地图”,它记录了存储区域中每个地址段对应的具体资源。它告诉用户从哪个地址开始到哪个地址结束是闪存、从哪个地址开始是SRAM、哪个地址范围是某个外设(如UART、SPI等)的寄存器等信息。
- 用途:通过查看存储器映射表,开发者可以准确地找到想要访问的资源的地址范围。例如,在编写程序时,如果要访问某个外设的寄存器,就需要根据这个表来确定正确的地址。详细可见文章末尾存储器映射表。
3、寄存器
- 概念:寄存器是一种特殊的存储单元,它位于芯片内部,用于控制和监视芯片的各种功能。可以把寄存器想象成一个个小的“控制盒”,每个“控制盒”负责一个特定的功能。例如,对于GPIO(通用输入输出端口),有控制引脚方向(输入或输出)的寄存器、控制引脚输出电平(高或低)的寄存器等。
- 功能分类:
- 控制寄存器:用于控制外设的工作模式。比如,定时器的控制寄存器可以设置定时器的计数模式(向上计数、向下计数等)、是否使能定时器等。
- 状态寄存器:用于反映外设的当前状态。以UART为例,状态寄存器可以指示是否有数据接收完成、是否有数据发送缓冲区为空等状态。
- 数据寄存器:用于存储要发送或接收的数据。在SPI通信中,数据寄存器用于存放要发送出去的数据,或者读取接收到的数据。
4、寄存器映射
- 概念:寄存器映射是将寄存器的物理地址映射到一个便于编程访问的虚拟地址空间。这样,在编程时就可以通过简单的地址操作来访问寄存器,而不需要关心寄存器的实际物理位置。
- 举例:GD32F450ZG单片机可能将一些外设寄存器映射到特定的地址范围,比如将GPIOA的寄存器映射到从地址A开始的一段连续地址,这样在程序中通过访问这个地址范围就相当于访问GPIOA的寄存器。
5、寄存器重映射
- 概念:寄存器重映射是改变寄存器的默认映射地址,将其映射到另外一个地址空间。这样做的目的通常是为了方便布线、解决资源冲突或者满足特定的应用需求。
- 举例:在某些情况下,默认的GPIO引脚对应的寄存器地址可能在布线时不太方便访问,通过寄存器重映射,可以将这些寄存器映射到一个更容易访问的地址,从而方便对GPIO进行操作。
6、总线基地址、外设基地址、外设寄存器地址
- 总线基地址:它是整个存储系统中某条总线(如APB总线、AHB总线)的起始地址。就像一条街道的起点门牌号,所有连接在这条总线上的外设的地址都是基于这个总线基地址来确定的。
- 外设基地址:是某个外设在总线上的起始地址。例如,GPIOA这个外设在总线上有一个基地址,从这个地址开始的一段连续地址空间就是GPIOA的寄存器地址范围。
- 外设寄存器地址:是外设中每个具体寄存器的地址。以GPIOA为例,有控制引脚方向的寄存器地址、控制引脚输出电平的寄存器地址等,这些都是基于GPIOA的外设基地址来确定的。
7、操作寄存器(以操作一个GPIO口为例)
以下是基于GD32F450ZG芯片,通过操作寄存器的方式来控制GPIOD的第7引脚输出高电平以点亮LED的示例代码(假设LED阳极接该引脚,阴极接地这种常见连接方式),以下代码使用C语言编写,且是示意性代码,你可能需要根据实际的编译环境等做适当调整:
// 首先定义寄存器相关的地址,这些地址可以从芯片手册中获取对应的值
#define RCC_AHB1ENR (*(volatile unsigned int *)0x40023830) // 时钟使能寄存器地址
#define GPIOD_MODER (*(volatile unsigned int *)0x40020C00) // GPIOD 模式寄存器地址
#define GPIOD_OTYPER (*(volatile unsigned int *)0x40020C04) // GPIOD 输出类型寄存器地址
#define GPIOD_OSPEEDR (*(volatile unsigned int *)0x40020C08) // GPIOD 输出速度寄存器地址
#define GPIOD_PUPDR (*(volatile unsigned int *)0x40020C0C) // GPIOD 上拉下拉寄存器地址
#define GPIOD_ODR (*(volatile unsigned int *)0x40020C14) // GPIOD 输出数据寄存器地址// 函数用于初始化GPIOD的相关配置
void GPIOD_Configuration(void)
{// 使能GPIOD时钟,对应RCC_AHB1ENR寄存器中相关位设置为1RCC_AHB1ENR |= (1 << 3); // 配置GPIOD的第7引脚为通用输出模式(01),参考芯片手册对MODER寄存器的描述GPIOD_MODER &= ~(3 << 14); // 先清除原来的设置GPIOD_MODER |= (1 << 14); // 设置为输出模式// 设置输出类型为推挽输出(0),对应OTYPER寄存器设置,默认就是推挽输出,这里做下示意性设置GPIOD_OTYPER &= ~(1 << 7); // 设置输出速度,这里设置为高速(11),可根据实际需求调整,参考OSPEEDR寄存器说明GPIOD_OSPEEDR &= ~(3 << 14);GPIOD_OSPEEDR |= (3 << 14);// 设置上拉下拉电阻,这里设置为无上下拉(00),参考PUPDR寄存器GPIOD_PUPDR &= ~(3 << 14);
}int main()
{// 进行GPIOD引脚的初始化配置GPIOD_Configuration();// 将GPIOD的第7引脚输出数据寄存器对应位置1,输出高电平GPIOD_ODR |= (1 << 7); while(1){// 可以在这里添加其他需要循环执行的代码,如果不需要可以为空循环}
}
下面详细解释一下代码各部分的含义:
1. 寄存器地址定义部分
通过 #define
宏定义了一系列寄存器的地址,使用 volatile
关键字修饰是为了告诉编译器该变量(实际代表的寄存器内容)可能会被硬件等外部因素改变,编译器不要对其访问做优化,要每次都从实际的内存地址(寄存器所在地址)读取其值。
例如 RCC_AHB1ENR (*(volatile unsigned int *)0x40023830)
就定义了 RCC_AHB1ENR
这个寄存器对应的内存地址是 0x40023830
,后续可以通过对这个定义好的变量进行读写操作来操作对应寄存器。
2. GPIOD_Configuration
函数部分
-
时钟使能:
首先在函数内通过RCC_AHB1ENR |= (1 << 3);
操作RCC_AHB1ENR
寄存器来使能GPIOD的时钟。在芯片中,各个外设模块(比如这里的GPIOD端口)都需要时钟才能正常工作,将该寄存器对应位(第3位对应GPIOD的时钟使能位,具体参考芯片手册的寄存器说明)置1就是开启其时钟。
-
引脚模式配置:
- 对于
GPIOD_MODER
寄存器,它用来设置每个引脚的工作模式(比如输入、输出、复用功能等)。先通过GPIOD_MODER &= ~(3 << 14);
清除原来对应引脚(第7引脚,对应寄存器的位是从低位开始算,第7引脚对应的位在第14、15两位控制,因为每两位控制一个引脚的模式,所以左移14位)的模式设置,然后GPIOD_MODER |= (1 << 14);
将其设置为通用输出模式(01表示输出模式,参考芯片手册的模式编码定义)。
- 对于
-
输出类型配置:
GPIOD_OTYPER
寄存器用于设置引脚是推挽输出还是开漏输出,这里GPIOD_OTYPER &= ~(1 << 7);
表示将第7引脚设置为推挽输出(默认一般就是推挽输出,这里做明确设置),也就是可以输出高低电平且具有一定的驱动能力。
-
输出速度配置:
通过操作GPIOD_OSPEEDR
寄存器来设置引脚的输出速度,这里GPIOD_OSPEEDR &= ~(3 << 14);
先清除原来的速度设置,然后GPIOD_OSPEEDR |= (3 << 14);
将其设置为高速输出(速度等级的编码同样参考芯片手册定义,这里设置为11表示高速),根据实际电路和对速度的需求可以选择不同的速度等级。
-
上拉下拉电阻配置:
最后使用GPIOD_PUPDR
寄存器设置引脚的上拉下拉情况,GPIOD_PUPDR &= ~(3 << 14);
操作是将第7引脚设置为无上下拉状态(00表示无上下拉,不同编码对应不同的上拉下拉设置,参考芯片手册)。
3. main
函数部分
-
首先调用
GPIOD_Configuration
函数对GPIOD的第7引脚进行初始化配置,使其具备合适的输出条件。 -
然后通过
GPIOD_ODR |= (1 << 7);
操作GPIOD_ODR
寄存器,将第7引脚对应的位(第7位)置1,这样就使得该引脚输出高电平,从而如果连接的LED电路正确(阳极接该引脚,阴极接地),就可以点亮LED。 -
最后的
while(1)
循环可以根据实际需求添加其他要不断执行的代码逻辑,如果暂时不需要额外逻辑,空循环即可保证程序持续运行并维持引脚的输出电平状态。
请注意,以上代码只是基于芯片手册对寄存器操作的常规实现示例,实际应用中可能需要考虑更多如系统初始化等完整的环境搭建内容,并且要严格对照芯片手册确保寄存器地址、位操作等都符合芯片的具体规定。
二、库函数
1、为什么要使用库函数
- 提高开发效率:直接操作寄存器来控制芯片的功能是非常繁琐的。例如,配置一个通用输入输出(GPIO)引脚,需要对多个寄存器进行位操作,包括配置模式寄存器、输出数据寄存器等。使用库函数可以避免这些复杂的底层寄存器操作,减少代码编写量和出错的概率。
- 增强代码的可移植性:不同的芯片可能有不同的寄存器地址和操作方式。如果使用库函数,只要库函数的接口不变,在不同的同系列芯片或者经过适当修改的其他芯片之间移植代码会更加方便。
- 便于代码维护:库函数将复杂的硬件操作封装起来,使代码结构更加清晰。当需要修改功能时,例如改变GPIO引脚的功能或者更新芯片的某些配置,只需要调用相应的库函数或者修改库函数的参数,而不需要在大量的寄存器操作代码中寻找和修改。
2、什么是库函数
- 库函数是芯片厂商或者第三方为了方便开发者使用芯片的功能而编写的一组函数。这些函数封装了对芯片内部寄存器的操作,为开发者提供了一个更高级的编程接口。以GD32F450ZG为例,它的库函数可以实现诸如GPIO配置、定时器设置、中断处理等各种功能。库函数通常是基于芯片的硬件手册编写的,它准确地反映了芯片的功能和特性。
3、库函数的详解(以GPIO为例)
- 函数分类:
- 初始化函数:用于配置GPIO引脚的模式(输入、输出、复用等)、速度等参数。例如,
gpio_mode_set()
函数用于设置GPIO引脚的模式,它会根据传入的参数配置相应的寄存器。 - 读写函数:用于读取GPIO引脚的输入状态或者写入输出状态。像
gpio_output_bit_set()
和gpio_output_bit_reset()
函数可以分别设置和清除GPIO引脚的输出电平。
- 初始化函数:用于配置GPIO引脚的模式(输入、输出、复用等)、速度等参数。例如,
- 参数含义:
- 以
gpio_mode_set()
函数为例,它通常需要传入GPIO端口(如GPIOD
)、引脚号(如GPIO_PIN_7
)、模式(如GPIO_MODE_OUTPUT
)等参数。这些参数明确了要操作的具体引脚和要设置的功能。
- 以
4、如何操作库函数(以点亮LED连接到PD7为例)
- 步骤一:包含头文件
- 首先需要包含相关的头文件,这些头文件包含了库函数的声明。对于GD32F450ZG的GPIO操作,通常需要包含
gd32f4xx_gpio.h
头文件。在C语言中,使用#include "gd32f4xx_gpio.h"
语句将头文件包含到源文件中。
- 首先需要包含相关的头文件,这些头文件包含了库函数的声明。对于GD32F450ZG的GPIO操作,通常需要包含
- 步骤二:初始化GPIO时钟
- 芯片的GPIO模块需要时钟才能正常工作。可以使用
rcu_periph_clock_enable()
函数来使能GPIOD的时钟。例如:
- 芯片的GPIO模块需要时钟才能正常工作。可以使用
rcu_periph_clock_enable(RCU_GPIOD);
- 步骤三:配置GPIO引脚模式
- 使用
gpio_mode_set()
函数将PD7配置为输出模式。示例代码如下:
- 使用
gpio_mode_set(GPIOD, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_7);
- 这里`GPIOD`表示要操作的是D端口,`GPIO_MODE_OUTPUT`表示设置为输出模式,- `GPIO_PUPD_NONE`表示不使用上下拉电阻,`GPIO_PIN_7`表示操作的是第7个引脚。
- 步骤四:点亮LED(设置高电平)
- 使用
gpio_output_bit_set()
函数将PD7引脚设置为高电平来点亮LED。代码如下:
- 使用
gpio_output_bit_set(GPIOD, GPIO_PIN_7);
- 完整示例代码如下:
#include "gd32f4xx.h"
#include "gd32f4xx_gpio.h"
int main()
{rcu_periph_clock_enable(RCU_GPIOD);gpio_mode_set(GPIOD, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_7);gpio_output_bit_set(GPIOD, GPIO_PIN_7);while (1);return 0;
}
- 在上述代码中,
while(1)
语句用于让程序保持运行状态,这样可以持续保持LED点亮。如果没有这个循环,程序执行完后可能会导致LED熄灭或者出现其他不可预期的行为。
请注意,以上代码可能需要根据具体的开发环境和工程配置进行适当的调整,如添加启动文件、正确配置链接脚本等。
三、寄存器和库函数的区别
1、寄存器与库函数的区别
- 操作层次不同
- 寄存器:是芯片内部的存储单元,直接与硬件电路相关联。例如,在GD32F450ZG芯片中,GPIO(通用输入输出)寄存器用于控制引脚的输入输出模式、电平状态等。每个寄存器都有特定的地址和位定义,开发者需要直接对这些寄存器进行读写操作来控制硬件。比如,通过向GPIO端口的控制寄存器写入特定的值来设置引脚为输入或输出模式。
- 库函数:是对寄存器操作的封装。它提供了一种更高级的编程接口,隐藏了寄存器操作的细节。例如,库函数内部会根据传入的参数(如设置引脚为输出模式)自动对相应的寄存器进行正确的读写操作,开发者只需要调用库函数并传入合适的参数,而不需要了解具体是哪个寄存器以及如何对其进行位操作。
- 代码形式不同
- 寄存器操作:代码通常涉及直接访问寄存器地址。在C语言中,可能会使用指针来访问寄存器。例如,
*(volatile uint32_t *)0x40021000 = 0x00000001;
(这只是一个简单示例,实际地址和值根据芯片手册确定),这里将值0x00000001
写入到地址为0x40021000
的寄存器中,这种代码比较底层,不易理解。 - 库函数调用:代码看起来更加简洁明了。以设置GPIO引脚模式为例,可能会有类似
gpio_mode_set(GPIOD, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_7);
这样的函数调用。其中gpio_mode_set
是库函数,后面的参数用于指定端口、模式、上下拉电阻状态和引脚号等信息。
- 寄存器操作:代码通常涉及直接访问寄存器地址。在C语言中,可能会使用指针来访问寄存器。例如,
2、寄存器操作的优势和劣势
- 优势
- 高度灵活性:可以实现对芯片硬件的最精细控制。开发者能够根据自己的需求精确地设置每个寄存器位,以实现特殊的功能或者对硬件进行优化。例如,在某些低功耗应用中,可以通过对特定寄存器的位操作来精确控制芯片各个模块的功耗状态。
- 资源占用少:由于直接操作寄存器,没有额外的代码封装层,所以生成的代码体积可能相对较小。这在资源受限的嵌入式系统中,如一些小型的传感器节点或者简单的微控制器应用中,是一个重要的优势。
- 执行效率高:没有函数调用的开销,直接对寄存器进行读写,在一些对实时性要求极高的应用场景中,能够更快地响应硬件事件。例如,在高速数据采集系统中,直接操作寄存器可以更快地获取和处理数据。
- 劣势
- 开发难度大:需要开发者深入了解芯片的寄存器手册,包括寄存器的地址、位定义、复位值等信息。对于复杂的芯片,寄存器数量众多,操作起来容易出错。例如,在配置一个复杂的通信接口(如SPI或I2C)时,需要正确设置多个相关寄存器的多个位,这对开发者的硬件知识和耐心是一个很大的考验。
- 代码可读性差:直接操作寄存器的代码通常是一些十六进制或二进制的常量赋值,以及指针操作,这些代码对于其他开发者或者后期维护代码的人员来说,理解起来非常困难。而且,代码缺乏高层次的抽象,使得业务逻辑被底层硬件操作细节所掩盖。
- 可移植性差:不同芯片的寄存器布局和功能定义不同。如果要将基于某个芯片寄存器操作的代码移植到另一个芯片上,几乎需要重新编写大部分代码。例如,从GD32F450ZG移植到另一个型号的微控制器,由于寄存器地址和功能可能完全不同,原有的寄存器操作代码无法直接使用。
3、库函数的优势和劣势
- 优势
- 易于开发:降低了开发难度,开发者不需要深入了解芯片的每个寄存器细节。只需要熟悉库函数的接口和功能,就可以快速实现对芯片硬件的控制。例如,使用库函数来配置定时器,只需要按照函数的参数要求传入定时时间、计数模式等信息,就可以完成定时器的配置,而不需要了解定时器相关寄存器的具体操作。
- 代码可读性好:库函数的调用使得代码更具有逻辑性和可读性。代码能够清晰地表达其功能,例如
gpio_output_bit_set(GPIOD, GPIO_PIN_7);
很容易理解是设置GPIOD端口的第7引脚为输出高电平。这种高层次的抽象使得代码的维护和协作开发更加方便。 - 可移植性强:在同一系列芯片或者具有相似库函数接口的芯片之间,代码移植相对容易。只要库函数的接口和功能没有大的变化,就可以在不同芯片上使用相同的代码逻辑。例如,在GD32系列的不同型号芯片中,如果库函数接口保持一致,就可以方便地移植GPIO控制代码。
- 劣势
- 灵活性相对较低:库函数提供了一种标准化的操作方式,可能无法满足一些特殊的硬件控制需求。例如,在某些非常特殊的硬件调试或者优化场景下,需要对寄存器进行一些非标准的位操作,而库函数可能没有提供这样的功能接口。
- 资源占用可能较多:由于库函数是对寄存器操作的封装,内部可能会包含一些额外的代码来处理参数检查、错误处理等功能。这可能导致生成的代码体积比直接操作寄存器的代码要大,在资源受限的系统中可能会受到限制。
- 执行效率可能稍低:库函数调用会带来一定的函数调用开销,包括参数传递、栈操作等。在对性能要求极高的场景下,可能会影响系统的实时响应速度。不过,在大多数嵌入式应用中,这种效率损失通常是可以接受的。