본문으로 건너뛰기
code0.HDL

레지스터·RegInit·클록 진행

리셋 값이 있는 레지스터를 만들고, :=의 오른쪽에서 현재 값을, 왼쪽에서 다음 값을 읽은 뒤, 테스트벤치에서 poke·peek·step으로 구동하며 에지 전에 움직이는 출력과 에지 뒤에만 움직이는 출력을 구분합니다.

이 장에서 배울 내용

  • Reg·RegInit·RegNext로 레지스터를 만들고 := 오른쪽의 현재 값과 왼쪽의 다음 값을 구분할 수 있습니다
  • chiseltest의 poke·peek·step으로 설계를 구동하고, 에지 전에 바뀌는 출력과 에지 뒤에야 바뀌는 출력을 예측할 수 있습니다
  • 카운터를 레지스터 하나와 연결 하나로 작성할 수 있습니다
  • 빠진 poke나 step 때문에 끝나지 않는 카운트다운을 진단할 수 있습니다
이 장에서 배우는 개념
  • Reg·RegInit·step
먼저 알아야 할 개념

1. 레지스터에는 현재 값과 다음 값이 있습니다

지금까지의 회로는 모두 조합 회로였습니다. 레지스터는 기억합니다. 클록의 다음 상승 에지까지 값을 유지하고, 에지에서만 새 값을 받습니다. Chisel에서 RegInit(0.U(8.W))는 리셋 뒤 값이 0인 8비트 레지스터를 만듭니다. 모든 Module에는 포트 목록에 쓰지 않는 암묵적 클록과 리셋이 있고, 레지스터는 둘을 자동으로 사용합니다. 이 과정은 전체에서 클록 하나와 상승 에지 레지스터만 가정합니다.

형식

val acc = RegInit(0.U(8.W))
acc := acc + io.in
위치어느 값뜻
:=의 오른쪽이나 식 안의 acc현재 값이번 사이클 동안 레지스터가 저장하고 있는 값
acc := ...의 왼쪽 acc다음 값다음 상승 에지 뒤에 저장될 값

레지스터 주변의 논리는 언제나 현재 값만 봅니다. 레지스터에 연결해도 같은 사이클에 읽히는 값은 바뀌지 않습니다. 다음 에지에서 레지스터가 무엇을 저장할지만 정해집니다. val acc는 레지스터 노드의 Scala 이름이고, 상태는 시뮬레이션에서 레지스터가 저장한 값입니다. 상태는 에지에서만 바뀝니다.

주의. 폭을 적은 리셋 값은 레지스터의 폭을 고정합니다. RegInit(0.U)처럼 폭을 적지 않으면 레지스터에 연결된 것에서 폭을 추론합니다. 연결이 자기 자신 더하기 1비트 리터럴만 있는 카운터라면 추론은 1비트로 정해지고 카운터는 0, 1, 0, 1을 반복합니다. 리셋 값에는 폭을 적으세요.

2. chiseltest에서 사이클 진행하기

테스트벤치에서는 다음 세 호출이 한 사이클을 이룹니다.

호출효과
poke입력을 즉시 바꿉니다
peek현재 레지스터 값과 방금 넣은 입력으로 계산된 출력을 읽습니다
clock.step()상승 에지 하나를 실행합니다. 이후 모든 레지스터가 다음 값을 저장합니다

참고. chiseltest의 test(...)는 테스트 본문을 실행하기 전에 리셋을 한 번 인가합니다. 그래서 본문의 첫 peek에서는 리셋 값이 있는 레지스터가 이미 리셋 값을 저장하고 있습니다.

3. 레지스터 생성자

생성자리셋 값용도
RegNext(x)없음x를 한 사이클 지연합니다. 타입과 폭은 x에서 추론됩니다
RegNext(x, init)init같은 지연이며, 리셋 후 값이 정해집니다
RegInit(init)init:=로 갱신하는 상태입니다. init에서 시작합니다
Reg(t)없음리셋 없는 타입 t의 상태입니다. :=로 갱신합니다

3.1 RegNext: 한 사이클 지연

RegNext는 인자로 받은 신호를 다음 값으로 연결한 레지스터를 만들고, 그 레지스터의 현재 값을 반환합니다.

형식

val 이름 = RegNext(다음_값)
val 이름 = RegNext(다음_값, 리셋_값)
val d1 = RegNext(io.in, 0.U)   // 한 사이클 전의 in
val d2 = RegNext(d1, 0.U)      // 두 사이클 전의 in

io.in에 5, 6, 7, 8을 한 사이클씩 넣으면 d1은 0, 5, 6, 7이 되고 d2는 0, 0, 5, 6이 됩니다. RegNext를 하나 거칠 때마다 지연이 한 사이클씩 늘어납니다.

3.2 RegInit: 리셋 값이 있는 상태

RegInit은 리셋 값만 정하고 다음 값은 정하지 않은 레지스터를 만듭니다. 다음 값은 이후 :=로 연결합니다.

형식

val 이름 = RegInit(리셋_값)
이름 := 다음_값
val cnt = RegInit(0.U(4.W))
cnt := cnt + 1.U
코드의미
RegInit(0.U(4.W))4비트 레지스터를 만들고 리셋 값을 0으로 정합니다
cnt := cnt + 1.U다음 값을 현재 값 + 1로 정합니다

cnt는 0부터 15까지 센 뒤, 16이 4비트에 들어가지 않으므로 다시 0부터 셉니다.

주의. 폭을 적은 리셋 값은 레지스터의 폭을 고정합니다. RegInit(0.U)처럼 폭을 적지 않으면 레지스터 폭은 연결된 값에서 추론되며, cnt + 1.U만 연결된 위 카운터에서는 폭 추론이 1비트를 정하고, 위 카운터는 0, 1, 0, 1을 반복합니다. 레지스터 리셋 값에는 폭을 적으세요.

3.3 Reg: 리셋 값이 없는 상태

Reg는 타입만 받아 리셋 값이 없는 레지스터를 만듭니다.

형식

val 이름 = Reg(타입)
val r = Reg(UInt(8.W))
when(io.en) {
  r := io.in
}

en이 1인 사이클에는 in을 저장하고, 0인 사이클에는 연결이 없으므로 현재 값을 유지합니다.

참고. 어떤 사이클에 연결을 받지 못한 레지스터는 현재 값을 유지합니다. wire와 달리 레지스터에는 기본값 연결이 필요 없습니다. 이 규칙 덕분에 when 안에서만 연결해도 미연결 오류가 발생하지 않습니다.

4. poke는 넣고, peek는 지금 읽고, step은 에지 하나를 진행합니다

chiseltest 테스트벤치는 호출 넷으로 이루어집니다.

호출효과
dut.io.x.poke(v)입력을 즉시 v로 구동합니다. 같은 포트를 다시 poke하기 전까지 값이 유지됩니다
dut.io.y.peek()현재 레지스터 값과 방금 넣은 입력으로 포트를 지금 읽습니다
dut.clock.step()상승 에지 하나를 실행합니다. 이후 모든 레지스터가 다음 값을 저장합니다
dut.io.y.expect(v)지금 읽어 v와 다르면 실행을 실패시킵니다

조합 출력은 poke를 즉시 따라갑니다. 레지스터 출력은 step 중에만 바뀝니다. 본문이 시작되기 전에 test(...)가 리셋을 한 사이클 동안 1로 유지한 뒤 풀어 주므로, 첫 peek에서 모든 RegInit은 이미 리셋 값을 저장하고 있습니다.

Acc.scala는 누적 합을 유지합니다. acc가 레지스터이고, io.next는 레지스터 더하기 현재 입력인 조합 출력이며, io.sum은 레지스터를 보여 줍니다. acc는 en이 1인 에지에서만 io.next를 받습니다.

Acc.scala
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
}
Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Acc) { dut =>
    // 이 줄 이전에 chiseltest가 reset을 이미 한 번 인가했다.
    println(s"START sum=${dut.io.sum.peek().litValue} reset=${dut.reset.peek().litValue}")
    dut.io.en.poke(true.B)
    dut.io.in.poke(3.U)
    println(s"POKE next=${dut.io.next.peek().litValue} sum=${dut.io.sum.peek().litValue}")
    dut.clock.step()
    println(s"STEP next=${dut.io.next.peek().litValue} sum=${dut.io.sum.peek().litValue}")
    // poke한 값은 테스트벤치가 다시 poke할 때까지 유지된다.
    dut.io.en.poke(false.B)
    dut.clock.step(2)
    dut.io.sum.expect(3.U)
    println(s"HOLD sum=${dut.io.sum.peek().litValue}")
    dut.reset.poke(true.B)
    dut.clock.step()
    dut.reset.poke(false.B)
    dut.io.sum.expect(0.U)
    println("RESET sum=0 checked")
  }
}

POKE 줄과 STEP 줄을 예측하세요. POKE 줄에서는 en과 in = 3을 넣었고 에지는 아직 없습니다.

확인 문제

POKE 줄에서 next와 sum은 무엇입니까?

답

확인 문제

step()을 한 번 한 뒤 next와 sum은 무엇입니까?

답

실행을 누르세요. HOLD 줄은 en이 0인 에지 두 번 뒤에 나오고, 마지막 줄들은 reset을 보통 입력처럼 구동합니다. 에지 하나 동안 1로, 그 뒤 다시 0으로 둡니다.

5. 테스트벤치 API 자세히 보기

5.1 test와 dut handle

테스트벤치는 모듈을 test에 전달하는 것으로 시작합니다. chiseltest는 모듈을 elaboration하고 시뮬레이터를 시작한 뒤, 실행 중인 회로를 가리키는 handle을 인자로 본문을 호출합니다. 이 handle은 관례상 "시험 대상 장치"(device under test)를 줄여 dut라고 부릅니다.

형식

test(new 모듈_이름) { dut =>
  // dut의 포트를 구동하고 검사
}
test(new 모듈_이름, annotation_목록, "이름") { dut => ... }
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Inverter) { dut =>
    dut.io.in.poke(true.B)
    dut.io.out.expect(false.B)
  }
}
코드의미
import chiseltest._poke, peek, expect, step을 사용할 수 있게 합니다
chiseltest.RawTester.testScalaTest 밖에서 쓰는 test입니다. 일반 object Main extends App에서 호출할 수 있습니다
new Inverterelaboration하고 시뮬레이션할 모듈입니다
dut =>본문은 실행 중인 회로를 dut로 받습니다. dut.io.in은 모듈의 io.in 포트입니다
annotation_목록선택 인자입니다. 다음 장의 파형 출력 같은 옵션을 전달합니다
"이름"선택 인자입니다. 작업 디렉터리 이름을 정합니다

chiseltest는 테스트 라이브러리이며 시뮬레이터가 아닙니다. 회로를 시뮬레이터 backend에 전달합니다. 이 실습 환경은 기본 backend인 treadle을 사용합니다. treadle은 테스트와 같은 JVM 안에서 실행되는 FIRRTL interpreter이므로 Verilog를 생성하지 않고 외부 시뮬레이터도 실행하지 않습니다.

5.2 poke: 입력 구동

poke는 입력 포트에 값을 넣습니다. 값은 포트 타입에 맞는 Chisel 리터럴입니다. 값은 즉시 적용되고, 같은 포트를 다시 poke할 때까지 유지됩니다.

형식

dut.io.포트.poke(값)
dut.io.en.poke(true.B)      // Bool
dut.io.in.poke(3.U)         // UInt
dut.io.offset.poke(-3.S)    // SInt
dut.reset.poke(true.B)      // reset도 입력입니다
규칙지키지 않으면
입력만 poke할 수 있습니다출력을 poke하면 chiseltest.UnpokeableException: Can only poke inputs가 발생합니다
값이 포트 폭에 맞아야 합니다8비트 포트에 300.U를 poke하면 값이 0 ... 255 범위에 맞지 않는다는 ChiselException이 발생합니다

5.3 peek: 포트 읽기

peek은 포트의 현재 값을 Chisel 리터럴로 반환합니다. Scala 코드에서 그 값을 쓰려면 변환해야 합니다.

형식

dut.io.포트.peek()                 // UInt<8>(3) 같은 Chisel 리터럴
dut.io.포트.peek().litValue        // BigInt
dut.io.플래그.peek().litToBoolean   // Bool 포트의 Boolean 값
val sum = dut.io.sum.peek().litValue
println(s"sum is $sum")
if (dut.io.busy.peek().litToBoolean) dut.clock.step()
호출반환 타입주된 용도
peek()Chisel 리터럴다른 chiseltest 호출에 전달
.litValueBigInt출력, 산술, Scala 숫자와 비교
.litToBooleanBooleanif, while 조건

주의. peek한 값은 .litValue로 변환한 뒤에 Scala 숫자와 비교하세요. dut.io.sum.peek() == 3은 컴파일되지만 Chisel 리터럴과 Int를 비교하므로 언제나 false입니다.

5.4 expect: 포트 검사

expect는 포트 값과 기댓값을 비교하고, 다르면 테스트를 실패시킵니다. 두 번째 인자로 실패 메시지에 덧붙일 설명을 줄 수 있습니다.

형식

dut.io.포트.expect(값)
dut.io.포트.expect(값, "설명")
dut.io.sum.expect(7.U, "sum after 3 + 4")

검사가 실패하면 chiseltest는 정해진 모양의 메시지를 담은 FailedExpectException을 발생시킵니다.

메시지 부분의미
In step N본문이 실행한 클록 step 수입니다. 리셋 사이클은 세지 않습니다
io_sum변환된 회로의 포트 이름입니다. 점이 밑줄로 바뀝니다
=7 (0x7)포트의 실제 값입니다
expected=8 (0x8)expect에 전달한 기댓값입니다
: 설명두 번째 인자로 준 설명입니다

예외를 잡지 않으면 테스트가 그 자리에서 끝나므로, 실패한 expect 뒤의 줄은 실행되지 않습니다.

5.5 clock.step: 시간 진행

clock.step은 클록을 상승 에지 하나 이상만큼 진행합니다. 상승 에지마다 모든 레지스터가 동시에 다음 값으로 바뀝니다.

형식

dut.clock.step()     // 상승 에지 하나
dut.clock.step(n)    // 상승 에지 n개

poke, peek, expect는 모두 클록 에지 사이에서 실행됩니다. 조합 논리는 입력이 바뀌는 즉시 다시 계산되므로 poke 바로 뒤의 peek은 새 조합 결과를 읽습니다. 레지스터 출력은 step 중에만 바뀝니다.

출력 종류poke에 반응step에 반응
a + b 같은 조합 출력step 없이 즉시 바뀝니다읽는 레지스터가 바뀌면 함께 바뀝니다
RegInit(...) 같은 레지스터 출력바뀌지 않습니다상승 에지마다 바뀝니다

6. 카운터는 두 줄입니다

Blinker.scala에는 Scala 함수가 둘 있습니다. def wrapCounter(max: Int): UInt = { ... }는 Scala Int를 받아 하드웨어 UInt를 반환하는 함수를 선언합니다. 본문이 레지스터를 만들어 반환하므로 호출마다 새 레지스터 하나가 생깁니다. log2Ceil(max + 1)은 0부터 max까지 담는 데 필요한 비트 수를 Scala 숫자로 계산합니다. Mux가 max에서 카운터를 0으로 되돌립니다. Mux가 없으면 레지스터는 2의 거듭제곱에서 되돌아갑니다. toggle(p)는 p가 참인 사이클마다 Bool 레지스터를 뒤집습니다.

Blinker.scala
import chisel3._
import chisel3.util.log2Ceil

class Blinker(n: Int) extends Module {
  val io = IO(new Bundle {
    val count = Output(UInt(4.W))
    val pulse = Output(Bool())
    val wave  = Output(Bool())
  })

  // 0부터 max까지 센 뒤 0으로 돌아간다.
  def wrapCounter(max: Int): UInt = {
    val x = RegInit(0.U(log2Ceil(max + 1).W))
    x := Mux(x === max.U, 0.U, x + 1.U)
    x
  }

  // p가 참인 사이클마다 상태를 반전한다.
  def toggle(p: Bool): Bool = {
    val x = RegInit(false.B)
    x := Mux(p, !x, x)
    x
  }

  val count = wrapCounter(n - 1)
  val pulse = count === 0.U
  io.count := count
  io.pulse := pulse
  io.wave  := toggle(pulse)
}
Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new Blinker(3)) { dut =>
    for (cycle <- 0 until 9) {
      val c = dut.io.count.peek().litValue
      val p = dut.io.pulse.peek().litValue
      val w = dut.io.wave.peek().litValue
      println(s"cycle=$cycle count=$c pulse=$p wave=$w")
      dut.clock.step()
    }
  }
}

테스트벤치는 에지 아홉 번마다 그 직전에 count, pulse, wave를 출력합니다. pulse는 count === 0.U인 조합 출력입니다. wave는 pulse 사이클을 끝내는 에지에서 뒤집힌 값을 저장하는 레지스터입니다.

확인 문제

사이클 0에서 pulse가 1입니다. 사이클 0과 사이클 1의 wave는 각각 무엇입니까?

답

실행하고 세 열을 함께 읽으세요. pulse는 세 사이클마다 나오고, wave는 pulse마다 한 사이클 뒤에 바뀌므로 주기가 여섯입니다.

6.1 레지스터를 반환하는 Scala 함수

레지스터를 만들고 그 레지스터를 반환하는 Scala 함수를 정의하면 상태가 있는 작은 블록을 재사용할 수 있습니다. 함수를 호출할 때마다 새 레지스터가 하나씩 만들어지고, 호출한 쪽은 레지스터의 현재 값을 받습니다.

형식

def 함수_이름(인자: 타입): 반환_타입 = {
  val r = RegInit(리셋_값)
  r := 다음_값
  r
}

6.2 log2Ceil로 폭 계산하기

chisel3.util.log2Ceil(n)은 n가지 값을 구분하는 데 필요한 비트 수를 Scala 정수로 계산합니다. 0부터 max까지 세는 카운터에는 log2Ceil(max + 1)비트가 필요합니다.

maxlog2Ceil(max + 1)표현 범위
220–3
430–7
730–7

7. poke 하나를 지워 보세요

Acc 테스트벤치에서 dut.io.en.poke(false.B) 줄을 지우고 다시 실행하세요.

확인 문제

en을 0으로 넣지 않으면 HOLD 줄 앞의 expect에서 무슨 일이 생깁니까?

답

8. 적용: 끝나지 않는 카운트다운

아래 채점 실습 문제는 done이 올라오지 않는 카운트다운입니다. 테스트벤치의 대기 루프가 chiseltest의 유휴 사이클 타임아웃에 걸릴 때까지 step을 반복합니다. 출력되는 WAIT 줄을 읽어 left가 더 이상 바뀌지 않는 값을 찾고, 감소를 지키는 조건을 "0이 될 때까지 센다"는 규칙과 비교하세요.

9. 자주 하는 실수

9.1 레지스터에 연결한 값을 같은 사이클에 읽으려 하기

// 잘못된 생각: out이 이번 사이클에 in과 같아진다
val r = RegInit(0.U(8.W))
r := io.in
io.out := r          // 이번 사이클에는 이전 값이 출력됩니다

// 바른 코드: 같은 사이클이 필요하면 레지스터를 거치지 않음
io.out := io.in

r := io.in은 다음 에지에서 저장할 값을 정할 뿐입니다. in에 9를 넣은 사이클에 out을 읽으면 이전 값이 보이고, 에지를 하나 지난 뒤에야 9가 보입니다. 출력이 예상보다 한 사이클 늦게 반응하면 신호가 거친 레지스터를 세어 보세요.

9.2 리셋 값에 폭을 적지 않기

// 잘못된 코드: 폭 추론이 1비트를 정함
val cnt = RegInit(0.U)
cnt := cnt + 1.U     // 0, 1, 0, 1, ...

// 바른 코드
val cnt = RegInit(0.U(8.W))
cnt := cnt + 1.U     // 0, 1, 2, 3, ...

0.U는 1비트 리터럴이고 cnt + 1.U도 cnt보다 넓어지지 않으므로, 폭 추론은 레지스터를 1비트로 정합니다. 출력 포트가 8비트여도 카운터는 0과 1만 반복합니다.

9.3 wrap 조건 없이 max까지만 세기를 기대하기

// 잘못된 코드: 2비트 레지스터이므로 0–3을 셈
val x = RegInit(0.U(log2Ceil(3).W))
x := x + 1.U

// 바른 코드: max에서 0으로 되돌림
val x = RegInit(0.U(log2Ceil(3).W))
x := Mux(x === 2.U, 0.U, x + 1.U)

레지스터 폭은 저장할 수 있는 범위만 정합니다. 카운터를 max에서 0으로 되돌리는 것은 Mux 하나뿐이므로, 그 Mux가 없으면 카운터는 2의 거듭제곱에서 순환합니다.

9.4 peek한 리터럴을 Scala 숫자와 바로 비교하기

// 잘못된 코드: 언제나 false
if (dut.io.sum.peek() == 3) println("three")

// 바른 코드
if (dut.io.sum.peek().litValue == 3) println("three")

peek()은 UInt<8>(3) 같은 Chisel 리터럴을 반환하며, 이 리터럴은 Int 3과 같지 않습니다. .litValue나 .litToBoolean으로 먼저 변환하거나, 기댓값을 안다면 expect를 쓰세요.

9.5 step 없이 레지스터 검사하기

dut.io.in.poke(3.U)
// 잘못된 코드: 레지스터는 아직 에지를 지나지 않았습니다
dut.io.sum.expect(3.U)

// 바른 코드
dut.io.in.poke(3.U)
dut.clock.step()
dut.io.sum.expect(3.U)

레지스터는 step 중에만 바뀝니다. 조합 출력은 poke 직후에 검사할 수 있지만 레지스터 출력은 step이 한 번 이상 필요합니다.

9.6 poke가 유지된다는 사실을 잊기

dut.io.en.poke(true.B)
dut.clock.step()
// 잘못된 코드: en이 여전히 1이므로 계속 누적됩니다
dut.clock.step(2)

// 바른 코드: en을 명시적으로 끕니다
dut.io.en.poke(false.B)
dut.clock.step(2)

입력은 다시 poke할 때까지 마지막 값을 유지합니다. 건드리지 않은 값이 나중 step에서 저절로 바뀐 것처럼 보이면 아직 유효한 이전 poke를 의심하세요.

9.7 출력 포트에 poke하기

// 잘못된 코드: UnpokeableException 발생
dut.io.sum.poke(1.U)

// 바른 코드: sum을 결정하는 입력을 구동한 뒤 검사합니다
dut.io.in.poke(1.U)
dut.clock.step()
dut.io.sum.expect(1.U)

출력은 회로가 구동합니다. 원하는 출력 값을 얻으려면 입력을 구동하고 설계가 그 값을 만들게 하세요.

10. 정리

요소뜻
RegInit(v)리셋 값이 v인 레지스터. v에 적은 폭이 레지스터 폭을 고정
:=의 오른쪽현재 값. 이번 사이클에 읽힘
:=의 왼쪽다음 값. 다음 상승 에지에서 저장
poke, peek, expect에지 사이에서 넣기, 지금 읽기, 지금 검사하기
clock.step()상승 에지 하나. 레지스터는 여기서만 움직임
def f(x: Int): UInt = { ... }Scala 함수. 호출마다 하드웨어를 다시 만듦

리셋 값 없는 Reg, RegNext, 리셋 타입은 순차 회로 장에, 테스트벤치 API와 유휴 사이클 타임아웃은 테스트벤치 장에 있습니다. 다음 수업에서는 조건에 따라 레지스터를 갱신하고, 리셋이 건드리는 것과 건드리지 않는 것을 봅니다.

11. 확인 문제

  1. x := x + 1.U에서 이번 사이클에 읽히는 x와 에지 후에 바뀌는 x는 각각 어느 쪽일까요?

    정답 보기

    오른쪽 x가 이번 사이클에 읽히는 현재 값이고, 왼쪽 x가 다음 상승 에지에서 저장되는 다음 값입니다.

  2. 에지 예제의 사이클 2에서 in이 여전히 1인데 rise가 0인 이유는 무엇일까요?

    정답 보기

    사이클 1의 끝 에지에서 last가 1을 저장했습니다. 사이클 2에서는 in과 last가 모두 1이므로 io.in && !last가 거짓입니다.

  3. 구형파 주기가 카운터 주기의 두 배이고, wave가 각 펄스 한 사이클 뒤에 바뀌는 이유는 무엇일까요?

    정답 보기

    토글은 펄스 하나마다 한 번 반전하므로 같은 레벨로 돌아오려면 펄스가 두 번 필요합니다. 또 토글 레지스터는 펄스 사이클의 끝 에지에서 반전된 값을 저장하므로 변화는 다음 사이클에 보입니다.

  4. RegInit(0.U)로 만든 카운터가 0과 1만 반복하는 이유는 무엇일까요?

    정답 보기

    리셋 값 0.U가 1비트이고, x + 1.U의 폭도 x의 폭보다 넓어지지 않습니다. 폭 추론이 레지스터를 1비트로 정하므로 1 다음 값이 0으로 잘립니다.

  5. 리셋이 비동기라면 reset-before-edge 줄에서 어떤 레지스터가 바뀌어야 하며, 지금의 테스트벤치로 그 동작을 직접 보일 수 없는 이유는 무엇일까요?

    정답 보기

    비동기 리셋은 에지를 기다리지 않으므로 리셋 값이 있는 withInit과 nextInit이 각각 5와 0으로 바로 바뀌어야 합니다. 하지만 RequireAsyncReset으로 리셋 타입을 바꾸면 테스트벤치의 dut.reset.poke(true.B)가 LiteralTypeException으로 멈추므로 그 줄까지 실행되지 않습니다.

확인 지점

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