PIDControl (pid_control v0.1.0)

A discrete implementation of a PID controller for building closed-loop control into embedded systems.

usage

Usage

The PIDControl.new/1 function returns an initialized PIDControl struct with the given configuration:

iex> pid = PIDControl.new(kp: 0.5, kd: 0.2, ki: 0.03)
%PIDControl{...}

Calling PIDControl.step/3 performs one discrete cycle of the PID loop for the given set_point and measurement values. The new PIDControl state is returned and the output can be accessed via the output key in the struct.

iex> pid = PIDControl.step(pid, 0.3, 0.312)
%PIDControl{
output: -0.11448221
#...
}

The step function can be called in each successive input-output cycle of the underling sensor/actuator.

example

Example

defmodule Controller do
use GenServer
# ...

def init(_) do
  Sensor.subcribe()
  pid = PIDControl.new(kp: 0.5, kd: 0.2, ki: 0.03)
  {:ok, pid}
end

def handle_info({:sensor, measurment}, pid) do
  pid = PIDControl.step(pid, @set_point, measurment)
  Actuator.command(pid.output)
  {:noreply, pid}
end
end

telemetry

Telemetry

If telemetry is set to true, telemetry for the PID state will be emitted each time the step function is called. By default, the event name will be [pid_control] and will contain the following measurements.

  • set_point - Current set point
  • measurement - Most recent measurement
  • error - Current error that is being fed to the PID
  • p - Proportional component of the output
  • i - Integral component of the output
  • d - Derivative component of the output
  • t - The time value used for the step function. This will always be the configured value t unless use_system_t is set to true.
  • output - Most recent output (which will be the sum of p, i, and d values)

This is really useful for manual tuning when combined with something like PheonixLiveDashboard.

Link to this section Summary

Functions

Creates a new PID controller.

Performs one step in the control loop. Given the set point and measurement, the PID state is advanced and the output of the PID is set to the output field of the PIDControl struct.

Link to this section Types

@type config() :: %{
  kp: float(),
  ki: float(),
  kd: float(),
  tau: float(),
  t: float(),
  output_min: float(),
  output_max: float(),
  use_system_t: boolean(),
  zero_d_on_set_point_change: boolean(),
  telemetry: boolean(),
  telemetry_event_name: [atom()]
}
@type t() :: %PIDControl{
  config: config(),
  d: float(),
  e0: float() | nil,
  i: float(),
  measurement_prev: float(),
  output: float(),
  p: float(),
  set_point_prev: float(),
  t: float(),
  time_prev: integer() | nil
}

Link to this section Functions

Link to this function

new(config \\ [])

@spec new(keyword()) :: t()

Creates a new PID controller.

Config Options:

  • kp, ki, kp - Parameters for each term in the PID. Any left blank will be set to 0
  • tau - Low-pass filter parameter for the calculated d term. A value of 1 will bypass the filtering. A value between 0 and 1 will filter out high-frequency noise in the derivative term.
  • t - Time factor for calculating the i and d term of the PID. A value of 1 will ignore any time parameters and treat each step as a single time unit. A value of 0.1, for example, will treat each step as a tenth of a time unit. If use_system_t is set to true, this value is ignored in favor of the system time (in seconds) between calls to step.
  • output_min and output_max - Minimum and maximum values that will be output from the PID. Any other values outside of this range will be clamped. These same values are used for anti-windup of the i term. Default is -1 and 1, respectively.
  • use_system_t - When set to true, the time used to calculate d and i terms of the PID will be derived from the measured time since the last call to step (using System.monotonic_time/0). When set to false, the configured value of t will be used. Defaults to false.
  • zero_d_on_set_point_change - When the set point changes rapidly, the d-term will suddenly spike, causing a sudden change in the PID output. Setting tau to a lower value will lessen this effect, but setting zero_d_on_set_point_change will force the d-term to zero for the step when the set_point changes. This is useful for situations when the set point changes suddenly and occasionally, as opposed to situations where the set_point is gradually changes (as when following a profile curve). Defaults to false.
  • telemetry - When set to true, the PID will automatically emit its own telemetry after each step. Defaults to false.
  • telemetry_prefix - If telemetry is set to true, this value defines the name that the event is published under. Defaults to [:pid_control]
Link to this function

step(pid, set_point, measurement)

@spec step(t(), float(), float()) :: t()

Performs one step in the control loop. Given the set point and measurement, the PID state is advanced and the output of the PID is set to the output field of the PIDControl struct.