4. GPIO编程
GPIO,全称是General Purpose Input Output,通用输入输出口。
说白了,它就是芯片和外界打交道的最基本方式。你想想看,一个嵌入式系统要控制LED亮灭、检测按键按下、驱动蜂鸣器,这些统统离不开GPIO。
我个人习惯把GPIO比作芯片的「手脚」——它能输出高低电平去驱动外设,也能读取外部信号的高低状态。今天我们就把它彻底搞明白。
4.1 GPIO工作原理
GPIO的内部结构,其实没那么神秘。每个GPIO引脚内部都有一堆MOS管和寄存器在配合工作。
我简单画个图帮你理解:
这张图里,你重点关注三个部分:
- 输出数据寄存器(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引脚,通常分三步走:
- 开启时钟——GPIO模块需要时钟才能工作,别漏了这一步。
- 配置模式——在CRL或CRH里设置引脚为输入、输出、复用功能等。
- 读写数据——输出用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编程,说白了就是三板斧:
- 开时钟——别省这一步。
- 配模式——输入还是输出?上拉还是浮空?想清楚再配。
- 读写数据——输出用BSRR,输入读IDR,消抖别忘。
我刚开始学嵌入式那会儿,觉得GPIO太简单了,不就是写个1写个0嘛。后来踩的坑多了才明白——越是基础的东西,越要扎实。寄存器配置错一位,整个系统都可能跑偏。
嗯,这一章的内容就到这儿。代码你可以直接拿去用,但建议自己动手敲一遍。只有亲手写过、调试过、踩过坑,这些知识才是你的。
课后小练习:试着把LED和按键结合起来——按一下按键,LED切换一种闪烁模式(快闪、慢闪、常亮、熄灭)。这个练习能帮你把输入输出彻底打通。