אחרי שפרסמתי את הפוסט נשארו לי בראש 2 שאלות עיקריות:
- איפה הנתונים שאצטרך לשלוח לכרטיס כדי לצרוב גרסה?
- מה קורה במעבד אחרי ה-bootloader, האם פונקציית ה-main יושבת בכתובת הראשונה ב-FLASH?
ובכן, בפוסט זה אמצע את התשובות על שתי השאלות.
איפה הנתונים שאצטרך לשלוח לכרטיס כדי לצרוב גרסה?
הסתכלתי על הפלט של הקומפילציה, ראיתי שהוא בונה קבצים שונים, כולל קבצי ה-test שלי ולבסוף רושם שהוא מייצר קובץ ELF וגם קובץ BIN:- קוד: בחר הכל
Linking .pio/build/seeed_xiao/firmware.elf
Checking size .pio/build/seeed_xiao/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [= ] 7.3% (used 2408 bytes from 32768 bytes)
Flash: [ ] 4.2% (used 11120 bytes from 262144 bytes)
Building .pio/build/seeed_xiao/firmware.bin
חיפוש של "wiki elf file format" בגוגל הביא לי שני מדריכים מעולים:
Executable and Linkable Format
Understanding the ELF File Format
אין ספק שיש שם את כל מה שאני צריך, אבל זה נראה לי קצת מסובך מדי. אני זוכר שצריך להיות קובץ HEX בפורמט פשוט יותר של כתובות לצריבה ונתונים מה לצרוב...
ממשיך לחפש איך גורמים ל-PlatformIO לייצר קובץ HEX. על קובץ BIN ויתרתי מראש כי זה יכול להיות בכל פורמט שתרצו, ולא מצאתי שום רמז בכותרת של ההקובץ מה זה יכול להיות.
אז חיפוש בגוגל הביא אותי לדוגמאות איך לייצר קובץ HEX, מסתבר שאפשר להוסיף סקריפט Python שיתבצע לפני או אחרי תהליך הקומפילציה, שם תוכלו לעשות מה שתרצו, כולל להורות לסביבה לייצר קובץ HEX. אז הוספתי קובץ extra_post.py עם תוכן הבא (הכל מתוך הדוגמה מהרשת):
- קוד: בחר הכל
Import("env")
# Custom HEX from ELF
env.AddPostAction(
"$BUILD_DIR/${PROGNAME}.elf",
env.VerboseAction(" ".join([
"$OBJCOPY", "-O", "ihex", "-R", ".eeprom",
"$BUILD_DIR/${PROGNAME}.elf", "$BUILD_DIR/${PROGNAME}.hex"
]), "Building $BUILD_DIR/${PROGNAME}.hex")
)
וצריך גם להוסיף שורה נוספת ל-platformio.ini של הפרוייקט:
- קוד: בחר הכל
extra_scripts = post:extra_post.py
קומפילציה נוספת ועכשיו גם קובץ HEX נוצר כחלק מה-build:
- קוד: בחר הכל
Linking .pio/build/seeed_xiao/firmware.elf
Building .pio/build/seeed_xiao/firmware.hex
Checking size .pio/build/seeed_xiao/firmware.elf
Advanced Memory Usage is available via "PlatformIO Home > Project Inspect"
RAM: [= ] 7.3% (used 2408 bytes from 32768 bytes)
Flash: [ ] 4.2% (used 11120 bytes from 262144 bytes)
Building .pio/build/seeed_xiao/firmware.bin
בואו נראה מה יש לנו בקובץ HEX:
- קוד: בחר הכל
:1020000000800020DD340000C5340000C53400002D
:1020100000000000000000000000000000000000C0
:10202000000000000000000000000000C5340000B7
:102030000000000000000000C53400003135000041
:10204000C5340000C5340000C5340000C5340000AC
:10205000C5340000C5340000C5340000C934000098
:10206000C5340000C5340000C5340000C53400008C
:10207000C5340000B13C0000C5340000C534000088
:10208000C5340000C5340000C5340000C53400006C
:1020900000000000C5340000C5340000C534000055
:1020A000C5340000C5340000C5340000C53400004C
...
עדיין לא ברור מה זה, אבל נראה שמתקרבים... רואים שיש שם איזה שהוא פורמט, יש מספרים רצים שנראים כמו כתובת (2000, 2010, 2020 וכו'). מזכיר שבפוסט הקודם מצאנו שהקוד ב-FLASH מתחיל בכתובת 0x2000, שזה 8K וזהו הגודל שהשאירו ל-bootloader:
- קוד: בחר הכל
MEMORY
{
FLASH (rx) : ORIGIN = 0x00000000+0x2000, LENGTH = 0x00040000-0x2000 /* First 8KB used by bootloader */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 0x00008000
}
חיפוש של "wiki hex file format" בגוגל מביא אותי להסבר מעולה של ויקיפדיה על הפורמט:
Intel HEX
מצאתי גם תוסף ל-VSCode בשם "Intel HEX format" שמסמן בצבעים את האיזורים השונים של כל שורה, מה שמקל מאוד להסתכל על תוכן הקובץ. מרפרוף בקובץ, לקראת הסוף מצאתי את הדברים המעניינים האלה:
השורה הרביעית בתמונה, עם קוד 02 בירוק, שלפי המדריך מציין את כתובת הייחוס לנתונים שבאים אחריו. צריך להכפיל ב-16h את הכתובת שרשומה, כלומר כתובת בסיס היא 0x30000. שורות הבאות הן עם כתובות 0x8000, 0x8010, 0x8020, כלומר הכתובות המלאות לשורות אלה הן 0x38000 והלאה, ואחריו שורות נוספות עם כתובות 0xC000, 0xC010, 0xC020, כלומר הכתובות המלאות לשורות אלה הן 0x3C000... ומה הכתובות האלה? אלה הכתובות של איזורי ה-FLASH שהגדרתי בפוסט הקודם וגרמתי לפונקציות הבדיקה להיכנס לכתובות אלה:
- קוד: בחר הכל
.func1 0x0000000000038000 0x24
*test1.cpp.o(.text .text*)
.text._Z9testFunc1v
0x0000000000038000 0x24 .pio/build/seeed_xiao/src/test1.cpp.o
0x0000000000038000 testFunc1()
*test1.cpp.o(.rodata*)
.func2 0x000000000003c000 0x24
*test2.cpp.o(.text .text*)
.text._Z9testFunc2v
0x000000000003c000 0x24 .pio/build/seeed_xiao/src/test2.cpp.o
0x000000000003c000 testFunc2()
*test2.cpp.o(.rodata*)
קובץ HEX הוא בהחלט הפורמט שצריך לשלוח לכרטיס כדי לעדכן גרסה. חשבתי על הנושא קצת אחרי הפוסט הקודם ונראה שצריך לשלוח את אותה גרסת התוכנה כשהיא מקומפלת לאיזור זכרון ראשון וגם גרסה שמקומפלת לאיזור זכרון השני ולתת לכרטיס לבחור את הגרסה שהוא צריך. אפשר יהיה לאחד את השורות של איזור זכרון 1 ושורות של איזור זכרון 2 לתוך קובץ אחד ולשלוח אותם יחד. אפשר לדלג על כל שאר הנתונים בקובץ. אכנס לפרטים לגבי זה בפוסט הבא.
אז על שאלה הראשונה יש לנו תשובה. בואו נמשיך לשאלה השניה:
מה קורה במעבד אחרי ה-bootloader?
מי שמכיר את עולם התכנות רק דרך המשקפיים של Arduino, יכול לחשוב שהקוד מתחיל לרוץ בפונפציית ()setup, אבל זה לא נכון, גם בקומפילציה של קוד ארדואינו יש פונקציית
()main
, שהיא בדרך כלל הפונקציה הראשונה שרצה בתוכניות. פונקציית ()setup
פשוט נקראת מתוך ה-()main
. אבל בעולם של מיקרובקרים לא תמיד ה-()main
היא ההתחלה, הרי יש את ה-bootloader שרץ לפני הכל והוא יכול לקפוץ לכל נקודה בזכרון ולהמשיך משם. ה-()main
עדיין יהיה קיים לפחות לצורכי הקומפילציה.אז בואו נראה מה יש לנו בזכרון בכתובת הראשונה של ה-FLASH מייד אחרי ה-bootloader... אני מניח שזה האיזור אליו תתבצע הקפיצה. חיפשתי את הקוד של ה-bootloader לכרטיס XIAO, מצאתי קובץ INO שיכול לצרוב את ה-bootloader, אבל התוכן שלו רשום שם כבינארי, כך שאני לא יודע להגיד בוודאות לאן מתבצעת הקפיצה, אבל בואו נראה מה נמצא בכתובת 0x2000 שזו הכתובת הראשונה אחרי ה-bootloader... בשביל זה נציץ בקובץ MAP ונחפש את האיזור הרצוי:
- קוד: בחר הכל
.text 0x0000000000002000 0x2a74
0x0000000000002000 __text_start__ = .
*(.isr_vector)
.isr_vector 0x0000000000002000 0xb4 .pio/build/seeed_xiao/libFrameworkArduino.a(cortex_handlers.c.o)
0x0000000000002000 exception_table
המממ... לפי השם נראה שיש שם טבלת פסיקות (ISR = Interrupt Service Routine). שם הטבלה הוא
exception_table
שמוגדר בקובץ cortex_handlers.c. מצאתי את הקובץ בספריות ההתקנה של PlatformIO, והנה הטבלה:- קוד: בחר הכל
/* Exception Table */
__attribute__ ((section(".isr_vector"))) const DeviceVectors exception_table =
{
/* Configure Initial Stack Pointer, using linker-generated symbols */
(void*) (&__StackTop),
(void*) Reset_Handler,
(void*) NMI_Handler,
(void*) HardFault_Handler,
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) SVC_Handler,
(void*) (0UL), /* Reserved */
(void*) (0UL), /* Reserved */
(void*) PendSV_Handler,
(void*) SysTick_Handler,
/* Configurable interrupts */
(void*) PM_Handler, /* 0 Power Manager */
(void*) SYSCTRL_Handler, /* 1 System Control */
(void*) WDT_Handler, /* 2 Watchdog Timer */
(void*) RTC_Handler, /* 3 Real-Time Counter */
(void*) EIC_Handler, /* 4 External Interrupt Controller */
(void*) NVMCTRL_Handler, /* 5 Non-Volatile Memory Controller */
(void*) DMAC_Handler, /* 6 Direct Memory Access Controller */
(void*) USB_Handler, /* 7 Universal Serial Bus */
(void*) EVSYS_Handler, /* 8 Event System Interface */
(void*) SERCOM0_Handler, /* 9 Serial Communication Interface 0 */
(void*) SERCOM1_Handler, /* 10 Serial Communication Interface 1 */
(void*) SERCOM2_Handler, /* 11 Serial Communication Interface 2 */
(void*) SERCOM3_Handler, /* 12 Serial Communication Interface 3 */
(void*) SERCOM4_Handler, /* 13 Serial Communication Interface 4 */
(void*) SERCOM5_Handler, /* 14 Serial Communication Interface 5 */
(void*) TCC0_Handler, /* 15 Timer Counter Control 0 */
(void*) TCC1_Handler, /* 16 Timer Counter Control 1 */
(void*) TCC2_Handler, /* 17 Timer Counter Control 2 */
(void*) TC3_Handler, /* 18 Basic Timer Counter 0 */
(void*) TC4_Handler, /* 19 Basic Timer Counter 1 */
(void*) TC5_Handler, /* 20 Basic Timer Counter 2 */
(void*) TC6_Handler, /* 21 Basic Timer Counter 3 */
(void*) TC7_Handler, /* 22 Basic Timer Counter 4 */
(void*) ADC_Handler, /* 23 Analog Digital Converter */
(void*) AC_Handler, /* 24 Analog Comparators */
(void*) DAC_Handler, /* 25 Digital Analog Converter */
(void*) PTC_Handler, /* 26 Peripheral Touch Controller */
(void*) I2S_Handler, /* 27 Inter-IC Sound Interface */
(void*) (0UL), /* Reserved */
};
מה שמייד קופץ לעין זו ההגדרה שטבלה זו צריכה ללכת לאיזור ה-
isr_vector.
:- קוד: בחר הכל
__attribute__ ((section(".isr_vector")))
נראה שלאיזור זה שמור המקום הראשון בהגדרת איזור ה-FLASH:
- קוד: בחר הכל
.text :
{
__text_start__ = .;
KEEP(*(.isr_vector))
*(.text*)
KEEP(*(.init))
KEEP(*(.fini))
/* .ctors */
*crtbegin.o(.ctors)
*crtbegin?.o(.ctors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
*(SORT(.ctors.*))
*(.ctors)
/* .dtors */
*crtbegin.o(.dtors)
*crtbegin?.o(.dtors)
*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
*(SORT(.dtors.*))
*(.dtors)
*(.rodata*)
KEEP(*(.eh_frame*))
} > FLASH
אוקי... אז זה לא
()main
וגם לא פונקציה אחרת...נראה שטבלת ה-
exception_table
כוללת כתובות של פונקציות שנקראות במקרים שונים, טיימרים, פסיקות חיצוניות, חיווי מרכיבי תקשורת טורית וכו', וגם כמה פונקציות מעניינות יותר: HardFault_Handler
שכנראה נקראת כשמשהו ממש נהרס (נראה לי הגיוני להתלבש על הפונקציה הזו עם כל הניסויים שלי, רוב הסיכויים שמשהו ידפק בדרך וזו תהיה הדרך שלי לזהות את המצב), WDT_Handler
כלב שמירה שהזכרתי בפוסט הקודם, שזה טוב, זה אומר שיש תמיכה חומרתית וצריך יהיה ללמוד איך להשתמש במנגנון זה, וגם Reset_Handler
שלפי השם נראה שמטפלת ב-reset, עדיין לא יודע אם עושה reset או הפונקציה היא זו שנקראת אחרי ה-reset. בואו נראה:- קוד: בחר הכל
extern int main(void);
/* This is called on processor reset to initialize the device and call main() */
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 ((&__data_start__ != &__data_end__) && (pSrc != pDest)) {
for (pDest = &__bss_start__; pDest < &__bss_end__; pDest++)
*pDest = 0;
}
#if defined(__FPU_USED) && defined(__SAMD51__)
/* Enable FPU */
SCB->CPACR |= (0xFu << 20);
__DSB();
__ISB();
#endif
SystemInit();
main();
while (1)
;
}
אהא! מעתיקים משהו עם הלולאות בתחילת הפונקציה, קריאה ל-
()SystemInit
לאתחול של המערכת (בעיקר תדר שעונים וכו') ואז... קריאה ל-()main
ומשם כבר מגיעים ל-()setup
ו-()loop
המוכרים.המעבד צריך לדעת איפה ממוקמת טבלת ה-ISR, כך שמאוד יכול להיות ש-bootloader הוא זה שמאתחל את הכתובת של הטבלה בריגיסטרים של המעבד ויכול להיות שאחרי זה קופץ לפונקציה שהכתובת שלה שרשומה ב-0x2004, שזה הפונקציה
Reset_Handler
. או שעושה איזה שהוא Soft-Reset למעבד, כך שהוא קופץ לשם כחלק מתהליך העליה. חיפשתי די הרבה במפרט של SAMD21, אבל לא הצלחתי למצוא הזכור לתהליך כזה. מצאתי את המדריך הזה שמזכיר את "Vector Table Offset Register (VTOR)" שעושה בדיוק את מה שאני מחפש, אבל אני לא מצליח למצוא קוד שמטפל בשינוי הכתובות. ושוב, יכול להיות שזה נעשה ב-bootloader שאין את הקוד שלו.כאן מצאתי את הכתובת של הרגיסטר הזה, אולי פשוט צריך לנסות לקרוא את הערך שלו ולראות מה יש שם...
אז מה זה אומר מבחינתי?
כתבתי בפוסט הקודם שבגלל שיש הרבה קוד שמתקמפל בנוסף לקוד שלי, הכיוון הנכון יהיה כנראה שכל הקוד ילך לאיזור זכרון שצריך יהיה לצרוב בעדכון תוכנה, חוץ מקוד מיוחד שצריך להישאר באיזור ההתחלתי של ה-FLASH. טבלת ה-
exception_table
מגיעה כחלק מסביבת הקומפילציה של כרטיס ה-XIAO, אז צריך לדאוג לעדכן גם אותה במקרה שהיא תשתנה (זה לא קוד שבשליטתי). אם אכן יש רגיסטר שמאפשר לציין את מיקום טבלת ה-ISR, אז קוד האתחול שישאר באיזור שלא מעודכן צריך יהיה להורות למעבד איפה נמצאת הטבלה האמיתית. וצריך גם לקוות שאין שום קוד מוחבא איפה שהוא שניגש לכתובת 0x2000 כדי לשנות את הטבלה במהלך הריצה..אם בסופו של דבר לא אמצא את הרגיסטר הזה, אז בנוסף לקוד המיוחד שלא מוחלף צריך גם לממש את טבלת ה-
exception_table
משלי שתשב בכתובת המקורית, והמימוש של כל הפונקציות בטבלה זו יהיה לקפוץ לפונקציות האמיתיות שצריכות לטפל בפסיקות.