facebook pixel בלוג: dual boot - הצלחה - www.4project.co.il
Main logo www.4project.co.il
כל הרכיבים לפרוייקט שלכם
עגלת קניות

העגלה ריקה

לקוחות נכבדים, אלה שעות הפעילות של המחסן במהלך פסח 2024:
ערב חג וחג ראשון (22-23/04) - המחסן סגור
חול המועד (24-25/04) - המחסן יפעל בין 8:00 עד 15:00
ערב חג וחג שני (28-29/04) - המחסן סגור
נחזור לפעילות רגילה ביום שלישי 30/04
חג שמח!

dual boot - הצלחה


2023-10-05 18:17:07

מבוא

זהו פוסט רביעי בדרך ל-dual boot ואפשרות לעדכן תוכנה מרחוק. ממליץ מאוד לקרוא את שלושת הפוסטים הקודמים כדי להבין את כל הפרטים של פוסט זה.

עמ;לק: כדי להתקדם עם הרעיונות שלי לפתרונות בית חכם, אני צריך שלרכיבים המפוזרים ברחבי הבית תהיה אפשרות לעדכן את התוכנה בלי שאני צריך להתחבר ל-USB שלהם כדי לצרוב משהו. הרכיבים יכולים להיות מוחבאים במקומות ממש לא נגישים (מסתור כביסה, ארגז של התריס, קופסאות חשמל וכו').

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

בפוסט הראשון מצאתי את הדרך להוסיף איזורי זכרון לקומפילציה. בפוסט זה אשתמש בידע זה כדי להגדיר את הזכרון בצורה הנוכונה למימוש ה-dual boot.
בפוסט השני חקרתי קצת יותר לעומק והבנתי מה צריך להיות בתחילת כל איזור זכרון כדי שהתוכנה תוכל לרוץ ממנו (ספויילר: לא main וגם לא setup). גם ידע זה יהיה נחוץ למימוש ה-dual boot. בפוסט השני הצלחתי להגיע גם לקובץ ה-HEX של הקומפילציה שמכיל את התוכנה בפורמט "Intel HEX format", ידע זה נחוץ כדי שנדע מה צריך לצרוב.
בפוסט השלישי הגעתי למצב שקוד ה-POSTBOOT שלי נצרב יחד עם קוד אחד מאיזורי הזיכרון של התוכנית הרגילה ותהליך ה-boot מצליח לעבור את כל השלבים.

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

טיפול ב-HEX

אחרי כל המחקר שעשיתי בפרקים הקודמים של פרוייקט זה, הגיע הזמן לצרוב את הקוד ב-BANK הזכרון שלא רצים ממנו. איך אפשר לדעת מאיזה BANK רצים? יש כמה דרכים, אבל החלטתי פשוט לבדוק את ערך הרגיסטר VTOR שאמור להצביע לטבלת המצביעים שנמצאת בתחילת ה-BANK ממנו הקוד רץ. הסברתי זאת בפוסט השלישי, שמה שה-bootloader המקורי עושה בסוף, הוא משנה את המצביע של ה-Stack לכתובת הראשונה בטבלת המצביעים, משנה את הרגיסטר VTOR להצביע לתחילת טבלת המצביעים וקופץ לכתובת שרשומה במצביע השני בטבלה. זה גם מה שאני עושה ב-POSTBOOT שרץ אחרי ה-bootloader המקורי. כך שה-VTOR תמיד מצביע לתחילת טבלת המצביעים, ולכל BANK זכרון יש טבלה משלה.

קודם כל, תעשו לעצמכם טובה, וכשאתם עובדים על פרוייקט שהוא קצת יותר מסובך מה-blink, תחלקו אותו למודולים לוגיים, כלומר לפחות לקבצים שונים. אם אתם כותבים את הקוד ב-C, אז תתחילו את שמות הפונקציות בקידות שמציינת את המודול, אם זה ב- ++C, אז תחלקו את הקוד למחלקות המתאימות. במקרה שלי, יש לי קבצים נפרדים ל-postboot מהניסויים הקודמים, ועכשיו הגדרתי מחלקה בשם fwUpgrade_c בקבצי h. ו-cpp. נפרדים שתכלול את כל הלוגיקה של עדכון התוכנה (Firmware Upgrade).

פונקציה זו מחזירה את מספר BANK הזכרון ממנו רצים כרגע:
קוד: בחר הכל
int fwUpgrade_c::getCurrentFlashBank()
{
    uint32_t vectorTable = SCB->VTOR;

    if (vectorTable == fwUpgrade_c::FLASH_BANK0_ADDRESS)
    {
        return 0;
    }
    else if (vectorTable == fwUpgrade_c::FLASH_BANK1_ADDRESS)
    {
        return 1;
    }
    else
    {
        // Something is wrong
        return -1;
    }
}


כמו שהסברתי קודם, התוכנית היא לקמפל כל גרסה פעמיים, פעם ל-BANK0 ופעם ל-BANK1. הצד שישלח את העדכון (נקרא לו Server) ידע איזו גרסה צריך לשלוח, הוא יוכל לשאול זאת מהצד שעושה את העדכון (נקרא לו Client) מאיזה BANK הוא רץ כרגע, וה-Client יוכל להחזיר את הערך של הפונקציה הזו.

כך נראות ההגדרות של הזכרון בקובץ ה-LD:
קוד: בחר הכל
MEMORY
{
  /*FLASH (rx) : ORIGIN = 0x00000000+0x2000, LENGTH = 0x00040000-0x2000*/ /* Original: First 8KB used by bootloader */

  /* First 8KB used by bootloader, 4K for post-boot logic */
  POSTBOOT (rx) : ORIGIN = 0x00000000+0x2000, LENGTH = 0x1000
  /* First 8KB used by bootloader, 4K post-boot, the rest is divided for 2 banks */

  FLASH (rx) : ORIGIN = 0x00000000+0x2000+0x1000, LENGTH = 0x0001E800
  /* FLASH (rx) : ORIGIN = 0x00000000+0x2000+0x1000, LENGTH = 0x0001E800 // Bank 0 */
  /* FLASH (rx) : ORIGIN = 0x0001E800+0x2000+0x1000, LENGTH = 0x0001E800 // Bank 1 */
  //FLASH (rx) : ORIGIN = 0x0001E800+0x2000+0x1000, LENGTH = 0x0001E800

  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}


כרגע אני פשוט מעביר את ה-// בין השורה הראשונה והאחרונה של הגדרת ה-FLASH כדי לקמפל ל-BANK0 או BANK1, אבל בסופו של דבר אפשר יהיה לעשות את זה בקלות ע"י סקריפט פשוט.

אז קימפלטי, יש לי את הקובץ HEX עם הקוד שצריך לצרוב. בפוסט השני ראינו את הפורמט של הקובץ. זהו קובץ טקסט עם מבנה פשוט שקל להבין גם בעין וגם לא מסובך במיוחד לטפל בו במיקרובקר. התכנון שלי היא שה-Server הוא זה שיטפל בקובץ זה ויעביר ל-Client את מה שצריך בצורה דיגיטלית, כך שכל הקוד שעושה את ה-parsing לפורמט בינתיים השארתי בפונקציית ()main לטובת הניסוי, אחרי זה אשתמש בקוד זה כשאכתוב את צד ה-Server. בינתיים אשתמש ב-Serial console כערוץ התקשורת עם הבקר ואעשה copy&paste ל-console את כל התוכן של קובץ ה-HEX. הקוד מחפש את התו ":" כסימן לתחילת שורת ה-HEX, שולף מהשורה את השדות, בודק שה-CRC תואם למידע שהגיע ומעביר את כל המידע לפונקציה ()processData של מחלקת ה-fwUpgrade_c, שם המידע יטופל בצורה לוגית (יש פקודות להחלפת כתובת, יש מידע שצריך לצרוב וכו').

כחלק מקוד זה הוספתי גם כמה דברים נוספים שיעזרו לי לדמות את פרוטוקול התקשורת בין ה-Server וה-Client וגם קצת Debug. כאשר אני שולח "S" כקלט לבקר, הוא קורא לפונקציה ()startUpgrade כדי לבצע את מה שצריך כחלק מהכנה לעדכון תוכנה. כאשר אני שולח "E", הוא קורא לפונקציה ()endUpgrade כדי לבצע בדיקות תקינות של מה שעודכן. כאשר אני שולח "?" הבקר ידפיס ל-Serial קצת מידע שנראה לי שימושי, כמו את מספר ה-BANK הנוכחי שהקוד רץ כרגע ממנו ו-64 בתים הראשונים של שני איזורי הזכרון כדי לראות את מה שנכתב שם. כל התוספות האלה לא מתנגשות עם הטיפול בקובץ HEX כי כל עוד אין את סימן התחלת שורת הנתונים של ה-HEX (סימן ה-":"), הקוד מדלג עליה.

הוספתי כמה הדפסות לפונקציות שונות כדי לראות שכל התהליך עובד עוד לפני שאני מנסה לצרוב משהו לתוך ה-FLASH. כשהכל רץ כמצופה, עברתי לשלב הבא: צריבה עצמה.

צריבת ה-FLASH

כמו שהזכרתי קודם, קוד של ה-bootloader לכרטיס ה-XIAO של SeeedStudio מגיע כמערך ארוך של נתונים וכמה פונקציות שצורבות את הנתונים האלה ל-FLASH. פונקציות אלה זה בדיוק מה שאני צריך כדי להשלים את תהליך העדכון שאני מתכנן. בסופו של דבר אלה פונקציות פשוטות יחסית שמשתמשות ברגיסטרים שונים ששולטים על הגישה ל-FLASH. כל התהליך מפורט היטב בפרק "NVMCTRL – Nonvolatile Memory Controller" של מפרט בקר ה-SAMD21.

לפני שנראה איך הם צורבים את ה-FLASH מתוך התוכנה שרצה, קודם צריך להבין איך עובדים עם זכרון FLASH...
כל גישה לזכרון FLASH מתבצעת במנות שנקראות PAGE (עמוד). במקרה שלנו גודל ה-PAGE הוא 64 בתים. כן, כל גושה, גם קריאה וגם כתיבה. בתוך המיקרובקר יש מודול שאחראי לגישות ל-FLASH, כך שכאשר אתם ניגשים לכתובת כלשהי בזכרון ה-FLASH, המודול הזה שולף את ה-PAGE לתוך זכרון זמני בתוך המיקרובקר (cache) ומחזיר לכם את הנתון שביקשתם. אם תקראו כתובת שעדיין בתוך ה-PAGE הנוכחי, הנתון ישלף מתוך ה-cache ולא מה-FLASH עצמו.
אותו הרעיון גם עם הכתיבה ל-FLASH. צריך לספק PAGE שלם של נתונים כדי שהוא יכתב ל-FLASH. למודול שמטפל בגישה ל-FLASH יש כל מני מצבי עבודה שונים, הקוד של ה-bootloader משתמש במצב עבודה אוטומטי, שלפי מה שרשום במפרט, פשוט כותבים את הנתונים לכתובות זכרון וכאשר יש כתיבה לכתובת אחרונה של PAGE, כל ה-PAGE נכתב ל-FLASH בצורה אוטומטית. הקוד של bootloader גם מנקה את זכרון ה-cache לפני תחילת הכתיבה ויוזם כתיבה במקרה שיש מקרה שלא ממלאים את כל ה-PAGE בנתונים (בשפה מקצועית קוראים לזה buffer flush).

בנוסף לכתיבה במנות, לפני שכותבים ל-FLASH, צריך לאפס/למחוק אותו (Erase). המחיקה גורמת להופעת 0xFF בכל הכתובות של ה-FLASH. בנוסף ל-PAGEs שהסברתי קודם, זכרון ה-FLASH מסודר בשורות (ROWs), כאשר בכל שורה יש 4 עמודים (PAGEs). פעולת המחיקה מתבצעת על כל השורה, כלומר מוחקים 4 עמודים (PAGEs) בפעולה אחת.

אוקי... עכשיו אפשר לחזור לפונקציות שמצאתי בקוד ה-bootloader של XIAO.
בואו נעבור על פונקציית ה-()setup של התוכנית:
קוד: בחר הכל
void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, HIGH);

    if (8 << NVMCTRL->PARAM.bit.PSZ != FLASH_PAGE_SIZE)
        while (1) {
        }

    __disable_irq();

    setBootProt(7); // 0k

    const uint8_t *ptr = bootloader;
    int i;

    for (i = 0; i < BOOTLOADER_K; ++i) {
        int crc = 0;
        for (int j = 0; j < 1024; ++j) {
            crc = add_crc(*ptr++, crc);
        }
        if (bootloader_crcs != crc) {
            while (1) {
            }
        }
    }

    for (i = 0; i < BOOTLOADER_K * 1024; i += FLASH_ROW_SIZE) {
        memcpy(pageBuf, &bootloader, FLASH_ROW_SIZE);
        flash_write_row((uint32_t *)(void *)i, (uint32_t *)(void *)pageBuf);
    }

    // re-base int vector back to bootloader, so that the flash erase below doesn't write over the
    // vectors
    SCB->VTOR = 0;

    // erase first row of this updater app, so the bootloader doesn't start us again
    flash_erase_row((uint32_t *)(void *)(BOOTLOADER_K * 1024));

    for (i = 0; i < 5; ++i) {
        digitalWrite(LED_BUILTIN, HIGH);
        mydelay(100);
        digitalWrite(LED_BUILTIN, LOW);
        mydelay(200);
    }

    setBootProt(2); // 8k

    while (1) {
    }
}


הפונקציה עוצרת את הפסיקות החיצוניות כדי שלא יפריעו במהלך הצריבה, מבטלת את הגנת הצריבה (הבקר מאפשר להגן על איזורים התחלתיים של הזכרון מפני כתיבה, מה שמעולה בתור הגנה על איזור ה-bootloader), עושים חישוב CRC על מערך הנתונים כדי לוודא שאף אחד לא שינה משהו. אחרי זה צורבים את הנתונים ע"י קריאה ל-()flash_write_row עבור כל FLASH_ROW_SIZE, כאשר לפני כל כתיבה מעתיקים את הנתונים למערך זמני בגודל של ה-ROW, שזה 4 יחידות של PAGE, כלומר סה"כ 256 בתים. אחרי זה מוחקים שורה ראשונה של אזור התוכנית הרגילה כדי להכריח לצרוב את התוכנית הבאה בעזרת ה-bootloader החדש, ונועלים את 8k הראשונים של ה-FLASH לכתיבה (8k הראשונים זה איזור ה-bootloader).

התהליך די פשוט. בואו נראה מה יש בתוך ה-()flash_write_row:
קוד: בחר הכל
void flash_write_row(uint32_t *dst, uint32_t *src) {
    flash_erase_row(dst);
    flash_write_words(dst, src, FLASH_ROW_SIZE / 4);
}


מוחקים את השורה בכתובת שציינו ומבצעים את הכתיבה. פונקציות שנקראות מתוך ()flash_write_row יורדות לרמת הרגיסטרים של מיקרובקר:
קוד: בחר הכל
static inline void wait_ready(void) {
    while (NVMCTRL->INTFLAG.bit.READY == 0) {
    }
}

void flash_erase_row(uint32_t *dst) {
    wait_ready();
    NVMCTRL->STATUS.reg = NVMCTRL_STATUS_MASK;

    // Execute "ER" Erase Row
    NVMCTRL->ADDR.reg = (uint32_t)dst / 2;
    NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_ER;
    wait_ready();
}

void flash_write_words(uint32_t *dst, uint32_t *src, uint32_t n_words) {
    // Set automatic page write
    NVMCTRL->CTRLB.bit.MANW = 0;

    while (n_words > 0) {
        uint32_t len = min(FLASH_PAGE_SIZE >> 2, n_words);
        n_words -= len;

        // Execute "PBC" Page Buffer Clear
        NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_PBC;
        wait_ready();

        // make sure there are no other memory writes here
        // otherwise we get lock-ups

        while (len--)
            *dst++ = *src++;

        // Execute "WP" Write Page
        NVMCTRL->CTRLA.reg = NVMCTRL_CTRLA_CMDEX_KEY | NVMCTRL_CTRLA_CMD_WP;
        wait_ready();
    }
}


ואלה בדיוק הפונקציות שאני צריך...

אבל לצרכים שלי אשנה קצת את התהליך כדי לצמצם שימוש בזכרון. ברור שצריך שיהיה מערך זמני בגודל של PAGE שצריך למלא לפני שצורבים אותו ל-FLASH, רק שהמערך בקוד שלהם הוא פי 4 יותר גדול, כי זה נוח יותר למחוק שורה של 4 עמודים וישר לצרוב את כל רביעה. במקרה שלי הנתונים יגיעו במנות קטנות (כל שורה של קובץ HEX מכילה עד 16 בתים של מידע), כך שאין את כל ה-256 בתים זמינים כמו במקרה שלהם, ואני גם לא רוצה לתפוס 256 בתים של זכרון RAM לטובת עדכון תוכנה שצריך להתבצע מתישהו, כשאפשר לעשות את זה בצורה יעילה יותר...

קצת הסבר לגבי החיסכון בזכרון:
כל מקרה לגופו, אבל במערכות Embedded נהוג לנהל את הזכרון RAM בצורה סטטית, כלומר להגדיר את הזכרונות הדרושים לכל התהליכים מההתחלה כמערכים סטטיים ולא לבצע הקצאת זיכרון דנמית (new, malloc וכו'). כך אפשר לראות בשלב הקומפילציה אם התוכנית גדולה מדי לזכרונות שיש לכם (גם RAM וגם FLASH). בצורה זו מבטיחים שיש זכרון זמין לכל תהליך, ולא יקרה מצב שתהליך כמו עדכון תוכנה לא יוכל להתבצע כי אין זכרון פנוי למערך הזמני שצריך להקצות. הקומפיילר של סביבת ה-Arduino כותב לכם כמה זכרון דרוש לתוכנית בצורה כזו:
קוד: בחר הכל
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM:   [=         ]   8.8% (used 2868 bytes from 32768 bytes)
Flash: [=         ]   5.7% (used 14824 bytes from 262144 bytes)


כך שצריך לחשוב על החיסכון תוך כדי תכנות וכתיבת הקוד...

אז מה שהחלטתי לעשות זה למחוק את כל הזכרון של ה-BANK שמתוכנן לצריבה בשלב של הכנות לעדכון. זוכרים את ה-"S" שאפשר לשלוח דרך ה-Console שהוספתי? פשוט עשיתי לולאה שרצה על כל האיזור שצריך למחוק בקפיצות של גודל השורה:
קוד: בחר הכל
int fwUpgrade_c::startUpgrade()
{
    bytesWritten = 0;
    baseAddress = 0;
    flashBufferStartAddress = 0;
    flashBufferOffset = 0;

    __disable_irq();

    // Clear other bank as a preparation for FLASH write
    switch(getCurrentFlashBank()) {
        case 0:
            for (uint32_t addr=FLASH_BANK1_ADDRESS; addr<FLASH_BANK1_ADDRESS+FLASH_BANK_SIZE; addr+=FLASH_ROW_SIZE) {
                flash_erase_row((uint32_t*)addr);
            }
            break;

        case 1:
            for (uint32_t addr=FLASH_BANK0_ADDRESS; addr<FLASH_BANK0_ADDRESS+FLASH_BANK_SIZE; addr+=FLASH_ROW_SIZE) {
                flash_erase_row((uint32_t*)addr);
            }
            break;

        default:
            // TODO report problem
            break;
    }

    __enable_irq();

    // TODO: Report start of FW upgrade
    return 1;
}


כך שאפשר יהיה לקרוא ל-()flash_write_words עם נתונים בגודל של ה-PAGE, כלומר אחרי שיתמלא מערך של 64 בתים, או בסוף של התהליך כשנשאר משהו במערך שצריך לעשות לו flush.

ניסיון ראשון

נראה שהכל מוכן לצריבה... צורב את הקוד העדכני דרך ה-USB ל-BANK0 שנצרב יחד עם ה-POSTBOOT שלי, שולח "S" דרך הקונסול כדי להתחיל את תהליך העדכון, שולח "?" כדי לראות את מצב הנתונים בשני איזורי הזכרון:
קוד: בחר הכל
Current bank: 0
Bank 0 content:
0x3000: 008000203D4D0000254D0000254D0000
0x3010: 00000000000000000000000000000000
0x3020: 000000000000000000000000254D0000
0x3030: 0000000000000000254D0000A14D0000
Bank 1 content:
0x21800: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
0x21810: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
0x21820: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
0x21830: FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF


הכל נראה תקין, התוכן של BANK0 נצרב דרך ה-bootloader הרגיל, כך שהוא תקין. תוכן של BANK1 הוא שורה של 0xFF כי איזור זה עבר איפוס, כך שגם זה תקין. עושה copy&paste של תוכן קובץ HEX שקימפלתי ל-BANK1, רואה הדפסות שמסמלות שהנתונים נצרבו, בודק שוב עם ה-"?" בקונסול, רואה ש-BANK1 השתנה לתוכן שתואם למה שהיה בקובץ HEX:
קוד: בחר הכל
Current bank: 0
Bank 0 content:
0x3000: 008000203D4D0000254D0000254D0000
0x3010: 00000000000000000000000000000000
0x3020: 000000000000000000000000254D0000
0x3030: 0000000000000000254D0000A14D0000
Bank 1 content:
0x21800: 00800020453502002D3502002D350200
0x21810: 00000000000000000000000000000000
0x21820: 0000000000000000000000002D350200
0x21830: 00000000000000002D350200A9350200


מרוצה עד הגג. מוסיף לתוך ה-()PostBoot_Reset_Handler של ה-POSTBOOT את הקוד שמגדיר כניסה מספר 10 ככניסה דיגיטלית עם נגד PULL_UP, קורא את הערך של הקו ולפי זה מחליט לאיזה BANK לקפוץ אחרי ה-POSTBOOT:
קוד: בחר הכל
// IN port 10, PA6
PORT->Group[0].PINCFG[6].reg = (uint8_t)(1 << 1 | 1 << 2);  // INEN + PULLEN
PORT->Group[0].DIRCLR.reg = (1 << 6);
PORT->Group[0].OUTCLR.reg = (1 << 6);

// port 10 (PA6) is set
if (PORT->Group[0].IN.reg & 1<<6) {
    // BANK 1
    LED_blink(2, 200);
    LED_delay(500);

    jump_to_application(0x0001E800+0x2000+0x1000);
} else {
    // BANK 0
    LED_blink(1, 200);
    LED_delay(500);

    jump_to_application(0x00000000 + 0x2000 + 0x1000);
}


לוקח חוט גישור, מחבר בין הכניסה מספר 10 ל-3.3V, עושה reset ו... שום דבר לא קורה... התוכנה נתקעת איפה שהוא, הלד שבתוך לולאת ה-()loop לא מהבהב Sad emoticon
הוספתי עוד הרבה הדפסות בתהליך הצריבה, השוואתי כל הנתונים שנצרבים ל-FLASH עם התוכן של קובץ ה-HEX, הכל תואם... אבל עדיין לא מצליח להגיע למצב שהתוכנית רצה מ-BANK1. לא היו לי עוד רעיונות איך להתקדם חוץ מלהוסיף הבהובים נוספים לקוד של ה-POSTBOOT או אולי גם ל-()Reset_Handler של התוכנית שנצרבת ל-BANK1, אבל לא היה לי זמן להמשיך עם זה, כך שעזבתי את הפרוייקט עד שיתפנה לי עוד קצת זמן.

ניסיון שני

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

ו... טא-דה! רואים שהצריבה לא כל כך הצליחה:
קוד: בחר הכל
Bank 0 content:
0x3000: 00800020414D0000294D0000294D0000
0x3010: 00000000000000000000000000000000
0x3020: 000000000000000000000000294D0000
0x3030: 0000000000000000294D0000A54D0000
0x3040: 294D0000294D0000294D0000294D0000
0x3050: 294D0000294D0000294D00002D4D0000
0x3060: 294D0000294D0000294D0000294D0000
0x3070: 294D000031550000294D0000294D0000
0x3080: 294D0000294D0000294D0000294D0000
0x3090: 00000000294D0000294D0000294D0000
0x30A0: 294D0000294D0000294D0000294D0000
0x30B0: 0000000010B5064C2378002B07D1054B
0x30C0: 002B02D0044800E000BF0123237010BD
0x30D0: 6C0200200000000078670000044B10B5
0x30E0: 002B03D00349044800E000BF10BDC046
0x30F0: 000000007002002078670000024A137D
0x3100: DB07FCD57047C04600400041C0239B01
0x3110: 036586239B024365F4235B0283650023
0x3120: 0360704701207047064B016D9A680023
0x3130: 914204D0416D0133914200D0023B1800
0x3140: 7047C04600ED00E070B504000D00FFF7
0x3150: EBFF002807D1636DAB4203D8A26D9B18
0x3160: AB42404170BD236D0020F5E710B50C00
0x3170: FFF7C4FF2022054BFF321A83044A6408
0x3180: DC611A80FFF7BAFF10BDC04600400041
0x3190: 02A5FFFF020000234C3270B503604360
0x31A0: 83600400138072B6FFF7BEFF002804D0
0x31B0: 01280FD062B6012070BD656D636DA26D
0x31C0: 9B18AB42F6D9290020000135FFF7CEFF
0x31D0: FF35F3E7256D236DA26D9B18AB42E9D9
0x31E0: 290020000135FFF7C1FFFF35F3E70000
0x31F0: F7B50F0080211C00104B16005A688A43
Bank 1 content:
0x21800: 00800020453502002D3502002D350200
0x21810: 00000000000000000000000000000000
0x21820: 0000000000000000000000002D350200
0x21830: 00000000000000002D350200A9350200
0x21840: 00000000003000000010020000200000
0x21850: 08210000010000002801000000340000
0x21860: 00010000000000000001000001000000
0x21870: 00000000000000002801000000000000
0x21880: 00000000000000000000000000200000
0x21890: 00000000000000000001000029000000
0x218A0: 00000000000000000000000000000000
0x218B0: 00000000000000002000000000000000
0x218C0: 00000000000000000000000000000000
0x218D0: 00000000000000000000000000000000
0x218E0: 00000000000000000000000000000000
0x218F0: 00000000000000000000000000000000
0x21900: 00000000000000000000000000000000
0x21910: 00000000000000004000000001000000
0x21920: 00000000000000000000000000000000
0x21930: 00000000000000000000000000000000
0x21940: 00000000000000000000000000000000
0x21950: 00000000000000000000000000000000
0x21960: 00000000000000000000000000000000
0x21970: 00000000000000000000000000000000
0x21980: 00000000000000000000000000000000
0x21990: 00000000000000004000000000000000
0x219A0: 00000000000000000000000000000000
0x219B0: 00000000000000000000000000000000
0x219C0: 00000000000000000000000000000000
0x219D0: 00000000000000000000000001000000
0x219E0: 00000000000000000000000000000000
0x219F0: 00000000000000000000000000000000


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

עברתי על הקוד עוד כמה פעמים עד שהגעתי למקור הבעיה. פונקציית הכתיבה ל-FLASH של ה-bootloader המקורי מצפה לקבל את כמות המילים (words) שהיא צריכה לצרוב ולא את כמות הבתים (bytes). שימו לב שבפונקצית ()flash_write_row של ה-bootloader המקורי שמוחקת שורה וכותבת לשם את המידע הם מחלקים את את הכמות שמועברת כפרמטר האחרון ל-()flash_write_words ב-4.
קוד: בחר הכל
void flash_write_row(uint32_t *dst, uint32_t *src) {
    flash_erase_row(dst);
    flash_write_words(dst, src, FLASH_ROW_SIZE / 4);
}


את זה פספסתי. בתוך ה-()flash_write_words רואים גם שהיא מעתיקה את המידע מילה-מילה (word-word) וגודל של המילה uint32_t הוא 4 בתים:
קוד: בחר הכל
void flash_write_words(uint32_t *dst, uint32_t *src, uint32_t n_words) {
...
while (len--)
    *dst++ = *src++;
...
}


חילקתי ב-4 גם בקריאה לפונקציה זו בקוד שלי והצריבה הצליחה! כך גם ה-boot מ-BANK1! יוהו!

תם ולא נשלם

המשימה (כמעט) הושלמה ובזה מסתיימת סדרת הפוסטים בנושא של dual-boot על כרטיס XIAO המבוסס על מיקרובקר SAMD21.
למה כמעט? כי צריך עוד לממש את הפונקציה ()endUpgrade שאמורה לחשב CRC על מה שנצרב ולבדוק שהכל נצרב בהצלחה. כדי שזה יעבוד צריך שה-Server ששולח את הנתונים יספק את ערך ה-CRC שה-Client יוכל להשוות מולו כדי להבין אם הכל נצרב תקין או לא.

צריך גם לממש את הלוגיקה שמאשרת שהגרסה החדשה שנצרבה והתחלנו לעבוד איתה היא תקינה. אולי הצריבה הצליחה, אבל אחרי שעשינו reset הכרטיס נתקע ולא מתפקד? למקרה זה אני מתכנן להשתמש בטיימר watchdog החומרתי שיאותחל לאיזה שהוא ערך הגיוני שבפרק הזמן הזה בוודאות תהיה תקשורת בין ה-Server וה-Client. אם התקשורת תהיה תקינה, אז עוצרים את ה-watchdog, אם משהו נתקע ולא מתפקד, ה-watchdog יגיע לפסיקה שמטפלת בטיימר אחרי שהוא פוקע (קישור אליו מופיע בטבלת הפסיקות שממוקמת בתחילת איזור זכרון ה-FLASH ממנו רצה התוכנית) והפסיקה תסמן שצריך לעלות שוב מהגרסה הקודמת ולדווח על הבעיה ל-Server.

לסמן איפה? ב-NVRAM שאין לי כרגע. זהו שלב הבא בפרוייקט.

התכנון הוא שיהיו 3 שדות ב-NVRAM לטובת מודול ה-Firmware upgrade:
  • דגל שמציין שבוצע עדכון תוכנה
  • דגל שמציין האם העדכון נכשל
  • מספר ה-BANK ממנו צריך להריץ את התוכנית ב-boot הבא (גם יכול להיות דגל כי יש לנו רק 2 איזורי זכרון)

בקוד של ה-POSTBOOT (שרץ בכל boot של הבקר, אחרי ה-bootloader הרגיל) בודקים האם עלינו אחרי עדכון תוכנה. אם לא, אז קופצים לאיזור זכרון שהשדה ב-NVRAM מפנה אליו.
אם העליה של הבקר היא אחרי העדכון, בודקים גם את הדגל שמציין האם העדכון הצליח או לא. אם הדגל FALSE, שמציין שהעדכון לא נכשל, זה סימן שהבקר עולה בפעם הראשונה אחרי העדכון, מפעילים טיימר ה-watchdog, משנים את הדגל שמציין שהעדכון נכשל ל-TRUE (ליתר בטחון שגם ה-watchdog לא יעבוד או שיהיה reset לפני שהטיימר פוקע) וקופצים לאיזור זכרון שהשדה ב-NVRAM מפנה אליו. במהלך הריצה, כשהתוכנית תבין שהכל תקין, ישתנה הדגל שמציין שבוצע עדכון תוכנה ל-FALSE ועוצרים את ה-watchdog. בזה מסתיים תהליך עדכון תוכנה תקין.

אם דגל שמציין שהעדכון נכשל ב-TRUE, זה אומר שהבקר עולה אחרי שהתוכנה שעודכנה לא פעלה כמו שצריך. קופצים לכתובת שונה ממה שרשום ב-NVRAM, כלומר אם רשום שצריך לעלות מ-BANK0, אז קופצים ל-BANK1. או להפך. כלומר מפעילים את הגרסה הקודמת שביצעה את העדכון. התוכנה הרגילה תזהה את המצב (דגל אחרי עדכון הוא TRUE, דגל של עדכון כושל גם ב-TRUE), תדווח על המצב ל-Server ויעדכן את ה-NVRAM כדי שבפעם הבאה הבקר יעלה מאיזור הזכרון הנוכחי (הגרסה הישנה).

מה בהמשך?

שלב הבא של הפרוייקט יהיה להוסיף את ה-NVRAM כדי שאפשר יהיה להשלים את הקוד של FW-Upgrade ולהתחיל עם פיתוח מודולים אחרים שכנראה גם הם יצטרכו זכרון NVRAM. הרכיב שתכננתי להשתמש בו הוא ATECC508A שלמעשה הוא לא ממש NVRAM, אלא בקר עזר לקריפטוגרפיה (הצפנות וחתימות), שיש בתוכו 10Kb של זכרון NVRAM לשימושים שונים, יצירה ובדיקה של חתימות על המידע שנשלח או מתקבל שיהיה שימושי בעתיד כדי לייצר תקשורת בטוחה יותר. לרכיב יש גם מספר סידורי ייחודי, שיעזור לזהות את כל ה-Clients שיהיו בפרוייקט. כדי להשלים את הצד החומרתי אוסיף גם שני רכיבי תקשורת RS485, כנראה שבב MAX485 או אחר שאפשר יהיה לעבוד איתו על מטריצה.

פרטים נוספים בפוסט הבא...

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

הודעות נוספות:

  • בלוג המסע לחיפוש ערכת אלקטרוניקה

    המסע לחיפוש ערכת אלקטרוניקה

    2023-08-14 09:13:50

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

  • בלוג נפרדים מ-Actobotics

    נפרדים מ-Actobotics

    2023-06-07 19:53:24

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

  • בלוג dual boot - נראה עובד

    dual boot - נראה עובד

    2023-05-05 21:38:04

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

  • בלוג שיפורים בהכנסת כתובת

    שיפורים בהכנסת כתובת

    2022-12-29 16:33:19

    האתר כל הזמן עובר שינויים ושיפורים. הרבה מהם כאלה שאתם לא ממש רואים, שיפורים בתשתיות ומערכת הניהל.
    שינוי האחרון נוגע בצורה בה אתם מכניסים כתובת למשלוח (לשליחים של UPS או דואר רשום).

  • בלוג שינוי חברת השליחויות ל-UPS פנים ארצי

    שינוי חברת השליחויות ל-UPS פנים ארצי

    2022-11-11 10:54:04

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