[参考设计] GPIO通用输入输出

[复制链接]
62|0
晓伍 发表于 2026-8-3 18:16 | 显示全部楼层 |阅读模式
一、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

您需要登录后才可以回帖 登录 | 注册

本版积分规则

211

主题

4765

帖子

1

粉丝
快速回复 在线客服 返回列表 返回顶部