תוך כדי עבודה על יצירת גל ריבועי באמצעות טיימר לצורך שידורי 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):
ובקוד, בתחילת הפונקציה main, זה נראה פשוט כך:
OSCCAL -= 4;
תוכנית B
דבר אחרון שראוי להזכיר כאן, הוא שכל מה שעשינו עם סף A של Timer0 אפשר לעשות גם עם סף B. זאת אומרת, בתוספת של שורות קוד בודדות, כמעט זהות לאלה הקיימות, נוכל להפעיל סרבו שני באמצעות פין אחר ולקבוע את הזווית של כל מנוע בנפרד. וזה עוד לפני שנגענו בטיימר השני!
הי עידו,
אני עושה שימוש בטייני 85 ומפעיל סרוו עם סוללה 3.7 וולט
כדי לחסוך בחשמל אני רוצה לנתק את הסיגנל אחרי שהסרוו הגיע למקום.
ניסיתי לשלוח פקודת analogWrite(0) אבל מידי פעם הוא חותך את הפולס האחרון (את ה- 1 לוגי) ואז הסרוו מקבל בפולס האחרון, פולס קצר יותר וזז לנקודה אחרת במקום להישאר במקום !
האם ניתן לשלוח אפס בדיוק כשהפולס של הסרוו נימצר באפס לוגי ?
רציתי לדעת אם ניתקלת בבעיה דומה ואם אתה יודע על פיתרון.
תודה רבה,
שי
למה אתה חושב שניתוק של הסיגנל יגרום לסרבו להישאר במקום?
אני לא יוצר עומס על הסרוו ואחרי שהוא זז למקום הרצוי, אני פשוט רוצה לנתק את הסיגנל כי הסרוו מרצד וצורך זרם. בדקתי את זה ע"י ניתוק פיסי של הסיגנל וזה נראה בסדר, אני מחפש דרך לעשות זאת דרך התוכנה.
אשמח לעזרה, תודה רבה.
סרבו מצפה בכל מקרה לסיגנל, של 1 עד 2 אלפיות השנייה כפי שכתבתי בפוסט. אם אין שום סיגנל, אי אפשר לדעת מה יקרה. כדי למנוע צריכת זרם אתה צריך לנתק לו את החשמל, לא את הסיגנל – אבל אם יש בעיה של "ריצודים" סביר להניח שהסיבה לזה נמצאת בכלל במקום אחר לגמרי.
האם הקוד בכוונה כך? או שישנה טעות?
TCCR0A &= ~(1 << WGM01 | 1 << WGM01);
כי המנוע לא מסתובב בצורה טובה כפי שניתן לראות בסרטון.
אני לא מבין איפה הבעיה, ולמה אתה חושב שדווקא השורה הזו לא בסדר…
כי יש בה OR של שני ערכים זהים.
טוב מאוחר מאשר לעולם לא 🙂 אני צריך לחזור ולבדוק מה בכלל היה שם, ואתקן בהתאם. תודה!
…אז קודם כל, אכן כתבתי את אותו קבוע פעמיים, ואחד מהם צריך היה להיות WGM00. במקרה *הספציפי הזה* לא היה הבדל בתוצאה הסופית, כי השתמשתי בקבועים האלה כדי לאפס את הביטים הרלוונטיים בעזרת פעולת AND, וברירת המחדל שלהם ברגיסטר היא 0, כך שגם ה-1 השגוי שנבע מהטעות שלי התאפס בסופו של דבר.
אילו שורות קוד יש להוסיף על מנת להפעיל סרבו שני,שלישי ורביעי?
תודה
ל-Timer0 יש רגיסטר OCR0B, איתו אפשר לשלוט בסרבו נוסף בשיטה שתוארה כאן. אפשר להיעזר גם ב-Timer1 לסרבו שלישי (עקרון דומה, רגיסטרים ואתחול קצת שונים). מעבר לזה, אולי עדיף לחשוב כבר על מיקרו-בקר אחר עם יותר רגליים/טיימרים…
היי עידו,
אני מעוניין להשתמש ב attiny85 להפעלת servo מסיבות מסוימות.
הצלחתי להפעיל סרבו אחד בצורה די טובה. ניסיתי כפי שאמרת לי להפעיל עוד servo אך ללא הצלחה ( הבנתי כיצד יש להתשמש עם הרגיסטר והטיימר אך הסתבכתי עם שורות הקוד שיש להוסיף). אשמח לעזרתך.
העלה את הקוד שלך ל-pastebin או מקום נוח אחר, תביא קישור ונסתכל…