הלו טייני #6.5: שלושים ושמונה קילוהרץ

הטיימרים (Timers) שב-ATtiny85, ולמעשה בכל מיקרו-בקר אחר כמעט, הם כלי עבודה חזקים מאד שכוללים מגוון של אופני פעולה ואופציות. בפוסט זה ניגע רק בפינה אחת קטנה של אחד הטיימרים, למטרה מאד ספציפית: יצירת גל ריבועי בתדר של 38KHz בדיוק.

חיישן IR לתדר 38KHz (באמצע, במעטפת המתכת) ו-IR LED (משמאלו)
חיישן IR לתדר 38KHz (באמצע, במעטפת המתכת) ו-IR LED (משמאלו)

התדר הזה, אם במקרה עוד לא נתקלתם בו, מקובל לצורך שידור נתונים באינפרה-אדום (כגון משלט רחוק של טלוויזיה). הסביבה הטבעית שלנו רוויה באור אינפרה-אדום (IR) שמגיע ממגוון מקורות, וכדי להבדיל בין שידור בעוצמה סבירה לבין רעש סביבתי סתם, חיישני ה-IR לתקשורת בנויים כך שהם מסננים החוצה כל אות שאינו בתדר 38KHz. כמובן, יש גם חיישני IR שעובדים אחרת או בתדרים אחרים, אך 38KHz הוא נפוץ ומוכר ביותר. לרוע המזל, את התדר הזה אנחנו צריכים ליצור לבד: נוריות ה-LED באור אינפרה-אדום פועלות בצורה "טיפשה" בדיוק כמו בצבעים אחרים, בלי שום תדר מובנה. אז למה דווקא עם טיימר, ואיך עושים את זה?

פתרונות אחרים

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

המשמעות של 38KHz היא שבין פסגה לפסגה של הגל יעברו 1/38,000 של שניה. כלומר, אם כדי ליצור את הגל אני משנה את ערך הפלט בפין מסוים מ-0V ל-5V ובחזרה שוב ושוב, אני צריך בעצם לשנות את הערך הזה כל 1/76,000 של שניה, כ-13.16 מיליוניות השניה. משתמש נאיבי מאד עלול להתפתות לנסות משהו בסגנון קוד ארדואינו כזה:

const byte WAVE_PIN = 2;

void setup() {
  pinMode(WAVE_PIN, OUTPUT);  
}

void loop() {
  digitalWrite(WAVE_PIN, HIGH);
  delayMicroseconds(13);
  digitalWrite(WAVE_PIN, LOW);
  delayMicroseconds(13);
}

אך זו תהיה טעות קטלנית משתי סיבות. ראשית, לפקודות שמופיעות בתוך loop יש זמן ריצה משלהן, והוא עשוי להיות ממושך – במיוחד כשמדובר, כמו כאן, בפונקציות ולא בפעולות "אטומיות". שנית, loop עצמה היא פונקציה, והיציאה ממנה והכניסה-מחדש לוקחות גם הן זמן. בואו נסתכל, דרך ה-Logic Analyzer, מה מתקבל בפועל בפין הפלט:

פלט הקוד הנאיבי לארדואינו בלוג'יק אנלייזר
פלט הקוד הנאיבי לארדואינו בלוג'יק אנלייזר

משך הזמן של מצב HIGH הוא בסביבות 17.8(!) מיליוניות השניה, מצב LOW נמשך כ-18.06 מיליוניות השניה (ההפרש בין המספרים הוא כנראה היציאה והכניסה מחדש ל-loop), ומדי פעם יש חריגה לא מוסברת של מצב LOW ל-24.3 מיליוניות השניה. גם אם ננסה להתחכם ולקצץ מפקודות ההשהיה את ההפרשים בין המצוי לרצוי, אנחנו בבעיה קשה: ברגע שהקוד שלנו ינסה לעשות משהו נוסף, ולו הקטן ביותר, זה יגרום לעיכובים נוספים וישבש לחלוטין את התוצאות. אין ברירה אלא להשתמש בטיימר, שפועל במקביל לקוד הראשי ואינו תלוי בו.

בחזרה ל-ATtiny85

לטייני יש שני טיימרים של 8 ביט כל אחד, עם השמות הלא-יצירתיים Timer/Counter0 ו-Timer/Counter1. לא ניכנס כרגע להבדלים ביניהם. לשניהם יש מונה שיכול לספור מ-0 עד 255 (או עד ערך ביניים כלשהו שנבחר בעצמנו). כאשר המונה מגיע לערך העליון שהוגדר הוא מתאפס, והאיפוס יכול להפעיל פסיקה (Interrupt). כמו כן, לכל טיימר יש שני ערכים מספריים נוספים (A ו-B) שאפשר להגדיר ולבדוק מול המונה, וכל אחד מהם יכול להפעיל – כשערכו שווה לזה של המונה – פסיקה משלו, או להשפיע על ערך הפלט בפין אחד ספציפי.

הסידור הזה, אגב, הוא שמאפשר לנו ליצור אותות PWM (אפנון רוחב פולס, Pulse Width Modulation) יציבים ובקלות. זו הסיבה לכך שיש ללוחות ארדואינו Uno רק שישה פינים שמסוגלים להוציא אות PWM בחומרה: למיקרו-בקר שם יש שלושה טיימרים, ולכל אחד מהם שני פינים שהוא מסוגל לתפעל ישירות (על ידי A ו-B).

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

חישובים ורגיסטרים

אנחנו ניצור את הגל הריבועי בעזרת Timer/Counter0, בפין PB0 (פין מס' 5 בטייני) שמחובר בחומרה לערך A של הטיימר הנ"ל. מכיוון שבחרתי לעבוד במהירות שעון של 16MHz, ותדר החלפת הפלט בפין צריך להתבצע 76,000 פעמים בשניה, זה אומר שעלינו להחליף את הפלט כל 210.52 מחזורי שעון. זה בטווח של המונה (0-255), אך לא מספר שלם ולכן נצטרך לעגל אותו למעלה ולהפסיד מעט מהדיוק. מצד שני, אין שום אחריות שתדר השעון של המיקרו-בקר עצמו יהיה 16MHz בדיוק נמרץ, והסטיה של שעון המערכת עשויה למעשה להיות גדולה יותר מהאפקט של עיגול המספר.

ראשית עלינו להגדיר את PB0 כפין פלט, ועל זה למדנו כאן. כעת, נעביר את טיימר 0 למצב CTC (איפוס טיימר בעת התאמה –  Clear Timer on Compare match). לשם כך עלינו לכתוב לביטים WGM0-WGM2 (שמפוזרים על פני שני רגיסטרים, TCCR0A ו-TCCR0B) את הערך 2 (בבינארי, 010). כמו כן, עלינו לכתוב לביטים COM0A0-COM0A1 ברגיסטר TCCR0A את הערך 1 (בבינארי 01), מה שיגרום לטיימר להחליף את מצב הפלט של הפין OC0A (פין 5 שהזכרתי קודם) ברגע ההתאמה. התאמה בין מה למה? בין המונה הכללי של הטיימר (ששמו TCNT0) לבין הרגיסטר OCR0A, שאת ערכו נצטרך להגדיר כ-210. למה 210? אמנם עיגלתי את מספר המחזורים כלפי מעלה, אבל הספירה מתחילה מ-0. ודבר אחרון וחשוב מאד, חייבים להגדיר את השעון שעליו הטיימר יסתמך. מכיוון שאנחנו רוצים לעבוד עם השעון הפנימי ובמהירות מלאה, נכתוב לביטים CS00-CS02 שב-TCCR0B את הערך 1.

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

#include <avr/io.h>

int main(void) {

    DDRB = 1; // Pin PB0 output
    TCCR0A = (1 << WGM01) + (1 << COM0A0);
    OCR0A = 210;
    TCCR0B = 1 << CS00; 

    while(1) {}

}

ואיתו אנחנו מקבלים מיד גל ריבועי יציב ונחמד בפין 5, רק שהוא לא בדיוק בתדר הנכון: המשך של כל מצב הוא כ-12.75 מיליוניות השניה, שזה כ-39.22KHz. כאמור, ייתכן שזו תוצאה של חוסר דיוק של שעון המערכת. עם קצת ניסוי וטעייה הגעתי לערך חדש עבור OCR0A: במקום 210 כתבתי שם 218, וזו התוצאה (הקליקו להגדלה):

גל ריבועי בתדר 38KHz בעזרת טיימר
גל ריבועי בתדר 38KHz בעזרת טיימר

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

זוהי רק ההתחלה

אם אחבר את פין PB0 ללד IR (עם נגד בטור, כמובן), הלד יהבהב בתדר 38KHz וכל חיישן IR תואם שיקלוט את האות ייתן פלט יציב בהתאם. רק מה, שכדי להעביר נתונים אנחנו צריכים יותר מאשר סתם פלט יציב. למעשה, הגל הריבועי שיצרתי אינו באמת הערוץ להעברת הנתונים, אלא רק "גל נושא" שמאפשר לחיישן להבדיל בין "0" ל-"1". את השליחה בפועל של ה-0 וה-1 האלה צריך לנהל בנפרד, רמה אחת למעלה – ועל כך בפוסטים עתידיים.

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

"בא לי בתזמון מושלם" – מקווה שזאת בחירת מילים מכוונת

תודה רבה,
כבר שאלתי פעם את השאלה, אנסה שוב..
אתה יכול להסביר קצת על הקוד עצמו, כי הוא לא אינטואטיבי כמו קוד רגיל של ארדואידנו.
לדוגמא איך מהשורה השנייה בקוד מקבלים שWGM2 מקבל את הערך 2 ו-COM0A0 מקבל את הערך 1.
אם פספסתי איזה פוסט בנושא אני מתנצל.

תריץ חיפוש בdatasheet. תכתוב "WGM2" ותראה שהוא חלק ממספר בינארי של שלוש סיביות ברגיסטר מסוים. כאשר קובעים:
WGM2 = 0
WGM 1 = 1
WGM0 = 0
מקבלים את הספר הבינארי 010 שהוא 2.
פשוט לחפור בdatasheet, אי אפשר להבין באמת מהקוד.

בא לי בתזמון מושלם, תודה.