The Rusty Clams

Rust adventures !

Introduction to YARIG

29 Mar 2026

What is YARIG ?

Description

YARIG is both a binary and library to generate the register description of an hardware block, available on crate.io. It stands for “Yet Another Register Interface Generator”, a reference to the fact that there is almost as many generator as there is company developping hardware IP or chips. There is fun blog post explaining on the subject and why you should not make it yourself and use a more standard solution :P

For those not familiar with the concept: most hardware block needs to be configurable and provides some status information easily accessible by a micro-controller (often called Control & status register (CSR) or more simply register interface). This is quite trivial to implement (an address decoder and a structure for each register) but it gets quickly repetitive and you need to maintain not only the hardware implementation (SystemVerilog, VHDL) but also the documentation (HTML, AsciiDoc, IP XAct, ..) and the software description (C, Rust or Python). So the idea is to have “one source of truth” to describe the registers and then use the generator to get all the needed views. While there is a standardized format for that (RDL or Register Description Language), I have opted to define my own format: it reuses a lot of concept used in RDL, like the separation of register definition and register instance but uses a format easier to read/write (at least for me :P) and more importantly this allows me to introduce features not existing in RDL. The syntax is fully described in the GitHub repo.

Why in Rust ?

Since this is a Rust oriented blog, I wanted to explain the language choice.

Funny enough, it did not start as Rust, but as a python script, more than 15 years ago when the commercial tool used in my old company was unceremoniously abandoned … Exactly like described in the blog post I referenced, creating a first version of the tool is quite easy, but it complexify over time: supporting more views and always adding more features.

I had two main issues with my python implementation:

So that’s when I started to get interested in Rust and decided to use this project to learn the language:

And finally I also used Rust to experiment with a small GUI to explore easily a register definition, especially useful at chip level, where you have multiple level of register definition. This uses the eGui crate which I found really nice to use for this kind of simple interface.

Syntax highlight

Syntax highlighting is available for both SublimeText (my editor of choice) or VS Code. In this blog post I will simply use the yaml highlight syntax as it is close enough.

Creating the register description of the Analog Device AD7771

This post will focus on the register description part. When I have time, I will do a separate post to explain how to implement a custom generator using the trait system Rather than writing a wall of test trying to introduce the different features of the library (there is already quite a lot of documentation on GitHub, I thought it would be more effective to try to write a register description using the datasheet of an existing chip. For this small introduction, I have selected the Analog Device AD7771 : this multi-channel ADC seems to be have a good mix of register kind, like register array, enumerated field, pulse field and field larger than the register size.

You can find the full datasheet here and have a look at the register summary/details chapters at the end

Top description

The first thing to describe is the name of the block, the software bus (like APB for example) and the clock and reset kind used.

Looking at the register summary the max address is 0x64 with 8 bits wide register: so we can use an interface with address on 7 bits and the data bus on 8 bits. I will name the software clock clk_rif so that it can be different from the hardware clock (clk by default) allowing clock gating, and a dedicated reset signal name rst_rif_n active low and asynchronous.

The syntax is:

rif: ad7771_rif
  interface: apb
  addrWidth:     7
  dataWidth:     8
  swClock  : clk_rif
  swReset  : rst_rif_n activeLow async

Registers can be group in pages (useful when some optional feature require many register). Here we will do something simple with only one page name Main

  - Main :
  	# Registers declaration
    registers:

Register/Field array

First set of registers for this ADC is the configuration per channel: it contains a gain on 2 bits, one bit to configured the channel as monitor, one to enable a channel meter multiplexer and 4 reserved bits.

Here we have two possible approach:

For the first approach the syntax would be:

    registers:
      - channel_cfg : "Channel configuration"
        - gain    = 0  7:6   "AFE Gain (power of 2)"
        - ref_mon = 0  5:5   "Channel used as reference monitor"
        - rx      = 0  4:4   "Channel meter Mux RX mode"
        - bias    = 0  2:0   "Hidden tuning register"
          hidden
    instances:
      - channel_cfg[8] @ 0x000
      	description: "Channel $i configuration"

For each field the reset value is set to 0, with a range to indicate the position inside the 8b register, followed by a simple description. Since I saw that the first three bits are marked as reserved with a split between the the 3 LSB and the bit 3, I thought I would demonstrate here the idea of public/private field: by flagging the bias field as hidden, it will appear differently in the generated view depending if the generator is called with a private or public flag: it will be renamed as reserved when the generator is set to public, with no description and named bias with its full description when set to private.

And as for the instance description the $i is used to reference the index of the instance so we can have different description for each instance. And note that it is also possible to change the reset value or even field description per instance.

For the second approach, the syntax is:

    registers:
      - channel_cfg[8] : "Channel $i configuration"
        - gain[1]    = 0  7:6   "AFE Gain (power of 2)"
        - ref_mon[1] = 0  5:5   "Channel used as reference monitor"
        - rx[1]      = 0  4:4   "Channel meter Mux RX mode"
        - bias[1]    = 0  2:0   "Hidden tuning register"
          hidden
    instances: auto

When defining the register directly as an array it means that all the register instances will be part of the same struct, and all fields must be arrays, with the final field array depth being a multiplication of the register and the field depth.

The corresponding SystemVerilog structure looks like this:

   typedef struct {
      logic [1:0] gain[8]; // AFE Gain (power of 2)
      logic ref_mon[8]; // Channel used as reference monitor
      logic rx[8]; // Channel meter Mux RX mode
      logic [3:0] bias[8]; // Hidden tuning register
   } t_channel_cfg_sw;

Note: The type of the register is suffixed with _sw as this is the structure controlled by the software. If part of the register were driven by the hardware, there would be another struct suffixed by _hw.

Grouping register in a single struct

Related to the per channel configuration, there is also one register containing one bit to disable each channel individually and another group of 8 register (one per channel) to provide a SyncOffset. At the hardware level it would be nice to have everything under the same struct, as all those signals likely goes to the same block.

To do that, the syntax is simply to provide a group name after the register name:

    registers:
      - channel_cfg[8] : (channel) "Channel $i configuration"
        - gain[1]    = 0  7:6   "AFE Gain (power of 2)"
        - ref_mon[1] = 0  5:5   "Channel used as reference monitor"
        - rx[1]      = 0  4:4   "Channel meter Mux RX mode"
        - bias[1] = 0 3:0 "Hidden tuning register"
          hidden
      - clock_disable : (channel) "Clock disable per channel"
        - disable_ch[8] = 0  0:0   "Channel $i Disable"
      - channel_sync_offset[8] : (channel) "Channel $i Sync offset"
        - sync_offset[1]  = +0  7:0   "Sync offset"

For the sync_offset field, I have decided to demonstrate here a simple way to specify that the register contains a signed value (using +0 as the reset value). Another approach would have been to add a signed property to the field.

And now we have the following structure:

   typedef struct {
      logic [1:0] gain[8]; // AFE Gain (power of 2)
      logic ref_mon[8]; // Channel used as reference monitor
      logic rx[8]; // Channel meter Mux RX mode
      logic [3:0] bias[8]; // Hidden tuning register
      logic disable_ch[8]; // Channel  Disable
      logic signed [7:0] sync_offset[8]; // Sync offset
   } t_channel_sw;

Enumerated field

One of the general user configuration register contains fields to control the drive strength of two different pins (SDO and DOUT). This is an enumerated value (Nominal, Strong, weak and extra Strong).

The syntax allows to re-use the definition of an enum, and also use a named value rather than the decimal one for the reset:

  - user_cfg2 : (user_cfg) "General user configuration 2 (drive/filter)"
    - filter_mode = 0  6:6   "Power down VCM buffer (active low)"
      enum: e_filter_mode
        - SINC3  = 0 "3-tap Sinc"
        - SINC5  = 1 "5-tap Sinc"
    - sar_diag_en = 0  5:5   "Sets SPI interface to read back SAR result on SDO"
    - sdo_drive_str = STRONG  4:3   "Configure drive strength for SDO"
      enum: e_drive_str
        - NOMINAL      = 0 "Nominal"
        - STRONG       = 1 "Strong"
        - WEAK         = 2 "Weak"
        - EXTRA_STRONG = 3 "Extra Strong"
    - dout_drive_str = NOMINAL 2:1   "Configure drive strength for DOUT"
      enum: e_drive_str
    - spi_sync = 0  0:0   "SYNC pulse generated through SPI"
      description:
        When low, this signal is AND’ed with the value on the START pin in the control module and generates a pulse in the SYNC_IN pin.
        When high, this bit is AND’ed with the value on START pin in the control module

In the SystemVerilog Package, this will add an enum definition, which can then be re-used easily in the design:

   typedef enum logic [1:0] {
      NOMINAL = 0, // Nominal
      STRONG = 1, // Strong
      WEAK = 2, // Weak
      EXTRA_STRONG = 3  // Extra Strong
   } e_drive_str;

And similar definition in the C header view:

/// Soft reset
typedef enum ad7771_soft_reset_t {
    AD7771_NONE = 0, //!< No effect
    AD7771_SECOND = 2, //!< 2nd write
    AD7771_FIRST = 3 //!< 1st write
} ad7771_soft_reset_t;

Field larger than register width

Register width is mainly decided by the micro-controller used (typically 8, 16 or 32 bits), so it is quite common to have to handle fields larger than that.

This device has a few of those field, for example the Sample Rate Converter configuration: the IF is on 16b, the divider on 12b.

This is easily achieved with the partial property: it flags a field as being only partially defined and the associated value correspond to the LSB position in the whole field:

  - src_n_msb : (decimation) "Sample Rate converter N MSB"
    - src_n = 0 3:0 "Sample Rate converter N[11:8]"
      partial 8
  - src_n_lsb : (decimation) "Sample Rate converter N LSB"
    - src_n = 0 7:0 "Sample Rate converter N[7:0]"
      partial 0
  - src_if_msb : (decimation) "Sample Rate converter IF MSB"
    - src_if = 0 7:0 "Sample Rate converter IF[15:8]"
      partial 8
  - src_if_lsb : (decimation) "Sample Rate converter IF LSB"
    - src_if = 0 7:0 "Sample Rate converter IF[7:0]"
      partial 0
  - src_load : (decimation) "Sample Rate converter load"
    - load_source = 0 7:7 "Select which option to load an SRC update"
    - load_update = 0 0:0 "Assert to load SRC registers into SRC"
      pulse

Note: I also added here one of the field with a pulse property: this means that when writing one to this field, it will automatically reset to 0 after one clock cycle (using the hardware clock).

Field array split on multiple registers

The error status register contains a field array or “error_loc” for each channel split on two registers. This is handled with the arrayPartial property:

  - error_status1 : (error_status) "Error Status 1"
    - chip_error1    5:5 "High when any error bit is high"
    - err_loc_ch[5]  0:0 "Error specific to channel $i"
  - error_status2 : (error_status) "Error Status 2"
    - chip_error2    5:5 "High when any error bit is high"
    - err_loc_gen[2] 3:3 "Error specific to gen_err[$(i+1)]"
    - err_loc_ch[3]  0:0 "Error specific to channel $i"
      arrayPartial 5

The resulting systemVerilog structure is:

    typedef struct {
       logic chip_error1; // High when any error bit is high
       logic err_loc_ch[8]; // Error specific to channel
       logic chip_error2; // High when any error bit is high
       logic err_loc_gen[2]; // Error specific to gen_err[(i+1)]
    } t_error_status_hw;

Note: those register are read-only and this is simply done by not providing a reset value. If you want to specify the reset value (because the hardware might set the value at something different from 0) you can be explicit by specifying - chip_error1 = 1 5:5 ro "High when any error bit if high".

Conclusion

The full description is available in he same repository as this blog here and a few views (SystemVerilog (both RTL and UVM RAL), HTML, Python and SVD) can be seen in the directory yarig just below.

This real use-case is just a small example of what is possible to achieve with YARIG, and it was a good test for to ensure that it is generic enough to handle things I am not familiar with. I used it for dozens of hardware and combined with all my teammates this was used to close to one hundred blocks (many are used as part of my regression tests to ensure update do not introduce bugs).

Among the many features not shown here I would mention the fact that you can create what I call a rifmux, a multiplexer of register interface allowing to put together many register description to describe a full SoC. It is also possible to include a RIF file in order to re-use register definition, reserve an address range for internal memory, define register as being external, meaning the register logic is outside of the generated file in case you have complex behavior not supported.