برمجة المتحكّمات بلغة مايكروبايثون · الفصل 20 — أنظمة التحكّمמערכות בקרה · תכנות מיקרו-בקרים בשפת MicroPython⌂📚🏠
الفصل 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 (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 — لوحة القوانين الشاملة. (נוסחאון הספר)