嵌入式学习总结:从 I2C 到 SPI 的驱动开发实战

1. 引言

在嵌入式开发中,I2C 和 SPI 是使用频率最高的两种串行通信协议。无论是读取传感器数据、驱动显示屏,还是连接 Flash 存储芯片,都离不开它们。本文结合个人学习经历,从协议原理、Linux 驱动框架到实际调试技巧,系统总结 I2C 与 SPI 驱动开发的核心知识点,希望能帮助初学者少走弯路。

2. 为什么嵌入式开发离不开 I2C 与 SPI

嵌入式系统通常资源受限,引脚数量有限,因此串行总线成为连接外设的主流方案。I2C 和 SPI 各有特点:

  • I2C:仅需两根线(SCL、SDA),支持多设备挂载,通过设备地址寻址,适合连接传感器、EEPROM 等低速设备。
  • SPI:通常需要四根线(SCLK、MOSI、MISO、CS),传输速率高,适合显示屏、ADC、Flash 等高速设备。

理解两者的差异,是选择通信方案的基础。

3. I2C 协议核心要点

3.1 物理层与时序

I2C 总线由 SCL(时钟线)和 SDA(数据线)组成,均为开漏输出,需要外接上拉电阻。通信由主机发起,基本时序包括:

  • 起始条件:SCL 为高时,SDA 由高变低。
  • 停止条件:SCL 为高时,SDA 由低变高。
  • 数据传输:每个字节 8 位,高位在前,第 9 个时钟周期由接收方返回 ACK。
c 复制代码
// I2C 起始与停止条件(GPIO 模拟时序示例)
static void i2c_start(void) {
    SDA_HIGH();
    SCL_HIGH();
    SDA_LOW();
    SCL_LOW();
}

static void i2c_stop(void) {
    SDA_LOW();
    SCL_HIGH();
    SDA_HIGH();
}

3.2 设备地址与读写操作

I2C 设备地址通常为 7 位,加上读写位组成 8 位。例如某传感器地址为 0x48,则读地址为 0x91,写地址为 0x90。写操作流程为:起始条件 → 发送设备地址+写位 → 发送寄存器地址 → 发送数据 → 停止条件。

c 复制代码
// 向 I2C 设备寄存器写入一个字节
int i2c_write_reg(uint8_t dev_addr, uint8_t reg, uint8_t val) {
    i2c_start();
    if (i2c_send_byte(dev_addr << 1 | 0) != 0) return -1;
    if (i2c_send_byte(reg) != 0) return -1;
    if (i2c_send_byte(val) != 0) return -1;
    i2c_stop();
    return 0;
}

3.3 实际调试经验

调试 I2C 时,最常遇到的问题就是设备无响应。排查思路如下:

  1. 检查上拉电阻是否焊接,阻值通常选 4.7kΩ。
  2. 用示波器或逻辑分析仪抓取 SCL、SDA 波形,确认时序是否正确。
  3. 确认设备地址是否正确,部分芯片支持多个地址引脚配置。
  4. 使用 i2cdetect 工具扫描总线,快速定位设备地址。

4. SPI 协议核心要点

4.1 四线制与工作模式

SPI 总线包含 SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)四根线。每个从设备独占一个 CS 引脚,因此不存在地址冲突问题。SPI 有四种工作模式(CPOL、CPHA 组合),需要根据从设备数据手册配置。

c 复制代码
// SPI 模式配置示例(Linux 用户态)
struct spi_ioc_transfer tr = {
    .tx_buf = (unsigned long)tx_buf,
    .rx_buf = (unsigned long)rx_buf,
    .len = len,
    .speed_hz = 1000000,
    .delay_usecs = 0,
    .bits_per_word = 8,
};
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);

4.2 全双工通信特点

SPI 是全双工通信,主机发送数据的同时也在接收数据。即使只需要读取从设备数据,主机也必须发送时钟和虚拟数据(通常为 0x00 或 0xFF)来驱动从设备输出。

c 复制代码
// SPI 读取示例:先发送命令字节,再读取数据
uint8_t spi_read_reg(uint8_t reg) {
    uint8_t tx[2] = {reg | 0x80, 0x00};
    uint8_t rx[2] = {0};
    spi_transfer(tx, rx, 2);
    return rx[1];
}

4.3 实际调试经验

SPI 调试中,CS 片选时序和时钟极性是最容易出错的地方:

  1. 确认从设备要求的 CPOL、CPHA 模式,配置错误会导致数据错位。
  2. 检查 CS 是否在传输前拉低、传输结束后拉高。
  3. 高速传输时注意信号完整性,必要时降低速率或缩短连线。
  4. 使用逻辑分析仪同时抓取四根线,对比数据手册时序图。

5. Linux 下的 I2C 与 SPI 驱动框架

5.1 I2C 设备驱动结构

在 Linux 内核中,I2C 驱动主要涉及 i2c_driver 和 i2c_client 两个核心结构体。驱动通过 i2c_add_driver 注册,匹配成功后调用 probe 函数。

c 复制代码
static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "my_sensor",
        .of_match_table = of_match_ptr(sensor_dt_ids),
    },
    .probe = sensor_probe,
    .remove = sensor_remove,
    .id_table = sensor_id_table,
};

module_i2c_driver(sensor_driver);

5.2 SPI 设备驱动结构

SPI 驱动框架与 I2C 类似,核心是 spi_driver 和 spi_device。通过设备树描述 SPI 从设备,驱动匹配后完成初始化。

c 复制代码
static struct spi_driver display_driver = {
    .driver = {
        .name = "my_display",
        .of_match_table = of_match_ptr(display_dt_ids),
    },
    .probe = display_probe,
    .remove = display_remove,
};

module_spi_driver(display_driver);

5.3 设备树配置要点

设备树是描述硬件连接的关键,I2C 和 SPI 节点的配置略有不同:

dts 复制代码
// I2C 设备节点示例
&i2c1 {
    status = "okay";
    sensor@48 {
        compatible = "my,sensor";
        reg = <0x48>;
    };
};

// SPI 设备节点示例
&spi2 {
    status = "okay";
    display@0 {
        compatible = "my,display";
        reg = <0>;
        spi-max-frequency = <1000000>;
    };
};

6. 用户态访问方式

对于快速验证,Linux 提供了用户态访问接口,无需编写内核驱动即可测试硬件。

6.1 I2C 用户态访问

c 复制代码
#include <linux/i2c-dev.h>
#include <fcntl.h>
#include <sys/ioctl.h>

int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x48);  // 设置从设备地址
write(fd, &reg, 1);          // 写寄存器地址
read(fd, &val, 1);           // 读取数据

6.2 SPI 用户态访问

c 复制代码
#include <linux/spi/spidev.h>

int fd = open("/dev/spidev2.0", O_RDWR);
ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
struct spi_ioc_transfer tr = { ... };
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);

7. 学习路线与实战建议

7.1 推荐学习路径

  1. 第一阶段:掌握 C 语言基础与 GPIO 操作,理解寄存器读写。
  2. 第二阶段:用 GPIO 模拟 I2C 时序,深入理解协议本质。
  3. 第三阶段:使用硬件 I2C 控制器,学习 Linux 驱动框架。
  4. 第四阶段:掌握 SPI 协议,对比两者差异。
  5. 第五阶段:结合具体项目(传感器、显示屏、Flash),完成完整驱动开发。

7.2 实战项目推荐

  • 温湿度传感器驱动:使用 I2C 接口读取 SHT30 数据。
  • OLED 显示屏驱动:使用 I2C 或 SPI 驱动 SSD1306。
  • SPI Flash 读写:驱动 W25Q64,实现数据存储功能。
  • 加速度传感器:通过 I2C 读取 MPU6050 数据。

7.3 调试工具清单

  • 逻辑分析仪:抓取总线时序,定位通信问题。
  • 示波器:观察信号质量,排查高速传输问题。
  • i2cdetect / i2cdump:扫描和读取 I2C 总线。
  • spidev_test:SPI 用户态测试工具。

8. 总结

I2C 和 SPI 是嵌入式开发的基础技能,掌握它们需要理论与实践相结合。我的学习体会是:先理解协议时序,再动手写代码,最后用工具验证波形。遇到问题不要盲目猜测,用逻辑分析仪抓波形是最有效的排查手段。希望这篇总结能帮助你在嵌入式学习的道路上更进一步。

相关推荐
老当益壮梁奶奶1 小时前
驱动学习(二):从零开始:驱动工程搭建与字符设备驱动框架详解(基于IMX6ULL)
c语言·arm开发·驱动开发·嵌入式硬件·学习
叶子野格1 小时前
《FPGA基本知识累积:看开发指南要懂的术语》
学习·fpga开发
qq_284274051 小时前
液压泵工作原理与性能参数笔记:能量转换与吸压油三必要条件、压力分级、排量流量与三种效率特性曲线、径向轴向柱塞泵变量原理及配流配油盘(含考点)
笔记·学习·自动化·制造
li星野2 小时前
【学习记录】射线补偿原理与实现:从物理模型到MATLAB代码
开发语言·学习·matlab
yangmu32032 小时前
单片机发展史:从 8051 到 AIoT 芯片
学习
qq_284274052 小时前
液压泵类型结构与液压马达笔记:柱塞泵奇数柱塞减震槽通轴斜轴、单双作用叶片泵八段曲线限压变量、齿轮泵排量困油径向力端面补偿、内啮合螺杆泵与高低速马达(含考点)
笔记·学习·自动化·制造
云半S一3 小时前
Agent学习总结二
经验分享·笔记·学习·agent
彧azz3 小时前
B-树学习总结:从结构特性到核心操作的系统复习
数据结构·笔记·b树·学习
燕落南枝3 小时前
HTML 学习笔记:从基础到音频标签
笔记·学习·html