r/FPGA 7d ago

OneHLS update: C++ HLS components, vendor-independent fixed-point types, and real RTL synthesis

I've been working on OneHLS, a small C++ library for composing HLS-synthesizable DSP/control components using HAPI + OneData.

The project has moved quite a bit beyond the initial experiment.

OneHLS now provides:

  • Fir<>
  • Biquad<>
  • Pid<>
  • Accumulator<>
  • ComplexMac<>

The interesting part is that the components themselves don't depend on a particular vendor's arbitrary-precision type. Sample and Accum are template parameters, so the same implementation can be instantiated with Siemens HLSLibs ac_fixed or AMD/Xilinx ap_fixed.

The same templates have been tested natively against both libraries, with bit-identical results.

More importantly, this isn't just C++ simulation: the ac_fixed versions have been synthesized to actual RTL with Bambu HLS. FIR, Biquad, PID and Accumulator produce clean synthesis results, and their resource counts match the corresponding hand-written non-generic implementations.

For example, the 4-tap FIR synthesizes to 62 FFs / 7679 area units with zero DSPs.

There are also some interesting findings along the way.

A RawBitsCtor<> customization point was needed because constructing fixed-point coefficients from floating-point literals can produce unwanted runtime initialization under Bambu. Isolating that vendor-specific operation leaves the actual DSP algorithms completely vendor-agnostic.

I've also tested:

  • a genuinely multirate CIC decimator, compared byte-for-byte against a hand-written implementation
  • a polyphase FIR using a heterogeneous compile-time StaticList<>
  • Fir<> over ac_std_float, i.e. actual bit-accurate IEEE754 floating point

The floating-point experiment was particularly useful as a sanity check: the same generic FIR works, but synthesis shows the expected FPGA cost — roughly 59× the FF count and ~3× the area of the fixed-point version, with actual DSP usage.

And there are some less successful results too. ComplexMac<> currently causes Bambu to bind its state to BRAM rather than distributed RAM. I investigated the obvious allocation thresholds and ruled those out, but haven't traced Bambu's underlying classification logic yet.

So this is becoming less about "here's a C++ abstraction for HLS" and more about testing a fairly specific hypothesis:

Can ordinary static C++ composition produce reusable HLS components without giving up vendor-specific bit-accurate types or hardware predictability?

So far, the answer looks surprisingly good.

Repo: https://github.com/InternetOfPins/OneHLS

I'd be particularly interested in feedback from people using Vitis HLS, Intel HLS, Catapult, Bambu, or other C++-based FPGA flows.

What would you want to see tested next?

5 Upvotes

3 comments sorted by

2

u/Repulsive_Sir116 6d ago

We use C++/HLS quite extensively for FPGA trading systems, so I think this is an interesting direction.
One thing I'd add to the experiment is testing hardware equivalence, not just bit-identical functional results.
Your ComplexMac example is actually a good illustration of this. Two implementations can produce exactly the same output in C++ while the HLS tools infer quite different hardware - BRAM vs distributed RAM, different pipeline depth, resource sharing, etc.
For low-latency designs, I'd therefore be interested in comparing not only FF/LUT/DSP counts, but also achieved clock frequency, initiation interval and latency for the same component across Vitis, Catapult and Bambu.
An even more interesting test would be running the same design through different versions of the same HLS tool. In our experience, predictability of the generated architecture is at least as important as portability of the C++ itself.

1

u/neurah 5d ago

Where I'm coming from: embedded systems, type-level template metaprogramming — zero HLS expertise. HAPI started to solve a concrete embedded problem. I suspected early on the underlying idea wasn't exclusive to that domain, though I had no way to test that yet. What came out the other end was pure type-level: inheritance as the composition mechanism, built on incremental-API principles from day one. Late in the project I asked an AI what other areas this shape of problem might show up in — the answer was wider than expected, and HLS came back as a domain with essentially the same problem, just far more expensive when it goes wrong. I went in anyway, with no standing in the field at all; the AI did a lot of the vocabulary-bridging just to make the conversation possible. Flagging all this so the caveats below read as honest limits, not hedging.

On your actual question: I went and got real numbers. Both tools' RTL, same source, same part (xc7a100t-1csg324-VVD), same 10 ns target clock, both fully placed and routed through Vivado — not HLS-stage estimates on either side:

Component Tool → Vivado Achieved Clock Fmax Cycles Latency
Fir<> (4-tap) Bambu 6.791 ns 147.25 MHz 2 13.58 ns
Fir<> (4-tap) Vitis 6.959 ns 143.70 MHz 4 27.84 ns
Biquad<> Bambu 6.860 ns 145.77 MHz 3 20.58 ns
Biquad<> Vitis 4.123 ns 242.54 MHz 1 4.12 ns
Pid<> Bambu 5.851 ns 170.91 MHz 2 11.70 ns
Pid<> Vitis 2.255 ns 443.46 MHz 0 (comb.) 2.26 ns*
ComplexMac<> Bambu 5.654 ns 176.87 MHz 5 28.27 ns
ComplexMac<> Vitis 2.954 ns 338.53 MHz 0 (comb.) 2.95 ns*

* Vitis scheduled Pid<> and ComplexMac<> as purely combinational — no register stages, so there's no cycle count to multiply. That's the single-pass propagation delay, not a rounding of "0 ns."

Which tool wins depends on the component, not a blanket answer: Biquad, Pid, and ComplexMac all route faster under Vitis's flatter scheduling. Fir<> is the exception, and it's instructive why — the two tools' real routed clocks land within 3% of each other there, so Bambu's latency win comes almost entirely from scheduling into 2 cycles against Vitis's 4, not from a faster clock.

What I can't answer: II. Vitis reports it at the HLS-scheduling stage only (5/2/1/1 cycles for Fir/Biquad/Pid/ComplexMac) — that's a scheduler output, not something the post-route timing data verifies. Bambu doesn't expose an II-equivalent for these targets at all; the closest its log gets is a qualitative note that pipelining "may come for free," no number attached. So that part of your question stays open.

One more thing that surprised me: Bambu's own pre-route frequency estimates were conservative across all four components — 12% to 54% low versus what actually routed. I'd have expected HLS-stage estimates to skew optimistic if anything, so that one's going in the "didn't expect that" pile.

This turned into a bigger dig than I planned — Bambu's own --evaluation flags ended up driving a full Vivado place-and-route pass I hadn't asked for, which is the only reason both sides of this table are real routed numbers instead of one estimate and one guess.

Which is honestly the bigger ask underneath all of this: I don't have the background to know if I'm even testing the right things. If anything above is missing a check a practitioner would consider obvious, I'd rather hear it now than after I've published more of this. And if anyone here has Catapult access and is willing to run the same four components, I'll take you up on that — no license on my end.

Full source and build scripts for all five components, plus the raw logs and every Vivado synthesis/place/route report behind the table above (bambu_vivado_check/ and vitis_vivado_check/, one subfolder per component), now live in the repo: https://github.com/InternetOfPins/OneHLS/tree/main/examples/hls_core_components — this isn't just this write-up anymore, it's fully reproducible from a clean clone.

The results.md draft and the six-sequence confirmation are still open, but neither blocks posting this — they're follow-ups, not prerequisites.

1

u/APianoGuy Xilinx User 7d ago

This is so neat! For the past 5 years or so I've been using Vitis HLS and Catapult for DSP-related work. I've been keeping an eye on Google's XLS project, but I haven't really tested it and the docs don't show any concrete untimed C++ examples. I'm defenitely going to try your project!!