1. 메모리는 레지스터의 배열입니다
레지스터는 값 하나를 담습니다. 메모리는 폭이 같은 값 여러 개를 담고, 주소로 그중 하나를
고릅니다. Verilog에서는 reg 벡터의 배열로 선언합니다. 이름 앞의 범위가 워드 하나의 폭이고,
이름 뒤의 범위가 워드의 개수입니다.
형식
reg [워드_최상위_비트:0] 이름 [0:워드_수-1];reg [7:0] mem [0:3]; // 8비트 워드 네 개
mem[2] <= 8'h5a; // 클록 에지에서 2번 워드에 쓰기
assign rdata = mem[raddr]; // raddr 번지의 워드 읽기| 부분 | 의미 |
|---|---|
이름 앞의 [7:0] | 워드 하나의 폭입니다. 보통의 벡터와 같습니다 |
이름 뒤의 [0:3] | 0번부터 3번까지 워드 네 개입니다 |
mem[2] | 워드 하나 전체입니다. 주소는 비트가 아니라 워드를 고릅니다 |
mem[raddr] | 주소에 신호를 쓸 수 있으며, 이렇게 읽는 것은 워드들 위의 멀티플렉서입니다 |
이 장의 예제는 메모리에 워드 단위로 접근합니다. 한 번도 쓰지 않은 워드를 읽으면 x가
나옵니다. 여기서는 always @(posedge clock) 안에서 <=로
쓰므로, 쓰기는 상승 에지에서만 일어납니다.
2. 에지에서 쓰고, 두 가지 방식으로 읽기
RegFile.v는 8비트 워드 네 개로 이루어진 레지스터 파일이며, 쓰기 포트 하나와 읽기 주소 하나를
가집니다. 같은 주소를 두 방식으로 읽습니다.
| 읽기 | 코드 | 데이터가 보이는 때 |
|---|---|---|
| 비동기 | assign rdata_async = mem[raddr]; | 주소와 같은 사이클. raddr나 워드가 바뀌면 바로 |
| 레지스터를 거친 읽기(동기) | 클록 블록 안의 rdata_sync <= mem[raddr]; | 한 사이클 뒤. 그 사이클을 끝내는 에지에서 워드를 붙잡습니다 |
// 4 x 8 레지스터 파일. 쓰기 포트 하나, 주소 하나를 두 방식으로 읽는다.
module regfile (
input clock,
input we,
input [1:0] waddr,
input [7:0] wdata,
input [1:0] raddr,
output [7:0] rdata_async,
output reg [7:0] rdata_sync
);
reg [7:0] mem [0:3];
// 쓰기는 상승 에지에서만 일어난다.
always @(posedge clock) begin
if (we) mem[waddr] <= wdata;
// 레지스터를 거친 읽기. raddr의 워드를 에지에서 샘플링해 저장한다.
rdata_sync <= mem[raddr];
end
// 비동기 읽기. rdata_async는 raddr와 mem을 바로 따른다.
assign rdata_async = mem[raddr];
endmodule테스트벤치는 하강 에지에서 입력을 바꾸고, 각 사이클을 끝내는 상승 에지 전에 그 사이클을
출력합니다. 따라서 RF 줄 하나는 그 사이클 동안 넣은 입력과 그동안 출력에 보이는 값입니다.
네 사이클은 다음과 같습니다.
| 사이클 | 쓰기 | raddr |
|---|---|---|
| 0 | 0번지에 11 | 0 |
| 1 | 1번지에 22 | 0 |
| 2 | 없음 | 1 |
| 3 | 없음 | 1 |
실행하기 전에 예측해 보세요. 쓰기는 그 사이클을 끝내는 에지에서 반영됩니다. 비동기 읽기는
지금 raddr에 있는 워드를, 레지스터를 거친 읽기는 에지 하나 전에 raddr에 있던 워드를
보입니다.
확인 문제
0번 사이클에 0번지로 11을 쓰고 raddr는 0입니다. async에 11이 처음 보이는 사이클은 몇 번입니까?
확인 문제
sync에 11이 처음 보이는 사이클은 몇 번입니까?
결과 확인. 아래 실행 결과의 처음 네 줄이 RF 줄입니다.
| 줄 | async | sync | 확인할 내용 |
|---|---|---|---|
c=0 | xx | xx | 아직 아무것도 쓰지 않았습니다 |
c=1 | 11 | xx | 비동기 읽기에는 쓴 값이 보이고, 레지스터는 이전 워드를 붙잡았습니다 |
c=2 | 22 | 11 | raddr가 1로 바뀌자 async는 바로 따르고 sync는 아직 0번지 값입니다 |
c=3 | 22 | 22 | 레지스터를 거친 읽기가 따라잡았습니다 |
레지스터를 거친 읽기는 한 사이클을 쓰는 대신 깔끔한 레지스터 출력을 줍니다. 많은 FPGA 블록 메모리는 레지스터를 거친 읽기만 제공하고, 비동기 읽기는 플립플롭이나 작은 분산 메모리로 만듭니다. 어느 쪽을 고르든 나머지 설계는 데이터가 올바른 사이클에 온다고 예상하고 있어야 합니다.
3. FIFO는 들어온 순서대로 워드를 내보냅니다
FIFO(first in, first out)는 큐로 쓰는 메모리로, 워드가 들어온 순서대로 나갑니다. Fifo.v는
WIDTH비트 워드를 최대 DEPTH개 담습니다. 상승 에지마다 적용되는 규칙은 다음과 같습니다.
| 에지의 입력 | 조건 | 효과 |
|---|---|---|
push | full이 아님 | din을 wr_ptr 자리에 쓰고 포인터를 옮깁니다. count + 1 |
push | full | 무시합니다. 아무것도 쓰지 않습니다 |
pop | empty가 아님 | rd_ptr를 옮겨 가장 오래된 워드를 꺼냅니다. count - 1 |
pop | empty | 무시합니다 |
push와 pop | 둘 다 허용됨 | 한 워드가 들어오고 한 워드가 나가 count는 그대로입니다 |
reset | 포인터와 count가 0으로 돌아갑니다 |
full은 count == DEPTH, empty는 count == 0입니다. 둘 다 count 레지스터에서 나오므로
상승 에지에서만 바뀝니다. dout은 rd_ptr 자리의 비동기 읽기입니다. FIFO가 비어 있지 않으면
가장 오래된 워드를 보여 주고, pop은 포인터를 옮깁니다. 비어 있을 때의 dout은 유효하지 않습니다.
// WIDTH비트 워드 DEPTH개를 담는 동기 FIFO.
module sync_fifo #(
parameter WIDTH = 8,
parameter DEPTH = 4
) (
input clock,
input reset,
input push,
input [WIDTH-1:0] din,
input pop,
output [WIDTH-1:0] dout,
output full,
output empty,
output reg [4:0] count
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
// 포인터를 직접 되돌리므로 DEPTH가 2의 거듭제곱일 필요는 없다(최대 16).
reg [3:0] wr_ptr, rd_ptr;
// 가득 찼을 때의 push와 비었을 때의 pop은 무시한다.
wire do_push = push && !full;
wire do_pop = pop && !empty;
always @(posedge clock) begin
if (reset) begin
wr_ptr <= 4'd0;
rd_ptr <= 4'd0;
count <= 5'd0;
end else begin
if (do_push) begin
mem[wr_ptr] <= din;
wr_ptr <= (wr_ptr == DEPTH - 1) ? 4'd0 : wr_ptr + 4'd1;
end
if (do_pop)
rd_ptr <= (rd_ptr == DEPTH - 1) ? 4'd0 : rd_ptr + 4'd1;
// push와 pop이 둘 다 허용되면 한 워드가 들어오고 한 워드가 나가므로 count는 그대로다.
count <= count + do_push - do_pop;
end
end
// 비어 있지 않으면 가장 오래된 워드가 비동기 읽기로 dout에 보인다.
assign dout = mem[rd_ptr];
assign full = (count == DEPTH);
assign empty = (count == 5'd0);
endmodule코드에서 세 가지를 눈여겨보세요.
do_push와do_pop은 허용된 push와 pop입니다. 상태를 바꾸는 곳은 모두 이 두 이름을 쓰므로, 무시된 push는 쓰지 못하고 무시된 pop은 포인터를 옮기지 못합니다.count <= count + do_push - do_pop;한 줄이 네 가지 조합을 모두 처리합니다.- 포인터는 넘침이 아니라
DEPTH - 1과의 비교로 되돌아가므로,DEPTH가 2의 거듭제곱이 아니어도 16까지는 됩니다.WIDTH와DEPTH는 파라미터이며, 인스턴스·계층·parameter 장에서처럼 인스턴스마다 정합니다.
4. FIFO 출력을 예측하고 실행하기
Tb.v는 레지스터 파일을 먼저 구동한 뒤 FIFO의 리셋을 풀고 사이클을 0부터 다시 셉니다. FIFO
줄도 RF 줄과 같은 규칙을 따릅니다. 그 사이클의 입력과, 사이클을 끝내는 에지 전의 출력입니다.
module tb;
reg clock = 0;
reg we = 0, reset = 1, push = 0, pop = 0;
reg [1:0] waddr = 0, raddr = 0;
reg [7:0] wdata = 0, din = 0;
wire [7:0] rdata_async, rdata_sync, dout;
wire full, empty;
wire [4:0] count;
integer c = 0;
regfile rf (.clock(clock), .we(we), .waddr(waddr), .wdata(wdata), .raddr(raddr),
.rdata_async(rdata_async), .rdata_sync(rdata_sync));
sync_fifo #(.WIDTH(8), .DEPTH(4)) fifo (.clock(clock), .reset(reset), .push(push),
.din(din), .pop(pop), .dout(dout), .full(full), .empty(empty), .count(count));
always #5 clock = ~clock;
// 하강 에지에서 한 사이클의 입력을 넣고, 그 사이클을 끝내는 상승 에지 전에 보이는 값을 출력한다.
task rf_cycle(input w, input [1:0] wa, input [7:0] wd, input [1:0] ra);
begin
@(negedge clock);
we = w; waddr = wa; wdata = wd; raddr = ra;
#1 $display("RF c=%0d we=%b waddr=%0d wdata=%h raddr=%0d | async=%h sync=%h",
c, we, waddr, wdata, raddr, rdata_async, rdata_sync);
c = c + 1;
end
endtask
task fifo_cycle(input p, input [7:0] d, input q);
begin
@(negedge clock);
push = p; din = d; pop = q;
#1 $display("FIFO c=%0d push=%b din=%h pop=%b | count=%0d full=%b empty=%b dout=%h",
c, push, din, pop, count, full, empty, dout);
c = c + 1;
end
endtask
initial begin
$dumpfile("dump.vcd");
$dumpvars(0, tb);
rf_cycle(1, 0, 8'h11, 0);
rf_cycle(1, 1, 8'h22, 0);
rf_cycle(0, 0, 8'h00, 1);
rf_cycle(0, 0, 8'h00, 1);
// 지금까지 FIFO는 리셋 상태였다. 리셋을 풀고 사이클을 0부터 다시 센다.
@(negedge clock) reset = 0;
c = 0;
fifo_cycle(1, 8'ha1, 0);
fifo_cycle(1, 8'ha2, 0);
fifo_cycle(1, 8'ha3, 0);
fifo_cycle(1, 8'ha4, 0);
fifo_cycle(1, 8'ha5, 0);
fifo_cycle(1, 8'ha6, 1);
fifo_cycle(1, 8'hb1, 1);
fifo_cycle(0, 8'h00, 1);
fifo_cycle(0, 8'h00, 1);
fifo_cycle(0, 8'h00, 1);
fifo_cycle(0, 8'h00, 1);
fifo_cycle(0, 8'h00, 0);
$finish;
end
endmodule이 개념 실습하기
다음 단계로 짧은 Verilog 문제에 이 개념을 적용해 보세요.
| 사이클 | 0~4 | 5 | 6 | 7~10 | 11 |
|---|---|---|---|---|---|
| 입력 | a1~a5 push | a6 push와 pop | b1 push와 pop | pop | 없음 |
실행하기 전에 예측해 보세요. 사이클마다 count, full, empty를 적은 뒤 답해 보세요.
확인 문제
full이 처음 1이 되는 FIFO 사이클은 몇 번입니까?
확인 문제
5번 사이클에 FIFO는 가득 찬 채로 push(a6)와 pop이 모두 1입니다. 6번 사이클의 count는 얼마입니까?
확인 문제
워드를 꺼내는 5, 6, 7, 8, 9번 사이클에 dout에는 어떤 워드가 보입니까?
결과 확인.
| 줄 | 확인할 내용 |
|---|---|
c=0 | count=0 empty=1, dout=xx. 저장된 워드가 없습니다 |
c=1~c=4 | count가 1에서 4로, c=4에서 full=1. dout은 가장 오래된 워드 a1에 머뭅니다 |
c=5 | 여전히 count=4. c=4의 a5 push는 무시되었습니다 |
c=6, c=7 | 두 줄 모두 count=3. c=6에서는 한 워드가 들어오고 한 워드가 나갔습니다 |
c=10 | count=0 empty=1. dout=a2는 메모리에 남은 예전 워드이며, 비어 있는 동안에는 의미가 없습니다 |
c=11 | 여전히 count=0. c=10의 pop은 무시되었습니다 |
파형을 열고 fifo.count, fifo.full, fifo.empty를 비교해 보세요. 셋 모두 상승 에지에서만,
같은 에지에서 바뀝니다.
5. 하나만 바꿔 보세요: 더 얕은 FIFO
Tb.v에서 .DEPTH(4)를 .DEPTH(2)로 바꾸고 다시 실행합니다. 다른 곳은 바꾸지 않습니다.
확인 문제
DEPTH가 2이면 full이 처음 1이 되는 FIFO 사이클은 몇 번입니까?
이제 c=2부터 c=5까지 full=1이고, 꺼내는 동안 dout에는 a1, a2, b1이 보입니다.
플래그가 파라미터와 비교하므로 Fifo.v의 다른 줄은 바꿀 필요가 없었습니다. DEPTH를 다시
4로 돌려 두세요.
6. 실습: FIFO가 채워진 정도를 추적하게 하기
점검 과제는 실습 문제 fifo-flags입니다. 이 문제의 FIFO에는 저장 공간, 포인터,
do_push/do_pop이 있지만, count는 바뀌지 않고 full과 empty는 0에 묶여 있습니다. 그래서
공개 출력에는 빈 FIFO에서 꺼내는 pop과 저장된 워드를 덮어쓰는 push가 보입니다. 올바른 출력을
먼저 예측한 뒤 count 갱신과 두 플래그를 쓰세요. 채점은 다른 WIDTH와 DEPTH로도 실행하므로
어디에서도 4를 가정하면 안 됩니다.
7. 정리
| 요소 | 뜻 |
|---|---|
reg [7:0] mem [0:N-1]; | 8비트 워드 N개. mem[addr]는 워드 하나 |
always @(posedge clock) 안의 mem[waddr] <= wdata; | 동기 쓰기. 에지마다 워드 하나 |
assign rdata = mem[raddr]; | 비동기 읽기. 주소와 같은 사이클에 데이터 |
클록 블록 안의 rdata <= mem[raddr]; | 레지스터를 거친 읽기. 한 사이클 뒤에 데이터 |
do_push = push && !full, do_pop = pop && !empty | 무시된 push와 pop은 상태를 바꾸지 않음 |
count <= count + do_push - do_pop; | push, pop, 둘 다, 둘 다 아님을 한 줄로 처리 |
full = (count == DEPTH), empty = (count == 0) | count에서 나온 플래그이므로 에지에서만 바뀜 |
8. 확인 문제
-
어떤 설계가 메모리를 레지스터를 거쳐 읽으면서, 주소를 넣은 바로 그 사이클에 읽기 데이터를 사용합니다. 이 설계가 보는 값은 무엇일까요?
정답 보기
이전 사이클 주소의 워드입니다. 레지스터를 거친 읽기는 주소를 넣은 사이클을 끝내는 에지에서 워드를 붙잡으므로 데이터는 한 사이클 뒤에 유효합니다. 설계가 한 사이클을 기다리거나 비동기 읽기를 써야 합니다.
-
FIFO에서
full = (wr_ptr == rd_ptr)를 쓸 수 없는 이유는 무엇일까요?정답 보기
두 포인터는 FIFO가 비었을 때도, 가득 찼을 때도 같습니다.
DEPTH번 push하면 쓰기 포인터가 한 바퀴 돌아 읽기 포인터와 만나기 때문입니다. 두 상태를 구분할 다른 정보가 필요하며, 여기서는count레지스터가 그 역할을 합니다. 포인터마다 비트를 하나 더 두는 방법도 흔히 씁니다.