FIFO深度怎么定?用突发流量和背压算清缓存预算
给FIFO填一个"看起来够用"的深度,最容易漏掉两件事:下游连续不读的时间,以及发出停写通知之后仍会到达的数据。本文用一个可复现的占用量模型,把突发峰值、背压尾流和实现约束分开计算。
一 先把"一个数据"说清楚
本文统一以等宽FIFO的一个存储字为单位,每拍最多写一个字、读一个字。若一个字是32位,12字对应48字节有效数据;包尾、通道号等随路信息也需要存储,但不能混入"有效载荷字节数"后直接当深度。
混合位宽时,写一次和读一次未必抵消。例如64位写、16位读,应先按位数或统一的小字单位记账,再换算为IP要求的写深度。不同频率的读写口则要放到同一时间轴,枚举时钟事件或采用有依据的到达上界、服务下界,不能直接减两个时钟域的"拍数"。
记第t拍开始时占用为qt,本拍真正接受的写入为wt,真正完成的出队为rt,则:
qt+1 = qt + wt - rt
注意这里是成功操作。空FIFO遇到读请求,没有数据可出;受背压的写请求,也不能直接记成已经入队。估算无损容量时,先用不受容量限制的模型观察所有应接收数据的峰值,再用候选深度重跑,检查是否拒写。
二 平均带宽够用 仍可能丢突发
考虑32拍观察窗口,初始为空:
- 第0至15拍每拍到达1字,之后不再到达
- 第0至7拍下游暂停
- 第8至15拍只在偶数拍读1字
- 第16拍起每拍可读1字
平均到达量只有0.5字/拍,窗口内有20次读机会。但前8拍已经堆积8字,接着8拍又净增4字,峰值达到12字。窗口后面的空闲读机会无法提前借来使用。
在本文的读写语义下,深度12无丢失,深度11丢1字。12只是这条流量轨迹的逻辑容量结果,实际IP可能要求更大或特定粒度的配置值,还要确认配置深度对应的可用容量。
如果同样的突发会重复,必须把突发间隔纳入模型,验证下一次开始前能否排空;若不能,初始占用就不再是零。一次突发通过,不等于持续业务通过。
三 停写通知之后 还会来多少字
假设占用到8字时触发停写,而从触发之后算起,数据通路还会送来3字;这期间不能保证下游读取。那么至少要留3字空位,即总容量至少11字。
一般地,若触发瞬间实际占用最多为T+δ,触发之后尚未计入占用、且无法撤回的数据最多为I字,最保守的条件是:
D >= T + δ + I
δ用于覆盖阈值越过或检测延迟造成的额外占用;I覆盖后续反馈延迟和已在途数据。两者要画出计数边界,避免把同一段流水线算两次。只有已知每拍最多到达W字、尾流持续至多L个完整计数拍时,才可用W×L估计对应部分。若控制是"整包接收后才允许停止",剩余包长也必须计入。
可以减去这段时间内有保证的出队量,但必须验证每个前缀时刻的峰值,不能拿整段总读取量抵扣更早出现的峰值。按本文的满时拒写语义,还必须保证每个有写入的拍开始时有空位;同拍读取不能抵扣该拍所需的接收空间。消费者随时可能停住时,安全预算先按零读取处理。
四 almost_full不等于立即停住
阈值信号的名字和时序要查具体IP。例如AMD的XPM_FIFO_ASYNC文档分别列出标准读模式、FWFT模式,以及读操作、写操作引起的标志更新延迟,不能统一套成"两个同步拍"。AMD UG1726 XPM_FIFO_ASYNC
Intel/Altera FIFO手册同样按模式与配置列延迟,并要求DCFIFO写侧依据wrfull、读侧依据rdempty决定操作。异步两侧的计数和状态不能当作一个即时共享计数器。FIFO状态与延迟;FIFO信号定义
因此,跨域状态可见延迟、阈值产生延迟和上游响应延迟,应按实际路径分别分析。某些延迟造成保守停写,另一些决定尾流预算,不应全部机械相加成"通用公式"。异步FIFO的CDC可靠性也不能靠增加容量代替。
五 用可运行模型验证预算
下面代码保存为demo.py,用Python 3执行即可,无第三方依赖。模型约定:空时不直通;满状态在拍开始时判断,因此满时即使同时读,也拒绝当拍写入。普通非空非满状态下,同时读写占用不变。其他实现可能支持满时替换,必须相应修改接收条件。
service表示读取机会,不是已完成读数;lost表示无法暂停的数据源所提供、但未被缓存接收的字数。模型不追踪数据内容和顺序。
python
"""Deterministic occupancy model only; not an RTL/IP simulation."""
def run(arrivals, service, depth=None, initial=0):
assert len(arrivals) == len(service)
assert initial >= 0 and (depth is None or depth >= initial)
q, peak, lost, trace = initial, initial, 0, []
for offered, budget in zip(arrivals, service):
assert offered >= 0 and budget >= 0
# No empty bypass; full-at-start cannot accept a write.
read = min(q, budget)
write = offered if depth is None else min(offered, depth - q)
lost += offered - write
q += write - read
peak = max(peak, q)
trace.append(q)
return peak, lost, q, trace
def stopped_source(threshold, tail, cycles=20):
# No reads. Trigger after reaching threshold, then tail more writes.
q, trigger, arrivals = 0, None, []
for t in range(cycles):
write = int(trigger is None or t <= trigger + tail)
arrivals.append(write)
q += write
if trigger is None and q >= threshold:
trigger = t
return arrivals
a = [int(t < 16) for t in range(32)]
s = [int(t >= 8 and (t >= 16 or t % 2 == 0)) for t in range(32)]
p, lost, end, trace = run(a, s, depth=12)
assert (p, lost, end) == (12, 0, 0)
assert run(a, s, depth=11)[1] == 1
print(f"burst: peak={p}, lost={lost}, end={end}; depth11_lost=1")
print("burst trace:", trace)
p, lost, end, _ = run([1]*100, [int(t % 2 == 0) for t in range(100)], 16)
assert (p, lost, end) == (16, 35, 16)
print(f"sustained: peak={p}, lost={lost}, end={end}")
a = stopped_source(threshold=8, tail=3)
assert run(a, [0]*20, 11)[:3] == (11, 0, 11)
assert run(a, [0]*20, 10)[1] == 1
assert run(stopped_source(8, 0), [0]*20, 8)[1] == 0
print("delayed stop: threshold=8, tail=3, peak=11; D11_lost=0, D10_lost=1")
assert run([], [], 0)[:3] == (0, 0, 0)
assert run([0], [1], 0)[:3] == (0, 0, 0)
assert run([1], [0], 0)[:3] == (0, 1, 0)
assert run([1], [1], 1)[:3] == (1, 0, 1) # empty: no bypass
assert run([1], [1], 1, initial=1)[:3] == (1, 1, 0) # full
assert run([1], [1], 2, initial=1)[:3] == (1, 0, 1) # middle
print("boundaries: zero capacity, empty/full/middle simultaneous, zero tail PASS")
print("ALL CHECKS PASS (Python occupancy model only)")
实际运行输出:
text
burst: peak=12, lost=0, end=0; depth11_lost=1
burst trace: [1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 0, 0, 0, 0]
sustained: peak=16, lost=35, end=16
delayed stop: threshold=8, tail=3, peak=11; D11_lost=0, D10_lost=1
boundaries: zero capacity, empty/full/middle simultaneous, zero tail PASS
ALL CHECKS PASS (Python occupancy model only)
持续超速负例中,上游100拍连续写,下游隔拍读,深度16仍丢35字。若这种正的长期速率差无限持续,且上游不能暂停、不能丢弃,也没有其他存储出口,任何有限FIFO最终都会装满。加深只能推迟失败时间。
零深度测试只是模型边界检查,不表示厂商IP支持深度零。空、满、中间占用的同时读写测试,则用于防止把"读写抵消"误用到边界。
六 落地时交付四个结果
先给出到达与最差服务的假设,再给出占用峰值;随后给出背压触发点与尾流预算,最后对照目标IP确认可用深度、阈值范围和空满语义。这里的突发案例与停写案例是两个独立例子,不能把12和3直接相加作为最终深度。
若预算已经包含反馈尾流,就不要再重复加一遍。把最长停顿、连续突发、最慢响应、复位后首包和实际时钟比带入目标IP仿真,并检查最终握手与溢出标志。这样得到的深度才有可解释的边界,而不只是一个经验数字。