2차원으로 표현되는 저장 공간
RAM, ROM, 레지스터 파일을 설계할때 주로 메모리가 사용된다.
// 1차원: 단일 레지스터
reg [7:0] single_reg; // 8비트 레지스터 1개
// 2차원: 메모리 배열
reg [7:0] memory [0:15]; // 8비트 레지스터 16개
// ↑ 데이터 폭 ↑ 깊이(주소 범위)
Verilog에서 단일 레지스터는 1차원 배열, 메모리는 2차원 배열로 구현한다.
메모리(=2차원 배열) 선언에도 여러가지 방식(스타일)이 존재한다.
// 방식 1: [시작:끝]
reg [7:0] mem_a [0:255]; // 256개 엔트리 (주소 0~255)
// 방식 2: [끝:시작] - 비권장
reg [7:0] mem_b [255:0]; // 동일하지만 혼란 유발
// 방식 3: 파라미터 활용 ✅ // 매우 권장되는 방식
parameter DEPTH = 256;
parameter WIDTH = 8;
reg [WIDTH-1:0] mem [0:DEPTH-1]; // WIDTH 폭의 레지스터 DEPTH 개 생성
reg [7:0] mem [0:15]; // 메모리 = 8비트 레지스터 16개
reg [3:0] addr; // 레지스터 16개이므로 주소 표현에 4비트 필요
reg [7:0] data; // 8비트 레지스터이므로 데이터 8비트
// ✅ 올바른 접근
data = mem[0]; // 고정 인덱스
data = mem[addr]; // 변수 인덱스
mem[addr] = 8'hFF; // 쓰기
// ❌ 불가능한 접근
data = mem[addr][3:0]; // 2단계 인덱싱 (일부 도구 미지원)
// ✅ 대안: 임시 변수 사용
wire [7:0] temp = mem[addr];
data = temp[3:0]; // 하위 4비트만 추출
위와 같이 배열의 특정 비트에 접근할 수 있다.
포트(주소/데이터/제어/클럭 세트)가 1개라서, 한 클럭 사이클에 메모리에 대해 한 “접근”만 할 수 있는 RAM
module single_port_ram #(
parameter DATA_WIDTH = 8,
parameter ADDR_WIDTH = 4
)(
input clk,
input we, // Write Enable
input [ADDR_WIDTH-1:0] addr,
input [DATA_WIDTH-1:0] din,
output reg [DATA_WIDTH-1:0] dout
);
// 메모리 선언
localparam DEPTH = 1 << ADDR_WIDTH; // 2^ADDR_WIDTH
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
always @(posedge clk) begin
if (we)
mem[addr] <= din; // 쓰기
dout <= mem[addr]; // 읽기 (1 클럭 지연)
end
endmodule
포트가 2개라서, 한 클럭 사이클에 두 번의 접근이 가능한 RAM
module dual_port_ram #(
parameter DATA_WIDTH = 8, // 데이터 크기
parameter ADDR_WIDTH = 4 // 주소 크기
)(
input clk,
// 포트 A (Read/Write)
input we_a,//포트 A의 write enable 신호
input [ADDR_WIDTH-1:0] addr_a, //A포트 주소
input [DATA_WIDTH-1:0] din_a, //A포트 입력 데이터
output reg [DATA_WIDTH-1:0] dout_a, //A포트 출력 데이터
// 포트 B (Read Only)
input [ADDR_WIDTH-1:0] addr_b, //B 포트 주소
output reg [DATA_WIDTH-1:0] dout_b // B포트 출력 데이터
);
localparam DEPTH = 1 << ADDR_WIDTH; // ADDR_WIDTH로 메모리 DEPTH 설정
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1]; // 메모리 생성
// 포트 A
always @(posedge clk) begin
if (we_a) //포트 A 쓰기신호가 1이면
mem[addr_a] <= din_a; //입력된 주소의 메모리에 입력된 값을 저장
dout_a <= mem[addr_a]; // 입력된 주소의 메모리에 있던 기존 값 출력
end
// 포트 B
always @(posedge clk) begin
dout_b <= mem[addr_b]; //b포트로 입력받은 주소에 있던 값을 출력
end
endmodule
┌──────────────────────────┐
clk ──┤ │
we ──┤ Register File │
waddr ──┤ (32 x 32) │
wdata ──┤ │
│ ├── rdata1
raddr1 ─┤ │
raddr2 ─┤ ├── rdata2
└──────────────────────────┘
위 구조의 레지스터 파일을 구현해볼 것이다.
module register_file #(
parameter DATA_WIDTH = 32, //데이터 비트
parameter REG_COUNT = 32, //레지스터 개수
parameter ADDR_WIDTH = 5 //주소 비트
)(
input clk, //클럭 신호
input we, //write enable
input [ADDR_WIDTH-1:0] waddr, //write address
input [DATA_WIDTH-1:0] wdata, //write data
input [ADDR_WIDTH-1:0] raddr1, //read address 1
input [ADDR_WIDTH-1:0] raddr2, //read address 2
output [DATA_WIDTH-1:0] rdata1, //read data 1
output [DATA_WIDTH-1:0] rdata2 //read data 2
);
reg [DATA_WIDTH-1:0] regs [0:REG_COUNT-1]; //32비트 레지스터 32개로 이루어진 메모리 생성
// 동기 쓰기
always @(posedge clk) begin
if (we && waddr != 0) //쓰기 신호, 쓰기 주소 둘다 0이 아닌 경우
regs[waddr] <= wdata; //쓰기 주소에 해당하는 레지스터에 wdata 저장
end
// 비동기 읽기 (조합 논리)
// raddr1이 0이 아니면 raddr1 주소에 저장된 데이터를 radat1로 출력
assign rdata1 = (raddr1 == 0) ? 0 : regs[raddr1];
// raddr2가 0이 아니면 raddr2 주소에 저장된 데이터를 radat2로 출력
assign rdata2 = (raddr2 == 0) ? 0 : regs[raddr2];
initial begin
regs[0] = 0; //초기화
end
endmodule
위와 같이 구현할 수 있다.