L11 Hardware Synthesis in Bluespec
MIT 6.004 2019 L11 Hardware Synthesis in Bluespec,由Silvina Hanono Wachman讲述。
这次讲座的主题为:Bluespec 中的硬件综合
主要内容
- 防护接口(Guarded Interfaces):介绍了如何使用防护接口表达时序电路。每个方法都有一个防护(即就绪信号),且每个动作的方法还会有一个使能信号,使得只有在就绪信号为真时,相应的动作才能被执行。同时,每种方法还可能包括输入和输出数据。
- 硬件综合(Hardware Synthesis):讨论了如何将Bluespec语言定义的模块转换为实际硬件。介绍了多个模块和实例,特别是关于GCD(最大公约数)计算的模块,如何实现,以及如何通过引入更多硬件来提高计算吞吐量。
- 高吞吐量GCD模块:介绍了一个改进的GCD模块,它内部使用两个基本的GCD模块来加速计算过程,实现了对GCD计算的并行处理,从而提高了处理速度。
- 电路合成实例:通过一元FIFO电路的例子详细解释了如何从Bluespec模块定义中合成硬件电路,包括如何处理各种信号和模块间的连接。
- 冲突矩阵(Conflict Matrices):介绍了如何使用冲突矩阵来分析并确保在电路设计中不同部分之间不会发生冲突,特别是在多个操作可能同时发生时如何处理这些操作的冲突问题。
分页知识点
防护接口
防护接口是表示时序电路的一种新方式,其中:
- 每个方法都有一个防护信号(
rdy
线),是其准备好执行的标志。 - 如果方法的返回值是有意义的,那只有在其防护信号为真时才成立。
- 每个动作方法都有一个使能信号(
en
线),仅当其防护信号为真时,该方法才能被激活(可以被设为真)。 - 每种方法都可以有输入数据。
- 值方法(Value methods)和动作值方法(ActionValue methods)可以有输出数据。
- 注意,en和rdy线是隐含的。
例如,对于一个FIFO(先进先出队列)接口,其定义如下:
interface Fifo#(numeric type size, type t);
method Action enq(t x); // 入队方法,有使能信号 'en'
method Action deq; // 出队方法
method t first; // 查看队首元素的值方法
endinterface
注意,入队和出队方法有关联的使能信号和就绪信号,而查看队首元素的值方法只有输出数据,因为它不改变状态,故没有使能信号。
GCD 接口
对于GCD(最大公约数)计算的模块,其接口和模块定义如下:
interface GCD;
method Action start(Bit#(32) a, Bit#(32) b); // 启动GCD计算的方法
method ActionValue#(Bit#(32)) getResult; // 获取GCD计算结果的方法
endinterface
module mkGCD (GCD);
// 定义寄存器用于存储GCD计算的中间状态和标志是否忙
Reg#(Bit#(32)) x <- mkReg(0); // 32位宽度的寄存器x,初值为0
Reg#(Bit#(32)) y <- mkReg(0); // 32位宽度的寄存器y,初值为0
Reg#(Bool) busy_flag <- mkReg(False); // 表示模块是否忙碌的布尔寄存器,初值为False
// 定义一个规则用于执行GCD计算
rule gcd;
if (x > y) begin x <= x - y; end // 如果x大于y,则x减去y
else if (x != 0) begin x <= y; y <= x; end // 如果x不为0,交换x和y
endrule
// 定义启动GCD计算的方法
method Action start(Bit#(32) a, Bit#(32) b) if(!busy_flag);
x <= a; y <= b; busy_flag <= True; // 将输入赋给x和y,并设置忙碌标志
endmethod
// 定义获取GCD结果的方法
method ActionValue#(Bit#(32)) getResult if(busy_flag && (x==0));
busy_flag <= False; return y;
// 如果计算完成(x为0且忙碌标志为真),返回y的值,并重置忙碌标志
endmethod
endmodule
在这个模块中,我们有两个寄存器x
和y
用于存储进行GCD计算的值,还有一个布尔寄存器busy_flag
用来表示模块是否正在进行计算。规则gcd
定义了GCD计算的实际步骤。start
方法启动计算,它接收两个32位宽度的输入参数,将它们存入寄存器x
和y
,并设置忙碌标志busy_flag
。getResult
方法在GCD计算完成后被调用,它检查忙碌标志和x
的值,如果条件满足则返回结果并清除忙碌标志。此模块展示了如何利用Bluespec编程语言定义时序电路的典型方式。
抽象的力量:另一种GCD实现
这是一个具有相同接口但具有双倍吞吐量的GCD模块,它并行使用两个gcd模块:
start
方法通过turnI
寄存器决定将输入转向哪个gcd模块,然后turnI
会翻转。- 类似地,
getResult
方法使用turnO
寄存器从适当的gcd模块获取输出,然后turnO
会翻转。
interface GCD;
method Action start(Bit#(32) a, Bit#(32) b); // 启动GCD计算的方法
method ActionValue#(Bit#(32)) getResult; // 获取GCD计算结果的方法
endinterface
高吞吐量GCD代码
module mkMultiGCD (GCD);
GCD gcd1 <- mkGCD(); // 实例化第一个GCD模块
GCD gcd2 <- mkGCD(); // 实例化第二个GCD模块
// 定义寄存器以跟踪当前该哪个GCD模块处理输入和输出
Reg#(Bool) turnI <- mkReg(False); // 输入轮换标志,默认为False
Reg#(Bool) turnO <- mkReg(False); // 输出轮换标志,默认为False
// 定义start方法,根据turnI的值决定输入发送到哪个GCD模块
method Action start(Bit#(32) a, Bit#(32) b);
if (turnI) gcd1.start(a,b); // 如果turnI为真,启动gcd1
else gcd2.start(a,b); // 否则启动gcd2
turnI <= !turnI; // 翻转输入轮换标志
endmethod
// 定义getResult方法,根据turnO的值从正确的GCD模块获取结果
method ActionValue#(Bit#(32)) getResult;
Bit#(32) y;
if (turnO) y <- gcd1.getResult; // 如果turnO为真,从gcd1获取结果
else y <- gcd2.getResult; // 否则从gcd2获取结果
turnO <= !turnO; // 翻转输出轮换标志
return y; // 返回获取的结果
endmethod
endmodule
在这个模块中,有两个GCD计算实例,gcd1
和 gcd2
,它们可以并行工作。turnI
和 turnO
寄存器用来确定哪个GCD模块将被用来处理下一个输入或产生输出。这通过两个方法 start
和 getResult
来控制。每次调用 start
或 getResult
,相关的轮换标志都会翻转,以确保负载在两个GCD模块之间平衡地分配。这种方法可以提高模块处理请求的能力,相比于单一的GCD实现,它能在相同的时间内处理更多的GCD计算请求。
这种并行工作的流程意在提高计算的吞吐量,这是通过同时使用两个GCD模块来实现的。每个GCD模块都能独立处理一组输入数据并计算最大公约数。通过在两个模块间分配工作,可以减少等待时间,从而在单位时间内完成更多的计算。
在单独的两个GCD模块的情况下,如果要进行两个独立的GCD计算,那么每个模块都会独立地处理一个计算。这意味着它们之间没有直接的协作,也没有共享任何状态或工作负载的机制。因此,如果一个模块完成了它的计算,而另一个仍在处理中,就不会有任何机制来利用已经空闲的模块来开始新的计算任务。
然而,在并行工作的GCD模块中,引入了
turnI
和turnO
这样的机制,这使得在两个GCD模块间动态分配计算成为可能。当一个计算请求到来时,start
方法会检查turnI
的状态,以确定将任务分配给哪个GCD模块。这样,如果一个模块正在忙于计算,新的请求可以立即转到另一个模块,而不是等待。同样的,当请求结果时,getResult
方法会检查turnO
来确定从哪个GCD模块获取结果,确保输出请求是平衡的。这种并行工作的意义在于:
- 提高效率:通过并行处理,系统可以在同一时间处理更多的请求,从而更有效地利用硬件资源。
- 减少延迟:分配到不同GCD模块的请求可以同时进行,降低了完成单个计算的平均时间。
- 增加吞吐量:由于可以同时处理多个计算,所以在给定的时间内,整体系统可以完成更多的GCD计算,从而提高吞吐量。
这种设计模式是一种经典的并发计算策略,通常称为“负载平衡”。在这种情况下,有两个独立的GCD计算单元,外加一个智能调度器,负责决定哪个GCD单元应该接收下一个计算任务。这个调度器通过
turnI
和turnO
两个寄存器来控制输入和输出。简单来说,外层的逻辑增加了如下功能:
- 智能调度:动态检查哪个GCD单元当前不忙,然后将新的计算任务分配给它。
- 状态切换:在每次
start
或getResult
调用后,通过翻转turnI
或turnO
来更改接下来任务的分配目标。这种设计的好处是:
- 当两个计算请求几乎同时到达时,它们可以被分配给两个不同的GCD单元,从而并行处理,这比单个GCD单元处理两个请求要快。
- 当一个GCD单元正在处理一个长期运算时,另一个单元仍然可以接受新的请求,这样就不会因为一个复杂的请求而阻塞系统。
这个概念在大型计算机系统中很常见,例如在服务器农场、云计算资源池以及任何需要高吞吐量和低延迟的场景中。这不仅限于计算GCD,还可以推广到其他任何可以并行处理的任务。这种模式的关键优势是它提高了资源的利用率,从而提高了整体系统性能。
硬件综合
如何将 Bluespec 代码合成到硬件中?
处理多重输入源的新型多路复用器结构
这个新型结构类似于多路复用器(mux),用于处理多个输入源。其功能包括:
x
的值由(v1 & x1) | (v2 & x2)
决定,v
的值由v1 | v2
决定。- 每个
xi
只有在对应的vi
为真时才具有意义的值。 - 编译器必须确保在任何给定时间,多路复用器最多只有一个
vi
输入为真;如果多个输入信号有效,电路将不可预测地行为。
如果 x
是一个n位的信号,这个结构需要重复n次。
FIFO电路
module mkFifo (Fifo#(1, t));
Reg#(t) d <- mkRegU; // 数据寄存器,未定义初始值
Reg#(Bool) v <- mkReg(False); // 有效位寄存器,初始值为False
// 入队方法:如果v为False(即FIFO为空),则允许入队操作
method Action enq(t x) if (!v);
v <= True; // 设置有效位为True
d <= x; // 将数据x写入寄存器d
endmethod
// 出队方法:如果v为True(即FIFO非空),则允许出队操作
method Action deq if (v);
v <= False; // 设置有效位为False
endmethod
// 查看队首元素的方法:如果v为True(即FIFO非空),则允许操作
method t first if (v);
return d; // 返回数据寄存器d中的值
endmethod
endmodule
接口和实例化状态:
- I/O接口定义了可以与电路交互的方法。
- 对于需要多重赋值的每个寄存器,需要插入一个多路复用器。
enq
、deq
和first
方法被编译,以便与FIFO接口中定义的操作相匹配。
此设计说明了如何在Bluespec中实现一个一元FIFO,即只能存储一个元素的队列。在这种实现中,d
寄存器存储队列中的数据,而 v
寄存器表示队列中是否有数据(即队列是否为空或非空)。FIFO的行为通过enq
(入队)、deq
(出队)和first
(查看队首元素)这三个方法来控制。入队时会检查FIFO是否为空,如果为空则允许新元素入队并设置v
为真;出队时将v
设置为假,表示FIFO为空;查看队首元素时,如果v
为真,则返回当前存储的数据。这种FIFO实现通常在硬件设计中用于缓冲和同步不同部分的操作。
重绘FIFO电路
这个模块是一个时序电路,其输入和输出线与其接口方法相对应。虽然它包含了状态元素,但它没有循环。
下一状态转移
部分真值表
输入 | 当前状态 | 下一状态 | 输出 |
---|---|---|---|
enq.en enq.data deq.en | dt vt | dt+1 vt+1 | enq.rdy deq.rdy first.rdy first.data |
0 x 0 | x 0 | x 0 | 1 0 0 - |
1 d 0 | x 0 | d 1 | 0 1 1 - |
0 x 0 | d 1 | d 1 | 0 1 1 d |
0 x 1 | d 1 | - 0 | 1 0 0 d |
非法输入的情况下,比如同时使能入队(enq.en
)和出队(deq.en
)信号,系统会处于不确定状态,这种情况需要通过设计来避免。在真值表的设计中,这样的情况通常会被明确指出为非法,以确保在实际硬件设计中不会发生。
在真值表中,我们可以看到,如果 enq.en
和 deq.en
都为 0,那么无论当前状态如何,都不会有任何变化,且 enq.rdy
始终为 1,表示可以进行入队操作。当 enq.en
为 1 时,表示有一个入队操作,此时如果 vt
(有效位)为 0,那么数据 d
将被加载到 dt
(数据寄存器)中,并且有效位 vt
将被设置为 1,deq.rdy
和 first.rdy
也会相应地被设置为 1,表示队列现在非空,可以进行出队操作或查看队首数据。当 deq.en
为 1 时,如果 vt
为 1,那么有效位将被清除,表示一个出队操作已发生。
“Tedious!”一词突出了真值表设计可能非常繁琐复杂的事实,尤其是对于有更多状态和输入组合的更复杂系统来说。这也是为什么现代硬件设计更倾向于使用如Bluespec这样的高级语言来描述硬件行为,因为它们可以更清晰、更直观地表达电路的行为和状态转换。
实时处理函数:电路
这是一个顺序机器的例子,展示了如何在电路中实时处理函数:
inQ
是输入队列,outQ
是输出队列。- 通过连接数据路径,收集准备好的信号,并使能调用的方法。
规则 stream
如下:
rule stream;
outQ.enq(f(inQ.first())); // 调用函数 f 处理 inQ 的首元素,并将结果入队至 outQ
inQ.deq; // 出队 inQ 的首元素
endrule
需要注意的是,enq.en
(入队使能)只有在 enq.rdy
(入队就绪)为真时才能为真;同样地,deq.en
(出队使能)只有在 deq.rdy
(出队就绪)为真时才能为真。
重绘时序电路
重绘的图形揭示了输入和输出线的布局:
- 所有输入线都用红色和蓝色标记。
- 所有输出线都用绿色和蓝色标记。
这张图展示了时序电路如何连接:输入队列 inQ
的 first
方法(查看首元素)被用作函数 f
的输入,函数的输出随后被传递给输出队列 outQ
的 enq
方法(入队)。同时,为了完成数据的流动,inQ
的 deq
方法(出队)被触发以清空队列中已经处理的元素。这个过程表明如何将输入队列中的元素连续传递给函数,并将结果连续送入输出队列,实现了数据流式处理的效果。
重要的是要注意这种模式下的数据流动性和顺序性——每个元素从 inQ
经过函数 f
被处理后,才能进入 outQ
。这是硬件设计中实现数据处理管道的一种方法,它允许在数据在电路中流动时进行实时处理,这种方式在硬件加速器和高性能计算中非常有用。
层次化时序电路
时序电路包含多个模块,每个模块代表一个顺序机器。这些模块由以下几部分定义:
- 输入/输出:输入和输出线路由模块类型(接口)定义。
- 组合逻辑:模块中定义的规则和方法定义了如何连接寄存器和模块的组合逻辑。这种逻辑不包含循环,也不受时钟信号的影响。
- 每个模块在其内部声明的内容包括它所需要的所有子模块。
硬件综合
高级思想如下:
- 每个模块代表一个顺序机器。
- 寄存器是一种基础模块——其实现在语言之外。
- 寄存器的位宽由其类型决定。
- 每个寄存器和模块都是明确实例化的。
- 模块的输入/输出线路源于其接口,即类型。
- 规则和方法定义了如何连接寄存器和模块的组合逻辑。
所产生的硬件是由一系列顺序机器组成的集合,整体上它自身表现为一个顺序机器。
在这种设计中,硬件的结构被划分为多个模块,每个模块可以单独实现特定的功能。模块化的设计允许开发者更容易地构建、测试和理解复杂的硬件系统。组合逻辑通常用于处理模块内部的状态转换,而时序逻辑(即顺序机器)用于跟踪系统的状态。
在Bluespec和其他硬件描述语言中,高级抽象允许设计者专注于功能和性能,而不必处理底层的逻辑门电路。这种方法也更容易被其他软件工具,比如自动化验证和综合工具所接受。最终,硬件综合工具将这些高级描述转换成可以在实际硬件上实现的电路设计。
寄存器:基础模块
interface Reg#(type t);
method Action _write(t x); // 写操作方法
method t _read; // 读操作方法
endinterface
寄存器是构成时序电路的基础构件,具体实现在Bluespec语言之外。可以使用 mkReg
或 mkRegU
函数创建寄存器。对于 _write
和 _read
方法,其就绪信号(rdy
)始终为真,这意味着寄存器随时准备好进行读写操作,不需要生成额外的控制信号。
特殊语法允许使用 x <= e
作为 x._write(e)
的简写,直接 x
作为 x._read
的简写,使得对寄存器的操作更加直观和简洁。
接口定义输入/输出线路
模块的输入和输出由模块的类型,即其接口定义来确定。
- 每个方法都有一个输出就绪线路(
rdy
)。 - 每个方法可能有零个或多个输入数据线路。
- 每个动作方法(
Action method
)和动作值方法(ActionValue method
)都有一个输入使能线路(en
)。 - 每个值方法(
Value method
)和动作值方法有一个输出数据线路。 - 动作方法没有输出数据线路。
例如,GCD的接口定义如下:
interface GCD;
method Action start(Bit#(32) a, Bit#(32) b); // 启动GCD计算的动作方法
method ActionValue#(Bit#(32)) getResult; // 获取GCD计算结果的动作值方法
endinterface
此接口定义了两种方法:start
用于启动GCD计算,并通过输入使能线(en
)和就绪线(rdy
)来控制。getResult
方法用于获取计算结果,也有对应的使能线和就绪线。
在时序电路设计中,寄存器是保持状态的关键元素,而方法及其相关的使能和就绪信号决定了电路的控制流和数据流。这些元素共同形成了电路的动态行为,使得设计者能够创建复杂的,响应输入变化的系统。
补充:
在Bluespec语言中,接口(interface)定义了模块对外的行为和可以调用的方法。对于每个定义在接口中的方法,Bluespec会自动提供一些特定的信号,以便于模块的正确同步和通信。具体来说,对于动作方法(Action methods)和动作值方法(ActionValue methods),Bluespec遵循以下规则:
- 动作方法(如
start
)会自动生成一个使能线(en
),用于触发该动作。这个使能线作为方法调用的一部分,表示当信号为高(true)时,动作可以执行。- 同样的方法还会有一个就绪线(
rdy
),这个信号由模块内部的逻辑控制,指示方法是否准备好被调用。只有当就绪线为高时,结合使能线为高的信号,动作才实际发生。- 对于动作值方法(如
getResult
),除了使能线和就绪线,还会有一个输出数据线,用于传递方法返回的值。这些信号不需要开发者显式声明,它们是由Bluespec的编译器根据接口的定义自动处理的。Bluespec的语义确保了每个动作方法或动作值方法在逻辑上都与这些控制信号相关联。因此,当你在接口中定义一个动作方法时,Bluespec知道需要为该方法生成和管理使能和就绪信号。
这种设计抽象了底层的硬件实现细节,允许设计者专注于高层的行为和模块间的交互,同时保证了硬件设计的正确性和可同步性。
编译防护(Guards)
Bluespec设计中规定了每个方法和规则的防护。防护可以明确声明;如果没有声明防护,则假定其为真。然而,防护也隐式地继承了所调用方法的防护。编译器从这些明确和隐式的防护中生成就绪信号(ready signal)。
module mkFifo (Fifo#(1, t));
Reg#(t) d <- mkRegU; // 数据寄存器,未定义初始值
Reg#(Bool) v <- mkReg(False); // 有效位寄存器,初始值为False
// 如果有效位v为False,则允许入队操作
method Action enq(t x) if (!v);
v <= True; // 设置有效位为True
d <= x; // 将数据x写入寄存器d
endmethod
// 如果有效位v为True,则允许出队操作
method Action deq if (v);
v <= False; // 将有效位设置为False
endmethod
// 如果有效位v为True,则允许读取队首数据
method t first if (v);
return d; // 返回寄存器d中的数据
endmethod
endmodule
防护定义了就绪信号:
enq
方法的就绪信号取决于有效位v
,仅在v
为False
时,enq
操作才是就绪的,这反映了FIFO队列为空的状态。deq
方法的就绪信号直接由有效位v
确定,仅在v
为True
时,deq
操作才是就绪的,这意味着FIFO队列中存在数据。first
方法的就绪信号也是由有效位v
确定,同样,只有当队列非空时,即v
为True
,才能执行first
操作并读取数据。
在Bluespec中,防护通常用于控制方法是否可以执行。防护提供了一种机制,通过它,方法可以根据电路的当前状态确定其操作是否应该被触发。这样,就能保证电路在任何给定时间都不会处于不确定或错误的状态,同时确保所有的操作都是按照正确的顺序执行的。
组合防护
Bluespec允许通过方法的组合来构建更复杂的控制逻辑。例如,一个名为multiGCD
的模块,它有两个子模块gcd1
和gcd2
,分别处理不同的GCD计算请求。模块multiGCD
使用turnI
和turnO
两个寄存器来决定哪个子模块接收新的输入和输出请求。
method Action start(Bit#(32) a, Bit#(32) b);
if (turnI) gcd1.start(a,b);
else gcd2.start(a,b);
turnI <= !turnI;
endmethod
这里的start
方法是条件性的,取决于turnI
的值。如果turnI
为真,gcd1
的start
方法会被调用;否则,调用gcd2
的start
方法。然后turnI
会被翻转,以便于下一次调用选择另一个子模块。
生成使能信号和关联数据
在Bluespec设计中,使能信号隐式地为每个被调用的动作(Action)和动作值(ActionValue)方法指定。
通过传播调用方法或规则的输入使能信号来实现这一点。这意味着当一个模块的方法被另一个模块的方法调用时,调用方的使能信号将会传递给被调用方的方法。如果调用方的方法被允许执行(即使能信号为真),则被调用方的方法也将被允许执行。
在multiGCD
的示例中,turnI
的状态决定了是调用gcd1
还是gcd2
模块的start
方法。gcd1.start.rdy
和gcd2.start.rdy
是这两个方法的就绪信号。multiGCD.start.rdy
就绪信号将会是这两个信号的组合,确保只有一个子模块的start
方法在任何时候被使能。
Bluespec中的这种机制简化了复杂的硬件设计,使得设计者可以专注于行为逻辑,而底层的使能信号传递和就绪信号生成都由编译器自动处理。这样不仅保证了设计的一致性和同步性,还简化了时序电路的设计和验证过程。
补充:
在Bluespec设计中,方法的调用通常涉及两种类型的信号:使能信号(
enable
)和就绪信号(ready
)。当一个模块(调用方)决定调用另一个模块(被调用方)中的方法时,调用方的使能信号会传递到被调用方的方法中,这种机制被称为“信号传播”。使能信号(Enable Signals)
使能信号指示一个操作何时应当发生。在Bluespec中,每个操作或方法调用都会有一个相关的使能信号。
就绪信号(Ready Signals)
就绪信号表示一个模块的特定操作或方法是否准备好被执行。它是从模块内部产生的,基于模块当前的状态和它的逻辑。
信号传播(Signal Propagation)
当一个模块想要调用另一个模块的方法时,它会检查被调用方法的就绪信号。如果就绪信号为真(即被调用方法准备好执行),调用方的使能信号则会“传递”到被调用方法上,触发该方法的执行。
例子 1:基本调用
假设模块A有一个方法
doWork()
,当模块B决定调用这个方法时,模块B会将它的使能信号设置为真(B.doWork_en = True
)。这个信号会被传递给模块A的doWork()
方法,只有在模块A的doWork_rdy
也为真时,操作才会发生。例子 2:条件调用
如果模块A的
doWork()
方法的执行依赖于某个特定的条件(比如,只有在模块A的内部状态为特定值时才执行),这个条件会作为就绪信号的一部分。因此,即使模块B的使能信号为真,除非条件满足(即doWork_rdy = True
),否则doWork()
不会执行。例子 3:组合调用
模块A可能有两个方法
prepare()
和execute()
,其中execute()
的调用依赖于prepare()
的完成。在这种情况下,execute_en
的使能信号不仅仅取决于外部的调用(比如模块B的指示),还取决于prepare_rdy
的状态。如果prepare()
方法已经准备好,execute()
的就绪信号就会变为真,这时外部的使能信号才会真正传递给execute()
。例子 4:多模块调用
在一个更复杂的场景中,如果一个操作需要跨多个模块的协作,例如模块A的操作依赖于模块B和模块C的就绪信号。在这种情况下,模块A的使能信号可能会被传递到模块B和模块C,并且只有当两个模块都准备好时,模块A的操作才会发生。
这种信号传播机制在Bluespec中是自动管理的,这使得复杂的多模块互动和依赖关系得以简化。设计者不需要手动编写每个模块之间的信号交互代码,而是可以依赖于Bluespec编译器来正确处理这些细节,这极大地提高了设计的抽象级别和可管理性。
规则和方法定义了组合逻辑和使能信号
Bluespec中的模块通常由多个方法和内部逻辑组成。下面是三个示例模块,它们展示了如何定义不同类型的操作,并且如何将它们映射到底层硬件逻辑。
示例模块1(mkEx1)
module mkEx1 (...);
Reg#(t) x <- mkRegU; // 未初始化的寄存器x
method Action f(t a);
x <= e; // x寄存器被赋值为e
endmethod
endmodule
此模块中,当动作方法f
被调用时,寄存器x
将被赋值为e
。没有条件,这意味着只要方法f
被使能并且寄存器x
就绪,x
就会接收新值。
示例模块2(mkEx2)
module mkEx2 (...);
Reg#(t) x <- mkRegU; // 未初始化的寄存器x
method Action f(t a);
if (b) x <= e; // 如果b为真,则x寄存器被赋值为e
endmethod
endmodule
这个模块展示了一个带有条件的动作。在这里,只有当布尔表达式b
为真时,动作方法f
才会更新寄存器x
的值为e
。
示例模块3(mkEx3)
module mkEx3 (...);
Reg#(t) x <- mkRegU; // 未初始化的寄存器x
method Action f(t a);
if (b) x <= e1; // 如果b为真,x寄存器被赋值为e1
else x <= e2; // 否则,x寄存器被赋值为e2
endmethod
endmodule
在此模块中,动作方法f
根据布尔表达式b
的真假,决定是将值e1
还是e2
赋给寄存器x
。
这些模块说明了如何在Bluespec中使用条件语句来控制寄存器的赋值过程。每个模块的方法都可以直接映射到具体的硬件操作,其中组合逻辑用于确定何时进行寄存器更新。
综合多个源到寄存器赋值
示例模块4(mkEx4)
module mkEx4 (...);
Reg#(t) x <- mkRegU; // 未初始化的寄存器x
method Action f(t a);
x <= e1; // 方法f将e1赋值给寄存器x
endmethod
method Action g(t a);
x <= e2; // 方法g将e2赋值给寄存器x
endmethod
endmodule
在这个模块中,有两个动作方法f
和g
都试图更新同一个寄存器x
。编译器必须确保f
和g
不会同时被使能,以避免赋值冲突。
为了解决可能的赋值冲突,编译器利用冲突矩阵(Conflict Matrix,CM)来管理每个模块的使能信号。冲突矩阵确保在任何时刻,只有一个操作可以更新寄存器x
。这在硬件设计中至关重要,因为它防止了不确定的行为,确保了电路的可靠性。
在Bluespec中,动作方法通过显式的(如if
语句中的条件)或隐式的(如冲突矩阵管理)防护来控制访问共享资源(如寄存器),从而允许安全、同步地执行复杂的操作序列。这种方法的自动化大大简化了并行或互斥访问共享状态的硬件设计。
冲突矩阵(CM)用于接口
冲突矩阵(Conflict Matrix,CM)定义了模块内部哪些方法可以同时被调用。针对寄存器的CM如下所示:
reg.r | reg.w | |
---|---|---|
reg.r | CF | < |
reg.w | > | C |
- CF(Conflict Free)意味着两个读操作可以并发执行。
- C(Conflict)表示两个写操作会发生冲突,不能同时执行。
<
>
表示读和写操作可以并发执行,但操作的执行顺序就好像是先读后写(a < b
意味着a
操作先于b
操作发生)。
寄存器的CM是系统地用来派生模块接口的CM的基础。CM也可以为任何规则集合定义。
一元FIFO
module mkFifo (Fifo#(1, t));
Reg#(t) d <- mkRegU; // 未初始化的数据寄存器
Reg#(Bool) v <- mkReg(False); // 用于表示FIFO状态的布尔寄存器
// 入队操作,如果FIFO为空(即v为False),则允许入队
method Action enq(t x) if (!v);
v <= True; // 将状态设为非空
d <= x; // 将数据写入寄存器
endmethod
// 出队操作,如果FIFO非空(即v为True),则允许出队
method Action deq if (v);
v <= False; // 将状态设为空
endmethod
// 查看FIFO中的数据操作,如果FIFO非空,则允许查看
method t first if (v);
return d; // 返回寄存器中的数据
endmethod
endmodule
对于一元FIFO,其冲突矩阵如下所示:
enq | deq | first | |
---|---|---|---|
enq | C | ME | ME |
deq | ME | C | > |
first | ME | < | CF |
ME(Mutually Exclusive)表示规则或方法之间不存在冲突,因为它们在任何时候都不会同时准备好(即它们不能同时为True)。
<
表示操作的顺序性,即 a < b
意味着 a
操作先于 b
操作发生。
在这种设计中,enq
和 deq
方法不能同时被调用,因为它们对同一个状态寄存器 v
进行相反的操作,同时只能执行一个。first
方法可以与 deq
同时发生,但其顺序是 first
方法先执行,随后才是 deq
。这样的设计确保了FIFO的状态在任何时间点上都是一致的。当实现硬件逻辑时,这些规则和冲突矩阵的使用能够避免不确定的行为,确保硬件的稳定性和可靠性。
补充:
冲突矩阵(Conflict Matrix,简称 CM)是用来描述在一个模块中不同方法间可能存在的冲突情况。它是一个表格,表中的每个单元格描述了两个方法是否可以同时执行。在Bluespec中,冲突矩阵是由编译器自动生成的。编译器会分析模块中所有方法的定义和它们对共享资源(如寄存器)的访问模式,然后生成冲突矩阵以确保在任何时刻不会有不一致或冲突的操作对这些共享资源进行访问。CM有助于编译器优化代码,并确定在同一时钟周期中哪些操作可以安全地并发执行。
GCD电路绘制作业
module mkGCD (GCD);
Reg#(Bit#(32)) x <- mkReg(0); // 寄存器x,初始值为0
Reg#(Bit#(32)) y <- mkReg(0); // 寄存器y,初始值为0
Reg#(Bool) busy <- mkReg(False); // 忙碌标志,初始值为False
// GCD的核心规则
rule gcd;
if (x >= y) begin x = x - y; end // 如果x大于等于y,则x减y
else if (x != 0) begin x = y; y = x; end // 否则如果x不等于0,则交换x和y
endrule
// 启动GCD计算的方法
method Action start(Bit#(32) a, Bit#(32) b) if (!busy);
x = a; y = b; busy = True; // 设置输入数值,并将忙碌标志设为True
endmethod
// 获取GCD结果的方法
method ActionValue(Bit#(32)) getResult if (busy && (x==0));
busy = False; return y; // 当x为0时,计算完成,返回结果,并将忙碌标志设为False
endmethod
endmodule
此模块定义了一个进行GCD计算的寄存器系统。gcd
规则定义了当寄存器 x
和 y
被设置后如何计算它们的最大公约数。start
方法用于初始化这些寄存器,并开始计算过程,而 getResult
方法用于在计算完成后检索结果。
在接口 GCD
中,有两种方法:
start
方法接受两个32位整数作为参数,并在模块不忙时启动GCD计算。getResult
方法在GCD计算完成后返回结果。
此电路的设计要求考虑到,当GCD正在计算时,不能开始另一个新的GCD计算。因此,busy
寄存器用于跟踪模块是否正在执行计算。如果 busy
为 False
,则允许调用 start
方法;如果 busy
为 True
且 x
为 0
,表示计算完成,此时可以调用 getResult
方法获取结果。这种设计确保了在计算进行时,不会有新的 start
方法调用干扰正在进行的计算。
在实际的硬件设计中,这些方法和规则能够转换为硬件逻辑,包括控制信号、数据路径和状态机,以便自动化执行GCD计算和管理。Bluespec语言的优势在于,它能够让设计师以高层次的方式描述复杂的硬件行为,同时自动化底层的硬件实现。