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:
- Tutorials - Step-by-step lessons working with Orbit
- User Guide - General procedures for "how-to" solve common problems
- Topic Guide - Explanations that clarify and provide more detail to particular topics
- 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:
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
- Visit Orbit's releases page on Github to find all of its official releases.
- Download the binary for your computer's architecture and operating system.
- Install Orbit. Either run the provided
install
executable or follow the manual instructions for placing Orbit's executable (orbit
for Unix andorbit.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
- 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
- Unzip the prebuilt package.
unzip orbit-0.22.1-x86_64-linux.zip
- 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
- 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
- Unzip the prebuilt package.
unzip orbit-0.22.1-x86_64-macos.zip
- 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
-
Open a new terminal (Powershell) to where Orbit was downloaded.
-
Unzip the prebuilt package.
$ expand-archive "./orbit-0.22.1-x86_64-windows.zip"
- Make a new directory to store this package.
$ new-item -path "$env:LOCALAPPDATA/Programs/orbit" -itemtype directory
- Move the package to the new directory.
$ copy-item "./orbit-0.22.1-x86_64-windows/*" -destination "$env:LOCALAPPDATA/Programs/orbit" -recurse
- 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:
-
Removes any executable in the executable's directory starting with
orbit-
(these are considered stale binaries, such asorbit-0.1.0
). -
Connects to https://github.com/chaseruskin/orbit/releases to find the most recent released version.
-
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.
-
Downloads the checksum file to a temporary directory to see if there is a prebuilt package available for the current architecture and operating system.
-
Downloads the package to a temporary directory and computes the checksum to verify the contents.
-
Renames the current executable by appending its version to the name (marking it as a stale binary, such as
orbit-0.1.1
). -
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. Runningorbit 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
(ororbit 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.
-
Tell Orbit which installed ip you wish to use by providing the name and version under the
[dependencies]
table in the Orbit.toml file. -
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 downloadedorbit.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] entry | Environment variable | Substitution variable |
---|---|---|
foo | ORBIT_ENV_FOO | orbit.env.foo |
github-user | ORBIT_ENV_GITHUB_USER | orbit.env.github.user |
Yilinx_Path | ORBIT_ENV_YILINX_PATH | orbit.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:
- All primary design unit identifiers in the current ip must be unique within the scope of the ip.
- 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 thenand_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.
Spec | Name | UUID | Version |
---|---|---|---|
gates:1.0.0 | gates | Automatically resolved if only 1 ip exists with the name gates | 1.0.0 |
ram | ram | Automatically resolved if only 1 ip exists with the name ram | latest |
fifo:2.3 | fifo | Automatically resolved if only 1 ip exists with the name fifo | 2.3.* |
cpu+71vs0nyo7lqjji6p6uzfviaoi:1.0.0 | cpu | 71vs0nyo7lqjji6p6uzfviaoi | 1.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 1 | Spec 2 | Collision |
---|---|---|
gates | GATES | true |
ram | rom | false |
fifo_cdc | Fifo-CDC | true |
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 ]
Level | Explanation |
---|---|
Major | Incompatible API changes |
Minor | Adding functionality in backward-compatible way |
Micro | Fixing bugs in backward-compatible way |
Label | Descriptive 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:
Requested | Returned |
---|---|
1 | 1.5.0 |
1.1 | NOT FOUND |
1.2 | 1.2.1 |
2 | 2.1.0 |
1.2.0 | 1.2.0 |
latest | 2.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 Fileset | Normalized Fileset |
---|---|
GOOD-SET | GOOD-SET |
Set-1 | SET-1 |
set_2 | SET-2 |
set_three | SET-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 pattern | Interpreted 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:
- Tab-separated values:
blueprint.tsv
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:
Fileset | Supported 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:
-
Local configuration file (current ip's
.orbit/conifg.toml
) -
Regional configuration files (parent directories of the current working directory)
-
Global configuration file (Orbit's
$ORBIT_HOME/config.toml
) -
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.
- target-dir - Default target directory.
- [test] - The test settings.
- default-target - Set the default target for tests.
- [build] - The build settings.
- default-target - Set the default target for builds.
- [vhdl-format] - VHDL code formatting.
- [verilog-format] - SystemVerilog/Verilog code formatting.
- [env] - The runtime environment variables.
- [[target]] - Define a 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 search
- orbit view
- orbit install
- orbit publish
- orbit download
- orbit remove
- orbit config
- orbit env
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:
- Parse the blueprint file
- Process the referenced files listed in the blueprint
- 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:
- Parse the blueprint file
- Process the referenced files listed in the blueprint
- 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 --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:
- Parse the blueprint file generated from the planning stage
- Process the listed hdl files using an EDA tool
- 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.