Skip to main content

<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.

#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

FieldTypeNotes
kpfloatProportional gain.
kifloatIntegral gain (per second).
kdfloatDerivative gain (seconds).
out_minfloatOutput 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_maxfloatOutput upper clamp.
integ_limitfloatAnti-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_measurementboolWhen 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

CallReturns
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

Questions about this page? Discuss in Community Forum