Skip to content

Commit 0ebf103

Browse files
committed
pbio/control: Use reduced kp gain at low speed.
Reduces stutter on motors with freely moving encoders in the shafts. See pybricks/support#366
1 parent 756a9d4 commit 0ebf103

File tree

4 files changed

+77
-4
lines changed

4 files changed

+77
-4
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
getter for this deadzone.
2222
- Fixed type checking optimized out on Move hub ([support#950]).
2323
- Fixed end-user stall flag coming up too early in position based control.
24+
- Further reduced stutter at low motor speeds ([support#366]).
2425

26+
[support#366]: https://github.com/pybricks/support/issues/366
2527
[support#829]: https://github.com/pybricks/support/issues/829
2628
[support#863]: https://github.com/pybricks/support/issues/863
2729
[support#904]: https://github.com/pybricks/support/issues/904

lib/pbio/include/pbio/control_settings.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,18 @@ typedef struct _pbio_control_settings_t {
8282
* Position error feedback constant.
8383
*/
8484
int32_t pid_kp;
85+
/**
86+
* Percentage of pid_kp to use for small errors at low speeds.
87+
*/
88+
int32_t pid_kp_low_pct;
89+
/**
90+
* Threshold error angle below which to use lower kp constant at low speeds.
91+
*/
92+
int32_t pid_kp_low_error_threshold;
93+
/**
94+
* Threshold speed below which to use the lower kp constant.
95+
*/
96+
int32_t pid_kp_low_speed_threshold;
8597
/**
8698
* Accumulated position error feedback constant.
8799
*/

lib/pbio/src/control.c

Lines changed: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,57 @@ static bool pbio_control_status_test(pbio_control_t *ctl, pbio_control_status_fl
7171
return ctl->status & flag;
7272
}
7373

74+
static int32_t pbio_control_get_pid_kp(pbio_control_settings_t *settings, int32_t position_error, int32_t target_error, int32_t abs_command_speed) {
75+
76+
// Reduced kp values are only needed for some motors under slow speed
77+
// conditions. For everything else, use the default kp value.
78+
if (abs_command_speed > settings->pid_kp_low_speed_threshold || position_error == 0) {
79+
return settings->pid_kp;
80+
}
81+
82+
// We only need a positive kp value, so work with absolute values
83+
// throughout this function to make things easy to follow.
84+
position_error = pbio_int_math_abs(position_error);
85+
target_error = pbio_int_math_abs(target_error);
86+
87+
// Lowest kp value, used when steadily turning at slow speed.
88+
const int32_t kp_low = settings->pid_kp * settings->pid_kp_low_pct / 100;
89+
90+
// Get equivalent kp value to produce a piece-wise affine (pwa) feedback in the
91+
// position error. It grows slower at first, and then at the configured rate.
92+
const int32_t kp_pwa = position_error <= settings->pid_kp_low_error_threshold ?
93+
// For small errors, feedback is linear in low kp value.
94+
kp_low :
95+
// Above the threshold, feedback grows with the default gain.
96+
settings->pid_kp - settings->pid_kp_low_error_threshold * (settings->pid_kp - kp_low) / position_error;
97+
98+
// Proportional control saturates where the error leads to maximum actuation.
99+
// For errors any smaller than that,
100+
const int32_t saturation_lower = pbio_control_settings_div_by_gain(settings->actuation_max, settings->pid_kp);
101+
102+
// Further away from the target, we can use the reduced value and still
103+
// guarantee maximum actuation, to avoid getting stuck.
104+
const int32_t saturation_upper = saturation_lower * 100 / settings->pid_kp_low_pct;
105+
106+
// Get the kp value as constrained by the condition to use maxium actuation
107+
// close to the target to guarantee that we can always get there.
108+
int32_t kp_target;
109+
if (target_error < saturation_lower) {
110+
kp_target = settings->pid_kp;
111+
} else if (target_error > saturation_upper) {
112+
kp_target = kp_low;
113+
} else {
114+
// In between, we gradually shift towards the higher value as we get
115+
// closer to the final target to avoid a sudden transition.
116+
kp_target = kp_low + settings->pid_kp *
117+
(100 - settings->pid_kp_low_pct) * (saturation_upper - target_error) /
118+
(saturation_upper - saturation_lower) / 100;
119+
}
120+
121+
// The most constrained objective is obtained by taking the highest value.
122+
return pbio_int_math_max(kp_pwa, kp_target);
123+
}
124+
74125
/**
75126
* Updates the PID controller state to calculate the next actuation step.
76127
*
@@ -98,23 +149,26 @@ void pbio_control_update(pbio_control_t *ctl, uint32_t time_now, pbio_control_st
98149
// Calculate integral control errors, depending on control type.
99150
int32_t integral_error;
100151
int32_t position_error_used;
152+
int32_t target_error;
101153
if (pbio_control_type_is_position(ctl)) {
102-
154+
// Get error to final position, used below to adjust controls near end.
155+
target_error = pbio_angle_diff_mdeg(&ref_end.position, &state->position);
103156
// Update count integral error and get current error state
104-
int32_t target_error = pbio_angle_diff_mdeg(&ref_end.position, &state->position);
105157
integral_error = pbio_position_integrator_update(&ctl->position_integrator, position_error, target_error);
106-
107158
// For position control, the proportional term is the real position error.
108159
position_error_used = position_error;
109160
} else {
110161
// For time/speed based commands, the main error is speed. It integrates into a quantity with unit of position.
111162
// There is no count integral control, because we do not need a second order integrator for speed control.
112163
position_error_used = pbio_speed_integrator_get_error(&ctl->speed_integrator, position_error);
113164
integral_error = 0;
165+
// Speed maneuvers don't have a specific angle end target, so the error is infinite.
166+
target_error = INT32_MAX;
114167
}
115168

116169
// Corresponding PID control signal
117-
int32_t torque_proportional = pbio_control_settings_mul_by_gain(position_error_used, ctl->settings.pid_kp);
170+
int32_t pid_kp = pbio_control_get_pid_kp(&ctl->settings, position_error, target_error, pbio_trajectory_get_abs_command_speed(&ctl->trajectory));
171+
int32_t torque_proportional = pbio_control_settings_mul_by_gain(position_error_used, pid_kp);
118172
int32_t torque_derivative = pbio_control_settings_mul_by_gain(speed_error, ctl->settings.pid_kd);
119173
int32_t torque_integral = pbio_control_settings_mul_by_gain(integral_error, ctl->settings.pid_ki);
120174

lib/pbio/src/motor/servo_settings.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ pbio_error_t pbio_servo_load_settings(pbio_control_settings_t *ctl, pbio_observe
237237
ctl->integral_change_max = DEG_TO_MDEG(15);
238238
ctl->integral_deadzone = DEG_TO_MDEG(8);
239239
ctl->smart_passive_hold_time = pbio_control_time_ms_to_ticks(100);
240+
ctl->pid_kp_low_pct = 25;
241+
ctl->pid_kp_low_error_threshold = DEG_TO_MDEG(5);
240242

241243
// Device type specific speed, acceleration, and PD settings.
242244
switch (id) {
@@ -339,6 +341,9 @@ pbio_error_t pbio_servo_load_settings(pbio_control_settings_t *ctl, pbio_observe
339341
// is given for all run commands), so we initialize it to the maximum.
340342
ctl->speed_default = ctl->speed_max;
341343

344+
// Take a definition for low speed as percentage of rated speed.
345+
ctl->pid_kp_low_speed_threshold = ctl->speed_max * 15 / 100;
346+
342347
// Deceleration defaults to same value as acceleration
343348
ctl->deceleration = ctl->acceleration;
344349

0 commit comments

Comments
 (0)