作者:KimonKarras,赛灵思公司研究工程师, kimonk@xilinx.com
James Hrica,赛灵思公司高级软件应用工程师, jhrica@xilinx.com
设计人员使用赛灵思级高层次综合工具,能以类似软件的方式用高级编程结构描述包处理系统,而使用RTL则难以实现。
不同层面的协议处理常见于各种新型通信系统,因为任何信息交流都需要使用某种通信协议。通信协议一般包含数据包。数据包由发送方创建,由接收方重新 组合,这些操作都要遵循协议规范。这样协议处理无处不在,需要FPGA设计人员特别关注。因此高效地实现协议处理功能对FPGA有非常重要的意义。
设计人员在视频处理和信号处理领域运用高层次综合(HLS)功能已取得巨大成功。使用HLS,用户可使用高级编程语言来表达硬件功能。为测试这种技 术用于包处理的效果,我们用赛灵思Vivado HLS工具构建了一个完整的原型系统,其结果确实令人振奋。Vivado HLS不仅让我们将开发时间缩减了一半,而且还减少了资源使用并降低了时延。我们的原型系统是一个简单的ARP/ICMP服务器,能对ping和地址解析 协议(ARP)请求做出响应并解析IP地址查询。
下面我们深入了解一下Vivado HLS是如何帮助设计人员解决在协议处理过程中遇到的主要问题。为了解这项技术的优势,应首先详细了解Vivado HLS,掌握其工作方式。
提高抽象层次
Vivado HLS能提高系统设计的抽象层次,为设计人员带来切实的帮助。Vivado HLS通过下面两种方法提高抽象层次:
使用C/C++作为编程语言,充分利用该语言中提供的高级结构;
提供更多数据原语,便于设计人员使用基础硬件构建块(位向量、队列等)。
与使用RTL相比,这两大特性有助于设计人员使用Vivado HLS更轻松地解决常见的协议系统设计难题。最终简化系统汇编,简化FIFO和存储器访问,实现控制流程的抽象。HLS的另一大优势是便于架构研究和仿真。
Vivado HLS把C++函数视为模块,函数定义等效于模块的RTL描述,函数调用等效于模块实例化。这种方法能减少需要用户编写的代码量,进而显著简化用于系统描述的结构代码,最终加速系统汇编进程。
在Vivado HLS中,存储器或FIFO可通过两种方法访问。一种是通过合适的对象(比如对流对象的读写)。另一种是直接访问综合工具随后将实现为Block RAM或分布式RAM的标准C阵列。综合工具会根据需要处理额外的信令、同步或寻址问题。
从控制流的角度,Vivado HLS从简单的FIFO接口到完整的AXI4-Stream均可提供整套流控制感知接口。使用这些接口,设计人员可直接访问数据,无需检查背压或数据可用性。Vivado HLS会适当地调度执行,应对一切紧急情况,同时确保正确完成执行。
设计人员还会感激Vivado HLS提供的另一项功能,即简便的架构研究功能。用户只需在代码中插入程序指令(如使用GUI或批处理模式时的Tcl命令),就可以把设计所需特性传递给 综合工具。这样用户可以在不修改设计代码本身的情况下研究大量备选架构方案。研究的范围可以是模块流水线化等根本性问题,也可以是FIFO队列深度等较常 见的问题。
最后,C和RTL仿真是Vivado HLS另一个大放异彩的地方。设计一般采用两步流程验证:第一步是C语言仿真。这个步骤中C/C++的编译和执行与常见的C/C++程序相同;第二步是C /RTL协仿真。在这步骤中,Vivado HLS会根据C/C++测试平台自动生成RTL测试平台,然后设置并执行RTL仿真,检查实现方案吧的正确性。
如能充分发挥这些优势,这将对于用户的系统设计大有裨益。这不仅体现在开发时间和生产力上,还由于Vivado HLS代码更加紧凑的特点,体现在代码可维护性和可读性上。此外通过高层次综合,用户仍能有效控制架构及其特性。正确理解和使用Vivado HLS程序对实现这一控制起着根本作用。
高层次综合在赛灵思提供的包处理解决方案的层级结构中起着承上启下、承前启后的作用。而Vivado SDNet(见《赛灵思杂志》第87期的封面专题报道)和RTL则对其起到补充作用。Vivado SDnet使用特定领域语言,提供一种大为简便但相当受限的协议处理系统表达方法。RTL则可以用于Vivado HLS无法表达的大量系统的实现工作(例如使用DCM或差分信号并需要详细时钟管理的各类系统)。虽然有种种局限,Vivado HLS仍然是在保证结果质量或设计人员灵活性的前提下设计大部分协议处理解决方案的有效途径。
设置简单系统
开始新设计时需要完成的最基本工作首先是确定设计的结构,然后将其实现在Vivado HLS中。Vivado
HLS中的基本系统构建块是C/C++函数。构建一个由模块和子模块组成的系统意味着需要用一个顶层函数来调用底层函数。图1所示的是一个极为简单的三级
流水线,我们以此为例来介绍Vivado HLS中系统构建的基本思路。一般采用流水线化设计执行协议处理,由每一级负责解决处理的特定部分。
构建一个由模块和子模块组成的系统意味着需要用一个顶层函数来调用底层函数。
例1:在Vivado HLS中创建简单系统
1 void topLevelModule(stream&inData,
stream&outData) {
2 #pragma HLS dataflow interval=1
3
4 #pragma INTERFACE axis port=inData
5 #pragma INTERFACE axis port=outData
6
7 static stream> modOne2modTwo;
8 static stream> modTwo2modThree;
9
10 moduleOne(inData, modOne2modTwo);
11 moduleTwo(modOne2modTwo, modTwo2modThree);
12 moduleThree(modTwo2modThree, outData);
13 }
例1中的代码用于创建顶层模块函数,供调用所有其它子函数使用。顶层模块函数使用两个参数,均属于“流”(stream)类(Vivado HLS库中提供的模块类之一)。流是一种HLS建模架构,代表准备以流方式交换的数据通过的接口。流可以实现为FIFO队列或内存,也可以是一种能够配合 任何C++架构使用的模板类。在本例中,我们定义了一种称为axiWord的数据结构(Struct),如例2所示。
例2:定义流接口使用的C++ 结构
structaxiWord {
ap_uint<64> data;
ap_uint<8>strb;
ap_uint<1> last;
};
该struct用于定义AXI4-Stream接口的部分字段。Vivado HLS能自动支持此类接口,使用编译指令(pragma)语句即可完成设定。编译指令是对高层次综合工具的指令,用于指导工具实现要求的结果。例1中第4 行和第5行的编译指令用于告知Vivado HLS这两个指令(具体是顶层模块的输入和输出端口)将使用AXI4-Stream接口。AXI4-Stream I/F包含两个必备信号,分别是有效信号和就绪信号,但它们没有包含在声明的数据结构中。这是由于Vivado HLS AX4 I/F会在内部处理这些信号,也就是说它们对用户逻辑而言是透明的。如前文所述,在使用AXI4-Stream I/F时,从用户处抽象流控制完全由Vivado HLS完成。
当然未必一定使用AXI4-Stream接口。Vivado HLS提供有丰富的总线接口。这里选择AXI4-Stream作为常见标准接口的示例,供用户进行包处理。
实现我们的设计的下一项工作是确保我们的三个模块彼此互联。这项工作也通过流完成,不过这次它们是位于顶层模块的内部。第7行和第8行用于声明实现 这一目标的两个流。这两个流使用了另一种Vivado HLS结构ap_uint。这是一种无符号一维位阵列,随后将按此对其操作。同时这也是又一种模板类,因此必须设定这个阵列的宽度。在本例中使用64位, 与顶层模块输入输出I/F的数据成员宽带匹配。还有一点需要详细说明的是这些流全部声明为静态变量。静态变量是指其值不随函数调用变化的一种变量。由于在 作为顺序C/C++程序执行时顶层模块(以及全部的子模块)每个时钟周期会被调用一次,所以任何需要保持其值不随时钟周期变化的变量都需要声明为静态变 量。
创建流水线设计
将要讨论的最后也是最重要的一个是编译指令。第2行中的数据流编译指令指示Vivado
HLS尽量以并行方式安排执行该函数的所有子函数。“internal”参数用于设置该模块的初始化间隔(II)。初始化间隔(II)告知Vivado
HLS该模块必须具备的处理新输入数据字的频次,故决定了设计的吞吐量。不过这并不妨碍模块内部的流水线化和拥有>1的时延。当II=2时,该模块
将用两个周期完成数据字的处理,然后再读入新的数据字。以这种方式Vivado
HLS可以简化模块最终的RTL。也就是说,在一个典型的协议处理应用中,设计必须具备每个时钟周期处理一个数据字的能力,故从现在起我们令II=1。
初始化间隔(II)告知Vivado HLS该模块必须具备的处理新输入数据字的频次,故决定了设计的吞吐量。
最后要解决的问题是函数调用本身。在Vivado HLS中,这个过程对应的是模块的实例化。传递给每个模块的参数实质上定义了模块的通信端口。在本例中,通过将输入连接到第一个模块,然后用 modOne2modTwo流把第一个模块连接到第二个模块,依次类推,将三个模块链接起来。
设置简单系统
协议处理一般情况下属于状态事务。必须先顺序读取在多个时钟周期内进入总线的数据包字,然后根据数据包的某些字段决定进一步操作。通常应对这种处理的方法
是使用状态机,对数据包进行迭代运算,完成必要的处理。例3是一种简单的状态机,用于根据上一级的输入丢弃或转发数据包。该函数接收三个参数:一个是通过
“inData”流接收到的输入分组数据;一个是通过“validBuffer”流显示数据包是否有效的1位旗标;第三个是称为“outData”的输出
分组数据流。注意Vivado HLS函数中的参数是按引用传递的。这在使用较为复杂的Vivado
HLS流的时候是必要的。ap_uint等较为简单的数据类型则可按值传递。
第2行中的流水线编译指令指示Vivado HLS将该函数流水线化,让初始化间隔为1(II=1),即每个时钟周期处理一个新的输入数据字。Vivado HLS负责核验设计,并确定需要在设计中引入多少个流水线级来满足调度限制要求。
例3:使用Vivado HLS的有限状态机
1 void dropper(stream&inData,
stream>&validBuffer,
stream&outData) {
2 #pragma HLS pipeline II=1 enable_flush
3
4 static enumdState {D_IDLE = 0, D_STREAM, D_
DROP} dropState;
5 axiWordcurrWord = {0, 0, 0, 0};
6
7 switch(dropState) {
8 case D_IDLE:
9 if (!validBuffer.empty() && !inData.empty()) {
10 ap_uint<1> valid = validBuffer.read();
11 inData.read(currWord);
12 if (valid) {
13 outData.write(currWord);
14 dropState = D_STREAM;
15 }
16 }
17 else
18 dropState = D_DROP;
19 break;
20 case D_STREAM:
21 if (!inData.empty()) {
22 inData.read(currWord);
23 outData.write(currWord);
24 if (currWord.last)
25 dropState = D_IDLE;
26 }
27 break;
28 case D_DROP:
29 if (!inData.empty()) {
30 inData.read(currWord);
31 if (currWord.last)
32 dropState = D_IDLE;
33 }
34 break;
35 }
36 }
第4行用于声明一个静态枚举变量,用于表达该FSM中的状态。使用枚举与否可以选择,不过能让代码更容易阅读,因为可以给状态适当地命名。不过使用 任何整数或ap_unit变量也能得到与之类似的结果。第5行用于声明一个“axiWord”类型的变量,用于存储准备从输入中读取的分组数据。
第7行中的开关语句用于表达实际的状态机。建议使用开关,但非强制要求。使用if-else决策树也能执行同样的功能。开关语句能够让Vivado HLS工具更高效地枚举所有状态,并优化得到的状态机RTL代码。
执行从D_IDLE状态开始,此时FSM从第10行和第11行的两个输入流读取。这两行分别代表两种流对象读取方法。这两种方法均从设定的流读取, 然后将结果存储到给定变量中。这种方法采取阻塞式读取,意味着如果该方法调用无法顺序执行,就会暂停执行该函数调用中的其余代码。在试图读取空流的时候会 发生这种情况。
本视频基于Xilinx公司的Artix-7FPGA器件以及各种丰富的入门和进阶外设,提供了一些典型的工程实例,帮助读者从FPGA基础知识、逻辑设计概念
本课程为“从零开始大战FPGA”系列课程的基础篇。课程通俗易懂、逻辑性强、示例丰富,课程中尤其强调在设计过程中对“时序”和“逻辑”的把控,以及硬件描述语言与硬件电路相对应的“
课程中首先会给大家讲解在企业中一般数字电路从算法到流片这整个过程中会涉及到哪些流程,都分别使用什么工具,以及其中每个流程都分别做了
@2003-2020 中国电子顶级开发网