FPGA 프로젝트를 하다 보면 소프트코어 CPU를 올리거나 커스텀 가속기를 붙이면서 메모리 구조를 어떻게 설계할지 계속 부딪치게 된다. 특히 플래시 메모리와 BRAM을 어디에 어떻게 배치할지가 성능과 리소스 사용량을 동시에 결정하는 지점처럼 느껴진다. 그래서 최근에 실험하고 정리했던 내용을 기준으로, 두 메모리의 특성과 활용 패턴을 한 번에 정리해 두기로 했다.

실습 환경은 Zynq 계열 SoC FPGA와 순수 FPGA(Artix-7 계열)를 혼용했고, 툴은 Vivado와 Vitis, 일부는 Quartus 기반 디자인을 참고했다. 세부 파라미터는 벤더마다 조금씩 다르지만, 아키텍처적인 관점에서는 공통점이 많다는 점을 전제로 정리한다.

메모리 계층과 역할 분리

먼저 FPGA 설계에서 플래시와 BRAM이 맡는 역할을 한 문장으로 요약하면 다음과 같이 정리된다. 플래시는 비휘발 부트/이미지 저장소이고, BRAM은 클럭 도메인 안에서 움직이는 연산용 로컬 메모리에 가깝다. 둘 다 “메모리”이지만, 역할을 섞어 쓰는 순간 타이밍과 대역폭에서 애매한 타협을 하게 된다.

일반적인 SoC 설계 관점에서 보면, 외부 플래시는 boot ROM + 프로그램 이미지 + 설정값 저장에 대응하는 레이어에 위치한다. 반면 BRAM은 L1/L2 캐시와 scratchpad RAM의 중간 어디쯤에 놓인 전용 버퍼에 가깝다. 이 관점으로 보면, 어떤 데이터를 어디에 두어야 하는지가 조금 더 명확해진다.

이제 각각의 특성을 더 구체적으로 살펴본 뒤, 실제 설계에서 어떻게 나누어 쓰는 것이 합리적인지로 이어 가기로 했다.

플래시 메모리 특성

FPGA에서 사용하는 플래시 메모리는 대체로 SPI/QSPI NOR 플래시이거나, 일부 보드에서는 eMMC, NAND가 붙어 있는 구조다. 이 글에서는 FPGA 구성 비트스트림과 펌웨어를 담는 용도로 가장 흔히 쓰이는 NOR 계열 플래시를 기준으로 본다.

플래시는 기본적으로 비휘발(non-volatile) 이기 때문에 전원을 꺼도 비트스트림과 소프트웨어 이미지를 그대로 유지한다. 이 덕분에 FPGA는 부팅 시 플래시에서 비트스트림을 읽어와 스스로를 구성(bitstream configuration)하고, 이어서 같은 플래시에서 소프트코어 CPU용 펌웨어 이미지나 Linux 부트로더를 가져온다1.
문제는 액세스 특성이 “코드 저장소”에 가깝다는 점이다. 읽기 속도는 시퀀셜 접근에 최적화되어 있고, 쓰기/지우기 단위가 크며, 쓰기 횟수 수명 제한이 존재한다. 즉, 자주 바뀌는 상태 데이터나 고대역폭 스트림 데이터를 플래시에 올려 두고 실시간으로 읽고 쓰는 것은 구조적으로 맞지 않는다.

또 하나의 현실적인 제약은 인터페이스 폭과 레이턴시다. QSPI를 기준으로 보면, 데이터 라인이 넓어져도 내부 명령 시퀀스와 페이지 경계, erase 블록 등으로 인해 지속 대역폭은 DRAM이나 BRAM에 비해 크게 낮고, 랜덤 액세스 레이턴시는 더 크다. 캐시가 있는 소프트코어 CPU에서 “코드 fetch” 용도로는 충분하지만, 스트리밍 필터의 line buffer 용도로 쓰기에는 맞지 않는 패턴이다.

이 특성을 종합하면, 플래시는 다음과 같은 용도에 적합한 것으로 보인다.

  • 비트스트림과 펌웨어 이미지 저장
  • 부팅 시 한 번 읽어서 DRAM/BRAM으로 옮기는 상수 데이터
  • 자주 바뀌지 않는 설정값(파라미터, 테이블)을 위한 비휘발 저장소

이제 연산 경로 안에서 사용되는 메모리 역할을 살펴보기 위해 BRAM 특성으로 넘어간다.

BRAM 특성

BRAM(Block RAM)은 FPGA 패브릭 내부에 있는 동기식(on-chip) 메모리 블록이다. LUT로 구현한 분산 RAM과 달리, 독립된 메모리 매크로 블록으로 제공되며, 보통 단일 클럭 도메인에서 1~2 클럭 레이턴시로 접근 가능한 동기식 RAM으로 모델링된다2.

BRAM의 가장 큰 특징은 지터가 없는 일정한 레이턴시넓은 인터페이스 폭이다. 주소를 제시하고 한두 클럭 뒤에 결과가 나오는 동작을 보장할 수 있기 때문에 파이프라인 설계와 타이밍 분석이 단순해진다. 특히 FIR 필터, 라인 버퍼, FIFO, 스크래치패드 등 데이터 경로에 직접 연결되는 메모리 용도로 적합하다.

대신 용량은 제한적이다. 중저가 디바이스에서는 수백 kB에서 수 MB 단위 수준이고, 대형 디바이스를 써도 수십 MB가 한계인 경우가 많다. 이 때문에 “모든 것을 BRAM에 넣겠다”는 전략은 곧바로 리소스 부족으로 이어진다. 결국 어떤 데이터를 BRAM에 올리고, 어떤 데이터는 외부 DRAM이나 플래시에 남길지 선택하는 문제가 핵심이 된다.

브랜드/디바이스마다 세부 옵션은 다르지만, 공통적으로 다음과 같은 기능이 제공되는 편이다.

  • True dual-port RAM (동시에 두 포트에서 접근 가능)
  • Byte enable, write enable을 통한 부분 쓰기
  • 초기값을 비트스트림에 포함하여 configuration 시점에 자동 초기화

이 기능들 덕분에 BRAM은 단순한 배열 저장소가 아니라, 클럭 도메인 내부에서 움직이는 소형 버퍼/캐시 역할까지 담당할 수 있다. 다음 섹션에서 플래시와의 역할 분리를 구체적인 패턴으로 묶어 본다.

플래시와 BRAM의 역할 분리 패턴

플래시와 BRAM을 설계 상에서 어떻게 나눠 쓰는지가 실제 프로젝트 유지보수성을 크게 좌우하는 듯하다. 실험과 기존 레퍼런스를 보면 대략 다음과 같은 패턴으로 정리된다.

첫째, 부팅과 구성(configuration) 경로는 플래시 중심으로 설계한다. 비트스트림, 부트로더, OS 이미지, 애플리케이션 바이너리는 모두 플래시에 두고, 부팅 시점에 필요한 부분만 DRAM이나 BRAM으로 옮겨 온다. 이 경로는 대역폭보다 안정성과 재현성을 우선하는 경로다.

둘째, 실시간 데이터 경로(real-time datapath) 에서 사용하는 버퍼는 BRAM으로 한정한다. 예를 들어 영상 처리 파이프라인의 line buffer, 필터 계수 저장, DMA 엔진의 descriptor ring, 패킷 버퍼의 헤더 캐시는 모두 BRAM 쪽에 두는 것이 타이밍 분석과 지터 측면에서 유리하다. 외부 DRAM을 사이에 두는 구조도 가능하지만, 이 경우에는 컨트롤러의 상태에 따라 변동 레이턴시가 생긴다는 점을 염두에 두어야 한다.

셋째, 플래시와 BRAM을 연결하는 완충 층으로서 DRAM이나 AXI 인터페이스를 두는 구조가 자주 쓰인다. 플래시에서 DRAM으로 대량 블록을 옮기고, 실제 연산은 DRAM+BRAM 조합으로 처리하는 방식이다. 이 구조에서는 BRAM이 DRAM에 대한 일종의 L1 캐시 또는 scratchpad로 동작하며, CPU나 DMA가 BRAM–DRAM 사이를 명시적으로 데이터 이동시키는 패턴이 된다3.

이렇게 역할을 나누어 두면, 어느 시점에 어느 메모리를 확장해야 하는지 판단하기도 쉬워진다. 다음으로, 설계 시 자주 마주치는 구체적인 트레이드오프를 정리해 본다.

레이턴시와 대역폭 관점의 비교

레이턴시 관점에서 보면 BRAM은 사실상 고정된 파이프라인 단계 하나로 모델링할 수 있다. 읽기 요청을 넣고 한두 클럭 뒤에 결과가 나오는 구조이기 때문에, 레지스터와 크게 다르지 않은 추론이 가능하다. 이 덕분에 state machine이나 파이프라인 스테이지를 설계할 때, “여기에서 한 사이클 늦게 값을 받는다”는 가정을 그대로 타이밍 다이어그램에 반영할 수 있다.

반면 플래시는 인터페이스 및 컨트롤러 계층이 포함되어 있기 때문에, 레이턴시가 외부 요인에 더 민감하게 반응한다. 명령 전송, 주소 전송, 더미 클럭, 페이지 캐시, 컨트롤러 내부 버퍼링 등 여러 층이 얽혀 있다. 실제로는 QSPI 플래시에서 읽기 캐시를 잘 설계하면 시퀀셜 읽기에서 꽤 준수한 대역폭을 얻을 수 있지만, 랜덤 읽기나 작은 블록 단위 접근에서는 BRAM과 비교할 수 있는 수준이 아니다4.

대역폭 측면에서도 BRAM은 설계자가 직접 포트 폭과 클럭을 설정해 줌으로써 실질적인 처리량을 제어할 수 있다. 예를 들어 128비트 폭에 200MHz 클럭을 사용하면 이론상 3.2GB/s 수준의 단일 포트 대역폭을 얻을 수 있다. 반면 QSPI 플래시는 핀 수와 프로토콜에 묶여 있어 수백 MB/s 단위에서 멈추는 것이 일반적이다.

따라서, 레이턴시와 대역폭을 모두 중요하게 보는 연산 경로는 BRAM 쪽에 올리고, 용량이 크면서 업데이트 빈도가 낮은 데이터는 플래시에 두고 부팅 시 옮기는 구조가 현실적인 타협점으로 보인다.

설계 시 체크해야 할 포인트

실제 프로젝트에서 플래시와 BRAM을 어떻게 배분할지 결정할 때는 몇 가지 질문을 먼저 던져 보는 것이 도움이 된다.

첫째, 이 데이터는 전원을 꺼도 유지되어야 하는가. 유지되어야 한다면 플래시에, 아니면 BRAM/DRAM 쪽으로 옮기는 후보가 된다. 파라미터 테이블이라도 리셋마다 다시 계산할 수 있는 값이라면 굳이 플래시에 올려 둘 필요는 없다.

둘째, 이 데이터는 얼마나 자주, 어떤 패턴으로 접근되는가. 매 클럭마다 읽고 쓰는 스트림 데이터는 BRAM에 두는 것이 타이밍 여유와 디버깅 편의성 모두에서 유리하다. 반면 몇 초에 한 번 읽는 설정값이라면 플래시에서 직접 읽어 오거나, 부팅 시 한 번만 DRAM으로 복사해도 충분하다.

셋째, 이 데이터의 전체 용량과 확장 가능성은 어떠한가. BRAM 용량이 빠듯한 디자인에서 대규모 테이블을 BRAM에 올리면, 이후에 파이프라인을 추가할 때 리소스가 부족해질 수 있다. 이런 경우에는 테이블을 플래시에 두고, 현재 동작에 필요한 일부만 BRAM 캐시로 가져오는 구조를 설계하는 편이 유지보수에 낫다.

이 질문들을 통과한 뒤에는, 구체적인 구현에서 AXI 인터커넥트, DMA 엔진, 캐시 정책을 어떻게 설정할지의 문제로 자연스럽게 내려갈 수 있다.

마치며

최근 FPGA 디자인들을 살펴보면, 플래시는 점점 더 “이미지 저장소”에 가깝게, BRAM은 “연산을 위한 적극적인 메모리”에 가깝게 사용되는 경향이 뚜렷해 보인다. 개인적으로는 비휘발성을 요구하는 데이터와 실시간성을 요구하는 데이터를 처음부터 분리해서 모델링해 두면, 설계 변경이 생겨도 메모리 구조를 다시 뒤엎을 일이 줄어드는 듯하다.

다음에는 BRAM 대신 울트라 RAM(URAM)이나 HBM 같은 다른 온칩/오프칩 메모리를 조합했을 때 어떤 계층 구조를 잡을 수 있는지까지 확장해서 비교해 볼 계획이다.

References

  1. Xilinx UG470 - 7 Series FPGAs Configuration - 7시리즈 FPGA의 플래시 기반 구성 흐름 정리 문서 

  2. Intel FPGA Memory Resources User Guide - BRAM/MLAB 구조와 타이밍 특성 설명 

  3. Zynq-7000 SoC Technical Reference Manual - 플래시, DDR, On-Chip Memory를 포함한 메모리 맵 구조 

  4. Spansion/Cypress Serial NOR Flash Datasheet - QSPI 플래시의 대역폭·레이턴시 특성 예시