본문으로 건너뛰기
code0.HDL

준비 조건·모듈 인자·합성 경계

매개변수·인자·proviso가 있는 모듈을 정의하고, method의 준비 조건을 헤더에 두어 조건이 거짓이면 호출하는 모든 rule이 멈추게 하며, 스택을 push와 pop으로 추적하고, 합성 경계에서 인터페이스가 지켜야 할 조건을 확인합니다.

이 장에서 배울 내용

  • 매개변수·인자·제공 인터페이스·proviso가 있는 모듈 헤더를 쓰고 짧은 형식으로 인스턴스화할 수 있습니다
  • method 헤더에 준비 조건을 두고, 자기 guard가 없는 호출 rule이 언제 멈추는지 예측할 수 있습니다
  • rule과 상태 변경 사이의 세 판단, 즉 method 준비·rule 활성화·rule 실행을 구분할 수 있습니다
  • 인터페이스가 분리 합성되는 모듈의 포트가 될 수 있는지 판정할 수 있습니다
이 장에서 배우는 개념
  • 준비 조건·모듈 인자·합성 경계
먼저 알아야 할 개념

1. 모듈 정의

모듈 정의는 제공하는 인터페이스를 이름 뒤에 적고, 그 다음에 상태 인스턴스, rule, 그리고 인터페이스가 약속한 모든 method의 정의를 나열합니다.

형식

module 모듈_이름(제공_인터페이스_타입);
  // 상태 인스턴스화
  // rule
  // method 정의
endmodule
module 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 (타입_제약_목록);
  ...
endmodule
module 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;
endrule

rule은 명시 조건과 본문이 호출하는 모든 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
methodready가 거짓일 때
guardedmethod가 준비되지 않습니다. 호출하는 rule 전체가 실행되지 않습니다
conditionalmethod는 항상 준비되어 있습니다. 호출하는 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 줄은 몇 줄 출력됩니까?

ImplicitCondition.bsv
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 문제에 이 개념을 적용해 보세요.

결과 확인.

사이클시작할 때 depthpush 준비실행되는 rule출력
00참fill10을 넣고, top은 이전 값 0을 읽습니다
11참fill11을 넣고, top은 10을 읽습니다
22거짓reportfill은 비활성화되고 report가 11을 출력합니다

pushed 줄이 두 개뿐이라는 사실이 결정적인 증거입니다. fill에는 depth를 검사하는 코드가 없지만, push가 준비되지 않은 사이클에는 fill 전체가 실행되지 않습니다. 같은 줄의 top now가 넣은 값보다 한 단계 늦은 이유는 top이 사이클 시작 상태를 읽기 때문입니다.

직접 바꿔 보세요.

  1. push의 조건만 depth < 3으로 바꿔 보세요. depth == 2에서도 push는 준비되지만, full은 이미 참이므로 report가 시뮬레이션을 끝냅니다. 준비 조건과 상태 method가 서로 다른 한계를 나타내므로 이 변경만으로는 일관된 3칸 용량을 구현하지 못합니다.
  2. 이번에는 push의 조건을 depth < 3으로, full을 depth == 3으로 함께 바꿔 보세요. pushed 12, top now 11 줄이 추가되고 마지막 줄은 top = 12가 됩니다. 용량을 바꿀 때는 준비 조건과 상태 method가 같은 한계를 표현해야 합니다.

8. 종합 예제: 스택

8.1 코드 읽기

Stack.bsv는 원소 타입을 고정하지 않은 4칸 스택을 정의합니다. 다음 순서로 읽으세요.

  1. StackIfc#(type element_type)의 네 method와 각 method의 종류
  2. mkStack의 provisos (Bits#(element_type, element_width))
  3. 상태 items(벡터 4칸)와 depth
  4. push, pop, top의 준비 조건
Stack.bsv
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

endpackage

Top.bsv는 Bit#(8) 원소를 쓰는 스택을 인스턴스화합니다. fill이 세 번 넣고, drain이 비어 있지 않은 동안 꺼내며, done이 빈 스택을 확인하고 끝냅니다.

Top.bsv
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

endpackage

8.2 사이클 추적

실행하기 전에 예측해 보세요. fill이 끝났을 때 items[0]과 depth는 무엇일까요? 첫 번째 popped 줄의 v와 top was 값은 같을까요?

확인 문제

pop 세 번은 어떤 순서로 값을 꺼냅니까?

답

결과 확인.

단계ruleitems[0] 변화depth 변화출력에서 확인할 내용
넣기 1–3fill10 → 11 → 120 → 3pushed 10, 11, 12
꺼내기 1drain12 → 113 → 2pop과 top이 같은 이전 상태의 12를 읽습니다
꺼내기 2drain11 → 102 → 111
꺼내기 3drain10 → 정해지지 않은 값1 → 010
끝donenotEmpty가 거짓이 되어 empty가 출력됩니다

shiftInAt0은 새 원소를 index 0에 넣고 나머지를 한 칸씩 밀어 냅니다. shiftInAtN은 반대 방향으로 밀어 index 0의 원소를 버립니다. pop이 반환하는 값과 같은 rule에서 읽은 top이 같은 이유는 두 호출 모두 사이클 시작 상태를 읽기 때문입니다.

직접 바꿔 보세요.

  1. pop에서 items <= shiftInAtN(items, ?); 줄을 지워 보세요. 컴파일은 그대로 성공하지만 popped 12, top was 12가 세 번 출력됩니다. depth만 줄고 index 0이 바뀌지 않기 때문입니다. 타입 검사는 이런 상태 전이 불변식을 검증하지 않습니다.
  2. 스택 용량을 바꾸려면 벡터 길이, 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;
endmodule

return 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 conditionmethod 헤더의 if입니다. 거짓이면 호출하는 rule 전체가 실행되지 않습니다. 모듈 상태는 참조할 수 있고 method 인자는 참조할 수 없습니다
헤더의 if와 본문의 if실행을 보류하는 준비 조건과, 실행은 되되 안에서 갈라지는 분기
준비, 활성화, 실행세 가지 별개의 검사입니다. 어느 하나가 실패해도 "바뀌지 않았다"로 보입니다
합성 경계(* synthesize *) 또는 -g. 인터페이스 타입이 구체적이고 비트로 변환 가능해야 합니다

12. 확인 문제

  1. FIFO inq의 first와 deq를 호출하는 rule에 명시 조건이 없습니다. inq가 비어 있으면 이 rule은 어떻게 될까요?

    정답 보기

    first와 deq의 implicit condition이 거짓이므로 rule 전체가 준비되지 않아 실행되지 않습니다. 호출 하나만 건너뛰는 일은 없습니다.

  2. 명시 조건이 없는 rule이 어느 순간부터 실행되지 않습니다. 가장 먼저 확인할 곳은 어디일까요?

    정답 보기

    rule이 호출하는 method의 implicit condition입니다. 호출 사슬의 어떤 method가 준비되지 않으면 rule 전체가 비활성화됩니다.

  3. 원소 타입이 매개변수인 mkStack에 (* synthesize *)를 직접 붙일 수 없는 이유는 무엇일까요?

    정답 보기

    합성 경계의 인터페이스는 포트로 변환되므로 모든 타입과 폭이 정해져 있어야 합니다. StackIfc#(Bit#(8))처럼 구체적인 타입을 제공하는 래퍼를 만들어 합성합니다.

아래 실습 문제에서는 호출자가 한계에 도달해도 멈추지 않습니다. method 종류, 읽기 시점, 준비 조건 중 어느 계약이 깨졌는지 먼저 판단해 보세요.

확인 지점

예제를 실행해 예측과 비교합니다

참고 자료

참고한 자료의 라이선스와 고지