From e0cd78007efc02c9c0bb1ae4edc7c71c275b2502 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Tue, 19 Nov 2024 15:37:14 +0100 Subject: [PATCH 1/6] add JuliaC example --- Project.toml | 3 +- README.md | 8 +++++ examples/juliac/Project.toml | 4 +++ examples/juliac/juliac_pid.jl | 50 ++++++++++++++++++++++++++++++ examples/juliac/test_juliac_pid.jl | 30 ++++++++++++++++++ src/DiscretePIDs.jl | 4 +++ test/runtests.jl | 5 +++ 7 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 examples/juliac/Project.toml create mode 100644 examples/juliac/juliac_pid.jl create mode 100644 examples/juliac/test_juliac_pid.jl diff --git a/Project.toml b/Project.toml index 45761e0..f3cabeb 100644 --- a/Project.toml +++ b/Project.toml @@ -13,7 +13,8 @@ julia = "1.7" AllocCheck = "9b6a8646-10ed-4001-bbdc-1d2f46dfbb1a" ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" FixedPointNumbers = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" +JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["AllocCheck", "Test", "ControlSystemsBase", "FixedPointNumbers"] +test = ["AllocCheck", "Test", "ControlSystemsBase", "FixedPointNumbers", "JET"] diff --git a/README.md b/README.md index bb7b38b..c60e661 100644 --- a/README.md +++ b/README.md @@ -195,6 +195,14 @@ plot([res, res_fp], plotu=true, lab=["Float64" "" string(T) ""]); ylabel!("u + d 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. +## Compilation using JuliaC +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: +```bash +julia +nightly --project /julia/contrib/juliac.jl --output-lib juliac_pid --trim=unsafe-warn --compile-ccallable juliac_pid.jl +``` +where `` 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. + + ## See also - [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. - [SymbolicControlSystems.jl](https://github.com/JuliaControl/SymbolicControlSystems.jl) For C-code generation of LTI systems. \ No newline at end of file diff --git a/examples/juliac/Project.toml b/examples/juliac/Project.toml new file mode 100644 index 0000000..ddecd7b --- /dev/null +++ b/examples/juliac/Project.toml @@ -0,0 +1,4 @@ +[deps] +ControlSystemsBase = "aaaaaaaa-a6ca-5380-bf3e-84a91bcd477e" +DiscretePIDs = "c1363496-6848-4723-8758-079b737f6baf" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" diff --git a/examples/juliac/juliac_pid.jl b/examples/juliac/juliac_pid.jl new file mode 100644 index 0000000..bb790ad --- /dev/null +++ b/examples/juliac/juliac_pid.jl @@ -0,0 +1,50 @@ +module JuliacPID +import DiscretePIDs +import DiscretePIDs: DiscretePID +import Base.@ccallable + +const T = Float64 # The numeric type used by the controller + +# Set the initial PID parameters here +const pid = DiscretePIDs.DiscretePID(; K = T(1), Ti = 1, Td = false, Ts = 1) + + +@ccallable function calculate_control!(r::T, y::T, uff::T)::T + DiscretePIDs.calculate_control!(pid, r, y, uff)::T +end + +function set_K!(K::T, r::T, y::T)::Cvoid + DiscretePIDs.set_K!(pid, K, r, y) + nothing +end + +function set_Ti!(Ti::T)::Cvoid + DiscretePIDs.set_Ti!(pid, Ti) + nothing +end + +function set_Td!(Td::T)::Cvoid + DiscretePIDs.set_Td!(pid, Td) + nothing +end + +@ccallable function reset_state!()::Cvoid + DiscretePIDs.reset_state!(pid) + nothing +end + +@ccallable function main()::Cint + println(Core.stdout, "I'm alive and well") + u = calculate_control!(0.0, 0.0, 0.0) + println(Core.stdout, u) + + Cint(0) +end + + +end + +# compile using something like +# cd(@__DIR__) +# run(`julia +nightly --project --experimental /home/fredrikb/repos/julia/contrib/juliac.jl --output-lib juliac_pid --trim=unsafe-warn --compile-ccallable juliac_pid.jl`) +# run(`ls -ltrh`) # marvel at the smallness of the binary diff --git a/examples/juliac/test_juliac_pid.jl b/examples/juliac/test_juliac_pid.jl new file mode 100644 index 0000000..b01483d --- /dev/null +++ b/examples/juliac/test_juliac_pid.jl @@ -0,0 +1,30 @@ +# Run this file with the same version of julia that you used to compile the shared library. +cd(@__DIR__) + +const T = Float64 +lib = Libc.Libdl.dlopen("/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so") +const calc = Libc.Libdl.dlsym(lib, :calculate_control!) + +function pid(r::T, y::T, uff::T) + ccall(calc, T, (T, T, T), r, y, uff) +end + +pid(0.0, 0.0, 0.0) # test + +using ControlSystemsBase, Plots +Tf = 15 # Simulation time +Ts = 0.01 # sample time + +P = c2d(ss(tf(1, [1, 1])), Ts) # Process to be controlled, discretized using zero-order hold + +ctrl = function(x,t) + y = (P.C*x)[] # measurement + d = 1 # disturbance + r = 0 # reference + u = pid(T(r), T(y), T(0)) + u + d # Plant input is control signal + disturbance +end + +res = lsim(P, ctrl, Tf) + +plot(res, plotu=true); ylabel!("u + d", sp=2) \ No newline at end of file diff --git a/src/DiscretePIDs.jl b/src/DiscretePIDs.jl index 06f9ff9..c3bfe6e 100644 --- a/src/DiscretePIDs.jl +++ b/src/DiscretePIDs.jl @@ -124,6 +124,7 @@ function set_K!(pid::DiscretePID, K, r, y) pid.bi = K * pid.Ts / pid.Ti pid.I = pid.I + Kold*(pid.b*r - y) - K*(pid.b*r - y) end + nothing end """ @@ -139,6 +140,7 @@ function set_Ti!(pid::DiscretePID{T}, Ti) where T else pid.bi = zero(T) end + nothing end """ @@ -151,6 +153,7 @@ function set_Td!(pid::DiscretePID, Td) pid.Td = Td pid.ad = Td / (Td + pid.N * pid.Ts) pid.bd = pid.K * pid.N * pid.ad + nothing end @@ -193,6 +196,7 @@ function reset_state!(pid::DiscretePID) pid.I = zero(pid.I) pid.D = zero(pid.D) pid.yold = zero(pid.yold) + nothing end end diff --git a/test/runtests.jl b/test/runtests.jl index 904bb45..9f0c5d5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ using DiscretePIDs using Test using ControlSystemsBase using AllocCheck +using JET @testset "DiscretePIDs.jl" begin @@ -95,6 +96,10 @@ reset_state!(pid) res3 = lsim(P, ctrl, Tf) @test res3.y == res2.y +@test_opt pid(1.0, 1.0) +@test_opt pid(1.0, 1.0, 1.0) +# @report_call pid(1.0, 1.0) + ## Test with FixedPointNumbers using FixedPointNumbers T = Fixed{Int16, 10} # 16-bit signed fixed-point with 10 bits for the fractional part From 4f3e47d4aa514c36d2fec679327e90696c3a5cab Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 27 Nov 2024 13:29:58 +0100 Subject: [PATCH 2/6] add c test program --- examples/juliac/juliac_pid.jl | 2 +- examples/juliac/test_juliac_pid.c | 48 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 examples/juliac/test_juliac_pid.c diff --git a/examples/juliac/juliac_pid.jl b/examples/juliac/juliac_pid.jl index bb790ad..abaecb2 100644 --- a/examples/juliac/juliac_pid.jl +++ b/examples/juliac/juliac_pid.jl @@ -46,5 +46,5 @@ end # compile using something like # cd(@__DIR__) -# run(`julia +nightly --project --experimental /home/fredrikb/repos/julia/contrib/juliac.jl --output-lib juliac_pid --trim=unsafe-warn --compile-ccallable juliac_pid.jl`) +# 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`) # run(`ls -ltrh`) # marvel at the smallness of the binary diff --git a/examples/juliac/test_juliac_pid.c b/examples/juliac/test_juliac_pid.c new file mode 100644 index 0000000..ad68cb3 --- /dev/null +++ b/examples/juliac/test_juliac_pid.c @@ -0,0 +1,48 @@ +#include +#include +#include +#include + +// Define the type of the function calculate_control +typedef double (*calculate_control_t)(double r, double y, double uff); + +int main() { + jl_init(); + + const char *lib_path = "/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so"; + + // Load the shared library + printf("Loading juliac_pid.so\n"); + void *lib_handle = dlopen(lib_path, RTLD_LAZY); + if (!lib_handle) { + fprintf(stderr, "Error: Unable to load library %s\n", dlerror()); + exit(EXIT_FAILURE); + } + printf("Loaded juliac_pid.so\n"); + + // Locate the calculate_control function + printf("Finding calculate_control!\n"); + calculate_control_t calculate_control = (calculate_control_t)dlsym(lib_handle, "calculate_control!"); + char *error = dlerror(); + if (error != NULL) { + fprintf(stderr, "Error: Unable to find symbol calculate_control!: %s\n", error); + dlclose(lib_handle); + exit(EXIT_FAILURE); + } + printf("Found calculate_control!\n"); + + // Call the function + double r = 0.0, y = 0.0, uff = 0.0; + double result = calculate_control(r, y, uff); + printf("calculate_control! returned: %f\n", result); + + // Close the library + dlclose(lib_handle); + + jl_atexit_hook(0); + return 0; +} + + +// export LD_LIBRARY_PATH=/home/fredrikb/repos/julia/usr/lib:$LD_LIBRARY_PATH +// 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 From b2f2d85968f9616273f08993b632c2e5591ccfdb Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Tue, 3 Dec 2024 13:54:27 +0100 Subject: [PATCH 3/6] make wrappers `2ccallable` again --- examples/juliac/juliac_pid.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/juliac/juliac_pid.jl b/examples/juliac/juliac_pid.jl index abaecb2..8909a7e 100644 --- a/examples/juliac/juliac_pid.jl +++ b/examples/juliac/juliac_pid.jl @@ -13,17 +13,17 @@ const pid = DiscretePIDs.DiscretePID(; K = T(1), Ti = 1, Td = false, Ts = 1) DiscretePIDs.calculate_control!(pid, r, y, uff)::T end -function set_K!(K::T, r::T, y::T)::Cvoid +@ccallable function set_K!(K::T, r::T, y::T)::Cvoid DiscretePIDs.set_K!(pid, K, r, y) nothing end -function set_Ti!(Ti::T)::Cvoid +@ccallable function set_Ti!(Ti::T)::Cvoid DiscretePIDs.set_Ti!(pid, Ti) nothing end -function set_Td!(Td::T)::Cvoid +@ccallable function set_Td!(Td::T)::Cvoid DiscretePIDs.set_Td!(pid, Td) nothing end @@ -33,13 +33,13 @@ end nothing end -@ccallable function main()::Cint - println(Core.stdout, "I'm alive and well") - u = calculate_control!(0.0, 0.0, 0.0) - println(Core.stdout, u) +# @ccallable function main()::Cint +# println(Core.stdout, "I'm alive and well") +# u = calculate_control!(0.0, 0.0, 0.0) +# println(Core.stdout, u) - Cint(0) -end +# Cint(0) +# end end From fd61f71662fcd4fb211ec4c4395ae6ddf80d8e44 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 4 Dec 2024 10:43:10 +0100 Subject: [PATCH 4/6] juliac demo working! Co-authored-by: Cody Tapscott --- examples/juliac/juliac_pid.jl | 4 +- examples/juliac/test_juliac_pid.c | 64 +++++++++++++++++++++--------- examples/juliac/test_juliac_pid.jl | 3 ++ src/DiscretePIDs.jl | 4 +- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/examples/juliac/juliac_pid.jl b/examples/juliac/juliac_pid.jl index 8909a7e..c8adaa9 100644 --- a/examples/juliac/juliac_pid.jl +++ b/examples/juliac/juliac_pid.jl @@ -44,7 +44,7 @@ end end -# compile using something like +# compile using something like this, modified to suit your local paths # cd(@__DIR__) # 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`) -# run(`ls -ltrh`) # marvel at the smallness of the binary +# run(`ls -ltrh`) diff --git a/examples/juliac/test_juliac_pid.c b/examples/juliac/test_juliac_pid.c index ad68cb3..17b6322 100644 --- a/examples/juliac/test_juliac_pid.c +++ b/examples/juliac/test_juliac_pid.c @@ -1,48 +1,74 @@ #include #include #include -#include +// #include -// Define the type of the function calculate_control +// Path to julia binary folder +#define JULIA_PATH "/home/fredrikb/repos/julia/usr/bin/" // NOTE: modify this path + +// Path to juliac compiled shared object file +#define LIB_PATH "/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so" // NOTE: modify this path + + + +// Define the types of the julia @ccallable functions +typedef void (*jl_init_with_image_t)(const char *bindir, const char *sysimage); typedef double (*calculate_control_t)(double r, double y, double uff); +typedef void (*set_K_t)(double K, double r, double y); +typedef void (*set_Ti_t)(double Ti); +typedef void (*set_Td_t)(double Td); +typedef void (*reset_state_t)(); -int main() { - jl_init(); - const char *lib_path = "/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so"; +int main() { // Load the shared library printf("Loading juliac_pid.so\n"); - void *lib_handle = dlopen(lib_path, RTLD_LAZY); + void *lib_handle = dlopen(LIB_PATH, RTLD_LAZY); if (!lib_handle) { fprintf(stderr, "Error: Unable to load library %s\n", dlerror()); exit(EXIT_FAILURE); } printf("Loaded juliac_pid.so\n"); - // Locate the calculate_control function - printf("Finding calculate_control!\n"); + // Locate the julia functions function + printf("Finding symbols\n"); + jl_init_with_image_t jl_init_with_image = (jl_init_with_image_t)dlsym(lib_handle, "jl_init_with_image"); + calculate_control_t calculate_control = (calculate_control_t)dlsym(lib_handle, "calculate_control!"); - char *error = dlerror(); - if (error != NULL) { - fprintf(stderr, "Error: Unable to find symbol calculate_control!: %s\n", error); - dlclose(lib_handle); + set_K_t set_K = (set_K_t)dlsym(lib_handle, "set_K!"); + set_Ti_t set_Ti = (set_Ti_t)dlsym(lib_handle, "set_Ti!"); + set_Td_t set_Td = (set_Td_t)dlsym(lib_handle, "set_Td!"); + reset_state_t reset_state = (reset_state_t)dlsym(lib_handle, "reset_state!"); + + + if (jl_init_with_image == NULL || calculate_control == NULL) { + char *error = dlerror(); + fprintf(stderr, "Error: Unable to find symbol: %s\n", error); exit(EXIT_FAILURE); } - printf("Found calculate_control!\n"); + printf("Found all symbols!\n"); + + // Init julia + jl_init_with_image(JULIA_PATH, LIB_PATH); - // Call the function - double r = 0.0, y = 0.0, uff = 0.0; + // Trivial test program that computes a few control outputs and modifies K + double r = 1.0, y = 0.0, uff = 0.0; double result = calculate_control(r, y, uff); printf("calculate_control! returned: %f\n", result); + result = calculate_control(r, y, uff); + printf("calculate_control! returned: %f\n", result); + set_K(0.0, r, y); + for (int i = 0; i < 3; ++i) { + result = calculate_control(r, y, uff); + printf("calculate_control! returned: %f\n", result); + } - // Close the library - dlclose(lib_handle); - - jl_atexit_hook(0); + // jl_atexit_hook(0); return 0; } +// Compile this C program using a command like the one above, modified to suit your paths // export LD_LIBRARY_PATH=/home/fredrikb/repos/julia/usr/lib:$LD_LIBRARY_PATH // 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 diff --git a/examples/juliac/test_juliac_pid.jl b/examples/juliac/test_juliac_pid.jl index b01483d..36845ab 100644 --- a/examples/juliac/test_juliac_pid.jl +++ b/examples/juliac/test_juliac_pid.jl @@ -2,8 +2,11 @@ cd(@__DIR__) const T = Float64 +@info("Loading juliac_pid.so") lib = Libc.Libdl.dlopen("/home/fredrikb/.julia/dev/DiscretePIDs/examples/juliac/juliac_pid.so") +@info("Loaded juliac_pid.so, finding calculate_control!") const calc = Libc.Libdl.dlsym(lib, :calculate_control!) +@info("Found calculate_control!") function pid(r::T, y::T, uff::T) ccall(calc, T, (T, T, T), r, y, uff) diff --git a/src/DiscretePIDs.jl b/src/DiscretePIDs.jl index c3bfe6e..14bf60f 100644 --- a/src/DiscretePIDs.jl +++ b/src/DiscretePIDs.jl @@ -71,7 +71,7 @@ u = calculate_control!(pid, r, y, uff) # Equivalent to the above - `D`: Derivative part - `yold`: Last measurement signal -See also [`calculate_control!`](@ref), [`set_K!`](@ref), [`set_Ti!`](@ref), [`set_Td!`](@ref) +See also [`calculate_control!`](@ref), [`set_K!`](@ref), [`set_Ti!`](@ref), [`set_Td!`](@ref), [`reset_state!`](@ref). """ function DiscretePID(; K::T = 1f0, @@ -115,6 +115,8 @@ end set_K!(pid::DiscretePID, K, r, y) 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`. + +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)`. """ function set_K!(pid::DiscretePID, K, r, y) Kold = pid.K From 011fd6185d4fcf3d9e97bdd8c1e40c3c51ef98d2 Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 4 Dec 2024 10:47:12 +0100 Subject: [PATCH 5/6] update juliac readme instrucitons --- README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c60e661..2b5ee41 100644 --- a/README.md +++ b/README.md @@ -198,9 +198,31 @@ The fixed-point controller behaves roughly the same in this case, but artifacts ## Compilation using JuliaC 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: ```bash -julia +nightly --project /julia/contrib/juliac.jl --output-lib juliac_pid --trim=unsafe-warn --compile-ccallable juliac_pid.jl +julia +nightly --project /julia/contrib/juliac.jl --output-lib juliac_pid --experimental --trim=unsafe-warn --compile-ccallable juliac_pid.jl ``` -where `` 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. +where `` 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 +```bash +export LD_LIBRARY_PATH=/julia/usr/lib:$LD_LIBRARY_PATH +gcc -o pid_program test_juliac_pid.c -I /julia/usr/include/julia -L/julia/usr/lib -ljulia -ldl +``` +and then run by +```bash +./pid_program +``` +which should produce the output +``` +DiscretePIDs/examples/juliac> ./pid_program +Loading juliac_pid.so +Loaded juliac_pid.so +Finding symbols +Found all symbols! +calculate_control! returned: 1.000000 +calculate_control! returned: 2.000000 +calculate_control! returned: 3.000000 +calculate_control! returned: 3.000000 +calculate_control! returned: 3.000000 +``` +At the time of writing, this requires a nightly version of julia ## See also From 7288696eb29444f2a1db5698e9974c2cd12f8b6a Mon Sep 17 00:00:00 2001 From: Fredrik Bagge Carlson Date: Wed, 4 Dec 2024 10:50:50 +0100 Subject: [PATCH 6/6] add note about julia from julia --- examples/juliac/test_juliac_pid.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/juliac/test_juliac_pid.jl b/examples/juliac/test_juliac_pid.jl index 36845ab..b07bff0 100644 --- a/examples/juliac/test_juliac_pid.jl +++ b/examples/juliac/test_juliac_pid.jl @@ -1,4 +1,5 @@ -# Run this file with the same version of julia that you used to compile the shared library. +# NOTE: it is currently not possible to call a julia-produced shared-library from julia. +# To test the compiled shared library, see test_juliac_pid.c instead. cd(@__DIR__) const T = Float64