Contributing to nd-120¶
Thanks for looking. This file says how to get a build running, what the code is expected to look like, and what will get a change sent back.
Getting a build¶
The work is done on Linux or WSL2 with bash. The simulators and the test suite run there; the FPGA vendor tools (Vivado, Gowin, Quartus) run on the Windows host and are only needed to build a bitstream. Ready-built bitstreams are on the releases page, so most changes need no vendor tool at all.
| # | Requirement | How to check |
|---|---|---|
| 1 | Icarus Verilog - the unit test benches (*_tb.v) |
iverilog -V \| head -1 |
| 2 | Verilator 5.x - the whole-machine simulation (test_*.cpp) |
verilator --version |
| 3 | Python 3, dosfstools, mtools, yosys - test helpers and the Tang netlist checks |
python3 --version |
The CI job installs exactly these on Ubuntu:
Every tool, its version on the development machine and what uses it is in Verilog/docs/PREREQUISITES.md. Every build option is in Verilog/docs/build-defines.md.
# from the repository root
python3 configure.py # once per clone: tools, submodules, microcode images, local.mk
make -C Verilog test # every self-checking test bench
make test itself needs no local setting (the CI runs it on a clean clone with only
cd Code/Microcode && python3 gen_wcs_image.py before it); configure.py is what makes the
microcode images on your machine and what every board build reads - see
Local settings.
make test runs every registered test bench, fail-fast: the first failure stops the run with
exit 1. make test-instr runs the INSTRUCTION-B trace gate, and make test-full adds the heavy
system gates. BUILDING.md has the simulator and board build steps.
The rule that will stop you first: every test bench is registered¶
A test bench lives in a sim/ folder next to the module it tests - there is no central test
tree. And every test bench must be added to the registry in
Verilog/tests/run_all_tests.sh, with a strict pass pattern
(TB_RESULT: PASS is the convention). A test bench that is not registered never runs, and the
suite's own first check (test-tb-catalog) fails when it finds one.
Every test bench must print a verdict a machine can check. A test that can pass silently can fail silently.
What the code is expected to look like¶
- Keep the Logisim structure. Most of the Verilog was first made from the Logisim-Evolution schematics; it is now kept by hand, so a fix to the logic belongs in both places.
- Names: internal signals use the
s_prefix; buses areBUSNAME_BITS(CD_15_0); active-low signals end in_n. - No
zinside the FPGA. A "3-state" buffer drives0when disabled, notz. - Flip-flops on silicon, latches only in simulation. The FPGA builds run in flip-flop mode;
test-no-latchesenforces it.make compareinVerilog/sim/proves a change leaves the latch and flip-flop traces identical. - Octal. The ND-120 is an octal machine; addresses and data values are written in octal.
- Test benches only. Never add a standalone program that prints something and exits - it cannot fail a build.
- Keep the comments, and add more. A comment that records a design-document page, a datasheet reference, or the reason a thing is NOT done the obvious way is the most valuable line in the file. Never delete one because a style guide prefers less; replace one only when it has become factually wrong.
- Plain words. In code, comments, commit messages and documentation alike. No jargon where a normal word works.
Naming¶
The organisation is RetroCoreLabs; the old name HackerCorpLabs is retired and must not
appear in anything new.
Documentation changes¶
There are two documentation checkers. Both must pass.
# from the repository root
python3 eng/check-docs.py # the RetroCore Labs house standard
make -C Verilog/tests test-docs-check # this repository's dead-link gate (runs in CI)
eng/check-docs.pyis the house-standard checker, shared by every RetroCore Labs repository. It fails on a relative link that does not resolve or leaves the repository root, a machine-specific absolute path, a hand-written contents list, a Mermaid diagram that is malformed or breaksMERMAID_COLOR_STANDARDS.md, and a README that does not follow the template. Waivers, each with its reason, are ineng/check-docs.json.Verilog/tests/check_md_links.pyis this repository's own gate. It resolves every relative link in every tracked.mdfile and is part ofmake test, so a dead link fails the CI run.
[!WARNING] Never put an absolute path in anything committed here. A drive letter or a home directory is correct on exactly one machine, and this repository is public. A path inside the repository goes repo-relative; a sibling ND repository is written
$ND_REPOS/<repo>/...; anything outside is described. A variable such as%USERPROFILE%or~is fine - that is the portable way to name a per-user location. There are no exceptions: the build scripts read tool and folder locations fromlocal.mkat the repository root (written byconfigure.py, see Local settings) or the environment, andmake testfails on any machine path in a tracked file.
Paths in scripts and build files¶
No script, Makefile, GTKWave save file or comment names a folder on one particular machine.
make -C Verilog/tests test-no-machine-paths (part of make test) fails on any tracked file
that does.
- A path inside the repository is worked out at run time from the script's own location
(Python
os.path.dirname(os.path.abspath(__file__)), sh$(cd "$(dirname "$0")" && pwd), make$(dir $(abspath $(lastword $(MAKEFILE_LIST)))), Tcl[file dirname [file normalize [info script]]], PowerShell$PSScriptRoot). In comments and documents it is written repo-relative. A GTKWave.gtkwfile stores its dump and save paths relative to its own folder. - A path outside the repository is one of the named settings in Local settings:
kept in
local.mk, or set in the environment. When a script needs one that is not set, it stops with a message naming it and the command that sets it.
Local settings¶
Everything that differs between machines - where the tools are installed, where builds go, where
the other ND repositories are - is a named setting. They live in local.mk at the repository
root: machine-local, gitignored, never committed, written by configure.py.
python3 configure.py # set up a clone: asks for what it cannot find
py configure.py # the same from a Windows shell (cmd / PowerShell)
python3 configure.py --check # every setting, where it came from, what is missing
python3 configure.py --set ND120_BUILD_DIR=<folder> # change one (NAME= removes it)
python3 configure.py --non-interactive # no questions: --set values, the environment, the tool search
make check-config # the same as --check, from any Makefile folder
What configure.py does: finds vivado, gw_sh (Gowin), quartus_sh, oss-cad-suite and
w64devkit and netlistsvg on the PATH and in the usual install folders (from WSL it also looks on the Windows
drives), reports verilator, iverilog, yosys, python3, make and git; asks only for what
it could not find, plus the build folder and ND_REPOS; writes local.mk; runs
git submodule update --init; makes the microcode preload images (Code/Microcode/gen_wcs_image.py
for the boards, --sim for the simulators); creates the build folder. Running it again keeps the
answers as defaults. It is Python 3 standard library only and runs the same on Linux, WSL and
Windows. A Windows program (vivado.bat, gw_sh.exe) is always stored as a Windows path, since
it is started through powershell.exe from WSL; every reader converts X:\... and /mnt/x/...
for its own side.
Which value wins, the same rule in every reader: a value in the environment wins over
local.mk, and under make a value on the command line (make NAME=value) wins over both.
Who reads local.mk: every Makefile that needs a setting, through paths.mk at the repository
root; the PowerShell scripts through Verilog/fpga/paths.ps1; the Vivado and Gowin Tcl scripts
through Verilog/fpga/paths.tcl (so vivado -source build.tcl from a Windows shell works too);
the Python and shell helpers through configure.py (configure.setting(), --get).
Checks come first. Each target checks the settings IT needs before doing any work and stops
with one message: which variable, what it is for, whether local.mk is missing or only lacks the
value, or that a tool path does not exist, and the command that fixes it. make test and the
simulator targets need none of them.
| Setting | What it is | Read by | Required |
|---|---|---|---|
ND120_BUILD_DIR |
where builds go: every board writes everything to <this>/<board>/ |
every board Makefile, build.tcl, the .ps1 wrappers |
yes, for any board build |
ND120_VIVADO |
the Vivado program (vivado.bat / vivado); make VIVADO=... overrides it |
Basys3, Cmod A7, Nexys 4 DDR, QMTECH, MEGA65 | for those builds |
ND120_VIVADO_LICENSE |
Vivado licence file list, handed on as XILINXD_LICENSE_FILE |
paths.mk, run_tool.ps1, the Vivado .ps1 wrappers |
no |
ND120_GOWIN |
the Gowin gw_sh program |
Tang Nano 20K make gowin, gowin_build.ps1 |
for the Gowin build |
ND120_QUARTUS |
the Quartus bin folder |
nothing yet (the MiSTer build runs Quartus in Docker) | no |
ND120_OSS_CAD_SUITE |
the oss-cad-suite folder | Tang Makefile (OSS flow), Verilog/ND-120-Yosys/synh.bat, BUS-IF gate example |
no (else the tools from PATH) |
ND120_W64DEVKIT |
the w64devkit folder | BUS-IF gate Windows example | no |
ND120_NETLISTSVG |
the netlistsvg program (npm, installed outside the repository) |
Verilog/tests/gen_schematics.py: the module schematics |
no (else netlistsvg from PATH; without it the schematics are not redrawn) |
ND_REPOS |
the folder holding the sibling ND checkouts (ND110Compile, RetroTerm, NDDeviceCore, ...) |
Verilog/tests/instruction-verify/ (golden traces, listings), Verilog/sim/compare_boot.py |
for make test-instr |
ND120_ILA_CSV |
an ILA capture exported from Vivado as CSV (iladata.csv) |
Verilog/sim/analyze_ila.py, compare_boot.py |
for those scripts |
ND120_ORACLE_DIR |
where long trace captures are kept (too big to commit) | Tang capture scripts, Verilog/sim/Makefile |
no |
ND120_SIM_WSLDIR |
the Verilog/sim folder as WSL sees it; by default worked out from the script's own location |
Verilog/sim/nd120_probe.py |
no |
ND120_FRESH_DIR |
an empty folder for make fresh-build (usually given on the command line) |
make fresh-build in every board folder |
for make fresh-build |
ND120_BASYS3_PROJECT (the old Basys3 Vivado project folder) is read by nothing since the Basys3
build became a non-project flow; configure.py keeps it only if you set it.
make fresh-build ND120_FRESH_DIR=<empty folder>, in any board folder, clones the current
commit into that folder, runs configure.py --non-interactive there with the same tool settings
but a build folder inside the clone, and runs the board's build target in the clone. It proves the
build needs nothing outside the repository. It never runs by itself.
The module docs and the docs site¶
The docs site, https://retrocorelabs.github.io/nd-120/, is built from the Markdown in this
repository by MkDocs with the Material theme. The settings are in
docs-site/mkdocs.yml; .github/workflows/jekyll-gh-pages.yml builds and publishes it on every
push to main (the site only - no synthesis, no bitstreams).
The module pages, the hierarchy and the site's page tree are generated, locally, and committed.
One command, in WSL or Linux, from Verilog/:
Use the system python3 (it needs Pillow): do not source ~/oss-cad-suite/environment first,
because that puts oss-cad-suite's own Python, which has no Pillow, ahead of it on the PATH.
It writes every module page (<folder>/doc/<Module>.md, with the symbol picture, the schematic
<Module>.svg and the Verilog source), Verilog/MODULES.md, Verilog/HIERARCHY.md and
docs-site/nav.yml (the left-hand page tree). The schematics need netlistsvg
(ND120_NETLISTSVG, see the table above). Install it outside the repository:
mkdir -p ~/tools/netlistsvg && cd ~/tools/netlistsvg && npm install netlistsvg
python3 configure.py --set ND120_NETLISTSVG=$HOME/tools/netlistsvg/node_modules/.bin/netlistsvg
To redraw only a few schematics (it still elaborates the build tops first, under a minute):
The generator reads the files in your working tree, not the last commit. Every module page
carries its Verilog source and a schematic drawn from it, so an uncommitted edit in the checkout
ends up on the published page. If you have work in progress, run the generator in a clean clone
of the commit you want documented, commit there, and bring that commit into your checkout
(git fetch <clone> main then git merge --ff-only FETCH_HEAD).
A port's description on its page is the //! comment on that port's line in the Verilog. To
change one, edit the comment in the .v file and regenerate; never edit a generated page.
Build the site locally before pushing a docs change, in a Python venv outside the repository.
--strict fails on any broken link, the same as the published build:
python3 -m venv ~/venvs/nd120-site
~/venvs/nd120-site/bin/pip install -r docs-site/requirements.txt
~/venvs/nd120-site/bin/mkdocs build --strict -f docs-site/mkdocs.yml -d ~/nd120-site-out
MkDocs does not allow the output folder inside the docs folder, and the docs folder is the whole
repository, so the output (-d) always goes outside the checkout.
Submitting a change¶
- Branch off
main. Do not commit tomaindirectly. - One logical change per commit. A commit message says what changed and why, in plain words - the subject line in the imperative, then a blank line, then the reasoning. If you measured something, put the number in the message.
make -C Verilog testpasses before you open the pull request.- Say in the pull request what you verified and how. "Tests pass" on its own is not useful;
"the SC2661 transmit bench now checks the bytes it sends, is registered, and
make testis green end to end" is. If you ran it on a board, say which board, which clock and which build. - If something is still broken or unfinished, say so plainly. A known gap that is written down is fine. One that is hidden is not.
Licence¶
By contributing you agree your work is licensed under the MIT licence in LICENSE.