上一篇文章以Popcount为例,介绍了带数据通路的有限状态机FSMD的写法与实现,对于后面写复杂的系统有很关键的指导意义。我们可以注意到,在FSMD的实现中,状态机之间的通信我们使用了Ready-Valid
握手协议,这是一种常见的通信接口协议,但每次都这么写显然有点复杂。而Chisel中自带了Ready-Valid
相关的函数DecoupledIO
,用于对数据信号进行Ready-Valid
协议的封装,这一篇文章我们就来学习这个重要又方便的函数。
子系统之间的通信可以泛化为数据的移动和用于流控制的握手。在上一篇文章的Popcount例子中,我们已经看到了一个用于输入输出数据的握手接口,这个接口就使用了Ready-Valid信号。
Ready-Valid接口是一种简单的控制流接口,包含:
data
:发送端向接收端发送的数据;valid
:发送端到接收端的信号,用于指示发送的数据是否有效;ready
:接收端到发送端的信号,用于指示是否可以接收数据;下面是Ready-Valid流控制的示意图:
发送端在data
准备好之后就会设置valid
信号,接收端在准备好接收一个字的数据的时候就会设置ready
信号。数据的传输会在两个信号,valid
信号和ready
信号,都被设置时才会进行。如果两个信号有任何一个没被设置,那就不会进行数据传输。
下图是当接收端在发送端准备好数据之前,就将ready
置有效时(从第一个时钟周期开始)的时序图:
上图中,数据在第三个时钟周期准备好,同时valid
置有效,此时ready
信号和valid
信号都被设置,数据传输进行。而在第四个时钟周期,发送端没有要发送的数据,接收端也没有准备好接受数据。如果接收端每个时钟周期都可以接受数据,那就叫作always ready
接口,ready
信号此时可以硬编码为true
。
下图是当发送端在接收端准备好接收数据之前,就将valid
信号设置时(从第一个时钟周期开始)的时序图:
数据传输发生在第三个时钟周期,同样从第四个时钟周期开始,发送端没有要发送的数据,接收端也没有准备好接受数据。类比always ready
接口,我们可以想到是不是有一个always valid
接口。确实,不过这种情况下数据可能在给出ready
信号的时候不改变,我们只能简单地丢弃这个握手信号。
下图是Ready-Valid接口的另一种变化的时序图:
上图中,在第一个时钟周期,ready
和valid
同时被设置一个时钟周期,数据D1
的传输也在这个周期进行。数据可以背靠背传输(每个时钟周期内),比如第四、第五个时钟周期内D2
和D3
的传输。(注意,这里的背靠背指的是连续两次,常用于NBA)
为了让两个模块通过Ready-Valid接口连接,必须要保证ready
和valid
都不依赖于彼此。由于这个接口实在是太常见了,所以Chisel里面定义了DecoupledIO
,定义类似这样:
class DecoupledIO[T <: Data](gen: T) extends Bundle {
val ready = Input(Bool())
val valid = Output(Bool())
val bits = Output(gen)
}
使用它需要导入util
包。这个DecoupledIO
根据数据data
的类型来参数化,用DecoupledIO
封装后,可以通过接口的bits
字段来访问data
。
当然了,显然这个DecoupledIO
接口是发送端的,而接收端的接口是完全反过来的,那么我们就需要使用Chisel中的另一个函数Flipped
了。Flipped
函数可以将一个Bundle
内的输入输出全都颠倒过来,所以如果一个发送端的Ready-Valid数据接口定义如下:
val out = DecoupledIO(UInt(8.W))
那么接收端的Ready-Valid数据接口在定义时在外面再套一个Flipped
就行了:
val in = Flipped(DecoupledIO(UInt(8.W)))
可能还存在这么一个问题,ready
和valid
信号是可能在被设置后、在没有数据被传输的情况下又取消设置。比如说,接收端可能ready
了一些时间,但未接受到数据,又因为一些其他事件取消了ready
;再比如发送端可能准备好了数据,valid
了一段时间,但是在未发送数据的情况下又取消了valid
。不管这种行为是否被允许,这不是Ready-Valid接口要讨论的内容,但是这需要被接口的具体用法来定义。
Chisel中使用DecoupledIO
的时候,对ready
和valid
信号的设置没有要求。但是,Chisel中的IrrevocableIO
类对发送端做了如下限制:
IrrevocableIO
是ReadyValidIO
的一个具体子类,当valid
为1,ready
为0时,能够保证bits
的值不改变。也就是说,一旦valid
为1后,他就不会变为0,直到ready
也变为1。
需要注意的是,这只是一个约定,并不能通过使用IrrevocableIO
类来强制规范(Irrevocable意思是不可撤销的)。
而AXI协议就为总线的每一个部分都使用了Ready-Valid接口,包括读地址、读数据、写地址和写数据。AXI协议限制接口一旦ready
或valid
被设置,那在数据完成传输之前就不允许取消设置ready
或valid
了。
通信状态机部分到这里就结束了,这篇文章对上一篇文章中使用的Ready-Valid接口进行了详细介绍,对时序和Chisel中提供的DecoupledIO
接口也进行了分析。复杂的、大型的数字电路内部各种模块的通信都可以泛化为通信状态机,所以通信接口也很重要。到目前为止,Chisel相关的内容已经基本结束了,但是Chisel的最强大的特性我们还没有详细学习,那就是作为硬件生成器。下一部分,我们将从Scala开始,详细说说Chisel作为硬件生成器的写法,允许我们写出参数化的硬件电路,极大地发挥可复用性。