1. 모듈 정의
모듈 정의는 제공하는 인터페이스를 이름 뒤에 적고, 그 다음에 상태 인스턴스, rule, 그리고
인터페이스가 약속한 모든 method의 정의를 나열합니다.
형식
module 모듈_이름(제공_인터페이스_타입);
// 상태 인스턴스화
// rule
// method 정의
endmodulemodule mkAccumulator(Accumulator);
Reg#(Bit#(8)) sum <- mkReg(0);
method Action add(Bit#(8) x);
sum <= sum + x;
endmethod
method Bit#(8) total = sum;
endmodule| 부분 | 역할 |
|---|---|
module mkAccumulator(Accumulator); | Accumulator 인터페이스를 제공하는 모듈 정의를 시작합니다 |
Reg#(Bit#(8)) sum <- mkReg(0); | 모듈 안의 상태입니다. 바깥에서는 직접 보이지 않습니다 |
method ... endmethod | 인터페이스가 약속한 method를 구현합니다 |
endmodule | 정의를 닫습니다 |
모듈은 제공 인터페이스의 모든 method를 정의해야 합니다. 제공 인터페이스를 생략하면
Empty를 제공한 것으로 봅니다.
2. 매개변수, 인자, provisos
모듈 헤더에는 제공 인터페이스 외에 elaboration 시점 매개변수, 모듈이 사용하는 인자, 타입
제약을 둘 수 있습니다.
형식
module 모듈_이름#(매개변수_목록)(인자_목록, 제공_인터페이스_타입)
provisos (타입_제약_목록);
...
endmodulemodule mkHolder#(t init)(Holder#(t))
provisos (Bits#(t, tw));
Reg#(t) stored <- mkReg(init);
method Action put(t x);
stored <= x;
endmethod
method t get = stored;
endmodule| 요소 | 위치 | 의미 |
|---|---|---|
| 매개변수 | #(...) 안 | elaboration할 때 정해지는 값입니다. 위 예에서는 초깃값 init입니다 |
| 인자 | 괄호 안, 제공 인터페이스 앞 | 클록, 리셋, 다른 인터페이스처럼 모듈이 사용하는 입력입니다 |
| 제공 인터페이스 | 괄호 안 마지막 | 클라이언트에 공개하는 인터페이스 타입입니다 |
provisos | 헤더 끝 | 본문이 성립하려면 타입이 만족해야 하는 조건입니다 |
Bits#(t, tw)는 "t를 비트로 표현할 수 있고 그 폭을 tw라고 부른다"는 조건입니다. 레지스터에
저장하려면 비트 표현이 있어야 하므로 이 proviso가 필요합니다. 조건은 모듈을 구체적인 타입으로
인스턴스화할 때마다 검사됩니다. proviso 전반은 타입클래스
장에서 다룹니다.
3. 인스턴스화할 때 인자 넘기기
매개변수와 인자가 있는 모듈을 짧은 형식으로 인스턴스화하면 실제 값을 한 괄호 목록에 씁니다.
매개변숫값이 먼저 오고 인자가 뒤따르며, 호출하는 쪽에는 #를 쓰지 않습니다.
Holder#(Bit#(8)) h <- mkHolder(8'd5);
SomeIfc unit <- mkSomeModule(4, sourceIfc,
clocked_by fastClock,
reset_by fastReset);모든 모듈에는 암묵적인 클록과 리셋이 있습니다. clocked_by와 reset_by는 그 인스턴스의 클록과
리셋을 바꿉니다.
참고. 인터페이스를 먼저 선언하고 다음 줄에서 모듈 인스턴스를 따로 만드는 긴 형식도
있습니다. StackIfc#(Bit#(8)) stack(); 다음에 mkStack stackModule(stack);을 쓰는 방식입니다.
SystemVerilog 호환을 위한 사용 중단 형식이므로 새 코드에는 짧은 형식을 씁니다.
4. method의 implicit condition
rule 본문이나 조건에서 호출하는 method에도 준비 조건이 있을 수 있습니다. 이 조건을
implicit condition이라고 합니다. 예를 들어 FIFO의 deq와 first는 FIFO가 비어 있으면 준비되지
않고, enq는 FIFO가 가득 차 있으면 준비되지 않습니다.
rule move; // 명시 조건은 없습니다
outq.enq(inq.first); // inq가 비어 있으면 first가 준비되지 않습니다
inq.deq;
endrulerule은 명시 조건과 본문이 호출하는 모든 method의 implicit condition이 함께 참일 때만
준비됩니다. 위 move는 조건을 적지 않았지만 inq가 비어 있거나 outq가 가득 차 있으면
실행되지 않습니다. 준비되지 않은 method 호출 하나만 건너뛰는 것이 아니라 rule 전체가 실행되지
않습니다.
참고. 일반 Reg의 읽기와 쓰기 method에는 저장된 값에 따라 달라지는 implicit condition이
없습니다. 레지스터만으로 이루어진 설계에서는 명시 조건만 보면 준비 여부를 판단할 수 있습니다.
직접 만드는 모듈에서는 method 헤더의 if (조건)이 그 method의 준비 조건입니다. 조건이 거짓인
사이클에는 method가 준비되지 않으므로, 그 method를 호출하는 rule 전체가 실행되지 않습니다.
형식
method 반환_타입 이름(인자) if (준비_조건);
...
endmethod
method 반환_타입 이름 if (준비_조건) = 식;method Action enq(Bit#(8) x) if (count < 4);
...
endmethod
method Bit#(8) first if (count > 0) = items[0];| 규칙 | 설명 |
|---|---|
| 조건이 참조할 수 있는 이름 | 모듈 안의 상태와 이름입니다 |
| 조건이 참조할 수 없는 이름 | 그 method 자신의 인자입니다. 참조하면 T0004 Unbound variable 오류가 발생합니다 |
| 조건을 생략하면 | 헤더 조건은 True입니다. 다만 호출한 method에서 준비 조건이 추가될 수 있습니다 |
| 조건의 위치 | 인터페이스 선언이 아니라 모듈의 method 정의에 둡니다 |
implicit condition은 "가득 찬 큐에 넣지 않는다", "빈 큐에서 꺼내지 않는다" 같은 불변식을 구현 쪽
한 곳에 모읍니다. 호출하는 rule은 같은 검사를 반복해서 적지 않아도 됩니다.
5. 헤더의 if와 본문의 if
세미콜론의 위치가 의미를 바꿉니다.
method Action guarded() if (ready); // 헤더의 if: 준비 조건
work;
endmethod
method Action conditional(); // 본문의 if: 일반 조건문
if (ready) work;
endmethod| method | ready가 거짓일 때 |
|---|---|
guarded | method가 준비되지 않습니다. 호출하는 rule 전체가 실행되지 않습니다 |
conditional | method는 항상 준비되어 있습니다. 호출하는 rule은 실행되고 work만 건너뜁니다 |
6. 준비, 활성화, 실행
rule이 실제로 실행되기까지는 세 단계의 판단이 있습니다.
| 단계 | 조건 |
|---|---|
| method 준비 | 호출 사슬에 있는 모든 method의 implicit condition이 참입니다 |
rule 활성화 | rule에 적은 명시 조건과 위의 준비 조건이 모두 참입니다 |
rule 실행 | 활성화된 rule이 같은 사이클에 선택된 다른 rule과 충돌하지 않습니다 |
세 단계 중 어디에서 막혀도 겉으로는 "상태가 바뀌지 않았다"로만 보입니다. 진단할 때는 method 준비, 명시 조건, 충돌을 따로 확인하세요. 충돌은 상태를 공유하는 rule 장의 주제입니다.
확인 문제
어떤 rule의 명시 조건이 참이고 호출하는 method도 모두 준비됐는데, 이번 사이클에 실행되지 않았습니다. 어느 판단에서 멈췄습니까?
7. 예제: 호출자까지 전달되는 준비 조건
ImplicitCondition.bsv의 push는 depth < 2일 때만 준비됩니다. 반면 mkTop의 rule fill에는
명시 조건이 없습니다. rule report는 b.full이 참일 때 마지막 값을 출력하고 시뮬레이션을
끝냅니다. 읽는 순서는 Bounded 인터페이스, push의 헤더 조건, fill, report입니다.
실행하기 전에 예측해 보세요. 첫 번째 pushed 줄의 top now 값은 10일까요, 0일까요?
report가 출력하는 top은 얼마일까요?
확인 문제
fill에는 명시 조건이 없습니다. pushed 줄은 몇 줄 출력됩니까?
package ImplicitCondition;
// 인터페이스는 조건 없이 메서드를 선언한다. if 절은 아래 정의에 붙는다.
interface Bounded;
method Action push(Bit#(8) x);
method Bit#(8) top;
method Bool full;
endinterface
module mkBounded(Bounded);
Reg#(Bit#(8)) stored <- mkReg(0);
Reg#(Bit#(4)) depth <- mkReg(0);
// if 절이 implicit condition이다. 조건이 거짓이면 method가 준비되지 않으며,
// 호출하는 rule도 실행되지 않는다.
method Action push(Bit#(8) x) if (depth < 2);
stored <= x;
depth <= depth + 1;
endmethod
method Bit#(8) top = stored;
method Bool full = depth == 2;
endmodule
(* synthesize *)
module mkTop(Empty);
Bounded b <- mkBounded;
Reg#(Bit#(8)) sent <- mkReg(10);
// 이 rule에는 depth를 검사하는 조건이 없다. push가 준비되지 않으면 rule도
// 실행되지 않는다.
rule fill;
b.push(sent);
$display("pushed %0d, top now %0d", sent, b.top);
sent <= sent + 1;
endrule
rule report (b.full);
$display("full, top = %0d", b.top);
$finish(0);
endrule
endmodule
endpackage이 개념 실습하기
다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.
결과 확인.
| 사이클 | 시작할 때 depth | push 준비 | 실행되는 rule | 출력 |
|---|---|---|---|---|
| 0 | 0 | 참 | fill | 10을 넣고, top은 이전 값 0을 읽습니다 |
| 1 | 1 | 참 | fill | 11을 넣고, top은 10을 읽습니다 |
| 2 | 2 | 거짓 | report | fill은 비활성화되고 report가 11을 출력합니다 |
pushed 줄이 두 개뿐이라는 사실이 결정적인 증거입니다. fill에는 depth를 검사하는 코드가
없지만, push가 준비되지 않은 사이클에는 fill 전체가 실행되지 않습니다. 같은 줄의 top now가
넣은 값보다 한 단계 늦은 이유는 top이 사이클 시작 상태를 읽기 때문입니다.
직접 바꿔 보세요.
push의 조건만depth < 3으로 바꿔 보세요.depth == 2에서도push는 준비되지만,full은 이미 참이므로report가 시뮬레이션을 끝냅니다. 준비 조건과 상태 method가 서로 다른 한계를 나타내므로 이 변경만으로는 일관된 3칸 용량을 구현하지 못합니다.- 이번에는
push의 조건을depth < 3으로,full을depth == 3으로 함께 바꿔 보세요.pushed 12, top now 11줄이 추가되고 마지막 줄은top = 12가 됩니다. 용량을 바꿀 때는 준비 조건과 상태 method가 같은 한계를 표현해야 합니다.
8. 종합 예제: 스택
8.1 코드 읽기
Stack.bsv는 원소 타입을 고정하지 않은 4칸 스택을 정의합니다. 다음 순서로 읽으세요.
StackIfc#(type element_type)의 네 method와 각 method의 종류mkStack의provisos (Bits#(element_type, element_width))- 상태
items(벡터 4칸)와depth push,pop,top의 준비 조건
package Stack;
import Vector::*;
export StackIfc(..), mkStack;
// 인터페이스는 타입이다. 이름 붙은 메서드의 모음이다.
// 매개변수로 하드웨어에 저장할 수 있는 원소 타입을 지정한다.
interface StackIfc#(type element_type);
method Action push(element_type x);
method ActionValue#(element_type) pop;
method element_type top;
method Bool notEmpty;
endinterface
// 모듈도 같은 방식으로 매개변수를 받는다. proviso는 본문에 필요한
// 타입 제약을 명시한다. 여기서는 원소를 비트로 표현할 수 있어야 한다.
module mkStack(StackIfc#(element_type))
provisos (Bits#(element_type, element_width));
Reg#(Vector#(4, element_type)) items <- mkRegU;
Reg#(Bit#(3)) depth <- mkReg(0);
// implicit condition. 가득 찬 스택에는 push할 수 없다.
// push가 준비되지 않으면 호출하는 rule도 실행되지 않는다.
method Action push(element_type x) if (depth < 4);
items <= shiftInAt0(items, x);
depth <= depth + 1;
endmethod
// ActionValue method는 상태를 바꾸면서 값을 반환한다.
// 인덱스 0 쪽으로 밀어야 다음 원소가 새 top이 된다.
// 깊이만 줄이면 top이 계속 같은 값을 반환한다.
method ActionValue#(element_type) pop if (depth > 0);
items <= shiftInAtN(items, ?);
depth <= depth - 1;
return items[0];
endmethod
method element_type top if (depth > 0) = items[0];
method Bool notEmpty = depth > 0;
endmodule
endpackageTop.bsv는 Bit#(8) 원소를 쓰는 스택을 인스턴스화합니다. fill이 세 번 넣고, drain이 비어 있지
않은 동안 꺼내며, done이 빈 스택을 확인하고 끝냅니다.
package Top;
import Stack::*;
import Vector::*;
(* synthesize *)
module mkTop(Empty);
// <-로 모듈을 인스턴스화하고 인터페이스를 이름에 연결한다.
// 원소 타입은 스택이 쓰이는 방식에서 추론된다.
StackIfc#(Bit#(8)) s <- mkStack;
Reg#(Bit#(8)) step <- mkReg(0);
rule fill (step < 3);
s.push(step + 10);
step <= step + 1;
$display("pushed %0d", step + 10);
endrule
rule drain (step == 3 && s.notEmpty);
// <-는 ActionValue method를 실행하고 반환값을 이름에 결합한다.
// 모듈 인스턴스화와 같은 기호다.
let v <- s.pop;
$display("popped %0d, top was %0d", v, s.top);
endrule
rule done (step == 3 && !s.notEmpty);
$display("empty");
$finish(0);
endrule
endmodule
endpackage8.2 사이클 추적
실행하기 전에 예측해 보세요. fill이 끝났을 때 items[0]과 depth는 무엇일까요? 첫 번째
popped 줄의 v와 top was 값은 같을까요?
확인 문제
pop 세 번은 어떤 순서로 값을 꺼냅니까?
결과 확인.
| 단계 | rule | items[0] 변화 | depth 변화 | 출력에서 확인할 내용 |
|---|---|---|---|---|
| 넣기 1–3 | fill | 10 → 11 → 12 | 0 → 3 | pushed 10, 11, 12 |
| 꺼내기 1 | drain | 12 → 11 | 3 → 2 | pop과 top이 같은 이전 상태의 12를 읽습니다 |
| 꺼내기 2 | drain | 11 → 10 | 2 → 1 | 11 |
| 꺼내기 3 | drain | 10 → 정해지지 않은 값 | 1 → 0 | 10 |
| 끝 | done | notEmpty가 거짓이 되어 empty가 출력됩니다 |
shiftInAt0은 새 원소를 index 0에 넣고 나머지를 한 칸씩 밀어 냅니다. shiftInAtN은 반대
방향으로 밀어 index 0의 원소를 버립니다. pop이 반환하는 값과 같은 rule에서 읽은 top이 같은
이유는 두 호출 모두 사이클 시작 상태를 읽기 때문입니다.
직접 바꿔 보세요.
pop에서items <= shiftInAtN(items, ?);줄을 지워 보세요. 컴파일은 그대로 성공하지만popped 12, top was 12가 세 번 출력됩니다.depth만 줄고 index 0이 바뀌지 않기 때문입니다. 타입 검사는 이런 상태 전이 불변식을 검증하지 않습니다.- 스택 용량을 바꾸려면 벡터 길이,
depth의 폭,push의 비교식을 함께 바꿔야 합니다. 세 곳이 같은 한계를 표현하지 않으면 스택이 넘치거나 일찍 막힙니다.
9. 모듈 합성
9.1 합성 경계 지정하기
Verilog 모듈로 따로 생성할 BSV 모듈은 (* synthesize *) 어트리뷰트나 bsc의 -g 모듈_이름
옵션으로 지정합니다. 이 장의 예제는 모두 mkTop에 (* synthesize *)를 붙이고 -g mkTop으로
컴파일합니다.
분리 합성하는 모듈의 인터페이스는 모든 타입이 구체적이어야 합니다. 원소 타입이 정해지지 않은
mkStack은 그대로 합성 경계가 될 수 없으므로, 구체적인 타입으로 감싼 래퍼를 만듭니다.
(* synthesize *)
module mkStack8(StackIfc#(Bit#(8)));
StackIfc#(Bit#(8)) stack <- mkStack;
return stack;
endmodulereturn stack;은 하위 인스턴스의 인터페이스를 그대로 이 모듈의 제공 인터페이스로 반환합니다.
9.2 인터페이스가 배선으로 바뀌는 조건
합성 경계의 인터페이스는 포트로 변환됩니다. 따라서 다음 조건을 만족해야 합니다.
| 항목 | 조건 |
|---|---|
| method 인자 | Bits 인스턴스가 있는 타입입니다 |
value method와 ActionValue method의 반환 타입 | Bits 인스턴스가 있는 타입입니다 |
| 하위 인터페이스 | 같은 조건을 재귀적으로 만족합니다 |
| 그 밖에 허용되는 타입 | Clock, Reset, Inout과 이들 또는 Bits 타입의 벡터입니다 |
주의. 합성 경계는 스케줄링에도 영향을 줍니다. method 인자가 물리 입력 포트 하나가 되면, 서로
다른 인잣값으로 같은 method를 부르는 두 호출이 같은 사이클에 충돌할 수 있습니다. 같은 모듈을
인라인하면 bsc가 두 호출을 함께 실행할 수 있는 경우도 있습니다.
9.3 다른 모듈 타입
보통 모듈의 타입은 Module#(ifc)이며, 분리 합성이 가능한 모듈 타입은 이것뿐입니다. 특별한 모듈
타입이 필요할 때는 module 뒤 대괄호에 적습니다.
module [Module] mkOrdinary(Empty);
module [ModuleCollect#(item_type)] mkCollector(Empty);ModuleCollect는 elaboration 중에 상태와 rule 외의 항목을 모으는 고급 기법입니다. 대부분의
설계에는 기본 Module만 필요합니다.
10. 자주 하는 실수
10.1 준비 조건을 method 본문의 if로 쓰기
// 잘못된 코드: push는 항상 준비되어 있습니다
method Action push(Bit#(8) x);
if (depth < 2) begin
stored <= x;
depth <= depth + 1;
end
endmethod
// 바른 코드: 한계에서 호출자가 멈춥니다
method Action push(Bit#(8) x) if (depth < 2);
stored <= x;
depth <= depth + 1;
endmethod본문의 if는 method를 막지 못하므로 호출하는 rule은 계속 실행됩니다. 7절의 예제를 이렇게
바꾸고 report의 조건을 b.full && sent == 13으로 늘려 실행하면, depth가 2인 사이클에도
fill이 실행되어 pushed 12, top now 11이 출력됩니다. 12는 저장되지 않았는데 fill의 출력과
sent 증가는 일어났습니다.
10.2 준비 조건에서 method 인자 참조하기
// 잘못된 코드: 준비 조건은 인자를 볼 수 없습니다
method Action push(Bit#(8) x) if (x < 100);
// 바른 코드: 인자 검사는 본문에서 합니다
method Action push(Bit#(8) x) if (depth < 2);
if (x < 100) stored <= x;
endmethod준비 조건은 호출자가 인자를 정하기 전에 "지금 호출할 수 있는가"를 알려 주는 신호입니다. 그래서
인자를 참조할 수 없고, 참조하면 T0004 오류 Unbound variable `x'가 발생합니다.
10.3 method의 준비 조건을 호출자에게 복사하기
// 잘못된 코드: push의 조건을 빼고 호출자가 대신 검사합니다
method Action push(Bit#(8) x);
...
endmethod
rule fill (depth < 2);
b.push(sent);
endrule
// 바른 코드: 조건은 method에 둡니다
method Action push(Bit#(8) x) if (depth < 2);
...
endmethod
rule fill;
b.push(sent);
endrule호출자의 조건은 그 호출자만 보호합니다. 같은 method를 부르는 다른 rule이 생기면 한계를 넘어서
호출할 수 있습니다. 명시 조건이 참인데 rule이 실행되지 않으면 호출 경로에 있는 method의 준비
조건부터 확인하세요.
11. 정리
| 주제 | 핵심 |
|---|---|
| 모듈 정의 | 인터페이스 하나를 제공하고 그 모든 method를 정의합니다. 인터페이스를 생략하면 Empty입니다 |
| 헤더 | #(매개변수)는 elaboration에서 정해지고, 인자는 인터페이스 앞에 오며, provisos는 타입 조건입니다 |
| 인스턴스화 | 짧은 형식은 매개변숫값 다음에 인자를 한 목록에 씁니다. clocked_by와 reset_by는 암묵적 클록과 리셋을 바꿉니다 |
rule | 명시 조건과 호출한 method의 모든 implicit condition이 참일 때 준비됩니다 |
implicit condition | method 헤더의 if입니다. 거짓이면 호출하는 rule 전체가 실행되지 않습니다. 모듈 상태는 참조할 수 있고 method 인자는 참조할 수 없습니다 |
헤더의 if와 본문의 if | 실행을 보류하는 준비 조건과, 실행은 되되 안에서 갈라지는 분기 |
| 준비, 활성화, 실행 | 세 가지 별개의 검사입니다. 어느 하나가 실패해도 "바뀌지 않았다"로 보입니다 |
| 합성 경계 | (* synthesize *) 또는 -g. 인터페이스 타입이 구체적이고 비트로 변환 가능해야 합니다 |
12. 확인 문제
-
FIFO
inq의first와deq를 호출하는rule에 명시 조건이 없습니다.inq가 비어 있으면 이rule은 어떻게 될까요?정답 보기
first와deq의implicit condition이 거짓이므로rule전체가 준비되지 않아 실행되지 않습니다. 호출 하나만 건너뛰는 일은 없습니다. -
명시 조건이 없는
rule이 어느 순간부터 실행되지 않습니다. 가장 먼저 확인할 곳은 어디일까요?정답 보기
rule이 호출하는 method의implicit condition입니다. 호출 사슬의 어떤 method가 준비되지 않으면rule전체가 비활성화됩니다. -
원소 타입이 매개변수인
mkStack에(* synthesize *)를 직접 붙일 수 없는 이유는 무엇일까요?정답 보기
합성 경계의 인터페이스는 포트로 변환되므로 모든 타입과 폭이 정해져 있어야 합니다.
StackIfc#(Bit#(8))처럼 구체적인 타입을 제공하는 래퍼를 만들어 합성합니다.
아래 실습 문제에서는 호출자가 한계에 도달해도 멈추지 않습니다. method 종류, 읽기 시점, 준비 조건 중 어느 계약이 깨졌는지 먼저 판단해 보세요.