Scripting with Pyosys¶
Pyosys is a limited subset of the Yosys C++ API (aka “libyosys”) made available using the Python programming language.
Like .ys and .tcl scripts, Pyosys provides an interface to write Yosys
scripts in the Python programming language, giving you the benefits of
a type system, control flow, object-oriented programming, and more; especially
that the other options lack a type system and control flow/OOP in Tcl is
limited.
Though unlike these two, Pyosys goes a bit further, allowing you to use the Yosys API to implement advanced functionality that would otherwise require custom passes written in C++.
Getting Pyosys¶
Pyosys supports CPython 3.8 or higher. You can access Pyosys using one of two methods:
Compiling Yosys with the Makefile flag
ENABLE_PYOSYS=1This adds the flag
-yto the Yosys binary, which allows you to execute Python scripts using an interpreter embedded in Yosys itself:yosys -y ./my_pyosys_script.pyDo note this requires some build-time dependencies to be available to Python, namely,
pybind11andcxxheaderparser. By default, the requireduvpackage will be used to create an ephemeral environment with the correct versions of the tools installed.You can force use of your current Python environment by passing the Makefile flag
PYOSYS_USE_UV=0.Installing the Pyosys wheels
On macOS and GNU/Linux you can install pre-built wheels of Yosys using
pip:python3 -m pip install pyosysWhich then allows you to run your scripts as follows:
python3 ./my_pyosys_script.py
Scripting and Database Inspection¶
To start with, you have to import libyosys as follows:
from pyosys import libyosys
As a reminder, Python allows you to alias imported modules and objects, so this import may be preferable for terseness:
from pyosys import libyosys as ys
Now, scripting is actually quite similar to .ys and .tcl script in that
you can provide mostly text commands. Albeit, you can construct your scripts
to use Python’s amenities like conditional execution, loops, and functions:
do_flatten = True
ys.run_pass("read_verilog tests/simple/fiedler-cooley.v")
ys.run_pass("hierarchy -check -auto-top")
if do_flatten:
ys.run_pass("flatten")
…but this does not strictly provide anything that Tcl scripts do not provide you with. The real power of using Pyosys comes from the fact you can manually instantiate, manage, and interact with the design database.
As an example, here is the same script with a manually instantiated design.
design = ys.Design()
ys.run_pass("read_verilog tests/simple/fiedler-cooley.v", design)
ys.run_pass("hierarchy -check -auto-top", design)
What’s new here is that you can manually inspect the design’s database. This
gives you access to a huge chunk of the design database API as declared in
the kernel/rtlil.h header.
For example, here’s how to list the input and output ports of the top module of your design:
top_module = design.top_module()
for id, wire in top_module.wires_.items():
if not wire.port_input and not wire.port_output:
continue
description = "input" if wire.port_input else "output"
description += " " + wire.name.str()
if wire.width != 1:
frm = wire.start_offset
to = wire.start_offset + wire.width
if wire.upto:
to, frm = frm, to
description += f" [{to}:{frm}]"
print(description)
Tip
C++ data structures in Yosys are bridged to Python such that they have a pretty similar API to Python objects, for example:
std::vectorsupports the same methods as iterables in Python.std::setand hashlibpoolsupport the same methods assets in Python. Whilesetis ordered,poolis not and modifications may cause a complete reordering of the set.dictsupports the same methods asdicts in Python, albeit it is unordered, and modifications may cause a complete reordering of the dictionary.idictuses a custom set of methods because it doesn’t map very cleanly to an existing Python data structure. Seepyosys/hashlib.hfor more info.
For most operations, the Python equivalents are also supported as arguments where they will automatically be cast to the right type, so you do not have to manually instantiate the right underlying C++ object(s) yourself.
Modifying the Database¶
Warning
Any modifications to the database may invalidate previous references held by Python, just as if you were writing C++. Pyosys does not currently attempt to keep deleted objects alive if a reference is held by Python.
You are not restricted to inspecting the database either: you have the ability to modify it, and introduce new elements and/or changes to your design.
As a demonstrative example, let’s assume we want to add an enable line to all flip-flops in our fiedler-cooley design.
First of all, we will run synth to convert all of the logic to
Yosys’s internal cell structure (see Gate-level cells):
ys.run_pass("synth", design)
Next, we need to add the new port. The method for this is Module::addWire.
Tip
IdString is Yosys’s internal representation of strings used as identifiers within Verilog designs. They are efficient as only integers are stored and passed around, but they can be translated to and from normal strings at will.
Pyosys will automatically cast Python strings to IdStrings for you, but the rules around IdStrings apply, namely that broadly:
Identifiers for internal cells must start with
$.All other identifiers must start with
\.
enable_line = top_module.addWire("\\enable")
enable_line.port_input = True
top_module.fixup_ports()
Notice how we modified the wire then called a method to make Yosys re-process the ports.
Next, we can iterate over all constituent cells, and if they are of the type
$_DFF_P_, we do two things:
Change their type to
$_DFFE_PP_to enable hooking up an enable signal.Hooking up the enable signal.
for cell in top_module.cells_.values():
if cell.type != "$_DFF_P_":
continue
cell.type = "$_DFFE_PP_"
cell.setPort("\\E", ys.SigSpec(enable_line))
To verify that you did everything correctly, it is prudent to call .check()
on the module you’re manipulating as follows after you’re done with a set of
changes:
top_module.check()
ys.run_pass("stat", design)
And then finally, write your outputs. Here, I choose an intermediate Verilog
file and synth_ice40 to map it to the iCE40 architecture.
ys.run_pass("write_verilog out.v", design)
ys.run_pass("synth_ice40 -json out.json", design)
And voilà, you will note that in the intermediate output, all always @
statements should have an if (enable).
Encapsulating as Passes¶
Just like when writing C++, you can encapsulate routines in terms of “passes”,
which adds your Pass to a global registry of commands accessible using
run_pass.
from pyosys import libyosys as ys
class AllEnablePass(ys.Pass):
def __init__(self):
super().__init__(
"all_enable",
"makes all _DFF_P_ registers require an enable signal"
)
def execute(self, args, design):
ys.log_header(design, "Adding enable signals\n")
ys.log_push()
top_module = design.top_module()
if "\\enable" not in top_module.wires_:
enable_line = top_module.addWire("\\enable")
enable_line.port_input = True
top_module.fixup_ports()
for cell in top_module.cells_.values():
if cell.type != "$_DFF_P_":
continue
cell.type = "$_DFFE_PP_"
cell.setPort("\\E", ys.SigSpec(enable_line))
ys.log_pop()
p = AllEnablePass() # register the pass
# using the pass
design = ys.Design()
ys.run_pass("read_verilog tests/simple/fiedler-cooley.v", design)
ys.run_pass("hierarchy -check -auto-top", design)
ys.run_pass("synth", design)
ys.run_pass("all_enable", design)
ys.run_pass("write_verilog out.v", design)
ys.run_pass("synth_ice40 -json out.json", design)
In general, abstract classes and virtual methods are not really supported by Pyosys due to their complexity, but there are two exceptions which are:
Passinkernel/register.hMonitorinkernel/rtlil.h