Simple object to represent digital systems, controllers, filters, etc.
This file contains \LaTeX equations and is much better read on a dedicated markdown renderer. A PDF version is included for readability, but no guarantee that it's up to date with the markdown.
Discrete linear time-invariant systems can be described by a linear constant coefficient difference equation of the following form $$ a_0 y_k + a_1 y_{k-1} + \dots + a_{n-1} y_{k-(n-1)} = b_0 u_k + b_1 u_{k-1} + \dots + b_{m-1} u_{k-(m-1)}, $$
where
Specification of the coefficients
This library defines a single object called DigiSys
which can be used to represent a discrete time system up to order MAX_LEN
parameter in DigiSys.h
The possible class signatures are as follows:
DigiSys(double num[], double den[], int num_len, int den_len);
DigiSys(double gain, double num[], double den[], int num_len, int den_len);
-
num[]
is an array of coefficients for the transfer function numerator, arranged as $\begin{bmatrix} b_0 & b_1 & \dots & b_{m-1} \end{bmatrix}$. -
den[]
is an array of coefficients for the transfer function denominator, arranged as $\begin{bmatrix} a_0 & a_1 & \dots & a_{n-1} \end{bmatrix}$. -
num_len
is the length of the transfer function numerator. Must satisfy$m \leq$ MAX_LEN
. -
den_len
is the length of the transfer function denominator. Must satisfy$n \leq$ MAX_LEN
. -
gain
is an optional parameter defining a constant$K$ which scales the whole transfer function. Assumed to be unity by default.
DigiSys
contains a single public method with the following signature
double DigiSys::update(double input);
- The argument
input
corresponds to the value of$u_k$ at the current timestep. - The return value corresponds to the value of
$y_k$ at the current timestep. - The necessary values of
$u_{k-j}$ and$y_{k-i}$ are stored internally in the object.
Consider designing a first-order discrete filter which operates at 100 Hz has a cutoff frequency of 10 Hz. We will use Matlab to design the filter since it creates coefficient vectors in the desired form for the DigiSys
object.
The filter can be designed using the following Matlab commands
ord = 1; % filter order
fs = 100; % sampling frequency [Hz]
fnyq = fs/2; % Nyquist frequency [Hz]
fc = 10; % filter cutoff frequency [Hz]
% calculate filter coefficients
[num, den] = butter(ord, fc/fnyq);
which results the following coefficient vectors
num =
0.2452 0.2452
den =
1.0000 -0.5095
These coefficients correspond to the transfer function $$ \frac{Y[z]}{U[z]} = \frac{0.2452 + 0.2452 z^{-1}}{1.0000 - 0.5095 z^{-1}} $$
which is a first-order Butterworth filter. Translating these into code:
#include <DigiSys.h>
const double num[] = {0.2452, 0.2452};
const double den[] = {1.0000, -0.5095};
const int m = 2;
const int n = 2;
DigiSys myFilter(num, den, m, n);
To use the filter on incoming sensor measurements, simply call the DigiSys.update
method and pass in the raw measurement data. For example, in an Arduino environment:
void loop() {
// get sensor reading
int raw_measurement = analogRead(sensor_pin);
// apply filter
double filtered_measurement = myFilter.update(raw_measurement);
/*
* do something with the filtered data...
*
* wait until the next sample time...
*/
}
When applying the filter to sensor measurements, ensure that the measurements are obtained at the same sample frequency that the filter was designed for. Sampling at different frequencies will result in erroneous filter calculations. I strongly encourage the use of timer interrupts for this rather than loop delay.
Note on numerical accuracy: The transfer function defined above has a static gain of
Consider a single-input single-output system controlled by a lead compensator. The compensator is given by the continuous-time transfer function $$ C(s) = 50 \frac{s + 1}{s + 10} $$
which adds approximately butter
command in Matlab, the filter is described by the continuous-time transfer function
$$ F(s) = \frac{100}{s + 100} $$
The following Matlab commands will convert the combined compensator and filter into a discrete-time transfer function based on a sampling frequency of
s = tf('s');
C = 50*(s + 1)/(s + 10); % compensator
F = 100/(s + 100); % filter
contr = F*C; % series connection of compensator and filter
dt = 1e-3; % sampling period [s]
contr_disc = c2d(contr, dt); % convert to discrete
resulting in the following coefficient vectors
contr_disc.Numerator{:} =
0 4.7364 -4.7317
contr_disc.Denominator{:} =
1.0000 -1.8949 0.8958
These coefficients correspond to the discrete-time transfer function $$ \frac{Y[z]}{U[z]} = \frac{4.7364 z^{-1} -4.7317 z^{-2}}{1.0000 -1.8949 z^{-1} + 0.8958 z^{-2}} $$
Notice that the numerator coefficients include a leading zero which cannot be ignored without changing the transfer function. Also note that the gain parameter has been absorbed into the numerator coefficients. To demonstrate the overloaded constructor, we will separate the static gain and rescale the numerator coefficients to obtain the new transfer function with unit static gain $$ \frac{Y[z]}{U[z]} = 5.000 \frac{0.9473 z^{-1} -0.9463 z^{-2}}{1.0000 -1.8949 z^{-1} + 0.8958 z^{-2}} $$
Translating this into code:
#include <DigiSys.h>
const double gain = 5.000;
const double num[] = {0, 0.9473, -0.9463};
const double den[] = {1.0000, -1.8949, 0.8958};
const int m = 3;
const int n = 3;
DigiSys myController(gain, num, den, m, n);
This filter + compensator can then be called in an appropriately timed loop and the control signals calculated from the raw measurements in one line
void control_function() {
// call this at 1000 Hz using a timer interrupt
// get sensor reading
int raw_measurement = analogRead(sensor_pin);
// calculate control signal using combined filter and compensator
double control_signal = myController.update(raw_measurement);
/*
* send control signal to actuator...
*
* maybe do some other stuff...
*
* wait until the next control cycle...
*/
}