Platform 드라이버 (platform_device / platform_driver)

리눅스 커널의 Platform 드라이버 모델은 PCIe/USB처럼 자동 열거가 불가능한 온칩(SoC) 주변장치를 위한 표준 드라이버 바인딩 계층입니다. platform_deviceplatform_driver 구조체, probe/remove 생명주기, platform_get_resource/devm_ioremap_resource 기반 리소스 획득, Device Tree·ACPI·레거시 방식의 매칭 메커니즘, devm_ 관리형 헬퍼를 활용한 안전한 드라이버 구조, MFD·Regmap과의 통합 패턴을 실무 예제와 함께 정리합니다.

전제 조건: 디바이스 드라이버Device Tree 문서를 먼저 읽으세요. Platform 드라이버는 리눅스 디바이스 모델의 가장 기본적인 구현 형태입니다.
일상 비유: Platform 드라이버는 건물 내 고정 배관과 같습니다. 벽에 고정된 배관(platform_device)은 자동 감지가 불가능하고, 도면(Device Tree)에 미리 기술해 두면 시스템이 초기화될 때 배관 담당 기술자(platform_driver)가 배정되어 작동을 시작합니다.

핵심 요약

  • platform_device — 주소, IRQ, DMA 채널 등 하드웨어 리소스를 기술하는 논리적 디바이스 객체입니다. SoC 내장 UART, I2C 컨트롤러, GPIO 등이 여기에 속합니다.
  • platform_driverprobe()/remove() 콜백과 매칭 테이블(of_match_table, acpi_match_table, id_table)을 포함하는 드라이버 구조체입니다.
  • 매칭 우선순위 — Device Tree → ACPI DSDT → id_table(레거시) 순으로 매칭을 시도합니다.
  • platform_get_resource() — 타입(IORESOURCE_MEM, IORESOURCE_IRQ)과 인덱스로 리소스를 획득합니다.
  • devm_ 헬퍼devm_ioremap_resource(), devm_clk_get() 등 device-managed 헬퍼는 드라이버 언바인드 시 자동 해제됩니다.
  • module_platform_driver()module_init/module_exitplatform_driver_register/platform_driver_unregister를 한 번에 선언하는 매크로입니다.
  • probe 반환값-EPROBE_DEFER를 반환하면 의존 리소스(클럭, 레귤레이터 등)가 준비될 때까지 재시도합니다.
  • syscore_ops / early_platform — 일반 드라이버 모델이 초기화되기 전에 구동되어야 하는 핵심 하드웨어를 위한 조기 초기화 경로도 있습니다.

단계별 이해

  1. 디바이스 등록 이해 — 하드웨어 기술 방식(Device Tree, ACPI, 레거시 보드 파일)에 따라 platform_device가 어떻게 생성되어 버스에 등록되는지 이해합니다.
  2. probe/remove 생명주기 파악 — 커널이 디바이스-드라이버 쌍을 매칭한 후 probe()를 호출하고, 언바인드 시 remove()를 호출하는 순서를 익힙니다.
  3. 리소스 획득 패턴 학습platform_get_resource()devm_ioremap_resource() 패턴과 platform_get_irq()devm_request_irq() 패턴을 실습합니다.
  4. devm_ 관리 자원 활용devm_clk_get(), devm_regulator_get(), devm_gpiod_get() 등 관리형 헬퍼를 사용하여 probe 실패 시 자동 정리가 되는 구조를 만듭니다.
  5. -EPROBE_DEFER 처리 — 의존 드라이버(클럭, 전원)가 아직 등록되지 않았을 때 반환하는 코드를 이해하고, 올바른 처리 방법을 익힙니다.
관련 페이지: 디바이스 드라이버 (디바이스 모델 전체), Device Tree (DT 바인딩), MFD (복합 디바이스), Regmap (레지스터 추상화), Regulator 프레임워크 (전원 관리)

개요 — Platform 버스란

Platform 버스(drivers/base/platform.c)는 PCIe·USB처럼 런타임에 장치를 검색할 수 없는 SoC 내장 주변장치를 위한 가상 버스(pseudo-bus)입니다. GPIO 컨트롤러, UART, I2C/SPI 컨트롤러, DMA 엔진, PWM, 타이머 등 거의 모든 SoC 내장 IP가 여기에 속합니다.

장치-드라이버 매칭은 세 가지 방식으로 이루어집니다.

방식매칭 테이블장치 기술 위치
Device Treeof_match_table (compatible 문자열).dts / .dtsi
ACPIacpi_match_table (HID/CID)DSDT / SSDT ACPI 테이블
레거시 (board file)id_table / .namearch/arm/mach-*/ 등 보드 파일

리눅스 커널은 매칭 우선순위를 DT → ACPI → id_table 순으로 적용합니다. 현대 SoC 드라이버는 거의 예외 없이 Device Tree 바인딩을 사용합니다.

Kernel Subsystems TTY/Serial IIO Sensors GPIO Chip clk_provider PWM Chip Regulator DMA Engine platform_driver (.probe / .remove / .driver.pm) uart-pl011arm,pl011 i2c-bcm2835brcm,bcm2835-i2c gpio-pl061arm,pl061 clk-bcm2835brcm,bcm2835-cprman pwm-bcm2835brcm,bcm2835-pwm ···기타 드라이버 Platform Bus (platform_bus_type) match(device, driver) → probe(device) → remove(device) platform_device (생성 경로) of_platform_populate()Device Tree acpi_create_platform_device()ACPI DSDT/SSDT platform_device_register()Board File (레거시) devm_mfd_add_devices()MFD 서브 디바이스 platform_device_alloc()동적 생성 devm_*_register() bus_match → probe() bus_add_device()

platform_device 등록

Device Tree 기반 등록 (현대적 방식)

Device Tree에 하드웨어를 기술하면 커널 부팅 시 of_platform_populate()가 DT 노드를 순회하며 platform_device를 자동으로 생성합니다.

/* arch/arm64/boot/dts/vendor/soc.dtsi */
uart0: serial@10000000 {
    compatible = "vendor,my-uart";
    reg = <0x10000000 0x1000>;  /* MMIO 기반 주소/크기 */
    interrupts = <GIC_SPI 30 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&uart_clk>;
    clock-names = "uart";
    status = "disabled";        /* 보드 파일에서 "okay"로 활성화 */
};
/* board.dts */
&uart0 {
    status = "okay";
};

레거시 보드 파일 등록

오래된 ARM 보드나 DT 미지원 플랫폼에서는 C 코드로 직접 platform_device를 정의합니다.

/* 메모리/IRQ 리소스 정의 */
static struct resource my_uart_resources[] = {
    {
        .start = 0x10000000,
        .end   = 0x10000FFF,
        .flags = IORESOURCE_MEM,
    },
    {
        .start = 30,
        .end   = 30,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device my_uart_device = {
    .name          = "my-uart",
    .id            = 0,
    .num_resources = ARRAY_SIZE(my_uart_resources),
    .resource      = my_uart_resources,
};

/* 보드 초기화 함수에서 등록 */
static void __init board_init(void)
{
    platform_device_register(&my_uart_device);
}

동적 등록 (드라이버에서 직접)

MFD 드라이버처럼 부모 드라이버가 자식 platform_device를 동적으로 생성하는 경우에는 platform_device_alloc() + platform_device_add()를 사용합니다.

struct platform_device *pdev;

pdev = platform_device_alloc("child-device", 0);
if (!pdev)
    return -ENOMEM;

pdev->dev.parent = &parent_pdev->dev;

ret = platform_device_add(pdev);
if (ret) {
    platform_device_put(pdev);
    return ret;
}

platform_driver 구조체

struct platform_driverstruct device_driver를 포함하며 probe/remove 콜백과 매칭 테이블을 담습니다.

/* include/linux/platform_device.h */
struct platform_driver {
    int (*probe)(struct platform_device *);
    void (*remove)(struct platform_device *);
    void (*shutdown)(struct platform_device *);
    int (*suspend)(struct platform_device *, pm_message_t state);
    int (*resume)(struct platform_device *);
    struct device_driver driver;
    const struct platform_device_id *id_table;
    bool prevent_deferred_probe;
    bool driver_managed_dma;
};

전형적인 드라이버 선언과 등록 패턴은 다음과 같습니다.

/* compatible 문자열로 DT 매칭 */
static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,my-uart", .data = &my_uart_variant_v1 },
    { .compatible = "vendor,my-uart-v2", .data = &my_uart_variant_v2 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_uart_of_match);

static const struct acpi_device_id my_uart_acpi_match[] = {
    { "VEND0001", 0 },
    { }
};
MODULE_DEVICE_TABLE(acpi, my_uart_acpi_match);

static struct platform_driver my_uart_driver = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name           = "my-uart",
        .of_match_table = my_uart_of_match,
        .acpi_match_table = ACPI_PTR(my_uart_acpi_match),
        .pm             = &my_uart_pm_ops,
    },
};

/* module_platform_driver()가 module_init/module_exit를 자동 생성 */
module_platform_driver(my_uart_driver);

probe / remove 생명주기

커널이 platform_device와 platform_driver를 매칭하면 probe()를 호출합니다. probe는 필요한 리소스를 모두 devm_ 계열 함수로 획득해야 합니다. probe 중간에 에러를 반환하면 그 전까지 획득한 모든 devm_ 리소스가 역순으로 자동 해제되므로 별도 에러 경로 정리 코드가 불필요합니다. remove()는 드라이버 언바인드 시 호출되며, devm_이 아닌 수동 할당 리소스만 여기서 해제합니다. 잘 작성된 현대 드라이버는 remove가 거의 비어 있습니다.

표준 probe 패턴

static int my_uart_probe(struct platform_device *pdev)
{
    struct my_uart_priv *priv;
    struct resource *res;
    int irq, ret;

    /* 1. private data 할당 (devm_ — remove 시 자동 해제) */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    priv->dev = &pdev->dev;

    /* 2. MMIO 리소스 획득 및 ioremap */
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* 3. IRQ 획득 */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;  /* -EPROBE_DEFER 포함 */

    /* 4. 클럭 획득 (미등록이면 -EPROBE_DEFER 반환) */
    priv->clk = devm_clk_get(&pdev->dev, "uart");
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);

    ret = clk_prepare_enable(priv->clk);
    if (ret)
        return ret;

    /* devm_add_action_or_reset으로 clk_disable_unprepare 등록 */
    ret = devm_add_action_or_reset(&pdev->dev,
                                   my_uart_disable_clk, priv);
    if (ret)
        return ret;

    /* 5. IRQ 등록 */
    ret = devm_request_irq(&pdev->dev, irq, my_uart_isr,
                           IRQF_SHARED, dev_name(&pdev->dev), priv);
    if (ret)
        return ret;

    /* 6. platform_set_drvdata: dev ↔ priv 연결 */
    platform_set_drvdata(pdev, priv);

    /* 7. 하드웨어 초기화 */
    my_uart_hw_init(priv);

    dev_info(&pdev->dev, "probed successfully\n");
    return 0;
}

remove 패턴

static void my_uart_remove(struct platform_device *pdev)
{
    struct my_uart_priv *priv = platform_get_drvdata(pdev);

    /* devm_ 리소스는 자동 해제되므로, 추가적인 수동 정리만 수행 */
    my_uart_hw_shutdown(priv);

    /* devm_ 핸들러에 등록하지 않은 클럭/레귤레이터 등은 여기서 해제 */
}
remove 반환값 변경: 리눅스 6.11부터 platform_driver.remove의 반환 타입이 int에서 void로 변경되었습니다. 반환값은 무시되었으므로 신규 드라이버는 void를 사용하세요.

리소스 관리

Platform 드라이버가 가장 먼저 하는 일은 DT 또는 보드 파일에 기술된 리소스(MMIO 주소, IRQ 번호, 클럭, 레귤레이터 등)를 획득하는 것입니다. devm_platform_ioremap_resource()request_mem_region() + ioremap()를 한 번에 수행하고 언바인드 시 자동으로 release_mem_region() + iounmap()을 수행합니다. 클럭이나 레귤레이터처럼 다른 드라이버가 제공하는 리소스를 획득할 때, 해당 드라이버가 아직 등록되지 않았다면 -EPROBE_DEFER가 반환되므로 반드시 에러를 전파해야 합니다.

MMIO 리소스 획득

/* 방법 1: 권장 — devm_platform_ioremap_resource() */
priv->base = devm_platform_ioremap_resource(pdev, 0);

/* 방법 2: 단계별 분리가 필요한 경우 */
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(&pdev->dev, res);

/* 방법 3: of_iomap (DT 전용, 권장하지 않음 — request_mem_region 미호출) */
priv->base = of_iomap(pdev->dev.of_node, 0);

IRQ 리소스 획득

/* 기본 IRQ (인덱스 0) */
irq = platform_get_irq(pdev, 0);
if (irq < 0)
    return irq;

/* 이름으로 IRQ 획득 (DT: interrupts-names = "rx", "tx") */
rx_irq = platform_get_irq_byname(pdev, "rx");
tx_irq = platform_get_irq_byname(pdev, "tx");

/* 옵션 IRQ — 없으면 -ENXIO 대신 0 반환 */
irq = platform_get_irq_optional(pdev, 1);

주요 devm_ 헬퍼 목록

헬퍼기능
devm_kzalloc()메모리 할당 (remove 시 자동 kfree)
devm_platform_ioremap_resource()MMIO 매핑 (request + ioremap)
devm_request_irq()IRQ 등록 (free_irq 자동)
devm_clk_get()클럭 핸들 획득
devm_regulator_get()레귤레이터 핸들 획득
devm_gpiod_get()GPIO 디스크립터 획득
devm_pinctrl_get()pinctrl 핸들 획득
devm_reset_control_get()리셋 컨트롤러 핸들
devm_add_action_or_reset()임의 정리 콜백 등록
devm_iio_device_register()IIO 디바이스 등록

probe / remove 생명주기 상태 다이어그램

버스 매칭 probe() 호출 platform_driver.probe(pdev) devm_ 리소스 획득 ioremap / irq / clk / gpio … -EPROBE_DEFER deferred_probe_list 등록 에러 반환 devm_ 자동 역순 해제 하드웨어 초기화 platform_set_drvdata() Probed (동작 중) 0 반환 → 바인딩 완료 remove() 호출 HW 종료 → devm_ 해제 의존 미준비 성공 새 드라이버 등록 시 재시도 음수 반환

-EPROBE_DEFER 처리

-EPROBE_DEFER는 probe 함수가 의존하는 리소스(클럭 드라이버, 레귤레이터, GPIO 컨트롤러 등)가 아직 등록되지 않았을 때 반환합니다. 커널은 이를 받으면 해당 드라이버를 deferred_probe_list에 등록하고, 새 드라이버가 등록될 때마다 목록을 재시도합니다.

static int my_uart_probe(struct platform_device *pdev)
{
    struct clk *clk;

    clk = devm_clk_get(&pdev->dev, "uart");
    if (IS_ERR(clk)) {
        /* 클럭 드라이버가 아직 없으면 -EPROBE_DEFER 반환 */
        return dev_err_probe(&pdev->dev, PTR_ERR(clk),
                             "failed to get uart clock\n");
    }
    /* ... */
}
dev_err_probe(): PTR_ERR()-EPROBE_DEFER이면 에러 메시지를 억제하고 dev_dbg()로만 출력합니다. 커널 로그가 불필요한 에러 메시지로 넘치는 것을 방지합니다.

Device Tree 매칭 심화

of_match_data 활용

of_match_table.data 필드를 이용하면 compatible별 variant 데이터를 전달할 수 있습니다.

struct my_uart_variant {
    u32 fifo_size;
    bool has_autobaud;
};

static const struct my_uart_variant variant_v1 = {
    .fifo_size   = 16,
    .has_autobaud = false,
};

static const struct my_uart_variant variant_v2 = {
    .fifo_size   = 64,
    .has_autobaud = true,
};

static const struct of_device_id my_uart_of_match[] = {
    { .compatible = "vendor,my-uart",    .data = &variant_v1 },
    { .compatible = "vendor,my-uart-v2", .data = &variant_v2 },
    { }
};

static int my_uart_probe(struct platform_device *pdev)
{
    const struct my_uart_variant *variant;

    variant = of_device_get_match_data(&pdev->dev);
    if (!variant)
        return -EINVAL;

    dev_dbg(&pdev->dev, "fifo_size=%u\n", variant->fifo_size);
    /* ... */
}

DT 프로퍼티 읽기

struct device_node *np = pdev->dev.of_node;
u32 fifo_size;
bool rs485_en;

/* 정수 프로퍼티 */
of_property_read_u32(np, "fifo-size", &fifo_size);

/* 불리언 프로퍼티 (존재하면 true) */
rs485_en = of_property_read_bool(np, "linux,rs485-enabled-at-boot-time");

/* 문자열 배열 */
const char *mode;
of_property_read_string(np, "mode", &mode);

전원 관리 통합

Platform 드라이버는 dev_pm_ops를 통해 시스템 suspend/resume 및 runtime PM을 구현합니다.

static int my_uart_suspend(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);

    clk_disable_unprepare(priv->clk);
    return 0;
}

static int my_uart_resume(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);

    return clk_prepare_enable(priv->clk);
}

/* Runtime PM: 유휴 시 클럭 자동 차단 */
static int my_uart_runtime_suspend(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);
    clk_disable_unprepare(priv->clk);
    return 0;
}

static int my_uart_runtime_resume(struct device *dev)
{
    struct my_uart_priv *priv = dev_get_drvdata(dev);
    return clk_prepare_enable(priv->clk);
}

static const struct dev_pm_ops my_uart_pm_ops = {
    SYSTEM_SLEEP_PM_OPS(my_uart_suspend, my_uart_resume)
    RUNTIME_PM_OPS(my_uart_runtime_suspend, my_uart_runtime_resume, NULL)
};

probe 마지막에 runtime PM을 활성화합니다.

pm_runtime_set_active(&pdev->dev);
pm_runtime_enable(&pdev->dev);
pm_runtime_set_autosuspend_delay(&pdev->dev, 500); /* 500ms 후 자동 suspend */
pm_runtime_use_autosuspend(&pdev->dev);

전체 드라이버 예제 (최소 구현)

아래는 MMIO 레지스터, IRQ, 클럭 하나씩을 사용하는 최소 platform 드라이버의 전체 구조입니다. probe에서는 devm_ 함수만 사용하여 에러 경로를 단순화했고, remove에서는 하드웨어 비활성화만 처리합니다. 실제 드라이버에서는 여기에 특정 서브시스템(serial, iio, gpio 등) 등록 코드가 추가됩니다.

#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/of.h>

struct my_dev_priv {
    void __iomem  *base;
    struct clk    *clk;
    struct device *dev;
    int            irq;
};

static irqreturn_t my_dev_isr(int irq, void *data)
{
    struct my_dev_priv *priv = data;
    u32 status = readl(priv->base + 0x00);

    if (!(status & BIT(0)))
        return IRQ_NONE;

    /* 인터럽트 처리 */
    writel(BIT(0), priv->base + 0x04);  /* clear */
    return IRQ_HANDLED;
}

static int my_dev_probe(struct platform_device *pdev)
{
    struct my_dev_priv *priv;
    int ret;

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

    priv->dev = &pdev->dev;

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

    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    priv->clk = devm_clk_get(&pdev->dev, NULL);
    if (IS_ERR(priv->clk))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->clk),
                             "failed to get clock\n");

    ret = clk_prepare_enable(priv->clk);
    if (ret)
        return ret;

    ret = devm_add_action_or_reset(&pdev->dev,
            (void (*)(void *))clk_disable_unprepare, priv->clk);
    if (ret)
        return ret;

    ret = devm_request_irq(&pdev->dev, priv->irq, my_dev_isr,
                           0, dev_name(&pdev->dev), priv);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, priv);

    /* 하드웨어 초기화 */
    writel(0x1, priv->base + 0x10);  /* enable */

    dev_info(&pdev->dev, "probed at %pR\n",
             platform_get_resource(pdev, IORESOURCE_MEM, 0));
    return 0;
}

static void my_dev_remove(struct platform_device *pdev)
{
    struct my_dev_priv *priv = platform_get_drvdata(pdev);

    writel(0x0, priv->base + 0x10);  /* disable */
}

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

static struct platform_driver my_dev_driver = {
    .probe  = my_dev_probe,
    .remove = my_dev_remove,
    .driver = {
        .name           = "my-device",
        .of_match_table = my_dev_of_match,
    },
};
module_platform_driver(my_dev_driver);

MODULE_DESCRIPTION("My Device Platform Driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Developer");

sysfs 커스텀 속성 추가

Platform 드라이버는 struct device_attribute를 통해 sysfs에 런타임 제어 인터페이스를 노출할 수 있습니다. 디바이스가 probe된 후 /sys/bus/platform/devices/<name>/ 아래에 파일로 나타납니다.

#include <linux/sysfs.h>

/* sysfs 읽기 핸들러 */
static ssize_t status_show(struct device *dev,
                            struct device_attribute *attr,
                            char *buf)
{
    struct my_dev_priv *priv = dev_get_drvdata(dev);
    u32 val = readl(priv->base + 0x00);

    return sysfs_emit(buf, "0x%08x\n", val);
}

/* sysfs 쓰기 핸들러 */
static ssize_t enable_store(struct device *dev,
                             struct device_attribute *attr,
                             const char *buf, size_t count)
{
    struct my_dev_priv *priv = dev_get_drvdata(dev);
    bool enable;
    int ret;

    ret = kstrtobool(buf, &enable);
    if (ret)
        return ret;

    writel(enable ? 1 : 0, priv->base + 0x10);
    return count;
}

/* DEVICE_ATTR_RO: 읽기 전용 (show만), DEVICE_ATTR_WO: 쓰기 전용 */
DEVICE_ATTR_RO(status);             /* /sys/.../status */
DEVICE_ATTR_RW(enable);             /* /sys/.../enable  (show+store) */

static struct attribute *my_dev_attrs[] = {
    &dev_attr_status.attr,
    &dev_attr_enable.attr,
    NULL,
};
ATTRIBUTE_GROUPS(my_dev);           /* my_dev_groups[] 자동 생성 */

static struct platform_driver my_dev_driver = {
    .probe  = my_dev_probe,
    .remove = my_dev_remove,
    .driver = {
        .name           = "my-device",
        .of_match_table = my_dev_of_match,
        .dev_groups     = my_dev_groups,  /* probe 완료 후 자동 생성 */
    },
};
sysfs_emit(): 리눅스 5.10부터 sprintf(buf, ...) 대신 sysfs_emit(buf, ...)을 사용합니다. PAGE_SIZE 경계를 자동으로 확인하여 버퍼 오버플로를 방지합니다.

비동기 probe (async_probe)

기본적으로 probe는 driver_register()를 호출한 커널 스레드에서 동기적으로 실행됩니다. probe가 오래 걸리는 드라이버(예: 펌웨어 로드, 느린 I2C 버스)가 많으면 부팅 시간이 늘어납니다. driver.probe_typePROBE_PREFER_ASYNCHRONOUS로 설정하면 workqueue에서 비동기로 실행됩니다.

static struct platform_driver my_slow_driver = {
    .probe  = my_slow_probe,
    .remove = my_slow_remove,
    .driver = {
        .name           = "my-slow-device",
        .of_match_table = my_slow_of_match,
        .probe_type     = PROBE_PREFER_ASYNCHRONOUS,
    },
};
비동기 probe 주의사항: probe가 완료되기 전에 다른 드라이버가 이 디바이스에 의존할 수 있으므로, PROBE_PREFER_ASYNCHRONOUS는 의존성이 없는 리프(leaf) 드라이버에만 사용하세요. 의존 드라이버가 -EPROBE_DEFER를 반환하면 정상적으로 재시도됩니다.

실제 커널 드라이버 사례 분석

리눅스 커널 소스에서 Platform 드라이버의 전형적인 패턴을 확인할 수 있습니다.

드라이버소스 파일주요 패턴
ARM PL011 UARTdrivers/tty/serial/amba-pl011.camba_driver (platform_driver 유사), DT 다중 compatible, AMBA 버스
BCM2835 GPIOdrivers/gpio/gpio-bcm2835.cirq_domain, gpiochip, platform_get_irq_byname
Raspberry Pi 클럭drivers/clk/bcm/clk-bcm2835.cof_clk_add_hw_provider, 복수 clk 등록
IMX6 UARTdrivers/tty/serial/imx.cDMA 연동, of_match_data variant, runtime PM
Rockchip SPIdrivers/spi/spi-rockchip.cSPI master 등록, DMA + PIO 이중 경로
Xilinx AXI DMAdrivers/dma/xilinx/xilinx_dma.cplatform_get_resource 복수, irq 복수, dmaengine 등록

AMBA 버스와 Platform 버스의 차이

ARM SoC의 AMBA 버스(PrimeCell 주변장치)는 struct amba_driveramba_bustype을 사용하지만, 내부 구조는 Platform 드라이버와 거의 동일합니다. PrimeCell ID(AMBA ID)로 자동 매칭되고 amba_driver_register()로 등록합니다.

static struct amba_id pl011_ids[] = {
    {
        .id   = 0x00041011,  /* ARM PL011 */
        .mask = 0x000fffff,
    },
    { 0, 0 },
};
MODULE_DEVICE_TABLE(amba, pl011_ids);

static struct amba_driver pl011_driver = {
    .drv = {
        .name           = "uart-pl011",
        .pm             = &pl011_dev_pm_ops,
        .of_match_table = of_match_ptr(pl011_of_match),
    },
    .id_table = pl011_ids,
    .probe    = pl011_probe,
    .remove   = pl011_remove,
};

디버깅

Platform 드라이버 문제는 대부분 세 가지에서 발생합니다. 드라이버가 아예 로드되지 않으면 MODULE_DEVICE_TABLE() 누락이나 compatible 오타를 확인합니다. probe가 실행되지 않으면 /sys/kernel/debug/devices_deferred에서 deferred 상태를 확인하세요. probe가 실행됐지만 오동작하면 dynamic_debugdev_dbg() 출력을 활성화하여 리소스 획득 순서를 추적합니다.

sysfs를 통한 상태 확인

# 등록된 platform 디바이스 목록
ls /sys/bus/platform/devices/

# 특정 디바이스의 드라이버 바인딩 확인
ls -la /sys/bus/platform/devices/10000000.serial/driver

# 드라이버가 바인딩된 디바이스 목록
ls /sys/bus/platform/drivers/my-uart/

# deferred probe 상태 확인
cat /sys/kernel/debug/devices_deferred

# 특정 디바이스 강제 바인딩/언바인딩
echo "10000000.serial" > /sys/bus/platform/drivers/my-uart/bind
echo "10000000.serial" > /sys/bus/platform/drivers/my-uart/unbind

probe 디버깅

# 동적 디버그 활성화 (드라이버 이름 기준)
echo "module my_uart +p" > /sys/kernel/debug/dynamic_debug/control

# initcall 디버그 (부팅 커맨드라인)
# kernel boot param: initcall_debug

# deferred probe 강제 재시도
echo 1 > /sys/kernel/debug/devices_deferred

흔한 실수

주의사항:
  • of_iomap() 사용 금지 (신규 코드): request_mem_region()을 호출하지 않아 충돌 감지가 불가능합니다. devm_platform_ioremap_resource()를 사용하세요.
  • -EPROBE_DEFER 처리 누락: platform_get_irq(), devm_clk_get() 등의 반환값을 반드시 확인하고, -EPROBE_DEFER를 전파해야 합니다.
  • probe 에러 경로에서 devm_ 미사용: 수동 할당 후 에러 경로에서 해제를 빠뜨리면 메모리 누수가 발생합니다.
  • platform_set_drvdata() 호출 위치: 모든 초기화 성공 후 맨 마지막에 호출해야 remove()에서 NULL 포인터 접근을 방지합니다.
  • MODULE_DEVICE_TABLE() 누락: 없으면 udev가 자동 모듈 로드를 못 합니다.