Post

[FPGA] Alchitry Au - DDR 메모리 예제

DDR3 메모리와 캐시를 사용하여 카운터를 구현한다.

[FPGA] Alchitry Au - DDR 메모리 예제

1. DDR(Double Data Rate) 메모리

DDR 메모리는 동기식 DRAM의 일종으로 클럭 주파수를 기준으로 두배의 데이터를 전송할 수 있다. Alchity Au 보드는 Artix-7 FPGA 칩을 사용한다. 이 Artix-7 FPGA는 내부 메모리 컨트롤러를 포함하고 있어, DDR3와 같은 메모리 모듈을 인터페이스할 수 있다. Vivado에서는 이를 활용하기 위해서 IP Catalog를 제공하지만 IP를 사용하기 위해서는 DDR3과 보드의 핀아웃(inout)에 대한 지식이 필요하다. 하지만 Alchitry lab을 사용하면 간단한 지식만으로도 조작이 가능하다.

IP(Interllectual Property) Catalog?
FPGA 설계를 쉽게 할 수 있도록 미리 정의해둔 하드웨어 모듈(IP)들의 라이브러리이다.

1-1. DDR 메모리 추가하기

코어 생성하기

먼저 Base Project 템플릿으로 새 프로젝트를 생성한다. 그리고 [Add Component] - [Generate MIG core (DDR)]를 통해 프로젝트에 Xilinx의 메모리 인터페이스를 세팅한다. 이 옵션은 alchitry au 보드를 사용할 때에만 활성화된다.

코어 생성 완료

그러면 파일 트리에 mig_7series_0이 생긴 것을 확인할 수 있다. 이는 메모리 인터페이스를 추가하기 위한 필수 명령어들이 프로젝트에 추가되었음을 의미하며, 이 파일은 FPGA와 DDR3 메모리 모듈 사이의 연결을 정의한 verilog 파일이다.

이 파일을 직접 사용하는 것도 괜찮지만, 이를 상위 계층의 모듈로 한번 더 감싸 사용하는 것이 더욱 편리하다.

[Add Component] - [Component Library] - [Memory] - [MIG Wrapper (DDR3)]를 추가한다. 그러면 두 개의 전역변수 구조가 정의된 mig_wrapper.luc가 생성된다. 또, 내부에 ddr3_로 시작하는 수많은 input과 output이 정의되어 있는데 이는 모두 DDR3 칩과 연결하기 위해 필요한 신호들이다. 이 신호들의 이름은 mig_7series_0 core에 실제로 있는 이름을 사용하기 때문에 이름을 수정하면 안된다.

1-2. 클록 세팅하기

메모리 인터페이스는 100MHz, 200MHz 클록 필요하다. 하지만 au보드가 100MHz를 기본으로 가지고 있으므로 200MHz만 생성하면 된다. clk_wiz_0은 클럭 생성기로 100MHz의 클럭을 입력으로 받아서 100MHz와 200MHz를 출력한다.

클록을 생성하기 위해서 [Add Component] - [Vivado IP Catalog]를 클릭해서 IP Catalog를 연다.

ip 카탈로그

이후 창 오른쪽에서 [IP Catalog] - [FPGA Features and Design]- [Cloking] -[Cloking Wizard]를 더블 클릭하여 새 창을 연다.

[Cloking Options]에서 Input clock Information의 Primary의 이름을 clk_in으로 수정해준다.

출력 클록 추가

또, [Output Clocks]를 클릭해 clk_out2를 200MHz로 추가해주고 reset의 타입을 ‘Active low’로 수정한다. Ok를 눌러 창을 닫은 후, 이후 열리는 창에서도 generate를 선택해준다.

클록 추가

그러면 위 이미지처럼 clk_wiz_0가 추가된다. 그리고 vivado 창을 닫고 alchitry lab을 보면 core 아래에 clk_wiz_0 파일이 추가된 것을 확인할 수 있다.

1-3. 명령 인터페이스 사용하기

core를 사용하기 위해서는 mig_wrapper 모듈의 전역 구조 변수만을 고려하면 된다. in에서 cmd(실제는 app_cmd를 의미)는 3비트의 크기를 가지지만 실제로 쓰이는 값음 0(쓰기), 1(읽기) 뿐이다.

쓰기는 FIFO인데, FIFO에 공간이 있으면 wr_rdy가 1로 활성화되며, 이때 we_enable을 1로 설정하여, wr_data에 적힌 데이터를 버퍼에 기록한다. 이때 wr_mask의 값에 따라 wr_data에서 해당되는 바이트만큼을 무시한다. 예를 들어, wr_mask16hFFFF로 설정하면 wr_data에 저장된 16바이트를 모두 무시하여 아무런 데이터도 전송되지 않는다.

읽기는 새 데이터가 준비되면 rd_valid를 1로 설정하고 읽은 데이터는 rd_data에 저장된다.

명령 인터페이스를 사용하기 위해서는 rdy를 1로 설정하고 이후 cmd를 0(쓰기) 또는 1(읽기)로, 관련 주소를 addr에 입력하고, enable를 1로 한다.

이처럼 쓰기 인터페이스, 읽기 인터페이스, 명령 인터페이스가 각각 독립적으로 존재하는 이유는 효율성 때문이다. 읽기가 수행되는 동안 쓰기가 실행될 수도 있고 읽을 데이터가 준비되기 전에 다른 명령어를 받을 수도 있다. 여러 명령이 수행되는 동안 또 다른 명령어들이 생성될 수 있지만, 이들은 입력되어진 순서대로 명령어들을 수행한다.

2. DDR을 활용한 카운터 예제

DDR3가 연속적인 값을 저장하고 읽어서 LED에 보여주는 예제를 수행해본다.

1
2
3
4
5
6
7
8
9
10
enum State {WRITE_DATA, WRITE_CMD, READ_CMD, WAIT_READ, DELAY}

.clk(mig.ui_clk) {
    .rst(rst) {
        dff state[$width(State)]
        dff ctr[24]
        dff address[8]
        dff led_reg[8]
    }
 }

먼저 상태에 대해서 정의한다. WRITE_DATA는 데이터를 기록할 준비를 위한 상태, WRITE_CMD는 쓰기 명령을 활성화하는 상태, READ_CMD는 읽기 명령어를 활성화하는 상태, WAIT_READ는 읽기가 처리되는 상태, DELAY는 읽은 데이터를 LED에 값이 잠시동안 보이도록 하는 상태이다. 그리고 상태 전환을 위한 dff와 LED 인지 시간, 주소, LED 불빛 표현을 위해 각각의 dff를 정의해준다.

1
2
3
4
5
6
7
8
mig.mem_in = <Memory.in>(
    .enable(0), 
    .cmd(3bx),
    .addr(28bx),
    .wr_data(128bx),
    .wr_mask(0),
    .wr_enable(0)
)

mig 모듈의 mem_in 구조체에서 모든 바이트를 사용할 예정이므로 wr_mask는 으로 설정한다. 다른 값들은 enable이 0일때 아무런 영향을 주지 않으므로 불확적 값을 나타내는 bx로 설정한다.

1
2
3
4
5
State.WRITE_DATA:
    mig.mem_in.wr_enable = 1
    mig.mem_in.wr_data = address.q
    if (mig.mem_out.wr_rdy)
        state.d = State.WRITE_CMD

먼저 WRITE_DATA 상태일 때에는 값을 쓰기 FIFO에 저장하고, 저장이 완료되어 wr_enablewr_rdy가 모두 1이면, WRITE_CMD 상태로 전환된다.

1
2
3
4
5
6
7
8
9
10
11
12
 State.WRITE_CMD:
    mig.mem_in.enable = 1
    mig.mem_in.cmd = 0 // 0 = write
    mig.mem_in.addr = c{address.q, 3b000} // first three bits of addr are for the 8 words in wr_data
    if (mig.mem_out.rdy) {
        address.d = address.q + 1
        state.d = State.WRITE_DATA
        if (address.q == 8hFF) {
            state.d = State.READ_CMD
            address.d = 0
        }
    }

이 상태에서는 쓰기 명령을 내보낸다. 이때 DDR3는 16비트 버스이고, 읽기/쓰기 작업은 128비트 블록 단위로 수행되기 때문에 8번의 16비트가 필요하다. 이 항목들이 연속적인 주소에 저장될 때에, 순서를 구분하기 위해서는 하위 3비트를 필요로 한다. (1~8까지를 구분하는 표현하는 3비트) 따라서 mig.mem_in.addr = c{address.q, 3b000}를 통해 하위 3비트를 0으로 설정한다.

256개의 주소가 모두 쓰여지면 READ_CMD 상태로 전환되며 읽기 명령을 발행한다.

1
2
3
4
5
6
State.READ_CMD:
    mig.mem_in.enable = 1
    mig.mem_in.cmd = 1 // 1 = read
    mig.mem_in.addr = c{address.q, 3b000}
    if (mig.mem_out.rdy)
        state.d = State.WAIT_READ

WAIT_READ 상태에서는 rd_valid가 1이 될때까지 기다린 후, LED의 상태를 설정한다.

1
2
3
4
5
6
State.WAIT_READ:
    if (mig.mem_out.rd_valid) {
        led_reg.d = mig.mem_out.rd_data[7:0]
        state.d = State.DELAY
        address.d = address.q + 1
    }

마지막으로 DELAY 상탱서는 LED의 값이 인지되도록 일정 시간이 흐른 후, 다시 READ_CMD 상태로 돌아온다. 이는 DDR3 첫 256개의 주소를 계속해서 읽게 된다.

3. Cache 예제

1
mig.mem_in.wr_data = address.q;  // address.q는 8비트 값

DDR 카운터에서 코드를 자세히 살펴보면 128비트를 저장할 수 있는 wr_data에서 8비트만을 사용한다. 이는 데이터가 매우 낭비되는 비효율적인 방식이다. 기존에는 128비트를 8개의 16비트에 저장했지만 이번엔 16개의 8비트에 저장해보자. 그러면 값이 달라지는 첫 8비트만을 쓰거나 읽어오면 되므로, 기존의 방식보다 빠르게 처리할 수 있다.

LRU Cache(Least Recently Used Cache)는 자주 사용되는 데이터를 빠르게 접근할 수 있도록 도와는 캐시로, 메모리 읽기 및 쓰기 성능을 최적화하는데 유용하다.

3-1. 캐시 추가하기

[Component Library] - [Memory] - [LRU Cache]로 LRU 캐시 모듈을 추가해준다.

이 캐시는 필요할 때만 외부 RAM에 접근하는 lazy하게 동작한다. 캐시는 독립적인 읽기와 쓰기 인터페이스를 제공하기 때문에 다른 사이클에서 읽기와 쓰기를 분리해서 수행해야 한다. 캐시의 특정 항목에 대해 읽거나 쓰기 작업이 수행되면, 해당 항목의 나이는 0이 되고, 나머지 모든 항목의 나이를 1씩 증가한다. 즉, 나이가 많은 항목일수록 가장 오래동안 쓰이지 않은 항목이 된다. 만약 새로운 캐시 공간이 필요하게 되면, 가장 오래된 항목을 추방하는 정책을 따른다. 항목의 가능한 최대 나이는 AGE_BITS로 설정하며, 이 값이 너무 작으면 나이가 빨리 포화되므로 캐시의 LRU 동작을 보장하기 어렵다. 하지만 일반적으로 사용에 따라 제일 오래된 항목이 제거되므로 해당 문제는 잘 발생하지 않는다. 현재 예시에서는 ENTRIES(캐시 항목 수)를 1로 설정하였기 때문에 AGE_BITS 또한 1로 설정하였다.

WORD_SIZE는 캐시가 읽거나 적을 데이터의 크기를 지정한다. 쓰기는 wr_ready가 1 일때, wr_data의 값을 wr_addr에 적힌 주소에 적은 후 wr_valid를 1로 설정한다. 읽기의 경우에는 rd_ready가 1일때, rd_addr에 읽을 주소를 적고, rd_cmd_valid를 1로 설정한다. 그 후, rd_data_valid가 1이 되면, rd_data를 읽어온다.

변경된 캐시의 값을 반대로 실제 메모리인 DDR3에 반영하기 위해서는 flush_ready를 1로 설정해야 한다.

4. 결과

프로젝트 빌드 후 보드에 로드하면 LED의 불빛이 카운트하듯이 변경된다.

5. 최종 코드 파일

ddr counter

ddr counter with cache

참고

Alchitry의 DDR3 Memory

This post is licensed under CC BY 4.0 by the author.