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

העגלה ריקה

לקוחות נכבדים,
בערב סוכות הראשון (16-17/10) המחסן יהיה סגור.
במהלך חול המועד סוכות (בין 20/10 ל-22/10) המחסן יפעל שעה וחצי פחות מהרגיל, בין 8:00 ל-15:00.
בערב סוכות השני (23-24/10) המחסן יהיה סגור.
נחזור לפעילות רגילה ביום ראשון, 27/10.
חג שמח!

dual boot - נראה עובד


2023-05-05 21:38:04

מבוא

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

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

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

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

אני ממליץ מאוד לקרוא את שני הפוסטים הקודמים כדי לקבל את ההקשר למה שאפרט בפוסט זה.

מה ה-bootloader עושה?

בואו נתחיל ממה שגיליתי לגבי הכתובות הראשונות ב-FLASH, איפה שנצרבת התוכנית שכתבנו. כמו שמצאתי בפוסט הקודם, בכתובת הראשונה מיד אחרי ה-bootloader נמצאת טבלת מצביעים (pointers) לדברים חיונים לתפקוד המיקרובקר. בכתובת הראשונה יש מצביע לתחילת ה-stack, בכתובת השניה יש מצביע לפונקציה שנקראת אחרי שהבקר עושה reset, או עולה בהפעלה הראשונה. אחריהם יש מצביעים לפונקציות חשובות אחרות שנקראות במצבים מוזרים כמו ביצוע משהו לא תקין (גישה לכתובת לא קיימת, חלוקה ב-0 וכו'), אירועים לגיטימיים כמו טיימרים, טיפול במצבי שינה שונים (Power Management), פסיקות מיחידות חומרתיות פנימיות כמו USB, ADC, DMA, תקשורת טורית וכו', וגם פסיקות חיצוניות שאפשר לקבל משינויים על קווי ה-DIO שאנו מחברים לרכיבים אחרים.

היה לי מעניין לראות מה קורה ב-bootloader של הבקר. די ברור לי שיש איזה שהוא רגיסטר שמצביע לאיזור הטבלה הזו. הרי הבקר זה רכיב גנרי, לא כולם רוצים ש-bootloader יתפוס את ה-8K הראשונים בזכרון, אולי מישהו רוצה שהוא יהיה 4K, או 16K, או בכלל בלי, שיריץ את התוכנית שנצרבת דרך ממשק אחר. אני מניח שבתוך ה-bootloader משנים את הרגיסטר הזה לכתובת הטבלה בתחילת איזור ה-FLASH שצורבים, שנמצא מיד אחרי ה-bootloader וקופצים לאיזור זה להמשך ביצוע התוכנית.
חיפשתי קוד של ה-bootloader לכרטיס ה-XIAO שאני מתכנן להשתמש בו ליחידות הקצה במימוש הבית החכם. כל מה שמצאתי זו תוכנית (sketch) שאפשר לטעון לסביבת ה-Arduino שתצרוב את ה-bootloader, אבל הקוד של ה-bootloader עצמו מופיע שם בצורה בינארית כמערך של תווים שנצרב בזכרון של הבקר. לא עוזר הרבה כדי להבין מה ה-bootloader עושה, אבל הקוד של הצריבה יהיה שימושי לצריבת התוכניות שלי באיזור ה-FLASH שלא בשימוש. גם משהו...
זה הקוד (בקובץ INO):
https://github.com/Seeed-Studio/ArduinoCore-samd/tree/master/bootloaders/XIAOM0

לא ויתרתי, חיפשתי אילו עוד כרטיסים יש בשוק שמבוססים על אותו המיקרובקר SAMD21, שנהיה די פופולרי אחרי ה-ATMega328 של כרטיסי ה-UNO. והנה, מצאתי את Arduino Zero של צוות הארדואינו עצמו. הם דוגלים בקוד פתוח, כך שהייתי בטוח שאמצא את מה שאני צריך. והנה התיקיה של קוד ה-bootloader שלהם:
https://github.com/arduino/ArduinoCore-samd/tree/master/bootloaders/zero

יש גם קובץ LD שמגדיר את איזורי הזכרון, מעניין איך זה מוגדר לקומפילציה של ה-bootloader...
קוד: בחר הכל
MEMORY
{
  FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 0x2000 /* First 8KB used by bootloader */
  RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000-0x0400 /* last 4 bytes used by bootloader to keep data between resets, but reserves 1024 bytes for sketches to have same possibility */
}


פשוט ומצומצם. 8K של ה-FLASH לטובת ה-bootloader, את זה אנחנו כבר יודעים...

מה נמצא בתחילת האיזור הזה? טבלת הפסיקות:
קוד: בחר הכל
.vectors :
  {
    KEEP(*(.isr_vector))
  } > FLASH


אוקי... אז מה קורה בסופו של ה-bootloader? איך עוברים לתוכנית שלנו ואיך ה-bootloader יודע איפה היא נמצאת? מחפש בקוד את ה-isr_vector ומגיע לקובץ "board_startup.c" עם טבלת המצביעים עם attribute של isr_vector כדי שתהליך ה-LINK ימקם את הטבלה הזו בתחילת איזור ה-FLASH, כמו שמוגדר בקובץ LD.

קוד: בחר הכל
/* Exception Table */
__attribute__ ((used, section(".isr_vector")))
const struct ConstVectors exception_table =
{
  /* Configure Initial Stack Pointer, using linker-generated symbols */
  .pvStack = (void*) (&__StackTop),

  .pfnReset_Handler      = (void*) Reset_Handler,
  .pfnNMI_Handler        = (void*) NMI_Handler,
  .pfnHardFault_Handler  = (void*) HardFault_Handler,
  .pfnReservedM12        = (void*) (0UL), /* Reserved */
  .pfnReservedM11        = (void*) (0UL), /* Reserved */
  .pfnReservedM10        = (void*) (0UL), /* Reserved */
  .pfnReservedM9         = (void*) (0UL), /* Reserved */
  .pfnReservedM8         = (void*) (0UL), /* Reserved */
  .pfnReservedM7         = (void*) (0UL), /* Reserved */
  .pfnReservedM6         = (void*) (0UL), /* Reserved */
  .pfnSVC_Handler        = (void*) SVC_Handler,
  .pfnReservedM4         = (void*) (0UL), /* Reserved */
  .pfnReservedM3         = (void*) (0UL), /* Reserved */
  .pfnPendSV_Handler     = (void*) PendSV_Handler,
  .pfnSysTick_Handler    = (void*) SysTick_Handler,
};


השורה השניה בטבלה הוא מצביע לפונקציה Reset_Handler שתקרא אחרי שהבקר יעשה reset. בואו נראה אם יש שם משהו מעניין...
קוד: בחר הכל
/**
* \brief This is the code that gets called on processor reset.
* Initializes the device and call the main() routine.
*/
void Reset_Handler( void )
{
  uint32_t *pSrc, *pDest;

  /* Initialize the initialized data section */
  pSrc = &__etext;
  pDest = &__data_start__;

  if ( (&__data_start__ != &__data_end__) && (pSrc != pDest) )
  {
    for (  ;  pDest < &__data_end__ ; pDest++, pSrc++ )
    {
      *pDest = *pSrc ;
    }
  }

  /* Clear the zero section */
  if ( &__bss_start__ != &__bss_end__ )
  {
    for ( pDest = &__bss_start__ ; pDest < &__bss_end__ ; pDest++ )
    {
      *pDest = 0ul ;
    }
  }

//  board_init(); // will be done in main() after app check

  /* Initialize the C library */
//  __libc_init_array();

  main();

  while (1);
}


כמו שהתאור של הפונקציה אומר, מאתחלים שם שני אזורי זכרון וקוראים ל-()main. ה-main עצמו מוגדר בקובץ אחר, כמו שמרמז ה-extern בהגדרה של הפונקציה בקובץ זה:
קוד: בחר הכל
extern int main(void);


שמתם לב שה-()main היא לא הפונקציה הראשונה שמתבצעת במיקרובקר? גם לא ה-()setup שהתרגלנו לה בעולם ה-Arduino.

באותה התיקיה של ה-bootloader יש גם קובץ "main.c", אז נראה לי די הגיוני לחפש שם...
()main היא פונקציה די ארוכה שעושה הרבה דברים, כמו אתחול של כל מני תכונות של הבקר וגם טיפול ב-USB וצריבה אם יש חיבור ותקשורת, אבל ממש בהתחלה היא קוראת לפונקציה ()check_start_application, שבודקת שיש תוכנית צרובה (ערכים של FLASH שלא צרוב יהיו 0xFFFFFFFF) ומחשבת את הכתובת של פונקצית ה-()Reset_Handler של התוכנית שצריכה לרוץ, שזה המצביע השני בטבלת המצביעים, זוכרים? אחרי זה בודקים עוד כמה תנאים ובסוף קוראים לפונקציה ()jump_to_application:
קוד: בחר הכל
/*
  * Test sketch stack pointer @ &__sketch_vectors_ptr
  * Stay in SAM-BA if value @ (&__sketch_vectors_ptr) == 0xFFFFFFFF (Erased flash cell value)
  */
  if (__sketch_vectors_ptr == 0xFFFFFFFF)
  {
    /* Stay in bootloader */
    return;
  }

  /*
  * Load the sketch Reset Handler address
  * __sketch_vectors_ptr is exported from linker script and point on first 32b word of sketch vector table
  * First 32b word is sketch stack
  * Second 32b word is sketch entry point: Reset_Handler()
  */
  pulSketch_Start_Address = &__sketch_vectors_ptr ;
  pulSketch_Start_Address++ ;

...

#ifdef CONFIGURE_PMIC
  jump_to_app = true;
#else
  jump_to_application();
#endif


רגע, רגע... מה זה ה-sketch_vectors_ptr__ הזה? הוא בעצם מצביע לתחילת התוכנית שצריך להריץ!
בתחילת הקובץ יש הגדרה של המשתנה:
קוד: בחר הכל
extern uint32_t __sketch_vectors_ptr; // Exported value from linker script


אוקי, בחזרה לקובץ ה-LD של התיקיה. שם מוצאים איך נקבע הערך של המשתנה בתהליך ה-LINK:
קוד: בחר הכל
PROVIDE(__sketch_vectors_ptr = ORIGIN(FLASH) + LENGTH(FLASH));


מה שאומר שהערך שלו יהיה הכתובת הראשונה שאחרי איזור ה-FLASH של ה-bootloader. אוקי... ככה הם מגיעים לאיזור של התוכנית..

אז מה קורה בפונקציה ()jump_to_application המסקרנת?
קוד: בחר הכל
static void jump_to_application(void) {

  /* Rebase the Stack Pointer */
  __set_MSP( (uint32_t)(__sketch_vectors_ptr) );

  /* Rebase the vector table base address */
  SCB->VTOR = ((uint32_t)(&__sketch_vectors_ptr) & SCB_VTOR_TBLOFF_Msk);

  /* Jump to application Reset Handler in the application */
  asm("bx %0"::"r"(*pulSketch_Start_Address));
}


בינגו! בדיוק מה שחיפשתי!
מאתחלים את ה-Stack לערך של המצביע הראשון בטבלה. מכניסים לרגיסטר VTOR את הכתובת של טבלת המצביעים וקופצים לפונקציה לפי הכתובת שבמצביע השני של הטבלה.
הקפיצה מתבצעת בעזרת קוד אסמבלר שמועבר לפונקציה ()asm.

תוכנית הפעולה

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

אז הנה מה שאני הולך לעשות:
אגדיר איזור זיכרון שיהיה מייד אחרי ה-bootloader, לשם מועבר פיקוד אחרי שהוא מסיים את התפקיד שלו. סוג של bootloader משלי. אקרא לאיזור זה POSTBOOT.
אגדיר שם את טבלת המצביעים בדיוק כמו שכל תוכנית מגדירה. ה-Reset_Handler שבמצביע השני של הטבלה יפנה לפונקציה שלי, שתבדוק איזה איזור זכרון צריך לעלות הפעם (איזורים כאלה מכנים BANK0 ו-BANK1). ואעשה בדיוק את אותו הדבר שפונקציית ה-()jump_to_application עושה, אשנה את הרגיסטר VTOR לטבלת המצביעים של התוכנית שצריכה לרוץ ב-BANK0 או BANK1 ואבצע קפיצה ל-Reset_Handler האמיתי.

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

באיזור זכרון זה צריך לשבת קוד עצמאי, כזה שלא משתמש בפונקציות של התוכנית הרגילה שתשב באיזור אחר כדי שלא תהיה שום תלות בכתובות אחרות. מה שאומר שאני לא יכול להשתמש בפונקציות כמו ()digitalRead או ספריה לתקשורת I2C שתתקמפל יחד עם שאר הקוד. שזה קצת בעיה כי התכנון שלי הוא להוסיף רכיב NVRAM לכרטיס, שמתקשר דרך I2C וההחלטה לאיזה BANK זכרון לקפוץ אחרי ה-POSTBOOT צריכה להתבסס על מה שיופיע ב-NVRAM. אצטרך להוסיף מימוש של תקשורת I2C בסיסית לקוד שישב שם כדי לתקשר עם ה-NVRAM.

שאר הזכרון יחולק לשני חלקים שווים שאותם אקרא BANK0 ו-BANK1 לטובת התוכנית הרגילה שצריכה לרוץ ולהיצרב ע"י מנגנון העדכון מרחוק. הקוד שיעשה את העדכון יהיה חלק מהתוכנית הרגילה. כלומר, התוכנית שתרוץ מ-BANK0 תוכל לצרוב את איזור ה-BANK1, לשנות את ה-NVRAM כדי שב-reset הבא הבקר יריץ את התוכנית שנצאת ב-BANK1. התוכנית תוכל לצרוב גם את איזור ה-POSTBOOT אם יהיה צורך, כך שבעצם יהיה מנגנון לעדכן את כל מה שארצה.

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

בגלל שלמיקרובקר SAMD21 אין NVRAM ואין לי משהו שמחובר לבקר כרגע, אוכל להשתמש בקו דיגיטלי של הכרטיס כדי להחליט איזו גרסה צריך לעלות. כמו שכתבתי, לא אוכל להשתמש בפונקציה ()digitalRead כדי שהקוד של ה-POSTBOOT לא ייגש לאיזור זכרון אחר (שיכול להשתנות בשלב כלשהו), לכן אצטרך לממש פונקציה משלי שתעשה בדיוק את אותו הדבר כמו ה-()digitalRead. שכפול הכרחי כדי לנתק בין הקוד של ה-POSTBOOT לקוד של שאר התוכנית. 

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

קוד: בחר הכל
MEMORY
{
  /* 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 */

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


וכמובן הגדרתי גם SECTION נוסף שיציב את תוכן הקובץ "postboot.c" לאיזור ה-POSTBOOT וימקם את טבלת המצביעים הזמנית לתחילת איזור הזכרון:
קוד: בחר הכל
.postboot :
{
    KEEP(*(.temp_isr_vector))

    *postboot.cpp.o (.text .text*)
    *postboot.cpp.o (.rodata*)
} > POSTBOOT



מלחמה עם הקומפיילר

עשיתי את כל מה שכתבתי עד עכשיו, הוספתי קובץ "postboot.c" עם טבלת המצביעים וקוד פשוט שעושה בדיוק את מה שה-bootloader עושה, מציב ערכים לרגיסטרים של Stack ו-VTOR וקופץ ל-Reset_Handler של BANK0, רק שהקומפיילר מתעקש לזרוק את כל התוספות האלה לאיזור אחר, שלא יצרב בסופו של דבר ל-FLASH. אפשר לראות את זה בקובץ ה-MAP (ראו פוסט הראשון בסדרה להסבר איך מייצרים קובץ זה).
לקומפיילרים יש המון הגדרות שונות ומשונות, ביניהן הגדרות של אופטימיזציות שאמורות להקטין ולייעל את התוצר. חלק מתהליך האופטימיזציה הוא לא לכלול קוד שלא קוראים לו משום מקום לתוצר הסופי ולקוד שהוספתי באמת לא נקרא כמו כל פונקציה רגילה ב-C, אלא קופצים אליו בצורה אחרת. לא רציתי לשנות את ההגדרות כדי לא לפגוע במשהו. ברור לי שצריך לשכנע את הקומפיילר בצורה אחרת.

קצת קריאה ברשת. חלק הציעו להוסיף איזה שהם attributes לפונקציות, אבל היו כאלה שטענו שזה לא עבד בשבילם, אבל היה פתרון פשוט יותר, וזה גם מה שעושים בקוד הרגיל וגם ב-bootloader. כל מה שצריך זה להגדיר struct עם מצביעים לפונקציות ומבני נתונים שרוצים שישארו בקומפילציה. הקומפיילר כנראה לא יודע אם ניגשים אליהם ישירות או בצורה מתוחכמת דרך מצביע לכתובת הזכרון, אז הוא משתכנע ומשאיר בתוצר את מה שה-struct מצביע אליו. זו התוספת:
קוד: בחר הכל
/*
  A hack to force the linker to include the functions and data structures even if it's not called from
  anywhere else: declare a structure and add pointers to the needed functions/structures in that struct instance
*/
typedef struct _dummy_struct
{
  void* p1;
  void* p2;
  void* p3;
  void *p4;
} dummy_struct;

dummy_struct dummy_var = {
  (void*) PostBoot_Reset_Handler,
  (void*) PostBoot_HardFault_Handler,
  (void*) jump_to_application,
  (void*) &temp_exception_table
};



debug באפלה

עשיתי את כל מה שתיארתי קודם. התוכנית שנצרבת ל-BANK0 היא blink רגיל עם הבהוב של הלד בצורה מחזורית. צורב ו... שום דבר לא קורה...
עברתי על הקוד כמה פעמים, לא רואה איזה שהיא בעיה. אין ברירה, צריך לעשות debug. אבל... בשלב ההתחלתי אין serial כדי להדפיס משהו. אני גם לא יודע באיזה שלב התוכנית נתקעת.
נכון, ל-SAMD21 יש חיבורי SWD שמאפשר להתחבר לבקר כדי לעשות debug, רק שאין לי בבית את הצורב/דבאגר כדי לעשות את זה. מה עושים? יש לד שאפשר לנדנד!

זה מחזיר אותי לתקופה די התחלתית של העבודה שלי בהייטק. עבדתי בסטארטאפ שפיתח צ'יפ BlueTooth שאמור היה להתחבר ל-USB של המדפסת ולאפשר להדפיס ישירות מטלפון הנייד. בתקופה ההיא זה היה משהו חדשני, לא היו מדפסות עם חיבור אלחוטי. אני הייתי בצוות התוכנה. אלה שפיתחו את החומרה של הבקר משום מה לא חשבו שצריך יהיה לעשות debug כלשהו לתוכנה שתרוץ בתוכו ולא השאירו שום קו שאפשר היה להשתמש בו כדי לסמן משהו לעולם החיצון. הדבר היחיד שהצ'יפ ידע לעשות זה להיות USB Host למדפסת. הדרך היחידה בשבילי לעשות debug זה להדפיס משהו על המדפסת... הרבה דפים בוזבזו בתקופה ההיא לצערי. עד שכתבתי מעטפת לקוד שלי כך שהייתי יכול להריץ אותו על המחשב בלי צורך לצרוב אותו בכלל על הבקר.

בחזרה לימים אלה... צריך לשחק עם הלד שיש על ה-XIAO כדי להבין איפה הקוד נתקע. נזכרתי שראיתי משהו דומה בקוד של ה-bootloader. נראה שגם הם השתמשו בשיטה זו כדי לעשות debug כי הקוד שנוגע בלד מפוזר בכל מני מקומות ועשו לו comment-out, כלומר הוא לא מתקמפל, אבל עדיין נשאר שם למקרה הצורך.

מצאתי את הפונקציות שמטפלות בלד והעתקתי אותם לקובץ "directHw.h" שהוספתי לתיקיית include. הפונקציות הן inline, שזה רמז לקומפיילר שינסה לשלב את הקוד של הפונקציות האלה ישירות בתוך הקוד ממנו הן נקראות במקום קפיצה לפונקציה וחזרה למקום ממנו היא נקראת. נהוג לפעול כך בפונקציות קטנות של שכבת ה-HAL - Hardware Abstraction Layer, שאלה בדרך כלל פונקציות קטנות שנוגעות ברכיבי חומרה של הבקר. לקפוץ לפונקציה שמבצעת משהו כל כך קצר ולחזור בחזרה למקום ממנו קפצו יהיה מאוד בזבזני.

קוד: בחר הכל
#include <stdio.h>

#define BOARD_LED_PORT                    (0)
#define BOARD_LED_PIN                    (17)

inline void LED_init(void) { PORT->Group[BOARD_LED_PORT].DIRSET.reg = (1<<BOARD_LED_PIN); }
inline void LED_on(void) { PORT->Group[BOARD_LED_PORT].OUTCLR.reg = (1<<BOARD_LED_PIN); }
inline void LED_off(void) { PORT->Group[BOARD_LED_PORT].OUTSET.reg = (1<<BOARD_LED_PIN); }
inline void LED_toggle(void) { PORT->Group[BOARD_LED_PORT].OUTTGL.reg = (1<<BOARD_LED_PIN); }
inline void LED_delay(uint32_t delayMs)
{
  for (uint32_t i=0; i<250*delayMs; i++)
      /* force compiler to not optimize this... */
      __asm__ __volatile__("");
};

inline void LED_blink(uint32_t count=1, uint32_t delayMs=250)
{
  for(uint32_t j=0; j<count; j++) {
    LED_on();
    LED_delay(delayMs);

    LED_off();
    LED_delay(delayMs);
  }
}

inline void LED_blinkUInt32(uint32_t num)
{
  LED_blink(1, 1500);
  LED_blink((num >> 28) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 24) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 20) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 16) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 12) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 8) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 4) & 0xF, 300);
  LED_delay(500);

  LED_blink(1, 1500);
  LED_blink((num >> 0) & 0xF, 300);
  LED_delay(500);
}


בהתחלה הוספתי רק את הפונקציות on, init ו-off והשתמשתי בהן בקוד שלי. מצאתי שאני מגיע לקוד של ה-postboot והקפיצה לקוד ב-BANK0 לא מצליחה. אגב, מסתבר שהלד על כרטיס ה-XIAO מחובר בלוגיקה הפוכה, נדלק כשמנקים את הרגיסטר ומספקים לו "0" לוגי, אז הייתי צריך להפוך את ה-on וה-off. 
אחרי זה הוספתי את פונקציית ה-blink יחד עם כמה IFs בקוד של ה-postboot כדי להבין מה קורה שם... הוספתי גם HardFault_Handler עם הבהוב מהיר כדי להבין אם אני עושה משהו ממש לא תקין, וגם זה קרה.
כל זה לא הספיק... בסוף הוספתי גם את ה-blinkUInt32 כדי לאותת ערכים בני 32bit, כל ניבל (nibble) בנפרד. זה עזר לי להבין שמשהו לא עובד בקוד שמחשב את הכתובת של פונקציית ה-()Reset_Handler של הקוד ב-BANK0 שהעתקתי מה-bootloader. שיניתי לקוד שלי ובום... גם הנדנוד של הלדים ב-postboot קורה וגם של ה-blink בקוד של ה-BANK0 אחריו.

אז בינתיים נראה שה-dual boot עובד!

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

כל זה בפוסט הבא...

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

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

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

    2022-12-29 16:33:19

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

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

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

    2022-11-11 10:54:04

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

  • בלוג קובץ HEX ואיך יודעים לאן לקפוץ

    קובץ HEX ואיך יודעים לאן לקפוץ

    2022-08-26 14:31:24

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

  • בלוג איזורי זכרון ב-Arduino

    איזורי זכרון ב-Arduino

    2022-08-20 17:37:13

    כבר הרבה מאוד זמן (שנים) רץ אצלי הרעיון לבנות מערכת בית חכם DIY ממש מאפס. הרעיון מלוטש מאוד, הדבר העיקרי שחסר כדי לממש את הרעיון הוא הזמן...
    אז היום החלטתי להשקיע בזה כמה שעות כדי לראות כמה זה יכול להיות קשה לייצר boot כפול בכרטיס ארדואינו.

  • בלוג איפה ה-ESP32?

    איפה ה-ESP32?

    2022-07-17 20:29:46

    אז מה הסיפור של כרטיסי פיתוח ESP32 שלא כל כך קל למצוא בארץ?
    השאלה בעצם רלוונטית לכל המוצרים עם יכולות תקשורת אלחוטית, ZigBee, WiFi, LoRa, Bluetooth והמודולים הפשוטים שפעם היו בשוק.