איך לא להרוג את ה-EEPROM

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

"בעוד כמה זמן?!" זעקה פתאום בבהלה קשישה אחת מהקהל.

"חמישה מיליארדי שנים, גברתי,"

"איזה מזל," נרגעה הקשישה, "חשבתי שאמרת חמישה מיליונים."

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

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

0. לפני שנתחיל…

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

בתור התחלה: האם מגבלת הכתיבות ל-EEPROM היא 100,000 בסך הכל הכללי? או אולי לכל בייט בו בנפרד? או בעצם לקבוצות של בייטים צמודים (מה שנקרא page)? המפרט של המיקרו-בקר של הארדואינו לא מתייחס לשאלה הזו במפורש, והתשובה לגמרי לא מובנת מאליה. למזלנו, אנשים טובים כבר ביצעו ניסויים (לא רשמיים, אמנם) וגילו שהפגיעה לאחר מספר כתיבות רב היא בבייט הספציפי שנכתב, ואילו הבייטים האחרים אינם מושפעים. בכל הניסויים הללו, שהתבצעו אמנם בתנאי טמפרטורה ומתח רגילים, כמות הכתיבות שהובילה לכשל נמדדה במיליונים.

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

עם המידע הזה, בואו ניגש לשיטות לעבודה נכונה עם ה-EEPROM.

1. לכתוב מעט

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

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

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

2. לעדכן, לא לדרוס

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

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

3. פיזור בין תוכניות

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

נניח שאני כותב תוכנית לארדואינו, שמשתמשת בבייט בכתובת 0 ב-EEPROM. אני מבצע המון ניסויים, בדיקות ושינויים, ובגללם הגעתי – סתם לדוגמה – לעשרת אלפים כתיבות. עכשיו כשיש לי גירסה סופית ובדוקה, למה לא לעבור לבייט מספר 812, או 70? צריך רק לשנות קבוע אחד בתוכנה, וכבר הרווחתי 10,000 כתיבות עתידיות. כמו כן, אם אני מעלה על אותו מיקרו-בקר תוכנית חדשה ושונה לגמרי, אין שום סיבה לעבוד עם הבייט המשומש ההוא – אפשר להתחיל בכיף בכתובת אקראית אחרת.

4. פיזור בתוך תוכניות

אם כל כך קל לפזר את הכתיבה, למה לא לעשות את זה בתוך התוכנית עצמה? במקום לסנג'ר כל פעם את הבייט האומלל בכתובת 0, למה לא לכתוב את גיבוי מצב המערכת לכתובת 0, אחרי זה ל-1, אחרי זה ל-2 וכן הלאה? ואם מגיעים לקצה ה-EEPROM, אין בעיה – פשוט מתחילים מהתחלה…

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

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

דבר ראשון, אני ממלא את ה-EEPROM בערכי 255. דברים כאלה עושים עם תוכנת עזר חד-פעמית קטנה, וחשוב להקפיד שהמילוי הזה ייעשה בפונקציה setup ולא בפונקציה loop. לא צריך להסביר למה, נכון?

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

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

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

זהירות, מלכודת!

הנה תרגיל מחשבתי קטן בשבילכם. כידוע, כתיבה ל-EEPROM היא פעולה ממושכת – 3.3 אלפיות השניה. נניח שכתבתי את ערך הגיבוי בכתובת 100, ובדיוק אז יש הפסקת חשמל. לא הספקתי לכתוב 255 בכתובת 101, והערך שיש שם הוא משהו ישן ולא רלוונטי. מה יקרה כשהחשמל יחזור? ומה עושים נגד זה?

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

יש לי שאלה קצת לא קשורה לנושא, אבל קשורה באופן כללי לתכנות צ'יפים. אני לא מומחה עם ארדוינו אונו (אבל יש לי רקע טכני), ואני מתחיל להבין איך לעבוד איתו. שמעתי שזה אפשרי להשתמש בארדוינו (Uno) כ PIC Progarmmer, ולכן אני רוצה לנסות קצת לתכנת צ'יפים. יש לי רכיב מסוג: PIC16C505 שאני מנסה לחבר, אבל אני לא בטוח איזה פינים להשתמש. לא מצאתי פין MCLR/Pvv ו אני לא בטוח אם אפשר להשתמש בפינים 8 ו 9 כ CLOCK ו כ DATA I/O…. אני רוצה (בתור התחלה) לקרוא את המידע המתוכנת מראש בצ'יפ, אז איך אני עושה את זה? תודה מראש,… לקרוא עוד »

בכל מקרה כתיבה מתבצעת בביטים, גם אם השכבה מעל מראה כתיבה ל"בלוק". יאיר, נראה לי שצריך להתייחס לזיכרון כאל רשימה מקושרת שהבייט האחרון מקושר לבייט הראשון. בצורה כזאת אין בייט ראשון או אחרון, רק את הבייט הקודם והבייט הבא. מכיוון ש-255 הוא ערך אסור (הנחה בסעיף 4), יש לנו בזיכון לכל היותר מקטע אחד בזיכרון הכולל רצף של n בייטים עם הערך 255, ואנחנו צריכים לכתוב לראשון (כלומר הקודם ביותר) ביניהם. הבעיה היא כמובן כאשר n=1 שאז צריך בפועל לכתוב את הערך 255 לתא הבא. אין כאן צורך בטרנזקציה, מכיוון שמקרה הקצה הוא קריסה של המערכת בין כתיבת 255 לבייט… לקרוא עוד »

מעניין מה קורה כאן עם הסדר של התגובות…
בכל מקרה סליחה על האירוך (הכותב אינו בוגר מדמ"ח)

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

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

אפשר להכניס סוג של TRNASACTION, שבו מוסיפים עוד ערך שמור (נאמר 254) שמייצג את תחילת תהליך השמירה. בתחילת התהליך כותבים 254 לתא 101, אז כותבים את הערך לתא 100 ואז כותבים 255 לתא 101. בעלייה, אם מוצאים 255, לוקחים את הערך שלפניו. אם רואים 254, לוקחים את הערך הקודם אם הוא שונה מ-255 או שני ערכים אחורה אם הוא שווה 255 (ואז מקבלים את הערך האחרון שנשמר בהצלחה). כמובן שזה מוסיף עוד שמירה, ואני לא בטוח שהאלגוריתם הזה מכסה הכל (יכול להיות שצריך להפוך את הסדר ולחפש קודם את 254 ולראות איך הוא מתנהג בהפעלה ראשונה וכשמגיעים לתא האחרון וכו'),… לקרוא עוד »

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

ואז השאלה הלוגית הבאה היא מה קורה כשאתה מגיע לבייט האחרון? האם יש אפשרות לבצע כתיבה אטומית לשני בייטים לא רצופים?

תשובה?