Once we have some Vult code written it’s time to generate some C/C++ code and run it on a target.
If you have followed the installation steps show in https://github.com/vult-dsp/vult you will have an executable called vultc
(if you compiled it by yourself it will be vultc.native
or vultc.byte
). This is a simple command line application that we will use to generate the code.
Here is the full code of the oversampled lowpass filter which we are gonna save in a file called filter.vult
.
fun biquad(x0, b0, b1, b2 ,a1 ,a2) : real { mem w1, w2; val w0 = x0 - a1w1 - a2w2; val y0 = b0w0 + b1w1 + b2*w2; w2, w1 = w1, w0; return y0; }
fun lowpass(x,w0,q) { mem b0,b1,b2,a1,a2; if(change(w0) || change(q)) { val cos_w = cos(w0); val alpha = sin(w0)/(2.0q); val den = 1.0 + alpha; a1 = (-2.0cos_w)/den; a2 = (1.0-alpha)/den; b0 = (1.0-cos_w)/(2.0den); b1 = (1.0-cos_w)/den; b2 = (1.0-cos_w)/(2.0den); } return biquad(x,b0,b1,b2,a1,a2); }
fun lowpass_2x(x,w0,q) { val fixed_w0 = w0/2.0; // first call to lowpass with context ‘inst’ _ = inst:lowpass(x,fixed_w0,q); // second call to lowpass with the same context ‘inst’ val y = inst:lowpass(x,fixed_w0,q); return y; }
Next we are gonna call the Vult compiler as follows:
$ ./vultc -ccode filter.vult
The compiler receives the flag -ccode
which instruct the compiler to generate C/C++ code. This command will print to the standard output the generated code. If we want to save it to a file we have to call the compiler as follows:
$ ./vultc -ccode filter.vult -o filter
This will generate three files: filter.h
, filter.cpp
and filter_tables.h
. Vult generates many auxiliary functions and types. For each function with memory (for example a function called foo
in the file Bar.vult
) there’s gonna be:
Bar_foo_type
Bar_foo_init
Bar_foo
In the case of the filter example the names are: Filter_lowpass_2x_type
, Filter_lowpass_2x_init
and Filter_lowpass_2x
.
To use this code in a file you need to:
Filter_lowpass_2x_type
Filter_lowpass_2x_init
Filter_lowpass_2x
For example:
#include “filter.h”
int main(void) { Filter_lowpass_2x_type filter; // initialization Filter_lowpass_2x_init(filter);
// inputs to the function float x = 0.0f; float w = 1.0f; float q = 1.0f;
// calling the function float result = Filter_lowpass_2x(filter,x,w,q);
return 0; }
In order to compile this code you need to add an include directory pointing to the location of the file vultin.h
. This file is located in the source tree under the folder runtime
. To link you will need to compile and link the file vultin.c
which is located in the same place (https://github.com/modlfo/vult/tree/master/runtime)
One thing to notice is that every function with memory will have as first argument a reference to a value of it’s corresponding type. In the above case, the function Filter_lowpass_2x
returns only one value, therefore the C/C++ will return a value. If the functions return more than one value Vult will automatically generate structures for the types. Here are few Vult functions an their corresponding C/C++ functions:
// single value return, with memory fun bar1(x:real) { mem y = x; return x; }
// multiple value return, no memory fun foo2(x:real) { return x,x; }
// multiple value return, with memory fun bar2(x:real) { mem y = x; return x,x; }
C/C++ declarations of the functions above:
// single value return, with memory float Example_bar1(Example__ctx_type_1 &_ctx, float x);
// multiple value return, no memory void Example_foo2(Example__ctx_type_2 &_ctx, float x);
// multiple value return, with memory void Example_bar2(Example__ctx_type_3 &_ctx, float x);
In the case of functions returning multiple values, the context type is always generated. In order to get the values from the context we can use the generated functions ending with _ret_X
where X
is the position of the returned value. For example, the functions returning multiple values show above generate:
// Gets the second returned value of function Example_foo2 float Example_foo2_ret_1(Example__ctx_type_2 &_ctx);
// Gets the first returned value of function Example_bar2 float Example_bar2_ret_0(Example__ctx_type_3 &_ctx);
// Gets the second returned value of function Example_bar2 float Example_bar2_ret_1(Example__ctx_type_3 &_ctx);
To call the function Example_foo2
from C++ you have to write:
// Call the function Example_foo2(inst, 0.0);
// Retrieve the result float value0 = Example_foo2_ret_0(inst); float value1 = Example_foo2_ret_1(inst);
When generating C/C++ code Vult by default uses floating point arithmetic (float
numbers). Floating point code is very efficient in big processors like the x86. However when compiled to small microcontrollers (like the ones found in Arduinos or some ARM processors) the code can be very inefficient because these processors do not have a dedicated floating point arithmetic unit.
Alternatively, fixed-point calculations can be used to perform computations with decimals (like 2.0*1.5
). Fixed-point arithmetic encodes the decimal numbers as integers and uses the integer arithmetic unit in small processors to perform calculations (https://en.wikipedia.org/wiki/Fixed-point_arithmetic). The result is that the operations can be performed efficiently at the expense of numeric precision.
Vult can generate all operations with real numbers as fixed-point with the format q16.16. This means that 16 bits a used to represent integers and 16 bits for decimals. This implies that the largest number that can be represented is 32767.0
and the smallest 0.0000152588
(the values are signed). Therefore, when generating code with fixed-point numbers one needs to be careful of not going beyond this numbers.
To generate code with fixed-point numbers we need to call Vult as follows:
$ ./vultc -ccode -real fixed filter.vult -o filter
This command will generate use the type fix16_t
instead of float
. For example, the declaration of the function lowpass_2x
changes to:
The file vultin.h
provides functions to convert among float
fix16_t
and int
values.