תכנות מיקרו-בקרים בשפת MicroPython · פרק 20 — מערכות בקרה⌂📚🏠
פרק 20

מערכות בקרה

Control Systems
4 שעות לימוד

עד עכשיו התוכנית קראה ואחר כך החליטה. בפרק הזה נסגור את המעגל: ההחלטה של התוכנית משנה את העולם, החיישן קורא את השינוי הזה, והקריאה החדשה משנה את ההחלטה. כך המכשיר עומד מול מה שלא צפינו מראש, כמו דלת שנפתחה או עומס שגדל.

בפרק הזה:
  • נבחין בין חוג פתוח לחוג סגור, ונצייר תרשים מלבנים עם כל חלקיו.
  • נבנה בקרה דו-מצבית ונחשב את זמן המחזור שלה, ובקרה פרופורציונלית עם PWM ונחשב את השגיאה המתמדת שלה.
  • נמדוד מהירות של מנוע בעזרת אנקודר, ונבין מה שולט בסרוו מבפנים.
  • נכתוב בקר PID בספרייה, נכוונן אותו איכותנית, ונבחר זמן דגימה.

20.1 חוג פתוח וחוג סגור (Open and Closed Loop)

טוסטר שעובד שתי דקות ונעצר אינו יודע דבר על צבע הלחם. מזגן שמודד את טמפרטורת החדר ומשנה את פעולתו לפיה יודע מה התוצאה. זה כל ההבדל.

חוג פתוח: שולחים את הפקודה בלי למדוד את התוצאה. כל מה שלא לקחנו בחשבון נשאר שגיאה.
בקרמפעילתהליךאות רצוי
חוג פתוח: החץ נע בכיוון אחד.
חוג סגור: מודדים את התוצאה, משווים אותה לרצוי, ומתקנים את הפקודה לפי ההפרש. המסלול שבו המדידה חוזרת אל ההשוואה נקרא משוב.
+−בקרמחשב פקודהמפעילPWM, ממסרתהליךארגז, מנועreuyהפרעהחיישןמשוב
תרשים מלבנים של חוג סגור. סימן המינוס בהשוואה הוא מה שגורם לו לתקן ולא להגביר.
שגיאה
e = r − y
סמלפירושיחידה
eהשגיאה: חיובית פירושה שאנחנו מתחת לרצוי—
rהאות הרצוי—
yהאות המצוי—
דוגמה 20.1
בארגז ייבוש r = 45 מעלות והחיישן אומר y = 41. מהי השגיאה? ומה על המחמם לעשות?
פתרון:
e = r − y
e = 45 − 41
e = 4

השגיאה חיובית, ולכן אנחנו מתחת לרצוי וצריך להגביר את החימום.

e = 4

כדי לראות זאת במספרים צריך תהליך שמגיב לפקודות שלנו. לכן נדמה בתוך התוכנית עצמה תהליך מסדר ראשון: ארגז שמתחמם ומתקרר בהדרגה לעבר ערך סופי. אין כאן ארגז אמיתי: הפונקציה process היא העולם כולו, והמספרים הם תוצר של המודל ולא מדידה. בלוח אמיתי מוחקים את הפונקציה וקוראים את החיישן.

תהליך מסדר ראשון, צעד בכל Ts
Tk+1 = Tk + a · (Ta + K · uk − Tk)
a = 1 − e−Ts/τ
סמלפירושיחידה
τקבוע הזמן, ובו עוברים 63% מן הדרך s
Kבכמה הפקודה המלאה מעלה את הטמפרטורה°C
ukהפקודה, מ-0 עד 1—
Taטמפרטורת הסביבה°C
Tsזמן הדגימה s
חוג פתוח: חישבנו שהפקודה u = 0.625 מגיעה ל-45 מעלות. ב-t = 80 הסביבה מתקררת מ-20 ל-12, ואיש אינו מתקן.
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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
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.1: לדוד חשמלי יש תרמוסטט: מהם r ו-y בו? ומהו המפעיל? ציינו הפרעה אחת.

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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
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
0408012016020304050tT[s][°C]הפרעה
הטמפרטורה בתוכנית הקודמת: גם אחרי ההפרעה היא נשארת סביב תחום ההיסטרזיס.

החוג הסגור הצליח: הטמפרטורה נשארה סביב התחום למרות שהסביבה התקררה, ויוצאת ממנו מעט כי אנחנו קוראים אותה פעם בשנייה. אבל המחיר הוא שמונה ההחלפות flips אינו מפסיק לעלות. לכל ממסר יש חיים מוגבלים במספר ההחלפות, ולכן נחשב כמה פעמים הוא יפעל.

דוגמה 20.2
הארגז שלנו: K = 40, τ = 20 s, Ta = 20 מעלות, והספים 44 ו-46. מהו זמן המחזור בקירוב?
פתרון:

קצב השינוי ליד 45 מעלות, כשהמחמם פועל (u = 1):

ΔTΔt = Ta + K · u − Tτ
ΔTΔt = 20 + 40 − 4520 = 0.75 °C/s

כשהמחמם כבוי (u = 0):

ΔTΔt = 20 − 4520 = −1.25 °C/s

חוצים שתי מעלות כלפי מעלה ושתי מעלות כלפי מטה, ולכן זמן המחזור tc הוא:

tc = 20.75 + 21.25
tc = 4.3 s
tc ≈ 4.3 s
שאלה 20.2: מרחיבים את התחום ל-43 ול-47. מה קורה לזמן המחזור, לתנודת הטמפרטורה ולחיי הממסר?

20.3 בקרה פרופורציונלית: אור קבוע עם PWM (Proportional Control)

עם PWM אין עוד צורך בקפיצה בין שני הקצוות: אפשר לתת מנה של פקודה שמתאימה לשגיאה. שגיאה גדולה פירושה פקודה גדולה, ושגיאה קטנה פירושה תיקון עדין. הדוגמה שלנו היא נורית ונגד תלוי אור שניצבים זה מול זה: רוצים תאורה קבועה על השולחן, בלילה וביום.

LED_EXT3V3LDRGND220 Ω10 kΩ
הנורית מאירה את הנגד התלוי אור, וכך המעגל נעשה חוג סגור דרך האוויר.
בקר פרופורציונלי ושגיאה מתמדת
u = Kp · e
ess = r − d1 + Kp · K
סמלפירושיחידה
Kpההגבר הפרופורציונלי—
essהשגיאה המתמדת, אחרי שהתהליך מתייצב—
dמה שמגיע לחיישן בלי הפקודה שלנו, כאן אור היום—
Kהגבר התהליך—

למה נשארת שגיאה? כי u = Kp · e: אילו השגיאה הייתה אפס, גם הפקודה הייתה אפס והנורית הייתה כבה. בקר פרופורציונלי צריך שגיאה כדי לתת פקודה.

דוגמה 20.3
r = 2 V, K = 2, Kp = 4, ואור היום נותן d = 0.3 V. מהי השגיאה המתמדת?
פתרון:
ess = r − d1 + Kp · K
ess = 2 − 0.31 + 4 · 2
ess = 0.189 V

הקריאה מתייצבת אפוא על 1.811 V. עם יום חזק יותר, d = 0.9 V, השגיאה קטנה ל-0.122 V והקריאה נעשית 1.878 V.

ess = 0.189 V
הנגד התלוי אור מדומה בתוך התוכנית: τ = 0.5 s. ב-t = 1.5 אור היום מתחזק, והתוכנית מעמעמת את הנורית בעצמה.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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 בתוכנית.
⚠
המעבדה אינה זמינה כעת. החומר בפרק שלם גם בלעדיה: ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר, או פותחים אותו ישירות מן הקישור שמתחת למסגרת.
https://fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch20_light
פותחים את המעבדה בדף נפרד ↗ fahed-lab.com/books/micropython/lab/mpy_lab?prog=ch20_light
שאלה 20.3: מעלים את Kp ל-9 בדוגמה. כמה תהיה השגיאה המתמדת? ולמה לא מעלים אותו בלי גבול?

20.4 מהירות המנוע: PWM עם אנקודר (Speed Control with an Encoder)

הפעלנו מנוע ב-PWM בסעיף 7.8, אבל זה היה חוג פתוח: כשהעומס גדל המהירות יורדת ואנחנו לא יודעים. כדי לסגור את החוג צריך למדוד את המהירות, וזו עבודתו של האנקודר: דיסק שמותקן על הציר ונותן מספר קבוע של פולסים בכל סיבוב.

+VMMLED_EXTRBאנקודרדיסק וחיישןצירBTND
המעגל מסעיף 7.8 עם האנקודר. מחברים יחד את הזנת המנוע ואת הארקה של הלוח, ולוקחים את יציאת האנקודר במתח שמתאים ללוח.
מהירות ממספר הפולסים
n = 60 · NP · T
Δn = 60P · T
סמלפירושיחידה
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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
pulses= 50  rpm=150
pulses= 50  rpm=150
pulses= 25  rpm=75
pulses= 25  rpm=75
אזהרה: עוצרים את הפסיקות לרגע סביב קריאת המונה ואיפוסו. אילו הגיע פולס בין שתי השורות, הוא היה הולך לאיבוד. במהירויות גבוהות מאוד הפולסים עוקבים מהר יותר ממה שהפסיקה מספיקה לטפל בו, ולכן בוחרים אנקודר עם פחות פולסים.
דוגמה 20.4
P = 40 וחלון T = 0.1 s. מהו ההפרש הקטן ביותר במהירות שאנחנו רואים?
פתרון:
Δn = 60P · T
Δn = 6040 · 0.1
Δn = 15 rpm

לכן הקריאה קופצת בצעדים של 15 rpm. חלון ארוך יותר נותן קריאה מדויקת יותר, אבל החוג נעשה איטי יותר.

Δn = 15 rpm

סוגרים את החוג בבקר פרופורציונלי. גם המנוע כאן מדומה, עם קבוע זמן 1 s ומהירות סופית 300 rpm בפקודה מלאה, והפולסים שלו מחושבים ממהירותו:

הרצוי הוא 150 rpm. ב-t = 4 העומס גדל.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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.4: רוצים לראות הפרש של 5 rpm באנקודר עם P = 40. מה אורך החלון?

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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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.5: מהם רוחב הפולס ומחזור העבודה ב-90 מעלות, בערכי ברירת המחדל בספרייה, 500 µs ו-2500 µs?

20.6 בקר PID (PID Controller)

הבקר הזה מחבר שלושה איברים, וכל איבר מסתכל על השגיאה בזמן אחר: הפרופורציונלי מסתכל על ההווה, האינטגרלי על העבר, והדיפרנציאלי על המגמה.

בקר PID
u = Kp · e + Ki · ∫e dt + Kd · dedt
Ik = Ik−1 + Ki · ek · Ts
uk = Kp · ek + Ik − Kd · yk − yk−1Ts
סמלפירושיחידה
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 out
טעות נפוצה: שימו לב ל-if out > self.hi: כשמגיעים לגבול המוצא לא שומרים את הסכום החדש. אילו שמרנו אותו, הוא היה ממשיך לגדול בזמן שהמחמם בהספק מלא, ומתרוקן רק אחרי שהטמפרטורה עוברת את היעד. זו התרוממות האינטגרל. במודל שלנו: עם ההגנה הטמפרטורה מגיעה ל-45.5 מעלות, ובלעדיה ל-54.9 מעלות.
שלושה בקרים על הארגז המדומה. העמודה האחרונה מודדת את תנועת הפקודה בכל צעד כשמוסיפים לקריאה רעש של ±0.1 מעלה.
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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
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
030609012020304050tT[s][°C]PPIr
תגובת P ו-PI מ-20 ל-45 מעלות. הקו המקווקו הוא הרצוי.

הטבלה שהודפסה מסכמת: PI מסלק את השגיאה המתמדת. התהליך שלנו מסדר ראשון, ולכן האיבר הדיפרנציאלי אינו מועיל בו: החריגה גדלה מעט כי הוא מוציא את הפקודה מגבולה מוקדם יותר, וכך הסכום מתחיל לצבור מוקדם יותר, ותנועת הפקודה גדלה מאוד בגלל הרעש. לכן בדרך כלל מתחילים ב-PI, ומוסיפים Kd רק אם התהליך איטי ומתעכב והחיישן נקי.

כיוונון איכותני:

  1. מציבים Ki = 0 ו-Kd = 0, ומעלים את Kp עד שהתגובה מהירה, לפני שמתחילה התנודה, ואחר כך מורידים אותו מעט.
  2. מעלים את Ki בהדרגה עד שהשגיאה המתמדת נעלמת בזמן סביר. אם החריגה גדלה, מורידים אותו.
  3. מוסיפים Kd מעט בלבד אם נשארת חריגה גדולה, ועוקבים אחרי הפקודה: אם היא רועדת, מורידים אותו או מעבירים את הקריאה במסנן.
כשמעליםהמהירותהחריגהשגיאה מתמדתהשפעת הרעש
Kpגדלהגדלהקטנהגדלה
Kiגדלהגדלהנעלמתמעטה
Kdמעטהקטנה בגבולות מסוימים, ולא תמידלא משתנהגדלה מאוד
הערה: זה כיוונון בניסוי ובתצפית. יש גם שיטות חישוב, ובהן שיטת Ziegler–Nichols, והן מחוץ להיקף הספר.
שאלה 20.6: טמפרטורה מתייצבת תמיד חצי מעלה מתחת ליעד, ואינה מתנדנדת. איזה הגבר משנים?

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))
ESP32 + Pico W · MicroPython 1.29 · simulated board
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
03060901202030405060tT[s][°C]1 s5 s20 s
אותו בקר בשלושה זמני דגימה. בזמן הגדול ביותר הטמפרטורה נשארת בתנודה מתמדת.

התנאי בדוגמה הזאת הוא a · (1 + Kp · K) < 2. עם Kp · K = 4 מקבלים a < 0.4, ומכאן Ts < 10.2 s. זה מה שרואים במספרים: ב-10 s אנחנו על שפת הגבול, והתנועה דועכת לאט מאוד, וב-20 s היא אינה דועכת לעולם.

בפועל לא מתקרבים לגבול. לוקחים את הכלל הנפוץ: עשר דגימות לפחות בקבוע זמן אחד, Ts ≤ τ/10. אין טעם להגזים גם בכיוון ההפוך: הטמפרטורה בארגז שלנו אינה משתנה בחלקיק שנייה, ובדגימות רבות מדי ההפרש בין שתי הקריאות באיבר הדיפרנציאלי נעשה רעש בלבד, וחלון האנקודר מצטמצם ודיוקו מידרדר, כפי שחישבנו ב-20.4.

התהליךקבוע זמן משוער בדוגמהזמן דגימה מתאים
הטמפרטורה בארגז שלנו20 s2 s או פחות
נורית ונגד תלוי אור0.5 s50 ms או פחות
המנוע שלנו1 s100 ms או פחות
טעות נפוצה: המשוואות מניחות זמן דגימה קבוע. time.sleep(0.1) לבדה אינה נותנת אותו, כי זמן החישוב וההדפסה מתווסף אליה. לכן מחשבים את מועד כל צעד ממועד הצעד הקודם בעזרת ticks_add, כמו בתוכנית הבאה. אפשר גם להשתמש בטיימר או במשימת asyncio מפרק 19.

נאגד עכשיו את כל מה שלמדנו: מנוע מדומה, פולסים של אנקודר, בקר PI מן הספרייה שלנו, וצעד קבוע:

התוכנית המסכמת, והעומס גדל ב-t = 6. השוו אותה לתוכנית של 20.4.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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 לפני העומס ואחריו. השגיאה המתמדת נעלמה, ונשארת רק תנועה קטנה בגודל צעד של האנקודר. מחזור העבודה עלה מעצמו כדי לפצות על העומס, וזו עבודתו של חוג סגור.

שאלה 20.7: לטמפרטורה קבוע זמן τ = 60 s. האם צריך דגימה כל 10 ms?

עיקרי הפרק:

סיכום הכללים בפרק:

הכללהנוסחה
שגיאהe = r − y
תהליך מסדר ראשוןTk+1 = Tk + a · (Ta + K · uk − Tk)
השגיאה המתמדת של בקר פרופורציונליess = r − d1 + Kp · K
מהירות מן האנקודרn = 60 · NP · T
בקר PIDu = 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زمن العيّنة
התשובות הסופיות לשאלות הפרק
20.1 r היא הטמפרטורה שכוונה, y היא קריאת חיישן המים, המפעיל הוא הממסר וגוף החימום, וההפרעה היא למשל כניסת מים קרים כשפותחים ברז.
20.2 זמן המחזור מוכפל לכ-8.5 s, ולכן ההחלפות מצטמצמות בחצי וחיי הממסר מתארכים, אבל גם התנודה מוכפלת, ל-4 מעלות.
20.3 ess = 0.089 V. אבל הגבר גדול מגדיל את הרעש, ועם השהיה בחוג התהליך מתחיל להתנדנד, כפי שנראה בסעיף 20.7.
20.4 T = 6040 · 5 = 0.3 s.
20.5 רוחב הפולס 1500 µs, וערך duty_u16 הוא 4915 מתוך 65535. שני הגבולות האלה נפוצים, ומשתנים מדגם לדגם.
20.6 מעלים את Ki, או מוסיפים אותו אם הוא אפס: השגיאה המתמדת היא עבודתו של האיבר האינטגרלי.
20.7 לא. מספיק Ts ≤ 6 s. דגימה כל 10 ms אינה נותנת מידע חדש, ומבזבזת את זמנו של המיקרו-בקר.

שאלות לתרגול:

רמה ראשונה — בסיסית
1.
r = 60 ו-y = 57. מהי השגיאה? ומה אם y = 62?
תשובות: e = 3, ואחר כך e = −2: סימן שלילי פירושו שאנחנו מעל הרצוי.
2.
ציירו את תרשים המלבנים של מגהץ עם תרמוסטט, וקראו לכל מלבן בשמו.
תשובות: הכפתור נותן r, אחריו ההשוואה, אחריה המפסק התרמי בתפקיד מפעיל, הלוחית היא התהליך, וחיישן הטמפרטורה מחזיר את y להשוואה. וההפרעה היא מגע עם בד לח.
3.
Kp = 3 ו-K = 2 ו-r − d = 1.4. מהי השגיאה המתמדת?
תשובות: ess = 1.41 + 6 = 0.2.
רמה שנייה — יישום
4.
אנקודר עם P = 100 וחלון T = 0.2 s. ספרנו 40 פולסים. מהי המהירות? ומהו ההפרש הקטן ביותר שאנחנו רואים?
תשובות: n = 60 · 40100 · 0.2 = 120 rpm, ו-Δn = 3 rpm.
5.
בארגז של הפרק מצמצמים את הספים ל-44.5 ול-45.5. מהו זמן המחזור בקירוב? ממה צריך לחשוש?
תשובות: t = 10.75 + 11.25 = 2.1 s. עם דגימה כל שנייה התחום צר יותר ממה שאנחנו מסוגלים לעקוב אחריו, והממסר מתעייף.
6.
תהליך עם τ = 5 s ובמוצא החיישן יש רעש. מהו זמן הדגימה המתאים? באיזה בקר מתחילים?
תשובות: Ts ≤ 0.5 s, ומתחילים ב-PI כי האיבר הדיפרנציאלי מגדיל את הרעש.
רמה שלישית — העמקה
7.
(א) למה כותבים בספרייה kd * slope על הקריאה ולא על השגיאה?
(ב) מה קורה בקריאה הראשונה?
תשובות: (א) כי שינוי פתאומי של r גורם להפרש שגיאה גדול מאוד, והפקודה קופצת. הקריאה אינה קופצת. (ב) self.last ריק, ולכן מחשיבים את השיפוע כאפס.
8.
(א) מתי קוראים ל-reset()?
(ב) מה קורה אם כיבינו את המחמם בכפתור והשארנו את החוג לצבור שגיאה, ואחר כך חזרנו?
תשובות: (א) בכל הפעלה מחדש של החוג. (ב) ההגנה בספרייה אינה רואה את הכפתור, ולכן הסכום מצטבר עד שהפקודה מגיעה לגבולה, ובחזרה מתקבלת חריגה גדולה. לכן עוצרים את update וקוראים ל-reset().
9.
בתוכנית של 20.7 רוצים Kp = 0.2. מהו זמן הדגימה המרבי המותר בתיאוריה?
תשובות: Kp · K = 8, ולכן a < 29, ו-Ts < 5 s. הכלל המעשי נשאר 2 s.
נוסחאון הספר: כל נוסחאות הפרק הזה מרוכזות, עם סרטוטיהן וטבלאות הסמלים שלהן, בפרק 25 — נוסחאון הספר.