ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PetaLinux下Zynq I2C配置全流程:从设备树到驱动调试

PetaLinux下Zynq I2C配置全流程:从设备树到驱动调试 最近在一个Zynq平台的项目里把一颗RTC和一板EEPROM接到了I2C总线上顺手把PetaLinux工程里的I2C配置完整捋了一遍。这套流程说复杂不复杂但坑不少尤其对于刚接触PetaLinux的人经常会卡在“明明改好了设备树为什么系统里就是看不到总线”这类问题上。这篇文章我把从硬件确认、设备树修改、内核配置、rootfs集成到最终部署验证的完整路径写出来都是我在实际项目中踩过、验证过的做法。写这篇文章的目标读者是用PetaLinux做Zynq/MPSoC开发的工程师尤其是刚把I2C外设加到硬件里、正准备让Linux跑通的这批人。我默认你已经会用Vivado建工程、能生成XSA也熟悉基本的Linux命令行。如果这些还不熟先补一补再来看也不迟。读完这篇文章你能独立把一个挂在PS侧或者EMIO上的I2C设备在Linux里完整跑起来并且遇到问题时知道从哪里开始排查。1. 配置前的整体思路与必备认知1.1 先分清是PS侧I2C还是PL侧I2CPetaLinux里配置I2C第一步不是打开配置文件而是先搞清楚你的I2C控制器到底接在哪边。Zynq-7000的PS内部自带两个I2C控制器I2C0和I2C1对应的设备树节点通常是i2ce0004000和i2ce0005000。驱动用的是Cadence的I2C IP核设备树里的compatible一般是cdns,i2c-r1p10或cdns,i2c-r1p14。这两个控制器可以直接用PS的MIO引脚比如I2C0的SCL/SDA可以分配到MIO[10:11]这类引脚上具体能分到哪些MIO组合要以Zynq-7000 TRM里的MIO表为准。如果你的I2C接口是通过PL逻辑扩展出来的比如接了Xilinx的AXI IIC IP核那情况就不一样了。这种情况设备树里会出现一个挂在AXI总线上的I2C控制器节点而且你需要确保PL侧的bitstream已经加载不然CPU访问不到控制器的寄存器地址。两种场景的排查思路完全不同所以在动手之前先确认这一点能帮你省掉后面好几个小时的盲目调试。怎么确认最简单的方法是打开Vivado工程的Block Design看I2C IP是直接从PS端连到MIO的IIC_0还是需要经过PL的AXI互联。也可以在PetaLinux启动后执行i2cdetect -l如果看到了类似i2c-0和i2c-1两个总线通常就是PS的两个控制器如果看到i2c-2、i2c-3那多半还挂了PL侧的I2C。1.2 配置I2C之前必须先拿到的四样信息原理图里I2C挂在哪条总线上是PS的I2C0还是I2C1还是PL侧扩展出来的某条总线。总线不同设备树里操作的对象就不同。外设的7位地址几乎所有I2C外设数据手册里都会给地址比如AT24C02的地址是0x50但很多资料会写成0xA0那是8位地址含读写位的写法。设备树、i2c-tools里用的都是7位地址。这个换算关系我后面还会再提。外设的工作模式是标准的100kHz、400kHz快速模式还是1MHz高速模式这决定设备树里的clock-frequency怎么填。内核里有没有对应的设备驱动比如EEPROM用at24驱动RTC用rtc-ds1307、rtc-pcf8563这类驱动传感器则五花八门。如果内核里没有对应驱动你写了设备树节点也没用只会多报一条Unknown device之类的错误。这些信息里面最容易忽略的是外设地址。I2C协议里地址分7位和10位两种绝大多数消费级芯片都是7位地址。数据手册里如果写0xA0那就是7位地址0x50左移一位加上读写位得到的。设备树里写reg属性时填7位地址0x50不是0xA0这一点我在第3章会用一个实际例子讲明白。2. 在PetaLinux工程里让I2C控制器跑起来2.1 从Vivado到XSA先把外设使能很多人一上来就改设备树发现怎么改都不生效最后回过头来查居然是硬件工程里根本没把I2C的PS控制器使能。在Vivado里如果你要让PS的I2C0工作需要打开Zynq PS配置界面在Peripheral I/O里勾选I2C 0然后在MIO Configuration里给SCL和SDA分配具体的MIO引脚。这一步没做后面的一切都是空中楼阁。完成硬件配置后重新生成bitstream并导出XSA。然后在PetaLinux工程里执行petalinux-config --get-hw-description/path/to/xsa目录这个命令会把工程里硬件相关的配置更新一遍。之后你可以在project-spec/meta-user/recipes-bsp/device-tree/files/目录下看到生成的设备树相关文件。这里有一个习惯我一直保持拿到新XSA之后先去查看一下生成的设备树里是否包含了I2C节点如果硬件使能正确通常会有类似这样的内容i2ce0004000 { compatible cdns,i2c-r1p10; reg 0xe0004000 0x1000; interrupts 0 25 4; clocks clkc 38; ... };注意这个节点里可能没有status okay而是继承自父节点或者默认打开。但如果你用的是其他的BSP包有时会写成status disabled这种情况需要在设备树中显式打开。2.2 设备树源文件里添加I2C控制器配置PetaLinux工程中用户自定义设备树的推荐位置是project-spec/meta-user/recipes-bsp/device-tree/files/system-user.dtsi。这个文件专门留给用户做修改使用避免直接改BSP自动生成的设备树因为后者在重新获取硬件描述时会被覆盖。要让I2C0跑起来在system-user.dtsi里加上/include/ system-conf.dtsi i2c0 { status okay; clock-frequency 100000; pinctrl-names default; };这里status okay是打开控制器clock-frequency设置总线频率。Zynq PS的I2C控制器时钟来自PS的I2C参考时钟一般默认能跑到400kHz但为了稳妥我在项目里如果外设没有特殊要求都会设成100kHz。外设跑得快不快有时候不重要稳定才是第一位的。还有一种情况是控制器默认引用了一个已经定义的引脚配置而你的硬件改到了别的MIO上这时候可能需要添加pinctrl-0属性来匹配。不过在大多数PetaLinux工程里PS侧I2C的引脚mux已经在启动时的固件配置里完成设备树里不需要重复描述。2.3 内核配置确保I2C驱动编进来设备树节点有了还得确保内核把对应的I2C控制器驱动编进去。执行petalinux-config -c kernel进入内核配置菜单然后查找以下几项CONFIG_I2CI2C核心支持必须为y或m。CONFIG_I2C_CADENCECadence I2C控制器驱动也就是PS侧I2C的驱动必须选上。CONFIG_I2C_CHARDEVI2C设备文件接口编译成内核模块后会在/dev/i2c-N生成设备节点i2c-tools依赖这个。CONFIG_EEPROM_AT24常见EEPROM驱动如果你要挂AT24C系列选上。CONFIG_RTC_DRV_DS1307、CONFIG_RTC_DRV_PCF8563等具体看你的RTC型号。PetaLinux的kernel配置有个特点你直接改内核源码里的.config下次petalinux-build的时候可能会被还原。正确的做法是把要开启的配置项写进PetaLinux用户配置文件中比如project-spec/meta-user/recipes-kernel/linux/linux-xlnx/user_*.cfg常见的文件名是user_2025.1.cfg或者直接就一个user.cfg根据你使用的PetaLinux版本不同而有差异。每次构建时它会自动合并进去。例如CONFIG_I2Cy CONFIG_I2C_CADENCEy CONFIG_I2C_CHARDEVy CONFIG_EEPROM_AT24y这样做的好处是配置可追溯、可重复换一台电脑重新构建时也能复现。我就是因为这个习惯在升级PetaLinux版本之后少踩了很多坑。3. 挂载具体I2C设备设备树、驱动与用户空间3.1 先扫描总线找到外设地址控制器跑起来之后先别急着写设备树子节点。我个人的习惯是先构建一个“最小系统”用i2cdetect把总线上的设备地址扫出来确认硬件连接没问题再继续往下做。构建好后启动板子执行i2cdetect -l能看到类似这样的输出i2c-0 i2c Cadence I2C Controller I2C adapter i2c-1 i2c Cadence I2C Controller I2C adapter这表示内核已经成功注册了两条I2C总线。接下来扫描总线0i2cdetect -y 0如果你的EEPROM地址是0x50屏幕上就会在0x50位置出现一个十六进制编号比如50表示在这个地址检测到了设备。如果没有出现先不要怀疑软件优先检查硬件常见的坑我在第5章里详细说。3.2 在设备树中描述I2C子设备扫描确认地址之后就可以在对应的I2C控制器节点下添加子设备了。假设I2C0上挂了一个AT24C02 EEPROM7位地址是0x50设备树里这么写i2c0 { status okay; clock-frequency 100000; eeprom50 { compatible atmel,24c02; reg 0x50; pagesize 8; }; };这里reg属性的0x50就是7位设备地址前面我一直强调的0x50与0xA0的区别就在这里。很多数据手册会给一个地址字节比如AT24C02的Device Address是1010 A2 A1 A0 R/W其中前四位固定为1010后三位由硬件引脚A0-A2决定最低位是读写位。如果把整个字节算下来是0xA0去掉最低位后就是0x50。设备树里要填0x50i2c-tools里扫出来的也是0x50。对于挂在同一总线上的多个设备直接在节点下并列添加即可i2c0 { eeprom50 { compatible atmel,24c02; reg 0x50; }; rtc51 { compatible pcf8563; reg 0x51; }; };要注意的是每个子节点的reg必须与硬件上的地址跳线一致。如果A0、A1、A2引脚悬空或者没接地址就是芯片手册里的默认值不少板子的默认值需要重新核对原理图。3.3 把厂商提供的设备树内容搬进PetaLinux工程实际项目中经常遇到这种情况你是从方案商或芯片原厂拿到了一套旧设备树里面已经写好了ADC、传感器、AD9361之类设备的I2C挂载节点现在要把它挪到新建的PetaLinux工程里。直接全文复制system-user.dtsi大概率会编译报错因为旧工程里引用的有些头文件路径、时钟节点名和新的BSP不一致。我的做法分三步第一步先对比新旧两个设备树里I2C控制器的节点名。比如旧工程里可能叫i2ce0004000新工程里可能有一个i2c0的别名写法不同但指向同一个控制器。优先使用新工程里的别名写法。第二步把旧设备树中目标外设的子节点单独摘出来只保留compatible、reg、interrupts这类核心属性以及外设本身必需的私有属性。像时钟、复位、引脚这类属性如果新工程里已有全局定义就删掉不要。比如AD9361的设备树节点会带clocks属性搬到新工程时先注释掉让驱动自动探测跑起来再按报错逐项补。第三步把整理好的节点粘贴到system-user.dtsi对应控制器下执行petalinux-build如果编译报语法错误检查括号是否配对、分号是否遗漏。很多时候设备树编译错误都是这类低级问题别慌编译器会告诉你具体在哪一行。3.4 内核I2C设备驱动注册机制速览设备树写对了能不能真正驱动起来还得看内核里有没有对应的I2C驱动以及驱动的匹配方式是否和设备树里的compatible一致。I2C设备驱动最常用的注册写法是static const struct of_device_id my_i2c_dt_ids[] { { .compatible myvendor,mydevice }, { } }; MODULE_DEVICE_TABLE(of, my_i2c_dt_ids); static struct i2c_driver my_i2c_driver { .driver { .name my_i2c_driver, .of_match_table my_i2c_dt_ids, }, .probe_new my_i2c_probe, .id_table my_i2c_id_table, }; module_i2c_driver(my_i2c_driver);当驱动加载时内核会把这个驱动的of_match_table里列出的compatible和设备树节点里声明的compatible做比较匹配上了就调用probe函数同时注册一个I2C客户端设备。如果你写的是自定义驱动就一定要确保设备树里compatible字符串和of_match_table里的完全一致多一点空格都不行。注册完成后你会在/sys/bus/i2c/devices/下看到类似0-0050这样的目录0-0050表示总线0上地址为0x50的设备。看到这个目录基本说明设备树解析和设备注册已经成功接下来就是用户空间的事情了。4. Rootfs集成与完整构建部署流程4.1 在PetaLinux rootfs中加入i2c-tools内核和设备树搞定了还需要用户空间的调试工具。PetaLinux默认不会把i2c-tools打进根文件系统需要手动配置。执行petalinux-config -c rootfs然后按菜单路径进入Filesystem Packages → console → tools → i2c-tools勾选上i2c-tools保存退出。这里建议把i2c-tools的dev和dbg包也一起选上有些调试工具如i2c-stub-from-dts在dbg包里。如果你更喜欢直接改配置文件可以在project-spec/meta-user/conf/user-rootfsconfig里添加CONFIG_i2c-tools然后在petalinux-config -c rootfs中确认生效。这个方法的优点是可以直接进版本管理换人换机器都能复现。总之只要rootfs配置已生效构建出的镜像里就会包含i2cdetect、i2cget、i2cset、i2cdump这一组命令行工具。4.2 重新构建、打包与制作SD卡启动镜像改完设备树、内核配置和rootfs之后回到PetaLinux工程根目录petalinux-build构建过程会重新编译设备树、内核以及根文件系统。构建完的产物分布在images/linux/目录下核心是三个文件BOOT.BIN、boot.scr和image.ub。如果BOOT.BIN不存在需要执行打包命令petalinux-package --boot --format BIN --fsbl images/linux/zynq_fsbl.elf --fpga images/linux/design.bit --u-boot images/linux/u-boot.elf这条命令会根据你实际使用的硬件工程生成包含FSBL、bitstream和U-Boot的BOOT.BIN。image.ub则是内核设备树根文件系统的镜像。制作SD卡时我的做法是sudo fdisk /dev/sdX # 创建一个FAT32分区大约500MB用于存放BOOT.BIN、boot.scr、image.ub # 剩余空间创建一个ext4分区用于根文件系统 sudo mkfs.vfat -n BOOT /dev/sdX1 sudo mkfs.ext4 -L rootfs /dev/sdX2 sudo mount /dev/sdX1 /mnt/boot sudo mount /dev/sdX2 /mnt/rootfs sudo cp images/linux/BOOT.BIN images/linux/boot.scr images/linux/image.ub /mnt/boot/ sudo tar xf images/linux/rootfs.tar.gz -C /mnt/rootfs sync从PetaLinux 2020.1之后的版本开始image.ub可以同时包含内核和设备树根文件系统则单独打包。只要FAT分区里有这三个文件开发板就能正常启动。4.3 启动后的完整验证清单板子启动后我一般按这个顺序验证I2C是否正常工作dmesg | grep i2c检查内核启动时是否打印了I2C控制器的注册信息。正常能看到类似“i2c /dev entries driver”或者“cdns-i2c e0004000.i2c: 400 kHz mmio ...”。i2cdetect -l确认有几个I2C adapter。i2cdetect -y 0扫描总线0上的设备确认目标设备地址出现。i2cget -y 0 0x50 0x00如果外设是EEPROM读取0x00地址的字节看返回值是否为0xFF或预期值。i2cset -y 0 0x50 0x00 0x55写入一个字节再i2cget读回来确认读写都正常。如果挂的是0.96寸OLED这类显示设备还可以配合i2cdump查看驱动初始化时往控制寄存器里写的数据。总之验证这一步的核心是确认总线通、地址对、读写工作正常做到这三条你的I2C软件链路就已经完全跑通了。5. 常见问题与排查技巧实录5.1 设备树改了但系统启动后没生效这是最高频的坑。很多人改了system-user.dtsi执行了petalinux-build烧进去却发现行为没变化。排查思路很简单先确认你启动时用的设备树是不是新编译出来的那份。如果你用的是image.ub设备树已经打进这个文件里了只重新生成system.dtb没有用必须重新打包image.ub。正确做法是重新执行petalinux-build让它把设备树、内核、根文件系统都重新打包。另一个坑是PetaLinux在构建时不一定每次都重新编译设备树如果只修改了dtsi但构建系统判断缓存未失效就会出现改了等于没改的情况。这时可以手动清理petalinux-build -c device-tree -x distclean petalinux-build设备树是否生效最直接的确认方法是启动后查看ls /proc/device-tree/如果节点存在再去soc目录或者对应地址目录下找i2ce0004000再看里面的status属性是不是okay。这个目录的内容就是内核解析设备树后的结果绕过了所有中间环节非常直观。5.2 i2cdetect扫描不到设备扫描不到设备时先用万用表量一下SCL和SDA引脚上的电平静态时都应该是高电平因为I2C总线靠上拉电阻默认拉高。如果某个引脚是低电平可能是上拉电阻没焊、I2C设备挂死、引脚mux配置错误或者地址冲突。我遇到过最隐蔽的一种情况是板子上某个I2C设备地址和i2cdetect扫描地址发生冲突导致扫描流程卡死表现为i2cdetect执行后停在某个地址半天不动。这时可以用i2cdetect -y -r 0强制使用读模式扫描或者用i2cdetect -y -q 0使用快速模式通常能绕过这类问题。还有一类常见问题是SDA和SCL接反了。原理图上看着没问题但实际PCB布线时交叉了这种情况用示波器抓波形最容易发现。如果示波器上能看到SCL有脉冲而SDA没有响应基本就是设备没有ACK这时先查地址、查供电最后再查接线。5.3 I2C挂在EMIO上怎么处理Zynq的PS侧I2C不一定只能走MIO也可以从EMIO引出到PL引脚。很多项目为了布线方便会让I2C信号从PL侧引脚出来这就涉及两个问题一是PL的bitstream必须加载EMIO引脚才有输出二是设备树里需要确保引脚复用配置正确。在硬件侧Vivado里依然是在PS的I2C外设配置中使能I2C但在MIO配置里选择EMIO而不是某个具体的MIO引脚。这样I2C控制器的信号就通过EMIO连到了PL逻辑再到板级引脚上。这种情况下设备树里I2C控制器的地址、中断和时钟都与MIO模式相同只是引脚属于PL侧。启动时如果bitstream没有加载你可能会看到I2C控制器已经注册成功但i2cdetect扫描时所有地址都无响应。这时优先确认FPGA加载是否成功cat /sys/class/fpga_manager/fpga0/state状态如果是active再检查PL侧引脚有没有被其他IP占用。EMIO的I2C还容易受PL侧IO标准配置影响比如电压域设置不对导致引脚电平根本拉不上去。这类问题软件上往往无从下手必须回到Vivado里核对引脚的IO Standard。5.4 内核日志与常见报错速查为了方便排查我把实际项目里常见的I2C报错整理成了一个速查表报错信息通常原因排查方向timeout waiting for bus总线上有设备拉低SCL或其他主设备占用用示波器看SCL电平检查是否有设备挂死arbitration lost多主设备同时发起传输或总线干扰严重检查上拉电阻阻值、总线长度、是否存在两个主设备Remote I/O error从设备无ACK核对7位地址、供电、接线用i2cdetect扫描controller timed out控制器时钟配置异常检查内核时钟树确认I2C控制器时钟频率正常i2c_xfer failed驱动与设备通信失败查看具体驱动代码确认寄存器访问时序还有一条经验i2cset写数据后立刻i2cget读回来的值不对有时不是I2C总线的问题是外设本身需要延时。尤其EEPROM写周期是5毫秒左右写完马上读可能读到旧数据。这时候加一个usleep或者sleep再读往往就正常了。这类问题在脚本里很常见别一上来就怀疑硬件。5.5 关于设备树覆盖与复用的一点建议我在多个PetaLinux版本之间切换后最大的体会是每个版本的BSP生成的设备树结构都可能有差异比如有的版本里I2C节点已经有status okay有的版本里没有。因此在system-user.dtsi里写控制器节点时不要只写子设备还要显式写上status okay和clock-frequency。这样即使自动生成的设备树随版本变化你的用户配置依然能稳定覆盖上去。另外如果老BSP里用了i2ce0004000这种路径式写法而新BSP已经定义了i2c0别名我建议优先使用i2c0。别名方式更直观而且在设备树编译时能自动匹配到具体控制器节点不至于因为路径差异导致节点没有被覆盖。我见过有人把整段i2ce0004000 { ... }复制进新工程的system-user.dtsi结果新工程里这个控制器节点名变成了i2ce0005000等于改了另一个控制器折腾了很久才发现问题。根据我个人的实践经验配置PetaLinux I2C这件事真正的分水岭不在于你会不会敲那几条命令而在于你把硬件、设备树、内核驱动这三层的关系理清没有。硬件给的是物理通路设备树告诉内核“这里有什么、地址是多少”驱动则负责真正把数据读写出来。三层只要有一层脱节表现出来就是“总线不通”或者“设备不识别”。先跑通i2cdetect再谈设备树、再调驱动这个顺序能帮你省掉至少一半的排查时间。最后分享一个我自己用的小技巧把下面这几条命令写成一个i2c_check.sh脚本放在板子里随时执行能快速定位八成以上的I2C问题#!/bin/sh echo I2C adapters i2cdetect -l echo Scan bus 0 i2cdetect -y 0 echo Scan bus 1 i2cdetect -y 1 echo Kernel i2c log dmesg | grep -i i2c | tail -n 20脚本不值钱值钱的是你拿到输出之后能一眼看出问题在哪条链路。I2C这个协议本身不复杂生命周期也长把一套排查方法固化下来以后在哪个平台、哪个项目里都通用。
返回列表