Device primitive instantiate with user defined code

Often it is desired to instantiate device primitives in a design.
This post is going to review, with an example, how device primitives
can be instantiated using user defined code.

In this example we will use the basic IO SERDES available in
some devices. This SERDES simply takes a parallel input and
generates a serial output. I call this simple to differentiate
between the more complex high speed transceiver available in
some high-end FPGAs.

My preference is to attempt and create vendor agnostic blocks
that will use the vendor specific templates, the following
conditionally instantiates the correct vendor block.

@myhdl.block
def simple_output_serdes(clock_par, clock_ser, reset, datain, serialout, 
                         vendor=None):
    """Simple output serializer

    Arguments:
        clock_par: The clock for the parallel data.
        clock_ser: The clock for the serial data, clock_ser should be N times
            greater than clock_par, where N is the number of bits being serialized
        reset: system active high reset
        datain: parallel data in
        serialout: serial data out 
    """
    assert vendor is None or vendor in ('A', 'L', 'X')
    vendor = 'my' if vendor is None else vendor

    # all the primitives have the same ports
    portmap = dict(
        clock_par=clock_par, clock_ser=clock_ser, reset=reset,
        datain=datain, serialout=serialout
    )
    # create a mapping to the different blocks
    blockmap = dict(
        my=my_output_serdes,
        A=alt_output_serdes,
        L=lat_output_serdes,       
        X=xil_output_serdes,
    )
    
    prim_inst = blockmap[vendor](**portmap)
    return prim_inst

In the above the my_output_serdes is a convertible / synthesizable
version the serializer, the others will use the user defined
code
to instantiate the device primitive.

x_prim_count = 0

@myhdl.block
def xil_output_serdes(clock_par, clock_ser, reset, datain, serialout):
    global x_prim_count
    serialout.driven = True
    datain.read = True
    inst_count = x_prim_count

    # do some checking, the primitive has limited modes
    assert len(datain) in range(1, 5)
    data_width = len(datain)

    # use the behavioral model for simulation this will be
    # overwritten during conversion
    prim_inst = my_output_serdes(clock_par, clock_ser, reset, 
                                 datain, serialout)
    x_prim_count += 1
    return prim_inst

xil_output_serdes.verilog_code = """
    
    wire [7:0] data = $datain;  
    wire clock_ser_n = ~$clock_ser;

    OSERDES2_$inst_count
        #(
          .BYPASS       ( "FALSE"),
          .DATA_RATE_OQ ( "SDR" ),
          .DATA_RATE_OT ( "SDR" ),
          .DATA_WIDTH   ( $data_width ),
          .OUTPUT_MODE  ( "SINGLE_ENDED" )
        )
    xil_oserdes_inst_$inst_count
       ( 
        .CLKDIV         ( $clock_par ),
        .CLK0           ( $clock_ser ),
        .CLK1           ( clock_ser_n ),
        .RST            ( $reset ),
        .IOCE           ( 1'b1 ),
        .D1             ( data[0] ),
        .D2             ( data[1] ),
        .D3             ( data[2] ),
        .D4             ( data[3] ),
        .OQ             ( $serialout )
      );
"""    

The above is just and example, in an actual implementation
there might be more parameters that are passed to the primitive
or the primitives are cascaded, etc.

2 Likes

@system @hgomersall replies are not spam.

Which part of the community flagged me??

I think it was automated.