Linux 커널 FPGA 프레임워크

리눅스 커널의 FPGA 서브시스템을 심층 분석합니다. FPGA Manager/Bridge/Region 프레임워크, DFL(Device Feature List), 부분 재구성(Partial Reconfiguration), PCIe 엔드포인트, sysfs/UIO/VFIO 인터페이스, Device Tree Overlay 연동, SoC FPGA(Zynq/Cyclone V), AXI 프로토콜, 커스텀 주변장치 개발, 커널 설정 옵션, 실전 드라이버 예제, 트러블슈팅까지 다룹니다.

전제 조건: FPGA, 디바이스 드라이버, PCI/PCIe, DMA 문서를 먼저 읽으세요. FPGA 하드웨어 아키텍처와 설계 흐름은 FPGA 페이지에서, Linux 드라이버 기초는 디바이스 드라이버 페이지에서 다룹니다.
일상 비유: FPGA Manager는 프린터 드라이버와 비슷합니다. USB 프린터든 네트워크 프린터든 "인쇄" 버튼 하나로 동작하듯, FPGA Manager는 Xilinx든 Intel이든 동일한 커널 API로 비트스트림을 프로그래밍합니다. Bridge는 프로그래밍 중 다른 트래픽을 차단하는 역할이고, Region은 이 모든 과정을 관리하는 상위 제어기입니다.

핵심 요약

  • FPGA Manager — 비트스트림 로드를 추상화하는 최하위 계층입니다. 벤더별 프로그래밍 프로토콜을 통합된 ops 구조로 감쌉니다
  • FPGA Bridge — 재구성 중 FPGA와 외부 버스 연결을 안전하게 차단/복원합니다
  • FPGA Region — Manager와 Bridge를 조합하여 영역별 프로그래밍을 관리하는 상위 프레임워크입니다
  • DFL — Intel FPGA 카드에서 PCIe BAR의 연결 리스트로 FPGA 기능을 자동 열거하는 프로토콜입니다

단계별 이해

  1. 1단계 — FPGA Manager: 비트스트림을 FPGA에 전송하는 최하위 계층을 이해합니다
  2. 2단계 — Bridge & Region: 재구성 중 버스 격리와 영역 관리를 학습합니다
  3. 3단계 — 부분 재구성: FPGA의 일부만 런타임에 교체하는 기술을 익힙니다
  4. 4단계 — DFL/PCIe: PCIe 기반 FPGA 카드의 커널 통합을 이해합니다
  5. 5단계 — SoC FPGA: Zynq/Cyclone V 등 PS-PL 연동을 학습합니다
  6. 6단계 — 드라이버 개발: 커스텀 FPGA IP를 위한 Linux 드라이버를 작성합니다

커널 개발자가 먼저 알아야 할 FPGA 최소 그림

커널 개발자 관점에서 FPGA를 처음 볼 때는 모든 것을 한꺼번에 이해하려고 하기보다, 비트스트림을 올리는 경로올린 뒤에 레지스터를 읽고 쓰는 경로를 먼저 분리해서 보는 것이 좋습니다. 실제 프로젝트가 아무리 복잡해도 처음에는 대개 다음 네 가지로 환원됩니다.

요소커널에서 보이는 형태초보자가 먼저 확인할 점
비트스트림펌웨어 파일, sysfs, FPGA Manager어떤 파일을 어떤 Manager가 어떤 순서로 적재합니까?
레지스터 인터페이스MMIO BAR 또는 AXI-Lite 윈도우레지스터 오프셋과 비트 필드가 문서와 일치합니까?
인터럽트플랫폼 IRQ 또는 MSI/MSI-X상태 비트를 읽은 뒤 어떻게 확인·해제합니까?
DMA 경로DMAEngine, IOMMU, PCIe DMA IP초기 학습 단계에서는 일단 제외하고 MMIO 제어부터 성공했습니까?
비트스트림 파일 firmware.bin FPGA Manager 적재·상태 전이·오류 처리 FPGA 패브릭 사용자 RTL + AXI-Lite 슬레이브 상태 비트, 카운터, 제어 레지스터 플랫폼 드라이버 readl(), writel(), IRQ 사용자 공간 sysfs / ioctl / mmap MMIO 레지스터 맵 0x00 CTRL, 0x04 STATUS, 0x08 COUNT

이 그림만 익혀도 학습 순서가 훨씬 단순해집니다. 먼저 비트스트림 적재가 성공하는지 확인하고, 그다음 MMIO 레지스터 하나를 읽고 쓰는 데 성공하면, 이후의 IRQ와 DMA는 그 위에 층층이 쌓는 문제로 바뀝니다.

가장 작은 MMIO 예제로 이해하기

기초 학습에서는 복잡한 가속기보다 제어 레지스터 1개, 상태 레지스터 1개, 카운터 1개만 있는 단순한 IP가 좋습니다. 예를 들어 다음과 같은 레지스터 맵을 가진 FPGA 주변장치를 생각해 보겠습니다.

오프셋이름비트의미
0x00CTRLbit 0카운터 시작
0x04STATUSbit 0동작 중(busy)
0x04STATUSbit 1완료 인터럽트 대기
0x08COUNT31:0누적 카운트 값
#define MY_CTRL         0x00
#define MY_STATUS       0x04
#define MY_COUNT        0x08
#define MY_CTRL_START   BIT(0)
#define MY_ST_BUSY      BIT(0)
#define MY_ST_DONE      BIT(1)

static void myfpga_start(void __iomem *base)
{
    writel(MY_CTRL_START, base + MY_CTRL);
}

static bool myfpga_done(void __iomem *base)
{
    u32 st = readl(base + MY_STATUS);
    return !!(st & MY_ST_DONE);
}

static u32 myfpga_count(void __iomem *base)
{
    return readl(base + MY_COUNT);
}

이 코드는 매우 단순하지만, 커널과 FPGA의 연결을 이해하는 데 필요한 거의 모든 기초가 들어 있습니다. base는 AXI-Lite 또는 PCIe BAR에 매핑된 MMIO 시작 주소이고, readl()/writel()은 결국 FPGA 패브릭 내부의 레지스터 슬라이스를 읽고 쓰는 행위입니다. 커널 개발자가 제일 먼저 체감해야 하는 사실은 레지스터 오프셋 표 하나가 RTL과 드라이버 사이의 계약서라는 점입니다.

my_accel@43c00000 {
    compatible = "vendor,my-counter";
    reg = <0x43c00000 0x1000>;
    interrupts = <0 89 4>;
};

SoC FPGA에서는 여기에 Device Tree 노드가 추가되고, PCIe 카드에서는 BAR 열거 정보가 이를 대신합니다. 그러나 핵심은 같습니다. 커널은 "이 주소 범위에 있는 레지스터를 어떤 드라이버가 어떤 의미로 해석하는가"만 알면 됩니다. 기초 학습 단계에서는 이 경로가 보이는 것만으로도 충분히 큰 진전입니다.

입문자가 따라야 할 학습 순서

  1. 하드웨어 쪽 최소 성공: FPGA 페이지의 LED 점멸기 실습처럼 비트스트림 적재와 기본 핀 동작을 먼저 성공시킵니다
  2. 레지스터 맵 정의: 제어/상태/데이터 레지스터를 2~3개만 가진 단순한 IP를 만듭니다
  3. 커널 MMIO 접근: 플랫폼 드라이버 또는 UIO로 readl(), writel()이 실제 값을 주고받는지 확인합니다
  4. 그다음 IRQ, 마지막에 DMA: 처음부터 DMA를 붙이지 말고, 인터럽트와 전송 경로를 단계적으로 늘립니다

FPGA Manager 프레임워크

리눅스 커널의 FPGA Manager 프레임워크는 비트스트림(Bitstream) 로드를 추상화하여, 다양한 제조사의 FPGA를 동일한 커널 인터페이스로 프로그래밍할 수 있도록 합니다. Xilinx Zynq, Intel SoC FPGA, Lattice iCE40 등 하드웨어마다 다른 프로그래밍 프로토콜을 통합된 ops 콜백 구조로 감싸, 유저스페이스와 다른 커널 서브시스템이 일관된 방법으로 FPGA를 구성할 수 있습니다.

프레임워크 아키텍처

커널의 FPGA 지원은 drivers/fpga/ 디렉토리에 집중되어 있으며, 3개의 핵심 프레임워크로 구성됩니다:

이 3계층의 관계를 정리하면 다음과 같습니다:

계층 핵심 구조체 역할 소스 파일
Region (최상위) struct fpga_region 재구성 조율, DT Overlay 처리 fpga-region.c
Bridge (중간) struct fpga_bridge 트래픽 격리 (enable/disable) fpga-bridge.c
Manager (최하위) struct fpga_manager 비트스트림 전송 fpga-mgr.c

drivers/fpga/ 디렉토리에는 이 프레임워크 코어 외에도 각 벤더별 구현 드라이버가 포함됩니다:

이러한 계층 구조를 통해, 유저스페이스는 FPGA 하드웨어의 구체적인 프로그래밍 프로토콜을 알 필요 없이, Region에 비트스트림 파일명만 전달하면 됩니다. Region이 자동으로 Bridge를 비활성화하고, Manager를 통해 비트스트림을 로드한 뒤, Bridge를 다시 활성화합니다.

Linux FPGA 서브시스템 아키텍처 유저스페이스, 커널, 하드웨어 3계층으로 구성된 Linux FPGA 서브시스템의 전체 아키텍처를 보여주는 다이어그램 유저스페이스 sysfs /sys/class/fpga_* OPAE SDK libfpga / fpgaconf configfs (DT Overlay) 커널 FPGA 서브시스템 fpga_region 오케스트레이터 (조율자) fpga_bridge 트래픽 격리 (enable/disable) fpga_manager 비트스트림 프로그래밍 DFL 프레임워크 dfl-pci, dfl-fme 플랫폼 드라이버 zynq, socfpga, ice40 소유 소유 하드웨어 FPGA 칩 Static Region (정적 영역) PR Region (동적 영역) ① bridge.disable() ② mgr.load() ③ bridge.enable()

fpga_manager 구조체와 fpga_manager_ops

struct fpga_manager는 커널이 FPGA 프로그래밍 하드웨어를 표현하는 핵심 구조체입니다. 각 FPGA Manager 인스턴스는 하나의 프로그래밍 인터페이스에 대응합니다:

필드 타입 설명
name const char * Manager 식별 이름 (sysfs에 표시)
dev struct device 커널 디바이스 모델 내장 구조체
ops const struct fpga_manager_ops * 하드웨어별 콜백 함수 테이블
priv void * 드라이버 프라이빗 데이터
state enum fpga_mgr_states 현재 Manager 상태
compat_id struct fpga_compat_id * 이미지 호환성 식별자

struct fpga_manager_ops는 각 FPGA 하드웨어 드라이버가 구현해야 하는 콜백 함수 테이블입니다:

콜백 필수 설명
state 선택 현재 FPGA 상태를 enum fpga_mgr_states로 반환합니다
write_init 선택 프로그래밍 시작 준비 (리셋, 초기화 시퀀스)
write write 또는 write_sg 중 하나 단일 연속 버퍼에서 비트스트림 데이터 전송
write_sg write 또는 write_sg 중 하나 스캐터-개더(Scatter-Gather) 리스트에서 비트스트림 데이터 전송
write_complete 선택 프로그래밍 완료 후 DONE 신호 확인 및 정리
fpga_remove 선택 Manager 해제 시 정리 작업
groups 선택 추가 sysfs 속성 그룹

writewrite_sg의 차이는 메모리 레이아웃에 있습니다. write는 커널이 단일 연속 버퍼(kmalloc 또는 vmalloc)에 비트스트림 전체를 올린 뒤 호출합니다. 수십 MB에 달하는 비트스트림의 경우 연속 메모리 할당이 실패할 수 있으므로, write_sg를 구현하면 커널이 struct sg_table로 분산된 페이지들을 직접 전달합니다. DMA를 사용하는 컨트롤러에서는 write_sg가 더 효율적입니다.

다음은 전형적인 FPGA Manager ops 구현 예제입니다:

static enum fpga_mgr_states my_fpga_state(struct fpga_manager *mgr)
{
    struct my_fpga_priv *priv = mgr->priv;
    u32 status = readl(priv->base + STATUS_REG);

    if (status & FPGA_DONE)
        return FPGA_MGR_STATE_OPERATING;
    if (status & FPGA_INIT_B)
        return FPGA_MGR_STATE_RESET;
    return FPGA_MGR_STATE_UNKNOWN;
}

static int my_fpga_write_init(struct fpga_manager *mgr,
                               struct fpga_image_info *info,
                               const char *buf, size_t count)
{
    struct my_fpga_priv *priv = mgr->priv;
    u32 val;

    /* 프로그래밍 모드 시작 */
    writel(FPGA_PROG_B, priv->base + CTRL_REG);
    usleep_range(10, 20);
    writel(0, priv->base + CTRL_REG);

    /* INIT_B 신호 대기 */
    return readl_poll_timeout(priv->base + STATUS_REG, val,
                              val & FPGA_INIT_B, 10, 1000000);
}

static int my_fpga_write(struct fpga_manager *mgr,
                          const char *buf, size_t count)
{
    struct my_fpga_priv *priv = mgr->priv;

    /* 비트스트림 데이터 전송 */
    iowrite32_rep(priv->base + DATA_REG, buf, count / 4);
    return 0;
}

static int my_fpga_write_complete(struct fpga_manager *mgr,
                                   struct fpga_image_info *info)
{
    struct my_fpga_priv *priv = mgr->priv;
    u32 val;

    /* DONE 신호 대기 */
    return readl_poll_timeout(priv->base + STATUS_REG, val,
                              val & FPGA_DONE, 10, 5000000);
}

static const struct fpga_manager_ops my_fpga_ops = {
    .state          = my_fpga_state,
    .write_init     = my_fpga_write_init,
    .write          = my_fpga_write,
    .write_complete = my_fpga_write_complete,
};
코드 설명
  • my_fpga_state(): FPGA의 현재 상태를 하드웨어 레지스터에서 읽어 enum fpga_mgr_states 값으로 매핑합니다. STATUS_REGFPGA_DONE 비트가 설정되어 있으면 FPGA가 정상 동작 중임을 의미하고, FPGA_INIT_B만 설정되어 있으면 리셋 상태입니다
  • my_fpga_write_init(): 비트스트림 전송 전 초기화 시퀀스를 수행합니다. FPGA_PROG_B 신호를 펄스로 인가하여 FPGA를 프로그래밍 모드로 진입시킨 뒤, readl_poll_timeout()으로 INIT_B 신호가 올라올 때까지 대기합니다. 타임아웃은 1초(1,000,000μs)이며, 10μs 간격으로 폴링합니다
  • my_fpga_write(): 비트스트림 바이너리 데이터를 FPGA의 데이터 레지스터에 32비트 단위로 반복 전송합니다. iowrite32_rep()는 같은 주소에 반복적으로 쓰기를 수행하는 I/O 함수입니다
  • my_fpga_write_complete(): 모든 데이터 전송이 끝난 뒤, FPGA가 구성을 완료했음을 나타내는 DONE 신호를 기다립니다. 타임아웃은 5초로, 대형 FPGA의 경우 구성 완료까지 시간이 걸릴 수 있기 때문입니다
  • my_fpga_ops: 4개의 콜백을 하나의 ops 구조체로 묶어 Manager에 등록합니다. write_sg를 구현하지 않았으므로, 커널은 단일 버퍼 경로(write)를 사용합니다

FPGA Manager 상태 머신

FPGA Manager는 내부적으로 상태 머신(State Machine)을 유지하며, 현재 FPGA가 어떤 단계에 있는지를 추적합니다. enum fpga_mgr_states에 정의된 상태들은 다음과 같습니다:

상태 설명
FPGA_MGR_STATE_UNKNOWN 0 상태를 알 수 없음 (초기값 또는 에러)
FPGA_MGR_STATE_POWER_OFF 1 FPGA 전원 꺼짐
FPGA_MGR_STATE_POWER_UP 2 FPGA 전원 인가 중
FPGA_MGR_STATE_RESET 3 FPGA 리셋 상태 (INIT_B 대기)
FPGA_MGR_STATE_FIRMWARE_REQ 4 펌웨어(비트스트림) 요청 중
FPGA_MGR_STATE_FIRMWARE_REQ_ERR 5 펌웨어 요청 실패
FPGA_MGR_STATE_WRITE_INIT 6 쓰기 초기화 중 (write_init 실행)
FPGA_MGR_STATE_WRITE_INIT_ERR 7 쓰기 초기화 실패
FPGA_MGR_STATE_WRITE 8 비트스트림 전송 중 (write 실행)
FPGA_MGR_STATE_WRITE_ERR 9 비트스트림 전송 실패
FPGA_MGR_STATE_WRITE_COMPLETE 10 전송 완료 확인 중 (write_complete 실행)
FPGA_MGR_STATE_WRITE_COMPLETE_ERR 11 전송 완료 확인 실패
FPGA_MGR_STATE_OPERATING 12 FPGA 정상 동작 중

정상적인 프로그래밍 흐름에서 상태 전이 순서는 다음과 같습니다:

  1. UNKNOWNPOWER_UP: FPGA에 전원이 인가됩니다
  2. POWER_UPRESET: 전원 안정 후 리셋 시퀀스가 진행됩니다
  3. RESETFIRMWARE_REQ: 커널이 request_firmware()로 비트스트림 파일을 요청합니다
  4. FIRMWARE_REQWRITE_INIT: 비트스트림 확보 후, write_init 콜백으로 FPGA를 프로그래밍 모드로 전환합니다
  5. WRITE_INITWRITE: 비트스트림 데이터를 write 콜백으로 전송합니다
  6. WRITEWRITE_COMPLETE: 전송 완료 후 write_complete로 DONE 신호를 확인합니다
  7. WRITE_COMPLETEOPERATING: FPGA 구성이 성공적으로 완료되어 정상 동작합니다

각 단계에서 에러가 발생하면 해당 에러 상태(*_ERR)로 전이됩니다. sysfs의 state 파일을 통해 현재 상태를 확인할 수 있으므로, 디버깅 시 FPGA 프로그래밍이 어느 단계에서 실패했는지 파악하는 데 유용합니다.

FPGA Manager 상태 머신 FPGA Manager의 상태 전이 다이어그램으로, UNKNOWN에서 OPERATING까지의 정상 흐름과 에러 경로를 표시 UNKNOWN POWER_UP RESET FIRMWARE_REQ WRITE_INIT WRITE WRITE_CMPL OPERATING FW_REQ_ERR WRITE_INIT_ERR WRITE_ERR CMPL_ERR 정상 전이 에러 경로

devm_fpga_mgr_register()

FPGA Manager를 커널에 등록하는 권장 방법은 devm_fpga_mgr_register()입니다. 이 함수는 디바이스 관리(devres) 기반으로 동작하여, 드라이버가 해제될 때 자동으로 Manager 등록이 해제됩니다. 내부적으로 다음 순서를 거칩니다:

  1. fpga_mgr_create(): struct fpga_manager를 할당하고 초기화합니다
  2. device_register(): 커널 디바이스 모델에 등록하여 /sys/class/fpga_manager/fpgaN 노드를 생성합니다
  3. devm_add_action_or_reset(): 드라이버 해제 시 자동 정리를 등록합니다

반환값은 struct fpga_manager *이며, 에러 시 ERR_PTR()로 인코딩된 에러 포인터를 반환합니다. 따라서 PTR_ERR_OR_ZERO()로 에러 코드를 추출하여 probe 함수의 반환값으로 사용할 수 있습니다.

다음은 FPGA Manager 드라이버의 완전한 probe 함수 및 드라이버 등록 예제입니다:

static int my_fpga_probe(struct platform_device *pdev)
{
    struct my_fpga_priv *priv;
    struct fpga_manager *mgr;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);

    mgr = devm_fpga_mgr_register(&pdev->dev, "My FPGA",
                                  &my_fpga_ops, priv);
    return PTR_ERR_OR_ZERO(mgr);
}

static const struct of_device_id my_fpga_of_match[] = {
    { .compatible = "vendor,my-fpga-mgr" },
    { }
};
MODULE_DEVICE_TABLE(of, my_fpga_of_match);

static struct platform_driver my_fpga_driver = {
    .probe  = my_fpga_probe,
    .driver = {
        .name = "my-fpga-mgr",
        .of_match_table = my_fpga_of_match,
    },
};
module_platform_driver(my_fpga_driver);
코드 설명
  • devm_kzalloc(): 디바이스 수명 주기에 연동되는 메모리 할당입니다. 드라이버 해제 시 자동으로 kfree()됩니다
  • devm_platform_ioremap_resource(): 플랫폼 리소스(DT의 reg 속성)를 가상 메모리에 매핑합니다. 기존의 platform_get_resource() + devm_ioremap_resource()를 하나로 합친 헬퍼입니다
  • devm_clk_get_enabled(): 클록을 가져와 활성화까지 한 번에 처리합니다. FPGA 프로그래밍 인터페이스에 클록이 필요한 경우 사용합니다
  • devm_fpga_mgr_register(): FPGA Manager를 커널에 등록합니다. 네 번째 인자 priv는 Manager의 priv 필드에 저장되어, 이후 ops 콜백에서 mgr->priv로 접근할 수 있습니다
  • PTR_ERR_OR_ZERO(): 포인터가 유효하면 0을, 에러 포인터이면 에러 코드를 반환합니다. probe 함수의 반환값으로 바로 사용할 수 있어 코드가 간결해집니다
  • module_platform_driver(): module_init/module_exit 보일러플레이트를 자동 생성하여 플랫폼 드라이버를 등록합니다

fpga_mgr_load() 경로

fpga_mgr_load()는 비트스트림을 FPGA에 실제로 전송하는 핵심 함수입니다. 이 함수는 Region 또는 다른 커널 코드에 의해 호출되며, 내부적으로 Manager의 ops 콜백을 순차적으로 실행합니다:

  1. write_init(mgr, info, buf, count): FPGA를 프로그래밍 모드로 전환합니다. bufcount 인자로 비트스트림의 처음 부분(헤더)을 전달받을 수 있습니다. 일부 FPGA는 헤더를 분석하여 비트스트림 호환성을 검증합니다
  2. write(mgr, buf, count): 비트스트림 데이터를 전송합니다. 대용량 비트스트림의 경우 이 콜백이 여러 번 호출될 수 있습니다. 또는 write_sg(mgr, sgt)가 구현되어 있으면 스캐터-개더 리스트를 통해 전송합니다
  3. write_complete(mgr, info): 전송 완료를 확인하고 FPGA가 정상 동작 상태에 진입했는지 검증합니다

비트스트림 소스에 따라 두 가지 로딩 경로가 존재합니다:

경로 함수 설명
펌웨어 경로 fpga_mgr_load() + request_firmware() /lib/firmware/에서 비트스트림 파일을 로드합니다. fpga_image_infofirmware_name으로 파일명을 지정합니다
버퍼 경로 fpga_mgr_buf_load() 이미 커널 메모리에 있는 비트스트림 버퍼를 직접 전달합니다. 유저스페이스에서 ioctl로 전달받은 데이터 등에 사용합니다

스캐터-개더(SG) 경로는 대형 비트스트림 처리에 특히 중요합니다. 수십 MB의 비트스트림을 단일 연속 메모리에 할당하는 것은 메모리 단편화(Fragmentation)로 인해 실패할 수 있습니다. SG 경로를 사용하면 struct sg_table로 물리적으로 분산된 페이지들의 리스트를 전달하여, 각 페이지를 개별적으로 DMA 전송할 수 있습니다. 드라이버가 write_sg 콜백을 구현하면 커널은 자동으로 이 경로를 선택합니다.

FPGA Bridge 프레임워크

FPGA Bridge 프레임워크는 FPGA 재구성 중 CPU와 FPGA 로직 사이의 데이터 경로를 안전하게 격리하는 메커니즘을 제공합니다. 재구성 도중 FPGA 내부 로직은 정의되지 않은 상태에 놓이므로, 이때 CPU가 FPGA 영역에 접근하면 예측할 수 없는 결과가 발생합니다.

브리지 개념

FPGA 재구성(Reconfiguration)은 FPGA 내부의 LUT, FF, 라우팅 리소스를 모두 재설정하는 과정입니다. 이 과정에서 기존에 구현되어 있던 AXI 슬레이브 인터페이스, 인터럽트 로직, DMA 엔진 등이 일시적으로 사라집니다. 만약 CPU가 이 시점에 해당 주소 공간에 접근하면 다음과 같은 문제가 발생할 수 있습니다:

FPGA Bridge는 이러한 위험을 방지하기 위해, 재구성 전에 CPU↔FPGA 사이의 데이터 경로를 물리적으로 차단합니다. SoC FPGA 설계에서 일반적으로 사용되는 브리지 유형은 다음과 같습니다:

브리지 유형 설명 커널 드라이버
HPS-to-FPGA Bridge ARM 프로세서(HPS)에서 FPGA 로직으로의 AXI 마스터 브리지입니다. 고성능 데이터 전송에 사용합니다 altera-hps2fpga-bridge.c
Lightweight HPS-to-FPGA 제어/상태 레지스터 접근을 위한 경량 브리지입니다. 대역폭은 낮지만 레이턴시가 짧습니다 altera-hps2fpga-bridge.c
FPGA-to-SDRAM Bridge FPGA 로직에서 DDR 메모리로의 직접 접근 브리지입니다. FPGA DMA 엔진이 사용합니다 altera-fpga2sdram-bridge.c
Freeze Bridge 부분 재구성(PR) 전용 브리지입니다. PR 영역의 경계에 배치되어 해당 파티션만 격리합니다 altera-freeze-bridge.c
PR Decoupler Xilinx의 부분 재구성 격리 브리지입니다. Freeze Bridge와 동일한 역할을 수행합니다 xilinx-pr-decoupler.c

fpga_bridge_ops 구현

struct fpga_bridge_ops는 브리지 드라이버가 구현해야 하는 콜백 함수 테이블입니다. 핵심 콜백은 단 하나, enable_set입니다:

선택적으로 enable_show 콜백을 구현하면 sysfs에서 현재 브리지 상태를 조회할 수 있습니다.

struct my_bridge_priv {
    void __iomem *base;
};

static int my_bridge_enable_set(struct fpga_bridge *bridge, bool enable)
{
    struct my_bridge_priv *priv = bridge->priv;
    u32 val;

    if (enable)
        writel(BRIDGE_ENABLE, priv->base + BRIDGE_CTRL);
    else
        writel(BRIDGE_DISABLE, priv->base + BRIDGE_CTRL);

    /* 상태 전환 완료 대기 */
    return readl_poll_timeout(priv->base + BRIDGE_STATUS, val,
                              (enable ? (val & BRIDGE_ACTIVE) :
                                       !(val & BRIDGE_ACTIVE)),
                              10, 100000);
}

static const struct fpga_bridge_ops my_bridge_ops = {
    .enable_set = my_bridge_enable_set,
};

static int my_bridge_probe(struct platform_device *pdev)
{
    struct my_bridge_priv *priv;
    struct fpga_bridge *br;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    br = devm_fpga_bridge_register(&pdev->dev, "My Bridge",
                                    &my_bridge_ops, priv);
    return PTR_ERR_OR_ZERO(br);
}
코드 설명
  • my_bridge_enable_set(): 브리지의 제어 레지스터에 ENABLE 또는 DISABLE 값을 씁니다. 하드웨어에 따라 상태 전환에 시간이 걸리므로, readl_poll_timeout()으로 실제 상태 변경을 확인합니다. 타임아웃 100ms, 폴링 간격 10μs입니다
  • 조건부 폴링: enable이 true이면 BRIDGE_ACTIVE 비트가 설정될 때까지, false이면 해당 비트가 클리어될 때까지 대기합니다
  • devm_fpga_bridge_register(): FPGA Bridge를 커널에 등록합니다. /sys/class/fpga_bridge/brN 노드가 생성되며, Manager와 마찬가지로 devres 기반 자동 해제를 지원합니다

브리지 라이프사이클

FPGA Bridge의 라이프사이클은 Region에 의해 관리됩니다. Region이 재구성을 수행할 때, 연결된 모든 브리지를 순서대로 제어합니다:

  1. Bridge Disable: fpga_bridges_disable()가 Region에 연결된 브리지 리스트를 순회하며 각 브리지의 enable_set(bridge, false)를 호출합니다. 이 단계에서 모든 CPU↔FPGA 트래픽이 차단됩니다
  2. Manager Load: fpga_mgr_load()가 비트스트림을 FPGA에 전송합니다. 브리지가 비활성화된 상태이므로 재구성이 안전하게 진행됩니다
  3. Bridge Enable: fpga_bridges_enable()가 브리지 리스트를 순회하며 enable_set(bridge, true)를 호출합니다. 새로운 FPGA 로직이 CPU에서 접근 가능해집니다

하나의 Region에 여러 브리지가 연결될 수 있습니다. 예를 들어 Intel SoC FPGA에서는 HPS-to-FPGA, Lightweight HPS-to-FPGA, FPGA-to-SDRAM 세 개의 브리지가 동시에 관리될 수 있습니다. fpga_bridges_disable()는 리스트의 순서대로, fpga_bridges_enable()는 역순으로 브리지를 제어하여, 의존성이 있는 브리지도 안전하게 처리합니다.

에러 처리에서 중요한 점은, 비트스트림 로드가 실패하면 브리지를 비활성 상태로 유지하는 것입니다. 재구성에 실패한 FPGA는 정의되지 않은 상태에 있으므로, 브리지를 활성화하면 위에서 설명한 버스 행업이나 데이터 손상이 발생할 수 있습니다.

FPGA Region 프레임워크

FPGA Region은 FPGA 서브시스템의 최상위 추상화 계층으로, 재구성 가능 영역을 표현하고 전체 재구성 과정을 오케스트레이션합니다.

리전 개념

FPGA Region은 "재구성 가능한 FPGA 영역"을 커널 디바이스 모델에서 표현합니다. 하나의 FPGA 칩에 여러 Region이 존재할 수 있으며, 재구성 범위에 따라 두 가지로 구분됩니다:

유형 설명 Region 수
전체 재구성(Full Reconfiguration) FPGA 전체를 하나의 Region으로 관리합니다. 비트스트림 로드 시 FPGA의 모든 로직이 교체됩니다 1개
부분 재구성(Partial Reconfiguration) FPGA 내 특정 영역을 개별 Region으로 관리합니다. 나머지 영역은 동작을 유지한 채, 해당 Region만 교체합니다 N개

각 Region은 다음 두 가지를 소유합니다:

Region의 핵심 역할은 이 Manager와 Bridge들을 조율하여, 안전한 재구성을 보장하는 것입니다. 유저스페이스나 다른 커널 서브시스템은 Region에만 비트스트림 정보를 전달하면 되고, Region이 내부적으로 Bridge disable → Manager load → Bridge enable 순서를 자동으로 실행합니다.

fpga_region_program_fpga() 경로

fpga_region_program_fpga()는 Region을 통해 FPGA 재구성을 수행하는 핵심 함수입니다. 내부 동작은 다음 3단계로 구성됩니다:

  1. fpga_bridges_disable(&region->bridge_list)

    Region에 연결된 모든 브리지를 비활성화합니다. 이 시점부터 CPU↔FPGA 트래픽이 차단됩니다. 브리지가 없는 경우(전체 재구성에서 브리지가 불필요한 하드웨어 등) 이 단계는 건너뜁니다.

  2. fpga_mgr_load(region->mgr, &region->info)

    Manager를 통해 비트스트림을 FPGA에 전송합니다. region->info에는 펌웨어 파일명, 플래그(전체/부분 재구성 등), 호환성 ID 등의 정보가 담겨 있습니다.

  3. fpga_bridges_enable(&region->bridge_list)

    재구성 완료 후 브리지를 다시 활성화하여 CPU↔FPGA 통신을 복원합니다.

에러 처리 정책은 매우 보수적입니다:

Device Tree Overlay 연동

FPGA Region의 가장 강력한 기능 중 하나는 Device Tree(DT) Overlay와의 연동입니다. FPGA에 새로운 로직을 로드하면, 해당 로직이 구현하는 하드웨어(SPI 컨트롤러, GPIO, DMA 엔진 등)에 대한 디바이스 노드도 함께 추가되어야 합니다. DT Overlay는 이 과정을 자동화합니다.

동작 흐름은 다음과 같습니다:

  1. 유저스페이스가 DT Overlay를 적용합니다 (configfs 또는 of_overlay_fdt_apply())
  2. 커널의 OF Notifier가 of_fpga_region_notify()를 호출합니다
  3. Region이 Overlay에서 firmware-name 속성을 읽어 비트스트림 파일명을 확인합니다
  4. Region이 fpga_region_program_fpga()를 실행하여 비트스트림을 로드합니다
  5. FPGA 재구성이 성공하면, Overlay의 나머지 디바이스 노드들이 DT에 추가됩니다
  6. 새로 추가된 디바이스 노드에 대해 해당 드라이버의 probe()가 자동으로 호출됩니다

이 연동 덕분에, 유저스페이스에서는 DT Overlay 하나만 적용하면 FPGA 재구성과 드라이버 바인딩이 모두 자동으로 이루어집니다. FPGA에 SPI 컨트롤러와 NOR 플래시를 구현한 비트스트림을 로드하면, 해당 SPI 컨트롤러 드라이버가 probe되고 NOR 플래시가 마운트 가능한 MTD 디바이스로 나타나는 것입니다.

OF FPGA Region(of-fpga-region.c)은 compatible = "fpga-region"으로 바인딩되며, DT Overlay Notifier에 등록되어 자동으로 Overlay 이벤트를 수신합니다. Region 노드의 fpga-mgr phandle이 연결된 Manager를, fpga-bridges phandle 리스트가 연결된 Bridge들을 지정합니다.

Region 프로그래밍 시퀀스 상세

fpga_region_program_fpga() 함수의 내부 흐름을 단계별로 살펴봅니다. 이 함수는 Region을 통한 FPGA 재구성의 전체 오케스트레이션을 담당합니다.

  1. Bridge 목록 획득 — fpga_bridge_get_to_list()

    Region에 연결된 모든 Bridge 참조를 획득합니다. OF FPGA Region의 경우 fpga-bridges phandle 리스트에서, Platform FPGA Region의 경우 region->bridge_list에서 Bridge를 열거합니다. 이 단계에서 각 Bridge의 참조 카운트가 증가합니다.

  2. Bridge 비활성화 — fpga_bridges_disable()

    Bridge 목록의 모든 Bridge를 순회하며 bridge->br_ops->enable_set(bridge, false)를 호출합니다. Freeze Controller를 통해 AXI 트래픽을 정지하거나, LW HPS-to-FPGA Bridge를 비활성화하는 등 하드웨어별 격리 동작이 수행됩니다. 하나라도 실패하면 전체 프로그래밍이 중단됩니다.

  3. 비트스트림 로드 — fpga_mgr_load()

    Manager의 write_init()write() 또는 write_sg()write_complete() 순서로 비트스트림을 FPGA에 전송합니다. region->info에 포함된 펌웨어 파일명, 플래그(FPGA_MGR_PARTIAL_RECONFIG 등), 카운트 정보가 Manager에 전달됩니다.

  4. Bridge 재활성화 — fpga_bridges_enable()

    비트스트림 로드가 성공한 경우에만 Bridge를 다시 활성화합니다. bridge->br_ops->enable_set(bridge, true)를 호출하여 CPU↔FPGA 트래픽을 복원합니다.

  5. 실패 시 Bridge 상태 복원

    비트스트림 로드(3단계)가 실패한 경우, Bridge는 의도적으로 비활성 상태를 유지합니다. 재구성에 실패한 FPGA에 CPU가 접근하면 버스 행업(hang)이나 데이터 손상이 발생할 수 있기 때문입니다. 이후 재구성을 재시도하거나, 전체 재구성으로 FPGA를 알려진 양호한 상태로 복원해야 합니다.

모든 단계가 완료된 후 fpga_bridges_put()로 Bridge 참조를 해제합니다. 이 참조 카운트 관리는 Region이 자동으로 수행하므로, 호출자는 Bridge의 수명을 직접 관리할 필요가 없습니다.

에러 처리 패턴

FPGA 프로그래밍 실패 시 시스템 안전을 보장하는 것이 Region 에러 처리의 핵심 목표입니다. 각 에러 코드가 의미하는 상태와 복구 방법은 다음과 같습니다.

에러 코드 원인 Bridge 상태 복구 방법
-ENODEV Manager가 등록되지 않았거나 참조 해제됨 변경 없음 Manager 드라이버 로드 확인
-EBUSY 다른 스레드에서 이미 프로그래밍 중 변경 없음 재시도 또는 동시 접근 방지
-EINVAL 비트스트림이 대상 FPGA와 불일치 비활성 유지 올바른 비트스트림으로 교체
-ETIMEDOUT FPGA DONE 신호 미수신 비활성 유지 하드웨어 연결 점검

Bridge가 비활성화된 상태에서 Manager 로드가 실패하면, Bridge는 반드시 비활성 상태로 유지되어야 합니다. 이는 "실패한 FPGA에 접근하지 않습니다"는 안전 원칙을 따르는 것입니다. 시스템 운영자는 cat /sys/class/fpga_bridge/*/state로 Bridge 상태를 확인하고, 원인을 해결한 후 재프로그래밍을 시도해야 합니다.

Device Tree에서 Region 노드를 정의하는 예시입니다:

/* Device Tree: FPGA Region 정의 */
fpga_region0: fpga-region@0 {
    compatible = "fpga-region";
    fpga-mgr = <&fpga_mgr0>;
    fpga-bridges = <&hps2fpga_bridge &lwh2f_bridge>;
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;
};

fpga_mgr0: fpga-manager@ff706000 {
    compatible = "altr,socfpga-fpga-mgr";
    reg = <0xff706000 0x1000>;
    interrupts = <0 175 4>;
};

hps2fpga_bridge: fpga-bridge@ff400000 {
    compatible = "altr,socfpga-hps2fpga-bridge";
    reg = <0xff400000 0x100000>;
    clocks = <&l4_main_clk>;
};

부분 재구성 심화

부분 재구성(Partial Reconfiguration, PR)은 FPGA 기술의 핵심 차별점 중 하나로, FPGA의 일부 영역만을 런타임에 교체할 수 있는 기능입니다. 전체 재구성과 달리, FPGA의 나머지 영역은 중단 없이 계속 동작합니다.

PR 개념

부분 재구성을 이해하기 위해서는 FPGA를 두 종류의 영역으로 구분해야 합니다:

정적 영역과 동적 영역 사이에는 반드시 파티션 핀(Partition Pin)이라 불리는 경계 인터페이스를 정의해야 합니다. 파티션 핀은 두 영역 간의 신호 연결 지점으로, FPGA 설계 도구(Vivado, Quartus)에서 정의합니다. 재구성 시 파티션 핀의 신호 방향과 폭은 모든 RM에서 동일해야 하며, 이를 통해 어떤 RM을 로드하든 정적 영역과 올바르게 연결됩니다.

설계 관점에서 PR의 핵심 제약은 다음과 같습니다:

커널 PR 워크플로

리눅스 커널에서 부분 재구성은 FPGA 서브시스템의 3계층(Region, Bridge, Manager)을 통해 수행됩니다. PR에서의 각 계층 역할은 다음과 같습니다:

계층 PR에서의 역할
Region PR 영역(RP)을 표현합니다. 전체 FPGA가 아닌, 특정 재구성 가능 파티션에 대응합니다
Bridge Freeze Bridge 또는 PR Decoupler로, 해당 RP의 경계만 격리합니다. 다른 RP와 정적 영역은 영향받지 않습니다
Manager 부분 비트스트림을 FPGA의 ICAP(Internal Configuration Access Port) 또는 PR 컨트롤러를 통해 전송합니다

DT Overlay를 활용한 PR 전체 흐름은 다음과 같습니다:

  1. DT Overlay 적용: 유저스페이스가 새 가속기에 대한 DT Overlay를 적용합니다. Overlay에는 firmware-name(부분 비트스트림 파일명)과 새로운 디바이스 노드가 포함됩니다
  2. Region Notified: OF Notifier가 해당 Region의 콜백을 호출합니다
  3. Bridge Disable: Region이 Freeze Bridge를 비활성화하여 해당 RP를 격리합니다. 정적 영역과 다른 RP는 정상 동작을 유지합니다
  4. PR Bitstream Load: Manager가 ICAP을 통해 부분 비트스트림을 해당 RP에 전송합니다
  5. Bridge Enable: Freeze Bridge를 다시 활성화하여 새 로직을 정적 영역에 연결합니다
  6. New Driver Probe: DT Overlay의 디바이스 노드들에 대해 해당 드라이버의 probe()가 호출됩니다
부분 재구성 워크플로 정적 영역, Freeze Bridge, PR 영역으로 구성된 부분 재구성의 전체 과정과 런타임 가속기 교체를 보여주는 다이어그램 정적 영역 (Static Region) CPU (ARM/x86) PCIe Hard IP DDR 컨트롤러 PR 컨트롤러 / ICAP Freeze Bridge 격리 스위치 ENABLED PR Region — Slot A Crypto Engine AES-256 + SHA-3 런타임 교체 PR Region — Slot A Packet Parser L2/L3 Header Parse PR 워크플로 Bridge Disable (격리) PR Bitstream Load via ICAP / PR Controller Bridge Enable (복원) New Driver Probe DT Overlay 디바이스 활성화 전체 소요 시간: ~100ms (PR 영역 크기에 따라)

브리지 격리와 안전한 전환

부분 재구성에서 브리지 격리는 전체 재구성보다 더욱 중요합니다. 전체 재구성에서는 FPGA 전체가 멈추므로 시스템이 그 사실을 인지하지만, PR에서는 정적 영역이 계속 동작하면서 PR 영역에 접근할 수 있기 때문입니다.

PR 전용 격리 메커니즘은 제조사마다 다른 IP로 제공됩니다:

IP 이름 제조사 동작 방식
Freeze Controller Intel (Altera) AXI 트래픽을 "freeze" 상태로 전환합니다. 진행 중인 트랜잭션이 완료될 때까지 대기한 뒤, 새 트랜잭션을 차단합니다. freeze 중 마스터가 요청을 보내면 기본 응답(0 또는 에러)을 반환합니다
PR Decoupler Xilinx (AMD) PR 영역의 모든 신호를 정의된 안전 값으로 고정합니다. 일반적으로 출력은 0, 인터럽트는 비활성, 핸드셰이크 신호는 idle 상태로 설정합니다

안전한 PR 전환 시퀀스는 다음과 같습니다:

  1. 진행 중인 트랜잭션 완료 대기: 현재 PR 영역에서 처리 중인 DMA 전송이나 AXI 트랜잭션이 완료될 때까지 기다립니다. 이를 위해 드라이버는 PR 영역의 "busy" 상태를 확인하거나, 일정 시간 대기합니다
  2. Freeze/Decouple 활성화: Freeze Controller 또는 PR Decoupler를 활성화하여 PR 영역을 격리합니다. 이 시점부터 PR 영역의 입출력은 안전 값으로 고정됩니다
  3. 부분 비트스트림 로드: ICAP 또는 PR 컨트롤러를 통해 새 비트스트림을 전송합니다
  4. Freeze/Decouple 해제: 재구성 완료 후 격리를 해제하여 새 로직을 정적 영역에 연결합니다

PR 활용 사례

부분 재구성은 다양한 실용적 시나리오에서 활용됩니다:

Vivado PR 설계 흐름

AMD Vivado에서 부분 재구성을 설계하는 워크플로는 다음과 같습니다.

  1. 정적/동적 영역 분리: 최상위 모듈에서 재구성 가능한 모듈을 별도의 인스턴스로 분리합니다. 정적 영역과 동적 영역 간의 인터페이스(파티션 핀)를 명확히 정의합니다
  2. Pblock 정의: 각 동적 영역이 차지할 물리적 FPGA 리소스 영역을 Pblock으로 지정합니다. Pblock은 CLB, BRAM, DSP 등의 리소스 경계에 맞춰야 합니다
  3. HD.RECONFIGURABLE 속성: 동적 영역의 모듈 인스턴스에 HD.RECONFIGURABLE 속성을 설정합니다
  4. 정적 영역 구현: 정적 영역을 합성·배치배선하고 DCP(Design Checkpoint)를 저장합니다
  5. 재구성 모듈 구현: 각 RM을 정적 DCP 위에 합성·배치배선합니다. 파티션 핀이 고정되어 있으므로 정적 영역과의 인터페이스가 보장됩니다
  6. 부분 비트스트림 생성: write_bitstream -cell로 동적 영역만의 부분 비트스트림을 생성합니다
# Vivado Tcl: PR 설계 흐름 핵심 명령어

# 1. 정적 영역에 Pblock 정의
create_pblock pblock_pr_slot0
add_cells_to_pblock pblock_pr_slot0 [get_cells {top_i/pr_slot0}]
resize_pblock pblock_pr_slot0 -add {SLICE_X0Y0:SLICE_X49Y99}
resize_pblock pblock_pr_slot0 -add {RAMB36_X0Y0:RAMB36_X2Y19}
resize_pblock pblock_pr_slot0 -add {DSP48E2_X0Y0:DSP48E2_X2Y39}

# 2. HD.RECONFIGURABLE 속성 설정
set_property HD.RECONFIGURABLE true [get_cells {top_i/pr_slot0}]

# 3. 정적 영역 구현 및 DCP 저장
opt_design
place_design
route_design
write_checkpoint -force static_impl.dcp

# 4. 재구성 모듈 A 구현
open_checkpoint static_impl.dcp
read_checkpoint -cell top_i/pr_slot0 rm_accel_a_synth.dcp
opt_design; place_design; route_design
write_checkpoint -force config_a.dcp
write_bitstream -force -cell top_i/pr_slot0 partial_a.bit

# 5. 재구성 모듈 B 구현 (정적 DCP 재사용)
open_checkpoint static_impl.dcp
read_checkpoint -cell top_i/pr_slot0 rm_accel_b_synth.dcp
opt_design; place_design; route_design
write_bitstream -force -cell top_i/pr_slot0 partial_b.bit

# 6. PR 검증 (정적+RM_A, 정적+RM_B 모두 검증)
pr_verify config_a.dcp config_b.dcp

PR 플로어플래닝 실전

PR 플로어플래닝은 부분 재구성의 성공을 좌우하는 핵심 단계입니다. 잘못된 플로어플랜은 타이밍 실패, 리소스 부족, 또는 라우팅 혼잡을 초래합니다.

FPGA Die — PR 플로어플랜 정적 영역 (Static) PCIe Hard IP DDR Controller PR Controller Freeze Bridge × 2 AXI Interconnect RP 0 (Reconfigurable Partition) CLB: X50~X99, Y50~Y99 BRAM: 20개, DSP: 40개 RM: 가속기 A / B / C 교체 가능 RP 1 (Reconfigurable Partition) CLB: X50~X99, Y0~Y49 BRAM: 20개, DSP: 40개 RM: 네트워크 / 스토리지 교체 가능 플로어플래닝 규칙 1. Pblock은 클록 리전 경계에 정렬 2. RP 간 리소스 밸런싱 (±10%) 3. RP에 BRAM/DSP 열 전체 포함 4. 파티션 핀 수 최소화 5. 라우팅 채널 여유 확보 6. Hard IP (PCIe/DDR) 인접 배치 ━━ 정적 ╌╌ 동적 (재구성 가능)

DFL (Device Feature List) 프레임워크

DFL(Device Feature List) 프레임워크는 Intel의 FPGA 가속 플랫폼을 위한 리눅스 커널 프레임워크입니다. PAC(Programmable Acceleration Card)와 같은 PCIe 기반 FPGA 카드에서, 하드웨어가 스스로의 기능을 기술(Self-describing)하는 구조를 활용하여 커널 드라이버를 자동으로 열거하고 바인딩합니다.

DFL 개념

전통적인 PCIe 디바이스에서는 벤더 ID와 디바이스 ID로 하드웨어를 식별하고, 고정된 레지스터 맵에 접근합니다. 그러나 FPGA 카드는 재구성 가능하므로, 카드에 어떤 기능이 구현되어 있는지 고정되어 있지 않습니다. DFL은 이 문제를 해결하기 위해, PCIe BAR 공간의 시작 부분에 DFH(Device Feature Header) 체인을 배치합니다.

DFH는 64비트 헤더로, 다음 정보를 포함합니다:

필드 비트 설명
Feature Type [63:60] 기능 유형 (4=AFU, 3=Private, 1=FME, 2=Port)
DFH Version [59:52] DFH 형식 버전
Feature Minor Rev [51:48] 기능의 마이너 리비전
End of List [40] 1이면 이 DFH가 체인의 마지막
Next DFH Offset [39:16] 다음 DFH까지의 바이트 오프셋
Feature Major Rev [15:12] 기능의 메이저 리비전
Feature ID [11:0] 기능 식별자

커널의 DFL 프레임워크는 PCIe BAR를 스캔하여 DFH 체인을 탐색하고, 발견된 각 기능(Feature)을 struct dfl_device로 등록합니다. 이렇게 등록된 DFL 디바이스는 dfl_driverfeature_id로 매칭되어 자동으로 probe됩니다.

DFL 아키텍처

DFL 기반 FPGA 카드는 일반적으로 다음 3개의 주요 구성 요소를 가집니다:

DFL 프레임워크 구조 PCIe BAR 공간의 DFH 체인과 FME, Port, AFU 구성 요소를 보여주는 DFL 프레임워크 아키텍처 다이어그램 PCIe BAR0 — DFH 체인 (연결 리스트) DFH [FME] DFH [Port0] DFH [AFU] DFH [...] (EOL) next_offset next_offset next_offset FME (FPGA Management Engine) 온도 모니터링 (Thermal) 전력 관리 (Power) 에러 보고 (Error) 부분 재구성 관리 (PR) 글로벌 성능 카운터 Port MMIO 접근 인터페이스 인터럽트 (MSI-X) 전달 에러 핸들링 AFU (Accelerator Functional Unit) AFU ID (128-bit GUID) 사용자 정의 레지스터 사용자 정의 로직 (가속기) 로컬 메모리 인터페이스 DMA 엔진 (선택) 접근

DFL 드라이버 구현

DFL 드라이버는 커널의 표준 드라이버 모델을 따르며, struct dfl_driver를 사용하여 등록합니다. dfl_device_id 테이블에서 type(AFU, FME 등)과 feature_id로 매칭합니다:

static int my_afu_probe(struct dfl_device *ddev)
{
    void __iomem *base;

    base = devm_ioremap_resource(&ddev->dev, &ddev->mmio_res);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* AFU 레지스터 접근 */
    dev_info(&ddev->dev, "AFU ID: 0x%llx\n",
             readq(base + AFU_ID_L) | ((u64)readq(base + AFU_ID_H) << 32));
    return 0;
}

static const struct dfl_device_id my_afu_ids[] = {
    { .type = DFL_ID_AFU, .feature_id = MY_AFU_FEATURE_ID },
    { }
};

static struct dfl_driver my_afu_driver = {
    .drv = {
        .name = "my-afu",
    },
    .id_table = my_afu_ids,
    .probe    = my_afu_probe,
};
module_dfl_driver(my_afu_driver);
코드 설명
  • dfl_device->mmio_res: DFL 프레임워크가 DFH 체인 탐색 시 각 기능의 MMIO 영역을 자동으로 파악하여 struct resource로 제공합니다. 드라이버는 이를 devm_ioremap_resource()로 매핑하기만 하면 됩니다
  • AFU ID 읽기: 각 AFU는 128비트 GUID를 가집니다. AFU_ID_L(하위 64비트)과 AFU_ID_H(상위 64비트) 레지스터에서 읽어 조합합니다. 이 ID로 호스트 소프트웨어가 특정 AFU를 식별합니다
  • DFL_ID_AFU: DFH의 Feature Type 필드와 매칭됩니다. AFU 외에 DFL_ID_FME_FEATURE, DFL_ID_PORT_FEATURE 등으로 FME/Port 하위 기능과 매칭할 수도 있습니다
  • module_dfl_driver(): module_platform_driver()와 동일한 패턴으로, DFL 버스에 드라이버를 등록하는 보일러플레이트를 자동 생성합니다

OPAE (Open Programmable Acceleration Engine)

OPAE(Open Programmable Acceleration Engine)는 Intel이 제공하는 오픈소스 유저스페이스 라이브러리 및 도구 모음으로, DFL 기반 FPGA 카드를 유저스페이스에서 편리하게 제어할 수 있게 합니다.

OPAE 스택의 주요 구성 요소는 다음과 같습니다:

구성 요소 설명
libopae-c FPGA 열거, AFU 접근, 공유 메모리 관리, 이벤트 처리 등의 C API를 제공합니다. 애플리케이션은 이 라이브러리를 통해 DFL sysfs/ioctl과 통신합니다
fpgainfo FPGA 카드의 상태 정보(온도, 전력, 에러, AFU ID 등)를 조회하는 CLI 도구입니다
fpgaconf 비트스트림(GBS 파일)을 FPGA에 로드하는 CLI 도구입니다. 내부적으로 DFL의 PR 메커니즘을 사용합니다
fpgad FPGA 이벤트 데몬입니다. 온도 경고, 에러 발생 등의 이벤트를 모니터링하고 대응합니다
OPAE SDK C++, Python 바인딩 및 시뮬레이션 환경을 포함한 개발 키트입니다

OPAE의 계층 구조는 다음과 같이 정리됩니다:

  1. Application: 사용자 가속 애플리케이션 (C/C++/Python)
  2. OPAE C API: libopae-c의 추상화된 FPGA 접근 인터페이스
  3. DFL sysfs/ioctl: 커널의 DFL 드라이버가 노출하는 인터페이스
  4. Kernel DFL: DFH 체인을 기반으로 하드웨어 기능을 열거하고 관리하는 커널 프레임워크

OPAE를 통해 유저스페이스 애플리케이션은 FPGA 카드를 열거하고, 특정 AFU를 열고, 공유 메모리 버퍼를 할당하여 가속기와 데이터를 교환할 수 있습니다. 커널의 DFL 드라이버와 OPAE 라이브러리가 하드웨어 추상화를 제공하므로, 애플리케이션은 PCIe BAR 레지스터를 직접 다루지 않아도 됩니다.

FPGA as PCIe Endpoint

FPGA를 PCIe 엔드포인트(Endpoint)로 사용하는 것은 가장 보편적인 FPGA 가속 구성입니다. FPGA에 내장된 PCIe 하드 IP 또는 소프트 IP가 PCIe 프로토콜을 처리하며, 호스트 CPU는 표준 PCIe 드라이버를 통해 FPGA와 통신합니다.

BAR 매핑과 레지스터 접근

FPGA PCIe 엔드포인트는 일반적으로 여러 BAR(Base Address Register)를 통해 호스트에 기능을 노출합니다:

BAR 일반적 용도 크기
BAR0 제어/상태 레지스터 (CSR). MMIO를 통해 FPGA 설정, 상태 확인, 커맨드 발행 등에 사용합니다 4KB ~ 64KB
BAR2 DMA 버퍼 또는 대용량 메모리 영역. FPGA의 온칩 SRAM이나 DDR 컨트롤러에 대한 직접 접근을 제공합니다 1MB ~ 수 GB
BAR4 추가 기능 영역. DFL 헤더, ROM 영역, 디버그 인터페이스 등에 사용합니다 가변

커널 드라이버에서 BAR에 접근하는 표준 방법은 다음과 같습니다:

FPGA PCIe 엔드포인트 아키텍처 호스트 CPU와 FPGA 사이의 PCIe 연결, BAR 매핑, DMA 엔진, MSI-X 인터럽트 경로를 보여주는 아키텍처 다이어그램 호스트 (CPU + 메모리) 유저스페이스 애플리케이션 리눅스 PCIe 드라이버 MMIO 접근 DMA 매핑 호스트 메모리 (DMA 버퍼) IRQ 핸들러 (MSI-X) PCIe Link Gen3/4/5 x8 / x16 FPGA PCIe Hard IP BAR0 (Registers) CSR, Status, Control User Logic 가속기 로직 BAR2 (DMA Buffer) 대용량 데이터 전송 DMA Engine Scatter-Gather DMA 전송 (Host Memory ↔ FPGA) MSI-X 생성기 인터럽트

FPGA PCIe DMA 엔진

대용량 데이터 전송에서 CPU가 직접 BAR 레지스터를 통해 데이터를 복사하는 것은 매우 비효율적입니다. 이를 해결하기 위해 FPGA 내부에 DMA(Direct Memory Access) 엔진을 구현합니다. FPGA DMA 엔진은 호스트 메모리와 FPGA 메모리 사이에서 CPU 개입 없이 대용량 데이터를 전송합니다.

일반적인 FPGA DMA 엔진의 구조는 스캐터-개더(SG) 디스크립터 링 방식입니다:

디스크립터 필드 설명
src_addr 소스 주소 (호스트→FPGA일 때 호스트 물리 주소, FPGA→호스트일 때 FPGA 내부 주소)
dst_addr 목적지 주소
length 전송 바이트 수
next_desc 다음 디스크립터의 주소 (체인 연결)
control 전송 방향, 인터럽트 발생 여부, 마지막 디스크립터 플래그 등

커널 드라이버에서 DMA 버퍼를 관리하는 주요 API는 다음과 같습니다:

MSI/MSI-X 인터럽트

FPGA PCIe 엔드포인트에서 호스트로 이벤트를 알리는 데 MSI(Message Signaled Interrupts) 또는 MSI-X를 사용합니다. MSI-X는 MSI의 확장으로, 더 많은 인터럽트 벡터(최대 2048개)와 개별 마스킹을 지원합니다.

FPGA에서의 인터럽트 흐름은 다음과 같습니다:

  1. FPGA 내부 로직이 이벤트를 감지합니다 (DMA 완료, 에러, 데이터 준비 등)
  2. FPGA의 인터럽트 컨트롤러 IP가 PCIe Hard IP에 MSI-X 메시지 전송을 요청합니다
  3. PCIe Hard IP가 호스트 메모리의 MSI-X 테이블 주소에 메시지를 기록합니다
  4. 호스트의 인터럽트 컨트롤러가 이를 감지하여 커널의 IRQ 핸들러를 호출합니다

커널 드라이버에서 MSI-X를 설정하는 방법은 다음과 같습니다:

PCIe FPGA 드라이버 작성 패턴

다음은 PCIe FPGA 엔드포인트 드라이버의 완전한 스켈레톤(Skeleton)입니다. BAR 매핑, DMA 버퍼 할당, MSI-X 인터럽트 설정을 모두 포함합니다:

#define MY_FPGA_VENDOR  0x1234
#define MY_FPGA_DEVICE  0x5678

struct my_pcie_fpga {
    struct pci_dev *pdev;
    void __iomem *bar0;
    void __iomem *bar2;
    dma_addr_t dma_handle;
    void *dma_buf;
};

static int my_pcie_probe(struct pci_dev *pdev,
                          const struct pci_device_id *id)
{
    struct my_pcie_fpga *fpga;
    int err;

    fpga = devm_kzalloc(&pdev->dev, sizeof(*fpga), GFP_KERNEL);
    if (!fpga)
        return -ENOMEM;
    fpga->pdev = pdev;

    err = pcim_enable_device(pdev);
    if (err)
        return err;

    err = pcim_iomap_regions(pdev, BIT(0) | BIT(2), "my-fpga");
    if (err)
        return err;

    fpga->bar0 = pcim_iomap_table(pdev)[0];
    fpga->bar2 = pcim_iomap_table(pdev)[2];

    pci_set_master(pdev);
    err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (err)
        return err;

    /* DMA 버퍼 할당 */
    fpga->dma_buf = dmam_alloc_coherent(&pdev->dev, DMA_BUF_SIZE,
                                         &fpga->dma_handle, GFP_KERNEL);
    if (!fpga->dma_buf)
        return -ENOMEM;

    /* MSI-X 인터럽트 */
    err = pci_alloc_irq_vectors(pdev, 1, 4, PCI_IRQ_MSIX | PCI_IRQ_MSI);
    if (err < 0)
        return err;

    err = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
                            my_fpga_irq, 0, "my-fpga", fpga);
    if (err)
        return err;

    pci_set_drvdata(pdev, fpga);
    dev_info(&pdev->dev, "FPGA version: 0x%08x\n",
             ioread32(fpga->bar0 + VERSION_REG));
    return 0;
}

static void my_pcie_remove(struct pci_dev *pdev)
{
    pci_free_irq_vectors(pdev);
}

static const struct pci_device_id my_pcie_ids[] = {
    { PCI_DEVICE(MY_FPGA_VENDOR, MY_FPGA_DEVICE) },
    { }
};
MODULE_DEVICE_TABLE(pci, my_pcie_ids);

static struct pci_driver my_pcie_driver = {
    .name     = "my-pcie-fpga",
    .id_table = my_pcie_ids,
    .probe    = my_pcie_probe,
    .remove   = my_pcie_remove,
};
module_pci_driver(my_pcie_driver);
코드 설명
  • pcim_enable_device(): PCI 디바이스를 활성화합니다. pci_enable_device()의 devres 버전으로, 드라이버 해제 시 자동으로 디바이스를 비활성화합니다
  • pcim_iomap_regions(): BIT(0) | BIT(2)로 BAR0과 BAR2를 한 번에 요청하고 매핑합니다. 내부적으로 pci_request_regions()pci_iomap()을 수행합니다
  • pci_set_master(): PCI 디바이스를 버스 마스터로 설정합니다. DMA 전송을 수행하려면 반드시 설정해야 합니다
  • dma_set_mask_and_coherent(): 64비트 DMA 주소 지원을 설정합니다. FPGA의 PCIe Hard IP가 64비트 주소를 지원하면 4GB 이상의 호스트 메모리에도 DMA가 가능합니다
  • dmam_alloc_coherent(): DMA 일관성 버퍼를 할당합니다. dma_handle에 물리 주소(DMA 주소)가, 반환값에 가상 주소가 저장됩니다. FPGA에는 dma_handle을 전달하고, CPU는 반환된 가상 주소로 접근합니다
  • pci_alloc_irq_vectors(): 최소 1개, 최대 4개의 인터럽트 벡터를 요청합니다. MSI-X를 우선 시도하고, 지원하지 않으면 MSI로 폴백합니다
  • pci_irq_vector(pdev, 0): 첫 번째 벡터의 Linux IRQ 번호를 반환합니다. 이 번호를 devm_request_irq()에 전달하여 핸들러를 등록합니다
  • pci_free_irq_vectors(): remove 콜백에서 인터럽트 벡터를 해제합니다. BAR 매핑과 DMA 버퍼는 devres로 자동 해제되지만, IRQ 벡터는 명시적 해제가 필요합니다
  • module_pci_driver(): PCI 드라이버의 init/exit 보일러플레이트를 자동 생성합니다

sysfs 인터페이스

리눅스 FPGA 서브시스템은 sysfs를 통해 유저스페이스에 표준화된 인터페이스를 제공합니다. /sys/class/fpga_manager/, /sys/class/fpga_bridge/, /sys/class/fpga_region/ 세 디렉토리 아래에 각 인스턴스가 생성됩니다.

FPGA Manager sysfs

각 FPGA Manager 인스턴스는 /sys/class/fpga_manager/fpgaN/ 아래에 다음 속성 파일들을 노출합니다:

파일 권한 설명
name 읽기 전용 Manager의 이름입니다. devm_fpga_mgr_register() 호출 시 전달한 문자열입니다
state 읽기 전용 현재 FPGA 상태입니다. unknown, power off, reset, operating 등의 문자열로 표시됩니다
status 읽기 전용 에러 상태 비트입니다. crc_error, incompatible_image_error, ip_protocol_error 등을 보고합니다. 드라이버가 status ops를 구현한 경우에만 유의미합니다
firmware_name 읽기/쓰기 로드할 비트스트림 파일명입니다 (일부 구현에서만 지원)

FPGA Bridge sysfs

각 FPGA Bridge 인스턴스는 /sys/class/fpga_bridge/brN/ 아래에 다음 속성을 노출합니다:

파일 권한 설명
name 읽기 전용 Bridge의 이름입니다
state 읽기 전용 현재 브리지 상태입니다. enabled(트래픽 통과) 또는 disabled(트래픽 차단)를 표시합니다

FPGA Region sysfs

FPGA Region은 /sys/class/fpga_region/regionN/ 아래에 속성을 노출합니다. 가장 중요한 속성은 firmware_name으로, 이 파일에 비트스트림 파일명을 쓰면 Region이 자동으로 전체 재구성 과정을 수행합니다.

다음은 sysfs를 통한 FPGA 관리 작업의 실제 예제입니다:

# FPGA Manager 상태 확인
cat /sys/class/fpga_manager/fpga0/name
# 출력 예: "SoCFPGA FPGA Manager"

cat /sys/class/fpga_manager/fpga0/state
# 출력 예: "operating"

# FPGA Bridge 상태 확인
cat /sys/class/fpga_bridge/br0/state
# 출력 예: "enabled"

cat /sys/class/fpga_bridge/br0/name
# 출력 예: "SoCFPGA hps2fpga Bridge"

# 비트스트림 로드 (Region을 통해)
# 비트스트림 파일은 /lib/firmware/ 에 위치해야 합니다
echo "my_design.rbf" > /sys/class/fpga_region/region0/firmware_name

# 로드 후 상태 확인
cat /sys/class/fpga_manager/fpga0/state
# 출력 예: "operating" (성공 시) 또는 "write_err" (실패 시)

# DFL 디바이스 정보 (Intel FPGA 카드)
ls /sys/bus/dfl/devices/
# 출력 예: dfl_dev.0  dfl_dev.1  dfl_dev.2

cat /sys/bus/dfl/devices/dfl_dev.0/feature_id
# 출력 예: 0x20 (AFU feature ID)

# 전체 FPGA 관련 sysfs 탐색
find /sys/class/fpga_* -type f -name "name" -exec sh -c 'echo "$(dirname {}): $(cat {})"' \;

UIO/VFIO for FPGA

FPGA를 유저스페이스에서 직접 제어하는 방법으로 UIO(Userspace I/O)와 VFIO(Virtual Function I/O) 두 가지 프레임워크가 있습니다. UIO는 간단한 레지스터 접근에, VFIO는 가상화 환경에서의 FPGA 패스스루에 적합합니다.

UIO를 통한 FPGA 접근

UIO는 최소한의 커널 드라이버로 FPGA 레지스터를 유저스페이스에서 직접 접근할 수 있게 하는 프레임워크입니다. 커널 드라이버는 인터럽트 처리와 메모리 매핑만 담당하고, 실제 하드웨어 로직은 유저스페이스 애플리케이션이 구현합니다.

UIO의 장점은 커널 드라이버 개발 없이도 FPGA와 통신할 수 있는 것입니다. FPGA 설계가 빈번히 변경되는 개발 단계에서, 매번 커널 드라이버를 수정하는 대신 유저스페이스 코드만 변경하면 됩니다. 단점은 인터럽트 처리 성능이 커널 드라이버보다 떨어지고, DMA 설정이 제한적이라는 것입니다.

UIO 커널 드라이버의 핵심 구현은 다음과 같습니다:

/* 커널 UIO 드라이버 */
static irqreturn_t my_uio_handler(int irq, struct uio_info *info)
{
    void __iomem *base = info->mem[0].internal_addr;
    u32 status = ioread32(base + IRQ_STATUS);

    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    iowrite32(status, base + IRQ_CLEAR);
    return IRQ_HANDLED;
}

static int my_uio_probe(struct platform_device *pdev)
{
    struct uio_info *info;
    struct resource *res;

    info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);

    info->name = "my-fpga-uio";
    info->version = "1.0";
    info->mem[0].addr = res->start;
    info->mem[0].size = resource_size(res);
    info->mem[0].memtype = UIO_MEM_PHYS;
    info->mem[0].internal_addr = devm_ioremap_resource(&pdev->dev, res);
    info->irq = platform_get_irq(pdev, 0);
    info->handler = my_uio_handler;

    return devm_uio_register_device(&pdev->dev, info);
}
코드 설명
  • my_uio_handler(): 인터럽트 핸들러입니다. 하드웨어 인터럽트 상태를 확인하고 클리어합니다. UIO 프레임워크는 이 핸들러가 IRQ_HANDLED를 반환하면, 유저스페이스의 read() 호출에 이벤트를 전달합니다
  • uio_info->mem[0]: 유저스페이스에 노출할 메모리 영역을 정의합니다. addr은 물리 주소, size는 영역 크기, memtype은 물리 메모리 타입입니다. 유저스페이스는 mmap(fd, 0)으로 이 영역에 접근합니다
  • internal_addr: 커널에서 사용하는 가상 주소입니다. 인터럽트 핸들러에서 레지스터에 접근할 때 사용합니다
  • devm_uio_register_device(): UIO 디바이스를 등록하여 /dev/uioN 노드를 생성합니다

유저스페이스에서 UIO 디바이스에 접근하는 방법은 다음과 같습니다:

/* 유저스페이스 UIO 접근 */
int fd = open("/dev/uio0", O_RDWR);
void *regs = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                   MAP_SHARED, fd, 0);

/* 레지스터 읽기/쓰기 */
uint32_t version = *(volatile uint32_t *)(regs + 0x00);
*(volatile uint32_t *)(regs + 0x04) = 0x1;

/* 인터럽트 대기 */
uint32_t irq_count;
read(fd, &irq_count, sizeof(irq_count));
코드 설명
  • mmap(NULL, 4096, ..., fd, 0): /dev/uio0의 첫 번째 메모리 영역(mem[0])을 유저스페이스 가상 주소에 매핑합니다. offset=0은 첫 번째 영역, offset=4096은 두 번째 영역에 해당합니다
  • volatile 포인터: 컴파일러가 레지스터 접근을 최적화하지 않도록 volatile을 사용합니다. 하드웨어 레지스터는 컴파일러가 예측할 수 없는 방식으로 값이 변경될 수 있습니다
  • read(fd, ...): 블로킹 호출로, 인터럽트가 발생할 때까지 대기합니다. 반환값은 인터럽트 발생 횟수입니다. select()/poll()/epoll()과 함께 사용할 수도 있습니다

VFIO 패스스루

VFIO(Virtual Function I/O)는 가상화 환경에서 FPGA를 가상 머신(VM)에 직접 할당(Passthrough)하기 위한 프레임워크입니다. UIO와 달리 IOMMU를 활용하여 DMA 격리를 보장하므로, VM이 다른 VM이나 호스트의 메모리에 접근하는 것을 방지합니다.

VFIO FPGA 패스스루의 주요 특징은 다음과 같습니다:

SR-IOV(Single Root I/O Virtualization)를 지원하는 FPGA 카드에서는, 하나의 물리적 FPGA(PF, Physical Function)를 여러 가상 기능(VF, Virtual Function)으로 분할할 수 있습니다. 각 VF는 독립적인 PCIe 디바이스로 보이므로, 각각을 서로 다른 VM에 할당할 수 있습니다. Intel PAC(Programmable Acceleration Card) 등 일부 FPGA 카드가 SR-IOV를 지원합니다.

VFIO 패스스루 설정 흐름은 다음과 같습니다:

  1. IOMMU 그룹 확인: ls /sys/kernel/iommu_groups/에서 FPGA가 속한 그룹을 확인합니다. 같은 IOMMU 그룹의 모든 디바이스가 함께 할당되어야 합니다
  2. vfio-pci 드라이버 바인드: FPGA를 기존 커널 드라이버에서 분리하고 vfio-pci 드라이버에 바인딩합니다
  3. VM 연결: QEMU 또는 다른 하이퍼바이저에서 VFIO 디바이스로 FPGA를 VM에 연결합니다
# FPGA의 IOMMU 그룹 확인
readlink /sys/bus/pci/devices/0000:03:00.0/iommu_group
# 출력 예: /sys/kernel/iommu_groups/15

# 기존 드라이버에서 분리
echo "0000:03:00.0" > /sys/bus/pci/devices/0000:03:00.0/driver/unbind

# vfio-pci 드라이버에 바인딩
echo "vfio-pci" > /sys/bus/pci/devices/0000:03:00.0/driver_override
echo "0000:03:00.0" > /sys/bus/pci/drivers_probe

# QEMU에서 FPGA 패스스루
qemu-system-x86_64 \
    -device vfio-pci,host=0000:03:00.0 \
    -m 4G \
    ...

UIO 드라이버 완전 예제

UIO 커널 모듈의 전체 구현을 보여줍니다. 메모리 매핑 정의, 인터럽트 핸들러 등록, 디바이스 등록까지 하나의 완전한 드라이버입니다.

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/uio_driver.h>
#include <linux/io.h>

#define FPGA_IRQ_STATUS  0x10
#define FPGA_IRQ_CLEAR   0x14
#define FPGA_IRQ_PENDING 0x01

struct my_fpga_uio {
    struct uio_info info;
    void __iomem *base;
};

static irqreturn_t my_fpga_irq(int irq, struct uio_info *info)
{
    struct my_fpga_uio *priv = container_of(info, struct my_fpga_uio, info);
    u32 status = ioread32(priv->base + FPGA_IRQ_STATUS);

    if (!(status & FPGA_IRQ_PENDING))
        return IRQ_NONE;

    iowrite32(status, priv->base + FPGA_IRQ_CLEAR);
    return IRQ_HANDLED;
}

static int my_fpga_uio_probe(struct platform_device *pdev)
{
    struct my_fpga_uio *priv;
    struct resource *res;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    priv->info.name = "my-fpga-accel";
    priv->info.version = "1.0";
    priv->info.mem[0].addr = res->start;
    priv->info.mem[0].size = resource_size(res);
    priv->info.mem[0].memtype = UIO_MEM_PHYS;
    priv->info.mem[0].internal_addr = priv->base;
    priv->info.irq = platform_get_irq(pdev, 0);
    priv->info.handler = my_fpga_irq;

    platform_set_drvdata(pdev, priv);
    return devm_uio_register_device(&pdev->dev, &priv->info);
}

유저스페이스에서 이 UIO 디바이스를 사용하는 완전한 예제입니다:

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdint.h>
#include <unistd.h>

int main(void)
{
    int fd = open("/dev/uio0", O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    /* FPGA 레지스터 영역 매핑 (offset 0 = mem[0]) */
    volatile uint32_t *regs = mmap(NULL, 4096,
        PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    /* 버전 레지스터 확인 */
    printf("FPGA version: 0x%08x\n", regs[0]);

    /* 가속기 시작 */
    regs[1] = 0x1;

    /* 인터럽트 대기 (블로킹) */
    uint32_t irq_count;
    read(fd, &irq_count, sizeof(irq_count));
    printf("IRQ count: %u, result: 0x%08x\n", irq_count, regs[2]);

    munmap((void *)regs, 4096);
    close(fd);
    return 0;
}

VFIO 패스스루 상세

VFIO를 활용한 FPGA 패스스루를 심층적으로 살펴봅니다.

IOMMU 그룹 개념

IOMMU 그룹은 IOMMU가 동일한 격리 단위로 관리하는 디바이스들의 집합입니다. 같은 PCIe 스위치 하위에 있거나 ACS(Access Control Services)가 비활성화된 디바이스들이 하나의 그룹을 형성합니다. VFIO로 FPGA를 패스스루하려면, 해당 FPGA가 속한 IOMMU 그룹의 모든 디바이스를 함께 할당해야 합니다. 그렇지 않으면 격리가 보장되지 않습니다.

vfio-pci 바인딩 절차

FPGA 디바이스를 vfio-pci 드라이버에 바인딩하는 전체 과정입니다:

#!/bin/bash
# VFIO FPGA 패스스루 설정 스크립트

# 1. IOMMU 그룹 확인
BDF="0000:03:00.0"
GROUP=$(basename $(readlink /sys/bus/pci/devices/$BDF/iommu_group))
echo "FPGA $BDF is in IOMMU group $GROUP"

# 2. 그룹 내 모든 디바이스 확인
echo "Devices in group $GROUP:"
ls /sys/kernel/iommu_groups/$GROUP/devices/

# 3. vfio-pci 모듈 로드
modprobe vfio-pci

# 4. Vendor/Device ID로 바인딩
echo "8086 0b30" > /sys/bus/pci/drivers/vfio-pci/new_id

# 또는 driver_override 방식
echo $BDF > /sys/bus/pci/devices/$BDF/driver/unbind
echo "vfio-pci" > /sys/bus/pci/devices/$BDF/driver_override
echo $BDF > /sys/bus/pci/drivers_probe

# 5. VFIO 디바이스 파일 확인
ls -la /dev/vfio/$GROUP

DPDK 연동

DPDK(Data Plane Development Kit)는 VFIO를 통해 FPGA의 DMA 엔진에 직접 접근할 수 있습니다. 커널 바이패스 방식으로 네트워크 패킷 처리를 가속하는 구조입니다. DPDK의 rte_eal_init()이 VFIO 컨테이너를 설정하고, rte_pci_map_device()가 FPGA BAR를 유저스페이스에 매핑합니다. 이를 통해 FPGA 내부의 DMA 엔진 레지스터에 직접 접근하여, 커널 개입 없이 패킷을 송수신할 수 있습니다.

보안 이점

VFIO의 핵심 보안 이점은 IOMMU에 의한 DMA 영역 제한입니다. FPGA는 하드웨어 로직이 사용자에 의해 프로그래밍되므로, 악의적인 DMA 접근을 시도하는 로직이 포함될 수 있습니다. IOMMU가 DMA 트랜잭션을 가로채어 허용된 물리 주소 범위로만 접근을 제한하므로, FPGA가 호스트 메모리의 민감한 영역이나 다른 VM의 메모리에 접근하는 것을 방지합니다. VM 환경에서는 각 VM에 할당된 FPGA가 해당 VM의 메모리에만 DMA를 수행할 수 있도록 IOMMU 페이지 테이블이 설정됩니다.

Device Tree Overlay와 FPGA

Device Tree(DT) Overlay는 FPGA 재구성과 밀접하게 연동되는 메커니즘으로, 런타임에 FPGA에 로드된 하드웨어 로직에 대한 디바이스 노드를 동적으로 추가하거나 제거할 수 있습니다. 이를 통해 FPGA 비트스트림 로드와 커널 드라이버 바인딩이 자동으로 연계됩니다.

오버레이 개념

전통적인 Device Tree는 시스템 부팅 시 한 번 로드되어, 고정된 하드웨어 구성을 기술합니다. 그러나 FPGA는 런타임에 하드웨어가 변경될 수 있으므로, 기존 DT만으로는 FPGA에 구현된 하드웨어를 표현할 수 없습니다. DT Overlay는 이 문제를 해결합니다:

구분 설명
Base DT 시스템의 정적 하드웨어를 기술합니다. PS(Processing System), 메모리 컨트롤러, UART, 이더넷 등 항상 존재하는 디바이스가 여기에 포함됩니다. 또한 FPGA Manager, Bridge, Region 노드도 Base DT에 정의됩니다
Overlay DT FPGA에 로드된 로직이 구현하는 하드웨어를 기술합니다. SPI 컨트롤러, I2C 마스터, GPIO, DMA 엔진, 사용자 정의 가속기 등이 될 수 있습니다. 비트스트림과 함께 .dtbo 파일로 배포됩니다

Overlay를 적용하면 Base DT의 특정 노드(주로 FPGA Region)에 자식 노드들이 동적으로 추가됩니다. 해제하면 추가된 노드들이 제거되고, 해당 드라이버도 자동으로 unbind됩니다.

FPGA 관련 DT 바인딩

FPGA 서브시스템의 DT 바인딩은 Base DT에서 Manager, Bridge, Region을 정의하는 것으로 시작합니다. 다음은 Intel(Altera) SoC FPGA의 DT 구성 예입니다:

/* Base Device Tree — FPGA Manager와 Region 정의 */
fpga_mgr0: fpga-mgr@ff706000 {
    compatible = "altr,socfpga-fpga-mgr";
    reg = <0xff706000 0x1000>;
    interrupts = <0 175 4>;
};

fpga_bridge0: fpga-bridge@ff400000 {
    compatible = "altr,socfpga-lwhps2fpga-bridge";
    reg = <0xff400000 0x100000>;
    resets = <&rst LWHPS2FPGA_RESET>;
    clocks = <&l4_main_clk>;
};

fpga_region0: fpga-region {
    compatible = "fpga-region";
    fpga-mgr = <&fpga_mgr0>;
    fpga-bridges = <&fpga_bridge0>;
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;
};
코드 설명
  • fpga-mgr@ff706000: SoC FPGA의 FPGA Manager 하드웨어 레지스터가 0xff706000에 위치합니다. compatible 문자열로 socfpga.c 드라이버와 매칭됩니다
  • fpga-bridge@ff400000: Lightweight HPS-to-FPGA 브리지입니다. 리셋과 클록 참조가 포함되어, 브리지 활성화/비활성화 시 하드웨어 리셋과 클록을 제어합니다
  • fpga-region: fpga-mgr phandle로 Manager를, fpga-bridges phandle 리스트로 Bridge들을 참조합니다. ranges 속성으로 자식 노드의 주소가 부모 주소 공간에 직접 매핑됨을 명시합니다

FPGA에 SPI 컨트롤러를 구현한 비트스트림을 로드하는 DT Overlay 예입니다:

/* DT Overlay — FPGA에 로드된 SPI 컨트롤러 */
/dts-v1/;
/plugin/;

&fpga_region0 {
    firmware-name = "spi_accel.rbf";

    spi@0 {
        compatible = "vendor,fpga-spi";
        reg = <0x00020000 0x100>;
        interrupts = <0 43 4>;
        #address-cells = <1>;
        #size-cells = <0>;

        flash@0 {
            compatible = "jedec,spi-nor";
            reg = <0>;
            spi-max-frequency = <50000000>;
        };
    };
};
코드 설명
  • /plugin/;: 이 DTS가 Overlay임을 나타냅니다. Overlay 컴파일러(dtc -@)가 이를 인식하여 적절한 fixup 정보를 생성합니다
  • &fpga_region0: Base DT의 fpga_region0 노드를 참조합니다. 이 Region에 Overlay의 내용이 추가됩니다
  • firmware-name: Region이 이 속성을 읽어 /lib/firmware/spi_accel.rbf에서 비트스트림을 로드합니다
  • spi@0: FPGA에 구현된 SPI 컨트롤러의 디바이스 노드입니다. reg = <0x00020000 0x100>은 FPGA 주소 공간 내 오프셋입니다
  • flash@0: SPI 컨트롤러에 연결된 NOR 플래시 칩의 디바이스 노드입니다. Overlay 적용 후 jedec,spi-nor 드라이버가 자동으로 probe됩니다

런타임 오버레이 적용

DT Overlay를 런타임에 적용하는 방법은 크게 두 가지입니다:

1. configfs 방식 (유저스페이스)

configfs를 통해 유저스페이스에서 DT Overlay를 적용할 수 있습니다. /sys/kernel/config/device-tree/overlays/ 디렉토리에 서브디렉토리를 만들고, 컴파일된 Overlay 바이너리(.dtbo)를 dtbo 파일에 쓰면 됩니다:

# configfs를 통한 DT Overlay 적용
# 1. Overlay 디렉토리 생성
mkdir /sys/kernel/config/device-tree/overlays/fpga0

# 2. 컴파일된 Overlay 바이너리 적용
cat spi_accel.dtbo > /sys/kernel/config/device-tree/overlays/fpga0/dtbo

# 적용 상태 확인
cat /sys/kernel/config/device-tree/overlays/fpga0/status
# 출력: "applied"

# FPGA 재구성과 새 디바이스가 자동으로 생성되었는지 확인
ls /sys/class/spi_master/
# 출력: spi0 (새로 생성된 SPI 마스터)

ls /dev/mtd*
# 출력: /dev/mtd0 (SPI NOR 플래시)

# Overlay 해제 (FPGA 디바이스와 드라이버 자동 제거)
rmdir /sys/kernel/config/device-tree/overlays/fpga0

2. 커널 API 방식

커널 모듈이나 드라이버에서 프로그래밍 방식으로 Overlay를 적용할 수 있습니다:

Overlay의 적용과 해제는 FPGA Region의 재구성과 연동됩니다. Overlay를 적용하면 Region이 자동으로 비트스트림을 로드하고, Overlay를 해제하면 Region이 브리지를 비활성화하여 FPGA 로직에 대한 접근을 차단합니다. 이 자동 연동 덕분에, FPGA의 하드웨어 변경과 소프트웨어(드라이버) 변경이 단일 작업(Overlay 적용/해제)으로 일관되게 처리됩니다.

DT Overlay를 사용할 때 주의할 점은 다음과 같습니다:

DTS 컴파일과 적용

DTS(Device Tree Source) 파일을 DTBO(Device Tree Blob Overlay)로 컴파일하고 런타임에 적용하는 전체 과정을 설명합니다.

컴파일

dtc(Device Tree Compiler)를 사용하여 .dts 소스를 .dtbo 바이너리로 변환합니다. -@ 옵션은 심볼 정보를 포함하여, Overlay가 Base DT의 노드를 phandle로 참조할 수 있게 합니다:

# DTS → DTBO 컴파일
dtc -@ -O dtb -o fpga_accel.dtbo fpga_accel.dts

# 컴파일된 DTBO 내용 확인 (역변환)
dtc -I dtb -O dts fpga_accel.dtbo

configfs를 통한 런타임 적용과 해제

#!/bin/bash
# DT Overlay 적용/확인/해제 스크립트

OVERLAY_NAME="accel0"
DTBO_FILE="fpga_accel.dtbo"
OVERLAY_DIR="/sys/kernel/config/device-tree/overlays"

# 1. Overlay 디렉토리 생성
mkdir -p ${OVERLAY_DIR}/${OVERLAY_NAME}

# 2. DTBO 바이너리 적용
cat ${DTBO_FILE} > ${OVERLAY_DIR}/${OVERLAY_NAME}/dtbo

# 3. 적용 상태 확인
STATUS=$(cat ${OVERLAY_DIR}/${OVERLAY_NAME}/status)
echo "Overlay status: ${STATUS}"

# 4. 적용된 노드 확인
ls /proc/device-tree/fpga-region@0/

# 5. Overlay 해제 (디바이스 노드 제거 + 드라이버 unbind)
rmdir ${OVERLAY_DIR}/${OVERLAY_NAME}

# 해제 후 노드가 사라졌는지 확인
ls /proc/device-tree/fpga-region@0/ 2>/dev/null || \
    echo "Overlay successfully removed"

복잡한 오버레이 예제

실제 시스템에서는 하나의 Overlay가 여러 PR 슬롯을 동시에 재구성하거나, 디바이스 간 의존성 체인을 포함하는 경우가 있습니다.

다중 PR 슬롯 오버레이

두 개의 FPGA Region을 하나의 Overlay로 동시에 재구성하는 예시입니다. Region마다 별도의 비트스트림을 지정하고, 각각에 구현된 하드웨어를 디바이스 노드로 추가합니다:

/* 다중 PR 슬롯 오버레이 */
/dts-v1/;
/plugin/;

&fpga_region0 {
    firmware-name = "slot0_dma_engine.rbf";
    dma@0x20000 {
        compatible = "my,fpga-dma";
        reg = <0x20000 0x1000>;
        interrupts = <0 89 4>;
    };
};

&fpga_region1 {
    firmware-name = "slot1_crypto.rbf";
    crypto@0x30000 {
        compatible = "my,fpga-crypto";
        reg = <0x30000 0x2000>;
        interrupts = <0 90 4>;
    };
};

디바이스 의존성 체인

FPGA에 SPI 컨트롤러 → SPI NOR 플래시 → MTD 파티션 체인을 구현한 경우, Overlay에서 이 의존성을 정확히 표현해야 합니다. 커널은 DT의 부모-자식 관계에 따라 SPI 컨트롤러를 먼저 probe한 후, NOR 플래시 드라이버를 probe하고, 최종적으로 MTD 파티션이 등록됩니다. 이 순서가 보장되려면, SPI 컨트롤러 노드 하위에 NOR 플래시 노드가, NOR 플래시 노드 하위에 파티션 노드가 위치해야 합니다.

오버레이 디버깅

DT Overlay 적용에 실패하는 경우의 진단 방법입니다.

/proc/device-tree/ 탐색

Overlay 적용 후 기대한 노드가 Device Tree에 추가되었는지 확인하는 가장 기본적인 방법입니다:

# Overlay 적용 후 노드 확인
find /proc/device-tree/ -name "compatible" | while read f; do
    echo "$f: $(cat $f | tr '\0' ' ')"
done

# 특정 노드의 속성 확인
hexdump -C /proc/device-tree/fpga-region@0/dma@20000/reg

of_overlay 에러 분석

dmesg에서 of_overlay 관련 에러 메시지를 분석하여 원인을 파악합니다:

에러 메시지 원인 해결 방법
"property ... not found" 필수 속성이 Overlay에 누락됨 DTS에서 해당 속성을 추가합니다
"node ... already exists" 이미 존재하는 노드를 중복 추가 시도 기존 Overlay를 먼저 해제하거나, 노드명을 변경합니다
"phandle ... not found" Base DT에 참조 대상 노드가 없음 Base DT에 해당 노드와 __symbols__가 있는지 확인합니다
"Failed to apply overlay" 리소스 충돌(주소/인터럽트 겹침) Overlay 간 주소/인터럽트 범위가 겹치지 않도록 조정합니다

가장 흔한 실패 원인은 phandle 불일치입니다. Base DT를 dtc -@ 옵션 없이 컴파일하면 __symbols__ 노드가 생성되지 않아, Overlay가 Base DT의 노드를 레이블로 참조할 수 없게 됩니다. 커널 부트 시 DT에 CONFIG_OF_RESOLVE=y가 설정되어 있어야 하며, Base DT 컴파일 시 반드시 -@ 옵션을 사용해야 합니다.

SoC FPGA

SoC FPGA(System on Chip FPGA)는 ARM 프로세서와 FPGA 패브릭(Fabric)을 단일 칩에 통합한 디바이스입니다. 프로세서 시스템(PS, Processing System)에서 리눅스를 실행하면서, 프로그래머블 로직(PL, Programmable Logic)에서 하드웨어 가속을 수행할 수 있습니다. PS와 PL 사이의 고대역폭 인터커넥트(Interconnect)를 통해 밀접하게 결합된 이기종 컴퓨팅(Heterogeneous Computing) 환경을 구성할 수 있습니다.

Xilinx Zynq-7000 / Zynq UltraScale+ MPSoC

Xilinx(현 AMD) Zynq 시리즈는 SoC FPGA의 대표적 제품군입니다.

Zynq-7000

Zynq UltraScale+ MPSoC (ZynqMP)

/* Zynq-7000 FPGA Manager 등록 흐름 (drivers/fpga/zynq-fpga.c) */
static const struct fpga_manager_ops zynq_fpga_ops = {
    .state          = zynq_fpga_ops_state,
    .write_init     = zynq_fpga_ops_write_init,
    .write          = zynq_fpga_ops_write,
    .write_complete = zynq_fpga_ops_write_complete,
};

static int zynq_fpga_probe(struct platform_device *pdev)
{
    struct zynq_fpga_priv *priv;
    struct fpga_manager *mgr;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* DevCfg 레지스터 매핑 */
    priv->io_base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->io_base))
        return PTR_ERR(priv->io_base);

    /* FPGA Manager 등록 — 프레임워크가 sysfs 인터페이스 자동 생성 */
    mgr = devm_fpga_mgr_register(&pdev->dev, "Xilinx Zynq FPGA Manager",
                                 &zynq_fpga_ops, priv);
    return PTR_ERR_OR_ZERO(mgr);
}

Intel Cyclone V SoC / Agilex SoC

Intel(구 Altera) SoC FPGA 제품군은 하드 프로세서 시스템(HPS)과 FPGA 패브릭을 통합합니다.

Cyclone V SoC

Agilex SoC

/* Intel SoC FPGA Freeze Bridge 드라이버 개념 (socfpga-freeze-bridge.c) */
static int socfpga_freeze_br_enable_set(struct fpga_bridge *bridge, bool enable)
{
    struct socfpga_freeze_br_priv *priv = bridge->priv;

    if (enable) {
        /* Freeze 해제: FPGA↔HPS 트래픽 허용 */
        writel(FREEZE_CTRL_UNFREEZE, priv->base + FREEZE_CTRL_OFFSET);
        return socfpga_freeze_br_wait_unfreeze(priv);
    } else {
        /* Freeze 설정: FPGA↔HPS 트래픽 차단 (재프로그래밍 안전 보장) */
        writel(FREEZE_CTRL_FREEZE, priv->base + FREEZE_CTRL_OFFSET);
        return socfpga_freeze_br_wait_freeze(priv);
    }
}

PS-PL 인터커넥트

SoC FPGA에서 PS(Processing System)와 PL(Programmable Logic) 사이의 데이터 교환은 여러 종류의 AXI 포트를 통해 이루어집니다. 각 포트는 용도와 성능 특성이 다르며, 설계 시 적절한 포트를 선택하는 것이 시스템 성능에 큰 영향을 미칩니다.

GP(General Purpose) 포트

HP(High Performance) 포트

ACP(Accelerator Coherency Port)

HPC(High Performance Coherent) 포트 — ZynqMP 전용

SoC FPGA PS-PL 인터커넥트 PS (Processing System) ARM Cortex-A53 × 4 L2 캐시 (1MB) DDR4 컨트롤러 GIC-400 주변장치: GigE, USB 3.0, SPI, I2C, UART, SD/SDIO PS 내부 AXI 인터커넥트 PL (Programmable Logic) CLB 어레이 BRAM / URAM DSP48E2 슬라이스 사용자 로직 PL 내부 AXI 인터커넥트 AXI 브리지 GP (32b) HP (128b) ACP (캐시 일관) HPC (128b, 일관) ~0.6 GB/s ~6.4 GB/s ×4 ~2.4 GB/s ~6.4 GB/s ×2

AXI 인터커넥트

AXI(Advanced eXtensible Interface)는 ARM에서 정의한 AMBA 버스 프로토콜의 일부로, SoC FPGA에서 PS와 PL 간의 데이터 전송에 사용되는 표준 인터페이스입니다. AXI 프로토콜에는 세 가지 주요 변종이 있으며, 각각 다른 용도에 최적화되어 있습니다.

특성 AXI4 (Full) AXI4-Lite AXI4-Stream
용도 메모리 매핑 고속 전송 레지스터 접근 스트리밍 데이터
버스트 전송 최대 256비트 없음 (단일 전송만) 해당 없음 (연속 스트림)
데이터 폭 8~1024비트 32비트 또는 64비트 가변
주소 지정 있음 (메모리 매핑) 있음 (메모리 매핑) 없음 (포인트-투-포인트)
핸드셰이크 VALID/READY VALID/READY VALID/READY + TLAST
채널 AW, W, B, AR, R AW, W, B, AR, R 단일 채널
복잡도 높음 낮음 중간
대표 용도 DMA, 메모리 접근 제어/상태 레지스터 비디오, 네트워크 데이터

AXI4 (Full)은 고대역폭 메모리 접근에 사용합니다. 버스트 전송(Burst Transfer)을 지원하여 한 번의 주소 지정으로 최대 256비트의 데이터를 연속으로 전송할 수 있습니다. 5개의 독립 채널(Write Address, Write Data, Write Response, Read Address, Read Data)을 통해 읽기와 쓰기를 동시에 수행할 수 있습니다.

AXI4-Lite는 단순한 레지스터 읽기/쓰기에 사용합니다. 버스트 전송을 지원하지 않으며, 매 전송마다 주소를 지정해야 합니다. FPGA 가속기의 제어 레지스터 인터페이스 구현에 주로 사용합니다.

AXI4-Stream은 포인트-투-포인트 스트리밍 데이터 전송에 사용합니다. 주소 개념이 없으며, VALID/READY 핸드셰이크와 TLAST 신호로 패킷 경계를 표시합니다. 비디오 파이프라인, 네트워크 패킷 처리 등 연속적인 데이터 흐름에 적합합니다.

AXI Interconnect IP는 여러 마스터와 슬레이브를 연결하는 스위치 역할을 합니다. 주소 디코딩, 데이터 폭 변환(예: 32비트 마스터 ↔ 64비트 슬레이브), 클록 도메인 교차(Clock Domain Crossing), 버스트 변환 등의 기능을 제공합니다.

PetaLinux / Yocto 기반 리눅스

SoC FPGA에서 리눅스를 실행하려면 하드웨어 설계에 맞는 BSP(Board Support Package)가 필요합니다. PetaLinux은 Xilinx(AMD)에서 제공하는 공식 임베디드 리눅스 개발 도구로, Yocto/OpenEmbedded 기반으로 동작합니다.

PetaLinux 워크플로

  1. Vivado에서 하드웨어 정의 내보내기: 블록 디자인(BD)을 완성한 후 XSA(Xilinx Shell Archive) 파일을 생성합니다
  2. PetaLinux 프로젝트 생성: petalinux-create 명령으로 프로젝트를 생성합니다
  3. 하드웨어 가져오기: petalinux-config --get-hw-description으로 XSA를 가져오면, 디바이스 트리(Device Tree)가 자동으로 생성됩니다
  4. 커널/rootfs 커스터마이징: petalinux-config -c kernel, petalinux-config -c rootfs로 구성을 변경합니다
  5. 빌드 및 패키징: petalinux-build, petalinux-package로 부팅 이미지를 생성합니다
# PetaLinux 워크플로 예시
# 1. 프로젝트 생성
petalinux-create -t project --template zynqMP -n my_project
cd my_project

# 2. 하드웨어 정의 가져오기 (Vivado에서 내보낸 XSA)
petalinux-config --get-hw-description=../vivado_export/system.xsa

# 3. 커널 설정 (FPGA 관련 옵션 활성화)
petalinux-config -c kernel

# 4. rootfs에 사용자 패키지 추가
petalinux-config -c rootfs

# 5. 빌드
petalinux-build

# 6. 부팅 이미지 패키징 (BOOT.BIN: FSBL + PMU FW + ATF + U-Boot + bitstream)
petalinux-package --boot --fsbl images/linux/zynqmp_fsbl.elf \
    --u-boot images/linux/u-boot.elf \
    --pmufw images/linux/pmufw.elf \
    --fpga images/linux/system.bit \
    --atf images/linux/bl31.elf

Yocto/OpenEmbedded 직접 사용

PetaLinux 대신 Yocto/OpenEmbedded를 직접 사용할 수도 있습니다. 이 경우 다음 메타 레이어를 사용합니다:

Device Tree는 하드웨어 설계에 따라 자동 생성되며, FPGA 내부의 IP 블록들이 디바이스 노드로 표현됩니다. 이를 통해 리눅스 커널이 FPGA 내부의 가속기, DMA 컨트롤러 등을 자동으로 인식하고 드라이버를 바인딩합니다.

IP 코어와 AXI 프로토콜

FPGA 설계에서 IP 코어(Intellectual Property Core)는 재사용 가능한 하드웨어 설계 블록을 의미합니다. 현대 FPGA SoC 설계의 핵심은 이러한 IP 코어들을 표준 버스 프로토콜로 연결하는 것이며, ARM이 개발한 AMBA(Advanced Microcontroller Bus Architecture) AXI(Advanced eXtensible Interface) 프로토콜이 사실상의 업계 표준으로 자리 잡고 있습니다.

AXI 프로토콜 상세

AMBA AXI 프로토콜은 버전과 용도에 따라 세 가지 변종으로 나뉩니다. 각 변종은 서로 다른 사용 사례에 최적화되어 있습니다.

특성AXI4 (Full)AXI4-LiteAXI4-Stream
용도고성능 메모리 매핑 I/O저속 제어/상태 레지스터연속 데이터 스트리밍
주소 지정메모리 매핑 (주소 채널)메모리 매핑 (단순화)주소 없음 (스트림)
버스트 지원최대 256비트단일 전송만해당 없음
데이터 폭32/64/128/256/512/1024비트32/64비트임의 폭
순서 보장ID 기반 순서 (out-of-order 가능)항상 순차FIFO 순서
대표 IPDDR 컨트롤러, DMAGPIO, UART, SPI 제어비디오, DSP, 네트워크

AXI4 5채널 아키텍처

AXI4 풀(Full) 프로토콜은 5개의 독립 채널로 구성되며, 각 채널은 유효(VALID)/준비(READY) 핸드셰이크로 독립적으로 동작합니다. 이 분리된 채널 구조가 높은 처리량과 파이프라인 효율을 가능하게 합니다.

버스트 타입

AXI4는 세 가지 버스트 타입을 지원합니다. 버스트 타입은 연속 전송에서 주소가 어떻게 변화하는지를 결정합니다.

미처리 트랜잭션(Outstanding Transaction)과 비순차 완료(Out-of-Order Completion)

AXI4 프로토콜은 높은 처리량을 위해 미처리 트랜잭션(Outstanding Transaction)을 지원합니다. 마스터는 이전 트랜잭션의 응답을 기다리지 않고 여러 개의 읽기/쓰기 요청을 연속으로 발행할 수 있습니다. 슬레이브는 AWID/ARID 필드를 통해 각 트랜잭션을 구분하며, 서로 다른 ID의 트랜잭션은 비순차적으로(out-of-order) 완료될 수 있습니다. 동일 ID의 트랜잭션은 발행 순서대로 완료되어야 합니다.

이 기능은 DRAM 컨트롤러처럼 접근 레이턴시가 가변적인 슬레이브에서 특히 유용합니다. 하나의 요청이 뱅크 충돌(Bank Conflict)로 지연되더라도, 다른 요청은 먼저 완료될 수 있어 전체 대역폭 활용도가 향상됩니다.

AXI4 5채널 토폴로지 — 마스터/슬레이브, 크로스바 인터커넥트 CPU / DMA AXI Master 0 AW, W, AR → ← B, R 가속기 IP AXI Master 1 AW, W, AR → ← B, R 네트워크 IP AXI Master 2 AW, W, AR → ← B, R AXI 크로스바 인터커넥트 주소 디코더 Address Decoder 쓰기 채널 스위치 AW + W + B 읽기 채널 스위치 AR + R 중재기(Arbiter) 라운드 로빈 / 우선순위 기반 Outstanding 트랜잭션 추적 리오더 버퍼 ID 기반 비순차 완료 지원 DDR 컨트롤러 AXI Slave 0 0x0000_0000 ~ 0x3FFF_FFFF 주변장치 (AXI-Lite) AXI Slave 1 0x4000_0000 ~ 0x7FFF_FFFF BRAM 컨트롤러 AXI Slave 2 0xC000_0000 ~ 0xC000_FFFF 5채널: AW (Write Address) W (Write Data) B (Write Response) AR (Read Address) R (Read Data) 각 채널은 VALID/READY 핸드셰이크로 독립 동작 — 다수 마스터/슬레이브 간 동시 전송 가능

AXI4-Stream 데이터 폭 변환기

AXI4-Stream은 주소 없이 연속 데이터를 전송하는 프로토콜로, 비디오/오디오/네트워크 데이터 스트림에 널리 사용됩니다. 서로 다른 데이터 폭의 IP를 연결할 때 폭 변환기(Width Converter)가 필요합니다. 다음은 좁은 스트림을 넓은 스트림으로 변환하는(Narrow to Wide) SystemVerilog RTL 예제입니다.

// AXI4-Stream 폭 변환기: 32비트 → 128비트 (4:1 패킹)
module axis_width_conv #(
    parameter int IN_WIDTH  = 32,
    parameter int OUT_WIDTH = 128,
    parameter int RATIO     = OUT_WIDTH / IN_WIDTH  // 4
)(
    input  logic                  clk,
    input  logic                  rst_n,

    // 입력 AXI4-Stream (좁은 쪽)
    input  logic [IN_WIDTH-1:0]   s_tdata,
    input  logic                  s_tvalid,
    output logic                  s_tready,
    input  logic                  s_tlast,

    // 출력 AXI4-Stream (넓은 쪽)
    output logic [OUT_WIDTH-1:0] m_tdata,
    output logic                  m_tvalid,
    input  logic                  m_tready,
    output logic                  m_tlast
);
    logic [$clog2(RATIO)-1:0] cnt;
    logic [OUT_WIDTH-1:0]       shift_reg;
    logic                        last_seen;

    // 입력 핸드셰이크: 출력 중이 아니면 수신 가능
    assign s_tready = !m_tvalid || m_tready;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt       <= '0;
            shift_reg <= '0;
            m_tvalid  <= 1'b0;
            m_tlast   <= 1'b0;
            last_seen <= 1'b0;
        end else begin
            // 출력 핸드셰이크 완료 시 valid 해제
            if (m_tvalid && m_tready) begin
                m_tvalid  <= 1'b0;
                m_tlast   <= 1'b0;
                last_seen <= 1'b0;
            end

            // 입력 핸드셰이크: 데이터를 시프트 레지스터에 패킹
            if (s_tvalid && s_tready) begin
                shift_reg[cnt*IN_WIDTH +: IN_WIDTH] <= s_tdata;
                if (s_tlast) last_seen <= 1'b1;

                if (cnt == RATIO - 1 || s_tlast) begin
                    // RATIO개 수집 완료 또는 tlast → 출력
                    m_tvalid <= 1'b1;
                    m_tlast  <= s_tlast || last_seen;
                    cnt      <= '0;
                end else begin
                    cnt <= cnt + 1;
                end
            end
        end
    end

    assign m_tdata = shift_reg;
endmodule

IP 통합 설계

현대 FPGA SoC 설계에서는 수십~수백 개의 IP 코어를 시각적 블록 디자인 환경에서 연결합니다. 주요 벤더별 통합 도구와 워크플로는 다음과 같습니다.

Vivado IP Integrator (IPI)

AMD/Xilinx의 Vivado IP Integrator는 블록 디자인(Block Design) 방식으로 IP를 연결합니다. Zynq PS(Processing System) 블록을 중심으로 AXI 인터커넥트, AXI GPIO, AXI DMA 등의 IP를 드래그 앤 드롭으로 배치하고, 자동 연결(Run Connection Automation) 기능으로 AXI 주소 매핑과 클록/리셋 연결을 자동화합니다. 완성된 블록 디자인은 .bd 파일로 저장되며, generate_target 명령으로 HDL 래퍼와 주소 맵이 생성됩니다.

Intel Platform Designer (Qsys)

Intel(구 Altera)의 Platform Designer(구 Qsys)는 Avalon 버스 프로토콜을 기본으로 사용하지만, AXI 어댑터를 통해 AXI IP도 통합할 수 있습니다. Nios II/V 소프트 프로세서 또는 HPS(Hard Processor System)를 중심으로 시스템을 구성합니다. .qsys 파일로 저장되며, qsys-generate로 HDL을 생성합니다.

IP 패키징 (Vivado IP Packager)

커스텀 RTL을 재사용 가능한 IP로 패키징하려면 다음 요소를 정의해야 합니다:

주소 매핑과 인터커넥트 구성

AXI 인터커넥트는 마스터-슬레이브 간 연결을 관리합니다. 주소 디코더가 마스터의 요청 주소를 기반으로 적절한 슬레이브에 라우팅하며, 중재기(Arbiter)가 다수 마스터의 동시 접근을 조율합니다. Vivado에서는 Address Editor에서 각 슬레이브의 베이스 주소와 크기를 지정하며, 이 주소 맵이 Device Tree 생성 시 reg 속성으로 반영됩니다.

AXI DMA 엔진 설계

DMA(Direct Memory Access)는 CPU 개입 없이 FPGA IP와 메모리 사이에서 대용량 데이터를 전송하는 핵심 메커니즘입니다. FPGA에서 DMA 엔진을 직접 설계하면 애플리케이션 요구사항에 정확히 맞는 전송 패턴을 구현할 수 있습니다.

스캐터-개더 DMA(Scatter-Gather DMA) 아키텍처

스캐터-개더 DMA는 물리적으로 불연속적인 메모리 영역을 하나의 DMA 트랜잭션으로 처리합니다. 디스크립터(Descriptor) 체인이 각 전송의 소스/목적지 주소, 길이, 다음 디스크립터 포인터를 포함합니다.

Linux DMA engine 프레임워크와의 통합

Linux 커널의 DMA engine 프레임워크(drivers/dma/)는 DMA 채널의 할당, 디스크립터 준비, 전송 제출, 완료 콜백을 표준화합니다. FPGA 커스텀 DMA 엔진의 드라이버는 struct dma_device를 등록하고, device_prep_dma_memcpy(), device_prep_slave_sg() 등의 콜백을 구현합니다.

DMA 서브시스템 상세는 DMA (Direct Memory Access) 페이지를 참고하세요.

AXI4 마스터 DMA 읽기 엔진 예제

다음은 메모리에서 데이터를 읽어 내부 FIFO로 전달하는 간단한 AXI4 마스터 DMA 읽기 엔진의 SystemVerilog 스니펫입니다.

// 간단한 AXI4 마스터 DMA 읽기 엔진
module axi_dma_read #(
    parameter int ADDR_W = 32,
    parameter int DATA_W = 64,
    parameter int BURST_LEN = 16   // 버스트당 비트 수 (ARLEN = BURST_LEN-1)
)(
    input  logic              clk,
    input  logic              rst_n,

    // 제어 인터페이스
    input  logic              start,
    input  logic [ADDR_W-1:0] src_addr,
    input  logic [15:0]       xfer_len,  // 총 전송 바이트
    output logic              done,
    output logic              irq,

    // AXI4 읽기 채널 (AR + R)
    output logic [ADDR_W-1:0] m_axi_araddr,
    output logic [7:0]        m_axi_arlen,
    output logic [2:0]        m_axi_arsize,
    output logic [1:0]        m_axi_arburst,
    output logic              m_axi_arvalid,
    input  logic              m_axi_arready,
    input  logic [DATA_W-1:0] m_axi_rdata,
    input  logic [1:0]        m_axi_rresp,
    input  logic              m_axi_rlast,
    input  logic              m_axi_rvalid,
    output logic              m_axi_rready,

    // 데이터 출력 FIFO
    output logic [DATA_W-1:0] fifo_wdata,
    output logic              fifo_wen,
    input  logic              fifo_full
);
    typedef enum logic [1:0] {
        IDLE, ADDR, DATA, COMPLETE
    } state_t;

    state_t state;
    logic [ADDR_W-1:0] cur_addr;
    logic [15:0]       remaining;

    localparam int BYTES_PER_BEAT = DATA_W / 8;
    localparam int BYTES_PER_BURST = BURST_LEN * BYTES_PER_BEAT;

    // AXI 고정 신호
    assign m_axi_arsize  = $clog2(BYTES_PER_BEAT);
    assign m_axi_arburst = 2'b01;  // INCR
    assign m_axi_rready  = !fifo_full;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= IDLE; done <= 0; irq <= 0;
            m_axi_arvalid <= 0;
        end else begin
            irq <= 0;
            case (state)
                IDLE: begin
                    done <= 0;
                    if (start) begin
                        cur_addr  <= src_addr;
                        remaining <= xfer_len;
                        state     <= ADDR;
                    end
                end
                ADDR: begin
                    m_axi_araddr  <= cur_addr;
                    m_axi_arlen   <= (remaining >= BYTES_PER_BURST)
                                     ? BURST_LEN - 1 : (remaining / BYTES_PER_BEAT) - 1;
                    m_axi_arvalid <= 1;
                    if (m_axi_arvalid && m_axi_arready) begin
                        m_axi_arvalid <= 0;
                        state         <= DATA;
                    end
                end
                DATA: begin
                    if (m_axi_rvalid && m_axi_rready) begin
                        remaining <= remaining - BYTES_PER_BEAT;
                        cur_addr  <= cur_addr + BYTES_PER_BEAT;
                        if (m_axi_rlast) begin
                            state <= (remaining <= BYTES_PER_BURST) ? COMPLETE : ADDR;
                        end
                    end
                end
                COMPLETE: begin
                    done  <= 1;
                    irq   <= 1;
                    state <= IDLE;
                end
            endcase
        end
    end

    assign fifo_wdata = m_axi_rdata;
    assign fifo_wen   = m_axi_rvalid && m_axi_rready;
endmodule

FPGA for Linux 커널 개발자

이 섹션은 FPGA 하드웨어 설계를 Linux 커널 드라이버와 연결하는 전체 워크플로를 다룹니다. RTL 설계부터 비트스트림 로딩, 디바이스 트리 바인딩, 커널 드라이버 프로빙, 유저 스페이스 접근까지의 End-to-End 과정을 설명합니다.

커스텀 주변장치 개발

RTL → 드라이버 End-to-End 워크플로

FPGA에서 커스텀 주변장치를 만들어 Linux에서 사용하려면, 하드웨어 설계(RTL)와 소프트웨어(커널 드라이버)를 동시에 개발해야 합니다. 전체 워크플로는 다음과 같습니다:

  1. RTL 설계: AXI-Lite 슬레이브 인터페이스를 포함하는 커스텀 IP를 Verilog/SystemVerilog로 설계합니다
  2. 시뮬레이션 검증: SystemVerilog 테스트벤치로 AXI 트랜잭션 레벨 검증을 수행합니다
  3. IP 패키징: Vivado IP Packager로 IP를 패키징하고 블록 디자인에 통합합니다
  4. 합성 & 비트스트림: 전체 설계를 합성하고 비트스트림을 생성합니다
  5. FPGA Manager 로딩: Linux FPGA Manager를 통해 비트스트림을 FPGA에 로드합니다
  6. Device Tree 바인딩: 커스텀 IP의 주소, 인터럽트, 호환성 문자열을 Device Tree에 기술합니다
  7. 커널 드라이버 프로빙: Device Tree 매칭으로 드라이버가 자동 프로빙되어 레지스터에 ioremap()으로 접근합니다
  8. 유저 스페이스 노출: /dev/ 캐릭터 디바이스, sysfs 속성, 또는 UIO로 유저 스페이스에 노출합니다

레지스터 맵 설계 규칙

FPGA IP의 AXI-Lite 레지스터 맵은 다음 규칙을 따르는 것이 좋습니다:

FPGA에서 인터럽트 전달

SoC FPGA(Zynq, Agilex)에서 PL(Programmable Logic)의 인터럽트를 PS(Processing System)로 전달하는 방법입니다:

FPGA → Linux 드라이버 통합 흐름 (End-to-End) RTL 설계 Verilog / SV AXI-Lite 슬레이브 합성 & P&R Vivado / Quartus 비트스트림 생성 FPGA Manager fpga_mgr_load() 비트스트림 로딩 Device Tree compatible, reg interrupts, clocks 드라이버 프로빙 platform_driver ioremap + IRQ 유저 스페이스 /dev/ 또는 sysfs UIO / mmap 레지스터 맵 설계 0x00: VERSION (RO) 0x04: SCRATCH (RW) — 버스 검증 0x08: CTRL (RW) 0x0C: STATUS (RO) Device Tree 노드 compatible = "vendor,my-accel-1.0"; reg = <0x43C00000 0x10000>; interrupts = <&gic 0 89 4>; 커널 드라이버 ver = ioread32(base + 0x00); iowrite32(0xDEADBEEF, base + 0x04); devm_request_irq(dev, irq, handler); SoC FPGA: PS-PL AXI 직접 연결 | PCIe FPGA: BAR 매핑 + MSI-X 인터럽트 인터럽트 경로: PL IRQ → GIC SPI → 커널 IRQ handler → 완료 통보 / wake-up

AXI-Lite 레지스터 인터페이스 패턴

AXI-Lite 레지스터 인터페이스는 FPGA IP와 CPU(또는 커널 드라이버) 사이의 가장 기본적인 통신 메커니즘입니다. 레지스터 타입별 동작 규약을 정확히 이해하는 것이 하드웨어-소프트웨어 인터페이스 설계의 핵심입니다.

레지스터 타입

버전 레지스터와 스크래치 레지스터 관례

버전 레지스터는 드라이버가 하드웨어 호환성을 확인하는 첫 번째 단계입니다. 일반적으로 [31:24] 메이저, [23:16] 마이너, [15:0] 패치 번호로 구성합니다. 스크래치 레지스터는 프로빙 시 0xDEADBEEF를 써서 읽어 AXI 버스가 정상 동작하는지 확인하는 전통적인 패턴입니다.

매칭 Linux 드라이버 코드

/* FPGA 커스텀 IP 드라이버 — 레지스터 접근 패턴 */
#define REG_VERSION       0x00
#define REG_SCRATCH       0x04
#define REG_CTRL          0x08
#define REG_STATUS        0x0C
#define REG_IRQ_STATUS    0x10
#define REG_IRQ_ENABLE    0x14

#define EXPECTED_VERSION  0x01000000  /* v1.0.0 */
#define SCRATCH_PATTERN   0xDEADBEEF

static int my_fpga_probe(struct platform_device *pdev)
{
    struct resource *res;
    void __iomem *base;
    u32 ver, scratch;
    int irq, ret;

    /* MMIO 리소스 매핑 */
    base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base))
        return PTR_ERR(base);

    /* 버전 확인 */
    ver = ioread32(base + REG_VERSION);
    if ((ver & 0xFF000000) != EXPECTED_VERSION) {
        dev_err(&pdev->dev, "version mismatch: 0x%08x\n", ver);
        return -ENODEV;
    }

    /* 스크래치 레지스터로 버스 검증 */
    iowrite32(SCRATCH_PATTERN, base + REG_SCRATCH);
    scratch = ioread32(base + REG_SCRATCH);
    if (scratch != SCRATCH_PATTERN) {
        dev_err(&pdev->dev, "bus verify failed: 0x%08x\n", scratch);
        return -EIO;
    }

    /* 인터럽트 등록 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;

    ret = devm_request_irq(&pdev->dev, irq, my_fpga_irq_handler,
                           0, "my-fpga", base);
    if (ret)
        return ret;

    /* 인터럽트 인에이블 */
    iowrite32(0x1, base + REG_IRQ_ENABLE);

    dev_info(&pdev->dev, "FPGA IP v%d.%d.%d probed\n",
             (ver >> 24) & 0xFF, (ver >> 16) & 0xFF, ver & 0xFFFF);
    return 0;
}

/* W1C 인터럽트 핸들러 */
static irqreturn_t my_fpga_irq_handler(int irq, void *data)
{
    void __iomem *base = data;
    u32 status = ioread32(base + REG_IRQ_STATUS);

    if (!status)
        return IRQ_NONE;

    /* W1C: 1을 써서 처리된 인터럽트 비트 클리어 */
    iowrite32(status, base + REG_IRQ_STATUS);

    /* 인터럽트별 처리 */
    if (status & BIT(0))
        /* DMA 완료 처리 */;
    if (status & BIT(1))
        /* 오류 처리 */;

    return IRQ_HANDLED;
}

static const struct of_device_id my_fpga_of_match[] = {
    { .compatible = "vendor,my-accel-1.0" },
    { }
};
MODULE_DEVICE_TABLE(of, my_fpga_of_match);

static struct platform_driver my_fpga_driver = {
    .probe  = my_fpga_probe,
    .driver = {
        .name = "my-fpga",
        .of_match_table = my_fpga_of_match,
    },
};
module_platform_driver(my_fpga_driver);

FPGA DMA 엔진과 커널 연동

고성능 FPGA 가속기는 CPU 개입 없이 대용량 데이터를 전송하기 위해 DMA 엔진을 사용합니다. FPGA DMA 엔진을 Linux 커널의 DMA engine 프레임워크와 통합하면, 다른 커널 서브시스템(네트워크, 스토리지, 비디오 등)이 표준 API로 DMA를 요청할 수 있습니다.

링 버퍼 디스크립터 DMA

FPGA DMA 엔진의 가장 일반적인 아키텍처는 링 버퍼(Ring Buffer) 디스크립터 방식입니다. 호스트 메모리에 디스크립터 배열을 할당하고, 마지막 디스크립터의 다음 포인터가 첫 번째 디스크립터를 가리켜 순환 구조를 형성합니다. 소프트웨어는 테일 포인터를 업데이트하여 새 디스크립터를 추가하고, 하드웨어는 헤드 포인터를 전진시키며 디스크립터를 소비합니다.

인터럽트 코얼레싱(Interrupt Coalescing)

매 디스크립터 완료마다 인터럽트를 발생시키면 CPU 오버헤드가 높아집니다. 인터럽트 코얼레싱은 다음 두 가지 조건을 조합하여 인터럽트 빈도를 줄입니다:

이 두 조건의 조합으로 높은 처리량(대량 데이터)과 낮은 지연(소량 데이터) 사이의 균형을 달성합니다. Linux의 ethtool -C로 네트워크 디바이스의 코얼레싱 파라미터를 조정하는 것과 동일한 원리입니다.

Linux dma_engine API 통합

FPGA DMA 엔진 드라이버는 struct dma_device를 등록하고 다음 주요 콜백을 구현합니다:

DMA engine 프레임워크의 상세 구조와 API는 DMA (Direct Memory Access) 페이지를 참고하세요.

완전한 FPGA 드라이버 예제

이 섹션에서는 가상 FPGA 가속기(my_accel)를 위한 완전한 Linux 드라이버를 단계별로 구성합니다. Platform 드라이버 등록부터 레지스터 접근, 인터럽트 처리, DMA 전송, 유저스페이스 인터페이스까지 하나의 일관된 예제로 전체 흐름을 보여줍니다.

FPGA 드라이버 아키텍처 (my_accel) probe() ioremap_resource() request_irq() dma_alloc_coherent() misc_register() 커널 드라이버 초기화 흐름 유저스페이스 사용 흐름 open(/dev/my_accel) ioctl(START_DMA) read(wait_irq) mmap(DMA buf) /dev/my_accel 디바이스 노드

Platform 드라이버 골격

platform_driver 구조체를 정의하고, probe()에서 리소스를 할당하며, remove()에서 정리하는 기본 골격입니다.

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/dma-mapping.h>
#include <linux/miscdevice.h>

struct my_accel {
    void __iomem       *regs;
    int                irq;
    dma_addr_t         dma_phys;
    void               *dma_virt;
    size_t             dma_size;
    struct completion  dma_done;
    struct miscdevice  mdev;
    struct device      *dev;
};

static const struct of_device_id my_accel_of_match[] = {
    { .compatible = "my,fpga-accel" },
    { /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, my_accel_of_match);

static int my_accel_probe(struct platform_device *pdev);
static void my_accel_remove(struct platform_device *pdev);

static struct platform_driver my_accel_driver = {
    .probe  = my_accel_probe,
    .remove = my_accel_remove,
    .driver = {
        .name = "my-fpga-accel",
        .of_match_table = my_accel_of_match,
    },
};
module_platform_driver(my_accel_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My FPGA Accelerator Driver");
MODULE_AUTHOR("Example");

레지스터 접근

devm_ioremap_resource()로 FPGA 메모리 영역을 매핑하고, ioread32()/iowrite32()로 레지스터에 접근합니다.

/* 레지스터 맵 정의 */
#define ACCEL_VERSION    0x00  /* 버전 (읽기 전용) */
#define ACCEL_CTRL       0x04  /* 제어 레지스터 */
#define ACCEL_STATUS     0x08  /* 상태 레지스터 */
#define ACCEL_DMA_ADDR   0x0C  /* DMA 주소 */
#define ACCEL_DMA_LEN    0x10  /* DMA 길이 */
#define ACCEL_DMA_CMD    0x14  /* DMA 명령 */
#define ACCEL_IRQ_STATUS 0x18  /* 인터럽트 상태 (W1C) */
#define ACCEL_IRQ_MASK   0x1C  /* 인터럽트 마스크 */

#define ACCEL_CTRL_START 0x01
#define ACCEL_CTRL_RESET 0x80
#define ACCEL_IRQ_DMA_DONE 0x01
#define ACCEL_IRQ_ERROR    0x02
#define ACCEL_EXPECTED_VER 0x20240101

/* probe()에서 레지스터 매핑 */
static int my_accel_probe(struct platform_device *pdev)
{
    struct my_accel *accel;
    struct resource *res;
    u32 version;

    accel = devm_kzalloc(&pdev->dev, sizeof(*accel), GFP_KERNEL);
    if (!accel)
        return -ENOMEM;

    accel->dev = &pdev->dev;
    platform_set_drvdata(pdev, accel);

    /* MMIO 레지스터 매핑 */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    accel->regs = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(accel->regs))
        return PTR_ERR(accel->regs);

    /* 버전 확인 */
    version = ioread32(accel->regs + ACCEL_VERSION);
    if (version != ACCEL_EXPECTED_VER) {
        dev_err(&pdev->dev, "unexpected version 0x%08x\n", version);
        return -ENODEV;
    }
    dev_info(&pdev->dev, "FPGA accel v%08x detected\n", version);

    /* 이후 단계: IRQ, DMA, misc 등록 ... */
코드 설명
  • devm_ioremap_resource(): Platform 리소스의 물리 주소를 커널 가상 주소에 매핑합니다. devm_ 접두사 덕분에 remove()에서 별도로 해제하지 않아도 됩니다
  • 버전 레지스터 확인: probe() 초기에 FPGA 하드웨어 버전을 읽어, 드라이버가 기대하는 하드웨어인지 검증합니다. 불일치 시 -ENODEV를 반환하여 바인딩을 거부합니다
  • W1C(Write-1-to-Clear): ACCEL_IRQ_STATUS는 W1C 방식으로, 해당 비트에 1을 쓰면 인터럽트가 클리어됩니다

인터럽트 처리

FPGA에서 발생하는 인터럽트를 처리하기 위해 devm_request_threaded_irq()를 사용합니다. 상반부(Hard IRQ)에서 빠르게 상태를 읽고, 하반부(Threaded IRQ)에서 DMA 완료 처리를 수행합니다.

/* 상반부: 하드 IRQ 핸들러 (인터럽트 컨텍스트) */
static irqreturn_t my_accel_irq(int irq, void *data)
{
    struct my_accel *accel = data;
    u32 status = ioread32(accel->regs + ACCEL_IRQ_STATUS);

    if (!(status & (ACCEL_IRQ_DMA_DONE | ACCEL_IRQ_ERROR)))
        return IRQ_NONE;

    /* W1C: 인터럽트 클리어 */
    iowrite32(status, accel->regs + ACCEL_IRQ_STATUS);

    return IRQ_WAKE_THREAD;
}

/* 하반부: 스레드 IRQ 핸들러 (프로세스 컨텍스트) */
static irqreturn_t my_accel_irq_thread(int irq, void *data)
{
    struct my_accel *accel = data;

    /* DMA 완료 → completion 시그널 */
    complete(&accel->dma_done);
    return IRQ_HANDLED;
}

/* probe()에서 IRQ 등록 */
    accel->irq = platform_get_irq(pdev, 0);
    if (accel->irq < 0)
        return accel->irq;

    init_completion(&accel->dma_done);

    ret = devm_request_threaded_irq(&pdev->dev, accel->irq,
        my_accel_irq, my_accel_irq_thread,
        IRQF_SHARED, "my-accel", accel);
    if (ret) {
        dev_err(&pdev->dev, "failed to request IRQ %d\n", accel->irq);
        return ret;
    }
코드 설명
  • 상반부 (my_accel_irq): 인터럽트 컨텍스트에서 실행됩니다. 상태 레지스터를 읽어 우리 디바이스의 인터럽트인지 확인하고, W1C로 클리어한 후 IRQ_WAKE_THREAD를 반환하여 하반부를 깨웁니다
  • 하반부 (my_accel_irq_thread): 프로세스 컨텍스트에서 실행되므로 슬립이 가능합니다. DMA 완료를 기다리는 스레드에 complete()로 시그널을 보냅니다
  • IRQF_SHARED: 여러 디바이스가 같은 인터럽트 라인을 공유할 수 있습니다. 상반부에서 IRQ_NONE을 반환하면 다른 핸들러로 전달됩니다

DMA 전송

FPGA 가속기의 DMA 전송을 위해 dma_alloc_coherent()로 일관성 있는(coherent) DMA 버퍼를 할당하고, FPGA 레지스터에 DMA 파라미터를 기록하여 전송을 시작합니다.

#define DMA_BUF_SIZE (1024 * 1024)  /* 1MB */

/* probe()에서 DMA 버퍼 할당 */
    dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));

    accel->dma_size = DMA_BUF_SIZE;
    accel->dma_virt = dma_alloc_coherent(&pdev->dev,
        accel->dma_size, &accel->dma_phys, GFP_KERNEL);
    if (!accel->dma_virt)
        return -ENOMEM;

    dev_info(&pdev->dev, "DMA buffer: virt=%p phys=%pad size=%zu\n",
        accel->dma_virt, &accel->dma_phys, accel->dma_size);

/* DMA 전송 시작 함수 */
static int my_accel_start_dma(struct my_accel *accel, size_t len)
{
    unsigned long timeout;

    if (len > accel->dma_size)
        return -EINVAL;

    /* completion 초기화 */
    reinit_completion(&accel->dma_done);

    /* DMA 파라미터를 FPGA 레지스터에 기록 */
    iowrite32((u32)accel->dma_phys, accel->regs + ACCEL_DMA_ADDR);
    iowrite32((u32)len, accel->regs + ACCEL_DMA_LEN);

    /* 인터럽트 활성화 + DMA 시작 */
    iowrite32(ACCEL_IRQ_DMA_DONE, accel->regs + ACCEL_IRQ_MASK);
    iowrite32(ACCEL_CTRL_START, accel->regs + ACCEL_DMA_CMD);

    /* DMA 완료 대기 (타임아웃 5초) */
    timeout = wait_for_completion_timeout(&accel->dma_done,
        msecs_to_jiffies(5000));
    if (!timeout) {
        dev_err(accel->dev, "DMA timeout\n");
        return -ETIMEDOUT;
    }

    return 0;
}
코드 설명
  • dma_alloc_coherent(): CPU와 디바이스가 모두 접근 가능한 DMA 버퍼를 할당합니다. 반환된 dma_phys는 FPGA에 전달할 버스 주소이고, dma_virt는 CPU가 사용할 가상 주소입니다
  • wait_for_completion_timeout(): FPGA의 DMA 완료 인터럽트가 발생할 때까지 블로킹 대기합니다. 타임아웃이 만료되면 0을 반환하여 에러를 감지합니다
  • 레지스터 기록 순서: DMA 주소와 길이를 먼저 설정한 후, 마지막으로 명령 레지스터에 시작 비트를 기록합니다. 이 순서를 지키지 않으면 불완전한 파라미터로 DMA가 시작될 수 있습니다

유저스페이스 인터페이스

misc_register()를 사용하여 /dev/my_accel 디바이스 노드를 생성하고, ioctl()mmap()을 통해 유저스페이스에 인터페이스를 제공합니다.

#define ACCEL_IOC_MAGIC  'A'
#define ACCEL_IOC_START  _IOW(ACCEL_IOC_MAGIC, 1, size_t)
#define ACCEL_IOC_STATUS _IOR(ACCEL_IOC_MAGIC, 2, u32)

static long my_accel_ioctl(struct file *filp, unsigned int cmd,
                          unsigned long arg)
{
    struct my_accel *accel = container_of(filp->private_data,
        struct my_accel, mdev);

    switch (cmd) {
    case ACCEL_IOC_START: {
        size_t len;
        if (copy_from_user(&len, (void __user *)arg, sizeof(len)))
            return -EFAULT;
        return my_accel_start_dma(accel, len);
    }
    case ACCEL_IOC_STATUS: {
        u32 status = ioread32(accel->regs + ACCEL_STATUS);
        if (copy_to_user((void __user *)arg, &status, sizeof(status)))
            return -EFAULT;
        return 0;
    }
    default:
        return -ENOTTY;
    }
}

static int my_accel_mmap(struct file *filp,
                        struct vm_area_struct *vma)
{
    struct my_accel *accel = container_of(filp->private_data,
        struct my_accel, mdev);

    return dma_mmap_coherent(accel->dev, vma,
        accel->dma_virt, accel->dma_phys, accel->dma_size);
}

static const struct file_operations my_accel_fops = {
    .owner          = THIS_MODULE,
    .unlocked_ioctl = my_accel_ioctl,
    .mmap           = my_accel_mmap,
};

/* probe()에서 misc 디바이스 등록 */
    accel->mdev.minor = MISC_DYNAMIC_MINOR;
    accel->mdev.name  = "my_accel";
    accel->mdev.fops  = &my_accel_fops;
    ret = misc_register(&accel->mdev);
    if (ret) {
        dev_err(&pdev->dev, "failed to register misc device\n");
        return ret;
    }

/* remove() */
static void my_accel_remove(struct platform_device *pdev)
{
    struct my_accel *accel = platform_get_drvdata(pdev);
    misc_deregister(&accel->mdev);
    dma_free_coherent(accel->dev, accel->dma_size,
        accel->dma_virt, accel->dma_phys);
}

유저스페이스에서 이 드라이버를 사용하는 예제입니다:

#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

#define ACCEL_IOC_MAGIC  'A'
#define ACCEL_IOC_START  _IOW(ACCEL_IOC_MAGIC, 1, size_t)

int main(void)
{
    int fd = open("/dev/my_accel", O_RDWR);
    size_t len = 4096;

    /* DMA 버퍼를 유저스페이스에 매핑 */
    void *buf = mmap(NULL, 1048576, PROT_READ | PROT_WRITE,
                      MAP_SHARED, fd, 0);

    /* 입력 데이터 준비 */
    memset(buf, 0xAB, len);

    /* DMA 전송 시작 (블로킹: 완료까지 대기) */
    ioctl(fd, ACCEL_IOC_START, &len);

    /* 결과 확인 */
    printf("Result: 0x%08x\n", *((uint32_t *)buf));

    munmap(buf, 1048576);
    close(fd);
    return 0;
}

커널 설정 옵션

리눅스 커널에서 FPGA 지원을 활성화하기 위한 주요 Kconfig 옵션입니다.

설정 옵션 설명
CONFIG_FPGA FPGA 프레임워크 코어 — FPGA Manager, Bridge, Region의 기반
CONFIG_FPGA_MGR_SOCFPGA Intel Cyclone V SoC FPGA Manager 드라이버
CONFIG_FPGA_MGR_SOCFPGA_A10 Intel Arria 10 SoC FPGA Manager 드라이버
CONFIG_FPGA_MGR_ZYNQ_FPGA Xilinx Zynq-7000 FPGA Manager 드라이버
CONFIG_FPGA_MGR_ZYNQMP_FPGA Xilinx ZynqMP FPGA Manager 드라이버
CONFIG_FPGA_MGR_XILINX_SPI Xilinx SPI Slave 모드 FPGA Manager 드라이버
CONFIG_FPGA_MGR_ICE40_SPI Lattice iCE40 SPI FPGA Manager 드라이버
CONFIG_FPGA_MGR_MACHXO2_SPI Lattice MachXO2 SPI FPGA Manager 드라이버
CONFIG_FPGA_BRIDGE FPGA Bridge 프레임워크 — CPU↔FPGA 인터페이스 관리
CONFIG_FPGA_REGION FPGA Region 프레임워크 — Manager + Bridge + 비트스트림 통합 관리
CONFIG_OF_FPGA_REGION Device Tree 기반 FPGA Region — DT Overlay로 동적 재구성
CONFIG_FPGA_DFL DFL (Device Feature List) 버스 드라이버
CONFIG_FPGA_DFL_FME DFL FME (FPGA Management Engine) 드라이버
CONFIG_FPGA_DFL_AFU DFL AFU (Accelerator Functional Unit) 드라이버
CONFIG_FPGA_DFL_PCI DFL PCIe 디바이스 드라이버
CONFIG_UIO Userspace I/O — 유저스페이스에서 FPGA 레지스터 접근
CONFIG_VFIO Virtual Function I/O — FPGA 디바이스 패스스루 (VM 활용)

일반적인 FPGA 개발 환경을 위한 커널 설정 예시입니다:

# FPGA 프레임워크 핵심 모듈
CONFIG_FPGA=m
CONFIG_FPGA_MGR_ZYNQMP_FPGA=m
CONFIG_FPGA_BRIDGE=m
CONFIG_FPGA_REGION=m
CONFIG_OF_FPGA_REGION=m

# DFL 프레임워크 (Intel FPGA PCIe 카드용)
CONFIG_FPGA_DFL=m
CONFIG_FPGA_DFL_FME=m
CONFIG_FPGA_DFL_AFU=m
CONFIG_FPGA_DFL_PCI=m

# 유저스페이스 접근
CONFIG_UIO=m
CONFIG_VFIO=m
CONFIG_VFIO_PCI=m

# DT Overlay 지원 (SoC FPGA 동적 재구성)
CONFIG_OF_OVERLAY=y
CONFIG_OF_CONFIGFS=y

보드별 설정 예시

대표적인 FPGA SoC 보드의 defconfig 스니펫과, 모듈(=m) 대 빌트인(=y) 선택 기준을 설명합니다.

Xilinx Zynq UltraScale+ MPSoC

# Zynq UltraScale+ FPGA 지원 (필수: 빌트인)
CONFIG_FPGA=y
CONFIG_FPGA_MGR_ZYNQMP_FPGA=y
CONFIG_FPGA_BRIDGE=y
CONFIG_FPGA_REGION=y
CONFIG_OF_FPGA_REGION=y

# PS↔PL 브리지 (부팅 시 필요 → 빌트인)
CONFIG_XILINX_PR_DECOUPLER=y

# DT Overlay (런타임 재구성 필수)
CONFIG_OF_OVERLAY=y
CONFIG_OF_CONFIGFS=y

# AXI DMA (모듈: 필요할 때 로드)
CONFIG_XILINX_DMA=m

Intel Agilex SoC FPGA

# Intel Agilex SoC FPGA Manager
CONFIG_FPGA=y
CONFIG_FPGA_MGR_STRATIX10_SOC=y
CONFIG_FPGA_BRIDGE=y
CONFIG_FPGA_REGION=y
CONFIG_OF_FPGA_REGION=y

# Intel Freeze Bridge
CONFIG_FPGA_FREEZE_BRIDGE=y

# DFL (PCIe FPGA 카드 연결 시)
CONFIG_FPGA_DFL=m
CONFIG_FPGA_DFL_FME=m
CONFIG_FPGA_DFL_AFU=m
CONFIG_FPGA_DFL_PCI=m

모듈(=m) vs 빌트인(=y) 판단 기준

기준 =y (빌트인) =m (모듈)
부팅 시 필요 루트 파일시스템이 FPGA에 있거나, 부팅 초기에 FPGA 구성이 필요한 경우 해당 없음
런타임 로드 해당 없음 핫플러그 FPGA 카드, 런타임 PR 등 필요할 때만 로드하는 경우
커널 크기 커널 이미지 증가 커널 이미지 최소화, 별도 .ko 파일
디버깅 항상 사용 가능 modprobe/rmmod로 재로드 가능하여 개발 편의

실전 예제

앞서 다룬 커널 FPGA 프레임워크를 실전에서 활용하는 완전한 예제를 제시합니다.

최소 FPGA Manager 드라이버

FPGA Manager 프레임워크에 등록하는 최소한의 커널 모듈 예제입니다. 실제 하드웨어 접근 대신 로그를 출력하여, 프레임워크의 동작을 이해하는 데 초점을 맞춥니다.

/*
 * minimal_fpga_mgr.c — 최소 FPGA Manager 드라이버 예제
 *
 * 이 모듈은 FPGA Manager 프레임워크의 구조를 보여주기 위한
 * 스텁(stub) 구현입니다. 실제 하드웨어 접근은 포함하지 않습니다.
 */
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fpga/fpga-mgr.h>

struct minimal_fpga_priv {
    void __iomem *base;
    u32 bytes_written;
};

static enum fpga_mgr_states minimal_fpga_state(struct fpga_manager *mgr)
{
    /* 실제 드라이버: 하드웨어 상태 레지스터를 읽어 반환 */
    return FPGA_MGR_STATE_UNKNOWN;
}

static int minimal_fpga_write_init(struct fpga_manager *mgr,
                                    struct fpga_image_info *info,
                                    const char *buf, size_t count)
{
    struct minimal_fpga_priv *priv = mgr->priv;

    dev_info(&mgr->dev, "프로그래밍 시작: flags=0x%lx\n", info->flags);
    priv->bytes_written = 0;

    /* 실제 드라이버: FPGA를 프로그래밍 모드로 전환 */
    return 0;
}

static int minimal_fpga_write(struct fpga_manager *mgr,
                               const char *buf, size_t count)
{
    struct minimal_fpga_priv *priv = mgr->priv;

    priv->bytes_written += count;
    dev_info(&mgr->dev, "비트스트림 쓰기: %zu 바이트 (누적: %u)\n",
             count, priv->bytes_written);

    /* 실제 드라이버: 비트스트림 데이터를 FPGA 구성 인터페이스에 전송 */
    return 0;
}

static int minimal_fpga_write_complete(struct fpga_manager *mgr,
                                        struct fpga_image_info *info)
{
    struct minimal_fpga_priv *priv = mgr->priv;

    dev_info(&mgr->dev, "프로그래밍 완료: 총 %u 바이트\n",
             priv->bytes_written);

    /* 실제 드라이버: DONE 핀 확인, 구성 완료 대기 */
    return 0;
}

static const struct fpga_manager_ops minimal_fpga_ops = {
    .state          = minimal_fpga_state,
    .write_init     = minimal_fpga_write_init,
    .write          = minimal_fpga_write,
    .write_complete = minimal_fpga_write_complete,
};

static int minimal_fpga_probe(struct platform_device *pdev)
{
    struct minimal_fpga_priv *priv;
    struct fpga_manager *mgr;

    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    mgr = devm_fpga_mgr_register(&pdev->dev, "Minimal FPGA Manager",
                                 &minimal_fpga_ops, priv);
    return PTR_ERR_OR_ZERO(mgr);
}

static const struct of_device_id minimal_fpga_of_match[] = {
    { .compatible = "vendor,minimal-fpga" },
    {},
};
MODULE_DEVICE_TABLE(of, minimal_fpga_of_match);

static struct platform_driver minimal_fpga_driver = {
    .probe  = minimal_fpga_probe,
    .driver = {
        .name           = "minimal-fpga-mgr",
        .of_match_table = minimal_fpga_of_match,
    },
};
module_platform_driver(minimal_fpga_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("Minimal FPGA Manager driver example");

PCIe FPGA 디바이스 드라이버

PCIe로 연결된 FPGA 가속기 카드용 디바이스 드라이버 예제입니다. DMA 전송과 유저스페이스 ioctl 인터페이스를 포함합니다.

/*
 * fpga_pcie_accel.c — PCIe FPGA 가속기 디바이스 드라이버
 *
 * BAR0: 제어/상태 레지스터 (MMIO)
 * BAR2: DMA 버퍼 영역
 * ioctl: 유저스페이스에서 DMA 전송 요청
 */
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/miscdevice.h>
#include <linux/dma-mapping.h>
#include <linux/uaccess.h>

#define FPGA_VENDOR_ID   0x1234
#define FPGA_DEVICE_ID   0x5678
#define DMA_BUF_SIZE     (4 * 1024 * 1024)  /* 4 MiB */

/* FPGA 레지스터 오프셋 */
#define REG_VERSION      0x0000
#define REG_CONTROL      0x0004
#define REG_STATUS       0x0008
#define REG_DMA_SRC      0x0010
#define REG_DMA_DST      0x0018
#define REG_DMA_LEN      0x0020
#define REG_DMA_CTRL     0x0024

/* ioctl 명령 */
#define FPGA_IOC_MAGIC   'F'
#define FPGA_IOC_DMA_TO_DEV   _IOW(FPGA_IOC_MAGIC, 1, struct fpga_dma_req)
#define FPGA_IOC_DMA_FROM_DEV _IOR(FPGA_IOC_MAGIC, 2, struct fpga_dma_req)
#define FPGA_IOC_GET_VERSION  _IOR(FPGA_IOC_MAGIC, 3, u32)

struct fpga_dma_req {
    u64 offset;     /* FPGA 측 메모리 오프셋 */
    u64 length;     /* 전송 크기 (바이트) */
};

struct fpga_pcie_dev {
    struct pci_dev *pdev;
    void __iomem *bar0;
    struct miscdevice misc;
    void *dma_buf;
    dma_addr_t dma_handle;
};

static long fpga_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    struct fpga_pcie_dev *fdev = container_of(file->private_data,
                                              struct fpga_pcie_dev, misc);
    struct fpga_dma_req req;
    u32 version;

    switch (cmd) {
    case FPGA_IOC_GET_VERSION:
        version = readl(fdev->bar0 + REG_VERSION);
        if (copy_to_user((void __user *)arg, &version, sizeof(version)))
            return -EFAULT;
        return 0;

    case FPGA_IOC_DMA_TO_DEV:
        if (copy_from_user(&req, (void __user *)arg, sizeof(req)))
            return -EFAULT;
        if (req.length > DMA_BUF_SIZE)
            return -EINVAL;

        /* DMA 전송 설정: 호스트 → FPGA */
        writeq(fdev->dma_handle, fdev->bar0 + REG_DMA_SRC);
        writeq(req.offset, fdev->bar0 + REG_DMA_DST);
        writel(req.length, fdev->bar0 + REG_DMA_LEN);
        writel(0x01, fdev->bar0 + REG_DMA_CTRL);  /* DMA 시작 */
        return 0;

    default:
        return -ENOTTY;
    }
}

static const struct file_operations fpga_fops = {
    .owner          = THIS_MODULE,
    .unlocked_ioctl = fpga_ioctl,
};

static int fpga_pcie_probe(struct pci_dev *pdev,
                           const struct pci_device_id *id)
{
    struct fpga_pcie_dev *fdev;
    int ret;

    fdev = devm_kzalloc(&pdev->dev, sizeof(*fdev), GFP_KERNEL);
    if (!fdev)
        return -ENOMEM;

    fdev->pdev = pdev;
    pci_set_drvdata(pdev, fdev);

    ret = pcim_enable_device(pdev);
    if (ret)
        return ret;

    ret = pcim_iomap_regions(pdev, BIT(0), "fpga-accel");
    if (ret)
        return ret;

    fdev->bar0 = pcim_iomap_table(pdev)[0];
    pci_set_master(pdev);

    /* DMA 마스크 설정 및 버퍼 할당 */
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
    if (ret)
        return ret;

    fdev->dma_buf = dma_alloc_coherent(&pdev->dev, DMA_BUF_SIZE,
                                        &fdev->dma_handle, GFP_KERNEL);
    if (!fdev->dma_buf)
        return -ENOMEM;

    /* misc 디바이스 등록 (/dev/fpga-accel0) */
    fdev->misc.minor = MISC_DYNAMIC_MINOR;
    fdev->misc.name  = "fpga-accel0";
    fdev->misc.fops  = &fpga_fops;
    ret = misc_register(&fdev->misc);
    if (ret)
        goto err_dma;

    dev_info(&pdev->dev, "FPGA 가속기 등록 완료, 버전: 0x%08x\n",
             readl(fdev->bar0 + REG_VERSION));
    return 0;

err_dma:
    dma_free_coherent(&pdev->dev, DMA_BUF_SIZE,
                      fdev->dma_buf, fdev->dma_handle);
    return ret;
}

static void fpga_pcie_remove(struct pci_dev *pdev)
{
    struct fpga_pcie_dev *fdev = pci_get_drvdata(pdev);

    misc_deregister(&fdev->misc);
    dma_free_coherent(&pdev->dev, DMA_BUF_SIZE,
                      fdev->dma_buf, fdev->dma_handle);
}

static const struct pci_device_id fpga_pcie_ids[] = {
    { PCI_DEVICE(FPGA_VENDOR_ID, FPGA_DEVICE_ID) },
    {},
};
MODULE_DEVICE_TABLE(pci, fpga_pcie_ids);

static struct pci_driver fpga_pcie_driver = {
    .name     = "fpga-pcie-accel",
    .id_table = fpga_pcie_ids,
    .probe    = fpga_pcie_probe,
    .remove   = fpga_pcie_remove,
};
module_pci_driver(fpga_pcie_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Example");
MODULE_DESCRIPTION("PCIe FPGA accelerator driver");

DT Overlay 동적 재구성 예제

Device Tree Overlay를 사용하여 SoC FPGA를 동적으로 재구성하는 전체 워크플로입니다. FPGA 비트스트림 로드와 디바이스 드라이버 바인딩이 단일 작업으로 처리됩니다.

1단계: DT Overlay 소스 작성

/* spi_accel.dts — SPI 인터페이스 FPGA 가속기용 DT Overlay */
/dts-v1/;
/plugin/;

&fpga_region0 {
    /* FPGA 비트스트림 파일 (firmware 디렉토리에 위치) */
    firmware-name = "spi_accel.rbf";

    #address-cells = <1>;
    #size-cells = <1>;

    /* FPGA 내부에 구현된 SPI 가속기 디바이스 */
    spi_accel: spi@0x43c00000 {
        compatible = "vendor,fpga-spi-accel";
        reg = <0x43c00000 0x1000>;
        interrupts = <0 29 4>;
        clocks = <&clkc 15>;
        spi-max-frequency = <50000000>;
    };
};

2단계: 전체 워크플로 실행

# 1. DT Overlay 소스 컴파일
dtc -O dtb -o spi_accel.dtbo -@ spi_accel.dts

# 2. firmware 디렉토리에 비트스트림 배치
cp spi_accel.rbf /lib/firmware/

# 3. DT Overlay 적용 (자동으로 FPGA 프로그래밍 + 디바이스 생성)
mkdir /sys/kernel/config/device-tree/overlays/spi_accel
cat spi_accel.dtbo > /sys/kernel/config/device-tree/overlays/spi_accel/dtbo

# 4. 결과 확인
cat /sys/class/fpga_manager/fpga0/state
# 출력: operating

ls /sys/bus/spi/devices/
# 출력: spi0.0 (FPGA 내 SPI 가속기 디바이스가 인식됨)

dmesg | tail -20
# FPGA 프로그래밍 성공, 드라이버 바인딩 메시지 확인

# 5. 해제 (FPGA 로직 비활성화 + 디바이스 제거)
rmdir /sys/kernel/config/device-tree/overlays/spi_accel

Overlay 적용 시 내부적으로 다음 순서로 처리됩니다:

  1. FPGA Region이 연결된 Bridge를 비활성화합니다 (트래픽 차단)
  2. FPGA Manager가 firmware-name에 지정된 비트스트림을 로드합니다
  3. 프로그래밍 성공 후 Bridge를 다시 활성화합니다
  4. Overlay에 정의된 디바이스 노드가 Device Tree에 추가되고, 해당 드라이버가 probe됩니다

트러블슈팅 & 디버깅

FPGA 관련 리눅스 커널 문제를 진단하고 해결하기 위한 실용적인 가이드입니다.

sysfs를 통한 상태 확인

FPGA Manager 상태 확인

# FPGA Manager 목록 확인
ls /sys/class/fpga_manager/
# 출력: fpga0  fpga1

# FPGA 상태 확인
cat /sys/class/fpga_manager/fpga0/name
cat /sys/class/fpga_manager/fpga0/state
# 기대 출력: operating (정상적으로 구성 완료)

# 상태가 "unknown" 또는 "error"인 경우 체크리스트:
# 1. 비트스트림 파일이 /lib/firmware/에 존재하는지 확인
# 2. 비트스트림이 해당 FPGA 디바이스와 호환되는지 확인
# 3. 전원 공급이 안정적인지 확인
# 4. dmesg에서 오류 로그 확인

FPGA Bridge 상태 확인

# Bridge 목록 확인
ls /sys/class/fpga_bridge/
# 출력: br0  br1  freeze0

# Bridge 활성화 상태 확인
cat /sys/class/fpga_bridge/br0/state
# 기대 출력: enabled (PS↔PL 트래픽 허용)

# Bridge가 "disabled"인 경우:
# - FPGA가 아직 프로그래밍되지 않았거나
# - 재프로그래밍 중이거나
# - 프로그래밍에 실패한 상태입니다

DFL 디바이스 열거 확인

# DFL 디바이스 목록
ls /sys/bus/dfl/devices/
# 출력: dfl_dev.0  dfl_dev.1  dfl_dev.2

# FME 디바이스 확인
cat /sys/bus/dfl/devices/dfl_dev.0/type
# 출력: fme

# AFU 포트 확인
ls /dev/dfl-fme.*
ls /dev/dfl-port.*

dmesg 로그 분석

FPGA 관련 커널 메시지에서 자주 나타나는 에러와 그 의미입니다.

# FPGA 관련 커널 로그만 필터링
dmesg | grep -i fpga

# 일반적인 에러 메시지와 대응

# "FPGA programming failed"
# → 원인: 비트스트림 포맷 불일치, 손상된 파일, 또는 전원 문제
# → 대응: 비트스트림을 재생성하고, FPGA 모델이 일치하는지 확인합니다

# "FPGA Timeout waiting for DONE"
# → 원인: FPGA 구성 완료 신호(DONE)를 수신하지 못함
# → 대응: 구성 인터페이스(JTAG/SPI/SelectMAP) 연결 확인, FPGA 전원 확인

# "FPGA bridge enable failed"
# → 원인: AXI 경로 행업(hang), PL 클록 미생성
# → 대응: 비트스트림이 올바른 AXI 인터페이스를 포함하는지 확인합니다

# ftrace로 FPGA 관련 함수 추적
echo function > /sys/kernel/debug/tracing/current_tracer
echo 'fpga_*' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 1 > /sys/kernel/debug/tracing/tracing_on
# ... FPGA 작업 수행 ...
cat /sys/kernel/debug/tracing/trace
echo 0 > /sys/kernel/debug/tracing/tracing_on

devmem으로 FPGA 레지스터 접근

devmem2 도구를 사용하면 FPGA의 MMIO(Memory-Mapped I/O) 레지스터를 직접 읽고 쓸 수 있습니다. 드라이버 개발 초기 단계에서 하드웨어 동작을 확인하는 데 유용합니다.

주의: devmem을 통한 직접 레지스터 접근은 디버깅 용도로만 사용해야 합니다. 잘못된 주소에 대한 접근이나 잘못된 값의 쓰기는 시스템 행업(hang) 또는 데이터 손실을 유발할 수 있습니다. 프로덕션 환경에서는 반드시 적절한 디바이스 드라이버를 사용해야 합니다.

# devmem2 설치 (배포판에 따라)
# apt install devmem2  또는  yum install devmem2

# FPGA 버전 레지스터 읽기 (BAR0 + 0x00)
devmem2 0xA0000000 w
# 출력: Value at address 0xA0000000: 0x20240101

# FPGA 제어 레지스터 쓰기
devmem2 0xA0000004 w 0x00000001

# FPGA DMA 상태 확인
devmem2 0xA0001000 w
# 비트 0: DMA busy, 비트 1: DMA done, 비트 2: DMA error

# 연속된 레지스터 덤프 (셸 스크립트)
for offset in $(seq 0 4 64); do
    addr=$(printf "0x%X" $((0xA0000000 + offset)))
    echo -n "$addr: "
    devmem2 $addr w 2>/dev/null | grep "Value"
done

일반적 실패 모드와 해결

FPGA 개발에서 반복적으로 발생하는 5가지 실패 시나리오와 각각의 체계적인 해결 방법입니다.

시나리오 1: 비트스트림 로드 실패

fpga_mgr_load()가 에러를 반환하는 경우입니다. 해결 순서:

  1. 비트스트림 파일이 /lib/firmware/에 존재하는지 확인합니다: ls -la /lib/firmware/accel.rbf
  2. FPGA 디바이스와 비트스트림 대상이 일치하는지 확인합니다. Zynq용 비트스트림을 Cyclone V에 로드하면 실패합니다
  3. 비트스트림 파일이 손상되지 않았는지 체크섬을 확인합니다: md5sum accel.rbf
  4. Manager 상태를 확인합니다: cat /sys/class/fpga_manager/fpga0/state가 "operating" 또는 "unknown"이어야 합니다. "error"면 하드웨어 문제가 있습니다

시나리오 2: Bridge 비활성화 안됨

Freeze Controller가 AXI 트래픽을 정지시키지 못하는 경우입니다. FPGA 쪽에서 AXI 트랜잭션이 완료되지 않고 대기 중이면, Bridge 비활성화가 블로킹될 수 있습니다. dmesg에서 "bridge disable timeout" 메시지를 확인하고, FPGA 로직이 AXI 프로토콜을 올바르게 구현하는지(특히 WREADY/RREADY 핸드셰이크) 검증해야 합니다.

시나리오 3: DFL 열거 실패

DFL 드라이버가 Feature 체인을 순회하지 못하는 경우입니다. BAR 매핑이 올바른지 lspci -vv로 확인하고, DFH(Device Feature Header) 체인의 무결성을 devmem2로 직접 검증합니다. DFH의 Type 필드(비트 63:60)가 올바르고, Next 오프셋(비트 39:16)이 유효한 범위 내인지 확인합니다.

시나리오 4: PR 영역 프로그래밍 후 디바이스 미인식

비트스트림 로드는 성공했으나 새 디바이스가 나타나지 않는 경우입니다. 대부분 DT Overlay가 적용되지 않았거나, Overlay에 정의된 compatible 문자열에 대응하는 드라이버가 없기 때문입니다. cat /sys/kernel/config/device-tree/overlays/*/status로 Overlay 적용 상태를 확인하고, dmesg | grep probe로 드라이버 바인딩 로그를 검색합니다.

시나리오 5: DMA 타임아웃

FPGA의 DMA 엔진이 완료 인터럽트를 발생시키지 않는 경우입니다. IOMMU가 활성화된 환경에서 DMA 주소 변환이 올바르게 설정되었는지 확인합니다. dmesg | grep -i iommu에서 "DMAR: DRHD" 에러가 없는지 확인하고, DMA 주소가 IOMMU 페이지 테이블에 매핑된 범위 내인지 검증합니다. IOMMU가 없는 환경에서는 물리 주소가 FPGA에 전달되므로, dma_alloc_coherent()가 반환한 dma_addr_t가 FPGA의 주소 범위(보통 32비트) 내인지 확인합니다.

sysfs 진단 워크플로

FPGA 문제 발생 시 체계적으로 진단하기 위한 4단계 워크플로입니다.

  1. 단계 1: Manager 상태 확인
    cat /sys/class/fpga_manager/*/state

    "operating"이면 정상, "unknown"이면 미구성, "error"면 하드웨어 이상입니다.

  2. 단계 2: 커널 로그 분석
    dmesg | grep -iE "fpga|dfl|bridge"

    에러 메시지의 시간순으로 첫 번째 에러가 근본 원인인 경우가 많습니다.

  3. 단계 3: Bridge 활성화 상태
    cat /sys/class/fpga_bridge/*/state

    Manager가 "operating"인데 Bridge가 "disabled"이면, 프로그래밍 후 Bridge 재활성화에 실패한 것입니다.

  4. 단계 4: FPGA 레지스터 직접 확인
    devmem2 0xA0000000 w

    버전 레지스터를 읽어 FPGA가 올바르게 구성되었는지 확인합니다. 값이 0xFFFFFFFF이면 Bridge가 비활성이거나 FPGA가 미구성 상태입니다.

진단 bash 스크립트

위의 진단 단계를 자동화하는 스크립트입니다.

#!/bin/bash
# FPGA 진단 스크립트

echo "=== FPGA Manager 상태 ==="
for mgr in /sys/class/fpga_manager/*/; do
    name=$(cat ${mgr}name 2>/dev/null)
    state=$(cat ${mgr}state 2>/dev/null)
    echo "  ${mgr##*/}: name=${name}, state=${state}"
done

echo "=== FPGA Bridge 상태 ==="
for br in /sys/class/fpga_bridge/*/; do
    name=$(cat ${br}name 2>/dev/null)
    state=$(cat ${br}state 2>/dev/null)
    echo "  ${br##*/}: name=${name}, state=${state}"
done

echo "=== DFL 디바이스 ==="
ls /sys/bus/dfl/devices/ 2>/dev/null || echo "  DFL 디바이스 없음"

echo "=== FPGA 관련 커널 로그 (최근 20줄) ==="
dmesg | grep -iE "fpga|dfl|bridge" | tail -20

echo "=== DT Overlay 상태 ==="
if [ -d /sys/kernel/config/device-tree/overlays ]; then
    for ov in /sys/kernel/config/device-tree/overlays/*/; do
        status=$(cat ${ov}status 2>/dev/null)
        echo "  ${ov##*/}: ${status}"
    done
else
    echo "  configfs overlay 미지원"
fi

참고자료

커널 소스 핵심 파일

리눅스 커널 FPGA 프레임워크의 핵심 소스 파일과 그 역할입니다.

파일 경로 역할
drivers/fpga/fpga-mgr.c FPGA Manager 코어 — fpga_mgr_register(), fpga_mgr_load() 등 Manager 프레임워크 API
drivers/fpga/fpga-bridge.c FPGA Bridge 코어 — fpga_bridge_register(), fpga_bridges_enable()/disable() 등 Bridge 제어
drivers/fpga/fpga-region.c FPGA Region 코어 — fpga_region_register(), fpga_region_program_fpga() 오케스트레이션
drivers/fpga/of-fpga-region.c Device Tree Overlay Region — DT Overlay 이벤트 수신, 자동 비트스트림 로드
drivers/fpga/dfl.c DFL 프레임워크 코어 — DFL 버스 드라이버, Feature Header 순회, 디바이스 열거
drivers/fpga/dfl-fme-main.c DFL FME 드라이버 — FPGA Management Engine, PR 관리, 전력/온도 모니터링
drivers/fpga/dfl-afu-main.c DFL AFU 드라이버 — Accelerator Functional Unit, 유저스페이스 인터페이스
include/linux/fpga/fpga-mgr.h FPGA Manager API 헤더 — fpga_manager_ops, fpga_image_info 구조체 정의

패치를 제출하거나 코드를 리뷰할 때는 LKML(Linux Kernel Mailing List)의 linux-fpga@vger.kernel.org 메일링 리스트를 활용합니다. scripts/get_maintainer.pl로 해당 파일의 유지관리자와 리뷰어를 확인할 수 있습니다.

커널 문서