.. _introduction: Quick introduction ================== In this quick introduction, we will consider describing a gear that might be used as some kind of filter. It will feature two pipelined MAC operations and a multiplication at the end, and use three coefficients *b0*, *b1* and *b2* for the calculation:: from pygears import gear @gear def filter(x, b0, b1, b2): x1 = mac(x, b0) x2 = mac(x1, b1) return x2 * b2 Notice the *@gear* decorator which will tells **PyGears** to treat this functions as a HDL module. It also allows for partial application and polymorphism which are not natively supported by the Python language. The variables *x, b0, b1, b2, x1, x2* are interface objects and represent connections between modules. Input arguments *x, b0, b1, b2* correspond to the input ports of the HDL module. In **PyGears** the function call corresponds to the HDL module instantiation. The *mac* gear will return an interface object, as all gears are required to do. Returned interface object corresponds to the output port connection from the MAC module, and can be passed to some other gear which will make the connection from the MAC's output to the this gear's input. Additionally, **PyGears** interfaces support some of the Python operators ('*' in this example) and can be used to infer corresponding HDL modules. The above gear describes the following composition: - first inputs *x* and *b0* are connected to the MAC module, - output of the first MAC and the input *b1* are fed to the second MAC module, - output of the second MAC is multiplied with *b2* which is connected to the output port of the *filter* module *Filter* gear can now be used in the design, by calling it as a function and supplying the 4 arguments, which will in HDL terms instantiate the *filter* module. The output of the *filter* gear is directly the interface object returned by the multiplication operator. If we have implementation of the MAC module in HDL, a gear wrapper needs to be provided, so that it can be used with **PyGears**:: from pygears import gear from pygears.typing import Uint @gear async def mac(a: Uint['w_a'], b: Uint['w_b']) -> Uint['w_a + w_b']: pass For the gears that are implemented in HDL, return type needs to be specified so that **PyGears** can infer the output interface object type, as opposed to the *filter* gear description, where the multiplication submodule was responsible for forming the output interface object, and the *filter* only passed it through. A generic version of the *mac* gear is described above, where it accepts interfaces of variable sized unsigned integers - Uint type. Generic types are described by using strings ('w_a', 'w_b' and 'w_a + w_b') for some of its parameters. These strings are resolved differently for input and output types. For the input types, the strings are resolved when the gear is called and the supplied arguments are matched against parameterized type definitions. If the matching succeeds, the values for the parameters are extracted and can be used for resolving the output types. Uint['w_a'] type maps to a logic vector in HDL with length *w_a*. The output type will thus have the number of bits equal to the sum of *w_a* and *w_b*. If some a type other than Uint is supplied to *mac*, the exception will be raised. Pipe operator ------------- Infix composition operator '|', aka pipe, is also supported, hence the module can be rewritten as:: from pygears import gear @gear def filter(x, b0, b1, b2): y = x | mac(b=b0) | mac(b=b1) return y * b2 This expression will unfold in the following manner: - Two versions of the MAC gears will be prepared by using function partial application, one where *b0* is passed for its argument *b* and the other where *b1* is passed for its argument *b*. In terms of the HDLs, this corresponds to one MAC module with interface *b0* connected to its *b* port and the other with *b1* interface connected to its *b* port. MAC modules are not instantiated at this moment since they didn't receive all required arguments. - Input *x* is piped to the first partially applied MAC gear and it is passed as its first argument *a*. At this moment, all required arguments are supplied to it, and *mac* gear is called. Types of the supplied arguments are checked, parameters and output type are resolved. Since *mac* gear contains no body, an interface object is created with the resolved output type and returned from the function. Variable number of arguments ---------------------------- Gears with variable number of arguments are supported using the Python mechanism for functions with variable number of arguments. Below an implementation of the variable size *filter* gear is given:: from pygears import gear @gear def filter(x, *b): y = x for bi in b[:-1]: y = y | mac(b=bi) return y * b[-1] Now, depending on the number of arguments supplied to the *filter* gear, corresponding number of MAC stages will be instantiated. Gear parameters --------------- Since all gear arguments are required to be interface objects, **PyGears** uses Python keyword-only argument mechanism to supply additional parameters to gears. In the following example, we will implement *filter* as a higher-order function, so that the filter stage can be implemented using an arbitrary gear, instead of it being fixed to the *mac* gear:: from pygears import gear @gear def filter(x, *b, stage): y = x for bi in b[:-1]: y = y | stage(b=bi) return y * b[-1] Gear parameters can be made optional, by supplying the default value:: from pygears import gear @gear def filter(x, *b, stage=mac): y = x for bi in b[:-1]: y = y | stage(b=bi) return y * b[-1] Type casting ------------ In the previous example, if *mac* gear is used, after each stage the interface size will increase, which is usually not the desired implementation. We can keep constant interface size by using type casting after each stage:: from pygears import gear @gear def filter(x, *b, stage=mac): y = x for bi in b[:-1]: y = (y | stage(b=bi)) >> x.dtype return y * b[-1] Interface type can be accessed via its *dtype* attribute. Let's for the sake of an example leave-out the type cast of the last multiplication. Multiplication operator will increase the size of the output interface to accommodate for the largest possible multiplication product. SystemVerilog generation ------------------------ SystemVerilog is generated by instantiating desired gears and calling **PyGears** *hdlgen* function. Here is an example of how this works for the *filter* gear:: from pygears import gear, Intf from pygears.typing import Uint from pygears.hdl import hdlgen @gear async def mac(a: Uint['w_a'], b: Uint['w_b']) -> Uint['w_a + w_b']: acc = Uint[a.dtype.width + b.dtype.width](0) while True: async with a as d_a, b as d_b: acc += d_a * d_b yield acc @gear def filter(x, *b, stage=mac): y = x for bi in b[:-1]: y = (y | stage(b=bi)) >> x.dtype return y * b[-1] x = Intf(Uint[16]) b = [Intf(Uint[16])]*4 iout = filter(x, *b) assert iout.dtype == Uint[32] hdlgen('/filter', outdir='~/filter_svlib') Since we are only interested in generating SystemVerilog files for the *filter* gear, it will be the only gear we will instantiate. Since *filter* needs to be passed input interfaces, we will manually instantiate interface objects of the desired type and pass them to the *filter*. Output interface of the *filter* is not needed, and we only used it to check whether we got correct output type (which is of course optional). Since we called *filter* with four coefficient interfaces *b* and didn't supply an alternative to the default *mac* stage, we will get a *filter* implementation with four MAC stages. **PyGears** will maintain a hierarchy of the instantiated gears in which each gear has been assigned a name. By default, gear instance gets the name of the function used to describe it. In this case, *filter* instance will be named 'filter'. Instances in the hierarchy can be accessed by via the path string. Path string follows the conventions of the Unix path syntax, where root '/' is auto-generated container for all the top gear instances (i.e. the ones not instantiated within other gears). In this case *filter* is one such gear, hence it is directly below root '/filter'. The *mac* gears are instantiated from within the *filter*, so their paths will be: '/filter/mac0', '/filter/mac1', '/filter/mac2' and '/filter/mac3'. So, if some gear instances have the same names on the same hierarchical level, their names will be suffixed with an increasing sequence of integers. Finally, it is possible to supply a custom name via gear *name* builtin parameter. This parameter is added by the *@gear* operator and need not be supplied in the function signature:: filter(x, *b, name="filt") Function *hdlgen* will generate needed hierarchical SystemVerilog modules with correct connections and instantiations of the submodules. In this example, HDL needs to be generated only for the *filter*. Other modules: *mac* and multiplication are already considered described in HDL. Hence, a single file 'filter.sv' will be generated inside '~/filter_svlib' folder.