网站制作论文总结,佛山市外贸网站建设价格,wordpress 菜单 调用,阜宁县住房城乡建设局网站1. 初识Uboot中的I2C#xff1a;两种路径#xff0c;一个目标 如果你玩过嵌入式开发#xff0c;尤其是需要在板子启动阶段#xff08;Uboot里#xff09;去读写个EEPROM、配置个PMIC电源芯片或者初始化一个传感器#xff0c;那你肯定绕不开I2C总线。在Uboot这个“裸奔”的…1. 初识Uboot中的I2C两种路径一个目标如果你玩过嵌入式开发尤其是需要在板子启动阶段Uboot里去读写个EEPROM、配置个PMIC电源芯片或者初始化一个传感器那你肯定绕不开I2C总线。在Uboot这个“裸奔”的环境里操作I2C和我们在成熟的Linux内核里用现成的驱动框架感觉完全不同更像是在和硬件直接“对话”。我刚开始接触这块的时候也踩过不少坑比如明明地址是对的但就是读不出数据或者写进去的值莫名其妙变了。后来我发现Uboot里操作I2C其实有两条路可以走就像去同一个目的地你可以选择走老路也可以选择上新修的高速。这两条路分别对应着两个关键的宏定义CONFIG_SYS_I2C和CONFIG_DM_I2C。前者是传统的、直接了当的“老路”我们称之为传统模式SYS_I2C后者则是遵循Uboot驱动模型Driver Model的“新高速”我们称之为驱动模型模式DM_I2C。简单来说CONFIG_SYS_I2C开启传统方式CONFIG_DM_I2C则启用新的驱动模型。现在很多新移植的板子尤其是使用较新版本Uboot比如2015年之后引入驱动模型的版本默认都会走向DM_I2C这条路。那么问题来了作为开发者我们该怎么选是图省事用老方法还是拥抱新框架这两种方式在代码里到底长什么样初始化流程差在哪API用起来手感如何今天我就结合自己这些年调试各种板子的经验把这“两条路”给你掰开揉碎了讲清楚帮你下次做选择时心里更有底。咱们的目标是不管黑猫白猫能抓到老鼠就是好猫但咱得知道每只猫的脾气和抓老鼠的套路。2. 传统模式SYS_I2C直来直去的“老炮儿”传统模式是Uboot里I2C驱动最原始、最直接的实现方式。它的设计哲学非常朴素给你几个全局函数你直接调用就行了不用关心总线是怎么管理的设备是怎么抽象的。这种方式在早期Uboot中非常普遍代码集中在drivers/i2c/目录下比如i2c_core.c这个文件就是它的核心。2.1 核心API与使用流程传统模式的使用流程可以说是“三板斧”非常固定。我画个简单的流程图在脑子里帮你理解选定总线告诉系统你要用哪一条I2C总线。因为一个SoC上可能有多个I2C控制器比如I2C0, I2C1。执行读写调用读写函数指定从设备地址、寄存器地址然后收/发数据。对应的核心API就那几个我当年都是直接背下来的/* 1. 设置当前使用的I2C总线编号 */ int i2c_set_bus_num(unsigned int bus); /* 2. 基础的读/写函数 */ int i2c_read(uint8_t chip, unsigned int addr, int alen, uint8_t *buffer, int len); int i2c_write(uint8_t chip, unsigned int addr, int alen, uint8_t *buffer, int len); /* 3. 常用的单字节寄存器读/写便捷函数 */ uint8_t i2c_reg_read(uint8_t addr, uint8_t reg); void i2c_reg_write(uint8_t addr, uint8_t reg, uint8_t val);我来给你拆解一下这几个参数是啥意思这是我当初调试时最容易迷糊的地方chip这是I2C从设备的7位地址。注意是7位比如一个EEPROM的地址是0x507位这里就填0x50。很多芯片手册会写8位的地址带上了读/写位比如写地址0xA0读地址0xA1这时候你需要右移一位得到0x50再传进来。Uboot会在内部帮你补上读/写位。addr这是从设备内部寄存器的地址。比如你想读EEPROM里偏移0x100处的数据这里就是0x100。alen上面这个寄存器地址的字节长度。常见的是1字节alen1比如很多传感器也有些设备是2字节地址alen2比如大容量的EEPROM。buffer和len就是数据缓冲区指针和要读写的数据长度了。一个典型的使用代码片段看起来是这样的unsigned char data_buf[10]; int ret; /* 切换到I2C1总线假设总线编号为1 */ i2c_set_bus_num(1); /* 从地址为0x68的设备例如一个RTC芯片读取其0x00寄存器开始的10个字节 */ ret i2c_read(0x68, 0x00, 1, data_buf, 10); if (ret) { printf(I2C read failed!\\n); } /* 向同一个设备的0x08寄存器写入一个字节0xAB */ i2c_reg_write(0x68, 0x08, 0xAB);2.2 优势与局限简单背后的代价这种传统模式最大的优点就是简单直观。对于快速验证、调试或者在一个已知的、简单的硬件平台上它非常高效。你不需要定义复杂的设备树DTS节点也不需要理解驱动模型那一套直接调用函数就能干活。我早期做项目为了赶进度经常就用这种方式先让硬件跑起来。但是它的缺点也同样明显项目复杂了之后就会感到掣肘全局状态易冲突i2c_set_bus_num设置的是全局当前总线。如果你的代码里有多处或者多个任务虽然Uboot基本是单任务需要操作不同的I2C总线就得非常小心地来回切换一不留神就串台了。缺乏设备抽象它只认“总线号”和“芯片地址”不认为这是一个“设备”。这意味着设备相关的配置比如速率、超时时间很难与这个“设备”绑定通常是以全局配置的形式存在。与内核驱动脱节它的配置和初始化方式与Linux内核的I2C子系统完全不同。内核里我们依赖设备树来描述硬件而传统模式往往是在板级头文件比如include/configs/board.h里用一堆宏来定义。这导致板级移植时Uboot和内核的I2C配置是两套逻辑维护起来麻烦容易出错。扩展性差想要支持一种新的I2C控制器你需要实现一整套指定的接口函数i2c_read,i2c_write等并集成到全局框架里耦合度比较高。正因为这些局限尤其是为了与内核的设备树模型对齐、提高代码的复用性和可维护性Uboot社区引入了驱动模型Driver ModelI2C子系统也随之升级到了DM_I2C模式。3. 驱动模型模式DM_I2C面向对象的“新框架”驱动模型Driver Model简称DM是Uboot仿照Linux内核设计的一套设备驱动管理框架。它的核心思想是面向对象和基于设备树。在DM框架下一切皆为“设备”udevice每个设备都属于一个“类”uclass比如I2C总线是一个类I2C控制器是这类下的具体设备挂载在I2C总线上的EEPROM、传感器等是从设备。DM_I2C就是I2C子系统在DM框架下的实现。它的代码主要位于drivers/i2c/i2c-uclass.c。使用它意味着你要按照DM的“规矩”来办事。3.1 核心概念与使用流程使用DM_I2C你不再直接操作全局函数而是要先获取代表硬件设备的udevice对象然后通过这个对象来操作。流程上比传统模式多了一步“设备获取”获取总线设备根据总线序列号类似之前的总线号找到对应的I2C控制器设备。获取芯片设备在指定的总线上根据从设备地址创建一个代表该芯片的“虚拟”设备对象。执行读写通过这个芯片设备对象进行读写操作。对应的API也换了一套风格/* 1. 根据Uclass ID和总线序列号获取总线设备的udevice指针 */ int uclass_get_device_by_seq(enum uclass_id id, int seq, struct udevice **devp); /* 对于I2C总线id是UCLASS_I2C */ /* 2. 获取代表特定从设备的udevice这里实际是获取一个“芯片描述句柄” */ int i2c_get_chip(struct udevice *bus, uint chip_addr, uint offset_len, struct udevice **devp); /* 3. 可选设置该芯片的寄存器地址长度 */ int i2c_set_chip_offset_len(struct udevice *dev, uint offset_len); /* 4. 通过设备句柄进行读写 */ int dm_i2c_read(struct udevice *dev, uint offset, uint8_t *buffer, int len); int dm_i2c_write(struct udevice *dev, uint offset, const uint8_t *buffer, int len); /* 以及便捷的单寄存器读写 */ int dm_i2c_reg_read(struct udevice *dev, uint offset); int dm_i2c_reg_write(struct udevice *dev, uint offset, uint value);看起来步骤多了但这样写出来的代码更清晰设备归属感更强。一个典型的使用例子struct udevice *bus_dev, *chip_dev; unsigned char data_buf[10]; int ret; /* 1. 获取序列号为1的I2C总线设备 */ ret uclass_get_device_by_seq(UCLASS_I2C, 1, bus_dev); if (ret) { printf(Failed to get I2C bus 1\\n); return; } /* 2. 获取总线上地址为0x68的芯片设备句柄并指定其寄存器地址长度为1字节 */ ret i2c_get_chip(bus_dev, 0x68, 1, chip_dev); if (ret) { printf(Failed to get chip at 0x68\\n); return; } /* 3. 现在可以通过chip_dev来操作了 */ ret dm_i2c_read(chip_dev, 0x00, data_buf, 10); if (ret) { printf(DM I2C read failed!\\n); } ret dm_i2c_reg_write(chip_dev, 0x08, 0xAB);3.2 优势与深度整合DM_I2C的优势正是为了解决传统模式的痛点设备树驱动这是最大的变革I2C控制器的启用、引脚复用配置、时钟频率等现在都可以并且推荐通过**设备树DTS**来描述。Uboot在启动时会解析设备树自动根据节点创建对应的udevice。这使得Uboot和Linux内核的硬件配置可以高度统一一份设备树或稍作修改两者都能用极大降低了移植和维护成本。真正的设备抽象每个I2C从设备芯片都可以通过i2c_get_chip获得一个独立的句柄。这个句柄可以携带该设备特有的信息如地址长度、特定标志位并且多个设备可以同时存在互不干扰。更好的封装与复用I2C控制器驱动以标准driver的形式注册实现了probe、remove、xfer等方法。驱动模型负责总线和设备的生命周期管理。想要支持新的I2C控制器你只需要实现一个符合DM框架的驱动即可与核心子系统解耦。与命令行完美集成DM_I2C与Uboot的i2c命令子系统是天然集成的。当你使用i2c dev、i2c probe、i2c md、i2c mw等命令时背后调用的就是DM_I2C的接口。命令可以自动发现设备树上已有的总线并列出探测到的设备交互体验更好。4. 关键差异对比与实战选择指南光讲原理可能还有点抽象我把这两种方式的核心差异整理成下面这个表格你可以一目了然地看到它们的区别特性维度传统模式 (SYS_I2C)驱动模型模式 (DM_I2C)核心宏CONFIG_SYS_I2CCONFIG_DM_I2C设计哲学过程式全局函数调用面向对象基于设备树和设备模型设备描述无设备概念仅地址和总线号有udevice对象代表总线或芯片硬件配置板级头文件中的宏定义设备树DTS节点初始化在init_sequence_f中显式调用由驱动模型自动probe设备树节点API风格i2c_read(chip, addr, ...)dm_i2c_read(udevice, offset, ...)多设备管理需手动切换全局总线号易冲突每个设备独立句柄并发性好与内核一致性差两套配置好可共享设备树代码位置drivers/i2c/i2c_core.c等drivers/i2c/i2c-uclass.c等适用场景老项目维护快速原型验证资源极度受限新项目开发复杂多总线系统追求与内核统一那么在实际项目中我们该如何选择呢根据我的经验可以遵循以下原则新项目无脑选DM_I2C如果你的Uboot版本支持现在主流版本都支持并且是从零开始移植或开发强烈建议使用DM_I2C。这是Uboot社区的主流和未来方向能让你享受到设备树带来的维护便利并且更好地与内核生态对接。维护老项目评估成本如果你接手的是一个使用传统模式的老代码库并且它工作稳定没有新增I2C设备的需求那么可能没必要大动干戈地切换到DM_I2C。毕竟修改涉及到底层初始化和所有调用I2C API的代码有一定风险。需要兼容新旧版本这里有一个非常重要的过渡特性Uboot考虑到了从传统模式迁移到驱动模型的困难提供了一个兼容层。通过定义宏CONFIG_DM_I2C_COMPAT你可以在启用DM_I2C的同时让原来那些基于i2c_read/i2c_write的老代码继续编译和运行。兼容层的代码在drivers/i2c/i2c-uclass-compat.c中它通过包装器将传统API调用转发到新的DM接口上。这在大型项目渐进式迁移时非常有用。资源与复杂度权衡DM_I2C会引入驱动模型本身的一些开销内存、代码尺寸。在极其资源受限的Bootloader中比如某些MCU的Bootloader如果只有一两个简单的I2C操作传统模式的精简优势可能更明显。但对于大多数应用处理器AP平台这点开销可以接受。5. 在Uboot命令行中玩转I2C无论底层是传统模式还是驱动模型对于开发者来说一个极其好用的工具就是Uboot的命令行I2C工具。它让你无需修改代码、重新编译烧录就能直接探测、读写I2C设备是硬件调试的“神器”。要启用它需要在配置中打开CONFIG_CMD_I2C。在Uboot命令行下输入i2c并回车就会看到所有子命令的用法。我挑几个最常用的给你讲讲并解释一下那些看起来有点奇怪的参数i2c bus列出当前系统中所有可用的I2C总线。在DM_I2C下这会显示从设备树探测到的总线。i2c dev [bus_num]显示或设置当前操作的I2C总线。例如i2c dev 1切换到总线1。i2c probe [addr]探测总线上存在的设备地址。不加参数会扫描所有地址加参数如i2c probe 0x50则检查该地址是否有设备响应。i2c md chip_addr reg_addr[.len] count这是最常用的读命令也是参数最需要理解的一个。chip_addr7位设备地址如0x50。reg_addr[.len]关键寄存器地址后面可以跟.0、.1或.2表示寄存器地址的字节长度。.1是默认值1字节可以省略。例如0x00.1或0x00表示从一字节地址0x00开始读0x4081.2表示从两字节地址0x4081开始读。count要读取的字节数量16进制表示。示例i2c md 0x5d 0x4081.2 2表示从地址0x5D的设备读取其16位寄存器地址0x4081开始的2个字节数据并显示在终端。i2c mw chip_addr reg_addr[.len] value [count]写命令。将value或连续多个value写入设备的指定寄存器地址。示例i2c mw 0x6b 0xd0.1 0x15 1表示向地址0x6B设备的0xD0寄存器1字节地址写入1个字节的值0x15。我强烈建议你在实际板子上多敲敲这些命令感受一下。特别是.len这个参数很多读写失败都是因为它设错了。比如操作一个16位地址的EEPROM如果你用了默认的.1地址只发了一个字节出去结果肯定不对。6. 迁移实践与常见问题排查最后分享一些从SYS_I2C迁移到DM_I2C或者直接使用DM_I2C时可能遇到的“坑”。迁移步骤确保Uboot版本支持检查你的Uboot版本是否已经包含了完整的DM_I2C驱动和你要使用的I2C控制器驱动。修改配置文件在板级配置头文件或defconfig中将CONFIG_SYS_I2C改为CONFIG_DM_I2C并添加CONFIG_DM_I2C_COMPAT以启用兼容层如果需要。添加设备树节点在Uboot的设备树源文件dts或dtsi中为你的I2C控制器添加节点内容通常可以参考内核的设备树。确保compatible字符串与驱动匹配并正确配置时钟、引脚复用等属性。更新板级初始化移除原来在板级代码中调用i2c_init等传统初始化函数的部分。DM框架会自动初始化。渐进式修改代码利用兼容层先保证系统能启动。然后逐步将代码中的i2c_read/i2c_write替换为dm_i2c_read/dm_i2c_write。记得修改获取设备句柄的逻辑。常见问题排查i2c probe找不到设备检查物理连接上拉电阻、电源、SDA/SCL线是否接对。检查设备地址确认是7位地址并且没有和内核手册的8位地址混淆。检查总线速度有些设备对初始化速度有要求确认Uboot中I2C控制器初始化速度是否合适可通过i2c speed命令查看和设置。检查设备树节点在DM_I2C下确认设备树中的I2C控制器节点已正确启用且引脚复用配置正确。读写数据不正确首要怀疑寄存器地址长度.len这是最高频的错误。用i2c md命令时务必根据芯片手册指定正确的.1或.2。检查读写时序有些芯片对读写序列有特殊要求比如需要发送一个“命令字节”再启动读操作。这时可能需要使用更底层的dm_i2c_xfer接口或编写特定芯片的驱动。电压电平匹配主控和从设备的I2C电平是否一致如1.8V vs 3.3V。驱动未正确绑定在Uboot命令行使用dm tree命令可以查看驱动模型下的设备树状态检查你的I2C总线设备是否被正确识别和绑定bound了驱动。使用i2c bus命令看你的总线是否出现在列表中。调试I2C逻辑分析仪或者带I2C解码功能的示波器几乎是必备的。它能让你直观地看到总线上发出的地址、数据、ACK/NACK信号很多软件上纠结半天的问题用仪器一看波形就真相大白。我记得有一次死活读不出一个传感器的ID用逻辑分析仪抓波形发现发出的设备地址根本不对最后发现是芯片手册的地址描述格式和Uboot要求的不一样右移一位就解决了。所以硬件调试眼见为实。