Skip to content

pid.hpp

Pid — a single-axis PID controller.

This header declares 2 types (11 members).

Extracted from include/shulib/control/pid.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.

Contents

struct PidConfig

struct PidConfig

Gains and the two bounds, every one of them in the CALLER's units (header note): kP multiplies whatever error unit is fed in, and the three terms must sum to the unit the caller wants back. All-default is a controller that returns 0 for every error, with no clamping anywhere.

struct, declared at include/shulib/control/pid.hpp:31.

PidConfig::kP

double kP = 0.0

Output per unit of error. The only term that ever applies on the first tick.

field, declared at include/shulib/control/pid.hpp:33.

PidConfig::kI

double kI = 0.0

Output per unit of accumulated error·seconds. Exactly 0 skips integration entirely — the accumulator is not even advanced, so integralAccumulator() stays 0 for a P/PD controller.

field, declared at include/shulib/control/pid.hpp:36.

PidConfig::kD

double kD = 0.0

Output per unit of error rate, realized as MINUS kD times the measurement's rate of change: a measurement climbing at 1 unit/s with kD = 2 SUBTRACTS 2 from the output. Differentiating the measurement instead of the error is what keeps a setpoint step from kicking D; while the setpoint is held the two agree, because then d(error)/dt = −d(measurement)/dt.

field, declared at include/shulib/control/pid.hpp:41.

PidConfig::integralLimit

double integralLimit = std::numeric_limits<double>::infinity()

Symmetric ± clamp on the I-TERM (kI·∫e·dt), not on the raw accumulator — the accumulator is then back-calculated to match, which is what makes windup past this bound impossible rather than merely invisible. Must be ≥ 0; the default, infinity, is no anti-windup limit at all.

field, declared at include/shulib/control/pid.hpp:45.

PidConfig::outputMin

double outputMin = -std::numeric_limits<double>::infinity()

Lower clamp on the returned output, applied after P + I + D are summed. Must be ≤ outputMax (checked at construction). Default −infinity: unclamped.

field, declared at include/shulib/control/pid.hpp:48.

PidConfig::outputMax

double outputMax = std::numeric_limits<double>::infinity()

Upper clamp on the returned output. Default +infinity: unclamped.

field, declared at include/shulib/control/pid.hpp:50.

class Pid

class Pid

One axis of PID, distinguished from the textbook loop by three properties the header argues for and the suite pins: derivative on measurement, back-calculated anti-windup, and dt taken from an INJECTED clock instead of read from the OS. STATEFUL: every update() overwrites the dt baseline and the remembered measurement, and (only when kI != 0) advances the integral, so the output depends on the call history and not on this tick's arguments alone. The first update() after construction or reset() has no baseline and applies P only. A repeat call is NOT automatically a different number, though: with kI == 0 and an unchanged measurement both I and D contribute nothing, so a P or PD controller returns the same output twice. Use one instance per axis, and reset() between motions — otherwise the previous motion's integral rides into the next one.

class, declared at include/shulib/control/pid.hpp:64.

Pid::Pid

Pid(const PidConfig& config, hal::IClock& clock)

config is copied (later edits to the caller's struct do nothing); clock is a NON-OWNING reference that must outlive this controller and is the sole source of dt. Rejects non-finite gains, a negative integralLimit and outputMin > outputMax — all at construction, so a controller that cannot be trusted never reaches a match.

function, declared at include/shulib/control/pid.hpp:70.

Pid::update

[[nodiscard]] double update(double setpoint, double measurement)

One control step: returns the clamped control output for (setpoint − measurement).

function, declared at include/shulib/control/pid.hpp:78.

Pid::reset

void reset()

Clear integral + derivative history (e.g. between motions). Gains/limits unchanged.

function, declared at include/shulib/control/pid.hpp:106.

Pid::lastError

[[nodiscard]] double lastError() const noexcept

setpoint − measurement as of the most recent update(), for telemetry — the law never reads it back. 0 before the first update() and after reset(); recorded on every tick, including the dt ≤ 0 ticks that contribute only P.

function, declared at include/shulib/control/pid.hpp:116.

Pid::integralAccumulator

[[nodiscard]] double integralAccumulator() const noexcept

The raw ∫e·dt in error·seconds, AFTER the anti-windup back-calculation — multiply by kI to recover the I-term that was actually added. Stays exactly 0 when kI == 0 (nothing accumulates) and is zeroed by reset(). Exposed so a test can prove the clamp bounds the accumulator itself and not just the output.

function, declared at include/shulib/control/pid.hpp:122.

Design commentary, from the header

The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.

The header’s own reasoning — 16 lines
 Pid — a single-axis PID controller (master plan §M2 control). The design choices that
 matter, each pinned by a test:
  * DERIVATIVE ON MEASUREMENT (not on error): D differentiates the measurement, so a
    setpoint step produces NO derivative kick — only real motion drives D.
  * INTEGRAL ANTI-WINDUP: the I-term is clamped to ±integralLimit and the accumulator is
    back-calculated, so it can never wind up past the clamp.
  * OUTPUT CLAMP to [outputMin, outputMax].
  * INJECTED CLOCK for dt — deterministic and host-testable via FakeClock. The first
    update (and any dt ≤ 0 tick) applies P ONLY: no derivative divide-by-zero, no integral
    step on a zero interval.

 Units are bare `double` BY DESIGN: a PID is a generic numeric law; the caller (the motion
 layer) applies it per-axis with matching units (error in inches/radians → output in
 in/s·rad/s or volts) and owns unit consistency. Inputs are assumed finite (guaranteed by
 the HAL finiteness convention, §7).

Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.