מידע טכני שימושי יותר או פחות, שיעזור לנו להבין טוב יותר את ה-EEPROM של המיקרו-בקר, וכן לקרוא ממנו, לכתוב אליו ובכלל לנצל אותו באופן אופטימלי.
כשעובדים בשפת C ומעלה, הגישה לזיכרון ה-SRAM של מיקרו-בקר היא בדרך כלל טריוויאלית ושקופה: אנחנו פשוט מגדירים משתנה או מערך, והקומפיילר דואג לכל השאר. בזיכרון ה-FLASH מאוחסן הקוד שלנו, ולרוב אנחנו לא נוגעים בו. בין שני סוגי הזיכרון האלה, כביכול, נמצא בשבבים רבים ה-EEPROM: מקום לאחסון של נתונים, שנשמרים גם כשאין אספקת חשמל. בניגוד ל-SRAM, הגישה ל-EEPROM לא רק לא-טריוויאלית, היא גם נעשית באופן שונה לגמרי בין דגמי מיקרו-בקרים שונים: לכל דגם יש טכניקות, יכולות, פיצ'רים ומגבלות משלו. אם נשקיע ונלמד אותם, נוכל להפיק ממנו הרבה יותר. אז הנה כמה דברים שכדאי לדעת על ה-EEPROM של ה-ATmega4809.
מיפוי
ל-4809 יש EEPROM רגיל בנפח 256 בייטים, שנמצאים בכתובות 0x1400 עד 0x14FF כולל. הוא מחולק ל-4 "דפים" (pages) בגודל 64 בייטים כל אחד – את המשמעות של זה נראה בהמשך.
יש דף אחד נוסף של EEPROM שנקרא User Row. הכתובת שלו היא 0x1300 עד 0x133F כולל, ובניגוד ל-EEPROM הרגיל, זיכרון זה לא נמחק כשמתבצעת מחיקה מלאה של הזיכרונות ("Chip Erase") בעת צריבת קוד חדש. זה יכול להיות שימושי, למשל, לשמירה של ערכי כיול שנמדדו לאורך זמן ובהשקעה רבה ושחבל מאוד לאבד.
עוד אזור שכדאי להכיר, אף על פי שהוא באמת לקריאה בלבד, נקרא Signature Row ומתחיל בכתובת 0x1100. שלושת הבייטים הראשונים בו הם מזהה דגם המיקרו-בקר (עבור ה-4809 זה 0x1E,0x96,0x51), ועשרת הבאים מהווים, יחדיו, קוד ייחודי לזיהוי של המיקרו-בקר הספציפי. בהנחה ש-Microchip לא פישלו בייצור, אין בעולם כולו שני 4809 עם אותו קוד, וגם זה מידע שיכול להיות חשוב ומועיל בסיטואציות מסוימות. יש ב-SIGROW עוד נתונים – ערכי כיול שונים – אך לא נדבר עליהם כאן.
קריאה
אם שמתם לב, כל תת-הסוגים של ה-EEPROM – ולמעשה, כל הזיכרונות של ה-4809 – נמצאים מבחינה לוגית/תכנותית באותו מרחב כתובות. המשמעות היא שאם נגדיר בקוד שלנו מצביע-לבייט, נוכל לתת לו כל כתובת שהיא בין 0x0000 ל-0xFFFF, ולקרוא דרכו כל בייט שנמצא ב-SRAM, ב-FLASH, ב-EEPROM או במקומות מוזרים עוד יותר. כדי לקרוא את הבייט הרביעי ב-User Row, למשל, אנחנו צריכים רק לזכור שכתובת הבסיס היא 0x1300, להוסיף לה 3 (כי מתחילים לספור מאפס), ולקרוא משם.
כתיבה
לכאורה, כפי שיצרו שיטה אחידה וידידותית-למשתמש לקריאה של סוגי הזיכרונות השונים, היו יכולים ליצור שיטה אחידה גם לכתיבה אליהם. אלא שתהליך הכתיבה הפיזי שונה עד כדי כך, שזה יהיה מאוד לא יעיל – שלא לדבר עם מגבלת מספר הכתיבות לזיכרונות מסוג EEPROM ו-FLASH; אם ממשק אחיד יפתה אותנו לגשת אליהם בלי חשבון, כמו אל SRAM, הם עלולים להישחק לחלוטין תוך שניות.
מה שנבחר בפועל הוא מעין פשרה. אנחנו כותבים בייטים ישירות לכתובות של ה-EEPROM וה-FLASH, אבל המידע הזה לא באמת נצרב לזיכרונות אלא רק מחכה באיזשהו חוצץ נסתר. רק כשנסיים לכתוב את הנתונים הדרושים, ברמת page, נריץ פקודה מיוחדת שתבצע את ההעתקה הפיזית מהחוצץ אל הזיכרון הקבוע.
מה פירוש המילים "ברמת page"? הכתיבה בפועל מתבצעת כאמור ביחידות של דפים שלמים, pages. החוצץ הנסתר בנוי כך שיספיק לדף אחד בלבד, אז אם אנחנו כותבים בייט לכתובת ששייכת לדף X ומיד אחר כך לכתובת ששייכת לדף Y, החוצץ "שוכח" את X ועובר להתייחס ל-Y. כשנשלח את פקודת הצריבה הסופית, רק דף Y ייצרב (אבל שימו לב למידע החשוב שבהמשך!)
אגב, פונקציה שמכילה את פקודת הצריבה יכולה להיראות כך:
עד כאן התיאוריה בגדול, אבל יש עוד הרבה פרטים קטנים וחשובים מאוד, חלקם מוזכרים ב-Datasheet וחלקם לא:
כתיבה לחוצץ היא פעולת AND
אם כתבתי לכתובת מסוימת ב-EEPROM את הערך 3, ומיד לאחר מכן התחרטתי וכתבתי שם 6, מה יהיה הערך שיישמר בסופו של דבר כשאתן את פקודת הצריבה?
בניגוד לשכל הישר, התשובה היא 2. זאת מפני שהערכים לא נכתבים כמות שהם, אלא מתבצעת כל פעם פעולת AND-ביטים בינם לבין מה שכבר קיים בחוצץ. כמובן, הערך ההתחלתי של כל הבייטים בחוצץ לאחר איפוס הוא 255. זה יהיה ברור יותר אם נסתכל ויזואלית על כל הערכים בייצוג בינארי:
255=0b11111111
003=0b00000011
006=0b00000110
002=0b00000010
השורה התחתונה היא התוצאה שמתקבלת מפעולת AND-ביטים ("&") על שלוש השורות שמעליה.
החוצץ שומר ערכים אחרי חציית גבול
שימו לב, יש כאן מלכודת רצינית. ב-Datasheet (סעיף 9.3.2.2, במהדורה DS40002173C) מוזכר שהחוצץ נמחק אוטומטית כשהמיקרו-בקר מתאפס, או בעקבות פקודת מחיקת חוצץ (כמובן), או בעקבות פקודת כתיבה או מחיקה של דף כלשהו, או בהתעוררות ממצב שינה. סבבה. אבל נסתכל על תסריט אחר:
- אני כותב 111 לכתובת 0x1400 – הבייט הראשון ב-page הראשון
- מסיבה כלשהי, החלטתי לא לצרוב את הערך אלא לעבור ל-page אחר, אז…
- אני כותב 222 לכתובת 0x1441 – הבייט השני ב-page השני
- עכשיו אני משלים רשמית את הצריבה. מה יופיע בשתי הכתובות האלה?
בכתובת הראשונה, 0x1400, יהיה אותו ערך שהיה שם קודם: כצפוי, הכתיבה הנוכחית לא נגעה בו. בכתובת השנייה, 0x1441, יופיע הערך 222 שכתבתי עכשיו. אבל בכתובת 0x1440 – הבייט הראשון של הדף השני – יופיע כעת הערך 111 ! זאת מפני שהחוצץ שומר את הערכים שנכתבו בו גם כשעוברים לכתובות ששייכות לדף אחר. כך, בדוגמה הזו, מה שנמצא בכתובת הראשונה בחוצץ נכתב לכתובת הראשונה ב-page, אף על פי שזה בכלל לא אותו page שאיתו התחלנו.
ומה לגבי ערכי ברירת המחדל של החוצץ, כל אותם 255? האם גם הם נכתבים בלי שליטה? לא. ליתר דיוק, ב-FLASH זה כן יקרה, שם כל בייט בחוצץ ייכתב בדיוק כמו שהוא, אבל ל-EEPROM יש תת-רזולוציה של בייט יחיד בכל דף, כך שאם שינינו רק בייט אחד בחוצץ, אז רק הוא ייכתב.
בזמן שכתבת
אחד ההבדלים הפונקציונליים הגדולים בין זיכרונות לא-נדיפים כמו ה-EEPROM לבין SRAM הוא זמן הכתיבה: צריבת ערכים חדשים ל-EEPROM נמשכת מספר אלפיות שנייה, שזה המון במונחי מיקרו-בקרים. וזה מעלה שתי שאלות מעניינות: ראשית, האם כתיבת הבייטים מהחוצץ נעשית באופן סדרתי או במקביל (כלומר, האם יש קשר בין מספר הבייטים הנכתבים לבין משך הצריבה)? ושנית, מה קורה אם אני כותב ערכים חדשים לחוצץ בזמן שהצריבה מתבצעת?
כתבתי קוד פשוט, בארדואינו (ללוח Nano Every שהמיקרו-בקר בו הוא ה-ATmega4809), שימדוד את הזמנים – גם בעצמו עם הפונקציה micros, וגם בהפעלה של פין פלט, שאותו בדקתי בנפרד עם סקופ. כתיבה של בייט יחיד ל-EEPROM נמשכה ארבע אלפיות השנייה, לפי שתי המדידות. זה מסתדר עם המידע שב-Datasheet. גם כתיבה של 10 בייטים נמשכה 4ms, וגם של 60 בייטים (כולם באותו page כמובן). התוצאה הזו לא מפתיעה – להיפך, ככה עובד הרעיון של page, וזה היה מוזר ומטריד אם כתיבה של 64 בייטים הייתה לוקחת רבע שנייה…
אבל ארבע אלפיות השנייה זה עדיין הרבה זמן, די והותר כדי לשחק עם הערכים שבחוצץ. כתבתי קוד שמתחיל צריבה של הערכים 1 עד 64 לדף מסוים, אבל בזמן ה-4ms של הצריבה, כותב את הערך 0x55 לכל אותם בייטים, בהפרשים של 55 מיליוניות השנייה בין בייט לבייט. לאכזבתי, כל הערכים המקוריים נכתבו בדיוק כפי שביקשתי בהתחלה, ולעדכון לא הייתה כל השפעה. מצד שני, כשהוספתי פקודה לצרוב שוב את החוצץ בסיום הצריבה הקודמת, כל הערכים הרלוונטיים ב-EEPROM התעדכנו ל-0x55. זאת אומרת, מרגע שניתנה פקודת צריבה, החוצץ מתפנה ומוכן לכתיבת ערכים חדשים לצריבה עתידית, ולא צריך להמתין לסיום הקודמת.
איך בודקים מתי הסתיימה צריבה? ביט 1 (השני מימין) ברגיסטר NVMCTRL.STATUS נקרא EEBUSY, וכאשר הערך שלו "1", זה אומר שה-EEPROM מבצע כרגע איזושהי פעולה. כשהוא "0", ה-EEPROM פנוי ומוכן לבצע פקודת מחיקה או צריבה.
ה-eeprom הזה מריח ומתנהג פשוט כמו פלאש.
סיכוי טוב שפיזית הוא מהווה חלק מהפלאש של הרכיב ולא יחיד. נפרדת, ורק מופו כתובות מסויימות אליו.
ברכיבים יותר ישנים של אטמל יש פקודה בשם SPM שכותבת ישירות לפלאש, בלי צורך במחיקה, וכך גם הכתיבה ל-eeprom בממשק הרלוונטי.
יכול להיות שפעם הכל היה eeprom והיום פלאש זול להם יותר… אז סגרו אזור מסויים שלא תוכל לרוץ ממנו ולדרוס לעצמך את הקוד בזמן ריצה בלי כוונה, וקראו לזה eeprom כי לזה כולם רגילים.
קונספירציה יפה, אבל – קודם כל, יש התחייבות ב-Datasheet ל-10K כתיבות לפלאש לעומת 100K ל-EEPROM, ובנוסף יש דגלים נפרדים ל-EEPROM Busy ול-Flash Busy. לדעתי, אם באמת היו מאחדים אותם פיזית (או מצליחים לייצר פלאש עם 100K כתיבות), הם היו דווקא משוויצים בפיצ'ר החדש הזה.
איך עוד היית בודק כזו השערה? האם צריכת הזרם בכתיבה אמורה להיות שונה? זה באמת קצת חשוד שבטבלת הצריכה של ה-peripherals לא מוזכר EEPROM אלא רק פלאש 😀
…עכשיו נזכרתי גם שגודל ה-page של הפלאש הוא 128, בניגוד ל-64 של ה-EEPROM. ליצור את ההבדלים האלה באופן מלאכותי רק כדי לשמר העמדת פנים של EEPROM נראה לי קצת מוגזם. אגב, זה לא היה נושא הפוסט אז לא הזכרתי את העניין, אבל בעזרת הפיוזים אפשר להגדיר אזור בפלאש לשמירה של נתונים (application data) בנפרד מהקוד. אולי אסקר את זה בפוסט קרוב.
אני מפליג עכשיו בספקולציות, אבל זכורני שרוחב הפקודות של AVR הוא 16 ביט, בעוד מידע מה-eeprom נקרא לזכרון שעובד ב-8 ביט.
יתכן ובכל זאת מדובר באותו בלוק של פלאש, רק שעבור הפקודות יש כל פעם שניים כאלה במקביל?
לא הצלחתי למצוא משהו מעניין על זה בדאטהשיט, אבל גם סביר שפספסתי. בנוסף אין סיבה שזה יעניין משתמש סביר ולכן לא הייתי מצפה שיופיע בדאטהשיט.
בכל מקרה, אם אני זוכר נכון ב-STM8 היה את השטיק הזה עם ה-eeprom שהוא פשוט סתם טווח כתובות בפלאש.