-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpid.py
169 lines (133 loc) · 5.54 KB
/
pid.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# This is a simpler version of https://github.com/m-lundberg/simple-pid/blob/master/simple_pid/pid.py#L198
# It has been adjusted for the raspberry pi pico
import time
def _clamp(value, limits):
lower, upper = limits
if value is None:
return None
elif (upper is not None) and (value > upper):
return upper
elif (lower is not None) and (value < lower):
return lower
return value
class PID(object):
"""A simple PID controller."""
def __init__(
self,
Kp=1.0,
Ki=0.0,
Kd=0.0,
set_point=0,
output_limits=(None, None),
disable_value=0,
starting_output=0.0
):
"""
Initialize a new PID controller.
:param Kp: The value for the proportional gain Kp
:param Ki: The value for the integral gain Ki
:param Kd: The value for the derivative gain Kd
:param set_point: The initial set-point that the PID will try to achieve
:param output_limits: The initial output limits to use, given as an iterable with 2
elements, for example: (lower, upper). The output will never go below the lower limit
or above the upper limit. Either of the limits can also be set to None to have no limit
in that direction. Setting output limits also avoids integral windup, since the
integral term will never be allowed to grow outside the limits.
:param starting_output: The starting point for the PID's output. If you start controlling
a system that is already at the set-point, you can set this to your best guess at what
output the PID should give when first calling it to avoid the PID outputting zero and
moving the system away from the set-point.
"""
self.Kp, self.Ki, self.Kd = Kp, Ki, Kd
self.set_point = set_point
self._min_output, self._max_output = None, None
self._proportional = 0
self._integral = 0
self._derivative = 0
self._last_time = time.ticks_us()
self._last_error = 0
self.output_limits = output_limits
self.active = True
self.disable_value = disable_value
# Set initial state of the controller
self._integral = _clamp(starting_output, output_limits)
def __call__(self, input_):
"""
Update the PID controller.
Call the PID controller with *input_* and calculate and return a control output if
sample_time seconds has passed since the last update. If no new output is calculated,
return the previous output instead (or None if no value has been calculated yet).
"""
now = time.ticks_us()
dt = (now - self._last_time)*1e-6
# Compute error terms
error = self.set_point - input_
d_error = error - self._last_error
# todo: make this avoid integral windup
self._proportional = self.Kp*error
self._integral += self.Ki*error*dt
self._derivative = self.Kd*d_error/dt
self._integral = _clamp(self._integral, self.output_limits)
# Compute final output
output = self._proportional + self._integral + self._derivative
output = _clamp(output, self.output_limits)
# Keep track of state
self._last_error = error
self._last_time = now
return dt, (output if self.active else self.disable_value)
def __repr__(self):
return (
'{self.__class__.__name__}('
'Kp={self.Kp!r}, Ki={self.Ki!r}, Kd={self.Kd!r}, '
'set_point={self.set_point!r}, output_limits={self.output_limits!r}'
')'
).format(self=self)
def disable(self):
self.active = False
def enable(self):
self.active = True
@property
def components(self):
"""
The P-, I- and D-terms from the last computation as separate components as a tuple. Useful
for visualizing what the controller is doing or when tuning hard-to-tune systems.
"""
return self._proportional, self._integral, self._derivative
@property
def tunings(self):
"""The tunings used by the controller as a tuple: (Kp, Ki, Kd)."""
return self.Kp, self.Ki, self.Kd
@tunings.setter
def tunings(self, tunings):
"""Set the PID tunings."""
self.Kp, self.Ki, self.Kd = tunings
@property
def output_limits(self):
"""
The current output limits as a 2-tuple: (lower, upper).
See also the *output_limits* parameter in :meth:`PID.__init__`.
"""
return self._min_output, self._max_output
@output_limits.setter
def output_limits(self, limits):
"""Set the output limits."""
if limits is None:
self._min_output, self._max_output = None, None
return
min_output, max_output = limits
if (None not in limits) and (max_output < min_output):
raise ValueError('lower limit must be less than upper limit')
self._min_output = min_output
self._max_output = max_output
self._integral = _clamp(self._integral, self.output_limits)
def reset(self):
"""
Reset the PID controller internals.
This sets each term to 0 as well as clearing the integral, the last output and the last
input (derivative calculation).
"""
self._proportional = 0
self._integral = 0
self._derivative = 0
self._integral = _clamp(self._integral, self.output_limits)
self._last_time = time.ticks_us()