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結構體為每個字段生成獨立的信號等三部分內容。
評論
查看更多