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 是根节点,里面套 cellcell 里套 viewview 里再套 interfacecontents。每个 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里的 portRefinstanceRef 顺序可能不一样。有的工具把端口名放前面,有的把例化名放前面。我建议解析时做个容错处理。

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网表文件 文本格式,括号嵌套 词法分析 tokenize → token流 语法分析 递归下降 → AST 数据结构构建 Cell / Instance / Net 拓扑提取 邻接表 / 连接关系 网表拓扑数据 关键数据结构 • Cell: 模块/单元 • Instance: 例化 • Net: 网络连线 • Port: 端口 • Pin: 引脚连接 拓扑 = 网络 ↔ 引脚映射

这张图把整个流程串起来了。从EDIF文件开始,经过词法分析、语法分析,再到数据结构构建,最后提取拓扑。每一步都有对应的代码实现。

说实话,这套流程我反复优化过好几版。最早用正则硬匹配,后来发现太脆弱。改用递归下降后,解析速度和稳定性都上来了。你如果自己写,建议也走这个路子。

好了,这一章就到这里。代码都在上面了,你可以直接拿去用。遇到问题,多看看EDIF标准文档,或者自己写几个测试用例跑一跑。


专注资料整理