In this section we will illustrate some common coding practices that are used in real-time DSP implementations and that will be used later in our examples.
As a quick recap, remember that if you need to store past values of a signal, the best solution is to use a circular buffer; assume that you need to access at most
past values of the signal
- set up an array
x_bufof length(of the appropriate data type)
- set up an index variable
ix, initialized at zero
- every time you receive a new sample, store it in the array at
with this, the expression
can be accessed as
x[(ix + M - k) % M].
In a microcontroller, where each CPU cycle counts, modulo operations are expensive but they can be avoided and replaced by binary masks if we choose
to be a power of two. In those cases,
ix % Mis equivalent to
ix & (M-1)and the bitwise AND is a much faster operation. Since
is the minimum number of past values that we need access to, we can always increase
until it reaches a power of two, especially when
Here is a simple example:
#define BUF_LEN 16
#define BUF_MSK 15 /* binary mask is always len - 1 */
uint16_t ix = 0;
/* storing sample x */
x_buf[ix++] = x;
ix &= BUF_MSK;
/* accessing x[n-k] */
uint16_t x_k = x_buf[(ix + BUF_LEN - k) & BUF_MSK];
Most signal processing algorithms require the use of sinusoidal functions. In a microcontroller, however, computing trigonometric values for arbitrary values of the angle is an expensive operation since it always involves some form of Taylor series approximation. Even using a few terms, as in
clearly requires a significant number of multiplications. A computationally cheaper alternative is based on the use of a lookup table. In a lookup table, we precompute the sinusoidal values that we need and use the time index
simply to retrieve the correct value.
In sinusoidal modulation we need to know the values of the sequence
for all values of
. However, if
is a rational multiple of
, that is, if
, then the sequence of sinusoidal values repeats exactly every
For instance, assume the input sampling frequency is
KHz and that our modulation frequency is
Hz. In this case
and therefore we simply need to precompute 80 values for the cosine and store them in an array
C, ..., C. The equation
y[n] = x[n] * C[n % 80]
Of course, we are trading computational time for memory here so, if
in the denominator is impractically large, the table lookup method may become prohibitive, especially on architectures such as the Nucleo which do not have a lot of onboard memory. Also note that this is one case in which we most likely won't be able to use binary masks instead of modulo operations since the period of the sinusoid is unlikely to be a power of two.
Another difficulty is when
is not a rational multiple of
. In this case, we may want to slightly adjust the modulation frequency to a value for which the rational multiple expression becomes valid.
All discrete-time signal processing data and algorithms make use of a free "time" variable
. As we know, in theory
so its value ranges from minus infinity to plus infinity. In an actual DSP application we are much more likely to:
- start the processing with all buffers empty and with(initial conditions)
- storein an unsigned integer variable and increment it at each iteration.
The second point in particular means that, in real time applications that may run for an arbitrary amount of time,
will increase until it reaches the maximum positive value that can be expressed by the variable and then roll over to zero. Since we certainly do not want this rollover to happen at random times and since the roll over is unavoidable, we need to establish a strategy to carry it out explicitly.
In practice, all real-time applications only use circular buffers, either explicitly (to access past input and output values or to access lookup tables) or implicitly (to compute the output of functions that are inherently periodic). As a consequence, we never need the exact value of
but only the position of a set of indices into synchronous circular buffers.
In our code, therefore, we will explicitly roll over these indexes independently and incrementally. To this end:
- to make sure that state variables used by different functions are stepped synchronously, we will define them as global-scope variables at the application level.
These types of variables are often referred to as state variables in C programming and they are usually much frowned upon; the truth is, in a microcontroller real-time application where performance is key, they simply cannot be avoided.