前言在上一篇文章中我们通过PWM子系统实现了LED亮度的动态调节。现在我们将进入嵌入式开发中最常用的总线之一——I2C并驱动一个经典的I2C EEPROM芯片AT24C02实现对其存储空间的读写访问。本文将完整展示一个基于I2C子系统的字符设备驱动将整个EEPROM作为一个文件用户程序可以通过/dev/at24c02像读写普通文件一样存取EEPROM中的数据。你将掌握I2C总线的基本原理与Linux I2C子系统架构设备树中I2C设备的描述方法i2c_driverprobe的标准写法使用i2c_smbus_*系列函数与I2C设备通信在file_operations中结合文件偏移实现硬件存储的读写一、I2C子系统简介1.1 I2C总线特点I2CInter-Integrated Circuit是飞利浦开发的两线式串行总线仅需**SDA数据线和SCL时钟线**即可连接多个设备。每个设备有唯一的7位或10位地址主机通过地址寻址从机进行数据收发。AT24C02 是一款容量为 256 字节2Kbit的 EEPROMI2C 接口地址通常为0x507位地址不含读写位。1.2 Linux I2C子系统架构内核I2C框架分为三层I2C适配器驱动原厂提供控制SoC内部的I2C控制器硬件负责产生时序。对应/dev/i2c-N。I2C核心提供统一的API实现适配器与设备驱动的匹配。I2C设备驱动我们编写通过i2c_client与具体芯片通信向上提供功能接口。我们编写的驱动属于I2C设备驱动通过struct i2c_driver注册与设备树中的I2C设备节点匹配。1.3 SMBus 接口内核提供了两套与I2C设备通信的API原始的i2c_transfer()和SMBus协议封装的i2c_smbus_*()。AT24C02完全兼容SMBus因此我们使用更简单的后者函数作用i2c_smbus_read_byte_data(client, addr)从设备指定地址读1字节i2c_smbus_write_byte_data(client, addr, val)向设备指定地址写1字节其中addr为片内偏移地址val为写入数据。二、设计思路在设备树的I2C控制器节点下添加AT24C02子节点指定reg 0x50。驱动使用i2c_driver通过of_match_table与设备树节点匹配。probe中获取i2c_client初始化字符设备框架创建设备节点/dev/at24c02。file_operationsread根据文件偏移*f_pos从EEPROM中读取一个字节。*f_pos超过255则返回0EOF。write根据文件偏移向EEPROM写入一个字节。open/release仅打印日志。当前驱动未实现llseek但cat、dd等工具通过顺序读写即可访问全部256字节。应用层可通过dd、hexdump或自定义程序读写整个EEPROM。三、设备树修改在板级设备树中找到当前使用的I2C控制器节点如i2c1在其中添加AT24C02子节点i2c1 { clock-frequency 100000; pinctrl-names default; pinctrl-0 pinctrl_i2c1; status okay; at24c02: eeprom50 { compatible yourname,at24c02; /* 与驱动的of_match_table一致 */ reg 0x50; /* I2C设备地址7位 */ status okay; }; };compatible自定义字符串驱动通过它匹配。reg设备的7位I2C地址。AT24C02硬件地址引脚全部接地时地址为0x50。若开发板上AT24C02挂接在其他I2C总线上如i2c2请对应修改。重新编译设备树并替换重启开发板。四、驱动代码实现新建文件at24c02_drv.c完整代码如下/* * at24c02_drv.c * AT24C02 EEPROM I2C 字符设备驱动。 * 使用 i2c_driver 匹配设备树节点通过 i2c_smbus 接口访问硬件。 * 设备节点 /dev/at24c02支持读写文件内容即为EEPROM全部256字节。 * 作者[你的ID] * 适配内核Linux 5.x (4.x 亦可) * 参考开发板i.MX6ULL */#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/i2c.h/* I2C 驱动头文件 */#includelinux/of.h#defineDEVICE_NAMEat24c02#defineCLASS_NAMEat24c02_class#defineEEPROM_SIZE256/* AT24C02 容量256 字节 */staticdev_tdev_num;staticstructcdevmy_cdev;staticstructclass*my_class;staticstructdevice*my_device;staticstructi2c_client*at24c02_client;/* 保存匹配到的I2C客户端 *//* 打开设备 */staticinteeprom_open(structinode*inode,structfile*file){pr_info(at24c02: device opened\n);return0;}/* 关闭设备 */staticinteeprom_release(structinode*inode,structfile*file){pr_info(at24c02: device closed\n);return0;}/* 读取设备每次读取一个字节从当前文件偏移处读取 */staticssize_teeprom_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){intret;charval;if(*f_posEEPROM_SIZE)return0;/* EOF *//* 从 EEPROM 的 *f_pos 地址读取一个字节 */reti2c_smbus_read_byte_data(at24c02_client,*f_pos);if(ret0){pr_err(at24c02: read at offset %lld failed, err%d\n,*f_pos,ret);return-EIO;}val(char)ret;if(copy_to_user(buf,val,1))return-EFAULT;(*f_pos);return1;}/* 写入设备每次写入一个字节写到当前文件偏移处 */staticssize_teeprom_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){intret;charval;if(*f_posEEPROM_SIZE)return-ENOSPC;/* 空间不足 */if(copy_from_user(val,buf,1))return-EFAULT;/* 向 EEPROM 的 *f_pos 地址写入一个字节 */reti2c_smbus_write_byte_data(at24c02_client,*f_pos,val);if(ret0){pr_err(at24c02: write at offset %lld failed, err%d\n,*f_pos,ret);return-EIO;}pr_info(at24c02: wrote 0x%02x to offset %lld\n,val,*f_pos);(*f_pos);return1;}staticstructfile_operationseeprom_fops{.ownerTHIS_MODULE,.openeeprom_open,.releaseeeprom_release,.readeeprom_read,.writeeeprom_write,};/* ---------------- i2c_driver 部分 ---------------- */staticinteeprom_probe(structi2c_client*client,conststructi2c_device_id*id){intret;pr_info(at24c02: probe called, addr0x%02x\n,client-addr);/* 保存 i2c_client供读写操作使用 */at24c02_clientclient;/* 1. 分配设备号 */retalloc_chrdev_region(dev_num,0,1,DEVICE_NAME);if(ret0){pr_err(at24c02: alloc_chrdev_region failed\n);returnret;}/* 2. 初始化 cdev */cdev_init(my_cdev,eeprom_fops);my_cdev.ownerTHIS_MODULE;retcdev_add(my_cdev,dev_num,1);if(ret){pr_err(at24c02: cdev_add failed\n);gotoerr_cdev_add;}/* 3. 创建 class */my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(at24c02: class_create failed\n);retPTR_ERR(my_class);gotoerr_class_create;}/* 4. 创建设备节点 */my_devicedevice_create(my_class,client-dev,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err(at24c02: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}pr_info(at24c02: /dev/%s created, EEPROM size%d bytes\n,DEVICE_NAME,EEPROM_SIZE);return0;err_device_create:class_destroy(my_class);err_class_create:cdev_del(my_cdev);err_cdev_add:unregister_chrdev_region(dev_num,1);returnret;}staticinteeprom_remove(structi2c_client*client){pr_info(at24c02: remove called\n);device_destroy(my_class,dev_num);class_destroy(my_class);cdev_del(my_cdev);unregister_chrdev_region(dev_num,1);return0;}/* 设备树匹配表 */staticconststructof_device_ideeprom_of_match[]{{.compatibleyourname,at24c02},{}};MODULE_DEVICE_TABLE(of,eeprom_of_match);/* i2c_device_id 表用于非设备树匹配同时有助 modprobe */staticconststructi2c_device_ideeprom_id[]{{at24c02,0},{}};MODULE_DEVICE_TABLE(i2c,eeprom_id);/* i2c_driver 结构体 */staticstructi2c_drivereeprom_driver{.probeeeprom_probe,.removeeeprom_remove,.driver{.nameat24c02,.ownerTHIS_MODULE,.of_match_tableeeprom_of_match,},.id_tableeeprom_id,};module_i2c_driver(eeprom_driver);/* 封装 module_init 和 module_exit 的宏 */MODULE_LICENSE(GPL);MODULE_AUTHOR(Your Name);MODULE_DESCRIPTION(AT24C02 EEPROM I2C character device driver);MODULE_VERSION(1.0);代码核心说明i2c_smbus_read_byte_data第一个参数是i2c_client第二个是片内地址。返回读到的字节0-255负数表示错误。i2c_smbus_write_byte_data向指定片内地址写入一个字节。读写函数每次只操作一个字节配合用户空间的cat、dd等工具这些工具会重复调用read/write直到传输完毕或读到EOF。错误路径使用goto进行完整清理与前面的文章风格一致。五、Makefile# Makefile for at24c02 KERNEL_DIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) obj-m : at24c02_drv.o all: make -C $(KERNEL_DIR) M$(PWD) modules clean: make -C $(KERNEL_DIR) M$(PWD) clean交叉编译时设置ARCH和CROSS_COMPILE。六、测试与验证6.1 检查I2C总线确保内核已加载I2C控制器驱动且设备可见# 查看i2c总线ls/dev/i2c-*# 通常有 /dev/i2c-0, /dev/i2c-1 等# 用i2cdetect检测设备需安装i2c-toolsi2cdetect-y1# 应该在 0x50 位置看到 50 字样6.2 加载驱动insmod at24c02_drv.kodmesg|tail# at24c02: probe called, addr0x50# at24c02: /dev/at24c02 created, EEPROM size256 bytes6.3 确认设备节点ls-l/dev/at24c02chmod666/dev/at24c026.4 读写测试写入数据# 写入 Hello EEPROM! 到起始位置echoHello EEPROM!/dev/at24c02# 或者用 dd 写入任意字节ddif/dev/urandomof/dev/at24c02bs1count256读取数据# 用 hexdump 查看前32字节ddif/dev/at24c02bs1count32|hexdump-C# 或者直接 catcat/dev/at24c02读出的内容应与写入的一致。读取/写入特定偏移可以通过dd的seek和skip参数实现但当前驱动未实现llseekdd会自动顺序读写全部内容。若需支持lseek可在file_operations中添加.llseek generic_file_llseek但此时需保证read/write正确处理偏移这里为了简洁未添加。6.5 卸载驱动rmmod at24c02_drv七、常见问题排查insmod后probe未调用检查设备树中compatible是否完全一致确认AT24C02硬件地址正确且连接正常i2cdetect能否看到设备。读写操作返回-EIO检查I2C时钟频率是否过快可降低clock-frequency到100000。确认AT24C02的写保护引脚WP未接高电平否则无法写入。检查i2c_client-addr是否正确驱动里可以直接printk打印。每次写后立刻读读出的数据不对AT24C02在写入一个字节后需要一定的内部编程时间约5ms。如果紧接着读取可能读到旧数据。虽然i2c_smbus_write_byte_data会等待ACK但在极少数高速连续读写时可能出现问题。若遇到此情况可在eeprom_write中写入后添加msleep(5)延迟。i2cdetect显示UU而非50说明该地址已被其他驱动程序占用可能是内核自带的at24驱动。需在内核配置中关闭CONFIG_EEPROM_AT24或修改设备树的compatible使其不被自带驱动识别。八、总结与下篇预告本文通过驱动AT24C02 EEPROM完整演示了I2C设备驱动的开发流程设备树描述硬件、i2c_driver匹配、SMBus接口通信、字符设备框架封装。这使得上层应用可以像操作文件一样读写硬件存储。下篇预告掌握了GPIO、PWM、I2C后下一篇文章我们将进入输入子系统驱动一个按键GPIO按键让按键事件通过标准的输入设备接口上报给用户空间届时我们的Linux设备驱动知识将更加完善。敬请期待如果本文对你有帮助欢迎点赞、收藏、关注。有任何技术疑问欢迎在评论区留言交流