מערכות בקרה
עד עכשיו התוכנית קראה ואחר כך החליטה. בפרק הזה נסגור את המעגל: ההחלטה של התוכנית משנה את העולם, החיישן קורא את השינוי הזה, והקריאה החדשה משנה את ההחלטה. כך המכשיר עומד מול מה שלא צפינו מראש, כמו דלת שנפתחה או עומס שגדל.
- נבחין בין חוג פתוח לחוג סגור, ונצייר תרשים מלבנים עם כל חלקיו.
- נבנה בקרה דו-מצבית ונחשב את זמן המחזור שלה, ובקרה פרופורציונלית עם PWM ונחשב את השגיאה המתמדת שלה.
- נמדוד מהירות של מנוע בעזרת אנקודר, ונבין מה שולט בסרוו מבפנים.
- נכתוב בקר PID בספרייה, נכוונן אותו איכותנית, ונבחר זמן דגימה.
20.1 חוג פתוח וחוג סגור (Open and Closed Loop)
טוסטר שעובד שתי דקות ונעצר אינו יודע דבר על צבע הלחם. מזגן שמודד את טמפרטורת החדר ומשנה את פעולתו לפיה יודע מה התוצאה. זה כל ההבדל.
- אות רצוי r: מה שאנחנו רוצים, למשל 45 מעלות.
- אות מצוי y: מה שהחיישן אומר.
- שגיאה e: ההפרש ביניהם, וזה כל מה שהבקר רואה.
- בקר: החלק בתוכנית שמחשב את הפקודה u מן השגיאה. קוראים לו כך כדי שלא יתבלבל עם המיקרו-בקר, שהוא הלוח כולו.
- מפעיל ותהליך מבוקר: המפעיל מבצע את הפקודה, והתהליך הוא מה שרוצים לשלוט בו.
- הפרעה: כל מה שמשפיע על התהליך מחוץ לתוכנית.
| סמל | פירוש | יחידה |
|---|---|---|
| e | השגיאה: חיובית פירושה שאנחנו מתחת לרצוי | — |
| r | האות הרצוי | — |
| y | האות המצוי | — |
השגיאה חיובית, ולכן אנחנו מתחת לרצוי וצריך להגביר את החימום.
כדי לראות זאת במספרים צריך תהליך שמגיב לפקודות שלנו. לכן נדמה בתוך התוכנית עצמה תהליך מסדר ראשון: ארגז שמתחמם ומתקרר בהדרגה לעבר ערך סופי. אין כאן ארגז אמיתי: הפונקציה process היא העולם כולו, והמספרים הם תוצר של המודל ולא מדידה. בלוח אמיתי מוחקים את הפונקציה וקוראים את החיישן.
| סמל | פירוש | יחידה |
|---|---|---|
| τ | קבוע הזמן, ובו עוברים 63% מן הדרך | s |
| K | בכמה הפקודה המלאה מעלה את הטמפרטורה | °C |
| uk | הפקודה, מ-0 עד 1 | — |
| Ta | טמפרטורת הסביבה | °C |
| Ts | זמן הדגימה | s |
import math
GAIN = 40.0
TAU = 20.0
DT = 1.0
A = 1 - math.exp(-DT / TAU)
def process(temp, room, u):
return temp + A * (room + GAIN * u - temp)
u = 0.625
temp = 20.0
room = 20.0
for t in range(1, 161):
if t == 80:
room = 12.0
temp = process(temp, room, u)
if t % 20 == 0:
print("t=%3d room=%.0f u=%.3f T=%.1f" % (t, room, u, temp))t= 20 room=20 u=0.625 T=35.8 t= 40 room=20 u=0.625 T=41.6 t= 60 room=20 u=0.625 T=43.8 t= 80 room=12 u=0.625 T=44.2 t=100 room=12 u=0.625 T=39.6 t=120 room=12 u=0.625 T=38.0 t=140 room=12 u=0.625 T=37.4 t=160 room=12 u=0.625 T=37.1
החישוב נכון לחלוטין עד שמגיעה ההפרעה. אחריה הטמפרטורה יורדת לעבר 37 מעלות, והתוכנית אינה יודעת.
20.2 בקרה דו-מצבית עם היסטרזיס (On/Off Control)
הבקר הפשוט ביותר מכיר שתי פקודות בלבד: להדליק או לכבות. בסעיף 6.8 ראינו למה סף אחד אינו מספיק, ואיך שני ספים מרוחקים מונעים רעידות. זו ההיסטרזיס, ולא נסביר אותה כאן שוב. החידוש הוא שהתהליך מגיב עכשיו לפקודה שלנו, ונוצר מחזור עצמי סביב האות הרצוי.
board.LED, והתהליך מדומה. אותה הפרעה, ב-t = 80.import math
from machine import Pin
import board
GAIN = 40.0
TAU = 20.0
DT = 1.0
A = 1 - math.exp(-DT / TAU)
LOW = 44.0
HIGH = 46.0
def process(temp, room, u):
return temp + A * (room + GAIN * u - temp)
heater = Pin(board.LED, Pin.OUT)
heater.off()
temp = 20.0
room = 20.0
flips = 0
for t in range(1, 161):
if t == 80:
room = 12.0
if temp < LOW and heater.value() == 0:
heater.on()
flips += 1
elif temp > HIGH and heater.value() == 1:
heater.off()
flips += 1
temp = process(temp, room, heater.value())
if t % 20 == 0:
print("t=%3d T=%.1f heater=%d flips=%d"
% (t, temp, heater.value(), flips))t= 20 T=45.3 heater=1 flips=1 t= 40 T=44.1 heater=0 flips=8 t= 60 T=45.3 heater=0 flips=14 t= 80 T=46.2 heater=1 flips=19 t=100 T=45.7 heater=1 flips=23 t=120 T=45.0 heater=1 flips=27 t=140 T=44.3 heater=1 flips=31 t=160 T=43.4 heater=1 flips=35
החוג הסגור הצליח: הטמפרטורה נשארה סביב התחום למרות שהסביבה התקררה, ויוצאת ממנו מעט כי אנחנו קוראים אותה פעם בשנייה. אבל המחיר הוא שמונה ההחלפות flips אינו מפסיק לעלות. לכל ממסר יש חיים מוגבלים במספר ההחלפות, ולכן נחשב כמה פעמים הוא יפעל.
קצב השינוי ליד 45 מעלות, כשהמחמם פועל (u = 1):
כשהמחמם כבוי (u = 0):
חוצים שתי מעלות כלפי מעלה ושתי מעלות כלפי מטה, ולכן זמן המחזור tc הוא:
20.3 בקרה פרופורציונלית: אור קבוע עם PWM (Proportional Control)
עם PWM אין עוד צורך בקפיצה בין שני הקצוות: אפשר לתת מנה של פקודה שמתאימה לשגיאה. שגיאה גדולה פירושה פקודה גדולה, ושגיאה קטנה פירושה תיקון עדין. הדוגמה שלנו היא נורית ונגד תלוי אור שניצבים זה מול זה: רוצים תאורה קבועה על השולחן, בלילה וביום.
| סמל | פירוש | יחידה |
|---|---|---|
| Kp | ההגבר הפרופורציונלי | — |
| ess | השגיאה המתמדת, אחרי שהתהליך מתייצב | — |
| d | מה שמגיע לחיישן בלי הפקודה שלנו, כאן אור היום | — |
| K | הגבר התהליך | — |
למה נשארת שגיאה? כי u = Kp · e: אילו השגיאה הייתה אפס, גם הפקודה הייתה אפס והנורית הייתה כבה. בקר פרופורציונלי צריך שגיאה כדי לתת פקודה.
הקריאה מתייצבת אפוא על 1.811 V. עם יום חזק יותר, d = 0.9 V, השגיאה קטנה ל-0.122 V והקריאה נעשית 1.878 V.
import math
import time
import board
GAIN = 2.0
TAU = 0.5
DT = 0.05
A = 1 - math.exp(-DT / TAU)
KP = 4.0
TARGET = 2.0
led = board.pwm(board.LED_EXT, 1000)
day = 0.3
v = day
for k in range(1, 61):
if k == 31:
day = 0.9
u = KP * (TARGET - v)
u = max(0.0, min(1.0, u))
led.duty_u16(int(u * 65535))
v += A * (day + GAIN * u - v)
if k % 10 == 0:
print("t=%.1f day=%.1f V=%.3f duty=%2.0f%%"
% (k * DT, day, v, u * 100))
time.sleep(DT)t=0.5 day=0.3 V=1.564 duty=100% t=1.0 day=0.3 V=1.811 duty=76% t=1.5 day=0.3 V=1.811 duty=76% t=2.0 day=0.9 V=1.878 duty=49% t=2.5 day=0.9 V=1.878 duty=49% t=3.0 day=0.9 V=1.878 duty=49%
הקריאה תואמת את החישוב. בלוח אמיתי מוחקים את המודל וקוראים את הנגד התלוי אור. התוכנית הבאה מוכנה לכך, ובה הפוטנציומטר קובע את האות הרצוי. בעולם המדומה כאן קריאת האור אינה עוקבת אחרי הנורית, ולכן נותנים לה ערכים עולים כדי לראות איך הפקודה נסוגה:
import time
import board
KP = 1.0
pot = board.analog_in(board.POT)
ldr = board.analog_in(board.LDR)
led = board.pwm(board.LED_EXT, 1000)
while True:
target = pot.read_u16() * 3.3 / 65535
light = ldr.read_u16() * 3.3 / 65535
u = KP * (target - light)
u = max(0.0, min(1.0, u))
led.duty_u16(int(u * 65535))
print("want %.2f have %.2f duty %3.0f%%"
% (target, light, u * 100))
time.sleep(1)want 2.00 have 0.50 duty 100% want 2.00 have 1.00 duty 100% want 2.00 have 1.50 duty 50% want 2.00 have 2.00 duty 0% want 2.00 have 2.50 duty 0% ...
KP בתוכנית.20.4 מהירות המנוע: PWM עם אנקודר (Speed Control with an Encoder)
הפעלנו מנוע ב-PWM בסעיף 7.8, אבל זה היה חוג פתוח: כשהעומס גדל המהירות יורדת ואנחנו לא יודעים. כדי לסגור את החוג צריך למדוד את המהירות, וזו עבודתו של האנקודר: דיסק שמותקן על הציר ונותן מספר קבוע של פולסים בכל סיבוב.
| סמל | פירוש | יחידה |
|---|---|---|
| n | המהירות | rpm |
| N | הפולסים שספרנו במהלך החלון | — |
| P | מספר הפולסים בסיבוב | — |
| T | אורך חלון הספירה | s |
| Δn | ההפרש הקטן ביותר במהירות שאנחנו רואים | rpm |
board.BTN. רכבת הפולסים כאן באה מן העולם המדומה, ומהירותה יורדת לחצי אחרי שנייה.import time
import machine
from machine import Pin
import board
PPR = 40
WINDOW = 0.5
pulses = 0
def on_edge(pin):
global pulses
pulses += 1
enc = Pin(board.BTN, Pin.IN, Pin.PULL_UP)
enc.irq(trigger=Pin.IRQ_FALLING, handler=on_edge)
for _ in range(4):
time.sleep(WINDOW)
state = machine.disable_irq()
n = pulses
pulses = 0
machine.enable_irq(state)
rpm = n * 60 / (PPR * WINDOW)
print("pulses=%3d rpm=%.0f" % (n, rpm))pulses= 50 rpm=150 pulses= 50 rpm=150 pulses= 25 rpm=75 pulses= 25 rpm=75
לכן הקריאה קופצת בצעדים של 15 rpm. חלון ארוך יותר נותן קריאה מדויקת יותר, אבל החוג נעשה איטי יותר.
סוגרים את החוג בבקר פרופורציונלי. גם המנוע כאן מדומה, עם קבוע זמן 1 s ומהירות סופית 300 rpm בפקודה מלאה, והפולסים שלו מחושבים ממהירותו:
import math
import time
import board
PPR = 40
DT = 0.1
GAIN = 300.0
TAU = 1.0
A = 1 - math.exp(-DT / TAU)
KP = 0.01
TARGET = 150.0
motor = board.pwm(board.LED_EXT, 1000)
speed = 0.0
turned = 0.0
load = 0.0
for k in range(1, 81):
if k == 41:
load = 60.0
turned += speed * PPR * DT / 60
pulses = int(turned)
turned -= pulses
rpm = pulses * 60 / (PPR * DT)
u = max(0.0, min(1.0, KP * (TARGET - rpm)))
motor.duty_u16(int(u * 65535))
speed += A * (GAIN * u - load - speed)
if k % 10 == 0:
print("t=%.0f load=%2.0f speed=%3.0f duty=%2.0f%%"
% (k * DT, load, speed, u * 100))
time.sleep(DT)t=1 load= 0 speed=112 duty=45% t=2 load= 0 speed=112 duty=30% t=3 load= 0 speed=112 duty=30% t=4 load= 0 speed=112 duty=45% t=5 load=60 speed=100 duty=60% t=6 load=60 speed=100 duty=60% t=7 load=60 speed=100 duty=60% t=8 load=60 speed= 96 duty=45%
רואים בבירור את החיסרון של הבקר הפרופורציונלי: המהירות אינה מגיעה לרצוי, ומתרחקת ממנו עוד יותר עם העומס. לפי הנוסחה של ess נשארת שגיאה של כ-37.5 rpm בלי עומס וכ-52.5 rpm עם עומס. מחזור העבודה קופץ בין שני ערכים, כי כל צעד של האנקודר שווה ל-15% מן הפקודה. האיבר האינטגרלי יטפל בשגיאה המתמדת.
20.5 זווית הזרוע: הסרוו (Angle Control)
הסרוו הוא חוג סגור מוכן: בתוכו מנוע קטן, פוטנציומטר על הציר שקורא את הזווית, ומעגל שמשווה אותה לרוחב הפולס. לכן אנחנו לא שולטים במנוע, אלא רק שולחים את האות הרצוי, והסרוו סוגר את החוג בעצמו.
אז מה נשאר לנו? לבחור איך האות הרצוי משתנה. קפיצה גדולה גורמת לזרוע להיטיח במהירות מרבית ולמשוך זרם גבוה. לכן מגבילים את השינוי בכל צעד:
servo.py מפרק 7, והפוטנציומטר בקצה העליון. הזרוע מתקדמת 25 מעלות בכל צעד עד שהיא מגיעה.import time
import board
from servo import Servo
MAX_STEP = 25
pot = board.analog_in(board.POT)
arm = Servo(board.pwm(board.SERVO, 50))
now = 90
arm.angle(now)
for _ in range(6):
target = pot.read_u16() * 180 / 65535
step = max(-MAX_STEP, min(MAX_STEP, target - now))
now += step
arm.angle(now)
print("target=%3.0f now=%3.0f" % (target, now))
time.sleep(0.5)target=180 now=115 target=180 now=140 target=180 now=165 target=180 now=180 target=180 now=180 target=180 now=180
20.6 בקר PID (PID Controller)
הבקר הזה מחבר שלושה איברים, וכל איבר מסתכל על השגיאה בזמן אחר: הפרופורציונלי מסתכל על ההווה, האינטגרלי על העבר, והדיפרנציאלי על המגמה.
| סמל | פירוש | יחידה |
|---|---|---|
| Ki | ההגבר האינטגרלי: מסכם את השגיאה לאורך הזמן | — |
| Kd | ההגבר הדיפרנציאלי: מגיב לקצב השינוי | — |
| Ik | סכום האיבר האינטגרלי עד הצעד k | — |
| Ts | זמן הדגימה | s |
האינטגרציה פותרת את בעיית השגיאה המתמדת: כל עוד יש שגיאה, ולו קטנה, הסכום ממשיך להשתנות, ולכן החוג מתייצב רק בשגיאה אפס. בנוסחה הבדידה כותבים את הנגזרת על הקריאה y ולא על השגיאה, ולכן אין קפיצה בפקודה כשמשנים את r.
dt.# pid.py - a discrete PID loop with output limits, the same on every board.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
class PID:
def __init__(self, kp, ki, kd, dt, lo=0.0, hi=1.0):
self.kp = kp
self.ki = ki
self.kd = kd
self.dt = dt
self.lo = lo
self.hi = hi
self.reset()
def reset(self):
self.total = 0.0
self.last = None
def update(self, target, measured):
error = target - measured
slope = 0.0
if self.last is not None:
slope = (measured - self.last) / self.dt
self.last = measured
total = self.total + self.ki * error * self.dt
out = self.kp * error + total - self.kd * slope
if out > self.hi:
return self.hi
if out < self.lo:
return self.lo
self.total = total
return outif out > self.hi: כשמגיעים לגבול המוצא לא שומרים את הסכום החדש. אילו שמרנו אותו, הוא היה ממשיך לגדול בזמן שהמחמם בהספק מלא, ומתרוקן רק אחרי שהטמפרטורה עוברת את היעד. זו התרוממות האינטגרל. במודל שלנו: עם ההגנה הטמפרטורה מגיעה ל-45.5 מעלות, ובלעדיה ל-54.9 מעלות.import math
from pid import PID
GAIN = 40.0
TAU = 20.0
DT = 1.0
A = 1 - math.exp(-DT / TAU)
TARGET = 45.0
def run(kp, ki, kd, noise):
loop = PID(kp, ki, kd, DT)
temp = 20.0
peak = temp
last_u = 0.0
moved = 0.0
for k in range(180):
n = noise if k % 2 else -noise
u = loop.update(TARGET, temp + n)
temp += A * (20.0 + GAIN * u - temp)
peak = max(peak, temp)
if k >= 150:
moved += abs(u - last_u)
last_u = u
return peak, temp, moved / 30
print("kind peak final output move")
for name, kp, ki, kd in (("P ", 0.1, 0.0, 0.0),
("PI ", 0.1, 0.02, 0.0),
("PID", 0.1, 0.02, 0.2)):
peak, final, _ = run(kp, ki, kd, 0.0)
_, _, move = run(kp, ki, kd, 0.1)
print("%s %.1f %.1f %.3f" % (name, peak, final, move))kind peak final output move P 40.0 40.0 0.022 PI 45.5 45.0 0.025 PID 46.2 45.0 0.208
הטבלה שהודפסה מסכמת: PI מסלק את השגיאה המתמדת. התהליך שלנו מסדר ראשון, ולכן האיבר הדיפרנציאלי אינו מועיל בו: החריגה גדלה מעט כי הוא מוציא את הפקודה מגבולה מוקדם יותר, וכך הסכום מתחיל לצבור מוקדם יותר, ותנועת הפקודה גדלה מאוד בגלל הרעש. לכן בדרך כלל מתחילים ב-PI, ומוסיפים Kd רק אם התהליך איטי ומתעכב והחיישן נקי.
כיוונון איכותני:
- מציבים Ki = 0 ו-Kd = 0, ומעלים את Kp עד שהתגובה מהירה, לפני שמתחילה התנודה, ואחר כך מורידים אותו מעט.
- מעלים את Ki בהדרגה עד שהשגיאה המתמדת נעלמת בזמן סביר. אם החריגה גדלה, מורידים אותו.
- מוסיפים Kd מעט בלבד אם נשארת חריגה גדולה, ועוקבים אחרי הפקודה: אם היא רועדת, מורידים אותו או מעבירים את הקריאה במסנן.
| כשמעלים | המהירות | החריגה | שגיאה מתמדת | השפעת הרעש |
|---|---|---|---|---|
| Kp | גדלה | גדלה | קטנה | גדלה |
| Ki | גדלה | גדלה | נעלמת | מעטה |
| Kd | מעטה | קטנה בגבולות מסוימים, ולא תמיד | לא משתנה | גדלה מאוד |
20.7 בחירת זמן הדגימה (Sampling Time)
התוכנית רואה את התהליך במרווחים, ובין לבין אינה יודעת דבר. אם מאחרים מדי, התיקון כבר ישן כשהוא מגיע, ועלול לדחוף לכיוון הלא נכון. ננסה את הבקר הפרופורציונלי עצמו (Kp = 0.1) על אותו ארגז, ונשנה רק את זמן הדגימה:
import math
GAIN = 40.0
TAU = 20.0
KP = 0.1
def run(dt):
a = 1 - math.exp(-dt / TAU)
temp = 20.0
peak = temp
low = 99.0
high = 0.0
steps = int(240 / dt)
for k in range(steps):
u = max(0.0, min(1.0, KP * (45.0 - temp)))
temp += a * (20.0 + GAIN * u - temp)
peak = max(peak, temp)
if k >= steps // 2:
low = min(low, temp)
high = max(high, temp)
return peak, high - low
for dt in (1, 5, 10, 20, 30):
peak, swing = run(dt)
print("Ts=%2d s peak=%.1f swing=%.1f" % (dt, peak, swing))Ts= 1 s peak=40.0 swing=0.0 Ts= 5 s peak=40.5 swing=0.0 Ts=10 s peak=44.1 swing=5.6 Ts=20 s peak=49.2 swing=18.5 Ts=30 s peak=52.7 swing=25.4
התנאי בדוגמה הזאת הוא a · (1 + Kp · K) < 2. עם Kp · K = 4 מקבלים a < 0.4, ומכאן Ts < 10.2 s. זה מה שרואים במספרים: ב-10 s אנחנו על שפת הגבול, והתנועה דועכת לאט מאוד, וב-20 s היא אינה דועכת לעולם.
בפועל לא מתקרבים לגבול. לוקחים את הכלל הנפוץ: עשר דגימות לפחות בקבוע זמן אחד, Ts ≤ τ/10. אין טעם להגזים גם בכיוון ההפוך: הטמפרטורה בארגז שלנו אינה משתנה בחלקיק שנייה, ובדגימות רבות מדי ההפרש בין שתי הקריאות באיבר הדיפרנציאלי נעשה רעש בלבד, וחלון האנקודר מצטמצם ודיוקו מידרדר, כפי שחישבנו ב-20.4.
| התהליך | קבוע זמן משוער בדוגמה | זמן דגימה מתאים |
|---|---|---|
| הטמפרטורה בארגז שלנו | 20 s | 2 s או פחות |
| נורית ונגד תלוי אור | 0.5 s | 50 ms או פחות |
| המנוע שלנו | 1 s | 100 ms או פחות |
time.sleep(0.1) לבדה אינה נותנת אותו, כי זמן החישוב וההדפסה מתווסף אליה. לכן מחשבים את מועד כל צעד ממועד הצעד הקודם בעזרת ticks_add, כמו בתוכנית הבאה. אפשר גם להשתמש בטיימר או במשימת asyncio מפרק 19.נאגד עכשיו את כל מה שלמדנו: מנוע מדומה, פולסים של אנקודר, בקר PI מן הספרייה שלנו, וצעד קבוע:
import math
import time
import board
from pid import PID
PPR = 40
PERIOD_MS = 100
DT = PERIOD_MS / 1000
GAIN = 300.0
TAU = 1.0
A = 1 - math.exp(-DT / TAU)
TARGET = 150.0
motor = board.pwm(board.LED_EXT, 1000)
loop = PID(0.004, 0.008, 0.0, DT)
speed = 0.0
turned = 0.0
load = 0.0
deadline = time.ticks_ms()
for k in range(1, 121):
if k == 61:
load = 60.0
turned += speed * PPR * DT / 60
pulses = int(turned)
turned -= pulses
u = loop.update(TARGET, pulses * 60 / (PPR * DT))
motor.duty_u16(int(u * 65535))
speed += A * (GAIN * u - load - speed)
if k % 15 == 0:
print("t=%4.1f load=%2.0f speed=%3.0f duty=%2.0f%%"
% (k * DT, load, speed, u * 100))
deadline = time.ticks_add(deadline, PERIOD_MS)
wait = time.ticks_diff(deadline, time.ticks_ms())
if wait > 0:
time.sleep_ms(wait)t= 1.5 load= 0 speed=157 duty=55% t= 3.0 load= 0 speed=155 duty=52% t= 4.5 load= 0 speed=149 duty=43% t= 6.0 load= 0 speed=151 duty=50% t= 7.5 load=60 speed=140 duty=72% t= 9.0 load=60 speed=151 duty=71% t=10.5 load=60 speed=151 duty=77% t=12.0 load=60 speed=151 duty=77%
עכשיו המהירות מגיעה בקירוב ל-150 rpm לפני העומס ואחריו. השגיאה המתמדת נעלמה, ונשארת רק תנועה קטנה בגודל צעד של האנקודר. מחזור העבודה עלה מעצמו כדי לפצות על העומס, וזו עבודתו של חוג סגור.
עיקרי הפרק:
- חוג פתוח אינו יודע את התוצאה, וחוג סגור מודד ומתקן לפי השגיאה e = r − y.
- בקרה דו-מצבית פשוטה ויש לה מחזור עצמי, ותחום ההיסטרזיס קובע את התנודה ואת מספר ההחלפות.
- בקר פרופורציונלי משאיר שגיאה מתמדת, כי הוא צריך שגיאה כדי לתת פקודה.
- האנקודר הופך מהירות לפולסים, וחלון הספירה מאזן בין דיוק לאיטיות.
- האיבר האינטגרלי מסלק את השגיאה המתמדת וצריך הגנה מהתרוממות, והאיבר הדיפרנציאלי מגדיל את הרעש.
- זמן הדגימה קבוע, ולפחות עשר דגימות בקבוע זמן אחד.
סיכום הכללים בפרק:
| הכלל | הנוסחה |
|---|---|
| שגיאה | e = r − y |
| תהליך מסדר ראשון | Tk+1 = Tk + a · (Ta + K · uk − Tk) |
| השגיאה המתמדת של בקר פרופורציונלי | ess = r − d1 + Kp · K |
| מהירות מן האנקודר | n = 60 · NP · T |
| בקר PID | u = Kp · e + Ki · ∫e dt + Kd · dedt |
פקודות הפרק:
| פקודה | מה היא עושה |
|---|---|
| PID(kp, ki, kd, dt, lo, hi) | בקר מן הספרייה, עם שני גבולות למוצא. |
| loop.update(target, measured) | צעד אחד: נותן את הפקודה החדשה. |
| loop.reset() | איפוס הסכום, כשמפעילים מחדש את החוג. |
| machine.disable_irq() | עוצר את הפסיקות לרגע, סביב משתנה משותף. |
| time.ticks_add(t, ms) | מועד הצעד הבא בזמן קבוע. |
מילון מונחים:
| עברית | English | ערבית |
|---|---|---|
| חוג פתוח | Open Loop | الحلقة المفتوحة |
| חוג סגור | Closed Loop | الحلقة المغلقة |
| משוב | Feedback | التغذية الراجعة |
| תרשים מלבנים | Block Diagram | المخطّط الصندوقي |
| אות רצוי | Set Point | القيمة المطلوبة |
| אות מצוי | Measured Value | القيمة المقيسة |
| שגיאה | Error | الخطأ |
| הפרעה | Disturbance | الاضطراب |
| בקר | Controller | المنظّم |
| תהליך מבוקר | Process | العملية |
| קבוע זמן | Time Constant | ثابت الزمن |
| שגיאה מתמדת | Steady-State Error | الخطأ الدائم |
| אנקודר | Encoder | المرمّز |
| התרוממות אינטגרל | Integrator Windup | الإشباع التكاملي |
| חריגה | Overshoot | التجاوز |
| זמן דגימה | Sampling Time | زمن العيّنة |
duty_u16 הוא 4915 מתוך 65535. שני הגבולות האלה נפוצים, ומשתנים מדגם לדגם.שאלות לתרגול:
kd * slope על הקריאה ולא על השגיאה?(ב) מה קורה בקריאה הראשונה?
self.last ריק, ולכן מחשיבים את השיפוע כאפס.reset()?(ב) מה קורה אם כיבינו את המחמם בכפתור והשארנו את החוג לצבור שגיאה, ואחר כך חזרנו?
update וקוראים ל-reset().