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

אחסון קבוע פירושו זיכרון ששומר מידע ללא מתח. בלוח STM32N6570-DK זהו רכיב Octo‑SPI NOR Flash חיצוני בנפח 1Gbit, כלומר 128MB. זיכרון ריצה הוא מקום שאפשר לקרוא ולכתוב במהירות בזמן שהמערכת פועלת. בתוך ה־STM32N6 יש 4.2MB SRAM, ועל הלוח יש גם 256Mbit של PSRAM חיצוני, כלומר 32MB. שימו לב להמרה: היצרן כותב ביטים, ואנחנו בדרך כלל חושבים בבתים.

ה־NOR Flash וה־PSRAM יושבים שניהם מחוץ למיקרו־בקר, אך הם אינם אותו דבר. ה־Flash שומר את הקוד גם כשהלוח כבוי ומתאים לאחסון Firmware, מודלים ונכסים. ה־PSRAM נמחק בכיבוי, אך הוא גדול ונוח ל־Framebuffers, לנתוני ביניים ולמודלים בזמן עבודה. ה־SRAM הפנימי קטן יותר, אבל קרוב לליבה ומתאים לקוד ולנתונים שצריכים זמן תגובה צפוי.

בתוך ה־STM32N6 כן נמצא Boot ROM של 128KB. זהו קוד ש־ST צרבה במפעל ואי אפשר לעדכן אותו כמו אפליקציה. אחרי Reset זה הקוד הראשון שרץ. הוא קורא את פיני BOOT, בודק מאיפה אמורה להגיע התמונה הראשונה ומכין את השלב הבא. קיימים גם ביטי OTP חד־פעמיים לצורכי זהות ומפתחות, אבל הם אינם תחליף ל־Flash של אפליקציה.

במצב Development, כאשר BOOT1 מוגדר ל־1, ה־Boot ROM משאיר למפתח נתיב נוח לדיבוג. ST‑LINK ו־STM32CubeIDE יכולים להוריד את הקוד ישירות ל־SRAM הפנימי ולהתחיל אותו משם. זו הדרך המהירה ביותר להגיע ל־LED מהבהב, אך חשוב להבין את התוצאה: ניתוק המתח מוחק את התוכנית. לחיצה על Debug אינה הוכחה שבניתם אתחול עצמאי.

במצב Flash boot, כאשר BOOT0 ו־BOOT1 הם 0, ה־Boot ROM פונה ל־Flash החיצוני. הוא מחפש שם תמונה עם STM32 Header תקין, טוען אותה לאזור הורדה ב־SRAM הפנימי ובודק את המידע הנדרש לפי מצב האבטחה של השבב. התמונה הזאת נקראת FSBL, קיצור של First Stage Boot Loader — טוען האתחול הראשון שבשליטתנו.

ל־FSBL יש עבודה שהאפליקציה הרגילה אינה יכולה לדלג עליה. הוא מגדיר שעונים, Cache ו־MPU, מאתחל את בקרי הזיכרון החיצוני, מעביר אותם למצב Memory‑Mapped כאשר צריך, מחליט איזו אפליקציה תקפה ואז קופץ אליה. אפשר לחשוב עליו כעל איש התחזוקה שפותח את הבניין ומדליק את החשמל לפני שהעובדים מגיעים. ה־Flash החיצוני קיים פיזית כבר בהדלקה, אבל עד שמישהו מגדיר את הממשק אליו אי אפשר להתייחס אליו כמו אל זיכרון רגיל.

האפשרות הפשוטה ביותר נקראת בפועל FSBL בסיסי או Load and Run קטן. הקוד של הטוען והאפליקציה הוא תמונה אחת. ה־Boot ROM מעתיק אותה מה־Flash החיצוני אל SRAM ומריץ אותה שם. בגלל מגבלת הורדה של 512KB וכ־1KB של כותרת ויישור, הקוד הזמין מוגבל לכ־511KB. רוב דוגמאות ה־HAL הפשוטות של STM32CubeN6 בנויות כך.

האפשרות השנייה היא LRUN דו־שלבי. ה־Boot ROM טוען FSBL קטן ל־SRAM. ה־FSBL מאתחל את הזיכרון החיצוני, מעתיק ממנו אפליקציה גדולה יותר ל־SRAM פנימי ואז קופץ אליה. בזמן העבודה הקוד נהנה מהזיכרון הפנימי, אבל משלם בזמן העתקה בהדלקה ותופס מקום שיכול היה לשמש Buffer‑ים, Stack ו־Heap.

האפשרות השלישית היא XIP, קיצור של Execute In Place. ה־FSBL מגדיר את ה־NOR Flash החיצוני כחלון זיכרון ואז המעבד קורא ממנו פקודות ישירות. כך שומרים את רוב ה־SRAM הפנימי לנתונים ויכולים להריץ תוכנה גדולה, אבל הגישה תלויה בבקר החיצוני, ב־Cache וברוחב הפס. קוד עם הרבה קפיצות אקראיות או פסיקות רגישות עשוי להרוויח מהעתקה ל־SRAM או ל־ITCM גם אם שאר האפליקציה נשארת ב־XIP.

אפשר גם להעתיק אפליקציה מה־NOR אל ה־PSRAM ולהריץ אותה משם. זה נותן מרחב גדול וכתיב, אך ה־PSRAM עדיין חיצוני, נדיף ואיטי יותר מזיכרון פנימי. ה־FSBL צריך להגדיר אותו ואת ה־MPU לפני הקפיצה. זו אפשרות שימושית כאשר התוכנה גדולה מדי ל־SRAM הפנימי אבל רוצים להימנע מחלק ממגבלות הכתיבה של Flash בזמן העבודה.

במוצר אמיתי הבחירה אינה חייבת להיות הכל או כלום. אפשר להשאיר את רוב הקוד ב־XIP, להעביר ISR‑ים ולולאות חמות ל־ITCM או SRAM, לשים נתונים רגישים לזמן ב־DTCM, ולהחזיק תמונות ומודלים ב־PSRAM. מי שקובע את המיקום הוא בעיקר ה־Linker Script: הוא מחבר כל Section לכתובת יעד, וקוד האתחול מעתיק Sections שצריכים לעבור לפני main. זו אינה בחירה שעושים אחרי הקומפילציה בעזרת כפתור אחד.

תרשים זרימת האתחול של STM32N6 דרך Boot ROM ו־FSBL אל שלוש אפשרויות הרצה
ה־Boot ROM תמיד מתחיל ראשון. ה־FSBL שרץ מ־SRAM פותח את הגישה לזיכרונות החיצוניים ואז בוחר אם להעתיק את האפליקציה ל־SRAM, להריץ אותה ב־XIP מה־NOR או להעתיק אותה ל־PSRAM.קרדיט: תרשים מקורי ל־NatiLab, מבוסס על מדריכי UM3234 ו־UM3249 של ST

כאן גם חשוב להפריד בין קוד לבין נתונים. קטעי text ו־rodata יכולים להישאר ב־Flash או להיות מועתקים ל־RAM. המשתנים המאותחלים ב־data נשמרים כחלק מתמונת ה־Firmware אך מועתקים ל־RAM בזמן האתחול. משתני bss אינם תופסים תוכן בקובץ; קוד האתחול רק מאפס עבורם אזור RAM. כאשר ה־Linker Script לא מתאים לתוכנית האתחול, מקבלים HardFault עוד לפני שרואים את ה־LED.

עכשיו אפשר לעשות את הניסוי הראשון. אני מניח שהלוח שנקנה הוא STM32N6570-DK. מחברים את USB‑C של ST‑LINK במחבר CN6, מוודאים שהמגשר JP2 נמצא על STLK, ומגדירים את BOOT1 למצב Development המסומן 1; במצב הזה BOOT0 אינו משנה. כדאי להשתמש בגרסאות תואמות של STM32CubeIDE, ‏STM32CubeProgrammer וחבילת STM32CubeN6, ולא לערבב באקראי Pack ישן עם IDE חדש.

במקום ליצור פרויקט ריק, מתחילים מדוגמת ST הרשמית GPIO_IOToggle. היא נמצאת בחבילת STM32CubeN6 תחת Projects, אחר כך STM32N6570-DK, ‏Examples, ‏GPIO ו־GPIO_IOToggle. בתוך STM32CubeIDE בוחרים File ואז Open Projects from File System, מצביעים על תיקיית STM32CubeIDE של הדוגמה, בונים ומפעילים Debug.

הדוגמה מגדירה את PO1 כיציאת Push‑Pull. הפין מחובר ל־LD1 הירוק והוא Active High, כלומר כתיבת 1 מדליקה אותו. בלולאה הראשית ST קוראת ל־HAL_GPIO_TogglePin עם LED1 וממתינה 100 מילישניות בעזרת HAL_Delay. הדוגמה הרשמית מגדירה את הליבה ל־600MHz, לא מפני שצריך 600MHz ל־LED, אלא מפני שהיא מספקת תצורת מערכת מוכנה ובדוקה ללוח.

כשה־LED מהבהב, עוצרים ומסתכלים על קובץ ה־map ועל ה־Linker Script. חפשו את כתובת ה־Reset_Handler, את אזור text, את data ואת bss. פתחו גם Memory View ב־CubeIDE ובדקו שה־PC נמצא בכתובת SRAM ולא בכתובת ה־NOR החיצוני. זהו הניסוי שמחבר בין המילים למציאות: כרגע הקוד רץ מ־RAM, לא מ־Flash.

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

בשלב הבא משתמשים באותה דוגמה כדי לבצע Flash boot. ה־README הרשמי שלה מסביר כיצד להוסיף לקובץ Project.bin כותרת בעזרת STM32 Signing Tool, לצרוב את Project‑trusted.bin ל־Flash החיצוני בכתובת 0x70000000, להעביר את שני מתגי BOOT ל־0 וללחוץ Reset. בגרסאות STM32CubeProgrammer מ־2.21 ומעלה ST דורשת גם את אפשרות align בכלי החתימה, ולכן עדיף להעתיק את הפקודה מה־README של גרסת החבילה המותקנת ולא ממדריך ישן.

השם trusted עלול להטעות. בדוגמת ההתחלה אפשר להוסיף Header ללא מפתחות כדי שה־Boot ROM יבין את מבנה התמונה. זה עדיין אינו Secure Boot למוצר. חתימה אמיתית דורשת ניהול מפתחות, Provisioning ומעבר מתוכנן של מצב החיים של השבב. אל תצרבו OTP ואל תנעלו את הרכיב תוך כדי ניסויי LED; הפעולות האלה חד־פעמיות ודורשות תהליך שחזור מסודר.

אחרי הצריבה עוברים ל־Flash boot ומאתחלים בלי Debug. ה־LED אמור לחזור להבהב. אבל גם עכשיו הפקודות אינן נקראות ישירות מה־Flash: דוגמת GPIO_IOToggle היא FSBL קטן. ה־Boot ROM קורא אותה מה־NOR, מעתיק אותה ל־SRAM ומריץ אותה משם. האחסון הוא חיצוני וקבוע; ההרצה פנימית ונדיפה.

כדי לראות XIP אמיתי, עוברים אחר כך אל Template_FSBL_XIP או אל מדריך ה־Blink ב־XIP של ST. שם מתקבלים לפחות שני פרויקטים: FSBL ואפליקציה. ה־FSBL מאתחל את XSPI וממפה את ה־Flash, והאפליקציה מקושרת לכתובת החיצונית. כאשר עוצרים ב־main ובודקים את ה־PC, הוא צריך להצביע אל חלון ה־Flash ולא אל SRAM.

כדי להשוות בצורה הוגנת, מריצים את אותו Blink בשלוש צורות: Debug ישירות מ־RAM, ‏Flash boot עם FSBL קטן שמועתק ל־RAM, ו־XIP עם FSBL נפרד. מודדים זמן מהפעלת מתח עד שינוי ה־LED הראשון, בודקים את כתובת ה־PC ומסתכלים כמה SRAM נשאר. ה־LED זהה; מה שלומדים הוא מחיר האתחול, מחיר הזיכרון ומורכבות הפרויקט.

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

אל תבלבלו בין שני זוגות של תמונות. ה־Boot ROM של STM32N6 יודע לחפש FSBL ראשון ושני ב־NOR, בהיסטים 0 ו־0x40000, ולנסות את השני אם אימות הראשון נכשל. אלה נתיבי התאוששות לשלב האתחול הראשון. לעומת זאת, Slot A ו־Slot B של האפליקציה הם החלטה של ה־FSBL או של Root of Trust שניתן לעדכון. הם צריכים Layout נפרד שאנו מגדירים.

לכן כדאי כבר מהיום לשמור את ה־FSBL קטן ומשעמם, ולא לערבב בו את כל האפליקציה העסקית. חלוקה בריאה ל־Flash החיצוני כוללת אזורי FSBL לפי ה־Layout הרשמי, אזור Boot או Root of Trust, שני Slots לאפליקציה, אזור הורדה או Scratch לפי שיטת ההתקנה, ומקום נפרד למודלים, הגדרות ונתוני ייצור. את הגדלים קובעים רק אחרי שמודדים את הבינארי, זמן ההעתקה וצריכת ה־RAM.

ל־STM32N6 יש יתרון גדול ל־OTA: ה־Flash החיצוני בלוח הוא 128MB, הרבה יותר מה־4.2MB הפנימיים. יש מקום נוח לשתי תמונות, למודל AI ולגרסת שחזור. אבל הגודל אינו פותר אטומיות. אסור שירידת מתח באמצע מחיקה, כתיבה או עדכון Metadata תשאיר את המכשיר בלי תמונה תקפה.

תהליך עדכון טוב מוריד תמונה ל־Slot שאינו פעיל, בודק Hash וחתימה, מסמן אותה כמועמדת ורק אז מבצע Reset. האתחול הבא מפעיל אותה במצב ניסיון. האפליקציה צריכה לאשר שהיא בריאה רק אחרי שעברו בדיקות אמיתיות — זיכרון חיצוני, תקשורת, Watchdog והציוד הקריטי. אם אין אישור בזמן, ה־Bootloader חוזר לגרסה הקודמת.

ST מספקת ב־STM32CubeN6 את OEMuRoT, פתרון המבוסס על MCUboot שמוסיף Secure Boot ו־Secure Firmware Update. זהו בסיס טוב יותר ל־OTA מאשר Bootloader פרטי שנכתב סביב כמה if‑ים ו־CRC. CRC יכול לזהות קובץ פגום; הוא אינו מוכיח מי יצר אותו. למוצר מחובר צריך אימות מקור, מניעת חזרה לגרסה פגיעה, הגנה על מפתחות ונתיב Recovery.

גם בחירת מקום ההרצה משפיעה על העדכון. באפליקציית XIP אי אפשר להתייחס ל־Flash שממנו המעבד קורא פקודות כאילו הוא דיסק רגיל ולמחוק אותו באמצע. צריך Slot אחר או Driver שרץ מזיכרון שאינו תלוי באזור הנכתב. באפליקציית LRUN הקוד כבר נמצא ב־RAM בזמן העבודה, ולכן כתיבה ל־Flash פשוטה יותר מבחינת Fetch של פקודות, אך עדיין צריך להגן על ה־FSBL ועל התמונה התקפה.

ה־Cache וה־DMA הם המלכודת הבאה. כאשר ה־CPU, מאיץ AI, תצוגה או DMA חולקים Buffer בזיכרון חיצוני, ייתכן שאחד מהם רואה עותק ישן שנשאר ב־Cache. לפני OTA זה מופיע כקובץ שהתקבל נכון ברשת אבל Hash שלו נכשל בזיכרון, או כ־Metadata שנכתב אך לא נראה לבעלים אחר. צריך להחליט מראש אילו אזורים Cacheable ולבצע Clean או Invalidate במקום הנכון.

סדר העבודה שאני ממליץ עליו הוא מדורג. קודם לגרום ל־GPIO_IOToggle לרוץ מ־RAM ולזהות את הכתובות. אחר כך לאתחל אותו עצמאית מה־Flash ולהבין את ה־Header וה־FSBL. אחר כך לעבור ל־LRUN עם FSBL ואפליקציה נפרדים. רק לאחר מכן להריץ את אותה אפליקציה ב־XIP, למדוד זמן אתחול ו־RAM פנוי, ולבסוף לפתוח את דוגמת OEMuRoT ולתכנן Slots.

בכל שלב שמרו Map file, רשמו כתובת טעינה, כתובת ריצה וגודל בינארי, והדליקו LED שונה או דפוס שונה בכל שלב אתחול. לדוגמה: הבהוב קצר בכניסה ל־FSBL, שני הבהובים לפני הקפיצה לאפליקציה והבהוב איטי אחרי שהאפליקציה אישרה בריאות. כך Boot שנראה מסתורי הופך לרצף שאפשר לראות גם בלי Debugger.

מה ששונה כאן מ־STM32 רגיל אינו רק שה־Flash עבר לרכיב חיצוני. ה־STM32N6 מכריח את המפתח להיות אדריכל זיכרון כבר בפרויקט הראשון: להפריד בין אחסון להרצה, להבין מי מאתחל כל זיכרון, לבחור מה מועתק ומה נשאר ב־XIP, ולשמור נתיב אתחול קטן שאפשר לסמוך עליו. זו מורכבות אמיתית, אבל היא גם מה שנותן מקום לאפליקציות ולמודלי AI גדולים בלי להפוך את השבב ל־MPU שמריץ Linux.

אם ה־LED הראשון ייבנה בשלושת המצבים האלה ויהיה ברור בכל רגע איפה הבינארי נשמר ואיפה ה־PC רץ, הבסיס ל־OTA כבר יהיה הרבה יותר חזק. עדיין לא תהיה מערכת עדכון, אבל יהיו הגבולות הנכונים: Boot ROM שאינו משתנה, FSBL קטן, Layout חיצוני מתועד, אפליקציה נפרדת ומדיניות ברורה להרצה מ־RAM או מ־Flash. זה בדיוק המקום הנכון להתחיל ממנו לפני שמוסיפים רשת, חתימות ו־Rollback.

הזווית ההנדסית

למה זה מעניין

ה־STM32N6 נותן כוח עיבוד וזיכרון חריגים למיקרו־בקר, אבל המחיר הוא ארכיטקטורת אתחול שצריך להבין מוקדם. Blink שרץ מ־RAM, אחר כך מ־Flash boot ולבסוף ב־XIP מלמד בפועל את ההבדל בין אחסון להרצה, את תפקיד ה־FSBL ואת גבולות הזיכרון. אותו בסיס מונע בהמשך את הטעויות הכואבות של OTA: דריסה של התמונה הפעילה, Bootloader גדול מדי, Layout לא מתועד ועדכון שאין ממנו Rollback.