2. RISC-V基础

GPIO,说白了就是芯片的「手脚」。你要让LED亮、让蜂鸣器响、让继电器动作,都得靠它。这一章我们就把CH32V203的GPIO输出功能彻底讲透——从硬件原理到寄存器操作,再到库函数封装,最后用两个实战项目收尾。

本章核心脉络:GPIO模块内部结构 → 寄存器映射关系 → 库函数设计思路 → 点亮LED → 流水灯实现

2.1 GPIO模块原理

CH32V203的GPIO模块,内部其实挺复杂的。我刚开始用RISC-V单片机时,以为GPIO就是简单的高低电平输出,后来踩过坑才明白——输出模式、驱动能力、复用功能,每个细节都可能让你调半天。

每个GPIO端口(如PA、PB、PC)包含16个引脚。每个引脚都可以独立配置为:

  • 输入模式:浮空、上拉、下拉、模拟
  • 输出模式:推挽、开漏
  • 复用功能:AFIO映射到外设
  • 特殊模式:如外部中断线

输出模式下,关键参数有两个:

参数 选项 适用场景
输出类型 推挽 / 开漏 推挽驱动LED;开漏用于I²C等总线
输出速度 2MHz / 10MHz / 50MHz 普通LED用2MHz足够;SPI等高速用50MHz

嗯,这里要注意:CH32V203的GPIO输出速度不是越高越好。速度越高,功耗和电磁干扰越大。我见过有人用50MHz去驱动一个LED,结果LED没亮,倒是把旁边的ADC采样干扰得一塌糊涂。

2.2 寄存器映射

CH32V203的GPIO寄存器,每个端口占用0x400个地址空间。以GPIOA为例,基地址是0x40010800。常用的寄存器就那么几个:

偏移地址 寄存器 功能 复位值
0x00 CFGLR 配置低8位引脚模式 0x44444444
0x04 CFGHR 配置高8位引脚模式 0x44444444
0x0C OUTDR 输出数据寄存器 0x00000000
0x10 BSHR 位设置/清除寄存器 0x00000000
0x14 BCR 位清除寄存器 0x00000000

我个人习惯用BSHR和BCR来控制单个引脚,而不是直接写OUTDR。为什么?因为BSHR是「原子操作」——你写1到低16位就置位对应引脚,写1到高16位就复位对应引脚。这样在多任务环境下不会出现「读-改-写」的竞争问题。

小技巧:配置CFGLR/CFGHR时,每个引脚占用4位。模式位[3:2]控制输入/输出,配置位[1:0]控制上拉/下拉/速度。比如0x3表示推挽输出50MHz,0xB表示复用推挽输出50MHz。

2.3 库函数封装

直接操作寄存器虽然快,但可读性差。我习惯封装一层库函数,把常用的操作抽象出来。下面是我在项目中常用的GPIO输出接口:

// gpio.h
#ifndef __GPIO_H
#define __GPIO_H

#include "ch32v20x.h"

typedef enum {
    GPIO_MODE_INPUT  = 0x4,  // 输入模式
    GPIO_MODE_OUT_PP = 0x3,  // 推挽输出
    GPIO_MODE_OUT_OD = 0x7   // 开漏输出
} GPIO_Mode;

typedef enum {
    GPIO_SPEED_2MHz  = 0x2,
    GPIO_SPEED_10MHz = 0x1,
    GPIO_SPEED_50MHz = 0x3
} GPIO_Speed;

void GPIO_Init(GPIO_TypeDef* GPIOx, uint16_t pin, GPIO_Mode mode, GPIO_Speed speed);
void GPIO_SetPin(GPIO_TypeDef* GPIOx, uint16_t pin);
void GPIO_ResetPin(GPIO_TypeDef* GPIOx, uint16_t pin);
void GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t pin);

#endif

实现的时候,核心就是操作CFGLR/CFGHR和BSHR/BCR。以初始化函数为例:

void GPIO_Init(GPIO_TypeDef* GPIOx, uint16_t pin, GPIO_Mode mode, GPIO_Speed speed) {
    uint32_t reg_offset = (pin < 8) ? 0x00 : 0x04;  // CFGLR or CFGHR
    uint32_t bit_pos = (pin & 0x07) * 4;             // 每个引脚4位
    uint32_t* cfg_reg = (uint32_t*)((uint32_t)GPIOx + reg_offset);
    
    // 清除该引脚配置位
    *cfg_reg &= ~(0xF << bit_pos);
    // 写入新模式
    *cfg_reg |= ((mode & 0x0F) | (speed & 0x0C)) << bit_pos;
}

你看,其实就是移位和掩码操作。但封装之后,调用起来就清爽多了:

GPIO_Init(GPIOA, GPIO_Pin_0, GPIO_MODE_OUT_PP, GPIO_SPEED_2MHz);
GPIO_SetPin(GPIOA, GPIO_Pin_0);  // PA0输出高电平

避坑指南:我曾经在初始化时忘记使能GPIO时钟,结果寄存器怎么写都没反应。CH32V203的GPIO时钟由RCC模块控制,必须先调用RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE)。

2.4 点亮LED实战

好,理论讲完了,咱们直接上手。假设LED接在PA0上,阳极通过电阻连到VCC,阴极连到PA0——也就是低电平点亮。完整代码:

#include "ch32v20x.h"

void Delay(uint32_t count) {
    while(count--);
}

int main(void) {
    // 1. 使能GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 2. 配置PA0为推挽输出,2MHz
    GPIO_Init(GPIOA, GPIO_Pin_0, GPIO_MODE_OUT_PP, GPIO_SPEED_2MHz);
    
    // 3. 输出低电平,点亮LED
    GPIO_ResetPin(GPIOA, GPIO_Pin_0);
    
    while(1) {
        // 主循环空转,LED常亮
    }
}

就这么简单?嗯,核心就三步:开时钟、配模式、写电平。但实际项目中,我建议你加上延时让LED闪烁一下,确认硬件没问题:

while(1) {
    GPIO_ResetPin(GPIOA, GPIO_Pin_0);  // 亮
    Delay(0xFFFFF);
    GPIO_SetPin(GPIOA, GPIO_Pin_0);    // 灭
    Delay(0xFFFFF);
}

2.5 流水灯实现

流水灯是GPIO输出的经典练习。假设8个LED接在PA0~PA7上,我们要让它们依次点亮。实现思路有两种:

  • 移位法:用一个变量做左移,每次取反后写入OUTDR
  • 查表法:预定义好每个状态的输出值,循环查表

我个人更喜欢查表法,因为代码更直观,也方便调整流水模式。来看实现:

// 流水灯模式表:低电平点亮
const uint16_t led_pattern[] = {
    0x00FE,  // PA0亮
    0x00FD,  // PA1亮
    0x00FB,  // PA2亮
    0x00F7,  // PA3亮
    0x00EF,  // PA4亮
    0x00DF,  // PA5亮
    0x00BF,  // PA6亮
    0x007F   // PA7亮
};

int main(void) {
    uint8_t i;
    
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    
    // 配置PA0~PA7为推挽输出
    for(i = 0; i < 8; i++) {
        GPIO_Init(GPIOA, GPIO_Pin_0 << i, GPIO_MODE_OUT_PP, GPIO_SPEED_2MHz);
    }
    
    while(1) {
        for(i = 0; i < 8; i++) {
            // 直接写OUTDR,低8位对应PA0~PA7
            GPIOA->OUTDR = led_pattern[i];
            Delay(0xFFFFF);
        }
    }
}

你想想看,如果不用查表法,用移位操作怎么写?大概是这样:

uint16_t led = 0x01;  // 初始PA0亮
while(1) {
    GPIOA->OUTDR = ~led;  // 取反是因为低电平点亮
    led <<= 1;
    if(led == 0x100) led = 0x01;  // 循环
    Delay(0xFFFFF);
}

两种方法都可以。但查表法更容易扩展——比如你想实现「来回流水」或者「跳跃流水」,改表就行,不用动逻辑。

关键总结:GPIO输出控制的核心就三件事——时钟使能、模式配置、电平操作。寄存器层面记住CFGLR/CFGHR配模式,BSHR/BCR写电平。库函数封装让代码更可读,但底层原理必须清楚,否则出了问题你都不知道从哪查起。

GPIO输出控制知识体系 GPIO输出控制 硬件原理 寄存器映射 库函数封装 推挽/开漏 输出速度 CFGLR/CFGHR BSHR/BCR 初始化函数 电平操作 点亮LED实战 流水灯实现 移位法 查表法 核心操作 时钟使能 模式配置 电平操作