Linux I2C主从驱动开发实战:从分层架构到probe调试

发布时间:2026/10/26 14:51:26
Linux I2C主从驱动开发实战:从分层架构到probe调试 1. 从“能通”到“能用”I2C主从驱动到底在解决什么问题很多人第一次接触Linux下的I2C都是从i2c-tools开始的。i2cdetect -y 1扫到地址i2cget读一个寄存器i2cset写一个值看到数据回来了就觉得“I2C搞定了”。但真正把一颗传感器、一颗EEPROM或者一块PMIC接进产品里你会发现i2cget能读通只是万里长征第一步——内核里那个i2c_adapter是怎么把时序发出去的、i2c_client是怎么和具体芯片绑定的、从设备地址为什么有时候要左移一位、probe函数里拿到的client-addr到底是不是你写在设备树里的那个值这些问题一个都绕不开。这篇内容聊的就是I2C驱动分层里最核心的一层主控制器驱动adapter驱动和从设备驱动client驱动。前者负责“怎么把电信号按时序打出去”后者负责“这颗芯片的寄存器怎么读写、数据怎么解释”。两者通过I2C核心层i2c-core解耦这个分层设计是整个I2C子系统能同时支撑几十种主控和上千种从设备的根本原因。适合谁来读如果你已经能跑通一个简单的i2c-dev用户态读写想搞清楚内核里i2c_adapter、i2c_algorithm、i2c_client、i2c_driver这几个结构体之间的咬合关系想自己写一个主控驱动或者给一颗新芯片写从设备驱动那这篇就是给你准备的。我会把分层逻辑、关键结构体、设备树匹配、probe时序、以及实际调试中最容易翻车的几个点拆开讲尽量做到看完能对着代码复现。先说一个贯穿全文的核心认知I2C驱动写得好不好八成取决于你对“谁在什么时候拥有总线”这件事理解得清不清楚。主控驱动管的是总线时序和仲裁从设备驱动管的是“轮到我说话时说什么”中间任何一层把睡眠、锁、时序搞错表现出来就是-ETIMEDOUT或者-ENXIO而且极难定位。后面所有章节其实都在围绕这个认知展开。2. 分层架构拆解adapter、algorithm、client、driver四者的咬合关系2.1 为什么I2C子系统要拆成“主控”和“从设备”两层先想一个最朴素的问题如果不用分层直接让每颗芯片的驱动自己去操作SoC的I2C寄存器发时序会怎样答案是——换一颗主控所有从设备驱动全部重写。这在现实里完全不可接受因为同一颗温度传感器可能挂在多种主控上同一颗主控又要接十几种外设。Linux的解法是把“发时序”这件事抽象成i2c_algorithm由主控驱动实现把“和某颗芯片对话”抽象成i2c_driver由从设备驱动实现中间用i2c_adapter代表一条物理总线用i2c_client代表挂在这条总线上的一个设备实例。核心层i2c-core.c负责把client和driver匹配起来并在需要传输时调用adapter背后的algorithm。这个拆法的直接好处是主控驱动只需要实现master_xfer或者smbus_xfer从设备驱动只需要调i2c_transfer、i2c_smbus_read_byte_data这类封装好的接口两边互不知道对方的存在。你写从设备驱动时完全不用关心底下是DesignWare的控制器还是GPIO模拟的控制器。2.2 四个核心结构体的职责边界把职责边界理清楚后面看代码就不会迷路。我用一张表把四个结构体的关键字段和职责列出来结构体代表什么关键字段谁来实现i2c_adapter一条物理I2C总线algo、nr、dev、timeout、retries主控驱动注册i2c_algorithm总线传输算法master_xfer、smbus_xfer、functionality主控驱动实现i2c_client挂在总线上的一个设备addr、adapter、dev、flags核心层根据设备树/板级信息创建i2c_driver一类设备的驱动probe、remove、id_table、driver.of_match_table从设备驱动注册这里有个容易混淆的点i2c_client不是驱动创建的是核心层在解析设备树或者板级i2c_board_info时创建的。驱动通过id_table或者of_match_table去“认领”已经存在的client认领成功才调probe。所以你在probe里拿到的client-addr是核心层早就填好的不是驱动自己算的。2.3 一次传输的完整调用链理解调用链比背结构体有用得多。假设从设备驱动调了一次i2c_transfer(adapter, msgs, 2)内核里大致发生这些事i2c-core检查adapter-algo-master_xfer是否存在不存在直接返回-EOPNOTSUPP。对每条msg做地址合法性检查确认msg-addr在7位或10位地址范围内。获取总线锁adapter-bus_lock防止多设备并发抢总线。调用adapter-algo-master_xfer(adapter, msgs, num)进入主控驱动。主控驱动把msg翻译成寄存器操作发START、写地址W、写寄存器、重复START、写地址R、读数据、发STOP。返回实际完成的msg数量核心层据此判断成功与否。这条链里第4步是主控驱动的全部战场第1到3步是核心层兜底第5步的时序正确性决定了成败。很多-ETIMEDOUT其实是第5步里START或ACK没处理好而不是从设备没响应。3. 主控制器驱动master_xfer里藏着的时序细节3.1 一个最小可用的master_xfer长什么样主控驱动的核心就是实现master_xfer。不同SoC的寄存器布局差别很大但骨架高度一致。下面这段是基于常见控制器寄存器模型写的示意代码重点看流程而不是具体寄存器偏移static int demo_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct demo_i2c *i2c i2c_get_adapdata(adap); int i, ret 0; for (i 0; i num; i) { struct i2c_msg *msg msgs[i]; bool is_read msg-flags I2C_M_RD; /* 1. 发START等待控制器空闲 */ ret demo_wait_idle(i2c); if (ret) goto out; demo_send_start(i2c); /* 2. 发地址字节7位地址左移一位最低位是读写方向 */ demo_write_byte(i2c, (msg-addr 1) | (is_read ? 1 : 0)); ret demo_wait_ack(i2c); if (ret) { demo_send_stop(i2c); goto out; } /* 3. 按方向搬数据 */ if (is_read) ret demo_read_bytes(i2c, msg-buf, msg-len); else ret demo_write_bytes(i2c, msg-buf, msg-len); /* 4. 最后一条msg才发STOP中间的用重复START */ if (i num - 1) demo_send_stop(i2c); } out: return ret ? ret : num; }这段代码里有两个细节值得单独拎出来说。第一地址左移i2c_msg里的addr是7位地址但总线上传输的是8位7位地址1位读写方向所以必须 1。第二STOP的时机只有最后一条msg才发STOP中间的msg之间用重复STARTrepeated start这是读寄存器这类“写地址再读数据”操作能成立的前提。如果你在每条msg后面都发STOP读操作会失败因为从设备在STOP后已经释放了内部地址指针。3.2 functionality回调不是摆设i2c_algorithm里除了master_xfer还有个functionality回调返回这个控制器支持哪些能力比如是否支持SMBus块读、是否支持10位地址、是否支持I2C_FUNC_NOSTART。这个返回值不是给用户看的是给核心层和上层驱动做能力判断用的。举个实际例子某颗PMIC驱动里用了i2c_smbus_read_i2c_block_data如果主控的functionality没声明I2C_FUNC_SMBUS_I2C_BLOCK核心层会直接返回-EOPNOTSUPP而不是让主控去硬发。所以写主控驱动时functionality要如实填写别图省事全返回I2C_FUNC_I2C否则上层用SMBus接口时会莫名其妙失败。3.3 超时与重试timeout和retries怎么定i2c_adapter里有timeout和retries两个字段。timeout单位是jiffies表示单次传输等待控制器完成的最长时间retries表示传输失败后的重试次数。这两个值不是随便填的。timeout的估算逻辑一次I2C传输的耗时约等于(起始开销 字节数 × 9位 × 位周期) × 消息数。标准模式100kHz下每位10微秒快速模式400kHz下每位2.5微秒。假设一次读16字节400kHz理论耗时约16 × 9 × 2.5 ≈ 360微秒加上START/STOP和ACK开销留10倍余量也就几毫秒。所以timeout设成msecs_to_jiffies(100)通常足够设太大反而会让真正卡死的传输拖很久才报错。retries一般设1到3。设0意味着一次失败直接返回适合对时序极其敏感、重试会引入副作用的场景设3适合总线干扰较大的板子。我个人的经验是调试阶段先设0把失败暴露出来量产配置再按实测误码率调到1或2。一上来就设3会把间歇性的时序问题掩盖掉。4. 从设备驱动probe里那些设备树不会告诉你的坑4.1 id_table与of_match_table的双轨匹配从设备驱动注册时核心层要判断“这个driver该不该管这个client”。判断依据有两条路老式的id_table基于i2c_device_id的名字匹配和新式的of_match_table基于设备树compatible匹配。两条路是“或”的关系任一匹配成功就会调probe。static const struct i2c_device_id demo_id[] { { demo-sensor, 0 }, { } }; MODULE_DEVICE_TABLE(i2c, demo_id); static const struct of_device_id demo_of_match[] { { .compatible vendor,demo-sensor }, { } }; MODULE_DEVICE_TABLE(of, demo_of_match); static struct i2c_driver demo_driver { .driver { .name demo-sensor, .of_match_table demo_of_match, }, .probe demo_probe, .remove demo_remove, .id_table demo_id, };这里有个高频坑设备树里的compatible写的是vendor,demo-sensor但id_table里写的是demo-sensor两者不一致时如果of_match_table漏了或者写错probe根本不会进来。现象是驱动加载成功、/sys/bus/i2c/drivers/下能看到driver目录但设备没绑定。排查方法后面第5章会讲。4.2 probe里第一件事确认client-addr很多新手在probe里直接用client-addr去算寄存器地址结果发现读出来的数据全是0xFF。原因往往是设备树里的reg属性和芯片手册里的地址对不上——有的是7位地址有的是8位地址含读写位。设备树里reg 0x48这个0x48是7位地址核心层填进client-addr的也是0x48。但有些芯片手册给的地址是0x908位形式即0x481如果你照着手册写reg 0x90核心层会认为这是个10位地址或者非法地址行为就不可预期了。判断方法7位地址范围是0x08到0x77超过0x77的基本可以确定是8位形式需要右移一位。probe里可以加一句防御性检查if (client-addr 0x08 || client-addr 0x77) { dev_err(client-dev, invalid 7-bit addr 0x%02x\n, client-addr); return -EINVAL; }4.3 寄存器读写封装别在probe里裸调i2c_transfer从设备驱动里读写寄存器优先用regmap或者i2c_smbus_*系列封装而不是自己拼i2c_msg调i2c_transfer。原因有三一是regmap自带缓存和位域操作二是SMBus封装处理了大小端和块传输细节三是裸调i2c_transfer容易在并发场景下漏锁。一个典型的寄存器读封装static int demo_read_reg(struct demo_chip *chip, u8 reg, u8 *val) { int ret i2c_smbus_read_byte_data(chip-client, reg); if (ret 0) return ret; *val ret; return 0; }注意i2c_smbus_read_byte_data返回的是int负数是错误码非负才是数据。直接强转成u8会把-ENXIO变成0xF3之类的值看起来像正常数据非常隐蔽。这个坑我在早期项目里踩过调了半天以为是芯片问题。4.4 中断与轮询的取舍很多传感器支持“数据就绪”中断probe里要不要申请中断取决于采样率和功耗要求。高采样率场景用中断避免轮询占CPU低功耗场景反而适合“配置阈值中断平时睡眠”。但要注意I2C传输本身不能在中断上下文里做因为i2c_transfer可能睡眠。正确做法是在中断处理里唤醒工作队列或线程化中断在进程上下文里完成I2C读取。static irqreturn_t demo_irq(int irq, void *data) { struct demo_chip *chip data; /* 只做标记实际读取放到工作队列 */ schedule_work(chip-work); return IRQ_HANDLED; }如果图省事在硬中断里直接调i2c_smbus_read_byte_data轻则报scheduling while atomic重则死机。5. 调试实战从-ENXIO到probe不进的完整排查链路5.1 先分清是“总线不通”还是“驱动没绑”遇到设备不工作第一步不是改代码是确认问题出在哪一层。按这个顺序查i2cdetect -y bus能不能扫到设备地址。扫不到问题在主控或硬件上拉电阻、供电、地址配置扫得到进第2步。ls /sys/bus/i2c/devices/看有没有对应地址的目录比如1-0048。没有说明设备树没解析出client检查reg和status。ls /sys/bus/i2c/drivers/driver名/看设备有没有绑定到驱动。没有说明匹配失败检查compatible和id_table。以上都正常但probe没打印检查probe返回值可能是probe里某步失败被静默返回了。这个顺序的价值在于它把“硬件层、设备树层、驱动匹配层、probe逻辑层”四层分开每层都有明确的观测点不会一上来就瞎改代码。5.2 一个真实的匹配失败案例某次给一颗EEPROM写驱动设备树写的是i2c1 { eeprom50 { compatible vendor,demo-eeprom; reg 0x50; }; };驱动里of_match_table写的是vendor,demo-eepromid_table写的是demo-eeprom。加载后i2cdetect能扫到0x50/sys/bus/i2c/devices/1-0050存在但/sys/bus/i2c/drivers/demo-eeprom/下没有绑定。查dmesg没有任何probe打印。排查过程先cat /sys/bus/i2c/devices/1-0050/name显示demo-eeprom再cat /sys/bus/i2c/devices/1-0050/of_node/compatible显示vendor,demo-eeprom。两者都对为什么没匹配最后发现是驱动模块加载顺序问题——设备树解析创建client时驱动还没加载等驱动加载时核心层的匹配逻辑走了id_table路径而id_table里的名字是demo-eepromclient的name也是demo-eeprom理论上应该匹配上。真正的原因是i2c_device_id的匹配是精确字符串比较但client的name在设备树场景下可能被改写成1-0050这种形式。解决办法是在id_table里同时保留demo-eeprom并确保of_match_table正确让匹配走OF路径。改完后probe正常进入。这个案例说明双轨匹配不是冗余是容错两条路都写对才稳。5.3 -ETIMEDOUT的三种典型成因-ETIMEDOUT是I2C调试里出现频率最高的错误码但成因差别很大现象可能原因排查手段所有设备都超时主控时钟没使能、引脚复用没配对查clock和pinctrl配置量SCL波形单个设备超时从设备供电异常、地址错、上拉缺失量该设备VCC和上拉电阻i2cdetect确认地址偶发超时总线电容过大、上拉阻值偏大、多主竞争示波器看上升沿减小上拉阻值或降速第三种最折磨人。曾经有一块板子常温下一切正常高温老化时偶发超时。最后定位是上拉电阻用了10k总线挂了8个设备等效电容偏大高温下上升沿变缓400kHz下采样点踩到边沿。把上拉改成4.7k、速率降到100kHz后稳定。经验设备多、走线长、速率高三者占两样就要重新算上拉和速率。5.4 用逻辑分析仪抓一次传输软件层查不出问题时抓波形是最快的。一次标准的“写寄存器地址再读一个字节”应该看到START、地址W、ACK、寄存器地址、ACK、重复START、地址R、ACK、数据、NACK、STOP。任何一步缺失或ACK变NACK问题就定位了。重点看两个地方重复START是否真的没有夹STOP以及最后一个字节后主机是否发了NACK。前者错会导致读操作读到错误地址后者错会导致从设备继续驱动数据线总线卡死。6. 并发、电源管理与那些文档里不写的经验6.1 总线锁与设备锁不是一回事i2c_transfer内部会拿adapter-bus_lock保证同一时刻只有一个传输在总线上。但这把锁保护的是“总线”不是“设备”。如果两个线程同时操作同一颗芯片的不同寄存器总线锁能防止时序交错但不能防止寄存器读改写竞争。比如线程A读配置寄存器、改bit0、写回线程B在中间改了bit1A写回时就把B的修改覆盖了。解决办法是在驱动里加设备级互斥锁mutex_lock(chip-io_lock); ret demo_read_reg(chip, REG_CFG, val); val | BIT(0); ret demo_write_reg(chip, REG_CFG, val); mutex_unlock(chip-io_lock);这个锁的粒度要覆盖“读-改-写”整个序列只包单次读写没用。6.2 runtime PM下I2C传输的坑启用runtime PM的设备在suspend后I2C控制器可能被关时钟。如果驱动在suspend状态下还调i2c_transfer会超时。正确做法是在传输前pm_runtime_get_sync传输后pm_runtime_put_autosuspend。但要注意pm_runtime_get_sync可能睡眠不能在原子上下文调用这和前面中断里不能直接I2C传输是同一个道理。6.3 设备树里那些容易写错的属性除了reg和compatible还有几个属性经常出问题clock-frequency写在主控节点下单位Hz。写了400000但硬件只支持100k会通信失败。i2c-scl-falling-time-ns/i2c-scl-rising-time-ns影响时序计算不写会用默认值极端情况下需要按实际波形填。status okay忘了写或者写成disabled整个节点不生效i2cdetect扫不到。6.4 我个人的几条硬经验第一新板子第一次上电先用i2cdetect确认所有预期地址都在再动驱动代码。硬件问题伪装成软件问题的情况太多了。第二probe里每个可能失败的分支都加dev_err别用dev_dbg。调试阶段dev_dbg默认不打印等于没加。第三从设备驱动的remove要和probe严格对称申请了中断就释放申请了时钟就关注册了设备节点就注销。不对称的后果是模块反复加载卸载后资源泄漏表现为第二次加载probe失败。第四别迷信i2c_smbus_*的返回值类型统一用int接判断 0再处理这个习惯能省掉大量隐蔽bug。第五多设备共用总线时优先降速而不是加大上拉。降速对时序余量的改善是线性的改上拉还要考虑功耗和驱动能力副作用更多。这套主从驱动的分层逻辑说到底就是把“时序”和“语义”彻底分开主控驱动对波形负责从设备驱动对数据含义负责核心层做撮合。把这条线理清楚再复杂的I2C问题都能拆到某一层去定位而不是在一堆-ETIMEDOUT里打转。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询