rom compiler top level

This commit is contained in:
Jacob Walker 2023-03-08 00:08:43 -08:00
parent 2d5199961d
commit 41f0b9a412
8 changed files with 481 additions and 89 deletions

View File

@ -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 *

View File

@ -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 *

View File

@ -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]

View File

@ -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")
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))

View File

@ -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
###################

160
compiler/rom.py Normal file
View File

@ -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)

123
compiler/rom_config.py Normal file
View File

@ -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

72
rom_compiler.py Executable file
View File

@ -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)