diff --git a/__init__.py b/__init__.py index 746ce73b..dd5751d5 100644 --- a/__init__.py +++ b/__init__.py @@ -44,3 +44,6 @@ from .globals import * # sram_config should be imported before sram from .sram_config import * from .sram import * + +from .rom_config import * +from .rom import * diff --git a/compiler/modules/__init__.py b/compiler/modules/__init__.py index ae748da9..3a514e7e 100755 --- a/compiler/modules/__init__.py +++ b/compiler/modules/__init__.py @@ -72,6 +72,7 @@ from .replica_pbitcell import * from .row_cap_array import * from .row_cap_bitcell_1port import * from .row_cap_bitcell_2port import * +from .rom_base_bank import * from .sense_amp_array import * from .sense_amp import * from .tri_gate_array import * diff --git a/compiler/modules/rom_base_array.py b/compiler/modules/rom_base_array.py index dd4c80d4..239af428 100644 --- a/compiler/modules/rom_base_array.py +++ b/compiler/modules/rom_base_array.py @@ -10,7 +10,7 @@ import math from .bitcell_base_array import bitcell_base_array from openram.base import vector -from openram import OPTS +from openram import OPTS, debug from openram.sram_factory import factory from openram.tech import drc, layer @@ -35,6 +35,7 @@ class rom_base_array(bitcell_base_array): self.array_col_size = self.column_size self.create_all_bitline_names() self.create_all_wordline_names() + # debug.info(1, "ROM array with rows: {0}, cols: {1}".format(self.row_size, self.column_size)) self.create_netlist() self.create_layout() @@ -148,6 +149,7 @@ class rom_base_array(bitcell_base_array): # when col = 0, bl_h is connected to precharge, otherwise connect to previous bl connection # when col = col_size - 1 connected column_sizeto gnd otherwise create new bl connection + # debug.info(1, "Create cell: r{0}, c{1}".format(row, col)) if row == self.row_size: bl_l = self.int_bl_list[col] diff --git a/compiler/modules/rom_base_bank.py b/compiler/modules/rom_base_bank.py index f8ab2eff..2619a587 100644 --- a/compiler/modules/rom_base_bank.py +++ b/compiler/modules/rom_base_bank.py @@ -6,10 +6,11 @@ # All rights reserved. # +import datetime from math import ceil, log, sqrt from openram.base import vector from openram.base import design -from openram import OPTS, debug +from openram import OPTS, debug, print_time from openram.sram_factory import factory from openram.tech import drc, layer, parameter @@ -21,120 +22,82 @@ class rom_base_bank(design): word size is in bytes """ - def __init__(self, strap_spacing=0, data_file=None, name="", word_size=2): + def __init__(self, name, rom_config): super().__init__(name=name) - self.word_size = word_size * 8 - self.read_binary(word_size=word_size, data_file=data_file, scramble_bits=True, endian="little") + self.rom_config = rom_config + rom_config.set_local_config(self) + self.word_size = self.word_bits + # self.read_binary(word_size=word_size, data_file=data_file, scramble_bits=True, endian="little") + # debug.info(1, "Rom data: {}".format(self.data)) self.num_outputs = self.rows self.num_inputs = ceil(log(self.rows, 2)) self.col_bits = ceil(log(self.words_per_row, 2)) self.row_bits = self.num_inputs - # self.data = [[0, 1, 0, 1], [1, 1, 1, 1], [1, 1, 0, 0], [0, 0, 1, 0]] - self.strap_spacing = strap_spacing - self.tap_spacing = 8 + self.tap_spacing = self.strap_spacing + + try: + from openram.tech import power_grid + self.supply_stack = power_grid + except ImportError: + # if no power_grid is specified by tech we use sensible defaults + # Route a M3/M4 grid + self.supply_stack = self.m3_stack + self.interconnect_layer = "m1" self.bitline_layer = "m1" self.wordline_layer = "m2" - if "li" in layer: self.route_stack = self.m1_stack else: self.route_stack = self.m2_stack self.route_layer = self.route_stack[0] - self.setup_layout_constants() - self.create_netlist() - if not OPTS.netlist_only: - self.create_layout() - """ - Reads a hexadecimal file from a given directory to be used as the data written to the ROM - endian is either "big" or "little" - word_size is the number of bytes per word - sets the row and column size based on the size of binary input, tries to keep array as square as possible, - """ - - def read_binary(self, data_file, word_size=2, endian="big", scramble_bits=False): - # Read data as hexidecimal text file - hex_file = open(data_file, 'r') - hex_data = hex_file.read() - - # Convert from hex into an int - data_int = int(hex_data, 16) - # Then from int into a right aligned, zero padded string - bin_string = bin(data_int)[2:].zfill(len(hex_data) * 4) - - # Then turn the string into a list of ints - bin_data = list(bin_string) - bin_data = [int(x) for x in bin_data] - - # data size in bytes - data_size = len(bin_data) / 8 - num_words = int(data_size / word_size) - - bytes_per_col = sqrt(num_words) - - self.words_per_row = int(ceil(bytes_per_col /(2*word_size))) - - bits_per_row = self.words_per_row * word_size * 8 - - self.cols = bits_per_row - self.rows = int(num_words / (self.words_per_row)) - chunked_data = [] - - for i in range(0, len(bin_data), bits_per_row): - row_data = bin_data[i:i + bits_per_row] - if len(row_data) < bits_per_row: - row_data = [0] * (bits_per_row - len(row_data)) + row_data - chunked_data.append(row_data) - - - # if endian == "big": - - - self.data = chunked_data - if scramble_bits: - scrambled_chunked = [] - - for row_data in chunked_data: - scambled_data = [] - for bit in range(self.word_size): - for word in range(self.words_per_row): - scambled_data.append(row_data[bit + word * self.word_size]) - scrambled_chunked.append(scambled_data) - self.data = scrambled_chunked - - - - # self.data.reverse() - - debug.info(1, "Read rom binary: length {0} bytes, {1} words, set number of cols to {2}, rows to {3}, with {4} words per row".format(data_size, num_words, self.cols, self.rows, self.words_per_row)) def create_netlist(self): + start_time = datetime.datetime.now() self.add_modules() self.add_pins() - - + self.create_instances() + if not OPTS.is_unit_test: + print_time("Submodules", datetime.datetime.now(), start_time) def create_layout(self): - self.create_instances() + + start_time = datetime.datetime.now() + + self.setup_layout_constants() self.place_instances() + if not OPTS.is_unit_test: + print_time("Placement", datetime.datetime.now(), start_time) + + start_time = datetime.datetime.now() + self.route_layout() + if not OPTS.is_unit_test: + print_time("Routing", datetime.datetime.now(), start_time) + + self.height = self.array_inst.height + self.width = self.array_inst.width + self.add_boundary() + + start_time = datetime.datetime.now() + if not OPTS.is_unit_test: + # We only enable final verification if we have routed the design + # Only run this if not a unit test, because unit test will also verify it. + self.DRC_LVS(final_verification=OPTS.route_supplies, force_check=OPTS.check_lvsdrc) + print_time("Verification", datetime.datetime.now(), start_time) + + def route_layout(self): self.route_decode_outputs() - self.route_precharge() - self.route_clock() self.route_array_outputs() self.place_top_level_pins() self.route_supplies() self.route_output_buffers() - self.height = self.array_inst.height - self.width = self.array_inst.width - self.add_boundary() - def setup_layout_constants(self): self.route_layer_width = drc["minwidth_{}".format(self.route_stack[0])] @@ -166,7 +129,7 @@ class rom_base_bank(design): # in sky130 the address control buffer is composed of 2 size 2 NAND gates, # with a beta of 3, each of these gates has gate capacitance of 2 min sized inverters, therefor a load of 4 - + addr_control_buffer_effort = parameter['beta'] + 1 # a single min sized nmos makes up 1/4 of the input capacitance of a min sized inverter bitcell_effort = 0.25 @@ -492,12 +455,72 @@ class rom_base_bank(design): pin_num = msb - self.col_bits self.copy_layout_pin(self.decode_inst, "A{}".format(pin_num), name) - - - def route_supplies(self): for inst in self.insts: if not inst.mod.name.__contains__("contact"): self.copy_layout_pin(inst, "vdd") - self.copy_layout_pin(inst, "gnd") \ No newline at end of file + self.copy_layout_pin(inst, "gnd") + + # """ + # Reads a hexadecimal file from a given directory to be used as the data written to the ROM + # endian is either "big" or "little" + # word_size is the number of bytes per word + # sets the row and column size based on the size of binary input, tries to keep array as square as possible, + # """ + + # def read_binary(self, data_file, word_size=2, endian="big", scramble_bits=False): + # # Read data as hexidecimal text file + # hex_file = open(data_file, 'r') + # hex_data = hex_file.read() + + # # Convert from hex into an int + # data_int = int(hex_data, 16) + # # Then from int into a right aligned, zero padded string + # bin_string = bin(data_int)[2:].zfill(len(hex_data) * 4) + + # # Then turn the string into a list of ints + # bin_data = list(bin_string) + # bin_data = [int(x) for x in bin_data] + + # # data size in bytes + # data_size = len(bin_data) / 8 + # num_words = int(data_size / word_size) + + # bytes_per_col = sqrt(num_words) + + # self.words_per_row = int(ceil(bytes_per_col /(2*word_size))) + + # bits_per_row = self.words_per_row * word_size * 8 + + # self.cols = bits_per_row + # self.rows = int(num_words / (self.words_per_row)) + # chunked_data = [] + + # for i in range(0, len(bin_data), bits_per_row): + # row_data = bin_data[i:i + bits_per_row] + # if len(row_data) < bits_per_row: + # row_data = [0] * (bits_per_row - len(row_data)) + row_data + # chunked_data.append(row_data) + + + # # if endian == "big": + + + # self.data = chunked_data + # if scramble_bits: + # scrambled_chunked = [] + + # for row_data in chunked_data: + # scambled_data = [] + # for bit in range(self.word_size): + # for word in range(self.words_per_row): + # scambled_data.append(row_data[bit + word * self.word_size]) + # scrambled_chunked.append(scambled_data) + # self.data = scrambled_chunked + + + + # # self.data.reverse() + + # debug.info(1, "Read rom binary: length {0} bytes, {1} words, set number of cols to {2}, rows to {3}, with {4} words per row".format(data_size, num_words, self.cols, self.rows, self.words_per_row)) diff --git a/compiler/options.py b/compiler/options.py index 14dc7b57..92427ac0 100644 --- a/compiler/options.py +++ b/compiler/options.py @@ -53,6 +53,14 @@ class options(optparse.Values): num_spare_rows = 0 num_spare_cols = 0 + ################### + # ROM configuration options + ################### + rom_endian = "little" + rom_data = None + strap_spacing = 8 + scramble_bits = True + ################### # Optimization options ################### diff --git a/compiler/rom.py b/compiler/rom.py new file mode 100644 index 00000000..53d7f435 --- /dev/null +++ b/compiler/rom.py @@ -0,0 +1,160 @@ +# See LICENSE for licensing information. +# +# Copyright (c) 2016-2023 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 os +import shutil +import datetime +from openram import debug +from openram import rom_config as config +from openram import OPTS, print_time + + +class rom(): + """ + This is not a design module, but contains an ROM design instance. + """ + def __init__(self, rom_config=None, name=None): + + # Create default configs if custom config isn't provided + if rom_config is None: + rom_config = config(rom_data=OPTS.rom_data, + word_size=OPTS.word_size, + words_per_row=OPTS.words_per_row, + rom_endian=OPTS.rom_endian, + strap_spacing=OPTS.strap_spacing) + + if name is None: + name = OPTS.output_name + + rom_config.set_local_config(self) + + # reset the static duplicate name checker for unit tests + # in case we create more than one ROM + from openram.base import design + design.name_map=[] + + debug.info(2, "create rom of size {0} with {1} num of words".format(self.word_size, + self.num_words)) + start_time = datetime.datetime.now() + + self.name = name + + import openram.modules.rom_base_bank as rom + + self.r = rom(name, rom_config) + + self.r.create_netlist() + if not OPTS.netlist_only: + self.r.create_layout() + + if not OPTS.is_unit_test: + print_time("ROM creation", datetime.datetime.now(), start_time) + + def sp_write(self, name, lvs=False, trim=False): + self.r.sp_write(name, lvs, trim) + + def gds_write(self, name): + self.r.gds_write(name) + + def verilog_write(self, name): + self.r.verilog_write(name) + + def extended_config_write(self, name): + """Dump config file with all options. + Include defaults and anything changed by input config.""" + f = open(name, "w") + var_dict = dict((name, getattr(OPTS, name)) for name in dir(OPTS) if not name.startswith('__') and not callable(getattr(OPTS, name))) + for var_name, var_value in var_dict.items(): + if isinstance(var_value, str): + f.write(str(var_name) + " = " + "\"" + str(var_value) + "\"\n") + else: + f.write(str(var_name) + " = " + str(var_value)+ "\n") + f.close() + + def save(self): + """ Save all the output files while reporting time to do it as well. """ + + # Import this at the last minute so that the proper tech file + # is loaded and the right tools are selected + from openram import verify + + # Save the spice file + start_time = datetime.datetime.now() + spname = OPTS.output_path + self.r.name + ".sp" + debug.print_raw("SP: Writing to {0}".format(spname)) + self.sp_write(spname) + + print_time("Spice writing", datetime.datetime.now(), start_time) + + if not OPTS.netlist_only: + # Write the layout + start_time = datetime.datetime.now() + gdsname = OPTS.output_path + self.r.name + ".gds" + debug.print_raw("GDS: Writing to {0}".format(gdsname)) + self.gds_write(gdsname) + if OPTS.check_lvsdrc: + verify.write_drc_script(cell_name=self.r.name, + gds_name=os.path.basename(gdsname), + extract=True, + final_verification=True, + output_path=OPTS.output_path) + print_time("GDS", datetime.datetime.now(), start_time) + + # Save the LVS file + start_time = datetime.datetime.now() + lvsname = OPTS.output_path + self.r.name + ".lvs.sp" + debug.print_raw("LVS: Writing to {0}".format(lvsname)) + self.sp_write(lvsname, lvs=True) + if not OPTS.netlist_only and OPTS.check_lvsdrc: + verify.write_lvs_script(cell_name=self.r.name, + gds_name=os.path.basename(gdsname), + sp_name=os.path.basename(lvsname), + final_verification=True, + output_path=OPTS.output_path) + print_time("LVS writing", datetime.datetime.now(), start_time) + + # Save the extracted spice file + if OPTS.use_pex: + start_time = datetime.datetime.now() + # Output the extracted design if requested + pexname = OPTS.output_path + self.r.name + ".pex.sp" + spname = OPTS.output_path + self.r.name + ".sp" + verify.run_pex(self.r.name, gdsname, spname, output=pexname) + sp_file = pexname + print_time("Extraction", datetime.datetime.now(), start_time) + else: + # Use generated spice file for characterization + sp_file = spname + + # Save a functional simulation file + + # TODO: Characterize the design + + + # Write the config file + start_time = datetime.datetime.now() + from shutil import copyfile + copyfile(OPTS.config_file, OPTS.output_path + OPTS.output_name + '.py') + debug.print_raw("Config: Writing to {0}".format(OPTS.output_path + OPTS.output_name + '.py')) + print_time("Config", datetime.datetime.now(), start_time) + + # TODO: Write the datasheet + + # TODO: Write a verilog model + # start_time = datetime.datetime.now() + # vname = OPTS.output_path + self.r.name + '.v' + # debug.print_raw("Verilog: Writing to {0}".format(vname)) + # self.verilog_write(vname) + # print_time("Verilog", datetime.datetime.now(), start_time) + + # Write out options if specified + if OPTS.output_extended_config: + start_time = datetime.datetime.now() + oname = OPTS.output_path + OPTS.output_name + "_extended.py" + debug.print_raw("Extended Config: Writing to {0}".format(oname)) + self.extended_config_write(oname) + print_time("Extended Config", datetime.datetime.now(), start_time) diff --git a/compiler/rom_config.py b/compiler/rom_config.py new file mode 100644 index 00000000..9e67e37a --- /dev/null +++ b/compiler/rom_config.py @@ -0,0 +1,123 @@ +# See LICENSE for licensing information. +# +# Copyright (c) 2016-2023 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. +# +from typing import List +from math import log, sqrt, ceil +from openram import debug +from openram.sram_factory import factory +from openram import OPTS + + + +class rom_config: + """ This is a structure that is used to hold the ROM configuration options. """ + + def __init__(self, word_size, rom_data, words_per_row=None, rom_endian="little", scramble_bits=True, strap_spacing=8): + self.word_size = word_size + self.word_bits = self.word_size * 8 + self.rom_data = rom_data + self.strap_spacing = strap_spacing + # TODO: This currently does nothing. It should change the behavior of the chunk funciton. + self.endian = rom_endian + + # This should pretty much always be true. If you want to make silicon art you might set to false + self.scramble_bits = scramble_bits + # This will get over-written when we determine the organization + self.words_per_row = words_per_row + + self.compute_sizes() + + def __str__(self): + """ override print function output """ + config_items = ["word_size", + "num_words", + "words_per_row", + "endian", + "strap_spacing", + "rom_data"] + str = "" + for item in config_items: + val = getattr(self, item) + str += "{} : {}\n".format(item, val) + return str + + def set_local_config(self, module): + """ Copy all of the member variables to the given module for convenience """ + + members = [attr for attr in dir(self) if not callable(getattr(self, attr)) and not attr.startswith("__")] + + # Copy all the variables to the local module + for member in members: + setattr(module, member, getattr(self, member)) + + def compute_sizes(self): + """ Computes the organization of the memory using data size by trying to make it a rectangle.""" + + # Read data as hexidecimal text file + hex_file = open(self.rom_data, 'r') + hex_data = hex_file.read() + + # Convert from hex into an int + data_int = int(hex_data, 16) + # Then from int into a right aligned, zero padded string + bin_string = bin(data_int)[2:].zfill(len(hex_data) * 4) + + # Then turn the string into a list of ints + bin_data = list(bin_string) + raw_data = [int(x) for x in bin_data] + + # data size in bytes + data_size = len(raw_data) / 8 + self.num_words = int(data_size / self.word_size) + + # If this was hard coded, don't dynamically compute it! + if not self.words_per_row: + + # Row size if the array was square + bytes_per_row = sqrt(self.num_words) + + # Heuristic to value longer wordlines over long bitlines. + # The extra factor of 2 in the denominator should make the array less square + self.words_per_row = int(ceil(bytes_per_row /(2*self.word_size))) + + self.cols = self.words_per_row * self.word_size * 8 + self.rows = int(self.num_words / self.words_per_row) + + self.chunk_data(raw_data) + + # Set word_per_row in OPTS + OPTS.words_per_row = self.words_per_row + debug.info(1, "Read rom data file: length {0} bytes, {1} words, set number of cols to {2}, rows to {3}, with {4} words per row".format(data_size, self.num_words, self.cols, self.rows, self.words_per_row)) + + + def chunk_data(self, raw_data: List[int]): + """ + Chunks a flat list of bits into rows based on the calculated ROM sizes. Handles scrambling of data + """ + bits_per_row = self.cols + + chunked_data = [] + + for i in range(0, len(raw_data), bits_per_row): + row_data = raw_data[i:i + bits_per_row] + if len(row_data) < bits_per_row: + row_data = [0] * (bits_per_row - len(row_data)) + row_data + chunked_data.append(row_data) + + self.data = chunked_data + + if self.scramble_bits: + scrambled_chunked = [] + + for row_data in chunked_data: + scambled_data = [] + for bit in range(self.word_bits): + for word in range(self.words_per_row): + scambled_data.append(row_data[bit + word * self.word_bits]) + scrambled_chunked.append(scambled_data) + self.data = scrambled_chunked + diff --git a/rom_compiler.py b/rom_compiler.py new file mode 100755 index 00000000..f09c1c0a --- /dev/null +++ b/rom_compiler.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# See LICENSE for licensing information. +# +# Copyright (c) 2016-2023 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. +# + +""" +ROM Compiler + +The output files append the given suffixes to the output name: +a spice (.sp) file for circuit simulation +a GDS2 (.gds) file containing the layout +""" + + +import sys +import os +import datetime + +# You don't need the next two lines if you're sure that openram package is installed +from common import * +make_openram_package() +import openram + + +(OPTS, args) = openram.parse_args() + + +# Check that we are left with a single configuration file as argument. +if len(args) != 1: + print(openram.USAGE) + sys.exit(2) + +# These depend on arguments, so don't load them until now. +from openram import debug + +# Parse config file and set up all the options +openram.init_openram(config_file=args[0]) + +# Only print banner here so it's not in unit tests +openram.print_banner() + +# Keep track of running stats +start_time = datetime.datetime.now() +openram.print_time("Start", start_time) + + +output_extensions = [ "sp", "v"] +# Only output lef/gds if back-end +if not OPTS.netlist_only: + output_extensions.extend(["gds"]) + +output_files = ["{0}{1}.{2}".format(OPTS.output_path, + OPTS.output_name, x) + for x in output_extensions] +debug.print_raw("Output files are: ") +for path in output_files: + debug.print_raw(path) + +from openram import rom + +r = rom() + +# Output the files for the resulting ROM +r.save() + +# Delete temp files etc. +openram.end_openram() +openram.print_time("End", datetime.datetime.now(), start_time) \ No newline at end of file