用 SystemVerilog 写一个可背压的流式 RLE 编码器:计数拆分、包尾与 flush 的完整实现

用 SystemVerilog 写一个可背压的流式 RLE 编码器:计数拆分、包尾与 flush 的完整实现

RLE(Run-Length Encoding,游程编码)把连续相同的符号变成"符号+次数"。算法本身很短:看到相同符号就加一,看到不同符号就输出上一段。搬到流式 RTL 后,真正容易出错的是边界:下游停止接收时能否保持数据?计数器恰好装满时,包尾放在哪里?输入换符号的同时带上 last,会不会把 last 给错游程?

本文实现一个原创的教学模块,并在 Windows 本机的 Vivado Simulator 2022.1 上运行测试。它使用自定义 ready/valid 接口,支持参数化符号和计数宽度、有限计数拆分、包尾与独立 flush 命令、背压和同步复位。目标是让状态和每次握手的含义可核对;它不是完整 AXI4-Stream IP,也没有进行综合、布局布线或硬件时序收敛验证。

1. 先明确数据格式和边界

输入是一串符号,每个已接受的符号可以带 s_last;输出 token 是 (m_symbol, m_count, m_last)。一个 token 表示重复 m_count 次的符号,m_last=1 表示这些符号中的最后一个也是当前包的最后一个。零计数不是合法 token。

默认 SYMBOL_W=8、COUNT_W=3,因此一个 token 最多表示 7 个相同符号。例如:

text 复制代码
输入:A A A B B C(last)
输出:(A,3,0) (B,2,0) (C,1,1)

输入:17 个 A,最后一个带 last
输出:(A,7,0) (A,7,0) (A,3,1)

输入:恰好 7 个 A,第 7 个带 last
输出:(A,7,1)

计数最大值是全 1,而不是能额外表示的 2**COUNT_W。合法参数要求 SYMBOL_W>=1、COUNT_W>=1。RTL 不接收运行时的最大计数配置;改计数宽度需要重新展开设计。

RLE 并非一定压缩。若只比较符号数据位与本文 token 字段,忽略输入包尾旁带及传输封装,默认配置每个 token 有 8+3+1=12 位。一段 7 个相同的 8 位符号可由一个 12 位 token 表示;若每个符号都不同,每个 8 位符号反而需要一个 12 位 token。这只是编码字段的大小比较,不是实测链路带宽或 FPGA 资源结论。

2. 接口契约:只在握手时接收

所有状态更新使用 clk 上升沿。除复位外,输入、输出、flush 都以 valid && ready 为接受事件。

信号 方向(相对编码器) 含义
clk、rst 输入 时钟;高有效同步复位
s_valid、s_ready 输入、输出 输入符号的握手
s_symbol[SYMBOL_W-1:0]、s_last 输入 符号及包尾标记
flush_valid、flush_ready 输入、输出 独立结束当前包的命令握手
m_valid、m_ready 输出、输入 输出 token 的握手
m_symbol、m_count、m_last 输出 游程 token 的完整负载

这里借用的是 ready/valid 的基本握手规则:双方同时有效才传输;发送方不能等 ready 才拉高 valid;发送方给出有效负载后,在完成握手前应保持负载与 valid。Arm《AMBA AXI-Stream Protocol Specification》IHI 0051B 第 2.2 节及 2.2.1 节明确说明了这些规则。本文只据此定义自有接口的流控约定,flush、字段布局和同步复位都是本模块自己的设计,并不由此声称 AXI 一致性。Arm 官方规范,第 2.2 节(PDF 第 18 页)

上游的义务是:拉高 s_valid 后,直到握手都保持 s_symbol 和 s_last;不能把"ready 曾经为高"当成实际接收。flush 发送方同样把 flush_valid 保持到握手,握手完成后撤销,否则每个满足握手的周期都算新命令。下游可以先等待 m_valid,然后才拉高 m_ready。

last 和 flush 的精确定义

s_last 只在输入握手时有意义,并且包含当前输入符号。即使这一项与前一项不同,包尾也属于新符号对应的 token,旧游程仍必须先输出 last=0。

flush 结束已经接受、尚未结束的当前包,并把最后一个游程作为 last=1 输出。没有当前游程时,flush 是被接受的空操作:不输出零计数 token,不创造空包,也不产生独立的结束标记。因此,这个接口不能表达空包。

当 flush_valid 与 s_valid 同时出现,flush 优先:s_ready=0,只有 flush 可以握手。上游继续保持输入负载,等 flush 和旧包处理完后,这个输入成为下一包的内容。此时即使 s_last=1,也不能把它算作已接收输入。若 flush 被无限连续提出,数据可能被饥饿;系统调度者必须避免这样的命令流。

复位优先级

rst=1 的上升沿取消当前游程、待结束状态和未握手的输出 token。复位边沿不计任何输入、输出或 flush 传输;复位也不会为已取消的数据生成包尾。接收方必须用相同的复位协议丢弃不完整事务。m_valid 在复位上升沿后清零,这不是异步清零;组合的 s_ready、flush_ready 在 rst 为高时已经为低。

3. 两份存储,避免同时处理过多事件

模块只有当前游程和输出寄存器两份数据存储:

text 复制代码
s_symbol/s_last → 当前游程 run_symbol/run_count + closing
                                      ↓
                         输出寄存器 m_symbol/m_count/m_last/m_valid
                                      ↓
                                   下游握手

run_count=0 表示当前游程为空,不再增加单独的 run_valid 位。closing=1 表示当前游程已包含一个被接受的 s_last,必须成为包尾 token。它可能需要等待输出寄存器空闲,但不再接受后续输入或 flush。

输出槽有 token 时,模块暂停输入。即使 m_ready=1,也不会在同一边沿既消费旧 token 又装入新 token;旧 token 完成握手的那个周期仍然不能接收新输入。这是有意采用的保守实现,可以直接看出何时允许改写输出寄存器。它适合讲清状态边界,不代表高吞吐实现。

当前游程可以连续累加;遇到变化或计数拆分才占用输出槽。计数未到上限、符号保持相同且没有 last 时,可以每拍接受一个符号。不同符号频繁出现时,输出槽的空泡会降低吞吐;下游无限背压也会使输入无限等待。本模块没有给出固定的全局延迟保证。

为什么计数满了不立即输出

假设已经接受 7 个 A,但第 7 个没有 last。此时可能马上到来 flush,也可能到来第 8 个 A,还可能到来一个 B。三种情况对已有满游程的包尾属性不一样:

后续事件 对已有 (A,7) 的处理 新游程
flush 握手 输出 (A,7,1) 空
接受第 8 个 A 输出 (A,7,0) (A,1)
接受 B 输出 (A,7,0) (B,1)

因此保留满游程,直到有下一项或 flush 提供足够信息。如果第 7 个 A 自身带 last,就直接进入 closing,随后输出 (A,7,1)。这样无需生成计数为零的"补丁包尾",也无需回头修改已经握手的 token。

如果上游不再提供数据,也不提供 last 或 flush,当前游程会继续保留。流本身没有"等待若干拍就自动结束"的概念,测试或系统集成时必须显式结束包。

一次符号变化加 last 的状态顺序

以已经缓存 A×3、输出槽为空为例:

上升沿 事件 边沿后的状态
T0 接受 B,last=1 输出槽为 (A,3,0),当前游程为 B×1,closing=1
T1... 下游背压 旧输出完整保持,B 及 closing 也保持
Tk 下游接收 (A,3,0) 输出槽清空,仍不接收新输入
Tk+1 处理 closing 输出槽变为 (B,1,1),当前游程清空
Tk+2 或之后 下游接收最终 token 本包传输结束

m_valid 的生成没有以 m_ready 为条件;即使下游一直没有 ready,也会先看到可接收的 token。

4. 完整核心 RTL

下面代码与交付目录的 rtl/rle_stream.sv 一致。所有状态使用非阻塞赋值;if (!m_valid) 读取的是该上升沿之前的 valid,因此旧 token 被消费的边沿不会顺便覆盖输出槽。

systemverilog 复制代码
// Original educational RLE encoder; custom ready/valid, NOT a full AXI interface.
// Legal parameters: SYMBOL_W >= 1, COUNT_W >= 1. MAX_RUN = 2**COUNT_W - 1.
// All transfers sample valid && ready on a rising edge with rst == 0.
module rle_stream #(
    parameter integer SYMBOL_W = 8,
    parameter integer COUNT_W = 3
) (
    input wire clk, input wire rst,
    input wire s_valid, output wire s_ready,
    input wire [SYMBOL_W-1:0] s_symbol, input wire s_last,
    input wire flush_valid, output wire flush_ready,
    output reg m_valid, input wire m_ready,
    output reg [SYMBOL_W-1:0] m_symbol,
    output reg [COUNT_W-1:0] m_count, output reg m_last
);
    localparam [COUNT_W-1:0] MAX_RUN = {COUNT_W{1'b1}};
    reg [SYMBOL_W-1:0] run_symbol;
    reg [COUNT_W-1:0] run_count; // zero means no buffered run
    reg closing; // buffered run must become the final token

    // Conservative one-slot output: no simultaneous consume/refill.
    // flush has priority; a waiting data source keeps its payload unchanged.
    wire available = !rst && !m_valid && !closing;
    assign flush_ready = available;
    assign s_ready = available && !flush_valid;

    always @(posedge clk) begin
        if (rst) begin
            run_symbol <= '0;
            run_count <= '0;
            closing <= 1'b0;
            m_valid <= 1'b0;
            m_symbol <= '0;
            m_count <= '0;
            m_last <= 1'b0;
        end else begin
            if (m_valid && m_ready)
                m_valid <= 1'b0;

            // This guard preserves valid and the entire payload under stalls.
            // m_ready never controls creation of an output token.
            if (!m_valid) begin
                if (closing) begin
                    m_valid <= 1'b1;
                    m_symbol <= run_symbol;
                    m_count <= run_count;
                    m_last <= 1'b1;
                    run_count <= '0;
                    closing <= 1'b0;
                end else if (flush_valid && flush_ready) begin
                    // Empty flush is a handshake with no output and no packet.
                    if (run_count != 0) begin
                        m_valid <= 1'b1;
                        m_symbol <= run_symbol;
                        m_count <= run_count;
                        m_last <= 1'b1;
                        run_count <= '0;
                    end
                end else if (s_valid && s_ready) begin
                    if (run_count == 0) begin
                        run_symbol <= s_symbol;
                        run_count <= {{(COUNT_W-1){1'b0}},1'b1};
                    end else if (s_symbol != run_symbol || run_count == MAX_RUN) begin
                        // Lookahead proves the old token is nonfinal. The new
                        // symbol is already accepted and becomes the next run.
                        m_valid <= 1'b1;
                        m_symbol <= run_symbol;
                        m_count <= run_count;
                        m_last <= 1'b0;
                        run_symbol <= s_symbol;
                        run_count <= {{(COUNT_W-1){1'b0}},1'b1};
                    end else begin
                        run_count <= run_count + 1'b1;
                    end
                    // Including on a symbol change/MAX split: last belongs to
                    // the newly accepted symbol, never to the emitted old run.
                    closing <= s_last;
                end
            end
        end
    end
endmodule

读代码时可以按优先级检查:复位最高;正常运行中先完成旧输出握手;只有旧输出槽原本为空时,才依次考虑 closing、flush、输入。closing 是内部待办状态,优先于外部命令;flush 再优先于输入。输出已经有效且被背压时,这些分支都不会修改负载。

增计数只发生在"已有游程、符号相同、计数未满"分支。满计数走拆分分支,所以不会先溢出再判断。COUNT_W=1 时最大值就是 1,每个 token 只能表示一个符号,但接口和包尾规则仍然成立。

5. 测试先行:不仅检查能不能编译

测试入口为 tb/tb_rle_stream.sv,重放入口为 replay.ps1。测试运行真实 RTL,未用软件编码器替代 DUT。主要计分板只观察外部握手:

  1. 每次输入握手,把原始符号和 last 放进期望队列。
  2. flush 握手且包非空时,把该包最后一个原始符号标成包尾。
  3. 每次输出握手,把 token 解码为 count 个符号,逐项比较符号和包尾位置。
  4. 复位丢弃剩余期望值,并记入取消计数,之后继续验证新事务。

这个模型没有调用或镜像 DUT 的游程状态机。仅验证解码等价仍然不够,例如错误地把长游程拆成许多 singleton token 也可以解码成功。因此,定向用例还直接核对手工期望的 token 元组及 token 数量,例如 (A,7,0)+(A,1,1)。背压监视器逐周期检查 m_valid、符号、计数和 last 的稳定性;输出计数不得为零或未知值。

随机部分使用独立的源端和接收端 xorshift32 状态,种子分别固定为 0x1a2b3c4d、0x9e3779b9。每个计数宽度运行 200 个长度 1~30 的随机包,源端插入空拍,下游约 10/16 的随机周期给 ready,部分包使用 last,其他包使用 flush。这里的概率描述是发生器规则,不代表每次有限运行的精确比例。

每个输入/flush 等待和 drain 都有超时,另有全局 150000 周期 watchdog。最后检查符号守恒 accepted = decoded + canceled 与包尾守恒 packets_in = packets_out + canceled_packets。总数差异只有明确的复位取消,不用"丢了也没关系"的宽松比较隐藏问题。

定向回归覆盖:

  • 单符号包;持续 valid 的长游程;大于两倍上限的连续相同值;频繁换值。
  • 最大计数恰好位于包尾;满计数后再来相同或不同的最终符号;换值与 last 同时发生。
  • ready 持续为低时主动产生最终 token,随后多拍保持并恢复接收。
  • 空 flush、非空 flush、恰好满计数时 flush、flush 和输入同时提出时的优先级。
  • 复位取消缓存游程、同时存在的受阻输出和 closing 游程、已经受阻的最终 token。

已实际观察到的 RED

先写接口 stub 与测试,stub 可以接收符号,但不产生任何输出;核心算法当时尚未实现。2026-11-01 在本机执行后,编译和展开成功,第一项行为检查失败:

text 复制代码
Fatal: single final symbol: missing output; decoded=0 accepted=1

原始日志、当时的 stub 和测试快照位于 evidence/red-cw3-assertion/。它证明测试确实能抓住"接受了数据却没有输出"的缺失实现。更早一次 evidence/red-cw3/ 因 Windows batch 转发 CW=3 参数的引号问题而展开失败;这只是工具调用问题,不计作 TDD 的 RED。修正参数传递并观察上述行为失败后,才写核心实现。首次 GREEN 后又增加了连续 valid、额外的复位守恒与参数边界覆盖,保留了最初证据,未把新增覆盖冒充最初 RED 的内容。

本机实际 GREEN 结果

测试环境为 Windows,Vivado Simulator v2022.1,时钟周期 10 ns;该仿真时钟只是激励,不是已验证硬件频率。最终测试全部通过:

COUNT_W / 最大计数 断言检查次数 接受符号 解码符号 复位取消符号 输出 token 背压稳定性检查 仿真周期
1 / 1 10876 3403 3399 4 3399 2011 13055
3 / 7 6719 3451 3447 4 1462 877 12650
8 / 255 8658 5435 5431 4 1427 872 16096

三组均接受 218 个明确结束的包,输出 216 个包尾,另 2 个包尾因复位取消;每组执行 71 次 flush,其中 1 次为空,执行 3 种运行中复位用例。这里"断言检查次数"是 testbench 显式累加的动态检查数,不是代码覆盖率或形式证明统计。

对应完整日志位于 evidence/green-cw1-final/、evidence/green-cw3-final/、evidence/green-cw8-final/。各组编译、展开、仿真进程退出码均为 0,日志包含 PASS RLE,没有检出 WARNING、ERROR 或 Fatal。3 位计数的一行结果为:

text 复制代码
PASS RLE CW=3 checks=6719 accepted=3451 decoded=3447 canceled=4 tokens=1462 packets_in=218 packets_out=216 canceled_packets=2 flushes=71 empty_flushes=1 stall_checks=877 consecutive_inputs=13 reset_cases=3 cycles=12650

独立复核与运行时变异测试

另一个独立审查过程为冻结后的 RTL 编写了独立 testbench,同时检查字面 token、解码后的已接受输入、包边界和背压保持。最终 COUNT_W=1、2、3 分别通过 4512、2911、2530 次断言检查,共 9953 次;flush 和输入并发时,测试保持未被接受的输入,随后验证它进入下一包。基线日志为 ../review/rle/baseline-verified/simulate.stdout.log,结束标记为 INDEPENDENT_PASS rle。

审查还分别在独立源文件副本中注入两种真实行为错误,生产 RTL 未修改:

变异 编译与展开 运行时捕获到的失败 日志目录
丢弃最终 token 的 last 成功 reference last,CW=1,cycle=39,385 ns ../review/rle/drop-last-verified/
背压时破坏输出 count 成功 all payload held through stall,CW=1,cycle=38,375 ns ../review/rle/corrupt-stall-verified/

两种变异的仿真原生退出码同样为 0,但都出现 INDEPENDENT_FAIL 的 Fatal 且没有成功标记,所以被判为运行时失败;它们不是用语法错误制造的"测试失败"。独立复核同时确认本文核心代码与冻结 RTL 一致。上述动态检查增强了对特定错误的发现能力,并不等于穷尽所有状态。

6. 如何复现

将第4节完整核心保存为 rle_stream.sv,将第8节完整测试台保存为 tb_rle_stream.sv,放到同一临时目录。在已配置 Vivado 2022.1 命令环境的 Windows 终端执行:

powershell 复制代码
xvlog.bat --sv rle_stream.sv tb_rle_stream.sv
xelab.bat tb_rle_stream -s rle_snapshot --debug typical --timescale 1ns/1ps -generic_top '"CW=3"'
xsim.bat rle_snapshot -runall

这里外层单引号让 PowerShell 将内部双引号作为字符传给 batch 包装器,避免参数被拆开。把 CW=3 分别改成 CW=1、CW=8,在独立目录重新编译展开,可复现另外两个计数宽度。工具安装路径不同,应先使用本机 Vivado 命令环境;没有安装或购买新工具的步骤。

成功判据是存在对应 PASS RLE 结尾,且没有 Fatal: 或失败记录。一个实际发现是本机 XSim 遇到 $fatal 时进程退出码也可能是0,所以不能仅凭退出码判通过。作者保存了完整命令、工具版本、源码哈希和原始日志;文中的运行结果来自这些记录。

7. 集成时应保留的约束

这是单时钟、无时钟域跨越的有限缓冲模块。一次 flush 必须保持到被接受,输入未握手时不能修改负载,复位必须在系统范围协调取消语义。包必须用 last 或 flush 显式结束;空包需要另行设计表示方式。参数为正是模块使用前提,不支持零位宽。

如果下一步要提高吞吐,可先围绕相同契约设计允许输出消费与装载并行的缓冲结构,并继续用解码计分板与字面 token 检查验证。当前代码的价值在于把每个边界处理放在可读的位置,而不是对任何器件、频率或数据分布给出性能承诺。

本文核心 RTL、测试和说明均为本次编写;未复制第三方实现。外部规范仅用于核对握手规则,访问日期为 2026-11-01。所有"通过"结论限于所列本机行为仿真与参数实例,不包含 FPGA 下载、综合资源、时序报告或板级测量。

8. 可直接保存的完整测试台

下面是本次通过原生仿真的完整测试台。源码头部的 "Mutations caught" 列举测试拟捕捉的错误类别;本次真正执行的行为变异只有前文明确列出的两种,不把注释清单当成额外实测结果。将核心 RTL 和测试台分别保存为对应文件,放到同一临时目录,在已配置 Vivado 命令环境的终端执行前文的 xvlog、xelab、xsim 命令即可复现。本文提到的 evidence/review 路径是作者留存证据的位置,不是公开下载链接。

systemverilog 复制代码
`timescale 1ns/1ps
// Mutations caught: dropped/duplicated/reordered symbols, broken count split,
// early/missing last, ready-dependent valid, stall overwrite, reset leakage.
module tb_rle_stream;
    parameter integer CW = 3;
    localparam integer MAX_RUN = (2**CW)-1;
    reg clk = 0;
    always #5 clk = ~clk;
    reg rst = 1, s_valid = 0, s_last = 0, flush_valid = 0;
    reg [7:0] s_symbol = 0;
    wire s_ready, flush_ready, m_valid, m_last;
    wire [7:0] m_symbol;
    wire [CW-1:0] m_count;
    reg m_ready = 0;
    rle_stream #(.COUNT_W(CW)) dut (.*);

    byte expected_symbol [0:65535];
    bit expected_last [0:65535];
    byte token_symbol [0:65535];
    integer token_count [0:65535];
    bit token_last [0:65535];
    integer head = 0, tail = 0, open_symbols = 0;
    integer accepted = 0, decoded = 0, canceled = 0, tokens = 0;
    integer packets_in = 0, packets_out = 0, canceled_packets = 0, flushes = 0, empty_flushes = 0;
    integer consecutive_inputs = 0;
    bit previous_input = 0;
    integer stall_checks = 0, checks = 0, cycles = 0, resets = 0;
    bit held = 0;
    reg [7:0] held_symbol;
    reg [CW-1:0] held_count;
    reg held_last;
    bit random_ready = 0;
    reg [31:0] source_rng = 32'h1a2b3c4d;
    reg [31:0] sink_rng = 32'h9e3779b9;
    integer mark, n, p, length;
    reg [7:0] value;

    // Separate deterministic generators avoid schedule-sensitive $urandom state.
    function automatic [31:0] next_rng(input [31:0] x);
        reg [31:0] y;
        begin y=x^(x<<13); y=y^(y>>17); next_rng=y^(y<<5); end
    endfunction

    always @(negedge clk) begin
        if (random_ready) begin
            sink_rng = next_rng(sink_rng);
            m_ready = (sink_rng[3:0] < 10);
        end
    end

    // Only external handshakes feed this decoder scoreboard. No DUT internals.
    always @(posedge clk) begin : scoreboard
        integer k;
        cycles = cycles + 1;
        if (cycles > 150000) $fatal(1, "watchdog: no bounded progress");
        if (rst) begin
            for(k=head;k<tail;k=k+1)
                if(expected_last[k]) canceled_packets=canceled_packets+1;
            canceled = canceled + tail-head;
            head = 0; tail = 0; open_symbols = 0; held = 0; previous_input=0;
        end else begin
            if (held) begin
                checks = checks+1; stall_checks = stall_checks+1;
                if (m_valid !== 1'b1 || m_symbol !== held_symbol ||
                    m_count !== held_count || m_last !== held_last)
                    $fatal(1, "output changed while stalled");
            end
            if (s_valid && s_ready) begin
                if(previous_input) consecutive_inputs=consecutive_inputs+1;
                if (tail >= 65536) $fatal(1, "scoreboard capacity exceeded");
                expected_symbol[tail] = s_symbol;
                expected_last[tail] = s_last;
                tail = tail+1; accepted = accepted+1;
                open_symbols = open_symbols+1;
                if (s_last) begin open_symbols=0; packets_in=packets_in+1; end
            end
            if (flush_valid && flush_ready) begin
                flushes=flushes+1;
                if (s_valid && s_ready) $fatal(1, "flush must win input arbitration");
                if (open_symbols != 0) begin
                    expected_last[tail-1]=1;
                    open_symbols=0; packets_in=packets_in+1;
                end else empty_flushes=empty_flushes+1;
            end
            if (m_valid) begin
                checks=checks+1;
                if (^m_count === 1'bx || m_count == 0 || m_count > MAX_RUN)
                    $fatal(1, "invalid output count");
            end
            if (m_valid && m_ready) begin
                token_symbol[tokens]=m_symbol;
                token_count[tokens]=m_count;
                token_last[tokens]=m_last;
                tokens=tokens+1;
                for (k=0; k<m_count; k=k+1) begin
                    checks=checks+1;
                    if (head >= tail) $fatal(1, "unexpected output symbol");
                    if (m_symbol !== expected_symbol[head])
                        $fatal(1, "decode mismatch at %0d", head);
                    if (expected_last[head] !== (m_last && k==m_count-1))
                        $fatal(1, "packet boundary mismatch at %0d", head);
                    head=head+1; decoded=decoded+1;
                end
                if (m_last) packets_out=packets_out+1;
            end
            held = m_valid && !m_ready;
            held_symbol=m_symbol; held_count=m_count; held_last=m_last;
            previous_input=s_valid && s_ready;
        end
    end

    task automatic send(input byte symbol, input bit last, input integer gaps);
        integer tries;
        begin
            repeat(gaps) @(negedge clk);
            @(negedge clk); s_valid=1; s_symbol=symbol; s_last=last;
            tries=0;
            do begin
                @(posedge clk); tries=tries+1;
                if (tries>1000) $fatal(1,"input handshake timeout");
            end while (!s_ready);
            @(negedge clk); s_valid=0; s_last=0;
        end
    endtask

    task automatic continuous_run(input byte symbol, input integer size);
        integer i, tries;
        begin
            @(negedge clk); s_valid=1; s_symbol=symbol;
            for(i=0;i<size;i=i+1) begin
                s_last=(i==size-1); tries=0;
                do begin
                    @(posedge clk); tries=tries+1;
                    if(tries>1000) $fatal(1,"continuous source timeout");
                end while(!s_ready);
                @(negedge clk);
            end
            s_valid=0; s_last=0;
        end
    endtask

    task automatic flush_packet;
        integer tries;
        begin
            @(negedge clk); flush_valid=1; tries=0;
            do begin
                @(posedge clk); tries=tries+1;
                if (tries>1000) $fatal(1,"flush handshake timeout");
            end while (!flush_ready);
            @(negedge clk); flush_valid=0;
        end
    endtask

    task automatic drain(input string label_text);
        integer tries;
        begin
            tries=0;
            while (head!=tail || m_valid) begin
                @(negedge clk); tries=tries+1;
                if (tries>1000)
                    $fatal(1,"%s: missing output; decoded=%0d accepted=%0d", label_text,head,tail);
            end
            repeat(4) @(negedge clk);
            $display("CASE PASS %s tokens=%0d",label_text,tokens);
        end
    endtask

    task automatic token_is(input integer index, input byte symbol,
                            input integer count, input bit last);
        begin
            checks=checks+1;
            if (index>=tokens || token_symbol[index]!==symbol ||
                token_count[index]!=count || token_last[index]!==last)
                $fatal(1,"literal token mismatch index=%0d want=(%h,%0d,%b)",index,symbol,count,last);
        end
    endtask

    task automatic token_total(input integer want);
        begin
            checks=checks+1;
            if (tokens!=want) $fatal(1,"token total got=%0d want=%0d",tokens,want);
        end
    endtask

    task automatic reset_now;
        begin
            @(negedge clk); rst=1; s_valid=0; flush_valid=0;
            @(posedge clk); #1;
            checks=checks+1; resets=resets+1;
            if (m_valid !== 0) $fatal(1,"synchronous reset did not clear output valid");
            @(negedge clk); rst=0;
        end
    endtask

    initial begin
        $display("RLE TEST CW=%0d MAX=%0d SOURCE_SEED=1a2b3c4d SINK_SEED=9e3779b9",CW,MAX_RUN);
        repeat(3) @(negedge clk);
        rst=0; m_ready=1;

        // First RED assertion: accepting one final symbol must produce one token.
        mark=tokens; send(8'h41,1,0); drain("single final symbol");
        token_total(mark+1); token_is(mark,8'h41,1,1);

        mark=tokens; continuous_run(8'h40,2*MAX_RUN+1);
        drain("continuous valid long run"); token_total(mark+3);
        token_is(mark,8'h40,MAX_RUN,0); token_is(mark+1,8'h40,MAX_RUN,0);
        token_is(mark+2,8'h40,1,1);

        mark=tokens;
        for(n=0;n<2*MAX_RUN+3;n=n+1) send(8'h42,n==2*MAX_RUN+2,0);
        drain("long repeated run");
        token_is(mark,8'h42,MAX_RUN,0);
        token_is(mark+1,8'h42,MAX_RUN,0);
        // CW=1 has three additional singleton tokens; other widths one count=3.
        if(CW>1) begin token_total(mark+3); token_is(mark+2,8'h42,3,1); end
        else begin token_total(mark+5); token_is(mark+4,8'h42,1,1); end

        mark=tokens;
        for(n=0;n<12;n=n+1) send(n+8'h50,n==11,0);
        drain("frequent symbol changes"); token_total(mark+12);
        for(n=0;n<12;n=n+1) token_is(mark+n,n+8'h50,1,n==11);

        mark=tokens;
        for(n=0;n<MAX_RUN;n=n+1) send(8'h61,n==MAX_RUN-1,0);
        drain("exact maximum at packet end");
        token_total(mark+1); token_is(mark,8'h61,MAX_RUN,1);

        mark=tokens;
        for(n=0;n<MAX_RUN;n=n+1) send(8'h62,0,0);
        send(8'h62,1,0); drain("maximum then same final symbol");
        token_total(mark+2); token_is(mark,8'h62,MAX_RUN,0); token_is(mark+1,8'h62,1,1);

        mark=tokens;
        for(n=0;n<MAX_RUN;n=n+1) send(8'h63,0,0);
        send(8'h64,1,0); drain("maximum then different final symbol");
        token_total(mark+2); token_is(mark,8'h63,MAX_RUN,0); token_is(mark+1,8'h64,1,1);

        mark=tokens; send(8'h65,0,0); send(8'h66,1,0);
        drain("symbol change plus last"); token_total(mark+2);
        token_is(mark,8'h65,1,0); token_is(mark+1,8'h66,1,1);

        mark=tokens; m_ready=0; send(8'h70,1,0);
        repeat(6) @(negedge clk);
        checks=checks+1;
        if (!m_valid || !m_last || m_symbol!=8'h70 || m_count!=1)
            $fatal(1,"final output must assert valid before ready");
        repeat(12) @(negedge clk);
        m_ready=1; drain("stalled final run"); token_total(mark+1);

        mark=tokens; flush_packet(); drain("empty flush no-op"); token_total(mark);
        mark=tokens; send(8'h71,0,0); flush_packet(); drain("flush closes pending run");
        token_total(mark+1); token_is(mark,8'h71,1,1);

        mark=tokens;
        for(n=0;n<MAX_RUN;n=n+1) send(8'h72,0,0);
        flush_packet(); drain("flush exact maximum");
        token_total(mark+1); token_is(mark,8'h72,MAX_RUN,1);

        // Both commands presented together: flush closes old packet first.
        mark=tokens; send(8'h73,0,0);
        @(negedge clk); flush_valid=1; s_valid=1; s_symbol=8'h74; s_last=1;
        @(posedge clk);
        if(!flush_ready || s_ready) $fatal(1,"flush priority violation");
        @(negedge clk); flush_valid=0;
        do @(posedge clk); while(!s_ready);
        @(negedge clk); s_valid=0; s_last=0;
        drain("flush and input simultaneous"); token_total(mark+2);
        token_is(mark,8'h73,1,1); token_is(mark+1,8'h74,1,1);

        mark=tokens; send(8'h80,0,0); reset_now();
        send(8'h81,1,0); drain("reset cancels buffered run");
        token_total(mark+1); token_is(mark,8'h81,1,1);

        mark=tokens; m_ready=0; send(8'h82,0,0); send(8'h83,1,0);
        repeat(6) @(negedge clk);
        if(!m_valid) $fatal(1,"reset scenario did not reach stalled output");
        reset_now(); m_ready=1;
        send(8'h84,1,0); drain("reset cancels stalled output and closing run");
        token_total(mark+1); token_is(mark,8'h84,1,1);

        mark=tokens; m_ready=0; send(8'h85,1,0);
        repeat(6) @(negedge clk);
        if(!m_valid || !m_last) $fatal(1,"reset scenario did not reach stalled final token");
        reset_now(); m_ready=1;
        send(8'h86,1,0); drain("reset cancels stalled final token");
        token_total(mark+1); token_is(mark,8'h86,1,1);

        // Random packets use independent symbol-level decode expectations.
        random_ready=1;
        for(p=0;p<200;p=p+1) begin
            source_rng=next_rng(source_rng); length=1+(source_rng%30); value=source_rng[15:8];
            for(n=0;n<length;n=n+1) begin
                source_rng=next_rng(source_rng);
                if(source_rng[2:0]<3) value=source_rng[15:8];
                send(value,(p%3!=0 && n==length-1),source_rng[5:4]);
            end
            if(p%3==0) flush_packet();
        end
        drain("200 random packets with source gaps and backpressure");
        random_ready=0; m_ready=1;
        repeat(10) @(negedge clk);
        checks=checks+1;
        if(accepted!=decoded+canceled || packets_in!=packets_out+canceled_packets ||
           head!=tail || open_symbols!=0 || stall_checks==0 || consecutive_inputs==0)
            $fatal(1,"conservation/coverage failure");
        $display("PASS RLE CW=%0d checks=%0d accepted=%0d decoded=%0d canceled=%0d tokens=%0d packets_in=%0d packets_out=%0d canceled_packets=%0d flushes=%0d empty_flushes=%0d stall_checks=%0d consecutive_inputs=%0d reset_cases=%0d cycles=%0d",CW,checks,accepted,decoded,canceled,tokens,packets_in,packets_out,canceled_packets,flushes,empty_flushes,stall_checks,consecutive_inputs,resets,cycles);
        $finish;
    end
endmodule

9. 继续阅读与接口检查

一阶累加器 PDM可以作为另一种带状态的流式 RTL 阅读例子,但它的输出并不是本文的游程 token。若前级存在多个数据源,可结合四路轮询仲裁检查上游调度,不能让优先的 flush 命令无限压住数据流。

一个具体接入问题:COUNT_W=3 时已收到7个相同符号,但都没带 last,下游同时背压,此时上游提出 flush,应在哪次握手后输出 last=1,哪些信号必须保持不变?

相关推荐
NamoAmitabha1286 小时前
ARM 不懂寄存器,它只懂内存地址--deepseek
fpga开发·zynq--ps
hahaha601619 小时前
3NRD降噪算法FPGA实现8并行度设计
fpga开发
松小白song19 小时前
一次“载电流显示为 0“的排查经历:负值钳位引发的踩坑
fpga开发
yingheceping1 天前
复旦微FPGA开发板厂商TOP5甄选榜:2026选型参考
fpga开发·fpga
NamoAmitabha1281 天前
PL 端的 GPIO IP(AXI GPIO)---PS控制----deepseek
fpga开发·zynq--ps
hahaha60161 天前
3D降噪算法YUV降噪FPGA实现
fpga开发
Hello-FPGA2 天前
基于RFSOC FPGA ZCU216 的CoaXPress 采集demo
fpga开发
9527华安2 天前
FPGA基于ADV7182解码PAL视频,Video Processing Subsystem去隔行,提供6套工程源码和技术支持
fpga开发·音视频·pal·adv7182
hahaha60162 天前
数字图像时域滤波或者3D降噪算法FPGA实现
算法·计算机视觉·fpga开发