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デバイスドライバ開発:
- newbusフレームワークでデバイスを抽象化
-
probe()→attach()→detach()のライフサイクル -
bus_alloc_resource_any()でリソース割り当て -
bus_space_read/writeでレジスタアクセス - DMAも
bus_dma_*でサポート
Linuxのドライバより構造がきれいだと個人的に思う。
次回予告
Part4: カスタムシステムコールを追加する
自分だけのシステムコールを作ってみよう。カーネルとユーザー空間の境界を理解する絶好の機会だ。
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!