原文:The Challenges of Synthesizing Hardware from C-Like Languages.
我们之所以选择将C语言作为硬件综合的目标,主要原因是大家对C语言已经比较熟悉了。人们也认为通过这种方式,我们很容易实现硬件-软件的协同设计。然而本文认为纯C语言并不是一个面向硬件编程的合适的语言,硬件的一大优势是并行性,但是C不提供原生的并行编程支持,因此综合器要么自动挖掘并行性,要么允许使用语言扩展来显式地定义并行性。本文的主要观点是,高效的硬件设计通常很难用传统的C语言来描述,就算真的可以用C语言来编程,其语义上也和软件上的C几乎没有相似之处。
本文主要讨论使用类C语言进行硬件综合,不讨论验证和算法设计等内容。对后者而言,类C语言(特别是SystemC和其他变体)已经在广泛使用。
类C硬件综合语言
上表列出了1980年代后期以来提出的一些类C硬件综合语言,如下面的代码是Cones语言的一个例子,函数中的内容会被翻译为组合电路。
INPUTS: IN[5];
OUTPUT: OUT[3];
rd53()
{
int count, i;
count = 0;
for (i=0 ; i<5 ; i++)
if (IN[i] == 1)
count = count + 1;
for (i=0 ; i<3 ; i++) {
OUT[i] = count & 0x01;
count = count >> 1;
}
}
下面的代码是HardwareC中的最大公约数算法,HardwareC是一种行为硬件语言。
#define SIZE 8
process gcd (xi, yi, rst, ou)
in port xi[SIZE], yi[SIZE];
in port rst;
out port ou[SIZE];
{
boolean x[SIZE], y[SIZE];
write ou = 0;
if ( rst ) <
x = read(xi);
y = read(yi);
>
if ((x != 0) & (y != 0))
repeat {
while (x >= y)
x = x – y;
<
x = y; /* swap x and y */
y = x;
>
} until (y == 0);
else
x = 0;
write ou = x;
}
SystemC是一种支持硬件和系统建模的C++变体,通过C++的类来对层次结构进行建模,并通过组合和时序过程来描述硬件,下面的代码是SystemC描述的硬件,功能是两位十进制数字到七段数码管的解码器,解码器产生组合逻辑,计数器产生时序逻辑。
#include “systemc.h”
#include <stdio.h>
struct decoder : sc_module {
sc_in<sc_uint<4> > number;
sc_out<sc_bv<7> > segments;
void compute() {
static sc_bv<7> codes[10] = {
0x7e, 0x30, 0x6d, 0x79, 0x33,
0x5b, 0x5f, 0x70, 0x7f, 0x7b };
if (number.read() < 10)
segments = codes[number.read()];
}
SC_CTOR(decoder) {
SC_METHOD(compute);
sensitive << number;
}
};
struct counter : sc_module {
sc_out<sc_uint<4> > tens;
sc_out<sc_uint<4> > ones;
sc_in_clk clk;
void tick() {
int one = 0, ten = 0;
for (;;) {
if (++one == 10) {
one = 0;
if (++ten == 10) ten = 0;
}
ones = one;
tens = ten;
wait();
}
}
SC_CTOR(counter) {
SC_CTHREAD(tick, clk.pos());
}
};
SpecC语言则是ANSI C的超集,增加了许多系统和硬件建模结构,包括FSM、并发、流水线等,下面的代码展示了SpecC的可综合RTL语言描述的状态机,其中wait(clk)
表示时钟周期边界。
behavior even(
in event clk,
in unsigned bit[1] rst,
in bit[31:0] Inport,
out bit[31:0] Outport,
in bit[1] Start,
out bit[1] Done,
out bit[31:0] idata,
in bit[31:0] iocount,
out bit[1] istart,
in bit[1] idone,
in bit[1] ack_istart,
out bit[1] ack_idone)
{
void main(void) {
bit[31:0] ocount;
bit[31:0] mask;
enum state { S0, S1, S2, S3 } state;
state = S0;
while (1) {
wait(clk);
if (rst == 1b) state = S0;
switch (state) {
case S0:
Done = 0b;
istart = 0b;
ack_idone = 0b;
if (Start == 1b) state = S1;
else state = S0;
break;
case S1:
mask = 0x0001;
idata = Inport;
istart = 1b;
if (ack_istart == 1b)
state = S2;
else state = S1;
break;
case S2:
istart = 0b;
ocount = iocount;
if (idone == 1b) state = S3;
else state = S2;
break;
case S3:
Outport = ocount & mask;
ack_idone = 1b;
Done = 1b;
if (idone == 0) state = S0;
else state = S3;
break;
}
}
}
};
并发
硬件和软件最大的区别在于执行模型,软件遵循冯诺依曼结构的基于内存的顺序执行模型,而硬件则从本质上来说就是并发的。并发编程仍然很困难,一方面是因为难度较大,另外一方面则是关于并行编程模型的分歧,如选择共享内存还是消息传递。
在顺序代码中挖掘并行性有三种主要方法,分别为指令级并行(已经在乱序超标量处理器中广泛应用)、流水线(会涉及到数据与控制依赖的问题)、进程级并行(取决于算法,且很难自动识别,需要程序员手动控制)。
有两种方法在C中实现并发。第一种方法在语言中添加并行结构,让程序员手动控制,另一种方法是让编译器自动识别并行性。这两种方法有各自的缺点,后者为编译器开发增加了难度,前者则要求程序员以一种全新的思维方式去编程,一个好的硬件规范语言必须能够有效表达并行算法。
时序
C语言中很难实现时序约束,也没有规定每条指令需要的时间,但为了实现硬件设计的性能目标,我们需要合适的指定并实现时序约束的机制。下面的代码就很难说明这个循环需要多少周期,不同的语言对此的解释并不一致。
for (i = 0 ; i < 8 ; i++) {
a[i] = c[i];
b[i] = d[i] || f[i];
}
编译器使用各种技术插入时钟周期边界,手动或自动。一个好的硬件综合语言需要可以明确或通过约束来指定时序,但也不应该要求程序员提供太多细节。
数据类型
数据类型是硬件和软件语言之间的另一个主要区别。C几乎不支持小于一个字节的类型,因此纯C的代码很容易被翻译成不必要的硬件。
编译器采用三种方法将硬件类型引入C程序。第一种是允许在语言之外调整数据的宽度,第二种是在C中添加硬件类型,基于C++语言的第三种方法是通过C++的类型系统提供类似硬件的类型。一个好的硬件语言需要一个允许精确定义硬件类型的类型系统,C++相比C显得更合适。
通信
类C语言建立在非常灵活的内存通信模型之上,隐含地将所有内存位置视为访问成本相同,但在现代内存层次结构中并非如此。设计人员通常可以预测这些存储器的行为并更有效地使用,但非常困难,且类C语言对内存访问提供的辅助支持很少。为了避免过长且无法预测的通信延迟,硬件设计人员根据系统的需要使用各种机制,从简单的线路到复杂的通信协议。
指针的问题
由于指针的不确定性,软件中的通信模式通常很难事先确定,指针别名也是一个很严重的问题。尽管可以使用指针分析来估计C程序的通信模式,但是精确的分析是不可能的,只能采取保守的方法,也会导致额外不必要的开销。
内存与通信
我们可以把类C语言大致分为两组。第一组忽略C的内存模型,不支持数组和指针,只关注局部变量;第二组则尽可能实现C的所有数据类型,支持struct、数组等多种存储方式。
硬件和软件在这方面的设计差异也是巨大的。软件的通信方式是面向事务的,但硬件则是通过物理上的连线的。软件设计者通常会忽略内存访问的模式,但硬件设计者一开始就要详细描述每个通信的通道,并最小化通信的开销。
元数据
由于硬件的层次远低于软件,有更多方法可以在硬件中实现特定的C结构。例如加法运算,CPU可能只有一条有用的加法指令,而在硬件中却有大量不同的加法器结构。因此,硬件的翻译过程比软件的翻译需要做出更多的决定,此外,正确的决策因设计约束而异,期望所有这些决策都是自动化的仍然是不现实的。
尽管我们似乎可以使用C++的运算符重载机制来手动指定各种约束,但这种机制的实现可能仍然非常困难。C++的重载机制是使用参数类型来定义的,但硬件中算法的选择通常由资源限制决定。
我们可以采用两种方法来指定各种元数据:放置在程序中(注释、pragma、添加的结构等)或者程序之外(文本文件、GUI等)。一个好的硬件规范语言需要一种方法来指导综合过程在不同的实现中进行选择,在功率和速度之间进行权衡。