Logic Simulation, Part 1

Posted on Tue 23 May 2023 in Logic

Introduction

I need a hobby.

Okay, fine. FINE! Damnit.

Let's write a logic simulator.

This is easily the best yak shaving exercise I can think of. In my day job verifying processors, I use industry standard RTL simulation and debug tools, along with all my colleagues. These tools are notoriously complicated, but also magical. I sometimes find myself thinking about how they work, and end up diving down a rabbit hole that leads to questions such as "what exactly is the nature of time". This is yak shaving 101. Let's do it.

Picking Tools

I have used a few simulators before, but never written one. Event simulation does not seem difficult to implement from first principles. After all:

If you wish to make an apple pie from scratch, you must first invent the universe.

—Carl Sagan

Let us begin by ...

It's a trap!

NO!

Let us instead begin by setting some reasonable constraints. The quickest way to a working prototype is choosing a set of functional, though possibly imperfect tools.

Requirements:

  • Python
  • Simulation API
  • Waveform debug tool

Why Python? First, I already have some experience with it. It is a good, general purpose language for prototyping. If this project ever becomes more than a diversion from doomscrolling Twitter, we will need something snappier like C/C++ or Rust. Donald Knuth once said:

Good idea, oh Knuth!

We use Python.

The most mature, general purpose simulation API looks like SimPy. It implements coroutines using Python generators. An intriguing idea would be to use Python's asyncio library. It uses Python's newer async/await syntax. The usim library uses asyncio, and claims to be a "drop in replacement" for SimPy version 4. I respect the hustle, but I have more experience with generators. Let's keep it simple for now, and put that one on the back burner. There is also MyHDL. This library looks good enough for production use, and therefore does not scratch my itch for spending the first fifty iterations getting all the details wrong. SimPy FTW.

Finally, we need a way to view the results of our simulation. Using an EDA tool such as VCS, Xcelium, or Questasim is not a reasonable option for most people. One possibility is WaveDrom, but the use case is not quite right. It is excellent for creating documentation, but not designed for debug. GTKWave looks usable, so we will start there. To translate simulated values into a "value change dump" (VCD) format, we will try pyvcd.

Clock

One of the first examples on the SimPy website is a clock. Since we will need a clock before building any state elements, let's start there.

A clock is a digital signal that oscillates between two phases. A "positive" clock, or pclk for short, repeats the pattern 1, 0, 1, 0, etc. A "negative" clock, or nclk for short, repeats the pattern 0, 1, 0, 1, etc. The amount of time it takes for the clock to repeat the pattern is known as the "period". The "duty cycle" is the percent of the period that the clock is active. We will define the first phase as the active phase. Finally, the "phase shift" is the amount of time before the clock starts. This allows you to adjust the alignment of several clocks relative to each other.

To keep things simple, we will use an integer description. All units of time are clock "ticks".

from simpy import Environment
from simpy.events import Timeout
from vcd import VCDWriter

def clock(
    env: Environment,
    vcdw: VCDWriter = None,
    scope: str = "top",
    init: bool = False,
    shift_ticks: int = 0,
    phase1_ticks: int = 1,
    phase2_ticks: int = 1) -> Generator[Timeout, None, None]:
    """
    Simulate a clock signal.

    _______/‾‾‾‾‾‾‾‾\________/‾‾‾‾‾‾‾‾\________

    <shift> <phase1> <phase2>

    Args:
        env: SimPy Environment instance
        vcdw: VCD Writer instance (for debug)
        scope: Hierarchical instance name.
        init: Initial clock signal value
        shift_ticks: Number of sim ticks to shift the first clock transition
        phase1_ticks: Number of sim ticks in the clock's active phase
        phase2_ticks: Number of sim ticks in the clock's passive phase

    The period is phase1_ticks + phase2_ticks
    The duty cycle is phase1_ticks / (phase1_ticks + phase2_ticks)

    Yields:
        A timeout event

    Raises:
        ValueError: An argument has the correct type, but an incorrect value
    """
    if shift_ticks < 0:
        raise ValueError(f"Expected shift_ticks >= 0, got {shift_ticks}")
    if phase1_ticks < 0:
        raise ValueError(f"Expected phase1_ticks >= 0, got {phase1_ticks}")
    if phase2_ticks < 0:
        raise ValueError(f"Expected phase2_ticks >= 0, got {phase2_ticks}")

    # Initialize value
    value = init
    if vcdw:
        var = vcdw.register_var(scope, "clock", "reg", size=1, init=value)

    # Optionally, shift phase from time zero
    if shift_ticks > 0:
        yield env.timeout(shift_ticks)

    # If no phase shift, this change happens at T=0
    value = not value
    if vcdw:
        vcdw.change(var, env.now, value)

    while True:
        yield env.timeout(phase1_ticks)
        value = not value
        if vcdw:
            vcdw.change(var, env.now, value)

        yield env.timeout(phase2_ticks)
        value = not value
        if vcdw:
            vcdw.change(var, env.now, value)

with open("testing.vcd", "w") as fout, VCDWriter(fout, timescale=(1, "ns")) as vcdw:
    env = Environment()
    env.process(clock(env, vcdw, scope="top", init=False,
                shift_ticks=2, phase1_ticks=3, phase2_ticks=4))
    env.run(until=20)

To open up the waveform with GTKWave:

gtkwave testing.vcd

If everything worked correctly, we should have something that looks like a clock signal:

Clock signal in GTKWave

I can already tell this is missing a bunch of stuff. For example, I have no idea how to trigger another coroutine to update on the clock's transition from 0 to 1, i.e. it's positive edge, or "posedge".

This is the end of Part 1. I have no plan. If the hobby sticks, a good next topic is to extend our example from one coroutine to two or three coroutines. For example, consider a flip flop with a reset and clock. This requires at least three coroutines. Until next time 😃.