棋子 · 3月13日

cocotb——一文看懂 stream lib

✎ 编 者 按
一文理清楚在 cocotb 中 stream 类型接口的 driver、monitor 是怎么运作的,如何运用这些库快速搭建自己的仿真平台。

lib 库安装

关于 stream 类型的总线,采用 valid,ready 握手形式进行传输。对于 stream 类型的握手信号处理,其时序处理逻辑一致,在 cocotb 中有一套完整的抽象接口。在 cocotbext-axi 库中,关于 stream 类型的总线定义了一整套完整的仿真框架,使用时通过 pip 安装:

pip3 install cocotbext-axi

仿真架构

有关 stream 类型接口的基础定义在 cocotbext.axi.stream 中定义,其基本架构如下:

image.png

  • 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 总线中的数据信号:

Image

如我们上面的例子,我们可以继承 StreamBus 定义一个 PortBus:其中_signals=["valid","ready","payload"]

StreamTransaction

StreamTransaction 类似于 UVM 中的 transaction,用于 stream 传输数据的生成,其核心也为信号的定义:

Image

我们这里传输的数据只有 payload,故我们也可以自己定义一个 PortTransaction,继承于 StreamTransaction,将_signals 定义为_signals=["payload"]。

Reset

Reset 主要用于复位信号处理,由于复位信号一般集中处理,这里无需展开

StreamBase

StreamBase 为 Stream 处理的基类,不会直接使用,其定义了 Stream 处理的基础元素,主要负责在仿真启动时信号的初始化:

Image

对于 ready/valid 信号,如果存在,并且允许对其赋初始值,将会对 ready/valid 赋初始值。对于 stream 中的其他信号,则根据使能项也可以配置为 x 态。此外,StreamBase 也提供了一些基本方法:

  • count():统计还在 pending 待处理的任务
  • empty():若没有待处理的任务,则返回 True
  • clear():类似于对组件进行清除所有状态

StreamPause

StreamPause 主要用于辅助生成是否要暂停传输供 StreamSource、StreamSink 使用:

Image

在使用时可以主要通过其提供的 set_pause_generator 传入一个 pause 的迭代函数:

Image

每当我们通过该函数设置 pause 的生成函数时,会将先前已注册的生成器给 kill 掉,随后拉起一个新的协程运行_run_pause:

Image

可以看到_run_pasue 路基会在每个时钟周期都会调用我们传入的 generator,其返回值赋值给 pause。这里我们传入的 generator 应当返回 0 或者 1。

如此,我们可以自己定义一个 pause 生成类:

Image

stream_pause 通过随机数生成器生成制定流量大小的反压率。

StreamSource

StreamSource 用于向 dut 灌入激励,驱动 valid 和 payload。其继承于 StreamBase、StreamPause。其做好了底层时许接口的封装,向 stream 总线发数据,其提供了两个 API:

Image

两个函数在调用时 obj 均为我们先前定义的 PortTransaction。send 函数会等待 queue(用于存放待发送任务 Transaction)非满后压入,在调用时需 await 等待。而 send_nowait 则不会等,直接写入,但如果 queue 满时将会报错。

StreamSource 默认 queue_occupancy_limit 为-1,即不限制 queue 的深度,如果需要限制,则需在例化使用时修改该参数。

此外,其也提供了几个辅助函数:

  • full():判断 queue 是否满
  • idle():判断当前 streamSource 是否处于空闲状态(接口上无任务传输,且 queue 为空)
  • wait():用于等待 StreamSource 排空所有任务

其在仿真期间运行函数为:

Image

其逻辑很简单,当 queue 有任务时且 pause 为 False 时才会向总线上发起数据传输(pause 来源于 StreamPause)。值得注意的是 StreamSource 不仅适用于 valid-ready 型,也适用于 valid-only 型接口

StreamMonitor

StreamMonitor 继承于 StreamBase,其用途为监控 stream 上的有效传输,并不做信号的驱动:

Image

每当检测到一个有效数据传输时,其会调用 Bus 中的 sample 函数生成一个 Transaction 对象,并压入 queue 中。在 case 中接受数据时其提供了两个 API:

Image

recv 用于从 queue 中获取一个任务,当 queue 为空时将会等待,而 recv_nowait 则直接从 queue 中获取,但如果 queue 为空则会报错。

StreamSink

StreamSink 则继承于 StreamMonitor 和 StreamPause。其和 StreamMonitor 相比,多了一个驱动 ready 信号的功能:

Image

在代码 406~407,会对 ready 进行赋,ready 信号值会收到 queue 是否满以及 Stream 中的 pause 信号共同决定

代码是如何运行起来的

在上面的代码中 StreamSource、StreamMonitor、StreamSink 在运行过程中都是运行的_run 函数,那么他们在例化后是怎么运行起来的,我们以 StreamSource 为例,其余均相同。

在 StreamSource 的初始化函数中:

Image

其调用了其父类 StreamBase 的初始化函数。StreamBase 继承于 Reset,StreamBase 在其__init__函数中调用了 Reset 中的_init_reset 函数:

Image

在例化时 reset_signal、active_level 均为 None,其会执行_update_reset 函数:

Image

_update_reset 函数中调用_handle_reset 函数,其传入的 new_state 为 False。_handle_reset 函数在 StreamBase 中被重写:

Image

由于传入的 state 为 False,故会执行 157 行,拉起一个协程来运行_run 函数。

stream_define

运行一个 stream 仿真,其需要定义一个 StreamBus、StreamTransaction、StreamSource、StreamSink、StreamMonitor。lib 中提供了一种便捷定义的形式 stream_define:

Image

我们在使用时只需要传入 name、signals 即可生成上面的这些:

Image

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)加入技术交流群,请备注研究方向。

推荐阅读
关注数
21743
内容数
1339
主要交流IC以及SoC设计流程相关的技术和知识
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息