统一的pid_update
我们希望用户可以通过 pid_new_with_*
函数创建包含想要特性的 PID
,
不管对于任意的 PID
都可以通过统一的 pid_update
函数更新 PID
。
struct Pid pid = pid_new(1, 0, 0);
// do something
while(1) pid_update(&pid, 0, 0, 0.1);
当用户想试一试其他的特性时,只需要将 pid_new*
函数替换为其他函数即可。
struct Pid pid = pid_new_with_integral_clamp(1, 0, 0, -10, 10);
// do something
while(1) pid_update(&pid, 0, 0, 0.1);
同时开发者想要添加一种新的特性时,只需要在 features.h
中添加新的 pid_new*
函数声明即可。
实现细节
float pid_update(struct Pid *pid, float target, float actual, float dt) {
float error = target - actual;
return pid->update(pid, error, dt);
}
pid_update
函数计算 error = target - actual
,然后调用 pid->update
函数指针指向的函数更新 PID
。
而不同的 pid_new*
函数,将设置 pid->update
函数指针指向的不同的函数。
结构细节
struct Pid {
float kp, ki, kd;
float previous;
float integral;
float (*update)(struct Pid *pid, float error, float dt);
union PidFeaturesOption option;
};
这是定义在 pid.h
中的 Pid
结构体,其中 update
字段是一个指向函数的指针。
可以如此理解 update
的类型声明
float (*update
)(struct Pid *pid, float error, float dt);
先声明了 update
是一个指针
float
(*update)(struct Pid *pid, float error, float dt)
;
然后声明了 *update
的类型为一个接受三个参数 struct Pid *, float, float
返回一个 float
的函数
所以 update
是一个指向函数的指针
也可以使用 typedef
来帮助理解
typedef float update_fn(struct Pid *pid, float error, float dt); // update_fn 是一个函数类型
update_fn *update; // update 是一个指针,指向的类型为 `update_fn`
在每次初始化的时候,设置 pid.update
函数指针指向的更新的函数
float pid_update_with_xxx(struct Pid *pid, float error, float dt) {
// do something
}
struct Pid pid_new_with_xxx(float kp, float ki, float kd) {
struct Pid pid = pid_new(kp, ki, kd);
pid.update = pid_update_with_xxx;
return pid;
}
这样每次调用 pid_update
就会调用 pid_update_with_xxx
函数,实现了类似于动态分发的效果。
虽然这样实现有可能丧失一些编译器优化机会,但带来的好处是可以轻松地添加新的特性。
而且,也可以通过更换函数指针实现第一次运行和后续更新不一致的情况
float pid_update_with_xxx(struct Pid *pid, float error, float dt) {
float differential = (error - pid->previous) / dt;
float average = (error + pid->previous) / 2.0;
pid->integral += average * dt;
pid->previous = error;
return pid_weighted_sum(pid, error, pid->integral, differential);
}
float pid_update_with_xxx_is_first(struct Pid *pid, float error, float dt) {
pid->previous = error; // 确保第一次微分为 0
pid->update = pid_update_with_xxx;
return pid_update_with_xxx(pid, error, dt);
}
struct Pid pid_new_with_xxx(float kp, float ki, float kd) {
struct Pid pid = pid_new(kp, ki, kd);
pid.update = pid_update_with_xxx_is_first;
return pid;
}
但是不同的 PID
还要存储一些其他的参数,比方说积分限幅,需要保存额外的限幅范围。
通过在 struct Pid
中增加一个 union PidFeaturesOption option
来实现
union PidFeaturesOption {
#ifdef PID_FEATURE_INTEGRAL_DECAY
float integral_decay_factor;
#endif // PID_FEATURE_INTEGRAL_DECAY
#ifdef PID_FEATURE_INTEGRAL_CLAMP
struct BoundRange integral_clamp_bound;
#endif // PID_FEATURE_INTEGRAL_CLAMP
#ifdef PID_FEATURE_INTEGRAL_SEPARATION
struct BoundRange integral_separation_error_threshold;
#endif // PID_FEATURE_INTEGRAL_SEPARATION
#ifdef PID_FEATURE_INTEGRAL_SLIDING_WINDOW
struct IntegralSlidingWindow integral_sliding_window;
#endif // PID_FEATURE_INTEGRAL_SLIDING_WINDOW
};
union
联合,同时只能有一个成员被使用,其大小是最大的那个成员的大小。
相当于说明需要一块区域(最大成员大小),然后以某个字段的类型来访问这块区域。
TIP
目前 union PidFeaturesOption option
字段的大小为 float * 2
为 8
个字节,
如果某个实现需要更大的 option
, 建议设置为默认不启用以减少不必要的空间浪费,
如:默认关闭的 PID_FEATURE_INTEGRAL_SLIDING_WINDOW
pid_new_with_xxx
将额外的数据保存在 option
中,
创建的 pid_update_with_xxx
函数知道如何使用对应的 option
。
features.h
1#if defined(PID_FEATURE_INTEGRAL_CLAMP) || \
2 defined(PID_FEATURE_INTEGRAL_SEPARATION)
3struct BoundRange {
4 float min;
5 float max;
6};
7#endif // PID_FEATURE_INTEGRAL_CLAMP || PID_FEATURE_INTEGRAL_SEPARATION
8
9union PidFeaturesOption {
10/// ...
11#ifdef PID_FEATURE_INTEGRAL_CLAMP
12 struct BoundRange integral_clamp_bound;
13#endif // PID_FEATURE_INTEGRAL_CLAMP
14/// ...
15}
features/integral-clamp.c
1#define PID_FEATURE_INTEGRAL_CLAMP
2
3#include "../pid.h"
4#include "../utils/generate-first-update.h"
5#include "../utils/num-limit-macro.h"
6
7float pid_update_with_integral_clamp(struct Pid *pid, float error, float dt) {
8 float differential = (error - pid->previous) / dt;
9
10 float average = (error + pid->previous) / 2.0;
11 pid->integral += average * dt;
12
13 // clamp integral
14 struct BoundRange clamp = pid->option.integral_clamp_bound;
15 pid->integral = LIMIT(pid->integral, clamp.min, clamp.max);
16
17 pid->previous = error;
18
19 return pid_weighted_sum(pid, error, pid->integral, differential);
20}
21
22PID_UPDATE_GENERATE_IS_FIRST(integral_clamp)
23
24struct Pid pid_new_with_integral_clamp(float kp, float ki, float kd,
25 float integral_clamp_bound_min,
26 float integral_clamp_bound_max) {
27 struct Pid pid = pid_new(kp, ki, kd);
28
29 struct BoundRange integral_clamp_bound = {
30 .min = integral_clamp_bound_min,
31 .max = integral_clamp_bound_max,
32 };
33 pid.option.integral_clamp_bound = integral_clamp_bound;
34
35 pid.update = pid_update_with_integral_clamp_is_first;
36
37 return pid;
38}