본문으로 건너뛰기
code0.HDL

조건부 갱신·유지·리셋

when 아래에서만 레지스터를 갱신하고 그 밖에서는 유지하게 하며, 생성 방식으로 리셋이 바꾸는 레지스터를 가려내고, 암묵적 리셋이 클록 에지에서 작용함을 확인하고, 우선순위를 소스 순서로 씁니다.

이 장에서 배울 내용

  • 레지스터를 when 안에서만 연결하고, 기본값이 필요 없는 이유를 설명합니다
  • 생성자만 보고 리셋이 그 레지스터를 바꾸는지, 첫 리셋 전의 값이 정해져 있는지 가려냅니다
  • 동기식인 암묵적 리셋이 클록 에지 전후에 하는 일을 예측하고, 리셋 타입 셋을 구분합니다
  • load·count·clear의 우선순위를 when 사슬의 순서로 씁니다
이 장에서 배우는 개념
  • when을 이용한 갱신·리셋
먼저 알아야 할 개념

1. when 아래에서 갱신하는 레지스터는 다른 사이클에서 값을 유지합니다

리셋이 비활성일 때, 레지스터의 연결을 when 안에 두면 조건이 성립하는 에지에서만 새 값을 받습니다. 다른 모든 사이클에서는 연결이 없으므로 현재 값을 유지합니다. 공통 기초의 enable이 바로 이것입니다. 빠진 경로가 오류가 되지 않는 유일한 경우이기도 합니다. 어떤 경로에서 구동되지 않는 wire는 시뮬레이션 전에 거절되지만, 레지스터는 그냥 유지하므로 기본값이나 .otherwise가 필요하지 않습니다. Mux·when·Wire 장이 wire와 출력에 적용한 미연결 검사는 when 안에서만 연결한 레지스터에는 적용되지 않습니다.

형식

val r = RegInit(0.U(4.W))
when (io.en) {
  r := io.in
}
사이클en에지가 r에 하는 일
모두1io.in을 저장
모두0현재 값 유지

2. 우선순위는 소스 순서로 씁니다

한 사이클에서 레지스터로 가는 연결 둘이 모두 적용될 수 있으면 Mux·when·Wire 장의 규칙이 정합니다. when/.elsewhen 사슬은 첫 번째 참 분기를 쓰고, 별개의 문장 사이에서는 나중 연결이 이깁니다. 카운트보다 앞서야 하는 load는 사슬의 첫 분기에 씁니다. 모든 것보다 앞서야 하는 clear는 사슬의 첫 분기에 쓰거나 마지막 별개 when으로 씁니다.

형식

when (io.load) {
  count := io.cycles
} .elsewhen (count =/= 0.U) {
  count := count - 1.U
}

load와 0이 아닌 count가 모두 참이면 레지스터는 load합니다. 감소 분기에는 도달하지 않습니다. 둘 다 거짓이면 연결이 없어 레지스터는 유지합니다.

3. 리셋은 리셋 값을 선언한 레지스터만 바꿉니다

리셋은 전체를 지우는 신호가 아닙니다. 레지스터마다 선언에 속하며, 리셋 값을 선언한 레지스터에만 적용됩니다.

생성 방식리셋 값리셋이 1인 동안 에지에서
RegInit(v)v다음 값 대신 v를 저장
RegNext(x, v)v같음
Reg(t)없음리셋 연결이 없음. 다음 값 논리를 그대로 따름
RegNext(x)없음같음

리셋 값이 없는 레지스터는 시작 값도 정해져 있지 않습니다. 시뮬레이터 treadle은 이런 레지스터를 0으로 시작하지만 하드웨어는 그 값을 보장하지 않습니다. 플래그, valid 비트, 카운터 같은 제어 상태는 어디서 시작하는지가 정확성을 정하므로 리셋 값이 필요합니다.

최상위 모듈의 암묵적 reset은 동기 리셋입니다. 1이 되는 순간이 아니라 클록 에지에서 작용합니다. chiseltest는 본문이 실행되기 전에 리셋을 한 사이클 인가하므로, 본문의 첫 peek에서 리셋 값이 있는 레지스터는 이미 리셋 값을 저장하고 있습니다. 테스트벤치는 dut.reset.poke(true.B)로 리셋을 보통 입력처럼 구동할 수도 있으며, 그 뒤 step이 있기 전까지는 아무것도 바뀌지 않습니다.

4. 리셋 하나를 지나는 레지스터 네 개

ResetDemo는 3절의 네 방식으로 만든 레지스터 넷을 가집니다.

레지스터생성다음 값
withInitRegInit(5.U(4.W))en이 1일 때 in
noInitReg(UInt(4.W))en이 1일 때 in
nextRegNext(io.in)매 사이클 in
nextInitRegNext(io.in, 0.U(4.W))매 사이클 in
ResetDemo.scala
import chisel3._

class ResetDemo extends Module {
  val io = IO(new Bundle {
    val en       = Input(Bool())
    val in       = Input(UInt(4.W))
    val withInit = Output(UInt(4.W))
    val noInit   = Output(UInt(4.W))
    val next     = Output(UInt(4.W))
    val nextInit = Output(UInt(4.W))
  })

  // 리셋이 이 레지스터에 5를 저장한다.
  val withInit = RegInit(5.U(4.W))
  // 리셋 값이 없으므로 리셋이 이 레지스터를 바꾸지 않는다.
  val noInit = Reg(UInt(4.W))
  when(io.en) {
    withInit := io.in
    noInit   := io.in
  }

  io.withInit := withInit
  io.noInit   := noInit
  io.next     := RegNext(io.in)
  io.nextInit := RegNext(io.in, 0.U(4.W))
}

테스트벤치는 처음에 withInit만 출력합니다. 리셋 값이 없는 레지스터의 시작 값에 의존하지 않기 위해서입니다. 이어서 모든 레지스터에 9를 저장하고, en을 0, in을 6으로 바꾸고 reset을 1로 넣은 뒤 에지 전에 출력하고, 한 번 step한 뒤 다시 출력하고, 마지막으로 리셋을 풀고 한 번 더 step합니다.

Tb.scala
import chisel3._
import chiseltest._
import chiseltest.RawTester.test

object Main extends App {
  test(new ResetDemo) { dut =>
    def show(phase: String): Unit = {
      val v = Seq(dut.io.withInit, dut.io.noInit, dut.io.next, dut.io.nextInit).map(_.peek().litValue)
      println(s"$phase withInit=${v(0)} noInit=${v(1)} next=${v(2)} nextInit=${v(3)}")
    }
    println(s"start withInit=${dut.io.withInit.peek().litValue}")

    // 모든 레지스터에 9를 저장한다.
    dut.io.en.poke(true.B)
    dut.io.in.poke(9.U)
    dut.clock.step()
    show("loaded")

    // in을 6, en을 0으로 두고 에지 하나 동안 리셋을 인가한다.
    dut.io.en.poke(false.B)
    dut.io.in.poke(6.U)
    dut.reset.poke(true.B)
    show("reset-before-edge")
    dut.clock.step()
    show("reset-after-edge")

    dut.reset.poke(false.B)
    dut.clock.step()
    show("released")
  }
}

이 개념 실습하기

다음 단계로 짧은 Chisel 문제에 이 개념을 적용해 보세요.

실행하기 전에 예측해 보세요. start 줄의 withInit은 무엇일까요? 이어서 reset을 1로 바꾼 직후, 에지 전에 출력되는 reset-before-edge 줄과 reset-after-edge 줄의 네 값을 예측하세요.

확인 문제

reset을 1로 넣은 직후이고 에지는 없었습니다. reset-before-edge 줄은 무엇을 보입니까?

답

확인 문제

reset이 1이고 en이 0인 에지 뒤에 noInit은 무엇을 보입니까?

확인 문제

같은 reset-after-edge 줄에서 next와 nextInit은 무엇을 보입니까?

답

결과 확인. 실행을 누르고 다섯 줄을 비교하세요.

출력 줄확인할 내용
startchiseltest가 본문 전에 리셋을 인가했으므로 withInit은 5입니다
loadeden이 1인 에지를 지나 네 레지스터 모두 9입니다
reset-before-edge네 값 모두 9 그대로입니다. 리셋을 인가하기만 해서는 레지스터가 바뀌지 않습니다
reset-after-edgewithInit은 리셋 값 5, noInit은 en이 0이라 9를 유지, next는 리셋과 무관하게 in을 따라 6, nextInit은 리셋 값 0입니다
releasedwithInit과 noInit은 en이 0이라 유지되고, nextInit은 다시 in을 따라 6이 됩니다

reset-before-edge 줄에서 아무 값도 바뀌지 않았으므로 이 모듈의 리셋은 클록 에지에서만 적용되는 동기 리셋입니다.

5. 리셋 값 하나를 없애 보세요

실행 패널에서 withInit의 생성을 RegInit(5.U(4.W))에서 Reg(UInt(4.W))로 바꾸고, 다시 실행하기 전에 reset-after-edge 줄을 예측해 보세요.

확인 문제

withInit을 Reg(UInt(4.W))로 만들면 reset-after-edge 줄에서 무엇을 보입니까?

6. 리셋 타입

6.1 세 가지 리셋 타입

Chisel의 리셋 타입은 세 가지이며 모두 trait Reset을 구현합니다.

타입생성생성되는 레지스터
BoolBool()동기 리셋 레지스터
AsyncResetAsyncReset()비동기 리셋 레지스터
ResetReset()추상 리셋입니다. FIRRTL 컴파일 중 구체 타입이 추론됩니다

Module의 암묵적 reset은 추상 리셋이며, 그 리셋을 구동하는 신호에서 구체 타입이 추론됩니다.

추상 리셋을 구동하는 신호추론 결과
AsyncReset, 추상 Reset, DontCare만AsyncReset
Bool과 AsyncReset이 함께오류
그 밖의 경우Bool

최상위 모듈의 리셋은 아무것도 구동하지 않으므로 Bool이 됩니다. 그래서 최상위로 쓴 ResetDemo의 리셋은 동기 리셋이고, 출력한 CHIRRTL은 input reset : UInt<1>을 선언합니다. 추상 리셋 덕분에 같은 블록을 수정 없이 동기 리셋 설계와 비동기 리셋 설계 양쪽에 재사용할 수 있습니다.

6.2 리셋 타입 고정하기

모듈에 trait를 섞으면 암묵적 리셋의 타입을 고정할 수 있습니다.

형식

class 모듈_이름 extends Module with RequireSyncReset   // reset은 Bool
class 모듈_이름 extends Module with RequireAsyncReset  // reset은 AsyncReset

ResetDemo의 선언을 class ResetDemo extends Module with RequireAsyncReset으로 바꾸고 CHIRRTL을 출력하면 포트가 input reset : AsyncReset으로 바뀌고 레지스터 선언은 그대로 남습니다. 이 상태에서 테스트벤치를 실행하면 dut.reset.poke(true.B)에서 chiseltest.LiteralTypeException: don't know how to poke 오류로 멈춥니다. 포트 타입이 실제로 AsyncReset이 되었으므로 Bool 값을 인가할 수 없습니다.

6.3 withReset으로 일부 영역의 리셋 바꾸기

withReset은 블록 안에서 만든 레지스터가 암묵적 리셋 대신 지정한 신호를 리셋으로 쓰게 합니다.

형식

withReset(리셋_신호) {
  // 이 블록 안에서 만든 레지스터는 리셋_신호로 리셋됩니다
}
val r = withReset(io.clear) { RegInit(0.U(8.W)) }
r := r + 1.U

io.clear가 Bool이므로 r은 동기 리셋 레지스터가 됩니다. clear를 1로 바꿔도 r은 바로 0이 되지 않고, 다음 에지를 지난 뒤 0이 됩니다.

주의. reset.asBool과 reset.asAsyncReset 형변환은 컴파일러가 검사하지 않습니다. 리셋의 실제 방식을 알고 있을 때만 사용하세요.

7. 자주 하는 실수

7.1 리셋을 인가하자마자 값이 바뀌기를 기대하기

// 잘못된 기대: 동기 리셋은 아직 적용되지 않았습니다
dut.reset.poke(true.B)
dut.io.withInit.peek()   // 이전 값 그대로

// 바른 순서: 에지를 하나 지난 뒤 확인
dut.reset.poke(true.B)
dut.clock.step()
dut.io.withInit.peek()   // 리셋 값

동기 리셋은 클록 에지에서만 적용됩니다. 테스트벤치에서 리셋 효과를 확인하려면 리셋을 인가한 상태로 clock.step()을 한 번 실행하세요.

8. 적용: 리셋 뒤에도 남는 플래그

아래 채점 실습 문제에는 오류 카운터 errors 옆에 오류 플래그 seen이 있습니다. 리셋 뒤 카운터는 0인데 플래그는 켜진 채이고, 첫 오류 전에는 플래그 값이 정해져 있지 않습니다. 두 선언을 3절의 표와 대조하세요. 바꿔야 할 선언은 하나입니다. 실행을 누르기 전에 출력되는 다섯 단계의 사이클별 표를 먼저 써 보세요.

9. 정리

규칙결과
when 안에서만 연결한 레지스터조건이 거짓인 사이클마다 유지. 기본값 불필요
우선순위사슬의 첫 참 분기. 별개 문장은 나중 것이 이김
RegInit(v), RegNext(x, v)리셋이 1인 에지에서 v로
Reg(t), RegNext(x)리셋과 무관. 시작 값 미정
최상위의 암묵적 리셋동기 리셋. poke한 뒤 step해야 효과가 보임
리셋 타입암묵적 리셋은 추상 Reset이며 최상위에서는 Bool로 추론됨. RequireSyncReset과 RequireAsyncReset으로 고정하고, withReset으로 일부 영역만 바꿈

10. 확인 문제

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

    정답 보기

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

확인 지점

실습 문제를 제출해 채점을 받습니다

채점은 서버의 비공개 테스트로 진행되며, 결과는 이 브라우저에만 기록됩니다.

참고 자료

참고한 자료의 라이선스와 고지