Paying it Forward: Documenting Your Hardware Project

Approaches to documenting a hardware description language using lxsocdoc

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

Introduction

About Me

Undocumented Hardware = Bad

(But so easy to do!)

Talk Outline

  1. How to write HDL Code
  2. Rationale behind lxsocdoc
  3. Examples of lxsocdoc
  4. Benefits of this approach

Motivation

// Hardware definitions of the SoC. Also is the main repo of
// documentation for the programmer-centric view
// of the hardware.
/* Start of memory range for the UART peripheral */
#define UART_OFFSET     0x10000000
/* Offset of the data register for the debug UART. A write
   here will send the data out of the UART. A write when a
   send is going on will halt the processor until the send is
   completed. A read will receive any byte that was received
   by the UART since the last read, or 0xFFFFFFFF when none
   was. There is no receive buffer, so it's possible to miss
   data if you don't poll frequently enough.
   The debug UART is always configured as 8N1. */
#define UART_DATA_REG   0x00

mach_defines.h, Hackaday 2019 Con Badge

About LiteX

  • Hardware Description Language embedded in Python
    • Doesn't run Python in hardware!
  • Emits Verilog (or Yosys netlists)
  • Makes it easy to create a SoC
  • Powers the LCA2020 video production setup

LiteX Primitives

class GPIOOut(Module, AutoCSR):
    def __init__(self, signal):
        self._out = CSRStorage(len(signal))
        self.comb += signal.eq(self._out.storage)

Case Study: SPI Bitbang Module

self.bitbang = CSRStorage(4)
If(self.bitbang.storage[3],
    dq.oe.eq(0)
).Else(
    dq.oe.eq(1)
),
# CPOL=0/CPHA=0 or CPOL=1/CPHA=1 only.
If(self.bitbang.storage[1],
    self.miso.status.eq(dq.i[1])
),
dq.o.eq(
    Cat(self.bitbang.storage[0], Replicate(1, spi_width-1))
)

Aside: Python Docstrings

def _format_cmd(cmd, spi_width):
    """
    `cmd` is the read instruction. Since everything is
    transmitted on all dq lines (cmd, adr and data), extend/
    interleave cmd to full pads.dq width even if dq1-dq3 are
    don't care during the command phase: For example, for
    N25Q128, 0xeb is the quad i/o fast read, and extended
    to 4 bits (dq1,dq2,dq3 high) is: 0xfffefeff
    """
    c = 2**(8*spi_width)-1
    for b in range(8):
        if not (cmd>>b)%2:
            c &= ~(1<<(b*spi_width))
    return c

New Register Definition

self.bitbang = CSRStorage(4, fields=[
    CSRField("mosi", description="Output value for MOSI..."
    CSRField("clk", description="Output value for SPI CLK..."
    CSRField("cs_n", description="Output value for SPI C..."
    CSRField("dir", description="Sets the dir...", 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.""")

Refactored SPI Bitbang

If(self.bitbang.fields.dir,
    dq.oe.eq(0)
).Else(
    dq.oe.eq(1)
),
# CPOL=0/CPHA=0 or CPOL=1/CPHA=1 only.
If(self.bitbang.fields.clk,
    self.miso.status.eq(dq.i[1])
),
dq.o.eq(
    Cat(self.bitbang.fields.mosi, Replicate(1, spi_width-1))
)

Generating a Manual

Interrupts

Undocumented Fields

More Documentation: ModuleDoc

Protocol Documentation

SVD: Documentation for Machines

SVD2Rust: Generating Safe Accessors

fn init(&mut self) {
    self.registers
        .ctrl
        .write(|w| w.exe().bit(true).curren().bit(true).rgbleden().bit(true));
    self.write(LEDDEN | FR250 | QUICK_STOP, LedRegister::LEDDCR0);

    // Set clock register to 12 MHz / 64 kHz - 1
    self.write(((12_000_000u32 / 64_000u32) - 1) as u8, LedRegister::LEDDBR);

    self.write(
        BREATHE_ENABLE | BREATHE_MODE_FIXED | breathe_rate_ms(128),
        LedRegister::LEDDBCRR,
    );
}

Renode: Fancy Register Logging

Benefits of Higher Level Languages

Documentation helps you

Documentation helps others

Thank you

Questions