Verilog에서 round 및 saturation 처리를 활용한 데이터 비트 절사 기법

1. 서론

디지털 신호 처리(DSP) 알고리즘을 Verilog로 구현할 때 데이터 양자화와 비트 절사는 필수적인 과정입니다. 실제 프로젝트에서는 정밀도를 유지하기 위해 데이터를 절사하기 전에 반올림(round) 처리를 먼저 수행합니다. 반올림 과정에서 캐리(carry)가 발생하여 데이터 범위를 초과하는 경우에는 포화(saturation) 처리를 적용합니다. 포화 처리란 계산 결과가 특정 데이터 형식으로 표현 가능한 최댓값을 넘으면 최댓값으로, 최솟값을 넘으면 최솟값으로 대체하는 기법입니다.

본 글에서는 유부호(signed) 고정소수점 데이터 형식을 mQn으로 표기합니다. 예를 들어, 32비트 전체 비트 폭에 16비트 소수부를 가진 유부호 수는 32Q16으로 표현합니다. mQn 형식은 다음 정보를 포함합니다:

  • 유부호 수이며, 최상위 비트(MSB)는 부호 비트입니다.
  • 전체 비트 폭은 m입니다.
  • 소수부 비트 폭은 n입니다.

2. Verilog에서 유부호 데이터의 확장과 절사

2.1 유부호 수와 무부호 수의 차이

4비트 이진수 4'b1011이 주어졌을 때:

  • 무부호(unsigned)로 해석하면 1*2^3 + 0*2^2 + 1*2^1 + 1*2^0 = 11입니다.
  • 유부호(signed)로 해석하면 1*(-2^3) + 0*2^2 + 1*2^1 + 1*2^0 = -5입니다.

유부호 수의 최상위 비트는 음수 가중치(-2^(m-1))를 가지므로, 표현 가능한 범위가 다릅니다. m비트 유부호 정수의 범위는 -2^(m-1)부터 2^(m-1)-1까지입니다.

2.2 부호 확장(Sign Extension)

유부호 정수를 더 넓은 비트 폭으로 확장할 때는 부호 비트를 그대로 채웁니다. 예를 들어 4'b1011(-5)을 6비트로 확장하면 6'b111011이 됩니다. 이 값도 -5임을 확인할 수 있습니다.

2.3 유부호 소수 데이터

4Q2 형식인 4'b10111*(-2^1) + 0*2^0 + 1*2^(-1) + 1*2^(-2) = -1.25로 계산됩니다. 소수점 위치에 따라 각 비트의 가중치가 결정됩니다. mQn 형식의 데이터 범위는 -2^(m-n-1)부터 2^(m-n) - 1/2^n까지입니다. 데이터를 mQn에서 pQq(p>m, q>n)로 확장할 때는 정수부는 부호 확장, 소수부는 LSB 뒤에 0을 추가합니다.

2.4 유부호 수의 덧셈

두 유부호 수를 더할 때는 먼저 소수점을 정렬한 후, 한 비트의 부호 확장을 추가로 수행하여 합의 오버플로를 방지해야 합니다. 예를 들어, 5Q24Q3 데이터를 더하려면 6Q3으로 맞춘 후, 다시 7Q3으로 확장하여 더합니다.

2.5 유부호 수의 곱셈

mQnaQb 형식의 데이터를 곱했을 때 오버플로 없이 저장하려면 결과는 (m+a)Q(n+b) 형식이어야 합니다.

3. 반올림(Round) 처리

데이터의 일부 하위 비트를 절사할 때, 단순히 버리는 것이 아니라 절사 구간의 패턴에 따라 반올림을 수행합니다. Verilog 코드 예시는 다음과 같습니다 (9Q6 데이터를 6Q3으로 절사하는 경우):


// a: 9Q6 format data
// 목표: 소수부 6비트를 3비트로 줄임
wire carry_bit = a[8] ? ( a[2] & (|a[1:0]) ) : a[2];
wire [8:0] a_round = {a[8], a[8:3]} + carry_bit;

핵심 규칙은 다음과 같습니다:

  • 양수: 절사 구간의 최상위 비트가 1이면 캐리(carry)를 발생시킵니다.
  • 음수: 절사 구간의 최상위 비트가 1이고 나머지 비트 중 하나라도 1이면 캐리를 발생시킵니다(이는 음수에서 반올림이 실질적으로 더해지는 효과를 냅니다).

4. 포화(Saturation) 처리

반올림 후에도 데이터가 목표 형식의 범위를 초과하면 포화 처리를 적용합니다. 즉, 양수 최대값은 MSB가 0이고 나머지가 모두 1인 값, 음수 최소값은 MSB가 1이고 나머지가 모두 0인 값으로 고정합니다.

5. 실전 예제: a + b * c 연산

5.1 문제 정의

  • a: 16Q14
  • b: 16Q14
  • c: 16Q15
  • 결과 s: 16Q14 (반올림 및 포화 처리 포함)

5.2 분석 및 구현

  1. b * c 계산: b * c 결과는 오버플로를 방지하기 위해 32Q29 형식을 사용합니다.
  2. 덧셈 준비: a를 32Q29로 확장하고(소수점 정렬을 위해 하위 15비트에 0 추가), 두 32Q29 값을 33Q29로 부호 확장하여 더합니다.
  3. 반올림: 33Q29 결과의 하위 15비트([14:0])를 절사할 때 위의 반올림 로직을 적용하여 19Q14 결과를 얻습니다.
  4. 포화 처리: 19Q1416Q14로 절사할 때, 상위 3비트([18:16])가 단순히 부호 확장인지 검사합니다. 만약 그렇지 않으면(즉, [18:15]4'b0000 또는 4'b1111이 아니면) 포화 처리를 적용합니다.

Verilog 모듈 코드:


module dsp(
    input               I_clk,
    input               I_rst_n,
    input  signed [15:0] I_a,  // 16Q14
    input  signed [15:0] I_b,  // 16Q14
    input  signed [15:0] I_c,  // 16Q15
    output signed [15:0] O_s    // 16Q14
);

reg signed [15:0] R_a_16Q14, R_b_16Q14, R_c_16Q15;
wire signed [31:0] W_mult_b_c_32Q29;
wire signed [32:0] W_s_33Q29;
wire signed [31:0] W_a_32Q29;
wire W_carry_bit;
wire [18:0] W_s_19Q14_round;

always @(posedge I_clk or negedge I_rst_n) begin
    if(!I_rst_n) begin
        R_a_16Q14 <= 16'd0;
        R_b_16Q14 <= 16'd0;
        R_c_16Q15 <= 16'd0;
    end else begin
        R_a_16Q14 <= I_a;
        R_b_16Q14 <= I_b;
        R_c_16Q15 <= I_c;
    end
end

assign W_mult_b_c_32Q29 = R_b_16Q14 * R_c_16Q15;
assign W_a_32Q29 = {R_a_16Q14[15], R_a_16Q14, {15{1'b0}}};
assign W_s_33Q29 = {W_a_32Q29[31], W_a_32Q29} + {W_mult_b_c_32Q29[31], W_mult_b_c_32Q29};

assign W_carry_bit = W_s_33Q29[32] ? (W_s_33Q29[14] & (|W_s_33Q29[13:0])) : W_s_33Q29[14];
assign W_s_19Q14_round = {W_s_33Q29[32], W_s_33Q29[32:15]} + W_carry_bit;

assign O_s = (W_s_19Q14_round[18:15] == 4'b0000 || W_s_19Q14_round[18:15] == 4'b1111)
             ? W_s_19Q14_round[15:0]
             : {W_s_19Q14_round[18], {15{~W_s_19Q14_round[18]}}};
endmodule

6. 검증 및 커버리지

Matlab을 사용하여 4096개의 테스트 데이터를 생성하고(a, b, c의 경계값 포함), $readmemh를 통해 Verilog 시뮬레이터에 입력합니다. Verilog 결과와 Matlab 결과를 비교하여 정확성을 확인합니다. VCS와 같은 도구로 조건 커버리지(Condition Coverage)가 100%인지 확인하여 모든 시나리오(양수 반올림, 양수 포화, 음수 반올림, 음수 포화 등)가 테스트되었는지 검증합니다.

태그: Verilog DSP rounding saturation bit-truncation

7월 10일 05:18에 게시됨