למיקרו-בקר הצנוע PIC12F675 יש זיכרון EEPROM בנפח 128 בייטים. כדי לכתוב או לקרוא מזיכרון זה, עלינו לבצע טקסי וודו מסתוריים. לשם מה צריך את ה-EEPROM, איך בדיוק נראים הטקסים האלה, ולמה הם חיוניים?
EEPROM
נתחיל בתזכורת קצרה על מהותו ותפקידו של ה-EEPROM, לטובת מי שלא עוקב אחרי הבלוג הזה מספיק זמן. ברוב המיקרו-בקרים כיום יש שלושה סוגי זיכרונות: ה-Flash, שבו מאוחסן בדרך כלל קוד התוכנה שכתבנו, ה-RAM (זיכרון גישה אקראית) שבו נמצאים ערכי משתנים ונתונים דינמיים נוספים, וה-EEPROM שאפשר לחשוב עליו כמעין מחסן כללי למידע.
סוגי הזיכרונות שונים זה מזה – בין השאר – במימוש הפיזי, במחיר הייצור, במהירות הקריאה/כתיבה, במספר הפעמים שניתן לכתוב אליהם, ובמה שקורה להם כשהמערכת מנותקת מהחשמל. הכתיבה ל-EEPROM איטית מאד, והוא מוגבל במספר הכתיבות. עם זאת, מגבלת הכתיבות שלו סלחנית בהרבה מזו של ה-Flash, הגישה אליו נוחה יותר (יחסית), ובניגוד ל-RAM, המידע שבו נשמר גם כשמנתקים את החשמל – מה שנקרא "זיכרון בלתי-נדיף". לכן משתמשים ב-EEPROM לעתים קרובות לאחסון של מידע שאינו משתנה לעולם (כגון טבלת סינוסים לחישובים), או של מידע שמשתנה לעתים רחוקות ושצריך לשרוד במקרה של הפסקת חשמל.
לדוגמה, דגמי מכוניות מסוימים מאפשרים כוונון חשמלי של מושב הנהג, ואף זוכרים את ההעדפות של נהגים שונים. זה מקרה קלאסי עבור EEPROM, מכיוון שכוונון המושב נעשה לעתים רחוקות יחסית, ואנחנו רוצים שהמידע יישמר גם אם מכבים את המנוע או מחליפים מצבר.
זהירות, שיבושים בדרך
ב-PIC12F675, זמן הכתיבה של בייט יחיד ל-EEPROM יכול להגיע – בהתאם למתח ההפעלה של המערכת ולטמפרטורה – עד שש אלפיות השניה (ראו טבלה 12.7 במפרט הטכני). במונחים של מיקרו-בקרים זה המון, ובזמן הזה יכולים לקרות המון דברים: פסיקה שכתבנו בעצמנו עלולה להתעורר ולשנות ערכים של משתנים, המערכת עצמה עלולה לעבור נפילת מתח, ועוד.
לכן, ומכיוון שה-EEPROM משמש לעתים קרובות לאחסון של מידע קריטי, תהליכי הגישה ל-EEPROM הם מורכבים יחסית וכוללים שורה של צעדים שחייבים להתבצע בסדר ובתזמון מדויק. הדבר מצמצם את הסיכוי שיישמרו ב-EEPROM נתונים שגויים – אלא אם, כמובן, מקור השגיאה הוא לוגיקה שגויה בתוכנה שלנו…
קריאה
נתחיל בקריאה, מכיוון שזו הפעולה הקלה והמהירה. כדי לא להסתבך, נתמקד כאן בקריאה של בייט יחיד – שהיא בכל מקרה הבסיס לקריאה של נתונים מורכבים יותר.
ראשית, עלינו לכתוב לרגיסטר EEADR את הכתובת של הבייט הרצוי בזיכרון ה-EEPROM. הרגיסטר עצמו הוא בן 8 ביטים, כך שהוא מכסה את כל טווח הכתובות הרלוונטי (0-127). לאחר מכן, צריך לכתוב "1" לביט RD (שהוא הביט הנמוך ביותר ברגיסטר EECON1) כדי להבהיר לפיק שאנחנו מעוניינים לבצע קריאה (מין קיצור שכזה, READ). מחזור פעולה אחד לאחר מכן, המידע המבוקש יהיה זמין לקריאה ברגיסטר EEDATA.
בקוד לקומפיילר XC8, פונקציה שמבצעת קריאה מה-EEPROM תיראה פחות או יותר כך:
char EEPROMRead(char addr) { EEADR = addr; RD = 1; return EEDATA; }
הערה: השתמשתי כאן בטיפוס char ולא כתבתי במפורש unsigned char, רק כדי ששורות הקוד כאן לא ייחתכו באמצע. ב-XC8, ברירת המחדל של char היא אמנם unsigned, אבל בקוד ראוי לשמו כותבים דברים כאלה במפורש. אפשר גם להגדיר את addr כ-const ולקוות שהקומפיילר יעשה עם זה משהו.
כתיבה
הכתיבה היא, כאמור, התהליך הממושך והמסובך. גם פה נדבר רק על בייט יחיד. ראשית, נכתוב לרגיסטר EEADR את הכתובת הרצויה, ולרגיסטר EEDATA נכתוב את הערך שברצוננו לשמור. עד כאן הגיוני – מכאן מתחיל הבלגן.
כדי להוכיח למיקרו-בקר שאנחנו רציניים בקטע של כתיבה ל-EEPROM, עלינו לכתוב 1 לביט WREN (כלומר WRite ENable), שהוא ביט מס' 2 – השלישי מימין, כביכול – ברגיסטר EECON1.
כדי למנוע מפסיקות להפריע לכתיבה, עלינו לנטרל אותן. לשם כך נכתוב 0 בביט GIE (הביט הגבוה ברגיסטר INTCON). פעולה זו תשבית את כל הפסיקות במערכת. בהתאם למה שהפסיקות שלכם עושות, ייתכן שכדאי להשבית אותן אפילו קודם, לפני כתיבת הכתובת והערך לשמירה.
"רק רגע אחד, חבוב," אומר עכשיו הפיק, "דיבורים לא עולים כסף. אם אתה באמת רציני בקשר אלינו, אתה חייב להתאמץ כדי להוכיח שאתה אוהב אותי — זאת אומרת, שאתה רוצה לכתוב ל-EEPROM!" ואיך עושים את זה?
- כותבים את הערך 85 (בהקסדצימלי 0x55) לרגיסטר EECON2
- מיד לאחר מכן כותבים את הערך 170 (הקס 0xAA) לאותו רגיסטר
- מיד לאחר מכן כותבים 1 לביט WR (ביט מס' 1 ב-EECON1).
רק אם השלבים האלה בוצעו בדיוק לפי הסדר ובלי הפרעות, הפיק ישתכנע וייגש סוף כל סוף למלאכת הכתיבה.
מה קורה עכשיו? הכתיבה לוקחת זמן, ובזמן הזה אנחנו יכולים להחזיר את הפסיקות לפעולה (באמצעות כתיבה של 1 ל-GIE) ולעשות דברים אחרים – או, אם אפשר, לחכות בסבלנות ולא לטלטל את הסירה. הפיק יודיע לנו על סיום המלאכה בשתי דרכים: הביט WR יחזור להיות 0, והביט EEIF (הביט הגבוה ביותר ברגיסטר PIR1) יהפוך ל-1. למעשה, הביט EEIF הוא דגל שקשור גם לפסיקה, ואם החזרנו כבר לפעולה את הפסיקות שעצרנו קודם, השינוי שלו יעורר את פסיקת "סיום כתיבה ל-EEPROM". פה נכנסים לתמונה גם עניינים מורכבים יותר של מצב שינה וכדומה, ולא ניכנס לזה. פשוט נחכה עד שאחד מהסימנים האלה יופיע ואז נדע שאפשר להמשיך.
איך ממשיכים? לא, עדיין לא חוזרים לקוד הרגיל – יש קצת ניקיונות שצריך לעשות. אנחנו צריכים להחזיר, ידנית, את EEIF ל-0, ולהודיע לפיק שגמרנו עם הכתיבה להפעם על ידי כתיבת 0 ל-WREN.
בקוד, כל הסיפור הזה נראה פחות או יותר ככה:
void EEPROMWrite(char addr, char data) { GIE = 0; // Disable interrupts EEADR = addr; EEDATA = data; WREN = 1; // Enable writing EECON2 = 0x55; // Unlock write, step 1 EECON2 = 0xAA; // Unlock write, step 2 WR = 1; // Start writing GIE = 1; // Enable interrupts while (!EEIF) ; // Wait for write completion WREN = 0; // Disable writing EEIF = 0; // Clear completion flag }
לסיום
את כל הפרטים האלה אף אחד לא אמור לנחש. הם כתובים במפורש, אם כי בצורה קצת יותר טכנית, ב-datasheet, ובמוקדם או במאוחר כל מתכנת מיקרו-בקרים צריך להגיע לשם.
מה שהוצג כאן הוא רק הבסיס לעבודה עם EEPROM, בפיק ובכלל. אם התוכנה שלכם רצינית, היא תבצע גם וידוא של הכתיבה ותגיב בהתאם אם יתגלה שלמרות הכל אירעה שגיאה. כמו כן, צריך לתכנן את הקוד כך שלא יגרום לבלאי מהיר מדי של ה-EEPROM, ושזמן הכתיבה הממושך לא יגרום לפגיעה זמנית בפונקציה קריטית כלשהי של המערכת.
בעתיד סביר להניח שנראה יותר ויותר מיקרו-בקרים עם זיכרון FRAM, שהוא גם בלתי נדיף כמו EEPROM וגם מהיר וללא מגבלת כתיבה, כמו RAM. אין ספק שהטכנולוגיה החדשה הזו תחסוך לנו הרבה כאבי ראש – אך עד שזה יקרה, כמו שאומרים, זה מה יש…
תוספת I: מה לדעתכם קורה אם כותבים ל-EEADR כתובת גדולה יותר מ-127? מסתבר שהפיק מתעלם מהביט הגבוה ביותר ומתחיל לספור שוב מהתחלה: כתובת 128 היא בעצם כתובת 0, כתובת 129 היא 1 וכו'. זהירות!
תוספת II: שימו לב, בסביבת הפיתוח MPLAB X, באפשרויות הצורב בתוך ה-Properties של הפרויקט, ברירת המחדל היא לנקות את ה-EEPROM לגמרי בכל צריבת קוד. כדי למנוע את זה, בטלו את הסימון בתיבה המתאימה.