libacfutils
A general purpose library of utility functions designed to make it easier to develop addons for the X-Plane flight simulator.
Loading...
Searching...
No Matches
pid_ctl.h
1/*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License, Version 1.0 only
6 * (the "License"). You may not use this file except in compliance
7 * with the License.
8 *
9 * You can obtain a copy of the license in the file COPYING
10 * or http://www.opensource.org/licenses/CDDL-1.0.
11 * See the License for the specific language governing permissions
12 * and limitations under the License.
13 *
14 * When distributing Covered Code, include this CDDL HEADER in each
15 * file and include the License file COPYING.
16 * If applicable, add the following below this CDDL HEADER, with the
17 * fields enclosed by brackets "[]" replaced with your own identifying
18 * information: Portions Copyright [yyyy] [name of copyright owner]
19 *
20 * CDDL HEADER END
21 */
22/*
23 * Copyright 2023 Saso Kiselkov. All rights reserved.
24 */
25
26#ifndef _ACFUTILS_PID_CTL_H_
27#define _ACFUTILS_PID_CTL_H_
28
29#include <math.h>
30#include <stdio.h>
31
32#include "sysmacros.h"
33#include "math.h"
34
35#ifdef __cplusplus
36extern "C" {
37#endif
38
39/*
40 * Generic proportional-integral-derivative (PID) controller implementation.
41 * PID controllers are useful tools for aircraft control problems such as
42 * autopilot control of flight control surfaces. See Wikipedia for more info.
43 *
44 * Initialize the controller with pid_ctl_init. Update the controller with
45 * a new error value using pid_ctl_update. Read controller outputs via
46 * pid_ctl_get. See respective functions for more info.
47 */
48
49typedef struct {
50 double e_prev; /* current error value */
51 double V_prev; /* previous actual value */
52 double integ; /* integrated value */
53 double deriv; /* derivative value */
54
55 double k_p_gain;
56 double k_p; /* proportional coefficient */
57 double k_i_gain;
58 double k_i; /* integral coefficient */
59 double lim_i;
60 double k_d; /* derivative coefficient */
61 double k_d_gain;
62 double r_d; /* derivative update rate */
63
64 bool_t integ_clamp;
65} pid_ctl_t;
66
67static inline void pid_ctl_reset(pid_ctl_t *pid);
68static inline void pid_ctl_set_k_p(pid_ctl_t *pid, double k_p);
69static inline void pid_ctl_set_k_i(pid_ctl_t *pid, double k_i);
70static inline void pid_ctl_set_lim_i(pid_ctl_t *pid, double lim_i);
71static inline void pid_ctl_set_k_d(pid_ctl_t *pid, double k_d);
72static inline void pid_ctl_set_r_d(pid_ctl_t *pid, double r_d);
73
74/*
75 * Initializes a PID controller.
76 *
77 * @param pid PID controller structure to be initialized.
78 * @param k_p Proportional coefficient (multiplier of how much the
79 * proportional input contributes to the output)..
80 * @param k_i Integral coefficient (multiplier of how much the integral
81 * input contributes to the output).
82 * @param lim_i Integration limit value of the controller. The integrated
83 * error value will be clamped to (-lim_i,+lim_i) inclusive. If you
84 * want your PID controller to be unclamped in the integration error
85 * value, call pid_ctl_set_integ_clamp with B_FALSE after init.
86 * @param k_d Derivative coefficient (multiplier of how much the derivative
87 * input contributes to the output).
88 * @param r_d Rate at which we update the derivative to the current rate
89 * value. This a FILTER_IN rate argument. Roughly what is expresses
90 * is how quickly the derivative approaches the new delta-error value
91 * per unit time. The higher the value, the slower the derivative
92 * approaches the current delta-error value.
93 */
94static inline void
95pid_ctl_init_noreset(pid_ctl_t *pid, double k_p, double k_i, double lim_i,
96 double k_d, double r_d)
97{
98 ASSERT(pid != NULL);
99 pid->k_p = k_p;
100 pid->k_p_gain = 1;
101 pid->k_i = k_i;
102 pid->k_i_gain = 1;
103 pid->lim_i = lim_i;
104 pid->k_d = k_d;
105 pid->k_d_gain = 1;
106 pid->r_d = r_d;
107 pid->integ_clamp = B_TRUE;
108}
109static inline void
110pid_ctl_init(pid_ctl_t *pid, double k_p, double k_i, double lim_i, double k_d,
111 double r_d)
112{
113 ASSERT(pid != NULL);
114 pid_ctl_init_noreset(pid, k_p, k_i, lim_i, k_d, r_d);
115 pid_ctl_reset(pid);
116}
117
118static inline void
119pid_ctl_set_integ_clamp(pid_ctl_t *pid, bool_t flag)
120{
121 ASSERT(pid != NULL);
122 pid->integ_clamp = flag;
123}
124
125/*
126 * Updates the PID controller with a new error value and a new "current"
127 * value. The error is used to calculate the proportional and integral
128 * response, whereas the current value is used to calculate the derivate
129 * response. Passing a separate current value is typically used to avoid
130 * derivate response kick when the system's set point is changed.
131 *
132 * @param e New error value with which to update the PID controller's
133 * proportional and integral responses.
134 * @param V New "current" process value which is used to update the
135 * controller's derivate response.
136 * @param d_t Delta-time elapsed since last update (arbitrary units,
137 * but usually seconds). This is used to control the rate at which
138 * the integral and derivative values are updated.
139 */
140static inline void
141pid_ctl_update_dV(pid_ctl_t *pid, double e, double V, double d_t)
142{
143 double delta_V;
144
145 ASSERT(pid != NULL);
146
147 delta_V = (V - pid->V_prev) / d_t;
148 if (isnan(pid->integ))
149 pid->integ = 0;
150 pid->integ = clamp(pid->integ + e * d_t, -pid->lim_i, pid->lim_i);
151 /*
152 * Clamp the integrated value to the current proportional value. This
153 * prevents excessive over-correcting when the value returns to center.
154 */
155 if (pid->integ_clamp) {
156 if (e < 0)
157 pid->integ = MAX(pid->integ, e);
158 else
159 pid->integ = MIN(pid->integ, e);
160 }
161 if (!isnan(delta_V))
162 FILTER_IN_NAN(pid->deriv, delta_V, d_t, pid->r_d);
163 pid->e_prev = e;
164 pid->V_prev = V;
165}
166
167/*
168 * Same as pid_ctl_update_dV, but passes the error value as the current
169 * value as well, which means all three responses from the PID controller
170 * are based only on the error value.
171 */
172static inline void
173pid_ctl_update(pid_ctl_t *pid, double e, double d_t)
174{
175 pid_ctl_update_dV(pid, e, e, d_t);
176}
177
178/*
179 * Reads the current output of a PID controller. You should call this
180 * after calling pid_ctl_update with a new value for the current
181 * simulator frame. Please note that the first call to a freshly
182 * initialized PID controller, or one that was reset by passing a NAN
183 * error value in pid_ctl_update, this function will return NAN. That's
184 * because the PID controller needs at least two update calls to
185 * establish value trends. So be prepared to test for (via isnan())
186 * and reject a NAN value from the PID controller.
187 */
188static inline double
189pid_ctl_get(const pid_ctl_t *pid)
190{
191 ASSERT(pid != NULL);
192 ASSERT(!isnan(pid->e_prev));
193 return (pid->k_p_gain * pid->k_p * pid->e_prev +
194 pid->k_i_gain * pid->k_i * pid->integ +
195 pid->k_d_gain * pid->k_d * pid->deriv);
196}
197
198/*
199 * Sets a PID controller to its initial "reset" state. After this, you
200 * must call pid_ctl_update at least twice before the PID controller
201 * starts returning non-NAN values from pid_ctl_get.
202 */
203static inline void
204pid_ctl_reset(pid_ctl_t *pid)
205{
206 ASSERT(pid != NULL);
207 pid->e_prev = NAN;
208 pid->V_prev = NAN;
209 pid->integ = NAN;
210 pid->deriv = NAN;
211}
212
213/*
214 * Sets the PID controller's proportional coefficient. Use this to
215 * dynamic reconfigure the PID controller after initializing it.
216 */
217static inline void
218pid_ctl_set_k_p(pid_ctl_t *pid, double k_p)
219{
220 ASSERT(pid != NULL);
221 pid->k_p = k_p;
222}
223
224static inline double
225pid_ctl_get_k_p(const pid_ctl_t *pid)
226{
227 ASSERT(pid != NULL);
228 return (pid->k_p);
229}
230
231static inline void
232pid_ctl_set_k_p_gain(pid_ctl_t *pid, double k_p_gain)
233{
234 ASSERT(pid != NULL);
235 pid->k_p_gain = k_p_gain;
236}
237
238static inline double
239pid_ctl_get_k_p_gain(const pid_ctl_t *pid)
240{
241 ASSERT(pid != NULL);
242 return (pid->k_p_gain);
243}
244
245/*
246 * Sets the PID controller's integral coefficient. Use this to
247 * dynamic reconfigure the PID controller after initializing it.
248 */
249static inline void
250pid_ctl_set_k_i(pid_ctl_t *pid, double k_i)
251{
252 ASSERT(pid != NULL);
253 pid->k_i = k_i;
254}
255
256static inline double
257pid_ctl_get_k_i(const pid_ctl_t *pid)
258{
259 ASSERT(pid != NULL);
260 return (pid->k_i);
261}
262
263static inline void
264pid_ctl_set_k_i_gain(pid_ctl_t *pid, double k_i_gain)
265{
266 ASSERT(pid != NULL);
267 pid->k_i_gain = k_i_gain;
268}
269
270static inline double
271pid_ctl_get_k_i_gain(const pid_ctl_t *pid)
272{
273 ASSERT(pid != NULL);
274 return (pid->k_i_gain);
275}
276
277/*
278 * Sets the PID controller's integration error value limit. Use
279 * pid_ctl_set_integ_clamp to disable integration error clamping.
280 */
281static inline void
282pid_ctl_set_lim_i(pid_ctl_t *pid, double lim_i)
283{
284 ASSERT(pid != NULL);
285 pid->lim_i = lim_i;
286}
287
288static inline double
289pid_ctl_get_lim_i(const pid_ctl_t *pid)
290{
291 ASSERT(pid != NULL);
292 return (pid->lim_i);
293}
294
295/*
296 * Sets the PID controller's integral coefficient. Use this to
297 * dynamic reconfigure the PID controller after initializing it.
298 */
299static inline void
300pid_ctl_set_k_d(pid_ctl_t *pid, double k_d)
301{
302 ASSERT(pid != NULL);
303 pid->k_d = k_d;
304}
305
306static inline double
307pid_ctl_get_k_d(const pid_ctl_t *pid)
308{
309 ASSERT(pid != NULL);
310 return (pid->k_d);
311}
312
313static inline void
314pid_ctl_set_k_d_gain(pid_ctl_t *pid, double k_d_gain)
315{
316 ASSERT(pid != NULL);
317 pid->k_d_gain = k_d_gain;
318}
319
320static inline double
321pid_ctl_get_k_d_gain(const pid_ctl_t *pid)
322{
323 ASSERT(pid != NULL);
324 return (pid->k_d_gain);
325}
326
327static inline void
328pid_ctl_set_r_d(pid_ctl_t *pid, double r_d)
329{
330 ASSERT(pid != NULL);
331 pid->r_d = r_d;
332}
333
334static inline double
335pid_ctl_get_r_d(const pid_ctl_t *pid)
336{
337 ASSERT(pid != NULL);
338 return (pid->r_d);
339}
340
341static inline void
342pid_ctl_set_integ(pid_ctl_t *pid, double integ)
343{
344 ASSERT(pid != NULL);
345 pid->integ = integ;
346}
347
348static inline double
349pid_ctl_get_integ(const pid_ctl_t *pid)
350{
351 ASSERT(pid != NULL);
352 return (pid->integ);
353}
354
355static inline void
356pid_ctl_set_deriv(pid_ctl_t *pid, double deriv)
357{
358 ASSERT(pid != NULL);
359 pid->deriv = deriv;
360}
361
362static inline double
363pid_ctl_get_deriv(const pid_ctl_t *pid)
364{
365 ASSERT(pid != NULL);
366 return (pid->deriv);
367}
368
369/*
370 * Sets all 3 gain values in one call.
371 */
372static inline void
373pid_ctl_set_gain(pid_ctl_t *pid, double gain)
374{
375 pid_ctl_set_k_p_gain(pid, gain);
376 pid_ctl_set_k_d_gain(pid, gain);
377 pid_ctl_set_k_i_gain(pid, gain);
378}
379
380#define PID_CTL_DEBUG(pid_ptr) \
381 do { \
382 const pid_ctl_t *pid = (pid_ptr); \
383 printf(#pid_ptr ": e: %f integ: %f deriv: %f\n", \
384 pid->e_prev, pid->integ, pid->deriv); \
385 } while (0)
386
387#ifdef __cplusplus
388}
389#endif
390
391#endif /* _ACFUTILS_PID_CTL_H_ */
#define ASSERT(x)
Definition assert.h:208
static double clamp(double x, double min_val, double max_val)
Definition math_core.h:60