אנחנו ממשיכים ללמוד את הפונקציות הבסיסיות של המיקרו-בקר הסיני הזול, והפעם נסתכל על פלט PWM בסיסי.
הפקת PWM במיקרו-בקרים, עד כמה שראיתי ושאני יודע, מתבצעת תמיד באותה שיטה: זהו בעצם מצב פעולה של טיימר, שבו מונה פנימי משנה אוטומטית את המצב של פין פלט בכל פעם שהוא מגיע לאחד משני ערכים (כאשר לרוב, אחד מהשניים הוא 0). סביב העיקרון הזה יש המון וריאציות, בהתאם לרמת התחכום של כל מיקרו-בקר; חלק מהווריאציות מוסיפות לנו גמישות בבחירת רזולוציה ותדר, אחרות מבצעות סנכרון בין מספר מונים כדי לאפשר בקרה מדויקת על מנועים, וכדומה. בפוסט זה נתמקד בהפקת ה-PWM הפשוטה ביותר.
המרכיבים
כמו בפוסט על פסיקת הטיימר, גם כאן נעבוד עם טיימר TIM2 כי הוא נחשב פשוט יותר, אף על פי שבהקשר של PWM ההבדל בינו לבין TIM1 לא אמור להיות משמעותי. כרגע, בשביל התרגיל, המטרה היא להפיק שני אותות PWM במקביל, בשני פינים שונים, עם Duty cycle שונה – אחד 75% והשני 25%.
לכל טיימר במיקרו-בקר שלנו יש ארבעה ערוצי PWM, ויש לנו גמישות מוגבלת מאוד בבחירת פיני הפלט עבורם. ב-TIM2, הבחירה הזו מוגדרת על ידי זוג הביטים TIM2_RM ברגיסטר AFIO_PCFR1 – רגיסטר שמיועד להגדרת כל מיני פונקציות אלטרנטיביות עבור פינים. הנה ההסבר הרלוונטי מה-Datasheet:

לדוגמה, הסתכלו בתמונה על השורה שמימין לכיתוב "RW", זו שמתחילה בתווים "01". היא אומרת שאם בחרנו את הערך "01" עבור זוג הביטים הזה, אז-
- ערוץ 1 (מסומן CH1) של הטיימר ישויך לפין PC5
- ערוץ 2 לפין PC2
- ערוץ 3 ל-PD2
- וערוץ 4 ל-PC1
הפירוט הזה, אגב, מתייחס לפורטים ברמת הסיליקון שבתוך השבב. הפינים שיהיו זמינים בפועל לשימוש תלויים במארז הספציפי (למשל, במארז SOIC-8 של ה-CH32V003, אין לנו בכלל גישה פיזית ל-PC5 או ל-PD2!) לכן, כשמתכננים מעגל, צריך לחשוב מראש כמה ערוצים נצטרך, ולבדוק בתשומת לב מאיפה נוכל לקבל אותם. יכול להיות שנהיה חייבים להיעזר בשני הטיימרים, ואז לא נוכל להשתמש בהם לצרכים אחרים. זיכרו שבסופו של דבר מדובר כאן על מיקרו-בקר קטן ולא מאוד משוכלל.
ההגדרה של TIM2_RM, בין אם ברירת המחדל או ערך אחר שנבחר, עדיין אינה מספיקה: צריך גם לקבוע מי מהפינים המשויכים יוציא פלט בפועל (הרי לא הגיוני שתמיד כל הארבעה יפעלו!) בנוסף, עלינו להגדיר את הפינים הרלוונטיים כפלט, וגם לחבר שעון לכל הפורטים הרלוונטיים – את זה ראינו בפוסט על ה-Blink. וכמובן, צריך להכין את הטיימר עצמו, ולקבוע עבור כל ערוץ את הסף שיקבע את ה-Duty cycle שלו. אז בואו נעשה את כל זה בצורה מסודרת.
המתכון
הנה שורות הקוד הדרושות כדי ליצור את אותות ה-PWM שהזכרתי קודם. בתוכנה ראויה לשמה נחלק אותן, כמובן, לפונקציות נפרדות של אתחול כללי, הפעלת/כיבוי PWM ועוד.
השורות הראשונות מחברות את שעון המערכת החיוני לשלוש תת-המערכות הרלוונטיות למשימה: פורט C (שבו נמצאים שני פיני הפלט שבהם נשתמש), הטיימר TIM2, ומודול הפונקציות האלטרנטיביות (AFIO):
// Enable clock for port C (bit 4, IOPCEN)
RCC->APB2PCENR |= 0x00000010;
// Enable clock for TIM2 (bit 0, TIM2EN)
RCC->APB1PCENR |= 0x00000001;
// Enable clock for the AFIO (bit 0, AFIOEN)
RCC->APB2PCENR |= 0x00000001;
השורות הבאות יאתחלו את הפינים הרלוונטיים למצב המתאים – פלט "Multiplexed", כלומר שמגיע ממודול פנימי ולא מכתיבה ישירה בקוד. האתחול הזה מתבצע גם מצד הפורט וגם מצד ה-AFIO.
// Using PC1 and PC2 (pins 5,6) for PWM output
// Set pins mode to Multiplexed Push-Pull, 50MHz
GPIOC->CFGLR = 0x44444BB4;
// Select the correct alternate pins for PWM:
// Mode 01: CH2 on PC2, and CH4 on PC1
// Bits TIM2_RM [9:8] should be 01
AFIO->PCFR1 &= ~0x00000300;
AFIO->PCFR1 |= 0x00000100;
כעת נגדיר מצב PWM Mode 1 (לא-רשמית, נקרא לו "המצב האינטואיטיבי") עבור שני הערוצים שמעניינים אותנו, לשם כך צריך לכתוב את הערכים "110" לביטים OCxM ברגיסטרי הבקרה של הערוצים, שנקראים CHCTRLx. כדי לבלבל אותנו, יש רק שני רגיסטרי בקרה, שכל אחד מהם שולט בשניים מהערוצים…
// Select PWM Mode 1 for each channel, 2 and 4
// OC2M is bits 14:12 of CHCTRL1
TIM2->CHCTLR1 &= ~0x7000;
TIM2->CHCTLR1 |= 0x6000;
// OC4M is bits 14:12 of CHCTRL2
TIM2->CHCTLR2 &= ~0x7000;
TIM2->CHCTLR2 |= 0x6000;
ועוד פעולה קטנה אבל קריטית ביותר – לחבר את הטיימר כמקור פלט עבור הפינים שחוברו לערוצים 2 ו-4:
// Enable output (CCxE) for channels 2 and 4
TIM2->CCER |= 0x1010;
השורות הבאות הן חלק מתהליך ההגדרה כפי שהוא מופיע ב-datasheet, אף על פי שהחשיבות שלהן תלויה במה שהקוד צריך. כשאנחנו מאפשרים את ה-Preload registers האלה, כל שינוי שנערוך בערכי הסף של הטיימר ושל מוני ה-PWM ייכנס לתוקף מיד. אם לא נאפשר אותם, השינוי יחכה עד לסיום המחזור הנוכחי של הטיימר.
// Enable preload register for both channels (OCxPE)
TIM2->CHCTLR1 |= 0x0800; // (bit 11, OC2PE)
TIM2->CHCTLR2 |= 0x0800; // (bit 11, OC4PE)
// Enable auto-reload of the TIM2
// preload register (bit 7, ARPE)
TIM2->CTLR1 |= 0x0080;
ועכשיו, סוף כל סוף, סיימנו עם ההגדרות הכלליות ואנחנו יכולים להגדיר את הסיגנל עצמו. דבר ראשון, יש לנו מונה אחד "כללי" של הטיימר, שסופר כברירת מחדל שוב ושוב מ-0 עד הערך של הרגיסטר ATRLR. נניח ששעון המערכת שלי הוא 8MHz, ושהטיימר ניזון ממנו ישירות, וש-ATRLR הוא 100. זה אומר שהמונה יחזור לאפס 80,000 פעמים בשנייה.
לכל ערוץ PWM יש ערך סף משלו (ברגיסטר CHxCVR, כאשר x הוא מספר הערוץ). כל עוד המונה הראשי נמוך מהערך הזה, נקבל כברירת מחדל HIGH בפין הפלט של אותו ערוץ; כאשר המונה הראשי עובר את הסף, נקבל שם LOW. ככה נוצר ה-Duty cycle. אז כדי לקבל Duty cycle של 75% כשהמונה הראשי סופר מ-0 עד 100, קלי קלות – נקבע ערך סף של 75!
// For easy 0-100 duty cycle
TIM2->ATRLR = 100;
// The Compare registers, per channel
TIM2->CH2CVR = 75;
TIM2->CH4CVR = 25;
ולסיום נותרו רק עוד שתי פעולות: לאתחל את הטיימר, כדי שכל הערכים שכתבנו לרגיסטרים שלו ייקלטו ויופעלו כמו שצריך, ואז לתת לו פקודה לרוץ:
// Reset the timer with all the new definitions
TIM2->SWEVGR |= 0x0001; // (bit 0, UG)
// Finally, start the timer
TIM2->CTLR1 |= 0x0001; // (bit 0, CEN)
זהו, עכשיו אפשר להריץ עוד קוד או סתם לולאה אינסופית, ושני הפינים שהגדרנו יוציאו אות PWM בדיוק כמו שביקשנו, כפי שאפשר לראות בפלט הסקופ הבא:
נ.ב. כשהתחלתי לכתוב את הפוסט הזה, הייתי סקרן וביקשתי מ-ChatGPT קוד שמגדיר פלט PWM בעזרת TIM2. הוא הפיק משהו מאוד יפה ומסודר ועם הערות ברורות, אבל קצת מנותק מהמציאות – הוא השתמש בשמות רגיסטרים שאינם קיימים, פיני פלט שאין אליהם בכלל גישה מהטיימר, ועוד. ראו הוזהרתם.