הלו טייני #3: פורט אחד קטן

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

ביט לכל רגל

מתוך שמונה הרגליים של הטייני, שתיים הן עבור החשמל ושש הן רגלי קלט/פלט (אם כי אחת מהן שמורה כמעט תמיד לצורך אתחול – Reset). כלומר, ליישום טיפוסי יש לנו חמש רגליים פנויות, שמוכרות בשמות הרשמיים PB0 עד PB4 (כאשר PB הוא כמובן קיצור של Port B). לרוע המזל, אין ממש קשר בין המספור הזה לבין מספור הרגליים הפיזיות של הג'וק. הנה שרטוט של הקשר הנכון בין הרגליים, שמותיהן – וגם מיקום הביט של כל אחת מהן ברגיסטרים של הפורט שנציג מייד:

מיפוי רגלי ה-ATtiny85 עבור פורט ה-I/O
מיפוי רגלי ה-ATtiny85 עבור פורט ה-I/O

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

שלושת הרגיסטרים של הפורט

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

העבודה עם פורט הקלט/פלט הדיגיטלי מתבצעת באמצעות שלושה רגיסטרים נפרדים:

DDRB – קיצור של Data Direction Register, רגיסטר כיוון הנתונים. הוא קובע את ה"כיוון" של זרימת המידע בכל רגל, זאת אומרת אם היא קלט (0) או פלט (1).

PORTB – קובע את ערך הפלט של כל רגל: 0 עבור אדמה/LOW, ו-1 עבור מתח ההפעלה של הרכיב/HIGH. כאשר רגל מוגדרת כרגל קלט (0 בביט המתאים ב-DDRB), שינוי של הביט ב-PORTB יחבר (1) או ינתק (0) את נגד ה-Pull-up הפנימי.

PINB – אפשר לחשוב על השם כקיצור של Port INput, והוא נותן לנו את ערך הקלט בכל רגל: נמוך (0) או גבוה (1). מה נחשב נמוך ומה גבוה? זה תלוי במתח ההפעלה של המיקרו-בקר, ומוצג בגרפים אי-שם בעומק המפרט של הרכיב. בקירוב גס, PINB יראה לנו 1 ברגל מסוימת כאשר המתח על הרגל הוא חצי ממתח ההפעלה ומעלה, ויראה לנו 0 כאשר המתח יהיה 0.45 ממתח ההפעלה ומטה. בין לבין נמצא תחום של "זיכרון", שבו הערך ב-PINB פשוט נשאר כפי שהיה קודם. וזה עוד לא הכל: לדברי המפרט, כאשר כותבים 1 לביט כלשהו ב-PINB, הערך התואם של PORTB פשוט מתחלף (מ-0 ל-1 או להיפך)!

כל זה קצת באוויר, אך יודגם בהמשך. בינתיים, העיקר שצריך לזכור הוא: DDRB קובע את הכיוון, את PORTB משנים כדי להשפיע על פלט, ואת PINB קוראים כדי לקרוא קלט.

מניפולציית ביטים: כתיבה

נשאלת השאלה – נניח שאני רוצה לקבוע שרגל PB0 (רגל פיזית מס' 5) תשמש לפלט. איך אני מגיע לביט הראשון מימין של DDRB כדי לכתוב שם את הערך 1 הדרוש?

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

DDRB = 1;

אז כל שאר הביטים – של PB1, PB2 וכו' יהפכו לאפס, וזה ממש לא לעניין. כדי לשנות ביט בודד צריך לעשות מניפולציית ביטים (Bit manipulation) באמצעות פעולות בינאריות. יש הרבה דרכים לגשת למשימה הזו; כאן אציג את הדרך שהיא, להתרשמותי, הנפוצה ביותר.

הפעולה הבינארית OR (בשפת C היא מסומנת על ידי האופרטור "|") מקבלת שני ביטים. אם שניהם 0, היא מחזירה 0. לעומת זאת, אם אחד מהם (או שניהם) הם 1, היא מחזירה 1. אפשר לחשוב על זה כעל חיבור של ביטים. כשעושים OR לשני בייטים, או טיפוסי משתנים אחרים, הפעולה הנ"ל מתבצעת על כל זוג ביטים בנפרד: הימניים של שני הבייטים, השניים מימין של שני הבייטים וכו'.

כלומר, אם אני רוצה להפוך את הביט הימני של DDRB ל-1, אני אכתוב קוד כזה:

DDRB = DDRB | 1;

או בצורה המקוצרת,

DDRB |= 1;

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

כדי להפוך את הביט השני מימין ל-1, נעשה OR עם הערך העשרוני של המספר הבינארי 10, כלומר 2:

DDRB |= 2;

וכן הלאה. כך אפשר גם לשנות מספר ביטים בו-זמנית.

מה עושים אם רוצים להפוך ביט מסוים ל-0? פעולת OR לא תעזור לנו, כי כאמור OR עם אפס משאיר את הערך כפי שהיה קודם. לשם כך יש לנו את AND.

הפעולה הבינארית AND (בשפת C, "&") היא מעין כפל של שני ביטים. מספיק שאחד מהם הוא 0, וגם התוצאה תהיה 0. רק אם שניהם 1 אז התוצאה תהיה 1. מבחינת התוצאה של פעולה על שני ביטים, זו תמונת ראי של OR: ביצוע של AND על ביט X ועל 1 ייתן לנו את X, ואילו ביצוע של AND על ביט X ועל 0 ייתן לנו 0.

לכן, אם אני רוצה להפוך את הביט הימני של DDRB ל-0, אני צריך לעשות דבר כזה:

DDRB &= 254;

מה פתאום 254? כי בבסיס בינארי, 254 מיתרגם ל-11111110, ואז שבעת הביטים משמאל יישארו כמו שהם, ורק הימני יתאפס.

שימו לב: בשפת C קיימים גם האופרטורים "||" ו-"&&", שמבצעים פעולות לוגיות. העיקרון דומה, אבל הם לא פועלים על ביטים בודדים ולכן לא מתאימים למטרותינו כאן.

לעתים קרובות אנחנו נעזרים גם באופרטורים ">>" ו-"<<", שלוקחים משתנה ו"מסיעים" (Shift) את כל הביטים שלו מספר צעדים שמאלה או ימינה, לפי כיוון החצים. הביטים החדשים במספר יהיו תמיד 0. לדוגמה, אם הערך של המשתנה N הוא 13, שמיתרגם לייצוג הבינארי 00001101, אז אחרי הפעולה

N = N >> 1;

הערך שלו יהיה 00000110 (כל הביטים זזו צעד אחד ימינה, כפי שביקשנו), שזה 6 עשרוני.

מניפולציית ביטים: קריאה

אז אנחנו יודעים איך כותבים ביטים. מה צריך לעשות כדי לקרוא אותם? כאן שוב באה לעזרתנו הפעולה AND, שמאפשרת לנו "לבודד" ביטים רצויים. למשל, הפעולה PINB & 4 מבודדת את הביט השלישי מימין (כי 4 בבינארי זה 100). אם הוא היה 0, התוצאה של החישוב תהיה 0, ואם הוא היה 1 נקבל 4 (כי הרי יש משמעות למיקום שלו).

מבחן התכל'ס

מספיק לקשקש – בואו נראה איך זה עובד בעולם האמתי. אני אחבר לרגליים PB0-PB2 של הטייני לדים ואגדיר אותן כיציאות. לרגליים PB3 ו-PB4 אחבר לחצנים (ל-GND עם נגד Pull-up פנימי) ואגדיר אותן כמובן כקלט. כאשר הלחצן של PB3 יילחץ אני אסיט את האורות צעד אחד "למטה", והלד ה"גבוה" יקבל ערך חדש (כבוי או דלוק) בהתאם למצב של הלחצן של PB4. כדי לחסוך בלגן עם נגדים, אשתמש באחד בלבד עבור כל שלושת הלדים, אפילו שהדבר עשוי לפגוע באחידות התאורה.

הנה הקוד:

// Connections:
// Pins 5,6,7 to + of LEDs
// Pins 2, 3 to Tactile switches (GND)

#define F_CPU 8000000

#include <avr/io.h>
#include <util/delay.h>

uint8_t temp;

int main(void)
{
  // Setup
  // Make PB4 and PB3 INPUT, PB2/1/0 OUTPUT
  DDRB = 7; // Binary 00000111 
  // Activate pull-up for input pins
  PORTB |= 24; // Binary 00011000

  while(1) {
    // Check if PB3 is pressed. Remember it's pulled up! 
    if (!(PINB & 8)) {

      // Isolate output values
      temp = PORTB & 7; 
      // Shift and add "1" if needed
      temp >>= 1;
      if (!(PINB & 16)) temp |= 4;
      // Inject this into PORTB without changing higher bits
      PORTB = (PORTB & 248) + temp;

      _delay_ms(5); // Debounce
      // Wait for button release
      while (!(PINB & 8));

    }
  }
}

והנה התוצאה:

לסיום הפוסט

הקוד הנ"ל, חייבים להודות, לא קריא במיוחד, אך זה בדיוק מה שקורה מאחורי הקלעים של פקודות כמו digitalRead ו-digitalWrite בארדואינו, רק שפה הכל הרבה יותר מהיר. פה ושם תראו גם קוד זהיר יותר, שבמקום להשתמש בקבועים מספריים עבור מיקומי הפינים (8, 16 וכו') מסתמך על קבועים מוגדרים מראש של הקומפיילר, עם שמות כמו PB0 (כן, בדיוק כמו השם של הרגל).

במיקרו-בקרים גדולים יותר, כמו ה-ATmega328 של הארדואינו Uno, יש כמובן פורטים נוספים עם הסימון A, C, D וכדומה. המידע על הפינים המשויכים אליהם יימצא תמיד במפרט של המיקרו-בקר.

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

מאוד נהנה מסידרת המאמרים.

הקוד היה הרבה יותר קריא אם היית משתמש במספרים בינארים במקום דצימלים (באמצעות הקידומת 0b לפני כל בינארי, לדוגמה: 0b0001000 עבור 8 וכו') כדי שהקוראים יבינו את הייצוג של האוגרים.

בכל אופן, אחת מסדרות המאמרים המאלפות שקראתי. לא פחות. 🙂