01、引言
由于接口控制信号上的差异,要实现Bluespec SystemVerilog(BSV)生成的代码和外部Verilog代码之间的正确交互是一件比较麻烦同时容易出错的事情。在BSV中, 模块之间的交互都是基于Action或ActionValue这两类method完成。下图展示了使用BSV设计的某一模块的接口定义及其实际生成的硬件端口。由图可见,除了被显式地定义为函数输入参数或返回类型的数据信号外,每个method都隐含着一对控制信号,即en和rdy。
其中,rdy信号指示该方法已经准备好被调用,而当外部模块调用该方法时会拉高对应的en信号。en-rdy控制信号和AXI总线中valid-ready信号的作用类似,这两对信号都保证了当通信双方都准备好时才能完成一拍数据的传输。虽然完成的功能相同,但这两对信号在具体的实现机制上仍存在一定差异:
首先,en-rdy和valid-ready并不是一一对应的关系。对于某个模块,en永远是输入信号,而rdy永远是输出信号。而在valid-ready握手协议中,master端输出valid接收ready,而slave端输出ready接收valid。因此,这两对信号有如下表所示的对应关系:
| | master | slave | | valid | rdy | en | | ready | en | rdy |
其次,是控制信号之间依赖关系的差异。在BSV中,method的en信号依赖于rdy信号,具体来说一个method只有在其准备好(rdy为高)时才能被调用。下图展示了这一依赖关系在硬件上的具体实现,即输入的en信号需要和输出的rdy信号相与后传递给下一级。而在AXI协议中,为了避免产生死锁(通信双方都等待对方准备好后再响应),其明确规定:master输出的valid信号不能依赖于ready信号,即不能等待slave侧准备好后再发起请求;相反,slave端输出的ready信号可以依赖于输入的valid,即可以等待master发起请求后再作响应。
在大部分使用Verilog实现的电路中,我们都会基于valid-ready握手协议实现模块之间的交互。如果在一个项目中,我们需要将BSV生成的代码和基于Verilog的设计进行交互,通常还需要实现一个转换模块,来处理valid-ready和en-rdy控制信号之间的交互。
除了控制信号的差异外,BSV生成的Verilog代码还存在如下问题:如果在BSV中将多个相关的输入/输出信号封装在一个结构体,那么在生成的Verilog接口中所有封装在一起的字段都会合并成单个信号。例如,在BSV中使用AXI-Stream总线时,为了方便信号传递,通常会将总线上的信号封装成一个结构体后在方法之间传递:
typedef struct {
Bit#(TMul#(keepWidth, 8)) tData;
Bit#(keepWidth) tKeep;
Bit#(usrWidth) tUser;
Bool tLast;
} AxiStream#(numeric type keepWidth, numeric type usrWidth) deriving(Bits);
interface AxiStreamExample;
interface Put#(AxiStream#(8, 1)) axiStreamSlave;
interface Get#(AxiStream#(8, 1)) axiStreamMaster;
endinterface
而在生成的Verilog代码中,结构体里定义的所有字段都合并到了axiStreamSlave_put/axiStreamMaster_get信号里:
module mkAxiStreamExample(
CLK,
RST_N,
axiStreamSlave_put,
EN_axiStreamSlave_put,
RDY_axiStreamSlave_put,
EN_axiStreamMaster_get,
axiStreamMaster_get,
RDY_axiStreamMaster_get
);
如果我们要将上述代码与其他Verilog模块交互,需要添加一个额外的模块对生成的mkAxiStreamExample进行封装。该模块需要完成两件事:
1)将合并的信号解析成每个独立的信号,
2)将en-rdy转换为valid-ready握手协议。
以Master端信号的封装为例,具体的实现代码如下:
module mkAxiStreamExampleWrapper(
input clk,
input reset_n,
output m_axis_tvalid,
input m_axis_tready,
output m_axis_tlast,
output m_axis_tuser,
output [63:0] m_axis_tdata,
output [ 7:0] m_axis_tkeep,
);
mkAxiStreamExample axiStreamExampleInst (
.CLK ( clk),
.RST_N (reset_n),
.RDY_axiStreamMaster_get (m_axis_tvalid),
.EN_axiStreamMaster_get (m_axis_tvalid & m_axis_tready),
.axiStreamMaster_get (
{m_axis_tdata, m_axis_tkeep, m_axis_tuser, m_axis_tlast}
)
);
endmodule
虽然上面展示的Verilog封装模块可以保证模块间正确的交互,但仍存在一些缺陷。首先,解析打包信号的方式与BSV中结构体的定义相关,如果结构的内容发生更改,封装模块解析出的结果就可能出错。其次,手动地处理en-rdy和valid-ready信号对之间的转换也容易出错。这些问题都降低了BSV项目的可维护性。
为了方便BSV和Verilog之间的交互,我们实现了blue-wrapper项目并提供了等同于上述Verilog封装的BSV实现,使得经过封装的BSV模块所生成的代码能够直接和其他Verilog模块进行交互。下文将介绍blue-wrapper的具体实现及其使用方式,主要包括三部分内容:
- PipeOut/PipeIn接口的定义,对应代码实现见 src/SemiFifo.bsv
- 基于PipeOut/PipeIn和Get/Put接口实现握手控制信号转换, 详细代码实现可见 src/BusConversion.bsv;
- 在控制信号转换的基础上,还需要对完成对数据信号的解析,blue-wrapper中分别提供了对AXI-Stream,AXI4-Lite和AXI4-Full等协议的支持;
下文将结合实际代码分别介绍这三部分的具体实现。
02、PipeOut/PipeIn接口
在基于valid-ready控制信号对的数据交互场景下,交互双方可分为Master和Slave。其中,Master端发起数据传输,可对应BSV中常用的Get接口,而Slave负责接收数据,因此可以对应BSV中的Put接口。除了Get/Put外,为了方便实现握手控制信号的转换,blue-wrapper中还额外定义了PipeOut/PipeIn接口,这两个接口分别封装了FIFOF接口出队侧(deq)和入队(enq)侧的方法,具体定义如下:
interface PipeIn#(type dType);
method Action enq(dType data);
method Bool notFull();
endinterface
interface PipeOut#(type dType);
method dType first();
method Action deq();
method Bool notEmpty();
endinterface
从实现功能的角度上看,PipeOut/PipeIn和Get/Put接口类似,都可分别实现数据的输出/输入。但对于Get/Put接口,其get/put方法所隐含的en-rdy控制信号在BSV中是无法访问的。而对于PipeOut/PipeIn接口,其将deq/enq方法对应的rdy信号分别通过notEmpty/notFull方法暴露出来,使得我们可以直接在BSV中对其进行访问,而这一点将极大地方便握手控制信号的转换。
03、握手控制信号转换
显式定义valid-ready信号
实现接口转换的第一步需要在interface中定义valid和ready信号对应的method,一方面使得生成的Verilog代码直接包含valid-ready信号对,另一方面方便我们在BSV中操纵这两个信号实现握手协议转换。在BSV中,一个方法的返回值对应Verilog的输出端口,而方法的输入参数对应输入端口,基于该原则,valid-ready协议的Master/Slave侧接口的BSV定义如下:
(* always_ready, always_enabled )
interface RawBusMaster#(type dType);
( result = "data" ) method dType data;
( result = "valid"*) method Bool valid;
(* prefix = "" ) method Action ready(( port = "ready" ) Bool rdy);
endinterface
( always_ready, always_enabled )
interface RawBusSlave#(type dType);
( prefix = "" ) method Action validData(
( port = "valid" ) Bool valid,
( port = "data" ) dType data
);
( result = "ready" *) method Bool ready;
endinterface
上述代码中,编译属性“always_ready”和“always_enabled”消除了每个method隐含的en-rdy控制信号对。同时,我们可以通过设置输出method的“result”属性和每个输入参数的“port”属性来指定生成的Verilog中每个端口的具体名称。
en-rdy和valid-ready之间的转换
在定义好包含valid-ready控制信号的interface后,下一步需要完成en-rdy到valid-ready握手控制信号的转换。blue-wrapper项目分别提供了两种不同的转换思路:
- 将需要封装的PipeOut/PipeIn接口作为参数传入转换模块供valid/ready信号对应的method调用;
- 将RawBusMaster/Slave接口分别封装成PipeIn/PipeOut接口供其他BSV模块调用;
基于第一种思路实现的转换模块包括: mkPipeOutToRawBusMaster/mkPipeInToRawBusSlave,以及mkGetToRawBusMaster和mkPutToRawBusSlave。以mkPipeOutToRawBusMaster为例,该模块接收PipeOut接口作为输入参数,并返回RawBusMaster接口,其中各个method的实现思路和具体代码如下:
- **valid:**对于Master端,其valid信号对应BSV中的rdy信号,而PipeOut接口通过notEmpty方法暴露出了deq方法对应的rdy,因此valid方法直接返回notEmpty的值;
- **data:**对应PipeOut接口的first方法;
- **ready:**作为方法输入参数传递的ready信号对应BSV中的en控制信号,该值为真时需要调用PipeOut接口的deq方法。由于在BSV中调用任意method,编译器都会自动保证上文提到的en-rdy的依赖关系,因此在调用deq方法时不需要额外检查其rdy信号来保证握手成功。
module mkPipeOutToRawBusMaster#(
PipeOut#(dType) pipe
)(RawBusMaster#(dType));
RWire#(dType) dataW < - mkRWire;
Wire#(Bool) readyW < - mkBypassWire;
rule passWire if (pipe.notEmpty);
dataW.wset(pipe.first);
endrule
rule passReady if (readyW);
pipe.deq;
endrule
method Bool valid = pipe.notEmpty;
method dType data = fromMaybe(?, dataW.wget);
method Action ready(Bool rdy);
readyW <= rdy;
endmethod
endmodule
对于PipeIn到RawBusSlave的转换,其实现的原理和Master端类似,具体代码如下:
module mkPipeInToRawBusSlave#(
PipeIn#(dType) pipe
)(RawBusSlave#(dType));
Wire#(Bool) validW < - mkBypassWire;
Wire#(dType) dataW < - mkBypassWire;
rule passData if (validW);
pipe.enq(dataW);
endrule
method Action validData(Bool valid, dType data);
validW <= valid;
dataW <= data;
endmethod
method Bool ready = pipe.notFull;
endmodule
对于Get/Put接口,由于在BSV中无法直接访问get/put方法的rdy信号,直接进行转换无法提取出master输出的valid信号以及slave输出的ready信号。因此,在blue-wrapper的实现中我们通过添加一个额外的FIFOF模块作为媒介,将Get/Put接口转换成PipeOut/PipeIn接口后,调用上面展示的两个模块实现控制信号的转换,以Get接口为例,具体的代码实现如下:
module mkGetToRawBusMaster#(
Get#(dType) get
)(RawBusMaster#(dType));
FIFOF#(dType) fifo < - mkFIFOF;
mkConnection(get, toPut(fifo));
let rawBus < - mkPipeOutToRawBusMaster(
convertFifoToPipeOut(fifo)
);
return rawBus;
endmodule
第二种实现思路对应代码中的: mkRawBusMasterToPut/mkRawBusMasterToGet ,以及mkRawBusMasterToPipeIn/mkRawBusMasterToPipeOut四个模块。这种转换方式分别用PipeIn/PipeOut或Put/Get接口封装RawBusMaster/RawBusSlave接口 。 对于其他BSV模块,可以通过PipeIn/Put接口将数据传入转换模块然后从RawBusMaster发送出去,同时可以通过PipeOut/Get接口获取从RawBusSlave上接收到的数据。这种实现方式和BSV提供的BVI接口类似。以master侧为例,具体的转换实现如下:
首先,我们需要定义RawBusMasterToPipeIn接口,其由两个子接口RawBusMaster和PipeIn组成,其中PipeIn用于封装RawBusMaster使其可供其他BSV模块调用。
对于RawBusMaster接口,其实现代码主要是在进行信号的传递, 从validData中取出valid和data方法的返回值,并将ready方法的输入参数传递给 readyW 。
对于PipeIn接口,notFull方法返回readyW的值,enq方法将传入的参数写入 validData。 同时为了保证握手成功,enq方法需要被readyW所守卫(guarded), 即当输入的ready信号为高时,才可调用enq方法传入数据。
interface RawBusMasterToPipeIn#(type dType);
interface RawBusMaster#(dType) rawBus;
interface PipeIn#(dType) pipe;
endinterface
module mkRawBusMasterToPipeIn(RawBusMasterToPipeIn#(dType));
RWire#(dType) validData < - mkRWire;
Wire#(Bool) readyW < - mkBypassWire;
interface RawBusMaster rawBus;
method Bool valid = isValid(validData.wget);
method dType data = fromMaybe(?, validData.wget);
method Action ready(Bool rdy);
readyW <= rdy;
endmethod
endinterface
interface PipeIn pipe;
method Bool notFull = readyW;
method Action enq(dType data) if (readyW);
validData.wset(data);
endmethod
endinterface
endmodule
对于Get/Put接口,也可以使用上述方法对RawBusSlave/RawBusMaster接口进行封装,其实现的关键点都是要为get和put方法设置正确的守卫信号以保证握手成功.
死锁问题
为了避免master和slave之间互相等待而产生死锁,AXI文档中规定master不能等待slave侧拉高ready后再输出有效的valid和data,但允许slave侧在master拉高valid之后再置ready为高。在BSV中对于每个method,en只有在rdy信号拉高后才能拉高。由于en-rdy和valid-ready不同的依赖关系,在交互的过程中就有可能导致双方产生死锁。下文将主要针对上面提到的两种封装方法,分析其是否会引入死锁问题。
- 第一种封装方式相当于是在转换模块中调用传入的PipeOut/PipeIn接口的方法。在BSV中调用任何方法,编译器都会自动地为en添加对于rdy的依赖,具体的en-rdy和valid-ready间的交互可以由下图所示。其中,en-rdy之间的依赖关系如红色虚线所示。当Slave侧为Verilog实现时,valid和ready之间可能存在如黑色虚线所示的依赖关系。由图可知,这两种依赖关系同向因此不会产生死锁。
- 第二种封装方式类似于BSV提供的BVI接口,其将Verilog信号封装成BSV中的method供其他模块调用。同样的,对于这些method,BSV会给en信号添加对rdy的依赖,如下图红色虚线所示。而基于Verilog实现的Slave端口,其valid-ready之间可能存在如黑色虚线所示的依赖关系。由图可见,en-rdy和valid-ready之间正好形成了一个死锁环路,即master端等待ready拉高后输出有效的valid,而slave等待master输出有效valid后再拉高ready。因此,在使用mkRawBusMasterToPipeOut转换模块时需要保证所对接的slave侧Verilog实现中不存在ready对valid的依赖。
04、信号解析
完成控制信号的转换后,我们已经实现了可生成valid-ready控制信号的 RawBusMaster#(dType) 和 RawBusSlave#(dType) 接口 。 但是,如果dType是用户定义的struct结构体,则生成的Verilog会将struct中的所有字段打包到一个信号中。因此,我们仍然需要在BSV中将结构体的每个字段解析为单独的信号,以生成直接可用的Verilog代码。blue-wrapper分别提供了针对AXI-Stream, AXI4-Lite以及AXI4-Full三种总线协议的信号解析实现。以Master端的AXI-Stream接口为例,其信号解析代码如下:
(*always_ready, always_enabled*)
interface RawAxiStreamMaster#(numeric type dataWidth, numeric type usrWidth);
(* result = "tvalid" *) method Bool tValid;
(* result = "tdata" *) method Bit#(dataWidth) tData;
(* result = "tkeep" *) method Bit#(keepWidth) tKeep;
(* result = "tlast" *) method Bool tLast;
(* result = "tuser" *) method Bit#(usrWidth) tUser;
(* always_enabled, prefix = "" *)
method Action tReady((* port="tready" *) Bool ready);
endinterface
module mkPipeOutToRawAxiStreamMaster#(
PipeOut#(AxiStream#(dataWidth, usrWidth)) pipe
)(RawAxiStreamMaster#(dataWidth, usrWidth));
let rawBus < - mkPipeOutToRawBusMaster(pipe);
return convertRawBusToRawAxiStreamMaster(rawBus);
interface RawAxiStreamMaster;
method Bool tValid = rawBus.valid;
method Bit#(dataWidth) tData = rawBus.data.tData;
method Bit#(keepWidth) tKeep = rawBus.data.tKeep;
method Bool tLast = rawBus.data.tLast;
method Bit#(usrWidth) tUser = rawBus.data.tUser;
method Action tReady(Bool rdy);
rawBus.ready(rdy);
endmethod
endinterface
endmodule
除了使用blue-wrapper中提供的三种常用总线接口的转换模块外,用户也可以仿照上述代码为自定义的接口实现对应的转换模块,具体的实现步骤如下:
- 首先,需要实现自定义Verilog接口对应的BSV接口。其中,每个输出信号都需要独立定义成一个method,每个输入信号都需定义成Action方法的一个输入参数;
- 使用使用上文的介绍的握手控制信号转换模块将Get/Put或PipeOut/PipeIn转换成 RawBusMaster/RawBusSlave ;
- 在得到RawBusMaster/RawBusSlave后将结构体中的字段和对应的method相连。
05、总结
对于BSV生成的硬件代码,其接口通常是基于en-rdy控制信号进行交互,而使用Verilog设计时,我们通常采用valid-ready信号对实现模块之间的交互。由于控制信号上的差异,将BSV生成的代码和Verilog设计进行交互通常需要额外的转换模块。针对该问题,blue-wrapper项目为BSV代码实现了相应的封装模块,使得封装后生成的Verilog代码能够直接和外部的Verilog代码进行交互。本文主要介绍了blue-wrapper背后的实现原理,具体包括: 定义PipeOut/PipeIn接口以提取出rdy信号;两种不同的握手控制信号转换的思路;以及解析struct结构体为每个字段生成独立的信号等三部分内容。
评论
查看更多