Part 4 - VHDL Export

Exporting from Gatery to VHDL

A diagram showing a sequence from gatery throught vhdl to icons of various vhdl-ingesting tools.

In the previous tutorial parts we created logic circuits, but so far we could not “extract” them from gatery. In this part, we will look at how to export the design to VHDL so that it can be used in regular synthesis tools and simulators such as Quartus, Vivado, Modelsim or GHDL.

  1. Structuring the Export
    1. Signal Naming
    2. Modules/Entities
  2. Post Processing
  3. Invoking the Export
    1. Targeting Specific Synthesis Tools
  4. Conclusion

Structuring the Export

The exported VHDL code is, at the end of the day, machine generated code and as such will never be on the same level as hand written VHDL code. But depending on the workflow and tools, it can be important to actually look at the VHDL code and to have some structure and stability in it. Gatery allows some level of control over the generated code, specifically over the signal naming and the formation of entities.

Signal Naming

The signals that are most likely to interact with hand written VHDL code are the IO-pins and clocks/resets (the latter are discussed in a later part of this tutorial). These allow explicitly setting a desired name (identifier) that will be used for the signal in the top level entity. If these desired names are unavailable (because they clash with a VHDL keyword or the identifier of a different signal), gatery will postfix them with increasing numbers to ensure that the VHDL code is still valid.

    UInt push_buttons = pinIn(4_b).setName("push_buttons");
    Bit single_button = pinIn().setName("that_single_button_that_is_all_alone");

    UInt color_led = 3_b;
    color_led = ...;
    pinOut(color_led).setName("led");

    Bit led = ...;
    pinOut(led).setName("led"); // Clashes with previous pinOut, one of them will be renamed to led_2

Signals can also be given explicit names with a similar behavior (name clashes are resolved by postfixing with numbers). Signals are named with the setName(...) function. A special preprocessor macro HCL_NAMED can be used to automatically set the C++ identifier as the desired name. It is important to note that the given name is always associated to that current state of the signal.

    UInt v = 32_b;
    v = ...;

    // (try to) name the current state of a "initial_v" in the VHDL code
    setName(v, "initial_v");

    v += 2;

    // (try to) name this new, incremented state of v "modified_v" in the VHDL code
    setName(v, "modified_v");

    UInt some_other_vector = v;

    // (try to) name some_other_vector "some_other_vector" in the VHDL code
    HCL_NAMED(some_other_vector);

Structs must be annotated with boost-hana in order for naming to propagate (recursively) to the members:

struct MyFloat {
    // Signals
    UInt mantissa;
    UInt exponent;
    Bit sign;
    
    // Meta Information
    unsigned biasOffset;
};

// This must list all members. It also must be
// outside of any namespaces. See Boost-Hana
// documentation for more details
BOOST_HANA_ADAPT_STRUCT(MyFloat, mantissa, exponent, sign, biasOffset);

int main() {
// ...

    MyFloat myFloat;
    myFloat = ...;

    HCL_NAMED(myFloat);
    // myFloat.mantissa will now on export be named "myFloat_mantissa", etc.

This even works for signals in containers:

#include <vector>
// ...

int main() {
    // ...

    std::vector<Bit> allTheBits;
    allTheBits.resize(42);
    allTheBits[0] = ...;
    // ...


    HCL_NAMED(allTheBits);
    // allTheBits[0] will now on export be named "allTheBits_0", etc.

Sometimes, the export needs to instantiate VHDL-signals (or variables) which have not been named explicitly. In this case, the name is composed of the names and operations out of which the signal is composed. However, it can be tricky to relate the name back to the gatery code, so if in doubt, proper naming is to be preferred.

Modules/Entities

By default, the VHDL export will create one top level entity with one combinatorial process (and one clocked process per clock and reset configuration, more on clocks later). That is, everything is combined into one entity, which can be extremely cumbersome when synthesis errors or timing reports refer to specific signals or paths.

Entity instance names and signal naming RTL-designs are usually structured as a tree of entity instantiations. Each instantiation of a sub entity is given an instantiation name to differentiate between different instantiations of the same entity. When a tool refers to a specific signal (or path of signals), the concatenation of the signal's name and all instance names leading up to it is concatenated to yield a unique and easy to follow identifier. This can be thought of like a semi-manual stack trace: It not only specifies which line of code is the culprit, but also how it got to be there.

Gatery allows grouping logic using GroupScopes. Similarly to the DesignScope, everything that happens is always added to the innermost, most recent GroupScope that is active.

    // Some logic (or just rewiring) that will end up in top level entity
    UInt vec = 4_b;
    vec = ...;
    UInt shift_amount = 2_b;
    shift_amount = ...;

    UInt shifted_vec;

    // Built a dynamic bit shifter that will end up in it's own sub-entity
    {
        // This creates the sub-entity
        GroupScope group(GroupScope::GroupType::ENTITY);

        // All logic that is build while the variable "group" exists 
        // (and is not superseeded by another GroupScope) will end up in the sub-entity.
        // vec and shift_amount automatically become inputs of the sub-entity.
        std::vector<UInt> shifted;
        for (auto i : utils::Range(vec.size()))
            shifted.push_back(vec << i);

        shifted_vec = mux(shift_amount, shifted);
    }
    
    // Build more logic in the top level entity
    // shifted_vec automatically becomes an output of the sub-entity
    do_sth_with(shifted_vec);

GroupScopes can be entities or areas. Entities are always translated into VHDL entities. Each nested entity becomes a new entity and respective entity instantiation. Areas on the other hand become processes, blocks, or are fused together, depending on how deep they are nested.

GroupScopes can be given a name. The instance is inferred from it. In the exported VHDL code, these names are used with the already familiar mechanisms for resolving name clashes.

    UInt shifted_vec;

    // Built a dynamic bit shifter that will end up in it's own sub-entity
    {
        // This creates the sub-entity
        GroupScope group(GroupScope::GroupType::ENTITY);
        group.setName("dynamic_bitshifter");

Note that inside gatery, there is not (yet) an actual instantiation mechanism. The preferred way is duplication through C++ for loops, function calls, etc. in order to be fully able to exploit C++ as the meta-level of RTL construction. Entities, created through GroupScopes, are similarly duplicated rather than instantiated. As of now, these duplicates are all exported to VHDL as individual entities without detecting and merging identical ones. In other words, the generated VHDL code will still suffer heavily from code duplication, even when using entities.

Despite this, structuring code into entities has a big benefit on the “generation stability” of the export. While the VHDL generation is perfectly deterministic (the same input always yields the same output), small changes in gatery code can potentially result in large changes in the generated VHDL code. This is an effect well known from other hardware construction languages as well. Lines of code can change around, name clashes can resolve in different ways, etc. Segmenting the export into entities not only makes the exported code much easier to navigate, name clashes and code generation inside an entity is largely unaffected by changes to other entities, thus resulting in much more stable output.

Post Processing

After constructing but before exporting (or simulating) the design must be post processed:

    // Construction 
    // ...

    // Post processing
    design.postprocess();

    // Export or simulation or both
    // ...

Post processing performs tasks such as:

  • Constant folding
  • Graph optimization
  • Signal name propagation
  • Technology mapping
  • Checking for unintentional clock domain crossings
  • Register retiming
  • Memory hazard logic construction
  • Reset logic construction

Some of these operations are necessary to bring the circuit into a valid state for export. Others, such as the optimization steps, can significantly reduce the amount of produced code and increase its readability.

Invoking the Export

Gatery supports VHDL export though the gtry::vhdl::VHDLExport class. An instance must be created with the target path. If a filename is given instead of a path, all entities are exported into a single file.

#include <gatery/export/vhdl/VHDLExport.h>

using namespace gtry;
using namespace gtry::vhdl;

// ....

	VHDLExport vhdl("vhdl/");
	vhdl(design.getCircuit());

Alongside the VHDL code, support files can be created:

	VHDLExport vhdl("vhdl/");
	vhdl.writeProjectFile("files.txt"); // Write a text file listing all .vhd files
	vhdl.writeConstraintsFile("constraints.txt"); // Write a text file listing all constraints
	vhdl.writeClocksFile("clocks.txt"); // Write a text file listing all clocks and their frequency
    vhdl.writeInstantiationTemplateVHDL("instantiationTemplate.vhd");  // Write a code snipped for instantiating the top entity.
	vhdl(design.getCircuit());

Targeting Specific Synthesis Tools

By default, the project and constraint files are simple text files and all signal attributes except for user defined ones are dropped. The exporter can target specific synthesis tools and translate attributes and support files into the flavor of the respective tool.

#include <gatery/export/vhdl/VHDLExport.h>
#include <gatery/scl/synthesisTools/IntelQuartus.h>

using namespace gtry;
using namespace gtry::vhdl;

// ....

	VHDLExport vhdl("vhdl/");
	vhdl.targetSynthesisTool(new IntelQuartus());
	vhdl.writeProjectFile("import_IPCore.tcl"); // Write a .tcl script for adding this entity to an existing project
	vhdl.writeStandAloneProjectFile("IPCore.qsf"); // Write a stand-along project file (+ Modelsim file)
	vhdl.writeConstraintsFile("constraints.sdc"); // Write constraints in Quartus syntax
	vhdl.writeClocksFile("clocks.sdc"); // Write clocks in Quartus syntax
    vhdl.writeInstantiationTemplateVHDL("instantiationTemplate.vhd"); // Write a code snipped for instantiating the top entity.
	vhdl(design.getCircuit());

Gatery also has rudimentary support for Xilinx Vivado and we are actively working on improving and extending the support.

#include <gatery/export/vhdl/VHDLExport.h>
#include <gatery/scl/synthesisTools/XilinxVivado.h>

using namespace gtry;
using namespace gtry::vhdl;

// ....

	VHDLExport vhdl("vhdl/");
	vhdl.targetSynthesisTool(new XilinxVivado());
	vhdl.writeProjectFile("import_IPCore.tcl"); // Write a .tcl script for adding this entity to an existing project
	vhdl.writeConstraintsFile("constraints.xdc"); // Write constraints in Vivado syntax
	vhdl.writeClocksFile("clocks.xdc"); // Write clocks in Vivado syntax
	vhdl(design.getCircuit());

The vhdl export can also track a simulation and write a VHDL based test bench with test vectors captured from the simulation. This will be covered at a later time.

Conclusion

This part showed how to annotate, structure, and prepare the design for export, as well as how to perform the actual export to VHDL. We did not cover how to export test benches, nor how to map parts of the design to vendor specific macros. Both will be discussed in a later part of the tutorial.

The next part of this series will show how to use clocks and registers, either to add state to a circuit or for pipelining purposes.