这一篇在干嘛?
做视频缓存、数据缓冲时,经常发现 FPGA 片内的 ERAM 只有几百 Kbit,根本不够装。安路这篇 APUG011 给了一个聪明的办法:把 EG4S 芯片里自带的 64Mb SDRAM 包一层壳,对外提供一个”长得和 ERAM 一模一样”的读写接口。你按普通 RAM 的方式读写,背后它帮你搞定 SDRAM 那些繁琐的初始化、刷新、时序。本文带你把这份参考设计从头到尾读明白。
先认识 ERAM:FPGA 里的”小仓库”
ERAM 是 FPGA 内部的嵌入式存储资源,在安路器件上通常一个大约 500Kbit 上下,不同系列会有差别。按读写口的方式,ERAM 分三种:
- 单口 RAM:只有一套地址总线,读和写共用一个口;
- 简单双口 RAM:两个地址总线,一个专门写、一个专门读,读写时钟可以独立;
- 真双口 RAM:两个地址总线,两个口都能读能写,读写时钟同样可以独立。

这三种形态决定了你平时怎么用片内 RAM。但不管哪种,容量都是硬伤——几幅高清图像就把它塞满了。容量不够时,常规思路是换更大规模的 FPGA,或者外挂 SDRAM/DDR。可外挂存储需要存储器控制器 IP 支持,对于小型器件或没有存储器硬 IP 的器件来说,开发难度、成本、产品面市时间都是挑战。
方案核心:把 SDRAM 包成”虚拟 ERAM”
APUG011 的思路很直接:利用 Anlogic EG4S 器件内部集成的 64Mb SDRAM(SIP 封装,芯片里自带,不用外挂),在它上面包一层控制器,对用户提供一套和 ERAM 操作方式一致的接口。这样用户只管像操作普通 RAM 那样发地址、发数据,完全不用碰 SDRAM 底层的读写调试。文档里把这个扩展出来的大存储简称为虚拟 ERAM。
性能指标先看一眼:
- 读写时钟可支持 200MHz;
- 32bit 位宽下,理论有效带宽 6.4Gbps,持续读或持续写时实际带宽与理论一致;读写有停顿时也能到 6Gbps 以上,利用率 95% 以上;
- 全部参数化实现,支持任意大小容量、不同位宽的 SDRAM 器件。

从框图看,整个方案中间是一个 SDRAM 控制器,向上暴露用户读写通道(写使能/写地址/写数据/掩码,读使能/读地址/读数据),向下驱动 EG4S 内置的 SDRAM。你拿到的就是上层那几个简单的 RAM 式信号。
最重要的一个限制
因为硬件机制的原因,这套方案不支持读写同时进行,读通道和写通道必须独立排斥运行——要么在写,要么在读,不能一边写一边读。

另外,方案提供两种刷新机制:一种是 IP 自刷新,用户不用参与,只需在读写前判断当前是否正在刷新;另一种由用户通过刷新标志自行控制。正常读写时你不需要操心刷新,但操作前如果用了方案自带的刷新机制,请先判断虚拟 ERAM 是否忙——如果忙,等 Sdr_init_ref_vld 信号变低后再做读写。
怎么写:写通道时序
写通道信号很简单:写使能、写地址、写数据,外加掩码信号(做字节级覆盖写)。每次写的长度没有限制,可以持续写入,甚至一次写完整片 SDRAM。唯一要注意:如果连续写入,SDRAM 时钟要工作在 64MHz 以上。

从时序图看,拉高 app_wr_en、给出地址和数据即可连续写入,和操作一块普通 RAM 没有区别。
怎么读:读通道时序
读更简单:给出读使能和读地址,然后等。拉高读使能后约 10 个时钟,控制器开始输出有效的数据输出使能(Sdr_rd_en)和有效数据(Sdr_rd_dout)。这 10 拍是 SDRAM 内部取数的固有延迟,你的下游逻辑要按这个延迟来对齐数据。

数据掩码:只想改其中几个字节怎么办
掩码的作用是”这次写操作里,哪些字节要真正写入,哪些保持原样”。如果每个时钟传输的数据宽度是 32bit,掩码就是 app_wr_dm[3:0],对应关系是:
| 掩码位 | 控制的字节 |
|---|---|
| app_wr_dm[0] | app_wr_din[7:0] |
| app_wr_dm[1] | app_wr_din[15:8] |
| app_wr_dm[2] | app_wr_din[23:16] |
| app_wr_dm[3] | app_wr_din[31:24] |
想把整个 32bit 全部屏蔽(不写入)时,把 app_wr_dm 设为 4'b1111 即可。

掩码还有个实用场景:当写入长度不是 4 的倍数时(比如写 5 个数据),第 5 个数据正常写入,第 6~8 个位置是凑数的无效数据,这时把这几个数据的 app_wr_dm[3:0] 设成 4'b1111 屏蔽掉,就不会污染存储内容。
跳跃地址读写:地址怎么跳才合法
FPGA 内的 ERAM 支持任意地址跳跃,本方案理论上也支持,但有个前提:数据是以 128bit 为一组(本方案标准位宽 32bit,即 4 个 32bit = 128bit)来组织的。所以:
app_wr_addr[1:0]必须固定为2'b00;- 任意更改的是
app_wr_addr[22:2]; - 简单理解:每 4 个时钟可以任意跳跃 1 次地址,4 个数据内部的地址只能连续累加。
时序图中的 x 代表任意地址,即组与组之间随便跳。

效率怎么拉满:突发与对齐规则
这套方案”高性能”的秘诀就在对齐规则上。每次写入或读出以 4 个数据为单位,每 4 个中第 1 个地址是首地址,第 2~4 个地址按首地址自动累加:

配合前面说的 SDRAM 200MHz、32bit 位宽,就能算出 6.4Gbps 的理论带宽。要想实际带宽不打折,记住两条:
- 每次读写的持续长度尽量是 4 的倍数;
- 长度实在不是 4 的倍数时,用掩码把凑数的无效数据屏蔽掉。
带宽心得
“持续写”或”持续读”时实际带宽等于理论带宽 6.4Gbps;写读交替且有停顿时也有 6Gbps 以上。所以尽量把应用组织成大块连续读写,而不是零散的单拍访问。
刷新:数据不丢的生命线
和所有 SDRAM 一样,数据必须定期刷新,否则会丢失。方案支持两种刷新方式,默认开启自刷新:
- 自刷新(默认):控制器自己对时钟计数,到点自动刷新。用户需要在
global_def.v里明确当前控制器时钟频率,用于计算周期——例如工作在 150MHz,时钟周期就是1000000000/150000000。一般 SDRAM 是 64ms 内对所有行全部刷新一次,最终把SELF_REFRESH_INTERVAL的计算结果代入代码即可。 - 手动刷新:把
sdr_init_ref模块里的self_refresh_open参数传 0,然后用户自行控制app_ref_req信号(至少拉高 1 个时钟周期)发起刷新,收到sdr_ref_ack表示本次刷新有效。
文档还给了刷新的判断原则:
- 数据内容在部分 SDRAM 里如 64ms 内不断被重复读写(比如图像数据),可以选用户自刷新场景,用户不用额外做刷新;
- 不刷新,数据内容就会丢失——这是硬性要求。
刷新期间别碰存储
使用方案自带的刷新机制时,操作前先看
Sdr_init_ref_vld:它为高说明正在刷新,等它变低再做读写,否则操作会和刷新冲突。
模块接口一览
顶层只提供用户读写通道信号;SDRAM 器件用的是 EG4S 内部自带的 SIP。若要换到其他 Anlogic 器件,把 SDRAM 部分的接口引出到工程顶层、分配 IO 即可。
| 信号 | 方向 | 功能说明 |
|---|---|---|
| Sdr_clk | in | 应用方案 SDRAM 工作时钟 |
| Sdr_clk_sft | in | 同频可能不同相位的 SDRAM 时钟 |
| Rst | in | 高有效 |
| 用户读写接口 | ||
| App_wr_en | in | 写使能,高有效 |
| App_wr_addr[20:0] | in | 写地址 |
| App_wr_din[31:0] | in | 写数据 |
| App_wr_dm[3:0] | in | 写入数据掩码 |
| App_rd_en | in | 读使能 |
| App_rd_addr[20:0] | in | 读地址 |
| Sdr_rd_en | out | 有效读数据输出使能 |
| Sdr_rd_dout[31:0] | out | 有效读出数据 |
| 控制信号 | ||
| Sdr_init_done | out | SDRAM 初始化完成 |
| Sdr_init_ref_vld | out | SDRAM 刷新有效标志 |
| Sdr_busy | out | SDRAM 忙状态(暂不使用) |
| App_ref_req | in | 用户自行请求刷新操作 |
| SDRAM 信号 | ||
| SDR_CLK | in | SDRAM 时钟 |
| SDR_CKE | in | SDRAM 的 CKE 时钟使能 |
| SDR_RAS | in | SDRAM 行有效 |
| SDR_CAS | in | SDRAM 片选 |
| SDR_WE | in | SDRAM 写使能 |
| SDR_BA[1:0] | in | SDRAM Bank |
| SDR_ADDR[10:0] | in | SDRAM 的操作地址 |
| SDR_DM[3:0] | in | SDRAM 的数据掩码 |
| SDR_DQ[31:0] | inout | SDRAM 数据总线 |
使用顺序记住三步:先等 Sdr_init_done(初始化完成)→ 再判断是否在刷新 → 然后才能任意长度读写。
资源占用:便宜得惊人
在 TD 软件版本 TD5.6.2_71036、EG4S-MINI-DEV3 Demo 板上验证的结果:
| 资源 | 类型 | 数值 |
|---|---|---|
| 最大频率 | c0 | 219MHz |
| 使用资源 | LUT | 447 |
| REG | 96 | |
| PLL | 1 | |
| IO | 2 | |
| BRAM | N/A | |
| DSP | N/A | |
| SERDES | N/A | |
| 适用器件 | — | Anlogic 全系器件(除 EG4S 外,其余器件需外挂 SDRAM) |
只用了 447 个 LUT 和 96 个寄存器,几乎不占资源,这就是”包一层壳”方案的好处。
调试与仿真
参考设计自带调试和仿真支持:
- 调试:工程里的
app_wrrd模块会产生递增的写入数据,读出来后再与递增数比对——数据正确则Check_OK信号为低电平,错误则为高电平。看这个信号就能快速确认读写链路是否可靠。

- 仿真:Test bench 支持 Modelsim 仿真,顶层仿真文件提供 25MHz 时钟让系统跑起来,出错会给出错误提示。指定好库后运行
sim.do文件即可一键仿真。
工程交付的关键信息:提供 Verilog/VHDL 参考设计、Test bench、IP Model,目标平台为 EG4S20-MINI-DEV3。
移植到自己的工程:10 条清单
- 上电后先等 SDRAM 初始化完成再开始工作;
- 地址跳跃和掩码严格按第二章的规则来(低 2 位补 0、按 128bit 组跳);
- 每次读写操作前判断
Sdr_busy是否忙; - 读写长度可以任意,但尽量是 4 的倍数;
- 地址可以任意跳跃,但以 4 为单位——倍数内不支持跳,倍数间可以任意跳;
- 读数据时,不同硬件使用不同的频率和相位,可能出现时钟和数据错一拍的情形,根据实际情况在应用中调整;
- 方案可支持 Anlogic 其他系列 FPGA;
- 默认使用 64Mb、32bit 宽的 SDRAM,更换器件时改
global_def.v里的全局宏:
`define DATA_WIDTH 32
`define ADDR_WIDTH 21
`define DM_WIDTH 4
`define ROW_WIDTH 11
`define BA_WIDTH 2
`define SDR_CLK_PERIOD 1000000000/150000000
`define SELF_REFRESH_INTERVAL 64000000/`SDR_CLK_PERIOD/2**`ROW_WIDTH- 确认使用哪种刷新方式,保证数据长期没有操作时不发生变化;
- SDRAM 地址位宽在
global_def文件中更改,因此必须把这个文件 include 进目标工程。
最佳实践总结
- 接口顺序:等初始化 → 避开刷新 → 检查忙 → 再读写;
- 数据组织:以 4 个数据为一组、长度取 4 的倍数、不足用掩码补齐屏蔽;
- 连续写入时 SDRAM 时钟不低于 64MHz,想跑满带宽就往 200MHz 上靠。
自测
- 为什么写 5 个数据时要把第 6~8 个数据的
app_wr_dm[3:0]设成4'b1111?不设会怎样?- 换了一颗行数不同的 SDRAM,需要改哪些宏?
SELF_REFRESH_INTERVAL是怎么算出来的?