ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AXI-Stream握手协议详解:TVALID与TREADY的RTL实现与调试技巧

AXI-Stream握手协议详解:TVALID与TREADY的RTL实现与调试技巧 1. AXI-Stream 握手协议的整体设计思路1.1 为什么流式数据传输需要 TVALID / TREADY 这组“暗号”做 FPGA 或者是 ASIC 前端设计的朋友对 AXI-Stream 这个东西一定不陌生。无论是 Xilinx 的 DMA IP、视频处理 IP还是自己写的内部数据通路几乎都跑在这个协议上。在我看来AXI-Stream 是 AMBA 家族里表面上最简单、实际最容易写错的一个协议原因恰恰集中在 TVALID 和 TREADY 这两个信号上。先想一个很朴素的问题假设模块 A 要持续往外发数据模块 B 要接收数据它们之间该怎样协调节奏如果 A 和 B 都是你自己设计的你当然可以在内部约定一个固定的节拍比如“每个周期都传一拍数据”。可一旦模块来自不同厂商或者数据速率不一致又或者中间跨越了不同时钟域那你就没法假设对方和你一拍一拍完美对齐了。这时候就需要一套双方都认可的“握手协议”让数据源和数据接收端可以互相告知“我现在有货”“我现在能收”。AXI-Stream 给出的答案就是 TVALID 和 TREADY。TVALID 由数据源source / master驱动代表“此刻 TDATA 上的数据有效”TREADY 由数据接收端sink / slave驱动代表“此刻我可以接收数据”。只有当 TVALID 和 TREADY 在同一个时钟上升沿同时为高这一拍数据才真正完成了传输。没有握手就没有传输没有传输数据就停在那里不许动。这套设计思路的核心是把“数据的有效性”和“通道的可用性”拆开来看。数据源只负责声明“我给出的数据是否有用”接收端只负责声明“我此刻能否接收”。两者相互独立、互不阻塞所以它天然支持背压backpressure也天然支持突发和流水线操作。相比那种用“一个 valid 一个固定时序”的简易方案AXI-Stream 的灵活性要大得多代价就是必须把握手的边界条件想清楚。1.2 AXI-Stream 与 AXI4、AXI-Lite 的定位差异很多新手会把 AXI-Stream 和 AXI4 混为一谈因为它们的名字很像。但两者的“性格”完全不同AXI4 和 AXI-Lite 是面向地址读写的内存映射型接口每次传输都要带上地址、数据、写使能、读使能这些信息通常用于 CPU 访问外设或内存控制器的场景。AXI-Stream 则没有地址通道它是纯粹的、连续的数据流接口只有数据信号、握手机号以及可选的辅助信号如 TKEEP、TLAST、TID、TDEST、TUSER。拿生活里的例子类比AXI4 像是“寄快递”你要写清楚收件人地址、寄件人信息、包裹内容一个包裹一个包裹地发出AXI-Stream 则像“自来水管”水一直在管子里流动你只需要控制阀门开关不用关心每一股水是从哪个街道来的。A 家 DMA 把数据从内存搬到 AXI-Stream 接口时转化的就是“内存寻址操作”到“流式节拍操作”这一层。搞清楚这种定位差异很有现实意义。你会经常遇到这样的场景自定义 IP 内部跑的是 AXI-Stream但外面接的是 AXI4 或 AXI-Lite 寄存器接口。这时候就要分清楚哪个接口上的握手规则更严格。AXI4 的读/写通道也有 VALID/READY 握手但它还多了地址通道通道之间的依赖关系更加复杂。AXI-Stream 简化到只有一个数据通道反而更容易讲清楚 VALID/READY 的时序关系。所以很多教程喜欢拿 AXI-Stream 作为入门握手的例子也正是因为它的信号少但规则一点没打折扣。1.3 TVALID 与 TREADY 的职责边界要写出可靠的 AXI-Stream 逻辑心中必须先画清楚两条线数据源必须保证“我说有效就一直有效直到被接收为止”接收端必须保证“我说就绪就可以在这一拍接收数据不要临时反悔”。TVALID 这一侧的职责是一旦拉高就代表 TDATA以及可选的 TKEEP、TLAST 等附属信号已经稳定有效。这是一种“承诺”我承诺这一拍的数据是对的而且我不会在中途把它撤掉。你什么时候收取决于你的 TREADY但我的 TVALID 在握手完成前会一直保持为高。如果你设计时写了“当 TREADY 拉低时就把 TVALID 也拉低”那这就是犯了大忌协议规定这种情况不被允许。TREADY 这一侧的职责是一旦为高接收端就表示“我准备好了你随时可以把数据发过来”。但这里有个细节TREADY 拉高并不意味着必须在这拍接收数据因为只有当 TVALID 也为高时数据才算传输成功。TREADY 更像是一张“入场券”——我答应你只要你说数据有效我立刻消耗掉这一拍。把边界画清楚后你会发现设计变得非常简单数据源只需要在“有数据可发”时拉高 TVALID然后等待是否被接收接收端只需要在“能收数据”时拉高 TREADY。双方各自维护自己的状态机不需要去关心对方内部到底在干什么。这种单向依赖的设计也最大限度地避免了组合逻辑环路。2. 核心握手规则深度拆解与实际要点2.1 四种状态组合真值表背后的含义TVALID 和 TREADY 各有高低两种状态组合起来就是四种情况。很多资料会直接画一张表格然后告诉你“只有两者都为高时才发生传输”。但如果只是背这个结论真正写 RTL 时还是会踩坑。我平时喜欢把这四种情况分成“等待”“阻塞”“传输”三种语义来理解下面逐一拆开讲。在 TVALID0、TREADY0 时没有任何数据可传输接收端也没有表示愿意接收。这是系统处于空闲状态的典型情况谁都不欠谁时钟继续走总线安静。在 TVALID0、TREADY1 时接收端已经“准备好”了等数据但数据源这一拍没有数据要发。这个状态很常见尤其对于那种从 FIFO 里读数据的模块FIFO 为空但输出侧已经把 TREADY 置高了。要注意的是接收端“准备好”不代表必须立即执行任何动作它只是在表达一种意愿。在 TVALID1、TREADY0 时数据源手里有货但接收端正忙收不了。这是背压发生的典型时刻数据会被“卡”在总线上。协议要求此时数据必须保持不变TVALID 必须保持为高直到 TREADY 也跳变为高、握手完成。如果在这一拍里数据变化了甚至 TVALID 被拉低了那就是协议违例。在 TVALID1、TREADY1 时一拍数据传输完成数据从数据源交到接收端。这是一个时钟周期里的“成功时刻”。如果数据源还有下一拍数据可以在下一拍继续把 TVALID 拉高如果没有了就把 TVALID 拉低。真值表是死的但理解状态语义后你写代码时就能条件反射式地判断这个信号在某个分支下该保持还是该变化占空比会不会受影响下游模块收到的背压信号是否会引起数据堆积这些判断力比记住表格本身更值钱。2.2 为什么 TVALID 不允许等待 TREADY这是 AXI-Stream 协议里最容易被忽视、也最致命的一条规则TVALID 一旦拉高就不能以 TREADY 为条件被撤销。换成电路语言就是TVALID 的拉低只能由“当前数据已经被接收”这件事来触发不能在 TREADY0 时主动放弃。试想一个反面场景你负责一个数据源模块发出的数据是计算出来的中间结果下一拍可能会因为运算更新而变化。你看到 TREADY0想着“反正没人收我就先拉低 TVALID 吧等 TREADY 变高再重新拉高”。这种行为看起来合理但协议明确禁止。为什么因为接收端可能正在按照自己的状态机跟踪总线上的数据。如果它在 TREADY1 后的某一个周期采样到 TVALID1 就准备接收可你中途又撤掉它根本无从判断你这拍数据到底算什么。尤其是当数据通道还带有 TLAST、TKEEP 这些控制信息时中途撤销会直接破坏包的完整性。更深层的原因是允许 TVALID 等待 TREADY 会引入组合逻辑环路风险。TREADY 的反压信号可能来自非常下游的 FIFO 满标志而 TVALID 又反过来作用在接收端的状态机上导致一个复杂的组合依赖链。这种环路在时序收敛时非常难处理而且仿真时可能会出现意想不到的振荡。AXI 协议的设计者正是为了避免这种混乱才把规则定得如此“霸道”一旦有效就必须等待直到被接收。那实际设计时怎么落实关键是把数据源的状态机拆成两步第一步判断“我有没有数据”有就寄存住并拉高 TVALID第二步当检测到 TVALID TREADY 时认为此拍已完成如果还有下一拍继续更新数据。如果没有数据了才允许拉低 TVALID。这样写出来的逻辑天然满足“TVALID 不等待 TREADY”这条规则。2.3 TREADY 的两种接法组合逻辑与寄存器输出另一个核心设计决策是 TREADY 的生成方式。接收端对外给出的 TREADY 有两种常见写法一种是用组合逻辑直接生成另一种是打一拍寄存器输出。两种方法各有适用场景用错了会带来莫名其妙的时序问题。组合逻辑 TREADY 的代表场景是FIFO 的读使能信号直接取反生成 TREADY。例如当 FIFO 非空时rd_en 为高此时 TREADY 可以直接等于“非空且空闲”。这种方式响应快、没有一拍延迟但代价是可能形成较长的组合逻辑链。如果 TREADY 需要由多个条件综合判断比如“FIFO 非空且上游选择门打开且状态机不在复位”那么这些条件的与或运算会全部落在握手路径上容易成为时序瓶颈。寄存器输出 TREADY 的优势是时序干净适合高速时钟。做法是内部维护一个 ready 寄存器每个周期根据是否接收数据来更新。比如“上一拍如果发生了握手且内部存储还没满则本拍 ready 置 1否则置 0”。这种写法把组合逻辑链路缩短了但代价是 TREADY 的响应滞后一拍。这在一对一连接里问题不大但在多级流水线里会影响总吞吐量需要综合考虑。我个人在写数据通路时默认会先采用寄存器输出的 TREADY 保证时序收敛然后用性能仿真去评估吞吐量。只有发现吞吐量确实因为额外一拍延迟而明显下降时才会考虑局部改成组合逻辑 TREADY并做严格时序约束。这算是一个“先求稳再求快”的实践原则。2.4 背压从下游一路传递到上游的“刹车信号”AXI-Stream 最迷人的一点就是背压的自然传递。想象一条流水线上游是数据源中间是你的处理模块下游是接收端。当接收端暂时不想收数据时它会把 TREADY 拉低。如果此时你的模块正打算往外输出数据由于 TREADY0你的输出数据和 TVALID 都会保持不变。你的模块内部存储空间会被持续占用一旦占满你也会自然而然地无法再从上游接收新数据。于是你也会拉低你的输入侧 TREADY让上游感知到反压。这个链条看起来顺理成章但实际工程里有一个常见误区有的人会在中间模块里加 FIFO但忘记让 FIFO 的满标志去控制输入 TREADY。结果就是FIFO 满了之后模块还在往 FIFO 里写数据被悄悄丢弃。要避免这个问题需要明确一点AXI-Stream 的背压不是靠“丢弃”而是靠“暂停”。任何一级都不能在没有完成握手的情况下丢掉数据否则整个数据流的包完整性就无法保证。在写背压逻辑时我还喜欢在关键节点加上 FIFO 深度监测和握手计数器。握手计数器统计的是“TVALID TREADY 的实际传输次数”它直接反映真实的吞吐量而不是仅仅看时钟频率。通过对比理想吞吐量和实际吞吐量能快速定位背压瓶颈在哪一级。3. RTL 实现与实操案例详解3.1 手写一个最简单的 AXI-Stream 从机接口直接进入代码。假设我们要写一个从机模块接收上游发来的 AXI-Stream 数据并把数据写入内部 FIFO。这个模块对外只有一组 AXI-Stream 输入s_axis_tdata、s_axis_tvalid、s_axis_tready、s_axis_tlast。内部使用一个双端口 FIFO写侧连接 AXI-Stream 的输入读侧连接用户逻辑。在 Verilog 里最核心的握手逻辑其实只有几行// 接收侧握手当 TVALID 和 TREADY 同时有效时数据被写入 FIFO assign s_axis_tready ~fifo_full; wire fifo_wr_en s_axis_tvalid s_axis_tready; always (posedge clk) begin if (rst_n 1b0) begin // 复位操作 end else if (fifo_wr_en) begin // 把 s_axis_tdata 写入 FIFO // 如果有 tlast则额外处理包结束标记 end end这段代码里我直接把 TREADY 定义为 FIFO 非满。看起来简单但它精准体现了握手协议的本质当 FIFO 满时TREADY 拉低上游数据会被卡住当 FIFO 有空位时TREADY 拉高只要上游有数据就立刻存入。不过这种写法有前提FIFO 的满标志必须是组合逻辑读出的否则 s_axis_tready 会滞后一两个周期导致 FIFO 可能被写穿。如果你用的是 Xilinx 的 FIFO IP 核要注意其 empty/full 标志通常有“嵌入式寄存器”和“无寄存器”两种模式后者延迟更小但时序更差。我用常见 FIFO IP 时会优先选择无寄存器的 full 标志并把 TREADY 生成链控制在两级以内这样既保证了协议正确又不会给时序收敛添乱。对于带 TLAST 的数据包还需要额外处理包结束标志。最简单的做法是在握手成功的那一拍把 s_axis_tlast 也同拍写入 FIFO 侧边带。这样下游读 FIFO 时可以知道当前数据项是否是一个包的最后一拍。3.2 一个简易数据生成器的完整代码解读数据源侧的逻辑同样不复杂但踩坑的细节更多。下面这个例子是一个简单的数据生成器它内部有一个计数器每个时钟周期自动产生一包递增数据。关键观察点是 TVALID 的生成和撤销逻辑。module data_generator ( input wire clk, input wire rst_n, output reg [31:0] m_axis_tdata, output reg m_axis_tvalid, input wire m_axis_tready ); reg [31:0] counter; wire handshake; reg data_valid_reg; assign handshake m_axis_tvalid m_axis_tready; // 数据有效标志初始为1每次握手后如果未到包尾继续保持1 always (posedge clk or negedge rst_n) begin if (!rst_n) begin m_axis_tvalid 1b0; data_valid_reg 1b0; end else if (handshake) begin // 当前拍已经完成传输根据逻辑决定下拍是否还有效 m_axis_tvalid data_valid_reg; end else if (!m_axis_tvalid) begin // 当前没有有效数据时如果有数据可以产生拉高 m_axis_tvalid 1b1; end end always (posedge clk or negedge rst_n) begin if (!rst_n) begin counter 32d0; data_valid_reg 1b1; end else if (handshake) begin counter counter 1b1; data_valid_reg 1b1; // 简化处理一直产生数据 end end always (*) begin m_axis_tdata counter; end endmodule上面这段代码为了演示简化了一些东西但核心思想是对的m_axis_tvalid 只有在握手成功后才会拉低或维持绝对不能因为 TREADY0 就拉低。很多人写成下面这种错误写法一旦遇到下游背压数据就会丢失// 错误示例用 TREADY 作为 TVALID 的撤销条件 assign m_axis_tvalid data_valid_reg m_axis_tready;这个错误写法等于把 TREADY 嵌入了 TVALID 生成逻辑。结果就是当 TREADY0 时TVALID 直接变低相当于把还没有被接收的数据撤掉了。下游可能根本还没准备好接收却被你告知“数据没了”。在实际硬件里这会造成上游数据永久丢失且仿真里很难一眼发现因为它并不一定会报错。数据生成器如果还涉及多拍延迟的算法模块则需要一个小型的状态寄存器缓存“待发送”的数据。通常是先用一个 ready 信号去锁存算法的输出再根据握手结果决定是否更新。我建议把数据路径和控制路径分开控制路径只维护 TVALID/TREADY 状态数据路径只在握手成功时被允许更新寄存器这样逻辑最清晰。3.3 多级流水线场景下的握手设计参考当数据通路变长比如一连串的浮点运算、图像滤波、FFT 处理你会发现每个流水级都需要包含自己的握手逻辑。典型的做法是每一级输入侧都挂一个 skid buffer滑动缓冲器输出侧保持组合逻辑的 TREADY。skid buffer 的作用是缓存一拍数据解决掉“TREADY 无法同拍响应”造成的吞吐损失。举个例子你的模块内部有一个三级流水线每一级都有一个“数据有效位”在随流水移动。输入端接收上游数据后将 valid 信号随数据一起打三拍输出端则根据输出的 valid 和下游 TREADY 来决定是否让整条流水前进。如果下游反压流水线必须整体暂停所有级的寄存器保持不动。核心控制逻辑可以这样设计// pipeline_en流水线使能信号 wire pipeline_en !out_ready_to_stall || (out_valid out_tready); // out_ready_to_stall 表示输出级前的暂存区已满需要停流 always (posedge clk) begin if (pipeline_en) begin stage1_reg stage1_next; stage2_reg stage2_next; stage3_reg stage3_next; end end这个设计的精髓在于一旦下游 TREADY0pipeline_en 会变为 0所有级都停住不会出现“上一级往前写了下一级却没地方放”的尴尬。这也正是 AXI-Stream 流水线的标准实现思路。实际项目中我会在多级处理模块的输入侧和输出侧各加一个小 FIFO作为“弹性缓冲”用于吸收短时背压抖动而不至于让整条流水线频繁启停。3.4 握手延迟如何影响吞吐量握手协议里存在一个隐性的性能指标握手延迟handshake latency。定义上它指的是“数据源准备好数据并拉高 TVALID”到“完成一次握手”之间的平均时钟周期数。这个延迟越低同等时钟频率下吞吐量越高。如果 TREADY 永远为高那么每个时钟周期都能完成一次握手吞吐率达 100%。一旦 TREADY 不是恒定高就需要认真评估背压的占空比。假设每个周期有 60% 的概率 TREADY1那平均吞吐率不是简单的 60%还要看 TVALID 是否始终为高。如果 TVALID 和 TREADY 的活跃周期互不重叠实际吞吐率可能低得可怜。我在实际项目里为了评估握手效率会专门在仿真环境中统计两类信息一类是“有效数据被阻塞的周期数”另一类是“接收端空闲但数据源没数据的周期数”。前者表明数据源在等待接收端是背压瓶颈后者表明接收端在等待数据源是数据源供给不足。这两个统计量各自指向不同的优化方向前者要优化下游接收速度或增加缓存深度后者要优化上游数据产生速度。这种定量分析比拍脑袋改 FIFO 深度要靠谱得多。4. 常见问题与排查技巧实录4.1 数据丢失的元凶错误拉低 TVALID我接手过不少同事的调试任务最常遇到的 AXI-Stream 问题就是数据丢失。表面看起来FIFO 读出来的数据偶尔少几拍或者图像出现花屏但抓波形又看不出明显异常。用协议分析器一查往往是上游把 TVALID 在未握手的情况下拉低了。典型的错误代码长这样always (posedge clk) begin if (m_axis_tready 1b0) m_axis_tvalid 1b0; // 错误 else if (next_data_valid) m_axis_tvalid 1b1; end这种写法在“TREADY0 时拉低 TVALID”的做法会直接违背协议导致数据源与接收端之间的约定被破坏。解决办法很简单删掉那个 TREADY 判断分支改成“只有握手成功后才更新 valid 状态”。排查的时候我建议在仿真波形里同时拉出 TVALID 和 TREADY用光标检查是否存在“TVALID 从 1 跌到 0 但同一时刻 TREADY 为 0”的情况。只要抓到一次基本就能认定这个 bug。4.2 死锁场景谁在等谁AXI-Stream 的死锁问题不像 SoC 里多主多从竞争锁那么复杂但也真实存在。最常见的死锁场景出现在数据源和接收端存在循环依赖的时候。比如模块 A 要等模块 B 发出某个控制信号才开始发数据模块 B 又要等模块 A 的数据到了才更新控制信号两边就互相卡死了。这种逻辑在功能上可能说得通但在握手语境下很危险尤其是两边都有 TREADY 参与的条件分支稍不留神就形成组合环。我在做视频处理 IP 时碰到过一个案例上游送来的数据包要求最后一个 TLAST 信号只有在内部状态机进入“包尾处理”状态时才会被消费而状态机进入“包尾处理”又需要先收到 TLAST。这个循环依赖在单模块内部看不太出来但在系统联调时整条链路就停在第一包数据的尾部所有模块的 TVALID 都拉高谁也没办法继续。排查死锁的方法我一般靠两点。第一在仿真脚本里加一个全局超时判断如果某个接口连续 N 个周期都没有成功握手则打印警告并暂停仿真。第二在总线上加断言SVA断言“TVALID 为高且持续超过一定周期数时必须发生握手”之类的不变量。死锁的本质是活锁中的特例它不会产生错误数据但会冻结整条链路。用超时机制能第一时间捕捉到。4.3 时序收敛与握手路径优化AXI-Stream 很容易成为时序瓶颈原因是握手相关的逻辑往往跨越多个模块边界而且 TREADY 的生成链可能极长。举个具体例子你的接收模块 TREADY 是由“FIFO 非满 状态机准备好 上游降速信号有效”这三个条件串联起来的再加上跨时钟域同步器这串组合逻辑可能超过 10 级。在 200MHz 以上时钟下很容易出现 setup 违例。针对这个情况我常用的优化手段有三个。第一把复杂的 TREADY 条件拆开提前一拍计算好中间结果用寄存器缓存让关键路径只是简单的与门。第二在长数据通路中间插入流水寄存器虽然会增加一拍延迟但对吞吐率影响很小。第三给关键的 valid/ready 路径设置准确的时序约束例如在 XDC 文件里设置 max_delay 或使用 set_multicycle_path避免工具盲目优化。很多时候Vivado 报出的一堆时序错误里真正的关键路径其实是那根被所有人忽略的 TREADY 信号而不是数据总线本身。4.4 仿真调试中的实用断言模板调试 AXI-Stream 接口时断言能帮你自动发现问题而不是靠人眼盯波形。SystemVerilog AssertionSVA里我最常用的有几条// 规则1TVALID 为高时TDATA 必须稳定数据不能在未握手时变化 property p_data_stable; (posedge clk) disable iff (!rst_n) (m_axis_tvalid !m_axis_tready) | ($stable(m_axis_tdata)); endproperty // 规则2一旦 TVALID 拉高在没有握手完成前不能拉低 property p_tvalid_hold; (posedge clk) disable iff (!rst_n) (m_axis_tvalid !m_axis_tready) | (m_axis_tvalid); endproperty // 规则3握手完成时数据成功传递 property p_handshake; (posedge clk) disable iff (!rst_n) (m_axis_tvalid m_axis_tready) |- ##1 ...; endproperty这些断言并不复杂但在回归测试里能帮你把接口协议错误拦截在仿真阶段。我曾经因为漏写第二条断言眼睁睁看着一个坏数据包在 FPGA 板上反复出现排查了一整天才定位。后来就把这组断言固化成一个公共的 assertion module所有 AXI-Stream 接口例化时都挂一份省了不少事。4.5 常见问题速查表现象可能原因排查重点数据偶尔丢失TVALID 在未握手时被拉低查 TVALID 生成逻辑是否依赖 TREADY链路卡死、不再传输存在循环依赖或死锁加超时监测查状态机依赖关系吞吐量低于预期TREADY 占空比低或握手延迟大统计握手成功周期数确认瓶颈级FIFO 写满但仍在写FIFO 满标志与 TREADY 逻辑有延迟检查满标志是组合输出还是寄存器输出图像花屏或包错位TLAST 没有同拍传递检查 TLAST 是否随 TDATA 一起寄存时序收敛失败TREADY 生成链过长拆状态、插流水寄存器、优化关键路径这个表里总结的是我在实际项目里反复踩过的坑。每次拿到一个新 IP 或者接手同事的代码我都会先按这张表做一轮静态检查大部分低级问题都能提前暴露出来。5. 从规范到工程几点实操心得聊到这里关于握手协议的原理、代码实践和调试方法都已经覆盖了。我在实际使用中发现很多人不是不理解协议规则而是在工程落地时容易被“我觉得应该这样”的主观判断带偏。硬件设计和软件不同协议里的每一个字都可能是用血的教训换来的尤其是 TVALID 和 TREADY 这种看似简单的信号一旦写错轻则数据丢失重则整个系统卡死。我个人特别建议在代码里把 valid 和 ready 的生成逻辑集中管理不要散落在各个 always 块里。你可以封装一个小型握手寄存模块把所有 valid/ready 相关的状态跳转都收拢到一处。这样改动一个判断条件时不会误伤到其他逻辑。代码审查时也只需要重点审查这一个核心模块效率会高很多。另外建议每次上板前都跑一轮带断言和握手覆盖率统计的仿真。仿真多花的两个小时可能省下的是整整两天的板级调试。这一点我从第一次因为 TVALID 提前撤销导致 FPGA 板子图像错乱时就深刻体会到了。AXI-Stream 的 TVALID 和 TREADY说到底就是一套“双方都要信守承诺”的约定。数据源承诺不轻易撤销接收端承诺就绪即收。只要把这两个承诺刻在脑子里再复杂的流水线写起来也会顺手很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进