| # lowRISC Hardware Primitives |
| |
| ## Concepts |
| |
| This directory contains basic building blocks to create a hardware design, |
| called primitives. A primitive is described by its name, and has a well-defined |
| list of ports and parameters. |
| |
| Under the hood, primitives are slightly special, as they can have multiple |
| implementations. In contrast to many other modules in a hardware design, |
| primitives must often be implemented in technology-dependent ways. For example, |
| a clock multiplexer for a Xilinx FPGA is implemented differently than one for |
| a specific ASIC technology. |
| |
| Not all primitives need to have multiple implementations. |
| |
| * Primitives with a single, generic, implementation are normal SystemVerilog |
| modules inside the `hw/ip/prim/rtl` directory. We call these primitives |
| "technology-independent primitives". |
| * Primitives with multiple implementations have only a FuseSoC core file in the |
| `hw/ip/prim` directory. The actual implementations are in "technology |
| libraries". We call these primitives "technology-dependent primitives". |
| |
| ### Abstract primitives |
| |
| Abstract primitives are wrappers around technology-dependent implementations of |
| primitives, with the ability to select a specific implementation if needed. |
| |
| In more technical terms, abstract primitives are SystemVerilog modules. The |
| example below shows one. |
| |
| ```systemverilog |
| `ifndef PRIM_DEFAULT_IMPL |
| `define PRIM_DEFAULT_IMPL prim_pkg::ImplGeneric |
| `endif |
| |
| module prim_pad_wrapper |
| #( |
| parameter int unsigned AttrDw = 6 |
| ) ( |
| inout wire inout_io, // bidirectional pad |
| output logic in_o, // input data |
| input out_i, // output data |
| input oe_i, // output enable |
| // additional attributes {drive strength, keeper, pull-up, pull-down, open-drain, invert} |
| input [AttrDw-1:0] attr_i |
| ); |
| parameter prim_pkg::impl_e Impl = `PRIM_DEFAULT_IMPL; |
| |
| if (Impl == prim_pkg::ImplGeneric) begin : gen_generic |
| prim_generic_pad_wrapper u_impl_generic ( |
| .* |
| ); |
| end else if (Impl == prim_pkg::ImplXilinx) begin : gen_xilinx |
| prim_xilinx_pad_wrapper u_impl_xilinx ( |
| .* |
| ); |
| end else begin : gen_failure |
| // TODO: Find code that works across tools and causes a compile failure |
| end |
| |
| endmodule |
| ``` |
| |
| As seen from the source code snippet, abstract primitives have the following |
| properties: |
| |
| - They have an `Impl` parameter which can be set to choose a specific |
| implementation of the primitive. |
| - The `Impl` parameter is set to a system-wide default determined by the |
| `PRIM_DEFAULT_IMPL` define. |
| - All ports and parameters of the abstract primitive are forwarded to the |
| implementations. |
| |
| ### Technology libraries |
| |
| Technology libraries collect implementations of primitives. |
| |
| At least one technology library must exist: the `generic` technology library, |
| which contains a pure-SystemVerilog implementation of the functionality. This |
| library is commonly used for simulations and as functional reference. The |
| `generic` technology library is contained in the `hw/ip/prim_generic` directory. |
| |
| In addition to the implementation in the `generic` library, primitives may be |
| implemented by as many other libraries as needed. |
| |
| Technology libraries are referenced by their name. |
| |
| ### Technology library discovery |
| |
| In many cases, technology libraries contain vendor-specific code which cannot be |
| shared widely or openly. Therefore, a FuseSoC looks for available technology |
| libraries at build time, and makes all libraries it finds available. |
| |
| The discovery is performed based on the agreed-on naming scheme for primitives. |
| |
| - FuseSoC scans all libraries (e.g. as specified by its `--cores-root` command |
| line argument) for cores. |
| - All cores with a name matching `lowrisc:prim_TECHLIBNAME:PRIMNAME` |
| are considered. `TECHLIBNAME` is then added to the list of technology |
| libraries. |
| |
| After the discovery process has completed, a script (`primgen`) creates |
| - an abstract primitive (see above), and |
| - an entry in the `prim_pkg` package in the form of `prim_pkg::ImplTechlibname` |
| to identify the technology library by its name. |
| |
| ## User Guide |
| |
| ### Use primitives |
| |
| Primitives are normal SystemVerilog modules, and can be used as usual: |
| * instantiate it like a normal SystemVerilog module, and |
| * add a dependency in the FuseSoC core file. |
| |
| Technology-dependent primitives have an additional parameter called `Impl`. |
| Set this parameter to use a specific implementation of the primitive for this |
| specific instance. For example: |
| |
| ```systemverilog |
| prim_ram_2p #( |
| .Width (TotalWidth), |
| .Depth (Depth), |
| // Force the use of the tsmc40lp technology library for this instance, instead |
| // of using the build-time default. |
| .Impl(prim_pkg::ImplTsmc40lp) |
| ) u_mem ( |
| .clk_a_i (clk_i), |
| ... |
| ) |
| ``` |
| |
| |
| ### Set the default technology library |
| |
| If no specific technology library is chosen for an instantiated primitive the |
| default library is used. The SystemVerilog define `PRIM_DEFAULT_IMPL` can be |
| used to set the default for the whole design. Set this define to one of the enum |
| values in `prim_pkg.sv` in the form `prim_pkg::ImplTechlibname`. `Techlibname` |
| is the capitalized name of the technology library. |
| |
| In the top-level FuseSoC core file the default technology library can be chosen |
| like this: |
| |
| ```yaml |
| # my_toplevel.core |
| |
| # Declare filesets and other things (omitted) |
| |
| parameters: |
| # Make the parameter known to FuseSoC to enable overrides from the |
| # command line. If not overwritten, use the generic technology library. |
| PRIM_DEFAULT_IMPL: |
| datatype: str |
| paramtype: vlogdefine |
| description: Primitives implementation to use, e.g. "prim_pkg::ImplGeneric". |
| default: prim_pkg::ImplGeneric |
| |
| targets: |
| fpga_synthesis: |
| filesets: |
| - my_rtl_files |
| parameters: |
| # Use the xilinx technology library for this target by default. |
| - PRIM_DEFAULT_IMPL=prim_pkg::ImplXilinx |
| toplevel: my_toplevel |
| ``` |
| |
| |
| ### Create a technology library |
| |
| To create a technology library follow these steps: |
| |
| - Choose a name for the new technology library. Names are all lower-case. |
| To ease sharing of technology libraries it is encouraged to pick a very |
| specific name, e.g. `tsmc40lp`, and not `asic`. |
| - Copy the `prim_generic` folder into an arbitrary location (can be outside |
| of this repository). Name the folder `prim_YOURLIBRARYNAME`. |
| - Replace the word `generic` everywhere with the name of your technology |
| library. This includes |
| - file and directory names (e.g. `prim_generic_ram1p.sv` becomes |
| `prim_tsmc40lp_ram1p.sv`), |
| - module names (e.g. `prim_generic_ram1p` becomes `prim_tsmc40lp_ram1p`), and |
| - all other references (grep for it!). |
| - Implement all primitives. Replace the module body of the generic |
| implementation with a technology-specific implementation as needed. Do *not* |
| modify the list of ports or parameters in any way! |
| |
| ## Implementation details |
| |
| Technology-dependent primitives are implemented as a FuseSoC generator. The |
| core of the primitive (e.g. `lowrisc:prim:rom` in `prim/prim_rom.core`) calls |
| a FuseSoC generator. This generator is the script `util/primgen.py`. As input, |
| the script receives a list of all cores found by FuseSoC anywhere in its search |
| path. The script then looks through the cores FuseSoC discovered and extracts |
| a list of technology libraries out of it. It then goes on to create the |
| abstract primitive (copying over the list of parameters and ports from the |
| generic implementation), and an associated core file, which depends on all |
| technology-dependent libraries that were found. |