SpinalHDL / VexiiRiscv

Like VexRiscv, but, Harder, Better, Faster, Stronger
MIT License
95 stars 10 forks source link

『Help』The features that can be used on naxriscv are not available on the vexiiriscv framework #6

Closed dreamflyings closed 8 months ago

dreamflyings commented 8 months ago

hi~ I still feel uncomfortable using the FiberPlugin framework of vexiiriscv. The code written by the plugin framework of naxriscv cannot be directly translated into the FiberPlugin framework. Can you help me take a look? Thank you for taking the time out of your busy schedule! 🙏

framework of naxriscv :

object Miaouuuu10 extends App{

  case class AlphaPlugin_P(
    a :Int ,
    b :Int
  )
  object AlphaPlugin_P extends AreaObject{
    val P = NaxParameter[AlphaPlugin_P]
  }
  class AlphaPlugin(var p:AlphaPlugin_P) extends Plugin{
    import AlphaPlugin_P._
    create config{
      P.set(p)
    }
    val sp = create early new Area{
      getServiceOption[BetaPlugin].map{b =>
        P.set(P.copy(a = b.p.a))
      }
    }
    val logic = create late new Area{
      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }
  case class BetaPlugin_P(
    a :Int ,
    b :Int
  )
  object BetaPlugin_P extends AreaObject{
    val P = NaxParameter[BetaPlugin_P]
  }
  class BetaPlugin(var p:BetaPlugin_P) extends Plugin{
    import BetaPlugin_P._
    create config{
      P.set(p)
    }
    val sp = create early  new Area{
      getServiceOption[AlphaPlugin].map{a =>
        P.set(P.copy(b = a.p.b + 2))
      }
    }
    val logic = create late new Area{
      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }

  SpinalVerilog{
    val plugins = ArrayBuffer[Plugin]()
    plugins += new AlphaPlugin((AlphaPlugin_P(a = 8, b = 8)))
    plugins += new BetaPlugin((BetaPlugin_P(a = 12, b = 12)))
    new Module{
      val frame = NaxScope(new DataBase) on {
        new Framework(plugins) //Will run the generation asynchronously
      }
    }
  }
}

framework of vexiiriscv :

object Miaouuuu10 extends App{

  case class AlphaPlugin_P(
    a :Int ,
    b :Int
  )
  object AlphaPlugin_P extends AreaObject{
    val P = blocking[AlphaPlugin_P]
  }
  class AlphaPlugin(var p:AlphaPlugin_P) extends FiberPlugin{
    import AlphaPlugin_P._
    during setup{
      P.set(p)
    }
    val sp = during setup new Area{
      host.get[BetaPlugin].map{b =>
        P.set(P.copy(a = b.p.a))
      }
    }
    val logic = during build new Area{
      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }
  case class BetaPlugin_P(
    a :Int ,
    b :Int
  )
  object BetaPlugin_P extends AreaObject{
    val P = blocking[BetaPlugin_P]
  }
  class BetaPlugin(var p:BetaPlugin_P) extends FiberPlugin{
    import BetaPlugin_P._
    during setup{
      P.set(p)
    }
    val sp = during setup new Area{
      host.get[AlphaPlugin].map{a =>
        P.set(P.copy(b = a.p.b + 2))
      }
    }
    val logic = during build new Area{
      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }

  SpinalVerilog{
    val plugins = ArrayBuffer[FiberPlugin]()
    plugins += new AlphaPlugin((AlphaPlugin_P(a = 8, b = 8)))
    plugins += new BetaPlugin((BetaPlugin_P(a = 12, b = 12)))
    VexiiRiscv(plugins)
  }
}
Dolu1990 commented 8 months ago

Hi,

Seems that in your example, val P = NaxParameter[AlphaPlugin_P] are used as a mutable thing, as it is set two times right ?

But that's not the intend of blocking[T]

The intend is to only set it once. So i'm not realy sure of the Miaouuuu10 intends.

Here is kina a tweeked version :

object Miaouuuu10 extends App{

  import spinal.core._
  import spinal.core.fiber._
  import spinal.lib.misc.plugin._
  import spinal.lib.CountOne
  import vexiiriscv._
  import scala.collection.mutable.ArrayBuffer

  case class AlphaPlugin_P(
                            a :Int ,
                            b :Int
                          )
  object AlphaPlugin_P extends AreaObject{
    val P = Database.blocking[AlphaPlugin_P]
    val P2 = Database.blocking[AlphaPlugin_P]
  }
  class AlphaPlugin(var p:AlphaPlugin_P) extends FiberPlugin{
    import AlphaPlugin_P._

    val logic = during setup new Area{
      awaitBuild()

      P.set(p)
      host.get[BetaPlugin].map{b =>
        P2.set(P.copy(a = b.p.a))
      }

      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }
  case class BetaPlugin_P(
                           a :Int ,
                           b :Int
                         )
  object BetaPlugin_P extends AreaObject{
    val P = blocking[BetaPlugin_P]
    val P2 = blocking[BetaPlugin_P]
  }
  class BetaPlugin(var p:BetaPlugin_P) extends FiberPlugin{
    import BetaPlugin_P._
    val sp = during setup new Area{
      awaitBuild()
      P.set(p)
      host.get[AlphaPlugin].map{a =>
        P2.set(P.copy(b = a.p.b + 2))
      }

      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }

  SpinalVerilog{
    val plugins = ArrayBuffer[FiberPlugin]()
    plugins += new AlphaPlugin((AlphaPlugin_P(a = 8, b = 8)))
    plugins += new BetaPlugin((BetaPlugin_P(a = 12, b = 12)))
    VexiiRiscv(plugins)
  }
}

Note that the idea is to only use the setup phase code for locking / retain other plugins. Everything else is done in build phase.

dreamflyings commented 8 months ago

Hi,

Seems that in your example, val P = NaxParameter[AlphaPlugin_P] are used as a mutable thing, as it is set two times right ?

But that's not the intend of blocking[T]

The intend is to only set it once. So i'm not realy sure of the Miaouuuu10 intends.

Here is kina a tweeked version :

object Miaouuuu10 extends App{

  import spinal.core._
  import spinal.core.fiber._
  import spinal.lib.misc.plugin._
  import spinal.lib.CountOne
  import vexiiriscv._
  import scala.collection.mutable.ArrayBuffer

  case class AlphaPlugin_P(
                            a :Int ,
                            b :Int
                          )
  object AlphaPlugin_P extends AreaObject{
    val P = Database.blocking[AlphaPlugin_P]
    val P2 = Database.blocking[AlphaPlugin_P]
  }
  class AlphaPlugin(var p:AlphaPlugin_P) extends FiberPlugin{
    import AlphaPlugin_P._

    val logic = during setup new Area{
      awaitBuild()

      P.set(p)
      host.get[BetaPlugin].map{b =>
        P2.set(P.copy(a = b.p.a))
      }

      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }
  case class BetaPlugin_P(
                           a :Int ,
                           b :Int
                         )
  object BetaPlugin_P extends AreaObject{
    val P = blocking[BetaPlugin_P]
    val P2 = blocking[BetaPlugin_P]
  }
  class BetaPlugin(var p:BetaPlugin_P) extends FiberPlugin{
    import BetaPlugin_P._
    val sp = during setup new Area{
      awaitBuild()
      P.set(p)
      host.get[AlphaPlugin].map{a =>
        P2.set(P.copy(b = a.p.b + 2))
      }

      val ain = in UInt(P.a bits)
      val bin = in UInt(P.b bits)
      val cout= out (ain + bin)
    }
  }

  SpinalVerilog{
    val plugins = ArrayBuffer[FiberPlugin]()
    plugins += new AlphaPlugin((AlphaPlugin_P(a = 8, b = 8)))
    plugins += new BetaPlugin((BetaPlugin_P(a = 12, b = 12)))
    VexiiRiscv(plugins)
  }
}

Note that the idea is to only use the setup phase code for locking / retain other plugins. Everything else is done in build phase.

Um, thank you! My intention is that it has an initial setting, and in other cases, it updates automatically according to the interrelationship between plugins (this is just an example). My design philosophy is to hope that the hardware I design can be like a living thing (or say it is like a cell, which can have various fusion and combination). For the underlying framework, if it can bring more possibilities and flexibility to the designer, it would be very nice.

Dolu1990 commented 8 months ago

My intention is that it has an initial setting, and in other cases, it updates automatically according to the interrelationship between plugins (this is just an example).

One issue with default value to "blocking" is that it can create race conditions (same in NaxRiscv framework). What i have in vexiiriscv in this kind of case, is more like a plugin which take full responsability over a blocking and which solve the value once by looking if other plugin have something to say about it via some software interface.

Else one way you can acheive it would be during setup phase the plugins which want to mutate the blocking would read it, then unset it, mutate the value set it. (unset isn't implemented but could be added)

dreamflyings commented 8 months ago

My intention is that it has an initial setting, and in other cases, it updates automatically according to the interrelationship between plugins (this is just an example).

One issue with default value to "blocking" is that it can create race conditions (same in NaxRiscv framework). What i have in vexiiriscv in this kind of case, is more like a plugin which take full responsability over a blocking and which solve the value once by looking if other plugin have something to say about it via some software interface.

Else one way you can acheive it would be during setup phase the plugins which want to mutate the blocking would read it, then unset it, mutate the value set it. (unset isn't implemented but could be added)

『iIdea』This is not urgent to modify, you can focus on the more important things on your side first (I wrote an update function). I have an idea, just for your reference. After using the FiberPlugin framework, I feel that this framework is not as simple and easy to use as the Framework framework. In the Framework framework, I can easily know the running order of each create, and I can also easily customize their execution order (through using Lock); but the execution order of the FiberPlugin framework, I still don’t understand, let alone customize, often make mistakes. I studied it and thought about the reason why the Framework framework is simple and easy to use, which is to rely on layered negotiation (this is a bit like the LasyModule framework in chiselHDL, it only has one negotiation layer, and the other layer is the hardware layer). Layered negotiation is simple and intuitive, and easy to use, I think it is determined by the characteristics of the hardware description language, because the hardware circuit can be parallel, so you can always put the things that need to be negotiated on the upper layer, and generate the circuit part on the lower layer. This kind of underlying algorithm design is concise and clear. The idea of the Framework framework is very good, I think we should absorb its advantages; FiberPlugin framework is also very good, although I still don’t know how to use it, I feel it is very flexible, but the learning cost is high. I wonder if we can combine the advantages of the two above, and the idea of layered negotiation that I just mentioned, to optimize and simplify our architecture. (As you said, FiberPlugin is like maskefile to handle dependencies; but, you should also know that people like us who are not very good at software programming, will be more accustomed to using cmake). Whether we use during setup, during build, create config, create early, create late, or awaitBuild (this will put the blocking thread to the end ). If we can use the idea of layered negotiation, hide lock (or only use it in very special cases), that would be great (it should also be as easy to combine as Component and area). What do you think? In addition, for the Miaouuuu2 example, if you use the layered negotiation method to design, you don’t need to explicitly use Retainer and await (maybe you just gave an example) THX!This is only an idea~

Dolu1990 commented 8 months ago

Framework framework

By "Framework framework" do you refer to the NaxRiscv one ?

Fondamentaly, i think the core behind the VexiiRiscv FiberPlugin is a super set of what the NaxRiscv supports. I mostly restrict VexiiRiscv to use a subset of it to avoid taking "non scalable" shortcuts. As quite some time, the easy/more aproachable API had drawback on the long term.

The issue i had with the NaxRiscv clear cut between one setup Area and build Area, is that it create two variable scope instead of a unified one.

The issue i had with a clear cut between negociation and elaboration phase, is that sometime it duplicate some code, as sometime you elaboration code can drive the negociation in a concise manner.

Whether we use during setup, during build, create config, create early, create late, or awaitBuild (this will put the blocking thread to the end )

Could you provide some pseudo code example of your ideal ?

I just pushed some doc for the ordering : https://github.com/SpinalHDL/SpinalDoc-RTD/commit/fc720a3c03cc8fe73581320658bdc3a8fa0a4318

Please, let's me know if anything is unclear, and if possible provide an example to be sure i understand the case as you intended :)

dreamflyings commented 8 months ago

Framework framework

By "Framework framework" do you refer to the NaxRiscv one ?

Fondamentaly, i think the core behind the VexiiRiscv FiberPlugin is a super set of what the NaxRiscv supports. I mostly restrict VexiiRiscv to use a subset of it to avoid taking "non scalable" shortcuts. As quite some time, the easy/more aproachable API had drawback on the long term.

The issue i had with the NaxRiscv clear cut between one setup Area and build Area, is that it create two variable scope instead of a unified one.

The issue i had with a clear cut between negociation and elaboration phase, is that sometime it duplicate some code, as sometime you elaboration code can drive the negociation in a concise manner.

Whether we use during setup, during build, create config, create early, create late, or awaitBuild (this will put the blocking thread to the end )

Could you provide some pseudo code example of your ideal ?

I just pushed some doc for the ordering : SpinalHDL/SpinalDoc-RTD@fc720a3

Please, let's me know if anything is unclear, and if possible provide an example to be sure i understand the case as you intended :)

Um, thank you for providing the document, I just offer an idea, without any preference. Here is a concise example I provide, to illustrate the meaning of ‘the upper layer negotiates, the lower layer builds’. This example can be easily changed to the naxriscv framework. The significance of this example is to describe the structure in a more concise and clear way, and use as few lock and ‘goto’ functions as possible that require thinking.

object Miaouuuu12 extends App {
  import spinal.core._
  import spinal.lib.misc.plugin._
  import spinal.core.fiber._
  class SubComponent extends Component {
    val host = new PluginHost()
  }
  class StatePlugin extends FiberPlugin {
    val logic = during build new Area {
      val signal = Reg(UInt(32 bits))
    }
  }
  class DriverPlugin extends FiberPlugin {
    var incrementBy = 0

    val logic = during build new Area {
      val sp = host[StatePlugin].logic
      sp.signal := sp.signal + incrementBy
    }
  }
  class SetupPlugin extends FiberPlugin {
    val logic = during setup new Area {
      val dp = host[DriverPlugin]
      dp.incrementBy += 1
    }
  }
  class TopLevel extends Component {
    val sub = new SubComponent()
    sub.host.asHostOf(
      new DriverPlugin(),
      new StatePlugin(),
      new SetupPlugin(),
      new SetupPlugin() //Let's add a second SetupPlugin, because we can
    )
  }
  SpinalVerilog{ new TopLevel }
}
Dolu1990 commented 8 months ago

Hi,

Thanks for the example. Yes right, you can use the setup phase for some negociation without lock in many cases, (as long as you have no more than one layer of negociation).

The issue in VexiiRiscv is that there is many layers of negociations (things depend on which depend on and so on), so i'm trying to implement thing in a general way, without assumptions. But i guess you are right, as long all the parameters leading to the execution of the setup phase aren't obtained through negociation, that's fine and easier to do like in the example.