PythonRegisterTree

Overview

The RegisterTree module is a Python library used to define FPGA register maps and generate artifacts from them, mainly:

  • VHDL register-tree modules (for SPI-accessible register interfaces)

  • Markdown register documentation

  • JSON export from in-memory register models

It is centered on a strict 16-bit register model and a JSON-driven workflow.

What it does

The module models three layers:

  1. Signal (bit field inside a register)

  2. Register (16-bit register containing non-overlapping signals)

  3. Tree (full register map grouped by logical categories)

From this model, it can generate:

  • a VHDL module with SPI command handling and register instances

  • signal-level top ports (*_i for RO, *_o for RW/CONST)

  • update pulses for RW registers (<register_name>_update_o)

Main files and roles

  • RegisterTree/signal.py: Signal definition and field metadata

  • RegisterTree/register.py: Register container and validation rules

  • RegisterTree/tree.py: full tree checks + VHDL generation

  • RegisterTree/jsonTools.py: JSON import/export helpers

  • RegisterTree/utils.py: helpers (name normalization, duplicates, ceil-div, command formatting)

  • RegisterTree/vhdl_module.py: generic VHDL source builder

  • register_tree_gen.py: CLI entrypoint to generate VHDL from JSON

JSON format

Top-level structure

The JSON root must be an object (dictionary). Each key is a register group name, and each value is a list of register objects.

Example:

{
  "generic_regs": [],
  "control_regs": [],
  "status_regs":  []
}

Group names are mainly for organization/documentation. Generation logic uses register content, not group semantics.

Register object: required keywords

Each register entry must define:

  • name (string): human-readable register name

  • address (string hex like "0x1A") unless auto-address mode is used

  • type (string): one of "rw", "ro", "const"

  • contents (list): list of signal objects

Example:

{
  "address": "0x02",
  "name": "pcs_status",
  "type": "ro",
  "contents": {}
}

Register type meaning

  • rw: Read/Write register

  • ro: Read-Only register

  • const: Constant register (compile-time fixed value)

Type-specific rules:

  • const: every signal must provide default

  • ro: signals must not provide default

  • rw: default is optional

Signal object: required keywords

Each signal in contents must define:

  • name (string): display name (documentation-facing)

  • description (string): meaning of the field

  • signal_name (string): technical/internal identifier (used for VHDL naming)

  • start_bit (integer): LSB index

  • end_bit (integer): MSB index

Optional signal keywords:

  • default (integer): reset/default value (required for const)

  • values (object): symbolic descriptions mapped to numeric codes

  • any additional custom metadata fields (preserved in dictionary export)

Signal example:

{
  "name": "TX Dual 0 enable",
  "description": "Enables Dual 0 of the PCS",
  "signal_name": "cfg_tx_d0_en",
  "start_bit": 0,
  "end_bit": 0,
  "default": 1
}

Validation constraints enforced

  • Register address uniqueness in a tree

  • Register name uniqueness (after normalization)

  • Signal-name uniqueness across tree (intended behavior)

  • Signal bit ranges must satisfy: - 0 <= start_bit <= 15 - 0 <= end_bit <= 15 - start_bit <= end_bit

  • Signals in same register must not overlap bit ranges

  • Names are normalized for VHDL safety (lowercase, spaces to underscores, special chars removed)

Complete minimal JSON example

{
  "generic_regs": [
    {
      "address": "0x00",
      "name": "fw_version",
      "type": "const",
      "contents": [
        {
          "name": "Bitstream version",
          "description": "Contains firmware ID",
          "signal_name": "cfg_fw_version",
          "start_bit": 0,
          "end_bit": 15,
          "default": 20225
        }
      ]
    }
  ],
  "pcs_regs": [
    {
      "address": "0x01",
      "name": "pcs_controls",
      "type": "rw",
      "contents": [
        {
          "name": "TX Dual 0 enable",
          "description": "Enable TX path",
          "signal_name": "cfg_tx_d0_en",
          "start_bit": 0,
          "end_bit": 0,
          "default": 1
        }
      ]
    },
    {
      "address": "0x02",
      "name": "pcs_status",
      "type": "ro",
      "contents": [
        {
          "name": "TX ready",
          "description": "High when transmitter is ready",
          "signal_name": "cfg_tx_ready",
          "start_bit": 0,
          "end_bit": 0
        }
      ]
    }
  ]
}

How to generate VHDL (CLI)

Use:

python3 Firmware/Scripts/register_tree_gen.py \
  -c Source/registers.json \
  -n registers \
  -o Source/modules/registers/hdl/registers.vhd \
  -b 8 -a 32 -r 64 -w 128

Important CLI options:

  • -c / --config_path: input JSON (required)

  • -n / --name: generated VHDL module name

  • -o / --output_path: output VHDL path

  • -u / --update_addresses: assign consecutive addresses in-memory while parsing

  • -v / --vsg_cfg_path: run VSG formatter after generation

Generating a Register Tree directly from Python

You can bypass JSON completely and build the register tree in code using Tree, Register, and Signal objects.

Basic workflow

  1. Create a Tree instance.

  2. Create Register objects with a RegisterType (RW, RO, CONST).

  3. Add Signal objects to each register (with optional defaults/values).

  4. Add registers to the tree with a group name.

  5. Generate VHDL (or Markdown / JSON export) from the tree.

Example

from RegisterTree.tree import Tree
from RegisterTree.register import Register, RegisterType
from RegisterTree.signal import Signal
from RegisterTree.jsonTools import dumpTreeToJSON

# 1) Create tree
tree = Tree("example_tree")

# 2) CONST register
reg_fw = Register("fw_version", 0x00, RegisterType.CONST)
sig_fw = Signal(
    signalName="cfg_fw_version",
    name="Bitstream version",
    description="Contains firmware ID",
    start_bit=0,
    end_bit=15
)
sig_fw.setDefault(20225)  # required for CONST fields
reg_fw.addSignal(sig_fw)
tree.addRegister(reg_fw, "generic_regs")

# 3) RW register
reg_ctrl = Register("pcs_controls", 0x01, RegisterType.RW)
sig_en = Signal(
    signalName="cfg_tx_d0_en",
    name="TX Dual 0 enable",
    description="Enable TX path",
    start_bit=0,
    end_bit=0
)
sig_en.setDefault(1)  # optional for RW
reg_ctrl.addSignal(sig_en)
tree.addRegister(reg_ctrl, "control_regs")

# 4) RO register
reg_status = Register("pcs_status", 0x02, RegisterType.RO)
sig_ready = Signal(
    signalName="cfg_tx_ready",
    name="TX ready",
    description="High when transmitter is ready",
    start_bit=0,
    end_bit=0
)
reg_status.addSignal(sig_ready)  # RO fields should not have default
tree.addRegister(reg_status, "status_regs")

# 5) Generate outputs
vhdl = tree.generateTreeModule(
    treeModuleName="registers",
    branching_factor=8,
    addr_cmd=32,
    read_cmd=64,
    write_cmd=128
)

markdown = tree.getMarkdown("Registers")
json_text = dumpTreeToJSON(tree)

# Save if needed
with open("registers.vhd", "w") as f:
    f.write(vhdl)

Important notes

  • The same validation rules as JSON mode apply (bit ranges, overlap, uniqueness, type rules).

  • Group names (second argument of tree.addRegister(...)) are used for organization/documentation.

  • You can attach custom metadata with setAddtionalInfo(...) on registers/signals; it is preserved in dictionary/JSON exports.

Other useful outputs

  • Markdown documentation:

    md = tree.getMarkdown("My Register Tree")
    
  • JSON re-serialization:

    from RegisterTree.jsonTools import dumpTreeToJSON
    json_text = dumpTreeToJSON(tree)
    

Practical guidance

  • Keep signal_name stable over time (it affects generated port names).

  • Use grouped top-level keys to keep large maps readable.

  • Prefer contiguous addresses unless sparse maps are intentional.

  • For const registers, verify every field has a valid default fitting its bit width.

  • For ro registers, never include default in JSON.