← All posts

Introducing Hover

Eray Mercan

languageco-simulationmna

Every machine worth controlling gets built twice.

Once in firmware: a loop that samples, decides, and drives, running on a discrete clock at whatever rate the MCU can sustain. And once in a simulation: a model of the motor, the power stage, the thermal load โ€” the continuous world that the firmware is supposed to be steering.

The two are written in different tools, by different people, at different times. And they quietly disagree. The simulation's plant advances on the solver's own adaptive timestep; the firmware's loop advances on its clock. Between those two clocks sits a co-simulation harness whose entire job is interpolating one into the other, and every interpolation is a small lie about when something actually happened. Most of the time the lie is harmless. Then a diode commutates half a microsecond before the control loop looks, and the bench behaves nothing like the model did.

Hover exists because that gap should not be the engineer's problem.

One file, two domains

A .hvr file describes the circuit and the logic together, and the compiler holds both to the same timestep:

  • analog modules are continuous device physics. They lower to a circuit matrix and are solved inside the Modified Nodal Analysis loop โ€” the same technique SPICE uses โ€” with R, L, C, the four controlled sources and the independent ones all solved together.
  • digital modules are discrete state and control. They run between solver steps and read the plant's own state directly: V(node) is the node voltage as of this step, not a value someone marshalled across a harness boundary.
  • Plain module is structure โ€” wiring, hierarchy, parameters. Modules exist at compile time only; what survives elaboration is nets and logic objects, so the hierarchy costs nothing at runtime.

There is no co-simulation harness because there are not two simulations to couple.

.save(main.ac_in1, main.dc_out);
.tran(0, 100m, 1u, 10u);
.solver(bdf2);

import <semiconductors/diode.hvr> as diode;

module main<>() [] {
    wire ac_in1, ac_in2, dc_out;
    module v_ac = AC_Source<24.0, 50.0>() [ac_in1, ac_in2];

    module D1 = diode.Diode<1p, 1.0, 0.026>() [ac_in1, dc_out];
    module D3 = diode.Diode<1p, 1.0, 0.026>() [ac_in2, dc_out];

    R<1k>()  [dc_out, gnd];
    C<10u>() [dc_out, gnd];   // MNA solves the ripple
}

Three directives set the run: .tran is the transient window and its step bounds, .solver picks the integrator, .save records any node or state by name. Tracing is free and unbounded โ€” no manual instrumentation, no fixed channel count.

What's underneath

The compiler is written in Go. It lexes and parses each file, resolves imports, runs semantic analysis, elaborates the circuit graph, then generates C++ that Zig compiles against a prebuilt runtime โ€” an Eigen-based MNA solver plus a small VM โ€” into a single native executable. You run that binary and it writes CSV.

Two consequences follow from being a compiler rather than an interpreter or a block-diagram tool. The first is that types are real: int, unsigned int, double and fixed-size arrays compile to native code, so a control loop can't hide a bug behind an implicit float promotion. The second is that the logic you prove is the logic you ship โ€” it's ordinary typed code, not a diagram that some code generator will reinterpret on its way to the MCU.

Choosing the integrator matters more in mixed systems than it does in either domain alone, so it's a per-run decision rather than a build-time one: euler_fixed when you want speed, BDF2 or NDF2 when a switching circuit turns stiff, trapezoidal or Gauss-Seidel in between. Seven in total, selected by one directive.

Switching edges get special treatment. A .zcd directive stops the solver exactly on the crossing instead of stepping across it, which is the difference between modelling dead-time and averaging it away.

Where it is

Hover is v0.8.0 and under active development. The documentation covers the language as it stands, and your first circuit builds a full-bridge rectifier end to end if you'd rather start by running something. Binaries for Linux, macOS and Windows are on the download page; the toolchain bootstraps itself with a single --setup.

It is early software, and I'd rather say so plainly than discover it with you later. The language is still moving, the diagnostics are rougher than they should be, and the standard library is smaller than the ambition. What is solid is the core idea and the thing it produces: one file, one timestep, one binary.

This blog is where the design notes, the solver internals and the release write-ups will land. If you build something with it โ€” or if it breaks in an interesting way โ€” the repository is the place to say so.

All posts ยท Subscribe via RSS.