[otp_ctrl/lc_ctrl] Refactor util scripts and share common code

Signed-off-by: Michael Schaffner <msf@opentitan.org>
diff --git a/hw/Makefile b/hw/Makefile
index 754adbb..34dd2a0 100644
--- a/hw/Makefile
+++ b/hw/Makefile
@@ -84,7 +84,7 @@
 	fi
 
 otp-mmap:
-	cd ${PRJ_DIR}/hw/ip/otp_ctrl/util && ./translate-mmap.py
+	cd ${PRJ_DIR}/hw/ip/otp_ctrl/util && ./gen-otp-mmap.py
 
 regs-header: $(ips_reg_header) banner
 
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv
index bd2f60f..51af433 100644
--- a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv
@@ -6,7 +6,7 @@
 //
 // DO NOT EDIT THIS FILE DIRECTLY.
 // It has been generated with
-// $ cd hw/ip/lc_ctrl/util/ && .gen-lc-state-enc.py --seed 10167336684108184581
+// $ cd hw/ip/otp_ctrl/util/ && .gen-lc-state-enc.py --seed 10167336684108184581
 //
 package lc_ctrl_state_pkg;
 
@@ -19,8 +19,8 @@
   //
   // - Minimum Hamming weight: 8
   // - Maximum Hamming weight: 16
-  // - Minimum Hamming distance: 6
-  // - Maximum Hamming distance: 18
+  // - Minimum Hamming distance from any other value: 6
+  // - Maximum Hamming distance from any other value: 18
   //
   // Hamming distance histogram:
   //
diff --git a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl
index 09dac78..4adbaeb 100644
--- a/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl
+++ b/hw/ip/lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl
@@ -6,12 +6,12 @@
 //
 // DO NOT EDIT THIS FILE DIRECTLY.
 // It has been generated with
-// $ cd hw/ip/lc_ctrl/util/ && .gen-lc-state-enc.py --seed ${config['seed']}
+// $ cd hw/ip/otp_ctrl/util/ && .gen-lc-state-enc.py --seed ${lc_st_enc.config['seed']}
 //
 package lc_ctrl_state_pkg;
 <%
-data_width = config['secded']['data_width']
-ecc_width  = config['secded']['ecc_width']
+data_width = lc_st_enc.config['secded']['data_width']
+ecc_width  = lc_st_enc.config['secded']['ecc_width']
 %>
   // These values have been generated such that they are incrementally writeable with respect
   // to the ECC polynomial specified. The values are used to define the life cycle manufacturing
@@ -20,14 +20,14 @@
   // The values are unique and have the following statistics (considering all ${data_width}
   // data and ${ecc_width} ECC bits):
   //
-  // - Minimum Hamming weight: ${config['stats']['min_hw']}
-  // - Maximum Hamming weight: ${config['stats']['max_hw']}
-  // - Minimum Hamming distance: ${config['stats']['min_hd']}
-  // - Maximum Hamming distance: ${config['stats']['max_hd']}
+  // - Minimum Hamming weight: ${lc_st_enc.gen['stats']['min_hw']}
+  // - Maximum Hamming weight: ${lc_st_enc.gen['stats']['max_hw']}
+  // - Minimum Hamming distance from any other value: ${lc_st_enc.gen['stats']['min_hd']}
+  // - Maximum Hamming distance from any other value: ${lc_st_enc.gen['stats']['max_hd']}
   //
   // Hamming distance histogram:
   //
-% for bar in config['stats']['bars']:
+% for bar in lc_st_enc.gen['stats']['bars']:
   // ${bar}
 % endfor
   //
@@ -36,21 +36,21 @@
   // the OTP ECC logic at runtime.
 
   // The A/B values are used for the encoded LC state.
-% for word in config['ab_words']:
+% for word in lc_st_enc.gen['ab_words']:
   parameter logic [${data_width-1}:0] A${loop.index} = ${data_width}'b${word[0][ecc_width:]}; // ECC: ${ecc_width}'b${word[0][0:ecc_width]}
   parameter logic [${data_width-1}:0] B${loop.index} = ${data_width}'b${word[1][ecc_width:]}; // ECC: ${ecc_width}'b${word[1][0:ecc_width]}
 
 % endfor
 
   // The C/D values are used for the encoded LC transition counter.
-% for word in config['cd_words']:
+% for word in lc_st_enc.gen['cd_words']:
   parameter logic [${data_width-1}:0] C${loop.index} = ${data_width}'b${word[0][ecc_width:]}; // ECC: ${ecc_width}'b${word[0][0:ecc_width]}
   parameter logic [${data_width-1}:0] D${loop.index} = ${data_width}'b${word[1][ecc_width:]}; // ECC: ${ecc_width}'b${word[1][0:ecc_width]}
 
 % endfor
 
   // The E/F values are used for the encoded ID state.
-% for word in config['ef_words']:
+% for word in lc_st_enc.gen['ef_words']:
   parameter logic [${data_width-1}:0] E${loop.index} = ${data_width}'b${word[0][ecc_width:]}; // ECC: ${ecc_width}'b${word[0][0:ecc_width]}
   parameter logic [${data_width-1}:0] F${loop.index} = ${data_width}'b${word[1][ecc_width:]}; // ECC: ${ecc_width}'b${word[1][0:ecc_width]}
 
diff --git a/hw/ip/lc_ctrl/util/gen-lc-state-enc.py b/hw/ip/lc_ctrl/util/gen-lc-state-enc.py
deleted file mode 100755
index fe78f31..0000000
--- a/hw/ip/lc_ctrl/util/gen-lc-state-enc.py
+++ /dev/null
@@ -1,400 +0,0 @@
-#!/usr/bin/env python3
-# Copyright lowRISC contributors.
-# Licensed under the Apache License, Version 2.0, see LICENSE for details.
-# SPDX-License-Identifier: Apache-2.0
-r"""Given an ECC encoding matrix, this script generates random life cycle
-state encodings that can be incrementally written to a memory protected with
-the ECC code specified.
-"""
-import argparse
-import logging as log
-import random
-import textwrap
-from pathlib import Path
-
-import hjson
-from mako.template import Template
-
-# State encoding definition
-LC_STATE_DEFINITION_FILE = "../data/lc_ctrl_state.hjson"
-# Code templates to render
-TEMPLATES = ["../rtl/lc_ctrl_state_pkg.sv.tpl"]
-
-
-def wrapped_docstring():
-    '''Return a text-wrapped version of the module docstring'''
-    paras = []
-    para = []
-    for line in __doc__.strip().split('\n'):
-        line = line.strip()
-        if not line:
-            if para:
-                paras.append('\n'.join(para))
-                para = []
-        else:
-            para.append(line)
-    if para:
-        paras.append('\n'.join(para))
-
-    return '\n\n'.join(textwrap.fill(p) for p in paras)
-
-
-def _check_int(x):
-    '''Check_int checks if input 'x' is decimal integer.'''
-    if isinstance(x, int):
-        return x
-    if not x.isdecimal():
-        log.error("{} is not a decimal number".format(x))
-        exit(1)
-    return int(x)
-
-
-def validate(config):
-    '''Validate configuration dict.'''
-
-    if 'secded' not in config:
-        log.error('Missing secded configuration')
-        exit(1)
-
-    config['secded'].setdefault('data_width', 0)
-    config['secded'].setdefault('ecc_width', 0)
-    config['secded'].setdefault('ecc_matrix', [[]])
-    config.setdefault('num_ab_words', 0)
-    config.setdefault('num_cd_words', 0)
-    config.setdefault('num_ef_words', 0)
-    config.setdefault('min_hw', 0)
-    config.setdefault('max_hw', 0)
-    config.setdefault('min_hd', 0)
-
-    config['secded']['data_width'] = _check_int(config['secded']['data_width'])
-    config['secded']['ecc_width'] = _check_int(config['secded']['ecc_width'])
-    config['num_ab_words'] = _check_int(config['num_ab_words'])
-    config['num_cd_words'] = _check_int(config['num_cd_words'])
-    config['num_ef_words'] = _check_int(config['num_ef_words'])
-    config['min_hw'] = _check_int(config['min_hw'])
-    config['max_hw'] = _check_int(config['max_hw'])
-    config['min_hd'] = _check_int(config['min_hd'])
-
-    total_width = config['secded']['data_width'] + config['secded']['ecc_width']
-
-    if config['min_hw'] >= total_width or \
-       config['max_hw'] > total_width or \
-       config['min_hw'] >= config['max_hw']:
-        log.error('Hamming weight constraints are inconsistent.')
-        exit(1)
-
-    if config['max_hw'] - config['min_hw'] + 1 < config['min_hd']:
-        log.error('Hamming distance constraint is inconsistent.')
-        exit(1)
-
-    if config['secded']['ecc_width'] != len(config['secded']['ecc_matrix']):
-        log.error('ECC matrix does not have correct number of rows')
-        exit(1)
-
-    for i, l in enumerate(config['secded']['ecc_matrix']):
-        for j, e in enumerate(l):
-            e = _check_int(e)
-            config['secded']['ecc_matrix'][i][j] = e
-
-
-def _is_valid_codeword(config, codeword):
-    '''Checks whether the bitstring is a valid ECC codeword.'''
-
-    data_width = config['secded']['data_width']
-    ecc_width = config['secded']['ecc_width']
-    if len(codeword) != (data_width + ecc_width):
-        log.error("Invalid codeword length {}".format(len(codeword)))
-        exit(1)
-
-    # Build syndrome and check whether it is zero.
-    syndrome = [0 for k in range(ecc_width)]
-
-    # The bitstring must be formatted as "data bits[N-1:0]" + "ecc bits[M-1:0]".
-    for j, fanin in enumerate(config['secded']['ecc_matrix']):
-        syndrome[j] = int(codeword[ecc_width - 1 - j])
-        for k in fanin:
-            syndrome[j] ^= int(codeword[ecc_width + data_width - 1 - k])
-
-    return sum(syndrome) == 0
-
-
-def _ecc_encode(config, dataword):
-    '''Calculate and prepend ECC bits.'''
-    if len(dataword) != config['secded']['data_width']:
-        log.error("Invalid codeword length {}".format(len(dataword)))
-        exit(1)
-
-    # Build syndrome
-    eccbits = ""
-    for fanin in config['secded']['ecc_matrix']:
-        bit = 0
-        for k in fanin:
-            bit ^= int(dataword[config['secded']['data_width'] - 1 - k])
-        eccbits += format(bit, '01b')
-
-    return eccbits[::-1] + dataword
-
-
-def _is_incremental_codeword(word1, word2):
-    '''Test whether word2 is incremental wrt word1.'''
-    if len(word1) != len(word2):
-        log.error('Words are not of equal size')
-        exit(1)
-
-    _word1 = int(word1, 2)
-    _word2 = int(word2, 2)
-
-    # This basically checks that the second word does not
-    # clear any bits that are set to 1 in the first word.
-    return ((_word1 & _word2) == _word1)
-
-
-def _scatter_bits(mask, bits):
-    '''Scatter the bits into unset positions of mask.'''
-    j = 0
-    scatterword = ''
-    for b in mask:
-        if b == '1':
-            scatterword += '1'
-        else:
-            scatterword += bits[j]
-            j += 1
-
-    return scatterword
-
-
-def _get_hd(word1, word2):
-    '''Calculate Hamming distance between two words.'''
-    if len(word1) != len(word2):
-        log.error('Words are not of equal size')
-        exit(1)
-    return bin(int(word1, 2) ^ int(word2, 2)).count('1')
-
-
-def _get_incremental_codewords(config, base_ecc, existing_words):
-    '''Get all possible incremental codewords fulfilling the constraints.'''
-
-    base_data = base_ecc[config['secded']['ecc_width']:]
-
-    # We only need to spin through data bits that have not been set yet.
-    # Hence, we first count how many bits are zero (and hence still
-    # modifyable). Then, we enumerate all possible combinations and scatter
-    # the bits of the enumerated values into the correct bit positions using
-    # the _scatter_bits() function.
-    incr_cands = []
-    free_bits = base_data.count('0')
-    for k in range(1, 2**free_bits):
-        # Get incremental dataword by scattering the enumeration bits
-        # into the zero bit positions in base_data.
-        incr_cand = _scatter_bits(base_data,
-                                  format(k, '0' + str(free_bits) + 'b'))
-        incr_cand_ecc = _ecc_encode(config, incr_cand)
-
-        # Dataword is correct by construction, but we need to check whether
-        # the ECC bits are incremental.
-        if _is_incremental_codeword(base_ecc, incr_cand_ecc):
-            # Check whether the candidate fulfills the maximum
-            # Hamming weight constraint.
-            if incr_cand_ecc.count('1') <= config['max_hw']:
-                # Check Hamming distance wrt all existing words.
-                for w in existing_words + [base_ecc]:
-                    if _get_hd(incr_cand_ecc, w) < config['min_hd']:
-                        break
-                else:
-                    incr_cands.append(incr_cand_ecc)
-
-    return incr_cands
-
-
-def _get_new_state_word_pair(config, existing_words):
-    '''Randomly generate a new incrementally writable word pair'''
-    while 1:
-        # Draw a random number and check whether it is unique and whether
-        # the Hamming weight is in range.
-        width = config['secded']['data_width']
-        ecc_width = config['secded']['ecc_width']
-        base = random.getrandbits(width)
-        base = format(base, '0' + str(width) + 'b')
-        base_cand_ecc = _ecc_encode(config, base)
-        # disallow all-zero and all-one states
-        pop_cnt = base_cand_ecc.count('1')
-        if pop_cnt >= config['min_hw'] and pop_cnt <= config['max_hw']:
-
-            # Check Hamming distance wrt all existing words
-            for w in existing_words:
-                if _get_hd(base_cand_ecc, w) < config['min_hd']:
-                    break
-            else:
-                # Get encoded incremental candidates.
-                incr_cands_ecc = _get_incremental_codewords(
-                    config, base_cand_ecc, existing_words)
-                # there are valid candidates, draw one at random.
-                # otherwise we just start over.
-                if incr_cands_ecc:
-                    incr_cand_ecc = random.choice(incr_cands_ecc)
-                    log.info('word {}: {}|{} -> {}|{}'.format(
-                        int(len(existing_words) / 2),
-                        base_cand_ecc[ecc_width:],
-                        base_cand_ecc[0:ecc_width],
-                        incr_cand_ecc[ecc_width:],
-                        incr_cand_ecc[0:ecc_width]))
-                    existing_words.append(base_cand_ecc)
-                    existing_words.append(incr_cand_ecc)
-                    return (base_cand_ecc, incr_cand_ecc)
-
-
-def _validate_words(config, words):
-    '''Validate generated words (base and incremental).'''
-    for k, w in enumerate(words):
-        # Check whether word is valid wrt to ECC polynomial.
-        if not _is_valid_codeword(config, w):
-            log.error('Codeword {} at index {} is not valid'.format(w, k))
-            exit(1)
-        # Check that word fulfills the Hamming weight constraints.
-        pop_cnt = w.count('1')
-        if pop_cnt < config['min_hw'] or pop_cnt > config['max_hw']:
-            log.error(
-                'Codeword {} at index {} has wrong Hamming weight'.format(
-                    w, k))
-            exit(1)
-        # Check Hamming distance wrt to all other existing words.
-        # If the constraint is larger than 0 this implies uniqueness.
-        if k < len(words) - 1:
-            for k2, w2 in enumerate(words[k + 1:]):
-                if _get_hd(w, w2) < config['min_hd']:
-                    log.error(
-                        'Hamming distance between codeword {} at index {} '
-                        'and codeword {} at index {} is too low.'.format(
-                            w, k, w2, k + 1 + k2))
-                    exit(1)
-
-
-def _hist_to_bars(hist, m):
-    '''Convert histogramm list into ASCII bar plot'''
-    bars = []
-    for i, j in enumerate(hist):
-        bar_prefix = "{:2}: ".format(i)
-        spaces = len(str(m)) - len(bar_prefix)
-        hist_bar = bar_prefix + (" " * spaces)
-        for k in range(j * 20 // max(hist)):
-            hist_bar += "|"
-        hist_bar += " ({:.2f}%)".format(100.0 * j / sum(hist)) if j else "--"
-        bars += [hist_bar]
-    return bars
-
-
-def hd_histogram(existing_words):
-    '''Build Hamming distance histogram'''
-    minimum_hd = len(existing_words[0])
-    maximum_hd = 0
-    minimum_hw = len(existing_words[0])
-    maximum_hw = 0
-    hist = [0] * (len(existing_words[0]) + 1)
-    for i, j in enumerate(existing_words):
-        minimum_hw = min(j.count('1'), minimum_hw)
-        maximum_hw = max(j.count('1'), maximum_hw)
-        if i < len(existing_words) - 1:
-            for k in existing_words[i + 1:]:
-                dist = _get_hd(j, k)
-                hist[dist] += 1
-                minimum_hd = min(dist, minimum_hd)
-                maximum_hd = max(dist, maximum_hd)
-
-    stats = {}
-    stats["hist"] = hist
-    stats["bars"] = _hist_to_bars(hist, len(existing_words))
-    stats["min_hd"] = minimum_hd
-    stats["max_hd"] = maximum_hd
-    stats["min_hw"] = minimum_hw
-    stats["max_hw"] = maximum_hw
-    return stats
-
-
-def generate_encoding(config):
-    '''Generates AB, CD and EF encodings and augments config structure.'''
-
-    word_types = ['ab_words', 'cd_words', 'ef_words']
-
-    # Inititalize with empty lists
-    for w in word_types:
-        config.setdefault(w, [])
-
-    # Generate new encoding words
-    existing_words = []
-    for w in word_types:
-        while len(config[w]) < config['num_' + w]:
-            new_word = _get_new_state_word_pair(config, existing_words)
-            config[w].append(new_word)
-
-    # Validate words (this must not fail at this point).
-    _validate_words(config, existing_words)
-
-    # Print out HD histogram
-    config['stats'] = hd_histogram(existing_words)
-
-    log.info('')
-    log.info('Hamming distance histogram:')
-    log.info('')
-    for bar in config['stats']["bars"]:
-        log.info(bar)
-    log.info('')
-    log.info('Minimum HD: {}'.format(config['stats']['min_hd']))
-    log.info('Maximum HD: {}'.format(config['stats']['max_hd']))
-    log.info('Minimum HW: {}'.format(config['stats']['min_hw']))
-    log.info('Maximum HW: {}'.format(config['stats']['max_hw']))
-
-
-def main():
-    log.basicConfig(level=log.INFO,
-                    format="%(asctime)s - %(message)s",
-                    datefmt="%Y-%m-%d %H:%M")
-
-    parser = argparse.ArgumentParser(
-        prog="gen-lc-state-enc",
-        description=wrapped_docstring(),
-        formatter_class=argparse.RawDescriptionHelpFormatter)
-
-    parser.add_argument('-s',
-                        '--seed',
-                        type=int,
-                        metavar='<seed>',
-                        help='Custom seed for RNG.')
-
-    args = parser.parse_args()
-
-    with open(LC_STATE_DEFINITION_FILE, 'r') as infile:
-        config = hjson.load(infile)
-
-        # If specified, override the seed for random netlist constant computation.
-        if args.seed:
-            log.warning('Commandline override of seed with {}.'.format(
-                args.seed))
-            config['seed'] = args.seed
-        # Otherwise, we either take it from the .hjson if present, or
-        # randomly generate a new seed if not.
-        else:
-            random.seed()
-            new_seed = random.getrandbits(64)
-            if config.setdefault('seed', new_seed) == new_seed:
-                log.warning(
-                    'No seed specified, setting to {}.'.format(new_seed))
-
-        # Initialize RNG.
-        random.seed(int(config['seed']))
-
-        # validate config and generate encoding
-        validate(config)
-        generate_encoding(config)
-
-        # render all templates
-        for template in TEMPLATES:
-            with open(template, 'r') as tplfile:
-                tpl = Template(tplfile.read())
-                with open(
-                        Path(template).parent.joinpath(Path(template).stem),
-                        'w') as outfile:
-                    outfile.write(tpl.render(config=config))
-
-
-if __name__ == "__main__":
-    main()
diff --git a/hw/ip/otp_ctrl/data/otp_ctrl.hjson b/hw/ip/otp_ctrl/data/otp_ctrl.hjson
index 94f026c..0083608 100644
--- a/hw/ip/otp_ctrl/data/otp_ctrl.hjson
+++ b/hw/ip/otp_ctrl/data/otp_ctrl.hjson
@@ -5,7 +5,7 @@
 // HJSON with partition metadata.
 //
 // DO NOT EDIT THIS FILE DIRECTLY.
-// It has been generated with hw/ip/otp_ctrl/util/translate-mmap.py
+// It has been generated with hw/ip/otp_ctrl/util/gen-otp-mmap.py
 
 
 { name: "otp_ctrl",
diff --git a/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl b/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl
index 5543748..cbc1aec 100644
--- a/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl
+++ b/hw/ip/otp_ctrl/data/otp_ctrl.hjson.tpl
@@ -5,10 +5,10 @@
 // HJSON with partition metadata.
 //
 // DO NOT EDIT THIS FILE DIRECTLY.
-// It has been generated with hw/ip/otp_ctrl/util/translate-mmap.py
+// It has been generated with hw/ip/otp_ctrl/util/gen-otp-mmap.py
 
 <%
-  num_part = len(config["partitions"])
+  num_part = len(otp_mmap.config["partitions"])
 
   def PascalCase(inp):
     oup = ''
@@ -106,7 +106,7 @@
     { name: "OtpByteAddrWidth",
       desc: "Width of the OTP byte address.",
       type: "int",
-      default: "${config["otp"]["byte_addr_width"]}",
+      default: "${otp_mmap.config["otp"]["byte_addr_width"]}",
       local: "true"
     },
     { name: "NumErrorEntries",
@@ -147,7 +147,7 @@
       default: "${num_part}",
       local: "true"
     },
-% for part in config["partitions"]:
+% for part in otp_mmap.config["partitions"]:
     { name: "${PascalCase(part["name"])}Offset",
       desc: "Offset of the ${part["name"]} partition",
       type: "int",
@@ -333,7 +333,7 @@
       tags: [ // OTP internal HW can modify status register
               "excl:CsrAllTests:CsrExclCheck"],
       fields: [
-% for k, part in enumerate(config["partitions"]):
+% for k, part in enumerate(otp_mmap.config["partitions"]):
         { bits: "${k}"
           name: "${part["name"]}_ERROR"
           desc: '''
@@ -734,7 +734,7 @@
     ////////////////////////////////////
     // Dynamic Locks of SW Parititons //
     ////////////////////////////////////
-% for part in config["partitions"]:
+% for part in otp_mmap.config["partitions"]:
   % if part["read_lock"].lower() == "csr":
     { name: "${part["name"]}_READ_LOCK",
       desc: '''
@@ -762,7 +762,7 @@
     ///////////////////////
     // Integrity Digests //
     ///////////////////////
-% for part in config["partitions"]:
+% for part in otp_mmap.config["partitions"]:
   % if part["sw_digest"]:
     { multireg: {
         name:     "${part["name"]}_DIGEST",
diff --git a/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson b/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson
index be0c200..90a1453 100644
--- a/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson
+++ b/hw/ip/otp_ctrl/data/otp_ctrl_mmap.hjson
@@ -2,11 +2,11 @@
 // Licensed under the Apache License, Version 2.0, see LICENSE for details.
 // SPDX-License-Identifier: Apache-2.0
 //
-// Use the translate-mmap.py script to update dependent files (like documentation
+// Use the gen-otp-mmap.py script to update dependent files (like documentation
 // tables the comportable hjson and metadata SV package):
 //
 // $ cd ${PROJ_ROOT}/hw/ip/otp_ctrl/util/
-// $ ./translate-mmap.py
+// $ ./gen-otp-mmap.py
 //
 // Make sure to regenerate the CSRs after converting the memory map:
 //
diff --git a/hw/ip/otp_ctrl/doc/otp_ctrl_digests.md b/hw/ip/otp_ctrl/doc/otp_ctrl_digests.md
index a6822ae..ffc83bf 100644
--- a/hw/ip/otp_ctrl/doc/otp_ctrl_digests.md
+++ b/hw/ip/otp_ctrl/doc/otp_ctrl_digests.md
@@ -1,6 +1,6 @@
 <!--
 DO NOT EDIT THIS FILE DIRECTLY.
-It has been generated with hw/ip/otp_ctrl/util/translate-mmap.py
+It has been generated with hw/ip/otp_ctrl/util/gen-otp-mmap.py
 -->
 
 |                      Digest Name                      |   Affected Partition  |  Calculated by HW  |
diff --git a/hw/ip/otp_ctrl/doc/otp_ctrl_mmap.md b/hw/ip/otp_ctrl/doc/otp_ctrl_mmap.md
index dbe79bb..2ccdcbc 100644
--- a/hw/ip/otp_ctrl/doc/otp_ctrl_mmap.md
+++ b/hw/ip/otp_ctrl/doc/otp_ctrl_mmap.md
@@ -1,6 +1,6 @@
 <!--
 DO NOT EDIT THIS FILE DIRECTLY.
-It has been generated with hw/ip/otp_ctrl/util/translate-mmap.py
+It has been generated with hw/ip/otp_ctrl/util/gen-otp-mmap.py
 -->
 
 |  Index  |   Partition    |  Size [B]  |  Access Granule  |                         Item                          |  Byte Address  |  Size [B]  |
diff --git a/hw/ip/otp_ctrl/doc/otp_ctrl_partitions.md b/hw/ip/otp_ctrl/doc/otp_ctrl_partitions.md
index abaf122..d06005c 100644
--- a/hw/ip/otp_ctrl/doc/otp_ctrl_partitions.md
+++ b/hw/ip/otp_ctrl/doc/otp_ctrl_partitions.md
@@ -1,6 +1,6 @@
 <!--
 DO NOT EDIT THIS FILE DIRECTLY.
-It has been generated with hw/ip/otp_ctrl/util/translate-mmap.py
+It has been generated with hw/ip/otp_ctrl/util/gen-otp-mmap.py
 -->
 
 |   Partition    |  Secret  |  Buffered  |  WR Lockable  |  RD Lockable  |                                                                                                                                                                                                                                                                    Description                                                                                                                                                                                                                                                                     |
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv
index 96cc29c..d8e4481 100644
--- a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv
@@ -6,7 +6,7 @@
 //
 // DO NOT EDIT THIS FILE DIRECTLY.
 // It has been generated with
-// $ cd hw/ip/otp_ctrl/util/ && ./translate-mmap.py --seed 10556718629619452145
+// $ cd hw/ip/otp_ctrl/util/ && ./gen-otp-mmap.py --seed 10556718629619452145
 //
 
 package otp_ctrl_part_pkg;
diff --git a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl
index 40f6018..03346f4 100644
--- a/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl
+++ b/hw/ip/otp_ctrl/rtl/otp_ctrl_part_pkg.sv.tpl
@@ -6,7 +6,7 @@
 //
 // DO NOT EDIT THIS FILE DIRECTLY.
 // It has been generated with
-// $ cd hw/ip/otp_ctrl/util/ && ./translate-mmap.py --seed ${config['seed']}
+// $ cd hw/ip/otp_ctrl/util/ && ./gen-otp-mmap.py --seed ${otp_mmap.config['seed']}
 //
 <%
   def PascalCase(inp):
@@ -26,11 +26,11 @@
   import otp_ctrl_pkg::*;
 
   localparam part_info_t PartInfo [NumPart] = '{
-% for part in config["partitions"]:
+% for part in otp_mmap.config["partitions"]:
     // ${part["name"]}
     '{
       variant:    ${part["variant"]},
-      offset:     ${config["otp"]["byte_addr_width"]}'d${part["offset"]},
+      offset:     ${otp_mmap.config["otp"]["byte_addr_width"]}'d${part["offset"]},
       size:       ${part["size"]},
       key_sel:    ${part["key_sel"] if part["key_sel"] != "NoKey" else "key_sel_e'('0)"},
       secret:     1'b${"1" if part["secret"] else "0"},
@@ -42,7 +42,7 @@
   };
 
   typedef enum {
-% for part in config["partitions"]:
+% for part in otp_mmap.config["partitions"]:
     ${PascalCase(part["name"])}Idx,
 % endfor
     // These are not "real partitions", but in terms of implementation it is convenient to
@@ -57,7 +57,7 @@
   parameter int NumAgents = int'(NumAgentsIdx);
 
   // Breakout types for easier access of individual items.
-% for part in config["partitions"]:
+% for part in otp_mmap.config["partitions"]:
   % if part["bkout_type"]:
   typedef struct packed {
     % for item in part["items"][::-1]:
@@ -73,11 +73,11 @@
 % endfor
 
   // OTP invalid partition default for buffered partitions.
-  parameter logic [${int(config["otp"]["depth"])*int(config["otp"]["width"])*8-1}:0] PartInvDefault = ${int(config["otp"]["depth"])*int(config["otp"]["width"])*8}'({
-  % for k, part in enumerate(config["partitions"][::-1]):
+  parameter logic [${int(otp_mmap.config["otp"]["depth"])*int(otp_mmap.config["otp"]["width"])*8-1}:0] PartInvDefault = ${int(otp_mmap.config["otp"]["depth"])*int(otp_mmap.config["otp"]["width"])*8}'({
+  % for k, part in enumerate(otp_mmap.config["partitions"][::-1]):
     ${int(part["size"])*8}'({
     % for item in part["items"][::-1]:
-      ${item["inv_default"]}${("\n    })," if k < len(config["partitions"])-1 else "\n    })});") if loop.last else ","}
+      ${item["inv_default"]}${("\n    })," if k < len(otp_mmap.config["partitions"])-1 else "\n    })});") if loop.last else ","}
     % endfor
   % endfor
 
diff --git a/hw/ip/otp_ctrl/util/LcStEnc.py b/hw/ip/otp_ctrl/util/LcStEnc.py
new file mode 100644
index 0000000..16256d1
--- /dev/null
+++ b/hw/ip/otp_ctrl/util/LcStEnc.py
@@ -0,0 +1,342 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Contains life cycle state encoding class which is
+used to generate new life cycle encodings.
+"""
+import logging as log
+import random
+
+from common import check_int
+
+
+def _is_valid_codeword(config, codeword):
+    '''Checks whether the bitstring is a valid ECC codeword.'''
+
+    data_width = config['secded']['data_width']
+    ecc_width = config['secded']['ecc_width']
+    if len(codeword) != (data_width + ecc_width):
+        log.error("Invalid codeword length {}".format(len(codeword)))
+        exit(1)
+
+    # Build syndrome and check whether it is zero.
+    syndrome = [0 for k in range(ecc_width)]
+
+    # The bitstring must be formatted as "data bits[N-1:0]" + "ecc bits[M-1:0]".
+    for j, fanin in enumerate(config['secded']['ecc_matrix']):
+        syndrome[j] = int(codeword[ecc_width - 1 - j])
+        for k in fanin:
+            syndrome[j] ^= int(codeword[ecc_width + data_width - 1 - k])
+
+    return sum(syndrome) == 0
+
+
+def _ecc_encode(config, dataword):
+    '''Calculate and prepend ECC bits.'''
+    if len(dataword) != config['secded']['data_width']:
+        log.error("Invalid codeword length {}".format(len(dataword)))
+        exit(1)
+
+    # Build syndrome
+    eccbits = ""
+    for fanin in config['secded']['ecc_matrix']:
+        bit = 0
+        for k in fanin:
+            bit ^= int(dataword[config['secded']['data_width'] - 1 - k])
+        eccbits += format(bit, '01b')
+
+    return eccbits[::-1] + dataword
+
+
+def _is_incremental_codeword(word1, word2):
+    '''Test whether word2 is incremental wrt word1.'''
+    if len(word1) != len(word2):
+        log.error('Words are not of equal size')
+        exit(1)
+
+    _word1 = int(word1, 2)
+    _word2 = int(word2, 2)
+
+    # This basically checks that the second word does not
+    # clear any bits that are set to 1 in the first word.
+    return ((_word1 & _word2) == _word1)
+
+
+def _scatter_bits(mask, bits):
+    '''Scatter the bits into unset positions of mask.'''
+    j = 0
+    scatterword = ''
+    for b in mask:
+        if b == '1':
+            scatterword += '1'
+        else:
+            scatterword += bits[j]
+            j += 1
+
+    return scatterword
+
+
+def _get_hd(word1, word2):
+    '''Calculate Hamming distance between two words.'''
+    if len(word1) != len(word2):
+        log.error('Words are not of equal size')
+        exit(1)
+    return bin(int(word1, 2) ^ int(word2, 2)).count('1')
+
+
+def _get_incremental_codewords(config, base_ecc, existing_words):
+    '''Get all possible incremental codewords fulfilling the constraints.'''
+
+    base_data = base_ecc[config['secded']['ecc_width']:]
+
+    # We only need to spin through data bits that have not been set yet.
+    # Hence, we first count how many bits are zero (and hence still
+    # modifyable). Then, we enumerate all possible combinations and scatter
+    # the bits of the enumerated values into the correct bit positions using
+    # the _scatter_bits() function.
+    incr_cands = []
+    free_bits = base_data.count('0')
+    for k in range(1, 2**free_bits):
+        # Get incremental dataword by scattering the enumeration bits
+        # into the zero bit positions in base_data.
+        incr_cand = _scatter_bits(base_data,
+                                  format(k, '0' + str(free_bits) + 'b'))
+        incr_cand_ecc = _ecc_encode(config, incr_cand)
+
+        # Dataword is correct by construction, but we need to check whether
+        # the ECC bits are incremental.
+        if _is_incremental_codeword(base_ecc, incr_cand_ecc):
+            # Check whether the candidate fulfills the maximum
+            # Hamming weight constraint.
+            if incr_cand_ecc.count('1') <= config['max_hw']:
+                # Check Hamming distance wrt all existing words.
+                for w in existing_words + [base_ecc]:
+                    if _get_hd(incr_cand_ecc, w) < config['min_hd']:
+                        break
+                else:
+                    incr_cands.append(incr_cand_ecc)
+
+    return incr_cands
+
+
+def _get_new_state_word_pair(config, existing_words):
+    '''Randomly generate a new incrementally writable word pair'''
+    while 1:
+        # Draw a random number and check whether it is unique and whether
+        # the Hamming weight is in range.
+        width = config['secded']['data_width']
+        ecc_width = config['secded']['ecc_width']
+        base = random.getrandbits(width)
+        base = format(base, '0' + str(width) + 'b')
+        base_cand_ecc = _ecc_encode(config, base)
+        # disallow all-zero and all-one states
+        pop_cnt = base_cand_ecc.count('1')
+        if pop_cnt >= config['min_hw'] and pop_cnt <= config['max_hw']:
+
+            # Check Hamming distance wrt all existing words
+            for w in existing_words:
+                if _get_hd(base_cand_ecc, w) < config['min_hd']:
+                    break
+            else:
+                # Get encoded incremental candidates.
+                incr_cands_ecc = _get_incremental_codewords(
+                    config, base_cand_ecc, existing_words)
+                # there are valid candidates, draw one at random.
+                # otherwise we just start over.
+                if incr_cands_ecc:
+                    incr_cand_ecc = random.choice(incr_cands_ecc)
+                    log.info('word {}: {}|{} -> {}|{}'.format(
+                        int(len(existing_words) / 2),
+                        base_cand_ecc[ecc_width:], base_cand_ecc[0:ecc_width],
+                        incr_cand_ecc[ecc_width:], incr_cand_ecc[0:ecc_width]))
+                    existing_words.append(base_cand_ecc)
+                    existing_words.append(incr_cand_ecc)
+                    return (base_cand_ecc, incr_cand_ecc)
+
+
+def _validate_words(config, words):
+    '''Validate generated words (base and incremental).'''
+    for k, w in enumerate(words):
+        # Check whether word is valid wrt to ECC polynomial.
+        if not _is_valid_codeword(config, w):
+            log.error('Codeword {} at index {} is not valid'.format(w, k))
+            exit(1)
+        # Check that word fulfills the Hamming weight constraints.
+        pop_cnt = w.count('1')
+        if pop_cnt < config['min_hw'] or pop_cnt > config['max_hw']:
+            log.error(
+                'Codeword {} at index {} has wrong Hamming weight'.format(
+                    w, k))
+            exit(1)
+        # Check Hamming distance wrt to all other existing words.
+        # If the constraint is larger than 0 this implies uniqueness.
+        if k < len(words) - 1:
+            for k2, w2 in enumerate(words[k + 1:]):
+                if _get_hd(w, w2) < config['min_hd']:
+                    log.error(
+                        'Hamming distance between codeword {} at index {} '
+                        'and codeword {} at index {} is too low.'.format(
+                            w, k, w2, k + 1 + k2))
+                    exit(1)
+
+
+def _hist_to_bars(hist, m):
+    '''Convert histogramm list into ASCII bar plot'''
+    bars = []
+    for i, j in enumerate(hist):
+        bar_prefix = "{:2}: ".format(i)
+        spaces = len(str(m)) - len(bar_prefix)
+        hist_bar = bar_prefix + (" " * spaces)
+        for k in range(j * 20 // max(hist)):
+            hist_bar += "|"
+        hist_bar += " ({:.2f}%)".format(100.0 * j / sum(hist)) if j else "--"
+        bars += [hist_bar]
+    return bars
+
+
+def hd_histogram(existing_words):
+    '''Build Hamming distance histogram'''
+    minimum_hd = len(existing_words[0])
+    maximum_hd = 0
+    minimum_hw = len(existing_words[0])
+    maximum_hw = 0
+    hist = [0] * (len(existing_words[0]) + 1)
+    for i, j in enumerate(existing_words):
+        minimum_hw = min(j.count('1'), minimum_hw)
+        maximum_hw = max(j.count('1'), maximum_hw)
+        if i < len(existing_words) - 1:
+            for k in existing_words[i + 1:]:
+                dist = _get_hd(j, k)
+                hist[dist] += 1
+                minimum_hd = min(dist, minimum_hd)
+                maximum_hd = max(dist, maximum_hd)
+
+    stats = {}
+    stats["hist"] = hist
+    stats["bars"] = _hist_to_bars(hist, len(existing_words))
+    stats["min_hd"] = minimum_hd
+    stats["max_hd"] = maximum_hd
+    stats["min_hw"] = minimum_hw
+    stats["max_hw"] = maximum_hw
+    return stats
+
+
+class LcStEnc():
+    '''Life cycle state encoding generator class
+
+    The constructor expects the parsed configuration
+    hjson to be passed in.
+    '''
+
+    # This holds the config dict.
+    config = {}
+    # Holds generated life cycle words.
+    gen = {
+        'ab_words': [],
+        'cd_words': [],
+        'ef_words': [],
+        'stats': [],
+    }
+
+    def __init__(self, config):
+        '''The constructor validates the configuration dict.'''
+
+        log.info('')
+        log.info('Generate life cycle state')
+        log.info('')
+
+        if 'seed' not in config:
+            log.error('Missing seed in configuration')
+            exit(1)
+
+        if 'secded' not in config:
+            log.error('Missing secded configuration')
+            exit(1)
+
+        config['secded'].setdefault('data_width', 0)
+        config['secded'].setdefault('ecc_width', 0)
+        config['secded'].setdefault('ecc_matrix', [[]])
+        config.setdefault('num_ab_words', 0)
+        config.setdefault('num_cd_words', 0)
+        config.setdefault('num_ef_words', 0)
+        config.setdefault('min_hw', 0)
+        config.setdefault('max_hw', 0)
+        config.setdefault('min_hd', 0)
+
+        config['seed'] = check_int(config['seed'])
+
+        log.info('Seed: {0:x}'.format(config['seed']))
+        log.info('')
+
+        config['secded']['data_width'] = check_int(
+            config['secded']['data_width'])
+        config['secded']['ecc_width'] = check_int(
+            config['secded']['ecc_width'])
+        config['num_ab_words'] = check_int(config['num_ab_words'])
+        config['num_cd_words'] = check_int(config['num_cd_words'])
+        config['num_ef_words'] = check_int(config['num_ef_words'])
+        config['min_hw'] = check_int(config['min_hw'])
+        config['max_hw'] = check_int(config['max_hw'])
+        config['min_hd'] = check_int(config['min_hd'])
+
+        total_width = config['secded']['data_width'] + config['secded'][
+            'ecc_width']
+
+        if config['min_hw'] >= total_width or \
+           config['max_hw'] > total_width or \
+           config['min_hw'] >= config['max_hw']:
+            log.error('Hamming weight constraints are inconsistent.')
+            exit(1)
+
+        if config['max_hw'] - config['min_hw'] + 1 < config['min_hd']:
+            log.error('Hamming distance constraint is inconsistent.')
+            exit(1)
+
+        if config['secded']['ecc_width'] != len(
+                config['secded']['ecc_matrix']):
+            log.error('ECC matrix does not have correct number of rows')
+            exit(1)
+
+        log.info('SECDED Matrix:')
+        for i, l in enumerate(config['secded']['ecc_matrix']):
+            log.info('ECC Bit {} Fanin: {}'.format(i, l))
+            for j, e in enumerate(l):
+                e = check_int(e)
+                config['secded']['ecc_matrix'][i][j] = e
+
+        log.info('')
+
+        self.config = config
+
+        # Re-initialize with seed to make results reproducible.
+        random.seed(int(self.config['seed']))
+
+        # Generate new encoding words
+        word_types = ['ab_words', 'cd_words', 'ef_words']
+        existing_words = []
+        for w in word_types:
+            while len(self.gen[w]) < self.config['num_' + w]:
+                new_word = _get_new_state_word_pair(self.config,
+                                                    existing_words)
+                self.gen[w].append(new_word)
+
+        # Validate words (this must not fail at this point).
+        _validate_words(self.config, existing_words)
+
+        # Print out HD histogram
+        self.gen['stats'] = hd_histogram(existing_words)
+
+        log.info('')
+        log.info('Hamming distance histogram:')
+        log.info('')
+        for bar in self.gen['stats']["bars"]:
+            log.info(bar)
+        log.info('')
+        log.info('Minimum HD: {}'.format(self.gen['stats']['min_hd']))
+        log.info('Maximum HD: {}'.format(self.gen['stats']['max_hd']))
+        log.info('Minimum HW: {}'.format(self.gen['stats']['min_hw']))
+        log.info('Maximum HW: {}'.format(self.gen['stats']['max_hw']))
+
+        log.info('')
+        log.info('Successfully generated life cycle state.')
+        log.info('')
diff --git a/hw/ip/otp_ctrl/util/OtpMemMap.py b/hw/ip/otp_ctrl/util/OtpMemMap.py
new file mode 100644
index 0000000..d843437
--- /dev/null
+++ b/hw/ip/otp_ctrl/util/OtpMemMap.py
@@ -0,0 +1,288 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""OTP memory map class, used to create the associated RTL and
+documentation, and to create OTP memory images for preloading.
+"""
+
+import logging as log
+import random
+from math import ceil, log2
+
+from tabulate import tabulate
+
+from common import check_bool, check_int
+
+DIGEST_SUFFIX = "_DIGEST"
+DIGEST_SIZE = 8
+
+
+class OtpMemMap():
+
+    # This holds the config dict.
+    config = {}
+
+    def __init__(self, config):
+
+        log.info('')
+        log.info('Parse and translate OTP memory map.')
+        log.info('')
+
+        if "seed" not in config:
+            log.error("Missing seed in configuration.")
+            exit(1)
+
+        config["seed"] = check_int(config["seed"])
+
+        # Initialize RNG.
+        random.seed(int(config['seed']))
+
+        offset = 0
+        num_part = 0
+        for part in config["partitions"]:
+            num_part += 1
+            # Defaults
+            part.setdefault("offset", offset)
+            part.setdefault("name", "unknown_name")
+            part.setdefault("variant", "Unbuffered")
+            part.setdefault("size", "0")
+            part.setdefault("secret", "false")
+            part.setdefault("sw_digest", "false")
+            part.setdefault("hw_digest", "false")
+            part.setdefault("write_lock", "none")
+            part.setdefault("read_lock", "none")
+            part.setdefault("key_sel", "NoKey")
+            log.info("Partition {} at offset {} with size {}".format(
+                part["name"], part["offset"], part["size"]))
+
+            # make sure these are boolean types (simplifies the mako templates)
+            part["secret"] = check_bool(part["secret"])
+            part["sw_digest"] = check_bool(part["sw_digest"])
+            part["hw_digest"] = check_bool(part["hw_digest"])
+            part["bkout_type"] = check_bool(part["bkout_type"])
+
+            # basic checks
+            if part["variant"] not in ["Unbuffered", "Buffered", "LifeCycle"]:
+                log.error("Invalid partition type {}".format(part["variant"]))
+                exit(1)
+
+            if part["key_sel"] not in [
+                    "NoKey", "Secret0Key", "Secret1Key", "Secret2Key"
+            ]:
+                log.error("Invalid key sel {}".format(part["key_sel"]))
+                exit(1)
+
+            if check_bool(part["secret"]) and part["key_sel"] == "NoKey":
+                log.error(
+                    "A secret partition needs a key select value other than NoKey"
+                )
+                exit(1)
+
+            if part["write_lock"].lower() not in ["digest", "csr", "none"]:
+                log.error("Invalid value for write_lock")
+                exit(1)
+
+            if part["read_lock"].lower() not in ["digest", "csr", "none"]:
+                log.error("Invalid value for read_lock")
+                exit(1)
+
+            if part["sw_digest"] and part["hw_digest"]:
+                log.error(
+                    "Partition cannot support both a SW and a HW digest at the same time."
+                )
+                exit(1)
+
+            if part["variant"] == "Unbuffered" and not part["sw_digest"]:
+                log.error(
+                    "Unbuffered partitions without digest are not supported at the moment."
+                )
+                exit(1)
+
+            if not part["sw_digest"] and not part["hw_digest"]:
+                if part["write_lock"].lower(
+                ) == "digest" or part["read_lock"].lower() == "digest":
+                    log.error(
+                        "A partition can only be write/read lockable if it has a hw or sw digest."
+                    )
+                    exit(1)
+
+            if check_int(part["offset"]) % 8:
+                log.error("Partition offset must be 64bit aligned")
+                exit(1)
+
+            if check_int(part["size"]) % 8:
+                log.error("Partition size must be 64bit aligned")
+                exit(1)
+
+            # Loop over items within a partition
+            for item in part["items"]:
+                item.setdefault("name", "unknown_name")
+                item.setdefault("size", "0")
+                item.setdefault("isdigest", "false")
+                item.setdefault("offset", offset)
+                # Generate random constant to be used when partition has
+                # not been initialized yet or when it is in error state.
+                if check_bool(item.setdefault("rand_inv_default", "false")):
+                    inv_default = random.getrandbits(
+                        check_int(item["size"]) * 8)
+                else:
+                    inv_default = 0
+                item.setdefault(
+                    "inv_default", "{}'h{:0X}".format(
+                        check_int(item["size"]) * 8, inv_default))
+                log.info("> Item {} at offset {} with size {}".format(
+                    item["name"], offset, item["size"]))
+                offset += check_int(item["size"])
+
+            # Place digest at the end of a partition.
+            if part["sw_digest"] or part["hw_digest"]:
+                part["items"].append({
+                    "name":
+                    part["name"] + DIGEST_SUFFIX,
+                    "size":
+                    DIGEST_SIZE,
+                    "offset":
+                    check_int(part["offset"]) + check_int(part["size"]) -
+                    DIGEST_SIZE,
+                    "isdigest":
+                    "True",
+                    "inv_default":
+                    "{256{1'b1}}"
+                })
+
+                log.info("> Adding digest {} at offset {} with size {}".format(
+                    part["name"] + DIGEST_SUFFIX, offset, DIGEST_SIZE))
+                offset += DIGEST_SIZE
+
+            if len(part["items"]) == 0:
+                log.warning("Partition does not contain any items.")
+
+            # check offsets and size
+            if offset > check_int(part["offset"]) + check_int(part["size"]):
+                log.error("Not enough space in partitition "
+                          "{} to accommodate all items. Bytes available "
+                          "= {}, bytes requested = {}".format(
+                              part["name"], part["size"],
+                              offset - part["offset"]))
+                exit(1)
+
+            offset = check_int(part["offset"]) + check_int(part["size"])
+
+        otp_size = check_int(config["otp"]["depth"]) * check_int(
+            config["otp"]["width"])
+        config["otp"]["size"] = otp_size
+        config["otp"]["addr_width"] = ceil(
+            log2(check_int(config["otp"]["depth"])))
+        config["otp"]["byte_addr_width"] = ceil(log2(check_int(otp_size)))
+
+        if offset > otp_size:
+            log.error(
+                "OTP is not big enough to store all partitions. "
+                "Bytes available {}, bytes required {}",
+                otp_size, offset)
+            exit(1)
+
+        log.info("Total number of partitions: {}".format(num_part))
+        log.info("Bytes available in OTP: {}".format(otp_size))
+        log.info("Bytes required for partitions: {}".format(offset))
+
+        self.config = config
+
+        log.info('')
+        log.info('Successfully parsed and translated OTP memory map.')
+        log.info('')
+
+
+    def create_partitions_table(self):
+        header = [
+            "Partition", "Secret", "Buffered", "WR Lockable", "RD Lockable",
+            "Description"
+        ]
+        table = [header]
+        colalign = ("center", ) * len(header)
+
+        for part in self.config["partitions"]:
+            is_secret = "yes" if check_bool(part["secret"]) else "no"
+            is_buffered = "yes" if part["variant"] in [
+                "Buffered", "LifeCycle"
+            ] else "no"
+            wr_lockable = "no"
+            if part["write_lock"].lower() in ["csr", "digest"]:
+                wr_lockable = "yes (" + part["write_lock"] + ")"
+            rd_lockable = "no"
+            if part["read_lock"].lower() in ["csr", "digest"]:
+                rd_lockable = "yes (" + part["read_lock"] + ")"
+            # remove newlines
+            desc = ' '.join(part["desc"].split())
+            row = [
+                part["name"], is_secret, is_buffered, wr_lockable, rd_lockable,
+                desc
+            ]
+            table.append(row)
+
+        return tabulate(table,
+                        headers="firstrow",
+                        tablefmt="pipe",
+                        colalign=colalign)
+
+    def create_mmap_table(self):
+        header = [
+            "Index", "Partition", "Size [B]", "Access Granule", "Item",
+            "Byte Address", "Size [B]"
+        ]
+        table = [header]
+        colalign = ("center", ) * len(header)
+
+        for k, part in enumerate(self.config["partitions"]):
+            for j, item in enumerate(part["items"]):
+                granule = "64bit" if check_bool(part["secret"]) else "32bit"
+
+                if check_bool(item["isdigest"]):
+                    granule = "64bit"
+                    name = "[{}](#Reg_{}_0)".format(item["name"],
+                                                    item["name"].lower())
+                else:
+                    name = item["name"]
+
+                if j == 0:
+                    row = [str(k), part["name"], str(part["size"]), granule]
+                else:
+                    row = ["", "", "", granule]
+
+                row.extend([
+                    name, "0x{:03X}".format(check_int(item["offset"])),
+                    str(item["size"])
+                ])
+
+                table.append(row)
+
+        return tabulate(table,
+                        headers="firstrow",
+                        tablefmt="pipe",
+                        colalign=colalign)
+
+    def create_digests_table(self):
+        header = ["Digest Name", " Affected Partition", "Calculated by HW"]
+        table = [header]
+        colalign = ("center", ) * len(header)
+
+        for part in self.config["partitions"]:
+            if check_bool(part["hw_digest"]) or check_bool(part["sw_digest"]):
+                is_hw_digest = "yes" if check_bool(part["hw_digest"]) else "no"
+                for item in part["items"]:
+                    if check_bool(item["isdigest"]):
+                        name = "[{}](#Reg_{}_0)".format(
+                            item["name"], item["name"].lower())
+                        row = [name, part["name"], is_hw_digest]
+                        table.append(row)
+                        break
+                else:
+                    log.error(
+                        "Partition with digest does not contain a digest item")
+                    exit(1)
+
+        return tabulate(table,
+                        headers="firstrow",
+                        tablefmt="pipe",
+                        colalign=colalign)
diff --git a/hw/ip/otp_ctrl/util/common.py b/hw/ip/otp_ctrl/util/common.py
new file mode 100644
index 0000000..6e482c3
--- /dev/null
+++ b/hw/ip/otp_ctrl/util/common.py
@@ -0,0 +1,53 @@
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Shared subfunctions.
+"""
+import logging as log
+import textwrap
+
+
+def wrapped_docstring():
+    '''Return a text-wrapped version of the module docstring'''
+    paras = []
+    para = []
+    for line in __doc__.strip().split('\n'):
+        line = line.strip()
+        if not line:
+            if para:
+                paras.append('\n'.join(para))
+                para = []
+        else:
+            para.append(line)
+    if para:
+        paras.append('\n'.join(para))
+
+    return '\n\n'.join(textwrap.fill(p) for p in paras)
+
+
+def check_bool(x):
+    """check_bool checks if input 'x' either a bool or
+       one of the following strings: ["true", "false"]
+
+        It returns value as Bool type.
+    """
+    if isinstance(x, bool):
+        return x
+    if not x.lower() in ["true", "false"]:
+        log.error("{} is not a boolean value.".format(x))
+        exit(1)
+    else:
+        return (x.lower() == "true")
+
+
+def check_int(x):
+    """check_int checks if input 'x' is decimal integer.
+
+        It returns value as an int type.
+    """
+    if isinstance(x, int):
+        return x
+    if not x.isdecimal():
+        log.error("{} is not a decimal number".format(x))
+        exit(1)
+    return int(x)
diff --git a/hw/ip/otp_ctrl/util/gen-lc-state-enc.py b/hw/ip/otp_ctrl/util/gen-lc-state-enc.py
new file mode 100755
index 0000000..d2284e3
--- /dev/null
+++ b/hw/ip/otp_ctrl/util/gen-lc-state-enc.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Given an ECC encoding matrix, this script generates random life cycle
+state encodings that can be incrementally written to a memory protected with
+the ECC code specified.
+"""
+import argparse
+import logging as log
+import random
+from pathlib import Path
+
+import hjson
+from mako.template import Template
+
+from LcStEnc import LcStEnc
+from common import wrapped_docstring
+
+# State encoding definition
+LC_STATE_DEFINITION_FILE = "../../lc_ctrl/data/lc_ctrl_state.hjson"
+# Code templates to render
+TEMPLATES = ["../../lc_ctrl/rtl/lc_ctrl_state_pkg.sv.tpl"]
+
+
+def main():
+    log.basicConfig(level=log.INFO,
+                    format="%(asctime)s - %(message)s",
+                    datefmt="%Y-%m-%d %H:%M")
+
+    parser = argparse.ArgumentParser(
+        prog="gen-lc-state-enc",
+        description=wrapped_docstring(),
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+
+    parser.add_argument('-s',
+                        '--seed',
+                        type=int,
+                        metavar='<seed>',
+                        help='Custom seed for RNG.')
+
+    args = parser.parse_args()
+
+    with open(LC_STATE_DEFINITION_FILE, 'r') as infile:
+        config = hjson.load(infile)
+
+        # If specified, override the seed for random netlist constant computation.
+        if args.seed:
+            log.warning('Commandline override of seed with {}.'.format(
+                args.seed))
+            config['seed'] = args.seed
+        # Otherwise, we either take it from the .hjson if present, or
+        # randomly generate a new seed if not.
+        else:
+            random.seed()
+            new_seed = random.getrandbits(64)
+            if config.setdefault('seed', new_seed) == new_seed:
+                log.warning(
+                    'No seed specified, setting to {}.'.format(new_seed))
+
+        # validate config and generate encoding
+        lc_st_enc = LcStEnc(config)
+
+        # render all templates
+        for template in TEMPLATES:
+            with open(template, 'r') as tplfile:
+                tpl = Template(tplfile.read())
+                with open(
+                        Path(template).parent.joinpath(Path(template).stem),
+                        'w') as outfile:
+                    outfile.write(tpl.render(lc_st_enc=lc_st_enc))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hw/ip/otp_ctrl/util/gen-otp-mmap.py b/hw/ip/otp_ctrl/util/gen-otp-mmap.py
new file mode 100755
index 0000000..333fb57
--- /dev/null
+++ b/hw/ip/otp_ctrl/util/gen-otp-mmap.py
@@ -0,0 +1,96 @@
+#!/usr/bin/env python3
+# Copyright lowRISC contributors.
+# Licensed under the Apache License, Version 2.0, see LICENSE for details.
+# SPDX-License-Identifier: Apache-2.0
+r"""Generate RTL and documentation collateral from OTP memory
+map definition file (hjson).
+"""
+import argparse
+import logging as log
+import random
+from pathlib import Path
+
+import hjson
+from mako.template import Template
+
+from common import wrapped_docstring
+# Import OTP memory map generator.
+from OtpMemMap import OtpMemMap
+
+TABLE_HEADER_COMMENT = '''<!--
+DO NOT EDIT THIS FILE DIRECTLY.
+It has been generated with hw/ip/otp_ctrl/util/gen-otp-mmap.py
+-->
+
+'''
+
+# memory map source
+MMAP_DEFINITION_FILE = "../data/otp_ctrl_mmap.hjson"
+# documentation tables to generate
+PARTITIONS_TABLE_FILE = "../doc/otp_ctrl_partitions.md"
+DIGESTS_TABLE_FILE = "../doc/otp_ctrl_digests.md"
+MMAP_TABLE_FILE = "../doc/otp_ctrl_mmap.md"
+# code templates to render
+TEMPLATES = ["../data/otp_ctrl.hjson.tpl", "../rtl/otp_ctrl_part_pkg.sv.tpl"]
+
+
+def main():
+    log.basicConfig(level=log.INFO,
+                    format="%(asctime)s - %(message)s",
+                    datefmt="%Y-%m-%d %H:%M")
+
+    parser = argparse.ArgumentParser(
+        prog="gen-otp-mmap",
+        description=wrapped_docstring(),
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+
+    # Generator options for compile time random netlist constants
+    parser.add_argument('--seed',
+                        type=int,
+                        metavar='<seed>',
+                        help='Custom seed for RNG to compute default values.')
+
+    args = parser.parse_args()
+
+    with open(MMAP_DEFINITION_FILE, 'r') as infile:
+        config = hjson.load(infile)
+
+        # If specified, override the seed for random netlist constant computation.
+        if args.seed:
+            log.warning('Commandline override of seed with {}.'.format(
+                args.seed))
+            config['seed'] = args.seed
+        # Otherwise, we either take it from the .hjson if present, or
+        # randomly generate a new seed if not.
+        else:
+            random.seed()
+            new_seed = random.getrandbits(64)
+            if config.setdefault('seed', new_seed) == new_seed:
+                log.warning(
+                    'No seed specified, setting to {}.'.format(new_seed))
+
+        otp_mmap = OtpMemMap(config)
+
+        with open(PARTITIONS_TABLE_FILE, 'w') as outfile:
+            outfile.write(TABLE_HEADER_COMMENT +
+                          otp_mmap.create_partitions_table())
+
+        with open(DIGESTS_TABLE_FILE, 'w') as outfile:
+            outfile.write(TABLE_HEADER_COMMENT +
+                          otp_mmap.create_digests_table())
+
+        with open(MMAP_TABLE_FILE, 'w') as outfile:
+            outfile.write(TABLE_HEADER_COMMENT + otp_mmap.create_mmap_table())
+
+        # render all templates
+        for template in TEMPLATES:
+            with open(template, 'r') as tplfile:
+                tpl = Template(tplfile.read())
+                with open(
+                        Path(template).parent.joinpath(Path(template).stem),
+                        'w') as outfile:
+                    outfile.write(tpl.render(otp_mmap=otp_mmap))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/hw/ip/otp_ctrl/util/translate-mmap.py b/hw/ip/otp_ctrl/util/translate-mmap.py
deleted file mode 100755
index c5f7d13..0000000
--- a/hw/ip/otp_ctrl/util/translate-mmap.py
+++ /dev/null
@@ -1,384 +0,0 @@
-#!/usr/bin/env python3
-# Copyright lowRISC contributors.
-# Licensed under the Apache License, Version 2.0, see LICENSE for details.
-# SPDX-License-Identifier: Apache-2.0
-r"""Convert mako template to Hjson register description
-"""
-import argparse
-import logging as log
-import textwrap
-import random
-from math import ceil, log2
-from pathlib import Path
-
-import hjson
-from mako.template import Template
-from tabulate import tabulate
-
-DIGEST_SUFFIX = "_DIGEST"
-DIGEST_SIZE = 8
-
-TABLE_HEADER_COMMENT = '''<!--
-DO NOT EDIT THIS FILE DIRECTLY.
-It has been generated with hw/ip/otp_ctrl/util/translate-mmap.py
--->
-
-'''
-
-# memory map source
-MMAP_DEFINITION_FILE = "../data/otp_ctrl_mmap.hjson"
-# documentation tables to generate
-PARTITIONS_TABLE_FILE = "../doc/otp_ctrl_partitions.md"
-DIGESTS_TABLE_FILE = "../doc/otp_ctrl_digests.md"
-MMAP_TABLE_FILE = "../doc/otp_ctrl_mmap.md"
-# code templates to render
-TEMPLATES = ["../data/otp_ctrl.hjson.tpl", "../rtl/otp_ctrl_part_pkg.sv.tpl"]
-
-
-def wrapped_docstring():
-    '''Return a text-wrapped version of the module docstring'''
-    paras = []
-    para = []
-    for line in __doc__.strip().split('\n'):
-        line = line.strip()
-        if not line:
-            if para:
-                paras.append('\n'.join(para))
-                para = []
-        else:
-            para.append(line)
-    if para:
-        paras.append('\n'.join(para))
-
-    return '\n\n'.join(textwrap.fill(p) for p in paras)
-
-
-def check_bool(x):
-    """check_bool checks if input 'x' either a bool or
-       one of the following strings: ["true", "false"]
-
-        It returns value as Bool type.
-    """
-    if isinstance(x, bool):
-        return x
-    if not x.lower() in ["true", "false"]:
-        log.error("{} is not a boolean value.".format(x))
-        exit(1)
-    else:
-        return (x.lower() == "true")
-
-
-def check_int(x):
-    """check_int checks if input 'x' is decimal integer.
-
-        It returns value as an int type.
-    """
-    if isinstance(x, int):
-        return x
-    if not x.isdecimal():
-        log.error("{} is not a decimal number".format(x))
-        exit(1)
-    return int(x)
-
-
-def validate(config):
-    offset = 0
-    num_part = 0
-    for part in config["partitions"]:
-        num_part += 1
-        # Defaults
-        part.setdefault("offset", offset)
-        part.setdefault("name", "unknown_name")
-        part.setdefault("variant", "Unbuffered")
-        part.setdefault("size", "0")
-        part.setdefault("secret", "false")
-        part.setdefault("sw_digest", "false")
-        part.setdefault("hw_digest", "false")
-        part.setdefault("write_lock", "none")
-        part.setdefault("read_lock", "none")
-        part.setdefault("key_sel", "NoKey")
-        log.info("Partition {} at offset {} with size {}".format(
-            part["name"], part["offset"], part["size"]))
-
-        # make sure these are boolean types (simplifies the mako templates)
-        part["secret"] = check_bool(part["secret"])
-        part["sw_digest"] = check_bool(part["sw_digest"])
-        part["hw_digest"] = check_bool(part["hw_digest"])
-        part["bkout_type"] = check_bool(part["bkout_type"])
-
-        # basic checks
-        if part["variant"] not in ["Unbuffered", "Buffered", "LifeCycle"]:
-            log.error("Invalid partition type {}".format(part["variant"]))
-            exit(1)
-
-        if part["key_sel"] not in [
-                "NoKey", "Secret0Key", "Secret1Key", "Secret2Key"
-        ]:
-            log.error("Invalid key sel {}".format(part["key_sel"]))
-            exit(1)
-
-        if check_bool(part["secret"]) and part["key_sel"] == "NoKey":
-            log.error(
-                "A secret partition needs a key select value other than NoKey")
-            exit(1)
-
-        if part["write_lock"].lower() not in ["digest", "csr", "none"]:
-            log.error("Invalid value for write_lock")
-            exit(1)
-
-        if part["read_lock"].lower() not in ["digest", "csr", "none"]:
-            log.error("Invalid value for read_lock")
-            exit(1)
-
-        if part["sw_digest"] and part["hw_digest"]:
-            log.error(
-                "Partition cannot support both a SW and a HW digest at the same time."
-            )
-            exit(1)
-
-        if part["variant"] == "Unbuffered" and not part["sw_digest"]:
-            log.error(
-                "Unbuffered partitions without digest are not supported at the moment."
-            )
-            exit(1)
-
-        if not part["sw_digest"] and not part["hw_digest"]:
-            if part["write_lock"].lower(
-            ) == "digest" or part["read_lock"].lower() == "digest":
-                log.error(
-                    "A partition can only be write/read lockable if it has a hw or sw digest."
-                )
-                exit(1)
-
-        if check_int(part["offset"]) % 8:
-            log.error("Partition offset must be 64bit aligned")
-            exit(1)
-
-        if check_int(part["size"]) % 8:
-            log.error("Partition size must be 64bit aligned")
-            exit(1)
-
-        # Loop over items within a partition
-        for item in part["items"]:
-            item.setdefault("name", "unknown_name")
-            item.setdefault("size", "0")
-            item.setdefault("isdigest", "false")
-            item.setdefault("offset", offset)
-            # Generate random constant to be used when partition has
-            # not been initialized yet or when it is in error state.
-            if check_bool(item.setdefault("rand_inv_default", "false")):
-                inv_default = random.getrandbits(check_int(item["size"])*8)
-            else:
-                inv_default = 0
-            item.setdefault("inv_default", "{}'h{:0X}".format(check_int(item["size"])*8, inv_default))
-            log.info("> Item {} at offset {} with size {}".format(
-                item["name"], offset, item["size"]))
-            offset += check_int(item["size"])
-
-        # Place digest at the end of a partition.
-        if part["sw_digest"] or part["hw_digest"]:
-            part["items"].append({
-                "name":
-                part["name"] + DIGEST_SUFFIX,
-                "size":
-                DIGEST_SIZE,
-                "offset":
-                check_int(part["offset"]) + check_int(part["size"]) -
-                DIGEST_SIZE,
-                "isdigest":
-                "True",
-                "inv_default":
-                "{256{1'b1}}"
-            })
-
-            log.info("> Adding digest {} at offset {} with size {}".format(
-                part["name"] + DIGEST_SUFFIX, offset, DIGEST_SIZE))
-            offset += DIGEST_SIZE
-
-        if len(part["items"]) == 0:
-            log.warning("Partition does not contain any items.")
-
-        # check offsets and size
-        if offset > check_int(part["offset"]) + check_int(part["size"]):
-            log.error("Not enough space in partitition "
-                      "{} to accommodate all items. Bytes available "
-                      "= {}, bytes requested = {}".format(
-                          part["name"], part["size"], offset - part["offset"]))
-            exit(1)
-
-        offset = check_int(part["offset"]) + check_int(part["size"])
-
-    otp_size = check_int(config["otp"]["depth"]) * check_int(
-        config["otp"]["width"])
-    config["otp"]["size"] = otp_size
-    config["otp"]["addr_width"] = ceil(log2(check_int(config["otp"]["depth"])))
-    config["otp"]["byte_addr_width"] = ceil(log2(check_int(otp_size)))
-
-    if offset > otp_size:
-        log.error(
-            "OTP is not big enough to store all partitions. Bytes available {}, bytes required {}",
-            otp_size, offset)
-        exit(1)
-
-    log.info("Total number of partitions: {}".format(num_part))
-    log.info("Bytes available in OTP: {}".format(otp_size))
-    log.info("Bytes required for partitions: {}".format(offset))
-
-
-def create_partitions_table(config):
-    header = [
-        "Partition", "Secret", "Buffered", "WR Lockable", "RD Lockable",
-        "Description"
-    ]
-    table = [header]
-    colalign = ("center", ) * len(header)
-
-    for part in config["partitions"]:
-        is_secret = "yes" if check_bool(part["secret"]) else "no"
-        is_buffered = "yes" if part["variant"] in ["Buffered", "LifeCycle"
-                                                   ] else "no"
-        wr_lockable = "no"
-        if part["write_lock"].lower() in ["csr", "digest"]:
-            wr_lockable = "yes (" + part["write_lock"] + ")"
-        rd_lockable = "no"
-        if part["read_lock"].lower() in ["csr", "digest"]:
-            rd_lockable = "yes (" + part["read_lock"] + ")"
-        # remove newlines
-        desc = ' '.join(part["desc"].split())
-        row = [
-            part["name"], is_secret, is_buffered, wr_lockable, rd_lockable,
-            desc
-        ]
-        table.append(row)
-
-    return tabulate(table,
-                    headers="firstrow",
-                    tablefmt="pipe",
-                    colalign=colalign)
-
-
-def create_mmap_table(config):
-    header = [
-        "Index", "Partition", "Size [B]", "Access Granule", "Item",
-        "Byte Address", "Size [B]"
-    ]
-    table = [header]
-    colalign = ("center", ) * len(header)
-
-    for k, part in enumerate(config["partitions"]):
-        for j, item in enumerate(part["items"]):
-            granule = "64bit" if check_bool(part["secret"]) else "32bit"
-
-            if check_bool(item["isdigest"]):
-                granule = "64bit"
-                name = "[{}](#Reg_{}_0)".format(item["name"],
-                                                item["name"].lower())
-            else:
-                name = item["name"]
-
-            if j == 0:
-                row = [str(k), part["name"], str(part["size"]), granule]
-            else:
-                row = ["", "", "", granule]
-
-            row.extend([
-                name, "0x{:03X}".format(check_int(item["offset"])),
-                str(item["size"])
-            ])
-
-            table.append(row)
-
-    return tabulate(table,
-                    headers="firstrow",
-                    tablefmt="pipe",
-                    colalign=colalign)
-
-
-def create_digests_table(config):
-    header = ["Digest Name", " Affected Partition", "Calculated by HW"]
-    table = [header]
-    colalign = ("center", ) * len(header)
-
-    for part in config["partitions"]:
-        if check_bool(part["hw_digest"]) or check_bool(part["sw_digest"]):
-            is_hw_digest = "yes" if check_bool(part["hw_digest"]) else "no"
-            for item in part["items"]:
-                if check_bool(item["isdigest"]):
-                    name = "[{}](#Reg_{}_0)".format(item["name"],
-                                                    item["name"].lower())
-                    row = [name, part["name"], is_hw_digest]
-                    table.append(row)
-                    break
-            else:
-                log.error(
-                    "Partition with digest does not contain a digest item")
-                exit(1)
-
-    return tabulate(table,
-                    headers="firstrow",
-                    tablefmt="pipe",
-                    colalign=colalign)
-
-
-def main():
-    log.basicConfig(level=log.INFO,
-                    format="%(asctime)s - %(message)s",
-                    datefmt="%Y-%m-%d %H:%M")
-
-    parser = argparse.ArgumentParser(
-        prog="translate-mmap",
-        description=wrapped_docstring(),
-        formatter_class=argparse.RawDescriptionHelpFormatter)
-
-    # Generator options for compile time random netlist constants
-    parser.add_argument(
-        '--seed',
-        type=int,
-        metavar='<seed>',
-        help='Custom seed for RNG to compute default values.')
-
-    args = parser.parse_args()
-
-    with open(MMAP_DEFINITION_FILE, 'r') as infile:
-        config = hjson.load(infile)
-
-        # If specified, override the seed for random netlist constant computation.
-        if args.seed:
-            log.warning('Commandline override of seed with {}.'.format(
-                args.seed))
-            config['seed'] = args.seed
-        # Otherwise, we either take it from the .hjson if present, or
-        # randomly generate a new seed if not.
-        else:
-            random.seed()
-            new_seed = random.getrandbits(64)
-            if config.setdefault('seed', new_seed) == new_seed:
-                log.warning(
-                    'No seed specified, setting to {}.'.format(new_seed))
-
-        # Initialize RNG.
-        random.seed(int(config['seed']))
-
-        validate(config)
-
-        with open(PARTITIONS_TABLE_FILE, 'w') as outfile:
-            outfile.write(TABLE_HEADER_COMMENT + create_partitions_table(config))
-
-        with open(DIGESTS_TABLE_FILE, 'w') as outfile:
-            outfile.write(TABLE_HEADER_COMMENT + create_digests_table(config))
-
-        with open(MMAP_TABLE_FILE, 'w') as outfile:
-            outfile.write(TABLE_HEADER_COMMENT + create_mmap_table(config))
-
-        # render all templates
-        for template in TEMPLATES:
-            with open(template, 'r') as tplfile:
-                tpl = Template(tplfile.read())
-                with open(
-                        Path(template).parent.joinpath(Path(template).stem),
-                        'w') as outfile:
-                    outfile.write(tpl.render(config=config))
-
-
-if __name__ == "__main__":
-    main()