Published on

FPGA 온칩 메모리 읽기 및 쓰기

Authors
  • avatar
    Name
    JaeHyeok CHOI
    Twitter
    none

FPGA 온칩 RAM 읽기 및 쓰기

  1. ip Catalog > Block Memory Generator > SD port RAM 선택

  2. Port A 세팅 Port A Witdh: 16 Port A Width: 512 Enable Port Type: Always Enalbed

  3. Port B 세팅 Port B Witdh: 16 Port B Width: 512 Enable Port Type: Always Enalbed Primitives Output Register: 체크 해제

RAM 포트 정의 및 타이밍

신호명방향설명
clkainA클록입력
weainA활성화됨
addrainA주소입력
dinainA데이터 입력
clkbinB클록입력
addrbinB주소입력
doutboutB데이터출력

RAM 쓰기 타이밍

RAM 읽기 타이밍

테스트 벤치

`timescale 1ns / 1ps
//////////////////////////////////////////////////////////////////////////////////
module ram_test(
                       input      sys_clk_p, //system clock 200Mhz on board
            input      sys_clk_n,            //system clock 200Mhz on board
                       input rst_n
               );

//-----------------------------------------------------------
reg[8:0]           w_addr;                 
reg[15:0]          w_data;                 
reg                wea;            
reg[8:0]           r_addr;                 
wire[15:0]         r_data;                 

wire clk ;

   IBUFDS #(
      .DIFF_TERM("FALSE"),       // Differential Termination
      .IBUF_LOW_PWR("TRUE"),     // Low power="TRUE", Highest performance="FALSE"
      .IOSTANDARD("DEFAULT")     // Specify the input I/O standard
   ) IBUFDS_inst (
      .O(clk),  // Buffer output
      .I(sys_clk_p),  // Diff_p buffer input (connect directly to top-level port)
      .IB(sys_clk_n) // Diff_n buffer input (connect directly to top-level port)
   );

always @(posedge clk or negedge rst_n)
begin
  if(!rst_n)
       r_addr <= 9'd0;
  else if (|w_addr)                    
    r_addr <= r_addr+1'b1;
  else
       r_addr <= 9'd0;
end

always@(posedge clk or negedge rst_n)
begin
  if(!rst_n)
         wea <= 1'b0;
  else
  begin
     if(&w_addr)                       
        wea <= 1'b0;
     else
        wea    <= 1'b1;        
  end
end

always@(posedge clk or negedge rst_n)
begin
  if(!rst_n)
  begin
         w_addr <= 9'd0;
         w_data <= 16'd1;
  end
  else
  begin
     if(wea)                                   
        begin
               if (&w_addr)                    
               begin
                       w_addr <= w_addr ;      
                       w_data <= w_data ;
               end
               else
               begin
                       w_addr <= w_addr + 1'b1;
                       w_data <= w_data + 1'b1;
               end
        end
  end
end

//-----------------------------------------------------------
ram_ip ram_ip_inst (
  .clka      (clk          ),     // input clka
  .wea       (wea          ),     // input [0 : 0] wea
  .addra     (w_addr       ),     // input [8 : 0] addra
  .dina      (w_data       ),     // input [15 : 0] dina
  .clkb      (clk          ),     // input clkb
  .addrb     (r_addr       ),     // input [8 : 0] addrb
  .doutb     (r_data       )      // output [15 : 0] doutb
);

ila_0 ila_0_inst (
       .clk    (clk    ),
       .probe0 (r_data ),
       .probe1 (r_addr )
);

endmodule

FPGA 온칩 ROM 읽기 및 쓰기

FPGA 자체는 SRAM 아키텍처를 기반으로 하며 전원이 거지면 프로그램이 사라진다. FPGA 내부의 RAM 리소스를 사용하여 ROM을 구현할 수는 있지만 ROM이 아니다. 이는 전원을 켤 때마다 이미 초기화된 값을 RAM에 쓰면서 동작하는 방식이다.

ROM 초기화 파일 생성

Xilinx FPGA의 온칩 ROM은 초기화 데이터 구성을 지원한다. rom_init.coe라는 파일을 만들어서 사용해야 한다.

ROM 초기화 파일의 내용 형식은 아래와 같다.

MEMORY_INITIALIZATION_RADIX=16;     // 각 데이터를 쉼표/공백/줄바꿈으로 구분, 마지막 데이터 뒤에 `세미콜론` 추가
MEMORY_INITIALIZATION_VECTOR= 
11,
22,
33,
44,
55,
66,
77,
88,
99,
aa,
bb,
cc,
dd,
ee,
ff,
00,
a1,
a2,
a3,
a4,
a5,
a6,
a7,
a8,
b1,
b2,
b3,
b4,
b5,
b6,
b7,
b8;

ROM IP 코어

Block Memory Generator > Basic > Single Port ROM

Port A Oprtions > 필요한 데이터 width, depth 설정 > Enable Port Type : Always Enalbed

Other Oprtions > Memory Initialization: Load init file: 에서 .coe 파일을 등록해준다.

ROM 테스트 프로그램

`timescale 1ns / 1ps

module rom_test(
       input      sys_clk_p,            //system clock 200Mhz postive pin
       input      sys_clk_n,            //system clock 200Mhz negetive pin
       input rst_n                      //리셋, 액티브 로우
    );

wire [7:0] rom_data;         //ROM 읽기 데이터
reg     [4:0] rom_addr;      //ROM 입력 주소

wire sys_clk ;

IBUFDS #(
   .DIFF_TERM("FALSE"),       // Differential Termination
   .IBUF_LOW_PWR("TRUE"),     // Low power="TRUE", Highest performance="FALSE"
   .IOSTANDARD("DEFAULT")     // Specify the input I/O standard
) IBUFDS_inst (
   .O(sys_clk),  // Buffer output
   .I(sys_clk_p),  // Diff_p buffer input (connect directly to top-level port)
   .IB(sys_clk_n) // Diff_n buffer input (connect directly to top-level port)
);

// 데이터를 읽을 ROM 주소 생성
always @ (posedge sys_clk or negedge rst_n)
begin
    if(!rst_n)
        rom_addr <= 10'd0;
    else
        rom_addr <= rom_addr+1'b1;
end

// ROM 인스턴스화
rom_ip rom_ip_inst
(
    .clka   (sys_clk    ),      //inoput clka
    .addra  (rom_addr   ),      //input [4:0] addra
    .douta  (rom_data   )       //output [7:0] douta
);

// 로직 분석기 인스턴스화
ila_0 ila_m0
(
    .clk    (sys_clk),
    .probe0 (rom_addr),
       .probe1 (rom_data)
);

endmodule

AX7010 용 핀 바인딩

############## clock and reset define##################
create_clock -period 20 [get_ports sys_clk]
set_property IOSTANDARD LVCMOS33 [get_ports {sys_clk}]
set_property PACKAGE_PIN U18 [get_ports {sys_clk}]

set_property IOSTANDARD LVCMOS33 [get_ports {rst_n}]
set_property PACKAGE_PIN N15 [get_ports {rst_n}]

시뮬레이션 결과

FPGA 온칩 FIFO 읽기 및 쓰기

FIFO는 FPGA 애플리케이션에서 매우 중요한 모듈이며, 데이터 캐싱, 크로스 클럭 도메인 데이터 처리 등에 널리 사용된다. FIFO를 잘 배우는 것이 FPGA의 핵심이며, FIFO를 유연하게 사용하는 것은 FPGA 엔지니어에게 필수적인 기술이다.

Xilinx는 Vivado에 FIFO IP 코어를 제공한다. IP 코어를 통해 FIFO를 인스턴스화하고 FIFO의 읽기 및 쓰기 타이밍에 따라 FIFO에 저장된 데이터를 쓰고 읽으면 된다.

실제로 FIFO는 RAM을 기반으로 많은 기능을 추가한다. FIFO의 일반적인 구조는 주로 읽기 및 쓰기의 두 부분으로 나뉜다. 다른 하나는 상태 신호, 빈 신호 및 전체 신호이다. RAM과 관련된 데이터 상태 신호의 가장 큰 차이점은 FIFO에는 주소라인이 없으며 임의의 주소에서 데이터를 읽을 수 없다는 것. 즉, FIFO는 특정 주소의 데이터를 마음대로 읽을 수 없다. 대신 이런 구조의 장점은 주소 라인을 직접 혹은 자주 제어할 필요가 없다는 것이다.

FIFO에 데이터를 쓴 후 스기 주소가 1씩 증가하며, FIFO에서 데이터를 읽은 후 읽기 주소가 1씩 증가한다. 이때 하나의 데이터를 쓰고 다른 데이터를 읽기 때문에 FIFO 상태는 비어있게 된다.

이번 섹션에서는 읽기 클럭이 75MHz, 쓰기 클럭이 100MHz인 비동기식 FIFO의 제어를 진행한다.

FIFO 포트 정의 및 타이밍

신호명방향설명
rstin재설정 신호
wr_clkin쓰기 클럭 입력
rd_clkin읽기 클럭 입력
dinin데이터 쓰기
wr_enin쓰기 가능
rd_enin읽기 가능
doutout데이터 읽기
fulloutFIFO Full
emptyoutFIFO Empty
rd_data_countout읽을 수 있는 데이터의 양
wr_data_countout기록된 데이터의 양

위 표를 보면 FIFO 외부에 FIFO 상태를 표시하는 output이 존재한다. Xilinx FIFO는 비동기 FIFO를 지원하며, 내부적으로 비동기 클럭 도메인간 동기화 로직을 포함한다.

FIFO 데이터 쓰기 및 읽기는 모두 클럭의 상승 에지에서 동작한다.

표준 FIFO 쓰기 타이밍

wr_en 신호가 High일 때 FIFO에 데이터를 쓴다. Almost_Full 신호가 유효하면 FIFO가 한 번만 데이터를 더 쓸 수 있음을 의미한다. 이때, 한 번 더 데이터를 쓰게 되면 FIFO의 오버플로가 발생하여 full 이 High가 된다.

표준 FIFO 읽기 타이밍

rd_en 신호가 High 면, FIFO 데이터를 읽고 다음 사이클에서 부터 데이터가 들어간다. valid는 데이터 유효 신호이고, Almost_empty는 읽을 데이터가 아직 남아 있음을 나타낸다.

FWFT FIFO 읽기 타이밍

FWFT 모드 데이터 읽기 타이밍 다이어그램에서 볼 수 있듯, rd_en 신호가 유효할 때 유효한 데이터 D0은 이미 준비되어 있고 데이터 라인에서 유효하며 다른 주기로 지연되지 않는다. 이것이 표준 FIFO와의 차이점이다.

테스트 프로그램

`timescale 1ns / 1ps
module fifo_test
       (
               input      sys_clk_p,            //system clock 200Mhz postive pin
               input      sys_clk_n,            //system clock 200Mhz negetive pin
               input      rst_n                    
       );

reg [15:0]                 w_data;
wire                       wr_en;
wire                       rd_en;
wire [15:0]                r_data;        
wire                       full;
wire                       empty;
wire [8:0]             rd_data_count;                 
wire [8:0]             wr_data_count;                 

wire                           clk_100M; 
wire                           clk_75M; 
wire                           locked; 
wire                           fifo_rst_n; 

wire                           wr_clk; 
wire                           rd_clk; 
reg [7:0]                   wcnt; 
reg [7:0]                   rcnt; 

clk_wiz_0 fifo_pll
 (
  // Clock out ports
  .clk_out1(clk_100M),                 // output clk_out1
  .clk_out2(clk_75M),                  // output clk_out2
  // Status and control signals
  .reset(~rst_n),                              // input reset
  .locked(locked),                     // output locked
  // Clock in ports
  .clk_in1_p(sys_clk_p),                                       // input clk_in1
  .clk_in1_n(sys_clk_n)                                        // input clk_in1
  );

assign fifo_rst_n      = locked        ;       
assign wr_clk          = clk_100M      ;       
assign rd_clk          = clk_75M       ;       

localparam      W_IDLE         = 1;
localparam      W_FIFO         = 2;

reg[2:0]  write_state;
reg[2:0]  next_write_state;

always@(posedge wr_clk or negedge fifo_rst_n)
begin
       if(!fifo_rst_n)
               write_state <= W_IDLE;
       else
               write_state <= next_write_state;
end

always@(*)
begin
       case(write_state)
               W_IDLE:
                       begin
                               if(wcnt == 8'd79)               
                                       next_write_state <= W_FIFO;
                               else
                                       next_write_state <= W_IDLE;
                       end
               W_FIFO:
                       next_write_state <= W_FIFO;                     
               default:
                       next_write_state <= W_IDLE;
       endcase
end

always@(posedge wr_clk or negedge fifo_rst_n)
begin
       if(!fifo_rst_n)
               wcnt <= 8'd0;
       else if (write_state == W_IDLE)
               wcnt <= wcnt + 1'b1 ;
       else
               wcnt <= 8'd0;
end

assign wr_en = (write_state == W_FIFO) ? ~full : 1'b0;

always@(posedge wr_clk or negedge fifo_rst_n)
begin
       if(!fifo_rst_n)
               w_data <= 16'd1;
       else if (wr_en)
               w_data <= w_data + 1'b1;
end

localparam      R_IDLE      = 1        ;
localparam      R_FIFO         = 2     ;
reg[2:0]  read_state;
reg[2:0]  next_read_state;

always@(posedge rd_clk or negedge fifo_rst_n)
begin
       if(!fifo_rst_n)
               read_state <= R_IDLE;
       else
               read_state <= next_read_state;
end

always@(*)
begin
       case(read_state)
               R_IDLE:
                       begin
                               if (rcnt == 8'd59)              
                                       next_read_state <= R_FIFO;
                               else
                                       next_read_state <= R_IDLE;
                       end
               R_FIFO:
                       next_read_state <= R_FIFO ;                     
               default:
                       next_read_state <= R_IDLE;
       endcase
end

always@(posedge rd_clk or negedge fifo_rst_n)
begin
       if(!fifo_rst_n)
               rcnt <= 8'd0;
       else if (write_state == W_IDLE)
               rcnt <= rcnt + 1'b1 ;
       else
               rcnt <= 8'd0;
end

assign rd_en = (read_state == R_FIFO) ? ~empty : 1'b0;

//-----------------------------------------------------------

fifo_ip fifo_ip_inst
(
  .rst            (~fifo_rst_n         ),   // input rst
  .wr_clk         (wr_clk              ),   // input wr_clk
  .rd_clk         (rd_clk              ),   // input rd_clk
  .din            (w_data              ),   // input [15 : 0] din
  .wr_en          (wr_en               ),   // input wr_en
  .rd_en          (rd_en               ),   // input rd_en
  .dout           (r_data              ),   // output [15 : 0] dout
  .full           (full                ),   // output full
  .empty          (empty               ),   // output empty
  .rd_data_count  (rd_data_count       ),   // output [8 : 0] rd_data_count
  .wr_data_count  (wr_data_count       )    // output [8 : 0] wr_data_count
);

ila_m0 ila_wfifo (
       .clk(wr_clk),
       .probe0(w_data),
       .probe1(wr_en),
       .probe2(full),
       .probe3(wr_data_count)
);

ila_m0 ila_rfifo (
       .clk(rd_clk),
       .probe0(r_data),
       .probe1(rd_en),
       .probe2(empty),
       .probe3(rd_data_count)
);

endmodule

이 코드에서는 PLL의 Locked 신호를 FIFO의 재설정으로 사용하고 100MHz 클럭을 쓰기 클럭에 할당하며 75MHz 클럭을 읽기 클럭에 할당한다.

주의할 점은 FIFO 설정이 기본적으로 안전 회로(?)를 사용한다는 것이다. 이 기능은 내부 RAM에 도착하는 입력 신호가 동기식인지 확인한다. 이 경우 비동기식으로 재설정되면 60사이클 동안 기다려야 한다. 75MHz의 60 사이클이므로 100MHz 클럭에서는 대략 (100/75)x60 = 60 사이클 정도가 필요하다.

따라서 쓰기 상태 머신에서는 쓰기 FIFO 상태에 들어갈 때까지 80 사이클을 기다린다.

always@(*)
begin
       case(write_state)
               W_IDLE:
                       begin
                               if(wcnt == 8'd79)
                                       next_write_state <= W_FIFO;
                               else
                                       next_write_state <= W_IDLE;
                       end
               W_FIFO:
                       next_write_state <= W_FIFO;                     
               default:
                       next_write_state <= W_IDLE;
       endcase
end
always@(*)
begin
       case(read_state)                // 읽기 상태 머신에서 읽기 상태가 될 때까지 60사이클을 기다린다.
               R_IDLE:
                       begin
                               if (rcnt == 8'd59)              
                                       next_read_state <= R_FIFO;
                               else
                                       next_read_state <= R_IDLE;
                       end
               R_FIFO:
                       next_read_state <= R_FIFO ;                     
               default:
                       next_read_state <= R_IDLE;
       endcase
end

FIFO가 가득 차지 않으면 FIFO에 계속 데이터를 쓴다. FIFO가 비어 있지 않으면 FIFO에서 데이터를 계속 읽는다.

assign wr_en = (write_state == W_FIFO) ? ~full : 1'b0;
assign rd_en = (read_state == R_FIFO) ? ~empty : 1'b0;