mirror of
https://github.com/corundum/corundum.git
synced 2025-01-16 08:12:53 +08:00
Add build automation scripts
This commit is contained in:
parent
2cc3dbd5cc
commit
8851b3b1ad
18
fpga/build_images.ini
Normal file
18
fpga/build_images.ini
Normal file
@ -0,0 +1,18 @@
|
||||
[general]
|
||||
prefix = corundum
|
||||
parallel = 16
|
||||
synth_parallel = 8
|
||||
dirs =
|
||||
mqnic
|
||||
|
||||
[vivado]
|
||||
settings_file = /opt/Xilinx/vivado-settings
|
||||
|
||||
[ise]
|
||||
settings_file = /opt/Xilinx/ise-settings
|
||||
|
||||
[quartus]
|
||||
settings_file = /opt/altera/quartus-settings
|
||||
|
||||
[quartus-pro]
|
||||
settings_file = /opt/altera/quartus-pro-settings
|
383
fpga/build_images.py
Executable file
383
fpga/build_images.py
Executable file
@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
|
||||
Copyright (c) 2022 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import configparser
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
|
||||
def run_cmd(cmd, cwd=None):
|
||||
return subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE).stdout.decode('utf-8').strip()
|
||||
|
||||
|
||||
async def run_cmd_async(cmd, *args, cwd=None):
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
cmd,
|
||||
*args,
|
||||
cwd=cwd,
|
||||
stdout=asyncio.subprocess.PIPE)
|
||||
|
||||
stdout, stderr = await proc.communicate()
|
||||
|
||||
if stdout:
|
||||
return stdout.decode()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def run_cmd_shell_async(cmd, cwd=None):
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
stdout=asyncio.subprocess.PIPE)
|
||||
|
||||
stdout, stderr = await proc.communicate()
|
||||
|
||||
if stdout:
|
||||
return stdout.decode()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class Build:
|
||||
def __init__(self, design, build_dir, prefix, output):
|
||||
self.design = design
|
||||
self.prefix = prefix
|
||||
self.output = output
|
||||
|
||||
self.settings_file = ""
|
||||
self.build_dir = build_dir
|
||||
self.build_cmd = "sleep 5"
|
||||
|
||||
self.outname = '-'.join(self.design)
|
||||
if prefix:
|
||||
self.outname = prefix + "-" + self.outname
|
||||
|
||||
self.output_file = ""
|
||||
self.output_ext = ".bin"
|
||||
|
||||
self.start_time = None
|
||||
self.elapsed_time = None
|
||||
|
||||
self.wns = None
|
||||
self.tns = None
|
||||
|
||||
self.phase = "Idle"
|
||||
|
||||
def get_status(self):
|
||||
s = f"{'/'.join(self.design)}: {self.phase}"
|
||||
|
||||
if self.wns is not None:
|
||||
s += f" (WNS: {self.wns}, TNS: {self.tns})"
|
||||
|
||||
if self.elapsed_time is not None:
|
||||
s += " ["+str(self.elapsed_time).split('.')[0]+"]"
|
||||
elif self.start_time is not None:
|
||||
s += " ["+str(datetime.datetime.now() - self.start_time).split('.')[0]+"]"
|
||||
|
||||
return s
|
||||
|
||||
def synth_done(self):
|
||||
if self.synth_sem is not None:
|
||||
self.synth_sem.release()
|
||||
self.synth_sem = None
|
||||
|
||||
def build_done(self):
|
||||
if self.build_sem is not None:
|
||||
self.build_sem.release()
|
||||
self.build_sem = None
|
||||
|
||||
async def run(self, build_sem, synth_sem):
|
||||
self.build_sem = build_sem
|
||||
self.synth_sem = synth_sem
|
||||
|
||||
self.phase = "Waiting (build)"
|
||||
if self.build_sem is not None:
|
||||
await self.build_sem.acquire()
|
||||
|
||||
self.phase = "Waiting (synth)"
|
||||
if self.synth_sem is not None:
|
||||
await self.synth_sem.acquire()
|
||||
|
||||
self.phase = "Starting"
|
||||
self.start_time = datetime.datetime.now()
|
||||
|
||||
build_cmd = self.build_cmd
|
||||
if self.settings_file:
|
||||
build_cmd = f"source {self.settings_file}; {build_cmd}"
|
||||
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
build_cmd,
|
||||
cwd=self.build_dir,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE)
|
||||
|
||||
while True:
|
||||
line = await proc.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.decode('utf-8').strip()
|
||||
|
||||
self.scan_log_line(line)
|
||||
|
||||
self.synth_done()
|
||||
self.build_done()
|
||||
|
||||
if os.path.isfile(self.output_file):
|
||||
self.phase = "Copying output file"
|
||||
await run_cmd_async("cp", "-p", self.output_file, os.path.join(self.output, self.outname+self.output_ext))
|
||||
self.phase = "Zipping output file"
|
||||
await run_cmd_async("zip", self.outname+".zip", self.outname+self.output_ext, cwd=self.output)
|
||||
self.phase = "Done"
|
||||
else:
|
||||
self.phase = "Failed"
|
||||
|
||||
self.elapsed_time = datetime.datetime.now() - self.start_time
|
||||
|
||||
def scan_log_line(self, line):
|
||||
pass
|
||||
|
||||
|
||||
class VivadoBuild(Build):
|
||||
def __init__(self, design, build_dir, prefix, output):
|
||||
super().__init__(design, build_dir, prefix, output)
|
||||
|
||||
self.settings_file = config['vivado'].get('settings_file')
|
||||
self.build_cmd = "make"
|
||||
|
||||
self.output_ext = ".bit"
|
||||
self.output_file = os.path.join(self.build_dir, "fpga"+self.output_ext)
|
||||
|
||||
self.vivado_phase = "init"
|
||||
|
||||
def scan_log_line(self, line):
|
||||
if line == "vivado -nojournal -nolog -mode batch -source create_project.tcl":
|
||||
self.vivado_phase = "init"
|
||||
self.phase = f"[{self.vivado_phase}] Creating Vivado project"
|
||||
if line == "vivado -nojournal -nolog -mode batch -source run_synth.tcl":
|
||||
self.vivado_phase = "synthesis"
|
||||
self.phase = f"[{self.vivado_phase}] Starting Vivado synthesis"
|
||||
if line == "vivado -nojournal -nolog -mode batch -source run_impl.tcl":
|
||||
self.synth_done()
|
||||
self.vivado_phase = "implementation"
|
||||
self.phase = f"[{self.vivado_phase}] Starting Vivado implementation"
|
||||
if line == "Starting Placer Task":
|
||||
self.vivado_phase = "implementation (placement)"
|
||||
if line == "Starting Routing Task":
|
||||
self.vivado_phase = "implementation (routing)"
|
||||
if line == "vivado -nojournal -nolog -mode batch -source generate_bit.tcl":
|
||||
self.vivado_phase = "bitfile generation"
|
||||
self.phase = f"[{self.vivado_phase}] Starting Vivado bitfile generation"
|
||||
|
||||
if line.startswith("Start") or line.startswith("Running") or line.startswith("Phase"):
|
||||
self.phase = f"[{self.vivado_phase}] "+line.split("|", 1)[0].split(":", 1)[0].strip()
|
||||
|
||||
m = re.search(r".*Timing Summary\s+\|\s+WNS=(\S+)\s+\|\s+TNS=(\S+)", line)
|
||||
if m:
|
||||
self.wns = m.group(1)
|
||||
self.tns = m.group(2)
|
||||
|
||||
|
||||
class IseBuild(Build):
|
||||
def __init__(self, design, build_dir, prefix, output):
|
||||
super().__init__(design, build_dir, prefix, output)
|
||||
|
||||
self.settings_file = config['ise'].get('settings_file')
|
||||
self.build_cmd = "make"
|
||||
|
||||
self.output_ext = ".bit"
|
||||
self.output_file = os.path.join(self.build_dir, "fpga"+self.output_ext)
|
||||
|
||||
def scan_log_line(self, line):
|
||||
if line.startswith('xst'):
|
||||
self.phase = "Running synthesis"
|
||||
elif line.startswith('ngdbuild'):
|
||||
self.synth_done()
|
||||
self.phase = "Running translate"
|
||||
elif line.startswith('map'):
|
||||
self.phase = "Running map"
|
||||
elif line.startswith('par'):
|
||||
self.phase = "Running placement and routing"
|
||||
elif line.startswith('trce'):
|
||||
self.phase = "Running timing analysis"
|
||||
elif line.startswith('bitgen'):
|
||||
self.phase = "Running bitfile generation"
|
||||
|
||||
|
||||
class QuartusBuild(Build):
|
||||
def __init__(self, design, build_dir, prefix, output):
|
||||
super().__init__(design, build_dir, prefix, output)
|
||||
|
||||
self.settings_file = config['quartus'].get('settings_file')
|
||||
self.build_cmd = "make"
|
||||
|
||||
self.output_ext = ".sof"
|
||||
self.output_file = os.path.join(self.build_dir, "fpga"+self.output_ext)
|
||||
|
||||
def scan_log_line(self, line):
|
||||
if line.startswith('quartus_map'):
|
||||
self.phase = "Running synthesis and mapping"
|
||||
elif line.startswith('quartus_fit'):
|
||||
self.synth_done()
|
||||
self.phase = "Running placement and routing"
|
||||
elif line.startswith('quartus_sta'):
|
||||
self.phase = "Running timing analysis"
|
||||
elif line.startswith('quartus_asm'):
|
||||
self.phase = "Running assembler"
|
||||
|
||||
|
||||
class QuartusProBuild(QuartusBuild):
|
||||
def __init__(self, design, build_dir, prefix, output):
|
||||
super().__init__(design, build_dir, prefix, output)
|
||||
|
||||
self.settings_file = config['quartus-pro'].get('settings_file')
|
||||
|
||||
|
||||
async def monitor_status(jobs):
|
||||
start_time = datetime.datetime.now()
|
||||
|
||||
while True:
|
||||
|
||||
print("")
|
||||
|
||||
done_count = 0
|
||||
|
||||
for job in jobs:
|
||||
print(job.get_status())
|
||||
if job.elapsed_time is not None:
|
||||
done_count += 1
|
||||
|
||||
s = f"Overall progress: {done_count}/{len(jobs)} jobs ({done_count/len(jobs)*100:.01f}%)"
|
||||
s += " ["+str(datetime.datetime.now() - start_time).split('.')[0]+"]"
|
||||
|
||||
print(s)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
async def main():
|
||||
config.read("build_images.ini")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--output_dir', type=str, default=None, help="Output directory")
|
||||
parser.add_argument('--prefix', type=str, default=config['general'].get('prefix', ''), help="Prefix")
|
||||
parser.add_argument('--clean', action='store_true', help="Clean")
|
||||
parser.add_argument('--parallel', type=int, default=config['general'].getint('parallel', 8), help="Parallel build runs")
|
||||
parser.add_argument('--synth_parallel', type=int, default=config['general'].getint('synth_parallel', 8), help="Parallel synthesis runs")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
version = run_cmd(["git", "describe", "--always", "--tags"])
|
||||
|
||||
prefix = args.prefix+"-"+version
|
||||
|
||||
if args.output_dir:
|
||||
output_dir = args.output_dir
|
||||
else:
|
||||
output_dir = os.path.abspath(os.path.join("bitfiles", prefix))
|
||||
|
||||
print(f"Git version: {version}")
|
||||
print(f"Output directory: {output_dir}")
|
||||
|
||||
print("Scanning...")
|
||||
|
||||
scan_dirs = [x.strip() for x in config['general'].get('dirs', '').strip().split()]
|
||||
jobs = []
|
||||
|
||||
if len(scan_dirs) == 0:
|
||||
scan_dirs = [os.getcwd()]
|
||||
|
||||
for d in scan_dirs:
|
||||
path = os.path.abspath(d)
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
if file == 'Makefile' and os.path.basename(root).startswith('fpga'):
|
||||
# found a makefile
|
||||
|
||||
design = os.path.relpath(root, path).split(os.sep)
|
||||
if len(scan_dirs) > 1:
|
||||
design = [d]+design
|
||||
design = [x for x in design if x != 'fpga']
|
||||
design = [x.removeprefix('fpga').strip("_") for x in design]
|
||||
|
||||
if os.path.exists(os.path.join(root, "..", "common", "vivado.mk")):
|
||||
# Vivado
|
||||
jobs.append(VivadoBuild(design, root, prefix, output_dir))
|
||||
|
||||
if os.path.exists(os.path.join(root, "..", "common", "xilinx.mk")):
|
||||
# ISE
|
||||
jobs.append(IseBuild(design, root, prefix, output_dir))
|
||||
|
||||
if (os.path.exists(os.path.join(root, "..", "common", "altera.mk")) or
|
||||
os.path.exists(os.path.join(root, "..", "common", "quartus.mk"))):
|
||||
# Quartus Prime
|
||||
jobs.append(QuartusBuild(design, root, prefix, output_dir))
|
||||
|
||||
if (os.path.exists(os.path.join(root, "..", "common", "quartus_pro.mk"))):
|
||||
# Quartus Prime Pro
|
||||
jobs.append(QuartusProBuild(design, root, prefix, output_dir))
|
||||
|
||||
jobs.sort(key=lambda job: job.design)
|
||||
|
||||
print(f"Found {len(jobs)} design variants")
|
||||
|
||||
print("Building...")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
build_sem = asyncio.Semaphore(args.parallel)
|
||||
synth_sem = asyncio.Semaphore(args.synth_parallel)
|
||||
|
||||
job_coros = []
|
||||
|
||||
for job in jobs:
|
||||
if args.clean:
|
||||
job.build_cmd = "make clean"
|
||||
|
||||
job_coros.append(asyncio.create_task(job.run(build_sem, synth_sem)))
|
||||
|
||||
status = asyncio.create_task(monitor_status(jobs))
|
||||
|
||||
await asyncio.wait(job_coros)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
|
||||
status.cancel()
|
||||
|
||||
run_cmd(["./collect_utilization.py",
|
||||
"--csv", os.path.join(output_dir, "utilization.csv"),
|
||||
"--log", os.path.join(output_dir, "utilization.txt")
|
||||
])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
284
fpga/collect_utilization.py
Executable file
284
fpga/collect_utilization.py
Executable file
@ -0,0 +1,284 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
|
||||
Copyright (c) 2022 Alex Forencich
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class record(object):
|
||||
def __init__(self):
|
||||
self.file = None
|
||||
self.project_dir = None
|
||||
self.tool = None
|
||||
self.date = None
|
||||
self.device = None
|
||||
self.lut_used = None
|
||||
self.lut_total = None
|
||||
self.ff_used = None
|
||||
self.ff_total = None
|
||||
self.bram_used = None
|
||||
self.bram_total = None
|
||||
self.uram_used = None
|
||||
self.uram_total = None
|
||||
self.wns = None
|
||||
self.tns = None
|
||||
|
||||
def format_str(self):
|
||||
s = f"File: {self.file}\n"
|
||||
s += f"Project directory: {self.project_dir}\n"
|
||||
s += f"Tool: {self.tool}\n"
|
||||
s += f"Date: {self.date}\n"
|
||||
s += f"Device: {self.device}\n"
|
||||
s += f"LUTs: {self.lut_used} / {self.lut_total} ({self.lut_used/self.lut_total*100:.2f}%)\n"
|
||||
s += f"FFs: {self.ff_used} / {self.ff_total} ({self.ff_used/self.ff_total*100:.2f}%)\n"
|
||||
if self.bram_total:
|
||||
s += f"BRAM: {self.bram_used} / {self.bram_total} ({self.bram_used/self.bram_total*100:.2f}%)\n"
|
||||
else:
|
||||
s += "BRAM: N/A\n"
|
||||
if self.uram_total:
|
||||
s += f"URAM: {self.uram_used} / {self.uram_total} ({self.uram_used/self.uram_total*100:.2f}%)\n"
|
||||
else:
|
||||
s += "URAM: N/A\n"
|
||||
s += f"Slack: WNS {self.wns} ns, TNS {self.tns} ns"
|
||||
return s
|
||||
|
||||
def format_csv(self):
|
||||
s = f"\"{self.file}\","
|
||||
s += f"\"{self.project_dir}\","
|
||||
s += f"\"{self.tool}\","
|
||||
s += f"\"{self.date}\","
|
||||
s += f"\"{self.device}\","
|
||||
s += f"{self.lut_used},{self.lut_total},"
|
||||
s += f"{self.ff_used},{self.ff_total},"
|
||||
s += f"{self.bram_used},{self.bram_total},"
|
||||
s += f"{self.uram_used},{self.uram_total},"
|
||||
s += f"{self.wns},{self.tns}\n"
|
||||
return s
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-d', '--dir', type=str, default='.', help="directory")
|
||||
parser.add_argument('--csv', type=str, help="CSV file")
|
||||
parser.add_argument('--log', type=str, help="log file")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
records = []
|
||||
|
||||
for root, dirs, files in os.walk(args.dir):
|
||||
for file in files:
|
||||
# Vivado
|
||||
if file.endswith("_utilization_placed.rpt"):
|
||||
r = record()
|
||||
|
||||
fn = os.path.join(root, file)
|
||||
r.file = fn
|
||||
r.project_dir = os.path.dirname(os.path.dirname(root))
|
||||
|
||||
s = ''
|
||||
with open(fn, 'r') as f:
|
||||
s = f.read()
|
||||
|
||||
m = re.search(r'Tool Version\s*:\s*(.+)', s)
|
||||
r.tool = m.group(1)
|
||||
|
||||
m = re.search(r'Date\s*:\s*(.+)', s)
|
||||
r.date = m.group(1)
|
||||
|
||||
m = re.search(r'Device\s*:\s*(.+)', s)
|
||||
r.device = m.group(1)
|
||||
|
||||
m = re.search(r'^\|\s*(?:CLB|Slice) LUTs\s*\|(.+)\|$', s, re.M)
|
||||
lst = m.group(1).split("|")
|
||||
r.lut_used = int(lst[0])
|
||||
r.lut_total = int(lst[2 if '.' in lst[3] else 3])
|
||||
|
||||
m = re.search(r'^\|\s*(?:CLB|Slice) Registers\s*\|(.+)\|$', s, re.M)
|
||||
lst = m.group(1).split("|")
|
||||
r.ff_used = int(lst[0])
|
||||
r.ff_total = int(lst[2 if '.' in lst[3] else 3])
|
||||
|
||||
m = re.search(r'^\|\s*Block RAM Tile\s*\|(.+)\|$', s, re.M)
|
||||
lst = m.group(1).split("|")
|
||||
r.bram_used = float(lst[0])
|
||||
r.bram_total = int(lst[2 if '.' in lst[3] else 3])
|
||||
|
||||
m = re.search(r'^\|\s*URAM\s*\|(.+)\|$', s, re.M)
|
||||
if m:
|
||||
lst = m.group(1).split("|")
|
||||
r.uram_used = int(lst[0])
|
||||
r.uram_total = int(lst[2 if '.' in lst[3] else 3])
|
||||
|
||||
fn = os.path.join(root, file.replace("_utilization_placed.rpt", "_timing_summary_routed.rpt"))
|
||||
if os.path.isfile(fn):
|
||||
|
||||
lines = []
|
||||
with open(fn, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
line = None
|
||||
for i in range(len(lines)):
|
||||
if "Design Timing Summary" in lines[i]:
|
||||
line = i
|
||||
|
||||
fields = lines[line+6].split()
|
||||
|
||||
r.wns = fields[0]
|
||||
r.tns = fields[1]
|
||||
|
||||
records.append(r)
|
||||
|
||||
# ISE
|
||||
if file.endswith("_map.mrp"):
|
||||
r = record()
|
||||
r.tool = "ISE"
|
||||
|
||||
fn = os.path.join(root, file)
|
||||
r.file = fn
|
||||
r.project_dir = root
|
||||
|
||||
s = ''
|
||||
with open(fn, 'r') as f:
|
||||
s = f.read()
|
||||
|
||||
m = re.search(r'Release\s*(\S+)\s*Map', s)
|
||||
r.tool = "ISE "+m.group(1)
|
||||
|
||||
m = re.search(r'Mapped Date\s*:\s*(.+)', s)
|
||||
r.date = m.group(1)
|
||||
|
||||
m = re.search(r'Target Device\s*:\s*(.+)', s)
|
||||
r.device = m.group(1)
|
||||
m = re.search(r'Target Package\s*:\s*(.+)', s)
|
||||
r.device += m.group(1)
|
||||
m = re.search(r'Target Speed\s*:\s*(.+)', s)
|
||||
r.device += m.group(1)
|
||||
|
||||
m = re.search(r'Slice LUTs\:\s*(\S+)\s*out of\s*(\S+)\s*(\d+)', s)
|
||||
r.lut_used = int(m.group(1).replace(',', ''))
|
||||
r.lut_total = int(m.group(2).replace(',', ''))
|
||||
|
||||
m = re.search(r'Slice Registers\:\s*(\S+)\s*out of\s*(\S+)\s*(\d+)', s)
|
||||
r.ff_used = int(m.group(1).replace(',', ''))
|
||||
r.ff_total = int(m.group(2).replace(',', ''))
|
||||
|
||||
m = re.search(r'(?:RAMB16BWER|RAMB36E1)[^:]*\:\s*(\S+)\s*out of\s*(\S+)\s*(\d+)', s)
|
||||
r.bram_used = float(m.group(1).replace(',', ''))
|
||||
r.bram_total = int(m.group(2).replace(',', ''))
|
||||
|
||||
records.append(r)
|
||||
|
||||
# Quartus
|
||||
if file.endswith(".fit.summary"):
|
||||
r = record()
|
||||
r.tool = "Quartus"
|
||||
|
||||
fn = os.path.join(root, file)
|
||||
r.file = fn
|
||||
r.project_dir = root
|
||||
|
||||
s = ''
|
||||
with open(fn, 'r') as f:
|
||||
s = f.read()
|
||||
|
||||
m = re.search(r'(Quartus.+Version\s*:\s*.+)', s)
|
||||
r.tool = m.group(1)
|
||||
|
||||
m = re.search(r'Fitter Status.+-\s*(.+)', s)
|
||||
r.date = m.group(1)
|
||||
|
||||
m = re.search(r'Device\s*:\s*(.+)', s)
|
||||
r.device = m.group(1)
|
||||
|
||||
m = re.search(r'Total combinational functions[^:]*\:\s*(\S+)\s*/\s*(\S+)\s*\([^\d]+(\d+)', s)
|
||||
if m:
|
||||
r.lut_used = int(m.group(1).replace(',', ''))
|
||||
r.lut_total = int(m.group(2).replace(',', ''))
|
||||
else:
|
||||
m = re.search(r'Logic utilization[^:]*\:\s*(\S+)\s*/\s*(\S+)\s*\([^\d]+(\d+)', s)
|
||||
r.lut_used = int(m.group(1).replace(',', ''))
|
||||
r.lut_total = int(m.group(2).replace(',', ''))
|
||||
|
||||
m = re.search(r'Dedicated logic registers[^:]*\:\s*(\S+)\s*/\s*(\S+)\s*\([^\d]+(\d+)', s)
|
||||
if m:
|
||||
r.ff_used = int(m.group(1).replace(',', ''))
|
||||
r.ff_total = int(m.group(2).replace(',', ''))
|
||||
|
||||
m = re.search(r'Total (?:dedicated logic)?\s*registers[^:]*\:\s*(\S+)', s)
|
||||
if m:
|
||||
r.ff_used = int(m.group(1).replace(',', ''))
|
||||
r.ff_total = r.lut_total
|
||||
|
||||
m = re.search(r'RAM Blocks[^:]*\:\s*(\S+)\s*/\s*(\S+)\s*\([^\d]+(\d+)', s)
|
||||
if m:
|
||||
r.bram_used = int(m.group(1).replace(',', ''))
|
||||
r.bram_total = int(m.group(2).replace(',', ''))
|
||||
|
||||
fn = os.path.join(root, file.replace(".fit.summary", ".sta.summary"))
|
||||
if os.path.isfile(fn):
|
||||
|
||||
lines = []
|
||||
with open(fn, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
wns = 1e9
|
||||
tns = 0
|
||||
|
||||
for line in lines:
|
||||
m = re.search(r'(\S+)\s*\:\s*(\S+)', line)
|
||||
if m:
|
||||
if m.group(1) == "Slack":
|
||||
wns = min(wns, float(m.group(2)))
|
||||
if m.group(1) == "TNS":
|
||||
tns += float(m.group(2))
|
||||
|
||||
r.wns = wns
|
||||
r.tns = tns
|
||||
|
||||
records.append(r)
|
||||
|
||||
records.sort(key=lambda r: r.file)
|
||||
|
||||
for r in records:
|
||||
print(r.format_str())
|
||||
print()
|
||||
|
||||
if args.log:
|
||||
with open(args.log, 'w') as f:
|
||||
for r in records:
|
||||
f.write(r.format_str())
|
||||
f.write("\n\n")
|
||||
|
||||
if args.csv:
|
||||
with open(args.csv, 'w') as f:
|
||||
f.write("file,project_dir,tool,date,device,lut_used,lut_total,ff_used,ff_total,bram_used,bram_total,uram_used,uram_total,wns,tns\n")
|
||||
for r in records:
|
||||
f.write(r.format_csv())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
4
fpga/watch.sh
Executable file
4
fpga/watch.sh
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
|
||||
watch "./collect_utilization.py --csv output.csv --log output.log > /dev/null ; cat output.csv | column -s, -t -H 1 -T 3 -c 210"
|
||||
|
Loading…
x
Reference in New Issue
Block a user