diff --git a/generators.core b/generators.core index 962849a..6085747 100644 --- a/generators.core +++ b/generators.core @@ -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. diff --git a/tests/test_virtual_pins.py b/tests/test_virtual_pins.py new file mode 100644 index 0000000..fc597c9 --- /dev/null +++ b/tests/test_virtual_pins.py @@ -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 diff --git a/virtual_pins.py b/virtual_pins.py new file mode 100644 index 0000000..dee9714 --- /dev/null +++ b/virtual_pins.py @@ -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() +