2. STM32N6系统架构与存储器映射
STM32N6是ST首款集成自研NPU的Cortex-M55微控制器,其系统架构与传统STM32F4/H7系列有显著差异。理解它的总线矩阵与存储器映射,是后续编写链接脚本、分配代码与数据段、以及让NPU与CPU协同工作的前提。本节从内核、总线、存储三个层次展开,并落到链接脚本与启动流程的实际分析上。
一、Cortex-M55内核与总线接口
Cortex-M55是ARMv8.1-M架构内核,支持Helium(MVE)向量扩展和TrustZone安全扩展。它对外提供三类主要总线接口:
- ICode/DCode总线:用于从代码区取指和取常量,通常连接到TCM或通过AXI访问Flash。
- 系统总线(System Bus):用于访问SRAM、外设等数据空间。
- AHB5外设接口:面向低带宽外设访问。
在STM32N6中,M55还通过AXI接口连接到一个多主多从的互连矩阵,使CPU、NPU、DMA、GPU等主设备可以并行访问不同从设备。这一点直接决定了存储器映射的设计:不同主设备访问同一块存储时的带宽和延迟并不相同。
二、AXI/AHB/APB总线矩阵
STM32N6的总线结构大致分为三层:
- AXI层:连接CPU、NPU、DMA、外部存储控制器(XSPI/PSRAM控制器)以及内部高速SRAM。带宽最高,支持多主并发。
- AHB层:连接中速外设,如DMA2D、LTDC、SDMMC等,通过AXI-to-AHB桥接入。
- APB层:连接低速外设,如USART、I2C、SPI、TIM等,通过AHB-to-APB桥接入。
这种分层意味着:当CPU通过APB访问一个UART寄存器时,路径是M55→AXI→AHB→APB,延迟远高于访问TCM。因此,中断服务程序和高频访问的变量应尽量放在TCM或紧耦合的SRAM中,而不是依赖编译器默认放置。
三、TCM、OCRAM与外部存储的定位
TCM(紧耦合存储器)分为ITCM和DTCM,直接挂在M55内核旁,访问延迟固定且极低,不经过AXI矩阵。ITCM适合放中断向量表、启动代码和实时性要求高的函数;DTCM适合放栈、堆和频繁读写的全局变量。TCM容量有限,STM32N6上通常为ITCM 64KB、DTCM 128KB量级,必须精打细算。
OCRAM是芯片内部的通用SRAM,容量约1MB,通过AXI矩阵访问。它比TCM慢,但容量大,适合放常规代码、DMA缓冲区和NPU的中间张量。由于NPU也通过AXI访问OCRAM,把NPU输入输出放在OCRAM可以避免与CPU争抢外部总线。
外部PSRAM通过XSPI控制器接入,地址从0x9000_0000开始。它的容量可达几十MB,适合放图像帧缓冲、音频缓冲和大型神经网络权重。但访问延迟远高于OCRAM,且带宽受XSPI接口限制,因此不适合放中断服务程序和频繁访问的变量。
外部Flash从0x7000_0000开始映射,存放固件镜像。STM32N6的启动流程中,BootROM会从Flash读取头部信息,将代码搬运到TCM或OCRAM执行。这意味着链接脚本中的加载地址(LMA)和运行地址(VMA)可能不同,需要正确设置。
四、链接脚本与启动流程分析
链接脚本的核心是描述“哪些段放在哪个地址”。以GNU ld为例,一个简化的STM32N6链接脚本片段如下:
MEMORY
{
ITCM (rx) : ORIGIN = 0x00000000, LENGTH = 64K
DTCM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
OCRAM (rwx) : ORIGIN = 0x24000000, LENGTH = 1024K
PSRAM (rwx) : ORIGIN = 0x90000000, LENGTH = 8192K
}
SECTIONS
{
.isr_vector : { KEEP(*(.isr_vector)) } > ITCM
.text : { *(.text*) } > OCRAM AT> FLASH
.data : { *(.data*) } > DTCM AT> FLASH
.bss : { *(.bss*) } > DTCM
.psram_buf (NOLOAD) : { *(.psram_buf) } > PSRAM
}
这里的关键点:.text和.data的加载地址在Flash,运行地址在OCRAM或DTCM。启动时,启动代码需要把.data从Flash复制到DTCM,把.text从Flash复制到OCRAM,并清零.bss。这个过程通常由Reset_Handler中的SystemInit和__libc_init_array之间的代码完成。
启动流程大致为:上电→BootROM读取Flash头部→配置时钟与XSPI→搬运镜像到目标存储→跳转到Reset_Handler→初始化.data/.bss→调用main。如果链接脚本中把.data的LMA设错,或者启动代码没有正确复制,就会出现变量初值丢失、程序跑飞等现象。
五、基于内存布局的变量与代码段分配实践
实际工程中,我们通过段属性把关键对象放到指定存储。例如,把中断服务程序放到ITCM:
__attribute__((section(".itcm_text")))
void TIM2_IRQHandler(void) { /* 高频中断 */ }
__attribute__((section(".dtcm_data")))
volatile uint32_t g_tick_count;
__attribute__((section(".psram_buf")))
uint8_t g_frame_buffer[320*240*2];
然后在链接脚本中为这些自定义段指定地址。判断分配是否合理,可以查看编译生成的.map文件,确认各段的起始地址和大小;也可以用调试器读取对应地址,验证变量确实落在目标区域。对于NPU相关缓冲区,建议放在OCRAM并确保按缓存行对齐,避免Cache一致性问题。
.map文件统计各段大小,再决定哪些函数和变量值得“搬”到TCM。不要盲目把所有代码都放ITCM,否则会溢出。总结来说,STM32N6的存储器映射不是一张静态表,而是与总线矩阵、启动流程、链接脚本紧密耦合的系统。理解“谁访问谁、走哪条总线、延迟多少”,才能在实际项目中做出合理的段分配决策。