어떤 이벤트(신호 변화/클록 에지)나 시뮬레이션 시작 시점에 의해 실행이 트리거되고,
블록 내부 문장이 위에서 아래로 순서대로 수행되는 코드 영역
Verilog 에서는
initial ... endalways ... end블록이 Procedural block으로 동작한다.
이 블록 안에서는 if, case, for, repeat 같은 제어문을 사용해 조건/반복 로직을 기술하고,
=(blocking), <=(non-blocking) 같은 절차적 대입(procedural assignment)을 수행한다.
(Procecural block 내부에서 reg 에만 값을 대입할 수 있고, wire 연결은 불가하다)
값을 할당하는 방식
Verilog 에는 2가지 할당 방식 (Blocking, Non Blocking)이 있고, 둘의 동작 방식이 완전히 다르다.
순차적으로 실행되는 할당
코드가 위에서부터 아래로 순서대로 실행되고, 할당 즉시 값이 반영된다.
다음 문장의 실행을 막고(block) 현재 문장을 완료한 후 진행하기 때문에 Blocking 할당이라 부른다.
예를들어, 아래와 같이 동작한다.
always @(*) begin
a = 1; // 1) a에 1 할당 (즉시 반영)
b = a; // 2) b에 a(=1) 할당
c = b; // 3) c에 b(=1) 할당
end
// 결과: a=1, b=1, c=1
Non-blocking = 동시 실행, 지연 반영
모든 우변을 동시에 읽고, 블록 끝에서 한꺼번에 좌변에 Update 한다.
정확히 말하면 우변을 동시에 읽고, NBA Region 에서 값을 업데이트한다.
NBA Region 이란 같은 시뮬레이션 시간(time slot) 안에서 이벤트들을 처리할 때 쓰는 “실행 단계” 중 하나로,
Non-Blocking Assignment (<=)의 좌변 업데이트를 수행하는 단계이다.
다음 문장의 실행을 막지 않고(non-block) 동시에 진행하므로 Non-blocking 할당이라 부른다.
예를들어, 아래와 같이 동작한다.
always @(posedge clk) begin
a <= 1; // 1) 우변 평가: 1
b <= a; // 2) 우변 평가: a의 "현재" 값 (아직 이전 값!)
c <= b; // 3) 우변 평가: b의 "현재" 값 (아직 이전 값!)
end
// 블록 끝에서 동시에 할당
이때 헷갈리기 쉬운점이 Non-blocking을 쓰더라도 Procedural block 에서 위에서
아래로 코드가 실행된다는 점은 변하지 않는다.
정확히말하면 우변 평가후, Update가 위->아래 순서로 일어나게된다.
always @(posedge clk) begin
a <= b;
a <= c;
end
위의 코드를 실행하면 결국 a 에는 c가 저장된다.
Non-blocking 으로 a에 값을 대입해도 결국 더욱 뒤에 실행되는 a <= c; 에 의해
앞에서 실행 되는 a <= b;가 덮어씌워지게된다.
3단 시프트를 Blocing 할당, Non-blocking 할당으로 구현해보자
// Blocking (=)
always @(posedge clk) begin
a = d;
b = a;
c = b;
end
// Non-blocking (<=)
always @(posedge clk) begin
a <= d;
b <= a;
c <= b;
end
Blocking으로 구현시 a,b,c가 전부 d값이 되므로 3단 시프트로 볼 수없다.
반면, 아래의 코드는 Non blocking Assignment 덕분에 3단 시프트가 성공적으로 구현된다.
예를들어
// 조합 논리 - Blocking (=) ✅
always @(*) begin
sum = a + b;
carry = (a + b) > 255;
end
// 순차 논리 - Non-blocking (<=) ✅
always @(posedge clk) begin
q1 <= d;
q2 <= q1;
q3 <= q2;
end
위와 같이 조합이면 Blocking, 순차면 Non-Blocking 사용해야 올바르게 기능이 구현된다.
// ❌ 순차 논리에 Blocking
always @(posedge clk) begin
a = d; // ❌ 위험!
b = a; // ❌ 의도와 다르게 동작
end
// ❌ 조합 논리에 Non-blocking
always @(*) begin
sum <= a + b; // ❌ 시뮬레이션 문제 발생
end
// ❌ 같은 블록에서 혼용
always @(posedge clk) begin
a = d; // ❌ Blocking
b <= a; // ❌ Non-blocking 혼용!
end
그렇다면 굳이 복잡하게 왜 이렇게 나누어야할까??
이는 하드웨어적 관점에서 이해할 수 있다.
조합 논리는 Clock 없이 연속적 할당으로 구현되고, 이는 HW적 연결을 의미한다.
따라서 Blocking으로 각각의 신호선에 연속적으로 값을 할당하는 것이 실제 동작과 일치한다.
출력 <---------AND_Gate--OR_Gate---- 입력
를 생각해보면 된다.
순차 논리(예를들어 카운터)는 모든 FF가 동시에 클럭 엣지에서 동작한다.
따라서 Non-blocking 으로 한 번에 레지스터 값들을 업데이트 하는것이 실제 동작과 일치한다.
스펙상 이전 입력/이전 출력을 기억해야 동작하면 순차 논리이고, 그렇지 않으면 조합 논리이다.
조합 회로의 대표적인 예시로 ALU가 있다.
module alu (
input wire [7:0] a, b,
input wire [1:0] op,
output reg [7:0] result,
output reg zero
);
always @(*) begin
// Blocking 사용 ✅
case (op)
2'b00: result = a + b;
2'b01: result = a - b;
2'b10: result = a & b;
2'b11: result = a | b;
endcase
zero = (result == 8'b0); // result 값 사용 가능!
end
endmodule
순차 회로의 대표적인 예시로 파이프라인(Pipeline)이 존재한다.
module pipeline_3stage (
input wire clk,
input wire rst_n,
input wire [7:0] data_in,
output reg [7:0] data_out
);
reg [7:0] stage1, stage2;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stage1 <= 8'b0;
stage2 <= 8'b0;
data_out <= 8'b0;
end
else begin
// Non-blocking 사용 ✅
stage1 <= data_in; // 1단계
stage2 <= stage1; // 2단계
data_out <= stage2; // 3단계
end
end
endmodule
이렇게 각 stage의 레지스터가 동시에 이전 값을 샘플한다 -> 3단 파이프라인 정상 동작 ✅