레지스터·RegInit·클록 진행 장에서 test, poke, peek, expect, clock.step으로 테스트벤치를 썼습니다. 이 장은 그 위에 얹히는 기법을 다룹니다. 본문이 실행되기 전에 무슨 일이 일어나는지, 실패한 expect를 어떻게 읽는지, 사건을 기다리는 반복문을 어떻게 끝내는지입니다.
1. 본문 실행 전에 일어나는 일
test(...)는 본문을 호출하기 전에 다음 네 작업을 차례로 수행합니다.
| 순서 | 작업 |
|---|---|
| 1 | 모듈을 elaboration하고 시뮬레이터용 회로로 변환합니다 |
| 2 | 작업 디렉터리를 만듭니다. 이름 인자가 있으면 test_run_dir/<이름>/, 없으면 test_run_dir/chisel_test_<밀리초>/입니다 |
| 3 | reset을 한 클록 사이클 동안 1로 유지한 뒤 해제합니다. 모든 RegInit이 초기값에서 시작합니다 |
| 4 | dut를 인자로 본문을 호출합니다 |
3번 작업 덕분에 테스트 시작 부분에서 설계를 직접 리셋할 필요가 없습니다. chiseltest가 리셋에 쓴 사이클은 실패 메시지의 step 번호에 포함되지 않습니다.
확인 문제
Acc에서 next는 acc + in이고 sum은 레지스터 acc입니다. 본문 처음에 en을 1, in을 3으로 인가하고 두 출력을 peek한 뒤 clock.step()을 부르고 다시 두 출력을 peek합니다. 두 번의 peek 결과는 무엇일까요?
2. 실패한 expect 읽기
성립하지 않은 expect는 테스트벤치가 만드는 가장 유용한 정보입니다. 어느 step에서 어느
포트의 두 값이 달랐는지 알려 주기 때문입니다.
다음 예제는 의도적으로 실패합니다. 같은 Acc를 사용하며, 테스트벤치는 en을 1로 두고
3과 4를 차례로 더한 뒤 누적 합을 8로 기대합니다.
import chisel3._
class Acc extends Module {
val io = IO(new Bundle {
val en = Input(Bool())
val in = Input(UInt(8.W))
val next = Output(UInt(8.W))
val sum = Output(UInt(8.W))
})
val acc = RegInit(0.U(8.W))
// next는 조합 출력이다. 클록 step 없이 poke를 따라간다.
io.next := acc + io.in
when (io.en) {
acc := io.next
}
io.sum := acc
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Acc) { dut =>
dut.io.en.poke(true.B)
dut.io.in.poke(3.U)
dut.clock.step()
dut.io.in.poke(4.U)
dut.clock.step()
// 잘못된 기댓값이다. 3 + 4는 8이 아니라 7이다.
dut.io.sum.expect(8.U, "running total after 3 + 4")
println("not reached")
}
}이 개념 실습하기
다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.
실행하기 전에 예측해 보세요. 메시지는 몇 번째 step을 보고할까요? 실제 값으로는
무엇이 나타날까요? not reached는 출력될까요?
결과 확인.
| 확인 항목 | 내용 |
|---|---|
| 잘못된 코드 | 3 + 4는 7인데 dut.io.sum.expect(8.U, ...)로 검사했습니다 |
| 실패 단계 | 시뮬레이션 단계입니다. 두 번째 step 뒤의 expect에서 실패하며 컴파일과 elaboration은 성공합니다 |
| 오류 메시지 | chiseltest.FailedExpectException: In step 2: io_sum=7 (0x7) did not equal expected=8 (0x8): running total after 3 + 4 |
| 고치는 방법 | 이 예제는 설계가 맞고 기댓값이 틀렸습니다. expect(7.U, ...)로 고치면 통과합니다 |
In step 2는 본문의 clock.step() 두 번을 센 값이며, 본문 전의 리셋 사이클은 들어가지
않습니다. expect 뒤의 println은 실행되지 않으므로 표준 출력은 비어 있습니다. stack
trace의 Tb.scala 줄 번호가 실패한 expect를 가리킵니다.
확인 문제
expect(8.U, ...) 줄을 첫 번째 clock.step() 바로 뒤로 옮기면 실패 메시지는 무엇으로 시작할까요?
실제 작업에서는 설계 버그가 원인인 경우도 그만큼 많습니다. 먼저 설계와 기댓값 가운데
어느 쪽이 틀렸는지 판단하세요. step 번호는 조사할 사이클을 알려 주므로, 그 사이클을
printf나 파형으로 확인하면 됩니다.
3. 사건 기다리기
3.1 대기 반복문
처리에 걸리는 사이클 수가 일정하지 않은 설계는 보통 출력 신호 하나로 완료를 알립니다.
테스트벤치는 peek과 step을 반복하며 그 신호를 기다립니다.
형식
while (!dut.io.done.peek().litToBoolean) {
dut.clock.step()
}var cycles = 0
while (!dut.io.ready.peek().litToBoolean) {
dut.clock.step()
cycles += 1
}
println(s"ready after $cycles cycles")설계가 신호를 끝내 1로 만들지 않으면 이 반복문은 스스로 끝나지 않습니다.
3.2 idle cycle timeout
chiseltest는 클록마다 idle cycle timeout을 둡니다. step마다 idle cycle 수가 1
증가하고, poke로 입력값이 바뀌면 0으로 돌아갑니다. 같은 값을 다시 poke하면 그대로입니다. peek과 expect는 이 수를 바꾸지
않습니다. 수가 한도에 도달하면 테스트가 TimeoutException으로 실패합니다.
형식
dut.clock.setTimeout(사이클_수) // 기본 한도: 1000
dut.clock.setTimeout(0) // 검사를 끕니다| step 사이의 호출 | idle cycle 수의 변화 |
|---|---|
step() | 1 증가합니다 |
입력값을 바꾸는 poke | 0이 됩니다. 같은 값을 다시 주면 변하지 않습니다 |
peek, expect | 변하지 않습니다 |
대기 반복문은 peek만 하므로 step이 계속 누적되다가 한도에서 멈춥니다. 신호를 읽기만
하는 것은 진행이 아니므로 이 동작이 바람직합니다.
3.3 예제: 끝나지 않는 대기 반복문
Waiter는 start가 1인 동안 count를 증가시키고, count가 10이 되면 done을 1로
만듭니다. 테스트벤치는 timeout을 20으로 정하고, start를 한 사이클 동안만 1로 둔 뒤
0으로 내리고 done을 기다립니다.
다음 예제는 의도적으로 실패합니다. start가 한 사이클 뒤 내려가므로 count는 1에서 멈추고
done은 1이 되지 않습니다. 실행하기 전에 timeout이 반복문을 멈출 때까지 step이 몇 번
실행될지 예측해 보세요.
import chisel3._
class Waiter extends Module {
val io = IO(new Bundle {
val start = Input(Bool())
val done = Output(Bool())
})
val count = RegInit(0.U(8.W))
// start가 1로 유지되는 동안에만 count가 증가한다.
when (io.start) {
count := count + 1.U
}
io.done := count === 10.U
}import chisel3._
import chiseltest._
import chiseltest.RawTester.test
object Main extends App {
test(new Waiter) { dut =>
dut.clock.setTimeout(20)
dut.io.start.poke(true.B)
dut.clock.step()
// 버그: start가 한 사이클 뒤 내려가므로 done이 1이 되지 않는다.
dut.io.start.poke(false.B)
var cycles = 1
while (!dut.io.done.peek().litToBoolean) {
dut.clock.step()
cycles += 1
}
println(s"DONE after $cycles cycles")
}
}이 개념 실습하기
다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.
결과 확인.
| 확인 항목 | 내용 |
|---|---|
| 잘못된 코드 | Waiter는 start가 1인 동안에만 세는데, 한 step 뒤 dut.io.start.poke(false.B)로 내렸습니다 |
| 실패 단계 | 시뮬레이션 단계입니다. 대기 반복문 안에서 실패합니다 |
| 오류 메시지 | chiseltest.TimeoutException: timeout on Waiter.clock ... at 20 idle cycles |
| 고치는 방법 | count가 10이 될 때까지 start를 1로 유지하거나, 펄스 한 번으로 세기 시작하도록 설계를 바꿉니다 |
메시지는 마지막 poke, 곧 start를 0으로 내린 뒤 step 20번이 지났다는 뜻입니다.
timeout은 done이 0으로 남은 원인을 알지 못하며 테스트벤치의 대기 시간에 상한만 둡니다.
DONE 줄은 출력되지 않습니다.
확인 문제
다음 가운데 하나만 바꿔서 이 테스트가 DONE을 출력하게 하는 것은 무엇일까요?
직접 바꿔 보세요.
dut.io.start.poke(false.B)를 지워 보세요.start가 1로 유지되어 step 10번 뒤 count가 10이 되고DONE after 10 cycles가 출력됩니다. 마지막poke이후 step 10번은 한도 20보다 작습니다.- 위 수정을 유지한 채
setTimeout(20)을setTimeout(5)로 바꿔 보세요. 설계는 올바른데도 이번에는at 5 idle cycles로 실패합니다. 설계의 정상 지연보다 짧은 timeout은 테스트벤치 버그입니다. - 원래 코드로 되돌리고
setTimeout(0)을 써 보세요. 검사가 꺼지므로 원래 버그가 있는 테스트는 스스로 끝나지 않습니다.
한도는 예상되는 가장 긴 정상 대기보다 크게, 멈춘 테스트가 시간을 낭비하지 않을 만큼 작게 정하세요.
4. 자주 하는 실수
4.1 step 없는 대기 반복문
// 잘못된 코드: 시간이 흐르지 않고 timeout도 멈추지 못합니다
while (!dut.io.done.peek().litToBoolean) { }
// 바른 코드
while (!dut.io.done.peek().litToBoolean) { dut.clock.step() }idle cycle timeout은 step을 셉니다. peek만 하는 반복문은 클록을 진행하지 않으므로
done이 바뀔 수 없고, timeout도 발생하지 않습니다.
5. 정리
| 주제 | 핵심 |
|---|---|
| 본문 실행 전 | test(...)가 모듈을 elaboration하고 treadle을 시작해 리셋을 한 번 인가한 뒤 해제합니다 |
expect 실패 | step 번호, 포트, 실제 값과 기댓값, 설명을 보고합니다 |
| step 번호 | 본문이 실행한 step 수입니다. 리셋 사이클은 준비 과정에 속합니다 |
| 대기 반복문 | while (peek(...)) clock.step(1) — 설계를 움직이는 것은 step입니다 |
setTimeout(n) | poke 없이 step n번이 지나면 실패합니다. 기본값 1000, 0이면 꺼집니다 |
6. 확인 문제
-
In step 2: io_sum=7 ...의 "step 2"는 무엇을 센 값이며, 리셋 사이클이 빠지는 이유는 무엇일까요?정답 보기
본문이 실행한 클록 step 수입니다. 리셋 사이클은 본문 실행 전 chiseltest의 준비 과정에 속하므로 세지 않습니다.
-
peek으로done을 반복해서 읽어도idle cycle수가 0으로 돌아가지 않는 이유는 무엇이며, 이 동작이 왜 유용할까요?정답 보기
idle cycle수는 입력값을 바꾸는poke가 0으로 되돌립니다. 신호를 읽는 것은 설계의 입력을 바꾸지 않습니다. 읽기만으로 수가 0이 된다면 오지 않는 신호를 기다리는 반복문이 영영 timeout에 걸리지 않습니다. -
정상 결과에 약 40 사이클이 걸리는 테스트에
setTimeout(30)이 있습니다. 어떤 일이 생기며 무엇을 고쳐야 할까요?정답 보기
기다리는 동안 입력을
poke하지 않으면 설계가 올바른데도idle cycle30에서TimeoutException으로 실패합니다. 한도를 40보다 크게, 예를 들어 60으로 올리면 실제로 멈춘 경우는 여전히 잡아낼 수 있습니다.