본문 바로가기
임베디드 개발/임베디드 리눅스

QEMU 기반 커스텀 PCIe 장치 에뮬레이션 - (4) DMA + MSI

by eteo 2026. 6. 14.
반응형

 

 

 

이전 글에서 작성한 커스텀 PCIe 장치의 기본 구조와 기능은 유지한채로, 데이터 전송 방식과 인터럽트 방식을 변경하였다.

  • 데이터 전송 : 장치의 RAM을 BAR1 영역에 할당하던 방식에서 DMA(Direct Memory Access) 전송 방식으로 변경
  • 인터럽트 : 레거시 INTx 방식에서 MSI(Message Signaled Interrupt) 방식으로 변경

 

 

 

 

 

PCI DMA의 이해

PCI DMA는 CPU의 개입 없이 하드웨어가 시스템 메모리에 직접 접근한다는 점에서 일반적인 생각하는 DMA와 동일하지만, DMA 엔진의 위치와 작동 방식에서 차이가 있다.

 

보통 임베디드 환경에서 말하는 DMA는 SoC/MCU 내부에 DMA 컨트롤러가 존재한다. 그래서 CPU가 초기 설정만 해두면 이후에는 DMA 컨트롤러가 메모리와 주변장치(ADC, UART, SPI 등) 간의 데이터 복사를 수행하며, 이러한 방식은 제3의 장치가 전송을 주도하기 때문에 Third-party DMA라고 부른다.

 

Peripheral ↔ DMA Controller ↔ System RAM

 

 

반면, PCI DMA는 DMA 컨트롤러가 PCI 장치 내부에 위치하며 장치가 Bus Master가 되어 호스트 시스템 메모리에 직접 접근하는 구조를 가진다. 이처럼 장치 스스로 전송을 주도하는 방식을 First-party DMA라고 부르며, 이를 가능하게 하는 핵심 개념은 Bus Mastering이다. (https://ko.wikipedia.org/wiki/버스_마스터링)

 

PCIe Device ↔ PCIe Bus ↔ Host Memory

 

 

 

 

BAR 접근 방식(MMIO)과 DMA 방식 비교

MMIO와 DMA 방식의 가장 큰 차이는 트랜잭션 시작을 누가 하느냐이다. 구체적으로 살펴보면 다음과 같다.

 

 

1. BAR를 통한 장치 메모리 접근 방식 (MMIO)

호스트의 PCI 드라이버가 장치의 BAR1 영역을 유저 공간에 매핑하면, 유저 애플리케이션이 해당 가상 주소에 읽기/쓰기를 수행할 때마다 다음 동작이 발생한다.

  1. CPU가 load/store 명령을 수행한다.
  2. 해당 주소가 PCIe 주소 공간으로 디코딩된다.
  3. 호스트 PCIe 컨트롤러가 이를 Memory Read/Write TLP 패킷으로 변환해 EP로 전달한다.

즉, 트랜잭션의 시작 주체는 항상 호스트이며, CPU의 메모리 접근 명령이 PCIe 트랜잭션으로 직접 변환된다.

 

 

2. DMA 방식 (Bus Mastering)

DMA Read의 흐름을 예 들면 다음과 같은 과정으로 진행된다.

  1. 호스트 PCI 드라이버가 호스트 RAM에 DMA 가능 메모리 영역을 확보한다. (ex. dma_alloc_coherent())
  2. PCI 드라이버가 장치 레지스터 설정을 통해 확보된 DMA 주소와 크기를 전달하며 DMA Read를 트리거한다.
  3. 장치(Endpoint, EP) 내부의 DMA 엔진이 설정된 주소와 데이터 길이를 기반으로 직접 Read 트랜잭션을 시작한다.
  4. EP의 Transaction Layer가 Memory Read Request TLP 패킷을 생성하여 호스트의 RC(Root Complex)로 전송한다.
  5. RC는 해당 요청을 수신하고, 메모리 컨트롤러를 통해 Host RAM에서 데이터를 읽는다.
  6. RC는 읽어온 데이터를 Completion with Data(CplD) TLP 패킷에 담아 EP로 회신한다.
  7. EP는 해당 패킷을 수신하여 내부 메모리에 데이터를 저장한다.

이 과정에서 장치가 Bus Master로 동작하며 자체 DMA 엔진을 통해 PCIe 트랜잭션을 직접 생성한다. (단, PCI Configuration Space의 Bus Master Enable 비트가 설정되어 있어야 한다.)

 

또한 호스트 측에서는 실제 데이터 이동이 PCIe 컨트롤러와 메모리 컨트롤러 하드웨어에 의해 처리되므로, CPU는 해당 작업에서 자유로워질 수 있다.

 

 

 

 

PCIe MSI 인터럽트

MSI(Message Signaled Interrupt)는 인터럽트를 Memory Write TLP 형태의 메시지로 전달하는 방식으로, INTx와 달리 별도의 물리적 인터럽트 신호선을 사용하지 않는다. INTx가 PCI 시절의 레거시 방식이라면, MSI(또는 MSI-X)는 확장성과 병렬성 측면에서 우수하며, 현대적인 PCIe 시스템에서는 사실상 기본 인터럽트 방식으로 사용한다.

 

 

 

 

레지스터 맵

 

Offset Register Access Description
0x00 REG_ID RO 장치 식별자 (0xFEE1600D)
0x04 REG_STATUS RO Bit0: Busy, Bit1: Error
0x08 REG_COMMAND WO Bit0: RESET (큐 및 상태 초기화)
0x40 REG_SQ_BASE_L RW SQ 배열 DMA 물리 주소 Low 32bit
0x44 REG_SQ_BASE_H RW SQ 배열 DMA 물리 주소 High 32bit
0x48 REG_SQ_SIZE RW SQ 슬롯 수 (N, 2의 거듭제곱으로 설정)
0x4C REG_SQ_TAIL RW 드라이버장치 도어벨 (SQ에 작업 추가 후 알림)
0x50 REG_SQ_HEAD RO 장치가 처리 중인 위치 (드라이버는 읽기만함)
0x54 REG_CQ_BASE_L RW CQ 배열 DMA 물리 주소 Low 32bit
0x58 REG_CQ_BASE_H RW CQ 배열 DMA 물리 주소 High 32bit
0x5C REG_CQ_SIZE RW CQ 슬롯 수 (N, SQ 슬롯 수와 동일하게 설정)
0x60 REG_CQ_HEAD RW 드라이버장치 도어벨 (CQ 처리 후 슬롯 반납)
0x64 REG_CQ_TAIL RO 장치가 완료 엔트리를 쓴 위치 (드라이버는 읽기만함)

 

 

 

 

 

 

기존 장치에서 설계가 달라진 부분만 좀 더 자세히 설명하도록 하겠다.

 

 

1. REG_IRQ_ACK 삭제

기존 INTx 방식에서 MSI 인터럽트 방식으로 전환하면서 ACK 레지스터를 제거하였다. INTx는 레벨 트리거 방식이므로 인터럽트 발생 시 라인이 지속적으로 활성 상태를 유지한다. 따라서 드라이버는 디바이스 레지스터를 통해 인터럽트 처리 완료 신호(ACK)를 보내야 하며, 그 전까지는 디바이스가 인터럽트 라인을 Deassert하지 못하여 동일 인터럽트로 인해 호스트가 반복적으로 깨어날 수 있다.

반면 MSI는 엣지 트리거 방식으로, 인터럽트가 PCIe Memory Write TLP 형태의 단발성 이벤트로 전달되므로 별도의 ACK 메커니즘이 불필요하다.

 

 

 

2. DMA 전송 최적화를 위한 SQ/CQ 추가

DMA를 쓰는 것만으로 CPU를 데이터 이동에서 해방시킬 순 있다. 그렇지만 드라이버가 다음 작업을 요청하기 전에 이전 작업의 완료를 기다리는 구조라면, 한 번에 하나의 작업만 진행할 수 있어서 처리량이 제한되고 DMA의 이점을 최대로 누리기 힘들다.

 

이러한 병목을 없애기 위해 NVMe에서 사용하는 SQ/CQ 구조를 사용하였다. 드라이버는 완료를 기다리지 않고 SQ(Submission Queue)에 작업을 계속 쌓고, 장치는 SQ를 비워가며 처리하고 완료된 항목의 정보 CQ(Completion Queue)에 통보하는 구조로 파이프라인 구성이 가능해진다.

 

 

구체적 동작 흐름은 다음과 같다.

 

  1. 작업 요청 제출 : 드라이버는 SQ에 작업요청 디스크립터를 쓰고, SQ Tail 포인터를 증가시켜 도어벨을 울린다.
  2. 장치 작업 : 장치는 도어벨을 감지하면 SQ에서 작업 요청을 가져오고, DMA를 통해 디스크립터에 명시된 호스트 시스템 메모리에 직접 접근하여 작업을 처리한다. 이 때 장치는 SQ Head를 증가시킨다.
  3. 작업 완료 보고 : 장치가 작업을 끝내면 CQ에 완료 엔트리를 쓰고, CQ Tail을 증가시킨다. 이후 인터럽트를 발생시켜 드라이버에게 알린다.
  4. 드라이버 확인 : 드라이버는 인터럽트를 받고 CQ를 확인하여 장치가 작업 완료한 항목을 처리한다. 처리가 끝나면 CQ Head를 증가시켜 도어벨을 울리고 장치에게 해당 슬롯을 반납한다.

 

즉, SQ에는 수행할 작업의 세부 정보가 담기고, CQ에는 어떤 SQ가 성공적으로 완료되었는지 정보가 담긴다. 각 큐의 포인터 중 2개는 드라이버가, 나머지 2개는 장치가 제어하는 구조이다.

 

구분 포인터 관리 주체 설명
SQ
(Submission Queue)
Tail 호스트 드라이버 새 작업을 넣고 "여기까지 읽어"라고 도어벨을 울림
Head 장치 장치가 명령을 어디까지 읽었는지 알림
CQ
(Completion Queue)
Tail 장치 장치가 작업 완료 정보를 쓰고 "여기까지 했다"고 알림
Head 호스트 드라이버 드라이버가 작업 완료 항목을 어디까지 읽었는지 도어벨을 울림

 

 

참고로 SQ/CQ의 엔트리 개수는 장치에 고정되어 있지 않고, 드라이버가 초기화 시점에 설정할 수 있다. SQ/CQ의 실체는 호스트 시스템 메모리에 존재하는 디스크립터/구조체 배열이며, 장치가 직접 접근해야 하므로 드라이버에서 초기화 시 DMA 가능 영역으로 할당해야한다.

 

한편 MMIO BAR에도 같은 용도의 SQ/CQ 포인터 레지스터가 존재하는데, 이쪽은 장치에 신호를 전달하거나 상태를 확인해 드라이버의 큐와 동기화하는 용도로 쓰인다.

 

 

 

3. 작업의 메타데이터를 BAR0이 아닌 SQ 디스크립터 통해 전달

BAR0에서 REG_FILTER, REG_WIDTH, REG_HEIGHT, REG_FORMAT, REG_PARAM 레지스터를 삭제하고, 작업과 관련된 메타데이터를 모두 SQ 디스크립터 안에 포함시켰다. 따라서 각 작업마다 필터 종류나 해상도 정보 등을 독립적으로 설정하는 것도 가능하다.

 

 

 

 

4. REG_STATUS의 비트필드 수정

  • 기존
    • Bit0: Busy, Bit1: Done, Bit2: Error
  • 수정
    • Bit0: Busy, Bit1: Error

장치의 작업 완료는 CQ를 통해 작업별로 통보되기 때문에, 전역 상태 레지스터에 Done 비트를 둘 이유가 사라져서 삭제했다.

 

 

 

5. REG_COMMAND의 Start 비트 삭제하고 Reset 비트로 대체

기존에는 REG_COMMAND의 Start 비트를 써서 장치에 작업 시작을 알렸지만, SQ/CQ 구조에선 드라이버가 REG_SQ_TAIL 도어벨을 울리는 것 자체가 작업 시작 신호이므로 별도 Start 비트를 둘 이유가 사라졌다.

 

대신 그 자리는 Reset 비트로 교체했는데, 드라이버의 비정상 종료 또는 작업 중단 상태에서 큐를 리셋하고 처음부터 다시 시작해야하는 경우에 대비한 것이다.

  

 

 

 

소스코드

#include "qemu/osdep.h"
#include "qemu/units.h"
#include "hw/pci/pci.h"
#include "hw/pci/pci_device.h"
#include "qom/object.h"
#include "qemu/thread.h"
#include "hw/pci/msi.h"

#define TYPE_PCI_VIP_DMA_DEV    "pci-vip-dma"

// Device ID
#define VIP_DMA_DEVICE_ID       0x600D

// BAR0 레지스터 오프셋
#define REG_ID                  0x00   // 장치 식별자 (0xFEE1600D) - RO
#define REG_STATUS              0x04   // 상태 (Bit0: Busy, Bit1: Error) - RO
#define REG_COMMAND             0x08   // Bit0: RESET (self-clearing) - WO

// SQ 관련 레지스터
#define REG_SQ_BASE_L           0x40   // SQ 배열 DMA 물리 주소 Low 32bit - RW
#define REG_SQ_BASE_H           0x44   // SQ 배열 DMA 물리 주소 High 32bit - RW
#define REG_SQ_SIZE             0x48   // SQ 슬롯 수 - RW
#define REG_SQ_TAIL             0x4C   // 드라이버 -> 장치 도어벨 (새 작업 추가) - RW
#define REG_SQ_HEAD             0x50   // 장치 처리 위치 - RO

// CQ 관련 레지스터
#define REG_CQ_BASE_L           0x54   // CQ 배열 DMA 물리 주소 Low 32bit - RW
#define REG_CQ_BASE_H           0x58   // CQ 배열 DMA 물리 주소 High 32bit - RW
#define REG_CQ_SIZE             0x5C   // CQ 슬롯 수 - RW
#define REG_CQ_HEAD             0x60   // 드라이버 -> 장치 도어벨 (슬롯 반납) - RW
#define REG_CQ_TAIL             0x64   // 장치 완료 위치 - RO

#define NUM_REGS                (0x68 / 4)  // 레지스터 배열 크기

#define INTERNAL_BUF_SIZE       (16 * MiB)

// 장치 식별자
#define VIP_IDENTIFIER          0xFEE1600DUL

// REG_STATUS bits
#define VIP_STATUS_BUSY         (1u << 0)
#define VIP_STATUS_ERROR        (1u << 1)

// REG_COMMAND bits
#define VIP_CMD_RESET           (1u << 0)

// Filter Type
#define VIP_FILT_GRAYSCALE      0
#define VIP_FILT_INVERT         1
#define VIP_FILT_BRIGHTNESS     2

// CQ 완료 상태
#define VIP_CQ_STATUS_OK        0
#define VIP_CQ_STATUS_ERROR     1

// SQ 디스크립터 구조체 (드라이버가 시스템 RAM에 작성, 장치가 DMA로 읽음)
// v1에서 BAR0 레지스터에 있던 필터/width/height/format/param이 이 구조체 안으로 이동
typedef struct {
    uint64_t in_addr;      // 입력 이미지 DMA 물리 주소
    uint64_t out_addr;     // 출력 이미지 DMA 물리 주소
    uint32_t width;
    uint32_t height;
    uint32_t format;       // 0: RGB24, 1: BGR24
    uint32_t filter;       // 0: Grayscale, 1: Invert, 2: Brightness
    uint32_t param;        // Brightness 파라미터 (Bit8: Sign, Bit7:0: 절댓값 0~255)
    uint32_t id;           // 작업 식별자 (드라이버가 부여, CQ에 그대로 반환됨)
    uint32_t reserved[2];  // 패딩 (64바이트 캐시라인 정렬 권장)
} VipSqDesc;               // 48 bytes

// CQ 엔트리 구조체 (장치가 DMA로 씀, 드라이버가 읽음)
typedef struct {
    uint32_t id;           // 완료된 작업 식별자 (SQ desc의 id와 동일)
    uint32_t status;       // 0: 성공, 1: 에러
    uint32_t sq_head;      // 완료 시점 장치의 SQ HEAD 위치
    uint32_t phase;        // 위상 비트 (링 한 바퀴마다 0<->1 반전, 새 엔트리 판별용)
} VipCqDesc;              // 16 bytes

// 장치 내부 상태 관리 구조체
typedef struct VipDmaState {
    PCIDevice pdev;         // PCIDevice 상속
    MemoryRegion mmio;      // BAR 0: 제어 및 큐 레지스터 영역

    QemuThread thread;      // 워커 스레드
    QemuMutex thr_mutex;    // 상태 동기화용 뮤텍스
    QemuCond thr_cond;      // 워커 스레드 작업 시작 신호 조건 변수
    bool stopping;          // 워커 스레드 종료 플래그
    bool resetting;         // RESET 요청 플래그 (abort 후 워커가 상태 초기화)

    QEMUBH *irq_bh;

    uint32_t regs[NUM_REGS];   // 내부 레지스터 값 보관 배열
    uint8_t *internal_buf;     // 장치 내부 처리용 버퍼

    // 장치가 내부적으로 관리하는 링 버퍼 상태
    uint32_t sq_head;   // 장치의 SQ 읽기 포인터 (regs[REG_SQ_HEAD/4]와 항상 동기화)
    uint32_t cq_tail;   // 장치의 CQ 쓰기 포인터 (regs[REG_CQ_TAIL/4]와 항상 동기화)
    uint32_t cq_phase;  // 현재 CQ 쓰기 위상 비트 (초기값 1, 한 바퀴마다 반전)
} VipDmaState;

OBJECT_DECLARE_SIMPLE_TYPE(VipDmaState, PCI_VIP_DMA_DEV)

// 나중에 메인루프 컨텍스트에서 지연호출 하기 위한 콜백 함수
static void vip_irq_bh(void *opaque)
{
    VipDmaState *s = opaque;

    // PCI 드라이버에 의해 MSI가 활성화되어 있는지 체크 후 벡터 0번으로 인터럽트를 통지
    // MSI는 INTx와 달리 ACK 없이 단순 메시지 전송으로 완료됨
    if (msi_enabled(&s->pdev)) {
        msi_notify(&s->pdev, 0);
    }
}

// 이미지 프로세싱 알고리즘 수행
// v2에서는 메타데이터(width/height/filter/param/format)를 SQ 디스크립터에서 직접 받음
static bool vip_process_image(VipDmaState *s, const VipSqDesc *desc) {
    uint8_t *buf = s->internal_buf;
    uint32_t w = desc->width;
    uint32_t h = desc->height;
    uint32_t filter = desc->filter;
    int32_t param = (desc->param & 0x100) ? -(int32_t)(desc->param & 0xFF) : (int32_t)(desc->param & 0xFF);
    uint32_t format = desc->format;

    const uint32_t bytes_per_pixel = 3; // RGB or BGR만 지원
    uint32_t total_bytes = w * h * bytes_per_pixel;

    if (total_bytes == 0 || total_bytes > INTERNAL_BUF_SIZE) {
        return false;
    }

    switch (filter) {
        case VIP_FILT_GRAYSCALE:
            // ...
            break;
        case VIP_FILT_INVERT:
            // ...
            break;
        case VIP_FILT_BRIGHTNESS:
            // ...
            break;
        default:
            return false;
    }
    return true;
}

// 워커 스레드
static void *vip_worker_thread(void *opaque) {
    VipDmaState *s = opaque;

    while (1) {
        qemu_mutex_lock(&s->thr_mutex);

        // SQ에 새 작업이 생기거나, reset/stop 요청이 올 때까지 대기
        while (!s->stopping && !s->resetting) {
            if (s->regs[REG_SQ_TAIL / 4] != s->sq_head)
                break;
            qemu_cond_wait(&s->thr_cond, &s->thr_mutex);
        }

        if (s->stopping) {
            qemu_mutex_unlock(&s->thr_mutex);
            break;
        }

        if (s->resetting) {
            goto do_reset;
        }

        // 처리에 필요한 정보 스냅샷 (뮤텍스 보호 하에 읽음)
        uint64_t sq_base = ((uint64_t)s->regs[REG_SQ_BASE_H / 4] << 32) | s->regs[REG_SQ_BASE_L / 4];
        uint64_t cq_base = ((uint64_t)s->regs[REG_CQ_BASE_H / 4] << 32) | s->regs[REG_CQ_BASE_L / 4];
        uint32_t sq_size = s->regs[REG_SQ_SIZE / 4];
        uint32_t cq_size = s->regs[REG_CQ_SIZE / 4];
        uint32_t sq_tail_snap = s->regs[REG_SQ_TAIL / 4];
        uint32_t cur_sq_head = s->sq_head;

        // REG_STATUS Busy 비트 설정
        s->regs[REG_STATUS / 4] |= VIP_STATUS_BUSY;

        qemu_mutex_unlock(&s->thr_mutex);   // DMA 작업 동안 뮤텍스 해제

        // 스냅샷한 sq_tail까지 대기 중인 SQ 엔트리를 모두 처리
        bool any_error = false;
        while (cur_sq_head != sq_tail_snap) {
            // RESET 또는 종료 요청 시 즉시 abort
            if (s->resetting || s->stopping) break;

            // 1. SQ[cur_sq_head] 디스크립터를 Host RAM에서 DMA Read
            VipSqDesc desc;
            uint64_t desc_addr = sq_base + (uint64_t)cur_sq_head * sizeof(VipSqDesc);
            pci_dma_read(&s->pdev, desc_addr, &desc, sizeof(desc));

            // 2. 입력 이미지를 Host RAM에서 장치 내부 버퍼로 DMA Read
            uint32_t img_size = desc.width * desc.height * 3;
            bool ok = false;
            if (img_size > 0 && img_size <= INTERNAL_BUF_SIZE) {
                pci_dma_read(&s->pdev, desc.in_addr, s->internal_buf, img_size);

                // 3. 이미지 처리 수행
                ok = vip_process_image(s, &desc);

                // 4. 처리된 이미지를 장치 내부 버퍼에서 Host RAM으로 DMA Write
                if (ok) {
                    pci_dma_write(&s->pdev, desc.out_addr, s->internal_buf, img_size);
                }
            }

            // 5. CQ full인 경우 드라이버가 CQ HEAD를 전진시킬 때까지 대기
            uint32_t next_cq_tail = (s->cq_tail + 1) % cq_size;
            qemu_mutex_lock(&s->thr_mutex);
            while (next_cq_tail == s->regs[REG_CQ_HEAD / 4] && !s->resetting && !s->stopping) {
                qemu_cond_wait(&s->thr_cond, &s->thr_mutex);
            }
            if (s->resetting || s->stopping) {
                qemu_mutex_unlock(&s->thr_mutex);
                break;
            }
            qemu_mutex_unlock(&s->thr_mutex);

            // 6. CQ[cq_tail]에 완료 엔트리 기록 (DMA Write)
            VipCqDesc cq_entry = {
                .id      = desc.id,
                .status  = ok ? VIP_CQ_STATUS_OK : VIP_CQ_STATUS_ERROR,
                .sq_head = cur_sq_head,
                .phase   = s->cq_phase,
            };
            uint64_t cq_addr = cq_base + (uint64_t)s->cq_tail * sizeof(VipCqDesc);
            pci_dma_write(&s->pdev, cq_addr, &cq_entry, sizeof(cq_entry));

            // 7. SQ HEAD, CQ TAIL 포인터 전진
            cur_sq_head = (cur_sq_head + 1) % sq_size;
            s->cq_tail = (s->cq_tail + 1) % cq_size;

            // CQ가 한 바퀴 돌면 phase 비트 반전 (드라이버의 새 엔트리 판별 기준)
            if (s->cq_tail == 0) {
                s->cq_phase ^= 1;
            }

            if (!ok) any_error = true;
        }

        // 8. 레지스터 업데이트 및 MSI 발생
        qemu_mutex_lock(&s->thr_mutex);

        if (s->resetting) {
            goto do_reset;
        }

        s->sq_head = cur_sq_head;
        s->regs[REG_SQ_HEAD / 4] = cur_sq_head;
        s->regs[REG_CQ_TAIL / 4] = s->cq_tail;
        s->regs[REG_STATUS / 4] &= ~VIP_STATUS_BUSY;
        if (any_error) {
            s->regs[REG_STATUS / 4] |= VIP_STATUS_ERROR;
        }
        qemu_mutex_unlock(&s->thr_mutex);

        qemu_bh_schedule(s->irq_bh);
        continue;

do_reset:
        // thr_mutex 보유 상태에서 진입
        s->sq_head  = 0;
        s->cq_tail  = 0;
        s->cq_phase = 1;
        s->regs[REG_SQ_TAIL / 4] = 0;
        s->regs[REG_SQ_HEAD / 4] = 0;
        s->regs[REG_CQ_HEAD / 4] = 0;
        s->regs[REG_CQ_TAIL / 4] = 0;
        s->regs[REG_STATUS / 4]  = 0;
        s->regs[REG_COMMAND / 4] = 0;
        s->resetting = false;
        qemu_mutex_unlock(&s->thr_mutex);
    }
    return NULL;
}

// MMIO read 콜백 함수
static uint64_t vip_mmio_read(void *opaque, hwaddr addr, unsigned size) {
    // addr은 할당된 BAR의 시작 점을 기준으로 오프셋 바이트가 들어옴

    VipDmaState *s = opaque;
    uint32_t index = addr / 4;

    // REG_ID 접근 시
    if (addr == REG_ID) {
        return VIP_IDENTIFIER;
    }

    // 정의된 레지스터 배열 범위 내 접근 시
    if (index < NUM_REGS) {
        return s->regs[index];
    }

    // QEMU는 64비트 값을 리턴해도 드라이버가 읽기 요청한 사이즈에 맞게 잘라서 전달함
    return ~0ULL;
}

// MMIO write 콜백 함수
static void vip_mmio_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) {
    VipDmaState *s = opaque;
    uint32_t index = addr / 4;
    if (index >= NUM_REGS) return;

    switch (addr) {
        // Read-Only
        case REG_ID:
        case REG_STATUS:
        case REG_SQ_HEAD:
        case REG_CQ_TAIL:
            break;

        case REG_COMMAND:
            // RESET 명령이면 워커 깨워 abort 신호 전송하고, 워커가 상태 초기화 수행
            if (val & VIP_CMD_RESET) {
                qemu_mutex_lock(&s->thr_mutex);
                s->resetting = true;
                qemu_cond_signal(&s->thr_cond);
                qemu_mutex_unlock(&s->thr_mutex);
            }
            break;

        case REG_SQ_TAIL:
            // 새 SQ 엔트리가 추가 알림 (드라이버 -> 장치 도어벨)
            // 워커를 깨워 작업 처리 시작
            qemu_mutex_lock(&s->thr_mutex);
            s->regs[index] = (uint32_t)val;
            qemu_cond_signal(&s->thr_cond);
            qemu_mutex_unlock(&s->thr_mutex);
            break;

        case REG_CQ_HEAD:
            // CQ 슬롯 소비 완료 알림 (드라이버 -> 장치 도어벨)
            // CQ full 상태로 대기 중인 워커를 깨움
            qemu_mutex_lock(&s->thr_mutex);
            s->regs[index] = (uint32_t)val;
            qemu_cond_signal(&s->thr_cond);
            qemu_mutex_unlock(&s->thr_mutex);
            break;

        default:
            s->regs[index] = (uint32_t)val;
            break;
    }
}

static const MemoryRegionOps vip_mmio_ops = {
    .read = vip_mmio_read,
    .write = vip_mmio_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
    // 구현한 콜백의 실제 데이터 처리 사이즈 범위
    // 이 범위 미만 또는 초과 사이즈로 접근 시 QEMU가 자동으로 병합하거나 쪼개서 콜백 호출함
    .impl = {
        .min_access_size = 4,
        .max_access_size = 4
    },
    // 접근 유효성 체크 범위, 생략 가능함
    .valid = {
        .min_access_size = 4,
        .max_access_size = 8
    },
};

// PCI 장치 생성 시 호출되는 초기화 함수
static void pci_vip_dma_realize(PCIDevice *pdev, Error **errp) {
    VipDmaState *s = PCI_VIP_DMA_DEV(pdev);

    // MSI 인터럽트 초기화 (벡터번호는 장치별로 0부터 시작하며, 1개만 사용함)
    if (msi_init(pdev, 0, 1, true, false, errp)) {
        return;
    }

    // BAR 0: 제어 및 상태(큐 설정 포함) 레지스터 영역 (4KB)
    memory_region_init_io(&s->mmio, OBJECT(s), &vip_mmio_ops, s, "vip-mmio", 4 * KiB);
    pci_register_bar(pdev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &s->mmio);

    // 내부 작업용 버퍼 16MB 동적 할당
    s->internal_buf = g_malloc0(INTERNAL_BUF_SIZE);

    // CQ phase 초기값 1로 설정 (드라이버가 CQ를 0으로 초기화하므로 1이어야 새 엔트리로 인식됨)
    s->cq_phase = 1;

    // 워커 스레드 시작
    qemu_mutex_init(&s->thr_mutex);
    qemu_cond_init(&s->thr_cond);
    qemu_thread_create(&s->thread, "vip-worker", vip_worker_thread, s, QEMU_THREAD_JOINABLE);
    s->irq_bh = qemu_bh_new(vip_irq_bh, s);
}

// PCI 장치 소멸 시 호출되는 함수
static void pci_vip_dma_uninit(PCIDevice *pdev) {
    VipDmaState *s = PCI_VIP_DMA_DEV(pdev);

    // 스레드 종료
    qemu_mutex_lock(&s->thr_mutex);
    s->stopping = true;
    qemu_cond_signal(&s->thr_cond);
    qemu_mutex_unlock(&s->thr_mutex);
    qemu_thread_join(&s->thread);

    qemu_bh_delete(s->irq_bh);
    g_free(s->internal_buf);
    msi_uninit(pdev);
}

// ...

 

자세한 사항은 주석으로 달았다.

 

 

QEMU에 해당 장치 지원을 추가하는 방법은 이전 글에서 설명했기 때문에 생략한다.

 

 

 

 

반응형