SV学习——数据类型(第二章)

verilog有1995和2001版本,而SV是verilog的延伸,SV发布的时候直接就是3.0,之后可能不再存在verilog,而是统一用SV。SV是完全兼容verilog的。verilog文件以.v结尾,SV文件以.sv结尾。语法是全部兼容的,SV是verilog的扩展和延伸。

Verilog中如何区分reg和wire?

verilog中有reg和wire两种数据类型,都是四值逻辑 0,1,x,z。什么时候用wire?verilog作为硬件描述语言,认为哪些变量应被实现为寄存器,哪些变量被实现为线网类型,有利于后端综合。什么时候用reg?

  • wire的本质是一条没有逻辑的连线,输入是什么输出就是什么。wire型数据常用来表示以assign关键字指定的组合逻辑信号。默认初始值为z(高阻状态)
  • reg型表示的寄存器类型,用于always模块内被赋值的信号,必须定义为reg型,代表触发器,常用于时序逻辑电路,(也可以用于always块表示的组合逻辑)reg相当于存储单元,默认初始值是x(未知状态)

从仿真的角度来说,HDL语言面对的是编译器(如Modelsim等),相当于软件思路。

  1. wire对应于连续赋值,如assign
  2. reg对应于过程赋值,如always,initial

从综合的角度来说,HDL语言面对的是综合器(如DC等),要从电路的角度来考虑。

  1. wire型的变量综合出来一般是一根导线;
  2. reg变量在always块中有两种情况:
  • always后的敏感表中是(a or b or c)形式的,也就是不带时钟边沿的,综合出来是组合逻辑
  • always后的敏感表中是(posedge clk)形式的,也就是带边沿的,综合出来一般是时序逻辑,会包含触发器(Flip-Flop)

总结:

  • 使用assign连续赋值语句,必须是wire类型,wire只能用在assign中,只能用于组合逻辑
  • 在always语句块中,只能是reg类型,综合出来可能是组合逻辑,也可能是时序逻辑。
  • 在initial语句块中,只能是reg类型

SV中的数据类型

如何理解SV中的logic类型?

SV中logic为四值逻辑,0,1,x,z。logic可以在大多数情况下,代替reg和wire类型,也就是说logic类型的变量既可以用assign连续赋值,也可以出现在always和initial语句块中。

在验证过程中,主要是驱动和连接硬件模块,不用去考虑变量被综合成什么,使用logic可以不用考虑是用reg还是wire,减少了出错的机会。

但是对于有多个驱动的情况,不能使用logic类型,只能使用wire类型。logic类型的变量只能接受单一驱动。

logic如果有多个驱动会怎样?

会出现error!

新的数据类型

sv中引入了双状态数据类型,可以用来减少内存使用量,提升仿真性能。

四值逻辑:0,1,x,z  默认为x,               integer,reg,logic,wire,tri

二值逻辑:0,1  默认为0,                          byte,shortint,int,longint,bit

有符号类型: byte,shortint,int,longint,integer

无符号类型: bit,logic,reg,wire,tri

四值逻辑转化成二值逻辑,x,z会变为0。

定宽数组

一维定宽数组

int a[3:0]        //声明一个宽度为4,名为a的数组,建议用这种方式命名
int a[0:3]        //a这个数组里面的元素是int类型
int a[4]          //也可以这样写  SV中允许只给出数组宽度的便捷声明方式 
//这里一定要注意,以上三种方式虽然声明的数组大小是一样的,但是数组的迭代顺序是不同的!!!
//如果是a[3:0],用foreach迭代时,先从a[3]开始,那么后续切片也要写成左边大,右边小的形式
//如果是a[0:3],用foreach迭代时,是从a[0]开始的,如果切片写成a[2:1],就会报错!!!
//如果是a[4],与a[0:3]是等价的

bit [31:0] array [2:0]
logic [7:0] arrayb [3:0]

 如何区分变量的声明和数组的声明?首先不管声明什么?变量有变量名,数组有数组名,如果名字后面有位宽,那么声明的是个数组,名字前面的位宽是这个数组中每个数的位宽。

 其实非合并数组和memory的声明非常类似,memory本质上是寄存器数组。不过memory声明的是reg类型的。

Q:如果没有声明数据的位宽,默认存放数据的位宽是1吗?

A: 这和声明的数据类型有关,比如int默认是32位的,bit不写就是单bit

这里一定要注意,以上三种方式虽然声明的数组大小是一样的,但是数组的迭代顺序是不同的!!!
如果是a[3:0],用foreach迭代时,先从a[3]开始,那么后续切片也要写成左边大,右边小的形式
如果是a[0:3],用foreach迭代时,是从a[0]开始的,如果切片写成a[2:1],就会报错!!!
如果是a[4],与a[0:3]是等价的

   

   

定宽数组的赋值问题
  • 首先定宽数组的赋值,不能直接写在module中,要放在initial begin或者过程块中
  • 给数组中一个元素赋值,不需要用  ‘{}  的形式,直接写数
  • 数组切片的方向要和数组声明的方向一致,否则切片失败
  • 切片数组可以用 ’{} 赋值
  • 定宽数组赋值时,为了和连接运算符区分,在前面加了一个单引号,连接运算符是连接多个bit位,最后返回的是一个数,而 '{} 里面放的是多个数的集合。

多维定宽数组

int array [7:0][3:0]   //声明8行4列的二维数组,前面是行,后面是列
int array3 [8][4]       //紧凑型声明
array [7][3]           //设置最后一个元素
array[0][0]array[0][1]array[0][2]array[0][3]
array[1][0]array[1][1]array[1][2]array[1][3]
array[2][0]array[2][1]array[2][2]array[2][3]
array[3][0]array[3][1]array[3][2]array[3][3]
array[4][0]array[4][1]array[4][2]array[4][3]
array[5][0]array[5][1]array[5][2]array[5][3]
array[6][0]array[6][1]array[6][2]array[6][3]
array[7][0]array[7][1]array[7][2]array[7][3]

问题:数组的命名方式对数组有什么影响?

关于bit和byte

  • Bit 意为"位"或"比特",是计算机运算的基础单位;Byte 意为"字节",是计算机文件大小的基本计算单位;
  • 1 bit 就是1位二进制数,比如 1 或者 0;1 Byte 就是 1 个字节,1 个字节是由 8 个二进制位组成的。比如1111111,00000000等。
  • Bit 只能代表一个内容,(0或者1,其中的一个),1 Byte 代表 256 个内容 = 2 的 8 次方。
  • 一个 bit 就是一个二进制数的最小单元,就像我们说我们能够拥有的最小金额的钱就是一分一样,只不过比特是在二进制数中罢了。如果将 8 个 0 或者 1 组合在一起,例如 0110 1010(在中间加一个空格是为了看起来清晰明了),就可以说它们是八个比特或者说一个字节。

常量数组

用一个单引号加大括号来初始化数组

int ascend[4] = '{0,1,2,3};     //对四个元素进行初始化
int descend[5];                //定义宽度为5的数组


descend = '{4,3,2,1,0};         //对5个元素赋值
descend[0:2] = '{5,6,7};        //对前三个元素赋值
ascend = '{4{8}};               //四个值全都为8
descend = '{9,8,default:1};     //剩下的缺省元素设置默认值
 

对合并数组和非合并数组的理解

首先一定要清楚什么样的命名方式是合并数组?什么样的是非合并数组?

数组索引,先定是哪一个抽屉,然后看是第几个字节组,然后看数的第几位 

在数组名之后的是这个数组里可以放几个数据,数组名之前的位宽表示每个数据是几位的!注意:合并数组和非合并数组的声明方式是不一样的

非合并数组的声明


/***********************************************
//非合并数组
bit [7:0] b_unpack[3]            //数组里有三个数,每个数的位宽是8
//所以,数组就像抽屉

b_unpack[2][3]                  //索引时先索引是第几个抽屉,然后再看数的第几位

对于非合并数组,就像在抽屉里放东西,规定了有三个抽屉,可以放三个数,每个抽屉最长可以放32位宽的数,但是现在放进来的数,每个只有8位宽,三个数各占一个抽屉,而且都没有把抽屉填满。非合并数组的方式,会浪费空间,抽屉有很大一部分没用到,

合并数组的声明

希望既可以把它当作为一个整体来访问,也可以把它分解成更小的单元。它既可以用作数组,也可以当成单独的数据,与非合并数组不同的是,它的存放方式是连续的比特集合,中间没有闲置的空间。在非合并数组中,字的低位用来存放数据,高位则不使用。

 bytes是一个具有4个字节的合并数组,以单字形式存放

bit[3:0][7:0] bytes;                //4个字节组装成32bit
bytes = 32'hCafe_Dada;
$displayh (bytes,,                  //显示所有的32比特
           bytes[3],,               //最高字节CA
           bytes[3][7];             //最高比特位“1”

合并/非合并数组(mixed arrays)的声明

  • barray则是一个具有3个类似bytes元素的数组
  • 使用一个下标可以得到一个字的数据barray[0],
  • 使用两个下标,可以得到一个字节的数据barray[0][3],
  • 使用三个下标,可以访问到单个比特位barray[0][1][6]。
  • 数组声明中在变量名后面指定了数组的大小,barray[3],这个维度是非合并的,所以在使用该数组时至少要有一个下标。
bit[3:0][7:0]barray[3]         //合并:3x32比特
bit[31:0] 1w = 32'h0123_4567   //字
bit[7:0][3:0] nibbles;         //合并数组
barray [0] = 1w;               //把1w的值给了barray[0]
barray [0][3] = 8'h01;          
barray [0][1][6] = 1'b1;
nibbles = barray[0]
bit [3:0][7:0] barray[3];          //每个抽屉里放了四个8位宽的数,有三个这样的抽屉

//数组名前面的声明必须都是[mab:lab],不能是[size]


合并数组和非合并数组的选择

  • 需要和标量相互转换时,使用合并数组会非常方便
  • 如果需要等待数组中的变化,则必须使用合并数组,当测试平台需要通过存储器数据的变化来唤醒时,会想到使用@操作符,但这个操作符只能用于标量或者合并数组,

数组操作,for和foreach

一维数组的循环遍历

initial begin
    bit [31:0] src[5], dst[5];
    for (int i=0;i<$size(src);i++)       //for就是正常的for循环
        src[i] = i;
    foreach (dst[j])
        dst[j] = src[j]*2;               //foreach自动遍历数组中的所有元素
end

多维数组的循环遍历

int md[2][3] = '{'{0,1,2},'{3,4,5}};
initial begin
    $display("initial value:");
    foreach (md[i,j])                
//foreach对多维的遍历并不是写成md[i][j],而是写在一个方括号里,用逗号隔开
       $display("md[%0d][%0d] = %0d",i,j,md[i][j])     



$display("new value:");
// 对最后三个元素赋值5
md = '{'{9,8,7},'{3{32'd5}}};
foreach (md[i,j])
    $display("md[%0d][%0d]=%0d",i,j,md[i][j]);
end                         

foreach会遍历原始声明中的数组范围

f[5] = f[4:0],而foreach(f[i]) 等同于for(i=0;i<=4;i++)

对于数组rev[6:2],foreach(rev[i])等同于for(int i = 6; i>=2; i--) 

数组的复制和比较

initial begin
    bit[31:0] src[5] = '{0,1,2,3,4};
              dst[5] = '{5,4,3,2,1};
    if (src == dst)
        $display("src==dst");
    else
        $dspllay("src!=dst");

    //把src所有元素值复制给src
    dst = src;
    //只改变一个元素的值
    src[0] = 5;

//第二种方法,用比较运算符
    $display("src %S dst",(src==dst)? "==":"!=");

//使用数组片段对第1-4个元素进行比较
    $display("src[1:4] %s dst[1:4]",(src[1:4]==dst[1:4])? "==":"!=");
end

同时使用位下标和数组下标

initial begin
    bit[31:0]src[5] = '{5{5}};
    $display src[0],,                //'b101
             src[0][0],,             //'b1
             src[0][2:1];            //'b10      第一个是数组下标,第二个是位下标

动态数组

如何声明?

如何赋值?

定宽数组的宽度在编译时就已经确定了,使用定宽数组时数组的宽度需要达到可以容纳的最大事务量,但实际的事务量可能会远远小于最大值,这就造成了存储空间的浪费,

SV中提供了动态数组,可以在仿真时分配空间或调整宽度,这样就可以使用最小的存储

  • 声明时用 []
  • 分配空间时用new[]
int dyn[],d2[];              //声明动态数组
initial begin
    dyn = new[5]             //分配5个元素
    foreach (dyn[j]) dyn[j] = j ;      //给元素赋值
    d2 = dyn;                // 复制一个动态数组
    d2[0] = 5;
    $display(dyn[0],d2[0]);
    dyn = new[20](dyn);
    dyn = new[100]
    dyn.delete();
end

系统函数$size的返回值是数组的宽度,动态数组中有内建子程序,如delete,size

队列

队列结合了链表和数组的优点。队列的声明使用[$]

  • 队列与链表相似,可以在一个队列中的任何地方增加或删除元素,在性能上的损失比动态数组小的多,因为动态数组需要分配新的数组并复制所有元素的值。
  • 队列与数组相似,可以通过索引实现对任一元素的访问,而不需要像链表那样去遍历目标元素之前的所有元素。
  • 可以在任何地方增加或删除元素,也可以使用索引
int j = 1,
    q2[$] = {3,4}               //{3,4} 队列的声明,赋值不需要使用'
    q[$]  = {0,2,5}             //{0,2,5}

initial begin
    q.insert(1,j);             //{0,1,2,5} 在索引数为1的位置之前插入元素1
    q.insert(3,q2);            //{0,1,2,3,4,5} 在5之前插入一个队列q2
    q.delete(1);               //{0,2,3,4,5} 删除第二个元素       

    q.push_front(6);            // {6,0,2,3,4,5}  在队列最前面插入元素6
    j = q.pop_back;             // {6,0,2,3,4}   j=5
    q.push_back(8);             // {6,0,2,3,4,8} 在队列末尾插入元素8
    j = q.pop_front;            // {0,2,3,4,8}   j=6
    foreach(q[i])
        $display(q[i]);         //打印整个队列
    q.delete();
end
  • 队列中的元素是连续存放的,所以在队列的前面或后面存放数据非常方便
  • 在队列中增加或删除元素需要对已经存在的数组进行搬移以腾出空间。队列越长,越耗时

关联数组

针对超大容量的数组,假如正在对一个有着几个G字节寻址范围的处理器进行建模,在典型的测试中,这个处理器可能只访问了用来存放可执行代码和数据的几百或几千个字节,这种情况下对几个G字节的存储空间进行分配和初始化显然是浪费的。

SV提供了关联数组类型,当对一个非常大的地址空间进行寻址时,SV只为实际写入的元素分配空间。仿真器使用 树或哈希表的形式来存放关联数组。

bit[63:0] assoc[bit[63:0]],idx = 1;           //联合数组的声明

repeat (64) begin
    assoc[idx] = idx;
    idx = idx << 1;
end

//使用foreach遍历数组
foreach (assoc[i])
    $display("assoc($h)=%h",i,assoc[i]);


//找到并删除第一个元素
assoc.first[idx];
assoc.delete(idx);

链表(SV中避免使用)

数组定位方法

在非合并数组中查找数据,可以使用数组定位方法,这些方法的返回值通常是一个队列 

min() 找出数组中最小值

max() 找出数组中最大值

unique() 返回的是数组中具有唯一值的队列,去掉重复的值

int f[6] = '{1,6,2,6,8,6};
int d[] = '{2,4,6,8,10};
int q[$] = {1,3,5,7,tq[$]};

tq = q.min();     // 1 
tq = d.max();     // 10
tq = f.unique;    // {1,6,2,8}

定位方法:find

int d[] = '{9,1,8,3,4,4},tq[$]; 
//找出所有大于3的元素
tq = d.find with (item>3);    //{9,8,4,4}

//等效代码
tq.delete();
foreach (d[i])
    if (d[i]>3)
        tq.push_back (d[i]);

tq = d.find_index with (item>3)      //找到所有大于3的元素的索引值
tq = d.find_first with (item>99)     //找到大于99的第一个元素
tq = d.find_first_index_with (item==8)     //找到等于8的第一个元素的索引值
tq = d.find_last with (item == 4)         //找到值等于4的最后一个元素
tq = d.find_last_index with (item ==4);    //找到等于4的最后一个元素的索引值

条件语句with中,item被称为重复参数,代表了数组中一个单独的元素,item是缺省的名字,你也可以指定别的名字,只要在数组方法的参数列表中列出来就可以了.

以下四种方法的表示是等价的

tq = d.find_first with (item ==4);
tq = d.find_first() with (item == 4);
tq = d.find_first(item) with (item == 4);

tq = d.find_first(x) with (x == 4);

数组排序方法

int d[] = '{9,1,8,3,4,4};
d.reverse();               //翻转数组 {4,4,3,8,1,9}
d.sort();                  //从小到大排列数组 {1,3,4,4,8,9}
d.rsort();                 //从大到小排列数组 {9,8,4,4,3,1}
d.shuffle();               //数组中元素随机排列 {9,8,1,4,3,4}

排序方法修改了原始数组,而数组定位方法新建了一个队列来保存结果

选择存储类型(数组类型的选择)

四大数组类型:定宽数组、动态数组、关联数组、队列

需要考虑的因素:

  • 位宽
  • 内存
  • 读取速度
  • 排序
  • 文件类型,文件结构

数组的位宽

  • 如果数组的索引是连续的整数0,1,2,3等,则应该使用定宽或动态数组
  • 当数组的宽度在编译时已经确定时,选择定宽数组
  • 程序运行时才知道数组宽度选择动态数组
  • 长度可变的数据包使用动态数组
  • 编写处理数组的子程序时,使用动态数组可以处理不同宽度的数组。带有定宽数组参数的子程序只能接受指定宽度的数组。
  • 数组索引不规则时,选择关联数组
  • 关联数组也可以用来对基于内容寻址的存储器建模
  • 仿真过程中元素数目变化很大的数组,队列是一个很好的选择

存储器用量

  • 使用双状态数据类型可以减少仿真时的存储器用量
  • 为了避免浪费空间,应尽量选择32比特的整数倍作为数据位宽

读取速度

  • 还应根据每个时钟周期内的存取次数来选择数据类型
  • 定宽和动态数组都是被存放在连续的存储器空间里,所以访问其中的任何元素耗时都相同,而与数组的大小无关。
  • 队列的读写速度与定宽或动态数组基本相当,队列首尾元素的存取几乎没有任何额外开销,而在队列中插入或删除元素需要对其他元素进行挪移,这时会变得很慢
  • 关联数组需要搜索,存取速度最慢

使用typedef创建新的类型

所谓自定义类型就是用户用一个具有意义的词语去简单的描述一个很长,很难理解的量。并且这个新的词在SV语法里面是没有定义的。

//SV中用户自定义类型
parameter OPSIZE = 8;
typedef reg[OPSIZE-1:0] opreg_t;

opera_t op_a,op_b;

typedef int unsigned uint; 这就是uint对int unsigned的指代;

当再一次使用的时候,uint a,b; //定义的就是int unsigned类型。

typedef bit [31:0] uint;       //32bit双状态无符号数
typedef int unsigned uint;     //等效的定义

使用typedef和使用宏有什么区别? 

宏定义与函数/typedef的区别_宏定义和typedef区别-CSDN博客

结构体类型(struct)

typedef经常和enum和struct一起使用,

enum来创建枚举类型;
struct来创建结构体类型。

// 不使用typedef的结构体声明
struct book 
{
    char author;
    char title;
    int  value;
}; 

// 不使用typedef的struct的例化
struct book  my_book;      例化时必须带着struct  

// 另一种写法是把声明和实例化放在一条语句中写
struct book 
{
    char author;
    char title;
    int  value;
} my_book; 

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

// 使用typedef的struct声明
typedef struct{
    int a;
    byte b;
    shorting c;
    int d;
} book_s;

book_s my_book = '{32'haaaa_aaaad,
                   8'hbb,
                   16'hcccc,
                   32'hdddd_dddd};

$display(my_book.a, my_book.b, my_book.c, my_book.d);    
struct结构体直接定义和用typedef定义有什么区别?  

C语言进阶-struct和typedef struct理解_c语言 struct{int :8}-CSDN博客

struct和typedef struct在c语言中的用法_typedef struct有什么好处-CSDN博客 

C语言结构体(struct)最全的讲解(万字干货)_struct a *b(const char *name)-CSDN博客

首先在不使用typedef的struct声明中,struct book中的book是标记名,这个标记名在struct语法中是可选的。但是非常不提倡写不加标记名的结构体。

但是这种标记名可以省略的规定,就给使用typedef带来了可乘之机,在使用typedef定义struct时,我们就可以省略struct后的标记名,直接用typedef定义的新名字

typedef struct book {
    int a;
    byte b;
    shorting c;
    int d;
} book_s;

这种写法是可以的,但是既然struct后面的标记名规定可以省略,那么使用typedef时就不必再写标记名,否则就是多此一举。 

那么使用typedef声明struct的好处在于例化时,可以不用写struct关键字了

// 不使用typedef 
struct book my_book;

// 使用typedef
book_s my_book;

枚举类型(ENUM)

C语言枚举详解-CSDN博客

到了枚举类型这里,理解思路和struct类型是一样的

// 枚举类型的声明,这下面写的一长串相当于 int, fload这样一个类型
enum DAY {
    MON=1,
    TUE,
    WEN,
    THR,
    FRI,
    SAT,
    SUN
};

// 枚举类型的例化 
enum DAY today = MON;
enum DAY tomorrow;
tomorrow = TUE;

// 把上面两个合并写在一起
enum DAY {
    MON=1,
    TUE,
    WEN,
    THR,
    FRI,
    SAT,
    SUN
} today;

那么使用typedef enum的写法如下:

// 枚举类型的声明,使用typedef的作用就是把写的一长串起了个新名字叫DAY
// 使得这种类型定义变量的时候,可以像int, fload这样的类型名一样简洁
typedef enum 省略标识名{
    MON=1,
    TUE,
    WEN,
    THR,
    FRI,
    SAT,
    SUN
} DAY; 

// 实例化
DAY today;
int a;
float b;

枚举类型的声明,使用typedef的作用就是把写的一长串起了个新名字叫DAY。使得这种类型定义变量的时候,可以像int, fload这样的类型名一样简洁

一个简单的枚举类型

enum {RED,BLUE,GREEN} color;

最简单的枚举类型声明包含了一个常量名称列表以及一个或多个变量,通过这种方式创建的是一个匿名的枚举类型,只能用于这个例子中声明的变量

创建一个署名的枚举类型有利于声明更多新变量,尤其是当这些变量被用作子程序参数或者模块端口时,你需要首先创建枚举类型,然后再创建相应的变量。

对于一个状态机,进行状态跳转的时候,以前可能是利用参数化的方法表明状态:

parameter INIT   =2'b00;
parameter DECODE =2'b01;
parameter IDLE   =2'b10;

现在我们可以用枚举类型的数据变量来更方便的表示不同的状态

typedef enum {INIT, DECODE, IDLE} fsmstate_e;      //默认INIT=0,DECODE=1,IDLE=2
fsmstate_e pstate, nstate;                         // 声明自定义类型变量
……
case (pstate)
    IDLE: nstate = INIT;                           // 数值赋值
    INIT: nstate = DECODE;
    default: nstate = IDLE;
endcase
$display(“Next state is %s”, nstate.name());       // 显示状态名,如果不用.name,则显示编号:0/1/2

使用后缀_e 来表示枚举类型,定义的枚举变量会默认为其中的元素赋值,当然我们也可以自定义其中的数值:

typedef enum {A=1,B,C,X=35,Y,Z} fsmstate_e;//此时A=1,B=2,C=3,X=35,Y=36,Z=37

枚举变量有且仅有一个数值。  

 枚举类型会被默认当成int类型存储,int类型的默认值是0,

枚举类型的转换

枚举类型的默认值是二值的int,可以直接将枚举变量直接赋值给int,但是int不能直接赋值给枚举变量,需要做一个cast转换, 

字符串

所有与字符串相关的处理,都使用string来保存和处理。 与字符串处理相关的还包括字符串的格式化函数。

  • 14
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值