第四章:网表解析基础——用Python解析EDIF网表
EDIF网表,说白了就是FPGA设计的“电路图纸”。你想想看,一个复杂的FPGA工程,综合工具会把它翻译成一种标准格式——EDIF。这个文件里记录了所有的模块、信号、实例化关系。我刚开始做逆向时,面对动辄几万行的EDIF文件,头都是大的。后来慢慢摸索出一套用Python解析的方法,今天分享给你。
4.1 EDIF文件长什么样?
先看一眼EDIF的骨架。它其实是一种Lisp风格的文本格式,括号嵌套括号。我截取一段真实的EDIF片段给你看:
(edif top
(cell TOP (cellType GENERIC)
(view netlist (viewType NETLIST)
(interface
(port clk (direction INPUT))
(port rst (direction INPUT))
(port data_out (direction OUTPUT))
)
(contents
(instance u_pll (cellRef PLL))
(instance u_reg (cellRef REG))
(net clk_net
(joined
(portRef clk (instanceRef u_pll))
(portRef clk_out (instanceRef u_reg))
)
)
)
)
)
)
嗯,这里要注意:EDIF的层级结构非常清晰。最外层是edif,里面包着cell,cell里又有view,view里分interface和contents。我个人习惯把这种结构想象成一棵树——根节点是edif,叶子节点是port、instance、net这些具体元素。
4.2 为什么选Python?
说实话,用Perl或者Tcl也能解析EDIF。但我坚持用Python,原因有三:
- 库多:Python有现成的解析库,比如
pyparsing,能省不少事 - 好调试:我在项目中遇到过EDIF文件格式不规范的情况,Python的异常信息比Perl友好太多
- 生态好:解析完网表后,后续做分析、画图、生成报告,Python一条龙搞定
pyparsing或者lxml(EDIF可以转XML)。我早期用正则踩过坑——EDIF里字符串可能包含括号,正则匹配会出错。
4.3 核心数据结构设计
解析之前,先想清楚我们要提取什么。我一般会设计三个核心类:
| 类名 | 属性 | 说明 |
|---|---|---|
Module |
name, ports, instances, nets | 对应EDIF里的cell |
Signal |
name, width, direction, connected_pins | 对应EDIF里的net/port |
Instance |
name, cell_ref, pin_connections | 对应EDIF里的instance |
你想想看,有了这三个类,整个网表就能被完整描述了。模块包含实例和信号,实例之间通过信号连接。这不就是电路的本质吗?
4.4 解析实战:从EDIF到Python对象
下面我写一个简化版的解析器。为了让你看得清楚,我不用第三方库,纯手写递归下降解析:
import re
class EdifParser:
def __init__(self, content):
self.tokens = self._tokenize(content)
self.pos = 0
def _tokenize(self, content):
# 把EDIF文本拆成token列表
# 处理括号、字符串、标识符
tokens = []
i = 0
while i < len(content):
if content[i] in '()':
tokens.append(content[i])
i += 1
elif content[i] == '"':
j = i + 1
while content[j] != '"':
j += 1
tokens.append(content[i:j+1])
i = j + 1
elif content[i] in ' \t\n\r':
i += 1
else:
j = i
while j < len(content) and content[j] not in '() \t\n\r"':
j += 1
tokens.append(content[i:j])
i = j
return tokens
def parse(self):
"""解析整个EDIF文件"""
self._expect('(')
self._expect('edif')
name = self._consume()
modules = []
while self.pos < len(self.tokens) and self.tokens[self.pos] == '(':
module = self._parse_cell()
if module:
modules.append(module)
return modules
def _parse_cell(self):
"""解析一个cell(模块)"""
self._expect('(')
self._expect('cell')
cell_name = self._consume()
# ... 解析interface和contents
# 这里省略具体实现,避免代码太长
self._expect(')')
return {'name': cell_name, 'ports': [], 'instances': [], 'nets': []}
(cellType GENERIC)。如果你写死了顺序,遇到没有这个属性的cell就会崩。我的建议是:解析时用“遇到什么处理什么”的模式,不要假设顺序。
4.5 提取模块信息
模块信息是网表的骨架。每个模块包含:
- 端口列表:输入、输出、双向口
- 子实例列表:调用了哪些其他模块
- 信号列表:内部连线
我一般会这样提取:
def extract_module_info(edif_data):
modules = []
for cell in edif_data:
module = {
'name': cell['name'],
'ports': [],
'instances': [],
'signals': []
}
# 提取端口
for port in cell.get('interface', []):
module['ports'].append({
'name': port['name'],
'direction': port['direction']
})
# 提取实例
for inst in cell.get('instances', []):
module['instances'].append({
'name': inst['name'],
'cell_ref': inst['cell_ref']
})
# 提取信号
for net in cell.get('nets', []):
module['signals'].append({
'name': net['name'],
'connections': net['joined']
})
modules.append(module)
return modules
4.6 提取信号连接关系
信号连接是网表的核心价值。说白了,你要知道哪个实例的哪个引脚连到了哪里。EDIF里用joined关键字表示连接:
def extract_connections(edif_data):
connections = []
for cell in edif_data:
for net in cell.get('nets', []):
for join in net.get('joined', []):
conn = {
'net_name': net['name'],
'instance': join.get('instanceRef', 'TOP'),
'port': join.get('portRef', '')
}
connections.append(conn)
return connections
嗯,这里要注意:有些EDIF文件里,instanceRef可能为空,表示这个端口是顶层端口。我在项目中遇到过这种情况,处理时一定要加判空。
4.7 知识体系总览
下面这张图,是我自己总结的EDIF解析知识体系。你可以把它当作一个检查清单:
4.8 避坑指南与实用技巧
最后,分享几个我实战中总结的经验:
mmap模块可以帮你做内存映射。
好了,EDIF解析的基础就讲到这里。你只要掌握了递归下降解析的思路,再加上对EDIF语法的理解,写一个解析器其实不难。记住:先搭骨架(模块),再填血肉(信号和实例),最后连筋脉(连接关系)。
公众号:蓝海资料掘金营,微信deep3321