<alp/pid.h> — PID Controller
A small, caller-owned PID (proportional-integral-derivative) control loop for motor, thermal, power, or attitude regulation. Features output clamping, integrator anti-windup, and derivative-on-measurement.
Unlike the DSP chain, a PID needs no pool and no handle — the caller allocates an alp_pid_t (typically static or on the stack), initialises it once, and calls alp_pid_step every control tick.
Header
#include <alp/pid.h>
board.yaml
The controller is opt-in — it compiles only when the pid library knob is selected, which emits CONFIG_ALP_SDK_PID:
libraries:
- pid
The implementation is pure C with no vendor dependency and builds on every OS target. The profile's hardware-accelerator bindings (ALP_PID_FPU, ALP_PID_TIMER) select a faster path on SoMs that expose one, transparently to this API; ALP_PID_INT_MATH is the always-available software floor.
Run a loop
static alp_pid_t pid;
const alp_pid_config_t cfg = {
.kp = 0.8f, .ki = 0.2f, .kd = 0.01f,
.out_min = -1.0f, .out_max = 1.0f,
.deriv_on_measurement = true,
};
alp_pid_init(&pid, &cfg);
for (;;) {
float u = alp_pid_step(&pid, setpoint, measured, dt_s);
actuate(u);
}
The step computes u = Kp*e + Ki*integral(e) - Kd*d, applies anti-windup to the integrator, and saturates u to the configured output range.
Config struct
| Field | Type | Notes |
|---|---|---|
kp | float | Proportional gain. |
ki | float | Integral gain (per second). |
kd | float | Derivative gain (seconds). |
out_min | float | Output lower clamp. The step result is saturated to [out_min, out_max]; set both to 0 to disable clamping. If out_max < out_min the values are treated as "no clamp". |
out_max | float | Output upper clamp. |
integ_limit | float | Anti-windup limit on the integrator accumulator, in error×time units. The integrator's magnitude is clamped to this. Pass <= 0 to auto-select half the output span, (out_max - out_min) / 2. |
deriv_on_measurement | bool | When true, the derivative acts on -measurement, which avoids the derivative kick a step change in setpoint would otherwise inject into the D term. When false, it acts on the error. Steady-state behaviour is identical either way. |
Functions
| Call | Returns |
|---|---|
alp_pid_init(alp_pid_t *pid, const alp_pid_config_t *cfg) | ALP_OK / ALP_ERR_INVAL (NULL pid or cfg). Copies cfg into pid and clears the integrator + derivative history. Safe to call again at any time to re-tune — it resets state. |
alp_pid_step(alp_pid_t *pid, float setpoint, float measurement, float dt_s) | float — the saturated control output. Returns 0.0f if pid is NULL or dt_s <= 0 (a non-positive dt_s does not advance state). |
alp_pid_reset(alp_pid_t *pid) | void. Clears the integrator and derivative history, keeping the tuning. NULL is a no-op. |
Use alp_pid_reset whenever the loop has been disengaged (e.g. motors disarmed), so a stale integrator does not slam the output on re-engage.
State struct
alp_pid_t is caller-owned and transparent (so it is stack-allocatable), holding the copied config, the integrator accumulator, and the derivative history. Treat it as opaque — do not read or write the fields directly; the layout is [ABI-EXPERIMENTAL] and may gain fields before v1.0. Allocate one per control loop.
ABI status
[ABI-EXPERIMENTAL] — new in v0.10. The struct layout may change before v1.0; pin your SDK to a specific commit if you depend on it.
See also
<alp/ahrs.h>— Madgwick orientation filter, same caller-owned no-handle shape<alp/pwm.h>— the usual actuator for a PID outputboard.yaml:libraries: