1. 가장 작은 BSV 프로그램부터 실행합니다
문법을 배우기 전에 먼저 실행해 봅니다. 완결된 가장 작은 BSV 설계에는 앞으로 모든 장에서 보게 될 세 부분이 있습니다.
| 부분 | 뜻 |
|---|---|
package | 파일입니다. 파일 하나에 패키지 하나이며, 이름은 파일 이름과 같습니다 |
module | 하드웨어 한 덩어리입니다. mkTop이 설계의 최상위입니다 |
rule | 하드웨어가 한 클록 사이클에 하는 일입니다. 조건이 없는 rule은 매 사이클 준비됩니다 |
FirstRule.bsv에는 rule이 greet 하나뿐이며, 한 줄을 출력하고 시뮬레이션을 끝냅니다.
실행을 누르기 전에 rule을 읽고 무엇이 출력될지 정해 보세요.
확인 문제
출력에 Hello, BSV는 몇 번 나타납니까?
package FirstRule;
(* synthesize *)
module mkTop(Empty);
// 조건이 없는 rule: 매 클록 사이클 준비된다.
rule greet;
$display("Hello, BSV");
$finish(0);
endrule
endmodule
endpackage실행을 누르세요. 처음 두 줄은 컴파일러가 출력합니다. Verilog file created: mkTop.v는 bsc가
BSV 모듈을 Verilog 모듈로 바꾸었다는 뜻이고, Verilog binary file created: sim.out은 그것으로
시뮬레이터를 만들었다는 뜻입니다. 설계가 출력한 것은 마지막 줄 Hello, BSV뿐입니다.
구조와 시뮬레이션. 서로 다른 두 가지 일이 일어났습니다. 먼저 bsc가 하드웨어를 만들었습니다.
모듈 mkTop이 생기고, 그 안의 rule greet에는 매 사이클 참인 enable 신호가 생깁니다. 그다음
시뮬레이터가 그 하드웨어를 클록마다 실행했습니다. $display와 $finish는 시뮬레이션에만 있는
시스템 태스크입니다. 칩으로 만들면 rule은 여전히 매 사이클 실행되지만, 아무것도 출력되지 않고
멈추지도 않습니다.
이제 프로그램을 바꿔 보세요. 실행하기 전에 결과를 먼저 예측합니다.
| 수정 | 예측할 것 |
|---|---|
문자열 "Hello, BSV"를 다른 문장으로 바꾸기 | 출력된 줄만 바뀝니다 |
$finish(0); 바로 앞 줄에 $display("still the same cycle"); 추가하기 | 몇 줄이 어떤 순서로 출력되는지 |
확인 문제
$finish(0) 앞에 두 번째 $display를 추가하면 설계는 무엇을 출력합니까?
확인 문제
실행하지 말고 예측해 보세요. $finish(0); 줄을 지우면 어떻게 됩니까?
2. BSV 파일 하나는 모듈을 담은 패키지입니다
BSV 소스 파일은 package 하나이고, 패키지 이름은 파일 이름과 같습니다. ValueMethod.bsv는
package ValueMethod;로 시작해 endpackage로 끝납니다. 패키지 안에서 하드웨어의 단위는
module입니다. 모듈 이름은 관례상 mk(make)로 시작합니다. 모듈 정의는 하드웨어를 만드는
방법이고, 누군가 인스턴스를 만들 때 비로소 하드웨어가 생기기 때문입니다.
형식
package 패키지_이름;
(* synthesize *)
module mk모듈_이름(인터페이스_타입);
// 상태 선언
// rule 정의
endmodule
endpackage괄호 안의 타입은 모듈이 바깥에 제공하는 인터페이스입니다. FirstRule.bsv의 각 줄은 다음과
같은 뜻입니다.
| 코드 | 의미 |
|---|---|
package FirstRule; … endpackage | 패키지를 열고 닫습니다. 파일 이름은 FirstRule.bsv여야 합니다 |
(* synthesize *) | 이 모듈을 독립된 Verilog 모듈로 생성하라는 어트리뷰트입니다 |
module mkTop(Empty); | 모듈 mkTop을 시작합니다. 괄호 안은 바깥에 제공하는 인터페이스 타입입니다 |
Empty | method가 하나도 없는 인터페이스입니다. 시뮬레이션만 이끄는 최상위 모듈에 씁니다 |
rule greet; … endrule | 조건 없는 rule입니다. 매 사이클 실행될 수 있습니다 |
$display, $finish(0) | 시뮬레이션에서 출력하고 시뮬레이션을 끝내는 시스템 태스크입니다 |
$로 시작하는 시스템 태스크는 시뮬레이션을 위한 구문입니다. bsc는 생성한 Verilog에서
이를 translate_off 표시 사이에 두어 합성이 건너뛰게 하므로, 시스템 태스크는 하드웨어가
되지 않습니다. 모든 예제의 최상위 모듈에는 (* synthesize *)가 붙어 있고, 실행 명령은
-g mkTop으로 그 모듈을 지정합니다. import와 export를 포함한 패키지 문법 전체는
패키지 장에서 다룹니다.
실행은 세 단계를 차례로 수행합니다. bsc -verilog -g mkTop이 BSV 소스를 Verilog로
컴파일하고, bsc -e mkTop -vsim iverilog가 Icarus Verilog로 시뮬레이션 실행 파일을 만들며,
vvp가 그 파일을 실행합니다.
3. 인터페이스는 모듈이 제공하는 것을 선언합니다
3.1 인터페이스 선언 형식
Verilog는 모듈의 바깥 모습을 포트로 적지만, BSV는 method로 적습니다. interface는 method
원형을 모은 타입입니다. 이름, 인자 타입, 결과 타입만 있고 모듈 안의 동작은 없습니다.
패키지 최상위에서 선언하며, 여러 모듈이 같은 인터페이스를 제공할 수 있습니다.
형식
interface 인터페이스_이름;
method 반환_타입 method_이름(인자_목록);
...
endinterfaceinterface Counter;
method Bit#(8) value;
endinterface
interface Accumulator;
method Action add(Bit#(8) x);
method Bit#(8) total;
endinterface| 코드 | 의미 |
|---|---|
interface Counter; | Counter라는 인터페이스 타입을 선언합니다. 타입 이름은 대문자로 시작합니다 |
method Bit#(8) value; | 인자가 없고 8비트 값을 반환하는 method입니다. 인자가 없으면 괄호를 생략할 수 있습니다 |
method Action add(Bit#(8) x); | 8비트 인자를 받아 상태를 바꾸는 method입니다 |
endinterface | 선언을 닫습니다 |
인터페이스에는 method의 이름과 타입만 적습니다. 구현과 method가 가질 수 있는 준비 조건은 모듈 안의 method 정의에 둡니다.
3.2 먼저 value method부터
method의 종류는 반환 타입으로 정해집니다. 이 장에서는 한 종류만 있으면 됩니다. Bit#(8)이나
Bool처럼 값을 반환하는 method가 **value method**입니다. 호출하면 모듈의 상태를 읽어 결과를
반환할 뿐 아무것도 바꾸지 않으므로, 두 번 읽어도 한 번 읽은 것과 같습니다.
Counter의 value는 Bit#(8)을 반환하므로 value method입니다. 반환 타입이 Action이나
ActionValue#(t)인 method는 상태를 바꿉니다. 위 Accumulator의 add가 그렇습니다. 두 종류가
무엇을 약속하는지, rule이 어떻게 호출하는지, 주어진 계약에 맞춰 세 종류 가운데 무엇을 고르는지는
레지스터와 rule을 직접 써 본 뒤 method:
value·Action·ActionValue 장에서 다룹니다.
4. 상태는 모듈 인스턴스로만 생깁니다
4.1 인스턴스화는 인터페이스를 이름에 결합합니다
BSV에서 사이클 사이에 값을 기억하는 상태는 모듈을 인스턴스화할 때만 생깁니다. 레지스터, FIFO, 메모리, 직접 만든 큰 컴포넌트가 모두 같은 형식으로 만들어집니다.
형식
인터페이스_타입 인스턴스_이름 <- 모듈_이름(인자);Reg#(Bit#(8)) count <- mkReg(42);
FIFO#(Bit#(8)) queue <- mkFIFO;| 코드 | 의미 |
|---|---|
Reg#(Bit#(8)) | 인스턴스가 제공하는 인터페이스 타입입니다. 8비트 레지스터 인터페이스입니다 |
count | 그 인터페이스에 붙인 이름입니다. 이후 count로 method를 호출합니다 |
<- | 모듈을 실행해 인스턴스를 만들고, 결과 인터페이스를 이름에 결합합니다 |
mkReg(42) | 인스턴스화할 모듈과 인자입니다. 42에서 시작하는 레지스터를 만듭니다 |
인자가 없는 모듈은 mkFIFO처럼 괄호를 생략할 수 있습니다. mkReg(42) 자체가 레지스터
모듈이고, Reg#(Bit#(8))은 그 모듈이 제공하는 인터페이스입니다.
참고. 일반 변수는 상태가 아닙니다. 지역 변수, 함수 인자, 패턴이 도입한 이름, 반복문
변수는 한 계산 안에서만 쓰이는 이름입니다. let x = queue.first;의 x는 새 저장 소자를
만들지 않고 queue가 지금 보여 주는 값을 가리킬 뿐입니다.
4.2 정의와 인스턴스
모듈 정의는 하드웨어를 만드는 방법을 적은 명세이고, 모듈 인스턴스는 그 명세로 만든 실제 하드웨어입니다. 같은 정의를 여러 번 인스턴스화하면 인스턴스마다 상태가 따로 생깁니다.
Reg#(Bit#(8)) a <- mkReg(0);
Reg#(Bit#(8)) b <- mkReg(0); // a와 별개인 두 번째 레지스터인스턴스는 엄격한 계층을 이룹니다. 최상위 모듈을 제외한 모든 인스턴스에는 부모가 정확히
하나 있습니다. 모듈 정의 이름의 mk는 명세와 인스턴스를 구분하는 표시입니다.
4.3 모듈은 상태를 갖고 method를 정의합니다
mkCounter는 Counter를 제공합니다. 본문은 두 줄이고, 두 줄은 서로 다른 종류입니다.
| 줄 | 종류 |
|---|---|
Reg#(Bit#(8)) count <- mkReg(42); | 상태. 8비트를 저장하는 레지스터이며 초깃값은 42입니다 |
method Bit#(8) value = count; | 배선. method의 결과는 레지스터의 현재 값입니다 |
식 안에서 count를 쓰면 현재 사이클이 시작할 때 레지스터가 가진 값을 읽습니다. 이 설계에는
count에 쓰는 곳이 없으므로 그 값은 매 사이클 42입니다. 레지스터에 쓰는 방법과 쓰는 시점을
정하는 rule은 레지스터와 rule 하나 장에서
다룹니다.
5. 테스트벤치가 모듈을 만들고 method를 읽습니다
모듈은 혼자서는 동작하지 않습니다. mkTop이 테스트벤치입니다. Counter c <- mkCounter;로
인스턴스를 만들고 rule 하나를 둡니다. rule은 한 클록 사이클에 실행될 수 있는 동작
묶음입니다. 이 rule에는 조건이 없으므로 첫 사이클부터 준비됩니다. 본문은 c.value를
읽고(인스턴스.method 표기), $display로 출력한 뒤 $finish(0)으로 시뮬레이션을 끝냅니다.
ValueMethod.bsv는 value method 하나만 가진 가장 작은 모듈입니다. 코드를 읽을 때는 다음
순서로 보세요.
Counter인터페이스에는value하나만 있습니다.mkCounter의 레지스터count는 42로 시작하고, 이 값을 쓰는 곳이 없습니다.mkTop은<-로mkCounter를 인스턴스화하고,rule show에서c.value를 읽습니다.
실행 전에 예측하세요.
확인 문제
value = 줄에 출력되는 수는 무엇입니까?
확인 문제
시뮬레이션이 끝날 때까지 count의 상태 전이는 몇 번 일어납니까?
package ValueMethod;
// 인터페이스는 모듈이 제공하는 것을 선언한다. value method는 상태를 읽어 결과를
// 반환하며 아무것도 변경하지 않는다.
interface Counter;
method Bit#(8) value;
endinterface
module mkCounter(Counter);
Reg#(Bit#(8)) count <- mkReg(42);
method Bit#(8) value = count;
endmodule
(* synthesize *)
module mkTop(Empty);
// 모듈은 <-로 인스턴스화하며, 그 결과가 인터페이스다.
Counter c <- mkCounter;
rule show;
$display("value = %0d", c.value);
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
실행을 누르세요. 실행 패널은 bsc로 패키지를 Verilog로 컴파일하고, Icarus Verilog로
시뮬레이터를 링크해 실행합니다.
| 출력 줄 | 확인할 내용 |
|---|---|
| 컴파일 메시지 | mkTop.v와 sim.out이 만들어졌다는 줄은 bsc의 컴파일과 링크가 성공했다는 뜻입니다 |
value = 42 | 레지스터 초깃값이 그대로 보입니다. c.value는 상태를 읽는 식일 뿐이므로 상태 전이는 0번입니다. 테스트벤치는 초깃값을 관찰하고 멈췄습니다 |
주의. 출력이 나왔다는 사실은 bsc 2026.01과 Icarus Verilog 11에서 컴파일과 시뮬레이션이
되었다는 뜻입니다. 이 과정에서 합성은 실행하지 않습니다. (* synthesize *)는 어느 모듈을
별도 Verilog 모듈로 만들지 정할 뿐, 설계가 칩으로 합성된다는 증명이 아닙니다.
6. 한 곳만 바꿔 보세요
실행 패널의 편집기에서 다음 두 수정을 하나씩 적용하고, 수정마다 다시 실행하세요.
| 수정 | 예상 |
|---|---|
mkReg(42)를 mkReg(7)로 | 출력값만 7로 바뀝니다. 구조는 그대로이고 상태 전이 횟수도 0번입니다 |
Counter c <- mkCounter;를 Counter c = mkCounter;로 | 컴파일 오류 T0020이 발생합니다. 기대 타입은 Counter인데 mkCounter는 인터페이스를 만드는 모듈 계산입니다 |
확인 문제
두 번째 수정의 잘못은 어느 시점에 드러납니까?
두 번째 수정은 자주 하는 실수이므로 컴파일러의 메시지를 그대로 읽어 둘 만합니다.
// 잘못된 코드
Counter c = mkCounter;
// 바른 코드
Counter c <- mkCounter;mkCounter는 인터페이스 값이 아니라 인터페이스를 만드는 모듈 계산입니다. =를 쓰면 T0020
오류에서 기대한 타입 Counter와 추론한 타입 a__#(Counter)가 다르다고 보고됩니다.
a__#(Counter)는 실행하면 Counter를 반환하는 모듈의 타입입니다. 그 실행은 <-만 합니다.
<-는 뒤에서 값을 반환하는 동작을 실행할 때 한 번 더 나옵니다. 두 용법의 뜻은 하나입니다.
무엇인가를 실행하고 그 결과를 이름에 결합합니다.
7. 타입과 elaboration
비트 폭은 타입의 일부입니다. Reg#(Bit#(8))은 8비트 값을 저장하는 레지스터 인터페이스이고,
폭이 맞지 않는 대입은 하드웨어가 생성되기 전에 타입 검사에서 검출됩니다.
타입·폭·명시적 변환 장에서 자세히 다룹니다.
컴파일은 두 시점을 구분합니다.
| 시점 | 정해지는 것 |
|---|---|
elaboration | 최상위 모듈부터 하위 모듈을 인스턴스화해 고정된 하드웨어 구조를 만듭니다 |
| 실행 | 만들어진 하드웨어에서 매 사이클 rule이 준비되고 상태가 바뀝니다 |
<-는 첫 시점에 속합니다. Counter c <- mkCounter;와 mkReg(42)는 구조를 만드는 동안 한
번 실행됩니다. rule이 실행되어도 새 모듈이 생기지 않습니다. 하드웨어 구조는
elaboration에서 고정됩니다.
참고. 모든 패키지에는 표준 프렐류드가 자동으로 들어오며 Reg, Bool, Empty 같은
기본 정의를 제공합니다. FIFO, 벡터, 메모리 같은 구성 요소는 bsc 라이브러리 패키지를
import해서 씁니다. BSV와 BH(Bluespec Haskell, Bluespec Classic)는 문법이 다르지만
의미론이 같으며, bsc는 .bsv와 .bs 파일을 한 설계에서 함께 처리합니다.
참고. rule의 의미는 Term Rewriting Systems에 기반한 형식 의미론으로 정의됩니다.
"준비된 rule 하나를 골라 원자적으로 실행한다"는 순차 모델에서 증명한 불변식은 생성된
병렬 하드웨어에서도 성립합니다. 정적·동적
의미론 장에서 이 모델을 정확히 다룹니다.
8. 적용: value method는 상태를 그대로 보고해야 합니다
아래 실습 문제의 mkCounter에는 레지스터에 쓰는 두 번째 method set이 있고, value는
count + 1로 정의되어 있습니다. 테스트벤치가 8'h2a를 넣으면 42가 아니라 43이 읽힙니다.
value method는 상태를 바꾸지 않고 관찰하며, 상태로부터 계산한 값을 반환할 수도 있습니다.
이 문제에서는 저장된 카운트 자체를 반환해야 합니다. 표현 가능한 모든 값이 그대로 보고되도록
value를 고치고 실행하세요.
이 장의 체크포인트는 예제를 실행해 출력 값을 예측과 비교하는 것입니다. 실습 문제는 이 과정의 첫 연습입니다.
9. 정리
| 요소 | 뜻 |
|---|---|
package … endpackage | 파일 하나, 패키지 하나, 같은 이름 |
interface … endinterface | method 원형을 모은 타입. 구현은 모듈에 둡니다 |
rule 이름; … endrule | 한 클록 사이클에 하는 일. 명시 조건이 없으면 method의 준비 조건과 스케줄이 허용하는 한 매 사이클 실행될 수 있습니다 |
value method | 값 타입을 반환하는 method. 관찰만 합니다. Action·ActionValue method는 뒤 장에서 다룹니다 |
module mk이름(Ifc); … endmodule | 인터페이스 Ifc를 제공하는 모듈 정의. 명세이며 하드웨어가 아닙니다 |
Reg#(t) r <- mkReg(초깃값); | t 값을 저장하는 레지스터 인스턴스. 상태는 인스턴스로만 생깁니다 |
method t 이름 = 식; | value method. 읽기만 하고 쓰지 않습니다 |
Ifc x <- mk이름; | <-로 만든 인스턴스. x.method로 사용합니다. =는 실행하지 않고 결합하므로 T0020 오류입니다 |
(* synthesize *) | 이 모듈을 독립된 Verilog 모듈로 생성 |
elaboration과 실행 | 인스턴스는 구조를 고정할 때 한 번 만들어지고, rule은 그 뒤 사이클마다 실행됩니다 |
10. 확인 문제
-
같은 모듈 정의를 두 번 인스턴스화하면 두 인스턴스의 상태가 분리되는 이유는 무엇일까요?
정답 보기
모듈 정의는 하드웨어를 만드는 명세일 뿐이고, 상태는
<-로 인스턴스화할 때마다 새로 생깁니다. 인스턴스마다 자신의 레지스터를 가집니다. -
elaboration에서 일어나는 일과 매 사이클 일어나는 일을 하나씩 들어 보세요.정답 보기
elaboration에서는mkReg,mkCounter같은 모듈이 인스턴스화되어 하드웨어 구조가 고정됩니다. 매 사이클에는rule의 준비 여부가 판단되고, 선택된rule의 레지스터 쓰기가 클록 경계에서 반영됩니다.