├── tests ├── __init__.py ├── base_hdl_test.py ├── test_vivado_ip.py └── test_cosimulation.py ├── examples ├── __init__.py └── dsp48e1 │ ├── __init__.py │ ├── README.rst │ ├── utils.py │ ├── simple_wrapper.py │ ├── test_simple_wrapper.py │ ├── dsp48e1.py │ └── test_dsp48e1.py ├── veriutils.cfg ├── .gitignore ├── .coveragerc ├── ovenbird ├── __init__.py ├── vivado_ip.py └── cosimulation.py ├── LICENSE ├── setup.py └── README.mkd /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/dsp48e1/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /veriutils.cfg: -------------------------------------------------------------------------------- 1 | [General] 2 | part=xc7z020clg484-1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | build 4 | dist 5 | htmlcov 6 | .coverage 7 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | source = 5 | ovenbird 6 | tests 7 | examples 8 | 9 | [report] 10 | exclude_lines = 11 | if verbose: 12 | if self._verbose: 13 | pragma: no cover 14 | if __debug__ 15 | -------------------------------------------------------------------------------- /examples/dsp48e1/README.rst: -------------------------------------------------------------------------------- 1 | This directory includes a dsp48e1 block, which is used and tested as-is 2 | by the `test_dsp48e1` functions. 3 | 4 | Also included is `simple_wrapper.py`. This demonstrates (and so tests) using 5 | the dsp48e1 in a hierarchical design. 6 | -------------------------------------------------------------------------------- /ovenbird/__init__.py: -------------------------------------------------------------------------------- 1 | from distutils import spawn as _spawn 2 | import subprocess as _subprocess 3 | 4 | VIVADO_EXECUTABLE = _spawn.find_executable('vivado') 5 | 6 | if VIVADO_EXECUTABLE is not None: 7 | vivado_version_exe = _subprocess.Popen( 8 | [VIVADO_EXECUTABLE, '-version'], stdin=_subprocess.PIPE, 9 | stdout=_subprocess.PIPE, stderr=_subprocess.PIPE) 10 | 11 | try: 12 | out, err = vivado_version_exe.communicate() 13 | VIVADO_VERSION = (out.split()[1][1:]).decode('utf8') 14 | except IndexError: 15 | VIVADO_VERSION = None 16 | 17 | else: 18 | VIVADO_VERSION = None 19 | 20 | from .cosimulation import * 21 | from .vivado_ip import * 22 | 23 | import myhdl as _myhdl 24 | class OvenbirdConversionError(_myhdl.ConversionError): 25 | pass 26 | -------------------------------------------------------------------------------- /examples/dsp48e1/utils.py: -------------------------------------------------------------------------------- 1 | 2 | from myhdl import * 3 | from random import random 4 | 5 | @block 6 | def weighted_random_reset_source(driven_reset, clock, 7 | active_probability=0.5): 8 | '''A random reset source that has a couple of cycles of initialisation 9 | first. 10 | ''' 11 | @instance 12 | def custom_reset(): 13 | driven_reset.next = 1 14 | yield(clock.posedge) 15 | driven_reset.next = 1 16 | yield(clock.posedge) 17 | while True: 18 | next_reset = random() 19 | # Be false when less than active_probability 20 | if next_reset > active_probability: 21 | driven_reset.next = 1 22 | else: 23 | driven_reset.next = 0 24 | 25 | yield(clock.posedge) 26 | 27 | return custom_reset 28 | -------------------------------------------------------------------------------- /examples/dsp48e1/simple_wrapper.py: -------------------------------------------------------------------------------- 1 | 2 | from .dsp48e1 import DSP48E1, DSP48E1_OPMODE_MULTIPLY, N_DSP48E1_OPMODES 3 | 4 | from myhdl import intbv, Signal, always_seq, instance, block 5 | 6 | # A simple wrapper around the DSP48E1, so all the tests for that 7 | # object should work. The point is to test the DSP48 being not the top level 8 | # in the hierarchy. 9 | 10 | @block 11 | def SimpleWrapper(A, B, P, clock_enable, reset, clock): 12 | '''A simple wrapper that implements the multiply function of the DSP48E1 13 | block. The arguments are trivially passed through to that block. 14 | ''' 15 | 16 | C = Signal(intbv(0, min=P.min, max=P.max)) 17 | opmode = Signal(intbv(0, min=0, max=N_DSP48E1_OPMODES)) 18 | 19 | @always_seq(clock.posedge, reset) 20 | def constant_signal_driver(): 21 | C.next = 0 22 | opmode.next = DSP48E1_OPMODE_MULTIPLY 23 | 24 | multiply_wrapper = DSP48E1( 25 | A, B, C, P, opmode, clock_enable, reset, clock) 26 | 27 | return multiply_wrapper, constant_signal_driver 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Henry Gomersall 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Ovenbird nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Knowledge Economy Developments Ltd 2 | # 3 | # Henry Gomersall 4 | # heng@kedevelopments.co.uk 5 | # 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 19 | # 20 | 21 | from setuptools import setup 22 | 23 | version = '0.9' 24 | 25 | package_data = {} 26 | 27 | long_description = ''' 28 | ''' 29 | 30 | setup_args = { 31 | 'name': 'Ovenbird', 32 | 'version': version, 33 | 'author': 'Henry Gomersall', 34 | 'author_email': 'heng@kedevelopments.co.uk', 35 | 'description': ('Tools for interacting with Vivado IP, both creating' 36 | 'and using.'), 37 | 'url': 'http://hgomersall.github.com/Ovenbird/', 38 | 'long_description': long_description, 39 | 'classifiers': [ 40 | 'Programming Language :: Python', 41 | 'Programming Language :: Python :: 3', 42 | 'Development Status :: 3 - Alpha', 43 | 'License :: OSI Approved :: GNU General Public License (GPL)', 44 | 'Operating System :: OS Independent', 45 | 'Intended Audience :: Developers', 46 | 'Intended Audience :: Science/Research', 47 | 'Topic :: Scientific/Engineering', 48 | ], 49 | 'packages':['ovenbird'], 50 | 'package_data': package_data, 51 | } 52 | 53 | if __name__ == '__main__': 54 | setup(**setup_args) 55 | -------------------------------------------------------------------------------- /README.mkd: -------------------------------------------------------------------------------- 1 | Ovenbird 2 | ======== 3 | 4 | A tool for merging the MyHDL workflow with Vivado (and possibly other tools 5 | further down the line). 6 | 7 | The emphasis at this stage is on extending the 8 | [Veriutils](https://github.com/hgomersall/Veriutils) toolkit to support 9 | Vivado. 10 | 11 | This involves two core features: 12 | 13 | 1. The `vivado_vhdl_cosimulation` and `vivado_verilog_cosimulation` functions. 14 | These are functionally equivalent to `veriutils.myhdl_cosimulation` (albeit 15 | with a couple of extra arguments), but run the device under test inside 16 | the Vivado simulator. 17 | 18 | 2. The VivadoIP block. This allows Vivado IP blocks (notably encrypted IP) 19 | to be included and controlled in a MyHDL hierarchy, with 20 | `vivado_*_cosimulation` using the correct IP block. Encrypted IP is not 21 | functionally accessible inside MyHDL, so it is necessary to reimplement the 22 | functionality of the block for MyHDL simulation. This is not as serious 23 | a drawback as one might initially imagine as the MyHDL implementation does 24 | not need to be convertible and typically the IP blocks can be easily 25 | implemented using the full power of Python (and all the libraries available) - 26 | indeed, in implementing the Python model of the IP block, you'll understand 27 | it properly. 28 | 29 | The best way of understanding how to use the tool is by looking at the 30 | examples in the `examples` directory, which also serve as a test bench. 31 | 32 | The examples demonstrate using Vivado with an IP block integrated with MyHDL, 33 | both standalone (`dsp48e1.py`) and as part of a hierarchy 34 | (`simple_wrapper.py`). 35 | 36 | In order to use Vivado, it is necessary to set up your environment. Under 37 | linux, this is something like: 38 | 39 | source /opt/Xilinx/Vivado/2019.2/settings64.sh 40 | 41 | I imagine there's something similar under Windows (though I've not tested it). 42 | 43 | If the executable is not in the path, the relevant tests will be skipped, or 44 | if you try to use the Vivado cosimulation, an `EnvironmentError` will be 45 | raised. 46 | 47 | Again, to use the Vivado capability, it is necessary to have a `veriutils.cfg` 48 | file in the current working directory to configure the part being used. 49 | An example one is provided. The need for `veriutils.cfg` will likely be 50 | removed in subsequent releases (much of the need for it has been removed 51 | in the creation of Ovenbird). 52 | 53 | Ovenbird depends on the MyHDL initial value support, which is now part of 54 | MyHDL master. 55 | 56 | The code is released under the BSD 3 clause license. See License.txt for the 57 | text of this. 58 | -------------------------------------------------------------------------------- /examples/dsp48e1/test_simple_wrapper.py: -------------------------------------------------------------------------------- 1 | 2 | from .simple_wrapper import SimpleWrapper 3 | from .test_dsp48e1 import DSP48E1TestCase 4 | 5 | import unittest 6 | 7 | from myhdl import always_seq, block 8 | from veriutils import myhdl_cosimulation 9 | from ovenbird import vivado_vhdl_cosimulation, VIVADO_EXECUTABLE 10 | 11 | class TestSimpleWrapperSimulation(DSP48E1TestCase): 12 | '''Test that wrapping a somewhat non-trivial object works fine. 13 | ''' 14 | def cosimulate(self, sim_cycles, dut_factory, ref_factory, args, 15 | arg_types, **kwargs): 16 | 17 | return myhdl_cosimulation(sim_cycles, dut_factory, ref_factory, 18 | args, arg_types, **kwargs) 19 | 20 | def test_basic_multiply(self): 21 | '''The basic multiply should be the product of A and B. 22 | ''' 23 | 24 | self.opmode.val[:] = self.operations['multiply'] 25 | 26 | @block 27 | def ref(**kwargs): 28 | 29 | P = kwargs['P'] 30 | A = kwargs['A'] 31 | B = kwargs['B'] 32 | clock = kwargs['clock'] 33 | reset = kwargs['reset'] 34 | 35 | @always_seq(clock.posedge, reset=reset) 36 | def test_basic_multiply(): 37 | P.next = A * B 38 | 39 | return test_basic_multiply 40 | 41 | args = self.default_args.copy() 42 | arg_types = self.default_arg_types.copy() 43 | 44 | # Get rid of unnecessary args 45 | del args['C'] 46 | del arg_types['C'] 47 | del args['opmode'] 48 | del arg_types['opmode'] 49 | 50 | cycles = 20 51 | dut_outputs, ref_outputs = self.cosimulate( 52 | cycles, SimpleWrapper, ref, args, arg_types) 53 | 54 | # There are pipeline_registers cycles latency on the output. 55 | # The reference above has only 1 cycle latency, so we need to offset 56 | # the results by pipeline_registers - 1 cycles. 57 | self.assertEqual(dut_outputs['P'][self.pipeline_registers - 1:], 58 | ref_outputs['P'][:-(self.pipeline_registers - 1)]) 59 | 60 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 'Vivado executable not in path') 61 | class TestSimpleWrapperVivadoSimulation(TestSimpleWrapperSimulation): 62 | '''The tests of TestDSP48E1Simulation should run under the Vivado 63 | simulator with VHDL. 64 | ''' 65 | 66 | def cosimulate(self, sim_cycles, dut_factory, ref_factory, args, 67 | arg_types, **kwargs): 68 | 69 | return vivado_vhdl_cosimulation(sim_cycles, dut_factory, ref_factory, 70 | args, arg_types, **kwargs) 71 | 72 | -------------------------------------------------------------------------------- /tests/base_hdl_test.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | from random import randrange 4 | from myhdl import Signal, intbv 5 | 6 | from mock import patch, call 7 | 8 | def get_signed_intbv_rand_signal(width, val_range=None, init_value=0): 9 | '''Create a signed intbv random signal. 10 | ''' 11 | if val_range is not None: 12 | min_val = val_range[0] 13 | max_val = val_range[1] 14 | else: 15 | min_val = -(2**(width - 1) - 1) 16 | max_val = 2**(width - 1) 17 | 18 | signal = Signal(intbv(init_value, min=min_val, max=max_val)) 19 | signal.val[:] = randrange(min_val, max_val) 20 | 21 | return signal, min_val, max_val 22 | 23 | 24 | def get_unsigned_intbv_rand_signal(width, init_value=0): 25 | '''Create an unsigned intbv random signal. 26 | ''' 27 | min_val = 0 28 | max_val = 2**(width) 29 | signal = Signal(intbv(val=init_value, min=min_val, max=max_val)) 30 | signal.val[:] = randrange(min_val, max_val) 31 | 32 | return signal, min_val, max_val 33 | 34 | 35 | class TestCase(unittest.TestCase): 36 | '''Implements a python version agnostic version of unittest.TestCase. 37 | ''' 38 | def __init__(self, *args, **kwargs): 39 | 40 | super(TestCase, self).__init__(*args, **kwargs) 41 | 42 | if not hasattr(self, 'assertRaisesRegex'): # pragma: no branch 43 | self.assertRaisesRegex = self.assertRaisesRegexp 44 | 45 | 46 | class HDLTestCase(TestCase): 47 | '''Add some useful HDL specific methods. 48 | ''' 49 | def setUp(self): 50 | self.default_args = {} 51 | 52 | def do_port_check_intbv_test(self, constructor, port_name, width=None, 53 | signed=False, val_range=None, 54 | attribute=None): 55 | '''Checks the intbv port test was performed on the specified port 56 | with the given port name and width. If attribute is not None, 57 | then the specified attribute on the given port name is used (e.g. 58 | for interfaces). 59 | ''' 60 | if attribute is None: 61 | port_to_check = self.default_args[port_name] 62 | else: 63 | port_to_check = getattr(self.default_args[port_name], attribute) 64 | port_name += '.%s' % (attribute,) 65 | 66 | patch_location = constructor.__module__ + '.check_intbv_signal' 67 | with patch(patch_location) as (mock_check_function): 68 | 69 | # Make the call 70 | constructor(**self.default_args) 71 | 72 | mock_check_function.call_args_list 73 | 74 | # Enforce a certain calling convention that guarantees everything 75 | # is nice and consistent. 76 | if width is not None: 77 | self.assertIn( 78 | call(port_to_check, port_name, width, signed=signed), 79 | mock_check_function.call_args_list) 80 | else: 81 | self.assertIn( 82 | call(port_to_check, port_name, val_range=val_range), 83 | mock_check_function.call_args_list) 84 | 85 | 86 | def do_port_check_bool_test(self, constructor, port_name): 87 | '''Checks the bool port test was performed on the specified port 88 | with the given port name. 89 | ''' 90 | port_to_check = self.default_args[port_name] 91 | 92 | patch_location = constructor.__module__ + '.check_bool_signal' 93 | with patch(patch_location) as (mock_check_function): 94 | 95 | # Make the call 96 | constructor(**self.default_args) 97 | self.assertIn(call(port_to_check, port_name), 98 | mock_check_function.call_args_list) 99 | 100 | def do_port_check_reset_test(self, constructor, port_name, active, isasync): 101 | '''Checks the reset port test was performed on the specified port 102 | with the given port name. 103 | ''' 104 | port_to_check = self.default_args[port_name] 105 | 106 | patch_location = constructor.__module__ + '.check_reset_signal' 107 | with patch(patch_location) as (mock_check_function): 108 | 109 | # Make the call 110 | constructor(**self.default_args) 111 | self.assertIn(call(port_to_check, port_name, active=active, 112 | isasync=isasync), 113 | mock_check_function.call_args_list) 114 | 115 | 116 | -------------------------------------------------------------------------------- /tests/test_vivado_ip.py: -------------------------------------------------------------------------------- 1 | from myhdl import * 2 | 3 | from unittest import TestCase 4 | 5 | from ovenbird import * 6 | 7 | 8 | class VivadoVectorOr(VivadoIP): 9 | 10 | def __init__(self): 11 | port_mappings = { 12 | 'in_A': (intbv(0)[8:], PortDirection.input, 'Op1'), 13 | 'in_B': (intbv(0)[8:], PortDirection.input, 'Op2'), 14 | 'output': (intbv(0)[8:], PortDirection.output, 'Res')} 15 | 16 | config = {'c_size': '8', 17 | 'c_operation': 'or'} 18 | 19 | entity_name = 'vector_or' 20 | ports = port_mappings 21 | ip_name = 'util_vector_logic' 22 | vendor = 'xilinx.com' 23 | library = 'ip' 24 | version = '2.0' 25 | 26 | VivadoIP.__init__( 27 | self, entity_name, port_mappings, ip_name, vendor, library, 28 | version, config) 29 | 30 | 31 | @block 32 | def comb_vector_or(in_A, in_B, output, ip_factory): 33 | 34 | length = len(in_A) 35 | 36 | @always_comb 37 | def vector_or(): 38 | for n in range(length): 39 | output.next[n] = in_A[n] or in_B[n] 40 | 41 | comb_vector_or.verilog_code = ip_factory.get_verilog_instance() 42 | comb_vector_or.vhdl_code = ip_factory.get_vhdl_instance() 43 | 44 | in_A.read = True 45 | in_B.read = True 46 | output.driven = 'wire' 47 | 48 | return vector_or 49 | 50 | @block 51 | def comb_vector_or_with_port_mappings( 52 | A, B, output, ip_factory, port_mappings): 53 | '''We change the signal names here to unmap from the expected in_A and 54 | in_B so port_mappings can do its thing. 55 | ''' 56 | length = len(A) 57 | 58 | @always_comb 59 | def vector_or(): 60 | for n in range(length): 61 | output.next[n] = A[n] or B[n] 62 | 63 | comb_vector_or_with_port_mappings.verilog_code = ( 64 | ip_factory.get_verilog_instance(**port_mappings)) 65 | comb_vector_or_with_port_mappings.vhdl_code = ( 66 | ip_factory.get_vhdl_instance(**port_mappings)) 67 | 68 | A.read = True 69 | B.read = True 70 | output.driven = 'wire' 71 | 72 | return vector_or 73 | 74 | @block 75 | def clocked_vector_or(clock, in_A, in_B, output, ip_factory, 76 | port_mappings=None): 77 | 78 | internal_sig = Signal(intbv(0)[len(in_A):]) 79 | 80 | @always(clock.posedge) 81 | def clocked_or(): 82 | output.next = internal_sig 83 | 84 | clock.read = True 85 | 86 | if port_mappings is None: 87 | comb_or = comb_vector_or(in_A, in_B, internal_sig, ip_factory) 88 | else: 89 | comb_or = comb_vector_or_with_port_mappings( 90 | in_A, in_B, internal_sig, ip_factory, port_mappings) 91 | 92 | return clocked_or, comb_or 93 | 94 | class VivadoIPTests(object): 95 | 96 | def setUp(self): 97 | self.ip_factory = VivadoVectorOr() 98 | 99 | self.args = { 100 | 'in_A': Signal(intbv(0)[8:]), 'in_B': Signal(intbv(0)[8:]), 101 | 'output': Signal(intbv(0)[8:]), 'clock': Signal(False), 102 | 'ip_factory': self.ip_factory} 103 | 104 | self.arg_types = { 105 | 'in_A': 'random', 'in_B': 'random', 'output': 'output', 106 | 'clock': 'clock', 'ip_factory': 'non-signal'} 107 | 108 | def vivado_sim_wrapper(self, sim_cycles, dut_factory, ref_factory, 109 | args, arg_types, **kwargs): 110 | 111 | raise NotImplementedError 112 | 113 | def test_basic(self): 114 | '''The IP block should return the same as the reference block. 115 | ''' 116 | 117 | dut_results, ref_results = self.vivado_sim_wrapper( 118 | 10, clocked_vector_or, clocked_vector_or, self.args, 119 | self.arg_types) 120 | 121 | self.assertEqual(dut_results, ref_results) 122 | 123 | def test_basic_with_port_mappings(self): 124 | '''The IP block should return the same as the reference block when 125 | the port_mappings arguments are used 126 | ''' 127 | self.args = { 128 | 'in_A': Signal(intbv(0)[8:]), 'in_B': Signal(intbv(0)[8:]), 129 | 'output': Signal(intbv(0)[8:]), 'clock': Signal(False), 130 | 'ip_factory': self.ip_factory} 131 | 132 | port_mappings = {'in_A': 'A', 'in_B': 'B'} 133 | 134 | self.args['port_mappings'] = port_mappings 135 | 136 | self.arg_types = { 137 | 'in_A': 'random', 'in_B': 'random', 'output': 'output', 138 | 'clock': 'clock', 'ip_factory': 'non-signal', 139 | 'port_mappings': 'non-signal'} 140 | 141 | dut_results, ref_results = self.vivado_sim_wrapper( 142 | 10, clocked_vector_or, clocked_vector_or, self.args, 143 | self.arg_types) 144 | 145 | self.assertEqual(dut_results, ref_results) 146 | 147 | 148 | class TestVivadoVerilogCosimulationFunction(VivadoIPTests, TestCase): 149 | '''There should be an alternative version of the cosimulation function 150 | that runs the device under test through the Vivado verilog simulator. 151 | ''' 152 | 153 | def vivado_sim_wrapper(self, sim_cycles, dut_factory, ref_factory, 154 | args, arg_types, **kwargs): 155 | 156 | return vivado_verilog_cosimulation( 157 | sim_cycles, dut_factory, ref_factory, args, arg_types, **kwargs) 158 | 159 | class TestVivadoVHDLCosimulationFunction(VivadoIPTests, TestCase): 160 | '''There should be an alternative version of the cosimulation function 161 | that runs the device under test through the Vivado verilog simulator. 162 | ''' 163 | 164 | def vivado_sim_wrapper(self, sim_cycles, dut_factory, ref_factory, 165 | args, arg_types, **kwargs): 166 | 167 | return vivado_vhdl_cosimulation( 168 | sim_cycles, dut_factory, ref_factory, args, arg_types, **kwargs) 169 | -------------------------------------------------------------------------------- /tests/test_cosimulation.py: -------------------------------------------------------------------------------- 1 | from tests.base_hdl_test import TestCase 2 | 3 | from myhdl import (intbv, modbv, enum, Signal, ResetSignal, instance, 4 | delay, always, always_seq, Simulation, StopSimulation, 5 | always_comb, block, BlockError, ConversionError, 6 | ToVHDLWarning) 7 | 8 | import unittest 9 | import copy 10 | from itertools import chain 11 | from random import randrange 12 | import warnings 13 | 14 | import os 15 | import tempfile 16 | import shutil 17 | 18 | import mock 19 | 20 | from veriutils import AVAILABLE_TIME_UNITS 21 | from veriutils.tests.test_convertible import ConvertibleCodeTestsMixin 22 | 23 | from ovenbird import ( 24 | VIVADO_EXECUTABLE, vivado_verilog_cosimulation, vivado_vhdl_cosimulation, 25 | VivadoError, OvenbirdConversionError) 26 | 27 | 28 | @block 29 | def _broken_factory(test_input, test_output, reset, clock): 30 | 31 | @always_seq(clock.posedge, reset=reset) 32 | def broken_identity(): 33 | test_output.next = test_input 34 | 35 | test_output.driven = 'reg' 36 | test_input.read = True 37 | 38 | _broken_factory.vhdl_code = ''' 39 | garbage 40 | ''' 41 | _broken_factory.verilog_code = ''' 42 | garbage 43 | ''' 44 | return broken_identity 45 | 46 | class VivadoCosimulationFunctionTests(ConvertibleCodeTestsMixin): 47 | # Common code for Vivado cosimulation tests. 48 | 49 | check_mocks = False 50 | 51 | def vivado_sim_wrapper(self, sim_cycles, dut_factory, ref_factory, 52 | args, arg_types, **kwargs): 53 | 54 | raise NotImplementedError 55 | 56 | def construct_and_simulate( 57 | self, sim_cycles, dut_factory, ref_factory, args, arg_types, 58 | **kwargs): 59 | 60 | if VIVADO_EXECUTABLE is None: 61 | raise unittest.SkipTest('Vivado executable not in path') 62 | 63 | return self.vivado_sim_wrapper( 64 | sim_cycles, dut_factory, ref_factory, args, arg_types, **kwargs) 65 | 66 | @unittest.expectedFailure 67 | def test_conversion_error_of_user_code(self): 68 | '''Conversion errors of user code should be presented to the user 69 | as a ConversionError. 70 | ''' 71 | # FIXME this test fails because of funny stateful issues in myhdl 72 | # conversion when convert is used more than once 73 | @block 74 | def failure_block(clock, input_signal, output_signal): 75 | 76 | @always(clock.posedge) 77 | def driver1(): 78 | output_signal.next = input_signal 79 | 80 | @always(clock.posedge) 81 | def driver2(): 82 | output_signal.next = input_signal 83 | 84 | return driver1, driver2 85 | 86 | args = {'clock': Signal(False), 87 | 'input_signal': Signal(False), 88 | 'output_signal': Signal(False)} 89 | 90 | arg_types = {'clock': 'clock', 91 | 'input_signal': 'custom', 92 | 'output_signal': 'output'} 93 | 94 | with self.assertRaises(ConversionError) as cm: 95 | self.vivado_sim_wrapper( 96 | 10, failure_block, failure_block, args, arg_types) 97 | 98 | # Make sure the asserion is exactly a ConversionError 99 | self.assertIs(type(cm.exception), ConversionError) 100 | 101 | def test_warnings_include_port_mappings(self): 102 | '''Conversion warnings should include a reference to the correct port 103 | name (not just the internal MyHDL name) 104 | ''' 105 | 106 | @block 107 | def convertible_block( 108 | clock, input_signal, output_signal, test_signal2): 109 | 110 | @always(clock.posedge) 111 | def driver(): 112 | output_signal.next = input_signal 113 | 114 | return driver 115 | 116 | args = {'clock': Signal(False), 117 | 'input_signal': Signal(False), 118 | 'output_signal': Signal(False), 119 | 'test_signal2': Signal(False)} 120 | 121 | arg_types = {'clock': 'clock', 122 | 'input_signal': 'random', 123 | 'output_signal': 'output', 124 | 'test_signal2': 'custom'} 125 | 126 | with warnings.catch_warnings(record=True) as w: 127 | self.vivado_sim_wrapper(10, convertible_block, convertible_block, 128 | args, arg_types) 129 | 130 | self.assertTrue('test_signal2' in str(w[0].message)) 131 | 132 | 133 | arg_types['test_signal2'] = 'output' 134 | 135 | with warnings.catch_warnings(record=True) as w: 136 | self.vivado_sim_wrapper(10, convertible_block, convertible_block, 137 | args, arg_types) 138 | 139 | self.assertTrue('test_signal2' in str(w[0].message)) 140 | 141 | def test_conversion_error_of_veriutils_convertible_top(self): 142 | '''Conversion errors of the veriutils convertible top should be 143 | presented as an OvenbirdConversionError. 144 | ''' 145 | @block 146 | def convertible_block(clock, input_signal, output_signal): 147 | 148 | @always(clock.posedge) 149 | def driver(): 150 | output_signal.next = input_signal 151 | 152 | return driver 153 | 154 | args = {'clock': Signal(False), 155 | 'input_signal': Signal(False), 156 | 'output_signal': Signal(False)} 157 | 158 | arg_types = {'clock': 'clock', 159 | 'input_signal': 'custom', 160 | 'output_signal': 'custom'} 161 | 162 | self.assertRaises(OvenbirdConversionError, self.vivado_sim_wrapper, 163 | 10, convertible_block, convertible_block, 164 | args, arg_types) 165 | 166 | 167 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 168 | 'Vivado executable not in path') 169 | def test_keep_tmp_files(self): 170 | '''It should be possible to keep the temporary files after simulation. 171 | ''' 172 | sim_cycles = 30 173 | 174 | # This method is slightly flaky - it's quite implementation dependent 175 | # and may break if mkdtemp is imported into the namespace of 176 | # cosimulation rather than tempfile, or if multiple calls are 177 | # made to mkdtemp. 178 | import tempfile, sys 179 | orig_mkdtemp = tempfile.mkdtemp 180 | 181 | dirs = [] 182 | def mkdtemp_wrapper(): 183 | new_dir = orig_mkdtemp() 184 | dirs.append(new_dir) 185 | 186 | return new_dir 187 | 188 | try: 189 | tempfile.mkdtemp = mkdtemp_wrapper 190 | 191 | # We also want to drop the helpful output message to keep 192 | # the test clean. 193 | sys.stdout = open(os.devnull, "w") 194 | self.vivado_sim_wrapper( 195 | sim_cycles, self.identity_factory, self.identity_factory, 196 | self.default_args, self.default_arg_types, 197 | keep_temp_files=True) 198 | 199 | self.assertTrue(os.path.exists(dirs[0])) 200 | 201 | finally: 202 | # clean up 203 | tempfile.mkdtemp = orig_mkdtemp 204 | sys.stdout = sys.__stdout__ 205 | try: 206 | shutil.rmtree(dirs[0]) 207 | except OSError: 208 | pass 209 | 210 | def test_missing_vivado_raises(self): 211 | '''Vivado missing from the path should raise an EnvironmentError. 212 | ''' 213 | sim_cycles = 30 214 | 215 | existing_PATH = os.environ['PATH'] 216 | import ovenbird 217 | existing_VIVADO_EXECUTABLE = ovenbird.VIVADO_EXECUTABLE 218 | ovenbird.VIVADO_EXECUTABLE = None 219 | try: 220 | os.environ['PATH'] = '' 221 | self.assertRaisesRegex( 222 | EnvironmentError, 'Vivado executable not in path', 223 | self.vivado_sim_wrapper, sim_cycles, 224 | self.identity_factory, self.identity_factory, 225 | self.default_args, self.default_arg_types) 226 | 227 | finally: 228 | os.environ['PATH'] = existing_PATH 229 | ovenbird.VIVADO_EXECUTABLE = existing_VIVADO_EXECUTABLE 230 | 231 | 232 | class TestVivadoVHDLCosimulationFunction(VivadoCosimulationFunctionTests, 233 | TestCase): 234 | '''There should be an alternative version of the cosimulation function 235 | that runs the device under test through the Vivado VHDL simulator. 236 | ''' 237 | 238 | def vivado_sim_wrapper(self, sim_cycles, dut_factory, ref_factory, 239 | args, arg_types, **kwargs): 240 | 241 | return vivado_vhdl_cosimulation( 242 | sim_cycles, dut_factory, ref_factory, args, arg_types, **kwargs) 243 | 244 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 245 | 'Vivado executable not in path') 246 | def test_invalid_time_unit(self): 247 | '''Passing something that is not a time unit should raise a ValueError 248 | ''' 249 | invalid_time_unit = 'A string' 250 | sim_cycles = 10 251 | 252 | self.assertRaisesRegex( 253 | ValueError, 254 | ('Invalid time unit. Please select from: ' + 255 | ', '.join(AVAILABLE_TIME_UNITS)), 256 | self.vivado_sim_wrapper, sim_cycles, 257 | self.identity_factory, self.identity_factory, 258 | self.default_args, self.default_arg_types, 259 | time_units=invalid_time_unit) 260 | 261 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 262 | 'Vivado executable not in path') 263 | def test_missing_hdl_file_raises(self): 264 | '''An EnvironmentError should be raised for a missing HDL file. 265 | 266 | If the settings stipulate a HDL file should be included, but it 267 | is not there, an EnvironmentError should be raised. 268 | ''' 269 | self.identity_factory.vhdl_dependencies = ['a_missing_file.vhd'] 270 | sim_cycles = 10 271 | self.assertRaisesRegex( 272 | EnvironmentError, 'An expected HDL file is missing', 273 | self.vivado_sim_wrapper, sim_cycles, self.identity_factory, 274 | self.identity_factory, self.default_args, self.default_arg_types) 275 | 276 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 277 | 'Vivado executable not in path') 278 | def test_vivado_VHDL_error_raises(self): 279 | '''Errors with VHDL code in Vivado should raise a RuntimeError. 280 | ''' 281 | sim_cycles = 30 282 | 283 | self.assertRaisesRegex( 284 | VivadoError, 'Error running the Vivado VHDL simulator', 285 | self.vivado_sim_wrapper, sim_cycles, 286 | _broken_factory, self.identity_factory, 287 | self.default_args, self.default_arg_types) 288 | 289 | class TestVivadoVerilogCosimulationFunction(VivadoCosimulationFunctionTests, 290 | TestCase): 291 | '''There should be an alternative version of the cosimulation function 292 | that runs the device under test through the Vivado verilog simulator. 293 | ''' 294 | 295 | def vivado_sim_wrapper(self, sim_cycles, dut_factory, ref_factory, 296 | args, arg_types, **kwargs): 297 | 298 | try: 299 | # We always set keep_temp_files in the call to 300 | # vivado_verilog_cosimulation so remove it if the calling test has 301 | # included it in the kwargs. 302 | del kwargs['keep_temp_files'] 303 | except KeyError: 304 | pass 305 | 306 | return vivado_verilog_cosimulation( 307 | sim_cycles, dut_factory, ref_factory, args, arg_types, keep_temp_files=True, **kwargs) 308 | 309 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 310 | 'Vivado executable not in path') 311 | def test_invalid_time_unit(self): 312 | '''Passing something that is not a time unit should raise a ValueError 313 | ''' 314 | invalid_time_unit = 'A string' 315 | sim_cycles = 10 316 | 317 | self.assertRaisesRegex( 318 | ValueError, 319 | ('Invalid time unit. Please select from: ' + 320 | ', '.join(AVAILABLE_TIME_UNITS)), 321 | self.vivado_sim_wrapper, sim_cycles, 322 | self.identity_factory, self.identity_factory, 323 | self.default_args, self.default_arg_types, 324 | time_units=invalid_time_unit) 325 | 326 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 327 | 'Vivado executable not in path') 328 | def test_missing_hdl_file_raises(self): 329 | '''An EnvironmentError should be raised for a missing HDL file. 330 | 331 | If the settings stipulate a HDL file should be included, but it 332 | is not there, an EnvironmentError should be raised. 333 | ''' 334 | self.identity_factory.verilog_dependencies = ['a_missing_file.v'] 335 | sim_cycles = 10 336 | self.assertRaisesRegex( 337 | EnvironmentError, 'An expected HDL file is missing', 338 | self.vivado_sim_wrapper, sim_cycles, self.identity_factory, 339 | self.identity_factory, self.default_args, self.default_arg_types) 340 | 341 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 342 | 'Vivado executable not in path') 343 | def test_vivado_verilog_error_raises(self): 344 | '''Errors with Verilog code in Vivado should raise a RuntimeError. 345 | ''' 346 | sim_cycles = 30 347 | 348 | self.assertRaisesRegex( 349 | VivadoError, 'Error running the Vivado Verilog simulator', 350 | self.vivado_sim_wrapper, sim_cycles, 351 | _broken_factory, self.identity_factory, 352 | self.default_args, self.default_arg_types) 353 | -------------------------------------------------------------------------------- /examples/dsp48e1/dsp48e1.py: -------------------------------------------------------------------------------- 1 | 2 | '''A module implementing the Xilinx DSP48E1 DSP slice, making use of the 3 | VivadoIP block. 4 | ''' 5 | 6 | from myhdl import ( 7 | always_seq, always_comb, Signal, intbv, enum, ConcatSignal, block) 8 | from veriutils import ( 9 | check_intbv_signal, check_bool_signal, check_reset_signal) 10 | from math import log, floor 11 | 12 | # The two bits we need from ovenbird in this example are VivadoIP and 13 | # PortDirection 14 | from ovenbird import VivadoIP, PortDirection 15 | 16 | # We firstly define a few constants specific to _this_ example, but not 17 | # relevant to understanding VivadoIP. 18 | 19 | # Opmode enumeration 20 | N_DSP48E1_OPMODES = 4 21 | DSP48E1_OPMODE_MULTIPLY = 0 22 | DSP48E1_OPMODE_MULTIPLY_ADD = 1 23 | DSP48E1_OPMODE_MULTIPLY_ACCUMULATE = 2 24 | DSP48E1_OPMODE_MULTIPLY_DECCUMULATE = 3 # deccumulate means to subtract from P 25 | 26 | # Set the values for the internal multiplexers 27 | X_ZEROS, X_M = 0, 1 28 | Y_ZEROS, Y_M = 0, 1 29 | Z_ZEROS, Z_P, Z_C = 0, 2, 3 30 | 31 | class VivadoDSPMacro(VivadoIP): 32 | '''An instance of the 33 | ''' 34 | 35 | def __init__(self): 36 | # In this example, the class takes no construction arguments. 37 | # 38 | # Having arguments is perfectly fine and is a good way to configure 39 | # the IP at run time, should that be desired. 40 | 41 | # The parent class __init__ is called with the following arguments 42 | # defined. These fully describe the IP to be instantiated and 43 | # (hopefully) are as flexible as instantiation inside Vivado. 44 | 45 | # port_mappings gives the mapping from signal names (which most easily 46 | # should be the same as those used by the subsequent MyHDL block, 47 | # though this can be updated when the instance is created) to 48 | # a tuple containing the MyHDL signal type, the signal direction and 49 | # the respective name that the IP block uses for the signal. 50 | # 51 | # The type is set using a valid MyHDL type, as follows. The length 52 | # of the bit vector should agree with that mandated by the created 53 | # IP block. In the case of VHDL, the signal is cast to std_logic or 54 | # std_logic_vector at conversion time. 55 | # 56 | # The port direction is set by the PortDirection enumeration (input 57 | # or output). 58 | # 59 | # The IP block signal name is simply a string and should agree with 60 | # the documentation. 61 | port_mappings = { 62 | 'A': (intbv(0, min=-(2**24-1), max=(2**24)), 63 | PortDirection.input, 'A'), 64 | 'B': (intbv(0, min=-(2**17-1), max=(2**17)), 65 | PortDirection.input, 'B'), 66 | 'C': (intbv(0, min=-(2**47-1), max=(2**47)), 67 | PortDirection.input, 'C'), 68 | 'P': (intbv(0, min=-(2**47-1), max=(2**47)), 69 | PortDirection.output, 'P'), 70 | 'opmode': (intbv(0)[2:], PortDirection.input, 'SEL'), 71 | 'reset': (intbv(0)[1:], PortDirection.input, 'SCLR'), 72 | 'clock': (intbv(0)[1:], PortDirection.input, 'CLK'), 73 | 'clock_enable': (intbv(0)[1:], PortDirection.input, 'CE')} 74 | 75 | # config is the meat of describing the IP block. Each key corresponds 76 | # to a CONFIG option on the macro. 77 | # The options should be described in the IP block documentation, 78 | # but the easiest way of working out the options is to create 79 | # an instance of the IP block in a dummy project (either with a 80 | # block diagram or through the IP Catalog) and then inspect that 81 | # instance through the tcl console. 82 | # 83 | # 84 | # It's possible to see what happens when you twiddle parameters in 85 | # the Vivado IP GUI by observing the tcl console and what CONFIG 86 | # values are changed. 87 | # 88 | # If using a block diagram to inspect the IP block, get a list of the 89 | # valid options with: 90 | # list_property [get_bd_cells ip_instance_name] 91 | # or 92 | # report_property [get_bd_cells ip_instance_name] to get more 93 | # information. 94 | # 95 | # If you're using the IP Catalog to twiddle the ip, then use 96 | # get_ips instead of get_bd_cells. 97 | # 98 | # Since all these values and keys are just strings, they can be 99 | # configured programatically based on arguments to this class 100 | # instance. This allows a huge amount of flexibility in interacting 101 | # with the IP framework in Vivado. 102 | config = {'instruction1': 'A*B', 103 | 'instruction2': 'A*B+C', 104 | 'instruction3': 'P+A*B', 105 | 'instruction4': 'P-A*B', 106 | 'pipeline_options': 'Expert', 107 | 'areg_3': 'false', 108 | 'breg_3': 'false', 109 | 'creg_3': 'false', 110 | 'opreg_3': 'false', 111 | 'has_ce': 'true', 112 | 'has_sclr': 'true', 113 | 'areg_4': 'true', 114 | 'breg_4': 'true', 115 | 'creg_4': 'true', 116 | 'creg_5': 'true', 117 | 'opreg_4': 'true', 118 | 'opreg_5': 'true', 119 | 'mreg_5': 'true', 120 | 'preg_6': 'true', 121 | 'a_width': '25', 122 | 'a_binarywidth': '0', 123 | 'b_width': '18', 124 | 'b_binarywidth': '0', 125 | 'c_width': '48', 126 | 'c_binarywidth': '0', 127 | 'pcin_binarywidth': '0', 128 | 'p_full_width': '48', 129 | 'p_width': '48', 130 | 'p_binarywidth': '0'} 131 | 132 | # Now we set the arguments to pass to the parent class, which fills 133 | # in all the values to create an instantiatable IP block. 134 | 135 | # entity_name is the name of this particular manifestation of the ip 136 | # block. It might be the case that, for example, xbip_dsp48_macro 137 | # is used in a different VivadoIP block, in which case a different 138 | # entity_name should be used (otherwise the resultant HDL code will 139 | # have a name conflict). 140 | entity_name = 'DSP48E1' 141 | 142 | # Ports is the port mapping lookup described above. 143 | ports = port_mappings 144 | 145 | # ip_name is the name of the ip block to be created 146 | ip_name = 'xbip_dsp48_macro' 147 | 148 | # vendor, library and version are the config options to fully 149 | # describe the ip block to Vivado 150 | vendor = 'xilinx.com' 151 | library = 'ip' 152 | version = '3.0' 153 | 154 | VivadoIP.__init__( 155 | self, entity_name, port_mappings, ip_name, vendor, library, 156 | version, config) 157 | 158 | # Since the VivadoIP block can be configurable, we need to create a 159 | # specific manifestation - passing arguments as necessary (which in this case 160 | # is not applicable as VivadoDSPMacro has no arguments defined). 161 | # 162 | # Note that this is not an instance of the IP block in the HDL sense, but 163 | # instead a fully described IP block from which instances can be created. 164 | dsp_macro = VivadoDSPMacro() 165 | 166 | @block 167 | def DSP48E1(A, B, C, P, opmode, clock_enable, reset, clock): 168 | '''A MyHDL DSP48E1 block, using the encrypted IP when it is converted. 169 | ''' 170 | 171 | # In this case, we've implemented something pretty close to how the 172 | # DSP block actually works internally. This is not a requirement, and 173 | # the Python code does not need to be convertible. 174 | 175 | # Check the inputs 176 | check_intbv_signal(A, 'A', 25, signed=True) 177 | check_intbv_signal(B, 'B', 18, signed=True) 178 | check_intbv_signal(C, 'C', 48, signed=True) 179 | check_intbv_signal(P, 'P', 48, signed=True) 180 | check_intbv_signal(opmode, 'opmode', val_range=(0, N_DSP48E1_OPMODES)) 181 | check_bool_signal(clock_enable, 'clock_enable') 182 | check_bool_signal(clock, 'clock') 183 | check_reset_signal(reset, 'reset', active=1, isasync=False) 184 | 185 | out_len = 48 186 | max_out = 2**(out_len - 1) - 1 # one bit for the sign 187 | min_out = -max_out 188 | 189 | A_register = Signal(intbv(val=0, min=A.min, max=A.max)) 190 | B_register = Signal(intbv(val=0, min=B.min, max=B.max)) 191 | 192 | M_register = Signal(intbv(val=0, min=min_out, max=max_out)) 193 | C_register1 = Signal(intbv(val=0, min=min_out, max=max_out)) 194 | C_register2 = Signal(intbv(val=0, min=min_out, max=max_out)) 195 | 196 | P_register = Signal(intbv(val=0, min=min_out, max=max_out)) 197 | 198 | # Set up the opmode registers. 199 | # Currently two input side registers. 200 | opmode_register1 = Signal(intbv(val=0, min=0, max=N_DSP48E1_OPMODES)) 201 | opmode_register2 = Signal(intbv(val=0, min=0, max=N_DSP48E1_OPMODES)) 202 | 203 | opmode_X = Signal(intbv(0)[2:]) 204 | opmode_Y = Signal(intbv(0)[2:]) 205 | opmode_Z = Signal(intbv(0)[3:]) 206 | 207 | X_output = intbv(val=X_ZEROS, min=min_out, max=max_out) 208 | Y_output = intbv(val=Y_ZEROS, min=min_out, max=max_out) 209 | Z_output = intbv(val=Z_ZEROS, min=min_out, max=max_out) 210 | 211 | ALUMODE_ACCUMULATE = 0 212 | ALUMODE_DECCUMULATE = 3 213 | alumode = Signal(intbv(0)[4:]) 214 | 215 | @always_seq(clock.posedge, reset=reset) 216 | def opmode_pipeline(): 217 | if clock_enable: # pragma: no branch 218 | opmode_register1.next = opmode 219 | opmode_register2.next = opmode_register1 220 | 221 | @always_comb 222 | def set_opmode_X(): 223 | if opmode_register2 == DSP48E1_OPMODE_MULTIPLY: 224 | opmode_X.next = X_M 225 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_ADD: 226 | opmode_X.next = X_M 227 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_ACCUMULATE: 228 | opmode_X.next = X_M 229 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_DECCUMULATE: 230 | opmode_X.next = X_M 231 | else: 232 | if __debug__: 233 | raise ValueError('Unsupported Y opmode: %d', opmode_Y) 234 | pass 235 | 236 | @always_comb 237 | def set_opmode_Y(): 238 | if opmode_register2 == DSP48E1_OPMODE_MULTIPLY: 239 | opmode_Y.next = Y_M 240 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_ADD: 241 | opmode_Y.next = Y_M 242 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_ACCUMULATE: 243 | opmode_Y.next = Y_M 244 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_DECCUMULATE: 245 | opmode_Y.next = Y_M 246 | else: 247 | if __debug__: 248 | raise ValueError('Unsupported Y opmode: %d', opmode_Y) 249 | pass 250 | 251 | @always_comb 252 | def set_opmode_Z(): 253 | if opmode_register2 == DSP48E1_OPMODE_MULTIPLY: 254 | opmode_Z.next = Z_ZEROS 255 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_ADD: 256 | opmode_Z.next = Z_C 257 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_ACCUMULATE: 258 | opmode_Z.next = Z_P 259 | elif opmode_register2 == DSP48E1_OPMODE_MULTIPLY_DECCUMULATE: 260 | opmode_Z.next = Z_P 261 | else: 262 | if __debug__: 263 | raise ValueError('Unsupported Y opmode: %d', opmode_Y) 264 | pass 265 | 266 | @always_comb 267 | def set_ALUMODE(): 268 | if opmode_register2 == DSP48E1_OPMODE_MULTIPLY_DECCUMULATE: 269 | alumode.next = ALUMODE_DECCUMULATE 270 | else: 271 | # default alumode 272 | alumode.next = ALUMODE_ACCUMULATE 273 | 274 | @always_comb 275 | def set_P(): 276 | P.next = P_register 277 | 278 | @always_seq(clock.posedge, reset=reset) 279 | def dsp48e1_block(): 280 | 281 | if clock_enable: # pragma: no branch 282 | # The partial products are combined in this implementation. 283 | # No problems with this as all we are doing is multiply/add or 284 | # multiply/accumulate. 285 | if opmode_X == X_M: 286 | X_output[:] = M_register 287 | else: 288 | if __debug__: 289 | raise ValueError('Unsupported X opmode: %d', opmode_X) 290 | pass 291 | 292 | if opmode_Y == Y_M: 293 | Y_output[:] = 0 # The full product is handled by X 294 | else: 295 | if __debug__: 296 | raise ValueError('Unsupported Y opmode: %d', opmode_Y) 297 | pass 298 | 299 | if opmode_Z == Z_ZEROS: 300 | Z_output[:] = 0 301 | elif opmode_Z == Z_C: 302 | Z_output[:] = C_register2 303 | elif opmode_Z == Z_P: 304 | Z_output[:] = P_register 305 | else: 306 | if __debug__: 307 | raise ValueError('Unsupported Z opmode: %d', opmode_Z) 308 | pass 309 | 310 | M_register.next = A_register * B_register 311 | 312 | A_register.next = A 313 | B_register.next = B 314 | 315 | C_register1.next = C 316 | C_register2.next = C_register1 317 | 318 | if alumode == ALUMODE_ACCUMULATE: 319 | P_register.next = Z_output + (X_output + Y_output) 320 | 321 | elif alumode == ALUMODE_DECCUMULATE: 322 | P_register.next = Z_output - (X_output + Y_output) 323 | 324 | A.read = True 325 | B.read = True 326 | C.read = True 327 | P.driven = 'wire' 328 | opmode.read = True 329 | clock_enable.read = True 330 | clock.read = True 331 | reset.read = True 332 | 333 | # The instance is created here. 334 | # get_verilog_instance and get_vhdl_instance return an instance of 335 | # a child class of string. This means it can be assigned directly to 336 | # the MyHDL expected verilog_code and vhdl_code, fitting trivially 337 | # into MyHDL's conversion tools. 338 | # The fact that a child class is used means the details of the IP 339 | # can be looked up from the v*_code attribute, wherever it is in the 340 | # hierarchy. This is how the cosimulation facility works. 341 | DSP48E1.verilog_code = dsp_macro.get_verilog_instance() 342 | DSP48E1.vhdl_code = dsp_macro.get_vhdl_instance() 343 | 344 | return (dsp48e1_block, opmode_pipeline, 345 | set_opmode_X, set_opmode_Y, set_opmode_Z, set_P, set_ALUMODE) 346 | 347 | -------------------------------------------------------------------------------- /ovenbird/vivado_ip.py: -------------------------------------------------------------------------------- 1 | 2 | from myhdl import * 3 | from enum import Enum 4 | 5 | import string 6 | 7 | import os 8 | 9 | import inspect 10 | 11 | __all__ = ['VivadoIP', 'PortDirection'] 12 | 13 | _vhdl_wrapper_template = string.Template(''' 14 | library IEEE; 15 | use IEEE.std_logic_1164.all; 16 | use IEEE.numeric_std.all; 17 | 18 | entity ${entity_name} is 19 | port ( 20 | ${entity_ports} 21 | ); 22 | end entity ${entity_name}; 23 | 24 | 25 | architecture MyHDL of ${entity_name} is 26 | 27 | component ${module_name} 28 | port ( 29 | ${ip_component_ports} 30 | ); 31 | end component ${module_name}; 32 | 33 | ${wrapped_signal_instantiations}; 34 | 35 | begin 36 | 37 | ${wrapped_signal_assignments}; 38 | 39 | ip_instance : ${module_name} 40 | port map ( 41 | ${ip_port_mappings} 42 | ); 43 | 44 | end architecture MyHDL; 45 | ''') 46 | 47 | _vhdl_instantiation_template = string.Template(''' 48 | ${instance_name}: entity work.${entity_name}(MyHDL) 49 | port map ( 50 | ${port_mappings} 51 | ); 52 | ''') 53 | 54 | _verilog_wrapper_template = string.Template(''' 55 | module entity_name ( 56 | ${entity_ports} 57 | ); 58 | ${module_name} ip_instance ( 59 | ${ip_port_mappings} 60 | ); 61 | 62 | endmodule 63 | ''') 64 | 65 | _verilog_instantiation_template = string.Template(''' 66 | ${module_name} ${instance_name} ( 67 | ${port_mappings} 68 | ); 69 | ''') 70 | 71 | class PortDirection(Enum): 72 | input = 1 73 | output = 2 74 | 75 | class VivadoIP(object): 76 | 77 | def __init__(self, entity_name, ports, ip_name, vendor, library, version, 78 | config): 79 | ''' 80 | Presents a piece of Vivado IP in a form that can be used to generate 81 | suitable HDL code, including generating a wrapper to perform the 82 | instantiation. 83 | 84 | ``entity_name`` is the name of the entity that can be instantiated 85 | in the main hdl. 86 | 87 | ``ports`` is a dictionary of ports. The keys are the port names and 88 | the values are tuples of ``(type, direction, ip_mapping)``. 89 | ``type`` should be an object akin to a myhdl type that returns a bit 90 | length with ``len()`` and where relevant has a min and max property. 91 | ``ip_mapping`` is the name of the corresponding port in the ip block. 92 | This is of prime importance in the construction of the HDL IP wrapper. 93 | Essentially when the wrapper is a new Verilog or VHDL block with 94 | an interface given by the keys of the ``ports`` dictionary. It is 95 | possible to use different port names from the calling MyHDL when 96 | the ``V*_instance`` is created (see the respective function for 97 | more information). 98 | 99 | ``ip_name`` is the name of the ip block that is created and wrapped. 100 | 101 | ``vendor`` is the name of the vendor supplying the ip block as 102 | used to define the location of the ip block - e.g. 'xilinx.com'. 103 | 104 | ``library`` is a string of the ip library name. 105 | 106 | ``version`` a string containing the ip version number e.g. '3.0'. 107 | 108 | ``config`` is a dictionary that that defines the config options to set 109 | on the entity at instantiation. They are not checked for validity at 110 | any point, so they should correspond to a valid IP config options. The 111 | full set of IP properties (which is a superset of the config options) 112 | can be queried with tcl in Vivado with ``list_property`` (to simply 113 | list) or ``report_property`` (for more detail). The config options are 114 | those prefixed with ``CONFIG.`` (which is not needed in the config 115 | arguments). The config values should all be strings. 116 | ''' 117 | 118 | self.entity_name = entity_name 119 | self.ip_name = ip_name 120 | self.module_name = ip_name + '_' + entity_name 121 | self.ports = ports 122 | 123 | self.vendor = vendor 124 | self.library = library 125 | self.version = version 126 | 127 | self.config = config 128 | 129 | self._vhdl_instance_idx = 0 130 | self._verilog_instance_idx = 0 131 | 132 | @property 133 | def tcl_string(self): 134 | '''The tcl string that will instantiate the IP and set any config 135 | options and output products. 136 | 137 | The name of the created object is set by concatenating 138 | the original entity name to the ip_name separated by an underscore. 139 | That is: ``entity_name + '_' + ip_name``. 140 | ''' 141 | tcl_creation_string = ( 142 | 'create_ip -name %s -vendor %s -library %s ' 143 | '-version %s -module_name %s\n' % ( 144 | self.ip_name, self.vendor, self.library, self.version, 145 | self.module_name)) 146 | 147 | config_string = ' '.join( 148 | 'CONFIG.%s {%s}' % (option, value) for 149 | option, value in self.config.items()) 150 | 151 | tcl_config_string = ( 152 | 'set_property -dict [list %s] [get_ips %s]\n' % 153 | (config_string, self.module_name)) 154 | 155 | tcl_output_products_string = ( 156 | 'generate_target all [get_ips %s]\n' % (self.module_name,)) 157 | 158 | tcl_complete_string = ( 159 | tcl_creation_string + tcl_config_string + 160 | tcl_output_products_string) 161 | 162 | return tcl_complete_string 163 | 164 | def get_vhdl_instance(self, **port_mappings): 165 | '''Create and return a ``HDLCodeWithIP`` object for instantiating 166 | a unique instance in VHDL. The ``HDLCodeWthIP`` block looks and acts 167 | like a string, but has access to the class instance that generated 168 | it (i.e. this object). 169 | 170 | The string returned is designed to be a suitable ``vhdl_code`` 171 | string that instantiates an instance of the IP wrapper HDL block in 172 | converted code. The actual IP wrapper needs to be created separately 173 | with a call to ``write_vhdl_wrapper``. 174 | 175 | ``port_mappings`` is an optional set of keyword arguments that maps 176 | port names of the IP wrapper to MyHDL signals. For example, if there 177 | is a port mapping ``output_port=myhdl_output_signal``, this will 178 | create the mapping in the returned string as 179 | ``'output_port=>${myhdl_output_signal}'``. Each key in 180 | ``port_mappings`` should correspond to a port defined in ``ports`` 181 | during initial instantiation, otherwise a ``ValueError`` will be 182 | raised. Note that ``${myhdl_output_signal}`` is actually a template 183 | string that is replaced at conversion time. 184 | 185 | Each signal passed in through ``port_mappings`` has its ``driven`` 186 | or ``read`` parameter set appropriately, meaning this is unncessary 187 | on the user side. 188 | 189 | ``port_mappings`` is optional; if ports are missing then the MyHDL 190 | name is set to be the same as what was set by ``ports`` during the 191 | initial instantiation of this object (i.e. if a hypothetical port 192 | ``A`` was not set by ``port_mappings``, the return string would 193 | contain ``'A=>${A}'``). 194 | 195 | In addition to the advantage of name changing and specifying 196 | read/driven automatically, explicitly setting ``port_mappings`` 197 | also allows for the port to be checked for type and length 198 | consisistency, avoiding potential problems being pushed to the VHDL 199 | tool. 200 | 201 | The name of the created instance is given by 202 | ``entity_name + '_' + str(N)``, where ``entity_name`` is the name 203 | set during instantiation of this object and ``N`` is a unique number 204 | giving the instance number. Each call to this method will generate a 205 | new instance (with ``N`` beginning at 0 and increasing). 206 | ''' 207 | 208 | instance_name = self.entity_name + '_' + str(self._vhdl_instance_idx) 209 | self._vhdl_instance_idx += 1 210 | 211 | port_mapping_strings = [] 212 | for port_name in self.ports: 213 | 214 | if port_name in port_mappings: 215 | instance_port_name = port_mappings[port_name] 216 | else: 217 | instance_port_name = port_name 218 | 219 | instance_port_string = ( 220 | '${' + instance_port_name.replace('.', '_') + '}') 221 | 222 | port_mapping_strings.append( 223 | '%s=>%s' % ( 224 | port_name.replace('.', '_'), instance_port_string)) 225 | 226 | all_port_mappings = ',\n '.join(port_mapping_strings) 227 | 228 | vhdl_instantiation_string = _vhdl_instantiation_template.substitute( 229 | instance_name=instance_name, 230 | entity_name=self.entity_name, 231 | port_mappings=all_port_mappings) 232 | 233 | return HDLCodeWithIP(vhdl_instantiation_string, self) 234 | 235 | def write_vhdl_wrapper(self, output_directory): 236 | '''Generates the vhdl file that wraps the IP block, wrapping the 237 | ports to convert between the myhdl types and names and the Vivado 238 | expected types and names. 239 | 240 | The file is written to ``output_directory``. 241 | 242 | Returns the filename that is written. 243 | ''' 244 | 245 | if not os.path.isdir(output_directory): 246 | raise IOError('%s is not a directory.' % (output_directory,)) 247 | 248 | entity_port_strings = [] 249 | wrapped_signal_instantiation_strings = [] 250 | wrapped_signal_assignment_strings = [] 251 | ip_component_port_strings = [] 252 | ip_port_mapping_strings = [] 253 | 254 | for port_name in self.ports: 255 | port_type = self.ports[port_name][0] 256 | port_direction = self.ports[port_name][1] 257 | ip_port_mapping = self.ports[port_name][2] 258 | 259 | port_length = len(port_type) 260 | if port_length > 1: 261 | ip_type_string = 'std_logic_vector' 262 | 263 | if port_type.min < 0: 264 | entity_type_string = 'signed' 265 | else: 266 | entity_type_string = 'unsigned' 267 | 268 | size_string = "(%d downto 0)" % (port_length - 1) 269 | else: 270 | port_is_signed = False 271 | entity_type_string = 'std_logic' 272 | ip_type_string = 'std_logic' 273 | size_string = '' 274 | 275 | if port_direction == PortDirection.input: 276 | direction_string = 'in' 277 | 278 | wrapped_signal_assignment_strings.append( 279 | 'wrapped_%s <= %s(%s)' % 280 | (port_name, ip_type_string, port_name)) 281 | 282 | elif port_direction == PortDirection.output: 283 | direction_string = "out" 284 | 285 | wrapped_signal_assignment_strings.append( 286 | '%s <= %s(wrapped_%s)' % 287 | (port_name, entity_type_string, port_name)) 288 | else: 289 | raise ValueError('Unsupported direction') 290 | 291 | entity_port_strings.append( 292 | '%s: %s %s%s' % (port_name, direction_string, 293 | entity_type_string, size_string)) 294 | 295 | wrapped_signal_instantiation_strings.append( 296 | 'signal wrapped_%s: %s%s' % ( 297 | port_name, ip_type_string, size_string)) 298 | 299 | ip_component_port_strings.append( 300 | '%s: %s %s%s' % (ip_port_mapping, direction_string, 301 | ip_type_string, size_string)) 302 | 303 | ip_port_mapping_strings.append( 304 | '%s => wrapped_%s' % (ip_port_mapping, port_name)) 305 | 306 | 307 | entity_ports = (';\n' + ' ' * 12).join(entity_port_strings) 308 | ip_component_ports = ( 309 | ';\n' + ' ' * 16).join(ip_component_port_strings) 310 | wrapped_signal_instantiations = ( 311 | ';\n' + ' ' * 4).join(wrapped_signal_instantiation_strings) 312 | 313 | wrapped_signal_assigments = ( 314 | ';\n' + ' ' * 4).join(wrapped_signal_assignment_strings) 315 | 316 | ip_port_mappings = (',\n' + ' ' * 16).join(ip_port_mapping_strings) 317 | 318 | vhdl_wrapper_string = _vhdl_wrapper_template.substitute( 319 | entity_ports=entity_ports, 320 | ip_component_ports=ip_component_ports, 321 | wrapped_signal_instantiations=wrapped_signal_instantiations, 322 | wrapped_signal_assignments=wrapped_signal_assigments, 323 | ip_port_mappings=ip_port_mappings, 324 | module_name=self.module_name, 325 | entity_name=self.entity_name) 326 | 327 | wrapper_filename = os.path.join( 328 | output_directory, self.entity_name + '.vhd') 329 | 330 | if os.path.exists(wrapper_filename): 331 | raise IOError('File %s already exists - ' 332 | 'refusing to overwrite it.') 333 | 334 | with open(wrapper_filename, 'w') as f: 335 | f.write(vhdl_wrapper_string) 336 | 337 | return wrapper_filename 338 | 339 | def get_verilog_instance(self, **port_mappings): 340 | '''Create and return a ``HDLCodeWithIP`` object for instantiating 341 | a unique instance in Verilog. The ``HDLCodeWthIP`` block looks and acts 342 | like a string, but has access to the class instance that generated 343 | it (i.e. this object). 344 | 345 | The string returned is designed to be a suitable ``verilog_code`` 346 | string that instantiates an instance of the IP wrapper HDL block in 347 | converted code. Unlike VHDL, it is unncessary to write a Verilog 348 | wrapper. 349 | 350 | ``port_mappings`` is an optional set of keyword arguments that maps 351 | port names of the IP wrapper to MyHDL signals. For example, if there 352 | is a port mapping ``output_port=myhdl_output_signal``, this will 353 | create the mapping in the returned string as 354 | ``'.output_port(${myhdl_output_signal})'``. Each key in 355 | ``port_mappings`` should correspond to a port defined in ``ports`` 356 | during initial instantiation, otherwise a ``ValueError`` will be 357 | raised. Note that ``${myhdl_output_signal}`` is actually a template 358 | string that is replaced at conversion time. 359 | 360 | Each signal passed in through ``port_mappings`` has its ``driven`` 361 | or ``read`` parameter set appropriately, meaning this is unncessary 362 | on the user side. 363 | 364 | ``port_mappings`` is optional; if ports are missing then the MyHDL 365 | name is set to be the same as what was set by ``ports`` during the 366 | initial instantiation of this object (i.e. if a hypothetical port 367 | ``A`` was not set by ``port_mappings``, the return string would 368 | contain ``'.A(${A})'``). 369 | 370 | In addition to the advantage of name changing and specifying 371 | read/driven automatically, explicitly setting ``port_mappings`` 372 | also allows for the port to be checked for type and length 373 | consisistency, avoiding potential problems being pushed to the VHDL 374 | tool. 375 | 376 | The name of the created instance is given by 377 | ``entity_name + '_' + str(N)``, where ``entity_name`` is the name 378 | set during instantiation of this object and ``N`` is a unique number 379 | giving the instance number. Each call to this method will generate a 380 | new instance (with ``N`` beginning at 0 and increasing). 381 | ''' 382 | '''Create and return a ``HDLCodeWithIP`` object for instantiating 383 | a unique instance in Verilog. The ``HDLCodeWthIP`` block looks and acts 384 | like a string, but has access to the class instance that generated 385 | (i.e. this object). 386 | 387 | The name of the created instance is given by 388 | ``self.entity_name + '_' + str(N)``, 389 | where ``N`` is a unique number giving the instance number. Each call 390 | to this method will generate a new instance (with ``N`` beginning 391 | at 0 and increasing). 392 | 393 | The port mappings are set to be a mapping from each port name on 394 | this object to a template variable of the same name. 395 | 396 | So, for example, if there is a port named `'A'`, then the mapping 397 | `'.A(${A})'` will be created. MyHDL will then replace `$A` with a 398 | suitable Verilog signal when the top level VHDL file is created. 399 | 400 | This requires that the MyHDL names are the same as the names set 401 | in this IP block. 402 | ''' 403 | instance_name = self.entity_name + '_' + str( 404 | self._verilog_instance_idx) 405 | 406 | self._verilog_instance_idx += 1 407 | 408 | port_mapping_strings = [] 409 | for port_name in self.ports: 410 | if port_name in port_mappings: 411 | instance_port_name = port_mappings[port_name] 412 | else: 413 | instance_port_name = port_name 414 | 415 | ip_port_mapping = self.ports[port_name][2] 416 | instance_port_string = '${' + instance_port_name + '}' 417 | 418 | port_mapping_strings.append( 419 | '.%s(%s)' % (ip_port_mapping, instance_port_string)) 420 | 421 | all_port_mappings = ',\n '.join(port_mapping_strings) 422 | 423 | verilog_instantiation_string = ( 424 | _verilog_instantiation_template.substitute( 425 | instance_name=instance_name, 426 | module_name=self.module_name, 427 | port_mappings=all_port_mappings)) 428 | 429 | return HDLCodeWithIP(verilog_instantiation_string, self) 430 | 431 | 432 | class HDLCodeWithIP(str): 433 | 434 | def __new__(cls, hdl_code, ip_instance): 435 | 436 | if not isinstance(ip_instance, VivadoIP): 437 | raise ValueError('ip_instance should be an instance of VivadoIP') 438 | 439 | new_obj = str.__new__(cls, hdl_code) 440 | new_obj.ip_instance = ip_instance 441 | return new_obj 442 | 443 | def __init__(self, hdl_code, ip_instance): 444 | '''Creates a code block from an IP block that can be 445 | assigned to the `.vhdl_code` or `.verilog_code` attributes of a 446 | myhdl block. 447 | 448 | This means the ip_instance attribute can be later extracted 449 | from the block.' 450 | ''' 451 | pass 452 | 453 | 454 | -------------------------------------------------------------------------------- /examples/dsp48e1/test_dsp48e1.py: -------------------------------------------------------------------------------- 1 | 2 | import unittest 3 | from tests.base_hdl_test import HDLTestCase, get_signed_intbv_rand_signal 4 | from .utils import weighted_random_reset_source 5 | from myhdl import (intbv, enum, Signal, ResetSignal, instance, block, 6 | delay, always, always_seq, Simulation, StopSimulation) 7 | 8 | from random import randrange 9 | import random 10 | 11 | from collections import deque 12 | 13 | from .dsp48e1 import ( 14 | DSP48E1, DSP48E1_OPMODE_MULTIPLY, DSP48E1_OPMODE_MULTIPLY_ADD, 15 | DSP48E1_OPMODE_MULTIPLY_ACCUMULATE, 16 | DSP48E1_OPMODE_MULTIPLY_DECCUMULATE) 17 | 18 | from veriutils import ( 19 | myhdl_cosimulation, copy_signal) 20 | 21 | from ovenbird import ( 22 | vivado_vhdl_cosimulation, vivado_verilog_cosimulation, VIVADO_EXECUTABLE) 23 | 24 | PERIOD = 10 25 | 26 | class DSP48E1TestCase(HDLTestCase): 27 | 28 | def setUp(self): 29 | 30 | self.len_A = 25 31 | self.len_B = 18 32 | self.len_C = 48 33 | self.len_P = 48 34 | 35 | self.clock = Signal(bool(1)) 36 | self.clock_enable = Signal(bool(1)) 37 | self.reset = ResetSignal(bool(0), active=1, isasync=False) 38 | 39 | self.A, self.a_min, self.a_max = ( 40 | get_signed_intbv_rand_signal(self.len_A)) 41 | self.B, self.b_min, self.b_max = ( 42 | get_signed_intbv_rand_signal(self.len_B)) 43 | 44 | initial_C, _c_min, _c_max = ( 45 | get_signed_intbv_rand_signal(self.len_C)) 46 | 47 | # Reduce the range of C, but not enough to reduce its bitwidth 48 | self.c_min = int(_c_min * 0.6) 49 | self.c_max = int(_c_max * 0.6) 50 | self.C = Signal(intbv(0, min=self.c_min, max=self.c_max)) 51 | self.C.val[:] = int(initial_C.val * 0.6) 52 | 53 | self.P, self.p_min, self.p_max = ( 54 | get_signed_intbv_rand_signal(self.len_P)) 55 | 56 | # Tweak the initialisations 57 | self.P.val[:] = 0 58 | 59 | self.operations = { 60 | 'multiply': DSP48E1_OPMODE_MULTIPLY, 61 | 'multiply_add': DSP48E1_OPMODE_MULTIPLY_ADD, 62 | 'multiply_accumulate': DSP48E1_OPMODE_MULTIPLY_ACCUMULATE, 63 | 'multiply_deccumulate': DSP48E1_OPMODE_MULTIPLY_DECCUMULATE 64 | } 65 | 66 | self.opmode = Signal(intbv(0, min=0, max=len(self.operations))) 67 | 68 | self.default_args = { 69 | 'A': self.A, 'B': self.B, 'C': self.C, 'P': self.P, 70 | 'opmode': self.opmode, 'reset': self.reset, 'clock': self.clock, 71 | 'clock_enable': self.clock_enable} 72 | 73 | self.default_arg_types = { 74 | 'A': 'random', 'B': 'random', 'C': 'random', 'P': 'output', 75 | 'opmode': 'custom', 'reset': 'init_reset', 'clock': 'clock', 76 | 'clock_enable': 'custom'} 77 | 78 | # Should work, no probs 79 | test = DSP48E1(**self.default_args) 80 | 81 | self.pipeline_registers = 3 82 | 83 | class TestDSP48E1Interface(DSP48E1TestCase): 84 | '''The DSP48E1 should have a well defined interface, with careful 85 | checking of the parameters. 86 | ''' 87 | 88 | def test_A_port_checked(self): 89 | '''The A port should be an 25 bit signed intbv. 90 | 91 | Anything else should raise a ValueError. 92 | ''' 93 | self.do_port_check_intbv_test(DSP48E1, 'A', 25, signed=True) 94 | 95 | def test_B_port_checked(self): 96 | '''The B port should be an 18 bit signed intbv. 97 | 98 | Anything else should raise a ValueError. 99 | ''' 100 | self.do_port_check_intbv_test(DSP48E1, 'B', 18, signed=True) 101 | 102 | def test_C_port_checked(self): 103 | '''The C port should be an 48 bit signed intbv. 104 | 105 | Anything else should raise a ValueError. 106 | ''' 107 | self.do_port_check_intbv_test(DSP48E1, 'C', 48, signed=True) 108 | 109 | def test_P_port_checked(self): 110 | '''The P port should be an 48 bit signed intbv. 111 | 112 | Anything else should raise a ValueError. 113 | ''' 114 | self.do_port_check_intbv_test(DSP48E1, 'P', 48, signed=True) 115 | 116 | def test_opmode_port_checked(self): 117 | '''The opmode port should be an unsigned intbv. 118 | 119 | The min and max values of the opmode port should be determined by 120 | the number of implemented opmodes. 121 | ''' 122 | opmode_range = (self.opmode.min, self.opmode.max) 123 | self.do_port_check_intbv_test(DSP48E1, 'opmode', 124 | val_range=opmode_range) 125 | 126 | def test_clock_port_checked(self): 127 | '''The clock port should be a boolean signal. 128 | 129 | Anything else should raise a ValueError. 130 | ''' 131 | self.do_port_check_bool_test(DSP48E1, 'clock') 132 | 133 | def test_clock_enable_port_checked(self): 134 | '''The clock enable port should be a boolean signal. 135 | 136 | Anything else should raise a ValueError. 137 | ''' 138 | self.do_port_check_bool_test(DSP48E1, 'clock_enable') 139 | 140 | def test_reset_port_checked(self): 141 | '''The reset port should be a boolean signal. 142 | 143 | Anything else should raise a ValueError. 144 | ''' 145 | self.do_port_check_reset_test(DSP48E1, 'reset', active=1, isasync=False) 146 | 147 | class TestDSP48E1Simulation(DSP48E1TestCase): 148 | '''The DSP48E1 slice should implement various bits of functionality that 149 | should be verifiable through simulation. 150 | ''' 151 | 152 | def cosimulate(self, sim_cycles, dut_factory, ref_factory, args, 153 | arg_types, **kwargs): 154 | 155 | return myhdl_cosimulation(sim_cycles, dut_factory, ref_factory, 156 | args, arg_types, **kwargs) 157 | 158 | def test_basic_multiply(self): 159 | '''The basic multiply with default Z should be the product of A and B. 160 | ''' 161 | 162 | reset = self.default_args['reset'] 163 | clock = self.default_args['clock'] 164 | @block 165 | def set_opmode(): 166 | @always_seq(clock.posedge, reset=reset) 167 | def _set_opmode(): 168 | self.opmode.next = self.operations['multiply'] 169 | 170 | return _set_opmode 171 | 172 | @block 173 | def ref(**kwargs): 174 | 175 | P = kwargs['P'] 176 | A = kwargs['A'] 177 | B = kwargs['B'] 178 | clock = kwargs['clock'] 179 | reset = kwargs['reset'] 180 | 181 | @always_seq(clock.posedge, reset=reset) 182 | def test_basic_multiply(): 183 | P.next = A * B 184 | 185 | return test_basic_multiply 186 | 187 | args = self.default_args.copy() 188 | arg_types = self.default_arg_types.copy() 189 | 190 | cycles = 20 191 | dut_outputs, ref_outputs = self.cosimulate( 192 | cycles, DSP48E1, ref, args, arg_types, 193 | custom_sources=[(set_opmode, (), {})]) 194 | 195 | # There are pipeline_registers cycles latency on the output. 196 | # The reference above has only 1 cycle latency, so we need to offset 197 | # the results by pipeline_registers - 1 cycles. 198 | self.assertEqual(dut_outputs['P'][self.pipeline_registers - 1:], 199 | ref_outputs['P'][:-(self.pipeline_registers - 1)]) 200 | 201 | def test_multiply_add(self): 202 | '''There should be a multiply-add mode, giving C + A * B 203 | ''' 204 | reset = self.default_args['reset'] 205 | clock = self.default_args['clock'] 206 | @block 207 | def set_opmode(): 208 | @always_seq(clock.posedge, reset=reset) 209 | def _set_opmode(): 210 | self.opmode.next = self.operations['multiply_add'] 211 | 212 | return _set_opmode 213 | 214 | @block 215 | def ref(**kwargs): 216 | 217 | P = kwargs['P'] 218 | A = kwargs['A'] 219 | B = kwargs['B'] 220 | C = kwargs['C'] 221 | clock = kwargs['clock'] 222 | reset = kwargs['reset'] 223 | 224 | @always_seq(clock.posedge, reset=reset) 225 | def test_basic_multiply(): 226 | P.next = A * B + C 227 | 228 | return test_basic_multiply 229 | 230 | args = self.default_args.copy() 231 | arg_types = self.default_arg_types.copy() 232 | 233 | cycles = 20 234 | dut_outputs, ref_outputs = self.cosimulate( 235 | cycles, DSP48E1, ref, args, arg_types, 236 | custom_sources=[(set_opmode, (), {})]) 237 | 238 | # There are pipeline_registers cycles latency on the output. 239 | # The reference above has only 1 cycle latency, so we need to offset 240 | # the results by pipeline_registers - 1 cycles. 241 | 242 | self.assertEqual(dut_outputs['P'][self.pipeline_registers - 1:], 243 | ref_outputs['P'][:-(self.pipeline_registers - 1)]) 244 | 245 | 246 | def test_multiply_accumulate(self): 247 | '''There should be a multiply-accumulate mode, giving P + A * B. 248 | 249 | P is defined to be the output, which is not pipelined. That is, 250 | the output should always be incremented by A*B as long as the 251 | multiply-accumulate is ongoing. 252 | ''' 253 | reset = self.default_args['reset'] 254 | clock = self.default_args['clock'] 255 | 256 | @block 257 | def set_opmode(): 258 | @always_seq(clock.posedge, reset=reset) 259 | def _set_opmode(): 260 | self.opmode.next = self.operations['multiply_accumulate'] 261 | 262 | return _set_opmode 263 | 264 | @block 265 | def ref(**kwargs): 266 | 267 | P = kwargs['P'] 268 | A = kwargs['A'] 269 | B = kwargs['B'] 270 | clock = kwargs['clock'] 271 | reset = kwargs['reset'] 272 | 273 | @always_seq(clock.posedge, reset=reset) 274 | def test_basic_multiply(): 275 | P.next = P + A * B 276 | 277 | return test_basic_multiply 278 | 279 | args = self.default_args.copy() 280 | arg_types = self.default_arg_types.copy() 281 | 282 | # Don't run too many cycles or you'll get an overflow! 283 | cycles = 20 284 | dut_outputs, ref_outputs = self.cosimulate( 285 | cycles, DSP48E1, ref, args, arg_types, 286 | custom_sources=[(set_opmode, (), {})]) 287 | 288 | # There are pipeline_registers cycles latency on the output. 289 | # The reference above has only 1 cycle latency, so we need to offset 290 | # the results by pipeline_registers - 1 cycles. 291 | self.assertEqual(dut_outputs['P'][self.pipeline_registers - 1:], 292 | ref_outputs['P'][:-(self.pipeline_registers - 1)]) 293 | 294 | def test_multiply_deccumulate(self): 295 | '''There should be a multiply-deccumulate mode, giving P - A * B. 296 | 297 | P is defined to be the output, which is not pipelined. That is, 298 | the output should be negated on every cycle and then incremented by 299 | A*B as long as the multiply-deccumulate is ongoing. 300 | ''' 301 | reset = self.default_args['reset'] 302 | clock = self.default_args['clock'] 303 | 304 | @block 305 | def set_opmode(): 306 | @always_seq(clock.posedge, reset=reset) 307 | def _set_opmode(): 308 | self.opmode.next = self.operations['multiply_deccumulate'] 309 | 310 | return _set_opmode 311 | 312 | @block 313 | def ref(**kwargs): 314 | 315 | P = kwargs['P'] 316 | A = kwargs['A'] 317 | B = kwargs['B'] 318 | clock = kwargs['clock'] 319 | reset = kwargs['reset'] 320 | 321 | @always_seq(clock.posedge, reset=reset) 322 | def test_basic_multiply(): 323 | 324 | P.next = P - A * B 325 | 326 | return test_basic_multiply 327 | 328 | args = self.default_args.copy() 329 | arg_types = self.default_arg_types.copy() 330 | 331 | # Don't run too many cycles or you'll get an overflow! 332 | cycles = 20 333 | dut_outputs, ref_outputs = self.cosimulate( 334 | cycles, DSP48E1, ref, args, arg_types, 335 | custom_sources=[(set_opmode, (), {})]) 336 | 337 | # There are pipeline_registers cycles latency on the output. 338 | # The reference above has only 1 cycle latency, so we need to offset 339 | # the results by pipeline_registers - 1 cycles. 340 | self.assertEqual(dut_outputs['P'][self.pipeline_registers - 1:], 341 | ref_outputs['P'][:-(self.pipeline_registers - 1)]) 342 | 343 | def test_clock_enable(self): 344 | '''clock_enable False should stop the pipeline being stepped. 345 | 346 | When clock_enable is False, the DSP48E1 should remain in an unchanged 347 | state until it is True again, unless the reset signal is active. 348 | ''' 349 | reset = self.default_args['reset'] 350 | clock = self.default_args['clock'] 351 | 352 | operation = self.operations['multiply'] 353 | 354 | @block 355 | def set_opmode(): 356 | @always_seq(clock.posedge, reset=reset) 357 | def _set_opmode(): 358 | self.opmode.next = operation 359 | 360 | return _set_opmode 361 | 362 | @block 363 | def ref(**kwargs): 364 | 365 | P = kwargs['P'] 366 | A = kwargs['A'] 367 | B = kwargs['B'] 368 | opmode = kwargs['opmode'] 369 | clock_enable = kwargs['clock_enable'] 370 | clock = kwargs['clock'] 371 | reset = kwargs['reset'] 372 | 373 | # Each pipeline should be pipeline_registers - 1 long since 374 | # there is one implicit register. 375 | A_pipeline = deque( 376 | [copy_signal(A) for _ in range(self.pipeline_registers - 1)]) 377 | B_pipeline = deque( 378 | [copy_signal(B) for _ in range(self.pipeline_registers - 1)]) 379 | opmode_pipeline = deque( 380 | [copy_signal(opmode) for _ in 381 | range(self.pipeline_registers - 1)]) 382 | 383 | @always(clock.posedge) 384 | def test_arbitrary_pipeline(): 385 | 386 | if reset == reset.active: 387 | for _A, _B, _opmode in zip( 388 | A_pipeline, B_pipeline, opmode_pipeline): 389 | 390 | _A.next = _A._init 391 | _B.next = _B._init 392 | _opmode.next = _opmode._init 393 | 394 | P.next = P._init 395 | 396 | else: 397 | 398 | if clock_enable: 399 | A_pipeline.append(copy_signal(A)) 400 | B_pipeline.append(copy_signal(B)) 401 | opmode_pipeline.append(copy_signal(opmode)) 402 | 403 | A_out = A_pipeline.popleft() 404 | B_out = B_pipeline.popleft() 405 | opmode_out = opmode_pipeline.popleft() 406 | 407 | P.next = A_out * B_out 408 | else: 409 | # Nothing changes 410 | pass 411 | 412 | return test_arbitrary_pipeline 413 | 414 | args = self.default_args.copy() 415 | arg_types = self.default_arg_types.copy() 416 | 417 | arg_types['clock_enable'] = 'random' 418 | 419 | # Don't run too many cycles or you'll get an overflow! 420 | cycles = 40 421 | dut_outputs, ref_outputs = self.cosimulate( 422 | cycles, DSP48E1, ref, args, arg_types, 423 | custom_sources=[(set_opmode, (), {})]) 424 | 425 | self.assertEqual(dut_outputs['P'], ref_outputs['P']) 426 | 427 | def test_reset_trumps_clock_enable(self): 428 | '''If reset is active then clock enable is ignored; the reset occurs. 429 | 430 | The reset should always happen if reset is active on a clock edge. 431 | ''' 432 | reset = self.default_args['reset'] 433 | clock = self.default_args['clock'] 434 | 435 | operation = self.operations['multiply'] 436 | 437 | @block 438 | def set_opmode(): 439 | @always_seq(clock.posedge, reset=reset) 440 | def _set_opmode(): 441 | self.opmode.next = operation 442 | 443 | return _set_opmode 444 | 445 | @block 446 | def ref(**kwargs): 447 | 448 | P = kwargs['P'] 449 | A = kwargs['A'] 450 | B = kwargs['B'] 451 | opmode = kwargs['opmode'] 452 | clock_enable = kwargs['clock_enable'] 453 | clock = kwargs['clock'] 454 | reset = kwargs['reset'] 455 | 456 | # Each pipeline should be pipeline_registers - 1 long since 457 | # there is one implicit register. 458 | A_pipeline = deque( 459 | [copy_signal(A) for _ in range(self.pipeline_registers - 1)]) 460 | B_pipeline = deque( 461 | [copy_signal(B) for _ in range(self.pipeline_registers - 1)]) 462 | opmode_pipeline = deque( 463 | [copy_signal(opmode) for _ in 464 | range(self.pipeline_registers - 1)]) 465 | 466 | @always(clock.posedge) 467 | def test_arbitrary_pipeline(): 468 | 469 | if reset == reset.active: 470 | for _A, _B, _opmode in zip( 471 | A_pipeline, B_pipeline, opmode_pipeline): 472 | 473 | _A.next = _A._init 474 | _B.next = _B._init 475 | _opmode.next = _opmode._init 476 | P.next = P._init 477 | else: 478 | 479 | if clock_enable: 480 | A_pipeline.append(copy_signal(A)) 481 | B_pipeline.append(copy_signal(B)) 482 | opmode_pipeline.append(copy_signal(opmode)) 483 | 484 | A_out = A_pipeline.popleft() 485 | B_out = B_pipeline.popleft() 486 | opmode_out = opmode_pipeline.popleft() 487 | 488 | P.next = A_out * B_out 489 | else: 490 | # Nothing changes 491 | pass 492 | 493 | return test_arbitrary_pipeline 494 | 495 | args = self.default_args.copy() 496 | arg_types = self.default_arg_types.copy() 497 | 498 | arg_types.update({'reset': 'custom_reset', 499 | 'clock_enable': 'random'}) 500 | 501 | custom_sources = [ 502 | (weighted_random_reset_source, 503 | (args['reset'], args['clock'], 0.7), {})] 504 | 505 | # Don't run too many cycles or you'll get an overflow! 506 | cycles = 40 507 | dut_outputs, ref_outputs = self.cosimulate( 508 | cycles, DSP48E1, ref, args, arg_types, 509 | custom_sources=custom_sources+[(set_opmode, (), {})]) 510 | 511 | self.assertEqual(dut_outputs['reset'], ref_outputs['reset']) 512 | self.assertEqual(dut_outputs['P'], ref_outputs['P']) 513 | 514 | def test_changing_modes(self): 515 | '''It should be possible to change modes dynamically. 516 | 517 | When the mode is changed, the mode should propagate through the 518 | pipeline with the data. That is, the mode should be attached to 519 | the input it accompanies. 520 | ''' 521 | 522 | # Create the (unique) reverse lookup 523 | opmode_reverse_lookup = { 524 | self.operations[key]: key for key in self.operations} 525 | 526 | @block 527 | def custom_reset_source(driven_reset, clock): 528 | dummy_reset = ResetSignal(bool(0), active=1, isasync=False) 529 | 530 | @instance 531 | def custom_reset(): 532 | driven_reset.next = 1 533 | yield(clock.posedge) 534 | driven_reset.next = 1 535 | yield(clock.posedge) 536 | while True: 537 | next_reset = randrange(0, 100) 538 | # Be false 90% of the time. 539 | if next_reset > 90: 540 | driven_reset.next = 1 541 | else: 542 | driven_reset.next = 0 543 | 544 | yield(clock.posedge) 545 | 546 | return custom_reset 547 | 548 | @block 549 | def ref(**kwargs): 550 | 551 | P = kwargs['P'] 552 | A = kwargs['A'] 553 | B = kwargs['B'] 554 | C = kwargs['C'] 555 | opmode = kwargs['opmode'] 556 | clock_enable = kwargs['clock_enable'] 557 | clock = kwargs['clock'] 558 | reset = kwargs['reset'] 559 | 560 | # Each pipeline should be pipeline_registers - 1 long since 561 | # there is one implicit register. 562 | A_pipeline = deque( 563 | [copy_signal(A) for _ in range(self.pipeline_registers - 1)]) 564 | B_pipeline = deque( 565 | [copy_signal(B) for _ in range(self.pipeline_registers - 1)]) 566 | C_pipeline = deque( 567 | [copy_signal(C) for _ in range(self.pipeline_registers - 1)]) 568 | opmode_pipeline = deque( 569 | [copy_signal(opmode) for _ in 570 | range(self.pipeline_registers - 1)]) 571 | 572 | @always(clock.posedge) 573 | def test_arbitrary_pipeline(): 574 | 575 | if reset == reset.active: 576 | for _A, _B, _C, _opmode in zip( 577 | A_pipeline, B_pipeline, C_pipeline, opmode_pipeline): 578 | 579 | _A.next = _A._init 580 | _B.next = _B._init 581 | _C.next = _C._init 582 | _opmode.next = _opmode._init 583 | P.next = P._init 584 | else: 585 | 586 | if clock_enable: 587 | A_pipeline.append(copy_signal(A)) 588 | B_pipeline.append(copy_signal(B)) 589 | C_pipeline.append(copy_signal(C)) 590 | opmode_pipeline.append(copy_signal(opmode)) 591 | 592 | A_out = A_pipeline.popleft() 593 | B_out = B_pipeline.popleft() 594 | C_out = C_pipeline.popleft() 595 | opmode_out = opmode_pipeline.popleft() 596 | 597 | if (opmode_reverse_lookup[int(opmode_out.val)] == 598 | 'multiply'): 599 | P.next = A_out * B_out 600 | 601 | elif (opmode_reverse_lookup[int(opmode_out.val)] == 602 | 'multiply_add'): 603 | P.next = A_out * B_out + C_out 604 | 605 | elif (opmode_reverse_lookup[int(opmode_out.val)] == 606 | 'multiply_accumulate'): 607 | P.next = P + A_out * B_out 608 | 609 | elif (opmode_reverse_lookup[int(opmode_out.val)] == 610 | 'multiply_deccumulate'): 611 | P.next = P - A_out * B_out 612 | 613 | 614 | return test_arbitrary_pipeline 615 | 616 | args = self.default_args.copy() 617 | arg_types = self.default_arg_types.copy() 618 | 619 | arg_types.update({'opmode': 'random', 620 | 'clock_enable': 'random', 621 | 'reset': 'custom_reset'}) 622 | 623 | custom_sources = [ 624 | (custom_reset_source, (args['reset'], args['clock']), {})] 625 | 626 | cycles = 100 627 | dut_outputs, ref_outputs = self.cosimulate( 628 | cycles, DSP48E1, ref, args, arg_types, 629 | custom_sources=custom_sources) 630 | 631 | self.assertEqual(dut_outputs['reset'], ref_outputs['reset']) 632 | self.assertEqual(dut_outputs['P'], ref_outputs['P']) 633 | 634 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 'Vivado executable not in path') 635 | class TestDSP48E1VivadoVHDLSimulation(TestDSP48E1Simulation): 636 | '''The tests of TestDSP48E1Simulation should run under the Vivado 637 | simulator using VHDL. 638 | ''' 639 | 640 | def cosimulate(self, sim_cycles, dut_factory, ref_factory, args, 641 | arg_types, **kwargs): 642 | 643 | return vivado_vhdl_cosimulation(sim_cycles, dut_factory, ref_factory, 644 | args, arg_types, **kwargs) 645 | 646 | 647 | @unittest.skipIf(VIVADO_EXECUTABLE is None, 'Vivado executable not in path') 648 | class TestDSP48E1VivadoVerilogSimulation(TestDSP48E1Simulation): 649 | '''The tests of TestDSP48E1Simulation should run under the Vivado 650 | simulator using VHDL. 651 | ''' 652 | 653 | def cosimulate(self, sim_cycles, dut_factory, ref_factory, args, 654 | arg_types, **kwargs): 655 | 656 | return vivado_verilog_cosimulation( 657 | sim_cycles, dut_factory, ref_factory, 658 | args, arg_types, **kwargs) 659 | -------------------------------------------------------------------------------- /ovenbird/cosimulation.py: -------------------------------------------------------------------------------- 1 | 2 | from veriutils import ( 3 | SynchronousTest, AxiStreamOutput, SignalOutput, AVAILABLE_TIME_UNITS) 4 | import veriutils.cosimulation 5 | 6 | import ovenbird 7 | 8 | from myhdl import * 9 | import myhdl 10 | from myhdl.conversion._toVHDL import _shortversion 11 | myhdl_vhdl_package_filename = "pck_myhdl_%s.vhd" % _shortversion 12 | 13 | from collections import deque 14 | 15 | import tempfile 16 | import os 17 | import string 18 | import shutil 19 | import subprocess 20 | import csv 21 | import copy 22 | import re 23 | import collections 24 | import warnings 25 | 26 | try: # pragma: no branch 27 | # Python 2 28 | from ConfigParser import RawConfigParser 29 | except ImportError: 30 | # Python 3 31 | from configparser import RawConfigParser 32 | 33 | __all__ = ['vivado_vhdl_cosimulation', 'vivado_verilog_cosimulation', 34 | 'VivadoError'] 35 | 36 | _simulate_tcl_template = string.Template(''' 37 | config_webtalk -user off 38 | create_project $project_name $project_path -part $part 39 | 40 | set_property target_language $target_language [current_project] 41 | set_param project.enableVHDL2008 1 42 | set_property enable_vhdl_2008 1 [current_project] 43 | 44 | $load_and_configure_ips 45 | add_files -norecurse {$vhdl_files $verilog_files $ip_additional_hdl_files} 46 | if {[string length [get_files {$vhdl_files}]] != 0} { 47 | set_property FILE_TYPE {VHDL 2008} [get_files {$vhdl_files}] 48 | } 49 | 50 | update_compile_order -fileset sources_1 51 | update_compile_order -fileset sim_1 52 | set_property -name {xsim.simulate.runtime} -value {${time}${time_units}} -objects [current_fileset -simset] 53 | launch_simulation 54 | $vcd_capture_script 55 | close_sim 56 | close_project 57 | ''') 58 | 59 | _vcd_capture_template = string.Template(''' 60 | restart 61 | open_vcd {${vcd_filename}} 62 | log_vcd 63 | run ${time}${time_units} 64 | close_vcd 65 | ''') 66 | 67 | def _populate_vivado_ip_list(block, hdl): 68 | 69 | try: 70 | if hdl == 'VHDL': 71 | vivado_ip_list = [block.vhdl_code.code.ip_instance] 72 | else: 73 | vivado_ip_list = [block.verilog_code.code.ip_instance] 74 | 75 | except AttributeError: 76 | vivado_ip_list = [] 77 | 78 | for sub in block.subs: 79 | if isinstance(sub, myhdl._block._Block): 80 | vivado_ip_list += _populate_vivado_ip_list(sub, hdl) 81 | 82 | return vivado_ip_list 83 | 84 | def _get_signal_names_to_port_names(filename, comment_string): 85 | 86 | with open(filename) as f: 87 | code = f.read() 88 | 89 | signal_name_mappings = {} 90 | for each in re.finditer( 91 | '^%s .*?$' % comment_string, code, re.MULTILINE): 92 | vals = code[each.start():each.end()].split() 93 | 94 | signal_name_mappings[vals[2]] = vals[3] 95 | 96 | return signal_name_mappings 97 | 98 | 99 | class VivadoError(RuntimeError): 100 | pass 101 | 102 | def _vivado_generic_cosimulation( 103 | target_language, cycles, dut_factory, ref_factory, args, 104 | arg_types, period, custom_sources, 105 | enforce_convertible_top_level_interfaces, keep_temp_files, config_file, 106 | template_path_prefix, vcd_name, time_units): 107 | 108 | if ovenbird.VIVADO_EXECUTABLE is None: 109 | raise EnvironmentError('Vivado executable not in path') 110 | 111 | if period is None: 112 | period = veriutils.cosimulation.PERIOD 113 | 114 | if time_units not in AVAILABLE_TIME_UNITS: 115 | raise ValueError( 116 | 'Invalid time unit. Please select from: ' + 117 | ', '.join(AVAILABLE_TIME_UNITS)) 118 | 119 | config = RawConfigParser() 120 | config.read(config_file) 121 | 122 | sim_object = SynchronousTest( 123 | dut_factory, ref_factory, args, arg_types, period, custom_sources, 124 | enforce_convertible_top_level_interfaces, time_units=time_units) 125 | 126 | # We need to create the test data 127 | myhdl_outputs = sim_object.cosimulate(cycles, vcd_name=vcd_name) 128 | 129 | 130 | # Most of the dut outputs will be the same as ref, we then overwrite 131 | # the others from the written file. 132 | dut_outputs = copy.deepcopy(myhdl_outputs[1]) 133 | ref_outputs = myhdl_outputs[1] 134 | 135 | # StopSimulation might be been called, so we should handle that. 136 | # Use the ref outputs, as that can't be None 137 | # outputs_length is the number of cycles we use for the vivado 138 | # cosimulation 139 | outputs_length = None 140 | for each_signal in ref_outputs: 141 | 142 | if not isinstance(ref_outputs[each_signal], SignalOutput): 143 | # We remove non signal-specific outputs from dut_outputs and 144 | # continue. These are things that should be added back in again. 145 | del dut_outputs[each_signal] 146 | continue 147 | 148 | if sim_object.elaborated_args[each_signal].type == 'output': 149 | # We also delete outputs which again should be added back in 150 | del dut_outputs[each_signal] 151 | 152 | _length = len(ref_outputs[each_signal]) 153 | 154 | if outputs_length is not None: 155 | assert outputs_length == _length 156 | 157 | outputs_length = _length 158 | 159 | assert outputs_length is not None 160 | 161 | # One cycle is lost in the vivado simulation for the propagation 162 | # delay between reading and writing. 163 | _cycles = outputs_length + 1 164 | 165 | # FIXME 166 | tmp_dir = tempfile.mkdtemp() 167 | #tmp_dir = '/tmp/tmpdecqrb_0' 168 | signal_output_filename = 'signal_outputs' 169 | signal_output_path = os.path.join(tmp_dir, signal_output_filename) 170 | 171 | try: 172 | project_name = 'tmp_project' 173 | project_path = os.path.join(tmp_dir, project_name) 174 | 175 | # FIXME - this should be uncommented. There is a bug in myhdl in which 176 | # multiple converts cause problems. 177 | # # Firstly check the dut is convertible 178 | # # We wrap the actual dut in an argumentless block so the 179 | # # issue of non-convertible top level signals goes away 180 | # @block 181 | # def minimal_wrapper(): 182 | # return dut_factory(**args) 183 | # 184 | # with warnings.catch_warnings(): 185 | # # We don't worry about warnings at this stage - they are to be 186 | # # expected. We only really care about errors. 187 | # warnings.simplefilter("ignore") 188 | # minimal_wrapper().convert(hdl=target_language, path=tmp_dir) 189 | 190 | time = period * _cycles 191 | 192 | try: 193 | ip_dependencies = dut_factory.ip_dependencies 194 | except AttributeError: 195 | ip_dependencies = () 196 | 197 | vhdl_files = [] 198 | verilog_files = [] 199 | ip_additional_hdl_files = [] 200 | 201 | load_and_configure_ips_tcl_string = '' 202 | 203 | if vcd_name is not None: 204 | vcd_filename = os.path.realpath(vcd_name + '.vivado.vcd') 205 | vcd_capture_script = _vcd_capture_template.safe_substitute( 206 | {'vcd_filename': vcd_filename, 207 | 'time': time, 208 | 'time_units': time_units,}) 209 | 210 | else: 211 | vcd_capture_script = '' 212 | 213 | if target_language == 'VHDL': 214 | try: 215 | vhdl_dependencies = list(dut_factory.vhdl_dependencies) 216 | except AttributeError: 217 | vhdl_dependencies = [] 218 | 219 | convertible_top_filename = os.path.join( 220 | tmp_dir, 'dut_convertible_top.vhd') 221 | 222 | vhdl_dut_files = [ 223 | convertible_top_filename, 224 | os.path.join(tmp_dir, myhdl_vhdl_package_filename)] 225 | 226 | vhdl_files += vhdl_dependencies + vhdl_dut_files 227 | 228 | # Generate the output VHDL files 229 | convertible_top = sim_object.dut_convertible_top( 230 | tmp_dir, signal_output_filename=signal_output_filename, 231 | axi_stream_packets_filename_prefix='axi_stream_out') 232 | 233 | ip_list = set(_populate_vivado_ip_list(convertible_top, 'VHDL')) 234 | 235 | for ip_object in ip_list: 236 | 237 | ip_additional_hdl_files.append( 238 | ip_object.write_vhdl_wrapper(tmp_dir)) 239 | 240 | load_and_configure_ips_tcl_string += ip_object.tcl_string 241 | 242 | toVHDL.initial_values = True 243 | 244 | with warnings.catch_warnings(record=True) as w: 245 | warnings.simplefilter('always', myhdl.ToVHDLWarning) 246 | try: 247 | convertible_top.convert(hdl='VHDL', path=tmp_dir) 248 | 249 | except myhdl.ConversionError as e: 250 | raise ovenbird.OvenbirdConversionError( 251 | 'The convertible top from Veriutils failed to convert ' 252 | 'with the following error:\n%s\n' 253 | 'Though this could be a problem with your code, it ' 254 | 'could also mean there is a problem with the ' 255 | 'way you set Veriutils up. Are all the signals defined ' 256 | 'correctly and the signal types set up correctly ' 257 | '(importantly, all the outputs are defined as such)? ' 258 | 'Alternatively it could be a bug in Veriutils.' % str(e)) 259 | # FIXME currently the conversion test to verify user code 260 | # is broken due to a myhdl bug (see above). The below 261 | # exception string should be enabled when the bug is fixed. 262 | #raise ovenbird.OvenbirdConversionError( 263 | # 'The convertible top from Veriutils failed to convert ' 264 | # 'with the following error: %s\n' 265 | # 'The code that has been passed in for verification (i.e. ' 266 | # 'that you wrote) has been verified as converting ' 267 | # 'properly. This means there could be a problem with the ' 268 | # 'way you set Veriutils up. Are all the signals defined ' 269 | # 'correctly and the signal types set up correctly ' 270 | # '(importantly, all the outputs are defined as such)? ' 271 | # 'Alternatively it could be a bug in Veriutils.') 272 | 273 | vhdl_conversion_warnings = w 274 | 275 | signal_name_mappings = _get_signal_names_to_port_names( 276 | convertible_top_filename, '--') 277 | 278 | for warning in vhdl_conversion_warnings: 279 | message = str(warning.message) 280 | 281 | for internal_name in signal_name_mappings: 282 | if internal_name in message: 283 | port_name = signal_name_mappings[internal_name] 284 | message = str.replace( 285 | message, internal_name, 286 | '%s (internally to VHDL: %s)' % 287 | (port_name, internal_name)) 288 | 289 | warnings.warn_explicit( 290 | message, warning.category, warning.filename, 291 | warning.lineno) 292 | 293 | elif target_language == 'Verilog': 294 | try: 295 | verilog_dependencies = list(dut_factory.verilog_dependencies) 296 | except AttributeError: 297 | verilog_dependencies = [] 298 | 299 | convertible_top_filename = os.path.join( 300 | tmp_dir, 'dut_convertible_top.v') 301 | verilog_dut_files = [convertible_top_filename,] 302 | 303 | verilog_files += verilog_dependencies + verilog_dut_files 304 | 305 | # Generate the output Verilog files 306 | convertible_top = sim_object.dut_convertible_top( 307 | tmp_dir, signal_output_filename=signal_output_filename, 308 | axi_stream_packets_filename_prefix='axi_stream_out') 309 | 310 | ip_list = set(_populate_vivado_ip_list(convertible_top, 'Verilog')) 311 | 312 | for ip_object in ip_list: 313 | load_and_configure_ips_tcl_string += ip_object.tcl_string 314 | 315 | toVerilog.initial_values = True 316 | 317 | with warnings.catch_warnings(record=True) as w: 318 | warnings.simplefilter('always', myhdl.ToVerilogWarning) 319 | 320 | # Generate the appropriate timescale based on the time_units. 321 | # This is in the form '1ns/1ns' (when time_units is ns). 322 | timescale = '1' + str(time_units) + '/1' + str(time_units) 323 | 324 | try: 325 | convertible_top.convert( 326 | hdl='Verilog', path=tmp_dir, timescale=timescale) 327 | 328 | except myhdl.ConversionError as e: 329 | 330 | raise ovenbird.OvenbirdConversionError( 331 | 'The convertible top from Veriutils failed to convert ' 332 | 'with the following error: %s\n' 333 | 'The code that has been passed in for verification (i.e. ' 334 | 'that you wrote) has been verified as converting ' 335 | 'properly. This means there could be a problem with the ' 336 | 'way you set Veriutils up. Are all the signals defined ' 337 | 'correctly and the signal types set up correctly ' 338 | '(importantly, all the outputs are defined as such)? ' 339 | 'Alternatively it could be a bug in Veriutils.' % str(e)) 340 | 341 | verilog_conversion_warnings = w 342 | 343 | signal_name_mappings = _get_signal_names_to_port_names( 344 | convertible_top_filename, '//') 345 | 346 | for warning in verilog_conversion_warnings: 347 | message = str(warning.message) 348 | 349 | for internal_name in signal_name_mappings: 350 | if internal_name in message: 351 | port_name = signal_name_mappings[internal_name] 352 | message = str.replace( 353 | message, internal_name, 354 | '%s (internally to Verilog: %s)' % 355 | (port_name, internal_name)) 356 | 357 | warnings.warn_explicit( 358 | message, warning.category, warning.filename, 359 | warning.lineno) 360 | 361 | else: 362 | raise ValueError('Target language must be \'Verilog\' or ' 363 | '\'VHDL\'') 364 | 365 | for each_hdl_file in (vhdl_files + verilog_files + 366 | ip_additional_hdl_files): 367 | # The files should all now exist 368 | if not os.path.exists(each_hdl_file): 369 | raise EnvironmentError( 370 | 'An expected HDL file is missing: %s' 371 | % (each_hdl_file)) 372 | 373 | vhdl_files_string = ' '.join(vhdl_files) 374 | verilog_files_string = ' '.join(verilog_files) 375 | ip_additional_hdl_files_string = ' '.join(ip_additional_hdl_files) 376 | 377 | template_substitutions = { 378 | 'target_language': target_language, 379 | 'part': config.get('General', 'part'), 380 | 'project_name': project_name, 381 | 'project_path': project_path, 382 | 'time': time, 383 | 'load_and_configure_ips': load_and_configure_ips_tcl_string, 384 | 'vhdl_files': vhdl_files_string, 385 | 'verilog_files': verilog_files_string, 386 | 'ip_additional_hdl_files': ip_additional_hdl_files_string, 387 | 'vcd_capture_script': vcd_capture_script, 388 | 'time_units': time_units,} 389 | 390 | simulate_script = _simulate_tcl_template.safe_substitute( 391 | template_substitutions) 392 | 393 | simulate_script_filename = os.path.join( 394 | tmp_dir, 'simulate_script.tcl') 395 | 396 | with open(simulate_script_filename, 'w') as simulate_script_file: 397 | simulate_script_file.write(simulate_script) 398 | 399 | vivado_process = subprocess.Popen( 400 | [ovenbird.VIVADO_EXECUTABLE, '-nolog', '-nojournal', '-mode', 401 | 'batch', '-source', simulate_script_filename], 402 | stdin=subprocess.PIPE, stdout=subprocess.PIPE, 403 | stderr=subprocess.PIPE) 404 | 405 | out, err = vivado_process.communicate() 406 | 407 | if err != b'': 408 | if target_language == 'VHDL': 409 | xvhdl_log_filename = os.path.join( 410 | tmp_dir, 'tmp_project', 'tmp_project.sim', 'sim_1', 411 | 'behav', 'xvhdl.log') 412 | 413 | if xvhdl_log_filename.encode() in err: 414 | with open(xvhdl_log_filename, 'r') as log_file: 415 | err += '\n' 416 | err += 'xvhdl.log:\n' 417 | err += log_file.read() 418 | 419 | raise VivadoError( 420 | 'Error running the Vivado VHDL simulator:\n%s' % err) 421 | 422 | elif target_language == 'Verilog': 423 | xvhdl_log_filename = os.path.join( 424 | tmp_dir, 'tmp_project', 'tmp_project.sim', 'sim_1', 425 | 'behav', 'xvlog.log') 426 | 427 | if xvhdl_log_filename.encode() in err: 428 | with open(xvhdl_log_filename, 'r') as log_file: 429 | err += '\n' 430 | err += 'xvlog.log:\n' 431 | err += log_file.read() 432 | 433 | raise VivadoError( 434 | 'Error running the Vivado Verilog simulator:\n%s' % err) 435 | 436 | with open(signal_output_path, 'r') as signal_output_file: 437 | signal_reader = csv.DictReader(signal_output_file, delimiter=',') 438 | 439 | vivado_signals = [row for row in signal_reader] 440 | 441 | vivado_signal_keys = vivado_signals[0].keys() 442 | 443 | # Rearrange the output signals into the correct form 444 | _vivado_signals = {key: [] for key in vivado_signals[0].keys()} 445 | 446 | for each_row in vivado_signals: 447 | for each_key in vivado_signal_keys: 448 | _vivado_signals[each_key].append(each_row[each_key]) 449 | 450 | vivado_signals = _vivado_signals 451 | 452 | interface_outputs = {} 453 | siglist_outputs = {} 454 | for each_signal_name_str in vivado_signals: 455 | 456 | each_dut_outputs = [] 457 | 458 | sig_container, signal_type, each_signal = ( 459 | each_signal_name_str.split(' ')) 460 | 461 | for dut_str_value in vivado_signals[each_signal_name_str]: 462 | try: 463 | if signal_type == 'bool': 464 | each_value = bool(int(dut_str_value)) 465 | else: 466 | # We assume an intbv 467 | _each_value = ( 468 | intbv(dut_str_value)[len(dut_str_value):]) 469 | 470 | if signal_type =='signed': 471 | each_value = _each_value.signed() 472 | else: 473 | each_value = _each_value 474 | 475 | except ValueError: 476 | # Probably an undefined. 477 | each_value = None 478 | 479 | each_dut_outputs.append(each_value) 480 | 481 | dut_outputs[each_signal] = each_dut_outputs 482 | 483 | # # add each per-signal list into a data structure that 484 | # # can be easily turned into the correct output when it is not 485 | # # possible to add it directly. 486 | # if sig_container == 'interface': 487 | # output_name_list = each_signal.split('.') 488 | # 489 | # # We have an interface, so group the recorded signals 490 | # # of the interface together. 491 | # 492 | # # FIXME Only one level of interface supported 493 | # interface_outputs.setdefault( 494 | # output_name_list[0], {})[output_name_list[1]] = ( 495 | # each_dut_outputs) 496 | # 497 | # elif sig_container == 'list': 498 | # # We have a list 499 | # parsed_header = re.search( 500 | # '(?P.*)\[(?P.*)\]', each_signal) 501 | # 502 | # siglist_name = parsed_header.group('list_name') 503 | # siglist_index = int(parsed_header.group('index')) 504 | # 505 | # siglist_outputs.setdefault( 506 | # siglist_name, {})[siglist_index] = each_dut_outputs 507 | # 508 | # else: 509 | # # We have a normal signal 510 | # dut_outputs[each_signal] = each_dut_outputs 511 | 512 | # Now convert the data structures into suitable outputs. 513 | 514 | # for each_siglist in siglist_outputs: 515 | # 516 | # # Order the values by the siglist_index 517 | # ordered_siglist_output = collections.OrderedDict(sorted( 518 | # siglist_outputs[each_siglist].items(), key=lambda t: t[0])) 519 | # 520 | # new_dut_output = [] 521 | # 522 | # for each_list_out in zip(*ordered_siglist_output.values()): 523 | # new_dut_output.append(list(each_list_out)) 524 | # 525 | # dut_outputs[each_siglist] = new_dut_output 526 | # 527 | # for each_interface in interface_outputs: 528 | # 529 | # signal_type = arg_types[each_interface] 530 | # 531 | # attr_names = interface_outputs[each_interface].keys() 532 | # 533 | # reordered_interface_outputs = zip( 534 | # *(interface_outputs[each_interface][key] for 535 | # key in attr_names)) 536 | # 537 | # # We need to write the interface values to dut_outputs, but 538 | # # taking the values from ref_outputs if the interface signal was 539 | # # not an output. 540 | # new_dut_output = [] 541 | # 542 | # if signal_type == 'axi_stream_out': 543 | # for ref_output, simulated_output in zip( 544 | # ref_outputs[each_interface]['signals'], 545 | # reordered_interface_outputs): 546 | # 547 | # new_interface_out = ref_output.copy() 548 | # new_interface_out.update( 549 | # dict(zip(attr_names, simulated_output))) 550 | # 551 | # new_dut_output.append(new_interface_out) 552 | # 553 | # dut_outputs[each_interface] = {'signals': new_dut_output} 554 | # else: 555 | # for ref_output, simulated_output in zip( 556 | # ref_outputs[each_interface], reordered_interface_outputs): 557 | # 558 | # new_interface_out = ref_output.copy() 559 | # new_interface_out.update( 560 | # dict(zip(attr_names, simulated_output))) 561 | # 562 | # new_dut_output.append(new_interface_out) 563 | # 564 | # dut_outputs[each_interface] = new_dut_output 565 | 566 | # Now extract the axi signals 567 | for each_interface in ( 568 | sim_object.elaborated_args.axi_stream_out_interfaces): 569 | 570 | completed_packets = {} 571 | 572 | axi_out_filename = os.path.join( 573 | tmp_dir, 'axi_stream_out' + '_' + each_interface) 574 | 575 | with open(axi_out_filename, 'r') as axi_out_file: 576 | axi_packet_reader = csv.DictReader( 577 | axi_out_file, delimiter=',') 578 | vivado_axi_packet = [row for row in axi_packet_reader] 579 | 580 | current_packets = {} 581 | for transaction in vivado_axi_packet: 582 | 583 | if 'TID' in transaction.keys(): 584 | stream_id = int(transaction['TID'], 2) 585 | else: 586 | stream_id = 0 587 | 588 | if 'TDEST' in transaction.keys(): 589 | stream_dest = int(transaction['TDEST'], 2) 590 | else: 591 | stream_dest = 0 592 | 593 | stream = (stream_id, stream_dest) 594 | if stream not in current_packets.keys(): 595 | current_packets[stream] = deque([]) 596 | 597 | current_packets[stream].append( 598 | int(transaction['TDATA'], 2)) 599 | try: 600 | if int(transaction['TLAST']): 601 | 602 | if stream not in completed_packets.keys(): 603 | completed_packets[stream] = deque([]) 604 | 605 | completed_packets[stream].append( 606 | current_packets[stream]) 607 | 608 | del(current_packets[stream]) 609 | except KeyError: 610 | pass 611 | 612 | for stream in completed_packets.keys(): 613 | if len(completed_packets[stream]) == 0: 614 | del(completed_packets[stream]) 615 | 616 | for stream in current_packets.keys(): 617 | if len(current_packets[stream]) == 0: 618 | del(current_packets[stream]) 619 | 620 | axi_signal_output = AxiStreamOutput({ 621 | 'packets': completed_packets, 622 | 'incomplete_packet': current_packets}) 623 | 624 | dut_outputs[each_interface] = axi_signal_output 625 | 626 | # for each_signal in ref_outputs: 627 | # 628 | # completed_packets = {} 629 | # if arg_types[each_signal] == 'axi_stream_out': 630 | # axi_out_filename = os.path.join( 631 | # tmp_dir, 'axi_stream_out' + '_' + each_signal) 632 | # 633 | # with open(axi_out_filename, 'r') as axi_out_file: 634 | # axi_packet_reader = csv.DictReader( 635 | # axi_out_file, delimiter=',') 636 | # vivado_axi_packet = [row for row in axi_packet_reader] 637 | # 638 | # current_packets = {} 639 | # for transaction in vivado_axi_packet: 640 | # 641 | # if 'TID' in transaction.keys(): 642 | # stream_id = int(transaction['TID'], 2) 643 | # else: 644 | # stream_id = 0 645 | # 646 | # if 'TDEST' in transaction.keys(): 647 | # stream_dest = int(transaction['TDEST'], 2) 648 | # else: 649 | # stream_dest = 0 650 | # 651 | # stream = (stream_id, stream_dest) 652 | # if stream not in current_packets.keys(): 653 | # current_packets[stream] = deque([]) 654 | # 655 | # current_packets[stream].append( 656 | # int(transaction['TDATA'], 2)) 657 | # try: 658 | # if int(transaction['TLAST']): 659 | # 660 | # if stream not in completed_packets.keys(): 661 | # completed_packets[stream] = deque([]) 662 | # 663 | # completed_packets[stream].append( 664 | # current_packets[stream]) 665 | # 666 | # del(current_packets[stream]) 667 | # except KeyError: 668 | # pass 669 | # 670 | # for stream in completed_packets.keys(): 671 | # if len(completed_packets[stream]) == 0: 672 | # del(completed_packets[stream]) 673 | # 674 | # for stream in current_packets.keys(): 675 | # if len(current_packets[stream]) == 0: 676 | # del(current_packets[stream]) 677 | # 678 | # dut_outputs[each_signal]['packets'] = completed_packets 679 | # dut_outputs[each_signal]['incomplete_packet'] = ( 680 | # current_packets) 681 | for each_signal in ref_outputs: 682 | if not isinstance(ref_outputs[each_signal], SignalOutput): 683 | continue 684 | 685 | # Now only output the correct number of cycles 686 | ref_outputs[each_signal] = ( 687 | ref_outputs[each_signal][:outputs_length]) 688 | dut_outputs[each_signal] = ( 689 | dut_outputs[each_signal][:outputs_length]) 690 | 691 | finally: 692 | if not keep_temp_files: 693 | shutil.rmtree(tmp_dir) 694 | else: 695 | print('As requested, the temporary files have not been deleted.' 696 | '\nThey can be found in %s.' % (tmp_dir,)) 697 | 698 | return dut_outputs, ref_outputs 699 | 700 | 701 | def vivado_vhdl_cosimulation( 702 | cycles, dut_factory, ref_factory, args, arg_types, 703 | period=None, custom_sources=None, 704 | enforce_convertible_top_level_interfaces=True, keep_temp_files=False, 705 | config_file='veriutils.cfg', template_path_prefix='', vcd_name=None, 706 | time_units='ns'): 707 | '''Run a cosimulation in which the device under test is simulated inside 708 | Vivado, using VHDL as the intermediate language. 709 | 710 | This function has exactly the same interface as myhdl_cosimulation. 711 | 712 | The outputs should be identical to from myhdl_cosimulation except for 713 | one important caveat: until values are initialised explicitly, they 714 | are recorded as undefined. Undefined values are set to None in the output. 715 | 716 | This is particularly noticeable in the case when an asynchronous reset 717 | is used. Care should be taken to handle the outputs appropriately. 718 | 719 | By default, all the temporary files are cleaned up after use. This 720 | behaviour can be turned off by settings ``keep_temp_files`` to ``True``. 721 | ''' 722 | 723 | target_language = 'VHDL' 724 | 725 | dut_outputs, ref_outputs = _vivado_generic_cosimulation( 726 | target_language, cycles, dut_factory, ref_factory, args, 727 | arg_types, period, custom_sources, 728 | enforce_convertible_top_level_interfaces, keep_temp_files, 729 | config_file, template_path_prefix, vcd_name, time_units) 730 | 731 | return dut_outputs, ref_outputs 732 | 733 | def vivado_verilog_cosimulation( 734 | cycles, dut_factory, ref_factory, args, arg_types, 735 | period=None, custom_sources=None, 736 | enforce_convertible_top_level_interfaces=True, keep_temp_files=False, 737 | config_file='veriutils.cfg', template_path_prefix='', vcd_name=None, 738 | time_units='ns'): 739 | '''Run a cosimulation in which the device under test is simulated inside 740 | Vivado, using Verilog as the intermediate language. 741 | 742 | This function has exactly the same interface as myhdl_cosimulation. 743 | 744 | The outputs should be identical to from myhdl_cosimulation except for 745 | one important caveat: until values are initialised explicitly, they 746 | are recorded as undefined. Undefined values are set to None in the output. 747 | 748 | This is particularly noticeable in the case when an asynchronous reset 749 | is used. Care should be taken to handle the outputs appropriately. 750 | 751 | By default, all the temporary files are cleaned up after use. This 752 | behaviour can be turned off by settings ``keep_temp_files`` to ``True``. 753 | ''' 754 | 755 | target_language = 'Verilog' 756 | 757 | dut_outputs, ref_outputs = _vivado_generic_cosimulation( 758 | target_language, cycles, dut_factory, ref_factory, args, 759 | arg_types, period, custom_sources, 760 | enforce_convertible_top_level_interfaces, keep_temp_files, 761 | config_file, template_path_prefix, vcd_name, time_units) 762 | 763 | return dut_outputs, ref_outputs 764 | 765 | --------------------------------------------------------------------------------