Skip to content

Latest commit

 

History

History
164 lines (134 loc) · 6.55 KB

File metadata and controls

164 lines (134 loc) · 6.55 KB

Writing your first element

This tutorial walks you from zero to a working, custom element that you write, compile, and run in the software emulator — no FPGA required. By the end you will have built SeqStamp, an element that stamps a running sequence number onto every packet, and wired it into a two-port topology.

If you have not built the toolchain yet, do getting_started.md first (steps 1–4). Everything below runs at verification level L2 (software emulator), so you only need cmake + gcc-11.

The vocabulary you need first

OpenClickNP borrows its model from Click and the ClickNP paper. Five terms cover almost everything:

  • Flit — the unit of data on every wire between elements. It is a fixed 64-byte struct (openclicknp::flit_t, see runtime/include/openclicknp/flit.hpp). Bytes 0–31 are packet payload; byte 32 holds the flags below; the rest is metadata you can use freely.
  • sop / eopstart-of-packet and end-of-packet flags on each flit (f.sop(), f.eop()). A packet larger than 32 bytes spans several flits: the first has sop set, the last has eop set. A one-flit packet has both. Per-packet logic keys off sop/eop; per-flit logic ignores them.
  • Lanes — the 64-byte flit is also addressable as four 64-bit lanes via f.get(lane) / f.set(lane, value) for lane in 0..3. Lanes 0–3 overlap the payload bytes; elements use them to pass small metadata (a hash, a verdict, a next-hop) alongside or instead of payload. There is no enforced schema — communicating elements agree by convention which lane carries what.
  • Ports & port masks — an element declares <N_in, N_out> ports. Inside .handler you test/read inputs and write outputs by 1-based index using the PORT_1 … PORT_8 masks. The handler returns a mask saying which inputs to wait on next iteration.
  • Signal RPC — the optional .signal block is a host-callable control hook (rule installs, counter reads, resets). An instance marked @ in the topology is host-controllable.

Two edge kinds connect instances: A -> B is lossless (the producer stalls when the channel is full); A => B is lossy (the producer drops on full — used for taps that must never backpressure the datapath).

Step 1 — make a scratch design directory

A "design" is any directory containing a topology.clnp. The build scripts take a path, so it can live anywhere:

mkdir -p my_designs/SeqStamp
cd my_designs/SeqStamp

Step 2 — write the element

Create SeqStamp.clnp next to where the topology will live. The compiler resolves import first relative to the importing file, then against the -I search path — so a local element needs no special flags.

// SeqStamp.clnp
// Stamp a per-packet sequence number into metadata lane 3.
.element SeqStamp <1, 1> {
    .state {
        // Persistent across iterations. Plain C++17.
        uint64_t seq;          // next sequence number to assign
    }
    .init {
        _state.seq = 0;        // runs once at launch
    }
    .handler {
        // Runs every iteration. Do nothing unless input 1 has a flit.
        if (test_input_port(PORT_1)) {
            openclicknp::flit_t f = read_input_port(PORT_1);
            if (f.sop()) {                 // first flit of a packet
                f.set(3, _state.seq);      // write seq into lane 3
                _state.seq++;
            }
            set_output_port(1, f);         // forward the (modified) flit
        }
        return PORT_1;                     // wait on input 1 next time
    }
    .signal (uint cmd, uint param) {
        // Host RPC. Report the count; cmd==1 resets it.
        outevent.lparam[0] = _state.seq;
        if (cmd == 1u) _state.seq = 0;
    }
}

Three things to notice:

  • .state / .init / .handler are plain C++17 bodies. The compiler passes them through opaquely to every backend (SW emu, SystemC, HLS, Verilator) — the same source becomes hardware. That is why you write ordinary C++ but avoid pointers/recursion/unbounded loops that won't synthesize.
  • read_input_port(PORT_1) both returns the flit and marks the port consumed; set_output_port(1, f) enqueues on output 1.
  • The handler returns a port mask. Returning PORT_1 means "next iteration, only wake me when input 1 has data." Multi-input elements return PORT_ALL or a computed mask.

See language.md for the full list of .handler built-ins (input_ready, clear_input_ready, last_output_failed, …).

Step 3 — write the topology

Create topology.clnp in the same directory. We forward ToR→NIC through SeqStamp, and pass the reverse direction straight through with the stock Pass element:

// topology.clnp
import "core/Pass.clnp";   // resolved via -I .../elements
import "SeqStamp.clnp";    // resolved relative to this file

SeqStamp :: stamp @        // '@' = host-controllable (signal RPC)
Pass     :: rev

tor_in -> stamp -> nic_out
nic_in -> rev   -> tor_out

tor_in/tor_out and nic_in/nic_out are pseudo-elements that map to the two QSFP28 cages on hardware and to packet sources/sinks in the emulator.

Step 4 — compile and run in the emulator

From the repo root:

./scripts/sim/run_emu.sh my_designs/SeqStamp

This compiles topology.clnp to the SW-emu backend, builds it against the runtime, and runs it. You should see the emulator launch, process flits, and exit cleanly. Each packet that crosses ToR→NIC now leaves with an incrementing value in lane 3, and stamp's signal RPC reports how many packets have been stamped.

To inspect generated code, look in build/SeqStamp/generated/.

Step 5 — add a behavioral test (optional but encouraged)

Library elements ship with a per-element test under tests/elements/ that replays canned flits and asserts on outputs (plain assert() + exit code — no external framework). If you promote your element into elements/<category>/, add tests/elements/test_SeqStamp.cpp and register it in tests/elements/CMakeLists.txt; ctest will then cover it. See CONTRIBUTING.md for the checklist.

Where to go next

  • language.md — full .clnp reference: element groups, *depth channel overrides, lossy edges, .timing { ii = N; }.
  • architecture.md — how a .clnp graph becomes six different backends.
  • examples/ — 40+ worked designs, each with its own README. Good next reads: IP_Forwarding (lookup tables), Firewall (host rule install), RateLimiter (token-bucket state).