본문으로 건너뛰기
code0.HDL

다형성과 매개변수화

Scala 타입 매개변수와 상한을 값 매개변수와 구분하고, 타입 템플릿·설정 객체·선택적 포트·by-name 모듈 인자로 여러 회로를 생성합니다.

이 장에서 배울 내용

  • 타입 매개변수, 연산을 보장하는 상한, 포트 형태를 정하는 gen 템플릿으로 모듈을 작성할 수 있습니다
  • 값 매개변수로 클래스 하나에서 여러 정의를 만들 수 있습니다
  • 모듈을 매개변수로 넘기고 elaboration이 만든 결과를 읽을 수 있습니다
  • 제네릭 본문에 필요한 가장 넓은 상한을 고르고, gen에 하드웨어가 아닌 Chisel 타입을 넘길 수 있습니다
  • require로 검사하는 case class에 매개변수를 묶고, Option으로 포트를 선택 사항으로 만들 수 있습니다
이 장에서 배우는 개념
  • 다형성과 매개변수화
먼저 알아야 할 개념

지금까지의 생성기는 모양이 고정되어 있었습니다. 8비트 카운터, 항목 네 개짜리 표, UInt만 고르는 멀티플렉서처럼 폭과 타입이 소스에 적혀 있었습니다. 실제 설계에서는 같은 구조가 여러 payload와 크기로 반복됩니다. 파이프라인 레지스터는 1바이트를 싣든 패킷 헤더를 싣든 구조가 같고, 누산기는 4비트든 32비트든 같은 회로입니다. 이 장은 공식 Chisel 문서 Explanations의 "Polymorphism and Parameterization"을 다루며, Scala 타입 매개변수와 생성자 인자로 클래스 하나가 회로 여러 개를 생성하는 방법을 배웁니다.

먼저 알아 두면 좋은 것. Bundle과 Vec 장의 Bundle, 하위 모듈과 생성기 장의 모듈 인스턴스화, 레지스터와 step 장의 RegInit과 RegNext, 폭·SInt·산술 장의 폭을 늘리는 덧셈 +&, Mux·when·Wire 장의 Mux를 사용합니다.

이 장의 내용은 모두 elaboration 중에 결정됩니다. 타입 매개변수는 회로가 생기기 전에 Scala 컴파일러가 확정하고, 생성자 인자는 생성기가 실행되는 동안 읽힙니다. 시뮬레이션되는 회로에는 T도 설정 객체도 남지 않습니다. 그 값에 따라 생성기가 만든 포트와 노드만 남습니다.

1. 매개변수의 두 종류

Chisel 클래스를 매개변수화하는 방법은 두 가지이며, 각각 잘못된 값이 검출되는 시점이 다릅니다.

종류예결정 주체잘못된 값을 검출하는 곳
타입 매개변수class Pick[T <: Data](gen: T)인자의 타입을 보고 Scala 컴파일러가 결정Scala 컴파일러의 상한 검사
값 매개변수class Accumulator(cfg: AccConfig)실행 중인 생성기 코드사용자가 작성한 require, elaboration 중

타입 매개변수는 "이 회로는 어떤 타입 계열의 payload에도 동작한다"는 뜻입니다. 값 매개변수는 생성기가 읽는 일반 Scala 값으로, 폭, 기능 스위치, 상수 목록 같은 것입니다.

확인 문제

FIFO 생성기에 설정 네 가지가 필요합니다. 생성자 인자가 아니라 하드웨어 입력 포트여야 하는 것은 무엇일까요?

답

2. 타입 매개변수

2.1 제네릭 함수

Scala 함수는 대괄호 안에 타입 매개변수를 받을 수 있습니다. 함수 본문에서 T는 호출하는 쪽이 전달한 타입을 나타냅니다.

형식

def 이름[T <: 상한](인자1: T, 인자2: T): 반환_타입 = 본문
def swapIf[T <: Data](c: Bool, a: T, b: T): (T, T) =
  (Mux(c, b, a), Mux(c, a, b))
구성 요소의미
[T <: Data]타입 매개변수 T를 선언합니다. <: Data가 상한입니다
a: T, b: T두 인자를 같은 T로 선언합니다
(T, T)같은 타입의 값 두 개를 쌍으로 반환합니다
UInt로 swapIf(sel, x, y) 호출Scala가 T = UInt로 추론합니다. Bundle을 전달하면 그 Bundle 타입이 됩니다

8비트 UInt 5와 8로 호출하면 c가 0일 때 (5, 8)을, 1일 때 (8, 5)를 반환합니다. 본문은 Mux만 쓰므로 같은 함수를 SInt, Bundle, Vec 값에도 사용할 수 있습니다.

주의. 두 인자를 같은 T로 선언해도 하드웨어 형태가 같다는 보장은 없습니다. Scala는 두 인자를 모두 포함하는 가장 구체적인 타입을 T로 추론합니다. swapIf(c, 1.U, -1.S)는 UInt와 SInt의 공통 상위 타입으로 컴파일된 뒤, elaboration 중에 Mux 안에서 can't create Mux with heterogeneous types로 중단됩니다. 폭이 다른 UInt 두 개도 허용되며, 이때 Mux는 넓은 쪽의 폭을 따릅니다. 하드웨어 형태는 Scala 컴파일러가 아니라 elaboration 중에 Chisel이 검사합니다.

2.2 상한

<: 뒤의 타입이 상한입니다. 상한은 T가 될 수 있는 타입의 범위를 정하며, 양쪽에 대한 약속입니다. 본문은 상한이 보장하는 연산만 쓸 수 있고, 호출하는 쪽은 상한 안의 타입만 전달할 수 있습니다.

형식

[T <: Data]    // 모든 Chisel 타입
[T <: Bits]    // 평평한 비트 벡터만: UInt, SInt, Bool
def lsb[T <: Bits](x: T): Bool = x(0)
상한허용하는 타입본문에서 쓸 수 있는 연산
T <: DataUInt, SInt, Bool, Bundle, Vec, 열거형Mux, :=, asUInt, getWidth처럼 모든 Data에 있는 연산
T <: BitsUInt, SInt, Bool위 연산과 x(0), x(3, 0) 같은 비트 추출

T <: Bits로 선언한 lsb는 lsb(5.U)와 lsb(-3.S)에서 모두 1을 반환합니다. 같은 본문을 T <: Data로 선언하면 T does not take parameters 컴파일 오류가 발생합니다. Bundle처럼 비트 번호로 꺼낼 수 없는 Data도 있기 때문입니다.

참고. 상한은 본문에 실제로 필요한 만큼만 좁히세요. 필요 이상으로 좁은 상한은 쓸 수 있는 payload를 컴파일 단계에서 막고, 필요보다 넓은 상한은 본문이 컴파일되지 않습니다.

확인 문제

def msb[T <: ???](x: T): Bool = x(x.getWidth - 1)에서 컴파일되는 가장 넓은 상한은 무엇일까요?

답

2.3 제네릭 모듈과 gen 템플릿

모듈 클래스도 타입 매개변수를 받을 수 있습니다. 그런데 포트를 선언하려면 Scala 타입 T만으로는 부족합니다. 정확한 폭과 필드 구성이 필요하고, Scala 타입 매개변수는 생성기가 실행될 때 값으로 쓸 수 없습니다. 그래서 제네릭 Chisel 클래스는 UInt(8.W)나 new Pixel 같은 Chisel 타입 객체를 gen: T 인자로 함께 받아 템플릿으로 사용합니다.

형식

class 이름[T <: Data](gen: T) extends Module {
  val io = IO(new Bundle {
    val in  = Input(gen)
    val out = Output(gen)
  })
}
class Passthrough[T <: Data](gen: T) extends Module {
  val io = IO(new Bundle {
    val in  = Input(gen)
    val out = Output(gen)
  })
  val tmp = Wire(gen)
  tmp    := io.in
  io.out := tmp
}

// new Passthrough(SInt(6.W))는 6비트 부호 있는 포트를 만든다
gen 사용만들어지는 것
Input(gen), Output(gen)템플릿과 같은 형태의 포트
Wire(gen)템플릿과 같은 형태의 wire
Reg(gen)템플릿과 같은 형태의 레지스터
gen.getWidthelaboration 중에 Scala Int로 얻는 템플릿의 폭

gen을 쓸 때마다 새 하드웨어가 생깁니다. 템플릿 자체는 어디에도 연결되지 않습니다.

주의. gen에는 하드웨어가 아니라 Chisel 타입을 전달해야 합니다. UInt(8.W)와 new Pixel은 타입이고, 0.U(8.W), io.in, Wire는 하드웨어입니다. 리터럴도 Scala 입장에서는 UInt이므로 하드웨어를 전달해도 컴파일은 되지만, elaboration 중에 ExpectedChiselTypeException: 'UInt<8>(0)' must be a Chisel type, not hardware가 발생합니다. 하드웨어 값만 가지고 있다면 chiselTypeOf(io.in)으로 타입을 얻으세요. Wire(chiselTypeOf(io.in))은 io.in과 같은 형태의 wire를 만듭니다.

2.4 예제: 클래스 하나로 두 payload 타입 생성하기

Pick.scala는 2절의 세 가지를 한 번에 사용합니다. 다음 순서로 읽으세요.

  • Pixel은 필드마다 8비트인, 필드 세 개짜리 Bundle입니다.
  • Choose는 제네릭 함수입니다. Bool 하나와 타입 T의 값 두 개를 받아 타입 T의 Mux를 반환합니다.
  • Pick[T <: Data]는 템플릿 gen으로 포트 세 개를 선언합니다.
  • println은 elaboration 중에 실행되며, 전달된 템플릿의 클래스 이름과 폭을 출력합니다.
Pick.scala
import chisel3._

class Pixel extends Bundle {
  val r = UInt(8.W)
  val g = UInt(8.W)
  val b = UInt(8.W)
}

object Choose {
  // 제네릭 함수다. 두 선택지는 같은 타입 T를 공유해야 한다.
  def apply[T <: Data](sel: Bool, whenFalse: T, whenTrue: T): T =
    Mux(sel, whenTrue, whenFalse)
}

class Pick[T <: Data](gen: T) extends Module {
  // gen은 Chisel 타입이며 포트의 형태만 정한다.
  val io = IO(new Bundle {
    val sel = Input(Bool())
    val a   = Input(gen)
    val b   = Input(gen)
    val out = Output(gen)
  })
  // T가 확정된 뒤 인스턴스마다 한 번 실행된다.
  println(s"ELABORATE Pick of ${gen.getClass.getSimpleName}, ${gen.getWidth} bits")
  io.out := Choose(io.sel, io.a, io.b)
}

Tb.scala는 테스트 두 개를 따로 실행합니다. 첫 번째는 Pick(UInt(8.W))를 만들고 a와 b에 17과 99를 인가한 뒤 sel 두 값에 대해 out을 읽습니다. 두 번째는 Pick(new Pixel)을 만들고 두 입력의 필드 여섯 개를 모두 인가한 뒤, 출력을 필드별로 출력하고 Bundle 전체를 하나의 UInt로 pack한 값도 출력합니다.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  // 같은 클래스를 8비트 UInt용으로 생성한다.
  test(new Pick(UInt(8.W))) { dut =>
    dut.io.a.poke(17.U)
    dut.io.b.poke(99.U)
    for (sel <- Seq(0, 1)) {
      dut.io.sel.poke((sel == 1).B)
      println(s"PICK UInt sel=$sel out=${dut.io.out.peek().litValue}")
    }
  }

  // 같은 클래스를 필드 세 개짜리 Bundle용으로 다시 생성한다.
  test(new Pick(new Pixel)) { dut =>
    dut.io.a.r.poke(1.U)
    dut.io.a.g.poke(2.U)
    dut.io.a.b.poke(3.U)
    dut.io.b.r.poke(10.U)
    dut.io.b.g.poke(20.U)
    dut.io.b.b.poke(30.U)
    for (sel <- Seq(0, 1)) {
      dut.io.sel.poke((sel == 1).B)
      val o = dut.io.out
      val packed = o.peek().asUInt.litValue.toString(16)
      println(s"PICK Pixel sel=$sel r=${o.r.peek().litValue} g=${o.g.peek().litValue} b=${o.b.peek().litValue} packed=0x$packed")
    }
  }
}

이 개념 실습하기

다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.

실행하기 전에 예측해 보세요.

  1. 두 ELABORATE 줄이 보고하는 폭은 각각 몇 비트일까요?
  2. sel = 1일 때 각 테스트의 out은 어느 입력일까요?
  3. pack하면 필드가 선언 순서대로 연결되고 첫 필드가 최상위 비트에 놓입니다. r=1, g=2, b=3을 pack한 16진수 값은 무엇일까요?

결과 확인.

출력 줄확인할 내용
ELABORATE Pick of UInt, 8 bits첫 인스턴스는 T = UInt, 8비트 포트로 생성되었습니다
PICK UInt sel=0, sel=117, 99 순서입니다. sel = 1이면 b가 선택됩니다
ELABORATE Pick of Pixel, 24 bits같은 클래스가 24비트 Bundle 포트를 가진 다른 회로를 생성했습니다
PICK Pixel sel=0, sel=1세 필드가 함께 전환됩니다. packed는 0x10203, 0xa141e이며 r이 최상위입니다

각 ELABORATE 줄은 해당 테스트의 시뮬레이션 줄 바로 앞에 나옵니다. 타입 매개변수는 Scala 컴파일 시점에 결정되고, elaboration 중에는 gen이 각 인스턴스의 하드웨어 형태를 정합니다. 생성된 두 회로는 따로 작성한 두 모듈만큼 서로 다릅니다. 시뮬레이션에는 제네릭한 요소가 남지 않습니다.

직접 바꿔 보세요.

  1. class Pick의 상한을 T <: Data에서 T <: Bits로 바꿔 보세요. 컴파일러가 new Pick(new Pixel)에 대해 inferred type arguments [Pixel] do not conform to ... type parameter bounds [T <: chisel3.Bits]를 보고합니다. 프로그램 전체가 먼저 컴파일되어야 하므로 올바른 UInt 테스트도 실행되지 않습니다.
  2. 상한을 되돌리고 첫 번째 테스트를 new Pick(0.U(8.W))로 바꿔 보세요. 컴파일은 되지만 Input(gen)에서 ExpectedChiselTypeException: 'UInt<8>(0)' must be a Chisel type, not hardware로 중단됩니다.

3. 값 매개변수

3.1 생성자 인자와 Scala if

가장 단순한 값 매개변수는 일반 생성자 인자입니다. 생성기는 if와 반복문을 포함한 평범한 Scala 코드로 인자를 읽고, 값에 따라 다른 하드웨어를 만듭니다.

형식

class 이름(매개변수1: Int, 매개변수2: Boolean) extends Module { ... }
class Delay(width: Int, stages: Int) extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(width.W))
    val out = Output(UInt(width.W))
  })
  io.out := (0 until stages).foldLeft(io.in)((d, _) => RegNext(d))
}

new Delay(8, 3)은 8비트 레지스터 세 개를 직렬로 만들고, new Delay(16, 1)은 16비트 레지스터 하나를 만듭니다. width와 stages는 회로 안에 존재하지 않습니다. 생성기가 무엇을 만들지 결정하는 데만 쓰입니다.

확인 문제

new Delay(4, 0)은 무엇을 만들까요?

답

3.2 case class로 매개변수 묶기

매개변수가 여러 개이면 case class로 묶으세요. 매개변수 집합에 이름과 기본값이 생기고, 검사와 파생 값을 한곳에 둘 수 있습니다.

형식

case class 이름(필드1: 타입 = 기본값, 필드2: 타입 = 기본값) {
  require(조건, "메시지")
  val 파생_값 = 식
}
case class BusParams(width: Int = 32, withDebug: Boolean = false) {
  require(width % 8 == 0, s"width must be a multiple of 8, got $width")
  val lanes = width / 8
}
구성 요소의미
width: Int = 32기본값이 있는 필드입니다. BusParams()는 32비트입니다
require(...)객체를 생성할 때 실행되므로, 어떤 모듈이 쓰기도 전에 검사됩니다
val lanes = width / 8모든 모듈이 같은 방식으로 계산하는 파생 값입니다
BusParams(64)lanes는 8입니다
BusParams(12)IllegalArgumentException: requirement failed: width must be a multiple of 8, got 12가 발생합니다

참고. require 메시지에 매개변수 이름과 값을 넣으세요. 여러 모듈을 거친 뒤에 나는 폭 오류보다, 문제의 매개변수를 직접 가리키는 메시지가 훨씬 고치기 쉽습니다.

3.3 Option으로 만드는 선택적 포트

Scala if로 포트의 존재 여부 자체를 정할 수 있습니다. 포트가 있으면 Some(...)으로 감싸고, 없으면 None을 씁니다.

형식

val 포트 = if (조건) Some(IO(Output(타입))) else None
포트.foreach(_ := 값)   // 포트가 있을 때만 연결
val debug = if (p.withDebug) Some(IO(Output(UInt(p.width.W)))) else None
debug.foreach(_ := data)
값효과
Some(IO(...))포트가 생성되고 foreach가 연결합니다
None회로에 포트가 없고 foreach는 아무것도 하지 않습니다

선택적 포트를 다루는 코드는 모듈이든 테스트벤치든 포트가 없는 경우를 처리해야 합니다. foreach, map, isDefined를 쓰고 .get은 쓰지 마세요.

3.4 예제: 설정 객체와 선택적 포트

Accumulator.scala는 3절의 내용을 누산기에 적용합니다. 다음 순서로 읽으세요.

  • AccConfig는 필드 두 개와, 폭이 2보다 작으면 예외를 발생시키는 require로 이루어져 있습니다.
  • Accumulator는 모든 포트와 레지스터 폭에 cfg.width를 씁니다.
  • carry는 cfg.withCarry가 참일 때만 Some(포트)입니다.
  • acc +& io.in은 acc보다 한 비트 넓습니다. 하위 비트는 레지스터로 돌아가고, 최상위 비트는 carry 포트가 있으면 그 포트를 구동합니다.
Accumulator.scala
import chisel3._

// elaboration 매개변수. 하드웨어가 생기기 전에 검사한다.
case class AccConfig(width: Int, withCarry: Boolean) {
  require(width >= 2, s"width must be at least 2, got $width")
}

class Accumulator(cfg: AccConfig) extends Module {
  val io = IO(new Bundle {
    val in  = Input(UInt(cfg.width.W))
    val sum = Output(UInt(cfg.width.W))
  })
  // carry 포트는 설정이 요구할 때만 생성된다.
  val carry = if (cfg.withCarry) Some(IO(Output(Bool()))) else None

  val acc = RegInit(0.U(cfg.width.W))
  // +&는 carry 비트를 유지하고 레지스터는 하위 비트만 저장한다.
  val next = acc +& io.in
  acc := next(cfg.width - 1, 0)
  io.sum := acc
  carry.foreach(_ := next(cfg.width))
}

Tb.scala는 carry 포트가 있는 4비트 설정과 없는 8비트 설정을 차례로 실행합니다. 설정마다 carry 포트가 있는지 출력한 뒤 5, 6, 7을 더하며, 클록 step 전마다 레지스터의 합과 진행 중인 덧셈의 자리올림을 출력합니다.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  val configs = Seq(AccConfig(4, withCarry = true), AccConfig(8, withCarry = false))
  // 설정마다 서로 다른 회로가 elaboration된다.
  for (cfg <- configs) {
    test(new Accumulator(cfg)) { dut =>
      println(s"CONFIG width=${cfg.width} carryPort=${dut.carry.isDefined}")
      for (v <- Seq(5, 6, 7)) {
        dut.io.in.poke(v.U)
        // 없는 포트는 None이므로 테스트벤치는 대시를 출력한다.
        val c = dut.carry.map(_.peek().litValue.toString).getOrElse("-")
        println(s"ADD in=$v sum=${dut.io.sum.peek().litValue} carry=$c")
        dut.clock.step()
      }
      println(s"FINAL sum=${dut.io.sum.peek().litValue}")
    }
  }
}

이 개념 실습하기

다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.

실행하기 전에 예측해 보세요. 4비트 설정의 추적표를 먼저 직접 채운 뒤 비교하세요.

step 전accio.inacc +& io.incarry다음 acc
105505
25611011
311718 (1_0010)12
  1. 8비트 테스트는 carry 값을 몇 번 출력하고, 그 대신 무엇을 출력할까요?
  2. 두 설정의 최종 합은 각각 얼마일까요?

결과 확인.

출력 줄확인할 내용
CONFIG width=4 carryPort=true첫 설정은 carry 포트를 생성했습니다
ADD in=7 sum=11 carry=111 + 7 = 18은 4비트를 넘습니다. carry 포트에 다섯 번째 비트가 나타납니다
FINAL sum=2레지스터는 18의 하위 4비트만 저장했습니다
CONFIG width=8 carryPort=false두 번째 설정에는 carry 포트가 아예 없습니다
8비트의 모든 줄에 carry=-dut.carry가 None이므로 테스트벤치의 map/getOrElse가 대시를 출력합니다
FINAL sum=188비트에는 18이 넘침 없이 들어갑니다

최종 합 2와 18은 같은 세 덧셈을 각각 16과 256으로 나눈 나머지입니다.

직접 바꿔 보세요.

  1. 첫 설정을 AccConfig(1, withCarry = true)로 바꿔 보세요. 회로가 생성되지 않습니다. case class를 생성하는 순간 java.lang.IllegalArgumentException: requirement failed: width must be at least 2, got 1이 발생합니다.
  2. 테스트벤치의 map/getOrElse 줄을 dut.carry.get.peek()로 바꿔 보세요. 4비트 테스트는 통과하지만 8비트 테스트에서 java.util.NoSuchElementException: None.get이 발생합니다.

4. 모듈을 매개변수로 받기

생성기는 다른 모듈을 매개변수로 받을 수도 있습니다. 바깥 모듈에 필요한 포트를 Scala trait로 선언하고, 타입 매개변수의 상한을 BaseModule과 그 trait로 지정합니다. 자식 모듈은 by-name 인자(=> T)로 전달해 부모의 Module(...) 호출 안에서 생성되게 합니다.

형식

class 바깥_모듈[T <: BaseModule with 포트_trait](child: => T) extends Module {
  val inner = Module(child)
}
import chisel3._
import chisel3.experimental.BaseModule

trait TwoInOne { def a: UInt; def b: UInt; def y: UInt }

class Wrap[T <: BaseModule with TwoInOne](child: => T) extends Module {
  val io = IO(new Bundle {
    val a = Input(UInt(8.W)); val b = Input(UInt(8.W)); val y = Output(UInt(8.W))
  })
  val inner = Module(child)
  inner.a := io.a; inner.b := io.b; io.y := inner.y
}
구성 요소의미
trait TwoInOneWrap이 사용하는 포트 목록입니다. 자식 모듈마다 이 포트를 선언합니다
T <: BaseModule with TwoInOneT는 모듈이어야 하고 그 포트를 가져야 합니다
child: => Tby-name 인자입니다. new Add는 미리 실행되지 않고 Module(child) 안에서 실행됩니다
inner.a := io.atrait가 a를 보장하므로 타입 검사를 통과합니다

덧셈 RawModule과 뺄셈 RawModule을 각각 전달하면 new Wrap(new Add)와 new Wrap(new Sub)가 같은 wrapper로 서로 다른 회로를 생성합니다. 입력 9와 4에 대해 각각 13과 5를 출력합니다. RawModule은 암묵적인 클록과 리셋이 없는 모듈입니다. 상한이 그냥 Module이라면 컴파일러가 inner.a를 검사할 수 없습니다.

5. 자주 하는 실수

5.1 gen에 하드웨어 전달하기

// 잘못된 코드: 0.U(8.W)는 하드웨어 리터럴
val p = Module(new Pick(0.U(8.W)))

// 바른 코드: Chisel 타입 전달
val p = Module(new Pick(UInt(8.W)))

잘못된 코드는 컴파일된 뒤 elaboration 중에 ExpectedChiselTypeException: ... must be a Chisel type, not hardware로 실패합니다. 하드웨어 값만 있다면 chiselTypeOf(값)을 전달하세요.

5.2 본문에 비해 너무 넓은 상한

// 잘못된 코드: 모든 Data를 비트 번호로 꺼낼 수는 없음
def lsb[T <: Data](x: T): Bool = x(0)

// 바른 코드: 비트 추출에는 Bits가 필요
def lsb[T <: Bits](x: T): Bool = x(0)

잘못된 코드는 T does not take parameters 컴파일 오류가 발생합니다. 상한은 본문이 쓰는 모든 연산을 보장해야 합니다.

5.3 하드웨어 신호를 Scala if로 분기하기

// 잘못된 코드: io.c는 elaboration 중에 값을 알 수 없는 하드웨어 Bool
io.y := (if (io.c) 1.U else 2.U)

// 바른 코드: 하드웨어 선택은 Mux(또는 when)
io.y := Mux(io.c, 1.U, 2.U)

컴파일러는 type mismatch; found: chisel3.Bool, required: Boolean을 보고합니다. Scala if는 cfg.withCarry처럼 생성기가 실행될 때 이미 정해진 매개변수에 쓰고, Mux와 when은 회로가 동작하는 동안 바뀌는 신호에 씁니다.

5.4 선택적 포트에 .get 호출하기

// 잘못된 코드: 포트를 생략한 설정에서 예외 발생
println(dut.carry.get.peek().litValue)

// 바른 코드: 포트가 없는 경우를 처리
println(dut.carry.map(_.peek().litValue.toString).getOrElse("-"))

None에 .get을 호출하면 java.util.NoSuchElementException: None.get이 발생합니다. 한 설정에서는 동작하고 다른 설정에서만 실패하므로 놓치기 쉬운 버그입니다.

6. 정리

주제핵심
타입 매개변수[T <: 상한]. 각 호출에서 Scala가 인자로부터 T를 추론합니다
상한Data는 모든 Chisel 타입을 허용합니다. Bits는 비트 추출을 허용하는 대신 Bundle을 막습니다
같은 T하드웨어 형태를 보장하지 않습니다. Mux가 elaboration 중에 하드웨어 타입을 검사합니다
gen 템플릿Input, Output, Wire, Reg가 쓰는 Chisel 타입입니다. 하드웨어를 전달하면 안 됩니다
chiselTypeOf하드웨어 값의 Chisel 타입을 얻습니다
값 매개변수일반 Scala 값입니다. 생성기가 실행될 때 if와 반복문으로 읽습니다
case class이름, 기본값, require 검사, 파생 값을 한곳에 둡니다
선택적 포트Some(IO(...)) 또는 None. foreach/map으로 다루고 .get은 쓰지 않습니다
모듈 매개변수T <: BaseModule with trait, by-name 인자 => T로 전달합니다

7. 확인 문제

  1. Pick에는 이미 타입 매개변수 T가 있습니다. 그런데도 gen이 필요한 이유는 무엇일까요?

    정답 보기

    포트를 만들려면 정확한 폭과 필드 구성이 필요한데, Scala 타입 매개변수는 생성기가 실행될 때 값으로 쓸 수 없습니다. gen은 그 정보를 담은 Chisel 타입 객체이므로 Input(gen)이 올바른 형태의 포트를 만들 수 있습니다.

  2. T <: Bits는 Pixel 테스트를 컴파일 단계에서 막지만 Pick(0.U(8.W))는 elaboration 중에야 실패합니다. 왜 다를까요?

    정답 보기

    상한은 Scala의 타입 규칙이고 Pixel은 Bits가 아니므로 컴파일러가 막습니다. 0.U(8.W)의 Scala 타입은 UInt라서 상한을 만족합니다. 그 값이 타입이 아니라 하드웨어라는 사실은 Chisel만 판단할 수 있으며, Input(gen)이 실행될 때 검사됩니다.

  3. 4비트 누산기에서 자리올림을 만드는 노드는 무엇이며, 레지스터보다 한 비트 넓은 이유는 무엇일까요?

    정답 보기

    next = acc +& io.in입니다. +&는 가장 넓은 피연산자보다 한 비트 넓은 결과를 반환하므로 4비트 두 값의 합은 5비트입니다. bit 4가 자리올림이고 bit 3부터 bit 0이 레지스터로 돌아갑니다.

  4. Choose(sel, 1.U, -1.S)는 컴파일될까요? 실행하면 어떻게 될까요?

    정답 보기

    컴파일됩니다. Scala가 UInt와 SInt의 공통 상위 타입을 T로 추론합니다. 그 뒤 elaboration 중에 Mux 안에서 can't create Mux with heterogeneous types로 중단됩니다.

  5. 세 번째 설정으로 AccConfig(6, withCarry = true)를 추가하면 입력 5, 6, 7에 대해 최종 합과 자리올림은 어떻게 나올까요?

    정답 보기

    6비트는 63까지 담으므로 5 + 6 + 7 = 18은 넘치지 않습니다. carryPort=true가 출력되고, 세 줄 모두 자리올림이 0이며, 최종 합은 18입니다.

확인 지점

예제를 실행해 예측과 비교합니다

참고 자료

참고한 자료의 라이선스와 고지