SV学习笔记(四)

OCP Open Closed Principle 开闭原则

随机约束和分布

为什么需要随机?

  • 芯片复杂度越来越高,在20年前 定向测试 已经无法满足验证的需求,而 随机测试 的比例逐渐提高。
  • 定向测试能找到你认为可能存在的缺陷,而 随机测试可以找到连你都没有想到的缺陷 。
  • 随机测试的环境要求比定向测试复杂,它需要激励、参考模型和在线比较。上百次的仿真不再需要人为参与,以此来提高验证效率。
  • 随机测试相对于定向测试可以减少相当多的代码量,而产生的激励较定向测试也更多样

为什么需要约束?

  • 如果随机没有约束,产生有效激励的同时,还 会产生大量的无效激励
  • 通过为随机添加约束,这种 随机自由是一种合法的随机 ,产生有效的测试激励。
  • 约束不是一成不变的,为了获取期望的测试范围或期待的数值范围,约束需要“变形”。
  • 随机的对象不只是一个数据,而是 有联系的变量集合 。通常这些变量集合会被封装在一个数据类里,同时需要类中声明数据之间的约束关系。因此,约束之后要产生一个随机数据的“求解器”,即在满足数据本身和数据之间约束关系的随机数值解。
  • 约束不但 可以指定数据的取值范围 ,还 可以指定各个数值的随机权重分布

我们需要随机什么?

  • 器件配置: 通过寄存器和系统信号
  • 环境配置: 随机化环境,例如合理的时钟和外部反馈信号。
  • 原始输入数据: 例如MCDF数据包的长度、带宽,数据间的顺序。
  • 延时: 握手信号之间的时序关系,例如valid和ready,req和ack之间的时序关系。
  • 协议异常: 如果反馈信号给出异常,那么设计是否可以保持后续数据处理的稳定性。

声明随机变量的类

  • 随机化是为了产生更多可能的驱动,我们倾向于将相关数据有机整理在一个类的同时,也用“rand”关键词来表明它的随机属性。
  • “randc”关键词表示周期性随机,即所有可能的值都赋过值后随机才可能重复,也就好比54张扑克牌抽牌游戏,rand代表每抽完一张放回去才可以下次抽牌,randc代表没抽完一张不需要放回就抽取下一张,如果抽完了,那就全部放回再次同样规则抽取。
  • rand和randc,只能声明类的变量,硬件域以及软件域的局部变量都不可以
  • 随机属性需要配合SV预定义的随机函数std::randomize()使用即通过声明rand变量,并且在后期调用randomize()函数才可以随机化变量。
  • 约束constraint也同随机变量一起在class中声明。
class packet;
    rand bit [31:0] src, dst, data[8];
    randc bit [7:0] kind;

    constraint c {
        src >10;
        src <15;
    }
endclass

//----------------------------------

Packet p;
initial begin
    p = new();
    //assert语句保证randomize成功,否则会报fatal(如果约束冲突,如src>15 and src<10则会随机失败)
    assert (p.randomize()) else $fatal(0, "Packet::randomize failed");
    transmit(p);
end

白话一刻

* `class packet;`:定义一个名为`packet`的类。  
* `rand bit [31:0] src, dst, data[8];`:声明了三个随机变量,分别是`src`(源地址)、`dst`(目标地址)和`data`(一个包含8个元素的数组,用于存储数据)。每个变量的位宽度为32位。  
* `randc bit [7:0] kind;`:声明了一个随机且唯一的变量`kind`,其位宽度为8位。`randc`意味着每次生成的`kind`值都是唯一的,直到所有可能的值都被使用完。  
* `constraint c { ... }`:定义了一个约束`c`,用于限制随机变量的取值范围或关系。这里,它指定了`src`的值必须大于10且小于15。
* `Packet p;`:声明了一个`packet`类型的变量`p`。注意,这里类名`packet`的首字母是大写的,这通常表示它是一个用户定义的类型,而不是SystemVerilog的内建类型。  
* `initial begin ... end`:`initial`块在仿真开始时执行一次。  
	+ `p = new();`:创建一个新的`packet`对象,并将其赋值给变量`p`。  
	+ `assert (p.randomize()) else $fatal(0, "Packet::randomize failed");`:调用`p`的`randomize`方法,该方法会根据类的约束随机设置`p`的成员变量的值。`assert`语句检查`randomize`是否成功。如果`randomize`失败(例如,由于约束冲突),则执行`else`部分的`$fatal`语句,导致仿真终止并输出错误消息。  
	+ `transmit(p);`:调用一个名为`transmit`的函数(这个函数在提供的代码片段中未定义),并将随机化的数据包`p`作为参数传递。

总之,这段代码定义了一个数据包类,并展示了如何创建和随机化这个类的实例,以及如何将这个实例传递给一个函数。这对于在仿真中生成随机化的数据包场景非常有用。

什么是约束

  • 约束表达式的求解是有SV的约束求解器自动完成的
  • 求解器能够选择满足约束的值,这个值是由SV的PRNG(伪随机数发生器)从一个初始值(seed)产生。只要改变种子的值,就可以改变CRT的行为。
  • SV标准定义了表达式的含义以及产生的合法值,但没有规定求解器计算约束的准确顺序。也就是,不同仿真器对于同一个约束类和种子求解出的数值可能不同。
  • 什么可以被约束?SV只能随机化二值数据类型,但数据位可以是二值或四值的,所以无法随机出x值和z值,也无法随机出字符串。
class date;
    rand bit [2:0] month; //note:
    rand bit [4:0] day;
    rand int year;

    constraint c_data {
        month inside {[1:12]};
        day inside {[1:31]};
        year inside {[2010:2030]};}
    }
endclass

请问:month=10,day=31,year=2020此组随机值可以产生吗?

答案:不能,因为month的声明是3位,所以不可能出现数值10,这也是经常会犯的错误,当你约束数据时,一定要与声明数据的位数相匹配。

class stim;
    const bit [31:0] CONGEST_ADDR = 42; //声明常数
    typedef enum {READ, WRITE, CONTROL} stim_e;
    randc stime_e kind;
    rand bit [31:0] len, src, dst;
    bit congestion_test;

    constraint c_stim {
        len < 1000;
        len > 0;

        if(congestion_test) (
            dst inside {[CONGEST_ADDR-100:CONGEST_ADDR+100]};
            src == CONGEST_ADDR;
        ) else (
            src inside {0, [10:20], [100:200]};
        )
    }
endclass

权重分布

  • 关键词dist可以在约束中用来产生随机数值的权重分布,这样某些值的选取机会要大于其他值。
  • dist操作符带有一个值的列表以及相应的权重,中间用 := 或 😕 分开。值和权重可以是常数,也可以是变量。
  • 权重不要百分比表示,权重的和也不必是100。
  • := 操作符表示值的范围内的每一个值的权重是相同的, 😕 操作符表示权重要平均分到范围内的每一个值。
rand int src, dst;

constraint c_dist {
    src dist {0:=40, [1:3]:=60;}
    // src=1, weight=40/220
    // src=2, weight=60/220
    // src=3, weight=60/220
    // src=4, weight=60/220

    dst dist {0:/40, [1:3]:/60;}
    // dst=1, weight=40/100
    // dst=2, weight=20/100
    // dst=3, weight=20/100
    // dst=4, weight=20/100
}

这段代码是使用SystemVerilog语言编写的,用于定义随机数生成的约束。SystemVerilog通常用于硬件描述和验证。下面我将详细解释这段代码:

systemverilog
rand int src, dst;
这行代码声明了两个随机整数变量:src 和 dst。rand 关键字表示这些变量在仿真期间可以被随机化。

systemverilog
constraint c_dist {  
    src dist {0:=40, [1:3]:=60;}  
    dst dist {0:/40, [1:3]:/60;}  
}
这部分定义了一个名为 c_dist 的约束,用于控制 src 和 dst 变量的随机化分布。

对于 src 变量:

systemverilog
src dist {0:=40, [1:3]:=60;}
{0:=40} 表示当 src 变量取值为 0 时,其权重是 40。
{[1:3]:=60} 表示当 src 变量取值为 1、2 或 3 时,其权重是 60。
权重可以理解为生成特定值的概率或可能性。权重越高,生成该值的概率越大。

对于 dst 变量:

systemverilog
dst dist {0:/40, [1:3]:/60;}
{0:/40} 表示当 dst 变量取值为 0 时,其权重是 40。
{[1:3]:/60} 表示当 dst 变量取值为 1、2 或 3 时,其权重是 60。
注意,src 和 dst 的权重分布使用了不同的语法。src 使用了 := 运算符,而 dst 使用了 :/ 运算符。这实际上是一个语法错误,因为在SystemVerilog中,权重分布应该使用统一的语法。通常,你会看到 := 用于指定绝对权重,而 :/ 用于指定相对权重。但在同一个约束中混合使用这两种语法是不正确的。

此外,注释部分解释了每个值的权重分布,但它似乎有些混淆,因为它似乎试图将权重与可能的取值范围相除来得到概率,但这不是正确的解释。实际上,权重是独立的数值,它们会被归一化以表示生成特定值的相对概率。

正确的解释应该是:

对于 src,总权重是 40 + 60 * 3 = 220。因此,src 取值为 0 的概率是 40/220,取值为 1、2 或 3 的概率是 60/220。
对于 dst,由于代码中的语法错误,我们不能直接计算权重分布。如果假设 dst 也使用绝对权重,并且语法被修正为 dst dist {0:=40, [1:3]:=60};,那么总权重将是 100(因为权重被错误地标记为相对权重)。但实际上,dst 的权重分布应该也使用绝对权重,并且总和应该与 src 一致,以便进行正确的概率计算。
为了修复这个问题,你应该选择使用绝对权重或相对权重,并确保 src 和 dst 的权重分布语法一致。如果你使用绝对权重,代码应该类似于:

systemverilog
rand int src, dst;  
  
constraint c_dist {  
    src dist {0:=40, [1:3]:=20}; // 总权重为 80  
    dst dist {0:=40, [1:3]:=20}; // 总权重也为 80,以保持一致性  
}
这样,src 和 dst 就会有相同的权重分布,每个值都有相同的概率被选中。

集合成员和inside

  • inside是常见的约束运算符,表示变量属于某个值的集合,除非还存在其他约束 ,否则随机变量在集合里取值的概率是相等的(集合里也可以是变量)。
  • 可以使用 $ 符指定最大或最小值。
rand int c;
int lo, hi;
constraint c_range{
    c inside {[lo:hi]};
}

//-------------------------------

rand bit [6:0] b;
rand bit [5:0] e;
constraint c_range {
    b inside {[$:4], [20:$]};
    e inside {[$:4], [20:$]};
}

条件约束

可以通过 -> 或者 if-else来让一个约束表达式在特定条件有效。

constraint c_io {
    (i_space_mode) -> addr[31] == 1'b1; //i_space_mode!=0
}

//--------------------------------------

constraint c_io {
    if(i_space_mode) //i_space_mode!=0
        addr[31] == 1'b1;
    else;
}

双向约束

  • 约束块不是自上而下的程序代码,它们是声明性代码,是并行的,所有的约束同时有效。
  • 约束是双向的,这表示它会同时计算所有的随机变量的约束,增加或删除任何一个变量的约束都会直接或间接的影响所有相关的值的选取。
  • 约束块可以声明多个,但是它们仍旧是并行的,如果对同一变量进行约束,取两者约束的交集,也就是两个约束都会生效,与写在一个约束块效果相同。
  • 子类会继承父类的约束。

约束块控制

打开或关闭约束

  • 一个类可以包含多个约束块,可以把不同约束块用于不同测试。

  • 一般情况下,各个约束块之间的约束内容是相互协调不违背的,因此通过随机函数产生的随机数可以找到合适的解。

  • 对于其他情况,例如跟胡不同需求,来选择使能哪些约束块,禁止哪些约束块,可以使用内建函数constraint_mode()打开或者关闭约束。

class packet;
    rand int length;
    constraint c_short {length inside {[1:32];}}
    constraint c_long {length inside {[1000:1032];}}
endclass

//------------------------

packet p;
initial begin
    p =new ();
    //create a long packet by disabling c_short
    p.c_short.constraint_mode(0);
    assert(p.randomize());
    transmit(p);
    //create a short packet by disabling all constraint and then enable only c_short
    p.constraint_mode(0);
    p.c_short.constraint_mode(1);
    assert(p.randomize());
    transmit(p);
end

内嵌约束

  • 伴随着复杂的约束,它们之间会相互作用,最终产生难以预测的结果。用来使能和禁止这些约束的代码也会增加测试的复杂性。
  • 经常增加或修改类的约束也可能会影响整个团队的工作,这需要考虑类的OCP原则(开放封闭原则,也就是哪些对外部开放,哪些不对外开放)。
  • SV允许使用 randomize() with来增加额外的约束,这和在类里增加约束是等效的,但同时要注意类内部约束和外部约束之间应该是协调的,如果出现违背,随机数会求解失败(求解失败,不同的工具报告形式不同,有的是error,有的是warning)。
class packet;
    rand int length;
    constraint c_short {soft length inside {[1:32];}}
endclass

//------------------------

packet p;
initial begin
    p =new ();
    assert(p.randomize() with {
        length inside {[36:46];};
        length != 40;         }
    );
    transmit(p);
end

上述例子中randomize() with{}约束与c_short产生可冲突,那会不会报错呢?

答案是不会,因为c_short约束前加了soft(软约束)关键字,意义就在于当外部或子类的约束发生冲突时,其优先级降低,不会影响外部或子类的约束。

随机函数

pre_randomize 和 post_randomize

  • 有时需要在调用randomize()之前或之后立即执行一些操作,例如在随机前设置一些非随机变量(上下限、条件值、权重),或者在随机化后需要计算数据的误差、分析和记录随机数据等。

  • SV提供两个预定义的void类型函数pre_randomize和post_randomize,用户可以类中定义这两个函数,分别在其中定义随机化前的行为和随机化后的行为。

  • 如果某个类中定义了pre_randomize或post_randomize,那么对象在执行randomize()之前或之后会分别执行这两个函数,所以pre_randomize和post_randomize可以看做是randomize函数的回调函数(callback function)。

和之前学的语言类的知识关联起来了,好像spring框架的前置和后置

系统随机数函数

SV提供了一些常用的系统随机函数,这些系统随机函数可以直接调用来返回随机数值:

  • $random()平均分布,返回32位有符号随机数。
  • $urandom()平均分布,返回32位无符号随机数。
  • $urandom_range()在指定范围内的平均分布。

随机化个别变量

  • 在调用randomize()时可以传递变量的一个子集,这样只会随机化类里的几个变量。
  • 只有参数列表里的变量才会被随机化,其他变量会被当做状态量而不会被随机化。
  • 所有的约束仍然保持有效。
  • 注意:类里所有没有被指定rand的变量也可以作为randomize()的参数而被随机化。
  • 注意:未进行随机化的变量默认初始值为0。
class rising;
    byte low;
    rand byte med, hi;
    constraint up {
        low<med; med<hi;
    }
endclass

//----------------------------------

initial begin
    rising r;
    r =new();
    r.randomize(); //随机化hi和med,不改变low
    r.randomize(med); //只随机化med
    r.randomize(low); //只随机化low
end

数组约束

约束数组的大小

  • 在约束随机标量的同时,我们也可以对随机化数组进行约束。
  • 多数情况下,数组的大小应该给定范围,防止生成过大体积的数组或空数组。
  • 此外,还可以在约束中结合数组的其他方法sum(), product(), and(), or(), 和xor()。
class dyn_size;
    rand logic [31:0] d[];
    constraint d_size {
        d,size() inside {[1:10];};
    }
endclass

约束数组的元素

  • SV可以利用foreach对数组每一个元素进行约束,和直接写出对固定大小数组的每一个元素相比,foreach更简洁。
  • 针对动态数组,foreach更适合于对非固定大小数组中每个元素的约束。
class good_sum5;
    rand uint len[];
    constraint c_len{
        foreach (len[i]) len[i] inside {[1:255]};
        len.sum() < 1024;
        len.size() inside {[1:8]};
    }
endclass

产生唯一元素值的数组

如果想要产生一个随机数组,它的每一个元素值都是唯一的,如果使用randc数组,数组中的每一个元素只会独立的随机化,并不会按照我们期望的使得数组中的元素值是唯一的。

解决方案1:

rand bit [7:0] data;
constraint c_data{
    foreach(data[i])
        foreach(data[j])
            if(i != j) data[i] != data[j];
}

解决方案2:

class randc_data;
    randc bit [7:0] data[64];
endclass

class data_array;
    bit [7:0] data_array [64];

    function void pre_randomize();
        randc_data rcd;
        rcd = new();
        foreach (data_array[i]) begin
            assert(rcd.randomize());
            data_array[i] = rcd.val;
        end
    endfunction
endclass

“randc”关键词表示周期性随机,即所有可能的值都赋过值后随机才可能重复,也就好比54张扑克牌抽牌游戏,rand代表每抽完一张放回去才可以下次抽牌,randc代表没抽完一张不需要放回就抽取下一张,如果抽完了,那就全部放回再次同样规则抽取。

  • 特别示例如下,首先“<=”代表小于等于,其次限定da.size为(3/4/5),实际不可能取到5,原因是da.size的约束体现在“da[i] <= da[i+1]”时,约束的是i和i+1为(3/4/5)。
rand bit [7:0] da[];
constraint c_da {
    da.size() inside {[3:5]};
    foreach(da[i]) da[i] <= da[i+1];
}

随机化句柄数组

  • 随机句柄数组的功能是在调用其所在类的随机函数时,随机函数会随机化数组中的每一个句柄所指向的对象。因此随机句柄数组的声明一定要添加rand来表示其随机化的属性,同时在调用随机函数前要保证句柄数组中的每一个句柄元素都是非悬空的,这需要早随机化之前为每一个元素句柄构建对象。

  • 如果要产生多个随机对象,那么你可能需要建立随机句柄数组。和整数数组不同,你需要在随机化前分配所有的元素,因为在随机求解器不会创建对象。使用动态数组可以按照需要分配最大数量的元素,然后再使用约束减小数组的大小。在随机化时,动态句柄数组的大小可以保持不变或减小,但不能增加。

parameter MAX_SIZE = 10;
class RandStuff;
    bit[1:0] value = 1;
endclass

class RandArray;
    rand RandStuff array[];
    constraint c_array {
        array.size() inside {[1:MAX_SIZE]};
    }
    function new();
        //分配最大容量
        array = new[MAX_SIZE];
        foreach (array[i]) array[i] = new();
    endfunction
endclass

//---------------------------

RandArray ra;
initial begin
    // 构造数组和所有对象
    ra = new();
    // 随机化数组,但可能会减小数组
    assert(ra.randomize());
    foreach(ra.array[i]) $display(ra.array[i].value);
end
  • 问题1:执行ra.randomize() with {array.size=2}时,array[0].value 和 array[0].value分别是多少?

答案都是1,首先value没有加rand,所以randomize不会随机value,仍然保持为1。

  • 问题2:为什么要分配最大容量?

答案是只有创建对象,并且分配最大容量,才能保证随机化时可能会碰到句柄数组悬空,无指向对象,随机会报错。

  • 总结:句柄数组的随机,首先查看句柄指向的对象内有没有rand变量,其次对句柄数组按最大容量进行例化

随机控制

  • 产生事务序列的另一个方法是使用SV的randsequence结构。这对于随机安排组织原子(atomic)测试序列很有帮助。
initial begin
    for (int i=0; i<15; i++) begin
        randsequence (stream)
            stream: cfg_read := 1 | //权重不一样
                    io_read := 2  | //权重不一样
                    mem_read := 5; //权重不一样
            cfg_read: (cfg_read_task;) |
                      (cfg_read_task;) cfg_read;
            mem_read: (mem_read_task;) |
                      (mem_read_task;) mem_read;
            io_read: (io_read_task;) |
                     (io_read_task;) io_read;
        endsequence
    end
end

这是一个伪代码或特定于某个工具(如SystemVerilog)的代码片段,用于描述一种随机序列生成机制。我会逐步解释这段代码的含义。

  • initial begin
    initial 是 SystemVerilog 中的一个块,它在仿真开始时就执行一次。begin … end 是定义该块范围的关键词。

    1. for (int i=0; i<15; i++) begin … end

这是一个循环结构,它将执行其内部的代码块 15 次。每次迭代,i 的值从 0 增加到 14。

    1. randsequence (stream)

randsequence 是 SystemVerilog 中用于描述随机序列的关键字。它允许用户为某个事件流(在这里是 stream)定义多个可能的随机序列。

    1. stream: cfg_read := 1 | io_read := 2 | mem_read := 5;

这定义了三个事件:cfg_read、io_read 和 mem_read,并分别为它们分配了权重。权重决定了这些事件在随机选择时出现的频率。例如,mem_read 事件出现的频率是 cfg_read 的 5 倍,是 io_read 的 2.5 倍。

    1. cfg_read: (cfg_read_task;) | (cfg_read_task;) cfg_read;

这定义了当 cfg_read 事件被选择时要执行的任务或操作。这里,cfg_read_task 被执行,然后事件流可以选择再次进入 cfg_read 状态(通过后面的 cfg_read)或者退出这个状态(因为有两个选择,但只执行一个)。

    1. mem_read: (mem_read_task;) | (mem_read_task;) mem_read;

与 cfg_read 类似,但这里是为 mem_read 事件定义的行为。

    1. io_read: (io_read_task;) | (io_read_task;) io_read;

同样,这是为 io_read 事件定义的行为。

  • 总结:

这段代码描述了一个循环,该循环运行 15 次。在每次迭代中,它都会根据定义的权重随机选择一个事件(cfg_read、io_read 或 mem_read)并执行相应的任务。

每个事件都有自己的任务(cfg_read_task、mem_read_task 和 io_read_task),并且在任务执行完毕后,可以选择再次进入相同的事件或退出。

这种随机性在模拟或测试复杂系统的行为时非常有用,因为它可以模拟多种可能的执行顺序或条件。

  • 我们也可以使用randcase来建立随机决策树,但它带来的问题是没有变量可供追踪调试。
initial begin
    int len;
    randcase:
        1: len = $urandom_range(0,2); //10%
        8: len = $urandom_range(3,5); //80%
        1: len = $urandom_range(6,7); //10%
    endcase
    $display("len=%0d", len);
end
  • 总结:
    • randsequence和randcase是针对轻量级的随机控制的应用。而我们可以通过定义随机类取代上述随机控制的功能,并且由于类的继承性使得后期维护代码时更加方便。
    • randsequence的相关功能我们在协调激励组件和测试用例时,可能会用到。
    • randcase则对应着随机约束中的dist权重约束 + if-else条件约束的组合。

参考资料

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SV Virtual是一种虚拟现实技术,它是一种用于创建虚拟现实环境的软件平台。该技术利用计算机生成的图像和声音来模拟真实世界或想象中的环境。SV Virtual可以用于多种领域,如游戏、娱乐、培训和仿真等。通过使用头戴式显示器和手柄等设备,用户可以沉浸在虚拟环境中,与虚拟对象进行交互和体验。 使用SV Virtual的过程一般包括以下几个步骤: 1. 硬件准备:首先,您需要一台具备一定配置要求的电脑或游戏主机,以及适配器和传感器等设备,如头戴式显示器、手柄或体感设备等。 2. 安装软件:根据您所使用的SV Virtual平台,您需要下载并安装相应的软件。这些软件可以从官方网站或其他可信来源获取。 3. 设置环境:在使用SV Virtual之前,您需要设置一个合适的环境。这包括清理活动空间、设置参考点或传感器位置、调整设备参数等。 4. 创建虚拟环境:使用SV Virtual软件,您可以创建自己想要的虚拟环境。这可以包括模拟现实世界的场景、游戏世界、培训模拟等。 5. 交互和体验:在虚拟环境中,您可以使用头戴式显示器上的显示和音频功能来看到和听到虚拟场景中的内容。您可以使用手柄或体感设备来与虚拟对象进行交互,如移动、抓取、控制等。 总的来说,SV Virtual是一种用于创建虚拟现实环境的软件平台,可以通过头戴式显示器和手柄等设备让用户沉浸在虚拟世界中。使用SV Virtual需要进行硬件准备、软件安装、环境设置和创建虚拟环境等步骤。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TrustZone_Hcoco

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值