2. 网表解析工具开发:Python解析EDIF网表、构建网表数据结构、网表拓扑提取
好,咱们进入第二章。这一章我打算聊聊网表解析工具的开发。说白了,就是怎么用Python把EDIF网表给“吃”进去,再在内存里搭出一套我们能操控的数据结构,最后把网表的拓扑关系给拎出来。
你可能会问,为什么非得自己写解析器?直接用现成的工具不行吗?嗯,我在项目里遇到过好几次,商业工具导出的EDIF文件,总有些“非标”写法。这时候,自己手里有套解析代码,改起来就快多了。
2.1 EDIF网表文件长什么样?
先看一眼EDIF文件的结构。它其实是个文本文件,格式有点像Lisp,全是括号嵌套括号。
(edif test_design
(cell TOP (cellType GENERIC)
(view netlist (viewType NETLIST)
(interface
(port clk (direction INPUT))
(port rst (direction INPUT))
(port data_in (direction INPUT))
(port data_out (direction OUTPUT))
)
(contents
(instance u_adder (cellRef adder_lib))
(instance u_reg (cellRef reg_lib))
(net clk_net (joined
(portRef clk)
(portRef u_adder clk)
(portRef u_reg clk)
))
)
)
)
)
看到没?edif 是根节点,里面套 cell,cell 里套 view,view 里再套 interface 和 contents。每个 instance 就是一个模块例化,每个 net 就是一根连线。
核心要点:EDIF 的层级结构就是:设计 → 单元(cell) → 视图(view) → 接口/内容。解析时,我们得一层层剥开。
2.2 用Python写个轻量解析器
我个人习惯用递归下降的方式来解析。为什么?因为EDIF本身就是递归结构,用递归解析最自然。
先写个词法分析器,把文本拆成token流:
import re
def tokenize(edif_text):
# 去掉注释
text = re.sub(r'\(comment.*?\)', '', edif_text, flags=re.DOTALL)
# 按括号和空白分割
tokens = re.findall(r'\(|\)|[^\s()]+', text)
return tokens
然后写个语法分析器,把token流变成Python的嵌套列表:
def parse_tokens(tokens):
def parse():
token = tokens.pop(0)
if token == '(':
lst = []
while tokens[0] != ')':
lst.append(parse())
tokens.pop(0) # 吃掉 ')'
return lst
else:
return token
return parse()
你看,就这么几行,一个EDIF的抽象语法树(AST)就出来了。解析结果大概是这样:
['edif', 'test_design',
['cell', 'TOP', ['cellType', 'GENERIC'],
['view', 'netlist', ['viewType', 'NETLIST'],
...
]
]
]
小技巧:解析时记得处理转义字符。EDIF里字符串可以用双引号括起来,里面可能有特殊字符。我踩过这个坑,解析到带空格的引脚名时直接崩了。
2.3 构建网表数据结构
AST拿到了,但直接用列表操作太痛苦。我们需要把它映射成Python对象。
我一般定义三个核心类:
class Cell:
def __init__(self, name, cell_type):
self.name = name
self.cell_type = cell_type
self.ports = [] # 端口列表
self.instances = [] # 例化单元列表
self.nets = [] # 连线列表
class Instance:
def __init__(self, name, cell_ref):
self.name = name
self.cell_ref = cell_ref # 引用的Cell名称
self.port_connections = {} # 端口 -> 网络名
class Net:
def __init__(self, name):
self.name = name
self.connections = [] # 连接的(instance, port)对
然后写个遍历函数,把AST里的信息填进去:
def build_netlist(ast):
design = Cell('__design__', 'DESIGN')
# 遍历所有cell
for item in ast:
if isinstance(item, list) and item[0] == 'cell':
cell = Cell(item[1], item[2][1])
# 解析端口、例化、连线...
design.instances.append(cell)
return design
嗯,这里要注意。EDIF里的 portRef 和 instanceRef 顺序可能不一样。有的工具把端口名放前面,有的把例化名放前面。我建议解析时做个容错处理。
2.4 网表拓扑提取
数据结构搭好了,接下来就是提取拓扑。说白了,就是搞清楚每个网络连了哪些引脚。
我常用的方法是构建一个邻接表:
def extract_topology(design):
# 网络 -> 连接的引脚列表
net_to_pins = {}
for net in design.nets:
pins = []
for conn in net.connections:
inst_name, port_name = conn
pins.append(f"{inst_name}.{port_name}")
net_to_pins[net.name] = pins
return net_to_pins
输出结果大概是这样:
{
'clk_net': ['TOP.clk', 'u_adder.clk', 'u_reg.clk'],
'data_in_net': ['TOP.data_in', 'u_adder.a'],
'sum_net': ['u_adder.sum', 'u_reg.d'],
...
}
为什么这么做? 有了这个映射,你就能快速回答“这根线连了哪些地方?”或者“这个引脚被哪些网络驱动?”。这在后面做逆向分析时特别有用。
2.5 实战中的坑与技巧
讲几个我实际踩过的坑:
- 大小写问题:EDIF标准不区分大小写,但有些工具生成的网表里大小写混用。解析时统一转小写,省得后面比对出问题。
- 总线处理:EDIF里总线可能用
(array ...)表示。我建议展开成单bit处理,这样拓扑关系更清晰。 - 层次化网表:如果设计有子模块,EDIF里会有嵌套的
cell。解析时记得递归展开,或者保留层次信息。
警告:千万不要假设EDIF文件是“干净”的。我遇到过工具生成的EDIF里有多余的空格、换行符,甚至括号不匹配的情况。解析器一定要有容错机制。
2.6 本章知识体系总览
下面这张图,是我自己总结的网表解析流程。你可以对照着看,心里有个谱。
这张图把整个流程串起来了。从EDIF文件开始,经过词法分析、语法分析,再到数据结构构建,最后提取拓扑。每一步都有对应的代码实现。
说实话,这套流程我反复优化过好几版。最早用正则硬匹配,后来发现太脆弱。改用递归下降后,解析速度和稳定性都上来了。你如果自己写,建议也走这个路子。
好了,这一章就到这里。代码都在上面了,你可以直接拿去用。遇到问题,多看看EDIF标准文档,或者自己写几个测试用例跑一跑。