IMX6ULL 驱动开发 | AP3216C I2C 驱动(一):用 6.12/7.1 框架拆透,现代内核 API 重写

📅 2026/7/8 13:13:46
IMX6ULL 驱动开发 | AP3216C I2C 驱动(一):用 6.12/7.1 框架拆透,现代内核 API 重写
IMX6ULL 驱动开发 | AP3216C I2C 驱动(一):用 6.12/7.1 框架拆透,现代内核 API 重写这个仓库已经开源所有教程主线内核移植跑新版本imx-linux/uboot都在这里或者一起来尝试跑7.1的Linux欢迎各位大佬观摩喜欢的话点个⭐昨天刚更新仓库地址https://github.com/Awesome-Embedded-Learning-Studio/imx-forge静态网页https://awesome-embedded-learning-studio.github.io/imx-forge/如果你手头那份 I2C 驱动教程还停在 4.1.15 时代——probe拖着i2c_device_id、remove还return 0、class_create双参数——搬到 6.12.49 / 7.1.0 上就是一堆编译错误加incompatible pointer type警告。本篇是 AP3216C 系列开篇我们用现代内核 API 把这颗三合一传感器IR/ALS/PS的驱动从头重写顺带把 I2C 框架总线—设备—驱动三方怎么对上眼一次拆透。前言从裸机思维到 Linux 的放权我们先把丑话说在前头如果你手上那份 I2C 驱动教程还是 4.1.15 内核时代的写法那它基本只能当历史文物看。probe后面拖着个const struct i2c_device_id *id、remove还在return 0、class_create(THIS_MODULE, ...)双参数——这些代码搬到我们手里的 6.12.49 或 7.1.0 上class_create那行直接给你一个编译错误其余的也都是incompatible pointer type警告。这一篇教程的任务就是用现代内核的 API把 AP3216C 这颗传感器的驱动从头到尾、干净利落地重写一遍。但比换 API更要紧的是换脑子。回想你在裸机篇是怎么写 I2C 的bsp_i2c.c直接戳 I.MX6U 的寄存器、配 GPIO、算波特率bsp_ap3216c.c把芯片手册的时序硬编码进去两坨代码焊得死死的。在裸机里这没毛病因为你是硬件唯一的操控者。可一到 Linux 里你就得学会一件别扭的事——放权。你得承认你并不拥有这条 I2C 总线你只是借用它。总线怎么把比特发出去那是内核和半导体厂商早就铺好的路你只管告诉它我要发什么。这种控制器操作和设备业务逻辑的分离就是 Linux I2C 框架的全部哲学也是这一章反复要敲打的那根弦。学习目标用module_i2c_driver() 单参数probevoid remove写出符合 6.12 / 7.1 规范的 I2C 设备驱动通过i2c_transfer/i2c_smbus_*完成寄存器读写最终把 AP3216C 的 IR / ALS / PS 三路真实数据读到用户空间。AP3216C一颗芯片三副面孔AP3216C 是 Lite-On立錡的一颗三合一传感器把红外IR、环境光ALS和接近距离PS塞进了一颗芯片对外只走一根 I2C。在我们这块 I.MX6U-ALPHA 板上它挂在 I2C1 总线上7 位从机地址是0x1e——这个数字你接下来会反复看到写错了通信直接 NACK。它的寄存器布局很简单我们挑后面要用到的几个先认个脸熟0x00系统配置、0x01中断状态、0x0A/0x0B是 IR 的低/高字节、0x0C/0x0D是 ALS 的低/高字节、0x0E/0x0F是 PS 的低/高字节。注意这几组数据寄存器是地址连续的所以我们可以一口气连读六个字节再按位拼装出三路数据——这个细节在驱动实现那一节会用到。完整的寄存器定义我们会单独放进ap3216creg.h免得代码里全是魔术数字。先认认环境我们这次跑在两套内核上驱动代码两边通用这里感谢AI下面的内容是它基于我们仓库编写的板子I.MX6U-ALPHAAP3216C 挂 I2C1地址0x1e内核linux-imx6.12.49NXP BSP主开发环境/mainline7.1.0进阶验证源码仓库third_party/linux-imx、third_party/linux_mainline交叉工具链arm-linux-gnueabihf-gcc编译测试程序老教程 vs 新内核到底差在哪在动手之前我们先把老写法和新写法的核心差异摆到台面上心里有个谱后面看到代码才不会困惑。最关键的几处probe从双参数变成了单参数老的const struct i2c_device_id *id被砍掉remove的返回值从int变成了void函数体里那个return 0得删class_create从双参数THIS_MODULE 名字变成了单参数只留名字驱动注册不再手写module_init/module_exit一行module_i2c_driver()宏搞定。这几处变化不是内核作者心血来潮每一条背后都有理由我们会在后面的章节里一条条讲清楚。配套文件本教程涉及的源码文件如下源码由你自己建立、照着各节的代码段敲进去我们不替你生成ap3216c.c—— 驱动主体I2C 框架 字符设备ap3216creg.h—— AP3216C 寄存器地址定义ap3216c_app.c—— 用户空间测试程序设备树片段 —— 在imx6ull-aes.dtsi的i2c1节点下挂ap3216c1e小结上半篇理清了什么这一节我们理清了为什么要重写、AP3216C 是个什么器件、环境长什么样以及新老写法差在哪。接下来我们先钻进 I2C 框架的内部把适配器、设备、驱动这三方的结构体关系彻底搞明白。I2C 驱动框架谁开卡车谁打包货物在动手写任何一行代码之前我们得先把一个根子上的问题想明白。回想你在裸机篇是怎么对付 AP3216C 的写一个bsp_i2c.c折腾 I.MX6U 的 I2C 控制器——配时钟、设 GPIO、算波特率再写一个bsp_ap3216c.c把芯片手册里那串寄存器读写序列硬编码进去。这两坨代码紧紧焊在一起某天换了颗传感器bsp_i2c.c得原样抄一遍某天换了块板子、I2C 控制器变了bsp_ap3216c.c里那一堆时序又得推倒重来。任何一个写过两块板子的人最后都会被逼到同一个岔路口必须把控制器的操作和设备的业务逻辑拆开。Linux 内核的设计者二十年前就走完了这条路而且走得很彻底。他们立了一套硬规矩叫 I2C 驱动框架控制器驱动只管比特怎么发出去设备驱动只管我要发什么中间靠 I2C 核心层那一套 API 把两头粘起来。我们要学习的就是这个东西。总线驱动适配器和它的算法总线驱动操作的对象是 SoC 上的 I2C 适配器内核把它抽象成了i2c_adapter定义在include/linux/i2c.h。字段一大堆我们只挑跟传输相关的核心成员看免得被管理字段晃花眼/* * i2c_adapter 结构体节选自 include/linux/i2c.h */structi2c_adapter{structmodule*owner;unsignedintclass;conststructi2c_algorithm*algo;/* 总线访问算法灵魂在这里 */void*algo_data;structrt_mutexbus_lock;inttimeout;/* 单位是 jiffies */intretries;structdevicedev;/* 适配器对应的 device */intnr;/* 总线号 */charname[48];structi2c_bus_recovery_info*bus_recovery_info;conststructi2c_adapter_quirks*quirks;/* ...... 管理字段省略 ...... */};这一大坨里真正决定这个适配器怎么动起来的只有algo一个指针。它指向i2c_algorithm相当于适配器的使用说明书——告诉内核这玩意儿到底是硬件控制器还是拿 GPIO 软件模拟出来的。它的 6.12.49 真实形态长这样/* * i2c_algorithm 结构体include/linux/i2c.h */structi2c_algorithm{/* * xfer 和 master_xfer 是 union二选一新代码用 xfer * master_xfer 是为兼容老代码留的老名字。你调的所有读写 API * 最终都落到这个函数指针上。 */union{int(*xfer)(structi2c_adapter*adap,structi2c_msg*msgs,intnum);int(*master_xfer)(structi2c_adapter*adap,structi2c_msg*msgs,intnum);};/* 原子上下文里用的版本崩溃恢复、调试器大多数驱动用不到 */union{int(*xfer_atomic)(structi2c_adapter*adap,structi2c_msg*msgs,intnum);int(*master_xfer_atomic)(structi2c_adapter*adap,structi2c_msg*msgs,intnum);};int(*smbus_xfer)(structi2c_adapter*adap,u16 addr,unsignedshortflags,charread_write,u8 command,intsize,unioni2c_smbus_data*data);/* 告诉内核这个适配器支持哪些能力 */u32(*functionality)(structi2c_adapter*adap);/* ...... I2C 从机模式相关成员省略 ...... */};这里有个细节值得停下来想一想xfer和master_xfer被塞进了同一个union。这不是内核作者闲得慌而是一次正在进行的命名迁移——master_xfer是老名字xfer是新名字两者完全等价你写哪个内核都认。看到这种union双名你心里就该有数这是个新旧交替的过渡期字段。下一节我们拆i2c-imx.c的时候会看到 I.MX6U 的适配器实际填的是哪一个。所以假如你是 SoC 厂商的工程师写一个 I2C 总线驱动的套路是定死的定义一个i2c_adapter实现xfer或smbus_xfer再用i2c_add_adapter或i2c_add_numbered_adapter把它注册进内核。这俩注册函数的区别只有一个——前者让内核动态分配总线号后者由你指定一个静态总线号。inti2c_add_adapter(structi2c_adapter*adapter);/* 动态分配总线号 */inti2c_add_numbered_adapter(structi2c_adapter*adap);/* 你指定总线号 */这里有个好消息也有个坏消息。好消息是像 I.MX6U 这种主流 SoCNXP 早就把这层写好了驱动就在drivers/i2c/busses/i2c-imx.c开箱即用。坏消息是你大概率这辈子都没机会亲手写一个总线驱动——除非跳槽去了原厂。所以对这一层我们的定位是使用者而不是创造者知道它存在、知道它向上暴露了哪些 API 就够了。真正要我们操刀的是下面这层。设备驱动i2c_client 描述你是谁i2c_driver 描述怎么干聊完了开卡车的现在轮到打包货物的——这才是开发者真正要操心的部分。这一层的主角是一对结构体i2c_client和i2c_driver。顺着 Linux总线-设备-驱动那套老传统总线有了剩下的自然就是设备和驱动。i2c_client描述挂在 I2C 总线上的一个具体设备每多一个 I2C 设备内核里就多一个对应实例/* * i2c_client 结构体include/linux/i2c.h */structi2c_client{unsignedshortflags;/* 标志位 */unsignedshortaddr;/* 7 位 I2C 地址存在低 7 位 */charname[I2C_NAME_SIZE];/* 设备名 */structi2c_adapter*adapter;/* 挂在哪个控制器上 */structdevicedev;/* 内嵌的 device */intirq;/* 中断号 */structlist_headdetected;/* ...... */};addr就是你那颗芯片的 7 位地址AP3216C 是0x1eadapter指向它挂在哪个控制器上name用于匹配驱动。这里有个关键认知要先建立起来这个结构体通常不是你手动填充的。它是 I2C 核心层在你写好设备树之后根据设备树节点自动生成的——你只管在设备树里写reg 0x1e内核替你把addr填好、把adapter指过去。我们要亲手写的是另一个结构体。i2c_driver才是这一章的主角和platform_driver长得像一个模子刻出来的用来注册驱动逻辑。看它在 6.12.49 里的真实定义/* * i2c_driver 结构体include/linux/i2c.h */structi2c_driver{unsignedintclass;/* 设备和驱动匹配成功后回调 —— 绝大多数初始化都在这里 */int(*probe)(structi2c_client*client);/* 设备拔掉或驱动卸载时回调 —— 注意返回值是 void */void(*remove)(structi2c_client*client);void(*shutdown)(structi2c_client*client);void(*alert)(structi2c_client*client,enumi2c_alert_protocolprotocol,unsignedintdata);structdevice_driverdriver;conststructi2c_device_id*id_table;/* 传统匹配表现代驱动可省略 *//* 下面两个是老式自动探测用的现代驱动不要碰 */int(*detect)(structi2c_client*client,structi2c_board_info*info);constunsignedshort*address_list;structlist_headclients;u32 flags;};把这份定义和老教程里的对照一下现代两个字落在哪儿就一目了然了。probe的签名是干干净净的int (*probe)(struct i2c_client *client)只有一个参数——老教程里那个const struct i2c_device_id *id早被内核咔嚓掉了因为现代驱动从设备树取数据根本用不着它。remove的返回类型从int变成了voidI2C 子系统比很多别的子系统都更早完成了这次迁移。你要是还照老教程写static int xxx_remove(...) { ...; return 0; }在新内核里赋给i2c_driver.remove这一步就会触发incompatible pointer type警告。注册函数这边老教程会让你手写一整套module_init/module_exit里面调i2c_add_driver。写法没错但内核早就备好了更省事的宏一行顶你原来十行/* include/linux/i2c.h */#definei2c_add_driver(driver)\i2c_register_driver(THIS_MODULE,driver)#definemodule_i2c_driver(__i2c_driver)\module_driver(__i2c_driver,i2c_add_driver,i2c_del_driver)i2c_add_driver本质就是帮你把THIS_MODULE塞进i2c_register_driver的宏module_i2c_driver更狠直接帮你生成module_init和module_exit前者调注册、后者调注销。i2c_register_driver内部还会替你设好driver-owner所以你连.driver.owner THIS_MODULE都不用写了。把这些凑齐一个现代的、完整的 I2C 设备驱动注册骨架就是下面这样往后你写的 I2C 驱动九成都是它的变体/* * 现代 i2c_driver 注册骨架 *//* 1、probe设备匹配上以后初始化硬件、注册字符设备都在这儿 */staticintap3216c_probe(structi2c_client*client){/* 函数具体程序 */return0;}/* 2、remove注意是 void别再 return 0 了 */staticvoidap3216c_remove(structi2c_client*client){/* 注销字符设备、释放资源 */}/* 3、设备树匹配表 —— 现代 I2C 驱动靠它就能匹配id_table 可以不要 */staticconststructof_device_idap3216c_of_match[]{{.compatibleimxaes,ap3216c},{/* sentinel */}};MODULE_DEVICE_TABLE(of,ap3216c_of_match);/* 4、驱动结构体 */staticstructi2c_driverap3216c_driver{.driver{.nameap3216c,.of_match_tableap3216c_of_match,},.probeap3216c_probe,.removeap3216c_remove,};/* 5、注册交给宏再也不用手写 module_init/module_exit */module_i2c_driver(ap3216c_driver);MODULE_AUTHOR(Charliechen114514);MODULE_LICENSE(GPL);⚠️ 踩坑预警如果你是照老教材抄过来的代码加载之前务必把这三处老写法改掉probe多出来的那个id参数得删remove的int返回值改成void、函数体里的return 0删掉.driver.owner THIS_MODULE这行可以直接去掉。这三样不改要么编译报警告要么行为压根不对。另外纯设备树匹配的驱动那张老的i2c_device_id表是可以整个省掉的——这点我们马上从内核源码里给你找到依据。它们是怎么对上眼的匹配机制到这儿适配器总线、设备i2c_client、驱动i2c_driver三方到齐剩下的问题只有一个内核凭什么把某颗芯片和某个驱动撮合到一起这个月老由I2C 核心层扮演源码在drivers/i2c/i2c-core-base.c。它不光提供了前面那些注册函数还维护着一条虚拟的 I2C 总线i2c_bus_type/* * i2c_bus_typedrivers/i2c/i2c-core-base.c * 现代内核里它已经是 const struct bus_type */conststructbus_typei2c_bus_type{.namei2c,.matchi2c_device_match,/* 匹配判断在这里 */.probei2c_device_probe,.removei2c_device_remove,.shutdowni2c_device_shutdown,};真正的撮合逻辑在.match指向的i2c_device_match里我们从i2c-core-base.c把它抠出来看/* * i2c_device_matchdrivers/i2c/i2c-core-base.c */staticinti2c_device_match(structdevice*dev,conststructdevice_driver*drv){structi2c_client*clienti2c_verify_client(dev);conststructi2c_driver*driver;/* 第一优先级设备树OF匹配比较 compatible 字符串 */if(i2c_of_match_device(drv-of_match_table,client))return1;/* 第二优先级ACPI 匹配x86/PC 那套嵌入式板子基本跳过 */if(acpi_driver_match_device(dev,drv))return1;/* 最后兜底老式 id_table 名字匹配 */driverto_i2c_driver(drv);if(i2c_match_id(driver-id_table,client))return1;return0;}这段逻辑读起来有种层层退让的味道。内核一上来先试设备树匹配——拿设备树节点里的compatible去和驱动of_match_table里登记的每一项比对上了就牵手成功后面两步根本不看。这也是为什么我们前面敢说现代驱动只靠of_match_table就够了只要这一步能命中id_table永远轮不到被查询。只有设备树这条路走不通时内核才会退而求其次试 ACPI嵌入式板子上基本不存在再不行才退到最老土的办法——拿i2c_client的name去和驱动id_table里的名字比。最后这一步是给那些既没设备树、也没 ACPI 的远古板子留的逃生通道我们这年头写驱动基本用不上。框架的全貌到此就清楚了。我们虽然把驱动分成了总线驱动和设备驱动两层但它俩绝不是老死不相往来——下一篇我们会钻进i2c-imx.c看看 I.MX6U 的总线驱动到底长什么样、又是怎么被注册进系统的。等看明白它怎么把搬运比特的能力封装好向上暴露再回过头写自己的设备驱动心里就有底了。