כדי להבין את התקלה צריך להתחיל במושג הכי יומיומי: HAL Tick. ספריית HAL של ST צריכה בסיס זמן כדי לדעת כמה זמן עבר. היא משתמשת בו בשביל HAL_Delay, בשביל timeouts, בשביל polling loops ובשביל חלק מדרייברי הפריפריה. מתחת למכסה המנוע יש מונה גלובלי בשם uwTick, ותדירות tick שמיוצגת דרך uwTickFreq. פעם במילישנייה, או בקצב אחר שהוגדר, פסיקה קוראת ל־HAL_IncTick ומקדמת את המונה. כשקוד HAL מחכה לדגל חומרה, הוא שומר את הערך הנוכחי של tick ובודק אם עבר מספיק זמן כדי לוותר ולחזור עם timeout.
הנקודה החשובה היא ש־timeout עובד רק אם הזמן עצמו זז. אם פריפריה עונה מיד, הרבה קוד יכול להיראות תקין גם כאשר ה־tick קפוא, כי אין צורך אמיתי ב־timeout. התקלה מופיעה דווקא ביום הרע: חיישן לא עונה, דגל BUSY לא משתחרר, תקשורת נתקעת או clock לא הגיע למצב יציב. במקום שהפונקציה תחזור אחרי כמה מילישניות עם שגיאה, היא נשארת בלולאה שנראית כמו deadlock. לכן HAL Tick הוא לא קישוט של HAL_Delay; הוא מנגנון בטיחות שמאפשר לקוד לצאת ממצב שבו החומרה אינה מתנהגת כצפוי.
ב־STM32 רגיל מקור הזמן הזה הוא לעיתים SysTick, טיימר פנימי של ליבת Cortex-M. SysTick נוח כי הוא קרוב לליבה ונמצא כמעט בכל פרויקט. אבל בפרויקטים עם RTOS, ולעיתים גם בפרויקטים עם TrustZone, מקובל להעביר את בסיס הזמן של HAL ל־Timer נפרד כמו TIM6 או TIM7. הסיבה פשוטה: RTOS משתמש בדרך כלל ב־SysTick לצרכים שלו, ו־TrustZone מוסיף עוד שאלה של בעלות ושל יעד פסיקה. אם HAL של ה־Non-Secure תלוי במקור זמן שהוגדר ונצרך בצד Secure, קל להגיע למצב שבו צד אחד חושב שהזמן מתקדם והצד השני נשאר בלי דופק.
כאן TrustZone משנה את צורת החשיבה. לפי תיעוד ST, TrustZone מפריד בין Secure world לבין Non-Secure world, וקוד Non-Secure אינו אמור לגשת חופשי לכתובות ולמשאבים של Secure. ה־M55 מתחיל את חייו בצד המאובטח, ומשם שרשרת האתחול מגדירה מי יקבל אילו אזורי זיכרון, אילו פריפריות ואילו פסיקות. זה אומר שגם אם ה־Secure image אתחל SysTick או Timer בזמן boot, זה לא מבטיח שה־Non-Secure image קיבל timebase פעיל משלו. המעבר ל־Non-Secure צריך להכין לא רק את ה־PC ואת ה־MSP, אלא גם את טבלת הווקטורים, הגדרות NVIC, בעלות הפריפריות, clocks וכל מה שמאפשר ל־HAL של הצד הלא־מאובטח לחיות לבד.
אפשר לחשוב על זה כמו על שני חדרי עבודה עם שעונים נפרדים. בחדר המאובטח יש שעון קיר שפועל, אבל הדלת בין החדרים נעולה. אם בחדר הלא־מאובטח אין שעון משלו, האנשים שם לא יכולים למדוד timeout גם אם בצד השני הזמן מתקדם יפה. בעולם האמיתי השעון הזה הוא interrupt שמגיע ל־HAL_IncTick, והחדרים הם אזורי זיכרון והרשאות חומרה. לכן השאלה הנכונה אינה רק האם יש tick במערכת, אלא האם יש tick שמגיע ל־HAL instance של ה־Non-Secure image.
בפרויקט שלנו הפרט המעודד הוא שלפי הקבצים ה־Non-Secure אינו אמור להישען על SysTick של Secure. הוא משתמש ב־TIM6 כ־HAL timebase. זו בחירה בריאה, כי היא נותנת ל־NS מקור זמן נפרד שאפשר לנהל, למדוד ולתקן. אבל היא גם יוצרת רשימת בדיקה ברורה: TIM6 צריך להיות מוקצה ל־Non-Secure או לפחות נגיש לו לפי חלוקת RIF, ה־clock שלו צריך להיות פעיל, הפסיקה שלו צריכה להגיע לטבלת הווקטורים של NS, וה־handler צריך לקרוא בסוף למסלול שמקדם את HAL Tick של ה־NS. אם אחד מהחלקים האלה נשאר בצד Secure או לא מאותחל אחרי הקפיצה, המונה לא יזוז.
עכשיו מגיעה הנקודה העדינה של uwTick ו־uwTickFreq. אלה משתנים גלובליים של HAL, אבל בפרויקט מרובה images אין משתנה גלובלי אחד לכל המערכת. כל image מקושר בנפרד, מקבל sections משלו, startup משלו וזיכרון runtime משלו. לכן ל־FSBL יכול להיות uwTick משלו, ל־Secure application יכול להיות uwTick משלו, ול־Non-Secure application צריך להיות uwTick משלו. אם ה־Non-Secure image ניגש בטעות למשתנה שנמצא בזיכרון Secure, זו כבר לא בעיית Timer אלא בעיית linker, memory partition או ערבוב קבצים בין images.
ב־STM32 עם TrustZone חשוב גם להבין את שפת הכתובות. בחלק מהמפות קיימים aliases שונים לאותו זיכרון פיזי או לאזורי זיכרון קרובים, כאשר כתובות שמתחילות ב־0x3... משמשות בדרך כלל לגישה Secure, וכתובות שמתחילות ב־0x2... משמשות בדרך כלל לגישה Non-Secure. לכן אם ב־map file של ה־NS רואים ש־uwTick או uwTickFreq נמצאים ב־0x3..., זו נורה אדומה. זה אומר שה־Non-Secure אולי מנסה לגעת בנתון שאינו שייך לו. אם הם נמצאים ב־0x2..., זה לא מוכיח שה־tick עובד, אבל זה מחליש מאוד את הטענה שהמשתנים חסומים בגלל שהם יושבים ב־Secure RAM.
במקרה שלנו, לפי ה־linker scripts שנבדקו, ה־FSBL מקושר ל־RAM באזור 0x34180400, כלומר Secure. ה־Secure application מקושר ל־RAM באזור 0x34000400, גם הוא Secure. ה־Non-Secure application מקושר ל־RAM באזור 0x24100400, כלומר Non-Secure. לכן המסקנה הראשונית היא שה־HAL runtime של ה־NS אמור לשבת במרחב 0x2 ולא במרחב 0x3. זה לא סוגר את החקירה, אבל זה משנה את ניסוח הטענה: אין כאן עדיין הוכחה ש־uwTickFreq של NS חסום בזיכרון Secure. יש כאן נקודה חשובה לבדיקה, שצריך לאמת עם כתובות actual מה־map file או מה־debugger.
הבדיקה הראשונה פשוטה: לפתוח את ה־map file של ה־Non-Secure image ולחפש את uwTick ואת uwTickFreq. לא את הסמלים של ה־FSBL, לא את הסמלים של Secure, אלא את הקובץ שנוצר מה־link של NS. אם שתי הכתובות מתחילות ב־0x2, ממשיכים לבדיקה הבאה. אם אחת מהן ב־0x3, עוצרים ובודקים Linker Script, קובצי startup, sections, וייתכן שגם ערבוב של ספריית HAL או object files בין הפרויקטים. כדאי לבדוק גם ש־VTOR של NS מצביע לטבלת הווקטורים של NS, כי tick יכול להיות תקין מבחינת משתנים ועדיין לא לקבל פסיקה.
הבדיקה השנייה היא runtime ולא תאוריה. בתחילת ה־Non-Secure application מדפיסים את HAL_GetTick, ממתינים פרק זמן קצר בלי להסתמך על HAL_Delay, ואז מדפיסים שוב. אפשר לבצע המתנה קצרה בלולאת CPU גסה רק לצורך הבדיקה, או להשתמש במקור זמן אחר שכבר ידוע כפעיל. אם הערך גדל, ה־NS tick חי. אם הערך נשאר קבוע, צריך לבדוק את TIM6, את ה־clock שלו, את ה־interrupt enable, את עדיפות הפסיקה, את routing הפסיקה לעולם הנכון ואת פונקציית ה־HAL_TIM_PeriodElapsedCallback או handler שמבצע HAL_IncTick. בשלב הזה כבר לא מסתפקים במשפט 'ה־Secure אתחל את הזמן', כי השאלה היא האם NS מקבל interrupt שמקדם את המונה שלו.
יש עוד בדיקה ששווה לבצע, מפני שהיא תופסת את המקרה המסוכן ביותר: לגרום בכוונה לפונקציית HAL ב־NS להגיע ל־timeout. למשל לבחור פעולה שניתן לנתק ממנה את התגובה באופן מבוקר, או להוסיף בדיקת timeout קטנה סביב דגל שלא אמור להשתחרר. אם הפונקציה חוזרת עם timeout, יש עדות מעשית שה־tick מתקדם גם במסלול התקלה. אם היא נתקעת לנצח, סימפטום ה־tick הקפוא כבר לא תיאורטי. זה חשוב מפני שמערכת יכולה לעבור הדגמה מלאה כל עוד כל הפריפריות עונות, ואז להיתקע רק בתנאי כשל אמיתי.
החלק המעניין פה הוא שההערה שקיבלנו נכונה ברמה העקרונית גם אם היא לא מוכיחה בעיה בפרויקט שלנו. HAL Tick ב־TrustZone הוא מוקש אמיתי, כי הוא יושב בדיוק בצומת בין קוד C רגיל, משתנים גלובליים, linker scripts, טבלת פסיקות, חלוקת משאבים ובעלות Secure/Non-Secure. מפתח שמגיע מ־STM32F4 רגיל עלול לחפש את התקלה בדרייבר I3C, ב־DMA או בחיישן, בזמן שהבעיה האמיתית היא שהזמן של HAL בצד הלא־מאובטח אינו מתקדם. לכן כל bring-up של STM32N6 צריך לכלול סעיף מפורש: להוכיח ש־HAL Tick חי בכל image שרץ קוד HAL.
המסקנה הטכנית שלי היא מאוזנת. הטענה ש־uwTickFreq של NS עלול להיות בזיכרון Secure היא טענה נכונה לבדיקה, אבל בפרויקט הנוכחי ה־linker מצביע על RAM של NS באזור 0x24100400, ולכן לא נראה שזו ההוכחה לכך שהפרויקט שבור. מה שכן צריך לעשות הוא למדוד: כתובת actual של uwTick, כתובת actual של uwTickFreq, התקדמות HAL_GetTick בזמן ריצה, והגעה אמיתית של פסיקת TIM6 ל־NS. רק אחרי ארבע הבדיקות האלה אפשר לומר אם יש בעיית timebase, בעיית interrupt routing, בעיית memory partition או פשוט חשד טוב שהתברר כתקין.
זו בדיוק צורת העבודה ש־STM32N6 דורש מאיתנו. לא להסתפק בכך שקוד רץ ב־Secure, לא להניח שה־Non-Secure קיבל את אותה תשתית, ולא להפוך הערה נכונה לאבחנה בלי כתובות ומדידה. בעולם של TrustZone גם הזמן הוא משאב עם בעלים. אם לא נותנים לו בעלים ברור בכל image, התקלה תחכה לרגע שבו הכי קשה להבין אותה: לא בזמן ה־LED הראשון, אלא בזמן timeout שאמור להציל אותנו ולא מגיע.
הזווית ההנדסית
למה זה מעניין
ב־STM32N6 עם TrustZone תקלה קטנה ב־HAL Tick יכולה להפוך ל־timeout שלא מסתיים, אבל היא לא תמיד מופיעה בזמן bring-up רגיל. הכתבה מלמדת מפתח שמגיע מ־STM32F4 לחשוב על הזמן כמשאב נפרד לכל image: Secure, Non-Secure ו־FSBL. היא גם מפרידה בין חשד נכון לבין הוכחה: בפרויקט הנוכחי כתובות ה־linker של ה־NS מצביעות על RAM לא־מאובטח, ולכן צריך לאמת את הכתובות actual ואת התקדמות HAL_GetTick לפני שמסיקים שהפרויקט שבור.
השיחה ממשיכה
תגובות
שאלות, תיקונים ורעיונות שאפשר לבנות מהם משהו.