[CSE228A] Lecture 5 - Collections

YumeIroVillain·2023년 8월 10일
0

Chisel 독학

목록 보기
34/44

Lecture 5 - Collections

Scala Seq

  • Scala의 collection library에 있음
  • Seq는 Ordered Collection이고, dafault값은 Immutable임.
  • ()로 인덱싱할 수 있음.
  • List와 Vector를 사용하기를 권장하는 guideline도 있음
  • Seq는 여러 자료형을 담을 수 있음.

Scala Range

  • until 또는 to 구문으로 instantiate 가능
  • START until END 구문으로 < 범위를 구현가능
  • 또한, START to END 구문으로 <= 범위를 구현가능
  • by를 통해, 몇씩 ++ 또는 --할 것인지 지정할 수 있음.

Scala for

  • Range나 Collection같은, iterable object를 순회함
  • iteration 간의 결과를 전달하기 위해 var를 사용하기도 함.

    마지막처럼, ; 및 i,j를 선언하여 두 변수에 대해 순회를 도는 것도 가능하다.
    if(i+j==4)라면, 해당 경우에 대해서만 for을 순회돈다는 얘기이다. 나머지는 continue;이다.

instantiate에 for문을 활용


n회 cycle을 단순지연시키는 모듈을 n을 통해 paramaterize하고,
for문으로 통해 code generate를 하면 아래와 같이 elaborate된다.

  • reg0, reg1이 만들어지는 것을 확인할 수 있다.
  • require을 통해, 코드 실수를 줄일 수 있다.

var과 for문을 통한, 좀 더 간결한 코딩

class DelayNCycles(n: Int) extends Module {
    val io = IO(new Bundle {
        val in  = Input(Bool())
        val out = Output(Bool())
    })
    var lastConn = io.in
    for (i <- 0 until n){
        println(s"${lastConn} = RegNext($lastConn)")
        lastConn = RegNext(lastConn)
    }
    io.out := lastConn
}
println(getVerilog(new DelayNCycles(3)))
  • lastConn = RegNext(lastConn)을 통해,
    Register Chain을 쭉쭉 만들어나가는 예시이다.

test에서의 for문 사용

Random Constraint Verification에서는 못쓰지만,
Manually하게 모든 경우의 수를 조사할때는 충분히 가치가 있다.

test(new CombLogic) { dut =>
    for (a <- Seq(true, false)) {
        for (b <- Seq(true, false)) {
            for (c <- Seq(true, false)) {
                dut.io.a.poke(a.B)
                dut.io.b.poke(b.B)
                dut.io.c.poke(c.B)
                println(s"$a, $b, $c")
                val expected = (a && b) || !c
                dut.io.out.expect(expected.B)
//                 dut.clock.step()
            }
        }
    }
}

실행결과

복습) Chisel printf는 once per cycle마다만 일어난다.

즉,

  • 이 경우, println문은 매번 실행시마다 수행되는데
    정작 모듈의 printf문은 마지막 1회만 수행된다.
    왜냐하면, printf문은 simulation 시 수행되고, 반드시 once per cycle만 수행되기 때문이다.
  • dut.clock.step()이 주석처리되었으므로, simulationdㅔ서는 딱 1사이클만 수행한 셈이 되고 그러므로 printf문은 단 1회만 elaborate 되었다.
  • printf문을 매회 출력하고 싶다면, clock.step()을 계속 해주면 된다.
    이 때, println은 clock.step()을 알빠노한다. 독립적으로 계속 출력한다.
  • clock.step()을 돌리니, 매회 elaborate가 발생하는 것을 확인할 수 있다.
  • 참고로, step(2)로 1회에 2clk씩 건너뛴다면 printf 또한 2회씩 출력된다.

Chisel Vec

  • 앞서 얘기한 range, Seq는 Scala things였다.
  • 용도
    • Mux의 Dynamic Addressing
    • 포트 수의 Parameterize화
  • Vec(요소 수, 자료형) 으로 instance화
  • Reg of Vec는 가능하다.
    Vec of Reg는 불가능하다.
    => (Reg(Vec(수, 자료형))) 만 가능.
  • Reg 대신, Wire로도 가능하다.
    => (Wire(Vec(수, 자료형))) 만 가능.
  • Wire는 In/Out 다 가능하다.
    참고로, Wire 대신 Input도 가능하고, Output도 가능하다.
    이 경우, 단방향으로 고정된다.
  • 아무튼 외우자. Vec는 이니시만 끊는다. 막타가 아니다.

ROM은 VecInit()으로 구현하자

  • result는 Seq로 선언되었다.
  • Seq에 :+ 연산자를 통해, chisel3.bool형 결과를 append했고,
    • 참고로, :는 좌변이 collection이라는 것을 의미하고,
    • +는 append를 의미한다.
  • 완성된 Seq를 VecInit을 통해 단번에 ROM으로 만들었다.
class DivByXTable(x: Int) extends Module {
    val io = IO(new Bundle {
        val in  = Input(UInt(4.W))
        val out = Output(Bool())
    })
    var results = Seq[Bool]()
    for (i <- 0 to 15) {
        results = results :+ (i % x == 0).B
    }
    val table = VecInit(results)
    io.out := table(io.in)
}

Chisel Mem

  • Dynamically Addressable & Mutable 한 memory이다.
  • Backend는 적절한 implement technology를 찾을 것이다.
    • 기본: 0사이클 걸리는 Combinational Read & Synchronous Write Memory(0clk for R and 1clk for W)
    • delay 파라미터 넣을 수 있음
    • SyncReadMem은 0이 아니라 1사이클 Read Delay를 가짐
  • Write Mask를 지원함.
  • Memory port는 명시적/묵시적 모두 가능함.

예시

  • Addressable이라는 뜻은, 말그대로 i_addr와 i_wen, i_data를 가지고 o_data를 가지는 진짜 메모리라는 뜻이다.
class RegFile(nRead: Int) extends Module {
    val io = IO(new Bundle {
        val r0addr = Input(UInt(5.W))
        val r1addr = Input(UInt(5.W))
        val w0addr = Input(UInt(5.W))
        val w0en =   Input(Bool())
        val w0data = Input(UInt(64.W))
        val r0out =  Output(UInt(64.W))
        val r1out =  Output(UInt(64.W))
    })
//     val regs = Mem(32, UInt(64.W))
    val regs = Reg(Vec(32, UInt(64.W)))
    io.r0out := regs(io.r0addr)
    io.r1out := regs(io.r1addr)
    when(io.w0en) {
        regs(io.w0addr) := io.w0data
    }
}

5비트 주소체계를 가지는 각 64비트짜리 Register File 32개를 선언하고자 한다면, 위처럼 쓰면 된다.
elab결과,


module RegFile(
  input         clock,
  input         reset,
  input  [4:0]  io_r0addr,
  input  [4:0]  io_r1addr,
  input  [4:0]  io_w0addr,
  input         io_w0en,
  input  [63:0] io_w0data,
  output [63:0] io_r0out,
  output [63:0] io_r1out
);
`ifdef RANDOMIZE_REG_INIT
  reg [63:0] _RAND_0;
  reg [63:0] _RAND_1;
  reg [63:0] _RAND_2;
  reg [63:0] _RAND_3;
  reg [63:0] _RAND_4;
  reg [63:0] _RAND_5;
  reg [63:0] _RAND_6;
  reg [63:0] _RAND_7;
  reg [63:0] _RAND_8;
  reg [63:0] _RAND_9;
  reg [63:0] _RAND_10;
...
  reg [63:0] _RAND_27;
  reg [63:0] _RAND_28;
  reg [63:0] _RAND_29;
  reg [63:0] _RAND_30;
  reg [63:0] _RAND_31;
`endif // RANDOMIZE_REG_INIT
  reg [63:0] regs_0; // @[cmd32.sc 12:19]
  reg [63:0] regs_1; // @[cmd32.sc 12:19]
  reg [63:0] regs_2; // @[cmd32.sc 12:19]
  reg [63:0] regs_3; // @[cmd32.sc 12:19]
  reg [63:0] regs_4; // @[cmd32.sc 12:19]
  reg [63:0] regs_5; // @[cmd32.sc 12:19]
  reg [63:0] regs_6; // @[cmd32.sc 12:19]
  ...
  reg [63:0] regs_20; // @[cmd32.sc 12:19]
  reg [63:0] regs_21; // @[cmd32.sc 12:19]
  reg [63:0] regs_22; // @[cmd32.sc 12:19]
  reg [63:0] regs_23; // @[cmd32.sc 12:19]
  reg [63:0] regs_24; // @[cmd32.sc 12:19]
  reg [63:0] regs_25; // @[cmd32.sc 12:19]
  reg [63:0] regs_26; // @[cmd32.sc 12:19]
  reg [63:0] regs_27; // @[cmd32.sc 12:19]
  reg [63:0] regs_28; // @[cmd32.sc 12:19]
  reg [63:0] regs_29; // @[cmd32.sc 12:19]
  reg [63:0] regs_30; // @[cmd32.sc 12:19]
  reg [63:0] regs_31; // @[cmd32.sc 12:19]
  wire [63:0] _GEN_1 = 5'h1 == io_r0addr ? regs_1 : regs_0; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_2 = 5'h2 == io_r0addr ? regs_2 : _GEN_1; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_3 = 5'h3 == io_r0addr ? regs_3 : _GEN_2; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_4 = 5'h4 == io_r0addr ? regs_4 : _GEN_3; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_5 = 5'h5 == io_r0addr ? regs_5 : _GEN_4; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_6 = 5'h6 == io_r0addr ? regs_6 : _GEN_5; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_7 = 5'h7 == io_r0addr ? regs_7 : _GEN_6; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_8 = 5'h8 == io_r0addr ? regs_8 : _GEN_7; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  wire [63:0] _GEN_9 = 5'h9 == io_r0addr ? regs_9 : _GEN_8; // @[cmd32.sc 13:14 cmd32.sc 13:14]
...
  wire [63:0] _GEN_59 = 5'h1b == io_r1addr ? regs_27 : _GEN_58; // @[cmd32.sc 14:14 cmd32.sc 14:14]
  wire [63:0] _GEN_60 = 5'h1c == io_r1addr ? regs_28 : _GEN_59; // @[cmd32.sc 14:14 cmd32.sc 14:14]
  wire [63:0] _GEN_61 = 5'h1d == io_r1addr ? regs_29 : _GEN_60; // @[cmd32.sc 14:14 cmd32.sc 14:14]
  wire [63:0] _GEN_62 = 5'h1e == io_r1addr ? regs_30 : _GEN_61; // @[cmd32.sc 14:14 cmd32.sc 14:14]
  assign io_r0out = 5'h1f == io_r0addr ? regs_31 : _GEN_30; // @[cmd32.sc 13:14 cmd32.sc 13:14]
  assign io_r1out = 5'h1f == io_r1addr ? regs_31 : _GEN_62; // @[cmd32.sc 14:14 cmd32.sc 14:14]
  always @(posedge clock) begin
    if (io_w0en) begin // @[cmd32.sc 15:19]
      if (5'h0 == io_w0addr) begin // @[cmd32.sc 16:25]
        regs_0 <= io_w0data; // @[cmd32.sc 16:25]
      end
    end
    if (io_w0en) begin // @[cmd32.sc 15:19]
      if (5'h1 == io_w0addr) begin // @[cmd32.sc 16:25]
        regs_1 <= io_w0data; // @[cmd32.sc 16:25]
      end
    end
    if (io_w0en) begin // @[cmd32.sc 15:19]
      if (5'h2 == io_w0addr) begin // @[cmd32.sc 16:25]
        regs_2 <= io_w0data; // @[cmd32.sc 16:25]
      end
    end
    if (io_w0en) begin // @[cmd32.sc 15:19]
      if (5'h3 == io_w0addr) begin // @[cmd32.sc 16:25]
        regs_3 <= io_w0data; // @[cmd32.sc 16:25]
      end
    end
    if (io_w0en) begin // @[cmd32.sc 15:19]
      if (5'h4 == io_w0addr) begin // @[cmd32.sc 16:25]
        regs_4 <= io_w0data; // @[cmd32.sc 16:25]
      end
    end
...
    if (io_w0en) begin // @[cmd32.sc 15:19]
      if (5'hb == io_w0addr) begin // @[cmd32.sc 16:25]
        regs_11 <= io_w0data; // @[cmd32.sc 16:25]
      end
    end
    if (io_w0en) begin // @[cmd32.sc 1
    ....

처럼 구현된다.
이 부분은 Verilog보다 훨씬 깔끔하다.

  • 읽고쓰는 동작이 동일주소에 동시에 일어난다면, 구현방식에 따라 달라진다.
  • Register File을 Read할 때, 단순히 regs(io.r0addr) 했지만
    • explicit하게 선언하고자 한다면, regs.read(io.r0addr) 해도 무방하다.
    • 마찬가지로, Write 시에도 regs.write(io.w0addr, io.w0data) 해도 무방하다.
    • 결과는 동일할 것이다.

input port가 nRead개인 Register File

class RegFile(nRead: Int) extends Module {
    val io = IO(new Bundle {
        val raddr  = Input(Vec(nRead, UInt(5.W)))
        val w0addr = Input(UInt(5.W))
        val w0en =   Input(Bool())
        val w0data = Input(UInt(64.W))
        val rout = Output(Vec(nRead, UInt(64.W)))
    })
    val regs = Mem(32, UInt(64.W))
    for (i <- 0 until nRead)
        io.rout(i) := regs(io.raddr(i))
    when(io.w0en) {
        regs(io.w0addr) := io.w0data
    }
}

elaborate 결과,

module RegFile(
  input         clock,
  input         reset,
  input  [4:0]  io_raddr_0,
  input  [4:0]  io_raddr_1,
  input  [4:0]  io_raddr_2,
  input  [4:0]  io_raddr_3,
  input  [4:0]  io_w0addr,
  input         io_w0en,
  input  [63:0] io_w0data,
  output [63:0] io_rout_0,
  output [63:0] io_rout_1,
  output [63:0] io_rout_2,
  output [63:0] io_rout_3
);
`ifdef RANDOMIZE_MEM_INIT
  reg [63:0] _RAND_0;
`endif // RANDOMIZE_MEM_INIT
  reg [63:0] regs [0:31]; // @[cmd40.sc 9:19]
  wire [63:0] regs_MPORT_data; // @[cmd40.sc 9:19]
  wire [4:0] regs_MPORT_addr; // @[cmd40.sc 9:19]
  wire [63:0] regs_MPORT_1_data; // @[cmd40.sc 9:19]
  wire [4:0] regs_MPORT_1_addr; // @[cmd40.sc 9:19]
  wire [63:0] regs_MPORT_2_data; // @[cmd40.sc 9:19]
  wire [4:0] regs_MPORT_2_addr; // @[cmd40.sc 9:19]
  wire [63:0] regs_MPORT_3_data; // @[cmd40.sc 9:19]
  wire [4:0] regs_MPORT_3_addr; // @[cmd40.sc 9:19]
  wire [63:0] regs_MPORT_4_data; // @[cmd40.sc 9:19]
  wire [4:0] regs_MPORT_4_addr; // @[cmd40.sc 9:19]
  wire  regs_MPORT_4_mask; // @[cmd40.sc 9:19]
  wire  regs_MPORT_4_en; // @[cmd40.sc 9:19]
  assign regs_MPORT_addr = io_raddr_0;
  assign regs_MPORT_data = regs[regs_MPORT_addr]; // @[cmd40.sc 9:19]
  assign regs_MPORT_1_addr = io_raddr_1;
  assign regs_MPORT_1_
  ...

로, 아주 쉽게 parameterize됨을 확인할 수 있다.
Verilog의 경우, input port 갯수자체는 paramaterize화가 까다로운 것으로 알고있다(packed array로 input을 선언하면 되겠다만...처리하기 툭하면 genvar쓰는 등, 여러모로 귀찮을 것이다)


정리

Seq

  • 가장 자주 사용하게 될 것
  • N개를 Addressing해야하고, generation time에 accessing 할 경우 사용

Vec, Mem

  • Generated HW를 동적 address component로 실물구현하고 싶을 때(대체로 Mux로 구현).
  • SyncReadMem도 사용가능
profile
HW SW 둘다 공부하는 혼종의 넋두리 블로그 / SKKU SSE 17 / SWM 11th

0개의 댓글