4、GPIO裸机驱动开发:从寄存器到实战
好,咱们进入第四讲。GPIO,说白了就是芯片的“手脚”。你想让芯片控制LED亮灭,或者检测按键有没有按下,都得靠它。我刚开始学SoC时,觉得GPIO太简单了,不就是写个1或0嘛。结果真到项目里,中断配置和防抖处理,可是坑了我不少时间。
今天咱们就把GPIO这块彻底吃透。从寄存器原理,到输出控制LED,再到输入检测按键,最后搞定中断和防抖。一条龙讲完。
4.1 GPIO模块寄存器详解
每个GPIO引脚,背后都有一堆寄存器在管着。你想想看,一个引脚既要能输出,又要能输入,还得能触发中断,光靠一个寄存器肯定不行。
以我常用的某款Cortex-M4 SoC为例,GPIO模块通常包含以下几类核心寄存器:
| 寄存器类型 | 功能描述 | 位宽 |
|---|---|---|
| GPIOx_MODER | 模式选择:输入、输出、复用功能、模拟 | 32位,每2位控制1个引脚 |
| GPIOx_OTYPER | 输出类型:推挽或开漏 | 32位,每1位控制1个引脚 |
| GPIOx_OSPEEDR | 输出速度:低速、中速、高速、超高速 | 32位,每2位控制1个引脚 |
| GPIOx_PUPDR | 上下拉配置:无、上拉、下拉、保留 | 32位,每2位控制1个引脚 |
| GPIOx_IDR | 输入数据寄存器,只读 | 32位,每1位对应1个引脚电平 |
| GPIOx_ODR | 输出数据寄存器 | 32位,每1位控制1个引脚输出 |
| GPIOx_BSRR | 位设置/清除寄存器,原子操作 | 32位,低16位置位,高16位复位 |
| GPIOx_AFRL/AFRH | 复用功能选择,每个引脚4位 | 32位,AFRL控制0-7,AFRH控制8-15 |
重点提醒:配置GPIO的第一步,永远是使能对应GPIO端口的时钟。我见过太多新手,寄存器配了半天,结果引脚没反应,最后发现是时钟没开。这个坑,我当年也踩过。
4.2 输出控制:点亮LED
LED驱动是GPIO最基础的应用。说白了,就是让引脚输出高电平或低电平。
我个人习惯用BSRR寄存器来控制输出,而不是直接写ODR。为什么?因为BSRR支持原子操作,你写ODR时如果被中断打断,可能会出问题。
来看一段实际代码:
/* 假设LED接在PA0引脚,低电平点亮 */
#define GPIOA_BASE 0x40020000
#define RCC_AHB1ENR (*(volatile uint32_t *)0x40023830)
#define GPIOA_MODER (*(volatile uint32_t *)(GPIOA_BASE + 0x00))
#define GPIOA_BSRR (*(volatile uint32_t *)(GPIOA_BASE + 0x18))
/* 初始化PA0为输出 */
void LED_Init(void)
{
// 1. 使能GPIOA时钟
RCC_AHB1ENR |= (1 << 0);
// 2. 设置PA0为输出模式(MODER[1:0] = 01)
GPIOA_MODER &= ~(0x3 << 0); // 先清零
GPIOA_MODER |= (0x1 << 0); // 设置为输出
// 3. 默认关闭LED(输出高电平)
GPIOA_BSRR = (1 << 16); // BR0置位,复位ODR0
}
/* 点亮LED */
void LED_On(void)
{
GPIOA_BSRR = (1 << 0); // BS0置位,设置ODR0为1
// 注意:如果LED是低电平点亮,这里应该用BR
}
/* 熄灭LED */
void LED_Off(void)
{
GPIOA_BSRR = (1 << 16); // BR0置位,复位ODR0
}
/* 翻转LED */
void LED_Toggle(void)
{
GPIOA_ODR ^= (1 << 0); // 异或操作翻转
}
小技巧:翻转LED时,用ODR异或的方式最简洁。但要注意,如果多个任务同时操作同一个引脚,还是用BSRR更安全。我在做多线程项目时,就因为这个吃过亏。
4.3 输入检测:按键读取
按键检测比LED输出要复杂一些。因为按键按下时,电平会变化,但机械开关会有抖动。
先看最基本的轮询方式读取按键:
/* 按键接在PC13,按下为低电平 */
#define GPIOC_IDR (*(volatile uint32_t *)(GPIOC_BASE + 0x10))
/* 初始化PC13为上拉输入 */
void Key_Init(void)
{
// 使能GPIOC时钟
RCC_AHB1ENR |= (1 << 2);
// 设置PC13为输入模式(MODER[27:26] = 00)
GPIOC_MODER &= ~(0x3 << 26);
// 使能内部上拉(PUPDR[27:26] = 01)
GPIOC_PUPDR &= ~(0x3 << 26);
GPIOC_PUPDR |= (0x1 << 26);
}
/* 读取按键状态,返回1表示按下,0表示未按下 */
uint8_t Key_Read(void)
{
if((GPIOC_IDR & (1 << 13)) == 0)
{
return 1; // 按下
}
else
{
return 0; // 未按下
}
}
这里有个细节:为什么用内部上拉?因为按键通常一端接地,另一端接GPIO。用内部上拉,按键没按时引脚被拉到高电平,按下时被拉到低电平。省一个外部电阻。
4.4 中断配置:上升沿与下降沿
轮询方式太浪费CPU了。你想想看,CPU一直在那死循环读按键,啥也干不了。所以实际项目中,我们都会用中断。
GPIO中断的配置流程,我总结为四步:
- 配置GPIO为输入模式(和上面一样)
- 使能SYSCFG时钟,配置外部中断线映射
- 配置中断触发方式:上升沿、下降沿或双边沿
- 使能NVIC中断,编写中断服务函数
来看完整代码:
/* 配置PC13为下降沿触发中断 */
#define SYSCFG_EXTICR4 (*(volatile uint32_t *)0x40013814)
#define EXTI_IMR (*(volatile uint32_t *)0x40013C00)
#define EXTI_FTSR (*(volatile uint32_t *)0x40013C0C)
#define EXTI_PR (*(volatile uint32_t *)0x40013C14)
void Key_Interrupt_Init(void)
{
// 1. GPIO配置为上拉输入(同上)
Key_Init();
// 2. 使能SYSCFG时钟
RCC_APB2ENR |= (1 << 14);
// 3. 将PC13映射到EXTI13
SYSCFG_EXTICR4 &= ~(0xF << 4); // 清零
SYSCFG_EXTICR4 |= (0x2 << 4); // 0x2对应GPIOC
// 4. 使能EXTI13中断
EXTI_IMR |= (1 << 13);
// 5. 配置下降沿触发(按键按下时产生下降沿)
EXTI_FTSR |= (1 << 13);
// 6. 配置并使能NVIC
NVIC_EnableIRQ(EXTI15_10_IRQn);
NVIC_SetPriority(EXTI15_10_IRQn, 1);
}
/* 中断服务函数 */
void EXTI15_10_IRQHandler(void)
{
if(EXTI_PR & (1 << 13))
{
// 清除中断标志位(写1清除)
EXTI_PR |= (1 << 13);
// 处理按键事件
// 注意:这里不能做耗时操作
LED_Toggle();
}
}
重要提醒:中断服务函数里一定要先清除中断标志位,再处理业务逻辑。我曾经在一个项目中,因为清除标志位的顺序搞反了,导致中断反复触发,系统直接卡死。排查了整整一天才找到问题。
4.5 防抖处理:软件与硬件方案
按键抖动是机械开关的物理特性。按下和松开时,电平会快速跳变几次,持续5-20ms。如果不做防抖,一次按键可能会触发多次中断。
我常用的防抖方案有三种:
4.5.1 软件延时防抖(最简单)
/* 软件延时防抖,适用于轮询方式 */
uint8_t Key_Read_Debounce(void)
{
if(Key_Read() == 1) // 第一次检测到按下
{
Delay_ms(10); // 延时10ms,避开抖动期
if(Key_Read() == 1) // 再次确认
{
// 等待按键松开
while(Key_Read() == 1);
return 1; // 返回有效按键
}
}
return 0;
}
4.5.2 硬件RC滤波(最可靠)
在按键和GPIO之间加一个RC低通滤波器。R取10kΩ,C取0.1μF,时间常数τ=RC=1ms。这样抖动的高频分量就被滤掉了。
嗯,这里要注意,RC滤波会增加响应延迟。如果按键需要快速响应,这个方案就不太合适。
4.5.3 定时器扫描防抖(最推荐)
我个人最推荐这种方式。用定时器每隔10ms扫描一次按键,连续两次读到相同状态才认为有效。
/* 定时器中断中调用,每10ms执行一次 */
static uint8_t key_state = 0; // 当前按键状态
static uint8_t key_confirm = 0; // 确认后的状态
void Key_Scan(void)
{
uint8_t current = Key_Read();
if(current == key_state)
{
// 两次采样一致,确认状态
if(key_confirm != current)
{
key_confirm = current;
if(current == 1) // 检测到按下
{
Key_Event_Handler(); // 触发按键事件
}
}
}
else
{
// 状态不一致,更新采样值
key_state = current;
}
}
避坑指南:我曾经在一个工业控制项目中,用了最简单的软件延时防抖。结果因为系统中有多个中断,延时函数被频繁打断,导致防抖时间不准,按键时而灵敏时而迟钝。后来改成定时器扫描方案,问题才彻底解决。所以,在有RTOS或多中断的系统里,千万别用阻塞式延时做防抖。
小结
GPIO看似简单,但要做好并不容易。从寄存器配置到中断处理,再到防抖设计,每一步都有讲究。我建议你在学习时,先拿一块开发板,把LED点亮、按键读取、中断触发这三个实验逐个做通。然后再把防抖加上去,看看效果对比。
下一讲,我们会讲定时器的裸机驱动开发。定时器和GPIO配合起来,能玩出很多花样。到时候见。