Vivado HLS推动协议处理系统蓬勃发展

2016年12月20日 | By ityounker | Filed in: FPGA.
Source: http://xilinx.eetrend.com/article/7676

作者: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:简单三级流水线
构建一个由模块和子模块组成的系统意味着需要用一个顶层函数来调用底层函数。

例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<ap_uint<64>> modOne2modTwo;
8 static stream<ap_uint<64>> 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<ap_uint<1>>&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行的两个输入流读取。这两行分别代表两种流对象读取方法。这两种方法均从设定的流读取,然后将结果存储到给定变量中。这种方法采取阻塞式读取,意味着如果该方法调用无法顺序执行,就会暂停执行该函数调用中的其余代码。在试图读取空流的时候会发生这种情况。

流分割和合并
在协议处理中,根据协议栈特定字段转发数据包给不同模块,然后在发送前将不同的流重新组合,是一项关键功能。Vivado HLS允许使用高级架构来推动这一转发过程,具体如例4中所示的流合并。

例4:简单的流合并情况
1 void merge(streaminData[NUM_MERGE_
STREAMS], stream&outData) {
2 #pragma HLS INLINE off
3 #pragma HLS pipeline II=1 enable_flush
4
5 static enummState{M_IDLE = 0, M_STREAM}
mergeState;
6 static ap_uint
rrCtr = 0;
7 static ap_uint
streamSource = 0;
8 axiWordinputWord = {0, 0, 0, 0};
9
10 switch(mergeState) {
11 case M_IDLE:
12 boolstreamEmpty[NUM_MERGE_STREAMS];
13 #pragma HLS ARRAY_PARTITION variable=stream-
Empty complete
14 for (uint8_t i=0;i<num_merge_streams;++i)
15 streamEmpty[i] = inData[i].empty();
16 for (uint8_t i=0;i<num_merge_streams;++i) {<br=””> 17 uint8_t tempCtr = streamSource + 1 + i;
18 if (tempCtr>= NUM_MERGE_STREAMS)
19 tempCtr -= NUM_MERGE_STREAMS;
20 if(!streamEmpty[tempCtr]) {
21 streamSource = tempCtr;
22 inputWord = inData[streamSource].
read();
23 outData.write(inputWord);
24 if (inputWord.last == 0)
25 mergeState = M_STREAM;
26 break;
27 }
28 }
29 break;
30 case M_STREAM:
31 if (!inData[streamSource].empty()) {
32 inData[streamSource].read(inputWord);
33 outData.write(inputWord);
34 if (inputWord.last == 1)
35 mergeState = M_IDLE;
36 }
37 break;
38 }
39 }

本例体现的是模块合并功能的使用,其中一个流阵列作为输入(inData),一个单流作为输出(outData)。这个模块的功能是以无区别的方式从输入流读取数据,然后将读取的数据输出给输出流。该模块采用双级FSM实现,其结构与前文介绍的结构一致。

FSM的第一个状态用于确保选择输入流的无区别性(fairness)。实现的方法是使用循环算法检查队列。该算法在完成上一队列的访问之后,即从下一队列起查找新的数据。第17到19行的代码采用的即是此循环算法。常量NUM_MERGE_STREAMS用于设定待合并的流的数量。接下来的第20行负责测试当前的流,其内容用tempCntr变量标示。如果当前流非空,则将其设置为活跃流(第21行)。然后从该流中读取数据(第22行)。如果读取的数据字不是最后一个数据字(由第24行负责检查),则状态机进入M_STREAM状态,然后输出来自该流的剩余数据字。在处理完成最后一个数据字后,FSM返回M_IDLE状态,然后重复上述过程。

这个模块引入了一个新的编译指令,称为“array_partition”。该编译指令能让Vivado HLS了解为了提高吞吐量,是否需要把一个阵列拆分为多个子阵列。如果未加设定,Vivado HLS会使用双端口BRAM来访问阵列。如果要在一个时钟周期中访问阵列两次以上,如果不适当地提高初始化间隔(II)的值,该工具将无法调度这些访问。在本例中,略去array_partition编译指令,将NUN_MERGE_STREAMS值设为8,就可以让II=4。但因为想能够在每个时钟周期内访问steamEmpty阵列的所有元素,让目标II=1,我们需要对这个阵列进行充分分区。在本例中,该阵列实现为一组基于触发器的寄存器。

拆分输入流的过程耳熟能详,把来自一个流的数据字正确地路由到一个流阵列即可。

抽取字段和重新对齐字段
在包处理中,抽取字段和重新对齐字段是最基本的操作之一。由于数据包一般是经过多个时钟周期内通过总线到达模块的,常见的情况是需要的字段要么在它们抵达的数据字中未能对齐,要么分散在多个数据字中(往往两种情况都有)。因此要处理这些字段,必须将它们从数据流中抽取出来,存入缓存然后重新对齐以便处理。

例5:源MAC地址抽取示例
1 if (!inData.empty()) {
2 inData.read(currWord);
3 switch(wordCount) {
4 case 0:
5 MAC_DST = currWord.data.range(47, 0);
6 MAC_SRC.range(15, 0) = currWord.data.
range(63, 48);
7 break;
8 case 1:
9 MAC_SRC.range(47 ,16) = currWord.
data.range(31, 0);
10 break;
11 case 2:
12 ……

例5是一个非常简单的字段抽取和再对齐示例。这个示例从以太网报头中抽取源MAC地址。数据通过称为“inData”的64位流抵达。在每个时钟周期读入数据(第2行)。随后根据读取的数据字执行合适的语句。因此在第5行中源MAC地址的头16位被抽取出来,并移位到MAC_SRC变量的起始部分。在下一时钟周期中,MAC地址的其余32位抵达总线,然后存入MAC_SRC变量的32位更高位中。

用多级层级创建系统
上文讨论了如何使用Vivado HLS实现简单的三级流水线。但是一般的包处理系统可能会包含分布在层级结构中多个层面的多个模块。图2即是这种系统的示例。在本例中,层级结构的第一层由两个模块组成,每个模块下面包括三个子模块。这个示例中的顶层模块与前面介绍的简单系统中顶层模块相似。但包含有三个子模块的较低层模块使用INLINE编译指令来解析函数,将其子模块推送到顶层,如例6所示。

例6:Vivado HLS中的中间模块
1 void module2(stream&inData,
stream&outData) {
2 #pragma HLS INLINE
3
4 ………

因此在Vivado HLS完成综合后,系统基本如图3所示。这样Vivado HLS就能正确地根据这些模块创建数据流架构,完成模块的流水线化,然后同步执行。在嵌入该函数后,各模块和信号保持原来的名称不变。

使用高级语言结构
高层次综合的主要优势之一在于可以使用高级语言结构来表达复杂对象,与传统RTL设计相比,显著提高了抽象水平。下面的例子是描述一个小型查找表。

图2 - 两级层级设计实例

图3 - 解析成流水线化Vivado HLS设计的中间层级
例7中的代码用于内容可寻址存储器(CAM)类定义,它使用类对象创建一个表,供存储和恢复上述原型系统的ARP数据。该类有一个私有成员,这个私有成员是一个由“noOfArpTableEntries”条“arpTableEntry”类型记录组成的阵列。这种类型属于一种数据结构,包括MAC地址、对应的IP地址和用于说明该条记录是否包含有效数据的一个数位。

例7:CAM类定义
1 class cam {
2 private:
3 arpTableEntryfilterEntries[noOfArpTableEntries];
4 public:
5 cam();
6 boolwrite(arpTableEntrywriteEntry);
7 boolclear(ap_uint<32>clearAddress);
8 arpTableEntrycompare(ap_uint<32>
searchAddress);
9 };

这个类也包括四种在这个表上运算方法(其中一个是构造器)。其中的一个,即比较法,用于实现真正的查找功能。本例通过提供IP地址来返回相应的MAC地址。处理的方法是使用“for”循环查找表中的每一条记录,搜索有相同IP地址的有效记录。然后完整地返回这条记录。如果没有找到,就返回无效记录。为让设计实现II=1的目标,必须完全展开这个循环。

例8:用于CAM类的比较法
1 arpTableEntry cam::compare(ap_uint<32>searchAddress)
{
2 for (uint8_t i=0;i<noofarptableentries;++i) {<br=””> 3 if (this->filterEntries[i].valid == 1 &&
searchAddress == this->filterEntries[i].ipAddress)
4 return this->filterEntries[i];
5 }
6 arpTableEntry temp = {0, 0, 0};
7 return temp;
8 }

上述经验和示例明确说明,用户可以使用Vivado HLS充分发挥高级编程结构的作用,用类似软件的方法描述包处理系统。采用RTL是难以实现的。

10GBps速率下的协议处理
与传统RTL相比,Vivado HLS可使用C/C++在FPGA上迅速方便地实现协议处理设计,充分发挥高级语言带来的效率提升优势。另外还具有下列优点:使用C函数轻松完成系统构建;数据通过流交换,提供类似FIFO的标准化接口;灵活的流控制和HLS编译指令,便于使用该工具实现需要的架构。借助这些功能,用户无需重写源代码就能够迅速判研多种不同设计方案的利弊。

出于解释这类设计的基本概念的目的,上文讨论了一种能够应答ping和ARP请求,解析IP地址查询的简单ARP服务器。结果证明用Vivado HLS设计的模块能够以10Gbp乃至更高的线速完成协议处理。


发表评论

邮箱地址不会被公开。 必填项已用*标注