BSV의 변수는 값에 붙인 이름이며, 클록 에지를 지나 값을 기억하는 저장소가 아닙니다.
문장은 이 이름에 값을 결합하고, 이름 위에서 반복하는 반복문은 만들 하드웨어의 양을 기술합니다.
문장마다 하는 일을 정확히 구분해야 상태를 잃거나 엉뚱한 사이클에 동작하는 설계를
피할 수 있습니다. 이 장은 BSV Language Reference Guide 제9장 "변수 선언과 문장"
가운데 변수 선언, 리터럴의 문맥, 반복문을 다룹니다. 레지스터 문장 <-와 <=는
레지스터와 rule 하나 장에서, if와 case는
선택·case·don't-care 값 장에서, 함수 정의는
Bool·Bit와 순수 함수 장에서 다룹니다.
이 장의 문장을 읽을 때마다 값에 이름을 붙이는 문장인지, 하드웨어를 구성하는 문장인지, 상태 변경을 요청하는 문장인지 판단해 보세요. 이 판단이 문장의 효과가 나타나는 시점을 알려 줍니다.
1. 변수 선언
1.1 선언과 초기값
선언은 식별자에 타입을 지정하고, 필요하면 초기값도 지정합니다. 타입이 같은 이름 여러 개를 선언 하나에 함께 쓸 수 있습니다.
형식
타입 이름;
타입 이름 = 식;
타입 이름1 = 식1, 이름2 = 식2;UInt#(8) count = 0;
Integer x = 16, y = 32;
Bool ready;| 코드 | 의미 |
|---|---|
UInt#(8) count = 0; | 0에서 시작하는 8비트 부호 없는 값 count입니다 |
Integer x = 16, y = 32; | 선언 하나로 Integer 값 두 개를 만듭니다 |
Bool ready; | 타입만 정하고 값은 아직 없습니다. 뒤에서 =로 값을 지정해야 합니다 |
Integer는 크기 제한이 없는 정수이며 elaboration 중에만 존재합니다. 반복문의
인덱스나 상수에 쓰기 좋지만 하드웨어 신호가 되지는 않습니다.
1.2 변수는 저장소가 아니라 값의 이름입니다
변수에는 =로 여러 번 대입할 수 있습니다. 대입할 때마다 이후 문장에서 그 이름이
가리키는 값이 바뀔 뿐입니다. 컴파일러는 이렇게 이어지는 값을 배선, 연산기,
멀티플렉서로 바꿉니다. 클록 에지가 와도 기억되는 값은 없습니다.
UInt#(8) total = a;
total = total + b; // total은 이제 a + b
total = total + c; // total은 이제 a + b + c. 덧셈기 두 개가 이어진 회로위 세 줄은 같은 사이클에 평가되는 덧셈기 두 개를 기술합니다. 다음 사이클까지 남아야
하는 값은 Reg 같은 상태 저장 모듈에 두어야 합니다.
rule step;
UInt#(8) acc = 0;
acc = acc + 1;
r <= acc;
endrule확인 문제
r은 0에서 시작하는 레지스터이고 step은 매 사이클 실행됩니다. step이 세 번 실행된 뒤 r에는 무엇이 들어 있을까요?
참고. BSV의 변수는 함수형 언어의 지역 이름과 비슷합니다. =를 메모리 쓰기가
아니라 "지금부터 이 이름은 이 값을 뜻한다"로 읽으세요.
1.3 배열
배열은 이름 뒤 대괄호에 크기를 적어 선언합니다. 크기는 elaboration 중에 계산할 수
있는 상수여야 합니다.
형식
타입 이름[크기];
타입 이름[크기] = { 값0, 값1, ... };Int#(5) samples[3] = { 14, 12, 9 };
Int#(5) middle = samples[1]; // 12| 부분 | 의미 |
|---|---|
Int#(5) | 원소의 타입입니다 |
[3] | 원소 세 개이며 인덱스는 0부터 2까지입니다 |
{ 14, 12, 9 } | 인덱스 순서대로 지정한 초기값입니다 |
BSV 배열은 값이나 모듈 인스턴스를 묶는 수단입니다. 읽기 포트와 쓰기 포트를 가진
메모리가 아니므로, 메모리가 필요하면 RegFile 같은 primitive를 사용합니다.
2. 선언된 타입이 식의 의미를 정합니다
2.1 왼쪽 타입이 리터럴을 해석합니다
37 같은 크기 미지정 정수 리터럴에는 타입이 정해져 있지 않습니다. 리터럴을 대입받는
선언이 타입을 제공하고, 그 타입의 Literal 인스턴스가 정수를 변환합니다. bsc가
인스턴스를 찾는 원리는 타입클래스와 proviso
장에서 다루며, 이 절에서는 그 결과만 사용합니다.
형식
타입 이름 = 정수_리터럴; // Literal#(타입)을 사용합니다UInt#(8) count = 37; // Literal#(UInt#(8)): 8비트 값 37
Int#(16) delta = -4; // Literal#(Int#(16)): 16비트 값 -4내장 타입은 elaboration 중에 리터럴의 범위도 검사합니다. UInt#(8) c = 300;은
오류 T0051, Literal 300 is not a valid UInt#(8).로 중단되며 시뮬레이션은 실행되지
않습니다.
확인 문제
다음 선언 가운데 T0051로 중단되는 것은 어느 것일까요?
8'd37처럼 폭을 적은 크기 지정 리터럴은 토큰에 폭이 들어 있으므로 Literal이
아니라 SizedLiteral 타입클래스가 처리합니다.
2.2 예제: 도메인 의미를 가진 리터럴
사용자 정의 타입은 리터럴에 고유한 의미를 줄 수 있습니다. OverloadedLiterals.bsv의
Percent는 정수로 주어진 퍼센트 값에 10을 곱해 10비트 필드에 저장합니다. 필드 이름과
출력 단위는 basisPoints이지만, 실제 저장 단위는 basis point가 아니라 0.1퍼센트포인트입니다.
구조체를 먼저 읽고, 인스턴스의 두 함수를 구분하세요. fromInteger는 소스의 정수를 저장 단위로
변환하고, inLiteralRange는 소스의 정수가 이 타입의 도메인에 속하는지 판정합니다.
실행하기 전에 예측해 보세요.
Percent utilization = 37;은 어떤 함수를 어떤 인자로 호출하며,basisPoints에는 무엇이 저장될까요?Percent full = 100;은 무엇을 저장할까요?- 10비트 필드에는 1023까지 저장할 수 있습니다. 그렇다면
inLiteralRange(probe, 100)과inLiteralRange(probe, 101)은 각각 무엇을 반환할까요?
확인 문제
Percent utilization = 37; 뒤에 basisPoints에는 무엇이 들어 있을까요?
package OverloadedLiterals;
typedef struct {
UInt#(10) basisPoints;
} Percent deriving (Bits, Eq);
instance Literal#(Percent);
function Percent fromInteger(Integer value);
// 정수로 주어진 퍼센트 값에 10을 곱해 저장한다.
// 필드 이름과 달리 저장 단위는 basis point가 아니라 0.1퍼센트포인트다.
return Percent { basisPoints: fromInteger(value * 10) };
endfunction
// 의미상 허용 범위는 10비트 필드로 표현할 수 있는 범위보다 좁다.
function Bool inLiteralRange(Percent target, Integer value);
return value >= 0 && value <= 100;
endfunction
endinstance
(* synthesize *)
module mkTop(Empty);
rule show;
// 선언된 Percent 타입이 크기 미지정 정수 리터럴에 쓸 이 Literal 인스턴스를 선택한다.
Percent utilization = 37;
Percent full = 100;
Percent probe = unpack(10'd0);
$display("37%% = %0d basis points", utilization.basisPoints);
$display("100%% = %0d basis points", full.basisPoints);
$display("100 valid = %0d", inLiteralRange(probe, 100));
$display("101 valid = %0d", inLiteralRange(probe, 101));
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 출력 줄 | 확인할 내용 |
|---|---|
37% | 선언이 Literal#(Percent)를 선택했고 fromInteger(37)이 370을 저장했습니다 |
100% | 같은 변환으로 1000이 저장되었습니다. 변환 방식은 토큰이 아니라 선언된 타입이 정합니다 |
100 valid | 1입니다. 100은 인스턴스가 정한 도메인 안에 있습니다 |
101 valid | 0입니다. 필드에 저장할 공간은 있지만 도메인 밖의 값입니다 |
직접 바꿔 보세요.
Percent full = 100;을Percent full = 101;로 바꾸어 보세요.UInt#(8)과 달리 빌드가 성공하고 두 번째 줄에 저장된 값 1010이 출력됩니다. 현재bsc는 사용자 정의inLiteralRange를 자동으로 호출하지 않습니다. 이 함수는 도메인을 문서로 남기고 직접 호출할 수 있게 할 뿐, 잘못된 리터럴을 스스로 막지 않습니다. 실험이 끝나면 100으로 되돌리세요.Percent utilization = 37;을UInt#(8) utilization = 37;로 바꾼다고 생각해 보세요. 다른 인스턴스가 선택되므로 같은 토큰이 평범한 값 37이 됩니다.
3. 반복문은 유한한 구조를 기술합니다
3.1 for와 while
for와 while은 문장을 반복합니다. bsc는 elaboration 중에 반복문을 정해진
개수의 본문 사본으로 펼칩니다. 따라서 반복문은 여러 사이클에 걸쳐 실행되는 과정이
아니라 만들 하드웨어의 양을 기술합니다.
형식
for (초기화; 조건; 갱신) 문장
while (조건) 문장Bit#(4) reversed = 0;
for (Integer i = 0; i < 4; i = i + 1)
reversed[i] = data[3 - i];| 부분 | 의미 |
|---|---|
Integer i = 0 | elaboration 중에만 쓰이는 인덱스입니다 |
i < 4 | 상수 경계이므로 본문 사본이 네 개 만들어집니다 |
reversed[i] = data[3 - i]; | 사본마다 비트 하나를 연결합니다 |
Integer 인덱스와 상수 경계를 쓰는 반복문은 항상 펼쳐집니다. 반대로 elaboration이
판단할 수 없는 실행 중의 값에 조건이 달려 있으면 펼치기가 끝나지 않습니다. 예를 들어
n을 레지스터에서 읽고 while (c != n) c = c + 1;이라고 쓰면, bsc는 하드웨어를
만들지 못하고 스택 공간 초과로 중단됩니다. 실제로 여러 사이클에 걸친 반복은
레지스터와 rule, 또는 StmtFSM으로 만드는 유한 상태 기계
장의 FSM 표기로 작성합니다.
3.2 예제: 1인 비트 세기
ControlFlowFunction.bsv는 반복문과 if로 바이트의 1인 비트 개수를 셉니다. 반환
타입과 count의 초기값을 먼저 확인하세요. 그다음 인덱스 0부터 7까지는 정적인
구조이고, 각 인덱스에서 검사하는 value[i]는 실행 중의 입력이라는 점을 구분하세요.
실행하기 전에 예측해 보세요.
- 입력
8'b10110101에서 검사가 하나씩 끝날 때마다count는 얼마가 될까요? - 함수 계산에는 1사이클이 필요할까요, 8사이클이 필요할까요?
확인 문제
countOnes는 for 반복문으로 비트 여덟 개를 검사합니다. 호출 한 번에 몇 사이클이 걸리고, 8'b10110101에 대해 무엇을 출력할까요?
package ControlFlowFunction;
function UInt#(4) countOnes(Bit#(8) value);
UInt#(4) count = 0;
// Integer 반복 변수와 상수 경계가 있으므로 elaboration이 검사 여덟 개를 모두 펼친다.
for (Integer i = 0; i < 8; i = i + 1) begin
if (value[i] == 1) begin
// 변수 대입은 조합 결과를 누적할 뿐 상태를 만들지 않는다.
count = count + 1;
end
end
return count;
endfunction
(* synthesize *)
module mkTop(Empty);
rule show;
$display("ones in b5 = %0d", countOnes(8'b10110101));
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인. elaboration이 검사 여덟 개를 펼칩니다. 최하위 비트부터 따라가면
다음과 같습니다.
| 인덱스 | 선택된 비트 | if 뒤의 count |
|---|---|---|
| 시작 | — | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 2 | 1 | 2 |
| 3 | 0 | 2 |
| 4 | 1 | 3 |
| 5 | 1 | 4 |
| 6 | 0 | 4 |
| 7 | 1 | 5 |
출력되는 개수는 5입니다. count = count + 1 여덟 개는 하나의 조합 함수를 이루며,
show가 실행되는 한 사이클 안에 평가됩니다. 반복 횟수를 세는 레지스터는 생기지
않습니다. 비트 검사와 덧셈기는 합성할 수 있고, $display와 $finish는 시뮬레이션
전용입니다.
직접 바꿔 보세요.
- 경계를
i < 8에서i < 4로 바꾸어 보세요. 비트 3–0의0101만 검사하므로 결과는 2입니다. 입력은 그대로이고, 경계가 하드웨어 구조를 바꾸었습니다. - 반복문을
Integer i = 0; while (i < 8) begin ... i = i + 1; end형태로 바꾸고 안쪽if는 그대로 두어 보세요. 조건이 여전히elaboration중에 정해지므로 결과는 5입니다.
4. 자주 하는 실수
4.1 =로 모듈 인스턴스화하기
// 잘못된 코드: 레지스터가 아니라 모듈 생성 과정 자체를 결합합니다
Reg#(UInt#(8)) r = mkReg(0);
// 바른 코드: 모듈을 실행하고 인터페이스를 결합합니다
Reg#(UInt#(8)) r <- mkReg(0);mkReg(0)은 모듈이므로 그 값은 Reg 인터페이스를 반환하는 구성 단계입니다. 이
단계를 실행하는 것은 <-뿐입니다. =를 쓰면 타입이 맞지 않아 bsc가 mkReg를
사용한 위치에서 타입 오류 T0080을 보고합니다. 세 가지 대입 형식 =, <-, <=의
비교는 레지스터와 rule 하나 장에 있습니다.
4.2 값을 지정하지 않은 경로 남기기
// 잘못된 코드: sel이 False이면 result에 값이 없습니다
UInt#(8) result;
if (sel) result = 5;
return result;
// 바른 코드: 모든 경로에서 값을 지정합니다
UInt#(8) result = 0;
if (sel) result = 5;
return result;확인 문제
잘못된 코드가 rule에서 호출하는 함수 안에 있습니다. bsc는 무엇을 보고할까요?
잘못된 코드는 먼저 경고 P0042(Assignment to `result' missing from some paths)를
받습니다. 함수를 사용하는 순간 elaboration은 sel이 False인 경로의 값이 필요해지고,
오류 G0028(`result' uses uninitialized value)로 중단됩니다. 호출되지 않는 함수는
경고만 받으므로, 첫 호출이 생길 때까지 실수가 드러나지 않을 수 있습니다. 이전 값을
기억하는 래치는 만들어지지 않습니다. if 앞에서 기본값을 지정하거나 모든 분기에서
값을 지정하세요.
5. 정리
| 주제 | 핵심 |
|---|---|
| 변수 | 값의 이름입니다. =로 다시 대입하면 저장소가 아니라 논리가 만들어집니다 |
| 배열 | 크기가 상수인 값이나 인스턴스의 묶음이며 메모리가 아닙니다 |
| 선언된 타입 | Literal 인스턴스를 선택합니다. 내장 타입은 T0051로 리터럴 범위를 검사합니다 |
| 값이 빠진 경로 | 경고 P0042가 나고, 값을 사용하면 오류 G0028로 중단됩니다. 분기 전에 초기값을 지정합니다 |
| 반복문 | elaboration 중에 펼쳐집니다. 실행 중의 반복은 레지스터와 rule로 작성합니다 |
6. 확인 문제
-
함수의 지역 누산 변수에 여덟 번 대입해도 8사이클 레지스터가 생기지 않는 이유는 무엇일까요?
정답 보기
=는 이후 문장에서 그 이름이 가리킬 값을 바꿀 뿐입니다.elaboration이 반복문을 이어진 덧셈기 여덟 개로 펼치고, 이 덧셈기들이 한 사이클에 평가되는 조합 함수 하나를 이룹니다. -
countOnes에서elaboration중에 정해지는 부분과 실행 중의 입력에 따라 달라지는 부분은 각각 무엇일까요?정답 보기
인덱스 0부터 7과 검사의 개수는
elaboration중에 정해집니다. 각 위치의 비트value[i]와 그에 따라 덧셈기가 1을 더하는지 여부는 실행 중에 정해집니다. -
Percent full = 101;을 쓰면 어떤 결과가 나오며,UInt#(8) c = 300;과는 어떻게 다를까요?정답 보기
Percent full = 101;은 컴파일되고basisPoints에 1010을 저장합니다.bsc가 사용자 정의inLiteralRange를 자동으로 호출하지 않기 때문입니다.UInt#(8) c = 300;은 내장 타입의 리터럴 범위 검사에 걸려elaboration중에T0051로 중단됩니다.
이 장의 실습 문제는 두 예제를 이어 갑니다. 퍼센트 예제 아래의 문제에서는 Percent
인스턴스가 잘못된 배율로 값을 저장하고 도메인 밖의 값까지 허용합니다. 비트 세기 예제
아래의 문제에서는 지금은 입력을 무시하는 개수 세기 함수를 완성합니다.