更多课程笔记请查看:【zynq裸机编程课程笔记合集】
http://www.corecourse.cn/forum.php?mod=viewthread&tid=29095
在上节课内容中,我们重点介绍了以硬件底层思维的方式来完成对GPIO的编程控制,驱动GPIO翻转,并最终让LED呈现闪烁效果。 这种编程方式,生成的代码尺寸小,执行效率高,适合在对性能和实时性要求较高的场合使用。 但同时,这种编程方法存在的问题也是非常明显。 首先,该方法需要编程人员对外设控制器的底层寄存器功能和读写方式有非常准确的认识和理解,一旦理解错误或者操作错误,就无法正确实现功能。 其次,这种编程方法使得开发者手写代码量更大。数字逻辑运算能力要求较高(主要是各种移位、或、与操作) 再者,这种方法编写的程序可读性较差,读者需要同样准确了解寄存器的详细功能,才能理解程序的具体含义,不利于排查程序bug
最后,这种方法编写的程序通用性较差,这类程序往往都是作者针对特定硬件直接编程,兼容性没有做过多考虑,比如我们前面使用MIO7点亮LED灯,就直接使用了GPIO0的BaseAddr,而如果使用MIO50来点亮LED,则BaseAddr需要改,LSW和MSW寄存器也需要换(MIO7操作MASK_DATA_LSW、MIO50操作MASK_DATA_MSW)。导致迁移到不同的硬件平台时,代码修改量较大。
如果我们编程者对Zynq芯片的各个外设控制器的寄存器功能不熟悉,或者本身编程能力较弱,那么使用这种基于寄存器的编程方式,编程和调试所消耗的时间和精力,就要多很多。所以,对于一些对程序尺寸无要求,或者对程序执行效率要求不高的场合,就可以使用另外一种更加简单高效的编程方法,也就是本节课程的主题——基于SDK硬件驱动库的编程方法。 什么是SDK硬件驱动库呢? 由于Zynq SoC FPGA 芯片,以及开发Zynq SoC FPGA的软件应用程序所用到的IDE(也就是SDK)都是由Xilinx开发的,所以,Xilinx原厂针对Zynq SoC FPGA 芯片的各个外设控制器,都编写了一套功能全面的驱动库,并与SDK软件深度融合,当用户在Vivado中创建好了含Zynq 处理器的系统后,SDK就能根据Vivado生成的系统信息,为使用到的各个硬件外设控制器生成编写好的驱动程序和应用程序库。并自动加入用户创建的软件工程中,用户使用时,只需要调用这些驱动程序或应用程序,就能完成对各个硬件外设控制器的使用。 举个例子 下面这段代码,同样实现了对ACZ702开发板上PS_LED的闪烁控制。 #include "xgpiops.h" #include "unistd.h" XGpioPs Gpio; XGpioPs_Config *ConfigPtr; int main(void) { ConfigPtr = XGpioPs_LookupConfig(XPAR_PS7_GPIO_0_DEVICE_ID); XGpioPs_CfgInitialize(&Gpio, ConfigPtr, ConfigPtr->BaseAddr); XGpioPs_SetDirectionPin(&Gpio, 7, 1); XGpioPs_SetOutputEnablePin(&Gpio, 7, 1); while(1) { //设置bit7输出1 XGpioPs_WritePin(&Gpio, 7, 0x1); usleep(500000); //设置bit7输出0 XGpioPs_WritePin(&Gpio, 7, 0x0); usleep(500000); } return 0; } |
该代码实现的功能和我们前面编写的基于寄存器的方式驱动GPIO点亮LED的程序完全相同,区别在于,基于寄存器直接读写的方式,我们进行了很多数据移位操作以实现对指定位的GPIO的操作,而在这个新的程序中,我们只需要传入需要操作的Pin编号,以及需要设置的0或者1的状态,就能够实现想要的功能。 同时,使用的各个函数的名称也能够非常直观的看出该函数的作用,以下为程序中使用到的函数名称和实现功能的对应表。
XGpioPs_LookupConfig | | | | | | XGpioPs_SetOutputEnablePin | | | |
使用这些函数,我们基本可以不用关心GPIO外设的寄存器的相关功能,只需要从应用的角度,直接调用以功能作为命名规则的函数,就能实现想要的功能,而具体的对寄存器的操作,则由这些函数底层完成。 由于这些函数是Xilinx原厂编写,经过了全球众多使用者的长期实践检验,所以其可靠性和合理性是非常高的,因此,使用这些驱动库,能让我们更加放心和省心。在加快我们项目开发的同时,还能降低我们程序出错的概率。所以,在对各种对性能和效率要求不高的场合,众多的工程师确实更倾向于使用这些SDK提供的驱动和应用库来完成自己的设计。
另外,在前面,我们也一直在说,在对性能和效率要求不高的场合,大家更多的使用这些库来编程。那么这些库究竟是因为什么原因,执行效率相对较低,又能低多少呢。 下面以一个最常用的函数,XGpioPs_WritePin的具体内容来分析。 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); } |
可以看到,进入到该函数后,首先定义了一系列的临时变量,并分别判断了传入参数的有效性。 接着,程序根据输入的管脚编号确定了该管脚所在的GPIO Bank以及位于该Bank中的高16位还是低16位。 然后,程序根据Pin管脚是大于在Bank中是位于高16位还是低16位,确定了需要操作的MASK_DATA寄存器。 最后,执行了和我们直接读写寄存器驱动LED编程时相同的方法,进行移位、取反、拼接等操作,最后再写入对应的GPIO Bank的MASK_DATA寄存器中。 所以,使用GPIO库,相比于我们直接写寄存器驱动GPIO,效率大概只有1/3左右。这也是我们说的这类驱动库效率相对较低的原因。
这种效率损失,在驱动LED这类应用中完全没有影响,基本可以忽略,所以也希望大家不要因此投鼠忌器,不敢使用驱动库。 凡事都要分场合,这种效率损失在有一类应用中影响较大,就是使用IO口模拟各种串行接口,例如I2C、SPI、UART等(SPI重要一点,UART和I2C本身速率也不高,影响一点也无所谓)。这类串行接口,工作时IO翻转次数远高于LED闪烁,同时可能会长期运行,所以使用直接读写寄存器的方式,就能够降低效率损失。另外,在一些需要尽可能节省每一个时钟周期的环节,如中断服务函数中执行,使用直接操作寄存器的方式能够显著提高中断处理速度,提升系统整体性能。 其实,驱动库里面进行的各种判断和合理性检查,我们使用寄存器编程时候,也不是没做,只不过我们是人为将这些过程在程序之外,在开发者的脑海中执行了,我们针对具体的硬件和应用场景,预先分析并处理好需要写入的值之后,最终只由CPU执行向寄存器中写入最终结果的步骤。
好了,讲到这里,相信大家对于使用SDK硬件驱动库编程已经有了一个较为立体的认知。也都跃跃欲试,那么紧接着问题就来了,我们该怎么知道,这些驱动库怎么使用?或者,换一种问法,上述我编写的驱动LED闪烁的程序,是以什么依据编写的呢,为啥就是用这几个函数,传入的参数又是什么意思,我怎么就知道程序该这么写呢? 接下来,我就以上一节课留给大家的作业为例,展示一下我是怎样查找资料,编写出该程序的。
其实,写出这个程序来,我也是参考的现成的例程,那么这个例程从哪里来呢,其实就在SDK的开发环境中默认提供好了。这里先带领大家看看如何找到指定外设控制器的应用例程。
|