Vivado HLS推动协议处理系统蓬勃发展(下)
2015年10月30日 15:33 发布者:designapp
接上篇4 设置简单系统
协议处理一般情况下属于状态事务。必须先顺序读取在多个时钟周期内进入总线的数据包字,然后根据数据包的某些字段决定进一步操作。通常应对这种处理的方法是使用状态机,对数据包进行迭代运算,完成必要的处理。例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 enum dState {D_IDLE = 0, D_STREAM, D_
DROP} dropState;
5 axiWord currWord = {0, 0, 0, 0};
6
7 switch(dropState) {
8 case D_IDLE:
9 if (!validBuffer.empty() && !inData.empty()) {
10 ap_uint 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行的两个输入流读取。这两行分别代表两种流对象读取方法。这两种方法均从设定的流读取,然后将结果存储到给定变量中。这种方法采取阻塞式读取,意味着如果该方法调用无法顺序执行,就会暂停执行该函数调用中的其余代码。在试图读取空流的时候会发生这种情况。
5 流分割和合并
在协议处理中,根据协议栈特定字段转发数据包给不同模块,然后在发送前将不同的流重新组合,是一项关键功能。Vivado HLS允许使用高级架构来推动这一转发过程,具体如例4中所示的流合并。
例4:简单的流合并情况
1 void merge(stream inData[NUM_MERGE_
STREAMS], stream&outData) {
2 #pragma HLS INLINE off
3 #pragma HLS pipeline II=1 enable_flush
4
5 static enum mState{M_IDLE = 0, M_STREAM}
mergeState;
6 static ap_uint
rrCtr = 0;
7 static ap_uint
streamSource = 0;
8 axiWord inputWord = {0, 0, 0, 0};
9
10 switch(mergeState) {
11 case M_IDLE:
12 bool streamEmpty;
13 #pragma HLS ARRAY_PARTITION variable=stream-
Empty complete
14 for (uint8_t i=0;i
15 streamEmpty = inData.empty();
16 for (uint8_t i=0;i
17 uint8_t tempCtr = streamSource + 1 + i;
18 if (tempCtr >= NUM_MERGE_STREAMS)
19 tempCtr -= NUM_MERGE_STREAMS;
20 if(!streamEmpty) {
21 streamSource = tempCtr;
22 inputWord = inData.
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.empty()) {
32 inData.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,我们需要对这个阵列进行充分分区。在本例中,该阵列实现为一组基于触发器的寄存器。
拆分输入流的过程耳熟能详,把来自一个流的数据字正确地路由到一个流阵列即可。
6 抽取字段和重新对齐字段
在包处理中,抽取字段和重新对齐字段是最基本的操作之一。由于数据包一般是经过多个时钟周期内通过总线到达模块的,常见的情况是需要的字段要么在它们抵达的数据字中未能对齐,要么分散在多个数据字中(往往两种情况都有)。因此要处理这些字段,必须将它们从数据流中抽取出来,存入缓存然后重新对齐以便处理。
例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位更高位中。
7 用多级层级创建系统
上文讨论了如何使用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就能正确地根据这些模块创建数据流架构,完成模块的流水线化,然后同步执行。在嵌入该函数后,各模块和信号保持原来的名称不变。
8 使用高级语言结构
高层次综合的主要优势之一在于可以使用高级语言结构来表达复杂对象,与传统RTL设计相比,显著提高了抽象水平。下面的例子是描述一个小型查找表。
例7中的代码用于内容可寻址存储器(CAM)类定义,它使用类对象创建一个表,供存储和恢复上述原型系统的ARP数据。该类有一个私有成员,这个私有成员是一个由“noOfArpTableEntries”条“arpTableEntry”类型记录组成的阵列。这种类型属于一种数据结构,包括MAC地址、对应的IP地址和用于说明该条记录是否包含有效数据的一个数位。
例7:CAM类定义
1 class cam {
2 private:
3 arpTableEntry filterEntries;
4 public:
5 cam();
6 bool write(arpTableEntry writeEntry);
7 bool clear(ap_uint clearAddress);
8 arpTableEntry compare(ap_uint
searchAddress);
9 };
这个类也包括四种在这个表上运算方法(其中一个是构造器)。其中的一个,即比较法,用于实现真正的查找功能。本例通过提供IP地址来返回相应的MAC地址。处理的方法是使用“for”循环查找表中的每一条记录,搜索有相同IP地址的有效记录。然后完整地返回这条记录。如果没有找到,就返回无效记录。为让设计实现II=1的目标,必须完全展开这个循环。
例8:用于CAM类的比较法
1 arpTableEntry cam::compare(ap_uint searchAddress)
{
2 for (uint8_t i=0; i
3 if (this->filterEntries.valid == 1 &&
searchAddress == this->filterEntries.ipAddress)
4 return this->filterEntries;
5 }
6 arpTableEntry temp = {0, 0, 0};
7 return temp;
8 }
上述经验和示例明确说明,用户可以使用Vivado HLS充分发挥高级编程结构的作用,用类似软件的方法描述包处理系统。采用RTL是难以实现的。
9 10GBps速率下的协议处理
与传统RTL相比,Vivado HLS可使用C/C++在FPGA上迅速方便地实现协议处理设计,充分发挥高级语言带来的效率提升优势。另外还具有下列优点:使用C函数轻松完成系统构建;数据通过流交换,提供类似FIFO的标准化接口;灵活的流控制和HLS编译指令,便于使用该工具实现需要的架构。借助这些功能,用户无需重写源代码就能够迅速判研多种不同设计方案的利弊。
出于解释这类设计的基本概念的目的,上文讨论了一种能够应答ping和ARP请求,解析IP地址查询的简单ARP服务器。结果证明用Vivado HLS设计的模块能够以10Gbp乃至更高的线速完成协议处理。