Motivations and Goal
The main purpose of this project was to improve my C programming skills and to have fun with a small project. So why did I choose to work on this particular topic? On the one hand, I had lost my access to Simulink, so I wanted to build the simplest Simulink that I could think of. On the other hand, I was inspired by the presentation of Anders Damsgaard. You can find the source code of the project here.
A great idea of Simulink is its block abstraction: you build your
simulation by connecting/building different blocks together. A similar
idea can be found in Unix with its concept of pipes. In Unix systems,
streams of text work as the universal interface between programs. The
output of a program A
can be used as the input of another program
B
by executing in the shell: ./A | ./B
. I wanted to leverage Unix
pipes to build my Simulink. As a result, each block becomes
an executable that takes its input from the standard input
, and
writes its output to the standard output
.
Below, you can find what I think are some advantages and disadvantages of this approach to simulation:
Advantages:
- It is a simple solution easy to extend: creating a new block is trivial.
- It integrates well with other existing tools, e.g.,
gnuplot
to plot the results of the simulation. - Everything is a file, so the simulations can easily be tracked using
git
which is great for having reproducible results. - It is easy to automate simulations with shell scripts or makefiles.
- Different simulations can be run in parallel using
make -j
. - For control law simulations, you can directly understand how to implement the controller on an embedded computer or micro-controller.
Disadvantages:
- Pipes are not great for feedback loops. To circumvent this limitation, all feedback loops must be implemented as individual blocks.
- Way way way less ergonomic than Simulink. It takes much more effort to run a simulation.
- There are not as many blocks as in Simulink.
- Simulations can be hard to debug.
The following sections present the conventions used in the project.
Transfer Function Representations
The files z-utils.h
and z-utils.c
provide useful abstractions and
functions to work with discrete transfer functions.
Considering the following transfer function: $$ G(z) = \frac{Y(z)}{U(z)} = \frac{b_0 + b_1 z^{-1} + b_2 z^{-2} + \ldots + b_m z^{-m}} {a_0 + a_1 z^{-1} + a_2 z^{-2} + \ldots + a_n z^{-n}} $$
where \(m \leq n\).
To use \(G(z)\) in a program, the coefficient \(b_i\) and \(a_i\) must be written into a file. The file format used to encode transfer function is better understood with an example. Let us consider the following transfer function:
$$ G(z) = \frac{1 + 2 z^{-1}} {1 + 3 z^{-1} + 4z^{-2} + 5 z^{-3}} $$
To load the transfer function \(G\) in our programs, we would create a file with the following content:
N 1 2
D 1 3 4 5
Time and Input/Output Conventions
Currently, all the tools work only with discrete time systems. Each
input/output line corresponds to the value(s) of the block at a time
step. This first column of an input/output file is reserved to
represent the simulation time. Again, this is better understood with
an example. Let us consider a “trivial block” stored in a file
z_trv.txt
. This block computes its output by adding its input to its
previous output:
$$ T(z) = \frac{Y(z)}{U(z)} = \frac{1}{1 - z^{-1}} $$ which gives us in the time domain: $$ y_k = y_{k-1} + u_{k} $$
We assume that the block’s initial condition, passed as a parameter to
the program, is equal to \(7\). Suppose that the input file
input.txt
given to this block is:
1 4
2 5
3 6
where the first column represents the simulation time, and the second
column represents the input given to the transfer function block. To
execute the simulation, we run z-tf -f z_trv.txt -i 7.0 < input.txt
,
which gives the following output:
1 4 11
2 5 16
3 6 22
Here the program adds a third column corresponding to the output of the transfer function. As a general rule, each block only adds columns to the standard output. As a consequence, the simulation data from all the blocks is never lost.
Feedback Loops
The z-cl
program computes the response of a system specified by the
control block diagram above. The user must specify the transfer
function for the controller \(C\) and the system \(G\). At each
time-step \(t_k\), the program outputs the signal values of
\(r_k\), \(e_k\), \(u_k\), and \(y_k\) separated by tabs.
Lessons Learned
The list below is a note for my future self, it enumerates different elements to consider for future projects:
- Think harder to find good names: create a glossary.
- Write tests as you go.
- Use all the tools to help with memory management from the beginning: compiler flags, valgrind.