테스트벤치는 모듈을 포트로만 관찰합니다. 모듈 안쪽 레지스터의 값이 틀렸을 때 디버그
포트를 새로 만들지 않고 그 값을 확인하려면 회로가 직접 값을 출력해야 합니다. Chisel의
printf는 시뮬레이션 회로에 들어가는 출력문입니다. 이 장은 공식 Chisel 문서
Explanations의 "Printing"과, 생성된 회로의 신호 이름을 정하는 "Naming"의 일부를
다룹니다.
먼저 알아 두면 좋은 것. 이 장은
다형성과 매개변수화 다음 장입니다.
예제는 레지스터와 step
장의 레지스터와 RegNext, Bundle과 Vec
장의 Bundle과 VecInit, 그리고 when 블록을 사용합니다. 지금까지 모든 Tb.scala에서 써
온 println이 printf와 어떻게 다른지도 이 장에서 설명합니다.
1. 모듈 안의 printf
1.1 출력문도 하드웨어입니다
모듈 본문에서 printf를 호출해도 그 Scala 줄이 실행되는 순간에는 아무것도 출력되지
않습니다. printf 호출은 +나 RegNext처럼 회로에 노드 하나를 추가합니다. 이 노드는
모듈의 클록에 연결되며 형식 문자열, 출력할 신호 목록, enable 조건을 가집니다.
시뮬레이터는 상승 에지마다 enable 조건을 확인하고, 조건이 참이면 신호 값을 형식에 맞춰
메시지 한 줄을 출력합니다.
형식
printf(cf"문자열 $신호%지정자 ${식}%지정자\n")다음 모듈은 매 사이클 증가하는 카운터 값을 출력합니다.
class Ticker extends Module {
val io = IO(new Bundle { val in = Input(UInt(4.W)) })
val count = RegInit(0.U(4.W))
count := count + 1.U
printf(cf"count=$count in=${io.in}\n")
}| 코드 | 의미 |
|---|---|
printf(...) | 모듈의 암묵적 클록에 연결된 출력 노드를 추가합니다 |
cf"..." | 문자열과 하드웨어 값으로 메시지를 구성합니다 |
$count | 레지스터 값을 넣습니다. 지정자가 없으므로 10진수입니다 |
${io.in} | 경로나 식을 넣을 때는 중괄호로 감쌉니다 |
\n | 줄을 끝냅니다. printf는 줄바꿈을 자동으로 붙이지 않습니다 |
에지에서 출력되는 값은 그 에지가 레지스터를 갱신하기 직전의 값입니다. Ticker의
첫 메시지는 count=0이고, 메시지가 출력된 뒤에 count가 1이 됩니다.
확인 문제
Tb.scala가 in = 3을 인가하고 리셋 뒤에 Ticker를 세 번 step합니다. 세 번째 메시지에 나오는 count 값은 무엇일까요?
참고. printf는 시뮬레이션용 디버깅 수단입니다. 칩의 논리로 만들어지지 않으며 회로가
계산하는 값에도 영향을 주지 않습니다.
1.2 출력 조건
printf가 어떤 에지에서 출력할지는 두 조건이 결정합니다.
- 감싸는
when블록.when (c)안의printf는c가 참인 에지에서만 출력합니다. - 리셋. Chisel은 모든
printf에 "리셋 중이 아님" 조건을 암묵적으로 추가합니다.reset이 1인 동안에는 모듈이 아무것도 출력하지 않으므로 초기화 사이클의 출력이 로그에 섞이지 않습니다.
형식
when (조건) {
printf(cf"...\n")
}when (io.valid && io.ready) {
printf(cf"accepted ${io.data}%x\n")
}이 printf는 전송이 성립한 사이클마다 한 줄을 출력하고 나머지 사이클에는 출력하지
않습니다. 관심 있는 사건으로 출력을 제한하면 긴 실행에서도 로그를 읽기 쉽습니다.
| 에지의 상황 | 출력 여부 |
|---|---|
reset이 1 | 모듈의 모든 printf가 출력하지 않습니다 |
reset이 0, 감싸는 when 없음 | 출력합니다 |
reset이 0, 감싸는 when 조건이 거짓 | 출력하지 않습니다 |
reset이 0, 감싸는 when 조건이 참 | 출력합니다 |
확인 문제
위의 accepted printf가 모듈 안에 있습니다. 테스트벤치가 리셋을 두 step 동안 1로 두고, valid를 1로 올린 뒤 ready를 1, 0, 1, 1로 바꾸며 네 번 step합니다. accepted 줄은 몇 번 나올까요?
1.3 출력할 수 있는 값
printf에는 범위 안의 모든 하드웨어 값을 넣을 수 있습니다. 포트, wire, 레지스터, 그리고
이들로 만든 식이 해당합니다. 반면 테스트 중에 바뀌는 Scala var는 따라가지 못합니다.
모듈 본문은 elaboration 중에 한 번만 실행되므로 형식 문자열에 들어간 Scala 값은 회로를
만들 때 고정됩니다.
2. 값의 출력 형식
2.1 cf interpolator
cf"..."는 Scala 문자열 interpolator와 모양이 같지만 문자열이 아니라 Printable을
만듭니다. Printable은 하드웨어 노드를 참조하는 메시지 기술입니다. $이름이나
${식} 바로 뒤에 형식 지정자를 붙일 수 있고, 지정자가 없으면 숫자는 10진수로
출력됩니다. 퍼센트 기호 자체를 출력하려면 %%를 씁니다.
형식
cf"label=$signal%x" // 값 하나, 16진수
cf"sum=${a + b}%d" // 식, 10진수
cf"load=$pct%%" // 값 뒤에 % 문자2.2 형식 지정자
| 지정자 | 출력 형태 | 8비트 값 65의 출력 |
|---|---|---|
%d 또는 생략 | 10진수 | 공백으로 오른쪽 정렬한 65 |
%x | 16진수, 앞을 0으로 채움 | 41 |
%b | 2진수, 공백으로 오른쪽 정렬 | 공백 하나 뒤의 1000001 |
%c | 값을 ASCII 문자로 출력 | A |
이 실습 환경의 시뮬레이터는 신호 폭에 따라 필드 폭을 고정합니다. 그래서 사이클이 바뀌어도 열이 정렬됩니다.
| 신호 | %d 필드 | %x 필드 | %b 필드 |
|---|---|---|---|
UInt(8.W) | 공백 포함 4칸 | 0으로 채운 2자리 | 공백 포함 8칸 |
UInt(4.W) | 공백 포함 3칸 | 1자리 | 공백 포함 4칸 |
Bool | 공백 포함 2칸 | 1자리 | 1칸 |
따라서 8비트 값 7은 %d로 7, %x로 07이 됩니다. 부호 있는 값은 10진수에서 부호와
함께 출력됩니다. -3을 담은 SInt(8.W)는 %d로 -3, %x로 2의 보수 비트인 fd가
출력됩니다.
주의. %c는 문자 코드를 그대로 출력합니다. 32보다 작은 값은 제어 문자이므로 화면에
보이지 않습니다. 예를 들어 7은 보이지 않는 벨 문자입니다. %c는 출력 가능한 ASCII 코드를
담은 신호에 사용하세요.
확인 문제
x는 10을 담은 UInt(4.W)입니다. 형식이 cf 뒤의 [$x%x|$x%b|$x]일 때 printf는 무엇을 출력할까요?
2.3 Vec와 Bundle 출력
$ 하나에 집합 타입 전체를 넣을 수 있습니다. Vec는 Vec(...) 안에 원소를 인덱스
순서로 나열합니다. Bundle은 클래스 이름 뒤에 필드 -> 값 쌍을 선언 순서대로
나열합니다.
class Pkt extends Bundle {
val kind = UInt(2.W)
val len = UInt(4.W)
}
val p = Wire(new Pkt)
val v = VecInit(io.b, io.b + 1.U)
printf(cf"p=$p v=$v\n")위 코드는 출력 형식을 보여 주는 조각이며, p.kind와 p.len을 구동하는 연결은 아직 없습니다.
두 필드를 각각 1.U와 io.b로 구동하고 io.b가 5이면, p= 뒤에는 1과 5를 담은
Pkt(kind -> ..., len -> ...)가, v= 뒤에는 5와 6을 담은 Vec(...)가 출력됩니다.
각 숫자는 %d 필드와 같은 폭으로 채워집니다.
Bundle 클래스는 toPrintable을 override해 출력 형식을 직접 정할 수 있습니다.
형식
override def toPrintable: Printable = cf"..."class Pkt extends Bundle {
val kind = UInt(2.W)
val len = UInt(4.W)
override def toPrintable: Printable = cf"<$kind:$len%x>"
}이렇게 정의하면 어느 모듈에서든 Pkt를 넣은 모든 cf가 이 형식을 사용합니다.
2.4 C 형식 printf
printf는 C처럼 일반 형식 문자열 뒤에 인자를 순서대로 나열하는 형태도 받습니다.
형식
printf("형식 문자열 %d %x\n", 첫째_인자, 둘째_인자)printf("a=%d b=%x\n", io.a, io.b)지정자와 필드 폭은 cf와 같습니다. C 형식은 형식 문자열을 다른 곳에서 만들어 받을 때
유용합니다. 직접 쓰는 메시지에는 cf가 낫습니다. cf는 값이 이름표 바로 옆에 있으므로
인자 순서가 어긋날 일이 없습니다.
2.5 예제: 모듈 안에서 값 추적하기
Tracer의 입력은 8비트 신호 하나입니다. 모듈 안에는 매 사이클 증가하는 카운터
cycle, 입력을 한 사이클 지연한 last, 현재 입력과 이전 입력을 담은 두 원소 Vec
window, 입력으로 만든 Sample Bundle이 있습니다. 끝에 있는 printf 세 개를 먼저
확인하세요.
- 조건 없이 출력하며, 형식 네 가지와
Vec,Bundle을 포함하는cf출력 when (io.in === 0.U)안의 출력io.in과last를 출력하는 C 형식 출력
import chisel3._
class Sample extends Bundle {
val addr = UInt(8.W)
val ok = Bool()
}
class Tracer extends Module {
val io = IO(new Bundle {
val in = Input(UInt(8.W))
})
val cycle = RegInit(0.U(8.W))
cycle := cycle + 1.U
val last = RegNext(io.in, 0.U)
val window = VecInit(io.in, last)
val s = Wire(new Sample)
s.addr := io.in
s.ok := io.in(0)
// 상승 에지마다 한 줄을 출력하며, 에지 직전의 값을 보고한다.
printf(cf"[$cycle%d] in=${io.in}%d hex=${io.in}%x bin=${io.in}%b win=$window s=$s\n")
// when 안의 printf는 조건이 참인 사이클에만 출력한다.
when (io.in === 0.U) {
printf(cf"[$cycle] zero input\n")
}
// C 형식: 일반 형식 문자열 뒤에 인자를 나열한다.
printf("C-style in=%d last=%x\n", io.in, last)
}Tb.scala는 reset을 한 step 동안 1로 유지한 뒤 해제하고 표지 문자열을 출력합니다.
이어서 65, 0, 255를 차례로 poke하며 클록 step 직전마다 TB poke 표지를 출력합니다.
import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Tracer) { dut =>
// reset이 1인 동안 printf는 출력하지 않는다.
dut.reset.poke(true.B)
dut.clock.step()
dut.reset.poke(false.B)
println("TB reset released")
for (v <- Seq(65, 0, 255)) {
dut.io.in.poke(v.U)
println(s"TB poke $v")
dut.clock.step()
}
println("TB done")
}
}이 개념 실습하기
다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.
printf는 에지 직전의 값을 출력합니다. 리셋 뒤 세 step의 값은 다음과 같습니다.
| step | cycle | io.in | last | window | 추가 출력 |
|---|---|---|---|---|---|
| 리셋 | 0 | 0 | 0 | 없음. 리셋이 모든 printf를 막습니다 | |
| 1 | 0 | 65 | 0 | Vec(65, 0) | 없음 |
| 2 | 1 | 0 | 65 | Vec(0, 65) | zero input |
| 3 | 2 | 255 | 0 | Vec(255, 0) | 없음 |
에지가 지나면 cycle은 1 증가하고 last는 방금 끝난 step의 입력을 저장합니다.
실행하기 전에 예측해 보세요.
- 각
TB poke표지 뒤에는 다음 테스트벤치 표지와 회로 메시지 가운데 어느 것이 먼저 출력될까요? - 입력이 255인 세 번째 step에서 C 형식 줄은
last를 무엇으로 출력할까요? zero input줄은 모두 몇 개일까요?ok는 입력의 bit 0입니다. 첫 step에서s=의ok는 무엇일까요?
결과 확인.
| 출력 | 확인할 내용 |
|---|---|
TB reset released 앞에 회로 출력이 없음 | 리셋 step에서는 printf가 출력하지 않았습니다 |
각 TB poke 표지 뒤의 회로 출력 | 메시지는 clock.step() 안에서 출력되므로 시간 순서상 표지 뒤에 나타납니다 |
첫 [0] 줄의 hex=41, bin= 1000001, ok -> 1 | 한 값을 세 형식으로 출력했습니다. 65는 홀수이므로 bit 0이 1입니다 |
cycle 1 줄의 win=Vec(0, 65) | last가 이전 사이클의 입력을 저장합니다 |
세 번째 C 형식 줄의 last=00 | 직전 입력이 0이었고 %x는 두 자리로 채웁니다 |
cycle 1에만 있는 zero input 한 줄 | when 분기는 조건이 참일 때만 출력합니다. 지정자 없는 [cycle]도 10진수로 출력됩니다 |
직접 바꿔 보세요.
-
Sample에 다음 override를 추가해 보세요.override def toPrintable: Printable = cf"@$addr%x/${Mux(ok, 'y'.U, 'n'.U)}%c"모든
s=필드가 기본 형태Sample(addr -> ..., ok -> ...)에서 짧은 형태로 바뀝니다. 입력 65는@41/y, 0은@00/n, 255는@ff/y가 됩니다. override도 하드웨어입니다.Mux가 사이클마다 문자 코드 둘 가운데 하나를 고르고,%c가 고른 코드를 문자로 출력합니다. -
C 형식
printf를 새when (io.in =/= 0.U)블록 안으로 옮겨 보세요. 입력이 0인 cycle 1에서는 C 형식 줄이 사라지고, cycle 0과 cycle 2에는 그대로 남습니다.
3. println과 printf
3.1 실행 단계가 다른 두 출력
Chisel 코드는 두 단계에 걸쳐 실행됩니다. elaboration 단계에서는 Scala가 모듈 본문을 한
번 실행해 회로를 만듭니다. 시뮬레이션 단계에서는 만들어진 회로가 사이클 단위로
동작합니다. println은 일반 Scala 함수이므로 그 줄을 실행하는 단계에서 출력합니다.
printf는 언제나 시뮬레이션 단계에서 출력합니다.
| 문장 | 실행 단계 | 실행 횟수 | 출력할 수 있는 것 |
|---|---|---|---|
모듈 본문의 println(...) | elaboration | 모듈 인스턴스마다 한 번 | Scala 값, 폭, 타입 |
테스트벤치의 println(...) | 테스트 실행 | 그 줄이 실행될 때마다 | peek으로 읽은 값 |
모듈 본문의 printf(...) | 시뮬레이션 | enable이 참인 상승 에지마다 | 하드웨어 값 |
형식
println(s"문자열 ${Scala_값}") // elaboration 중 한 번
printf(cf"문자열 $하드웨어\n") // enable이 참인 에지마다class Widths extends Module {
val io = IO(new Bundle { val a = Input(UInt(8.W)) })
println(s"a is ${io.a.getWidth} bits") // 한 번 출력: a is 8 bits
println(s"a = ${io.a}") // 값이 아니라 객체 설명을 출력
}두 번째 println은 Widths.io.a: IO[UInt<8>] 같은 객체 설명을 출력합니다.
elaboration 중의 포트에는 타입과 폭은 있지만 아직 값이 없습니다.
확인 문제
Ticker 본문에 ELAB Ticker를 쓰는 println과 조건 없는 printf가 하나씩 있습니다. 테스트벤치가 모듈을 한 번 만들고 리셋을 두 step 동안 유지한 뒤 네 번 더 step합니다. ELAB 줄과 printf 줄은 각각 몇 개일까요?
3.2 예제: printf 안의 s"..."
Scala에 익숙하면 습관처럼 s"..."를 쓰기 쉽습니다. printf 안의 s interpolator는
회로를 만드는 동안 Chisel 객체의 toString을 호출해 그 문자열을 메시지에 고정합니다.
그러면 모든 사이클이 wire의 값 대신 같은 객체 설명을 출력합니다. Chisel은 이 형태를
찾아 Scala 컴파일 오류로 처리합니다.
다음 예제는 의도적으로 컴파일 오류를 발생시킵니다. Probe가 printf에
s"in = $in\n"을 전달합니다.
import chisel3._
class Probe extends Module {
val in = IO(Input(UInt(8.W)))
// s-interpolator는 wire 값이 아니라 Scala 객체를 문자열로 만든다.
printf(s"in = $in\n")
}
object Main extends App {
println(circt.stage.ChiselStage.emitCHIRRTL(new Probe))
}결과 확인.
| 확인 항목 | 내용 |
|---|---|
| 잘못된 코드 | printf(s"in = $in\n") |
| 실패 단계 | Scala 컴파일 단계입니다. 회로가 만들어지기 전이며 Main은 실행되지 않습니다 |
| 오류 메시지 | error: The s-interpolator prints the Scala .toString of Data objects와 두 가지 대안 |
| 고치는 방법 | 사이클마다 하드웨어 값을 출력하려면 cf"를 씁니다. 회로를 만드는 동안 한 번만 출력하려면 s"..."와 함께 println을 씁니다 |
4. 신호 이름이 정해지는 방식
4.1 val 이름이 신호 이름이 됩니다
생성된 FIRRTL, Verilog, 파형은 신호를 이름으로 보여 줍니다. 모든 예제 명령의
-Xplugin 플래그가 불러오는 Chisel 컴파일러 플러그인은 하드웨어 값의 이름을 그 값을
담은 val의 이름으로 정합니다. val에 담지 않은 값은 밑줄로 시작하는 생성 이름을
받으며, 생성 이름에는 연결 대상의 이름이 들어가는 경우가 많습니다.
형식
val 이름 = 하드웨어_식val sum = io.a + io.b
io.y := sum
io.z := io.a - io.b| 소스 | 생성된 FIRRTL의 이름 |
|---|---|
val sum = io.a + io.b | sum. 내부의 전체 폭 덧셈은 _sum_T입니다 |
io.z := io.a - io.b | 연결 대상 io.z에서 만든 _io_z_T와 _io_z_T_1 |
이름 없는 printf 세 개 | printf, printf_1, printf_2 |
Tracer에서도 레지스터는 cycle과 last, wire는 window와 s라는 이름을 받습니다.
소스에 이름을 문자열로 적은 곳은 없습니다.
4.2 suggestName
val을 따로 만들 정도는 아닌 식에는 suggestName으로 원하는 이름을 붙입니다.
형식
식.suggestName("이름")io.w := (io.a ^ io.b).suggestName("mix")이 XOR 노드의 이름은 io_w_mix입니다. Chisel은 연결문 안에서 만든 값의 이름 앞에 연결
대상의 이름을 붙이므로, 제안한 이름은 최종 이름의 일부가 됩니다. 별도 줄에서
val mix = io.a ^ io.b로 선언하면 이름은 정확히 mix입니다.
참고. _io_z_T_1 같은 생성 이름은 주변 코드가 바뀌면 달라질 수 있습니다. 로그나
파형에서 찾을 신호에는 val 이름을 주세요. 파형 장에서는 이름 있는 값만 VCD 파일에
기록된다는 사실을 확인합니다.
5. 자주 하는 실수
5.1 cf"..." 대신 s"..." 사용
// 잘못된 코드: 컴파일 오류
printf(s"in = $in\n")
// 바른 코드: enable이 참인 에지마다 값을 출력
printf(cf"in = $in\n")s interpolator는 elaboration 중에 Scala 객체를 문자열로 만듭니다. 하드웨어 값을
참조하는 것은 cf뿐이므로 s 형태에는
The s-interpolator prints the Scala .toString of Data objects 오류가 발생합니다.
5.2 \n 빠뜨리기
// 잘못된 코드: 두 메시지가 한 줄에 이어집니다
printf(cf"a=$a")
printf(cf"b=$b")
// 바른 코드
printf(cf"a=$a\n")
printf(cf"b=$b\n")printf는 받은 문자만 정확히 출력합니다. \n이 없으면 다음 메시지나 테스트벤치의 다음
println이 같은 줄에 이어집니다.
5.3 에지 이후의 값을 기대하기
count := count + 1.U
printf(cf"count=$count\n") // 이 에지에서 갱신되기 전의 값printf는 에지 직전의 값을 출력합니다. 같은 에지에서 갱신되는 레지스터는 이전 값으로
출력되므로 출력이 한 사이클 늦은 것처럼 보입니다. 정상 동작입니다. 새 값은 다음 줄에서
확인하세요.
5.4 println으로 하드웨어 값 출력하기
// 잘못된 코드: elaboration 중에 타입 설명을 한 번 출력
println(s"sum = $sum")
// 바른 코드: 에지마다 값을 출력
printf(cf"sum = $sum\n")모듈 본문은 한 번만 실행됩니다. 그 안의 println이 보는 것은 시뮬레이션 값이 아니라
Chisel 객체입니다.
5.5 리셋 중의 출력을 기대하기
// 테스트벤치가 reset을 두 step 동안 1로 유지
dut.reset.poke(true.B)
dut.clock.step(2) // 모듈의 printf는 출력하지 않습니다reset이 1인 동안에는 모든 printf가 꺼집니다. 로그가 늦게 시작하는 것처럼 보이면 버그를
찾기 전에 리셋 사이클 수부터 세어 보세요.
6. 정리
| 주제 | 핵심 |
|---|---|
printf | enable이 참인 상승 에지마다 메시지를 출력하는 하드웨어 노드입니다 |
| 출력 시점 | 에지가 레지스터를 갱신하기 직전의 값을 출력합니다 |
| enable | 감싸는 when 조건과 암묵적 "리셋 중이 아님" 조건으로 정해집니다 |
cf"..." | $x나 ${식} 뒤에 %d, %x, %b, %c를 붙입니다. %%는 퍼센트 기호입니다 |
| 필드 폭 | 신호 폭에 따라 고정되어 열이 정렬됩니다 |
| 집합 타입 | 기본 형태는 Vec(...)와 클래스(필드 -> 값)이며, toPrintable로 바꿉니다 |
println | 일반 Scala입니다. 모듈에서는 elaboration 중 한 번, 테스트벤치에서는 호출마다 실행됩니다 |
| 이름 | val 이름이 신호 이름이 되고, 이름 없는 값은 _로 시작하는 이름을 받습니다. suggestName은 이름을 제안합니다 |
7. 확인 문제
-
printf예제에서 테스트벤치가 방금 65를poke했는데 첫 번째 출력의last가 0인 이유는 무엇일까요?정답 보기
printf는 에지 직전의 값을 출력합니다. 첫 에지 직전에last는 아직 리셋 값 0을 가지고 있습니다.last는 메시지가 출력된 뒤 그 에지에서 65를 저장합니다. -
어떤 에지에서
printf가 출력하지 않게 만드는 조건 두 가지는 무엇일까요?정답 보기
조건이 거짓인
when이printf를 감싸는 경우와reset이 1인 경우입니다. -
printf(s"...")가 쓸모없는 문자열을 출력하는 대신 컴파일 오류가 되는 이유는 무엇일까요?정답 보기
sinterpolator는elaboration중에 Chisel 객체를 한 번 문자열로 바꾸고, 모든 사이클이 그 문자열을 반복합니다. 이런 코드는 거의 언제나 실수이므로 Chisel은 Scala 코드를 컴파일할 때 오류로 알려 줍니다. -
UInt(4.W)의%d필드는 몇 칸이며, 필드 폭이 고정되면 무엇이 편리할까요?정답 보기
3칸입니다. 같은 신호는 모든 줄에서 같은 폭을 쓰므로 사이클이 바뀌어도 값이 같은 열에 놓이고 비교하기 쉽습니다.
-
val total = a + b로 쓰면 FIRRTL에서 결과 노드의 이름은 무엇일까요?out := a + b로 쓰면 어떤 이름이 예상될까요?정답 보기
결과 노드는
total이고, 내부의 전체 폭 덧셈은_total_T입니다.out := a + b에는val이 없으므로 연결 대상에서 만든 생성 이름_out_T와_out_T_1이 붙습니다.