1. always @(posedge clock)이 레지스터입니다
조합 블록은 입력이 바뀔 때마다 실행됩니다. 레지스터는 클록 에지에서만 바뀌고, Verilog는 그
사실을 감지 목록에 적습니다. always @(posedge clock)은 상승 에지마다 본문을 한 번 실행하고
다른 시점에는 실행하지 않습니다. 본문이 대입한 값은 다음 에지까지 저장됩니다. 합성 도구가
플립플롭으로 바꾸는 표준 형식이 이것이며, 이 과정은 클록 하나와 상승 에지 동기 설계만
가정합니다.
형식
always @(posedge clock) begin
count <= count + 4'd1; // 현재 값을 읽고 다음 값을 저장
end우변은 현재 값, 공통 기초의 Q를 읽습니다. 대입은 다음 값 D를 저장하고, 그 값은 에지 뒤에만
보입니다. count는 reg로 선언해야 합니다. 여기서는 그 키워드가 드디어 저장과 짝을 이루지만,
레지스터를 만드는 것은 키워드가 아니라 감지 목록의 에지입니다.
참고. #5 같은 지연, $display 같은 시스템 태스크, 시험 순서를 적은 initial 블록은
시뮬레이션을 위한 구문입니다. 합성 도구는 이런 구문을 무시하거나 거부하며, 어디까지
받아들이는지는 도구마다 다릅니다. @(posedge clock)은 다릅니다. always 블록의 감지 목록으로
쓴 @(posedge clock)은 합성 가능한 레지스터를 기술하는 표준 형식이며, 아래 Counter.v가 바로
그렇게 씁니다. 테스트벤치 안에서 순서 중간에 기다리는 용도로 쓸 때만 시뮬레이션 장치입니다.
DUT는 레지스터와 조합 논리의 관용 형식으로 작성하고, 테스트벤치는 시뮬레이션 구문을 자유롭게
사용합니다. 이 장의 모든 예제는 Icarus 11로 시뮬레이션만 하며 합성은 하지 않습니다.
2. nonblocking <=은 지금 읽고 에지의 다른 일이 끝난 뒤 갱신합니다
nonblocking 대입 <=은 우변을 즉시 계산하고, 대상의 갱신은 같은 시뮬레이션 시각의 뒤쪽
단계인 nonblocking update 영역으로 미룹니다. 한 에지가 일으킨 모든 <=은 설계의 어느 블록에
있든 에지 이전 값을 읽고 그 뒤에 함께 갱신됩니다. 공통 기초에서 본 한 사이클 규칙이 바로
이것입니다. 에지 뒤의 사이클에서 레지스터는 에지 이전 값으로 계산한 결과를 보입니다.
| 대입 | 형식 | 우변 | 대상이 바뀌는 시점 | 같은 블록의 다음 문장이 보는 값 |
|---|---|---|---|---|
| blocking | 대상 = 식; | 지금 계산 | 즉시 | 새 값 |
| nonblocking | 대상 <= 식; | 지금 계산 | 이 시각의 active 문장이 모두 끝난 뒤, nonblocking update 영역 | 이전 값 |
reg [3:0] x;
initial begin
x = 1;
x <= 2;
$display("x=%0d", x); // 1. <=의 갱신이 아직 일어나지 않았습니다
#1 $display("x=%0d", x); // 2
end클록 논리는 "모든 레지스터가 에지 이전 값으로 다음 값을 정한다"는 뜻이며, 이 의미를 그대로
나타내는 대입이 <=입니다. 같은 사이클에 레지스터를 읽으면 이전 값이 나오는 것은, 그것이
<=로 쓴 에지 기반 레지스터이기 때문입니다. 모든 신호에 적용되는 법칙은 아닙니다. wire나
=로 대입한 변수는 새 값을 바로 보입니다.
3. 한 시간 단계 안의 실행 순서
3.1 스케줄링 영역
시뮬레이터는 시간을 단계별로 진행하며, 한 시간 단계 안의 작업을 정해진 영역 순서로 처리합니다. 테스트벤치를 쓸 때 알아야 할 영역은 세 개입니다.
| 영역 | 실행 내용 |
|---|---|
| active | 실행 가능한 절차 문장, blocking = 대입, 연속 대입 갱신, $display |
| nonblocking update | active 영역에서 실행된 모든 <=의 갱신 |
| postponed | $strobe, $monitor 출력. 그 시간 단계의 다른 모든 갱신이 끝난 뒤입니다 |
같은 상승 에지에서 깨어나는 절차 블록 두 개는 둘 다 active 영역에서 실행되며, 두 블록의 실행 순서는 표준이 정하지 않습니다. 테스트벤치의 경쟁 조건은 대부분 이 미정 순서에서 생깁니다. 다음 규칙을 지키면 실행 순서가 결과에 영향을 주지 않습니다.
| 위치 | 규칙 |
|---|---|
| 클록 논리 | 레지스터는 <=로 대입합니다. 모든 블록이 에지 이전 값을 읽게 됩니다 |
| 테스트벤치 입력 | 하강 에지처럼 샘플링 에지에서 떨어진 시점에 바꿉니다 |
| 테스트벤치 출력 읽기 | 에지에서 읽으면 이전 값을 본다는 점을 전제로 하거나, 에지의 갱신이 끝난 뒤 읽습니다 |
3.2 $display, $strobe, $monitor
세 시스템 태스크는 같은 형식 문자열을 받지만 출력하는 시점이 다릅니다.
형식
$display("형식", 값, ...);
$strobe("형식", 값, ...);
$monitor("형식", 값, ...);| 태스크 | 출력 시점 | 출력 횟수 |
|---|---|---|
$display | 문장이 실행되는 순간. active 영역입니다 | 실행할 때마다 한 번 |
$strobe | 그 시간 단계가 끝날 때. postponed 영역입니다 | 실행할 때마다 한 번 |
$monitor | 인자 가운데 하나라도 바뀐 시간 단계가 끝날 때 | 한 번 호출하면 이후 계속 |
reg [3:0] a = 0, b = 0;
initial $monitor("t=%0t a=%0d b=%0d", $time, a, b);
initial begin
#10 a = 1;
#10 b = 2;
#5 a = 3; b = 4; // 같은 시간 단계의 변화 두 개
end이 코드에서 $monitor는 시간 0, 10, 20, 25에 한 줄씩 출력합니다. 시간 25에는 a와 b가
함께 바뀌었지만 그 시간 단계가 끝난 뒤 한 번만 출력합니다.
4. 카운터 추적을 에지에서, 그리고 에지 뒤에 읽습니다
Counter.v는 동기 리셋이 있는 4비트 카운터입니다. 상승 에지마다 reset이 1이면 0을, 아니면
현재 값에 1을 더한 값을 저장합니다. 리셋은 에지에서만 적용되는 동기 리셋이며, 리셋과
enable의 규칙은 리셋·enable·우선순위 장에서
정리합니다.
// 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테스트벤치는 always #5 clock = ~clock;으로 클록을 만들므로 상승 에지는 5, 15, 25, …에
옵니다. reset은 1로 시작해 시간 20의 하강 에지에서 내려갑니다. 또 다른 블록이 상승 에지마다
두 줄을 출력합니다. EDGE 줄은 즉시 실행되는 $display, AFTER 줄은 같은 시각의 맨 끝에
출력하는 $strobe입니다.
| 구성 | 동작 |
|---|---|
clock | 0에서 시작해 5마다 반전합니다 |
reset | 1에서 시작합니다 |
always @(posedge clock) | 상승 에지마다 $display로 EDGE 줄을, $strobe로 AFTER 줄을 출력합니다 |
initial | 상승 에지 두 번을 기다리고, 다음 하강 에지에서 reset을 내린 뒤, 상승 에지 네 번을 더 기다리고 1 뒤에 종료합니다 |
module tb;
reg clock = 0;
reg reset = 1;
wire [3:0] count;
counter dut (.clock(clock), .reset(reset), .count(count));
// 주기 10. 상승 에지는 5, 15, 25, ...
always #5 clock = ~clock;
// $display는 에지 시점에, $strobe는 에지의 갱신이 끝난 뒤 출력한다.
always @(posedge clock) begin
$display("EDGE t=%0t reset=%b count=%0d", $time, reset, count);
$strobe("AFTER t=%0t count=%0d", $time, count);
end
initial begin
repeat (2) @(posedge clock);
// 입력은 샘플링과 반 주기 떨어진 하강 에지에서 바꾼다.
@(negedge clock) reset = 0;
repeat (4) @(posedge clock);
#1 $finish;
end
endmodule실행하기 전에 예측해 보세요.
- 시간 5의 첫
EDGE줄에서count는 무엇일까요? reset이 처음 0인 시간 25에서EDGE줄과AFTER줄은 각각 0일까요, 1일까요?$finish까지 카운터는 몇 번 증가할까요?
확인 문제
첫 상승 에지인 시간 5의 EDGE 줄은 count를 무엇으로 보입니까?
확인 문제
시간 25에 reset이 처음으로 0입니다. EDGE 줄과 AFTER 줄은 각각 무엇을 보입니까?
결과 확인. 실행을 누르고 줄 쌍을 하나씩 다음 표와 비교하세요.
| 시간 | 사건 | EDGE | AFTER |
|---|---|---|---|
| 5 | 첫 상승 에지, reset = 1 | x. 아직 아무도 count에 대입하지 않았습니다 | 0. 리셋이 적용되었습니다 |
| 15 | 두 번째 상승 에지, reset = 1 | 0 | 0 |
| 20 | 하강 에지. 테스트벤치가 reset을 내립니다 | ||
| 25 | 상승 에지, reset = 0 | 0 | 1 |
| 35, 45, 55 | 상승 에지 | 1, 2, 3 | 2, 3, 4 |
EDGE 줄이 에지 이전 값을 보이는 이유는 하나로 정확히 말할 수 있습니다. $display는 그
시각의 active 영역에서 실행되고, 카운터의 <=은 그 뒤에 오는 nonblocking update 영역에서
갱신됩니다. $strobe는 postponed 영역까지 기다리므로 AFTER 줄은 에지가 만든 값을 보입니다.
#1 뒤에 실행되는 $display라면 새 값을 보입니다. 첫 줄의 x는 리셋이 필요한 이유를 보여
줍니다. 리셋이 1인 첫 에지 전까지 레지스터의 값은 x입니다.
직접 바꿔 보세요. Counter.v의 <= 두 개를 =로 바꿔 보세요. 카운터는 같은 순서로
세지만 갱신이 active 영역에서 일어나므로 테스트벤치의 $display와 경쟁합니다. 이 실습
환경의 Icarus에서는 시간 35와 55의 EDGE 줄이 새 값 2와 4를, 시간 45의 EDGE 줄은 이전
값 2를 출력합니다. 소스 어디에도 이 순서를 정하는 부분은 없습니다. AFTER 줄은
$strobe가 시간 단계의 끝을 기다리므로 그대로 올바릅니다.
주의. 에지에서 값을 읽는 테스트벤치는 설계가 <=를 쓸 때만 믿을 수 있습니다.
5. 교환하는 레지스터 두 개를 두 번 씁니다
Swap.v에는 한 글자만 다른 모듈 두 개가 있습니다. 두 모듈 모두 load가 1인 동안 x = 1,
y = 2를 적재하고, 이후 상승 에지마다 x와 y를 교환합니다. 하나는 <=로, 하나는 =로
씁니다. 두 else 분기를 나란히 비교해 보세요.
// 상승 에지마다 값을 교환하는 레지스터 두 개.
module swap_nonblocking (
input clock,
input load,
output reg [3:0] x,
output reg [3:0] y
);
// <=는 이전 값 두 개를 먼저 읽은 뒤 두 레지스터를 갱신한다.
always @(posedge clock) begin
if (load) begin x <= 4'd1; y <= 4'd2; end
else begin x <= y; y <= x; end
end
endmodule
// <= 자리에 =를 쓴 같은 코드.
module swap_blocking (
input clock,
input load,
output reg [3:0] x,
output reg [3:0] y
);
// =는 x를 즉시 갱신하므로 다음 줄은 새 x를 읽는다.
always @(posedge clock) begin
if (load) begin x = 4'd1; y = 4'd2; end
else begin x = y; y = x; end
end
endmoduleTb.v는 두 모듈을 모두 인스턴스로 만들고, 첫 에지 동안 load를 1로 유지하며, 하강 에지마다
두 쌍을 출력합니다. 레지스터가 바뀐 지 반 주기 뒤에 읽으므로 3절의 경쟁 조건이 생기지 않고,
각 줄은 에지 뒤의 값을 보입니다.
module tb;
reg clock = 0;
reg load = 1;
wire [3:0] nx, ny, bx, by;
integer cycle = 0;
swap_nonblocking good (.clock(clock), .load(load), .x(nx), .y(ny));
swap_blocking bad (.clock(clock), .load(load), .x(bx), .y(by));
always #5 clock = ~clock;
// 모든 레지스터가 안정된 하강 에지에서 샘플링한다.
always @(negedge clock) begin
$display("CYCLE %0d load=%b nonblocking x=%0d y=%0d blocking x=%0d y=%0d",
cycle, load, nx, ny, bx, by);
cycle = cycle + 1;
load = 0;
if (cycle == 4) $finish;
end
endmodule실행하기 전에 예측해 보세요. 첫 교환 에지 뒤에 각 모듈의 x와 y는 무엇일까요? 두 번째
교환 뒤에는 어떻게 될까요?
확인 문제
첫 교환 에지 뒤에 두 모듈은 각각 무엇을 담고 있습니까?
결과 확인. 실행을 누르세요.
| 출력 줄 | nonblocking | blocking |
|---|---|---|
CYCLE 0 | x=1 y=2. 적재 직후입니다 | x=1 y=2 |
CYCLE 1 | x=2 y=1. 두 우변을 레지스터가 바뀌기 전에 읽었습니다 | x=2 y=2. x = y로 x가 먼저 2가 되고, y = x가 새 x를 읽었습니다 |
CYCLE 2, CYCLE 3 | 1,2와 2,1을 번갈아 출력합니다 | 2,2에서 바뀌지 않습니다 |
blocking 모듈의 결과는 시뮬레이터의 특이 동작이 아닙니다. y를 두 레지스터에 복사하는 다른
하드웨어를 기술한 것이고, 합성 도구도 그 회로를 만듭니다. <=에서는 두 문장의 순서가 결과에
영향을 주지 않고, =에서는 순서가 설계 자체입니다.
6. 하나만 바꿔 보세요
blocking 모듈에서 두 문장의 순서를 y = x; x = y;로 뒤집고, 실행 전에 예측하세요.
확인 문제
blocking 분기를 y = x; x = y;로 쓰면 x와 y는 어떤 값에 머뭅니까?
y가 이전 x인 1을 받고 x가 새 y를 복사하므로 두 레지스터는 1,1에 고정됩니다. =를
쓰면 문장 순서가 회로를 바꾸고, <=를 쓰면 바꾸지 않습니다. 그래서 클록 always 블록에는
<=를 쓰고, =는 조합 논리를 기술하는 always @* 블록에 씁니다.
7. 이어 붙인 레지스터는 하나마다 한 에지씩 늦습니다
한 클록 블록에서 <=로 쓴 레지스터 두 개 s1 <= in; s2 <= s1;은 2단 지연선입니다. 에지마다
s2는 에지 이전의 s1 값을 받으므로, 한 에지에서 샘플링된 in의 값은 그 다음 에지 뒤에
s2에 도달합니다. s1보다 한 에지 늦습니다. 표를 채워 보세요. 비교 버튼은 모델과 처음
어긋나는 사이클을 알려 줍니다.
예측한 뒤 비교 · delay2
비교하기 전에 모든 칸을 채우세요. 빈칸도 불일치로 처리합니다.
| 사이클 | in에지에서 샘플링한 입력 | s1에지 직후 | s2에지 직후 |
|---|---|---|---|
| 0 | 1 | ||
| 1 | 0 | ||
| 2 | 1 | ||
| 3 | 1 | ||
| 4 | 0 | ||
| 5 | 0 |
두 단을 모두 blocking 대입 s1 = in; s2 = s1;로 쓰면 s2가 새 s1을 읽으므로, 두 단은
모든 줄에서 같은 값을 출력해 레지스터 하나처럼 동작합니다. 둘째 대입만 =로 바꾸면 첫째
대입의 <= 갱신이 아직 일어나지 않았으므로 여전히 이전 s1을 읽습니다.
이런 단계를 파형에서 관찰하는 방법은
레지스터 단계·관찰 시점·파형 장에 있고,
파형 기록 장의 실습 문제 delay-line이 바로 이
결함을 고치는 과제입니다.
8. 자주 하는 실수
8.1 샘플링 에지에서 입력 바꾸기
// 잘못된 코드: DUT가 입력을 읽는 에지에서 입력을 바꿉니다
@(posedge clock) add = 1;
// 바른 코드: 반 주기 떨어진 하강 에지에서 바꿉니다
@(negedge clock) add = 1;상승 에지에서 테스트벤치와 DUT가 모두 깨어나면 어느 쪽이 먼저 실행될지 정해져 있지
않습니다. DUT가 새 입력을 볼지 이전 입력을 볼지는 시뮬레이터의 실행 순서에 따라 달라집니다.
입력은 하강 에지에서 바꾸세요. 카운터 추적의 Tb.v가 reset을 그렇게 바꿉니다.
9. 정리
| 요소 | 뜻 |
|---|---|
always @(posedge clock) | 상승 에지마다 한 번 실행되는 블록. 레지스터이며 합성 도구가 받아들이는 형식 |
q <= 식; | 지금 읽고 같은 시각의 nonblocking update 영역에서 갱신 |
| active, nonblocking update, postponed | 한 시간 단계 안의 실행 순서. 같은 에지에 깨어난 두 블록은 active 영역에서 정해지지 않은 순서로 실행 |
에지에서의 $display | 에지 이전 값 |
$strobe, $monitor, #1, 하강 에지 | 에지가 만든 값. $monitor는 인자가 바뀔 때마다 계속 출력 |
클록 블록의 = | 다음 문장이 새 값을 읽음. 다른 회로 |
클록 논리는 <=, 조합 always @*는 = | 에지에서 읽는 테스트벤치를 믿을 수 있게 하는 규칙 |
10. 확인 문제
-
카운터 추적에서 시간 25의
EDGE줄은 0,AFTER줄은 1인 이유는 무엇일까요?정답 보기
EDGE줄의$display는 active 영역에서 실행되므로 카운터의<=갱신 전 값인 0을 읽습니다.AFTER줄의$strobe는 postponed 영역에서 출력하므로 갱신이 끝난 값 1을 읽습니다. -
테스트벤치가
reset을 한 번도 1로 만들지 않으면 카운터 추적은 어떻게 될까요?정답 보기
count는 처음부터x이고,x + 1도x이므로 모든 줄이x를 출력합니다. 레지스터는 리셋이나 대입이 있어야 알려진 값을 가집니다. -
swap_blocking에서는 두 문장의 순서가 결과를 바꾸는데swap_nonblocking에서는 바꾸지 않는 이유는 무엇일까요?정답 보기
=는 대상을 즉시 바꾸므로 뒤 문장이 앞 문장의 결과를 읽습니다.<=는 두 우변을 모두 에지 이전 값으로 계산한 뒤 한꺼번에 갱신하므로, 어느 문장을 먼저 써도 같은 값을 읽습니다.
이 장에는 실습 문제가 없고, 예제 두 개를 실행하는 것이 점검 과제입니다. 테스트벤치가 스스로
결과를 검사하게 하는 태스크, !== 비교, $fatal, watchdog은
테스트벤치 기법 장에 있습니다.