兆易创新 UINIO-MCU-GD32F350 固件库开发指南
早在新冠疫情爆发前的 2019 年,就曾经撰写过一篇关于 ARM 标准库的技术长文 《意法半导体 STM32F103 标准库典型实例》 ,文章非常详尽的介绍了各种常见片上外设资源的应用。时至 4 年以后的今天,国产微控制器在工程实践领域已经得到了广泛运用,因而基于兆易创新 推出的国产 ARM 微控制器,设计和制作了 UINIO-MCU-GD32F350RBT6 这款开源核心板,同时撰写了本篇文章作为配套的资料教程,希冀为国产芯片的商业化普及尽自己一份绵薄之力。
UINIO-MCU-GD32F350RBT6
是一款采用 LQFP64 封装的 GD32F350RBT6
微控制器核心板,基于 ARM Cortex-M4 内核架构,主频高达
108MHz
,拥有 128K
容量 Flash,以及
16K
的 SRAM。而 UINIO-MCU-GD32F103C
采用 LQFP48 封装的 GD32F103Cxxx 系列微控制器(包括
GD32F103CBT6
、GD32F103C8T6
、GD32F103C6T6
、GD32F103C4T6
),基于
ARM Cortex-M3 内核架构,主频达到
108MHz
,拥有 16K ~ 128K
容量 Flash,以及
6K ~ 20K
的 SRAM。
准备 GD32 支持包 & 固件库
这里以 UINIO-MCU-GD32F350RBT6 核心板作为例子,首先需要前往兆易创新的 GD32 MCU 微控制器 官方网站,把如下两个开发资源下载到本地计算机:
- GD32F350RBT6 固件库
GD32F3x0_Firmware_Library_V2.2.1
。 - Keil uVision5 开发环境的支持包
GigaDevice.GD32F3x0_DFP.3.0.2.pack
。
然后,启动 Keil uVision5
开发环境,开始导入或者在线安装
GigaDevice.GD32F3x0_DFP.3.0.2.pack
支持包:
接下来,解压 GD32F3x0_Firmware_Library_V2.2.1
固件库,此时会得到如下一系列目录:
- Docs:包含有官方评估板的原理图和固件库的使用指南。
- Examples:各种 GD32F350RBT6 片上外设的官方示例源程序。
- Firmware:包含有内核库
CMSIS
、标准外设库GD32F3x0_standard_peripheral
、USB 文件系统库GD32F3x0_usbfs_library
三个子目录。 - Template:集成开发环境 IAR 和 Keil uVision4 的工程模板,包含有 LED 闪烁、USART 打印、按键控制的简单示例程序。
- Utilities:一些第三方组件和 GD32 配套的评估板测试文件。
其中 Examples 下面的每一个子目录,都对应着一种片上外设的示例程序,里面通常会包含有如下的源文件:
main.c
:主程序源文件。systick.h
:SysTick 精准延时头文件;systick.c
:SysTick 精准延时源文件;GD32f3x0.it.h
:中断处理程序头文件;GD32f3x0_it.c
:中断处理程序源文件(未使用中断,所有函数体为空);GD32f3x0_libopt.h
:通过预处理语句#include
包含指定的外设库.h
头文件(默认导入全部外设);
而 Firmware 目录下面包含有 GD32F350RBT6 固件库的核心源文件:
CMSIS
子目录包含有 ARM Cortex-M4 内核的支持文件、启动代码、库引导文件,以及GD32F3x0
的全局头文件和系统配置文件。GD32F3x0_standard_peripheral
子目录下的Include
包含了固件库所需要的头文件,而Source
则包含有固件库所需的源文件。
测试 UINIO-MCU-GD32 核心板
打开 GD32F3x0_Firmware_Library_V2.2.1
固件库下面的
Template
目录,删掉除开 Keil_project
目录之外的其它文件与目录,然后将 Examples\GPIO\Running_led
内的全部源文件,拷贝至 Template
目录当中,从而获得如下的文件目录结构:
1 | Template |
鼠标双击 Template
目录下面的工程描述文件
Project.uvproj
,启动 Keil uVision5。由于
GD32F3x0_Firmware_Library_V2.2.1
当中的示例工程采用的是
Keil uVision4
建立和编译,因而此时会弹出下面的错误信息:
按下【确定】按钮忽略这些错误信息,依次选择顶部菜单栏上面的【Project -> Manage -> Migrate to Version 5 Format...】,把工程迁移成为 Keil uVision5 兼容的格式:
此时会提示工程描述文件需要从 Keil uVision4 的
Project.uvproj
保存为 Keil uVision5 的
Project.uvprojx
,直接按下【确定】按钮即可:
在开始接下来的操作之前,需要先将 Keil uVision5 工程的编译目标切换为 UINIO-MCU-GD32F350RBT6 核心板所使用的型号【GD32F350】:
点击顶部工具栏上的【Options for
Target...】按钮,指定目标选项对话框里的【ARM
Compiler】版本为 compiler version 5
:
注意:这里必须修改 Keil uVision5 当中 ARM 编译器版本,否则会导致后续的编译操作出现错误,具体请参考 《ARM 调试工具 UINIO-DAP-Link 应用详解》 一文的 添加 ARM Compiler version 5 小节内容。
切换至对话框的【Output】选项卡,在勾选【Create HEX
File】的同时,把【Name of Executable】修改为
Project.hex
(务必添加 .hex
后缀,否则默认烧录的是 .axf
文件):
再切换至对话框当中的【Debug】选项卡,此时需要将 UINIO-DAP-Link 插入至计算机的 USB 接口,然后在下拉选择【CMSIS-DAP Debugger】之后,再按下右侧的【Settings】按钮:
此时会弹出调试器设置对话框,这里我们选择【UINIO-CMSIS-DAP】,并且将【Max
Clock】配置为 10MHz
:
最后再切换至【Flash Download】选项卡,勾选【Reset and Run】,并且点击【Add】按钮添加片上 Flash 的编程算法:
完成上述配置步骤之后点击【OK】,回到 Keil uVision5
的主界面,此时按下快捷键【F7】或者顶部工具栏上的【Build】按钮编译示例工程,再按下快捷键【F8】或者【Download】按钮将编译后得到的
.hex
程序下载至 UINIO-MCU-GD32F350RBT6
核心板运行,此时整个工程的目录文件结构如下所示,其中的 Out
目录保存着编译后产生的十六进制 .hex
文件:
1 | Template |
该示例程序会每间隔 4 秒的时间,循环切换
UINIO-MCU-GD32F350RBT6 的四个 GPIO 引脚
C2
、C10
、C11
、C12
的高低电平状态,此时通过万用表就可以测量出运行结果,从而方便的判断出程序是否下载成功,以及核心板运行是否存在有故障。
注意:DAPLink 是 ARM 系列微控制器开发过程当中,程序下载与调试不可少的工具,相关资料和设计资源可以参考笔者之前撰写的《ARM 调试工具 UINIO-DAP-Link 应用详解》 一文。
搭建 Keil uVision5 自定义工程
新建目录与拷贝源文件
本节内容开始尝试自己动手搭建 Keil uVision5
工程,首先新建一个名称为 Keil-GD32F350RBT6
的 Keil
uVision5
工程,并将其保存至同名的目录下面,然后再新建如下一系列子目录,并且将固件库里的源文件拷贝至对应的子目录:
- Applications:保存应用层相关的源文件。
- Documents:用于存放 Markdown
说明文档,可以预先放置一个
README.md
文件。 - Drivers:存放针对 UINIO-MCU-GD32F350RBT6 定制的板级驱动程序。
- Firmware:用于放置
GD32F3x0_Firmware_Library_V2.2.1
当中Firmware
目录下的全部内容(即CMSIS
、GD32F3x0_standard_peripheral
、GD32F3x0_usbfs_library
三个子目录)。 - Sources:用于保存
GD32F3x0_Firmware_Library_V2.2.1
下面的Template
目录当中,除IAR_project
、Keil_project
、readme.txt
之外的文件(即main.c/h
、systick.c/h
、gd32f3x0_it.c/h
、gd32f3x0_libopt.h
七个源文件)。
创建分组与添加源文件
鼠标点击 Keil uVision5 顶部菜单栏上面的【File Extensions, Books and Environment...】按钮:
在弹出的工程管理项对话框当中,分别将左侧的【Project
Targets】命名为
Keil-GD32F350RBT6
,而中间的【Groups】则分别建立如下几个分组,并且通过右侧的
Files
向指定分组添加相应的源文件:
- CMSIS 分组:分别添加
Keil-GD32F350RBT6\Firmware\CMSIS\GD\GD32F3x0\Source
目录下的system_gd32f3x0.c
外设接入层源文件,以及Keil-GD32F350RBT6\Firmware\CMSIS\GD\GD32F3x0\Source\ARM
目录下的startup_gd32f3x0.s
启动文件(添加对话框的文件类型要修改为.s
)。 - Drivers 分组:暂时不需要添加任何源文件。
- Firmware 分组:按需添加
Keil-GD32F350RBT6\Firmware\GD32F3x0_standard_peripheral\Source
目录下的.c
源文件(其中的gd32f3x0_rcu.c
和gd32f3x0_gpio.c
属于必须添加)。 - Documents 分组:将
Keil-GD32F350RBT6\Documents
目录下新建的README.md
文件添加进去。 - Applications 分组:添加
Keil-GD32F350RBT6\Sources
目录下的main.c
、systick.c
、gd32f3x0_it.c
三个源文件。 - Sources 分组:暂时不需要添加任何源文件。
完成上述操作之后,在工程管理项对话框当中,各个分组下面的源文件情况如下图所示:
点击【OK】按钮关闭工程管理项对话框,此时 Keil uVision5 左侧呈现的工程目录结构如下面所示:
移除 Source 目录下的冗余代码
为了避免工程搭建过程当中,直接拷贝官方固件库 Template
目录下的源文件,出现冗余代码导致编译错误的情况,接下来还需要对
Source
目录进行一些清理工作。首先需要删除掉该目录下
main.c
源文件里多余的内容,只需要保留如下所示的代码:
1 |
|
除此之外,还需要再移除掉 Source
目录下
gd32f3x0_it.c
源文件里面,如下所示的无效代码片段:
1 | /*! |
配置编译器路径与选项
点击 Keil uVision5 工具栏顶部的【Options for
target】,在弹出的目标选项对话框当中,首先切换至【C/C++】选项卡,将【Define】输入框设置为
USE_STDPERIPH_DRIVER,GD32F3X0,GD32F350
:
然后再点击对话框当中【Include Paths】输入框右侧的按钮,配置 ARM 编译器分别包含 Keil-GD32F350RBT6 工程目录下的如下路径:
.\Sources
.\Firmware\CMSIS
.\Firmware\CMSIS\GD\GD32F3x0\Include
.\Firmware\GD32F3x0_standard_peripheral\Include
接下来切换至【Target】选项卡,选择 ARM 编译器的版本为 5,并勾选界面上 Keil uVision5 自带的用于串口重定向的【Use MicroLIB】工具库:
最后切换到【Output】选项卡,将【Name of Executable】输入框设置为
Keil-GD32F350RBT6.hex
,并且勾选 Create HEX
File 使得编译结果为十六进制 .hex
格式:
测试工程的编译下载
完成上述配置工作之后,关闭 Keil uVision5
界面上的全部对话框,然后按下快捷键【F7】或者顶部工具栏上的【Build】按钮,将新建工程里的相关源文件编译为一个
Keil-GD32F350RBT6.hex
文件:
1 | Build started: Project: Keil-GD32F350RBT6 |
如果编译结果显示 0 Error(s), 0 Warning(s)
,说明这个
Keil uVision5
工程已经搭建成功。接下来,就可以按下快捷键【F8】或者顶部工具栏上的【Download】按钮,把编译后得到的十六进制文件
Keil-GD32F350RBT6.hex
,通过 UINIO-DAP-Link
下载至 UINIO-MCU-GD32F350RBT6 核心板上面运行:
1 | Load "D:\\Workspace\\UINIO-MCU-GD32F350RBT6\\Keil-GD32F350RBT6\\Objects\\Keil-GD32F350RBT6.hex" |
注意:为了大家能够方便快速的搭建测试项目,该自定义工程已被保存到开源硬件项目 UINIO-MCU-GD32F350RBT6 的
Keil-GD32F350RBT6
目录里面。
让 Keil uVision5 支持中文注释
鼠标依次选择 Keil uVision5 菜单栏上的 【Edit ->
Configration... -> 】,将弹出窗口【Editor】选项卡下的
Encoding
选择为 Chinese GB2312 (Simplified)
就可以支持中文注释:
注意:这种方式会导致 Keil uVision 显示的源代码字体非常不美观,更佳的处理办法是利用 Sublime 等文本编辑器提供的 ConvertToUTF8 插件,将源代码文件全部转换为 UTF-8 格式的编码。
使用 AStyle 格式化源代码
AStyle 是一款用于对 C/C++ 源代码进行格式化的开源插件,鼠标点击 Keil uVision5 菜单栏上的【Tools -> Customize Tools Menu】:
在弹出的对话框当中进行如下的设置,其中的 Command
就是 astyle.exe
可执行文件所在的路径:
- Command:
D:\Software\Tech\AStyle\astyle.exe
- AStyle
All:
"$E*.c" "$E*.h" --style=google --indent=spaces=2
。 - AStyle
File:
!E --style=google --indent=spaces=2
。
完成上述步骤之后,就可以在 Keil uVision5 的菜单栏上发现【Tools -> AStyle All】和【Tools -> AStyle File】两条自定义菜单项:
MCU 微控制器系统结构概览
芯片资源简介
GD32F350RBT6 是一款采用 Arm Cortex-M4 内核架构的 32
位微控制器,工作频率为 108MHz
,工作电压范围在
2.6V ~ 3.6V
之间,工作温度介于 -40°C ~ +85°C
范围。提供高达 128KB
的片上 Flash 闪存和
16KB
的 SRAM
内存,其它的片上资源情况可以参考下面表格:
资源名称 | 数量 | 资源名称 | 数量 |
---|---|---|---|
12 位 ADC | 1 个 | SPI | 2 个 |
12 位 DAC | 1 个 | I2C | 2 个 |
通用比较器 CMP | 2 个 | USART | 2 个 |
通用 16 位定时器 | 5 个 | I2S | 1 个 |
通用 32 位定时器 | 1 个 | HDMI-CEC | 1 个 |
基本定时器 | 1 个 | TSI | 1 个 |
PWM 高级定时器 | 1 个 | USBFS 全速 USB | 1 个 |
UINIO-MCU-GD32F350RBT6 采用的
GD32F350RBT6 微控制器使用的是 LQFP64
封装形式,其具体 64 个引脚的功能分配可以参见下图:
ARM Cortex-M4 内核架构
ARM Cortex-M4 系列微控制器基于 ARMv7 架构,其内核主要由下面一系列的功能单元构成:
- 嵌套式向量型中断控制器(NVIC,Nested Vectored Interrupt Controller)。
- 浮点运算单元(FPU,Floating Point Unit)。
- 闪存地址重载及断点单元(FPB,Flash Patch Breakpoint)。
- 串行线调试接口(SW-DP,Serial-Wire Debug Port)。
- 数据观测点及跟踪单元(DWT,Data Watchpoint And Trace)。
- 指令跟踪宏单元(ITM,Instrumentation Trace Macrocell)。
- 跟踪端口接口单元(TPIU,Trace Port Interface Unit)。
- 内部总线矩阵(Bus Matrix,用于实现 I-Code 指令总线、D-Code 数据总线、System 系统总线、PPB 专用总线、AHB-AP 调试专用总线的相互联接)。
GD32F350RBT6 外设架构
GD32F350RBT6
微控制器的整体系统架构如下面的框图所示,其中
AHB(Advanced High performance
Bus)高级高性能总线矩阵采用的是多层总线结构,支持多个主从设备之间实现并行通信,其中主设备包含有来自
ARM Cortex-M4 内核架构的
I-Code 指令总线
、D-Code 数据总线
、System 系统总线
,以及来自于内核外部的
DMA 总线
:
- I-Code 总线:即 Instruction Code,用于从
0x 0000 0000 ~ 0x 1FFF FFFF
代码区域获取向量。 - D-Code 总线:即 Data Code,用于加载和存储数据,以及调试访问代码区域。
- System 系统总线:用于获取指令和向量、加载与存储数据、调试访问系统区域(包括内部 SRAM 和外设区域)。
- DMA 总线:用于直接内存访问(DMA,Direct Memory Access)的传输总线。
除此之外,AHB 总线矩阵的从设备包含有来自 Flash 存储控制器的 IBUS 和 DBUS 总线、SRM 控制器总线,以及 AHB1 和 AHB2 总线:
- AHB2 总线连接了 A、B、C、D、F 一共五组 GPIO 端口。
- AHB1 总线连接的是其它片上外设资源,其通过两组 AHB-APB 总线桥(AHB to APB Bridge 1/2)分别提供了 AHB1 总线与高级外设总线(APB,Advanced Peripheral Bus)之间的同步连接。
地址空间映射
ARM Cortex M4
内核采用了哈佛结构,使用相互独立的总线来读取指令和操作数据。这些指令和数据都存储在一个大小为
4GB
的相同地址空间(因为 ARM
Cortex M4 的地址总线宽度为 32
位,所以其对应的地址范围为 2
的 32
次方等于
4GB),但是处于不同的地址范围:
观察上面的表格可以发现 GD32F350RBT6 的片上外设地址空间被划分为 AHB1 和 AHB2 总线、APB1 和 APB2 总线共四个部分,这些总线的最低地址被称为总线基地址,也就是挂载在该总线上第 1 个外设的地址,而每个外设的最低地址则被称为外设基地址,每个外设的地址范围内都分布着该外设所对应的寄存器,通过操作这些寄存器就可以达到控制外设的目的。
操作寄存器 → 运用固件库
操作寄存器
如果需要将 AHB 总线上的 GPIOA
外设对应的 16 个引脚全部置为
1
,那么就需要去配置端口输出控制寄存器
GPIOx_OCTL
,通过查询用户手册可以知道其地址偏移量为
0x14
:
由于 GPIOA 的外设基地址为
0x4800 0000
,所以寄存器 GPIOA_OCTL
的地址计算方式如下所示:
1 | 0x4800 0000 + 0x0000 0014 = 0x4800 0014 |
换而言之,将寄存器 OCTL(0~15)
相应的位设置为 1
,就可以把对应的
GPIOA(0~15)
控制为高电平。如果要让全部 16
个引脚输出高电平,那么相应的 GPIOA_OCTL
寄存器的高 16
位可以置为 0
而低 16 位置为 1
,即
0000 0000 0000 0000 1111 1111 1111 1111
,转换为十六进制就是
0x0000FFFF
:
1 | *(unsigned int*)(0x48000014) = 0x0000FFFF; // 将 GPIOA 外设对应的 16 个引脚全部输出高电平 |
像上面这样直接对寄存器地址进行操作会比较麻烦,下面可以通过宏定义
#define
,为每一个寄存器地址都分配一个名称:
1 |
|
为了进一步简化代码,可以将指针类型 *
的声明合并到
GPIOA_OCTL
的宏定义当中:
1 |
|
运用库函数
兆易创新官方固件库
GD32F3x0_Firmware_Library_V2.2.1
当中标准外设库
Firmware\GD32F3x0_standard_peripheral
目录下的
Include\gd32f3x0_gpio.h
和
Source\gd32f3x0_gpio.c
两个源文件,提供有一系列用于操作
GPIO 的库函数:
其中的
void gpio_port_write(uint32_t gpio_periph, uint16_t data)
函数可以用于向特定的 GPIO 端口写入状态值:
1 | gpio_port_write(GPIOB, 0xFFFF); |
该函数被定义在 Source\gd32f3x0_gpio.c
源文件当中,可以看到其函数体内调用了 GPIO_OCTL(gpio_periph)
函数:
1 | /*! |
而这个 GPIO_OCTL(gpio_periph)
函数又被预定义在了
Include\gd32f3x0_gpio.h
头文件里面,其最终调用的是
REG32(addr)
函数:
1 |
REG32(addr)
函数的定义位于官方固件库
Firmware\CMSIS\GD\GD32F3x0\Include
目录下的
gd32f3x0.h
头文件当中:
1 |
把前面寄存器 GPIOA_OCTL
的地址计算式
0x4800 0000 + 0x0000 0014
作为 addr
参数代入之后,就会发现标准外设库底层也是在操作寄存器,只是在使用的时候更加直观简单:
1 |
注意:通过寄存器直接控制外设,性能开销更少,运行更加迅速,适用于片上资源有限,且对于实时性要求较高的场景。而使用标准外设库来操控外设,其优势主要体现在提升代码的开发效率以及可读性与可维护性。
通过 GPIO 寄存器控制 LED
使用 UINIO-MCU-GD32F350RBT6 核心板来控制 GPIO 端口的输出,整体需要经历下面几个步骤:
- 开启指定 GPIO 的端口时钟;
- 配置指定 GPIO 的工作模式;
- 配置指定 GPIO 的输出类型;
开始编写代码之前,首先需要将一枚 4.7K
的电阻
R1
与一枚 LED 发光二极管串联,然后再连接到
UINIO-MCU-GD32F350RBT6 核心板的 GPIOB8
引脚,当该引脚输出高电平的时候 LED
发光二极管就会点亮,而输出低电平的时候 LED
发光二极管就会熄灭,具体的电路连接关系请参考下面的示意图:
开启 GPIO 的端口时钟
由于 GD32F350RBT6 的外设时钟资源默认情况下都是关闭的,所以在配置外设之前需要先开启其对应的时钟。
AHB 总线使能寄存器 RCU_AHBEN
GPIOB
引脚分组被挂载到了 GD32F350RBT6
微控制器的 AHB 总线下面,在用户手册的
复位和时钟单元(RCU)
章节里,描述了 AHB
总线使能寄存器 RCU_AHBEN
的地址偏移量为
0x14
、复位值为 0x0000 0014
,可以按照 8
位的字节、16 位的半字以及 32
位的字进行访问:
而 AHB 总线使能寄存器
RCU_AHBEN
位于复位和时钟单元 RCU
外设的地址范围之内,由于 RCU 的外设基地址为
0x4002 1000
,所以 RCU_AHB1EN
寄存器的实际地址计算过程如下面等式所示:
1 | RCU_AHBEN = RCU 的外设基地址 + AHB 总线使能寄存器偏移量 = 0x4002 1000 + 0x14 = 0x4002 1014 |
根据用户手册当中接下来的内容,可以发现 RCU_AHB1EN
寄存器的第 18
位 PBEN
就是 GPIOB
时钟的使能位:
所以只需要往 RCU_AHB1EN
寄存器的第 18 位写入
1
,其它位保持不变,就可以实现对 GPIOB
外设时钟的使能,这里我们可以通过一个或运算和移位运算来完成:
1 | RCU_AHBEN |= (1 << 18) |
注意:上面等式要使能的是第几位,就向右移多少位。例如上面等式向第 18 位写入
1
,所以就右移18
位。
配置 GPIO 的工作模式
接下来,着手配置 GD32F350RBT6 的 GPIO 工作模式,这里具体可以划分为下面两个步骤:
- 将端口控制寄存器
GPIOx_CTL
配置为输入模式(默认)
/输出模式
/备用功能模式
/模拟模式
; - 将端口上下拉寄存器
GPIOx_PUD
配置为上拉模式
/下拉模式
/悬空模式(默认)
;
配置端口控制寄存器 GPIOB_CTL
已知 GPIOB 寄存器的基地址为
0x4800 0400
,而端口控制寄存器 GPIOB_CTL
的地址偏移量为 0x00
:
从而就可以计算出 GPIOB 端口控制寄存器
GPIOB_CTL
的实际地址为:
1 | GPIOB_CTL = 0x4800 0400 + 0x00 = 0x4800 0400 |
该寄存器通过两个位来进行控制,例如这里需要操作的是 Pin8
引脚,就是需要控制 GPIOB_CTL
寄存器的第 17 和 16
位。通过将这两位配置为 01
,就可以将 GPIOB8
端口配置为输出模式。此时向 GPIOB_CTL
寄存器写入的二进制数据为
0000 0000 0000 0001 0000 0000 0000 0000
,转换为十六进制就是
00010000
。为了确保其它位不会被修改,需要先将第 15 和第 14
两位置零,然后再将其配置为 0
和 1
:
1 | GPIOB_CTL &= 0xFFFCFFFF; // 把第 17 和 16 位置为 00 |
除此之外,还可以采用下面的计算方式进行配置:
1 | GPIOB_CTL &= ~(0x03 << (2 * 8)); // 把第 17 和 16 位置为 00 |
注意:上面代码当中的数值
8
对应的是GPIOB8
,反之如果是GPIOB5
则可以将该值替换为5
。
配置端口上下拉寄存器 GPIOB_PUD
将 GPIOB8
引脚配置为输出模式之后,还需要再进一步通过端口上下拉寄存器
GPIOB_PUD
将其进一步配置为悬空模式(默认值,即没有上下拉电阻):
同样已知 GPIOB 寄存器的基地址为
0x4800 0400
,而端口上下拉寄存器 GPIOB_PUD
的地址偏移量为 0x0C
,从而就可以计算出其实际地址为:
1 | GPIOB_PUD = 0x4800 0400 + 0x0C = 0x4800 040C |
该寄存器同样通过 GPIOB_PUD
寄存器的第 17 和第 16
两个位来进行控制:
使用时也依然需要先进行清零,然后再将其配置为 00
所代表的悬空模式:
1 | GPIOB_PUD &= ~(0x03 << (2 * 8)); // 将第 17 和 16 位清零 |
配置 GPIO 的输出类型
配置 UINIO-MCU-GD32F350RBT6 的 GPIO 输出类型也可以划分为如下两个步骤:
- 配置端口输出模式寄存器
GPIOx_OMODE
,也就是选择推挽输出还是开漏输出; - 配置端口速度寄存器
GPIOx_OSPD
的输出速度等级,在这里我们选择50MHz
的频率;
端口输出模式寄存器 GPIOB_OMODE
GPIO
的开漏输出模式需要外接上拉电阻,才能够输出高电平,不适用于当前的电路连接关系,在这里我们需要通过端口输出模式寄存器
GPIOB_OMODE
,将其设置为推挽输出模式:
同样已知 GPIOB 的寄存器基地址为
0x4800 0400
,而端口输出模式寄存器 GPIOB_OMODE
的地址偏移量为 0x04
,那么 GPIOB_OMODE
的准确寄存器地址为:
1 | GPIOB_OMODE = 0x4800 0400 + 0x04 = 0x4800 0404 |
根据上图的描述可知,向 GPIOB_OMODE
寄存器的第 8 位写入
0
,就可以将其配置为推挽输出模式:
1 | GPIOB_OMODE &= ~(0x01 << 8) // 将 GPIOB_OMODE 的第 8 位置为 0 |
端口速度寄存器 GPIOB_OSPD
接下来,需要再将端口速度寄存器
GPIOx_OSPD
的输出频率设置为 50MHz
:
根据前面的计算方法,已知 GPIOB 的寄存器基地址为
0x4800 0400
,而端口速度寄存器 GPIOB_OSPD
的地址偏移量为 0x08
,则 GPIOB_OSPD
的准确寄存器地址,可以按照如下方式进行计算得到:
1 | GPIOB_OSPD = 0x4800 0400 + 0x08 = 0x4800 0408 |
根据上图描述的信息,可以向 GPIOB_OSPD
寄存器的第 17 和
第 16 位写入 10
(复位值),就可以将其配置为
2MHz
的输出速率:
1 | GPIOB_OSPD |= (0x02 << (2 * 8)); // 向第 17 和 16 位写入 10 |
而向 GPIOB_OSPD
寄存器的第 17 和 第 16 位写入
01
,则可以将其配置为 10MHz
的输出速率:
1 | GPIOB_OSPD |= (0x01 << (2 * 8)); // 向第 17 和 16 位写入 10 |
如果向 GPIOB_OSPD
寄存器的第 17 和 第 16 位写入的是
11
,则可以将其配置为 50MHz
的输出速率,也就是当前需要为 UINIO-MCU-GD32F350RBT6 的
GPIOB
配置的目标频率:
1 | GPIOB_OSPD &= ~(0x03 << (2 * 8)); // 向第 17 和 16 位写入 11 |
注意:十六进制
0x03
的二进制形式为0000 0011
,十六进制0x02
的二进制形式为0000 0010
,十六进制0x01
的二进制形式为0000 0001
。
控制 GPIO 的输出状态
配置好 GPIOB8
对应的端口时钟
、工作模式
、输出类型
之后,就可以通过使其输出高电平点亮
LED 发光二极管,或者通过低电平熄灭 LED 发光二极管。
端口输出控制寄存器 GPIOB_OCTL
根据用户手册已知 GPIOB 的寄存器基地址为
0x4800 0400
,而端口输出模式寄存器 GPIOB_OCTL
的地址偏移量为 0x14
:
那么端口输出控制寄存器 GPIOB_OCTL
的实际地址,就可以通过下面的等式计算得到:
1 | GPIOB_OCTL = 0x4800 0400 + 0x14 = 0x4800 0414 |
通过向上图当中 GPIOB_OCTL
寄存器的第 8 位
OCTL8
位写入 1
或者
0
,就可以控制相应的 GPIO
引脚输出高电平或者低电平:
1 | GPIOB_OCTL &= ~ (0x01 << 8); // 输出低电平 |
端口位操作寄存器 GPIOB_BOP
除此之外,我们还可以通过端口位操作寄存器
GPIOB_BOP
来操作 GPIO 端口的状态。根据用户手册已知
GPIOB 的寄存器基地址为
0x4800 0400
,而端口输出模式寄存器 GPIOB_BOP
的地址偏移量为 0x18
:
那么端口输出控制寄存器 GPIOB_BOP
的实际地址,就可以通过下面的计算过程获得:
1 | GPIOB_BOP = 0x4800 0400 + 0x18 = 0x4800 0418 |
观察可以发现 GPIOB_BOP
寄存器的高 16 位和低 16
位的每一位,都分别对应着一个 GPIO 引脚。其中低十六位
\(CR_{0 \sim 15}\) 是置 1
位,而高十六位 \(BOP_{0 \sim
15}\) 则属于清 0
位:
GPIOB_BOP
寄存器的高十六位 \(CR_{0 \sim 15}\):置为1
输出低电平,置0
电平状态不改变;GPIOB_BOP
寄存器的低十六位 \(BOP_{0 \sim 15}\):置为1
输出高电平,置0
电平状态不改变;
1 | GPIOB_BOP |= (0x01 << (8 + 16)); // 输出低电平 |
完整 Keil µVision 工程代码
在 Keil-GD32F350RBT6 工程的 Driver
目录下建立一个名为 LED
的子目录,然后分别新建
LED.h
和 LED.c
两个源文件,并且在
main.c
里包含 LED.h
头文件,全部的示例代码内容如下面所示,即
UINIO-MCU-GD32F350RBT6 工程 Examples
目录下的 1-LED-Register
工程:
Drivers/LED.h
1 | /*========== LED.h ==========*/ |
Drivers/LED.c
1 | /*========== LED.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
通过 GPIO 固件库控制 LED
本节内容将采用兆易创新官方提供的标准外设固件库
GD32F3x0_Firmware_Library_V2.2.1
来完成点亮 LED
的实验,这通常需要经历如下四个步骤:
- 调用
rcu_periph_clock_enable()
固件库函数使能 GPIO 端口对应的外设时钟:
1 | void rcu_periph_clock_enable(rcu_periph_enum periph); |
- 通过
gpio_mode_set()
函数配置 GPIO 端口的工作模式以及设置上下拉电阻状态:
1 | void gpio_mode_set(uint32_t gpio_periph, uint32_t mode, uint32_t pull_up_down, uint32_t pin); |
- 通过
gpio_output_options_set()
函数配置指定 GPIO 引脚的输出类型与速率:
1 | `void gpio_output_options_set(uint32_t gpio_periph, uint8_t otype, uint32_t speed, uint32_t pin); |
- 通过
gpio_bit_set/write()
指定 GPIO 引脚的电平状态:
1 | void gpio_bit_set/write(uint32_t gpio_periph, uint32_t pin) |
使能 GPIO 外设时钟
官方固件库 Firmware\GD32F3x0_standard_peripheral\Include
目录下的头文件 gd32f3x0_rcu.h
里,定义了一个专门用于使能外设时钟的库函数:
1 | void rcu_periph_clock_enable(rcu_periph_enum periph) |
这个函数的 periph
参数是一个
rcu_periph_enum
枚举类型的变量,其具体的定义如下面所示:
1 | /* peripheral clock enable */ |
观察可以发现,如果向 rcu_periph_clock_enable()
函数传入上述枚举类型变量当中的枚举值
RCU_GPIOB
,就可以使能 GPIOB
对应的外设时钟:
1 | rcu_periph_clock_enable(RCU_GPIOB); |
配置 GPIO 模式
类似的,固件库
Firmware\GD32F3x0_standard_peripheral\Include
目录下的头文件 gd32f3x0_gpio.h
里定义了一个用于设置
GPIO 工作模式的函数:
1 | /* set GPIO mode */ |
该函数的四个参数,分别用于
设置 GPIO 分组
、配置工作模式
、选择上下拉状态
、指定 GPIO 引脚
,具体参数选项请参考下面的源代码片断:
1 | /* GPIOx(x=A,B,C,D,F) definitions */ |
例如现在要配置 GPIOB8
引脚为悬空输出模式,则只需要向
gpio_mode_set()
函数传入相应的参数即可:
1 | gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, GPIO_PIN_8); |
配置 GPIO 输出类型与速度
固件库 Firmware\GD32F3x0_standard_peripheral\Include
目录下的 gd32f3x0_gpio.h
头文件里面,同样定义有一个用于设置
GPIO 输出类型和速率的库函数:
1 | void gpio_output_options_set(uint32_t gpio_periph, uint8_t otype, uint32_t speed, uint32_t pin); |
这个函数的四个参数,则是分别用于
设置 GPIO 分组
、配置输出类型
、最大输出速率
,具体的参数选项同样请参考下面的源代码片断:
1 | /* GPIO output type */ |
例如当前要配置 GPIOB8
引脚为推挽输出方式,其最大输出速率为
50MHz
,则只需要向 gpio_output_options_set()
函数传入下面的参数即可:
1 | gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8); |
指定 GPIO 引脚电平状态
固件库的 gd32f3x0_gpio.h
头文件里,存在着下面三个可以用于定义 GPIO
引脚电平状态的函数:
1 | /* set GPIO pin bit */ |
其中 gpio_bit_set()
和 gpio_bit_reset()
函数用于指定 GPIO 引脚为固定的高电平状态:
1 | gpio_bit_set(GPIOB, GPIO_PIN_8); // 指定 GPIOB8 引脚为高电平 |
而 gpio_bit_write()
函数则可以用来灵活的设置 GPIO
引脚为高电平或者低电平状态:
1 | gpio_bit_write(GPIOB, GPIO_PIN_8, 0); // 让 GPIOB8 引脚输出低电平 |
完整 Keil µVision 工程代码
接下来,将 Keil-GD32F350RBT6 示例工程里的
LED.h
和 LED.c
以及 main.c
替换为使用固件库的版本,全部的示例代码内容如下面所示,即
UINIO-MCU-GD32F350RBT6 工程 Examples
目录下的 2-LED-Library
工程:
Drivers/LED.h
1 | /*========== UINIO_LED.h ==========*/ |
Drivers/LED.c
1 | /*========== UINIO_LED.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
启动文件 startup_gd32f3x0.s 剖析
在开启进一步的标准固件库学习之前,首先需要了解
Keil-GD32F350RBT6 工程的启动顺序,其中
Firmware\CMSIS\GD\GD32F3x0\Source\ARM
目录下的
startup_gd32f3x0.s
源文件是 GD32F350RBT6
微控制器上电复位之后,执行的第一段程序(由汇编语言编写),该程序主要完成了如下几项工作:
- 配置栈信息;
- 配置堆信息;
- 映射向量表;
- 设置复位处理程序;
- 定义异常/外部中断处理程序;
- 初始化用户堆栈;
在接下来的内容当中,将会根据执行顺序依次探讨
startup_gd32f3x0.s
当中各个代码块的功能与用途。
配置栈信息
栈主要用于存放局部变量
、函数调用
、函数形式参数
,其由高向低生长,且容量不能超过片上
SRAM 存储器的容量大小。
1 | ; <h> Stack Configuration |
上面的汇编代码,开辟了一个大小为 0X00000400
(1KB) 名称为
STACK
的栈,其中 NOINIT
表示不初始化,READWRITE
表示可读可写,ALIGN=3
表示 \(2^3 = 8\) 字节对齐。最后的
__initial_sp
表示栈的结束地址,也就是栈顶地址。
配置堆信息
堆主要用于完成动态内存分配,其由低向高生长,例如
malloc()
函数申请的内存就位于在堆上面。
1 | ; <h> Heap Configuration |
上面的汇编代码,开辟了一个大小为 0X00000400
(1KB) 名称为
HEAP
的堆,同样的 NOINIT
表示不初始化,READWRITE
表示可读可写,ALIGN=3
表示 \(2^3 = 8\) 字节对齐。
除此之外,__heap_base
表示堆的起始地址,而
__heap_limit
表示堆的结束地址。后续的
PRESERVE8
表示保留 8 字节对齐,而 THUMB
表示兼容 THUMB 指令集。
映射向量表
向量表是一个 32 位 WORD
字数组,其按照 4 字节进行边界对齐,从片上 Flash
的零地址开始进行放置,这个数组保存着一系列程序的入口地址,当
GD32F350RBT6
微控制器处于不同的预定义状态时,就会通过查找向量表,进入执行对应地址的程序:
1 | ; /* reset Vector Mapped to at Address 0 */ |
上述汇编代码中的 __Vectors
表示向量表的起始地址,而 __Vectors_End
表示向量表的结束地址。除此之外,其中的 DCD
指令用于分配和初始化一个或者多个以字
Word
为单位的内存空间,并且以 4 字节进行对齐。
设置复位处理程序
复位处理程序是 GD32F350RBT6
上电之后首个要运行的程序,其首先会调用 SystemInit
函数初始化系统时钟,然后再调用 C 库函数 __main
进入用户定义的主函数 main()
:
SystemInit()
是一个 ARM 标准库函数,定义在 Keil-GD32F350RBT6 工程Firmware\CMSIS\GD\GD32F3x0\Source
目录下的system_gd32f3x0.c
当中(即 CMSIS Cortex-M4 外设接入层源文件),主要用于初始化各种系统时钟。__main
是一个标准 C 库函数,主要用于初始化用户堆栈,并且会在最后调用自定义的main()
函数,这也就是main()
总是作为 Keil uVision5 工程入口函数的原因所在。
1 | AREA |.text|, CODE, READONLY |
注意:上述源文件当中的第一句代码,用于定义一个名称为
.text
的只读代码段区域。
定义异常/外部中断处理程序
接下来的代码片段,定义了一系列的异常处理程序和外部中断处理程序,而这些程序的完整实现则保存在外部的
.c
源文件当中。当出现相关异常或者发生指定外部中断的时候,程序的执行流程就会跳转至这里指定的各种服务程序当中:
1 | ;/* dummy Exception Handlers */ |
注意:上述汇编代码当中的
B .
语句表示进入了一个无限循环,即通俗意义上的死循环。
初始化用户堆栈
接着判断当前 Keil uVision5 工程是否启用有
__MICROLIB
库,如果有启用就赋予栈顶地址
__initial_sp
、堆起始地址
__heap_base
、堆结束地址
__heap_limit
。如果没有启用,则会使用双段存储器模式,并且由用户来初始化堆栈空间:
1 | ALIGN |
注意:上述代码当中的
END
是一个汇编程序结束标记。
时钟配置 system_gd32f3x0.c 解析
前面已经介绍过,由汇编语言编写的系统启动文件
startup_gd32f3x0.s
调用了
Keil-GD32F350RBT6 工程的
Firmware\CMSIS\GD\GD32F3x0\Source\system_gd32f3x0.c
源文件当中,由 C 语言编写的系统初始化函数
SystemInit()
:
1 | /*! |
可以看到该函数最终调用的是同样定义在这个源文件里的
system_clock_config()
方法:
1 | /*! |
由于在 system_gd32f3x0.c
源文件的开头位置,存在着如下针对 GDF350
系列微控制器的宏定义:
1 |
所以 system_clock_config()
方法最终实际调用的是该源文件中的函数
system_clock_108m_hxtal()
,这个函数的具体定义如下所示:
1 |
|
锁相环(PLL,Phase Locking Loop)是一种反馈控制电路,其工作过程当中,当输出信号频率与输入信号频率相同时,可以使输出电压与输入电压保持固定的相位差,就像输入/输出电压的相位被锁住了一样,所以这种电路被称为锁相环。GD32F350RBT6 内部的 PLL 主要用于根据特定的外部晶振信号来生成其它频率的信号。
观察可以发现,上述代码使能了 GD32F350RBT6 内部的 PLL
锁相坏,由于当前 UINIO-MCU-GD32F350RBT6
核心板的外部贴片晶振频率为
8MHz
,所以高速外部晶体振荡器时钟
HXTAL = 8MHz
,这样锁相环的输出频率可以按照
如下方式进行计算:
\[ PLL = \frac{HXTAL}{2} \times 27 = \frac{8MHz}{2} \times 27 = 108 MHz \]
上述代码选择了锁相环的输出作为系统时钟
SYSCLK
,并且将高级高性能总线(AHB,Advanced
High-performance Bus)时钟配置为了与系统时钟的频率相等:
\[ AHB = SYSCLK = 108 MHz \]
而两条高级外设总线(APB,Advanced Peripheral Bus)时钟频率分别为 AHB 总线时钟的二分之一:
\[ \begin{cases} APB1 = \frac{AHB}{2} = \frac{108MHz}{2} = 54MHz \\ APB2 = \frac{AHB}{2} = \frac{108MHz}{2} = 54MHz \end{cases} \]
系统滴答定时器 SysTick
SysTick 定时器是一个拥有自动重装载能力的 24
位向下计数器,所有 ARM Cortex-M4
内核微控制器都具备该定时器,从而能够方便的在不同型号微控制器之间进行代码移植。当设定
SysTick
定时器的初始值并且使能之后,每经过一个系统时钟周期,定时器的计数值就会减去
1
,当减至 0
的时候,SysTick
就会自动重新装载初始值,并且继续开始计数,同时置位内部的
COUNTFLAG
标志位,并且触发中断(如果使能有相应的定时器中断)。
观察 GD32F350RBT6
微控制器时钟树可以发现,108MHz
频率的 AHB
总线时钟 CK_AHB
,在经过 8 分频之后,默认作为了 SysTick
系统定时器的时钟源。
使用 SysTick_Config() 配置寄存器
标准固件库 Firmware\CMSIS\core_cm4.h
源文件中的
SysTick_Type
结构体类型,定义了系统滴答定时器 SysTick
相关的寄存器:
1 | /** \brief Structure type to access the System Timer (SysTick). |
该源文件中的 SysTick_Config()
函数,则是用于对上述
SysTick
相关的寄存器进行配置,在初始化和启动系统滴答定时器的同时,产生周期性的中断。概而言之,其主要完成了下面四个步骤的工作:
- 设置
LOAD
重载寄存器的初始值; - 设置 SysTick 定时器中断的优先级为
(1 << 4) - 1 = 15
,即优先级为最低; - 配置
VAL
寄存器,装载 SysTick 的计数值; - 配置
CTRL
寄存器,使能 SysTick 的时钟源、中断、以及外设本身;
1 | /** \brief System Tick Configuration |
滴答定时器官方示例 systick.c
Keil-GD32F350RBT6 示例工程 Sources
目录下提供的 systick.c
源文件,其中的
systick_config()
方法就封装并且调用了上面的
SysTick_Config()
函数,在使能 SysTick
中断服务程序与定时器的同时,以系统时钟频率的千分之一
SystemCoreClock / 1000U
作为 SysTick
滴答定时器配置参数,也就是每 1 秒计数一千次,每一次 1
毫秒(如果修改为 SystemCoreClock / 1000000U
则可以实现微秒级的延时)。
而 systick.c
源文件当中提供的另一个函数
delay_1ms()
,则是以 count
参数(单位为毫秒)进行定时计数,当 volatile
关键字修饰的全局变量 delay
被自减至零的时候,就会自动退出该函数的执行。
1 | /* the systick configuration file */ |
除此之外,上面 systick.c
代码中定义的
delay_decrement()
函数,则会被
Keil-GD32F350RBT6 工程下
Sources/gd32f3x0_it.h/c
源文件内的 SysTick 中断服务程序
SysTick_Handler()
调用,具体调用代码如下所示:
1 | /*! |
当每一次进入 SysTick
系统滴答定时器中断的时候,上面这个函数就会被调用一次,从而就完成了一次对于
delay
变量的自减。
编写 main.c 测试代码
接下来,将前面 LCD 示例工程当中的
main.c
源文件修改为如下的代码,使得 LED
发光二极管可以每间隔 1 秒钟循环进行闪烁:
1 | /*========== main.c ==========*/ |
UINIO_SysTick_Delay_us/ms()
SysTick 系统滴答定时器的 counter
从 reload
值往下递减到零的时候,CTRL
寄存器相应的位就会被置为
1
,而读取该位的时候,其值又会自动被清零,所以利用这个特点就能够以非常简短的代码,实现类似于官方
SysTic 示例的定时器延时功能:
Drivers/SysTick.h
1 | /*========== SysTick.h ==========*/ |
Drivers/SysTick.c
1 | /*========== SysTick.c ==========*/ |
修改 main.c 测试代码
这里可以修改前面的 main.c
源文件,通过自定义的
UINIO_SysTick_Delay_us()
和
UINIO_SysTick_Delay_ms()
函数来进行延时,从而实现相同的 LED
间隔 1 秒循环闪烁的效果:
1 | /*========== main.c ==========*/ |
注意:本节内容涉及的全部源代码,已经保存在 UINIO-MCU-GD32F350RBT6 核心板工程
Examples
目录下的3-Systick
。
库函数修改 SysTick 的时钟源
Keil-GD32F350RBT6 示例工程在
Firmware\GD32F3x0_standard_peripheral\Source\gd32f3x0_misc.c
源文件内提供有一个名为 systick_clksource_set()
的库函数,可以用于修改 SysTick
的时钟源,其功能与参数说明如下面表格所示:
可以看到,该库函数可以用于选择 SysTick 系统滴答定时器的时钟源,可以选择的参数有如下两个:
SYSTICK_CLKSOURCE_HCLK
:系统滴答定时器时钟源来自 AHB 时钟;SYSTICK_CLKSOURCE_HCLK_DIV8
: 系统滴答定时器时钟源来自 AHB 时钟 8 分频(默认);
1 | /*! |
基于位带 Bit Band 执行位操作
嵌入式开发过程当中,经常需要进行位操作(即对一个比特位进行读写),早期的
STC51 系列单片机可以通过关键字 sbit
实现位操作,但是 ARM Cortex-M4
架构的微控制器并不存在类似语法,而是通过提供位带别名区到位带区的映射来实现对比特位的操作。
位带别名区 → 位带区
ARM Cortex-M4
存储映射当中包含有位带别名区(Bit Band
Alias)和位带区(Bit Band
Region)两个区域,通过将位带别名区(Bit Band
Alias)当中的每 1 个 Word
字映射到位带区(Bit Band Region)里的
Bit
位(ARM 体系结构中 1
个字的长度为 32
位),这样操作位带别名区当中的字,就等于操作位带区相应的位,具体原理可以参照下面示意图:
基于 ARM Cortex-M4 架构的 GD32F350RBT6 微控制器,分别在两个区域实现了位带功能(即从位带别名区到位带区的映射):
- 外设 Peripheral 的
0x44000000 ~ 0x42000000
地址范围属于位带别名区,而最低的0x40100000 ~ 0x40000000
区别则属于位带区。 - 静态随机存储器 SRAM 的
0x24000000 ~ 0x22000000
地址范围属于位带别名区,而最低的0x20100000 ~ 0x20000000
区别则属于位带区。
建立通用的映射公式
下面的公式展示了位带别名区域当中的每 1 个字,如何对应到位带区域的相应位上面:
1 | 映射到位带区目标位的别名区的字地址 = 位带别名区起始地址 + (位带区目标位所在字节的地址偏移量 × 32) + (目标位在对应字节当中的位置 × 4) |
根据上面的公式,位带区目标位的序号为 number
(取值范围
0 <= number <= 31
,具体由待操作的目标寄存器决定),则该比特位在别名区的对应地址为:
1 | 目标位映射到外设别名区的地址 = 0x42000000 + (位带区目标位所在字节的地址 - 0x40000000) * 8 * 4 + (number * 4); |
注意:上述公式当中,因为 1 个字节有 8 位,所以需要乘以
8
,而 1 个位膨胀之后对应着 4 个字节,所以需要再乘以4
。
接下来,可以将上述的两个公式合并,成为一个用于将位带区地址
address
和位序号 bit_number
转换为位带别名区地址的 BITBAND()
宏定义函数:
1 |
上述宏定义语句当中的 address & 0xF0000000
用于取出
4
或者 2
,并且以此来判断当前操作的是 SRAM
还是外设别名区:
- 如果取出的是
4
,加上0X0200 0000
之后等于外设别名区的起始地址0X4200 0000
; - 如果取出的是
2
,加上0X0200 0000
之后等于 SRAM 别名区的起始地址0X2200 0000
;
而上述宏定义语句当中 address & 0x00FF FFFF
得到的结果,与减去 0X2000 0000
或者
0X4000 0000
得到的结果相同,而后续的
<< 5
以及 << 2
则分别起到了乘以
32
和乘以 4
的作用(即两种计算方式获得的二进制、十进制、十六进制结果完全相同):
乘法与位运算的对应关系 | |||
---|---|---|---|
10 * 2 = 10 << 1 |
10 * 4 = 10 << 2 |
10 * 8 = 10 << 3 |
10 * 16 = 10 << 4 |
10 * 32 = 10 << 5 |
10 * 64 = 10 << 6 |
10 * 108 = 10 << 7 |
10 * 256 = 10 << 8 |
除法与位运算的对应关系 | |||
---|---|---|---|
10 / 2 = 10 >> 1 |
10 / 4 = 10 >> 2 |
10 / 8 = 10 >> 3 |
10 / 16 = 10 >> 4 |
10 / 32 = 10 >> 5 |
10 / 64 = 10 >> 6 |
10 / 108 = 10 >> 7 |
10 / 256 = 10 >> 8 |
最后,就可以通过指针操作位带别名区的地址,进而实现对于位带区相应比特位的操作:
1 |
由于 GD32F350RBT6 微控制器的
GPIOA ~ GPIOF
基地址定义如下面列表所示:
- GPIOA 基地址:
0x4800 0000
; - GPIOB 基地址:
0x4800 0400
; - GPIOC 基地址:
0x4800 0800
; - GPIOD 基地址:
0x4800 0C00
; - GPIOF 基地址:
0x4800 1400
;
则可以将上述 GPIO 接口所对应的端口输出控制寄存器
GPIOx_OCTL
以及端口输入状态寄存器
GPIOx_ISTAT
地址,封装为如下的宏定义语句:
1 | /* 端口输出控制寄存器 GPIOx_OCTL 地址 */ |
上述代码当中的 GPIOx
已经被定义在固件库的
gd32f3x0_gpio.h
头文件当中,编译的时候已经由 Keil
uVision5
自动包含相关路径。接下来的时间,就可以基于上面列出的寄存器地址,进一步将它们封装为控制
GPIO 输入与输出的宏定义函数:
1 | /* 控制 GPIOA 的输入与输出 */ |
完整 Keil µVision 工程代码
BitBand.h
把上述的宏定义代码保存至 Drivers/BitBand
目录下的
BitBand.h
头文件里:
1 | /*========== BitBand.h ==========*/ |
main.c
继续修改之前的 LED 闪烁示例工程 main.c
源文件,在包含
BitBand.h
头文件的同时,通过调用宏定义函数
GPIOB_Out(8)
修改 GPIOB8
引脚的电平状态:
1 | /*========== main.c ==========*/ |
除此之外,也可以将位带操作相关的宏定义代码,更加直观的声明在
main.c
源文件当中:
1 | /*========== main.c ==========*/ |
GPIO 输入模式与按键
按键使用原理
微动开关或者轻触按键是通过内部的触点
与弹片
来实现导通与截止,将其连接至
GD32F350RBT6 的 GPIO 引脚,就可以通过检测其按下之后
GPIO 引脚获取的电平状态,来判断当前是处于按下还是松开的情况:
当按键在被按下或者松开的时候,会由于触点上弹片的弹性作用,发生
5ms ~ 10ms
机械抖动,为了避免 GPIO
得到错误的状态,必须考虑采取一定的措施去消除这种抖动所带来的干扰:
- 硬件消抖:微动开关两侧并联上一枚电容,利用其充放电作用吸收抖动产生的振荡。
- 软件消抖:当开关按下时,通过延时代码规避掉抖动发生的时间。
首先,将一枚轻触按键 SW1
连接到
UINIO-MCU-GD32F350RBT6 的 GPIOB9
引脚,其中一端连接至 3V3
引脚,而另一端经过位号为
R3
的 10KΩ
下拉电阻之后连接至
GND
引脚:
然后,使用固件库提供的工具函数,把 GPIOB9
配置为带下拉电阻的输入模式,通过检测该引脚的电平状态,就可以判断按键
SW1
是否被按下(按键松开为低电平,按键按下为高电平)。
完整 Keil µVision 工程代码
如前所述,使用 GPIO 端口的输入输出功能,通常会需要经历下面三个步骤:
- 通过
void rcu_periph_clock_enable(rcu_periph_enum periph);
固件库函数启用 GPIO 端口对应的外设时钟。 - 通过
void gpio_mode_set(uint32_t gpio_periph, uint32_t mode, uint32_t pull_up_down, uint32_t pin);
固件库函数配置 GPIO 端口的工作模式(输入模式GPIO_MODE_INPUT
、输出模式GPIO_MODE_OUTPUT
、备用功能模式GPIO_MODE_AF
、模拟模式GPIO_MODE_ANALOG
),以及设置上下拉电阻状态(悬空无上下拉GPIO_PUPD_NONE
、带上拉电阻GPIO_PUPD_PULLUP
、带下拉电阻GPIO_PUPD_PULLDOWN
)。 - 通过
void gpio_bit_toggle(uint32_t gpio_periph, uint32_t pin);
固件库函数翻转 LED 对应 GPIO 引脚的电平状态。
注意:本节内容所涉及的全部测试代码,已保存在 UINIO-MCU-GD32F350RBT6 核心板开源项目
Examples
目录下的 5-Key 工程当中。
Drivers/key.h
1 | /*========== Key.h ==========*/ |
Drivers/key.c
1 | /*========== Key.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
USART 通用同步/异步收发器
USART 串行协议分析
通用同步/异步收发器(USART,Universal
Synchronous/Asynchronous Receiver
Transmitter)是一种基于数据帧的串行数据通信方式,每一个数据帧都会以
1 个起始位
开始,并且以 1
个停止位
结束,其数据帧的基本格式如下面示意图所示:
- 起始位:首先发送一个起始位,通常为逻辑低电平
0
,用于通知接收端数据即将开始发送。 - 数据位:紧接着是数据位,可以有
5 ~ 8
位长度,按照由 LSB(最低有效位)到 MSB(最高有效位)的顺序发送。 - 校验位:数据位之后是可选的奇偶校验位,用于检查数据传输过程当中,是否存在错误。
- 停止位:最后是停止位,通常为逻辑高电平
1
,用于标记数据帧传输完毕。
注意:空闲帧与停止位一样均为高电平,如果 USART 连接断开,则下拉为低电平,从而成为断开帧。
USART
串行接口可以工作在单工(单向通信)、半双工(双向分时通信)、全双工(双向通信)模式下。每个
USART 通信设备之间的 波特率(单位为
bit/s
,即每秒钟传送的比特位数)、数据位、停止位、奇偶校验位
必须保持一致。
将 UINIO-MCU-GD32F350RBT6 核心板与另一款 UINIO
系列开源硬件 UINIO-USB-UART
串口调试器 ,参照下图的线路相互进行连接(即
UINIO-MCU-GD32F350RBT6 核心板的 GPIOA9
和
GPIOA10
分别连接至 UINIO-USB-UART
串口调试器的 RXD
和 TXD
引脚),并且将后者的
Type-C 接口通过 USB 线缆连接至计算机,从而建立起与串行通信上位机软件的
USART 连接,进而可以查看到后续实验代码所打印出的测试数据。
完整 Keil µVision 工程代码
使用 GD32F350RBT6 的片上 USART 外设进行通信,需要经历下面六个步骤:
- 使能 USART 和 GPIO 外设时钟
rcu_periph_clock_enable()
。 - 配置 GPIO 复用模式
gpio_af_set()
。 - 配置 GPIO 的工作模式
gpio_mode_set()
。 - 配置 GPIO 的输出模式与速度
gpio_output_options_set
。 - 复位 USART 外设
usart_deinit()
,并且配置其工作参数usart_deinit()
、usart_baudrate_set()
、usart_parity_config()
、usart_word_length_set()
、usart_stop_bit_set()
。 - 使能 USART 串口
usart_enable()
及其发送功能usart_transmit_config()
。
注意:本节内容所涉及的全部测试代码,已保存在 UINIO-MCU-GD32F350RBT6 核心板开源项目
Examples
目录下的 6-USART 工程当中。
Drivers/USART.h
1 |
|
Drivers/USART.c
1 |
|
Sources/main.c
1 | /*========== main.c ==========*/ |
外部中断 EXTI
嵌套向量中断控制 NVIC
GD32F350RBT6 微控制器所采用的 ARM Cortex-M4 内核架构集成有嵌套式矢量型中断控制器(NVIC,Nested Vectored Interrupt Controller),主要用于管理和处理中断:
- 中断管理:当中断源产生中断信号的时候,NVIC 就会捕获处理这些信号。
- 优先级处理:通过对中断优先级进行排序,NVIC 会确保高优先级的中断首先得到处理。
- 中断嵌套:如果在一个中断服务程序当中,发生了另外一个更高优先级的中断,NVIC 会暂停处理当前中断,转而处理更高优先级的中断。
- 向量中断:每一个中断都关联有一个固定地址的中断服务程序,当中断发生时 NVIC 就会根据这个地址去执行相应的中断服务程序。
- 中断屏蔽:NVIC 允许通过编程来屏蔽某些中断,以防止它们被处理,该功能在特定情况下非常有用,例如需要执行关键任务而不希望被其它中断事件打断的时候。
- 低功耗模式支持:NVIC 与微控制器的低功耗模式紧密集成,休眠模式下 NVIC 可以检测外部中断并且唤醒 MCU,从而实现在低功耗状态下的快速响应。
外部中断/事件控制器 EXTI
外部中断/事件控制器(EXTI,External Interrupt/Event Controller)负责检测来自于中断源的中断请求,并且通知微控制器进行处理。其包含有 24 个相互独立的边沿检测电路(每个边沿检测电路都可以独立进行配置与屏蔽),能够在微控制器内核当中产生中断请求以及唤醒事件。每一个中断都拥有 4 位的中断优先级配置位,可以提供 16 个中断优先等级,并且这些中断都拥有着 上升沿、下降沿、任意边沿 三种触发方式:
- 中断线配置:配置外部中断线(EXTI Line),也就是 MCU 当中用于接收中断请求的物理线路。
- 触发条件方式:设置中断的触发方式,即
上升沿
、下降沿
、任意边沿
。 - 中断优先级管理:分配不同的中断优先级,确保 MCU 能够按照预期的顺序与优先级处理中断请求。
- 中断请求生成与处理:当外部事件满足中断触发条件时,生成一个中断请求发送给 NVIC(嵌套向量中断控制器),后者会根据中断的优先级来决定是否响应该中断,如果需要响应,就会暂停执行当前的程序,转而将控制权移交给指定的 ISR(中断服务程序,Interrupt Service Routine)。
- 中断服务程序执行:在 ISR 中断服务程序当中,可以编写代码处理外部事件。待处理完毕之后,就会将控制权返还给刚才被中断的程序,从之前暂停的位置继续运行。
EXTI 中断的触发源可以来自于
GPIOA/B/C/F (0~15)
引脚,以及
LVD
(低电压检测)、RTC
(实时时钟)、CEC
(HDMI
的 CEC
控制器)、CMP
(比较器)、USB
、USART
等片上外设:
注意:上述表格当中 EXTI 中断线与触发源的对应关系非常重要。
接下来的实验里,首先需要将一枚轻触按键 SW2
连接到
UINIO-MCU-GD32F350RBT6 核心板的 GPIOA0
引脚,其中一端连接至 3V3
,而另外一端经过位号为
R5
的 10KΩ
下拉电阻之后连接至
GND
。然后再把 LED3
的一端通过限流电阻
R4
连接至 GPIOB8 引脚,另外一端连接到
GND
引脚:
最后,再将 UINIO-MCU-GD32F350RBT6 的
GPIOA9 和 GPIOA10 分别连接至
UINIO-USB-UART 串口调试器的 RXD
和
TXD
引脚,以便于通过上位机软件观察 USART
串口输出的调试数据。
完整 Keil µVision 工程代码
使用 GD32F350RBT6 微控制器的 EXTI 外部中断功能,通常需要经历下面一系列步骤:
- 通过
rcu_periph_clock_enable()
使能 GPIO 引脚和 CGFCMP 系统配置外设时钟。 - 调用
nvic_priority_group_set()
配置优先级分组。 - 调用
nvic_irq_enable()
使能 NVIC 中断,并且配置抢占优先级和响应优先级。 - 通过
syscfg_exti_line_config()
将中断线与 GPIO 引脚进行连接。 - 调用
exti_init()
设置中断线、中断模式、触发类型。 - 使能中断线
exti_interrupt_enable()
,并且清除中断标志位exti_interrupt_flag_clear()
。 - 编写已经在
startup_gd32f3x0.s
启动文件当中定义好名称,且参数和返回值皆为void
的中断服务函数(每次中断执行完毕之后都需要清除一下中断标志位)。
注意:本节内容所涉及的全部测试代码,已保存在 UINIO-MCU-GD32F350RBT6 核心板开源项目
Examples
目录下的 7-EXTI-Key 工程当中。
Drivers/EXTI-Key.h
1 | /*========== EXTI-Key.h ==========*/ |
注意:上述代码当中的中断线 0 和 1 的中断服务函数名称
EXTI0_1_IRQHandler
已经被定义在startup_gd32f3x0.s
启动文件当中。
Drivers/EXTI-Key.c
1 | /*========== EXTI-Key.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
定时器 TIMER 概览
GD32F350RBT6
微控制器的定时器是一个可编程的无符号计数器,支持输入捕获
与输出比较
,可以按照功能特性被划分为六种类型:
- 高级定时器(
TIMER0
); - 通用定时器 L0(
TIMER1
和TIMER2
); - 通用定时器 L2(
TIMER13
); - 通用定时器 L3(
TIMER14
); - 通用定时器 L4(
TIMER15
和TIMER16
); - 基本定时器(
TIMER5
);
也就是 1 个 16
位高级定时器(TIMER0
),1 个 32
位通用定时器(TIMER1
),5 个 16
位通用定时器(TIMER2
、TIMER13 ~ TIMER16
),1
个 16 位基本定时器(TIMER5
)。
高级定时器 TIMER0
高级定时器(TIMER0)属于可编程的四通道定时器,包含有 16 位无符号计数器,支持输入捕获与输出比较。可以用于产生 PWM 信号控制电机(包含有死区时间插入模块)以及进行电源管理,其主要特性如下表所示:
高级定时器 TIMER0 特性 | 功能描述 |
---|---|
总通道数 | 4 通道 |
计数器宽度 | TIMER0 是 16
位 |
时钟源可选 | 内部时钟、内部触发、外部输入、外部触发 |
多种计数模式 | 向上计数、向下计数、中央计数 |
正交编码器接口 | 用于追踪运动和分辨旋转方向和位置 |
霍尔传感器接口 | 可以用于控制三相电机 |
可编程的预分频器 | 16 位(运行时可以被改变) |
每个通道可配置 | 输入捕获模式、输出比较模式、可编程的 PWM 模式、单脉冲模式 |
可编程的死区时间 | 支持 |
自动重装载功能 | 支持 |
可编程的计数器重复功能 | 支持 |
中止输入功能 | 支持 |
中断输出和 DMA 请求 | 更新事件、触发事件、比较/捕获事件、换相事件、中止事件 |
多个定时器的菊链 | 使得一个定时器,能够同时启动多个定时器 |
定时器的同步 | 允许被选择的定时器在同一个时钟周期开始计数 |
定时器主-从管理 | 支持 |
下面的结构框图提供了高级定时器(TIMER0)的内部配置细节:
通用定时器 L0 - TIMER1, TIMER2
通用定时器 L0(TIMER1, TIMER2)同样属于可编程的四通道定时器,包含有 16 位无符号计数器,支持输入捕获与输出比较。可以用于产生 PWM 信号控制电机以及进行电源管理,其主要特性如下表所示:
高级定时器 TIMER1, TIMER2 特性 | 功能描述 |
---|---|
总通道数 | 4 通道 |
计数器宽度 | TIMER2 是 16
位,TIMER1 是 32 位 |
时钟源可选 | 内部时钟、内部触发、外部输入、外部触发 |
多种计数模式 | 向上计数、向下计数、中央计数 |
正交编码器接口 | 用于追踪运动和分辨旋转方向和位置 |
霍尔传感器接口 | 可以用于控制三相电机 |
可编程的预分频器 | 16 位(运行时可以被改变) |
每个通道可配置 | 输入捕获模式、输出比较模式、可编程的 PWM 模式、单脉冲模式 |
自动重装载功能 | 支持 |
中断输出和 DMA 请求 | 更新事件、触发事件、比较/捕获事件 |
多个定时器的菊链 | 使得一个定时器,能够同时启动多个定时器 |
定时器的同步 | 允许被选择的定时器在同一个时钟周期开始计数 |
定时器主-从管理 | 支持 |
下面的结构框图提供了通用定时器 L0(TIMER1, TIMER2)的内部配置细节:
通用定时器 L2 - TIMER13
通用定时器 L2(TIMER13)属于可编程的单通道定时器,包含有 16 位无符号计数器,支持输入捕获与输出比较。可以用于产生 PWM 信号控制电机以及进行电源管理,其主要特性如下表所示:
高级定时器 TIMER13 特性 | 功能描述 |
---|---|
总通道数 | 1 通道 |
计数器宽度 | TIMER13 是 16
位 |
时钟源可选 | 内部时钟 |
计数模式 | 向上计数 |
可编程的预分频器 | 16 位(运行时可以被改变) |
每个通道可配置 | 输入捕获模式、输出比较模式、可编程的 PWM 模式 |
自动重装载功能 | 支持 |
中断输出 | 更新事件、比较/捕获事件 |
下面的结构框图提供了通用定时器 L2(TIMER13)的内部配置细节:
通用定时器 L3 - TIMER14
通用定时器 L3(TIMER14)属于可编程的两通道定时器,包含有 16 位无符号计数器,支持输入捕获与输出比较。可以用于产生 PWM 信号控制电机(包含有死区时间插入模块)以及进行电源管理,其主要特性如下表所示:
高级定时器 TIMER14 特性 | 功能描述 |
---|---|
总通道数 | 2 通道 |
计数器宽度 | TIMER14 是 16
位 |
时钟源可选 | 内部时钟、内部触发、外部输入 |
计数模式 | 向上计数 |
可编程的预分频器 | 16 位(运行时可以被改变) |
每个通道可配置 | 输入捕获模式、输出比较模式、可编程的 PWM 模式、单脉冲模式 |
可编程的死区时间 | 支持 |
自动重装载功能 | 支持 |
可编程的计数器重复功能 | 支持 |
中止输入功能 | 支持 |
中断输出和 DMA 请求 | 更新事件、比较/捕获事件、换相事件、中止事件 |
多个定时器的菊链 | 使得一个定时器,能够同时启动多个定时器 |
定时器的同步 | 使得一个定时器,能够同时启动多个定时器 |
定时器主-从管理 | 支持 |
下面的结构框图提供了通用定时器 L3(TIMER14)的内部配置细节:
通用定时器 L4 - TIMER15, TIMER16
通用定时器 L4(TIMER15, TIMER16)属于可编程的单通道定时器,包含有 16 位无符号计数器,支持输入捕获与输出比较。可以用于产生 PWM 信号控制电机(包含有死区时间插入模块)以及进行电源管理,其主要特性如下表所示:
高级定时器 TIMER15, TIMER16 特性 | 功能描述 |
---|---|
总通道数 | 单通道 |
计数器宽度 | TIMER15 和
TIMER16 都是 16 位 |
时钟源可选 | 内部时钟 |
计数模式 | 向上计数 |
可编程的预分频器 | 16 位(运行时可以被改变) |
每个通道可配置 | 输入捕获模式、输出比较模式、可编程的 PWM 模式、单脉冲模式 |
可编程的死区时间 | 支持 |
自动重装载功能 | 支持 |
可编程的计数器重复功能 | 支持 |
中止输入功能 | 支持 |
中断输出和 DMA 请求 | 更新事件、比较/捕获事件、换相事件、中止事件 |
下面的结构框图提供了通用定时器 L4(TIMER15, TIMER16)的内部配置细节:
基本定时器 - TIMER5
基本定时器(TIMER5)包含有 16 位无符号计数器,支持输入捕获与输出比较。可以用于通用定时器,产生 DMA 请求,以及为 DAC 数模转换提供时钟,其主要特性如下表所示:
基本定时器 TIMER5 特性 | 功能描述 |
---|---|
计数器宽度 | TIMER5 是 16
位 |
时钟源可选 | 内部时钟 |
计数模式 | 向上计数 |
可编程的预分频器 | 16 位(运行时可以被改变) |
自动重装载功能 | 支持 |
中断输出和 DMA 请求 | 更新事件 |
下面的结构框图提供了基本定时器(TIMER5)的内部配置细节:
基本定时器 TIMER5 与中断
本章节内容,将会利用基本定时器 TIMER5
以及其关联的定时器中断,来实现让 LED 每隔 1
秒不间断进行闪烁的实验。其中,定时器时钟和运行参数的配置,是两个比较重要的知识点,需要大家在实验过程当中特别留意。
定时器时钟配置
观察下面定时器相关的时钟树,可以发现如果 APB
总线的时钟分频系数为
1
,那么定时器时钟频率就会与 AHB
总线保持一致。否则,定时器的时钟频率会被设定为 APB
总线频率的 2 倍:
可以看到,系统时钟 CK_SYS
在经过
AHB 预分频器之后,可以得到 AHB 总线时钟
CK_AHB
。而这个 CK_AHB
再经过 APB1 和
APB2 预分频器之后,就可以得到定时器时钟
CK_TIMERx
,具体请参考下面的计算公式:
\[ CK_{TIMERx} = \frac{CK_{AHB}}{APB_{x预分频值} \div 2} \]
由于固件库 system_gd32f3x0.c
源文件的
system_clock_config()
函数当中,已经将
APB1 和 APB2 的预分频值设定为
2
:
1 | /* APB2 = AHB/2 */ |
根据上面的计算公式,就可以知道定时器时钟
CK_TIMERx
与 AHB 总线时钟
CK_AHB
的时钟频率值相等:
\[ CK_{TIMERx} = \frac{CK_{AHB}}{2 \div 2} = CK_{AHB} = 108MHz \]
定时器工作参数配置
官方固件库 gd32f3x0_timer.h
头文件当中定义的结构体变量
timer_parameter_struct
,可以用于配置定时器的相关工作参数:
1 | /* constants definitions */ |
在下面的列表里,展示了这些参数的具体功能与用途:
- prescaler:时钟的 16 位 预分频值,取值范围为
0 ~ 65535
。 - alignedmode:对齐模式,可供选取的值有
TIMER_COUNTER_EDGE
、TIMER_COUNTER_CENTER_DOWN
、TIMER_COUNTER_CENTER_UP
、TIMER_COUNTER_CENTER_BOTH
。 - counterdirection:计数方向,可供选取的值有
TIMER_COUNTER_UP
和TIMER_COUNTER_DOWN
。 - period:周期,取值范围为
0 ~ 65535
,当计数器达到周期值的时候,计数值将会清零,可以配合计数器时钟频率计算出中断时间。 - clockdivision:时钟分频因子,可供选取的值有
TIMER_CKDIV_DIV1
、TIMER_CKDIV_DIV2
、TIMER_CKDIV_DIV4
,主要用于输入捕获场景。 - repetitioncounter:重复计数器值(仅限于高级定时器),取值范围为
0 ~ 255
。
实验电路的搭建
类似于前面 《通过 GPIO 固件库控制
LED》 章节的实验电路,这里同样将 4.7K
限流电阻
R1
与 LED 发光二极管串联之后,再连接到
UINIO-MCU-GD32F350RBT6 核心板的 GPIOB8
引脚(高电平点亮,低电平熄灭):
除此之外,还需要再将 UINIO-MCU-GD32F350RBT6 核心板的
GPIOA9 和 GPIOA10 引脚,分别连接至
UINIO-USB-UART 串口调试器的 RXD
和
TXD
引脚,这样就可以完成实验电路的搭建。
完整 Keil µVision 工程代码
本节内容的实验,主要基于 16 位的基本定时器
TIMER5
来实现 LED 每间隔 1
秒进行闪烁的效果,完成该功能大致需要经历下面六个步骤:
- 配置定时器时钟,由于固件库已经默认
CK_TIMERx = CK_AHB = 108MHz
,所以本示例缺省该步骤。 - 配置并且初始化定时器,也就是设置
timer_parameter_struct
结构体的成员属性,然后调用timer_init()
初始化定时器。 - 调用
nvic_irq_enable()
设置定时器中断的优先级。 - 调用
timer_interrupt_enable()
使能定时器更新中断事件。 - 调用
timer_enable()
函数使能定时器自身。 - 自定义基本定时器 TIMER5 相关的中断服务函数
TIMER5_DAC_IRQHandler()
,该函数名称已在启动文件startup_gd32f3x0.s
进行过声明。
注意:本节内容所涉及的全部测试代码,已保存在 UINIO-MCU-GD32F350RBT6 核心板开源项目
Examples
目录下的 8-Timer-LED 工程当中。
Drivers/Timer-LED.h
1 | /*========== TIMER_LED.h ==========*/ |
Drivers/Timer-LED.c
1 | /*========== TIMER_LED.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
通用定时器 TIMER1 与 PWM
脉冲宽度调制 PWM 简介
脉冲宽度调制(PWM,Pulse-width modulation)是一种通过将电平信号分散为离散形式,从而达到调整电压和频率,乃至于平均功率的目的。
这项技术可以用于动态控制 LED 亮度乃至于电机转速,其主要涉及到如下三个重要的参数:
- 频率(Frequency):单位时间内周期性事件的重复次数,即 PWM 在 1 秒钟之内,脉冲信号完整周期的出现次数,其值等于 \(频率 f = \frac{1}{周期 T}\),单位为赫兹。
- 周期(Period):一个完整信号周期所持续的时间,其值等于 \(周期 T = \frac{1}{频率 f}\),单位为秒。
- 占空比(Duty Cycle):在一个完整的脉冲信号周期当中,高电平所占据的百分比值。
定时器 TIMER1 的 PWM 通道
GD32F350RBT6 微控制器的 TIMER1
是一个
通用定时器,拥有四路 PWM 通道,其中的每一路通道都对应着
1 个 GPIO 引脚(需要进行复用设置)。通过下面的表格,可以发现
GPIOA5
引脚的复用功能 AF2
,对应的就是
TIMER1
定时器的 CH0
通道:
注意:GPIO 的复用功能可以通过固件库函数
void gpio_af_set(uint32_t gpio_periph, uint32_t alt_func_num, uint32_t pin)
进行设置。
PWM 脉冲频率的计算
根据下面通用定时器 TIMER1
的结构框图,可以观察到该定时器各个通道时钟信号的来龙去脉。其中带有层叠效果的框图,表示其对应有影子寄存器:
注意:影子寄存器可以让指令重复使用相同的寄存器编码,但是在不同模式下,这些编码对应的是不同的物理寄存器。
相比于之前基本定时器的实验,本实验需要将定时器配置函数
UINIO_PWM_Config()
的时钟分频值修改为
108
,从而使得分频后的定时器时钟频率等于:
\[ 分频后的时钟频率 PSC_{CLK} = \frac{定时器时钟频率 108MHz}{预分频值108} = 1MHz \]
再根据下面的公式,就可以计算得到此时 PWM 脉冲宽度调制信号的输出频率为
100Hz
:
\[ PWM 输出频率 = \frac{分频后的时钟频率 1MHz}{周期值 10000 微秒} = 100Hz \]
注意:该脉冲频率远高于肉眼可以鉴别出的
50Hz
临界闪烁频率,所以不会导致 LED 发生明显的闪烁现象,可以呈现出比较完美的呼吸灯效果。
实验电路的搭建
类似于之前 《基本定时器 TIMER5
与中断》 章节的实验电路,这里同样需要将 4.7K
限流电阻
R1
与 LED
发光二极管进行串联,有所不同之处在于这里需要将其连接至
UINIO-MCU-GD32F350RBT6 核心板的 GPIOA5
引脚,然后由通用定时器 TIMER1
的通道
0
输出 PWM 脉冲信号:
除此之外,依然需要把 UINIO-MCU-GD32F350RBT6 核心板的
GPIOA9 和 GPIOA10 引脚,分别连接至
UINIO-USB-UART 串口调试器的 RXD
和
TXD
引脚,从而能够使用串口上位机软件,查看到当前 LED
的亮灭状态调试信息。
完整 Keil µVision 工程代码
本实验通过 PWM 输出脉冲波来实现 LED 的呼吸灯效果,大致上需要经历如下一系列的配置过程:
- 调用
gpio_af_set()
配置 PWM 功能对应 GPIO 引脚的复用功能。 - 使用
timer_init()
配置 PWM 定时器参数。 - 使用
timer_channel_output_config()
配置 PWM 输出通道参数。 - 通过
timer_channel_output_pulse_value_config()
函数将定时器TIMER1
通道输出的脉冲值置为0
。 - 使用
timer_channel_output_mode_config()
配置定时器输出通道的比较模式为 PWM 模式 0。 - 使用
timer_channel_output_shadow_config()
失能定时器输出通道的比较影子寄存器。 - 调用
timer_auto_reload_shadow_enable()
使能定时器自动重载影子寄存器。 - 调用
timer_enable()
使能 PWM 相关的定时器。 - 循环调用
timer_channel_output_pulse_value_config()
函数,通过动态设定脉冲值(介于0 ~ 65535
范围)实现 LED 的呼吸灯效果。
Drivers/PWM-LED.h
1 | /*========== PWM_LED.h ==========*/ |
Drivers/PWM-LED.c
1 | /*========== PWM_LED.h ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
直接存储器存取 DMA 与中断
DMA 功能简介
直接存储器存取(DMA,Direct Memory
Access)主要运用在不占用内核计算资源的情况下,进行数据的传递(外设 → 存储器
、存储器 → 外设
、存储器 → 存储器
)。GD32F350RBT6
只拥有一个 DMA 控制器,其拥有 7
个通道,每个通道都用于处理各个外设的存储器访问请求,这些外设包括有
ADC、SPI、I2C、USART、DAC、I2S
以及定时器。
观察上面的 DMA 功能结构框图,可以发现 DMA 控制器主要由如下四个部分组成:
- 通过 AHB 总线从接口进行 DMA 配置。
- 通过 AHB 总线主接口进行数据传输。
- 由仲裁器(Arbiter)对 DMA 请求的优先级进行管理。
- 控制存储器或者外设的状态,并且管理计数器。
实验电路的搭建
本节内容的实验,需要通过 USART 输出 DMA 传输过来的数据信息,所以依然要把 UINIO-MCU-GD32F350RBT6 核心板与另外一款 UINIO 系列开源硬件 UINIO-USB-UART 串口调试器 ,参照下图的线路进行相互连接:
也就是把 UINIO-MCU-GD32F350RBT6 核心板的
GPIOA9
和 GPIOA10
引脚,分别连接至
UINIO-USB-UART 串口调试器的 RXD
和
TXD
引脚,然后将后者的 Type-C 接口通过 USB
线缆连接到计算机,进而可以借助 COMTransmit
等串口调试助手软件,查看到 DMA 传输过来的各种数据和日志信息。
完整 Keil µVision 工程代码
当使用 DMA 进行数据传输时,会首先从源地址读取数据,然后再将读取的数据存储到目的地址,使用时通常需要遵循如下步骤:
- 通过
rcu_periph_clock_enable(RCU_DMA)
使能 DMA 外设时钟。 - 配置 DMA 参数结构体
dma_parameter_struct
。 - 初始化 DMA 通道
dma_init()
。 - 调用
dma_circulation_enable/disable()
和dma_memory_to_memory_enable/disable()
配置 DMA 相关模式。 - 执行
dma_interrupt_enable()
使能 DMA 中断。 - 执行
dma_channel_enable()
使能 DMA 通道本身。
Sources/gd32f3x0_it.c
1 | /*========== gd32f3x0_it.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
ADC 模数转换器外设
ADC 外设简介
GD32F350RBT6 微控制器集成有 12
位逐次逼近型模数转换器(ADC,Analog
Digital Converter),可以采集来自 16
个外部通道(即 MCU 引脚)、2
个内部通道,以及电池电压
VBAT
通道的模拟信号。采样转换完成之后,转换结果可以按照最低/最高有效位的对齐方式,保存在相应的数据寄存器当中。
注意:逐次逼近型 ADC 通过产生一系列比较电压,逐次与输入的模拟电压信号进行比较,以一次一次逐步接近的方式,将模似信号转换成最接近的数字信号。
ADC 内部输入信号 | 功能说明 | ADC 输入引脚定义 | 功能说明 |
---|---|---|---|
\(V_{SENSE}\) | 内部温度传感器输出电压。 | \(VDDA\) | 模拟电源正等于 \(V_{DD}\),\(2.6V \le VDDA \le 3.6V\)。 |
\(V_{REFINT}\) | 内部参考输出电压。 | \(VSSA\) | 模拟电源负等于 \(V_{SS}\),通过磁珠单点接入
GND 。 |
\(V_{BAT} / 2\) | 硬件输入电压除以二。 | \(ADCx_IN [15:0]\) | 多达 16 路外部通道。 |
注意:UINIO-MCU-GD32F350RBT6 核心板的模拟电源负引脚
VSSA
,使用了对于100Mhz
高频杂散信号存在1KΩ
阻抗的磁珠进行单点接地;
ADC 采样通道与模式
GD32F350RBT6 微控制器上的这总共 19 条 ADC 采样通道,都支持如下几种运行模式:
- 单次转换模式:每进行 1 次 ADC 转换后,ADC 就会自动停止,并将结果保存在 ADC 数据寄存器当中。
- 扫描模式:用于对多个输入通道进行依次采集,ADC 会根据配置的通道采集顺序,对多个通道依次进行采样转换。
- 连续转换模式:当 ADC 完成 1
次转换之后,就会启动另外 1
次转换,周而复始,直至
外部触发
或者软件触发
停止这个转换过程。 - 间断模式:用于在注入通道(即在规则通道转换时,需要强行插入的通道)和常规通道之间进行切换,ADC 会优先转换注入通道,完成之后再自动切换到常规通道进行转换。
ADC 采样的触发方式主要有外部触发和软件触发两种:
- 外部触发:在外部输入信号的
上升沿
或者下降沿
,都可以触发规则组或者注入组的 ADC 转换。 - 软件触发:由软件控制在固定的时间点进行 ADC 转换,通常用于采集精度要求较高的场景。
ADC 性能参数
使用 ADC 模数转换器外设的时候,需要特别注意下面三个主要性能参数:
- 分辨率:表示 ADC 转换器的输出精度,单位为
bit
位,分辨率越高,采样精度也就越高,但是 ADC 所花费的采样转换时间就会越长。 - 采样率:表示 ADC
每秒对于模拟信号进行采样的次数,单位为赫兹
Hz
或者样本数量Sample / 秒S
。采样率高就表示 ADC 能够更快的将模拟信号转换为数字信号,从而更加准确的反映模拟信号的变化。 - 采样范围:是指 ADC 可以采集到的模拟电压输入信号范围,通常位于参考电压 \(V_{REF}\) 范围之内,即 \(0V \le ADC \le V_{REF}\)。
实验电路的搭建
本节的实验会将 UINIO-MCU-GD32F350RBT6 核心板的
GPIOC1 作为 ADC 采样引脚,分别去获取
TP1
(连接至 3V3
)和 TP2
(连接至
GND
)两个测试点的电压数据,同时仍然将核心板与另外一款 UINIO
系列开源硬件 UINIO-USB-UART
串口调试器 ,参照下面的示意图相互进行连接:
即 UINIO-MCU-GD32F350RBT6 核心板的
GPIOA9
和 GPIOA10
引脚,分别连接至
UINIO-USB-UART 串口调试器的 RXD
和
TXD
引脚,然后将后者的 Type-C 接口通过 USB
线缆连接到计算机,从而借助串口调试助手软件 COMTransmit
查看 ADC 采集到的数据信息。
完整 Keil µVision 工程代码
本实验通过将 UINIO-MCU-GD32F350RBT6 核心板的
GPIOC1 作为 ADC 采样引脚,分别去获取核心板上
3V3
和 GND
引脚的电压数据,并且通过 USART
串口将这些数据打印出来。实现这个功能,需要遵循如下一系列的步骤去配置 ADC
外设:
- 使用
rcu_periph_clock_enable()
使能 GPIO 和 ADC 外设时钟。 - 通过
rcu_adc_clock_config()
配置 ADC 时钟。 - 通过
gpio_mode_set()
配置 GPIO 引脚为模拟输入模式。 - 配置 ADC 的特殊功能
adc_special_function_config()
、数据对齐方式adc_data_alignment_config()
、分辨率adc_resolution_config()
、通道长度adc_channel_length_config()
。 - 配置 ADC 通道的触发源
adc_external_trigger_source_config()
。 - 使能 ADC
的触发方式是软件触发还是外部触发
adc_external/software_trigger_config()
。 - 调用
adc_enable()
使能 ADC 外设以及adc_calibration_enable()
使能校准功能 。 - 配置 ADC 规则通道组或者插入通道组
adc_regular/inserted_channel_config()
。 - 使能 ADC 外部触发或者软件触发功能
adc_external/software_trigger_enable()
。 - 持续判断通道组转换结束标志位
ADC_FLAG_EOC
,然后再通过adc_regular_data_read()
读取范围为0 ~ 4095
的 ADC 采样数据。
在上面的配置过程当中,adc_data_alignment_config()
函数所配置的数据对齐方式是指:
- 右对齐模式:ADC
采集到的数据被右对齐到最低位,不足的位数填充
0
,该模式可以在不损失精度的前提下,获得更好的动态范围。 - 左对齐模式:ADC
采集到的数据被左对齐到最高位,不足的位数填充
0
,虽然该模式可以提高采样的分辨率,但是会降低动态范围。
Drivers/ADC.h
1 | /*========== ADC.h ==========*/ |
Drivers/ADC.c
1 | /*========== ADC.c ==========*/ |
Sources/main.c
1 | /*========== main.c ==========*/ |
I2C 集成电路总线
SPI 串行外设总线
兆易创新 UINIO-MCU-GD32F350 固件库开发指南