blob: 8678bb29cf2292be9cf7a468d993e33ba6da2187 [file]
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright 2020-2022 F4PGA Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
import json
import os
import re
import subprocess
import tempfile
from . import utils
def get_verbose():
"""Return if in verbose mode."""
verbose = 0
for e in ["V", "VERBOSE"]:
if e not in os.environ:
continue
verbose = int(os.environ[e])
break
return verbose > 0
def get_yosys():
"""
Searches for the Yosys binary. If the env. var. "YOSYS" is set, then it
checks if it points to a valid executable binary. Otherwise it searches
in PATH for binaries named "yosys" and returns the first one found.
"""
def is_exe(fpath):
"""
Returns True if a file exists and is executable.
"""
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
# The environmental variable "YOSYS" is set. It should point to the Yosys
# executable.
if "YOSYS" in os.environ:
fpath = os.environ["YOSYS"]
if not is_exe(fpath):
return None
return fpath
# Look for the 'yosys' binary in the current PATH but only if the PATH
# variable is available.
elif "PATH" in os.environ:
for path in os.environ["PATH"].split(os.pathsep):
fpath = os.path.join(path, "yosys")
if is_exe(fpath):
return fpath
# Couldn't find Yosys.
return None
def determine_select_prefix():
"""
Older and newer versions of Yosys exhibit different behavior of the
'select' command regarding black/white boxes. Newer version requires a
prefix before some queries. This function determines whether the prefix
is required or not.
"""
# Query help string of the select command
cmd = ["-p", "help select"]
stdout = get_output(cmd, no_common_args=True)
# Look for the phrase. If found then the prefix is required
PHRASE = "prefix the pattern with '='"
if PHRASE in stdout:
return "="
# No prefix needed
return ""
def get_yosys_common_args():
return ["-e", "wire '[^']*' is assigned in a block", "-q"]
def get_output(params, no_common_args=False):
"""Run Yosys with given command line parameters, and return
stdout as a string. Raises CalledProcessError on a non-zero exit code."""
verbose = get_verbose()
cmd = [get_yosys()]
if not no_common_args:
cmd += get_yosys_common_args()
cmd += params
if verbose:
msg = ""
msg += "command".ljust(9).ljust(80, "=") + "\n"
msg += str(cmd)
print(msg)
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# Get the output
stdout, stderr = p.communicate()
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
retcode = p.wait()
if verbose:
msg = ""
if len(stdout):
msg += "stdout".ljust(9).ljust(80, "=") + "\n"
msg += stdout
if len(stderr):
msg += "stderr".ljust(9).ljust(80, "=") + "\n"
msg += stderr
msg += "exitcode".ljust(9).ljust(80, "=") + "\n"
msg += "{}\n".format(retcode)
msg += "=" * 80 + "\n"
print(msg)
if retcode != 0:
emsg = ""
emsg += "Yosys failed with exit code {}\n".format(retcode)
emsg += "Command: '{}'\n".format(" ".join(cmd))
emsg += "Message:\n"
emsg += "\n".join([" " + v for v in stderr.splitlines()])
raise subprocess.CalledProcessError(retcode, cmd, emsg)
return stdout
defines = []
includes = []
def add_define(defname):
"""Add a Verilog define to the list of defines to set in Yosys"""
defines.append(defname)
def get_defines():
"""Return a list of set Verilog defines, as a list of arguments
to pass to Yosys `read_verilog`"""
return " ".join(["-D" + _ for _ in defines])
def add_include(path):
""" Add a path to search when reading verilog to the list of
includes set in Yosys"""
includes.append(path)
def get_includes():
"""Return a list of include directories, as a list of arguments
to pass to Yosys `read_verilog`"""
return " ".join(["-I" + _ for _ in includes])
def commands(commands, infiles=[]):
"""Run a given string containing Yosys commands
Inputs
-------
commands : string of Yosys commands to run
infiles : list of input files
"""
commands = "read_verilog {} {} {}; ".format(
get_defines(), get_includes(), " ".join(infiles)) + commands
params = ["-p", commands]
return get_output(params)
def script(script, infiles=[]):
"""Run a Yosys script given a path to the script
Inputs
-------
script : path to Yosys script to run
infiles : list of input files
"""
params = ["-s", script] + infiles
return get_output(params)
def vlog_to_json(
infiles, flatten=False, aig=False, mode=None, module_with_mode=None):
"""
Convert Verilog to a JSON representation using Yosys
Inputs
-------
infiles : list of input files
flatten : set to flatten output hierarchy
aig : generate And-Inverter-Graph modules for gates
mode : set to a value other than None to use `chparam` to
set the value of the MODE parameter
module_with_mode : the name of the module to apply `mode` to
"""
prep_opts = "-flatten" if flatten else ""
json_opts = "-aig" if aig else ""
if mode is not None:
mode_str = 'chparam -set MODE "{}" {}; '.format(mode, module_with_mode)
else:
mode_str = ""
cmds = "{}prep {}; write_json {}".format(mode_str, prep_opts, json_opts)
try:
j = utils.strip_yosys_json(commands(cmds, infiles))
except subprocess.CalledProcessError as ex:
print(ex.output)
exit(-1)
return json.loads(j)
def extract_pin(module, pstr, _regex=re.compile(r"([^/]+)/([^/]+)")):
"""
Extract the pin from a line of the result of a Yosys select command, or
None if the command result is irrelevant (e.g. does not correspond to the
correct module)
Inputs
-------
module: Name of module to extract pins from
pstr: Line from Yosys select command (`module/pin` format)
"""
m = re.match(r"([^/]+)/([^/]+)", pstr)
if m and m.group(1) == module:
return m.group(2)
else:
return None
def do_select(infiles, module, expr, prep=False, flatten=False):
"""
Run a Yosys select command (given the expression and input files)
on a module and return the result as a list of pins
Inputs
-------
infiles: List of Verilog source files to pass to Yosys
module: Name of module to run command on
expr: Yosys selector expression for select command
prep: Run prep command before selecting.
flatten: Flatten module when running prep.
"""
# TODO: All of these functions involve a fairly large number of calls to
# Yosys. Although performance here is unlikely to be a major priority any
# time soon, it might be worth investigating better options?
f = ""
if flatten:
f = "-flatten"
p = ""
if prep:
p = "prep -top {} {};".format(module, f)
else:
p = "proc;"
outfile = tempfile.mktemp()
sel_cmd = "{} cd {}; select -write {} {}".format(p, module, outfile, expr)
try:
commands(sel_cmd, infiles)
except subprocess.CalledProcessError as ex:
print(ex.output)
exit(-1)
pins = []
with open(outfile, 'r') as f:
for net in f:
snet = net.strip()
if (len(snet) > 0):
pin = extract_pin(module, snet)
if pin is not None:
pins.append(pin)
os.remove(outfile)
return pins
def get_combinational_sinks(infiles, module, innet):
"""Return a list of output ports which are combinational sinks of a given
input.
Inputs
-------
infiles: List of Verilog source files to pass to Yosys
module: Name of module to run command on
innet: Name of input net to find sinks of
"""
return do_select(
infiles, module, "={} %co* =o:* %i ={} %d".format(innet, innet))
def list_clocks(infiles, module):
"""Return a list of clocks in the module
Inputs
-------
infiles: List of Verilog source files to pass to Yosys
module: Name of module to run command on
"""
return do_select(
infiles, module,
"=c:* %x:+[CLK]:+[clk]:+[clock]:+[CLOCK] =c:* %d =x:* %i")
def get_clock_assoc_signals(infiles, module, clk):
"""Return the list of signals associated with a given clock.
Inputs
-------
infiles: List of Verilog source files to pass to Yosys
module: Name of module to run command on
clk: Name of clock to find associated signals
"""
return do_select(
infiles, module,
"select -list ={} %a %co* %x =i:* =o:* %u %i =a:ASSOC_CLOCK={} %u ={} "
"%d".format(clk, clk, clk))
# Find things which affect the given output
# show w:*D_IN_0 %a %ci*
# Find things which are affected by the given clock.
# show w:*INPUT_CLK %a %co*
# Find things which are affect by the given signal - combinational only.
# select -list w:*INPUT_CLK %a %co* %x x:* %i
def get_related_output_for_input(infiles, module, signal):
""".
Inputs
-------
infiles: List of Verilog source files to pass to Yosys
module: Name of module to run command on
clk: Name of clock to find associated signals
"""
return do_select(
infiles, module, "select -list =w:*{} %a %co* =o:* %i".format(signal))
def get_related_inputs_for_input(infiles, module, signal):
""".
Inputs
-------
infiles: List of Verilog source files to pass to Yosys
module: Name of module to run command on
clk: Name of clock to find associated signals
"""
return [
x for x in do_select(
infiles, module, "select -list =w:*{} %a %co* %x =i:* %i".format(
signal)) if x != signal
]