josuah.net | panoramix-labs.fr

cv | links | quotes | mail

Different Clock Domains With Verilator

Clock Domain Crossing (CDC) can be difficult to get right. in particular for a beginner like me. Simulating it seemed essential to make sure there were no Verilog bug before testing on hardware.

I currently use Verilator for simulation. It produces a simulation.vcd file to open in something like gtkwave.

ZipCPU solved this problem with great art already. But I did not want to increase the complexity of my small simulation.cpp nor use as many abstractions. So here is my approach:

Conventions

I am more of a C developer than C++ one, so I use structs intead of classes.

Each cosimulator such as UART or SPI has an associated struct holding all its belongings.

Vsimulation *vsim;	// Verilator global simulation state
VerilatedVCD *vcd;	// used for dumping state to a VCD file

// setup Verilator with "simulation.vcd" as dump file
void
simulation_init(int argc, char **argv)
{
	Verilated::commandArgs(argc, argv);
	Verilated::traceEverOn(true);

	vsim = new Vsimulation;
	vcd = new VerilatedVcdC;

	vsim->trace(vcd, 99);
	vcd->open("simulation.vcd");
}

_tick_posedge() and _tick_negedge()

Every clock domain will have its clock, that we will want to toggle up and down as clocks do.

Each of the toggle-up and toggle down action can change the state of combinational logics in the design. This is a good opportunity to capture the signals and dump them in the simulation.vcd file:

typedef uint64_t nanosecond_t;

void
simulation_eval(nanosecond_t ns)
{
	vsim->eval();
	vcd->dump(ns);
}

This means that every time we update the combinational logic, we dump all states on the VCD file. Sounds good!

Now, to update a main clock for instance named clk:

void
simulation_tick_posedge(nanoseond_t ns)
{
	vsim->clk = 1;
	simulation_eval(ns);
}

void
simulation_tick_negedge(nanosecond_t ns)
{
	vsim->clk = 0;
	simulation_eval(ns);
}

All that is left is call each of these function in turn with the time ns that increases.

An extra pair of function like these is to be written for every clock domain to maintain. Thankfully these functions are very small and easy to write.

The Big Picture

How to simulate clocks out of sync? The easiest I have found is a variable ns holding the time increasing in a loop, and react upon it with if (ns % PERIOD == PHASE).

This is wrapped in POSEDGE() and NEGEDGE() macros for convenience.

That way, PERIOD and PHASE can be chosen to any arbitrary value to have any kind of clock frequency and phase, to test all combination we want.

#define POSEDGE(ns, period, phase) \
	((ns) % (period) == (phase))

#define NEGEDGE(ns, period, phase) \
	((ns) % (period) == ((phase) + (period)) / 2 % (period))

#define CLK_SYS_PERIOD 30
#define CLK_SYS_PHASE 3

#define CLK_SPI_PERIOD 31
#define CLK_SPI_PHASE 0

int
main(int argc, char **argv)
{
        struct spi spi;

        simulation_init(argc, argv);
        spi_init(&spi);

        vsim->spi_csn = 0;

        for (nanosecond_t ns = 0; ns < 20000; ns++) {
                // SYS clock domain
                if (POSEDGE(ns, CLK_SYS_PERIOD, CLK_SYS_PHASE))
                        simulation_tick_posedge(ns);
                if (NEGEDGE(ns, CLK_SYS_PERIOD, CLK_SYS_PHASE))
                        simulation_tick_negedge(ns);

                // SPI clock domain
                if (POSEDGE(ns, CLK_SPI_PERIOD, CLK_SPI_PHASE))
                        spi_tick_posedge(&spi, ns);
                if (NEGEDGE(ns, CLK_SPI_PERIOD, CLK_SPI_PHASE))
                        spi_tick_negedge(&spi, ns);
        }

        simulation_finish();
	return 0;
}

In that example, we have two clocks slightly drifting by 1ns, starting at a small phase offset of 3ns.

It might even be possible to change the period and phase in the middle of the simulation to simulate clock drift or reconfiguration.