之前的几篇文章主要集中在 Zynq SoC 的处理系统 (PS) 方面,包括:
然而,从设计角度来看,Zynq SoC 真正令人兴奋的方面是创建一个使用 Zynq 可编程逻辑 (PL) 的应用程序。使用 PL 将任务从 PS 加载到 PL 端,为其他任务回收处理器带宽从而加速任务。此外,PS 端可以控制 PL 端在经典的片上系统应用中执行的操作。使用 Zynq SoC 的 PL 端可以提高系统性能、降低功耗并为实时事件提供可预测的延迟。
Zynq PS 和 PL 通过以下接口互连:
以下是说明这些不同接口点的框图:
ARM 的 AXI 是一种面向突发的协议,旨在提供高带宽同时提供低延迟。每个 AXI 端口都包含独立的读写通道。要求不高的接口使用的 AXI 协议的一个版本是 AXI4-Lite,它是一种更简单的协议,可用于寄存器式控制/状态接口。例如,Zynq XADC 使用 AXI4-Lite 接口连接到 Zynq PS。有关 AXI 协议的更多信息,请访问:
http://www.arm.com/products/system-ip/amba/amba-open-specifications.php
Zynq SoC 支持三种不同的 AXI 传输类型,可以使用它们来连接PS到设备的PL端:
下表定义了每个接口的理论带宽:
必须使用 Zynq SoC 的 DMA 控制器才能达到上表中列出的最大速度。作为一个额外的好处,当 PS 是主机时,DMA 控制器减少了 Zynq SoC 的 ARM Cortex-A9 MPCore 处理器的负载。在不使用 DMA 控制器的情况下,从 PS 到 PL 端的最大传输速率为 25Mbytes/sec。
总而言之,在 PS 和 PL 之间使用了惊人的 14.4Gbytes/sec(115.2Gbits/sec)的理论带宽!
这一节将使用 AXI 接口在 Zynq SoC 的可编程逻辑结构中创建外设。
第一步是打开 Vivado 设计并从工具选项下选择“创建和封装 IP”选项-create and package IP。
这将打开一个对话框,允许创建 AXI4 外设。对话框的第一个实际页面提供了许多选项,用于创建新 IP 或将当前设计或目录转换为 IP 模块。
选择“创建新的 AXI4 外设 - Create new AXI4 peripheral”选项并将其指向预定义的 IP 位置。可以使用 Vivado 主页上的管理 IP 部分创建新的 IP 位置。
然后,该对话框允许输入要用于新外围设备的库、名称、描述和公司 URL。对于这个非常简单的示例(稍后我将对其进行扩展)。
下面的对话框是一个功能强大的对话框,我们可以在其中定义我们希望指定的 AXI4 接口类型:
这个初始示例非常简单,以便我可以演示创建外设所需的流程,在 Vivado 中实现它,然后将其导出到 SDK。出于这个原因,我将只有四个寄存器的 AXI4-L ite 接口,然后我们可以使用软件对其进行寻址。这些寄存器可用于控制设计的可编程逻辑方面的功能操作。
最后的“创建外围设备-create peripheral”对话框允许选择一个选项来为新外围设备生成驱动程序文件。这是一个重要的步骤,因为它将使外设与 SDK 的使用更加简单。
一旦“Create Peripheral”向导关闭,可以打开创建的 VHDL 文件并添加自定义硬件设计以在 PL 中执行想要的功能。我将只使用我们创建的四个寄存器,因此可以不编辑文件。创建了外围设备后,我们希望在 Vivado 设计中连接和使用它。这样做非常简单。我们打开系统框图并从左侧菜单中选择添加 IP 选项。应该能够找到在此菜单中创建的外围设备。可用外围设备按字母顺序列出。
将此 IP 模块拖入设计中,然后将其连接到 AXI GP 总线,其中 Vivado 提供运行连接自动化工具。
运行该工具会产生我们可以实施的设计。
可以通过单击地址编辑器选项卡来修改外设的地址范围。请注意,4k 地址空间是允许的最小地址空间,这对于我们的 4 寄存器示例来说过于慷慨了。幸运的是,Zynq SoC 中的 ARM Cortex-A9 MPCore 处理器拥有大量的地址空间。
一旦 Vivado 自动完成连接及地址空间分配完毕,如下图所示,我们就可以实现设计并将其导出到 SDK。然后我们就可以开始使用我们的外围设备了。
注意,可以检查实施报告以确保包含已创建的外围设备。
上面我们已经产生了一个AXI外设,接下来就是在SDK中验证这个外设的正确性。
使用 Vivado 创建 AXI4 外设并生成BIN文件。
创建了设计的硬件组件后,我们现在需要将其导出到我们的 SDK 设计中,以便我们可以编写软件来驱动它。第一步是在 Vivado 中打开当前工程,编译生成BIN文件,然后将硬件导出到 SDK。(如果尝试导出硬件时,SDK 已在使用中,则会收到警告。)如果不将硬件导出到 SDK,则下次打开 SDK 时,需要将硬件定义和板级支持包更新,否则将无法使用它们。还需要更新设计中定义的存储库,以包括包含外围设备的 IP 存储库。
打开 xparameters.h 文件(在 BSP 包含文件中)以查看专用于新 AXI4 外设的地址空间:
下一步是打开 System.MSS 文件并自定义要使用的 BSP在外设创建过程中生成的驱动程序而不是通用驱动程序。
重新构建项目可确保将驱动程序文件加载到 BSP 中。这是一个非常有用的步骤,因为这些文件还包含一个简单的自检程序,可以使用该程序来测试外设的软件接口是否正确,然后再开始使用它进行更高级的操作。使用此测试程序还表明我们已在 Vivado 中正确实例化了硬件。
在 BSP 下 libsrc 中,将看到许多新 AXI4 外设的文件。这些文件允许像使用原生外围设备(例如 XADC 和 GPIO)一样读取和写入外围设备,我们之前在其他文章中一直在使用它们。
对于这个简单的示例,文件 myip.h 包含我们可以用来驱动新外设的三个函数。
MYIP_mReadReg(BaseAddress, RegOffset)
MYIP_mWriteReg(BaseAddress, RegOffset, Data)
XStatus MYIP_Reg_SelfTest( void * baseaddr_p);
除自检功能外,读取和写入功能都映射到通用函数 Xil_In32 和 Xil_Out32,它们在 Xil_io.h 中定义。然而,使用创建的函数可以使代码更具可读性,因为被寻址的外设非常清晰。
对于这个例子,我们在外设中只有四个寄存器,所以我们将只使用自检,它将写入和读取所有寄存器并报告通过或失败。这个测试让我们相信我们已经获得了正确的硬件和软件环境,一旦我们在外围模块中定义了它们,我们就可以继续使用更高级的功能。在下一篇文章中,我们将研究如何使用 HDL 代码向外设添加一些功能,以从处理系统中卸载功能并提高系统性能。
假设我们想在 Zynq 中执行更复杂的计算,例如针对工业控制系统。通常,这些系统将具有多个模拟输入(通过 ADC),由热敏电阻、热电偶、压力传感器、铂电阻温度计 (PRT) 等传感器驱动。
很多时候,来自这些传感器的数据需要传递函数来将来自 ADC 的原始数据值转换为可用于进一步处理的数据。一个很好的例子是 Zynq XADC,它在 XADCPS.h 中包含许多函数/宏,用于将原始 XADC 值转换为电压或温度。但是,这些转换非常简单。假如这些计算变得越复杂,则需要 Zynq 处理时间就越多。如果使用 Zynq SoC 的可编程逻辑 (PL) 端来执行这些计算,则可以大大加快计算速度。附带的好处是,处理器还可以腾出时间来执行其他软件任务,因此可以通过使用 PL 进行计算来提高处理带宽。
传递函数越复杂,计算结果所需的处理器时间就越多。我们可以使用以millibars为单位的大气压力转换为以米(meters)为单位的高度的示例来演示这种转换。下面的传递函数给出了压力在 0 到 10 millibars之间的海拔高度:
使用 Zynq SoC 的处理系统 (PS) Zynq 实现这个传递函数非常简单,使用下面的代码行,其中“结果-result”是一个浮点数;a、b 和 c 是上述传递函数中定义的常数;i 是输入值
result = ((float)a*(i*i)) + ((float)b*(i)) + (float)c;
对于这个例子,我将使用嵌套在“for”循环中的代码来模拟上面输入值中的步骤。代码通过 STDOUT 输出结果。因为我要计算执行这个计算所需的时间,我将使用私有计时器来确定这个时间,如下:
for(i=0.0; i<10.0; i = i +0.1 ){
XScuTimer_LoadTimer(&Timer, TIMER_LOAD_VALUE);
timer_start = XScuTimer_GetCounterValue(&Timer);
XScuTimer_Start(&Timer);
result = ((float)a*(i*i)) + ((float)b*(i)) + (float)c;
XScuTimer_Stop(&Timer);
timer_value = XScuTimer_GetCounterValue(&Timer);
printf("%f,%f,%lu,%lu,
",i,result,timer_start, timer_value);
}
虽然此代码可能无法提供最准确的时序参考,但足以证明我们研究的原理。在Zynq板上运行上述代码,在终端窗口中获得了以下结果。注意:
对此输出进行一些简单的分析表明,计算结果平均需要 25 个 CPU_3x2x 时钟周期。。使用 666MHz 处理器时钟,此计算需要 76 ns。我相信很多人会看到ADC输出不是浮点数而是一个定点数。使用整数数学重写函数代码导致时钟周期的平均数非常相似。但是我认为对于这个例子,浮点数会更容易使用,并且不需要解释定点数系统背后的原理。
在确定了 Zynq 的 PS 端执行中等复杂度传递函数需要多长时间的基准之后,我们下一次可以看看当我们将相同的函数转移到设备的 PL 端时,我们能以多快的速度计算这个函数。
上一节我们使用PS计算了一个公式,接下来我们将使用PL端加速这一公式计算,但是PL端的特点是只能进行定点计算,所以这一小节我们将说明一下定点数工作原理。
在数字系统中有两种表示数字的方法:定点或浮点。定点表示将小数点保持在固定位置,这就大大简化了算术运算。如图所示,定点数由称为整数和小数部分的两部分组成:数字的整数部分在隐含小数点的左侧,小数部分在右侧。
上述定点数能够使用二进制补码表示表示介于 0.0 和 255.9906375 之间的无符号数或介于 –128.9906375 和 127.9906375 之间的有符号数。
浮点数分为指数和尾数两部分。浮点表示允许小数点根据值的大小在数字内浮动。定点表示的主要缺点是要表示更大的数字或使用小数获得更准确的结果,需要更多的位。虽然 FPGA 可以同时支持定点数和浮点数,但大多数应用程序都采用定点数系统,因为它们比浮点数系统更易于实现。
在设计中,我们可以选择使用无符号或有符号数字。通常,选择受到正在实施的算法的限制。无符号数可以表示 0 到 2n – 1 的范围,并且始终表示正数。有符号数使用补码数系统来表示正数和负数。二进制补码系统允许通过简单地将两个数字相加来从另一个数字中减去一个数字。补码数可以表示的范围是:- (2n-1) ~ + (2n-1 – 1)
表示定点数内整数位和小数位之间分割的正常方式是 x,y,其中 x 表示整数位的数量,y 表示小数位的数量。例如 8,8 代表 8 个整数位和 8 个小数位,而 16,0 代表 16 个整数和 0 个小数位。
在许多情况下,整数和小数位数将在设计时确定,这个时间通常在算法从浮点转换之后。由于 FPGA 的灵活性,我们可以表示任意位长的定点数;我们不仅限于 32、64 甚至 128 位寄存器。FPGA 对 15 位、37 位或 1024 位寄存器同样适用。
所需整数位的数量取决于该数字需要存储的最大整数值。小数位数取决于最终结果的所需精度。要确定所需的整数位数,我们可以使用以下等式:
例如,表示 0.0 到 423.0 之间的值所需的整数位数是:
我们需要 9 个整数位,允许表示 0 到 511 的范围.
两个定点操作数的小数点必须对齐才能加、减或除这两个数字。也就是说,一个 x,8 数字只能添加到、减去或除以同样在 x,8 表示形式中的数字。要对不同 x,y 格式的数字执行算术运算,我们必须首先对齐小数点。请注意,对齐除法的小数点并不是绝对必要的。但是,实现定点除法需要仔细考虑,以确保在这种情况下得到正确的结果。
同样,两个定点数相乘时,小数点也不需要对齐。例如,将两个定点数相乘,格式为 14,2 和 10,6,结果为 24,8(格式为 24 个整数位和 8 个小数位)。对于除以固定常数,我们当然可以通过计算常数的倒数然后将该常数结果用作乘数来简化设计。
上一节简单说了PL端实现定点计算的一些基础知识,接下来就是专注于在系统中实现PL端加速的工作。
在我们开始切割代码之前,我们需要确定我们将在这个特定实现中使用的比例因子(小数点的位置)。在此示例中,输入信号的范围在 0 到 10 之间,因此我们可以将 4 个十进制位和 12 个小数位打包成一个 16 位输入向量。
上面的公式就是我们要实现的,它具有三个常数 A、B 和 C:A = -0.0088 B = 1.7673 C =131.29。我们需要在实现中处理(缩放)这些常数。在 FPGA 中这样做的好处在于,我们可以对每个常数进行不同的缩放以优化性能,如下表所示:
当我们实现上述等式时,我们需要考虑合成向量的扩展,对于术语 Ax^2和 Bx 定义如下:
要使用常数 C 执行最终加法,我们需要对齐小数点。因此,我们需要将结果和 Ax^2和 Bx 除以 2 的幂,以将小数点与 C 对齐。result也将被格式化为这个值,即 8,8。
计算完上述内容后,我们就准备好在前几节创建的 Vivado 外设工程中实施设计。
第一个实现步骤是在 Vivado 中打开框图视图,右键单击IP,然后选择“Edit in IP Packager”。一旦 IP Packager 在顶层文件中打开,我们就可以轻松实现一个简单的过程,在多个时钟周期内执行计算。(本示例中为五个时钟,尽管可以进一步优化。)
现在我们可以在将更新的硬件导出到 SDK 之前,在 Vivado 中重新打包和重建项目(记得更新版本号)。
在 SDK 中,我们可以使用与以前相同的方法,除了现在使用定点数字系统而不是前面示例中使用的浮点系统:
for(i=0; i<2560; i = i+25 ){
XScuTimer_LoadTimer(&Timer, TIMER_LOAD_VALUE);
timer_start = XScuTimer_GetCounterValue(&Timer);
XScuTimer_Start(&Timer);
ADAMS_PERIHPERAL_mWriteReg(Adam_Low, 4, i);
result = ADAMS_PERIHPERAL_mReadReg(Adam_Low, 12);
XScuTimer_Stop(&Timer);
timer_value = XScuTimer_GetCounterValue(&Timer);
printf("%d,%lu,%lu,%lu,
",i,result,timer_start, timer_value);
}
当上面的代码在ZYNQ板上运行时,我们在串行链路上看到以下结果输出:
33610 的结果等于 131.289 除以 2^8 时,这是正确的并且符合浮点计算。尽管数值结果相同,但最大的区别在于执行计算所需的时间。虽然外围设计的实际计算只需要 5 个时钟,但生成结果需要 140 个时钟或 420ns,而在 Zynq SoC 的 PS 侧使用 ARM Cortex-A9 处理器则需要 25 个 CPU 时钟。
为什么会出现差异?PL端不应该更快吗?主要原因时外围 I/O 时间开销。在使用 PL 端时,我们必须考虑 AXI 总线上的总线延迟和 AXI 总线频率,在此应用中为 142.8MHz(请求为 150MHz)。AXI 总线开销导致计算时间长于预期。然而,一切都没有错。错的是我做错了方向:因为这种 I/O 开销时间,将任务转移到 Zynq SoC 的 PL 并不是以这种方式使用的。
那么如果我们要采取更合理的方法,需要怎么做?DMA
下一篇文章我们将使用DMA来搬运数据,看下结果是不是我们要的~~
本文部分源文件:
https://gitee.com/openfpga/zynq-chronicles/blob/master/VHDL_part24.vhd
审核编辑 :李倩
全部0条评论
快来发表一下你的评论吧 !