4. 裸机启动流程

GPIO,说白了就是芯片跟外界打招呼的手脚。我刚开始做嵌入式那会儿,觉得GPIO不就是个高低电平嘛,有啥好研究的?直到有一次,一个开漏输出没配好,整个I²C总线卡死,排查了整整两天……嗯,从那以后我再也不敢小看这几个引脚了。

今天咱们就把GPIO的几种模式、推挽与开漏的区别、上下拉电阻怎么选,以及怎么用Rust的HAL库点亮一个LED,一次性讲透。

4.1 GPIO模式:输入、输出、复用

一个GPIO引脚,在不同场景下扮演不同角色。你可以把它想象成一个多功能的工具箱——关键是要选对工具头。

模式 典型用途 内部电路状态
输入模式 读取按键、检测电平 高阻态,引脚电平由外部驱动
输出模式 驱动LED、控制继电器 推挽或开漏,由寄存器驱动
复用功能 UART、SPI、I²C等外设 引脚交给片上外设控制

我个人习惯,在初始化阶段就把每个引脚的模式想清楚。别偷懒——我见过太多人把本该配成复用的引脚配成输出,结果外设死活不工作。

核心原则:一个引脚同一时间只能处于一种模式。切换模式前,务必确保当前外设已经释放对该引脚的控制。

4.2 推挽输出 vs 开漏输出

这两个概念,很多初学者容易搞混。我简单解释一下:

  • 推挽输出(Push-Pull):输出高电平时,内部PMOS导通,直接拉到VCC;输出低电平时,内部NMOS导通,直接拉到GND。说白了,就是既能主动推高,也能主动拉低。
  • 开漏输出(Open-Drain):只有拉低的能力。输出高电平时,引脚处于高阻态,必须靠外部上拉电阻把电平拉高。

你想想看,为什么要有开漏这种“半残”模式?因为多个开漏输出可以接在同一条线上——谁拉低谁说话,这就是I²C总线的基础。

特性 推挽输出 开漏输出
驱动能力 强,可直接驱动LED 弱,需外部上拉
电平转换 不支持 可通过上拉电阻实现
多设备共享 不能(会短路) 可以(线与逻辑)
典型应用 普通GPIO、SPI I²C、中断信号

我曾经踩过的坑:把I²C的SCL和SDA配成了推挽输出。结果两个设备同时输出不同电平,直接短路,芯片发烫。记住:总线协议要求开漏,就别自作聪明。

4.3 上拉与下拉电阻

输入模式下,引脚如果悬空,电平是不确定的。这时候就需要上拉或下拉电阻来给一个确定的默认状态。

  • 上拉电阻:默认高电平,外部拉低时读取到低电平。适合按键检测(按下为低)。
  • 下拉电阻:默认低电平,外部拉高时读取到高电平。适合某些传感器输出高有效的场景。

现代MCU内部一般都集成了可编程的上拉/下拉电阻,阻值通常在30kΩ~50kΩ之间。我个人习惯,能用内部电阻就不用外部的——省一个电阻是一分钱,量产时你就知道这有多重要了。

小技巧:如果外部已经有上拉电阻(比如I²C总线),就别再使能内部上拉了。两个电阻并联,等效阻值会变小,功耗增加,信号质量反而下降。

4.4 使用HAL库控制LED

好了,理论说完了,咱们来点实际的。下面是用Rust的embassy-stm32 HAL库点亮一个LED的完整代码。

假设LED接在PA5引脚,高电平点亮。

use embassy_stm32::gpio::{Level, Output, Pin, Speed};
use embassy_stm32::Peripherals;

#[embassy_executor::main]
async fn main(_spawner: embassy_executor::Spawner) {
    let p = Peripherals::take();
    
    // 配置PA5为推挽输出,初始低电平
    let mut led = Output::new(
        p.PA5,
        Level::Low,
        Speed::VeryHigh,
    );
    
    // 点亮LED
    led.set_high();
    
    // 或者用toggle翻转
    // led.toggle();
    
    loop {
        // 你的主循环
    }
}

这段代码看起来简单,但有几个细节值得注意:

  • Level::Low是初始状态。我建议LED初始化为灭,避免上电瞬间闪一下。
  • Speed::VeryHigh设置的是IO口的翻转速率。驱动LED用中速就够了,高速会增加功耗和噪声。
  • set_high()和set_low()是HAL封装好的方法,内部操作的是BSRR寄存器——比直接写ODR更安全,不会影响其他引脚。

性能提示:如果你需要频繁翻转GPIO(比如软件PWM),直接用toggle()方法。它内部用的是ODR寄存器的异或操作,比先读再写快一个数量级。

4.5 知识结构总览

下面这张图,把GPIO配置的整个决策流程串起来了。我建议你把它存下来,以后配引脚时照着走一遍,基本不会出错。

GPIO配置决策流程 确定引脚功能 输入还是输出? 输入模式 是否需要上下拉? 配置上拉/下拉 输出模式 推挽还是开漏? 配置输出类型/速度 初始化完成 💡 复用功能模式 如果使用外设,直接配为复用模式

这张图的逻辑其实很简单:先确定你要用这个引脚干什么,然后根据输入还是输出走不同的分支。复用功能本质上也是一种“输出交给外设”的模式,所以归在输出分支里。

4.6 实战:按键控制LED

光会点亮LED还不够,咱们来个完整的输入输出配合——用按键控制LED亮灭。

use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use embassy_stm32::Peripherals;

#[embassy_executor::main]
async fn main(_spawner: embassy_executor::Spawner) {
    let p = Peripherals::take();
    
    // PA5: 推挽输出,驱动LED
    let mut led = Output::new(
        p.PA5,
        Level::Low,
        Speed::Medium,
    );
    
    // PC13: 输入模式,内部上拉,读取按键
    // 按键按下时,引脚被拉低
    let button = Input::new(
        p.PC13,
        Pull::Up,
    );
    
    loop {
        if button.is_low() {
            led.set_high();   // 按键按下,点亮
        } else {
            led.set_low();    // 松开,熄灭
        }
    }
}

这里有个细节:按键用了内部上拉,所以默认是高电平。按下时引脚被拉到GND,读取到低电平。这种接法很常见,省一个外部电阻。

注意:实际项目中,按键需要做消抖处理。上面的代码只是为了演示GPIO的基本用法。消抖可以用硬件(RC滤波)或软件(延时去抖),咱们在后面的章节会专门讲。

好了,GPIO这部分就到这里。记住:模式选对,上下拉配好,输出类型搞清楚,你的引脚就不会出幺蛾子。下次咱们聊定时器,那个坑更多……到时候我慢慢给你讲。