前言
前面先简单介绍这个实验的基本流程,了解了基本背景后,再重点讲解驱动程序的编写特点,以及用户面对Xilinx官方驱动包时如何使用。
一、功能描述
通过MIO实现对LED灯的控制,使其以固定频率闪烁。
二、实验步骤
1、硬件部分
① 创建系统工程
② 创建BD工程
③ 配置zynq IP:配置电压、串口、时钟、GPIO
④ 连线、编译、导出
2、软件部分
① 创建软件工程
② 添加对应驱动的头文件
③ 查看头文件中关于此驱动的相关参数定义和函数定义
④ 查找硬件配置后对应接口模块是否存在
⑤ 初始化一个接口
⑥ 设置GPIO输出方向
⑦ 配置连接LED的那一位GPIO引脚输出
⑧ 循环给定值0和1值给连接LED的GPIO引脚
三、GPIO驱动程序结构性分析
这里使用的是GPIO驱动程序包,所以首先找到相关的头文件。
在include和libsrc中均可以看到
接下来看下这个头文件的结构,都定义了哪些东西,在xgpiops.h头文件中。
首先,定义了中断类型、bank、引脚数目等信息。
其次,定义了两个结构体,一个保存设备号和基地址,用于查找外设GPIO的存在,另一个保存调用GPIO实例所需要的基本信息,用于初始化操作,分别如下:
最后是函数定义,这里主要关注注释部分说明,定义了哪些函数以及函数所在的文件位置信息,方便我们查看和调用。具体表现为:
/* Functions in xgpiops.c */
/* Bank APIs in xgpiops.c */
/* Pin APIs in xgpiops.c */
/* Diagnostic functions in xgpiops_selftest.c */
/* Functions in xgpiops_intr.c */
/* Bank APIs in xgpiops_intr.c */
/* Pin APIs in xgpiops_intr.c */
/* Functions in xgpiops_sinit.c */
四、调用函数实现实验软件功能
按照第二部分说明的软件顺序,首先需要添加头文件,这里需要添加的头文件有:
#include "xgpiops.h"
#include "sleep.h"
之所以需要添加sleep.h头文件,是因为循环点亮LED,中间需要延时操作,方便用户观察亮灭情况。
1、查找和初始化GPIO外设资源
按照Xilinx的套路,首先需要查找GPIO定义,调用函数
XGpioPs_Config *XGpioPs_LookupConfig(u16 DeviceId);
函数位于xgpiops_sinit.c文件中,函数详情:
XGpioPs_Config *XGpioPs_LookupConfig(u16 DeviceId)
{
XGpioPs_Config *CfgPtr = NULL;
u32 Index;
for (Index = 0U; Index < (u32)XPAR_XGPIOPS_NUM_INSTANCES; Index++) {
if (XGpioPs_ConfigTable[Index].DeviceId == DeviceId) {
CfgPtr = &XGpioPs_ConfigTable[Index];
break;
}
}
return (XGpioPs_Config *)CfgPtr;
}
先定义一个XGpioPs_Config类型的指针,准备接收特定设备号下 查找到的基地址;依次循环查表,表以数组的形式存放于xgpiops_g.c中,如下:
当输入的设备号一致时,就将表中的内容一一对应复制给XGpioPs_Config结构体中元素并返回。(设备号device和定义的GPIO驱动个数XPAR_XGPIOPS_NUM_INSTANCES是自动生成,位于xparameters.h文件中)
接下来回到主函数,判断下查找是否成功,返回状态值。
接着初始化外设GPIO,调用初始化函数
s32 XGpioPs_CfgInitialize(XGpioPs *InstancePtr, XGpioPs_Config *ConfigPtr, u32 EffectiveAddr)
初始化函数调用之前,先定义一个XGpioPs结构体类型的指针实例,用于保存初始化后的关于GPIO的参数,这样做的目的可以使用户初始化很多这样的GPIO以便使用。
初始化函数中,在进行一个判断传入的三个参数是否为空后,然后对保存GPIO基本信息的参数进行赋值。赋值操作如下:
InstancePtr->IsReady = 0U;
InstancePtr->GpioConfig.BaseAddr = EffectiveAddr;
InstancePtr->GpioConfig.DeviceId = ConfigPtr->DeviceId;
InstancePtr->Handler = (XGpioPs_Handler)StubHandler;
InstancePtr->Platform = XGetPlatform_Info();
参数分别为:
① 准备信号赋值0,表示暂时没有初始化完成;
② 基地址
③ 设备号
④ 所有状态的处理程序
⑤ 设备数据
接下来,根据设备参数确定初始化结构体中最大引脚数和最大bank数目。
接下来,关闭GPIO所有中断,这里没有使用GPIO的中断功能,最后将初始化完成后的准备信号赋值,表示此次对GPIO的、初始化工作已经完成,可以使用。
回到主函数,根据初始化函数的返回值,同样做一个判断,是否完成初始化工作。
至此,关于外设GPIO的查找和初始化工作已经全部完成,接下来就是功能性程序。
2、设置GPIO输出方向
调用设置GPIO输出方向的函数
void XGpioPs_SetDirectionPin(XGpioPs *InstancePtr, u32 Pin, u32 Direction)
void XGpioPs_SetDirectionPin(XGpioPs *InstancePtr, u32 Pin, u32 Direction)
{
u8 Bank;
u8 PinNumber;
u32 DirModeReg;
Xil_AssertVoid(InstancePtr != NULL);
Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);
Xil_AssertVoid(Pin < InstancePtr->MaxPinNum);
Xil_AssertVoid(Direction <= (u32)1);
/* Get the Bank number and Pin number within the bank. */
XGpioPs_GetBankPin((u8)Pin, &Bank, &PinNumber);
DirModeReg = XGpioPs_ReadReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET);
if (Direction!=(u32)0) { /* Output Direction */
DirModeReg |= ((u32)1 << (u32)PinNumber);
} else { /* Input Direction */
DirModeReg &= ~ ((u32)1 << (u32)PinNumber);
}
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET, DirModeReg);
}
分析1;判断参数的传入是否为空,具体包括初始化结构体是否为空,结构体中准备信号是否表示为初始化已经准备好,引脚传入是否大于最大引脚数,方向设置参数是否大于1(方向设置参数定义为0或者1)。
Xil_AssertVoid(InstancePtr != NULL);
Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);
Xil_AssertVoid(Pin < InstancePtr->MaxPinNum);
Xil_AssertVoid(Direction <= (u32)1);
分析2:获取bank号、pin脚在所在bank位置的编号
XGpioPs_GetBankPin((u8)Pin, &Bank, &PinNumber);
void XGpioPs_GetBankPin(u8 PinNumber, u8 *BankNumber, u8 *PinNumberInBank)
{
u32 XGpioPsPinTable[6] = {0};
u32 Platform = XGetPlatform_Info();
if (Platform == XPLAT_ZYNQ)
{
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;
while (*BankNumber < 4U) {
if (PinNumber <= XGpioPsPinTable[*BankNumber]) {
break;
}
(*BankNumber)++;
}
}
if (*BankNumber == (u8)0) {
*PinNumberInBank = PinNumber;
} else {
*PinNumberInBank = (u8)((u32)PinNumber %
(XGpioPsPinTable[*BankNumber - (u8)1] + (u32)1));
}
}
还是根据设备数据进行判断,这里我做了代码简化,删除了XPLAT_ZYNQ_ULTRA_MP设备下的bank和pin数据信息。
XPLAT_ZYNQ一共118个引脚,4个bank,通过数组的形式进行划分:
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 */
通过while语句,定位传入的PIN脚属于哪一个bank的范围。
*BankNumber = 0U;
while (*BankNumber < 4U) {
if (PinNumber <= XGpioPsPinTable[*BankNumber]) {
break;
}
(*BankNumber)++;
}
bank确定了以后,开始确定pin脚在此bank中的位置。如果属于bank0,则引脚号就是这个引脚所在bank的位置,如果不是bank0,则需要计算,将引脚数除以上个bank引脚最大数取余数。具体表现为:
if (*BankNumber == (u8)0) {
*PinNumberInBank = PinNumber;
} else {
*PinNumberInBank = (u8)((u32)PinNumber %
(XGpioPsPinTable[*BankNumber - (u8)1] + (u32)1));
}
在获取了引脚号所在bank以及所在bank的位置信息以后,便可以通过写特定寄存器的方式设置对应GPIO引脚的方向了。
分析3:写寄存器,设置对应GPIO方向
首先,查找datasheet,找到设置GPIO方向的寄存器地址。
在写寄存器之前,先读取寄存器的值,存放于一个变量,然后将变量的值进行重新赋值,最后将重新赋值的变量写入寄存器。
DirModeReg = XGpioPs_ReadReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET);
if (Direction!=(u32)0) { /* Output Direction */
DirModeReg |= ((u32)1 << (u32)PinNumber);
} else { /* Input Direction */
DirModeReg &= ~ ((u32)1 << (u32)PinNumber);
}
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_DIRM_OFFSET, DirModeReg);
这里涉及读、写函数:
#define XGpioPs_ReadReg(BaseAddr, RegOffset) \
Xil_In32((BaseAddr) + (u32)(RegOffset))
#define XGpioPs_WriteReg(BaseAddr, RegOffset, Data) \
Xil_Out32((BaseAddr) + (u32)(RegOffset), (u32)(Data))
下面以写函数为例具体分析,函数可以拆分为以下几个步骤:
① 定义了一个函数
#define XGpioPs_WriteReg(BaseAddr, RegOffset, Data)
② 给这个函数赋予具体操作,也就是函数体
Xil_Out32((BaseAddr) + (u32)(RegOffset), (u32)(Data))
函数体里调用了static INLINE void Xil_Out32(UINTPTR Addr, u32 Value)函数。
static INLINE void Xil_Out32(UINTPTR Addr, u32 Value)
{
#ifndef ENABLE_SAFETY
volatile u32 *LocalAddr = (volatile u32 *)Addr;
*LocalAddr = Value;
#else
XStl_RegUpdate(Addr, Value);
#endif
}
这个函数的意义就是像特定的地址写入特定的值,参数一是地址信息,参数二是数据信息。
3、配置MIO第7位输出(实验中MIO第7位连接到外部LED)
查找datasheet,找到使能对应位输出的寄存器。
void XGpioPs_SetOutputEnablePin(XGpioPs *InstancePtr, u32 Pin, u32 OpEnable)
void XGpioPs_SetOutputEnablePin(XGpioPs *InstancePtr, u32 Pin, u32 OpEnable)
{
u8 Bank;
u8 PinNumber;
u32 OpEnableReg;
Xil_AssertVoid(InstancePtr != NULL);
Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);
Xil_AssertVoid(Pin < InstancePtr->MaxPinNum);
Xil_AssertVoid(OpEnable <= (u32)1);
/* Get the Bank number and Pin number within the bank. */
XGpioPs_GetBankPin((u8)Pin, &Bank, &PinNumber);
OpEnableReg = XGpioPs_ReadReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_OUTEN_OFFSET);
if (OpEnable != (u32)0) { /* Enable Output Enable */
OpEnableReg |= ((u32)1 << (u32)PinNumber);
} else { /* Disable Output Enable */
OpEnableReg &= ~ ((u32)1 << (u32)PinNumber);
}
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_OUTEN_OFFSET, OpEnableReg);
}
首先,同样进行参数传入的确认工作,不再赘述。
其次,获取引脚所在bank和引脚所在bank的位置信息,和上面设置GPIO输出方向里面调用的函数一样,不再赘述。
接下来就是写特定寄存器使能特定位输出。
方法和上面写方向寄存器一一致,具体表现为:
OpEnableReg = XGpioPs_ReadReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_OUTEN_OFFSET);
if (OpEnable != (u32)0) { /* Enable Output Enable */
OpEnableReg |= ((u32)1 << (u32)PinNumber);
} else { /* Disable Output Enable */
OpEnableReg &= ~ ((u32)1 << (u32)PinNumber);
}
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_REG_MASK_OFFSET) +
XGPIOPS_OUTEN_OFFSET, OpEnableReg);
4、写引脚值,点亮和熄灭外部连接的LED
while(1)
{
XGpioPs_WritePin(&psGpioInstancePtr, iPinNumber, 1);//点亮MIO的第7位输出1
sleep(1); //延时
XGpioPs_WritePin(&psGpioInstancePtr, iPinNumber, 0);//熄灭MIO的第7位输出0
sleep(1); //延时
}
引用了sleep()函数,参数为1,表示延时1S时间,让LED的亮灭肉眼可见。这就是为什么头文件加入sleep.h的原因。
具体分析写引脚值函数:
void XGpioPs_WritePin(XGpioPs *InstancePtr, u32 Pin, u32 Data)
void XGpioPs_WritePin(XGpioPs *InstancePtr, u32 Pin, u32 Data)
{
u32 RegOffset;
u32 Value;
u8 Bank;
u8 PinNumber;
u32 DataVar = Data;
Xil_AssertVoid(InstancePtr != NULL);
Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);
Xil_AssertVoid(Pin < InstancePtr->MaxPinNum);
/* Get the Bank number and Pin number within the bank. */
XGpioPs_GetBankPin((u8)Pin, &Bank, &PinNumber);
if (PinNumber > 15U) {
/* There are only 16 data bits in bit maskable register. */
PinNumber -= (u8)16;
RegOffset = XGPIOPS_DATA_MSW_OFFSET;
} else {
RegOffset = XGPIOPS_DATA_LSW_OFFSET;
}
/*
* Get the 32 bit value to be written to the Mask/Data register where
* the upper 16 bits is the mask and lower 16 bits is the data.
*/
DataVar &= (u32)0x01;
Value = ~((u32)1 << (PinNumber + 16U)) & ((DataVar << PinNumber) | 0xFFFF0000U);
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_DATA_MASK_OFFSET) +
RegOffset, Value);
}
基本思路如下:
① 判断参数传入是否为空
② 获取bank号和pin所在bank的位置信息
③ 由于bank中对于低16位引脚和高16位引脚的赋值在不同的寄存器,所以需要做一个判断,确定写入寄存器地址
if (PinNumber > 15U) {
/* There are only 16 data bits in bit maskable register. */
PinNumber -= (u8)16;
RegOffset = XGPIOPS_DATA_MSW_OFFSET;
} else {
RegOffset = XGPIOPS_DATA_LSW_OFFSET;
}
④ 因为写入的数据只能是0或者1,所以做一个与运算,再根据pin所在位置是高16位还是低16位,进行待写入数据的移位。
DataVar &= (u32)0x01;
Value = ~((u32)1 << (PinNumber + 16U)) & ((DataVar << PinNumber) | 0xFFFF0000U);
⑤ 写入待写入数据至特定寄存器,寄存器地址同样从datasheet查找。
XGpioPs_WriteReg(InstancePtr->GpioConfig.BaseAddr,
((u32)(Bank) * XGPIOPS_DATA_MASK_OFFSET) +
RegOffset, Value);
完毕!