From 38b92dd78d9f1d6c1734f8dab5f7ef3d7aeec395 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper560 <48131108+GCHQDeveloper560@users.noreply.github.com> Date: Thu, 21 Nov 2019 09:32:54 +0000 Subject: [PATCH 1/2] Add a generator for Quartus VIRTUAL_PIN constraints Basic pytest tests are included, though the directory structure and lack of any settings mean the tests must be run with "python -m pytest tests/" to get the current directory in the Python path. --- generators.core | 26 ++++++ tests/test_virtual_pins.py | 173 +++++++++++++++++++++++++++++++++++++ virtual_pins.py | 129 +++++++++++++++++++++++++++ 3 files changed, 328 insertions(+) create mode 100644 tests/test_virtual_pins.py create mode 100644 virtual_pins.py diff --git a/generators.core b/generators.core index 962849a..2647089 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() + From 5317aa401f90e040daa0bc0f8cbc834c765f5137 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper560 <48131108+GCHQDeveloper560@users.noreply.github.com> Date: Wed, 4 Dec 2019 09:30:20 +0000 Subject: [PATCH 2/2] Replace tab that causes a parse error with spaces --- generators.core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators.core b/generators.core index 2647089..6085747 100644 --- a/generators.core +++ b/generators.core @@ -102,7 +102,7 @@ generators: Parameters: - input_file: The HDL file containing the module to be parsed. The most + 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).