본문으로 건너뛰기
code0.HDL

레지스터와 rule 하나

mkReg로 레지스터를 선언하고 rule 하나에서 <=로 쓰며, rule 안의 읽기는 사이클 시작 값을 보고 쓴 값은 다음 사이클에 나타난다는 사실을 확인합니다.

이 장에서 배울 내용

  • mkReg나 mkRegU로 레지스터를 선언하고 인스턴스화(<-), 결합(=), 레지스터 쓰기(<=)를 구분할 수 있습니다
  • rule 안의 모든 읽기가 사이클 시작 값을 보고 쓰기는 에지에서 함께 반영된다는 사실로 결과를 예측할 수 있습니다
  • 명시 조건으로 rule 두 개를 사이클 단위로 추적할 수 있습니다
  • 비트 선택 쓰기로 레지스터 일부를 바꾸고, 여전히 레지스터 전체를 쓴다는 점을 설명할 수 있습니다
이 장에서 배우는 개념
  • Reg·rule·현재/다음 상태
먼저 알아야 할 개념

1. 레지스터는 값을 기억하는 모듈 인스턴스입니다

지금까지의 회로는 모두 조합 회로였습니다. 함수의 결과는 한 사이클 안에서 인자를 따라갔고 아무것도 남지 않았습니다. BSV에서 상태는 값을 저장하는 모듈을 인스턴스화할 때 생기고, 가장 기본적인 것이 레지스터입니다. 레지스터는 mkReg 모듈을 인스턴스화해 만들고, Reg#(t) 인터페이스로 사용합니다.

1.1 레지스터 선언

형식

Reg#(값의_타입) 이름 <- mkReg(초깃값);
Reg#(Bit#(4)) count <- mkReg(0);      // 4비트, 초깃값 0
Reg#(Bool)    done  <- mkReg(False);  // 참/거짓, 초깃값 False
부분의미
Reg#(Bit#(4))4비트 값을 저장하는 레지스터 인터페이스 타입입니다
count인스턴스 이름입니다. 소문자로 시작해야 합니다
<-오른쪽 모듈을 인스턴스화하고 그 인터페이스를 이름에 연결합니다
mkReg(0)리셋 값이 0인 레지스터 모듈입니다

<-는 하드웨어 구조를 만드는 동안 인스턴스를 한 번 만듭니다. 이 과정은 클록 하나, 상승 에지 설계, 그리고 사이클 0 전에 모든 mkReg를 초깃값으로 놓는 리셋을 전제합니다. 식 안에서 count를 쓰면 레지스터를 읽습니다. 읽히는 값은 현재 사이클이 시작할 때 레지스터가 가진 값, 즉 공통 기초의 현재 값 Q입니다.

주의. <-는 인스턴스화 기호이고 <=는 레지스터 쓰기 기호입니다. 모양이 비슷하지만 뜻이 전혀 다릅니다. 인스턴스화는 하드웨어 구조를 만들 때 한 번 일어나고, 레지스터 쓰기는 rule이 실행될 때마다 일어납니다.

1.2 mkReg와 mkRegU

mkReg는 리셋 값이 있는 레지스터를 만들고, mkRegU는 리셋 논리가 없는 레지스터를 만듭니다.

형식

Reg#(타입) 이름 <- mkReg(리셋_값);   // 리셋 직후 값이 정해져 있음
Reg#(타입) 이름 <- mkRegU;           // 리셋 직후 값이 정해져 있지 않음
Reg#(UInt#(4)) count <- mkReg(0);

rule tick;
  count <= count + 1;   // 이전 값을 읽고 새 값을 쓴다
endrule
코드의미
Reg#(UInt#(4))UInt#(4)를 저장하는 레지스터의 인터페이스입니다
<- mkReg(0)리셋 값이 0인 레지스터를 인스턴스화합니다
식 안의 count레지스터의 _read method를 호출합니다
count <= ...레지스터의 _write method를 호출합니다

레지스터는 결국 정해진 개수의 비트를 저장하므로 저장 타입에 Bits 인스턴스가 있어야 합니다.

주의. mkRegU는 다른 논리가 읽기 전에 반드시 값을 쓴다는 보장이 있을 때만 쓰세요. 리셋 직후의 값이 설계 동작에 포함된다면 mkReg를 써야 합니다.

2. 대입처럼 보이는 세 문장

2.1 결합, 인스턴스화, 쓰기

BSV에는 대입처럼 보이는 문장이 세 가지 있습니다. 세 문장은 의미도, 효과가 나타나는 시점도 다릅니다.

형식

이름 = 식;                    // 값에 이름을 결합합니다
이름 <- 모듈_또는_Action;      // 모듈이나 ActionValue의 결과를 결합합니다
레지스터 <= 식;               // 레지스터 쓰기를 예약합니다
형식하는 일효과가 나타나는 시점
value = expression;이름에 값을 결합합니다바로 다음 문장부터. 상태는 바뀌지 않습니다
result <- context;모듈을 인스턴스화하거나 ActionValue를 실행하고 결과를 결합합니다모듈 구성 시점 또는 Action 실행 시점
register <= expression;상태에 대한 쓰기를 예약합니다rule이 실행된 사이클을 끝내는 클록 에지. 감싸는 rule이 실행되는 매 사이클
Reg#(UInt#(8)) counter <- mkReg(0);   // <- : 모듈 인스턴스화
UInt#(8) next = counter + 1;           // =  : 값에 이름 붙이기
counter <= next;                       // <= : 레지스터 쓰기 예약

주의. count = count + 1;은 레지스터 쓰기가 아닙니다. count는 지역 변수가 아니라 인터페이스이고, bsc는 P0039로 멈추면서 메시지에서 <=를 제안합니다.

2.2 쓰기 기호는 다음 값을 정하고, 읽기는 사이클 시작 값을 봅니다

식 안에 레지스터 이름을 쓰면 현재 값을 읽습니다. 쓰기에는 <=를 사용합니다.

형식

레지스터_이름 <= 식;
count <= count + 1;   // 현재 값에 1을 더한 값을 다음 값으로 지정

<= 쓰기는 즉시 반영되지 않습니다. count <= count + 1;은 현재 사이클을 끝내는 클록 에지에서 count + 1을 저장하라는 요청이고, 새 값은 다음 사이클부터 읽힙니다. 같은 rule 안의 모든 count 읽기는 그 줄 앞이든 뒤든 사이클 시작 값을 봅니다. 오른쪽 식이 공통 기초의 다음 값 D이고, <=가 레지스터 입력으로 들어가는 선입니다.

rule step;
  r <= 5;
  $display("%0d", r);   // 여전히 r의 이전 값을 출력합니다
endrule

r이 1에서 시작했다면 이 rule은 1을 출력하고, 5는 다음 사이클에 읽힙니다. 소프트웨어의 순차 대입과 다르게, 한 rule의 쓰기는 모두 함께 반영됩니다.

_read와 _write는 _read가 _write보다 앞서는 스케줄을 가진 일반 method이며, 이 스케줄이 mkReg의 동작을 정합니다. 다른 상태 요소는 다른 스케줄을 선언합니다. 앞의 tick에서 count + 1은 쓰기 앞뒤에 어떤 문장이 있든 항상 이전 count를 사용합니다.

2.3 let으로 타입 추론하기

let은 이름을 선언하면서 타입을 초기화 식에서 추론합니다.

형식

let 이름 = 식;
let 이름 <- 모듈_또는_Action;
let encoded = pack(message);   // message의 타입에서 결정됩니다
let hi = word[15:8];           // Bit#(8)

초기화 식만으로 타입이 하나로 정해질 때 let을 쓰세요. 타입이 하드웨어 폭을 드러내야 하거나 식의 해석에 타입이 필요하면 직접 적습니다. 어디에서도 타입이 정해지지 않는 식은 let을 써도 오류입니다. let w = unpack(r); 뒤에 pack(w)만 쓰면 w의 타입이 정해지지 않으므로 bsc는 T0033("There is not enough explicit type information to deduce the types of all expressions.")을 보고합니다.

3. rule이 쓰기가 일어나는 때를 정합니다

3.1 rule 선언과 명시 조건

rule은 이름, 조건, 동작 본문으로 이루어집니다. 이름 뒤 괄호 안의 Bool 식이 rule의 명시 조건이며 흔히 guard라고 부릅니다. 조건이 참인 모든 사이클에 rule은 실행될 수 있고, 실행되면 본문의 모든 쓰기가 에지에서 함께 저장됩니다. 하나의 상태 전이입니다. 그 사이클에 실행된 어떤 rule도 쓰지 않은 레지스터는 값을 유지합니다.

형식

rule 이름 (조건);
  동작;
endrule
rule step (count < 5);
  count <= count + 1;
  $display("count = %0d", count);
endrule

rule always_on;          // 조건이 없으면 (True)와 같습니다
  $display("tick");
endrule
부분의미
steprule 이름입니다. 소문자로 시작합니다
(count < 5)명시 조건입니다. 이 조건이 참인 사이클에만 실행될 수 있습니다
count <= count + 1;rule이 실행될 때 일어나는 상태 변경입니다

상태가 mkReg 레지스터만인 설계에서는 조건만으로 rule의 준비 여부가 정해집니다. 레지스터의 읽기와 쓰기는 언제나 준비되어 있습니다. 호출한 method에서 오는 조건은 guard: 유지와 정지 장에서 더합니다.

3.2 한 사이클 동안 일어나는 일

bsc가 생성하는 스케줄러는 매 사이클 같은 순서로 동작합니다.

시점일어나는 일
사이클 시작모든 조건과 식이 현재 Reg 값을 읽습니다(wire, mkCReg 포트, bypass FIFO는 같은 사이클의 쓰기를 볼 수 있습니다. FIFO·split·라이브러리 primitive 장 참고)
사이클 동안스케줄러가 함께 실행할 수 있는 준비된 rule의 집합을 고르고 다음 값을 계산합니다
클록 에지선택된 rule들의 쓰기가 저장됩니다

여러 rule이 한 사이클을 공유하려면, 그 결과가 어떤 합법적인 순서로 하나씩 실행한 결과와 같아야 합니다. 이 요구 덕분에 각 rule을 따로 떼어 추론할 수 있습니다. 두 rule이 같은 상태를 다룰 때의 동작은 상태를 공유하는 rule 장에서 다룹니다.

4. rule 두 개를 예측한 뒤 실행하세요

Intro.bsv에는 레지스터 두 개와 rule 두 개가 있습니다. 코드를 읽으며 다음 세 가지를 확인하세요.

요소동작
count, donecount는 0에서 시작하는 4비트 레지스터이고 done은 False에서 시작하는 Bool입니다
stepcount < 5인 동안 실행되어 count + 1을 쓰고 count를 출력합니다
finishcount == 5 && !done일 때 실행되어 done을 세우고 count를 출력한 뒤 시뮬레이션을 끝냅니다

두 조건은 같은 사이클에 함께 참이 되지 않으므로 매 사이클 둘 중 많아야 하나가 실행됩니다.

Intro.bsv
package Intro;

// 초깃값이 0인 4비트 레지스터다. Reg는 인터페이스이고
// mkReg는 이를 제공하는 모듈이다.
(* synthesize *)
module mkTop(Empty);

  Reg#(Bit#(4)) count <- mkReg(0);
  Reg#(Bool)    done  <- mkReg(False);

  // rule은 명시 조건과 모든 implicit condition이 성립하고 scheduling이
  // 허용할 때 실행될 수 있다.
  rule step (count < 5);
    count <= count + 1;
    $display("count = %0d", count);
  endrule

  // 두 rule의 조건은 같은 사이클에 참이 되지 않는다. count는 한 번만
  // 5에 도달하며, 그 다음 사이클에 finish가 준비된다.
  rule finish (count == 5 && !done);
    done <= True;
    $display("done at count = %0d", count);
    $finish(0);
  endrule

endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

실행 전에 종이에 추적하세요. 사이클 시작의 count, 실행되는 rule, 출력되는 값을 각각 한 열로 적습니다.

확인 문제

step은 count + 1을 쓴 다음 count를 출력합니다. 첫 번째로 출력되는 수는 무엇입니까?

답

확인 문제

count = 줄은 몇 줄 출력됩니까?

확인 문제

done at count = 줄에는 어떤 수가 나옵니까?

실행을 누르고 출력 줄을 추적표와 비교하세요.

사이클 시작의 count실행되는 rule출력 값에지 뒤 상태
0step0count가 1
1step1count가 2
2step2count가 3
3step3count가 4
4step4count가 5
5finish5done이 True. 시뮬레이션 종료

count = 줄은 0부터 4까지입니다. step은 쓰기를 요청한 뒤 출력하지만 쓰기는 에지에서 저장되므로 출력은 이전 값을 보입니다. 저장한 값 1부터 4는 각각 다음 사이클의 step이 출력합니다. 5가 저장된 뒤에는 guard가 거짓이므로 step이 출력하지 않습니다. 대신 finish가 step의 다섯 번째 쓰기가 저장된 다음 사이클에 5를 읽어 done at count = 줄에 출력합니다.

5. 한 곳만 바꿔 보세요

실행 패널에서 step 안의 두 줄 순서를 바꿔 $display가 먼저, 쓰기가 나중에 오게 하세요. 실행 전에 무엇이 달라질지 정하세요.

확인 문제

display를 쓰기 위로 옮기면 출력은 어떻게 됩니까?

답

변경예상 결과
step 안의 쓰기와 $display 순서 바꾸기출력이 같습니다. 두 문장은 하나의 원자적 전이이고 쓰기는 에지에서 반영됩니다
두 5를 모두 3으로 바꾸기count = 줄이 세 줄(0, 1, 2)이 되고 done at count =는 3을 보입니다
step의 조건만 count < 3으로 바꾸기count가 3에서 멈추고 finish는 준비되지 않습니다. guard: 유지와 정지 장이 바로 이 경우를 다룹니다

6. 비트 선택 쓰기는 레지스터의 일부를 바꿉니다

6.1 비트 선택 쓰기는 읽기·수정·쓰기입니다

값이 필요한 곳에 레지스터를 쓰면 레지스터가 암묵적으로 읽힙니다. <=는 레지스터를 쓰며, 왼쪽에서 레지스터의 일부를 지정할 수 있습니다.

형식

타입 이름 = 레지스터;        // 읽기
레지스터 <= 식;              // 전체 쓰기
레지스터[상위:하위] <= 값;    // 비트 선택 쓰기
레지스터.필드 <= 값;         // 구조체 레지스터의 필드 쓰기
Bit#(16) snapshot = word;     // 읽기
word <= snapshot + 1;         // 레지스터 전체 쓰기
word[7:0] <= 8'h34;           // 하위 바이트만 바꾸기

비트 선택 쓰기는 레지스터 전체를 읽고, 고치고, 다시 쓰는 동작입니다. 선택하지 않은 비트는 현재 값에서 복사되고, 클록 에지에서 레지스터 전체가 기록됩니다. word[7:0] <= 8'h34;는 word <= { word[15:8], 8'h34 };와 같은 뜻입니다.

주의. 기본 동작이 레지스터 전체 쓰기이므로, 같은 레지스터의 서로 다른 구간을 쓰는 두 rule도 서로 충돌합니다. 비트 선택을 써도 레지스터를 여러 rule이 나누어 소유할 수 없습니다.

6.2 예제: 하위 바이트만 바꾸기

RegisterSelection.bsv에서 word는 16'hABCD, updated는 False에서 시작합니다. 두 초깃값과 updateLowerByte, show의 조건을 먼저 확인하세요. 실행하기 전에 예측해 보세요. 첫 사이클에 실행될 수 있는 rule은 무엇이고, show가 절반만 갱신된 값을 볼 수 있을까요?

확인 문제

show는 어떤 16진수 값을 출력합니까?

RegisterSelection.bsv
package RegisterSelection;

(* synthesize *)
module mkTop(Empty);
  Reg#(Bit#(16)) word <- mkReg(16'hABCD);
  Reg#(Bool) updated <- mkReg(False);

  rule updateLowerByte (!updated);
    // 구간 쓰기는 선택한 구간 밖의 비트를 유지한다.
    word[7:0] <= 8'h34;
    // 이 갱신도 클록 에지에서 레지스터 전체에 대한 쓰기 하나로 처리된다.
    updated <= True;
  endrule

  rule show (updated);
    $display("word = %h", word);
    $finish(0);
  endrule
endmodule

endpackage

이 개념 실습하기

다음 단계로 짧은 BSV 문제에 이 개념을 적용해 보세요.

관찰 시점wordupdated일어나는 일
사이클 0 시작16'hABCDFalseupdateLowerByte만 실행할 수 있습니다
사이클 0 Action이전 값이전 값16'hAB34와 True의 쓰기가 예약됩니다
첫 클록 에지16'hAB34True두 쓰기가 함께 반영됩니다
사이클 1 시작16'hAB34Trueshow가 ab34를 출력합니다

상위 바이트 ab는 비트 선택 쓰기가 현재 값을 복사했으므로 남았습니다. 첫 rule의 두 쓰기가 같은 에지에서 반영되므로 show는 절반만 갱신된 값을 볼 수 없습니다.

직접 바꿔 보세요.

  1. word[7:0] <= 8'h34;를 word <= zeroExtend(8'h34);로 바꾸어 보세요. 컴파일은 그대로 성공하지만 상위 바이트를 복사하지 않으므로 show는 0034를 출력합니다.
  2. 대신 word <= { word[15:8], 8'h34 };로 바꾸어 보세요. 다시 ab34가 출력됩니다. 비트 선택 쓰기가 짧게 표현하던 읽기·수정·쓰기를 직접 적은 형태입니다.

7. 적용: 카운터를 움직이고 상위 바이트를 지키세요

첫 번째 채점 문제는 rule이 count <= count;를 쓰는 카운터입니다. rule은 준비되어 매 사이클 실행되지만 다음 값이 현재 값과 같으므로 테스트벤치는 매 줄 0을 출력합니다. 그 대입 하나만 바꿔 각 실행이 현재 값의 다음 값을 예약하게 하세요. 테스트벤치의 rule은 사이클마다 value와 counting을 한 번 출력하며 사이클 시작 상태를 읽습니다. 그래서 기대하는 추적은 0, 1, 2, 3, 4, 5 그리고 5 유지입니다. 실행 전에 counting이 거짓으로 바뀌는 줄을 예측하세요.

두 번째 문제는 6절의 실수를 모듈 안에 옮긴 것입니다. patchLow는 저장된 16비트 워드의 하위 바이트만 바꿔야 하지만 wordReg <= zeroExtend(replacement);를 쓰므로 불러온 상위 바이트 ab가 사라지고, 실행 결과는 ab34 대신 0034입니다. 기존 쓰기의 대상을 7번부터 0번 비트의 선택으로 좁히세요. 임시 레지스터나 연결 연산은 필요하지 않습니다.

8. 자주 하는 실수

8.1 =로 레지스터 쓰기

// 잘못된 코드: =로는 레지스터를 바꿀 수 없습니다
rule step;
  count = count + 1;
endrule

// 바른 코드: 레지스터 쓰기를 예약합니다
rule step;
  count <= count + 1;
endrule

=는 지역 이름에 값을 결합하는데, rule 안에서 count는 지역 이름이 아닙니다. bsc는 P0039(Assignment of `count' without declaration, or non-local assignment in possibly wrong context)를 보고하며, 메시지 안에서 perhaps you meant "<=" instead라고 안내합니다.

8.2 지역 변수에 <= 쓰기

// 잘못된 코드: x는 레지스터가 아니라 값입니다
UInt#(8) x = 0;
x <= 5;

// 바른 코드: 값을 다시 결합합니다
UInt#(8) x = 0;
x = 5;

<=의 왼쪽에는 레지스터나 쓰기 method가 있는 인터페이스가 와야 합니다. UInt#(8) 값에 쓰면 bsc는 T0066("The left-hand side of an assignment must be a register or interface type.")을 보고합니다.

8.3 같은 rule에서 쓴 값을 바로 읽으려 하기

// 잘못된 기대: 새 값이 아니라 이전 값이 출력됩니다
rule step;
  count <= count + 1;
  $display("%0d", count);
endrule

// 바른 코드: 새 값을 한 번 계산하고 그 이름을 사용합니다
rule step;
  let next = count + 1;
  count <= next;
  $display("%0d", next);
endrule

쓴 값은 클록 에지에서 저장되고, rule 안의 레지스터 읽기는 언제나 진입 상태를 봅니다. 같은 rule에서 새 값이 필요하면 =나 let으로 이름을 붙이고 그 이름을 사용하세요.

8.4 mkRegU 레지스터를 쓰기 전에 읽기

// 잘못된 코드: 처음 사이클들이 정해지지 않은 값을 읽습니다
Reg#(UInt#(8)) total <- mkRegU;
rule accumulate;
  total <= total + 1;
endrule

// 바른 코드: 시작 값이 동작의 일부입니다
Reg#(UInt#(8)) total <- mkReg(0);

mkRegU는 값이 언제나 읽기 전에 쓰일 때만 리셋 논리를 줄입니다. 누적 합은 첫 사이클부터 자기 값을 읽습니다.

9. 정리

요소의미
Reg#(t) r <- mkReg(init);t를 저장하는 레지스터. 리셋 뒤 값은 init. mkRegU는 리셋 값을 정하지 않습니다
식 안의 r사이클 시작에 가진 현재 값. _read method
r <= expr;사이클을 끝내는 에지에서 저장되는 다음 값. _write method
=, <-, <=값 결합, 모듈이나 ActionValue 결과 결합, 레지스터 쓰기 예약
let초기화 식에서 타입을 추론합니다. 타입이 정해지지 않으면 오류 T0033
rule name (cond); … endrulecond가 참이고 준비 조건과 스케줄링이 허용하면 실행될 수 있습니다. 쓰기는 함께 반영되며, 명시 조건을 생략하면 True입니다
한 사이클조건은 사이클 시작 상태를 읽고, 스케줄러가 준비된 rule을 고르고, 에지가 쓰기를 저장합니다
r[hi:lo] <= v;선택하지 않은 비트를 복사하며 여전히 레지스터 전체를 씁니다
rule 안의 $display사이클 시작 값을 출력합니다

10. 확인 문제

  1. Intro.bsv의 step은 count <= count + 1을 예약한 뒤 count를 출력합니다. 어떤 값이 출력되며, 첫 출력이 0인 이유는 무엇입니까?

    답 보기

    rule이 시작될 때 count가 가진 값이 출력됩니다. <= 쓰기는 클록 에지에서 저장되므로 같은 사이클의 $display는 사이클 시작 값을 읽고, 첫 실행에서 그 값은 0입니다. 증가한 값은 다음 사이클에야 읽힙니다.

  2. 비트 선택 쓰기를 word <= zeroExtend(8'h34);로 바꾸면 show는 무엇을 출력하며, 컴파일이 여전히 성공하는 이유는 무엇입니까?

    답 보기

    0034를 출력합니다. 타입이 맞으므로 컴파일 오류는 없습니다. 상위 바이트를 현재 값에서 복사하지 않으므로 다음 상태만 틀립니다.

확인 지점

실습 문제를 제출해 채점을 받습니다

채점은 서버의 비공개 테스트로 진행되며, 결과는 이 브라우저에만 기록됩니다.

참고 자료

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