שפת C למתחילים: ביטים, חלק א'

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

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

ביטים כמייצגי מספרים

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

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

1 – 10 – 100 – 1,000 – 10,000 – 100,000 – 1,000,000 – …

לא צריך להיות גאון במתמטיקה כדי לזהות שכל המספרים האלה הם חזקות של 10:

100 – 101 – 102 – 103 – 104– 105 – 106 – …

כלומר, כל ספרה מוכפלת למעשה בעשר-בחזקת-המיקום-שלה, כאשר ספירת המיקומים מתחילה מאפס. לדוגמה, אם המספר הוא 321, הספרה 1 שנמצאת במיקום אפס תוכפל בעשר בחזקת אפס (=1). הספרה 2 שנמצאת במיקום אחד תוכפל בעשר בחזקת אחד (=20) והספרה 3 תוכפל בעשר בחזקת שניים (=300).

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

20 – 21 – 22 – 23 – 24– 25 – 26 – …

נבצע את החישובים בשיטה העשרונית ונקבל

1 – 2 – 4 – 8 – 16 – 32 – 64 …

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

111011

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

במספר הזה היו שישה ביטים. בשפות תכנות, לעומת זאת, יחידת הנתונים הבסיסית ביותר1 היא הבייט (byte), שכולל שמונה ביטים. מה טווח המספרים שאפשר לייצג באמצעות בייט אחד? במילים אחרות, כמה זה 00000000 וכמה זה 11111111?

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

אפרופו, הביט הכי ימני ברצף מכונה בשפה המקצועית LSB, ראשי תיבות של Least Significant Bit (הביט הכי פחות חשוב), ואילו השמאלי ביותר נקרא MSB (הביט הכי חשוב – Most Significant Bit). אתם תיתקלו במונחים האלה, בדרך כלל, בפרוטוקולים של תקשורת נתונים, שמעבירים ביטים באופן סדרתי. הם עשויים להעביר את המידע החל מה-MSB ימינה (MSB First) או החל מה-LSB שמאלה (LSB First).

ומה עושים כשהמספר שלילי?

טיפוס byte מיועד למספרים חיוביים בלבד, אך יש לא מעט טיפוסים עם סימן (מה שנקרא signed). איך נציין מספרים שליליים אם כל מה שיש לנו זה 0 ו-1? אנחנו – והקומפיילר – פשוט משתמשים במוסכמה: אם המשתנה הוא מטיפוס signed, ה-MSB יקבל תפקיד של "ביט סימן". אם ה-MSB הוא 0 אז המספר חיובי או אפס, ואם הוא 1 אז המספר שלילי.

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

// B means it's a number in binary representation
char c = B10000000;

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

void loop() {

  // The "(int)" part casts c into an int
  Serial.println( (int) c );
  // Empty infinite loop;
  for ( ; ; ) ;

}

הטיפוס char הוא בן 8 ביטים, כמו בייט, אך מסוגל לקבל ערכים שליליים. הריצו את הקוד ותראו שהרצף 10000000 שהזנו מיתרגם למספר 128-. כלומר, כשה-MSB הוא 1, זה לא סתם היפוך סימן עבור שאר הביטים, אלא משהו אחר. מה בדיוק? לא אסבך אתכם עם הפרטים כי למזלנו, רק לעתים רחוקות אנחנו צריכים להתעסק עם הדקויות של מספרים שליליים בזמן עבודה עם ביטים. מי שרוצה בכל זאת לדעת מוזמן לעיין כאן.

לקרוא ולכתוב בבינארי

בתחילת הקוד למעלה הופיע דבר מוזר, שלמעשה לא שייך לשפת C ה"רשמית" וקיים רק בניב הייעודי לארדואינו: השמת ערכים בני 8 ביטים ישירות בבסיס בינארי. צריך רק להקליד אות B גדולה, ומיד אחריה רצף של 1 ושל 0. אם הרצף הזה קצר מדי, הקומפיילר יקרא אותו כאילו הוספתם ביטי אפס לפניו. כלומר, המספר B100 הוא זהה מבחינת הקומפיילר ל-B00000100. רצפים ארוכים מדי לא יזוהו כלל.

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

byte b = B00111011;

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

void loop() {

  Serial.println(b);
  Serial.println(b, BIN);
  // Empty infinite loop;
  for ( ; ; ) ;

}

הוא 59 עשרוני רגיל, ואחריו הייצוג הבינארי 111011. הסוד הוא במילת המפתח BIN שהוספה ל-println השני. אפשר להציג מספרים בבסיסים נוספים, אך זה כבר פחות רלוונטי לנו כרגע. אתם יכולים לקרוא על זה במפרט של הפקודה Serial.println.

שיעורי בית

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

  1. כיתבו עשרה מספרים אקראיים (עשרוניים) בין 0 ל-255
  2. כיתבו עשרה מספרים אקראיים (בינאריים) בין 00000000 ל-11111111.
  3. המירו את העשרוניים שכתבתם לייצוג בינארי (תחשבו קצת, יש לכם מספיק מידע איך לעשות את זה), והמירו את הבינאריים לייצוג עשרוני. אחרי שתסיימו, תוכלו להשתמש בקוד הארדואינו שלמעלה כדי לבדוק את עצמכם.

בהצלחה!

1 קיימים טריקים, כולל בשפת C עצמה, ליצירת משתנים בני פחות מ-8 ביטים, אך אלו הם משתנים "מדומים" – חישובים שנערכים מאחורי הקלעים על ידי המעבד על משתנים "טבעיים" בני 8 ביטים (או יותר).

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

תודה, עשה לי סדר בבאלגן…
גיליתי היום את הבלוג שלך ואני מקווה לבקר בו הרבה בעתיד..
תודה

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

קלטתי אותך… אבל את הכוונה שלי הבנת.
מיחקו את מה שאמרתי! אין משתנה כזה! דרך התבטאות עקומה שלי בלבד 🙂

שווה להעיר שהתשובה להאם ברירת המחדל של char היא signed או unsigned היא תלוית קומפילר ולא מוגדרת בתקן של c, לכן עדיף לכתוב
signed char varname;
דרך אגב, בתיעוד של ארדואינו גם מתחמקים מלומר שchar זה שמונה סיביות.

"The size of the char datatype is at least 8 bits"
https://www.arduino.cc/reference/en/language/variables/data-types/char/

🙂 ושוב תודה.
(והפעם למרות שיש לי שיעורים משלי אני עושה גם את השיעורי בית שלך… זה פשוט מרתק)

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

תודה!
ואחלה בלוג!