Skip to content

Commit b82044d

Browse files
add JuliaC example (JuliaControl#13)
* add JuliaC example * add c test program * make wrappers `2ccallable` again * juliac demo working! Co-authored-by: Cody Tapscott <topolarity@tapscott.me> * update juliac readme instrucitons * add note about julia from julia --------- Co-authored-by: Cody Tapscott <topolarity@tapscott.me>
1 parent 7a1c376 commit b82044d

8 files changed

+206
-2
lines changed

Project.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ julia = "1.7"
1313
AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a"
1414
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
1515
FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93"
16+
JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"
1617
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1718

1819
[targets]
19-
test = ["AllocCheck", "Test", "ControlSystemsBase", "FixedPointNumbers"]
20+
test = ["AllocCheck", "Test", "ControlSystemsBase", "FixedPointNumbers", "JET"]

README.md

+30
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,36 @@ plot([res, res_fp], plotu=true, lab=["Float64" "" string(T) ""]); ylabel!("u + d
195195

196196
The fixed-point controller behaves roughly the same in this case, but artifacts are clearly visible. If the number of bits used for the fractional part is decreased, the controller will start to misbehave.
197197

198+
## Compilation using JuliaC
199+
The file `examples/juliac/juliac_pid.jl` contains a JuliaC-compatible interface that can be compiled into a C-callable shared library using JuliaC. To compile the file, run the following from the `examples/juliac` folder:
200+
```bash
201+
julia +nightly --project <PATH_TO_JULIA_REPO>/julia/contrib/juliac.jl --output-lib juliac_pid --experimental --trim=unsafe-warn --compile-ccallable juliac_pid.jl
202+
```
203+
where `<PATH_TO_JULIA_REPO>` should be replaced with the path to the Julia repository on your system. The command will generate a shared library `juliac_pid` that can be called from C. The file `examples/juliac/juliac_pid.h` contains the C-compatible interface to the shared library. The C program may be compiled with a command like
204+
```bash
205+
export LD_LIBRARY_PATH=<PATH_TO_JULIA_REPO>/julia/usr/lib:$LD_LIBRARY_PATH
206+
gcc -o pid_program test_juliac_pid.c -I <PATH_TO_JULIA_REPO>/julia/usr/include/julia -L<PATH_TO_JULIA_REPO>/julia/usr/lib -ljulia -ldl
207+
```
208+
and then run by
209+
```bash
210+
./pid_program
211+
```
212+
which should produce the output
213+
```
214+
DiscretePIDs/examples/juliac> ./pid_program
215+
Loading juliac_pid.so
216+
Loaded juliac_pid.so
217+
Finding symbols
218+
Found all symbols!
219+
calculate_control! returned: 1.000000
220+
calculate_control! returned: 2.000000
221+
calculate_control! returned: 3.000000
222+
calculate_control! returned: 3.000000
223+
calculate_control! returned: 3.000000
224+
```
225+
At the time of writing, this requires a nightly version of julia
226+
227+
198228
## See also
199229
- [TrajectoryLimiters.jl](https://github.com/baggepinnen/TrajectoryLimiters.jl) To generate dynamically feasible reference trajectories with bounded velocity and acceleration given an instantaneous reference $r(t)$ which may change abruptly.
200230
- [SymbolicControlSystems.jl](https://github.com/JuliaControl/SymbolicControlSystems.jl) For C-code generation of LTI systems.

examples/juliac/Project.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[deps]
2+
ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e"
3+
DiscretePIDs = "c1363496-6848-4723-8758-079b737f6baf"
4+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

examples/juliac/juliac_pid.jl

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
module JuliacPID
2+
import DiscretePIDs
3+
import DiscretePIDs: DiscretePID
4+
import Base.@ccallable
5+
6+
const T = Float64 # The numeric type used by the controller
7+
8+
# Set the initial PID parameters here
9+
const pid = DiscretePIDs.DiscretePID(; K = T(1), Ti = 1, Td = false, Ts = 1)
10+
11+
12+
@ccallable function calculate_control!(r::T, y::T, uff::T)::T
13+
DiscretePIDs.calculate_control!(pid, r, y, uff)::T
14+
end
15+
16+
@ccallable function set_K!(K::T, r::T, y::T)::Cvoid
17+
DiscretePIDs.set_K!(pid, K, r, y)
18+
nothing
19+
end
20+
21+
@ccallable function set_Ti!(Ti::T)::Cvoid
22+
DiscretePIDs.set_Ti!(pid, Ti)
23+
nothing
24+
end
25+
26+
@ccallable function set_Td!(Td::T)::Cvoid
27+
DiscretePIDs.set_Td!(pid, Td)
28+
nothing
29+
end
30+
31+
@ccallable function reset_state!()::Cvoid
32+
DiscretePIDs.reset_state!(pid)
33+
nothing
34+
end
35+
36+
# @ccallable function main()::Cint
37+
# println(Core.stdout, "I'm alive and well")
38+
# u = calculate_control!(0.0, 0.0, 0.0)
39+
# println(Core.stdout, u)
40+
41+
# Cint(0)
42+
# end
43+
44+
45+
end
46+
47+
# compile using something like this, modified to suit your local paths
48+
# cd(@__DIR__)
49+
# run(`/home/fredrikb/repos/julia/julia --project --experimental /home/fredrikb/repos/julia/contrib/juliac.jl --output-lib juliac_pid --experimental --trim=unsafe-warn --compile-ccallable juliac_pid.jl`)
50+
# run(`ls -ltrh`)

examples/juliac/test_juliac_pid.c

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include <stdio.h>
2+
#include <stdlib.h>
3+
#include <dlfcn.h>
4+
// #include <julia.h>
5+
6+
// Path to julia binary folder
7+
#define JULIA_PATH "/home/fredrikb/repos/julia/usr/bin/" // NOTE: modify this path
8+
9+
// Path to juliac compiled shared object file
10+
#define LIB_PATH "/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so" // NOTE: modify this path
11+
12+
13+
14+
// Define the types of the julia @ccallable functions
15+
typedef void (*jl_init_with_image_t)(const char *bindir, const char *sysimage);
16+
typedef double (*calculate_control_t)(double r, double y, double uff);
17+
typedef void (*set_K_t)(double K, double r, double y);
18+
typedef void (*set_Ti_t)(double Ti);
19+
typedef void (*set_Td_t)(double Td);
20+
typedef void (*reset_state_t)();
21+
22+
23+
int main() {
24+
25+
// Load the shared library
26+
printf("Loading juliac_pid.so\n");
27+
void *lib_handle = dlopen(LIB_PATH, RTLD_LAZY);
28+
if (!lib_handle) {
29+
fprintf(stderr, "Error: Unable to load library %s\n", dlerror());
30+
exit(EXIT_FAILURE);
31+
}
32+
printf("Loaded juliac_pid.so\n");
33+
34+
// Locate the julia functions function
35+
printf("Finding symbols\n");
36+
jl_init_with_image_t jl_init_with_image = (jl_init_with_image_t)dlsym(lib_handle, "jl_init_with_image");
37+
38+
calculate_control_t calculate_control = (calculate_control_t)dlsym(lib_handle, "calculate_control!");
39+
set_K_t set_K = (set_K_t)dlsym(lib_handle, "set_K!");
40+
set_Ti_t set_Ti = (set_Ti_t)dlsym(lib_handle, "set_Ti!");
41+
set_Td_t set_Td = (set_Td_t)dlsym(lib_handle, "set_Td!");
42+
reset_state_t reset_state = (reset_state_t)dlsym(lib_handle, "reset_state!");
43+
44+
45+
if (jl_init_with_image == NULL || calculate_control == NULL) {
46+
char *error = dlerror();
47+
fprintf(stderr, "Error: Unable to find symbol: %s\n", error);
48+
exit(EXIT_FAILURE);
49+
}
50+
printf("Found all symbols!\n");
51+
52+
// Init julia
53+
jl_init_with_image(JULIA_PATH, LIB_PATH);
54+
55+
// Trivial test program that computes a few control outputs and modifies K
56+
double r = 1.0, y = 0.0, uff = 0.0;
57+
double result = calculate_control(r, y, uff);
58+
printf("calculate_control! returned: %f\n", result);
59+
result = calculate_control(r, y, uff);
60+
printf("calculate_control! returned: %f\n", result);
61+
set_K(0.0, r, y);
62+
for (int i = 0; i < 3; ++i) {
63+
result = calculate_control(r, y, uff);
64+
printf("calculate_control! returned: %f\n", result);
65+
}
66+
67+
// jl_atexit_hook(0);
68+
return 0;
69+
}
70+
71+
72+
// Compile this C program using a command like the one above, modified to suit your paths
73+
// export LD_LIBRARY_PATH=/home/fredrikb/repos/julia/usr/lib:$LD_LIBRARY_PATH
74+
// gcc -o pid_program test_juliac_pid.c -I /home/fredrikb/repos/julia/usr/include/julia -L/home/fredrikb/repos/julia/usr/lib -ljulia -ldl

examples/juliac/test_juliac_pid.jl

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# NOTE: it is currently not possible to call a julia-produced shared-library from julia.
2+
# To test the compiled shared library, see test_juliac_pid.c instead.
3+
cd(@__DIR__)
4+
5+
const T = Float64
6+
@info("Loading juliac_pid.so")
7+
lib = Libc.Libdl.dlopen("/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so")
8+
@info("Loaded juliac_pid.so, finding calculate_control!")
9+
const calc = Libc.Libdl.dlsym(lib, :calculate_control!)
10+
@info("Found calculate_control!")
11+
12+
function pid(r::T, y::T, uff::T)
13+
ccall(calc, T, (T, T, T), r, y, uff)
14+
end
15+
16+
pid(0.0, 0.0, 0.0) # test
17+
18+
using ControlSystemsBase, Plots
19+
Tf = 15 # Simulation time
20+
Ts = 0.01 # sample time
21+
22+
P = c2d(ss(tf(1, [1, 1])), Ts) # Process to be controlled, discretized using zero-order hold
23+
24+
ctrl = function(x,t)
25+
y = (P.C*x)[] # measurement
26+
d = 1 # disturbance
27+
r = 0 # reference
28+
u = pid(T(r), T(y), T(0))
29+
u + d # Plant input is control signal + disturbance
30+
end
31+
32+
res = lsim(P, ctrl, Tf)
33+
34+
plot(res, plotu=true); ylabel!("u + d", sp=2)

src/DiscretePIDs.jl

+7-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ u = calculate_control!(pid, r, y, uff) # Equivalent to the above
7171
- `D`: Derivative part
7272
- `yold`: Last measurement signal
7373
74-
See also [`calculate_control!`](@ref), [`set_K!`](@ref), [`set_Ti!`](@ref), [`set_Td!`](@ref)
74+
See also [`calculate_control!`](@ref), [`set_K!`](@ref), [`set_Ti!`](@ref), [`set_Td!`](@ref), [`reset_state!`](@ref).
7575
"""
7676
function DiscretePID(;
7777
K::T = 1f0,
@@ -115,6 +115,8 @@ end
115115
set_K!(pid::DiscretePID, K, r, y)
116116
117117
Update `K` in the PID controller. This function takes the current reference and measurement as well in order to provide bumpless transfer. This is realized by updating the internal state `I`.
118+
119+
Note: Due to the bumpless transfer, setting ``K = 0`` does not imply that the controller output will be 0 if the integral state is non zero. To reset the controller state, call `reset_state!(pid)`.
118120
"""
119121
function set_K!(pid::DiscretePID, K, r, y)
120122
Kold = pid.K
@@ -124,6 +126,7 @@ function set_K!(pid::DiscretePID, K, r, y)
124126
pid.bi = K * pid.Ts / pid.Ti
125127
pid.I = pid.I + Kold*(pid.b*r - y) - K*(pid.b*r - y)
126128
end
129+
nothing
127130
end
128131

129132
"""
@@ -139,6 +142,7 @@ function set_Ti!(pid::DiscretePID{T}, Ti) where T
139142
else
140143
pid.bi = zero(T)
141144
end
145+
nothing
142146
end
143147

144148
"""
@@ -151,6 +155,7 @@ function set_Td!(pid::DiscretePID, Td)
151155
pid.Td = Td
152156
pid.ad = Td / (Td + pid.N * pid.Ts)
153157
pid.bd = pid.K * pid.N * pid.ad
158+
nothing
154159
end
155160

156161

@@ -193,6 +198,7 @@ function reset_state!(pid::DiscretePID)
193198
pid.I = zero(pid.I)
194199
pid.D = zero(pid.D)
195200
pid.yold = zero(pid.yold)
201+
nothing
196202
end
197203

198204
end

test/runtests.jl

+5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ using DiscretePIDs
22
using Test
33
using ControlSystemsBase
44
using AllocCheck
5+
using JET
56

67
@testset "DiscretePIDs.jl" begin
78

@@ -95,6 +96,10 @@ reset_state!(pid)
9596
res3 = lsim(P, ctrl, Tf)
9697
@test res3.y == res2.y
9798

99+
@test_opt pid(1.0, 1.0)
100+
@test_opt pid(1.0, 1.0, 1.0)
101+
# @report_call pid(1.0, 1.0)
102+
98103
## Test with FixedPointNumbers
99104
using FixedPointNumbers
100105
T = Fixed{Int16, 10} # 16-bit signed fixed-point with 10 bits for the fractional part

0 commit comments

Comments
 (0)