3. 串口通信协议实现
GPIO,说白了就是芯片的「手脚」。你要让LED亮起来,或者检测按键有没有按下,都得靠它。这一章我们就把GD32V的GPIO彻底搞明白。
3.1 GPIO功能框图
先看整体结构。GD32V的GPIO模块内部其实挺复杂的,但我们可以把它拆成几个关键部分:
核心要点:GPIO的每个引脚,内部都有一套完整的「输入-输出」通路。你配置的每个寄存器,都在控制这条通路上的某个开关。
从图上你能看到,数据流其实分两条路:
- 输出路径:CPU写OCTL寄存器 → 输出驱动 → 引脚
- 输入路径:引脚电平 → 施密特触发器整形 → ISTAT寄存器 → CPU读取
嗯,这里要注意:施密特触发器的作用是消除信号抖动。我在做按键检测时就吃过亏——没开施密特,结果一个按键按下去,读出来十几个跳变。
3.2 寄存器映射
GD32V的GPIO寄存器,说白了就是一堆内存地址。每个GPIO端口(PA、PB、PC...)都占用一段连续的地址空间。
| 寄存器名称 | 偏移地址 | 功能描述 | 复位值 |
|---|---|---|---|
| CTL0 | 0x00 | 端口控制寄存器0(配置引脚0-7) | 0x44444444 |
| CTL1 | 0x04 | 端口控制寄存器1(配置引脚8-15) | 0x44444444 |
| ISTAT | 0x08 | 输入状态寄存器(只读) | 0x0000XXXX |
| OCTL | 0x0C | 输出控制寄存器(可读写) | 0x00000000 |
| BOP | 0x10 | 位操作寄存器(原子操作) | 0x00000000 |
| BC | 0x14 | 位清除寄存器 | 0x00000000 |
个人经验:我习惯用BOP寄存器来做LED翻转。它支持原子操作,写一次就能同时设置和清除多个引脚,比先读OCTL再修改再写回去快得多。在FreeRTOS任务里,这能减少临界区保护的麻烦。
每个引脚的控制寄存器CTLx,每4位配置一个引脚。这4位的含义如下:
| 位[3:2] | 模式 | 位[1:0] | 配置 |
|---|---|---|---|
| 00 | 输入模式 | 00 | 模拟输入 |
| 00 | 输入模式 | 01 | 浮空输入 |
| 00 | 输入模式 | 10 | 上拉/下拉输入 |
| 01 | 输出模式 | 00 | 推挽输出 |
| 01 | 输出模式 | 01 | 开漏输出 |
| 10 | 复用功能 | 00 | 复用推挽输出 |
| 10 | 复用功能 | 01 | 复用开漏输出 |
避坑指南:我曾经在项目里把LED引脚配成了开漏输出,结果灯死活不亮。后来查了半天才发现——开漏输出需要外部上拉电阻,而我的板子上没有。推挽输出才是LED的正确姿势。
3.3 点亮LED实战
好,理论说完了,咱们直接上手。假设LED接在PA0引脚,低电平点亮。
第一步,开启GPIOA的时钟。GD32V的GPIO挂在APB2总线上,对应的时钟使能位在RCU寄存器里。
/* 开启GPIOA时钟 */
RCU->APB2EN |= RCU_APB2EN_PAEN;
第二步,配置PA0为推挽输出,速度50MHz。
/* 配置PA0为推挽输出,50MHz */
GPIOA->CTL0 &= ~(0xF << 0); /* 先清除低4位 */
GPIOA->CTL0 |= (0x1 << 0); /* 01 00 = 推挽输出 */
第三步,点亮LED。
/* 点亮LED:PA0输出低电平 */
GPIOA->BC = (1 << 0); /* 位清除寄存器,写1清零 */
/* 熄灭LED:PA0输出高电平 */
GPIOA->BOP = (1 << 0); /* 位操作寄存器,写1置位 */
完整代码示例:
void led_init(void)
{
/* 1. 开启时钟 */
RCU->APB2EN |= RCU_APB2EN_PAEN;
/* 2. 配置模式 */
GPIOA->CTL0 &= ~(0xF << 0);
GPIOA->CTL0 |= (0x1 << 0); /* 推挽输出 */
/* 3. 默认熄灭 */
GPIOA->BOP = (1 << 0);
}
void led_on(void)
{
GPIOA->BC = (1 << 0);
}
void led_off(void)
{
GPIOA->BOP = (1 << 0);
}
你想想看,整个过程其实就是三步:开时钟、配模式、写数据。GD32V的寄存器操作比STM32还要直接一些,没有那么多封装层。
3.4 按键输入检测
按键检测比LED输出稍微复杂一点。关键点在于:去抖。
假设按键接在PA1,按下为低电平(外部上拉)。
/* 配置PA1为上拉输入 */
GPIOA->CTL0 &= ~(0xF << 4); /* 清除位[7:4] */
GPIOA->CTL0 |= (0x8 << 4); /* 1000 = 上拉/下拉输入模式 */
读取按键状态:
/* 读取PA1电平 */
uint8_t key_state = (GPIOA->ISTAT >> 1) & 0x01;
但直接这么读,你会遇到抖动问题。机械按键按下和释放时,会有几毫秒到十几毫秒的抖动。
我的做法:用定时器做10ms的定时扫描,或者用状态机做软件去抖。在FreeRTOS里,我习惯创建一个按键任务,每10ms读一次,连续读到3次相同状态才认为有效。
一个简单的软件去抖实现:
uint8_t key_debounce(void)
{
uint8_t state1, state2;
state1 = (GPIOA->ISTAT >> 1) & 0x01;
delay_ms(10); /* 延时10ms */
state2 = (GPIOA->ISTAT >> 1) & 0x01;
if (state1 == state2) {
return state1; /* 两次一致,返回有效电平 */
} else {
return 0xFF; /* 抖动中,返回无效 */
}
}
避坑指南:我曾经在项目里直接用delay_ms做去抖,结果系统响应变得很迟钝。后来改成用定时器+状态机的方式,既不阻塞CPU,又能准确检测按键。在FreeRTOS里,用vTaskDelay替代delay_ms是更好的选择。
最后,把按键和LED结合起来:按下按键,LED翻转。
void key_led_demo(void)
{
uint8_t key_val;
key_val = key_debounce();
if (key_val == 0) { /* 按键按下 */
GPIOA->BOP = (1 << 16) | (1 << 0); /* 翻转PA0 */
/* 注意:BOP寄存器高16位是清除位,低16位是置位位 */
/* 写高16位对应位为1 = 清零,写低16位对应位为1 = 置位 */
/* 同时写同一引脚的高位和低位 = 翻转 */
}
}
嗯,这里有个小技巧:BOP寄存器的设计很巧妙。高16位写1对应清零,低16位写1对应置位。如果你同时写同一引脚的高位和低位,就实现了翻转——一次原子操作,不需要关中断。
这一章的内容就到这里。GPIO看似简单,但它是所有外设通信的基础。你把这些寄存器操作练熟了,后面学SPI、I2C、UART都会轻松很多。