Chisel教程——04.Chisel中的控制流

控制流

动机

本系列到目前为止,Chisel中的软硬件之间都有很强的对应关系。但引入控制流之后就不一样了,对软硬件的看法就应该有很大的分歧了。

本节会在生成器软件和硬件中都引入控制流。如果重新连接到一个Chisel连线会怎么样呢?如何让一个多路选择器有两个以上的输入呢?本节会给出这两个问题的答案。

最后连接语义

前面提到,Chisel通过运算符:=来连接组件,由于各种原因,允许发射多条连接语句到相同的组件。

对于赋值之后重新赋值的多条语句,最后一条连接语句会生效:

class LastConnect extends Module {
  val io = IO(new Bundle {
    val in = Input(UInt(4.W))
    val out = Output(UInt(4.W))
  })
  io.out := 1.U
  io.out := 2.U
  io.out := 3.U
  io.out := 4.U
}

//  Test LastConnect
test(new LastConnect) { c => c.io.out.expect(4.U) } // Assert that the output correctly has 4
println("SUCCESS!!") // Scala Code: if we get here, our tests passed!

whenelsewhenotherwise

Chisel中主要的条件逻辑实现是whenelsewhenotherwise结构,一般看起来是这样的:

when(someBooleanCondition) {
  // things to do when true
}.elsewhen(someOtherBooleanCondition) {
  // things to do on this condition
}.otherwise {
  // things to do if none of th boolean conditions are true
}

他们仨必须按照上面的顺序出现,后面的可以省略,可以出现任意多的elsewhen子句。

任何为真的子句条件都会终止执行,子句执行的动作可以是复杂的语句块或者嵌套的when

与Scala中的if不同的是,when中的子句不会返回值。所以不能这么用:

val result = when(squareIt) { x * x}.otherwise {x}

解决方案会在后面的连线(Wire)小节提到。

下面是一个用when构造Chisel条件语句的例子:

import chisel3._
import chisel3.util._

class Max3 extends Module {
  val io = IO(new Bundle {
    val in_a = Input(UInt(16.W))
    val in_b = Input(UInt(16.W))
    val in_c = Input(UInt(16.W))
    val out  = Output(UInt(16.W))
  })

  when(io.in_a >= io.in_b && io.in_a >= io.in_c) {
    io.out := io.in_a
  }.elsewhen(io.in_b >= io.in_c) {
    io.out := io.in_b
  }.otherwise {
    io.out := io.in_c
  }
}

object Max3 extends App {
  println(getVerilogString(new Max3()))
}

输出为:

module Max3(
  input         clock,
  input         reset,
  input  [15:0] io_in_a,
  input  [15:0] io_in_b,
  input  [15:0] io_in_c,
  output [15:0] io_out
);
  wire [15:0] _GEN_0 = io_in_b >= io_in_c ? io_in_b : io_in_c; // @[MyModule.scala 14:34 15:12 17:12]
  assign io_out = io_in_a >= io_in_b & io_in_a >= io_in_c ? io_in_a : _GEN_0; // @[MyModule.scala 12:50 13:12]
endmodule

测试部分:

import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "MyOperators"
  it should "get right results" in {
    test(new Max3) { c =>
      // verify that the max of the three inputs is correct
      c.io.in_a.poke(6.U)
      c.io.in_b.poke(4.U)
      c.io.in_c.poke(2.U)
      c.io.out.expect(6.U)  // input 1 should be biggest
      c.io.in_b.poke(7.U)
      c.io.out.expect(7.U)  // now input 2 is
      c.io.in_c.poke(11.U)
      c.io.out.expect(11.U) // and now input 3
      c.io.in_c.poke(3.U)
      c.io.out.expect(7.U)  // show that decreasing an input works as well
      c.io.in_a.poke(9.U)
      c.io.in_b.poke(9.U)
      c.io.in_c.poke(6.U)
      c.io.out.expect(9.U)  // still get max with tie
    }

    println("SUCCESS!!") // Scala Code: if we get here, our tests passed!
  }
}

测试通过。

Wire结构

前面提到when不能返回值,Chisel中的Wire结构就是解决这个问题的方法之一。Wire定义了一个电路组件,他可以出现在:=操作符的左边或右边。

下面以一个四输入的使用Wire的排序电路为例:

为了演示,首先设计一个小的组合排序器,接受四个数值输入得到四个数值输出,考虑下面的图:

在这里插入图片描述

其中红色的线表示左边的值比右边的小,直接复制值就行,黑色的线表示左边的值比右边的值大时交换两个的值。

图中展示了一系列以row开头的格子,可以使用wire来构造这些格子,作为存放复制或交换的值的地方。

下面是代码实现,目前比较冗长,后面会有缩减的方法:

import chisel3._
import chisel3.util._

class Sort4 extends Module {
  val io = IO(new Bundle {
    val in0 = Input(UInt(16.W))
    val in1 = Input(UInt(16.W))
    val in2 = Input(UInt(16.W))
    val in3 = Input(UInt(16.W))
    val out0 = Output(UInt(16.W))
    val out1 = Output(UInt(16.W))
    val out2 = Output(UInt(16.W))
    val out3 = Output(UInt(16.W))
  })

  val row10 = Wire(UInt(16.W))
  val row11 = Wire(UInt(16.W))
  val row12 = Wire(UInt(16.W))
  val row13 = Wire(UInt(16.W))

  when(io.in0 < io.in1) {
    row10 := io.in0
    row11 := io.in1
  }.otherwise {
    row10 := io.in1
    row11 := io.in0
  }

  when(io.in2 < io.in3 ) {
    row12 := io.in2
    row13 := io.in3
  }.otherwise {
    row12 := io.in3
    row13 := io.in2
  }

  val row20 = Wire(UInt(16.W))
  val row21 = Wire(UInt(16.W))
  val row22 = Wire(UInt(16.W))
  val row23 = Wire(UInt(16.W))

  when(row10 < row13) {
    row20 := row10
    row23 := row13
  }.otherwise {
    row20 := row13
    row23 := row10
  }

  when(row11 < row12) {
    row21 := row11
    row22 := row12
  }.otherwise {
    row21 := row12
    row22 := row11
  }

  when(row20 < row21) {
    io.out0 := row20
    io.out1 := row21
  }.otherwise {
    io.out0 := row21
    io.out1 := row20
  }

  when(row22 < row23) {
    io.out2 := row22
    io.out3 := row23
  }.otherwise {
    io.out2 := row23
    io.out3 := row22
  }
}

object Sort4 extends App {
  println(getVerilogString(new Sort4()))
}

输出的Verilog代码如下:

module Sort4(
  input         clock,
  input         reset,
  input  [15:0] io_in0,
  input  [15:0] io_in1,
  input  [15:0] io_in2,
  input  [15:0] io_in3,
  output [15:0] io_out0,
  output [15:0] io_out1,
  output [15:0] io_out2,
  output [15:0] io_out3
);
  wire [15:0] row10 = io_in0 < io_in1 ? io_in0 : io_in1; // @[MyModule.scala 21:25 22:11 25:11]
  wire [15:0] row11 = io_in0 < io_in1 ? io_in1 : io_in0; // @[MyModule.scala 21:25 23:11 26:11]
  wire [15:0] row12 = io_in2 < io_in3 ? io_in2 : io_in3; // @[MyModule.scala 29:26 30:11 33:11]
  wire [15:0] row13 = io_in2 < io_in3 ? io_in3 : io_in2; // @[MyModule.scala 29:26 31:11 34:11]
  wire [15:0] row20 = row10 < row13 ? row10 : row13; // @[MyModule.scala 42:23 43:11 46:11]
  wire [15:0] row23 = row10 < row13 ? row13 : row10; // @[MyModule.scala 42:23 44:11 47:11]
  wire [15:0] row21 = row11 < row12 ? row11 : row12; // @[MyModule.scala 50:23 51:11 54:11]
  wire [15:0] row22 = row11 < row12 ? row12 : row11; // @[MyModule.scala 50:23 52:11 55:11]
  assign io_out0 = row20 < row21 ? row20 : row21; // @[MyModule.scala 58:23 59:13 62:13]
  assign io_out1 = row20 < row21 ? row21 : row20; // @[MyModule.scala 58:23 60:13 63:13]
  assign io_out2 = row22 < row23 ? row22 : row23; // @[MyModule.scala 66:23 67:13 70:13]
  assign io_out3 = row22 < row23 ? row23 : row22; // @[MyModule.scala 66:23 68:13 71:13]
endmodule

测试代码:

import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
  behavior of "MyOperators"
  it should "get right results" in {
    test(new Sort4) { c =>
      // verify the inputs are sorted
      c.io.in0.poke(3.U)
      c.io.in1.poke(6.U)
      c.io.in2.poke(9.U)
      c.io.in3.poke(12.U)
      c.io.out0.expect(3.U)
      c.io.out1.expect(6.U)
      c.io.out2.expect(9.U)
      c.io.out3.expect(12.U)

      c.io.in0.poke(13.U)
      c.io.in1.poke(4.U)
      c.io.in2.poke(6.U)
      c.io.in3.poke(1.U)
      c.io.out0.expect(1.U)
      c.io.out1.expect(4.U)
      c.io.out2.expect(6.U)
      c.io.out3.expect(13.U)

      c.io.in0.poke(13.U)
      c.io.in1.poke(6.U)
      c.io.in2.poke(4.U)
      c.io.in3.poke(1.U)
      c.io.out0.expect(1.U)
      c.io.out1.expect(4.U)
      c.io.out2.expect(6.U)
      c.io.out3.expect(13.U)
    }
    println("SUCCESS!!")
  }
}

测试通过。

测试器也可以利用Scala中List的特性(Scala Standard Library 2.12.13 - scala.collection.immutable.List (scala-lang.org)),构造各种输入的排列组合:

test(new Sort4) { c =>
  List(1, 2, 3, 4).permutations.foreach { 
    case i0 :: i1 :: i2 :: i3 :: Nil => {
        println(s"Sorting $i0 $i1 $i2 $i3")
        c.io.in0.poke(i0.U)
        c.io.in1.poke(i1.U)
        c.io.in2.poke(i2.U)
        c.io.in3.poke(i3.U)
        c.io.out0.expect(1.U)
        c.io.out1.expect(2.U)
        c.io.out2.expect(3.U)
        c.io.out3.expect(4.U)
    }
    case _ => println(s"Matching Error!")
  }
}
println("SUCCESS!!")

这里的case是Scala的经典用法。前面List(1, 2, 3, 4).permutations.foreach得到的是各种排列的List的迭代器,每个List作为变量输入到后面的偏函数中。后面的case用于匹配模式,i0 :: i1 :: i2 :: i3是构造了一个模式,用于匹配列表中的四个值,功能相当于将列表中的值解包给四个变量。

感谢@浑水摸鱼大师 指出这里代码中的问题(见评论),目前已经修改:

博主,我是从头跟着你的帖子学习下来的,我在复现Sort4代码时,发现你简化版的Test里case i0 :: i1 :: i2 :: i3这行代码一直报错,应该修改为case i0 :: i1 :: i2 :: i3 :: Nil,意思是先创建一个空列表Nil,再逐步向列表头部添加元素i3、i2、i1、i0。

练习

多项式电路

x 2 − 2 x + 1 2 x 2 + 6 x + 3 4 x 2 − 10 x − 5 x^2 - 2x + 1\\ 2x^2 + 6x + 3\\ 4x^2 - 10x -5 x22x+12x2+6x+34x210x5

构造一个电路用于计算上面的多项式,一个选择器输入用于决定计算哪个多项式。

提示:使用Wire来存放 x 2 x^2 x2,这样就只需要计算一次了。

首先用测试驱动开发的思想用Scala写一个模型:

def poly0(x: Int): Int = {x * x - 2 * x + 1}
def poly1(x: Int): Int = {2 * x * x + 6 * x + 3}
def poly2(x: Int): Int = {4 * x * x - 10 * x - 5}

def main(args: Array[String]): Unit = {
    assert(poly0(0) == 1)
    assert(poly1(0) == 3)
    assert(poly2(0) == -5)

    assert(poly0(1) == 0)
    assert(poly1(1) == 11)
    assert(poly2(1) == -11)
}

测试通过,现在把这三个包装为一个参数化的函数,像硬件一样。使用Scala中的if语句,基于输入select选择对应的多项式:

def poly0(x: Int): Int = {x * x - 2 * x + 1}
def poly1(x: Int): Int = {2 * x * x + 6 * x + 3}
def poly2(x: Int): Int = {4 * x * x - 10 * x - 5}

def poly(select: Int, x: Int): Int = {
    if(select == 0) {
        poly0(x)
    } else if(select == 1) {
        poly1(x)
    } else {
        poly2(x)
    }
}

def main(args: Array[String]): Unit = {
    assert(poly(1, 0) == 3)
    assert(poly(1, 1) == 11)
    assert(poly(2, 1) == -11)
}

测试通过。

那我们现在按照原来的思路实现就行了:

import chisel3._
import chisel3.util._

class Polynomial extends Module {
  val io = IO(new Bundle {
    val select = Input(UInt(2.W))
    val x = Input(SInt(32.W))
    val out = Output(SInt(32.W))
  })

  val result = Wire(SInt(32.W))
  val square = Wire(SInt(32.W))

  square := io.x * io.x
  when(io.select === 0.U) {
    result := square - 2.S * io.x + 1.S
  }.elsewhen(io.select === 1.U) {
    result := 2.S * square + 6.S * io.x + 3.S
  }.otherwise {
    result := 4.S * square - 10.S * io.x -5.S
  }

  io.out := result
}

object Polynomial extends App {
  println(getVerilogString(new Polynomial))
}

输出如下:

module Polynomial(
  input         clock,
  input         reset,
  input  [1:0]  io_select,
  input  [31:0] io_x,
  output [31:0] io_out
);
  wire [63:0] _square_T = $signed(io_x) * $signed(io_x); // @[MyModule.scala 14:18]
  wire [34:0] _result_T = 3'sh2 * $signed(io_x); // @[MyModule.scala 16:28]
  wire [31:0] square = _square_T[31:0]; // @[MyModule.scala 12:20 14:10]
  wire [34:0] _GEN_3 = {{3{square[31]}},square}; // @[MyModule.scala 16:22]
  wire [34:0] _result_T_3 = $signed(_GEN_3) - $signed(_result_T); // @[MyModule.scala 16:22]
  wire [34:0] _result_T_6 = $signed(_result_T_3) + 35'sh1; // @[MyModule.scala 16:35]
  wire [34:0] _result_T_7 = 3'sh2 * $signed(square); // @[MyModule.scala 18:19]
  wire [35:0] _result_T_8 = 4'sh6 * $signed(io_x); // @[MyModule.scala 18:34]
  wire [35:0] _GEN_4 = {{1{_result_T_7[34]}},_result_T_7}; // @[MyModule.scala 18:28]
  wire [35:0] _result_T_11 = $signed(_GEN_4) + $signed(_result_T_8); // @[MyModule.scala 18:28]
  wire [35:0] _result_T_14 = $signed(_result_T_11) + 36'sh3; // @[MyModule.scala 18:41]
  wire [35:0] _result_T_15 = 4'sh4 * $signed(square); // @[MyModule.scala 20:19]
  wire [36:0] _result_T_16 = 5'sha * $signed(io_x); // @[MyModule.scala 20:35]
  wire [36:0] _GEN_5 = {{1{_result_T_15[35]}},_result_T_15}; // @[MyModule.scala 20:28]
  wire [36:0] _result_T_19 = $signed(_GEN_5) - $signed(_result_T_16); // @[MyModule.scala 20:28]
  wire [36:0] _result_T_22 = $signed(_result_T_19) - 37'sh5; // @[MyModule.scala 20:42]
  wire [36:0] _GEN_0 = io_select == 2'h1 ? $signed({{1{_result_T_14[35]}},_result_T_14}) : $signed(_result_T_22); // @[MyModule.scala 17:33 18:12 20:12]
  wire [36:0] _GEN_1 = io_select == 2'h0 ? $signed({{2{_result_T_6[34]}},_result_T_6}) : $signed(_GEN_0); // @[MyModule.scala 15:27 16:12]
  assign io_out = _GEN_1[31:0]; // @[MyModule.scala 11:20]
endmodule

测试代码如下:

import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
  def poly0(x: Int): Int = {x * x - 2 * x + 1}
  def poly1(x: Int): Int = {2 * x * x + 6 * x + 3}
  def poly2(x: Int): Int = {4 * x * x - 10 * x - 5}
  def poly(select: Int, x: Int): Int = {
    if(select == 0) {
      poly0(x)
    } else if(select == 1) {
      poly1(x)
    } else {
      poly2(x)
    }
  }
  
  behavior of "MyOperators"
  it should "get right results" in {
    test(new Polynomial) { c =>
      for(x <- 0 to 20) {
        for(select <- 0 to 2) {
          c.io.select.poke(select.U)
          c.io.x.poke(x.S)
          c.io.out.expect(poly(select, x).S)
        }
      }
    }
    println("SUCCESS!!")
  }
}

测试通过。

有限状态机(FSM)

通过卡诺图来优化状态机逻辑太蛋疼了,可以用综合工具来优化,还会产生不直观、不可读的代码。而用Chisel的控制流和最后连接语义来编写是明智的选择。

一个硕士在学习生涯会经历四个状态:空闲(idel),写代码(coding),写论文(writing),毕业(graduate)。这几种状态的转换基于三种条件:咖啡,灵光乍现的idea,来自导师的pressure。一旦毕业了,就会回到空闲状态。

下面的FSM图展示了这些状态和它们之间的转换:

在这里插入图片描述

图中所有没标签的转换(即没有输入)都会返回到空闲状态而不是留在当前状态。输入的优先级是coffee > idea > pressure,所以当处于空闲状态的时候如果同时收到了咖啡和压力,就会转移到写代码状态。

下面是Scala的测试代码:

import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
  def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)

  def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
    var nextState = states("idle")
    if (state == states("idle")) {
      if (coffee) {nextState = states("coding")}
      else if (idea) {nextState = states("idle")}
      else if (pressure) {nextState = states("writing")}
    } else if (state == states("coding")) {
      if (coffee) {nextState = states("coding")}
      else if (idea || pressure) {nextState = states("writing")}
    } else if (state == states("writing")) {
      if (coffee || idea) {nextState = states("writing")}
      else if (pressure) {nextState = states("grad")}
    }
    nextState
  }

  (0 until states.size).foreach{ state => assert(gradLife(state, false, false, false) == states("idle")) }
  assert(gradLife(states("writing"), true, false, true) == states("writing"))
  assert(gradLife(states("idle"), true, true, true) == states("coding"))
  assert(gradLife(states("idle"), false, true, true) == states("idle"))
  assert(gradLife(states("grad"), false, false, false) == states("idle"))
}

目前还没学习到时序逻辑,所以这里的当前状态就用一个到模块的输入来表示,下一个状态就用一个输出来表示,跟上面的gradLife功能一致。现在用Chisel来实现。

Chisel提供了一个方便的状态机映射函数,叫做Enum,为了使用这些状态,将他们当作UInt字面值来使用。

注意,硬件上判断相等与否用的是三个等号===

实现如下:

import chisel3._
import chisel3.util._

class GradLife extends Module {
  val io = IO(new Bundle {
    val state = Input(UInt(2.W))
    val coffee = Input(Bool())
    val idea = Input(Bool())
    val pressure = Input(Bool())
    val nextState = Output(UInt(2.W))
  })

  val idle :: coding :: writing :: grad :: Nil = Enum(4)

  io.nextState := idle
  when(io.state === idle) {
    when(io.coffee) {
      io.nextState := coding
    }.elsewhen(io.idea) {
      io.nextState := idle
    }.elsewhen(io.pressure) {
      io.nextState := writing
    }
  }.elsewhen(io.state === coding) {
    when(io.coffee) {
      io.nextState := coding
    }.elsewhen(io.idea || io.pressure) {
      io.nextState := writing
    }
  }.elsewhen(io.state === writing) {
    when(io.coffee || io.idea) {
      io.nextState := writing
    }.elsewhen(io.pressure) {
      io.nextState := grad
    }
  }
}

object GradLife extends App {
  println(getVerilogString(new GradLife))
}

输出的Verilog代码如下:

module GradLife(
  input        clock,
  input        reset,
  input  [1:0] io_state,
  input        io_coffee,
  input        io_idea,
  input        io_pressure,
  output [1:0] io_nextState
);
  wire [1:0] _GEN_0 = io_pressure ? 2'h2 : 2'h0; // @[MyModule.scala 15:16 21:29 22:20]
  wire [1:0] _GEN_1 = io_idea ? 2'h0 : _GEN_0; // @[MyModule.scala 19:25 20:20]
  wire [1:0] _GEN_2 = io_coffee ? 2'h1 : _GEN_1; // @[MyModule.scala 17:21 18:20]
  wire [1:0] _GEN_3 = io_idea | io_pressure ? 2'h2 : 2'h0; // @[MyModule.scala 15:16 27:40 28:20]
  wire [1:0] _GEN_4 = io_coffee ? 2'h1 : _GEN_3; // @[MyModule.scala 25:21 26:20]
  wire [1:0] _GEN_5 = io_pressure ? 2'h3 : 2'h0; // @[MyModule.scala 15:16 33:29 34:20]
  wire [1:0] _GEN_6 = io_coffee | io_idea ? 2'h2 : _GEN_5; // @[MyModule.scala 31:32 32:20]
  wire [1:0] _GEN_7 = io_state == 2'h2 ? _GEN_6 : 2'h0; // @[MyModule.scala 15:16 30:36]
  wire [1:0] _GEN_8 = io_state == 2'h1 ? _GEN_4 : _GEN_7; // @[MyModule.scala 24:35]
  assign io_nextState = io_state == 2'h0 ? _GEN_2 : _GEN_8; // @[MyModule.scala 16:27]
endmodule

测试代码如下:

import chisel3._
import chiseltest._
import org.scalatest.flatspec.AnyFlatSpec

class MyModuleTest extends AnyFlatSpec with ChiselScalatestTester {
  def states = Map("idle" -> 0, "coding" -> 1, "writing" -> 2, "grad" -> 3)

  def gradLife (state: Int, coffee: Boolean, idea: Boolean, pressure: Boolean): Int = {
    var nextState = states("idle")
    if (state == states("idle")) {
      if (coffee) {nextState = states("coding")}
      else if (idea) {nextState = states("idle")}
      else if (pressure) {nextState = states("writing")}
    } else if (state == states("coding")) {
      if (coffee) {nextState = states("coding")}
      else if (idea || pressure) {nextState = states("writing")}
    } else if (state == states("writing")) {
      if (coffee || idea) {nextState = states("writing")}
      else if (pressure) {nextState = states("grad")}
    }
    nextState
  }
  
  behavior of "MyOperators"
  it should "get right results" in {
    test(new GradLife) { c =>
      for (state <- 0 to 3) {
        for (coffee <- List(true, false)) {
          for (idea <- List(true, false)) {
            for (pressure <- List(true, false)) {
              c.io.state.poke(state.U)
              c.io.coffee.poke(coffee.B)
              c.io.idea.poke(idea.B)
              c.io.pressure.poke(pressure.B)
              c.io.nextState.expect(gradLife(state, coffee, idea, pressure).U)
            }
          }
        }
      }
    }
    println("SUCCESS!!")
  }
}

测试通过。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值