一、GPIO 输入输出口简介
GPIO(General Purpose Input/Output)是一种可编程的数字信号引脚,可以通过软件配置为输入模式或输出模式,用于与外部电路进行数字信号交互。简单来说,它可以:
- **作为输出**:控制 LED 亮灭、驱动继电器、产生 PWM 信号等。
- **作为输入**:读取按键状态、检测传感器信号、接收数字数据等。
GD32F30x 系列每个 GPIO 口(如 GPIOA、GPIOB)最多包含 16 个引脚(PIN0~PIN15),每个引脚可独立配置模式和频率。
二、GPIO 的主要工作模式
| 模式 | 方向 | 典型应用 | 特点 |
|---|---|---|---|
| **推挽输出** | 输出 | 驱动 LED、电机控制 | 高低电平均可主动驱动,驱动能力强;内部由一对互补 MOSFET 构成,输出低电平时 N-MOS 导通到 GND,输出高电平时 P-MOS 导通到 VDD。 |
| **开漏输出** | 输出 | I²C 总线、多设备共享、电平转换 | 只能主动拉低,不能主动拉高;必须外接上拉电阻才能输出高电平。多个设备可共用一根线与逻辑,适合线与场景。 |
| **浮空输入** | 输入 | 按键检测(需外接上拉或下拉) | 引脚电平完全由外部电路决定,内部无上拉/下拉;外部引脚悬空时电平不确定,容易受干扰,因此实际使用时通常需要外接上拉或下拉电阻来确定默认电平。 |
| **上拉输入** | 输入 | 按键检测(低电平有效) | 内部上拉电阻将引脚默认拉高;未连接外部信号时读入为高电平,按下按键接地时读入低电平。 |
| **下拉输入** | 输入 | 按键检测(高电平有效) | 内部下拉电阻将引脚默认拉低;未连接外部信号时读入为低电平,按下按键接 VDD 时读入高电平。 |
| **模拟输入** | 输入 | ADC 采样 | 引脚连接到内部 ADC 模块,可读取连续变化的模拟电压;此时施密特触发器关闭,不消耗数字输入电流。 |
| **复用功能** | 特殊 | UART、SPI 、I²C、CAN | 引脚由内部外设模块接管,可配置为复用推挽或复用开漏输出;常用于通信接口、定时器捕获/比较等功能。 |
三、软件架构
基于 GD32 标准外设库的嵌入式软件通常采用分层架构,各层职责清晰、便于移植和维护:
┌─────────────────────────────────┐
│ 应用层 (Application) │ main.c — 业务逻辑、流水灯调度
├─────────────────────────────────┤
│ 中间件/服务层 (Service) │ 延时、调度器、协议栈(可选)
├─────────────────────────────────┤
│ 驱动层 (Driver) │ led_driver — 硬件抽象、GPIO 封装
├─────────────────────────────────┤
│ HAL / 标准外设库 (Firmware) │ gd32f30x_xxx — 寄存器级接口
├─────────────────────────────────┤
│ 硬件层 (Hardware) │ GD32F30x 芯片、LED、按键
└─────────────────────────────────┘
**各层说明:
- **硬件层**:物理芯片与外部电路,包括 GD32F30x MCU、LED 灯、按键、晶振、电源等。
- **HAL / 标准外设库层**:GD32 官方提供的外设驱动库(如 `gd32f30x_gpio.h`、`gd32f30x_rcu.h`),封装了对寄存器的操作,提供统一的 API 接口(如 `gpio_init()`、`rcu_periph_clock_enable()`)。
- **驱动层**:对具体硬件模块的抽象封装,如 `led_driver.c/.h`,将 GPIO 操作封装为 `LedDrvInit()`、`TurnOnLed()` 等语义化接口,使得应用层不直接接触 GPIO 寄存器。
- **中间件/服务层**:提供通用服务,如软件延时、系统滴答(SysTick)定时调度、任务轮询等。简单项目可能直接由应用层调用。
- **应用层**:`main.c` 中的业务逻辑。调用驱动层接口实现功能,如跑马灯效果、按键响应、通信控制等。
**分层设计的优势:** 更换硬件平台时只需修改驱动层,应用层代码无需变动;驱动层可跨项目复用;单元测试可在驱动层层面单独进行。
四、时钟树
4.1 总线架构
ARM Cortex-M4 内核的总线矩阵中,与存储器访问直接相关的有三条总线接口:
- **I-CODE 总线(指令总线)**:连接 Flash 存储器,用于取指令(代码执行)。CPU 通过 I-CODE 总线从 Flash 中高速读取指令。
- **D-CODE 总线(数据总线)**:连接 Flash 存储器,用于读取常量数据(如 const 修饰的数组、字符串字面量),避免数据访问与指令取指争抢同一总线。
- **系统总线(System Bus)**:连接 SRAM(内存)和 AHB 总线矩阵,通过 AHB 总线矩阵进一步访问 APB1、APB2 上的片上外设。
GD32F30x 内部包含多条总线,外设挂载在不同总线上:
| 总线 | 时钟频率 | 挂载外设举例 |
|---|---|---|
| **AHB** | 最高 120MHz(HCLK) | 系统总线矩阵、DMA、SDIO、FMC |
| **APB2** | 最高 60MHz(PCLK2) | GPIOA~GPIOF、USART0、SPI0、ADC、TIMER0/7/8 |
| **APB1** | 最高 30MHz(PCLK1) | USART1/2、SPI1/2、I2C、TIMER1~6、RTC、CAN |
在实际编程中,使能某外设时钟前必须清楚它属于哪条总线,调用对应的时钟使能函数。例如 GPIOA 在 APB2 上,通过 `rcu_periph_clock_enable(RCU_GPIOA)` 使能(GD32 库内部已封装总线选择逻辑)。
4.2 时钟源
GD32F30x 提供多种时钟源,可根据精度、功耗、成本需求选择:
| 时钟源 | 类型 | 频率 | 精度 | 典型用途 |
|---|---|---|---|---|
| **HSI**(高速内部 RC 振荡器) | 内部 | 8 MHz | 约 ±1%(典型),温漂较大 | 系统启动默认时钟;简单应用如驱动数码管、LED、继电器等对时序精度要求不高的场景 |
| **HSE**(高速外部晶振) | 外部 | 4~32 MHz(常用 8 MHz) | 约 ±10~30 ppm,精度高 | 需要精确波特率的通信场景(UART、SPI、CAN),经 PLL 倍频后作为系统主时钟 |
| **LSI**(低速内部 RC 振荡器) | 内部 | 约 40 kHz | 精度低 | 独立看门狗(IWDG)、简单定时唤醒 |
| **LSE**(低速外部晶振) | 外部 | 32.768 kHz | 高精度 | RTC 实时时钟,提供精确的秒脉冲基准
**选型对比:**
- 内部晶振(HSI)无需额外元器件,PCB 面积小、BOM 成本低,适用于对时序精度要求不高的场合(数码管显示、LED 闪烁、继电器控制、简单的单片机控制逻辑)。
- 外部晶振(HSE)精度高、温漂小,是产品需要和外部通信(如串口、SPI、CAN、USB 等)时必不可少的时钟源——因为这些协议对波特率和时序容差要求严格,内部 RC 振荡器的精度无法满足。
- RTC 实时时钟通常必须使用 **LSE(32.768 kHz 低速外部晶振)**,因为 32.768 kHz 恰好等于 2¹⁵ Hz,经过 15 级分频后精确得到 1 Hz(每秒一次),且外部晶振 ppm 级精度可保证时钟长期误差在可接受范围内。
4.3 时钟信号路径(简化)
┌─────────┐
HSE ───▶│ │
8MHz │ PLL │──▶ PLLCLK(最高 120MHz)
HSI ───▶│ │
(8MHz) └─────────┘
│
▼
┌──────────────┐
│ 系统时钟 MUX │──▶ SYSCLK(HCLK)
└──────────────┘
│
┌────────┼────────┐
▼ ▼ ▼
AHB APB2 APB1
(HCLK) (PCLK2) (PCLK1)
系统上电后默认使用 HSI(8 MHz),用户可根据需要在初始化时将系统时钟切换至 HSE+PLL 以获得更高频率和更好精度。
五、系统时钟配置及 CK_OUT 输出
`CK_OUT0` 是 GD32 的时钟输出功能,可将内部时钟信号引出到指定引脚上,用于调试测量或为外部芯片提供时钟。
```c
/* 使能 GPIOA 时钟(GPIOA 挂载在 APB2 总线上)*/
rcu_periph_clock_enable(RCU_GPIOA);
/* GPIOA_8 配置为复用推挽输出,驱动频率 50MHz */
gpio_init(GPIOA, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_8);
/* 将系统时钟(SYSCLK)输出到 CK_OUT0,从 PA8 引脚引出波形 */
/* 使用示波器在 PA8 上即可观测当前系统时钟频率 */
rcu_ckout0_config(RCU_CKOUT0SRC_CKSYS);
**用途:** 在开发调试阶段,通过 PA8 引脚引出系统时钟波形,用示波器/逻辑分析仪测量频率,可以快速验证时钟配置是否正确(如 PLL 倍频后的频率是否达到预期值 120MHz)。
**CK_OUT0 可选择的时钟源包括:** 系统时钟(SYSCLK)、HSI、HSE、PLL 时钟等,通过 `rcu_ckout0_config()` 的参数切换
六、流水灯驱动代码(完整版)
## 6.1 驱动头文件 `led_driver.h`
```c
#ifndef __LED_DRIVER_H__
#define __LED_DRIVER_H__
#include <stdint.h>
#include "gd32f30x.h"
/* LED 逻辑标号,方便应用层调用 */
#define LED1 0
#define LED2 1
#define LED3 2
/**
***********************************************************
* @brief LED 硬件初始化(使能 GPIO 时钟、配置引脚为推挽输出)
* @param 无
* @return 无
***********************************************************
*/
void LedDrvInit(void);
/**
***********************************************************
* @brief 点亮指定 LED
* @param ledNo LED 标号,取值范围 0 ~ 2
* @return 无;若 ledNo 超出范围则直接返回
***********************************************************
*/
void TurnOnLed(uint8_t ledNo);
/**
***********************************************************
* @brief 熄灭指定 LED
* @param ledNo LED 标号,取值范围 0 ~ 2
* @return 无;若 ledNo 超出范围则直接返回
***********************************************************
*/
void TurnOffLed(uint8_t ledNo);
/**
***********************************************************
* @brief 翻转指定 LED 状态(亮→灭 或 灭→亮)
* @param ledNo LED 标号,取值范围 0 ~ 2
* @return 无;若 ledNo 超出范围则直接返回
***********************************************************
*/
void ToggleLed(uint8_t ledNo);
/**
***********************************************************
* @brief 获取指定 LED 当前状态
* @param ledNo LED 标号,取值范围 0 ~ 2
* @return SET(亮) 或 RESET(灭);若 ledNo 超范围则行为未定义
***********************************************************
*/
FlagStatus GetLedState(uint8_t ledNo);
#endif /* __LED_DRIVER_H__ */
```
## 6.2 驱动源文件 `led_driver.c`
```c
#include <stdint.h>
#include "gd32f30x.h"
#include "led_driver.h"
/* LED 硬件配置结构体:将 GPIO 端口、引脚、时钟绑定在一起 */
typedef struct
{
rcu_periph_enum rcu; /* 该 LED 所在 GPIO 端口的时钟 */
uint32_t port; /* GPIO 端口基地址,如 GPIOA、GPIOE */
uint32_t pin; /* 引脚号,如 GPIO_PIN_8 */
} LedHwCfg_t;
/* LED 硬件配置表:按 LED 标号 0、1、2 依次定义 */
static const LedHwCfg_t g_ledCfgList[] =
{
{RCU_GPIOA, GPIOA, GPIO_PIN_8}, /* LED1 — PA8 */
{RCU_GPIOE, GPIOE, GPIO_PIN_6}, /* LED2 — PE6 */
{RCU_GPIOF, GPIOF, GPIO_PIN_6} /* LED3 — PF6 */
};
/* 通过宏自动计算 LED 数量,避免手动维护常量 */
#define LED_NUM_MAX (sizeof(g_ledCfgList) / sizeof(LedHwCfg_t))
/**
***********************************************************
* @brief LED 硬件初始化
* @NOTE 遍历配置表,依次使能各 GPIO 端口时钟,
* 并将引脚配置为推挽输出模式,速度 2MHz
* @param 无
* @return 无
***********************************************************
*/
void LedDrvInit(void)
{
for (uint8_t i = 0; i < LED_NUM_MAX; i++)
{
/* 使能该 LED 所在的 GPIO 端口时钟 */
rcu_periph_clock_enable(g_ledCfgList.rcu);
/* 初始化引脚:推挽输出、最大翻转速率 2MHz */
gpio_init(g_ledCfgList.port,
GPIO_MODE_OUT_PP,
GPIO_OSPEED_2MHZ,
g_ledCfgList.pin);
}
}
/**
***********************************************************
* @brief 点亮指定 LED
* @param ledNo LED 标号,取值范围 0 ~ (LED_NUM_MAX - 1)
* @return 无
***********************************************************
*/
void TurnOnLed(uint8_t ledNo)
{
if (ledNo >= LED_NUM_MAX)
{
return;
}
/* SET 将引脚置高电平,LED 阳极接 GPIO、阴极接地时 LED 点亮 */
gpio_bit_write(g_ledCfgList[ledNo].port,
g_ledCfgList[ledNo].pin,
SET);
}
/**
***********************************************************
* @brief 熄灭指定 LED
* @param ledNo LED 标号,取值范围 0 ~ (LED_NUM_MAX - 1)
* @return 无
***********************************************************
*/
void TurnOffLed(uint8_t ledNo)
{
if (ledNo >= LED_NUM_MAX)
{
return;
}
/* RESET 将引脚置低电平,LED 熄灭 */
gpio_bit_write(g_ledCfgList[ledNo].port,
g_ledCfgList[ledNo].pin,
RESET);
}
/**
***********************************************************
* @brief 翻转指定 LED 状态
* @param ledNo LED 标号,取值范围 0 ~ (LED_NUM_MAX - 1)
* @return 无
***********************************************************
*/
void ToggleLed(uint8_t ledNo)
{
if (ledNo >= LED_NUM_MAX)
{
return;
}
/* 先读取当前输出电平状态,再写入相反电平 */
FlagStatus status = gpio_output_bit_get(g_ledCfgList[ledNo].port,
g_ledCfgList[ledNo].pin);
gpio_bit_write(g_ledCfgList[ledNo].port,
g_ledCfgList[ledNo].pin,
(bit_status)(!status));
}
/**
***********************************************************
* @brief 获取指定 LED 当前状态
* @param ledNo LED 标号,取值范围 0 ~ (LED_NUM_MAX - 1)
* @return SET(亮 / 高电平) 或 RESET(灭 / 低电平)
* @note 调用方应确保 ledNo 在有效范围内
***********************************************************
*/
FlagStatus GetLedState(uint8_t ledNo)
{
return gpio_output_bit_get(g_ledCfgList[ledNo].port,
g_ledCfgList[ledNo].pin);
}
```
**关键代码说明:**
- **配置表驱动设计**:`g_ledCfgList[]` 将每颗 LED 的硬件信息(时钟、端口、引脚)集中管理。新增或更换 LED 只需修改此数组,驱动函数代码无需变动——这是嵌入式驱动设计中常用的表驱动模式。
- **GPIO 速度选择**:`GPIO_OSPEED_2MHZ` 用于 LED 控制足够,更高的速度(如 50MHz)会增加 EMI 噪声和功耗,在没有高频翻转需求时选择较低速度。
- **边界检查**:每个操作函数在访问数组前都检查 `ledNo >= LED_NUM_MAX`,防止数组越界导致未定义行为,这是嵌入式代码中必要的防御性编程实践。
---
## 6.3 应用层示例 `main.c`
```c
#include "gd32f30x.h"
#include "led_driver.h"
#include "systick.h"
/* 流水灯方向标志 */
static uint8_t g_currentLed = LED1;
static uint8_t g_runDirection = 0; /* 0: 递增方向, 1: 递减方向 */
/**
***********************************************************
* @brief 系统时钟初始化
* @note 使用外部 8MHz HSE + PLL 倍频至 120MHz
***********************************************************
*/
static void SystemClockInit(void)
{
/* 使能 HSE */
rcu_osci_on(RCU_HXTAL);
rcu_osci_stab_wait(RCU_HXTAL);
/* 配置 PLL: HSE * 15 = 8MHz * 15 = 120MHz */
rcu_pll_config(RCU_PLLSRC_HXTAL, RCU_PLL_MUL15);
/* 使能 PLL */
rcu_osci_on(RCU_PLL);
rcu_osci_stab_wait(RCU_PLL);
/* 切换系统时钟源为 PLL */
rcu_system_clock_source_config(RCU_SCS_PLL);
/* 配置 AHB、APB1、APB2 分频系数 */
rcu_ahb_clock_config(RCU_AHB_CKSYS_DIV1); /* HCLK = 120MHz */
rcu_apb1_clock_config(RCU_APB1_HCLK_DIV4); /* PCLK1 = 30MHz */
rcu_apb2_clock_config(RCU_APB2_HCLK_DIV2); /* PCLK2 = 60MHz */
}
/**
***********************************************************
* @brief 系统滴答初始化(1ms 时基)
***********************************************************
*/
static void SysTickInit(void)
{
/* SysTick 时钟源 = HCLK = 120MHz,装载值使每秒产生 1000 次中断 */
if (SysTick_Config(SystemCoreClock / 1000U))
{
/* 装载值超出 24 位范围时会返回 1,此处为异常分支 */
while (1)
{
}
}
NVIC_SetPriority(SysTick_IRQn, 0);
}
/**
***********************************************************
* @brief 简单软件延时(阻塞式,单位 ms)
* @param ms 延时时间,单位毫秒
* @note 依赖 1ms SysTick 时基
***********************************************************
*/
static void DelayMs(uint32_t ms)
{
uint32_t start = g_sysTickCount; /* 全局滴答计数器,在 SysTick_Handler 中递增 */
while ((g_sysTickCount - start) < ms)
{
/* 空循环等待,或可调用 __WFI() 进入低功耗 */
}
}
/**
***********************************************************
* @brief 流水灯主程序
* @note 三颗 LED 依次点亮,形成跑马灯效果;到达两端后反向
***********************************************************
*/
int main(void)
{
/* 1. 系统时钟初始化:HSE + PLL → 120MHz */
SystemClockInit();
/* 2. SysTick 初始化(1ms 时基) */
SysTickInit();
/* 3. LED 驱动初始化 */
LedDrvInit();
/* 4. 全部 LED 先熄灭 */
for (uint8_t i = 0; i < LED_NUM_MAX; i++)
{
TurnOffLed(i);
}
/* 5. 流水灯循环 */
while (1)
{
/* 熄灭当前 LED */
TurnOffLed(g_currentLed);
/* 根据方向移动到下一颗 LED */
if (g_runDirection == 0)
{
/* 正向递增 */
g_currentLed++;
if (g_currentLed >= LED_NUM_MAX)
{
g_currentLed = LED_NUM_MAX - 2; /* 倒数第二颗 */
g_runDirection = 1; /* 切换为递减方向 */
}
}
else
{
/* 反向递减 */
if (g_currentLed == 0)
{
g_currentLed = 1; /* 第二颗 */
g_runDirection = 0; /* 切换为递增方向 */
}
else
{
g_currentLed--;
}
}
/* 点亮新位置的 LED */
TurnOnLed(g_currentLed);
/* 延时 500ms,控制流水灯速度 */
DelayMs(500);
}
}
```
**流水灯逻辑说明:**
- 初始化后先熄灭全部 LED,避免上电瞬间出现不确定的闪烁。
- 主循环中先熄灭当前 LED,再根据方向变量计算下一位置,然后点亮新 LED——形成"灭→移→亮"的效果。
- 方向在到达边界时翻转,形成来回跑动(乒乓灯)效果,比单向循环更生动。
- DelayMs(500) 控制每次切换间隔,可调节此值改变流水灯速度(200ms 更快,800ms 更慢)。
---
# 七、扩展阅读建议
1. **按键消抖与状态机**:将 GPIO 输入与定时器结合,实现可靠的按键检测。
2. **PWM 呼吸灯**:使用定时器输出比较功能,通过改变 PWM 占空比实现 LED 渐亮渐灭效果。
3. **中断驱动防阻塞**:将 DelayMs 改为 SysTick 中断 + 状态机,避免 `while(1)` 中占用 CPU 做空等待。
4. **RTOS 移植**:在 FreeRTOS 等系统下,将流水灯改为独立任务,通过 `vTaskDelay()` 实现非阻塞延时。
————————————————
版权声明:本文为CSDN博主「原来是小志同学呀」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/2402_88673453/article/details/161601498
|
|