Loading...
Searching...
No Matches
Driver architecture and workflow

This document explains how drivers are structured and how they operate.

This is useful to know both for debugging and for writing new drivers.

(For details about how the graph does scheduling, which is tied to the driver, see Graph Scheduling ).

# Clocks

A driver is a node that starts graph cycles. Typically, this is accomplished by using a timer that periodically invokes a callback, or by an interrupt.

Drivers use the monotonic system clock as the reference for timestamping. Note that "monotonic system clock" does not refer to the `MONOTONIC_RAW` clock in Linux, but rather, to the regular monotonic clock.

Drivers may actually be run by a custom internal clock instead of the monotonic system clock. One example would be a sound card DAC's clock. Another would be a network adapter with a built in PHC. Or, the driver may be using a system clock other than the monotonic system clock. The driver then needs to perform some sort of timestamp translation and drift compensation from that internal clock to the monotonic clock, since it still needs to generate monotonic clock timestamps for the beginning cycle. (More on that below.)

# Updates and graph cycle start

Every time a driver starts a graph cycle, it must update the contents of the spa_io_clock instance that is assigned to them through the spa_node_methods::set_io callback. The fields of the struct must be updated as follows:

- spa_io_clock::nsec : Must be set to the time (according to the monotonic system clock) when the cycle that the driver is about to trigger started. To minimize jitter, it is usually a good idea to increment this by a computed amount (instead of sampling a timestamp from the monotonic system clock) except for when the driver starts and when discontinuities occur in its clock. The computed amount can be fixed, or varying over time, for example due to adjustments made by a DLL (more on that below).
- spa_io_clock::rate : Set to a value that can translate samples to nanoseconds.
- spa_io_clock::position : Current cycle position, in samples. This is the ideal position of the graph cycle (this is explained in greater detail further below). It is incremented by the duration (in samples) at the beginning of each cycle. If a discontinuity is experienced by the driver that results in a discontinuity in the position of the old and the current cycle, consider setting the SPA_IO_CLOCK_FLAG_DISCONT flag to inform other nodes about this.
- spa_io_clock::duration : Duration of this new cycle, in samples.
- spa_io_clock::rate_diff : A decimal value that is set to whatever correction factor the driver applied to for a drift between an internal driver clock and the monotonic system clock. A value above 1.0 means that the internal driver clock is faster than the monotonic system clock, and vice versa. Always set this to 1.0 if the driver is directly using the monotonic clock.
- spa_io_clock::next_nsec : Must be set to the time (according to the monotonic system clock) when the cycle that comes after the current one is to be started. In some cases, this may actually be in the past relative to nsec, for example, when some internal driver clock experienced a discontinuity. Consider setting the SPA_IO_CLOCK_FLAG_DISCONT flag in such a case. Just like with nsec, to minimize jitter, it is usually a good idea to increment this by a computed amount except for when the driver starts and when discontinuities occur in its clock.

The driver node signals the start of the graph cycle by calling spa_node_call_ready with the SPA_STATUS_HAVE_DATA and SPA_STATUS_NEED_DATA flags passed to that function call. That call must happen inside the thread that runs the data loop assigned to the driver node.

Drivers must make sure that the next cycle is started at the time indicated by the spa_io_clock::next_nsec timestamp. They do not have to use the monotonic clock itself for scheduling the next cycle. If for example the internal clock can directly be used with `timerfd` , the next cycle can be triggered that way.

As mentioned above, the spa_io_clock::position field is the ideal position of the graph cycle, in samples. This contrasts with spa_io_clock::nsec, which is the moment in monotonic clock time when the cycle actually happens. This is an important distinction when driver is run by a clock that is different to the monotonic clock. In that case, the spa_io_clock::nsec timestamps are adjusted to match the pace of that different clock (explained in the section below). In such a case, spa_io_clock::position still is incremented by the duration in samples. This is important, since nodes and modules may use this field as an offset within their own internal ring buffers or similar structures, using the position field as an offset within said data structures. This requires the position field to advance in a continuous way. By incrementing by the duration, this requirement is met.

# Using clocks other than the monotonic clock

As mentioned earlier, the driver may be run by an internal clock that is different to the monotonic clock. If that other clock can be directly used for scheduling graph cycle initiations, then it is sufficient to compute the offset between that clock and the monotonic clock (that is, offset = monotonic_clock_time - other_clock_time) at each cycle and use that offset to translate that other clock's time to the monotonic clock time. This is accomplished by adding that offset to the spa_io_clock::nsec and spa_io_clock::next_nsec fields. For example, when the driver uses the realtime system clock instead of the monotonic system clock, then that realtime clock can still be used with `timerfd` to schedule callback invocations within the data loop. Then, computing the (monotonic_clock_time - realtime_clock_time) offset is sufficient, as mentioned, to be able to translate clock's time to monotonic time for spa_io_clock::nsec and spa_io_clock::next_nsec (which require monotonic clock timestamps).

If however that other clock cannot be used for scheduling graph cycle initiations directly (for example, because the API of that clock has no functionality to trigger callbacks), then, in addition to the aforementioned offset, the driver has to use the monotonic clock for triggering callbacks (usually via `timerfd`) and adjust the time when callbacks are invoked such that they match the pace of that other clock.

As an example (clock speed difference exaggerated for sake of clarity), suppose the other clock is twice as fast as the monotonic clock. Then the monotonic clock timestamps have to be calculated in a manner that halves the durations between said timestamps, and the spa_io_clock::rate_diff field is set to 2.0.

The dummy node driver uses a DLL for this purpose. It is fed the difference between the expected position (in samples) and the actual position (derived from the current time of the driver's internal clock), passes the delta between these two quantities into the DLL, and the DLL computes a correction factor (2.0 in the above example) which is used for scaling durations between `timerfd` timeouts. This forms a control loop, since the correction factor causes the spa_io_clock::next_nsec increments (that is, the durations between the timerfd timeouts) to be adjusted such that, over time, the difference between the expected position and the actual position reaches zero. Keep in mind the notes above about spa_io_clock::position being the ideal position of the graph cycle, meaning that even in this case, the duration it is incremented by is not scaled by the correction factor; the duration in samples remains unchanged.

(Other popular control loop mechanisms that are suitable alternatives to the DLL are PID controllers and Kalman filters.)