前回はとりあえずLazyModuleを使った最小のモジュールを作ってみる、という話をまとめた。
その際に以下の要素があればLazyModule
を使ったモジュールを作れるということがわかった。
Parameters
から派生したモジュールのパラメータクラスLazyModule
から派生したモジュールの皮でモジュール間の最終的な接続はここっぽい?LazyModuleImp
から派生したモジュールの実装本体
上記のようにLazyModuleImp
が実装本体っぽいという当たりがついたので、実際にRocket Chipの実装の解析が少し進むかも!!ということでコードを呼んでみた内容をまとめていく。
例のRocketシステム (ExampleRocketSystem)
例の、、、と書くとおかしな感じが漂うけど、今回見ていきたいのはTestHarness
クラス内でインスタンスされているExampleRocketSystem
だ。
知りたいことは色々あるのだが、今回は
- そもそもモジュールのIOポートの宣言ってどこなの??
という部分にしぼって解析をしたいと思う。ということで実装部分を見てみよう。
- system/ExampleRocketSystem.scala
/** Example Top with periphery devices and ports, and a Rocket subsystem */ class ExampleRocketSystem(implicit p: Parameters) extends RocketSubsystem with HasAsyncExtInterrupts with CanHaveMasterAXI4MemPort with CanHaveMasterAXI4MMIOPort with CanHaveSlaveAXI4Port with HasPeripheryBootROM { override lazy val module = new ExampleRocketSystemModuleImp(this) ~略~ } class ExampleRocketSystemModuleImp[+L <: ExampleRocketSystem](_outer: L) extends RocketSubsystemModuleImp(_outer) with HasRTCModuleImp with HasExtInterruptsModuleImp with CanHaveMasterAXI4MemPortModuleImp with CanHaveMasterAXI4MMIOPortModuleImp with CanHaveSlaveAXI4PortModuleImp with HasPeripheryBootROMModuleImp with DontTouch
"~略~"と書いたが省略するほど長くはなくて、内部のポートやら接続を行うと思われるメソッドが幾つか呼び出されるだけになっていて、このExampleRocketSystem
自体にはほぼ何も実装が無い。
ExampleRocketSystemのIOポートを探す
構成的には継承してる各種Trait群が実装実体なのだとは思うのだが、闇雲に追うのも大変なのでまずはmake
によって生成されたコードで何がポートに出てきているかを確認しておく。
細かい信号は接頭辞となる部分だけ端折って載せます(RTL生成の元コードがFIRRTLのコードだからChiselのソースと対応してなくて面倒だったのでFIRRTLの方のコード)。
module ExampleRocketSystem : input clock : Clock input reset : UInt<1> output auto : {} output debug : output mem_axi4 : output mmio_axi4 : input l2_frontend_bus_axi4 :
中身的には上記のようになってて
- debug関連のI/F
- メモリへのAXI4 I/F
- MMIOへのAXI4 I/F
- L2用のバスへのAXI4 IF
がくっついてるという感じ。上記の下3つが対応しているのはExampleRocketSystem
にMix-inされている以下の3つだと思われるのでこれらを確認していく。
with CanHaveMasterAXI4MemPort with CanHaveMasterAXI4MMIOPort with CanHaveSlaveAXI4Port with CanHaveMasterAXI4MemPortModuleImp with CanHaveMasterAXI4MMIOPortModuleImp with CanHaveSlaveAXI4PortModuleImp
CanHaveMasterAXI4MemPort / CanHaveMasterAXI4MemPortModuleImp
まずはソースコードを。
trait CanHaveMasterAXI4MemPort { this: BaseSubsystem => val module: CanHaveMasterAXI4MemPortModuleImp val memAXI4Node = p(ExtMem).map { case MemoryPortParams(memPortParams, nMemoryChannels) => val portName = "axi4" val device = new MemoryDevice val memAXI4Node = AXI4SlaveNode(Seq.tabulate(nMemoryChannels) { channel => val base = AddressSet(memPortParams.base, memPortParams.size-1) val filter = AddressSet(channel * mbus.blockBytes, ~((nMemoryChannels-1) * mbus.blockBytes)) AXI4SlavePortParameters( slaves = Seq(AXI4SlaveParameters( address = base.intersect(filter).toList, resources = device.reg, regionType = RegionType.UNCACHED, // cacheable executable = true, supportsWrite = TransferSizes(1, mbus.blockBytes), supportsRead = TransferSizes(1, mbus.blockBytes), interleavedId = Some(0))), // slave does not interleave read responses beatBytes = memPortParams.beatBytes) }) memAXI4Node := mbus.toDRAMController(Some(portName)) { AXI4UserYanker() := AXI4IdIndexer(memPortParams.idBits) := TLToAXI4() } memAXI4Node } } /** Actually generates the corresponding IO in the concrete Module */ trait CanHaveMasterAXI4MemPortModuleImp extends LazyModuleImp { val outer: CanHaveMasterAXI4MemPort val mem_axi4 = outer.memAXI4Node.map(x => IO(HeterogeneousBag.fromNode(x.in))) (mem_axi4 zip outer.memAXI4Node) foreach { case (io, node) => (io zip node.in).foreach { case (io, (bundle, _)) => io <> bundle } } def connectSimAXIMem() { (mem_axi4 zip outer.memAXI4Node).foreach { case (io, node) => (io zip node.in).foreach { case (io, (_, edge)) => val mem = LazyModule(new SimAXIMem(edge, size = p(ExtMem).get.master.size)) Module(mem.module).io.axi4.head <> io } } } }
CanHaveMasterAXI4MMIOPort / CanHaveMasterAXI4MMIOPortModuleImp
同様にAXI4MMIOについても確認していく。
こちらも先程と同様mmio_axi4
がIO
でラップされている。
/** Adds a AXI4 port to the system intended to master an MMIO device bus */ trait CanHaveMasterAXI4MMIOPort { this: BaseSubsystem => private val mmioPortParamsOpt = p(ExtBus) private val portName = "mmio_port_axi4" private val device = new SimpleBus(portName.kebab, Nil) val mmioAXI4Node = AXI4SlaveNode( mmioPortParamsOpt.map(params => AXI4SlavePortParameters( slaves = Seq(AXI4SlaveParameters( address = AddressSet.misaligned(params.base, params.size), resources = device.ranges, executable = params.executable, supportsWrite = TransferSizes(1, params.maxXferBytes), supportsRead = TransferSizes(1, params.maxXferBytes))), beatBytes = params.beatBytes)).toSeq) mmioPortParamsOpt.map { params => mmioAXI4Node := sbus.toFixedWidthPort(Some(portName)) { (AXI4Buffer() := AXI4UserYanker() := AXI4Deinterleaver(sbus.blockBytes) := AXI4IdIndexer(params.idBits) := TLToAXI4()) } } } /** Actually generates the corresponding IO in the concrete Module */ trait CanHaveMasterAXI4MMIOPortModuleImp extends LazyModuleImp { val outer: CanHaveMasterAXI4MMIOPort val mmio_axi4 = IO(HeterogeneousBag.fromNode(outer.mmioAXI4Node.in)) (mmio_axi4 zip outer.mmioAXI4Node.in) foreach { case (io, (bundle, _)) => io <> bundle } def connectSimAXIMMIO() { (mmio_axi4 zip outer.mmioAXI4Node.in) foreach { case (io, (_, edge)) => // test harness size capped to 4KB (ignoring p(ExtMem).get.master.size) val mmio_mem = LazyModule(new SimAXIMem(edge, size = 4096)) Module(mmio_mem.module).io.axi4.head <> io } } }
CanHaveSlaveAXI4Port / CanHaveSlaveAXI4PortImp
こちらはRocketChipが備えるAXIのSlaveポートの記述で先程と同様にl2_frontend_bus_axi4
がIO
でラップされている。
/** Adds an AXI4 port to the system intended to be a slave on an MMIO device bus */ trait CanHaveSlaveAXI4Port { this: BaseSubsystem => private val slavePortParamsOpt = p(ExtIn) private val portName = "slave_port_axi4" private val fifoBits = 1 val l2FrontendAXI4Node = AXI4MasterNode( slavePortParamsOpt.map(params => AXI4MasterPortParameters( masters = Seq(AXI4MasterParameters( name = portName.kebab, id = IdRange(0, 1 << params.idBits))))).toSeq) slavePortParamsOpt.map { params => fbus.fromPort(Some(portName), buffer = BufferParams.default) { (TLWidthWidget(params.beatBytes) := AXI4ToTL() := AXI4UserYanker(Some(1 << (params.sourceBits - fifoBits - 1))) := AXI4Fragmenter() := AXI4IdIndexer(fifoBits)) } := l2FrontendAXI4Node } } /** Actually generates the corresponding IO in the concrete Module */ trait CanHaveSlaveAXI4PortModuleImp extends LazyModuleImp { val outer: CanHaveSlaveAXI4Port val l2_frontend_bus_axi4 = IO(HeterogeneousBag.fromNode(outer.l2FrontendAXI4Node.out).flip) (outer.l2FrontendAXI4Node.out zip l2_frontend_bus_axi4) foreach { case ((bundle, _), io) => bundle <> io } }
AXIを使うモジュールの一般形?
こうやって3つくらい並べると、大体共通してる部分があるのでこの辺はお作法的に捉えても使う分には問題なさそうでもある。 ざっくり今回見てきたAXIの3つのモジュールについてその共通部分を括りだして一般化??してみると、以下のような感じだろうか(まだわからない部分を含む。。ご容赦ください)
trait <TraitPort> { this: <LazyModuleの派生クラス> => private val <ポートのパラメータ変数> = p(<対応するポートのField>) private val portName = "mmio_port_axi4" private val device = new SimpleBus(portName.kebab, Nil) val <ノード変数> = <AXI4MasterNode or AXI4SlaveNode>( <ポートのパラメータ変数>.map(params => <AXI4SlavePortParameters or AXI4MasterPortParameters>( slaves = Seq(AXI4SlaveParameters( // 対応する設定 ) <ポートのパラメータ変数>.map { params => mmioAXI<ノード変数> := <接続(ここはまだ追えてない)> { // ノード間の接続の関係っぽい?? } } } trait <TraitPortModuleImp> extends LazyModuleImp { val outer: <TraitPort> val <ポート名> = IO(HeterogeneousBag.fromNode(outer.<ノード変数>.<in or out>)) (<ポート名> zip outer.<ノード変数>.<in or out>) foreach { case (io, (bundle, _)) => io <> bundle } def <connect***>() { (<ポート名> zip outer.<ノード変数>.<in or out>) foreach { case (io, (_, edge)) => // 必要な処理 // connectSimAXIMMIOの場合ならSimAXIMemを実体化して<ポート名>と接続 } } }
ということでIOポートについてはこれらがFIRRTLを経てRTLになっていると思って良さそう。
ここまでに見てきた2つの***Imp
クラスで定義されているconnect***
メソッドはTestHarness
内でExampleRocketSystem.module
を評価した後に実行されている。
つまりExampleRocketSystem.module
を評価してmodule
を取り出したタイミングでは最終的な意味での配線は行われていない感じになってる、、、ように思える。
次はそのへんの接続部分に着目して解析を進めていこうと思う。
あとRocketChipで使われているMultiIOModule
についても少し気になることがあるので、それは別途紹介する予定。