cocob provides a periodic clock class Clock which depends on Timer trigger. I will start with an examples of Clock usage.

clock = Clock(dut.clk, 10, units="us")
cocotb.start_soon(clock.start())

Note That start_soon is just starting cocotb coroutine after the current routines yields. Any let’s focus on Clock

In Clock, The initialization __init__ sets some local vars (most importantly period and half_period)

118         BaseClock.__init__(self, signal)
126         self.period = get_sim_steps(period, units)
127         self.half_period = get_sim_steps(period / 2.0, units)
128         self.frequency = 1.0 / get_time_from_sim_steps(self.period, units="us")
130         self.signal = signal

Side note, get_sim_steps calculates steps from real time passed to Clock

122 def get_sim_steps(
123     time: Union[Real, Decimal], units: str = "step", *, round_mode: str = "error"
124 ) -> int:
125     """Calculates the number of simulation time steps for a given amount of *time*.

start creates a Timer and keeps toggling when that timer triggers. See lines 157-160 below.

148         t = Timer(self.half_period)
149         if cycles is None:
150             it = itertools.count()
151         else:
152             it = range(cycles)
153
154         # branch outside for loop for performance (decision has to be taken only once)
155         if start_high:
156             for _ in it:
157                 self.signal.value = 1
158                 await t
159                 self.signal.value = 0
160                 await t
161         else:
162             for _ in it:
163                 self.signal.value = 0
164                 await t
165                 self.signal.value = 1
166                 await t

In Timer, cbhdl is set to register_timed_callback with required time and callback.

167 class Timer(GPITrigger):
168     """Fire after the specified simulation time period has elapsed."""
169
...
...
 270     def prime(self, callback):
 271         """Register for a timed callback."""
 272         if self.cbhdl is None:
 273             self.cbhdl = simulator.register_timed_callback(
 274                 self.sim_steps, callback, self
 275             )
 276             if self.cbhdl is None:
 277                 raise TriggerException("Unable set up %s Trigger" % (str(self)))
 278         GPITrigger.prime(self, callback)

register_timed_callback is cpython implementation to register VPI callback that calls the routine passed from python. scheduler is the one calling the `prime() with callback coro.

 523     gpi_sim_hdl sig_hdl = ((gpi_hdl_Object<gpi_sim_hdl> *)pSigHdl)->hdl;
 524
 525     // Extract the callback function
 526     PyObject *function = PyTuple_GetItem(args, 1);
 527     if (!PyCallable_Check(function)) {
 528         PyErr_SetString(PyExc_TypeError,
 529                         "Attempt to register value change callback without "
 530                         "passing a callable callback!\n");
 531         return NULL;
 532     }
 533     Py_INCREF(function);
 534
 535     PyObject *pedge = PyTuple_GetItem(args, 2);
 536     int edge = (int)PyLong_AsLong(pedge);
 537
 538     // Remaining args for function
 539     PyObject *fArgs = PyTuple_GetSlice(args, 3, numargs);  // New reference
 540     if (fArgs == NULL) {
 541         return NULL;
 542     }
 543
 544     callback_data *cb_data = callback_data_new(function, fArgs, NULL);
 545     if (cb_data == NULL) {
 546         return NULL;

For completeness, GPITrigger and Trigger classes are below. The important part in Trigger is __await__ as this is what get back when await is called. Note that Trigger is Awaitable with __await__ return self when coro tries to await the trigger

142 class GPITrigger(Trigger):
143     """Base Trigger class for GPI triggers.
...
...
150     def __init__(self):
151         Trigger.__init__(self)
65 class Trigger(Awaitable):
66     """Base class to derive from."""
...
...
121     @property
122     def _outcome(self):
123         """The result that `await this_trigger` produces in a coroutine.
124
125         The default is to produce the trigger itself, which is done for
126         ease of use with :class:`~cocotb.triggers.First`.
127         """
128         return outcomes.Value(self)
129
130     def __await__(self):
131         # hand the trigger back to the scheduler trampoline
132         return (yield self)
133