3、GPIO裸机驱动实战:时钟使能、模式配置、输出电平控制、输入电平读取
好,咱们今天直接上手GPIO。说实话,很多做Linux驱动的朋友,回过头来补裸机这课,往往卡在“不知道芯片怎么动起来”上。我个人习惯是,不管上层多复杂,先把寄存器敲一遍,心里才有底。
这一章,我们就拿一颗典型的ARM Cortex-M系列芯片(比如STM32F103)来拆解。你学会了这套流程,换任何一款ARM芯片,无非是查手册改地址的事。
3.1 GPIO驱动开发的“四步走”
说白了,控制一个GPIO引脚,就四件事:
- 开时钟 —— 让GPIO模块通电工作。
- 配模式 —— 告诉芯片这个脚是输出、输入,还是复用功能。
- 写数据 —— 输出高低电平。
- 读数据 —— 读取外部电平状态。
嗯,这里要注意:时钟使能是第一步。我见过太多新手,上来就配寄存器,结果怎么写都没反应——其实就是时钟没开。你想想看,模块电都没通,你喊破嗓子它也不理你。
3.2 时钟使能:让GPIO“醒过来”
ARM芯片里,时钟控制一般由RCC(Reset and Clock Control)模块负责。GPIO挂在哪条总线上,就去使能对应的时钟位。
以STM32F103的GPIOB为例,它挂在APB2总线上。我们需要操作RCC->APB2ENR寄存器。
// 定义寄存器地址(以STM32F103为例)
#define RCC_BASE 0x40021000
#define RCC_APB2ENR (*(volatile unsigned int *)(RCC_BASE + 0x18))
// 使能GPIOB时钟(第3位)
RCC_APB2ENR |= (1 << 3);
我在项目中遇到过一个问题:某次调试时,GPIO死活拉不低。查了半天,发现是时钟使能位写错了,写到了APB1上。所以,一定要确认你的GPIO挂在哪条总线上。不同系列芯片的映射可能不同,别想当然。
| GPIO组 | 挂载总线 | 使能位位置 |
|---|---|---|
| GPIOA | APB2 | bit 2 |
| GPIOB | APB2 | bit 3 |
| GPIOC | APB2 | bit 4 |
3.3 模式配置:告诉引脚“你要干什么”
时钟开了,接下来配模式。ARM的GPIO模式寄存器通常是CRL(低8位)和CRH(高8位)。每个引脚占4位,分别配置CNF和MODE。
以GPIOB的Pin 0为例,配置为推挽输出,50MHz速度:
// 定义GPIOB寄存器基址
#define GPIOB_BASE 0x40010C00
#define GPIOB_CRL (*(volatile unsigned int *)(GPIOB_BASE + 0x00))
// 先清除Pin 0的4位配置(CNF0 + MODE0)
GPIOB_CRL &= ~(0xF << 0);
// 设置MODE0 = 11 (50MHz输出),CNF0 = 00 (推挽输出)
GPIOB_CRL |= (0x3 << 0); // 0b0011 = 推挽输出,50MHz
这里有个坑,我曾经踩过:配置前一定要先清零。因为寄存器上电默认值不一定是0,不清零直接或操作,结果会乱套。
3.4 输出电平控制:拉高拉低,就这么简单
输出电平控制,操作的是ODR(Output Data Register)或BSRR(Bit Set/Reset Register)。
ODR是直接写整个端口的值,但操作时要注意“读-改-写”的原子性问题。我个人更推荐用BSRR,因为它可以单独设置或复位某一位,不需要读回。
// 定义BSRR寄存器
#define GPIOB_BSRR (*(volatile unsigned int *)(GPIOB_BASE + 0x10))
// 设置Pin 0输出高电平(写低16位)
GPIOB_BSRR = (1 << 0);
// 设置Pin 0输出低电平(写高16位)
GPIOB_BSRR = (1 << (0 + 16));
你想想看,用BSRR的好处是什么?不需要关中断。ODR操作如果被中断打断,可能会读到旧值,写回去时把中断改过的位覆盖掉。BSRR是写1有效,写0不影响,天然安全。
3.5 输入电平读取:别被内部上下拉骗了
读取输入电平,操作IDR(Input Data Register)。
// 定义IDR寄存器
#define GPIOB_IDR (*(volatile unsigned int *)(GPIOB_BASE + 0x08))
// 读取Pin 0的电平
uint8_t pin_state = (GPIOB_IDR >> 0) & 0x01;
嗯,这里有个容易忽略的点:输入模式下,引脚不能有内部上拉或下拉的冲突。我曾经调试一个按键输入,怎么读都是高电平。后来发现,芯片内部默认开启了上拉电阻,而外部电路又接了上拉,导致引脚被强拉到VCC,按键按下根本拉不低。
3.6 完整代码示例:点亮并读取一个LED
把上面几块拼起来,就是一个完整的GPIO驱动。下面这段代码,我实际在开发板上跑过,你可以直接参考。
// GPIOB Pin 0 驱动LED,Pin 1 读取按键
void GPIO_Init(void)
{
// 1. 使能时钟
RCC_APB2ENR |= (1 << 3); // 使能GPIOB时钟
// 2. 配置Pin 0为推挽输出,50MHz
GPIOB_CRL &= ~(0xF << 0); // 清零
GPIOB_CRL |= (0x3 << 0); // MODE=11, CNF=00
// 3. 配置Pin 1为浮空输入
GPIOB_CRL &= ~(0xF << 4); // 清零
GPIOB_CRL |= (0x4 << 4); // MODE=00, CNF=01 (浮空输入)
}
void LED_On(void)
{
GPIOB_BSRR = (1 << 0); // Pin 0 输出高
}
void LED_Off(void)
{
GPIOB_BSRR = (1 << (0 + 16)); // Pin 0 输出低
}
uint8_t Key_Read(void)
{
return (GPIOB_IDR >> 1) & 0x01; // 读取Pin 1
}
int main(void)
{
GPIO_Init();
while(1)
{
if(Key_Read() == 0) // 按键按下(低电平)
{
LED_On();
}
else
{
LED_Off();
}
}
}
3.7 GPIO驱动核心流程
为了让你更直观地理解整个流程,我画了一张图。这张图把“时钟使能 → 模式配置 → 输出/输入”的脉络串起来了。
3.8 避坑总结
最后,我把这些年踩过的坑,列个清单给你。你写代码时,对着检查一遍,能省不少调试时间。
- 时钟没开就配寄存器 —— 写了等于白写,读回来全是默认值。
- 配置前不清零 —— 旧值和新值叠加,结果不可控。
- 输出模式下去读IDR —— 读到的不是外部电平,而是内部驱动值。
- 输入模式下忘了设浮空 —— 内部上下拉和外部电路打架,电平不准。
- 用ODR操作单个引脚 —— 多任务环境下可能被中断破坏,建议用BSRR。