搞清楚了前面Axi4写通道的实现方式,那么Axi4ReadOnlyDecoder也就好理解了。
Axi4ReadOnlyDecoder
对于Axi4读操作而言,其指令的完成由ar、r两个通道完成,相较于写操作,其通道数虽然少了一个,但也是两个方向的数据流:
- ar:master——>slave
- r:slave——>master
那么,与Axi4WriteOnlyDecoder相同,Axi4ReadOnlyDecoder也需要考虑:
- 每个写通道readIssuingCapability的支持。
- 通道切换契机的选择,确保每个通道(ar,r)在切换通道之前均能保障之前的数据传输完成。
- 地址译码没有命中任何slave端口的处理。
基于上述考虑,及对于每个slave端口,其时序并不能保障先后顺序(后接受数据的slave端口可能先返回读数据)。为此,Axi4ReadOnlyDecoder设计时采用了如下原则:
- 对于每个通道,均支持readIssuingCapability,但如果当前指令地址译码命中的slave端口和上次不一样,那么则需等待之前的数据完成传输才进行本次数据传输。
- 设计单独的Axi4ReadOnlyErrorSlave用于处理地址未命中任何slave端口的情况。
模块参数配置
Axi4ReadOnlyDecoder例化参数包括:
- axiConfig: Axi4Config,Axi4总线参数配置。
- decodings: Seq[SizeMapping],Slave端口的地址段划分。地址段不能有重叠。
- pendingMax:Int,在接收新指令时允许链路中待完成的指令个数。值得注意的是该参数需为2^n-1。
代码解析
由于少了一个通道,Axi4ReadOnlyDecoder设计相对较容易些(代码只有区区50行),理解了前面Axi4WriteOnlyDecoder设计,Axi4ReadOblyDecoder理解起来也会容易很多。
readIssuingCapability的支持及通道的实现中链路上的指令通过pendingCmdCounter计数器来实现:
代码简洁清晰,其值代表了在链路上尚未完成的指令个数。读操作译码逻辑为:
在译码逻辑里,包含了地址译码未命中所有slave通道的情况。而pendingSels及pendingError则用于readData通道的数据路由。这里有一点得的注意:
对于readCmd,当一个读指令被消耗后(arvalid&arready),若没有新的指令到来,那么readCmd.addr需保持不变,否则pendingels及pendingError将会变化可能导致读数据通道的译码错误。而在Axi4 Spec里并无此要求。
有了上面的这些实现,那接下来就是指令发射时机的控制了:
val allowCmd = pendingCmdCounter === 0 ||
(pendingCmdCounter =/= pendingMax &&
pendingSels === decodedCmdSels)
与Axi4WriteOnlyDecoder类似,读指令的发射只有在pendingCmdCounter 为0(此时链路上没有未完成的指令)或者pendingCmdCounter还未达到上限且当前译码状态与上次译码状态一致(表明了这次指令和上次指令选中的都是同一个slave端口)那么允许这次指令的发射。如此,确保了在通道切换时所有之前的指令都已完成后才会切换到新的通道。
错误的处理例化了一个Axi4ReadOnlySlaveError:
可以看到,只有所有slave端口的地址段大小总和小于Axi4总线能覆盖的地址范围时方例化 Axi4ReadOnlySlaveError。
好了,所有的设计要素都实现了,那么剩下的就是连线了:
io.input.readCmd.ready := ((decodedCmdSels & io.outputs.map(_.readCmd.ready).asBits).orR || (if(decodingErrorPossible) (decodedCmdError && errorSlave.io.axi.readCmd.ready) else False)) && allowCmd
if(decodingErrorPossible) {
errorSlave.io.axi.readCmd.valid := io.input.readCmd.valid && decodedCmdError && allowCmd
errorSlave.io.axi.readCmd.payload := io.input.readCmd.payload
}
for((output,sel) <- (io.outputs,decodedCmdSels.asBools).zipped){
output.readCmd.valid := io.input.readCmd.valid && sel && allowCmd
output.readCmd.payload := io.input.readCmd.payload
}
//Wire ReadRsp
io.input.readRsp.valid := io.outputs.map(_.readRsp.valid).asBits.orR
io.input.readRsp.payload := MuxOH(pendingSels,io.outputs.map(_.readRsp.payload))
if(decodingErrorPossible) {
io.input.readRsp.valid setWhen(errorSlave.io.axi.readRsp.valid)
when(pendingError){
if(axiConfig.useId) io.input.readRsp.id := errorSlave.io.axi.readRsp.id
if(axiConfig.useResp) io.input.readRsp.resp := errorSlave.io.axi.readRsp.resp
if(axiConfig.useLast) io.input.readRsp.last := errorSlave.io.axi.readRsp.last
}
errorSlave.io.axi.readRsp.ready := io.input.readRsp.ready
}
io.outputs.foreach(_.readRsp.ready := io.input.readRsp.ready)
这里可以看到,对于读返回据的处理,利用了pendingSels信号进行读返回数据通道的选取。得益于allowCmd设计的机制,可以确保不会错误通道的选取。而整个代码简洁而又优雅,区区五十行代码完整实现了一个模块,相较于Verilog实现,可读性有着极大的提升。
☆ END ☆
作者:玉骐
原文链接:https://mp.weixin.qq.com/s/nQ8YWgu7Mr0fjHeEQTMTXA
微信公众号:
推荐阅读
更多SpinalHDL技术干货请关注Spinal FPGA专栏。