有些人可能以為只有在超弦理論才會有超過十維空間的討論, 但其實IC設計這個圈子的工程師每天都在摸這個很玄的東東, 每天都在被其騷擾的不可開交呢!
IC實體設計工程師工作多年, 可能從沒認真看過所謂的setup & hold timing到底長麼樣子? 例如下圖, 是某製程工藝下一個multi-bit flip-flop描述在liberty裡的timing constraint. 它像是飄在不同PVT corner維度中兩張抖動的薄膜, 或許有人將它稱之為流形(manifold).而我們一般所討論的timing constraint(或setup & hold window)則指在某個PVT corner中, 某個clock & data transition的條件下兩張薄膜snapshot間的距離, 此時以z軸為零的平面當作clock capture的時間點, 而data不允許在這個window變化(稱之為violation).
由圖可知, 當clock transition固定在某個值, 這個violation window會隨著data transition劣化並且以非線性的關係擴大, 也就是setup & hold同時都變得非常糟. 反之, 若clock & data transition都做得太好(搞了很多buffers), 將會發現這個flip-flop先天skew for setup, 也就是對hold time不友善, 若巧妙的將clock & data都放緩則可以將這顆flip-flop與data path操作在較佳的甜蜜點上.
可能很多人沒認真想過, 萬一這兩張薄膜曲率不是單調的遞增或遞減(monotonic)? 例如在非常早期的28nm製程, 如下圖馬鞍面的情形可以預期3x3的lookup table肯定讓EDA tool在做interpolation時就搞掉了1~2ps的準確度, 所以無辜的工程師被告知「你得加一些uncertainty」! 說穿了, 很多margin都是許許多多對物理與資料科學的不求甚解造成的, 我們未來可以逐一揭發一些黑箱.
延續上面的思維, 我們可以把所有PVT corner加進來一起視覺化, 看看趨勢. 這點還蠻重要, 因為timing closure為何難搞? 為何EDA tool搞出一堆buffers? 或許有時候只是cell本身沒做在適合的工作區域喔!
有了這樣的基礎, 我們就可以寫個腳本把某製程元件庫所有flip-flop一次攤開來檢視一翻, 看看我們期望的大宗(majority)工作區域(例如clock/data transition=120/300ps@40˚C), 這些cell體質上是怎麼樣的期望值? 有時不難發現, 有些cell是skewed for setup, 有些是skewed for hold, 有些TT表現可以但SS弱掉或是FF與SS叉開遙遠, 有些timing window跨PVT corner overlap. 這下才知道, 或許有時候是cell從single-bit porting時沒做好導致 timing window shift, 有時某些skewed for setup/hold cell應該在synthesis階段禁用, 除非我們能善待所有data path上所有flops都在適合的工作區域, 並且不讓EDA tool胡搞瞎搞!
講了一些引述, 目的是要激起大家對資料科學的興趣, 而IC設計當然就得先從timing library分析著手. 不過蠻遺憾的, 總覺得這個廣為使用的standard “liberty”非常笨拙. 不清楚這個圈子的人為何可以忍受這結構著麼不嚴謹, 使用上又極沒效率的liberty檔案格式這麼久?
為了分析元件PVT特性並找出可能優化的契機, 各家的工程師只好浪費許多重複開發的工, 寫了一堆歪七扭八的parser(包括下面這支範例程式), 只為了梳理出那可能只佔3/1000的有用資訊. 以一天工作八小時為例, 可能得花上7小時的時間去搞那些沒必要的parser, 然後花55分鐘去釐清各家因為不嚴謹的檔案輸出格式與所開發程式之間交互產生的bug, 最後總算有5分鐘時間去分析資料. 為什麼這些EDA大角先進們不乾脆直接給個JSON就好了呢? 一行程式碼根本都不用寫啊!
import pandas as pd import gzip, re, json class Liberty: def __init__(self): self.content = [] self.libDec = { 'library':('value',''), 'technology':('value',''), 'delay_model':('value',''), 'slew_derate_from_library':('value',''), 'nom_process':('value',''), 'nom_temperature':('value',''), 'nom_voltage':('value',''), 'voltage_map':('value',''), 'capacitive_load_unit':('value',''), 'voltage_unit':('value',''), 'current_unit':('value',''), 'time_unit':('value',''), 'operating_conditions':('callback',self.parse_as_undefgroup), 'wire_load':('callback',self.parse_as_undefgroup), 'wire_load_selection':('callback',self.parse_as_undefgroup), 'lu_table_template':('callback',self.parse_as_undefgroup), 'power_lut_template':('callback',self.parse_as_undefgroup), 'normalized_driver_waveform':('callback',self.parse_as_undefgroup), 'cell':('callback',self.parse_as_cell), '_L':('bos',''), '_R':('eos','') } self.cellDec = { 'area':('value',0), 'antenna_diode_type':('waive',0), 'cell_footprint':('value',0), 'cell_leakage_power':('value',0), 'is_level_shifter':('value',0), 'is_clock_cell':('value',0), 'is_clock_isolation_cell':('value',0), 'is_isolation_cell':('value',0), 'always_on':('value',0), 'switch_cell_type':('value',''), 'retention_cell':('value',''), 'cell_description':('waive',''), 'user_function_class':('waive',''), 'level_shifter_type':('value',''), 'input_voltage_range':('value',''), 'output_voltage_range':('value',''), 'dont_use':('value',''), 'dont_touch':('value',''), 'define':('waive',''), 'clock_gating_integrated_cell':('value',''), 'scaling_factors':('callback',self.parse_as_undefgroup), 'intrinsic_parasitic':('callback',self.parse_as_undefgroup), 'dynamic_current':('callback',self.parse_as_undefgroup), 'test_cell':('callback',self.parse_as_undefgroup), 'statetable':('callback',self.parse_as_undefgroup), 'pg_pin':('callback',self.parse_as_undefgroup), 'leakage_power':('callback',self.parse_as_leakage_power), 'leakage_current':('callback',self.parse_as_undefgroup), 'ff':('callback',self.parse_as_undefgroup), 'ff_bank':('callback',self.parse_as_undefgroup), 'bundle':('callback',self.parse_as_bundle), 'latch':('callback',self.parse_as_undefgroup), 'latch_bank':('callback',self.parse_as_undefgroup), 'clock_condition':('callback',self.parse_as_undefgroup), 'clear_condition':('callback',self.parse_as_undefgroup), 'preset_condition':('callback',self.parse_as_undefgroup), 'retention_condition':('callback',self.parse_as_undefgroup), 'dc_current':('callback',self.parse_as_undefgroup), 'pin':('callback',self.parse_as_pin), '_L':('bos',''), '_R':('eos','') } self.bundDec = { 'members':('waive',''), 'direction':('waive',''), 'functione':('waive',''), 'power_down_function':('waive',''), 'pin':('callback',self.parse_as_pin), '_L':('bos',''), '_R':('eos','') } self.pinDec = { 'direction':('value',0), 'capacitance':('value',0), 'driver_type':('value',''), 'three_state':('value',''), 'clock':('value',''), 'clock_gate_clock_pin':('value',''), 'clock_gate_enable_pin':('value',''), 'clock_gate_test_pin':('value',''), 'clock_gate_out_pin':('value',''), 'state_function':('waive',''), 'internal_node':('waive',''), 'nextstate_type':('waive',''), 'driver_waveform_fall':('waive',''), 'driver_waveform_rise':('waive',''), 'related_ground_pin':('waive',''), 'related_power_pin':('waive',''), 'related_bias_pin':('waive',''), 'rise_capacitance':('value',''), 'fall_capacitance':('value',''), 'power_down_function':('waive',''), 'function':('value',''), 'max_capacitance':('waive',''), 'min_capacitance':('waive',''), 'max_transition':('value',0), 'antenna_diode_related_ground_pins':('waive',''), 'internal_power':('callback',self.parse_as_internal_power), 'timing':('callback',self.parse_as_timing), 'receiver_capacitance':('waive',self.parse_as_undefgroup), # CCSN '_L':('bos',''), '_R':('eos','') } self.timeDec={ 'related_pin':('value',''), 'timing_sense':('value',''), 'timing_type':('value',''), 'sdf_cond':('value',''), 'when':('value',''), 'cell_rise':('callback',self.parse_as_lut), 'cell_fall':('callback',self.parse_as_lut), 'rise_transition':('callback',self.parse_as_lut), 'fall_transition':('callback',self.parse_as_lut), 'rise_constraint':('callback',self.parse_as_lut), 'fall_constraint':('callback',self.parse_as_lut), 'output_current_fall':('waive',self.parse_as_undefgroup), # CCSN 'output_current_rise':('waive',self.parse_as_undefgroup), # CCSN 'receiver_capacitance1_rise':('waive',self.parse_as_undefgroup), # CCSN 'receiver_capacitance2_rise':('waive',self.parse_as_undefgroup), # CCSN 'receiver_capacitance1_fall':('waive',self.parse_as_undefgroup), # CCSN 'receiver_capacitance2_fall':('waive',self.parse_as_undefgroup), # CCSN '_L':('bos',''), '_R':('eos','') } self.powerDec = { 'rise_power':('callback',self.parse_as_lut), 'fall_power':('callback',self.parse_as_lut), 'related_pin':('value',''), 'related_pg_pin':('value',''), 'when':('value',''), '_L':('bos',''), '_R':('eos','') } self.leakageDec = { 'value':0, 'related_pg_pin':'', 'when':'' } def parse_as_undefgroup(self,ti,args=''): if self.content[ti+1]!='_L': # simple or complex attributes return ti size = len(self.content) ii = ti state = 0 while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='_L': state += 1 elif token=='_R': state -= 1 if state==0: break ii += 1 return ii def parse_as_bundle(self,ti,cnode): size = len(self.content) ii = ti+1 while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='': ii+=1 continue if self.bundDec.get(token)!=None: t,func = self.bundDec[token] if t=='callback': ii = func(ii,cnode) elif t=='value': pass elif t=='waive': ii = self.parse_as_undefgroup(ii,token) elif t=='bos': # begin of scope ii += 1 continue else: # end of scope break else: ii = self.parse_as_undefgroup(ii,token) ii+=1 return ii def parse_as_pin(self,ti,cnode): iname = re.split(r'[()]',self.content[ti])[1] size = len(self.content) ii = ti+1 tnode = {} # returned timing/power table inode = {'name':iname} # temporary parse data while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='': ii += 1 continue if self.pinDec.get(token)!=None: t,func = self.pinDec[token] if t=='callback': ii,tnode = func(ii) if inode.get(token)==None: inode[token] = [] inode[token].append(tnode) elif t=='value': val = re.split(r'[:]',tokens)[1] inode[token] = val.strip('"') elif t=='waive': ii = self.parse_as_undefgroup(ii,token) elif t=='bos': # begin of scope ii += 1 continue else: # end of scope break else: ii = self.parse_as_undefgroup(ii,token) ii += 1 cnode['pin'][iname] = inode return ii def parse_as_lut(self,ti,vType): size = len(self.content) ii = ti+1 vnode = {} # temporary parse data while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='' or token=='_L': ii += 1 continue elif token=='_R': # enf of scope break if token[:6]=='index_': vt = re.split(r'[():"]',tokens)[2] vl = vt.split(',') vnode[token]=list(map(float,vl)) elif token=='values': vt = "".join(re.split(r'[():"]',tokens)[1:]) vl = vt.split(',') vnode['values']=list(map(float,vl)) else: print(f' LUT unknow> {token}') ii += 1 return ii,vnode def parse_as_timing(self,ti): size = len(self.content) ii=ti+1 tnode = {} # temporary parse data vnode = {} while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='': ii += 1 continue if self.timeDec.get(token)!=None: t,func = self.timeDec[token] if t=='callback': ii,vnode = func(ii,token) tnode[token] = vnode elif t=='value': val = re.split(r'[():]',tokens)[1] tnode[token] = val.strip('"') elif t=='waive': ii = self.parse_as_undefgroup(ii,token) elif t=='bos': # begin of scope ii += 1 continue else: # end of scope break else: ii = self.parse_as_undefgroup(ii,token) ii += 1 return ii,tnode def parse_as_internal_power(self,ti): size = len(self.content) ii = ti+1 pnode={} # temporary parse data while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='': ii += 1 continue if self.powerDec.get(token)!=None: t,func = self.powerDec[token] if t=='callback': ii,vnode = func(ii,token) pnode[token] = vnode elif t=='value': val = re.split(r'[():]',tokens)[1] pnode[token] = val.strip('"') elif t=='bos': # begin of scope ii+=1 continue else: # end of scope break else: ii = self.parse_as_undefgroup(ii,token) ii += 1 return ii,pnode def parse_as_leakage_power(self,ti,cnode): node = {} ei = self.content.index('_R',ti) ii = ti while ii<ei: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if self.leakageDec.get(token)!=None: val = re.split(r'[():]',tokens)[1] node[token] = val.strip('"') ii += 1 cnode['leakage_power'].append(node) return ei def parse_as_cell(self,ti,libnode): cname = re.split(r'[()]',self.content[ti])[1].strip('"') cnode={ 'name':cname, 'pin':{}, 'leakage_power':[] } size = len(self.content) ii = ti+1 while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='': ii += 1 continue if self.cellDec.get(token)!=None: t,func = self.cellDec[token] if t=='callback': ii = func(ii,cnode) elif t=='value': val = re.split(r'[():]',tokens)[1] cnode[token]=val.strip('"') elif t=='bos' or t=='waive': # begin of scope ii += 1 continue else: # end of scope break else: ii = self.parse_as_undefgroup(ii,token) ii += 1 libnode['cell'][cname] = cnode return ii # load liberty file def read_lib(self,lib,gzFlag=False): print('load liberty %s ...' % lib) if gzFlag==True: with gzip.open(lib,'rt') as f: text = f.read() else: with open(lib,'r') as f: text = f.read() text = re.sub(r'//.*\n|\\\n|[ \t]+','',text) # remove white space text = re.sub(r'/\*.*\*/','',text) # remove comments text = re.sub(r'{','@_L@',text) # replace left bracket text = re.sub(r'}','@_R@',text) # replace right bracket self.content = re.split(r'[@;\n]+',text) ii,size = 0,len(self.content) libnode = {'cell':{}} while ii<size: tokens = self.content[ii] token = re.split(r'[():]',tokens)[0] if token=='': ii += 1 continue if self.libDec.get(token)!=None: t,func = self.libDec[token] if t=='callback': ii = func(ii,libnode) elif t=='value': val = re.split(r'[():]',tokens)[1] libnode[token] = val.strip('"') elif t=='bos' or t=='waive': # begin of scope ii += 1 continue else: # end of scope break else: # waive unknow token as attribute statement ii = self.parse_as_undefgroup(ii,token) ii += 1 return libnode def dump_json(self,libnode,fname): jnode = json.dumps(libnode,sort_keys=True,ensure_ascii=False,indent=0,separators=(',',':')) with open(fname,mode='w') as f: f.write(jnode) print('liberty %s is saved into %s (JSON) ...' % (libnode['library'],fname)) # convert JSON string into dictionary def load_json(self,fname): print('load library %s ...' % (fname)) with open(fname,mode='r') as f: jnode = f.read() libnode = json.loads(jnode) return libnode # guery data with cell level def get_cells(self,libnode,cname_re='-',cfp_re='-'): cnodes = [] cells = libnode['cell'] for ckey in cells.keys(): cnode = cells[ckey] fp = cnode['cell_footprint'].strip('"') if cnode.get('cell_footprint')!=None else '' if bool(re.match(cfp_re,fp)) or bool(re.match(cname_re,ckey)): cnodes.append(cnode) return cnodes # list of cell nodes # ttype := timing type (optional) # ctype := constraint/table type (must) # ttype: ctype # rising_edge: {cell_rise, cell_fall, rise_transition, fall_transition} # falling_edge: {cell_rise, cell_fall, rise_transition, fall_transition} # setup_rising: {rise_constraint, fall_constraint} # hold_rising: {rise_constraint, fall_constraint} def get_cell_timing(self,cnode,ttype=None,ctype=None): dnodes = {} incL = set(['rise_constraint','fall_constraint','cell_rise','cell_fall','rise_transition','fall_transition']) for pin in cnode['pin'].keys(): inode = cnode['pin'][pin] if inode.get('timing')==None: continue tables = inode['timing'] for tnode in tables: if ttype!=None: if ttype not in tnode['timing_type']: continue cond = tnode['when'] if tnode.get('when')!=None else '' arc = ",".join([tnode['related_pin'],pin,cond]) kL = set(tnode.keys())&incL for ckey in kL: if ctype!=None and ctype not in ckey: continue lut = tnode[ckey] tkey = tnode.get('timing_type') dnodes[arc,tkey,ckey] = lut return dnodes # dict {('iPin,oPin,when',ttype,ctype): lut} # convert liberty DB to pandas dataframe def lib2df(self,lnode): cnodeL = self.get_cells(lnode,'.*') dfL,cellL = [],[] for cnode in cnodeL: lutL = self.get_cell_timing(cnode) if len(lutL)==0: continue d = pd.DataFrame.from_dict(lutL,orient='index') dfL += [d] cellL += [cnode['name']] df = pd.concat(dfL,keys=cellL) df.index.names = ['cell','arc','ttype','ctype'] return df.sort_index(axis=1)
上面這段程式碼寫得並不好, 目的只是讓大家方便將平時不太仔細研究的liberty做適當的萃取, 從而可以視覺化並掌握更大尺度的趨勢, 而不是只看某個PVT corner中基於某個輸入狀態以及某個transition & load下的一個snapshot. (繞口)
下面是使用這個工具的範例, 我們將liberty讀進來輸出成JSON(相當於Python的dictionary格式). 這樣的好處是工程師不需要寫parser去處理liberty, 工程師可以輕易的以Python內建的資料格式加工處理而真正把時間花在元件特性的分析上.
lutil = Liberty() lib = '/xxx.lib' lnode = lutil.read_lib(lib) cnode = lnode['cell']['DFFULVT'] inode = cnode[‘pin’][‘Q0’] lut = inode['timing'][0]['cell_fall'] index_1,index_2,values = lut.values()
有了上面的基礎, 我們就可以進一步將資料格式轉成CSV或Pandas DataFrame, 好處是我們可以將多個不同PVT corner甚至不同製程的資料一起做分析, 透過類似Scipy的interpolation套件我們可以針對所預期量產後須關注的元件工作區間, 觀察其更廣泛的趨勢而不再只是一個點.
lutil.dump_json(lnode,'xxxlib.json') lnode = lutil.load_json('xxxlib.json') df = lutil.lib2df(lnode) df.to_csv(‘xxxlib.csv’)