simulation scheduling: 클록이 바뀌어도 출력은 아직 이전 값일 수 있다
클록이 1이 되는 순간을 기다렸는데 레지스터에는 이전 값이 남아 있다. 한 번 더 기다리면 기대한 값이 나온다. 이런 실패를 만나면 DUT가 한 사이클 늦게 계산한다고 생각하기 쉽다. 그러나 사건 기반 시뮬레이터에서는 같은 시뮬레이션 시각 안에서도 클록 변화와 순차 회로 실행, 출력 갱신이 서로 다른 단계에 놓인다.
simulation scheduling을 이해하면 시간을 얼마나 기다릴지보다 어느 단계의 값을 읽을지를 먼저 결정할 수 있다.
같은 시각 안에서 일이 진행된다
cocotb 2.0.0의 timing model은 한 time step 안에 HDL 계산과 값 변화, 안정화 단계가 반복될 수 있다고 설명한다. 이 반복은 시뮬레이션 시간을 앞으로 보내지 않는다. 클록이 바뀐 시각이 10ns라면 레지스터와 조합 출력의 갱신까지 모두 10ns에 일어날 수 있다. 파형의 시간축만 보면 붙어 있지만, 읽을 수 있는 순간은 같지 않다. cocotb, The Timing Model
설명용으로 다음 회로를 생각하자. reset 이후 q=0이고, d=1은 상승 edge 전에 충분히 안정되어 있다.
always_ff @(posedge clk)
q <= d;
assign y = q ^ 1'b1;
사건 기반 시뮬레이터에서 RisingEdge(clk)를 기다린 코루틴은 클록 값이 1로 바뀐 시점에 깨어날 수 있다. 이때 클록에 반응하는 HDL이 아직 실행되지 않았다면 q=0, y=1이다. 순차 블록이 실행되어 q가 1로 갱신되고 조합 계산까지 끝나면 q=1, y=0이 된다.
| 읽는 단계 | clk | q | y |
|---|---|---|---|
| 클록 변화 직후, 관련 HDL 실행 전 | 1 | 0 | 1 |
| 그 time step의 계산이 끝난 뒤 | 1 | 1 | 0 |
두 행은 다른 클록 사이클을 뜻하지 않는다. 같은 상승 edge에 대한 계산이 진행되는 서로 다른 순간이다. 따라서 첫 행을 보고 ‘DUT가 입력을 놓쳤다’고 판정하면 테스트벤치가 오류를 만든다.
edge와 계산 완료는 다른 기다림이다
cocotb 문서는 RisingEdge, FallingEdge, ValueChange가 관찰하는 신호 변화와 그 변화에 따른 HDL 실행을 구분한다. edge는 사건의 시작을 알려 준다. 그 뒤 안정된 결과를 읽으려면 목적에 맞는 단계를 추가로 기다려야 한다.
다음 코드는 단일 비트 d를 하강 edge에 설정하고 다음 상승 edge의 최종 결과를 확인하는 예다. 클록이 계속 동작하고 reset이 해제되어 있으며, 다른 코루틴이 d를 동시에 바꾸지 않는다고 가정한다.
from cocotb.triggers import FallingEdge, RisingEdge, ReadOnly
await FallingEdge(dut.clk)
dut.d.value = 1
await RisingEdge(dut.clk)
await ReadOnly()
if int(dut.q.value) != 1 or int(dut.y.value) != 0:
raise AssertionError("settled result mismatch")
ReadOnly()는 현재 time step의 끝에서 값을 읽도록 한다. 이 단계에서는 HDL 계산이 끝났고 값을 쓰는 것은 허용되지 않는다. 결과를 검사한 뒤 다음 입력을 바꾸려면 다음의 쓰기 가능한 시점으로 이동해야 한다. 같은 ReadOnly 단계 안에서 입력을 다시 쓰는 코드는 이 원칙과 맞지 않는다. cocotb, Values Change 및 End of Time Step
ReadWrite()는 현재 evaluation cycle의 계산이 안정된 시점으로 이동하지만, 그곳에서 다시 값을 쓰면 계산이 더 이어질 수 있다. 최종 관찰을 하려는 코드와 다음 계산을 유발하려는 코드는 이 차이를 기준으로 기다림을 선택한다.
다른 시뮬레이터에서는 문제가 숨어 보일 수 있다
여기서 ‘edge 직후에는 항상 이전 값’이라고 일반화하면 또 다른 오해가 생긴다. 같은 cocotb 문서는 Verilator의 동작 차이를 따로 설명한다. Verilator는 값 변화 중간마다 코루틴을 깨우는 대신 계산이 안정될 때까지 처리하므로, 관련 edge trigger가 Values Settle 단계에서 처리될 수 있다. 문서는 이를 암묵적인 ReadWrite와 같은 효과로 설명한다. cocotb, Differences in Verilator
그래서 edge만 기다리는 검사가 한 환경에서는 통과하고 다른 사건 기반 환경에서는 실패할 수 있다. 이는 곧바로 회로 동작이 다르다는 증거가 아니다. 테스트벤치가 어떤 단계의 값을 요구하는지 명시하지 않았는지 먼저 봐야 한다. 최종 결과를 원한다면 명시적인 ReadOnly가 읽기 의도를 드러낸다.
작은 Timer를 덧붙이는 방법은 시뮬레이션 시간을 실제로 이동시킨다. 그 지연 사이에 다른 사건이 생기거나 time precision 설정이 바뀌면, 원래 검사하려던 순간과 멀어질 수 있다. 계산 완료를 기다리는 목적이라면 단계를 기다리는 쪽이 더 직접적이다.
다만 모든 관찰을 ReadOnly로 옮기는 것도 답은 아니다. 상승 edge에서 받아들인 입력을 기록하는 monitor는 edge 직전의 유효한 입력을 기준으로 해야 할 수 있다. 순차 회로가 edge 이후 valid를 내렸다면 최종 값만 읽어서 방금 성립한 전송을 놓칠 수 있다. 입력 전송의 판정과 출력 계산 완료의 검사는 각각의 인터페이스 계약에 맞는 시점을 정해야 한다.
simulation scheduling의 핵심은 ‘조금 늦게 읽자’가 아니다. edge가 발생했다는 사실과 그 edge의 계산이 끝났다는 사실을 구별하는 것이다. 이전 값이 보였을 때 시간을 늘리기 전에 읽기 단계부터 확인하면, 회로의 실제 지연과 테스트벤치의 관찰 오류를 나눌 수 있다.