✎ 编 者 按
一文理清楚在 cocotb 中 stream 类型接口的 driver、monitor 是怎么运作的,如何运用这些库快速搭建自己的仿真平台。
lib 库安装
关于 stream 类型的总线,采用 valid,ready 握手形式进行传输。对于 stream 类型的握手信号处理,其时序处理逻辑一致,在 cocotb 中有一套完整的抽象接口。在 cocotbext-axi 库中,关于 stream 类型的总线定义了一整套完整的仿真框架,使用时通过 pip 安装:
pip3 install cocotbext-axi
仿真架构
有关 stream 类型接口的基础定义在 cocotbext.axi.stream 中定义,其基本架构如下:
- StreamBus:用于定义一组 stream 类型的总线
- StreamTransaction:类似于 uvm 中的 transaction,是 stream 进行数据传输的基本单元。
- Reset:用于处理复位相关信号
- StreamBase:Strean 基类
- StreamPause:用于生成接口反压
- StreamSource:作为 master 驱动 valid,data 至 dut
- StreamMonitor:监控 stream 总线的数据传输,生成 transaction
- StreamSink:具备 StreamMonitor 的功能,并作为 slave 侧驱动 ready 信号。
本文以一个 stream 输入 stream 输出的小 demo 为例,对上述这些做一个说明,rtl 代码如下:
// Generator : SpinalHDL v1.11.0 git head : 63852c61e498798f4e293594ce53fcb02c45eb6b
// Component : StreamDemo
module StreamDemo (
input wire data_in_valid,
output wire data_in_ready,
input wire [7:0] data_in_payload,
output wire data_out_valid,
input wire data_out_ready,
output wire [7:0] data_out_payload,
input wire clk,
input wire reset
);
wire data_in_s2mPipe_valid;
reg data_in_s2mPipe_ready;
wire [7:0] data_in_s2mPipe_payload;
reg data_in_rValidN;
reg [7:0] data_in_rData;
wire data_in_s2mPipe_m2sPipe_valid;
wire data_in_s2mPipe_m2sPipe_ready;
wire [7:0] data_in_s2mPipe_m2sPipe_payload;
reg data_in_s2mPipe_rValid;
reg [7:0] data_in_s2mPipe_rData;
wire when_Stream_l393;
assign data_in_ready = data_in_rValidN;
assign data_in_s2mPipe_valid = (data_in_valid || (! data_in_rValidN));
assign data_in_s2mPipe_payload = (data_in_rValidN ? data_in_payload : data_in_rData);
always @(*) begin
data_in_s2mPipe_ready = data_in_s2mPipe_m2sPipe_ready;
if(when_Stream_l393) begin
data_in_s2mPipe_ready = 1'b1;
end
end
assign when_Stream_l393 = (! data_in_s2mPipe_m2sPipe_valid);
assign data_in_s2mPipe_m2sPipe_valid = data_in_s2mPipe_rValid;
assign data_in_s2mPipe_m2sPipe_payload = data_in_s2mPipe_rData;
assign data_out_valid = data_in_s2mPipe_m2sPipe_valid;
assign data_in_s2mPipe_m2sPipe_ready = data_out_ready;
assign data_out_payload = data_in_s2mPipe_m2sPipe_payload;
always @(posedge clk or posedge reset) begin
if(reset) begin
data_in_rValidN <= 1'b1;
data_in_s2mPipe_rValid <= 1'b0;
end else begin
if(data_in_valid) begin
data_in_rValidN <= 1'b0;
end
if(data_in_s2mPipe_ready) begin
data_in_rValidN <= 1'b1;
end
if(data_in_s2mPipe_ready) begin
data_in_s2mPipe_rValid <= data_in_s2mPipe_valid;
end
end
end
always @(posedge clk) begin
if(data_in_ready) begin
data_in_rData <= data_in_payload;
end
if(data_in_s2mPipe_ready) begin
data_in_s2mPipe_rData <= data_in_s2mPipe_payload;
end
end
endmodule
StreamBus
在之前的文章《看一看 Bus 是怎么玩儿的### 中,对于 Bus 已有详细的介绍。StreamBus 集成 Bus,并无做过多扩展。StreamBus 中用于定义一组 stream 类型的总线,包含 valid、ready、data 等数据。StreamBus 中核心是定义我们 stream 总线中的数据信号:
如我们上面的例子,我们可以继承 StreamBus 定义一个 PortBus:其中_signals=["valid","ready","payload"]
StreamTransaction
StreamTransaction 类似于 UVM 中的 transaction,用于 stream 传输数据的生成,其核心也为信号的定义:
我们这里传输的数据只有 payload,故我们也可以自己定义一个 PortTransaction,继承于 StreamTransaction,将_signals 定义为_signals=["payload"]。
Reset
Reset 主要用于复位信号处理,由于复位信号一般集中处理,这里无需展开
StreamBase
StreamBase 为 Stream 处理的基类,不会直接使用,其定义了 Stream 处理的基础元素,主要负责在仿真启动时信号的初始化:
对于 ready/valid 信号,如果存在,并且允许对其赋初始值,将会对 ready/valid 赋初始值。对于 stream 中的其他信号,则根据使能项也可以配置为 x 态。此外,StreamBase 也提供了一些基本方法:
- count():统计还在 pending 待处理的任务
- empty():若没有待处理的任务,则返回 True
- clear():类似于对组件进行清除所有状态
StreamPause
StreamPause 主要用于辅助生成是否要暂停传输供 StreamSource、StreamSink 使用:
在使用时可以主要通过其提供的 set_pause_generator 传入一个 pause 的迭代函数:
每当我们通过该函数设置 pause 的生成函数时,会将先前已注册的生成器给 kill 掉,随后拉起一个新的协程运行_run_pause:
可以看到_run_pasue 路基会在每个时钟周期都会调用我们传入的 generator,其返回值赋值给 pause。这里我们传入的 generator 应当返回 0 或者 1。
如此,我们可以自己定义一个 pause 生成类:
stream_pause 通过随机数生成器生成制定流量大小的反压率。
StreamSource
StreamSource 用于向 dut 灌入激励,驱动 valid 和 payload。其继承于 StreamBase、StreamPause。其做好了底层时许接口的封装,向 stream 总线发数据,其提供了两个 API:
两个函数在调用时 obj 均为我们先前定义的 PortTransaction。send 函数会等待 queue(用于存放待发送任务 Transaction)非满后压入,在调用时需 await 等待。而 send_nowait 则不会等,直接写入,但如果 queue 满时将会报错。
StreamSource 默认 queue_occupancy_limit 为-1,即不限制 queue 的深度,如果需要限制,则需在例化使用时修改该参数。
此外,其也提供了几个辅助函数:
- full():判断 queue 是否满
- idle():判断当前 streamSource 是否处于空闲状态(接口上无任务传输,且 queue 为空)
- wait():用于等待 StreamSource 排空所有任务
其在仿真期间运行函数为:
其逻辑很简单,当 queue 有任务时且 pause 为 False 时才会向总线上发起数据传输(pause 来源于 StreamPause)。值得注意的是 StreamSource 不仅适用于 valid-ready 型,也适用于 valid-only 型接口
StreamMonitor
StreamMonitor 继承于 StreamBase,其用途为监控 stream 上的有效传输,并不做信号的驱动:
每当检测到一个有效数据传输时,其会调用 Bus 中的 sample 函数生成一个 Transaction 对象,并压入 queue 中。在 case 中接受数据时其提供了两个 API:
recv 用于从 queue 中获取一个任务,当 queue 为空时将会等待,而 recv_nowait 则直接从 queue 中获取,但如果 queue 为空则会报错。
StreamSink
StreamSink 则继承于 StreamMonitor 和 StreamPause。其和 StreamMonitor 相比,多了一个驱动 ready 信号的功能:
在代码 406~407,会对 ready 进行赋,ready 信号值会收到 queue 是否满以及 Stream 中的 pause 信号共同决定
代码是如何运行起来的
在上面的代码中 StreamSource、StreamMonitor、StreamSink 在运行过程中都是运行的_run 函数,那么他们在例化后是怎么运行起来的,我们以 StreamSource 为例,其余均相同。
在 StreamSource 的初始化函数中:
其调用了其父类 StreamBase 的初始化函数。StreamBase 继承于 Reset,StreamBase 在其__init__函数中调用了 Reset 中的_init_reset 函数:
在例化时 reset_signal、active_level 均为 None,其会执行_update_reset 函数:
_update_reset 函数中调用_handle_reset 函数,其传入的 new_state 为 False。_handle_reset 函数在 StreamBase 中被重写:
由于传入的 state 为 False,故会执行 157 行,拉起一个协程来运行_run 函数。
stream_define
运行一个 stream 仿真,其需要定义一个 StreamBus、StreamTransaction、StreamSource、StreamSink、StreamMonitor。lib 中提供了一种便捷定义的形式 stream_define:
我们在使用时只需要传入 name、signals 即可生成上面的这些:
demo
这里给出一个完整的 demo、配合上面的 rtl 代码:
#!/usr/bin/python3
from cocotbext.axi.stream import *
from cocotb.handle import *
from cocotb.clock import Clock
from cocotb.triggers import RisingEdge,ClockCycles
from cocotb.queue import Queue
from random import randint
#==========================================================================================
# Stream Driver Inst
#==========================================================================================
PortBus,PortTransaction,PortMaster,PortSlave,PortMonitor=define_stream(
name="Port",
signals=["payload","valid","ready"]
)
#==========================================================================================
# stream pause
#==========================================================================================
class StreamPause(object):
def __init__(self):
self.flow_percent=100
def stream_pause(self):
while True:
yield randint(0,100) > self.flow_percent
#==========================================================================================
# SimEnv
#==========================================================================================
class SimEnv(object):
def __init__(self,dut:SimHandleBase):
self.dut=dut
self.clk=dut.clk
self.clk_sample=RisingEdge(self.clk)
self.port_in_mst=PortMaster(bus=PortBus(entity=self.dut, prefix="data_in"),clock=self.dut.clk)
print(self.port_in_mst.bus._signals)
self.port_out_slv=PortSlave(bus=PortBus(entity=self.dut, prefix="data_out"),clock=self.dut.clk)
self.port_in_pause=StreamPause()
self.port_out_pause=StreamPause()
self.port_in_mst.set_pause_generator(self.port_in_pause.stream_pause())
self.port_out_slv.set_pause_generator(self.port_out_pause.stream_pause())
self.clk_th=cocotb.start_soon(Clock(signal=dut.clk,period=4,units='ns').start())
self.refQueue=Queue()
async def start(self):
await self._reset()
self.scb_th=cocotb.start_soon(self.scb())
async def wait(self):
await self.port_in_mst.wait()
await self.port_out_slv.wait()
async def _reset(self):
self.dut.reset.value = 1
await ClockCycles(signal=self.clk,num_cycles=10)
self.dut.reset.value = 0
async def send_data(self,data):
port_trans=PortTransaction()
ref_trans=PortTransaction()
print(ref_trans._signals)
ref_trans.payload=data
port_trans.payload=data
self.refQueue.put_nowait(ref_trans)
await self.port_in_mst.send(port_trans)
async def scb(self):
while True:
dut_result=await self.port_out_slv.recv()
ref_result=self.refQueue.get_nowait()
assert dut_result.payload == ref_result.payload
#==========================================================================================
# TestCase
#==========================================================================================
@cocotb.test()
async def test0(dut):
simEnv=SimEnv(dut=dut)
await simEnv.start()
simEnv.port_in_pause.flow_percent=50
simEnv.port_out_pause.flow_percent=50
for index in range(200):
await simEnv.send_data(index)
cocotb.log.info("wait done")
await simEnv.wait()
Makefile 文件:
TOPLEVEL_LANG = verilog
VERILOG_SOURCES = $(shell pwd)/StreamDemo.sv
TOPLEVEL = StreamDemo
MODULE = test
EXTRA_ARGS += --trace --trace-fst --trace-structs
include $(shell cocotb-config --makefiles)/Makefile.sim
END
作者:玉骐
文章来源:Spinal FPGA
推荐阅读
更多 IC 设计干货请关注IC设计专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。