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

חשוב להפריד כבר בהתחלה בין ארבעה דברים שבשיחה רגילה מתערבבים למילה אחת, AI — בינה מלאכותית. הנתונים הם המדידות והתגיות שאספנו; המודל הוא מבנה מתמטי עם אלפי מספרים שנלמדו; כלי האימון הוא התוכנה שמשנה את המספרים האלה לפי הטעויות; וה־NPU הוא חומרה שמבצעת במהירות את החישובים של המודל המוגמר. ה־NPU אינו מתאמן בפרויקט הזה ואינו מבין מהי אבן במובן אנושי. הוא מקבל טבלה של מספרים ומחזיר ארבעה ציונים.

גם המילה תמונה עלולה להטעות. ה־VL53L9CX אינו מצלם צבע כמו מצלמת טלפון, אלא מאיר את הסצנה באור תת־אדום ומודד זמן מעוף, Time of Flight או בקיצור ToF: כמה זמן עבר עד שהאור חזר מכל אזור. לפי ST, החיישן מספק עד 54 על 42 אזורי עומק. בפרופיל שלנו כל פריים הוא 2,268 מספרים, וכל מספר הוא מרחק במילימטרים. יד קרובה ורקע רחוק נראים כצורה גם בלי צבע, ולכן אפשר לזהות מחווה בתנאי תאורה שבהם מצלמת RGB רגילה הייתה צריכה להתמודד עם צל, גוון עור וטקסטורה.

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

תרשים זרימה מחיישן ToF דרך DMA, איסוף Python, אימון, קוונטיזציה וחזרה אל ה־NPU
יש כאן שתי לולאות שמתחברות: הלוח מייצר פריימים בזמן אמת, והמחשב הופך דוגמאות מתויגות למודל שחוזר אל אותה קושחה.קרדיט: תרשים מקורי ל־NatiLab, מבוסס על קוד ודו״חות פרויקט N6

התחנה הראשונה היא הקושחה. החיישן מודיע על פריים חדש באמצעות קו פסיקה, ואז משימת רכישה ב־ThreadX — מערכת ההפעלה בזמן אמת שלנו — מתחילה קריאה דרך I3C. אפשר לחשוב על I3C ככביש תקשורת מהיר בין המיקרו־בקר לחיישן. המעבד מגדיר מה להעביר, אבל מנגנון DMA מבצע את העברת הבתים בין הפריפריה לזיכרון. DMA דומה למלגזה שמזיזה ארגזים במחסן: מנהל העבודה נותן יעד וכמות, ואינו צריך לשאת כל ארגז ביד.

בפרויקט יש שלושה מאגרי raw קבועים, כלומר שלושה מקומות ששומרים את פריים היצרן המלא לפני הפענוח, וכל אחד בגודל 14,842 בתים. משימת הרכישה לוקחת מאגר פנוי, ה־DMA ממלא אותו, וכאשר callback — פונקציה קצרה שנקראת עם סיום פעולה — מודיע שההעברה הסתיימה, המצביע למאגר עובר לתור מוכן. משימת עיבוד נפרדת לוקחת את המצביע, מפענחת את מבנה היצרן ומפיקה ממנו מפת עומק של 54 על 42 ערכי float, מספרים שיכולים לשמור גם חלק שברי. כך החיישן יכול להתחיל את הפריים הבא בזמן שהמעבד עדיין מעבד את הקודם. אם העיבוד מפגר, המערכת מעדיפה לזרוק פריים ישן שטרם נתפס ולא להציג תמונה מיושנת.

הבחירה בשלושה מאגרים קבועים ובתורים של מצביעים היא אופטימיזציה חשובה יותר ממה שנראה. אין malloc — בקשת זיכרון דינמית — בכל פריים, אין העתקה של 14KB ממשימה למשימה, ובכל רגע ברור מי הבעלים של כל מאגר. בעולם Embedded זה מקטין קפיצות בזמן הריצה, מקטין פיצול של heap, אזור הזיכרון שממנו נלקחות הקצאות דינמיות, ומונע מצב שבו רכישה מהירה דורסת נתונים שעיבוד איטי עדיין קורא. אופטימיזציה כאן אינה רק שהקוד רץ מהר; היא שהזרימה נשארת צפויה גם אחרי אלפי פריימים.

כדי שהמחשב יוכל להקליט, הוספנו לקושחה פקודה בשם DATASET STREAM. לכל פריים היא בונה רשומה בינארית מסוג N6DF v2: כותרת של 64 בתים, ואחריה 4,536 בתים של עומק — 2,268 ערכים של 16 ביט. בכותרת נמצאים ממדי התמונה, טווח המדידה, מספר הפריים, חותמת זמן, תוצאת ה־NPU ומונים. יש CRC אחד לנתוני העומק ו־CRC נוסף לכותרת. CRC הוא מעין טביעת אצבע קצרה שמאפשרת לזהות בית שנפגם או נעלם בדרך.

מספר הפריים חשוב לא פחות מה־CRC. CRC תקין מוכיח שהחבילה שקיבלנו שלמה, אבל אינו מוכיח שהלוח לא שלח את אותה חבילה מאה פעמים. לכן Python דורש שמזהי הפריימים יתקדמו, יודע להסתנכרן מחדש אחרי טקסט או בית חסר, ועוצר אם הזרם נתקע. שני מאגרים סטטיים בצד הקושחה משמשים להכנת רשומות ה־USB; אם שניהם תפוסים, הפריים נספר כדילוג והחיישן ממשיך. איסוף נתונים לעולם לא אמור לחסום את משימת הרכישה.

כאן נכנסים קובצי BAT ו־Python. קובץ BAT אינו אלגוריתם AI; הוא מעטפת Windows שמגדירה תיקייה, בוחרת את Python הנכון, מדפיסה הסבר ומפעילה סקריפט עם פרמטרים. היתרון שלו הוא שאפשר ללחוץ על 01_CAPTURE.bat ולקבל מסלול מודרך בלי לזכור פקודה ארוכה. Python הוא זה שפותח את ה־COM, מפענח N6DF, מציג חלון, שומר קבצים ובהמשך מפעיל את TensorFlow. ההפרדה מאפשרת לכל שלב להיות קטן, ניתן להרצה חוזרת וקל יותר לדיבוג.

השלב הראשון, 00_SETUP.bat, יוצר סביבה וירטואלית מקומית של Python 3.11. סביבה וירטואלית היא ארגז כלים סגור לפרויקט: גרסאות NumPy, ‏Pillow, ‏pyserial ו־TensorFlow — ספריית למידת המכונה שבה נאמן את הרשת — אינן תלויות במה שהותקן במקרה במחשב. הקובץ requirements.txt מתאר אילו חבילות נדרשות, וההתקנה נשמרת תחת training/.venv. זה מסביר מדוע יש כל כך הרבה קבצים בתיקייה הזאת: רובם ספריות מוכנות, לא קוד שכתבנו שורה אחר שורה.

ב־01_CAPTURE.bat התוכנה מאתרת את חיבור ה־USB של הלוח, מכבה תצוגת טקסט שעלולה להתערבב בזרם ומפעילה DATASET STREAM. המקשים R, ‏P, ‏S ו־N בוחרים אבן, נייר, מספריים או ללא מחווה, ורווח פותח וסוגר burst — רצף צילום אחד. יש ספירה לאחור של שנייה וחצי כדי שלא נשמור בטעות את המעבר בין תנוחות, וקצב השמירה הוא שלוש דגימות בשנייה. התוכנה גם מדלגת על פריימים כמעט זהים, מפני שמאה עותקים של אותה יד אינם מאה דוגמאות חדשות.

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

כל דגימה נשמרת בשלוש צורות, מפני שלכל צורה תפקיד אחר. קובץ NPZ שומר מערך uint16 מדויק ונוח ל־Python; PNG בגווני אפור של 16 ביט שומר את אותם מילימטרים בפורמט פתוח; ו־PNG צבעוני ומוגדל הוא רק תצוגה לעין. uint16 פירושו מספר שלם ללא סימן ברוחב 16 ביט, ולכן הוא יכול לשמור מרחקים עד 65,535. הערך 65,535 שמור אצלנו לפיקסל לא תקין. אסור לאמן מתמונת התצוגה הצבעונית, מפני שהצבע וההמרה ל־8 ביט כבר זרקו מידע.

לכל דגימה נוספת רשומת JSONL עם תגית, session, ‏burst, מזהה פריים ו־SHA-256. ‏Session הוא סבב הפעלה של תוכנת הצילום, ו־JSONL הוא קובץ טקסט שבו כל שורה היא אובייקט JSON עצמאי, ולכן אפשר להוסיף דגימות בלי לשכתב את כל הקובץ. ‏SHA-256 הוא hash, טביעת אצבע ארוכה של התוכן; אם בית בקובץ השתנה, ההשוואה תיכשל. הכתיבה בנויה כך שהרשומה מתווספת רק אחרי שכל קובצי הדגימה נכתבו. אם החשמל נפל באמצע, נשאר אולי קובץ חלקי, אבל הוא אינו מוצג כאילו זו דוגמה תקינה.

בפועל אספנו 910 דגימות תקינות בחמישה sessions: ‏410 ללא מחווה, 150 אבן, 150 נייר ו־200 מספריים. שלב 03 עבר על כל קובץ, בדק ממדים של 42 על 54, טיפוס uint16, ‏hash, קובץ PNG מלווה ויחס פיקסלים לא תקינים. הוא לא מצא שגיאות שלמות ולא מצא פריימים זהים לחלוטין. זו כמות יפה לניסוי לימודי, אבל היא עדיין קטנה ומגיעה מסביבה מוגבלת. מספר קבצים גדול אינו תחליף לאנשים שונים, מרחקים שונים ורקעים שונים.

לפני אימון מחלקים את הדוגמאות ל־train, ‏validation ו־test. קבוצת train היא חומר הלימוד; validation היא מבחן ביניים שמכוון החלטות כמו מתי לעצור; ו־test נשמר בצד עד הסוף כדי לבדוק מה המודל עושה על מידע שלא שימש לבנייתו. הטעות הקלאסית היא לפצל באקראי כל פריים. אז פריים 100 יכול להיכנס ל־train ופריים 101 הכמעט זהה ל־test, והמודל נראה חכם רק מפני שכבר ראה את אותה תנוחה.

לכן אצלנו כל burst נשאר בשלמותו באחת משלוש הקבוצות. האלגוריתם מחפש חלוקה קרובה ל־70/15/15, אך גם דורש שלכל מחלקה יהיו לפחות עשר דוגמאות בכל קבוצה ולפחות חצי מן הדוגמאות ב־train. התוצאה הנוכחית היא 641 דוגמאות לאימון, 137 ל־validation ו־132 ל־test. השיטה הזאת מחמירה יותר, ולכן הדיוק נמוך יותר אך אמיתי יותר. היא בודקת הכללה בין רצפי צילום, לא זיכרון של פריימים שכנים.

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

בגרסה שנבדקה אנחנו לוקחים רק ערכים בין 100 ל־1,200 מילימטר, מחשבים את אחוזון 5 של המדידות הקרובות, ושומרים את מה שנמצא עד 160 מילימטר מאחוריו. אחוזון 5 הוא ערך שרק כחמישה אחוזים מן המדידות הקרובות ממנו; הוא עמיד יותר מפיקסל מינימום יחיד שעלול להיות רעש. סביב האזור שנותר בונים מלבן עם שוליים של שני פיקסלים, מגדילים אותו ל־64 על 50 תוך שמירת יחס הממדים, וממרכזים על רקע שחור. פיקסל קרוב מקבל ערך גבוה, רחוק מקבל ערך נמוך, וההגדלה משתמשת ב־nearest neighbor — העתקת השכן הקרוב — כדי שלא להמציא מרחקי ביניים.

התוצאה היא tensor בגודל 1×50×64×1. ‏Tensor אינו מושג מיסטי; זו פשוט טבלת מספרים בעלת כמה ממדים. המספר הראשון הוא batch של דוגמה אחת, אחריו גובה ורוחב, והמספר האחרון הוא ערוץ יחיד של עומק. הסדר הזה נקרא NHWC. קוד Python וקוד C בלוח מממשים את אותו חוזה, ו־model_contract.json מקפיא צורה, טיפוס, סדר מחלקות, scale, ‏zero point ו־hash של המודל.

TensorFlow הוא ספריית תוכנה שמאפשרת לתאר גרף של פעולות מתמטיות, להריץ אותו על הרבה דוגמאות ולחשב כיצד לשנות את המשקולות כדי להקטין שגיאה. Keras הוא הממשק הגבוה והקריא שבו בנינו את הרשת: שכבה אחר שכבה, בלי לכתוב ידנית נגזרות ומכפלות מטריצות. ‏TensorFlow מבצע את האימון במחשב; הוא אינו נצרב בשלמותו על ה־STM32. לאחר האימון אנחנו שומרים מודל Keras, ממירים אותו ל־TFLite, ורק אז כלי ST מתרגם אותו למימוש שמתאים ל־NPU.

הרשת שלנו היא CNN, רשת קונבולוציה. במקום לחבר כל פיקסל לכל פיקסל, מסנן קטן של 3 על 3 עובר על התמונה ומחפש דפוס מקומי. בתחילת האימון המספרים במסנן כמעט אקראיים; עם כל תיקון הם נעשים רגישים לקצוות, כתמים או צירופים שעוזרים להפריד בין המחוות. שכבות עמוקות יותר מקבלות את התכונות מן השכבות הקודמות ורואות אזור גדול יותר של היד. זה נקרא receptive field — כמה מן הקלט יכול להשפיע על תוצאה פנימית אחת.

המבנה המדויק מתחיל בהמרת ערכי 0 עד 255 לטווח 0 עד 1. אחר כך באות ארבע שכבות Conv2D עם 16, ‏32, ‏48 ו־64 ערוצים, ובכל אחת מופעל ReLU: ערך שלילי הופך לאפס וערך חיובי נשאר, כדי שהרשת תוכל לבנות קשרים שאינם רק חיבור לינארי אחד גדול. אחרי שלוש השכבות הראשונות יש MaxPool שמקטין את המפה ושומר תגובות חזקות, ובסוף Global Average Pooling מסכם כל ערוץ על פני כל המיקום. שכבת Dense של 32 יחידות משלבת את התכונות, ושכבה אחרונה מחזירה ארבעה ציונים דרך softmax. ‏Softmax הופך את הציונים להתפלגות שסכומה אחד, ולכן אפשר להציג confidence, אך confidence גבוה אינו הוכחה שהתשובה נכונה.

ברשת יש 48,596 parameters — בעיקר משקולות ו־bias. משקל הוא מספר שמחליט כמה אות פנימי אחד ישפיע על הבא; bias הוא היסט שמאפשר להזיז את סף התגובה. עבור דוגמה אחת, דו״ח STEdgeAI סופר בערך 8.32 מיליון פעולות multiply-accumulate, חיבור של מכפלה אל סכום. אלה הרבה חישובים לאדם, אך מבנה קטן יחסית למאיץ ייעודי. ההחלטה לשמור שכבות סטנדרטיות ופשוטות נועדה לא רק להקטין את המודל, אלא לאפשר לכלי ST למפות את רובו לחומרה.

האימון עובד במחזורים שנקראים epochs. בכל epoch המודל רואה את כל קבוצת האימון במנות של 32 דוגמאות, מחשב loss — מספר שמייצג עד כמה התשובות רחוקות מן התגיות — ומנגנון backpropagation מחשב לאיזה כיוון להזיז כל משקל. האלגוריתם Adam קובע את גודל הצעדים. ההרצה הוגדרה לעד 35 epochs ובפועל נרשמו 32. checkpoint נכתב אחרי כל epoch, ולכן נפילה של המחשב אינה מחייבת להתחיל מחדש, כל עוד ה־hash של הנתונים וההגדרות לא השתנה.

מספר דוגמאות ה־none גדול ממספר דוגמאות אבן ונייר, ולכן האימון משתמש ב־class weights. טעות במחלקה קטנה מקבלת משקל גבוה יותר, כדי שהמודל לא ילמד לנצח פשוט על ידי בחירה במחלקה הנפוצה. גם מדד ה־validation מחושב בצורה מאוזנת, ו־early stopping עוצר כאשר אין שיפור מספיק ומחזיר את המשקולות הטובות ביותר. כל אלה מגינים מפני מספר יפה שמסתיר מחווה חלשה, אבל הם אינם יכולים ליצור גיוון שלא צולם.

כאן מגיע המספר שחשוב לא לטשטש: על קבוצת ה־test המודל הנוכחי הגיע ל־50.38% דיוק מאוזן. אבן קיבלה 60%, ‏none כ־55.1%, נייר 44% ומספריים כ־42.4%. זה טוב מעט משער האיכות הלימודי שהוגדר ל־50%, אך רחוק ממודל שהייתי מכניס למוצר. תשתית האיסוף, האימון וההרצה עובדת; איכות המסווג עדיין דורשת יותר אנשים, יותר sessions, מרחקים ורקעים, ואולי שינוי preprocessing או ארכיטקטורה.

אחרי האימון יש לנו מודל שמחשב בעיקר במספרי float של 32 ביט. float מאפשר טווח ודיוק נוחים ללמידה, אבל הוא תופס ארבעה בתים לכל מספר ודורש חומרה וזרימת נתונים יקרות יותר. קוונטיזציה ממפה את המספרים הרציפים לרשת צפופה של ערכים שלמים; בפרויקט הזה בחרנו 8 ביט, כלומר 256 ערכים אפשריים. לא השתמשנו ב־4 ביט: ייצוג כזה היה מקטין עוד את המשקולות, אבל משאיר רק 16 ערכים, מגדיל את הסיכון לאובדן דיוק ותלוי בתמיכה המדויקת של החומרה והקומפיילר. ‏scale אומר כמה יחידות אמיתיות מייצג צעד אחד, ו־zero point אומר איזה מספר שלם מייצג אפס.

ב־06_QUANTIZE.bat ‏TensorFlow Lite Converter מקבל עד 200 דוגמאות מייצגות מתוך train ורואה אילו טווחים באמת עוברים בכל שכבה. החוזה שהוא מפיק דורש קלט uint8 ופלט int8, וכל קבוצת ה־test רצה שוב דרך TFLite Interpreter כדי למדוד את ירידת הדיוק ולא רק לבדוק שנוצר קובץ. המודל הכמותי תופס 59,568 בתים, ובבדיקה הזאת לא איבד דיוק ביחס למודל ה־float. בדו״ח STEdgeAI רואים שהקונבולוציות והחישוב הכבד ממופים למספרים כמותיים ול־Neural‑ART, אך שכבת ההמרה שבכניסה ו־softmax שביציאה נשארות מקטעי מעטפת היברידיים או תוכנתיים. לכן INT8 מתאר את חוזה הקלט, הפלט והחישוב המרכזי; הוא אינו מבטיח שכל פעולה קטנה בגרף רצה באותו בלוק חומרה.

הפלט הוא ארבעה מספרי int8 עם scale של 1/256 ו־zero point של מינוס 128. לדוגמה, ערך מינוס 128 מייצג הסתברות קרובה לאפס, וערך קרוב ל־127 מייצג הסתברות קרובה לאחד. הקושחה בוחרת את המספר הגדול ביותר וממירה אותו לאלף חלקים לצורך התצוגה. ארבעת המספרים הגולמיים נשמרים גם בזרם הבדיקה מן הלוח, מפני שהשוואת המילה ROCK בלבד יכולה להסתיר הפרש גדול בין המחשב ללוח.

TFLite עדיין אינו קובץ שה־NPU של STM32N6 מריץ ישירות. בשלב 07 מפעילים את STEdgeAI Core CLI — כלי שורת הפקודה של ST — עם יעד stm32n6 ועם האפשרות st-neural-art. הכלי מנתח את הגרף, מחליט אילו פעולות ירוצו בחומרת Neural‑ART, מקצה מאגרי זיכרון ומייצר קובצי C, קובצי header שמגדירים את הממשק, תיאורי העברה ו־blob, קובץ בינארי גולמי של משקולות. הסקריפט שלנו מסרב לקבל פלט שאין בו סימני LL_ATON או blob משקולות, כדי שכשל בכלי לא יהפוך בשקט להרצה איטית על Cortex-M55, ליבת המעבד הכללית.

NPU הוא Neural Processing Unit — יחידת עיבוד שנבנתה למכפלות, קונבולוציות ותנועת נתונים שחוזרות ברשתות נוירונים. ה־CPU עדיין מפעיל את מערכת ההפעלה, קורא את החיישן, מכין את הקלט וקורא לפונקציית run. ה־NPU מבצע את הליבה החישובית במבנה מקבילי ויעיל יותר. בדו״ח של המודל הנוכחי יש עשרה מקטעי ביצוע: שבעה בחומרת ה־NPU, אחד היברידי ושניים בתוכנה, כולל פעולות מעטפת כמו המרה ו־softmax. לכן המילה NPU אינה אומרת שכל שורה ברשת רצה באותו בלוק חומרה.

כדי להבין את השלב הבא צריך להכיר ארבעה סוגי זיכרון. קוד הוא רצף ההוראות; weights הם המספרים הקבועים שנלמדו; activations הן תוצאות הביניים שנוצרות ונדרסות בכל inference; ו־workspace הוא מקום זמני ל־preprocessing, תורים ותקשורת. כלי הקומפילציה צריך לקבוע איפה כל אחד יחיה ומי יקרא או יכתוב אותו. אם שתי שכבות מקבלות בטעות אותו אזור בזמן חופף, המודל אינו רק איטי — הוא מחזיר שטויות או מפיל את הלוח.

במיפוי הנוכחי Neural‑ART משתמש בכ־18.5KiB של activations ב־SRAM5. ‏SRAM הוא זיכרון העבודה הפנימי והמהיר של הבקר; המספרים 2, ‏3, ‏5 ו־6 מציינים בנקים נפרדים שאפשר לתת להם תפקידים שונים. ‏Blob המשקולות הוא 49,809 בתים ומועתק ל־SRAM6 בכתובת 0x24350000. החלק העליון של SRAM3, חלון של 176KiB, שמור למאגרים זמניים של ה־CPU: תמונת העומק, histogram, ‏crop ומאגרי תקשורת. SRAM2 נשאר עבור האפליקציה, מחסניות ThreadX וה־heap הגדול שספריית הטרנספורמציה של VL53L9 צריכה. שלב השילוב מסרב למודל עתידי שבחר SRAM3 או למשקולות שאינן נכנסות ל־SRAM6, במקום לאפשר overlap שקט.

מדוע המשקולות נמצאות גם ב־Flash וגם ב־SRAM6? ב־Flash הן חלק ממערך const בתוך תמונת Non‑Secure החתומה, כדי שיישמרו גם אחרי כיבוי. בזמן האתחול RPS_AI_Init מעתיקה אותן ל־SRAM6, שממנו ה־NPU יכול לקרוא במהירות ובמסלול המתאים. כך קוד הרשת והמשקולות שלו נמצאים באותו קובץ עדכון. אם עדכון A/B חוזר לגרסה הקודמת, גם הקוד וגם המשקולות חוזרים יחד; אי אפשר להפעיל בטעות קוד חדש עם weights ישנים.

בדרך פגשנו תקלה מלמדת במיוחד. STEdgeAI יצר במקור תיאורי DMA למשקולות ב־xSPI חיצוני וסימן אותן cacheable. העברנו את הכתובות ל־SRAM6, אבל שינוי כתובת לבדו השאיר את תכונת ההעברה הישנה. ה־NPU ניסה לבחור מסלול BUSIF שמתאים לזיכרון המטמון החיצוני ונפל ב־BUSIF1 כבר בקריאת המשקל הראשונה. שלב 08 משנה גם כל descriptor כזה ל־non-cacheable; זו דוגמה לכך שכתובת אומרת איפה הנתון נמצא, אך מאפייני cache ו־DMA אומרים באיזו דרך החומרה אמורה להגיע אליו.

Cache הוא זיכרון קטן ומהיר שמחזיק עותק של נתונים כדי לחסוך גישה לזיכרון איטי יותר. הבעיה מתחילה כאשר CPU, ‏DMA ו־NPU מחזיקים או רואים עותקים שונים: אחד כתב ערך חדש, ואחר עדיין קורא עותק ישן. לכן קוד שנוצר על ידי ST כולל פעולות clean, שדוחפות כתיבה החוצה, ו־invalidate, שמבטלת עותק ישן לפני קריאה. מאגרי DMA גם מיושרים לגבולות שמתאימים לחומרה. במקרה שלנו משקולות SRAM6 מוגדרות non-cacheable, בעוד activations מנוהלות ב־SRAM5 לפי תיאורי הרשת. סנכרון זיכרון הוא חלק מן האלגוריתם המעשי, גם אם אינו מופיע בתרשים ה־CNN.

תרשים חלוקת Flash, SRAM2, SRAM3, SRAM5 ו־SRAM6 בין קוד, מאגרים, אקטיבציות ומשקולות
המודל אינו קובץ אחד שיושב במקום אחד: הוא נשמר עם הקושחה ב־Flash, המשקולות מועתקות ל־SRAM6, תוצאות הביניים חיות ב־SRAM5 ומאגרי ה־CPU מופרדים ב־SRAM3.קרדיט: תרשים מקורי ל־NatiLab, מבוסס על linker, פלט STEdgeAI וקוד האתחול של פרויקט N6

עכשיו אפשר לענות במפורש על השאלה מהי התוכנה שנצרבת. קובצי BAT ו־Python נשארים במחשב ולעולם אינם נצרבים לבקר; הם מייצרים נתונים ותוצרים. אל הלוח נכנסת firmware — קושחה — שנכתבה בעיקר ב־C יחד עם קוד הרשת שהפיק STEdgeAI ומערך המשקולות. compiler מתרגם כל קובץ C להוראות מכונה של Cortex‑M55, וה־linker מחבר את כל החלקים לתמונה אחת וממקם כל קטע לפי קובץ ה־linker. לכן שינוי ב־SRAM או בכתובת משקולות הוא החלטת build, לא קובץ שמעתיקים ידנית אחרי הצריבה.

ה־linker גם מפיק map file, רשימה שמראה באיזו כתובת נמצא כל קוד ומאגר וכמה מקום הוא תופס. אצלנו הוא שומר את החלון העליון של SRAM3 ל־npu_shared_bss ובודק בזמן הקישור שהמאגר אינו גדול מ־176KiB. אחרי הקישור נוצרת תמונת Non‑Secure בינארית עם headers, קוד, runtime ומשקולות. כלי החתימה מוסיף מידע שמאפשר לשרשרת האתחול לזהות את התמונה ולוודא שלא הוחלפה או נפגמה. חתימה אינה מאיצה את המודל; היא עונה על השאלה אם מותר לסמוך על התוכנה שעומדת לרוץ.

ל־STM32N6 אין Flash פנימי שבו האפליקציה נשמרת, ולכן התמונות הקבועות נמצאות ב־NOR Flash חיצוני. לאחר reset רץ קודם BootROM שנמצא בשבב, אחריו FSBL — מנהל אתחול קטן שמכין את הזיכרון החיצוני — אחריו אפליקציית Secure שמגדירה TrustZone, ולבסוף אפליקציית Non‑Secure שבה נמצאים ThreadX, החיישן והמודל. במסלול הקבוע ה־FSBL קורא את התמונות מן ה־Flash ומכין אותן להרצה ב־SRAM. במסלול הפיתוח אפשר לטעון תמונות ישירות ל־SRAM דרך SWD; זה מהיר ובטוח יותר לניסוי, אך נעלם ב־reset.

יש גם גבול TrustZone, מנגנון חומרה שמפריד בין אזור מאובטח לבין קוד היישום הרגיל. הקוד שמריץ את המודל נמצא בעולם Non‑Secure, אבל בתחילת האתחול עולם Secure צריך להפעיל את שעוני NPU ו־CACHEAXI, לפתוח הרשאות ל־SRAM3 עד SRAM6, להגדיר את מאפייני ה־NPU כ־bus master ולהעביר את פסיקות ה־NPU לעולם Non‑Secure. לכן יש שלב bootstrap חד־פעמי דרך SWD, חיבור הדיבאג והצריבה, עבור FSBL ו־Secure. עדכון האפליקציה הרגיל אינו רשאי לשנות בשקט את גבולות האבטחה האלה.

בזמן ריצה RPS_AI_Init מעתיקה את המשקולות, מאתחלת את runtime של ST — ספריית ההרצה שמנהלת את Neural‑ART — ויוצרת context, מבנה ששומר את מצב הרשת ואת כתובות המאגרים. היא מקבלת מצביעים, כלומר כתובות, למאגר הקלט והפלט. בכל פריים משימת העיבוד ממירה את עומק ה־float למילימטרים, בונה histogram, חותכת ומגדילה ישירות אל מאגר הקלט של הרשת. אחר כך היא קוראת stai_rps_tof_run במצב סינכרוני ומחכה לסיום. במדידת החומרה האחרונה זמן ה־inference שדווח היה כשלוש מילישניות. לאחר מכן הקוד בוחר את הציון הגדול ביותר ושומר class, ‏confidence, מזהה פריים ומונה ריצות.

אותו snapshot של תוצאת ה־NPU נמסר גם למפת ה־USB וגם למשימת המסך. זה נשמע פרט קטן, אבל בלי snapshot המסך עלול לצייר את פריים 101 ולכתוב מתחתיו תוצאה מפריים 100 או 102. גם N6DF v2 מכניס את מזהה פריים העומק ואת מזהה פריים ה־NPU לאותה רשומה. כך התצוגה והבדיקה אינן מנחשות לפי זמן; הן יודעות שהתוצאה שייכת בדיוק למדידה הזאת.

שלב 09 הוא HIL, ‏Hardware in the Loop — בדיקה שבה החומרה האמיתית משתתפת. הסקריפט מפעיל את ה־NPU, קורא מאה פריימים מן החיישן, מריץ כל אחד גם ב־TFLite על המחשב ומשווה את ארבעת ציוני ה־int8 לארבעת הציונים שהגיעו מן STM32N6. הוא דורש CRC תקין, מזהי פריים מתקדמים, תוצאת NPU של אותו פריים ומונה ריצות שעולה. בבדיקה האחרונה התקבלו 100 מתוך 100 תוצאות תואמות במחלקה, כיסוי של 100%, והפרש מרבי של עשר יחידות int8 מול סף מותר של 16.

גם כאן צריך לקרוא נכון את ההצלחה. מאה הפריימים באותה בדיקת HIL סווגו כ־none הן במחשב והן בלוח. לכן הבדיקה הוכיחה שהחיישן, הפרוטוקול, ה־preprocessing, ‏TFLite ו־Neural‑ART עובדים על אותם נתונים ומגיעים לתוצאות קרובות; היא לא הוכיחה שהמודל מזהה ידיים ב־95% דיוק. מבחן מערכת ומבחן איכות מודל הם שני מבחנים שונים. הראשון עבר, והשני עדיין מציג כ־50% דיוק מאוזן על test.

עכשיו אפשר להבין את המילה אופטימיזציה בפרויקט הזה. הקוונטיזציה מקטינה מספרים ל־8 ביט; ה־CNN הקטן מגביל את מספר המשקולות וה־MACC; ה־NPU מאיץ את הליבה החישובית; DMA מזיז נתוני חיישן ותצוגה בלי לולאות byte של ה־CPU; תורים של מצביעים מונעים העתקות; מאגרים קבועים מונעים allocation; ומיפוי SRAM מציב כל סוג נתון קרוב למנוע שמשתמש בו. אף טריק בודד אינו הסיפור. ההישג הוא שכל החלקים מסכימים על צורה, בעלות, כתובת ותזמון.

זו גם הסיבה שיש שלבים ממוספרים וקובצי state. ‏00 מכין סביבה, 01 מצלם, 03 בודק, 04 מכין ומפצל, 05 מאמן, 06 מכמת, 07 מקמפל ל־Neural‑ART, ‏08 משלב את הקוד והמשקולות, 09 בודק מול הלוח, 10 טוען ל־RAM לפיתוח ו־11 בונה עדכון חתום ל־Flash. כל שלב שומר fingerprint של הקלט והתוצרים. הרצה חוזרת יכולה למחזר תוצר תואם, אבל שינוי בנתונים או בקונפיגורציה גורר את השלבים הרלוונטיים מחדש. ‏BAT הוא השלט; Python הוא מנהל התהליך; הכלים של TensorFlow ו־ST הם המכונות הכבדות.

מסלול RAM ומסלול Flash נפרדים בכוונה. בזמן פיתוח 10_LOAD_RAM בונה וטוען Secure ו־Non‑Secure ל־SRAM דרך SWD, בלי חתימה, בלי העלאת version ובלי סיכון לגרסה הקבועה; reset מוחק את הניסוי. רק אחרי HIL מסלול ה־release אמור לייצר חבילת n6fw חתומה, לשלוח אותה ב־XMODEM — פרוטוקול פשוט להעברת קובץ — אל ה־slot הלא פעיל ולהפעיל מנגנון trial ו־rollback. המשקולות נמצאות באותה תמונה חתומה, ולכן חוזה העדכון נשמר גם עבור המודל. נכון לעכשיו מסלול ה־RAM וה־HIL אומתו על החומרה, ואילו העברת חבילת ה־NPU דרך XMODEM ובדיקת rollback פיזית עדיין ממתינות לאימות.

אם צריך לזכור את כל הכתבה במשפט אחד, זה המשפט: בנינו שרשרת של חוזים. החיישן מתחייב על 54×42 מרחקים; N6DF מתחייב על מבנה, CRC ומספר פריים; preprocessing מתחייב על 64×50 ערכי uint8; model_contract מתחייב על סדר המחלקות והקוונטיזציה; STEdgeAI מתחייב על קוד, משקולות ומפת זיכרון; והקושחה מתחייבת מי הבעלים של כל buffer ומתי תוצאה תקפה. ברגע שחוזה אחד משתנה בלי האחרים, הקסם נעלם. ברגע שכל החוזים נבדקים, מה שנראה כמו דוקטורט הופך לרצף של פעולות הנדסיות שאפשר להסביר, למדוד ולשפר.

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

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

למה זה מעניין

החלק המעניין כאן אינו רק שה־STM32N6 מחזיר את המילה ROCK. בנינו שרשרת שלמה ומדידה: חיישן עומק, I3C ו־DMA, פרוטוקול עם CRC ומזהי פריים, איסוף נתונים, CNN, קוונטיזציה, קומפילציה ל־Neural‑ART, מיפוי SRAM והשוואת HIL מול TFLite. השרשרת עברה בדיקת חומרה, אבל דיוק המודל הנוכחי הוא רק כ־50% בממוצע מאוזן — ולכן ההישג האמיתי הוא שיש עכשיו בסיס אמין שאפשר לשפר באמצעות נתונים, במקום הדגמה חד־פעמית שאי אפשר להסביר או למדוד.