第4章 GPIO编程:从原理到实战

GPIO,全称是General Purpose Input Output,通用输入输出口。

说白了,它就是芯片和外界打交道的最基本方式。你想想看,一个嵌入式系统要控制LED亮灭、检测按键按下、驱动蜂鸣器,这些统统离不开GPIO。

我个人习惯把GPIO比作芯片的「手脚」——它能输出高低电平去驱动外设,也能读取外部信号的高低状态。今天我们就把它彻底搞明白。

4.1 GPIO工作原理

GPIO的内部结构,其实没那么神秘。每个GPIO引脚内部都有一堆MOS管和寄存器在配合工作。

我简单画个图帮你理解:

GPIO内部结构框图 输出数据寄存器 (ODR) 输入数据寄存器 (IDR) 控制/配置寄存器 (CRL/CRH) 输出驱动电路 施密特触发器 GPIO引脚 (Pad) 上拉/下拉电阻 实线:数据路径 虚线:控制路径

这张图里,你重点关注三个部分:

  • 输出数据寄存器(ODR):你往里面写1,引脚就输出高电平;写0,就输出低电平。
  • 输入数据寄存器(IDR):引脚上的电平状态,会实时反映到这个寄存器里。
  • 控制寄存器(CRL/CRH):配置引脚的模式——是输入还是输出?要不要上拉?速度多快?

核心要点:GPIO的本质就是「读写寄存器」。你操作的是寄存器,但影响的是引脚电平。记住这句话,后面所有代码你都能看懂。

4.2 GPIO寄存器配置

以STM32F103为例,每个GPIO端口有7个寄存器。但常用的就这几个:

寄存器 全称 作用 偏移地址
CRL 端口配置低寄存器 配置引脚0~7的模式 0x00
CRH 端口配置高寄存器 配置引脚8~15的模式 0x04
IDR 输入数据寄存器 读取引脚电平 0x08
ODR 输出数据寄存器 设置引脚输出电平 0x0C
BSRR 位设置/清除寄存器 原子操作置位/复位 0x10

配置一个GPIO引脚,通常分三步走:

  1. 开启时钟——GPIO模块需要时钟才能工作,别漏了这一步。
  2. 配置模式——在CRL或CRH里设置引脚为输入、输出、复用功能等。
  3. 读写数据——输出用ODR/BSRR,输入用IDR。

我的小技巧:配置输出时,尽量用BSRR寄存器而不是直接写ODR。BSRR支持原子操作,不会因为中断打断而出错。我在一个电机控制项目里就吃过这个亏——中断里改了ODR的低位,主循环改高位,结果引脚状态乱跳。换成BSRR后问题立刻解决。

4.3 LED灯控制实验

好了,理论说完了,咱们直接上手写代码。

这个实验的目标:控制一个LED以1秒间隔闪烁。LED接在PA0引脚,高电平点亮。

// LED闪烁实验 - 基于STM32F103
#include "stm32f10x.h"

void delay_ms(uint32_t ms) {
    uint32_t i, j;
    for(i = 0; i < ms; i++)
        for(j = 0; j < 8000; j++);
}

void GPIO_Init_LED(void) {
    // 1. 开启GPIOA时钟
    RCC->APB2ENR |= (1 << 2);   // RCC_APB2ENR的bit2对应GPIOA
    
    // 2. 配置PA0为推挽输出,50MHz
    GPIOA->CRL &= ~(0xF << 0);  // 先清除CNF0和MODE0
    GPIOA->CRL |= (0x3 << 0);   // MODE0 = 11 (50MHz输出)
    // CNF0 = 00 (推挽输出)
}

int main(void) {
    GPIO_Init_LED();
    
    while(1) {
        // 点亮LED - 使用BSRR的BS0位
        GPIOA->BSRR = (1 << 0);
        delay_ms(500);
        
        // 熄灭LED - 使用BSRR的BR0位
        GPIOA->BSRR = (1 << 16);
        delay_ms(500);
    }
}

代码很简单,但我得提醒你几个坑:

  • 时钟必须开——我见过太多新手忘了开时钟,然后对着数据手册查半天为什么寄存器写不进去。
  • 配置前先清零——CRL/CRH每个引脚占4位,配置前不清零,新旧值会叠加出奇怪的效果。
  • BSRR的高16位是复位——bit16对应引脚0的复位,别搞反了。

曾经有个坑:我在一个量产项目里,LED驱动用了ODR直接赋值。结果生产了1000块板子,有3块LED亮度异常。查了两天才发现——那3块板子在GPIO初始化时,ODR的默认值不是0,导致LED上电瞬间闪了一下。后来全部改成BSRR操作,问题消失。所以,输出操作我强烈建议用BSRR。

4.4 按键输入实验

按键输入和LED输出正好相反——我们要读取引脚电平,判断按键是否按下。

这里有个关键问题:机械按键会抖动。你按下去的一瞬间,电平会跳变好几次,如果不做处理,一次按键会被误判成多次。

// 按键输入实验 - 检测PA1引脚,低电平表示按下
#include "stm32f10x.h"

void GPIO_Init_Key(void) {
    // 1. 开启GPIOA时钟
    RCC->APB2ENR |= (1 << 2);
    
    // 2. 配置PA1为浮空输入
    GPIOA->CRL &= ~(0xF << 4);  // 清除CNF1和MODE1
    // MODE1 = 00 (输入模式)
    // CNF1 = 01 (浮空输入)
    GPIOA->CRL |= (0x4 << 4);
}

// 软件消抖 - 延时10ms后再次检测
uint8_t Key_Scan(void) {
    if((GPIOA->IDR & (1 << 1)) == 0) {  // 检测到按下
        delay_ms(10);                      // 延时消抖
        if((GPIOA->IDR & (1 << 1)) == 0) { // 再次确认
            // 等待按键释放
            while((GPIOA->IDR & (1 << 1)) == 0);
            return 1;  // 返回按键有效
        }
    }
    return 0;
}

int main(void) {
    GPIO_Init_Key();
    GPIO_Init_LED();  // 复用上一节的LED初始化
    
    while(1) {
        if(Key_Scan()) {
            // 按键按下,翻转LED状态
            GPIOA->ODR ^= (1 << 0);
        }
    }
}

按键输入有几个要点:

  • 上拉还是浮空?——如果按键一端接地,另一端接引脚,就用上拉输入(内部上拉电阻拉高,按下拉低)。如果外部已经有上拉电阻,用浮空输入也行。
  • 消抖是必须的——10ms延时是最常见的做法。我试过5ms在某些按键上还是会误触发,10ms比较稳妥。
  • 等待释放——上面的代码里,我用了while等待按键释放。这样一次按下只触发一次动作。如果你想要长按效果,可以改成计时检测。

实际项目经验:在工业设备上,我遇到过按键被干扰误触发的情况。后来在硬件上加了一个100nF电容对地滤波,软件里把消抖时间延长到20ms,问题才彻底解决。所以,硬件滤波 + 软件消抖才是最佳组合。

4.5 总结一下

GPIO编程,说白了就是三板斧:

  1. 开时钟——别省这一步。
  2. 配模式——输入还是输出?上拉还是浮空?想清楚再配。
  3. 读写数据——输出用BSRR,输入读IDR,消抖别忘。

我刚开始学嵌入式那会儿,觉得GPIO太简单了,不就是写个1写个0嘛。后来踩的坑多了才明白——越是基础的东西,越要扎实。寄存器配置错一位,整个系统都可能跑偏。

嗯,这一章的内容就到这儿。代码你可以直接拿去用,但建议自己动手敲一遍。只有亲手写过、调试过、踩过坑,这些知识才是你的。

课后小练习:试着把LED和按键结合起来——按一下按键,LED切换一种闪烁模式(快闪、慢闪、常亮、熄灭)。这个练习能帮你把输入输出彻底打通。

可可精品资料库