본문으로 건너뛰기
code0.HDL

레지스터 단계·관찰 시점·파형

레지스터를 단계로 이어 붙이고, 테스트벤치가 에지에서·#1 뒤에·$strobe로·파형에서 각각 무엇을 보는지 익히고, $dumpfile과 $dumpvars로 VCD 기록을 시작하고 `timescale로 시간에 단위를 주고, 펄스가 정확히 한 사이클인 상승 에지 검출기를 만듭니다.

이 장에서 배울 내용

  • 레지스터 두 개를 단계로 이어 붙이고 값이 어느 에지 뒤에 어느 단계에 나타나는지 말할 수 있습니다
  • 테스트벤치의 출력 방식에서 각 줄이 에지 이전 값인지 이후 값인지 판단할 수 있습니다
  • $dumpfile과 $dumpvars로 VCD 기록을 시작하고, header와 변화 기록을 읽고, `timescale로 시간에 단위를 줄 수 있습니다
  • 파형이 바뀌는 시점으로 조합 경로와 레지스터를 구분하고, 한 사이클 폭의 상승 에지 펄스를 만들 수 있습니다
이 장에서 배우는 개념
  • 레지스터 단계·관찰 시점
먼저 알아야 할 개념

1. 레지스터 단계 하나는 에지 하나만큼 늦춥니다

한 클록 블록에서 <=로 쓴 레지스터 두 개는 두 단계입니다. 상승 에지마다 첫째는 입력을 받고 둘째는 그 에지 이전에 첫째가 가지고 있던 값을 받습니다. 두 우변이 어느 레지스터도 갱신되기 전에 읽히기 때문입니다. 어느 에지에서 s1에 샘플링된 값은 다음 에지 뒤에 s2에 있습니다.

always @(posedge clock) begin
  s1 <= in;
  s2 <= s1;    // 이 에지 이전의 s1
end

확인 문제

어느 상승 에지에서 in이 1로 샘플링되었습니다. 그 에지를 포함해 상승 에지 몇 번 뒤에 s2가 1을 보입니까?

2. 숫자를 비교하기 전에 어디를 보고 있는지 정합니다

클록 설계에는 에지 이전 값과 에지 이후 값이 있고, 테스트벤치는 둘 가운데 어느 쪽이든 볼 수 있습니다. 추적과 예측이 어긋나면 설계가 틀렸다고 판단하기 전에 어느 쪽 값을 출력했는지 확인하세요.

테스트벤치가 보는 방법보이는 값이유
always @(posedge clock) 안의 $display에지 이전 값$display는 active 영역에서 실행되고 <=은 같은 시각의 그 뒤에 갱신
같은 블록의 $strobe에지 이후 값$strobe는 모든 갱신이 끝난 시각의 끝에 출력
에지 뒤의 #1, 또는 @(negedge clock)에지 이후 값시간이 지나 에지의 갱신이 모두 끝남
파형모든 변화와 그 시각시뮬레이터가 선택한 신호의 변화를 바뀔 때마다 기록

규칙은 하나입니다. 숫자를 읽기 전에 이 표의 어느 행이 그 숫자를 만들었는지 정하세요. 처음 두 행 뒤에 있는 스케줄링 영역인 active, nonblocking update, postponed는 레지스터와 nonblocking 대입 장에서 다룹니다.

3. 파형은 변화의 기록입니다

파형은 시뮬레이션 동안 선택한 신호의 값이 언제 어떻게 바뀌었는지를 모두 기록한 것입니다. 테스트벤치 출력은 출력하기로 정한 값만 보여 주지만, 파형에는 한 사이클 늦은 펄스, 클록 에지 사이에서 움직이는 출력, x에서 벗어나지 못한 레지스터처럼 예상하지 못한 문제까지 그대로 남습니다. Verilog 시뮬레이터는 파형을 IEEE Std 1364-2005가 정의한 텍스트 형식인 VCD(value change dump) 파일로 기록합니다.

파형은 시뮬레이션 중에 기록됩니다. elaboration은 어떤 신호가 어떤 계층 이름으로 존재하는지 정하고, 시뮬레이터는 기록하기로 선택된 신호가 바뀔 때마다 파일에 한 줄을 남깁니다. 실습 환경은 실행 작업 디렉터리의 dump.vcd를 실행 결과와 함께 받아 출력 아래에 파형으로 그립니다.

3.1 header와 본문

VCD 파일은 신호를 선언하는 header와, 시간 순서대로 값 변화를 적은 본문으로 이루어집니다.

부분내용
header$timescale과 중첩된 $scope module ... $end 블록입니다. 각 scope 안의 $var 줄이 신호의 종류, 폭, ! 같은 짧은 식별자, 이름을 선언합니다
본문#25 같은 줄이 현재 시간을 정하고, 그 뒤의 줄은 그 시간에 값이 바뀐 신호만 기록합니다. 1비트 신호는 1!처럼, 벡터는 b11 "처럼 값 뒤에 식별자를 씁니다

다음 테스트벤치는 1비트 en과 2비트 n을 기록합니다.

module tb;
  reg       en = 0;
  reg [1:0] n  = 0;
  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
    #10 en = 1;
    #10 n = 2'd3;
    #5  $finish;
  end
endmodule

Icarus가 이 코드로 만드는 dump.vcd의 주요 부분은 다음과 같습니다.

$timescale
  1s
$end
$scope module tb $end
$var reg 1 ! en $end
$var reg 2 " n [1:0] $end
$upscope $end
$enddefinitions $end
#0
$dumpvars
b0 "
0!
$end
#10
1!
#20
b11 "
#25
줄의미
$var reg 1 ! en $end1비트 reg en을 식별자 !로 선언합니다
$var reg 2 " n [1:0] $end2비트 reg n을 식별자 "로 선언합니다
#0과 $dumpvars ... $end기록을 시작한 시점의 모든 신호 값입니다
#10, 1!시간 10에 en이 1이 되었습니다
#20, b11 "시간 20에 n이 11이 되었습니다
#25$finish가 실행된 마지막 시간입니다

3.2 변화만 기록합니다

본문은 표본을 일정 간격으로 찍은 표가 아니라 변화의 기록입니다. 값이 바뀌지 않은 신호는 한 줄도 남기지 않습니다. 뷰어는 각 신호의 마지막 값을 다음 변화까지 유지해 파형을 복원합니다.

참고. 실습 환경의 뷰어는 header 필드가 아니라 첫 값 변화의 시간에서 시간축을 시작합니다. 여러 scope에 나타나는 같은 식별자의 신호는 가장 바깥 scope에 한 번만 표시합니다.

4. 기록 시작하기

4.1 $dumpfile과 $dumpvars

형식

$dumpfile("파일_이름");
$dumpvars(깊이, scope, scope, ...);
initial begin
  $dumpfile("dump.vcd");
  $dumpvars(0, tb);
end
호출효과
$dumpfile("dump.vcd")출력 파일 이름을 정합니다. 호출이 없으면 Icarus는 dump.vcd를 씁니다
$dumpvars(0, tb)tb와 그 아래 모든 계층의 신호를 기록합니다. 기록은 이 호출이 실행된 시점부터 시작합니다

$dumpfile은 $dumpvars보다 먼저 호출합니다. 두 호출은 보통 테스트벤치 initial 블록의 첫머리, 곧 시간 0에 둡니다. 깊이 0은 tb와 그 아래 모든 계층입니다. 다른 깊이와 계층 깊숙한 scope를 기록하는 방법은 파형 기록 장에서 다룹니다.

4.2 예제: 카운터 기록하기

Counter.v는 앞 두 장의 동기 리셋 4비트 카운터입니다.

Counter.v
// active-high 동기 리셋을 가진 4비트 카운터.
module counter (
  input            clock,
  input            reset,
  output reg [3:0] count
);
  always @(posedge clock) begin
    if (reset) count <= 4'd0;
    else       count <= count + 4'd1;
  end
endmodule

Tb.v는 initial 블록 첫머리에서 $dumpfile로 파일 이름을 정하고 $dumpvars(0, tb)로 tb와 그 아래 전체를 기록합니다. 이후 상승 에지 두 번 동안 reset을 유지하고, 시간 20의 하강 에지에서 reset을 내린 뒤, 상승 에지 여섯 번을 더 기다리고 여덟 번째 상승 에지에서 한 시간 단위 뒤에 DONE 줄을 출력합니다.

Tb.v
module tb;
  reg        clock = 0;
  reg        reset = 1;
  wire [3:0] count;

  counter dut (.clock(clock), .reset(reset), .count(count));

  always #5 clock = ~clock;

  initial begin
    // 파일 이름을 먼저 정한 뒤 기록 범위를 고른다. 깊이 0은 모든 계층이다.
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
    repeat (2) @(posedge clock);
    @(negedge clock) reset = 0;
    repeat (6) @(posedge clock);
    #1 $display("DONE t=%0t count=%0d", $time, count);
    $finish;
  end
endmodule

실행하기 전에 예측해 보세요.

  1. 파일에는 어떤 scope가 있으며, 각 scope에는 어떤 신호가 있을까요?
  2. 시간 0부터 첫 상승 에지까지 count는 어떤 값으로 그려질까요?
  3. count가 바뀌는 시간과 reset이 내려가는 시간은 언제일까요?

확인 문제

시간 0부터 첫 상승 에지인 5까지 파형에서 count는 무엇으로 그려집니까?

답

결과 확인. 실행을 누르세요. 출력은 파일이 열렸음을 알리는 Icarus의 VCD info: 줄로 시작하고, 시간 76에서 count=6을 보이는 DONE 줄이 이어집니다. 그 아래 파형을 테스트벤치와 비교해 읽으세요.

시간파형이유
0count = x, reset = 1, clock = 0기록 시작 시점의 값입니다. 레지스터에는 아직 아무도 대입하지 않았습니다
5clock 상승, count = 0reset이 1인 첫 상승 에지입니다
20reset 하강. 클록 하강 에지와 같은 시간테스트벤치가 하강 에지에서 입력을 바꿉니다
25, 35, …, 75count가 상승 에지에서만 1, 2, …, 6으로 바뀝니다동기 리셋 레지스터는 자기 클록 에지에서만 바뀝니다

DONE 줄은 #1 뒤에 출력되므로 같은 레지스터의 에지 이후 값, 곧 2절 표의 셋째 행이고, 그래서 파형의 마지막 계단과 일치합니다.

header의 scope는 tb와 tb.dut 두 개입니다. 테스트벤치의 net count와 카운터의 레지스터 count는 항상 같은 값을 가지지만 계층 안에서 서로 다른 객체이므로 식별자가 다른 두 신호로 기록됩니다. 반면 tb.dut의 clock 포트는 tb의 clock과 같은 net이므로 같은 식별자를 씁니다. 파일의 $timescale은 1s입니다. 소스가 시간 단위를 정하지 않았으므로 시뮬레이션 시간 단위 하나가 1초로 기록됩니다.

직접 바꿔 보세요.

바꿀 곳예상 결과
$dumpfile 줄 삭제Icarus의 기본 파일 이름이 dump.vcd이므로 파형은 그대로입니다
$dumpvars(0, tb);를 repeat (2) @(posedge clock); 아래로 이동기록이 시간 15에 시작합니다. 초기 값 기록에서 count는 이미 0이고 시간축도 15에서 시작합니다. 첫 에지와 그 전의 x는 파일에 없습니다

주의. 다른 시뮬레이터는 기본 파일 이름이 다를 수 있으므로 $dumpfile 호출을 유지하세요. 파형은 기록을 시작한 이후의 사건만 담습니다.

5. `timescale

5.1 시간 단위와 정밀도

# 지연의 숫자는 시간 단위의 개수입니다. `timescale 컴파일러 지시어가 그 단위의 물리적 크기와, 지연을 반올림하는 가장 작은 간격인 정밀도를 정합니다.

형식

`timescale 단위 / 정밀도
`timescale 1ns / 1ps

module tb;
  initial begin
    #5    $display("%0.1f", $realtime);   // 5.0
    #2.25 $display("%0.2f", $realtime);   // 7.25
  end
endmodule
부분의미
`컴파일러 지시어를 시작합니다. 문장이 아니므로 끝에 ;를 쓰지 않습니다
1ns시간 단위입니다. #5는 5 ns입니다
1ps정밀도입니다. 지연은 1 ps 단위로 반올림됩니다
$realtime현재 시간을 시간 단위 기준의 실수로 반환합니다

지시어는 같은 컴파일에서 뒤따르는 모든 모듈에 적용됩니다. 설계 파일과 테스트벤치 파일 모두에 같은 지시어를 두세요.

항목`timescale 1ns / 1ps 있음`timescale 없음
#55 ns5 단위. Icarus 기본값은 1 s입니다
#10.5정확히 10.5 ns11 단위로 반올림됩니다
VCD $timescale1ps. 모든 시간을 ps로 기록합니다1s
$realtimens 단위의 실수단위 개수

VCD는 정밀도 단위로 시간을 기록하므로 #5는 파일에 #5000으로 나타납니다. 뷰어가 다시 ns로 변환해 보여 줍니다.

5.2 예제: 조합 경로와 레지스터 경로

AndPaths.v는 a & b를 두 번 계산합니다. y_comb는 assign으로, y_reg는 상승 에지에서 갱신하는 레지스터로 만듭니다. 두 파일 모두 같은 `timescale 1ns / 1ps로 시작하므로 클록 주기는 10 ns이고 테스트벤치는 입력을 0.5 ns 단위로 움직일 수 있습니다.

AndPaths.v
`timescale 1ns / 1ps

// 같은 AND 함수를 wire와 레지스터로 각각 구현한다.
module and_paths (
  input      clock,
  input      a,
  input      b,
  output     y_comb,
  output reg y_reg
);
  assign y_comb = a & b;
  always @(posedge clock) y_reg <= a & b;
endmodule

Tb.v는 주기 10 ns의 클록을 만들고 다음 순서로 입력을 바꾼 뒤, $realtime으로 DONE 줄을 출력합니다. 상승 에지는 5, 15, 25, 35, 45, 55 ns에 있습니다.

시간(ns)입력 변화
12a = 1
18b = 1
28.5b = 0
29.5b = 1. 1 ns 동안의 짧은 하강입니다
49.5a = 0
Tb.v
`timescale 1ns / 1ps

module tb;
  reg  clock = 0;
  reg  a = 0, b = 0;
  wire y_comb, y_reg;

  and_paths dut (.clock(clock), .a(a), .b(b), .y_comb(y_comb), .y_reg(y_reg));

  // 이제 시간 단위 하나가 1 ns이므로 주기는 10 ns다.
  always #5 clock = ~clock;

  initial begin
    $dumpfile("dump.vcd");
    $dumpvars(0, tb);
    #12   a = 1;
    #6    b = 1;
    // 두 상승 에지 사이에 있는, 클록 주기보다 짧은 펄스.
    #10.5 b = 0;
    #1    b = 1;
    #20   a = 0;
    #10;
    $display("DONE t=%0.1f ns y_comb=%b y_reg=%b", $realtime, y_comb, y_reg);
    $finish;
  end
endmodule

이 개념 실습하기

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

실행하기 전에 예측해 보세요. 두 출력 각각에 대해 답해 보세요.

  1. 처음 1이 되는 시간은 언제일까요?
  2. 28.5~29.5 ns의 짧은 하강이 파형에 나타날까요?
  3. 다시 0이 되는 시간은 언제일까요?

확인 문제

b는 28.5부터 29.5 ns까지 0입니다. y_reg에 그 하강이 나타납니까?

답

확인 문제

y_reg가 처음 1이 되는 시각은 몇 ns입니까?

결과 확인. 실행을 누르고 두 파형을 나란히 읽으세요. DONE 줄은 59.5 ns와 두 출력의 값 0을 보입니다. 파형은 다음 표와 같습니다.

시간(ns)사건y_comby_reg
5상승 에지00. 초기 x를 대체합니다
18a가 이미 1인 상태에서 b 상승즉시 1변화 없음
25상승 에지1
28.5~29.5b가 1 ns 동안 00이 되었다가 다시 1변화 없음. 그 구간에 에지가 없습니다
49.5a 하강즉시 0변화 없음
55상승 에지0

y_comb는 1 ns짜리 하강까지 포함해 입력을 즉시 따릅니다. 실제 하드웨어라면 이런 짧은 하강을 glitch라고 부릅니다. y_reg는 상승 에지에서만 바뀌므로 하강을 보지 못하고, 조합 값보다 최대 한 주기 늦게 따라갑니다. 파일에서는 모든 시간이 ps로 기록되므로 하강 구간이 #28500과 #29500 두 줄로 나타납니다.

신호가 바뀌는 시점은 그 신호를 구동하는 논리의 종류를 짐작하게 합니다. 에지 사이에서 바뀌면 에지 사이에 움직인 입력에서 오는 조합 경로입니다. 에지에서만 바뀌는 것은 레지스터의 특징이지만, 레지스터 출력만 입력으로 받는 조합 논리도 에지에서만 바뀌고, 비동기 리셋이나 negedge 레지스터는 상승 에지 사이에서도 바뀝니다. 짐작이지 증명은 아닙니다.

직접 바꿔 보세요. 두 파일에서 `timescale 줄을 지우고, 결과를 먼저 예측해 보세요. 시간 단위는 Icarus 기본값으로, 정밀도는 한 단위로 바뀝니다.

항목예상 결과
#10.511로 반올림되어 b는 29에 내려가고 30에 올라갑니다
DONE 줄60.0을 보입니다
VCD $timescale1s입니다
y_comb의 하강여전히 나타나지만 길이가 정확히 한 단위입니다

주의. `timescale을 명시하지 않은 이 예제는 Icarus의 기본 시간 단위와 정밀도를 사용하며, VCD에는 1s로 기록됩니다. 따라서 소수 지연이 반올림되어 예상과 다른 시각이 될 수 있습니다.

6. 상승 에지 검출기는 두 표본을 비교합니다

상승은 현재 표본과 이전 표본 사이의 관계입니다. 이전 표본을 레지스터에 두면 펄스는 "지금은 1, 전에는 0"입니다.

always @(posedge clock) begin
  prev  <= level;
  pulse <= level & ~prev;   // 여기서 prev는 아직 이전 표본
end

두 대입이 모두 nonblocking이므로 첫 줄이 prev에 대입해도 둘째 줄은 에지 이전의 prev를 읽습니다. level이 1이고 prev가 0인 에지에서 pulse는 그 에지 뒤에 1이 되어 다음 에지까지 1을 유지합니다. 정확히 한 주기입니다. 아래 모델은 같은 prev를 가집니다. 모델의 rise는 사이클 동안의 조합 값 in & !prev이고, 레지스터 pulse는 그 값을 사이클을 끝내는 에지에서 붙잡은 것이므로 한 에지 늦게 보입니다. 에지 뒤의 prev와 사이클 동안의 rise를 채우세요.

예측한 뒤 비교 · rise-detect

비교하기 전에 모든 칸을 채우세요. 빈칸도 불일치로 처리합니다.

상태표
사이클in에지에서 샘플링한 입력prev에지 직후rise출력
00
11
21
31
40
51

7. 실습: 상승마다 펄스 하나

점검 과제는 실습 문제 rise-pulse입니다. 시작 코드의 pulse는 level을 그대로 복사하므로 입력이 세 사이클 동안 1이면 출력도 세 사이클 내내 1입니다. 한 사이클짜리 펄스가 아닙니다. 테스트벤치는 하강 에지에서 level을 바꾸고 다음 하강 에지에서 출력하므로 각 줄은 에지 이후 값입니다. 먼저 기대하는 pulse 열을 적고, TODO 아래 한 줄을 바꾼 뒤, 글자만이 아니라 파형을 확인하세요. 모든 펄스는 상승 에지에서 다음 상승 에지까지 이어져야 하고, level이 에지 사이에 움직여도 pulse는 에지 사이에 움직이지 않아야 합니다.

8. 정리

요소뜻
s1 <= in; s2 <= s1;두 단계. 단계마다 에지 하나의 지연
에지에서의 $display에지 이전 값. $strobe, #1, negedge는 에지 이후 값
VCD 구조header가 scope와 신호를 선언하고, 본문이 시간 순서의 값 변화를 기록
변화 기록값이 바뀐 신호만 기록. 본문은 기록 시작 시점의 모든 값으로 시작
$dumpfile, $dumpvars(0, tb)파일 이름을 정한 뒤 그 시점부터 tb와 그 아래 전체를 기록
`timescale# 지연의 단위와 반올림 정밀도를 정함. VCD는 정밀도 단위로 시간을 기록
에지 사이의 변화움직인 입력에서 오는 조합 경로. 에지에서만 바뀌면 레지스터일 가능성이 크지만 증명은 아님
pulse <= level & ~prev두 표본으로 검출한 상승. 폭은 한 주기

scope와 깊이를 고르는 방법, 그리고 파형을 비우거나 늦게 시작하게 만드는 실수는 파형 기록 장에서 다룹니다.

9. 확인 문제

  1. 카운터 VCD에 count가 두 번 나오는 이유와, 두 파형이 항상 같은 이유는 무엇일까요?

    정답 보기

    tb의 net count와 tb.dut의 레지스터 count는 계층 안에서 서로 다른 객체이므로 각각 기록됩니다. 테스트벤치의 net이 카운터의 출력 포트에 연결되어 있어 항상 레지스터와 같은 값을 보입니다.

  2. 카운터 파형에서 시간 5 전에 count가 x인 이유는 무엇이며, 시간 15에 기록을 시작하면 파일은 어떻게 달라질까요?

    정답 보기

    첫 상승 에지 전에는 아무도 레지스터에 대입하지 않았으므로 x입니다. 시간 15에 기록을 시작하면 초기 값 기록이 시간 15에 만들어지고, 그때 count는 이미 0이므로 x 구간과 첫 에지가 파일에 없습니다.

  3. b의 1 ns 하강이 y_comb에는 나타나고 y_reg에는 나타나지 않는 이유는 무엇일까요?

    정답 보기

    y_comb는 assign이므로 입력이 바뀌는 즉시 다시 계산됩니다. y_reg는 상승 에지에서만 값을 읽는데, 28.5~29.5 ns 구간에는 상승 에지가 없으므로 하강을 보지 못합니다.

  4. `timescale 1ns / 1ps에서 #2.5 지연은 VCD 본문에 몇으로 기록될까요?

    정답 보기

    VCD는 정밀도 단위인 ps로 시간을 기록하므로 2.5 ns는 2500으로 기록됩니다. 시간 줄에서는 직전 시간에 2500을 더한 값으로 나타납니다.

확인 지점

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

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

참고 자료

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