Join or fail

So … you noticed that the final exercise failed, didn’t you ?

This is due to the fact that we are simulating hardware in software. More precisely, we’re simulating hardware that runs everything in parallel. The simulation is done in software, which typically is sequential.

Now how to fix this ?

Enter concurrent processes

To achieve support for parallel pieces of software, threads can be used. In Verilog2001 the fork…join construct was added. SystemVerilog adds two additional variants: fork…join_any and fork…join_none. How this multi-threaded approach works, can be explained easiest with a drawing.

Let’s assume there are four “tasks” in software: a, b, c, and d. Note that this can also be a function or method call, or a simple one-line statement. Simply writing   a; b; c; d; will start task a. When this is done, task b will be started, and so on.

To start every task in its own dedicated thread, they should be wrapped in a fork-statement. The three available options for a fork-statement are:

  • fork … join runs all the statements in parallel. The first statement after the fork … join (task d) will be executed when all parallel statements are finished.
  • fork … join_any runs all the statements in parallel. The first statement after the fork … join_any (task d) will be executed when the first of the parallel statements is finished.
  • fork … join_none runs all the statements in parallel. The first statement after the fork … join_none (task d) will be executed immediately after initiating the parallel statements.

Simulation screenshot driver

Adding disable fork and wait fork to the mix, gives you a lot of control.

  • disable fork; terminates all remaining forked blocks
  • wait fork; causes calling process to block until all the sub-statements are completed.

The disable fork statement stops all active threads that were spawned from the current thread. The problem is that this may accidentally stop threads that you did not intended to.

Back to the Environment

With all that tackled, the environment can be fixed.

When the run() method is called, 2 threads need to be started in parallel: 1 for the driver, and 1 for the monitor. This allows to run both instances in parallel.

The fork is closed with a join_any.

After giving 10 clock cycles of spin-up, the downstream threads are started in a join_any. If you remember, the Monitor has a forever loop, so the this.mon.run() will never end. This implies that the fork is ended when the driver ends.

To illustrate this, the Transcript windows of both simulations (with and without fork) are shown below.

class environment;

  virtual gbprocessor_iface ifc;

  driver drv;
  monitor mon;

  function new(virtual gbprocessor_iface ifc);
    this.drv = new(ifc);
    this.mon = new(ifc);
  endfunction : new

  task run();
    fork
      this.drv.run_addition();
      this.mon.run();
    join_any;

    $display("[ENV]: end of run()");

  endtask : run

endclass : environment

MON

Take a close look at the timestamp of the message that stated the Monitor will start working.