מבוא
זהו פוסט שלישי בדרך ל-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 יחליט איזו תוכנית להריץ.
כל זה בפוסט הבא...