深入嵌入式I2C驱动开发:从协议到代码的实战指南

在一块小小的MCU板子上,你可能只看到两根细线——SDA和SCL,却连接着温度传感器、EEPROM、RTC、OLED屏幕……它们安静地挂在I2C总线上,默默传递数据。这看似简单的“两根线”,背后是一整套精密的通信机制与复杂的软件架构。

如果你曾为 i2cdetect 扫不到设备而抓狂,或因ACK失败反复检查焊接;如果你写过设备树却不知为何驱动不绑定——那么本文就是为你准备的。我们将 绕开教科书式的罗列 ,用工程师的视角,带你穿透I2C控制器驱动的本质:从硬件时序到内核框架,从寄存器配置到调试秘籍,一步步构建完整的认知链条。


为什么是I2C?它真的只是“两根线”那么简单吗?

现代嵌入式系统对资源极其敏感。以一个智能手环为例:空间有限、功耗苛刻、引脚紧张。此时,SPI需要至少4根线(CS/SCK/MOSI/MISO),UART点对点通信效率低,而 I2C仅需SDA+SCL两根线即可接入多个外设 ,成为首选。

但别被它的简洁迷惑。I2C不是“接上线就能通”的玩具协议。它的半双工特性、开漏输出设计、多主仲裁机制,决定了其软硬件协同的复杂性。尤其当你面对的是Linux内核中的I2C子系统时,你会发现:

驱动开发 ≠ 直接操作寄存器
而是理解“谁在控制、如何抽象、怎样注册”的分层逻辑。

我们先抛开代码,回到最根本的问题:一次成功的I2C通信,到底经历了什么?


I2C通信全过程拆解:从起始信号到停止条件

想象你在敲门进入一间办公室:
- 敲门动作 = 起始条件(Start)
- 报出姓名 = 发送地址 + 读写位
- 对方应答“请进” = ACK
- 交谈内容 = 数据传输
- 道别离开 = 停止条件(Stop)

I2C正是这样一套“礼貌对话”机制。

关键帧结构一览

阶段 说明
Start SCL高电平时,SDA由高变低
Address + R/W 7位地址左移一位,最低位填0(写)或1(读)
ACK/NACK 接收方在第9个时钟周期拉低SDA表示确认
Data Bytes 每字节后都需ACK,支持连续传输
Repeated Start 不发送Stop,直接开始新事务,用于读写切换
Stop SCL高电平时,SDA由低变高

举个常见场景:读取AT24C02 EEPROM中偏移0x00的数据。

[START] → [0xA0] → [0x00] → [REPEATED START] → [0xA1] → [DATA] → [STOP]
   ↓        ↓         ↓             ↓              ↓         ↓
 写设备    写地址   应答成功      读设备        应答成功   返回数据

这个过程看似简单,但在底层,每一个边沿变化都要精确控制。谁来负责生成这些时序?答案是—— I2C控制器 。


I2C控制器的角色:你是总线上的“交通指挥官”

每个SoC内部都有一个或多个I2C控制器模块(也叫适配器)。比如STM32的I2C1、全志A64的TWI0。它们的作用不是“通信”,而是 产生符合规范的电气信号与时序 。

你可以把它看作交警:
- 红绿灯节奏 = SCL时钟频率
- 允许哪辆车通行 = 地址寻址
- 处理堵车冲突 = 多主仲裁
- 应急处理 = Clock Stretching(从机拉低SCL请求延时)

而在Linux中,这套“交通管理系统”被抽象成了三层架构。


Linux I2C子系统:三层模型如何协作?

不要一上来就看源码。我们用一个比喻来理解:

  • I2C Core :城市交通管理局,制定规则、登记车辆、分配路线
  • Adapter Driver :某个路口的红绿灯控制系统,具体执行放行指令
  • Client Driver :一辆车上的人,知道自己要去哪里,但依赖红绿灯通行

分层职责清晰划分

层级 职责 典型实现
Core 统一接口管理、设备探测、适配器注册 i2c-core.c
Adapter 控制器初始化、时钟设置、 master_xfer 实现 sunxi_twi.c
Client 针对外设功能编程,如读温湿度 sht30.c

这种分层让不同厂商可以复用同一套生态。例如,无论你是用瑞芯微还是NXP的芯片,只要实现了标准adapter接口,就能使用现有的SHT30驱动。


编写你的第一个I2C控制器驱动:关键步骤详解

现在我们切入实战。假设你要为一款基于Allwinner平台的新板卡编写I2C驱动,该控制器名为TWI0。

第一步:获取硬件资源(内存、时钟、中断)

所有控制器都是内存映射的外设。你需要知道它的基地址、时钟源、中断号。

通常通过设备树传入:

twi0: i2c@01c2ac00 {
    compatible = "allwinner,sun50i-a64-twi";
    reg = <0x01c2ac00 0x400>;
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&ccu CLK_BUS_TWI0>, <&ccu CLK_TWI0>;
    clock-names = "bus", "twi";
    pinctrl-names = "default";
    pinctrl-0 = <&twi0_pins>;
    status = "disabled";
};

驱动加载时,会自动解析这些信息。

第二步:初始化控制器硬件

这是最关键的一步。主要包括:

  1. 使能时钟
  2. 映射寄存器空间
  3. 配置GPIO复用
  4. 设置通信速率

示例代码片段:

static int sunxi_i2c_probe(struct platform_device *pdev)
{
    struct sunxi_i2c *i2c;
    struct resource *res;

    i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);
    if (!i2c)
        return -ENOMEM;

    /* 获取寄存器地址 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    i2c->regs = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(i2c->regs))
        return PTR_ERR(i2c->regs);

    /* 获取并开启时钟 */
    i2c->clk = devm_clk_get(&pdev->dev, "twi");
    clk_prepare_enable(i2c->clk);

    /* 获取APB总线时钟用于波特率计算 */
    i2c->apb_clk = devm_clk_get(&pdev->dev, "bus");

    /* 请求中断 */
    i2c->irq = platform_get_irq(pdev, 0);
    devm_request_irq(&pdev->dev, i2c->irq, sunxi_i2c_irq, 0, 
                     dev_name(&pdev->dev), i2c);

    /* 初始化adapter结构 */
    i2c->adap.owner = THIS_MODULE;
    i2c->adap.algo = &sunxi_i2c_algo;     // 核心算法
    i2c->adap.dev.parent = &pdev->dev;
    i2c->adap.nr = pdev->id;
    strscpy(i2c->adap.name, "sunxi-i2c", sizeof(i2c->adap.name));
    i2c_set_adapdata(&i2c->adap, i2c);

    /* 注册到I2C core */
    ret = i2c_add_numbered_adapter(&i2c->adap);
    if (ret) {
        clk_disable_unprepare(i2c->clk);
        return ret;
    }

    platform_set_drvdata(pdev, i2c);
    return 0;
}

注意这里调用了 i2c_add_numbered_adapter() —— 它会触发内核扫描此总线上所有已声明的I2C设备,并尝试匹配驱动。


实现 master_xfer :驱动的心脏

这个函数是整个控制器驱动的核心,负责完成一组消息传输( struct i2c_msg *msgs )。

典型的流程如下:

static int sunxi_i2c_xfer(struct i2c_adapter *adap,
                          struct i2c_msg *msgs, int num)
{
    struct sunxi_i2c *i2c = i2c_get_adapdata(adap);
    int ret, i;

    mutex_lock(&i2c->lock);

    for (i = 0; i < num; i++) {
        ret = sunxi_i2c_do_transfer(i2c, &msgs[i]);
        if (ret) {
            dev_dbg(&adap->dev, "transfer failed at msg %d\n", i);
            break;
        }
    }

    mutex_unlock(&i2c->lock);
    return (i == num) ? num : -EIO;
}

其中 sunxi_i2c_do_transfer() 要处理:
- 启动条件
- 发送地址并等待ACK
- 连续发送/接收数据
- 处理Re-start
- 最终Stop

如果使用中断模式,还需设置completion或wait_queue等待完成。


设备怎么上总线?设备树说了算

很多初学者困惑:“我设备明明焊好了,为什么系统找不到?” 很大可能是设备没在设备树里声明。

正确的做法是在对应I2C节点下添加子设备:

&twi0 {
    status = "okay";
    clock-frequency = <400000>;  /* 400kHz */

    sht30: sht30@44 {
        compatible = "sensirion,sht30";
        reg = <0x44>;
    };

    oled: oled@3c {
        compatible = "solomon,ssd1306fb";
        reg = <0x3c>;
        vdd-supply = <&reg_dc1sw>;
    };
};

只要compatible匹配已有驱动,内核就会自动创建 i2c_client 并调用 .probe 函数。

⚠️ 小贴士: reg 是7位地址!不要写成0x88(那是8位格式)


客户端驱动示例:SHT30温湿度传感器实战

我们来看一个真实客户端驱动的关键部分:

static int sht30_probe(struct i2c_client *client,
                       const struct i2c_device_id *id)
{
    struct iio_dev *indio_dev;
    struct sht30_data *data;

    indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data));
    if (!indio_dev)
        return -ENOMEM;

    data = iio_priv(indio_dev);
    data->client = client;

    /* 设置IIO回调 */
    indio_dev->info = &sht30_iio_info;
    indio_dev->name = "sht30";
    indio_dev->modes = INDIO_DIRECT_MODE;

    /* 发送单次测量命令 */
    int ret = i2c_smbus_write_word_data(client, 0x2C, 0x06);
    if (ret < 0)
        return ret;

    msleep(20); /* 等待转换完成 */

    /* 读取6字节原始数据 */
    ret = i2c_master_recv(client, data->buffer, 6);
    if (ret != 6)
        return -EIO;

    /* 解析数据 */
    u16 temp_raw = (data->buffer[0] << 8) | data->buffer[1];
    u16 humi_raw = (data->buffer[3] << 8) | data->buffer[4];

    dev_info(&client->dev, "温度: %.3f°C, 湿度: %.1f%%",
             temp_raw * 175.0 / 65535.0 - 45.0,
             humi_raw * 100.0 / 65535.0);

    return devm_iio_device_register(&client->dev, indio_dev);
}

static const struct of_device_id sht30_of_match[] = {
    { .compatible = "sensirion,sht30" },
    { }
};
MODULE_DEVICE_TABLE(of, sht30_of_match);

module_i2c_driver(sht30_driver);

几个重点:
- 使用 i2c_smbus_write_word_data() 发送命令(适用于支持SMBus的设备)
- i2c_master_recv() 是同步读取,适合小数据量
- 结合IIO子系统,可通过 /sys/bus/iio/devices/ 访问数据


常见问题排查手册:老司机的经验都在这儿了

🔧 问题1: i2cdetect -y 1 扫不到任何设备

可能原因:
- 上拉电阻缺失(SDA/SCL始终为低)
- 地址错误(误将8位地址当作7位)
- 设备未供电或焊接不良
- 设备树中 status="disabled"

排查方法:
1. 用万用表测SDA/SCL电压是否约为VDD(有上拉才会拉高)
2. 查芯片手册确认地址(如SHT30默认0x44,非0x88)
3. 检查 .dts 中 status = "okay"

📉 问题2:ACK Failure频繁出现

典型表现: 日志报 NACK from device 或传输中途断开

解决方案:
- 增加启动后的延迟(某些传感器需几百ms初始化)
- 降低I2C速率(尝试从400kHz降到100kHz)
- 检查电源稳定性(电压波动可能导致从机复位)

🌀 问题3:总线锁死,SCL被拉低无法恢复

现象: 总线挂起,后续通信全部失败

恢复手段:
- 软件模拟9个时钟脉冲(强制从机释放总线):

for (int i = 0; i < 9; i++) {
    gpio_set_value(scl_gpio, 0);
    udelay(5);
    gpio_set_value(scl_gpio, 1);
    udelay(5);
}
  • 硬件复位从设备
  • 重启I2C控制器

性能与可靠性设计建议

✅ 上拉电阻怎么选?

经验值: 4.7kΩ 是通用选择。

更精确计算要考虑:
- 总线电容(PCB走线+引脚输入电容,一般≤400pF)
- 上升时间要求(标准模式tr ≤ 1000ns)

公式:
$$
R_{pull} \geq \frac{t_r}{0.8 \times C_{bus}}
\quad \text{(单位:欧姆)}
$$

例如:$ t_r = 300ns, C_{bus}=200pF $ → $ R ≈ 1.875kΩ $,可选2.2kΩ。

⚡ 速率匹配原则

总线上所有设备必须支持相同的速度模式。若有一个只支持100kbps,则整个总线只能运行在此速率。

建议:
- 开发阶段统一设为100kHz
- 成熟后根据设备规格提升至400kHz

🔍 调试利器推荐

工具 用途
逻辑分析仪 (Saleae/DSView) 抓取真实波形,查看Start/ACK/数据
i2c-tools i2cdetect , i2cget , i2cset 快速验证
内核调试选项 CONFIG_I2C_DEBUG_CORE=y 输出详细日志
示波器 观察信号完整性、上升沿陡峭度

写在最后:I2C的未来不止于“两根线”

虽然I2C诞生于上世纪80年代,但它仍在进化。新一代的 MIPI I3C 正在崛起:
- 支持高达12.5 Mbps速率
- 动态地址分配
- 命令码机制减少冗余传输
- 向下兼容I2C设备

然而,目前绝大多数嵌入式项目仍基于传统I2C。掌握其驱动开发能力,意味着你能:
- 自主移植新型传感器
- 优化通信稳定性
- 构建低功耗传感网络
- 快速定位硬件问题

更重要的是,它教会你一种思维方式: 如何将物理世界的电信号,转化为可编程、可调度、可维护的软件抽象。

下次当你看到那两根细细的导线,请记住——它们承载的不只是数据,还有整个嵌入式系统的呼吸节律。

如果你正在调试某个I2C设备遇到了难题,欢迎在评论区留言,我们一起分析波形、解读手册、找出那个藏得最深的bug。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐