שידור Serial מ-ATtiny85 (ובכלל)

האובייקט Serial בארדואינו הוא כלי שימושי במיוחד לתקשורת ולדיבוג בסיסי, מכיוון שאפשר לשדר דרכו מידע ממוקד ומפורט, בפורמט נוח שאפשר גם לשמור, לעבד ולתעד במחשב. כשעוברים מארדואינו למיקרו-בקרים קטנים שאין להם חומרה מתאימה, חסרונו של ה-Serial מורגש היטב, אבל אם היישום שלנו מאפשר להקצות לעניין טיימר אחד ופין I/O אחד, אנחנו יכולים לכתוב בעצמנו פונקציות שישלחו מידע באותה שיטה בדיוק!

איך נראה שידור ב-Serial

כדי לא להסתבך יותר מדי, נתרכז כאן בצורת השידור של ברירת המחדל בארדואינו – 9600,8,N,1, כלומר קצב שידור של 9600 ביטים לשניה, 8 ביטים בכל יחידת נתונים, ללא (No) ביט זוגיות שיכול לעזור באיתור שגיאות, ועם ביט עצירה (Stop bit) יחיד בסוף. מי שרוצה להבין מה בדיוק כל המונחים האלה אומרים מוזמן/ת להתחיל לחפור כאן.

קצב השידור הזה אומר שכל ביט "נמשך" 104.17 מיליוניות השניה. עם הנתון הזה, אנחנו מוכנים להסתכל על שידור אמתי מארדואינו ולראות איך בדיוק הוא בנוי. כדי לסייע בפענוח, נשלח שתי סדרות ביטים שאמורות להיות קלות לזיהוי בעין: 11001010 (או 202 עשרוני), ו-01101001 (או 105 עשרוני). ככה נראה הקוד לארדואינו:

void setup() {
  Serial.begin(9600);
}

void loop() {
   Serial.write(202);
   Serial.write(105); // ASCII "i"
   delay(2000);
}

וככה זה נראה על מסך ה-Logic Analyzer:

הסיגנל בשידור סריאלי של 202 ו-105 מארדואינו
הסיגנל בשידור סריאלי של 202 ו-105 מארדואינו (לחצו להגדלה)

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

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

סימון מרווחי זמן של ביטים בשידור
סימון מרווחי זמן של ביטים בשידור

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

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

איך ליצור שידור כזה – הרמה העקרונית

יש לנו כעת שלוש משימות. הראשונה היא לכוון טיימר כלשהו כך שיתזמן לנו את הביטים בקצב הנכון. אנחנו עובדים כאן עם ATtiny85, אז ניקח את Timer0 הנוח. לצורך העניין, נעבוד במהירות שעון של 16MHz, מה שאומר שנצטרך פסיקת טיימר אחת ל-1667 מחזורי שעון בערך. המונה של הטיימר הוא 8 ביט בלבד ומגיע רק עד 255, אז נבחר Prescaler מבין הערכים האפשריים שייתן לנו תוצאה עגולה ככל האפשר. הערך 64 נותן לנו ספירה של כמעט בדיוק 26. נכוון את הסף העליון של הטיימר ל-26 ויש לנו "תקתוק" במהירות קרובה מאד לרצוי.

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

שלב ראשון – הטיימר

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

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

int main(void) {
    
    // Define PB0 as output
    DDRB = 1 << PB0; 
    
    // Set top value for counter 0
    OCR0A = 26;
    // No A/B match output; just CTC mode
    TCCR0A = 1 << WGM01;
    // Set prescaler to clk/64 
    TCCR0B = (1 << CS01) | 1 << (CS00);
    // Activate timer0 A match interrupt
    TIMSK = 1 << OCIE0A;
    // Enable interrupts
    sei();
    
    while(1) {
    }
}

// Timer 0 A-match interrupt
ISR(TIMER0_COMPA_vect) {
  // Toggle output on PB0
  PORTB ^= (1 << PB0);    
}

בינגו!

סיגנל מבוסס טיימר בתדר (כמעט בדיוק) 9600 הרץ
סיגנל מבוסס טיימר בתדר (כמעט בדיוק) 9600 הרץ

שלב שני – בייט שלם

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

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

volatile struct {
  uint8_t dataByte;
  uint8_t bitsLeft;    
} txData = {0, 0};


void sendBySerial(const uint8_t data) {
  txData.dataByte = data;
  txData.bitsLeft = 10;
  // Reset counter
  TCNT0 = 0;
  // Activate timer0 A match interrupt
  TIMSK = 1 << OCIE0A;
} // sendBySerial


// Timer 0 A-match interrupt
ISR(TIMER0_COMPA_vect) {
  
  uint8_t bitVal;

  switch (txData.bitsLeft) {
    case 10: bitVal = 0; break;
    case  1: bitVal = 1; break;
    case  0: TIMSK &= ~(1 << OCIE0A); return;
    default: 
      bitVal = txData.dataByte & 1;
      txData.dataByte >>= 1;
  } // switch

  if (bitVal) PORTB |= (1 << PB0); 
   else PORTB &= ~(1 << PB0);
  --txData.bitsLeft;
}

שלב שלישי: מחרוזות

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

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

לסיום, מכיוון שפלט הפין אינו קשור ישירות לחומרה של הטיימר אלא מנוהל על ידי תוכנה, הוספתי את האופציה לבחור את פין הפלט הרצוי. הקוד הסופי נמצא כאן, והנה מה שהתקבל מהטייני למחשב דרך מתאם USB-to-UART:

המידע שנשלח באמצעות תקשורת סריאלית מהטייני
המידע שנשלח באמצעות תקשורת סריאלית מהטייני

תוספת: הקוד הנ"ל במלואו, כשהוא מקומפל, תופס קצת פחות מ-5% מנפח הזיכרון של ה-ATtiny85, גם ב-Flash וגם ב-SRAM.

להרשמה
הודע לי על
0 Comments
Inline Feedbacks
הראה את כל התגובות