FPGA 실전 설계 – BRAM 초기화 가이드

저전력 AI 반도체 아키텍처를 FPGA에 올리기 위해 가장 먼저 마주하는 난관 중 하나는 바로 ‘메모리 설계’입니다. 특히 MAC(Multiply-Accumulate) 연산기에 들어갈 방대한 가중치(Weight) 데이터를 BRAM에 효율적으로 적재하는 것은 전체 시스템의 개발 속도와 유연성을 결정짓는 매우 중요한 작업입니다.

보통 자일링스(Xilinx) Vivado 환경을 처음 접하면 Block Memory Generator IP를 생성하고 .coe 파일을 연결하는 방식을 배웁니다. 하지만 모델이 업데이트 되고 FPGA bit를 합성할 때마다 가중치가 바뀌는데, 매번 IP를 다시 생성(Regenerate)하고 합성해야 한다면 어떨까요? 설계가 고도화될수록 이 방식은 엄청난 비효율을 낳습니다.

이번 글에서는, 실제 엣지 디바이스용 AI NPU를 FPGA로 설계하며 직접 부딪히고 깨달은 효율적인 BRAM 초기화와 가장 확실한 하드웨어 레벨의 검증 팁 3가지를 공유해보겠습니다.

1. GUI와 .coe 파일에서 벗어나기: $readmemh의 활용

하드웨어 설계도 소프트웨어처럼 유연해야 합니다. 파이썬(Python) 환경에서 훈련된 가중치를 64-bit Hex 포맷으로 추출한 뒤, 이를 .mem 파일로 저장하여 RTL(SystemVerilog) 코드 단에서 직접 불러오는 방식이 훨씬 우수합니다.

// BRAM으로 추론될 메모리 배열 선언
(* ram_style="block" *) logic [63:0] ram [0:1023];

initial begin
    $readmemh("weights_data.mem", ram);
end

실무 트러블슈팅 팁:

파일 경로를 동적으로 할당하기 위해 파라미터로 ‘string’ 타입을 사용할 경우, Vivado 합성 단계에서 [Synth 8-27] 에러가 발생하며 툴이 멈추거나 파일 경로를 인식하지 못하는 경우가 잦습니다.

Vivado는 합성 시점에서 동적 문자열 처리에 취약합니다. 이를 해결하려면 파일 이름을 파라미터로 넘기지 말고 모듈 내부에 하드코딩된 로컬 경로를 사용하거나, logic [8*MAX_CHARS-1:0]과 같은 넓은 비트 폭의 배열로 묶어서 전달하는 것이 안전합니다.

2. BRAM 추론(Inference) 실패의 주범: 메모리 배열의 Reset

코드를 완벽하게 작성했다고 생각했는데, 합성 결과를 열어보면 BRAM이 아니라 수천 개의 LUT와 Flip-flop으로 도배되어 있는 끔찍한 상황을 마주할 때가 있습니다. 이 경우 높은 확률로 메모리 배열 자체에 리셋(Reset) 신호를 연결했기 때문입니다.

FPGA 내부의 물리적인 BRAM 프리미티브(예: RAMB36E2) 코어는 단 한 클럭 사이클 만에 전체 메모리 셀을 0으로 초기화하는 하드웨어적인 비동기 리셋 기능을 지원하지 않습니다.

해결 방법:

  • 데이터가 저장되는 메모리 배열(ram) 자체에는 비동기/동기 리셋을 절대 적용하지 마세요.
  • 대신 데이터가 밖으로 나가는 출력 레지스터(Output Register)에만 리셋 로직을 적용해야 합니다.
  • 추가로, Vivado에게 명시적으로 “이 배열은 무조건 BRAM으로 만들어라”라고 지시하기 위해 배열 선언부 앞에 (* ram_style=”block” *) 속성을 반드시 부여하세요.

3. Cell Properties로 1초 만에 초기화 완벽 검증하기

초기값 로딩 로직을 짰다면, 이 데이터가 실제 FPGA 비트스트림(Bitstream)에 잘 박혔는지 의구심이 듭니다. 파일 경로 문제로 실제 하드웨어에서는 데이터가 누락되는 일이 흔하기 때문입니다. 이를 확인하기 위해 Post-Implementation Functional Simulation을 돌리는 분들이 많습니다.

하지만 Versal 시리즈처럼 거대하고 복잡한 최신 디바이스의 경우, 이 시뮬레이션 자체가 Vivado 툴에서 제한적으로만 지원되어 Module not found 에러를 뿜으며 실행조차 안 될 때가 많습니다. 이럴 때는 시뮬레이션을 돌릴 필요가 없습니다. 구현(Implementation)된 물리적 netlist를 직접 열어 데이터를 눈으로 확인하면 됩니다.

궁극의 1초 검증법:

  1. Flow Navigator에서 Open Implemented Design을 엽니다.
  2. Ctrl + F를 눌러 해당 BRAM 셀(예: RAMB36E2)을 검색하여 클릭합니다.
  3. 화면 하단의 Cell Properties 창에서 Properties 탭을 엽니다.
  4. INIT_00 ~ INIT_7F, 그리고 INITP_00 파라미터의 값을 확인합니다.
Cell properties

“내 데이터는 64-bit인데, 왜 256-bit로 꽉 차 있죠?” (데이터 패킹의 비밀)

이 속성 창을 처음 보면 당황할 수 있습니다. 내가 넣은 .mem 파일은 한 줄에 64-bit인데, Vivado 속성 창에는 256’hB40C… 처럼 256-bit의 긴 Hex 데이터가 들어가 있기 때문입니다.

이것은 논리적 설계와 물리적 구조의 차이에서 오는 데이터 패킹(Packing) 현상입니다.

  • 물리적 BRAM 셀은 한 줄(INIT_xx)에 256-bit씩 묶어서 데이터를 관리합니다.
  • Vivado는 공간 낭비를 막기 위해, 우리가 넣은 64-bit 데이터 4개를 한 줄에 몰아 넣습니다.
  • 또한, 원래 패리티(Parity) 비트를 저장하는 공간인 INITP_xx 마저도, 우리가 64-bit라는 넓은 데이터 폭을 사용했기 때문에 일반 데이터 저장용으로 끌어다 써서 꽉꽉 채워 넣은 것입니다.

참고: AMD

유사한 게시물