3. GPIO 与基本外设:从寄存器到实战

各位同学,今天我们来啃 GPIO 这块硬骨头。说实话,GPIO 是单片机和外界的「嘴巴」——你所有传感器输入、电机控制、通信握手,最后都得靠这几个引脚。CMS32 的 GPIO 模块设计得挺规整,但坑也不少。我当年第一个项目就在输出模式上栽过跟头,今天咱们一次性讲透。

3.1 GPIO 寄存器详解

CMS32 的 GPIO 寄存器,说白了就是一组 32 位的控制开关。每个引脚对应几个关键寄存器位,你往里面写 0 或 1,引脚的行为就变了。我个人习惯把寄存器分成三类:

寄存器类别 典型寄存器 作用
方向控制 PxDIR 0=输入,1=输出
数据锁存 PxOUT 输出时写电平,输入时读电平
功能复用 PxAFSEL 选择 GPIO 还是外设功能

嗯,这里要注意:PxOUT 寄存器在输入模式下也能写,但写进去的值不会影响引脚电平,而是存在锁存器里。等你切到输出模式,这个值才会生效。我曾经调试一个按键电路,发现切换方向后引脚电平不对,查了半天才发现是之前写 PxOUT 时留了个 0 在里边。

我的习惯:每次切换 GPIO 方向前,先写 PxOUT 确定初始电平。比如要输出高电平,先写 PxOUT 对应位为 1,再改 PxDIR 为输出。这样引脚不会出现毛刺。

3.2 推挽输出 vs 开漏输出

这两个模式是初学者最容易搞混的地方。我直接说人话:

  • 推挽输出:内部两个管子轮流导通,一个推高,一个拉低。输出高电平时直接怼到 VDD,低电平时直接拉到 GND。驱动能力强,适合直接驱动 LED、蜂鸣器。
  • 开漏输出:只有拉低能力,没有推高能力。想输出高电平?必须外接上拉电阻。好处是可以实现「线与」逻辑,多个开漏输出可以共用一个上拉电阻。

你想想看,什么时候必须用开漏?I²C 总线!因为多个设备要共用一条数据线,如果都用推挽,一个设备输出高、另一个输出低,直接短路。开漏输出配合上拉电阻,谁拉低谁说了算,不会打架。

实战经验:我做过一个多机通信项目,用 CMS32 的 GPIO 模拟 I²C。一开始偷懒用了推挽模式,结果两个从机同时应答时总线电平乱跳。改成开漏输出加上拉电阻,问题立刻解决。记住:总线类通信,优先考虑开漏。

3.3 外部中断配置

外部中断是 GPIO 的「主动汇报」机制。不用你轮询查电平,引脚一变化,CPU 自动跳进中断服务函数。CMS32 的外部中断支持以下几种触发方式:

触发方式 适用场景 注意事项
上升沿触发 按键松开检测 容易受噪声干扰
下降沿触发 按键按下检测 需要去抖处理
双边沿触发 编码器脉冲计数 中断频率可能很高
低电平触发 唤醒休眠模式 电平持续会反复触发

配置步骤其实就四步:

  1. 设置 GPIO 为输入模式(PxDIR 对应位写 0)
  2. 使能内部上拉或下拉电阻(可选,取决于外部电路)
  3. 选择触发方式(上升沿/下降沿/双边沿/电平)
  4. 使能中断,编写中断服务函数
避坑指南:我曾经在 CMS32 上配置外部中断时,忘了清除中断标志位就退出中断服务函数。结果中断反复触发,CPU 卡死在中断里,主循环根本跑不起来。记住:进中断第一件事,清标志位。

3.4 实战:按键控制 LED 灯

理论讲完了,咱们直接上手。这个实战的目标很简单:按一下按键,LED 状态翻转。但我会把 GPIO 配置、外部中断、软件去抖全部揉进去。

硬件连接

  • LED:接 PA0,推挽输出,串联 330Ω 限流电阻到 GND
  • 按键:接 PA1,外部中断下降沿触发,内部上拉电阻使能

代码实现

#include "CMS32.h"

// 中断标志,用于主循环判断
volatile uint8_t key_pressed = 0;

void GPIO_Init(void)
{
    // PA0 推挽输出,初始低电平
    PAOUT &= ~(1 << 0);
    PADIR |= (1 << 0);
    
    // PA1 输入,内部上拉
    PADIR &= ~(1 << 1);
    PAPUEN |= (1 << 1);   // 使能上拉
    
    // 配置外部中断:下降沿触发
    EXICR1 &= ~(0x03 << 2);  // 清配置位
    EXICR1 |= (0x02 << 2);   // 下降沿触发
    
    // 使能中断
    EXIEN |= (1 << 1);
    NVIC_EnableIRQ(EXTI1_IRQn);
}

void EXTI1_IRQHandler(void)
{
    // 清中断标志
    EXIFG |= (1 << 1);
    
    // 软件去抖:延时 10ms 再检测一次
    delay_ms(10);
    if((PAIN & (1 << 1)) == 0)  // 确实按下
    {
        key_pressed = 1;
    }
}

int main(void)
{
    GPIO_Init();
    
    while(1)
    {
        if(key_pressed)
        {
            key_pressed = 0;
            // 翻转 LED
            PAOUT ^= (1 << 0);
        }
        // 其他任务...
    }
}
关于去抖:机械按键按下时会有 5-20ms 的抖动期。我在中断里加了个 10ms 延时再检测,这是最朴素但有效的办法。如果要求更高,可以用定时器做状态机去抖,但小项目这样够用了。

3.5 本章知识体系

下面这张图把 GPIO 的核心知识点串起来了,你对照着看,思路会更清晰。

GPIO 知识体系 GPIO 核心 寄存器配置 输出模式 外部中断 PxDIR 方向 PxOUT 数据 推挽输出 开漏输出 触发方式 实战:按键控制 LED(中断 + 去抖) 核心思路:寄存器配置决定引脚行为 → 输出模式选择影响驱动方式 → 中断机制实现事件驱动 → 实战整合所有知识点 记住:先配方向,再配模式,最后使能中断。顺序错了,引脚可能不受控。