Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions generators.core
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,29 @@ generators:
directory for this generator.

Examples of template usage are in the examples directory.

virtual_pins:
interpreter: python
command: virtual_pins.py
description: Generate virtual pin constraints for a design
usage: |
Generate Quartus VIRTUAL_PIN constraints for all of the pins of a design.

The Intel example for making all pins virtual [1] requires synthesis to
be run. This generator uses a quicker process of just parsing an HDL file.

[1] https://www.intel.com/content/www/us/en/programmable/support/support-resources/design-examples/design-software/tcl/all_virtual_pins.html

Parameters:

input_file: The HDL file containing the module to be parsed. The most
reliable method is to supply the full absolute path.
Relative paths are assumed to be relative to the core being
built (the FuseSoC files_root variable).

output_file (optional): The file where the generated TCL will be placed.
By default this is "virtual_pins.tcl"

ignored_ports (optional): A list of ports that should be skipped when
making VIRTUAL_PIN assignments. By default
this is clk, clock, rst, and reset.
173 changes: 173 additions & 0 deletions tests/test_virtual_pins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import pytest
import virtual_pins


# This should probably be a fixture, but I had trouble getting parameters passed to it.
def setup_gen(tmp_path, hdl, ext):

hdl_path = tmp_path / ("test" + ext)
tcl_path = tmp_path / "test.tcl"

gen_config = {
"parameters": {"input_file": str(hdl_path), "output_file": str(tcl_path)},
"vlnv": "bogus:core:here",
"files_root": tmp_path,
}

hdl_path.write_text(hdl)

return (virtual_pins.VirtualPinGenerator(data=gen_config), tcl_path)


# HDL and expected results are included in the parameters as embedded strings.
# This can be tough to read, so perhaps they should be moved to external files
# or just module-level variables

@pytest.mark.parametrize(
"bad_hdl,bad_hdl_ext",
[
(
"""
module syntax_error (
input a,
input b,
output c
);
// Oops I forgot my semicolon
c <= a & b
endmodule
""",
".v",
),
(
"""
library IEEE;
use ieee.std_logic_1164.all;

entity syntax_error is
port (
-- Oops my comma should be a semicolon
a : in std_logic,
b : in std_logic;
c : out std_logic
);
end entity syntax_error;

architecture test of syntax_error is
begin
c <= a and b;
end architecture test;
""",
".vhd",
),
],
)
def test_syntax_err(tmp_path, bad_hdl, bad_hdl_ext):

from hdlConvertor._hdlConvertor import ParseException

uut, output = setup_gen(tmp_path, bad_hdl, bad_hdl_ext)

with pytest.raises(ParseException):
uut.run()


@pytest.mark.parametrize(
"hdl,hdl_ext,expected",
[
(
"""
module acc #(
parameter WIDTH = 13,
parameter COUNT = 10
)(
input clock,
input reset,
input load,
input [WIDTH-1:0] a,
input [WIDTH-1:0] b,
output [2*WIDTH-1:0] acc,
output valid
);

always @(posedge clock, posedge reset)
begin
if (reset)
begin
acc <= 2*WIDTH-1'b0;
valid <= 1'b0;
end else begin
valid <= 1'b1;
acc <= acc + a + b;
end
end

endmodule
""",
".v",
"""set ports {load a b acc valid}
foreach p $ports {
set_instance_assignment -name VIRTUAL_PIN ON -to $p
}
""",
),
(
"""
library IEEE;
use IEEE.std_logic_1164.all;
use IEEE.numeric_std.all;

entity ACC is
generic (
WIDTH : positive := 13;
COUNT : positive := 10
);
port (
clock : in std_logic;
reset : in std_logic;
load : in std_logic;
a : in std_logic_vector(WIDTH-1 downto 0);
b : in std_logic_vector(WIDTH-1 downto 0);
acc : out std_logic_vector(2*WIDTH-1 downto 0);
valid : out std_logic
);
end entity ACC;

architecture test of ACC is

signal acc_i : unsigned(2*WIDTH-1 downto 0);

begin

acc <= std_logic_vector(acc_i);

process (clock, reset)
begin
if reset = '1' then
acc <= (others => '0');
valid <= '0';
else
if rising_edge(clock) then
valid <= '1';
acc_i <= acc_i + unsigned(a) + unsigned(b);
end if;
end if;
end process;
end architecture test;
""",
".vhd",
"""set ports {load a b acc valid}
foreach p $ports {
set_instance_assignment -name VIRTUAL_PIN ON -to $p
}
""",
),
],
)
def test_basic_ports(tmp_path, hdl, hdl_ext, expected):

uut, output = setup_gen(tmp_path, hdl, hdl_ext)

uut.run()

assert output.read_text() == expected
129 changes: 129 additions & 0 deletions virtual_pins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import sys
import pathlib

from fusesoc.capi2.generator import Generator

# hdlparse was considered but didn't appear to extract VHDL entities. Someone
# else reported a similar problem in
# https://github.com/kevinpt/hdlparse/issues/6
#
# hdlConvertor has a less refined interface, but seemed better than doing
# something custom with pyparsing or similar

import hdlConvertor
from hdlConvertor.language import Language

class VirtualPinGenerator(Generator):

@staticmethod
def _get_lang(f):
ext = f.suffix

ext_to_lang = {
'.v' : Language.VERILOG,
'.sv' : Language.SYSTEM_VERILOG,
'.vhd' : Language.VHDL,
'.vhdl' : Language.VHDL
}

if ext not in ext_to_lang:
print("Unable to map extension {} to a language".format(ext))
exit(1)
else:
return ext_to_lang[ext]

def run(self):

ignored_ports_default = ['clk', 'clock', 'rst', 'reset']

input_file = pathlib.Path(self.config.get('input_file'))
output_file = self.config.get('output_file', 'virtual_pins.tcl')
ignored_ports = self.config.get('ignored_ports', ignored_ports_default)

convertor = hdlConvertor.HdlConvertor()

lang = self._get_lang(input_file)

# Finding the input file can be tricky since the generator runs
# in a different directory (on Linux
# ~/.cache/fusesoc/generated) and doesn't know where FuseSoC
# was originally run. If the file is associated with the core being built self.files_root
# should give us the parent path. However, if the input file is
# associated with a lower-level core a full path is likely to be
# required.

# If the input file is a relative path look for it in self.files_root
if input_file.is_absolute():
parse_file = input_file
else:
parse_file = pathlib.Path(self.files_root).joinpath(input_file)

if not parse_file.exists():
print("Can't find input file:", parse_file)
exit(1)

lang = self._get_lang(parse_file)

# Currently just search the directory of the input file for Verilog includes
include_dirs = [ str(parse_file.parent) ]

# hdlConvertor is currently a bit chatty, outputing text the user
# probably doesn't want to see about unsupported features, etc. like
# the following:
#
# /path/to/file.vhd:18:0: DesignFileParser.visitContext_item - library_clause Conversion to Python object not implemented
# ...libraryieee;...
#
# It would perhaps be nice to capture this output, but that's
# non-trivial since hdlConvertor uses a C++ parser. See
#
# https://stackoverflow.com/questions/52219393/how-do-i-capture-stderr-in-python
#
# and the linked blog for how to do this if required

ast = convertor.parse(str(parse_file), lang, include_dirs)

# Find modules
hdl_modules = [m for m in ast.objs if isinstance(m, hdlConvertor.hdlAst.HdlModuleDec)]

if len(hdl_modules) == 0:
print("Found no module or entity declarations")
exit(1)
elif len(hdl_modules) > 1:
print("Found multiple module declarations but only using the first")

# Get port names
all_ports = [p.name for p in hdl_modules[0].ports]
filtered_ports = [ p for p in all_ports if p not in ignored_ports]

f = open(output_file, 'w');

# The generated TCL code loops over a list of ports:
#
# set ports {port_a port_b port_c port_d}
#
# foreach p $ports {
# set_instance_assignment -name VIRTUAL_PIN ON -to $p
# }
#
# It may be simpler to just do the looping in Python with something like the following:
#
# for p in filtered_ports:
# f.write('set_instance_assignment -name VIRTUAL_PIN ON -to {}\n'.format(p))

tcl = """set ports {{{}}}
foreach p $ports {{
set_instance_assignment -name VIRTUAL_PIN ON -to $p
}}
"""

f.write(tcl.format(' '.join(filtered_ports)))

f.close()
self.add_files([{output_file : {'file_type' : 'tclSource'}}])

if __name__ == "__main__":
g = VirtualPinGenerator()
g.run()
g.write()