| #!/usr/bin/env python3 |
| """ |
| Generates a number of random MMCM configuration cases. |
| |
| The scripts uses Jinja2 template of a MMCM design to generate the given number |
| of random MMCM configurations. Generated designs are intended to be used for |
| verification of MMCM register content calculation perforemd in VPR techmap. |
| """ |
| import argparse |
| import random |
| from collections import namedtuple |
| |
| import jinja2 |
| |
| # ============================================================================= |
| |
| ClkOut = namedtuple("ClkOut", "index enabled divide duty phase") |
| |
| # ============================================================================= |
| |
| |
| def generate_case(): |
| """ |
| Generates a single set of parameters for MMCM |
| """ |
| |
| def gen_mult_and_phase(frac_en): |
| """ |
| For CLKFBOUT |
| """ |
| |
| # Fractional multiplier ranges from 2.0 to 64.0. The step is 1/8. |
| if frac_en: |
| |
| # Make sure to generate a fractional value |
| while True: |
| mult = random.uniform(2.0, 64.0) |
| mult = int(mult * 8.0) / 8.0 |
| |
| if mult != int(mult): |
| break |
| |
| # Integer divider ranges from 2 to 64 |
| else: |
| mult = random.randint(2, 64) |
| |
| phase = random.uniform(0.0, +180.0) |
| |
| # The phase shift requires to be quantized into 45 / mult steps. |
| quant = 45.0 / mult |
| phase = int((phase + quant / 2) / quant) * quant |
| |
| return mult, phase |
| |
| def gen_div_and_phase(frac_en): |
| """ |
| For CLKOUTn |
| """ |
| |
| # Fractional multiplier ranges from 2.0 to 128.0. The step is 1/8. |
| if frac_en: |
| |
| # Make sure to generate a fractional value |
| while True: |
| divide = random.uniform(2.0, 128.0) |
| divide = int(divide * 8.0) / 8.0 |
| |
| if divide != int(divide): |
| break |
| |
| # Integer divider ranges from 2 to 128 |
| else: |
| divide = random.randint(2, 128) |
| |
| phase = random.uniform(0.0, +180.0) |
| |
| # The phase shift requires to be quantized into 45 / mult steps. |
| quant = 45.0 / divide |
| phase = int((phase + quant / 2) / quant) * quant |
| |
| return divide, phase |
| |
| params = {} |
| |
| # Other settings |
| params["bandwidth"] = random.choice(( |
| "LOW", |
| "HIGH", |
| "OPTIMIZED", |
| )) |
| |
| # Input clock divider (1 to 106) |
| params["divclk_divide"] = random.randint(1, 106) |
| |
| # Feedback clock |
| frac_en = random.random() > 0.5 |
| mult, phase = gen_mult_and_phase(frac_en) |
| |
| params["clkfbout_mult"] = mult |
| params["clkfbout_phase"] = phase |
| |
| # Clock outputs |
| params["clkout"] = [] |
| for i in range(7): |
| |
| # Fractional divider is only available for CLKOUT0 |
| frac_en = random.random() > 0.5 |
| if i > 0: |
| frac_en = False |
| |
| divide, phase = gen_div_and_phase(frac_en) |
| |
| # Duty cycle must be 50% when fractional divider is enabled |
| duty = 0.5 # random.uniform(0.10, 0.90) |
| if frac_en: |
| duty = 0.5 |
| |
| # FIXME: After running a generated design through SymbiFlow, fasm2bels |
| # and Vivado a DRC PLCR-1 error is emitted. To work that around all |
| # clocks above CLKOUT1 will be disabled |
| is_enabled = (i <= 1) |
| |
| params["clkout"].append( |
| ClkOut( |
| index=i, |
| enabled=is_enabled, |
| divide=divide, |
| duty=duty, |
| phase=phase |
| ) |
| ) |
| |
| return params |
| |
| |
| def validate_params(params): |
| """ |
| Validates MMCM parameters agains legal VCO frequency range. Makes Vivado |
| don't complain. |
| """ |
| |
| # VCO operating ranges [MHz] (for xc7a35tcsg324-1 speed grade -1) |
| vco_range = (600.0, 1200.0) |
| |
| mul = params["clkfbout_mult"] |
| div = params["divclk_divide"] |
| |
| # It is impossible to meet VCO freq. constraints both for 100MHz and 50Mhz |
| # input. Hence we check only for 100. |
| for f_clkin in (100.0, ): |
| |
| f_vco = f_clkin * mul / div |
| |
| if f_vco < vco_range[0]: |
| return False |
| if f_vco > vco_range[1]: |
| return False |
| |
| return True |
| |
| |
| # ============================================================================= |
| |
| |
| def main(): |
| |
| parser = argparse.ArgumentParser( |
| description=__doc__, |
| formatter_class=argparse.RawDescriptionHelpFormatter |
| ) |
| |
| parser.add_argument( |
| "--template", |
| type=str, |
| default="mmcm_random_case.tpl", |
| help="Design template" |
| ) |
| parser.add_argument( |
| "--output", |
| type=str, |
| default="mmcm_random_case{:d}.v", |
| help="Output file name pattern to be used with str.format()" |
| ) |
| parser.add_argument( |
| "--count", type=int, default=10, help="Number of cases to generate" |
| ) |
| parser.add_argument( |
| "--seed", type=int, default=1, help="Random generator seed" |
| ) |
| |
| args = parser.parse_args() |
| |
| random.seed(args.seed) |
| |
| # Load the template |
| with open(args.template, "r") as fp: |
| template = jinja2.Template(fp.read()) |
| |
| # Generate cases |
| for i in range(args.count): |
| |
| # Generate random MMCM configuration |
| while True: |
| params = generate_case() |
| if validate_params(params): |
| break |
| |
| ks = sorted(list(params.keys())) |
| for k in ks: |
| print(k, params[k]) |
| |
| # Render and save the template |
| vlog = template.render(**params) |
| |
| fname = args.output.format(i) |
| with open(fname, "w") as fp: |
| fp.write(vlog) |
| |
| |
| # ============================================================================= |
| |
| if __name__ == "__main__": |
| main() |