포트를 필드 하나씩 나열하는 방식은 모듈 하나에서는 충분합니다. 하지만 같은 신호 묶음이 모듈
경계 다섯 개를 지나가면, 필드 하나를 바꿀 때마다 모든 경계를 함께 고쳐야 합니다. Chisel은
인터페이스를 타입으로 다룹니다. 신호 묶음을 Bundle 클래스로 한 번 정의하고, 상속으로 확장하고,
같은 모양의 인터페이스끼리 한 문장으로 연결합니다.
먼저 알아 두면 좋은 것.
Bundle과 Vec 장의 Bundle,
Flipped, Output 방향과
하위 모듈과 생성기 장의
Module(new ...) 인스턴스화를 사용합니다. ready-valid 예제의 생산자는
레지스터와 step 장의
RegInit 카운터이고, 앞 장 메모리까지 다룬 데이터 경로에 흐름
제어를 더하는 내용입니다.
인터페이스 클래스와 일괄 연결은 모두 elaboration 단계에서 처리됩니다. <>는 Scala 코드로 두
Bundle 타입을 함께 순회하면서 leaf 신호마다 일반 연결 하나를 기록하고, 생성된 회로에는
io_out_valid 같은 평탄화된 포트 이름만 남습니다. 반면 ready-valid 핸드셰이크의 전송 여부는
시뮬레이션 중 매 사이클 두 신호의 값으로 정해집니다.
1. 인터페이스를 Bundle 타입으로 정의하기
1.1 인터페이스 클래스
인터페이스는 Bundle을 상속한 클래스입니다. 필드마다 Output 또는 Input으로 방향을
붙이며, 방향은 이 인터페이스를 포트로 쓰는 모듈의 시점에서 읽습니다.
형식
class 인터페이스_이름 extends Bundle {
val 필드_이름 = Output(타입)
val 필드_이름 = Input(타입)
}class Req extends Bundle {
val addr = Output(UInt(8.W))
val valid = Output(Bool())
}Req는 요청을 보내는 쪽, 곧 생산자 시점의 인터페이스입니다. 두 필드가 모두 출력입니다.
1.2 상속으로 필드 추가하기
Bundle 클래스를 상속하면 상위 클래스의 필드를 모두 물려받고 새 필드를 추가할 수 있습니다.
형식
class 하위_이름 extends 상위_이름 {
val 추가_필드 = Output(타입)
}class ReqWithId extends Req {
val id = Output(UInt(2.W))
}ReqWithId는 addr, valid, id 세 필드를 가집니다.
주의. Scala에서는 Req를 받는 곳에 하위 클래스 ReqWithId를 넘길 수 있습니다. 하지만 연결
연산자는 Scala 클래스가 아니라 필드 이름을 비교합니다. Req와 ReqWithId는 <>의 양쪽에서
서로 대체되지 않습니다.
1.3 Flipped로 방향 뒤집기
Flipped(...)는 감싼 Bundle 안의 모든 leaf 방향을 뒤집습니다. 중첩된 Bundle까지 재귀적으로
뒤집으며, 두 번 뒤집으면 원래 방향이 됩니다. 그래서 생산자 시점으로 정의한 클래스 하나로 소비자
시점도 기술할 수 있습니다.
형식
val 이름 = Flipped(new 인터페이스_이름)class PassIO extends Bundle {
val in = Flipped(new ReqWithId) // addr, valid, id가 모두 입력
val out = new ReqWithId // addr, valid, id가 모두 출력
}| 필드 | 방향 |
|---|---|
in.addr, in.valid, in.id | Flipped로 뒤집혀 모두 입력입니다 |
out.addr, out.valid, out.id | 클래스에 선언한 그대로 모두 출력입니다 |
확인 문제
class Resp extends Bundle { val data = Output(UInt(8.W)) }, class Port extends Bundle { val addr = Output(UInt(8.W)); val resp = Flipped(new Resp) }입니다. 어떤 모듈이 io = IO(Flipped(new Port))를 선언하면 그 모듈의 입력은 무엇일까요?
1.4 인터페이스의 Vec
인터페이스 클래스도 타입이므로 Vec의 원소가 될 수 있습니다. 공식 문서는 crossbar 포트를 다음처럼
기술합니다.
class CrossbarIO(n: Int) extends Bundle {
val in = Vec(n, Flipped(new ParityLink))
val sel = Input(UInt(log2Ceil(n).W))
val out = Vec(n, new ParityLink)
}<>는 Vec도 원소 단위로 순회합니다. 길이가 다른 두 Vec을 연결하면 elaboration이
Left and Right are different length Vecs 오류로 중단됩니다.
2. 일괄 연결 <>
2.1 <>가 하는 일
<>는 "번들 값"을 옮기는 연산이 아닙니다. 두 Bundle 타입을 함께 순회하면서 leaf마다 연결을
하나씩 만듭니다.
형식
왼쪽 <> 오른쪽class Pass extends Module {
val io = IO(new PassIO)
io.out <> io.in // addr, valid, id 세 연결
}| 단계 | <>가 수행하는 일 |
|---|---|
| 1 | 왼쪽의 각 필드를 오른쪽의 같은 이름 필드와 짝짓습니다 |
| 2 | 한쪽에만 있는 이름이 있으면 elaboration을 중단합니다 |
| 3 | leaf 쌍마다 어느 쪽이 구동할 수 있는지 판정합니다. 현재 모듈의 출력과 하위 모듈의 입력이 구동 대상입니다 |
| 4 | 판정한 방향으로 leaf마다 := 하나를 기록합니다 |
3단계가 <>의 핵심입니다. ready-valid 링크는 valid와 데이터가 한 방향으로, ready가 반대
방향으로 흐릅니다. :=는 언제나 오른쪽에서 왼쪽으로만 구동하므로 이런 링크를 한 문장으로
연결하지 못합니다. <>는 leaf마다 구동하는 쪽을 따로 찾아 연결합니다.
2.2 연결 규칙
| 규칙 | 이유 |
|---|---|
<>는 양쪽 필드 이름이 정확히 일치해야 합니다 | 한쪽에만 있는 필드가 실수인지 의도인지 Chisel이 판단할 수 없습니다 |
<>는 leaf마다 합법적인 구동 방향이 하나 있어야 합니다 | 양쪽이 모두 구동하려 하면 방향을 정할 수 없습니다 |
:=는 한 방향이며 오른쪽의 여분 필드를 무시합니다 | 편리하지만, 빠진 필드에 대한 경고가 없습니다 |
2.3 예제: 인터페이스 상속과 연쇄 연결
Chain.scala를 위에서부터 읽으세요.
| 선언 | 내용 |
|---|---|
Link | data, valid 두 필드로 이루어진 생산자 시점 인터페이스입니다 |
ParityLink | Link를 상속하고 parity 필드를 추가합니다 |
StageIO | in은 Flipped로 감싸 세 필드가 모두 입력이고, out의 세 필드는 출력입니다 |
AddOne | data + 1을 내보내고 새 값의 parity를 다시 계산합니다. 들어온 parity 비트가 data의 축약 XOR(xorR)과 다르면 valid를 0으로 만듭니다 |
Chain | AddOne 두 개를 만들고 <> 세 개로 상위 입력, 첫 단, 둘째 단, 상위 출력을 차례로 연결합니다 |
import chisel3._
class Link extends Bundle {
val data = Output(UInt(8.W))
val valid = Output(Bool())
}
// 하위 클래스는 data와 valid를 상속하고 필드 하나를 추가한다.
class ParityLink extends Link {
val parity = Output(Bool())
}
// Flipped는 in의 모든 필드를 stage의 입력으로 바꾼다.
class StageIO extends Bundle {
val in = Flipped(new ParityLink)
val out = new ParityLink
}
class AddOne extends Module {
val io = IO(new StageIO)
// parity 비트가 data와 맞지 않으면 그 word를 무효로 만든다.
val parityOk = io.in.data.xorR === io.in.parity
val sum = io.in.data + 1.U
io.out.data := sum
io.out.valid := io.in.valid && parityOk
io.out.parity := sum.xorR
}
class Chain extends Module {
val io = IO(new StageIO)
val first = Module(new AddOne)
val second = Module(new AddOne)
first.io.in <> io.in
second.io.in <> first.io.out
io.out <> second.io.out
}Tb.scala는 elaboration 중에 ParityLink의 필드 이름을 FIELDS 줄로 출력하고, 이어서 세
입력을 넣습니다. parity가 맞는 5(1이 두 개이므로 parity 0), parity 비트가 틀린 5, parity가 맞는
254(1이 일곱 개이므로 parity 1)입니다.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
println(s"FIELDS ${(new ParityLink).elements.keys.mkString(",")}")
test(new Chain) { dut =>
for ((value, parity) <- Seq((5, false), (5, true), (254, true))) {
dut.io.in.data.poke(value.U)
dut.io.in.parity.poke(parity.B)
dut.io.in.valid.poke(true.B)
val out = dut.io.out
println(s"CHAIN in=$value parity=${parity.B.litValue} -> out=${out.data.peek().litValue} parity=${out.parity.peek().litValue} valid=${out.valid.peek().litValue}")
}
}
}이 개념 실습하기
다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.
실행하기 전에 예측해 보세요.
FIELDS줄에는 필드가 몇 개, 어떤 순서로 출력될까요?- 각 입력에 대해 체인 출력의
data,parity,valid는 무엇일까요? - 첫 단에서 무효가 된 word는 둘째 단을 지나도 무효일까요?
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
FIELDS | parity, valid, data 세 이름입니다. 하위 클래스의 parity와 상속한 두 필드가 모두 있으므로 상속이 동작합니다 |
in=5 parity=0 | 두 단이 모두 검사를 통과합니다. 6을 거쳐 7이 되고 7의 parity는 1이므로 out=7 parity=1 valid=1입니다 |
in=5 parity=1 | 첫 단이 검사에 실패해 valid를 0으로 만듭니다. 둘째 단은 자기 검사 결과와 들어온 valid를 AND하므로 체인 출력도 valid=0입니다. data는 그대로 7입니다 |
in=254 parity=1 | 첫 단이 255를, 둘째 단이 0을 만듭니다. +는 8비트를 유지하므로 합이 순환하고, 0의 parity는 0입니다 |
FIELDS 줄의 순서는 선언 순서의 역순입니다. Chisel은 asUInt로 묶을 때 먼저 선언한 필드가
최상위 비트에 놓이도록 원소를 이 순서로 저장합니다.
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
second.io.in <> first.io.out을 second.io.in := first.io.out으로 | 출력이 같습니다. 이 링크의 모든 필드가 같은 방향으로 흐르므로 :=로도 기술할 수 있습니다 |
StageIO.in에서 Flipped 제거 | first.io.out과 second.io.in이 모두 하위 모듈의 출력이 되어 leaf 양쪽이 구동하려 합니다. elaboration이 ChiselException으로 중단되고 메시지는 failed @.parity: Both Left and Right are drivers로 끝납니다 |
주의. 첫 번째 변경은 ready-valid 링크에서는 실패합니다. 4.3절에서 같은 변경을 다시 적용해 비교해 보세요.
3. 짝이 없는 필드
3.1 예제: 필드 수가 다른 두 인터페이스
일괄 연결은 엄격하기 때문에 편리합니다. 다음 예제는 4비트 tag를 추가한 입력 TaggedLink를
출력 Link에 io.out <> io.in으로 연결합니다. 왼쪽 피연산자는 io.out입니다.
이 예제는 의도적으로 elaboration 오류를 발생시킵니다. <>가 tag의 짝을 왼쪽에서 찾지
못하므로 ChiselException이 발생합니다. 메시지에는 두 피연산자와 각각의 Scala 타입, 순회가 멈춘
필드 경로, 필드가 없는 쪽이 함께 나옵니다.
import chisel3._
class Link extends Bundle {
val data = Output(UInt(8.W))
val valid = Output(Bool())
}
class TaggedLink extends Link {
val tag = Output(UInt(4.W))
}
class Bridge extends Module {
val io = IO(new Bundle {
val in = Flipped(new TaggedLink)
val out = new Link
})
// <>는 이름으로 필드를 짝짓는다. 왼쪽에는 tag의 짝이 없다.
io.out <> io.in
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new Bridge))
}실행하기 전에 예측해 보세요. 오류 메시지는 Left와 Right 가운데 어느 쪽에 필드가 없다고
보고할까요? FIRRTL 출력은 나타날까요?
결과 확인.
| 확인 항목 | 내용 |
|---|---|
| 잘못된 코드 | 필드가 두 개인 Link와 세 개인 TaggedLink를 <>로 연결했습니다 |
| 실패 단계 | elaboration입니다. new Bridge를 구성하는 도중에 예외가 발생하므로 FIRRTL은 출력되지 않습니다 |
| 오류 메시지 | Bridge.io.out: IO[Link]와 Bridge.io.in: IO[TaggedLink]가 두 피연산자이고, failed @.tag: Left Record missing field (tag)로 끝납니다 |
| 고치는 방법 | tag를 전달하려면 출력도 TaggedLink로 선언합니다. 버리려면 공통 필드만 명시적으로 연결합니다 |
메시지의 @.tag는 번들 안에서 순회가 멈춘 경로이고, Left Record missing field는 왼쪽
피연산자인 Link에 tag가 없다는 뜻입니다.
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
피연산자 순서를 바꿔 io.in <> io.out | 같은 실패가 Right Record missing field (tag)로 보고됩니다 |
io.out := io.in | elaboration이 성공합니다. 생성된 FIRRTL은 data와 valid만 연결하고 tag는 경고 없이 빠집니다 |
io.out :<>= io.in | 더 이른 Scala 컴파일 단계에서 Cannot prove that Link =:= TaggedLink 오류가 발생합니다 |
out도 TaggedLink로 선언 | 모든 이름에 짝이 생겨 elaboration이 성공합니다 |
:=는 불일치를 허용하므로 신호가 경고 없이 사라집니다. :<>=는 Scala 타입이 다르면 Chisel이
실행되기도 전에 컴파일 오류를 발생시킵니다.
4. ready-valid와 Decoupled
4.1 Decoupled 인터페이스
흐름 제어가 필요한 링크에는 직접 만든 valid/ready 쌍 대신 chisel3.util의 Decoupled를
씁니다. Queue와 arbiter 같은 라이브러리 구성 요소가 이 인터페이스를 사용하며, <> 한 문장으로
연결됩니다.
형식
val 이름 = Decoupled(데이터_타입) // 생산자 시점
val 이름 = Flipped(Decoupled(데이터_타입)) // 소비자 시점| 필드 | 생산자 시점 방향 | 의미 |
|---|---|---|
valid | 출력 | 이번 사이클에 bits가 유효한 값이라는 표시입니다 |
bits | 출력 | 전송할 데이터입니다 |
ready | 입력 | 소비자가 이번 사이클에 값을 받을 수 있다는 표시입니다 |
같은 사이클에 valid와 ready가 모두 1이면 그 사이클에 값 하나가 전송됩니다. fire 메서드가
이 조건, 곧 valid && ready를 계산합니다.
val io = IO(new Bundle {
val in = Flipped(Decoupled(UInt(8.W)))
val seen = Output(UInt(8.W))
})
io.in.ready := true.B // 언제나 받을 수 있음
io.seen := Mux(io.in.fire, io.in.bits, 0.U) // 전송된 사이클에만 값4.2 Queue
Queue는 Decoupled 입력 enq와 Decoupled 출력 deq를 가진 FIFO입니다. 생산자와 소비자
사이에 끼우면 어느 쪽도 고치지 않고 타이밍을 분리할 수 있습니다.
형식
val q = Module(new Queue(데이터_타입, 깊이))
q.io.enq <> 생산자_출력
소비자_입력 <> q.io.deq
출력 <> Queue(생산자_출력, 깊이) // 함수형 생성io.out <> Queue(io.in, 2)함수형 Queue(...)는 큐 인스턴스를 만들고 enq를 인자에 연결한 뒤 deq를 반환합니다. 모듈
장의 companion apply와 같은 방식입니다.
참고. 기본 Queue의 입력 ready는 큐가 가득 차지 않은 동안에만 1입니다.
4.3 예제: 큐를 통과하는 ready-valid
Producer의 출력은 Decoupled(UInt(8.W))입니다. 보낸 값이 다섯 개 미만이면 10 + sent를
제공하고, 카운터 sent는 io.out.fire가 참인 사이클, 곧 소비자가 실제로 값을 받은 사이클에만
증가합니다. Pipeline은 생산자와 자기 Decoupled 출력 사이에 2단 Queue를 둡니다. 두 <>는
각각 leaf 세 개를 연결합니다. valid와 bits는 소비자 쪽으로, ready는 생산자 쪽으로
흐릅니다. 추가 출력 두 개는 큐 입력의 전송 여부와 큐 점유 수를 보여 줍니다.
import chisel3._
import chisel3.util._
class Producer extends Module {
val io = IO(new Bundle {
val out = Decoupled(UInt(8.W))
})
val sent = RegInit(0.U(3.W))
// 값 다섯 개를 제공하고, 소비자가 받아 간 사이클에만 다음 값으로 넘어간다.
io.out.valid := sent < 5.U
io.out.bits := 10.U + sent
when(io.out.fire) {
sent := sent + 1.U
}
}
class Pipeline extends Module {
val io = IO(new Bundle {
val out = Decoupled(UInt(8.W))
val enqFire = Output(Bool())
val count = Output(UInt(2.W))
})
val producer = Module(new Producer)
val queue = Module(new Queue(UInt(8.W), 2))
// 링크마다 <> 하나가 valid, bits, ready를 각자의 방향으로 연결한다.
queue.io.enq <> producer.io.out
io.out <> queue.io.deq
io.enqFire := queue.io.enq.fire
io.count := queue.io.count
}Tb.scala가 소비자 역할을 합니다. 사이클 0–2에는 ready를 0으로, 사이클 3부터는 1로 둡니다.
클록 에지 직전마다 ready, 큐 입력 전송 여부, 점유 수, 출력에서 전송된 값을 출력하며, 전송이
없으면 -를 출력합니다.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Pipeline) { dut =>
for (cycle <- 0 until 9) {
// 테스트벤치가 소비자 역할을 하며 처음 세 사이클은 데이터를 받지 않는다.
val ready = cycle >= 3
dut.io.out.ready.poke(ready.B)
val enq = dut.io.enqFire.peek().litToBoolean
val valid = dut.io.out.valid.peek().litToBoolean
val bits = dut.io.out.bits.peek().litValue
val deq = if (valid && ready) s"$bits" else "-"
println(s"cycle=$cycle ready=${ready.B.litValue} enq=${enq.B.litValue} count=${dut.io.count.peek().litValue} deq=$deq")
dut.clock.step()
}
}
}이 개념 실습하기
다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.
실행하기 전에 예측해 보세요.
- 생산자가 처음으로 값을 보내지 못하는 사이클은 언제일까요?
- 소비자가 받기 시작하는 사이클 3에 큐 입력에서도 전송이 일어날까요?
- 마지막 값 14가 출력되는 사이클은 언제일까요?
결과 확인. 추적에는 두 가지 사실만 필요합니다. valid와 ready가 같은 사이클에 모두
1이면 전송이 일어나고, 기본 Queue의 입력 ready는 가득 차지 않은 동안에만 1입니다.
| 사이클 | 소비자 ready | 큐 입력 | 점유 수 | 출력 |
|---|---|---|---|---|
| 0 | 0 | 10 수락 | 0 | 없음 |
| 1 | 0 | 11 수락 | 1 | 없음 |
| 2 | 0 | 가득 참. 생산자가 12를 유지 | 2 | 없음 |
| 3 | 1 | 사이클 시작 시점에 여전히 가득 참 | 2 | 10 |
| 4–6 | 1 | 12, 13, 14 수락 | 1 | 11, 12, 13 |
| 7 | 1 | 생산자 종료 | 1 | 14 |
| 8 | 1 | 없음 | 0 | 없음 |
사이클 2는 backpressure입니다. 생산자의 valid는 1이지만 큐가 ready를 0으로 내리므로
fire가 거짓이고 sent는 증가하지 않습니다. 값 12는 잃거나 중복되지 않고 bits에 유지됩니다.
사이클 3은 pipe 옵션이 없는 큐가 가득 찬 상태에서는 같은 사이클에 받고 내보내지 못한다는 것을
보여 줍니다. 출력으로 비워진 칸은 한 사이클 뒤에 입력 쪽에 반영됩니다. 사이클 4부터는 매 사이클
값 하나가 들어오고 하나가 나갑니다.
직접 바꿔 보세요.
| 바꿀 곳 | 예상 결과 |
|---|---|
queue.io.enq <> producer.io.out을 :=로 | io.enq.ready in Queue2_UInt8 cannot be written from module Producer 메시지와 함께 elaboration이 ChiselException으로 중단됩니다. :=가 반대 방향으로 흐르는 ready까지 오른쪽에서 왼쪽으로 구동하려 했습니다 |
두 <>를 :<>=로 | 추적 결과가 같습니다. 같은 타입의 DecoupledIO 두 개에서 valid/bits는 정방향, ready는 역방향으로 연결됩니다 |
큐를 new Queue(UInt(8.W), 2, pipe = true)로 | 같은 사이클에 출력이 비워지면 가득 찬 큐도 입력을 받습니다. 사이클 3에 큐 입력도 전송되고 생산자가 한 사이클 일찍 끝나며 점유 수는 2를 유지합니다. 출력 열은 그대로이며 10–14가 사이클 3–7에 출력됩니다 |
생산자의 when(io.out.fire)를 when(io.out.valid)로 | 가득 찬 큐가 값을 받지 않은 사이클 2와 3에도 카운터가 증가합니다. 12와 13을 건너뛰어 소비자는 10, 11, 14만 받습니다 |
참고. Decoupled는 전송 전까지 valid나 bits를 유지한다고 보장하지 않습니다.
IrrevocableIO는 valid가 1이 된 뒤 전송될 때까지 bits를 유지한다는 규약을 따릅니다. 이
규약은 관례일 뿐이며 Chisel은 검사 회로를 생성하지 않습니다.
확인 문제
Tb.scala를 고쳐 소비자가 사이클 0부터 ready를 계속 1로 두면, 마지막 값 14는 몇 번째 사이클에 파이프라인을 나갈까요?
5. 연결 연산자 선택
새 코드에는 Connectable 문서가 권장하는 연산자 계열을 사용합니다. 기호마다 뜻이 정해져
있습니다. :는 소비자(왼쪽), =는 생산자(오른쪽), <는 오른쪽에서 왼쪽으로 구동하는 leaf,
>는 왼쪽에서 오른쪽으로 구동하는 leaf, #는 방향 무시를 뜻합니다.
| 연산자 | 구동 대상 | 사용하는 경우 |
|---|---|---|
c := p | 뒤집힌 leaf를 포함해 c의 모든 leaf를 p에서 구동합니다. 하위 모듈의 ready처럼 이쪽에서 구동할 수 없는 leaf가 있으면 실패합니다 | 단일 값, 한 방향 Bundle |
c <> p | leaf마다 구동할 수 있는 쪽에서 구동합니다 | 기존 코드, 모듈 포트 연결 |
c :<>= p | c의 정렬된 leaf는 p에서, p의 뒤집힌 leaf는 c에서 구동합니다 | 새 코드의 ready-valid 같은 혼합 방향 링크 |
c :<= p / c :>= p | 정렬된 leaf만 / 뒤집힌 leaf만 구동합니다 | 혼합 방향 링크를 절반씩 연결 |
c :#= p | 방향과 무관하게 c의 모든 leaf를 p에서 구동합니다 | 링크 관찰, 인터페이스 전체를 wire에 복사 |
:<>=는 양쪽 구조가 같아야 하며 일부는 Scala 컴파일 단계에서 검사합니다. 3절의 불일치 변형이
elaboration 전에 실패한 이유가 이 검사입니다. 의도적으로 모양이 다른 인터페이스를 연결하는
waiver와 squeeze 기능도 Connectable 문서에 있지만, 이 트랙에서는 사용하지 않습니다.
확인 문제
모니터가 관찰하는 Decoupled 포트의 모든 leaf를 ready까지 포함해 Wire에 담으려 합니다. 모든 leaf는 포트 쪽에서 구동되어야 합니다. 인터페이스 전체를 복사하는 연산자는 무엇일까요?
6. 자주 하는 실수
6.1 ready-valid 링크를 :=로 연결하기
// 잘못된 코드: ready까지 오른쪽에서 왼쪽으로 구동하려 함
queue.io.enq := producer.io.out
// 바른 코드: leaf마다 방향을 정해 연결
queue.io.enq <> producer.io.out:=는 모든 leaf를 한 방향으로만 구동합니다. ready는 소비자에서 생산자로 흐르므로
elaboration이 ... cannot be written from module Producer 오류로 중단됩니다.
6.2 소비자 쪽 포트에 Flipped를 빠뜨리기
// 잘못된 코드: in의 필드도 출력
class StageIO extends Bundle {
val in = new ParityLink
val out = new ParityLink
}
// 바른 코드: in은 소비자 시점
class StageIO extends Bundle {
val in = Flipped(new ParityLink)
val out = new ParityLink
}Flipped가 없으면 두 단을 잇는 링크의 양쪽이 모두 하위 모듈의 출력이 됩니다. <>가 구동
방향을 정할 수 없으므로 Both Left and Right are drivers 오류가 발생합니다.
6.3 :=로 필드 수가 다른 인터페이스 연결하기
// 잘못된 코드: tag가 경고 없이 빠짐
io.out := io.in // out: Link, in: TaggedLink
// 바른 코드: 출력도 tag를 가진 인터페이스로 선언
val out = new TaggedLink
io.out <> io.in:=는 오른쪽에만 있는 필드를 무시합니다. 오류도 경고도 없이 tag가 하류에 도착하지 않으므로,
필드가 사라지면 연결 연산자부터 확인하세요.
6.4 fire가 아니라 valid만 보고 다음 값으로 넘어가기
// 잘못된 코드: 소비자가 받지 않은 사이클에도 증가
when(io.out.valid) { sent := sent + 1.U }
// 바른 코드: 실제로 전송된 사이클에만 증가
when(io.out.fire) { sent := sent + 1.U }valid는 생산자가 값을 제공한다는 뜻일 뿐입니다. 소비자의 ready가 0인 사이클에 다음 값으로
넘어가면 제공하던 값이 사라집니다.
7. 정리
| 주제 | 핵심 |
|---|---|
| 인터페이스 | Bundle 클래스로 정의하는 타입입니다. 상속으로 필드를 추가합니다 |
Flipped | 감싼 Bundle의 모든 leaf 방향을 재귀적으로 뒤집습니다. 생산자 시점 클래스 하나로 소비자 시점도 기술합니다 |
<> | 이름이 같은 필드를 짝짓고 leaf마다 구동 방향을 정합니다. 이름이 맞지 않으면 elaboration 오류입니다 |
:= | 한 방향으로만 구동하며 오른쪽의 여분 필드를 경고 없이 버립니다 |
Decoupled | valid, bits, ready의 묶음입니다. valid와 ready가 모두 1인 사이클에만 전송이 일어나며 fire가 그 조건입니다 |
Queue | Decoupled 링크 사이에 끼워 타이밍을 분리합니다. 기본 큐는 가득 차면 입력 ready를 0으로 둡니다 |
:<>= | 새 코드에 권장하는 혼합 방향 연결입니다. 타입 불일치를 Scala 컴파일 단계에서 검출합니다 |
8. 확인 문제
-
ParityLink가parity를 마지막에 선언하는데FIELDS줄에서parity가 먼저 출력되는 이유는 무엇일까요?정답 보기
Chisel은
Bundle원소를 선언 순서의 역순으로 저장합니다. 그래야asUInt로 묶었을 때 먼저 선언한 필드가 최상위 비트에 놓이기 때문입니다. -
입력 parity 비트가 틀리면 둘째 단의 출력
valid까지 0이 되는 이유는 무엇일까요?정답 보기
첫 단이 검사에 실패해
valid를 0으로 내보내고, 둘째 단은 자기 검사 결과와 들어온valid를 AND합니다. 들어온valid가 0이므로 둘째 단의 출력valid도 0입니다. -
:=가 체인의 두 단은 연결하지만Decoupled생산자와 큐는 연결하지 못하는 이유는 무엇일까요?정답 보기
체인 링크의 필드는 모두 같은 방향으로 흐르므로 한 방향 연결로 충분합니다.
Decoupled링크의ready는 반대 방향으로 흐르는데,:=는 그 leaf까지 오른쪽에서 왼쪽으로 구동하려 하므로elaboration오류가 발생합니다. -
큐 추적에서 소비자가 읽기 시작한 사이클 3에 큐 입력이 전송되지 않는 이유는 무엇일까요?
정답 보기
사이클 3이 시작될 때 큐는 가득 차 있고, 기본 큐의 입력
ready는 가득 차지 않은 동안에만 1입니다. 사이클 3의 출력으로 비워진 칸은 다음 사이클에야 입력 쪽에 반영됩니다. -
tag불일치를 Scala 컴파일 단계에서 검출하는 연산자와 경고 없이 통과시키는 연산자는 각각 무엇일까요?정답 보기
:<>=가Cannot prove that Link =:= TaggedLink컴파일 오류로 검출합니다.:=는elaboration에 성공하고tag를 경고 없이 버립니다.