4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【FreeBSD】PCIデバイスドライバを書いてハードウェアを制御する【カーネル開発Part3】

Posted at

FreeBSDカーネル開発シリーズ

Part1 ビルド Part2 モジュール Part3 ドライバ Part4 システムコール Part5 DTrace
✅ Done ✅ Done 👈 Now - -

はじめに

前回はカーネルモジュールの基本を学んだ。

今回は実際のハードウェアを制御するデバイスドライバを書く。

FreeBSDのデバイスドライバフレームワーク(newbus)を使って、PCIデバイスを認識・制御する方法を解説する。

newbusフレームワーク

FreeBSDのデバイスドライバはnewbusというフレームワークで書く。

┌─────────────────────────────────────────────────────────────┐
│                    デバイスツリー                            │
│                                                              │
│                       root0                                  │
│                         │                                    │
│                      nexus0                                  │
│                    /   │   \                               │
│               acpi0   pci0   isab0                          │
│                 │       │      │                            │
│              cpu0   em0(NIC)  isa0                          │
│                     ahci0    uart0                          │
│                       │                                      │
│                    ada0(SSD)                                │
└─────────────────────────────────────────────────────────────┘

各デバイスは親子関係を持ち、バスを通じて接続される。

PCIデバイスの基本

PCI設定空間

┌─────────────────────────────────────────────────────────────┐
│                  PCI Configuration Space                     │
│                                                              │
│  Offset 00h: Vendor ID (2 bytes) | Device ID (2 bytes)      │
│  Offset 04h: Command | Status                                │
│  Offset 08h: Revision | Class Code (3 bytes)                 │
│  Offset 0Ch: Cache Line | Latency | Header Type | BIST      │
│  Offset 10h-24h: Base Address Registers (BAR0-5)            │
│  Offset 2Ch: Subsystem Vendor ID | Subsystem ID             │
│  Offset 3Ch: Interrupt Line | Interrupt Pin                  │
└─────────────────────────────────────────────────────────────┘

pciconfで確認

pciconf -lv
# em0@pci0:0:25:0:        class=0x020000 rev=0x02 hdr=0x00 vendor=0x8086 device=0x153a subvendor=0x8086 subdevice=0x153a
#     vendor     = 'Intel Corporation'
#     device     = 'Ethernet Connection I217-LM'
#     class      = network
#     subclass   = ethernet
  • vendor=0x8086: Intel
  • device=0x153a: I217-LM
  • pci0:0:25:0: bus:device:function

最小のPCIドライバ

ソースコード

mkdir -p ~/kmod/mypci
cd ~/kmod/mypci
vi mypci.c
#include <sys/param.h>
#include <sys/module.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/rman.h>

#include <dev/pci/pcivar.h>
#include <dev/pci/pcireg.h>

/* サポートするデバイスのリスト */
/* 実際には存在しないダミーID */
#define MY_VENDOR_ID    0x1234
#define MY_DEVICE_ID    0x5678

struct mypci_softc {
    device_t        dev;
    struct resource *mem_res;
    int             mem_rid;
    struct resource *irq_res;
    int             irq_rid;
    void            *irq_handle;
};

/* probe: このドライバがデバイスをサポートするか判定 */
static int
mypci_probe(device_t dev)
{
    uint16_t vendor, device;

    vendor = pci_get_vendor(dev);
    device = pci_get_device(dev);

    if (vendor == MY_VENDOR_ID && device == MY_DEVICE_ID) {
        device_set_desc(dev, "My PCI Device");
        return (BUS_PROBE_DEFAULT);
    }

    return (ENXIO);
}

/* attach: デバイスの初期化 */
static int
mypci_attach(device_t dev)
{
    struct mypci_softc *sc;

    sc = device_get_softc(dev);
    sc->dev = dev;

    /* BAR0をマップ */
    sc->mem_rid = PCIR_BAR(0);
    sc->mem_res = bus_alloc_resource_any(dev, SYS_RES_MEMORY,
                                         &sc->mem_rid, RF_ACTIVE);
    if (sc->mem_res == NULL) {
        device_printf(dev, "could not allocate memory resource\n");
        return (ENXIO);
    }

    device_printf(dev, "BAR0 mapped at %p, size %lu\n",
                  rman_get_virtual(sc->mem_res),
                  rman_get_size(sc->mem_res));

    /* 割り込みを設定 */
    sc->irq_rid = 0;
    sc->irq_res = bus_alloc_resource_any(dev, SYS_RES_IRQ,
                                         &sc->irq_rid,
                                         RF_ACTIVE | RF_SHAREABLE);
    if (sc->irq_res == NULL) {
        device_printf(dev, "could not allocate IRQ resource\n");
        /* 割り込みなしでも続行 */
    }

    device_printf(dev, "attached successfully\n");
    return (0);
}

/* detach: デバイスのクリーンアップ */
static int
mypci_detach(device_t dev)
{
    struct mypci_softc *sc;

    sc = device_get_softc(dev);

    if (sc->irq_handle != NULL) {
        bus_teardown_intr(dev, sc->irq_res, sc->irq_handle);
    }
    if (sc->irq_res != NULL) {
        bus_release_resource(dev, SYS_RES_IRQ, sc->irq_rid, sc->irq_res);
    }
    if (sc->mem_res != NULL) {
        bus_release_resource(dev, SYS_RES_MEMORY, sc->mem_rid, sc->mem_res);
    }

    device_printf(dev, "detached\n");
    return (0);
}

/* デバイスメソッド */
static device_method_t mypci_methods[] = {
    DEVMETHOD(device_probe,     mypci_probe),
    DEVMETHOD(device_attach,    mypci_attach),
    DEVMETHOD(device_detach,    mypci_detach),
    DEVMETHOD_END
};

static driver_t mypci_driver = {
    "mypci",
    mypci_methods,
    sizeof(struct mypci_softc)
};

DRIVER_MODULE(mypci, pci, mypci_driver, 0, 0);

Makefile

KMOD=   mypci
SRCS=   mypci.c
SRCS+=  bus_if.h device_if.h pci_if.h

.include <bsd.kmod.mk>

ビルド

make

このドライバは実際には存在しないデバイスID(0x1234:0x5678)を使っているので、ロードしても何も起きない。

実際のデバイスに合わせてVendor ID/Device IDを変更する必要がある。

メモリマップドI/O

PCIデバイスのレジスタにアクセスする方法。

#include <machine/bus.h>

struct mypci_softc {
    bus_space_tag_t     mem_tag;
    bus_space_handle_t  mem_handle;
    /* ... */
};

static int
mypci_attach(device_t dev)
{
    struct mypci_softc *sc = device_get_softc(dev);

    /* ... リソース割り当て ... */

    sc->mem_tag = rman_get_bustag(sc->mem_res);
    sc->mem_handle = rman_get_bushandle(sc->mem_res);

    /* レジスタ読み取り (32bit) */
    uint32_t val = bus_space_read_4(sc->mem_tag, sc->mem_handle, 0x00);

    /* レジスタ書き込み (32bit) */
    bus_space_write_4(sc->mem_tag, sc->mem_handle, 0x00, 0x12345678);

    /* 8/16bitアクセス */
    uint8_t  val8  = bus_space_read_1(sc->mem_tag, sc->mem_handle, 0x10);
    uint16_t val16 = bus_space_read_2(sc->mem_tag, sc->mem_handle, 0x12);

    return (0);
}

割り込みハンドラ

/* 割り込みハンドラ */
static void
mypci_intr(void *arg)
{
    struct mypci_softc *sc = arg;
    uint32_t status;

    /* 割り込みステータスを読む */
    status = bus_space_read_4(sc->mem_tag, sc->mem_handle, REG_INT_STATUS);

    if (status == 0) {
        /* 自分の割り込みではない(共有割り込みの場合) */
        return;
    }

    /* 割り込みをクリア */
    bus_space_write_4(sc->mem_tag, sc->mem_handle, REG_INT_STATUS, status);

    device_printf(sc->dev, "interrupt! status=0x%08x\n", status);
}

static int
mypci_attach(device_t dev)
{
    struct mypci_softc *sc = device_get_softc(dev);
    int error;

    /* ... リソース割り当て ... */

    /* 割り込みハンドラを登録 */
    error = bus_setup_intr(dev, sc->irq_res,
                           INTR_TYPE_NET | INTR_MPSAFE,
                           NULL,           /* filter (高速パス) */
                           mypci_intr,     /* handler */
                           sc,             /* arg */
                           &sc->irq_handle);
    if (error) {
        device_printf(dev, "could not setup interrupt\n");
        return (error);
    }

    return (0);
}

MSI/MSI-X割り込み

レガシー割り込みよりMSI/MSI-Xが効率的。

static int
mypci_attach(device_t dev)
{
    struct mypci_softc *sc = device_get_softc(dev);
    int msi_count;

    /* MSIを有効化 */
    msi_count = pci_msi_count(dev);
    if (msi_count > 0) {
        msi_count = 1;  /* 1本だけ使う */
        if (pci_alloc_msi(dev, &msi_count) == 0) {
            device_printf(dev, "Using MSI interrupt\n");
            sc->irq_rid = 1;  /* MSIの場合はRID=1 */
        }
    }

    /* MSI-Xの場合 */
    int msix_count = pci_msix_count(dev);
    if (msix_count > 0) {
        msix_count = MIN(msix_count, 4);  /* 最大4本 */
        if (pci_alloc_msix(dev, &msix_count) == 0) {
            device_printf(dev, "Using %d MSI-X interrupts\n", msix_count);
        }
    }

    /* ... 割り込みリソース割り当て ... */

    return (0);
}

DMAバッファ

DMAを使ってデバイスとメモリ間でデータ転送。

#include <machine/bus.h>

struct mypci_softc {
    bus_dma_tag_t   dma_tag;
    bus_dmamap_t    dma_map;
    void           *dma_vaddr;      /* 仮想アドレス */
    bus_addr_t      dma_paddr;      /* 物理アドレス */
    /* ... */
};

static void
mypci_dmamap_cb(void *arg, bus_dma_segment_t *segs, int nseg, int error)
{
    if (error)
        return;
    *(bus_addr_t *)arg = segs[0].ds_addr;
}

static int
mypci_attach(device_t dev)
{
    struct mypci_softc *sc = device_get_softc(dev);
    int error;

    /* DMAタグ作成 */
    error = bus_dma_tag_create(
        bus_get_dma_tag(dev),   /* 親タグ */
        4,                       /* アラインメント */
        0,                       /* バウンダリ */
        BUS_SPACE_MAXADDR,      /* lowaddr */
        BUS_SPACE_MAXADDR,      /* highaddr */
        NULL, NULL,              /* filter */
        4096,                    /* サイズ */
        1,                       /* nsegments */
        4096,                    /* maxsegsz */
        0,                       /* flags */
        NULL, NULL,              /* lockfunc */
        &sc->dma_tag);
    if (error) {
        device_printf(dev, "could not create DMA tag\n");
        return (error);
    }

    /* DMAマップ作成 */
    error = bus_dmamem_alloc(sc->dma_tag, &sc->dma_vaddr,
                             BUS_DMA_WAITOK | BUS_DMA_COHERENT,
                             &sc->dma_map);
    if (error) {
        device_printf(dev, "could not allocate DMA memory\n");
        return (error);
    }

    /* 物理アドレスを取得 */
    error = bus_dmamap_load(sc->dma_tag, sc->dma_map, sc->dma_vaddr,
                            4096, mypci_dmamap_cb, &sc->dma_paddr, 0);
    if (error) {
        device_printf(dev, "could not load DMA map\n");
        return (error);
    }

    device_printf(dev, "DMA buffer: vaddr=%p, paddr=0x%lx\n",
                  sc->dma_vaddr, (unsigned long)sc->dma_paddr);

    return (0);
}

static int
mypci_detach(device_t dev)
{
    struct mypci_softc *sc = device_get_softc(dev);

    if (sc->dma_map != NULL) {
        bus_dmamap_unload(sc->dma_tag, sc->dma_map);
        bus_dmamem_free(sc->dma_tag, sc->dma_vaddr, sc->dma_map);
    }
    if (sc->dma_tag != NULL) {
        bus_dma_tag_destroy(sc->dma_tag);
    }

    return (0);
}

実際のドライバを読む

FreeBSDの実際のドライバソースを読んでみよう。

# シンプルなネットワークドライバ
less /usr/src/sys/dev/dc/if_dc.c       # DEC 21143 (古いけどシンプル)

# Intel NIC
less /usr/src/sys/dev/e1000/if_em.c   # Intel Gigabit Ethernet

# NVMe
less /usr/src/sys/dev/nvme/nvme.c

# AHCI (SATA)
less /usr/src/sys/dev/ahci/ahci.c

デバッグTips

device_printf

device_printf(dev, "debug: value=0x%x\n", value);

sysctl経由でデバッグ

SYSCTL_INT(_hw, OID_AUTO, mypci_debug, CTLFLAG_RW,
    &mypci_debug_level, 0, "Debug level");

if (mypci_debug_level > 0) {
    device_printf(dev, "debug info...\n");
}

pciconfでレジスタを直接読む

# 設定空間のダンプ
pciconf -r pci0:0:25:0

# 特定オフセットを読む
pciconf -r pci0:0:25:0 0x10  # BAR0

まとめ

FreeBSDのPCIデバイスドライバ開発:

  1. newbusフレームワークでデバイスを抽象化
  2. probe()attach()detach()のライフサイクル
  3. bus_alloc_resource_any()でリソース割り当て
  4. bus_space_read/writeでレジスタアクセス
  5. DMAもbus_dma_*でサポート

Linuxのドライバより構造がきれいだと個人的に思う。

次回予告

Part4: カスタムシステムコールを追加する

自分だけのシステムコールを作ってみよう。カーネルとユーザー空間の境界を理解する絶好の機会だ。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?