blob: dd3ba2ec29fe3527e9a4ecc23abfdea0ebe08592 [file] [log] [blame]
"""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)