| #!/usr/bin/env python3 | |
| from __future__ import print_function | |
| import csv | |
| import os | |
| import re | |
| import textwrap | |
| from tabulate import tabulate | |
| import enum | |
| from typing import List, Tuple | |
| from dataclasses import dataclass, field, asdict | |
| def filesafe_template(s): | |
| s = s.replace('(', '') | |
| s = s.replace(')', '') | |
| s = s.replace(' ', '_') | |
| s = s.replace('/', '') | |
| s = s.replace('.-', '_dotdash') | |
| return s | |
| class RuleFlags(enum.Enum): | |
| """ | |
| Note: some rules contain correction factors to compensate possible mask | |
| defect and unpredicted process biases) | |
| """ | |
| P = u'Rule applies to periphery only (outside :drc_tag:`areaid.ce`). A corresponding core rule may or may not exist.' | |
| NE = u'Rule not checked for esd_nwell_tap. There are no corresponding rule for esd_nwell_tap.' | |
| NC = u'Rule not checked by DRC. It should be used as a guideline only.' | |
| TC = u'Rule not checked for cell name "*_tech_CD_top*"' | |
| A = u'Rule documents a functionality implemented in CL algorithms and may not be checked by DRC.' | |
| AD = u'Rule documents a functionality implemented in CL algorithms and checked by DRC.' | |
| DE = u'Rule not checked for source of Drain Extended device' | |
| LVS = u'Rule handled by LVS' | |
| F = u'Rule intended for Frame only, not checked inside Die' | |
| DNF = u'Drawn Not equal Final. The drawn rule does not reflect the final dimension on silicon. See table J for details.' | |
| RC = u'Recommended rule at the chip level, required rule at the IP level.' | |
| RR = u'Recommended rule at any IP level' | |
| AL = u'Rules applicable only to Al BE flows' | |
| CU = u'Rules applicable only to Cu BE flows' | |
| IR = u'IR drop check compering Al database and slotted Cu database for the same product (2 gds files) must be clean' | |
| EXEMPT = u'Rule is an exception?' | |
| @dataclass | |
| class Rule: | |
| name: str = '' | |
| description: str = '' | |
| flags: Tuple[RuleFlags] = field(default_factory=tuple) | |
| value: str = '' | |
| unit: str = '' | |
| @dataclass | |
| class RuleTable: | |
| fname: str = '' | |
| order: int = -1 | |
| template: str = '' | |
| name: str = '' | |
| description: str = '' | |
| image: str = '' | |
| rules: List[Rule] = field(default_factory=list, repr=False) | |
| enabled: bool = True | |
| notes: str = '' | |
| @property | |
| def csv_fname(self): | |
| return 'p{:03d}-{}.csv'.format(self.order, self.fname) | |
| data = [[]] | |
| for l in open('periphery.csv', encoding='utf8'): | |
| if '.-)' in l: | |
| data.append([]) | |
| only_comma = True | |
| for i in l: | |
| if i not in ' ,\n': | |
| only_comma = False | |
| break | |
| if only_comma: | |
| continue | |
| data[-1].append(l) | |
| no_pics = [ | |
| 'p024-hvtr_dotdash.svg', | |
| 'p033-licon_dotdash.svg', | |
| 'p042-via3_dotdash.svg', | |
| 'p043-nsm_dotdash.svg', | |
| 'p044-m5_dotdash.svg', | |
| 'p044-via4_dotdash.svg', | |
| 'p045-pad_dotdash.svg', | |
| 'p045-rdl_dotdash.svg', | |
| 'p053-hv_dotdash_dotdash.svg', | |
| 'p055-uhvi_dotdash_dotdash.svg', | |
| 'p055-ulvt-_dotdash.svg', | |
| 'p055-vhvi_dotdash_dotdash.svg', | |
| ] | |
| image_files = {} | |
| image_location = {} | |
| image_re = re.compile('p([0-9][0-9][0-9])-([^.]*)\.svg') | |
| for i in sorted(os.listdir('.')+no_pics): | |
| if not i.endswith('.svg'): | |
| continue | |
| page, name = image_re.match(i).groups() | |
| page = int(page, 10) | |
| if name in image_files: | |
| assert page-1 == image_location[name], (name, i, image_files[name], page, image_location[name]) | |
| if i not in no_pics: | |
| image_files[name] = i | |
| image_location[name] = page | |
| rule_tables = [] | |
| for d in data[1:]: | |
| rows = list(csv.reader(d)) | |
| rule_template, rule_name, a, process = rows.pop(0) | |
| rule_template = rule_template.strip() | |
| rule_name = rule_name.strip() | |
| a = a.strip() | |
| process = process.strip() | |
| assert a == '', (d[0], a) | |
| assert process in ('sky130', ''), process | |
| image_name = filesafe_template(rule_template) | |
| if 'High Voltage' in rule_name and not 'hv' in rule_template: | |
| rule_template = rule_template.replace('(', '(hv') | |
| # Extract the function line (if it exists) | |
| func = 'Function: Defines '+rule_name+' (FIXME)' | |
| if rows[0][1].startswith('Function:'): | |
| a, func, b, c = rows.pop(0) | |
| assert a == '', a | |
| assert b == '', b | |
| assert c == '', c | |
| # Strip off the notes | |
| notes = [] | |
| for i, r in enumerate(rows): | |
| if r[0] not in ('Note', ''): | |
| break | |
| if r[1] == '': | |
| assert r[2] == '', (r[2], rule_name, i, r) | |
| assert r[3] == 'NA', (r[3], rule_name, i, r) | |
| continue | |
| assert r[1] != '', (r[1], rule_name, i, r) | |
| assert r[2] == '', (r[2], rule_name, i, r) | |
| assert r[3] == '', (r[3], rule_name, i, r) | |
| notes.append(r[1]) | |
| rows = rows[i:] | |
| # Strip off the flags table | |
| should_strip_flags = False | |
| for i, r in enumerate(rows): | |
| if r[0] == 'Use' and r[1] == 'Explanation': | |
| should_strip_flags = True | |
| break | |
| if should_strip_flags: | |
| rows = rows[:i] | |
| # Remove rows with Section in the first column | |
| rows = [r for r in rows if not r[0].startswith('Section ')] | |
| # Join together description which span multiple rows. | |
| continued_index = [] | |
| for i, row in enumerate(rows): | |
| if row[0] == '': | |
| continued_index.append(i) | |
| for i in reversed(continued_index): | |
| previous_row = rows[i-1] | |
| l = rows[i] | |
| a, extra, b, c = rows.pop(i) | |
| assert a.strip() == '', (a, l) | |
| assert b.strip() in ('', previous_row[2]), (b, l, previous_row) | |
| assert c.strip() in ('', 'NA', 'N/A', previous_row[3]), (c, l, previous_row) | |
| if extra.strip() == '': | |
| continue | |
| previous_row[1] += '\n'+extra | |
| # Calculate the actual rule name. | |
| pr = None | |
| values = [] | |
| for r in rows: | |
| values.append(r[-1]) | |
| if r[0].startswith('.'): | |
| r[0] = rule_template.replace('.-.-', r[0]) | |
| elif r[0].strip() == '': | |
| r[0] = pr[0] | |
| else: | |
| r[0] = rule_template.replace('.-', '.'+r[0], 1) | |
| pr = r | |
| rt = RuleTable() | |
| rt.template = rule_template | |
| rt.fname = filesafe_template(rule_template) | |
| rt.name = rule_name | |
| rt.description = func | |
| if notes: | |
| rt.notes = "\n\n".join(notes) | |
| if rt.fname in image_files: | |
| rt.image = image_files[rt.fname] | |
| if rt.fname in image_location: | |
| rt.order = image_location[rt.fname] | |
| # Check for all the rules having Not Applicable values | |
| if len(values) == len([v for v in values if v in ('NA', 'N/A')]): | |
| rt.enabled = False | |
| for r in rows: | |
| assert len(r) == 5, r | |
| if r[0] == 'Use' and r[1] == 'Explanation': | |
| break | |
| rc = Rule() | |
| rc.name = r[0] | |
| rc.description = r[1].strip() | |
| flags = [getattr(RuleFlags, f.upper().replace(',', '')) for f in r[2].strip().split()] | |
| if r[3] == 'NC': | |
| flags.append(RuleFlags.NC) | |
| r[3] = '' | |
| rc.flags = tuple(flags) | |
| rc.value = r[3] | |
| rc.unit = r[4].strip() | |
| rt.rules.append(rc) | |
| if rule_tables: | |
| assert rt.order >= rule_tables[-1].order, "{} >= {}\n{}\n{}\n".format(rt.order, rule_tables[-1].order, rt, rule_tables[-1]) | |
| rule_tables.append(rt) | |
| continue | |
| PERIPHERY_RULES_FILE = os.path.join('..', 'periphery-rules.rst') | |
| rst = open(PERIPHERY_RULES_FILE, 'w', encoding='utf8') | |
| rst.write("""\ | |
| .. Do **not** modify this file it is generated from the periphery.csv file | |
| found in the periphery directory using the | |
| ./periphery/periphery-split-csv.py script. Instead run `make | |
| rules/periphery-rules.rst` in the ./docs directory. | |
| .. list-table:: | |
| :header-rows: 1 | |
| :stub-columns: 1 | |
| :widths: 10 75 | |
| * - Use | |
| - Explanation | |
| """) | |
| for e in RuleFlags: | |
| rst.write("""\ | |
| * - | |
| .. _{e.name}: | |
| :drc_flag:`{e.name}` | |
| - {e.value} | |
| """.format(e=e)) | |
| for rt in rule_tables: | |
| rst.write("""\ | |
| :drc_rule:`{rt.template}` | |
| {hd} | |
| """.format(rt=rt, hd='-'*(len(rt.template)+len(':drc_rule:``')))) | |
| if rt.notes: | |
| rst.write("""\ | |
| .. note:: | |
| {} | |
| """.format(textwrap.indent(rt.notes, prefix=' '))) | |
| headers = ('Name', 'Description', 'Flags', 'Value', 'Unit') | |
| headers_fmt = (':drc_rule:`Name`', 'Description', ':drc_flag:`Flags`', 'Value', 'Unit') | |
| rst.write("""\ | |
| .. list-table:: {rt.description} | |
| :header-rows: 1 | |
| :stub-columns: 1 | |
| :widths: 9 73 6 6 6 | |
| * - {h} | |
| """.format(rt=rt,h='\n - '.join(headers_fmt))) | |
| for r in rt.rules: | |
| f = ' '.join(':drc_flag:`{}`'.format(f.name) for f in r.flags) | |
| if '\\n- ' in r.description: # bullet list description | |
| r.description = r.description.replace('\\n- ','\n - ') | |
| elif '\\n' in r.description: # multi line description | |
| r.description = '\n'.join( [ '| '+l for l in r.description.split('\\n') ] ) | |
| else: | |
| r.description = r.description.lstrip(' -') # one item bullet list to text | |
| d = textwrap.indent(r.description, prefix=' ').strip() | |
| rst.write("""\ | |
| * - :drc_rule:`{r.name}` | |
| - {d} | |
| - {f} | |
| - {r.value} | |
| - {r.unit} | |
| """.format(r=r, d=d, f=f)) | |
| rst.write('\n\n') | |
| with open(rt.csv_fname, 'w', newline='', encoding='utf8') as f: | |
| w = csv.DictWriter(f, headers, lineterminator='\n') | |
| w.writeheader() | |
| for r in rt.rules: | |
| d = {f: getattr(r, f.lower()) for f in headers} | |
| d['Flags'] = ' '.join(f.name for f in d['Flags']) | |
| w.writerow(d) | |
| if rt.image: | |
| rst.write("""\ | |
| .. figure:: {image_file} | |
| :width: 100% | |
| :align: center | |
| """.format(image_file=os.path.join('periphery', rt.image))) | |
| rst.close() | |
| with open(PERIPHERY_RULES_FILE, encoding='utf8') as f: | |
| print(f.read()) | |