[prim/secded] Find optimal Fan-In

The SECDED generator is revised to find the optimal fan-in logic.
Current one uses random shuffle to find the parity matrix that meets the
ideal fan-in value.

Later, it would be useful to find the deterministically such as choosing
the row that doesn't violate the ideal fan-in one-by-one.

Signed-off-by: Eunchan Kim <eunchan@opentitan.org>
diff --git a/hw/ip/prim/util/secded_gen.py b/hw/ip/prim/util/secded_gen.py
index 872cad9..a86f709 100755
--- a/hw/ip/prim/util/secded_gen.py
+++ b/hw/ip/prim/util/secded_gen.py
@@ -8,6 +8,8 @@
 of Hamming code + parity). Please refer https://arxiv.org/pdf/0803.1217.pdf
 """
 
+# TODO: Add FPV assertions in the encoder/decoder module
+
 import argparse
 import itertools
 import logging as log
@@ -51,23 +53,60 @@
     return fanin
 
 
-def print_comb(n, k, m, cur_m, codes, start_cnt):
+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 print_comb(n,
+               k,
+               m,
+               cur_m,
+               codes,
+               start_cnt,
+               max_width=100,
+               prefix="",
+               first_indent=0):
+    """Print XOR comb.
+
+    @param[max_width]    Maximum Width of str
+    @param[prefix]       The prepend string at the first line
+    @param[first_indent] The number of character that indented at the first line
+        e.g. first_indent := 2
+            {prefix}in[nn] ...
+                  ^ in[nn] ^ in[nn]
+
+    result:
+        {prefix}in[nn] ^ ... in[nn]
+                ^ in[nn] ^ ... in[nn];
+    """
     outstr = ""
+    line = prefix
+    prepend_len = len(prefix)
     cnt = start_cnt
     first = True
     for j in range(k):
-        if cnt == 7:
-            cnt = 0
-            outstr += "\n"
-            outstr += "                 "
-
+        temp_str = ""
         if cur_m in codes[j]:
             if not first:
-                outstr += " ^"
+                temp_str += " ^"
             if first:
                 first = False
-            cnt += 1
-            outstr += " in[%d]" % (j)
+            temp_str += " in[%d]" % (j)
+            temp_len = len(temp_str)
+
+            if len(line) + temp_len > max_width:
+                outstr += line + "\n"
+                line = ' ' * (prepend_len - first_indent) + temp_str
+            else:
+                line += temp_str
+    outstr += line + ";\n"
     return outstr
 
 
@@ -78,9 +117,8 @@
 
     for i in range(m):
         # Print parity computation
-        outstr += "  assign out[%d] =" % (i + k)
-        outstr += print_comb(n, k, m, i, codes, 0)
-        outstr += " ;\n"
+        outstr += print_comb(n, k, m, i, codes, 0, 100,
+                             "  assign out[%d] =" % (i + k), 2)
     return outstr
 
 
@@ -94,11 +132,10 @@
     outstr += "\n"
     outstr += "  // Syndrome calculation\n"
     for i in range(m):
-        outstr += "  assign syndrome_o[%d] = in[%d] ^ " % (i, k + i)
-
         # Print combination
-        outstr += print_comb(n, k, m, i, codes, 1)
-        outstr += " ;\n"
+        outstr += print_comb(n, k, m, i, codes, 1, 100,
+                             "  assign syndrome_o[%d] = in[%d] ^" % (i, k + i),
+                             len(" in[%d] ^" % (k + i)) + 2)
 
     outstr += "\n"
     outstr += "  // Corrected output calculation\n"
@@ -177,28 +214,72 @@
     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 3x4 matrix as below
+    # | 1 0 0 0 x x x |
+    # | 0 1 0 0 x x x |
+    # | 0 0 1 0 x x x |
+    # | 0 0 0 1 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
+        # 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))
 
-        # Let's shuffle
-        random.shuffle(candidate)
-
         if len(candidate) <= required_row:
             # we need more round use all of them
             codes.extend(candidate)
             required_row -= len(candidate)
         else:
-            # we can completed in this round
-            # but search lowest fan-in codes
-            # at this time, just pick lowest
-            codes.extend(candidate[0:required_row])
-            required_row = 0
+            ## 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!