blob: f597a905314a69dfa14a65e59e4db76852eddbd8 [file] [log] [blame]
Tim Edwards55f4d0e2020-07-05 15:41:02 -04001#!/usr/bin/env python3
2"""
3cdl2spi.py : netlist processor
4Copyright (c) 2016, 2020 efabless Corporation.
5All rights reserved.
6
7usage: cdl2spi.py <inCDLfile> [<outSPCfile>] [options...]
8Writes to .spi to outSPCfile, or stdout if no output argument given. Sets exit
9status if there were non-zero errors. Most errors/warnings are annotated in-line
10in the stdout each before the relevant line.
11"""
12
13import sys, getopt
14import os
15import re
16import textwrap
17
18# Convert linear scale to area scale suffix
19# (e.g., if linear scale is 1e-6 ('u') then area scales as 1e-12 ('p'))
20
21def getAreaScale(dscale):
22 ascale = ''
23 if dscale == 'm':
24 ascale = 'u'
25 elif dscale == 'u':
26 ascale = 'p'
27 elif dscale == 'n':
28 ascale = 'a'
29 return ascale
30
31# Check nm (instanceName) in the context of sub (subckt): is it used yet?
32# If not used yet, mark it used, and return as-is.
33# Else generate a unique suffixed version, and mark it used, return it.
34# If 1M suffixes don't generate a unique name, throw exception.
35# hasInm : global hash, key of hash is (subckt, iname)
36
37hasInm = {}
38def uniqInm(sub, nm):
39 subl=sub.lower()
40 nml=nm.lower()
41 if not (subl, nml) in hasInm:
42 hasInm[ (subl, nml) ] = 1
43 return nm
44 for i in range(1000000):
45 nm2 = nm + "_q" + str(i)
46 nm2l = nm2.lower()
47 if not (subl, nm2l) in hasInm:
48 hasInm[ (subl, nm2l) ] = 1
49 return nm2
50 # not caught anywhere, and gives (intended) non-zero exit status
51 raise AssertionError("uniqInm: range overflow for (%s,%s)" % (sub, nm))
52
53# Map illegal characters in an nm (instanceName) in context of sub (subckt).
54# For ngspice, '/' is illegal in instanceNames. Replace it with '|', BUT
55# then make sure the new name is still unique: does not collide with a name
56# used so far or another already derived unique name.
57
58inmBadChars='/'
59inmRplChars='|'
60inmBadCharREX=re.compile( "["+ inmBadChars+"]" )
61
62def mapInm(sub, nm):
63 nm2 = inmBadCharREX.sub(inmRplChars, nm)
64 return uniqInm(sub, nm2)
65
66# Process subckt line (array of tokens). Return new array of tokens.
67# There might be a ' /' in the line that needs to be deleted. It may be standalone ' / ', or
68# butting against the next token. It may be before all pins, after all pins, or between pins.
69# Do not touch / in a parameter assignment expression.
70# Do not touch / embedded in a pinName.
71# May touch / butting front of very first parameter assignment expression.
72# .subckt NM / p1 p2 p3 x=y g=h
73# .subckt NM /p1 p2 p3 x=y g=h
74# .subckt NM p1 p2 / p3 x=y g=h
75# .subckt NM p1 p2 /p3 x=y g=h
76# .subckt NM p1 p2 p3 / x=y g=h
77# .subckt NM p1 p2 p3 /x=y g=h
78# .subckt NM p1 p2 p3 x=y g=(a/b) (don't touch this /)
79# .subckt NM p1 p2/3/4 p3 x=y g=(a/b) (don't touch these /)
80
81def mapSubcktDef(tok):
82 # find index of one-past first token (beyond ".subckt NM") containing an =, if any
83 param0 = len(tok)
84 for i in range(2, len(tok)):
85 if '=' in tok[i]:
86 param0 = i+1
87 break
88 # find first token before or including that 1st-param, starting with /:
89 # strip the slash.
90 for i in range(2, param0):
91 if tok[i][0] == '/':
92 tok[i] = tok[i][1:]
93 if tok[i] == "":
94 del tok[i]
95 break
96 return tok
97
98def test_mapSubcktInst1():
99 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 p3".split())))
100 print( " ".join(mapSubcktDef( ".subckt abc / p1 p2 p3".split())))
101 print( " ".join(mapSubcktDef( ".subckt abc /p1 p2 p3".split())))
102 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 /p3".split())))
103 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 / p3".split())))
104 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 p3 x=4 /y=5".split())))
105 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 p3 x=4/2 y=5".split())))
106 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 p3 / x=4/2 y=5".split())))
107 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 p3 x=4/2 /y=5".split())))
108 print( " ".join(mapSubcktDef( ".subckt abc p1 p2 p3 /x=4/2 y=5".split())))
109 print( " ".join(mapSubcktDef( ".subckt abc p1/2/3 p2 p3 /x=4/2 y=5".split())))
110
111# Process subckt instance line (array of tokens). Return new array of tokens.
112# (This function does not map possible illegal-chars in instanceName).
113# There might be a ' /' in the line that needs to be deleted. It may be standalone ' / ', or
114# butting against the next token. It can only be after pins, before or butting subcktName.
115#
116# Do not touch / in, butting, or after 1st parameter assignment expression.
117# Do not touch / embedded in a netName.
118# Do not touch / embedded in instanceName (they are handled separately elsewhere).
119# xa/b/c p1 p2 p3 / NM x=y g=h
120# xa/b/c p1 p2 p3 /NM x=y g=h
121# xabc p1 p2/3/4 p3 /NM x=(a/b) g=h
122# xabc p1 p2/3/4 p3 / NM x=(a/b) g=h
123# xabc p1 p2/3/4 p3 NM x=(a/b) / g=h (don't touch; perhaps needs to be an error trapped somewhere)
124# xabc p1 p2/3/4 p3 NM / x=(a/b) g=h (don't touch; perhaps needs to be an error trapped somewhere)
125# xa/b/c p1 p2/3/4 p3 NM x=(a/b) g=h (don't touch these /)
126
127def mapSubcktInst(tok):
128 # find index of first token (beyond "x<iname>") containing an =, if any
129 param0 = tlen = len(tok)
130 for i in range(1, tlen):
131 if '=' in tok[i]:
132 param0 = i
133 break
134 # Determine modelName index. Either just prior to 1st-param (if any) else last token.
135 modndx = tlen - 1
136 if param0 < tlen:
137 modndx = param0 - 1;
138 # If modndx now points to a standalone /, that can't be (would yield missing/empty modelName).
139 # Actual modelName must be before it. We need to check, possibly strip / on/before actual modelName.
140 # (Even though standlone / after model are most likely an independent error: we don't touch 'em).
141 while modndx > 1 and tok[modndx] == "/":
142 modndx-=1
143 # Check for standalone / before modelName. Else for modelName starting with /.
144 slashndx = modndx - 1
145 if slashndx > 0 and tok[slashndx] == "/":
146 del tok[slashndx]
147 else:
148 if modndx > 0 and tok[modndx].startswith("/"):
149 tok[modndx] = tok[modndx][1:]
150 return tok
151
152def test_mapSubcktInst2():
153 print( " ".join(mapSubcktInst( "xa/b/c p1 p2 p3 / NM x=y g=h".split())))
154 print( " ".join(mapSubcktInst( "xa/b/c p1 p2 p3 /NM x=y g=h".split())))
155 print( " ".join(mapSubcktInst( "xabc p1 p2/3/4 p3 /NM x=(a/b) g=h".split())))
156 print( " ".join(mapSubcktInst( "xabc p1 p2/3/4 p3 / NM x=(a/b) g=h".split())))
157 print( " ".join(mapSubcktInst( "xabc p1 p2/3/4 p3 NM x=(a/b) / g=h".split())))
158 print( " ".join(mapSubcktInst( "xabc p1 p2/3/4 p3 NM / x=(a/b) g=h".split())))
159 print( " ".join(mapSubcktInst( "xabc p1 p2/3/4 p3 /NM / x=(a/b) g=h".split())))
160 print( " ".join(mapSubcktInst( "xabc p1 p2/3/4 p3 / NM / x=(a/b) g=h".split())))
161 print( " ".join(mapSubcktInst( "xa/b/c p1 p2/3/4 p3 NM x=(a/b) g=h".split())))
162 print( " ".join(mapSubcktInst( "xa/b/c NM x=(a/b) g=h".split())))
163 print( " ".join(mapSubcktInst( "xa/b/c / NM x=(a/b) g=h".split())))
164 print( " ".join(mapSubcktInst( "xa/b/c /NM x=(a/b) g=h".split())))
165 print( " ".join(mapSubcktInst( "xa/b/c /NM".split())))
166
167# Primitives with M=<n> need to add additional par1=<n>.
168# Process token list, return new token list.
169# note: line at this point may be like: m... p1 p2 p3 p4 NMOS M=1 $blah W=... L=...
170# meaning M=1 is not necessarily in a block of all parameter-assignments at EOL.
171# But by processing the line from end backwards, we pick up LAST M= if there are
172# multiple (which condition really should get flagged as an error).
173# And M= is more likely towards end of the line than front of line (thus faster).
174# If "M=" with no value, do nothing (should also be a flagged error).
175
176def mapMfactor(tok, options={}):
177 # find index of M=* if any, starting from end.
178 # "addinm" is an additional parameter that takes the same argument as M
179 addinm = options['addinm'] if 'addinm' in options else []
180 mndx = 0
181 val = ""
182 for i in range(len(tok)-1, 0, -1):
183 if tok[i].lower().startswith("m="):
184 mndx = i
185 break
186 if mndx > 0:
187 val = tok[i][2:]
188 if val != "":
189 for p in addinm:
190 tok += [ addinm + val]
191 return tok
192
193def test_mapMfactor():
194 print( " ".join(mapMfactor( "m1 p1 p2 p3 p4 NM M=joe".split())))
195 print( " ".join(mapMfactor( "m1 p1 p2 p3 p4 NM M= $SUB=agnd".split())))
196 print( " ".join(mapMfactor( "m1 p1 p2 p3 p4 NM M=2 $SUB=agnd WM=4".split())))
197 print( " ".join(mapMfactor( "m1 p1 p2 p3 p4 NM".split())))
198
199# From $nm=... strip the $. Preserve order on the line. No attempt to
200# detect any resultant collisions. "W=5 $W=10" becomes "W=5 W=10".
201# Don't touch $SUB=... or $[...] or $.model=... or $blah (no assigment).
202
203def mapCDLparam(tok):
204 for i in range(1, len(tok)):
205 if not tok[i].startswith("$"):
206 continue
207 eqi = tok[i].find("=")
208 if eqi > 1:
209 pnm = tok[i][1:eqi]
210 pnml = pnm.lower()
211 if pnml in ("sub",".model"):
212 continue
213 tok[i] = tok[i][1:]
214 return tok
215
216def test_CDLparam():
217 print( " ".join(mapCDLparam( "m1 p1 p2 p3 p4 NM M=joe".split())))
218 print( " ".join(mapCDLparam( "m1 p1 p2 p3 p4 NM M= $SUB=agnd $.model=NM3 $LDD".split())))
219 print( " ".join(mapCDLparam( "m1 p1 p2 p3 p4 NM M= $SUB=agnd $[NM3]".split())))
220 print( " ".join(mapCDLparam( "m1 p1 p2 p3 p4 NM M=joe $X=y".split())))
221 print( " ".join(mapCDLparam( "m1 p1 p2 p3 p4 NM M= $SUB=agnd $.model=NM3 $Z=4 $Z=5".split())))
222 print( " ".join(mapCDLparam( "m1 p1 p2 p3 p4 NM M= W=1 $W=2 W=3 $SUB=agnd $[NM3]".split())))
223
224# Extract $SUB=<tname>. and $[mnm] (or $.model=<mnm>) from tokens.
225# Return array of three items: [ <tname>, <mnm>, tok ] where tok is remainder.
226# Absent $SUB= or model directives give "".
227# Since we delete tokens, process tokens in reverse order.
228
229def mapCDLtermModel(tok):
230 cdlTerm=""
231 cdlModel=""
232 for i in range(len(tok)-1, 0, -1):
233 if not tok[i].startswith("$"):
Tim Edwardsf00bbc62020-10-14 17:29:58 -0400234 if '=' in tok[i]:
Tim Edwardsdc8149d2020-10-13 14:28:03 -0400235 continue
236 elif cdlModel == '':
237 cdlModel = tok[i]
238 del tok[i]
239 break
Tim Edwards55f4d0e2020-07-05 15:41:02 -0400240 tokl = tok[i].lower()
241 if tokl.startswith("$sub="):
242 if cdlTerm == "":
243 cdlTerm = tok[i][5:]
244 del tok[i]
245 continue
246 if tokl.startswith("$.model="):
247 if cdlModel == "":
248 cdlModel = tok[i][8:]
249 del tok[i]
250 continue
251 if tokl.startswith("$[") and tokl.endswith("]"):
252 if cdlModel == "":
253 cdlModel = tok[i][2:-1]
254 del tok[i]
255 continue
256 return [ cdlTerm, cdlModel, tok ]
257
258def test_CDLtermModel():
259 print( mapCDLtermModel( "m1 p1 p2 p3 p4 NM M=joe".split()))
260 print( mapCDLtermModel( "m1 p1 p2 p3 p4 NM $SUB=agnd".split()))
261 print( mapCDLtermModel( "m1 p1 p2 p3 p4 NM $SUB= $[PMOS] M=joe".split()))
262 print( mapCDLtermModel( "m1 p1 p2 p3 p4 NM $sUb=vssa $.MoDeL=PM4 M=joe".split()))
263
264# Determine if a single word looks like a plain numeric spice value.
265# It means a real-number with optional scale suffix, and optional unit suffix.
266# Only unit-suffix we support is m (meters) (because CDL-std describes it).
267# Only scale factors supported are: t,g,meg,k,mil,m,u,n,p,f
268# This does not arithmetically compute anything.
269# Just returns True or False.
270# 220p 10nm -40g 2milm .34e+3 3.1e-4 .34e+3pm 3.1e-4meg
271# (Arguable we should strip a unit-suffix)?
272# def isPlainNumeric(word):
273
274# Segregate any remaining $* items from input tokens.
275# Return [ assignments, directives, remaining ] where each are lists.
276# Those that look like assigments $nm=... are separated from $blah.
277
278def mapDiscard(tok):
279 tlen = len(tok)
280 assign=[]
281 directive=[]
282 for i in range(len(tok)-1, 0, -1):
283 if not tok[i].startswith("$"):
284 continue
285 if "=" in tok[i]:
286 assign += [ tok[i] ]
287 del tok[i]
288 continue
289 directive += [ tok[i] ]
290 del tok[i]
291 return [ assign, directive, tok ]
292
293def test_mapDiscard():
294 print( mapDiscard( "m1 p1 p2 p3 p4 NM $X=4 $LDD M=joe $SUB=agnd ".split()))
295 print( mapDiscard( "m1 p1 p2 p3 p4 NM $X $LDD M=joe $SUB=agnd ".split()))
296 print( mapDiscard( "m1 p1 p2 p3 p4 NM M=joe SUB=agnd ".split()))
297
298# From a token-slice, partition into assignments and non-assignments.
299# Return [ assigns, nonAssigns] where each are lists.
300
301def mapPartAssign(tok):
302 tlen = len(tok)
303 assign=[]
304 nona=[]
305 for i in range(len(tok)):
306 if "=" in tok[i]:
307 assign += [ tok[i] ]
308 continue
309 nona += [ tok[i] ]
310 return [ assign, nona ]
311
312def test_mapPartAssign():
313 print( mapPartAssign( "NM X=4 220nm -1.2e-5g LDD M=joe".split()))
314 print( mapPartAssign( "X=4 M=joe".split()))
315 print( mapPartAssign( "NM 220nm -1.2e-5g LDD".split()))
316 print( mapPartAssign( "".split()))
317
318# Find an assignment to nm in the token list (nm=val).
319# Return [val, tok]. If edit is True, the nm=val is removed from return tok.
320# If multiple nm=... the last one is used. If del is True, all nm=... are removed.
321
322def mapLookup(tok, nm, edit):
323 tlen = len(tok)
324 val=""
325 nmeq = nm.lower() + "="
326 nmeqlen = len(nmeq)
327 for i in range(len(tok)-1, 0, -1):
328 if not tok[i].lower().startswith(nmeq):
329 continue
330 if val == "":
331 val = tok[i][nmeqlen:]
332 if edit:
333 del tok[i]
334 return [ val, tok ]
335
336def test_mapLookup():
337 print( mapLookup( "cnm t1 t2 area=220p PERimeter=100u M=joe par1=1".split(), "periMETER", True))
338 print( mapLookup( "m1 p1 p2 p3 p4 NM $X=4 $LDD M=joe $SUB=agnd ".split(), "x", True))
339 print( mapLookup( "m1 p1 p2 p3 p4 NM X=4 $LDD M=joe $SUB=agnd ".split(), "x", True))
340 print( mapLookup( "m1 p1 p2 p3 p4 NM x=4 $LDD M=joe $SUB=agnd ".split(), "x", True))
341 print( mapLookup( "m1 p1 p2 p3 p4 NM x=4 X=5 xy=6 $LDD M=joe $SUB=agnd ".split(), "x", True))
342 print( mapLookup( "m1 p1 p2 p3 p4 NM x=4 X=5 xy=6 $LDD M=joe $SUB=agnd ".split(), "x", False))
343
344# Format a diode. cdlTerm and cdlModel are passed in but ignored/unused.
345# Processes tok and returns a final token list to be output.
346# If after "dnm t1 t2 modelName ", there are plain numerics (index 4,5), take them as area and peri,
347# (override other area= peri= parameters), format them as area=... peri=...
348# (Caller already error checked the 1st minimum FOUR fields are there).
349
350def mapDiode(cdlTerm, cdlModel, tok, options={}):
351 ignore = options['ignore'] if 'ignore' in options else []
352 # strip remaining $* directives
353 [ ign, ign, tok ] = mapDiscard(tok)
354 # Find explicit area= peri=, remove from tok.
355 [area, tok] = mapLookup(tok, "area", True)
356 [peri, tok] = mapLookup(tok, "peri", True)
357 for p in ignore:
358 [ign, tok] = mapLookup(tok, p, True)
359 # For just token-slice after modelName, partition into assignments and non-assigns.
360 [assign, nona] = mapPartAssign(tok[4:])
361 tok = tok[0:4]
362 # TODO: If we have more than two non-assignments it should be an error?
363 # Override area/peri with 1st/2nd non-assigment values.
364 if len(nona) > 0:
365 area = nona.pop(0)
366 if len(nona) > 0:
367 peri = nona.pop(0)
368 if area != "":
369 tok += [ "area=" + area ]
370 if peri != "":
371 tok += [ "peri=" + peri ]
372 tok += nona
373 tok += assign
374 return tok
375
376def test_mapDiode():
377 print( mapDiode( "", "", "dnm t1 t2 DN 220p 100u M=joe par1=1".split()))
378 print( mapDiode( "", "", "dnm t1 t2 DN peri=100u area=220p M=joe par1=1".split()))
379 print( mapDiode( "", "", "dnm t1 t2 DN M=joe par1=1".split()))
380
381# Format a mosfet. cdlTerm and cdlModel are passed in but ignored/unused.
382# Processes tok and returns a final token list to be output.
383# If after "mnm t1 t2 t3 t4 modelName ", there are plain numerics (index 6,7), take them as W and L,
384# (override other W= L= parameters), format them as W=... L=...
385# (Caller already error checked the 1st minimum SIX fields are there).
386
387def mapMos(cdlTerm, cdlModel, tok, options={}):
388 ignore = options['ignore'] if 'ignore' in options else []
389 # strip remaining $* directives
390 [ ign, ign, tok ] = mapDiscard(tok)
391 # Find explicit W= L=, remove from tok.
392 [w, tok] = mapLookup(tok, "w", True)
393 [l, tok] = mapLookup(tok, "l", True)
394 for p in ignore:
395 [ign, tok] = mapLookup(tok, p, True)
396 # For scaling, find AS, PS, AD, PD, SA, SB, SC, and SD
397 [sarea, tok] = mapLookup(tok, "as", True)
398 [darea, tok] = mapLookup(tok, "ad", True)
399 [sperim, tok] = mapLookup(tok, "ps", True)
400 [dperim, tok] = mapLookup(tok, "pd", True)
401 [sa, tok] = mapLookup(tok, "sa", True)
402 [sb, tok] = mapLookup(tok, "sb", True)
403 [sd, tok] = mapLookup(tok, "sd", True)
404
405 dscale = options['dscale'] if 'dscale' in options else ''
406 ascale = getAreaScale(dscale)
407
408 # For just token-slice after modelName, partition into assignments and non-assigns.
409 [assign, nona] = mapPartAssign(tok[6:])
410 tok = tok[0:6]
411 # TODO: If we have more than two non-assignments it should be an error?
412 # Override W/L with 1st/2nd non-assigment values.
413 if len(nona) > 0:
414 w = nona.pop(0)
415 if len(nona) > 0:
416 l = nona.pop(0)
417 if w != "":
418 tok += ["W=" + w + dscale]
419 if l != "":
420 tok += ["L=" + l + dscale]
421 if darea != "":
422 tok += ["AD=" + darea + ascale]
423 if sarea != "":
424 tok += ["AS=" + sarea + ascale]
425 if dperim != "":
426 tok += ["PD=" + dperim + dscale]
427 if sperim != "":
428 tok += ["PS=" + sperim + dscale]
429 if sa != "":
430 tok += ["SA=" + sa + dscale]
431 if sb != "":
432 tok += ["SB=" + sb + dscale]
433 if sd != "":
434 tok += ["SD=" + sd + dscale]
435 tok += nona
436 tok += assign
437 return tok
438
439def test_mapMos():
440 print( mapMos( "", "", "mnm t1 t2 t3 t4 NM 220p 100u M=joe par1=1".split()))
441 print( mapMos( "", "", "mnm t1 t2 t3 t4 NM L=100u W=220p M=joe par1=1".split()))
442 print( mapMos( "", "", "mnm t1 t2 t3 t4 PM M=joe par1=1".split()))
443
444# Format a cap.
445# Processes tok and returns a final token list to be output.
446# Optional cdlTerm adds a 3rd terminal.
447# If after "cnm t1 t2 ", there is plain numeric or C=numeric they are DISCARDED.
448# area/peri/perimeter assignments are respected. Both peri/perimeter assign to perm=
449# in the output. No perimeter= appears in the output.
450# (Caller already error checked the 1st minimum 3 fields are there; plus cdlModel is non-null).
451
452def mapCap(cdlTerm, cdlModel, tok, options={}):
453 ignore = options['ignore'] if 'ignore' in options else []
454 # strip remaining $* directives
455 [ ign, ign, tok ] = mapDiscard(tok)
456 # Find explicit area= peri= perimeter=, remove from tok. peri overwrites perimeter,
457 # both assign to perim. Lookup/discard a C=.
458 [area, tok] = mapLookup(tok, "area", True)
459 [perim, tok] = mapLookup(tok, "perimeter", True)
460 [length, tok] = mapLookup(tok, "l", True)
461 [width, tok] = mapLookup(tok, "w", True)
462 [peri, tok] = mapLookup(tok, "peri", True)
463 if peri == "":
464 peri = perim
465 [ign, tok] = mapLookup(tok, "c", True)
466 for p in ignore:
467 [ign, tok] = mapLookup(tok, p, True)
468 # For just token-slice after modelName, partition into assignments and non-assigns.
469 # We ignore the nonassignments. Need remaining assignments for M= par1=.
470 [assign, nona] = mapPartAssign(tok[3:])
471 dscale = options['dscale'] if 'dscale' in options else ''
472 ascale = getAreaScale(dscale)
473 tok = tok[0:3]
474 if cdlTerm != "":
475 tok += [ cdlTerm ]
476 if cdlModel != "":
477 tok += [ cdlModel ]
478 if area != "":
479 tok += [ "area=" + area + ascale]
480 if peri != "":
481 tok += [ "peri=" + peri + dscale]
482 if length != "":
483 tok += [ "L=" + length + dscale]
484 if width != "":
485 tok += [ "W=" + width + dscale]
486 tok += assign
487 return tok
488
489def test_mapCap():
490 print( mapCap( "", "CPP", "cnm t1 t2 area=220p peri=100u M=joe par1=1".split()))
491 print( mapCap( "", "CPP", "cnm t1 t2 area=220p perimeter=100u M=joe par1=1".split()))
492 print( mapCap( "", "CPP", "cnm t1 t2 area=220p peri=199u perimeter=100u M=joe par1=1".split()))
493 print( mapCap( "", "CPP", "cnm t1 t2 M=joe par1=1".split()))
494 print( mapCap( "", "CPP", "cnm t1 t2 C=444 area=220p peri=199u perimeter=100u M=joe par1=1".split()))
495 print( mapCap( "", "CPP", "cnm t1 t2 444 M=joe par1=1".split()))
496 print( mapCap( "agnd", "CPP2", "cnm t1 t2 $LDD 220p M=joe par1=1".split()))
497
498# Format a res.
499# Processes tok and returns a final token list to be output.
500# Optional cdlTerm adds a 3rd terminal.
501# If after "rnm t1 t2 ", there is plain numeric or R=numeric they are DISCARDED.
502# W/L assignments are respected.
503# (Caller already error checked the 1st minimum 3 fields are there; plus cdlModel is non-null).
504
505def mapRes(cdlTerm, cdlModel, tok, options={}):
506 dscale = options['dscale'] if 'dscale' in options else ''
507 ignore = options['ignore'] if 'ignore' in options else []
508 # strip remaining $* directives
509 [ ign, ign, tok ] = mapDiscard(tok)
510 # Find explicit w/l, remove from tok.
511 # Lookup/discard a R=.
512 [w, tok] = mapLookup(tok, "w", True)
513 [l, tok] = mapLookup(tok, "l", True)
514 [r, tok] = mapLookup(tok, "r", True)
515 for p in ignore:
516 [ign, tok] = mapLookup(tok, p, True)
517 # For just token-slice after modelName, partition into assignments and non-assigns.
518 # We ignore the nonassignments. Need remaining assignments for M= par1=.
519 [assign, nona] = mapPartAssign(tok[3:])
520 if len(nona) > 0:
521 r = nona.pop(0)
522 tok = tok[0:3]
523 if cdlTerm != "":
524 tok += [ cdlTerm ]
525 if cdlModel != "":
526 tok += [ cdlModel ]
527 if w != "":
528 tok += [ "W=" + w + dscale]
529 if l != "":
530 tok += [ "L=" + l + dscale]
531 # Convert name "short" to zero resistance
532 if r == "short":
533 tok += [ "0" ]
534 tok += assign
535 return tok
536
537def test_mapRes():
538 print( mapRes( "", "RPP1", "rnm t1 t2 w=2 L=1 M=joe par1=1".split()))
539 print( mapRes( "", "RPP1", "rnm t1 t2 444 w=2 L=1 M=joe par1=1".split()))
540 print( mapRes( "", "RPP1", "rnm t1 t2 R=444 w=2 L=1 M=joe par1=1".split()))
541 print( mapRes( "", "R2", "rnm t1 t2 L=2 W=10 M=joe par1=1".split()))
542 print( mapRes( "", "RM2", "rnm t1 t2 area=220p perim=199u perimeter=100u M=joe par1=1".split()))
543 print( mapRes( "", "RM2", "rnm t1 t2 M=joe par1=1".split()))
544 print( mapRes( "agnd", "RM3", "rnm t1 t2 $LDD 220p M=joe par1=1".split()))
545 print( mapRes( "agnd", "RM3", "rnm t1 t2 $LDD 220p L=4 W=12 M=joe par1=1".split()))
546
547# Format a bipolar. cdlTerm is optional. cdlModel is ignored.
548# Processes tok and returns a final token list to be output.
549# Optional cdlTerm adds an optional 4th terminal.
550# If after "qnm t1 t2 t3 model", there are plain numeric (not x=y) they are DISCARDED.
551# (Caller already error checked the 1st minimum 5 fields are there; plus cdlModel is null).
552
553def mapBipolar(cdlTerm, cdlModel, tok, options={}):
554 # strip remaining $* directives
555 ignore = options['ignore'] if 'ignore' in options else []
556 [ ign, ign, tok ] = mapDiscard(tok)
557 for p in ignore:
558 [ign, tok] = mapLookup(tok, p, True)
559 # For just token-slice after modelName, partition into assignments and non-assigns.
560 # We ignore the nonassignments. Need remaining assignments for M= par1=.
561 [assign, nona] = mapPartAssign(tok[5:])
562 # Start with "qnm t1 t2 t3". Insert optional 4th term. Then insert modelName.
563 model = tok[4]
564 tok = tok[0:4]
565 if cdlTerm != "":
566 tok += [ cdlTerm ]
567 tok += [ model ]
568 tok += assign
569 return tok
570
571def test_mapBipolar():
572 print( mapBipolar( "", "any", "qnm t1 t2 t3 QP1 M=joe par1=1".split()))
573 print( mapBipolar( "", "", "qnm t1 t2 t3 QP2 M=joe par1=1".split()))
574 print( mapBipolar( "", "", "qnm t1 t2 t3 QP2 $EA=12 M=joe par1=1".split()))
575 print( mapBipolar( "", "", "qnm t1 t2 t3 QP3 M=joe EA=14 par1=1".split()))
576 print( mapBipolar( "agnd", "", "qnm t1 t2 t3 QP4 $LDD 220p M=joe par1=1".split()))
577 print( mapBipolar( "agnd", "any", "qnm t1 t2 t3 QP4 $LDD 220p L=4 W=12 M=joe par1=1".split()))
578
579#------------------------------------------------------------------------
580# Main routine to do the conversion from CDL format to SPICE format
581#------------------------------------------------------------------------
582
583def cdl2spice(fnmIn, fnmOut, options):
584
585 err = 0
586 warn = 0
587
588 # Open and read input file
589
590 try:
591 with open(fnmIn, 'r') as inFile:
592 cdltext = inFile.read()
593 # Unwrap continuation lines
594 lines = cdltext.replace('\n+', ' ').splitlines()
595 except:
596 print('cdl2spi.py: failed to open ' + fnmIn + ' for reading.', file=sys.stderr)
597 return 1
598
599 # Loop over original CDL:
600 # record existing instanceNames (in subckt-context), for efficient membership
601 # tests later. Track the subckt-context, instanceNames only need to be unique
602 # within current subckt.
603
604 sub = ""
605 for i in lines:
606 if i == "":
607 continue
608 tok = i.split()
609 tlen = len(tok)
610 if tlen == 0:
611 continue
612 t0 = tok[0].lower()
613 if t0 == '.subckt' and tlen > 1:
614 sub = tok[1].lower()
615 continue
616 if t0 == '.ends':
617 sub = ""
618 continue
619 c0 = tok[0][0].lower()
620 if c0 in '.*':
621 continue
622 # this will ignore primitive-devices (jfet) we don't support.
623 # TODO: flag them somewhere else as an ERROR.
624 if not c0 in primch2:
625 continue
626 # a primitive-device or subckt-instance we care about and support
627 # For subckt-instances record the instanceName MINUS lead x.
628 nm = tok[0]
629 if c0 == 'x':
630 nm = nm[1:]
631 hasInm[ (sub, nm) ] = 1
632
633
634 # loop over original CDL: do conversions.
635 # Track the subckt-context while we go; instanceNames only need to be unique
636 # within current subckt.
637
638 sub = ""
639 tmp = []
640 for i in lines:
641 tok = i.split()
642 tlen = len(tok)
643 # AS-IS: empty line or all (preserved) whitespace
644 if tlen == 0:
645 tmp += [ i ]
646 continue
647
648 # get 1st-token original, as lowercase, and 1st-char of 1st-token lowercase.
649 T0 = tok[0]
650 t0 = T0.lower()
651 c0 = t0[0]
652
653 # AS-IS: comment
654 if c0 == '*':
655 tmp += [i]
656 continue
657
658 # AS-IS: .ends; update subckt-context to outside-of-a-subckt
659 if t0 == '.ends':
660 sub = ""
661 tmp += [i]
662 continue
663
664 # change .param to a comment, output it
665 if t0 == '.param':
666 tmp += ["*"+i]
667 continue
668
669 # track .subckt context; process / in .subckt line, and output it.
670 if t0 == '.subckt':
671 if tlen < 2:
672 err+=1
673 msg = "*cdl2spi.py: ERROR: Missing subckt name:"
674 tmp += [ msg, i ]
675 continue
676 T1 = tok[1]
677 sub = T1.lower()
678 tok = mapSubcktDef(tok)
679 tmp += [ " ".join(tok) ]
680 continue
681
682 # subckt instance line. Process /, map instanceName (exclude x), and output it.
683 if c0 == 'x':
684 nm = T0[1:]
685 if nm == "":
686 err+=1
687 msg = "*cdl2spi.py: ERROR: Missing subckt instance name:"
688 tmp += [ msg, i ]
689 continue
690 inm = mapInm(sub, nm)
691 tok[0] = T0[0] + inm
692 tok = mapSubcktInst(tok)
693 tmp += [ " ".join(tok) ]
694 continue
695
696 # all primitives: need instanceName mapped, including 1st char in name.
697 # all primitives: need M=n copied to an added par1=n
698 # all primitives: Except for $SUB=... $[...] strip $ from $nm=... parameters.
699 # all primitives: Isolate $SUB and $[...] for further processing (in
700 # primitive-specific sections).
701
702 cdlTerm=""
703 cdlModel=""
704 if c0 in primch:
705 nm = T0[1:]
706 if nm == "":
707 err+=1
708 msg = "*cdl2spi.py: ERROR: Missing primitive instance name:"
709 tmp += [ msg, i ]
710 continue
711 nm = T0
712 nm = mapInm(sub, nm)
713 tok[0] = nm
714 tok = mapMfactor(tok, options)
715 tok = mapCDLparam(tok)
716 [cdlTerm, cdlModel, tok] = mapCDLtermModel(tok)
717
718 # diode formats:
719 # dname t1 t2 model <numericA> <numericP> m=...
720 # l:dname t1 t2 model {<numericA>} {<numericP>} {m=...} {$SUB=...}
721 # out format:
722 # Xdname t1 t2 model area=<numericA> peri=<numericP> m=... par1=...
723 # We flag $SUB=... : because so far (for XFAB) we CHOOSE not to support three
724 # terminal diodes.
725 # CDL-std does not define $[...] as available for diodes, so we silently ignore
726 # it.
727 # Always 2 terminals and a modelName.
728 # We already have peri=... and area=... and have ambiguity with plain numerics.
729 # TODO: generate a warning in case of ambiguity, but prefer plain numerics
730 # (with nm= added).
731
732 if c0 == "d":
733 tlen = len(tok)
734 if tlen < 4:
735 err+=1
736 msg = "*cdl2spi.py: ERROR: Diode does not have minimum two terminals and model:"
737 tmp += [ msg, i ]
738 continue
739 if cdlTerm != "":
740 err+=1
741 msg = "*cdl2spi.py: ERROR: Diode does not support $SUB=...:"
742 tmp += [ msg, i ]
743 continue
744 tok = mapDiode(cdlTerm, cdlModel, tok, options)
745 # add X to tok0.
746 if options['subckt']:
747 tok[0] = "X" + tok[0]
748 tmp += [ " ".join(tok) ]
749 continue
750
751 # mosfet formats:
752 # mname t1 t2 t3 t4 model W=... L=... m=...
753 # l:mname t1 t2 t3 t4 model {W=... L=...} {m=...} {$NONSWAP} {$LDD[type]}
754 # l:mname t1 t2 t3 t4 model <width> <length> {m=...} {$NONSWAP} {$LDD[type]}
755 # output format:
756 # Xmname t1 t2 t3 t4 model W=... L=... m=... par1=...
757 # Fixed 4 terminals and a modelName.
758 # May already have W= L= and ambiguity with plain numerics.
759 # TODO: generate a warning in case of ambiguity, but prefer plain numerics
760 # (with nm= added).
761 if c0 == "m":
762 tlen = len(tok)
763 if tlen < 6:
764 err+=1
765 msg = "*cdl2spi.py: ERROR: Mosfet does not have minimum four terminals and model:"
766 tmp += [ msg, i ]
767 continue
768 if cdlTerm != "":
769 err+=1
770 msg = "*cdl2spi.py: ERROR: Mosfet does not support $SUB=...:"
771 tmp += [ msg, i ]
772 continue
773 tok = mapMos(cdlTerm, cdlModel, tok, options)
774 # add X to tok0.
775 if options['subckt']:
776 tok[0] = "X" + tok[0]
777 tmp += [ " ".join(tok) ]
778 continue
779
780 # cap formats:
781 # cname t1 t2 <numeric0> $[model] $SUB=t3 m=...
782 # cname t1 t2 <numeric0> $[model] m=...
783 #? cname t1 t2 C=<numeric0> $[model] $SUB=t3 m=...
784 #? cname t1 t2 <numeric0> $[model] $SUB=t3 area=<numericA> perimeter=<numericP> m=...
785 #? cname t1 t2 <numeric0> $[model] $SUB=t3 area=<numericA> peri=<numericP> m=...
786 #l:cname t1 t2 {<numeric0>} {$[model]} {$SUB=t3} {m=...}
787 # out formats:
788 # Xcname t1 t2 model area=<numericA> peri=<numericP> m=... par1=...
789 # Xcname t1 t2 t3 model area=<numericA> peri=<numericP> m=... par1=...
790 # We require inm, two terminals. Require $[model]. Optional 3rd-term $SUB=...
791 # If both peri and perimeter, peri overrides.
792 # Both area/peri are optional. The optional [C=]numeric0 is discarded always.
793
794 if c0 == "c":
795 tlen = len(tok)
796 if tlen < 3:
797 err+=1
798 msg = "*cdl2spi.py: ERROR: Cap does not have minimum two terminals:"
799 tmp += [ msg, i ]
800 continue
801 if cdlModel == "":
802 err+=1
803 msg = "*cdl2spi.py: ERROR: Cap missing required $[<model>] directive:"
804 tmp += [ msg, i ]
805 continue
806 tok = mapCap(cdlTerm, cdlModel, tok, options)
807 # add X to tok0.
808 if options['subckt']:
809 tok[0] = "X" + tok[0]
810 tmp += [ " ".join(tok) ]
811 continue
812
813 # res formats:
814 # rname n1 n2 <numeric> $SUB=t3 $[model] $w=... $l=... m=...
815 # c:rname n1 n2 R=<numeric> $[model] w=... l=... m=... $SUB=t3
816 # l:rname n1 n2 {<numeric>} {$SUB=t3} {$[model]} {$w=...} {$l=...} {m=...}
817 # (all after n1,n2 optional)
818 # We require $[model]. And add 3rd term IFF $SUB=.
819 # out format:
820 # Xrname n1 n2 t3 model w=... l=... m=... par1=...
821 if c0 == "r":
822 tlen = len(tok)
823 if tlen < 3:
824 err+=1
825 msg = "*cdl2spi.py: ERROR: Res does not have minimum two terminals:"
826 tmp += [ msg, i ]
827 continue
828 if cdlModel == "":
829 err+=1
830 msg = "*cdl2spi.py: ERROR: Res missing required $[<model>] directive:"
831 tmp += [ msg, i ]
832 continue
833 tok = mapRes(cdlTerm, cdlModel, tok, options)
834 # add X to tok0.
835 if options['subckt']:
836 tok[0] = "X" + tok[0]
837 tmp += [ " ".join(tok) ]
838 continue
839
840 # bipolar formats:
841 # qname n1 n2 n3 model <numeric> M=... $EA=...
842 # qname n1 n2 n3 model $EA=... <numeric> M=...
843 # qname n1 n2 n3 model {$EA=...} {$W=...} {$L=...} {$SUB=...} {M=...}
844 # No: l:qname n1 n2 n3 {nsub} model {$EA=...} {$W=...} {$L=...} {$SUB=...} {M=...}
845 # CDL-std adds {nsub} way to add substrate before model: We don't support it.
846 # Add 3rd term IFF $SUB=. We propagate optional W/L (or derived from $W/$L).
847 # EA is emitterSize; not supported by XFAB: deleted.
848 # We require 3-terminals and model. It is an error to specify $[model].
849 #
850 # out format:
851 # Xqname n1 n2 n3 model M=... par1=...
852 if c0 == "q":
853 tlen = len(tok)
854 if tlen < 5:
855 err+=1
856 msg = "*cdl2spi.py: ERROR: Bipolar does not have minimum three terminals and a model:"
857 tmp += [ msg, i ]
858 continue
859 if cdlModel != "":
860 err+=1
861 msg = "*cdl2spi.py: ERROR: Bipolar does not support $[<model>] directive:"
862 tmp += [ msg, i ]
863 continue
864 tok = mapBipolar(cdlTerm, cdlModel, tok, options)
865 # add X to tok0.
866 if options['subckt']:
867 tok[0] = "X" + tok[0]
868 tmp += [ " ".join(tok) ]
869 continue
870
871 # Anything else. What to do, preserve AS-IS with warning, or
872 # flag them as ERRORs?
873 tmp += [ "*cdl2spi.py: ERROR: unrecognized line:", i ]
874 err+=1
875 # tmp += [ "*cdl2spi.py: WARNING: unrecognized line:", " ".join(tok) ]
876 # tmp += [ "*cdl2spi.py: WARNING: unrecognized line:", i ]
877 # warn+=1
878
879 # Re-wrap continuation lines at 80 characters
880 lines = []
881 for line in tmp:
882 lines.append('\n+ '.join(textwrap.wrap(line, 80)))
883
884 # Write output
885
886 if fnmOut == sys.stdout:
887 for i in lines:
888 print(i)
889 else:
890 try:
891 with open(fnmOut, 'w') as outFile:
892 for i in lines:
893 print(i, file=outFile)
894 except:
895 print('cdl2spi.py: failed to open ' + fnmOut + ' for writing.', file=sys.stderr)
896 return 1
897
898 # exit status: indicates if there were errors.
899 print( "*cdl2spi.py: %d errors, %d warnings" % (err, warn))
900 return err
901
902if __name__ == '__main__':
903
904 options = {}
905
906 # Set option defaults
907 options['debug'] = False
908 options['subckt'] = False
909 options['dscale'] = ''
910 options['addinm'] = []
911 options['ignore'] = []
912
913 arguments = []
914 for item in sys.argv[1:]:
915 if item.find('-', 0) == 0:
916 thisopt = item.split('=')
917 optname = thisopt[0][1:]
918 optval = '='.join(thisopt[1:])
919 if not optname in options:
920 print('Unknown option -' + optname + '; ignoring.')
921 else:
922 lastoptval = options[optname]
923 if len(thisopt) == 1:
924 options[optname] = True
925 elif lastoptval == '':
926 options[optname] = optval
927 else:
928 options[optname].append(optval)
929 else:
930 arguments.append(item)
931
932 # Supported primitive devices (FET, diode, resistor, capacitor, bipolar)
933 primch = 'mdrcq'
934 primch2 = 'mdrcqx'
935
936 if len(arguments) > 0:
937 fnmIn = arguments[0]
938
939 if len(arguments) > 1:
940 fnmOut = arguments[1]
941 else:
942 fnmOut = sys.stdout
943
944 if options['debug']:
945 test_mapSubcktInst1()
946 test_mapSubcktInst2()
947 test_mapMfactor()
948 test_CDLparam()
949 test_CDLtermModel()
950 test_mapDiscard()
951 test_mapPartAssign()
952 test_mapLookup()
953 test_mapDiode()
954 test_mapMos()
955 test_mapCap()
956 test_mapRes()
957 test_mapBipolar()
958
959 elif len(arguments) > 2 or len(arguments) < 1 :
960 print('Usage: cdl2spi.py <cdlFileName> [<spiFileName>]')
961 print(' Options:' )
962 print(' -debug run debug tests')
963 print(' -dscale=<suffix> rescale lengths with <suffix>')
964 print(' -addinm=<param> add multiplier parameter <param>')
965 print(' -ignore=<param> ignore parameter <param>')
966 print(' -subckt convert primitive devices to subcircuits')
967 sys.exit(1)
968
969 else:
970 if options['debug'] == True:
971 print('Diagnostic: options = ' + str(options))
972 result = cdl2spice(fnmIn, fnmOut, options)
973 sys.exit(result)
974