1.1 initial과 always: 시뮬레이션과 합성의 경계
Verilog를 사용한 디지털 설계에서 initial과 always는 표면적으로는 유사해 보이지만, 실제로는 목적과 동작 방식에서 근본적인 차이를 가진다. 이 두 키워드의 선택은 설계가 시뮬레이션 단계에만 머무를 것인지, 실제 FPGA나 ASIC으로 구현될 수 있는지 여부를 결정한다.
initial 블록은 오직 시뮬레이션 환경에서만 의미가 있으며, 주로 테스트벤치(testbench) 내에서 신호의 초기 상태를 설정하거나 입력 패턴을 생성하는 데 사용된다. 다음 코드는 리셋 신호와 클럭을 순차적으로 제어하여 DUT(설계 대상 회로)를 검증하는 예시이다:
initial begin
clk = 0;
rst_n = 0;
#10 rst_n = 1;
#100 $finish;
end
always #5 clk = ~clk; // 10ns 주기 클럭 생성
이 코드는 시뮬레이터 상에서 정확하게 시간 기반으로 동작하지만, 어떤 종합 툴도 이 initial 블록을 하드웨어로 변환하지 않는다. FPGA는 전원이 인가되면 무조건 동일한 초기 상태에서 시작해야 하며, 시뮬레이션 시간 t=0에만 유효한 "초기화"는 물리적 회로에서는 의미가 없다. 따라서 RTL 설계 코드에서는 initial 사용을 엄격히 금지해야 한다.
반면 always 블록은 하드웨어 동작을 직접 모델링하는 핵심 구성 요소이다. 이 블록은 특정 신호의 변화에 반응하여 실행되며, 민감도 리스트(sensitivity list)에 의해 제어된다. 민감도 리스트는 회로의 입력 변화를 감지하는 트리거 역할을 한다.
클럭 기반 시퀀셜 로직 모델링
동기 회로는 클럭 신호의 에지를 기준으로 상태를 갱신한다. 이를 표현하기 위해 다음과 같은 패턴이 사용된다:
reg [7:0] counter_reg;
wire [7:0] next_count;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
counter_reg <= 8'd0;
else
counter_reg <= next_count;
end
assign next_count = counter_reg + 1;
이 경우 posedge clk에 따라 플립플롭이 동작하며, 비동기 리셋은 negedge rst_n에 의해 즉시 반영된다. 여기서 중요한 점은 상태 저장 변수에 대한 할당이 비블로킹 할당(<=)을 사용해야 한다는 것이다. 이는 여러 레지스터 갱신이 동시에 발생해야 하는 시퀀셜 로직의 본질을 반영한다.
레벨 기반 조합 로직 모델링
입력 변화에 즉시 반응하는 조합 회로는 전형적으로 @(*) 또는 전체 입력 목록을 포함하는 민감도 리스트를 사용한다:
reg out_val;
always @(*) begin
case(sel)
2'b00: out_val = a & b;
2'b01: out_val = a | b;
2'b10: out_val = a ^ b;
default: out_val = 1'b0;
endcase
end
이 구조는 멀티플렉서 기반의 조합 로직으로 종합되며, 출력 값은 선택 신호 sel이나 입력 a, b가 변경되는 순간 즉시 재계산된다. 이때 반드시 블로킹 할당(=)을 사용해야 하며, 이는 연속적인 조건 평가 순서를 보장하기 때문이다.
1.2 블로킹 vs 비블로킹 할당: 타이밍 정확성을 위한 규칙
Verilog에서 가장 흔하면서도 치명적인 실수 중 하나는 잘못된 할당 연산자의 사용이다. 이 두 방식은 시뮬레이션 동작과 종합 결과 모두에 영향을 미친다.
블로킹 할당 (=)은 C 언어의 일반적인 할당처럼 순차적으로 실행된다. 한 줄이 완료된 후 다음 줄로 진행되며, 이는 조합 로직 내에서 변수 간 의존성이 존재할 때 필수적이다:
always @(*) begin
temp = a + b;
result = temp >> 1; // temp 값이 즉시 반영됨
end
반면 비블로킹 할당 (<=)은 해당 always 블록 내 모든 할당 문의 우변이 먼저 평가된 후, 마지막에 좌변에 동시 갱신된다. 이는 병렬 동작하는 플립플롭들의 동기화된 업데이트를 모델링하는 데 적합하다:
always @(posedge clk) begin
q1 <= d1;
q2 <= q1; // 이전 클럭 사이클의 q1 값이 유지됨
end
만약 위 예제에서 =를 사용했다면, q2는 현재 사이클의 d1 값을 그대로 복사하게 되어 예기치 않은 데이터 경로 문제가 발생한다.
1.3 조건문의 하드웨어 매핑: if-else와 case의 구조적 차이
Verilog의 조건문은 종합 과정에서 특정 하드웨어 구조로 변환되며, 그 결과는 조건의 우선순위와 구조에 따라 달라진다.
if-else 체인은 중첩된 멀티플렉서로 구현되며, 상위 조건일수록 더 높은 우선순위를 가지므로 지연 시간이 짧다:
always @(*) begin
if (ctrl_a)
output = val_a;
else if (ctrl_b)
output = val_b;
else
output = val_c;
end
이 경우 ctrl_a가 활성화되면 다른 조건은 무시되며, 회로상으로는 최상위 MUX에 연결되어 빠른 응답이 가능하다. 반면 case 문은 일반적으로 균등한 우선순위를 가지며, 각 항목이 독립된 비교기를 거쳐 멀티플렉서 네트워크로 결합된다:
always @(*) begin
case (sel)
2'b00: out_sig = x;
2'b01: out_sig = y;
2'b10: out_sig = z;
2'b11: out_sig = w;
endcase
end
이러한 구조는 디코더 기반의 멀티플렉서로 매핑되며, 선택 신호의 해독과 함께 출력이 결정된다. casez나 caser를 사용하면 더 효율적인 매스크 기반 매칭이 가능하나, 종합 결과의 예측 가능성에는 주의가 필요하다.