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 的外部中断支持以下几种触发方式:
| 触发方式 | 适用场景 | 注意事项 |
|---|---|---|
| 上升沿触发 | 按键松开检测 | 容易受噪声干扰 |
| 下降沿触发 | 按键按下检测 | 需要去抖处理 |
| 双边沿触发 | 编码器脉冲计数 | 中断频率可能很高 |
| 低电平触发 | 唤醒休眠模式 | 电平持续会反复触发 |
配置步骤其实就四步:
- 设置 GPIO 为输入模式(PxDIR 对应位写 0)
- 使能内部上拉或下拉电阻(可选,取决于外部电路)
- 选择触发方式(上升沿/下降沿/双边沿/电平)
- 使能中断,编写中断服务函数
避坑指南:我曾经在 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 的核心知识点串起来了,你对照着看,思路会更清晰。