Paying it Forward: Documenting Your Hardware Project

Approaches to documenting a hardware description language using lxsocdoc

Sean Cross - https://xobs.io/ - @xobs

Hardware is different

  1. Massively Parallel
This is the Open ISA miniconf, which today tends to mean FPGAs. This means that hardware and software are both extensible, and developers will be able to extend the hardware in addition to making modifications to your software package. Undocumented hardware is bad. There are all sorts of quirks, and even if you have the source code, it can be very difficult to read. I'm the primary developer for the Fomu project, and this talk will cover some of the issues I've run into with respect to documentation. It is most directly related to the LiteX and Migen projects, but the concepts will carry over into any other Hardware Description Language you may use. I'll briefly cover various methods of writing HDL code, then cover the rationale behind the approach we take with lxsocdoc, then give an example of how to use lxsocdoc and how you might apply it to your language. Finally, I'll cover the implications of having documented hardware and how this will help you pay it forward. Verilog and VHDL are kind of the C or assembly of the FPGA world. They're universal, but somewhat unwieldy to use. You need to manually set up your address decoders, and documentation is very free-form. Common approaches today involve comments in the HDL and/or C header files. This works, but we can do better. We just need to describe the hardware better. ```//Hardware definitions of the SoC. Also is the main repo of documentation for the //programmer-centric view of the hardware.``` Fomu uses LiteX, which is related to Migen. This is a hardware description language written in Python. You write Python code and run the program, and it generates a design file -- either Verilog code, or a Yosys netlist. There are many other alternatives such as SpinalHDL or Chisel. By writing in Python as opposed to direct Verilog, we get a lot of nice primitives. The examples from this talk are taken from lxsocdoc and LiteX, but most higher-level hardware description languages can take similar approaches to documentation. In LiteX, two of the primitives used to expose hardware registers to the CPU softcore are CSRStorage and CSRStatus. Instead of manually wiring up a crossbar and decoding the addresses ourselves, we just need to write `self.regname = CSRStatus(8)`, and the build system will wire up 8 bits of read-only memory to the target CPU. Similarly, `self.othername = CSRStorage(8)` will give 8-bits of write-only memory. This works well, but exposes a new problem: Documentation. As an example, I was working with the SPI Flash block in litex, and wanted to know how the bitbang driver worked. There wasn't any documentation except the source, which looked like this: self.bitbang = CSRStorage(4) If(self.bitbang.storage[3], dq.oe.eq(0) ).Else( dq.oe.eq(1) ), If(self.bitbang.storage[1], # CPOL=0/CPHA=0 or CPOL=1/CPHA=1 only. self.miso.status.eq(dq.i[1]) ), dq.o.eq(Cat(self.bitbang.storage[0], Replicate(1, spi_width-1))) You can kind of understand it, but it does take a lot of mental power to work through it. I started by creating aliases for the various elements in the storage array, but then I thought: There has to be a better way! As an aside, Python has something called Pydoc and Docstrings. These are comments that go at the top of functions and classes that let you describe what a Python object is and how to use it. This is almost what we want, except once the final SoC is generated we don't really care so much about things like constructor arguments or method properties. Documentation for the end user is different from documentation for the module developer. This is when I hit upon the idea of `lxsocdoc`. The basic idea is that Python is really good at introspecting Python, so let's add a little bit more information to the CSR objects to make our life easier. And so, after working with the LiteX creator Florent, we refactored the bitbang definition to this: self.bitbang = CSRStorage(4, fields=[ CSRField("mosi", description="Output value for MOSI pin, valid whenever ``dir`` is ``0``."), CSRField("clk", description="Output value for SPI CLK pin."), CSRField("cs_n", description="Output value for SPI CSn pin."), CSRField("dir", description="Sets the direction for *ALL* SPI data pins except CLK and CSn.", values=[ ("0", "OUT", "SPI pins are all output"), ("1", "IN", "SPI pins are all input"), ]) ], description=""" Bitbang controls for SPI output. Only standard 1x SPI is supported, and as a result all four wires are ganged together. This means that it is only possible to perform half-duplex operations, using this SPI core. """) Now the actual bitbang logic looks like: If(self.bitbang.fields.dir, dq.oe.eq(0) ).Else( dq.oe.eq(1) ), If(self.bitbang.fields.clk, # CPOL=0/CPHA=0 or CPOL=1/CPHA=1 only. self.miso.status.eq(dq.i[1]) ), dq.o.eq(Cat(self.bitbang.fields.mosi, Replicate(1, spi_width-1))) This is a little bit easier to understand -- no longer are we looking at indices in an array to determine what field does what. Instead we get actual named fields. But because Python can introspect Python very easily, this is just the beginning. After the design is elaborated and the output file is generated, we can iterate through the resulting tree and pick out any CSR objects and using any additional information. We can actually generate a full reference manual, just like one you would receive from a SoC Vendor. For example, this is what the start of the Fomu SPI Flash documentation looks like: [Register Listing for LXSPI] This is already pretty useful. You can hand this file to someone and show them how your design works. But the title of this talk is called "Paying it Forward", and I can tell you from experience that having such a reference manual for yourself while developing software for your own hardware is still invaluable. Hardware designs are complex things, and not having to decode bitfield offsets in your head or constantly referring to various sections of code to see how it's implemented saves valuable time. So now we have register documentation. Can we do better? Of course we can. SoC reference manuals are more than just register definitions. They also include background information on protocols, as well as more elaboration on how the block works. We can take a cue from CSRs themselves, and add module documentation in a similar fashion. ---ModuleDoc--- Having documentation for humans is great, but we can go one step further and make documentation for computers. SVD is an XML format defined by ARM that defines various aspects about a chip, including memory layout, interrupt map, and register sets. SVD includes information such as default values and field bits, all information we have thanks to the introspectability of Python. In addition to generating a reference manual for humans, we can generate an SVD file that's usable in a wide variety of areas. For example, we can turn an SVD file into a Rust Peripheral Access Crate (PAC) using `SVD2Rust`, giving us an easy way to safely access all peripherals on a device. We can also import this SVD file into an emulator such as Renode, which will print out fields and flags that get accessed, giving us greater visibility into what a program is doing. lxsocdoc intro to litex/migen concept of mixins concept of documentation sections what the output can look like what's coming in the future documenting interrupts introspecting classes other approaches how you can help why this helps you Benefits: * Generating reference manuals * SVD * SVD2Rust