I2C与SPI设备驱动实战:嵌入式Linux下的外设通信协议开发完整指南
一、总线选择的工程决策:I2C与SPI的本质差异
I2C和SPI是嵌入式Linux中最常见的两种外设通信总线,但很多选型决策停留在"看引脚数"的直觉层面。实际上,两条总线在物理层、协议层和软件栈上都有本质差异,选型需要从四个核心维度综合评估:引脚数量、通信速率、拓扑复杂度和内核驱动的成熟度。
flowchart TD A[外设通信需求分析] --> B{MCU引脚资源紧张?} B -->|是,仅2根可用| C{速率要求} B -->|否,引脚充裕| D{速率要求与实时性} C -->|低于400kbps| E[I2C: 2线SDA+SCL] C -->|超过1Mbps| F[SPI: 至少4线MISO+MOSI+SCLK+CS] D -->|低于10Mbps| G{拓扑复杂度分析} D -->|高于10Mbps| F G -->|多从设备共享总线| E G -->|单从设备独占或星型连接| H[SPI也可选用] E --> I[设备树配置与驱动编写] F --> I H --> I I --> J[集成测试:逻辑分析仪验证] style E fill:#3498db,color:#fff style F fill:#e67e22,color:#fff style J fill:#27ae60,color:#fff在实际工程经验中,规则非常清晰:温度、湿度、加速度、陀螺仪等传感器几乎都用I2C,因为它们的数据量小(每帧几个字节)、不需要高速率(400kHz以内足够)、总线上可以挂多个设备(最多127个,受7位地址限制)。而LCD屏幕、SPI Flash、高速ADC/DAC等外设几乎必用SPI,因为它们的吞吐量要求(1~50MHz)远超I2C的能力范围,而且通常是控制器的独占设备。
I2C的另一个优势是硬件实现简单——两根线加两个上拉电阻即可。但这恰恰也是I2C最常见故障的根源:上拉电阻的阻值选择影响总线速率和抗干扰能力。SPI不需要上拉电阻,但它的多线结构(最少4线,带中断可能到6线)在PCB布局时需要更谨慎的布线规划。
二、I2C设备驱动的标准框架与关键细节
I2C驱动在Linux内核中有非常成熟的标准框架:i2c_driver结构体是核心入口,probe函数负责设备初始化和资源申请,remove负责清理,id_table和of_match_table负责设备和驱动的匹配。
/* i2c_temp_sensor.c — I2C温度传感器完整驱动 */ #include <linux/i2c.h> #include <linux/module.h> #include <linux/of.h> #include <linux/sysfs.h> #include <linux/delay.h> #define SENSOR_REG_TEMP 0x00 #define SENSOR_REG_CONF 0x01 #define SENSOR_REG_HYST 0x02 #define SENSOR_I2C_ADDR 0x48 #define SENSOR_CONV_DELAY 10 /* ADC转换等待时间ms */ struct sensor_data { struct i2c_client *client; struct mutex lock; int last_temp; /* 单位: m°C */ }; static int sensor_write_reg( struct i2c_client *client, u8 reg, u8 value ) { u8 buf[2] = {reg, value}; int ret; ret = i2c_master_send(client, buf, 2); if (ret != 2) { dev_err(&client->dev, "I2C write reg 0x%02x failed: %d\n", reg, ret); return ret < 0 ? ret : -EIO; } return 0; } static int sensor_read_temp( struct i2c_client *client, int *temp ) { u8 reg = SENSOR_REG_TEMP; u8 buf[2] = {0}; int ret; /* 第一步:发出寄存器地址 */ ret = i2c_master_send(client, ®, 1); if (ret < 0) { dev_err(&client->dev, "I2C write reg addr failed: %d\n", ret); return ret; } /* 等待ADC完成转换 */ msleep(SENSOR_CONV_DELAY); /* 第二步:读取温度值(2字节大端序) */ ret = i2c_master_recv(client, buf, 2); if (ret < 0) { dev_err(&client->dev, "I2C read temp failed: %d\n", ret); return ret; } /* 将2字节原始值拼为16位有符号整数 */ *temp = (buf[0] << 8) | buf[1]; /* 补码转有符号(如果最高位为1) */ if (*temp & 0x8000) *temp -= 65536; /* 典型传感器:12位精度,LSB=0.0625°C */ /* 转换为毫摄氏度(m°C) */ *temp = (*temp * 625) / 10; return 0; } static int sensor_probe(struct i2c_client *client) { struct sensor_data *data; struct device *dev = &client->dev; int ret, temp; /* 使用设备资源管理分配,无需手动释放 */ data = devm_kzalloc( dev, sizeof(*data), GFP_KERNEL ); if (!data) return -ENOMEM; >/* spi_flash.c — SPI Flash驱动(含DMA) */ #include <linux/spi/spi.h> #include <linux/dma-mapping.h> #define FLASH_CMD_READ 0x03 #define FLASH_CMD_WREN 0x06 #define FLASH_PAGE_SIZE 256 #define FLASH_SPI_SPEED 20000000 /* 20MHz */ struct flash_data { struct spi_device *spi; struct mutex lock; u8 *tx_buf; u8 *rx_buf; dma_addr_t tx_dma; dma_addr_t rx_dma; }; static void flash_setup_spi(struct spi_device *spi) { /* 配置SPI工作模式: * MODE0: CPOL=0, CPHA=0 * 即空闲时钟低电平,上升沿采样 */ spi->mode = SPI_MODE_0; spi->bits_per_word = 8; spi->max_speed_hz = FLASH_SPI_SPEED; if (spi_setup(spi)) dev_err(&spi->dev, "SPI setup failed\n"); } static int flash_read_page( struct spi_device *spi, u32 addr, u8 *buf, size_t len ) { struct spi_transfer xfer[2] = {0}; struct spi_message msg; u8 cmd[4]; int ret; /* 构造命令头:读命令(1B)+地址(3B) */ cmd[0] = FLASH_CMD_READ; cmd[1] = (addr >> 16) & 0xFF; cmd[2] = (addr >> 8) & 0xFF; cmd[3] = addr & 0xFF; /* 第一个传输段:发送命令+地址 */ xfer[0].tx_buf = cmd; xfer[0].len = 4; /* 第二个传输段:接收数据(可使用DMA加速) */ xfer[1].rx_buf = buf; xfer[1].len = len; /* 构造并同步执行SPI消息 */ spi_message_init(&msg); spi_message_add_tail(&xfer[0], &msg); spi_message_add_tail(&xfer[1], &msg); ret = spi_sync(spi, &msg); if (ret) dev_err(&spi->dev, "SPI read page failed: %d\n", ret); return ret; } static int flash_probe(struct spi_device *spi) { struct flash_data *data; struct device *dev = &spi->dev; flash_setup_spi(spi); data = devm_kzalloc( dev, sizeof(*data), GFP_KERNEL ); if (!data) return -ENOMEM; >/* 设备树配置:I2C和SPI总线的完整声明 */ /* I2C总线 */ &i2c1 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_i2c1>; clock-frequency = <100000>; /* 100kHz标准模式 */ status = "okay"; temp_sensor: temp-sensor@48 { compatible = "vendor,temp-sensor"; reg = <0x48>; /* 7位I2C地址 */ interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_FALLING>; }; }; /* SPI总线 */ &ecspi2 { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_ecspi2>; cs-gpios = <&gpio5 13 GPIO_ACTIVE_LOW>; status = "okay"; flash: flash@0 { compatible = "vendor,spi-flash"; reg = <0>; /* 片选索引0 */ spi-max-frequency = <20000000>; spi-cpha; /* CPHA=1, MODE1 */ }; }; /* GPIO引脚复用 */ &iomuxc { pinctrl_i2c1: i2c1grp { fsl,pins = < MX8MQ_IOMUXC_I2C1_SCL_I2C1_SCL 0x4000007f MX8MQ_IOMUXC_I2C1_SDA_I2C1_SDA 0x4000007f >; }; pinctrl_ecspi2: ecspi2grp { fsl,pins = < MX8MQ_IOMUXC_ECSPI2_SCLK_ECSPI2_SCLK 0x82 MX8MQ_IOMUXC_ECSPI2_MOSI_ECSPI2_MOSI 0x82 MX8MQ_IOMUXC_ECSPI2_MISO_ECSPI2_MISO 0x82 MX8MQ_IOMUXC_ECSPI2_SS0_GPIO5_IO13 0x82 >; }; };调试三板斧在实际工程中非常重要:
- I2C设备检测:
i2cdetect -y 1扫描总线上所有地址,确认设备在位。如果设备显示UU表示被内核驱动占用(正常),显示数字地址表示设备在线但无驱动接管(异常),显示--表示无回应(硬件连接或上拉电阻问题)。 - SPI通道测试:
spidev_test -D /dev/spidev0.0测试SPI通道的基本读写,确认信号连通。 - 逻辑分析仪排查:当软件层面查不出问题时,逻辑分析仪是终极武器。连接到SDA/SCL(I2C)或MOSI/MISO/SCLK/CS(SPI),直接观察总线波形。I2C常见问题包括上拉电阻过大导致信号边沿变缓、时钟拉伸过长。SPI常见问题包括时钟极性和相位配置错误、片选信号在传输完成前提前拉高。
五、总结
- I2C适合引脚少(2线)、多从设备(最多127个)、速率不超过400kbps的低速传感器场景;SPI适合高速(1~50MHz)、全双工、外设独占的显示和存储场景
- I2C驱动标准框架:i2c_driver结构体(probe/remove/id_table)、devm_kzalloc资源管理、probe末尾做验证性读取确认硬件就绪
- SPI驱动核心概念:spi_transfer数组+spi_message实现命令/数据连续传输,高速场景必须用dmam_alloc_coherent分配DMA缓冲区
- 设备树是硬件到驱动的配置桥梁,compatible属性匹配驱动of_match_table,reg属性指定I2C地址或SPI片选索引
- 调试三阶段:软件层面(i2cdetect/spidev_test/dmesg)→电压层面(万用表测上拉电压)→信号层面(逻辑分析仪看波形),按这个顺序逐层排查效率最高