הלו טייני #7: אותות PWM בחומרה

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


למי ששכח: מה זה PWM

אות PWM – אפנון רוחב פולס, באנגלית Pulse Width Modulation – הוא אות שמזכיר גל ריבועי, אך בעוד שבגל ריבועי "קלאסי" כל מחזור (מפסגה לפסגה) מתחלק שווה בשווה בין ה-HIGH ל-LOW, באות PWM נתחי הזמן היחסיים של הערכים האלה יכולים להשתנות. אות PWM עם "מחזור פעילות" (באנגלית Duty Cycle) של 50% הוא גל ריבועי, במחזור פעילות של 25% ה-HIGH יהווה רק רבע מזמן המחזור וכך הלאה.

משך כל מחזור שלם כזה, או ה"תדר" של האות, יכול להיות מה שרק נרצה, במגבלות החומרה כמובן. בארדואינו, ה-PWM הסטנדרטי שמופק באמצעות פקודת analogWrite הוא בתדר של כ-490 הרץ, כלומר כשתי אלפיות שניה לכל מחזור. תדרים שונים מתאימים למטרות שונות; בפוסט זה נשתמש ב-PWM למטרה מוכרת ומצוינת להדגמות: שליטה בבהירות הנתפסת של נוריות LED.

הגדרות PWM בטייני – התיאוריה

אם אני זוכר נכון, כל מיקרו-בקר שראיתי אי-פעם כלל אפשרות בחומרה להפקת אותות PWM. ליצור אות שכזה בתוכנה זה לא מסובך – אפילו קוד Blink הבסיסי ביותר הוא הרי PWM איטי במחזור פעילות של 50% – אבל בתוכנה קשה להבטיח תזמון מדויק של הגל, ואם המיקרו-בקר צריך לבצע גם פעולות אחרות באותו זמן זה הופך לבלתי אפשרי כמעט. לפעמים הדיוק לא חשוב, למשל בפרויקט "אש באח" – אף צופה לא יבחין בסטיות קטנות בתזמון, ואפילו אם כן זה פשוט ייראה כמו חלק מהאפקט. במקרים אחרים, הדיוק קריטי ואז מומלץ להשתמש בחומרה (וצריך לכתוב ככה פחות קוד). גם analogWrite, אפרופו, מנצלת את מודול החומרה ל-PWM ובגלל זה היא מוגבלת לששת הפינים של "פלט אנלוגי" של הארדואינו.

בניגוד לשמועות שמתרוצצות פה ושם ברשת, לטייני יש לא שניים אלא שלושה פיני "פלט אנלוגי" בלתי תלויים: PB0 שמקושר לסף A של הטיימר Timer0, פין PB4 שמקושר לסף B של Timer1, ופין PB2 שמקושר, למרבה הצער, גם לסף B של Timer0 וגם לסף A של Timer1. אם תסתכלו בעיון ב-datasheet תראו שגם פין PB3 מסוגל בעיקרון להוציא אות PWM, אך רק כמשלים ("תמונת ראי") של PB4, כך שאי אפשר לחלץ ממנו בחומרה אות PWM רביעי בלתי-תלוי.

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

תכל'ס

נתחיל ב-Timer0. אופן הפעולה שמתאים לנו ומעניין אותנו כעת נקרא Fast PWM Mode, וכדי להגיע אליו צריך לקבוע את הביטים WGM0, WGM01, WGM02, שמפוזרים בשני הרגיסטרים TCCR0A ו-TCCR0B לערך 3 (בבינארי 011).

בנוסף, צריך להגיד לחומרה מה לעשות בעת התאמה של המונה ל-A או ל-B, וזה נעשה בתוך הרגיסטר TCCR0A, בעזרת הביטים COM0A0, COM0A1 ל-A ו-COM0B0, COM0B1 ל-B. בשני המקרים, הערך הנוח לנו כרגע בכל זוג ביטים הוא 2 (בבינארי 10), שגורם לפין המתאים לתת HIGH בתחילת כל ספירה של הטיימר, ו-LOW ברגע שיש התאמה. לא לשכוח להגדיר את הפינים האלה כפלט באמצעות הרגיסטר DDRB. הדבר היחיד שנותר הוא לקבוע את הספים עצמם, שמגדירים לנו בעצם את מחזור הפעילות. אלה הרגיסטרים OCR0A ו-OCR0B.

הנה קוד לדוגמה, שמריץ אותות PWM משתנים ונפרדים בפינים PB0 ו-PB1. שימו לב להגדרה של Prescaler להאטה של הטיימר (במקרה זה פי 256); הוספתי אותה כי מסתבר שבמהירות גבוהה מדי, נוריות ה-LED לא עומדות בקצב ונראות כבויות לאורך כל הזמן. הפיוזים של השעון כוונו מראש למתנד פנימי במהירות 8MHz.

#define F_CPU 8000000UL

#include <avr/io.h>
#include <util/delay.h>

int main(void) {

    DDRB = 3; // PB0, PB1 outputs
    OCR0A = 0; // A threshold
    OCR0B = 128; // B Threshold
    TCCR0B = 4; // Clock/256 prescaler
    // Fast PWM mode; A+B 0 on match, 1 at BOTTOM
    TCCR0A = 0xA3; // Binary 10 10 00 11
    
    while(1) {
       _delay_ms(7); 
       // Change duty cycle
       OCR0A++; 
       OCR0B++; 
    }
 }

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

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

#define F_CPU 8000000UL

#include <avr/io.h>
#include <util/delay.h>

int main(void) {

    DDRB = 3 | (1 << PB4); // PB0/1/4 outputs
    // Timer0
    OCR0A = 0; // A threshold
    OCR0B = 85; // B Threshold
    TCCR0B = 4; // Clock/256 prescaler
    // Fast PWM mode; A+B 0 on match, 1 at BOTTOM
    TCCR0A = 0xA3; 
      
    // Timer1
    TCCR1 = 9; // Clock/256 prescaler
    // Pulse Width Modulator B Enable, 
    // 0 on match, 1 at BOTTOM
    GTCCR = (1 << PWM1B) | (1 << COM1B0); 
    OCR1B = 170; // B Threshold
	  
    while(1) {
      _delay_ms(7);
      // Change duty cycle
      OCR0A++; 
      OCR0B++;
      OCR1B++;		
    }
}

ומה הלאה?

כפי שאפשר לראות בסרטון למעלה, זה עובד יפה – וכדי לקבוע מחזור פעולה ספציפי כל מה שעלינו לעשות הוא לתת ערך תואם לרגיסטר הסף המתאים ולמחוק את פקודות ה-++. אבל מה קורה כאשר נותנים לרגיסטרים את הערכים הקיצוניים, 0 או 255? לפי ה-datasheet, ערך מקסימלי ייתן אמנם את התוצאה הצפויה – אות אחיד, אבל ערך מינימום יגרום ל"קפיצת" מתח קצרצרה בתחילת כל מחזור. כדי לקבל אות PWM עם duty cycle אמתי של 0% צריך למעשה להשבית את הפעלת הפין על ידי כתיבת 0 לביטים המתאימים ברגיסטרים – למשל COM0A0 ו-COM0A1 עבור פין PB0.

נקודה חשובה נוספת היא שאפשר להגדיר, בנוסף לשינוי המתח בפינים, גם פסיקה "רגילה" לטיימר, וכך לנצל אותו למטרות נוספות. באופן כזה, למשל, הפונקציה millis של ארדואינו יכולה להמשיך לעבוד במקביל להפקת אות PWM על פינים שמחוברים לטיימר 0. עם זאת, ספריות ארדואינו רבות משנות את הקצב של הטיימרים או את השימוש שהם עושים בספי A ו-B, ועלולות לשבש את הפעולה של analogWrite (או להיפך). ראו הוזהרתם.

להרשמה
הודע לי על
5 Comments
מהכי חדשה
מהכי ישנה לפי הצבעות
Inline Feedbacks
הראה את כל התגובות

הי, כמעט כל אתר שאני בודק בו מחזיק בעיקר (אם לא רק) את הטיני85, מה הסיבה לזה? הרי הוא לא נראה מועיל במיוחד אפילו לעומת אטיני אחרים כמו ה804 וה1604.

אחלה אתר הבית הלבן

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