使用 C/C++ 将图像处理任务转交给可编程逻辑

SDSoC 让编程人员能够构建完整的硬件— 软件系统,且不牺牲性能。

作者:Olivier Tremois 赛灵思公司DSP 现场应用工程师 olivier.tremois@xilinx.com

当今的医疗、工业及越来越多其他应用领域的“标准”图像处理系统变得越来越先进。很多情况下,图像处理复杂性已经超出了带 GPU 加速功能的 PC的处理能力范围。设计团队在提高图像处理质量标准,增加产品特性的同时,他们还必须满足客户对最终产品的更小型化、移动性、电池供电等要求。众多现有平台都在努力满足如此复杂的要求。

幸运的是,设计团队可利用赛灵思 Zynq®-7000 All Programmable SoC 和最新赛灵思 SDSoC ™ 开发环境创建出小型、低功耗、特性丰富并带有基于 C/C++ 语言的高级成像系统的产品。我们来了解一下如何使用 SDSoC环境对图像流水线处理系统进行加速,以实现上述目标。我用不到一周时间就完成了这个项目,且实现了大幅度的系统加速。

我们的图像批量处理系统将读取存储在SD 卡中的图像,并利用不同的噪声级参数和结构元素形状的参数来处理这些图像。

图像批处理
我们的实例系统使用专门的相机获取图像,然后以批量模式处理图像。图像尺寸达 3,000 x 2,000像素(600 万像素)。尽管处理后的图像不是实时视频,但目的是通过图像流水线尽可能快地发送图像。这里所用的流水线相当简单:将 RGB 图像转换为灰度图像;添加椒盐噪声;以及用三个滤波器(膨胀、中值和腐蚀)对噪声图像进行滤波。膨胀、中值和腐蚀滤波器均属于中值滤波器系列,这类滤波器主要用来(但并非专门用于)消除脉冲噪声以实现图像增强。这些都是非线性滤波器,不涉及任何算术运算,而且功能仅限于数据排序和采集。尽管算法不是太复杂,但是当处理大图像时会耗用相当多的处理时间,原因在于处理器的有序性,在给定时间内只处理 1 个像素。

中值滤波器是非线性滤波器,需要逐像素地计算输出图像。方法是取特定形状(称为结构元素)内输入像素的临近像素,将它们进行排序,并选
出位于 pth rank 上的像素。腐蚀滤波器选择最小值(p=1)。膨胀滤波器选择最大值(p=N,其中 N 是结构元素的像素数量)。中值滤波器选择中间值(p =[N/2])。通常,结构元素是正方形、菱形或十字形(图 1)。

图 1 — 7x7 边界框中的结构元素

图 1 — 7x7 边界框中的结构元素

我们的图像批处理系统会读取存储在 SD 卡中的图像,并利用不同的噪声级参数和结构元素形状的参数来处理这些图像。运行频率为 667MHz 的Zynq-7000 SoC 的双核 ARM® Cortex ™ -A9 处理器负责执行计算任务。

软件实现
首先,我们用 C++ 编写完整的应用程序,这样就可估算 Cortex-A9 的计算性能。应用程序包含一系列函数,用以读写 SD 卡上的 BMP 图像,计算亮度,添加噪声,并执行各种滤波器功能。采用 SDSoC开发环境的 SDDebug 配置,能够通过赛灵思ZC702 评估平台在 Linux 操作系统下快速进行软件实现。

为生成真正可使用的可执行文件,我们选择选项 O3 启动所有编译器优化。结构元素的形状是应用程序的一个参数,这样我们可以采用任何适合放在7 x 7 像素边界框中的结构元素。对流水线时延(图 2)有影响的参数是图像尺寸(#Size),结构元素中有效像素的数量(#Shape)。最小化时延能改善系统性能。FPGA 对涉及很多加法和乘法运算的信号处理算法执行得非常好。我们的系统实例将展示可编程逻辑不仅善于强力计算,而且还善于执行更标准的数据处理。

图 2 - Zynq 处理系统的运行时间

图 2 - Zynq 处理系统的运行时间

基本特性分析(图 3)显示,通过 RGB 值计算亮度 (0.13%),以及为像素添加噪声 (0.34%),这些操作在软件中运行得非常快。中值滤波器占用了总时间中的大部分时间( 达 92.33%)。文件读取和保存也会占用时间。

图 3 — 初始软件的特征分析结果

图 3 — 初始软件的特征分析结果

将函数转移到硬件
加速的首要目的是在每个时钟周期内处理一个新样本。重写部分代码重写以及重新设计接口可实现大幅加速。即使片上可编程逻辑 (PL) 的时钟速率远低于处理系统 (PS) 的时钟速率,但是能够在每个时钟周期内处理一个输入像素也可以实现大幅加速。

中值滤波器是唯一被转移到硬件的函数。在SDSoC 环境中将功能转移到 PL 是一件非常容易的事情,只需在 Project Explorer 中右键点击即可,而且不会添加任何指令(除了在接口位置添加外),也不用修改任何一行代码来提高性能。这些修改由嵌入式编程人员负责,这就说明了为何初始加速通常不那么明显。

以上指定的函数包含两个贯穿整个图像的嵌套循环。它还包含多个贯穿结构元素的子回路,可对所有元素进行排序。在本例中,我们使用标准气泡排序算法。其他复杂度低的算法适合通过微处理器实现,而这种算法的规则性更适合硬件实现:
for ( i=0; i for ( j=0; j {
Some Code
for ( s=0; s for ( k=0; k k++)
for ( l=0; l l++)
{
Swap pixels if not correctly ordered
}
}

由于我们想在每个时钟周期内处理一个 1 个输出图像像素,因此必须添加一条指令以便针对每个时钟周期启动像素矢量排序。我们利用值为 1 的启动间距 (II) 对经过图像纵列的第二个循环进行流水线化处理。(II 是指新的循环迭代启动之前所需经过的时钟周期数量。)通过使用这条指令,SDSoC 环境将自动展开剩下的内循环,让硬件能够并行处理所有迭代。

加速的首要目的是能够在每个时钟周期中处理一个新样本。重写部分代码以及重新设计接口可实现大幅加速。

在单核处理器中实现的图像处理算法很容易用代码编写,因为各种处理器功能使数据可以在外部存储器和处理器本身之间平稳传输。存储器高速缓存 L1 和 L2 会暂时存储以后可能复用的数据,从而缩短数据存取时延。

这种机制在 FPGA 中不是默认存在。尽管这样我们就无法使用同一 C/C++ 源代码创建硬件加速器,但我们可以设计一个性能和尺寸完全适合我们应用的存储器高速缓存。这是一个很好的例子,这种情况下我们修改 C/C++ 源代码的目的不是保持相同的功能,而是将性能提升到一定程度以便满足我们的要求。赛灵思的 Vivado® 高层次综合 (HLS) 是一种 SDSoC 引擎,能够从 C/C++ 代码生成寄存器传输级 (RTL) IP ;HLS 会考虑到我们的指令,生成一种适用于我们代码的硬件架构。这就是为什么分析图像处理代码时不会自动生成线缓冲器和分析窗口;Vivado HLS 忠于原来的代码,这样能防止工具在未经开发者同意的情况下采取并隐藏优化措施。

熟悉硬件图像处理的设计人员对线缓冲器和分析窗口了如指掌。为避免从外部存储器中多次读取同一像素,像素会临时存储在内存 (Block RAM)中,如果剩下的执行过程再也用不到这些像素,那么这些像素会被覆盖。Block RAM 有两个端口,这两个端口可用作存储器读取、存储器写入或二者存储器读写。当加速器接受了与 L 行和 C 列对应的像素,就会从线缓冲器中读取所有与 C 列和 (L-1 …L-6) 行对应的像素,并重新写入另一个位置,如图4 所示。为了实现每个时钟周期内处理 1 个像素这一目标,必须以一个时钟周期的吞吐量执行所有数据移动。

图 4 — 当接收新像素时线缓冲器中的数据移动

图 4 — 当接收新像素时线缓冲器中的数据移动

此外,像素邻域中的所有像素以及结构元素也必须在一个时钟周期内访问。为此,我们还需要定义一个分析窗口,其中包含需要处理的特定像
素(随像素不同而不同)。在 SDSoC 环境和 VHLS中,代码不以任何形式进行时控; 工具会针对所用的资源和我们的指令将任何可以并行处理的任务均并行化。在我们的图像样本批处理系统代码中,我们通过使用正确的分区指令(图 5) 声明两个数组,从而为代码添加线缓冲器和分析窗口。然后,我们将数据运动描述为对这些数组的读/ 写访问(图 6)。

图 5 — 针对线缓冲器、分析窗口和结构元素进行的数组声明

图 5 — 针对线缓冲器、分析窗口和结构元素进行的数组声明

图 6 — 线缓冲器之间的数据移动

图 6 — 线缓冲器之间的数据移动

由于依赖数组中的数据访问,因此像素值排序过程在硬件架构中实现起来会比较复杂。软件实现方法所使用的 C 代码需要取得像素(已通过结构元素对像素进行了验证)的向量,并使用标准冒泡排序法对向量排序。还有一些效率更高的算法,但是这些算法只有对较大向量才能发挥显著优势。算法的复杂程度与结构元素的像素数量平方成正比,我们这个实例设计是 (7 x 7)2。

在硬件中,架构必须针对最坏情况来进行设计。如果我们要实现每个时钟周期内处理 1 个像素这个目标,需要实现非常规则的结构。为此,我们规定输入向量总是具有最大尺寸(7 x 7),而且所有未验证的像素都具有数值 0,这样它们会处于排序向量底部。我们还要针对最差情况设计级数,即使对于具有较少有效像素的结构元素来说级数可能更低。只有相同向量不在每级重用时,才会发生不同级的并行化。结果得到一个数组,在这个数组中初始向量从列索引 0 入,从列索引 7 x 7 = 49 出(如图7 和 8 所示)。

图 7 —10 元素向量的排序网络

图 7 —10 元素向量的排序网络

图 8 — Figure 8 — Sorting network described in C

图 8 — Figure 8 — Sorting network described in C

SDSOC 系统编译器
SDSoC 并非简单的全系统编译器。它进行大量代码分析,以决定要求在硬件中实现的函数最适合使用哪种数据移动器,并决定将数据移动器连接到哪个端口。对于函数的每个参数,我们必须确定最适合使用 ARM® AMBA® AXI4-Lite、AXI4-Fullmemory-mapped 还是 AXI4-Stream 数据移动器。

我们还需要确定使用哪个连接器:AXI4 高性能 (HP) 端口、通用 (GP) 端口或加速器一致性端口(ACP),甚至是来自其他加速器的端口,可在 SDSoC环境中构建或者包含在板支持包 (BSP) 中。

然后,SDSoC 环境创建一个设计,添加所有必要的 IP 以构成功能完整的系统,例如 AXI4 Stream 数据移动器的直接存储器访问 (DMA) ; 并修改 C 语言源代码(而非初始的C++ 代码),以调用硬件。本例中,接口非常简单:通过 AXI4-Stream 和 DMA 访问两个输入数组和三个输出数组,通过 AXI4-Lite 设置几个标量。我们不必考虑 DMA 的设置,也不必检查标量寄存器的访问地址;SDSoC 环境可自动管理所有事情。

在构建样本系统时,我首先确认源代码是否兼Vivado HLS,然后添加 VHLS 指令。我使用特定的 SDSoC 指令来指定数据在物理空间内连续存储(通过函数 sds_alloc 分配的存储器) ,并指定通过DMA 来访问数据(图 9)。

图 9 — SDSoC 环境中用于覆盖默认行为的指令

图 9 — SDSoC 环境中用于覆盖默认行为的指令

然后,我把构建配置切换至 SDEstimate,以粗略估算所能实现的加速(图 10)。我不必为这个步骤等候很长时间,因为此时尚未构建硬件。SDSoC环境可通过处理器运行时间(使用针对硬件修改的代码计算出的(这比使用初始针对处理器修改的代码计算得出的慢)并将编译器优化参数设定为–O0)和时钟周期数量(采用 VHLS 计算得出的,作为硬件加速器的时延)计算出加速估算值. 该时延是硬件加速器的最大时延,因此这个估算值应该作为粗略估算。

图 10 — SDEstimate 阶段获得的性能估算值

图 10 — SDEstimate 阶段获得的性能估算值

就硬件加速器本身而言加速效果几乎达 700倍。“main”级有很多文件访问需要花费不少时间;这就是为什么总体加速“仅为”5 倍。实际上,我们可以选择计算全局加速所涉及的顶层函数,这样就可获得更有意义的加速值。

流程的最后一步是构建整个系统。这个阶段,构建所有加速器都并连接到处理器。然后,修改C++ 源代码以启动和控制这些加速器(而非调用初始 C 函数)。在这个阶段,我们可以得到使用硬件加速器实现的准确加速值,其中考虑了所有进出DDR 的数据。这个加速值还考虑了清除缓存的时间,因为我们的数据位于存储器的可缓存部分。

硬件加速器占用的时间与图像的大小(而非结构元素的大小)成正比这就是为什么结构元素中的有效像素数量越多,加速比就会越高。图 11 中的时延是整个图像流水线的时延,包含软件和硬件元素。开发这个项目时,构建软件应用是时间最长的一个阶段。在此之后,修改代码的时间不足 2 小时,这样就可得到完全兼容的 Vivado HLS 代码,并具备正确的指令来优化吞吐量。考虑到该设计的硬件部分较大(芯片的半个查找表),完成最后阶段—— 综合、布局布线、比特流、SD 卡—— 耗用2 个多小时。

图 11 — 可编程逻辑中带加速功能的 Zynq 处理系统的运行时间

图 11 — 可编程逻辑中带加速功能的 Zynq 处理系统的运行时间

SDSoC 环境的系统级特性分析集成工具、可编程逻辑中的自动软件加速功能以及全系统优化编译—— 自动生成正确的连接以最小化存储器访问瓶颈—— 使我能够在不到一周的时间里完成这个实例项目。

如果使用标准的RTL 流程创建加速器,并凭借我自己的编程能力来利用不同驱动程序以修改C 代码,那么根本无法在这么短的时间内完成。