▼點(diǎn)擊下方名片,關(guān)注公眾號(hào),獲取更多精彩內(nèi)容▼
這里是《賀老師講嵌入式AI》,我是《嵌入式AI:讓單片機(jī)學(xué)會(huì)思考》課程主理人,專注AI在MCU上的落地實(shí)踐。
關(guān)注公眾號(hào)后,回復(fù)【嵌入式AI】,可以免費(fèi)獲取更多賀老師準(zhǔn)備的專業(yè)資料。
把 STM32N647 的官方 NPU 工程導(dǎo)入 CubeIDE,Project Explorer 會(huì)同時(shí)出現(xiàn) FSBL、App_Secure、App_NonSecure、atonn 和 LL_ATON。第一次接觸這套工程,最容易卡住的地方并非 C 語(yǔ)法,而是入口分散:?jiǎn)?dòng)代碼在三個(gè) main.c,模型描述在生成文件里,真正的 NPU 調(diào)度又進(jìn)入了中間件。
這篇文章只完成一件事:沿著一次推理的執(zhí)行路徑,把每一類數(shù)據(jù)和每一個(gè)關(guān)鍵入口定位到文件。讀完以后,你應(yīng)該能夠指出模型權(quán)重、激活緩沖、輸入輸出張量和 NPU 運(yùn)行入口分別位于哪里。
先看工程樹。AI_SNS 是一個(gè)工程組,內(nèi)部包含 AI_SNS_FSBL、AI_SNS_AppliSecure 和 AI_SNS_AppliNonSecure。NonSecure 工程中還能看到 AI、atonn、Src 和鏈接腳本。后面的模型調(diào)用都從這里展開。

圖 1:真實(shí) CubeIDE 工程樹,NonSecure 工程已展開

圖 2:FSBL、Secure、NonSecure 與生成文件的職責(zé)邊界
FSBL/Src/main.c 先完成 MPU、供電和系統(tǒng)時(shí)鐘配置。這個(gè)工程將 CPU 和 NPU 配置到 800 MHz,AXI 配置到 400 MHz,隨后調(diào)用 MX_XSPI2_Init()、MX_EXTMEM_Init() 和 BOOT_Application()。
BOOT_Application() 位于 stm32_boot_xip.c。它先調(diào)用 MapMemory(),把啟動(dòng)存儲(chǔ)器切換到 Memory-Mapped 模式;然后從映射地址讀取向量表,設(shè)置 VTOR、MSP 和復(fù)位入口,最終跳入 Secure 鏡像。FSBL 沒有參與模型推理,它負(fù)責(zé)把后續(xù)程序需要的外部 Flash 訪問(wèn)鏈路準(zhǔn)備好。
App_Secure/Src/main.c 依次執(zhí)行 system_init_post()、init_external_memories()、NPU_Config() 和 SystemIsolation_Config()。NPU_Config() 打開 NPU 與 CACHEAXI 時(shí)鐘并釋放復(fù)位,然后通過(guò) RIMC 和 RISC 將 NPU 配置為特權(quán) NonSecure 主設(shè)備。SystemIsolation_Config() 繼續(xù)處理 NPU RAM、XSPI2、CACHEAXI 配置端口和 GPIO 的 Secure/NonSecure 屬性。
完成隔離配置后,Secure 代碼把 SCB_NS->VTOR 指向 0x70180400,從 NonSecure 向量表取出 MSP_NS 和 Reset Handler,再調(diào)用 NonSecure_ResetHandler()。NPU 推理代碼因此可以放在 NonSecure 工程中運(yùn)行,同時(shí)保留 Secure 側(cè)對(duì)啟動(dòng)和資源邊界的控制。
進(jìn)入 App_NonSecure/Src/main.c 后,代碼打開 I-Cache 和 D-Cache,調(diào)用 npu_cache_init() 與 LL_ATON_RT_RuntimeInit()。宏 LL_ATON_DECLARE_NAMED_NN_INSTANCE_AND_INTERFACE(Default) 創(chuàng)建兩個(gè)關(guān)鍵對(duì)象:NN_Instance_Default 保存運(yùn)行狀態(tài),NN_Interface_Default 提供輸入、輸出和內(nèi)部 Buffer Info 查詢接口。

圖 3:真實(shí)工程中的張量綁定、網(wǎng)絡(luò)初始化和 RunEpochBlock 調(diào)用
NN_Interface_Default.input_buffers_info() 返回輸入描述符,output_buffers_info() 返回輸出描述符。LL_Buffer_addr_start() 再把“基地址 + 偏移”轉(zhuǎn)換為應(yīng)用可以訪問(wèn)的指針。這個(gè)示例只有一個(gè)輸入和一個(gè)輸出,因此代碼直接使用數(shù)組第 0 項(xiàng)。
調(diào)用 LL_ATON_RT_Init_Network() 后,網(wǎng)絡(luò)實(shí)例的 current_epoch_block 指向 network.c 生成的 Epoch Block 數(shù)組。主循環(huán)隨后反復(fù)調(diào)用 LL_ATON_RT_RunEpochBlock()。一次函數(shù)調(diào)用只負(fù)責(zé)檢查當(dāng)前 Epoch、啟動(dòng)或收尾當(dāng)前 Epoch,并決定下一步動(dòng)作。

圖 4:主循環(huán)根據(jù) NO_WFE、WFE、DONE 三種返回值推進(jìn)推理
返回 LL_ATON_RT_NO_WFE 時(shí),主循環(huán)立即進(jìn)入下一次調(diào)度;返回 LL_ATON_RT_WFE 時(shí),代碼調(diào)用 LL_ATON_OSAL_WFE() 等待 NPU 或流引擎事件;返回 LL_ATON_RT_DONE 時(shí),所有 Epoch 已完成。生成文件中的 Epoch 2 至 Epoch 5 標(biāo)記為硬件執(zhí)行,Epoch 6 標(biāo)記為 pure_sw,最后的整數(shù) Softmax 由軟件函數(shù)完成。
輸出張量存放 10 個(gè) INT8 數(shù)值。第 i 個(gè)結(jié)果的反量化公式為 (q[i] + 128) × 0.00390625。當(dāng)前示例將 _pre_process(buffer_in) 與 _post_process(buffer_out) 注釋掉了。實(shí)際應(yīng)用需要在前者中寫入符合量化參數(shù)的 28×28 輸入,在后者中完成反量化、最大值搜索和類別映射。
App_NonSecure/atonn/network.c 是定位模型數(shù)據(jù)的核心文件。它由 ST Edge AI Core 自動(dòng)生成,文件頭記錄了 --cache-maintenance=true 與 --Ocache-opt=true。文件內(nèi)部包含算子配置、Epoch Block、張量描述符和緩存維護(hù)范圍。這個(gè)文件適合閱讀和打斷點(diǎn),重新生成模型時(shí)應(yīng)由工具覆蓋,手工修改會(huì)造成生成結(jié)果與模型版本脫節(jié)。
Conv2D_3_weights 和 Gemm_10_weights_transposed_3 的 addr_base 都是 0x71000000,并且 is_param=1。生成代碼只保存參數(shù)地址和偏移,NPU 在推理時(shí)從 XSPI Flash 讀取系數(shù)。本地參考包的 atonn 目錄只有 ATON.h 與 network.c,沒有單獨(dú)的 *_atonbuf*.raw 權(quán)重鏡像。使用完整導(dǎo)出包復(fù)現(xiàn)時(shí),需要把權(quán)重鏡像寫入生成代碼指定的外部 Flash 地址。缺少這一步,調(diào)用鏈可以執(zhí)行,模型計(jì)算無(wú)法得到可信結(jié)果。
輸入、輸出和多個(gè)中間張量共享 0x242E0000 這塊 NPU RAM,通過(guò)不同偏移和生命周期復(fù)用內(nèi)存。輸入占用偏移 0 至 783,輸出占用偏移 528 至 537,兩者地址區(qū)間發(fā)生重疊。這個(gè)安排由編譯器根據(jù)張量生命周期完成;輸出生成時(shí),前面占用同一地址的輸入數(shù)據(jù)已經(jīng)失去后續(xù)用途。
App_NonSecure/Src/npu_cache.c 中的 npu_cache_init() 初始化 CACHEAXI HAL 句柄。具體的 Clean 和 Invalidate 范圍由生成網(wǎng)絡(luò)決定。例如本工程會(huì)對(duì) 0x242E0200 附近執(zhí)行失效操作,并對(duì) 0x242E0000 起始的一段激活區(qū)執(zhí)行 Clean。CPU 填好輸入以后,NPU 需要看到最新數(shù)據(jù);NPU 寫完輸出以后,CPU 也需要讀取最新結(jié)果。生成器開啟緩存維護(hù)后,這些范圍會(huì)進(jìn)入 Epoch 的開始或結(jié)束邏輯。

圖 5:NonSecure 工程的真實(shí)鏈接腳本,RAM 從 0x24100000 開始,ROM 從 0x70180400 開始
NonSecure 鏈接腳本確定程序代碼與普通 RAM 的落點(diǎn);partition_stm32n657xx.h 和 SystemIsolation_Config() 決定對(duì)應(yīng)地址能否被 NonSecure CPU 與 NPU 主設(shè)備訪問(wèn)。遇到 SecureFault、總線錯(cuò)誤或 NPU 讀不到外部權(quán)重時(shí),需要同時(shí)檢查鏈接地址、SAU 區(qū)域、RISAF 區(qū)域和 NPU Master 屬性。只檢查 .ld 文件無(wú)法覆蓋訪問(wèn)權(quán)限問(wèn)題。
先使用 Debug 配置構(gòu)建三個(gè)工程,再在 NonSecure 工程設(shè)置五個(gè)斷點(diǎn):main.c:129 檢查描述符,main.c:139 檢查網(wǎng)絡(luò)實(shí)例初始化,main.c:142 檢查 Epoch 調(diào)度,ll_aton_runtime.c:583 進(jìn)入 Runtime,main.c:150 確認(rèn)一次推理結(jié)束。
在 Expressions 視圖加入 buffer_in、buffer_out、ibuffersInfos[0]、obuffersInfos[0]、NN_Instance_Default.exec_state.inference_started、NN_Instance_Default.exec_state.current_epoch_block 和 ll_aton_rt_ret。地址、張量名稱和偏移必須與 network.c 一致。

圖 6:按真實(shí)生成文件配置的 Variables、Expressions 與 Call Stack 核對(duì)圖
調(diào)試注意:LL_ATON_RT_RunEpochBlock() 和 main() 兩層調(diào)用關(guān)系應(yīng)當(dāng)存在;更深層的 __LL_ATON_RT_ExecStartEpochBlock() 可能因優(yōu)化等級(jí)而折疊。 |
完成以下四項(xiàng)即可通過(guò)本文驗(yàn)收:在 network.c 指出權(quán)重基址 0x71000000;在 Buffer Info 中指出輸入 0x242E0000 和輸出 0x242E0210;在同一文件指出中間激活的地址與復(fù)用范圍;在 ll_aton_runtime.c 指出 LL_ATON_RT_RunEpochBlock() 是 NPU 運(yùn)行入口,并能從調(diào)用棧返回 App_NonSecure/Src/main.c。
| 最終結(jié)論 |
工程依據(jù):本地 STM32N6570-DK SNS NPU 參考工程的 FSBL、App_Secure、App_NonSecure、network.c、LL_ATON Runtime、鏈接腳本與 Debug Map 文件。

END