vw builds designs made of VHDL, Vivado IP, testbenches, and a Rust kernel
driver. It resolves dependencies, drives the Vivado flow, runs testbenches,
builds the driver, and does that work on Oxide Cloud build instances.
cargo install --git https://github.com/oxidecomputer/vwvw reads a GitHub token from ~/.netrc — entries for github.com and
api.github.com. It fetches private dependencies with it and identifies you to
the build service with it.
machine github.com
login <your-github-username>
password <a personal access token>
Vivado and the illumos toolchain run on cloud instances, not on your machine. Create an environment, then work in your workspace as normal:
vw cloud create my-env --wait # vivado, helios and artifact cloud instances
vw check # parse and analyze the design
vw run # build the FPGA image
vw bench run # run testbenches
vw driver build --release # build the kernel driver
vw cloud artifacts --all # download build output
vw cloud delete my-env # release the instancesvw cloud create records the environment in vw-cloud.toml, so nothing after
it has to name one. $VW_ENV and an explicit --env still win, in that order.
vw run, vw check, vw bench run and vw driver build synchronize the
workspace to the environment before they run, and stream output back as it
happens. Pass --local to any of them to run on this machine instead, which
needs the toolchains installed locally.
An environment holds a source tree per workspace rather than one tree, so the
three instances it costs are shared by everything you are working on. Which
tree a command acts on is the workspace's name — [workspace] name from
vw.toml, unless this checkout says otherwise.
Two checkouts of the same project would otherwise resolve to the same name and
overwrite each other on every sync, which is what vw cloud set workspace is
for:
cd ~/src/redhawk-feature
vw cloud set workspace redhawk-feat # this checkout gets a tree of its own
vw cloud workspaces --sizes # what is on the environment, and how old
vw cloud forget redhawk-old # tree, build output and artifactsThat name is only the key — which directory the tree goes in and which bucket
its artifacts land in. It is deliberately not [workspace] name, which is
what a design's own imports resolve through (src @redhawk/...) and what
vw::project_name reports: change that and the two checkouts you are comparing
would differ in a way that has nothing to do with the change under test.
Both settings live in vw-cloud.toml beside vw.toml, which vw adds to
.gitignore — they describe a checkout, not the project, and committing one
would send everybody on that branch to the same tree. Every sync prints which
environment and workspace it is pushing to, and where each name came from.
A vw older than workspaces still works against a current service: it names no
workspace, so it gets a reserved one called default, entirely its own. That
is why vw cloud workspaces may list a default nobody created — it is
whatever has been synced by a client that has not been upgraded yet, and
vw cloud forget default removes it like any other.
By default the vw client talks to the vw build service at
https://vw-cloud.dev.
Other deployments are reached by naming them. A beta runs at
https://beta.vw-cloud.dev, for trying a build of vw-svc and its agents before
it becomes the one everybody uses:
export VW_SVC_URL=https://beta.vw-cloud.dev
vw cloud list # the beta's environments, not production'sA URL is the whole of how a deployment is chosen — there is no flag naming one,
and nothing in the client knows how many there are. It is an environment
variable as well as vw cloud --url because vw run, vw check and the rest
have nowhere to put a flag, the same reason VW_ENV is a variable.
Each deployment's environments are a separate set, and its instances boot its
own images, so an environment created against one is only reachable against
that one. vw cloud admin speaks to a second listener on port 2053, which does
not follow --url; --admin-url or VW_SVC_ADMIN_URL names it.
vw --help lists every command, and vw <command> --help its options.
vw.toml workspace configuration and dependencies
vw.lock resolved dependency commits
design.htcl build entry point
flow/*.htcl individual flow stages
ip/**/*.htcl IP configuration
hdl/**/*.vhd VHDL design sources
bench/ testbenches
driver/ Rust kernel driver, its own cargo workspace
constraints/ XDC and SDC, optionally per-stage under synth/ place/ route/
target/ build output — never synchronized, never committed
vw finds every source from this layout, so nothing else has to be configured.
vw sources design and vw sources driver print the exact file list a build
reads.
vw repl is an interactive htcl session against a live Vivado worker. It
sources the workspace's design.htcl on start, or --load <file> to source a
different one.
This is usually what you want while working on the design. vw run is a
one-shot build; the REPL keeps Vivado up with the design still loaded, so when
something fails or synthesizes into something you did not expect, every Vivado
analysis command is available against the design as it stands — timing, cells,
nets, clocks, utilization, DRC. Fix the htcl or VHDL, re-source, and go again
without paying for another elaboration.
It can also start from a checkpoint rather than from source:
vw repl --from-synth-checkpoint # or --from-place-checkpoint, --from-route-checkpointThat opens the saved checkpoint for that stage and drops you at a prompt with the design loaded, which is the fastest way to look at the result of a run that has already happened.
vw bench run runs the workspace's testbenches with NVC, as many at once as
there are cores.
vw bench run # everything
vw bench run parser # only names containing "parser"
vw bench listThere are three kinds of testbench, and one command each:
vw bench init fifo # pure VHDL
vw cosim init fifo # Rust cosim
vw mist init fifo # mixed-signal (VHDL + Xyce)Each writes a bench that runs, and passes, straight away — so the plumbing is
known to work before the first check is written. Nothing else has to be
registered anywhere: a Rust bench is added to the bench cargo workspace, and
vw bench run finds all three kinds on its own. vw bench remove,
vw cosim remove and vw mist remove take one back out again, workspace
membership included.
Point one at a design entity and its interface is read out of the workspace and wired up:
vw bench init fifo --dut flit_fifo
vw cosim init fifo --dut flit_fifo --clock 250e6
vw mist init fifo --entity flit_fifo --clock 26.5625e9A pure VHDL bench comes out with a signal per port carrying that port's own type, the generic map and the port map filled in.
A cosim bench comes out with a Rust handle per port. There is no VHDL harness:
nvc elaborates the design entity itself and loads the driver beside it, so
there is no second copy of the port list to keep in step. A record port
becomes one handle per element, since that is how the simulator presents it,
and everything the driver writes is sized from the signal rather than from the
type it was declared with — so a subtype, or a width that comes from a
generic, needs nothing said about it. Generics without defaults are surfaced
in cosim.toml, which is where the entity is named and where elaboration gets
its -g overrides.
A mixed-signal bench comes out with a mist.toml mapping every output the
bridge can read to an analog source, and a circuit with a DAC for each.
Cosim testbenches are built on rust-cosim: the stimulus and checking are a Rust program driving the VHDL, rather than a VHDL harness.
Types cross the boundary rather than being maintained twice. A VHDL record
carrying the serialize_rust attribute gets a matching Rust type generated by
anodizer, so the testbench sees
the same record the design does. It regenerates when the design sources change.
That normally happens invisibly, which is no use when the generator itself is
what you are working on. vw cosim anodize <bench> runs it on purpose — cache
off, what it found reported — and then compiles that bench against the result,
because a generator that succeeds and emits Rust which does not compile is the
failure that otherwise surfaces much later:
vw cosim anodize # just run the generator
vw cosim anodize fifo # ...and build bench/fifo against it
vw cosim anodize fifo --local # on this machineMixed-mode digital/analog simulation runs against
Xyce. A bench directory containing a mist.toml
gets a bridge crate connecting NVC to Xyce over VHPI, so a testbench can drive
an analog model alongside the digital design.
All of that is prepared by vw before the testbenches run — the generated types, the Rust builds, and the mixed-signal scaffolding.
A build needs at least two machines: Linux with Vivado for FPGA images, and
illumos for the kernel driver. vw-svc manages environments of those machines
on an Oxide Cloud Computer, and vw drives them.
┌───────────────────────────────────────────────┐
│ Oxide Cloud Computer │
│ │
│ ┌───────────────────────────┐ │
│ │ environment ├┐ │
src, │ │ ┌──────────┐ ┌──────────┐ │├┐ │
┌────────┐ commands │ ┌────────┐ │ │ vivado │ │ helios │ │││ │
│ │─────────────┼─▶│ │ │ │ instance │ │ instance │ │││ │
│ vw-cli │ │ │ vw-svc │ │ └──────────┘ └──────────┘ │││ │
│ │◀────────────┼─ │ │ │ ┌──────────┐ │││ │
└────────┘ results │ └────────┘ │ │ artifact │ │││ │
│ │ │ instance │ │││ │
│ │ └──────────┘ │││ │
│ └┬──────────────────────────┘││ │
│ └┬──────────────────────────┘│ │
│ └───────────────────────────┘ │
│ │
└───────────────────────────────────────────────┘
An environment is three instances. The vivado instance runs the Vivado flow, testbenches and htcl. The helios instance builds the driver. The artifact instance runs an S3 server holding build output and the intermediate artifacts the other two share.
vw never talks to an instance directly. It sends source and commands to
vw-svc, which decides who owns the environment and relays to a vw-agent on
each instance. Long-running commands are relayed over websockets, so a
synth/place/route run streams output as it happens and Ctrl-C interrupts it.
Synchronization is content-addressed: a sync sends only file contents the
instance does not already have. Both instances get the whole workspace: the
design and the driver are one project, and which files a build reads is not a
line that stays put. target/ is never sent in either direction, which is what
lets Vivado checkpoints on an instance survive from one command to the next.
Each instance keeps a tree, a content store and an artifact bucket per
workspace, all keyed by the same name. Nothing declares which workspaces an
environment has — one exists because somebody synchronized it — so
vw cloud workspaces asks the instance rather than the service.
Build output lands in the artifact instance's S3 store as it is produced.
vw cloud artifacts <env> lists it and --get <pattern> downloads by glob
('*.edif', 'reports/*place*'), streamed through vw-svc so clients never
need to reach the artifact instance themselves.
vw cloud list # your environments
vw cloud get # instance states and addresses
vw cloud keys # ssh key for the instances
vw cloud sync [--watch] # push the workspace explicitly
vw cloud artifacts --get '*.pdi'
vw cloud workspaces [--sizes] # what is on the environment
vw cloud forget <workspace> # remove one, with its artifacts
vw cloud set workspace <name> # this checkout's tree on the environment
vw cloud set environment <name> # the environment it builds in
vw cloud admin list # every environment, for administratorsEvery command that takes an environment takes it as an optional argument,
falling back to $VW_ENV, then to vw-cloud.toml, then to your only
environment. vw cloud create and vw cloud delete are the exceptions: both
name what they act on, because inferring it is meaningless for one and
dangerous for the other.
Running the service is documented in vw-svc/dist/.
[workspace]
name = "my-design"
version = "0.1.0"
# htcl library — no `src`, the whole repository is the module
[dependencies.vivado-cmd]
repo = "https://github.com/oxidecomputer/vivado-cmd.htcl"
branch = "main"
# VHDL sources — `src` selects directories, files, or globs
[dependencies.quartz_common]
repo = "https://github.com/oxidecomputer/quartz"
branch = "main"
src = ["hdl/ip/vhd/common/utils", "hdl/ip/vhd/fifos"]
exclude = ["**/sims/**", "**/*_tb.vhd"]vw update resolves branches to commits, records them in vw.lock, and caches
sources under ~/.vw/deps/<name>-<commit>/. A build fetches what the lockfile
names, so an instance gets exactly what your machine would.
Variants build the same sources for different boards:
[[workspace.variants]]
name = "vpk120"
part = "xcvp1202-vsva2785-2MHP-e-S"
top = "top_vpk120"
default = true
exclusive = ["hdl/top-vpk120.vhd", "hdl/ethernet-vpk120.vhd"]Select one with --variant <name>. An exclusive file belongs only to its
variant.
Each flow stage fingerprints its inputs by content and writes a Vivado checkpoint next to that fingerprint. A stage whose inputs are unchanged reads its checkpoint instead of running; a stage whose inputs changed discards its checkpoint and every downstream one. When a stage does not reuse its checkpoint, it says why.
docs/htcl.md— the htcl language, and writing modules in it