지금까지의 생성기는 모양이 고정되어 있었습니다. 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, Booldef lsb[T <: Bits](x: T): Bool = x(0)| 상한 | 허용하는 타입 | 본문에서 쓸 수 있는 연산 |
|---|---|---|
T <: Data | UInt, SInt, Bool, Bundle, Vec, 열거형 | Mux, :=, asUInt, getWidth처럼 모든 Data에 있는 연산 |
T <: Bits | UInt, 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.getWidth | elaboration 중에 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중에 실행되며, 전달된 템플릿의 클래스 이름과 폭을 출력합니다.
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한 값도 출력합니다.
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 문제에 이 개념을 적용해 보세요.
실행하기 전에 예측해 보세요.
- 두
ELABORATE줄이 보고하는 폭은 각각 몇 비트일까요? sel = 1일 때 각 테스트의out은 어느 입력일까요?pack하면 필드가 선언 순서대로 연결되고 첫 필드가 최상위 비트에 놓입니다.r=1, g=2, b=3을pack한 16진수 값은 무엇일까요?
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
ELABORATE Pick of UInt, 8 bits | 첫 인스턴스는 T = UInt, 8비트 포트로 생성되었습니다 |
PICK UInt sel=0, sel=1 | 17, 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이 각 인스턴스의 하드웨어 형태를
정합니다. 생성된 두 회로는 따로 작성한 두 모듈만큼 서로 다릅니다. 시뮬레이션에는 제네릭한 요소가 남지 않습니다.
직접 바꿔 보세요.
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테스트도 실행되지 않습니다.- 상한을 되돌리고 첫 번째 테스트를
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포트가 있으면 그 포트를 구동합니다.
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 전마다 레지스터의 합과 진행 중인
덧셈의 자리올림을 출력합니다.
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 전 | acc | io.in | acc +& io.in | carry | 다음 acc |
|---|---|---|---|---|---|
| 1 | 0 | 5 | 5 | 0 | 5 |
| 2 | 5 | 6 | 11 | 0 | 11 |
| 3 | 11 | 7 | 18 (1_0010) | 1 | 2 |
- 8비트 테스트는
carry값을 몇 번 출력하고, 그 대신 무엇을 출력할까요? - 두 설정의 최종 합은 각각 얼마일까요?
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
CONFIG width=4 carryPort=true | 첫 설정은 carry 포트를 생성했습니다 |
ADD in=7 sum=11 carry=1 | 11 + 7 = 18은 4비트를 넘습니다. carry 포트에 다섯 번째 비트가 나타납니다 |
FINAL sum=2 | 레지스터는 18의 하위 4비트만 저장했습니다 |
CONFIG width=8 carryPort=false | 두 번째 설정에는 carry 포트가 아예 없습니다 |
8비트의 모든 줄에 carry=- | dut.carry가 None이므로 테스트벤치의 map/getOrElse가 대시를 출력합니다 |
FINAL sum=18 | 8비트에는 18이 넘침 없이 들어갑니다 |
최종 합 2와 18은 같은 세 덧셈을 각각 16과 256으로 나눈 나머지입니다.
직접 바꿔 보세요.
- 첫 설정을
AccConfig(1, withCarry = true)로 바꿔 보세요. 회로가 생성되지 않습니다.case class를 생성하는 순간java.lang.IllegalArgumentException: requirement failed: width must be at least 2, got 1이 발생합니다. - 테스트벤치의
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 TwoInOne | Wrap이 사용하는 포트 목록입니다. 자식 모듈마다 이 포트를 선언합니다 |
T <: BaseModule with TwoInOne | T는 모듈이어야 하고 그 포트를 가져야 합니다 |
child: => T | by-name 인자입니다. new Add는 미리 실행되지 않고 Module(child) 안에서 실행됩니다 |
inner.a := io.a | trait가 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. 확인 문제
-
Pick에는 이미 타입 매개변수T가 있습니다. 그런데도gen이 필요한 이유는 무엇일까요?정답 보기
포트를 만들려면 정확한 폭과 필드 구성이 필요한데, Scala 타입 매개변수는 생성기가 실행될 때 값으로 쓸 수 없습니다.
gen은 그 정보를 담은 Chisel 타입 객체이므로Input(gen)이 올바른 형태의 포트를 만들 수 있습니다. -
T <: Bits는Pixel테스트를 컴파일 단계에서 막지만Pick(0.U(8.W))는elaboration중에야 실패합니다. 왜 다를까요?정답 보기
상한은 Scala의 타입 규칙이고
Pixel은Bits가 아니므로 컴파일러가 막습니다.0.U(8.W)의 Scala 타입은UInt라서 상한을 만족합니다. 그 값이 타입이 아니라 하드웨어라는 사실은 Chisel만 판단할 수 있으며,Input(gen)이 실행될 때 검사됩니다. -
4비트 누산기에서 자리올림을 만드는 노드는 무엇이며, 레지스터보다 한 비트 넓은 이유는 무엇일까요?
정답 보기
next = acc +& io.in입니다.+&는 가장 넓은 피연산자보다 한 비트 넓은 결과를 반환하므로 4비트 두 값의 합은 5비트입니다. bit 4가 자리올림이고 bit 3부터 bit 0이 레지스터로 돌아갑니다. -
Choose(sel, 1.U, -1.S)는 컴파일될까요? 실행하면 어떻게 될까요?정답 보기
컴파일됩니다. Scala가
UInt와SInt의 공통 상위 타입을T로 추론합니다. 그 뒤elaboration중에Mux안에서can't create Mux with heterogeneous types로 중단됩니다. -
세 번째 설정으로
AccConfig(6, withCarry = true)를 추가하면 입력 5, 6, 7에 대해 최종 합과 자리올림은 어떻게 나올까요?정답 보기
6비트는 63까지 담으므로 5 + 6 + 7 = 18은 넘치지 않습니다.
carryPort=true가 출력되고, 세 줄 모두 자리올림이 0이며, 최종 합은 18입니다.