Python API

The Python API provides the ability to build systems from C components. Designs can be simulated using Python as a rich verification environment. Designs can be converted into Verilog and targeted to FPGAs using the FPGA vendors synthesis tools.

from chips.api.api import *
class chips.api.api.Chip(name)

A chip is a canvas to which you can add inputs outputs, components and wires. When you create a chips all you need to give it is a name.

mychip = Chip("mychip")

The interface to a Chip may defined by calling Input and Output objects. Each input and output object is given a name. The name will be used in the generated Verilog. While any string can be used for the name, if you intend to generate Verilog output, the name should be a valid Verilog identifier.

Input(mychip, "input_1")
Input(mychip, "input_2")
Output(mychip, "output_1")

The implementation of a chip is defined by creating and instancing components. For example a simple adder can be created as follows:

#define a component
adder = Component(C_file = """
        input_1 = input("input_1");
        input_2 = input("input_2");
        output_1 = output("output_1");
        void main(){
            while(1){
                fputc(fgetc(input_1)+fgetc(input_2), output_1);
            }
        }
""", inline=True)

The adder can then be instanced, and connected up by calling the adder. When an added in instanced, the inputs and outputs arguments should be supplied. These dictionaries specify how the inputs and outputs of the component should be connected. The dictionary key should be the input/output name, and the value should be an Input, Output or Wire instance:

#instance a component
my_adder = adder(
    inputs = {
        "input_1" : input_1,
        "input_2" : input_2,
    },
    outputs = {
        "output_1" : output_1,
    },
)

HDLs provide the ability define new components by connecting components together. Chips doesn’t provide a means to do this. There’s no need. A Python function does the job nicely. A function can be used to build a four input adder out of 2 input adders for example:

def four_input_adder(chip, input_a, input_b, input_c, input_d, output_z):

    adder = Component(C_file = """
            a = input("a");
            b = input("b");
            z = output("z");
            void main(){
                while(1){
                    fputc(fgetc(a)+fgetc(b), z);
                }
            }
    """, inline=True)

    wire_a = Wire(chip)
    wire_b = Wire(chip)

    adder(mychip,
        inputs = {"a" : input_a, "b" : input_b},
        outputs = {"z" : wire_a})

    adder(mychip,
        inputs = {"a" : input_c, "b" : input_d},
        outputs = {"z" : wire_b})

    adder(mychip,
        inputs = {"a" : wire_a, "b" : wire_b},
        outputs = {"z" : output_z})

mychip = Chip("mychip")
input_a = Input(mychip, "a")
input_b = Input(mychip, "b")
input_c = Input(mychip, "c")
input_d = Input(mychip, "d")
output_z = Output(mychip, "z")
four_input_adder(mychip, input_a, input_b, input_c, input_d, output_z)

A diagrammatic representation of the Chip is shown below.

       +-------+       +-------+
       | adder |       | adder |
A =====>       >=======>       >=====> Z
B =====>       |       |       |
       +-------+       |       |
                       |       |
       +-------+       |       |
       | adder |       |       |
C =====>       >=======>       |
D =====>       |       |       |
       +-------+       +-------+

Functions provide a means to build more complex components out of simple ones, but it doesn’t stop there. By providing the basic building blocks, you can use all the features of the Python language to build chips.

Ideas:

  • Create multiple instances using loops.
  • Use tuples, arrays or dictionaries to group wires into more complex structures.
  • Use a GUI interface to customise a components or chips.
  • Build libraries of components using modules or packages.
  • Document designs with docutils or sphinx.

There are two ways to transfer data between the python environment, and the Chip simulation.

The first and most flexible method is to subclass the Input and Output classes, overriding the data_source and data_sink methods. By defining your own data_source and data_sink methods, you can interface to other Python code. Using this method allows the simulation to interact with its environment on the fly.

The second simpler method is to employ the Stimulus or Response classes. These classes are themselves inherited from Input and Output. The Stimulus class is provided with a Python sequence object for example a list, and iterator or a generator at the time it is created created. The Response class store data as the simulation progresses, and is itself a sequence object.

It is simple to run the simulation, which should be initiated with a reset:

mychip.simulation_reset()

The simulation can be run for a single cycle:

mychip.simulation_step()

The simulation_run method executes the simulation until all processes complete (which may not happen):

mychip.simulation_run()

There are a couple of methods to terminate the simulation, by waiting for simulation time to elapse, or for a certain amount of output data to be accumulated.

#run simulation for 1000 cycles
while mychip.time < 1000:
    mychip.simulation_step()


#run simulation until 1000 data items are collected
response = Response(chip, "output", "int")
while len(response) < 1000:
    mychip.simulation_step()

Chips designs can be programmed into FPGAs. Chips uses Verilog as its output because it is supported by FPGA vendors build tools. Chips output almost will be compatible with any FPGA family. Synthesisable Verilog code s generated by calling the generate_verilog method.

mychip.generate_verilog()

You can also generate a matching testbench using the generate_testbench method. You can also specify the simulation run time in clock cycles.

mychip.generate_testbench(1000) #1000 clocks

To compile the design in Icarus Verilog, use the compile_iverilog method. You can also run the code directly if you pass True to the compile_iverilog function. This is most useful to verify that chips components match their native python simulations. In most cases Verilog simulations will only be needed to by Chips developers.

mychip.compile_iverilog(True)

The generated Verilog code is dependent on the chips_lib.v file which is output alongside the synthesisable Verilog.

compile_iverilog(run=False)

Synopsis:

chip.compile_iverilog(run=False)

Description:

Compile using the external iverilog simulator.

Arguments:

run: (optional) run the simulation.

Returns:

None
cosim()

Synopsis:

chip.generate_testbench(stop_clocks=None)

Description:

Generate a Verilog testbench.

Arguments:

stop_clocks: The number of clock cycles for the simulation to run.

Returns:

None
cosim_step()

Synopsis:

chip.cosim_step()

Description:

Run cosim for one cycle.

Arguments:

None

Returns:

None
generate_testbench(stop_clocks=None)

Synopsis:

chip.generate_testbench(stop_clocks=None)

Description:

Generate a Verilog testbench.

Arguments:

stop_clocks: The number of clock cycles for the simulation to run.

Returns:

None
generate_verilog()

Synopsis:

chip.generate_verilog(name)

Description:

Generate synthesisable Verilog output.

Arguments:

None

Returns:

None
simulation_reset()

Synopsis:

chip.simulation_reset()

Description:

Reset the simulation.

Arguments:

None

Returns:

None
simulation_run()

Synopsis:

chip.simulation_run()

Description:

Run the simulation until all processes terminate.

Arguments:

None

Returns:

None
simulation_step()

Synopsis:

chip.simulation_step()

Description:

Run the simulation for one cycle.

Arguments:

None

Returns:

None
class chips.api.api.Component(C_file, options={}, inline=False)

The Component class defines a new type of component.

Components are written in C. You can supply the C code as a file name, or directly as a string:

#Call an external C file
my_component = Component(C_file="adder.c")

#Supply C code directly
my_component = Adder(C_file="""
    unsigned in1 = input("in1");
    unsigned in2 = input("in2");
    unsigned out = input("out");
    void main(){
        while(1){
            fputc(fgetc(in1) + fgetc(in2), out);
        }
    }
""", inline=True)

Once you have defined a component you can use the __call__ method to create an instance of the component:

...

adder_1 = Adder(
    chip,
    inputs = {"in1":a, "in2":b},
    outputs = {"out":z},
    parameters = {}
)

You can make many instances of a component by “calling” the component. Each time you make an instance, you must specify the Chip it belongs to, and connect up the inputs and outputs of the Component.

class chips.api.api.Input(chip, name)

An Input takes data from outside the Chip, and feeds it into the input of a Component. When you create an Input, you need to specify the Chip it belongs to, and the name it will be given.

input_a = Input(mychip, "A")
input_b = Input(mychip, "B")
input_c = Input(mychip, "C")
input_d = Input(mychip, "D")

In simulation, the Input calls the data_source member function, to model an input in simulation, subclass the Input and override the data_source method:

from chips.api.api import Input

stimulus = iter([1, 2, 3, 4, 5])

class MyInput(Input):

    def data_source(self):
        return next(stimulus)
data_source()

Override this function in your application

simulation_reset()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_reset() instead

simulation_step()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_step() instead

simulation_update()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_update() instead

class chips.api.api.Output(chip, name)

An Output takes data from a Component output, and sends it outside the Chip. When you create an Output you must tell it which Chip it belongs to, and the name it will be given.

In simulation, the Output calls the data_source member function, to model an output in simulation, subclass the Output and override the data_sink method:

from chips.api.api import Output

class MyOutput(Output):

    def data_sink(self, data):
        print data
data_sink()

override this function in your application

simulation_reset()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_reset() instead

simulation_step()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_step() instead

simulation_update()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_update() instead

class chips.api.api.Wire(chip)

A Wire is a point to point connection, a stream, that connects an output from one component to the input of another. A Wire can only have one source of data, and one data sink. When you create a Wire, you must tell it which Chip it belongs to:

wire_a = Wire(mychip)
wire_b = Wire(mychip)
simulation_reset()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_reset() instead

simulation_update()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_update() instead

class chips.api.api.Stimulus(chip, name, type_, sequence)

Stimulus is a subclass of Input. A Stimulus input provides a convenient means to supply data to the Chips simulation using any python sequence object for example a, list, an iterator or a generator.

from chips.api.api import Sequence

mychip = Chip("a chip")
...

Sequence(mychip, "counter", "int", range(1, 100))

mychip.simulation_reset()
mychip.simulation_run()
data_source()

This is a private function, you shouldn’t need to call this directly.

simulation_reset()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_reset() instead

class chips.api.api.Response(chip, name, type_)

Response is a subclass of Output. A Response output provides a convenient means to extract data to the Chips simulation. A response behaves as a Python iterator.

from chips.api.api import Response

mychip = Chip("a chip")
...

sinx = Response(mychip, "sinx", "int")

mychip.simulation_reset()
mychip.simulation_run()

plot(sinx)
data_sink(value)

This is a private function, you shouldn’t need to call this directly.

simulation_reset()

This is a private function, you shouldn’t need to call this directly. Use Chip.simulation_reset() instead

class chips.api.api.VerilogComponent(C_file, V_file, options={}, inline=False)

The VerilogComponent is derived from Component. The VerilogComponent does not use the C Compiler to generate the Verilog implementation, but allows the user to supply Verilog directly. This is useful on occasions when hand-crafted Verilog is needed in a performance critical section, or if some pre-existing Verilog code needs to be employed.

my_component = Adder(C_file="adder.c", V_file="adder.v")