✎ 编 者 按
在 cocotb 里,通过将信号在 python 中进行合并的方式来对仿真进行提速。
从 cocotbext-axi 说起
对于一个 RTL 设计而言,无论什么功能,其接口均可以抽象为 valid-only(Flow)接口或者 valid-ready(stream)接口。那么在 cocotbext-axi 里,其已经提供了一套非常不错的处理机制。在之前的文章《cocotb——一文看懂 stream lib》中对于如何使用,已经有了相对详细的分析。对于 cocotb 而言,其仿真效率一直是一言难尽的。那么当我们的接口都抽象为 Flow 接口或者 Stream 接口时,对于其中的 payload 信号,是分开赋值驱动还是一并驱动,哪个效率相对而言更高一些呢?
这里做了两个 RTL demo:
case classAddDemo() extendsComponent{
val data_in=slave(Stream(Vec(UInt(8 bits),256)))
val data_out=master(Stream(Vec(UInt(8 bits),256)))
data_out<-<data_in.translateWith(Vec(data_in.payload.map(255-_)))
}
case classAddDemo1() extendsComponent{
val data_in=slave(Stream(Bits(8*256 bits)))
val data_out=master(Stream(Bits(8*256 bits)))
data_out<-<data_in.translateWith(data_in.payload.asBits.subdivideIn(8 bits).map(255-_.asUInt).asBits())
}
两个 demo 的输入、输出都是 Stream 接口,差别在于 AddDemo 中的 data_in、data_out 的 payload 为 256 个 8bit UInt、而对于 AddDemo1 中,其 data_in、data_out 的 payload 为一个完成的 2K 位宽的 UInt。
对于 AddDemo,在采用 cocotb 进行仿真时,其 driver、monitor 定义如下:
SimEnv 定义如下:
在 send_data 中,通过借用 python 的 setattr 方法对于每个 payload 进行赋值。
而对于 AddDemo1,其 paylaod 只有一个。
对于两个 Demo,进行相同的仿真 Case,分别输入 50000 组数据的输入与校验。最终仿真结果:
相较于采用数据分离的形式,采用数据合并的方式还是有不少的效率提升。
那么对于我们的仿真而言,完全可以采用将抽象后的借口的除 valid/ready 信号外均打包成一个信号。在 python 与仿真器之间进行交互时降低信号驱动/采样的交互次数。
lib 改造
在确定了上述的策略,那么我们可以对 cocotbext-axi 提供的 define_stream 函数进行改造。
首先,我们需要自己定义 StreamTransaction:
这里定义 PortTransaction 继承 StreamTransaction。_signals 用于定义总线限号 payload。此外,也定义了待封装解封装的信号。定义 pack 方法用于将 data 打包成要驱动的 payload 信号值,而 unpack 则用于将采样到的 payload 信号解析赋值到 data 中。
既然我们自己定义了 StreamTransaction,那么就需要对 define_stream 进行改造:
defdefine_stream1(name, signals,transaction, optional_signals=None, valid_signal=None, ready_signal=None, signal_widths=None):
all_signals = signals.copy()
if optional_signals isNone:
optional_signals = []
else:
all_signals += optional_signals
if valid_signal isNone:
for s in all_signals:
if s.lower().endswith('valid'):
valid_signal = s
if valid_signal notin all_signals:
signals += valid_signal
if ready_signal isNone:
for s in all_signals:
if s.lower().endswith('ready'):
ready_signal = s
else:
if ready_signal notin all_signals:
signals += ready_signal
if signal_widths isNone:
signal_widths = {}
if valid_signal notin signal_widths:
signal_widths[valid_signal] = 1
if ready_signal notin signal_widths:
signal_widths[ready_signal] = 1
filtered_signals = []
for s in all_signals:
if s notin (ready_signal, valid_signal):
filtered_signals.append(s)
attrib = {}
attrib['_signals'] = signals
attrib['_optional_signals'] = optional_signals
bus = type(name+"Bus", (StreamBus,), attrib)
attrib = {s: 0for s in filtered_signals}
attrib['_signals'] = filtered_signals
#transaction = type(name+"Transaction", (StreamTransaction,), attrib)
attrib = {}
attrib['_signals'] = signals
attrib['_optional_signals'] = optional_signals
attrib['_signal_widths'] = signal_widths
attrib['_ready_signal'] = ready_signal
attrib['_valid_signal'] = valid_signal
attrib['_transaction_obj'] = transaction
attrib['_bus_obj'] = bus
source = type(name+"Source", (StreamSource,), attrib)
sink = type(name+"Sink", (StreamSink,), attrib)
monitor = type(name+"Monitor", (StreamMonitor,), attrib)
return bus, source, sink, monitor
在定义 driver、monitor 时,则采用如下方式:
最终,在搭建 SimEnv 时,则可以做如下改造:
这里在 send_data 函数中,通过在 275 行调用 pack 将数据打包合并到待驱动的信号上。而在 scb 中,对于采样到的信号,则在 283 行通过 unpack 将信号解析出来。
如此,则能够达成我们的目的。
写在最后
相较于 SpinalHDL 而言,cocotb 的效率仍旧是差很多的。之前在仿真效对比和加速也有过两篇文章《给仿真加点速》、《既生瑜何生亮——SpinalHDL VS Cocotb》。由于在本地电脑上都是采用的 Verilator 进行仿真,在 SpinalHDL 上也做了一个简单类似的测试,但测试结果却是分开驱动仿真反倒比合并驱动采样的时间消耗要高一些。据说对于 Verilator 仿真器两者的使用方式略有差别,对于商业仿真器有待验证。
END
作者:玉骐
文章来源:Spinal FPGA
推荐阅读
更多 IC 设计干货请关注IC设计专栏。欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。