Drive Bluespec simulations from Haskell and render terminal waveforms, in the spirit of Jane Street's hardcaml_waveterm — but targeting bsc-generated Bluesim models instead of Hardcaml circuits.
┌Signals────────┐┌Waves───────────────────────────────────────────┐
│clock ││┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │
│ ││ └───┘ └───┘ └───┘ └───┘ └───┘ └───│
│EN_clear ││ ┌───────┐ │
│ ││────────────────────────┘ └───────────────│
│EN_incr ││ ┌───────────────┐ │
│ ││────────┘ └───────────────────────│
│ ││────────────────┬───────┬───────┬───────────────│
│count ││ 00 │01 │02 │00 │
│ ││────────────────┴───────┴───────┴───────────────│
│ ││ │
└───────────────┘└────────────────────────────────────────────────┘
src/Bscwave/— Haskell libraryFFI— foreign imports for thebsim_*C ABIInterface—Port n/Bit nwith type-level widths, and theInterfacetypeclass that generated port records implementSim—Sim i oparameterised over typed input/output records;createenumerates ports via theInterfaceinstances. A captured cycle reflects the register state from the previous posedge (matches HardcamlCyclesimsemantics — input at cycle K affectsdoutat K+1).Waveform— sample capture, driven bytraverseI_over the same recordsRender— terminal renderer: two-line clock, two-line binaries, three-line multi-bit values with run-merging and zero-padded hex
app/GenPorts.hs—bscwave-gen-portscodegen. Parses theinit_symbol(...)table in a bsc-emitted<model>.cxxand writes a Haskell module exposingInputs/Outputshigher-kinded recordscsrc/bsim_wrapper.cxx— generic C++ shim over the Bluesimbk_*kernel API. One wrapper for any bsc design; usesdlsym(RTLD_DEFAULT, "new_MODEL_<name>")for the model factory andbk_lookup_symbolfor port access.examples/counter-tb/— runnable example, see below
cd examples/counter-tb
./build.sh # bsc + g++ + bscwave-gen-ports
LD_LIBRARY_PATH=./src cabal run testbenchbuild.sh does three steps:
bsc -simonCounter.bsv→ model objects +sim.so.sog++link →libsim.so(bscwave wrapper + model)bscwave-gen-ports mkCounter.cxx -o app/MkCounter.hs→ typedInputs/Outputsrecords, one field per bsc symbol
The testbench drives the generated record:
import qualified MkCounter as C
sim <- create @C.Inputs @C.Outputs C.modelName
let i = inputs sim
writePort (C.en_clear i) 1 -- :: Bit 1, masked to width
writePort (C.eN_incr i) 0
simStep simTypos on port names and widths are caught at compile time:
C.en_clera i→ "Not in scope: ‘C.en_clera’ … Perhaps use ‘C.en_clear’"writePort (C.count o) (0 :: Bit 1)→ "Couldn't match type ‘1’ with ‘8’"
- Write your BSV module (e.g.
Foo.bsvexportingmkFoo). - Run
bsc -sim -g mkFoo -u Foo.bsv && bsc -sim -e mkFoo -o sim.so mkFoo.ba(or follow the pattern inexamples/counter-tb/build.sh). - Run
bscwave-gen-ports mkFoo.cxx -o app/MkFoo.hs. This producesInputs f/Outputs frecords with one field per port:SYM_PORTentries (e.g.EN_clear) → input field, width from the symbol's bit countSYM_MODULEentries (registers) → output field, width pulled from the C++ constructor call (INST_count(simHdl, "count", this, 8u, …))SYM_DEF(intermediate signals likeWILL_FIRE_*) → skipped
- Link
g++to producelibsim.so(see counter-tb'sbuild.sh). - Write
Main.hs:sim <- create @C.Inputs @C.Outputs C.modelName (waves, sim') <- Waveform.create sim writePort (C.<field> (inputs sim')) <value> simStep sim' ... Render.print waves
cabal run testbenchwithLD_LIBRARY_PATHpointing atlibsim.so.
Regenerate MkFoo.hs whenever the BSV interface changes — the next
cabal build will fail to compile if a port was renamed or its width
changed.
- GHC 9.6+ with cabal
- bsc on
PATH libgmp-dev,dl, a recent g++