-
-
Notifications
You must be signed in to change notification settings - Fork 307
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
Generate diagrams based on SpinalHDL code #1079
Conversation
Good work, Zhaokun.
|
This is impressive! I agree this should be under
Small suggestion, if the design has a fixed frequency maybe we could put that on the diagram too? |
lib/tools would be better. |
One more thing, as we talked before, I think the clk is not necessarily connect visibly. There is already clockdomain color, though. |
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.
This looks quite promising, I added a few small comments below that I stumbled over when trying it out/skimming over it.
Apart from that:
I played around a bit, and e.g. this code:
case class Inner() extends Component {
val io = new Bundle {
val i = in port Bool()
val o = out port Bool()
}
io.o := RegNext(io.i)
}
will generate this image:
with the output of the register not being connected.
Also this:
case class DiagramDemo() extends Component {
val io = new Bundle {
val i = in port Bool()
val i2 = in port Bool()
val sel = in port Bool()
val o = out port Bool()
val i2b = in port Bits(2 bit)
val muxout = out port Bool()
val regout = out port Bool()
}
io.muxout := io.i2b.mux(
0 -> (io.i & io.i2),
1 -> (io.i | io.i2),
2 -> (io.i ^ io.i2),
3 -> (io.i)
)
io.regout := RegNextWhen(io.i, io.sel)
io.o := False
when(io.i) { io.o := True }
}
generates
Where we see a similar thing with the outputs not being connected, and the assignments to io.o
missing completely.
Nitpick: The blue on blue colorscheme of the examples above is kind of hard to read if you ask me.
PS: please also run scalafmt on the code.
|<html> | ||
|<head> | ||
| <meta charset="UTF-8"> | ||
| <title>RTL连接图</title> |
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.
Please keep with the remainder of SpinalHDL and use an English title
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.
Sorrry,i forgot to change it.I have changed it to "RTL diagrams based on SpinalHDL code"
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.
That's better, we could also use the component name of the top component in the title e.g. f"{rtl.toplevelName} RTL diagrams"
val fileName = rtl.toplevelName + ".html" | ||
val file = new File(fileName) | ||
val pw = new FileWriter(file) | ||
val builder = new StringBuilder() |
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.
You have a writer
already, why not use that directly (and also pass it to the sub-components for use there?) There is no need to build the string fully in memory?
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.
I changed it based on your advice.
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.
I think I explained poorly: What I meant was to use the writer directly, without the StringBuilder. The StringBuilder will piece everything together in RAM, whereas if you use the writer it can be flushed to disk. But you'd have to pass the writer to GenerateOneDiagram.beginDraw(writer)
to do that.
(and if you want/need the output in a String you can still use StringWriter
if you use the base Writer
as the parameter type)
I have made modifications to the program and fixed some problems based on your suggestions .And I placed it in the spinal.lib.tools package. I have also added a test file which will generate an .html file. |
This is an environment requirement. Just like we should install Quartus before we use Qsysnify. It's default value can be set as "https://github.com/davidthings/hdelk", so that if user is online no manual copy is required. |
| }) | ||
| } | ||
|</script> | ||
|<script src="https://cdn.jsdelivr.net/gh/davidthings/hdelk@master/js/elk.bundled.js"></script> |
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.
This can be a default URL base, however, for users do not have network access might want to change this.
So use a variable is better here.:)
No worries - stuff like that happens ;-) I tested the latest version - the CDN links are much more comfortable than the local ones, but I agree with @Readon that it would be nice to have to option of passing an alternative path as a configuration. I played around a bit more and stumbled over another small thing that's unexpected - it's cool that bundles are shown as a single signal, but accessing a signal from a bundle does not seem to get rendered/rendered incorrectly ( case class ElkDemo() extends Component {
val io = new Bundle {
val axii = slave port Axi4(Axi4Config(addressWidth=32, dataWidth=32, useId=false))
val axio = master port Axi4(Axi4Config(addressWidth=32, dataWidth=32, useId=false))
val test = out port Bool()
val testR = out port Bool() setAsReg()
}
io.axii >> io.axio
io.test := io.axii.r.valid
io.testR := io.axii.w.valid
} |
add a new edge label. fix a bug.
Thank you so much for testing for me! I have solved one bug. As for the signal testR,it is a little bit complex.Please give me some time to solve it. at the latest version:
|
add colors to ports.
Maybe it would be good to use black as default node color, and just distinguish the clock domains by port color only? |
Great job! It's really pleasant to see the first circuit diagram generator application building upon Things so far look good to me but still, I have a little advice. From an architectural point of view, the generator is tightly coupled with the SpinalHDL internal data model. The process of data transformation is like SpinalHDL data model -> Elk HTML format. A better way to make things more scalable or extensible is to add a middle layer, such as SpinalHDL data model -> JSON -> Elk HTML. The middle JSON format serves as a module interface database that holds all the necessary information for the generator to draw the diagram. There will be two benefits if a middle layer is introduced.
Though, it's just a piece of advice and requires time and effort to reconstruct the project (I can see that it's nearly finished and not the early stage of the project). Thus, you can ignore my opinion and keep working. I'm very much looking forward to seeing the final product :) |
Good idea!
There are still problems to be solved, as soon as we tried to draw diagram for Spinal cord or um, whose top level also contains amount of logic inside. |
I appreciate your PR very much. Without the ModuleAnalyzer/DataAnalyzer , this project would have been very difficult. However, since this project relies heavily on the internal structure of SpinalHDL, it would be difficult to reconstruct to support a universal format. Additionally, I am not yet familiar enough with JSON and Scala, so I may not be able to complete the reconstruction. I apologize for not being able to accept your suggestion. |
@@ -0,0 +1,552 @@ | |||
package spinal.lib.tools | |||
|
|||
|
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.
I think you should run scalafmt on this code to avoid fmt-lint reports.
/** Initializing data */ | ||
private val fileName = topLevelName + ".html" | ||
private val file = new File(fileName) | ||
private val pw = new FileWriter(file, true) |
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.
This pw could be named as writer?
for (topInOut <- topInOuts) { | ||
if (haveParent(topInOut)) { | ||
val rootParent = findParent(topInOut) | ||
if (rootParent.flatten.head.isInput) topNode.inPorts.add(ElkPort(rootParent.getName(),findPortHighlight(topInOut))) |
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.
How about use pattern match here?
else if (allClk.size == 1) topNode.highlight = clkMap(allClk.head.toString()) | ||
} | ||
|
||
private def GenAllNodes(): 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.
Maybe we should use a hierarchy class architecture to help to clean this logic.
} | ||
|
||
private def GenAllEdges(): Unit = { | ||
/** Getting the sonName of net */ |
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.
This is really too big to look over.
pw.write(s"""{id:"${thisNode.labelName}",\n""") | ||
if (thisNode.typeName != "") pw.write(s"""type:"${thisNode.typeName}",\n""") | ||
if (thisNode.highlight != 0) pw.write(s"""highlight:${thisNode.highlight},\n""") | ||
if (thisNode.inPorts.nonEmpty) { |
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.
How about moving this logic into port specific class? so all related logic can be put into that.
Above code seems identical while it is in or out.
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.
A few comments of stuff that I observed. Most are suggestions that I think would make the code easier to read, some are small issues.
Keep up the good work!
val parentList = thisSon.getRefOwnersChain() | ||
var returnParent = thisSon.parent | ||
val loop = new Breaks | ||
loop.breakable { |
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.
Instead of using breakable
I'd could just return
when you have found your parent, faster and IMHO easier to read
If you want to keep the breakable there is no need for the loop object in this case (since the breakable is not nested), you can
import Breaks.{breakable, break}
breakable {
// ...
break
// ...
}
} | ||
|
||
private def haveRegParent(thisSon: BaseType): Boolean = { | ||
var judge = false |
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.
Same as above, simple return true
when you have found your answer - simpler to read and faster since it would stop iterating as soon as you know there is a Data
On another note: The function is called haveRegParent
indicating that you are looking for a register, but you are never checking that the Data
is a register?
val allClk = moduleAnalyze.getClocks | ||
var clkCounter = 1 | ||
for (thisClk <- allClk) { | ||
if (!clkMap.contains(thisClk.clock.getName())) { |
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.
Is there a specific reason why you are using the names in the dictionary instead of the signals themselves? Two CDs can actually have the same name...
Nitpick: clkMap
is maybe a little bit misleading since you also add reset and softReset signals to the map
Using the name of the clock lead to this diagram:
For this code:
case class DiagramDemo() extends Component {
val io = new Bundle {
val i = in port Bool()
val subo = out port Bool()
val subo2 = out port Bool()
}
val sub = new Component {
val i = in port Bool()
val o = out port Bool()
val myClockDomain = ClockDomain.external("clk")
val coreArea = new ClockingArea(myClockDomain) {
o := RegNext(i)
}
}
val sub2 = new Component {
val i = in port Bool()
val o = out port Bool()
val myClockDomain = ClockDomain.external("clk")
val coreArea = new ClockingArea(myClockDomain) {
o := RegNext(i)
}
}
sub.i := io.i
io.subo := sub.o
sub2.i := io.i
io.subo2 := sub2.o
}
In reality this can happen when reusing multiple modules (that's why spinal internally only uses the name of the clock as a weak name, in the HDL the two are named clk_clk
and clk_clk_1
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.
Thank you for your reviews .They are very helpful .I have already made the changes for the other reviews, but I encountered some issues that I couldn't solve when dealing with this particular review. I was unable to obtain the necessary information to distinguish between two clock domains with the same name, so I have submitted an issue. #1090
private val allInOuts = moduleAnalyze.getPins(_ => true) | ||
private val everyRegisters = allRegisters ++ systemRegisters | ||
|
||
private def haveParent(thisSon: BaseType): Boolean = { |
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.
private def haveParent(thisSon: BaseType): Boolean = { | |
private def hasParent(thisSon: BaseType): Boolean = { |
/** Generating the color mapping for clk */ | ||
private def GenColorMap(): Unit = { | ||
val allClk = moduleAnalyze.getClocks | ||
var clkCounter = 1 |
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.
nit: you could skip counting here and use val clkCounter = clkMap.size + 2
inside the loop
} | ||
|
||
for (topInOut <- topInOuts) { | ||
if (haveParent(topInOut)) { |
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.
the code itself
If I understand correctly you are using findParent
to find the bundle a signal is part of? And then after flattening it check the direction to see if the bundle is an input or output? If so then this code won't reliably work for IMasterSlave
bundles since you are not guaranteed that head
will reflect whether the bundle in a master (=output) or slave (=input).
case class Example() extends Bundle with IMasterSlave {
val aa = Bool()
val bb = Bool()
def asMaster() = {
in(aa)
out(bb)
}
}
case class DiagramDemo() extends Component {
val io = new Bundle {
val slave_port = slave port Example()
val master_port = master port Example()
}
io.master_port <> io.slave_port
}
generates:
(if it does not show for you, just swap in/out: out(aa)
and in(bb)
)
repetition
The same (?) code is used just a few lines further down (176). Make a function that you call twice.
alternative implementation
I have observed that every time you use haveParent
you also use findParent
.
Consider changing findParent
to return an Option[Data]
and None
if there is no parent. That would remove the need to loop through the signals twice.
Here you could then match...
returnParent = dataParent | ||
loop.break() | ||
} else { | ||
dataParent.setName( |
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.
The diagrammer should not modify that data model of the HDL code.
} | ||
|
||
/** Integrating all methods to draw an image */ | ||
def beginDraw(): 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.
You should pass the writer as a parameter instead of opening it for append during construction and closing it here.
} | ||
|
||
/** Generating HDElk language for Node */ | ||
private def drawNodes(thisNode: ElkNode): 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.
I'd move this code into the ElkNode
}</button></a> \n""") | ||
} | ||
writer.write(s"""</div><br><br><br><br>\n""") | ||
writer.close() |
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.
See above note about not opening/closing the writer multiple times. It should really only be opened once and then passed as a parameter.
Rebuilt some function.
delete tansprant color delete the clk
change the legend
I have refactored my code. Please let me know if there are any shortcomings in terms of functionality and which parts of the code still need modification. I will do my best to make the necessary changes. |
Hi ^^ overall, maybe the issue currently is for none-small design.
One typical example could be recent reimplementation of the StreamFifo, which is now very much structured using Area |
Change some judgement of bus.
Hi ^^
So i would say, in your screenshot above, combinatorial signals (stuff which isn't reg nor io) as for instance when_test12_l64, instead of being a box, could just represent connectivity.
right :) something things are even mixed :/ |
Thank you again for your suggestions. ^^ |
I think we can merge this with a compatible API first. Then we could modify it if it is necessary later. |
Context, Motivation & Description
This pull request attempts to implement the idea #635 to automatically generate diagrams based on SpinalHDL code. I have created a new function called GenerateDiagrams that can be used in the following way:
This function will generate a new file named _.html .
If you have an internet connection, you can simply open the HTML file and the dependencies will be loaded automatically from the specified URLs. You can see several diagrams in the pages.
These diagrams are generated by the HDElk tool.(https://github.com/davidthings/hdelk ).
If you do not have an internet connection, you need to download the JS packages from this library and place them in the same folder as your HTML file. Then, according to the readme of this library, you need to add the dependencies to your HTML file. Then you can open the HTML file to see the diagrams.
Example:
It will generate this diagram:
![image](https://user-images.githubusercontent.com/117883290/230715684-7abefc73-b4d1-44fe-88ed-8ce279b88397.png)
I have also tested the GenerateDiagrams class on some more complex SpinalHDL projects to ensure the accuracy of the generated diagrams.
One issue that currently exists is that if you use many complex logic operations in your project, the system will automatically generate many registers to implement these operations, and the generated diagrams may become messy as a result.I have tried various methods to simplify the generated diagrams, but the results have been less than satisfactory.Please let me know if you have any thoughts or suggestions on this approach.
Impact on code generation
None
Checklist
/** */
?