Linux GPIO模拟I2C驱动源码拆解与时序避坑
发布时间:2026/10/28 7:40:30 锦皓数字建站

简介这份海思平台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 子系统里注册出一个新的 adapteri2cdetect -l里多出来一路i2c-3i2cdetect -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_ALGOBITMakefile单目标编译也能直接挂进内核树dts/i2c-my-gpio.dtsi设备树片段含scl-gpios/sda-gpios与delay-ustools/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约 100kHzprobe 里会覆盖 */ .timeout 200, /* 单位 ms单次传输超时 */ };四个回调的语义要记牢setsda(1)是“释放 SDA 让从机接管”不是“输出高电平”getsda返回的是总线真实电平可能被从机拉低。udelay是半周期5 微秒对应 100kHz2.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 还是重复 STARTstatic 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 两条路线的实测差别对比项路线 Ai2c-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 SoCs 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) cleanKDIR必须指向一棵已经配置并编译过的内核树里面要有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 时写法完全一样 */ sensor48 { 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 wi2cdetect的输出是最直观的健康检查。全--说明总线上一个设备都没响应全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这个 mutexmutex 在原子上下文里是不能拿的。同理驱动里大量的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_getgpio_direction_output换成gpiod_direction_outputgpio_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 个 ACK9 个时钟足够任何从机把当前传输走完。循环条件里的!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再拿示波器量三个关键时序参数最后用压测工具跑满一万次不出错才合进主线。从那以后我每次碰位操作总线都强制先量波形再看代码——因为软件上看起来完全正确的时序落到引脚上可能是另一个样子。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。