שליטה במנוע סרבו עם טיימר ב-ATtiny85

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

המטרה

מסיבות היסטוריות שונות ומשונות, זווית הסיבוב של מנועי הסרבו הנפוצים נקבעת לפי משך הזמן של 1 לוגי בתוך מסגרת של 20 אלפיות השניה. הטווח המקובל הוא אלפית שניה אחת עד שתיים. כלומר, אם במהלך פרק זמן של 20ms הסרבו מקבל מתח בקרה לאורך אלפית שניה אחת בלבד, ו-0 לוגי ב-19 אלפיות השניה שנותרו, הוא מסתובב לנקודת המינימום שלו. אם הוא מקבל מתח ל-2ms, הוא מסתובב לנקודת המקסימום*.

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

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

חישובים ראשוניים

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

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

מו"מ עם החומרה

שני הטיימרים הם בני 8 ביט בלבד, אז כדי לחסוך חישובים נוספים, נרצה ששתי אלפיות שניה יסתכמו ב-256 "תקתוקים" של טיימר.

במהירות שעון של 16MHz, שתי אלפיות השניה הן 32,000 מחזורים. לטיימר0 יש מבחר מוגבל של ערכי Prescaler לחלוקת השעון, ולרוע המזל, הערך שהכי קרוב למה שאנחנו צריכים (128, שהיה נותן לנו 250 תקתוקים) לא ביניהם. הערכים הקרובים הם 64 (שנותן 500 תקתוקים – יותר מדי לספירה פשוטה ב-8 ביט) ו-256 (שמשאיר לנו 125 תקתוקים בסך הכל, מה שיוביל לרזולוציה די עלובה). האופציות כעת הן לעבור לטיימר1 המורכב קצת יותר – או, מה שנעשה כאן, להאט את מהירות השעון של המיקרו-בקר כולו בחצי ל-8MHz ולהשתמש ב-prescaler של 64.

אם 2ms מתחלקות ל-256 חלקים, וה-1 הלוגי צריך לפעול מינימום 1ms בכל סיבוב, נשארים לנו 128 ערכים כדי לקבוע את הזווית של הסרבו. זה פחות מ-180 מעלות כמובן, ואם היינו מחפשים דיוק מושלם היה עלינו לעבור לשיטה אחרת. במקרה הספציפי הזה, מכיוון שאני משתמש במנועי סרבו סיניים זולים במיוחד, סביר להניח שהרזולוציה של הסרבו עצמו לא מגיעה למעלה אחת, כך ש-1.4 מעלות לכל תקתוק של הטיימר זה לא כל כך נורא.

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

הכלי שעומד לרשותנו לביצוע תיקונים כאלה הוא הרגיסטר OSCCAL, אותו יש לכוון לכל ג'וק בנפרד בהתאם לחוסר-הדיוק הספציפי שלו (ובמקרים מסוימים, גם בהתאם לתנאי הסביבה שבה הוא פועל). בדרך כלל משתמשים ב-OSCCAL כדי להבטיח שהשעון הפנימי קרוב ככל האפשר לשעון ה"אמתי", אבל אף אחד לא אמר שאסור להשתמש בו גם למטרות אחרות…

קדימה לקוד

כל המלל למעלה מסתכם במעט קוד. אנחנו נשתמש בפין PB0 כפין הבקרה לסרבו ונגדיר שתי פסיקות ל-Timer0: פסיקת Overflow רגילה, שמתרחשת אחת ל-256 תקתוקים, ופסיקת CompA שמתרחשת כאשר מספר התקתוקים זהה למספר שנציין ברגיסטר OCR0A. בתוך הפסיקה הראשונה נספור מחזורים בעזרת מונה, ונעלה את הפין ל-HIGH בכל מחזור עשירי, כלומר אחת ל-20ms. הפסיקה השניה תוריד את הפין חזרה ל-LOW (או תשאיר אותו ככה אם הוא היה LOW מקודם). שינוי של OCR0A יקבע את משך הסיגנל – ואת זווית הסרבו.

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint8_t current2msCycle = 10;
uint8_t servoAngle;

// Range 0-126 only; 127 collides with OVF
void setServoAngle(const uint8_t a) {
  OCR0A = 128 + a;
}

int main(void)
{
  // Hardware setup
  // I/O
   DDRB = 1 << PB0; 
  // Timers
  // Ensure normal mode for timer0
   TCCR0A &= ~(1 << WGM01 | 1 << WGM01);
   TCCR0B &= ~(1 << WGM02);
  // Set timer0 prescaler to 64
   TCCR0B &= ~(1 << CS02);
   TCCR0B |= 1 << CS01 | 1 << CS00;
  // Interrupts
  // Timer0 overflow and A match
   TIMSK |= 1 << TOIE0 | 1 << OCIE0A; 
    
  // Software setup
   setServoAngle(64); // middle

  // Enable interrupts  
   sei();
    
  while(1) {
  }

}

ISR(TIMER0_OVF_vect) {
  // Turn servo pin HIGH once every 20ms
  current2msCycle--;
  if (!current2msCycle) {
    PORTB |= 1 << PB0;
    current2msCycle = 10;
  }
}

ISR(TIMER0_COMPA_vect) {
  // Turn servo pin LOW
  PORTB &= ~(1 << PB0);    
}

(הערה חשובה: בקוד הנ"ל טעיתי וכתבתי את הקבוע WGM01 פעמיים, במקום פעם WGM00 ופעם WGM01. ראו בתגובות למטה)

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

יש בעיה קטנה אחת בקוד למעלה, שמצוינת שם בהערה: כאשר אנחנו שולחים לסרבו את הערך הקיצוני 127, כלומר נותנים ל-OCR0A את הערך 255, שתי הפסיקות מתרחשות למעשה "באותו זמן", ובגלל סדר העדיפויות שלהן הפין מועבר ל-LOW מיד בהתחלת המחזור במקום ממש בסוף. אפשר לחשוב על כמה דרכים לפתור את זה. במקרה שלי, עבור הרובוט שמופיע בסרטון למעלה, זה לא קריטי כי ממילא אני לא מתכוון להגיע לערכים הקיצוניים.

תיקוני OSCCAL

הגיע הזמן לעבור לסקופ לבדיקות. הסיגנל שהתקבל נראה טוב, אבל השעון טיפה מהיר מדי: ציר X כולו הוא 20ms, ובכל זאת אנחנו רואים את תחילת ה-HIGH השני (מסומנת בחץ אדום בתמונה למטה). סביר להניח שההפרש לא מספיק גדול כדי לשבש את פעילות הסרבו, אבל אם כבר הגענו עד כאן, למה לא להמשיך?

חוסר דיוק בתזמון האות לסרבו
חוסר דיוק בתזמון האות לסרבו

התחלתי לבצע שינויים קלים בערך של OSCCAL. הוא מגיע עם ערך ברירת מחדל צרוב מהמפעל, כך שלצורך כוונון נוח יותר להוסיף או להחסיר ממנו מאשר לנסות למצוא ערך אבסולוטי. מהר מאד הגעתי להפרש שנתן את התוצאה היפה הבאה (ציר X כאן הוא 50ms וכל קו אנכי הוא 5ms):

תזמון אות סרבו שתוקן בעזרת כוונון הרגיסטר OSCCAL
תזמון אות סרבו שתוקן בעזרת כוונון הרגיסטר OSCCAL

ובקוד, בתחילת הפונקציה main, זה נראה פשוט כך:

    OSCCAL -= 4;

תוכנית B

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

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

הי עידו,
אני עושה שימוש בטייני 85 ומפעיל סרוו עם סוללה 3.7 וולט
כדי לחסוך בחשמל אני רוצה לנתק את הסיגנל אחרי שהסרוו הגיע למקום.
ניסיתי לשלוח פקודת analogWrite(0) אבל מידי פעם הוא חותך את הפולס האחרון (את ה- 1 לוגי) ואז הסרוו מקבל בפולס האחרון, פולס קצר יותר וזז לנקודה אחרת במקום להישאר במקום !
האם ניתן לשלוח אפס בדיוק כשהפולס של הסרוו נימצר באפס לוגי ?
רציתי לדעת אם ניתקלת בבעיה דומה ואם אתה יודע על פיתרון.

תודה רבה,
שי

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

האם הקוד בכוונה כך? או שישנה טעות?
TCCR0A &= ~(1 << WGM01 | 1 << WGM01);
כי המנוע לא מסתובב בצורה טובה כפי שניתן לראות בסרטון.

כי יש בה OR של שני ערכים זהים.

אילו שורות קוד יש להוסיף על מנת להפעיל סרבו שני,שלישי ורביעי?
תודה

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