hexforge / build flow
Build flow
Four programs, three intermediate files, no daemon and no project database. You can stop after any stage, read the file it produced, edit it by hand if you really want to, and carry on.
hxc - front end
Lexes and parses the accepted subset, then elaborates: parameters are resolved, generate loops unrolled, hierarchy flattened into a single namespace with dotted names kept only as comments. Register inference follows the usual rule - an always @(posedge clk) block with non-blocking assignments becomes flip-flops, anything combinational becomes gates. Constant folding and dead-cell removal run once, in that order.
The output is text. A fragment of blink.hxn, which is roughly as readable as this format gets:
# hexforge netlist v4
# source blink.v, 2026-10-04T09:12:41Z, hxc 0.7.2
module blink
port in clk
port in rst_n
port out led
clock clk posedge
reg count[23:0] clk async_none
cell and2 u_14 a=count.0 b=count.1 y=n_31
cell not1 u_15 a=rst_n y=n_32
net led = count.23
Flattening before synthesis is a simplification with a cost: error messages point at a flat cell name, and tracing u_14 back to a line of HDL means reading the comment block at the top of the module. Keeping full hierarchy through the flow is on the list for 0.8.
The constraints file
Pin assignment and clock declarations live in a .hxs file next to the source. The syntax is small enough to learn from one example:
# examples/blink/pebble-25.hxs
set_io clk A7
set_io rst_n C9
set_io led B3
create_clock clk 12.0MHz
set_false_path rst_n -> *
Four directives are understood: set_io, create_clock, set_input_delay and set_output_delay, plus set_false_path for the analyser. Anything else in the file is an error, not a warning - a constraint that is silently ignored is worse than a build that stops.
hxsyn - mapping, placement, routing
- Mapping. Cut-based LUT mapping with k = 4, depth first and area as the tie-break. No technology-specific tricks, no carry-chain inference beyond a pattern match on
a + bwith a single-bit carry. - Packing. LUTs and flip-flops go into logic tiles of eight each. The packer is greedy with one refinement pass, and it is the weakest part of the tool: above about 90% occupancy it starts failing to find legal arrangements, which is exactly what the red
pack_dense_m1test records. - Placement. Simulated annealing on a wirelength cost, seeded from
--seed(default 1). Typical runs take a fifth of total build time. - Routing. Negotiated congestion, the familiar iterative scheme where nets rip up and re-route while the cost of shared resources climbs. It gives up after 40 iterations and reports unrouted nets rather than producing a half-routed design.
Timing is not part of any of those four steps. Placement optimises wirelength, not slack, and the router has no notion of a critical path. On designs below a few thousand LUTs this matters less than it sounds; above that it is the main reason results fall behind mature tools, and it is stated plainly on the timing page.
hxpack - bitstream
Serialises the routed design into device frames, prepends the configuration header for the mode the board uses, and appends a CRC. Nothing clever happens here; the only interesting flag is --dump-frames, which writes a human-readable frame listing next to the bitstream for when a board refuses to configure and you need to know whether the problem is upstream.
Bitstream compression is not implemented. On a T4-25K a full bitstream is 141 312 bytes, which fits every supported board's flash with room to spare, so the work has never been justified.
Determinism
The same source, constraints, device files, seed and version produce a byte-identical bitstream. Each release is checked by building all eight examples twice on two machines and comparing hashes. The build is single-threaded end to end, which costs wall-clock time and buys this property - a trade the project intends to keep.
Change the seed and you get a different placement and slightly different timing; that is the cheapest knob when a design misses its target by a few per cent. Sweeping four or five seeds is standard practice here, and hxta on the result tells you which one to keep.
Typical runtimes
| Design | LUT4 | hxc | hxsyn | hxpack |
|---|---|---|---|---|
| blink | 25 | 0.02 s | 0.6 s | 0.3 s |
| uart_loopback | 310 | 0.05 s | 3.1 s | 0.3 s |
| vga_bars | 980 | 0.11 s | 14 s | 0.4 s |
| fifo_cdc | 2 140 | 0.21 s | 48 s | 0.4 s |
| congested T4-45K case | 19 400 | 1.8 s | 4 min | 0.9 s |
What the flow does not contain
- No simulator. Verify your HDL with an external simulator before synthesis; the compiler is two-state and will not reproduce any behaviour that depends on unknown values.
- No incremental build. Change one line and everything re-runs. For designs this size that is acceptable; for larger ones it would not be.
- No floorplanning beyond pin assignment. You cannot pin a module to a region.
- No retiming, no clock gating insertion, no logic replication for timing.
- No programming step, by choice - the loader that came with your board already works.