Attention

This documentation is a work in progress. Expect to see errors and unfinished things.

build-tools newad

newad Documentation

newad.py is a python script used to simplify creation of software-settable registers within Verilog, typically in an FPGA context. The amount of boilerplate code required is marginally zero. In the background, addresses are generated and bus decoders are created.

The current version is based on “magic comments” (see examples below), which imposes some limitations on its use. We have hopes to rewrite the system someday to use Verilog attributes and a Real Verilog parser.

Ports on the input Verilog file can be marked (using an external comment) as a register to show up in the final address map. Some properties of those registers can be controlled with additional flags in the comment.

The output (therefore functionality) of newad.py can be changed depending on how it is called from the CLI. These calls are typically buried in a Makefile. Users can call newad with -h option to see all of its arguments:

usage: newad.py [-h] [-i INPUT_FILE] [-o OUTPUT] [-d DIR_LIST]
                [-a ADDR_MAP_HEADER] [-r REGMAP] [-l] [-m] [-pl] [-w LB_WIDTH]
                [-b BASE_ADDR] [-p CLK_PREFIX]

Automatic address generator: Parses Verilog lines and generates addresses and
decoders for registers declared external across module instantiations

optional arguments:
  -h, --help            show this help message and exit
  -i INPUT_FILE, --input_file INPUT_FILE
                        A top level file to start the parser
  -o OUTPUT, --output OUTPUT
                        Outputs generated header file
  -d DIR_LIST, --dir_list DIR_LIST
                        A list of directories to look for Verilog source files. <dir_0>[,<dir_1>]*
  -a ADDR_MAP_HEADER, --addr_map_header ADDR_MAP_HEADER
                        Outputs generated address map header file
  -r REGMAP, --regmap REGMAP
                        Outputs generated address map in json format
  -l, --low_res         When not selected generates a separate address name for each
  -m, --gen_mirror      Generates a mirror where all registers and register arrays with size < 32are available for readback
  -pl, --plot_map       Plots the register map using a broken bar graph
  -w LB_WIDTH, --lb_width LB_WIDTH
                        Set the address width of the local bus from which the generated registers are decoded
  -b BASE_ADDR, --base_addr BASE_ADDR
                        Set the base address of the register map to be generated from here
  -p CLK_PREFIX, --clk_prefix CLK_PREFIX
                        Prefix of the clock domain in which decoding is done [currently ignored], appends _clk

The workflow of newad

The main input to newad is essentially two arguments: the top Verilog file to start the parser, and the list of directories where modules can be found.

newad starts by parsing the top file, and then starts going deeper into the hierarchy. There are two main processes happening during this traverse:

1) Looking for input/output ports labeled external. These will turn into software-settable registers. Some of its properties are deduced from the native Verilog syntax: bit width, signed or not. Additional options can be set by more magic comments; see below.

The following Verilog snippet shows a 12-bit register defined as external.

input [11:0] phase_step, // external

2) Looking for Verilog module instantiations marked auto (short for automatic), for which newad needs to generate port assignments. When such an instantiation is found, newad recurses to look deeper into the hierarchy.

The following Verilog snippet shows how an instantiated Verilog module is marked auto by a developer:

pair_couple drive_couple // auto
        (.clk(clk), .iq(iq),
        .drive(prompt_drive), .lo_phase(lo_phase_d),
        .pair(fwd_ref),
        `AUTOMATIC_drive_couple
);

In this example, software-settable ports found within drive_couple will get filled in using the machine-generated macro AUTOMATIC_drive_couple. These ports will be automatically propagated outwards to the bus controller and decoder.

Register Attributes

Each port defined as external using the comment of Verilog will end up as a software-settable register with an automatically-assigned address. Behavior of this port can be modified by adding more attributes in the Verilog source file:

  • single-cycle: the port will only stay high (asserted) for a single cycle when written. Maps nicely to operations like “clear” and “trigger”, where no state is held in the register.

  • we-strobe: reserved for special cases where the register semantics requires access to the write-enable signal plus the data bus, like pushing into a FIFO. Implementing that behavior, given the write-enable strobe, is still the job of the HDL program.

Verilog Header Generation

When used with -a argument, newad creates a Verilog header containing the address map for given top level object. This file will have the name addr_map_<module_name>.vh. Inside the file, newad will place all address decoding for each register.

The following is an example of a decoded register address macro definition.

`define ADDR_HIT_digitizer_dsp_real_sim_mux_shell_0_dsp_ff_driver_mem (lb4_addr[0][`LB_HI:11]==4096) // digitizer_dsp bitwidth: 11, base_addr: 8388608

When used with -r argument, newad creates an address map in .json format.

Managing clock domains

By default, registers are set in the lb_clk domain in the top-level bus controller module. There are two provisions to override that.

1) In an instantiation, the default clock domain for ports for that instance can be set. Example:

prc_dsp prc_dsp // auto clk1x
        (.clk(adc_clk), .qmode(qmode[1:0]), .adc_data(adc_data),
        .iq_result1(iq_cav01), .iq_result2(iq_cav23), .qmode_out(qmode_out),
        .cosd(cosa), .sind(sina), .phase_zero(phase_zero), .fwd_in(fwd_in), .rev_in(rev_in), .phs_avg_sum(phs_avg_sum),
        `AUTOMATIC_prc_dsp
);

Registers in the prc_dsp module will be created in the clk1x_clk domain (happens to be equivalent to adc_clk).

2) Within a list of ports, the clock domain can be set in a sticky manner with comments of the form newad-force foo domain. Example:

// newad-force lb domain
input [0:0] trace_reset_we,  // external we-strobe
input [0:0] trace_ack,  // -- external single-cycle
// newad-force clk1x domain
input [0:0] trace_reset,  // -- external single-cycle
input [13:0] cic_period,  // external
input [7:0] trace_keep, // external
input [3:0] cic_shift, // external
// newad-force lb domain
input start_fdbk_dac_enable,  // external
input buf_trig,  // external
// newad-force clk2x domain
input [15:0] amplitude,  // external
input [19:0] ddsa_phstep_h,  // external
input [11:0] ddsa_phstep_l,  // external
input [11:0] ddsa_modulo,  // external

The result will include cic_period in the clk1x_clk domain, and buf_trig in the lb_clk domain. The hope is that option (1) will cover most use cases.

Note also in this example that the trace_ack and trace_reset ports will not be managed by newad. The extra -- intentionally disables the pattern-match for the external keyword.

You are encouraged to occasionally cross-check the register decoder produced by newad with its -o flag, typically named <module_name>_auto.vh, to make sure clock domains and other behavior are emitted as intended. Those files can be long, but should be legible to Verilog programmers. They even have helpful comments at the beginning showing how newad has traversed the Verilog hierarchy.

It is the responsibility of the bus controller to create local busses in each of the clock domains needed by its submodules.

Register names and uniqueness

Software-visible register names are created based on the instance hierarchy. For example, ssa_stim_ampstep is a name generated for register ampstep in module instance name ssa_stim. The newad-generated json file contains the mapping from name to (generated) address. That json file is normally compressed, held in FPGA memory, and made available to software. Application software can therefore always refer to registers by name.

A more exotic example is shell_1_dsp_fdbk_core_mp_proc_sel_thresh. Here the instance name hierarchy is shell, dsp, fdbk_core, mp_proc, the register name is sel_thresh, and the extra 1 comes from shell being created in a Verilog generate loop. The (abbreviated) syntax for that example is

genvar c_n;
generate for (c_n=0; c_n < 2; c_n=c_n+1) begin: cryomodule_cavity
    llrf_shell shell // auto(c_n,2) lb4[c_n]
        (.clk(adc_clk),
        ...
        `AUTOMATIC_shell
    );
end endgenerate

Additional Resources

https://gitlab.lbl.gov/hdl-libraries/bedrock/-/wikis/home/Newad-HOWTO