3. GPIO 基础与寄存器操作:模式配置、推挽与开漏、寄存器级编程实战

GPIO 是嵌入式开发里最基础、也最绕不开的外设。说白了,它就是芯片和外界打交道的那双脚——要么往外送信号,要么往里收信号。CH32V203 的 GPIO 模块继承自 RISC-V 内核的通用设计,但又有一些自己的脾气。今天我就带你把它摸透。

3.1 GPIO 模式配置:你得知道这四种状态

CH32V203 的每个 GPIO 引脚都可以独立配置成四种基本模式。我刚开始用的时候,总觉得模式多反而容易搞混,后来发现其实就两类:输出和输入,每种再分两种。

模式 方向 典型用途 内部结构特点
推挽输出 输出 驱动 LED、普通数字信号 PMOS + NMOS 交替导通
开漏输出 输出 I²C 总线、多设备共享线 只有 NMOS,高阻需外接上拉
浮空输入 输入 外部电平检测(无上下拉) 高阻态,电平不确定
上拉/下拉输入 输入 按键检测、电平锁定 内部弱上拉/弱下拉电阻

嗯,这里要注意:浮空输入在引脚悬空时,电平是随机跳变的。我在一个温控项目里吃过这个亏——按键没按下,程序却以为按下了。后来换成上拉输入,问题立刻解决。

3.2 推挽输出 vs 开漏输出:到底差在哪?

这两个概念,很多初学者容易混淆。我换个说法你就明白了。

推挽输出,就像两个开关轮流工作:一个负责把电平拉到 VCC,一个负责拉到 GND。输出高电平时,PMOS 导通,NMOS 关断;输出低电平时反过来。所以推挽输出的驱动能力强,信号完整。

开漏输出,只保留了下拉的那个 NMOS 管。输出低电平时,NMOS 导通,引脚拉到 GND;输出高电平时,NMOS 关断,引脚变成高阻态。这时候如果没有外部上拉电阻,引脚电平就是悬空的。

核心区别一句话:推挽能主动推高和拉低,开漏只能主动拉低,高电平靠外部电阻拉上去。

我个人习惯在 I²C 总线上用开漏输出,因为多个设备可以共用一根线,谁拉低谁说话,不会打架。而驱动 LED 或者继电器,我肯定用推挽,省事又可靠。

3.3 寄存器级编程实战:直接操作才是硬道理

CH32V203 的 GPIO 寄存器不多,但每个都很关键。我列几个最常用的:

  • GPIOx_CFGLR / GPIOx_CFGHR:配置模式(每 4 位控制一个引脚)
  • GPIOx_INDR:输入数据寄存器(只读)
  • GPIOx_OUTDR:输出数据寄存器(可读可写)
  • GPIOx_BSHR:位设置/清除寄存器(写 1 有效)
  • GPIOx_BCR:位清除寄存器(写 1 清零)

你想想看,用库函数固然方便,但一旦遇到时序敏感或者资源受限的场景,寄存器操作才是王道。我曾经在一个电机控制项目里,因为库函数多了一层封装,导致 PWM 信号延迟了几个微秒,电机抖动得厉害。换成直接写寄存器,问题立刻消失。

3.3.1 配置推挽输出:点亮一个 LED

假设我们要把 PA0 配置成推挽输出,驱动一个 LED。代码很简单:

// 开启 GPIOA 时钟(RCC 寄存器操作)
RCC->APB2PCENR |= RCC_APB2Periph_GPIOA;

// 配置 PA0 为推挽输出,50MHz 速度
// CFGLR 每 4 位控制一个引脚,PA0 对应 bit[3:0]
// 模式:0011(输出,50MHz),配置:00(推挽)
GPIOA->CFGLR &= ~(0xF << 0);   // 先清零
GPIOA->CFGLR |= (0x3 << 0);    // 设置模式

// 输出高电平,点亮 LED
GPIOA->BSHR = (1 << 0);        // 写 1 置位
// 或者直接写 OUTDR
// GPIOA->OUTDR |= (1 << 0);

小技巧:用 BSHR 寄存器操作单个引脚,比直接读写 OUTDR 更安全。因为 BSHR 是原子操作,不会影响其他引脚的状态。我在多线程中断里一直用这个方式。

3.3.2 配置开漏输出:模拟 I²C 的 SDA 线

开漏输出的配置和推挽几乎一样,只是 CFGLR 的配置位不同:

// 配置 PB1 为开漏输出
// 模式:0011(输出,50MHz),配置:01(开漏)
GPIOB->CFGLR &= ~(0xF << 4);   // PB1 对应 bit[7:4]
GPIOB->CFGLR |= (0x7 << 4);    // 0b0111 = 开漏输出

// 输出低电平(主动拉低)
GPIOB->BCR = (1 << 1);         // 写 1 清零

// 输出高电平(释放总线,靠外部上拉)
GPIOB->BSHR = (1 << 1);        // 写 1 置位,实际引脚变高阻

注意看,开漏输出时,BSHR 写 1 并不会真的把引脚拉到 VCC,而是让引脚变成高阻态。外部上拉电阻会把电平拉高。这个细节我当年调试 I²C 时卡了半天,后来用示波器一看才明白。

3.3.3 配置输入模式:读取按键状态

按键检测一般用上拉输入,省掉外部电阻:

// 配置 PC13 为上拉输入
// 模式:1000(输入,上拉/下拉),配置:10(上拉)
GPIOC->CFGLR &= ~(0xF << 20);  // PC13 对应 bit[23:20]
GPIOC->CFGLR |= (0x8 << 20);   // 输入模式

// 使能内部上拉(通过 OUTDR 控制上拉/下拉)
GPIOC->OUTDR |= (1 << 13);     // 写 1 使能上拉

// 读取按键状态
uint8_t key_state = (GPIOC->INDR >> 13) & 0x01;

避坑指南:我曾经在读取按键时直接用了 INDR 的值做判断,结果因为没做消抖,一次按下触发了十几次中断。后来加了 10ms 延时消抖,才稳定下来。寄存器操作再快,也扛不住机械抖动。

3.4 知识结构图:GPIO 操作全景

下面这张图把 GPIO 的配置、操作和典型场景串起来了,你可以对照着看。

CH32V203 GPIO 操作全景图 CH32V203 GPIO 操作全景图 GPIO 核心 输出模式 推挽输出 开漏输出 输入模式 浮空输入 上拉/下拉输入 寄存器操作:CFGLR/CFGHR → INDR/OUTDR → BSHR/BCR 配置模式 → 读取/写入数据 → 位操作(原子操作) LED 驱动 / 继电器 I²C 总线 / 按键检测

3.5 实战要点总结

  • 配置前先开时钟:RCC 寄存器不使能,GPIO 寄存器写不进去。这个顺序我见过太多人搞反。
  • CFGLR/CFGHR 每 4 位控制一个引脚:低 2 位是模式,高 2 位是配置。别搞混了位偏移。
  • BSHR 写 1 置位,BCR 写 1 清零:这两个寄存器写 0 无效,所以不用担心误操作其他引脚。
  • 开漏输出必须外接上拉电阻:内部上拉太弱,驱动能力不够。I²C 标准里上拉电阻一般是 4.7kΩ。
  • 输入模式的上拉/下拉由 OUTDR 控制:OUTDR 写 1 使能上拉,写 0 使能下拉。这个设计有点反直觉,但用熟了就好。

我的习惯:每次写 GPIO 配置前,先画一个引脚功能表,把每个引脚的模式、初始电平、用途列清楚。这样写代码时不会乱,调试时也容易定位问题。