blob: 33a6841c456b531f51d17df3d9ebf9b2f4e74831 [file] [log] [blame]
#!/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"""This script generates sparse FSM encodings that fulfill a minimum
Hamming distance requirement. It uses a heuristic that incrementally
draws random state encodings until a solution has been found.
Depending on the parameterization, the script may not find a solution right
away. In such cases, the script should be rerun after tweaking the d/m/n
parameters. E.g. in order to increase the chances for success, the state
space can be made more sparse by increasing n, or the Hamming distance
threshold d can be lowered.
Note however that the Hamming distance d should be set to 3 at minimum.
It is recommended to set this value to 4-5 for security critical FSMs.
The custom seed s can be used to make subsequent runs of the script
deterministic. If not specified, the script randomly picks a seed.
"""
import argparse
import logging as log
import math
import random
import sys
from lib.common import get_hd, hd_histogram, wrapped_docstring
MAX_DRAWS = 10000
MAX_RESTARTS = 10000
SV_INSTRUCTIONS = """
------------------------------------------------------
| COPY PASTE THE CODE TEMPLATE BELOW INTO YOUR RTL |
| IMPLEMENTATION, INLUDING THE COMMENT AND PRIM_FLOP |
| IN ORDER TO EASE AUDITABILITY AND REPRODUCIBILITY. |
------------------------------------------------------
"""
C_INSTRUCTIONS = """
------------------------------------------------
| COPY PASTE THE CODE TEMPLATE BELOW INTO YOUR |
| C HEADER, INLUDING THE COMMENT IN ORDER TO |
| EASE AUDITABILITY AND REPRODUCIBILITY. |
------------------------------------------------
"""
RUST_INSTRUCTIONS = """
------------------------------------------------
| COPY PASTE THE CODE TEMPLATE BELOW INTO YOUR |
| RUST FILE, INLUDING THE COMMENT IN ORDER TO |
| EASE AUDITABILITY AND REPRODUCIBILITY. |
------------------------------------------------
"""
def main():
log.basicConfig(level=log.INFO,
format="%(levelname)s: %(message)s")
parser = argparse.ArgumentParser(
prog="sparse-fsm-encode",
description=wrapped_docstring(),
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'-d',
type=int,
default=5,
metavar='<minimum HD>',
help='Minimum Hamming distance between encoded states.')
parser.add_argument('-m',
type=int,
default=7,
metavar='<#states>',
help='Number of states to encode.')
parser.add_argument('-n',
type=int,
default=10,
metavar='<#nbits>',
help='Encoding length [bit].')
parser.add_argument('-s',
type=int,
metavar='<seed>',
help='Custom seed for RNG.')
parser.add_argument('--language',
choices=['sv', 'c', 'rust'],
default='sv',
help='Choose the language of the generated enum.')
parser.add_argument('--avoid-zero',
action='store_true',
help=('Also enforce a minimum hamming '
'distance from the zero word.'))
args = parser.parse_args()
if args.language in ['c', 'rust']:
if args.n not in [8, 16, 32]:
log.error(
"When using C or Rust, widths must be a power-of-two "
"at least a byte (8 bits) wide. You chose %d." % (args.n, ))
sys.exit(1)
if args.m < 2:
log.error(
'Number of states %d must be at least 2.' % (args.m))
sys.exit(1)
if args.m > 2**args.n:
log.error(
'Statespace 2^%d not large enough to accommodate %d states.' %
(args.n, args.m))
sys.exit(1)
if (args.d >= args.n) and not(args.d == args.n and args.m == 2):
log.error(
'State is only %d bits wide, which is not enough to fulfill a '
'minimum Hamming distance constraint of %d. ' % (args.n, args.d))
sys.exit(1)
if args.d <= 0:
log.error('Hamming distance must be > 0.')
sys.exit(1)
if args.d < 3:
log.warning(
'A value of 4-5 is recommended for the minimum Hamming distance '
'constraint. At a minimum, this should be set to 3.')
# If no seed has been provided, we choose a seed and print it
# into the generated output later on such that this run can be
# reproduced.
if args.s is None:
random.seed()
args.s = random.getrandbits(32)
random.seed(args.s)
# This is a heuristic that opportunistically draws random
# state encodings and check whether they fulfill the minimum
# Hamming distance constraint.
# Other solutions that use a brute-force approach would be
# possible as well (see e.g. https://math.stackexchange.com/
# questions/891528/generating-a-binary-code-with-maximized-hamming-distance).
# However, due to the sparse nature of the state space, this
# probabilistic heuristic works pretty well for most practical
# cases, and it scales favorably to large N.
num_draws = 0
num_restarts = 0
rnd = random.getrandbits(args.n)
encodings = []
min_popcnt = args.d if args.avoid_zero else 1
while len(encodings) < args.m:
# if we iterate for too long, start over.
if num_draws >= MAX_DRAWS:
num_draws = 0
num_restarts += 1
rnd = random.getrandbits(args.n)
encodings = []
# if we restarted for too many times, abort.
if num_restarts >= MAX_RESTARTS:
log.error(
'Did not find a solution after restarting {} times. This is '
'an indicator that not many (or even no) solutions exist for '
'the current parameterization. Rerun the script and/or adjust '
'the d/m/n parameters. E.g. make the state space more sparse by '
'increasing n, or lower the minimum Hamming distance threshold d.'
.format(num_restarts))
sys.exit(1)
num_draws += 1
# draw a candidate and check whether it fulfills the minimum
# distance requirement with respect to other encodings.
rnd = random.getrandbits(args.n)
cand = format(rnd, '0' + str(args.n) + 'b')
# disallow all-zero and all-one states
pop_cnt = cand.count('1')
if pop_cnt < args.n and pop_cnt >= min_popcnt:
for k in encodings:
# disallow candidates that are the complement of other states
if int(cand, 2) == ~int(k, 2):
break
# disallow candidates that are too close to other states
if get_hd(cand, k) < args.d:
break
else:
encodings.append(cand)
# Get Hamming distance statistics.
stats = hd_histogram(encodings)
if args.language == "sv":
print(SV_INSTRUCTIONS)
print("// Encoding generated with:\n"
"// $ ./util/design/sparse-fsm-encode.py -d {} -m {} -n {} \\\n"
"// -s {} --language=sv{}\n"
"//\n"
"// Hamming distance histogram:\n"
"//".format(args.d, args.m, args.n, args.s,
' --avoid-zero' if args.avoid_zero else ''))
for bar in stats['bars']:
print('// ' + bar)
print("//\n"
"// Minimum Hamming distance: {}\n"
"// Maximum Hamming distance: {}\n"
"// Minimum Hamming weight: {}\n"
"// Maximum Hamming weight: {}\n"
"//\n"
"localparam int StateWidth = {};\n"
"typedef enum logic [StateWidth-1:0] {{".format(
stats['min_hd'], stats['max_hd'], stats['min_hw'],
stats['max_hw'], args.n))
fmt_str = " State{} {}= {}'b{}"
state_str = ""
for j, k in enumerate(encodings):
pad = ""
for i in range(len(str(args.m)) - len(str(j))):
pad += " "
comma = "," if j < len(encodings) - 1 else ""
print(fmt_str.format(j, pad, args.n, k) + comma)
state_str += " State{}: ;\n".format(j)
# print FSM template
print('''}} state_e;
state_e state_d, state_q;
always_comb begin : p_fsm
// Default assignments
state_d = state_q;
unique case (state_q)
{} default: ; // Consider triggering an error or alert in this case.
endcase
end
`PRIM_FLOP_SPARSE_FSM(u_state_regs, state_d, state_q, state_e, State0)
'''.format(state_str))
elif args.language == "c":
print(C_INSTRUCTIONS)
print("/*\n"
" * Encoding generated with\n"
" * $ ./util/design/sparse-fsm-encode.py -d {} -m {} -n {} \\\n"
" * -s {} --language=c\n"
" *\n"
" * Hamming distance histogram:\n"
" *".format(args.d, args.m, args.n, args.s))
for hist_bar in stats['bars']:
print(" * " + hist_bar)
print(" *\n"
" * Minimum Hamming distance: {}\n"
" * Maximum Hamming distance: {}\n"
" * Minimum Hamming weight: {}\n"
" * Maximum Hamming weight: {}\n"
" */\n"
"typedef enum my_state {{".format(stats['min_hd'],
stats['max_hd'],
stats['min_hw'],
stats['max_hw']))
fmt_str = " kMyState{0:} {1:}= 0x{3:0" + str(math.ceil(
args.n / 4)) + "x}"
for j, k in enumerate(encodings):
pad = ""
for i in range(len(str(args.m)) - len(str(j))):
pad += " "
print(fmt_str.format(j, pad, args.n, int(k, 2)) + ",")
# print FSM template
print("} my_state_t;")
elif args.language == 'rust':
print(RUST_INSTRUCTIONS)
print("///```text\n"
"/// Encoding generated with\n"
"/// $ ./util/design/sparse-fsm-encode.py -d {} -m {} -n {} \\\n"
"/// -s {} --language=rust\n"
"///\n"
"/// Hamming distance histogram:\n"
"///".format(args.d, args.m, args.n, args.s))
for hist_bar in stats['bars']:
print("/// " + hist_bar)
print("///\n"
"/// Minimum Hamming distance: {}\n"
"/// Maximum Hamming distance: {}\n"
"/// Minimum Hamming weight: {}\n"
"/// Maximum Hamming weight: {}\n"
"///```\n"
"#[derive(Clone,Copy,Eq,PartialEq,Ord,ParitalOrd,Hash,Debug)]\n"
"#[repr(transparent)]\n"
"struct MyState(u{});\n"
"\n"
"impl MyState {{".format(stats['min_hd'], stats['max_hd'],
stats['min_hw'], stats['max_hw'],
args.n))
fmt_str = " const MY_STATE{0:}: MyState {1:}= MyState(0x{3:0" + str(
math.ceil(args.n / 4)) + "x})"
for j, k in enumerate(encodings):
pad = ""
for i in range(len(str(args.m)) - len(str(j))):
pad += " "
print(fmt_str.format(j, pad, args.n, int(k, 2)) + ";")
print("}")
if __name__ == "__main__":
main()