4. 存储器映射与地址空间

在 PolarFire SoC 上做软硬件协同开发,第一件必须建立起来的“共同语言”就是存储器映射。CPU 发出的每一个取指、取数和外设访问,最终都要落到一个物理地址上;而 FPGA fabric 里的自定义逻辑、DDR 中的大块数据、eNVM 中的启动代码,也都要通过地址被 CPU 看见。如果地址空间理解错了,现象往往不是“报错”,而是读到全 0、全 F、总线挂死或数据错位,排查成本很高。因此本节先梳理 PolarFire SoC 的地址空间结构,再给出一个可实际运行的地址访问验证程序,用“读—写—回读—比对”的方式确认各区域是否按预期可达。

一、PolarFire SoC 地址空间总览

PolarFire SoC 采用 64 位地址空间,但实际使用中主要落在低 4GB 范围内。其核心特点是:CPU 集群(E51 监控核 + 4 个 U54 应用核)通过 AXI 交换网络访问 DDR、L2 cache、eNVM、sNVM 以及各类外设;而 FPGA fabric 中的逻辑则通过 FIC(Fabric Interface Controller)与 SoC 总线互连。理解地址映射,本质上是理解“谁在总线的哪一端、通过哪个接口、映射到哪个地址窗口”。

几个关键区域的作用如下:

  • DDR:外部大容量内存,通常映射在 0x8000_0000 起始(具体以 Libero 生成的 MSS 配置和 HSS 设备树为准),用于运行 Linux、存放应用数据和大缓冲区。
  • L2 cache:CPU 集群共享的二级缓存,可配置为 cache 或紧耦合的 scratchpad(LIM)。作为 scratchpad 时,它有一段独立地址窗口,访问延迟远低于 DDR,适合放中断向量、关键代码或核间共享数据。
  • eNVM:非易失性存储器,存放启动镜像(HSS、U-Boot、FPGA 位流等),通常映射在 0x2022_0000 附近,容量约 128KB 级别,是只读或受保护写入的启动介质。
  • sNVM:安全非易失性存储器,用于存放密钥、配置参数、设备序列号等,容量小(KB 级),访问需通过特定控制器,不是普通内存式读写。
  • FIC:Fabric Interface Controller,是 CPU 侧总线与 FPGA fabric 之间的桥。CPU 通过 FIC 的 AXI 从端口访问 fabric 内实例化的 IP;fabric 内的主设备也可通过 FIC 反向访问 DDR 或 L2。
  • AXI/AHB 总线:SoC 内部高速互连以 AXI 为主,低速外设(UART、SPI、GPIO、I2C 等)多挂在 AHB/APB 上,地址窗口彼此独立。

需要特别强调的是:FPGA fabric 的地址分配不是固定的。在 Libero 中为 fabric 内的 AXI 从设备分配基地址时,该地址必须落在 FIC 的地址窗口内,并与 MSS 配置、设备树保持一致。如果 fabric IP 的基地址与 DDR 或外设窗口重叠,就会出现访问冲突。

PolarFire SoC 地址空间与访问路径示意 CPU 集群 E51 监控核 U54 × 4 应用核 取指 / 取数 / 外设访问 L2 Cache 可配置为 Cache 或 Scratchpad(LIM) 低延迟共享区 AXI 互连 高速主总线 AHB/APB 低速外设 地址译码与路由 FIC Fabric Interface Controller CPU ↔ Fabric 桥 DDR 0x8000_0000 起 大容量数据 / Linux eNVM 0x2022_0000 附近 启动镜像 / 位流 sNVM 安全非易失存储 密钥 / 配置参数 FPGA Fabric 地址窗口 Libero 中为 AXI 从设备分配基地址,须落在 FIC 窗口内 自定义 IP / 加速器 / 寄存器组

二、地址访问验证程序的设计思路

验证程序的目标不是“跑通一个功能”,而是回答三个问题:该地址是否可访问?写入后能否正确回读?不同区域的行为是否符合其介质特性?为此,程序采用“模式写入 + 回读比对”的方法,并针对不同区域设置不同的测试策略。

测试模式选择上,常用 0x55AA55AA、0xAA55AA55、0x00000000、0xFFFFFFFF 以及地址相关模式(如写入地址本身)。地址相关模式能有效发现地址线粘连或译码错误:如果写入 addr 却读回另一个值,说明地址译码或数据通路存在问题。

需要区分三类区域:

  1. 可读写 RAM 类(DDR、L2 scratchpad):可执行完整写入—回读—比对,并做多地址遍历。
  2. 只读或受保护类(eNVM):只做读取验证,确认能读到非全 0/全 F 的合理数据,不执行破坏性写入。
  3. 控制器访问类(sNVM):通过专用寄存器/驱动接口读取,不按普通内存指针直接解引用。
直接对未知地址执行写入可能触发总线错误甚至挂死。验证程序应先读后写,对只读区域只读不写;对 fabric 地址,务必确认 Libero 中已分配且设备树一致后再写入。

三、验证程序示例

下面给出一个基于裸机/HSS 环境的 C 语言验证片段。地址为示例值,实际使用时请替换为当前工程的 MSS 配置与设备树中的真实地址。

#include <stdint.h>
#include <stdio.h>

#define DDR_BASE        0x80000000UL
#define L2_SCRATCH_BASE 0x08000000UL   /* 示例:L2 LIM 窗口 */
#define ENVM_BASE       0x20220000UL   /* 示例:eNVM 窗口 */
#define FABRIC_BASE     0x60000000UL   /* 示例:FIC 窗口内 fabric IP */

static const uint32_t patterns[] = {
    0x55AA55AA, 0xAA55AA55, 0x00000000, 0xFFFFFFFF
};

/* 对可读写区域做写入-回读比对 */
int verify_rw(const char *name, volatile uint32_t *base, uint32_t words)
{
    int errors = 0;
    for (uint32_t i = 0; i < words; i++) {
        for (int p = 0; p < 4; p++) {
            uint32_t addr_val = (uint32_t)(base + i);
            uint32_t w = patterns[p];
            base[i] = w;
            __sync_synchronize();          /* 内存屏障,确保写入完成 */
            uint32_t r = base[i];
            if (r != w) {
                printf("[FAIL] %s off=0x%08X wr=0x%08X rd=0x%08X\n",
                       name, i * 4, w, r);
                errors++;
            }
        }
        /* 地址相关模式 */
        base[i] = addr_val;
        __sync_synchronize();
        if (base[i] != addr_val) {
            printf("[FAIL] %s addr-pattern off=0x%08X\n", name, i * 4);
            errors++;
        }
    }
    printf("[%s] %s, tested %u words\n",
           errors ? "FAIL" : "PASS", name, words);
    return errors;
}

/* 只读区域:确认可读且非全 0/全 F */
int verify_ro(const char *name, volatile uint32_t *base, uint32_t words)
{
    uint32_t all0 = 1, allf = 1;
    for (uint32_t i = 0; i < words; i++) {
        uint32_t r = base[i];
        if (r != 0x00000000) all0 = 0;
        if (r != 0xFFFFFFFF) allf = 0;
    }
    if (all0 || allf) {
        printf("[FAIL] %s readback all %s\n", name, all0 ? "0" : "F");
        return 1;
    }
    printf("[PASS] %s readable, not stuck\n", name);
    return 0;
}

int main(void)
{
    int err = 0;
    err += verify_rw("DDR",    (volatile uint32_t *)DDR_BASE,        64);
    err += verify_rw("L2_LIM", (volatile uint32_t *)L2_SCRATCH_BASE, 64);
    err += verify_ro("eNVM",   (volatile uint32_t *)ENVM_BASE,       16);
    err += verify_rw("FABRIC", (volatile uint32_t *)FABRIC_BASE,     16);
    printf("total errors = %d\n", err);
    return err;
}

程序中的 __sync_synchronize() 用于保证写入在回读前真正到达目标,避免编译器优化或写缓冲导致“假通过”。对 DDR 和 L2 各测 64 个字,对 fabric 测 16 个字,是因为 fabric 寄存器组通常较小,且部分寄存器可能有副作用,测试范围不宜过大。

四、结果判读与常见问题

运行后应关注三类现象:

  • 全部 PASS:说明地址译码、数据通路、时序均正常,可继续做功能开发。
  • 某区域全 0 或全 F:通常表示该地址窗口未使能、时钟未开、或 fabric IP 未正确例化。例如 FIC 窗口读回全 0,应检查 Libero 中 FIC 是否使能、fabric IP 基地址是否与代码一致。
  • 地址相关模式失败:说明地址线存在粘连或译码错误,需核对 MSS 地址分配与设备树。

此外,若访问 DDR 时出现数据错位但模式测试通过,应怀疑 cache 一致性问题:DDR 区域可能被 cache 缓存,写入后未回写。此时可在测试前后执行 cache flush/invalidate,或将测试区域配置为非缓存属性。对 L2 scratchpad,则要注意其作为 LIM 时地址窗口与 cache 模式不同,不能混用。

建议把上述验证程序作为上电自检的一部分,在 HSS 阶段或应用启动早期运行。一旦地址映射被修改(例如重新生成 Libero 工程、调整 fabric 地址),重新执行一次即可快速确认软硬件地址约定是否仍然一致。

掌握地址映射并具备一套可复用的验证程序,是后续在 PolarFire SoC 上做 fabric 加速器、DMA 搬运和核间通信的基础。地址对了,数据通路才可信;数据通路可信,协同开发才有稳定的起点。