---
title: "입력이 하나가 되자 깨지는 RTL 매개변수 경계값"
author: "브레이브브라우저쓰자"
published_at: "2026-10-06T12:42:04.825Z"
updated_at: "2026-10-05T13:47:55.928Z"
tags: ["rtl 매개변수 경계값"]
canonical_url: "https://ariser.io/@writer_a9817d5a8de0/%EC%9E%85%EB%A0%A5%EC%9D%B4-%ED%95%98%EB%82%98%EA%B0%80-%EB%90%98%EC%9E%90-%EA%B9%A8%EC%A7%80%EB%8A%94-rtl-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EA%B2%BD%EA%B3%84%EA%B0%92"
ai_assisted: true
---

# 입력이 하나가 되자 깨지는 RTL 매개변수 경계값

이 글은 AI가 활용됐습니다.

[원문](https://ariser.io/@writer_a9817d5a8de0/%EC%9E%85%EB%A0%A5%EC%9D%B4-%ED%95%98%EB%82%98%EA%B0%80-%EB%90%98%EC%9E%90-%EA%B9%A8%EC%A7%80%EB%8A%94-rtl-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EA%B2%BD%EA%B3%84%EA%B0%92)

입력 개수가 16일 때 잘 동작하던 범용 모듈을 한 개짜리 구성에 재사용했더니, 인덱스 폭이 이상해지고 일부 배열 참조가 깨진다. 지원 규모를 줄였는데 더 복잡한 문제가 나타난 것이다. **RTL 매개변수 경계값은 큰 회로를 작게 축소하는 설정이 아니라, 회로의 구조 자체가 다른 경우가 생기는 지점**이다.

매개변수화된 RTL은 실행 중 설정 값을 바꾸는 소프트웨어 함수와 다르다. N을 정하면 elaboration 단계에서 포트 폭, 배열 크기, generate 분기와 회로의 연결이 결정된다. 따라서 N=1이 N=16의 단순한 축소판이라는 보장은 없다. 내부 노드가 아예 필요 없어지는 경우를 별도 구조로 표현해야 할 수 있다.

## clog2가 0을 돌려주는 정확한 의미

N개 중 하나를 고르는 인덱스에 필요한 정보량은 ceil(log2 N)비트다. N=8이면 3비트, N=3이면 2비트, N=1이면 0비트다. 하나밖에 없다면 선택 정보를 저장할 필요가 없다는 수학적 결과다.

```systemverilog
parameter int unsigned N = 1;
localparam int unsigned IDX_W = $clog2(N);
logic [IDX_W-1:0] index;
```

N=1이면 범위가 [-1:0]이 된다. 이를 “0비트 벡터”라고 읽으면 안 된다. packed 범위의 크기는 양 끝 차이의 절댓값에 1을 더하므로, [-1:0]은 두 위치를 가진 범위가 될 수 있다. 도구가 역방향 범위나 예상하지 못한 폭에 경고를 낼 수도 있지만, 0비트로 조용히 사라지는 선언이 아니다.

포트에서 최소 한 비트가 필요하다면 폭을 다음처럼 정할 수 있다.

```systemverilog
localparam int unsigned IDX_W = (N > 1) ? $clog2(N) : 1;
logic [IDX_W-1:0] index;
```

이 수정은 선언의 형태를 안정시키지만, N=1에서 index=1이 합법이 된다는 뜻은 아니다. 유효 인덱스는 여전히 0뿐이다. **표현할 수 있는 비트 패턴의 수와 실제 항목의 수를 구별해야 한다.**

## 폭을 고친 뒤에도 남는 범위 문제

N=3이라면 IDX_W=2이고 0, 1, 2, 3을 표현할 수 있다. 하지만 배열에는 0~2만 존재한다. 비트폭이 맞아 보인다는 이유로 index를 그대로 배열 접근에 쓰면 값 3에서 범위를 벗어난다. 해당 조건의 시뮬레이션 결과와 합성 처리는 구조와 도구에 따라 달라질 수 있으므로, 의미 있는 접근 전에 index<N이라는 계약이 필요하다.

순환 포인터도 비슷하다. 2비트 포인터를 매번 1씩 증가시키면 0→1→2→3→0이다. N=3인 구조의 원하는 순서는 0→1→2→0이다. 2의 거듭제곱 크기에서는 자연스러운 비트 오버플로가 wrap을 대신하지만, 임의 크기에서는 마지막 유효 인덱스 비교가 필요하다.

```systemverilog
always_comb begin
  index_d = index_q;
  if (advance) begin
    if (index_q == IDX_W'(N-1))
      index_d = '0;
    else
      index_d = index_q + IDX_W'(1);
  end
end
```

이 코드는 N>=1, index_q가 유효 범위 안에 있다는 조건의 설명용 다음 상태 계산이다. N=1에서는 terminal 값도 0이므로 advance해도 0을 유지한다. 불법 index에서의 복구까지 정의하는 코드는 아니며, 그 요구가 있으면 별도로 추가해야 한다. 폭 문제 하나를 해결한 뒤 정상 범위 가정을 잊는 일이 흔하다.

## 노드가 사라지는 경우는 generate로 갈라야 한다

N개의 입력을 이진 트리로 고른다고 하자. 일반 구성에는 중간 선택 노드가 필요하지만 N=1이면 입력을 출력으로 바로 연결하면 된다. “중간 노드 수=N-1”이라는 식을 모든 구성의 배열 선언에 그대로 대입하면, 노드가 없는 경우를 언어가 표현하는 방식과 충돌할 수 있다.

이때에는 generate로 구조를 나누는 편이 분명하다.

```systemverilog
if (N == 1) begin : gen_single
  assign selected = inputs[0];
end else begin : gen_multiple
  // N >= 2인 선택 회로를 여기에 구성한다.
end
```

주석 부분은 완성된 트리 코드가 아니라 분기 범위를 보여 주는 표시다. N=1에서 필요 없는 배열과 연산은 gen_multiple 안에 두어 해당 구조가 elaboration되지 않게 한다. 런타임 if로 결과만 가리면, 잘못된 폭이나 참조가 이미 회로 구성 과정에서 문제가 될 수 있다.

[PULP common_cells의 라운드로빈 중재기](https://github.com/pulp-platform/common_cells/blob/master/src/cc_rr_arb_tree.sv)는 입력 하나를 직접 통과시키는 별도 분기와 최소 인덱스 폭을 가진다. [leading-zero counter 구현](https://github.com/pulp-platform/common_cells/blob/master/src/cc_lzc.sv)도 작은 폭의 특수 구조를 나눈다. 이 사례들은 범용성을 긴 공식 하나로 만드는 것보다, 구조가 달라지는 경계를 드러내는 방식이 유용하다는 것을 보여 준다.

## 지원하지 않는 설정도 계약의 일부다

N=0을 지원하지 않는다면 입력을 하나로 간주해 조용히 진행해서는 안 된다. 초기 검사나 elaboration 검사로 허용 범위를 명시해야 한다. 폭 계산에서 0을 1로 바꾸는 방어와, 항목 수 0을 지원한다고 주장하는 것은 다르다. 마찬가지로 특정 파이프라인 깊이 0을 bypass로 지원한다면, reset과 ready의 연결도 그 구조에 맞게 정의되어야 한다.

경계 검증에서는 큰 기본값 하나보다 1, 2, 3처럼 구조가 바뀌는 설정이 더 많은 정보를 줄 수 있다. 1은 선택 노드가 사라지는 경우, 2는 최소 트리, 3은 비트 표현 수와 실제 개수가 달라지는 경우다. 4 같은 거듭제곱을 함께 보면 자연 wrap에 의존하는 오류도 구별하기 쉽다. 모든 설정을 무작정 늘리기보다 어떤 구조적 가정을 흔드는지 정해 고르는 방식이다.

처음의 한 개짜리 모듈이 어려워진 이유는 작은 값에 특수한 수학이 숨어 있어서다. 0비트의 선택 정보와 0개의 중간 노드는, 반드시 언어에서 선언 가능한 0폭 신호와 0크기 회로로 자동 변환되지 않는다. **RTL 매개변수 경계값을 다룬다는 것은 폭의 최소값, 유효 인덱스, 필요 없는 구조, 지원 범위를 각각 정의하는 일**이다. 그 네 가지가 맞으면 범용 RTL의 가장 작은 구성도 설계의 정식 구성으로 다룰 수 있다.

