Post

[FPGA] Alchitry Au - RAM 예제

시리얼 통신으로 데이터를 송/수신하고 RAM 사용법에 대해 배운다.

[FPGA] Alchitry Au - RAM 예제

이전 예제에서는 “h”가 수신될 때, 보드는 “Hello World”를 출력하였다. 이번엔 이 예제를 조금 수정해, 이름을 포함시켜 인사하도록 하자.

프로젝트 클론

이전 프로젝트를 열고 [Menu]- [Save Project As…]하면 동일한 새로운 프로젝트를 생성할 수 있다.

1. RAM(Random Access Memory)

RAM은 데이터를 임시로 저장하고 빠르고 읽고 쓸 수 있는 메모리 장치이다.

심플 램

먼저 이름을 보관할 RAM 컴포넌트를 생성해주자. 해당 컴포넌트는 lucid 언어가 아닌 verilog로 작성되어 있다. FPGA에는 실제 RAM(BRAM)이 존재하기에 만약 작성된 RAM이 충분히 크다면, FPGA는 실제 RAM을 사용하여 이를 구현한다. RAM은 이전 예제의 ROM과 비슷하게 작동하나 읽기 뿐만 아니라 주소에 값을 쓸 수도 있다. write_enable이 1이면, write_data에 저장된 데이터를 해당 주소에 적는다.

WIDTH는 각 항목의 크기를 지정하고, ENTRIES는 항목의 개수를 설정한다. 두 값의 default는 1로 설정되어 있다.

2. Greeter 모듈 수정하기

2-1. ROM 제거하기

greeter 모듈에서 ROM 모듈을 제거하고 대신에 constant 변수에 값을 저장한다.

1
2
const HELLO_TEXT = $reverse("\r\nHello @!\r\n") // reverse so index 0 is the left most letter
const PROMPT_TEXT = $reverse("Please type your name: ")

이전에 예제에선 TEXT를 거꾸로 입력했던 것을 기억할 것이다. 이번에는 $reverse() 함수를 사용하여 더 쉽게 이를 처리하도록 한다. 문자열의 각 캐릭터는 HELLO_TEXT[i]와 같이 인덱스로 접근이 가능하다. @는 이름을 삽입할 위치를 알려주는 기호이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
enum States {IDLE, PROMPT, LISTEN, HELLO}

.clk(clk) {
    .rst(rst) {
        dff state[$width(States)]   // our state machine
    }

    dff hello_count[$clog2($width(HELLO_TEXT, 0))]
    dff prompt_count[$clog2($width(PROMPT_TEXT, 0))]
    dff name_count[5]    // 5 allows for 2^5 = 32 letters

    simple_ram ram (#WIDTH(8), #ENTRIES($pow(2,$width(name_count.q))))
}

이전 프로젝트에서 FSM의 상태를 저장했던 것처럼 시작 상태를 IDLE, 이름을 물어보는 상태를 PROMPT, 이름을 듣는 상태를 LISTEN 그리고 마지막으로 인사와 이름을 출력하는 HELLO 상태로 분류하자. PROMPT와 HELLO의 경우에는 우리가 몇 개의 캐릭터를 출력했는지 추적하기 위해 카운터가 각각 필요하다. 그리고 입력할 이름의 저장하기 위해서 LISTEN에 대한 카운터(name_count)도 필요하다.

TEXT의 $width(TEXT, 0)은 전체 문자의 수, $width(TEXT, 1)은 각 문자 크기인 8비트를 반환한다. 그리고 name_counter의 크기를 5로 설정했으므로 ram의 ENTRIES도 2^5인 32로 설정한다.

2-3. 기본값 설정

1
2
3
4
5
6
7
8
9
always {
    ram.address = name_count.q // use name_count as the address
    ram.write_data = 8hxx      // don't care
    ram.write_enable = 0       // read by default

    new_tx = 0                 // default to no new data
    tx_data = 8hxx             // don't care\
    ...
}

name_count의 q가 증가할 때마다 이름의 다음 캐릭터를 출력하므로 RAM의 주소가 곧 name_count.q와 동일하다. 아직 RAM에 기록되면 안되므로 write_enable을 0으로, 출력 new_tx 또한 0으로 비활성화한다.

2-2. FSM 상태에 따라 행동 정의하기

LISTEN 상태일 때는 사용자가 입력하는 이름을 듣고 RAM에 기록해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
States.LISTEN:
    if (new_rx) { // wait for a new byte
        ram.write_data = rx_data       // write the received letter to RAM
        ram.write_enable = 1               // signal we want to write
        name_count.d = name_count.q + 1  // increment the address
 
        new_tx = rx_data != "\n" && rx_data != "\r" // only echo non-new line characters
        tx_data = rx_data // echo text back so you can see what you type
 
        // if we run out of space or they pressed enter, change state
        if (&name_count.q || rx_data == "\n" || rx_data == "\r") {
            state.d = States.HELLO
            name_count.d = 0  // reset name_count
        }
    }

new_rx가 활성화되어 새로운 데이터가 들어오면, 해당 데이터를 RAM에 저장한다. 또한 입력한 이름을 송신측에서도 볼 수 있도록 다시 전송해준다. 즉, new_tx를 1로 활성화하고, tx_data에 rx_data를 전달해준다.

그리고 모든 이름이 입력되어 사용자가 enter(\n)가 입력하거나, 이름을 저장할 공간이 부족한 경우에 상태를 HELLO로 이동한다.

HELLO 상태일 때는 HELLO_TEXT를 출력하고 RAM에 저장되어 있는 이름을 함께 출력하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
States.HELLO:
    if (!tx_busy) { // wait for tx to not be busy
        if (HELLO_TEXT[hello_count.q] != "@") { // if we are not at the sentry
            hello_count.d = hello_count.q + 1    
            new_tx = 1                           // new data to send
            tx_data = HELLO_TEXT[hello_count.q]  
        } else {                                // we are at the sentry
            name_count.d = name_count.q + 1      // increment the name_count letter
 
            if (ram.read_data != "\n" && ram.read_data != "\r") // if we are not at the end
                new_tx = 1
 
            tx_data = ram.read_data             // send the letter from the RAM
 
            // if we are at the end of the name or out of letters to send
            if (ram.read_data == "\n" || ram.read_data == "\r" || &name_count.q) {
                hello_count.d = hello_count.q + 1  // increment hello_count to pass the sentry
            }
        }
 
        // if we have sent all of HELLO_TEXT
        if (hello_count.q == $width(HELLO_TEXT, 0) - 1)
            state.d = States.IDLE // return to IDLE
    }

tx_busy가 0인 즉, 어떠한 데이터도 전송하고 있지 않을 때 전송을 시작한다. HELLO_TEXT를 전송하기 위해서 hello_count 카운터를 사용하고, RAM에 저장된 이름을 전송하기 위해선 name_count 카운터를 사용한다. HELLO_TEXT를 전송 중에 ‘@’를 만나게 되면 else문에서 RAM의 내용을 출력한다. RAM의 내용을 모두 출력한 후에야 hello_count의 값을 증가시켜 모든 출력을 종료한다. 그리고 IDLE 상태로 전환한다.

IDLEPROMPT는 각각 초기 상태, 이름 질문을 출력을 하는 상태이다 따라서 이전 프로젝트의 IDLE과 GREET와 거의 동일하나, 다음 상태를 각각 PROMT, LISTEN으로 설정하면 된다.

3. 프로젝트 실행하기

alchitry_top.luc는 이전 프로젝트와 동일하게 사용하면 된다.

3-1. 결과

입력한 이름을 인사 후, 그대로 출력해준다. 이름을 32자 이상 입력하면 이를 끊고 인사를 한다.

ex5 결과

3-2. 최종 코드 파일

ex5 greeting with name 예제의 코드 링크

참고

Alchitry의 Hello YOUR_NAME_HERE

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