| #!/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"""SECDED encoder/decoder generator | |
| Current version doesn't optimize Fan-In. It uses Hsiao code (modified version | |
| of Hamming code + parity). Please refer https://arxiv.org/pdf/0803.1217.pdf | |
| For some further background and info on the differences between Hamming and | |
| Hsiao SECDED codes, refer to https://ieeexplore.ieee.org/document/8110065.g | |
| """ | |
| import argparse | |
| import functools | |
| import itertools | |
| import logging as log | |
| import math | |
| import random | |
| import hjson | |
| import subprocess | |
| from typing import Any, Dict, List, Tuple | |
| from pathlib import Path | |
| COPYRIGHT = """// Copyright lowRISC contributors. | |
| // Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| // SPDX-License-Identifier: Apache-2.0 | |
| // | |
| """ | |
| C_SRC_TOP = """ | |
| #include "secded_enc.h" | |
| #include <stdbool.h> | |
| #include <stdint.h> | |
| // Calculates even parity for a 64-bit word | |
| static uint8_t calc_parity(uint64_t word, bool invert) { | |
| bool parity = false; | |
| while (word) { | |
| if (word & 1) { | |
| parity = !parity; | |
| } | |
| word >>= 1; | |
| } | |
| return parity ^ invert; | |
| } | |
| """ | |
| C_H_TOP = """ | |
| #ifndef OPENTITAN_HW_IP_PRIM_DV_PRIM_SECDED_SECDED_ENC_H_ | |
| #define OPENTITAN_HW_IP_PRIM_DV_PRIM_SECDED_SECDED_ENC_H_ | |
| #include <stdint.h> | |
| #ifdef __cplusplus | |
| extern "C" { | |
| #endif // __cplusplus | |
| // Integrity encode functions for varying bit widths matching the functionality | |
| // of the RTL modules of the same name. Each takes an array of bytes in | |
| // little-endian order and returns the calculated integrity bits. | |
| """ | |
| C_H_FOOT = """ | |
| #ifdef __cplusplus | |
| } // extern "C" | |
| #endif // __cplusplus | |
| #endif // OPENTITAN_HW_IP_PRIM_DV_PRIM_SECDED_SECDED_ENC_H_ | |
| """ | |
| CODE_OPTIONS = {'hsiao': '', | |
| 'inv_hsiao': '_inv', | |
| 'hamming': '_hamming', | |
| 'inv_hamming': '_inv_hamming'} | |
| # secded configurations | |
| SECDED_CFG_FILE = "util/design/data/secded_cfg.hjson" | |
| PROJ_ROOT = Path(__file__).parent.parent.parent | |
| SECDED_CFG_PATH = Path(PROJ_ROOT) / SECDED_CFG_FILE | |
| # The seed we use to initialise the PRNG when running the randomised algorithm | |
| # to choose constants for Hsiao codes. | |
| _RND_SEED = 123 | |
| def min_paritysize(k): | |
| # SECDED --> Hamming distance 'd': 4 | |
| # 2^(m-1) should cover (m+k) | |
| for m in range(2, 10): | |
| if 2**m >= (k + m + 1): | |
| return m + 1 | |
| return -1 | |
| def ideal_fanin(k, m): | |
| """Compute Ideal Max Fanin of any bit in the ecc codes.""" | |
| fanin = 0 | |
| needed = k | |
| for select in range(3, m + 1, 2): | |
| combinations = list(itertools.combinations(range(m), select)) | |
| if len(combinations) <= needed: | |
| fanin += int(math.ceil(float(len(combinations) * select) / m)) | |
| needed -= len(combinations) | |
| else: | |
| fanin += int(math.ceil(float(needed * select) / m)) | |
| needed = 0 | |
| if not needed: | |
| break | |
| return fanin | |
| def calc_fanin(width, codes): | |
| """Sum the ones in a column""" | |
| fanins = [0] * width | |
| log.info("Calc Code: {}".format(codes)) | |
| for i in codes: | |
| for e in i: | |
| fanins[e] += 1 | |
| return fanins | |
| def calc_bitmasks(k, m, codes, dec): | |
| # Transform fanin indices into bitmask. | |
| fanin_masks = [0] * m | |
| for i, c in enumerate(codes): | |
| for j in c: | |
| fanin_masks[j] += 1 << i | |
| # For decode ops, include ECC bit position. | |
| if dec: | |
| for j in range(m): | |
| fanin_masks[j] += 1 << (k + j) | |
| return tuple(fanin_masks) | |
| def print_secded_enum_and_util_fns(cfgs): | |
| enum_vals = [" SecdedNone"] | |
| parity_width_vals = [] | |
| data_width_vals = [] | |
| for cfg in cfgs: | |
| k = cfg['k'] | |
| m = cfg['m'] | |
| n = k + m | |
| suffix = CODE_OPTIONS[cfg['code_type']] | |
| suffix = suffix.split('_') | |
| suffix = [x.capitalize() for x in suffix] | |
| formatted_suffix = ''.join(suffix) | |
| enum_name = " Secded%s_%s_%s" % (formatted_suffix, n, k) | |
| enum_vals.append(enum_name) | |
| parity_width = " %s: return %s;" % (enum_name, m) | |
| parity_width_vals.append(parity_width) | |
| data_width = " %s: return %s;" % (enum_name, k) | |
| data_width_vals.append(data_width) | |
| enum_str = ",\n".join(enum_vals) | |
| parity_width_fn_str = "\n".join(parity_width_vals) | |
| data_width_fn_str = "\n".join(data_width_vals) | |
| enum_str = ''' | |
| typedef enum int {{ | |
| {} | |
| }} prim_secded_e; | |
| function automatic int get_ecc_data_width(prim_secded_e ecc_type); | |
| case (ecc_type) | |
| {} | |
| // Return a non-zero width to avoid VCS compile issues | |
| default: return 32; | |
| endcase | |
| endfunction | |
| function automatic int get_ecc_parity_width(prim_secded_e ecc_type); | |
| case (ecc_type) | |
| {} | |
| default: return 0; | |
| endcase | |
| endfunction | |
| '''.format(enum_str, data_width_fn_str, parity_width_fn_str) | |
| return enum_str | |
| def print_pkg_allzero(n, k, m, codes, suffix, codetype): | |
| suffix = suffix.split('_') | |
| suffix = [x.capitalize() for x in suffix] | |
| suffix = ''.join(suffix) | |
| invecc = 0 | |
| invcode = 0 | |
| if codetype in ["inv_hsiao", "inv_hamming"]: | |
| for x in range(m): | |
| invecc += (x % 2) << x | |
| invcode = invecc << k | |
| zerostr = f''' | |
| parameter logic [{m-1}:0] Secded{suffix}{n}{k}ZeroEcc = {m}'h{invecc:0X}; | |
| parameter logic [{n-1}:0] Secded{suffix}{n}{k}ZeroWord = {n}'h{invcode:0X}; | |
| ''' | |
| return zerostr | |
| def print_pkg_types(n, k, m, codes, suffix, codetype): | |
| typename = "secded%s_%d_%d_t" % (suffix, n, k) | |
| typestr = ''' | |
| typedef struct packed {{ | |
| logic [{}:0] data; | |
| logic [{}:0] syndrome; | |
| logic [1:0] err; | |
| }} {}; | |
| '''.format((k - 1), (m - 1), typename) | |
| return typestr | |
| def print_fn(n, k, m, codes, suffix, codetype, inv=False): | |
| enc_out = print_enc(n, k, m, codes, codetype) | |
| dec_out = print_dec(n, k, m, codes, codetype, "function") | |
| typename = "secded%s_%d_%d_t" % (suffix, n, k) | |
| module_name = "prim_secded%s_%d_%d" % (suffix, n, k) | |
| outstr = ''' | |
| function automatic logic [{}:0] | |
| {}_enc (logic [{}:0] data_i); | |
| logic [{}:0] data_o; | |
| {} return data_o; | |
| endfunction | |
| function automatic {} | |
| {}_dec (logic [{}:0] data_i); | |
| logic [{}:0] data_o; | |
| logic [{}:0] syndrome_o; | |
| logic [1:0] err_o; | |
| {} dec; | |
| {} | |
| dec.data = data_o; | |
| dec.syndrome = syndrome_o; | |
| dec.err = err_o; | |
| return dec; | |
| endfunction | |
| '''.format((n - 1), module_name, (k - 1), (n - 1), enc_out, | |
| typename, module_name, (n - 1), (k - 1), (m - 1), typename, dec_out) | |
| return outstr | |
| def print_enc(n, k, m, codes, codetype): | |
| invert = 1 if codetype in ["inv_hsiao", "inv_hamming"] else 0 | |
| outstr = " data_o = {}'(data_i);\n".format(n) | |
| hex_format = str(n) + "'h{:0" + str((n + 3) // 4) + "X}" | |
| format_str = " data_o[{}] = ^(data_o & " + hex_format + ");\n" | |
| # Print parity computation If inverted encoding is turned on, we only | |
| # invert every odd bit so that both all-one and all-zero encodings are not | |
| # possible. This works for most encodings generated if the fanin is | |
| # balanced (such as inverted Hsiao codes). However, since there is no | |
| # guarantee, an FPV assertion is added to prove that all-zero and all-one | |
| # encodings do not exist if an inverted code is used. | |
| inv_mask = 0 | |
| for j, mask in enumerate(calc_bitmasks(k, m, codes, False)): | |
| inv_mask += (j % 2) << j | |
| outstr += format_str.format(j + k, mask) | |
| # Selectively invert parity bits as determined above. | |
| if invert: | |
| outstr += (" data_o ^= " + hex_format + ";\n").format(inv_mask << k) | |
| return outstr | |
| def calc_syndrome(code): | |
| log.info("in syndrome {}".format(code)) | |
| return sum(map((lambda x: 2**x), code)) | |
| def print_dec(n, k, m, codes, codetype, print_type="logic"): | |
| outstr = "" | |
| outstr += " // Syndrome calculation\n" | |
| hexfmt = str(n) + "'h{:0" + str((n + 3) // 4) + "X}" | |
| format_str = " syndrome_o[{}] = ^(" | |
| # Add ECC bit inversion if needed (see print_enc function). | |
| if codetype in ["inv_hsiao", "inv_hamming"]: | |
| invval = 0 | |
| for x in range(m): | |
| invval += (x % 2) << x | |
| format_str += "(data_i ^ " + hexfmt.format(invval << k) + ")" | |
| else: | |
| format_str += "data_i" | |
| format_str += " & " + hexfmt + ");\n" | |
| # Print syndrome computation | |
| for j, mask in enumerate(calc_bitmasks(k, m, codes, True)): | |
| outstr += format_str.format(j, mask) | |
| outstr += "\n" | |
| outstr += " // Corrected output calculation\n" | |
| for i in range(k): | |
| outstr += " data_o[%d] = (syndrome_o == %d'h%x) ^ data_i[%d];\n" % ( | |
| i, m, calc_syndrome(codes[i]), i) | |
| outstr += "\n" | |
| outstr += " // err_o calc. bit0: single error, bit1: double error\n" | |
| # The Hsiao and Hamming syndromes are interpreted slightly differently. | |
| if codetype in ["hamming", "inv_hamming"]: | |
| outstr += " err_o[0] = syndrome_o[%d];\n" % (m - 1) | |
| outstr += " err_o[1] = |syndrome_o[%d:0] & ~syndrome_o[%d];\n" % ( | |
| m - 2, m - 1) | |
| else: | |
| outstr += " err_o[0] = ^syndrome_o;\n" | |
| outstr += " err_o[1] = ~err_o[0] & (|syndrome_o);\n" | |
| return outstr | |
| def verify(cfgs): | |
| error = 0 | |
| for cfg in cfgs['cfgs']: | |
| if (cfg['k'] <= 1 or cfg['k'] > 120): | |
| error += 1 | |
| log.error("Current tool doesn't support the value k (%d)", cfg['k']) | |
| if (cfg['m'] <= 1 or cfg['m'] > 20): | |
| error += 1 | |
| log.error("Current tool doesn't support the value m (%d)", cfg['m']) | |
| # Calculate 'm' (parity size) | |
| min_m = min_paritysize(cfg['k']) | |
| if (cfg['m'] < min_m): | |
| error += 1 | |
| log.error("given \'m\' argument is smaller than minimum requirement " + | |
| "using calculated minimum (%d)", min_m) | |
| # Error check code selection | |
| if (cfg['code_type'] not in CODE_OPTIONS): | |
| error += 1 | |
| log.error("Invalid code {} selected, use one of {}".format( | |
| cfg['code_type'], CODE_OPTIONS)) | |
| return error | |
| def _ecc_pick_code(config: Dict[str, Any], codetype: str, k: int) -> Tuple[int, List[int], int]: | |
| # first check to see if bit width is supported among configuration | |
| codes = None | |
| bitmasks = None | |
| m = None | |
| for cfg in config['cfgs']: | |
| if cfg['k'] == k and cfg['code_type'] == codetype: | |
| m = cfg['m'] | |
| codes = gen_code(codetype, k, m) | |
| bitmasks = calc_bitmasks(k, m, codes, False) | |
| invert = 1 if codetype in ['inv_hsiao', 'inv_hamming'] else 0 | |
| return (m, bitmasks, invert) | |
| # error if k not supported | |
| raise Exception(f'ECC for length {k} of type {codetype} unsupported') | |
| @functools.lru_cache(maxsize=None) | |
| def _ecc_encode(k: int, | |
| m: int, bitmasks: List[int], invert: int, | |
| dataword: int) -> int: | |
| assert 0 <= dataword < (1 << k) | |
| # represent supplied dataword as a binary string | |
| word_bin = format(dataword, '0' + str(k) + 'b') | |
| codeword = word_bin | |
| for k, mask in enumerate(bitmasks): | |
| bit = 0 | |
| log.debug(f'codeword: {codeword}') | |
| log.debug(f'mask: {hex(mask)}') | |
| mask = (format(mask, '0' + str(k + m) + 'b')) | |
| # reverse codeword for index selection | |
| # This is because the LSB is the farthest entry in the string | |
| codeword_rev = codeword[::-1] | |
| for idx, f in enumerate(mask[::-1]): | |
| if int(f): | |
| bit ^= int(codeword_rev[idx]) | |
| # Add ECC bit inversion if needed (see print_enc function). | |
| bit ^= (invert & k % 2) | |
| codeword = str(bit) + codeword | |
| return codeword | |
| def ecc_encode(config: Dict[str, Any], codetype: str, k: int, dataword: int) -> Tuple[int, int]: | |
| log.info(f"Encoding ECC for {hex(dataword)}") | |
| m, bitmasks, invert = _ecc_pick_code(config, codetype, k) | |
| codeword = _ecc_encode(k, m, bitmasks, invert, dataword) | |
| # Debug printouts | |
| log.debug(f'original hex: {hex(dataword)}') | |
| log.debug(f'codeword hex: {hex(int(codeword,2))}') | |
| return int(codeword, 2), m | |
| def ecc_encode_some(config: Dict[str, Any], | |
| codetype: str, | |
| k: int, | |
| datawords: int) -> Tuple[List[int], int]: | |
| m, bitmasks, invert = _ecc_pick_code(config, codetype, k) | |
| codewords = [int(_ecc_encode(k, m, bitmasks, invert, w), 2) | |
| for w in datawords] | |
| return codewords, m | |
| def gen_code(codetype, k, m): | |
| # The hsiao_code generator uses (pseudo)random values to pick good ECC | |
| # constants. Rather than exposing the seed, we pick a fixed one here to | |
| # ensure everything stays stable in future. | |
| old_rnd_state = random.getstate() | |
| random.seed(_RND_SEED) | |
| try: | |
| return globals()["_{}_code".format(codetype)](k, m) | |
| finally: | |
| random.setstate(old_rnd_state) | |
| def generate(cfgs, args): | |
| pkg_out_str = "" | |
| pkg_type_str = "" | |
| c_src_filename = args.c_outdir + "/" + "secded_enc.c" | |
| c_h_filename = args.c_outdir + "/" + "secded_enc.h" | |
| with open(c_src_filename, "w") as f: | |
| f.write(COPYRIGHT) | |
| f.write("// SECDED encode code generated by\n") | |
| f.write(f"// util/design/secded_gen.py from {SECDED_CFG_FILE}\n\n") | |
| f.write(C_SRC_TOP) | |
| with open(c_h_filename, "w") as f: | |
| f.write(COPYRIGHT) | |
| f.write("// SECDED encode code generated by\n") | |
| f.write(f"// util/design/secded_gen.py from {SECDED_CFG_FILE}\n") | |
| f.write(C_H_TOP) | |
| for cfg in cfgs['cfgs']: | |
| log.debug("Working on {}".format(cfg)) | |
| k = cfg['k'] | |
| m = cfg['m'] | |
| n = k + m | |
| codetype = cfg['code_type'] | |
| suffix = CODE_OPTIONS[codetype] | |
| codes = gen_code(codetype, k, m) | |
| # write out rtl files | |
| write_enc_dec_files(n, k, m, codes, suffix, args.outdir, codetype) | |
| # write out C files, only hsiao codes are supported | |
| if codetype in ["hsiao", "inv_hsiao"]: | |
| write_c_files(n, k, m, codes, suffix, c_src_filename, c_h_filename, | |
| codetype) | |
| # write out all-zero word values for all codes | |
| pkg_type_str += print_pkg_allzero(n, k, m, codes, suffix, codetype) | |
| # write out package typedefs | |
| pkg_type_str += print_pkg_types(n, k, m, codes, suffix, codetype) | |
| # print out functions | |
| pkg_out_str += print_fn(n, k, m, codes, suffix, codetype) | |
| if not args.no_fpv: | |
| write_fpv_files(n, k, m, codes, suffix, args.fpv_outdir, codetype) | |
| with open(c_h_filename, "a") as f: | |
| f.write(C_H_FOOT) | |
| format_c_files(c_src_filename, c_h_filename) | |
| # create enum of various ECC types - useful for DV purposes in mem_bkdr_if | |
| enum_str = print_secded_enum_and_util_fns(cfgs['cfgs']) | |
| # write out package file | |
| full_pkg_str = enum_str + pkg_type_str + pkg_out_str | |
| write_pkg_file(args.outdir, full_pkg_str) | |
| def _inv_hsiao_code(k, m): | |
| return _hsiao_code(k, m) | |
| # k = data bits | |
| # m = parity bits | |
| # generate hsiao code | |
| def _hsiao_code(k, m): | |
| # using itertools combinations, generate odd number of 1 in a row | |
| required_row = k # k rows are needed, decreasing everytime when it acquite | |
| fanin_ideal = ideal_fanin(k, m) | |
| log.info("Ideal Fan-In value: %d" % fanin_ideal) | |
| # Each entry represents a row in below parity matrix | |
| # Entry is tuple and the value inside is the position of ones | |
| # e.g. (0,1,2) in m:=7 | |
| # row -> [1 1 1 0 0 0 0] | |
| codes = [] | |
| # Find code matrix ======================================================= | |
| # This is main part to find the parity matrix. | |
| # For example, find SECDED for 4bit message is to find 4x4 matrix as below | |
| # | 1 0 0 0 x x x x | | |
| # | 0 1 0 0 x x x x | | |
| # | 0 0 1 0 x x x x | | |
| # | 0 0 0 1 x x x x | | |
| # Then message _k_ X matrix_code ==> original message with parity | |
| # | |
| # Make a row to have even number of 1 including the I matrix. | |
| # This helps to calculate the syndrom at the decoding stage. | |
| # To reduce the max fan-in, Starting with smallest number 3. | |
| # the number means the number of one in a row. | |
| # Small number of ones means smaller fan-in overall. | |
| for step in range(3, m + 1, 2): | |
| # starting from 3 as I matrix represents data | |
| # Increased by 2 as number of 1 should be even in a row (odd excluding I) | |
| # get the list of combinations [0, .., m-1] with `step` | |
| # e.g. step := 3 ==> [(0,1,2), (0,1,3), ... ] | |
| candidate = list(itertools.combinations(range(m), step)) | |
| if len(candidate) <= required_row: | |
| # we need more round use all of them | |
| codes.extend(candidate) | |
| required_row -= len(candidate) | |
| else: | |
| # Find optimized fan-in ========================================== | |
| # Calculate each row fan-in with current | |
| fanins = calc_fanin(m, codes) | |
| while required_row != 0: | |
| # Let's shuffle | |
| # Shuffling makes the sequence randomized --> it reduces the | |
| # fanin as the code takes randomly at the end of the round | |
| # TODO: There should be a clever way to find the subset without | |
| # random retrying. | |
| # Suggested this algorithm | |
| # https://en.wikipedia.org/wiki/Assignment_problem | |
| random.shuffle(candidate) | |
| # Take a subset | |
| subset = candidate[0:required_row] | |
| subset_fanins = calc_fanin(m, subset) | |
| # Check if it exceeds Ideal Fan-In | |
| ideal = True | |
| for i in range(m): | |
| if fanins[i] + subset_fanins[i] > fanin_ideal: | |
| # Exceeded. Retry | |
| ideal = False | |
| break | |
| if ideal: | |
| required_row = 0 | |
| # Append to the code matrix | |
| codes.extend(subset) | |
| if required_row == 0: | |
| # Found everything! | |
| break | |
| log.info("Hsiao codes {}".format(codes)) | |
| return codes | |
| def _inv_hamming_code(k, m): | |
| return _hamming_code(k, m) | |
| # Generate hamming code. | |
| # total_cnt = number of total bits | |
| # data_cnt = number of data bits | |
| # parity_cnt = number of parity bits | |
| @functools.lru_cache(maxsize=None) | |
| def _hamming_code(data_cnt, parity_cnt): | |
| # construct a list of code tuples. | |
| # Tuple corresponds to each bit position and shows which parity bit it participates in | |
| # Only the data bits are shown, the parity bits are not. | |
| total_cnt = data_cnt + parity_cnt | |
| last_parity = parity_cnt - 1 | |
| # Data bits. | |
| # This excludes bit 0, the last bit, and any bit whose index is a power of 2. | |
| data_bits = [b for b in range(1, total_cnt) if b & (b - 1)] | |
| # Include the closest previous parity bit if it's odd. | |
| # Also include the final parity bit since it includes everything. | |
| codes = [ | |
| tuple(p for p in range(last_parity) if (b >> p) & 1) + (last_parity,) for b in data_bits | |
| ] | |
| # Final parity bit includes all ECC bits. | |
| codes += [(last_parity,)] * last_parity | |
| log.info("Hamming codes %s", codes) | |
| return codes | |
| def write_pkg_file(outdir, pkg_str): | |
| with open(outdir + "/" + "prim_secded_pkg.sv", "w") as f: | |
| outstr = '''{}// SECDED package generated by | |
| // util/design/secded_gen.py from {} | |
| package prim_secded_pkg; | |
| {} | |
| endpackage | |
| '''.format(COPYRIGHT, SECDED_CFG_FILE, pkg_str) | |
| f.write(outstr) | |
| def bytes_to_c_type(num_bytes): | |
| if num_bytes == 1: | |
| return 'uint8_t' | |
| elif num_bytes <= 2: | |
| return 'uint16_t' | |
| elif num_bytes <= 4: | |
| return 'uint32_t' | |
| elif num_bytes <= 8: | |
| return 'uint64_t' | |
| return None | |
| def write_c_files(n, k, m, codes, suffix, c_src_filename, c_h_filename, | |
| codetype): | |
| in_bytes = math.ceil(k / 8) | |
| out_bytes = math.ceil(m / 8) | |
| if (k > 64): | |
| log.warning(f"Cannot generate C encoder for k = {k}." | |
| " The tool has no support for k > 64 for C encoder " | |
| "generation") | |
| return | |
| in_type = bytes_to_c_type(in_bytes) | |
| out_type = bytes_to_c_type(out_bytes) | |
| assert in_type | |
| assert out_type | |
| assert codetype in ["hsiao", "inv_hsiao"] | |
| invert = (codetype == "inv_hsiao") | |
| with open(c_src_filename, "a") as f: | |
| # Write out function prototype in src | |
| f.write(f"\n{out_type} enc_secded{suffix}_{n}_{k}" | |
| f"(const uint8_t bytes[{in_bytes}]) {{\n") | |
| # Form a single word from the incoming byte data | |
| f.write(f"{in_type} word = ") | |
| f.write(" | ".join( | |
| [f"(({in_type})bytes[{i}] << {i*8})" for i in range(in_bytes)])) | |
| f.write(";\n\n") | |
| # AND the word with the codes, calculating parity of each and combine | |
| # into a single word of integrity bits | |
| f.write("return ") | |
| parity_bit_masks = enumerate(calc_bitmasks(k, m, codes, False)) | |
| # Add ECC bit inversion if needed (see print_enc function). | |
| f.write(" | ".join( | |
| [f"(calc_parity(word & 0x{mask:x}, " | |
| f"{'true' if invert and (par_bit % 2) else 'false'}) << {par_bit})" | |
| for par_bit, mask in parity_bit_masks])) | |
| f.write(";\n}\n") | |
| with open(c_h_filename, "a") as f: | |
| # Write out function declaration in header | |
| f.write(f"{out_type} enc_secded{suffix}_{n}_{k}" | |
| f"(const uint8_t bytes[{in_bytes}]);\n") | |
| def format_c_files(c_src_filename, c_h_filename): | |
| try: | |
| # Call clang-format to in-place format generated C code. If there are | |
| # any issues log a warning. | |
| result = subprocess.run(['./ci/bazelisk.sh', 'run', '//quality:clang_format_fix', '--', | |
| c_src_filename, c_h_filename], stderr=subprocess.PIPE, | |
| universal_newlines=True) | |
| result.check_returncode() | |
| except Exception as e: | |
| stderr = '' | |
| if result: | |
| stderr = '\n' + result.stderr | |
| log.warning(f"Could not format generated C source: {e}{stderr}") | |
| def write_enc_dec_files(n, k, m, codes, suffix, outdir, codetype): | |
| enc_out = print_enc(n, k, m, codes, codetype) | |
| module_name = "prim_secded%s_%d_%d" % (suffix, n, k) | |
| with open(outdir + "/" + module_name + "_enc.sv", "w") as f: | |
| outstr = '''{}// SECDED encoder generated by util/design/secded_gen.py | |
| module {}_enc ( | |
| input [{}:0] data_i, | |
| output logic [{}:0] data_o | |
| ); | |
| always_comb begin : p_encode | |
| {} end | |
| endmodule : {}_enc | |
| '''.format(COPYRIGHT, module_name, (k - 1), (n - 1), enc_out, module_name) | |
| f.write(outstr) | |
| dec_out = print_dec(n, k, m, codes, codetype) | |
| with open(outdir + "/" + module_name + "_dec.sv", "w") as f: | |
| outstr = '''{}// SECDED decoder generated by util/design/secded_gen.py | |
| module {}_dec ( | |
| input [{}:0] data_i, | |
| output logic [{}:0] data_o, | |
| output logic [{}:0] syndrome_o, | |
| output logic [1:0] err_o | |
| ); | |
| always_comb begin : p_encode | |
| {} end | |
| endmodule : {}_dec | |
| '''.format(COPYRIGHT, module_name, (n - 1), (k - 1), (m - 1), | |
| dec_out, module_name) | |
| f.write(outstr) | |
| def write_fpv_files(n, k, m, codes, suffix, outdir, codetype): | |
| module_name = "prim_secded%s_%d_%d" % (suffix, n, k) | |
| with open(outdir + "/tb/" + module_name + "_tb.sv", "w") as f: | |
| outstr = '''{}// SECDED FPV testbench generated by util/design/secded_gen.py | |
| module {}_tb ( | |
| input clk_i, | |
| input rst_ni, | |
| input [{}:0] data_i, | |
| output logic [{}:0] data_o, | |
| output logic [{}:0] encoded_o, | |
| output logic [{}:0] syndrome_o, | |
| output logic [1:0] err_o, | |
| input [{}:0] error_inject_i | |
| ); | |
| {}_enc {}_enc ( | |
| .data_i, | |
| .data_o(encoded_o) | |
| ); | |
| {}_dec {}_dec ( | |
| .data_i(encoded_o ^ error_inject_i), | |
| .data_o, | |
| .syndrome_o, | |
| .err_o | |
| ); | |
| endmodule : {}_tb | |
| '''.format(COPYRIGHT, module_name, (k - 1), (k - 1), (n - 1), (m - 1), (n - 1), | |
| module_name, module_name, module_name, module_name, module_name) | |
| f.write(outstr) | |
| # Additional assertions for inverted codes. | |
| if codetype in ["inv_hsiao", "inv_hamming"]: | |
| inv_asserts = ''' | |
| // Check that all-one and all-zero data does not result in all-one or all-zero codewords | |
| `ASSERT(AllZerosCheck_A, data_i == '0 |-> encoded_o != '0) | |
| `ASSERT(AllOnesCheck_A, data_i == '1 |-> encoded_o != '1) | |
| ''' | |
| else: | |
| inv_asserts = "" | |
| with open(outdir + "/vip/" + module_name + "_assert_fpv.sv", "w") as f: | |
| outstr = '''{}// SECDED FPV assertion file generated by util/design/secded_gen.py | |
| module {}_assert_fpv ( | |
| input clk_i, | |
| input rst_ni, | |
| input [{}:0] data_i, | |
| input [{}:0] data_o, | |
| input [{}:0] encoded_o, | |
| input [{}:0] syndrome_o, | |
| input [1:0] err_o, | |
| input [{}:0] error_inject_i | |
| ); | |
| // Inject a maximum of two errors simultaneously. | |
| `ASSUME_FPV(MaxTwoErrors_M, $countones(error_inject_i) <= 2) | |
| // Single bit error detection | |
| `ASSERT(SingleErrorDetect_A, $countones(error_inject_i) == 1 |-> err_o[0]) | |
| `ASSERT(SingleErrorDetectReverse_A, err_o[0] |-> $countones(error_inject_i) == 1) | |
| // Double bit error detection | |
| `ASSERT(DoubleErrorDetect_A, $countones(error_inject_i) == 2 |-> err_o[1]) | |
| `ASSERT(DoubleErrorDetectReverse_A, err_o[1] |-> $countones(error_inject_i) == 2) | |
| // Single bit error correction (implicitly tests the syndrome output) | |
| `ASSERT(SingleErrorCorrect_A, $countones(error_inject_i) < 2 |-> data_i == data_o) | |
| // Basic syndrome check | |
| `ASSERT(SyndromeCheck_A, |syndrome_o |-> $countones(error_inject_i) > 0) | |
| `ASSERT(SyndromeCheckReverse_A, $countones(error_inject_i) > 0 |-> |syndrome_o) | |
| {} | |
| endmodule : {}_assert_fpv | |
| '''.format(COPYRIGHT, module_name, (k - 1), (k - 1), (n - 1), (m - 1), (n - 1), | |
| inv_asserts, module_name) | |
| f.write(outstr) | |
| with open(outdir + "/tb/" + module_name + "_bind_fpv.sv", "w") as f: | |
| outstr = '''{}// SECDED FPV bind file generated by util/design/secded_gen.py | |
| module {}_bind_fpv; | |
| bind {}_tb | |
| {}_assert_fpv {}_assert_fpv ( | |
| .clk_i, | |
| .rst_ni, | |
| .data_i, | |
| .data_o, | |
| .encoded_o, | |
| .syndrome_o, | |
| .err_o, | |
| .error_inject_i | |
| ); | |
| endmodule : {}_bind_fpv | |
| '''.format(COPYRIGHT, module_name, module_name, module_name, module_name, | |
| module_name) | |
| f.write(outstr) | |
| with open(outdir + "/" + module_name + "_fpv.core", "w") as f: | |
| outstr = '''CAPI=2: | |
| # Copyright lowRISC contributors. | |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. | |
| # SPDX-License-Identifier: Apache-2.0 | |
| name: "lowrisc:fpv:{}_fpv:0.1" | |
| description: "SECDED FPV target" | |
| filesets: | |
| files_formal: | |
| depend: | |
| - lowrisc:prim:all | |
| - lowrisc:prim:secded | |
| files: | |
| - vip/{}_assert_fpv.sv | |
| - tb/{}_tb.sv | |
| - tb/{}_bind_fpv.sv | |
| file_type: systemVerilogSource | |
| targets: | |
| default: &default_target | |
| # note, this setting is just used | |
| # to generate a file list for jg | |
| default_tool: icarus | |
| filesets: | |
| - files_formal | |
| toplevel: | |
| - {}_tb | |
| formal: | |
| <<: *default_target | |
| lint: | |
| <<: *default_target | |
| '''.format(module_name, module_name, module_name, module_name, module_name) | |
| f.write(outstr) | |
| def load_secded_config() -> Dict[str, Any]: | |
| return hjson.load(SECDED_CFG_PATH.open()) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| prog="secded_gen", | |
| description='''This tool generates Single Error Correction Double Error | |
| Detection(SECDED) encoder and decoder modules in SystemVerilog. | |
| ''') | |
| parser.add_argument('--no_fpv', | |
| action='store_true', | |
| help='Do not generate FPV testbench.') | |
| parser.add_argument('--outdir', | |
| default='hw/ip/prim/rtl/', | |
| help=''' | |
| Output directory. The output file will be named | |
| `prim_secded_<n>_<k>_enc/dec.sv` (default: %(default)s) | |
| ''') | |
| parser.add_argument('--fpv_outdir', | |
| default='hw/ip/prim/fpv/', | |
| help=''' | |
| FPV output directory. The output files will have | |
| the base name `prim_secded_<n>_<k>_*_fpv` (default: %(default)s) | |
| ''') | |
| parser.add_argument('--c_outdir', | |
| default='hw/ip/prim/dv/prim_secded', | |
| help=''' | |
| C output directory. The output files are named secded_enc.c and | |
| secded_enc.h | |
| ''') | |
| parser.add_argument('--verbose', '-v', action='store_true', help='Verbose') | |
| args = parser.parse_args() | |
| if (args.verbose): | |
| log.basicConfig(format="%(levelname)s: %(message)s", level=log.DEBUG) | |
| else: | |
| log.basicConfig(format="%(levelname)s: %(message)s") | |
| with open(SECDED_CFG_FILE, 'r') as infile: | |
| config = hjson.load(infile) | |
| # Error checking | |
| error = verify(config) | |
| if (error): | |
| exit(1) | |
| # Generate outputs | |
| generate(config, args) | |
| if __name__ == "__main__": | |
| main() |