IMX6ULL: AP3216C 驱动层实现 —— I2C 框架与字符设备的合体这个仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里或者一起来尝试跑7.1的Linux欢迎各位大佬观摩喜欢的话点个⭐昨天刚更新仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://awesome-embedded-learning-studio.github.io/imx-forge/前面两节我们把框架和适配器都摸透了现在终于到了真正动代码的时候。这一节我们要把 AP3216C 的驱动主体写出来——它是一个I2C 设备驱动和字符设备驱动的合体对下用 I2C 框架的 API 跟芯片通信对上用字符设备的接口把数据暴露给用户空间。涉及两个源码文件ap3216creg.h放寄存器定义ap3216c.c放驱动主体。我们按寄存器表 → 设备结构体 → 读写封装 → 数据读取 → probe/remove → 字符设备接口 → 注册的顺序一块一块把它搭起来。寄存器定义先把词汇表列出来驱动和芯片对话靠的是寄存器地址。为了不让代码里满是魔术数字我们把这些地址统一收进ap3216creg.h/* ap3216creg.h */#ifndefAP3216C_H#defineAP3216C_H#defineAP3216C_SYSTEMCONG0x00/* 系统配置寄存器 */#defineAP3216C_INTSTATUS0x01/* 中断状态寄存器 */#defineAP3216C_INTCLEAR0x02/* 中断清除寄存器 */#defineAP3216C_IRDATALOW0x0A/* IR 数据低字节 */#defineAP3216C_IRDATAHIGH0x0B/* IR 数据高字节 */#defineAP3216C_ALSDATALOW0x0C/* ALS 数据低字节 */#defineAP3216C_ALSDATAHIGH0x0D/* ALS 数据高字节 */#defineAP3216C_PSDATALOW0x0E/* PS 数据低字节 */#defineAP3216C_PSDATAHIGH0x0F/* PS 数据高字节 */#endif注意 IR / ALS / PS 这三组数据寄存器的地址是连续的0x0A~0x0F这个细节后面读数据时会用到——我们可以从一个起点AP3216C_IRDATALOW开始连读六个字节就把三路数据全捞回来。设备结构体把状态拢到一起每个字符设备驱动都需要一个结构体来挂自己的全部状态。AP3216C 驱动的设备结构体长这样/* ap3216c.c */#includelinux/module.h#includelinux/i2c.h#includelinux/cdev.h#includelinux/device.h#includelinux/delay.h#includelinux/uaccess.h#includeap3216creg.h#defineAP3216C_NAMEap3216cstructap3216c_dev{dev_tdevid;/* 设备号 */structcdevcdev;/* 字符设备 */structclass*class;/* 设备类 */structdevice*device;/* 设备节点 */structi2c_client*client;/* 关联的 i2c_client —— 通信靠它 */unsignedshortir;/* IR 数据 */unsignedshortals;/* ALS 数据 */unsignedshortps;/* PS 数据 */};这里有个和老教程不一样的设计我们不再用一个void *private_data黑箱指针存i2c_client而是堂堂正正地声明一个struct i2c_client *client成员。类型明确编译器能帮你查错读代码的人也不用去猜这个void *到底指什么。ir/als/ps三个字段缓存最新一次读到的传感器数据等用户空间来read时直接拷给它。寄存器读写先看 i2c_transfer 的原始姿势我们调的那些读写最终都会落到适配器的master_xfer上——但内核给设备驱动准备的接口有好几层从亲手拼消息到一行搞定都有。为了搞懂机制我们先看最原始的那层亲手拼i2c_msg再交给i2c_transfer。I2C 读一个寄存器本质上是个两步动作——先发寄存器地址一次写再收数据一次读所以得拼两条i2c_msg/* 教学版手拼 i2c_msg 读取一个寄存器和 i2c_smbus_read_byte_data 等价 */staticintap3216c_read_reg_xfer(structap3216c_dev*dev,u8 reg){structi2c_client*clientdev-client;structi2c_msgmsg[2];u8 val;intret;/* msg[0]写操作把要读的寄存器地址发出去 */msg[0].addrclient-addr;/* 从机地址 */msg[0].flags0;/* 0 写 */msg[0].bufreg;/* 发的内容是寄存器地址 */msg[0].len1;/* msg[1]读操作把数据收回来 */msg[1].addrclient-addr;msg[1].flagsI2C_M_RD;/* I2C_M_RD 读 */msg[1].bufval;msg[1].len1;reti2c_transfer(client-adapter,msg,2);return(ret2)?val:-EREMOTEIO;}你把i2c_msg想象成一封贴好邮票、写好地址但还没投递的信addr是收件人I2C 地址flags决定是寄信还是索取buf是信纸len是几页。i2c_transfer一次把这组消息全发出去返回值是成功投递的消息数——我们发了两条所以必须返回2才算成功少了就说明通信出了问题我们用-EREMOTEIO对端 I/O 失败回报。这套写地址 读数据的两步走是绝大多数带寄存器地址的 I2C 设备的标准时序少一步芯片就不理你。理解了这个机制你就会明白为什么内核还要再封一层更省事的接口。对于8 位寄存器、8 位数据这种最常见的情况亲手拼两条msg实在啰嗦于是内核提供了i2c_smbus_read_byte_data/i2c_smbus_write_byte_data——它们内部干的事和上面这段一模一样只是替你把i2c_msg拼好了/* 主线版用 SMBus 便捷函数一行搞定 */staticintap3216c_read_reg(structap3216c_dev*dev,u8 reg){returni2c_smbus_read_byte_data(dev-client,reg);}staticintap3216c_write_reg(structap3216c_dev*dev,u8 reg,u8 val){returni2c_smbus_write_byte_data(dev-client,reg,val);}i2c_smbus_read_byte_data(client, reg)返回的就是读到的字节值出错返回负数i2c_smbus_write_byte_data(client, reg, val)写一个字节。能用这两个函数就别再手拼i2c_msg——代码短、错不了。我们这个驱动的正式代码就用这一对。什么时候还得回去手拼 i2c_msg当设备的读写时序比较非标准时便捷函数就不够用了比如某些器件要求一次写多个寄存器、或者读的时候首字节格式特殊。那种情况就得退回i2c_transfer亲手拼消息。另外内核还提供i2c_smbus_read_i2c_block_data/i2c_smbus_write_i2c_block_data做连续多字节读写比循环单字节读效率更高——AP3216C 连读六个字节其实可以用块读这里为了讲解清晰先用循环单字节。读三路数据ap3216c_readdata读写封装好了读三路数据就是顺着连续的寄存器地址一个个读、再按位拼装/* 读取并解析 IR / ALS / PS 三路数据 */staticvoidap3216c_readdata(structap3216c_dev*dev){u8 i,buf[6];/* 0x0A~0x0F 连续六个字节IR_L/IR_H/ALS_L/ALS_H/PS_L/PS_H */for(i0;i6;i)buf[i]ap3216c_read_reg(dev,AP3216C_IRDATALOWi);/* IR10 位有效。bit71 表示溢出无效 */if(buf[0]0x80)dev-ir0;elsedev-ir((unsignedshort)buf[1]2)|(buf[0]0x03);/* ALS16 位 */dev-als((unsignedshort)buf[3]8)|buf[2];/* PSbit61 表示溢出无效 */if(buf[4]0x40)dev-ps0;elsedev-ps((unsignedshort)(buf[5]0x3F)4)|(buf[4]0x0F);}这里最容易翻车的是那些位掩码——buf[0] 0x80判断 IR 是否溢出、buf[1] 2 | buf[0] 0x03把散在两个字节里的 10 位 IR 数据拼起来。这些规则全来自数据手册少一个 0x03或者左移位数写错读出来的数据就是乱码。这种地方别凭感觉对着手册抄。probe把一切都串起来probe是驱动真正干活的地方。设备匹配成功后内核回调它我们要在这儿把字符设备搭起来、把i2c_client存好、再把芯片初始化成工作状态staticintap3216c_probe(structi2c_client*client){structap3216c_dev*dev;intret;/* 1、分配设备结构体devm_ 版本会在设备移除时自动释放 */devdevm_kzalloc(client-dev,sizeof(*dev),GFP_KERNEL);if(!dev)return-ENOMEM;dev-clientclient;i2c_set_clientdata(client,dev);/* 把 dev 挂到 client 上remove 时能取回 *//* 2、注册字符设备号 */retalloc_chrdev_region(dev-devid,0,1,AP3216C_NAME);if(ret)returnret;/* 3、初始化并添加 cdev */cdev_init(dev-cdev,ap3216c_ops);retcdev_add(dev-cdev,dev-devid,1);if(ret)gotodel_region;/* 4、创建类 —— 注意 class_create 现在是单参数不再要 THIS_MODULE */dev-classclass_create(AP3216C_NAME);if(IS_ERR(dev-class)){retPTR_ERR(dev-class);gotodel_cdev;}/* 5、创建设备节点 /dev/ap3216c */dev-devicedevice_create(dev-class,NULL,dev-devid,NULL,AP3216C_NAME);if(IS_ERR(dev-device)){retPTR_ERR(dev-device);gotodestroy_class;}/* 6、初始化 AP3216C先复位再开启 ALSPSIR */ap3216c_write_reg(dev,AP3216C_SYSTEMCONG,0x04);/* 0x04 软复位 */msleep(10);/* 复位需要一点时间 */ap3216c_write_reg(dev,AP3216C_SYSTEMCONG,0x03);/* 0x03 开启 ALSPSIR */return0;destroy_class:class_destroy(dev-class);del_cdev:cdev_del(dev-cdev);del_region:unregister_chrdev_region(dev-devid,1);returnret;}这段代码里有几处现代写法值得专门点出来。devm_kzalloc(client-dev, ...)用的是托管分配——它绑在client-dev上设备移除时内核自动帮你kfree所以remove里我们不用也不能再去释放dev。i2c_set_clientdata(client, dev)把设备结构体挂到i2c_client上这是 I2C 子系统的标准玩法等会儿remove里用i2c_get_clientdata就能原样取回。class_create(AP3216C_NAME)——注意只有一个参数老教程里那个THIS_MODULE早从 6.4 开始就被砍掉了你要是写成双参数6.12 / 7.1 直接编译报错。最后芯片初始化那两句是 AP3216C 的开机仪式写0x04软复位、睡 10ms 等它缓过来、再写0x03把三路采集全打开。probe里出错处理用了一串goto标签按先创建的后清理的相反顺序回滚。这是内核驱动里几乎强制的写法——别嫌丑它保证任何一步失败都不会留下半拉子资源。removevoid按相反顺序清理remove是probe的镜像而且在新内核里它没有返回值staticvoidap3216c_remove(structi2c_client*client){structap3216c_dev*devi2c_get_clientdata(client);device_destroy(dev-class,dev-devid);class_destroy(dev-class);cdev_del(dev-cdev);unregister_chrdev_region(dev-devid,1);/* dev 由 devm_kzalloc 分配这里不用手动释放 */}i2c_get_clientdata取回probe里存进去的dev然后按device_destroy → class_destroy → cdev_del → unregister_chrdev_region的顺序逐个回收和probe里创建的顺序正好反过来。注意这里没有kfree(dev)——因为它是devm_kzalloc分配的内核会自动收回。如果你在这儿再手动kfree一次等着收获一个 double-free 的内核崩溃。字符设备接口open / read / release用户空间通过/dev/ap3216c跟驱动打交道交互方式由file_operations定义。这里我们用container_of从inode-i_cdev反推出设备结构体——这是支持多设备的标准写法比全局变量专业得多staticintap3216c_open(structinode*inode,structfile*filp){/* 从 inode 里的 cdev 反推设备结构体存进 filp-private_data */structap3216c_dev*devcontainer_of(inode-i_cdev,structap3216c_dev,cdev);filp-private_datadev;return0;}staticssize_tap3216c_read(structfile*filp,char__user*buf,size_tcnt,loff_t*off){structap3216c_dev*devfilp-private_data;unsignedshortdata[3];ap3216c_readdata(dev);data[0]dev-ir;data[1]dev-als;data[2]dev-ps;if(cntsizeof(data))cntsizeof(data);if(copy_to_user(buf,data,cnt))return-EFAULT;returncnt;}staticintap3216c_release(structinode*inode,structfile*filp){return0;}staticconststructfile_operationsap3216c_ops{.ownerTHIS_MODULE,.openap3216c_open,.readap3216c_read,.releaseap3216c_release,};open里那句container_of(inode-i_cdev, struct ap3216c_dev, cdev)是精髓inode里藏着这个设备节点对应的cdev而我们的cdev就嵌在ap3216c_dev结构体里container_of反推出外层结构体的地址。拿到dev后塞进filp-private_data这样read里一行filp-private_data就能取回不用再依赖任何全局变量——即便板子上挂了三颗 AP3216C每路也能各管各的。read里先调ap3216c_readdata刷新数据再把ir/als/ps三个unsigned short打包copy_to_user给用户空间。注册module_i2c_driver 一行收尾最后把驱动挂上 I2C 总线。还记得框架节里那个module_i2c_driver宏吗这里就用上它staticconststructof_device_idap3216c_of_match[]{{.compatibleimxaes,ap3216c},{/* sentinel */}};MODULE_DEVICE_TABLE(of,ap3216c_of_match);staticstructi2c_driverap3216c_driver{.driver{.nameAP3216C_NAME,.of_match_tableap3216c_of_match,},.probeap3216c_probe,.removeap3216c_remove,};module_i2c_driver(ap3216c_driver);MODULE_AUTHOR(Charliechen114514);MODULE_DESCRIPTION(AP3216C ambient light / proximity / IR sensor driver (modern I2C API));MODULE_LICENSE(GPL);of_match_table里的imxaes,ap3216c必须和设备树里那个compatible一字不差——这是驱动和设备配对的暗号对不上probe永远不会被调用。module_i2c_driver(ap3216c_driver)这一行同时替我们生成了module_init调i2c_add_driver和module_exit调i2c_del_driver还顺手设好了owner所以你既不用手写那两个函数也不用写.driver.owner THIS_MODULE。整份驱动到这里就齐活了。小结这一节我们写完了 AP3216C 的驱动主体用i2c_smbus_*做寄存器读写顺手用i2c_transfer讲清了底层机制用container_offilp-private_data做多设备友好的字符设备接口probe用devm_kzalloc托管内存、用单参数class_create建节点remove是void、按相反顺序清理最后module_i2c_driver一行注册。代码是现代的、能直接在 6.12 / 7.1 上编过。但这一切要跑起来还差最后一块拼图——设备树。下一节我们就把 AP3216C 画进内核的硬件地图里。