diff --git a/flow/scripts/memories/gen_memories_test.py b/flow/scripts/memories/gen_memories_test.py index ffd02593d9..8c7080bf89 100644 --- a/flow/scripts/memories/gen_memories_test.py +++ b/flow/scripts/memories/gen_memories_test.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import json +import re import sys import tempfile import unittest @@ -134,6 +135,36 @@ def test_additional_memories_forces_conversion(self): self.assertTrue(tags["idiomatic"]) self.assertIn("forced", tags["reason"]) + def test_deep_memory_is_folded_square(self): + # 512x17 with one bit line per column is 2.3 x 166.6 um, 73:1, a + # sliver macro placement cannot fit; the column mux folds it. The + # fold squares the bit-cell counts, and a bit cell is not square, + # so the bound leaves room for that. + netlist = json.loads(json.dumps(SAMPLE_YOSYS_NETLIST)) + mem = netlist["modules"].pop("\\mem_128x32") + mem["cells"]["\\$mem_0"]["parameters"].update(SIZE=512, WIDTH=17) + netlist["modules"] = {"\\mem_512x17": mem} + d = Path(self.tmp.name) + (d / "netlist.json").write_text(json.dumps(netlist)) + code = gen_memories.main( + [ + "--platform", + "asap7", + "--out-dir", + str(d / "memories"), + "--json", + str(d / "memories.json"), + "--yosys-json", + str(d / "netlist.json"), + ] + ) + self.assertEqual(code, 0) + lef = (d / "memories" / "mem_512x17.lef").read_text() + size = re.search(r"SIZE\s+(\S+)\s+BY\s+(\S+)", lef) + self.assertIsNotNone(size, "no SIZE in the generated LEF") + width, height = map(float, size.groups()) + self.assertLessEqual(max(width, height) / min(width, height), 8.0) + def test_unsupported_platform_fails_clearly(self): code, _d = self.run_generator(platform="nangate45") self.assertEqual(code, 1) diff --git a/tools/FakeRAM2.0/orfs_asap7/generate.py b/tools/FakeRAM2.0/orfs_asap7/generate.py index be30f8f927..66811e1f24 100644 --- a/tools/FakeRAM2.0/orfs_asap7/generate.py +++ b/tools/FakeRAM2.0/orfs_asap7/generate.py @@ -20,7 +20,6 @@ "manufacturing_grid_nm": 1, "contacted_poly_pitch_nm": 54, "fin_pitch_nm": 27, - "column_mux_factor": 1, "snap_width_nm": 190, "snap_height_nm": 1400, } diff --git a/tools/FakeRAM2.0/test/macro_dim_test.py b/tools/FakeRAM2.0/test/macro_dim_test.py index 849fc04516..37820e936a 100755 --- a/tools/FakeRAM2.0/test/macro_dim_test.py +++ b/tools/FakeRAM2.0/test/macro_dim_test.py @@ -109,6 +109,39 @@ def test_macro_dim_bitcell_override(self): self.assertAlmostEqual(width, exp_width, delta=self._delta) self.assertAlmostEqual(height, exp_height, delta=self._delta) + def _dimensions(self, column_mux_factor, width, depth, banks): + """Macro dimensions, column_mux_factor None leaves the key out""" + + process_data = TestUtils.get_base_process_data().copy() + del process_data["column_mux_factor"] + if column_mux_factor is not None: + process_data["column_mux_factor"] = column_mux_factor + return Process(process_data).get_macro_dimensions(width, depth, banks, 0) + + def test_macro_dim_column_mux_picked(self): + """An absent column_mux_factor folds a deep memory square""" + + # sqrt(512 / 17) = 5.5, the nearest power of two is 4 + picked = self._dimensions(None, 17, 512, 1) + self.assertEqual(picked, self._dimensions(4, 17, 512, 1)) + width, height = picked + self.assertLessEqual(max(width, height) / min(width, height), 8.0) + + def test_macro_dim_column_mux_explicit(self): + """An explicit column_mux_factor is kept, even when it is a sliver""" + + width, height = self._dimensions(1, 17, 512, 1) + self.assertGreater(height / width, 50.0) + + def test_macro_dim_column_mux_after_banks(self): + """The factor is picked for the array banking leaves""" + + # 4 banks make 2048x39 a 512x156 array: sqrt(512 / 156) = 1.8, + # so 2, where sqrt(2048 / 39) = 7.2 would have given 8 + self.assertEqual( + self._dimensions(None, 39, 2048, 4), self._dimensions(2, 39, 2048, 4) + ) + if __name__ == "__main__": unittest.main() diff --git a/tools/FakeRAM2.0/test/process_test.py b/tools/FakeRAM2.0/test/process_test.py index e61757e564..779dc36516 100755 --- a/tools/FakeRAM2.0/test/process_test.py +++ b/tools/FakeRAM2.0/test/process_test.py @@ -114,6 +114,14 @@ def test_process_optional_snap(self): + (process.get_pin_width_um() / 2.0), ) + def test_process_optional_column_mux(self): + """Tests that column_mux_factor is left to be picked per memory""" + + process_data = self._base_data.copy() + del process_data["column_mux_factor"] + process = Process(process_data) + self.assertIsNone(process.get_column_mux_factor()) + def test_process_misaligned_pin_mfg_grid_pitch(self): """ Tests detection that pin pitch is not a multiple of the metal track diff --git a/tools/FakeRAM2.0/utils/class_process.py b/tools/FakeRAM2.0/utils/class_process.py index 487a853e6a..58fd503fb3 100644 --- a/tools/FakeRAM2.0/utils/class_process.py +++ b/tools/FakeRAM2.0/utils/class_process.py @@ -8,6 +8,32 @@ ################################################################################ +import math + + +def pick_column_mux_factor(rows, bits): + """Fold a tall array into a square one. + + The array is rows/factor cells tall and bits*factor cells wide, so + the factor that squares it is sqrt(rows/bits). Rounded to a power of + two because a column mux selects one of 2^n bit lines, and clamped + at 1 so a wide, shallow memory is left alone. + + Area does not change: this moves cells between the two dimensions + rather than adding or removing any. + """ + if rows <= 0 or bits <= 0: + return 1 + ideal = math.sqrt(float(rows) / float(bits)) + if ideal <= 1.0: + return 1 + lo = 2 ** math.floor(math.log2(ideal)) + hi = lo * 2 + # Nearest in log space, which is what "closest to square" means when + # the candidates are powers of two. + return lo if (ideal / lo) <= (hi / ideal) else hi + + class Process: def __init__(self, json_data): """Initialize from json_data imported from config file""" @@ -23,9 +49,14 @@ def __init__(self, json_data): self.contacted_poly_pitch_nm = json_data.get("contacted_poly_pitch_nm", None) self.fin_pitch_nm = json_data.get("fin_pitch_nm", None) self.manufacturing_grid_nm = int(json_data["manufacturing_grid_nm"]) - self.column_mux_factor = int(json_data["column_mux_factor"]) # Optional keys + # Absent means get_macro_dimensions picks one per memory + self.column_mux_factor = ( + int(json_data["column_mux_factor"]) + if "column_mux_factor" in json_data + else None + ) self.snap_width_nm = ( int(json_data["snap_width_nm"]) if "snap_width_nm" in json_data else 1 ) @@ -99,7 +130,6 @@ def get_macro_dimensions(self, width_in_bits, depth, num_banks, additional_heigh and process parameters """ - column_mux_factor = self.column_mux_factor bitcell_width, bitcell_height = self.get_bitcell_dimensions() all_bitcell_height = bitcell_height * depth @@ -111,6 +141,11 @@ def get_macro_dimensions(self, width_in_bits, depth, num_banks, additional_heigh elif num_banks != 1: raise Exception("Unsupported number of banks: {}".format(num_banks)) + column_mux_factor = self.column_mux_factor + if column_mux_factor is None: + column_mux_factor = pick_column_mux_factor( + depth / num_banks, width_in_bits * num_banks + ) all_bitcell_height = all_bitcell_height / column_mux_factor all_bitcell_width = all_bitcell_width * column_mux_factor @@ -202,7 +237,7 @@ def get_y_step(self): return self.y_step def get_column_mux_factor(self): - """Returns the column mux factor""" + """Returns the column mux factor, None if picked per memory""" return self.column_mux_factor def set_calc_dimensions(self, val):