DSP 内存
DSP 内存架构
HiFi 5 DSP 内存分为片内和片外两个区域。
DSP 片内内存 (DSP 独占)
ICache
DCache
DTCM
DSP 片外内存 (与 KM4, KR4 共享)
SRAM
PSRAM
这些内存在访问速度和容量上呈现显著差异。
在访问性能方面:
DTCM 和 DCache 具有最优的实时性,与 DSP 同频运行,可实现单周期数据获取;
SRAM 以 240MHz 频率和 64-bit 位宽提供次级性能;
PSRAM 虽标称 250MHz 频率,但因采用 16-bit 物理位宽(8-bit DDR),实际带宽最低。
在容量配置上呈现反向特性:
PSRAM 提供最大 16MB 可扩展空间(具体容量依芯片型号而定)。DSP 默认运行在 PSRAM,占用 KM4、KR4 之后剩余的 PSRAM 空间,可获得较大的堆;
SRAM 总容量 512KB,由 KM4、KR4 与 DSP 共享。默认布局下 DSP 仅将高频访问的代码/数据放入 SRAM(
sram_dsp段)以加速。各段大小由 MCU 侧布局配置决定(详见后文 DSP 内存布局 章节);DTCM 和 DCache 作为专用高速存储,容量最小但延迟最低。DTCM 分为 DRAM0 / DRAM1 两块物理独立的 128KB。
DSP 内存访问速度
源 |
目标 |
内存访问速度 (MB/s) |
搬运方式 |
|---|---|---|---|
SRAM |
DTCM |
1899 |
iDMA |
SRAM |
DCache |
1791 |
memcpy |
PSRAM |
DTCM |
430 |
iDMA |
PSRAM |
DCache |
425 |
memcpy |
备注
实验条件: DSP 500MHz, SRAM 240MHz, PSRAM 250MHz。
不同的实验条件下,内存访问速度可能不同。表格中的数据为测得的上限值。
DSP 内存访问方式
由于内存的访问速度会影响 DSP 的运算性能,甚至成为瓶颈。因此,运行在 DSP 上的算法应尽可能优先使用 DTCM, SRAM, 再使用 PSRAM。
把数据放进快速内存有两种途径:
链接期静态放置 :用 section 属性把变量或函数固定到某块内存(如
.dram0.data、.sram_dsp.data),地址在链接时确定,适合常驻数据。详见后文 DSP 内存布局 。运行期动态分配 :用类型化堆 API 从指定内存申请 buffer,适合大小或数量在运行时才确定的场景。详见后文 从指定内存动态分配 。
实际应用时,可根据算法模型大小,选择不同的数据存放位置。例如:
若模型小于 256KB,在程序启动后,可预先将数据全部加载到 DTCM 中并常驻。
若模型很大,则可以在 PSRAM 和 DTCM 之间搬运数据。
有两种方式可以主动将数据从 PSRAM 搬运到 DTCM:
memcpy
iDMA
和 memcpy 相比, iDMA 的优势是可以释放 CPU 算力,在 iDMA 传输过程中 DSP 可以继续执行其他任务。然而, 在访问 PSRAM 的速度上,iDMA 并不具有明显优势。
搬运大数据块(64KB/128KB)时,iDMA 稍快。
搬运小数据块(8KB/16KB/32KB)时, 反而 memcpy 更快。
从指定内存动态分配
DSP 的默认堆行为不变: malloc / calloc / realloc / free 仍然从默认堆分配。默认堆位于 PSRAM(无 PSRAM 的芯片上位于 SRAM 尾部)。
在此之上提供一组类型化堆 API,用于 从指定的那一块内存 分配。头文件 bsp/include/dsp_heap_types.h 。
typedef enum {
TYPE_DTCM0 = 0, /* DRAM0,DSP 独占,non-cacheable */
TYPE_DTCM1, /* DRAM1,DSP 独占,non-cacheable */
TYPE_SRAM /* SRAM,cacheable writeback */
} MALLOC_TYPES;
void *rtos_heap_types_malloc (uint32_t size, MALLOC_TYPES type);
void *rtos_heap_types_zmalloc(uint32_t size, MALLOC_TYPES type); /* 清零 */
void *rtos_heap_types_calloc (uint32_t num, uint32_t size, MALLOC_TYPES type);
void *rtos_heap_types_realloc(void *pbuf, uint32_t size, MALLOC_TYPES type);
void rtos_heap_types_free (void *pbuf); /* 不带 type */
使用示例:把滤波器的 scratch buffer 放到 DTCM0,失败则退回默认堆。
#include "dsp_heap_types.h"
int16_t *buf = rtos_heap_types_malloc(4096, TYPE_DTCM0);
if (buf == NULL) {
buf = malloc(4096); /* 由调用方决定是否退让,API 不会自动退让 */
}
/* ... */
rtos_heap_types_free(buf); /* 两种来源都可以用它释放 */
使用时需注意以下几点:
分配失败不会退到别的内存,直接返回 NULL 。调用方要 DTCM 是为了 DTCM 的延迟,静默给回 PSRAM 会让性能问题变得难以发现。是否退让由调用方自己决定。
释放不需要传 type 。归属由地址反查得出,默认堆的指针(含普通
malloc得到的)传给rtos_heap_types_free也能正确释放。返回的 buffer 恒为 16 字节对齐 ,因此可以直接对它做 HiFi5 宽向量访问。
仅限任务上下文 ,与
malloc/free的限制相同,不可在中断中调用。cache 由调用方负责 。DTCM 是 non-cacheable;SRAM 与 PSRAM 是 writeback。若 buffer 要与 KM4/KR4 或 DMA 共享,需自行调用
xthal_dcache_region_writeback_inv()。另有
rtos_heap_types_get_free_size()、rtos_heap_types_get_largest_free_block()、rtos_heap_types_get_min_ever_free_size()等查询接口,可用于度量内存预算;dsp_heap_types_dump()打印各块内存的当前状态。
各内存池的范围
池的边界取自 LSP 的 segment 符号( _memmap_seg_<seg>_end .. _memmap_seg_<seg>_max ),也就是该 segment 未被 .data / .bss 占用的那段尾部。因此无论 LSP 怎样重新生成,池都不会与静态放置的数据重叠;反过来,静态放到 DTCM 的数据越多,可动态分配的 DTCM 就越少。
类型 |
来源 segment |
说明 |
|---|---|---|
|
|
最快,DSP 独占,non-cacheable |
|
|
同上,物理上是另一块 128KB |
|
|
次快,与 KM4/KR4 共享总线;行为随 LSP 变化,见下 |
备注
TYPE_SRAM 的行为取决于当前 LSP:
RTK_LSP/RTK_LSP_XIP(默认堆在 PSRAM):从独立的 SRAM 池分配,与默认堆互不影响。该池位于 SRAM 高地址的 256KB(0x20040040起至SRAM_END);低地址部分留给链接期的.sram_dsp.*段使用。RTK_LSP_SRAM(无 PSRAM 的芯片,默认堆本身就在 SRAM):此时再开一个 SRAM 池就是自我重叠,因此TYPE_SRAM直接转发到默认堆,与malloc走同一条路径。可用dsp_heap_types_sram_is_alias()在运行时判断当前处于哪一种。
两种情况下 TYPE_SRAM 拿到的都是 SRAM,区别在于它是否与默认堆共用空间。 转发模式下需额外注意 :默认堆耗尽会触发 assert 而不是返回 NULL,因此这种模式下 TYPE_SRAM 不再有“失败返回 NULL”的保证。
iDMA 双缓冲区数据搬运样例
iDMA 的使用方式详见 Xtensa 文档。
本样例演示使用双缓冲区将数据从 PSRAM 搬运到 DTCM,边搬边算实现加速。
伪代码
1#define ALIGN(x) __attribute__((aligned(x)))
2#define DRAM0 __attribute__((section(".dram0.data")))
3#define DRAM1 __attribute__((section(".dram1.data")))
4
5int8_t ALIGN(16) DRAM0 dst_ping[USER_BUFFER_SIZE];
6int8_t ALIGN(16) DRAM1 dst_pong[USER_BUFFER_SIZE];
7
8#define NUM_DESCRIPTORS 2
9IDMA_BUFFER_DEFINE(dmaBuffer, NUM_DESCRIPTORS, IDMA_1D_DESC);
10
11void idma_pingpong_buffers_example(void) {
12 idma_init(0, MAX_BLOCK_16, 16, TICK_CYCLES_1, 0, NULL);
13 idma_init_loop(dmaBuffer, IDMA_1D_DESC, NUM_DESCRIPTORS, NULL, NULL);
14
15 // prepare the first data
16 idma_copy_desc(dst_ping, ...);
17
18 // wait for the first idma finish
19 while (idma_buffer_status() > 0) {}
20
21 // prepare the second data
22 idma_copy_desc(dst_pong, src, size, 0);
23
24 // do the first process
25 user_process_1(dst_ping,....)
26
27 // wait for the second idma finish
28 while (idma_buffer_status() > 0) {}
29
30 // prepare the third data
31 idma_copy_desc(dst_ping, ...);
32
33 // do the second process
34 user_process_2(dst_pong,....)
35
36 // wait for the third idma finish
37 while (idma_buffer_status() > 0) {}
38 // prepare the fourth data
39 idma_copy_desc(dst_pong, src, size, 0);
40 // do the third process
41 user_process_3(dst_ping,....)
42 // wait for the fourth idma finish
43 while (idma_buffer_status() > 0) {}
44 // prepare the fifth data
45 idma_copy_desc(dst_ping, ...);
46 // do the fourth process
47 user_process_4(dst_pong,....)
48 ......
49}
代码包含以下几个部分:
定义 iDMA 缓冲区
#define NUM_DESCRIPTORS 2 IDMA_BUFFER_DEFINE(dmaBuffer, NUM_DESCRIPTORS, IDMA_1D_DESC);
初始化 iDMA
idma_init(0, MAX_BLOCK_16, 16, TICK_CYCLES_1, 0, NULL); idma_init_loop(dmaBuffer, IDMA_1D_DESC, NUM_DESCRIPTORS, NULL, NULL);
定义两块位于 DTCM 上的数据缓冲区
#define ALIGN(x) __attribute__((aligned(x))) #define DRAM0 __attribute__((section(".dram0.data"))) #define DRAM1 __attribute__((section(".dram1.data"))) int8_t ALIGN(16) DRAM0 dst_ping[USER_BUFFER_SIZE]; int8_t ALIGN(16) DRAM1 dst_pong[USER_BUFFER_SIZE];
第 N 次搬运,更新描述符并调度
idma_copy_desc(dst_X, src, size, 0); while (idma_buffer_status() > 0) {} user_process_N(dst_X,....)
在伪代码中,为了方便理解,将顺序执行的代码分为两列:
左侧是 1,3,5,... 奇数次搬运和运算,使用 ping 数据缓冲区。
右侧是 2,4,6,... 偶数次搬运和计算,使用 pong 数据缓冲区。
第 N 次的搬运和计算与第 N 以及第 N+1 次交杂在一起。搬运第 N 份数据的同时计算第 N-1 份数据,从而达到边搬运边计算的目的。
备注
用到 iDMA 时,需要在 DTCM 中放置少量的 iDMA 描述符数据,约几百字节,因此用户可使用的 DTCM 空间略小于 256KB。
DSP 内存布局
警告
改完任何影响内存布局的 Kconfig,都必须用 python 脚本重新生成 DSP 的 LSP 并重新编译 DSP。
不限于 CONFIG_DSP_* :IMG1 的位置( CONFIG_IMG1_SRAM / CONFIG_IMG1_FLASH )、
TrustZone( CONFIG_TRUSTZONE 、 CONFIG_IMG3_SRAM 、 CONFIG_TZ_S_SIZE )、
CONFIG_DATA_HEAP_SRAM / CONFIG_DATA_HEAP_PSRAM 、 CONFIG_SRAM_END
都会移动 DSP 段的基址。沿用过期的固件 DSP 会链接到错误的基址而异常或者内存被浪费。
建议使用 ameba-dsp-development Agent Skill 来辅助调整和检查 LSP 问题。
DSP 默认布局
DSP 工程是通过 Linker Support Package (LSP) 来描述内存布局。LSP 指定了用于生成可执行文件的目标文件及其内存分布,并为特定目标环境的链接器提供配置便利。 详见 Xtensa 文档。
默认布局示意图如下:
在 Xplorer 中看到的 LSP 示例如下:
DSP 可使用 sram_dsp、entry_table 和 extra_reset_mem 作为系统内存,DRAM0/1 作为本地数据内存。Reset vector 存放于 entry_table。DRAM0/1 仅能存储数据。
Call0 ABI:可以在 sram_dsp 和 extra_reset_mem 放置代码和数据。
Window ABI:代码只能放在 extra_reset_mem,数据可放在 sram_dsp 和 extra_reset_mem。
将代码/数据放到 SRAM
SRAM 的带宽和延迟显著优于 PSRAM。在默认 LSP(RTK_LSP)中包含一个名为 sram_dsp 的段,将代码或数据放入 SRAM 有助于提高计算速度。
如下图所示,RTK_LSP 默认在 SRAM 中定义了三个段内存区间: .sram_dsp.text 、 .sram_dsp.data 和 .sram_dsp.literal 。
若需要将单个函数放到 SRAM,可这样声明和定义:
extern void place_into_sram()__attribute__ ((section(".sram_dsp.text"))); void place_into_sram(){ //detailed implentation }
若需要将数据(如数组)放到 SRAM,可这样:
__attribute__ ((section(".sram_dsp.data"))) int array_in_psram[100];
若需要将某个源文件(如
ameba_clk_rom.c)的所有函数都放到 SRAM:
调整内存布局
MCU 与 DSP 共享 PSRAM 和 SRAM,DTCM 仅供 DSP 使用。
MCU 侧的 menuconfig 配置( .config )与 ameba_layout.ld 是内存布局的唯一真值源,DSP 的 LSP 必须与其保持一致。
因此调整布局遵循固定流程:先在 MCU 侧改布局,再把 DSP 的 LSP 同步过去,最后 MCU 与 DSP 两个工程都要重新编译 。
无需手动编辑 ameba_layout.ld 中的地址宏,也无需手动运行 lsp_modify.py 。布局由 Kconfig 驱动,DSP 的 LSP 由同步脚本自动生成。
DSP 默认运行在 PSRAM。PSRAM 从低地址到高地址依次为 TrustZone → KR4 → KM4 → DSP ,DSP 段自动向上占用到 PSRAM 顶部,默认起始地址为 0x60300000 。通过调整 KM4 段大小即可改变留给 DSP 的空间:
Kconfig |
含义 |
默认 (KB) |
|---|---|---|
|
KR4(NP) 段,紧接 TZ;调整它不会移动 KM4/DSP 的基址 |
1536 |
|
KM4(AP) 段(含 TZ);调小即可给 DSP 留出更多空间 |
1536 |
DSP 段大小 = PSRAM 顶部 − (TZ + KR4 + KM4)。
操作步骤:
在 MCU 侧修改布局 :进入 MCU SDK 的 menuconfig,在
CONFIG Link Option中调整上述配置。例如把 KM4 段调小,即可给 DSP 留出更多 PSRAM。同步 DSP 的 LSP :在 MCU SDK 根目录运行同步脚本。它会读取最新的
.config与ameba_layout.ld,重新生成与之匹配的 DSP LSP(RTK_LSP),并同步 MPU table:python3 tools/scripts/dsp_layout_sync.py备注
生成 LSP 不需要预先编译整个 MCU 固件,只要
.config是最新的(改完 Kconfig 值即生效)。重新编译两个工程 :DSP 与 MCU 缺一不可。先重新编译 DSP,生成新的
dsp.bin/dsp_all.bin;再重新编译 MCU 并打包。MCU 打包时会校验 DSP bin 的加载地址是否与当前布局一致,不一致会直接报错(提示 DSP bin 已过期,需重新编译 DSP)。
备注
使用同步脚本前,需确保 Xtensa 工具链的 bin 目录已加入系统
PATH,否则找不到可执行文件。若需在 DSP 侧切换所使用的 LSP(例如无 PSRAM 的芯片改用 SRAM 版
RTK_LSP_SRAM),请在 Xplorer 中通过工程属性(Linker 设置)选择对应 LSP 。若曾修改过 MPU 的其它属性,请在同步脚本新生成的
mpu_table.c上同步修改,并确认其已加入编译工程。