ICE40 HX1K Hardware Model#
Todo
This documentation was drafted from code analysis and needs verification against hardware datasheets.
This page documents how the BitstreamEvolution framework models and interacts
with the Lattice ICE40 HX1K FPGA. The primary implementation lives in
FileBasedCircuit (see src/Circuit/FileBasedCircuit.py),
which reads, mutates, compiles, and uploads bitstream configurations to physical
hardware through the IceStorm open-source toolchain.
ICE40 HX1K Overview#
The ICE40 HX1K is a low-power FPGA from the Lattice iCE40 family. It contains 1,280 logic cells arranged in a tile grid. The BitstreamEvolution project uses this FPGA as the target for evolved digital circuits: genetic algorithm individuals are represented as bitstream configurations that are uploaded to the device and evaluated for fitness based on the resulting circuit behavior (e.g., oscillation counting or waveform analysis).
The IceStorm reverse-engineered toolchain provides full open-source support for
the ICE40 family, including an ASCII bitstream representation (.asc files)
that the framework manipulates directly.
ASC File Format#
The IceStorm .asc (ASCII) format is a human-readable representation of the
FPGA’s configuration bitstream. FileBasedCircuit operates on these files
using memory-mapped I/O.
Tile Headers#
Each logic tile in the file is introduced by a header line of the form:
.logic_tile X Y
where X and Y are the integer coordinates of the tile on the FPGA grid.
The framework locates tiles by scanning for the byte string b".logic_tile"
and parsing the coordinates that follow.
Bit Rows#
After each .logic_tile header, subsequent lines contain rows of
space-delimited 0 and 1 characters representing the configuration bits
for that tile. Because the file is accessed through a memory-mapped buffer, bit
values are encountered as their ASCII byte codes:
Character |
ASCII Code (byte value) |
Logical Value |
|---|---|---|
|
48 |
|
|
49 |
|
The get_bitstream and set_bitstream methods in FileBasedCircuit
convert between these ASCII byte values and Python bool lists.
File Attributes (Metadata)#
The .asc format supports comment lines, which the framework repurposes for
metadata storage. A special comment line stores name-value pairs:
.comment FILE_ATTRIBUTES fitness={42.5} generation={100}
Attributes are encoded as name={value} pairs on a single .comment
FILE_ATTRIBUTES line. The get_file_attribute and set_file_attribute
methods on FileBasedCircuit provide read/write access to these attributes.
If the comment line does not yet exist, set_file_attribute prepends one to
the file.
Tile Grid Structure#
Todo
Verify tile coordinate ranges against hardware datasheets. The ranges below
are hard-coded in FileBasedCircuit.__tile_is_included and are specific to
the ICE40 HX1K. A different iCE40 model (e.g., HX8K) would require
different ranges.
Only a subset of the full tile grid is available for evolutionary modification. The valid tile coordinates are defined as:
VALID_TILE_X = range(4, 10) # X in {4, 5, 6, 7, 8, 9}
VALID_TILE_Y = range(1, 17) # Y in {1, 2, ..., 16}
Tiles outside these ranges are skipped during mutation and bitstream
extraction. The __tile_is_included method parses the X Y coordinates
from the .asc header following each .logic_tile tag and checks
membership in these ranges.
The method handles multi-digit coordinate values by locating the space between
X and Y and the newline at the end of the header, then decoding the
byte slices as UTF-8 strings before converting to integers.
Routing Types#
Todo
Verify routing row selections against hardware datasheets. The row selections below are noted in the source as “dated” and may not reflect the latest routing protocol.
Within each valid tile, only specific rows and columns of configuration bits are subject to evolutionary modification. The set of modifiable rows is determined by the routing type, configured per circuit at construction time.
MOORE#
When routing_type == "MOORE", the modifiable rows within each tile are:
rows = [1, 2, 13]
This routing type exposes three rows for mutation, including row 13 which provides access to additional routing resources.
NEWSE#
When routing_type == "NEWSE", the modifiable rows are:
rows = [1, 2]
This is a more restricted routing type, limiting modification to only the first two rows of each tile.
Accessed Columns#
In addition to the routing-type-determined rows, the specific columns available
for modification are supplied via the accessed_columns parameter at circuit
construction time. The framework iterates over every combination of modifiable
row and accessed column to reach individual bits:
pos = line_start + line_size * (row - 1) + int(col)
where line_start is the byte offset of the first data line in the tile, and
line_size is the width of each line (including the trailing newline).
Compilation Flow#
The path from an evolved .asc file to a running FPGA configuration follows
these steps:
.asc file --> icepack --> .bin file --> iceprog --> FPGA
Flush mmap – Before compilation, the memory-mapped file is flushed to ensure all in-memory mutations are written to disk:
self._hardware_file.flush()
icepack – The IceStorm
icepackutility converts the ASCII.ascfile into a binary.binbitstream:COMPILE_CMD = "icepack" run([COMPILE_CMD, self.__hardware_filepath, self.__bitstream_filepath])
iceprog – The
iceprogutility uploads the compiled.binfile to the target FPGA over USB. The-dflag selects a specific device when multiple FPGAs are connected:RUN_CMD = "iceprog" run([RUN_CMD, self.__bitstream_filepath, "-d", fpga.id])
Settling delay – A one-second
sleep(1)follows upload to allow the FPGA to initialize before fitness evaluation begins.
Memory-Mapped I/O#
FileBasedCircuit uses Python’s mmap module to memory-map the
.asc hardware file. This avoids repeated file open/read/write/close cycles
during the many small, random-access mutations that occur across an
evolutionary run.
The memory-mapped file is created during __init__:
hardware_file = open(self.__hardware_filepath, "r+")
self._hardware_file = mmap(hardware_file.fileno(), 0)
hardware_file.close()
Key considerations:
Read/write access – The file is opened in
"r+"mode so the mmap supports both reading bit values and writing mutations back in place.Flushing before compile –
self._hardware_file.flush()is called beforeicepackruns to ensure the on-disk file reflects all pending changes.Windows compatibility – The
copy_frommethod closes the mmap before overwriting the underlying file withshutil.copyfile, then re-opens and re-maps it. This is required because Windows locks files that have active memory mappings.Byte-level access – Individual bits are accessed by index (
hardware_file[pos]), returning integer byte values (48 or 49) that correspond to ASCII'0'and'1'.