1. 项目缘起:为什么需要关注AXI IIC IP?
在FPGA开发中,尤其是基于Xilinx(现AMD)平台的系统设计,我们常常需要与各种低速外设打交道,比如EEPROM、温度传感器、实时时钟芯片或者一些特定的显示模块。这些设备大多通过I2C(Inter-Integrated Circuit)总线进行通信。I2C协议虽然简单,只有两根线(SDA和SCL),但要在FPGA里用纯逻辑(Verilog/VHDL)从头实现一个稳定、可靠且功能完整的I2C控制器,并处理好与处理器(如MicroBlaze或ARM Cortex)的交互,其实是个挺费时费力的活儿。你得考虑时钟拉伸、总线仲裁、多主机支持、中断处理等一系列细节,稍有不慎就会在调试阶段陷入“玄学”困境。
这时候,Xilinx提供的AXI IIC IP核就成了一个非常优雅的解决方案。它本质上是一个将I2C控制器功能封装好,并通过标准的AXI4-Lite接口暴露给处理器的软核。对于开发者而言,这意味着你不需要再纠结于I2C协议的底层时序,而是可以像在单片机或Linux驱动里那样,通过读写内存映射寄存器的方式来轻松操控I2C总线。这个IP核大大简化了FPGA片上系统(SoC)中处理器与I2C外设的集成难度,提升了开发效率。我最近在一个需要频繁读写外部EEPROM和配置多个传感器模块的项目中深度使用了这个IP,过程中积累了一些官方文档里不会细说的实战经验和避坑要点,在这里和大家分享一下。
2. AXI IIC IP核功能与架构解析
在深入配置和使用之前,我们有必要先搞清楚这个IP核到底能做什么,以及它的内部是如何工作的。理解其架构,对于后续的调试和性能优化至关重要。
2.1 核心功能特性
Xilinx的AXI IIC IP核并非一个“简化版”的I2C控制器,它提供了相当丰富的功能,足以应对大多数复杂场景:
- 标准模式与快速模式支持 :兼容I2C标准模式(100 kbps)和快速模式(400 kbps)。这是最基础也是最重要的功能。
- 多主机总线仲裁 :IP核可以工作在主机发送器、主机接收器、从机发送器和从机接收器模式。当总线上有多个主机时,它能参与仲裁,确保总线访问的公平性。
- 时钟拉伸(Clock Stretching) :支持作为从机时的时钟拉伸功能,这对于与那些需要额外时间处理数据(例如执行内部写周期)的从设备通信是必须的。
- 10位地址寻址 :除了标准的7位设备地址,还支持10位地址模式,可以访问更多的从设备。
- 可编程时钟频率 :通过配置输入时钟和分频器,可以精确设定所需的SCL时钟频率。
- 中断驱动与轮询模式 :数据传输状态(如传输完成、仲裁丢失、从机被寻址等)可以通过中断信号通知处理器,也支持处理器轮询状态寄存器,为不同实时性要求的应用提供了灵活性。
- DMA支持(可选) :在高级配置中,可以启用AXI4-Stream接口,与Xilinx的DMA IP配合,实现大批量数据(如从EEPROM读取大量配置数据)的高效搬移,解放处理器。
2.2 内部架构与寄存器概览
从处理器(如MicroBlaze)的角度看,AXI IIC IP核就是一段映射到其地址空间的内存区域。这段空间里分布着多个控制与状态寄存器(CSR)。我们所有的操作,归根结底就是读写这些寄存器。几个最关键的寄存器包括:
- 控制寄存器(IIC_CR) :这是“大脑”。你可以在这里使能IP核(EN位)、选择主机/从机模式(MSMS位)、决定是发送还是接收(TX位)、发送重复起始条件(RSTA位)等。任何一次传输的发起,都始于对这个寄存器的正确配置。
- 数据寄存器(IIC_DTR)与接收寄存器(IIC_DRR) :这是“嘴巴”和“耳朵”。当IP核处于发送状态时,你需要把要发送的字节(可以是设备地址+读写位,也可以是数据字节)写入IIC_DTR。当IP核处于接收状态时,从总线上读取到的字节会出现在IIC_DRR中,供你读取。
-
状态寄存器(IIC_SR)
:这是“眼睛”。它实时反映了IP核和I2C总线的状态。最重要的几个状态位包括:
- Tx/Rx FIFO状态位 :指示发送/接收FIFO是空还是满。
- 传输完成位(TIC) :当一次字节传输(包括地址和数据)完成时,此位会被置1。这是轮询模式下判断操作是否完成的主要标志。
- 仲裁丢失位(ARBLOST) :如果IP核在尝试成为总线主机的过程中仲裁失败,此位会被置1。出现这个情况,通常需要软件重新初始化传输。
- 总线忙位(BB) :指示I2C总线当前是否被占用。在发起传输前检查此位是个好习惯。
- 全局中断使能寄存器(GIER)与IP中断使能/状态寄存器(IER/ISR) :如果你选择使用中断模式,就需要配置这些寄存器来使能特定事件(如传输完成)的中断,并在中断服务程序(ISR)中读取ISR来确认中断源并清除中断标志。
理解这些寄存器的功能,是编写底层驱动代码的基础。整个通信流程,就是按照I2C协议时序,通过软件有序地读写这些寄存器来驱动的。
3. 在Vivado中配置与集成AXI IIC IP核
理论清楚了,我们开始在Vivado设计套件中实际操作。这个过程虽然GUI化,但有几个配置选项直接影响IP核的行为和资源占用,需要仔细斟酌。
3.1 IP核参数配置详解
在Vivado的Block Design中,添加“AXI IIC” IP核后,双击打开配置界面。以下几个标签页的配置是关键:
-
“Board”标签页 :如果你的开发板上有预定义的I2C接口(比如PMOD接口或专用I2C引脚),Vivado可能会在这里自动识别并帮你选择正确的板级接口。如果没有,或者你想自定义引脚,可以忽略这一步,后续在约束文件中手动指定。
-
“IP Configuration”标签页 :这是核心配置区。
- IIC Speed Mode :根据你的外设支持情况,选择“Standard Mode (100kbps)”或“Fast Mode (400kbps)”。如果你的从设备只支持标准模式,却配置为快速模式,通信必定失败。
-
SDA/SCL High Time and Low Time
:高级选项。通常保持默认的“7”即可,它对应于标准或快速模式的标准占空比。只有当你有非常特殊的时序要求,或者需要非标准速率时,才需要手动计算并修改这些值。它们与输入时钟频率(
s_axi_aclk)共同决定了最终的SCL频率。计算公式大致为:SCL频率 = 输入时钟频率 / ( (High Time + Low Time + 6) * 2 )。保持默认时,IP核会自动根据你选择的Speed Mode和输入时钟频率计算出合适的分频系数。 - Enable IIC with DMA :如果你预计有大量连续数据的读写(例如从EEPROM加载一幅图片的配置数据),强烈建议勾选此选项。这会为IP核添加AXI4-Stream接口,后续可以连接AXI DMA IP,实现数据在DDR内存和IIC IP之间的直接搬移,极大提升吞吐量并降低CPU负载。对于简单的单字节读写,可以不勾选。
- Transmit / Receive FIFO Depth :FIFO的深度。默认16通常够用。如果你启用了DMA并用于大数据传输,可以考虑适当增大接收FIFO深度,以防止DMA来不及取走数据而导致溢出。增大FIFO深度会消耗更多的Block RAM资源。
-
“Addressing”标签页 :这里配置从机模式下的设备地址(如果IP核被用作从机)。在大多数作为主机的应用场景下,此配置无关紧要。
配置完成后,点击“OK”,Vivado会生成该IP核。你需要用AXI Interconnect(或SmartConnect)将它的
S_AXI
接口连接到处理器的AXI总线,并将它的
IIC
接口(即
scl_i/o
,
sda_i/o
)引出到顶层端口。
3.2 引脚约束与电平注意事项
将
IIC
端口引出到顶层后,下一步是在XDC约束文件中指定具体的FPGA引脚。
# 示例:将I2C信号约束到Bank 35的某个IO上
set_property PACKAGE_PIN AG13 [get_ports {iic_0_scl_io}]
set_property IOSTANDARD LVCMOS33 [get_ports {iic_0_scl_io}]
set_property PACKAGE_PIN AG14 [get_ports {iic_0_sda_io}]
set_property IOSTANDARD LVCMOS33 [get_ports {iic_0_sda_io}]
这里有一个
极其重要但容易被忽略的坑
:I2C总线是
开漏(Open-Drain)
输出。这意味着FPGA引脚不能配置为普通的推挽输出。幸运的是,AXI IIC IP核的
scl_io
和
sda_io
是
三态(Tristate)
双向端口。在Xilinx FPGA上,当你将这样的端口约束到普通IO引脚时,综合工具通常会自动推断出正确的开漏缓冲器。
但是,为了绝对可靠,特别是当你在代码中手动例化I/O缓冲器时,需要确保配置正确。更常见的实际问题出现在 物理层面 :你必须确保在SDA和SCL线上有 上拉电阻 !这个电阻通常不在FPGA内部,需要你在PCB上或实验板中额外添加,阻值一般在1kΩ到10kΩ之间(常见4.7kΩ),具体取决于总线电容和速度。没有上拉电阻,总线电平永远无法被拉高,通信根本无法进行。这是我调试时第一个要检查的硬件问题。
4. 软件驱动开发:从寄存器操作到应用函数
硬件设计生成比特流并下载到FPGA后,接下来的重心就转移到软件上。我们需要在SDK(Vitis)中编写代码来驱动这个IP核。Xilinx提供了底层驱动库(
xiic.h
,
xiic.c
),但理解其下的寄存器操作逻辑,能让你在出问题时更快地定位。
4.1 底层寄存器操作流程剖析
我们以最典型的“向从设备写入一个字节数据”为例,拆解其寄存器级别的操作流程。假设从设备地址是0x50(7位地址),我们要向其寄存器0x00写入数据0xAB。
- 初始化与使能 :首先,向控制寄存器(CR)写入特定值,使能IIC核心(设置EN位),并确保它处于空闲状态。
- 等待总线空闲 :轮询状态寄存器(SR),检查总线忙(BB)位是否为0。如果总线被其他设备占用,则需要等待。
-
生成起始条件并发送地址(写)
:
- 将控制寄存器的MSMS位(Master Start/Stop)置1,TX位(Transmit)置1。这告诉IP核:“请作为主机,准备发送数据,并产生起始条件”。
-
将
从设备地址左移一位,并将最低位置0(表示写操作)
,即
(0x50 << 1) | 0x00 = 0xA0,写入数据发送寄存器(DTR)。 - IP核会自动将DTR中的数据放到总线上,并控制时序。
- 等待地址发送完成 :轮询状态寄存器(SR)的传输完成中断标志位(TIC)。当TIC变为1,表示地址字节(含读写位)已发送完毕。 此时必须读取SR来清除TIC位 ,否则后续状态无法更新。
- 检查应答 :在地址发送后,IP核会检测从设备返回的ACK。如果从设备无应答,状态寄存器中的从机应答位会反映异常。驱动库通常会处理这个检查,如果无应答,会返回错误。
- 发送数据字节 :确认地址应答正常后,将数据字节0xAB写入数据发送寄存器(DTR)。
- 等待数据发送完成并检查应答 :再次轮询TIC位,等待其置1后清除。同样,IP核会检查从设备对这个数据字节的ACK。
- 生成停止条件 :所有数据发送完毕后,将控制寄存器(CR)的MSMS位清零。IP核检测到这个变化,会在总线上产生一个停止条件,结束本次传输。
这个过程看似繁琐,但Xilinx的驱动库
XIic_Send
函数已经为我们封装好了所有这些步骤。然而,当你遇到通信失败时,能够单步调试,查看每一步操作后相关寄存器的值,是定位问题的终极手段。
4.2 使用Xilinx驱动库(xil库)的实战代码
在实际项目中,我们当然不会每次都去操作寄存器。Xilinx的BSP(Board Support Package)会自动为AXI IIC IP生成一个实例,并初始化好驱动。我们的应用代码通常这样写:
#include “xiic.h”
#include “xparameters.h” // 包含IP核的基地址等硬件定义
// 假设在Vivado中IP实例名为 axi_iic_0
#define IIC_DEVICE_ID XPAR_AXI_IIC_0_DEVICE_ID
XIic IicInstance; // IIC驱动实例
int IicWriteData(u8 SlaveAddr, u8 *DataPtr, int ByteCount) {
int Status;
Status = XIic_Send(&IicInstance, SlaveAddr, DataPtr, ByteCount, XIIC_STOP);
if (Status != XST_SUCCESS) {
xil_printf(“IIC Write Failed: 0x%x\r\n”, Status);
// 可以在这里加入更详细的错误处理,比如检查XIic_GetLastError()
}
return Status;
}
int main() {
int Status;
XIic_Config *ConfigPtr;
u8 WriteBuffer[2] = {0x00, 0xAB}; // 寄存器地址 + 数据
// 1. 查找并初始化IIC驱动
ConfigPtr = XIic_LookupConfig(IIC_DEVICE_ID);
if (ConfigPtr == NULL) {
return XST_FAILURE;
}
Status = XIic_CfgInitialize(&IicInstance, ConfigPtr, ConfigPtr->BaseAddress);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
// 2. 启动IIC控制器(设置为主机)
Status = XIic_Start(&IicInstance);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
// 3. 设置总线速度(可选,通常在配置IP时已设定)
// XIic_SetBusSpeed(&IicInstance, 100000); // 100kHz
// 4. 执行写操作:向地址0x50的设备的0x00寄存器写入0xAB
Status = IicWriteData(0x50, WriteBuffer, 2);
if (Status != XST_SUCCESS) {
// 错误处理
}
// ... 其他操作
// 5. 停止IIC控制器
XIic_Stop(&IicInstance);
return 0;
}
这段代码清晰展示了使用驱动库的标准流程:查找配置 -> 初始化 -> 启动 -> 执行传输 -> 停止。
XIic_Send
函数的最后一个参数
XIIC_STOP
指示在传输结束后产生停止条件。如果是连续写入多个字节,中间不需要停止条件,可以使用
XIIC_REPEATED_START
参数。
4.3 中断模式与DMA模式配置要点
-
中断模式
:对于不希望CPU死循环轮询的应用,可以启用中断。需要在Vivado中确保IP核的中断输出
ip2intc_irpt连接到处理器的中断控制器(如AXI INTC)。在软件上,调用XIic_SetSendHandler()或XIic_SetRecvHandler()设置回调函数,并调用XIic_SetStatusHandler()设置状态回调。最后通过XIic_EnableIntr()使能中断。在中断服务程序或回调函数中处理传输完成事件,效率更高。 -
DMA模式
:这是处理大数据量传输的利器。配置更复杂一些:
- 在Vivado中勾选“Enable IIC with DMA”。
-
添加并配置一个AXI DMA IP核,将其
S_AXIS_S2MM(流到内存映射)连接到IIC IP的M_AXI_STREAM_RX,将其M_AXIS_MM2S(内存映射到流)连接到IIC IP的S_AXI_STREAM_TX。 -
将DMA的
S_AXI_LITE接口连接到处理器,用于控制;将M_AXI接口连接到DDR控制器,用于数据搬移。 - 软件上,你需要同时编写IIC驱动和DMA驱动。流程变为:配置DMA传输(源/目标地址、长度)-> 启动DMA -> 启动IIC传输(作为流数据的消费者/生产者)。DMA会自动在内存和IIC的FIFO之间搬运数据,搬运完成后通过中断通知CPU。这能实现接近总线理论带宽的持续传输。
5. 调试实战:常见问题与排查心法
即便按照上述步骤操作,I2C通信依然可能失败。下面分享几个我踩过的坑和对应的排查思路,这可能是比正常使用流程更有价值的部分。
5.1 通信完全无响应:从硬件到软件的逐级排查
当用逻辑分析仪或示波器抓取总线,发现SCL或SDA完全没有波形,或者处理器调用
XIic_Send
后一直卡住时,请按以下顺序排查:
-
硬件第一
:
- 上拉电阻 :确认SDA和SCL线上是否有上拉电阻?用万用表测量总线空闲时的电压,是否约为电源电压(如3.3V)?如果电压为0或很低,肯定是上拉问题。
- 引脚约束 :确认XDC文件中约束的引脚是否正确?是否与硬件原理图一致?生成比特流后,可以通过Vivado的“I/O Ports”窗口再次确认。
- 电源与地 :确保FPGA和外设共地,且电源稳定。
-
软件初始化
:
-
IP核未使能
:你的代码是否成功调用了
XIic_Start()?这个函数会写入控制寄存器使能IP核。可以在调试器中单步执行,并查看IP核基地址处的控制寄存器值。 -
时钟与复位
:确认提供给AXI IIC IP核的
s_axi_aclk和s_axi_aresetn信号是否正常。复位信号是否已解除(高电平)?时钟频率是否与IP配置时输入的“s_axi_aclk Frequency (MHz)”一致?一个常见的疏忽是Block Design中时钟生成IP(Clocking Wizard)的输出频率与AXI IIC IP配置中的输入时钟频率假设不符。
-
IP核未使能
:你的代码是否成功调用了
-
总线状态检查
:
- 在发起传输前,先读取状态寄存器(SR)的Bus Busy(BB)位。如果总线一直为忙,可能是某个从设备(或者另一个主机)拉低了总线且没有释放。尝试断电重启整个系统。在复杂系统中,软件异常可能导致IP核没有正确发送停止条件,使总线挂死。这时可能需要一个“总线恢复”序列(在SCL上手动产生9个时钟脉冲)来复位从设备,但这通常需要直接控制GPIO模拟实现,超出了AXI IIC IP的范畴。
5.2 有波形但数据错误:时序与从设备行为分析
如果总线有波形,但数据不对,或者从设备不应答(NACK),可以抓取波形进行分析:
-
地址或数据错误
:用逻辑分析仪解码I2C波形,首先看发送的7位地址是否正确。注意,
XIic_Send函数要求的SlaveAddr参数是 7位地址 ,而IP核底层发送时,会自动左移一位并加上读写位。如果你错误地传入了8位地址(即已经左移过的),会导致寻址失败。这是新手最容易犯的错误之一。 -
时钟速度不匹配
:检查SCL的频率。如果配置为100kHz,但实际测量远高于或低于此值,可能是输入时钟
s_axi_aclk频率设置错误,或者分频系数计算有误。确保IP配置中的输入时钟频率与实际情况一致。 -
从设备特定协议
:很多I2C设备有自己的协议。例如,写一个EEPROM时,通常需要先发送设备地址+写位,再发送内存地址(16位或8位),最后才是数据。你需要确保你发送的字节序列完全符合从设备的数据手册要求。
XIic_Send函数只是简单地将你给的缓冲区字节依次发出,它不关心缓冲区里是地址还是数据。 - 从设备忙 :例如,向EEPROM写入一个字节后,EEPROM内部需要几毫秒时间进行擦写操作,在此期间它不会响应I2C总线上的任何命令(即“忙”状态)。如果你在这段时间内立即发起下一次读写,会收到NACK。正确的做法是在写操作后,延时足够时间,或者发送一个“查询”命令(发送设备地址+读位),直到设备应答ACK为止。这需要你在应用层代码中实现。
5.3 使用Vivado ILA进行在线调试
对于集成在FPGA内部的信号,逻辑分析仪无能为力。这时,Vivado的集成逻辑分析仪(ILA)就是神器。你可以将AXI IIC IP核的以下信号添加到ILA核中进行观察:
-
sda_i,sda_o,sda_t:可以看到IP核内部对SDA线的输入、输出和三态控制信号。这能帮你判断IP核是否在正确驱动总线。 -
scl_i,scl_o,scl_t:同上,用于SCL线。 -
IP核的AXI接口信号,如
S_AXI_AWADDR,S_AXI_WDATA,S_AXI_ARADDR等,可以观察处理器发来的具体读写命令和数据,验证软件驱动是否正确。 -
IP核的中断输出信号
ip2intc_irpt。
通过触发条件设置(例如当控制寄存器的MSMS位变化时触发),你可以捕获一次完整的I2C传输过程,并与预期的寄存器操作序列对比,精准定位是软件指令错误,还是IP核内部状态机异常。
6. 性能优化与高级应用场景
在基本功能跑通之后,我们可以考虑如何让系统更高效、更可靠。
6.1 中断与DMA模式下的吞吐量优化
对于需要频繁或大量访问I2C设备的应用(如持续读取传感器数据流),轮询模式会大量占用CPU资源。
-
中断模式优化
:确保中断服务程序(ISR)尽可能短小精悍。通常只在ISR中设置标志位、拷贝必要数据,然后将耗时的处理(如数据解析、存储)放到主循环或低优先级任务中。避免在ISR中调用可能阻塞的函数(如
xil_printf)。 -
DMA模式优化
:这是提升吞吐量的终极方案。关键在于合理设置DMA的传输长度和中断阈值。对于I2C这类相对低速的总线,DMA的搬移速度远快于I2C发送速度,因此通常不会成为瓶颈。但需要注意:
- 内存对齐 :确保DMA传输的源和目标地址符合DMA引擎的对齐要求(通常是32位或64位对齐),否则会触发异常或降低性能。
-
缓存一致性
:如果CPU的Data Cache被启用,而DMA直接读写DDR内存,会存在缓存一致性问题。CPU写入发送缓冲区的数据可能还留在Cache里,未被写回DDR,导致DMA读到旧数据;或者DMA将接收数据写入DDR后,CPU读到的可能是Cache中的旧数据。解决方法是在DMA传输前后,使用
Xil_DCacheFlushRange()(刷新)和Xil_DCacheInvalidateRange()(无效化)函数来维护缓存一致性。这是使用DMA时一个非常隐蔽的坑。
6.2 多主机环境下的稳定性设计
当系统中存在多个AXI IIC IP核(或多个主机,如FPGA内的处理器和外部MCU)共享同一条I2C总线时,需要仔细设计。
- 软件仲裁 :最稳妥的方式是在软件层面实现一个互斥锁(mutex)机制。任何任务在访问I2C总线前,必须先获取锁。这可以避免多个主机同时发起传输导致的硬件仲裁。虽然AXI IIC IP支持硬件仲裁,但软件仲裁更易于管理和调试。
- 总线监控与恢复 :增加一个“看门狗”任务,定期检查总线状态。如果发现总线长时间处于“忙”状态(可能由于某个主机崩溃导致),该任务可以尝试初始化自己的AXI IIC IP核,并发送一个停止条件(如果可能),或者触发系统复位来恢复总线。这增加了系统的鲁棒性。
6.3 与自定义IP或逻辑的协同工作
在一些高级应用中,AXI IIC IP可能不是终点,而是一个桥梁。例如:
- 作为从设备 :你可以将AXI IIC IP配置为从机模式,让外部主机(如一个树莓派)通过I2C总线来读取FPGA内部的状态寄存器,或者写入配置参数。这时,你需要编写相应的从机中断处理程序,来响应主机的读写请求。
- 流数据接口扩展 :利用其AXI4-Stream接口,你可以将I2C接收到的数据实时流式传输给另一个自定义的IP核进行处理(如滤波、解码),然后再通过DMA存入内存或发送出去。这种设计将I2C数据采集和后处理完全硬件化,实现了极高的效率和确定性。
通过AXI IIC IP核,Xilinx将复杂的I2C协议处理标准化、模块化,让开发者能更专注于上层应用逻辑。从简单的传感器读到复杂的多主机DMA传输,它提供了一个坚实可靠的基础。掌握它,意味着你在FPGA系统集成中又打通了一个关键环节。希望这些从实战中总结出的配置细节、代码框架和排查思路,能帮助你在项目中更顺畅地驾驭这个强大的IP核。

989

被折叠的 条评论
为什么被折叠?



