BSV 파일 하나가 패키지 하나입니다. 패키지는 따로 컴파일되는 단위이자, 이름을
바깥에 공개할지 감출지 정하는 단위입니다. 설계가 여러 파일로 커지면 패키지가 "어떤
이름을 누가 쓸 수 있는가"를 정합니다. 이 장은 BSV Language Reference Guide 제3장의
패키지 체계를 다룹니다. 모듈과 인터페이스
장과 레지스터와 rule 하나 장의 모듈, rule,
레지스터, 어휘 요소 장의 식별자 규칙, 그리고
method: value·Action·ActionValue 장과
준비 조건·모듈 인자·합성 경계 장의
method와 준비 조건을 알고 있어야 합니다.
1. 패키지 선언
1.1 패키지의 기본 형식
패키지는 package로 시작해 endpackage로 끝납니다. 아래 형식은 export를
import보다 먼저 적었지만, 다시 내보내기 예제처럼 import 뒤에 export를
적어도 컴파일됩니다. 두 선언 사이에 반드시 지켜야 하는 순서는 아닙니다.
형식
package 패키지_이름;
export 공개할_이름, ...; // 생략 가능
import 다른_패키지::*; // 생략 가능
// 최상위 정의: 타입, 인터페이스, 모듈, 함수, 상수
endpackage: 패키지_이름package Counter;
import FIFO::*;
Bit#(8) limit = 10;
module mkCounter(Empty);
Reg#(Bit#(8)) count <- mkReg(0);
endmodule
endpackage: Counter| 부분 | 의미 |
|---|---|
package Counter; | 패키지 이름을 정합니다. 파일 이름은 Counter.bsv여야 합니다 |
import FIFO::*; | 라이브러리 패키지 FIFO가 공개한 이름을 들여옵니다 |
Bit#(8) limit = 10; | 패키지 수준 상수입니다 |
endpackage: Counter | 패키지를 닫습니다. 콜론 뒤 이름은 생략할 수 있습니다 |
1.2 패키지 이름과 파일 이름
패키지 이름은 파일 이름과 같아야 합니다. bsc는 import Tally::*;를 만나면
Tally.bsv 파일을 찾아 읽기 때문입니다. 두 이름이 다르면 의존성을 따라가는
-u 컴파일에서 S0001 오류가 발생합니다.
여는 package 줄과 닫는 endpackage 줄은 함께 생략할 수 있습니다. 이때는 파일
이름이 패키지 이름이 됩니다.
1.3 정의 순서
패키지 수준의 정의는 어떤 순서로 써도 됩니다. 모듈이 파일 뒤쪽에 정의된 함수나 타입을 앞에서 사용해도 문제없습니다.
2. export: 공개할 이름 정하기
2.1 기본 공개 규칙
export 선언이 하나도 없으면 패키지가 직접 정의한 이름은 모두 공개되고, 다른
패키지에서 들여온 이름은 공개되지 않습니다. export 선언이 하나라도 있으면 나열한
항목만 공개됩니다.
export 선언 | 공개되는 이름 |
|---|---|
| 없음 | 이 패키지가 정의한 모든 이름 |
| 하나 이상 | 나열한 이름만 |
2.2 export 형식과 (..)
형식
export 이름1, 이름2, 타입_이름(..);export TallyIfc(..), mkTally;| 항목 | 공개되는 것 |
|---|---|
mkTally | 모듈 이름 |
TallyIfc | 타입 이름만. 바깥에서는 내부를 볼 수 없는 추상 타입이 됩니다 |
TallyIfc(..) | 타입 이름과 내부 표현. 인터페이스라면 method, 구조체라면 필드, tagged union이라면 태그가 공개됩니다 |
(..) 없이 인터페이스를 공개하면 들여온 쪽에서 그 인터페이스의 method를 호출할 수
없습니다. 현재 bsc는 enum label과 인터페이스 method를 공개할 때도 (..)를
요구합니다.
참고. BSV Language Reference Guide는 (..)를 구조체와 union에 대해서만
설명합니다. 이 장의 내용과 예제는 현재 컴파일러의 동작을 기준으로 합니다.
2.3 다시 내보내기
들여온 이름도 export 목록에 적으면 다시 공개됩니다. export P::*;는 패키지 P가
공개한 이름을 모두 다시 공개합니다.
import Tally::*;
export Tally::*; // 이 패키지를 들여오는 쪽에서 Tally를 따로 들여올 필요가 없습니다export가 없는 두 패키지에 기본 규칙을 적용해 보세요.
package PkgA;
import FIFO::*;
module mkA(Empty);
FIFO#(Bit#(8)) q <- mkFIFO;
endmodule
endpackage
package PkgB;
import PkgA::*;
(* synthesize *)
module mkTop(Empty);
FIFO#(Bit#(8)) q <- mkFIFO;
endmodule
endpackage확인 문제
PkgA.bsv와 PkgB.bsv를 bsc -u로 컴파일합니다. PkgB는 어떻게 될까요?
3. import: 다른 패키지의 이름 들여오기
형식
import 패키지_이름::*;import Tally::*; // Tally가 공개한 모든 이름을 현재 범위로 들여옵니다::*는 생략할 수 없습니다. BSV에는 무엇을 들여올지 적지 않는 import 형태가 없으므로
import Tally;라고 쓰면 P0005 구문 오류가 발생합니다. 메시지는 ::가 와야 할 위치를
가리킵니다.
import는 이름을 쓸 수 있게 해 줄 뿐 하드웨어를 만들지 않습니다. 하드웨어는 모듈을
인스턴스화할 때 생깁니다.
4. 범위, 이름 충돌, 한정 이름
4.1 어휘적 범위
BSV는 어휘적 범위를 사용합니다. 이름은 소스에서 그 이름을 둘러싼 가장 가까운 선언을 가리키므로, 안쪽 범위의 선언이 바깥 범위의 같은 이름을 가립니다.
Bit#(8) limit = 10; // 패키지 범위
module mkTop(Empty);
rule show;
Bit#(8) limit = 3; // rule 범위: 바깥 limit을 가립니다
$display("%0d", limit); // 3이 출력됩니다
$finish(0);
endrule
endmodule가장 바깥 범위는 패키지이고, 모듈, 인터페이스, 함수, method, rule, action 블록,
반복문이 안쪽 범위를 만듭니다.
4.2 한정 이름
이름은 언제나 선언 하나만 가리켜야 합니다. 현재 패키지의 정의와 들여온 이름이 겹쳐
x가 모호해지면 한정 이름 P::x로 패키지 P가 공개한 x를 지정합니다.
형식
패키지_이름::이름Tally::TallyIfc c <- Tally::mkTally; // 어느 패키지의 이름인지 명시5. 표준 프렐류드
표준 프렐류드는 import 선언 없이 모든 패키지에 자동으로
들어옵니다. Bit, Bool, Reg, mkReg, Empty 같은 기본 정의가 여기에 있습니다.
FIFO, Vector 같은 그 밖의 라이브러리 패키지는 직접 import해야 합니다.
주의. 프렐류드에 있는 이름과 같은 이름을 새로 정의하면 매번 한정 이름을 써야 해서 소스가 읽기 어려워집니다. 프렐류드 이름과 겹치지 않는 이름을 고르세요.
6. 예제: 두 파일로 나눈 설계
6.1 코드 읽기
예제는 두 파일로 이루어집니다. Tally.bsv는 카운터 인터페이스와 모듈을 정의하고,
Top.bsv는 그것을 들여와 사용합니다. 먼저 Tally.bsv에서 다음을 찾으세요.
export목록에 있는 공개 이름 두 개- 목록에 없는 패키지 수준 상수
limit incrementmethod 헤더의 조건if (count < limit)
// 패키지 이름은 파일 이름과 일치해야 한다. Tally.bsv에는 package Tally가 온다.
package Tally;
// 내보낸 것만 바깥에서 보인다. export 절이 없으면
// 여기 정의된 모든 것이 보인다.
export TallyIfc(..), mkTally;
interface TallyIfc;
method Action increment;
method Bit#(8) value;
endinterface
// 내보내지 않음. 이 패키지 안에서만 보인다.
Bit#(8) limit = 5;
module mkTally(TallyIfc);
Reg#(Bit#(8)) count <- mkReg(0);
method Action increment if (count < limit);
count <= count + 1;
endmethod
method Bit#(8) value = count;
endmodule
endpackage다음으로 Top.bsv에서 import 선언, mkTally 인스턴스, rule 두 개를 찾으세요.
drive에는 명시 조건이 없고, stop은 c.value == 5일 때 준비됩니다.
package Top;
// Tally가 내보낸 모든 이름을 들여온다. ::*는 생략할 수 없다.
import Tally::*;
(* synthesize *)
module mkTop(Empty);
TallyIfc c <- mkTally;
rule drive;
c.increment;
$display("value = %0d", c.value);
endrule
rule stop (c.value == 5);
$display("stopped at %0d", c.value);
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
컴파일 명령은 bsc -u로 Top.bsv만 지정합니다. -u를 주면 bsc가 import를 따라
필요한 패키지(Tally.bsv)를 찾아 먼저 컴파일합니다.
6.2 실행하기 전에 예측해 보세요
Top이 쓰는TallyIfc와mkTally는 어느 파일에서 정의될까요?drive는increment를 몇 번 호출할 수 있을까요?drive가 출력하는 값은 그 사이클의 쓰기가 반영되기 전의 값일까요, 반영된 뒤의 값일까요?
확인 문제
drive 헤더에는 조건이 없습니다. stop이 시뮬레이션을 끝내기 전까지 value = 줄은 몇 개 출력될까요?
6.3 결과 확인
출력 줄을 다음 상태 추적표와 비교하세요.
사이클 시작의 count | increment 준비 | 실행되는 rule | 출력되는 값 | 경계 뒤 count |
|---|---|---|---|---|
| 0 | 준비됨 | drive | value = 0 | 1 |
| 1–3 | 준비됨 | drive | value = 1–3 | 2–4 |
| 4 | 준비됨 | drive | value = 4 | 5 |
| 5 | 준비 안 됨 | stop | stopped at 5 | 시뮬레이션 종료 |
| 관찰 | 의미 |
|---|---|
value = 줄 다섯 개 | drive가 다섯 번 실행되었습니다. 같은 rule 안의 c.value는 쓰기 전의 값을 읽습니다 |
count가 5일 때 drive가 멈춤 | increment의 implicit condition이 거짓이 되어 drive 전체가 준비되지 않았습니다 |
stopped at 5 | stop이 5를 관찰하고 시뮬레이션을 끝냈습니다 |
이 예제는 서로 독립된 두 규칙을 보여 줍니다. 첫째, Top은 Tally가 공개한 이름만 쓸
수 있습니다. 둘째, 호출되는 method의 준비 조건은 패키지 경계를 넘어 호출하는 rule의
실행을 막습니다. drive 헤더에는 조건이 없지만, increment의 조건이 drive에
전달됩니다.
참고. mkTally에는 (* synthesize *)가 없으므로 인스턴스화하는 모듈 안에
인라인됩니다. 독립된 Verilog 모듈로 생성되는 것은 mkTop 하나입니다. 하드웨어 경계는
패키지가 아니라 (* synthesize *) 어트리뷰트가 정합니다.
6.4 직접 바꿔 보세요
| 바꿀 곳 | 예상 결과 |
|---|---|
Tally.bsv의 limit을 7로 | 제공된 툴체인에서는 출력이 같습니다. count가 5일 때 increment는 여전히 준비되지만 stop이 시뮬레이션을 끝냅니다 |
Tally.bsv의 limit을 3으로 | count가 3에서 멈추고 stop이 준비되지 않으므로 시뮬레이션이 끝나지 않습니다 |
export 목록에서 mkTally 삭제 | Top.bsv 컴파일에서 T0004 Unbound variable `mkTally' 오류가 발생합니다 |
확인 문제
Tally.bsv의 export 목록을 export TallyIfc, mkTally; ((..) 없이)로 바꿉니다. Top.bsv를 컴파일하면 어떻게 될까요?
limit은 비공개 상수이므로 Top.bsv를 고치지 않고도 동작 정책을 바꿀 수 있습니다.
limit을 3으로 바꾼 경우에는 stop의 조건을 c.value == 5에 묶지 않고, 카운터가
더 이상 증가하지 않는 상태를 감지하도록 바꿔야 어떤 limit에서도 시뮬레이션이
끝납니다.
7. 자주 하는 실수
7.1 파일 이름과 다른 패키지 이름
// 잘못된 코드: 파일 이름은 Tally.bsv
package Counter;
// 바른 코드
package Tally;-u 컴파일에서 S0001 File name `Tally' does not match package name `Counter'
오류가 발생합니다. 파일 이름이나 패키지 이름 중 하나를 바꿔 일치시키세요.
7.2 ::* 없는 import
// 잘못된 코드
import Tally;
// 바른 코드
import Tally::*;::*가 없으면 P0005 구문 오류가 발생하며, 메시지는 ::가 와야 한다고 알려 줍니다.
7.3 공개하지 않은 이름 쓰기
// Tally.bsv: 잘못된 코드
export TallyIfc(..); // mkTally가 빠짐
// Tally.bsv: 바른 코드
export TallyIfc(..), mkTally;Top.bsv에서 T0004 Unbound variable `mkTally' 오류가 발생합니다. 증상은 "알 수 없는
이름"이지만 원인은 모듈 문법이 아니라 공개 범위입니다. Top.bsv에 import를 하나 더
추가해도 해결되지 않습니다. Tally가 공개하지 않은 이름은 어떤 import로도 들여올 수
없습니다. 같은 이유로 비공개 상수 limit을 Top.bsv에서 읽어도 T0004가 발생합니다.
7.4 인터페이스를 (..) 없이 공개하기
// 잘못된 코드: 인터페이스가 추상 타입이 됩니다
export TallyIfc, mkTally;
// 바른 코드: method까지 공개합니다
export TallyIfc(..), mkTally;Top.bsv의 c.increment와 c.value에서 T0139 오류가 발생합니다. 메시지는
Tally::TallyIfc가 추상 타입이라 필드를 볼 수 없다고 설명합니다.
8. 정리
| 주제 | 핵심 |
|---|---|
| 패키지 선언 | 파일 하나에 패키지 하나이며, 패키지 이름과 파일 이름이 같아야 합니다 |
export | 선언이 없으면 직접 정의한 이름을 모두 공개하고, 있으면 나열한 이름만 공개합니다 |
(..) | 타입의 내부 표현까지 공개합니다. 인터페이스의 method를 쓰려면 필요합니다 |
import | import P::*; 형식만 있으며 하드웨어를 만들지 않습니다 |
| 범위 | 가장 가까운 선언이 이깁니다. 모호하면 한정 이름 P::x를 씁니다 |
| 프렐류드 | 모든 패키지에 자동으로 들어옵니다 |
| 하드웨어 경계 | 패키지가 아니라 (* synthesize *)가 정합니다 |
9. 확인 문제
-
import가 하드웨어를 만들지 않는 이유는 무엇일까요?정답 보기
import는 다른 패키지가 공개한 이름을 현재 범위에서 쓸 수 있게 할 뿐입니다. 하드웨어는mkTally같은 모듈을 인스턴스화할 때 생깁니다. -
export선언이 없던 패키지에 첫export선언을 추가하면 무엇이 달라질까요?정답 보기
이전에는 패키지가 정의한 이름이 모두 공개되었지만, 이제는
export에 나열한 이름만 공개됩니다. 나머지 이름은 패키지 밖에서 보이지 않습니다. -
drive헤더에는 조건이 없는데count가 5가 되면 실행이 멈추는 이유는 무엇일까요?정답 보기
drive가 호출하는increment에implicit conditioncount < limit이 있습니다. 이 조건이 거짓이면increment가 준비되지 않고, 그 method를 호출하는drive전체가 실행되지 않습니다. -
Top.bsv에서mkTally를 찾을 수 없다는 오류가 났습니다. 어느 파일부터 확인해야 할까요?정답 보기
Tally.bsv의export목록부터 확인합니다. 이름이 선언된 위치, 그 이름이 공개되었는지, 들여오는 쪽의import순서로 확인합니다. -
타입을
TypeName(..)대신TypeName으로만 공개하는 편이 나은 경우는 언제일까요?정답 보기
바깥에서 타입 이름만 전달하고 내부 표현에는 의존하지 못하게 하려는 경우입니다. 예를 들어 구조체의 필드 구성을 나중에 바꿔도 사용하는 쪽이 영향을 받지 않게 할 때 추상 타입으로 공개합니다.
이 장의 실습 문제는 모두 패키지가 공개한 인터페이스와 감춘 상태 사이의 계약을
다룹니다. 첫 문제에서는 value method가 비공개 레지스터 값을 바깥으로 전달하지 못해
출력이 믿을 수 없는 값이 됩니다. 두 번째 문제에서는 method의 준비 조건이 비공개 상수가
아닌 고정 숫자와 비교하고 있어, 상수를 바꿔도 동작이 바뀌지 않습니다. 세 번째 문제에서는
패키지 수준 상수가 정해지지 않아 여러 method의 관찰이 서로 맞지 않습니다.