HDL4SE:软件工程师学习Verilog语言(九)

9 Verilog中的层次化结构

前面已经看到,Verilog支持对电路的层次化描述,具体的办法是通过模块中实例化其他模块,形成一个层次化的树状结构,树的根就是顶层模块,也就是一个verilog应用的主模块。FPGA的主模块对应的是FPGA芯片的外部I/O结构,一般是可以配置I/O属性的。ASIC的主模块则对应ASIC管芯的I/O结构。
子模块实例化时可以指定实例化参数,并通过端口与模块中的其他子模块或者线网连接,模块中的其他描述结构最终都编译为基本单元的实例化以及它们之间的连接。

9.1 module及module实例化

模块的定义以module开始,endmodule结束,也可以用macromodule开始,规范中没有给出它们之间的区别,只说由实现来定义,使用时一般可以不加区分地使用,当然如果有的开发工具有特别的说法,应该参考它的手册。
module前面可以有一个attribute列表,我们的HDL4SE的基本单元就是将基本单元的CLSID和软件库信息放在这里。module后面跟一个标识符,作为模块的名称,再后面是可选的实例化参数表,作为模块实例化时的参数表,然后是一个可选的端口表。一般而言端口表是需要出现的,没有任何端口的话,模块有用吗?(可能在仿真意义上有用,实际电路中不引出任何I/O信号的电路是没有意义的,当然电源模块之类除外)。后面就是一系列的模块内部元素,可以包括线网寄存器变量声明,持续性赋值,子模块实例化,always块,initial块等。
端口表定义时,可以只给出一个名字,此时端口的定义是不完整的,在module中还需要给出端口的属性。比如:

module test(a,b,c,d,e,f,g,h);
input [7:0] a; // 指定一个无符号的线网输入端口
input [7:0] b;
input signed [7:0] c;
input signed [7:0] d; // 指定带符号的线网输入端口
output [7:0] e; // 指定无符号线网输出端口
output [7:0] f;
output signed [7:0] g;
output signed [7:0] h; // 指定带符号线网输出端口
wire signed [7:0] b; // 端口b指定为带符号线网,此时宽度必须一致
wire [7:0] c; // 端口c按前面指定为带符号的
reg signed [7:0] f; // 端口f指定为带符号reg类型
reg [7:0] g; // 端口g指定为带符号reg
endmodule

子模块实例化时,一般应该给出实例化参数和端口连接表。这两个部分格式有点象,可以有两种方式给出,一种是按照顺序给出,不需要给出内部参数/端口的名字,按照顺序给出实例化参数/端口对应的实参的表达式即可。比如:

(* 
   HDL4SE="LCOM", 
   CLSID="D5152459-6798-49C8-8376-21EBE8A9EE3C",
   softmodule="hdl4se" 
*) 
module hdl4se_split4
    #(parameter INPUTWIDTH=32, 
      OUTPUTWIDTH0=8, OUTPUTFROM0=0, 
      OUTPUTWIDTH1=8, OUTPUTFROM1=8,
      OUTPUTWIDTH2=8, OUTPUTFROM2=16, 
      OUTPUTWIDTH3=8, OUTPUTFROM3=24
    )
    (
      input [INPUTWIDTH-1:0] wirein,
      output [OUTPUTWIDTH0-1:0] wireout0,
      output [OUTPUTWIDTH1-1:0] wireout1,
      output [OUTPUTWIDTH2-1:0] wireout2,
      output [OUTPUTWIDTH3-1:0] wireout3
    );
  wire [INPUTWIDTH-1:0] wirein;
  wire [OUTPUTWIDTH0-1:0] wireout0;
  wire [OUTPUTWIDTH1-1:0] wireout1;
  wire [OUTPUTWIDTH2-1:0] wireout2;
  wire [OUTPUTWIDTH3-1:0] wireout3;
  assign wireout0 = wirein[OUTPUTWIDTH0+OUTPUTFROM0-1:OUTPUTFROM0];
  assign wireout1 = wirein[OUTPUTWIDTH1+OUTPUTFROM1-1:OUTPUTFROM1];
  assign wireout2 = wirein[OUTPUTWIDTH2+OUTPUTFROM2-1:OUTPUTFROM2];
  assign wireout3 = wirein[OUTPUTWIDTH3+OUTPUTFROM3-1:OUTPUTFROM3];
endmodule

是我们的HDL4SE基本单元hdl4se_split4的模块定义,当然实际实现时这个模块是用c语言的LCOM对象实现的。
实例化时可以用按照顺序给出参数或端口。比如:

hdl4se_split4
     #(32,1,0,1,1,1,2,1,3)
  bReadData_wButton012(
      bReadData,
      wButton0Pressed,
      wButton1Pressed,
      wButton2Pressed,
      wnouse
    );

也可以按照名称给出参数或端口连接:

hdl4se_split4
     #(.INPUTWIDTH(32),
       .OUTPUTWIDTH0(1), .OUTPUTFROM0(0), 
       .OUTPUTWIDTH1(1), .OUTPUTFROM1(1),
       .OUTPUTWIDTH2(1), .OUTPUTFROM2(2), 
       .OUTPUTWIDTH3(1), .OUTPUTFROM3(3)
     ) 
  bReadData_wButton012(
      .wirein(bReadData),
      .wireout0(wButton0Pressed),
      .wireout1(wButton1Pressed),
      .wireout2(wButton2Pressed),
      .wireout3(wnouse)
    );

按照顺序给出时,中间不能缺某个参数或连接,按照名称给出时,顺序可以不按声明的给,如果缺了某个参数,就取模块定义时给的默认值,如果缺了某个端口连接表达式,则表示该端口没有连接(如果是输入端口,表示该端口处于一种悬空状态,输入值是不确定的,这与高阻态有点类似了)。
同一个表(参数表或端口连接表)中只能选择一种方式,按顺序或按名字不能混着用。

9.2 实例化参数

模块在定义时可以定义参数表,每个参数还可以给出默认值,如果实例化时不给出参数的值,则取默认值。参数可以在模块定义开始的参数表中定义,也可以在模块中用parameter开始的语句定义。
模块内部还可以定义局部参数,用localparam开始,局部参数的初始化可以初始化为常数表达式。所谓的常数表达式,是指表达式中可以出现常数值或参数或其他局部参数,当然局部参数不能相互引用。
局部参数和参数的区别在于,参数可以在实例化时重新指定,局部参数则不行,局部参数更像一种内部为了方便使用的表达式,但是局部参数可以出现在常数表达式中。
实例化时子模块的参数有两种办法指定:一种是在实例化参数表中指定,另一种是在模块中用defparam语句指定。如果对同一个参数用了两种指定方法,则以defparam语句的优先。因此模块实例中的参数取值的优先顺序是:defparam指定的表达式,实例化参数表中指定的参数,模块定义中的默认参数。如果模块中定义参数时没有指定类型或位宽,则以指定的为准,如果定义时指定了类型或位宽,则外部指定参数转换为内部的类型和位宽。比如:

module foo(a,b);
 real r1,r2;
 parameter [2:0] A = 3'h2;
 parameter B = 3'h2;
 initial begin
 r1 = A;
 r2 = B;
 $display("r1 is %f r2 is %f",r1,r2);
 end
endmodule // foo
module bar;
 wire a,b;
 defparam f1.A = 5.1415;
 defparam f1.B = 5.1415;
 foo f1(a,b);
endmodule // bar

模块foo中的参数A是指定了位宽的,因此defparam f1.A=5.1415在实例f1中5.1415转换为整数5,然后低两位2‘b01赋予实例f1的参数A。参数B没有指定类型和位宽,于是f1中B就取为实数5.1415。
注意使用defparam时,可以跨层对参数进行设置,也就是可以对子模块的子模块的参数进行设置。
模块内部指定参数的默认值时,可以指定为包括其他参数的一个表达式,然而如果外部重新指定该参数时,就不受这个表达式影响了。(如果要定义外部不能重新指定的参数,可以用局部参数),比如:

parameter
 word_size = 32,
 memory_size = word_size * 4096

定义了两个参数,如果外部指定了参数word_size,没有指定memory_size,则memory_size根据word_size计算出来,如果外面指定了memory_size参数,则以外面的参数为准。

9.3 实例化时端口连接的规则

模块实例化时,模块实例的端口通过端口连接表与外部连接,前面介绍过可以通过顺序连接表或者命名连接表两种方式进行链接。连接时遵守下面的规则:

  1. 实数表达式不能直接与端口连接,如果实数表达式需要连接到端口上,则需要用 r e a l t o b i t s 进 行 转 换 , 可 以 用 realtobits进行转换,可以用 realtobitsbitstoreal转换回来。HDL4SE中不支持实数,一般认为实数是不可综合的。
  2. input和inout类型的端口只能是线网类型,不能是reg类型。
  3. 端口连接到表达式被视为一个持续性赋值,实例化时子模块的inout和output端口只能连接到变量(reg等),线网,或者线网中的某一位(必须由常数表达式指定),或者线网中的一个部分(必须由常数表达式指定),或者上述类型中的连接。
  4. 如果线网连接的端口都是类型uwire,应该在线网无法合并时进行警告。
  5. 如果线网连接的端口是不同类型的线网,则应该按规则进行合并,按照合并后的类型实现功能。HDl4SE不支持除了wire之外的线网类型,因此这里不多介绍,感兴趣的可以参考IEEE. 1364-2005的12.3.10。
    6.signed无法通过端口连接传递,如果类型,位宽不一致,则应该转换为模块内部定义的类型或位宽。

9.4 生成结构

verilog语言中定义了一种生成结构的语言要素,能够根据条件或者循环生成一系列可生成的结构。所谓可生成的结构,就是除了端口声明,参数声明,specify块和specparam声明以外的各种模块内能够出现的语言结构。生成结构提供一种可以根据实例化参数来调整模块内部结构的能力。它有可能让一些重复的结构更加简洁地表达出来。
有两种生成结构,循环和条件,循环生成结构可以让一个生成块在一个模块中多次实例化,条件生成结构包括if-结构和case结构,可以根据条件生成需要的结构。
值得注意的是,生成结构的语法要素是在模型建立过程中进行的,也就是说在综合到目标平台时,生成结构中的结构其实是已经确定的,因此生成结构可以是RTL可综合的。这要求其中的循环控制参数和条件控制参数都必须是常数表达式,就是只包括常数或参数的表达式。这其中还要求循环必须在有限次内终止,循环中用到的循环变量要用genvar特别声明,不能用通用的变量声明。
verilog中可以用generate和endgenerate定义一个所谓的generate区,当然其实也可以不用定义generate区,规范中没有区分用不用generate区的区别。如果使用的话,generate和endgenerate必须配对使用,而且不能嵌套使用。–这么麻烦又没什么用,那还不如不用算了。

9.4.1 循环生成结构

循环生成结构用类似于for循环语句的方式定义,区别在于循环索引变量必须用genvar定义。循环生成结构可以嵌套。注意这个for循环中的初始部分和后面的修改部分必须对genvar声明的变量进行赋值,在初始部分不能出现对循环变量的引用。在循环生成结构的内部,可以使用一个隐形的局部参数,这个参数与循环变量同名。注意嵌套的循环生成结构不能用同名的循环控制变量。另外,由于是局部参数,因此在循环生成结构中不允许对循环控制变量进行赋值。这样做确保循环控制参数的修改只在for语句中进行。
循环生成结构中如果有多个项目,需要用begin end括起来,这个根for循环后面只能有一个语句是一致的。用begin和end括起来的块可以进行命名。
循环生成结构的例子:

module ripple_adder(co, sum, a, b, ci); 
 parameter SIZE = 4; 
 output [SIZE-1:0] sum; 
 output co; 
 input [SIZE-1:0] a, b; 
 input ci; 
 wire [SIZE :0] c; 
 wire [SIZE-1:0] t [1:3]; 
 genvar i; 
 assign c[0] = ci; 
 for(i=0; i<SIZE; i=i+1) begin
 	assign t[1][i] = a[i] ^ b[i];
 	assign sum[i] = t[1][i] ^ c[i];
 	assign t[2][i] = a[i] & b[i];
 	assign t[3][i] = t[1][i] & c[i];
 	assign c[i+1] = t[2][i] | t[3][i];
 end
 assign co = c[SIZE]; 
endmodule

这段代码生成了一个所谓的行波进位加法器(ripple carry adder),它可以在实例化时指定加法器的位宽。如果不使用生成结构,则不同位宽的加法器必须重新写一遍代码,定义不同的module来实现。
当然这样的写法其实也就是为了简洁,有点像软件工程师追求的一样的事情用一段代码来写,其实最终生成的电路是一样的,实例化后该生成多少逻辑还是生成多少逻辑了。

9.4.2 条件生成结构

条件生成结构有两种形式,一种是if-条件生成结构,一种是case-条件生成结构。例如:

module multiplier(a,b,product); 
parameter a_width = 8, b_width = 8; 
localparam product_width = a_width+b_width; 
input [a_width-1:0] a; 
input [b_width-1:0] b; 
output [product_width-1:0] product; 
generate
 if((a_width < 8) || (b_width < 8)) begin
 	CLA_multiplier #(a_width,b_width) u1(a, b, product); 
 end
 else begin
 	WALLACE_multiplier #(a_width,b_width) u1(a, b, product); 
 end
endgenerate
// The hierarchical instance name is mult.u1
endmodule

这段代码定义了一个可以设置宽度的乘法器,根据输入的宽度选择不同的乘法器模块进行实例化。这样做可以将乘法器接口和模块统一起来,外部使用的时候通过一个统一的模块形式使用,内部则根据操作数不同的位宽选择不同的乘法器。
如果选择情况比较多,可以用case-条件生成结构来表达:

generate
 case (WIDTH)
 1: begin: adder // 1-bit adder implementation
 adder_1bit x1(co, sum, a, b, ci); 
 end
 2: begin: adder // 2-bit adder implementation
 adder_2bit x1(co, sum, a, b, ci); 
 end
 default: 
 begin: adder // others - carry look-ahead adder
 adder_cla #(WIDTH) x1(co, sum, a, b, ci); 
 end
 endcase
// The hierarchical instance name is adder.x1 
endgenerate

9.5 分层访问

在verilog语言中,允许命名过的语法结构跨层访问。事实上,每一个标识符都有一个对应的唯一的多级路径名,这个多级的层次命名包括模块名一级下面定义的各种项目,比如任务,命名语句块,以及其中声明的各种名字。这种多级的层次化命名是树状结构,有点像文件系统中的树状目录结构一样,对应每个文件都有一个全路径名。其中每个实例化模块,生成块实例,任务,函数,命名的begin/end块和fork/join块都定义了一个层级。其中的每个声明从顶层模型开始都有一个唯一的全路径名对应。
跟文件系统有点不同的是,verilog中的全路径名是各个层级名字之间用小数点隔开,而不是文件系统中用斜杠隔开。比如在下面的代码中:

module mod (in); 
input in; 
always @(posedge in) begin : keep 
	reg hold; 
	hold = in; 
end
endmodule

module cct (stim1, stim2);
input stim1, stim2;
// instantiate mod
mod amod(stim1), bmod(stim2);
endmodule

module wave;
reg stim1, stim2;
cct a(stim1, stim2); // instantiate cct
initial begin :wave1
	#100 fork :innerwave
		reg hold;
	join
	#150 begin
	stim1 = 0;
	end
end
endmodule

其中的名称构成了下面的一棵树状结构:

wave a wave1 amod bmod innerwave keepa keepb keep keep

其中keepa和keepb是一样的,这里主要是用编辑器绘制树状图有不能重名,因此在图中有区分。
这样,下面这些带层次的名字就可以在整个设计中唯一代表对应的名字了:

wave 
wave.a.bmod
wave.stim1 
wave.a.bmod.in
wave.stim2 
wave.a.bmod.keep
wave.a 
wave.a.bmod.keep.hold
wave.a.stim1 
wave.wave1
wave.a.stim2 
wave.wave1.innerwave
wave.a.amod 
wave.wave1.innerwave.hold
wave.a.amod.in
wave.a.amod.keep
wave.a.amod.keep.hold

在使用时,不必要每次都用全路径名来访问,可以相对于当前位置来访问,同层之间是可以直接访问的,当然也可以访问低层次的名字,比如:

begin
	fork :mod_1
		reg x;
		mod_2.x = 1;
	join
	fork :mod_2
		reg x;
		mod_1.x = 0;
	join
end

也可以访问比自己层级高的模型下的函数名,命名块,线网,参数,端口,任务,变量等,比如:

module a;
	integer i;
	b a_b1();
endmodule

module b;
	integer i;
	c b_c1(), b_c2();
	initial // downward path references two copies of i:
		#10 b_c1.i = 2; // a.a_b1.b_c1.i, d.d_b1.b_c1.i
endmodule

module c;
	integer i;
	initial begin // local name references four copies of i:
		i = 1; // a.a_b1.b_c1.i, a.a_b1.b_c2.i, 
		// d.d_b1.b_c1.i, d.d_b1.b_c2.i
		b.i = 1; // upward path references two copies of i:
		// a.a_b1.i, d.d_b1.i
	end
endmodule

module d;
	integer i;
	b d_b1();
	initial begin // full path name references each copy of i
		a.i = 1; d.i = 5;
		a.a_b1.i = 2; d.d_b1.i = 6;
		a.a_b1.b_c1.i = 3; d.d_b1.b_c1.i = 7;
		a.a_b1.b_c2.i = 4; d.d_b1.b_c2.i = 8;
	end
endmodule

具体的访问规则是scope_name.item_name…。其中的解析规则是:

  • scope是指module, 任务,函数,命名块(复合语句),生成结构块,其中生成结构块如果没有命名,则系统会根据顺序给个名字。
  • 解析从引用分层名字所在的位置开始查找,先与当前位置所在的scope中是否有相应的scope名字与scope_name相同,如果没有找到,则往外一层scope查找,到外一层已经是module还是没有找到,则查找所有的模块名。
  • 在任何一层找到scope_name后,就往下解析各层名字,直到找到对应的对象。
  • 同一个scope中声明的名字不能相同,不同层中声明的名字如果相同,则按照层就近有效原则。这条规则对条件生成结构是个例外,条件生成块中各个条件子句虽然在同一个scope中,但是实际生成时只会有一个条件子句有效,因此名字可以相同(考虑到上层需要统一名字访问,实际上应该相同才是)。
  • 如果一个名字在当前scope中没有,就到上层scope中查找,直到module层面。
    例如:
task t;
	reg s;
	begin : b
 		reg r;
 		t.b.r = 0;// 下面三行实际上访问的是同一个对象
 		b.r = 0;
 		r = 0;
 		t.s = 0;// 下面两行访问同一个对象
 		s = 0;
	end
endtas

9.6 模型建立

verilog源代码编译完成后,就要从顶层模块开始构建整个电路。这个构建过程主要是逐层完成module的模块实例化,主要工作包括建立模块树结构,计算并配置实例化参数,解析分层标识符,建立线网的链接(与模块实例),对生成结构,还要执行各个生成结构,生成对应的模块实例和端口连接。
其中,对于生成结构,这些任务的完成顺序是很重要的,因为生成结构生成的实例化模块和连接跟参数有关系,但是defparam又可能出现在任何地方以最高优先级来修改参数,因此规定建立过程顺序如下:

  • 1.给出一个启动module表作为起点模型表。
  • 2.从起点模型表开始逐层往下建立,直到遇上生成结构则先不执行生成操作,只是计算参数,建立过程中所有的参数都用默认值,实例化参数表或defparam语句计算并设置到实例中。
  • 3.执行前面所有的生成结构生成操作,建立相应的模型和连接。生成过程中可能又会遇上新生成的defparam语句和新的带参数实例化的描述,这个就构成了新的起点模型表,跳到2继续执行,反复执行直到起点模型表为空。
    生成结构的存在有可能导致出现新的结构,从而让前面的defparam中的分层标识符所指的对象出现了变化。比如:
module m;
 	m1 n(); 
endmodule

module m1;
	parameter p = 2;
	defparam m.n.p = 1;
 	initial $display(m.n.p);
 	generate
 		if (p == 1) begin : m
 			m2 n();
 		end
 	endgenerate
endmodule
 
module m2;
 parameter p = 3;
endmodule

在这段代码中,开始的顶层模型是m,模型建立过程中实例化了模型m1作为m的实例n,m1中有参数p,这样defparam m.n.p=1实际上设置的是m1内的参数p的值。然后生成结构中因为p等于1,于是在m1中生成了命名块m以及其下面的实例n(模型m2),在m2中有参数p,此时按照规则,defparam m.n.p指向的应该是命名块m下的实例化n中的参数p,也就是说defparam中的同一个名字的指向对象变了。这种情况是一种错误,应该避免出现。一个defparam在模型建立过程中只会设置一次参数。

9.7 进展报告

至此,verilog语言中我们关心的部分其实都过了一遍了,后面会以例子为主,一方面通过实际的例子,加强学习verilog语言,另一方面,也继续完善HDL4SE系统。软件方面,目前已经能够接受网表级的verilog输入,甚至可以支持在持续性赋值中的部分表达式,原来只能接受纯网表的输入,比如俄罗斯方块的例子,主模块最开始运行起来的verilog代码是:

`include "hdl4secell.v"

/* 用c写的俄罗斯方块控制器 */
(* 
  HDL4SE="LCOM", 
  CLSID="6f8a9aa6-ec57-4734-a183-7871ed57ea95", 
  softmodule="hdl4se" 
*) 
module teris_ctrl
  (
    input  wClk,
    input  nwReset,
    output [31:0] bWriteAddr,
    output [31:0] bWriteData,
    input  [31:0] bKeyPressed
  );
endmodule

module main(
    input wClk, nwReset,
    output wWrite,
    output [31:0] bWriteAddr,
    output [31:0] bWriteData,
    output [3:0]  bWriteMask,
    output wRead,
    output [31:0] bReadAddr,
    input [31:0]  bReadData);

/* 游戏控制器 */
	teris_ctrl ctrl(wClk, nwReset, bWriteAddr, bWriteData, bReadData);

/*我们一直在读按键的状态*/
	hdl4se_const #(1, 1) const_wRead(wRead);
	hdl4se_const #(32, 32'hF000_0000) const_bReadAddr(bReadAddr);
/*总在写*/
    hdl4se_const #(1, 1) const_wWrite(wWrite);

endmodule

现在可以接受下面带表达式的持续性赋值描述了:

`include "hdl4secell.v"

/* 用c写的俄罗斯方块控制器 */
(* 
  HDL4SE="LCOM", 
  CLSID="6f8a9aa6-ec57-4734-a183-7871ed57ea95", 
  softmodule="hdl4se" 
*) 
module teris_ctrl
  (
    input  wClk,
    input  nwReset,
    output        wWrite,
    output [31:0] bWriteAddr,
    output [31:0] bWriteData,
    input  [31:0] bKeyPressed
  );
endmodule

module main(
    input wClk, nwReset,
    output wWrite,
    output [31:0] bWriteAddr,
    output [31:0] bWriteData,
    output [3:0]  bWriteMask,
    output wRead,
    output [31:0] bReadAddr,
    input [31:0]  bReadData);

    wire [31:0] bctrlwaddr;
    assign bWriteAddr = bctrlwaddr + 32'hf000_0000;
/* 游戏控制器 */
	teris_ctrl ctrl(wClk, nwReset,wWrite, bctrlwaddr, bWriteData, bReadData);

/*我们一直在读按键的状态*/
    assign wRead = 1'b1;
    assign bReadAddr = 32'hF000_0000;

endmodule

后面的工作会继续将这个例子中的游戏控制器部分逐步转成全部用verilog语言实现。也会在适当时机引入一些新的例子,逐步过渡到RISC-V CPU核的实现。最终我们会在RISC-V下运行c语言版本的俄罗斯方块游戏!

【请参考】
1.HDL4SE:软件工程师学习Verilog语言(八)
2.HDL4SE:软件工程师学习Verilog语言(七)
3.HDL4SE:软件工程师学习Verilog语言(六)
4.HDL4SE:软件工程师学习Verilog语言(五)
5.HDL4SE:软件工程师学习Verilog语言(四)
6.HDL4SE:软件工程师学习Verilog语言(三)
7.HDL4SE:软件工程师学习Verilog语言(二)
8.HDL4SE:软件工程师学习Verilog语言(一)
9.LCOM:轻量级组件对象模型
10.LCOM:带数据的接口
11.工具下载:在64位windows下的bison 3.7和flex 2.6.4
12.git: verilog-parser开源项目
13.git: HDL4SE项目
14.git: LCOM项目
15.git: GLFW项目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

饶先宏

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

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

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

打赏作者

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

抵扣说明:

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

余额充值