الخرج التماثلي: PWM وDAC
في الفصل السابق قرأنا جهدا متغيّرا. والآن الاتجاه المعاكس: كيف نُخرج من طرف لا يعرف إلا 0 و3.3 V ضوءا خافتا، أو زاوية محرّك، أو نغمة؟ الجواب في أغلب الحالات خدعة زمنية اسمها PWM. وفي حالة واحدة فقط نجد مخرجا تماثليا حقيقيا، وعلى لوحة واحدة.
- نحسب الجهد المتوسّط وقيمة
duty_u16من دورة العمل. - نتحكّم في إضاءة ثنائي باعث للضوء بمقياس الجهد، ونحرّك محرّكا مؤازرا بمكتبة
servo.py. - نولّد نغمة بتغيير التردد، ونفرّق بين PWM وDAC.
- نحسب مرشّح RC الذي يعوّض غياب DAC في Pico W.
- نشغّل محرّك تيار مستمر عبر ترانزستور أو جسر H، دون تيار من طرف المتحكّم.
7.1 مبدأ تعديل عرض النبضة – עיקרון PWM (Pulse Width Modulation)
لا يستطيع الطرف الرقمي إعطاء 1.65 V. لكنه يستطيع التبديل بين 0 و3.3 V بسرعة كبيرة. إذا كان الحمل أبطأ من التبديل، كالعين أو المحرّك، فهو يشعر بالمتوسّط فقط.
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| ton | زمن النبضة العالية، وT = 1/f زمن الدورة | s |
| VH | الجهد العالي للطرف | V |
7.2 المخرج board.pwm والأمر duty_u16 – פקודות PWM
ننشئ المخرج بالدالة board.pwm(pin, freq). وكما في read_u16، يأخذ duty_u16 عددا بين 0 و65535 في كل اللوحات، فيكون N = D · 65535. فدورة عمل 30% تعني N = 19661.
| ESP32 | Pico W | |
|---|---|---|
| مجال التردد حسب التوثيق | 1 Hz – 40 MHz | 8 Hz – 62.5 MHz عند ساعة 125 MHz |
| عدد القنوات | 16 قناة، و8 ترددات مختلفة | 8 شرائح في كل منها قناتان |
| القيد | التردد الأعلى يقلّل دقة دورة العمل | قناتا الشريحة تشتركان في التردد |
LED_EXT. وعلى ESP32 يذكر التوثيق أن دورة العمل الافتراضية 50%، فنحدّدها مباشرة بعد إنشاء المخرج.import board
import time
led = board.pwm(board.LED_EXT, 1000)
for percent in (0, 25, 50, 100):
duty = percent * 65535 // 100
led.duty_u16(duty)
v = duty * 3.3 / 65535
print("%3d%% duty_u16 = %5d Vavg = %.3f V" % (percent, duty, v))
time.sleep(1)0% duty_u16 = 0 Vavg = 0.000 V 25% duty_u16 = 16383 Vavg = 0.825 V 50% duty_u16 = 32767 Vavg = 1.650 V 100% duty_u16 = 65535 Vavg = 3.300 V
7.3 تعتيم الضوء بمقياس الجهد – עמעום נורית בפוטנציומטר
نجمع الآن الفصلين: board.analog_in من الفصل 6 يعطي 0–65535، وduty_u16 يأخذ 0–65535، فلا حاجة إلى أي تحويل. ومقاومة التوالي تحدّد التيار. بجهد أمامي نموذجي 2 V تعطي 330 Ω تيارا قدره 3.9 mA، وهي قيمة صغيرة وآمنة للطرف.
import board
import time
pot = board.analog_in(board.POT)
led = board.pwm(board.LED_EXT, 1000)
while True:
raw = pot.read_u16()
led.duty_u16(raw)
print("duty_u16 = %5d (%3.0f%%)" % (raw, raw * 100 / 65535))
time.sleep(1)duty_u16 = 0 ( 0%) duty_u16 = 32776 ( 50%) duty_u16 = 65535 (100%) ...
7.4 المحرّك المؤازر – סרבו (Servo)
المحرّك المؤازر (סרבו) لا يقرأ الجهد المتوسّط، بل عرض النبضة (רוחב פולס). نرسل نبضة كل 20 ms، أي بتردد 50 Hz. والعرض النموذجي بين 0.5 ms و2.5 ms يقابل زاوية بين 0° و180°. هذه قيم نموذجية تختلف بين الطرازات، فنراجع ورقة بيانات المحرّك.
ومنها قيمة duty_u16: N = tpT · 65535. فلمنتصف المجال: N = 1.520 · 65535 = 4915.
نضع هذا الحساب في مكتبة نعيد استعمالها، ونعطيها كائن PWM جاهزا، فتبقى مستقلة عن اللوحة:
# servo.py - a hobby servo on any PWM output, the same on every board.
# (c) 2026 Fahed Ghanem - MIT License - fahed-lab.com
PERIOD_US = 20000
class Servo:
def __init__(self, pwm, min_us=500, max_us=2500):
self.pwm = pwm
self.min_us = min_us
self.max_us = max_us
pwm.freq(50)
def pulse(self, us):
us = max(self.min_us, min(self.max_us, us))
self.pwm.duty_u16(int(us * 65535 / PERIOD_US + 0.5))
def angle(self, deg):
deg = max(0, min(180, deg))
span = self.max_us - self.min_us
self.pulse(self.min_us + span * deg / 180)
def off(self):
self.pwm.duty_u16(0)import board
import time
from servo import Servo
arm = Servo(board.pwm(board.SERVO, 50))
for deg in (0, 90, 180):
arm.angle(deg)
print("angle %3d duty_u16 = %d" % (deg, arm.pwm.duty_u16()))
time.sleep_ms(500)angle 0 duty_u16 = 1638 angle 90 duty_u16 = 4915 angle 180 duty_u16 = 8192
min_us وmax_us.7.5 الطنّان والتردد – זמזם ותדר (Buzzer)
حتى الآن غيّرنا دورة العمل وثبّتنا التردد. في الطنّان (זמזם) السلبي نفعل العكس: التردد هو النغمة، ودورة العمل 50% تكفي. ونغمات السلّم الموسيقي تتضاعف كل 12 درجة، فيعطينا f = 440 · 2n/12 النغمة التي تبعد n درجة عن A4. أما الطنّان الفعّال فله مذبذب داخلي، ويطلق نغمة واحدة بمجرد تغذيته، فلا يفيده تغيير التردد.
import board
import time
buzzer = board.pwm(board.BUZZER, 440)
for n, name in ((0, "A4"), (3, "C5"), (7, "E5")):
f = int(440 * 2 ** (n / 12) + 0.5)
buzzer.freq(f)
buzzer.duty_u16(32768)
print(name, f, "Hz")
time.sleep_ms(300)
buzzer.duty_u16(0)A4 440 Hz C5 523 Hz E5 659 Hz
7.6 المحوّل الرقمي التماثلي – ממיר D/A, רק ב-ESP32 (DAC)
حسب التوثيق، يوجد في ESP32 على الطرفين 25 و26، ويأخذ write عددا من 8 بتّات، بين 0 و255. ولا يوجد في Pico W، ولا في Raspberry Pi 4/5. ونسخ أخرى من شرائح ESP32 تختلف في ذلك، فنراجع توثيقها.
وجهد المخرج V = N255 · Vref، حيث N العدد المكتوب وVref نحو 3.3 V.
الخطوة الواحدة هنا 12.9 mV، أي أكبر بكثير من خطوة المحوّل التماثلي الرقمي في الفصل 6.
لنتأكد، نوصل الطرف 25 بالطرف 34 بسلك، ونقرأ ما كتبناه بالمحوّل الذي نعرفه:
import board
import time
from machine import DAC, Pin
dac = DAC(Pin(board.DAC_OUT))
adc = board.analog_in(board.POT)
for n in (0, 128, 255):
dac.write(n)
time.sleep_ms(10)
v = adc.read_u16() * 3.3 / 65535
print("write %3d read back %.3f V" % (n, v))write 0 read back 0.000 V write 128 read back 1.657 V write 255 read back 3.300 V
7.7 على Pico W: مرشّح RC – PWM עם מסנן RC
ما دام Pico W بلا محوّل رقمي تماثلي، نمرّر موجة PWM في مرشّح (מסנן) من مقاومة ومكثّف. المكثّف يشحن ويفرغ قليلا في كل دورة، فيبقى الجهد عليه قرب المتوسّط مع تموّج (אדווה) صغير.
| الرمز | المعنى | وحدة القياس |
|---|---|---|
| ΔV | التموّج عند تردد f، وهو أكبر عند D = 0.5. ونريد fc أقل بكثير من f | V |
الثمن هو البطء: زمن الاستقرار نحو 5RC، أي 50 ms.
import board
out = board.pwm(board.LED_EXT, 20000)
for v in (0.5, 1.0, 1.65, 2.5):
out.duty_u16(int(v / 3.3 * 65535 + 0.5))
print("%.2f V duty_u16 = %5d" % (v, out.duty_u16()))0.50 V duty_u16 = 9930 1.00 V duty_u16 = 19859 1.65 V duty_u16 = 32768 2.50 V duty_u16 = 49648
| PWM | DAC | |
|---|---|---|
| على الطرف | موجة مربّعة: 0 أو 3.3 V | جهد ثابت حقيقي |
| الدقة | 16 بتّا في الأمر، والفعلية حسب التردد | 8 بتّات |
| اللوحات | ESP32 وPico W وPi 4/5 | ESP32 فقط |
| يناسب | الضوء، المحرّك، المحرّك المؤازر، النغمة | جهد مرجع، إشارة صوتية بسيطة |
7.8 محرّك التيار المستمر – מנוע DC (DC Motor)
المحرّك يحتاج إلى تيار أكبر بكثير مما يعطيه طرف المتحكّم، ولا نوصله بالطرف أبدا. الطرف يتحكّم في مفتاح فقط، وهو ترانزستور (טרנזיסטור)، وتيار المحرّك يأتي من تغذية منفصلة. وعندما ينفتح المفتاح يحاول ملف المحرّك إبقاء تياره، فيولّد جهدا عاليا قد يتلف الترانزستور. لذلك نضع بالتوازي مع المحرّك ثنائي الدوران الحرّ (דיודת חופש)، فيجد التيار طريقا يتلاشى فيه.
نختار القيمة القياسية 680 Ω، فيكون IB = 3.82 mA. ونتحقق من حد الطرف في ورقة بيانات اللوحة.
الترانزستور يدير المحرّك في اتجاه واحد. ولعكس الاتجاه نحتاج إلى جسر H (גשר H): أربعة مفاتيح، إغلاق S1 وS4 يمرّر التيار في اتجاه، وإغلاق S3 وS2 يعكسه. ولا يجوز إغلاق مفتاحين في جهة واحدة معا، لأن ذلك قصر مباشر على التغذية. في الشرائح الجاهزة نرسل PWM إلى أحد مدخلي الشريحة و0 إلى الآخر.
board لمدخلي الشريحة. نمرّ بالصفر قبل العكس ليتباطأ المحرّك ولا يكبر التيار.import board
import time
in1 = board.pwm(board.LED_EXT, 1000)
in2 = board.pwm(board.BUZZER, 1000)
def drive(percent):
duty = abs(percent) * 65535 // 100
a = duty if percent > 0 else 0
b = duty if percent < 0 else 0
in1.duty_u16(a)
in2.duty_u16(b)
print("speed %4d%% in1 = %5d in2 = %5d" % (percent, a, b))
for speed in (60, 100, 0, -50, 0):
drive(speed)
time.sleep_ms(500)speed 60% in1 = 39321 in2 = 0 speed 100% in1 = 65535 in2 = 0 speed 0% in1 = 0 in2 = 0 speed -50% in1 = 0 in2 = 32767 speed 0% in1 = 0 in2 = 0
7.9 على Raspberry Pi 4/5
لا توجد machine، ولا مخرج تماثلي. المكتبة gpiozero تعطي الصنف PWMLED، وخاصيته value بين 0 و1، وتردده الافتراضي 100 Hz حسب توثيقها. وللقراءة من مقياس جهد نعود إلى ADS1115 في الفصل 6. لم يُشغَّل هذا القطع عند بناء الكتاب.
from gpiozero import PWMLED from signal import pause led = PWMLED(18) led.value = 0.25 pause()
أهمّ أفكار الفصل:
- الطرف الرقمي لا يعطي إلا مستويين، وPWM يعتمد على حمل أبطأ من التبديل.
duty_u16يأخذ 0–65535 مثلread_u16، فيتصل مقياس الجهد بالضوء مباشرة.- المحرّك المؤازر يقرأ عرض النبضة، والطنّان يقرأ التردد.
- DAC جهد حقيقي من 8 بتّات على ESP32 وحده، وعلى Pico W نستعمل PWM مع مرشّح RC.
- لا تيار حمل من طرف المتحكّم: ترانزستور أو جسر H، مع ثنائي الدوران الحرّ.
ملخّص قوانين الفصل:
| القانون | الصيغة |
|---|---|
| الجهد المتوسّط | Vavg = D · VH |
| قيمة duty_u16 | N = D · 65535 |
| مخرج DAC | V = N255 · Vref |
| تردد القطع | fc = 12π R C |
| التموّج | ΔV ≈ VH · D · (1 − D)f · R · C |
أوامر الفصل:
| الأمر | عمله |
|---|---|
| board.pwm(pin, freq) | مخرج PWM على طرف. |
| p.duty_u16(n) | دورة العمل، من 0 إلى 65535. |
| p.freq(f) | تغيير التردد. |
| Servo(pwm).angle(deg) | زاوية المحرّك المؤازر، من مكتبة servo.py. |
| DAC(Pin(25)).write(n) | جهد تماثلي من 0 إلى 255، على ESP32 فقط. |
قاموس المصطلحات – מילון מונחים:
| بالعربية | بالعبرية | English |
|---|---|---|
| تعديل عرض النبضة | אפנון רוחב פולס | Pulse Width Modulation |
| دورة العمل | מחזור עבודה | Duty Cycle |
| عرض النبضة | רוחב פולס | Pulse Width |
| المحرّك المؤازر | סרבו | Servo Motor |
| المحوّل الرقمي التماثلي | ממיר ספרתי לאנלוגי | Digital-to-Analog Converter |
| المرشّح | מסנן | Filter |
| التموّج | אדווה | Ripple |
| ثنائي الدوران الحرّ | דיודת חופש | Flyback Diode |
| جسر H | גשר H | H-Bridge |
أسئلة للتمرين:
duty_u16 لنبضة محرّك مؤازر عرضها 2 ms؟raw * raw // 65535. ما ناتجه عند 50% من المجال؟ وما دورة العمل الناتجة؟duty_u16 بين 0° و180° في servo.py؟(ب) كم خطوة للدرجة الواحدة؟