Realizing a discrete dynamic system
In this lab exercise, you will write a general-purpose program capable of approximating the performance of any SISO, linear time-invariant (LTI) system. Your program will implement this by realizing a difference equation.
The objectives of this exercise are:
- To use real-time clock interrupts to provide timing
- To implement an arbitrary transfer function generator
- To introduce analog-to-digital and digital-to-analog conversion
The T1 target system will be required, but only the components listed in appendix B are needed for this lab exercise. Set up each component in accordance with the instructions for your specific T1 version in section A.2. Connect your components in the manner described in the “Online Resources” section of the web page for this lab exercise: https://rtcbook.org/6n.
Introduction
Control, measurement, and signal processing systems often include SISO dynamic elements that can be described by a linear differential equation. The dynamic characteristics of the complete system are determined through the appropriate design of these elements. In this lab exercise, you will use the techniques of real-time computing to implement a general-purpose program that is capable of approximating the performance of any SISO, linear, time-invariant system. The system input and output will both be analog electrical signals.
Your program will implement an approximation to a linear differential equation called a difference equation (section 6.4). It takes the form \[ a_{0}y(n)+a_{1}y(n-1)+\dots+a_{N}y(n-N) = b_{0}x(n)+b_{1}x(n-1)+\dots+b_{M}x(n-M), \] for \(n = 0, 1, 2, \dots\), where \(x(n)\) is a sequence of periodically digitized values of the analog input signal, \(y(n)\) is a sequence of values that determine the output signal, and \(a_{k}, k=0,1, \dots,N\) and \(b_{\kappa}, \kappa=0,1,\dots,M\) are constants, as shown in figure 6.17.
This equation can also be written in summation form: \[\sum_{k=0}^{N} a_{k}y(n-k)=\sum_{\kappa=0}^{M} b_{\kappa}x(n-\kappa), \qquad{(1)}\] or, solving eq. 1 for the current output sample \(y(n)\), \[y(n)=\frac{1}{a_{0}}\left[\sum_{\kappa=0}^{M} b_{\kappa}x(n-\kappa)-\sum_{k=1}^{N} a_{k}y(n-k)\right]. \qquad{(2)}\] Notice that the most recent output value \(y(n)\) depends on previous values of \(y\) and on the previous and current values of \(x\).
At the beginning of each BTI, your ISR will read the analog-to-digital converter (ADC) to obtain the current input value, compute the current value of the output \(y(n)\), and apply the current output value to the digital-to-analog converter (DAC). This process continues until a backspace is entered via the keypad.
The input voltage will be derived from a function generator. Both the input and output voltages will be displayed on the oscilloscope.
You will use three new tools in this lab exercise: an interrupt timer, the ADC, and the DAC in the myRIO.
The biquad cascade
Although we could implement the difference eq. 1 as shown, the sensitivity of the output to the coefficients leads to numerical inaccuracies as the order of the system \(N\) becomes large, so we will represent the \(N\)th-order system as a sequence of second-order systems as described in section 6.4.
Again, this technique is called a biquad cascade, and it is illustrated in figure 6.18.
Notice that the output of each second-order section (biquad) is the input to the subsequent section. Each biquad implements the same second-order difference equation, but with different coefficients, inputs, outputs, and internal states. For example, the current output \(y_i(n)\) from the \(i\)th section would be \[ y_i(n) = \left[b_{0_i}x_i(n) +b_{1_i}x_i(n-1)+b_{2_i}x_i(n-2) -a_{1_i}y_i(n-1)-a_{2_i}y_i(n-2)\right]/a_{0_i}. \qquad{(3)}\]
Of course, a first- or second-order transfer function would require only one biquad. Depending on the value of \(N\), some of the coefficients of at least one biquad may be zero. We will implement a function to handle any value of \(N\).
The MATLAB Control System Toolbox contains convenient functions for the conversion of continuous models to discrete models:
The function
c2d()
, “continuous to discrete,” computes an equivalent discrete system from a continuous system. For example,SYSD = c2d(SYS, T, 'tustin'); % SYS is a continuous system model
The function
tf2sos()
, “transfer function to second-order sections,” breaks a transfer function into second-order biquad sections. For example,Cd = tf(SYSD); % SYSD is a discrete system model b, a] = tfdata(Cd, 'v'); % tf coefficients [sos = tf2sos(b, a); % biquad sections
Writing the program
The program should consist of a main()
function and
an interrupt service routine (ISR) running in a separate thread. See section 6.6. The ISR is set to execute with a period of
\(0.5\) ms (determined by the Timer
IRQ), and computes the DAC output from the ADC input by means of a
difference equation.
Main function
The only tasks of main()
will be the
following:
- Set up and enable the Timer IRQ interrupt.
- Enter a loop, ending only when a
is received from the keypad. Use
getkey()
, that we wrote in lab 3. - Signal the timer thread to terminate using the
irqThreadRdy
flag and wait for the thread to terminate.
Interrupt service routine
The ISR thread implements a dynamic system. The heart of the ISR is a
while
loop
that checks the irqThreadRdy
flag (set
in main()
) to
see if the thread should continue.
Before the loop begins, initialize the AIO, and set the analog output
connector C channel-1 (AOC1
) to \(0\) V.
Each time through the loop do the following:
- Get ready for the next interrupt by waiting for the IRQ to assert.
Then, write the time interval to wait between interrupts (BTI) to the
IRQTIMERWRITE
register and writeTRUE
to theIRQTIMERSETTIME
register. - Read the analog input connector C channel 0
AIC0
to obtain the current input value \(x(n)\) (see section 6.5). - Call a function
cascade()
(see below) to calculate the current value of the output \(y(n)\) by computing all of the sections in the biquad cascade. Each biquad section is computed according to subsection 6.4.3. - Send the output value to analog output connector C channel 1
AOC1
. - Acknowledge the interrupt.
After the loop terminates, save the response to Lab6.mat (see section D.1).
The ISR must allocate storage for variables and arrays associated with the discrete dynamic system, including:
- The length of the BTI in microseconds
- The number of biquad sections \(n_s\)
- The system constants (\(a_k\) and \(b_\kappa\)) for the biquad sections
The dynamic system corresponding to the collection of biquad sections can be conveniently referred to and manipulated by first defining a structure to contain the coefficients and previous values of input and output for a single biquad section. We might define a “biquad” structure as
struct biquad {
double b0; double b1; double b2; // numerator
double a0; double a1; double a2; // denominator
double x0; double x1; double x2; // input
double y1; double y2; // output
};
This definition should be placed just before the prototypes section of your program.
Then, a specific dynamic system can be defined as an array of these biquad structures. In this structure, each array element corresponds to an individual biquad section:
int myFilter_ns = 2; // No. of sections
uint32_t timeoutValue = 500; // T - us; f_s = 2000 Hz
static struct biquad myFilter[] = {
{1.0000e+00, 9.9999e-01, 0.0000e+00,
1.0000e+00, -8.8177e-01, 0.0000e+00, 0, 0, 0, 0, 0},
{2.1878e-04, 4.3755e-04, 2.1878e-04,
1.0000e+00, -1.8674e+00, 8.8220e-01, 0, 0, 0, 0, 0}
};
The dynamic system corresponding to the array myFilter
is a low-pass filter derived using
Tustin’s method from the transfer function of the three-pole continuous
system: \[
\frac{V_\text{out}(s)}{V_\text{in}(s)}=\frac{
{\omega_n}^3}{(s+\omega_n)(s^2+2\zeta\omega_n s+{\omega_n}^2)}\quad,
\] where \(\omega_n=2\pi\cdot
40\) rad/s, and \(\zeta=0.5\).
This system belongs to a class of filters called Butterworth
filters, which are signal-processing filters designed to have the
flattest possible frequency response in the passband.
This system description can be placed within the ISR, near its
beginning. The first two lines establish the number of biquad sections,
and the length of the BTI in microseconds. Finally, myFilter
is the name of an array of biquad
structures being initialized.
For testing purposes, the initialized constants in this example correspond to a system of two biquad sections (\(n_s=2\)), encoding a unity-gain low-pass filter with sampling frequency of \(2000\) Hz. Derived using Tustin’s method, they correspond to a third-order continuous system with a pair of complex poles with natural frequency of \(40\) Hz, and with damping ratio \(0.5\). The remaining real pole is at \(40\) Hz.
Crazy about pointers!
The most challenging programming task is the calculation of the current output value \(y(n)\). The use of pointers makes the calculation both straightforward and efficient.
The cascade()
function
The cascade()
function
implements the complete dynamic system by passing the measured input
through the string of biquad sections. The ISR must pass to cascade()
the value
of the current input \(x(n)\) measured
by the ADC, the array of biquad structures containing the coefficients,
the number of biquad sections \(n_s\),
and history variables (\(x\)s and \(y\)s) for all sections. Its prototype
should be
double cascade(double xin, // input
struct biquad *fa, // biquad array
int ns, // no. segments
double ymin, // min output
double ymax); // max output
Here, xin
is the current system
input, fa
is the name of an array of
biquad structures, ns
is the
corresponding number of biquad sections, and ymin
and ymax
are the saturation limits.
In this example, myFilter
would be
passed through fa
. The value returned
by cascade()
is the current value of the system output \(y(n)\).
Programming
cascade()
An efficient way to code cascade()
is to
allocate a pointer f
in cascade()
that will
be used to point to elements of the array of biquad structures. Begin
the function by setting the pointer to the first element in the array
(i.e., the first biquad): f = fa
. Variables
inside the biquad structure are accessed by using the pointer name,
(e.g., f->a0
, f->b0
, f->x0
, f->y1
, etc.).
(The ->
operator is equivalent to dereferencing and then accessing a member—say,
(*f).a0
—and
is typed as a minus sign immediately followed by >
.)
Then, loop \(n_s\) times, to cycle
through each of the biquad sections in the array. At the beginning of
each loop, the output value y0
of the
previous biquad must be passed to the input value f->x0
of the
current biquad.
Within the loop, coding the output value y0
might look like
= (f->b0 * f->x0 + f->b1 * f->x1 + /* etc */ ) / f->a0; y0
See eq. 3.
Each time through the loop, after the output value has been computed, the previous values of \(x\) and \(y\) must be updated so they will be correct at the next time step. For example,
->x2 = f->x1; f->x1 = f->x0; // etc. f
At the end of the loop, the pointer f
is incremented to advance to the next
biquad in the array.
One more point: If the DAC is given a value beyond its range \([-10, +10]\) V, it will saturate its output
value appropriately. However, our difference eq. 3 depends on previous
values of the output, but it doesn’t saturate. To correct this
disparity, cascade()
should
saturate the output y0
of the
final biquad before it is saved for the next
iteration.
For example, define the macro as follows:
#define SATURATE(x,lo,hi) ((x) < (lo) ? (lo) : (x) > (hi) ? (hi) : (x))
Pass appropriate values of the xmin
and xmax
parameters to cascade()
. Then, for
the last biquad, immediately after y0
is computed, saturate its value as follows
= SATURATE(y0, ymin, ymax); y0
Emulation
As an aid to debugging, our T1 C library includes a software “emulation” (a dynamic model) of the analog input channel. The emulator allows you to run your code and save the MATLAB file without being connected to the function generator or oscilloscope.
To activate the emulator, #include
the header
file emulate.h
. A T1 C
Library function sets the characteristics of the emulated function
generator. Its prototype is
void fg_set(uint32_t mode,
double amp,
double freq_Hz);
where
mode
sets the waveform shape (0: square wave, 1: sine wave)amp
sets the waveform amplitude in voltsfreq_Hz
sets the waveform frequency in hertz
When you want to execute your code on the myRIO using the hardware interfaces, comment out the included emulator header and rebuild the project.
Laboratory exploration and evaluation
Write the main()
program
described in subsection L6.2, including the timer ISR,
but instead of writing cascade()
, simply
write the input directly to the output. That is, first, implement and
debug everything except the calculation of the biquad cascade. Set up
the main()
program and the ISR, including all arrays and timing. In the ISR, simply
pass the input value from the ADC directly to the DAC. For example,
= Aio_Read(&AIC0);
VADin (&AOC1, VADin); Aio_Write
This will allow you to observe the input and output on the oscilloscope and determine if the interrupt timing is functioning properly.
Write the cascade()
function
described in subsection L6.2. Augment the main()
function
written in problem L6.1 such that the cascade()
function
computes the output.
Augment the timer ISR to save the input and output of cascade()
in \(500\)-point buffers (see section D.1). After the timer loop ends, save the buffers
to Lab6.mat.
Using the oscilloscope (DC coupled), observe the step response of the system by applying a low-frequency square wave (e.g., at \(8\) Hz) with an amplitude of \(5\) V as the input with the function generator.
Transfer the step response data saved to Lab6.mat to your development computer and load it
into MATLAB. Plot and compare the measured step response to the
theoretical response of the corresponding continuous system. To simulate
the theoretical response, MATLAB’s lsim()
is a
good choice.
Compare the measured and theoretical responses and explain any differences.
Again, using the oscilloscope (AC coupled), observe the frequency response by altering the frequency of a 5-V input sine wave.
Record (i.e., write down) the amplitude and phase of the output relative to the input sine wave at the following frequencies: \(5\), \(10\), \(20\), \(40\), \(60\), \(100\), \(140\), and \(200\) Hz.
From the data gathered in problem L6.5, compute the transfer function magnitude in decibels at each frequency.
In MATLAB, plot the theoretical magnitude in decibels and
phase in degrees versus the frequency (in hertz on a
logarithmic scale) for the continuous system transfer function. To
generate the data for this plot, MATLAB’s bode()
is a
good choice. Note that the specifications for the plot format require
you to generate the plot separately from your call to bode()
. Plot
the corresponding measured data as discrete symbols on top of the
theoretical frequency response. Compare the measured and theoretical
responses and explain any differences.