Delta cycles are non time-consuming timesteps used by VHDL simulators for modelling events during execution of VHDL code. They are events that happen in zero simulation time after a preceding event.

VHDL is a parallel programming language, while computers and CPUs work in a sequential manner. When a normal programming language is run, the CPU executes one instruction after the other. While in VHDL, there can be multiple sequences of logic that react to each other in ways that are not compatible with the standard computer architecture.

To precisely model the behavior of digital logic, simulators use an event-based approach for executing VHDL code.

Trivial example with no events

Let’s first look at what happens when we run this trivial VHDL code in the ModelSim VHDL simulator:

library ieee;
use ieee.std_logic_1164.all;

entity DeltaCyclesTb is
end entity;

architecture sim of DeltaCyclesTb is

    signal Sig1 : std_logic := '0';

begin

end architecture;

When I tell ModelSim to simulate this design for 200 hours, that works just fine. In fact, the simulation completes almost instantly and the command prompt gives me a new line where I can enter the next command. How can it be that this very long simulation completed so fast?

VSIM 1> run 200 hr
VSIM 2>

You already know the answer intuitively. There is not a lot going on in this code. The Sig1 signal is '0' right from the start, and it doesn’t change at all. It is easy to know what the output will look like in a second or an hour from now, because it is exactly the same as now.

From the simulator’s point of view, there are no events in this VHDL code.

Event scheduling

This is the code from the previous example, but with an additional process that inverts the value of Sig1 every 5 nanoseconds:

library ieee;
use ieee.std_logic_1164.all;

entity DeltaCyclesTb is
end entity;

architecture sim of DeltaCyclesTb is

    signal Sig1 : std_logic := '0';

begin

    process is
    begin
        Sig1 <= not Sig1;
        wait for 5 ns;
    end process;

end architecture;

If we now try to simulate for 200 hours, it will take a lot of time. Instead, let’s run for 100 nanoseconds and have a look at the output in the waveform:

VSIM 1> run 100 ns
VSIM 2>

Waveform of Sig1 alternating between '0' and '1'

Unsurprisingly, Sig1 changes between '0' and '1' with a frequency of 100 MHz. Every time a signal changes, it is an event. But how does this work internally in the simulator?

Simulators use a technique called event scheduling to precisely model the behavior of VHDL code while doing the least amount of work.

At the start of the simulation, Sig1 has its initial value of '0'. Then, the simulator will run the process and stop at the wait for 5 ns; line. In VHDL, signal values are only updated when the process hits a Wait statement. In practice, the simulator will schedule the signal to have the new value in the next delta cycle.

The signal was '0' at the start of the simulation, but changes to '1' before any time has passed at all. Although we are still at 0 ns simulation time, the signal now has a new value.

This is possible because a delta cycle is a zero-time timestep.

As the simulator executes the code, it will maintain an internal table of the next events that are scheduled to happen. For the above example, the event schedule will look like this:

Simulation time Sig1 current value Sig1 scheduled value Time to next event
0 ns ‘0’ ‘1’ 1 delta cycle
0 ns + 1 delta cycle ‘1’ ‘1’ 5 ns
5 ns ‘1’ ‘0’ 1 delta cycle
5 ns + 1 delta cycle ‘0’ ‘0’ 5 ns
The pattern repeats itself indefinitely

Whenever a process is paused as the result of a Wait statement, the simulator keeps track of the future values of signals driven by that process, and how long into the future it shall skip ahead before the next event is due to happen.

Show all delta cycles in the ModelSim waveform

By default, ModelSim will only log the value that an object has at the start and at the end of a timestep. You will not be able to see beyond delta cycle +1 without changing the configuration file.

To make ModelSim show all delta cycles, “WLFCollapseMode = 0” must be set in the modelsim.ini file as shown below. The modelsim.ini file is located in the folder where you installed ModelSim.

Set WLFCollapseMode = 0 in modelsim.ini to see all delta cycles

Remember to restart ModelSim after changing the ini file.

See how to install the free student edition of ModelSim here

Waveform with delta cycles expanded

In most simulators, it is possible to view delta cycle delays in the waveform. In ModelSim you can do this by enabling “Expanded Time Deltas Mode”. Then, while placing the cursor on the transition you want to examine, press “Expand Time At Active Cursor”. After zooming in sufficiently, delta cycle delays should be visible:

Waveform with delta cycles showing

The above waveform shows delta cycles on the Sig1 signal from the previous code example.

Delta cycles as a result of sensitivity lists

We now know that a delta cycle delay will be produced if signal values have changed when the program hits a Wait statement. But what about processes with sensitivity lists?

In the code below, another process has been added to the testbench. A process which is sensitive to Sig1. It will assign to Sig2 the value of Sig1 whenever Sig1 changes.

library ieee;
use ieee.std_logic_1164.all;

entity DeltaCyclesTb is
end entity;

architecture sim of DeltaCyclesTb is

    signal Sig1 : std_logic := '0';
    signal Sig2 : std_logic;

begin

    process is
    begin
        Sig1 <= not Sig1;
        wait for 5 ns;
    end process;

    process(Sig1) is
    begin
        Sig2 <= Sig1;
    end process;

end architecture;

The two signals appear to change simultaneously in the waveform, as one would expect:

Sig1 and Sig2 appear synchronous in the waveform

When expanded to show delta cycles in the time period from 0 to 10 ns, the waveform looks like this:

Waveform showing Sig1 and Sig2 from 0 to 10 ns with delta cycles expanded

At 0 ns, none of the processes have run yet. Therefore, all signals will have their initial values. Sig1 has the value '0', which is the initial value we specified when the signal was declared. But Sig2 has the value 'U'. We didn’t explicitly give Sig2 an initial value. The 'U' value means “Uninitialized”. It is the default value of the std_logic type.

To understand what happens next, let’s take a step back and try to remember what a process with a sensitivity list really is. A process with a sensitivity list is equal to a process with a Wait On statement at the end. This is a well-defined part of the VHDL standard.

Using this information, we can rewrite process number two into:

process is
begin
    Sig2 <= Sig1;
    wait on Sig1;
end process;

When program execution starts, Sig2 will be scheduled to get the value of Sig1 in the next delta cycle. Then, the process will sleep until Sig1 changes. When that happens, the process will wake up and the value will be copied once again.

The event schedule for the first 5 ns will look like this:

Time + delta Sig1 current / next Sig2 current / next Time to next event
0 ns ‘0’ → ‘1’ ‘U’ → ‘0’ 1 delta cycle
0 ns +1 ‘1’ ‘0’ → ‘1’ 1 delta cycle
0 ns +2 ‘1’ ‘1’ 5 ns
5 ns ‘1’ → ‘0’ ‘1’ 1 delta cycle
5 ns +1 ‘0’ ‘1’ → ‘0’ 1 delta cycle
5 ns +2 ‘0’ ‘0’ 5 ns

The event schedule matches the waveform with expanded delta cycles. Take notice of the signal values in the last delta cycle in each of the timesteps. That is, when the “Time + delta” column reads “0 ns + 2” and “5 ns +2”.

Neither Sig1 nor Sig2 have a scheduled value at this point. That’s how the simulator knows that it is time to proceed to the next timestep which is not a delta cycle. All changes have propagated through the logic, and there are no more scheduled changes.

There is no need for another delta cycle, because all signals have stable values now. Nothing more can happen at this time. Another delta cycle would just result in the same values. The simulator jumps ahead to the next process that is due to wake up from a Wait For statement.

Simulator run time

Due to this scheduling scheme, simulator run time has no relation to the time values that are used in the simulated design. Waiting for 5 nanoseconds takes the same time as waiting for 5 hours. It’s just another value for the next timestep in the scheduling table.

The number of timesteps do affect the simulator run time. Every time the simulator has to stop and start, it adds to the run time. It makes the simulation slower because the simulator has to keep track of processes that wake up as a result of changing signal values, which may in turn cause additional delta cycles.

The number of processes that are sensitive to the signals that are changing, also affects the speed of the simulation. Usually, most of the logic will be sensitive to the clock signal. Therefore, lengthy simulations on clocked designs may take a substantial amount of time to simulate.

In the How to create a Timer tutorial, we avoided this problem by lowering the clock frequency in the testbench.

Creating delta cycles

Sometimes, you may want to deliberately create a delta cycle delay. In production (RTL) code it is pointless, but in testbenches, it can be quite useful. Delta cycle pulses can be used to perform inter-process communication without consuming simulation time.

Testbenches often contain processes that act as bus functional models (BFMs). They provide the device under test (DUT) with stimuli and verify that the output from it is correct. Because testbenches don’t have to be synthesizable, we can use delta cycle pulses to our advantage.

It may sound counter-intuitive, but wait for 0 ns; is a perfectly valid VHDL statement.

As we already know, signal values are only updated when the process hits a wait statement. By waiting for a zero-time value, signals will get their scheduled values without any simulation time passing. The signals will get the new values in the next delta cycle, but in zero simulation time.

Consider this code that creates two mirrored signal patterns:

library ieee;
use ieee.std_logic_1164.all;

entity DeltaCyclesTb is
end entity;

architecture sim of DeltaCyclesTb is

    signal Trigger : std_logic := '0';
    signal Sig1 : std_logic := '0';
    signal Sig2 : std_logic := '0';

begin

    process is
    begin
        wait for 100000 ns;

        -- Create a delta cycle pulse
        Trigger <= '1';
        wait for 0 ns;
        Trigger <= '0';

    end process;

    process is
    begin
        -- Wait for trigger pulse
        wait until Trigger = '1';
        
        for i in 1 to 50 loop
            Sig1 <= not Sig1;
            wait for i * 10 ns;
        end loop;
        
    end process;

    process is
    begin
        -- Wait for trigger pulse
        wait until Trigger = '1';
        
        for i in 1 to 50 loop
            Sig2 <= not Sig2;
            wait for (50 - i * 1) * 10 ns;
        end loop;
        
    end process;

end architecture;

The above code results in the waveform that is shown below. The two signal patterns are mirrored, but start and end at the exact same time. The Trigger signal is used for synchronously starting two processes that create each pattern.

The Trigger signal is pulsed within a delta cycle, shown by the yellow arrow in the waveform. Its value is '1' for the duration of zero time, but one delta cycle.

Waveform showing Trigger, Sig1, and Sig2

The same waveform is shown below, zoomed in on the trigger pulse, now with time deltas expanded in ModelSim. We can see from the timeline that Trigger is '1' between delta cycles +1 and +2.

Waveform zoomed in on Trigger signal around delta cycle pulse

This is an example of how you can use delta cycles to your advantage when creating testbenches, BFMs, and verification components.

Pitfalls

Signal value transitions that are seemingly in sync, but separated by delta cycles, can lead to bugs that are difficult to detect. The novice VHDL developer may even perceive delta cycles as a bug or imperfection in the VHDL language.

Delta cycles are neither bugs nor imperfections. They are an integral part of the scheme that enables us to design concurrent logic by using a programming language.

Even so, let’s go over two common problems that may arise due to delta cycle delays.

Copying the clock signal

A practice that will almost definitely lead to undesirable results is copying the clock signal. We have all done it at some point in our careers. You create a copy of the clock signal to give it a new name, or for some other reason. Suddenly, all sorts of strange things happen when you run the simulation.

When you copy the clock signal, the copy will lag behind the original by one delta cycle. If there are different processes using different copies of the clock, they will no longer be in sync.

In the below code, the number of elapsed clock periods, Cnt, is counted by the one-liner process. The first process with a sensitivity list pulses Sig1 when the counter is 5. While the last process is an equivalent that pulses Sig2, but is sensitive to a copy of the clock.

library ieee;
use ieee.std_logic_1164.all;

entity DeltaCyclesTb is
end entity;

architecture sim of DeltaCyclesTb is

    signal Clk : std_logic := '1';
    signal Clk_copy : std_logic;
    signal Cnt : integer := 0;
    signal Sig1 : std_logic := '0';
    signal Sig2 : std_logic := '0';

begin

    Clk <= not Clk after 5 ns;
    Cnt <= Cnt + 1 when rising_edge(Clk);
    Clk_copy <= Clk;
    
    -- Pulse after 5 clock cycles
    process(Clk) is
    begin
        if rising_edge(Clk) then
            Sig1 <= '0';
            if Cnt = 5 then
                Sig1 <= '1';
            end if;
        end if;
    end process;
    
    -- Equivalent process, but using Clk_Copy
    process(Clk_copy) is
    begin
        if rising_edge(Clk_copy) then
            Sig2 <= '0';
            if Cnt = 5 then
                Sig2 <= '1';
            end if;
        end if;
    end process;

end architecture;

In the waveform below, the two clocks appear to be in sync. But still, the identical processes behave differently. The process that uses a copy of the clock reacts one clock cycle before the one that is using the original.

Waveform showing that Clk and Clk_Copy appear to be in sync, but Sig1 and Sig2 behave differently

The below image shows the same waveform, but with time deltas expanded around the transition of the counter from 4 to 5. Here, it’s possible to spot the reason for the error. The copied clock is delayed by one delta cycle.

Clock copy waveform with expanded time deltas

At the rising edge of the original clock, the process that is sensitive to it wakes up and samples Cnt, which is 4. The process that increments Cnt is also sensitive to the original clock. It too wakes up in the same delta cycle and schedules a change to the Cnt signal.

The change is effective in the next delta cycle. Meanwhile, the process using a copy of the clock has been delayed by one delta cycle because of the delta cycle delay on the copied clock. When it wakes up, the Cnt signal has changed and is now 5. It will sample the new counter value. Therefore, it will behave differently from the process using the original clock.

Simulation mismatch due to delta cycle delay

Now you know that copying the clock is bad because it can lead to the simulation not working properly. But what happens if we try to synthesize code that contains copied clock signals?

I implemented a LED blink module on the Lattice iCEstick development board. It contains two identical counter processes and a process for toggling the LED. But one of the counter processes are using a copied clock. If the two counters are synchronous, the LED will blink periodically.

Watch the video to find out how it went!

The testbench which instantiates the module:

library ieee;
use ieee.std_logic_1164.all;

entity DeltaCyclesTb is
end entity;

architecture sim of DeltaCyclesTb is

    signal Clk : std_logic := '1';
    signal Led1 : std_logic;

begin

    Clk <= not Clk after 41.66 ns; -- 12 MHz

    i_DeltaCycles : entity work.DeltaCycles(rtl) port map (
        Clk => Clk,
        Led1 => Led1
    );

end architecture;

The LED blink module that is instantiated in the testbench and which is running on the Lattice iCEstick:

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity DeltaCycles is
port (
    Clk : in std_logic;
    Led1 : out std_logic
    );
end entity;

architecture rtl of DeltaCycles is

    signal Clk_copy : std_logic;
    signal Cnt1 : unsigned(23 downto 0) := (others => '0');
    signal Cnt2 : unsigned(23 downto 0) := (others => '0');
    signal Led1_i : std_logic := '0';

begin

    Led1 <= Led1_i;
    
    Clk_copy <= Clk;

    Cnt1 <= Cnt1 + 1 when rising_edge(Clk);
    Cnt2 <= Cnt2 + 1 when rising_edge(Clk_copy);

    process(Clk_copy) is
    begin
        if rising_edge(Clk_copy) then
            if Cnt1 = 0 and Cnt2 = 0 then
                Led1_i <= not Led1_i;
            end if;
        end if;
    end process;

end architecture;

The waveform with expanded time deltas show how the delta cycle delay on the clock prevents the toggle process from sampling the counters when they are equal:

Waveform showing delta cycle delay between the clocks

But why did the module work when implemented on an FPGA board?

The synthesizer ignored the copied clock signal and treated the two clocks with different names as one single net. The synthesizer’s job is to create a netlist from the code that can be implemented with the available physical resources. Now, how should a zero-time delay on the same wire be implemented?

Think about that.

With the line Clk_copy <= Clk; we are saying; whenever Clk changes, transfer that value to Clk_copy in zero time. The synthesizer is doing the only thing it can do. By joining the two signals to create a single net, the value will be transferred as fast as possible.

Gate level simulation

What about the simulation, is there a bug in ModelSim? No, there is no bug.

What we are experiencing is a consequence of simulating a digital electronic circuit using a higher level programming language. ModelSim does not have any knowledge of the technology we are going to implement the code on. It simulates the VHDL code, which is at a higher abstraction level than the logic gates and flip-flops are.

There are options in ModelSim and other VHDL simulators to perform gate level simulation. This is a process where the implemented netlist is fed back into the simulator from the synthesizer. If we had done a gate level simulation on the design, we would have seen the same behavior as we did in real life.

Are delta cycles evil?

You should know by now that delta cycles are not evil. There is nothing mystical or magical about them. They are in fact what makes it possible for us to simulate VHDL with predictable, repeatable results.

However, when coding with VHDL, you need to be aware of the differences between simulation and synthesis. You need to keep a mental event schedule which you invoke when you create a new process or update a signal.

Master the delta cycle and you will for sure become a better VHDL engineer.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.