The Book of Orbit

Orbit is an agile package manager and extensible build tool for hardware description languages (HDL). Its design is driven by two core principles:

  • minimize technical debt associated with evolving HDL codebases
  • tailor to the wide range of users and their possible workflows with a high degree of modularity and extensibility

Orbit's use case is targeted toward anyone interested in developing digital hardware; this includes industrial, academic, and personal settings. Create your next commerical product, university lab assignment, or personal project, using a tool that is tailored to today's advanced development processes.

Sections

This "book" is loosely divided into 4 sections:

  1. Tutorials - Step-by-step lessons working with Orbit
  2. User Guide - General procedures for "how-to" solve common problems
  3. Topic Guide - Explanations that clarify and provide more detail to particular topics
  4. Reference - Technical information

About the Project

Orbit is available free to use and open source to encourage adoption, contribution, and integration among the hardware community. We rely on the open source community for feedback and new ideas, while remaining focused on our design goals and principles.

The project is currently open-source under the GPL-3.0 license and is available on GitHub.

About the Documentation

The documentation system and methodology adopted by Orbit is inspired by Divio.

Getting Started

This section walks you the basic steps to get Orbit up and running on your local computer.

Command Line Notation

Throughout the tutorials and the rest of the book, commands will be shown that are used in the terminal. Lines that are entered in a terminal are denoted by starting with a $. The $ is just a special marker; it is not to be entered with the command. Lines that don't start with a $ are typically the output of the previous command.

There are two processes to obtaining and maintaining Orbit:

  1. Installing
  2. Upgrading

Seeking Help

Orbit is a package manager and development tool. With learning new tools there is always a learning curve. Orbit tries to make it less intimidating to use by offering help and information in a variety of ways:

  • To see a list of common commands and options, just use orbit with no arguments.
  • To view quick summaries on commands, use -h, --help flags.
  • To view more detailed manual pages and information, use orbit help.

But since you are here, complete documentation can be found on this website.

Motivation

If you work in the realm of computers, you may hear the phrase: "Hardware is hard." But have you ever thought about why?

There are many possible answers that may lead to this phrase's existence, such as hardware involves battling real-world physics, or that hardware requires knowledge across a wide range of skills, or that hardware has infinitely many ways to go wrong. However, we decide to focus on one particular reason that arises in the context of digital hardware: a slow and expensive development cycle.

Background

To describe hardware today, you typically use a hardware description language (HDL). The two most promiment HDLs used and supported by electronic design automation (EDA) tools are VHDL and Verilog. HDLs are not software programing languages, but rather a language to describe how a circuit is intended to be structured or function.

The core similarity between describing digital hardware and writing software programs is that the developer must apply the creative process by editing files called source code. Source code is used as an input to a back end process, whether it be compilation, simulation, or synthesis. Software and hardware encounter similar problems when working with source code, although software arguably has better tools that already do a good job in solving some of their problems.

Let's talk about some observations about source code development in the layer above hardware: software. Software development today not only has nice languages to develop with, but the entire ecosystem surrounding that language is a pleasant experience. Take the Rust programming language as an example. Not only does Rust have moden language constructs that forces programmers to rethink how to write their program in a safer way regarding computer memory, but the infrastructure surrounding Rust is very accessible and easy to use, which involves the compiler, code management, and code testing. This is great for software development, but does the same hold true for hardware development?

The problem

At the hardware layer, life is unfortunately not so simple. Not only are the promiment languages being used for things outside of their original intention, but the surrounding infrastructure around these languages is very lacking. Proprietary and expensive tools force developers to adapt to non-standardized practices, making it difficult to share code across systems or other tools, leading to the introduction of the concept called vendor lock-in. Vendor lock-in fragments the community and makes it increasingly difficult to find portable and easily usable source code. Hardware does not have as big of a footprint in the open-source world in comparison to software's vibrant open-source community.

Without the right infrastructure and tools to easily share, reuse, and maintain source code, hardware experiences slow and expensive development cycles. At this point, you be wondering what are the right tools and infrastructure that hardware needs? To answer this question, you may have to shift your perspective about hardware. Thinking of hardware with the waterfall approach of being built and done with is the wrong mindset. Hardware is rarely built once and forgotten about. Instead, one must think of hardware with a more agile approach in that it evolves and improves upon many iterations. This concept of hardware evolving over time requires special attention when creating the supportive infrastructure around it. But this infrastructure and tooling does not exist for the common HDL languages such as VHDL and Verilog.

Without the right infrastructure and tools, many source code maintenance tasks related to development, also known as technical debt, become exponentially more time-consuming and increasingly difficult as time goes on and the code base grows in size and complexity. The technical debt falls upon the developer to manually handle because there is no automated systematic tool to instead handle it. Large technical debt in turn leads to long and expensive hardware development cycles, and therefore it is a common goal to strive to minimize technical debt. The solution at this point is not to continue down the dark path ahead, but instead to take a look at how software managed to minimize this problem: package management.

The solution

With the right tools, hardware development can experience faster development cycles and lower development costs. A tool that can make this experience true is a package manager. A package manager's role is to organize and automate the tasks related to source code management. A well-designed package manager can minimize technical debt, while a poorly-designed package manager simply shifts the technical debt to possibly new and different tasks. Software programming languages have become increasingly good at handling source code management through the creation of their own package managers, such as go mod for Go, Cargo for Rust, and pip for Python.

Having well-designed and easily accessible tools is important to any form of development. Creativity's greatest limiting factor is the tools of which are available to turn an idea into a reality.

Orbit is an agile package manager for HDLs. It automates the process of organizing, maintaining, and reusing HDL source code across projects. Since it regularly interacts with the developer, it is designed to be simple and intuitive to use while highly tolerant against errors. Given the HDL landscape is fragmented and requires various EDA tools to carry out different tasks, Orbit is highly flexible- developers can setup their own targets with minimal effort to execute whatever EDA tool they prefer. Orbit operates as the intermediary between the developer's HDL source code and their EDA tools.

One of the major goals behind Orbit is to support the developer in shifting their perspective of hardware toward a more agile approach in being something that lives and evolves over time. By using Orbit as an HDL package manager, hardware development becomes an enjoyable experience with more of the developer's time devoted to actually creating cool things, not racking up technical debt and fighting to manage millions of files. After all, the saying should be "Hardware is cool", right?

Installing

There are two main methods for getting Orbit running on your computer: downloading a precompiled binary or by using Cargo.

Reminder: By installing and using Orbit, you accept usage under its GPL-3.0 license.

1. Using a precompiled binary

  1. Visit Orbit's releases page on Github to find all of its official releases.
  2. Download the binary for your computer's architecture and operating system.
  3. Install Orbit. Either run the provided install executable or follow the manual instructions for placing Orbit's executable (orbit for Unix and orbit.exe for Windows) in a location recognized by the PATH environment variable.

There are multiple solutions to accomplish step 3. The following outlines one way to manually install Orbit depending on the user's operating system.

Linux

  1. Download the latest prebuilt package.
curl -LO https://github.com/chaseruskin/orbit/releases/download/0.22.1/orbit-0.22.1-x86_64-linux.zip
  1. Unzip the prebuilt package.
unzip orbit-0.22.1-x86_64-linux.zip
  1. Move the executable to a location already set in the PATH environment variable.
mv ./orbit-0.22.1-x86_64-linux/bin/orbit /usr/local/bin/orbit

macOS

  1. Download the latest prebuilt package.
curl -LO https://github.com/chaseruskin/orbit/releases/download/0.22.1/orbit-0.22.1-x86_64-macos.zip
  1. Unzip the prebuilt package.
unzip orbit-0.22.1-x86_64-macos.zip
  1. Move the executable to a location already set in the PATH environment variable.
mv ./orbit-0.22.1-x86_64-macos/bin/orbit /usr/local/bin/orbit

Windows

  1. Open a new terminal (Powershell) to where Orbit was downloaded.

  2. Unzip the prebuilt package.

$ expand-archive "./orbit-0.22.1-x86_64-windows.zip"
  1. Make a new directory to store this package.
$ new-item -path "$env:LOCALAPPDATA/Programs/orbit" -itemtype directory
  1. Move the package to the new directory.
$ copy-item "./orbit-0.22.1-x86_64-windows/*" -destination "$env:LOCALAPPDATA/Programs/orbit" -recurse
  1. Edit the user-level PATH environment variable in Control Panel by adding %LOCALAPPDATA%\Programs\orbit\bin.

2. Installing with Cargo

To install the latest version through Cargo:

$ cargo install --git https://github.com/chaseruskin/orbit.git --bin orbit --tag 0.22.1

This will build the orbit binary and place it a path already set in the PATH environment variable.

Checking if Orbit is installed correctly

To verify Orbit is working correctly on your system, let's open a new terminal session and print it's current version.

$ orbit --version
orbit 0.22.1

This should print out your version of Orbit you installed. Congratulations! You are now ready to begin using Orbit.

Upgrading

Once Orbit is installed, it can be self-upgraded to the latest official released version found on its Github repository.

$ orbit --upgrade

This behavior performs the following strategy:

  1. Removes any executable in the executable's directory starting with orbit- (these are considered stale binaries, such as orbit-0.1.0).

  2. Connects to https://github.com/chaseruskin/orbit/releases to find the most recent released version.

  3. Checks if the most recent version online is ahead of the currently installed version.

Note: If the version online is newer, a prompt will appear to confirm you wish to install the new version. This prompt can be bypassed by adding the --force flag to the previous command.

  1. Downloads the checksum file to a temporary directory to see if there is a prebuilt package available for the current architecture and operating system.

  2. Downloads the package to a temporary directory and computes the checksum to verify the contents.

  3. Renames the current executable by appending its version to the name (marking it as a stale binary, such as orbit-0.1.1).

  4. Unzips the package and moves the new executable to the original executable's location.

Note: If you wish to remove the newly created stale binary after an upgrade, rerunning orbit --upgrade immediately again will perform step 1 and stop at step 3.

Tutorials

This section provides step-by-step instructions through examples of basic ways for interacting with orbit.

Command Line Notation

Throughout the tutorials and the rest of the book, commands will be shown that are used in the terminal. Lines that are entered in a terminal are denoted by starting with a $. The $ is just a special marker; it is not to be entered with the command. Lines that don't start with a $ are typically the output of the previous command.

Before starting the tutorials, it is assumed you possess basic knowledge of the following topics:

  • sending commands through the command-line
  • digital logic concepts
  • navigating your computer's local file system

Note: The HDL code used for this section may be very didactic in nature and is not intended to reflect production-level code or any particular coding style.

First Project: Gates

In this tutorial, you will learn how to:

1. Create an ip from scratch
2. Use Orbit to integrate an entity into a larger design
3. Build a design using a simple target
4. Release a version of an ip

Creating an ip

First, navigate to a directory in your file system where you would like to store the project. From there, let's issue our first orbit command:

$ orbit new gates

A directory called "gates" should now exist and look like the following tree structure:

gates/
└─ Orbit.toml

Let's create our first design unit for describing a NAND gate. Feel free to copy the following code into a file called "nand_gate.vhd" that exists in our project directory "/gates".

Filename: nand_gate.vhd

library ieee;
use ieee.std_logic_1164.all;

entity nand_gate is
  port(
    a, b : in std_logic;
    x : out std_logic
  );
end entity;

architecture rtl of nand_gate is
begin

  x <= a nand b;

end architecture;

Integrating design units

Consider for an instant that our HDL only supports the nand keyword and is missing the other logic gates such as or, and, and xor.

Recalling our basic knowledge of digital circuits, we know a NAND gate is a universal gate, so let's compose other gates using our newly created nand_gate entity. Create a new file for our next design unit to describe an AND gate.

Filename: and_gate.vhd

library ieee;
use ieee.std_logic_1164.all;

entity and_gate is
  port(
    a, b : in std_logic;
    y : out std_logic
  );
end entity;

architecture rtl of and_gate is
begin

    -- What to put here?

end architecture;

After some thinking, we realize we can use two NAND gates together to construct an AND gate. Let's use Orbit to help us integrate our nand_gate entity into the and_gate's architecture.

$ orbit get nand_gate --component --signals --instance
component nand_gate
  port(
    a : in std_logic;
    b : in std_logic;
    x : out std_logic
  );
end component;

signal a : std_logic;
signal b : std_logic;
signal x : std_logic;

uX : nand_gate
  port map(
    a => a,
    b => b,
    x => x
  );

With this single command, Orbit provided us with:

  • the component declaration
  • signals for the port interface
  • an instantiation template

Sweet! After some quick copy/pasting and signal renaming, we have our architecture described for an AND gate.

Filename: and_gate.vhd

library ieee;
use ieee.std_logic_1164.all;

entity and_gate is
  port(
    a, b : in std_logic;
    y : out std_logic
  );
end entity;

architecture rtl of and_gate is
  
  component nand_gate
    port(
      a : in std_logic;
      b : in std_logic;
      x : out std_logic
    );
  end component;

  signal x : std_logic;

begin

  u1 : nand_gate
    port map(
      a => a,
      b => b,
      x => x
    );

  u2 : nand_gate
    port map(
      a => x,
      b => x,
      x => y
    );

end architecture;

Let's make a quick check to verify our and_gate is using our nand_gate.

$ orbit tree
and_gate
└─ nand_gate

Cool! We got a hierarchical view of our top-most design unit.

Building an ip for a scripted workflow

After all of our hard work, we are excited to show off our latest design on the newest Yilinx FPGA that just arrived in the mail. You realize you need a way to get your HDL code to the Yilinx synthesis tool in order to generate the final bitstream for your FPGA.

To make this possible, Orbit builds a project through two stages: planning and execution. Although both stages occur together, users must define their own targets to be invoked during execution. This explicit separation of layers between planning and execution enable the user to tailor the build process to their specific requirements.

Creating a target

A target is a command invoked by Orbit for execution during the build process. In this example, we will write a script and have our target's command call our script to execute our process. In other words, you could say we are targeting the Yilinx tool. Let's make a simple target for our Yilinx synthesis tool using the Python programming language.

Filename: .orbit/yilinx.py

file_order = []
# Read and parse the blueprint file
with open('blueprint.tsv') as blueprint:
    rules = blueprint.readlines()
    for r in rules:
        fileset, lib, path = r.strip().split('\t')
        if fileset == 'VHDL':
            file_order += [(lib, path)]
    pass

# Use the Yilinx tool to perform synthesize on the HDL files
for (lib, path) in file_order:
    print('YILINX:', 'Synthesizing file ' + str(path) +' into ' + str(lib) + '...')

# Use the Yilinx tool to perform placement and routing
print('YILINX:','Performing place-and-route...')

# Use the Yilinx tool to generate the bitstream
print('YILINX:', 'Generating bitstream...')
with open('fpga.bit', 'w') as bitstream:
    bitstream.write('011010101101' * 2)

print('YILINX:','Bitstream saved at: target/yilinx/fpga.bit')

For Orbit to know about our target, we need to give information to Orbit about the target. This is done in a configuration file. For this example, we edit the project-level configurations.

Filename: .orbit/config.toml

[[target]]
name = "yilinx"
description = "Generate bitstreams for Yilinx FPGAs"
command = "python"
args = ["yilinx.py"]

Calling a target

$ orbit build --target yilinx
YILINX: Synthesizing file /Users/chase/tutorials/gates/nand_gate.vhd into gates...
YILINX: Synthesizing file /Users/chase/tutorials/gates/and_gate.vhd into gates...
YILINX: Performing place-and-route...
YILINX: Generating bitstream...
YILINX: Bitstream saved at: target/yilinx/fpga.bit

Typically, we create targets to interface with EDA tools which will in turn produce desired output files, called artifacts. We see Yilinx saved our bitstream artifact for us to program our FPGA. Cool!

Filename: target/yilinx/fpga.bit

011010101101011010101101

Making an ip and its design units reusable

Now we are ready to move on to more advanced topics, so let's go ahead and store an immutable reference to this project to use in other projects in our developer journey.

$ orbit install

This command ran a series of steps that packaged our project and placed it into our cache. Internally, Orbit knows where our cache is and can reference designs from our cache when we request them. Let's make sure our project was properly installed by viewing our entire ip catalog.

$ orbit search
gates                       0.1.0       install

And there it is! Let's continue to the next tutorial, where we introduce dependencies across ips.

Additional notes on project structure

Our final project structure looks like the following:

gates/
├─ .orbit/
│  ├─ config.toml
│  └─ yilinx.py
├─ target/
│  ├─ CACHEDIR.TAG
|  └─ yilinx/
│     ├─ .env
│     ├─ blueprint.tsv
│     └─ fpga.bit
├─ Orbit.toml
├─ Orbit.lock
├─ and_gate.vhd
└─ nand_gate.vhd
  • The configurations stored in "/.orbit" exist only for this project; to store configurations that persist across projects make changes to the $ORBIT_HOME directory.

  • Orbit creates an output directory to store the blueprint and any tool output files during a build. These files should reside in "/target" and may change often during development (probably don't check this directory into version control).

  • Orbit creates a lock file "Orbit.lock" to store all the information required to manage and recreate the exact state of this project. It is a good idea to always keep it and to not manually edit it (probably be sure to check this file into version control).

Dependencies: Half adder

In this tutorial, you will learn how to:

1. Specify an external ip as a dependency
2. Use Orbit to learn more about external ips and their design units
3. Leverage Orbit across ips to integrate an entity into a separate ip

Referencing external ips

After completing the gates project from the previous tutorial ahead of schedule, you take a well deserved vacation. Now you have returned to work and are tasked with building a half adder.

Let's create a new project. Navigate to a directory in your file system where you would like to store the project.

$ orbit new half-add --lib adding

For the rest of this tutorial, we will be working relative to the project directory "/half-add" that was created by the previous command.

Remembering our impressive work with the gates project, we realize we can reuse some of the already designed and tested components from there. Let's make sure it's installed so that we can use it.

$ orbit search gates
gates                       0.1.0       install

Awesome! Our next step is tell Orbit that our current project, half-add, wants to use gates as a dependency.

Add a new entry for gates to the dependencies table in our project's manifest, Orbit.toml.

Filename: Orbit.toml

[ip]
name = "half-add"
library = "adding"
version = "0.1.0"

# See more keys and their definitions at https://chaseruskin.github.io/orbit/reference/manifest.html

[dependencies]
gates = "0.1.0"

We've referenced it, now we have to use it!

Learning about ips

Your memory is a little foggy on what gates actually did, and what entities were available. Luckily, we can query for information through Orbit about ips and their design units.

Let's remember what entities we have at our disposal.

$ orbit view gates --units
and_gate                            entity        public 
nand_gate                           entity        public 

Okay, how did we implement the NAND gate architecture?

$ orbit read --ip gates nand_gate --start architecture
architecture rtl of nand_gate is
begin

  x <= a nand b;

end architecture;

Cool, we had used the VHDL keyword nand to describe that particular circuit. Sometimes it may be insightful to read code snippets and comments from external design units when trying to integrate them into a new project.

Integrating design units across ips

Let's use the NAND gate we previously defined to construct a half adder circuit.

$ orbit get --ip gates nand_gate --library --signals --instance
library gates;

signal a : std_logic;
signal b : std_logic;
signal x : std_logic;

uX : entity gates.nand_gate
  port map(
    a => a,
    b => b,
    x => x
  );

A half adder can be constructed with 5 NAND gates. It's time to copy/paste our NAND gate instances into our new file "half_add.vhd".

Filename: half_add.vhd

library ieee;
use ieee.std_logic_1164.all;

library gates;

entity half_add is
  port(
    a, b : in std_logic;
    c, s : out std_logic
  );
end entity;

architecture rtl of half_add is
  
  signal x4, x1, x2 : std_logic;

begin

  -- 1st layer: This gate creates the first NAND intermediate output.
  u4 : entity gates.nand_gate
    port map(
      a => a,
      b => b,
      x => x4
    );
  
  -- 2nd layer: Perform NAND with input 'a' and the 1st layer's output.
  u1 : entity gates.nand_gate
    port map(
      a => a,
      b => x4,
      x => x1
    );

  -- 2nd layer: Perform NAND with input 'b' and the 1st layer's output.
  u2 : entity gates.nand_gate
    port map(
      a => x4,
      b => b,
      x => x2
    );

  -- 3rd layer: This gate produces the final sum signal ('a' XOR 'b').
  u3 : entity gates.nand_gate
    port map(
      a => x1,
      b => x2,
      x => s
    );

  -- 3rd layer: This gate produces the final carry out signal ('a' AND 'b').
  u5 : entity gates.nand_gate
    port map(
      a => x4,
      b => x4,
      x => c
    );

end architecture;

Let's inspect the design hierarchy to make sure our circuit and its components are identified by Orbit.

$ orbit tree --format long
half_add (half-add:0.1.0)
└─ nand_gate (gates:0.1.0)

Finally, let's install this ip to the cache for future reuse as well. But before we can install any ip to our cache, an ip must have an up to date lockfile.

Lockfiles are updated whenever a user calls orbit build or orbit test, but they can also be updated with the dedicated orbit lock command. Let's go ahead and generate the lockfile now.

$ orbit lock

Now we can safely install the ip to our catalog.

$ orbit install

Nice, now we have successfully reused designs across ips! However, maybe we should have designed all the logic gates in the gates ip...

Additional notes on dependencies

Before integrating a design unit from an external ip into a separate project, it's important to first update the Orbit.toml file. This manifest file has a dependencies section, which allows you to tell Orbit which ips to bring into the current project scope. Without the ips in scope, Orbit may be unable to identify where you got a reference for a particular design unit. Orbit denotes an unknown design unit with a ? when displaying the design hierarchy.

half_add (half-add:0.1.0)
└─ nand_gate ?

After introducing dependencies at the project level, it's also important to maintain an up-to-date lockfile, Orbit.lock. In most cases, Orbit will automatically generate it when it needs it, however, you as the user can also explicitly request Orbit to update the lockfile.

$ orbit lock

The lockfile saves information for Orbit to use later when needing to reconstruct the state of an ip. This includes saving information about all ip dependencies, their checksums, and potential sources of retrieval. Remember, the Orbit.lock file is automatically managed by Orbit and does not require direct user editing.

Gates: Revisited

In this tutorial, you will learn how to:

1. Edit an existing ip
2. Use environment variables and command-line arguments to create more robust targets
3. Release the next version for an existing ip

Editing the gates ip

It seems we left out some logic gates when we last worked on the gates project, so let's implement them now. Navigate to the directory in your file system where you currently store the gates project.

For the rest of this tutorial, we will be working relative to the project directory "/gates" that currently stores the gates project.

Let's implement the OR gate while restricting our design to only NAND gates like before.

$ orbit get nand_gate --signals --instance
library work;

signal a : std_logic;
signal b : std_logic;
signal x : std_logic;

uX : entity work.nand_gate
  port map(
    a => a,
    b => b,
    x => x
  );

An OR gate can be constructed using 3 NAND gates. Let's copy/paste our NAND gate instances into our new file "or_gate.vhd".

Filename: or_gate.vhd

library ieee;
use ieee.std_logic_1164.all;

library work;

entity or_gate is
  port(
    a, b : in std_logic;
    y : out std_logic
  );
end entity;

architecture rtl of or_gate is

  signal x1, x2 : std_logic;

begin
  -- 1st layer: This gate negates the first input 'a'.
  u1 : entity work.nand_gate
    port map(
      a => a,
      b => a,
      x => x1
    );

  -- 1st layer: This gate negates the second input 'b'.
  u2 : entity work.nand_gate
    port map(
      a => b,
      b => b,
      x => x2
    );
    
  -- 2nd layer: This gate produces the final output ('a' OR 'b').
  u3 : entity work.nand_gate
    port map(
      a => x1,
      b => x2,
      x => y
    );

end architecture;

Showing the list of possible design units for the current project should now include the OR gate entity.

$ orbit view --units
and_gate                            entity        public
nand_gate                           entity        public 
or_gate                             entity        public 

Extending the Yilinx target

Next, we want to program our Yilinx FPGA with the OR gate design to test it on the board. However, there are some quick updates we first want to apply to the ".orbit/yilinx.py" script.

  • We want a way to specify which I/O pins of the FPGA will be used during placement and routing
  • We want a way to specify whether to program the FPGA bitstream to SRAM storage (volatile) or flash storage (nonvolatile).

After searching through Yilinx documentation for hours, you learn that the Yilinx design tool can accept .ydc files for FPGA pin assignments. Let's edit our yilinx target to collect any .ydc files our project may have during Orbit's planning step.

Filename: .orbit/config.toml

[[target]]
name = "yilinx"
command = "python"
description = "Generate bitstreams for Yilinx FPGAs"
args = ["yilinx.py"]
# Define the type of extra file(s) to collect during planning
fileset.YDCF = "*.ydc"

Now let's create our pin assignment file for our OR gate design.

Filename: pins.ydc

A1=a
A2=b
C7=y

Next, let's edit the Python script for the yilinx target to allow the Yilinx tool to use our .ydc file if we ever collect one into our blueprint file. We also want to accept command-line arguments to optionally program our FPGA using SRAM or flash storage.

Filename: .orbit/yilinx.py

import sys, os

# Handle command-line arguments
PROG_SRAM = bool(sys.argv.count('--sram') > 0)
PROG_FLASH = bool(sys.argv.count('--flash') > 0)

# Get environment variables set by orbit for this particular build
BLUEPRINT = os.environ.get("ORBIT_BLUEPRINT")
OUTPUT_PATH = os.environ.get("ORBIT_OUTPUT_PATH")
TOP_LEVEL = os.environ.get("ORBIT_TOP")

synth_order = []
constraints_file = None

# Parse the blueprint file created by orbit
with open(BLUEPRINT) as blueprint:
    rules = blueprint.readlines()
    for r in rules:
        fileset, lib, path = r.strip().split('\t')
        if fileset == 'VHDL':
            synth_order += [(lib, path)]
        if fileset == 'YDCF':
            constraints_file = path
    pass

# Run the Yilinx tool from synthesis to bistream generation
for (lib, path) in synth_order:
    print('YILINX:', 'Synthesizing file ' + str(path) + ' into ' + str(lib) + '...')

print('YILINX:','Performing place-and-route...')

# Read the Yilinx design constraints file to map pins to I/O top-level ports.
if constraints_file != None:
    with open(constraints_file, 'r') as ydc:
        mapping = [x.strip().split('=') for x in ydc.readlines()]
    for pin, port in mapping:
        print('YILINX:', 'Mapping pin ' + str(pin) + ' to port ' + str(port) + '...')
    pass

print('YILINX:', 'Generating bitstream...')

BIT_FILE = TOP_LEVEL + '.bit'
with open(BIT_FILE, 'w') as bitstream:
    for byte in [bin(b)[2:] for b in bytes(TOP_LEVEL, 'utf-8')]:
        bitstream.write(byte)

print('YILINX:','Bitstream saved at: '+ str(OUTPUT_PATH + '/' + BIT_FILE))

# Optionally allow the user to program the FPGA using flash or SRAM configuration
if PROG_FLASH == True and PROG_SRAM == False:
    print('YILINX:', 'Programming bitstream to flash...')
elif PROG_SRAM == True:
    print('YILINX:', 'Programming bitstream to SRAM...')
  

With all these changes, we can now go ahead and program our FPGA as we want!

To execute our latest changes to our Yilinx target build process, let's use orbit build. Recall that this command can be divided into two distinct stages: planning and execution. Planning is completed by Orbit once it generates a blueprint file. During planning, it looked at what files the target also requests under it's fileset TOML configuration.

$ orbit build --target yilinx --top or_gate -- --flash

Let's take a look at the blueprint file Orbit created during the build process.

Filename: target/yilinx/blueprint.tsv

YDCF	gates	/Users/chase/tutorials/gates/pins.ydc
VHDL	gates	/Users/chase/tutorials/gates/nand_gate.vhd
VHDL	gates	/Users/chase/tutorials/gates/or_gate.vhd

Notice also how we passed a command-line argument --flash, to our target process for execution. Any arguments that are found after -- are ignored by Orbit and sent directly to the targeted process.

Reviewing the output from our target's execution, we see that our FPGA was programmed successfully.

YILINX: Synthesizing file /Users/chase/tutorials/gates/nand_gate.vhd into gates...
YILINX: Synthesizing file /Users/chase/tutorials/gates/or_gate.vhd into gates...
YILINX: Performing place-and-route...
YILINX: Mapping pin A1 to port a...
YILINX: Mapping pin A2 to port b...
YILINX: Mapping pin C7 to port y...
YILINX: Generating bitstream...
YILINX: Bitstream saved at: target/or_gate.bit
YILINX: Programming bitstream to flash...

As expected, the bitstream is also written and saved within our target's output path.

Filename: target/yilinx/or_gate.bit

1101111111001010111111100111110000111101001100101

Awesome! We added some pretty advanced settings to our yilinx target to make it more robust for future use. Let's configure this target to be used with any of our ongoing projects by editing the global configuration file through the command-line.

$ orbit config --global --append include="$(orbit env ORBIT_IP_PATH)/.orbit/config.toml"

Now when we call Orbit from any directory, we can see our yilinx target is available to use.

$ orbit build --list
yilinx          Generate bitstreams for Yilinx FPGAs

Rereleasing the gates ip

We made changes to the gates ip, and now we want to have the ability to use these new updates or continue using the old changes. To do this, we want to update the version number in the manifest file. Let's edit the Orbit.toml file's version field to contain version "1.0.0".

Filename: Orbit.toml

[ip]
name = "gates"
version = "1.0.0"

# See more keys and their definitions at https://chaseruskin.github.io/orbit/reference/manifest.html

[dependencies]

Finally, let's release version 1.0.0 for the gates ip by installing it to our cache.

$ orbit install

One last look at the catalog shows the latest version of gates we have installed is indeed 1.0.0. Nice work!

$ orbit search gates
gates                       1.0.0     install

Final Project: Full adder

In this tutorial, you will learn how to:

1. Depend on multiple ips for a single project
2. Use Orbit to overcome namespace pollution
3. Build a project with a globally-configured target

Specifying multiple dependencies for an ip

After the quick detour back to the gates ip, we are ready to tackle our final challenge in this mini tutorial series: the full adder. Like our previous projects, navigate to a directory in your file system where you would like to store the project.

orbit new full-add --lib adding

For the rest of this tutorial, we will be working relative to the project directory "/full-add" that currently stores our new full-add project.

For this final project, we will need circuits described in both the gate ip and half-add ip. Let's quickly recall if version 1.0.0 of gates has the OR gate we will need.

$ orbit view gates:1.0.0 --units
and_gate                            entity        public 
nand_gate                           entity        public 
or_gate                             entity        public 

Yup! It's there, and we know we will need some half adders as well. Let's add both ips to our manifest file.

filename: Orbit.toml

[ip]
name = "full-add"
library = "adding"
version = "0.1.0"

# See more keys and their definitions at https://chaseruskin.github.io/orbit/reference/manifest.html

[dependencies]
gates = "1.0.0"
half-add = "0.1.0"

Okay, time to start coding!

Overcoming HDL problems: Namespace pollution

Our full adder circuit will be constructed of 2 half adders and an OR gate. Let's collect some HDL code snippets to use for our full adder circuit.

$ orbit get half_add --ip half-add --library --instance
library adding;

uX : entity adding.half_add
  port map(
    a => a,
    b => b,
    c => c,
    s => s
  );

And let's get the code snippet for the OR gate as well.

$ orbit get or_gate --ip gates:1.0.0 --library --instance
library gates;

uX : entity gates.or_gate
  port map(
    a => a,
    b => b,
    y => y
  );

Let's combine these circuits together into our new file for our full adder implementation.

Filename: full_add.vhd

library ieee;
use ieee.std_logic_1164.all;

library adding;
library gates;

entity full_add is
  port(
    a, b, cin : in std_logic;
    cout, s : out std_logic
  );
end entity;

architecture rtl of full_add is
  
  signal c_ha0, s_ha0, c_ha1 : std_logic;

begin

  -- 1st layer: Peform half of the addition operation.
  u_ha0 : entity adding.half_add
    port map(
      a => a,
      b => b,
      c => c_ha0,
      s => s_ha0
    );

  -- 2nd layer: Compute the final sum term.
  u_ha1 : entity adding.half_add
    port map(
      a => s_ha0,
      b => cin,
      c => c_ha1,
      s => s
    );

  -- 3rd layer: Check both c terms from the half adders for the final cout term.
  u_or0 : entity gates.or_gate
    port map(
      a => c_ha0,
      b => c_ha1,
      y => cout
    );

end architecture;

Our design heirarchy is getting more complex; we have full adders constructed of half adders and OR gates, half adders constructed of NAND gates, OR gates constructed of... uh-oh. More NAND gates.

The NAND gate design unit used in the OR gates is different from NAND gates used in the half adders because they reside in different versions of the gates ip (essentially different ips). So did we just define an NAND gate entity twice with the same identifier? Yes, and thanks to Orbit, this situation is okay.

Huh?

Typical EDA tools will complain and error out when primary design units share the same name. How would they know which one is being used where? Fortunately, Orbit is one step ahead of these tools due to implementing an algorithm called dynamic symbol transformation.

Let's take a look at the design tree hierarchy. You may notice something interesting.

$ orbit tree --format long
full_add (full-add:0.1.0)
├─ or_gate (gates:1.0.0)
│  └─ nand_gate (gates:1.0.0)
└─ half_add (half-add:0.1.0)
   └─ nand_gate_56ade36a78 (gates:0.1.0)

The entities from gates version 0.1.0 and version 1.0.0 are allowed to co-exist in this design. To circumvent EDA tool problems during builds, Orbit appends the beginning checksum digits from the ip of the unit in conflict to the design unit's identifier. Any design units that referenced the unit in conflict will also be updated to properly reference the new identifier for the unit in conflict.

To us though, these slight identifier renamings remain hidden because they occur among indirect dependencies in relation to our current project. When deciding which design unit to rename, Orbit will always choose to rename the unit that is used as an indirect dependency. This key choice allows us to keep using the original unit name when integrating design units into the current project.

Okay, so what?

This may be a silly example, but there is a key takeaway here. Designs are constantly evolving. When creating the latest module, you never know what will come next. By allowing the state of a design to live on while also providing support for new growth, a user no longer spends their time trying to manage compatibility among the increasingly interconnected dependencies. Instead, there exists a freedom to continue to innovate.

Reusing targets that are globally-configured

To conclude this mini tutorial series, let's generate a bitstream for the Yilinx FPGA with our full adder implementation.

First, let's verify our yilinx target is available to us after appending it to our global configuration file in the previous tutorial.

$ orbit build --list
yilinx          Generate bitstreams for Yilinx FPGAs

We can review more details about a particular target by specifying it with the "--target" command-line option while providing "--list" as well.

$ orbit build --list --target yilinx
Name:    yilinx
Command: python "yilinx.py" 
Root:    /Users/chase/tutorials/gates/.orbit
Filesets:
    PIN-FILE        **/*.ydc

Generate bitstreams for Yilinx FPGAs

Let's build our current project using the yilinx target for our full adder.

$ orbit build --target yilinx --top full_add

Opening the blueprint file created by Orbit during the planning stage shows we are indeed using different files for the different NAND gate design units, and the files are in a topologically-sorted order.

Filename: target/yilinx/blueprint.tsv

VHDL	gates	/Users/chase/.orbit/cache/gates-0.1.0-fe9ec9d99e/nand_gate.vhd
VHDL	adding	/Users/chase/.orbit/cache/half-add-0.1.0-1c537df196/half_add.vhd
VHDL	gates	/Users/chase/.orbit/cache/gates-1.0.0-4cb065a539/nand_gate.vhd
VHDL	gates	/Users/chase/.orbit/cache/gates-1.0.0-4cb065a539/or_gate.vhd
VHDL	adding	/Users/chase/tutorials/full-add/full_add.vhd

Inspecting the output displayed to the console shows our target executed it's process successfully with the creation of a .bit file.

YILINX: Synthesizing file /Users/chase/.orbit/cache/gates-0.1.0-fe9ec9d99e/nand_gate.vhd into gates...
YILINX: Synthesizing file /Users/chase/.orbit/cache/half-add-0.1.0-1c537df196/half_add.vhd into adding...
YILINX: Synthesizing file /Users/chase/.orbit/cache/gates-1.0.0-4cb065a539/nand_gate.vhd into gates...
YILINX: Synthesizing file /Users/chase/.orbit/cache/gates-1.0.0-4cb065a539/or_gate.vhd into gates...
YILINX: Synthesizing file /Users/chase/tutorials/full-add/full_add.vhd into adding...
YILINX: Performing place-and-route...
YILINX: Generating bitstream...
YILINX: Bitstream saved at: target/yilinx/full_add.bit

Great work! This marks the end to this tutorial series, but the beginning of your experience with Orbit, an agile package manager and extensible build tool for HDLs.

User Guide

This section provides general solutions to common questions when working with orbit.

Overview

Users interact with Orbit by issuing commands through the command-line interface.

The following illustrates an example development workflow that identifies where some Orbit commands could be used during an ip's development cycle:

Managing Ip

Targeting Builds

Topic Guide

This section provides explanations and clarity to the various systems operated by orbit.

Overview

Orbit is an agile package manager and extensible build tool for HDLs.

Key concepts

  • Orbit manages your ips using a group of file system directories that together make up the Catalog. The catalog has 3 levels that store increasingly more information about a particular ip: Channels, the Archive, and the Cache.

  • An ip's manifest may be stored in a user-defined channel so that a user can find that ip. Running orbit install will download the ip from its defined source found in its channel and create a compressed snapshot of the ip in the archive. Once the compressed snapshot is saved, Orbit will decompress the archived snapshot into an immutable reference of the ip at the cache level. The usage of checksums prevents users from editing ips in the cache.

  • Every ip requires a Manifest file, named Orbit.toml. This is a simple TOML file maintained by the user. The manifest file documents basic metadata about the ip, like its name and version, as well as the ip's list of direct dependencies.

  • An ip saves its world state by storing a Lockfile, called Orbit.lock, alongside the manifest. A lockfile lists all of the resolved ip dependencies required for the local ip and how to retrieve those ips if necessary again. Running orbit lock will build an ip-level graph to resolve the entire ip-level dependency tree and store this information in the lockfile.

  • Users customize their experience with Orbit using Configurations, which collections of Targets, Settings, and Protocols. All of these items are defined in an Orbit configuration file, called config.toml. Configurations allow users to reuse and share their workflows across teams and ips.

  • To build (or test) a design within a local ip, Orbit runs a Build Process. The build process takes as input the local ip's Lockfile, Source Files (hdl code), Auxiliary Files (any other file types needed), and a specified Target. Running orbit build (or orbit test) will enter the build process.

  • The build process occurs in 2 stages: the Planning Stage and the Execution Stage. During the planning stage, Orbit generates a Blueprint, which is a single file that lists all the files required to perform the build. During the execution stage, Orbit calls the specified Target's commmand, which typically reads the previously generated blueprint and processes the files using some user-defined EDA tool. The final output from the build process is typically one or more Artifacts, which are one or more files generated from the user-defined EDA tool.

  • Publish a new version of an ip when it is ready by posting it to a user-defined channel. This method enables other users who also have that channel configured to seamlessly discover and access that new version of the ip. Running orbit publish will run a series of checks and then copy the ip's manifest to the proper location within the specified channel.

Other notes

  • Backend EDA tools and workflows (makefiles, TCL scripts, etc.) are decoupled from ip and are able to be reused across projects by creating targets in the configuration file (config.toml).

  • Orbit does not require a version control system (VCS). Orbit is intended to work with any VCS (git, mercurial, svn, etc.).

  • Orbit solves the namespace collision problem by a variant of name mangling when primary design unit identifiers conflict in the dependency tree (dynamic symbol transformation).

  • Orbit generates a lockfile (Orbit.lock) during the planning stage of the build process. The lockfile saves the entire state such that Orbit can return to this state at a later time or on a different computing system. All necessary data that is required to reproduce the build is stored in the lockfile. The lockfile is maintained by Orbit and should be checked into versionc control.

  • Orbit generates a blueprint during the planning stage of the build process. The blueprint is a single file that lists the HDL source code files required for the particular build in topologically sorted order. Targets can also specify other file types to be collected into the blueprint. The blueprint is an artifact to be consumed by the target's process during the exection stage of the build process. Since it can frequently change with each build, it should not be checked into version control.

Agile Package Management

Orbit is an agile package manager for HDLs. Orbit supports a wide range of commands to the user to automate codebase management processes for installing ip, referencing ip, and removing ip.

Installing

orbit install

Before you can reference an ip in your current project, you must first make sure the ip exists on your local filesystem. Orbit manages ip through its ip catalog. The catalog consists of multiple file paths that Orbit maintains for ips at varying states of accessibility.

To make an ip accessible by another project, it must first be installed to your ip catalog.

Referencing

orbit view, orbit get

Once an ip is installed to your ip catalog, the design units of that ip are available to be referenced in the current design.

  1. Tell Orbit which installed ip you wish to use by providing the name and version under the [dependencies] table in the Orbit.toml file.

  2. Instantiate one of the available design units from the dependency in your source code. At this point, any build that uses this HDL source code will be correctly sorted during the planning stage in topological order.

Removing

orbit remove

An ip can be removed from the catalog when it is no longer supported or needed to be used again. By removing the ip, Orbit deletes the ip's contents stored in the catalog, effectively forgetting that it existed.

Extensible Builds

Orbit is an extensible build tool for HDLs. Orbit separates the build process into two stages: planning and execution. When the build process happens, both stages are operated together in sequential order. Orbit provides two entry points into the build process: orbit test and orbit build.

What makes Orbit extensible? Well, Orbit does not define the execution stage by default. It leaves it upon the user to add their own execution processes, called targets. A target can be added through modifying an Orbit configuration file.

Orbit leaves the execution stage undefined by default because there are a wide range of different backend EDA tools available that enforce different requirements and even change requirements and behaviors across versions. It would be a nightmare to try to design a "one-script-fits-all" approach because everyone's computing system and choice of tool is so diverse.

Test or build?

Orbit provides two entry points into the build process: orbit test and orbit build. Each entry point is suited for a particular type of build process.

If you are trying to run a simulation (accompanied by an HDL testbench), then you should use the orbit test entry. This command allows you to enter the build process by specifying the testbench using --tb <unit> and its design-under-test using --dut <unit>. This entry is typically used for verification workflows, where the end result of the build process is more concerned about making sure all steps in the process complete successfully with no errors.

For any non-testing workflow (one that lacks an HDL testbench), then you should use the orbit build entry. This command allows you to enter the build process by specifying the top level using --top <unit>. This entry is typically used for any workflow where the end result of the build process is more concerned about producing output files (commonly called artifacts), such as a bitstream or synthesis report.

Planning

During the planning stage, Orbit resolves all source code dependencies to generate a single file that lists all the necessary source files in topologically sorted order. This file that stores the ordered list of source file paths is called the blueprint.

Orbit sets runtime environment variables that can be accessed during the execution stage by the specified target.

Execution

The execution stage occurs after the planning stage. During the execution stage, Orbit invokes the specified target's command with its set of determined arguments. The arguments to include are taken from the predefined list in the configuration file as well as any additional arguments found on the command line that appear after an empty double switch (--).

Typically, the target's process involves reading the blueprint previously generated from the planning stage and performing some task to generate an artifact. An artifact is what the build produces at the end of its execution, which may be anything, from a synthesis report to a bitstream file.

Catalog

As a package manager, Orbit must know what ips are available and where ips are stored on your local file system so that they can be operated on. Orbit stores your ips in the catalog. The catalog is a set of directories on your local file system maintained by orbit. These directories are typically hidden from the user because they are not regularly interfacing with the file system contents at these locations and manually tampering the contents may cause trouble for Orbit when it tries to use them.

There are three levels to the catalog: the cache, the archive, and channels.

Cache

The cache maintains the ips that are currently installed on your local file system. Installed ips can be immediately added as a dependency to your current project.

Default location: $ORBIT_HOME/cache

Archive

The archive maintains the ips that are currently downloaded on your local file system. Downloaded ips can be added as a dependency to your current project only after being installed to the cache.

Default location: $ORBIT_HOME/archive

Channels

Channels are user-defined directories to set up as registries to maintain the ips that are currently available to download or install. These ips may require internet to download their contents and then install to your cache.

Default location: $ORBIT_HOME/channels

Adding a new channel is as simple as adding a directory to the location where channels are defined.

Ip

Ips are the core component that Orbit operates on as a package manager. First, let's understand some key terms related to ip in the context of Orbit.

Anatomy of an ip

A developer's tasks often involve interfacing with a collection of closely related files (source code, scripts, text files). This collection of closely related files is typically stored under a single directory and is called a project.

The core operations of a package manager revolve around packages. A package is a project with additional information provided by the developer. This "additional information" is called metadata, and it is written to a special file called a manifest. The manifest must be placed at the project's root directory. Without manifests, a package manager would not know which projects it should manage and what each project's current state is in regards to being a package.

In the context of being a package manager for digital hardware, Orbit calls a package an ip. An ip's manifest file is "Orbit.toml", with case-sensitivity.

Working ip

Typically, developers work on one project at a given time (while we can work on projects concurrently, we unfortunately are not parallel processors...yet). The working ip is the ip that is currently being developed at a given moment. The working ip is found by Orbit by checking along the working directory and its parent directories. Some Orbit commands only work when they are called within the working ip (orbit lock, orbit build).

Types of files inside an ip

Since Orbit focuses on digital hardware projects, it automatically detects and manages files that store HDL source code. Files that store HDL source code are called source files. Any other files, such as scripts and test vectors, are considered auxiliary files.

Auxiliary files can be injected into the planning stage by specifying filesets for the given target. A fileset is glob-style pattern that collects matching files under a common name within the working ip. These matched files will appear in the target's generated blueprint file for future execution.

So, what files are inside an ip?

  • Source files: Stores HDL source code (VHDL, Verilog)
  • Auxiliary files: Any additional files that do not store source code
  • Manifest file (Orbit.toml): Stores the ip's metadata provided by the user
  • Lock file (Orbit.lock): Saves the ip's world state for reproducibility purposes

All files except the lock file are expected to be edited by the user. Orbit automatically maintains the lock file to ensure it can reproduce the ip's world state in the future.

Reserved names

File names that begin with ".orbit-" are reserved for internal use and are not allowed at the root directory of an ip. Files that are named with this pattern are used by Orbit in the ip catalog to store additional metadata about the ip.

Ip names

An ip's name is a human-readable name given to an ip so users can easily recall and locate relevant packages.

gates

An ip's specification, more commonly called a spec, is the full resolved name of an ip. As of now, the spec involves the ip's name and ip's version separated by a : character.

gates:1.0.0

When asking Orbit to operate on a particular ip outside of the working ip, Orbit will usually ask you to provide the ip's spec. Orbit uses the spec to lookup the ip in the catalog and then continues operation.

Targets

Orbit operates at the front end of hardware development. At the back end of hardware development is where the true "processing" occurs. A particular "process" that occurs in the back end and produces some result is called a target.

Orbit has no built-in targets. Since hardware development varies widely in the tools available, the systems on which it happens, and the processes that occur, Orbit has not built-in targets. This design choice gives users flexibility in configuring the types of workflows that are most important to them.

At the front end, Orbit frequently interacts with the user to handle operations and run processes within their hardware development workflow. The main role of Orbit is to organize, reference, and prepare HDL source code for the back end.

Targets typically take in as input the blueprint, which is the final output file from Orbit that has prepared the list of HDL files for input to the back end.

Defining Targets

Users can setup a target in the configuration file config.toml. For all the available parameters to define a target, see [[target]].

Protocols

A protocol is a series of steps requried to get a package from the internet. Protocols exist because there are numerous ways to access data from the internet depending on your development environment. Orbit tries to be as modular and flexible as possible by introducing protocols.

Protocols are required during the download process to acquire a package for potential cache installation.

Default protocol

Orbit has a default protocol that relies on the Rust curl crate to make HTTP requests. This protocol assumes the provided URLs point to a zip archive containing the targeted package. The protocol will extract the zip file to the queue, which is a special temporary directory handled by Orbit. Orbit generates and manages a different queue directory for each package that must be downloaded.

Using the default protocol

To use the default protocol, modify the desired project's manifest to only specify the URL as the source. The default protocol assumes the URL points to a publicly accessible zip archive.

Filename: Orbit.toml

[ip]
name = "orbit"
version = "1.0.0"
source = "https://github.com/chaseruskin/orbit/archive/refs/tags/1.0.0.zip"
# ...

Custom protocols

A user can define a custom protocol for accessing packages from the internet by modifying the configuration file.

Orbit sets the current directory for the custom protocol execution to already be the queue directory. This means when a custom protocol is executed, whatever files it downloads and extracts to the current directory is the directory Orbit expects to find the IP.

Example

One possible protocol relies on using the git command-line tool.

Filename: config.toml

[[protocol]]
name = "gitit"
summary = "Access packages through git to handle remote repositories"
command = "git"
args = ["clone", "-b", "{{ orbit.ip.version }}", "{{ orbit.ip.source.url }}"]

This protocol calls git and clones from the IP's URL while checking out the branch/tag that matches the IP's version number. These values are resolved at runtime by Orbit through variable substitution.

More complex protocols may require using a scripting language such as Python to perform the necessary steps.

Using a custom protocol

To use a custom protocol, modify the desired project's manifest file to specify the URL as well as the defined protocol's name. It is each user of the package's responsibility to ensure the necessary protocol(s) are properly configured in their settings.

Filename: Orbit.toml

[ip]
name = "orbit"
version = "1.0.0"
source = { url = "https://github.com/chaseruskin/orbit.git", protocol = "gitit" }
# ...

Channels

As your codebase evolves over time, you may have accrued a lot of ips. However, an issue arises regarding discovery- how do others quickly find all ips that have been released?

Orbit solves this problem by using channels. A channel is a lightweight decentralized registry index. In other words, a channel is a directory that contains multiple ip manifests. With this approach, users can simply configure channels to discover the many available released ips.

Channels can be as manual or automated as you prefer. You can configure commands to run for a channel's synchronization hook, pre-publish hook, and post-publish hook. Channels are encouraged to be as automated as possible by defining these fields in the channel's configuration.

Adding a new ip to a channel

Orbit automates the process of adding an ip to a channel with orbit publish.

The ip's manifest gets placed in the channel by using its generated index path. The index path can be read from the ORBIT_CHAN_INDEX environment variable during a channel's pre-publish or post-publish hook processes.

Example

[[channel]]
name = "hyperspace-labs"
description = "Available ip from hyperspace labs"
root = "." # Optional, default is "."

# If the channel is stored on the internet, synchronize with its remote location
sync.command = "git"
sync.args = ["pull"]

# Issue this command immediately before adding the ip to the channel
pre.command = "git"
pre.args = ["pull"]

# Issue this command immediately after adding the ip to the channel
post.command = "python"
post.args = ["publish.py"]

Orbit.lock

The Orbit.lock lock file is a file that captures every dependency required for the current ip. This includes information about exact versions of dependencies and how to get them if any are missing from the cache.

The lock file is managed by Orbit, and formalizes the data the user provided in the Orbit.toml manifest file. The lock file is required for every ip and should not be manually edited.

With a lock file, the current state of the ip can be reproduced at a later time and in any environment. If the current ip uses version control, then it is recommended to track Orbit.lock to ensure reproducibility across environments.

To update the current ip's lock file, use orbit lock. The lock file will also automatically be updated before the build process when using orbit build or orbit test.

Note: An ip's lock file contains all the data required by it to reproduce its current state, so it does not require reading the lock files of its dependencies.

File Visibility

An ip's manifest allows for users to set an exclude field, which can store a list of user-defined file patterns for Orbit to ignore during file discovery.

Format

Listing files in the exclude field follow the same syntax as .gitignore files. See the pattern format for more information:

Resolving errors

Orbit prevents duplicate primary design units to be identified within certain situations. For example, duplicate design unit names are not allowed within the same project because Orbit cannot resolve ambiguity in which unit is used where.

An error may look like the following:

error: duplicate primary design units identified as "foo"

location 1: rtl/foo1.vhd:20:1
location 2: rtl/foo2.vhd:1:1

hint: resolve this error by either
    1) renaming one of the units to a unique identifier
    2) adding one of the file paths to the manifest's "ip.exclude" field

The exclude field can be used in this scenario to tell Orbit to ignore reading a particular file during the HDL source code dependency analysis.

Filename: Orbit.toml

[ip]
# ...
exclude = [
    "rtl/foo2.vhd"
]

In this example, the value for the above exclude field in the local ip's manifest will resolve the previous error because it prevents Orbit from seeing the file "rtl/foo2.vhd" during any file discovery operations.

String Swapping

String swapping is the process of injecting runtime information into specific locations of strings.

This process allows permissible strings to become generic enough to avoid having the user frequently update them with redundant information or accidently recall the incorrect value.

Details

String swap works with key-value pairs. When Orbit sees the correct syntax indicating a known key, it will replace the key's contents with its value in its location within the string.

To have a key substituted with its value, use double opening curly brackets {{ to denote the beginning of a key and double closing curly brackets }} to end the key. Whitespace is ignored around the key within the curly bracket sequences.

When Orbit gets a permissible string, it will parse the characters to check if a key exists and should be swapped with its value. If it finds a valid known key, then it replaces everything from and within the curly bracket sequences with the variable's value. If it cannot find a valid key that matches the name, it leaves that sequence of the string unmodified.

Permissible strings

Not every string is checked for string swapping. Strings that are not allowed to have string swapping ignore any existing keys in the text, leaving the entire string unmodified.

The following lists the instances when a string is permitted to perform string swapping:

Manifest files

The string pattern for an ip's source.url field is allowed to contain any of the following keys:

  • orbit.ip.name: The name of the ip being downloaded.
  • orbit.ip.version: The version of the ip being downloaded.

The string pattern for an ip's source.tag field is allowed to contain any of the following keys:

  • orbit.ip.name: The name of the ip being downloaded.
  • orbit.ip.version: The version of the ip being downloaded.

Fileset patterns

The string pattern for a target's fileset configuration is allowed to contain any of the following keys:

  • orbit.top.name: The top-level design unit name.
  • orbit.tb.name: The testbench design unit name.
  • orbit.dut.name: The design-under-test design unit name.
  • orbit.env.*: Any environment variables loaded from configuration files.

Protocol arguments

The argument list defined in a protocol's configuration is allowed to contain any of the following keys:

  • orbit.queue: The directory that Orbit expects the ip temporarily placed immediately after download.
  • orbit.ip.name: The name of the ip being downloaded.
  • orbit.ip.version: The version of the ip being downloaded.
  • orbit.ip.source.url: The URL for the ip being downloaded.
  • orbit.ip.source.protocol: The protocol specified by the ip being downloaded
  • orbit.ip.source.tag: The tag (if provided) specified by the ip being downloaded.
  • orbit.env.*: Any environment variables loaded from configuration files.

Target arguments

The argument list defined in a target's configuration is allowed to contain any of the following keys:

  • orbit.ip.name: The name of the local ip.
  • orbit.ip.library: The HDL library of the local ip.
  • orbit.ip.version: The version of the local ip.
  • orbit.ip.checksum: The truncated most recent checksum of the local ip.
  • orbit.env.*: Any environment variables loaded from configuration files.
  • orbit.top.name: The top-level design unit name.
  • orbit.tb.name: The testbench design unit name.
  • orbit.dut.name: The design-under-test design unit name.

Example

Consider an ip with the following manifest data:

[ip]
name = "foo"
version = "1.2.0"
source = "https://github.com/hyperspace-labs/foo/archive/refs/tags/{{orbit.ip.version}}.zip

The source field of an ip's manifest is one string that is allowed to string swap. For its string, we specify a key, "orbit.ip.version", by enclosing it in double curly brackets. This tells Orbit that any time it uses this string, it should replace {{orbit.ip.version}} with 1.2.0, the value associated with that key.

By using string swapping, we can reduce the amount of times redundant information has to be maintained, or delay providing information when we may not know the value until runtime.

Environment variable translation examples

A key recognized by Orbit during string swapping can be an environment variable key. For an environment variable key to be recognized as a key in the context of string swapping, the environment variable key is converted to lowercase with each "_" character replaced by a "." character.

Consider the environment variables set in an Orbit configuration file:

[env]
foo = "bar"
github-user = "chaseruskin"
Yilinx_Path = "/Users/chase/fpga/bin/yilinx"

This configuration translates to the following variables:

TOML [env] entryEnvironment variableSubstitution variable
fooORBIT_ENV_FOOorbit.env.foo
github-userORBIT_ENV_GITHUB_USERorbit.env.github.user
Yilinx_PathORBIT_ENV_YILINX_PATHorbit.env.yilinx.path

Dynamic Symbol Transformation

This technique is related to name mangling in programming languages. Name mangling is a technique used to solve problems regarding the need to resolve unique names for programming entities. You can learn more about name mangling here.

Problem

Before we begin, it is important to understand the problem we are trying to solve. An issue inherent to VHDL, Verilog, SystemVerilog, and many other languages is namespace pollution, which is when a large number of programming language variables/identifiers/units/classes are defined at the global level. To learn more about namespace pollution, here is a StackOverflow post that explains it in relation to Javascript.

Namespace pollution can lead to namespace clashes. As you define more primary design units at the same scope, you are more likely to have two things that accidently have the same name. This is at the core the problem we are going to solve, because HDL compilers and synthesizers are not built to gracefully handle clashes and will error out when a primary design unit at the same scope has multiple definitions.

In VHDL/Verilog, a common example of a namespace clash is when different files define an entity/module by the same name, which may have different behaviors. Namespace clashes may start to appear when a higher-level ip requires the same entity/module from an ip but as different versions throughout its dependency tree.

Solution

We solve the namespace pollution problem with an algorithm called dynamic symbol transformation (DST). The DST algorithm solves the namespace clashing problem by rewriting conflicts with a new unique identifier without losing information in the original identifier.

Limitations

Orbit automatically handles resolving duplicate identifiers for primary design units due to two design contraints. The limitations are:

  1. All primary design unit identifiers in the current ip must be unique within the scope of the ip.
  2. All primary design units identifiers in the current ip must be unique within the scope of the ip's direct dependencies. An identifier can be duplicated for primary design units across indirect dependencies.

Example

This section walks through a basic demonstration of the DST algorithm. First, it defines some terminology, and then walks through the algorithm's functionality.

Symbols

Within the context of VHDL, let's consider a symbol to be the identifier of a primary design unit. A primary design unit is a VHDL construct that exists at the global namespace. There are four primary design units:

  • entity
  • package
  • configuration
  • context

Note: VHDL does support the concept of libraries, which can add 1 level of nested scope to a primary design unit, but this implementation only pushes the namespace clashing problem back 1 level.

Within the context of Verilog/SystemVerilog, let's consider a symbol to be the identifier of a design element. A design element is a Verilog/SystemVerilog construct that exists at the global namespace. For Verilog, there is only two design elements (module and primitive), but for SystemVerilog, there are seven design elements:

  • module
  • program
  • interface
  • checker
  • package
  • primitive
  • configuration

In the following code, the symbol nand_g corresponds to a module.

Filename: lab1/nand_g.v

module nand_g (
  input wire a, 
  input wire b,
  output wire c
);

Remember that this identifier could appear again at the same namespace level (since its global across all source code files), even if it has a different interface/implementation.

Now imagine you are integrating HDL code from various existing ips. As you instantiate modules within larger modules, you realize there exists another module named nand_g in the hierarchy, but this one has a different behavior and port interface than the previously defined nand_g circuit from the "lab1/" directory.

Filename: lab3/nand_g.v

module nand_g (
  input wire[3:0] x,
  input wire[3:0] y,
  output wire[3:0] z
);

Since the current ip requires both code segments, then traditionally your EDA tool would complain to you and be unable to resolve which nand_g to be used where. It then falls on the developer to rename one of the modules where it is defined and everywhere it is referenced, which introduces additional overhead in time and possibilities for errors. This problem is solved with DST.

Setup

Consider the following project-level ip dependency tree:

The gray node (final-project) is the local ip you are currently working within, the green nodes (lab2, lab3) are the direct dependencies to the local ip, and the blue node (lab1) is an indirect dependency to the local ip.

Within each project, there exists one or more HDL source code files describing design units.

Imagine the final-project ip has a module called half_add which is the root of circuit hierarchy. From there, it reuses entities from the other ip.

Consider then HDL-level dependency tree:

Notice lab1 and lab3 both have the nand_g module, but their interfaces and functionality are different as previously mentioned. How can we allow both units in the hierarchy while resolving the namespace clash?

Transformation

DST identifies namespace clashes within the current dependency graph and automatically resolve the conflicts to produce a clean unambiguous graph.

The yellow nodes (lab2, lab1) are the ips that had their source code modified due to DST. Since the modified contents of these ips no longer matches their original contents, the modifications are stored as separate entries in the catalog's cache apart from their original entries.

The red node (nand_g.v) is the HDL design element that must be dynamically renamed due to the namespace clash for nand_g. The identifier nand_g in lab1 was appended with the first 10 digits of the original ip's checksum (fbe4720d0). This transforms lab1's nand_g module into nand_g_fbe4720d0, which is unique and no longer clashes with nand_g in lab3.

Note: DST specifically chose to not rename the nand_g from lab3. If had decided to rename the nand_g from lab3, the user would be burdened with tracking and maintaining the new renamed unique identifier in the local ip (final-project). Since DST never renames identifiers in direct dependencies, DST is always abstracted away from the user and has zero overhead. While direct dependencies may be modified due to neigboring an ip that undergoes DST, direct dependencies are never chosen for DST.

The orange nodes (and_g.v, xor_g.v) are the HDL design elements that reference/instantiate the design element that was marked for symbol transformation. Once the ip targeted for DST (lab1) resolves the namespace clash, we must update the references for this design element in all the upstream neighboring ips (lab2). Since their references are now broken due to nand_g being renamed to nand_g_fbe4720d0, the source code is analyzed and updated to fix the broken references of nand_g to nand_g_fbe4720d0.

The final unambiguous HDL-level dependency graph is the following:

half_add (final-project)
├─ nand_g (lab3)
│  ├─ not_g (lab2)
│  └─ and_g (lab2)*
|     └─ nand_g_fbe4720d0 (lab1)*
└─ xor_g (lab2)*

The * indicates the modules that had their source code modified to either rename the namespace collision or update its references to the new renamed identifier.

Summary

To recap, DST handled the namespace clash by transforming, or renaming, the module nand_g within lab1. The nand_g identifier was appended with the first 10 digits of the original lab1 ip's checksum (fbe4720d0) to make it nand_g_fbe4720d0. This transformation occurred at that ip's source code level (lab1), and modifications were made to the source code for all dependent neighbors of lab1, which was only lab2 in this example. The source code in lab2 had to be updated to rename the references that were originally nand_g to nand_g_fbe4720d0. Each ip that had source code modifications have their changes saved to their own entries in the catalog's cache, such that the original entries are still intact and available for future use.

Emphasis

Dynamic symbol transformation lets Orbit avoid the major issues and frustrations of package management that stem from dependency incompatibility. As projects grow in complexity and the number of dependencies increases, Orbit can continue to allow users to integrate different verisons of the same package throughout the overall design while retaining dependency compatibility. Conflicts in incompatible versions are avoided within the dependency graph through DST. You can learn more about dependency incompatibility here.

Further Reading

  • https://stephencoakley.com/2019/04/24/how-rust-solved-dependency-hell

Reference

This section provides technical information for the various components used throughout orbit.

Manifest

The Orbit.toml file for each ip is called its manifest. It is written in the TOML format. It is maintained by the user and contains metadata that is needed to build the ip. The manifest is read by Orbit to help automatically generate the ip's lock file, Orbit.lock.

Note: The manifest's file name is "Orbit.toml", with respect to case-sensitivity.

Every manifest file consists of the following sections:

  • [ip] - Defines an ip.
    • name - The name of the ip.
    • uuid - The universally unique identifier of the ip.
    • description - A short description of the ip.
    • version - The version of the ip.
    • authors - The authors of the ip.
    • library - The HDL library for the design units within the ip.
    • keywords - A list of simple words categorizing the ip.
    • source - The URL for remotely retrieving the ip.
    • channels - The channels to update when publishing the ip.
    • public - Files to be visible to other ip.
    • include - Files to include during file discovery.
    • exclude - Files to exclude during file discovery.
    • readme - The path to the README file.
    • [metadata] - An unchecked section for custom fields.
  • [dependencies] - Ip dependencies.
  • [dev-dependencies] - Ip dependencies only used for ongoing development.

The [ip] section

The first section in a Orbit.toml file is [ip].

[ip]
name = "my-ip" # the name of the package
uuid = "ecj831jmc018hhhgl1d4rzgw8" # the universally unique identifier
version = "0.1.0" # the current version

The only fields required by Orbit are name, uuid, and version.

The name field

[ip]
name = "my-ip"
# ...

The uuid field

A random string consisting of 25 characters in base36 encoding (a-z0-9).

[ip]
# ...
uuid = "ecj831jmc018hhhgl1d4rzgw8"

The version field

[ip]
# ...
version = "0.1.0"

The authors field

[ip]
# ...
authors = ["Duncan Idaho", "Gurney Halleck"]

The library field

[ip]
# ...
library = "work"

The description field

[ip]
# ...
description = "A short description of the ip"

The keywords field

[ip]
# ...
keywords = ["cpu", "risc"]

The source field

[ip]
# ...
source = "https://github.com/chaseruskin/orbit/archive/refs/tags/1.0.0.zip"
[ip]
# ...
source = { url = "https://github.com/chaseruskin/orbit.git", protocol = "git", tag = "1.0.0" }

The channels field

[ip]
# ...
channels = ["hyperspace-labs"]

The public field

[ip]
# ...
public = ["/rtl"]

The public field can be used to explicitly specify which files are visible to other ip when being when being referenced as a dependency. The list contains glob-style patterns that conform to .gitignore file semantics, and are always compared relative that ip's root directory.

If no public field is present, then all files are implicitly specified as visible (public) to other ip when being referenced as a dependency.

The include field

The include field can be used to explicitly specify which files to include during source code analysis.

[ip]
# ...
include = ["/rtl"]

Using include and exclude is mutually exclusive; setting include will override any value of exclude. If include and exclude are omitted, then all files from the root of the ip will be included.

The exclude field

The exclude field can be used to explicitly specify which files to exclude during source code analysis.

[ip]
# ...
exclude = ["/deprec"]

Using include and exclude is mutually exclusive; setting include will override any value of exclude. If include and exclude are omitted, then all files from the root of the ip will be included.

Files that are always excluded are those found in directories that contain a "CACHEDIR.TAG" file. For example, every target output directory Orbit creates is excluded because they contain this file.

The readme field

[ip]
# ...
readme = "README.md"

The [metadata] section

Any type of TOML entry is allowed in this section, as Orbit ignores this section.

[ip.metadata]
custom-field-1 = true
custom-field-2 = "hello world"
# ...

The [dependencies] section

The [dependencies] section is a table of direct dependencies required for the current ip.

[dependencies]
gates = "1.0.0"
uart = "2.3.1"

If the ip has no dependencies, the section can be omitted from the manifest. The ips listed in this section will always be included in the build graph.

The [dev-dependencies] section

The [dev-dependencies] section is a table of direct dependencies required for the current ip.

[dev-dependencies]
testkit = "1.3.7"
logic-analyzer = "4.8.0"

If the ip has no development dependencies, the section can be omitted from the manifest. The ips listed in this section will not be included in the build graph for when this ip is used as a dependency itself.

Names

In order to identify an ip among others, Orbit requires users to assign a human-readable name to each created ip. In addition to the human-readable name, Orbit assigns a universally unique identifier (UUID) to each ip. UUIDs are required in order to avoid namespace collisions at the ip level in Orbit's decentralized system.

Name

The ip name is a unique string of characters that abides by a certain set of rules. It is a single name and is defined under the "name" field in an ip's manifest. Every ip is required to have a name. The name should not change over the course of an ip's lifetime.

[ip]
name = "cpu"
# ...

Rules

The following rules currently apply to a name values:

  • begins with an ASCII letter (a-z, A-Z)
  • contains only ASCII letters (a-z, A-Z), ASCII digits (0-9), dashes -, and underscores _
  • cannot end with a dash - or underscore _

UUID

The ip uuid is a unique string of characters encoded in base36 (a-z0-9). An encoded UUID is 25 characters long and is generated using Version 4 UUID. It is defined under the "uuid" field in an ip's manifest. Every ip is required to have a uuid. The uuid should not change over the course of an ip's lifetime.

[ip]
# ...
uuid = "71vs0nyo7lqjji6p6uzfviaoi"

Rules

The following rules currently apply to uuid values:

  • contains only ASCII lowercase letters (a-z) and ASCII digits (0-9)
  • is 25 characters long

Version

The ip version is a series of 3 numbers separated by decimal characters (.) with an optional label suffix attached with a dash character (-). The version should be updated over the course of an ip's lifetime when significant enough changes to the project require a new version value.

[ip]
# ...
version = "1.0.0"

Rules

The following rules currently apply to version values:

  • contains only ASCII digits (0-9) for each of the 3 version fields
  • each version field is separated by a decimal character (.)
  • a label can be attached to the end by using a dash character (-)
  • labels can only contain ASCII letters (a-z, A-Z), ASCII digits (0-9), and decimal characters (.)

Ip specification

An ip specification, commonly abbreviated to spec, is the total unambiguous reference to a specific ip at a particular version.

spec ::= <name>[+uuid][:version]

Example specifications

The following provides various valid inputs when defining an ip's spec and how it decomposes into its parts.

SpecNameUUIDVersion
gates:1.0.0gatesAutomatically resolved if only 1 ip exists with the name gates1.0.0
ramramAutomatically resolved if only 1 ip exists with the name ramlatest
fifo:2.3fifoAutomatically resolved if only 1 ip exists with the name fifo2.3.*
cpu+71vs0nyo7lqjji6p6uzfviaoi:1.0.0cpu71vs0nyo7lqjji6p6uzfviaoi1.0.0

Namespace Collisions

Two different ip's may share a common name within the catalog even though their contents are different. Two names are considered equal if their lowercase mapping is identical, where dashes (-) also map to underscores (_).

Spec 1Spec 2Collision
gatesGATEStrue
ramromfalse
fifo_cdcFifo-CDCtrue

To resolve namespace collisions at the ip level, Orbit uses UUIDs. When there are multiple ips in the catalog that share the same name, a user must then explicitly include the UUID of the requested ip to disambiguate between ips under the same name.

Libraries

An ip can optionally belong to a library. An ip's library is a higher-level scope that loosely groups together multiple ips. This library identification is used for grouping the HDL source code itself into their language-defined libraries as well.

A library can be defined through the "library" field in the ip's manifest file. Its format follows the same rules as the ip's name. If no library is defined in the ip's manifest, then the default library is the ip's name.

Versions

Code evolves over time, and versions provide a method for capturing a project's state at a given time stamp.

Orbit uses the semantic versioning scheme for capturing project's state at given time periods. Semantic versioning uses 3 numeric values to signify different levels of change.

version ::= major "." minor "." micro [ "-" label ]
LevelExplanation
MajorIncompatible API changes
MinorAdding functionality in backward-compatible way
MicroFixing bugs in backward-compatible way
LabelDescriptive modifier to show status of upcoming version

To learn more about semantic versioning, visit the official website.

Determining the next version number based on a project's recent code changes can be an opinionated process, so it's recommended to also keep a changelog highlighting the differences among versions.

Note: An alternative to semantic versioning is calender versioning, which also operates on the basis of using 3 digits. To learn more about calender versioning visit the official website.

Rules

  • Each level may only contain ASCII digits (0-9).
  • A label must follow a dash character (-) and cannot be empty.
  • Labels can consist of ASCII letters (a-z, A-Z), ASCII digits (0-9), and/or decimal characters (.)

All 3 levels must be given a numeric value consisting of only digits separated by a dot (.) character. This is considered a fully qualified version.

1.0.0

In some scenarios a partially qualified version can be accepted. This means one or more of the version's levels are omitted. A label is not required for a version to be considered fully qualified.

1
1.0

When given a partially qualified version, Orbit references the maximum version available that satifies the partially qualified version. If no version is specified, it assumes the request is for the latest known version. The latest known version can also be explicitly requested by inputting latest as the version. Assume the known released versions for a given IP are as listed:

Versions
2.1.0
1.5.0
1.2.1
1.2.0
1.0.0

The following illustrates the mapping from the partially specified requested version to its fully specified known version that would be returned:

RequestedReturned
11.5.0
1.1NOT FOUND
1.21.2.1
22.1.0
1.2.01.2.0
latest2.1.0
(omitted)2.1.0

Example

A fully qualified version must be written in every project's manifest file.

[ip]
# ...
version = "1.5.4"
# ...

A specific (or partially speific) version can be requested for an IP on the command-line by placing a colon : character between the package's name and the requested version.

$ orbit install gates:1.5.4
$ orbit get nor_gate --ip gates:1.5

Comparing versions

The following pseudocode provides additional help in learning about how versions are compared (selecting a "higher" version).

IF major levels are not equal:
    RETURN version with larger major level value.
ELSE IF minor levels are not equal:
    RETURN version with larger minor level value.
ELSE:
    RETURN version with larger patch level value. 

Filesets

A fileset is group of files identified by a common file pattern. Typically they are denoted by a common file extension, such as .txt, but a fileset can more broadly be grouped under any glob-style pattern.

A fileset itself consists of a name and a pattern.

  • The name is a string that is normalized to ALL-UPPERCASE-WITH-HYPENS. It is used to identify which fileset a file belongs to.
  • The pattern is a glob-style pattern. It is used to perform glob-style matching when searching the file system for files to add to a fileset.

Built-in filesets

There are built-in filesets that Orbit uses that have special rules and work across all ip, including dependencies. The following filesets are currently built-in with Orbit:

  • VHDL: VHDL files (.vhd, .vhdl)
  • VLOG: Verilog files (.v, .vl, .verilog, .vlg, .vh)
  • SYSV: SystemVerilog files (.sv, .svh)

Custom filesets

Custom filesets are filesets that are be defined by the user for a specific target. These filesets are only searched for in the local ip and do not extend any of the ip's dependencies.

If the pattern does not start with an explicit relative path symbol (.), then Orbit assumes to look for the fileset across every possible path in the local ip by implicitly prepending the pattern with **/.

Name normalization examples

User-defined FilesetNormalized Fileset
GOOD-SETGOOD-SET
Set-1SET-1
set_2SET-2
set_threeSET-THREE

The normalized fileset name is the name that will be written to the blueprint file when collecting filesets. This design choice is for consistency across targets when reading and parsing the blueprint.

Custom pattern assumption examples

User-defined patternInterpreted pattern
*.txt**/*.txt
Boards/*.toml**/Boards/*.toml
./specific/path.log./specific/path.log

The custom patterns begin their search for files at the local ip's root directory. The interpreted pattern is the actual glob-style pattern used when collecting files for custom filesets.

Blueprint

The blueprint is a file containing a list of files required for a particular back end. This single file is the main method Orbit uses to communicate information to a target's process.

When the blueprint is created, it is saved to the local ip's target output directory.

Formats

The currently supported formats are:

Specifications

Each blueprint format may contain different information and store it in a different way. Refer to each specification to see exactly how the data is communicated through their blueprint.

Attributes that are consistent across all formats are the fileset, library, and filepath.

The fileset is the group name for the file pattern that matched the given rule's file.

The library is the hdl defined library for the ip which the given file at this particular step was found.

The filepath is the absolute file system path to the given rule's file.

Built-in Filesets

The following filesets are already recognized by Orbit and are used for identifying hdl source code:

FilesetSupported file extensions
VHDL.vhd, .vhdl
VLOG.v, .vl, .verilog, .vlg, .vh
SYSV.sv, .svh

Tab-separated values

  • Advantages
    • Simple and easy to parse for back ends
  • Disadvantages
    • Limited information is sent

The file is divided into a series of steps, each separated by a newline character (\n).

STEP
STEP
...

A step contains information about a particular file. Every step always has 3 components: a fileset, a library, and a filepath. Each component in a step is separated by a tab character (\t).

FILESET	LIBRARY	FILEPATH

Examples

PYMDL	lc3b	/Users/chase/projects/lc3b/sim/models/alu_tb.py
VHDL	lc3b	/Users/chase/projects/lc3b/rtl/const_pkg.vhd
VHDL	base2	/Users/chase/.orbit/cache/base2-1.0.0-aac9159285/pkg/base2.vhd
VHDL	lc3b	/Users/chase/projects/lc3b/rtl/alu.vhd
VHDL	lc3b	/Users/chase/projects/lc3b/sim/alu_tb.vhd

Environment Variables

Orbit's configuration can be customized with the setting of specific environment variables. These variables can be accessed anytime Orbit is executed.

  • ORBIT_HOME - Location where Orbit stores its data. By default Orbit reads and writes to "$HOME/.orbit" on Unix systems and "%USERPROFILE%/.orbit" on Windows systems.

  • NO_COLOR - If set, do not print colorized output to the terminal.

  • ORBIT_WIN_LITERAL_CMD - If set, disables the default behavior of checking for programs ending with ".exe" then ".bat" when a program name without extension is not found on Windows systems.

Runtime environment variables

Orbit also sets environment variables during runtime such that any subprocesses within Orbit, such as targets, can access necessary information.

  • ORBIT_MANIFEST_DIR - The full path for the directory that contains the current ip's manifest.

  • ORBIT_IP_NAME - The name of the current ip.

  • ORBIT_IP_UUID - The uuid of the current ip.

  • ORBIT_IP_LIBRARY - The interpretated HDL library of the current ip.

  • ORBIT_IP_VERSION - The version of the current ip.

  • ORBIT_IP_CHECKSUM - The first 10 characters from the latest checksum of the current ip.

  • ORBIT_TARGET - The name of the target selected for the latest build process.

  • ORBIT_TOP_NAME - The top level design's identifier for the latest build process, only if the build process was a build.

  • ORBIT_TB_NAME - The testbench's identifier for the latest build process, only if the build process was a test.

  • ORBIT_DUT_NAME - The design under test's identifier for the latest build process, only if the build process was a test.

  • ORBIT_BLUEPRINT - The file name for the blueprint created from the planning stage of the latest build process. The file name includes the file's extension.

  • ORBIT_TARGET_DIR - Directory where all generated artifacts from any targets will be stored, relative to the current ip's directory. Default is "target".

  • ORBIT_OUT_DIR - The folder where all generated artifacts for the current target will be stored. This folder is inside the target directory for the current ip, and is unique for each selected target. Default is the target's name.

  • ORBIT_CHAN_INDEX - The full path for the directory where the current ip's manifest will be placed for the current channel in the publishing process.

Checking the environment

See orbit env for checking environment variables on the command-line. Not all environment variables, especially runtime environment variables, may be available.

Configuration

The config.toml file stores settings and extends Orbit's functionality. It is written in the TOML format. It is maintained by the developer and can be shared across teams for consistent development environments.

Note: The configuration's file name is "config.toml", with respect to case-sensitivity.

Paths

When a field is expected to be a file system path, Orbit has the ability to resolve relative paths. The path is determined in relation to the currently processed config.toml's parent directory. This design choice was implemented in order to allow for path definitions to be valid across developer machines when sharing configurations. It is recommended to use relative paths when setting a path to a field in a config.toml.

Precedence

Orbit supports multiple levels of configuration. Each level has its own order of precedence. The order of precedence is the following:

  1. Local configuration file (current ip's .orbit/conifg.toml)

  2. Regional configuration files (parent directories of the current working directory)

  3. Global configuration file (Orbit's $ORBIT_HOME/config.toml)

  4. Included configuration files (order-preserving list found for the global configuration's include field)

The configuration files are processed in the order defined above. When a configuration file defines a field, no other configuration files later in the process will be able to override its value. If a field is never provided an explicit value, the Orbit's default will be used.

Tip: You can modify some values in the configuration file through the command-line by using the orbit config command.

Every configuration file consists of the following sections:

  • include - Lists other config.toml files to process. This field is only allowed for the global configuration file.
  • [general] - The general settings.
  • [test] - The test settings.
  • [build] - The build settings.
  • [vhdl-format] - VHDL code formatting.
  • [verilog-format] - SystemVerilog/Verilog code formatting.
  • [env] - The runtime environment variables.
  • [[target]] - Define a target.
    • name - The name of the target.
    • description - A short description of the target.
    • command - The command to execute the target.
    • args - Arguments to pass to the command.
    • plans - The list of supported blueprint file formats.
    • [fileset] - Filesets to collect for the target.
  • [[protocol]] - Define a protocol.
    • name - The name of the protocol.
    • description - A short description of the protocol.
    • command - The command to execute the protocol.
    • args - Arguments to pass to the command.
  • [[channel]] - Define a channel.
    • name - The name of the channel.
    • description - A short description of the channel.
    • root - The directory where the channel exists.
    • sync.command - The command to execute when synchronizing the channel.
    • sync.args - Arguments to pass to the command during synchronization.
    • pre.command - The command to execute immediately before launch.
    • pre.args - Arguments to pass to the command immediately before launch.
    • post.command - The command to execute immediately after launch.
    • post.args - Arguments to pass to the command immediately after launch.

The include field

include = [
    "profiles/p1/config.toml",
    "profiles/p2/config.toml",
    "channels/c1/config.toml"
]

The [general] section

The target-dir field

Define the default output directory to create for the planning and building phases. This value can be overridden on the command-line when the --target-dir option is available. When this field is not defined, the default value for the build directory is "target".

[general]
target-dir = "target"
# ...

The [test] section

The default-target field

Sets the default target when calling orbit test. If the default target is set to be used and it cannot be found among the known targets, it will error.

[test]
default-target = "foo"

The [build] section

The default-target field

Sets the default target when calling orbit build. If the default target is set to be used and it cannot be found among the known targets, it will error.

[build]
default-target = "bar"

The [vhdl-format] section

The currently supported entries are demonstrated in the following code snippet. Entries not present will be set to their default values.

[vhdl-format]
# enable colored output for VHDL code snippets
highlight-syntax = true
# number of whitespace characters per tab/indentation
tab-size = 2
# insert a tab before 'generic' and 'port' interface declarations
indent-interface = true
# automatically align a signal or constant's subtype with its other identifiers
type-auto-alignment = false
# number of whitespace characters after alignment (before the `:` character)
type-offset = 0
# automatically align an instantiation's mapping along its port connections
mapping-auto-alignment = false
# number of whitespace characters before port connection (before the `=>` character)
mapping-offset = 1
# the default instance name
instance-name = "uX"

The [verilog-format] section

The currently supported entries are demonstrated in the following code snippet. Entries not present will be set to their default values. This section currently applies its settings to SystemVerilog and Verilog source code.

[verilog-format]
# enable colored output for code snippets (TODO)
highlight-syntax = false
# number of whitespace characters per tab/indentation
tab-size = 2
# automatically align a port or parameter's name with the module's other names
name-auto-alignment = false
# number of additional whitespace characters after alignment
name-alignmnet = 0
# number of whitespaces before a range specifier
range-offset = 0
# automatically align an instantiation's mapping along its port connections
mapping-auto-alignment = false
# number of whitespace characters before port connection (before the `(` character)
mapping-offset = 0
# the default instance name
instance-name = "uX"

The [env] section

The user can define an arbitrary number of their own entries with their determined value represented in string format.

[env]
foo = "0" # Accessible as ORBIT_ENV_FOO
super-bar = "1" # Accessible as ORBIT_ENV_SUPER_BAR

The [[target]] array

The name field

[[target]]
name = "dump-blueprint"

The description field

[[target]]
# ...
description = "Print the blueprint contents to the screen"

The command field

[[target]]
# ...
command = "cat"

The args field

[[target]]
# ...
args = ["blueprint.tsv"]

The plans field

[[target]]
# ...
plans = ["tsv"]

The type of blueprint files supported by the particular target. If a list is provided, the default plan used is the first item in the list. If a plan is provided on the command-line, then it must be a valid plan and found within the target's defined list.

If this field is left blank or not defined, then the default plan is "tsv".

The [fileset] section

[[target]]
# ...
fileset.pymdl = "{{ orbit.bench }}.py"

The [[protocol]] array

The name field

See [target]'s definition.

The description field

See [target]'s definition.

The command field

See [target]'s definition.

The args field

See [target]'s definition.

The [[channel]] array

The name field

See [target]'s definition.

The description field

See [target]'s definition.

The root field

The file system path where the channel exists, relative to the configuration file where it is defined.

[[channel]]
# ...
root = "./index"

The sync.command field

See [target]'s definition.

The sync.args field

See [target]'s definition.

The pre.command field

See [target]'s definition.

The pre.args field

See [target]'s definition.

The post.command field

See [target]'s definition.

The post.args field

See [target]'s definition.

JSON Output

The orbit get command allows a user to receive various pieces of information related to a design unit, such its component declaration, defined architectures, or entity instantiation.

It also allows users to export the unit's interface with the --json flag. This is convenient when you wish to pass this information in a more machine-readable format to another tool/program.

Schema

The following schema is implemented for the json output:

{
  "identifier": string
  "generics": [
    {
        "identifier": string
        "mode": string
        "type": string // null if blank
        "default": string // null if blank
    }
  ]
  "ports": [
    {
        "identifier": string
        "mode": string
        "type": string // null if blank
        "default": string // null if blank
    }
  ]
  "architectures": [
      string
  ],
  "language": string
}

The "language" field is allowed to be one of three values: "vhdl", "verilog", or "systemverilog".

References

Some ideas about exporting json from orbit get can be found at this blog post.

Command line

The command line is the main way of interacting with orbit. Let's understand some terminology and rules for communicating to orbit through the command line.

Syntax

Angular brackets (< >) denotes that a user input is required. The label within the angular brackets gives a hint to the user as to what type of value to enter here.

Square brackets ([ ]) denotes that the input is optional and is not required to get the command to successfully run.

Jargon

Orbit uses subcommands, arguments, options, flags, and switches.

Subcommand

Subcommands are special keywords to route to a particular action. Each subcommand inherits all supercommand's available options. They are the first positional argument following the call to orbit.

orbit get

Argument

An argument is a value interpreted based on its position in the input. Arguments must be included when requested by Orbit within angular brackets (< >).

orbit new <path>

Arguments may be omitted if they are wrapped with square brackets ([ ]).

orbit search [<ip>]

Flag

A flag is a simple boolean on-off conditional to alter a command's behavior, that is true when present and false otherwise.

--help

Flags are options that do not take an argument and can be omitted.

Option

An option is a type of flag that, when provided, is required to have an argument assigned to it. The argument may immediately proceed the option's flag separated by whitespace.

--plugin <name>

The argument may also be attached to the option's flag with an equal sign =.

--plugin=<name>

Options are able to be omitted.

Switch

A switch is a shorthand flag denoted by a single dash and a single character.

-h

Multiple switches can be chained onto the same dash.

-ci

If a switch is associated with an option, it must be declared last on a chain with its argument to immediately follow separated by whitespace or an equal sign =.

-o <file>

Common flags and options may have a shorthand switch associated with them. For example, --help can be alternatively passed with just -h.

Argument terminator

The argument terminator is a special no-op flag -- that tells the command-line interpreter to parse up until this symbol.

Some scenarios will allow you to pass arguments through orbit to an internally executed command. You can pass these arguments by using the argument terminator.

Examples

An example of using the argument terminator is brought up when calling a plugin through orbit during the building step.

$ orbit build --plugin yilinx -- --sram

In this command, orbit does not interpret the "--sram" flag, but instead passes it to the plugin named "yilinx" to handle.

Commands

Commands are loosely grouped into two types: development and management. Commands used for development often operate on your local ip. Commands used for management often use Orbit to gain insight into external ip found in the catalog.

The commands are listed below in their group and in a typical order of usage for a possible workflow.

Development

Management

orbit new

NAME

new - create a new ip

SYNOPSIS

orbit new [options] <path>

DESCRIPTION

Creates a new ip at the target directory <path>. The path is assumed to not already exist. A new directory will be created at the file system destination that contains a minimal manifest file.

If no name is supplied, then the ip's name defaults to the final directory name taken from <path>. Using the --name option allows this field to be explicitly set.

For initializing an already existing project into an ip, see the init command.

OPTIONS

<path>
      Directory to create for the ip

--name <name>
      Set the resulting ip's name

--lib <lib>
      Set the resulting ip's library

EXAMPLES

orbit new gates
orbit new eecs/lab1 --name adder

orbit init

NAME

init - initialize an ip from an existing project

SYNOPSIS

orbit init [options] [<path>]

DESCRIPTION

Initializes an ip at the file system directory <path>. If not path is provided, then it defaults to the current working directory.

If no name is provided, then the resulting ip's name defaults to the directory's name. Using the --name option allows the ip's name to be explicitly set.

Under certain circumstances, you may need a new uuid. This situation will be uncommon for many users, but nonetheless it exists. To display a new uuid that can be copied into an existing manifest, use the --uuid option. All other options are ignored when this option is present. Keep in mind that an ip's uuid is not intended to change over the course of its lifetime.

To create a new ip from a non-existing directory, see the new command.

OPTIONS

<path>
      Directory to initialize

--name <name>
      Set the resulting ip's name

--lib <lib>
      Set the resulting ip's library

--uuid
      Print a new uuid and exit

EXAMPLES

orbit init
orbit init projects/gates
orbit init --name adder
orbit init --uuid

orbit view

NAME

view - display metadata of an ip

SYNOPSIS

orbit view [options] [<ip>]

DESCRIPTION

Displays various bits of information about a particular ip. If no ip is provided, then it displays information related to the local ip.

To display manifest information, no additional options are required.

To display the defined HDL design elements within the ip, use the --units option. For non-local ip, its protected and private design elements are hidden from the results. To display design elements of all visibility levels the --all option must also be present.

To display the known versions for an ip, use the --versions option.

OPTIONS

<ip>
      Ip specification

--versions, -v
      Display the list of known versions

--units, -u
      Display the hdl design elements defined for this ip

--all, -a
      Include any private or hidden results

EXAMPLES

orbit view --units
orbit view gates:1.0.0 -u --all
orbit view gates --versions
orbit view gates:1 -v

orbit read

NAME

read - lookup hdl source code

SYNOPSIS

orbit read [options] <unit>

DESCRIPTION

Navigates hdl source code to lookup requested hdl code snippets. Looking up hdl source code to see its implementation can help gain a better understanding of the code being reused in your current design.

By default, the resulting code is displayed to the console. To write the results to a file for improved readability, use the --save option. Combining the --locate option with the --save option will append the line and column number of the identified code snippet to the end of the resulting file path.

If no ip is provided by the --ip option, then it will assume to search the local ip for the provided design unit.

The values for options --start, --end, and --doc must be valid hdl code. The code is interpreted in the native language of the provided design unit.

The --doc option will attempt to find the comments immediately preceding the identified code snippet.

A design unit must visible in order for it to return the respective source code. When reading a design unit that exists within the local ip, it can be any visibility. When reading a design unit that exists outside of the local ip, its visibility must be "public" or "protected". Design units that are set to "private" visibility are not allowed to be read outside of their ip.

Every time this command is called, it attempts to clean the temporary directory where it saves resulting files. To keep existing files on the next call of this command, use the --no-clean option.

OPTIONS

<unit>
      Read the file for this primary design unit

--ip <spec>
      Ip specification

--doc <code>
      Find the preceding comments to the code snippet

--save
      Write the results to a temporary read-only file

--start <code>
      Start the lookup after jumping to this code snippet

--end <code>
      Stop the lookup after finding this code snippet

--limit <n>
      Maximum number of source code lines to return

--no-clean
      Do not clean the temporary directory of existing files

--locate
      Append the line and column number to the resulting file

EXAMPLES

orbit read and_gate --limit 25
orbit read math_pkg --ip math --doc "function clog2" --start "package math_pkg"
orbit read math_pkg --ip math --doc "function flog2p1" --save --locate

orbit get

NAME

get - fetch an hdl unit for code integration

SYNOPSIS

orbit get [options] <unit>

DESCRIPTION

Returns hdl code snippets for the provided design unit to be integrated into the current design. The code snippets are returned in the native hdl language of the identified design unit. Code snippets are designed to be copy and pasted from the console to the current design for quick code integration.

If an ip is not provided with the --ip option, then it will search the local ip for the requested design unit.

If the design unit is in VHDL with the --instance option being used without the --component option, then it will return the direct instantiation code style (VHDL-93 feature).

Copying unit instantiations into higher-level entities will not automatically track source code references across ips. In order to properly establish source code reference tracking across ips, the local ip's manifest must have an up to date [dependencies] table that lists all the ips from which it references source code.

An identifier prefix or suffix can be attached to the signal declarations and the instantiation's port connection signals by using --signal-prefix and --signal-suffix respectively. These optional texts are treated as normal strings and are not checked for correct hdl coding syntax.

When no output options are specified, this command by default will display the unit's declaration.

A design unit must visible in order for it to return the respective code snippets. When fetching a design unit that exists within the local ip, it can be any visibility. When fetching a design unit that exists outside of the local ip, its visibility must be "public". Design units that are set to "protected" or "private" visibility are not allowed to be referenced across ips.

Exporting the unit's declaration information can be accomplished by using the --json option. The valid json is produced with minimal formatting for encouragement to be processed by other programs.

OPTIONS

<unit>
      Primary design unit identifier

--ip <spec>
      Ip specification

--json
      Export the unit's information as valid json

--library, -l
      Display the unit's library declaration

--component, -c
      Display the unit's declaration

--signals, -s
      Display the constant and signal declarations

--instance, -i
      Display the unit's instantiation

--architecture, -a
      Display the unit's architectures

--name <identifier>
      Set the instance's identifier

--signal-prefix <str>
      Prepend information to the instance's signals

--signal-suffix <str>
      Append information to the instance's signals

EXAMPLES

orbit get and_gate --ip gates:1.0.0 --component
orbit get ram --ip mem:2 -csi
orbit get uart -si --name uart_inst0
orbit get or_gate --ip gates --json

orbit tree

NAME

tree - show the dependency graph

SYNOPSIS

orbit tree [options] [<unit>...]

DESCRIPTION

Shows the hierarchical tree structure of the hardware design starting from a root node.

By default, it will try to automatically detect the root node for the local ip. If there is ambiguity in determining what node can be the root, then all root nodes and their respective trees will be displayed. To only display the tree of a particular node, use the <unit> option.

There are two trees available to view: hdl and ip. By default, the hdl dependency graph is displayed. The hdl graph shows the composition of usable entities/modules. To generate this graph, it analyzes each VHDL architecture and ignores Verilog compiler directives. If an unidentified entity is instantiated, it will appear as a leaf in the graph and will be considered as a "black box" denoted by the "?" character next to its position in the tree.

Using the --format option can alter how much information is displayed for each hdl design unit in the tree composition. By default, only the design unit's name is displayed for each unit.

To display the ip dependency graph, use the --ip option.

If the tree's character output is not displaying properly, then the tree can be displayed using a set of standard ASCII characters with the --ascii option.

OPTIONS

<unit>...
      Uppermost hdl unit of the dependency tree

--format <fmt>
      Determine how to display nodes ('long', 'short')

--ascii
      Limit the textual tree characters to the 128 ascii set

--ip
      Switch to the ip dependency graph

EXAMPLES

orbit tree
orbit tree top --format long
orbit tree --ip --ascii

orbit lock

NAME

lock - save the world state of an ip

SYNOPSIS

orbit lock [options]

DESCRIPTION

Saves the state of the world for the local ip. To accomplish this, Orbit reads the local ip's manifest file, "Orbit.toml", to resolve any missing ip dependencies. It writes the information required to reproduce this state to the ip's lock file, "Orbit.lock".

A local ip must exist for this command to execute.

It is encouraged to check the lock file into version control such that other users trying to reconstruct the ip can reproduce the ip's current state. The lock file should not be manually edited by the user.

To capture the world state for the local ip, Orbit downloads and installs any unresolved ip dependencies. If an installed dependency's computed checksum does not match the checksum stored in the lock file, it assumes the installation is corrupt and will reinstall the dependency to the cache.

OPTIONS

--force
      Ignore reading the precomputed lock file

EXAMPLES

orbit lock
orbit lock --force

orbit test

NAME

test - run a test

SYNOPSIS

orbit test [options] [--] [args]...

DESCRIPTION

This command prepares a given target and then executes the target.

While this command functions similar to orbit build, the targets that are encouraged to be used with this command are ones that are designed to either "pass" or "fail", typically through a return code. This command requires a testbench, if you do not want to set a testbench, see orbit build.

A target must be provided for the test command to run. A default target can be specified in a configuration file, which will be used when a target is omitted from the command-line.

If --list is used, then it will display a list of the available targets to the user. Using --list in combination with a target from --target will display any detailed help information the target has documented in its definition.

A target typically goes through three steps for the testing process:

  1. Parse the blueprint file
  2. Process the referenced files listed in the blueprint
  3. Verify the hdl source code passes all tests

Any command-line arguments entered after the terminating flag -- will be passed in the received order as arguments to the subprocess's command. If a target already has defined arguments, the additional arguments passed from the command-line will follow the previously defined arguments.

The target's process will spawn from the current working ip's output directory, which is $ORBIT_TARGET_DIR/$ORBIT_TARGET.

OPTIONS

--target, -t <name>
      Target to execute

--dut <unit>
      Set the device under test

--tb <unit>
      Set the top level testbench unit

--plan <format>
      Set the blueprint file format

--target-dir <dir>
      The relative directory where the target starts

--command <path>
      Overwrite the target's command

--list
      View available targets and exit

--all
      Include all hdl files of the working ip

--fileset <key=glob>...
      A glob-style pattern identified by name to include in the blueprint

--no-clean
      Do not clean the target folder before execution

--force
      Force the target to execute

--verbose
      Display the command being executed

args
      Arguments to pass to the target

EXAMPLES

orbit test --dut adder --tb adder_tb --target modelsim -- --lint

orbit build

NAME

build - plan and execute a target

SYNOPSIS

orbit build [options] [--] [args]...

DESCRIPTION

This command prepares a given target and then executes the target.

While this command functions similar to orbit test, the targets that are encouraged to be used with this command are ones that produce artifacts at the end of their execution process. This command does not allow the top to be a testbench, if you want to set a testbench, see orbit test.

A target must be provided for the build command to run. A default target can be specified in a configuration file, which will be used when a target is omitted from the command-line.

If --list is used, then it will display a list of the available targets to the user. Using --list in combination with a target from --target will display any detailed help information the target has documented in its definition.

A target typically goes through three steps for the building process:

  1. Parse the blueprint file
  2. Process the referenced files listed in the blueprint
  3. Generate a artifact(s)

Any command-line arguments entered after the terminating flag -- will be passed in the received order as arguments to the subprocess's command. If a target already has defined arguments, the additional arguments passed from the command-line will follow the previously defined arguments.

The target's process will spawn from the current working ip's output directory, which is $ORBIT_TARGET_DIR/$ORBIT_TARGET.

OPTIONS

--target, -t <name>
      Target to execute

--top <unit>
      Set the top level design unit

--plan <format>
      Set the blueprint file format

--target-dir <dir>
      The relative directory where the target starts

--command <path>
      Overwrite the target's command

--list
      View available targets and exit

--all
      Include all hdl files of the working ip

--fileset <key=glob>...
      A glob-style pattern identified by name to include in the blueprint

--force
      Force the target to execute

--no-clean
      Do not clean the target folder before execution

--verbose
      Display the command being executed

args
      Arguments to pass to the target

EXAMPLES

orbit build --target xsim -- --elab
orbit build --command python3 --target pysim
orbit build --all --target-dir build --target ghdl
orbit build --target xsim --force -- --help

orbit publish

NAME

publish - post an ip to a channel

SYNOPSIS

orbit publish [options]

DESCRIPTION

Performs a series of checks for a local ip and then releases it to its specified channel(s).

There are multiple checks that are performed before an ip can be published. First, the ip must have an up to date lockfile with no relative dependencies. The ip's manifest must also have a value for the source field. In addition, Orbit must be able to construct the hdl source code graph without errors. Finally, the ip is downloaded from its source url and temporarily installed to verify its contents match those of the local ip.

Posting an ip to a channel involves copying the ip's manifest file to a path within the channel known as the index. For every publish of an ip, the index corresponds to a unique path within the channel that gets created by Orbit. A channel's pre-publish and post-publish hooks can get the value for the ip's index by reading the ORBIT_IP_INDEX environment variable.

By default, this command performs a dry run, which executes all of the steps in the process except for actually posting the ip to its channel(s). To run the command to completion, use the --ready option.

OPTIONS

--ready, -y
      Run the operation to completion

--no-install
      Do not install the ip for future use

--list
      View available channels and exit

EXAMPLES

orbit publish
orbit publish --ready

orbit search

NAME

search - browse the ip catalog

SYNOPSIS

orbit search [options] [<ip>]

DESCRIPTION

Returns a list of the ip found in the catalog.

By default, all ip in the catalog will be returned. To filter by ip name, use the <ip> option. To limit the number of results, use the --limit option.

An ip can be stored across three different levels: installed in the cache, downloaded to the archive, and available via channels. By default, all levels are searched for ip. Applying a level filter (--install, --download, --available options) will restrict the search to only checking the filtered levels for ip.

A resulting ip is only read from one level, even when multiple levels are searched. When an ip exists at multiple levels, the catalog imposes a priority on which level to choose. Installed ip have higher priority over downloaded ip, and downloaded ip have higher priority over available ip.

Results can also be filtered by keyword using the --keyword option. By default, if an ip matches at least one filter then it will be returned in the result. To collect only ip that match each presented filter, use the --match option.

If an ip has a higher version that exists and is not currently installed, then an asterisk character "*" will appear next the ip's version. To update the ip to the latest version, see the install command.

OPTIONS

<ip>
      Ip's name

--install, -i
      Filter ip installed to the cache

--download, -d
      Filter ip downloaded to the archive

--available, -a
      Filter ip available via channels

--keyword <term>...
      Include ip that have this keyword

--limit <n>
      Maximum number of results to return

--match
      Return results that pass each filter

EXAMPLES

orbit search axi
orbit search --keyword memory --keyword ecc
orbit search --keyword cdc --limit 20 -i

orbit install

NAME

install - store an immutable reference to an ip

SYNOPSIS

orbit install [options]

DESCRIPTION

This command will place an ip into the cache. By default, the specified version is the 'latest' released version orbit can identify.

When this command is ran without specifying the or a source (such as --url or --path), it will attempt to install the current working ip, if it exists.

By default, any dependencies required only for development by the target ip are omitted from installation. To also install these dependencies, use --all.

If a protocol is recognized using --protocol, then an optional tag can also be supplied to help the protocol with providing any additional information it may require.

To remove ip from the catalog, see the remove command.

OPTIONS

<ip>
      Ip specification

--url <url>
      Url to install the ip from the internet

--path <path>
      Path to install the ip from local file system

--protocol <name>
      Use a configured protocol to download ip

--tag <tag>
      Unique tag to provide to the protocol

--force
      Install the ip regardless of the cache slot occupancy

--offline
      Skip checking coherency with source

--list
      View available protocols and exit

--all
      Install all dependencies (including development)

EXAMPLES

orbit install
orbit install lcd_driver:2.0
orbit install adder:1.0.0 --url https://my.adder/project.zip
orbit install alu:2.3.7 --path ./projects/alu --force

orbit remove

NAME

remove - delete an ip from the catalog

SYNOPSIS

orbit remove [options] <ip>

DESCRIPTION

Deletes save data for a known ip from the catalog. The ip's data for its particular version is removed from the catalog's cache and the catalog's archive.

By default, an interactive prompt will appear to confirm with the user if the correct ip is okay to be removed. To skip this interactive prompt and assume it is correct without confirmation, use the --force option.

To add ip to the catalog, see the install command.

OPTIONS

<ip>
      Ip specification

--force
      Skip interactive prompts

--verbose
      Display where the removal occurs

EXAMPLES

orbit remove gates
orbit remove gates:1.0.1 --force

orbit env

NAME

env - print orbit environment information

SYNOPSIS

orbit env [options]

DESCRIPTION

Displays environment variables as key-value pairs related to Orbit.

By default, this command prints information as a shell script. If one or more variable names are given as arguments using <key>, then it will print the value of each provided variable on its own line.

Environment information can change based on where the command is executed.

Environment variables that are known only at runtime are not displayed. Be sure to review the documentation for a list of all environment variables set by Orbit.

OPTIONS

<key>...
      Display this variable's value

EXAMPLES

orbit env
orbit env ORBIT_HOME
orbit env ORBIT_MANIFEST_DIR ORBIT_IP_NAME

orbit config

NAME

config - modify configuration data

SYNOPSIS

orbit config [options] [<path>]

DESCRIPTION

Provides an entry point to the current configuration data through the command-line.

To list the configuration files that are currently being used, use the --list option. The configuration files are sorted in order from highest precedence to lowest precedence. This means values that are set in files higher in the list overwrite values that may have existed from files lowering in the list.

Providing the path of a configuration file using the <path> option will limit the accessible data to only the data found in the file. If no path is specified, then it will display the aggregated result of the current configuration data from across all files in use.

If there are no options set to modify data, then the resulting configuration data will be displayed.

To modify a field, the full key must be provided. Fields located inside tables require decimal characters "." to delimit between the key names. Each modified field is edited in the configuration file has the lowest precedence and would allow the changes to take effect. Files that won't be edited are configuration files that are included in the global config file. If the field does not exist in any configuration level, then the field will be modified at in the global config file.

When modifying data, additions are processed before deletions. This means all --push options occur before --pop options, and all --set options occur before --unset options. Not every configuration field can be edited through the command-line. More complex fields may require manual edits by opening its respective file.

OPTIONS

<path>
      The destination to read/write configuration data

--push <key=value>...
      Add a new value to a key's list

--pop <key>...
      Remove the last value from a key's list

--set <key=value>...
      Store the value as the key's entry

--unset <key>...
      Delete the key's entry

--list
      Print the list of configuration files and exit

EXAMPLES

orbit config --push include="profiles/hyperspacelab"
orbit config ~/.orbit/config.toml --unset env.vivado_path

Archive

The archive is the location where compressed snapshots of a specific ip's version exist. The compressed ip files are unable to be referenced as dependencies, but they are able to be installed to the cache for usage. The ip's in the archvie are considered downloaded.

Blueprint

A blueprint is a single file that lists all the necessary source files and auxiliary files for the build process of a particular target. The source files are listed in topologically-sorted order. The blueprint is generated by Orbit during the planning stage of the build process, and is typically immediately used as input to the execution stage of the build process.

A blueprint can store the information collected from the planning stage in different file formats, called schemes. The default scheme is tab-separated values (TSV).

Cache

The cache is the location where immutable references to a specific ip's version exist. An ip's dependencies are referenced from the cache. The ip that are found at the cache level are considered installed.

Catalog

The catalog is the user's entire space of currently known ip to Orbit. It consists of 3 main layers: cache, downloads, channels.

Channels

The channels are a set of decentralized registries that store the manifests for versions of ip. No source code is stored in a channel, however, Orbit is able to use the manifest as means to download that ip to the archive for local filesystem access. The ip that are found at the channels level are considered available. Users are encouraged to create and share their own channels.

Fileset

A fileset is a glob-style pattern for collecting files under a given name. Filesets are used to group common files together into the blueprint during the planning stage of the build process. A target can define its own filesets.

Intellectual Property (IP)

An ip is a project with a manifest file at its root directory. At a minimum, an ip has two attributes: a name and a version.

Ip Specification (spec)

The spec describes the format for identifying and referencing an ip. Each ip in the user's catalog must have a unique spec. The format of a ip's spec is: <name>[:<version>].

Local Ip

The local ip is the ip detected from the current working directory on the command-line. Some commands can only be executed from the local ip, such as orbit lock and orbit build.

Lockfile

A lockfile is a file that exactly describes an ip's dependencies. It is generated and maintained by Orbit. The lockfile should be checked into your version control system for reproducible builds. It is not to be manually edited by the user.

From the lockfile, Orbit is able to download missing dependencies, install missing dependencies, and verify the data integrity of installed dependencies.

Manifest

A manifest is a file that decscribes an ip recognized by Orbit. Manifest files are exactly named Orbit.toml. The manifest is intended to be written by the user.

Orbit.lock

See lockfile.

Orbit.toml

See manifest.

Package

See IP.

Profile

A profile is a user-defined group of plugins, settings, and/or channels under a single directory. A profile does not necessarily have to have all listed aspects in order to be considered a "profile".

Profiles are useful for quickly sharing and maintaining common development standards and workflows among a team environment.

Project

A project is a collection of HDL source files and any other required files related to a specific application or library. Placing a manifest in a project makes it an ip.

SystemVerilog

SystemVerilog is a hardware description and hardware verification language used to model, design, simulate, test, and implement electronic systems.

Target

A target is a user-defined command that is to be invoked by Orbit during the execution stage of the build process. A target typically follows 3 steps:

  1. Parse the blueprint file generated from the planning stage
  2. Process the listed hdl files using an EDA tool
  3. Test the design or build an output product

Targets can accept additional arguments from the command-line and define additional filesets to be collected for the planning stage. Users are encouraged to create and share their own targets.

VHDL

VHSIC Hardware Design Language (VHDL) is a hardware descrption language to model the behavior of digitally electronic circuits.

Verilog

Verilog is a hardware description language to model electronic systems. The final standard (IEEE 1364-2005) for Verilog merged into the SystemVerilog standard. Today, Verilog has been officially part of the SystemVerilog language.