一、ready-valid接口
Arbiter
和
Queue
都使用了
ready-valid
接口,該類型的端口在單一資料信号的基礎上又添加了ready和valid信号以使用
ready-valid
握手協定。它包含3個信号:
- ready:高有效時表示資料接收者
已經準備好接收信号,由consumer驅動。consumer
- valid:高有效時表示資料生産者
已經準備好待發送的資料了,由producer驅動。producer
- bits:是要在producer與consumer之間傳輸的資料。
需要注意的是,valid和ready信号之間不能存在組合邏輯關系,valid信号應該隻依賴于此時的源資料是否有效,ready信号應該隻依賴于此時的資料接收者是否準備好接收資料了。當在某個時鐘周期,valid和ready同時有效時,資料被視為傳輸。
建立ready-valid接口很簡單,使用單例對象Decoupled即可建立,有以下兩種形式:
-
:可以傳入任意的資料類型,然後傳回一個ready-valid接口,此時ready是input信号,valid和bits都是output信号。是以它是屬于資料生産者producer的端口。Decoupled(...)
-
:Flipped()會将ready-valid接口的信号方向進行取反,是以此時ready是output信号,valid和bits都是input信号。是以它是屬于資料接收者consumer的端口。Flipped(Decoupled(...))
資料接收者和發送者都是相對的,一定要根據具體的情況正确設定信号方向。
二、仲裁器Arbiter
Chisel内建了兩種仲裁器,一種是優先仲裁器,另一種是循環仲裁器。
- 優先仲裁器的輸入通道的優先級是固定的,每次都是選擇多個有效通道中優先級最高的。
- 而循環仲裁器每次都從不同的起點開始仲裁,采用輪詢方式檢視各個通道是否有請求,優先選擇先查到的有效通道。由于起點是依次變化的,是以每個通道總體來說具有相同的優先級。
第一種仲裁器優先仲裁器
Arbiter
在chisel3.util包下面,隻定義了Arbiter類,沒有單例對象,是以每次都需要通過new來建立Arbiter對象。
建立Arbiter對象的方式如下所示:
需要提供兩個參數,gen是傳輸的資料的類型,n是待仲裁對象的個數,也即資料發送者producer的個數。資料接收者consumer的個數為預設為1。
Arbiter内部使用
ArbiterIO
定義端口,而ArbiterIO内部又使用
Decoupled()
建立最終所需的ready-valid接口,定義如下:
class ArbiterIO[T <: Data](private val gen: T, val n: Int) extends Bundle {
val in = Flipped(Vec(n, Decoupled(gen)))
val out = Decoupled(gen)
val chosen = Output(UInt(log2Ceil(n).W))
}
可以看出,它會建立n個和producer連接配接的ready-valid接口,1個和consumer連接配接的ready-valid接口,以及一個表示最終選擇了哪個producer的chosen變量,該變量的值表示被選擇的producer在所有待仲裁對象中的索引,從0開始。
下面定義一個二選一仲裁器MyArbiter,并在代碼中例化了Arbiter:
class MyArbiter extends Module {
val io = IO(new Bundle {
val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
val out = Decoupled(UInt(8.W))
val chosen = Output(UInt())
})
val arbiter = Module(new Arbiter(UInt(8.W), 2)) // 2 to 1 Priority Arbiter
arbiter.io.in <> io.in
io.out <> arbiter.io.out
io.chosen := arbiter.io.chosen
}
生成的verliog代碼如下:
module Arbiter(
output io_in_0_ready,
input io_in_0_valid,
input [7:0] io_in_0_bits,
output io_in_1_ready,
input io_in_1_valid,
input [7:0] io_in_1_bits,
input io_out_ready,
output io_out_valid,
output [7:0] io_out_bits,
output io_chosen
);
wire grant_1 = ~io_in_0_valid; // @[Arbiter.scala 31:78]
assign io_in_0_ready = io_out_ready; // @[Arbiter.scala 134:19]
assign io_in_1_ready = grant_1 & io_out_ready; // @[Arbiter.scala 134:19]
assign io_out_valid = ~grant_1 | io_in_1_valid; // @[Arbiter.scala 135:31]
// @[Arbiter.scala 126:27 Arbiter.scala 128:19 Arbiter.scala 124:15]
assign io_out_bits = io_in_0_valid ? io_in_0_bits : io_in_1_bits;
// @[Arbiter.scala 126:27 Arbiter.scala 127:17 Arbiter.scala 123:13]
assign io_chosen = io_in_0_valid ? 1'h0 : 1'h1;
endmodule
module MyArbiter(
input clock,
input reset,
output io_in_0_ready,
input io_in_0_valid,
input [7:0] io_in_0_bits,
output io_in_1_ready,
input io_in_1_valid,
input [7:0] io_in_1_bits,
input io_out_ready,
output io_out_valid,
output [7:0] io_out_bits,
output io_chosen
);
wire arbiter_io_in_0_ready; // @[Arbiter.scala 37:23]
wire arbiter_io_in_0_valid; // @[Arbiter.scala 37:23]
wire [7:0] arbiter_io_in_0_bits; // @[Arbiter.scala 37:23]
wire arbiter_io_in_1_ready; // @[Arbiter.scala 37:23]
wire arbiter_io_in_1_valid; // @[Arbiter.scala 37:23]
wire [7:0] arbiter_io_in_1_bits; // @[Arbiter.scala 37:23]
wire arbiter_io_out_ready; // @[Arbiter.scala 37:23]
wire arbiter_io_out_valid; // @[Arbiter.scala 37:23]
wire [7:0] arbiter_io_out_bits; // @[Arbiter.scala 37:23]
wire arbiter_io_chosen; // @[Arbiter.scala 37:23]
Arbiter arbiter ( // @[Arbiter.scala 37:23]
.io_in_0_ready(arbiter_io_in_0_ready),
.io_in_0_valid(arbiter_io_in_0_valid),
.io_in_0_bits(arbiter_io_in_0_bits),
.io_in_1_ready(arbiter_io_in_1_ready),
.io_in_1_valid(arbiter_io_in_1_valid),
.io_in_1_bits(arbiter_io_in_1_bits),
.io_out_ready(arbiter_io_out_ready),
.io_out_valid(arbiter_io_out_valid),
.io_out_bits(arbiter_io_out_bits),
.io_chosen(arbiter_io_chosen)
);
assign io_in_0_ready = arbiter_io_in_0_ready; // @[Arbiter.scala 38:17]
assign io_in_1_ready = arbiter_io_in_1_ready; // @[Arbiter.scala 38:17]
assign io_out_valid = arbiter_io_out_valid; // @[Arbiter.scala 39:10]
assign io_out_bits = arbiter_io_out_bits; // @[Arbiter.scala 39:10]
assign io_chosen = arbiter_io_chosen; // @[Arbiter.scala 40:13]
assign arbiter_io_in_0_valid = io_in_0_valid; // @[Arbiter.scala 38:17]
assign arbiter_io_in_0_bits = io_in_0_bits; // @[Arbiter.scala 38:17]
assign arbiter_io_in_1_valid = io_in_1_valid; // @[Arbiter.scala 38:17]
assign arbiter_io_in_1_bits = io_in_1_bits; // @[Arbiter.scala 38:17]
assign arbiter_io_out_ready = io_out_ready; // @[Arbiter.scala 39:10]
endmodule
Verilog代碼中生成了兩個module,第一個module Arbiter對應的是例化的優先仲裁器Arbiter,第二個module MyArbiter對應的是頂層子產品MyArbiter。
下面是一個例子,通過一個Arbiter的具體的輸入輸出資料的情況,來了解一下其工作邏輯:
test(new Module {
// Example circuit using a priority arbiter
val io = IO(new Bundle {
val in = Flipped(Vec(2, Decoupled(UInt(8.W))))
val out = Decoupled(UInt(8.W))
})
// Arbiter doesn't have a convenience constructor, so it's built like any Module
val arbiter = Module(new Arbiter(UInt(8.W), 2)) // 2 to 1 Priority Arbiter
arbiter.io.in <> io.in
io.out <> arbiter.io.out
}) { c =>
c.io.in(0).valid.poke(false.B)
c.io.in(1).valid.poke(false.B)
c.io.out.ready.poke(false.B)
println(s"Start:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
c.io.in(1).valid.poke(true.B) // Valid input 1
c.io.in(1).bits.poke(42.U)
c.io.out.ready.poke(true.B)
// What do you think the output will be?
println(s"valid input 1:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
c.io.in(0).valid.poke(true.B) // Valid inputs 0 and 1
c.io.in(0).bits.poke(43.U)
// What do you think the output will be? Which inputs will be ready?
println(s"valid inputs 0 and 1:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
c.io.in(1).valid.poke(false.B) // Valid input 0
// What do you think the output will be?
println(s"valid input 0:")
println(s"\tin(0).ready=${c.io.in(0).ready.peek().litValue}, in(1).ready=${c.io.in(1).ready.peek().litValue}")
println(s"\tout.valid=${c.io.out.valid.peek().litValue}, out.bits=${c.io.out.bits.peek().litValue}")
}
Elaborating design…
Done elaborating.
Start:
in(0).ready=0, in(1).ready=0
out.valid=0, out.bits=0
valid input 1:
in(0).ready=1, in(1).ready=1
out.valid=1, out.bits=42
valid inputs 0 and 1:
in(0).ready=1, in(1).ready=0
out.valid=1, out.bits=43
valid input 0:
in(0).ready=1, in(1).ready=0
out.valid=1, out.bits=43
test Helper_Anon Success: 0 tests passed in 2 cycles in 0.077434 seconds 25.83 Hz
沒有什麼需要特别說明的,因為它就是一個組合邏輯的子產品。
第二種仲裁器循環仲裁器
RRArbiter
也在chisel3.util包下面,并且隻定義了RRArbiter類,沒有單例對象,是以每次都需要通過new來建立RRArbiter對象。它的建立與調用方式和Arbiter是一樣的,隻是内部實作的仲裁邏輯不同。
三、隊列Queue
Chisel内建了隊列
Queue
,它會建立一個使用ready-valid接口 的FIFO,在chisel3.util包下面既定義了Queue類,也定義了其單例對象,是以有兩種建立Queue對象的方式。
Queue内部使用
QueueIO
定義端口,QueueIO最終仍然是使用
Decoupled()
建立所需的ready-valid接口,定義如下:
class QueueIO[T <: Data](private val gen: T, val entries: Int) extends Bundle
{
val enq = Flipped(EnqIO(gen))
val deq = Flipped(DeqIO(gen))
val count = Output(UInt(log2Ceil(entries + 1).W))
}
object EnqIO {
def apply[T<:Data](gen: T): DecoupledIO[T] = Decoupled(gen)
}
object DeqIO {
def apply[T<:Data](gen: T): DecoupledIO[T] = Flipped(Decoupled(gen))
}
-
是用來寫資料的端口,是以它和資料生産者producer連接配接;enq
-
是用來讀資料的端口,是以它和資料接收者consumer連接配接;deq
-
表示此時Queue中的資料個數。count
可以通過以下兩種形式使用Queue:
第一個參數是存儲的資料的類型,第二個參數是存儲的資料的深度。該方式傳回的是一個Queue對象,該對象包含QueueIO屬性,是以我們可以在代碼中通路QueueIO的
enq deq count
這三種端口信号。
第一個參數是
ReadyValidIO[T]
類型的端口,第二個參數是存儲的資料的深度,預設值為2。該方式傳回的是
DecoupledIO[T]
類型的讀資料端口,也即上述的deq,是以我們不能在代碼中通路enq和count。
以上兩種形式由于傳回的對象不一樣,是以在使用時也有一些不同,下面通過兩個例子分别展示一下這兩種形式的具體使用方法。
- 第一種形式的使用案例:
class MyQueue extends Module {
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val out = Decoupled(UInt(8.W))
val cnt = Output(UInt(4.W))
})
val q = Module(new Queue(UInt(8.W), entries = 16))
q.io.enq <> io.in
io.out <> q.io.deq
io.cnt := q.io.count
}
- 第二種形式的使用案例:
class MyQueue extends Module {
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val out = Decoupled(UInt(8.W))
})
val q = Queue(io.in, 2)
io.out <> q
}
上述兩段代碼都調用了Queue,是以在各自生成的verilog代碼中,會定義Queue對應的
module Queue
,該module會在頂層
module MyQueue
中被例化。兩者生成的module Queue的端口定義分别如下:
module Queue(
input clock,
input reset,
output io_enq_ready,
input io_enq_valid,
input [7:0] io_enq_bits,
input io_deq_ready,
output io_deq_valid,
output [7:0] io_deq_bits,
output [4:0] io_count
);
module Queue(
input clock,
input reset,
output io_enq_ready,
input io_enq_valid,
input [7:0] io_enq_bits,
input io_deq_ready,
output io_deq_valid,
output [7:0] io_deq_bits
);
可以看出,module Queue的端口中都有所需的兩對ready-valid握手信号,并且這兩對信号方向相反,這是因為它們分别是用來寫資料和讀資料的。
在第二種形式中,是不會有io_count端口的,因為我們無法使用QueueIO中的count。
此外,Queue對象的
empty和full
屬性我們也通路不到,但是由于在
class Queue
中有如下定義:
io.deq.valid := !empty
io.enq.ready := !Full
是以,我們就可以通過
io.deq.valid
和
io.enq.ready
間接地通路
empty
和
full
信号,通過這兩個信号來完成和
empty
和
full
信号有關的一些邏輯。
下面是一個例子,通過一個Queue(上述第二種使用形式)的具體的輸入輸出資料的情況,來了解一下其工作邏輯,注意它是一個時序子產品:
- 首先,先看下生成的verilog代碼
module Queue(
input clock,
input reset,
output io_enq_ready,
input io_enq_valid,
input [7:0] io_enq_bits,
input io_deq_ready,
output io_deq_valid,
output [7:0] io_deq_bits,
output [4:0] io_count
);
`ifdef RANDOMIZE_MEM_INIT
reg [31:0] _RAND_0;
`endif // RANDOMIZE_MEM_INIT
`ifdef RANDOMIZE_REG_INIT
reg [31:0] _RAND_1;
reg [31:0] _RAND_2;
reg [31:0] _RAND_3;
`endif // RANDOMIZE_REG_INIT
reg [7:0] ram [0:15]; // @[Decoupled.scala 218:16]
wire [7:0] ram_io_deq_bits_MPORT_data; // @[Decoupled.scala 218:16]
wire [3:0] ram_io_deq_bits_MPORT_addr; // @[Decoupled.scala 218:16]
wire [7:0] ram_MPORT_data; // @[Decoupled.scala 218:16]
wire [3:0] ram_MPORT_addr; // @[Decoupled.scala 218:16]
wire ram_MPORT_mask; // @[Decoupled.scala 218:16]
wire ram_MPORT_en; // @[Decoupled.scala 218:16]
reg [3:0] enq_ptr_value; // @[Counter.scala 60:40]
reg [3:0] deq_ptr_value; // @[Counter.scala 60:40]
reg maybe_full; // @[Decoupled.scala 221:27]
wire ptr_match = enq_ptr_value == deq_ptr_value; // @[Decoupled.scala 223:33]
wire empty = ptr_match & ~maybe_full; // @[Decoupled.scala 224:25]
wire full = ptr_match & maybe_full; // @[Decoupled.scala 225:24]
wire do_enq = io_enq_ready & io_enq_valid; // @[Decoupled.scala 40:37]
wire do_deq = io_deq_ready & io_deq_valid; // @[Decoupled.scala 40:37]
wire [3:0] _value_T_1 = enq_ptr_value + 4'h1; // @[Counter.scala 76:24]
wire [3:0] _value_T_3 = deq_ptr_value + 4'h1; // @[Counter.scala 76:24]
wire [3:0] ptr_diff = enq_ptr_value - deq_ptr_value; // @[Decoupled.scala 257:32]
wire [4:0] _io_count_T_1 = maybe_full & ptr_match ? 5'h10 : 5'h0; // @[Decoupled.scala 259:20]
wire [4:0] _GEN_8 = {{1'd0}, ptr_diff}; // @[Decoupled.scala 259:62]
assign ram_io_deq_bits_MPORT_addr = deq_ptr_value;
assign ram_io_deq_bits_MPORT_data = ram[ram_io_deq_bits_MPORT_addr]; // @[Decoupled.scala 218:16]
assign ram_MPORT_data = io_enq_bits;
assign ram_MPORT_addr = enq_ptr_value;
assign ram_MPORT_mask = 1'h1;
assign ram_MPORT_en = io_enq_ready & io_enq_valid;
assign io_enq_ready = ~full; // @[Decoupled.scala 241:19]
assign io_deq_valid = ~empty; // @[Decoupled.scala 240:19]
assign io_deq_bits = ram_io_deq_bits_MPORT_data; // @[Decoupled.scala 242:15]
assign io_count = _io_count_T_1 | _GEN_8; // @[Decoupled.scala 259:62]
always @(posedge clock) begin
if(ram_MPORT_en & ram_MPORT_mask) begin
ram[ram_MPORT_addr] <= ram_MPORT_data; // @[Decoupled.scala 218:16]
end
if (reset) begin // @[Counter.scala 60:40]
enq_ptr_value <= 4'h0; // @[Counter.scala 60:40]
end else if (do_enq) begin // @[Decoupled.scala 229:17]
enq_ptr_value <= _value_T_1; // @[Counter.scala 76:15]
end
if (reset) begin // @[Counter.scala 60:40]
deq_ptr_value <= 4'h0; // @[Counter.scala 60:40]
end else if (do_deq) begin // @[Decoupled.scala 233:17]
deq_ptr_value <= _value_T_3; // @[Counter.scala 76:15]
end
if (reset) begin // @[Decoupled.scala 221:27]
maybe_full <= 1'h0; // @[Decoupled.scala 221:27]
end else if (do_enq != do_deq) begin // @[Decoupled.scala 236:28]
maybe_full <= do_enq; // @[Decoupled.scala 237:16]
end
end
endmodule
module MyQueue(
input clock,
input reset,
output io_in_ready,
input io_in_valid,
input [7:0] io_in_bits,
input io_out_ready,
output io_out_valid,
output [7:0] io_out_bits,
output [3:0] io_cnt
);
wire q_clock; // @[Queue.scala 14:17]
wire q_reset; // @[Queue.scala 14:17]
wire q_io_enq_ready; // @[Queue.scala 14:17]
wire q_io_enq_valid; // @[Queue.scala 14:17]
wire [7:0] q_io_enq_bits; // @[Queue.scala 14:17]
wire q_io_deq_ready; // @[Queue.scala 14:17]
wire q_io_deq_valid; // @[Queue.scala 14:17]
wire [7:0] q_io_deq_bits; // @[Queue.scala 14:17]
wire [4:0] q_io_count; // @[Queue.scala 14:17]
Queue q ( // @[Queue.scala 14:17]
.clock(q_clock),
.reset(q_reset),
.io_enq_ready(q_io_enq_ready),
.io_enq_valid(q_io_enq_valid),
.io_enq_bits(q_io_enq_bits),
.io_deq_ready(q_io_deq_ready),
.io_deq_valid(q_io_deq_valid),
.io_deq_bits(q_io_deq_bits),
.io_count(q_io_count)
);
assign io_in_ready = q_io_enq_ready; // @[Queue.scala 16:12]
assign io_out_valid = q_io_deq_valid; // @[Queue.scala 17:10]
assign io_out_bits = q_io_deq_bits; // @[Queue.scala 17:10]
assign io_cnt = q_io_count[3:0]; // @[Queue.scala 18:10]
assign q_clock = clock;
assign q_reset = reset;
assign q_io_enq_valid = io_in_valid; // @[Queue.scala 16:12]
assign q_io_enq_bits = io_in_bits; // @[Queue.scala 16:12]
assign q_io_deq_ready = io_out_ready; // @[Queue.scala 17:10]
endmodule
- 然後,再看下測試案例
test(new Module {
// Example circuit using a Queue
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val out = Decoupled(UInt(8.W))
})
val queue = Queue(io.in, 2) // 2-element queue
io.out <> queue
}) { c =>
c.io.out.ready.poke(false.B)
c.io.in.valid.poke(true.B) // Enqueue an element
c.io.in.bits.poke(42.U)
println(s"Starting:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
c.io.in.valid.poke(true.B) // Enqueue another element
c.io.in.bits.poke(43.U)
// What do you think io.out.valid and io.out.bits will be?
println(s"After first enqueue:")
println(s"\tio.in: ready=${c.io.in.ready.peek().litValue}")
println(s"\tio.out: valid=${c.io.out.valid.peek().litValue}, bits=${c.io.out.bits.peek().litValue}")
c.clock.step(1)
c.io.in.valid.poke(true.B) // Read a element, attempt to enqueue
c.io.in.bits.poke(44.U)
c.io.out.ready.poke(true.B)
// What do you think io.in.ready will be, and will this enqueue succeed, and what will be read?
println(s"On first read:")
println(s"\tio.in: ready=${c.io.in.ready.peek()}")
println(s"\tio.out: valid=${c.io.out.valid.peek()}, bits=${c.io.out.bits.peek()}")
c.clock.step(1)
c.io.in.valid.poke(false.B) // Read elements out
c.io.out.ready.poke(true.B)
// What do you think will be read here?
println(s"On second read:")
println(s"\tio.in: ready=${c.io.in.ready.peek()}")
println(s"\tio.out: valid=${c.io.out.valid.peek()}, bits=${c.io.out.bits.peek()}")
c.clock.step(1)
// Will a third read produce anything?
println(s"On third read:")
println(s"\tio.in: ready=${c.io.in.ready.peek()}")
println(s"\tio.out: valid=${c.io.out.valid.peek()}, bits=${c.io.out.bits.peek()}")
c.clock.step(1)
}
Elaborating design…
Done elaborating.
Starting:
io.in: ready=1
io.out: valid=0, bits=0
After first enqueue:
io.in: ready=1
io.out: valid=1, bits=42
On first read:
io.in: ready=Bool(false)
io.out: valid=Bool(true), bits=UInt<8>(42)
On second read:
io.in: ready=Bool(true)
io.out: valid=Bool(true), bits=UInt<8>(43)
On third read:
io.in: ready=Bool(true)
io.out: valid=Bool(false), bits=UInt<8>(42)
test Helper_Anon Success: 0 tests passed in 7 cycles in 0.013966 seconds 501.23 Hz
關鍵在于上圖中的幾段代碼:
- 隻要非空,io_out_valid就為1,如果io_out_ready也為1,那麼do_deq就會為1,讀資料就被使能了;
- 隻要不滿,io_in_ready就為1,如果io_in_valid也為1,那麼do_enq就會為1,寫資料就被使能了;
- 讀寫要執行一下step才能完成;
- 隻要queue中有資料,那麼io_out_bits就會随時将此時指向的資料放在端口上,是以你會看到即使沒有使能讀資料,io_out_bits也會有值。