使用软件定义内存
本节将介绍如何在 xcore.ai 目标系统中应用软件定义内存技术。
软件定义内存指的是一个具有基地址 0x40000000 和大小为 0x40000000 字节的内存区域。这一段内存的内容完全由软件决定,因此称之为软件定义内存。
当程序尝试读取这一区域内的数据时,系统首先会在一级缓存中搜索所需的内容。如果缓存中没有找到,系统会激活一个软件填充处理程序(该程序在另一个独立的逻辑核心上运行)。此处理程序负责提供一个完整的缓存行数据以响应读取请求,并将其存放入缓存中。
对这一区域进行写操作时,数据会被直接写入缓存。
被写入的缓存行会被标记为“脏”状态,这意味着缓存中的数据版本比软件定义的后备存储中的版本更新。当需要从缓存中移除这个“脏”状态的行以便为新的数据腾出空间时,会触发一个“驱逐”软件处理程序。
这一特性通常用于实现对存储在闪存中数据的缓存,尤其是当应用程序访问数据时表现出空间和时间局部性的特征时。
一级缓存
每个 Tile 都配备有一级缓存,这与用于访问 LPDDR 的缓存是相同的。因此,不建议在同一个 Tile 上同时使用 LPDDR 和软件定义内存。
一级缓存位于 xCORE Tile 与 LPDDR 内存之间,是一个统一的指令和数据(I 和 D)缓存,采用全关联映射、写回策略,拥有 8 个缓存行,每行 32 字节,替换策略为近似 LRU(最近最少使用)。
提供了 xCORE 指令来预取、失效和刷新此缓存。
应用程序实现
应用程序可以直接管理软件定义内存,通过实现填充和驱逐软件处理程序来访问应用程序特定的后备存储数据。此外,也可以利用 XTC 工具来辅助将应用程序对象放置在闪存中。
软件定义内存区域默认在系统复位时不启用,任何访问都会引发异常。
xcore/swmem_fill.h 和 xcore/swmem_evict.h 提供了管理缓存内容的 API。
XTC 工具对闪存存储的支持
可执行代码或数据可以被存储在闪存中,随后通过软件定义内存区域被应用程序访问。代码或数据需要被标记以放置在一个特定的段中,段名需以 .SwMem 作为前缀,例如:
__attribute__((section(".SwMem_data")))
unsigned int mydata = 12345678;
如上所示,mydata 将被存储在使用 xflash 构建的闪存映像中,以便能够通过软件定义内存区域访问。
可执行代码(函数)和数据均可标注为存储在闪存中。
以下代码将触发软件填充处理程序从该区域提取 mydata 的内容,因为它尚未存在于一级缓存中:
unsigned int newdata = mydata;
一旦软件填充处理程序获取了数据并将其存入缓存,再次读取 mydata 将不会触发软件填充处理程序,除非包含 mydata 的缓存行因为有其他八个缓存行被填充而被驱逐。
编译用于软件定义内存的程序
详情请参见为外部内存(LPDDR)和软件定义内存编译。
示例
提供了两个示例;一个用于软件填充处理程序,另一个用于软件驱逐处理程序。这些示例利用 XTC 工具的支持,将标注的对象存储在闪存中。
填充处理程序示例
下面的示例展示了如何使用这项功能。示例中使用了两个逻辑核心:一个作为需要读取闪存中数据的“应用程序”,另一个作为软件填充处理程序,负责从闪存获取数据并将其缓存供应用程序使用。
填充处理程序利用 xmos_flash.h 提供的 API 从闪存读取数据,并通过 xcore/swmem_fill.h 提供的 API 将数据写入一级缓存。必须定义并初始化符号 __swmem_address 为 0xFFFFFFFF。系统启 动时会用提供进入闪存中注释应用程序数据偏移量的值覆盖它。
在本示例中,读取地址 0x50000000 会使运行在逻辑核心上的填充处理程序循环终止。
构建示例的命令如下:
$ xcc main.c main.xc -o main.xe -lquadspi -target=XCORE-AI-EXPLORER -mcmodel=large
#include <stdio.h>
#include <xcore/parallel.h>
#include <xcore/swmem_fill.h>
#include <xmos_flash.h>
__attribute__((section(".SwMem_data")))
const unsigned int my_array[20] = {
1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
};
flash_ports_t flash_ports_0 =
{
PORT_SQI_CS,
PORT_SQI_SCLK,
PORT_SQI_SIO,
XS1_CLKBLK_5
};
flash_clock_config_t flash_clock_config =
{
1,
8,
8,
1,
0,
};
flash_qe_config_t flash_qe_config_0 =
{
flash_qe_location_status_reg_0,
flash_qe_bit_6
};
flash_handle_t flash_handle;
// We must initialise this to a value such that it is not memset to zero during C runtime startup
#define SWMEM_ADDRESS_UNINITIALISED 0xffffffff
volatile unsigned int __swmem_address = SWMEM_ADDRESS_UNINITIALISED;
static unsigned int nibble_swap_word(unsigned int x)
{
return ((x & 0xf0f0f0f0) >> 4) | ((x & 0x0f0f0f0f) << 4);
}
void swmem_fill(swmem_fill_t handle, fill_slot_t address) {
swmem_fill_buffer_t buf;
unsigned int * buf_ptr = (unsigned int *) buf;
flash_read_quad(&flash_handle, (address - (void *)XS1_SWMEM_BASE + __swmem_address) >> 2, buf_ptr, SWMEM_FILL_SIZE_WORDS);
for (unsigned int i=0; i < SWMEM_FILL_SIZE_WORDS; i++)
{
buf_ptr[i] = nibble_swap_word(buf_ptr[i]);
}
swmem_fill_populate_from_buffer(handle, address, buf);
}
swmem_fill_t swmem_setup() {
flash_connect(&flash_handle, &flash_ports_0, flash_clock_config, flash_qe_config_0);
if (__swmem_address == SWMEM_ADDRESS_UNINITIALISED)
{
__swmem_address = 0;
}
return swmem_fill_get();
}
void swmem_teardown(swmem_fill_t fill_handle) {
swmem_fill_free(fill_handle);
flash_disconnect(&flash_handle);
}
static const fill_slot_t swmem_terminate_address = (void *)0x50000000;
DECLARE_JOB(swmem_handler, (swmem_fill_t))
void swmem_handler(swmem_fill_t fill_handle)
{
fill_slot_t address = 0;
while (address != swmem_terminate_address)
{
address = swmem_fill_in_address(fill_handle);
swmem_fill(fill_handle, address);
swmem_fill_populate_word_done(fill_handle, address);
}
}
DECLARE_JOB(use_swmem, (void))
void use_swmem(void)
{
volatile unsigned long a = 0;
for (int i = 0; i < 20; i++) {
printf("Result: 0x%08x\n", my_array[i]);
a = my_array[i];
}
a = *(const volatile unsigned long *)swmem_terminate_address;
}
void tile_main(void) {
swmem_fill_t fill_handle = swmem_setup();
PAR_JOBS(
PJOB(swmem_handler, (fill_handle)),
PJOB(use_swmem, ())
);
swmem_teardown(fill_handle);
}
#include <platform.h>
#include <stdio.h>
void tile_main(void);
int main(void) {
par {
on tile[0]: par {
tile_main();
}
on tile[1]: par {
}
}
return 0;
}
驱逐处理程序示例
以下示例展示了一个软件驱逐处理程序的 main.c 文件。
#include <stdio.h>
#include <xcore/parallel.h>
#include <xcore/swmem_evict.h>
#include <xcore/minicache.h>
#include <xmos_flash.h>
__attribute__((section(".SwMem_data")))
unsigned char my_array[512] = {};
DECLARE_JOB(swmem_handler, (swmem_evict_t))
void swmem_handler(swmem_evict_t evict_handle)
{
for (unsigned evictions = 0; evictions < 16; evictions += 1)
{
evict_slot_t address = swmem_evict_in_address(evict_handle);
unsigned long mask = swmem_evict_get_dirty_mask(evict_handle, address);
unsigned long buf[SWMEM_EVICT_SIZE_WORDS];
swmem_evict_to_buffer(evict_handle, address, buf);
printf("对地址 %p 的驱逐操作,脏数据掩码 %lx;数据内容:\n", address, mask);
for (unsigned i = 0; i < SWMEM_EVICT_SIZE_WORDS; i += 1)
{
printf(i == SWMEM_EVICT_SIZE_WORDS - 1 ? "%lx\n" : "%lx ", buf[i]);
}
}
}
DECLARE_JOB(use_swmem, (void))
void use_swmem(void)
{
volatile unsigned char *a = my_array;
for (unsigned i = 0; i < sizeof(my_array); i += 8)
{
*((volatile unsigned long *)(a + i)) = (unsigned long)&a[i];
a[i + 4] = i/4;
a[i + 5] = 0;
a[i + 7] = 255;
if (i % 16) { a[i + 6] = 10; }
}
// 执行 asm volatile ("flush");
minicache_flush();
}
void tile_main(void) {
swmem_evict_t evict_handle = swmem_evict_get();
PAR_JOBS(
PJOB(swmem_handler, (evict_handle)),
PJOB(use_swmem, ())
);
swmem_evict_free(evict_handle);
}
使用 xrun 和 xgdb
当应用程序通过 xflash 写入闪存时,__swmem_address 的值会是一个偏移量,该偏移量指向闪存中可以获取到标注过的应用对象的位置。
但是,通过 xrun 或 xgdb 来运行应用程序时,由于不会执行闪存的引导加载程序,__swmem_address 将保持其初始值 0xFFFFFFFF。
需要从镜像中提取原本由 xflash 写入偏移位置的数据,并将其写入到闪存的底部。如上例所示,当 __swmem_address 的值为 0xFFFFFFFF 时,它将被设置为 0。
将数据提取并写入到闪存底部的过程如下:
$ xobjdump --strip main.xe
$ xobjdump --split main.xb
$ xflash --reverse --write-all image_n0c0.swmem --target XCORE-AI-EXPLORER
为了匹配 xflash 存储完整应用程序在闪存中的格式,数据的 nibble 必须进行交换。在此示例中,交换是通过 xflash 的 --reverse 选项完成的。