입력이 하나가 되자 깨지는 RTL 매개변수 경계값
입력 개수가 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비트다. 하나밖에 없다면 선택 정보를 저장할 필요가 없다는 수학적 결과다.
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비트로 조용히 사라지는 선언이 아니다.
포트에서 최소 한 비트가 필요하다면 폭을 다음처럼 정할 수 있다.
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을 대신하지만, 임의 크기에서는 마지막 유효 인덱스 비교가 필요하다.
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로 구조를 나누는 편이 분명하다.
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의 라운드로빈 중재기는 입력 하나를 직접 통과시키는 별도 분기와 최소 인덱스 폭을 가진다. leading-zero counter 구현도 작은 폭의 특수 구조를 나눈다. 이 사례들은 범용성을 긴 공식 하나로 만드는 것보다, 구조가 달라지는 경계를 드러내는 방식이 유용하다는 것을 보여 준다.
지원하지 않는 설정도 계약의 일부다
N=0을 지원하지 않는다면 입력을 하나로 간주해 조용히 진행해서는 안 된다. 초기 검사나 elaboration 검사로 허용 범위를 명시해야 한다. 폭 계산에서 0을 1로 바꾸는 방어와, 항목 수 0을 지원한다고 주장하는 것은 다르다. 마찬가지로 특정 파이프라인 깊이 0을 bypass로 지원한다면, reset과 ready의 연결도 그 구조에 맞게 정의되어야 한다.
경계 검증에서는 큰 기본값 하나보다 1, 2, 3처럼 구조가 바뀌는 설정이 더 많은 정보를 줄 수 있다. 1은 선택 노드가 사라지는 경우, 2는 최소 트리, 3은 비트 표현 수와 실제 개수가 달라지는 경우다. 4 같은 거듭제곱을 함께 보면 자연 wrap에 의존하는 오류도 구별하기 쉽다. 모든 설정을 무작정 늘리기보다 어떤 구조적 가정을 흔드는지 정해 고르는 방식이다.
처음의 한 개짜리 모듈이 어려워진 이유는 작은 값에 특수한 수학이 숨어 있어서다. 0비트의 선택 정보와 0개의 중간 노드는, 반드시 언어에서 선언 가능한 0폭 신호와 0크기 회로로 자동 변환되지 않는다. RTL 매개변수 경계값을 다룬다는 것은 폭의 최소값, 유효 인덱스, 필요 없는 구조, 지원 범위를 각각 정의하는 일이다. 그 네 가지가 맞으면 범용 RTL의 가장 작은 구성도 설계의 정식 구성으로 다룰 수 있다.