-
-
Notifications
You must be signed in to change notification settings - Fork 303
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: protocol simulation API; implemented for AHB #879
base: dev
Are you sure you want to change the base?
Conversation
Also it would be good to have a "faster" |
Lol I get a relevant error here, but it fails to compile earlier because of other parts of the code on my machine… https://github.com/SpinalHDL/SpinalHDL/actions/runs/3175845982/jobs/5174410648#step:4:244 EDIT: fixed |
Just my 0.02€ after having a quick look at 1AM... ;-) I had a look over it and I have general thoughts: some part of this I really like, while others feel to me like they are trying to solve non-issues: One such thing is the machinery that you need to create the sequences of accesses, where there already is a way to do this: just fork and e.g. call I also find the seperation that is there in the sim classes for Stream, Apb, Bmb into a driver/agent and a monitor to be quite advantageous - it makes writing testcases often much easier - could this split be implemented here as well? I think thats enough negativity for 1am |
Creating sequences is useful for pipelined protocols like AHB, and to be able to describe the whole behavior once, it is useful to start transactions before the end of the previous one while managing
I don't know about it, will see that.
|-( |
htrans | ||
) | ||
|
||
def apply(attr: Hburst): AhbAttributes = copy(hburst = Some(attr)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Funny pattern XD
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it too unidiomatic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, it is a bit too much i think ^^
As instead you can use case class and named argument :
case class AhbAttributes(
val hburst: Option[Int] = None,
val hmastlock: Option[Boolean] = None,
val hprot: Option[Int] = None,
val hsize: Option[Int] = None,
val htrans: Option[Int] = None
)
val x = AhbAttributes(hprot=Some(2))
val y = x.copy(hsize=Some(32), htrans=Some(64))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed I started with a case class, then I wanted to add def hburst(value)
and it obviously did not work so I removed case
, then I realized I wanted to access values by these names so I left things half done etc.
Now I've added withAttribute
and put case class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But overall, is the following a good pattern ?
def apply(attr: Hburst): AhbAttributes = copy(hburst = Some(attr))
I would say, it is fancy, but overall add quite some verbosity in the library + extra complexity. So i guess the best is to remove it ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, as there is implicit conversion from Hanything
to Int
, the withAttr
would work anyway. Maybe I should also remove attribute classes which do not enable to build values… Yes, I'll definitely clean things here ^^'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I am still wondering (since I wrote the first commit) if gathering attributes in a struct is really useful? I mean, I the PR I show the version with it and without it, and sometimes parts of it are ignored, maybe I can just remove this class after all… And this notion is not in the protocol…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I am still wondering (since I wrote the first commit) if gathering attributes in a struct is really useful? I mean, I the PR I show the version with it and without it, and sometimes parts of it are ignored, maybe I can just remove this class after all… And this notion is not in the protocol…
Ahh that's very much usage dependent, as you feel it. i don't realy have an opinon on it. Just that sometime, keeping things more raw, isn't a bad thing ^^
* Boots current transfer, which will boot the subsequent transfer of the | ||
* sequence, etc. | ||
*/ | ||
def boot(): Unit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm maybe start instead of boot ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Wilco
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well actually, to me boot conveys the idea that I start this one and then it will start the next one etc. Start is just "I start this one". It is related to your other question about transfer vs. sequence. (All is related lol) What do you think about it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to me boot conveys the idea that I start this one and then it will start the next one etc.
Ahhh, this is usage dependent, is see.
so, in some ways, i'm not sure we should bake in the linked list to the Task, but more decouplate things, where a sequance would be a Task which would run multiple sub task sequancialy.
Maybe that the first question to be answer ^^
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed I agree with splitting logic. (If it is easier to explain it is easier to maintain 😉) I am responding in main thread.
What is the difference between a transfer and a sequance ? |
Interesting point for docs (maybe design too actually). A transfer is a single transfer, and a sequence is several transfers set to start the next one when appropriate / ASAP. I find it useful to describe master's behavior once (but you can still start another sequence later if you want), especially for pipelined protocols like AHB. For instance:
|
Hmm, so, What's about changing the name of Transfer to Task ? i mean, that would be a bit more usage agnostic. Also, maybe the support of Task agregation should not be backed in the Task class ? But more be like a Task specialisation : class Task{
def start ...
def await ...
}
class TaskQueue extends Task{
val tasks = Queue[Task]()
...
}
val t = TaskQueue(t1, t2, t3)
t += t4 ? I'm not sure of any thing, just guessing XD |
I think it is a good point, for instance for I2C there would be elementary Tasks (START, ACK, STOP) that are composed to do actual transfers? On the other hand I am afraid the name is too generic and lacks an clear definition.
I would like that too, but I IMHO ideally only the current transfer knows when the next one can start, so I think splitting this way would require cyclic references. When I wrote this, I was inspired by a methodology where a C program is run in a core to drive a bus, so to imitate this (with a more lightweight approach), I make it possible to define the master's behavior once and then the other stuff, but maybe it is not what we want. |
The API is not protocol-dependent and can be implemented for other protocols as well (even Stream and Flow I guess, will have to try ;). Notice how the way AHB is implemented matches the ARM specification. Same for its tests. This is WIP, the API is not stabilized yet (aka function names can still change). Also, it is not possible to check if it works because other parts of the code do not build. (It was working before being integrated, I swear ;) Discussion to have / things to change: * What about AhbAttributes for master behavior? Should I use it for all transactions or there is a better way to do this? * Slave "setOn" functions should be renamed to explicitly manage 3 types of arguments: reader (addr => data) and writer (addr, data => Unit); then Behavior (Req => Resp) and finally Builder (Req => () => resp, aka Req => Handler). Then implicit conversion Behavior => Builder should be removed. * Should I add a class to just read signals and check that all "must" of the spec are applied or is it already a thing? * AhbMaster is not implemented yet (it is almost the same, just one signal changes I think) * Parts of tester/src/test/scala/spinal/tester/scalatest/lib/bus/amba3/ahblite/sim/hardware.scala are already implemented somewhere I guess.
c88035a
to
ebc95f5
Compare
ebc95f5
to
84bc87c
Compare
If clearer definition is required for some cases, we can always create children classes.
cyclic references ? where ? Also, what's about having a clean Task interface, and having a LinkedTask which would be close to your current implementation ? |
By too generic I mean it is not specific to the notion of protocol and the name could conflict for instance with, I don't know, a part of Spinal's phases, or something related to pipelines or anything. This is why I used To explain what I am thinking to with "cyclic references", let's use the almost-pseudo-code-looks-like-scala example below: val a, b = new Task
val s = new Seq[Task](a, b) Then, when Spliting logic into |
Hmm i often have seen "Transaction" or else "Beat" (for burst oriented stuff)
Exactly, logic of s would be some thing like : while(queue.notEmpty){
val t = queue.head
t.start()
t.await()
queue.pop()
} |
Reduces confusion between CONST and instance (the latter is converted into const) Reduces logic repetition
About driver vs. monitor, I have read the Hence, about the draft implementation, we do not want to pop or delete tasks / transfers / transactions because in the end they contain the read value + useful debug information (for instance for AHB, the number of wait states inserted for this specific transfer, and the total duration of the transfer, in clock cycles). I could use something like I must admit that I was thinking "transaction" when I was writing the API first, then I mixed with "transfer" when implementing AHB, and finally I made things uniform with "transfer" 😁 Maybe "transaction" should be used, because it is more generic because it may not transfer actual data? |
Here is a self contained example (may need a few imports, note you need SpinalHDL upstream), it introduce abolutly zero superflu waits. : object SimTasker extends App{
def TaskBody[T](body : => T) : TaskBody[T] = new TaskBody(body)
class TaskBody[T](body : => T) extends Task{
var yielding : T = null.asInstanceOf[T]
val mutex = SimMutex()
mutex.lock()
override def run() : Unit = {
yielding = body
mutex.unlock()
}
override def await(): Unit = mutex.await()
def getValue() : T = {
await()
yielding
}
}
class TaskQueue extends Task{
val queue = mutable.Queue[Task]()
val mutex = SimMutex()
mutex.lock()
override def run() = {
while(queue.nonEmpty) queue.dequeue().run()
mutex.unlock()
}
override def await(): Unit = mutex.await()
}
SimConfig.withWave.compile(new Component{
val value = in UInt(8 bits)
RegNext(value) //Dummy
}).doSim{dut =>
dut.clockDomain.forkStimulus(10)
dut.value #= 0
// Create tasks
val taskA = TaskBody{
dut.value #= 1
dut.clockDomain.waitSampling()
dut.value #= 2
dut.clockDomain.waitSampling()
println(s"taskA completion at time ${simTime}")
"miaou"
}
val taskB = TaskBody{
dut.value #= 3
dut.clockDomain.waitSampling()
dut.value #= 4
println(s"taskB completion at time ${simTime}")
"rawrr"
}
val taskC = TaskBody{
dut.clockDomain.waitSampling()
dut.value #= 5
dut.clockDomain.waitSampling()
dut.value #= 6
println(s"taskC completion at time ${simTime}")
"wuff"
}
//Create a sequance of tasks
val tasks = new TaskQueue()
tasks.queue ++= List(taskA, taskB, taskC)
//Fork a thread which will print the result of taskA
fork{
println(s"taskA gived ${taskA.getValue()} at sim time ${simTime}")
}
// Start of the main thread active waits
dut.clockDomain.waitSampling()
tasks.run() //This is a blocking call
println(s"taskB gived ${taskB.getValue()} (end of sim at ${simTime})")
}
} Print :
Hmmm transaction is in itself kind of specific, overall here we are describing code which has to run, so why not just Task or something neutral ? |
Thanks for the After thinking to it, I do not want to use Below is my current draft implementation (not tested on AHB) for the new API. There is the same principle of links with It has a When I mentioned that a TaskSeq would So there is the trait, and an object to build a Task from a sequence of tasks. package spinal.lib.sim.protocolSim.master
object Task {
def apply[T <: Task](ts: Seq[T]): TaskSeq[T] = new TaskSeq(ts)
}
trait Task {
def start(): Unit
final def run(): Unit = { start(); await() }
def await(): Unit
def isDone: Boolean
val defaultOnReady: () => Unit
protected var onReady: Option[() => Unit] = None
protected final def beReady(): Unit = (onReady.getOrElse(defaultOnReady))()
final def setOnReady(handler: => Unit): Unit = {
assert(onReady.isEmpty, "setOnReady called twice on the same transaction")
onReady = Some(() => handler)
}
final def unsetOnReady(): Unit = onReady = None
} The definition of a sequence of tasks is below, with at quick test. It is still possible to use package spinal.lib.sim.protocolSim.master
class TaskSeq[T <: Task](seq: Seq[T]) extends Seq[T] with Task {
def iterator: Iterator[T] = seq.iterator
def length: Int = seq.length
def apply(idx: Int): T = seq.apply(idx)
def start() = head.start()
def await() = last.await()
def isDone: Boolean = last.isDone
val defaultOnReady: () => Unit = last.defaultOnReady
for (i <- 0 until length - 1)
seq(i).setOnReady(seq(i + 1).start())
last.setOnReady(beReady())
}
object Test extends App {
case class Printer(text: String) extends Task {
private var _isDone = false
def start(): Unit = { println(text); _isDone = true; beReady() }
def await(): Unit = assert(isDone)
def isDone: Boolean = _isDone
val defaultOnReady: () => Unit = { () => println("reset prints") }
def specificStuff() = println("specific")
}
case class Printer2(n: Int) extends Task {
private var _isDone = false
def start(): Unit = { println(n); _isDone = true; beReady() }
def await(): Unit = assert(isDone)
def isDone: Boolean = _isDone
val defaultOnReady: () => Unit = { () => println("reset prints") }
}
val ts = Task(Seq(Printer("Hello"), Printer("World")))
val ts2 = Task(Seq(Printer("hello"), Printer2(5)))
ts.setOnReady(println("done"))
ts.run()
ts.foreach { t => t.specificStuff() }
ts2.run()
//ts2.foreach { t => t.specificStuff() }
} Notice that types are uniformized to the more specific type possible by Scala, which makes it possible to do I did not want to use |
^ Comments appreciated before I migrate the whole thing to this new API |
You don't have to botther about multi threading race condition, as SpinalHDL enforce things to always happen on the same core in a cooperative maner (coroutine like)
What is the purpose of those ? Is var onReady a way to have some callback on the completion of a task ? |
If I do not update callbacks when the
Okay for you? |
Those two seems good for me ^^ But the onReady realy seems like a hacky thing. To me it should be a task in itself or something. More generaly, what kind of feature the PR want to add ? |
The API is not protocol-dependent and can be implemented for other protocols as well (even Stream and Flow I guess, will have to try ;).
Notice how the way AHB is implemented matches the ARM specification. Same for its tests.
This is WIP, the API is not stabilized yet (aka function names can still change). Also, it is not possible to check if it works because other parts of the code do not build. (It was working before being integrated, I swear ;)
Discussion to have / things to change: