ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux GPIO模拟I2C驱动源码拆解与时序避坑

Linux GPIO模拟I2C驱动源码拆解与时序避坑

简介:这份海思平台GPIO模拟I2C驱动源码面向缺少硬件I2C控制器或需节省外设资源的嵌入式Linux驱动开发者,重点解决用普通GPIO引脚模拟I2C总线通信的问题。压缩包共3个文件,包含1个C源文件、1个头文件和1个Makefile,整体约4KB,分别承载驱动逻辑实现、数据结构与接口声明以及内核模块编译配置,体量轻但结构完整。已有994人学习下载,说明其在相关开发场景中具有一定参考价值。读者可从中了解GPIO请求与方向配置、I2C适配器注册、起始与停止信号生成、地址读写、数据位传输、应答检测及超时错误处理等关键环节,并借助Makefile尝试编译验证。源码通过GPIO高低电平切换还原I2C时序,适合作为理解Linux I2C子系统与GPIO模拟总线实现的入门样例,也便于在缺少硬件控制器的板卡上做移植和二次开发参考。

1. 从一个“扫不到 0x48”的板子说起

去年帮一个做工业网关的团队排查问题:主控自带的两路硬件 I2C 都已经被 EEPROM 和 RTC 占满,项目后期又加了一颗温湿度传感器,物理上没地方挂了。最后的解决办法是用两个闲置 GPIO 写了一套 GPIO 模拟 I2C 驱动源码,在 Linux I2C 子系统里注册出一个新的 adapter,i2cdetect -l里多出来一路i2c-3,i2cdetect -y -r 3才重新扫到 0x48。这套东西适合两类人:一类是硬件设计定型后 I2C 通道不够、又不愿意改板的;另一类是要跟一颗时序很“个性”的从机通信,硬件控制器死活配不出来的。它本质上就是用软件逐位生成 SCL/SDA 波形,代价是 CPU 占用和最高速率,换来的是引脚任意选、时序任意调。

2. 驱动全貌:GPIO 模拟 I2C 在 Linux I2C 子系统里站在哪一层

在动手看代码前,先把位置摆正。Linux 的 I2C 栈是分层的:最上面是i2c-dev暴露的/dev/i2c-N字符设备和各种从机驱动(比如一颗温度传感器的hwmon驱动),中间是i2c-core负责i2c_transfer的分发和从机设备的匹配,最下面是struct i2c_algorithm里的master_xfer,负责真正把数据打到线上。硬件控制器驱动实现的是最下面这一层;GPIO 模拟 I2C 实现的也还是这一层,只不过“打波形”的动作从寄存器变成了gpiod_set_value()。搞清这一点很关键,因为它意味着:上层从机驱动、设备树里的reg = <0x48>、i2c-tools这一整套东西完全不用改,换的只是底座。

2.1 三条路的取舍:控制器驱动、i2c-algo-bit、用户态实现

真到了“I2C 口不够”这一步,其实有三条路可选,我一般按下面的顺序排除。

第一条是加一颗 I2C mux(比如 9548 那种 1 转 8 的),硬件方案最干净,但需要改板,项目后期基本没戏。第二条是内核里的 GPIO 位操作方案,也就是这份源码走的路,用i2c-algo-bit这个通用算法层,或者自己写master_xfer。它的好处是对上层完全透明,从机驱动照常写,设备树照常配,甚至可以在上面挂at24这类现成驱动。第三条是纯用户态实现,用gpiod的字符设备加ioctl自己打时序,好处是调试方便、不用编译内核模块,坏处是每次传输都要进内核拿引脚、调度延迟不可控,几十 kHz 以上就开始飘,而且没法让内核里的从机驱动直接用。

我一般这么选:板子已经定型、只是缺一路低速总线,走第二条;只是临时抓个数据、验证一下从机能不能通信,走第三条。真正产品化的东西不要走第三条,调度一抖动就出玄学问题。

这里还有个小分叉:第二条路里,是直接用内核现成的i2c-gpio.c(就是设备树里写compatible = "i2c-gpio"那个),还是自己写一份?现成的那份当然能用,但很多人拿到这份源码的原因是现成的不够用——比如需要在传输前后加片选控制、需要非标准的时钟拉伸处理、或者内核版本太老没有i2c-gpio的scl-gpios新绑定。自己写一份,等于把这些口子全留着。

2.2 源码目录与文件职责

这份源码的组织方式是很典型的单模块 out-of-tree 驱动,目录不大,但该有的都有:

文件职责
i2c-my-gpio.c驱动主体:引脚抽象、时序实现、adapter 注册与注销
i2c-my-gpio.h私有结构体struct my_i2c_gpio与平台数据的定义
Kconfig提供CONFIG_I2C_MY_GPIO开关,select I2C_ALGOBIT
Makefile单目标编译,也能直接挂进内核树
dts/i2c-my-gpio.dtsi设备树片段,含scl-gpios/sda-gpios与delay-us
tools/i2c-stress.c用户态压测小程序,循环i2cget验证长时间稳定性

有一点值得说:i2c-my-gpio.h里我只放结构体和常量,不放函数原型。因为整个驱动只有.c一个编译单元,跨文件调用几乎没有,把原型摊在头文件里反而增加阅读成本。真正要暴露给别的模块的东西,走EXPORT_SYMBOL_GPL才需要头文件。

2.3 一次读操作的完整调用链

把链路串起来看,读者心里才有地图。用户态执行i2cget -y 3 0x48 0x00 w,走的是这么一条路:

i2cget (用户态) → open("/dev/i2c-3") + ioctl(I2C_RDWR) → i2c-dev.c 里的 i2cdev_ioctl_rdwr() → i2c_transfer(adap, msgs, 2) → __i2c_transfer() 拿 adap->bus_lock → adap->algo->master_xfer(adap, msgs, num) → [路线A] i2c_algo_bit 的 bit_xfer() → [路线B] 我们自己的 my_i2c_xfer() → setscl / setsda / getsda → gpiod_set_value() / gpiod_get_value() → SoC 的 GPIO 控制器寄存器

这条链上最容易出问题的是倒数第二层:gpiod_set_value()本身不保证时序,它只是写寄存器;两次调用之间的间隔由函数调用开销和udelay共同决定。SoC 主频、编译器优化等级、是否开了CONFIG_PREEMPT,都会让实际波形和你在纸上画的差一截。所以这类驱动里udelay给得宁可保守一点,也别抠那几微秒。

另外提醒一句,i2c_transfer是可能睡眠的(内部有 mutex),只能在进程上下文调用。如果你在中断处理函数里直接调它,内核会给你一个BUG: scheduling while atomic,这个是血泪经验,后面避坑章节还会展开。

3. 源码逐段拆解:一条 SCL/SDA 波形是怎么被“造”出来的

这一章是重点。整套驱动拆到最小,其实就是四个动作:把 SCL 拉高拉低、把 SDA 拉高拉低、读 SDA 的电平、读 SCL 的电平。剩下的 START、STOP、ACK、字节收发,全是在这四个动作上按时序拼出来的。

3.1 引脚抽象层:open-drain 的软件模拟

I2C 是开漏总线,器件只能把线拉低,拉高靠上拉电阻。GPIO 默认是推挽输出,直接推挽接上去会出问题:一个器件输出高、另一个输出低,就是短路。所以软件模拟必须做成“输出 0 时是推挽低,输出 1 时切成高阻输入”。

现在的内核里gpiod层已经支持这个语义了,设备树里给引脚加GPIO_OPEN_DRAIN标志,gpiod_set_value(desc, 1)就是释放总线,gpiod_set_value(desc, 0)就是拉低。如果你的平台或内核版本还不支持,就得自己切方向:

/* 私有数据结构:整个驱动就靠这一个结构体传递上下文 */ struct my_i2c_gpio { struct i2c_adapter adap; /* 注册进 I2C 子系统的适配器 */ struct i2c_algo_bit_data bit; /* 路线A 用的算法层数据 */ struct gpio_desc *scl; struct gpio_desc *sda; u32 delay_us; /* 半周期延时,5 表示约 100kHz */ bool sda_open_drain; /* 平台是否支持硬件开漏 */ }; /* SDA 拉低:确定输出 0 */ static inline void sda_low(struct my_i2c_gpio *priv) { if (priv->sda_open_drain) gpiod_set_value(priv->sda, 0); else { gpiod_direction_output(priv->sda, 0); } } /* SDA 释放:支持开漏时写 1 即可,否则切成输入靠上拉拉高 */ static inline void sda_high(struct my_i2c_gpio *priv) { if (priv->sda_open_drain) gpiod_set_value(priv->sda, 1); else gpiod_direction_input(priv->sda); } /* 读 SDA 电平:此时引脚必须处于输入态 */ static inline int sda_get(struct my_i2c_gpio *priv) { return gpiod_get_value(priv->sda); }

这段的逻辑很清楚:sda_low一定让引脚变成输出低电平,sda_high一定让引脚不再驱动总线。参数sda_open_drain是从设备树或平台数据里读出来的,如果板子上的引脚没有硬件开漏能力,就老老实实走gpiod_direction_input/output这条路,多花几次寄存器写,但安全。SCL 一般由主机独占,可以一直保持输出态,不用做这套切换,代码里对应scl_low/scl_high两个更简单的函数。

注意:切换方向是有代价的,gpiod_direction_input()在某些 GPIO 控制器上会触发一阵额外的寄存器操作,实测下来单次开销可能有几百纳秒。如果你的delay_us设到 1 甚至更小,这个开销就会明显把占空比拉歪,波形变得更接近“短低长高”。

3.2 路线 A:只填四个回调,把时序交给 i2c-algo-bit

如果时序要求不特殊,我强烈建议走这条。i2c-algo-bit已经把所有标准时序、重试、超时、总线恢复都写好了,你只要告诉它“怎么拉、怎么读”。

static void my_setscl(void *data, int state) { struct my_i2c_gpio *priv = data; if (state) scl_high(priv); else scl_low(priv); } static void my_setsda(void *data, int state) { struct my_i2c_gpio *priv = data; if (state) sda_high(priv); else sda_low(priv); } static int my_getsda(void *data) { return sda_get((struct my_i2c_gpio *)data); } /* 如果从机支持时钟拉伸,这个回调必须实现,否则 algo-bit 无法等待 */ static int my_getscl(void *data) { struct my_i2c_gpio *priv = data; return gpiod_get_value(priv->scl); } static const struct i2c_algo_bit_data my_bit_data = { .setsda = my_setsda, .setscl = my_setscl, .getsda = my_getsda, .getscl = my_getscl, .udelay = 5, /* 半周期 5us,约 100kHz,probe 里会覆盖 */ .timeout = 200, /* 单位 ms,单次传输超时 */ };

四个回调的语义要记牢:setsda(1)是“释放 SDA 让从机接管”,不是“输出高电平”;getsda返回的是总线真实电平,可能被从机拉低。udelay是半周期,5 微秒对应 100kHz,2.5 微秒对应 400kHz,但后者在大部分国产 SoC 的 GPIO 上已经不太现实了。timeout是单次传输出错时的兜底时间,别设太小,有些从机做内部 EEPROM 写入的时候会把 SCL 拉住几百毫秒。

注册的时候用i2c_bit_add_bus(),它会替你填好adap->algo,你只要把bit.data指回自己的结构体:

static int my_i2c_gpio_probe(struct platform_device *pdev) { struct my_i2c_gpio *priv; int ret; priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL); if (!priv) return -ENOMEM; priv->scl = devm_gpiod_get(&pdev->dev, "scl", GPIOD_OUT_HIGH); if (IS_ERR(priv->scl)) return dev_err_probe(&pdev->dev, PTR_ERR(priv->scl), "no scl gpio\n"); priv->sda = devm_gpiod_get(&pdev->dev, "sda", GPIOD_OUT_HIGH); if (IS_ERR(priv->sda)) return dev_err_probe(&pdev->dev, PTR_ERR(priv->sda), "no sda gpio\n"); device_property_read_u32(&pdev->dev, "i2c-gpio,delay-us", &priv->delay_us); priv->bit = my_bit_data; priv->bit.data = priv; priv->bit.udelay = priv->delay_us; priv->adap.owner = THIS_MODULE; priv->adap.algo_data = &priv->bit; priv->adap.dev.parent = &pdev->dev; priv->adap.dev.of_node = pdev->dev.of_node; strscpy(priv->adap.name, "i2c-my-gpio", sizeof(priv->adap.name)); i2c_set_adapdata(&priv->adap, priv); platform_set_drvdata(pdev, priv); ret = i2c_bit_add_bus(&priv->adap); /* 内部会自动分配总线号 */ if (ret) return dev_err_probe(&pdev->dev, ret, "add bus failed\n"); return 0; }

几个点容易漏:priv->bit = my_bit_data;这里是结构体整体拷贝,不是指针赋值,写完还要重新把.data指回去,否则回调里拿到的是野指针。priv->adap.dev.of_node = pdev->dev.of_node;这一行也不能省,它决定了挂在总线下方的从机节点能不能被正确匹配。i2c_bit_add_bus是动态分配总线号,想在设备树里固定用某一路,得换成i2c_bit_add_numbered_bus。

3.3 路线 B:自己写 START/STOP/字节收发

什么时候需要自己写?我遇到过两种情况:一是从机的地址建立时间很怪,用i2c-algo-bit的节奏它老是 NACK;二是要在一次传输中间插固定长度的空时钟做唤醒。这时候就得硬啃时序,把master_xfer自己填上。

先看起停条件。START 的定义是 SCL 为高期间 SDA 由高变低,STOP 是 SCL 为高期间 SDA 由低变高:

/* 起始条件:SCL 高电平期间,SDA 由高变低 */ static void my_i2c_start(struct my_i2c_gpio *priv) { sda_high(priv); udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); sda_low(priv); /* 关键沿:此时 SCL 仍为高 */ udelay(priv->delay_us); scl_low(priv); } /* 停止条件:SCL 高电平期间,SDA 由低变高 */ static void my_i2c_stop(struct my_i2c_gpio *priv) { sda_low(priv); udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); sda_high(priv); /* 关键沿:此时 SCL 仍为高 */ udelay(priv->delay_us); }

delay_us在这里决定了建立时间和保持时间。I2C 标准要求 START 之后的保持时间至少 4 微秒(标准模式),所以delay_us至少要能覆盖这个数。很多从机对建立时间宽容,对保持时间不宽容,所以我在sda_low之后、scl_low之前这一段一定会加延时,别图快省掉。

写字节和等 ACK 是一体的,第 9 个时钟必须由主机释放 SDA,让从机把它拉低:

/* 返回 0 表示收到 ACK,返回 1 表示 NACK */ static int my_i2c_write_byte(struct my_i2c_gpio *priv, u8 byte) { int i, ack; for (i = 7; i >= 0; i--) { scl_low(priv); if (byte & (1 << i)) sda_high(priv); /* 释放,靠上拉出高电平 */ else sda_low(priv); udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); } /* 第 9 个时钟:主机释放 SDA,采样从机是否拉低 */ scl_low(priv); sda_high(priv); udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); ack = sda_get(priv); /* 0 = ACK, 1 = NACK */ scl_low(priv); return ack; }

读字节反过来,主机负责在第 9 个时钟给出 ACK 或 NACK,读最后一个字节时必须回 NACK,不然从机会一直发下去:

static u8 my_i2c_read_byte(struct my_i2c_gpio *priv, bool ack) { u8 byte = 0; int i; sda_high(priv); /* 释放 SDA,准备接收 */ for (i = 0; i < 8; i++) { udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); byte = (byte << 1) | (sda_get(priv) ? 1 : 0); scl_low(priv); } /* 第 9 个时钟:主机回应 */ if (ack) sda_low(priv); else sda_high(priv); udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); scl_low(priv); sda_high(priv); /* 结束语后总要释放总线 */ return byte; }

把这两个函数拼进master_xfer,就是一套完整的传输逻辑。要注意读写方向、重复起始(I2C_M_NOSTART)、以及num > 1时多个i2c_msg之间到底该发 START 还是重复 START:

static int my_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) { struct my_i2c_gpio *priv = i2c_get_adapdata(adap); int i, j, ret = num; for (i = 0; i < num; i++) { struct i2c_msg *msg = &msgs[i]; bool rd = msg->flags & I2C_M_RD; my_i2c_start(priv); /* 地址 + 读写位,从机回 NACK 直接放弃这一条 */ if (my_i2c_write_byte(priv, (msg->addr << 1) | (rd ? 1 : 0))) { my_i2c_stop(priv); ret = -ENXIO; /* 找不到从机,对应 i2cdetect 的 UU */ break; } if (rd) { for (j = 0; j < msg->len; j++) msg->buf[j] = my_i2c_read_byte(priv, j < msg->len - 1); } else { for (j = 0; j < msg->len; j++) { if (my_i2c_write_byte(priv, msg->buf[j])) { ret = -EIO; break; } } } if (ret < 0) { my_i2c_stop(priv); break; } if (i == num - 1) my_i2c_stop(priv); } return ret; }

ret = num是master_xfer的返回值约定:成功时必须返回处理的i2c_msg数量,不是 0。我第一次写的时候返回了 0,上层一直以为传输失败,疯狂重试。-ENXIO这个错误码也有讲究,i2c-dev会把它翻译成用户态看到的通信失败,i2cdetect靠这个判断地址上有没有设备。-EIO则留给数据阶段的错误。

3.4 两条路线的实测差别

对比项路线 A(i2c-algo-bit)路线 B(自己写 master_xfer)
代码量约 150 行约 350 行
时钟拉伸支持实现getscl即自动支持需要自己在拉高后轮询
总线恢复自带要自己加 9 时钟恢复
最高稳定速率100kHz 左右视优化程度,可到 200kHz
适配怪时序只能调udelay可任意插时钟、改建立时间

大多数项目到 100kHz 就够了,传感器、EEPROM、RTC 这类外设根本跑不满。400kHz 是另一个世界,udelay(2)在开了CONFIG_PREEMPT的内核上经常被调度器打断,实测波形抖得厉害,得上udelay加local_irq_save那套,但那样又会拉高中断延迟。我一般会先把频率压到 100kHz,稳住再说。所以默认推荐路线 A,除非确实有非标时序要处理。表格里那个“200kHz”是在主频比较高、延迟恒定的平台上测的,换个平台不保证。

4. 跑起来:从编译到上板验证的完整流程

代码看懂了,接下来是把它变成能跑的东西。这个驱动的编译方式有两种,挂进内核树或者单独编模块,我一般先用单独编模块的方式验证,改一行编一次,几秒钟出结果,比重新烧内核快得多。

4.1 Kconfig 与 Makefile 接入

如果打算长期使用,还是老老实实挂进内核树,放到drivers/i2c/busses/下面:

config I2C_MY_GPIO tristate "GPIO bit-banging I2C bus adapter (custom)" depends on I2C && GPIOLIB select I2C_ALGOBIT help Say Y here if you want to use two GPIO pins as an I2C bus. This is useful when the SoC's hardware I2C controllers are already occupied. If unsure, say N.

select I2C_ALGOBIT这一行不能少,路线 A 依赖它。depends on GPIOLIB也是必须的,不然在没开 GPIO 框架的配置下会编译失败。

Makefile 就一行:

obj-$(CONFIG_I2C_MY_GPIO) += i2c-my-gpio.o

单独编模块的话,在源码目录里放一个更简单的 Makefile:

# 交叉编译示例,KDIR 指向你已经编译过的内核源码树 KDIR ?= /path/to/kernel ARCH ?= arm CROSS_COMPILE ?= arm-linux-gnueabihf- PWD := $(shell pwd) obj-m += i2c-my-gpio.o all: $(MAKE) -C $(KDIR) M=$(PWD) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

KDIR必须指向一棵已经配置并编译过的内核树,里面要有Module.symvers,否则加载时会报Unknown symbol。这是新手最容易踩的一脚:拿一份干净的源码目录去编模块,编译能过,insmod就翻车。

4.2 设备树节点怎么写

引脚号怎么算是个老问题。绝大部分 SoC 的 GPIO 是分组编号的,设备树里用<&gpio1 12 GPIO_ACTIVE_HIGH>这种形式,12是组内偏移,不是全局编号。具体哪一组、哪个脚能用,得对着原理图和引脚复用表查,没有捷径。

/* 挂在根节点下,或者任意合适的父节点里 */ i2c_gpio: i2c-gpio { compatible = "my,i2c-gpio"; #address-cells = <1>; #size-cells = <0>; /* 新绑定:scl-gpios / sda-gpios */ scl-gpios = <&gpio1 12 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; sda-gpios = <&gpio1 13 (GPIO_ACTIVE_HIGH | GPIO_OPEN_DRAIN)>; i2c-gpio,delay-us = <5>; /* 半周期 5us,约 100kHz */ clock-frequency = <100000>; /* 从机就挂在这里,和用硬件 I2C 时写法完全一样 */ sensor@48 { compatible = "my,temp-sensor"; reg = <0x48>; }; };

GPIO_OPEN_DRAIN这个标志要看平台支持情况,支持的话写上去最省事,驱动里那句sda_open_drain就能取到真值;不支持就换成GPIO_ACTIVE_HIGH,驱动自动走切方向那条路。老内核用的是gpios = <&gpio1 12 0>, <&gpio1 13 0>;加上i2c-gpio,scl/i2c-gpio,sda的写法,如果你在移植老代码,注意这两套绑定的差别。

还有一件事必须确认:这两个脚的 pinctrl 有没有被别的外设占着。有些 SoC 默认把这些脚复用成 PWM 或者 UART,设备树里不加pinctrl-names/pinctrl-0覆盖的话,写 GPIO 寄存器完全没反应,示波器上一条直线。这种情况我踩过,排查了两个小时,最后发现是 pinctrl 没配。

4.3 上板验证:从 dmesg 到逻辑分析仪

模块编译好之后,推上板子加载:

# 加载驱动,看 probe 有没有成功 insmod i2c-my-gpio.ko dmesg | tail -20 # 正常会看到 i2c i2c-3: i2c-my-gpio: bus registered 之类的字样 # 列出当前所有 I2C 适配器,确认多出来一路 i2cdetect -l # 扫描总线,-r 表示用读方式探测,兼容性更好 i2cdetect -y -r 3 # 读一个 16 位寄存器,验证数据通路 i2cget -y 3 0x48 0x00 w

i2cdetect的输出是最直观的健康检查。全--说明总线上一个设备都没响应;全UU说明地址被内核驱动占着,是正常现象,说明驱动层已经匹配上了;某个地址显示数字说明从机应答了。如果扫描时整块都是乱的、时有时无,先别急着改代码,拿万用表和示波器量一下空闲时 SCL 和 SDA 的电平,两者都应该是高。任一为低,先解决硬件问题。

没有逻辑分析仪的时候,可以用i2c-stress.c那种循环压测的小工具跑个几千次,看有没有偶发失败。偶发失败是最难搞的,往往是时序余量不足或者上拉电阻偏大导致的边沿太缓。有分析仪的话,抓一次 START 到 STOP 的完整波形,量三件事:SCL 高电平宽度、START 之后的保持时间、SDA 在 SCL 高电平期间是否稳定。这三项合格,基本就没问题了。

5. 避坑与排查:调 I2C 时序最容易翻车的五个点

这一章写的是我在这类驱动上真实踩过的坑,按“现象 → 原因 → 解决”整理。每一条都对应一个具体的排查动作,遇到问题时可以直接照着比对。

5.1 现象:i2cdetect 全超时,示波器上 SCL 有波形但 SDA 一直低

原因通常是两个:一是上拉电阻缺失或阻值太大。I2C 标准模式一般用 4.7k,快速模式用 2.2k,有些板子为了省电用了 10k,配上长走线和大的引脚电容,上升沿能慢到几微秒,从机根本采不到高电平。二是 SDA 被从机拉死在低电平,比如从机在传输中途掉电,或者上一次传输被异常中断,从机还在等第 9 个时钟。

解决:先量空闲电平,SDA 为低就说明被拉死,补 9 个时钟让它把剩下的位吐完,再发一个 STOP。这个恢复逻辑后面二次开发那章会给代码。上拉阻值的问题改板子最彻底,改不了就在驱动里把delay-us调大,给上升沿多点时间。我见过把delay-us从 5 改到 20 才好使的板子,代价是频率掉到 25kHz,但功能稳了。

5.2 现象:能读到数据,但跑几分钟就开始偶发 NACK,重启又好了

这多半是时序余量不够。delay_us = 2在大部分平台上名义上是 200kHz 以上,但udelay的精度受限于定时器粒度,实际可能是 1.6 微秒也可能是 3 微秒,占空比一歪,从机就认不出来了。另外开了CONFIG_PREEMPT之后,进程可能在任何位置被抢占,一个几十微秒的调度延迟就能把整个字节打乱。

解决:把delay_us加到 5 甚至 10,先把稳定性拿到手;如果对速度有要求,在my_i2c_write_byte的位循环外面套local_irq_save/restore,把整个字节的时序保护起来,代价是中断延迟变大,要评估。还有一个隐蔽的点:gpiod_get_value()在某些控制器上会走 I2C 或者 SPI 总线去读,那就不只是慢的问题了,根本不能在位循环里用。查一下 GPIO 控制器的实现,确认它是内存映射的。

5.3 现象:第一次上电 probe 成功,reboot 之后 i2cdetect 全是--

原因往往不在驱动,而在引脚复用状态。有些平台的 bootloader 会把引脚配成别的功能,内核启动时如果设备树里没有明确覆盖 pinctrl,引脚就停在 bootloader 留下的状态。还有一种情况是电源域没打开,GPIO 控制器本身没上电,gpiod_get返回了合法的描述符但写寄存器没反应。

解决:在设备树节点里显式加pinctrl-names = "default"; pinctrl-0 = <&i2c_gpio_pins>;,并且在 pinctrl 节点里把两个脚配成 GPIO 功能、开漏或上拉。然后在probe里打印一次gpiod_get之后的方向和电平,用cat /sys/kernel/debug/gpio对照看,一眼就能看出引脚到底被谁占着。

5.4 现象:在中断处理函数里调用i2c_transfer,内核直接报scheduling while atomic

这个不是玄学,是设计使然。i2c_transfer内部要拿adap->bus_lock这个 mutex,mutex 在原子上下文里是不能拿的。同理,驱动里大量的udelay虽然不睡眠,但i2c_transfer本身会睡。

解决:把 I2C 访问挪到工作队列或者线程化的中断下半部里做。如果确实需要在中断里快速读一个寄存器,考虑用i2c_smbus_read_byte_data的原子版本,或者干脆用 GPIO 直接读状态脚,别走 I2C。我在一个项目里见过把传感器读取塞进 timer 回调的写法,跑一段时间必然崩,改成workqueue之后彻底消停。

5.5 现象:多个从机挂在同一路,其中一个出问题后整条总线都瘫了

I2C 是共享总线,任何一个从机把 SDA 拉死,主机就再也发不出 START。这在多从机场景里很常见,尤其是那种热插拔的模块,拔的瞬间正好在传输中途。

解决:在每次master_xfer之前做一次总线健康检查,检测 SDA 是否为低,是就触发恢复流程。另外把timeout设合理,别设成 0 或者特别大,让出错能快速返回。还有个小技巧:给每个从机在设备树里加独立的status属性,出问题的那个可以单独status = "disabled"屏蔽掉,剩下的继续工作,不用整机重启。

6. 二次开发:把这套驱动改成能跨板子复用的通用位操作 I2C

这份源码拿来直接用没问题,但真要落到多个项目上,我建议做三件事:把引脚操作彻底gpiod化、补上总线恢复、把时序参数全部提到设备树。

第一件事是去掉所有gpio_*老接口。老接口用的是全局 GPIO 编号,换个平台编号全变,驱动得跟着改。gpiod_*用描述符,编号的事交给设备树,驱动里一行不用动。迁移的时候注意gpio_request要换成devm_gpiod_get,gpio_direction_output换成gpiod_direction_output,gpio_set_value换成gpiod_set_value,语义基本一一对应,但gpiod多了一层 active-low 的处理,设备树里写GPIO_ACTIVE_LOW的话,gpiod_set_value(desc, 1)实际输出的是低电平,这点要特别注意,别把这层搞反了。

第二件事是补总线恢复。这段代码不长,但能救很多现场:

/* 从机把 SDA 拉死时,主机补最多 9 个时钟让它把当前字节吐完 */ static void my_i2c_bus_recover(struct my_i2c_gpio *priv) { int i; sda_high(priv); /* 先释放 SDA */ udelay(priv->delay_us); for (i = 0; i < 9 && !sda_get(priv); i++) { scl_low(priv); udelay(priv->delay_us); scl_high(priv); udelay(priv->delay_us); } my_i2c_stop(priv); /* 补一个 STOP,把总线状态复位 */ }

把它挂在master_xfer入口,或者做成adap->algo->functionality之外的一个 sysfs 触发点,出问题时手动敲一下。参数9是因为一个字节最多 8 位加 1 个 ACK,9 个时钟足够任何从机把当前传输走完。循环条件里的!sda_get(priv)是关键,一旦 SDA 被释放就提前退出,不用死等满 9 个。

第三件事是把参数表拉出来,全部走设备树,别在代码里写死:

参数设备树属性典型值说明
半周期延时i2c-gpio,delay-us55us 约 100kHz,越大越稳越慢
传输超时i2c-gpio,timeout-ms200从机写 EEPROM 时要留够
开漏支持GPIO_OPEN_DRAIN视平台不支持则自动切方向
总线恢复次数i2c-gpio,recover-clocks9一般不用改

验证方法很简单:同一份.ko分别推到两块引脚不同的板子上,只改设备树不改代码,i2cdetect都能扫到设备,就说明移植性做到位了。我一般还会在probe里打一行日志,把实际用的引脚号、延时、总线号全打出来,现场排查时看一眼 dmesg 就知道配置有没有生效。

最后说个习惯。这套驱动我后来又在三个项目里用过,每次都是同一套流程:先在单独编模块的环境下跑通i2cdetect,再拿示波器量三个关键时序参数,最后用压测工具跑满一万次不出错才合进主线。从那以后我每次碰位操作总线,都强制先量波形再看代码——因为软件上看起来完全正确的时序,落到引脚上可能是另一个样子。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进