Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple clocks #8

Merged
merged 6 commits into from
Sep 20, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ exeception is the output data is incorrect.
assert all([d < allowed_error for d in differences])


We then use ``slccodec.test_utils.register_test_with_vunit`` to generate an appropriate testbench and input
We then use ``slvcodec.test_utils.register_test_with_vunit`` to generate an appropriate testbench and input
data file, and register the produced test with vunit. VUnit can then be run as normal.


Expand Down
14 changes: 9 additions & 5 deletions examples/complex_mag2.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,25 @@ def fixed_to_float(self, f):
r = f / pow(2, self.fixed_width-2)
return r

def make_input_data(self, seed=None, n_data=3000):
def make_input_data(self):
input_data = [{
'i': {'real': random.randint(self.min_fixed, self.max_fixed),
'imag': random.randint(self.min_fixed, self.max_fixed)},
} for i in range(self.n_data)]

return input_data

def check_output_data(self, input_data, output_data):
inputs = [self.fixed_to_float(d['i']['real']) + self.fixed_to_float(d['i']['imag']) * 1j
for d in input_data]
input_float_mag2s = [abs(v)*abs(v) for v in inputs]
input_float_mag2s = [abs(v)*abs(v) for v in inputs]
outputs = [self.fixed_to_float(d['o']) for d in output_data]
differences = [abs(expected - actual) for expected, actual in zip(input_float_mag2s, outputs)]
differences = [abs(expected - actual)
for expected, actual in zip(input_float_mag2s, outputs)]
allowed_error = 1/pow(2, self.fixed_width-2)
assert all([d < allowed_error for d in differences])


if __name__ == '__main__':
def run_test():
random.seed(0)
# Initialize vunit with command line parameters.
vu = config.setup_vunit()
Expand All @@ -63,3 +63,7 @@ def check_output_data(self, input_data, output_data):
# Run the tests with VUnit
vu.set_sim_option('disable_ieee_warnings', True)
vu.main()


if __name__ == '__main__':
run_test()
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
'axilent==0.0.0',
],
dependency_links=[
'git+https://github.com/benreynwar/fusesoc_generators@9a374ae8fdfdbe0f1448d7973783464614990d9c#egg=fusesoc_generators-0.0.0',
'git+https://github.com/benreynwar/fusesoc_generators@1632aaf22016667912dfa77d6999501414b39600#egg=fusesoc_generators-0.0.0',
'git+https://github.com/benreynwar/pyvivado@680364d2fbd04679ef06ea229702e0e6e5394af3#egg=pyvivado-0.0.0',
'git+https://github.com/benreynwar/axilent@963c6824f2fdbd2a3beaef2682fb7461ddcb5a86#egg=axilent-0.0.0',
],
Expand Down
62 changes: 54 additions & 8 deletions slvcodec/entity.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import collections
import logging
import re

from slvcodec import package, math_parser, typs, resolution

Expand Down Expand Up @@ -133,13 +134,56 @@ def input_ports(self):
if (port.direction == 'in') and (port.name not in CLOCK_NAMES)])
return input_ports

def inputs_to_slv(self, inputs, generics):
def group_ports_by_clock_domain(self, clock_domains):
'''
`clock_domains` is a dictionary associating a clock name, with a list of regex patterns
for the ports that are in that clock domain.

The function returns a dictionary mapping a clock name to a tuple of input and output
ports in that domain.
'''
clock_names = clock_domains.keys()
matched_names = set(clock_names)
grouped_ports = {}
for clock_name, signal_patterns in clock_domains.items():
regexes = [re.compile(pattern) for pattern in signal_patterns]
input_matches = [p for name, p in self.ports.items()
if any([regex.match(name) for regex in regexes]) and
(name not in clock_names) and (p.direction == 'in')]
input_match_names = [p.name for p in input_matches]
input_already_assigned = set(input_match_names) & matched_names
if input_already_assigned:
raise Exception('Signals {} are assigned to two clock domains'.format(
input_already_assigned))
matched_names |= set(input_match_names)
output_matches = [p for name, p in self.ports.items()
if any([regex.match(name) for regex in regexes]) and
(name not in clock_names) and (p.direction == 'out')]
output_match_names = [p.name for p in output_matches]
output_already_assigned = (set(output_match_names) & matched_names)
if output_already_assigned:
raise Exception('Signals {} are assigned to two clock domains'.format(
output_already_assigned))
matched_names |= set(output_match_names)
grouped_ports[clock_name] = (input_matches, output_matches)
all_names = set(self.ports.keys())
unassigned_names = all_names - matched_names
if unassigned_names:
raise Exception('Not all signals assigned: {}'.format(unassigned_names))
assert set(matched_names) == set(all_names)
return grouped_ports

def inputs_to_slv(self, inputs, generics, subset_only=False):
'''
Takes a dictionary of inputs, and a dictionary of generics parameters for the entity
and produces a string of '0's and '1's representing the input values.
If `subset_only` is False then it is allowed that `inputs` is a subset of all the
input ports. The generated slv will only contain values for those signals.
'''
slvs = []
for port in self.input_ports().values():
if subset_only and (port.name not in inputs):
continue
port_inputs = inputs.get(port.name, None)
try:
port_slv = port.typ.to_slv(port_inputs, generics)
Expand All @@ -157,19 +201,21 @@ def inputs_to_slv(self, inputs, generics):
slv = ''.join(reversed(slvs))
return slv

def ports_from_slv(self, slv, generics, direction):
def ports_from_slv(self, slv, generics, direction, subset=None):
'''
Extract port values from an slv string.

'slv': A string of 0's and 1's representing the port data.
'generics': The generic parameters for the entity.
'directrion': Whether we are extracting input or output ports.
'direction': Whether we are extracting input or output ports.
`subset`: An optional list of the signals present in the slv.
'''
assert direction in ('in', 'out')
pos = 0
outputs = {}
for port in self.ports.values():
if (port.direction == direction) and (port.name not in CLOCK_NAMES):
if ((port.direction == direction) and (port.name not in CLOCK_NAMES) and
((subset is None) or (port.name in subset))):
width_symbol = typs.make_substitute_generics_function(generics)(port.typ.width)
width = math_parser.get_value(width_symbol)
intwidth = int(width)
Expand All @@ -183,18 +229,18 @@ def ports_from_slv(self, slv, generics, direction):
outputs[port.name] = port_value
return outputs

def outputs_from_slv(self, slv, generics):
def outputs_from_slv(self, slv, generics, subset=None):
'''
Extract output port values from a string of 0's and 1's.
'''
slv = slv.strip()
data = self.ports_from_slv(slv, generics, 'out')
data = self.ports_from_slv(slv, generics, 'out', subset)
return data

def inputs_from_slv(self, slv, generics):
def inputs_from_slv(self, slv, generics, subset=None):
'''
Extract input port values from a string of 0's and 1's.
'''
slv = slv.strip()
data = self.ports_from_slv(slv, generics, 'in')
data = self.ports_from_slv(slv, generics, 'in', subset)
return data
151 changes: 144 additions & 7 deletions slvcodec/filetestbench_generator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import os
import re

import jinja2

Expand Down Expand Up @@ -116,7 +117,8 @@ def make_filetestbench(enty, add_double_wrapper=False, use_vunit=True,
output_slv_declarations, output_slv_definitions])
clk_names = [p.name for p in enty.ports.values()
if (p.direction == 'in') and (p.name in entity.CLOCK_NAMES)]
clk_connections = '\n'.join(['{} => {},'.format(clk, clk) for clk in clk_names])
assert len(clk_names) in (0, 1)
clk_connections = '\n'.join(['{} => clk,'.format(clk) for clk in clk_names])
connections = ',\n'.join(['{} => {}.{}'.format(
p.name, {'in': 'input_data', 'out': 'output_data'}[p.direction], p.name)
for p in enty.ports.values() if p.name not in clk_names])
Expand All @@ -125,10 +127,10 @@ def make_filetestbench(enty, add_double_wrapper=False, use_vunit=True,
# Read in the testbench template and format it.
if use_vunit:
template_fn = os.path.join(os.path.dirname(__file__), 'templates',
'file_testbench.vhd')
'file_testbench.vhd')
else:
template_fn = os.path.join(os.path.dirname(__file__), 'templates',
'file_testbench_no_vunit.vhd')
'file_testbench_no_vunit.vhd')
if add_double_wrapper:
dut_name = enty.identifier + '_toslvcodec'
else:
Expand All @@ -149,8 +151,137 @@ def make_filetestbench(enty, add_double_wrapper=False, use_vunit=True,
return filetestbench


def process_signals(signals, type_name):
names_and_types = [(p.name, p.typ) for p in signals]
record = typs.Record(type_name, names_and_types)
slv_declarations, slv_definitions = package_generator.make_record_declarations_and_definitions(
record)
if signals:
definitions = [record.declaration(), slv_declarations, slv_definitions]
else:
# When there are no signals don't bother writing definitions because an
# empty record definition is not allowed.
definitions = []
return {
'record': record,
'slv_declarations': slv_declarations,
'slv_definitions': slv_definitions,
'definitions': definitions,
}


def make_use_clauses(enty):
# Generate use clauses required by the testbench.
use_clauses = '\n'.join([
'use {}.{}.{};'.format(u.library, u.design_unit, u.name_within)
for u in enty.uses.values()])
use_clauses += '\n' + '\n'.join([
'use {}.{}_slvcodec.{};'.format(u.library, u.design_unit, u.name_within)
for u in enty.uses.values()
if u.library not in ('ieee', 'std') and '_slvcodec' not in u.design_unit])
return use_clauses


def make_generic_params(enty, default_generics=None):
# Get the list of generic parameters for the testbench.
if default_generics is None:
default_generics = {}
else:
default_generics = default_generics.copy()

generic_params = []
for k, v in default_generics.items():
if isinstance(v, str) and (len(v) > 0) and (v[0] != "'"):
default_generics[k] = '"' + v + '"'

for g in enty.generics.values():
as_str = '{}: {}'.format(g.name, g.typ)
if g.name in default_generics:
as_str += ' := {}'.format(default_generics[g.name])
as_str += ';'
generic_params.append(as_str)
generic_params = '\n'.join(generic_params)[:-1]
return generic_params


def make_filetestbench_multiple_clocks(
enty, clock_domains, add_double_wrapper=False,
default_output_path=None, default_generics=None,
clock_periods=None, clock_offsets=None):
'''
Generate a testbench that reads inputs from a file, and writes outputs to
a file.
Args:
`enty`: A resolved entity object parsed from the VHDL.
`clock_domains`: An optional dictionary that maps clock_names to the
signals in their clock domains.
'''
grouped_ports = enty.group_ports_by_clock_domain(clock_domains)
definitions = []
connections = []
for clock_name, inputs_and_outputs in grouped_ports.items():
inputs, outputs = inputs_and_outputs
input_info = process_signals(inputs, 't_{}_inputs'.format(clock_name))
output_info = process_signals(outputs, 't_{}_outputs'.format(clock_name))
definitions += input_info['definitions'] + output_info['definitions']
for p in inputs:
connections.append('{} => {}_input_data.{}'.format(p.name, clock_name, p.name))
for p in outputs:
connections.append('{} => {}_output_data.{}'.format(p.name, clock_name, p.name))

use_clauses = make_use_clauses(enty)
generic_params = make_generic_params(enty, default_generics)

clock_names = list(clock_domains.keys())
# Combine the input and output record definitions with the slv conversion
# functions.
definitions = '\n'.join(definitions)
clk_connections = '\n'.join(['{} => {}_clk,'.format(clk, clk)
for clk in clock_names])
connections = ',\n'.join(connections)
dut_generics = ',\n'.join(['{} => {}'.format(g.name, g.name)
for g in enty.generics.values()])
# Read in the testbench template and format it.
template_fn = os.path.join(os.path.dirname(__file__), 'templates',
'file_testbench_multiple_clocks.vhd')
if add_double_wrapper:
dut_name = enty.identifier + '_toslvcodec'
else:
dut_name = enty.identifier
if clock_periods is None:
clock_periods = {}
if clock_offsets is None:
clock_offsets = {}

# Check that the first clock has signals.
# This is important since this is the file that will determine when the
# simulation terminates.
assert clock_domains[clock_names[0]]

clock_infos = [(name, clock_periods.get(name, '10 ns'), clock_offsets.get(name, '0 ns'),
len(clock_domains[name]) > 0)
for name in clock_names]
with open(template_fn, 'r') as f:
filetestbench_template = jinja2.Template(f.read())
filetestbench = filetestbench_template.render(
test_name='{}_tb'.format(enty.identifier),
use_clauses=use_clauses,
generic_params=generic_params,
definitions=definitions,
dut_generics=dut_generics,
dut_name=dut_name,
clk_connections=clk_connections,
connections=connections,
clock_names=clock_names,
clock_infos=clock_infos,
output_path=default_output_path,
)
return filetestbench


def prepare_files(directory, filenames, top_entity, add_double_wrapper=False, use_vunit=True,
dut_directory=None, default_generics=None, default_output_path=None):
dut_directory=None, default_generics=None, default_output_path=None,
clock_domains=None, clock_periods=None, clock_offsets=None):
'''
Parses VHDL files, and generates a testbench for `top_entity`.
Returns a tuple of a list of testbench files, and a dictionary
Expand All @@ -170,9 +301,15 @@ def prepare_files(directory, filenames, top_entity, add_double_wrapper=False, us
os.path.join(config.vhdldir, 'clock.vhd'),
]
# Make file testbench
ftb = make_filetestbench(resolved_entity, add_double_wrapper, use_vunit=use_vunit,
default_generics=default_generics,
default_output_path=default_output_path,)
if clock_domains and len(clock_domains) > 1:
ftb = make_filetestbench_multiple_clocks(
resolved_entity, clock_domains, add_double_wrapper, default_generics=default_generics,
default_output_path=default_output_path,
clock_periods=clock_periods, clock_offsets=clock_offsets)
else:
ftb = make_filetestbench(resolved_entity, add_double_wrapper, use_vunit=use_vunit,
default_generics=default_generics,
default_output_path=default_output_path,)
ftb_fn = os.path.join(directory, '{}_tb.vhd'.format(
resolved_entity.identifier))
with open(ftb_fn, 'w') as f:
Expand Down
6 changes: 3 additions & 3 deletions slvcodec/package_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ def make_record_declarations_and_definitions(record_type):
name, subtype = name_and_subtype
indices_names_and_widths.append(
(index, name, math_parser.str_expression(subtype.width)))
definitions = definitions_template.render(
type=record_type.identifier,
indices_names_and_widths=indices_names_and_widths)
definitions = definitions_template.render(
type=record_type.identifier,
indices_names_and_widths=indices_names_and_widths)
return declarations, definitions


Expand Down