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中,而不是依赖编译器默认放置。

STM32N6 总线矩阵与存储器映射示意 Cortex-M55 ICode/DCode/System NPU AXI Master DMA / GPU AXI Master AXI 互连矩阵 多主多从 / 并发访问 TCM / SRAM / XSPI PSRAM 控制器 AHB 桥 APB 桥 TCM (ITCM/DTCM) 0x0000_0000 / 0x2000_0000 OCRAM / SRAM 0x2400_0000 起 外部 PSRAM 0x9000_0000 起 外部 Flash (XSPI) 0x7000_0000 起 关键地址划分(示例,以具体型号参考手册为准) ITCM:0x0000_0000,通常 64KB,存放中断向量与高频代码 DTCM:0x2000_0000,通常 128KB,存放栈、堆与实时数据 OCRAM:0x2400_0000,约 1MB,存放常规代码与数据 外部 PSRAM:0x9000_0000,容量由外接器件决定,适合大缓冲区 外部 Flash:0x7000_0000,存放固件镜像,启动时由 BootROM 搬运 注:以上地址为教学示例,实际以 STM32N6 参考手册与数据手册为准。

三、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一致性问题。

TCM和OCRAM的容量有限,建议先用.map文件统计各段大小,再决定哪些函数和变量值得“搬”到TCM。不要盲目把所有代码都放ITCM,否则会溢出。
外部PSRAM和Flash的地址映射依赖XSPI控制器的初始化。如果在SystemInit之前访问这些区域,会触发总线错误。启动代码中必须确保XSPI配置完成后再搬运镜像。

总结来说,STM32N6的存储器映射不是一张静态表,而是与总线矩阵、启动流程、链接脚本紧密耦合的系统。理解“谁访问谁、走哪条总线、延迟多少”,才能在实际项目中做出合理的段分配决策。