Tutorial:Basic usage

In this tutorial we will write a simple fixed point multiplier. It serves as a basic example of turning DSP models into HDL in sensible and testable way.

Source code

VHDL conversion

Model and unit-tests

Lets follow the model and test-driven development, it goes like this:

  • Write simplest possible model of what you want to do
  • Experiment with it, dont throw away the experiments, rather turn them into unit-tests
  • Profit (use the same tests to develop and verify HDL code)

Start by defining an model:

from pyha.common.hwsim import HW
import numpy as np


class BigFir(HW):
    def __init__(self, coef):
        self.coef = coef

    def model_main(self, input_list):
        # note that model works on lists
        return np.array(input_list) * self.coef

Note

model_main function is reserved for defining the model. It is not convertable to VHDL, we use it to verify RTL against this.

Note

Pyha operates on classes, they must be derived from pyha.HW.

Next step is to write all the tests we can imagine, later we can use the same tests on RTL code.

One possible test, that pushes some data into the model and compares the output with the expected output.

def test_basic():
    from pyha.simulation.simulation_interface import SIM_MODEL, assert_sim_match, SIM_HW_MODEL, SIM_RTL, SIM_GATE
    dut = BigFir(coef=0.5)
    inputs = [0.1, 0.2, 0.3, 0.2, 0.1]
    expect = [0.05, 0.1, 0.15, 0.1, 0.05]

    assert_sim_match(dut, None, expect, inputs, simulations=[SIM_MODEL])
pyha.simulation.simulation_interface.assert_sim_match(model, types, expected, *x, simulations=None, rtol=1e-05, atol=1e-09, dir_path=None, skip_first=0)

Run bunch of simulations and assert that they match outputs.

Parameters:
  • model – Instance of your class
  • types – If main is defined, provide input types for each argument, all arguments will be automatically casted to these types.
  • expected – Expected output of the simulation. If None, assert all simulations match eachother.
  • x – Inputs, if you have multiple inputs, use *x for unpacking.
  • simulations – Simulations to run and assert:
  • SIM_MODEL: runs model (‘model_main’)
  • SIM_HW_MODEL: runs HW model (‘main’)
  • SIM_RTL: converts to VHDL and runs RTL simulation via GHDL and Cocotb
  • SIM_GATE: runs sources trough Quartus and simulates the generated netlist

Note

If None(default), runs all simulations. SIM_HW_MODEL must be run if SIM_RTL or SIM_GATE are going to run.

Parameters:
  • rtol – Relative tolerance for assertion. Look np.testing.assert_allclose.
  • atol – Absolute tolerance for assertion. Look np.testing.assert_allclose.
  • dir_path – Where are conversion outputs written, if empty uses temporary directory.
  • skip_first – Skip first ‘n’ samples for assertion.

Note

If you use PyCharm you can run unit-tests by right clicking on the function name and selecting ‘Run py.test..’ You may need to set File-Settings-Tools-Python Integrated Tools-Default Test Runner = py.test. You can also run PyTest from console $ pytest

Hardware model

Assuming we have now enough knowledge and unit-tests we can start implementing the Hardware model.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pyha.common.const import Const
from pyha.common.sfix import Sfix, resize, fixed_truncate
from pyha.common.hwsim import HW
import numpy as np


class BigFir(HW):
    def __init__(self, coef):
        self.coef = coef

        # define output registers
        # bounds will be determined during simulation
        self.out_resized = Sfix()

        # constants
        self.coef_f = Sfix(coef, 0, -17)

        # uncomment this and quartus will optimize away multiplication (assuming coef=0.5)
        # self.coef_f = Const(Sfix(coef, 0, -17))

    def main(self, input):
        # this will also infer saturation logic
        # for registers you always assign to self.next
        self.next.out_resized = resize(input * self.coef_f, size_res=input,
                                       round_style=fixed_truncate)

        return self.out_resized

    def model_main(self, input_list):
        # note that model works on lists
        return np.array(input_list) * self.coef

In Line 13, we defined a register named out_resized. It is using lazy-Sfix notation, meaning that the actual bounds are derived from the data you feed into the model.

Note

All the class variables are interpreted as registers, unconvertable types like float or Numpy arrays will be ignord for conversion. All the assignments to registers go trough self.next

Line 16 turns the floating point coef into fixed-point.

Note

main function is reserved for defining the HDL model, this is convertable to VHDL. This is the main entry to the model, you can call other functions if needed.

On line 23 resized result of multiplication is assigned to register. This also infers saturation logic.

Results are returned on line 25, multiple values can be returned in Pyha.

Testing

Only minor modifications are required to adapt the test function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def test_basic():
    from pyha.simulation.simulation_interface import SIM_MODEL, assert_sim_match, SIM_HW_MODEL, SIM_RTL, SIM_GATE
    dut = BigFir(coef=0.5)
    inputs = [0.1, 0.2, 0.3, 0.2, 0.1]
    expect = [0.05, 0.1, 0.15, 0.1, 0.05]

    assert_sim_match(dut,
                     [Sfix(left=0, right=-17)],
                     expect, inputs,
                     simulations=[SIM_MODEL, SIM_HW_MODEL])

On line 8 we added the input signature of our ‘main’ function and on line 10 we added a HW simulation instruction.

Note

SIM_HW_MODEL is Python based simulation, you can use debugger to see how your ‘main’ function is called. Debugger is quite an useful tool in Pyha designs since everything is fully sequentially executed.

Note

You can write models in such way that input signature determines the output types. Your VHDL conversion will depend on this.

Upon running the test:

INFO:Running MODEL simulation!
INFO:Running HW_MODEL simulation!
ERROR:##############################################################
ERROR:##############################################################
ERROR:              "HW_MODEL" failed
ERROR:##############################################################
ERROR:##############################################################

... stack trace ...
AssertionError:
Not equal to tolerance rtol=1e-05, atol=1e-09
E
(mismatch 100.0%)
x: array([ 0.05,  0.1 ,         0.15,       0.05,       0.1])
y: array([ 0.  ,  0.050003,     0.099998,   0.150002,   0.099998])

Hardware simulation failed, looking closely reveals the expected and actual outputs are just delayed by 1.

Alternatively you can use a debug function:

pyha.simulation.simulation_interface.plot_assert_sim_match(model, types, expected, *x, simulations=None, rtol=1e-05, atol=1e-09, dir_path=None, skip_first=0)

Same arguments as assert_sim_match. Instead of asserting it plots all the simulations.

It would output:

_images/basic_plot.png

This is an standard hardware behaviour. Pyha provides special variable self._delay that specifies the delay of the model, it is useful:

  • Document the delay of your blocks
  • Upper level blocks can use it to define their own delay
  • Pyha simulations will adjust for the delay, so you can easily compare to your model.

Note

Use self._delay to match hardware delay against models

After setting the self._delay = 1 in the __init__, we get:

AssertionError:
Not equal to tolerance rtol=1e-05, atol=1e-09
(mismatch 80.0%)
x: array([ 0.05,        0.1 ,       0.15,       0.05,        0.1 ])
y: array([ 0.050003,    0.099998,   0.150002,   0.050003,    0.099998])

Note

rtol=1e-5 requires that ~5 digits after decimal point must match. rtol=1e-4 would require 4 digits to match.

Now values are aligned, but the tolerances are too strict, we are using fixed-point after all. One way to solve this would be to add more bits to fixed-point type, for example Sfix(left=0, right=-19). Better way is to set rtol = 1e-4. We want to keep 18 bit fixed-point numbers because Intel Cyclone FPGAs DSP blocks are of this size.

In general i am okay when simulations pass rtol=1e-3. You may need to adjust atol, when failing numbers are close to 0.

Here is the final code that passes assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pyha.common.const import Const
from pyha.common.sfix import Sfix, resize, fixed_truncate
from pyha.common.hwsim import HW
import numpy as np

class BigFir(HW):
    def __init__(self, coef):
        self.coef = coef

        # define output registers
        # bounds will be determined during simulation
        self.out_resized = Sfix()

        # constants
        self.coef_f = Sfix(coef, 0, -17)

        # uncomment this and quartus will optimize away multiplication (assuming coef=0.5)
        # self.coef_f = Const(Sfix(coef, 0, -17))

        self._delay = 1

    def main(self, input):
        # this will also infer saturation logic
        # for registers you always assign to self.next
        self.next.out_resized = resize(input * self.coef_f, size_res=input,
                                       round_style=fixed_truncate)

        return self.out_resized

    def model_main(self, input_list):
        # note that model works on lists
        return np.array(input_list) * self.coef


def test_basic():
    from pyha.simulation.simulation_interface import SIM_MODEL, assert_sim_match, SIM_HW_MODEL, SIM_RTL, SIM_GATE
    dut = BigFir(coef=0.5)
    inputs = [0.1, 0.2, 0.3, 0.2, 0.1]
    expect = [0.05, 0.1, 0.15, 0.1, 0.05]

    assert_sim_match(dut,
                     [Sfix(left=0, right=-17)],
                     expect, inputs,
                     simulations=[SIM_MODEL, SIM_HW_MODEL],
                     rtol=1e-4)

RTL simulations

Add SIM_RTL to the simulations list.

Note

SIM_RTL converts sources to VHDL and runs RTL simulation by using GHDL simulator.

In case you want to view the converted VHDL files, you can use dir_path option.

Example:

assert_sim_match(dut,
                 [Sfix(left=0, right=-17)],
                 expect, inputs,
                 simulations=[SIM_MODEL, SIM_HW_MODEL, SIM_RTL],
                 dir_path='~/vhdl_conversion')

GATE simulation and Quartus

Add SIM_GATE to the simulations list.

Note

SIM_GATE runs VHDL sources trough Quartus and uses the generated generated netlist for simulation. Use to gain ~full confidence in your design. It is slow!

Running the GATE simulation, will produce ‘quartus’ directory in dir_path. One useful tool in Quartus software is RTL viewer, it can be opened from Tools-Netlist viewers-RTL viewer.

RTL of this tutorial:

_images/basic_rtl.png

Note

Design will be optimized if you mark self.coef as Const, Quartus will use shift instead of multiply.