3、GPIO接口驱动开发:GPIO原理、寄存器配置、输入输出模式、中断配置

各位好,我是老李。做嵌入式这些年,GPIO 是我打交道最多的外设,没有之一。你想想看,一个芯片少则几十个引脚,多则上百个,大部分都是 GPIO。说白了,它就是芯片和外界沟通的「手脚」。今天咱们就好好聊聊 GPIO 的驱动开发,从原理到代码,把坑都踩一遍。

3.1 GPIO 基本原理

GPIO,全称 General Purpose Input Output,通用输入输出口。每个 GPIO 引脚内部其实是一堆 MOS 管和寄存器。我习惯把它理解成一个「三态门」:可以输出高电平、输出低电平,或者高阻态(输入)。

为什么会有高阻态?嗯,这里要注意。当多个设备共享一条总线时,如果某个设备不发言,它就必须把引脚置为高阻,否则会把总线拉死。我在项目中遇到过,两个 SPI 从设备共用一个片选引脚,结果一个设备没配成高阻,直接把总线电平钳位了,排查了整整两天。

核心要点:GPIO 的三种基本状态——输出高、输出低、高阻输入。所有高级功能(中断、复用)都基于此。

3.2 寄存器配置——别小看这一步

配置 GPIO,说白了就是往寄存器里写值。不同厂商的寄存器布局不同,但套路都差不多。以 STM32 为例,每个 GPIO 端口有以下几个关键寄存器:

寄存器 功能 我常用的配置
MODER 模式选择(输入/输出/复用/模拟) 00=输入,01=输出,10=复用,11=模拟
OTYPER 输出类型(推挽/开漏) 0=推挽,1=开漏
OSPEEDR 输出速度 低速用 00,高速用 11
PUPDR 上下拉配置 00=无,01=上拉,10=下拉
IDR / ODR 输入/输出数据寄存器 读 IDR 获取电平,写 ODR 设置电平

我个人习惯,配置 GPIO 时先配 MODER,再配 PUPDR。为什么?因为 MODER 决定了引脚是输入还是输出,而 PUPDR 决定了输入时的默认电平。顺序反了,可能会产生瞬间的毛刺。

小技巧:配置输出引脚时,先写 ODR 再改 MODER。这样可以避免输出一个不确定的电平。我曾经因为顺序搞反,导致一个继电器在上电瞬间误动作,差点把板子烧了。

3.3 输入输出模式——实战中的选择

输入模式很简单,读 IDR 就行。但输出模式有讲究:推挽和开漏。

  • 推挽输出:可以主动拉高或拉低,驱动能力强。适合驱动 LED、蜂鸣器等。
  • 开漏输出:只能拉低,不能拉高。需要外部上拉电阻。适合 I2C 总线、多设备共享线路。

你想想看,如果 I2C 用推挽输出,两个设备同时拉高拉低,那不就短路了吗?所以 I2C 必须用开漏。我在项目中遇到过,有人把 I2C 的 SDA 配成了推挽,结果通信时好时坏,最后发现是引脚打架了。

// 配置 PA0 为推挽输出,速度 10MHz,无上下拉
GPIOA->MODER   &= ~(0x3 << 0);   // 先清零
GPIOA->MODER   |=  (0x1 << 0);   // 设为输出
GPIOA->OTYPER  &= ~(0x1 << 0);   // 推挽
GPIOA->OSPEEDR &= ~(0x3 << 0);   // 清零
GPIOA->OSPEEDR |=  (0x1 << 0);   // 10MHz
GPIOA->PUPDR   &= ~(0x3 << 0);   // 无上下拉

// 输出高电平
GPIOA->ODR |= (1 << 0);
// 输出低电平
GPIOA->ODR &= ~(1 << 0);

警告:开漏输出时,如果外部没有上拉电阻,引脚会一直处于低电平。我曾经调试一个 I2C 设备,死活读不到 ACK,最后发现是忘了焊上拉电阻。嗯,这种低级错误,犯一次就记住了。

3.4 中断配置——让 CPU 别闲着

轮询太浪费 CPU 了。你想想看,CPU 每秒几百万次循环,就为了等一个按键按下?太傻了。中断才是正道。

GPIO 中断的配置步骤,我总结为四步:

  1. 使能 SYSCFG 时钟——这是配置中断映射的前提。
  2. 配置 EXTI 线——把 GPIO 引脚映射到对应的 EXTI 中断线。
  3. 配置触发方式——上升沿、下降沿、或者双边沿。
  4. 使能 NVIC 中断——最后别忘了打开总中断。

我记得有一次,一个同事配置了中断,但死活进不去。我一看,原来是忘了使能 SYSCFG 时钟。这种问题,说白了就是 datasheet 没看全。

// 配置 PA0 为下降沿触发中断
// 1. 使能 SYSCFG 时钟
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;

// 2. 将 PA0 映射到 EXTI0
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;

// 3. 配置下降沿触发
EXTI->FTSR |= EXTI_FTSR_TR0;   // 下降沿
EXTI->RTSR &= ~EXTI_RTSR_TR0;  // 关闭上升沿

// 4. 取消屏蔽中断线
EXTI->IMR |= EXTI_IMR_MR0;

// 5. 配置并使能 NVIC
NVIC_SetPriority(EXTI0_IRQn, 2);
NVIC_EnableIRQ(EXTI0_IRQn);

关键点:中断服务函数里,一定要记得清除中断标志位。否则会一直进中断,系统直接卡死。我见过有人忘了清标志,结果中断里放了个延时,整个系统像抽风一样。

3.5 避坑指南——我踩过的那些坑

做 GPIO 驱动,有几个坑是新手必踩的。我列出来,你们看看:

  • 坑一:引脚复用冲突。同一个引脚可能被多个外设占用。比如 PA9 既是 USART1_TX,也是 TIM1_CH2。配置前一定要查 datasheet 的引脚功能表。
  • 坑二:上下拉电阻选错。按键输入一般用上拉,但如果你用了下拉,按键按下时电平变化方向就反了。我曾经因为这个,把按键逻辑写反了,按下去是松开,松开是按下。
  • 坑三:中断优先级搞乱。多个中断共享优先级时,高优先级的中断会打断低优先级的。如果 GPIO 中断优先级设得太高,可能会影响系统实时性。
  • 坑四:忘记配置时钟。GPIO 模块本身也需要时钟。很多芯片默认关闭所有外设时钟,你得手动打开。我刚开始做 STM32 时,经常忘了开 GPIOA 的时钟,结果寄存器写不进去。

我的习惯:每次配置 GPIO 前,先写一个 GPIO_Init() 函数,把时钟、模式、速度、上下拉、中断全部配好。这样后期维护起来,一眼就能看出问题。

好了,GPIO 这部分就聊到这儿。下一章咱们讲定时器,那个更刺激。有什么问题,欢迎随时交流。