☰
ZYNQ IIC驱动实战:从PS端裸机到Linux设备树调试指南
2026/10/13 12:03:02 网站建设 项目流程

1. 先聊清楚:ZYNQ 上 IIC 究竟由谁来驱动

做嵌入式这些年,IIC 总线是我调得最多的低速总线,没有之一。温度传感器、EEPROM、RTC、姿态芯片、电源管理芯片,十块板子有九块挂着 IIC 设备。ZYNQ 基础教程里经常拿“驱动 IIC 外设”当第一课,原因也简单:它同时涉及硬件连线、寄存器操作、驱动接口和排查手段,是一个小但完整的外设驱动闭环。这篇文章主要讲 ZYNQ 的 PS 端 IIC 控制器,也就是从 ARM 侧直接操作 I2C0/I2C1,分别在裸机和 Linux 两种环境下跑通,并顺手把硬件上那些坑讲明白。如果你是第一次在 ZYNQ 上挂一块 EEPROM 或传感器,按这里的思路来,基本能少走一半弯路。

很多人一看到 ZYNQ,下意识就当成“FPGA 开发板”,满脑子都是 Verilog 和 IP 核。但 ZYNQ 的特殊之处在于它是一颗异构 SoC,PS 端本来就是一个完整的 ARM 处理器系统,自带两个 IIC 控制器。所谓“驱动 IIC 外设”,大多数情况下不是让你写 Verilog,而是用 C 代码操作 PS 端控制器,和单片机开发非常像。真正要用 PL 逻辑去做的场景也存在,但那是另一条技术路线。所以我先把这两条路分清楚,再往下讲怎么动手。

1.1 PS 端 IIC 控制器和 PL 端 AXI IIC,区别在哪里

PS 端的 IIC 控制器挂在 ARM 的互联总线上,通过一组寄存器被 CPU 访问。ZYNQ-7000 系列一般提供 I2C0 和 I2C1 两路控制器,引脚从 MIO 复用出来,例如 I2C0 经常分配在 MIO10/MIO11,I2C1 经常分配在 MIO14/MIO15,具体以芯片管脚手册和你的工程配置为准。这两路控制器不占 PL 逻辑资源,BSP 和 Linux 内核里都有现成驱动,裸机下也有 Xilinx 封装好的 XIicPs 驱动。

PL 端的 AXI IIC 则是另一种方式。当你需要把 IIC 外设接到 FPGA 可编程逻辑侧的引脚时,可以例化一个 AXI IIC IP 核,把它挂到 AXI 总线上,CPU 通过 AXI-Lite 接口读写它的寄存器。这样做的好处是引脚位置灵活、可以按需例化多个实例,但代价是消耗 PL 资源,驱动和中断路径也更绕。

举个实际选型例子:如果板子上的 EEPROM 直接连在 PS 的 MIO 引脚上,那毫无疑问用 PS IIC;如果某个传感器只能接到 PL 侧的扩展口,或者你的逻辑需求是 FPGA 直接采样传感器数据,那再用 AXI IIC。基础阶段我强烈建议先吃透 PS IIC,因为这个控制器足够简单、好排查,而且后续看 Linux 设备树也能直接对上。

1.2 一开始就要做的选型判断

不要看到“IIC 驱动”就直奔代码。动手前先问自己三个问题:从设备挂在哪条总线?用哪一路控制器?最终跑裸机还是 Linux?这三个问题直接决定后面所有步骤。

如果你的外设从 PS MIO 引出,裸机环境下,直接用 PS IIC 控制器;Linux 环境下,设备树里已经生成了 i2c0 和 i2c1 节点,挂上从设备子节点即可,也很顺畅。如果你的外设从 PL 自定义引脚引出,那就需要在 Vivado 里添加 AXI IIC 核,这时再考虑 PL 侧方案。还有一种常见情况是板子上 IIC 设备特别多,一条总线负载太重,可以把设备分到 I2C0 和 I2C1 两条总线上,必要时再加总线扩展芯片,但这是后话。

从学习路径上说,把 PS IIC 作为基础是性价比最高的。它让你先忽略 FPGA 逻辑层面的复杂度,把注意力集中在“总线协议 + 驱动接口 + 调试手段”这三件事上。等这条路走通了,再去看 AXI IIC,你会发现很多概念是共通的。

2. 动手之前,硬件准备别偷懒

软件调不通的时候,十有八九问题在硬件。IIC 是低速总线,理论上不复杂,但恰恰因为简单,硬件上的小问题容易被忽略。上拉电阻、设备地址、电平域、地线,任何一环出问题,代码写得再漂亮也白搭。

2.1 上拉电阻:为什么 IIC 总线不能省略

IIC 的物理层和 SPI、UART 不一样,SCL 和 SDA 都是开漏结构。控制器只能把总线拉低,想让它恢复高电平,必须靠外部上拉电阻。这条总线的“空闲状态”是高电平,一旦没有上拉电阻,总线就会浮空,量到的电压可能很低,也可能被干扰信号来回打,通讯自然失败。

我用过最多的上拉阻值是 4.7kΩ,适合 100kHz 标准速率。如果跑到 400kHz 快速模式,建议换成 2.2kΩ 甚至 1kΩ,因为速率上来之后,总线上升沿时间必须够短,阻值太大导致上升沿太缓,数据位就采样错了。当然,阻值也不能无限小,太小会增加灌电流,不过实际设计中只要不是极低阻值,问题都不大。判断方法很简单:板卡断电后用万用表量 SCL 和 SDA 对地阻值,通常能看到几 kΩ 的上拉电阻;上电后量空闲电平,应该都接近对应电压域的 VDD。

提示:有些开发板会把上拉电阻做到板子上,尤其是 PS 端 MIO,省得你再外接。但用杜邦线外接传感器时,飞线长度一旦超过十几厘米,就要重新考虑阻值和速率。我踩过这样的坑:温度传感器用 20cm 杜邦线挂在 400kHz 下,数据乱跳,降到 100kHz 就稳了。

2.2 从设备地址和寄存器地址,先看仔细

IIC 寻址常用的 7 位地址,读写位放在最低位。很多芯片手册上写“器件地址 0xA0/0xA1”,这里 0xA0 是写地址,0xA1 是读地址,它们是 8 位地址。而你在设备树 reg 属性里、或者在裸机驱动函数里填的,通常应该是 7 位地址 0x50。

这个换算关系是:0xA0 右移一位得到 0x50。芯片手册如果写“1010000A”,那前 7 位就是器件地址,最低 A 是读写位。以 AT24C 系列 EEPROM 为例,A2/A1/A0 引脚决定了低三位,所以同一颗芯片可以同时存在 0x50 到 0x57 多个地址。你实际用的是哪个,取决于硬件怎么接。

寄存器地址也要注意长度。有些传感器寄存器地址只有 1 个字节,有些需要 2 个字节,例如容量大的 EEPROM。发送流程会因此不同:1 字节地址的设备,发送“设备地址 + 寄存器地址 + 数据”即可;2 字节地址的设备,要先发高字节再发低字节,顺序错了读回来的数据全是乱的。

2.3 电平匹配和地线

PS 端 MIO 引脚的 IO 电压由相关 Bank 供电决定,ZYNQ 大部分板卡 PS 侧是 3.3V。如果你的从设备是 5V 逻辑,不能直接连,需要电平转换;如果是 1.8V 的传感器,同样要注意电平域匹配,必要时加转换芯片,比如常见的 PCA9306 双向电平转换方案。

还有一条容易被忽略:硬件连接必须保证共地。IIC 是参考地线的单端信号,主设备、从设备、调试工具之间如果地没接在一起,信号完全没有参考点,表现就是扫描不到设备或读取乱码。我遇到过几次“设备明明在板子上却扫描不到”的情况,最后都是地线没接好或者接触不良。

3. 裸机驱动:SDK 里把 IIC 跑起来

裸机是最容易理解 IIC 控制器的方式,没有操作系统的干扰,你可以直接看到驱动调用的每一个接口。Xilinx 的 BSP 已经把寄存器操作封装成了 XIicPs 驱动,我们实际要做的事情很简单:初始化控制器、设置速率和模式、然后调发送接收函数。

3.1 在 Vivado 和 SDK 里启用 IIC 控制器

先在 Vivado 中打开 Zynq PS 配置界面,在 I2C 页面使能需要使用的控制器,比如使能 I2C0,并确认 MIO 分配是否正确,再设置目标速率。这一步会生成硬件描述文件,也就是 .xsa 文件。之后把 .xsa 导入 SDK 或 Vitis,创建一个 standalone 平台的应用工程,BSP 会自动带上 XIicPs 驱动。

在工程代码里,你需要用到两个关键宏:设备 ID 和基地址。它们由 BSP 根据你的硬件配置生成,定义在 xparameters.h 中。命名类似XPAR_XIICPS_0_DEVICE_ID和XPAR_XIICPS_0_BASEADDR。如果你在 Vivado 里使能的 I2C0,那对应的是 XIICPS_0;如果使能 I2C1,则是 XIICPS_1。这里有个很常见的低级错误:配置了 I2C1,代码里却用 XIICPS_0 的 ID 初始化,结果当然什么都调不通。

3.2 初始化配置:时钟分频和速率选择

XIicPs 驱动初始化分为三步:查找配置、初始化实例、设置时钟和选项。代码如下:

#include "xiicps.h" #include "xparameters.h" #define IIC_DEVICE_ID XPAR_XIICPS_0_DEVICE_ID XIicPs IicInstance; XIicPs_Config *IicConfig; IicConfig = XIicPs_LookupConfig(IIC_DEVICE_ID); if (IicConfig == NULL) { return XST_FAILURE; } XIicPs_CfgInitialize(&IicInstance, IicConfig, IicConfig->BaseAddress); XIicPs_SetSClk(&IicInstance, 100000); XIicPs_SetOptions(&IicInstance, XIICPS_MASTER_OPTION | XIICPS_SCLK_GENERATOR_OPTION | XIICPS_REPETITIVE_START_OPTION);

XIicPs_SetSClk的作用是设置目标 SCL 时钟频率,驱动会结合控制器输入时钟自动计算分频系数,不需要你手动去改寄存器。基础学习用 100kHz 标准速率,稳定第一。

XIicPs_SetOptions里那三个选项建议都打开:MASTER_OPTION 把控制器设为主模式;SCLK_GENERATOR_OPTION 使能时钟发生器,这是 SCL 能输出的前提之一;REPETITIVE_START_OPTION 使能重复起始条件,很多读操作流程需要它,比如先写寄存器地址再切换为读。

注意:如果总线上挂了多个设备、电容偏大,400kHz 下通讯不稳定,最简单的办法是直接降到 100kHz,或者按下拉上拉阻值。速率不是越大越好,稳定才是第一优先级。

3.3 用 XIicPs 的轮询方式读写 EEPROM

下面是我常用的轮询读写函数。以 EEPROM 为例,写入函数把寄存器地址和数据拼在同一个缓冲里,一次性由XIicPs_MasterSend发出去;读取函数先发送寄存器地址,再用XIicPs_MasterRecv读回数据。

static int iic_write_reg(u8 dev_addr, u8 reg_addr, u8 *data, int len) { u8 buf[32]; int timeout = 100000; if (len > 30) { return -1; } buf[0] = reg_addr; memcpy(&buf[1], data, len); XIicPs_MasterSend(&IicInstance, buf, len + 1, dev_addr); while (XIicPs_BusIsBusy(&IicInstance) && timeout--) { /* 等待传输结束 */ } if (timeout <= 0) { return -1; } return 0; } static int iic_read_reg(u8 dev_addr, u8 reg_addr, u8 *data, int len) { int timeout = 100000; XIicPs_MasterSend(&IicInstance, &reg_addr, 1, dev_addr); while (XIicPs_BusIsBusy(&IicInstance) && timeout--) { /* 等待发送结束 */ } XIicPs_MasterRecv(&IicInstance, data, len, dev_addr); while (XIicPs_BusIsBusy(&IicInstance) && timeout--) { /* 等待接收结束 */ } if (timeout <= 0) { return -1; } return 0; }

使用示例:向 0x50 地址的 EEPROM,从寄存器地址 0x00 开始写入 4 个字节:

u8 data[4] = {0x11, 0x22, 0x33, 0x44}; iic_write_reg(0x50, 0x00, data, 4);

再读回:

u8 readback[4]; iic_read_reg(0x50, 0x00, readback, 4);

有两点我要强调。第一,XIicPs_MasterSend和XIicPs_MasterRecv的参数里,设备地址填 7 位地址,读写位由驱动内部处理,你不要自己把 0xA1 这种 8 位地址传进去。第二,一定要加超时判断。没有超时保护的话,总线一旦异常,程序会卡死在等待循环里,裸机环境只能复位,这一步能救你很多次。

EEPROM 写入还有一个特点:页写入存在页边界限制。AT24C32 这类芯片,一页通常是 32 字节,如果你跨页写入,数据会回卷到该页开头,导致写入内容错乱。所以连续写入大块数据时要按页切分。做基础实验时,写 4 字节一般不会触发这个问题,但不代表你可以无视它。

3.4 什么时候该上中断

轮询方式简单直接,但它的缺点是 CPU 会一直占着等待传输完成。如果系统里只有 IIC 任务,这没问题;如果同时还要跑网络协议栈、显示刷新或其他外设,浪费就大了。这时可以改用中断方式。

中断方式的完整流程是:初始化 GIC 中断控制器,把 XIicPs 的中断 ID 绑定到自己的中断服务函数;在XIicPs_SetStatusHandler里注册一个回调,传输完成或出错时由中断触发回调;在回调函数里根据状态寄存器判断是发送完成、接收完成还是错误,置标志通知主流程。BSP 自带的示例工程里就有中断版本可以参考。不要自己硬写寄存器,先用驱动接口把链路跑通,再去看底层细节。

高级一点还可以结合 FIFO 甚至 DMA,不过那属于优化话题。基础阶段把轮询调通,理解一次传输的完整生命周期,比贸然上中断更有价值。

4. Linux 下驱动:设备树、设备节点和调试命令

裸机跑通说明硬件链路正常。做完这一步别急着收工,再把它放到 Linux 环境里过一遍。ZYNQ 跑 Linux 时,IIC 控制器由内核 I2C 子系统管理,用户可以通过设备树描述硬件连接,然后用 i2c-tools 和 sysfs 快速验证。

4.1 设备树里怎么挂 IIC 从设备

在 ZYNQ Linux 设备树里,PS 端的 IIC 节点通常以i2c@e0004000和i2c@e0005000的形式存在,compatible 一般是cdns,i2c-r1p10。这个节点是芯片级设备树里提前定义好的,你一般不用从零写,只需要确认 status 是 okay,并且设置适当的时钟频率。

要把从设备挂上去,就在对应的 i2c 节点下增加子节点。例如挂一颗 AT24C32 EEPROM,设备地址 0x50:

&i2c0 { clock-frequency = <100000>; status = "okay"; eeprom@50 { compatible = "atmel,24c32"; reg = <0x50>; pagesize = <32>; }; };

compatible 里的atmel,24c32对应内核里的 at24 驱动,它几乎兼容大部分 IIC EEPROM。reg 填 7 位设备地址。pagesize 告诉驱动页大小,避免跨页写入问题。加好之后重新编译设备树并启动系统,如果驱动加载成功,你会看到/sys/bus/i2c/devices/0-0050/eeprom这个文件出现。

有一种情况是设备树里没有挂具体从设备,系统里也没有对应内核驱动,但你只想临时验证 IIC 总线上有什么设备。这时只要内核开启CONFIG_I2C_CHARDEV,即 i2c-dev 支持,就可以在用户态通过 i2c-tools 操作。

4.2 用 i2cdetect 找到设备

i2c-tools 是 Linux 下调试 IIC 的第一利器。先看系统里有哪些 IIC 总线:

i2cdetect -l

假设输出有 i2c-0 和 i2c-1,对应 ZYNQ 的两路控制器。然后扫描总线:

i2cdetect -y 0 i2cdetect -y 1

扫描结果是一个十六进制地址表。如果 0x50 位置出现了 0x50,说明该地址有设备响应。如果位置显示UU,说明该地址被某内核驱动占用,扫描程序不会去访问它。如果所有位置都是--,要么总线上没有设备,要么设备没上电,要么地址不在这条总线上。

注意:i2cdetect -y会向总线上每个地址发送探测报文,不是所有 IIC 设备都喜欢被探测。尤其是电源管理芯片、复杂的传感器,探测可能触发意外状态。所以调试重点设备时,我更喜欢直接i2cget指定地址读一个确定的寄存器,而不是无差别扫描。设备多的总线,扫描前先看原理图,心里有数再动手。

4.3 在 Linux 里读写 EEPROM

如果设备树里挂载了 at24 驱动,读写 EEPROM 可以直接操作 sysfs 节点。读取:

dd if=/sys/bus/i2c/devices/0-0050/eeprom of=/tmp/eeprom.bin bs=1 count=256

写入前先确认数据文件内容:把要写的数据做成小文件,再用 dd 写回。

dd if=/tmp/data.bin of=/sys/bus/i2c/devices/0-0050/eeprom bs=1 count=16

如果没有挂载任何内核驱动,也可以用 i2c-tools 直接操作。例如向地址 0x50 的 EEPROM,寄存器地址 0x00,写入 0xAB:

i2cset -y 0 0x50 0x00 0xAB

读回:

i2cget -y 0 0x50 0x00

i2cset 的格式是“总线号 设备地址 寄存器地址 数据值”,非常直观。调试传感器也可以用它读状态寄存器,比写测试程序快得多。

有一点要提醒:EEPROM 有写寿命,虽然实际用几千几万次问题不大,但调试时不要写个脚本让它无限循环写,真把重要区域写废了哭都来不及。对 Linux 下那种直接暴露的 eeprom 节点,更要谨慎操作,备份好原始数据。

5. 调试经验和踩坑实录

IIC 调试的难点往往不是协议本身,而是问题现象千奇百怪。下面这几类是我反复遇到的,每一条都可以对应到具体的排查动作。

5.1 总线被拉死,SDA/SCL 电压不正常的排查

最典型的故障是总线空闲时 SDA 或者 SCL 保持低电平。正常情况两条线都应该接近高电平,谁把它拉低了,谁就有问题。

排查思路是从外到内:先量电压,确认是否真的低;再看板上上拉电阻是否存在、是否正确接电源;然后断开可疑从设备的 SCL/SDA 引脚,看总线是否恢复;如果恢复,基本可以锁定是那个设备进入异常状态。我这里还见过一种情况:某个从设备上电时序不对,在 IIC 初始化之前就抢占了 SDA,导致整条总线锁死。

有些 IIC 设备在异常状态下可以通过连续翻转 SCL 9 个时钟周期来释放总线。调试时可以先尝试这个操作,不行就重新上下电。裸机程序里卡在 BusIsBusy 等待,多半就是总线被拉死,先查电平,再查代码,不要反着来。

5.2 有响应但数据不对的排查

设备能被扫描到,说明地址和硬件连接大体没问题,但读出的数据不对,问题常常出在寄存器访问流程上。

寄存器地址长度是最常见的坑。有些芯片手册写寄存器地址是 8 位,有些写 16 位。EEPROM 容量大一点之后,内部寻址通常要 2 字节地址,比如 24C64 及以上。如果你按照 24C02 的方式只发送 1 字节地址,读出来的数据就会错位甚至全是 0xFF。

发送顺序也要看手册。多字节寄存器是先发高字节还是低字节,不同器件规定不同。有的传感器要求先写控制字节,再写寄存器地址,没摸清就直接按惯性操作,大概率拿不到有效数据。这时候把手册里的时序图翻出来,一条一条比对我上面写的代码,问题通常就藏在某个字节顺序里。

5.3 NACK 和地址问题的排查

发送设备地址后如果收到 NACK,表示总线上没有设备对这个地址应答。先确认地址本身有没有写错:7 位地址还是 8 位地址,寄存器里填的是不是把读写位也当成了地址。0x50 和 0xA0 之间的换算我前面讲过,这是新手最容易搞混的地方。

再确认硬件连接:SCL 和 SDA 有没有接反,A2/A1/A0 地址引脚有没有按原理图设置,从设备供电有没有到位。这些信息在原理图前比在代码前放十倍精力都不亏。还有一种情况是设备确实在,但处于写保护状态,读操作正常,写操作没反应或者回读不一致,这时候要查 WP 引脚有没有被正确控制。

遇到写 EEPROM 成功但回读还是旧数据,先别急着怀疑 IIC 时序,很可能是写周期还没结束。EEPROM 每次写之后有一段内部写时间,例如几毫秒到十几毫秒,写完立刻读会读到旧内容,加个延时再看。

5.4 逻辑分析仪实测波形

排查 IIC 问题,我最推荐的终极工具是逻辑分析仪,便宜那种就够用。把 SDA 和 SCL 接到逻辑分析仪的通道上,地线共用,然后触发开始抓波形。

抓到的波形能看到 START 条件、设备地址、读写位、每个字节的 ACK/NACK、数据内容、STOP 条件。你不需要背协议,只要对照器件手册的时序图,一眼就能看出软件发送的地址和实际器件的地址是不是对得上,也能看出 NACK 出现在哪里。

我印象最深的一次调试,是一颗传感器读取不稳定。代码看起来没有任何问题,改地址、降速率都没用。最后用逻辑分析仪一看,控制器发出的地址字节和我预期完全一致,但中间某个寄存器的取值被协议分析器标成了不支持的值,原因是数据手册里该寄存器需要先写一个选择字节。顺着波形和手册逐字节核对,几十分钟就解决了问题。没有逻辑分析仪,就只能靠猜。

6. 最后,一点个人体会

6.1 引脚、地址、波形,三件事排在最前面

做了这么多 IIC 调试,我总结成一句话:先看引脚,再对地址,最后上波形。引脚指 MIO 分配和硬件连线,地址指 7 位/8 位换算和从设备实际接法,波形指用逻辑分析仪把协议层揭开。代码层面的问题往往不是问题,这三项才是 IIC 调试的大头。

如果你刚接触 ZYNQ,我建议把裸机例程完整手写一遍,不要复制粘贴。手写过程中自然要去查 XIicPs 的 API、看返回状态、理解总线忙标志,这些细节一旦过手,后面在 Linux 下遇到问题就不会发怵。

6.2 两个建议:裸机和 Linux 都跑一遍

如果板子能跑 Linux,哪怕你最终交付的是裸机程序,我也建议先把 Linux 上的 i2c-tools 跑一遍。因为i2cdetect能迅速告诉你总线上到底有哪些地址,这个信息对检查硬件连接非常有用。裸机下写一个扫描程序也不是不行,但 Linux 下现成工具更省事。

裸机调通之后的代码不要急着删。留一份简单的读写函数,后续接新传感器时,可以快速验证“能不能读、主地址对不对”。把它当作一套硬件自检工具,比每次从零写驱动效率高得多。调 IIC 说到底不是技术难题,而是耐心盘清楚每一个细节的过程。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询