三、寄存器和库函数介绍
1. 寄存器介绍
1.1 存储器映射
存储器本身是不具有地址的,是一块具有特定功能的内存单元,它的地址是由芯片厂商或用户分配,给存储器分配地址的过程就叫做存储区映射。给内存单元分配地址之后,就可以通过指针去操作内存地址。
1.2 存储器映射表
我们知道,HC32是一个32位的单片机,它的地址范围为2的32次方,也就是4GB的地址空间。为了降低不同客户在相同应用时的软件复杂度,存储映射是按Cortex-M4处理器提供的规则预先定义的。在存储器映射表中,一部分地址空间由Arm Cortex-M4的系统外设所占用,且不可更改。其余部分地址空间可由芯片供应商定义使用。
关于存储映射表的内容,大家可以去查看用户手册的第62页。
1.3 什么是寄存器
寄存器是具有特定功能的内存单元,通过操作这些内存单元可以驱动外设工作。寄存器按功能又可分为指令寄存器、地址寄存器和数据寄存器,处理器可以使用相互独立的总线来读取指令和加载/存储数据。
1.4 寄存器映射
程序存储器,数据存储器,寄存器和I / O端口都在同一个线性的4 GB的地址空间之内。每一个寄存器都对应不同的功能,操作相应的寄存器就可以配置不同的功能。如果我们要控制某个外设工作,那我们可以找到这个单元的起始地址,然后通过c语言指针的方式来访问这些内存单元。但通常我们会给这个特殊的内存单元取一个名字,这个给已经分配好地址的有特定功能的内存单元取别名的过程就叫寄存器映射,这个别名就是我们所说的寄存器。
1.5 寄存器重映射
给寄存器再分配一个地址的过程叫做寄存器重映射。
1.6 总线基地址
片上外设区域分为10条总线,分别为:
- APB1 外设总线(EMB/Timers/SPI/USART/I2S/HRPWM/EFM) 速度最大120MHz
- APB2 外设总线(Timers/SPI/USART/I2S) 速度最大120MHz
- APB3 外设总线(ADC/DAC/TRNG) 速度最大120MHz
- APB4 外设总线(FCM/WDT/SWDT/CMP/EMU/CTC/OTS/RTC/VBAT/WKTM/I2C) 速度最大60MHz
- APB5 外设总线(Timers/HRPWM) 速度最大240MHz
- AHB1 外设总线(DMPU/KEYSCAN/INTC/DCU/GPIO/DMA/CMU/DVP/MAU/FMAC) 速度最大240MHz
- AHB2 外设总线(CAN/SDIOC/USBFS) 速度最大120MHz
- AHB3 外设总线(SDIOC/ETHMAC) 速度最大120MHz
- AHB4 外设总线(AES/HASH/CRC/CAN/USBHS) 速度最大120MHz
- AHB5 外设总线(SMC/DMC/SMCR/DMCR/NFC/QSPI) 速度最大240MHz
根据外设速度的不同,不同的总线挂载着不同的外设。总线的最低地址我们称为该总线的基地址,总线基地址也是挂载在该总线上的首个外设的地址。关于总线上挂载的外设更多详细信息请查看数据手册的第19页。 、
关于总线基地址和地址范围如表1-6-1所示。
1.7 外设寄存器地址
在外设的地址范围内,分布着该外设的寄存器。
以GPIO外设为例,GPIO外设地址范围内有很多个寄存器,每一个都有特定的功能,通过操作对应的寄存器来配置GPIO的功能。每个寄存器都为32位,占4个字节,这里我们以GPIOA端口的寄存器进行介绍。
1.8 如何操作寄存器
上面我们了解到了寄存器映射和GPIO外设寄存器的相关地址,那我们怎么去编写代码操作寄存器呢?下面我们就来简单介绍一下。
比如我们想让GPIOA端口的16个引脚都置1。我们需要去配置端口输出寄存器PODRx ,通过查找用户手册258页可以知道这个寄存器的地址偏移量为0x04,PODRx的基地址为0x40053800 + 0x04,所以PODRA 寄存器的地址为0x40053800 + 0x04 + 0x10*0 = 0x40053804,那我们就是对这个地址进行操作。那如何操作呢?
关于端口位操作寄存器说明如图1-9-1所示。
通过图1-9-1可以了解到要想使能所有引脚配置为1,只需要将PODRA 对应的位置1即可。也就是配置PODRA 寄存器的高16位为0,低16位为1,换成十六进制就是0x0000FFFF。
- 通过绝对地址访问内存单元
/* GPIOA 端口的16个引脚全部输出高电平 */
*(unsigned int*)(0x40053804) = 0xFFFF;
2
3
说明:
(unsigned int*)的作用是将0x40053804这个立即数强制类型转化为无符号整形地址,告诉编译器这是一个地址。
(unsigned int)是相当于地址,也就是对这个地址对应的内存空间的值,(unsigned int*)(0x40053804) = 0xFFFF;就是相当于对0x40053804这个内存空间赋值为0xFFFF。
之后我们还需要对 POERx 寄存器进行配置,这个是输出许可寄存器。当 I/O 端口被设置成 GPO 功能时,且此寄存器设为 1 时, PODRx 设定值将输出到对应 I/O 端口。
此寄存器设为 0 时,输出关闭,端口为高阻态。不存在端口对应位请不要写 1 。
所以我们需要将对应的GPIO置1。
*(unsigned int*)(0x40053806) = 0xFFFF;
- 通过别名访问内存单元
通过上面的方式确实可以对寄存器地址进行操作,但是操作起来很麻烦,用户也不能清晰的明白这个地址对应的功能。如果我们给每个地址都起一个名字,这样看到名字是不是就知道这个地址对应什么功能了呢。
/* GPIOA 端口的16个引脚全部输出高电平 */
#define GPIOA_ODR (unsigned int*)(0x40053804)
#define GPIOA_OER (unsigned int*)(0x40053806)
*GPIOA_ODR = 0xFFFF;
*GPIOA_OER = 0xFFFF;
2
3
4
5
6
7
2. 库函数介绍
2.1 为什么要使用库函数
从上一节我们了解到如何去用寄存器驱动外设,但我们也同时了解到HC32的寄存器数量非常多,这么多的寄存器光是定义就需要花费很多的时间,更不用说还要去查找对应的功能,找到对应的地址,然后配置需要的值,这在难度和时间上都是不可取的。为此,库函数就在这种情况下应运而生,库函数能使我们的开发效率大大提高。关于HC32的标准外设库函数,HC32的官方已经给我们开发好了,我们只需要移植到我们的工程使用即可。库函数的使用不需要让我们去了解硬件的机制,只需要根据需要的功能去查找对应的函数,然后调用即可,大大降低了开发要求。
2.2 库函数简单介绍
从前面的章节可以知道创建的工程模板里面有HC32F4A0的标准外设库,我们可以打开其中的hc32_ll_gpio.h和hc32_ll_gpio.c文件进行查看,也可以参考HC32F4A0的固件库使用手册进行参考。
说白了,其实库函数就是在寄存器的基础上又封装了一层,使操作起来更简单,最后还是通过寄存器来实现的。
3. 寄存器和库函数的区别
从上面的章节中,我们了解到使用寄存器和库函数最终都是对地址进行操作,那它们之间有什么区别呢?什么时候用寄存器什么时候用库函数呢?
- 寄存器更能理解原理,更直观,库函数相对来说屏蔽底层,直接面向应用。
- 使用库函数较寄存器代码量会增大,库函数会把所有情况都考虑到函数里,有时会造成代码的冗余。
- 库函数使用起来相对简单,容易上手,可快速开发应用,大大提高效率。
- 寄存器占用内存少,速度快,在资源有限或者要求执行速度的情况下寄存器是一个不错的选择。