| """Provide utilites for converting between FASM and icebox representations. | |
| FASM represents enabling features from a default "empty" configuration. | |
| IceBox documents what each bit does even if should be set by default (unused). | |
| Therefore any differences between ice40 parts are hidden from the FASM | |
| portion. For example IE, REN, and Ram PowerUp are inverted for 1k parts comparted to | |
| all other families. | |
| FASM features attempt to maintain the IceStrom nomenclature. | |
| Exceptions: | |
| - '/' are replaced with '_' as '/' is not valid for FASM feature name | |
| - RAM CBITS are combined into READ_MODE and WRITE_MODE FASM features | |
| - IO configuration bits are combined into SimpleInput and SimpleOut FASM features | |
| """ | |
| from io import StringIO | |
| import re | |
| import unittest | |
| import numpy as np | |
| import icebox | |
| import fasm | |
| from ice40_feature import Feature, IceDbEntry, FasmEntry | |
| # hardcoded permutation | |
| # * https://github.com/YosysHQ/nextpnr/blob/343569105ddf7c97316922774dc4d70d1d4f7c9f/ice40/bitstream.cc#L460-L468 | |
| # * https://github.com/cliffordwolf/icestorm/blob/master/icebox/icebox_hlc2asc.py#L925-L936 | |
| # * https://github.com/YosysHQ/arachne-pnr/blob/840bdfdeb38809f9f6af4d89dd7b22959b176fdd/src/place.cc#L1573-L1627 | |
| LUT_BITS = [4, 14, 15, 5, 6, 16, 17, 7, 3, 13, 12, 2, 1, 11, 10, 0] | |
| LUT_CTRL = { | |
| "CarryEnable": 8, | |
| "DffEnable": 9, | |
| "Set_NoReset": 18, | |
| "AsyncSetReset": 19 | |
| } | |
| def _nibbles_to_bits(line): | |
| """Convert from icebox hex string for ramdata in asc files to an array of Bool""" | |
| res = [] | |
| for ch in line: | |
| res += [xx == "1" for xx in "{:4b}".format(int(ch, 16))] | |
| res.reverse() | |
| return res | |
| def _bits_to_nibbles(arr): | |
| """Convert from array of Bool to icebox hex string used for ramdata in asc files""" | |
| res = [] | |
| for ii in range(0, len(arr), 4): | |
| nibble_val = sum( | |
| (1 << ii) for ii, xx in enumerate(arr[ii:ii + 4]) if xx | |
| ) | |
| res += "{:x}".format(nibble_val) | |
| res.reverse() | |
| return "".join(res) | |
| def _tile_to_array(tile, is_hex=False): | |
| """Convert text icedb tile to a numpy array""" | |
| if is_hex: | |
| array = np.array([_nibbles_to_bits(line) for line in tile], dtype=bool) | |
| else: | |
| array = np.array( | |
| [[int(xx) for xx in line] for line in tile], dtype=bool | |
| ) | |
| return array | |
| def _array_to_tile(tile_bits, tile, is_hex=False): | |
| """Convert a numpy array to text icedb tile | |
| Modifies tile in place to simplify updating of iceconfig | |
| """ | |
| if is_hex: | |
| for ii, xx in enumerate(tile_bits): | |
| tile[ii] = _bits_to_nibbles(xx) | |
| else: | |
| for ii, xx in enumerate(tile_bits): | |
| tile[ii] = tile_bits.shape[1] * "%d" % tuple(xx) | |
| def _lut_to_lc(lut, ctrl): | |
| """Convert lut and ctrl dict to permuted lc bits""" | |
| res = 20 * [lut[0]] | |
| for ii, bit in enumerate(LUT_BITS): | |
| res[bit] = lut[ii] | |
| for k, v in ctrl.items(): | |
| res[LUT_CTRL[k]] = v | |
| return res | |
| def _lc_to_lut(lc): | |
| """Convert from lc bits to unpermuted lut table and control dict""" | |
| lut = [lc[val] for val in LUT_BITS] | |
| ctrl = {k: lc[v] for k, v in LUT_CTRL.items()} | |
| return lut, ctrl | |
| def _get_feature_bits(tile, cond): | |
| """Get bitmask and values for the tile from an icebox db entry""" | |
| bm = np.zeros(tile.shape, dtype=bool) | |
| bp = np.zeros(tile.shape, dtype=bool) | |
| for ii in cond: | |
| neg, x, y = ii | |
| bm[x][y] = True | |
| bp[x][y] = neg != "!" | |
| return bm, bp | |
| def _set_feature_bits(ic, loc, bits): | |
| """Set bits for a specifc location in an iceconfig""" | |
| tile = ic.tile(*loc) | |
| tile_bits = _tile_to_array(tile) | |
| bm, bv = _get_feature_bits(tile_bits, bits) | |
| tile_bits[bm] = bv[bm] | |
| _array_to_tile(tile_bits, tile) | |
| def _get_iceconfig_bits(tile, cond): | |
| """Get bitmask and values for the tile from an icebox db entry""" | |
| bm = np.zeros(tile.shape, dtype=bool) | |
| bp = np.zeros(tile.shape, dtype=bool) | |
| for ii in cond: | |
| mm = re.match(r"([\!]?)B([0-9]+)\[([0-9]*)\]", ii) | |
| neg = mm.group(1) | |
| inds = [int(mm.group(ii)) for ii in range(2, 4)] | |
| bm[inds[0]][inds[1]] = 1 | |
| bp[inds[0]][inds[1]] = neg != "!" | |
| return bm, bp | |
| def _check_iceconfig_entry(tile, entry): | |
| """Check an entry is set for a tile""" | |
| bm, bp = _get_iceconfig_bits(tile, entry[0]) | |
| return np.all(bp[bm] == tile[bm]) | |
| def _inv_bit_tuple(bit_tuple): | |
| if bit_tuple[0] == "": | |
| neg = "!" | |
| else: | |
| neg = "" | |
| return tuple(neg) + bit_tuple[1:] | |
| class FeatureAccumulator(dict): | |
| """Class to help accumulate iCE40 features for output as FASM file or a FASM DB""" | |
| def append_feature(self, feature): | |
| self[feature.to_fasm_entry().feature] = feature | |
| def append_ice_entry( | |
| self, tile_type, tile_loc, bits, names, idx, negate=False | |
| ): | |
| if negate: | |
| feature = Feature.from_icedb_entry( | |
| IceDbEntry(tile_type, tile_loc, bits, names, idx) | |
| ) | |
| feature.bit_tuples = [ | |
| _inv_bit_tuple(bt) for bt in feature.bit_tuples | |
| ] | |
| else: | |
| feature = Feature.from_icedb_entry( | |
| IceDbEntry(tile_type, tile_loc, bits, names, idx) | |
| ) | |
| self.append_feature(feature) | |
| return feature | |
| def as_fasm_db(self, outf=StringIO()): | |
| for key in sorted(self.keys()): | |
| entry = self[key].to_fasm_entry() | |
| print( | |
| "{} {}".format(entry.feature, " ".join(entry.bits)), file=outf | |
| ) | |
| return outf | |
| def as_fasm(self, outf=StringIO()): | |
| for key in sorted(self.keys()): | |
| print(key, file=outf) | |
| return outf | |
| def read_ice_db(ic): | |
| """Read icebox database from iceconfig and construct a dictionary of features""" | |
| # TODO: undo Hack to invert some specific signals | |
| device_1k = ic.device == "1k" | |
| accum = FeatureAccumulator() | |
| locs = [(x, y) for x in range(ic.max_x + 1) for y in range(ic.max_y + 1)] | |
| for tile_loc in locs: | |
| if ic.tile(*tile_loc) is None: | |
| continue | |
| tile_type = ic.tile_type(*tile_loc) | |
| for entry in ic.tile_db(*tile_loc): | |
| if entry[1].startswith("LC_"): | |
| lut, ctrl = _lc_to_lut(entry[0]) | |
| for ii, bit in enumerate(lut): | |
| names = entry[1:] + ["INIT"] | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, [bit], names, ii | |
| ) | |
| for name, bit in ctrl.items(): | |
| names = entry[1:] + [name] | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, [bit], names, None | |
| ) | |
| # entries to generate the negated case | |
| elif ( | |
| tile_type == "IO" and device_1k and | |
| (entry[-1].startswith("IE_") or entry[-1].startswith("REN_"))): | |
| accum.append_ice_entry( | |
| tile_type, | |
| tile_loc, | |
| entry[0], | |
| entry[1:], | |
| None, | |
| negate=True | |
| ) | |
| elif device_1k and tile_type == "RAMB" and entry[-1] == "PowerUp": | |
| accum.append_ice_entry( | |
| tile_type, | |
| tile_loc, | |
| entry[0], | |
| entry[1:], | |
| None, | |
| negate=True | |
| ) | |
| elif tile_type == "RAMT" and entry[-1].startswith("CBIT"): | |
| matches = re.match(r"CBIT_([0-9]+)", entry[-1]) | |
| assert matches is not None, "Expected 'CBIT_n' received {}".format( | |
| entry[-1] | |
| ) | |
| cbit_offset = matches.group(1) | |
| cbit_translation = { | |
| "0": ("WRITE_MODE", 0), | |
| "1": ("WRITE_MODE", 1), | |
| "2": ("READ_MODE", 0), | |
| "3": ("READ_MODE", 1), | |
| } | |
| val = cbit_translation.get(cbit_offset) | |
| if val is not None: | |
| ramb_tile_loc = (tile_loc[0], tile_loc[1] - 1) | |
| accum.append_ice_entry( | |
| "RAMB", ramb_tile_loc, entry[0], [val[0]], val[1] | |
| ) | |
| else: | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, entry[0], entry[1:], None | |
| ) | |
| else: | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, entry[0], entry[1:], None | |
| ) | |
| # add RAM data entries features | |
| tile_type = "RAMB" | |
| for ram_loc in ic.ramb_tiles: | |
| for i in range(16): | |
| feature_name = "INIT{:X}".format(i) | |
| for j in range(256): | |
| bit = "B{}[{}]".format(i, j) | |
| accum.append_ice_entry( | |
| tile_type, ram_loc, [bit], [feature_name], j | |
| ) | |
| # TODO: extra bits? | |
| return accum | |
| def _iceboxdb_to_fasmdb(ic, outf=StringIO()): | |
| """Read in an icebox config and output the exhaustive list of bits with fasm compatible names | |
| expand RAM and LUT INIT bits | |
| """ | |
| fasmdb = read_ice_db(ic) | |
| return fasmdb.as_fasm_db(outf) | |
| def generate_fasm_db(outf, device): | |
| """Generate FASM style DB for a ice40 device""" | |
| ic = icebox.iceconfig() | |
| init_method_name = "setup_empty_{}".format(device.lower()[2:]) | |
| assert hasattr( | |
| ic, init_method_name | |
| ), "no icebox method to init empty device" | |
| getattr(ic, init_method_name)() | |
| return _iceboxdb_to_fasmdb(ic, outf) | |
| def asc_to_fasm(filename, outf=StringIO()): | |
| """Generate a fasm output from an asc""" | |
| ic = icebox.iceconfig() | |
| ic.read_file(filename) | |
| return iceconfig_to_fasm(ic, outf) | |
| def iceconfig_to_fasm(ic, outf=StringIO()): | |
| """Read iceconfig and generate FASM features. | |
| Useful for generating FASM from ASC file. | |
| """ | |
| # TODO: undo Hack to invert some specific signals | |
| device_1k = ic.device == "1k" | |
| accum = FeatureAccumulator() | |
| locs = [(x, y) for x in range(ic.max_x + 1) for y in range(ic.max_y + 1)] | |
| for tile_loc in locs: | |
| bits = ic.tile(*tile_loc) | |
| if bits is None: | |
| continue | |
| tile_bits = _tile_to_array(bits) | |
| tile_type = ic.tile_type(*tile_loc) | |
| for entry in ic.tile_db(*tile_loc): | |
| # LC_ entries are treated differently as it's not a match, but a pattern | |
| if entry[1].startswith("LC_"): | |
| bm, _ = _get_iceconfig_bits(tile_bits, entry[0]) | |
| bits = tile_bits[bm] | |
| if np.any(bits): | |
| lut, ctrl = _lc_to_lut(bits) | |
| for ii, bit in enumerate(lut): | |
| if bit: | |
| names = entry[1:] + ["INIT"] | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, [], names, ii | |
| ) | |
| for name, bit in ctrl.items(): | |
| if bit: | |
| names = entry[1:] + [name] | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, [], names, None | |
| ) | |
| elif ( | |
| tile_type == "IO" and device_1k and | |
| (entry[-1].startswith("IE_") or entry[-1].startswith("REN_"))): | |
| accum.append_ice_entry( | |
| tile_type, | |
| tile_loc, | |
| entry[0], | |
| entry[1:], | |
| None, | |
| negate=True | |
| ) | |
| elif device_1k and tile_type == "RAMB" and entry[-1] == "PowerUp": | |
| accum.append_ice_entry( | |
| tile_type, | |
| tile_loc, | |
| entry[0], | |
| entry[1:], | |
| None, | |
| negate=True | |
| ) | |
| elif tile_type == "RAMT" and entry[-1].startswith("CBIT"): | |
| matches = re.match(r"CBIT_([0-9]+)", entry[-1]) | |
| assert matches is not None, "Expected 'CBIT_n' received {}".format( | |
| entry[-1] | |
| ) | |
| cbit_offset = matches.group(1) | |
| cbit_translation = { | |
| "0": ("WRITE_MODE", 0), | |
| "1": ("WRITE_MODE", 1), | |
| "2": ("READ_MODE", 0), | |
| "3": ("READ_MODE", 1), | |
| } | |
| val = cbit_translation.get(cbit_offset) | |
| if val is not None: | |
| ramb_tile_loc = (tile_loc[0], tile_loc[1] - 1) | |
| accum.append_ice_entry( | |
| "RAMB", ramb_tile_loc, entry[0], [val[0]], val[1] | |
| ) | |
| else: | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, entry[0], entry[1:], None | |
| ) | |
| else: | |
| if _check_iceconfig_entry(tile_bits, entry): | |
| accum.append_ice_entry( | |
| tile_type, tile_loc, entry[0], entry[1:], None | |
| ) | |
| # ram data | |
| for tile_loc, hexd in ic.ram_data.items(): | |
| tile_bits = _tile_to_array(hexd, is_hex=True) | |
| tile_type = "RAMB" | |
| for x, y in zip(*np.where(tile_bits)): | |
| feature_name = "INIT{:X}".format(x) | |
| bit = "B{}[{}]".format(x, y) | |
| entry = [[bit], feature_name] | |
| feature = Feature.from_icedb_entry( | |
| IceDbEntry(tile_type, tile_loc, entry[0], entry[1:], y) | |
| ) | |
| print(feature.to_fasm_entry().feature, file=outf) | |
| # TODO: extra_bits | |
| return accum.as_fasm(outf) | |
| def fasm_to_asc(in_fasm, outf, device): | |
| """Convert an FASM input to an ASC file | |
| Set input enable defaults, RAM powerup, and enables all ColBufCtrl (until modeled in VPR see: #464) | |
| """ | |
| ic = icebox.iceconfig() | |
| init_method_name = "setup_empty_{}".format(device.lower()[2:]) | |
| assert hasattr( | |
| ic, init_method_name | |
| ), "no icebox method to init empty device" | |
| getattr(ic, init_method_name)() | |
| device_1k = ic.device == "1k" | |
| fasmdb = read_ice_db(ic) | |
| # TODO: upstream init "default" bitstream | |
| locs = [(x, y) for x in range(ic.max_x + 1) for y in range(ic.max_y + 1)] | |
| for tile_loc in locs: | |
| tile = ic.tile(*tile_loc) | |
| if tile is None: | |
| continue | |
| db = ic.tile_db(*tile_loc) | |
| tile_type = ic.tile_type(*tile_loc) | |
| for entry in db: | |
| if (device_1k and | |
| ((tile_type == "IO" and entry[-1] in ["IE_0", "IE_1"]) or | |
| (tile_type == "RAMB" and entry[-1] == "PowerUp")) | |
| or (entry[-2] == "ColBufCtrl")): | |
| tile_bits = _tile_to_array(tile) | |
| tile_type = ic.tile_type(*tile_loc) | |
| bm, bv = _get_iceconfig_bits(tile_bits, entry[0]) | |
| tile_bits[bm] = bv[bm] | |
| _array_to_tile(tile_bits, tile) | |
| missing_features = [] | |
| for line in fasm.parse_fasm_string(in_fasm.read()): | |
| if not line.set_feature: | |
| continue | |
| line_strs = tuple(fasm.fasm_line_to_string(line)) | |
| assert len(line_strs) == 1 | |
| feature = Feature.from_fasm_entry( | |
| FasmEntry(line.set_feature.feature, []) | |
| ) | |
| def find_ieren(ic, loc, iob): | |
| tmap = [ | |
| xx[3:] | |
| for xx in ic.ieren_db() | |
| if xx[:3] == (tuple(loc) + (iob, )) | |
| ] | |
| assert len(tmap) < 2, "expected 1 IEREN_DB entry found {}".format( | |
| len(tmap) | |
| ) | |
| if len(tmap) == 0: | |
| print("no ieren found for {}".format((tile_loc + (iob, )))) | |
| return | |
| return tmap[0] | |
| # fix up for IO | |
| if feature.parts[-1] == "SimpleInput": | |
| iob = int(feature.parts[-2][-1]) | |
| new_ieren = find_ieren(ic, feature.loc, iob) | |
| feature.parts[-1] = "PINTYPE_0" | |
| db_entry = fasmdb[feature.to_fasm_entry().feature] | |
| _set_feature_bits(ic, db_entry.loc, db_entry.bit_tuples) | |
| feature.loc = new_ieren[:2] | |
| feature.parts[-2] = "IoCtrl" | |
| feature.parts[-1] = "IE_{}".format(new_ieren[2]) | |
| db_entry = fasmdb[feature.to_fasm_entry().feature] | |
| _set_feature_bits(ic, db_entry.loc, db_entry.bit_tuples) | |
| feature.parts[-1] = "REN_{}".format(new_ieren[2]) | |
| db_entry = fasmdb[feature.to_fasm_entry().feature] | |
| _set_feature_bits(ic, db_entry.loc, db_entry.bit_tuples) | |
| continue | |
| if feature.parts[-1] == "SimpleOutput": | |
| iob = int(feature.parts[-2][-1]) | |
| new_ieren = find_ieren(ic, feature.loc, iob) | |
| feature.parts[-1] = "PINTYPE_3" | |
| db_entry = fasmdb[feature.to_fasm_entry().feature] | |
| _set_feature_bits(ic, db_entry.loc, db_entry.bit_tuples) | |
| feature.parts[-1] = "PINTYPE_4" | |
| db_entry = fasmdb[feature.to_fasm_entry().feature] | |
| _set_feature_bits(ic, db_entry.loc, db_entry.bit_tuples) | |
| feature.loc = new_ieren[:2] | |
| feature.parts[-2] = "IoCtrl" | |
| feature.parts[-1] = "REN_{}".format(new_ieren[2]) | |
| db_entry = fasmdb[feature.to_fasm_entry().feature] | |
| _set_feature_bits(ic, db_entry.loc, db_entry.bit_tuples) | |
| continue | |
| ## special case for RAM INIT values | |
| tile_type, loc = feature.tile_type, feature.loc | |
| if tile_type == "RAMB" and feature.parts[-1].startswith("INIT"): | |
| tloc = tuple(loc) | |
| if tloc not in ic.ram_data: | |
| ic.ram_data[tloc] = [64 * "0" for _ in range(16)] | |
| tile = ic.ram_data[tloc] | |
| tile_bits = _tile_to_array(tile, is_hex=True) | |
| elif tile_type == "RAMB" and feature.parts[-1].endswith("_MODE"): | |
| # hack to force modes to the RAMT | |
| tile = ic.tile(loc[0], loc[1] + 1) | |
| tile_bits = _tile_to_array(tile) | |
| else: | |
| tile = ic.tile(*loc) | |
| tile_bits = _tile_to_array(tile) | |
| # lookup feature and convert | |
| for canonical_feature in fasm.canonical_features(line.set_feature): | |
| key = fasm.set_feature_to_str(canonical_feature) | |
| feature = fasmdb[key] | |
| bm, bv = _get_feature_bits(tile_bits, feature.bit_tuples) | |
| tile_bits[bm] = bv[bm] | |
| if tile_type == "RAMB" and feature.parts[-1].startswith("INIT"): | |
| _array_to_tile(tile_bits, tile, is_hex=True) | |
| else: | |
| _array_to_tile(tile_bits, tile) | |
| # TODO: would be nice to upstream a way to write to non-files | |
| ic.write_file(outf.name) | |
| if __name__ == "__main__": | |
| import argparse | |
| import sys | |
| parser = argparse.ArgumentParser(description="Dump FASM DB from icebox") | |
| parser.add_argument("device", help="Device type (eg lp1k, hx8k)") | |
| parser.add_argument( | |
| "--output", | |
| help="Output file", | |
| type=argparse.FileType("w"), | |
| default=sys.stdout | |
| ) | |
| args = parser.parse_args() | |
| generate_fasm_db(args.output, args.device) |