RTC Website

Realizing a discrete dynamic system

TX

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:

  1. To use real-time clock interrupts to provide timing
  2. To implement an arbitrary transfer function generator
  3. 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.

 Figure 6.17
Figure 6.17: A discrete system with sample rate T, input x, and output y.

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.

 Figure 6.18
Figure 6.18: A biquad cascade representing an Nth-order system.

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:

  1. 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
  2. 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:

  1. Set up and enable the Timer IRQ interrupt.
  2. Enter a loop, ending only when a keyboard_backspace is received from the keypad. Use getkey(), that we wrote in lab 3.
  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:

  1. 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 write TRUE to the IRQTIMERSETTIME register.
  2. Read the analog input connector C channel 0 AIC0 to obtain the current input value \(x(n)\) (see section 6.5).
  3. 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.
  4. Send the output value to analog output connector C channel 1 AOC1.
  5. 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:

  1. The length of the BTI in microseconds
  2. The number of biquad sections \(n_s\)
  3. 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

y0 = (f->b0 * f->x0 + f->b1 * f->x1 + /* etc */ ) / f->a0;

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,

f->x2 = f->x1; f->x1 = f->x0; // etc.

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

y0 = SATURATE(y0, ymin, ymax);

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

  1. mode sets the waveform shape (0: square wave, 1: sine wave)
  2. amp sets the waveform amplitude in volts
  3. freq_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,

VADin = Aio_Read(&AIC0);
Aio_Write(&AOC1, VADin);

This will allow you to observe the input and output on the oscilloscope and determine if the interrupt timing is functioning properly.

TODO explain

Reference the listing:

Reference a line number: lab1:nifpga_status

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.

Online Resources for Section L6