
简介一套面向海思平台的通用输入输出口模拟I2C总线驱动源码包主要为了解决在没有硬件I2C控制器或希望节省芯片资源时的通信需求。压缩包共3个文件包括一个C语言源文件、一个头文件以及一个Makefile构建脚本整体大小仅为4KB代码非常精简。头文件定义了关键的数据结构和函数接口源文件实现了设备探测与移除、消息收发、应答检测以及各类时序控制Makefile用于驱动模块的编译与链接。资料对起始信号、停止信号、读写时序、超时与错误处理均有清晰实现适合具备一定Linux驱动基础、希望深入理解模拟I2C协议栈的开发者参考学习。目前已有991人浏览学习源码结构完整、逻辑清晰可直接对照分析并移植到相似平台便于嵌入式调试和二次开发。1. 海思平台为什么需要 GPIO 模拟 I2C从引脚不够到时序可控第一次拿到海思平台的评估板时我对着原理图数了半天 I2C 总线主控的硬件 I2C 控制器一共就那几路一路给了图像传感器一路挂在音频芯片剩下的要么被占用要么需要和别的外设多路复用。而手头的 EEPROM 和温湿度传感器还等着挂总线。硬件设计改不了唯一能做的就是从 GPIO 里挤出两个脚用软件把 I2C 时序拉出来。GPIO 模拟 I2Cbit-banging正是干这个的拿出任意两个空闲 GPIO分别当作 SCL 和 SDA依靠驱动里的延时和方向切换把 START、数据位、ACK、STOP 一步步“搓”出来。这篇文章不打算只贴一段源码我会把模拟 I2C 的时序原理、海思平台上的驱动实现、对接从设备的方法以及最容易翻车的几个地方一次讲透。适合刚接触海思 BSP、想自己维护一路模拟 I2C 的工程师也适合被串行总线问题追着跑的老手。2. 模拟 I2C 的时序基础与 GPIO 选型先让逻辑在示波器上成立写代码之前先把时序逻辑在纸上拉一遍这一步省不了。GPIO 模拟 I2C 的本质是把硬件控制器内部的状态机用代码重放一遍。你对时序边沿定义得越清楚后面在示波器上排查问题就越省事。很多人上来就抄代码结果对着波形找不出问题在哪就是因为没有把 I2C 协议拆成最基础的边沿事件。2.1 I2C 时序四要素START、STOP、ACK、数据位怎么模拟I2C 总线的空闲态是 SCL 和 SDA 都保持高电平。起始条件 START 的定义是在 SCL 为高时将 SDA 从高拉低停止条件 STOP 则相反在 SCL 为高时将 SDA 从低拉高。数据位传输时主机必须在 SCL 低电平期间改变 SDA 电平然后在 SCL 拉高后保持稳定从机在 SCL 高电平期间采样。ACK 则发生在第 9 个时钟此时主机释放 SDA从机把 SDA 拉低表示应答。对应到 GPIO 操作先定义一组宏后面所有函数都建立在它们之上#define I2C_SCL_H() gpio_set_value(scl_gpio, 1) #define I2C_SCL_L() gpio_set_value(scl_gpio, 0) #define I2C_SDA_H() gpio_set_value(sda_gpio, 1) #define I2C_SDA_L() gpio_set_value(sda_gpio, 0) #define I2C_SDA_IN() gpio_direction_input(sda_gpio) #define I2C_SDA_OUT() gpio_direction_output(sda_gpio, 1) #define I2C_SDA_READ() gpio_get_value(sda_gpio)这里有个容易被忽略的细节I2C_SDA_OUT()除了把引脚切成输出还会把 SDA 默认拉到高。这样在模拟“释放总线”时先把方向切成输出并置高再切输入SDA 不会出现短时间的悬空。而真正读从机数据时SDA 要靠外部上拉电阻维持高电平所以方向切换的先后顺序会影响波形质量。START 和 STOP 用了两次半周期延时保证 SCL 高电平时 SDA 能稳定跳变static void i2c_start(void) { I2C_SDA_H(); I2C_SCL_H(); i2c_half_delay(); I2C_SDA_L(); i2c_half_delay(); I2C_SCL_L(); } static void i2c_stop(void) { I2C_SDA_L(); I2C_SCL_H(); i2c_half_delay(); I2C_SDA_H(); i2c_half_delay(); }写一个数据位是在 SCL 低电平期间修改 SDA再拉高 SCL 让从机采样最后拉低 SCL 结束这个位static void i2c_write_bit(int bit) { I2C_SDA_OUT(); if (bit) I2C_SDA_H(); else I2C_SDA_L(); i2c_half_delay(); I2C_SCL_H(); i2c_half_delay(); I2C_SCL_L(); i2c_half_delay(); }读一个数据位则相反主机先释放 SDA切输入再拉高 SCL采样最后拉低 SCLstatic int i2c_read_bit(void) { int val; I2C_SDA_IN(); I2C_SCL_H(); i2c_half_delay(); val I2C_SDA_READ(); I2C_SCL_L(); i2c_half_delay(); I2C_SDA_OUT(); return val; }逻辑看起来很简单但实际跑起来方向切换、上拉电阻、延时选择都会影响结果。第 3 章会把完整驱动串起来第 5 章会集中讲坑。2.2 海思怎么操作 GPIO内核 gpio 接口与寄存器方式的差异海思平台的内核里有两条操作 GPIO 的路线。一条是标准内核接口gpio_request、gpio_direction_output、gpio_set_value这是大多数 BSP 驱动的默认选择。另一条是直接 ioremap GPIO 寄存器通过读写寄存器值翻转引脚常见于对时序要求很高的模拟逻辑。在驱动里我一般优先用标准接口原因只有一个可以快速换引脚。开发阶段经常需要把 SCL 或 SDA 换到另一个 GPIO 上标准接口只要改 module_param 传进来的引脚号不用改代码。而寄存器方式需要查具体型号的 IO 地址、位偏移、读写锁换一块板子可能还得重写一遍。标准接口的缺点是慢gpio_set_value内部有锁和调用链但模拟 100kbps 的 I2C 完全够用。一个典型初始化片段如下static int scl_gpio 23; static int sda_gpio 24; module_param(scl_gpio, int, 0644); module_param(sda_gpio, int, 0644); static int gpio_i2c_init_gpio(void) { if (gpio_request(scl_gpio, i2c-scl)) return -EBUSY; if (gpio_request(sda_gpio, i2c-sda)) return -EBUSY; gpio_direction_output(scl_gpio, 1); gpio_direction_output(sda_gpio, 1); return 0; }参数说明scl_gpio和sda_gpio是模块加载时传入的 GPIO 编号编号要查板子原理图和芯片 GPIO 手册不是随便填。初始化时把两个引脚都配置为输出且默认输出 1是为了让总线进入空闲态。如果这步做反了SDA 和 SCL 一上来就是低电平整个总线直接被“锁死”。有些海思平台的老 SDK 里没有完整的设备树GPIO 可能已经被 pinmux 复用成其他功能。这种情况gpio_request不会报错但引脚电平就是不对需要回到平台驱动里确认 mux 寄存器。第 5.5 节会专门讲。2.3 电平与上拉电阻模拟 I2C 翻车的第一现场GPIO 模拟 I2C 和硬件 I2C 控制器最大的区别是硬件控制器内部有真正的开漏输出GPIO 通常没有。GPIO 只能靠“输出 0”和“切换输入”来模拟 SDA 被释放此时 SDA 必须依赖外部上拉电阻回到高电平。如果板子上没有焊上拉读 SDA 永远读到 0驱动会以为从机一直在响应或者一直不响应。上拉电阻的选择要根据总线电平来。海思平台 IO 电平常见的是 1.8V 和 3.3V参考经验值如下总线电平推荐上拉阻值现象与调整1.8V1kΩ ~ 2.2kΩ阻值太大上升沿会拖得很慢3.3V2.2kΩ ~ 4.7kΩ常用 2.2kΩ短总线很稳5V少见4.7kΩ ~ 10kΩ注意 GPIO 耐压一般要电平转换拿到板子后先不要写驱动用示波器量一下 SCL 和 SDA 的空闲电平确认两个引脚都是高。然后手动把 SDA 拉低再放开看上升沿时间。上升沿超过 1us模拟 400kbps 基本没戏超过 3us连 100kbps 都可能要加大延时。这个检查只需要 5 分钟能避开一半以上的模拟 I2C 玄学问题。注意如果你的板子上没有上拉电阻软件写得再完美也没用。SDA 在输入状态下会漂读到的数据全看环境噪声。3. 从零写一个内核态 GPIO 模拟 I2C 驱动核心源码与参数说明这一章给出可以抄作业的驱动骨架。我选择注册一个 misc 设备而不是一上来就做一个标准的 I2C adapter。原因是 misc 设备最快能跑通调试阶段只需要 insmod 后打开/dev/gpio_i2c就能验证时序不需要牵扯 i2c-core 的设备模型。等确认从设备能正常通信再考虑把代码升级为标准 adapter这部分放到第 6 章。3.1 驱动框架与设备注册misc 设备三步走misc 设备在内核里是“小而杂”设备不需要自己在 /dev 下建节点内核会在注册时自动创建。完整流程三步模块加载时申请 GPIO实现 file_operations用misc_register注册。先定义用户态和内核态共用的请求结构体和 ioctl 命令这是 ioctl 传参的载体struct gpio_i2c_req { unsigned char addr; /* 7位从机地址 */ unsigned char reg; /* 寄存器/字地址 */ unsigned char __user *buf; /* 数据缓冲区 */ size_t len; /* 数据长度 */ }; #define GPIO_I2C_MAGIC 0x49 #define GPIO_I2C_WRITE _IOW(GPIO_I2C_MAGIC, 0x01, struct gpio_i2c_req) #define GPIO_I2C_READ _IOWR(GPIO_I2C_MAGIC, 0x02, struct gpio_i2c_req)然后写 fops 和注册代码static int gpio_i2c_open(struct inode *inode, struct file *filp) { if (!gpio_is_valid(scl_gpio) || !gpio_is_valid(sda_gpio)) return -ENODEV; return 0; } static long gpio_i2c_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { struct gpio_i2c_req req; int ret 0; if (copy_from_user(req, (void __user *)arg, sizeof(req))) return -EFAULT; switch (cmd) { case GPIO_I2C_WRITE: ret gpio_i2c_write_request(req); break; case GPIO_I2C_READ: ret gpio_i2c_read_request(req); break; default: return -ENOTTY; } if (copy_to_user((void __user *)arg, req, sizeof(req))) return -EFAULT; return ret; } static const struct file_operations gpio_i2c_fops { .owner THIS_MODULE, .open gpio_i2c_open, .unlocked_ioctl gpio_i2c_ioctl, }; static struct miscdevice gpio_i2c_miscdev { .minor MISC_DYNAMIC_MINOR, .name gpio_i2c, .fops gpio_i2c_fops, }; static int __init gpio_i2c_init(void) { int ret; ret gpio_i2c_init_gpio(); if (ret) return ret; ret misc_register(gpio_i2c_miscdev); if (ret) { gpio_free(scl_gpio); gpio_free(sda_gpio); return ret; } return 0; } module_init(gpio_i2c_init);逻辑说明copy_from_user先把用户态传进来的struct gpio_i2c_req拷贝到内核但其中的buf仍然是指向用户态地址的指针后续读写数据时还需要再解引用一次。unlocked_ioctl是现在主流的 ioctl 实现不需要 BKL 锁。misc_register成功后/dev/gpio_i2c节点会自动出现不需要 mknod。3.2 时序核心源码字节读写与 START/STOP/ACK 的 C 实现有了前面的宏和 start/stop剩下的就是把字节、ACK、多字节读取拼起来。这段是整篇最核心的代码建议照着敲一遍不要直接复制然后当黑匣子用。static int i2c_delay 10; /* SCL半周期延时单位us */ module_param(i2c_delay, int, 0644); static void i2c_half_delay(void) { udelay(i2c_delay); } /* 发送一个字节返回0表示收到ACK返回负值表示从机无应答 */ static int i2c_write_byte(unsigned char byte) { int i, ack; for (i 7; i 0; i--) i2c_write_bit((byte i) 0x01); /* 第9个时钟释放SDA让从机拉低表示ACK */ I2C_SDA_IN(); I2C_SCL_H(); i2c_half_delay(); i2c_half_delay(); ack (I2C_SDA_READ() 0) ? 0 : -EIO; I2C_SCL_L(); i2c_half_delay(); I2C_SDA_OUT(); return ack; } /* 读取一个字节last_nak为1表示这是最后一个字节主机要回NAK */ static unsigned char i2c_read_byte(int last_nak) { int i; unsigned char byte 0; I2C_SDA_IN(); for (i 0; i 8; i) { I2C_SCL_H(); i2c_half_delay(); byte (byte 1) | I2C_SDA_READ(); I2C_SCL_L(); i2c_half_delay(); } /* 主机在最后一个字节回NAK否则回ACK */ if (last_nak) i2c_write_bit(1); else i2c_write_bit(0); return byte; }i2c_write_byte里有个容易看漏的点在I2C_SDA_IN()之后连续调用了两次i2c_half_delay()。原因是最后一个数据位结束时SDA 还停留在主机驱动的电平上。如果最后一位是 0SDA 被拉低此时切换输入后外部上拉需要时间把它拉回高电平然后从机才有机会拉低它给出 ACK。延时不加ACK 采样点就会落在 SDA 还没稳定的那个窗口里波形看着对程序却经常报 -EIO。多字节读写请求是直接给用户态用的。写请求是START 设备地址(W) 寄存器地址 数据然后 STOP。读请求则是START 设备地址(W) 寄存器地址 repeated START 设备地址(R) 读数据 STOPstatic int gpio_i2c_write_request(struct gpio_i2c_req *req) { unsigned char *kbuf; int ret, i; if (req-len 64) return -EINVAL; kbuf kmalloc(req-len, GFP_KERNEL); if (!kbuf) return -ENOMEM; if (copy_from_user(kbuf, req-buf, req-len)) { kfree(kbuf); return -EFAULT; } i2c_start(); ret i2c_write_byte((req-addr 1) | 0); /* 写方向 */ if (ret) goto out; ret i2c_write_byte(req-reg); if (ret) goto out; for (i 0; i req-len; i) { ret i2c_write_byte(kbuf[i]); if (ret) goto out; } i2c_stop(); out: kfree(kbuf); return ret; } static int gpio_i2c_read_request(struct gpio_i2c_req *req) { unsigned char *kbuf; int ret, i; if (req-len 64) return -EINVAL; kbuf kmalloc(req-len, GFP_KERNEL); if (!kbuf) return -ENOMEM; i2c_start(); ret i2c_write_byte((req-addr 1) | 0); /* 先写寄存器地址 */ if (ret) goto out; ret i2c_write_byte(req-reg); if (ret) goto out; i2c_start(); /* repeated START */ ret i2c_write_byte((req-addr 1) | 1); /* 读方向 */ if (ret) goto out; for (i 0; i req-len; i) kbuf[i] i2c_read_byte(i req-len - 1 ? 1 : 0); i2c_stop(); if (copy_to_user(req-buf, kbuf, req-len)) ret -EFAULT; out: kfree(kbuf); return ret; }参数说明req-addr是 7 位 I2C 从机地址例如 EEPROM 是 0x50。驱动内部要把它左移一位最低位表示方向。req-len限制在 64 字节以内是为了和内核栈的简单交互保持一致真要读写一页 256 字节可以改大kmalloc长度但每笔事务占用 GPIO 的时间会更长容易被上层任务打断。3.3 关键参数延时设计、上拉电阻与时钟拉伸探测整个模拟 I2C 驱动里最值得花时间调的是i2c_delay。它决定了 SCL 半周期长度直接对应总线速率。标准 100kbps 模式要求低电平周期不小于 4.7us所以半周期取 5us 左右400kbps 模式要求低电平不小于 1.3us半周期取 2us。但由于海思平台 GPIO 操作本身有开销udelay(5)实际拉出来的低电平可能已经接近 6~7us所以建议用示波器实测后再定。参数经验默认值调整方向坑i2c_delay10us100kbps示波器实测后修正改太小会丢 ACKscl_gpio板级引脚号通过 module_param 传入必须避开已复用引脚sda_gpio板级引脚号通过 module_param 传入同上timeout1000 次循环时钟拉伸超时传感器测量时可能等几百 ms如果对接的是带测量状态的传感器比如温湿度芯片从机在测量期间会把 SCL 拉低这就是时钟拉伸。主机必须在每个时钟高电平期间检查 SCL 是否被拉低拉低就等待不能继续发下一个时钟。对应实现如下static int i2c_wait_scl_high(void) { int timeout 1000; I2C_SCL_IN(); while (timeout--) { if (I2C_SCL_READ()) return 0; udelay(1); } return -ETIMEDOUT; }启用这个函数后i2c_write_bit和读位流程里的I2C_SCL_H()后面要跟进i2c_wait_scl_high()。注意I2C_SCL_IN()和I2C_SCL_OUT()的方向切换是有开销的所以在不需要时钟拉伸的 EEPROM 场景下别开这个功能白白拖慢时序。3.4 编译与加载Makefile 和 insmod 后的首测驱动代码放在海思 SDK 提供的内核源码树外编译需要一个 Makefile。KERNEL_DIR 指到 SDK 里的内核目录交叉编译器由 SDK 环境变量导出obj-m : gpio_i2c.o KERNEL_DIR : /opt/hisilicon/kernel/linux-4.9 ARCH : arm CROSS_COMPILE : arm-himix100-linux- all: $(MAKE) -C $(KERNEL_DIR) M$(PWD) ARCH$(ARCH) CROSS_COMPILE$(CROSS_COMPILE) modules clean: $(MAKE) -C $(CROSS_COMPILE) M$(PWD) clean注意CROSS_COMPILE前缀要和你 SDK 自带工具链一致不同 SDK 名字不一样用ls /opt/hisilicon/确认。编译得到gpio_i2c.ko后拷贝到板子上加载insmod gpio_i2c.ko scl_gpio23 sda_gpio24 i2c_delay10 ls -l /dev/gpio_i2c dmesg | tail -20如果gpio_request失败dmesg会打印具体的 GPIO 编号这时候先检查这个脚是不是被别的驱动占用了。加载成功且设备节点存在后不要急着接传感器先用示波器或逻辑分析仪抓一次i2c_start和i2c_stop确认 SDA、SCL 的边沿和空闲电平都正常。这一步能让你把“驱动问题”和“外部硬件问题”彻底分开。4. 用 GPIO 模拟 I2C 对接真实从设备EEPROM 与温湿度传感器的读写验证驱动能 insmod、设备节点能打开只是第一步。要证明这套模拟 I2C 真能用必须挂一颗真实从设备做读写回环。我一般先拿 EEPROM 验证写读一致性再用温湿度传感器验证多次读和时钟拉伸场景。4.1 找一颗最廉价的从设备EEPROM 读写流程最常见的 EEPROM 从机地址是 0x507 位地址驱动内部左移后变成 0xA0/0xA1。写流程是START 设备地址(W) 字地址 数据STOP读流程是START 设备地址(W) 字地址 repeated START 设备地址(R) 读数据STOP。用户态测试程序可以直接用 ioctl 封装好的接口不需要关心底层时序#include stdio.h #include fcntl.h #include sys/ioctl.h #include string.h #include unistd.h struct gpio_i2c_req { unsigned char addr; unsigned char reg; unsigned char *buf; size_t len; }; #define GPIO_I2C_MAGIC 0x49 #define GPIO_I2C_WRITE _IOW(GPIO_I2C_MAGIC, 0x01, struct gpio_i2c_req) #define GPIO_I2C_READ _IOWR(GPIO_I2C_MAGIC, 0x02, struct gpio_i2c_req) int main(void) { int fd open(/dev/gpio_i2c, O_RDWR); struct gpio_i2c_req req; char wbuf[] hello; char rbuf[8] {0}; if (fd 0) { perror(open); return 1; } /* 写5个字节到字地址0 */ req.addr 0x50; req.reg 0x00; req.buf (unsigned char *)wbuf; req.len 5; if (ioctl(fd, GPIO_I2C_WRITE, req) 0) { perror(write); return 1; } usleep(10000); /* EEPROM内部写周期 */ /* 从字地址0读回5个字节 */ req.reg 0x00; req.buf (unsigned char *)rbuf; req.len 5; if (ioctl(fd, GPIO_I2C_READ, req) 0) { perror(read); return 1; } printf(read: %.5s\n, rbuf); close(fd); return 0; }这段代码里的usleep(10000)不能省。EEPROM 完成一次页写入后内部还需要一段时间把数据固化到存储单元如果立刻发读命令读回来的可能是旧数据或者全 0xFF。不同芯片这个时间不一样常见的是 5ms 左右取 10ms 比较稳妥。4.2 对接温湿度传感器一次读多个字节的时序坑温湿度传感器和 EEPROM 不一样很多型号要求在发起测量命令后等待转换完成期间从机会拉伸 SCL。如果驱动没有处理时钟拉伸主板发过来的时钟会在测量期间撞上从机的“刹车”导致通信失败。现象是第一次读能通第二次回读全是 0xFF或者偶发无响应。多字节读取还有一个容易错的地方最后一个字节主机必须回 NAK然后才能发 STOP。如果驱动在最后一个字节之后仍然回 ACK从机可能会继续驱动 SDA和你发的 STOP 产生总线竞争。多数传感器的数据手册写得很清楚但代码里这一步经常被忽略。在 3.2 节的i2c_read_byte(int last_nak)里last_nak1已经处理了这个问题。如果你的传感器还要求“写一个命令寄存器然后读多字节”就复用gpio_i2c_read_request的结构把命令字节放在req-reg里如果传感器不需要寄存器地址、直接读就行可以把req-reg当成一个任意固定值或者新增一个不带 reg 的 ioctl 命令。我一般会在驱动里再补一个GPIO_I2C_READ_NO_REG避免为这种传感器硬凑一个寄存器号。4.3 验证步骤从 dmesg 到数据回读全流程完整的验证流程分三步。第一步确认硬件环境SDA、SCL 空闲电平为高上拉电阻已焊示波器探头挂在引脚上。第二步加载驱动并跑用户态测试先写 EEPROM 再读确认读回内容一致。第三步如果 EEPROM 通了但传感器不通优先用逻辑分析仪抓 START 到 STOP 的完整波形数一下时钟个数和 ACK 位置。insmod gpio_i2c.ko scl_gpio23 sda_gpio24 i2c_delay10 ./gpio_i2c_eeprom_test dmesg | tail -10如果读回的数据每字节都是 0xFF先怀疑上拉和 SDA 读方向如果第一个字节对、后面不对去查 ACK 采样点如果换一个 GPIO 就不工作去查 pinmux。这一套排查顺序能覆盖绝大多数模拟 I2C 失败场景。5. GPIO 模拟 I2C 避坑手册5 条踩坑记录与排查方法模拟 I2C 的坑不只在驱动代码里更多在硬件设计、内核配置和并发环境里。下面记几条我反复遇到的每一条都按“现象 - 原因 - 解决”来写。5.1 现象SDA 上升沿像一条缓坡原因上拉电阻没焊或阻值过大第一次调 GPIO 模拟 I2C 时我在示波器上看到 SCL 挺正常SDA 的上升沿却是一段接近 2us 的斜线逻辑分析仪能解出来但只要把速率提到 400kbps 就完全乱码。原因是板子的 SDA 上拉电阻没有焊只靠芯片内部非常弱的上拉在工作。后来补了一个 2.2kΩ 上拉波形立刻变成垂直沿。如果你也遇到类似情况不要急着调i2c_delay。先量 SDA 从 0 到 1 的上升时间如果超过 1us问题基本都在上拉。解决方法是换小阻值上拉或者检查 PCB 上这个网络的寄生电容。I2C 总线拉得太长、过孔太多也会让上升沿变慢这是硬件决定的软件加延时只能治标。5.2 现象第一字节能通、后续字节全乱原因ACK 后 SDA 方向没切回来这是一个很隐蔽的代码问题。i2c_write_byte在发完 8 个数据位后会切 SDA 为输入采样完 ACK 后如果忘记把 SDA 切回输出下一字节的第一个数据位就会写不进去因为 SDA 一直处于输入状态主机根本拉不动它。表现就是从机能 ACK 第一个设备地址但后续字节它完全收不到逻辑分析仪上看 SDA 在第二个字节期间纹丝不动。解决方法是严格按照 3.2 节代码的顺序I2C_SDA_IN()采样 ACKSCL 拉低后立即I2C_SDA_OUT()再继续下一字节。如果代码里用了不同函数拼装务必把方向切换检查一遍。这也解释了为什么很多人第一次抄网上的模拟 I2C 代码会翻车抄的片段漏了I2C_SDA_OUT()。5.3 现象传感器偶发无响应原因时钟拉伸没处理温湿度传感器在启动测量后会把 SCL 拉低直到内部转换完成。如果驱动完全没做时钟拉伸检测只是按固定udelay往前发时钟从机等于在被强制打断通信自然失败。这个故障不是每次都出现因为传感器转换时间有波动读得快可能刚好躲过去读得慢一定翻车。解决方法是启用 3.3 节的i2c_wait_scl_high()在每次 SCL 拉高后检查是否真的变高。注意检查后要有一个超时退出否则从机故障永久拉低 SCL驱动会死循环。超时时间我习惯设 100ms 左右因为正常传感器的转换时间很少超过这个值。5.4 现象在中断或定时器里调用驱动直接挂死原因GPIO 操作在原子上下文模拟 I2C 驱动里的gpio_set_value在部分内核配置下会导致睡眠因为它可能走 I2C 扩展芯片或需要拿互斥锁。如果在中断上下文直接调用 ioctl 或封装的写读函数系统可能直接死锁或 oops。我第一次在定时器里调用读函数时板子一跑就重启回头看崩溃栈全是 gpio 锁。解决方法是把 I2C 事务放到工作队列或线程里中断里只置一个标志位由内核线程统一处理。如果一定要同步访问可以改用gpio_raw_set_value这类原子接口但要确认目标 GPIO 是真的内存映射引脚不是挂在总线扩展芯片上的。5.5 现象同一套源码在另一块板子上波形完全不对原因pinmux 复用冲突海思平台的引脚复用非常灵活同一个物理引脚可能被配置成 UART、SPI、PWM 或 GPIO。某块板子的内核设备树里这个引脚默认被配成其他功能你的驱动虽然gpio_request成功了但 pinmux 还在别人手里实际驱动不了电平。现象是代码没问题示波器却量不到波或者引脚电平被其他外设来回拉。解决方法是先查板级 dts 或 SDK 的 pinmux 表格确认这两个 GPIO 没有被其他节点占用然后在内核启动日志里搜pinmux相关配置。更稳妥的做法是直接用 SDK 提供的himm工具在 uboot 阶段改写 mux 寄存器或者通过设备树 pinctrl 把两个引脚配成 GPIO 功能。这一步属于硬件平台定制换一块板子必须重新查。6. 进阶把模拟 I2C 接入 Linux I2C 子系统让 i2c-tools 直接操作MISC 设备方案始终是“单机版”驱动只对你自己的 ioctl 负责。如果你想在设备树里挂一个传感器驱动或者用i2cdetect、i2cget这些标准工具来调试就需要把模拟 I2C 升级成内核 I2C 子系统里的一个 adapter。好消息是内核本来就提供了现成的算法不需要自己把时序再写一遍。6.1 用 i2c-algo-bit 复用内核算法从手写时序到两行注册内核的 i2c-algo-bit 模块实现了完整的 bit-banging 算法你只需要提供 setsda、setscl、getsda、getscl 四个回调。它内部会处理大多数边界情况比手写折腾 ACK 方向安全得多。接入方法如下#include linux/i2c-algo-bit.h #include linux/i2c.h static struct i2c_adapter gpio_i2c_adapter; static struct i2c_algo_bit_data gpio_i2c_bit_data; static void gpio_i2c_setscl(void *data, int state) { if (state) gpio_direction_output(scl_gpio, 1); else gpio_direction_output(scl_gpio, 0); } static void gpio_i2c_setsda(void *data, int state) { if (state) gpio_direction_output(sda_gpio, 1); else gpio_direction_output(sda_gpio, 0); } static int gpio_i2c_getscl(void *data) { gpio_direction_input(scl_gpio); return gpio_get_value(scl_gpio); } static int gpio_i2c_getsda(void *data) { gpio_direction_input(sda_gpio); return gpio_get_value(sda_gpio); } static int __init gpio_i2c_adapter_init(void) { gpio_i2c_bit_data.setsda gpio_i2c_setsda; gpio_i2c_bit_data.setscl gpio_i2c_setscl; gpio_i2c_bit_data.getsda gpio_i2c_getsda; gpio_i2c_bit_data.getscl gpio_i2c_getscl; gpio_i2c_bit_data.udelay i2c_delay; gpio_i2c_bit_data.timeout HZ; gpio_i2c_adapter.owner THIS_MODULE; gpio_i2c_adapter.class I2C_CLASS_HWMON; gpio_i2c_adapter.algo_data gpio_i2c_bit_data; gpio_i2c_adapter.name gpio-i2c; return i2c_bit_add_bus(gpio_i2c_adapter); }i2c_bit_add_bus会把i2c_algo_bit的算法绑定到 adapter 上省去手动填algo的麻烦。展开以后设备的 START、STOP、ACK 都由内核算法完成你只需要保证四个回调的速度够快、方向切换正确。这也是我推荐“产品化”时的标准做法而不是长期维护自己那套 MISC 驱动的时序代码。6.2 接入后的验证与限制哪些场景不能省掉硬件控制器接入子系统后验证方式立刻变成标准 I2C 工具链。加载模块后先用i2cdetect -l看 adapter 是否注册成功再用i2cdetect -y bus号扫描从机地址。如果探测到 EEPROM说明地址应答正常用i2cget -y bus 0x50 0x00能直接读到字节比写 ioctl 测试程序快得多。但必须清醒一点GPIO 模拟 I2C 始终是“软实时”的。它受内核调度影响SCL 半周期会有抖动所以不适合高速传输也不适合需要精确时序的外设。比如某些传感器要求连续读取不能间隔太久如果系统里刚好有高优先级中断在忙总线上的波形就被拉长采样点就偏了。硬件 I2C 控制器有硬件 FIFO 和中断能在低 CPU 占用下保证时序这是 GPIO 模拟比不了的。6.3 我的取舍什么时候保留 MISC、什么时候改用硬件 I2C如果是调试阶段我一般保留 MISC 设备方案因为它最简单出了问题可以直接在 ioctl 里加打印不用被内核 I2C 子系统的层层封装绕晕。如果是量产 BSP 里的固定外设我会至少把它升级成 i2c-algo-bit adapter让设备模型正规化。如果板子上还有空闲的硬件 I2C 控制器我绝对优先用硬件控制器哪怕要多接一根飞线。模拟 I2C 的成本不在那几颗上拉电阻而在调试它的时间。时序要看示波器、方向切换要仔细检查、并发要考虑锁这些问题每一个都要花真金白银。我的习惯是任何一对 GPIO 模拟 I2C 的引脚在原理图上必须标注“软件 I2C”这样硬件工程师改版时才有机会换成硬件控制器。希望帮到你。本文还有配套的精品资源点击获取