Altifigence Academy

58 / 63 · 개념

FIFO·메모리 검증 계획과 독립 scoreboard

구현 내부의 포인터에 의존하지 않는 queue scoreboard를 작성하고, 경계 조건·reset·valid·초기화하지 않은 주소의 읽기를 검증 계획에 포함합니다.

기대값은 DUT와 독립적으로 계산하기

검증기는 명세에서 구한 예상값과 DUT의 출력을 비교합니다. FIFOFIFO 먼저 넣은 데이터를 먼저 꺼내는 큐입니다. 가득 차거나 비어 있을 때의 처리 방식과 읽기·쓰기를 동시에 허용할 조건도 정해야 합니다. 자세히 보기 내부의 rptr, wptr를 복사해 예상값을 계산하면 같은 포인터 오류를 반복할 수 있습니다. 독립적인 scoreboardscoreboard 검증 환경에서 예상 데이터와 실제 출력을 순서대로 대조하는 데 쓰는 참조 모델입니다. 구현 내부 상태를 그대로 복사하지 않고 독립적으로 계산합니다.는 SystemVerilog queue를 사용해 데이터의 순서만 저장합니다. push를 수락하면 뒤에 추가하고, pop을 수락하면 앞에서 꺼냅니다.

검증 계획에는 상태와 요청의 조합을 적습니다. empty·일부만 채워진 상태·full 각각에서 요청이 없는 경우, push만 있는 경우, pop만 있는 경우, 두 요청이 동시에 있는 경우를 확인합니다. 포인터가 반복해서 처음으로 돌아오는 wrap-around, 연속 pop, 데이터가 남아 있을 때의 reset도 확인합니다. 각 경우를 검사했는지 기록하고, 실패를 재현할 수 있도록 난수 seed와 실패한 입력을 남깁니다.

요청을 수락해야 하는지는 DUT의 full이나 occupancy가 아니라, queue의 이전 크기 NN으로 계산합니다.

q=pop∧(N>0)p=push∧((N<4)∨q)N+=N+p−q\begin{aligned} q&=\mathrm{pop}\land(N>0)\\ p&=\mathrm{push}\land((N<4)\lor q)\\ N^{+}&=N+p-q \end{aligned}

기존 head를 먼저 꺼낸 뒤 새 데이터를 붙여야, full에서 두 요청이 동시에 와도 이전 head를 올바르게 비교할 수 있습니다. 예상한 값이 q=1q=1일 때만 dout을 비교합니다.

전체 테스트벤치

아래는 앞 강의의 fifo4와 함께 컴파일하는 전체 테스트벤치입니다. 지연과 queue를 사용하며, 입력은 하강 에지에서 바꿉니다. 상승 에지로부터 1ps 뒤에 DUT의 nonblockingnonblocking 순차 RTL에서 <=로 표현하는 대입입니다. 우변을 평가한 뒤 갱신을 예약하므로 같은 에지의 레지스터들이 이전 상태를 기준으로 계산할 수 있습니다. 자세히 보기 대입이 반영된 결과를 비교합니다.

SystemVerilog
module tb_fifo4;
  timeunit 1ns;
  timeprecision 1ps;
  typedef logic [7:0] word_t;
  logic clk=0, rst=1, push=0, pop=0;
  word_t din=0, dout;
  logic dout_valid, full, empty;
  logic [2:0] occupancy;
  word_t model[$], expected;
  bit expect_pop, expect_push;
  fifo4 dut (.*);
  always #5ns clk = ~clk;

  always @(posedge clk) begin
    if (rst) begin
      model.delete();
      expect_pop = 0;
    end else begin
      expect_pop = pop && (model.size() != 0);
      expect_push = push && ((model.size() < 4) || expect_pop);
      if (expect_pop) expected = model.pop_front();
      if (expect_push) model.push_back(din);
    end
    #1ps;
    assert (dout_valid === expect_pop)
      else $fatal(1, "valid mismatch");
    if (expect_pop)
      assert (dout === expected)
        else $fatal(1, "data mismatch");
    assert (int'(occupancy) === model.size())
      else $fatal(1, "count mismatch");
    assert (empty === (model.size() == 0))
      else $fatal(1, "empty mismatch");
    assert (full === (model.size() == 4))
      else $fatal(1, "full mismatch");
  end

  task automatic step(input bit r, p, o, input word_t d);
    @(negedge clk);
    rst=r; push=p; pop=o; din=d;
  endtask

  initial begin
    step(0,0,1,0);
    step(0,1,1,8'hA0);
    for (int i=1; i<4; i++)
      step(0,1,0,word_t'(160+i));
    step(0,1,0,8'hEE);
    step(0,1,1,8'hB0);
    repeat (5) step(0,0,1,0);
    repeat (3) begin
      for (int i=0; i<4; i++) step(0,1,0,word_t'(i));
      repeat (4) step(0,0,1,0);
    end
    step(0,1,0,8'h5A);
    step(1,1,1,8'hFF);
    step(0,0,1,0);
    repeat (100)
      step(0,bit'($urandom_range(1,0)),bit'($urandom_range(1,0)),
           word_t'($urandom));
    repeat (4) step(0,0,1,0);
    @(negedge clk);
    $display("PASS");
    $finish;
  end
endmodule

첫 상승 에지에서는 rst=1이므로 DUT와 모델을 비웁니다. ===는 X까지 엄격히 비교합니다. valid가 0일 때는 초기화하지 않은 dout을 비교하지 않지만, valid 자체가 X이면 실패합니다. 마지막 네 번의 pop으로 남은 데이터를 모두 검사합니다. PASS는 이 테스트를 실제로 실행해 모든 assertion을 통과했을 때 나오는 메시지입니다. 여기 제시한 코드를 이미 실행했다는 뜻은 아닙니다.

쓰기 데이터 B0를 넣기 전에 기존 head인 A0를 예상 출력으로 따로 저장합니다. 포인터나 물리 슬롯 대신 데이터의 논리적인 순서를 비교하는 과정입니다.

full 동시 요청에서 scoreboard의 처리 순서
  1. 이전 queue: [A0, A1, A2, A3]
  2. pop 예상값: 기존 head A0를 보관
  3. push 반영: [A1, A2, A3, B0]
  4. 응답 비교: valid=1일 때 dout=A0

경계 예제로 scoreboard를 검산하기

queue가 [A0,A1,A2,A3]인 상태에서 push만 들어오면 이미 크기가 4이므로 새 데이터 EE는 버립니다. 다음으로 두 요청이 동시에 들어오면 expected=A0을 먼저 보관하고 B0를 뒤에 붙여 [A1,A2,A3,B0]를 만듭니다. DUT가 새 데이터 B0를 즉시 내보내면 데이터 assertion이 실패합니다. 반대로 empty에서 두 요청이 동시에 들어오면 expect_pop=0, expect_push=1이므로 출력은 비교하지 않고 데이터 하나만 저장합니다. reset 중에는 두 요청을 무시하고 queue를 비우며 valid=0인지 검사합니다.

메모리 검증에도 같은 원칙을 적용합니다. 주소별 참조 배열과 초기화 여부를 기록하는 initialized 표를 둡니다. read-first에서는 참조 배열에 쓰기를 반영하기 전에 읽기 예상값을 구합니다. 레지스터레지스터 여러 비트의 상태를 저장하는 회로입니다. 이 과정의 동기식 레지스터는 클록 에지에서 정해진 입력을 저장합니다. 자세히 보기 파일의 bypass를 검증할 때는 충돌 시 wr_data를 선택한 뒤 참조 배열에 쓰기를 반영합니다. 초기화하지 않은 주소의 읽기를 금지할지, 값이 정해지지 않은 응답을 허용할지는 테스트의 전제에 명시해야 합니다. 임의로 0을 정답으로 삼아서는 안 됩니다. 주소로 접근하는 메모리에는 별도의 검증기가 필요합니다.

full에서 push와 pop이 동시에 들어올 때 새 값을 출력하는 오류, 동시 수락 때 개수를 늘리는 오류, empty에서 pop을 요청했을 때 valid를 켜는 오류를 각각 넣어 보면 해당 검사가 실패해야 합니다. 이런 방식으로 테스트를 점검하고 경계 조건별 coverage를 확인하면 빠진 검사를 찾을 수 있습니다. RAM 자원 배치와 타이밍은 합성과 타이밍 검증으로 따로 확인합니다.

직접 생각해 보기

잘못된 scoreboard가 push 요청을 먼저 queue에 넣고, 그 뒤 갱신된 크기로 pop 허용 여부를 판단합니다. empty에서 push와 pop이 동시에 들어오면 어떤 잘못된 판단을 하게 되나요? full에서 두 요청이 동시에 들어오면 예상 데이터를 언제 저장해야 하나요? reset 뒤에 매번 dout=0인지 검사하는 규칙도 타당한지 설명하세요.

해설 보기

empty에서 두 요청이 동시에 들어오면 새 항목을 즉시 pop할 수 있다고 잘못 판단해 expect_pop=1로 만듭니다. 명세에서는 에지 직전의 크기로 수락 여부를 정하므로 push만 수락하고 출력 valid는 0이어야 합니다. full에서는 queue의 기존 head를 먼저 expected에 저장하고 제거한 뒤, 새 데이터를 맨 뒤에 추가합니다. reset은 dout을 0으로 만들지 않으므로 valid=0인지, queue와 occupancy가 비었는지만 검사합니다. 데이터는 expect_pop=1일 때 비교하되 DUT의 valid도 따로 비교해야 합니다. 그래야 valid를 항상 0으로 내보내는 오류를 놓치지 않습니다.

선택은 이 브라우저에만 적용됩니다. 언제든 푸터에서 변경할 수 있습니다.