One of the things I love about reading open source is randomly finding unused features. There are many reasons a part of the code is not used, It could can experimental, untested or it is only there because the developer had too much fun writing it :) Anyway, It’s interesting when i find these bits..

In cocotb,I found built-in utility to generate wavedrom. It’s defined in cocotb/wavedrom.py. From comments, I modified dff_simple_test to dump dut.q and dut.d sampled on dut.clk.

@cocotb.test()
async def dff_simple_test(dut):
    """Test that d propagates to q"""

    with trace(dut.d, dut.q,  clk=dut.clk) as waves:
        clock = Clock(dut.clk, 10, units="us")  # Create a 10us period clock on port clk
        cocotb.start_soon(clock.start())  # Start the clock

        await FallingEdge(dut.clk)  # Synchronize with the clock
        for i in range(10):
            val = random.randint(0, 1)
            dut.d.value = val  # Assign the random value val to the input port d
            await FallingEdge(dut.clk)
            assert dut.q.value == val, f"output q was incorrect on the {i}th cycle"

        # Dump to JSON format compatible with WaveDrom
        j = waves.dumpj()
        print(j)

And the generated wavedrom json is generated. Super cool, Right!

{
    "signal": [
        {
            "name": "clock",
            "wave": "p.........."
        },
        {
            "name": "d",
            "wave": "z010.10.101"
        },
        {
            "name": "q",
            "wave": "z010.10.101"
        }
    ]
}

Deep dive Link to heading

The context manager registers coroutine to _monitor

162     def __enter__(self):
163         for sig in self._signals:
164             sig.clear()
165         self.enable()
166         self._coro = cocotb.start_soon(self._monitor())
167         return self

_monitor awaits on _clock and sample each signal

139
140     async def _monitor(self):
141         self._clocks = 0
142         while True:
143             await RisingEdge(self._clock)
144             await ReadOnly()
145             if not self._enabled:
146                 continue
147             self._clocks += 1
148             for sig in self._signals:
149                 sig.sample()
150

sample is defined in Wavedrom not trace. It detects the change of signal and sample it. If not changed, . is used to indicate no-change in wavedrom.

 42
 43     def sample(self):
 44         """Record a sample of the signal value at this point in time."""
 45
 46         def _lastval(samples):
 47             for x in range(len(samples) - 1, -1, -1):
 48                 if samples[x] not in "=.|":
 49                     return samples[x]
 50             return None
 51
 52         for name, hdl in self._hdls.items():
 53             val = hdl.value
 54             valstr = val.binstr.lower()
 55
 56             # Decide what character to use to represent this signal
 57             if len(valstr) == 1:
 58                 char = valstr
 59             elif "x" in valstr:
 60                 char = "x"
 61             elif "u" in valstr:
 62                 char = "u"
 63             elif "z" in valstr:
 64                 char = "z"
 65             else:
 66                 if (
 67                     len(self._data[name])
 68                     and self._data[name][-1] == int(val)
 69                     and self._samples[name][-1] in "=."
 70                 ):
 71                     char = "."
 72                 else:
 73                     char = "="
 74                     self._data[name].append(int(val))
 75
 76             # Detect if this is unchanged
 77             if len(valstr) == 1 and char == _lastval(self._samples[name]):
 78                 char = "."
 79             self._samples[name].append(char)