תכנות מיקרו-בקרים בשפת MicroPython · פרק 7 — מוצא אנלוגי: PWM ו-DAC⌂📚🏠
פרק 7

מוצא אנלוגי: PWM ו-DAC

Analog Output
3 שעות לימוד

בפרק הקודם קראנו מתח משתנה. עכשיו הכיוון ההפוך: איך מוציאים מהדק שמכיר רק 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 במהירות גבוהה. אם העומס איטי מן המיתוג, כמו העין או מנוע, הוא מרגיש רק את הממוצע.

אפנון רוחב פולס (PWM): גל ריבועי בתדר קבוע, ואנחנו שולטים בזמן שבו הוא נשאר גבוה בכל מחזור.
D = 25%0.825 VD = 50%1.65 VD = 75%2.475 VT
שלושה מחזורי עבודה באותו תדר. הקו המקווקו הוא המתח הממוצע.
מחזור עבודה ומתח ממוצע
D = tonT
Vavg = D · VH
סמלפירושיחידה
tonזמן הפולס הגבוה, ו-T = 1/f הוא זמן המחזור s
VHהמתח הגבוה של ההדק V
דוגמה 7.1
מחזור עבודה 25% על הדק שמתחו 3.3 V. מהו המתח הממוצע?
פתרון:
Vavg = D · VH = 0.25 · 3.3 = 0.825 V
Vavg = 0.825 V
שאלה 7.1: אנחנו רוצים מתח ממוצע של 2.2 V מהדק שמתחו 3.3 V. מהו מחזור העבודה?

7.2 המוצא board.pwm והפקודה duty_u16

יוצרים את המוצא בפונקציה board.pwm(pin, freq). וכמו ב-read_u16, duty_u16 מקבל מספר בין 0 ל-65535 בכל הלוחות, ולכן N = D · 65535. מחזור עבודה של 30% פירושו N = 19661.

ESP32Pico W
תחום התדר לפי התיעוד1 Hz – 40 MHz8 Hz – 62.5 MHz בשעון 125 MHz
מספר הערוצים16 ערוצים, ו-8 תדרים שונים8 פרוסות, ובכל אחת שני ערוצים
מגבלהתדר גבוה מקטין את דיוק מחזור העבודהשני ערוצי הפרוסה חולקים את התדר
אזהרה: הנורית שעל לוח Pico W מחוברת לשבב האלחוטי ולא להדק PWM רגיל, ולכן אי אפשר להשתמש בה בפרק הזה. במקומה משתמשים ב-LED_EXT. וב-ESP32 התיעוד אומר שמחזור העבודה ברירת המחדל הוא 50%, ולכן קובעים אותו מיד אחרי יצירת המוצא.
ההדפסה מחשבת את הממוצע מן הנוסחה. נסו אותו במעבדה: prog=pwm_duty.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
  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, ערך קטן ובטוח להדק.

3V3POTGNDLED_EXT330 Ω10 kΩ
הפוטנציומטר על הכניסה האנלוגית, והנורית על מוצא ה-PWM.
המתח על הפוטנציומטר משתנה בכל שנייה.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
duty_u16 =     0  (  0%)
duty_u16 = 32776  ( 50%)
duty_u16 = 65535  (100%)
...
▶ניסוי חי — עמעום נורית בפוטנציומטרהזיזו את הפוטנציומטר, וצפו בבהירות.
⚠
המעבדה אינה זמינה כעת. החומר בפרק שלם גם בלעדיה: ממשיכים לקרוא, וחוזרים לניסוי מאוחר יותר, או פותחים אותו ישירות מן הקישור שמתחת למסגרת.
https://fahed-lab.com/books/micropython/lab/mpy_lab?prog=pwm_dim
פותחים את המעבדה בדף נפרד ↗ fahed-lab.com/books/micropython/lab/mpy_lab?prog=pwm_dim

7.4 סרבו (Servo)

סרבו אינו קורא את המתח הממוצע אלא את רוחב הפולס. שולחים פולס כל 20 ms, כלומר בתדר 50 Hz. הרוחב האופייני, בין 0.5 ms ל-2.5 ms, מתאים לזווית בין 0° ל-180°. אלה ערכים אופייניים שמשתנים בין דגמים, ולכן בודקים בדף הנתונים של המנוע.

0203.3tV[ms]1.5 ms
פולס של 1.5 ms כל 20 ms.
SERVOGNDServoPWM+5 V
הזנה חיצונית עם אדמה משותפת.

ומכאן הערך של duty_u16: N = tpT · 65535. לאמצע התחום: N = 1.520 · 65535 = 4915.

את החישוב הזה שמים בספרייה שאפשר להשתמש בה שוב, ונותנים לה אובייקט PWM מוכן, וכך היא נשארת בלתי תלויה בלוח:

הקובץ servo.py, בכל הלוחות
# 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)
אותם מספרי duty_u16 שבדוגמה.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
angle   0  duty_u16 = 1638
angle  90  duty_u16 = 4915
angle 180  duty_u16 = 8192
אזהרה: לא מזינים את הסרבו מההדק 3V3: זרם ההפעלה שלו גדול ממה שהלוח נותן, ועלול לאתחל אותו. משתמשים בהזנה חיצונית ומחברים את שתי האדמות יחד. ואם המנוע רועד בשני הקצוות, מצמצמים את התחום בעזרת הארגומנטים min_us ו-max_us.

7.5 זמזם ותדר (Buzzer)

עד עכשיו שינינו את מחזור העבודה והשארנו את התדר קבוע. בזמזם פסיבי עושים להפך: התדר הוא הצליל, ומחזור עבודה של 50% מספיק. צלילי הסולם המוזיקלי מוכפלים כל 12 חצאי טון, ולכן f = 440 · 2n/12 נותן את הצליל שמרוחק n חצאי טון מ-A4. לזמזם אקטיבי, לעומת זאת, יש מתנד פנימי, והוא משמיע צליל אחד כשמזינים אותו, ולכן שינוי התדר לא עוזר.

BUZZERGND
זמזם פסיבי בין ההדק ל-GND. זמזם בזרם גדול מפעילים דרך טרנזיסטור, כמו בסעיף 7.8.
משנים את התדר בפקודה freq, ומשתיקים את הזמזם במחזור עבודה 0.
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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
A4 440 Hz
C5 523 Hz
E5 659 Hz

7.6 ממיר ספרתי לאנלוגי, רק ב-ESP32 (DAC)

ממיר ספרתי לאנלוגי (DAC): מעגל שממיר מספר למתח קבוע אמיתי על ההדק, בלי מיתוג. שמו באנגלית DAC.

לפי התיעוד, ב-ESP32 הוא קיים בהדקים 25 ו-26, ו-write מקבל מספר של 8 סיביות, בין 0 ל-255. אין אותו ב-Pico W ולא ב-Raspberry Pi 4/5. גרסאות אחרות של שבבי ESP32 שונות בזה, ולכן בודקים בתיעוד שלהן.

מתח המוצא הוא V = N255 · Vref, כאשר N הוא המספר שנכתב ו-Vref הוא כ-3.3 V.

דוגמה 7.2
אנחנו רוצים 1.5 V. איזה מספר כותבים, ואיזה מתח מקבלים?
פתרון:
N = 1.53.3 · 255 ≈ 116
V = 116255 · 3.3 = 1.501 V

המדרגה כאן היא 12.9 mV, גדולה בהרבה ממדרגת הממיר האנלוגי לספרתי בפרק 6.

V = 1.501 V

כדי לוודא, מחברים בחוט את הדק 25 להדק 34, וקוראים את מה שכתבנו בממיר שאנחנו כבר מכירים:

ב-ESP32 בלבד. והמספרים כאן אידיאליים: בלוח אמיתי נוספת שגיאה של שני הממירים.
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))
ESP32 · MicroPython 1.29 · simulated board
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: PWM עם מסנן RC

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

PWMVout10 kΩ1 µF
מסנן RC על מוצא PWM.
1.653.3tV
במחזור ארוך מן המציאות, כדי שהאדווה תיראה: המתח עולה ומתייצב סביב הממוצע.
תכנון המסנן
fc = 12π R C
ΔV ≈ VH · D · (1 − D)f · R · C
סמלפירושיחידה
ΔVהאדווה בתדר f, והיא הגדולה ביותר כש-D = 0.5. רוצים ש-fc יהיה קטן בהרבה מ-f V
דוגמה 7.3
R = 10 kΩ, C = 1 µF ו-D = 0.5. מהו תדר הניתוק? ומהי האדווה ב-20 kHz?
פתרון:
fc = 12π · 104 · 10−6 = 15.9 Hz
ΔV20k = 3.3 · 0.2520000 · 10−2 = 4.1 mV

המחיר הוא האיטיות: זמן ההתייצבות הוא כ-5RC, כלומר 50 ms.

fc = 15.9 Hz
מה שכותבים כדי לקבל את המתח הרצוי אחרי המסנן. הסימולציה אינה כוללת את המסנן, ולכן אינה מודדת את התוצאה.
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()))
ESP32 + Pico W · MicroPython 1.29 · simulated board
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 אינו ממיר ספרתי לאנלוגי. על ההדק עצמו יש רק 0 ו-3.3 V. מכשיר מדידה איטי מציג את הממוצע, אבל הממיר בפרק 6 עלול לקרוא 0 או 3.3 V לפי רגע הקריאה. ואין למשוך זרם ממוצא המסנן, כי הנגד שלו נמצא בדרכו של הזרם והמתח יורד.
PWMDAC
על ההדקגל ריבועי: 0 או 3.3 Vמתח קבוע אמיתי
דיוק16 סיביות בפקודה, והדיוק בפועל לפי התדר8 סיביות
לוחותESP32, Pico W ו-Pi 4/5ESP32 בלבד
מתאים לאור, מנוע, סרבו, צלילמתח ייחוס, אות שמע פשוט

7.8 מנוע DC (DC Motor)

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

דוגמה 7.4
מנוע שזרמו 200 mA, ונניח הגבר מובטח β = 50 לדוגמה הזאת. מהו נגד הבסיס? (מתח הבסיס כ-0.7 V.)
פתרון:
IB = IMβ = 20050 = 4 mA
RB = 3.3 − 0.74 mA = 650 Ω

בוחרים את הערך הסטנדרטי 680 Ω, ואז IB = 3.82 mA. ובודקים את הגבול של ההדק בדף הנתונים של הלוח.

RB = 680 Ω
הערה: זו הנחה לתרגול. ברוויה אמינה β קטן בהרבה, ודפי נתונים מציינים רוויה לרוב ב-β = 10. למנוע כזה פירוש הדבר 20 mA מן ההדק, וזה הרבה. לכן לזרמים גדולים יותר משתמשים בטרנזיסטור MOSFET לרמה לוגית או בשבב דרייבר.

הטרנזיסטור מפעיל את המנוע בכיוון אחד. כדי להפוך כיוון צריך גשר H: ארבעה מתגים. סגירת S1 ו-S4 מעבירה זרם בכיוון אחד, וסגירת S3 ו-S2 הופכת אותו. אסור לסגור יחד שני מתגים בצד אחד, כי זה קצר ישיר בהזנה. בשבבים מוכנים שולחים PWM לאחת משתי כניסות השבב ו-0 לאחרת.

+VMMPWMRB
מתג NPN בצד האדמה, ושתי האדמות מחוברות יחד.
+VGNDS1S2S3S4M
גשר H: המתגים S1 ו-S4 סגורים.
שני הדקים של 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)
ESP32 + Pico W · MicroPython 1.29 · simulated board
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. קטע הקוד הזה לא הורץ בעת בניית הספר.

ב-Raspberry Pi 4/5
from gpiozero import PWMLED
from signal import pause

led = PWMLED(18)
led.value = 0.25
pause()

עיקרי הפרק:

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

הכללהנוסחה
המתח הממוצעVavg = D · VH
ערך duty_u16N = D · 65535
מוצא DACV = 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ثنائي الدوران الحرّ
גשר HH-Bridgeجسر H
התשובות הסופיות לשאלות הפרק
7.1 D = 66.7%.

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

רמה ראשונה — בסיס
1.
גל PWM בתדר 500 Hz ובמחזור עבודה 40%. מהו זמן המחזור, ומהו ton?
תשובות: T = 2 ms, ו-ton = 0.8 ms.
רמה שנייה — יישום
2.
מהו הערך של duty_u16 לפולס של סרבו ברוחב 2 ms?
תשובות: N = 6554.
3.
העין אינה מרגישה אור בצורה לינארית, ולכן משתמשים בריבוע הערך: raw * raw // 65535. מהי התוצאה ב-50% מן התחום? ומהו מחזור העבודה המתקבל?
תשובות: N = 16383, כלומר מחזור עבודה של כ-25%.
רמה שלישית — העמקה
4.
(א) כמה צעדים של duty_u16 יש בין 0° ל-180° ב-servo.py?
(ב) כמה צעדים יש למעלה אחת?
תשובות: (א) 8192 − 1638 = 6554. (ב) כ-36.4 צעדים.
5.
מסנן עם R = 10 kΩ ותדר PWM של 5 kHz. מהו ה-C הקטן ביותר שנותן אדווה שאינה עולה על 10 mV ב-D = 0.5?
תשובות: C ≥ 1.65 µF, ובוחרים את הערך הסטנדרטי הבא.
נוסחאון הספר: כל נוסחאות הפרק הזה מרוכזות, עם סרטוטיהן וטבלאות הסמלים שלהן, בפרק 25 — נוסחאון הספר.