[RTL] RTL 산술 연산: 비트 확장, 포화 연산, 그리고 반올림

RTL 설계를 하다 보면 wire [7:0] a, b, c를 선언해 놓고 무심코 assign c = a + b;라고 적는 경우가 많습니다. 문법 에러는 없지만, 이 코드는 특정 상황에서 데이터가 깨지는 ‘Silent Bug’의 주범이 됩니다.

디지털 회로에서 숫자를 다룬다는 것은 단순히 수학 계산을 넘어, 비트 폭(Bit Width)과 데이터 타입(Signed/Unsigned)을 물리적으로 관리해야 함을 의미합니다.

이번 글에서는 RTL 엔지니어가 반드시 정복해야 할 산술 연산의 3대장, 비트 확장 규칙, 부호 있는 연산의 함정, 그리고 DSP 설계의 꽃인 포화 연산(Saturation)과 반올림(Rounding) 기법을 정리해 드립니다.

1. 비트 폭은 저절로 늘어나지 않는다 (Bit Width Expansion)

하드웨어는 정해진 그릇(Register)의 크기만큼만 값을 담을 수 있습니다. 연산 결과가 그릇보다 크면 넘쳐흐르는 Overflow가 발생하여 데이터가 왜곡됩니다.

더하기와 빼기 (Addition / Subtraction)

N비트 수 두 개를 더하면 결과는 최대 N+1비트가 필요합니다.

  • 규칙: Sum_Width = Max(A_Width, B_Width) + 1
  • 예시: 8비트(255) + 8비트(255) = 510 (9비트 필요)

곱하기 (Multiplication)

N비트와 M비트를 곱하면 결과는 N+M비트가 필요합니다.

  • 규칙: Prod_Width = A_Width + B_Width
  • 예시: 8-bit(255) * 8-bit(255) = 65,025 (16-bit 필요)

Tip: RTL 설계할 때 미리 결과 와이어(wire)의 폭을 계산해서 넉넉하게 선언하는 습관을 들여야 합니다.

2. ‘Signed’와 ‘Unsigned’

Verilog에서 가장 치명적인 버그는 부호 있는 수(Signed)와 부호 없는 수(Unsigned)를 섞어서 연산할 때 발생합니다.

2’s Complement (2의 보수)와 함정

Verilog 표준에 따르면, 연산식에 단 하나라도 unsigned 변수가 섞여 있으면, 전체 식을 unsigned로 처리해 버립니다. 이를 Implicit Casting(암묵적 형변환)이라 합니다.

reg signed   [3:0] a = -2;  // 1110 (음수)
reg unsigned [3:0] b =  1;  // 0001 (양수)
wire signed  [4:0] result;

assign result = a + b; 
// 기대값: -1
// 실제값: a를 unsigned 14로 해석 -> 14 + 1 = 15 (치명적 오류!)

해결책: 모든 변수를 명시적으로 signed로 선언하거나, $signed() 시스템 함수를 사용하여 강제로 타입을 맞춰줘야 합니다.

3. 포화 연산 (Saturation Logic) 구현하기

오디오나 영상 신호 처리에서 오버플로우는 노이즈(Noise)가 됩니다.

  • Wrap-around (일반 동작): 최대값을 넘으면 0으로 돌아감 (예: 255 + 1 = 0).
  • Saturation (포화 연산): 최대값을 넘으면 최대값에 고정(Clipping) (예: 255 + 1 = 255).

RTL에서는 연산 결과의 최상위 비트(Overflow bit)를 감지하여 MUX로 값을 고정합니다.

4. 실무 테크닉: 반올림 (Rounding) 효율적으로 구현하기

나눗셈을 하거나 비트를 줄일 때(Truncation), 단순히 하위 비트를 버리면 값은 항상 작아지는 방향(Floor)으로 오차가 생깁니다. 이를 보정하기 위해 반올림(Round Half Up)을 사용합니다.

특히 ‘2로 나누기’ (평균 구하기 등)를 할 때, 굳이 복잡한 덧셈기 없이 최하위 비트(LSB)를 활용하는 매우 효율적인 실무 패턴이 있습니다.

원리: 나머지가 0.5면 올려라

이진수에서 LSB(최하위 비트)는 1 또는 0입니다.

  • 오른쪽으로 1비트 시프트(>>1) 하는 것은 2로 나눈 몫(Integer Div)을 의미합니다.
  • 이때 사라지는 LSB가 1이라면, 원래 값은 X.5였다는 뜻입니다.
  • 따라서 몫에 LSB를 더해주면 자연스럽게 반올림이 됩니다.

실전 코드: 평균값 구하기 (Rounding 적용)

module calc_average_round (
    input  wire [7:0] data_a,
    input  wire [7:0] data_b,
    output reg  [7:0] avg_out
);
    // 1. 합계 구하기 (오버플로우 방지 1비트 확장)
    wire [8:0] sum;
    assign sum = data_a + data_b;

    // 2. 2로 나누면서 반올림 (Divide by 2 with Rounding)
    // 원리: (Sum / 2) + (Sum % 2)
    // sum[8:1] : 상위 비트들 (2로 나눈 몫)
    // sum[0]   : 최하위 비트 (나머지, 즉 0.5 여부)
    
    always @(*) begin
        // 몫에 나머지를 더한다.
        avg_out = sum[8:1] + sum[0];
    end

endmodule

동작 검증

  • 합이 10일 때 (1010):
    • sum[8:1] = 101 (5)
    • sum[0] = 0
    • 결과: 5 + 0 = 5 (10 / 2 = 5) -> 정확함
  • 합이 11일 때 (1011):
    • sum[8:1] = 101 (5)
    • sum[0] = 1
    • 결과: 5 + 1 = 6 (11 / 2 = 5.5 반올림 -> 6) -> 정확함

이 방식은 별도의 비교기(Comparator) 없이 Adder 하나만으로 반올림을 구현할 수 있어 면적과 타이밍 면에서 매우 효율적입니다.

5. 결론: 디테일이 퀄리티를 만든다

RTL 설계에서 산술 연산은 가장 기초적이지만, 가장 많은 실수가 나오는 부분입니다.

  1. Bit Width: 연산 결과가 커질 것을 대비해 그릇을 키우세요.
  2. Type: signedunsigned를 절대 섞어 쓰지 마세요.
  3. Refinement: 필터나 영상 처리에는 Saturation을, 평균 계산에는 LSB 더하기(Rounding) 기법을 활용하세요.

참고: IEEE Standard

유사한 게시물