Tim Edwards | d14571f | 2021-01-06 14:49:27 -0500 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 2 | #----------------------------------------------------------------------- |
| 3 | # netlist_to_layout.py |
| 4 | #----------------------------------------------------------------------- |
| 5 | # |
| 6 | # Generate a magic layout from a SPICE netlist, running magic in batch |
| 7 | # mode and calling up the PDK selections non-interactively for each |
| 8 | # component in the netlist. |
| 9 | # |
| 10 | #--------------------------------------------------------------------- |
| 11 | # Written by Tim Edwards |
| 12 | # efabless, inc. |
| 13 | # November 17, 2016 |
| 14 | # Updated December 17, 2016 |
| 15 | # Version 1.0 |
| 16 | # Imported December 22, 2020 to open_pdks |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 17 | # Updated February 10, 2021 for use running on netlist alone |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 18 | #--------------------------------------------------------------------- |
| 19 | |
| 20 | import os |
| 21 | import re |
| 22 | import sys |
| 23 | import subprocess |
| 24 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 25 | def generate_layout_start(library, ofile=sys.stdout): |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 26 | global debugmode |
| 27 | if debugmode: |
| 28 | print('Writing layout generating script.') |
| 29 | |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 30 | # Write a couple of simplifying procedures |
| 31 | print('#!/usr/bin/env wish', file=ofile) |
| 32 | print('#-------------------------------------', file=ofile) |
| 33 | print('# Script to create layout from netlist', file=ofile) |
| 34 | print('# Source this in magic.', file=ofile) |
| 35 | print('#-----------------------------------------', file=ofile) |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 36 | print('drc off', file=ofile) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 37 | print('proc move_forward {instname} {', file=ofile) |
| 38 | print(' select cell $instname', file=ofile) |
| 39 | print(' set anum [lindex [array -list count] 1]', file=ofile) |
| 40 | print(' set xpitch [lindex [array -list pitch] 0]', file=ofile) |
| 41 | print(' set bbox [box values]', file=ofile) |
| 42 | print(' set posx [lindex $bbox 0]', file=ofile) |
| 43 | print(' set posy [lindex $bbox 1]', file=ofile) |
| 44 | print(' set width [expr [lindex $bbox 2] - $posx]', file=ofile) |
| 45 | print(' set posx [expr $posx + $width + $xpitch * $anum]', file=ofile) |
| 46 | print(' box position ${posx}i ${posy}i', file=ofile) |
| 47 | print(' return [lindex $bbox 3]', file=ofile) |
| 48 | print('}', file=ofile) |
| 49 | print('', file=ofile) |
| 50 | print('proc get_and_move_inst {cellname instname anum} {', file=ofile) |
| 51 | print(' set newinst [getcell $cellname]', file=ofile) |
| 52 | print(' select cell $newinst', file=ofile) |
| 53 | print(' if {$newinst == ""} {return}', file=ofile) |
| 54 | print(' identify $instname', file=ofile) |
| 55 | print(' if {$anum > 1} {array 1 $anum}', file=ofile) |
| 56 | print(' set bbox [box values]', file=ofile) |
| 57 | print(' set posx [lindex $bbox 2]', file=ofile) |
| 58 | print(' set posy [lindex $bbox 1]', file=ofile) |
| 59 | print(' box position ${posx}i ${posy}i', file=ofile) |
| 60 | print(' return [lindex $bbox 3]', file=ofile) |
| 61 | print('}', file=ofile) |
| 62 | print('', file=ofile) |
| 63 | print('proc add_pin {pinname portnum} {', file=ofile) |
| 64 | print(' box size 1um 1um', file=ofile) |
| 65 | print(' paint m1', file=ofile) |
| 66 | print(' label $pinname FreeSans 16 0 0 0 c m1', file=ofile) |
| 67 | print(' port make $portnum', file=ofile) |
| 68 | print(' box move s 2um', file=ofile) |
| 69 | print('}', file=ofile) |
| 70 | print('', file=ofile) |
| 71 | if not library: |
| 72 | print('namespace import ${PDKNAMESPACE}::*', file=ofile) |
| 73 | print('suspendall', file=ofile) |
| 74 | return ofile |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 75 | |
| 76 | def generate_layout_add(subname, subpins, complist, library, ofile=sys.stdout): |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 77 | global debugmode |
| 78 | if debugmode: |
| 79 | if subpins: |
| 80 | print(' Generating layout for subcircuit ' + subname + '.') |
| 81 | else: |
| 82 | print(' Generating layout for top level circuit ' + subname + '.') |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 83 | |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 84 | gparmrex = re.compile('([^= \t]+)=([^=]+)') |
| 85 | sparmrex = re.compile('([^= \t]+)=([^= \t]+)[ \t]*(.*)') |
| 86 | expr1rex = re.compile('([^= \t]+)=\'([^\']+)\'[ \t]*(.*)') |
| 87 | expr2rex = re.compile('([^= \t]+)=\{([^\}]+)\}[ \t]*(.*)') |
| 88 | tokrex = re.compile('([^ \t]+)[ \t]*(.*)') |
| 89 | |
| 90 | if subname: |
| 91 | print('load ' + subname + ' -quiet', file=ofile) |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 92 | |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 93 | print('box 0um 0um 0um 0um', file=ofile) |
| 94 | print('', file=ofile) |
| 95 | |
| 96 | # Generate all of the pins as labels |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 97 | if subpins: |
| 98 | pinlist = subpins.split() |
| 99 | i = 0 |
| 100 | for pin in pinlist: |
| 101 | # Escape [ and ] in pin name |
| 102 | pin_esc = pin.replace('[', '\[').replace(']', '\]') |
| 103 | # To be done: watch for key=value parameters |
| 104 | print('add_pin ' + pin_esc + ' ' + str(i), file=ofile) |
| 105 | i += 1 |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 106 | |
| 107 | # Set initial position for importing cells |
| 108 | print('box size 0 0', file=ofile) |
| 109 | print('set posx 0', file=ofile) |
| 110 | print('set posy [expr {round(3 / [cif scale out])}]', file=ofile) |
| 111 | print('box position ${posx}i ${posy}i', file=ofile) |
| 112 | |
| 113 | for comp in complist: |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 114 | pinlist = [] |
| 115 | paramlist = [] |
| 116 | |
| 117 | # Parse into pins, device name, and parameters. Make sure parameters |
| 118 | # incorporate quoted expressions as {} or ''. |
| 119 | rest = comp |
| 120 | while rest and rest != '': |
| 121 | gmatch = gparmrex.match(rest) |
| 122 | if gmatch: |
| 123 | break |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 124 | else: |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 125 | tmatch = tokrex.match(rest) |
| 126 | if tmatch: |
| 127 | token = tmatch.group(1) |
| 128 | pinlist.append(token) |
| 129 | rest = tmatch.group(2) |
| 130 | else: |
| 131 | rest = '' |
| 132 | |
| 133 | while rest and rest != '': |
| 134 | ematch = expr1rex.match(rest) |
| 135 | if ematch: |
| 136 | pname = ematch.group(1) |
| 137 | value = ematch.group(2) |
| 138 | paramlist.append((pname, '{' + value + '}')) |
| 139 | rest = ematch.group(3) |
| 140 | else: |
| 141 | ematch = expr2rex.match(rest) |
| 142 | if ematch: |
| 143 | pname = ematch.group(1) |
| 144 | value = ematch.group(2) |
| 145 | paramlist.append((pname, '{' + value + '}')) |
| 146 | rest = ematch.group(3) |
| 147 | else: |
| 148 | smatch = sparmrex.match(rest) |
| 149 | if smatch: |
| 150 | pname = smatch.group(1) |
| 151 | value = smatch.group(2) |
| 152 | paramlist.append((pname, value)) |
| 153 | rest = smatch.group(3) |
| 154 | else: |
| 155 | print('Error parsing line "' + comp + '"') |
| 156 | print('at: "' + rest + '"') |
| 157 | rest = '' |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 158 | |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 159 | if len(pinlist) < 2: |
| 160 | print('Error: No device type found in line "' + comp + '"') |
| 161 | print('Tokens found are: ' + ', '.join(pinlist)) |
| 162 | continue |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 163 | |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 164 | instname = pinlist[0] |
| 165 | devtype = pinlist[-1] |
| 166 | pinlist = pinlist[0:-1] |
| 167 | |
| 168 | # Diagnostic |
| 169 | if debugmode: |
| 170 | print(' Adding component ' + devtype + ' instance ' + instname) |
| 171 | |
| 172 | mult = 1 |
| 173 | for param in paramlist: |
| 174 | parmname = param[0] |
| 175 | parmval = param[1] |
| 176 | if parmname.upper() == 'M': |
| 177 | try: |
| 178 | mult = int(parmval) |
| 179 | except ValueError: |
| 180 | # This takes care of multiplier expressions, as long |
| 181 | # as they don't reference parameter names themselves. |
| 182 | mult = eval(eval(parmval)) |
| 183 | |
| 184 | # devtype is assumed to be in library. If not, it will attempt to use |
| 185 | # 'getcell' on devtype. NOTE: Current usage is to not pass a library |
| 186 | # to netlist_to_layout.py but to rely on the PDK Tcl script to define |
| 187 | # variable PDKNAMESPACE, which is the namespace to use for low-level |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 188 | # components, and may not be the same name as the technology node. |
| 189 | if library: |
| 190 | libdev = library + '::' + devtype |
| 191 | else: |
| 192 | libdev = '${PDKNAMESPACE}::' + devtype |
| 193 | outparts = [] |
| 194 | outparts.append('magic::gencell ' + libdev + ' ' + instname) |
| 195 | |
| 196 | # Output all parameters. Parameters not used by the toolkit are ignored |
| 197 | # by the toolkit. |
| 198 | outparts.append('-spice') |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 199 | for param in paramlist: |
| 200 | outparts.append(str(param[0]).lower()) |
| 201 | outparts.append(param[1]) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 202 | |
| 203 | outstring = ' '.join(outparts) |
| 204 | print('if {[catch {' + outstring + '}]} {', file=ofile) |
| 205 | print(' get_and_move_inst ' + devtype + ' ' + instname |
| 206 | + ' ' + str(mult), file=ofile) |
| 207 | print('} else {', file=ofile) |
| 208 | print(' move_forward ' + instname, file=ofile) |
| 209 | print('}', file=ofile) |
| 210 | print('', file=ofile) |
| 211 | print('save ' + subname, file=ofile) |
| 212 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 213 | def generate_layout_end(ofile=sys.stdout): |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 214 | global debugmode |
| 215 | |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 216 | print('resumeall', file=ofile) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 217 | print('writeall force', file=ofile) |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 218 | print('quit -noprompt', file=ofile) |
| 219 | |
Tim Edwards | 708a942 | 2021-03-09 22:07:22 -0500 | [diff] [blame] | 220 | def parse_layout(topname, lines, library, ofile): |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 221 | global debugmode |
| 222 | |
| 223 | subrex = re.compile('.subckt[ \t]+(.*)$', re.IGNORECASE) |
| 224 | devrex = re.compile('[xmcrbdivq]([^ \t]+)[ \t](.*)$', re.IGNORECASE) |
| 225 | namerex = re.compile('([^= \t]+)[ \t]+(.*)$', re.IGNORECASE) |
| 226 | endsrex = re.compile('^[ \t]*\.ends', re.IGNORECASE) |
| 227 | |
| 228 | insub = False |
| 229 | subname = '' |
| 230 | subpins = '' |
| 231 | complist = [] |
| 232 | toplist = [] |
| 233 | |
| 234 | for line in lines: |
| 235 | if not insub: |
| 236 | lmatch = subrex.match(line) |
| 237 | if lmatch: |
| 238 | rest = lmatch.group(1) |
| 239 | smatch = namerex.match(rest) |
| 240 | if smatch: |
| 241 | subname = smatch.group(1) |
| 242 | subpins = smatch.group(2) |
| 243 | insub = True |
| 244 | else: |
| 245 | print('File ' + inputfile + ': Failure to parse line ' + line) |
| 246 | else: |
| 247 | dmatch = devrex.match(line) |
| 248 | if dmatch: |
| 249 | toplist.append(line) |
| 250 | else: |
| 251 | lmatch = endsrex.match(line) |
| 252 | if lmatch: |
| 253 | insub = False |
| 254 | generate_layout_add(subname, subpins, complist, library, ofile) |
| 255 | subname = None |
| 256 | subpins = None |
| 257 | complist = [] |
| 258 | else: |
| 259 | dmatch = devrex.match(line) |
| 260 | if dmatch: |
| 261 | complist.append(line) |
| 262 | |
| 263 | # Add any top-level components |
| 264 | if toplist: |
| 265 | generate_layout_add(topname, None, toplist, library, ofile) |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 266 | |
| 267 | def usage(): |
| 268 | print('Usage:') |
| 269 | print(' netlist_to_layout.py <filename> [<namespace>] [-options]') |
| 270 | print('') |
| 271 | print('Arguments:') |
| 272 | print(' <filename> is the path to the SPICE netlist to import to magic') |
| 273 | print(' <namespace> is the namespace of the PDK') |
| 274 | print('') |
| 275 | print('Options:') |
| 276 | print(' -keep Keep the working script after completion.') |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 277 | print(' -debug Provide verbose output while generating script.') |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 278 | print(' -help Print this help text.') |
| 279 | |
| 280 | # Main procedure |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 281 | |
| 282 | if __name__ == '__main__': |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 283 | global debugmode |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 284 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 285 | # Parse command line for options and arguments |
| 286 | optionlist = [] |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 287 | arguments = [] |
| 288 | for item in sys.argv[1:]: |
| 289 | if item.find('-', 0) == 0: |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 290 | optionlist.append(item) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 291 | else: |
| 292 | arguments.append(item) |
| 293 | |
| 294 | if len(arguments) > 0: |
| 295 | inputfile = arguments[0] |
| 296 | if len(arguments) > 1: |
| 297 | library = arguments[1] |
| 298 | else: |
| 299 | library = None |
| 300 | else: |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 301 | usage() |
| 302 | sys.exit(0) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 303 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 304 | debugmode = False |
| 305 | keepmode = False |
| 306 | |
| 307 | for item in optionlist: |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 308 | result = item.split('=') |
| 309 | if result[0] == '-help': |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 310 | usage() |
| 311 | sys.exit(0) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 312 | elif result[0] == '-debug': |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 313 | debugmode = True |
| 314 | elif result[0] == '-keep': |
| 315 | keepmode = True |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 316 | else: |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 317 | usage() |
| 318 | sys.exit(1) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 319 | |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 320 | netpath = os.path.split(inputfile)[0] |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 321 | if netpath == '': |
| 322 | netpath = os.getcwd() |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 323 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 324 | if os.path.splitext(inputfile)[1] == '.sch': |
| 325 | print('Sorry, automatic conversion of schematic to netlist not yet supported.') |
| 326 | sys.exit(1) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 327 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 328 | netroot = os.path.split(netpath)[0] |
| 329 | magpath = os.path.join(netroot, 'mag') |
| 330 | if not os.path.isdir(magpath): |
| 331 | print('Error: Layout path "' + magpath + '" does not exist or is not readable.') |
| 332 | sys.exit(1) |
| 333 | |
| 334 | # NOTE: There should be some attempt to find the installed PDK magicrc file |
| 335 | # if there is no mag/ directory. |
| 336 | rcfile = '.magicrc' |
| 337 | rcfilepath = os.path.join(magpath, rcfile) |
| 338 | if not os.path.isfile(rcfilepath): |
| 339 | print('Error: No startup script file "' + rcfilepath + '"') |
| 340 | sys.exit(1) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 341 | |
| 342 | # Read SPICE netlist |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 343 | with open(inputfile, 'r') as ifile: |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 344 | if debugmode: |
| 345 | print('Reading file ' + inputfile) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 346 | spicetext = ifile.read() |
| 347 | |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 348 | # Contatenate continuation lines |
| 349 | spicelines = spicetext.replace('\n+', ' ').splitlines() |
| 350 | |
Tim Edwards | 3e4e81e | 2021-02-13 17:13:05 -0500 | [diff] [blame] | 351 | filename = os.path.split(inputfile)[1] |
| 352 | topname = os.path.splitext(filename)[0] |
| 353 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 354 | scriptfile = 'generate_layout.tcl' |
| 355 | scriptpath = os.path.join(magpath, scriptfile) |
Tim Edwards | 59a7846 | 2020-12-22 10:34:20 -0500 | [diff] [blame] | 356 | |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 357 | with open(scriptpath, 'w') as ofile: |
| 358 | generate_layout_start(library, ofile) |
Tim Edwards | 708a942 | 2021-03-09 22:07:22 -0500 | [diff] [blame] | 359 | parse_layout(topname, spicelines, library, ofile) |
Tim Edwards | 1168a8b | 2021-02-10 22:06:54 -0500 | [diff] [blame] | 360 | generate_layout_end(ofile) |
| 361 | |
| 362 | myenv = os.environ.copy() |
| 363 | myenv['MAGTYPE'] = 'mag' |
| 364 | |
| 365 | # Run the layout generator |
| 366 | mproc = subprocess.run(['magic', '-dnull', '-noconsole', '-rcfile', |
| 367 | rcfile, scriptfile], |
| 368 | stdin = subprocess.DEVNULL, stdout = subprocess.PIPE, |
| 369 | stderr = subprocess.PIPE, cwd = magpath, |
| 370 | env = myenv, universal_newlines = True) |
| 371 | if mproc.stdout: |
| 372 | for line in mproc.stdout.splitlines(): |
| 373 | print(line) |
| 374 | if mproc.stderr: |
| 375 | print('Error message output from magic:') |
| 376 | for line in mproc.stderr.splitlines(): |
| 377 | print(line) |
| 378 | if mproc.returncode != 0: |
| 379 | print('ERROR: Magic exited with status ' + str(mproc.returncode)) |
| 380 | |
| 381 | # Clean up |
| 382 | if not keepmode: |
| 383 | os.remove(scriptpath) |
| 384 | |
| 385 | sys.exit(0) |