From c31d71bf238c58f37a28a484468d34a9753bc81c Mon Sep 17 00:00:00 2001 From: Dan Petrisko Date: Mon, 23 Nov 2020 16:43:48 -0800 Subject: [PATCH 01/45] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d6b948ef..a63d00eb 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ The default for openram.py is specified in the configuration file. # Porting to a New Technology -If you want to support a enw technology, you will need to create: +If you want to support a new technology, you will need to create: + a setup script for each technology you want to use + a technology directory for each technology with the base cells From f428ff4bfd01e719a6ff9caeebf7f45678c3cb77 Mon Sep 17 00:00:00 2001 From: mrg Date: Thu, 7 Jan 2021 10:33:21 -0800 Subject: [PATCH 02/45] v1.1.14 --- compiler/globals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/globals.py b/compiler/globals.py index 00f1a2da..7ec3ed15 100644 --- a/compiler/globals.py +++ b/compiler/globals.py @@ -19,7 +19,7 @@ import re import copy import importlib -VERSION = "1.1.13" +VERSION = "1.1.14" NAME = "OpenRAM v{}".format(VERSION) USAGE = "openram.py [options] \nUse -h for help.\n" From 671470f5f2d249547f4ce80c54b93dc8ed6104ca Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 8 Mar 2021 14:40:36 -0800 Subject: [PATCH 03/45] Skywater changes. Default 1 thread and no temp subdirectory. Add skywater setup/hold golden data Add CLI option for simulation threads (-m) Add compatibility mode option and nomodcheck for ngspice to speed up sky130 model loading. Make subdir when using default /tmp dir. Pass num_threads so temp subdirs are created. --- .github/workflows/ci.yml | 4 +-- compiler/characterizer/charutils.py | 2 ++ compiler/characterizer/stimuli.py | 2 ++ compiler/debug.py | 15 +++++++++- ...1r.py => riscv_scn4m_subm_2kbyte_1rw1r.py} | 0 compiler/globals.py | 29 ++++++++++++++++--- compiler/modules/bank.py | 3 ++ compiler/modules/hierarchical_predecode.py | 19 +++++++----- compiler/modules/hierarchical_predecode2x4.py | 4 +-- compiler/modules/hierarchical_predecode3x8.py | 4 +-- .../modules/hierarchical_predecode4x16.py | 4 +-- compiler/options.py | 2 +- compiler/tests/21_hspice_setuphold_test.py | 10 ++++--- compiler/tests/21_ngspice_setuphold_test.py | 10 ++++--- compiler/tests/30_openram_back_end_test.py | 3 ++ compiler/tests/30_openram_front_end_test.py | 3 ++ compiler/tests/regress.py | 3 +- compiler/tests/testutils.py | 10 +++---- 18 files changed, 91 insertions(+), 36 deletions(-) rename compiler/example_configs/{riscv_scn4m_subm_2skbyte_1rw1r.py => riscv_scn4m_subm_2kbyte_1rw1r.py} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 262e65ca..d50b93d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: scn4me_subm Archives - path: ${{ github.workspace }}/scn4me_subm_temp/*/* + path: $OPENRAM_HOME/*.zip freepdk45: runs-on: self-hosted steps: @@ -38,7 +38,7 @@ jobs: uses: actions/upload-artifact@v2 with: name: FreePDK45 Archives - path: ${{ github.workspace }}/freepdk45_temp/*/* + path: $OPENRAM_HOME/*.zip # coverage_stats: # if: ${{ always() }} # needs: [scn4me_subm, freepdk45] diff --git a/compiler/characterizer/charutils.py b/compiler/characterizer/charutils.py index b618f0db..8f33c279 100644 --- a/compiler/characterizer/charutils.py +++ b/compiler/characterizer/charutils.py @@ -31,6 +31,8 @@ def parse_spice_list(filename, key): f = open(full_filename, "r") except IOError: debug.error("Unable to open spice output file: {0}".format(full_filename),1) + debug.archive() + contents = f.read() f.close() # val = re.search(r"{0}\s*=\s*(-?\d+.?\d*\S*)\s+.*".format(key), contents) diff --git a/compiler/characterizer/stimuli.py b/compiler/characterizer/stimuli.py index 4eee9e5c..06b6058d 100644 --- a/compiler/characterizer/stimuli.py +++ b/compiler/characterizer/stimuli.py @@ -360,6 +360,8 @@ class stimuli(): # -r {2}timing.raw ng_cfg = open("{}.spiceinit".format(OPTS.openram_temp), "w") ng_cfg.write("set num_threads={}\n".format(OPTS.num_sim_threads)) + ng_cfg.write("set ngbehavior=hsa\n") + ng_cfg.write("set ng_nomodcheck\n") ng_cfg.close() cmd = "{0} -b -o {2}timing.lis {1}".format(OPTS.spice_exe, diff --git a/compiler/debug.py b/compiler/debug.py index f14b1224..f8a4807d 100644 --- a/compiler/debug.py +++ b/compiler/debug.py @@ -43,7 +43,7 @@ def error(str, return_value=0): if globals.OPTS.debug: pdb.set_trace() - + assert return_value == 0 @@ -108,7 +108,20 @@ def info(lev, str): print_raw("[{0}/{1}]: {2}".format(class_name, frm[0].f_code.co_name, str)) + +def archive(): + from globals import OPTS + try: + OPENRAM_HOME = os.path.abspath(os.environ.get("OPENRAM_HOME")) + except: + error("$OPENRAM_HOME is not properly defined.", 1) + import shutil + zip_file = "{0}/{1}_{2}".format(OPENRAM_HOME, "fail_", os.getpid()) + info(0, "Archiving failed files to {}.zip".format(zip_file)) + shutil.make_archive(zip_file, 'zip', OPTS.openram_temp) + + def bp(): """ An empty function so you can set soft breakpoints in pdb. diff --git a/compiler/example_configs/riscv_scn4m_subm_2skbyte_1rw1r.py b/compiler/example_configs/riscv_scn4m_subm_2kbyte_1rw1r.py similarity index 100% rename from compiler/example_configs/riscv_scn4m_subm_2skbyte_1rw1r.py rename to compiler/example_configs/riscv_scn4m_subm_2kbyte_1rw1r.py diff --git a/compiler/globals.py b/compiler/globals.py index b977e8e3..1a4bd5c5 100644 --- a/compiler/globals.py +++ b/compiler/globals.py @@ -61,8 +61,13 @@ def parse_args(): optparse.make_option("-j", "--threads", action="store", type="int", - help="Specify the number of threads (default: 2)", + help="Specify the number of threads (default: 1)", dest="num_threads"), + optparse.make_option("-m", "--sim_threads", + action="store", + type="int", + help="Specify the number of spice simulation threads (default: 2)", + dest="num_sim_threads"), optparse.make_option("-v", "--verbose", action="count", @@ -381,6 +386,10 @@ def purge_temp(): """ Remove the temp directory. """ debug.info(1, "Purging temp directory: {}".format(OPTS.openram_temp)) + #import inspect + #s = inspect.stack() + #print("Purge {0} in dir {1}".format(s[3].filename, OPTS.openram_temp)) + # This annoyingly means you have to re-cd into # the directory each debug iteration # shutil.rmtree(OPTS.openram_temp, ignore_errors=True) @@ -429,9 +438,15 @@ def setup_paths(): if "__pycache__" not in full_path: sys.path.append("{0}".format(full_path)) - # Use a unique temp subdirectory - OPTS.openram_temp += "/openram_{0}_{1}_temp/".format(getpass.getuser(), - os.getpid()) + # Use a unique temp subdirectory if multithreaded + if OPTS.num_threads > 1 or OPTS.openram_temp == "/tmp": + + # Make a unique subdir + tempdir = "/openram_{0}_{1}_temp".format(getpass.getuser(), + os.getpid()) + # Only add the unique subdir one time + if tempdir not in OPTS.openram_temp: + OPTS.openram_temp += tempdir if not OPTS.openram_temp.endswith('/'): OPTS.openram_temp += "/" @@ -470,6 +485,12 @@ def init_paths(): except OSError as e: if e.errno == 17: # errno.EEXIST os.chmod(OPTS.openram_temp, 0o750) + #import inspect + #s = inspect.stack() + #from pprint import pprint + #pprint(s) + #print("Test {0} in dir {1}".format(s[2].filename, OPTS.openram_temp)) + # Don't delete the output dir, it may have other files! # make the directory if it doesn't exist diff --git a/compiler/modules/bank.py b/compiler/modules/bank.py index 7f7fb0d4..488439fd 100644 --- a/compiler/modules/bank.py +++ b/compiler/modules/bank.py @@ -526,13 +526,16 @@ class bank(design.design): height=self.dff.height) elif self.col_addr_size == 2: self.column_decoder = factory.create(module_type="hierarchical_predecode2x4", + column_decoder=True, height=self.dff.height) elif self.col_addr_size == 3: self.column_decoder = factory.create(module_type="hierarchical_predecode3x8", + column_decoder=True, height=self.dff.height) elif self.col_addr_size == 4: self.column_decoder = factory.create(module_type="hierarchical_predecode4x16", + column_decoder=True, height=self.dff.height) else: # No error checking before? diff --git a/compiler/modules/hierarchical_predecode.py b/compiler/modules/hierarchical_predecode.py index bd192694..c2ed5949 100644 --- a/compiler/modules/hierarchical_predecode.py +++ b/compiler/modules/hierarchical_predecode.py @@ -18,19 +18,17 @@ class hierarchical_predecode(design.design): """ Pre 2x4 and 3x8 and TBD 4x16 decoder shared code. """ - def __init__(self, name, input_number, height=None): + def __init__(self, name, input_number, column_decoder=False, height=None): self.number_of_inputs = input_number b = factory.create(module_type=OPTS.bitcell) if not height: self.cell_height = b.height - self.column_decoder = False else: self.cell_height = height - # If we are pitch matched to the bitcell, it's a predecoder - # otherwise it's a column decoder (out of pgates) - self.column_decoder = (height != b.height) + + self.column_decoder = column_decoder self.number_of_outputs = int(math.pow(2, self.number_of_inputs)) super().__init__(name) @@ -87,8 +85,15 @@ class hierarchical_predecode(design.design): self.bus_layer = layer_props.hierarchical_predecode.bus_layer self.bus_directions = layer_props.hierarchical_predecode.bus_directions - self.bus_pitch = getattr(self, self.bus_layer + "_pitch") - self.bus_space = layer_props.hierarchical_predecode.bus_space_factor * getattr(self, self.bus_layer + "_space") + + if self.column_decoder: + # Column decoders may be routed on M2/M3 if there's a write mask + self.bus_pitch = self.m3_pitch + self.bus_space = self.m3_space + else: + self.bus_pitch = getattr(self, self.bus_layer + "_pitch") + self.bus_space = getattr(self, self.bus_layer + "_space") + self.bus_space = layer_props.hierarchical_predecode.bus_space_factor * self.bus_space self.input_layer = layer_props.hierarchical_predecode.input_layer self.output_layer = layer_props.hierarchical_predecode.output_layer self.output_layer_pitch = getattr(self, self.output_layer + "_pitch") diff --git a/compiler/modules/hierarchical_predecode2x4.py b/compiler/modules/hierarchical_predecode2x4.py index c212a3c6..941a0756 100644 --- a/compiler/modules/hierarchical_predecode2x4.py +++ b/compiler/modules/hierarchical_predecode2x4.py @@ -13,8 +13,8 @@ class hierarchical_predecode2x4(hierarchical_predecode): """ Pre 2x4 decoder used in hierarchical_decoder. """ - def __init__(self, name, height=None): - super().__init__( name, 2, height) + def __init__(self, name, column_decoder=False, height=None): + super().__init__(name, 2, column_decoder, height) self.create_netlist() if not OPTS.netlist_only: diff --git a/compiler/modules/hierarchical_predecode3x8.py b/compiler/modules/hierarchical_predecode3x8.py index 61c25094..ef70a282 100644 --- a/compiler/modules/hierarchical_predecode3x8.py +++ b/compiler/modules/hierarchical_predecode3x8.py @@ -13,8 +13,8 @@ class hierarchical_predecode3x8(hierarchical_predecode): """ Pre 3x8 decoder used in hierarchical_decoder. """ - def __init__(self, name, height=None): - super().__init__(name, 3, height) + def __init__(self, name, column_decoder=False, height=None): + super().__init__(name, 3, column_decoder, height) self.create_netlist() if not OPTS.netlist_only: diff --git a/compiler/modules/hierarchical_predecode4x16.py b/compiler/modules/hierarchical_predecode4x16.py index fe1426d8..64eef96d 100644 --- a/compiler/modules/hierarchical_predecode4x16.py +++ b/compiler/modules/hierarchical_predecode4x16.py @@ -13,8 +13,8 @@ class hierarchical_predecode4x16(hierarchical_predecode): """ Pre 4x16 decoder used in hierarchical_decoder. """ - def __init__(self, name, height=None): - super().__init__(name, 4, height) + def __init__(self, name, column_decoder=False, height=None): + super().__init__(name, 4, column_decoder, height) self.create_netlist() if not OPTS.netlist_only: diff --git a/compiler/options.py b/compiler/options.py index e3a9a76e..551c3950 100644 --- a/compiler/options.py +++ b/compiler/options.py @@ -133,7 +133,7 @@ class options(optparse.Values): magic_exe = None # Number of threads to use - num_threads = 2 + num_threads = 1 # Number of threads to use in ngspice/hspice num_sim_threads = 2 diff --git a/compiler/tests/21_hspice_setuphold_test.py b/compiler/tests/21_hspice_setuphold_test.py index e97a8aca..634b2982 100755 --- a/compiler/tests/21_hspice_setuphold_test.py +++ b/compiler/tests/21_hspice_setuphold_test.py @@ -12,8 +12,7 @@ import sys, os sys.path.append(os.getenv("OPENRAM_HOME")) import globals from globals import OPTS -from sram_factory import factory -import debug + class timing_setup_test(openram_test): @@ -29,14 +28,12 @@ class timing_setup_test(openram_test): import characterizer reload(characterizer) from characterizer import setup_hold - import sram import tech slews = [tech.spice["rise_time"]*2] corner = (OPTS.process_corners[0], OPTS.supply_voltages[0], OPTS.temperatures[0]) sh = setup_hold(corner) data = sh.analyze(slews,slews) - #print data if OPTS.tech_name == "freepdk45": golden_data = {'hold_times_HL': [-0.0158691], 'hold_times_LH': [-0.0158691], @@ -47,6 +44,11 @@ class timing_setup_test(openram_test): 'hold_times_LH': [-0.11718749999999999], 'setup_times_HL': [0.16357419999999998], 'setup_times_LH': [0.1757812]} + elif OPTS.tech_name == "sky130": + golden_data = {'hold_times_HL': [-0.05615234], + 'hold_times_LH': [-0.03173828], + 'setup_times_HL': [0.078125], + 'setup_times_LH': [0.1025391]} else: self.assertTrue(False) # other techs fail diff --git a/compiler/tests/21_ngspice_setuphold_test.py b/compiler/tests/21_ngspice_setuphold_test.py index da6d07ff..dab02e7d 100755 --- a/compiler/tests/21_ngspice_setuphold_test.py +++ b/compiler/tests/21_ngspice_setuphold_test.py @@ -12,8 +12,7 @@ import sys, os sys.path.append(os.getenv("OPENRAM_HOME")) import globals from globals import OPTS -from sram_factory import factory -import debug + class timing_setup_test(openram_test): @@ -29,14 +28,12 @@ class timing_setup_test(openram_test): import characterizer reload(characterizer) from characterizer import setup_hold - import sram import tech slews = [tech.spice["rise_time"]*2] corner = (OPTS.process_corners[0], OPTS.supply_voltages[0], OPTS.temperatures[0]) sh = setup_hold(corner) data = sh.analyze(slews,slews) - #print data if OPTS.tech_name == "freepdk45": golden_data = {'hold_times_HL': [-0.01586914], 'hold_times_LH': [-0.01586914], @@ -47,6 +44,11 @@ class timing_setup_test(openram_test): 'hold_times_LH': [-0.1293945], 'setup_times_HL': [0.1757812], 'setup_times_LH': [0.1879883]} + elif OPTS.tech_name == "sky130": + golden_data = {'hold_times_HL': [-0.05615234], + 'hold_times_LH': [-0.03173828], + 'setup_times_HL': [0.078125], + 'setup_times_LH': [0.1025391]} else: self.assertTrue(False) # other techs fail diff --git a/compiler/tests/30_openram_back_end_test.py b/compiler/tests/30_openram_back_end_test.py index c2060656..74232db0 100755 --- a/compiler/tests/30_openram_back_end_test.py +++ b/compiler/tests/30_openram_back_end_test.py @@ -49,6 +49,9 @@ class openram_back_end_test(openram_test): if OPTS.tech_name: options += " -t {}".format(OPTS.tech_name) + if OPTS.num_threads: + options += " -j {}".format(OPTS.num_threads) + # Always perform code coverage if OPTS.coverage == 0: debug.warning("Failed to find coverage installation. This can be installed with pip3 install coverage") diff --git a/compiler/tests/30_openram_front_end_test.py b/compiler/tests/30_openram_front_end_test.py index 1c190840..9c134152 100755 --- a/compiler/tests/30_openram_front_end_test.py +++ b/compiler/tests/30_openram_front_end_test.py @@ -49,6 +49,9 @@ class openram_front_end_test(openram_test): if OPTS.tech_name: options += " -t {}".format(OPTS.tech_name) + if OPTS.num_threads: + options += " -j {}".format(OPTS.num_threads) + # Always perform code coverage if OPTS.coverage == 0: debug.warning("Failed to find coverage installation. This can be installed with pip3 install coverage") diff --git a/compiler/tests/regress.py b/compiler/tests/regress.py index 878182a5..ea7e1991 100755 --- a/compiler/tests/regress.py +++ b/compiler/tests/regress.py @@ -13,7 +13,6 @@ import sys, os sys.path.append(os.getenv("OPENRAM_HOME")) import globals from subunit import ProtocolTestCase, TestProtocolClient -from subunit.test_results import AutoTimingTestResultDecorator from testtools import ConcurrentTestSuite (OPTS, args) = globals.parse_args() @@ -71,7 +70,7 @@ def fork_tests(num_threads): stream = os.fdopen(c2pwrite, 'wb', 0) os.close(c2pread) sys.stdin.close() - test_suite_result = AutoTimingTestResultDecorator(TestProtocolClient(stream)) + test_suite_result = TestProtocolClient(stream) test_suite.run(test_suite_result) except EBADF: try: diff --git a/compiler/tests/testutils.py b/compiler/tests/testutils.py index b6468749..4d8bf573 100644 --- a/compiler/tests/testutils.py +++ b/compiler/tests/testutils.py @@ -28,10 +28,9 @@ class openram_test(unittest.TestCase): result=verify.run_drc(w.name, tempgds, None) if result != 0: self.fail("DRC failed: {}".format(w.name)) - - if not OPTS.keep_temp: + elif not OPTS.keep_temp: self.cleanup() - + def local_check(self, a, final_verification=False): self.reset() @@ -74,10 +73,10 @@ class openram_test(unittest.TestCase): # shutil.make_archive(zip_file, 'zip', OPTS.openram_temp) self.fail("LVS mismatch: {}".format(a.name)) + if lvs_result == 0 and drc_result == 0 and not OPTS.keep_temp: + self.cleanup() # For debug... # import pdb; pdb.set_trace() - if not OPTS.keep_temp: - self.cleanup() def run_pex(self, a, output=None): tempspice = "{}.sp".format(a.name) @@ -104,6 +103,7 @@ class openram_test(unittest.TestCase): def cleanup(self): """ Reset the duplicate checker and cleanup files. """ + files = glob.glob(OPTS.openram_temp + '*') for f in files: # Only remove the files From 7b270514e1b528a76bdcf1729383f88e79665b2f Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 22 Mar 2021 15:51:07 -0700 Subject: [PATCH 04/45] Update multithreaded regression. Only do 2 threads for 30 tests. Don't archive results since they are purged anyways. 16 threads for regression. Purge temp during regression. --- .github/workflows/ci.yml | 28 ++++++++++----------- compiler/tests/30_openram_back_end_test.py | 3 +-- compiler/tests/30_openram_front_end_test.py | 3 +-- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d50b93d2..a95db618 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,13 +13,13 @@ jobs: export OPENRAM_TECH="`pwd`/technology:/software/PDKs/skywater-tech" export OPENRAM_TMP="${{ github.workspace }}/scn4me_subm_temp" #python3-coverage run -p $OPENRAM_HOME/tests/regress.py -j 12 -t scn4m_subm - $OPENRAM_HOME/tests/regress.py -j 12 -t scn4m_subm - - name: Archive - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: scn4me_subm Archives - path: $OPENRAM_HOME/*.zip + $OPENRAM_HOME/tests/regress.py -j 16 -t scn4m_subm + # - name: Archive + # if: ${{ failure() }} + # uses: actions/upload-artifact@v2 + # with: + # name: scn4me_subm Archives + # path: ${{ github.workspace }}/scn4me_subm_temp/ freepdk45: runs-on: self-hosted steps: @@ -32,13 +32,13 @@ jobs: export OPENRAM_TECH="`pwd`/technology:/software/PDKs/skywater-tech" export OPENRAM_TMP="${{ github.workspace }}/freepdk45_temp" #python3-coverage run -p $OPENRAM_HOME/tests/regress.py -j 12 -t freepdk45 - $OPENRAM_HOME/tests/regress.py -j 12 -t freepdk45 - - name: Archive - if: ${{ failure() }} - uses: actions/upload-artifact@v2 - with: - name: FreePDK45 Archives - path: $OPENRAM_HOME/*.zip + $OPENRAM_HOME/tests/regress.py -j 16 -t freepdk45 + # - name: Archive + # if: ${{ failure() }} + # uses: actions/upload-artifact@v2 + # with: + # name: FreePDK45 Archives + # path: ${{ github.workspace }}/freepdk45_temp/ # coverage_stats: # if: ${{ always() }} # needs: [scn4me_subm, freepdk45] diff --git a/compiler/tests/30_openram_back_end_test.py b/compiler/tests/30_openram_back_end_test.py index 74232db0..c67b8249 100755 --- a/compiler/tests/30_openram_back_end_test.py +++ b/compiler/tests/30_openram_back_end_test.py @@ -49,8 +49,7 @@ class openram_back_end_test(openram_test): if OPTS.tech_name: options += " -t {}".format(OPTS.tech_name) - if OPTS.num_threads: - options += " -j {}".format(OPTS.num_threads) + options += " -j 2" # Always perform code coverage if OPTS.coverage == 0: diff --git a/compiler/tests/30_openram_front_end_test.py b/compiler/tests/30_openram_front_end_test.py index 9c134152..87b280dc 100755 --- a/compiler/tests/30_openram_front_end_test.py +++ b/compiler/tests/30_openram_front_end_test.py @@ -49,8 +49,7 @@ class openram_front_end_test(openram_test): if OPTS.tech_name: options += " -t {}".format(OPTS.tech_name) - if OPTS.num_threads: - options += " -j {}".format(OPTS.num_threads) + options += " -j 2" # Always perform code coverage if OPTS.coverage == 0: From fae72ca993ca55fe1198f3cceab6bea8684cc7d7 Mon Sep 17 00:00:00 2001 From: mrg Date: Tue, 23 Mar 2021 13:06:36 -0700 Subject: [PATCH 05/45] Test new archive options for github actions. --- .github/workflows/ci.yml | 32 ++++++++++++++++---------------- compiler/tests/testutils.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a95db618..894cbd55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,17 +9,17 @@ jobs: - name: SCMOS test run: | . /home/github-runner/setup-paths.sh - export OPENRAM_HOME="`pwd`/compiler" - export OPENRAM_TECH="`pwd`/technology:/software/PDKs/skywater-tech" + export OPENRAM_HOME="${{ github.workspace }}/compiler" + export OPENRAM_TECH="${{ github.workspace }}/technology:/software/PDKs/skywater-tech" export OPENRAM_TMP="${{ github.workspace }}/scn4me_subm_temp" #python3-coverage run -p $OPENRAM_HOME/tests/regress.py -j 12 -t scn4m_subm $OPENRAM_HOME/tests/regress.py -j 16 -t scn4m_subm - # - name: Archive - # if: ${{ failure() }} - # uses: actions/upload-artifact@v2 - # with: - # name: scn4me_subm Archives - # path: ${{ github.workspace }}/scn4me_subm_temp/ + - name: Archive + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: scn4me_subm Archives + path: ${{ github.workspace }}/*.zip freepdk45: runs-on: self-hosted steps: @@ -28,17 +28,17 @@ jobs: - name: FreePDK45 test run: | . /home/github-runner/setup-paths.sh - export OPENRAM_HOME="`pwd`/compiler" - export OPENRAM_TECH="`pwd`/technology:/software/PDKs/skywater-tech" + export OPENRAM_HOME="${{ github.workspace }}/compiler" + export OPENRAM_TECH="${{ github.workspace }}/technology:/software/PDKs/skywater-tech" export OPENRAM_TMP="${{ github.workspace }}/freepdk45_temp" #python3-coverage run -p $OPENRAM_HOME/tests/regress.py -j 12 -t freepdk45 $OPENRAM_HOME/tests/regress.py -j 16 -t freepdk45 - # - name: Archive - # if: ${{ failure() }} - # uses: actions/upload-artifact@v2 - # with: - # name: FreePDK45 Archives - # path: ${{ github.workspace }}/freepdk45_temp/ + - name: Archive + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: FreePDK45 Archives + path: ${{ github.workspace }}/*.zip # coverage_stats: # if: ${{ always() }} # needs: [scn4me_subm, freepdk45] diff --git a/compiler/tests/testutils.py b/compiler/tests/testutils.py index 4d8bf573..ed24f8cf 100644 --- a/compiler/tests/testutils.py +++ b/compiler/tests/testutils.py @@ -17,6 +17,23 @@ import traceback class openram_test(unittest.TestCase): """ Base unit test that we have some shared classes in. """ + def fail(self, msg): + import inspect + s = inspect.stack() + base_filename = os.path.splitext(os.path.basename(s[2].filename))[0] + + try: + OPENRAM_HOME = os.path.abspath(os.environ.get("OPENRAM_HOME")) + except: + debug.error("$OPENRAM_HOME is not properly defined.", 1) + + import shutil + zip_file = "{0}/../{1}_{2}".format(OPENRAM_HOME, base_filename, os.getpid()) + debug.info(0, "Archiving failed temp files {0} to {1}".format(OPTS.openram_temp, zip_file)) + shutil.make_archive(zip_file, 'zip', OPTS.openram_temp) + + super().fail(msg) + def local_drc_check(self, w): self.reset() From e144f03b23c9a821cf5a0c67dc09bb5f8c55e9df Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 24 Mar 2021 11:15:59 -0700 Subject: [PATCH 06/45] Add status for supply routing. --- compiler/router/supply_tree_router.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/router/supply_tree_router.py b/compiler/router/supply_tree_router.py index 0b29119d..e0455602 100644 --- a/compiler/router/supply_tree_router.py +++ b/compiler/router/supply_tree_router.py @@ -79,8 +79,8 @@ class supply_tree_router(router): """ remaining_components = sum(not x.is_routed() for x in self.pin_groups[pin_name]) - debug.info(1, "Routing {0} with {1} pin components to connect.".format(pin_name, - remaining_components)) + debug.info(1, "Routing {0} with {1} pins.".format(pin_name, + remaining_components)) # Create full graph debug.info(2, "Creating adjacency matrix") @@ -108,7 +108,9 @@ class supply_tree_router(router): connections.append((x, y)) # Route MST components - for (src, dest) in connections: + for index, (src, dest) in enumerate(connections): + if not (index % 100): + debug.info(0, "{0} supply segments routed, {1} remaining.".format(index, len(connections) - index)) self.route_signal(pin_name, src, dest) # if pin_name == "gnd": # print("\nSRC {}: ".format(src) + str(self.pin_groups[pin_name][src].grids) + str(self.pin_groups[pin_name][src].blockages)) From 4a40e96f6d3522335f3ad7ed5a7cdc5c5d5f2ec9 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 24 Mar 2021 14:32:10 -0700 Subject: [PATCH 07/45] Control logic route changes. Move wl_en to top control signal. Route wl_en directly to port_address. Reorder input bus to bank. --- compiler/modules/bank.py | 25 +++------ compiler/modules/control_logic.py | 10 ++-- compiler/sram/sram_1bank.py | 42 ++++++++++++++ .../tests/20_sram_1bank_4mux_1rw_1r_test.py | 56 +++++++++++++++++++ 4 files changed, 110 insertions(+), 23 deletions(-) create mode 100755 compiler/tests/20_sram_1bank_4mux_1rw_1r_test.py diff --git a/compiler/modules/bank.py b/compiler/modules/bank.py index 488439fd..9c2b2add 100644 --- a/compiler/modules/bank.py +++ b/compiler/modules/bank.py @@ -329,13 +329,13 @@ class bank(design.design): self.input_control_signals = [] port_num = 0 for port in range(OPTS.num_rw_ports): - self.input_control_signals.append(["s_en{}".format(port_num), "w_en{}".format(port_num), "p_en_bar{}".format(port_num), "wl_en{}".format(port_num)]) + self.input_control_signals.append(["p_en_bar{}".format(port_num), "s_en{}".format(port_num), "w_en{}".format(port_num)]) port_num += 1 for port in range(OPTS.num_w_ports): - self.input_control_signals.append(["w_en{}".format(port_num), "p_en_bar{}".format(port_num), "wl_en{}".format(port_num)]) + self.input_control_signals.append(["p_en_bar{}".format(port_num), "w_en{}".format(port_num)]) port_num += 1 for port in range(OPTS.num_r_ports): - self.input_control_signals.append(["s_en{}".format(port_num), "p_en_bar{}".format(port_num), "wl_en{}".format(port_num)]) + self.input_control_signals.append(["p_en_bar{}".format(port_num), "s_en{}".format(port_num)]) port_num += 1 # Number of control lines in the bus for each port @@ -691,6 +691,8 @@ class bank(design.design): make_pins=(self.num_banks==1), pitch=self.m3_pitch) + self.copy_layout_pin(self.port_address_inst[0], "wl_en", self.prefix + "wl_en0") + # Port 1 if len(self.all_ports)==2: # The other control bus is routed up to two pitches above the bitcell array @@ -706,6 +708,8 @@ class bank(design.design): make_pins=(self.num_banks==1), pitch=self.m3_pitch) + self.copy_layout_pin(self.port_address_inst[1], "wl_en", self.prefix + "wl_en1") + def route_port_data_to_bitcell_array(self, port): """ Routing of BL and BR between port data and bitcell array """ @@ -1054,21 +1058,6 @@ class bank(design.design): to_layer="m2", offset=control_pos) - # clk to wordline_driver - control_signal = self.prefix + "wl_en{}".format(port) - if port % 2: - pin_pos = self.port_address_inst[port].get_pin("wl_en").uc() - control_y_offset = self.bus_pins[port][control_signal].by() - mid_pos = vector(pin_pos.x, control_y_offset + self.m1_pitch) - else: - pin_pos = self.port_address_inst[port].get_pin("wl_en").bc() - control_y_offset = self.bus_pins[port][control_signal].uy() - mid_pos = vector(pin_pos.x, control_y_offset - self.m1_pitch) - control_x_offset = self.bus_pins[port][control_signal].cx() - control_pos = vector(control_x_offset, mid_pos.y) - self.add_wire(self.m1_stack, [pin_pos, mid_pos, control_pos]) - self.add_via_center(layers=self.m1_stack, - offset=control_pos) def graph_exclude_precharge(self): """ diff --git a/compiler/modules/control_logic.py b/compiler/modules/control_logic.py index 0c9d50bf..5aec6872 100644 --- a/compiler/modules/control_logic.py +++ b/compiler/modules/control_logic.py @@ -333,8 +333,9 @@ class control_logic(design.design): row += 1 self.place_gated_clk_buf_row(row) row += 1 - self.place_wlen_row(row) - row += 1 + if (self.port_type == "rw") or (self.port_type == "r"): + self.place_sen_row(row) + row += 1 if (self.port_type == "rw") or (self.port_type == "w"): self.place_wen_row(row) height = self.w_en_gate_inst.uy() @@ -345,9 +346,8 @@ class control_logic(design.design): if (self.port_type == "rw") or (self.port_type == "w"): self.place_rbl_delay_row(row) row += 1 - if (self.port_type == "rw") or (self.port_type == "r"): - self.place_sen_row(row) - row += 1 + self.place_wlen_row(row) + row += 1 self.place_delay(row) height = self.delay_inst.uy() control_center_y = self.delay_inst.by() diff --git a/compiler/sram/sram_1bank.py b/compiler/sram/sram_1bank.py index d831c047..6bc2cd43 100644 --- a/compiler/sram/sram_1bank.py +++ b/compiler/sram/sram_1bank.py @@ -340,6 +340,15 @@ class sram_1bank(sram_base): def route_dff(self, port, add_routes): + # This is only done when we add_routes because the data channel will be larger + # so that can be used for area estimation. + if add_routes: + self.route_col_addr_dffs(port) + + self.route_data_dffs(port, add_routes) + + def route_col_addr_dffs(self, port): + route_map = [] # column mux dff is routed on it's own since it is to the far end @@ -351,6 +360,38 @@ class sram_1bank(sram_base): bank_pins = [self.bank_inst.get_pin(x) for x in bank_names] route_map.extend(list(zip(bank_pins, dff_pins))) + if len(route_map) > 0: + + layer_stack = self.m1_stack + + if port == 0: + offset = vector(self.control_logic_insts[port].rx() + self.dff.width, + - self.data_bus_size[port] + 2 * self.m3_pitch) + cr = channel_route(netlist=route_map, + offset=offset, + layer_stack=layer_stack, + parent=self) + # This causes problem in magic since it sometimes cannot extract connectivity of isntances + # with no active devices. + self.add_inst(cr.name, cr) + self.connect_inst([]) + #self.add_flat_inst(cr.name, cr) + else: + offset = vector(0, + self.bank.height + self.m3_pitch) + cr = channel_route(netlist=route_map, + offset=offset, + layer_stack=layer_stack, + parent=self) + # This causes problem in magic since it sometimes cannot extract connectivity of isntances + # with no active devices. + self.add_inst(cr.name, cr) + self.connect_inst([]) + #self.add_flat_inst(cr.name, cr) + + def route_data_dffs(self, port, add_routes): + route_map = [] + # wmask dff if self.num_wmasks > 0 and port in self.write_ports: dff_names = ["dout_{}".format(x) for x in range(self.num_wmasks)] @@ -377,6 +418,7 @@ class sram_1bank(sram_base): if len(route_map) > 0: + # The write masks will have blockages on M1 if self.num_wmasks > 0 and port in self.write_ports: layer_stack = self.m3_stack else: diff --git a/compiler/tests/20_sram_1bank_4mux_1rw_1r_test.py b/compiler/tests/20_sram_1bank_4mux_1rw_1r_test.py new file mode 100755 index 00000000..8bcc0415 --- /dev/null +++ b/compiler/tests/20_sram_1bank_4mux_1rw_1r_test.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +# See LICENSE for licensing information. +# +# Copyright (c) 2016-2021 Regents of the University of California and The Board +# of Regents for the Oklahoma Agricultural and Mechanical College +# (acting for and on behalf of Oklahoma State University) +# All rights reserved. +# +import unittest +from testutils import * +import sys, os +sys.path.append(os.getenv("OPENRAM_HOME")) +import globals +from globals import OPTS +from sram_factory import factory +import debug + + +class sram_1bank_4mux_1rw_1r_test(openram_test): + + def runTest(self): + config_file = "{}/tests/configs/config".format(os.getenv("OPENRAM_HOME")) + globals.init_openram(config_file) + from sram_config import sram_config + + OPTS.num_rw_ports = 1 + OPTS.num_r_ports = 1 + OPTS.num_w_ports = 0 + globals.setup_bitcell() + + c = sram_config(word_size=4, + num_words=64, + num_banks=1) + + c.words_per_row=4 + c.recompute_sizes() + debug.info(1, "Layout test for {}rw,{}r,{}w sram " + "with {} bit words, {} words, {} words per " + "row, {} banks".format(OPTS.num_rw_ports, + OPTS.num_r_ports, + OPTS.num_w_ports, + c.word_size, + c.num_words, + c.words_per_row, + c.num_banks)) + a = factory.create(module_type="sram", sram_config=c) + self.local_check(a, final_verification=True) + + globals.end_openram() + +# run the test from the command line +if __name__ == "__main__": + (OPTS, args) = globals.parse_args() + del sys.argv[1:] + header(__file__, OPTS.tech_name) + unittest.main(testRunner=debugTestRunner()) From 6e2f60353c3ccf86d6cfd1ed78961817bafbc635 Mon Sep 17 00:00:00 2001 From: mrg Date: Thu, 25 Mar 2021 10:00:24 -0700 Subject: [PATCH 08/45] Add wells to driver stages. Remove unnecessary height/center in control logic. --- compiler/modules/control_logic.py | 2 -- compiler/pgates/pdriver.py | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/compiler/modules/control_logic.py b/compiler/modules/control_logic.py index 5aec6872..eb9a21ed 100644 --- a/compiler/modules/control_logic.py +++ b/compiler/modules/control_logic.py @@ -338,8 +338,6 @@ class control_logic(design.design): row += 1 if (self.port_type == "rw") or (self.port_type == "w"): self.place_wen_row(row) - height = self.w_en_gate_inst.uy() - control_center_y = self.w_en_gate_inst.uy() row += 1 self.place_pen_row(row) row += 1 diff --git a/compiler/pgates/pdriver.py b/compiler/pgates/pdriver.py index 4662577b..bbadb9ab 100644 --- a/compiler/pgates/pdriver.py +++ b/compiler/pgates/pdriver.py @@ -87,13 +87,11 @@ class pdriver(pgate.pgate): def add_modules(self): self.inv_list = [] - add_well = self.add_wells for size in self.size_list: temp_inv = factory.create(module_type="pinv", size=size, height=self.height, - add_wells=add_well) - add_well=False + add_wells=self.add_wells) self.inv_list.append(temp_inv) self.add_mod(temp_inv) From e681806f0dd2641113585a9c6a1faf8b7a9e576c Mon Sep 17 00:00:00 2001 From: mrg Date: Thu, 25 Mar 2021 10:02:34 -0700 Subject: [PATCH 09/45] Update to 24 threads. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 894cbd55..ef2ef6a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: export OPENRAM_TECH="${{ github.workspace }}/technology:/software/PDKs/skywater-tech" export OPENRAM_TMP="${{ github.workspace }}/scn4me_subm_temp" #python3-coverage run -p $OPENRAM_HOME/tests/regress.py -j 12 -t scn4m_subm - $OPENRAM_HOME/tests/regress.py -j 16 -t scn4m_subm + $OPENRAM_HOME/tests/regress.py -j 24 -t scn4m_subm - name: Archive if: ${{ failure() }} uses: actions/upload-artifact@v2 @@ -32,7 +32,7 @@ jobs: export OPENRAM_TECH="${{ github.workspace }}/technology:/software/PDKs/skywater-tech" export OPENRAM_TMP="${{ github.workspace }}/freepdk45_temp" #python3-coverage run -p $OPENRAM_HOME/tests/regress.py -j 12 -t freepdk45 - $OPENRAM_HOME/tests/regress.py -j 16 -t freepdk45 + $OPENRAM_HOME/tests/regress.py -j 24 -t freepdk45 - name: Archive if: ${{ failure() }} uses: actions/upload-artifact@v2 From b9086dbbe5746264dc7c3e29f983d0d9afba34d1 Mon Sep 17 00:00:00 2001 From: mrg Date: Fri, 26 Mar 2021 06:56:58 -0700 Subject: [PATCH 10/45] Add unit test times to output. --- compiler/tests/testutils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/compiler/tests/testutils.py b/compiler/tests/testutils.py index ed24f8cf..987b64e4 100644 --- a/compiler/tests/testutils.py +++ b/compiler/tests/testutils.py @@ -12,11 +12,19 @@ from globals import OPTS import debug import pdb import traceback +import time class openram_test(unittest.TestCase): """ Base unit test that we have some shared classes in. """ + def setUp(self): + self.start_time = time.time() + + def tearDown(self): + duration = time.time() - self.start_time + print('%s: %.3fs' % (self.id(), duration)) + def fail(self, msg): import inspect s = inspect.stack() From 7e29dd7ff2cc78df18d229cce05cf33bb0514c7e Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 31 Mar 2021 09:38:06 -0700 Subject: [PATCH 11/45] Reduce verbosity of routing info --- compiler/router/supply_tree_router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/router/supply_tree_router.py b/compiler/router/supply_tree_router.py index e0455602..ba54ee39 100644 --- a/compiler/router/supply_tree_router.py +++ b/compiler/router/supply_tree_router.py @@ -110,7 +110,7 @@ class supply_tree_router(router): # Route MST components for index, (src, dest) in enumerate(connections): if not (index % 100): - debug.info(0, "{0} supply segments routed, {1} remaining.".format(index, len(connections) - index)) + debug.info(1, "{0} supply segments routed, {1} remaining.".format(index, len(connections) - index)) self.route_signal(pin_name, src, dest) # if pin_name == "gnd": # print("\nSRC {}: ".format(src) + str(self.pin_groups[pin_name][src].grids) + str(self.pin_groups[pin_name][src].blockages)) From c7f99aef2c1dc211bac1ae854b8c4d7ea6b78a3f Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 31 Mar 2021 12:14:20 -0700 Subject: [PATCH 12/45] Add functional comment to aid debugging checks. --- compiler/characterizer/functional.py | 33 ++++++++++++++++++---------- compiler/characterizer/setup_hold.py | 8 ++++++- compiler/characterizer/stimuli.py | 25 +++++++++------------ 3 files changed, 38 insertions(+), 28 deletions(-) diff --git a/compiler/characterizer/functional.py b/compiler/characterizer/functional.py index fb0a176f..dad4fd96 100644 --- a/compiler/characterizer/functional.py +++ b/compiler/characterizer/functional.py @@ -227,18 +227,18 @@ class functional(simulation): def add_read_check(self, word, port): """ Add to the check array to ensure a read works. """ try: - self.check + self.check_count except: - self.check = 0 - self.read_check.append([word, "{0}{1}".format(self.dout_name, port), self.t_current + self.period, self.check]) - self.check += 1 + self.check_count = 0 + self.read_check.append([word, "{0}{1}".format(self.dout_name, port), self.t_current + self.period, self.check_count]) + self.check_count += 1 def read_stim_results(self): # Extract dout values from spice timing.lis - for (word, dout_port, eo_period, check) in self.read_check: + for (word, dout_port, eo_period, check_count) in self.read_check: sp_read_value = "" for bit in range(self.word_size + self.num_spare_cols): - value = parse_spice_list("timing", "v{0}.{1}ck{2}".format(dout_port.lower(), bit, check)) + value = parse_spice_list("timing", "v{0}.{1}ck{2}".format(dout_port.lower(), bit, check_count)) try: value = float(value) if value > self.v_high: @@ -260,7 +260,7 @@ class functional(simulation): return (0, error) - self.read_results.append([sp_read_value, dout_port, eo_period, check]) + self.read_results.append([sp_read_value, dout_port, eo_period, check_count]) return (1, "SUCCESS") def check_stim_results(self): @@ -432,12 +432,21 @@ class functional(simulation): # Generate dout value measurements self.sf.write("\n * Generation of dout measurements\n") for (word, dout_port, eo_period, check) in self.read_check: - t_intital = eo_period - 0.01 * self.period + t_initial = eo_period - 0.01 * self.period t_final = eo_period + 0.01 * self.period - for bit in range(self.word_size + self.num_spare_cols): - self.stim.gen_meas_value(meas_name="V{0}_{1}ck{2}".format(dout_port, bit, check), - dout="{0}_{1}".format(dout_port, bit), - t_intital=t_intital, + num_bits = self.word_size + self.num_spare_cols + for bit in range(num_bits): + measure_name = "V{0}_{1}ck{2}".format(dout_port, bit, check) + signal_name = "{0}_{1}".format(dout_port, bit) + voltage_value = self.stim.get_voltage(word[num_bits - bit - 1]) + + self.stim.add_comment("* CHECK {0} {1} = {2} time = {3}".format(signal_name, + measure_name, + voltage_value, + eo_period)) + self.stim.gen_meas_value(meas_name=measure_name, + dout=signal_name, + t_initial=t_initial, t_final=t_final) self.stim.write_control(self.cycle_times[-1] + self.period) diff --git a/compiler/characterizer/setup_hold.py b/compiler/characterizer/setup_hold.py index 83ec835b..b323078a 100644 --- a/compiler/characterizer/setup_hold.py +++ b/compiler/characterizer/setup_hold.py @@ -82,7 +82,13 @@ class setup_hold(): """ self.sf.write("\n* Generation of the data and clk signals\n") - incorrect_value = self.stim.get_inverse_value(correct_value) + if correct_value == 1: + incorrect_value = 0 + elif correct_value == 0: + incorrect_value = 1 + else: + debug.error("Invalid value {}".format(correct_value)) + if mode=="HOLD": init_value = incorrect_value start_value = correct_value diff --git a/compiler/characterizer/stimuli.py b/compiler/characterizer/stimuli.py index 06b6058d..0895d57e 100644 --- a/compiler/characterizer/stimuli.py +++ b/compiler/characterizer/stimuli.py @@ -169,22 +169,14 @@ class stimuli(): def gen_constant(self, sig_name, v_val): """ Generates a constant signal with reference voltage and the voltage value """ self.sf.write("V{0} {0} 0 DC {1}\n".format(sig_name, v_val)) - - def get_inverse_voltage(self, value): - if value > 0.5 * self.voltage: + + def get_voltage(self, value): + if value == "0" or value == 0: return 0 - elif value <= 0.5 * self.voltage: + elif value == "1" or value == 1: return self.voltage else: - debug.error("Invalid value to get an inverse of: {0}".format(value)) - - def get_inverse_value(self, value): - if value > 0.5: - return 0 - elif value <= 0.5: - return 1 - else: - debug.error("Invalid value to get an inverse of: {0}".format(value)) + debug.error("Invalid value to get a voltage of: {0}".format(value)) def gen_meas_delay(self, meas_name, trig_name, targ_name, trig_val, targ_val, trig_dir, targ_dir, trig_td, targ_td): """ Creates the .meas statement for the measurement of delay """ @@ -228,8 +220,8 @@ class stimuli(): t_initial, t_final)) - def gen_meas_value(self, meas_name, dout, t_intital, t_final): - measure_string=".meas tran {0} AVG v({1}) FROM={2}n TO={3}n\n\n".format(meas_name, dout, t_intital, t_final) + def gen_meas_value(self, meas_name, dout, t_initial, t_final): + measure_string=".meas tran {0} AVG v({1}) FROM={2}n TO={3}n\n\n".format(meas_name, dout, t_initial, t_final) self.sf.write(measure_string) def write_control(self, end_time, runlvl=4): @@ -310,6 +302,9 @@ class stimuli(): for item in list(includes): self.sf.write(".include \"{0}\"\n".format(item)) + def add_comment(self, msg): + self.sf.write(msg + "\n") + def write_supply(self): """ Writes supply voltage statements """ gnd_node_name = "0" From 014c95f761fa77e579e7cafee3902716d519c9b5 Mon Sep 17 00:00:00 2001 From: mrg Date: Thu, 1 Apr 2021 16:48:15 -0700 Subject: [PATCH 13/45] Add accounting output to ngspice --- compiler/characterizer/functional.py | 2 +- compiler/characterizer/stimuli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/characterizer/functional.py b/compiler/characterizer/functional.py index dad4fd96..35444b15 100644 --- a/compiler/characterizer/functional.py +++ b/compiler/characterizer/functional.py @@ -439,7 +439,7 @@ class functional(simulation): measure_name = "V{0}_{1}ck{2}".format(dout_port, bit, check) signal_name = "{0}_{1}".format(dout_port, bit) voltage_value = self.stim.get_voltage(word[num_bits - bit - 1]) - + self.stim.add_comment("* CHECK {0} {1} = {2} time = {3}".format(signal_name, measure_name, voltage_value, diff --git a/compiler/characterizer/stimuli.py b/compiler/characterizer/stimuli.py index 0895d57e..d60cab85 100644 --- a/compiler/characterizer/stimuli.py +++ b/compiler/characterizer/stimuli.py @@ -246,7 +246,7 @@ class stimuli(): # which is more accurate, but slower than the default trapezoid method # Do not remove this or it may not converge due to some "pa_00" nodes # unless you figure out what these are. - self.sf.write(".OPTIONS POST=1 RELTOL={0} PROBE method=gear\n".format(reltol)) + self.sf.write(".OPTIONS POST=1 RELTOL={0} PROBE method=gear ACCT\n".format(reltol)) elif OPTS.spice_name == "spectre": self.sf.write("simulator lang=spectre\n") if OPTS.use_pex: From e0024fa79a5851c4876c4f33890807da6a511394 Mon Sep 17 00:00:00 2001 From: mrg Date: Tue, 6 Apr 2021 12:52:50 -0700 Subject: [PATCH 14/45] Add verbosity to error output --- compiler/router/pin_group.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/compiler/router/pin_group.py b/compiler/router/pin_group.py index 5f9da465..7a5d8817 100644 --- a/compiler/router/pin_group.py +++ b/compiler/router/pin_group.py @@ -148,8 +148,9 @@ class pin_group: enclosure = self.router.compute_pin_enclosure(ll, ur, ll.z) pin_list.append(enclosure) - debug.check(len(pin_list) > 0, - "Did not find any enclosures.") + if len(pin_list) == 0: + debug.error("Did not find any enclosures for {}".format(self.name)) + self.router.write_debug_gds("pin_enclosure_error.gds") # Now simplify the enclosure list new_pin_list = self.remove_redundant_shapes(pin_list) From 31d3e6cb26022e4cab05413daae63a55af2aa55c Mon Sep 17 00:00:00 2001 From: mrg Date: Tue, 6 Apr 2021 12:53:10 -0700 Subject: [PATCH 15/45] Change LWL layers --- compiler/modules/local_bitcell_array.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index 67eac001..f0427c51 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -239,7 +239,12 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): out_loc = out_pin.lc() mid_loc = vector(self.wl_insts[port].lx() - 1.5 * self.m3_pitch, out_loc.y) in_loc = in_pin.rc() - self.add_path(out_pin.layer, [out_loc, mid_loc, in_loc]) + + self.add_path(out_pin.layer, [out_loc, mid_loc]) + self.add_via_stack_center(from_layer=out_pin.layer, + to_layer=in_pin.layer, + offset=mid_loc) + self.add_path(in_pin.layer, [mid_loc, in_loc]) def get_main_array_top(self): return self.bitcell_array_inst.by() + self.bitcell_array.get_main_array_top() From d609e4ea04774dcf574ba7b64b36b81ac965276b Mon Sep 17 00:00:00 2001 From: mrg Date: Tue, 6 Apr 2021 17:01:52 -0700 Subject: [PATCH 16/45] Reimplement trim options (except on unit tests). Allow trim netlist to be used for delay and functional simulation. Each class implements a "trim_insts" set of instances that can be removed. By default far left, right, top and bottom cells in the bitcell arrays are kept. Use lvs option in sp_write Fix lvs option in sram. --- compiler/base/hierarchy_design.py | 6 +- compiler/base/hierarchy_spice.py | 33 +++++----- compiler/characterizer/delay.py | 10 +-- compiler/characterizer/functional.py | 30 ++++++--- compiler/characterizer/lib.py | 7 +-- compiler/characterizer/simulation.py | 5 +- compiler/characterizer/trim_spice.py | 26 ++++---- compiler/modules/bitcell_array.py | 6 +- compiler/sram/sram.py | 7 +-- compiler/sram/sram_base.py | 9 ++- compiler/tests/21_hspice_delay_test.py | 63 +++++++++---------- compiler/tests/21_ngspice_delay_test.py | 58 ++++++++--------- .../tests/22_psram_1bank_2mux_func_test.py | 6 +- .../tests/22_psram_1bank_4mux_func_test.py | 6 +- .../tests/22_psram_1bank_8mux_func_test.py | 6 +- .../tests/22_psram_1bank_nomux_func_test.py | 6 +- .../tests/22_sram_1bank_2mux_func_test.py | 6 +- .../22_sram_1bank_2mux_global_func_test.py | 6 +- .../22_sram_1bank_2mux_sparecols_func_test.py | 6 +- .../tests/22_sram_1bank_4mux_func_test.py | 6 +- .../tests/22_sram_1bank_8mux_func_test.py | 6 +- .../22_sram_1bank_nomux_1rw_1r_func_test.py | 6 +- .../tests/22_sram_1bank_nomux_func_test.py | 5 +- ...22_sram_1bank_nomux_sparecols_func_test.py | 5 +- .../22_sram_1bank_wmask_1rw_1r_func_test.py | 6 +- compiler/tests/22_sram_wmask_func_test.py | 6 +- compiler/tests/23_lib_sram_test.py | 1 - compiler/tests/26_sram_pex_test.py | 2 +- compiler/tests/50_riscv_func_test.py | 6 +- compiler/tests/testutils.py | 2 +- 30 files changed, 147 insertions(+), 206 deletions(-) diff --git a/compiler/base/hierarchy_design.py b/compiler/base/hierarchy_design.py index fe1f4c55..d99c7363 100644 --- a/compiler/base/hierarchy_design.py +++ b/compiler/base/hierarchy_design.py @@ -53,7 +53,7 @@ class hierarchy_design(hierarchy_spice.spice, hierarchy_layout.layout): elif (OPTS.inline_lvsdrc or force_check or final_verification): tempspice = "{}.sp".format(self.name) - self.lvs_write("{0}{1}".format(OPTS.openram_temp, tempspice)) + self.sp_write("{0}{1}".format(OPTS.openram_temp, tempspice), lvs=True) tempgds = "{}.gds".format(self.name) self.gds_write("{0}{1}".format(OPTS.openram_temp, tempgds)) # Final verification option does not allow nets to be connected by label. @@ -82,7 +82,7 @@ class hierarchy_design(hierarchy_spice.spice, hierarchy_layout.layout): return elif (not OPTS.is_unit_test and OPTS.check_lvsdrc and (OPTS.inline_lvsdrc or final_verification)): tempspice = "{}.sp".format(self.name) - self.lvs_write("{0}{1}".format(OPTS.openram_temp, tempspice)) + self.sp_write("{0}{1}".format(OPTS.openram_temp, tempspice), lvs=True) tempgds = "{}.gds".format(self.cell_name) self.gds_write("{0}{1}".format(OPTS.openram_temp, tempgds)) num_errors = verify.run_drc(self.cell_name, tempgds, tempspice, final_verification=final_verification) @@ -102,7 +102,7 @@ class hierarchy_design(hierarchy_spice.spice, hierarchy_layout.layout): return elif (not OPTS.is_unit_test and OPTS.check_lvsdrc and (OPTS.inline_lvsdrc or final_verification)): tempspice = "{}.sp".format(self.cell_name) - self.lvs_write("{0}{1}".format(OPTS.openram_temp, tempspice)) + self.sp_write("{0}{1}".format(OPTS.openram_temp, tempspice), lvs=True) tempgds = "{}.gds".format(self.name) self.gds_write("{0}{1}".format(OPTS.openram_temp, tempgds)) num_errors = verify.run_lvs(self.name, tempgds, tempspice, final_verification=final_verification) diff --git a/compiler/base/hierarchy_spice.py b/compiler/base/hierarchy_spice.py index 51d2c3b7..2f2d3ec9 100644 --- a/compiler/base/hierarchy_spice.py +++ b/compiler/base/hierarchy_spice.py @@ -63,6 +63,8 @@ class spice(): self.conns = [] # If this is set, it will out output subckt or isntances of this (for row/col caps etc.) self.no_instances = False + # If we are doing a trimmed netlist, these are the instance that will be filtered + self.trim_insts = set() # Keep track of any comments to add the the spice try: self.commments @@ -312,10 +314,11 @@ class spice(): return True return False - def sp_write_file(self, sp, usedMODS, lvs_netlist=False): + def sp_write_file(self, sp, usedMODS, lvs=False, trim=False): """ Recursive spice subcircuit write; - Writes the spice subcircuit from the library or the dynamically generated one + Writes the spice subcircuit from the library or the dynamically generated one. + Trim netlist is intended ONLY for bitcell arrays. """ if self.no_instances: @@ -328,7 +331,7 @@ class spice(): if self.contains(i, usedMODS): continue usedMODS.append(i) - i.sp_write_file(sp, usedMODS, lvs_netlist) + i.sp_write_file(sp, usedMODS, lvs, trim) if len(self.insts) == 0: return @@ -371,10 +374,16 @@ class spice(): # these are wires and paths if self.conns[i] == []: continue + # Instance with no devices in it needs no subckt/instance if self.insts[i].mod.no_instances: continue - if lvs_netlist and hasattr(self.insts[i].mod, "lvs_device"): + + # If this is a trimmed netlist, skip it by adding comment char + if trim and self.insts[i].name in self.trim_insts: + sp.write("* ") + + if lvs and hasattr(self.insts[i].mod, "lvs_device"): sp.write(self.insts[i].mod.lvs_device.format(self.insts[i].name, " ".join(self.conns[i]))) sp.write("\n") @@ -394,30 +403,20 @@ class spice(): # Including the file path makes the unit test fail for other users. # if os.path.isfile(self.sp_file): # sp.write("\n* {0}\n".format(self.sp_file)) - if lvs_netlist and hasattr(self, "lvs"): + if lvs and hasattr(self, "lvs"): sp.write("\n".join(self.lvs)) else: sp.write("\n".join(self.spice)) sp.write("\n") - def sp_write(self, spname): + def sp_write(self, spname, lvs=False, trim=False): """Writes the spice to files""" debug.info(3, "Writing to {0}".format(spname)) spfile = open(spname, 'w') spfile.write("*FIRST LINE IS A COMMENT\n") usedMODS = list() - self.sp_write_file(spfile, usedMODS) - del usedMODS - spfile.close() - - def lvs_write(self, spname): - """Writes the lvs to files""" - debug.info(3, "Writing to {0}".format(spname)) - spfile = open(spname, 'w') - spfile.write("*FIRST LINE IS A COMMENT\n") - usedMODS = list() - self.sp_write_file(spfile, usedMODS, True) + self.sp_write_file(spfile, usedMODS, lvs=lvs, trim=trim) del usedMODS spfile.close() diff --git a/compiler/characterizer/delay.py b/compiler/characterizer/delay.py index f491d8b0..168a73e9 100644 --- a/compiler/characterizer/delay.py +++ b/compiler/characterizer/delay.py @@ -1100,14 +1100,8 @@ class delay(simulation): # Set up to trim the netlist here if that is enabled if OPTS.trim_netlist: - self.trim_sp_file = "{}reduced.sp".format(OPTS.openram_temp) - self.trimsp=trim_spice(self.sp_file, self.trim_sp_file) - self.trimsp.set_configuration(self.num_banks, - self.num_rows, - self.num_cols, - self.word_size, - self.num_spare_rows) - self.trimsp.trim(self.probe_address, self.probe_data) + self.trim_sp_file = "{}trimmed.sp".format(OPTS.openram_temp) + self.sram.sp_write(self.trim_sp_file, lvs=False, trim=True) else: # The non-reduced netlist file when it is disabled self.trim_sp_file = "{}sram.sp".format(OPTS.openram_temp) diff --git a/compiler/characterizer/functional.py b/compiler/characterizer/functional.py index 35444b15..0435d22e 100644 --- a/compiler/characterizer/functional.py +++ b/compiler/characterizer/functional.py @@ -21,13 +21,17 @@ class functional(simulation): for successful SRAM operation. """ - def __init__(self, sram, spfile, corner=None, cycles=15, period=None, output_path=None): + def __init__(self, sram, spfile=None, corner=None, cycles=15, period=None, output_path=None): super().__init__(sram, spfile, corner) # Seed the characterizer with a constant seed for unit tests if OPTS.is_unit_test: random.seed(12345) + if not spfile: + # self.sp_file is assigned in base class + sram.sp_write(self.sp_file, trim=OPTS.trim_netlist) + if not corner: corner = (OPTS.process_corners[0], OPTS.supply_voltages[0], OPTS.temperatures[0]) @@ -46,7 +50,18 @@ class functional(simulation): if not self.num_spare_cols: self.num_spare_cols = 0 + if self.num_spare_cols > 0: + debug.error("Functional simulation not debugged with spare columns.") + # FIXME: we need to remember the correct value of the spare columns + self.max_value = 2 ** (self.word_size + self.num_spare_cols) - 1 + # If trim is set, specify the valid addresses + self.valid_addresses = set() + self.max_address = 2**self.addr_size - 1 + (self.num_spare_rows * self.words_per_row) + if OPTS.trim_netlist: + for i in range(self.words_per_row): + self.valid_addresses.add(i) + self.valid_addresses.add(self.max_address - i) self.probe_address, self.probe_data = '0' * self.addr_size, 0 self.set_corner(corner) self.set_spice_constants() @@ -300,21 +315,16 @@ class functional(simulation): def gen_data(self): """ Generates a random word to write. """ - if not self.num_spare_cols: - random_value = random.randint(0, (2 ** self.word_size) - 1) - else: - random_value1 = random.randint(0, (2 ** self.word_size) - 1) - random_value2 = random.randint(0, (2 ** self.num_spare_cols) - 1) - random_value = random_value1 + random_value2 + random_value = random.randint(0, self.max_value) data_bits = self.convert_to_bin(random_value, False) return data_bits def gen_addr(self): """ Generates a random address value to write to. """ - if self.num_spare_rows==0: - random_value = random.randint(0, (2 ** self.addr_size) - 1) + if self.valid_addresses: + random_value = random.sample(self.valid_addresses, 1)[0] else: - random_value = random.randint(0, ((2 ** (self.addr_size - 1) - 1)) + (self.num_spare_rows * self.words_per_row)) + random_value = random.randint(0, self.max_address) addr_bits = self.convert_to_bin(random_value, True) return addr_bits diff --git a/compiler/characterizer/lib.py b/compiler/characterizer/lib.py index d37119a9..aa892b3d 100644 --- a/compiler/characterizer/lib.py +++ b/compiler/characterizer/lib.py @@ -5,9 +5,8 @@ # (acting for and on behalf of Oklahoma State University) # All rights reserved. # -import os,sys,re +import os import debug -import math import datetime from .setup_hold import * from .delay import * @@ -16,6 +15,7 @@ import tech import numpy as np from globals import OPTS + class lib: """ lib file generation.""" @@ -601,7 +601,6 @@ class lib: from .elmore import elmore as model else: debug.error("{} model not recognized. See options.py for available models.".format(OPTS.model_name)) - import math m = model(self.sram, self.sp_file, self.corner) char_results = m.get_lib_values(self.slews,self.loads) @@ -834,4 +833,4 @@ class lib: #FIXME: should be read_fall_power datasheet.write("{0},{1},".format('write_fall_power_{}'.format(port), read0_power)) - \ No newline at end of file + diff --git a/compiler/characterizer/simulation.py b/compiler/characterizer/simulation.py index 0afe2459..5becbacf 100644 --- a/compiler/characterizer/simulation.py +++ b/compiler/characterizer/simulation.py @@ -27,7 +27,10 @@ class simulation(): self.num_spare_cols = 0 else: self.num_spare_cols = self.sram.num_spare_cols - self.sp_file = spfile + if not spfile: + self.sp_file = OPTS.openram_temp + "sram.sp" + else: + self.sp_file = spfile self.all_ports = self.sram.all_ports self.readwrite_ports = self.sram.readwrite_ports diff --git a/compiler/characterizer/trim_spice.py b/compiler/characterizer/trim_spice.py index d659212c..e8499d5c 100644 --- a/compiler/characterizer/trim_spice.py +++ b/compiler/characterizer/trim_spice.py @@ -9,6 +9,7 @@ import debug from math import log,ceil import re + class trim_spice(): """ A utility to trim redundant parts of an SRAM spice netlist. @@ -29,7 +30,6 @@ class trim_spice(): for i in range(len(self.spice)): self.spice[i] = self.spice[i].rstrip(" \n") - self.sp_buffer = self.spice def set_configuration(self, banks, rows, columns, word_size): @@ -46,21 +46,23 @@ class trim_spice(): self.col_addr_size = int(log(self.words_per_row, 2)) self.bank_addr_size = self.col_addr_size + self.row_addr_size self.addr_size = self.bank_addr_size + int(log(self.num_banks, 2)) - - + def trim(self, address, data_bit): - """ Reduce the spice netlist but KEEP the given bits at the - address (and things that will add capacitive load!)""" + """ + Reduce the spice netlist but KEEP the given bits at the + address (and things that will add capacitive load!) + """ # Always start fresh if we do multiple reductions self.sp_buffer = self.spice # Split up the address and convert to an int - wl_address = int(address[self.col_addr_size:],2) - if self.col_addr_size>0: - col_address = int(address[0:self.col_addr_size],2) + wl_address = int(address[self.col_addr_size:], 2) + if self.col_addr_size > 0: + col_address = int(address[0:self.col_addr_size], 2) else: col_address = 0 + # 1. Keep cells in the bitcell array based on WL and BL wl_name = "wl_{}".format(wl_address) bl_name = "bl_{}".format(int(self.words_per_row*data_bit + col_address)) @@ -81,7 +83,6 @@ class trim_spice(): self.sp_buffer.insert(0, "* It should NOT be used for LVS!!") self.sp_buffer.insert(0, "* WARNING: This is a TRIMMED NETLIST.") - wl_regex = r"wl\d*_{}".format(wl_address) bl_regex = r"bl\d*_{}".format(int(self.words_per_row*data_bit + col_address)) self.remove_insts("bitcell_array",[wl_regex,bl_regex]) @@ -91,11 +92,11 @@ class trim_spice(): #self.remove_insts("sense_amp_array",[bl_regex]) # 3. Keep column muxes basd on BL - self.remove_insts("column_mux_array",[bl_regex]) + self.remove_insts("column_mux_array", [bl_regex]) # 4. Keep write driver based on DATA data_regex = r"data_{}".format(data_bit) - self.remove_insts("write_driver_array",[data_regex]) + self.remove_insts("write_driver_array", [data_regex]) # 5. Keep wordline driver based on WL # Need to keep the gater too @@ -111,7 +112,6 @@ class trim_spice(): sp.write("\n".join(self.sp_buffer)) sp.close() - def remove_insts(self, subckt_name, keep_inst_list): """This will remove all of the instances in the list from the named subckt that DO NOT contain a term in the list. It just does a @@ -119,7 +119,7 @@ class trim_spice(): net connection, the instance name, anything.. """ removed_insts = 0 - #Expects keep_inst_list are regex patterns. Compile them here. + # Expects keep_inst_list are regex patterns. Compile them here. compiled_patterns = [re.compile(pattern) for pattern in keep_inst_list] start_name = ".SUBCKT {}".format(subckt_name) diff --git a/compiler/modules/bitcell_array.py b/compiler/modules/bitcell_array.py index c7d8ff81..9d1cc0de 100644 --- a/compiler/modules/bitcell_array.py +++ b/compiler/modules/bitcell_array.py @@ -64,7 +64,11 @@ class bitcell_array(bitcell_base_array): self.cell_inst[row, col]=self.add_inst(name=name, mod=self.cell) self.connect_inst(self.get_bitcell_pins(row, col)) - + + # If it is a "core" cell, it could be trimmed for sim time + if col>0 and col0 and row Date: Tue, 6 Apr 2021 19:01:33 -0700 Subject: [PATCH 17/45] Remove lvs_write from sram --- compiler/sram/sram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/sram/sram.py b/compiler/sram/sram.py index 00fbd4df..599ef666 100644 --- a/compiler/sram/sram.py +++ b/compiler/sram/sram.py @@ -120,7 +120,7 @@ class sram(): start_time = datetime.datetime.now() lvsname = OPTS.output_path + self.s.name + ".lvs.sp" debug.print_raw("LVS: Writing to {0}".format(lvsname)) - self.lvs_write(lvsname) + self.sp_write(lvsname, lvs=True) if not OPTS.netlist_only and OPTS.check_lvsdrc: verify.write_lvs_script(cell_name=self.s.name, gds_name=os.path.basename(gdsname), From 5843aa037c5f72d88053acf8b4c02558b244ed35 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 7 Apr 2021 10:33:48 -0700 Subject: [PATCH 18/45] Update functional test to use spare columns separately. Fix no spare columns data width error. --- compiler/characterizer/functional.py | 58 +++++++++++++++------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/compiler/characterizer/functional.py b/compiler/characterizer/functional.py index 0435d22e..7deee2ef 100644 --- a/compiler/characterizer/functional.py +++ b/compiler/characterizer/functional.py @@ -50,11 +50,10 @@ class functional(simulation): if not self.num_spare_cols: self.num_spare_cols = 0 - if self.num_spare_cols > 0: - debug.error("Functional simulation not debugged with spare columns.") - # FIXME: we need to remember the correct value of the spare columns - self.max_value = 2 ** (self.word_size + self.num_spare_cols) - 1 + self.max_data = 2 ** self.word_size - 1 + self.max_col_data = 2 ** self.num_spare_cols - 1 + self.words_per_row_bits = int(math.log(self.words_per_row) / math.log(2)) # If trim is set, specify the valid addresses self.valid_addresses = set() self.max_address = 2**self.addr_size - 1 + (self.num_spare_rows * self.words_per_row) @@ -81,6 +80,7 @@ class functional(simulation): self.num_cycles = cycles # This is to have ordered keys for random selection self.stored_words = collections.OrderedDict() + self.stored_spares = collections.OrderedDict() self.read_check = [] self.read_results = [] @@ -136,10 +136,11 @@ class functional(simulation): # 1. Write all the write ports first to seed a bunch of locations. for port in self.write_ports: addr = self.gen_addr() - word = self.gen_data() + (word, spare) = self.gen_data() comment = self.gen_cycle_comment("write", word, addr, "1" * self.num_wmasks, port, self.t_current) - self.add_write_one_port(comment, addr, word, "1" * self.num_wmasks, port) + self.add_write_one_port(comment, addr, word + spare, "1" * self.num_wmasks, port) self.stored_words[addr] = word + self.stored_spares[addr[:-self.words_per_row_bits]] = spare # All other read-only ports are noops. for port in self.read_ports: @@ -185,27 +186,31 @@ class functional(simulation): if addr in w_addrs: self.add_noop_one_port(port) else: - word = self.gen_data() + (word, spare) = self.gen_data() comment = self.gen_cycle_comment("write", word, addr, "1" * self.num_wmasks, port, self.t_current) - self.add_write_one_port(comment, addr, word, "1" * self.num_wmasks, port) + self.add_write_one_port(comment, addr, word + spare, "1" * self.num_wmasks, port) self.stored_words[addr] = word + self.stored_spares[addr[:-self.words_per_row_bits]] = spare w_addrs.append(addr) elif op == "partial_write": # write only to a word that's been written to - (addr, old_word) = self.get_data() + (addr, old_word, old_spares) = self.get_data() # two ports cannot write to the same address if addr in w_addrs: self.add_noop_one_port(port) else: - word = self.gen_data() + (word, spare) = self.gen_data() wmask = self.gen_wmask() new_word = self.gen_masked_data(old_word, word, wmask) comment = self.gen_cycle_comment("partial_write", word, addr, wmask, port, self.t_current) - self.add_write_one_port(comment, addr, word, wmask, port) + self.add_write_one_port(comment, addr, word + spare, wmask, port) self.stored_words[addr] = new_word + self.stored_spares[addr[:-self.words_per_row_bits]] = spare w_addrs.append(addr) else: (addr, word) = random.choice(list(self.stored_words.items())) + spare = self.stored_spares[addr[:-self.words_per_row_bits]] + combined_word = word + spare # The write driver is not sized sufficiently to drive through the two # bitcell access transistors to the read port. So, for now, we do not allow # a simultaneous write and read to the same address on different ports. This @@ -213,9 +218,9 @@ class functional(simulation): if addr in w_addrs: self.add_noop_one_port(port) else: - comment = self.gen_cycle_comment("read", word, addr, "0" * self.num_wmasks, port, self.t_current) + comment = self.gen_cycle_comment("read", combined_word, addr, "0" * self.num_wmasks, port, self.t_current) self.add_read_one_port(comment, addr, port) - self.add_read_check(word, port) + self.add_read_check(combined_word, port) self.cycle_times.append(self.t_current) self.t_current += self.period @@ -315,9 +320,14 @@ class functional(simulation): def gen_data(self): """ Generates a random word to write. """ - random_value = random.randint(0, self.max_value) - data_bits = self.convert_to_bin(random_value, False) - return data_bits + random_value = random.randint(0, self.max_data) + data_bits = self.convert_to_bin(random_value, self.word_size) + if self.num_spare_cols>0: + random_value = random.randint(0, self.max_col_data) + spare_bits = self.convert_to_bin(random_value, self.num_spare_cols) + else: + spare_bits = "" + return data_bits, spare_bits def gen_addr(self): """ Generates a random address value to write to. """ @@ -325,7 +335,7 @@ class functional(simulation): random_value = random.sample(self.valid_addresses, 1)[0] else: random_value = random.randint(0, self.max_address) - addr_bits = self.convert_to_bin(random_value, True) + addr_bits = self.convert_to_bin(random_value, self.addr_size) return addr_bits def get_data(self): @@ -333,19 +343,13 @@ class functional(simulation): # Used for write masks since they should be writing to previously written addresses addr = random.choice(list(self.stored_words.keys())) word = self.stored_words[addr] - return (addr, word) + spare = self.stored_spares[addr[:-self.words_per_row_bits]] + return (addr, word, spare) - def convert_to_bin(self, value, is_addr): - """ Converts addr & word to usable binary values. """ + def convert_to_bin(self, value, size): new_value = str.replace(bin(value), "0b", "") - if(is_addr): - expected_value = self.addr_size - else: - expected_value = self.word_size + self.num_spare_cols - for i in range(expected_value - len(new_value)): + for i in range(size - len(new_value)): new_value = "0" + new_value - - # print("Binary Conversion: {} to {}".format(value, new_value)) return new_value def write_functional_stimulus(self): From 229b0059c4035b3a6c0e5a7e6d59e8ce3531222f Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 7 Apr 2021 12:17:16 -0700 Subject: [PATCH 19/45] Add perimeter margin to expand pins outside perimeter for OpenRoad router. --- compiler/router/router.py | 17 ++++++++++------- compiler/router/signal_escape_router.py | 13 +++++++++---- compiler/sram/sram_base.py | 4 +++- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/compiler/router/router.py b/compiler/router/router.py index dc0c8e8d..aa01c71f 100644 --- a/compiler/router/router.py +++ b/compiler/router/router.py @@ -28,7 +28,7 @@ class router(router_tech): route on a given layer. This is limited to two layer routes. It populates blockages on a grid class. """ - def __init__(self, layers, design, gds_filename=None, bbox=None, route_track_width=1): + def __init__(self, layers, design, gds_filename=None, bbox=None, margin=0, route_track_width=1): """ This will instantiate a copy of the gds file or the module at (0,0) and route on top of this. The blockages from the gds/module will be @@ -83,9 +83,11 @@ class router(router_tech): # A list of path blockages (they might be expanded for wide metal DRC) self.path_blockages = [] - self.init_bbox(bbox) + # The perimeter pins should be placed outside the SRAM macro by a distance + self.margin = margin + self.init_bbox(bbox, margin) - def init_bbox(self, bbox=None): + def init_bbox(self, bbox=None, margin=0): """ Initialize the ll,ur values with the paramter or using the layout boundary. """ @@ -99,18 +101,19 @@ class router(router_tech): else: self.ll, self.ur = bbox - self.bbox = (self.ll, self.ur) + margin_offset = vector(margin, margin) + self.bbox = (self.ll - margin_offset, self.ur + margin_offset) size = self.ur - self.ll - debug.info(1, "Size: {0} x {1}".format(size.x, size.y)) + debug.info(1, "Size: {0} x {1} with perimeter margin {2}".format(size.x, size.y, margin)) def get_bbox(self): return self.bbox - def create_routing_grid(self, router_type, bbox=None): + def create_routing_grid(self, router_type): """ Create a sprase routing grid with A* expansion functions. """ - self.init_bbox(bbox) + self.init_bbox(self.bbox, self.margin) self.rg = router_type(self.ll, self.ur, self.track_width) def clear_pins(self): diff --git a/compiler/router/signal_escape_router.py b/compiler/router/signal_escape_router.py index 846925bd..a9531290 100644 --- a/compiler/router/signal_escape_router.py +++ b/compiler/router/signal_escape_router.py @@ -17,12 +17,17 @@ class signal_escape_router(router): A router that routes signals to perimeter and makes pins. """ - def __init__(self, layers, design, bbox=None, gds_filename=None): + def __init__(self, layers, design, bbox=None, margin=0, gds_filename=None): """ This will route on layers in design. It will get the blockages from either the gds file name or the design itself (by saving to a gds file). """ - router.__init__(self, layers, design, gds_filename, bbox) + router.__init__(self, + layers=layers, + design=design, + gds_filename=gds_filename, + bbox=bbox, + margin=margin) def perimeter_dist(self, pin_name): """ @@ -54,8 +59,8 @@ class signal_escape_router(router): start_time = datetime.now() for pin_name in ordered_pin_names: self.route_signal(pin_name) - #if pin_name == "dout1[1]": - # self.write_debug_gds("postroute.gds", False) + # if pin_name == "dout0[1]": + # self.write_debug_gds("postroute.gds", True) print_time("Maze routing pins",datetime.now(), start_time, 3) diff --git a/compiler/sram/sram_base.py b/compiler/sram/sram_base.py index b683dbbc..fa0692e0 100644 --- a/compiler/sram/sram_base.py +++ b/compiler/sram/sram_base.py @@ -306,7 +306,9 @@ class sram_base(design, verilog, lef): pins_to_route.append("spare_wen{0}[{1}]".format(port, bit)) from signal_escape_router import signal_escape_router as router - rtr=router(self.m3_stack, self) + rtr=router(layers=self.m3_stack, + design=self, + margin=4 * self.m3_pitch) rtr.escape_route(pins_to_route) def compute_bus_sizes(self): From 61b1b90dd3f8fe0a7141cb003ef4768cc6adfed8 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 7 Apr 2021 14:23:47 -0700 Subject: [PATCH 20/45] Use built in binary conversion. Improve spare debug output. --- compiler/characterizer/functional.py | 52 ++++++++++++++++------------ 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/compiler/characterizer/functional.py b/compiler/characterizer/functional.py index 7deee2ef..f93de85c 100644 --- a/compiler/characterizer/functional.py +++ b/compiler/characterizer/functional.py @@ -9,6 +9,7 @@ import collections import debug import random import math +from numpy import binary_repr from .stimuli import * from .charutils import * from globals import OPTS @@ -53,7 +54,13 @@ class functional(simulation): self.max_data = 2 ** self.word_size - 1 self.max_col_data = 2 ** self.num_spare_cols - 1 - self.words_per_row_bits = int(math.log(self.words_per_row) / math.log(2)) + if self.words_per_row>1: + # This will truncate bits for word addressing in a row_addr_dff + # This makes one set of spares per row by using top bits of the address + self.addr_spare_index = -int(math.log(self.words_per_row) / math.log(2)) + else: + # This will select the entire address when one word per row + self.addr_spare_index = self.addr_size # If trim is set, specify the valid addresses self.valid_addresses = set() self.max_address = 2**self.addr_size - 1 + (self.num_spare_rows * self.words_per_row) @@ -137,10 +144,11 @@ class functional(simulation): for port in self.write_ports: addr = self.gen_addr() (word, spare) = self.gen_data() - comment = self.gen_cycle_comment("write", word, addr, "1" * self.num_wmasks, port, self.t_current) + combined_word = "{}+{}".format(word, spare) + comment = self.gen_cycle_comment("write", combined_word, addr, "1" * self.num_wmasks, port, self.t_current) self.add_write_one_port(comment, addr, word + spare, "1" * self.num_wmasks, port) self.stored_words[addr] = word - self.stored_spares[addr[:-self.words_per_row_bits]] = spare + self.stored_spares[addr[:self.addr_spare_index]] = spare # All other read-only ports are noops. for port in self.read_ports: @@ -158,7 +166,9 @@ class functional(simulation): if port in self.write_ports: self.add_noop_one_port(port) else: - comment = self.gen_cycle_comment("read", word, addr, "0" * self.num_wmasks, port, self.t_current) + (addr, word, spare) = self.get_data() + combined_word = "{}+{}".format(word, spare) + comment = self.gen_cycle_comment("read", combined_word, addr, "0" * self.num_wmasks, port, self.t_current) self.add_read_one_port(comment, addr, port) self.add_read_check(word, port) self.cycle_times.append(self.t_current) @@ -187,14 +197,15 @@ class functional(simulation): self.add_noop_one_port(port) else: (word, spare) = self.gen_data() - comment = self.gen_cycle_comment("write", word, addr, "1" * self.num_wmasks, port, self.t_current) + combined_word = "{}+{}".format(word, spare) + comment = self.gen_cycle_comment("write", combined_word, addr, "1" * self.num_wmasks, port, self.t_current) self.add_write_one_port(comment, addr, word + spare, "1" * self.num_wmasks, port) self.stored_words[addr] = word - self.stored_spares[addr[:-self.words_per_row_bits]] = spare + self.stored_spares[addr[:self.addr_spare_index]] = spare w_addrs.append(addr) elif op == "partial_write": # write only to a word that's been written to - (addr, old_word, old_spares) = self.get_data() + (addr, old_word, old_spare) = self.get_data() # two ports cannot write to the same address if addr in w_addrs: self.add_noop_one_port(port) @@ -202,15 +213,16 @@ class functional(simulation): (word, spare) = self.gen_data() wmask = self.gen_wmask() new_word = self.gen_masked_data(old_word, word, wmask) - comment = self.gen_cycle_comment("partial_write", word, addr, wmask, port, self.t_current) + combined_word = "{}+{}".format(word, spare) + comment = self.gen_cycle_comment("partial_write", combined_word, addr, wmask, port, self.t_current) self.add_write_one_port(comment, addr, word + spare, wmask, port) self.stored_words[addr] = new_word - self.stored_spares[addr[:-self.words_per_row_bits]] = spare + self.stored_spares[addr[:self.addr_spare_index]] = spare w_addrs.append(addr) else: (addr, word) = random.choice(list(self.stored_words.items())) - spare = self.stored_spares[addr[:-self.words_per_row_bits]] - combined_word = word + spare + spare = self.stored_spares[addr[:self.addr_spare_index]] + combined_word = "{}+{}".format(word, spare) # The write driver is not sized sufficiently to drive through the two # bitcell access transistors to the read port. So, for now, we do not allow # a simultaneous write and read to the same address on different ports. This @@ -220,7 +232,7 @@ class functional(simulation): else: comment = self.gen_cycle_comment("read", combined_word, addr, "0" * self.num_wmasks, port, self.t_current) self.add_read_one_port(comment, addr, port) - self.add_read_check(combined_word, port) + self.add_read_check(word + spare, port) self.cycle_times.append(self.t_current) self.t_current += self.period @@ -286,7 +298,7 @@ class functional(simulation): def check_stim_results(self): for i in range(len(self.read_check)): if self.read_check[i][0] != self.read_results[i][0]: - str = "FAILED: {0} value {1} does not match written value {2} read during cycle {3} at time {4}n" + str = "FAILED: {0} read value {1} does not match written value {2} during cycle {3} at time {4}n" error = str.format(self.read_results[i][1], self.read_results[i][0], self.read_check[i][0], @@ -321,10 +333,10 @@ class functional(simulation): def gen_data(self): """ Generates a random word to write. """ random_value = random.randint(0, self.max_data) - data_bits = self.convert_to_bin(random_value, self.word_size) + data_bits = binary_repr(random_value, self.word_size) if self.num_spare_cols>0: random_value = random.randint(0, self.max_col_data) - spare_bits = self.convert_to_bin(random_value, self.num_spare_cols) + spare_bits = binary_repr(random_value, self.num_spare_cols) else: spare_bits = "" return data_bits, spare_bits @@ -335,7 +347,7 @@ class functional(simulation): random_value = random.sample(self.valid_addresses, 1)[0] else: random_value = random.randint(0, self.max_address) - addr_bits = self.convert_to_bin(random_value, self.addr_size) + addr_bits = binary_repr(random_value, self.addr_size) return addr_bits def get_data(self): @@ -343,15 +355,9 @@ class functional(simulation): # Used for write masks since they should be writing to previously written addresses addr = random.choice(list(self.stored_words.keys())) word = self.stored_words[addr] - spare = self.stored_spares[addr[:-self.words_per_row_bits]] + spare = self.stored_spares[addr[:self.addr_spare_index]] return (addr, word, spare) - def convert_to_bin(self, value, size): - new_value = str.replace(bin(value), "0b", "") - for i in range(size - len(new_value)): - new_value = "0" + new_value - return new_value - def write_functional_stimulus(self): """ Writes SPICE stimulus. """ self.stim_sp = "functional_stim.sp" From b510925bdb3d980eafdcba2068d08a00a51b1f04 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 7 Apr 2021 15:19:12 -0700 Subject: [PATCH 21/45] Enable pruning by default (except on unit tests) --- compiler/options.py | 2 +- compiler/tests/22_psram_1bank_2mux_func_test.py | 3 ++- compiler/tests/22_psram_1bank_4mux_func_test.py | 3 ++- compiler/tests/22_psram_1bank_8mux_func_test.py | 3 ++- compiler/tests/22_psram_1bank_nomux_func_test.py | 3 ++- compiler/tests/22_sram_1bank_2mux_func_test.py | 3 ++- compiler/tests/22_sram_1bank_2mux_global_func_test.py | 3 ++- compiler/tests/22_sram_1bank_2mux_sparecols_func_test.py | 1 + compiler/tests/22_sram_1bank_4mux_func_test.py | 3 ++- compiler/tests/22_sram_1bank_8mux_func_test.py | 3 ++- compiler/tests/22_sram_1bank_nomux_1rw_1r_func_test.py | 2 ++ compiler/tests/22_sram_1bank_nomux_func_test.py | 1 + compiler/tests/22_sram_1bank_nomux_sparecols_func_test.py | 1 + compiler/tests/22_sram_1bank_wmask_1rw_1r_func_test.py | 2 ++ compiler/tests/22_sram_wmask_func_test.py | 1 + 15 files changed, 25 insertions(+), 9 deletions(-) diff --git a/compiler/options.py b/compiler/options.py index 551c3950..67ac14d7 100644 --- a/compiler/options.py +++ b/compiler/options.py @@ -102,7 +102,7 @@ class options(optparse.Values): # This determines whether LVS and DRC is checked for every submodule. inline_lvsdrc = False # Remove noncritical memory cells for characterization speed-up - trim_netlist = False + trim_netlist = True # Run with extracted parasitics use_pex = False # Output config with all options diff --git a/compiler/tests/22_psram_1bank_2mux_func_test.py b/compiler/tests/22_psram_1bank_2mux_func_test.py index ef42fec1..f5f4cd8f 100755 --- a/compiler/tests/22_psram_1bank_2mux_func_test.py +++ b/compiler/tests/22_psram_1bank_2mux_func_test.py @@ -23,7 +23,8 @@ class psram_1bank_2mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + OPTS.bitcell = "pbitcell" OPTS.replica_bitcell="replica_pbitcell" OPTS.dummy_bitcell="dummy_pbitcell" diff --git a/compiler/tests/22_psram_1bank_4mux_func_test.py b/compiler/tests/22_psram_1bank_4mux_func_test.py index 7e4e11b9..f422728b 100755 --- a/compiler/tests/22_psram_1bank_4mux_func_test.py +++ b/compiler/tests/22_psram_1bank_4mux_func_test.py @@ -24,7 +24,8 @@ class psram_1bank_4mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + OPTS.bitcell = "pbitcell" OPTS.replica_bitcell="replica_pbitcell" OPTS.dummy_bitcell="dummy_pbitcell" diff --git a/compiler/tests/22_psram_1bank_8mux_func_test.py b/compiler/tests/22_psram_1bank_8mux_func_test.py index d44f1326..f0247808 100755 --- a/compiler/tests/22_psram_1bank_8mux_func_test.py +++ b/compiler/tests/22_psram_1bank_8mux_func_test.py @@ -24,7 +24,8 @@ class psram_1bank_8mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + OPTS.bitcell = "pbitcell" OPTS.replica_bitcell="replica_pbitcell" OPTS.dummy_bitcell="dummy_pbitcell" diff --git a/compiler/tests/22_psram_1bank_nomux_func_test.py b/compiler/tests/22_psram_1bank_nomux_func_test.py index a6b666d3..203fd0ac 100755 --- a/compiler/tests/22_psram_1bank_nomux_func_test.py +++ b/compiler/tests/22_psram_1bank_nomux_func_test.py @@ -23,7 +23,8 @@ class psram_1bank_nomux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + OPTS.bitcell = "pbitcell" OPTS.replica_bitcell="replica_pbitcell" OPTS.dummy_bitcell="dummy_pbitcell" diff --git a/compiler/tests/22_sram_1bank_2mux_func_test.py b/compiler/tests/22_sram_1bank_2mux_func_test.py index ba9e4129..cca6aeeb 100755 --- a/compiler/tests/22_sram_1bank_2mux_func_test.py +++ b/compiler/tests/22_sram_1bank_2mux_func_test.py @@ -24,7 +24,8 @@ class sram_1bank_2mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload import characterizer diff --git a/compiler/tests/22_sram_1bank_2mux_global_func_test.py b/compiler/tests/22_sram_1bank_2mux_global_func_test.py index f5f875b9..14de7ba8 100755 --- a/compiler/tests/22_sram_1bank_2mux_global_func_test.py +++ b/compiler/tests/22_sram_1bank_2mux_global_func_test.py @@ -24,7 +24,8 @@ class sram_1bank_2mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload import characterizer diff --git a/compiler/tests/22_sram_1bank_2mux_sparecols_func_test.py b/compiler/tests/22_sram_1bank_2mux_sparecols_func_test.py index 2242a002..c84c70a5 100755 --- a/compiler/tests/22_sram_1bank_2mux_sparecols_func_test.py +++ b/compiler/tests/22_sram_1bank_2mux_sparecols_func_test.py @@ -24,6 +24,7 @@ class sram_1bank_2mux_sparecols_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True + OPTS.trim_netlist = False # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload diff --git a/compiler/tests/22_sram_1bank_4mux_func_test.py b/compiler/tests/22_sram_1bank_4mux_func_test.py index 8633f7fa..4f7035cc 100755 --- a/compiler/tests/22_sram_1bank_4mux_func_test.py +++ b/compiler/tests/22_sram_1bank_4mux_func_test.py @@ -24,7 +24,8 @@ class sram_1bank_4mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload import characterizer diff --git a/compiler/tests/22_sram_1bank_8mux_func_test.py b/compiler/tests/22_sram_1bank_8mux_func_test.py index 1f50b579..cc6456cf 100755 --- a/compiler/tests/22_sram_1bank_8mux_func_test.py +++ b/compiler/tests/22_sram_1bank_8mux_func_test.py @@ -24,7 +24,8 @@ class sram_1bank_8mux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True - + OPTS.trim_netlist = False + # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload import characterizer diff --git a/compiler/tests/22_sram_1bank_nomux_1rw_1r_func_test.py b/compiler/tests/22_sram_1bank_nomux_1rw_1r_func_test.py index af465e62..c0518957 100755 --- a/compiler/tests/22_sram_1bank_nomux_1rw_1r_func_test.py +++ b/compiler/tests/22_sram_1bank_nomux_1rw_1r_func_test.py @@ -23,6 +23,8 @@ class psram_1bank_nomux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True + OPTS.trim_netlist = False + OPTS.num_rw_ports = 1 OPTS.num_w_ports = 0 OPTS.num_r_ports = 1 diff --git a/compiler/tests/22_sram_1bank_nomux_func_test.py b/compiler/tests/22_sram_1bank_nomux_func_test.py index 4c2910f5..ff33ead5 100755 --- a/compiler/tests/22_sram_1bank_nomux_func_test.py +++ b/compiler/tests/22_sram_1bank_nomux_func_test.py @@ -24,6 +24,7 @@ class sram_1bank_nomux_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True + OPTS.trim_netlist = False # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload diff --git a/compiler/tests/22_sram_1bank_nomux_sparecols_func_test.py b/compiler/tests/22_sram_1bank_nomux_sparecols_func_test.py index eec158d6..0d649ace 100755 --- a/compiler/tests/22_sram_1bank_nomux_sparecols_func_test.py +++ b/compiler/tests/22_sram_1bank_nomux_sparecols_func_test.py @@ -24,6 +24,7 @@ class sram_1bank_nomux_sparecols_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True + OPTS.trim_netlist = False # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload diff --git a/compiler/tests/22_sram_1bank_wmask_1rw_1r_func_test.py b/compiler/tests/22_sram_1bank_wmask_1rw_1r_func_test.py index 775597e1..6808462e 100755 --- a/compiler/tests/22_sram_1bank_wmask_1rw_1r_func_test.py +++ b/compiler/tests/22_sram_1bank_wmask_1rw_1r_func_test.py @@ -23,6 +23,8 @@ class sram_wmask_1w_1r_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True + OPTS.trim_netlist = False + OPTS.num_rw_ports = 1 OPTS.num_w_ports = 0 OPTS.num_r_ports = 1 diff --git a/compiler/tests/22_sram_wmask_func_test.py b/compiler/tests/22_sram_wmask_func_test.py index 3927bca2..f41b3d98 100755 --- a/compiler/tests/22_sram_wmask_func_test.py +++ b/compiler/tests/22_sram_wmask_func_test.py @@ -24,6 +24,7 @@ class sram_wmask_func_test(openram_test): globals.init_openram(config_file) OPTS.analytical_delay = False OPTS.netlist_only = True + OPTS.trim_netlist = False # This is a hack to reload the characterizer __init__ with the spice version from importlib import reload From e706f776ebe98b052621461b6779b186f856368f Mon Sep 17 00:00:00 2001 From: mrg Date: Tue, 13 Apr 2021 16:24:13 -0700 Subject: [PATCH 22/45] Offset macro to 0,0 which was accidentally comented by a PR --- compiler/sram/sram_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/sram/sram_base.py b/compiler/sram/sram_base.py index fa0692e0..753200b8 100644 --- a/compiler/sram/sram_base.py +++ b/compiler/sram/sram_base.py @@ -196,7 +196,7 @@ class sram_base(design, verilog, lef): self.add_lvs_correspondence_points() - # self.offset_all_coordinates() + self.offset_all_coordinates() highest_coord = self.find_highest_coords() self.width = highest_coord[0] From 0e48e020c1764d89fe20168ec89ac57fed00a759 Mon Sep 17 00:00:00 2001 From: mrg Date: Tue, 13 Apr 2021 16:24:28 -0700 Subject: [PATCH 23/45] Use pins in computing bbox offsets --- compiler/base/hierarchy_layout.py | 61 ++++++++++++++----------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/compiler/base/hierarchy_layout.py b/compiler/base/hierarchy_layout.py index fad5c8ae..f10280aa 100644 --- a/compiler/base/hierarchy_layout.py +++ b/compiler/base/hierarchy_layout.py @@ -15,6 +15,7 @@ from tech import layer_indices from tech import layer_stacks from tech import preferred_directions import os +import sys from globals import OPTS from vector import vector from pin_layout import pin_layout @@ -111,52 +112,44 @@ class layout(): Finds the lowest set of 2d cartesian coordinates within this layout """ + lowestx = lowesty = sys.maxsize if len(self.objs) > 0: - lowestx1 = min(obj.lx() for obj in self.objs if obj.name != "label") - lowesty1 = min(obj.by() for obj in self.objs if obj.name != "label") - else: - lowestx1 = lowesty1 = None + lowestx = min(min(obj.lx() for obj in self.objs if obj.name != "label"), lowestx) + lowesty = min(min(obj.by() for obj in self.objs if obj.name != "label"), lowesty) + if len(self.insts) > 0: - lowestx2 = min(inst.lx() for inst in self.insts) - lowesty2 = min(inst.by() for inst in self.insts) - else: - lowestx2 = lowesty2 = None + lowestx = min(min(inst.lx() for inst in self.insts), lowestx) + lowesty = min(min(inst.by() for inst in self.insts), lowesty) - if lowestx1 == None and lowestx2 == None: - return None - elif lowestx1 == None: - return vector(lowestx2, lowesty2) - elif lowestx2 == None: - return vector(lowestx1, lowesty1) - else: - return vector(min(lowestx1, lowestx2), min(lowesty1, lowesty2)) + if len(self.pin_map) > 0: + for pin_set in self.pin_map.values(): + lowestx = min(min(pin.lx() for pin in pin_set), lowestx) + lowesty = min(min(pin.by() for pin in pin_set), lowesty) + + return vector(lowestx, lowesty) def find_highest_coords(self): """ Finds the highest set of 2d cartesian coordinates within this layout """ + highestx = highesty = -sys.maxsize - 1 + if len(self.objs) > 0: - highestx1 = max(obj.rx() for obj in self.objs if obj.name != "label") - highesty1 = max(obj.uy() for obj in self.objs if obj.name != "label") - else: - highestx1 = highesty1 = None - if len(self.insts) > 0: - highestx2 = max(inst.rx() for inst in self.insts) - highesty2 = max(inst.uy() for inst in self.insts) - else: - highestx2 = highesty2 = None + highestx = max(max(obj.rx() for obj in self.objs if obj.name != "label"), highestx) + highesty = max(max(obj.uy() for obj in self.objs if obj.name != "label"), highesty) - if highestx1 == None and highestx2 == None: - return None - elif highestx1 == None: - return vector(highestx2, highesty2) - elif highestx2 == None: - return vector(highestx1, highesty1) - else: - return vector(max(highestx1, highestx2), - max(highesty1, highesty2)) + if len(self.insts) > 0: + highestx = max(max(inst.rx() for inst in self.insts), highestx) + highesty = max(max(inst.uy() for inst in self.insts), highesty) + + if len(self.pin_map) > 0: + for pin_set in self.pin_map.values(): + highestx = max(max(pin.rx() for pin in pin_set), highestx) + highesty = max(max(pin.uy() for pin in pin_set), highesty) + + return vector(highestx, highesty) def find_highest_layer_coords(self, layer): """ From a730fd0f10768171e165125f2b884a89c7fcee2a Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 14 Apr 2021 10:01:43 -0700 Subject: [PATCH 24/45] Use magic for LEF abstract. Fix supply perimter pin. --- compiler/base/hierarchy_layout.py | 4 +++ compiler/base/lef.py | 51 +++++++++++++++++++++++++++++-- compiler/router/router.py | 3 +- compiler/sram/sram_base.py | 18 +++++++---- 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/compiler/base/hierarchy_layout.py b/compiler/base/hierarchy_layout.py index f10280aa..36a54937 100644 --- a/compiler/base/hierarchy_layout.py +++ b/compiler/base/hierarchy_layout.py @@ -124,6 +124,8 @@ class layout(): if len(self.pin_map) > 0: for pin_set in self.pin_map.values(): + if len(pin_set) == 0: + continue lowestx = min(min(pin.lx() for pin in pin_set), lowestx) lowesty = min(min(pin.by() for pin in pin_set), lowesty) @@ -146,6 +148,8 @@ class layout(): if len(self.pin_map) > 0: for pin_set in self.pin_map.values(): + if len(pin_set) == 0: + continue highestx = max(max(pin.rx() for pin in pin_set), highestx) highesty = max(max(pin.uy() for pin in pin_set), highesty) diff --git a/compiler/base/lef.py b/compiler/base/lef.py index 942a8ac2..8ca4918b 100644 --- a/compiler/base/lef.py +++ b/compiler/base/lef.py @@ -7,6 +7,9 @@ # import debug from tech import layer_names +import os +import shutil +from globals import OPTS class lef: @@ -23,9 +26,53 @@ class lef: # Round to ensure float values are divisible by 0.0025 (the manufacturing grid) self.round_grid = 4 + def magic_lef_write(self, lef_name): + """ Use a magic script to perform LEF creation. """ + debug.info(3, "Writing abstracted LEF to {0}".format(lef_name)) + + # Copy .magicrc file into the output directory + magic_file = OPTS.openram_tech + "tech/.magicrc" + if os.path.exists(magic_file): + shutil.copy(magic_file, OPTS.openram_temp) + else: + debug.warning("Could not locate .magicrc file: {}".format(magic_file)) + + gds_name = OPTS.openram_temp + "{}.gds".format(self.name) + self.gds_write(gds_name) + + run_file = OPTS.openram_temp + "run_lef.sh" + f = open(run_file, "w") + f.write("#!/bin/sh\n") + f.write('export OPENRAM_TECH="{}"\n'.format(os.environ['OPENRAM_TECH'])) + f.write('echo "$(date): Starting GDS to MAG using Magic {}"\n'.format(OPTS.drc_exe[1])) + f.write('\n') + f.write("{} -dnull -noconsole << EOF\n".format(OPTS.drc_exe[1])) + f.write("drc off\n") + f.write("gds polygon subcell true\n") + f.write("gds warning default\n") + f.write("gds flatten true\n") + f.write("gds ordering true\n") + f.write("gds readonly true\n") + f.write("gds read {}\n".format(gds_name)) + f.write('puts "Finished reading gds {}"\n'.format(gds_name)) + f.write("load {}\n".format(self.name)) + f.write('puts "Finished loading cell {}"\n'.format(self.name)) + f.write("cellname delete \\(UNNAMED\\)\n") + f.write("lef write {} -hide\n".format(lef_name)) + f.write('puts "Finished writing LEF cell {}"\n'.format(self.name)) + f.close() + os.system("chmod u+x {}".format(run_file)) + from run_script import run_script + (outfile, errfile, resultsfile) = run_script(self.name, "lef") + def lef_write(self, lef_name): - """Write the entire lef of the object to the file.""" - debug.info(3, "Writing to {0}".format(lef_name)) + """ Write the entire lef of the object to the file. """ + + if OPTS.drc_exe[0] == "magic": + self.magic_lef_write(lef_name) + return + + debug.info(3, "Writing detailed LEF to {0}".format(lef_name)) self.indent = "" # To maintain the indent level easily diff --git a/compiler/router/router.py b/compiler/router/router.py index aa01c71f..5213af4a 100644 --- a/compiler/router/router.py +++ b/compiler/router/router.py @@ -1215,8 +1215,9 @@ class router(router_tech): return None - def get_pin(self, pin_name): + def get_ll_pin(self, pin_name): """ Return the lowest, leftest pin group """ + keep_pin = None for index,pg in enumerate(self.pin_groups[pin_name]): for pin in pg.enclosures: diff --git a/compiler/sram/sram_base.py b/compiler/sram/sram_base.py index 753200b8..6dacdd90 100644 --- a/compiler/sram/sram_base.py +++ b/compiler/sram/sram_base.py @@ -248,21 +248,27 @@ class sram_base(design, verilog, lef): # Find the lowest leftest pin for vdd and gnd for pin_name in ["vdd", "gnd"]: - # Copy the pin shape to rectangles + # Copy the pin shape(s) to rectangles for pin in self.get_pins(pin_name): self.add_rect(pin.layer, pin.ll(), pin.width(), pin.height()) - # Remove the pins + + # Remove the pin shape(s) self.remove_layout_pin(pin_name) - pin = rtr.get_pin(pin_name) - + # Get the lowest, leftest pin + pin = rtr.get_ll_pin(pin_name) + + # Add it as an IO pin to the perimeter + lowest_coord = self.find_lowest_coords() + pin_width = pin.rx() - lowest_coord.x + pin_offset = vector(lowest_coord.x, pin.by()) self.add_layout_pin(pin_name, pin.layer, - pin.ll(), - pin.width(), + pin_offset, + pin_width, pin.height()) def route_escape_pins(self): From 3eed6bb8ff0fc2c6761064c63f9aadc23c8db4aa Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 14 Apr 2021 11:07:38 -0700 Subject: [PATCH 25/45] Check for None before checking DRC tool --- compiler/base/lef.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/base/lef.py b/compiler/base/lef.py index 8ca4918b..a5c1910a 100644 --- a/compiler/base/lef.py +++ b/compiler/base/lef.py @@ -68,7 +68,7 @@ class lef: def lef_write(self, lef_name): """ Write the entire lef of the object to the file. """ - if OPTS.drc_exe[0] == "magic": + if OPTS.drc_exe and OPTS.drc_exe[0] == "magic": self.magic_lef_write(lef_name) return From 41226087bac97962b590037c649fb6ae362ac0b6 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 14 Apr 2021 13:55:21 -0700 Subject: [PATCH 26/45] Use separate mXp pin layer if it exists --- compiler/base/pin_layout.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/compiler/base/pin_layout.py b/compiler/base/pin_layout.py index eb4cb2ad..e8c6f0a5 100644 --- a/compiler/base/pin_layout.py +++ b/compiler/base/pin_layout.py @@ -369,11 +369,23 @@ class pin_layout: debug.info(4, "writing pin (" + str(self.layer) + "):" + str(self.width()) + "x" + str(self.height()) + " @ " + str(self.ll())) - (layer_num, purpose) = layer[self.layer] + + # Try to use the pin layer if it exists, otherwise + # use the regular layer try: - from tech import pin_purpose + (pin_layer_num, pin_purpose) = layer[self.layer + "p"] + except KeyError: + (pin_layer_num, pin_purpose) = layer[self.layer] + (layer_num, purpose) = layer[self.layer] + + # Try to use a global pin purpose if it exists, + # otherwise, use the regular purpose + try: + from tech import pin_purpose as global_pin_purpose + pin_purpose = global_pin_purpose except ImportError: - pin_purpose = purpose + pass + try: from tech import label_purpose except ImportError: @@ -385,9 +397,9 @@ class pin_layout: width=self.width(), height=self.height(), center=False) - # Draw a second pin shape too - if pin_purpose != purpose: - newLayout.addBox(layerNumber=layer_num, + # Draw a second pin shape too if it is different + if not self.same_lpp((pin_layer_num, pin_purpose), (layer_num, purpose)): + newLayout.addBox(layerNumber=pin_layer_num, purposeNumber=pin_purpose, offsetInMicrons=self.ll(), width=self.width(), From 1d657abebc82d5939d2c1be45e0c5704e5ca8c47 Mon Sep 17 00:00:00 2001 From: Olof Kindgren Date: Thu, 15 Apr 2021 22:33:57 +0200 Subject: [PATCH 27/45] Add VERBOSE parameter to generated verilog model This allows disabling the $display commands that are generated for every read and write access to the model. The verilog output has been tested with the following example script from compiler.base.verilog import verilog v = verilog() v.num_words = 256 v.word_size = 32 v.write_size = 8 v.name = "sky130_sram_1kbyte_1rw1r_32x256_8" v.all_ports = [0,1] v.readwrite_ports = [0] v.read_ports = [0,1] v.write_ports = [0] v.addr_size=8 v.verilog_write("mymodule.v") Signed-off-by: Olof Kindgren --- compiler/base/verilog.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/compiler/base/verilog.py b/compiler/base/verilog.py index 989d9d9c..00ca1610 100644 --- a/compiler/base/verilog.py +++ b/compiler/base/verilog.py @@ -61,6 +61,7 @@ class verilog: self.vf.write(" parameter RAM_DEPTH = 1 << ADDR_WIDTH;\n") self.vf.write(" // FIXME: This delay is arbitrary.\n") self.vf.write(" parameter DELAY = 3 ;\n") + self.vf.write(" parameter VERBOSE = 1 ; //Set to 0 to only display warnings\n") self.vf.write("\n") for port in self.all_ports: @@ -130,19 +131,19 @@ class verilog: if port in self.read_ports: self.vf.write(" dout{0} = {1}'bx;\n".format(port, self.word_size)) if port in self.readwrite_ports: - self.vf.write(" if ( !csb{0}_reg && web{0}_reg ) \n".format(port)) + self.vf.write(" if ( !csb{0}_reg && web{0}_reg && VERBOSE ) \n".format(port)) self.vf.write(" $display($time,\" Reading %m addr{0}=%b dout{0}=%b\",addr{0}_reg,mem[addr{0}_reg]);\n".format(port)) elif port in self.read_ports: - self.vf.write(" if ( !csb{0}_reg ) \n".format(port)) + self.vf.write(" if ( !csb{0}_reg && VERBOSE ) \n".format(port)) self.vf.write(" $display($time,\" Reading %m addr{0}=%b dout{0}=%b\",addr{0}_reg,mem[addr{0}_reg]);\n".format(port)) if port in self.readwrite_ports: - self.vf.write(" if ( !csb{0}_reg && !web{0}_reg )\n".format(port)) + self.vf.write(" if ( !csb{0}_reg && !web{0}_reg && VERBOSE )\n".format(port)) if self.write_size: self.vf.write(" $display($time,\" Writing %m addr{0}=%b din{0}=%b wmask{0}=%b\",addr{0}_reg,din{0}_reg,wmask{0}_reg);\n".format(port)) else: self.vf.write(" $display($time,\" Writing %m addr{0}=%b din{0}=%b\",addr{0}_reg,din{0}_reg);\n".format(port)) elif port in self.write_ports: - self.vf.write(" if ( !csb{0}_reg )\n".format(port)) + self.vf.write(" if ( !csb{0}_reg && VERBOSE )\n".format(port)) if self.write_size: self.vf.write(" $display($time,\" Writing %m addr{0}=%b din{0}=%b wmask{0}=%b\",addr{0}_reg,din{0}_reg,wmask{0}_reg);\n".format(port)) else: From 688a1f1e6076305f706dac6756c2f844d90a8c66 Mon Sep 17 00:00:00 2001 From: Olof Kindgren Date: Thu, 15 Apr 2021 22:39:49 +0200 Subject: [PATCH 28/45] Add HOLD_DELAY parameter for dout in verilog model Signed-off-by: Olof Kindgren --- compiler/base/verilog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compiler/base/verilog.py b/compiler/base/verilog.py index 00ca1610..a1eab007 100644 --- a/compiler/base/verilog.py +++ b/compiler/base/verilog.py @@ -62,6 +62,7 @@ class verilog: self.vf.write(" // FIXME: This delay is arbitrary.\n") self.vf.write(" parameter DELAY = 3 ;\n") self.vf.write(" parameter VERBOSE = 1 ; //Set to 0 to only display warnings\n") + self.vf.write(" parameter T_HOLD = 1 ; //Delay to hold dout value after posedge. Value is arbitrary\n") self.vf.write("\n") for port in self.all_ports: @@ -129,7 +130,7 @@ class verilog: if port in self.write_ports: self.vf.write(" din{0}_reg = din{0};\n".format(port)) if port in self.read_ports: - self.vf.write(" dout{0} = {1}'bx;\n".format(port, self.word_size)) + self.vf.write(" #(T_HOLD) dout{0} = {1}'bx;\n".format(port, self.word_size)) if port in self.readwrite_ports: self.vf.write(" if ( !csb{0}_reg && web{0}_reg && VERBOSE ) \n".format(port)) self.vf.write(" $display($time,\" Reading %m addr{0}=%b dout{0}=%b\",addr{0}_reg,mem[addr{0}_reg]);\n".format(port)) From 5b556e6ef575bf863a2260f10c41b5244c66e0c6 Mon Sep 17 00:00:00 2001 From: mrg Date: Thu, 15 Apr 2021 15:48:20 -0700 Subject: [PATCH 29/45] Update unit test results with new Verilog models. --- compiler/tests/golden/sram_2_16_1_freepdk45.v | 8 +++++--- compiler/tests/golden/sram_2_16_1_scn4m_subm.v | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/compiler/tests/golden/sram_2_16_1_freepdk45.v b/compiler/tests/golden/sram_2_16_1_freepdk45.v index 5edd2fee..46f89fa5 100644 --- a/compiler/tests/golden/sram_2_16_1_freepdk45.v +++ b/compiler/tests/golden/sram_2_16_1_freepdk45.v @@ -12,6 +12,8 @@ module sram_2_16_1_freepdk45( parameter RAM_DEPTH = 1 << ADDR_WIDTH; // FIXME: This delay is arbitrary. parameter DELAY = 3 ; + parameter VERBOSE = 1 ; //Set to 0 to only display warnings + parameter T_HOLD = 1 ; //Delay to hold dout value after posedge. Value is arbitrary input clk0; // clock input csb0; // active low chip select @@ -33,10 +35,10 @@ module sram_2_16_1_freepdk45( web0_reg = web0; addr0_reg = addr0; din0_reg = din0; - dout0 = 2'bx; - if ( !csb0_reg && web0_reg ) + #(T_HOLD) dout0 = 2'bx; + if ( !csb0_reg && web0_reg && VERBOSE ) $display($time," Reading %m addr0=%b dout0=%b",addr0_reg,mem[addr0_reg]); - if ( !csb0_reg && !web0_reg ) + if ( !csb0_reg && !web0_reg && VERBOSE ) $display($time," Writing %m addr0=%b din0=%b",addr0_reg,din0_reg); end diff --git a/compiler/tests/golden/sram_2_16_1_scn4m_subm.v b/compiler/tests/golden/sram_2_16_1_scn4m_subm.v index cec47c19..cb95036b 100644 --- a/compiler/tests/golden/sram_2_16_1_scn4m_subm.v +++ b/compiler/tests/golden/sram_2_16_1_scn4m_subm.v @@ -12,6 +12,8 @@ module sram_2_16_1_scn4m_subm( parameter RAM_DEPTH = 1 << ADDR_WIDTH; // FIXME: This delay is arbitrary. parameter DELAY = 3 ; + parameter VERBOSE = 1 ; //Set to 0 to only display warnings + parameter T_HOLD = 1 ; //Delay to hold dout value after posedge. Value is arbitrary input clk0; // clock input csb0; // active low chip select @@ -33,10 +35,10 @@ module sram_2_16_1_scn4m_subm( web0_reg = web0; addr0_reg = addr0; din0_reg = din0; - dout0 = 2'bx; - if ( !csb0_reg && web0_reg ) + #(T_HOLD) dout0 = 2'bx; + if ( !csb0_reg && web0_reg && VERBOSE ) $display($time," Reading %m addr0=%b dout0=%b",addr0_reg,mem[addr0_reg]); - if ( !csb0_reg && !web0_reg ) + if ( !csb0_reg && !web0_reg && VERBOSE ) $display($time," Writing %m addr0=%b din0=%b",addr0_reg,din0_reg); end From 3def8959605bb18d1773cf05ae0623914f403c9b Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 09:31:18 -0700 Subject: [PATCH 30/45] Respect the bus spacing parameter in predecoder. --- compiler/modules/hierarchical_predecode.py | 77 ++++++++++++---------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/compiler/modules/hierarchical_predecode.py b/compiler/modules/hierarchical_predecode.py index c2ed5949..f83516d2 100644 --- a/compiler/modules/hierarchical_predecode.py +++ b/compiler/modules/hierarchical_predecode.py @@ -85,7 +85,6 @@ class hierarchical_predecode(design.design): self.bus_layer = layer_props.hierarchical_predecode.bus_layer self.bus_directions = layer_props.hierarchical_predecode.bus_directions - if self.column_decoder: # Column decoders may be routed on M2/M3 if there's a write mask self.bus_pitch = self.m3_pitch @@ -119,7 +118,8 @@ class hierarchical_predecode(design.design): self.input_rails = self.create_vertical_bus(layer=self.bus_layer, offset=offset, names=input_names, - length=self.height - 2 * self.bus_pitch) + length=self.height - 2 * self.bus_pitch, + pitch=self.bus_pitch) invert_names = ["Abar_{}".format(x) for x in range(self.number_of_inputs)] non_invert_names = ["A_{}".format(x) for x in range(self.number_of_inputs)] @@ -128,7 +128,8 @@ class hierarchical_predecode(design.design): self.decode_rails = self.create_vertical_bus(layer=self.bus_layer, offset=offset, names=decode_names, - length=self.height - 2 * self.bus_pitch) + length=self.height - 2 * self.bus_pitch, + pitch=self.bus_pitch) def create_input_inverters(self): """ Create the input inverters to invert input signals for the decode stage. """ @@ -180,10 +181,12 @@ class hierarchical_predecode(design.design): mirror=mirror) def route(self): + self.route_input_inverters() + self.route_output_inverters() self.route_inputs_to_rails() - self.route_and_to_rails() - self.route_output_and() + self.route_input_ands() + self.route_output_ands() self.route_vdd_gnd() def route_inputs_to_rails(self): @@ -215,7 +218,7 @@ class hierarchical_predecode(design.design): to_layer=self.bus_layer, offset=[self.decode_rails[a_pin].cx(), y_offset]) - def route_output_and(self): + def route_output_ands(self): """ Route all conections of the outputs and gates """ @@ -230,12 +233,40 @@ class hierarchical_predecode(design.design): def route_input_inverters(self): """ - Route all conections of the inputs inverters [Inputs, outputs, vdd, gnd] + Route all conections of the inverter inputs + """ + for inv_num in range(self.number_of_inputs): + in_pin = "in_{}".format(inv_num) + + # route input + pin = self.inv_inst[inv_num].get_pin("A") + inv_in_pos = pin.center() + in_pos = vector(self.input_rails[in_pin].cx(), inv_in_pos.y) + self.add_path(self.input_layer, [in_pos, inv_in_pos]) + + # Inverter input pin + self.add_via_stack_center(from_layer=pin.layer, + to_layer=self.input_layer, + offset=inv_in_pos) + # Input rail pin position + via=self.add_via_stack_center(from_layer=self.input_layer, + to_layer=self.bus_layer, + offset=in_pos, + directions=self.bus_directions) + + # Create the input pin at this location on the rail + self.add_layout_pin_rect_center(text=in_pin, + layer=self.bus_layer, + offset=in_pos, + height=via.mod.second_layer_height, + width=via.mod.second_layer_width) + + def route_output_inverters(self): + """ + Route all conections of the inverter outputs """ for inv_num in range(self.number_of_inputs): out_pin = "Abar_{}".format(inv_num) - in_pin = "in_{}".format(inv_num) - inv_out_pin = self.inv_inst[inv_num].get_pin("Z") # add output so that it is just below the vdd or gnd rail @@ -255,31 +286,11 @@ class hierarchical_predecode(design.design): offset=rail_pos, directions=self.bus_directions) - # route input - pin = self.inv_inst[inv_num].get_pin("A") - inv_in_pos = pin.center() - in_pos = vector(self.input_rails[in_pin].cx(), inv_in_pos.y) - self.add_path(self.input_layer, [in_pos, inv_in_pos]) - self.add_via_stack_center(from_layer=pin.layer, - to_layer=self.input_layer, - offset=inv_in_pos) - via=self.add_via_stack_center(from_layer=self.input_layer, - to_layer=self.bus_layer, - offset=in_pos) - # Create the input pin at this location on the rail - self.add_layout_pin_rect_center(text=in_pin, - layer=self.bus_layer, - offset=in_pos, - height=via.mod.second_layer_height, - width=via.mod.second_layer_width) + def route_input_ands(self): + """ + Route the different permutations of the NAND/AND decocer cells. + """ - # This is a hack to fix via-to-via spacing issues, but it is currently - # causing its own DRC problems. - # if layer_props.hierarchical_predecode.vertical_supply: - # below_rail = vector(self.decode_rails[out_pin].cx(), y_offset - (self.cell_height / 2)) - # self.add_path(self.bus_layer, [rail_pos, below_rail], width=self.li_width + self.m1_enclose_mcon * 2) - - def route_and_to_rails(self): # This 2D array defines the connection mapping and_input_line_combination = self.get_and_input_line_combination() for k in range(self.number_of_outputs): From 439003e203af3d1bea01affa57e6ca15f82dd4e6 Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 09:31:18 -0700 Subject: [PATCH 31/45] Respect the bus spacing parameter in predecoder. --- compiler/modules/hierarchical_predecode.py | 77 ++++++++++++---------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/compiler/modules/hierarchical_predecode.py b/compiler/modules/hierarchical_predecode.py index c2ed5949..f83516d2 100644 --- a/compiler/modules/hierarchical_predecode.py +++ b/compiler/modules/hierarchical_predecode.py @@ -85,7 +85,6 @@ class hierarchical_predecode(design.design): self.bus_layer = layer_props.hierarchical_predecode.bus_layer self.bus_directions = layer_props.hierarchical_predecode.bus_directions - if self.column_decoder: # Column decoders may be routed on M2/M3 if there's a write mask self.bus_pitch = self.m3_pitch @@ -119,7 +118,8 @@ class hierarchical_predecode(design.design): self.input_rails = self.create_vertical_bus(layer=self.bus_layer, offset=offset, names=input_names, - length=self.height - 2 * self.bus_pitch) + length=self.height - 2 * self.bus_pitch, + pitch=self.bus_pitch) invert_names = ["Abar_{}".format(x) for x in range(self.number_of_inputs)] non_invert_names = ["A_{}".format(x) for x in range(self.number_of_inputs)] @@ -128,7 +128,8 @@ class hierarchical_predecode(design.design): self.decode_rails = self.create_vertical_bus(layer=self.bus_layer, offset=offset, names=decode_names, - length=self.height - 2 * self.bus_pitch) + length=self.height - 2 * self.bus_pitch, + pitch=self.bus_pitch) def create_input_inverters(self): """ Create the input inverters to invert input signals for the decode stage. """ @@ -180,10 +181,12 @@ class hierarchical_predecode(design.design): mirror=mirror) def route(self): + self.route_input_inverters() + self.route_output_inverters() self.route_inputs_to_rails() - self.route_and_to_rails() - self.route_output_and() + self.route_input_ands() + self.route_output_ands() self.route_vdd_gnd() def route_inputs_to_rails(self): @@ -215,7 +218,7 @@ class hierarchical_predecode(design.design): to_layer=self.bus_layer, offset=[self.decode_rails[a_pin].cx(), y_offset]) - def route_output_and(self): + def route_output_ands(self): """ Route all conections of the outputs and gates """ @@ -230,12 +233,40 @@ class hierarchical_predecode(design.design): def route_input_inverters(self): """ - Route all conections of the inputs inverters [Inputs, outputs, vdd, gnd] + Route all conections of the inverter inputs + """ + for inv_num in range(self.number_of_inputs): + in_pin = "in_{}".format(inv_num) + + # route input + pin = self.inv_inst[inv_num].get_pin("A") + inv_in_pos = pin.center() + in_pos = vector(self.input_rails[in_pin].cx(), inv_in_pos.y) + self.add_path(self.input_layer, [in_pos, inv_in_pos]) + + # Inverter input pin + self.add_via_stack_center(from_layer=pin.layer, + to_layer=self.input_layer, + offset=inv_in_pos) + # Input rail pin position + via=self.add_via_stack_center(from_layer=self.input_layer, + to_layer=self.bus_layer, + offset=in_pos, + directions=self.bus_directions) + + # Create the input pin at this location on the rail + self.add_layout_pin_rect_center(text=in_pin, + layer=self.bus_layer, + offset=in_pos, + height=via.mod.second_layer_height, + width=via.mod.second_layer_width) + + def route_output_inverters(self): + """ + Route all conections of the inverter outputs """ for inv_num in range(self.number_of_inputs): out_pin = "Abar_{}".format(inv_num) - in_pin = "in_{}".format(inv_num) - inv_out_pin = self.inv_inst[inv_num].get_pin("Z") # add output so that it is just below the vdd or gnd rail @@ -255,31 +286,11 @@ class hierarchical_predecode(design.design): offset=rail_pos, directions=self.bus_directions) - # route input - pin = self.inv_inst[inv_num].get_pin("A") - inv_in_pos = pin.center() - in_pos = vector(self.input_rails[in_pin].cx(), inv_in_pos.y) - self.add_path(self.input_layer, [in_pos, inv_in_pos]) - self.add_via_stack_center(from_layer=pin.layer, - to_layer=self.input_layer, - offset=inv_in_pos) - via=self.add_via_stack_center(from_layer=self.input_layer, - to_layer=self.bus_layer, - offset=in_pos) - # Create the input pin at this location on the rail - self.add_layout_pin_rect_center(text=in_pin, - layer=self.bus_layer, - offset=in_pos, - height=via.mod.second_layer_height, - width=via.mod.second_layer_width) + def route_input_ands(self): + """ + Route the different permutations of the NAND/AND decocer cells. + """ - # This is a hack to fix via-to-via spacing issues, but it is currently - # causing its own DRC problems. - # if layer_props.hierarchical_predecode.vertical_supply: - # below_rail = vector(self.decode_rails[out_pin].cx(), y_offset - (self.cell_height / 2)) - # self.add_path(self.bus_layer, [rail_pos, below_rail], width=self.li_width + self.m1_enclose_mcon * 2) - - def route_and_to_rails(self): # This 2D array defines the connection mapping and_input_line_combination = self.get_and_input_line_combination() for k in range(self.number_of_outputs): From 9b40102bbb6386d852e84e71d99610e61fb944b3 Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 11:54:35 -0700 Subject: [PATCH 32/45] v1.1.15 --- compiler/globals.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/globals.py b/compiler/globals.py index 0df210ca..68d055ff 100644 --- a/compiler/globals.py +++ b/compiler/globals.py @@ -22,7 +22,7 @@ import getpass import subprocess -VERSION = "1.1.14" +VERSION = "1.1.15" NAME = "OpenRAM v{}".format(VERSION) USAGE = "openram.py [options] \nUse -h for help.\n" From 23aa69ec4012ce48933374e0a4c826cb0024c13a Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 13:24:18 -0700 Subject: [PATCH 33/45] Add logo and remove master text from README. --- README.md | 5 +- images/OpenRAM_logo_yellow_transparent.png | Bin 0 -> 23916 bytes images/OpenRAM_logo_yellow_transparent.svg | 220 +++++++++++++++++++++ 3 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 images/OpenRAM_logo_yellow_transparent.png create mode 100644 images/OpenRAM_logo_yellow_transparent.svg diff --git a/README.md b/README.md index e459644a..b6d5398e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,9 @@ +![](./images/OpenRAM_logo_yellow_transparent.svg) # OpenRAM [![Python 3.5](https://img.shields.io/badge/Python-3.5-green.svg)](https://www.python.org/) [![License: BSD 3-clause](./images/license_badge.svg)](./LICENSE) - -Master: [![Download](./images/download-stable-blue.svg)](https://github.com/VLSIDA/OpenRAM/archive/master.zip) - -Dev: [![Download](./images/download-unstable-blue.svg)](https://github.com/VLSIDA/OpenRAM/archive/dev.zip) An open-source static random access memory (SRAM) compiler. diff --git a/images/OpenRAM_logo_yellow_transparent.png b/images/OpenRAM_logo_yellow_transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..17ce794f7af86b99683f0d6d504e5d5faf1ec70a GIT binary patch literal 23916 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 literal 0 HcmV?d00001 diff --git a/images/OpenRAM_logo_yellow_transparent.svg b/images/OpenRAM_logo_yellow_transparent.svg new file mode 100644 index 00000000..bb02ae6c --- /dev/null +++ b/images/OpenRAM_logo_yellow_transparent.svg @@ -0,0 +1,220 @@ + + + + + + + + image/svg+xml + + + + + + OpenRAM + + + + + + + + + + + + + + + + + + + + + + + + + + + From 17f87c50a7fa7ee02d6c4ea90aeb2dcc9024c5fd Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 14:23:14 -0700 Subject: [PATCH 34/45] Add custom parameter for wordline layer --- compiler/base/custom_layer_properties.py | 18 ++++++++++++++++++ compiler/modules/global_bitcell_array.py | 12 +++++++++++- compiler/modules/local_bitcell_array.py | 11 ++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/compiler/base/custom_layer_properties.py b/compiler/base/custom_layer_properties.py index 7f8e5993..a8c6509b 100644 --- a/compiler/base/custom_layer_properties.py +++ b/compiler/base/custom_layer_properties.py @@ -123,6 +123,12 @@ class _wordline_driver: self.vertical_supply = vertical_supply +class _bitcell_array: + def __init__(self, + wordline_layer): + self.wordline_layer = wordline_layer + + class layer_properties(): """ This contains meta information about the module routing layers. These @@ -159,6 +165,10 @@ class layer_properties(): self._wordline_driver = _wordline_driver(vertical_supply=False) + self._local_bitcell_array = _bitcell_array(wordline_layer="m3") + + self._global_bitcell_array = _bitcell_array(wordline_layer="m3") + @property def bank(self): return self._bank @@ -191,3 +201,11 @@ class layer_properties(): def wordline_driver(self): return self._wordline_driver + @property + def global_bitcell_array(self): + return self._global_bitcell_array + + @property + def local_bitcell_array(self): + return self._local_bitcell_array + diff --git a/compiler/modules/global_bitcell_array.py b/compiler/modules/global_bitcell_array.py index 655cdbcf..8eb527d2 100644 --- a/compiler/modules/global_bitcell_array.py +++ b/compiler/modules/global_bitcell_array.py @@ -11,6 +11,7 @@ from sram_factory import factory from vector import vector import debug from numpy import cumsum +from tech import layer_properties as layer_props class global_bitcell_array(bitcell_base_array.bitcell_base_array): @@ -223,11 +224,20 @@ class global_bitcell_array(bitcell_base_array.bitcell_base_array): new_name = "{0}_{1}".format(base_name, col + col_value) self.copy_layout_pin(inst, pin_name, new_name) + # Add the global word lines + wl_layer = layer_props.global_bitcell_array.wordline_layer + for wl_name in self.local_mods[0].get_inputs(): + for local_inst in self.local_insts: + wl_pin = local_inst.get_pin(wl_name) + self.add_via_stack_center(from_layer=wl_pin.layer, + to_layer=wl_layer, + offset=wl_pin.center()) + left_pin = self.local_insts[0].get_pin(wl_name) right_pin = self.local_insts[-1].get_pin(wl_name) self.add_layout_pin_segment_center(text=wl_name, - layer=left_pin.layer, + layer=wl_layer, start=left_pin.lc(), end=right_pin.rc()) diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index f0427c51..d8c81aea 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -10,6 +10,7 @@ from globals import OPTS from sram_factory import factory from vector import vector import debug +from tech import layer_properties as layer_props class local_bitcell_array(bitcell_base_array.bitcell_base_array): @@ -199,18 +200,22 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): wordline_pins = self.wl_array.get_inputs() + wl_layer = layer_props.global_bitcell_array.wordline_layer + wl_pitch = getattr(self, "{}_pitch".format(wl_layer)) + for (wl_name, in_pin_name) in zip(wordline_names, wordline_pins): # wl_pin = self.bitcell_array_inst.get_pin(wl_name) in_pin = self.wl_insts[port].get_pin(in_pin_name) y_offset = in_pin.cy() + if port == 0: - y_offset -= 2 * self.m3_pitch + y_offset -= 2 * wl_pitch else: - y_offset += 2 * self.m3_pitch + y_offset += 2 * wl_pitch self.add_layout_pin_segment_center(text=wl_name, - layer="m3", + layer=wl_layer, start=vector(self.wl_insts[port].lx(), y_offset), end=vector(self.wl_insts[port].lx() + self.wl_array.width, y_offset)) From 4604a5034eaf8531a11d65a3dcd081494710b7d5 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 10:07:37 -0700 Subject: [PATCH 35/45] Abstracted LEF added. Params for array wordline layers. --- compiler/base/custom_layer_properties.py | 6 +- compiler/base/hierarchy_layout.py | 5 +- compiler/base/lef.py | 87 +++++++++++++++++++----- compiler/base/pin_layout.py | 40 +++++++++-- compiler/modules/local_bitcell_array.py | 32 ++++----- compiler/options.py | 3 + compiler/sram/sram_base.py | 7 +- 7 files changed, 139 insertions(+), 41 deletions(-) diff --git a/compiler/base/custom_layer_properties.py b/compiler/base/custom_layer_properties.py index a8c6509b..eff24f82 100644 --- a/compiler/base/custom_layer_properties.py +++ b/compiler/base/custom_layer_properties.py @@ -125,8 +125,10 @@ class _wordline_driver: class _bitcell_array: def __init__(self, - wordline_layer): + wordline_layer, + wordline_pitch_factor=2): self.wordline_layer = wordline_layer + self.wordline_pitch_factor = wordline_pitch_factor class layer_properties(): @@ -165,7 +167,7 @@ class layer_properties(): self._wordline_driver = _wordline_driver(vertical_supply=False) - self._local_bitcell_array = _bitcell_array(wordline_layer="m3") + self._local_bitcell_array = _bitcell_array(wordline_layer="m2") self._global_bitcell_array = _bitcell_array(wordline_layer="m3") diff --git a/compiler/base/hierarchy_layout.py b/compiler/base/hierarchy_layout.py index 36a54937..1e2add8d 100644 --- a/compiler/base/hierarchy_layout.py +++ b/compiler/base/hierarchy_layout.py @@ -674,7 +674,8 @@ class layout(): directions=None, size=[1, 1], implant_type=None, - well_type=None): + well_type=None, + min_area=False): """ Punch a stack of vias from a start layer to a target layer by the center. """ @@ -708,7 +709,7 @@ class layout(): implant_type=implant_type, well_type=well_type) - if cur_layer != from_layer: + if cur_layer != from_layer or min_area: self.add_min_area_rect_center(cur_layer, offset, via.mod.first_layer_width, diff --git a/compiler/base/lef.py b/compiler/base/lef.py index a5c1910a..9ff02816 100644 --- a/compiler/base/lef.py +++ b/compiler/base/lef.py @@ -10,6 +10,8 @@ from tech import layer_names import os import shutil from globals import OPTS +from vector import vector +from pin_layout import pin_layout class lef: @@ -68,13 +70,63 @@ class lef: def lef_write(self, lef_name): """ Write the entire lef of the object to the file. """ - if OPTS.drc_exe and OPTS.drc_exe[0] == "magic": - self.magic_lef_write(lef_name) - return + if OPTS.detailed_lef: + debug.info(3, "Writing detailed LEF to {0}".format(lef_name)) + self.detailed_lef_write(lef_name) + else: + debug.info(3, "Writing abstract LEF to {0}".format(lef_name)) + # Can possibly use magic lef write to create the LEF + # if OPTS.drc_exe and OPTS.drc_exe[0] == "magic": + # self.magic_lef_write(lef_name) + # return + self.abstract_lef_write(lef_name) - debug.info(3, "Writing detailed LEF to {0}".format(lef_name)) + def abstract_lef_write(self, lef_name): + # To maintain the indent level easily + self.indent = "" - self.indent = "" # To maintain the indent level easily + self.lef = open(lef_name, "w") + self.lef_write_header() + + # Start with blockages on all layers the size of the block + # minus the pin escape margin (hard coded to 4 x m3 pitch) + # These are a pin_layout to use their geometric functions + perimeter_margin = self.m3_pitch + self.blockages = {} + for layer_name in self.lef_layers: + self.blockages[layer_name]=[] + for layer_name in self.lef_layers: + ll = vector(perimeter_margin, perimeter_margin) + ur = vector(self.width - perimeter_margin, self.height - perimeter_margin) + self.blockages[layer_name].append(pin_layout("", + [ll, ur], + layer_name)) + + # For each pin, remove the blockage and add the pin + for pin_name in self.pins: + pin = self.get_pin(pin_name) + inflated_pin = pin.inflated_pin(multiple=1) + for blockage in self.blockages[pin.layer]: + if blockage.overlaps(inflated_pin): + intersection_shape = blockage.intersection(inflated_pin) + # If it is zero area, don't add the pin + if intersection_shape[0][0]==intersection_shape[1][0] or intersection_shape[0][1]==intersection_shape[1][1]: + continue + # Remove the old blockage and add the new ones + self.blockages[pin.layer].remove(blockage) + intersection_pin = pin_layout("", intersection_shape, inflated_pin.layer) + new_blockages = blockage.cut(intersection_pin) + self.blockages[pin.layer].extend(new_blockages) + + self.lef_write_pin(pin_name) + + self.lef_write_obstructions(abstracted=True) + self.lef_write_footer() + self.lef.close() + + def detailed_lef_write(self, lef_name): + # To maintain the indent level easily + self.indent = "" self.lef = open(lef_name, "w") self.lef_write_header() @@ -136,24 +188,29 @@ class lef: self.indent = self.indent[:-3] self.lef.write("{0}END {1}\n".format(self.indent, name)) - def lef_write_obstructions(self): + def lef_write_obstructions(self, abstracted=False): """ Write all the obstructions on each layer """ self.lef.write("{0}OBS\n".format(self.indent)) for layer in self.lef_layers: self.lef.write("{0}LAYER {1} ;\n".format(self.indent, layer_names[layer])) self.indent += " " - blockages = self.get_blockages(layer, True) - for b in blockages: - self.lef_write_shape(b) + if abstracted: + blockages = self.blockages[layer] + for b in blockages: + self.lef_write_shape(b.rect) + else: + blockages = self.get_blockages(layer, True) + for b in blockages: + self.lef_write_shape(b) self.indent = self.indent[:-3] self.lef.write("{0}END\n".format(self.indent)) - def lef_write_shape(self, rect): - if len(rect) == 2: + def lef_write_shape(self, obj): + if len(obj) == 2: """ Write a LEF rectangle """ self.lef.write("{0}RECT ".format(self.indent)) - for item in rect: - # print(rect) + for item in obj: + # print(obj) self.lef.write(" {0} {1}".format(round(item[0], self.round_grid), round(item[1], @@ -162,12 +219,10 @@ class lef: else: """ Write a LEF polygon """ self.lef.write("{0}POLYGON ".format(self.indent)) - for item in rect: + for item in obj: self.lef.write(" {0} {1}".format(round(item[0], self.round_grid), round(item[1], self.round_grid))) - # for i in range(0,len(rect)): - # self.lef.write(" {0} {1}".format(round(rect[i][0],self.round_grid), round(rect[i][1],self.round_grid))) self.lef.write(" ;\n") diff --git a/compiler/base/pin_layout.py b/compiler/base/pin_layout.py index e8c6f0a5..e6baa4fc 100644 --- a/compiler/base/pin_layout.py +++ b/compiler/base/pin_layout.py @@ -139,13 +139,13 @@ class pin_layout: min_area = drc("{}_minarea".format(self.layer)) pass - def inflate(self, spacing=None): + def inflate(self, spacing=None, multiple=0.5): """ Inflate the rectangle by the spacing (or other rule) and return the new rectangle. """ if not spacing: - spacing = 0.5*drc("{0}_to_{0}".format(self.layer)) + spacing = multiple*drc("{0}_to_{0}".format(self.layer)) (ll, ur) = self.rect spacing = vector(spacing, spacing) @@ -154,15 +154,23 @@ class pin_layout: return (newll, newur) + def inflated_pin(self, spacing=None, multiple=0.5): + """ + Inflate the rectangle by the spacing (or other rule) + and return the new rectangle. + """ + inflated_area = self.inflate(spacing, multiple) + return pin_layout(self.name, inflated_area, self.layer) + def intersection(self, other): """ Check if a shape overlaps with a rectangle """ (ll, ur) = self.rect (oll, our) = other.rect min_x = max(ll.x, oll.x) - max_x = min(ll.x, oll.x) + max_x = min(ur.x, our.x) min_y = max(ll.y, oll.y) - max_y = min(ll.y, oll.y) + max_y = min(ur.y, our.y) return [vector(min_x, min_y), vector(max_x, max_y)] @@ -578,6 +586,30 @@ class pin_layout: return None + def cut(self, shape): + """ + Return a set of shapes that are this shape minus the argument shape. + """ + # Make the unique coordinates in X and Y directions + x_offsets = sorted([self.lx(), self.rx(), shape.lx(), shape.rx()]) + y_offsets = sorted([self.by(), self.uy(), shape.by(), shape.uy()]) + + new_shapes = [] + # Create all of the shapes + for x1, x2 in zip(x_offsets[0:], x_offsets[1:]): + if x1==x2: + continue + for y1, y2 in zip(y_offsets[0:], y_offsets[1:]): + if y1==y2: + continue + new_shape = pin_layout("", [vector(x1, y1), vector(x2, y2)], self.lpp) + # Don't add the existing shape in if it overlaps the pin shape + if new_shape.contains(shape): + continue + new_shapes.append(new_shape) + + return new_shapes + def same_lpp(self, lpp1, lpp2): """ Check if the layers and purposes are the same. diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index d8c81aea..68552d57 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -191,6 +191,11 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): def route(self): + global_wl_layer = layer_props.global_bitcell_array.wordline_layer + global_wl_pitch = getattr(self, "{}_pitch".format(global_wl_layer)) + global_wl_pitch_factor = layer_props.global_bitcell_array.wordline_pitch_factor + local_wl_layer = layer_props.local_bitcell_array.wordline_layer + # Route the global wordlines for port in self.all_ports: if port == 0: @@ -200,9 +205,6 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): wordline_pins = self.wl_array.get_inputs() - wl_layer = layer_props.global_bitcell_array.wordline_layer - wl_pitch = getattr(self, "{}_pitch".format(wl_layer)) - for (wl_name, in_pin_name) in zip(wordline_names, wordline_pins): # wl_pin = self.bitcell_array_inst.get_pin(wl_name) in_pin = self.wl_insts[port].get_pin(in_pin_name) @@ -210,23 +212,21 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): y_offset = in_pin.cy() if port == 0: - y_offset -= 2 * wl_pitch + y_offset -= global_wl_pitch_factor * global_wl_pitch else: - y_offset += 2 * wl_pitch - - self.add_layout_pin_segment_center(text=wl_name, - layer=wl_layer, - start=vector(self.wl_insts[port].lx(), y_offset), - end=vector(self.wl_insts[port].lx() + self.wl_array.width, y_offset)) - + y_offset += global_wl_pitch_factor * global_wl_pitch mid = vector(in_pin.cx(), y_offset) - self.add_path("m2", [in_pin.center(), mid]) + + self.add_layout_pin_rect_center(text=wl_name, + layer=global_wl_layer, + offset=mid) + + self.add_path(local_wl_layer, [in_pin.center(), mid]) self.add_via_stack_center(from_layer=in_pin.layer, - to_layer="m2", - offset=in_pin.center()) - self.add_via_center(self.m2_stack, - offset=mid) + to_layer=local_wl_layer, + offset=mid, + min_area=True) # Route the buffers for port in self.all_ports: diff --git a/compiler/options.py b/compiler/options.py index 67ac14d7..cd54b2c6 100644 --- a/compiler/options.py +++ b/compiler/options.py @@ -153,6 +153,9 @@ class options(optparse.Values): # Route the input/output pins to the perimeter perimeter_pins = True + # Detailed or abstract LEF view + detailed_lef = False + keep_temp = False diff --git a/compiler/sram/sram_base.py b/compiler/sram/sram_base.py index 6dacdd90..7621f67b 100644 --- a/compiler/sram/sram_base.py +++ b/compiler/sram/sram_base.py @@ -263,13 +263,18 @@ class sram_base(design, verilog, lef): # Add it as an IO pin to the perimeter lowest_coord = self.find_lowest_coords() - pin_width = pin.rx() - lowest_coord.x + route_width = pin.rx() - lowest_coord.x + pin_width = 2 * getattr(self, "{}_width".format(pin.layer)) pin_offset = vector(lowest_coord.x, pin.by()) self.add_layout_pin(pin_name, pin.layer, pin_offset, pin_width, pin.height()) + self.add_rect(pin.layer, + pin_offset, + route_width, + pin.height()) def route_escape_pins(self): """ From f61904e864165647a5178056950ee6e73ffdf12c Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 10:33:19 -0700 Subject: [PATCH 36/45] Crop boundary of logo --- images/OpenRAM_logo_yellow_transparent.png | Bin 23916 -> 20730 bytes images/OpenRAM_logo_yellow_transparent.svg | 164 +++++++++++---------- 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/images/OpenRAM_logo_yellow_transparent.png b/images/OpenRAM_logo_yellow_transparent.png index 17ce794f7af86b99683f0d6d504e5d5faf1ec70a..425d406a5ca47cdc3882bf328bfe848ccae81983 100644 GIT binary patch literal 20730 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 z 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 diff --git a/images/OpenRAM_logo_yellow_transparent.svg b/images/OpenRAM_logo_yellow_transparent.svg index bb02ae6c..5c276be7 100644 --- a/images/OpenRAM_logo_yellow_transparent.svg +++ b/images/OpenRAM_logo_yellow_transparent.svg @@ -7,14 +7,14 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="640" - height="480" - viewBox="0 0 639.99999 480.00001" + width="605.40302" + height="165.26472" + viewBox="0 0 605.40301 165.26473" id="svg2" version="1.1" - inkscape:version="0.91 r13725" + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" sodipodi:docname="OpenRAM_logo_yellow_transparent.svg" - inkscape:export-filename="/Users/mrg/Google Drive File Stream/Team Drives/OpenRAM/images/OpenRAM_logo_yellow_transparent.png" + inkscape:export-filename="/home/mrg/openram/images/OpenRAM_logo_yellow_transparent.png" inkscape:export-xdpi="150" inkscape:export-ydpi="150"> + inkscape:current-layer="svg2" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" /> OpenRAM + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:80px;font-family:Futura;-inkscape-font-specification:'Futura Bold';fill:#003c6c;fill-opacity:1" + y="113.18625" + x="173.17645">OpenRAM + d="m 53.960768,13.421563 v 21.96078" + style="fill:none;fill-rule:evenodd;stroke:#003c6c;stroke-width:3.69754982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:connector-curvature="0" /> + d="m 81.568617,13.421563 v 21.96078" + id="path8112" + inkscape:connector-curvature="0" /> + d="m 109.17646,13.421563 v 21.96078" + style="fill:none;fill-rule:evenodd;stroke:#003c6c;stroke-width:3.69754982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:connector-curvature="0" /> + d="M 53.960768,151.84317 V 129.88239" + id="path8137" + inkscape:connector-curvature="0" /> + d="M 81.568617,151.84317 V 129.88239" + style="fill:none;fill-rule:evenodd;stroke:#003c6c;stroke-width:3.69754982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:connector-curvature="0" /> + d="M 109.17646,151.84317 V 129.88239" + id="path8149" + inkscape:connector-curvature="0" /> + d="M 151.21568,56.715693 H 129.2549" + id="path8157" + inkscape:connector-curvature="0" /> + d="M 151.21568,84.323543 H 129.2549" + style="fill:none;fill-rule:evenodd;stroke:#003c6c;stroke-width:3.69754982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:connector-curvature="0" /> + d="M 151.21568,111.93138 H 129.2549" + id="path8169" + inkscape:connector-curvature="0" /> + d="m 13.421548,56.715693 h 21.96078" + style="fill:none;fill-rule:evenodd;stroke:#003c6c;stroke-width:3.69754982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:connector-curvature="0" /> + d="m 13.421548,84.323543 h 21.96078" + id="path8183" + inkscape:connector-curvature="0" /> + d="m 13.421548,111.93138 h 21.96078" + style="fill:none;fill-rule:evenodd;stroke:#003c6c;stroke-width:3.69754982;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + inkscape:connector-curvature="0" /> From 3421055ec408e30d9e8afe2fcc02b45c1eda15e0 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 10:39:15 -0700 Subject: [PATCH 37/45] Update python requirements --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index b6d5398e..2ffd4420 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,7 @@ things that need to be fixed. The OpenRAM compiler has very few dependencies: + [Ngspice] 26 (or later) or HSpice I-2013.12-1 (or later) or CustomSim 2017 (or later) + Python 3.5 or higher -+ Python numpy (pip3 install numpy to install) -+ Python scipy (pip3 install scipy to install) ++ Various Python packages (pip install -r requirements.txt) If you want to perform DRC and LVS, you will need either: + Calibre (for [FreePDK45]) From 584349c911d5c81544d3988ab1b0f5f04cec9b1e Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 14:23:14 -0700 Subject: [PATCH 38/45] Add custom parameter for wordline layer --- compiler/base/custom_layer_properties.py | 18 ++++++++++++++++++ compiler/modules/global_bitcell_array.py | 12 +++++++++++- compiler/modules/local_bitcell_array.py | 11 ++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/compiler/base/custom_layer_properties.py b/compiler/base/custom_layer_properties.py index 7f8e5993..a8c6509b 100644 --- a/compiler/base/custom_layer_properties.py +++ b/compiler/base/custom_layer_properties.py @@ -123,6 +123,12 @@ class _wordline_driver: self.vertical_supply = vertical_supply +class _bitcell_array: + def __init__(self, + wordline_layer): + self.wordline_layer = wordline_layer + + class layer_properties(): """ This contains meta information about the module routing layers. These @@ -159,6 +165,10 @@ class layer_properties(): self._wordline_driver = _wordline_driver(vertical_supply=False) + self._local_bitcell_array = _bitcell_array(wordline_layer="m3") + + self._global_bitcell_array = _bitcell_array(wordline_layer="m3") + @property def bank(self): return self._bank @@ -191,3 +201,11 @@ class layer_properties(): def wordline_driver(self): return self._wordline_driver + @property + def global_bitcell_array(self): + return self._global_bitcell_array + + @property + def local_bitcell_array(self): + return self._local_bitcell_array + diff --git a/compiler/modules/global_bitcell_array.py b/compiler/modules/global_bitcell_array.py index 655cdbcf..8eb527d2 100644 --- a/compiler/modules/global_bitcell_array.py +++ b/compiler/modules/global_bitcell_array.py @@ -11,6 +11,7 @@ from sram_factory import factory from vector import vector import debug from numpy import cumsum +from tech import layer_properties as layer_props class global_bitcell_array(bitcell_base_array.bitcell_base_array): @@ -223,11 +224,20 @@ class global_bitcell_array(bitcell_base_array.bitcell_base_array): new_name = "{0}_{1}".format(base_name, col + col_value) self.copy_layout_pin(inst, pin_name, new_name) + # Add the global word lines + wl_layer = layer_props.global_bitcell_array.wordline_layer + for wl_name in self.local_mods[0].get_inputs(): + for local_inst in self.local_insts: + wl_pin = local_inst.get_pin(wl_name) + self.add_via_stack_center(from_layer=wl_pin.layer, + to_layer=wl_layer, + offset=wl_pin.center()) + left_pin = self.local_insts[0].get_pin(wl_name) right_pin = self.local_insts[-1].get_pin(wl_name) self.add_layout_pin_segment_center(text=wl_name, - layer=left_pin.layer, + layer=wl_layer, start=left_pin.lc(), end=right_pin.rc()) diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index f0427c51..d8c81aea 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -10,6 +10,7 @@ from globals import OPTS from sram_factory import factory from vector import vector import debug +from tech import layer_properties as layer_props class local_bitcell_array(bitcell_base_array.bitcell_base_array): @@ -199,18 +200,22 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): wordline_pins = self.wl_array.get_inputs() + wl_layer = layer_props.global_bitcell_array.wordline_layer + wl_pitch = getattr(self, "{}_pitch".format(wl_layer)) + for (wl_name, in_pin_name) in zip(wordline_names, wordline_pins): # wl_pin = self.bitcell_array_inst.get_pin(wl_name) in_pin = self.wl_insts[port].get_pin(in_pin_name) y_offset = in_pin.cy() + if port == 0: - y_offset -= 2 * self.m3_pitch + y_offset -= 2 * wl_pitch else: - y_offset += 2 * self.m3_pitch + y_offset += 2 * wl_pitch self.add_layout_pin_segment_center(text=wl_name, - layer="m3", + layer=wl_layer, start=vector(self.wl_insts[port].lx(), y_offset), end=vector(self.wl_insts[port].lx() + self.wl_array.width, y_offset)) From f45efe3db6b915e40c6ca537157bb417b8a6986c Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 10:07:37 -0700 Subject: [PATCH 39/45] Abstracted LEF added. Params for array wordline layers. --- compiler/base/custom_layer_properties.py | 6 +- compiler/base/hierarchy_layout.py | 5 +- compiler/base/lef.py | 87 +++++++++++++++++++----- compiler/base/pin_layout.py | 40 +++++++++-- compiler/modules/local_bitcell_array.py | 32 ++++----- compiler/options.py | 3 + compiler/sram/sram_base.py | 7 +- 7 files changed, 139 insertions(+), 41 deletions(-) diff --git a/compiler/base/custom_layer_properties.py b/compiler/base/custom_layer_properties.py index a8c6509b..eff24f82 100644 --- a/compiler/base/custom_layer_properties.py +++ b/compiler/base/custom_layer_properties.py @@ -125,8 +125,10 @@ class _wordline_driver: class _bitcell_array: def __init__(self, - wordline_layer): + wordline_layer, + wordline_pitch_factor=2): self.wordline_layer = wordline_layer + self.wordline_pitch_factor = wordline_pitch_factor class layer_properties(): @@ -165,7 +167,7 @@ class layer_properties(): self._wordline_driver = _wordline_driver(vertical_supply=False) - self._local_bitcell_array = _bitcell_array(wordline_layer="m3") + self._local_bitcell_array = _bitcell_array(wordline_layer="m2") self._global_bitcell_array = _bitcell_array(wordline_layer="m3") diff --git a/compiler/base/hierarchy_layout.py b/compiler/base/hierarchy_layout.py index 36a54937..1e2add8d 100644 --- a/compiler/base/hierarchy_layout.py +++ b/compiler/base/hierarchy_layout.py @@ -674,7 +674,8 @@ class layout(): directions=None, size=[1, 1], implant_type=None, - well_type=None): + well_type=None, + min_area=False): """ Punch a stack of vias from a start layer to a target layer by the center. """ @@ -708,7 +709,7 @@ class layout(): implant_type=implant_type, well_type=well_type) - if cur_layer != from_layer: + if cur_layer != from_layer or min_area: self.add_min_area_rect_center(cur_layer, offset, via.mod.first_layer_width, diff --git a/compiler/base/lef.py b/compiler/base/lef.py index a5c1910a..9ff02816 100644 --- a/compiler/base/lef.py +++ b/compiler/base/lef.py @@ -10,6 +10,8 @@ from tech import layer_names import os import shutil from globals import OPTS +from vector import vector +from pin_layout import pin_layout class lef: @@ -68,13 +70,63 @@ class lef: def lef_write(self, lef_name): """ Write the entire lef of the object to the file. """ - if OPTS.drc_exe and OPTS.drc_exe[0] == "magic": - self.magic_lef_write(lef_name) - return + if OPTS.detailed_lef: + debug.info(3, "Writing detailed LEF to {0}".format(lef_name)) + self.detailed_lef_write(lef_name) + else: + debug.info(3, "Writing abstract LEF to {0}".format(lef_name)) + # Can possibly use magic lef write to create the LEF + # if OPTS.drc_exe and OPTS.drc_exe[0] == "magic": + # self.magic_lef_write(lef_name) + # return + self.abstract_lef_write(lef_name) - debug.info(3, "Writing detailed LEF to {0}".format(lef_name)) + def abstract_lef_write(self, lef_name): + # To maintain the indent level easily + self.indent = "" - self.indent = "" # To maintain the indent level easily + self.lef = open(lef_name, "w") + self.lef_write_header() + + # Start with blockages on all layers the size of the block + # minus the pin escape margin (hard coded to 4 x m3 pitch) + # These are a pin_layout to use their geometric functions + perimeter_margin = self.m3_pitch + self.blockages = {} + for layer_name in self.lef_layers: + self.blockages[layer_name]=[] + for layer_name in self.lef_layers: + ll = vector(perimeter_margin, perimeter_margin) + ur = vector(self.width - perimeter_margin, self.height - perimeter_margin) + self.blockages[layer_name].append(pin_layout("", + [ll, ur], + layer_name)) + + # For each pin, remove the blockage and add the pin + for pin_name in self.pins: + pin = self.get_pin(pin_name) + inflated_pin = pin.inflated_pin(multiple=1) + for blockage in self.blockages[pin.layer]: + if blockage.overlaps(inflated_pin): + intersection_shape = blockage.intersection(inflated_pin) + # If it is zero area, don't add the pin + if intersection_shape[0][0]==intersection_shape[1][0] or intersection_shape[0][1]==intersection_shape[1][1]: + continue + # Remove the old blockage and add the new ones + self.blockages[pin.layer].remove(blockage) + intersection_pin = pin_layout("", intersection_shape, inflated_pin.layer) + new_blockages = blockage.cut(intersection_pin) + self.blockages[pin.layer].extend(new_blockages) + + self.lef_write_pin(pin_name) + + self.lef_write_obstructions(abstracted=True) + self.lef_write_footer() + self.lef.close() + + def detailed_lef_write(self, lef_name): + # To maintain the indent level easily + self.indent = "" self.lef = open(lef_name, "w") self.lef_write_header() @@ -136,24 +188,29 @@ class lef: self.indent = self.indent[:-3] self.lef.write("{0}END {1}\n".format(self.indent, name)) - def lef_write_obstructions(self): + def lef_write_obstructions(self, abstracted=False): """ Write all the obstructions on each layer """ self.lef.write("{0}OBS\n".format(self.indent)) for layer in self.lef_layers: self.lef.write("{0}LAYER {1} ;\n".format(self.indent, layer_names[layer])) self.indent += " " - blockages = self.get_blockages(layer, True) - for b in blockages: - self.lef_write_shape(b) + if abstracted: + blockages = self.blockages[layer] + for b in blockages: + self.lef_write_shape(b.rect) + else: + blockages = self.get_blockages(layer, True) + for b in blockages: + self.lef_write_shape(b) self.indent = self.indent[:-3] self.lef.write("{0}END\n".format(self.indent)) - def lef_write_shape(self, rect): - if len(rect) == 2: + def lef_write_shape(self, obj): + if len(obj) == 2: """ Write a LEF rectangle """ self.lef.write("{0}RECT ".format(self.indent)) - for item in rect: - # print(rect) + for item in obj: + # print(obj) self.lef.write(" {0} {1}".format(round(item[0], self.round_grid), round(item[1], @@ -162,12 +219,10 @@ class lef: else: """ Write a LEF polygon """ self.lef.write("{0}POLYGON ".format(self.indent)) - for item in rect: + for item in obj: self.lef.write(" {0} {1}".format(round(item[0], self.round_grid), round(item[1], self.round_grid))) - # for i in range(0,len(rect)): - # self.lef.write(" {0} {1}".format(round(rect[i][0],self.round_grid), round(rect[i][1],self.round_grid))) self.lef.write(" ;\n") diff --git a/compiler/base/pin_layout.py b/compiler/base/pin_layout.py index e8c6f0a5..e6baa4fc 100644 --- a/compiler/base/pin_layout.py +++ b/compiler/base/pin_layout.py @@ -139,13 +139,13 @@ class pin_layout: min_area = drc("{}_minarea".format(self.layer)) pass - def inflate(self, spacing=None): + def inflate(self, spacing=None, multiple=0.5): """ Inflate the rectangle by the spacing (or other rule) and return the new rectangle. """ if not spacing: - spacing = 0.5*drc("{0}_to_{0}".format(self.layer)) + spacing = multiple*drc("{0}_to_{0}".format(self.layer)) (ll, ur) = self.rect spacing = vector(spacing, spacing) @@ -154,15 +154,23 @@ class pin_layout: return (newll, newur) + def inflated_pin(self, spacing=None, multiple=0.5): + """ + Inflate the rectangle by the spacing (or other rule) + and return the new rectangle. + """ + inflated_area = self.inflate(spacing, multiple) + return pin_layout(self.name, inflated_area, self.layer) + def intersection(self, other): """ Check if a shape overlaps with a rectangle """ (ll, ur) = self.rect (oll, our) = other.rect min_x = max(ll.x, oll.x) - max_x = min(ll.x, oll.x) + max_x = min(ur.x, our.x) min_y = max(ll.y, oll.y) - max_y = min(ll.y, oll.y) + max_y = min(ur.y, our.y) return [vector(min_x, min_y), vector(max_x, max_y)] @@ -578,6 +586,30 @@ class pin_layout: return None + def cut(self, shape): + """ + Return a set of shapes that are this shape minus the argument shape. + """ + # Make the unique coordinates in X and Y directions + x_offsets = sorted([self.lx(), self.rx(), shape.lx(), shape.rx()]) + y_offsets = sorted([self.by(), self.uy(), shape.by(), shape.uy()]) + + new_shapes = [] + # Create all of the shapes + for x1, x2 in zip(x_offsets[0:], x_offsets[1:]): + if x1==x2: + continue + for y1, y2 in zip(y_offsets[0:], y_offsets[1:]): + if y1==y2: + continue + new_shape = pin_layout("", [vector(x1, y1), vector(x2, y2)], self.lpp) + # Don't add the existing shape in if it overlaps the pin shape + if new_shape.contains(shape): + continue + new_shapes.append(new_shape) + + return new_shapes + def same_lpp(self, lpp1, lpp2): """ Check if the layers and purposes are the same. diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index d8c81aea..68552d57 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -191,6 +191,11 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): def route(self): + global_wl_layer = layer_props.global_bitcell_array.wordline_layer + global_wl_pitch = getattr(self, "{}_pitch".format(global_wl_layer)) + global_wl_pitch_factor = layer_props.global_bitcell_array.wordline_pitch_factor + local_wl_layer = layer_props.local_bitcell_array.wordline_layer + # Route the global wordlines for port in self.all_ports: if port == 0: @@ -200,9 +205,6 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): wordline_pins = self.wl_array.get_inputs() - wl_layer = layer_props.global_bitcell_array.wordline_layer - wl_pitch = getattr(self, "{}_pitch".format(wl_layer)) - for (wl_name, in_pin_name) in zip(wordline_names, wordline_pins): # wl_pin = self.bitcell_array_inst.get_pin(wl_name) in_pin = self.wl_insts[port].get_pin(in_pin_name) @@ -210,23 +212,21 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): y_offset = in_pin.cy() if port == 0: - y_offset -= 2 * wl_pitch + y_offset -= global_wl_pitch_factor * global_wl_pitch else: - y_offset += 2 * wl_pitch - - self.add_layout_pin_segment_center(text=wl_name, - layer=wl_layer, - start=vector(self.wl_insts[port].lx(), y_offset), - end=vector(self.wl_insts[port].lx() + self.wl_array.width, y_offset)) - + y_offset += global_wl_pitch_factor * global_wl_pitch mid = vector(in_pin.cx(), y_offset) - self.add_path("m2", [in_pin.center(), mid]) + + self.add_layout_pin_rect_center(text=wl_name, + layer=global_wl_layer, + offset=mid) + + self.add_path(local_wl_layer, [in_pin.center(), mid]) self.add_via_stack_center(from_layer=in_pin.layer, - to_layer="m2", - offset=in_pin.center()) - self.add_via_center(self.m2_stack, - offset=mid) + to_layer=local_wl_layer, + offset=mid, + min_area=True) # Route the buffers for port in self.all_ports: diff --git a/compiler/options.py b/compiler/options.py index 67ac14d7..cd54b2c6 100644 --- a/compiler/options.py +++ b/compiler/options.py @@ -153,6 +153,9 @@ class options(optparse.Values): # Route the input/output pins to the perimeter perimeter_pins = True + # Detailed or abstract LEF view + detailed_lef = False + keep_temp = False diff --git a/compiler/sram/sram_base.py b/compiler/sram/sram_base.py index 6dacdd90..7621f67b 100644 --- a/compiler/sram/sram_base.py +++ b/compiler/sram/sram_base.py @@ -263,13 +263,18 @@ class sram_base(design, verilog, lef): # Add it as an IO pin to the perimeter lowest_coord = self.find_lowest_coords() - pin_width = pin.rx() - lowest_coord.x + route_width = pin.rx() - lowest_coord.x + pin_width = 2 * getattr(self, "{}_width".format(pin.layer)) pin_offset = vector(lowest_coord.x, pin.by()) self.add_layout_pin(pin_name, pin.layer, pin_offset, pin_width, pin.height()) + self.add_rect(pin.layer, + pin_offset, + route_width, + pin.height()) def route_escape_pins(self): """ From 419836411c5be054e656245f6137876094243e59 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 11:33:18 -0700 Subject: [PATCH 40/45] Fix missing via for global wordlines. --- compiler/modules/local_bitcell_array.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index 68552d57..30da5bf1 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -217,17 +217,20 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): y_offset += global_wl_pitch_factor * global_wl_pitch mid = vector(in_pin.cx(), y_offset) + # A short jog to the global line + self.add_via_stack_center(from_layer=in_pin.layer, + to_layer=local_wl_layer, + offset=in_pin.center(), + min_area=True) + self.add_path(local_wl_layer, [in_pin.center(), mid]) + self.add_via_stack_center(from_layer=local_wl_layer, + to_layer=global_wl_layer, + offset=mid, + min_area=True) + # Add the global WL pin self.add_layout_pin_rect_center(text=wl_name, layer=global_wl_layer, offset=mid) - - self.add_path(local_wl_layer, [in_pin.center(), mid]) - - self.add_via_stack_center(from_layer=in_pin.layer, - to_layer=local_wl_layer, - offset=mid, - min_area=True) - # Route the buffers for port in self.all_ports: driver_outputs = self.driver_wordline_outputs[port] From 9f0ae7552ca69ba1b1ac5f449e5dc72963c7ccd9 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 13:00:49 -0700 Subject: [PATCH 41/45] Update download link to stable.zip link. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2ffd4420..674d13f2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Python 3.5](https://img.shields.io/badge/Python-3.5-green.svg)](https://www.python.org/) [![License: BSD 3-clause](./images/license_badge.svg)](./LICENSE) -[![Download](./images/download-stable-blue.svg)](https://github.com/VLSIDA/OpenRAM/archive/master.zip) +[![Download](./images/download-stable-blue.svg)](https://github.com/VLSIDA/OpenRAM/archive/stable.zip) [![Download](./images/download-unstable-blue.svg)](https://github.com/VLSIDA/OpenRAM/archive/dev.zip) An open-source static random access memory (SRAM) compiler. From 1190eb76e89f54a146691305dae723fcc8dfb3d4 Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 14:15:24 -0700 Subject: [PATCH 42/45] Add over-ride languages --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 2e6daefb..23fcc05d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -*.sp linguist-vendored +*.sp linguist-language=Spice +*.tf linquist-language=Tech File From b8c7fcf182fe35bdd5275d4a7bb9ecdaff374359 Mon Sep 17 00:00:00 2001 From: Hunter Nichols Date: Wed, 21 Apr 2021 15:53:27 -0700 Subject: [PATCH 43/45] Removed measurement check which conflicts with multiport memories --- compiler/characterizer/elmore.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/compiler/characterizer/elmore.py b/compiler/characterizer/elmore.py index b9f99f02..3d897f80 100644 --- a/compiler/characterizer/elmore.py +++ b/compiler/characterizer/elmore.py @@ -78,8 +78,6 @@ class elmore(simulation): port_data[port][mname].append(total_delay.delay / 1e3) elif "slew" in mname and port in self.read_ports: port_data[port][mname].append(total_delay.slew / 1e3) - else: - debug.error("Measurement name not recognized: {}".format(mname), 1) # Margin for error in period. Calculated by averaging required margin for a small and large # memory. FIXME: margin is quite large, should be looked into. From 15b0583ff23ec53de2e7653c1e4ac04393e424a1 Mon Sep 17 00:00:00 2001 From: mrg Date: Mon, 19 Apr 2021 14:23:14 -0700 Subject: [PATCH 44/45] Add custom parameter for wordline layer --- compiler/modules/local_bitcell_array.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index 30da5bf1..31371fe9 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -215,6 +215,12 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): y_offset -= global_wl_pitch_factor * global_wl_pitch else: y_offset += global_wl_pitch_factor * global_wl_pitch + + self.add_layout_pin_segment_center(text=wl_name, + layer=global_wl_layer, + start=vector(self.wl_insts[port].lx(), y_offset), + end=vector(self.wl_insts[port].lx() + self.wl_array.width, y_offset)) + mid = vector(in_pin.cx(), y_offset) # A short jog to the global line From 35fcb3f6316e1d780de07d688831c286e5796f7d Mon Sep 17 00:00:00 2001 From: mrg Date: Wed, 21 Apr 2021 10:07:37 -0700 Subject: [PATCH 45/45] Abstracted LEF added. Params for array wordline layers. --- compiler/modules/local_bitcell_array.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/compiler/modules/local_bitcell_array.py b/compiler/modules/local_bitcell_array.py index 31371fe9..cbea3ce9 100644 --- a/compiler/modules/local_bitcell_array.py +++ b/compiler/modules/local_bitcell_array.py @@ -215,14 +215,14 @@ class local_bitcell_array(bitcell_base_array.bitcell_base_array): y_offset -= global_wl_pitch_factor * global_wl_pitch else: y_offset += global_wl_pitch_factor * global_wl_pitch - - self.add_layout_pin_segment_center(text=wl_name, - layer=global_wl_layer, - start=vector(self.wl_insts[port].lx(), y_offset), - end=vector(self.wl_insts[port].lx() + self.wl_array.width, y_offset)) - mid = vector(in_pin.cx(), y_offset) + self.add_layout_pin_rect_center(text=wl_name, + layer=global_wl_layer, + offset=mid) + + self.add_path(local_wl_layer, [in_pin.center(), mid]) + # A short jog to the global line self.add_via_stack_center(from_layer=in_pin.layer, to_layer=local_wl_layer,