0%

Overview

我们在调用memory的时候,通常使用如下code

1
val mem = Mem(Bits(32 bit), 1024)

其生成的Verilog,通常是reg array的形式,以模仿regfile或者SRAM的行为。在FPGA开发里面,可以用工具自动推断BRAM。而另外一种方式,则是将生成的Memory作为一个blackbox。

Standard Mem Black Boxer

Spinal提供标准的黑盒方法,使用方法:

1
2
3
SpinalConfig()
.addStandardMemBlackboxing(blackboxAll) //黑盒策略
.generateVhdl(new TopLevel)

黑盒策略有以下几种

Policy Kind Description
blackboxAll Blackbox all memory.Throw an error on unblackboxable memory
blackboxAllWhatsYouCan Blackbox all memory that is blackboxable
blackboxRequestedAndUninferable Blackbox memory specified by the user and memory that is known to be uninferable (mixed-width, …).Throw an error on unblackboxable memory
blackboxOnlyIfRequested Blackbox memory specified by the userThrow an error on unblackboxable memory

除开上面的标准黑盒方法,我们可以自定义自己的黑盒方法,以例化fab-specific SRAM。

Customized Black Boxer

基本类

Phase中涉及到mem blackbox flow的phase uml类图如下:

上图中,红色字体标识的类是我们自定义的Black boxer,用以实现SRAM的例化。

流程

下图展示了Memory黑盒化的流程图。

蓝色框框是我们自定义黑盒化方法的地方。有几个例子,一个是在 spinal.core.internals.Phase 内的 PhaseMemBlackBoxingDefault 内,另一个例子是NaxRiscv里的 XOR-based SRAM。

TODO
TODO

Overview

SpinalHDL存在大量的零碎的feature,很不起眼但是非常有用。 core 模块和 lib 模块均包含有 util ,这里略微总结不在官方文档内的比较有用的功能。

隐式转换

从Scalal原生类型隐式转换到Spinal的硬件类型,并提供硬件描述相关功能。抑或是对一些原有类型的扩展。

Seq到Vec

import spinal.lib._

class TraversableOncePimped[T <: Data](pimped: Seq[T])

  • 提供类似于Vec使用UInt访问内部数据的功能
  • 提供 reduceBalanceTree
  • 提供 asBits
  • 提供简单的集合查找和计数功能:
    • def sExist(condition: T => Bool): Bool = (pimped map condition).fold(False)(_ || _)
    • def sContains(value: T) : Bool = sExist(_ === value)
    • def sFindFirst(condition: T => Bool) : (Bool,UInt)
    • def sCount(condition: T => Bool): UInt = SetCount((pimped.map(condition)))
    • def sCount(value: T): UInt = sCount(_ === value)

特殊的, Seq[Bool] 还提供 orR, andR, xorR 功能,将内部的Boolean全部与或非起来。

更基本的类 class TraversableOnceAnyPimped[T <: Any](pimped: Seq[T]) 支持 whenIndexed 功能,

1
2
3
4
5
6
7
8
def apply(id : UInt)(gen : (T) => Unit): Unit ={
assert(widthOf(id) == log2Up(pimped.size))
for((e,i) <- pimped.zipWithIndex) {
when(id === i){
gen(e)
}
}
}

Tuple到Bits

import spinal.core._

class TuplePimperBase(product: Product)

提供类似于Verilog位拼接的功能,如:

1
2
3
assign {cout, s} = {a, b, c};
||
(cout, s) := (a, b, c).asBits

字符串转Vec[Bits]

1
val avec = "0123abcd".toVecOfByte

Stream、Flow及Fragment自动转成其payload

需要 import DataCarrier._

不在需要引用 payload 来获得其payload。

时钟域的扩展

提供异步复位同步释放的扩展,可以从异步复位时钟域经由同步器获得新的同步释放时钟域

def withBufferedResetFrom(resetCd : ClockDomain, bufferDepth : Int = BufferCC.defaultDepth.get) : ClockDomain

对Seq的when

  • whenMasked
    1
    2
    3
    whenMasked(avec, amask: Bits) {
    active(sth)
    }
  • whenIndexed
    1
    2
    3
    whenIndexed(avec, aindex: UInt) {
    active(sth)
    }

带优先级的Mux

1
2
3
4
5
6
7
8
9
10
11
object PriorityMux{
def apply[T <: Data](in: Seq[(Bool, T)]): T = {
if (in.size == 1) {
in.head._2
} else {
Mux(in.head._1, in.head._2, apply(in.tail)) //Inttelij right code marked red
}
}
def apply[T <: Data](sel: Seq[Bool], in: Seq[T]): T = apply(sel zip in)
def apply[T <: Data](sel: Bits, in: Seq[T]): T = apply(sel.asBools.zip(in))
}

延迟的标志信号 DelayEvent

可以将一个标志信号延时多个cycle。(对这一flag直接 Delay 不是不行,只是在延迟的cycle数太大时过于浪费寄存器资源)存在两个控制延迟的方式,

  • def apply(event: Bool, cycle: BigInt): Bool
  • def apply(event: Bool, cycle: UInt): Bool

计数器扩展

可增减计数器,多请求计数器。

  • CounterUpDown 提供 decrement 方法减一
  • 多请求:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    object CounterMultiRequest {
    def apply(width: Int, requests : (Bool,(UInt) => UInt)*): UInt = {
    val counter = Reg(UInt(width bit)) init(0)
    var counterNext = cloneOf(counter)
    counterNext := counter
    for((cond,func) <- requests){
    when(cond){
    counterNext \= func(counterNext)
    }
    }
    counter := counterNext
    counter
    }
    }

toBoolean() 函数

waitUntil(cond), cond不能是函数的返回值的toBoolean

1
2
3
4
waitUntil(axi4.aw.fire.toBoolean) // Wrong
//Exception in thread "main" java.util.NoSuchElementException: next on empty iterator

waitUnitl(axi4.aw.valid.toBoolean && axi4.aw.ready.toBoolean) // Correct

亦或者说,由于fire的定义如下:

1
def fire: Bool = aw.valid & aw.ready

因此,不能对这一无括号的函数使用toBoolean函数。

软件的归软件,硬件的归硬件

在Testbench里,不能调用那些用于做硬件描述的函数,比如上面的 fire() 函数,这个函数的本质是一段硬件描述,只能出现在component里面。再比如对Bits,UInt类等等,索引它的某一位,这一段依旧是硬件描述,不能出现在component之外的地方,比如testbench中。

我们只能对已经定义好的变量,使用 toBoolean() 这样的函数将其代表的数值结果读取到软件域。这一点是它和Verilog这类HVL所完全不同的特点。

waitUnitl()

类似于sv中的 @(event) 等待某个事件的触发。

大部分时候并不需要使用到这个函数。通常使用的是

1
clockDomain.waitActiveEdgeWhere(cond :=> Boolean)

这个函数的语义:一直等待到cond成立,并在成立后的第一个active edge退出等待。

所有的wait函数,只能在thread中使用,不能再callback中使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// right
fork {
dut.clockDomain.waitActiveEdge(3)
}

// right
fork {
waitUntil(row_addr == fft_config.row)
}

// wrong
dut.clockDomain.onSamplings {
dut.clockDomain.waitActiveEdge(3)
}

等待某个延时 #10ps

可以使用函数sleep()

1
sleep(10)

fork

尽量不在 clockDomain.onsampling {…} 内部使用 fork

使用toX()函数,将硬件域的数据转换至软件域

做定点数相关的转换时,注意数据类型本身的位宽问题。比如

1
2
val a_sint = 1 << bitCounts // when bit counts < 32 
val a_slong = 1L << bitCounts // when 32 <= bit counts < 64

性能问题

SpinalSim使用Verilator仿真,分为三个阶段:

  1. verilate阶段:将Verilog文件翻译为cpp
  2. compile阶段:将cpp文件加上testbench的cpp文件编译为可执行文件
  3. simulation阶段:运行可执行文件,开始仿真过程

对于大设计,会导致以上三个阶段的运行速度不合理的慢,因此需要合理优化

1 Verilate阶段

数量巨大的register,以及Mux的sel信号的位宽太大(两位数)以及其他的一些问题可能会导致这一阶段所消耗的时间太长。

生成的Verilog网表大小太大也会导致这一阶段时间冗长。

解决办法:1. 改变你的电路结构,规避上面的问题。这些问题往往意味设计的不合理性。2. 使用层次化的verilate流程,但是spinalsim对此没有支持,不好做。

2 Compile阶段

默认情况下,spinalsim使用g++进行多线程编译,因此这一阶段的运行时间与工作站的CPU核数密切相关。尽量在多核系统上跑。

对于可能需要重复多次编译同样设计的问题,安装ccache,并将环境变量OBJCACHE指向ccache。这样可以缓存上次编译过的cpp内容,有效避免重复编译。

3 simulation阶段

默认情况下,spinalsim不会使用verilator的多线程仿真。需要使用simulation flag: --threads <cpu_num> --trace-threads <cpu_num> 。具体设置如下:

1
SimConfig.addSimulationFlag("--threads 32 --trace-threads 32")

这样可以使仿真可执行文件以多线程的形式运行。

时钟域的例化和管理通常发生在top level,时钟信号和复位信号的互联在顶层或者次顶层。

Spinal中提供了ClockDomain类来建模和管理不同的时钟域。

一个系统如下:

https://wswslzp.github.io/2022/03/04/spinal/clockdomain/clockdomain.png

系统内部包含一个外部时钟和三个内部时钟域。外部时钟域,即时钟源在片外,比如片外晶振,其产生的时钟信号通过IO pad连入chip,作为片内PLL的时钟输入,或者直接用。内部时钟域则是时钟源在片内产生的,比如PLL产生的时钟信号。

时钟域的例化

外部时钟域

1
2
// 例化一个前缀为“ref”的外部时钟域
val ref_clk_dom1 = ClockDomain.external(name = "ref")

如上,例化了一个外部时钟域。它会在模块的顶层产生对应的前缀的时钟信号和复位信号,作为chip的输入端口。我们可以直接使用ClockingArea来给我们的设计定义为这一时钟域,也可以使用 mapClockDomain(<ext_clk_dom>, clock=<driven_clk>, reset = <driven_reset>) 来让外部时钟域定义的时钟信号和复位信号,驱动IP定义的时钟信号和复位信号。

1
2
3
4
5
6
7
8
9
10
11
val iopll = IOPLL()
val por = POR()

// 不能直接调用外部时钟域的clock、reset去驱动内部的时钟,只能通过mapClockDomain的方法
iopll.mapClockDomain(ref_clk_dom1, clock = iopll.refclk) // OK
iopll.refclk := ref_clk_dom1.clock // ERROR!

// 或者使用 ClockingArea 包住用户的逻辑
val ref_clk_area = new ClockingArea(ref_clk_dom1){
...
}

内部时钟域

1
2
3
4
5
6
// 例化一个前缀为“sys”的内部时钟域
val sys_clk_dom1 = ClockDomain.internal(name = "sys")

// 将内部时钟源和内部时钟域绑定起来
sys_clk_dom1.clock := iopll.outclk0
sys_clk_dom1.reset := por.ninit_done

与外部时钟域不同,内部时钟域的时钟信号和复位信号都由chip内部产生,需要用户自己定义内部时钟源和内部时钟域的连接关系。

它的使用也和外部时钟域类似,主要也是两种:

  • 使用 mapClockDomain 函数,将时钟域和IP的时钟域绑定
  • 使用 ClockingArea,将时钟域和用户逻辑绑定

时钟域的绑定

Component的类内自带一个默认的 clockDomain ,其自动绑定到下面的时序单元。

通过上述的方法例化的时钟域,类似于入栈,压在默认时钟域上方,这些时钟域对应的 ClockingArea 中的用户逻辑,包括 ComponentBlackBox 都会使用相应的栈顶的时钟域而不是栈底的时钟域。

用户逻辑的绑定

两种方式,一种直接将逻辑放在对应的时钟域内:

1
2
3
4
val aclk = ClockDomain.external("a")
aclk {
// some logic here
}

缺点在于,内部逻辑是匿名的,无法被外部访问。声明只能都放在外面。适用于toplevel的连接。优点是简洁,不易产生cdc问题。

另外一种,声明一个 ClockingArea ,包住一个时钟域。随后逻辑放在Area内部。外面的逻辑可以直接访问内部逻辑。缺点是可能有cdc问题(人为失误)。另外Spinal提供预设的三种Clocking Area:

  • SlowArea 分频时钟域,该Area下的时钟域是当前时钟域的一个整数分频
  • ResetArea 复位域,该Area下的时钟域与当前时钟域共享一个时钟,但是引入新的复位信号。原复位信号是否起作用,可以选择, cumulative 为true则起作用
  • ClockEnableArea 门控时钟域,该Area下的时钟共享当前时钟域的时钟,并引入一个新的信号作为门控信号。

IP的绑定

通过 BlackBox 的方法: mapClockDomainmapCurrentClockDomain 。传参有:

  • clockDomain: 用作驱动的时钟域(mapCurrentClockDomain 没有这一参数,它使用的就是当前时钟域,即IP被例化的位置所拥有的时钟域)
  • clock: 被驱动的时钟信号
  • reset:被驱动的复位信号
  • enable:被驱动的时钟门控信号

Overview

Instance

从DSL层面,一个component内包含两种语句statement

  • Declaration:声明,对应于出现的各个Data
  • Assignment:赋值,除了声明Data、子模块外的,所有语句均作为表达式的容器 ExpressionContainer
    • 逻辑计算
    • 连接
      • 模块端口连接
      • 寄存器连接
  • Tree:控制流
    • When
    • Switch
  • 断言:

语句和实际写在scala里的一个语句不同。Scala语句,往往嵌套多个Spinal语句。

语句作为容器,装载不同类型的表达式 Expression

  • 数据:直接对应表达式中涉及的Data
  • 操作符:逻辑计算的各种一元、二元操作符 Operator;操作符同样是 表达式容器,包含其涉及的操作数。

Operation

遍历

  • foreach,遍历容器内的元素,但只迭代一级
  • walk,迭代至最底层

置换

  • remap,将一个Expression,通过一个function映射到新的一个Expression

删除

  • remove

语句容器 ScopeStatement

每个语句都处于一个 parentScope 中,这样一个Scope,以 ScopeStatement 的形式出现。

定义:class ScopeStatement(var parentStatement: TreeStatement)

每个Component都有一个 dslBody 作为其Scope,容纳Statement。每个 dslBodyparentStatement 都是null,因为其直接是模块第一层容器。Scope也包含一个指向其Component的引用 component

WhenSwitch 语句也可以张开一个Scope,在 dslBody 的基础上。相当于入栈一个新的Scope。

Scope以环形链表来组织其内部的语句,每个scope有一个 headlast 指针,指向链表头尾两条语句。其以 prependappend 的方法来插入语句。语句作为链表的节点,有 nextScopeStatementlastScopeStatement 两个引用来指向其上一个和下一个节点语句。语句的 parentScope 指向自己所属的Scope。

当一条语句写在一个Scope内时,其首先会把自己加入到当前Scope中。如BaseType,就是这么做的

1
2
3
4
DslScopeStack.get match {
case null =>
case scope => scope.append(this) //当你声明一个数据 自动加入栈顶Scope
}

Scope提供迭代语句的迭代器方法,以及foreach和walk的迭代方法。进一步,可以只迭代 DeclarationStatement

表达式容器

仅仅提供容器基本操作。

一级遍历

遍历内部表达式,仅仅一级遍历,不会对内部表达式做进一步遍历。

1
2
def foreachExpression(func: (Expression) => Unit): Unit
def foreachDrivingExpression(func: (Expression) => Unit): Unit = foreachExpression(func)

全部遍历

遍历内部表达式,直至最底层。

def walkExpression(func: (Expression) => Unit): Unit

def walkExpressionPostorder(func: (Expression) => Unit): Unit

def walkDrivingExpressions(func: (Expression) => Unit): Unit

置换元素

给一个置换函数,可以将容器内的表达式根据函数置换为新的表达式。

1
2
def remapExpressions(func: (Expression) => Expression): Unit
def remapDrivingExpressions(func: (Expression) => Expression): Unit = remapExpressions(func)

遍历置换

遍历的同时,做置换。

def walkRemapExpressions(func: (Expression) => Expression): Unit

def walkRemapDrivingExpressions(func: (Expression) => Expression): Unit

表达式

Expression 定义,继承BaseNode,一个表达式自己也是表达式容器。

trait Expression extends BaseNode with ExpressionContainer

覆写了foreachDrivingExpression

数据

这里的数据,不是 Data

数字信号 BaseType

即Spinal最基本,最单元的几种数据类型,Bool、BitVector、枚举、结构体

注意到,数字信号本身即是表达式,也是一条声明语句。

模拟信号 AnalogDriver

包含一个data expression,一个enable expression。因此遍历和置换操作就发生在这两个表达式上。

字面值 Literal

各种数据类型的字面值。数字信号的字面量。表达式遍历和置换操作被取消。

修饰符 Modifier

是Expression 的别名,包括下面两种:

类型转换, Cast

在不同的基本数据类型之间做类型转换。包含一个 input ,遍历和置换发生在input

操作符Operator

也是Expression 的别名。进一步分为:

  • 一元,常数:两个实现基本一样,只包含一个 source 表达式。
  • 二元:包含 leftright 两个子表达式。

多选符Multiplexer

包含一个选择表达式 select 和待选的表达式数组 inputs

二选符BinaryMultiplexer

包含一个条件 cond , 以及两个带选项, whenTruewhenFalse

访问符SubAccess

取出bitvector中的一个bitfiled,其首先包含被访问的 source 。和赋值类似,访问操作可以是静态的和动态的,可以访问一个bit或者一段bitfield。

  • 静态访问 Fixed,访问的位置固定
    • Bit access:包含固定整数 bitId
    • Range access:包含固定整数 hi, lo
  • 动态访问 Floating,访问的位置由表达式指定
    • Bit access: 包含 bitId 表达式
    • Range access:包含 offset 表达式,以及固定整数 size

位宽 Resize

不同数据类型的位宽调整。包含被调节位宽的 input

赋值AssignmentExpression

赋值分为两种,静态索引和动态索引。BitVectorAssignmentExpression直接 继承 AssignmentExpression ,包含被赋值的数据 out 。因此迭代映射均作用在 out 上面。而对 driving expression的处理则被取消。

  • Fixed,除开包含 out
    • BitAssignmentFixed, 包含一个index来选择一个bit
    • RangedAssignmentFixed, 包含hi和lo来选择一个bit field
  • Floating,除开包含 out 外,
    • BitAssignmentFloating, 还包含 bitId 表达式用来索引一个单独的bit
    • RangedAssignmentFixed, 还包含 offset表达式用来索引一个范围

语句

Statement定义

trait Statement extends ExpressionContainer with ContextUser with ScalaLocated with BaseNode

方法:删除自己

语句安排为两个分类,TreeStatementLeafStatement

Leaf

直接继承Statement。几种最底层的语句,

  • DeclarationStatement, 数据节点

数据节点对foreach、remap子表达式的操作进行了覆写,这两种操作被无效了,因为它们没有子表达式。

  • AssignmentStatement ,赋值连接

赋值包含 targetsource 两个表达式。

  • Memory Port,读写端口,伪数据节点

Tree

继承Statement,但自己同样包含语句的容器 ScopeStatement,区分为两种

  • WhenStatement, 包含两个语句子容器 whenTruewhenFalse ,以及作为条件表达式的 cond
  • SwitchStatement,包含 value 表达式,以及多个 SwitchStatementElement 。每个 SwitchStatementElement 包含一个 key 表达式,和一个语句子容器 scopeStatement

Tree意味着包含子tree,最后到leaf。因此额外提供针对语句的遍历操作(依赖Scope的遍历)

  • def foreachStatements(func: Statement => Unit)
  • def walkStatements(func : Statement => Unit): Unit

也有进一步,只针对leaf的遍历:

  • def walkLeafStatements(func: LeafStatement => Unit): Unit

更进一步,只针对leaf中的非memory port:

  • def foreachDeclarations(func: DeclarationStatement => Unit): Unit
  • def walkDeclarations(func: DeclarationStatement => Unit): Unit

Overview

Stream实现了valid-ready握手协议,用于blocking传输。基于Stream,SpinalHDL提供了大量的方法,以及相关的building blocks。

小逻辑块

以下介绍9大类与Stream相关的常用硬件电路实现,它们均作为Stream的成员方法提供,往往不会单独成为一个Module。这些方法的特点是其返回值往往就是被驱动的下游,因此可以好几种方法级联使用,轻松组合出想要的连接。

Flow转换

将blocking传输转为non-blocking 传输,即flow协议。

  • toFlow ,flow valid、payload直接赋值,ready always true
  • toFlowFire , flow valid等于fire,payload直接赋值
  • asFlow ,ready不管,flow valid、payload直接赋值

突发传输

在Burst连续传输的情况下,我们需要给负载payload增加一个last标志。

  • addFragmentLast(last : Bool) : Stream[Fragment[T]] last由外部计数器逻辑处理
  • addFragmentLast(counter: Counter) : Stream[Fragment[T]] last根据传入的计数器处理

状态检测

用于表明当前stream所处的状态,是否拥堵?是否空闲?是否正处于握手状态?

  • isStall 表示当前cycle是否正在堵塞,ready处于deassert状态
  • isNew 表示当前cycle是否有新的transaction发生,为堵塞结束后的第一个cycle
  • isFree 表示当前并不拥堵:ready为高或者valid为低,没有data需要上路。
  • fire 表示当前cycle是否处于握手阶段

Repeater

在link上面增加寄存器(repeater),以优化timing。

  • m2sPipe(collapsBubble: Boolean=true, crossClockData: Boolean=false, flush: Bool=null,holdPayload: Boolean=false) , 给forward flow,即valid、payload部分打拍。同样的, stage() 则是 m2sPipe()
  • s2mPipe(stagesCount: Int) 则是在backward ready上打拍,forward部分仍然直连。
  • validPipe() 则是只在valid上打拍,payload与ready没打拍
  • halfPipe() 所有信号都打拍,而bandwidth小了一半。
  • pipeline 等于上面的各种组合

以上函数返回新的被驱动的Stream

连接符号

以下函数、操作符返回操作符右边的stream,即不是 this stream,而是作为函数参数的 that stream。这里的连接符号和上面各种打拍组合起来,方便使用。

  • <<, >> 直连
  • <-<, >-> 只有Flow的部分被加了一级流水,即m2sPipe
  • </<, >/> 只有ready部分加了一级流水,即s2mPipe
  • <-/<, >/-> Flow和ready的部分均加了一级流水,即m2sPipe.s2mPipe

还有其他连接符,如 & ~ ~~ 这类后面再说。

流量控制

这些方法,通过一个condition信号,来完成基本的流量控制任务,比如halt住,丢弃掉,或者放入buffer(FIFO)。

  • continueWhen(cond: Bool) \ haltWhen(cond: Bool) ,当cond为高的时候,前者才继续传输,而后者则阻塞传输。(符号 &continueWhen(cond) 一样)
  • throwWhen(cond: Bool) \ takeWhen(cond: Bool) , 当cond为高,前者丢弃掉当前的事务,而后者则只有cond为低的时候才丢弃。

或者通过一个FIFO,来缓存事务,以增加吞吐

  • queue(size: Int) ,在link上增加一个深度为size的同步FIFO作为一个buffer。
  • queueWithOccupancy(size: Int): (Stream[T],UInt) ,在 credit-base 传输中,可以返回buffer当前的占用率,作为credit告知upstream是否还能继续发起传输。
  • queueWithAvailability(size: Int): (Stream[T], UInt) ,返回buffer及其当前空置率,告知upstream。
  • queueLowLatency(size: Int, latency: Int = 0) 增加一个无延迟或低延迟的buffer

分频处理

下游时钟域和上游同步,但被分频得更慢。(不一定用在分频时钟的设计中

  • repeat(times: Int): (Stream[T], UInt) ,每个beat都被至少重复times次,使之能被下游的时钟catch。上游的吞吐因此降低了times倍
  • slowdown(factor: Int): Stream[Vec[T]] ,内部有一个移位寄存器做串并转换,下游的payload比上游宽factor倍,在ready始终enable下,每隔factor个cycle,下游fire一次。上游的吞吐不会降低。

CDC处理

存在多时钟域的时候,可能需要处理跨时钟域的握手问题。包括简单的二级buffer,或者异步FIFO。

二级buffer:

  • ccToggle(pushClock: ClockDomain, popClock: ClockDomain) 打两拍
  • ccToggleWithoutBuffer(pushClock: ClockDomain, popClock: ClockDomain) 不打拍(??)直接过。

异步FIFO:

  • queue(size: Int, pushClock: ClockDomain, popClock: ClockDomain) , 增加一个异步FIFO
  • queueWithPushOccupancy(size: Int, pushClock: ClockDomain, popClock: ClockDomain): (Stream[T], UInt) ,返回异步buffer及其占用率

负载转换

在某些场景下,上游的Stream需要经过某个处理,再与下游的Stream连接。这里,我们把this称为变换前的上游,that则称为变换后的下游。

上下游直连,负载不变:

  • connectFrom(that: Stream[T]): Stream[T] ,返回下游
  • arbitrationFrom(that: Stream[T]): Unit ,只直连上下流的握手信号,payload给其他逻辑管。

通常情况下,我们提供一个函数,描述了负载转换的逻辑。

这里,函数 transformation: (this.payload.type, that.payload.type) => Unit 表示了这种转换:

  • translateFrom(that)(transformation) 握手信号直连,返回this 上游。

镜像地, transformation: (that.payload.type, this.payload.type) => Unit :

  • translateInto(that)(transformation) 用法与上面对偶。返回 that 下游。

转换函数还可以是更直接的形式transformer: (T) => T2 ,直接表示从上游的payload到下流payload的一个转换函数,并通过连接符 ~~ 来使用,返回下游

1
2
3
val upStream: Stream[T]
...
val downStream: Stream[T2] = upStream ~~ { upPayload => doSomethingOn(upPayload) }

另外,若转换由外部逻辑预先完成,我们只需要将转换后的payload直接送给下游即可:

  • translateWith(thatPayload:T) that是新的下游的payload,返回下游。
  • 连接符 ~ 和上面的方法一样。

还有的场景下,下游的payload数据类型有变化,只是继承上游的握手信号,但暂时不对下游的payload做连接

  • swapPayload(that: HardType[T2]): Stream[T2] 返回变换类型的新下游。

或者做bit-level连接:

  • transmuteWith[T2 <: Data](that: HardType[T2]): Stream[T2] 返回新下游,下游负载被上游做了bit-level赋值。

大逻辑块

这里Stream相关的功能,往往以单独的模块提供。提供的模块包括:仲裁器,Mux/DeMux,Fork/Join, 同步FIFO/异步FIFO,二级buffer,位宽调制器等。

仲裁处理

详见SpinalHDL文档。这里做点补充:

提供的仲裁机制:

  • lowerFirst:优先级固定。
  • sequentialOrder:记录已经发射的次数,第几次发射,就选择第几号
  • roundRobin:不必多说。

提供的锁机制:

  • none:不锁,仲裁结果随时可能变化
  • transactionLock:满足valid-ready握手规则,一旦仲裁后,valid assert而ready尚未 assert,必须维持仲裁结果不变。这里只是单发传输
  • fragmentLock: 满足握手规则的前提下,支持突发传输。在last assert之前,保持仲裁结果不变。

Mux\Demux

NoC的crossbar结构基本单元。

上游多个stream连接到下游一个stream,用一个选择信号选通上游某个通道,使用mux;往往配合仲裁器使用,选择信号由仲裁器给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
object StreamMux {
def apply[T <: Data](select: UInt, inputs: Seq[Stream[T]]): Stream[T] = {
val vec = Vec(inputs)
StreamMux(select, vec)
}

def apply[T <: Data](select: UInt, inputs: Vec[Stream[T]]): Stream[T] = {
val c = new StreamMux(inputs(0).payload, inputs.length)
(c.io.inputs, inputs).zipped.foreach(_ << _)
c.io.select:= select
c.io.output
}
}

上游一个stream连接到下游多个stream,用一个选择信号选通下游某个通道,使用demux;往往配合decoder使用,根据地址将传输送往特定的slave。

1
2
3
4
5
6
7
8
object StreamDemux{
def apply[T <: Data](input: Stream[T], select : UInt, portCount: Int) : Vec[Stream[T]] = {
val c = new StreamDemux(input.payload,portCount)
c.io.input<< input
c.io.select:= select
c.io.outputs
}
}

Fork

上游需要广播到多个下游。

1
2
3
4
5
6
7
object StreamFork {
def apply[T <: Data](input: Stream[T], portCount: Int, synchronous: Boolean = false): Vec[Stream[T]] = {
val fork = new StreamFork(input.payloadType, portCount, synchronous).setCompositeName(input, "fork", true)
fork.io.input<< input
return fork.io.outputs
}
}

同步机制:

  • 如果需要同步,那么只有当所有下游都ready,上游才能fire,下游可以同时拿到东西。这一机制可以造成死锁。同时违反axi握手依赖规则。
  • 反之,每个下游单独ready,单独拿到东西。上游在所有下游全部fire之前stall住。

Join

上游的多个stream,其payload打包,送给一个下游stream。握手信号做了简单处理,使得下游只有在上游都fire了才能fire。

1
2
3
4
5
6
7
8
object StreamJoin {
...
// 简单打包两个相同的payload
def apply[T1 <: Data,T2 <: Data](source1: Stream[T1], source2: Stream[T2]): Stream[TupleBundle2[T1, T2]]
// 这里只保留上游join到下游的握手信号,payload另外给,因为不同的payload可能类型不同
def arg(sources : Stream[_]*) : Event
// 多个类型相同的payload放在一起,打包成一个vec
def vec[T <: Data](sources: Seq[Stream[T]]): Stream[Vec[T]]

FIFO

同步FIFO和异步FIFO不用多说,embed在上面的小逻辑块部分。

需要补充的:

  • StreamFifoLowLatency 低延迟同步FIFO
  • StreamFifoMultiChannelSharedSpace 没做完??

位宽调制

上下游之间数据位宽不同,连接需要做一定处理。上游宽度大,下游需要拆分成多笔传输;下游宽度大,上游多笔传输合并成一笔。

单发传输:

1
2
3
4
5
6
7
object StreamWidthAdapter {
...
def make[T <: Data, T2 <: Data](input : Stream[T], outputPayloadType : HardType[T2], endianness: Endianness = LITTLE, padding : Boolean = false) : Stream[T2] = {
val ret = Stream(outputPayloadType())
StreamWidthAdapter(input,ret,endianness,padding)
ret
}

突发传输:

1
2
3
4
5
def make[T <: Data, T2 <: Data](input : Stream[Fragment[T]], outputPayloadType : HardType[T2], endianness: Endianness = LITTLE, padding : Boolean = false) : Stream[Fragment[T2]] = {
val ret = Stream(Fragment(outputPayloadType()))
StreamFragmentWidthAdapter(input,ret,endianness,padding)
ret
}

StreamTransactionExtender

用途不明。

功能:动态输入一个计数上限,计数器到达以前上游stall,直至达到上限才fire。下游payload根据计数器值,发生相应变化或者不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class StreamTransactionExtender[T <: Data, T2 <: Data](
dataType: HardType[T],
outDataType: HardType[T2],
countWidth: Int,
var driver: (UInt, T) => T2 // 下游负载变化函数
) extends Component {
val io = new Bundle {
val count = in UInt (countWidth bit) //计数上限
val input = slave Stream dataType
val output = master Stream outDataType
}
...

object StreamTransactionExtender {
def apply[T <: Data](input: Stream[T], count: UInt)(
driver: (UInt, T) => T = (_: UInt, p: T) => p // 默认不变
): Stream[T] = {
val c = new StreamTransactionExtender(input.payloadType, input.payloadType, count.getBitsWidth, driver)
c.io.input << input
c.io.count := count
c.io.output
}
...

验证用具

spinal.lib.sim._ 下,有专门用于验证Stream接口的工具。包括了基本的 StreamDriver 和基本的 StreamMonitor

Stream Driver

其核心定义:

class StreamDriver[T <: Data](stream : Stream[T], clockDomain: ClockDomain, var driver : (T) => Boolean)

除了传入一个Stream接口以外,还要提供一个driver函数,等效于以下操作:

1
2
3
4
5
6
7
8
9
10
11
fork{
stream.valid #= false
while(true) {
clockDomain.waitSampling(transactionDelay())
clockDomain.waitSamplingWhere(driver(stream.payload))
stream.valid #= true
clockDomain.waitSamplingWhere(stream.ready.toBoolean)
stream.valid #= false
stream.payload.randomize()
}
}

driver只需对payload进行驱动操作,以及控制仿真进程的结束。

Stream Monitor

其定义如下:

1
2
3
4
5
6
7
8
class StreamMonitor[T <: Data](stream : Stream[T], clockDomain: ClockDomain){
valcallbacks= ArrayBuffer[(T) => Unit]()

def addCallback(callback : (T) => Unit): this.type = {
callbacks+= callback
this
}
...

需要用户手动添加回调函数进行数据收集操作。

Overview

用于Non-blocking传输的协议,由valid标志表示当前payload的有效性。相比于存在握手规则约束的Stream,其使用更加简单,更加方便,因此相关的方法也更少,更简单。

Stream转换

将Flow转换为Stream的函数

  • toStream(overflow: Bool, fifoSize: Int, overflowOccupancyAt: Int): Stream[T] ,返回一个Stream,并给一个输出信号overflow。在转换到Stream后,接一个FIFO,在超过一定深度后给assert overflow。
  • toStream(overflow: Bool): Stream[T] ,类似上面,但没有接一个overflow,只是没有接一个FIFO。如果有传一个overflow的引用进去,那么输出一个overflow,当下游不ready的时候拉高。

上面的第一个函数,是在转换Stream之后才加buffer。我们可以在转换为Stream之前加buffer
gg

  • queueWithOccupancy(size: Int): (Stream[T], UInt)
  • queueWithAvailability(size: Int): (Stream[T], UInt)

Repeater

给valid-payload打拍

  • m2sPipe(holdPayload : Boolean = false, flush : Bool = null): Flow[T] ,类似于Stream的m2sPipe。 stage() 和 m2sPipe() 一样。

负载控制

不像Stream可以控制上游的流量,这里引入condition来控制valid。

  • takeWhen(cond: Bool): Flow[T]
  • throwWhen(cond: Bool): Flow[T]

负载转换

类似于Stream,同样提供了简单的负载转换方法

  • translateWith[T2 <: Data](that: T2): Flow[T2]
  • translateFrom[T2 <: Data](that: Flow[T2])(dataAssignment: (T, that.payload.type) => Unit): Flow[T]

CDC处理

ccToggle(pushClock: ClockDomain, popClock: ClockDomain) : Flow[T]

返回下游Flow,处在另一个时钟域。

正则匹配

判断是否匹配

1
2
3
// <string>.matches(<regex_str>)
if("123".matches("""\d+""")) println("yes")
if("t0".matches("""\w+""")) println("yes")

提取子表达式

1
2
3
4
5
6
7
8
9
10
import scala.util.matching._
val pattern = """(\w+) = (\w+);""".r
//pattern必须全部cover住被匹配的字符串
//1.
val pattern(a, b) = "shit = game;" // val a = shit; val b = game
//2. no exception
"shit = game" match {
case pattern(a, b) => ...
case _ => println("no matches")
}

Publish到本地

发布

我们将自己定义的repository,发布到本地某一个目录,需要在build.sbt文件里加上一句:

1
2
3
4
5
6
7
8
9
10
val spinalVersion = "1.6.0"

//加上这一句
publishTo := Some(Resolver.file("MyRepo", file("/mnt/d/lzp/riscv/tmp/package")))

lazy val root = (project in file(".")).
settings(
inThisBuild(List(
organization := "com.github.spinalhdl",
...

随后在sbt shell里运行 publish 即可将自己的package命名为MyRepo,发布的jar文件保存在/mnt/d/lzp/riscv/tmp/package下面。

使用

要使用自己先前发布的本地package,需要在调用改package的project的build.sbt里面加上一句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scalaVersion := "2.12.13"
val spinalVersion = "1.6.0"
val vexRiscVersion = "2.0.0"

//加上这一句,分别对应Windows和WSL
resolvers += "MyRepo" at "file:///D:/lzp/riscv/tmp/package"
//resolvers += "MyRepo" at "file:///mnt/d/lzp/riscv/tmp/package"

libraryDependencies ++= Seq(
"com.github.spinalhdl" %% "spinalhdl-core" % spinalVersion,
"com.github.spinalhdl" %% "spinalhdl-lib" % spinalVersion,
compilerPlugin("com.github.spinalhdl" %% "spinalhdl-idsl-plugin" % spinalVersion),
//这里是刚刚发布的package,会从上面的resolver那里找。
"com.github.spinalhdl" %% "vexriscv" % vexRiscVersion
)

reload之后,即可使用自己发布package。

发布后的包结构

发布后的package会放在resolver添加的Repo对应的目录下。下面通常会有三级目录:

1
<organization>/<repo_name>_<scala_version>/<repo_version>

第三级目录下就是对应package的各种jar。

如果组织名本身就是分层的,比如 com.github.spinal 那么对应的也有可能分为多级目录。