platform 기반 장치 드라이버: LED 제어 구현

이전 챕터에서 다룬 장치 드라이버는 단순한 GPIO 입출력만을 처리했지만, I2C, SPI, LCD와 같은 복잡한 외부 장치의 경우 더 체계적인 설계가 필요합니다. 이를 위해 리눅스 커널은 드라이버의 분리 및 계층화를 도입하여 재사용성과 유지보수성을 높였습니다. 이 구조에서 탄생한 것이 바로 platform 장치 드라이버이며, 이후 대부분의 임베디드 시스템 개발에서 주로 사용하는 핵심 개념입니다.

드라이버의 분리와 계층화

1. 드라이버 분리 (Separation)

리눅스는 대규모이고 복잡한 시스템이므로 코드 중복을 최소화해야 합니다. 특히 장치 드라이버는 커널 전체 코드량의 대부분을 차지하므로, 각 플랫폼마다 동일한 장치(예: MPU6050)에 대해 별도의 드라이버를 작성하는 것은 비효율적입니다. 따라서 호스트 드라이버(I2C 컨트롤러 등)와 장치 드라이버(MPU6050 등)를 명확히 분리하는 방식이 도입되었습니다.

각 플랫폼의 호스트 드라이버는 고유하며, 반면 장치 드라이버는 하나만 존재하면 됩니다. 예를 들어, 모든 아키텍처에서 동일한 인터페이스를 사용하는 MPU6050은 하나의 드라이버로 관리할 수 있습니다. 이는 장치 정보(예: 연결된 I2C 버스, 속도 등)를 장치 드라이버 자체에서 떼어내고, 장치 트리(Device Tree) 또는 기타 메타데이터를 통해 동적으로 획득하도록 설계됩니다.

이 구조는 bus-driver-device 모델이라고 불리며, 커널 내에서 총선(또는 가상 총선), 드라이버, 장치 간의 매칭을 담당합니다. 여기서 총선은 드라이버와 장치를 연결하는 '중개자' 역할을 하며, 적절한 매칭이 이루어지면 probe() 함수가 실행됩니다.

2. 드라이버 계층화 (Layering)

드라이버는 계층 구조로 설계되어 각 계층이 특정 책임을 맡습니다. 예를 들어, 입력 서브시스템(input)은 아래에서부터 장치 원시 데이터 수집 → 커널 핵심 처리 → 사용자 공간으로의 전달까지 단계적으로 진행됩니다. 개발자는 input_report_key() 등의 함수를 통해 입력 이벤트를 보고만 하면 되며, 그 이후 처리는 상위 계층에서 담당합니다. 이러한 계층화는 개발자의 부담을 크게 줄여줍니다.

platform 드라이버 모델 소개

특정 총선 없이도 장치 드라이버를 표준화하고 싶은 경우, 리눅스는 가상 총선platform_bus_type을 제공합니다. 이는 platform_deviceplatform_driver의 매칭을 가능하게 하며, 널리 사용되는 구조입니다.

1. platform 총선

struct bus_type 구조체는 총선의 기본 인터페이스를 정의하며, platform_bus_type은 이 구조체의 인스턴스입니다. 매칭은 platform_match() 함수에서 수행되며, 다음과 같은 순서로 일치 여부를 판단합니다:

  1. OF 매칭: 장치 트리의 compatible 속성과 드라이버의 of_match_table 비교
  2. ACPI 매칭: ACPI 기반 시스템에서의 매칭
  3. ID 테이블 매칭: platform_driver.id_table을 통한 일치 확인
  4. 이름 매칭: 드라이버 이름과 장치 이름 직접 비교

대부분의 경우, 마지막 방법인 이름 매칭이 가장 일반적으로 사용됩니다.

2. platform 드라이버 구조

struct platform_driver는 실제 드라이버 로직을 포함하며, probe(), remove() 함수를 정의합니다. 주요 구성 요소는 다음과 같습니다:

  • probe(): 장치와 매칭되면 호출되며, 장치 초기화 및 자원 할당 작업 수행
  • driver: device_driver 구조체로, 공통 인터페이스 제공
  • id_table: 지원하는 장치 유형 목록

드라이버 등록은 platform_driver_register()로 수행되며, 해제는 platform_driver_unregister()로 처리합니다.

LED 드라이버 구현 예제

다음은 플랫폼 드라이버로 구현한 간단한 LED 제어 예제입니다. 장치 트리에서 compatible = "alientek,led"로 정의된 장치와 매칭되며, 해당 장치의 led-gpio 속성에서 GPIO 번호를 가져옵니다.

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/fs.h>

#define LEDDEV_NAME "dtsplatled"
#define LEDON 0
#define LEDOFF 1

struct led_dev {
    dev_t dev_id;
    struct cdev cdev;
    struct class *cls;
    struct device *dev;
    int gpio_pin;
};

static struct led_dev led_info;

static void led_control(int state)
{
    if (state == LEDON)
        gpio_set_value(led_info.gpio_pin, 0);
    else
        gpio_set_value(led_info.gpio_pin, 1);
}

static int led_init_gpio(struct device_node *np)
{
    int ret;

    led_info.gpio_pin = of_get_named_gpio(np, "led-gpio", 0);
    if (!gpio_is_valid(led_info.gpio_pin)) {
        pr_err("Failed to get GPIO\n");
        return -EINVAL;
    }

    ret = gpio_request(led_info.gpio_pin, "led_gpio");
    if (ret) {
        pr_err("GPIO request failed\n");
        return ret;
    }

    gpio_direction_output(led_info.gpio_pin, 1);
    return 0;
}

static int led_open(struct inode *inode, struct file *filp)
{
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *off)
{
    unsigned char data;
    if (copy_from_user(&data, buf, 1))
        return -EFAULT;

    led_control(data ? LEDON : LEDOFF);
    return 1;
}

static const struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
};

static int led_probe(struct platform_device *pdev)
{
    int ret;

    pr_info("LED driver matched with device\n");

    ret = led_init_gpio(pdev->dev.of_node);
    if (ret)
        return ret;

    ret = alloc_chrdev_region(&led_info.dev_id, 0, 1, LEDDEV_NAME);
    if (ret) {
        pr_err("Failed to allocate region\n");
        goto err_free_gpio;
    }

    cdev_init(&led_info.cdev, &led_fops);
    ret = cdev_add(&led_info.cdev, led_info.dev_id, 1);
    if (ret)
        goto err_unreg;

    led_info.cls = class_create(THIS_MODULE, LEDDEV_NAME);
    if (IS_ERR(led_info.cls))
        goto err_del_cdev;

    led_info.dev = device_create(led_info.cls, NULL, led_info.dev_id, NULL, LEDDEV_NAME);
    if (IS_ERR(led_info.dev))
        goto err_destroy_cls;

    return 0;

err_destroy_cls:
    class_destroy(led_info.cls);
err_del_cdev:
    cdev_del(&led_info.cdev);
err_unreg:
    unregister_chrdev_region(led_info.dev_id, 1);
err_free_gpio:
    gpio_free(led_info.gpio_pin);
    return -ENODEV;
}

static int led_remove(struct platform_device *pdev)
{
    gpio_set_value(led_info.gpio_pin, 1);
    gpio_free(led_info.gpio_pin);
    cdev_del(&led_info.cdev);
    unregister_chrdev_region(led_info.dev_id, 1);
    device_destroy(led_info.cls, led_info.dev_id);
    class_destroy(led_info.cls);
    return 0;
}

static const struct of_device_id led_of_match[] = {
    { .compatible = "alientek,led" },
    { /* Sentinel */ }
};

MODULE_DEVICE_TABLE(of, led_of_match);

static struct platform_driver led_platform_driver = {
    .driver = {
        .name = "stm32mp1-led",
        .of_match_table = led_of_match,
    },
    .probe = led_probe,
    .remove = led_remove,
};

static int __init led_init(void)
{
    return platform_driver_register(&led_platform_driver);
}

static void __exit led_exit(void)
{
    platform_driver_unregister(&led_platform_driver);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");

장치 트리 기반 구현

장치 트리는 platform_device를 직접 정의하지 않아도 됩니다. 대신, compatible 속성과 pinctrl, led-gpio 등을 통해 장치 정보를 설명합니다.

gpioled {
    compatible = "alientek,led";
    pinctrl-names = "default";
    status = "okay";
    pinctrl-0 = <&led_pins_a>;
    led-gpio = <&gpioi 0 GPIO_ACTIVE_LOW>;
};

이렇게 설정하면, 커널은 장치 트리 정보를 읽어들여 platform_match()를 통해 드라이버와 매칭하고, probe()를 실행합니다.

또한, 핀 재사용GPIO 점유 상태를 반드시 확인해야 합니다. 예를 들어, 한 핀은 동시에 여러 기능으로 사용될 수 있지만, 실시간에는 하나의 기능만 활성화됩니다. 리눅스에서는 각 핀이 하나의 목적만 갖도록 설계해야 하며, 다른 장치가 이미 사용 중인지 반드시 확인해야 합니다.

태그: platform_driver device_tree GPIO of_match_table cdev

6월 4일 01:10에 게시됨