AXI-Stream 位宽适配器:窄→宽拼包、宽→窄拆包与 TKEEP 残拍
更新时间:2026-09-19。本文是 AXI4-Stream 与 DMA 的数据通路深文,握手规则见 valid/ready 握手。位宽适配常内建在 AXI Crossbar 与 DMA 里。
本文要回答的问题
- 窄口发的数据怎么攒成宽拍(upsize),宽拍怎么拆成窄拍(downsize)?
- 包长不是宽度整数倍时,TKEEP/TLAST 怎么正确传递?
- 上下游速率不同时反压怎么处理,什么时候出气泡?
- 比例非 2 的幂、或非整数倍时怎么办?
一、为什么要位宽适配
DMA 可能用 512 位喂数据,而后级加速器只吃 32 位;传感器 8 位流要拼到 64 位进内存。两侧各选最合适宽度(面积/频率/带宽权衡),中间放 width converter 做宽度与拍速的换算。单位时间字节数守恒:窄口高频多拍 ↔ 宽口低频少拍。
二、窄→宽 Upsizer:攒拍拼装
以 32→64 位(比例 R=2)为例:用一个宽位宽的拼装寄存器 + 一个「已收拍数」计数。
// 收窄拍:按 beat_cnt 决定放进低半还是上半,使能对应字节
always @(posedge clk) begin
if (s_valid && m_ready) begin
if (beat_cnt==0) begin
pack[31:0] <= s_data; keep_l <= s_keep;
end else begin
pack[63:32] <= s_data; keep_h <= s_keep;
end
end
end
// 攒满 R 拍,或遇到 TLAST(包提前结束),输出一个宽拍- 攒满 R 拍:拉高 m_valid 输出完整宽拍。
- 未攒满就来 TLAST:立刻输出一个宽拍,其中缺失的字节用 TKEEP=0 标记,TLAST 随这拍输出。这是 upsizer 的核心残包逻辑。
- 输出节奏:每 R 个输入拍产生 1 个输出拍(输入带宽够时)。
三、宽→窄 Downsizer:拆拍分发
64→32:锁存一个宽拍,用计数器(sel)依次选择低半、上半输出,每拍一个窄拍,原始 TKEEP 对应切片下发,TLAST 只在最后一个窄拍(对应原宽拍 TLAST 且 sel=R−1)上拉起。
assign s_ready = m_ready && (sel==R-1 || !m_valid); // 最后一窄拍被收走才可换宽拍
assign m_data = sel==0 ? hold[31:0] : hold[63:32];
assign m_keep = sel==0 ? keep_l : keep_h;
assign m_last = is_last_pkt && (sel==last_idx); // 残包要按 keep 算最后有效拍注意 downsizer 里一个宽拍对应的多个窄拍通常不可抢占、连续发完(不能在中途插另一包数据),否则 TID/保序会乱;发完才接收下一个宽拍。
四、TKEEP/TLAST 残包的精确定义
- 上行(窄→宽):把各窄拍 keep 拼成宽 keep;不足宽拍的高字节 keep=0;m_last 在收到 s_last 的那一输出拍拉起。
- 下行(宽→窄):把宽 keep 拆给各窄拍;最后一个「有任意 keep=1」的窄拍才带 m_last,后面全 keep=0 的空拍不发(避免下游收到假拍)。
- 整拍 keep 全 0 且 last 的语义要与下游约定:多数实现会传一个 keep=0、last=1 的空拍表示零长包,是否保留下游需一致。
- TID/TDEST/TUSER 在一个包内保持,跨包才可变;适配期间需随包寄存。
五、反压、气泡与 Skid
握手是标准 valid/ready,但转换器内部有缓冲后两侧 ready 关系变复杂:
- Downsizer:未发完一个宽拍前
s_ready应低(或靠内部 buffer 提前收下一个宽拍以减少气泡)。 - Upsizer:未攒满时可以持续吃窄拍(m_valid 还没起),攒满后只有 m_ready 才能继续。
- 想做到无气泡、并且切断 ready 的长组合路径,输入/输出端各可加一级 Skid Buffer(见 Skid Buffer 实现),这也是 IP 里 width converter 常带「registered/lightweight」两档的原因。
- 若两侧带宽(字节/秒)不匹配,转换无法消除带宽差:窄口平均供给 < 宽口需求时宽口必然稀疏;反之窄口会被反压。必要处加弹性 FIFO/异步 FIFO(见 异步 FIFO CDC)。
六、比例问题
- 2 的幂倍(2/4/8/16):计数器 + 固定 barrel mux,最常见。
- 整数但非 2 幂(如 32→96,R=3):同样计数,只是切片数不同。
- 非整数比(如 24↔32 位):字节粒度重新打包,需以字节 lane 为单位用小状态机持续填充/排空,比例约成 LCM 宽度循环(24 与 32 的 LCM=96:4 个 24 拍 ↔ 3 个 32 拍)。AXI-Stream 允许任意 8 倍数宽度,但实际尽量选 2 的幂以简化。
- 两端位宽都不是对方整数倍时,通用实现用「累积字节缓冲 + 按目的宽度切」的 FIFO 式方案。
七、常见的坑
- 残包 keep 错:窄→宽把缺失字节填成 keep=1,下游读到垃圾;宽→窄多发空拍或漏掉 last。
- TLAST 位置错:在非最后有效拍拉高,或拆包后 last 丢失,包边界错乱(DMA 无法关 buffer)。
- 拆包中途被抢占:一个宽拍没拆完就换数据,字节顺序/保序破坏。
- ready 组合环路:s_ready 直接由远端 m_ready + 内部计数长组合驱动,频率差;加 skid/寄存 ready。
- 以为转换能补带宽:窄口喂不饱宽口时抱怨有空泡——适配器只改宽度不改平均字节率。
- TID/TUSER 未随包保:多流复用时把包的 sideband 串到别的包。
相关与延伸
协议见 AXI4-Stream 与 DMA;握手见 valid/ready 握手 与 Skid Buffer;跨时钟见 异步 FIFO CDC;内存侧宽度转换见 AXI Crossbar。
总结
位宽适配器按字节守恒做窄↔宽换算:upsizer 攒满 R 个窄拍发一个宽拍、提前遇 TLAST 就用 TKEEP=0 补残拍,downsizer 锁存宽拍后连续拆成 R 个窄拍、只在最后一个有效拍拉 TLAST 且不发全空拍;非整数比退到字节粒度按 LCM 重排,内部加 Skid Buffer 切断 ready 长路径并消除气泡,但适配器只能改宽度、补不了两侧的平均带宽差。