Catapult SL高层次综合算法C工具的开发技巧
Pixelworks 司马苗
[摘要]
Mentor公司提供的Catapult SL开发工具,不仅是快速算法验证,也是模块级结构设计和分析的有力工具,能有效地缩短产品的上市时间。本文首先回顾Catapult 综合工具开发的基本流程,然后介绍Catapult SL基于类的开发技巧以及视频算法中遇到的Preload和Teardown控制的技巧,最后列举一些Catapult SL开发过程中节约时间的技巧。
一、概述
随着系统级芯片(SOC)设计复杂度的不断增加,电子系统级(ESL)的设计和验证方法学得到越来越广泛的应用。ESL设计是能够让SoC设计工程师以紧耦合方式开发、优化和验证复杂系统架构和嵌入式软件的一套方法学,它还提供下游寄存器传输级(RTL)实现和验证基础。已有许多世界领先的系统和半导体公司采用ESL设计,以便为他们创新的终端产品获得成功提供必需的先进功能性和高性能。Gartner Dataquest预测,ESL工具市场将在未来5年达到35.7%的年复合增长率,而目前ESL工具缺乏是“使寄存器传输级(RTL)工具销售保持增长的唯一因素”。而Mentor Graphics拥有ESL领域有全套的领先设计、仿真及实现解决方案(BridgePoint、Platform Express、Perspecta、Catapult SL Synthesis)可以帮助设计公司提高设计的抽象层次,缩短设计时间。
目前Catapult 综合工具是业内最成熟的能利用非定时的纯C++语言来产生高品质RTL 描述的算法综合工具,速度最快可达到传统人工方式的20倍,为缩短了产品的上市时间提供了可能。它支持设计人员面向各种微结构和接口设计实施详细的What-if分析,进而得到比较优化的硬件设计,并可以复用算法开发时的C++ Testbench来验证生成的RTL代码,从而保证原算法与RTL代码功能的一致性,减少人工引入错误的可能性。
逐点半导体(Pixelworks)主要致力于图像和视频处理芯片的开发,是显示器,电视机,投影仪等消费电子产品的芯片的重要供应商。我们通过使用Catapult工具来加速算法到芯片的开发过程,提高设计的可靠性。
二、设计流程
传统的ASIC设计流程比较复杂,流程中的每一步都需要详细的文档工作和复杂的验证,以保证设计正确。算法工程师,结构工程师和RTL工程师之间的反复比较多,而且如果算法工程师,结构工程师和RTL工程师对结构或者算法的理解有误差的话,会造成RTL设计上的
缺陷或失误。
为了减少设计的反复,缩短产品的上市时间,可以采用Mentor的Catapult SL设计流程如图1所示,直接由定点的C++代码综合出可综合的RTL代码。该流程的好处是我们结构工程师和算法工程师不必等RTL工程师写完RTL代码后再进行算法的验证和优化,并且可以节省大量算法工程师,结构工程师与RTL设计工程师之间反复沟通算法设计的时间。
使用Catapult SL开发环境,可以方便的把基于C++的算法加入的项目之中,选择适当的芯片设置,定义不同微架构方案(流水、并行、串行),评估不同方案使用的资源情况,最后生成RTL和验证环境,在ModelSim中进行仿真。开发环境支持对多个方案对比,以便开发人员选择最优的方案。对于验证成功的RTL代码环可以进一步调用Mentor的工具进行FPGA综合或者采用Magama /Synopsys /Cadence 综合工具进行ASIC综合。
图1. Catapult SL的ASIC设计流程
三、常用的设计技巧
Catapult SL开发工具最初作为基于C的算法向RTL代码转换和验证的工具引入。对于算法工程师而言,Catapult开发工具可以方便地用于通信算法,图像处理算法、音频算法和视频算法的开发。而对于系统工程师或者架构工程师而言,Catapult工具也可以用于视频芯片中模块级结构的设计和开发。
这一节主要介绍Catapult SL提供的开发技巧:基于类和模板(Template)的编程方式,定义寄存器,分配存储空间,输入输出,隐藏Preload和Teardown控制,以及使用RTL风格的C代码。
3.1基于类和模板的编程方式
基于类和模板的编程有许多优势。首先,从结构设计的角度基于类的编程可以使得硬件结构变得清晰,比如可以把Memory以及和Memory紧密结合的逻辑放在一起。其次,使用类和模板使得代码可以模块化重复利用。
下面结合视频解码中运动补偿的例子来说明。在进行视频解码的运动补偿时我们需要前向参考块和后向参考块,每个参考块又有亮度和两个色度分量,一共六个模块,只需通过定义一个模板就可以实现。
//example for class and template
01: template
02: class reference_block
03: {
04: public:
05: int ref_blocks[IN_HEIGHT][IN_WIDTH];
06: int intermediate[2][OUT_WIDTH+1];
07: void input(...);
08: void do_mc(int out[OUT_WIDTH])
9: {
10: get_pixels();
11: biliear(out);
12: }
13: void get_pixels(...);
14: void biliear(int out[OUT_WIDTH]);
15: }
16: void top_mc()
17: {
18: static reference_block<16,16,8,8> ref_y_forw;
19: static reference_block<16,16,8,8> ref_y_back;
20: static reference_block< 8, 8,4,4> ref_u_forw;
21: static reference_block< 8, 8,4,4> ref_u_back;
22: static reference_block< 8, 8,4,4> ref_v_forw;
23: static reference_block< 8, 8,4,4> ref_v_back;
24: ...
25: int y_forw[8];
26: int y_back[8];
27: int u_forw[4];
28: int u_back[4];
29: int v_forw[4];
30: int v_back[4];
31: ...
32: for (...)
33: for(...)
34: { ...
35: ref_y_forw.do_mc(y_forw);
36: ref_y_back.do_mc(y_back);
37: ref_u_forw.do_mc(u_forw);
38: ref_u_back.do_mc(u_back);
39: ref_v_forw.do_mc(v_forw);
40: ref_v_back.do_mc(v_back);
41: ...
42: }
43: }
图2. 基于类和模板的编程方式
上面图2第1行到第15行定义了一个叫做reference_block的模板,他的功能是输入4个块,放入内部的SRAM之中,然后根据半像素精度的运动适量从中2行乘以一个块宽度加1个像素,然后用这些像素作双线性插值得到1乘以块宽度个像素进行输出。在这个模板中,形成运动补偿的参考块的双线性插值算法,和它所使用的数据:输入的4个块,中间结果OUT_WIDTH+1封装在一起,完成所需的任务。
在18到23行生成了6个对象,分别是YUV分量前向后向参考块,对象的定义是静态的保证每个对象成员变量也是静态的,Catapult工具会把其中的数组映射成寄存器或者SRAM。
而25到30定义的数组最后映射成寄存器可用来保存结果。参考块可以用来和残差信号相加的到最终的解码结果。使用类似的方法使用类把算法实现的结构搭建起来。
3.2 使用类的成员变量数组定义寄存器
在使用模板 和类定义一组算法或者模块的结构时,需要考虑问题是在那里存贮数据,数据的格式是什么样子的,和算法之间有何联系? 一般而言可以考虑下面4个步骤: ?算法的所有输入首先保存在input buffer之中;
?在运算之前把当前要用的数据从input buffer 之中取出来放入寄存器,也就是intermediate buffer 之中;
?进行计算;
?计算结果直接输出。
一般输入缓冲区和中间结果缓冲区定义在类或者模板之中。而最终结果(特别是数组),定义于在主函数之中,便于子函数之间交换数据。一般而言类中的数组和主函数中的数组都可以映射成寄存器或者SRAM,比如图2中第5,6,18到23行定义的数组都可以映射为寄存器或者SRAM。
需要注意的是,中间变量数组最好不要象图2的第13行那样定义在类的成员函数中。在类例化之后,各个对象的成员变量是相互独立的,可以生成不同的SRAM,而成员函数定义的数组有可能被Catapult工具认为是多个对象共用一个SRAM,从而导致数据混乱。
3.3接口综合以及break都需要明确的条件
Catapult开发工具提供了丰富的接口资源,如连线型,带使能端的、带硬件握手的、SPRAM,DPRAM以及总线接口和Streaming(数据流)接口。我们为了节省面积,减少对Memory 的需求,采用了Catapult 提供的Streaming接口,如图3所示。在对基于Streaming的输
入和输出进行操作的时候,需要加上相应的约束条件以保证数据的正确性。
in out
out in 例如下面图4的例子中第15和20行分别为输入和输出增加了约束条件。这样才能反映输入时preload 和输出时teardown 的情况。避免出现输入输出Streaming 溢出的情况。 // example for conditional read write and break
01: #define MAX_OUT 20
02: #define MAX_INN 200
03: void top_level_function(int in_a[1000], int in_b[1000], int out_c[1000])
04: {
05: int loop_outer, loop_inner;
06:
07: for( loop_outer = 0; loop_outer < MAX_OUT; loop_outer++)
08: {
09: if(loop_outer>=11) break;
10: for( loop_inner = 0; loop_inner < MAX_INN; loop_inner++)
11: {
12: if(loop_inner >=103) break;
13: if((loop_inner < 100)&&(loop_outer<10))
14: {
15: tmp_a = *in_a++;
16: tmp_b = *in_b++;
17: ...
18: }
19:
20: if((loop_inner>= 3 && loop_inner <103) && (loop_outer>= 1 && loop_outer <11) 21: {
22: *out_c++ = tmp_out;
23: }
24: }
25: }
26: }
3.3 Break 语法的支持 Catapult 支持break 语句,并可利用break 来减少流水线中冗余周期,如图5所示。 图3. Streaming 接口示意
图4. Streaming 接口及break 语句的支持
//break to remove the redudancy cycle
1: void test(int16 a[NUM], int16 b[NUM], int32 dout[NUM],uint10 ctrl)
//set the constrains to the test function with pipeline initialization interval =1
2: {
3: ILOOP:for(int i=0;i < ctrl;i++)
4: {
5: dout[i] = a[i] * b[i];
6: }
7: }
8:void test(int16 a[NUM], int16 b[NUM], int32 dout[NUM],uint10 ctrl)
9: {
10: ILOOP:for(int i=0; i 11: { 12: dout[i] = a[i] * b[i]; 13:if(i>= ctrl) 14: break; 15: } 16:} 冗余周期 图5. Break消除冗余周期 此外使用break语句控制两层循环的次数时,需要注意的是break语句执行的条件和输出条件必须是明确互斥的,不能有”交集”。例如图4中不能因为在第12行有 “if(loop_inner >=103) break;”就去掉第20行中loop_inner <103 的限制,同理不能因为第9行 “if(loop_outer>=11) break;”而省略第20行中“loop_outer <11”。这样才可避免Catapult 工具生成的RTL和原始的C++代码仿真的不匹配。 3.4 Preload和Teardown隐藏在主程序之外 在图4的例子中13行和20行分别控制了Preload和Teardown过程。但是下列情况下想利用13行和20行这样简单方式控制Preload和Teardown会十分困难: ?有复杂的信号控制Preload和Teardown,比如在视频算法中行同步和场同步信号参与Preload和Teardown; ?在循环里有多个子函数需要执行,而他们Preload和Teardown的条件不同; ?希望把主函数中的循环次数变为形式上的循环次数,真正的循环次数由参数控制。 //example for hidden preload and teardown 01: #define MAX_OUT 20 02: #define MAX_INN 200 03: class timing_control 04: { 05: public: 06: int inner_max; 07: int outer_max; 08: int load_enable; 09: int save_enable; 10: int fun1_enable; 11: int fun2_enable; 12: int inner_break; 13: int outer_break; 14: void init(int _inner_max, int _outer_max) 15: { 16: inner_max = _inner_max; 17: outer_max = _outer_max; 18: load_enable = false; 19: save_enable = false; 20: fun1_enable = false; 21: fun2_enable = false; 22: inner_break = false; 23: outer_break = false; 24: } 25: void check(int cur_inner, int cur_outer, int vsync, int hsync) 26: { 27: if((loop_inner < 100)&&(loop_outer<10)) 28: { load_enable =true ; } 29: else 30: { load_enable = false; } 31: 32: if((loop_inner>= 1 && loop_inner <101) && (loop_outer>= 0 && loop_outer <10) 33: { fun1_enable =true ; } 34: else 35: { fun1_enable = false; } 36: 37: if((loop_inner>= 2 && loop_inner <102) && (loop_outer>= 1 && loop_outer <11) 38: { fun2_enable =true ; } 39: else 40: { fun2_enable = false; } 41: 42: 43: if((loop_inner>= 3 && loop_inner <103) && (loop_outer>= 1 && loop_outer <11) 44: { load_enable =true ; } 45: else 46: { load_enable = false; } 47: 48: if(loop_inner >=103 && hsync) 49: {inner_break = true;} 50: else 51: {inner_break = false;} 52: 53: if(loop_out>=11 && vsync) break; 54: {outer_break = true;} 55: else 56: {outer_break = false;} 57: } 58: } 59: void fun1(...) 60: { 61: ..... 62: } 63: void fun2(...) 64: { 65: ..... 66: } 67: 68: void top_level_function(int in_a[1000], int in_b[1000], int out_c[1000], int hsync, int vsync) 69: { 70: int loop_outer, loop_inner; 71: static timing_control timing_controller; 72: timing_controller.init(100, 10); 73: 74: for( loop_outer = 0; loop_outer < MAX_OUT; loop_outer++) 75: { 76: if(timing_controller.outer_break) break; 77: for( loop_inner = 0; loop_inner < MAX_INN; loop_inner++) 78: { 79: timing_controller.check(loop_inner, loop_outer, vsync, hsync) ; 80: if(timing_controller.inner_break) break; 81: if(timing_controller.load_enable) 82: { 83: tmp_a = *in_a++; 84: tmp_b = *in_b++; 85: ...... 86: } 87: if(timing_controller.fun1_enable) 88: { 89: fun1(......); 90: } 91: if(timing_controller.fun2_enable) 92: { 93: fun2(......); 94: } 95: if(timing_controller.save_enable) 96: { 97: *out_c++ = tmp_out; 98: } 99: } 100: } 101:} 图6. 隐藏Preload和Teardown 在上面图6的例子里使用timing_controller这个module为主函数中所有的读写和函数调用产生enable信号,这样主函数的框架变得清晰明了,当某一函数的enable信号的生成机制改变的时候也不需要对主函数进行修改。每一个函数是否执行仅仅和自己的enable 有关而与其他信号无关,便于Debug和对生成的RTL代码进行优化。在timing_controller 可以使用非常复杂的enable信号生成算法而不会把主函数变得非常复杂,难于调试。此时,主函数的循环只是引入了clock信号而已,其他都由timing_controller控制。 3.5可综合的C++代码 正如传统的编写RTL代码一样,综合结果的优劣取决于RTL代码的编写风格,同样为了使Catapult SL生成高质量的RTL代码。我们采用以下几种方法来优化我们的C++代码。 ?避免使用双变量乘法 01: // 1/4 pixel linear weight 02: void fun1(int in_a, int in_b, int weight, int &out_c) 03: { 04: out_c = (in_a * weight + ( 4 - weight) * in_b + 2)/4 ; 05: } 06: void fun2(int in_a, int in_b, int weight, int &out_c) 07: { 08: if (weight == 0) 09: { out_c = in_b; } 10: if (weight == 1) 11: { out_c = (in_a + 3 * in_b + 2)/4 ; } 12: if (weight == 2) 13: { out_c = (in_a + in_b + 1)/2 ; } 14: if (weight == 3) 15: { out_c = (in_b + 3 * in_a + 2)/4 ; } 16: if (weight == 4) 17: { out_c = in_a; } 18: } 19: void fun_top(....) 20: { 21: int a[8]; 22: int b[8]; 23: int c[8]; 24: ... 25: for( int outer = 0; outer < MAX_LOOP; outer++) 26: { 27: ... 28: for( int i = 0; i<8; i++) 29: { 30: // difficalt to pass RTL verification 31: //fun1(a[i], b[i], weight, c[i]); 32: 33: // easy to pass RTL verification 34: fun2(a[i], b[i], weight, c[i]); 35: } 36: ... 37: } 38: } 39: 图7. 避免使用双变量乘法 例如上面图7第2行的fun1和第6行的fun2实现一样的功能四分之一像素精度的运动补偿,fun1中乘法器两侧都是变量而且一共有2x8个乘法操作,生成RTL代码和验证的时 间较长,而改为fun2的写法之后可以很快地生成RTL代码,而且C++代码的功能和生成的 RTL代码的功能能很快地通过验证。 ?减少嵌套的层次 在上面第8行到第17行的if语句都是并行的如果改成下图8面写法会降低RTL性能。 08: if (weight == 0) 09: { out_c = in_b; } 10: else if (weight == 1) 11: { out_c = (in_a + 3 * in_b + 2)/4 ; } 12: else if (weight == 2) 13: { out_c = (in_a + in_b + 1)/2 ; } 14: else if (weight == 3) 15: { out_c = (in_b + 3 * in_a + 2)/4 ; } 16: else if (weight == 4) 17: { out_c = in_a; } 图8. 减少嵌套的层次尽量并行执行 ?增加中间变量,避免反馈 在下面图9例子里4行到11行形成了把sum和一个数相加在反馈给sum的逻辑,Catapult SL产生的RTL代码会有同样的反馈回路,关于sum的电路迟延会加大。而使用19行到21行的实现方法生成的RTL性能更好。 01: void fun1(int a[8], int &sum) 02: { 03: sum = 0; 04: sum += a[0]; 05: sum += a[1]; 06: sum += a[2]; 07: sum += a[3]; 08: sum += a[4]; 09: sum += a[5]; 10: sum += a[6]; 11: sum += a[7]; 12: 13: } 14: 15: void fun2(int a[8], int &sum) 16: { 17: int tmp03, tmp47; 18: 19: tmp03 = a[0] + a[1] + a[2] + a[3]; 20: tmp03 = a[4] + a[5] + a[6] + a[7]; 21: sum = tmp03 + tmp47; 22: } 图9. 增加中间变量,避免反馈 四、节约时间的技巧 在综合比较大的算法或者结构设计时,综合时间成为一个重要的问题,无论算法工程师还是结构工程师都希望尽快得到综合结果。为了节约时间可以从以下三方面入手。 4.1充分调试C++代码 ?算法输出必须正确,满足设计要求; ?没有语法错误,符合Catapult的格式要求; ?没有内存越界和泄露,streaming的大小或者说定义这个streaming的数组的大小必须大于等于对这个streaming访问时涉及到的范围; ?如果C++代码是在visual C++环境下编译执行的,要保证C++代码在Linux 编译工具下也可以正确的编译执行并且产生正确的结果。 4.2使用递增的实现方法 如果设计比较复杂,如果直接根据最终输出的错误进行调试会比较困难,可以在一些中间结果上设置“Probe”输出。例如原始的代码如图10所示,第一步首先验证Sub_fun_1 的输出是否正确,然后验证第二步Sub_fun_2的输出是否正确,在某一步出现错误时可以有针对性地进行Debug。 // original code and final code Void sub_fun_1(int in_a, int in_b, int &out_c); Void sub_fun_2(int in_a, int in_b, int &out_c); Void sub_fun_3(int in_a, int in_b, int &out_c); Void Main_function(int a[1000], int b[1000], int c[1000]) { int I; int t1, t2, t3, t4, t5; for(I = 0; i< 1000; i++) { T1 = a[i]; T2 = b[i]; Sub_fun_1(t1, t2, t3); Sub_fun_2(t3, t2, t4); Sub_fun_3(t3, t4, t5); *c++ = t5; } } 图10. 原始代码 // step 1 debug code Void sub_fun_1(int in_a, int in_b, int &out_c); Void sub_fun_2(int in_a, int in_b, int &out_c); Void sub_fun_3(int in_a, int in_b, int &out_c); Void Main_function(int a[1000], int b[1000], int c[1000]) { int I; int t1, t2, t3, t4, t5; for(I = 0; i< 1000; i++) { T1 = a[i]; T2 = b[i]; Sub_fun_1(t1, t2, t3); *c++ = t3; Sub_fun_2(t3, t2, t4); Sub_fun_3(t3, t4, t5); //*c++ = t5; } } 图11. 第一步调试 // step 2 debug code Void sub_fun_1(int in_a, int in_b, int &out_c); Void sub_fun_2(int in_a, int in_b, int &out_c); Void sub_fun_3(int in_a, int in_b, int &out_c); Void Main_function(int a[1000], int b[1000], int c[1000]) { int I; int t1, t2, t3, t4, t5; for(I = 0; i< 1000; i++) { T1 = a[i]; T2 = b[i]; Sub_fun_1(t1, t2, t3); //*c++ = t3; Sub_fun_2(t3, t2, t4); *c++ = t4; Sub_fun_3(t3, t4, t5); //*c++ = t5; } } 图12. 第二步调试 4.3简化约束条件 为了快速验证可以快速地验证C++算法实现是否正确,可以简化约束,从低频率开始实现。 当算法功能正确后,我们可以进一步地优化时序,由于在整个实现的过程中,生成RTL 的时间约占整个时间的50%左右,我们可以先针对不同约束进行调度,等得到满足时序/面积的方案后再最终生成RTL代码。 五、结论 Catapult SL开发工具最初用于帮助算法工程师把算法快速地转化为RTL代码。Catapult SL开发工具倾向于帮助算法工程师完成结构上的优化,并且把算法的各个部分放在一起以减少冗余。这样的策略对于一维的算法比较有效。但对于图像算法和视频算法而言,数据之间的相互依赖关系比较复杂,需要算法工程师和结构工程师更多的参与到对设计的RTL层次结构的初步规划。 我们认为Catapult SL开发工具是有效的算法到RTL实现和验证的工具,同时也是非常强大的结构分析和优化工具。采用了上文介绍的开发技巧后,对于我们产品的上市时间的缩短起到了良好的作用。