硬件接口基础:GPIO、I2C、SPI、UART

各位同学,咱们今天聊点实在的。做嵌入式驱动开发,说白了就是跟芯片的脚打交道。你得知道它什么时候拉高、什么时候拉低,数据怎么从这根线传到那根线。我刚开始接触这行的时候,总觉得协议栈很神秘,后来发现——嗯,底层无非就是这四种接口:GPIO、I2C、SPI、UART。把这四个吃透了,市面上九成以上的外设你都能搞定。

GPIO:最基础的“开关”

GPIO 的全称是 General-Purpose Input/Output,通用输入输出口。你可以把它想象成一个智能开关——既能读外面的电平(输入),也能控制外面的电平(输出)。

我个人习惯把 GPIO 的工作模式分成三类:

  • 推挽输出:内部有俩管子,一个推高、一个拉低。输出高电平就是直接怼到 VCC,低电平就是拉到 GND。驱动能力强,适合控制 LED、继电器这类负载。
  • 开漏输出:只有拉低的能力,没有推高的能力。想输出高电平?得靠外部上拉电阻。我最早在 I2C 总线上看到这个模式时还纳闷——为什么不能直接推高?后来才明白,开漏才能实现“线与”功能,多个设备同时拉低总线不会打架。
  • 浮空输入:引脚既不拉高也不拉低,电平完全由外部决定。这时候如果外部没接信号,引脚电平就是不确定的——我踩过这个坑,按键没按下时读到的值乱跳,后来加了个上拉电阻才稳定。
小技巧:调试 GPIO 时,先用万用表量引脚电压,再用逻辑分析仪看波形。我见过太多人上来就写代码,结果发现硬件根本没焊好。

I2C 协议:两根线的“对话”

I2C 只用两根线——SCL(时钟)和 SDA(数据)。但别小看这两根线,它支持一主多从、多主多从,每个设备都有自己的地址。我当年第一次调 I2C 传感器时,折腾了一整天没反应,最后发现是从机地址写错了——嗯,这种低级错误谁没犯过呢?

I2C 的时序其实不复杂,核心就几个动作:

动作 SDA 行为 SCL 行为
起始条件 高→低 高
停止条件 低→高 高
数据位传输 数据有效期间不变 高电平采样
应答位 从机拉低表示 ACK 第 9 个时钟

写驱动的时候,我建议你把起始、停止、发送字节、接收字节这几个基本函数先写好。剩下的就是拼积木——比如读一个寄存器,就是:起始→发设备地址+写位→发寄存器地址→重复起始→发设备地址+读位→读数据→停止。

避坑指南:我曾经在一个项目里发现 I2C 偶尔卡死,查了半天是 SCL 被从机拉低了——这叫“时钟拉伸”。主设备得支持超时重试,否则整个总线就挂了。

SPI 协议:全双工的“高速公路”

SPI 比 I2C 快得多,因为它有四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。而且它是全双工的——发数据的同时也能收数据。我调过 SPI 接口的 LCD 屏,刷新率比 I2C 的屏快了一个数量级。

SPI 的时序参数主要看四个:

  • CPOL(时钟极性):空闲时时钟是高还是低
  • CPHA(时钟相位):数据在时钟的上升沿还是下降沿采样
  • 数据位宽:通常是 8 位,但也有 16 位、32 位的
  • 传输速率:从几百 KHz 到几十 MHz 都有

CPOL 和 CPHA 组合起来就是四种模式。我见过最坑的情况是:芯片手册写的是 Mode 0,但实际要配成 Mode 3 才能正常工作——说白了,手册也可能有错,最终得靠逻辑分析仪确认。

// SPI 发送一个字节的伪代码
uint8_t spi_transfer(uint8_t data_out) {
    uint8_t data_in = 0;
    for (int i = 7; i >= 0; i--) {
        // 在时钟边沿发送数据位
        if (data_out & (1 << i))
            MOSI_HIGH();
        else
            MOSI_LOW();
        // 产生时钟脉冲
        SCLK_HIGH();
        delay_half_period();
        // 在另一个边沿采样数据
        if (MISO_READ())
            data_in |= (1 << i);
        SCLK_LOW();
        delay_half_period();
    }
    return data_in;
}

关键点:SPI 没有应答机制,主设备只管发,从设备只管收。如果从设备没准备好,数据就丢了。所以片选信号的时序很重要——我习惯在 CS 拉低后等几个微秒再开始发时钟。

UART:最古老的“串口”

UART 就是咱们常说的串口。它只需要两根线——TX(发送)和 RX(接收),而且不需要时钟线。为什么?因为收发双方约定好相同的波特率,各自用自己的时钟采样。我最早学单片机就是从串口打印 "Hello World" 开始的,那时候觉得这东西真神奇。

UART 的一帧数据长这样:

  1. 起始位:1 位,拉低表示开始传输
  2. 数据位:5~9 位,通常 8 位
  3. 校验位:可选,奇校验或偶校验
  4. 停止位:1 位或 2 位,拉高表示结束

波特率是 UART 最关键的参数。9600 波特率意味着每秒传 9600 个位,算上起始位和停止位,实际传一个字节大概需要 1ms 左右。我调过 115200 的串口,也调过 2400 的——速度越慢越稳定,但传输大文件时能急死人。

避坑指南:我曾经在一个项目里发现串口收的数据偶尔错位,查了半天是收发双方的波特率有 2% 的误差。UART 对时钟精度要求不高,但误差超过 3% 就容易丢数据。所以别偷懒,用示波器量一下实际波特率。

四种接口的对比

特性 GPIO I2C SPI UART
线数 1 根 2 根 4 根 2 根
传输方式 并行/串行 半双工 全双工 全双工
速度 取决于软件 100KHz~3.4MHz 最高几十 MHz 最高几 MHz
从机数量 无限制 127 个(7 位地址) 取决于片选引脚 点对点
典型应用 按键、LED 传感器、EEPROM LCD、ADC、SD 卡 调试口、GPS 模块

知识体系总览

下面这张图把四种接口的核心要素串起来了。你写驱动的时候,脑子里要有这张图——先看接口类型,再看时序参数,最后写代码。

硬件接口基础:四种核心协议 GPIO 推挽 / 开漏 / 浮空 I2C SCL + SDA SPI SCLK + MOSI + MISO + CS UART TX + RX 关键参数:时序、速率、地址/片选、数据位宽 驱动开发流程:读手册 → 配引脚 → 写时序 → 验证波形 四种接口的选型建议 • 需要简单控制(LED、按键)→ GPIO • 需要连接多个低速传感器 → I2C(线少,但速度慢) • 需要高速传输(LCD、SD卡)→ SPI(速度快,但线多) • 需要远距离或与PC通信 → UART(简单可靠)

好了,这一章的内容就这些。GPIO 是基础中的基础,I2C 和 SPI 是嵌入式系统里最常用的总线,UART 则是调试利器。你把这些时序和原理搞清楚了,后面写驱动就是水到渠成的事。