ZYNQ学习笔记1-裸机-PS端中断配置、IO配置及PS/PL数据的AXI交互(2-1)
最近在学习ZYNQ芯片的使用,都是一些基础的内容,主要是为了防止忘记和放弃,在这里打卡记录一下学习过程,以后有时间来翻看一下,复习复习。有需要的朋友们也欢迎参考讨论。
例程简介
本次的例程是通过PL端的IO触发PS端的中断。涉及到中断的配置,IO的配置和PS/PL间数据的AXI交互内容。由于涉及到PL的内容,所以需要做一个BD文件。这里涉及到的IP是ZYNQ及AXI_GPIO。
PL端配置
ZYNQ IP核配置

ZYNQ外设配置只配置了串口,主要是为了打印文本。不是必须的,如果不需要打印文本的话可以不设置,但一般为了创建的demo程序方便都会勾选上。勾选哪一个要看自己的硬件,串口在硬件原理图上分配的是哪一个引脚这里就要勾选对应的位置。

DDR配置主要配的是颗粒型号,也就是Memory Part 这里选的是MT41J256M16 RE-125。DDR的真实型号可能不是这个,但并不重要,一般而言只要位宽没选错就问题不大。打开MT41J256M16 RE-125手册看下型号所代表的具体含义如下:

可以看出MT41J256M16 RE-125的具体含义,MT41J是系列名,这里没有详细解释,打开ZYNQ的DDR配置页可以发现还有MT41K系列,翻看手册可以发现MT41J是DDR3颗粒只能1.5V供电,MT41K是DDR3L,支持1.5V和1.35V供电。256M16是颗粒的组织架构形式,具体是指地址==深度是256M位宽是16位,相乘256Mx16bit也就是颗粒单颗存储容量2Gb,也就是这个是一个2Gb,0.5GB的DDR颗粒。RE是指的封装,96球,宽体的封装。125是指速度等级,不同的等级的速度参数不一样,详细还需要翻翻手册,不过其实不纠结也可以,一般问题不大,随便选一个中间的时序过不了再翻手册也不晚,一般选颗粒选个位宽深度就可以了。

由于要触发PL到PS的中断,所以中断配置页是需要勾选的,选择IRQ中断就可以。看简介可以看到IRQ中断是指来自于PL的中断,中断位置是共享中断端口。其他的是从PL到私有中断。
AXI GPIO配置

AXI GPIO的设置页面如上图所示。查阅ip核手册可以了解各选项作用如下:
All inputs:将该GPIO通道的位设置为仅输入模式。默认情况为不选中
All Outputs:相反,将该GPIO通道的位设置为仅输出模式。默认情况也是不选中
GPIO Width:用来定义扩展的GPIO宽度,范围是1-32,默认是32。这里我只用了一个GPIO设置的值是1
Default Output Value:用来设置这个GPIO通道所有使能位的默认输出值,默认的输出是0,也就是低电平
Default Tri State Value:用来配置每个通道的方向。也就是GPIO是输入还是输出,默认是全1(输入模式)。
Enable Dual Channel:用于启用第二个GPIO通道(GPIO2)。默认情况下,此参数未选中,将AXI GPIO配置为单通道模式。当此参数启用时,GPIO2的相关选项将被激活,同时也会配置通道2(GPIO2)的寄存器。
GPIO2选项卡的内容和1一样就不啰嗦了-略
Enable Interrupt:用于启用GPIO模块中的中断控制逻辑和中断寄存器。默认情况下,此选项未选中,即中断未使能。
后续的操作就是套路了如自动连线,创建顶层,输出结果等。
PS端代码
完整代码
c
#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xgpiops.h"
#include "xgpio.h"
#include "xscugic.h"
#include "sleep.h"
#define GPIO_DEVICE_ID XPAR_GPIO_0_DEVICE_ID
#define GPIO_AXI_ID XPAR_AXI_GPIO_0_DEVICE_ID
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define GPIO_INTERRUPT_ID XPAR_XGPIOPS_0_INTR
#define INTC_GPIO_INTERRUPT_ID XPAR_FABRIC_AXI_GPIO_0_IP2INTC_IRPT_INTR //AXI中断ID
#define AXI_GPIO_CHANNEL1 1
void IntrHandler(void *CallBackRef, u32 Bank, u32 Status);
XGpioPs Gpio_ps; /* The Instance of the GPIO Driver */
XGpio axi_Gpio; /* The Instance of the GPIO Driver */
//XScuGic Intc; /* The Instance of the Interrupt Controller Driver */
XScuGic_Config *IntcConfig;
XScuGic GicInstancePtr;
XGpio_Config *Int_XGpio_Config;
#define LED 0
u32 press;
int main()
{
XGpioPs_Config *ConfigPtr;
u32 value=0;
//PS IO初始化
//根据器件ID 查找器件的配置信息
ConfigPtr = XGpioPs_LookupConfig(GPIO_DEVICE_ID);
//对GPIO驱动初始化
XGpioPs_CfgInitialize(&Gpio_ps, ConfigPtr, ConfigPtr->BaseAddr);
//设置引脚方向 0:输入,1:输出
XGpioPs_SetDirectionPin(&Gpio_ps, LED, 1);
//设置输出使能 0:不使能,1:使能
XGpioPs_SetOutputEnablePin(&Gpio_ps, LED, 1);
//向LED写0
XGpioPs_WritePin(&Gpio_ps, LED, 0x0);
//初始化PL AXI GPIO IP
XGpio_Initialize(&axi_Gpio, GPIO_AXI_ID);
//获取中断控制器的配置信息
IntcConfig = XScuGic_LookupConfig( INTC_DEVICE_ID);
//根据配置信息初始化中断控制器
XScuGic_CfgInitialize(&GicInstancePtr, IntcConfig, IntcConfig->CpuBaseAddress);
//关联GPIO中断处理程序
XScuGic_Connect(&GicInstancePtr, INTC_GPIO_INTERRUPT_ID,
(Xil_ExceptionHandler)IntrHandler,
(void *) &axi_Gpio);
//使能AXI GPIO 通道1中断
XGpio_InterruptEnable(&axi_Gpio, AXI_GPIO_CHANNEL1);
//使能AXI GPIO全局中断
XGpio_InterruptGlobalEnable(&axi_Gpio);
//使能GIC的 AXI GPIO中断
XScuGic_Enable(&GicInstancePtr, INTC_GPIO_INTERRUPT_ID);
//设置中断触发条件及优先级
XScuGic_SetPriorityTriggerType(&GicInstancePtr, INTC_GPIO_INTERRUPT_ID, 0xA0, 0x3);
//异常初始化
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
&GicInstancePtr);
//使能异常处理
Xil_ExceptionEnableMask(XIL_EXCEPTION_IRQ);
for(;;){
if(press){
usleep(2000);
if(!XGpio_DiscreteRead(&axi_Gpio, AXI_GPIO_CHANNEL1))
{
print(" hello ");
press = 0;
}
//使能AXI GPIO 通道1中断
XGpio_InterruptEnable(&axi_Gpio, AXI_GPIO_CHANNEL1);
}
}
}
void IntrHandler(void *CallBackRef, u32 Bank, u32 Status)
{
XGpio *axi_Gpio = (XGpio *)CallBackRef;
press = 1;
//清除中断标志 保证下次中断可以重新进入
XGpio_InterruptClear(axi_Gpio, AXI_GPIO_CHANNEL1);
//关闭AXI GPIO中断
XGpio_InterruptDisable(axi_Gpio, AXI_GPIO_CHANNEL1);
}
分段解释
这是一个简单的中断演示程序,因为这些功能都比较基础没必要搞得花里胡哨,所以只是把中断功能实现就算了,具体功能收到中断之后会打印个hello。整体比较粗糙。其实本来想做一个PL的按键按一下,PS的灯就亮一下的,但做完发现还得加去抖,加判断,就拉倒把,放弃。主打一个能跑就行·。所以程序里有一个PS端的IO,有一个PL端的IO,又互不相关。
- 宏定义
GPIO_DEVICE_ID:PS端GPIO的设备ID,在xparameters.h中定义
GPIO_AXI_ID:PL端AXI GPIO的设备ID,在xparameters.h中定义
INTC_DEVICE_ID:中断控制器(SCU GIC)的设备ID,在xparameters.h中定义
INTC_GPIO_INTERRUPT_ID:AXI GPIO连接到GIC的中断ID,,在xparameters.h中定义
AXI_GPIO_CHANNEL1:AXI GPIO的通道1,用于连接按键,在xparameters.h中定义
LED:PS端LED对应的引脚号(此处为0,实际需根据硬件调整)。
-
主函数
(1)初始化PS端GPIO(控制LED)
XGpioPs_LookupConfig 根据设备ID查找PS GPIO的配置结构体,包含基地址等。
XGpioPs_CfgInitialize 用查到的配置信息初始化PS GPIO驱动实例Gpio_ps,完成硬件映射
XGpioPs_SetDirectionPin
XGpioPs_SetOutputEnablePin
XGpioPs_WritePin 这三行将LED引脚配置为输出并初始化状态,不过后面就没有再改这个状态
(2)初始化PL端AXI GPIO(读取按键)
XGpio_Initialize 根据设备ID初始化AXI GPIO实例axi_Gpio,该IP核在PL中实现,用于读取外部按键输入
XScuGic_LookupConfig:获取GIC的配置信息。
XScuGic_CfgInitialize:初始化GIC驱动实例GicInstancePtr,建立与硬件的连接。
(3)初始化中断控制器(GIC)
XScuGic_LookupConfig:获取GIC的配置信息。
XScuGic_CfgInitialize:初始化GIC驱动实例GicInstancePtr,建立与硬件的连接
(4)连接中断处理函数
XScuGic_Connect:这个程序中将AXI GPIO的中断ID与自定义的中断处理函数IntrHandler绑定。当该中断发生时,GIC会调用IntrHandler,并将axi_Gpio实例作为回调参数传递。
(5)配置AXI GPIO内部中断
XGpio_InterruptEnable
XGpio_InterruptGlobalEnable:这两步是AXI GPIO自身的中断使能。必须先使能具体通道的中断,再使能全局中断,才能向外发出中断请求。
(6)配置GIC中断参数并使能
XScuGic_SetPriorityTriggerType:设置该中断的优先级(0xA0)和触发类型(0x3,通常为高电平敏感)。需与硬件设计匹配。
XScuGic_Enable:在GIC中使能该中断,允许其传递给CPU。
(7)注册异常处理函数并全局使能中断
Xil_ExceptionRegisterHandler:将GIC的中断处理入口XScuGic_InterruptHandler注册为CPU IRQ异常的处理函数。这样,当CPU收到IRQ异常时,会跳转到该函数,再由它分发到具体的中断处理程序(即我们之前绑定的IntrHandler)。
Xil_ExceptionEnableMask:使能CPU的IRQ异常,允许中断被响应。
至此,所有硬件初始化完成,中断系统准备就绪。
(8)主循环
主循环持续检查press标志。该标志由中断处理函数设置。
一旦press为真,先延时2ms进行硬件去抖(防止机械抖动导致误判)。
然后读取AXI GPIO通道1的输入值。假设按键按下时输入为0,则执行打印操作,并清除press标志。
最后重新使能AXI GPIO通道1的中断(因为在中断处理函数中被临时禁用了),为下一次按键做准备
-
中断处理函数
XGpio_InterruptClear:清除AXI GPIO通道1的中断挂起状态,保证下次发生中断后可以重复响应
XGpio_InterruptDisable:临时禁用该通道的中断。这是为了避免在去抖和读取期间再次触发中断,造成标志被反复设置或逻辑混乱。主循环中处理完成后会重新使能。
各函数功能分析
1. XGpioPs_LookupConfig函数
- 函数功能
XGpioPs_LookupConfig 是 Xilinx SDK 中 PS GPIO 驱动(XGpioPs)提供的一个查找函数,其核心作用是根据传入的设备ID,在系统自动生成的配置表 XGpioPs_ConfigTable 中检索匹配的配置项,并返回指向该配置结构体的指针。该函数通常用于驱动初始化之前,帮助上层代码获取设备的硬件信息(如寄存器基地址、中断号等),以便后续调用 XGpioPs_CfgInitialize 完成驱动实例的初始化。 - 函数主体
c
/*****************************************************************************/
/**
*
* 该函数根据唯一的设备 ID 查找对应的设备配置信息。
* 系统中的每个设备配置信息都保存在 XGpioPs_ConfigTable[] 表中。
*
* @param DeviceId 是待查找设备的唯一设备 ID。
*
* @return 指向与给定设备 ID 匹配的配置表项的指针;若未找到匹配项,则返回 NULL。
*
* @note 无。
*
******************************************************************************/
XGpioPs_Config *XGpioPs_LookupConfig(u16 DeviceId) // 函数定义:接收16位设备ID,返回配置结构体指针
{
XGpioPs_Config *CfgPtr = NULL; // 初始化指针为NULL,用于保存找到的配置项地址
u32 Index; // 定义循环索引变量
for (Index = 0U; Index < (u32)XPAR_XGPIOPS_NUM_INSTANCES; Index++) {
// 遍历配置表,XPAR_XGPIOPS_NUM_INSTANCES 是系统生成的宏,表示PS GPIO实例总数
if (XGpioPs_ConfigTable[Index].DeviceId == DeviceId) {
// 如果当前表项的DeviceId与传入的DeviceId匹配
CfgPtr = &XGpioPs_ConfigTable[Index]; // 将该表项的地址赋给CfgPtr
break; // 找到后立即退出循环
}
}
return (XGpioPs_Config *)CfgPtr; // 返回配置指针(若未找到则为NULL)
}
- 编程逻辑
首先,函数声明了一个指向配置结构体的指针 CfgPtr 并初始化为 NULL,这一步确保了在任何情况下(尤其是查找失败时)函数都有明确的返回值,避免了野指针风险。接着,函数通过一个 for 循环遍历全局配置表 XGpioPs_ConfigTable[],循环上限使用系统生成的宏 XPAR_XGPIOPS_NUM_INSTANCES,使得代码能自动适配不同的硬件设计,无需硬编码实例数量。在循环体内,逐一比对每个表项的 DeviceId 与传入的参数,若匹配则将该表项的地址存入 CfgPtr,并立即 break 跳出循环------因为设备 ID 具有唯一性,找到后无需继续查找。最终,函数返回 CfgPtr(可能是有效的配置指针或 NULL),调用者可根据返回值判断查找是否成功。 - 辅助说明
打开 XGpioPs_ConfigTable[]数据如下:
c
XGpioPs_Config XGpioPs_ConfigTable[XPAR_XGPIOPS_NUM_INSTANCES] =
{
{
XPAR_PS7_GPIO_0_DEVICE_ID,//这里的值是0
XPAR_PS7_GPIO_0_BASEADDR //这里的值是0xE000A000
}
};
可以看到指针返回值主要是ID和其相关的GPIO基地址。也就是说通过XGpioPs_LookupConfig函数系统获得了XGpioPs_Config 变量,其中的信息是GPIO DEVICE0的基址是0xE000A000。
打开UG585可以看到GPIO的基地址正是0xE000A000;

2. XGpioPs_CfgInitialize函数
XGpioPs_CfgInitialize 负责初始化一个 PS GPIO 驱动实例,其核心任务将ConfigPtr中包含的信息合并到InstancePtr变量(包括ID和基址信息),并根据运行平台(Zynq‑7000、Zynq UltraScale+ MPSoC 或 Versal)设置相应的引脚数与 Bank 数。
c
/*****************************************************************************/
/*
3. 4. 该函数初始化一个 XGpioPs 实例/驱动。
5. XGpioPs 实例结构体的所有成员均被初始化,
6. 并向 Bank 状态处理函数分配存根处理函数(StubHandlers)。
7. 8. @param InstancePtr 是指向 XGpioPs 实例的指针。
9. @param ConfigPtr 指向 XGpioPs 设备配置结构体。
10. @param EffectiveAddr 是虚拟内存地址空间中的设备基地址。
11. 如果不使用地址转换,则应传递物理地址。
12. 如果在调用此函数后更改地址映射,可能会发生意外错误。
13. 14. @return 始终返回 XST_SUCCESS。
15. 16. @note 无。
17. ******************************************************************************/
s32 XGpioPs_CfgInitialize(XGpioPs *InstancePtr, const XGpioPs_Config *ConfigPtr,
u32 EffectiveAddr)
{
s32 Status = XST_SUCCESS; // 定义状态变量,初始化为成功
u8 i; // 循环计数器
Xil_AssertNonvoid(InstancePtr != NULL); // 断言实例指针非空(非void版,不返回值)
Xil_AssertNonvoid(ConfigPtr != NULL); // 断言配置指针非空
Xil_AssertNonvoid(EffectiveAddr != (u32)0); // 断言有效地址非0
/*
* 为实例数据设置一些默认值,在所有成员成功初始化之前,
* 不将设备标记为就绪可用。
*/
InstancePtr->IsReady = 0U; // 初始化为未就绪(0)
InstancePtr->GpioConfig.BaseAddr = EffectiveAddr; // 设置基地址
InstancePtr->GpioConfig.DeviceId = ConfigPtr->DeviceId; // 设置设备ID
InstancePtr->Handler = (XGpioPs_Handler)StubHandler; // 设置中断处理函数为存根函数,具体见辅助说明
InstancePtr->Platform = XGetPlatform_Info(); // 获取当前平台信息(Zynq/ZynqMP/Versal等),这里返回的值是:XPLAT_ZYNQ,具体见辅助说明;
/* 根据平台初始化 Bank 数据 */
if (InstancePtr->Platform == (u32)XPLAT_ZYNQ_ULTRA_MP) { // 如果是 Zynq UltraScale+ MPSoC 平台
/*
* ZynqMP GPIO 设备的最大引脚数
* 0 - 25, Bank 0
* 26 - 51, Bank 1
* 52 - 77, Bank 2
* 78 - 109, Bank 3
* 110 - 141, Bank 4
* 142 - 173, Bank 5
*/
InstancePtr->MaxPinNum = (u32)174; // 最大引脚数174
InstancePtr->MaxBanks = (u8)6; // 最大bank数6
}
else if (InstancePtr->Platform == (u32)XPLAT_VERSAL) { // 如果是 Versal 平台
if(InstancePtr->PmcGpio == (u32)FALSE) // 如果不是 PMC GPIO(即 PS GPIO)
{
/* PS_GPIO 设备的最大引脚数
* 0 -25, Bank 0
* 26-57, Bank 3
*/
InstancePtr->MaxPinNum = (u32)58; // 最大引脚数58
InstancePtr->MaxBanks = (u8)4; // 最大bank数4
}
else // 如果是 PMC GPIO
{
/* PMC_GPIO 设备的最大引脚数
* 0 - 25,Bank 0
* 26 - 51,Bank 1
* 52 - 83,Bank 3
* 84 - 115, Bank 4
*/
InstancePtr->MaxPinNum = (u32)116; // 最大引脚数116
InstancePtr->MaxBanks = (u8)5; // 最大bank数5
}
}
else { // 其他平台其实就是ZYNQ;
/*
* GPIO 设备的最大引脚数
* 0 - 31, Bank 0
* 32 - 53, Bank 1
* 54 - 85, Bank 2
* 86 - 117, Bank 3
*/
InstancePtr->MaxPinNum = (u32)118; // 最大引脚数118
InstancePtr->MaxBanks = (u8)4; // 最大bank数4
}
/*
* 默认情况下,GPIO 中断未在硬件中被屏蔽。
* 为所有 4 个 bank 中的所有引脚禁用中断。
*/
for (i=(u8)0U; i < InstancePtr->MaxBanks; i++) { // 遍历每个 bank
if (InstancePtr->Platform == XPLAT_VERSAL){ // 如果是 Versal 平台
if(InstancePtr->PmcGpio == (u32)FALSE) // 非 PMC GPIO(PS GPIO)
{
if((i== (u8)XGPIOPS_ONE)||(i== (u8)XGPIOPS_TWO))
{
continue; // 跳过 bank 1 和 bank 2(这些 bank 在 PS GPIO 中不存在)
}
// 向中断禁用寄存器写入 0xFFFFFFFF,禁用该 bank 所有引脚的中断
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(i) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_INTDIS_OFFSET, 0xFFFFFFFFU);
}
else // 如果是 PMC GPIO
{
if(i==(u32)XGPIOPS_TWO)
{
continue; // 跳过 bank 2(PMC GPIO 中没有 bank 2)
}
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(i) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_INTDIS_OFFSET, 0xFFFFFFFFU);
}
}
else // 非 Versal 平台(Zynq/ZynqMP)
{
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(i) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_INTDIS_OFFSET, 0xFFFFFFFFU); // 禁用该 bank 所有引脚中断,具体见辅助说明,
}
}
/* 指示组件现在已就绪可用 */
InstancePtr->IsReady = XIL_COMPONENT_IS_READY; // 设置为就绪标志
return Status; // 返回状态(始终为 XST_SUCCESS)
}
- 辅助说明
1> 打开StubHandler函数可以看到,这是一个空函数,就是用来占位的。
c
void StubHandler(const void *CallBackRef, u32 Bank, u32 Status)
{
(void) CallBackRef;
(void) Bank;
(void) Status;
Xil_AssertVoidAlways();
}
2>XGetPlatform_Info函数用来反映当前的平台是哪一个。返回值由宏定义确定,宏定义由bsp生成。这里的返回值是XPLAT_ZYNQ
c
u32 XGetPlatform_Info()
{
#if defined (versal)
return XPLAT_VERSAL;
#elif defined (ARMR5) || (__aarch64__) || (ARMA53_32) || (PSU_PMU)
return XPLAT_ZYNQ_ULTRA_MP;
#elif (__microblaze__)
return XPLAT_MICROBLAZE;
#else
return XPLAT_ZYNQ;
#endif
}
3>宏函数XGpioPs_WriteReg,作用是将data写到baseaddr+RegOffset的地址里。
c
#define XGpioPs_WriteReg(BaseAddr, RegOffset, Data) \
Xil_Out32((BaseAddr) + (u32)(RegOffset), (u32)(Data))
在这里BaseAddr是0xE000A000(XGpioPs_LookupConfig查到的值),offset值是0x00000214U (XGPIOPS_INTDIS_OFFSET)。相加就是0xE000A214。查看UG585,可知定义如下

可以看出来寄存器是GPIO的中断禁止寄存器。设置1关闭GPIO的中断功能。函数关闭了所有GPIO的中断功能。
- XGpioPs_SetDirectionPin
- 函数功能
XGpioPs_SetDirectionPin 用于设置 GPIO 中单个引脚的方向(输入或输出)。它根据传入的引脚号和方向值,计算引脚所属的 bank 及在 bank 内的位号,然后修改对应 bank 的方向模式寄存器(DIRM)中的相应位
c
/****************************************************************************/
/**
*
* 设置指定引脚的方向。
*
* @param InstancePtr 是指向 XGpioPs 实例的指针。
* @param Pin 是要写入数据的引脚编号。
* 在 Zynq 中有效值为 0-117,在 Zynq Ultrascale+ MP 中为 0-173。
* @param Direction 是要为指定引脚设置的方向。
* 有效值为 0(输入方向)或 1(输出方向)。
*
* @return 无。
*
*****************************************************************************/
void XGpioPs_SetDirectionPin(const XGpioPs *InstancePtr, u32 Pin, u32 Direction)
{
u8 Bank; // 用于存储引脚所在的 bank 编号(0~最大bank数-1)
u8 PinNumber; // 用于存储引脚在 bank 内的位号(0~31)
u32 DirModeReg; // 用于保存读出的方向模式寄存器值
Xil_AssertVoid(InstancePtr != NULL); // 断言:实例指针非空
Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY); // 断言:实例已初始化就绪
Xil_AssertVoid(Pin < InstancePtr->MaxPinNum); // 断言:引脚号在有效范围内
Xil_AssertVoid(Direction <= (u32)1); // 断言:方向值合法(0或1)
/* 获取引脚所属的 bank 编号以及在 bank 内的位号。 */
#ifdef versal
// 若是 Versal 平台,调用支持 PMC/PS 区分的版本(需要 InstancePtr 参数)
XGpioPs_GetBankPin(InstancePtr, (u8)Pin, &Bank, &PinNumber);
#else
// 其他平台(Zynq、ZynqMP)使用简单版本
XGpioPs_GetBankPin((u8)Pin, &Bank, &PinNumber);
#endif
// 读取对应 bank 的方向模式寄存器(DIRM)当前值
DirModeReg = XGpioPs_ReadReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET);
if (Direction != (u32)0) { // 若方向值非0(即为1),表示设置为输出方向
// 将对应位设置为 1(输出)
DirModeReg |= ((u32)1 << (u32)PinNumber);
} else { // 否则为输入方向
// 将对应位清零(输入)
DirModeReg &= ~((u32)1 << (u32)PinNumber);
}
// 将修改后的值写回方向模式寄存器
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET, DirModeReg);
}
- 编程逻辑
XGpioPs_SetDirectionPin 首先通过断言检查实例指针、实例就绪状态、引脚号是否越界以及方向值是否合法,确保参数有效。接着根据平台调用 XGpioPs_GetBankPin(Versal 平台需传入实例指针以区分 PMC/PS GPIO,其他平台仅传引脚号)将全局引脚号转换为所属的 bank 编号和 bank 内的位号。然后读取对应 bank 的方向模式寄存器(DIRM)当前值,根据传入的方向值修改目标位:若方向为输出(非 0)则将该位置 1,若为输入(0)则将该位清零。最后将更新后的寄存器值写回硬件,完成引脚方向配置。该函数通过读-改-写的方式保证不影响同一 bank 内其他引脚的方向设置。 - 辅助说明
1> XGpioPs_GetBankPin函数
c
/****************************************************************************/
/*
* 获取给定 GPIO 引脚号对应的 Bank 编号以及在 Bank 内的引脚号。
*
* @param PinNumber 是 GPIO 设备中的引脚编号。
* @param BankNumber 返回该 GPIO 引脚所在的 Bank 编号。
* 有效值为 0 到 XGPIOPS_MAX_BANKS - 1。
* @param PinNumberInBank 返回引脚在 Bank 内的编号。
*
* @return 无。
*
* @note 无。
*
*****************************************************************************/
#ifdef versal
// 如果是 Versal 平台,函数原型包含 InstancePtr 参数(用于区分 PMC/PS GPIO)
void XGpioPs_GetBankPin(const XGpioPs *InstancePtr, u8 PinNumber, u8 *BankNumber, u8 *PinNumberInBank)
#else
// 其他平台(Zynq、ZynqMP)函数原型不需要 InstancePtr
void XGpioPs_GetBankPin(u8 PinNumber, u8 *BankNumber, u8 *PinNumberInBank)
#endif
{
u32 XGpioPsPinTable[6] = {0}; // 定义数组用于存储每个 bank 的最大引脚号,最多支持 6 个 bank
#ifdef versal
u8 i=(u8)0; // 仅 Versal 平台需要,用作循环计数器
#endif
u32 Platform = XGetPlatform_Info(); // 获取当前运行平台信息(Zynq/ZynqMP/Versal),前面介绍过
if (Platform == (u32)XPLAT_ZYNQ_ULTRA_MP) { // 如果平台是 Zynq UltraScale+ MPSoC
/*
* 该结构定义了在单个引脚操作时,引脚号到 bank 的映射关系
*/
// 填充每个 bank 的最大引脚号(累计最大值)
XGpioPsPinTable[0] = (u32)25; /* 0 - 25, Bank 0 */
XGpioPsPinTable[1] = (u32)51; /* 26 - 51, Bank 1 */
XGpioPsPinTable[2] = (u32)77; /* 52 - 77, Bank 2 */
XGpioPsPinTable[3] = (u32)109; /* 78 - 109, Bank 3 */
XGpioPsPinTable[4] = (u32)141; /* 110 - 141, Bank 4 */
XGpioPsPinTable[5] = (u32)173; /* 142 - 173 Bank 5 */
*BankNumber = 0U; // 从 bank 0 开始查找
while (*BankNumber < XGPIOPS_SIX) { // 遍历最多 6 个 bank
if (PinNumber <= XGpioPsPinTable[*BankNumber]) { // 如果引脚号小于等于当前 bank 的最大值
break; // 则命中当前 bank,退出循环
}
(*BankNumber)++; // 否则检查下一个 bank
}
}
#ifdef versal
else if(Platform == XPLAT_VERSAL) // 如果平台是 Versal
{
if(InstancePtr->PmcGpio == (u32)(FALSE)) // 如果是 PS GPIO(非 PMC)
{
// PS GPIO 的引脚分布:bank 0 和 bank 3
XGpioPsPinTable[0] = (u32)25; /* 0 - 25, Bank 0 */
XGpioPsPinTable[1] = (u32)57; /* 26 - 57, Bank 3 */
*BankNumber =0U; // 先假设为 bank 0
if(PinNumber <= XGpioPsPinTable[*BankNumber]) // 如果引脚在 0-25 范围内
{
*BankNumber = (u8)XGPIOPS_ZERO; // 则 bank = 0
}
else // 否则引脚在 26-57 范围内
{
*BankNumber = (u8)XGPIOPS_THREE; // 则 bank = 3
}
}
else // 如果是 PMC GPIO
{
// PMC GPIO 的引脚分布:bank 0,1,3,4
XGpioPsPinTable[0] = (u32)25; /* 0 - 25, Bank 0 */
XGpioPsPinTable[1] = (u32)51; /* 26 - 51, Bank 1 */
XGpioPsPinTable[2] = (u32)83; /* 52 - 83, Bank 3 */
XGpioPsPinTable[3] = (u32)115; /*84 - 115, Bank 4 */
*BankNumber =0U; // 从 bank 0 开始查找
while(i < XGPIOPS_FOUR) // 遍历 4 个表项(实际对应 4 个 bank)
{
if(i <= (u8)XGPIOPS_ONE) // 对于 i=0 和 i=1,即 bank 0 和 bank 1
{
if (PinNumber <= XGpioPsPinTable[i]) // 如果引脚号小于等于当前表项的最大值
{
*BankNumber = (u8)i; // 则 bank = i
break; // 退出循环
}
i++; // 否则继续检查下一个表项
}
else // 对于 i=2 和 i=3,实际对应 bank 3 和 bank 4(中间跳过了 bank 2)
{
if (PinNumber <= XGpioPsPinTable[i]) // 如果引脚号小于等于当前表项的最大值
{
*BankNumber = (u8)i+1U; // 则 bank = i+1(i=2 -> bank=3, i=3 -> bank=4)
break;
}
i++;
}
}
}
}
#endif
//下面是这个函数的核心代码 定位引脚在哪个bank中
else { // 默认平台:Zynq-7000
// Zynq-7000 的引脚分布:4 个 bank,0-31, 32-53, 54-85, 86-117
XGpioPsPinTable[0] = (u32)31; /* 0 - 31, Bank 0 */
XGpioPsPinTable[1] = (u32)53; /* 32 - 53, Bank 1 */
XGpioPsPinTable[2] = (u32)85; /* 54 - 85, Bank 2 */
XGpioPsPinTable[3] = (u32)117; /* 86 - 117 Bank 3 */
*BankNumber = 0U; // 从 bank 0 开始
while (*BankNumber < XGPIOPS_FOUR) { // 遍历 4 个 bank
if (PinNumber <= XGpioPsPinTable[*BankNumber]) { // 找到第一个最大引脚号 >= 引脚号 的 bank
break;
}
(*BankNumber)++;
}
}
// 以下是计算 PinNumberInBank(引脚在 bank 内的偏移,也就是当前引脚在bank中的位置)
if (*BankNumber == (u8)0) { // 如果 bank 为 0,则引脚号就是偏移
*PinNumberInBank = PinNumber;
}
#ifdef versal
else if(Platform == XPLAT_VERSAL) // Versal 平台需要特殊处理偏移计算
{
if(InstancePtr->PmcGpio == (u32)(FALSE)) // PS GPIO 情况
{
// 偏移 = 引脚号 - (bank 0 的最大值 + 1)
*PinNumberInBank = (u8)((u32)PinNumber - (XGpioPsPinTable[0] + (u32)1));
}
else // PMC GPIO 情况
{
if((*BankNumber ==(u8)XGPIOPS_THREE) || (*BankNumber ==(u8)XGPIOPS_FOUR))
{
// 对于 bank 3 或 bank 4,偏移 = 引脚号 % (前一表项最大值 + 1)
// 注意这里使用了 XGpioPsPinTable[*BankNumber - 2] 对应前一个 bank 的累计最大值
*PinNumberInBank = (u8)((u32)PinNumber %
(XGpioPsPinTable[*BankNumber - (u8)XGPIOPS_TWO] + (u32)1));
}
else // 对于 bank 0 或 bank 1
{
// 偏移 = 引脚号 % (前一表项最大值 + 1)
*PinNumberInBank = (u8)((u32)PinNumber %
(XGpioPsPinTable[*BankNumber - (u8)1] + (u32)1));
}
}
}
#endif
else { // 非 Versal 平台(Zynq/ZynqMP)的偏移计算
// 偏移 = 引脚号 % (前一个 bank 的最大值 + 1)
*PinNumberInBank = (u8)((u32)PinNumber %
(XGpioPsPinTable[*BankNumber - (u8)1] + (u32)1));
}
}
- 编程逻辑
XGpioPs_GetBankPin·是一个根据平台和引脚号计算所属 bank 及 bank 内偏移的辅助函数,其实现根据平台(Zynq‑7000、Zynq UltraScale+ MPSoC、Versal)和 GPIO 类型(PS GPIO 或 PMC GPIO)采用不同的引脚分布表。函数首先获取当前平台信息,对于 ZynqMP 平台使用 6 个 bank(引脚 0‑173)的映射表,通过循环比较确定引脚所在 bank;对于 Versal 平台则进一步区分 PMC GPIO 与 PS GPIO,分别使用不同的 bank 分布(PS GPIO 为 bank 0 与 bank 3,PMC GPIO 为 bank 0、1、3、4),并采用条件分支快速定位 bank;对于默认的 Zynq‑7000 平台则使用 4 个 bank(引脚 0‑117)的映射表。确定 bank 后,根据平台与 bank 编号计算引脚在 bank 内的位号:对于非 Versal 平台或 Versal 下非 PMC 的特殊情况,通常用引脚号减去前一 bank 的累计最大值再加 1 的方式获得;而对于 Versal 平台中的 PMC GPIO 情况,则采用取模运算得到引脚在 bank 内的偏移。整个函数通过条件编译(versal宏)和运行时的平台判断,实现了对多种硬件架构的统一适配,为上层引脚级操作提供了准确的 bank 与位号解析。

根据UG585介绍可以看出来,除了54个MIO还有两个EMIObank,分别是bank2,bank3,和程序一一对应。
2> XGpioPs_ReadReg(InstancePtr->GpioConfig.BaseAddr, ((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET);
XGpioPs_ReadReg函数的功能不再赘述,直接定位两个宏定义 XGPIOPS_REG_MASK_OFFSET(地址0x00000040)和XGPIOPS_DIRM_OFFSET(地址0x00000204)相加是0x00000244,正好是DIRM_1寄存器(方向寄存器,详见UG585)。之所以用XGPIOPS_REG_MASK_OFFSET和XGPIOPS_DIRM_OFFSET拼是因为XGPIOPS_REG_MASK_OFFSET之前(即偏移地址小于0x00000040的地址)均为Maskable寄存器,从XGPIOPS_DIRM_OFFSET后才是真正用来配置的寄存器。所以真正的偏移地址是两者之和。而基址是来自InstancePtr也就是在XGpioPs_CfgInitialize函数中绑定的基址信息(0xE000A000)。写方向寄存器和这个分析方式向相同不再赘述。
