디바이스 드라이버 (Device Drivers)

Linux 커널 디바이스 드라이버: character/block/network 디바이스, platform driver, DMA 종합 가이드.

관련 표준: Device Tree Specification (하드웨어 기술 형식), PCIe 6.0 (디바이스 인터커넥트), ACPI 6.5 (디바이스 열거/전원) — 커널 디바이스 드라이버 프레임워크가 참조하는 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.

디바이스 드라이버 개요

디바이스 드라이버는 커널과 하드웨어 사이의 인터페이스입니다. Linux에서는 세 가지 주요 디바이스 유형으로 분류합니다:

유형인터페이스예시
Character Device바이트 스트림, 순차 접근시리얼, 터미널, /dev/null
Block Device고정 크기 블록, 랜덤 접근디스크, SSD, USB 스토리지
Network Device패킷 기반, 소켓 API이더넷, WiFi

Character Device 드라이버

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>

static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;

static int my_open(struct inode *inode, struct file *file)
{
    pr_info("device opened\n");
    return 0;
}

static ssize_t my_read(struct file *file,
    char __user *buf, size_t len, loff_t *off)
{
    char msg[] = "Hello from driver\n";
    if (*off >= sizeof(msg))
        return 0;
    if (copy_to_user(buf, msg + *off, min(len, sizeof(msg) - *off)))
        return -EFAULT;
    *off += len;
    return len;
}

static const struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open  = my_open,
    .read  = my_read,
};

static int __init my_init(void)
{
    alloc_chrdev_region(&dev_num, 0, 1, "mydev");
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, dev_num, 1);
    my_class = class_create("mydev_class");
    device_create(my_class, NULL, dev_num, NULL, "mydev");
    return 0;
}

Platform Driver

Platform Device 개념

PCI, USB 등 자동 열거(enumeration)가 가능한 버스와 달리, SoC(System-on-Chip) 내장 컨트롤러(UART, SPI, I2C, GPIO, 타이머 등)는 버스 프로토콜로 디바이스를 발견할 수 없습니다. 커널은 이러한 디바이스를 위해 Platform Bus라는 가상 버스를 제공합니다. Platform Bus는 실제 하드웨어 버스가 아니라, device-driver 매칭 인프라를 재사용하기 위한 소프트웨어 추상화입니다.

Platform Device의 하드웨어 정보(레지스터 주소, IRQ 번호 등)는 세 가지 방식으로 커널에 전달됩니다:

공급 방식설명대표 환경
Device TreeDTS/DTB 파일에 하드웨어 기술, 부트로더가 전달ARM, RISC-V, PowerPC
ACPIACPI 테이블(DSDT/SSDT)에 디바이스 기술x86 서버/데스크톱, ARM 서버
정적 등록 (레거시)보드 파일에서 platform_device_register() 직접 호출구형 ARM 보드 (비권장)

Device Tree의 상세 문법, 프로퍼티, OF API 등은 Device Tree 심화 섹션을 참고하세요.

struct platform_device 분석

struct platform_device는 platform bus에 연결된 디바이스 하나를 나타냅니다. include/linux/platform_device.h에 정의되어 있습니다.

struct platform_device {
    const char      *name;          /* 디바이스 이름 (name 매칭에 사용) */
    int             id;             /* 인스턴스 번호 (-1이면 단일) */
    struct device   dev;            /* 임베디드 generic device */
    struct resource *resource;       /* I/O, 메모리, IRQ 리소스 배열 */
    u32             num_resources;  /* resource 배열 크기 */
    const struct platform_device_id *id_entry; /* 매칭된 id_table 항목 */
    const char      *driver_override; /* 강제 드라이버 바인딩 */
};

핵심 필드별 용도:

필드용도
dev.platform_data보드 레벨 데이터 전달 (레거시). void * 포인터로 드라이버별 구조체 전달
dev.of_nodeDevice Tree에서 생성된 경우, 해당 DT 노드 (struct device_node *)
dev.fwnode통합 firmware 노드. DT와 ACPI를 추상화하는 struct fwnode_handle *
resourceMMIO 주소 범위, IRQ 번호, DMA 채널 등 하드웨어 리소스
driver_overridesysfs에서 수동으로 특정 드라이버를 강제 바인딩할 때 사용

struct resource는 디바이스의 하드웨어 리소스를 기술합니다:

struct resource {
    resource_size_t start;  /* 리소스 시작 주소/번호 */
    resource_size_t end;    /* 리소스 끝 주소/번호 (inclusive) */
    const char      *name;
    unsigned long   flags;  /* 리소스 유형 */
};

/* 주요 리소스 유형 플래그 */
IORESOURCE_MEM   /* MMIO 메모리 영역 */
IORESOURCE_IRQ   /* 인터럽트 번호 */
IORESOURCE_DMA   /* DMA 채널 */
IORESOURCE_IO    /* I/O 포트 (x86) */

struct platform_driver 분석

struct platform_driver는 platform device를 다루는 드라이버를 나타냅니다:

struct platform_driver {
    int  (*probe)(struct platform_device *);    /* 디바이스 발견 시 호출 */
    void (*remove)(struct platform_device *);   /* 디바이스 제거 시 호출 */
    void (*shutdown)(struct platform_device *); /* reboot/poweroff 시 호출 */
    int  (*suspend)(struct platform_device *, pm_message_t); /* 레거시 PM */
    int  (*resume)(struct platform_device *);   /* 레거시 PM */
    struct device_driver driver;   /* 임베디드 generic driver */
    const struct platform_device_id *id_table; /* name+data 기반 매칭 */
    bool prevent_deferred_probe;
};

driver 필드의 주요 하위 필드:

필드용도
driver.name드라이버 이름. sysfs 경로와 최후순위 매칭에 사용
driver.of_match_tableDevice Tree compatible 매칭 테이블
driver.acpi_match_tableACPI HID/CID 매칭 테이블
driver.pmstruct dev_pm_ops * — 최신 PM 콜백 (전원 관리 참고)
driver.ownerTHIS_MODULE (매크로가 자동 설정)

id_table은 이름 기반 매칭에 사용되며, 드라이버별 데이터를 전달할 수 있습니다:

struct platform_device_id {
    char name[PLATFORM_NAME_SIZE];
    kernel_ulong_t driver_data;  /* 드라이버별 private 데이터 */
};

/* 사용 예: 여러 변형 디바이스를 하나의 드라이버로 지원 */
enum { CHIP_V1, CHIP_V2, CHIP_V3 };

static const struct platform_device_id my_ids[] = {
    { "my-chip-v1", CHIP_V1 },
    { "my-chip-v2", CHIP_V2 },
    { "my-chip-v3", CHIP_V3 },
    { }  /* sentinel */
};

매칭 메커니즘

Platform Bus의 platform_match() 함수는 다음 4단계 우선순위로 device-driver 매칭을 수행합니다:

우선순위매칭 방식비교 대상
1 (최고)driver_overridesysfs에서 수동 설정된 드라이버 이름과 driver.name 비교
2of_match_tableDT 노드의 compatible과 드라이버의 of_device_id 배열 비교
3acpi_match_tableACPI 디바이스 HID/CID와 acpi_device_id 배열 비교
4id_tableplatform_device.nameplatform_device_id.name 비교
5 (최저)driver.nameplatform_device.namedriver.name 문자열 비교

커널 내부의 platform_match() 핵심 로직:

/* drivers/base/platform.c */
static int platform_match(struct device *dev, struct device_driver *drv)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct platform_driver *pdrv = to_platform_driver(drv);

    /* 1단계: driver_override (sysfs 강제 바인딩) */
    if (pdev->driver_override)
        return !strcmp(pdev->driver_override, drv->name);

    /* 2단계: Device Tree compatible 매칭 */
    if (of_driver_match_device(dev, drv))
        return 1;

    /* 3단계: ACPI 매칭 */
    if (acpi_driver_match_device(dev, drv))
        return 1;

    /* 4단계: id_table 매칭 */
    if (pdrv->id_table)
        return platform_match_id(pdrv->id_table, pdev) != NULL;

    /* 5단계: driver.name 문자열 매칭 (fallback) */
    return (strcmp(pdev->name, drv->name) == 0);
}

각 매칭 방식별 드라이버 정의 예제:

/* Device Tree 매칭 (가장 일반적) */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-ctrl-v2", .data = &chip_v2_data },
    { .compatible = "vendor,my-ctrl-v1", .data = &chip_v1_data },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

/* ACPI 매칭 */
static const struct acpi_device_id my_acpi_ids[] = {
    { "VNDR0001", (kernel_ulong_t)&chip_v1_data },
    { "VNDR0002", (kernel_ulong_t)&chip_v2_data },
    { }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name           = "my-ctrl",
        .of_match_table = my_of_ids,
        .acpi_match_table = ACPI_PTR(my_acpi_ids),
    },
};

Platform Device 등록

Platform Device가 커널에 등록되는 세 가지 경로:

1. 정적 등록 (레거시 보드 파일)

DT 이전의 ARM 보드에서 사용하던 방식입니다. 현재는 비권장이지만 레거시 코드에서 여전히 볼 수 있습니다:

/* 레거시 보드 파일 (arch/arm/mach-xxx/) */
static struct resource my_resources[] = {
    [0] = {
        .start = 0x40000000,
        .end   = 0x40000FFF,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = 42,   /* IRQ 번호 */
        .end   = 42,
        .flags = IORESOURCE_IRQ,
    },
};

static struct platform_device my_pdev = {
    .name          = "my-ctrl",
    .id            = -1,
    .resource      = my_resources,
    .num_resources = ARRAY_SIZE(my_resources),
};

/* 보드 초기화 함수에서 */
platform_device_register(&my_pdev);

편의 함수로 간결하게 등록할 수도 있습니다:

/* 간편 등록 함수들 */
platform_device_register_simple("my-ctrl", -1, my_resources, 2);
platform_device_register_data(NULL, "my-ctrl", -1, &pdata, sizeof(pdata));
platform_device_register_resndata(NULL, "my-ctrl", -1,
    my_resources, 2, &pdata, sizeof(pdata));

2. Device Tree 기반 자동 생성

현대 임베디드 시스템의 표준 방식입니다. 부팅 시 커널이 DTB를 파싱하여 of_platform_populate()로 platform_device를 자동 생성합니다:

/* 커널 부팅 시 자동 실행 (drivers/of/platform.c) */
of_platform_default_populate(NULL, NULL, NULL);
    → DT의 최상위 "simple-bus" / "simple-mfd" 등의 자식 노드를
      순회하며 of_platform_device_create() 호출
    → struct platform_device 자동 생성 + 등록

3. ACPI 기반 자동 생성

x86 및 ARM 서버에서 ACPI 테이블의 디바이스 정보로 platform_device를 자동 생성합니다:

/* ACPI 열거 — DSDT/SSDT의 Device() 오브젝트가 platform_device로 변환
 *
 * ACPI 네임스페이스 예:
 *   Device (MYCT) {
 *       Name (_HID, "VNDR0001")
 *       Name (_CRS, ResourceTemplate() {
 *           Memory32Fixed(ReadWrite, 0x40000000, 0x1000)
 *           Interrupt(ResourceConsumer, ...) { 42 }
 *       })
 *   }
 *
 * → acpi_default_enumeration()이 platform_device로 변환
 */

probe/remove 생명주기

probe()는 device-driver 매칭이 성공했을 때 커널이 호출하는 진입점입니다. remove()는 디바이스 언바인드 또는 드라이버 언로드 시 역순으로 정리합니다.

probe 호출 조건:

probe 전형적 순서:

static int my_probe(struct platform_device *pdev)
{
    struct my_priv *priv;
    void __iomem *base;
    int irq, ret;

    /* 1. private 데이터 할당 */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

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

    /* 3. 리소스 획득 — IRQ */
    irq = platform_get_irq(pdev, 0);
    if (irq < 0)
        return irq;  /* 에러 코드 전파 */

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

    /* 4. 클록, 리셋, 레귤레이터 등 하드웨어 리소스 */
    priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
    if (IS_ERR(priv->clk))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->clk),
                              "failed to get clock\n");

    /* 5. 하드웨어 초기화 */
    writel(CTRL_ENABLE, base + REG_CTRL);

    /* 6. 서브시스템 등록 (misc device, input, net 등) */
    ret = misc_register(&priv->miscdev);
    if (ret)
        return ret;

    /* 7. private 데이터를 디바이스에 연결 */
    platform_set_drvdata(pdev, priv);

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

dev_err_probe()-EPROBE_DEFER를 자동으로 감지하여 dev_dbg 수준으로 로깅하고, 그 외 에러는 dev_err로 출력합니다. probe deferral 메시지가 dmesg를 오염시키는 것을 방지하므로 적극 활용하세요.

remove — 역순 정리:

static void my_remove(struct platform_device *pdev)
{
    struct my_priv *priv = platform_get_drvdata(pdev);

    /* probe의 역순으로 정리 */
    misc_deregister(&priv->miscdev);

    /* devm_ 계열 리소스는 자동 해제되므로 별도 처리 불필요 */
    /* devm_kzalloc, devm_request_irq, devm_ioremap 등 */
}

devm_* (managed) API를 사용하면 remove에서 명시적 해제가 불필요합니다. 상세한 devm API 목록은 Managed Device Resources (devm) 섹션을 참고하세요.

shutdown 콜백:

shutdown()은 시스템 reboot 또는 poweroff 시 호출됩니다. DMA 전송 중단, 인터럽트 비활성화 등 하드웨어를 안전한 상태로 전환합니다:

static void my_shutdown(struct platform_device *pdev)
{
    struct my_priv *priv = platform_get_drvdata(pdev);

    /* 하드웨어를 안전한 상태로 전환 */
    writel(0, priv->base + REG_CTRL);      /* 컨트롤러 비활성화 */
    disable_irq(priv->irq);                /* 인터럽트 차단 */
}

리소스 접근 API

Platform 드라이버에서 디바이스의 하드웨어 리소스에 접근하는 API들:

API용도
platform_get_resource(pdev, type, index)타입별 N번째 리소스 구조체 반환
platform_get_resource_byname(pdev, type, name)이름으로 리소스 검색
platform_get_irq(pdev, index)N번째 IRQ 번호 반환 (DT/ACPI 추상화)
platform_get_irq_byname(pdev, name)이름으로 IRQ 검색
devm_platform_ioremap_resource(pdev, index)리소스 획득 + ioremap 한 번에 수행
devm_platform_ioremap_resource_byname(pdev, name)이름으로 리소스 획득 + ioremap
platform_get_drvdata(pdev)드라이버 private 데이터 반환
platform_set_drvdata(pdev, data)드라이버 private 데이터 저장

사용 예제:

/* 인덱스 기반 접근 */
struct resource *mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
int irq = platform_get_irq(pdev, 0);

/* 이름 기반 접근 (DT에서 reg-names, interrupt-names 지정 시) */
struct resource *cfg = platform_get_resource_byname(pdev, IORESOURCE_MEM, "config");
int tx_irq = platform_get_irq_byname(pdev, "tx");
int rx_irq = platform_get_irq_byname(pdev, "rx");

/* 리소스 획득 + ioremap 통합 (권장) */
void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(base))
    return PTR_ERR(base);

/* private 데이터 저장/조회 */
platform_set_drvdata(pdev, priv);          /* probe에서 */
struct my_priv *p = platform_get_drvdata(pdev);  /* remove 등에서 */

platform_get_resource()devm_ioremap_resource()를 별도로 호출하는 것보다 devm_platform_ioremap_resource() 하나로 통합하는 것이 권장됩니다. NULL 체크와 영역 요청, 매핑을 한 번에 처리하여 코드가 간결해지고 에러 처리 누락을 방지합니다.

편의 매크로

module_platform_driver()는 모듈 init/exit 보일러플레이트를 자동 생성합니다:

/* include/linux/platform_device.h */
#define module_platform_driver(__platform_driver) \
    module_driver(__platform_driver, platform_driver_register, \
                  platform_driver_unregister)

/* 위 매크로는 아래와 동일한 코드를 생성 */
static int __init my_driver_init(void)
{
    return platform_driver_register(&my_driver);
}
module_init(my_driver_init);

static void __exit my_driver_exit(void)
{
    platform_driver_unregister(&my_driver);
}
module_exit(my_driver_exit);

관련 편의 매크로들:

매크로용도
module_platform_driver(drv)모듈용. init/exit에서 register/unregister 자동 생성
builtin_platform_driver(drv)빌트인 전용. device_initcall로 자동 등록
builtin_platform_driver_probe(drv, probe)빌트인 전용. probe를 __init 섹션에 배치하여 메모리 절약
MODULE_DEVICE_TABLE(of, ids)모듈 자동 로딩용 alias 생성. depmod가 인식
MODULE_DEVICE_TABLE(acpi, ids)ACPI 기반 모듈 자동 로딩용 alias 생성

MODULE_DEVICE_TABLE()modules.alias 파일에 항목을 추가하여, 디바이스 발견 시 udev가 해당 모듈을 자동 로드할 수 있게 합니다:

/* 드라이버 코드 */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-ctrl" },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

/* → /lib/modules/.../modules.alias 에 추가됨:
 *   alias of:N*T*Cvendor,my-ctrlC* my_driver
 * → DT에 "vendor,my-ctrl" 노드가 있으면 udev가 my_driver.ko 자동 로드
 */

sysfs 인터페이스

Platform Bus는 sysfs를 통해 사용자 공간에 노출됩니다:

# Platform Bus 디렉토리 구조
/sys/bus/platform/
├── devices/              # 등록된 모든 platform device 심볼릭 링크
│   ├── 40000000.uart → ../../../devices/platform/40000000.uart
│   ├── 40010000.spi  → ../../../devices/platform/40010000.spi
│   └── ...
└── drivers/              # 등록된 모든 platform driver
    ├── my-ctrl/
    │   ├── bind          # 디바이스 이름 기록 시 수동 바인딩
    │   ├── unbind        # 디바이스 이름 기록 시 언바인딩
    │   ├── module → ../../../../module/my_ctrl
    │   └── 40000000.uart → ../../../../devices/platform/40000000.uart
    └── ...

수동 드라이버 바인딩/언바인딩:

# 디바이스를 현재 드라이버에서 분리
echo "40000000.uart" > /sys/bus/platform/drivers/my-ctrl/unbind

# driver_override로 다른 드라이버 강제 지정
echo "other-driver" > /sys/devices/platform/40000000.uart/driver_override

# 새 드라이버에 수동 바인딩
echo "40000000.uart" > /sys/bus/platform/drivers/other-driver/bind

# driver_override 해제 (빈 문자열 기록)
echo "" > /sys/devices/platform/40000000.uart/driver_override
💡

driver_override는 VFIO 등에서 디바이스를 사용자 공간 드라이버에 바인딩할 때 유용합니다. platform_match()의 최우선 매칭 경로이므로 DT/ACPI 매칭보다 우선합니다.

완전한 Platform Driver 예제:

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

struct my_priv {
    void __iomem   *base;
    struct clk     *clk;
    int            irq;
};

static irqreturn_t my_irq_handler(int irq, void *data)
{
    struct my_priv *priv = data;
    u32 status = readl(priv->base + REG_STATUS);

    if (!(status & IRQ_PENDING))
        return IRQ_NONE;

    writel(status, priv->base + REG_STATUS);  /* ACK */
    return IRQ_HANDLED;
}

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_priv *priv;

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

    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_enabled(dev, NULL);
    if (IS_ERR(priv->clk))
        return dev_err_probe(dev, PTR_ERR(priv->clk), "clk failed\n");

    return devm_request_irq(dev, priv->irq, my_irq_handler,
                            0, dev_name(dev), priv);
}

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

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .driver = {
        .name           = "my-ctrl",
        .of_match_table = my_of_ids,
    },
};
module_platform_driver(my_driver);

MODULE_DESCRIPTION("My Platform Controller Driver");
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Author Name");

DMA (Direct Memory Access)

심화 학습: DMA 매핑 API 전체(Coherent/Streaming/SG/Pool), IOMMU, SWIOTLB, CMA, DMA-BUF, P2P DMA, 캐시 일관성, 보안, 디버깅에 대한 종합 가이드는 DMA 심화 페이지를 참조하십시오.
#include <linux/dma-mapping.h>

/* Coherent DMA (CPU 캐시와 일관성 보장) */
void *buf = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
dma_free_coherent(dev, size, buf, dma_handle);

/* Streaming DMA (방향 지정, 캐시 sync 필요) */
dma_addr_t dma = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
/* ... DMA 전송 ... */
dma_unmap_single(dev, dma, size, DMA_TO_DEVICE);

DMA 매핑 후에는 반드시 dma_mapping_error()로 오류를 확인해야 합니다. IOMMU가 활성화된 시스템에서는 매핑 실패가 가능합니다.

Linux 디바이스 모델 (Device Model)

Linux 디바이스 모델은 커널 2.6에서 도입된 통합 프레임워크로, 모든 디바이스와 드라이버를 bus → device → driver의 계층으로 추상화합니다. 이 모델은 kobject 인프라 위에 구축되며, sysfs(/sys/)를 통해 사용자 공간에 노출됩니다. udev/systemd-udevd가 디바이스 이벤트를 수신하고, 전원 관리(PM)와 핫플러그가 일관된 인터페이스로 동작합니다.

struct device — 모든 디바이스의 기반

struct device는 커널에 등록되는 모든 디바이스의 공통 기반 구조체입니다. PCI, USB, platform, I2C 등 각 버스별 디바이스 구조체(pci_dev, usb_device, platform_device 등)는 이 구조체를 임베디드합니다.

/* include/linux/device.h (핵심 필드 발췌) */
struct device {
    struct kobject           kobj;           /* sysfs 디렉토리 노드 */
    struct device           *parent;         /* 부모 디바이스 (물리적 계층) */
    const struct device_type *type;          /* 디바이스 타입 (선택적) */

    struct bus_type         *bus;            /* 소속 버스 */
    struct device_driver    *driver;         /* 바인딩된 드라이버 (NULL이면 미바인딩) */

    void                    *platform_data;  /* 보드 레벨 데이터 (레거시) */
    void                    *driver_data;    /* 드라이버 private 데이터 */

    struct device_node      *of_node;        /* Device Tree 노드 */
    struct fwnode_handle    *fwnode;         /* 통합 FW 노드 (DT/ACPI) */

    struct class            *class;          /* 디바이스 클래스 (net, block 등) */
    dev_t                    devt;           /* /dev 노드 번호 (major:minor) */

    const struct dev_pm_ops *pm;             /* 전원 관리 콜백 */
    struct dev_pm_info       power;          /* PM 런타임 상태 */

    struct dma_map_ops      *dma_ops;        /* DMA 매핑 연산 */
    u64                     *dma_mask;       /* DMA 주소 마스크 */
    u64                      coherent_dma_mask;

    struct list_head         devres_head;    /* devm 리소스 리스트 */
};

주요 도우미 함수:

/* 디바이스 등록/해제 */
int device_register(struct device *dev);    /* device_initialize + device_add */
void device_unregister(struct device *dev); /* device_del + put_device */

/* 참조 카운팅 */
struct device *get_device(struct device *dev);  /* kobject_get 래퍼 */
void put_device(struct device *dev);              /* kobject_put 래퍼 */

/* driver_data 접근 */
void *dev_get_drvdata(const struct device *dev);
void dev_set_drvdata(struct device *dev, void *data);

/* 로깅 (dev_name 자동 포함) */
dev_err(&dev, "error: %d\n", ret);
dev_warn(&dev, "warning\n");
dev_info(&dev, "initialized\n");
dev_dbg(&dev, "debug message\n");
dev_err_probe(&dev, ret, "deferred 시 dbg, 그 외 err\n");

struct device_driver — 드라이버 공통 구조체

struct device_driver는 모든 드라이버의 공통 기반입니다. 버스별 드라이버 구조체(pci_driver, platform_driver 등)가 이를 임베디드합니다.

/* include/linux/device/driver.h */
struct device_driver {
    const char              *name;           /* 드라이버 이름 */
    const struct bus_type   *bus;            /* 소속 버스 */
    struct module           *owner;          /* THIS_MODULE */

    const struct of_device_id   *of_match_table;   /* DT compatible 매칭 */
    const struct acpi_device_id *acpi_match_table; /* ACPI HID 매칭 */

    int  (*probe)(struct device *dev);           /* 버스별 probe가 호출 */
    void (*remove)(struct device *dev);          /* 언바인딩 시 호출 */
    void (*shutdown)(struct device *dev);        /* reboot/poweroff 시 */

    const struct dev_pm_ops *pm;                 /* PM 콜백 */
    const struct attribute_group **dev_groups;    /* 드라이버 sysfs 속성 */

    bool suppress_bind_attrs;  /* bind/unbind sysfs 파일 숨김 */
    enum probe_type probe_type; /* PROBE_DEFAULT_STRATEGY / PROBE_FORCE_SYNCHRONOUS */
};

struct bus_type — 버스 추상화

struct bus_type은 PCI, USB, Platform, I2C 등 버스 유형을 추상화합니다. 각 버스는 자신만의 device-driver 매칭 규칙과 probe 메커니즘을 정의합니다.

/* include/linux/device/bus.h */
struct bus_type {
    const char          *name;      /* 버스 이름 (sysfs: /sys/bus/NAME/) */
    const char          *dev_name;  /* 디바이스 이름 생성 접두어 */

    /* 핵심 콜백 */
    int  (*match)(struct device *dev, struct device_driver *drv);
    int  (*probe)(struct device *dev);
    void (*remove)(struct device *dev);
    void (*shutdown)(struct device *dev);

    /* uevent 환경변수 추가 (udev용) */
    int  (*uevent)(const struct device *dev, struct kobj_uevent_env *env);

    /* PM 콜백 */
    const struct dev_pm_ops *pm;

    /* 내부 데이터 */
    struct subsys_private *p;  /* klist_devices, klist_drivers, kset 등 */
};

/* 버스 등록/해제 */
int bus_register(const struct bus_type *bus);
void bus_unregister(const struct bus_type *bus);

커널에 등록된 주요 버스:

bus_typesysfs 경로match 방식디바이스 열거 방식
platform_bus_type/sys/bus/platform/DT compatible, ACPI HID, nameDT/ACPI/정적 등록
pci_bus_type/sys/bus/pci/vendor:device:class IDPCI 설정 공간 스캔
usb_bus_type/sys/bus/usb/vendor:product:class IDUSB 열거 프로토콜
i2c_bus_type/sys/bus/i2c/DT compatible, i2c_device_idDT/보드 파일/ACPI
spi_bus_type/sys/bus/spi/DT compatible, spi_device_idDT/보드 파일/ACPI
virtio_bus/sys/bus/virtio/device ID + feature bitsvirtio PCI/MMIO 스캔

struct class — 디바이스 분류

struct class는 기능별로 디바이스를 분류합니다. 버스(bus)가 물리적 연결을 나타낸다면, 클래스(class)는 논리적 기능을 나타냅니다. 같은 PCI 버스에 연결된 디바이스라도 네트워크 카드는 net 클래스, 그래픽 카드는 drm 클래스에 속합니다.

/* 클래스 정의 예 */
static const struct class my_class = {
    .name    = "my_subsystem",     /* /sys/class/my_subsystem/ */
    .devnode = my_devnode,           /* /dev 노드 이름/퍼미션 제어 */
};

/* 클래스 등록 */
int ret = class_register(&my_class);

/* 클래스 소속 디바이스 생성 — /dev 노드도 자동 생성 (udev 연동) */
struct device *dev = device_create(&my_class, parent,
                                    MKDEV(major, minor),
                                    priv, "my_dev%d", index);

/* 제거 */
device_destroy(&my_class, MKDEV(major, minor));
class_unregister(&my_class);
# sysfs에서 class 확인
/sys/class/
├── net/            # 네트워크 인터페이스 (eth0, wlan0, lo ...)
├── block/          # 블록 디바이스 (sda, nvme0n1 ...)
├── input/          # 입력 디바이스 (event0, mouse0 ...)
├── tty/            # 터미널 디바이스 (ttyS0, ttyUSB0 ...)
├── drm/            # GPU (card0, renderD128 ...)
├── hwmon/          # 하드웨어 모니터링 (hwmon0 ...)
├── gpio/           # GPIO 칩과 라인
└── thermal/        # 열 관리 (thermal_zone0 ...)

# class 디바이스는 실제 디바이스로의 심볼릭 링크
$ ls -l /sys/class/net/eth0
lrwxrwxrwx ... eth0 -> ../../devices/pci0000:00/0000:00:1f.6/net/eth0

Device-Driver 바인딩 흐름

디바이스와 드라이버가 연결(bind)되는 과정은 디바이스 모델의 핵심 메커니즘입니다:

Device-Driver 바인딩 흐름 device_add(dev) driver_register(drv) bus->match(dev, drv) 매칭 성공 really_probe(dev, drv) bus->probe(dev) 또는 drv->probe(dev) return 0 → 바인딩 완료 성공 -EPROBE_DEFER → 재시도 큐 지연 에러 → 바인딩 실패 실패 kobject_uevent(KOBJ_BIND) → udev
device_add() 또는 driver_register() 시점에 버스의 match → probe 흐름이 실행됨
/* drivers/base/dd.c — 바인딩 핵심 흐름 (간략화) */
static int really_probe(struct device *dev, struct device_driver *drv)
{
    /* 1. dev->driver 설정 */
    dev->driver = drv;

    /* 2. 핀 설정 (pinctrl default state 적용) */
    pinctrl_bind_pins(dev);

    /* 3. DMA 설정 */
    dma_configure(dev);

    /* 4. probe 호출 — 버스 probe가 있으면 우선, 없으면 드라이버 probe */
    if (dev->bus->probe)
        ret = dev->bus->probe(dev);       /* platform_drv_probe() 등 */
    else if (drv->probe)
        ret = drv->probe(dev);

    /* 5. 결과 처리 */
    if (ret == -EPROBE_DEFER) {
        driver_deferred_probe_add(dev);  /* 재시도 큐에 추가 */
    } else if (ret == 0) {
        driver_bound(dev);               /* 바인딩 완료 */
        kobject_uevent(&dev->kobj, KOBJ_BIND);
    }
    return ret;
}

Deferred Probing (지연 프로빙)

커널 부팅 시 디바이스 간 의존성 때문에 probe 순서가 중요합니다. 예를 들어 SPI 컨트롤러 드라이버가 아직 로드되지 않은 상태에서 SPI 디바이스의 probe가 먼저 호출될 수 있습니다. 이때 -EPROBE_DEFER를 반환하면 커널이 해당 디바이스를 지연 큐(deferred probe list)에 넣고, 다른 probe가 성공할 때마다 재시도합니다.

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

    /* 의존하는 리소스가 아직 준비되지 않을 수 있음 */
    priv->clk = devm_clk_get(dev, NULL);
    if (IS_ERR(priv->clk)) {
        /* -EPROBE_DEFER이면 자동으로 재시도 큐에 추가됨 */
        return dev_err_probe(dev, PTR_ERR(priv->clk),
                              "failed to get clock\n");
    }

    priv->regulator = devm_regulator_get(dev, "vdd");
    if (IS_ERR(priv->regulator))
        return dev_err_probe(dev, PTR_ERR(priv->regulator),
                              "failed to get regulator\n");
    /* ... */
}

/* dev_err_probe()의 동작:
 *   - PTR_ERR가 -EPROBE_DEFER이면 → dev_dbg 수준 로깅 (dmesg 조용히)
 *   - 그 외 에러이면 → dev_err 수준 로깅
 *   - 두 경우 모두 에러 코드를 그대로 반환
 */
# 지연 프로빙 상태 확인
$ cat /sys/kernel/debug/devices_deferred
platform soc:spi@40010000: -517 (EPROBE_DEFER)
platform soc:i2c@40020000: -517 (EPROBE_DEFER)

# 부팅 완료 후에도 deferred 상태인 디바이스는 의존성 미해결
# 흔한 원인: 누락된 드라이버 모듈, DT 설정 오류, 순환 의존
💡

Deferred Probe 디버깅: /sys/kernel/debug/devices_deferred에서 대기 중인 디바이스 목록을 확인하세요. 커널 파라미터 driver_deferred_probe_timeout=30을 설정하면 지정된 초 이후 대기를 중단하고 경고를 출력합니다. fw_devlink=on(기본값)은 DT/ACPI 의존성을 자동으로 추적하여 올바른 probe 순서를 보장합니다.

fwnode — 통합 펌웨어 노드

struct fwnode_handle은 Device Tree와 ACPI를 통합하는 추상화 계층입니다. 하나의 드라이버 코드로 DT 환경(ARM)과 ACPI 환경(x86 서버) 모두를 지원할 수 있습니다.

#include <linux/property.h>

/* fwnode 통합 API — DT/ACPI 구분 없이 프로퍼티 접근 */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    u32 freq;
    const char *label;

    /* DT: device_property → of_property_read_u32 */
    /* ACPI: device_property → acpi_dev_get_property */
    if (device_property_read_u32(dev, "clock-frequency", &freq))
        freq = 100000;  /* 기본값 */

    if (device_property_read_string(dev, "label", &label))
        label = "default";

    /* 불리언 프로퍼티 (존재 여부만 확인) */
    bool wakeup = device_property_read_bool(dev, "wakeup-source");

    /* 배열 프로퍼티 */
    u32 regs[4];
    device_property_read_u32_array(dev, "reg-offsets", regs, 4);

    /* 자식 노드 순회 */
    struct fwnode_handle *child;
    device_for_each_child_node(dev, child) {
        u32 addr;
        fwnode_property_read_u32(child, "reg", &addr);
        /* ... 자식 디바이스 설정 ... */
    }
    return 0;
}

fwnode 통합 API 주요 함수:

통합 API (fwnode/property)DT 전용 (of_*)ACPI 전용
device_property_read_u32()of_property_read_u32()acpi_dev_get_property()
device_property_read_string()of_property_read_string()acpi_dev_get_property()
device_property_read_bool()of_property_read_bool()-
device_get_match_data()of_device_get_match_data()acpi_device_get_match_data()
device_for_each_child_node()for_each_child_of_node()-

새로운 드라이버를 작성할 때는 of_* / acpi_* 전용 API 대신 통합 device_property_* API를 사용하세요. DT와 ACPI를 동시에 지원하는 드라이버를 작성할 수 있으며, 소프트웨어 노드(software_node)를 통한 단위 테스트도 가능해집니다.

struct device_type — 디바이스 세분화

device_type은 같은 버스에 속하지만 유형이 다른 디바이스를 구분합니다. USB 버스에서 디바이스(usb_device)와 인터페이스(usb_interface)가 대표적 예입니다.

struct device_type {
    const char *name;                           /* uevent DEVTYPE= 값 */
    const struct attribute_group **groups;       /* 추가 sysfs 속성 */
    int  (*uevent)(const struct device *, struct kobj_uevent_env *);
    char *(*devnode)(const struct device *, umode_t *, kuid_t *, kgid_t *);
    void (*release)(struct device *);
    const struct dev_pm_ops *pm;
};

/* 예: 블록 디바이스에서 disk_type과 partition_type 구분 */
/* net_device에서 이더넷, Wi-Fi, 브리지 등 구분 */
/* udev 규칙에서 DEVTYPE으로 매칭:
 *   SUBSYSTEM=="net", DEVTYPE=="wlan", ... */

디바이스 모델 전체 계층 구조

Linux Device Model 계층 구조 bus_type klist_devices (디바이스 목록) klist_drivers (드라이버 목록) struct device kobject (sysfs) of_node / fwnode struct device kobject (sysfs) of_node / fwnode device_driver probe / remove of/acpi_match device_driver probe / remove of/acpi_match match() → probe() class (논리적 그룹: net, block ...) dev->class /sys/bus/ | /sys/devices/ | /sys/class/ | /sys/block/
bus_type이 device와 driver를 연결하고, class가 디바이스를 논리적으로 분류

커스텀 버스 구현 예제

드라이버 서브시스템 개발자를 위한 커스텀 버스 타입 구현 예제입니다:

/* 커스텀 버스: 간단한 ID 기반 매칭 */
struct mybus_device {
    struct device dev;
    u32 device_id;
    const char *name;
};

struct mybus_driver {
    struct device_driver driver;
    const u32 *id_table;        /* 지원하는 device_id 배열 */
    int  (*probe)(struct mybus_device *);
    void (*remove)(struct mybus_device *);
};

/* match 콜백: device_id와 드라이버의 id_table 비교 */
static int mybus_match(struct device *dev, struct device_driver *drv)
{
    struct mybus_device *mdev = container_of(dev, struct mybus_device, dev);
    struct mybus_driver *mdrv = container_of(drv, struct mybus_driver, driver);

    for (const u32 *id = mdrv->id_table; *id; id++) {
        if (*id == mdev->device_id)
            return 1;  /* 매칭 성공 */
    }
    return 0;
}

/* probe 콜백: 버스 레벨에서 드라이버의 probe 호출 */
static int mybus_probe(struct device *dev)
{
    struct mybus_device *mdev = container_of(dev, struct mybus_device, dev);
    struct mybus_driver *mdrv = container_of(dev->driver,
                                              struct mybus_driver, driver);
    return mdrv->probe(mdev);
}

/* uevent: udev에 MYBUS_ID 환경변수 전달 */
static int mybus_uevent(const struct device *dev,
                        struct kobj_uevent_env *env)
{
    const struct mybus_device *mdev =
        container_of(dev, struct mybus_device, dev);
    return add_uevent_var(env, "MYBUS_ID=%u", mdev->device_id);
}

static const struct bus_type mybus_type = {
    .name   = "mybus",
    .match  = mybus_match,
    .probe  = mybus_probe,
    .uevent = mybus_uevent,
};

/* 초기화: /sys/bus/mybus/ 생성 */
bus_register(&mybus_type);

sysfs 디바이스 모델 전체 맵

# /sys 최상위 구조와 디바이스 모델의 관계
/sys/
├── bus/                    # 버스 타입별 디렉토리
│   ├── platform/
│   │   ├── devices/        # 등록된 platform device → /sys/devices/ 심링크
│   │   ├── drivers/        # 등록된 platform driver (bind/unbind 포함)
│   │   ├── drivers_autoprobe  # 1이면 자동 바인딩 활성
│   │   └── uevent          # 버스 레벨 uevent 트리거
│   ├── pci/
│   ├── usb/
│   ├── i2c/
│   └── spi/
│
├── devices/                # 물리적 디바이스 트리 (실제 디렉토리)
│   ├── platform/           # platform bus 디바이스
│   │   └── 40000000.uart/
│   │       ├── driver → ../../../bus/platform/drivers/my-uart
│   │       ├── of_node → ../../../firmware/devicetree/...
│   │       ├── uevent
│   │       ├── power/      # PM 런타임 상태
│   │       └── ...
│   ├── pci0000:00/         # PCI 호스트 브리지
│   │   └── 0000:00:1f.6/   # PCI 디바이스
│   │       ├── net/eth0/   # 네트워크 인터페이스
│   │       └── ...
│   └── system/             # CPU, 메모리 등 시스템 디바이스
│       ├── cpu/
│       └── memory/
│
├── class/                  # 기능별 디바이스 분류 (심링크)
│   ├── net/
│   │   └── eth0 → ../../devices/pci0000:00/.../net/eth0
│   ├── block/
│   └── tty/
│
├── firmware/               # 펌웨어 노드 (DT, ACPI, DMI)
│   ├── devicetree/         # Device Tree 노드 트리
│   └── acpi/               # ACPI 테이블/네임스페이스
│
└── module/                 # 로드된 커널 모듈
    └── my_driver/
        ├── parameters/     # 모듈 파라미터
        └── drivers/        # 이 모듈이 등록한 드라이버 심링크

/sys/devices/실제 디렉토리(물리적 디바이스 계층)이고, /sys/bus/.../devices//sys/class/...심볼릭 링크입니다. 디바이스 정보를 읽을 때는 심링크를 따라가도 되지만, canonical 경로는 /sys/devices/ 아래에 있습니다. udevadm info --path=/sys/devices/...으로 디바이스의 전체 속성을 확인할 수 있습니다.

커널 Notifier Chain (이벤트 통지 체인)

Linux 커널은 다양한 서브시스템 간에 이벤트를 전파하기 위해 Notifier Chain 메커니즘을 제공합니다. 이는 Observer(관찰자) / Publish-Subscribe 패턴의 커널 구현으로, 특정 이벤트가 발생했을 때 미리 등록된 콜백 함수들을 순차적으로 호출합니다. CPU hotplug, 네트워크 디바이스 상태 변경, reboot, panic 등 커널의 핵심 이벤트 대부분이 이 메커니즘을 통해 전파됩니다.

Notifier Chain 개요

Notifier Chain의 핵심은 struct notifier_block입니다. 이벤트를 수신하려는 서브시스템은 콜백 함수와 우선순위를 담은 notifier_block을 체인에 등록하고, 이벤트 발생 시 체인에 등록된 모든 콜백이 우선순위 순서대로 호출됩니다.

/* include/linux/notifier.h */
struct notifier_block {
    notifier_fn_t notifier_call;  /* 콜백 함수 포인터 */
    struct notifier_block __rcu *next;  /* 다음 블록 (우선순위 정렬 연결 리스트) */
    int priority;  /* 우선순위 (높을수록 먼저 호출, 기본값 0) */
};

/* 콜백 함수 시그니처 */
typedef int (*notifier_fn_t)(struct notifier_block *nb,
                            unsigned long action, void *data);

콜백 함수의 매개변수:

💡

Notifier Chain vs 대안: 직접 함수 호출은 두 서브시스템 간 강한 결합을 만들고, workqueue는 비동기 처리에 적합합니다. Notifier Chain은 동기적이면서 느슨한 결합이 필요할 때 — 즉, 여러 독립 서브시스템이 동일한 이벤트에 반응해야 하는 경우에 이상적입니다.

Notifier Chain 4가지 유형

커널은 사용 컨텍스트에 따라 4가지 유형의 Notifier Chain을 제공합니다. 각 유형은 잠금 메커니즘과 호출 가능한 컨텍스트가 다릅니다.

유형 잠금 방식 콜백에서 sleep 호출 컨텍스트 주요 사용처
Atomic spinlock + RCU 불가 인터럽트, atomic 컨텍스트 panic, die, reboot
Blocking rw_semaphore 가능 프로세스 컨텍스트만 netdevice, inetaddr, PM
Raw 잠금 없음 컨텍스트에 따라 호출자가 직접 동기화 저수준 CPU hotplug
SRCU SRCU (Sleepable RCU) 가능 프로세스 컨텍스트 PM notifier
/* 각 유형의 헤드 구조체 (include/linux/notifier.h) */
struct atomic_notifier_head {
    spinlock_t lock;
    struct notifier_block __rcu *head;
};

struct blocking_notifier_head {
    struct rw_semaphore rwsem;
    struct notifier_block __rcu *head;
};

struct raw_notifier_head {
    struct notifier_block __rcu *head;
};

struct srcu_notifier_head {
    struct mutex mutex;
    struct srcu_struct srcu;
    struct notifier_block __rcu *head;
};

각 유형별 초기화 매크로:

/* 정적 초기화 매크로 */
ATOMIC_NOTIFIER_HEAD(my_atomic_chain);
BLOCKING_NOTIFIER_HEAD(my_blocking_chain);
RAW_NOTIFIER_HEAD(my_raw_chain);

/* SRCU는 동적 초기화 필요 */
struct srcu_notifier_head my_srcu_chain;
srcu_init_notifier_head(&my_srcu_chain);

/* 동적 초기화 함수 (atomic/blocking/raw에도 사용 가능) */
ATOMIC_INIT_NOTIFIER_HEAD(&my_atomic_chain);
BLOCKING_INIT_NOTIFIER_HEAD(&my_blocking_chain);
RAW_INIT_NOTIFIER_HEAD(&my_raw_chain);

유형 선택 기준: 인터럽트/NMI 컨텍스트에서 호출되면 Atomic, 콜백에서 sleep이 필요하면 Blocking 또는 SRCU를 사용하세요. Raw는 호출자가 동기화를 직접 관리해야 하므로 일반적으로 권장하지 않습니다. SRCU는 Blocking과 유사하지만 read-side가 SRCU 보호를 받아 콜백 해제가 더 안전합니다.

Notifier Chain API 상세

모든 유형은 동일한 패턴의 API를 따릅니다. {type}_notifier_chain_register(), {type}_notifier_chain_unregister(), {type}_notifier_call_chain()으로 구성됩니다.

등록 / 해제

/* Atomic Notifier Chain */
int atomic_notifier_chain_register(struct atomic_notifier_head *nh,
                                    struct notifier_block *nb);
int atomic_notifier_chain_unregister(struct atomic_notifier_head *nh,
                                      struct notifier_block *nb);

/* Blocking Notifier Chain */
int blocking_notifier_chain_register(struct blocking_notifier_head *nh,
                                      struct notifier_block *nb);
int blocking_notifier_chain_unregister(struct blocking_notifier_head *nh,
                                        struct notifier_block *nb);

/* Raw Notifier Chain */
int raw_notifier_chain_register(struct raw_notifier_head *nh,
                                  struct notifier_block *nb);
int raw_notifier_chain_unregister(struct raw_notifier_head *nh,
                                    struct notifier_block *nb);

/* SRCU Notifier Chain */
int srcu_notifier_chain_register(struct srcu_notifier_head *nh,
                                   struct notifier_block *nb);
int srcu_notifier_chain_unregister(struct srcu_notifier_head *nh,
                                     struct notifier_block *nb);

모든 등록 함수는 성공 시 0을 반환합니다. 등록 시 notifier_block우선순위 내림차순으로 연결 리스트에 삽입됩니다 (높은 priority가 먼저 호출됨).

통지 호출

/* 체인의 모든 콜백을 호출 */
int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
                                unsigned long val, void *v);
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
                                  unsigned long val, void *v);
int raw_notifier_call_chain(struct raw_notifier_head *nh,
                              unsigned long val, void *v);
int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
                               unsigned long val, void *v);

콜백 반환값

콜백 함수는 다음 상수 중 하나를 반환해야 합니다:

반환값 의미
NOTIFY_DONE 0x0000 이벤트에 관심 없음, 다음 콜백 계속
NOTIFY_OK 0x0001 정상 처리됨, 다음 콜백 계속
NOTIFY_STOP_MASK 0x8000 체인 순회 중단 플래그
NOTIFY_BAD NOTIFY_STOP_MASK | 0x0002 에러 발생, 체인 즉시 중단
NOTIFY_STOP NOTIFY_OK | NOTIFY_STOP_MASK 정상이지만 체인 중단 (나머지 콜백 건너뜀)
/* call_chain()의 반환값에서 errno 추출 */
static inline int notifier_to_errno(int ret)
{
    ret &= ~NOTIFY_STOP_MASK;
    return ret > NOTIFY_OK ? ret - 1 : 0;
}

/* errno를 notifier 반환값으로 변환 */
static inline int notifier_from_errno(int err)
{
    if (err)
        return NOTIFY_STOP_MASK | (NOTIFY_OK - err);
    return NOTIFY_OK;
}

NOTIFY_BAD의 용도: NOTIFY_BAD를 반환하면 이벤트 발생 주체에게 "거부"를 알릴 수 있습니다. 예를 들어, PM notifier에서 NOTIFY_BAD를 반환하면 suspend가 취소됩니다. call_chain() 호출자는 notifier_to_errno()로 반환값을 검사합니다.

커널 주요 Notifier Chain 목록

다음은 커널에서 가장 자주 사용되는 주요 Notifier Chain입니다:

Notifier Chain 유형 헤더/소스 주요 이벤트 등록 API
reboot_notifier_list Blocking kernel/reboot.c SYS_RESTART, SYS_HALT, SYS_POWER_OFF register_reboot_notifier()
panic_notifier_list Atomic kernel/panic.c 커널 패닉 발생 atomic_notifier_chain_register()
netdev_chain Raw net/core/dev.c NETDEV_UP, NETDEV_DOWN, NETDEV_CHANGE, ... register_netdevice_notifier()
inetaddr_chain Blocking net/ipv4/devinet.c NETDEV_UP, NETDEV_DOWN (IPv4 주소 변경) register_inetaddr_notifier()
inet6addr_chain Blocking net/ipv6/addrconf.c NETDEV_UP, NETDEV_DOWN (IPv6 주소 변경) register_inet6addr_notifier()
pm_chain_head Blocking kernel/power/main.c PM_SUSPEND_PREPARE, PM_POST_SUSPEND, ... register_pm_notifier()
module_notify_list Blocking kernel/module/main.c MODULE_STATE_LIVE, MODULE_STATE_COMING, ... register_module_notifier()
keyboard_notifier_list Atomic drivers/tty/vt/keyboard.c KBD_KEYCODE, KBD_KEYSYM, ... register_keyboard_notifier()
die_chain Atomic arch/*/kernel/traps.c DIE_OOPS, DIE_GPF, DIE_TRAP, ... register_die_notifier()
fb_notifier_list Blocking drivers/video/fbdev/core/fbmem.c FB_EVENT_MODE_CHANGE, FB_EVENT_BLANK, ... fb_register_client()

Notifier Chain 동작 흐름

Notifier Chain 동작 흐름 이벤트 발생 (예: netdev_change) raw_notifier_call_chain(&netdev_chain, val, dev) 우선순위 순서 nb1->notifier_call(nb1, val, dev) priority=10 NOTIFY_OK nb2->notifier_call(nb2, val, dev) priority=0 NOTIFY_DONE nb3->notifier_call(nb3, val, dev) priority=-5 NOTIFY_STOP nb4->notifier_call(...) 건너뜀! 최종 반환 호출자에게 NOTIFY_STOP 반환

실전 구현 예제

예제 1: 네트워크 디바이스 Notifier

네트워크 인터페이스의 상태 변경(UP/DOWN)을 감지하는 가장 일반적인 notifier 사용 예제입니다:

#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/notifier.h>

static int my_netdev_event(struct notifier_block *nb,
                            unsigned long event, void *ptr)
{
    struct net_device *dev = netdev_notifier_info_to_dev(ptr);

    switch (event) {
    case NETDEV_UP:
        pr_info("netdev UP: %s (ifindex=%d)\n",
                dev->name, dev->ifindex);
        break;
    case NETDEV_DOWN:
        pr_info("netdev DOWN: %s (ifindex=%d)\n",
                dev->name, dev->ifindex);
        break;
    case NETDEV_REGISTER:
        pr_info("netdev REGISTER: %s\n", dev->name);
        break;
    case NETDEV_UNREGISTER:
        pr_info("netdev UNREGISTER: %s\n", dev->name);
        break;
    default:
        break;
    }

    return NOTIFY_DONE;
}

static struct notifier_block my_netdev_nb = {
    .notifier_call = my_netdev_event,
    .priority = 0,
};

static int __init my_notifier_init(void)
{
    int ret;

    ret = register_netdevice_notifier(&my_netdev_nb);
    if (ret) {
        pr_err("netdev notifier 등록 실패: %d\n", ret);
        return ret;
    }

    pr_info("netdev notifier 등록 완료\n");
    return 0;
}

static void __exit my_notifier_exit(void)
{
    unregister_netdevice_notifier(&my_netdev_nb);
    pr_info("netdev notifier 해제 완료\n");
}

module_init(my_notifier_init);
module_exit(my_notifier_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Network device notifier example");
💡

netdev_notifier_info_to_dev(): 커널 3.11부터 netdevice notifier의 data 매개변수는 struct net_device *가 아닌 struct netdev_notifier_info *입니다. 반드시 netdev_notifier_info_to_dev() 헬퍼를 사용하세요.

예제 2: 커스텀 Notifier Chain 정의 및 사용

자체 Notifier Chain을 정의하여 모듈 간 이벤트를 전파하는 예제입니다:

#include <linux/module.h>
#include <linux/notifier.h>

/* ===== 이벤트 정의 (공유 헤더에 배치) ===== */
#define MY_EVENT_START    0x01
#define MY_EVENT_STOP     0x02
#define MY_EVENT_ERROR    0x03

struct my_event_data {
    const char *name;
    int code;
};

/* ===== 이벤트 발행 측 (Producer) ===== */
static BLOCKING_NOTIFIER_HEAD(my_event_chain);

/* 외부 모듈이 사용할 등록/해제 API */
int my_register_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_register(&my_event_chain, nb);
}
EXPORT_SYMBOL_GPL(my_register_notifier);

int my_unregister_notifier(struct notifier_block *nb)
{
    return blocking_notifier_chain_unregister(&my_event_chain, nb);
}
EXPORT_SYMBOL_GPL(my_unregister_notifier);

/* 이벤트 발생 시 호출 */
void my_fire_event(unsigned long event, struct my_event_data *data)
{
    int ret;

    ret = blocking_notifier_call_chain(&my_event_chain, event, data);
    if (notifier_to_errno(ret))
        pr_warn("이벤트 %lu 처리 중 에러 발생\n", event);
}

/* ===== 이벤트 수신 측 (Consumer) ===== */
static int my_event_handler(struct notifier_block *nb,
                             unsigned long event, void *data)
{
    struct my_event_data *ev = data;

    switch (event) {
    case MY_EVENT_START:
        pr_info("[Consumer] START: name=%s, code=%d\n",
                ev->name, ev->code);
        return NOTIFY_OK;
    case MY_EVENT_ERROR:
        pr_err("[Consumer] ERROR: name=%s, code=%d\n",
               ev->name, ev->code);
        return notifier_from_errno(-EIO);  /* 에러 전파 */
    default:
        return NOTIFY_DONE;
    }
}

static struct notifier_block my_handler_nb = {
    .notifier_call = my_event_handler,
    .priority = 0,
};

static int __init consumer_init(void)
{
    return my_register_notifier(&my_handler_nb);
}

static void __exit consumer_exit(void)
{
    my_unregister_notifier(&my_handler_nb);
}

module_init(consumer_init);
module_exit(consumer_exit);
MODULE_LICENSE("GPL");

Notifier Chain 내부 구현

Notifier Chain의 핵심 구현은 kernel/notifier.c에 있습니다. 모든 유형이 공유하는 내부 함수를 살펴보겠습니다.

우선순위 기반 삽입

등록 시 notifier_chain_register()는 우선순위 내림차순으로 연결 리스트에 삽입합니다:

/* kernel/notifier.c — 핵심 내부 함수 (간략화) */
static int notifier_chain_register(
    struct notifier_block **nl,
    struct notifier_block *n)
{
    while (*nl != NULL) {
        if (unlikely((*nl) == n)) {
            WARN(1, "notifier already registered");
            return -EEXIST;
        }
        /* 우선순위 내림차순 정렬: 높은 값이 리스트 앞쪽 */
        if (n->priority > (*nl)->priority)
            break;
        nl = &((*nl)->next);
    }
    n->next = *nl;
    rcu_assign_pointer(*nl, n);
    return 0;
}

체인 순회 (call_chain)

/* kernel/notifier.c — 통지 순회 (간략화) */
static int notifier_call_chain(
    struct notifier_block **nl,
    unsigned long val, void *v,
    int nr_to_call, int *nr_calls)
{
    int ret = NOTIFY_DONE;
    struct notifier_block *nb, *next_nb;

    nb = rcu_dereference_raw(*nl);
    while (nb && nr_to_call) {
        next_nb = rcu_dereference_raw(nb->next);

        ret = nb->notifier_call(nb, val, v);

        if (nr_calls)
            (*nr_calls)++;

        /* NOTIFY_STOP_MASK이 설정되면 순회 중단 */
        if (ret & NOTIFY_STOP_MASK)
            break;

        nb = next_nb;
        nr_to_call--;
    }
    return ret;
}

각 유형별 잠금 차이

/* Atomic: spinlock으로 등록 보호, RCU로 순회 보호 */
int atomic_notifier_call_chain(struct atomic_notifier_head *nh,
                                unsigned long val, void *v)
{
    int ret;
    rcu_read_lock();
    ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
    rcu_read_unlock();
    return ret;
}

/* Blocking: rw_semaphore로 등록/순회 모두 보호 */
int blocking_notifier_call_chain(struct blocking_notifier_head *nh,
                                  unsigned long val, void *v)
{
    int ret;
    if (rcu_access_pointer(nh->head)) {
        down_read(&nh->rwsem);
        ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
        up_read(&nh->rwsem);
    } else {
        ret = NOTIFY_DONE;
    }
    return ret;
}

/* SRCU: Sleepable RCU로 순회 보호, mutex로 등록 보호 */
int srcu_notifier_call_chain(struct srcu_notifier_head *nh,
                               unsigned long val, void *v)
{
    int ret, idx;
    idx = srcu_read_lock(&nh->srcu);
    ret = notifier_call_chain(&nh->head, val, v, -1, NULL);
    srcu_read_unlock(&nh->srcu, idx);
    return ret;
}

RCU 보호의 의미: Atomic notifier의 순회는 rcu_read_lock()으로 보호됩니다. 이는 unregister() 후에도 RCU grace period가 완료될 때까지 해제된 notifier_block이 유효한 메모리를 참조함을 보장합니다. 따라서 unregister()와 콜백 실행 사이에 race condition이 발생하지 않습니다. Blocking notifier는 rw_semaphore가 이 역할을 대신합니다.

주의사항 및 디버깅

흔한 실수와 해결책

문제 원인 해결책
Atomic notifier 콜백에서 BUG/scheduling 콜백 내에서 sleep 가능 함수 호출 (mutex, kmalloc(GFP_KERNEL) 등) Blocking 유형으로 변경하거나, 콜백에서 workqueue로 지연 처리
모듈 언로드 시 크래시 module_exit에서 unregister 누락 반드시 exit에서 해제; devm_ 래퍼 활용 검토
Deadlock 콜백 내에서 동일 체인에 register/unregister 시도 콜백 안에서 체인 수정 금지; workqueue로 지연 처리
이벤트 누락 등록 전에 이미 발생한 이벤트 등록 후 현재 상태를 수동 확인하는 초기화 코드 추가
긴 체인으로 인한 지연 체인에 많은 콜백이 등록되어 순회 시간 증가 콜백을 가볍게 유지; 무거운 처리는 workqueue로 분리

Deadlock 시나리오 상세

/* 위험! Blocking notifier 콜백 내에서 동일 체인 수정 */
static int bad_callback(struct notifier_block *nb,
                         unsigned long event, void *data)
{
    /* call_chain()이 rwsem read-lock을 잡고 있는 상태에서
       unregister()가 write-lock을 요청 → DEADLOCK */
    blocking_notifier_chain_unregister(&chain, nb);  /* 절대 금지! */
    return NOTIFY_DONE;
}

/* 올바른 방법: workqueue로 지연 해제 */
static struct work_struct unreg_work;

static void deferred_unregister(struct work_struct *work)
{
    blocking_notifier_chain_unregister(&chain, &my_nb);
}

static int safe_callback(struct notifier_block *nb,
                          unsigned long event, void *data)
{
    if (event == SOME_FINAL_EVENT) {
        schedule_work(&unreg_work);  /* workqueue에서 안전하게 해제 */
        return NOTIFY_STOP;
    }
    return NOTIFY_DONE;
}

Blocking notifier 콜백 안에서 동일 체인을 수정(register/unregister)하면 deadlock이 발생합니다. call_chain()down_read()를 잡고 있는 상태에서 unregister()down_write()를 시도하기 때문입니다. 반드시 workqueue나 별도 컨텍스트에서 해제하세요.

ftrace / bpftrace를 이용한 디버깅

# ftrace로 notifier_call_chain 호출 추적
echo 1 > /sys/kernel/debug/tracing/events/notifier/notifier_run/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace_pipe

# bpftrace로 특정 notifier 호출 시간 측정
bpftrace -e '
kprobe:blocking_notifier_call_chain {
    @start[tid] = nsecs;
}
kretprobe:blocking_notifier_call_chain /@start[tid]/ {
    @latency_us = hist((nsecs - @start[tid]) / 1000);
    delete(@start[tid]);
}'

# 특정 체인에 등록된 notifier 확인 (debugfs)
# CONFIG_DEBUG_NOTIFIERS=y 필요
cat /sys/kernel/debug/notifier_chains
💡

CONFIG_DEBUG_NOTIFIERS: 이 옵션을 활성화하면 notifier 콜백의 반환값을 검증하고, 잘못된 반환값에 대해 WARN()을 출력합니다. 개발 커널에서는 항상 활성화하는 것을 권장합니다. make menuconfig에서 Kernel hacking → Debug Notifiers에서 설정할 수 있습니다.

Block Device 드라이버

#include <linux/blkdev.h>
#include <linux/blk-mq.h>

static struct gendisk *my_disk;
static struct blk_mq_tag_set tag_set;

/* blk-mq 요청 처리 */
static blk_status_t my_queue_rq(struct blk_mq_hw_ctx *hctx,
    const struct blk_mq_queue_data *bd)
{
    struct request *rq = bd->rq;
    struct bio_vec bvec;
    struct req_iterator iter;

    blk_mq_start_request(rq);

    rq_for_each_segment(bvec, rq, iter) {
        sector_t sector = iter.iter.bi_sector;
        void *buf = page_address(bvec.bv_page) + bvec.bv_offset;
        unsigned len = bvec.bv_len;

        if (rq_data_dir(rq) == WRITE)
            memcpy(my_data + sector * 512, buf, len);
        else
            memcpy(buf, my_data + sector * 512, len);
    }

    blk_mq_end_request(rq, BLK_STS_OK);
    return BLK_STS_OK;
}

static const struct blk_mq_ops my_mq_ops = {
    .queue_rq = my_queue_rq,
};

Network Device 드라이버

#include <linux/netdevice.h>
#include <linux/etherdevice.h>

static netdev_tx_t my_start_xmit(struct sk_buff *skb,
    struct net_device *dev)
{
    /* 패킷을 하드웨어로 전송 */
    write_to_hw_tx_ring(skb->data, skb->len);

    dev->stats.tx_packets++;
    dev->stats.tx_bytes += skb->len;

    dev_kfree_skb(skb);
    return NETDEV_TX_OK;
}

static int my_net_open(struct net_device *dev)
{
    netif_start_queue(dev);
    napi_enable(&priv->napi);
    enable_hw_irq(priv);
    return 0;
}

static const struct net_device_ops my_netdev_ops = {
    .ndo_open       = my_net_open,
    .ndo_stop       = my_net_stop,
    .ndo_start_xmit = my_start_xmit,
    .ndo_get_stats64 = my_get_stats64,
};

앞서 살펴본 드라이버 등록 방식은 x86/ACPI 환경 기준입니다. ARM, RISC-V 등 임베디드 플랫폼에서는 PCI/USB처럼 자동 열거가 불가능한 SoC 디바이스가 대부분이므로, Device Tree가 하드웨어 기술의 핵심 메커니즘입니다. 다음 섹션에서 Device Tree의 구조와 커널 연동을 상세히 다룹니다.

Device Tree (DTS/DTB) 심화

Device Tree는 하드웨어 구성을 기술하는 데이터 구조로, PCI/USB처럼 자동 열거(enumeration)가 불가능한 SoC 내장 디바이스의 정보를 커널에 전달합니다. ARM, RISC-V, PowerPC 등 임베디드 플랫폼에서 필수적이며, Open Firmware(IEEE 1275) 표준에서 유래했습니다.

DTS 처리 흐름: .dts (소스) → dtc (컴파일러) → .dtb (바이너리 블롭) → 부트로더가 메모리에 로드 → 커널이 파싱하여 struct device_node 트리 구축 → 드라이버가 of_* API로 프로퍼티 조회

Device Tree 아키텍처

빌드 시점 (Build Time) .dts / .dtsi Device Tree Source dtc DT Compiler .dtb FDT Binary Blob .dtbo (Overlay) 런타임 수정 가능 부팅 시점 (Boot Time) 부트로더 (U-Boot / UEFI) DTB → 메모리 로드 + x0 전달 Overlay 병합 (선택) 커널 (Kernel Space) unflatten_device_tree() struct device_node 트리 /proc/device-tree/ of_platform_populate() platform_driver i2c_driver spi_driver of_*() API fwnode API /sys/firmware/devicetree/base/ compatible 매칭 → probe() 호출
Device Tree 처리 흐름 — 빌드, 부팅, 커널 파싱, 드라이버 매칭까지

DTS 문법 상세

/*
 * Device Tree Source (.dts) 문법
 *
 * 기본 구조: 노드(node)와 프로퍼티(property)의 트리
 *
 * 노드 형식:
 *   [label:] node-name[@unit-address] {
 *       [properties];
 *       [child nodes];
 *   };
 *
 * 프로퍼티 데이터 타입:
 *   - 빈 값:          속성 존재만 의미 (boolean)
 *   - u32:           < 0x1234 >
 *   - u64:           /bits/ 64 < 0x1234567890 >
 *   - 문자열:         "hello"
 *   - 문자열 목록:     "first", "second"
 *   - 바이트 배열:     [00 11 22 33]
 *   - phandle 참조:   <&label>
 *   - 혼합:           < 0x1234 >, "string", [00 ff]
 */

/* ===== 완전한 DTS 예제 ===== */
/dts-v1/;

/* .dtsi 인클루드 — SoC 공통 정의 재사용 */
#include "my-soc.dtsi"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/clock/my-soc-clk.h>

/ {
    /* 루트 노드 — 보드 전체 정보 */
    model = "MyVendor MyBoard Rev.A";
    compatible = "myvendor,myboard", "myvendor,my-soc";

    /* #address-cells / #size-cells:
     * 자식 노드의 reg 프로퍼티 해석 방법 지정
     * #address-cells = <2> → 주소가 u32 × 2 = 64-bit
     * #size-cells = <1> → 크기가 u32 × 1 = 32-bit */
    #address-cells = <2>;
    #size-cells = <2>;

    /* chosen 노드 — 부트로더→커널 런타임 파라미터 */
    chosen {
        bootargs = "console=ttyS0,115200 root=/dev/mmcblk0p2 rw";
        stdout-path = "serial0:115200n8";
    };

    /* aliases — 노드에 짧은 이름 부여 */
    aliases {
        serial0 = &uart0;
        ethernet0 = ð0;
        mmc0 = &sdhci0;
    };

    /* memory 노드 — 물리 메모리 레이아웃 */
    memory@80000000 {
        device_type = "memory";
        reg = <0x0 0x80000000 0x0 0x40000000>;  /* 1 GiB @ 0x80000000 */
    };

    /* cpus 노드 */
    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu@0 {
            device_type = "cpu";
            compatible = "arm,cortex-a53";
            reg = <0x0>;
            enable-method = "psci";
            clocks = <&cpu_clk>;
            operating-points-v2 = <&cpu_opp_table>;
        };
        cpu@1 {
            device_type = "cpu";
            compatible = "arm,cortex-a53";
            reg = <0x1>;
            enable-method = "psci";
        };
    };

    /* SoC 버스 — 주소 공간 정의 */
    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges = <0x0 0x0 0x0 0x40000000>;  /* 자식→부모 주소 변환 */

        /* 인터럽트 컨트롤러 */
        gic: interrupt-controller@1c81000 {
            compatible = "arm,gic-400";
            interrupt-controller;        /* 빈 프로퍼티 (boolean) */
            #interrupt-cells = <3>;     /* 자식의 interrupts 해석: type irq flags */
            reg = <0x1c81000 0x1000>,   /* GICD */
                  <0x1c82000 0x2000>;   /* GICC */
        };

        /* 클럭 컨트롤러 — phandle로 참조 */
        ccu: clock-controller@1c20000 {
            compatible = "myvendor,my-soc-ccu";
            reg = <0x1c20000 0x400>;
            clocks = <&osc24m>, <&osc32k>;
            clock-names = "hosc", "losc";
            #clock-cells = <1>;      /* 자식이 참조 시 인덱스 1개 */
            #reset-cells = <1>;
        };

        /* UART — label로 phandle 자동 생성 */
        uart0: serial@1c28000 {
            compatible = "myvendor,my-soc-uart", "snps,dw-apb-uart";
            reg = <0x1c28000 0x400>;
            interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&ccu CLK_UART0>;
            resets = <&ccu RST_UART0>;
            reg-shift = <2>;
            reg-io-width = <4>;
            status = "okay";
        };

        /* I2C 컨트롤러 + 자식 디바이스 */
        i2c0: i2c@1c2ac00 {
            compatible = "myvendor,my-soc-i2c";
            reg = <0x1c2ac00 0x400>;
            interrupts = <GIC_SPI 6 IRQ_TYPE_LEVEL_HIGH>;
            clocks = <&ccu CLK_I2C0>;
            resets = <&ccu RST_I2C0>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "okay";

            /* I2C 슬레이브 디바이스 */
            sensor@48 {
                compatible = "ti,tmp102";
                reg = <0x48>;          /* I2C 주소 */
                interrupt-parent = <&gic>;
                interrupts = <GIC_SPI 20 IRQ_TYPE_EDGE_FALLING>;
            };

            pmic@34 {
                compatible = "xpower,axp803";
                reg = <0x34>;
                interrupt-parent = <&gic>;
                interrupts = <GIC_SPI 32 IRQ_TYPE_LEVEL_LOW>;

                /* 서브노드: PMIC 내 레귤레이터 */
                regulators {
                    reg_dcdc1: dcdc1 {
                        regulator-name = "vcc-3v3";
                        regulator-min-microvolt = <3300000>;
                        regulator-max-microvolt = <3300000>;
                        regulator-always-on;
                    };
                };
            };
        };
    };
};

표준 프로퍼티 레퍼런스

프로퍼티타입설명예시
compatiblestring-list드라이버 매칭 키. 구체적→일반적 순서"vendor,exact", "vendor,fallback"
regprop-encoded주소/크기 쌍. 해석은 부모의 #address-cells/#size-cells에 의존<0x10000 0x1000>
interruptsprop-encoded인터럽트 지정자. 해석은 인터럽트 컨트롤러의 #interrupt-cells에 의존<GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>
interrupt-parentphandle인터럽트 컨트롤러 참조 (생략 시 부모 노드에서 상속)<&gic>
clocksphandle+args클럭 소스 참조<&ccu CLK_UART0>
clock-namesstring-list클럭 이름 (clocks와 순서 대응)"apb", "mod"
resetsphandle+args리셋 컨트롤러 참조<&ccu RST_UART0>
statusstring"okay"=활성, "disabled"=비활성"okay"
#address-cellsu32자식 reg의 주소 u32 개수<2>
#size-cellsu32자식 reg의 크기 u32 개수 (0이면 크기 없음)<1>
rangesprop-encoded자식→부모 주소 변환. 빈 값이면 1:1 매핑<0x0 0x0 0x10000000 0x1000000>
dma-rangesprop-encodedDMA 주소 변환 (CPU 주소 ≠ DMA 주소일 때)<0x0 0x0 0x80000000 0x80000000>
pinctrl-0phandle-list핀 설정 참조 (상태 0=default)<&uart0_pins>
pinctrl-namesstring-list핀 설정 상태 이름"default", "sleep"
*-gpiosphandle+argsGPIO 참조 (접두사가 이름)reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>
*-supplyphandle전원 레귤레이터 참조vcc-supply = <®_3v3>

.dtsi 인클루드 구조와 오버라이드

/*
 * .dtsi (Device Tree Source Include) — SoC 공통 정의
 * .dts  — 보드별 최종 파일, .dtsi를 인클루드하고 오버라이드
 *
 * 계층 구조 예시:
 *   arch/arm64/boot/dts/
 *   ├── myvendor/
 *   │   ├── my-soc.dtsi          ← SoC 공통 (IP 블록, 클럭, 인터럽트)
 *   │   ├── my-soc-gpu.dtsi      ← GPU 관련 (선택적 인클루드)
 *   │   ├── myboard-rev-a.dts    ← 보드 A (오버라이드, 확장)
 *   │   └── myboard-rev-b.dts    ← 보드 B (다른 설정)
 *
 * 오버라이드 규칙:
 * - .dts에서 .dtsi의 노드를 재정의하면 프로퍼티가 병합/덮어쓰기
 * - &label 참조로 기존 노드를 수정 (노드 경로 생략 가능)
 */

/* === my-soc.dtsi (SoC 공통) === */
/ {
    soc {
        uart0: serial@1c28000 {
            compatible = "myvendor,my-soc-uart";
            reg = <0x1c28000 0x400>;
            clocks = <&ccu CLK_UART0>;
            status = "disabled";  /* 기본: 비활성 */
        };

        i2c0: i2c@1c2ac00 {
            compatible = "myvendor,my-soc-i2c";
            reg = <0x1c2ac00 0x400>;
            #address-cells = <1>;
            #size-cells = <0>;
            status = "disabled";
        };
    };
};

/* === myboard-rev-a.dts (보드별) === */
/dts-v1/;
#include "my-soc.dtsi"

/ {
    model = "MyBoard Rev.A";
};

/* &label 참조로 기존 노드 오버라이드 */
&uart0 {
    status = "okay";         /* 이 보드에서 UART0 활성화 */
    pinctrl-0 = <&uart0_pins>; /* 핀 설정 추가 */
    pinctrl-names = "default";
};

&i2c0 {
    status = "okay";

    /* 이 보드에 연결된 센서 추가 */
    accelerometer@1d {
        compatible = "st,lis3dh";
        reg = <0x1d>;
        interrupt-parent = <&gic>;
        interrupts = <GIC_SPI 25 IRQ_TYPE_EDGE_RISING>;
        vdd-supply = <®_3v3>;
    };
};

Device Tree Overlay

/*
 * Device Tree Overlay (.dtbo):
 *
 * 런타임에 기존 DTB에 노드/프로퍼티를 추가·수정·삭제합니다.
 * 용도:
 *   - HAT/Cape/Shield 등 확장 보드 자동 인식
 *   - Raspberry Pi, BeagleBone 등에서 광범위하게 사용
 *   - 재부팅 없이 하드웨어 구성 변경 (configfs 기반)
 *
 * Overlay 문법:
 *   /plugin/; 지시어로 overlay 파일임을 선언
 *   fragment 또는 __overlay__ 블록으로 수정할 노드 지정
 */

/* === my-hat-overlay.dts === */
/dts-v1/;
/plugin/;

/* &{/path} 또는 &label로 대상 노드 참조 */
&i2c0 {
    #address-cells = <1>;
    #size-cells = <0>;
    status = "okay";

    /* HAT에 장착된 OLED 디스플레이 */
    oled@3c {
        compatible = "solomon,ssd1306";
        reg = <0x3c>;
        width = <128>;
        height = <64>;
        solomon,com-invdir;
    };
};

/* fragment 문법 (대체 형식) */
/ {
    fragment@0 {
        target = <&spi0>;
        __overlay__ {
            status = "okay";
            cs-gpios = <&gpio 8 GPIO_ACTIVE_LOW>;

            can0: can@0 {
                compatible = "microchip,mcp2515";
                reg = <0>;
                spi-max-frequency = <10000000>;
                clocks = <&can_osc>;
                interrupt-parent = <&gpio>;
                interrupts = <25 IRQ_TYPE_EDGE_FALLING>;
            };
        };
    };
};

# Overlay 컴파일
$ dtc -I dts -O dtb -@ -o my-hat.dtbo my-hat-overlay.dts
# -@ : __symbols__ 노드 생성 (overlay 심볼 해석에 필요)

# configfs를 통한 런타임 적용 (v4.4+)
$ mkdir -p /sys/kernel/config/device-tree/overlays/my-hat
$ cat my-hat.dtbo > /sys/kernel/config/device-tree/overlays/my-hat/dtbo
# → 커널이 overlay를 live DT에 병합, 새 디바이스 probe

# Overlay 제거
$ rmdir /sys/kernel/config/device-tree/overlays/my-hat
# → 관련 디바이스 remove, DT에서 노드 제거

# U-Boot에서 부팅 시 적용
# fdt apply ${fdtoverlay_addr}

Device Tree Bindings

/*
 * DT Binding = 특정 하드웨어에 필요한 프로퍼티 규격
 *
 * 위치: Documentation/devicetree/bindings/
 * 형식: YAML schema (dt-schema, v5.2+) 또는 텍스트 문서 (레거시)
 *
 * 검증 도구:
 *   make dt_binding_check    ← YAML 스키마 자체 검증
 *   make dtbs_check          ← DTB가 바인딩을 준수하는지 검증
 *
 * compatible 문자열 규칙:
 *   "vendor,device[-version]"
 *   vendor: JEDEC 또는 Documentation/devicetree/bindings/vendor-prefixes.yaml
 *   device: 구체적 칩/IP 이름
 *
 * 예시:
 *   "ti,am335x-uart"       ← TI AM335x SoC의 UART
 *   "samsung,exynos4210-i2c" ← Samsung Exynos4210의 I2C
 *   "snps,dw-apb-uart"     ← Synopsys DesignWare APB UART (IP 블록)
 */

# YAML 바인딩 예시: Documentation/devicetree/bindings/serial/snps,dw-apb-uart.yaml
# (간략화)

# $id: http://devicetree.org/schemas/serial/snps,dw-apb-uart.yaml#
# $schema: http://devicetree.org/meta-schemas/core.yaml#
# title: Synopsys DesignWare ABP UART
# 
# properties:
#   compatible:
#     oneOf:
#       - items:
#           - enum:
#               - myvendor,my-soc-uart
#           - const: snps,dw-apb-uart
#   reg:
#     maxItems: 1
#   interrupts:
#     maxItems: 1
#   clocks:
#     minItems: 1
#     maxItems: 2
#   clock-names:
#     items:
#       - const: baudclk
#       - const: apb_pclk
#   reg-shift:
#     enum: [0, 2]
# 
# required:
#   - compatible
#   - reg
#   - interrupts
#   - clocks

# 바인딩 검증 실행
$ make dt_binding_check DT_SCHEMA_FILES=serial/snps,dw-apb-uart.yaml
$ make dtbs_check DT_SCHEMA_FILES=serial/snps,dw-apb-uart.yaml

DTS 컴파일과 디컴파일

# ===== DTS → DTB 컴파일 =====

# 커널 빌드 시스템을 통해 (권장)
$ make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
# → arch/arm64/boot/dts/myvendor/*.dtb 생성

# 특정 DTB만 빌드
$ make ARCH=arm64 myvendor/myboard-rev-a.dtb

# DTB 설치
$ make ARCH=arm64 INSTALL_DTBS_PATH=/boot/dtbs dtbs_install

# dtc 직접 사용
$ dtc -I dts -O dtb -o myboard.dtb myboard.dts
# -I: 입력 형식 (dts, dtb, fs)
# -O: 출력 형식 (dts, dtb, asm)

# ===== DTB → DTS 디컴파일 =====
$ dtc -I dtb -O dts -o decompiled.dts myboard.dtb

# 실행 중인 시스템의 live DT 디컴파일
$ dtc -I fs -O dts -o live-dt.dts /sys/firmware/devicetree/base/

# ===== DTB 정보 조회 =====
$ fdtdump myboard.dtb | head -50        # 구조 덤프
$ fdtget myboard.dtb /soc/serial@1c28000 compatible
myvendor,my-soc-uart snps,dw-apb-uart
$ fdtget -t x myboard.dtb /soc/serial@1c28000 reg
1c28000 400

# DTB 수정 (디버깅/테스트용)
$ fdtput myboard.dtb /soc/serial@1c28000 status -ts "disabled"

# ===== CPP 전처리 =====
# 커널 빌드 시스템은 DTS를 dtc에 전달하기 전에 C 전처리기(cpp)를 먼저 실행합니다.
# 따라서 #include, #define, #ifdef 등 C 전처리 지시어가 DTS에서 동작합니다.
#
# dt-bindings/ 헤더: include/dt-bindings/ 디렉토리의 .h 파일
# → GPIO, 인터럽트, 클럭 등의 숫자 상수를 매크로로 정의
# 예: #include <dt-bindings/gpio/gpio.h>
#     GPIO_ACTIVE_HIGH = 0, GPIO_ACTIVE_LOW = 1

커널 OF(Open Firmware) API

#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/of_gpio.h>

/* ===== 프로퍼티 읽기 ===== */

u32 val;
of_property_read_u32(np, "my-prop", &val);       /* u32 1개 */

u32 arr[4];
of_property_read_u32_array(np, "my-array", arr, 4); /* u32 배열 */

u64 val64;
of_property_read_u64(np, "my-u64", &val64);    /* u64 */

const char *str;
of_property_read_string(np, "label", &str);    /* 문자열 */

int count = of_property_read_string_helper(      /* 문자열 목록 */
    np, "clock-names", NULL, 0, 0);

bool present = of_property_read_bool(np, "big-endian"); /* boolean */

/* ===== 노드 탐색 ===== */

struct device_node *child;
for_each_child_of_node(np, child) {             /* 자식 순회 */
    /* child 처리... */
}

struct device_node *node;
node = of_find_compatible_node(NULL, NULL,
    "myvendor,my-device");                        /* compatible로 검색 */

node = of_find_node_by_path("/soc/serial@1c28000"); /* 경로로 검색 */

node = of_parse_phandle(np, "clocks", 0);       /* phandle 참조 해석 */

/* ===== 리소스 가져오기 ===== */

struct resource res;
of_address_to_resource(np, 0, &res);            /* reg → struct resource */
void __iomem *base = of_iomap(np, 0);          /* reg → ioremap */

int irq = of_irq_get(np, 0);                    /* interrupts → IRQ 번호 */
int irq2 = platform_get_irq(pdev, 0);           /* platform 래퍼 (권장) */

/* ===== compatible 매칭 확인 ===== */

bool match = of_device_is_compatible(np, "vendor,dev");

const struct of_device_id *id;
id = of_match_device(my_of_ids, &pdev->dev);
if (id && id->data) {
    /* match-specific 데이터 사용 */
    const struct my_hw_data *hw = id->data;
}

Device Tree + Platform Driver 통합

/* ===== 완전한 DT 기반 Platform Driver 예제 ===== */

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

/* 칩 버전별 데이터 */
struct my_hw_data {
    int fifo_depth;
    bool has_dma;
};

static const struct my_hw_data hw_v1 = { .fifo_depth = 16, .has_dma = false };
static const struct my_hw_data hw_v2 = { .fifo_depth = 64, .has_dma = true  };

/* of_device_id: compatible 문자열 → 드라이버 매칭 테이블 */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "myvendor,my-device-v1", .data = &hw_v1 },
    { .compatible = "myvendor,my-device-v2", .data = &hw_v2 },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

struct my_dev {
    void __iomem *base;
    struct clk *clk;
    struct reset_control *rst;
    const struct my_hw_data *hw;
    int irq;
};

static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    struct my_dev *priv;
    u32 fifo_thr;
    int ret;

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

    /* 1. compatible에 연결된 하드웨어 데이터 가져오기 */
    priv->hw = of_device_get_match_data(dev);
    if (!priv->hw)
        return -ENODEV;

    /* 2. reg → MMIO 매핑 (devm 관리) */
    priv->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    /* 3. interrupts → IRQ 번호 */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    /* 4. clocks → 클럭 가져오기 + 활성화 */
    priv->clk = devm_clk_get_enabled(dev, NULL);  /* v6.3+ */
    if (IS_ERR(priv->clk))
        return dev_err_probe(dev, PTR_ERR(priv->clk),
                             "failed to get clock\n");

    /* 5. resets → 리셋 제어 */
    priv->rst = devm_reset_control_get_exclusive(dev, NULL);
    if (IS_ERR(priv->rst))
        return PTR_ERR(priv->rst);
    reset_control_deassert(priv->rst);

    /* 6. 커스텀 프로퍼티 읽기 (선택적, 기본값 지원) */
    ret = of_property_read_u32(dev->of_node, "fifo-threshold", &fifo_thr);
    if (ret)
        fifo_thr = priv->hw->fifo_depth / 2;  /* DT에 없으면 기본값 */

    platform_set_drvdata(pdev, priv);

    dev_info(dev, "probed: fifo=%d dma=%d irq=%d\n",
             priv->hw->fifo_depth, priv->hw->has_dma, priv->irq);
    return 0;
}

static void my_remove(struct platform_device *pdev)
{
    struct my_dev *priv = platform_get_drvdata(pdev);
    reset_control_assert(priv->rst);
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_ids,         /* DT 매칭 테이블 등록 */
        .pm = &my_pm_ops,                    /* 전원 관리 (선택) */
    },
};
module_platform_driver(my_driver);

/*
 * 매칭 순서 (우선순위):
 * 1. of_match_table  — Device Tree compatible 매칭
 * 2. acpi_match_table — ACPI _HID 매칭
 * 3. id_table         — platform_device_id 이름 매칭
 * 4. driver.name      — platform_device.name 직접 비교 (폴백)
 */
fwnode API (v4.13+): Device Tree와 ACPI 양쪽을 지원하는 드라이버는 of_* 대신 device_property_read_*(fwnode) API를 사용하면 DT/ACPI 코드를 통일할 수 있습니다. 예: device_property_read_u32(dev, "fifo-depth", &val)

특수 노드와 고급 패턴

/* ===== 주요 특수 노드들 ===== */

/* 1. reserved-memory — 커널이 사용하지 않을 메모리 영역 */
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    /* CMA (Contiguous Memory Allocator) 영역 */
    linux,cma {
        compatible = "shared-dma-pool";
        reusable;
        size = <0x0 0x10000000>;   /* 256 MiB */
        linux,cma-default;
    };

    /* 펌웨어 전용 영역 */
    fw_reserved: framebuffer@be000000 {
        reg = <0x0 0xbe000000 0x0 0x2000000>;
        no-map;                  /* 커널이 매핑하지 않음 */
    };
};

/* 2. GPIO hog — 부팅 시 GPIO를 고정 상태로 설정 */
&gpio1 {
    led-hog {
        gpio-hog;
        gpios = <10 GPIO_ACTIVE_HIGH>;
        output-high;
        line-name = "status-led";
    };
};

/* 3. 클럭/레귤레이터 고정 정의 (물리 클럭을 DT에서 선언) */
osc24m: oscillator-24m {
    compatible = "fixed-clock";
    #clock-cells = <0>;
    clock-frequency = <24000000>;     /* 24 MHz */
    clock-output-names = "osc24m";
};

reg_3v3: regulator-3v3 {
    compatible = "regulator-fixed";
    regulator-name = "vcc-3v3";
    regulator-min-microvolt = <3300000>;
    regulator-max-microvolt = <3300000>;
    regulator-always-on;
};

/* 4. OPP (Operating Performance Points) 테이블 */
cpu_opp_table: opp-table {
    compatible = "operating-points-v2";

    opp-600000000 {
        opp-hz = /bits/ 64 <600000000>;
        opp-microvolt = <900000>;
    };
    opp-1200000000 {
        opp-hz = /bits/ 64 <1200000000>;
        opp-microvolt = <1100000>;
    };
    opp-1800000000 {
        opp-hz = /bits/ 64 <1800000000>;
        opp-microvolt = <1300000>;
        opp-suspend;                 /* suspend 시 이 OPP 사용 */
    };
};

/* 5. 인터럽트 매핑 (interrupt-map) — PCI 등 */
pcie@10000000 {
    interrupt-map-mask = <0x1800 0 0 7>;
    interrupt-map =
        <0x0000 0 0 1 &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
        <0x0000 0 0 2 &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>;
};

Device Tree 디버깅

# ===== 실행 중인 시스템에서 DT 확인 =====

# Live Device Tree (procfs)
$ ls /proc/device-tree/
#address-cells  cpus     memory@80000000  soc
#size-cells     chosen   model            compatible

# 특정 노드의 프로퍼티 읽기
$ cat /proc/device-tree/model
MyVendor MyBoard Rev.A
$ hexdump -C /proc/device-tree/soc/serial@1c28000/reg
00000000  01 c2 80 00 00 00 04 00

# sysfs를 통한 접근 (동일한 데이터)
$ ls /sys/firmware/devicetree/base/
$ cat /sys/firmware/devicetree/base/compatible

# ===== 커널 로그에서 DT 관련 메시지 =====
$ dmesg | grep -iE 'device.?tree|of_|dts|dtb|compatible'
OF: fdt: Machine model: MyVendor MyBoard Rev.A
OF: fdt: Ignoring memory range 0x0 - 0x80000000

# ===== probe 실패 디버깅 =====

# 매칭되지 않은(드라이버 없는) 디바이스 확인
$ ls /sys/bus/platform/devices/
# 1c28000.serial  1c2ac00.i2c  ...

# 특정 디바이스의 드라이버 바인딩 상태
$ ls -la /sys/bus/platform/devices/1c28000.serial/driver
# symlink → 해당 드라이버 (없으면 매칭 실패)

# deferred probe 목록 (의존성 대기 중)
$ cat /sys/kernel/debug/devices_deferred
# 1c2ac00.i2c  ← 클럭/레귤레이터 등 의존성 미충족

# 드라이버 강제 바인드/언바인드
$ echo "1c28000.serial" > /sys/bus/platform/drivers/my-device/bind
$ echo "1c28000.serial" > /sys/bus/platform/drivers/my-device/unbind

# ===== Overlay 상태 확인 =====
$ ls /sys/kernel/config/device-tree/overlays/
my-hat/
$ cat /sys/kernel/config/device-tree/overlays/my-hat/status
applied

# ===== ftrace로 DT 매칭 추적 =====
$ echo 1 > /sys/kernel/tracing/events/bus/bus_add_device/enable
$ echo 1 > /sys/kernel/tracing/events/bus/driver_bound/enable
$ cat /sys/kernel/tracing/trace_pipe
# bus_add_device: device 1c28000.serial
# driver_bound: device 1c28000.serial driver my-device

# ===== DT Validation (빌드 시) =====
$ make ARCH=arm64 dt_binding_check    # YAML 스키마 검증
$ make ARCH=arm64 dtbs_check          # DTB vs 바인딩 검증
$ make ARCH=arm64 W=1 dtbs            # 경고 활성화 빌드
DT 작성 시 주의사항:
  • compatible 문자열은 가장 구체적인 것을 먼저, 일반적인 폴백을 나중에 기술합니다
  • status = "disabled"인 노드는 드라이버가 probe되지 않습니다. .dtsi에서 기본 disabled → .dts에서 필요한 것만 "okay"
  • reg 프로퍼티의 해석은 부모의 #address-cells/#size-cells에 따라 달라집니다. 실수하면 잘못된 주소로 매핑
  • phandle 참조(&label)는 레이블이 정의된 노드를 가리킵니다. 존재하지 않는 레이블은 컴파일 오류
  • 새로운 바인딩은 반드시 YAML 스키마를 작성하고 dt_binding_check로 검증해야 합니다
  • Overlay 사용 시 dtc -@로 기본 DTB를 컴파일해야 __symbols__ 노드가 포함되어 런타임 심볼 해석이 가능합니다

FDT 바이너리 포맷 (Flattened Device Tree)

DTB 파일은 Flattened Device Tree(FDT) 바이너리 포맷으로 저장됩니다. 부트로더가 이 바이너리를 메모리에 로드하고, 커널의 unflatten_device_tree()가 파싱하여 struct device_node 트리를 구축합니다.

DTB (Flattened Device Tree) 메모리 레이아웃 struct fdt_header (40 bytes) magic: 0xD00DFEED totalsize | off_dt_struct | off_dt_strings off_mem_rsvmap | version(17) | boot_cpuid_phys off_dt_struct → Structure Block 시작 off_dt_strings → Strings Block 시작 off_mem_rsvmap → Reserved Map 시작 totalsize → DTB 전체 크기 Memory Reservation Block { address(u64), size(u64) } 쌍의 배열 — {0,0}으로 종료 0x28 Structure Block FDT_BEGIN_NODE (0x01) + name + padding FDT_PROP (0x03) + len + nameoff + data + padding FDT_BEGIN_NODE (자식) ... FDT_END_NODE FDT_END_NODE (0x02) FDT_END (0x09) FDT_PROP 구조: nameoff → Strings Block의 프로퍼티 이름 오프셋 Strings Block "compatible\0reg\0interrupts\0status\0..." (NUL 종료 문자열) Free Space (Overlay 확장 여유) totalsize
DTB 바이너리 포맷 — fdt_header가 각 블록의 오프셋을 지정
/* ===== FDT 헤더 구조체 (include/linux/libfdt_env.h → scripts/dtc/libfdt/) ===== */

struct fdt_header {
    fdt32_t magic;             /* 0xD00DFEED (big-endian) */
    fdt32_t totalsize;          /* DTB 전체 크기 (bytes) */
    fdt32_t off_dt_struct;      /* Structure Block 시작 오프셋 */
    fdt32_t off_dt_strings;     /* Strings Block 시작 오프셋 */
    fdt32_t off_mem_rsvmap;     /* Memory Reservation Block 오프셋 */
    fdt32_t version;            /* 포맷 버전 (현재 17) */
    fdt32_t last_comp_version;  /* 호환 가능한 최소 버전 (16) */
    fdt32_t boot_cpuid_phys;   /* 부팅 CPU의 physical ID */
    fdt32_t size_dt_strings;   /* Strings Block 크기 */
    fdt32_t size_dt_struct;    /* Structure Block 크기 */
};

/* FDT는 모두 big-endian으로 저장됨 — cpu_to_fdt32() / fdt32_to_cpu() 로 변환 */

/* ===== Structure Block 토큰 ===== */
#define FDT_BEGIN_NODE  0x00000001  /* 노드 시작 + 이름(NUL종료, 4-byte 정렬) */
#define FDT_END_NODE    0x00000002  /* 노드 종료 */
#define FDT_PROP        0x00000003  /* 프로퍼티: len(u32) + nameoff(u32) + data */
#define FDT_NOP         0x00000004  /* 무시 (편집 시 패딩용) */
#define FDT_END         0x00000009  /* Structure Block 종료 */

/* ===== Memory Reservation Block =====
 * 커널이 사용하면 안 되는 물리 메모리 영역 (예: DTB 자체, 펌웨어 영역)
 * { uint64_t address; uint64_t size; } 쌍의 배열
 * address=0, size=0 엔트리로 종료
 *
 * 참고: reserved-memory DT 노드와 다름!
 * - Memory Reservation Block: FDT 바이너리 레벨, early boot에서 처리
 * - reserved-memory 노드: DT 노드 레벨, memblock 서브시스템에서 처리
 */

/* ===== FDT 프로퍼티 인코딩 예시 =====
 *
 * DTS: compatible = "myvendor,my-soc-uart", "snps,dw-apb-uart";
 *
 * Structure Block에 저장되는 바이너리:
 * [FDT_PROP]                       ← 0x00000003
 * [len = 39]                       ← 두 문자열 + NUL 포함 길이
 * [nameoff = 0]                    ← Strings Block에서 "compatible" 오프셋
 * "myvendor,my-soc-uart\0snps,dw-apb-uart\0"  ← 실제 데이터
 * [padding]                        ← 4-byte 정렬 맞춤
 *
 * DTS: reg = <0x1c28000 0x400>;
 *
 * [FDT_PROP]
 * [len = 8]                        ← u32 × 2 = 8 bytes
 * [nameoff = 11]                   ← Strings Block에서 "reg" 오프셋
 * [0x01C28000] [0x00000400]        ← big-endian u32 값들
 */

/* ===== 커널에서 FDT 직접 접근 (early boot) ===== */
#include <linux/of_fdt.h>

/* early_init_dt_scan(): 부팅 초기에 FDT에서 핵심 정보 추출 */
void __init early_init_dt_scan_nodes(void)
{
    /* chosen 노드에서 bootargs, initrd 위치 추출 */
    early_init_dt_scan_chosen(boot_command_line);

    /* /memory 노드에서 물리 메모리 범위 추출 → memblock에 등록 */
    early_init_dt_scan_memory();

    /* root 노드에서 #address-cells, #size-cells 가져오기 */
    early_init_dt_scan_root();
}

/* unflatten: FDT 바이너리 → struct device_node 트리 변환 */
void __init unflatten_device_tree(void)
{
    /* 1차 패스: 필요한 메모리 크기 계산 */
    /* 2차 패스: device_node + property 구조체 할당 및 연결 */
    __unflatten_device_tree(initial_boot_params, NULL,
                           &of_root, early_init_dt_alloc_memory_arch, false);

    /* of_root: 전역 루트 device_node 포인터 */
    /* /proc/device-tree/와 /sys/firmware/devicetree/base/로 노출 */
}

struct device_node / struct property 내부 구조

unflatten_device_tree() 완료 후 커널 메모리에 존재하는 자료구조입니다. 모든 of_* API는 이 구조체를 통해 DT 정보에 접근합니다.

/* include/linux/of.h */

struct device_node {
    const char *name;            /* 노드 이름 (@ 앞 부분) */
    phandle phandle;              /* 고유 식별자 (phandle 프로퍼티 값) */
    const char *full_name;        /* 전체 경로명 또는 name[@unit-address] */
    struct fwnode_handle fwnode;   /* 펌웨어 노드 추상화 (DT/ACPI 통합) */

    struct property *properties;  /* 프로퍼티 연결 리스트 헤드 */
    struct property *deadprops;   /* 제거된 프로퍼티 (overlay undo용) */

    /* 트리 탐색 포인터 */
    struct device_node *parent;   /* 부모 노드 */
    struct device_node *child;    /* 첫 번째 자식 */
    struct device_node *sibling;  /* 다음 형제 */

#if defined(CONFIG_OF_KOBJ)
    struct kobject kobj;           /* sysfs 표현 (/sys/firmware/devicetree/) */
#endif
    unsigned long _flags;          /* OF_POPULATED, OF_DETACHED 등 */
    void *data;                    /* 드라이버 private 데이터 */
};

/* 플래그 상수 */
#define OF_DYNAMIC       1  /* overlay로 동적 생성된 노드 */
#define OF_DETACHED      2  /* 트리에서 분리된 노드 */
#define OF_POPULATED     3  /* platform_device가 이미 생성됨 */
#define OF_POPULATED_BUS 4  /* 자식 디바이스들도 생성됨 */

struct property {
    char *name;                   /* 프로퍼티 이름 ("compatible", "reg" 등) */
    int length;                    /* 값의 바이트 길이 */
    void *value;                   /* 프로퍼티 값 (raw 바이트) */
    struct property *next;        /* 같은 노드의 다음 프로퍼티 */
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
    unsigned long _flags;
#endif
#if defined(CONFIG_OF_KOBJ)
    struct bin_attribute attr;    /* sysfs 바이너리 속성 */
#endif
};

/* ===== device_node 트리 순회 매크로 ===== */

/* 모든 자식 노드 순회 */
for_each_child_of_node(parent, child) { ... }

/* available(status != "disabled") 자식만 순회 */
for_each_available_child_of_node(parent, child) { ... }

/* 특정 compatible을 가진 노드만 순회 */
for_each_compatible_node(dn, type, compatible) { ... }

/* 특정 프로퍼티를 가진 노드 순회 */
for_each_node_with_property(dn, prop_name) { ... }

/* of_node 참조 카운팅 */
struct device_node *np = of_node_get(node);  /* refcount++ */
of_node_put(np);                                /* refcount-- */
/* for_each_* 매크로는 루프 내에서 자동으로 get/put 처리
 * 주의: break로 루프를 탈출하면 of_node_put()을 수동 호출해야 함! */

/* ===== 노드 → platform_device 변환 흐름 =====
 *
 * 1. unflatten_device_tree() → device_node 트리 구축
 * 2. of_platform_default_populate()
 *    → 루트의 direct children 중 compatible 있는 노드를 platform_device로 생성
 *    → "simple-bus", "simple-mfd", "isa", "arm,amba-bus" compatible의 노드는
 *       재귀적으로 자식도 platform_device로 생성
 * 3. 각 platform_device의 compatible과 등록된 platform_driver의 of_match_table 비교
 * 4. 매칭 성공 → driver->probe() 호출
 * 5. probe 시 의존성(clk, regulator 등) 미충족이면 -EPROBE_DEFER 반환
 *    → 나중에 재시도 (deferred probe)
 */

주소 변환 (Address Translation) 상세

Device Tree에서 각 버스 레벨마다 독립적인 주소 공간을 가집니다. ranges 프로퍼티가 자식 주소 공간 → 부모 주소 공간으로의 변환 규칙을 정의합니다.

DT 주소 변환: ranges 프로퍼티 동작 / (루트): #address-cells=<2>, #size-cells=<2> CPU 물리 주소 공간 (64-bit) soc: #address-cells=<1>, #size-cells=<1> ranges = <0x0 0x0 0x0 0x40000000>; child_addr(1 cell) → parent_addr(2 cells): 0x0 → 0x0_0000_0000, size=1GiB serial@1c28000: reg=<0x1c28000 0x400> 로컬 주소 0x01C28000 → CPU 주소 0x0_01C28000 pcie@10000000: #address-cells=<3> PCI 주소 공간 → CPU 주소 공간 변환 ranges = <0x02000000 0x0 0x20000000 0x0 0x20000000 0x0 0x10000000>; PCI MEM 0x20000000 → CPU 0x20000000 (256MiB)
주소 변환 체인 — 각 bus 레벨의 ranges가 자식→부모 주소를 변환
/* ===== ranges 프로퍼티 해석 규칙 =====
 *
 * ranges = < child_addr  parent_addr  length >;
 *
 * - child_addr의 셀 수 = 현재 노드의 #address-cells
 * - parent_addr의 셀 수 = 부모 노드의 #address-cells
 * - length의 셀 수 = 현재 노드의 #size-cells
 * - 빈 ranges (ranges;) → 1:1 매핑 (주소 동일)
 * - ranges 없음 → 자식 주소를 부모 주소로 변환 불가 (독립 주소 공간)
 */

/* 예시 1: 단순 SoC 버스 — 오프셋 변환 */
/ {
    #address-cells = <2>;   /* 루트: 64-bit 주소 */
    #size-cells = <2>;

    soc {
        compatible = "simple-bus";
        #address-cells = <1>; /* SoC: 32-bit 주소 */
        #size-cells = <1>;
        /* child(1 cell)  parent(2 cells)  size(1 cell)
         * 0x0          → 0x0_0000_0000    1 GiB 범위 */
        ranges = <0x0  0x0 0x0  0x40000000>;

        /* serial의 reg 0x1c28000은:
         * child_addr = 0x01c28000
         * ranges 적용: 0x01c28000 + 0x0 = 0x0_01c28000 (CPU 물리 주소) */
        serial@1c28000 {
            reg = <0x1c28000 0x400>;
        };
    };
};

/* 예시 2: 다중 ranges — 여러 주소 윈도우 */
soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges = <0x00000000  0x0 0x00000000  0x20000000>,  /* 0~512M: 1:1 */
             <0x40000000  0x0 0x40000000  0x20000000>;  /* 1G~1.5G */
};

/* 예시 3: PCI 주소 공간 (#address-cells = <3>) */
pcie@10000000 {
    compatible = "pci-host-ecam-generic";
    /* PCI는 #address-cells = 3: (phys.hi  phys.mid  phys.lo)
     * phys.hi 비트 구조:
     *   [31]    = relocatable
     *   [30:29] = 프리페치 (01=I/O, 10=32-bit MEM, 11=64-bit MEM)
     *   [24]    = prefetchable
     *   [23:16] = bus number
     *   [15:11] = device number
     *   [10:8]  = function number
     *   [7:0]   = register number */
    #address-cells = <3>;
    #size-cells = <2>;

    /* PCI 주소(3 cells) → CPU 주소(2 cells), 크기(2 cells) */
    ranges =
        /* I/O 공간: PCI I/O 0x0 → CPU 0x1000_0000, 64KiB */
        <0x01000000 0x0 0x00000000  0x0 0x10000000  0x0 0x00010000>,
        /* 32-bit MEM: PCI MEM 0x2000_0000 → CPU 0x2000_0000, 256MiB */
        <0x02000000 0x0 0x20000000  0x0 0x20000000  0x0 0x10000000>,
        /* 64-bit MEM (prefetchable): PCI 0x8_0000_0000 → CPU 0x8_0000_0000, 4GiB */
        <0x43000000 0x8 0x00000000  0x8 0x00000000  0x1 0x00000000>;
};

/* ===== 커널의 주소 변환 API ===== */
#include <linux/of_address.h>

/* of_translate_address(): DT 주소 → CPU 물리 주소 변환
 * ranges 체인을 루트까지 재귀적으로 따라가며 변환 */
u64 cpu_addr = of_translate_address(np, addr_prop);

/* of_address_to_resource(): reg → struct resource 변환
 * 내부적으로 of_translate_address() + 크기 정보 포함 */
struct resource res;
of_address_to_resource(np, 0, &res);  /* 첫 번째 reg 엔트리 */
/* res.start = 변환된 CPU 물리 주소
 * res.end   = start + size - 1
 * res.flags = IORESOURCE_MEM 또는 IORESOURCE_IO */

/* of_translate_dma_address(): DMA 주소 변환 (dma-ranges 사용) */
u64 dma_addr = of_translate_dma_address(np, addr_prop);

/* dma-ranges: DMA 엔진이 보는 주소 ≠ CPU 물리 주소일 때
 * 예: GPU나 DMA 컨트롤러가 IOMMU 없이 다른 주소로 메모리 접근 */
soc {
    /* DMA 주소 0x0 → CPU 물리 주소 0x8000_0000 */
    dma-ranges = <0x0  0x0 0x80000000  0x80000000>;
};

인터럽트 도메인과 Nexus 노드 심화

Device Tree의 인터럽트 계층은 디바이스 트리 구조(부모-자식)와 독립적입니다. interrupt-parent가 인터럽트 도메인 트리를 형성하고, interrupt-map이 도메인 간 인터럽트 번호 변환을 수행합니다.

/* ===== 인터럽트 처리 핵심 개념 =====
 *
 * 1. interrupt-controller: 이 노드가 인터럽트 컨트롤러임을 선언 (빈 프로퍼티)
 * 2. #interrupt-cells: 자식이 interrupts에 넣는 셀 수 (GIC=3, GPIO=2 등)
 * 3. interrupt-parent: 인터럽트를 수신할 컨트롤러 (생략 시 DT 부모에서 상속)
 * 4. interrupts: 인터럽트 지정자 (해석은 컨트롤러의 #interrupt-cells에 의존)
 * 5. interrupt-map: 인터럽트 도메인 간 변환 (nexus 노드에서 사용)
 */

/* ===== GIC (ARM Generic Interrupt Controller) ===== */
gic: interrupt-controller@1c81000 {
    compatible = "arm,gic-400";
    interrupt-controller;
    #interrupt-cells = <3>;
    /* 셀 해석:
     * [0] type: 0=SPI(Shared), 1=PPI(Private Per-Processor)
     * [1] irq number: SPI=0~987, PPI=0~15 (GIC HW IRQ = SPI+32, PPI+16)
     * [2] flags: 1=rising edge, 2=falling edge, 4=level high, 8=level low */
    reg = <0x1c81000 0x1000>,
          <0x1c82000 0x2000>;
};

/* ===== GPIO 인터럽트 컨트롤러 (계층적) ===== */
gpio0: gpio@1c20800 {
    compatible = "myvendor,my-soc-gpio";
    reg = <0x1c20800 0x40>;
    /* GPIO 컨트롤러이면서 인터럽트 컨트롤러 */
    gpio-controller;
    #gpio-cells = <2>;
    interrupt-controller;
    #interrupt-cells = <2>;
    /* 셀 해석: [0] GPIO 핀 번호, [1] 트리거 타입 (IRQ_TYPE_*) */

    /* 이 GPIO 컨트롤러의 인터럽트가 GIC로 전달됨 */
    interrupt-parent = <&gic>;
    interrupts = <GIC_SPI 11 IRQ_TYPE_LEVEL_HIGH>;
};

/* GPIO 핀을 인터럽트로 사용하는 디바이스 */
button@0 {
    compatible = "gpio-keys";
    interrupt-parent = <&gpio0>;     /* GIC가 아닌 GPIO 컨트롤러로! */
    interrupts = <7 IRQ_TYPE_EDGE_FALLING>;  /* GPIO 핀 7, 하강 에지 */
};

/* ===== interrupt-map (Nexus 노드) =====
 *
 * PCI, USB 등의 버스에서 디바이스 인터럽트를 부모 컨트롤러로 변환합니다.
 * nexus 노드: interrupt-controller는 아니지만 interrupt-map으로 변환 수행
 */
pcie@10000000 {
    /* PCI 인터럽트: INTA=1, INTB=2, INTC=3, INTD=4 */
    #interrupt-cells = <1>;

    /* interrupt-map-mask: 매칭에 사용할 비트 마스크
     * PCI 주소(3 cells) + 인터럽트(1 cell) 총 4 cells
     * device 번호(bit 15:11)와 인터럽트 번호만 매칭 */
    interrupt-map-mask = <0xf800 0 0 7>;

    /* interrupt-map: (child_unit_addr  child_irq  parent  parent_irq)
     * child_unit_addr: #address-cells 만큼의 셀 (AND mask 적용 후 비교)
     * child_irq: #interrupt-cells 만큼의 셀
     * parent: phandle → 부모 인터럽트 컨트롤러
     * parent_irq: 부모의 #interrupt-cells 만큼의 셀 */
    interrupt-map =
        /* Device 0, INTA → GIC SPI 100 */
        <0x0000 0 0 1  &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 0, INTB → GIC SPI 101 */
        <0x0000 0 0 2  &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 0, INTC → GIC SPI 102 */
        <0x0000 0 0 3  &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 0, INTD → GIC SPI 103 */
        <0x0000 0 0 4  &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>,
        /* Device 1, INTA → GIC SPI 104 (rotation: INTB부터 시작) */
        <0x0800 0 0 1  &gic GIC_SPI 101 IRQ_TYPE_LEVEL_HIGH>,
        <0x0800 0 0 2  &gic GIC_SPI 102 IRQ_TYPE_LEVEL_HIGH>,
        <0x0800 0 0 3  &gic GIC_SPI 103 IRQ_TYPE_LEVEL_HIGH>,
        <0x0800 0 0 4  &gic GIC_SPI 100 IRQ_TYPE_LEVEL_HIGH>;

    /* PCI 인터럽트 회전(swizzle):
     * IRQ = (device_slot + interrupt_pin - 1) % 4 + 1
     * 이를 통해 여러 디바이스의 인터럽트가 4개 GIC IRQ에 분산 */
};

/* ===== interrupts-extended: 여러 컨트롤러의 인터럽트를 한 노드에서 사용 ===== */
my-device {
    /* interrupt-parent + interrupts는 하나의 컨트롤러만 가능.
     * interrupts-extended는 여러 컨트롤러의 인터럽트를 지정 가능 */
    interrupts-extended =
        <&gic GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH>,    /* GIC에서 오는 인터럽트 */
        <&gpio0 7 IRQ_TYPE_EDGE_FALLING>;           /* GPIO에서 오는 인터럽트 */
    interrupt-names = "data-irq", "wakeup-irq";
};

/* ===== 커널 인터럽트 도메인 API (drivers/irqchip/) ===== */
#include <linux/irqdomain.h>

/* irq_domain: HW IRQ 번호 → Linux virq(가상 IRQ) 번호 매핑
 * 각 인터럽트 컨트롤러가 자신의 도메인을 등록
 * DT의 interrupts 값이 HW IRQ로, irq_domain을 통해 Linux IRQ로 변환 */

struct irq_domain *domain;
domain = irq_domain_add_linear(np, nr_irqs, &my_domain_ops, priv);
/* linear: HW IRQ → virq 직접 테이블 매핑 (소규모)
 * hierarchy: 계층적 도메인 (GIC→GPIO 등 cascaded 구조) */

domain = irq_domain_create_hierarchy(parent_domain, 0, nr_irqs,
    of_fwnode_handle(np), &my_domain_ops, priv);
/* hierarchy 도메인: 인터럽트 처리가 여러 컨트롤러를 거침
 * button → GPIO IRQ 7 → GIC SPI 11 → CPU
 * 각 단계의 도메인이 HW IRQ를 변환 */

Pinctrl 서브시스템과 DT 연동

SoC의 핀 다중화(muxing)와 전기적 설정을 DT에서 선언합니다. 드라이버의 probe() 시 자동으로 pinctrl-0이 적용됩니다.

/* ===== 핀 컨트롤러 노드 (SoC .dtsi) ===== */
pio: pinctrl@1c20800 {
    compatible = "myvendor,my-soc-pinctrl";
    reg = <0x1c20800 0x400>;
    clocks = <&ccu CLK_APB1>;

    /* UART0 핀 그룹 정의 */
    uart0_pins: uart0-pins {
        pins = "PA4", "PA5";       /* TX, RX */
        function = "uart0";         /* 핀 기능 선택 (mux) */
        drive-strength = <10>;     /* mA 단위 출력 세기 */
        bias-pull-up;                /* 풀업 활성화 */
    };

    uart0_sleep_pins: uart0-sleep-pins {
        pins = "PA4", "PA5";
        function = "gpio_in";       /* sleep 시 GPIO 입력으로 */
        bias-disable;
    };

    /* I2C0 핀 그룹 */
    i2c0_pins: i2c0-pins {
        pins = "PA11", "PA12";    /* SDA, SCL */
        function = "i2c0";
        drive-strength = <10>;
        bias-pull-up;
    };

    /* SPI0 핀 그룹 + CS */
    spi0_pins: spi0-pins {
        pins = "PC0", "PC1", "PC2", "PC3"; /* CLK, MOSI, MISO, CS */
        function = "spi0";
        drive-strength = <10>;
    };

    /* GPIO 키 (외부 풀업, 내부 바이어스 없음) */
    key_pins: key-pins {
        pins = "PG7";
        function = "gpio_in";
        bias-disable;
    };
};

/* ===== 디바이스에서 pinctrl 참조 ===== */
&uart0 {
    pinctrl-names = "default", "sleep";
    pinctrl-0 = <&uart0_pins>;        /* "default" 상태 (probe 시 적용) */
    pinctrl-1 = <&uart0_sleep_pins>;   /* "sleep" 상태 (suspend 시 적용) */
    status = "okay";
};

/* pinctrl-names와 pinctrl-N은 순서 대응:
 * pinctrl-names[0] = "default" → pinctrl-0
 * pinctrl-names[1] = "sleep"   → pinctrl-1
 *
 * 커널 PM 시스템이 suspend/resume 시 자동으로 상태 전환:
 * probe → "default", suspend → "sleep", resume → "default"
 *
 * "init" 상태: probe 중에만 사용, probe 완료 후 "default"로 전환 */

/* ===== 핀 설정 바인딩 주요 프로퍼티 (vendor-independent) ===== */
/*
 * pins:            핀 이름 목록
 * groups:          핀 그룹 이름 (대체)
 * function:        핀 기능 (mux 선택)
 * bias-disable:    바이어스 없음
 * bias-pull-up:    내부 풀업 활성화
 * bias-pull-down:  내부 풀다운 활성화
 * drive-strength:  출력 드라이브 세기 (mA)
 * input-enable:    입력 활성화
 * output-high:     출력 High로 설정
 * output-low:      출력 Low로 설정
 * slew-rate:       슬루율 (0=slow, 1=fast)
 */

IOMMU와 DMA 관련 DT 프로퍼티

/* ===== IOMMU (I/O Memory Management Unit) DT 바인딩 ===== */

/* IOMMU 컨트롤러 노드 */
smmu: iommu@12c00000 {
    compatible = "arm,smmu-v2";
    reg = <0x12c00000 0x10000>;
    #iommu-cells = <1>;              /* 자식이 참조 시 stream ID 1개 */
    interrupts = <GIC_SPI 50 IRQ_TYPE_LEVEL_HIGH>;
};

/* DMA를 수행하는 디바이스에서 IOMMU 참조 */
gpu@12000000 {
    compatible = "vendor,my-gpu";
    reg = <0x12000000 0x10000>;
    iommus = <&smmu 0x100>;          /* SMMU stream ID = 0x100 */
    /* 커널이 자동으로 IOMMU 도메인을 설정하여 DMA 주소 변환 수행 */
};

ethernet@1c30000 {
    compatible = "vendor,my-eth";
    reg = <0x1c30000 0x10000>;
    iommus = <&smmu 0x200>;          /* 다른 stream ID */
};

/* ===== DMA 관련 프로퍼티 ===== */

my-device@1000 {
    compatible = "vendor,my-dev";

    /* dma-coherent: 하드웨어가 캐시 코히어런시 보장
     * → 커널이 수동 캐시 flush/invalidate 생략 (성능 향상) */
    dma-coherent;

    /* dma-ranges가 부모에 있으면 DMA 주소 ≠ CPU 주소 */

    /* DMA 컨트롤러 참조 (slave DMA 사용 시) */
    dmas = <&dma_controller 5>, <&dma_controller 6>;
    dma-names = "tx", "rx";
};

/* DMA 컨트롤러 노드 */
dma_controller: dma-controller@1c02000 {
    compatible = "myvendor,my-soc-dma";
    reg = <0x1c02000 0x1000>;
    interrupts = <GIC_SPI 27 IRQ_TYPE_LEVEL_HIGH>;
    #dma-cells = <1>;              /* 자식 참조 시 채널 번호 1개 */
    clocks = <&ccu CLK_DMA>;
    resets = <&ccu RST_DMA>;
};

/* ===== 커널에서 DMA 채널 가져오기 ===== */
#include <linux/dmaengine.h>

struct dma_chan *tx_chan, *rx_chan;
tx_chan = dma_request_chan(dev, "tx");  /* dma-names의 "tx"에 대응하는 채널 */
rx_chan = dma_request_chan(dev, "rx");  /* dma-names의 "rx"에 대응하는 채널 */

/* restricted-dma-pool: 특정 디바이스용 DMA 메모리 제한 */
reserved-memory {
    #address-cells = <2>;
    #size-cells = <2>;
    ranges;

    gpu_dma_pool: dma-pool@90000000 {
        compatible = "restricted-dma-pool";
        reg = <0x0 0x90000000 0x0 0x10000000>;  /* 256MiB */
    };
};

gpu@12000000 {
    memory-region = <&gpu_dma_pool>;  /* 이 디바이스의 DMA는 이 영역만 사용 */
};

Thermal-zones DT 바인딩

/* ===== SoC 온도 센서와 쿨링 제어를 DT에서 정의 ===== */

/* 온도 센서 노드 */
tsensor: thermal-sensor@1c25000 {
    compatible = "myvendor,my-soc-thermal";
    reg = <0x1c25000 0x400>;
    #thermal-sensor-cells = <1>;   /* 센서 인덱스 1개 (다중 존) */
    clocks = <&ccu CLK_THS>;
    resets = <&ccu RST_THS>;
};

/* 쿨링 디바이스: CPU freq 스로틀링 */
/* cpu 노드에 #cooling-cells = <2>; 추가 필요 */
&cpu0 {
    #cooling-cells = <2>;  /* min_state, max_state */
};

/* thermal-zones 노드 */
thermal-zones {
    /* 각 zone = 센서 + 트립 포인트 + 쿨링 맵 */
    cpu-thermal {
        polling-delay-passive = <250>;  /* 트립 후 폴링 주기 (ms) */
        polling-delay = <1000>;          /* 평상시 폴링 주기 (ms) */

        thermal-sensors = <&tsensor 0>;  /* 센서 0번 (CPU zone) */

        trips {
            /* 패시브 쿨링: CPU freq 스로틀 시작 */
            cpu_alert: cpu-alert {
                temperature = <75000>;  /* 75°C (밀리도) */
                hysteresis = <2000>;    /* 73°C에서 해제 */
                type = "passive";
            };
            /* 크리티컬: 시스템 셧다운 */
            cpu_crit: cpu-critical {
                temperature = <100000>; /* 100°C */
                hysteresis = <0>;
                type = "critical";
            };
            /* 핫: 능동 쿨링(팬) 시작 */
            cpu_hot: cpu-hot {
                temperature = <85000>;  /* 85°C */
                hysteresis = <5000>;
                type = "hot";
            };
        };

        cooling-maps {
            /* 75°C 이상: CPU freq 스로틀 (state 0~최대) */
            cpu-throttle {
                trip = <&cpu_alert>;
                cooling-device = <&cpu0
                    THERMAL_NO_LIMIT       /* min state */
                    THERMAL_NO_LIMIT>;    /* max state */
            };
            /* 85°C 이상: 팬 활성화 */
            fan-cooling {
                trip = <&cpu_hot>;
                cooling-device = <&fan0 0 3>;  /* 팬 레벨 0~3 */
            };
        };
    };

    gpu-thermal {
        polling-delay-passive = <250>;
        polling-delay = <1000>;
        thermal-sensors = <&tsensor 1>;  /* 센서 1번 (GPU zone) */

        trips {
            gpu_alert: gpu-alert {
                temperature = <80000>;
                hysteresis = <2000>;
                type = "passive";
            };
        };
    };
};

/* 팬 제어용 PWM 쿨링 디바이스 */
fan0: pwm-fan {
    compatible = "pwm-fan";
    pwms = <&pwm0 0 25000>;            /* PWM 채널 0, 25kHz */
    #cooling-cells = <2>;
    cooling-levels = <0 64 128 255>;  /* state 0~3의 PWM duty */
};

/* sysfs 확인 */
/* /sys/class/thermal/thermal_zone0/temp     → 현재 온도 */
/* /sys/class/thermal/thermal_zone0/type     → "cpu-thermal" */
/* /sys/class/thermal/thermal_zone0/trip_point_0_temp → 75000 */
/* /sys/class/thermal/cooling_device0/cur_state → 현재 쿨링 레벨 */

전력 도메인 (Power Domain) DT

/* ===== 전력 도메인: SoC 내 독립적으로 전원을 제어할 수 있는 영역 =====
 *
 * SoC 설계에서 GPU, DSP, ISP 등은 별도 전력 도메인에 배치되어
 * 사용하지 않을 때 완전히 전원을 차단(power gating)할 수 있습니다.
 * DT에서 이 관계를 선언하면 커널 PM 시스템이 자동 관리합니다.
 */

/* 전력 도메인 컨트롤러 (PMU, Power Management Unit) */
pmu: power-controller@1c20000 {
    compatible = "myvendor,my-soc-power";
    reg = <0x1c20000 0x100>;
    #power-domain-cells = <1>;    /* 도메인 인덱스 1개 */

    /* 서브노드 형태도 가능 (일부 SoC) */
    pd_gpu: power-domain@0 {
        reg = <0>;
        #power-domain-cells = <0>;
        clocks = <&ccu CLK_GPU>;
        resets = <&ccu RST_GPU>;
    };
    pd_dsp: power-domain@1 {
        reg = <1>;
        #power-domain-cells = <0>;
    };
};

/* 디바이스에서 전력 도메인 참조 */
gpu@12000000 {
    compatible = "vendor,my-gpu";
    reg = <0x12000000 0x10000>;

    /* 방법 1: 인덱스 기반 (#power-domain-cells = <1>) */
    power-domains = <&pmu 0>;       /* 도메인 0 = GPU */

    /* 방법 2: 서브노드 phandle (#power-domain-cells = <0>) */
    /* power-domains = <&pd_gpu>; */

    power-domain-names = "gpu";
};

/* 여러 전력 도메인에 걸친 디바이스 */
isp@14000000 {
    compatible = "vendor,my-isp";
    reg = <0x14000000 0x10000>;
    power-domains = <&pmu 2>, <&pmu 3>;
    power-domain-names = "isp-core", "isp-io";
};

/* ===== 커널에서 전력 도메인 관리 =====
 *
 * Runtime PM과 연동:
 * - pm_runtime_get_sync() → 전력 도메인 ON (참조 카운트 기반)
 * - pm_runtime_put()      → 전력 도메인 OFF (모든 사용자가 put하면)
 *
 * 커널 내부 흐름:
 * 1. DT 파싱 → genpd(Generic Power Domain) 구조체 생성
 * 2. pm_genpd_add_device() → 디바이스를 도메인에 연결
 * 3. dev_pm_domain_attach() → probe 시 자동 호출
 * 4. Runtime PM 콜백에서 genpd_power_on/off() 자동 호출
 *
 * 디버깅:
 * $ cat /sys/kernel/debug/pm_genpd/pm_genpd_summary
 *   domain                status  /device          runtime status
 *   gpu_pd                on      /12000000.gpu    active
 *   dsp_pd                off
 */

Device Tree vs ACPI 비교

항목Device Tree (DT)ACPI
기원Open Firmware (IEEE 1275), PowerPC/SPARCIntel, x86 서버/데스크톱
주요 플랫폼ARM, RISC-V, PowerPC, MIPSx86, ARM 서버 (SBSA)
데이터 형식DTS(텍스트) → DTB(바이너리), 정적 데이터ASL(텍스트) → AML(바이코드), 실행 가능 메서드 포함
하드웨어 기술선언적 (데이터만)선언적 + 절차적 (AML 메서드 실행 가능)
런타임 수정Overlay (.dtbo, configfs)동적 테이블 로드 (SSDT), hotplug
전원 관리DT 프로퍼티 + 커널 드라이버에서 직접 구현_PS0/_PS3 메서드, _PR0 등 펌웨어가 전원 제어 수행
인터럽트interrupts, interrupt-map_CRS(Current Resource Settings) 내 IRQ 디스크립터
열 관리thermal-zones DT 노드_TMP, _PSV, _CRT, _ACx 메서드
디바이스 식별compatible 문자열_HID (Hardware ID), _CID (Compatible ID)
리소스 기술reg, interrupts, clocks 등 개별 프로퍼티_CRS 버퍼에 Memory32/IRQ/DMA 리소스 패킹
커널 APIof_*() (DT 전용)acpi_*() (ACPI 전용)
통합 APIdevice_property_*() / fwnode_*() — DT/ACPI 양쪽 지원
바인딩 문서Documentation/devicetree/bindings/ (YAML)ACPI Spec + DSDT/SSDT (벤더 구현)
검증 도구dt_binding_check, dtbs_checkiasl (Intel ASL Compiler), acpidump
fwnode API — DT/ACPI 통합 드라이버: DT와 ACPI 양쪽을 지원하는 드라이버를 작성할 때는 of_*() 대신 device_property_*() 또는 fwnode_property_*()를 사용합니다. 커널이 런타임에 DT/ACPI를 판별하여 적절한 백엔드를 호출합니다.
/* ===== fwnode API: DT/ACPI 통합 드라이버 패턴 ===== */
#include <linux/property.h>

static int my_unified_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;
    u32 val;
    const char *str;
    bool flag;

    /* of_property_read_u32() 대신 → DT/ACPI 모두 동작 */
    device_property_read_u32(dev, "fifo-depth", &val);
    device_property_read_string(dev, "label", &str);
    flag = device_property_read_bool(dev, "big-endian");

    /* fwnode 기반 자식 순회 */
    struct fwnode_handle *child;
    device_for_each_child_node(dev, child) {
        u32 reg;
        fwnode_property_read_u32(child, "reg", ®);
    }

    return 0;
}

/* DT + ACPI 듀얼 매칭 테이블 */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-dev" },
    { }
};

#ifdef CONFIG_ACPI
static const struct acpi_device_id my_acpi_ids[] = {
    { "VNDR0001", 0 },    /* _HID 매칭 */
    { }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);
#endif

static struct platform_driver my_driver = {
    .probe  = my_unified_probe,
    .driver = {
        .name = "my-device",
        .of_match_table = my_of_ids,
        .acpi_match_table = ACPI_PTR(my_acpi_ids),
    },
};

실제 SoC DTS 분석 (Raspberry Pi / Allwinner)

/* ===== 실제 커널 소스 DTS 구조 분석 =====
 *
 * 커널 소스 내 DTS 파일 위치:
 *   arch/arm64/boot/dts/broadcom/   ← Raspberry Pi 4/5
 *   arch/arm64/boot/dts/allwinner/  ← Allwinner (Pine64, OrangePi)
 *   arch/arm64/boot/dts/rockchip/   ← Rockchip (Rock5B)
 *   arch/arm64/boot/dts/amlogic/    ← Amlogic (Odroid)
 *   arch/arm64/boot/dts/freescale/  ← NXP i.MX
 *   arch/arm64/boot/dts/qcom/       ← Qualcomm
 *
 * 일반적인 .dtsi/.dts 계층 구조:
 *   SoC계열.dtsi       ← SoC 공통 (예: sun50i-h5.dtsi)
 *   └── SoC.dtsi       ← 특정 SoC (예: sun50i-h5.dtsi → sun50i-a64.dtsi 포함)
 *       └── Board.dts  ← 보드별 (예: sun50i-h5-orangepi-pc2.dts)
 */

/* ===== Raspberry Pi 4B (BCM2711) DTS 구조 분석 ===== */
/*
 * arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dts
 * └── #include "bcm2711.dtsi"
 *     └── #include "bcm283x.dtsi"  ← BCM SoC 공통
 *
 * BCM2711 특징:
 * - VideoCore GPU가 주소 공간을 관리 (VC 주소 ≠ ARM 주소)
 * - dma-ranges로 VC↔ARM 주소 변환
 * - 독자적인 인터럽트 컨트롤러 (GIC-400)
 */

/* bcm283x.dtsi 핵심 구조 (간략화) */
/ {
    compatible = "brcm,bcm2835";
    #address-cells = <1>;
    #size-cells = <1>;

    soc {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        /* ARM 주소 0x7E000000이 버스 주소 0xFE000000으로 매핑 (BCM2711) */
        ranges = <0x7e000000  0xfe000000  0x01800000>;
        /* DMA 엔진은 레거시 주소를 사용 */
        dma-ranges = <0xc0000000  0x00000000  0x40000000>;

        gpio: gpio@7e200000 {
            compatible = "brcm,bcm2711-gpio";
            reg = <0x7e200000 0xb4>;
            gpio-controller;
            #gpio-cells = <2>;
            interrupt-controller;
            #interrupt-cells = <2>;
            gpio-ranges = <&gpio 0 0 58>; /* pinctrl 연동 */
        };

        uart0: serial@7e201000 {
            compatible = "arm,pl011", "arm,primecell";
            reg = <0x7e201000 0x200>;
            clocks = <&clocks BCM2835_CLOCK_UART>,
                     <&clocks BCM2835_CLOCK_VPU>;
            clock-names = "uartclk", "apb_pclk";
            arm,primecell-periphid = <0x00241011>;
            status = "disabled";
        };
    };
};

/* bcm2711-rpi-4-b.dts에서 오버라이드 */
&uart0 {
    pinctrl-names = "default";
    pinctrl-0 = <&uart0_gpio14>;
    status = "okay";
};

/* ===== Allwinner H6 (Pine H64) DTS 구조 분석 ===== */
/*
 * arch/arm64/boot/dts/allwinner/sun50i-h6-pine-h64.dts
 * └── #include "sun50i-h6.dtsi"
 *
 * Allwinner 특징:
 * - CCU(Clock Control Unit) 드라이버가 클럭 + 리셋 모두 관리
 * - R_ 접두사 노드: Always-On 도메인 (대기 전력)
 * - MBUS: 메모리 버스 대역폭 제어
 */

/* sun50i-h6.dtsi 핵심 구조 (간략화) */
/ {
    #address-cells = <1>;
    #size-cells = <1>;

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;

        cpu0: cpu@0 {
            compatible = "arm,cortex-a53";
            device_type = "cpu";
            reg = <0>;
            enable-method = "psci";
            clocks = <&ccu CLK_CPUX>;
            operating-points-v2 = <&cpu_opp_table>;
            #cooling-cells = <2>;
        };
    };

    /* PSCI: ARM 표준 CPU 전원 관리 인터페이스 */
    psci {
        compatible = "arm,psci-1.0";
        method = "smc";  /* Secure Monitor Call */
    };

    soc@3000000 {
        compatible = "simple-bus";
        #address-cells = <1>;
        #size-cells = <1>;
        ranges = <0x0 0x03000000 0x1000000>;

        /* CCU: 클럭 + 리셋 통합 컨트롤러 */
        ccu: clock@3001000 {
            compatible = "allwinner,sun50i-h6-ccu";
            reg = <0x01000 0x1000>;
            clocks = <&osc24M>, <&rtc 0>, <&rtc 2>;
            clock-names = "hosc", "losc", "iosc";
            #clock-cells = <1>;
            #reset-cells = <1>;
        };

        /* EMAC (이더넷) — 완전한 DT 바인딩 예 */
        emac: ethernet@5020000 {
            compatible = "allwinner,sun50i-h6-emac",
                         "allwinner,sun50i-a64-emac";
            reg = <0x5020000 0x10000>;
            interrupts = <GIC_SPI 12 IRQ_TYPE_LEVEL_HIGH>;
            interrupt-names = "macirq";
            clocks = <&ccu CLK_BUS_EMAC>;
            clock-names = "stmmaceth";
            resets = <&ccu RST_BUS_EMAC>;
            reset-names = "stmmaceth";
            syscon = <&syscon>;
            status = "disabled";

            mdio: mdio {
                compatible = "snps,dwmac-mdio";
                #address-cells = <1>;
                #size-cells = <0>;
            };
        };
    };
};

/* 보드 .dts에서 활성화 + PHY 추가 */
&emac {
    pinctrl-names = "default";
    pinctrl-0 = <&ext_rgmii_pins>;
    phy-mode = "rgmii-id";
    phy-handle = <&ext_rgmii_phy>;
    phy-supply = <®_gmac_3v3>;
    status = "okay";
};

&mdio {
    ext_rgmii_phy: ethernet-phy@1 {
        compatible = "ethernet-phy-ieee802.3-c22";
        reg = <1>;               /* PHY 주소 */
        reset-gpios = <&pio 3 14 GPIO_ACTIVE_LOW>;
        reset-assert-us = <15000>;
        reset-deassert-us = <40000>;
    };
};
DTS 읽기 연습 팁: 커널 소스의 arch/arm64/boot/dts/ 디렉토리에서 실제 SoC의 .dtsi 파일을 읽어보면 DT 구조를 빠르게 이해할 수 있습니다. 특히 compatible 문자열로 커널에서 대응하는 드라이버(drivers/)를 검색하면 DT↔드라이버 연결 관계를 파악할 수 있습니다: git grep "allwinner,sun50i-h6-emac" drivers/

Managed Device Resources (devm)

devm_* API는 디바이스 해제 시 리소스를 자동으로 정리합니다. 에러 경로에서의 수동 해제가 불필요합니다.

일반 APIManaged API리소스
kmalloc()devm_kmalloc()메모리
ioremap()devm_ioremap()MMIO 매핑
request_irq()devm_request_irq()인터럽트
clk_get()devm_clk_get()클럭
regulator_get()devm_regulator_get()레귤레이터
gpio_request()devm_gpio_request()GPIO
/* devm 사용 예: 에러 시 자동 해제, goto 불필요 */
static int my_probe(struct platform_device *pdev)
{
    struct device *dev = &pdev->dev;

    void *data = devm_kzalloc(dev, 4096, GFP_KERNEL);
    if (!data) return -ENOMEM;

    void __iomem *base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(base)) return PTR_ERR(base);

    int irq = platform_get_irq(pdev, 0);
    devm_request_irq(dev, irq, my_handler, 0, "mydev", data);
    /* remove 시 모든 리소스 자동 해제 */
    return 0;
}

전원 관리 (PM)

static int my_suspend(struct device *dev)
{
    /* 하드웨어 상태 저장, 클럭 비활성화 */
    clk_disable_unprepare(priv->clk);
    return 0;
}

static int my_resume(struct device *dev)
{
    /* 하드웨어 상태 복원 */
    clk_prepare_enable(priv->clk);
    reinit_hardware(priv);
    return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(my_pm_ops, my_suspend, my_resume);

static struct platform_driver my_driver = {
    .driver = {
        .name = "my-device",
        .pm   = pm_sleep_ptr(&my_pm_ops),
    },
};

DMA 심화 — 주의사항과 고려사항

DMA 매핑 API 유형과 선택 기준

API수명캐시 일관성사용 시나리오
dma_alloc_coherent() 장기 (드라이버 수명) H/W 보장 (uncached/write-combined) 디스크립터 링, 커맨드 큐, 공유 상태
dma_map_single() 단기 (한 번의 전송) S/W sync 필요 패킷 버퍼, 단일 블록 I/O
dma_map_sg() 단기 S/W sync 필요 Scatter-Gather I/O, 대용량 전송
dma_map_page() 단기 S/W sync 필요 highmem 페이지의 DMA 전송
dma_alloc_noncoherent() 장기 S/W sync 필요 대용량 버퍼 (coherent의 성능 오버헤드 회피)
dma_pool_create() 장기 (풀 관리) H/W 보장 소규모 coherent 버퍼를 빈번하게 할당/해제

DMA 방향(Direction)과 캐시 동기화

/* DMA 방향 상수 — 반드시 정확히 지정해야 함 */
DMA_TO_DEVICE       /* CPU → Device: CPU 캐시 → 메모리 flush */
DMA_FROM_DEVICE     /* Device → CPU: 캐시 invalidate */
DMA_BIDIRECTIONAL   /* 양방향: flush + invalidate (비용 큼) */

/* Streaming DMA의 올바른 사용 패턴 */
dma_addr_t dma = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma)) {
    dev_err(dev, "DMA mapping failed\n");
    return -ENOMEM;
}

/* DMA 전송 시작 (H/W에 dma 주소 전달) */
writel(dma, hw_base + DMA_ADDR_REG);
writel(len, hw_base + DMA_LEN_REG);
writel(DMA_START, hw_base + DMA_CTRL_REG);

/* 전송 완료 후 — CPU가 데이터를 읽기 전에 반드시 sync */
dma_sync_single_for_cpu(dev, dma, len, DMA_FROM_DEVICE);
/* 이제 CPU에서 buf 데이터를 안전하게 읽을 수 있음 */
process_data(buf, len);

/* 다시 디바이스에게 버퍼를 넘기려면 */
dma_sync_single_for_device(dev, dma, len, DMA_FROM_DEVICE);

/* 최종 해제 */
dma_unmap_single(dev, dma, len, DMA_FROM_DEVICE);
DMA 방향 오지정의 위험: DMA_TO_DEVICE로 매핑한 버퍼에서 디바이스가 쓴 데이터를 CPU가 읽으면 stale cache 데이터가 반환될 수 있습니다. 캐시가 invalidate되지 않았기 때문입니다. 이런 버그는 간헐적으로 발생하여 디버깅이 매우 어렵습니다. DMA_BIDIRECTIONAL은 안전하지만 캐시 연산이 두 배이므로 성능 저하가 있습니다.

IOMMU와 DMA 주소 공간

/* IOMMU 존재 시 DMA 주소 변환 흐름:
 *
 *  CPU Virtual Addr → (MMU) → Physical Addr
 *  DMA/Bus Addr     → (IOMMU) → Physical Addr
 *
 *  IOMMU가 없으면: dma_addr_t == phys_addr_t (1:1 매핑)
 *  IOMMU가 있으면: dma_addr_t는 IOMMU가 매핑한 I/O 가상 주소
 */

/* DMA 주소 마스크 설정 — 디바이스의 주소 지정 능력 선언 */
int ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
    /* 64비트 실패 시 32비트로 fallback */
    ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
    if (ret) {
        dev_err(dev, "No suitable DMA available\n");
        return ret;
    }
}
IOMMU 구현플랫폼커널 드라이버주요 기능
Intel VT-d x86 (Intel) drivers/iommu/intel/ DMA remapping, interrupt remapping, ATS
AMD-Vi x86 (AMD) drivers/iommu/amd/ DMA remapping, interrupt remapping, v2 page table
ARM SMMU ARM/ARM64 drivers/iommu/arm/arm-smmu-v3/ Stage-1/2 변환, PCIe ATS, HTTU
SWIOTLB 모든 플랫폼 kernel/dma/swiotlb.c 소프트웨어 bounce buffer (IOMMU 없을 때 fallback)

DMA 프로그래밍 핵심 주의사항

반드시 지켜야 할 DMA 규칙:
  1. 매핑 후 에러 체크dma_mapping_error()를 항상 호출. IOMMU 공간 소진 시 실패 가능
  2. bounce buffer 인지 — SWIOTLB 사용 시 실제 복사가 발생하여 성능 저하. dma_set_mask()로 64비트 지원 확인
  3. DMA 주소 수명 — map과 unmap 사이에서만 유효. unmap 후 DMA 주소 사용 금지
  4. 캐시 라인 공유 금지 — DMA 버퍼가 다른 데이터와 같은 캐시 라인을 공유하면 false sharing 발생. ____cacheline_aligned 사용
  5. Scatter-Gather 병합 — IOMMU는 물리적으로 불연속인 페이지를 DMA 주소 공간에서 연속으로 매핑 가능. dma_map_sg()sg_dma_len()이 원래 세그먼트와 다를 수 있음
  6. DMA coherent 메모리의 성능 — ARM 등 non-x86에서 coherent 메모리는 uncached로 할당되어 CPU 접근이 느림. 대용량 데이터는 streaming DMA 선호
  7. 64비트 DMA 주소 — 레거시 디바이스는 32비트 DMA만 지원. 4GB 이상 메모리 시스템에서 SWIOTLB bounce buffer 사용됨

DMA 디버깅

# DMA 디버그 활성화 (커널 부트 파라미터)
dma_debug=on

# 또는 CONFIG_DMA_API_DEBUG=y 로 빌드

# DMA 디버그 통계 확인
cat /sys/kernel/debug/dma-api/error_count
cat /sys/kernel/debug/dma-api/all_errors
cat /sys/kernel/debug/dma-api/num_errors

# 일반적으로 검출되는 DMA 오류:
# - DMA-API: device driver frees DMA memory with wrong function
# - DMA-API: device driver maps memory from kernel text
# - DMA-API: device driver tries to sync DMA memory it has not allocated

커널 버스 서브시스템 심화

PCI/PCIe 서브시스템

#include <linux/pci.h>

/* PCI 디바이스 ID 테이블 */
static const struct pci_device_id my_pci_ids[] = {
    { PCI_DEVICE(0x8086, 0x1234) },  /* vendor, device */
    { PCI_DEVICE_CLASS(0x020000, 0xFFFF00) }, /* 클래스 매칭 */
    { 0, }
};
MODULE_DEVICE_TABLE(pci, my_pci_ids);

/* PCI 드라이버 프로브 */
static int my_pci_probe(struct pci_dev *pdev,
        const struct pci_device_id *id)
{
    int ret;

    /* 1. PCI 디바이스 활성화 */
    ret = pcim_enable_device(pdev);  /* devm 버전 */

    /* 2. BAR (Base Address Register) 매핑 */
    ret = pcim_iomap_regions(pdev, BIT(0), "my_driver");
    void __iomem *base = pcim_iomap_table(pdev)[0];

    /* 3. DMA 마스크 설정 */
    ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));

    /* 4. Bus Master 활성화 (DMA 사용 시 필수) */
    pci_set_master(pdev);

    /* 5. MSI/MSI-X 인터럽트 설정 */
    ret = pci_alloc_irq_vectors(pdev, 1, 32, PCI_IRQ_MSIX | PCI_IRQ_MSI);

    return 0;
}

static struct pci_driver my_pci_driver = {
    .name     = "my_pci",
    .id_table = my_pci_ids,
    .probe    = my_pci_probe,
};
module_pci_driver(my_pci_driver);
PCI/PCIe 드라이버 주의사항:
  • BAR 매핑ioremap() 반환값은 __iomem 포인터. readl()/writel()로만 접근 (직접 포인터 역참조 금지)
  • Config Space 접근pci_read_config_dword()로 구성 공간 읽기. 잘못된 접근은 시스템 행(hang) 유발 가능
  • MSI vs MSI-X — MSI는 최대 32개, MSI-X는 2048개 벡터. 멀티큐 디바이스에서는 MSI-X 필수
  • SR-IOVpci_enable_sriov()로 VF 생성. VF 드라이버는 PF와 별도로 바인딩
  • AER (Advanced Error Reporting) — PCIe 에러 복구 콜백 pci_error_handlers 등록 권장
  • ASPM (Active State Power Management) — 전력 절감이지만 지연 증가. 고성능 NIC에서 pcie_aspm=off 필요한 경우 있음

I2C/SMBus 서브시스템

/* === I2C (Inter-Integrated Circuit) 프로토콜 개요 ===
 *
 * Philips(NXP)가 1982년 개발한 2-wire 직렬 버스.
 * 저속 주변장치(센서, EEPROM, RTC, PMIC 등) 연결에 표준적으로 사용.
 *
 * 물리 계층:
 *   SDA (Serial Data)  — 양방향 데이터 라인
 *   SCL (Serial Clock)  — 마스터가 생성하는 클럭 라인
 *   둘 다 오픈 드레인 + 외부 풀업 저항 (보통 4.7kΩ~10kΩ)
 *   → 다중 마스터/슬레이브가 하나의 버스를 공유 (wired-AND)
 *
 * 속도 모드:
 *   Standard Mode (SM)     —  100 kbit/s    (기본)
 *   Fast Mode (FM)         —  400 kbit/s    (대부분의 센서)
 *   Fast Mode Plus (FM+)   —    1 Mbit/s    (20mA 전류 드라이버)
 *   High Speed Mode (Hs)   —  3.4 Mbit/s    (마스터 코드 필요)
 *   Ultra Fast Mode (UFm)  —    5 Mbit/s    (단방향, 푸시풀)
 *
 * 트랜잭션 포맷:
 *   [S] [ADDR(7bit)] [R/W(1bit)] [ACK] [DATA(8bit)] [ACK] ... [P]
 *
 *   S = START 조건 (SDA↓ while SCL=HIGH)
 *   P = STOP  조건 (SDA↑ while SCL=HIGH)
 *   Sr = Repeated START (STOP 없이 새 트랜잭션 시작)
 *   ACK  = 수신자가 SDA를 LOW로 당김 (정상)
 *   NACK = 수신자가 응답하지 않음 (SDA=HIGH 유지)
 *
 * 7-bit 주소: 0x00~0x7F (유효 범위 0x08~0x77, 나머지는 예약)
 * 10-bit 주소: 첫 바이트 11110XX + 두 번째 바이트 8bit (드물게 사용)
 *
 * Clock Stretching:
 *   슬레이브가 SCL을 LOW로 유지하여 마스터를 대기시킴
 *   → 느린 디바이스의 처리 시간 확보
 *   → 하드웨어 I2C 컨트롤러가 지원해야 함
 *
 * Multi-Master:
 *   여러 마스터가 동시에 전송 시도 → 중재(Arbitration)
 *   SDA에서 0(LOW)을 보낸 마스터가 1(HIGH)을 읽으면 → 중재 패배, 버스 양보
 */
/* === SMBus vs I2C 차이 ===
 *
 * SMBus(System Management Bus)는 Intel이 1995년 정의한 I2C의 부분집합.
 * PC 메인보드의 전원 관리, 팬 제어, 온도 센서, DIMM SPD EEPROM 등에 사용.
 *
 * ┌────────────────────┬──────────────────────┬──────────────────────┐
 * │     항목           │       I2C            │       SMBus          │
 * ├────────────────────┼──────────────────────┼──────────────────────┤
 * │ 전압 범위          │ Vdd 자유 (1.8~5V)   │ 1.8V 또는 3.3V 고정 │
 * │ 최대 속도          │ 3.4 Mbit/s (Hs)     │ 100kHz (1.0)         │
 * │                    │                      │ 400kHz (2.0)         │
 * │                    │                      │ 1MHz (3.0)           │
 * │ 클럭 최소 주파수   │ 없음 (DC 허용)       │ 10kHz (타임아웃)     │
 * │ 타임아웃           │ 없음                 │ 25~35ms SCL LOW      │
 * │ 최대 데이터 크기   │ 제한 없음            │ 32바이트 (블록)      │
 * │ 주소 해석 (ARP)    │ 없음                 │ 지원 (동적 주소 할당)│
 * │ 패킷 에러 체크     │ 없음                 │ PEC (CRC-8) 선택적   │
 * │ Alert 핀           │ 없음                 │ SMBALERT# (인터럽트)  │
 * │ Host Notify        │ 없음                 │ 슬레이브→마스터 알림 │
 * └────────────────────┴──────────────────────┴──────────────────────┘
 *
 * 리눅스 커널에서의 권장:
 *   - 디바이스가 SMBus 호환이면 → i2c_smbus_*() 함수 사용
 *   - SMBus 전용 컨트롤러(Intel PCH 등)에서도 동작 보장
 *   - Raw I2C 필요 시에만 → i2c_transfer() 사용
 *     (SMBus 전용 컨트롤러에서는 동작 안 할 수 있음)
 */

/* === SMBus 프로토콜 명령 유형 ===
 *
 * Quick Command:     [S][Addr][R/W][A][P]           (데이터 없이 R/W 비트만)
 * Send Byte:         [S][Addr][W][A][Data][A][P]    (1바이트 전송)
 * Receive Byte:      [S][Addr][R][A][Data][NA][P]   (1바이트 수신)
 * Write Byte:        [S][Addr][W][A][Cmd][A][Data][A][P]
 * Read Byte:         [S][Addr][W][A][Cmd][A][Sr][Addr][R][A][Data][NA][P]
 * Write Word:        [S][Addr][W][A][Cmd][A][DataLow][A][DataHigh][A][P]
 * Read Word:         Write Cmd → Repeated Start → Read 2 bytes
 * Block Write:       [S][Addr][W][A][Cmd][A][Count][A][Data0]...[DataN][A][P]
 * Block Read:        Write Cmd → Sr → Read Count + Data bytes
 * I2C Block:         커널 확장 (Count 바이트 없이 직접 블록 전송)
 * Process Call:      Write Word → Sr → Read Word (요청-응답)
 * Block Process Call: Block Write → Sr → Block Read
 *
 * Cmd = 커맨드/레지스터 주소 (8-bit)
 * Count = 이후 데이터 바이트 수 (1~32)
 * PEC = 선택적 CRC-8 체크 바이트 (마지막에 추가)
 */
/* === 리눅스 I2C 서브시스템 아키텍처 ===
 *
 * 소스: drivers/i2c/
 *
 * ┌───────────────────────────────────────────────────┐
 * │            사용자 공간 (i2c-tools, 응용 프로그램)  │
 * │   i2cdetect / i2cdump / i2cget / i2cset           │
 * ├────────────────────┬──────────────────────────────┤
 * │  /dev/i2c-N        │  sysfs (/sys/bus/i2c/)       │
 * │  (i2c-dev 캐릭터)  │  (디바이스/드라이버 바인딩)  │
 * ├────────────────────┴──────────────────────────────┤
 * │              I2C Core (drivers/i2c/i2c-core-*.c)  │
 * │   i2c_transfer() / i2c_smbus_xfer()               │
 * │   디바이스 등록/매칭, 버스 잠금, 재시도 로직       │
 * ├───────────────────────────────────────────────────┤
 * │   I2C Adapter Driver (I2C 컨트롤러 H/W 드라이버)  │
 * │   drivers/i2c/busses/i2c-designware-*.c (Intel)   │
 * │   drivers/i2c/busses/i2c-bcm2835.c (Raspberry Pi) │
 * │   drivers/i2c/busses/i2c-imx.c (i.MX)             │
 * │   drivers/i2c/busses/i2c-i801.c (Intel PCH SMBus) │
 * ├───────────────────────────────────────────────────┤
 * │              하드웨어 I2C/SMBus 컨트롤러           │
 * └───────────────────────────────────────────────────┘
 *
 * 핵심 구조체:
 *   struct i2c_adapter  — I2C 버스(컨트롤러) 인스턴스
 *   struct i2c_algorithm — 컨트롤러의 전송 함수 (master_xfer, smbus_xfer)
 *   struct i2c_client   — 버스에 연결된 I2C 슬레이브 디바이스
 *   struct i2c_driver   — 슬레이브 디바이스 드라이버
 *   struct i2c_msg      — 단일 I2C 메시지 (방향, 주소, 버퍼, 길이)
 */
#include <linux/i2c.h>

/* === I2C 디바이스 드라이버 (클라이언트 드라이버) === */

/* 디바이스 ID 테이블 (platform 매칭용) */
static const struct i2c_device_id my_i2c_ids[] = {
    { "my_sensor", 0 },
    { "my_sensor_v2", 1 },  /* driver_data로 변형 구분 */
    { }
};
MODULE_DEVICE_TABLE(i2c, my_i2c_ids);

/* Device Tree 매칭 */
static const struct of_device_id my_of_ids[] = {
    { .compatible = "vendor,my-sensor", .data = (void *)0 },
    { .compatible = "vendor,my-sensor-v2", .data = (void *)1 },
    { }
};
MODULE_DEVICE_TABLE(of, my_of_ids);

/* ACPI 매칭 (x86 플랫폼) */
static const struct acpi_device_id my_acpi_ids[] = {
    { "VNDR0001", 0 },  /* ACPI _HID */
    { }
};
MODULE_DEVICE_TABLE(acpi, my_acpi_ids);

static int my_i2c_probe(struct i2c_client *client)
{
    u8 reg_val;
    s32 ret;

    /* SMBus 기능 확인 */
    if (!i2c_check_functionality(client->adapter,
            I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA)) {
        dev_err(&client->dev, "SMBus byte/word 미지원\n");
        return -ENODEV;
    }

    /* === SMBus 전송 함수 (권장) === */

    /* 1바이트 레지스터 읽기 */
    ret = i2c_smbus_read_byte_data(client, 0x00);
    if (ret < 0)
        return ret;
    reg_val = ret & 0xFF;

    /* 1바이트 레지스터 쓰기 */
    ret = i2c_smbus_write_byte_data(client, 0x01, 0xFF);

    /* 2바이트(word) 읽기/쓰기 */
    s32 word = i2c_smbus_read_word_data(client, 0x02);
    i2c_smbus_write_word_data(client, 0x02, 0x1234);

    /* 블록 읽기 (최대 I2C_SMBUS_BLOCK_MAX = 32 바이트) */
    u8 block[I2C_SMBUS_BLOCK_MAX];
    ret = i2c_smbus_read_i2c_block_data(client, 0x10,
                                          sizeof(block), block);

    /* === Raw I2C 전송 (SMBus로 불가능한 경우) === */
    u8 reg_addr = 0x20;
    u8 buf[256];
    struct i2c_msg msgs[2] = {
        {   /* 첫 번째 메시지: 레지스터 주소 쓰기 */
            .addr = client->addr,
            .flags = 0,          /* 쓰기 */
            .len = 1,
            .buf = ®_addr,
        },
        {   /* 두 번째 메시지: 데이터 읽기 (Repeated START) */
            .addr = client->addr,
            .flags = I2C_M_RD,   /* 읽기 */
            .len = sizeof(buf),
            .buf = buf,
        },
    };
    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2)  /* 반환값 = 성공한 메시지 수 */
        return ret < 0 ? ret : -EIO;

    return 0;
}

static struct i2c_driver my_i2c_driver = {
    .driver = {
        .name = "my_sensor",
        .of_match_table = my_of_ids,
        .acpi_match_table = my_acpi_ids,
        .pm = &my_pm_ops,          /* 전원 관리 콜백 */
    },
    .probe = my_i2c_probe,
    .id_table = my_i2c_ids,
};
module_i2c_driver(my_i2c_driver);
/* module_i2c_driver(): module_init + module_exit +
 * i2c_add_driver / i2c_del_driver를 자동 생성하는 매크로 */
/* === regmap을 통한 I2C 레지스터 접근 (권장 패턴) ===
 *
 * 소스: drivers/base/regmap/regmap-i2c.c
 *
 * regmap은 I2C/SPI/MMIO 등 다양한 버스에 대해 통일된 레지스터 접근 API를 제공.
 * 캐싱, 바이트 순서 변환, 범위 검증, 디버깅을 자동 처리.
 */
#include <linux/regmap.h>

static const struct regmap_config my_regmap_config = {
    .reg_bits = 8,               /* 레지스터 주소 비트 수 */
    .val_bits = 8,               /* 레지스터 값 비트 수 */
    .max_register = 0xFF,        /* 최대 레지스터 주소 (범위 검증) */
    .cache_type = REGCACHE_RBTREE, /* 레지스터 캐시 (suspend/resume 복원) */
    .volatile_reg = my_volatile,  /* 캐시하지 않을 레지스터 (상태 레지스터 등) */
};

static int my_probe(struct i2c_client *client)
{
    struct regmap *regmap;
    unsigned int val;

    regmap = devm_regmap_init_i2c(client, &my_regmap_config);
    if (IS_ERR(regmap))
        return PTR_ERR(regmap);

    /* 통일된 읽기/쓰기 API */
    regmap_read(regmap, 0x00, &val);
    regmap_write(regmap, 0x01, 0xFF);

    /* 비트 조작 (read-modify-write 원자적 수행) */
    regmap_update_bits(regmap, 0x02,
        0x30,    /* mask: 비트 4,5 */
        0x20);   /* val:  비트 5만 설정 */

    /* 벌크 읽기/쓰기 */
    regmap_bulk_read(regmap, 0x10, buf, 16);
    regmap_bulk_write(regmap, 0x20, data, 8);

    return 0;
}

/* suspend 시 regmap 캐시 자동 활용:
 * regcache_cache_only(regmap, true)  → H/W 접근 차단, 캐시만 사용
 * regcache_mark_dirty(regmap)        → 모든 캐시를 dirty 표시
 * regcache_cache_only(regmap, false) → H/W 접근 재개
 * regcache_sync(regmap)              → dirty 레지스터를 H/W에 기록 (resume) */
/* === I2C Adapter (컨트롤러) 드라이버 ===
 *
 * I2C 컨트롤러 하드웨어를 구동하는 드라이버.
 * i2c_algorithm을 구현하여 I2C core에 등록.
 */
static int my_i2c_xfer(struct i2c_adapter *adap,
                        struct i2c_msg *msgs, int num)
{
    struct my_i2c_dev *dev = i2c_get_adapdata(adap);
    int i;

    for (i = 0; i < num; i++) {
        if (msgs[i].flags & I2C_M_RD)
            my_hw_read(dev, &msgs[i]);    /* H/W 레지스터로 읽기 */
        else
            my_hw_write(dev, &msgs[i]);   /* H/W 레지스터로 쓰기 */
    }
    return num;  /* 성공한 메시지 수 반환 */
}

static u32 my_i2c_func(struct i2c_adapter *adap)
{
    /* 이 컨트롤러가 지원하는 기능 플래그 반환 */
    return I2C_FUNC_I2C              /* raw I2C 전송 */
         | I2C_FUNC_SMBUS_BYTE_DATA   /* SMBus byte 읽기/쓰기 */
         | I2C_FUNC_SMBUS_WORD_DATA   /* SMBus word 읽기/쓰기 */
         | I2C_FUNC_SMBUS_BLOCK_DATA  /* SMBus block 전송 */
         | I2C_FUNC_10BIT_ADDR;       /* 10-bit 주소 지원 */
}

static const struct i2c_algorithm my_i2c_algo = {
    .master_xfer = my_i2c_xfer,       /* raw I2C 전송 구현 */
    .functionality = my_i2c_func,      /* 기능 플래그 조회 */
    /* .smbus_xfer = my_smbus_xfer,  — SMBus 전용 컨트롤러일 때 */
};

/* Adapter 등록 (platform driver의 probe에서) */
static int my_i2c_controller_probe(struct platform_device *pdev)
{
    struct my_i2c_dev *dev;
    int ret;

    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);

    dev->adap.owner = THIS_MODULE;
    dev->adap.algo = &my_i2c_algo;
    dev->adap.dev.parent = &pdev->dev;
    dev->adap.dev.of_node = pdev->dev.of_node;  /* DT 자식 디바이스 열거 */
    strlcpy(dev->adap.name, "my-i2c", sizeof(dev->adap.name));
    i2c_set_adapdata(&dev->adap, dev);

    ret = devm_i2c_add_adapter(&pdev->dev, &dev->adap);
    /* devm: remove 시 자동 i2c_del_adapter() */

    return ret;
}
/* === I2C Mux (멀티플렉서) ===
 *
 * 소스: drivers/i2c/i2c-mux.c, drivers/i2c/muxes/
 *
 * 하나의 I2C 버스를 여러 세그먼트로 분할.
 * 동일 주소의 디바이스를 서로 다른 세그먼트에 배치 가능.
 *
 * 일반적인 MUX 칩: PCA9548A (8채널), PCA9546A (4채널), TCA9548A
 *
 * Device Tree 예시:
 *   i2c-mux@70 {
 *       compatible = "nxp,pca9548";
 *       reg = <0x70>;
 *       #address-cells = <1>;
 *       #size-cells = <0>;
 *
 *       i2c@0 {  ← 채널 0 (가상 I2C 버스)
 *           reg = <0>;
 *           #address-cells = <1>;
 *           #size-cells = <0>;
 *           sensor@48 { compatible = "ti,tmp102"; reg = <0x48>; };
 *       };
 *       i2c@1 {  ← 채널 1 (같은 주소 0x48도 가능)
 *           reg = <1>;
 *           sensor@48 { compatible = "ti,tmp102"; reg = <0x48>; };
 *       };
 *   };
 *
 * 커널 내부: i2c_mux_alloc() → i2c_mux_add_adapter()
 * → 각 채널마다 가상 i2c_adapter 생성
 * → select/deselect 콜백으로 MUX 채널 전환 */
/* === I2C Slave Mode (커널 5.2+) ===
 *
 * 소스: drivers/i2c/i2c-slave-*.c
 *
 * 리눅스를 I2C 슬레이브로 동작시키는 기능.
 * 임베디드에서 BMC(Baseboard Management Controller) 등에 활용.
 *
 * i2c_slave_register(client, I2C_SLAVE_DEFAULTS, slave_cb)
 *
 * 콜백 이벤트:
 *   I2C_SLAVE_WRITE_REQUESTED — 마스터가 쓰기 시작
 *   I2C_SLAVE_WRITE_RECEIVED  — 데이터 바이트 수신
 *   I2C_SLAVE_READ_REQUESTED  — 마스터가 읽기 요청
 *   I2C_SLAVE_READ_PROCESSED  — 데이터 바이트 전송 완료
 *   I2C_SLAVE_STOP            — STOP 조건 감지
 *
 * 기본 제공 슬레이브 백엔드:
 *   i2c-slave-eeprom  — EEPROM 에뮬레이션 (256바이트)
 *   i2c-slave-testunit — 테스트용 슬레이브
 */
# === I2C/SMBus 사용자 공간 도구 (i2c-tools) ===

# 시스템의 I2C 버스 목록 확인
ls /dev/i2c-*
# /dev/i2c-0  /dev/i2c-1  /dev/i2c-2 ...

# I2C 버스 컨트롤러 정보
cat /sys/bus/i2c/devices/i2c-0/name
# "SMBus I801 adapter at efa0"  (Intel PCH)

# 버스 스캔 — 응답하는 슬레이브 주소 탐지
i2cdetect -y 0           # Quick Command 방식
i2cdetect -y -r 0        # Read Byte 방식 (Quick 미지원 디바이스)
#      0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
# 20: -- -- -- -- -- -- -- --
# 30: -- -- -- -- -- -- -- --
# 40: -- -- -- -- -- -- -- --
# 50: 50 -- -- -- -- -- -- --  ← 0x50에 디바이스 존재 (EEPROM)

# 단일 레지스터 읽기/쓰기
i2cget -y 0 0x50 0x00          # 버스0, 주소0x50, 레지스터0x00 읽기
i2cset -y 0 0x50 0x00 0xFF     # 레지스터 0x00에 0xFF 쓰기

# 전체 레지스터 덤프
i2cdump -y 0 0x50              # 기본: SMBus byte 방식
i2cdump -y -r 0x00-0x0f 0 0x50 # 범위 지정 덤프

# raw I2C 메시지 전송 (i2c-tools 4.0+)
i2ctransfer -y 0 w1@0x50 0x00 r4  # reg 0x00 쓰기 후 4바이트 읽기
# w1@0x50 = 쓰기 1바이트 주소 0x50, r4 = 읽기 4바이트
i2ctransfer -y 0 w2@0x50 0x00 0x10 0xFF  # 2바이트 레지스터 + 데이터 쓰기

# SMBus 컨트롤러 기능 확인
i2cdetect -F 0
# I2C                            yes
# SMBus Quick Command             yes
# SMBus Byte                      yes
# SMBus Byte Data                 yes
# SMBus Word Data                 yes
# SMBus Block Data                yes

# DIMM SPD EEPROM 읽기 (DDR4/DDR5 메모리 정보)
# SMBus에 연결된 DIMM SPD (주소 0x50~0x57)
i2cdump -y 0 0x50 s  # SMBus block 방식으로 SPD 덤프
decode-dimms           # i2c-tools 부가 도구: SPD 데이터를 사람이 읽을 수 있게 파싱

# 커널 I2C 디바이스 트리 확인
ls /sys/bus/i2c/devices/
# 0-0050  1-0020  i2c-0  i2c-1
# 형식: {버스번호}-{16진수주소}
cat /sys/bus/i2c/devices/0-0050/name
I2C/SMBus 주의사항:
  • SMBus 전용 컨트롤러 — Intel PCH의 i2c-i801은 SMBus 전용. i2c_transfer()로 32바이트 초과 전송 불가. i2c_check_functionality()로 반드시 확인
  • 버스 잠금i2c_transfer()는 내부적으로 adapter mutex를 잡음. 콜백 안에서 동일 adapter 재접근 시 교착. i2c_transfer_buffer_flags() 또는 __i2c_transfer() 사용
  • 원자적 I2C — panic/reboot 경로에서는 mutex를 잡을 수 없음. i2c_algorithm.master_xfer_atomic 콜백 구현 필요 (폴링 방식)
  • 주소 충돌 — 같은 버스에 동일 주소 디바이스 불가. I2C MUX로 해결하거나 i2c_new_ancillary_device()로 보조 주소 사용
  • 풀업 저항 — 버스에 디바이스가 많으면 총 버스 커패시턴스 증가 → 풀업 저항값 낮춰야 함. 400pF 초과 시 Fast Mode 불가
  • 전압 레벨 — 1.8V/3.3V 혼재 시 레벨 시프터 필요. 풀업 전압과 디바이스 Vdd가 일치해야 함

I3C (MIPI I3C) 서브시스템

특성I2CSMBus 3.0I3C (Basic)I3C (HDR)
최대 속도3.4 Mbps (Hs)1 Mbps12.5 Mbps (SDR)25 Mbps (HDR-DDR)
출력 방식오픈 드레인오픈 드레인푸시풀 (SDR)푸시풀
주소 할당고정 7/10bit고정/ARP동적 (DAA)동적 (DAA)
인터럽트별도 IRQ 라인SMBALERT#In-Band (IBI)In-Band (IBI)
Hot-Join미지원미지원지원지원
I2C 호환I2C 하위호환레거시 I2C 혼용레거시 I2C 혼용
전력외부 풀업 필요외부 풀업 필요내부 풀업 (SDA)내부 풀업 (SDA)
핀 수SDA, SCLSDA, SCLSDA, SCL (동일)SDA, SCL (동일)
커널 소스drivers/i2c/drivers/i2c/drivers/i3c/ (5.0+)
/* === I3C (MIPI Improved Inter-Integrated Circuit) 개요 ===
 *
 * MIPI Alliance가 2017년 발표한 차세대 저속 직렬 버스 규격.
 * I2C의 한계를 극복하면서 물리적 호환성(SDA/SCL 2-wire) 유지.
 *
 * 핵심 특징:
 *
 * 1. 동적 주소 할당 (DAA — Dynamic Address Assignment)
 *    - I2C: 제조 시 고정된 7-bit 주소 → 동일 칩 다수 사용 시 충돌
 *    - I3C: 마스터가 버스 초기화 시 각 디바이스에 7-bit 주소 동적 할당
 *    - ENTDAA CCC: 디바이스의 48-bit Provisioned ID로 식별 후 주소 배정
 *    - SETDASA CCC: 레거시 I2C 디바이스에 정적 주소 설정
 *
 * 2. In-Band Interrupt (IBI)
 *    - I2C: 인터럽트마다 별도 GPIO 라인 필요 → 핀 수 증가
 *    - I3C: 슬레이브가 SDA 라인으로 직접 인터럽트 신호 전달
 *    - 마스터가 버스 유휴 시 SDA를 모니터링 → 슬레이브가 LOW로 당김
 *    - 중재 후 해당 슬레이브의 주소 + MDB(Mandatory Data Byte) 수신
 *    - → 추가 GPIO 핀 불필요 → 패키지 크기/비용 절감
 *
 * 3. HDR (High Data Rate) 모드
 *    - SDR (Single Data Rate): 12.5 Mbps (기본, 오픈 드레인/푸시풀 혼합)
 *    - HDR-DDR (Double Data Rate): 25 Mbps (양 에지 데이터 전송)
 *    - HDR-TSP (Ternary Symbol Pure): 25 Mbps (3-레벨 신호)
 *    - HDR-TSL (Ternary Symbol Legacy): HDR-TSP + I2C 호환 레벨
 *    - HDR-BT (Bulk Transport): 최대 50 Mbps (I3C v1.1.1)
 *
 * 4. Hot-Join
 *    - 동작 중인 버스에 새 디바이스 추가 가능
 *    - 새 디바이스가 Hot-Join 요청 → 마스터가 DAA 수행 → 주소 할당
 *
 * 5. CCC (Common Command Codes)
 *    - 브로드캐스트 CCC: 모든 디바이스에 전달 (RSTDAA, ENEC, DISEC 등)
 *    - 다이렉트 CCC: 특정 디바이스에 전달 (SETDASA, GETMRL 등)
 */

/* === I3C 버스 초기화 순서 ===
 *
 * 1. 마스터가 SCL을 12.5MHz로 설정, 버스 리셋
 * 2. RSTDAA (브로드캐스트) — 기존 동적 주소 모두 해제
 * 3. SETDASA (다이렉트)  — 레거시 I2C 디바이스에 정적 주소 할당
 * 4. ENTDAA (브로드캐스트) — I3C 디바이스 동적 주소 할당
 *    ├── 각 디바이스가 48-bit PID(Provisioned ID) 응답
 *    │     ├── bits[47:33] — MIPI 제조사 ID
 *    │     ├── bits[32]    — ID 유형 (랜덤/고정)
 *    │     └── bits[31:0]  — 파트/인스턴스 번호
 *    ├── BCR (Bus Characteristics Register, 8-bit)
 *    │     ├── bit[7:6] — 디바이스 역할 (I3C slave, Master-capable 등)
 *    │     ├── bit[5]   — HDR 지원 여부
 *    │     ├── bit[3]   — IBI 페이로드 유무
 *    │     └── bit[1]   — IBI 요청 가능 여부
 *    ├── DCR (Device Characteristics Register, 8-bit)
 *    │     → 디바이스 타입: 가속도계, 자이로, 기압계, 온도 등
 *    └── 마스터가 7-bit 동적 주소 배정 → ACK → 다음 디바이스
 *
 * 5. ENEC (브로드캐스트) — IBI, Hot-Join 이벤트 활성화
 * 6. 정상 통신 시작 (SDR 또는 HDR 모드)
 */
/* === 리눅스 I3C 서브시스템 아키텍처 (커널 5.0+) ===
 *
 * 소스: drivers/i3c/
 *
 * ┌───────────────────────────────────────────────────┐
 * │           사용자 공간 (향후 i3c-tools 등)         │
 * ├───────────────────────────────────────────────────┤
 * │              I3C Core (drivers/i3c/master.c)      │
 * │   i3c_device_do_priv_xfers()  — 프라이빗 전송     │
 * │   i3c_device_send_ccc_cmd()   — CCC 명령          │
 * │   DAA 수행, IBI 관리, 디바이스 등록/매칭           │
 * ├───────────────────────────────────────────────────┤
 * │   I3C Master Controller Driver                    │
 * │   drivers/i3c/master/dw-i3c-master.c (DesignWare) │
 * │   drivers/i3c/master/svc-i3c-master.c (Silvaco)   │
 * │   drivers/i3c/master/mipi-i3c-hci.c (MIPI HCI)    │
 * │   drivers/i3c/master/cdns-i3c-master.c (Cadence)   │
 * ├───────────────────────────────────────────────────┤
 * │              하드웨어 I3C 컨트롤러                 │
 * └───────────────────────────────────────────────────┘
 *
 * 핵심 구조체:
 *   struct i3c_master_controller — I3C 마스터 인스턴스
 *   struct i3c_master_controller_ops — 컨트롤러 드라이버 콜백
 *   struct i3c_device             — I3C 디바이스 (동적 주소 보유)
 *   struct i3c_driver             — I3C 디바이스 드라이버
 *   struct i2c_dev_desc           — I3C 버스의 레거시 I2C 디바이스
 */

/* === I3C 디바이스 드라이버 예시 === */
#include <linux/i3c/device.h>
#include <linux/i3c/master.h>

static const struct i3c_device_id my_i3c_ids[] = {
    /* MIPI 제조사 ID + 파트 ID + 추가 정보 */
    I3C_DEVICE(0x0123, 0x0456, NULL),
    /* 또는 DCR 기반 매칭 (디바이스 타입별) */
    I3C_DEVICE_EXTRA_INFO(0x0123, 0x0456, 0, NULL),
    { }
};
MODULE_DEVICE_TABLE(i3c, my_i3c_ids);

static int my_i3c_probe(struct i3c_device *i3cdev)
{
    struct i3c_device_info info;
    u8 tx_buf[2], rx_buf[4];

    /* 디바이스 정보 조회 (PID, BCR, DCR, 동적 주소) */
    i3c_device_get_info(i3cdev, &info);
    dev_info(&i3cdev->dev,
        "PID: 0x%012llx, BCR: 0x%02x, DCR: 0x%02x, addr: 0x%02x\n",
        info.pid, info.bcr, info.dcr, info.dyn_addr);

    /* === 프라이빗 전송 (SDR 모드) ===
     * I2C의 i2c_transfer()에 해당 */
    struct i3c_priv_xfer xfers[2] = {
        {   /* 레지스터 주소 쓰기 */
            .rnw = false,         /* 쓰기 */
            .len = sizeof(tx_buf),
            .data.out = tx_buf,
        },
        {   /* 데이터 읽기 */
            .rnw = true,          /* 읽기 */
            .len = sizeof(rx_buf),
            .data.in = rx_buf,
        },
    };
    tx_buf[0] = 0x00;  /* 레지스터 주소 */
    i3c_device_do_priv_xfers(i3cdev, xfers, 2);

    /* === IBI (In-Band Interrupt) 등록 === */
    struct i3c_ibi_setup ibi_setup = {
        .handler = my_ibi_handler,     /* IBI 수신 콜백 */
        .max_payload_len = 2,          /* IBI 페이로드 최대 크기 */
        .num_slots = 4,                /* IBI 큐 슬롯 수 */
    };
    i3c_device_request_ibi(i3cdev, &ibi_setup);
    i3c_device_enable_ibi(i3cdev);

    return 0;
}

/* IBI 핸들러 — 워크큐 컨텍스트에서 호출 */
static void my_ibi_handler(struct i3c_device *i3cdev,
                            const struct i3c_ibi_payload *payload)
{
    /* payload->data: IBI와 함께 전달된 데이터 (MDB + 추가 바이트)
     * payload->len:  페이로드 길이
     * MDB(Mandatory Data Byte): 인터럽트 원인 식별자 */
    dev_dbg(&i3cdev->dev, "IBI: MDB=0x%02x, len=%zu\n",
        payload->data[0], payload->len);
}

static void my_i3c_remove(struct i3c_device *i3cdev)
{
    i3c_device_disable_ibi(i3cdev);
    i3c_device_free_ibi(i3cdev);
}

static struct i3c_driver my_i3c_driver = {
    .driver = { .name = "my_i3c_sensor" },
    .probe = my_i3c_probe,
    .remove = my_i3c_remove,
    .id_table = my_i3c_ids,
};
module_i3c_driver(my_i3c_driver);
/* === I3C Master Controller 드라이버 핵심 콜백 ===
 *
 * 컨트롤러 하드웨어를 구동하는 드라이버가 구현해야 하는 ops:
 */
struct i3c_master_controller_ops {
    /* 버스 초기화 (DAA 수행, 클럭 설정 등) */
    int (*bus_init)(struct i3c_master_controller *master);
    void (*bus_cleanup)(struct i3c_master_controller *master);

    /* 동적 주소 할당 (ENTDAA 수행) */
    int (*do_daa)(struct i3c_master_controller *master);

    /* I3C 프라이빗 전송 (SDR/HDR) */
    int (*send_ccc_cmd)(struct i3c_master_controller *master,
                         struct i3c_ccc_cmd *cmd);
    int (*priv_xfers)(struct i3c_dev_desc *dev,
                       struct i3c_priv_xfer *xfers, int nxfers);

    /* 레거시 I2C 전송 (버스의 I2C 디바이스용) */
    int (*i2c_xfers)(struct i2c_dev_desc *dev,
                      const struct i2c_msg *xfers, int nxfers);

    /* IBI (In-Band Interrupt) 관리 */
    int (*request_ibi)(struct i3c_dev_desc *dev,
                        const struct i3c_ibi_setup *req);
    void (*free_ibi)(struct i3c_dev_desc *dev);
    int (*enable_ibi)(struct i3c_dev_desc *dev);
    int (*disable_ibi)(struct i3c_dev_desc *dev);
    void (*recycle_ibi_slot)(struct i3c_dev_desc *dev,
                              struct i3c_ibi_slot *slot);
};

/* Device Tree 예시:
 *
 * i3c-master@d040000 {
 *     compatible = "snps,dw-i3c-master";
 *     reg = <0x0d040000 0x1000>;
 *     interrupts = ;
 *     clocks = <&i3c_clk>;
 *     #address-cells = <3>;  ← I3C: 3셀 (정적주소, PID 상위, PID 하위)
 *     #size-cells = <0>;
 *     i2c-scl-hz = <400000>;   ← 레거시 I2C SCL 속도
 *     i3c-scl-hz = <12500000>; ← I3C SCL 속도
 *
 *     ← I3C 디바이스 (동적 주소 할당 대상)
 *     sensor@0,11220000000 {
 *         reg = <0 0x112 0x20000000>;  ← 정적주소=0, PID
 *         assigned-address = <0x09>;    ← 원하는 동적 주소 (힌트)
 *     };
 *
 *     ← 레거시 I2C 디바이스 (정적 주소)
 *     eeprom@50 {
 *         compatible = "atmel,24c64";
 *         reg = <0x50 0 0>;  ← I2C 주소 0x50
 *     };
 * };
 */
I3C 도입 현황 (2024~): I3C는 주로 모바일 SoC(Qualcomm, Samsung)의 센서 허브에서 먼저 도입되었습니다. 가속도계, 자이로스코프, 기압계 등의 저전력 센서를 소수 핀으로 연결하는 데 최적화되어 있습니다. 서버/데스크톱 환경에서는 아직 I2C/SMBus가 지배적이며, DDR5 DIMM의 온도 센서 인터페이스에 I3C Basic이 채택되기 시작했습니다.

SPI 서브시스템

/* === SPI (Serial Peripheral Interface) 프로토콜 개요 ===
 *
 * Motorola가 1980년대 개발한 전이중(full-duplex) 동기식 직렬 버스.
 * I2C보다 빠른 속도가 필요한 주변장치(Flash, ADC/DAC, 디스플레이, 센서) 연결에 사용.
 *
 * 물리 계층 (4-wire 기본):
 *   SCLK (Serial Clock)  — 마스터가 생성하는 클럭
 *   MOSI (Master Out Slave In) — 마스터→슬레이브 데이터 (= SDO, COPI)
 *   MISO (Master In Slave Out) — 슬레이브→마스터 데이터 (= SDI, CIPO)
 *   CS/SS (Chip Select)   — 슬레이브 선택 (Active Low, 디바이스별 1개)
 *
 *   ┌──────────┐          SCLK           ┌──────────┐
 *   │          │─────────────────────────→│          │
 *   │          │          MOSI            │          │
 *   │  Master  │─────────────────────────→│  Slave   │
 *   │  (SoC)   │          MISO            │ (Flash)  │
 *   │          │←─────────────────────────│          │
 *   │          │          CS#             │          │
 *   │          │─────────────────────────→│          │
 *   └──────────┘                          └──────────┘
 *
 * I2C와의 핵심 차이:
 *   - 풀업 저항 불필요 (푸시풀 출력)
 *   - 주소 체계 없음 — CS 라인으로 디바이스 선택
 *   - 전이중 통신 — 동시에 송수신 가능
 *   - 클럭 속도 제한 없음 — 수십~수백 MHz 가능 (PCB 설계가 제한 요인)
 *   - 디바이스마다 CS 핀 1개 필요 → 다수 디바이스 시 핀 소모
 *
 * SPI 모드 (CPOL/CPHA):
 *   ┌──────┬──────┬──────┬─────────────────────────────────────┐
 *   │ Mode │ CPOL │ CPHA │ 설명                                │
 *   ├──────┼──────┼──────┼─────────────────────────────────────┤
 *   │  0   │  0   │  0   │ 유휴=LOW,  첫 번째 에지(상승)에서 샘플 │
 *   │  1   │  0   │  1   │ 유휴=LOW,  두 번째 에지(하강)에서 샘플 │
 *   │  2   │  1   │  0   │ 유휴=HIGH, 첫 번째 에지(하강)에서 샘플 │
 *   │  3   │  1   │  1   │ 유휴=HIGH, 두 번째 에지(상승)에서 샘플 │
 *   └──────┴──────┴──────┴─────────────────────────────────────┘
 *
 * 변형 프로토콜:
 *   - Dual SPI: MOSI/MISO를 양방향 2-bit으로 → 2배 속도 (QSPI Flash 읽기)
 *   - Quad SPI (QSPI): 4-bit 데이터 라인 → 4배 속도 (NOR Flash 표준)
 *   - Octal SPI (OSPI): 8-bit 데이터 라인 → 8배 속도 (xSPI/HyperBus)
 *   - 3-wire SPI: MOSI/MISO를 단일 양방향 라인으로 (반이중)
 */
/* === 리눅스 SPI 서브시스템 아키텍처 ===
 *
 * 소스: drivers/spi/
 *
 * ┌────────────────────────────────────────────────────────┐
 * │          사용자 공간                                    │
 * │   /dev/spidevB.C  (B=버스, C=CS)                       │
 * │   libgpiod, spi-tools, flashrom 등                     │
 * ├────────────────────────────────────────────────────────┤
 * │          SPI Core (drivers/spi/spi.c)                  │
 * │   spi_sync() / spi_async()  — 메시지 전송              │
 * │   spi_register_controller() — 컨트롤러 등록            │
 * │   spi_register_driver()     — 디바이스 드라이버 등록    │
 * │   DMA 매핑, CS 관리, 큐 처리, 통계                     │
 * ├────────────────────────────────────────────────────────┤
 * │   SPI Controller Driver (= Master Driver)              │
 * │   drivers/spi/spi-bcm2835.c   (Raspberry Pi)           │
 * │   drivers/spi/spi-sun6i.c     (Allwinner)              │
 * │   drivers/spi/spi-stm32.c     (STM32)                  │
 * │   drivers/spi/spi-rockchip.c  (Rockchip)               │
 * │   drivers/spi/spi-imx.c       (i.MX)                   │
 * │   drivers/spi/spi-pl022.c     (ARM PrimeCell)           │
 * ├────────────────────────────────────────────────────────┤
 * │          하드웨어 SPI 컨트롤러 (FIFO, DMA 채널)         │
 * └────────────────────────────────────────────────────────┘
 *
 * 핵심 구조체:
 *   struct spi_controller  — SPI 마스터/슬레이브 컨트롤러 인스턴스
 *   struct spi_device      — SPI 버스의 개별 디바이스 (CS, 모드, 속도 보유)
 *   struct spi_driver      — SPI 디바이스 드라이버 (probe/remove 콜백)
 *   struct spi_transfer    — 단일 전송 단위 (tx_buf, rx_buf, len, speed_hz)
 *   struct spi_message     — 전송 묶음 (atomic CS assert ~ deassert)
 */
/* === SPI 디바이스 드라이버 개발 === */
#include <linux/spi/spi.h>
#include <linux/module.h>

struct my_spi_data {
    struct spi_device *spi;
    u8 tx_buf[64] ____cacheline_aligned;  /* DMA 정렬 */
    u8 rx_buf[64] ____cacheline_aligned;
};

static int my_spi_read_reg(struct my_spi_data *priv,
                            u8 reg, u8 *val, size_t len)
{
    struct spi_transfer xfers[2] = {
        {
            .tx_buf = ®,
            .len = 1,
        },
        {
            .rx_buf = val,
            .len = len,
        },
    };
    struct spi_message msg;

    /* spi_message에 transfer 체인 구성 */
    spi_message_init(&msg);
    spi_message_add_tail(&xfers[0], &msg);
    spi_message_add_tail(&xfers[1], &msg);

    /* 동기 전송: CS assert → xfers[0] → xfers[1] → CS deassert */
    return spi_sync(priv->spi, &msg);
}

static int my_spi_probe(struct spi_device *spi)
{
    struct my_spi_data *priv;
    u8 chip_id;
    int ret;

    /* SPI 모드, 비트 순서, 워드 크기 설정 */
    spi->mode = SPI_MODE_0;       /* CPOL=0, CPHA=0 */
    spi->bits_per_word = 8;
    spi->max_speed_hz = 10000000; /* 10 MHz */
    ret = spi_setup(spi);
    if (ret)
        return ret;

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

    priv->spi = spi;
    spi_set_drvdata(spi, priv);

    /* Chip ID 읽기 */
    ret = my_spi_read_reg(priv, 0x00, &chip_id, 1);
    if (ret)
        return ret;

    dev_info(&spi->dev, "Chip ID: 0x%02x, SPI mode %d, %u Hz\n",
        chip_id, spi->mode, spi->max_speed_hz);

    return 0;
}

/* SPI 간편 API — 단일 전송 시 spi_message 구성 불필요 */
static int my_spi_simple_ops(struct spi_device *spi)
{
    u8 cmd = 0x9F;        /* JEDEC Read ID */
    u8 id[3];
    int ret;

    /* spi_write(): 단순 쓰기 (CS assert → 데이터 전송 → CS deassert) */
    ret = spi_write(spi, &cmd, 1);

    /* spi_read(): 단순 읽기 */
    ret = spi_read(spi, id, 3);

    /* spi_write_then_read(): 쓰기 후 읽기 (가장 많이 사용)
     * 내부적으로 임시 DMA 버퍼 할당 → 소량 데이터에 적합 */
    ret = spi_write_then_read(spi, &cmd, 1, id, 3);
    /* id[0]=제조사, id[1]=메모리타입, id[2]=용량 */

    /* spi_w8r8(): 1바이트 쓰고 1바이트 읽기 (레지스터 읽기) */
    ret = spi_w8r8(spi, 0x05);  /* 상태 레지스터 읽기, 반환값=레지스터값 */

    /* spi_w8r16(): 1바이트 쓰고 2바이트 읽기 (16-bit 레지스터) */
    ret = spi_w8r16(spi, 0x01); /* 반환값=16bit 값 (big-endian) */

    return ret;
}

/* Device Tree / ACPI 매칭 테이블 */
static const struct of_device_id my_spi_of_match[] = {
    { .compatible = "vendor,my-spi-sensor" },
    { .compatible = "vendor,my-spi-adc", .data = &adc_chip_info },
    { }
};
MODULE_DEVICE_TABLE(of, my_spi_of_match);

static const struct spi_device_id my_spi_ids[] = {
    { "my-spi-sensor", 0 },
    { "my-spi-adc",    1 },
    { }
};
MODULE_DEVICE_TABLE(spi, my_spi_ids);

static struct spi_driver my_spi_driver = {
    .driver = {
        .name = "my-spi-device",
        .of_match_table = my_spi_of_match,
    },
    .probe = my_spi_probe,
    .id_table = my_spi_ids,
};
module_spi_driver(my_spi_driver);
/* === SPI 비동기 전송 (고성능/DMA) ===
 *
 * spi_async()는 비블로킹. 완료 시 콜백 호출.
 * DMA 전송 시 반드시 DMA-safe 버퍼 사용 (kmalloc, not stack/global).
 */

static void my_spi_complete(void *context)
{
    struct completion *done = context;
    complete(done);
}

static int my_spi_async_xfer(struct my_spi_data *priv,
                              u8 *tx, u8 *rx, size_t len)
{
    struct spi_transfer xfer = {
        .tx_buf = tx,
        .rx_buf = rx,
        .len = len,
        .speed_hz = 20000000,   /* 전송별 속도 오버라이드 */
        .bits_per_word = 8,
    };
    struct spi_message msg;
    DECLARE_COMPLETION_ONSTACK(done);

    spi_message_init(&msg);
    spi_message_add_tail(&xfer, &msg);
    msg.complete = my_spi_complete;
    msg.context = &done;

    /* 비블로킹 전송 — 즉시 반환, 완료 시 콜백 */
    int ret = spi_async(priv->spi, &msg);
    if (ret)
        return ret;

    /* 완료 대기 (또는 워크큐에서 처리) */
    wait_for_completion(&done);
    return msg.status;
}

/* === SPI와 regmap 통합 ===
 *
 * I2C와 동일하게 regmap API로 통합 접근 가능.
 * 내부적으로 SPI 전송을 자동 처리.
 */
#include <linux/regmap.h>

static const struct regmap_config my_spi_regmap_cfg = {
    .reg_bits = 8,          /* 레지스터 주소 비트 수 */
    .val_bits = 8,          /* 레지스터 값 비트 수 */
    .max_register = 0xFF,
    .read_flag_mask = 0x80,  /* 읽기 시 레지스터 주소에 OR (디바이스 관례) */
    .cache_type = REGCACHE_RBTREE,
};

/* probe에서: */
struct regmap *regmap = devm_regmap_init_spi(spi, &my_spi_regmap_cfg);
regmap_read(regmap, 0x00, &chip_id);   /* 내부적으로 SPI 전송 */
regmap_write(regmap, 0x01, 0x42);      /* 레지스터 쓰기 */
regmap_update_bits(regmap, 0x02, 0x0F, 0x05); /* RMW */
/* === SPI 컨트롤러 (Master) 드라이버 핵심 구조 ===
 *
 * SoC의 SPI 하드웨어 블록을 제어하는 드라이버.
 * spi_controller 구조체의 콜백을 구현.
 */

static int my_spi_transfer_one(struct spi_controller *ctlr,
                                struct spi_device *spi,
                                struct spi_transfer *xfer)
{
    /* 하드웨어 레지스터에 FIFO/DMA로 데이터 전송
     *
     * xfer->tx_buf  — 송신 데이터 (NULL이면 더미 바이트 전송)
     * xfer->rx_buf  — 수신 버퍼   (NULL이면 수신 데이터 폐기)
     * xfer->len     — 전송 바이트 수
     * xfer->speed_hz — 이 전송의 클럭 속도
     * xfer->bits_per_word — 워드 크기 (8, 16, 32)
     *
     * 반환: 0=완료, 1=진행중(비동기 완료 시 spi_finalize_current_transfer 호출)
     */

    /* 클럭 분주비 설정 */
    my_hw_set_speed(ctlr, xfer->speed_hz);

    /* FIFO 방식 전송 예시 */
    for (int i = 0; i < xfer->len; i++) {
        if (xfer->tx_buf)
            writel(xfer->tx_buf[i], regs + SPI_TX_FIFO);
        else
            writel(0x00, regs + SPI_TX_FIFO);

        /* TX 완료 대기 */
        my_hw_wait_tx_done(ctlr);

        if (xfer->rx_buf)
            xfer->rx_buf[i] = readl(regs + SPI_RX_FIFO);
    }

    return 0;
}

static int my_spi_controller_probe(struct platform_device *pdev)
{
    struct spi_controller *ctlr;

    /* SPI 마스터 컨트롤러 할당 (0 = 추가 private 데이터 크기) */
    ctlr = devm_spi_alloc_master(&pdev->dev, 0);
    if (!ctlr)
        return -ENOMEM;

    /* 컨트롤러 능력 설정 */
    ctlr->mode_bits = SPI_CPOL | SPI_CPHA | SPI_CS_HIGH | SPI_LSB_FIRST;
    ctlr->bits_per_word_mask = SPI_BPW_MASK(8) | SPI_BPW_MASK(16);
    ctlr->min_speed_hz = 100000;     /* 100 KHz */
    ctlr->max_speed_hz = 50000000;   /* 50 MHz */
    ctlr->num_chipselect = 4;        /* CS 라인 수 */
    ctlr->bus_num = -1;              /* 자동 할당 */

    /* 핵심 콜백 등록 */
    ctlr->transfer_one = my_spi_transfer_one;
    ctlr->set_cs = my_spi_set_cs;    /* CS assert/deassert */
    ctlr->use_gpio_descriptors = true; /* DT의 cs-gpios 자동 처리 */

    /* DMA 지원 설정 (선택) */
    ctlr->can_dma = my_spi_can_dma;
    ctlr->max_dma_len = 65536;

    platform_set_drvdata(pdev, ctlr);
    return devm_spi_register_controller(&pdev->dev, ctlr);
}
/* === SPI Device Tree 바인딩 ===
 *
 * SPI 컨트롤러 노드 아래에 슬레이브 디바이스를 자식 노드로 정의.
 * reg 속성은 CS(Chip Select) 번호.
 */

/* spi0: spi@fe204000 {
 *     compatible = "brcm,bcm2835-spi";
 *     reg = <0xfe204000 0x200>;
 *     interrupts = ;
 *     clocks = <&clk_spi>;
 *     #address-cells = <1>;   ← CS 번호
 *     #size-cells = <0>;
 *     cs-gpios = <&gpio 8 GPIO_ACTIVE_LOW>,   ← CS0: GPIO8
 *                <&gpio 7 GPIO_ACTIVE_LOW>;    ← CS1: GPIO7
 *     dmas = <&dma 6>, <&dma 7>;
 *     dma-names = "tx", "rx";
 *     status = "okay";
 *
 *     ← CS0에 연결된 NOR Flash
 *     flash@0 {
 *         compatible = "jedec,spi-nor";
 *         reg = <0>;                    ← CS 번호 = 0
 *         spi-max-frequency = <50000000>; ← 최대 50MHz
 *         spi-rx-bus-width = <4>;       ← Quad SPI 읽기 (4-bit)
 *         spi-tx-bus-width = <1>;       ← 단일 라인 쓰기
 *         m25p,fast-read;               ← Fast Read 명령 사용
 *
 *         partitions {
 *             compatible = "fixed-partitions";
 *             #address-cells = <1>;
 *             #size-cells = <1>;
 *
 *             boot@0 {
 *                 reg = <0x000000 0x100000>; ← 1MB 부트 파티션
 *                 label = "boot";
 *             };
 *             rootfs@100000 {
 *                 reg = <0x100000 0xF00000>;
 *                 label = "rootfs";
 *             };
 *         };
 *     };
 *
 *     ← CS1에 연결된 ADC
 *     adc@1 {
 *         compatible = "ti,ads7950";
 *         reg = <1>;                    ← CS 번호 = 1
 *         spi-max-frequency = <10000000>;
 *         spi-cpol;                     ← CPOL=1 (Mode 2 or 3)
 *         spi-cpha;                     ← CPHA=1 (Mode 1 or 3) → Mode 3
 *         #io-channel-cells = <1>;
 *         vref-supply = <&vref_3v3>;
 *     };
 * }; */
/* === spidev — 유저스페이스 SPI 접근 ===
 *
 * /dev/spidevB.C (B=버스번호, C=CS번호)를 통해
 * 유저스페이스에서 직접 SPI 전송 가능.
 *
 * Device Tree에서 spidev 활성화:
 *   test_device@0 {
 *       compatible = "linux,spidev";  ← 또는 "rohm,dh2228fv" 등
 *       reg = <0>;
 *       spi-max-frequency = <1000000>;
 *   };
 */
/* 유저스페이스 SPI 프로그래밍 예시 */
#include <fcntl.h>
#include <linux/spi/spidev.h>
#include <sys/ioctl.h>

int main(void)
{
    int fd = open("/dev/spidev0.0", O_RDWR);

    /* SPI 모드 설정 */
    uint8_t mode = SPI_MODE_0;
    ioctl(fd, SPI_IOC_WR_MODE, &mode);

    /* 클럭 속도 설정 */
    uint32_t speed = 1000000;  /* 1 MHz */
    ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);

    /* 전이중 전송 (ioctl) */
    uint8_t tx[] = {0x9F, 0x00, 0x00, 0x00};
    uint8_t rx[4] = {0};

    struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = 4,
        .speed_hz = speed,
        .bits_per_word = 8,
    };
    ioctl(fd, SPI_IOC_MESSAGE(1), &tr);
    /* rx[1..3] = JEDEC ID (제조사, 타입, 용량) */

    close(fd);
    return 0;
}
# SPI 디버깅/확인 명령

# 등록된 SPI 디바이스 확인
ls /sys/bus/spi/devices/
# spi0.0  spi0.1  spi1.0
# 형식: spi{버스}.{CS}

# SPI 디바이스 상세 정보
cat /sys/bus/spi/devices/spi0.0/modalias
# spi:spidev
cat /sys/bus/spi/devices/spi0.0/max_speed_hz
# 1000000

# SPI 컨트롤러 통계 (커널 4.16+)
cat /sys/class/spi_master/spi0/statistics/transfers
cat /sys/class/spi_master/spi0/statistics/bytes
cat /sys/class/spi_master/spi0/statistics/errors

# spi-tools를 이용한 유저스페이스 전송
# spi-config -d /dev/spidev0.0 -q  # 현재 설정 조회
# spi-pipe -d /dev/spidev0.0 -s 1000000 < tx_data > rx_data

# flashrom으로 SPI NOR Flash 읽기/쓰기
# flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=10000 -r backup.bin
SPI 주의사항:
  • DMA 버퍼 정렬spi_write_then_read()는 내부적으로 DMA-safe 버퍼를 할당하므로 편리하지만, 대량 전송 시 kmalloc()으로 할당한 DMA-safe 버퍼에 spi_sync() 사용. 스택/글로벌 변수는 DMA 불가
  • CS 타이밍 — 일부 디바이스는 CS assert 후 데이터 전송까지 딜레이 필요. spi_transfer.delay.value.cs_change_delay로 제어
  • CPOL/CPHA 불일치 — 모드가 디바이스 스펙과 맞지 않으면 데이터 corruption. 데이터시트의 타이밍 다이어그램에서 클럭 극성과 샘플링 에지 반드시 확인
  • CS GPIO — 하드웨어 CS가 부족하면 GPIO를 CS로 사용 (cs-gpios DT 속성). GPIO CS는 소프트웨어 제어라 속도 저하 가능
  • Quad/Dual SPIspi-rx-bus-width/spi-tx-bus-width DT 속성 설정 필수. 컨트롤러 드라이버가 SPI_RX_QUAD/SPI_TX_QUAD 지원해야 함
  • 전이중 vs 반이중 — 대부분의 SPI Flash는 명령/주소 전송 후 데이터 수신 (반이중 방식). spi_transfer에서 tx_bufrx_buf를 동시 설정하면 진정한 전이중. Flash 프로토콜에서는 각 phase를 별도 spi_transfer로 분리

GPIO 서브시스템

/* === GPIO (General Purpose Input/Output) 서브시스템 개요 ===
 *
 * GPIO는 소프트웨어로 제어 가능한 범용 디지털 핀.
 * LED 제어, 버튼 입력, 리셋 신호, 인터럽트, CS 라인 등에 사용.
 *
 * 리눅스 GPIO 서브시스템 아키텍처:
 *
 * ┌─────────────────────────────────────────────────────────────┐
 * │              사용자 공간                                     │
 * │   /dev/gpiochipN  (libgpiod, gpioget, gpioset, gpiomon)    │
 * │   /sys/class/gpio/ (레거시 sysfs — deprecated)              │
 * ├─────────────────────────────────────────────────────────────┤
 * │              GPIO Character Device (drivers/gpio/gpiolib-cdev.c) │
 * │   GPIO_V2_GET_LINE_IOCTL  — 라인 요청                      │
 * │   GPIO_V2_LINE_SET_VALUES — 값 설정                         │
 * │   GPIO_V2_LINE_GET_VALUES — 값 읽기                         │
 * │   poll()/read()           — 에지 이벤트 감지                │
 * ├─────────────────────────────────────────────────────────────┤
 * │              gpiolib Core (drivers/gpio/gpiolib.c)          │
 * │   gpiod_get() / gpiod_set_value() — descriptor API         │
 * │   gpiochip_add_data()             — 칩 등록                 │
 * │   GPIO ↔ IRQ 도메인 연결                                    │
 * │   pinctrl 연동 (GPIO muxing)                                │
 * ├─────────────────────────────────────────────────────────────┤
 * │   GPIO Controller Drivers (gpio_chip 구현)                  │
 * │   drivers/gpio/gpio-mmio.c     (MMIO 기반 범용)             │
 * │   drivers/gpio/gpio-pl061.c    (ARM PrimeCell)              │
 * │   drivers/gpio/gpio-dwapb.c    (DesignWare APB)             │
 * │   drivers/gpio/gpio-pca953x.c  (I2C GPIO expander)         │
 * │   drivers/gpio/gpio-mcp23s08.c (SPI GPIO expander)         │
 * │   drivers/gpio/gpio-rockchip.c (Rockchip)                  │
 * ├─────────────────────────────────────────────────────────────┤
 * │              하드웨어 GPIO 컨트롤러 (SoC / I2C / SPI)       │
 * └─────────────────────────────────────────────────────────────┘
 *
 * API 진화 (반드시 최신 API 사용):
 *   v1 (레거시): gpio_request() / gpio_set_value() — deprecated
 *   v2 (현재):   gpiod_get()    / gpiod_set_value() — descriptor 기반
 *   sysfs:       /sys/class/gpio/export — deprecated (커널 4.8+에서 chardev 도입)
 *   chardev:     /dev/gpiochipN + ioctl — 현재 권장 유저스페이스 인터페이스
 */
/* === GPIO 소비자 API (커널 드라이버에서 GPIO 사용) === */
#include <linux/gpio/consumer.h>

static int my_probe(struct platform_device *pdev)
{
    struct gpio_desc *reset_gpio, *irq_gpio;
    struct gpio_descs *leds;

    /* === 기본: 단일 GPIO 가져오기 ===
     * devm_gpiod_get(dev, con_id, flags)
     *   con_id: DT의 "{con_id}-gpios" 속성에서 가져옴
     *   flags:  GPIOD_OUT_LOW, GPIOD_OUT_HIGH, GPIOD_IN,
     *           GPIOD_OUT_LOW_OPEN_DRAIN, GPIOD_ASIS
     */
    reset_gpio = devm_gpiod_get(&pdev->dev, "reset", GPIOD_OUT_HIGH);
    if (IS_ERR(reset_gpio))
        return PTR_ERR(reset_gpio);

    /* === 선택적 GPIO (없어도 에러 아님) === */
    struct gpio_desc *enable_gpio;
    enable_gpio = devm_gpiod_get_optional(&pdev->dev, "enable", GPIOD_OUT_LOW);
    /* NULL 반환이면 DT에 해당 GPIO 없음 → 정상 */

    /* === 복수 GPIO 한번에 가져오기 === */
    leds = devm_gpiod_get_array(&pdev->dev, "led", GPIOD_OUT_LOW);
    /* DT: led-gpios = <&gpio1 0 0>, <&gpio1 1 0>, <&gpio1 2 0>;
     * leds->ndescs = 3, leds->desc[0..2] */

    /* === 인덱스로 GPIO 가져오기 (동일 이름 복수) === */
    struct gpio_desc *cs_gpio;
    cs_gpio = devm_gpiod_get_index(&pdev->dev, "cs", 1, GPIOD_OUT_HIGH);
    /* DT: cs-gpios = <&gpio1 8 0>, <&gpio1 7 0>; → index 1 = gpio1 7 */

    /* === GPIO 출력 제어 === */
    gpiod_set_value_cansleep(reset_gpio, 0);  /* 논리 LOW */
    msleep(10);
    gpiod_set_value_cansleep(reset_gpio, 1);  /* 논리 HIGH */

    /* === 복수 GPIO 한번에 설정 === */
    unsigned long values[] = { 0x05 }; /* 비트마스크: LED0=1, LED1=0, LED2=1 */
    gpiod_set_array_value_cansleep(leds->ndescs, leds->desc,
                                    leds->info, values);

    /* === GPIO 입력 읽기 === */
    int val = gpiod_get_value_cansleep(irq_gpio); /* 0 또는 1 (논리값) */

    /* === GPIO 방향 런타임 변경 === */
    gpiod_direction_output(reset_gpio, 1); /* 출력으로 변경, 값=1 */
    gpiod_direction_input(reset_gpio);      /* 입력으로 변경 */

    /* === GPIO → 인터럽트 변환 === */
    irq_gpio = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
    int irq = gpiod_to_irq(irq_gpio);
    if (irq < 0)
        return irq;

    devm_request_threaded_irq(&pdev->dev, irq,
        NULL,              /* hardirq handler (NULL → threaded only) */
        my_irq_handler,    /* threaded handler */
        IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
        "my_gpio_irq", priv);

    return 0;
}

/* === Device Tree 예시 === */
/* my_device {
 *     compatible = "vendor,my-device";
 *
 *     // GPIO 소비자 속성 (이름-gpios = <&컨트롤러 핀번호 플래그>)
 *     reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
 *     irq-gpios   = <&gpio2 12 GPIO_ACTIVE_HIGH>;
 *     enable-gpios = <&gpio1 20 GPIO_ACTIVE_HIGH>;
 *
 *     // 복수 GPIO (배열)
 *     led-gpios = <&gpio1 0 GPIO_ACTIVE_HIGH>,  // LED0
 *                 <&gpio1 1 GPIO_ACTIVE_HIGH>,  // LED1
 *                 <&gpio1 2 GPIO_ACTIVE_LOW>;   // LED2 (active low)
 *
 *     // CS GPIO (SPI 컨트롤러에서도 사용)
 *     cs-gpios = <&gpio1 8 GPIO_ACTIVE_LOW>,
 *                <&gpio1 7 GPIO_ACTIVE_LOW>;
 * }; */
/* === GPIO 컨트롤러 드라이버 (gpio_chip 구현) ===
 *
 * SoC 내장 GPIO 블록이나 I2C/SPI GPIO expander를 제어.
 * struct gpio_chip의 콜백을 구현.
 */
#include <linux/gpio/driver.h>

struct my_gpio {
    struct gpio_chip gc;
    void __iomem *regs;
    struct irq_chip irq_chip;
    raw_spinlock_t lock;
};

/* 방향 설정 콜백 */
static int my_gpio_direction_input(struct gpio_chip *gc,
                                    unsigned int offset)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val;
    unsigned long flags;

    raw_spin_lock_irqsave(&priv->lock, flags);
    val = readl(priv->regs + GPIO_DIR_REG);
    val &= ~BIT(offset);   /* 0 = 입력 */
    writel(val, priv->regs + GPIO_DIR_REG);
    raw_spin_unlock_irqrestore(&priv->lock, flags);

    return 0;
}

static int my_gpio_direction_output(struct gpio_chip *gc,
                                     unsigned int offset, int value)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val;
    unsigned long flags;

    raw_spin_lock_irqsave(&priv->lock, flags);

    /* 출력 값 먼저 설정 */
    val = readl(priv->regs + GPIO_DATA_REG);
    if (value)
        val |= BIT(offset);
    else
        val &= ~BIT(offset);
    writel(val, priv->regs + GPIO_DATA_REG);

    /* 방향을 출력으로 설정 */
    val = readl(priv->regs + GPIO_DIR_REG);
    val |= BIT(offset);    /* 1 = 출력 */
    writel(val, priv->regs + GPIO_DIR_REG);

    raw_spin_unlock_irqrestore(&priv->lock, flags);

    return 0;
}

/* 값 읽기/쓰기 콜백 */
static int my_gpio_get(struct gpio_chip *gc, unsigned int offset)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    return !!(readl(priv->regs + GPIO_DATA_REG) & BIT(offset));
}

static void my_gpio_set(struct gpio_chip *gc,
                          unsigned int offset, int value)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val;
    unsigned long flags;

    raw_spin_lock_irqsave(&priv->lock, flags);
    val = readl(priv->regs + GPIO_DATA_REG);
    if (value)
        val |= BIT(offset);
    else
        val &= ~BIT(offset);
    writel(val, priv->regs + GPIO_DATA_REG);
    raw_spin_unlock_irqrestore(&priv->lock, flags);
}

/* 복수 핀 한번에 읽기 (성능 최적화) */
static int my_gpio_get_multiple(struct gpio_chip *gc,
                                unsigned long *mask,
                                unsigned long *bits)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    *bits = readl(priv->regs + GPIO_DATA_REG) & *mask;
    return 0;
}

/* 복수 핀 한번에 쓰기 */
static void my_gpio_set_multiple(struct gpio_chip *gc,
                                  unsigned long *mask,
                                  unsigned long *bits)
{
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val;
    unsigned long flags;

    raw_spin_lock_irqsave(&priv->lock, flags);
    val = readl(priv->regs + GPIO_DATA_REG);
    val = (val & ~*mask) | (*bits & *mask);
    writel(val, priv->regs + GPIO_DATA_REG);
    raw_spin_unlock_irqrestore(&priv->lock, flags);
}

/* GPIO 컨트롤러 등록 */
static int my_gpio_probe(struct platform_device *pdev)
{
    struct my_gpio *priv;

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

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

    raw_spin_lock_init(&priv->lock);

    /* gpio_chip 초기화 */
    priv->gc.label = "my-gpio";
    priv->gc.parent = &pdev->dev;
    priv->gc.owner = THIS_MODULE;
    priv->gc.base = -1;              /* 동적 번호 할당 */
    priv->gc.ngpio = 32;             /* 핀 수 */
    priv->gc.direction_input = my_gpio_direction_input;
    priv->gc.direction_output = my_gpio_direction_output;
    priv->gc.get = my_gpio_get;
    priv->gc.set = my_gpio_set;
    priv->gc.get_multiple = my_gpio_get_multiple;
    priv->gc.set_multiple = my_gpio_set_multiple;
    priv->gc.can_sleep = false;     /* MMIO → atomic 접근 가능 */

    return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}
/* === GPIO 인터럽트 지원 (irqchip 통합) ===
 *
 * GPIO 컨트롤러가 인터럽트를 지원하면 gpio_irq_chip을 설정.
 * gpiolib이 irq_domain을 자동 생성하여 gpiod_to_irq() 지원.
 */

static void my_gpio_irq_ack(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio *priv = gpiochip_get_data(gc);
    writel(BIT(d->hwirq), priv->regs + GPIO_INT_CLR);
}

static void my_gpio_irq_mask(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val = readl(priv->regs + GPIO_INT_EN);
    val &= ~BIT(d->hwirq);
    writel(val, priv->regs + GPIO_INT_EN);
}

static void my_gpio_irq_unmask(struct irq_data *d)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val = readl(priv->regs + GPIO_INT_EN);
    val |= BIT(d->hwirq);
    writel(val, priv->regs + GPIO_INT_EN);
}

static int my_gpio_irq_set_type(struct irq_data *d,
                                 unsigned int type)
{
    struct gpio_chip *gc = irq_data_get_irq_chip_data(d);
    struct my_gpio *priv = gpiochip_get_data(gc);
    u32 val = readl(priv->regs + GPIO_INT_TYPE);

    switch (type & IRQ_TYPE_SENSE_MASK) {
    case IRQ_TYPE_EDGE_RISING:
        val |= BIT(d->hwirq);     /* 상승 에지 */
        break;
    case IRQ_TYPE_EDGE_FALLING:
        val &= ~BIT(d->hwirq);    /* 하강 에지 */
        break;
    case IRQ_TYPE_EDGE_BOTH:
        /* 양쪽 에지 — 하드웨어 지원 여부에 따라 SW 에뮬레이션 */
        break;
    case IRQ_TYPE_LEVEL_HIGH:
    case IRQ_TYPE_LEVEL_LOW:
        /* 레벨 트리거 설정 */
        break;
    default:
        return -EINVAL;
    }

    writel(val, priv->regs + GPIO_INT_TYPE);
    return 0;
}

/* GPIO 인터럽트 핸들러 (부모 IRQ에서 호출) */
static void my_gpio_irq_handler(struct irq_desc *desc)
{
    struct gpio_chip *gc = irq_desc_get_handler_data(desc);
    struct my_gpio *priv = gpiochip_get_data(gc);
    struct irq_chip *irqchip = irq_desc_get_chip(desc);
    u32 pending;

    chained_irq_enter(irqchip, desc);

    pending = readl(priv->regs + GPIO_INT_STATUS);
    while (pending) {
        int bit = __ffs(pending);
        generic_handle_domain_irq(gc->irq.domain, bit);
        pending &= ~BIT(bit);
    }

    chained_irq_exit(irqchip, desc);
}

/* probe에서 GPIO irqchip 등록 (커널 5.x+ 방식) */
static int my_gpio_probe_with_irq(struct platform_device *pdev)
{
    struct my_gpio *priv;
    struct gpio_irq_chip *girq;
    int parent_irq;

    /* ... gpio_chip 기본 설정 생략 ... */

    /* irq_chip 설정 */
    priv->irq_chip.name = "my-gpio-irq";
    priv->irq_chip.irq_ack = my_gpio_irq_ack;
    priv->irq_chip.irq_mask = my_gpio_irq_mask;
    priv->irq_chip.irq_unmask = my_gpio_irq_unmask;
    priv->irq_chip.irq_set_type = my_gpio_irq_set_type;
    priv->irq_chip.flags = IRQCHIP_IMMUTABLE;
    INIT_IRQ_DEFAULT_HANDLER(priv->irq_chip);

    parent_irq = platform_get_irq(pdev, 0);

    /* gpio_chip에 irqchip 연결 */
    girq = &priv->gc.irq;
    gpio_irq_chip_set_chip(girq, &priv->irq_chip);
    girq->parent_handler = my_gpio_irq_handler;
    girq->num_parents = 1;
    girq->parents = devm_kcalloc(&pdev->dev, 1,
                                   sizeof(*girq->parents), GFP_KERNEL);
    girq->parents[0] = parent_irq;
    girq->default_type = IRQ_TYPE_NONE;
    girq->handler = handle_bad_irq; /* set_type에서 변경 */

    return devm_gpiochip_add_data(&pdev->dev, &priv->gc, priv);
}
/* === GPIO Expander (I2C/SPI 기반 외부 GPIO 컨트롤러) ===
 *
 * SoC의 GPIO 핀이 부족할 때 I2C/SPI로 연결하는 GPIO 확장 칩.
 * 커널에 이미 다수의 GPIO expander 드라이버가 포함되어 있음.
 *
 * 주요 칩과 커널 드라이버:
 *   ┌──────────────┬──────────┬────────┬─────────────────────────┐
 *   │ 칩           │ 인터페이스│ GPIO수 │ 커널 드라이버            │
 *   ├──────────────┼──────────┼────────┼─────────────────────────┤
 *   │ PCA9535/9555 │ I2C      │ 16     │ gpio-pca953x.c          │
 *   │ PCA9534/9538 │ I2C      │ 8      │ gpio-pca953x.c          │
 *   │ PCAL6524     │ I2C      │ 24     │ gpio-pca953x.c          │
 *   │ MCP23017     │ I2C      │ 16     │ gpio-mcp23s08.c (공용)  │
 *   │ MCP23S17     │ SPI      │ 16     │ gpio-mcp23s08.c         │
 *   │ PCF8574      │ I2C      │ 8      │ gpio-pcf857x.c          │
 *   │ TCA6416      │ I2C      │ 16     │ gpio-pca953x.c          │
 *   │ SX1509       │ I2C      │ 16     │ gpio-sx150x.c           │
 *   │ MAX7301      │ SPI      │ 28     │ gpio-max7301.c          │
 *   └──────────────┴──────────┴────────┴─────────────────────────┘
 *
 * I2C GPIO expander의 핵심 차이:
 *   - can_sleep = true → gpiod_set_value_cansleep() 만 사용 가능
 *   - IRQ 컨텍스트(hardirq)에서 접근 불가 → threaded IRQ 사용 필수
 *   - I2C 전송 지연으로 토글 속도 제한 (수십 kHz 이하)
 *
 * Device Tree 예시:
 *
 * &i2c1 {
 *     gpio_exp: gpio@20 {
 *         compatible = "nxp,pca9555";
 *         reg = <0x20>;
 *         gpio-controller;
 *         #gpio-cells = <2>;
 *         interrupt-parent = <&gpio1>;     ← SoC GPIO를 부모 IRQ로
 *         interrupts = <15 IRQ_TYPE_EDGE_FALLING>;
 *         interrupt-controller;
 *         #interrupt-cells = <2>;
 *
 *         // GPIO 라인 이름 지정 (디버깅용)
 *         gpio-line-names = "EXT_LED0", "EXT_LED1", "EXT_BTN0", "EXT_BTN1",
 *                           "EXT_CS0", "EXT_CS1", "EXT_RST", "EXT_EN",
 *                           "IO8", "IO9", "IO10", "IO11",
 *                           "IO12", "IO13", "IO14", "IO15";
 *     };
 * };
 *
 * // 다른 디바이스에서 expander의 GPIO 참조
 * my_device {
 *     enable-gpios = <&gpio_exp 7 GPIO_ACTIVE_HIGH>;  // EXT_EN
 *     reset-gpios  = <&gpio_exp 6 GPIO_ACTIVE_LOW>;   // EXT_RST
 * };
 */
/* === GPIO와 pinctrl 연동 ===
 *
 * 대부분의 SoC에서 GPIO 핀은 다중 기능(mux) 핀.
 * 동일 핀이 GPIO, UART TX, SPI MOSI 등으로 사용 가능.
 * pinctrl 서브시스템이 핀 기능 선택과 전기적 특성(풀업/풀다운, 드라이브 강도) 관리.
 *
 * GPIO 요청 시 자동으로 pinctrl과 연동:
 *   gpiod_get() → gpiolib → pinctrl_gpio_request()
 *                          → 핀을 GPIO 모드로 mux
 *   gpiod_put() → gpiolib → pinctrl_gpio_free()
 *                          → 핀 해제
 *
 * gpio_chip에서 pinctrl 연동을 위한 gpio_ranges 설정:
 */

/* Device Tree에서 GPIO ↔ pinctrl 매핑:
 *
 * gpio1: gpio@e6051000 {
 *     compatible = "renesas,gpio-r8a7795";
 *     reg = <0 0xe6051000 0 0x50>;
 *     #gpio-cells = <2>;
 *     gpio-controller;
 *     gpio-ranges = <&pfc 0 32 32>;
 *     // &pfc: pinctrl 노드
 *     // 0: GPIO 시작 오프셋
 *     // 32: pinctrl 핀 시작 번호
 *     // 32: 핀 개수
 *
 *     // 핀 설정 (풀업, 드라이브 강도 등)은 pinctrl에서:
 *     // &pfc {
 *     //     button_pins: button {
 *     //         pins = "GP_1_4";
 *     //         bias-pull-up;
 *     //     };
 *     // };
 * };
 */
# === GPIO 유저스페이스 도구 (libgpiod) ===
# libgpiod는 /dev/gpiochipN chardev 기반의 현대적 유저스페이스 GPIO 라이브러리
# sysfs (/sys/class/gpio/export)는 deprecated — libgpiod v2 사용 권장

# GPIO 컨트롤러 목록
gpiodetect
# gpiochip0 [pinctrl-bcm2835] (54 lines)
# gpiochip1 [pca9555] (16 lines)

# GPIO 라인 정보 (방향, 활성 상태, 사용자)
gpioinfo gpiochip0
# gpiochip0 - 54 lines:
#   line   0: "ID_SDA"    unused   input  active-high
#   line   1: "ID_SCL"    unused   input  active-high
#   line   2: "SDA1"      "i2c1"   input  active-high [used]
#   line  18: "GPIO18"    unused   input  active-high

# GPIO 값 읽기 (라인 번호 지정)
gpioget gpiochip0 18
# 0 또는 1

# GPIO 값 쓰기 (출력)
gpioset gpiochip0 18=1     # GPIO18을 HIGH
gpioset gpiochip0 18=0     # GPIO18을 LOW

# 복수 GPIO 동시 제어
gpioset gpiochip0 17=1 18=0 27=1

# GPIO 라인 이름으로 접근 (gpio-line-names DT 속성 설정 시)
gpioget --by-name EXT_BTN0
gpioset --by-name EXT_LED0=1

# GPIO 이벤트 모니터링 (에지 감지)
gpiomon gpiochip0 18
# 18  1  1707564890.123456789  rising
# 18  0  1707564891.234567890  falling

# 특정 에지만 감지
gpiomon --rising-edge gpiochip0 18
gpiomon --falling-edge gpiochip0 18

# 복수 라인 동시 모니터링
gpiomon gpiochip0 17 18 27

# 타임아웃 설정 (밀리초)
gpioget --bias=pull-up gpiochip0 18    # 풀업 활성화하고 읽기
gpioget --bias=pull-down gpiochip0 18  # 풀다운 활성화하고 읽기
gpioget --active-low gpiochip0 18      # active-low로 해석
# === GPIO sysfs 디버깅 정보 ===

# 등록된 모든 GPIO 컨트롤러 목록
ls /sys/bus/gpio/devices/
# gpiochip0  gpiochip1

# GPIO 컨트롤러 상세 정보
cat /sys/class/gpio/gpiochip0/label
# pinctrl-bcm2835
cat /sys/class/gpio/gpiochip0/ngpio
# 54
cat /sys/class/gpio/gpiochip0/base
# 0

# debugfs GPIO 상태 (커널 디버깅에 유용)
cat /sys/kernel/debug/gpio
# gpiochip0: GPIOs 0-53, parent: 20200000.gpio, pinctrl-bcm2835:
#  gpio-2   (SDA1                ) in  hi IRQ
#  gpio-3   (SCL1                ) in  hi IRQ
#  gpio-18  (                    ) out lo
# gpiochip1: GPIOs 496-511, parent: 1-0020, pca9555:
#  gpio-496 (EXT_LED0            ) out lo
#  gpio-497 (EXT_LED1            ) out hi

# pinctrl 연동 상태
cat /sys/kernel/debug/pinctrl/pinctrl-bcm2835/gpio-ranges
# GPIO ranges handled:
#  0: pinctrl-bcm2835 GPIOS [0 - 53] PINS [0 - 53]
GPIO 주의사항:
  • 레거시 API 사용 금지gpio_request()/gpio_set_value()는 deprecated. 반드시 gpiod_* descriptor API 사용. sysfs /sys/class/gpio/export도 deprecated → /dev/gpiochipN chardev 사용
  • cansleep 구분 — I2C/SPI GPIO expander는 can_sleep=true. 반드시 gpiod_set_value_cansleep() 사용. hardirq 컨텍스트에서 호출 시 BUG. gpiod_set_value()는 MMIO 기반 GPIO만 (atomic)
  • Active Low — Device Tree에서 GPIO_ACTIVE_LOW 플래그 시 gpiolib이 논리 반전 처리. gpiod_set_value(gpio, 1)은 논리 HIGH → 실제 물리 핀 LOW 출력. 드라이버는 항상 논리 값으로만 사용
  • DT 속성 이름 — GPIO 소비자 속성은 반드시 xxx-gpios 형식 (xxx-gpio는 레거시). devm_gpiod_get(dev, "xxx", ...)에서 "xxx"만 지정
  • 오픈 드레인/소스 — LED 같은 경우 GPIO_OPEN_DRAIN 플래그나 GPIOD_OUT_LOW_OPEN_DRAIN으로 설정. 하드웨어가 지원하지 않으면 gpiolib이 SW 에뮬레이션
  • 디바운싱 — 기계식 스위치/버튼은 gpiod_set_debounce(gpio, usec)로 하드웨어 디바운싱 설정. 미지원 시 SW 디바운스(gpio-keys 드라이버의 debounce-interval DT 속성)
  • GPIO hog — 특정 GPIO를 부팅 시 고정 상태로 설정: DT에서 gpio-hog; gpios = <5 0>; output-high; → 드라이버 없이도 GPIO 상태 보장
GPIO hog Device Tree 예시:
&gpio1 {
    usb_pwr_en {
        gpio-hog;
        gpios = <5 GPIO_ACTIVE_HIGH>;
        output-high;           /* 부팅 시 HIGH 출력 (USB 전원 활성) */
        line-name = "USB_PWR_EN";
    };
    debug_led {
        gpio-hog;
        gpios = <10 GPIO_ACTIVE_LOW>;
        output-low;            /* 부팅 시 LOW 출력 (LED 꺼짐) */
        line-name = "DEBUG_LED";
    };
};

SATA (libata) 서브시스템

계층커널 코드역할
SCSI 상위층 drivers/scsi/ 블록 I/O 요청을 SCSI 명령으로 변환
libata 코어 drivers/ata/libata-core.c ATA 명령 세트, EH(에러 핸들링), 링크 관리
AHCI 드라이버 drivers/ata/ahci.c AHCI HBA 레지스터 제어, NCQ, FIS 전송
하드웨어 SATA PHY, 디스크/SSD
# SATA 링크 상태 확인
dmesg | grep -i ata
# ata1: SATA link up 6.0 Gbps (SStatus 133 SControl 300)

# AHCI 포트 정보
cat /sys/class/ata_port/ata1/port_no

# NCQ (Native Command Queuing) 지원 확인
hdparm -I /dev/sda | grep -i ncq
# Queue depth: 32

# SATA 전원 관리 (ALPM)
cat /sys/class/scsi_host/host0/link_power_management_policy
# max_performance | medium_power | min_power | med_power_with_dipm

버스 비교 요약

버스토폴로지최대 속도프로빙 방식주요 용도
PCI/PCIe트리 (Root Complex → Switch → Endpoint)PCIe 5.0: 32 GT/s/lanePCI enumeration (자동)GPU, NIC, NVMe, QAT
I2C멀티마스터 직렬 버스3.4 MbpsDevice Tree / ACPI센서, EEPROM, PMIC
SPI마스터-슬레이브 (CS 라인)100+ MbpsDevice TreeFlash, ADC, 디스플레이
GPIO점대점 디지털 라인MHz급 토글Device Tree / ACPI리셋, LED, 인터럽트
SATA점대점 (포트별 디바이스)6 Gbps (SATA III)AHCI 자동 감지HDD, SSD (레거시)
NVMePCIe 직접 연결PCIe 대역폭PCI enumeration고성능 SSD
USB트리 (Hub 계층)20 Gbps (USB 3.2)USB enumeration (핫플러그)주변장치 범용
MDIO직렬 버스 (PHY 전용)2.5 MHzDevice Tree이더넷 PHY 제어
버스 드라이버 개발 공통 원칙:
  • 반드시 devm_* managed resource API 사용 (메모리 누수 방지)
  • Device Tree / ACPI 바인딩을 정확히 정의하고 Documentation/devicetree/bindings/에 문서화
  • 프로브 순서(probe ordering)에 의존하지 말 것 — -EPROBE_DEFER 반환으로 의존성 해결
  • 전원 관리(PM) 콜백 반드시 구현 — suspend/resume/runtime_pm
  • 에러 경로에서 모든 리소스 정리 — devm_* 사용 시 자동 정리되지만, 순서 의존적 해제 시 주의

입출력 장치 서브시스템 심화

Input 서브시스템 (키보드, 마우스, 터치)

리눅스 Input 서브시스템(drivers/input/)은 키보드, 마우스, 터치스크린, 조이스틱, 리모컨 등 모든 종류의 입력 장치를 통합 관리하는 프레임워크입니다. 하드웨어별 드라이버가 이벤트를 생성하면, Input Core가 이를 적절한 핸들러(evdev, kbd, mousedev 등)로 라우팅하여 유저 공간에 전달합니다.

Input 서브시스템 아키텍처 User Space libinput / X11 evtest / evemu SDL / Qt Input systemd-logind console (VT) /dev/input/eventN, mouseN, jsN Event Handlers (input_handler) evdev kbd mousedev joydev rfkill-input input-leds input_handle (연결) Input Core (drivers/input/input.c) 디바이스 등록 · 이벤트 라우팅 · handler 매칭 · input_event 전파 Input Device Drivers (input_dev) atkbd hid-generic gpio-keys goodix_ts i2c-hid xpad custom Hardware PS/2 (i8042) USB HID I2C 터치패널 SPI 디지타이저 GPIO 버튼 Bluetooth HID serio (PS/2) · USB · I2C · SPI · GPIO · Bluetooth (hidp)

Input 서브시스템은 3계층으로 구성됩니다:

핵심 데이터 구조

/* === struct input_dev — Input 디바이스를 나타내는 핵심 구조체 ===
 * include/linux/input.h
 * 하드웨어 드라이버가 할당·등록하며, 지원하는 이벤트 유형과 코드를 비트마스크로 선언 */
struct input_dev {
    const char *name;           /* 사람이 읽을 수 있는 디바이스 이름 */
    const char *phys;           /* 물리적 경로 (예: "usb-0000:00:1d.0-1/input0") */
    const char *uniq;           /* 고유 식별자 (시리얼 번호 등) */
    struct input_id id;          /* bustype, vendor, product, version */

    /* 이벤트 능력(capability) 비트마스크 */
    unsigned long evbit[BITS_TO_LONGS(EV_CNT)];    /* 지원 이벤트 유형 */
    unsigned long keybit[BITS_TO_LONGS(KEY_CNT)];  /* 지원 키 코드 */
    unsigned long relbit[BITS_TO_LONGS(REL_CNT)];  /* 상대축 코드 */
    unsigned long absbit[BITS_TO_LONGS(ABS_CNT)];  /* 절대축 코드 */
    unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)];  /* 기타 이벤트 */
    unsigned long ledbit[BITS_TO_LONGS(LED_CNT)];  /* LED 상태 */
    unsigned long swbit[BITS_TO_LONGS(SW_CNT)];    /* 스위치 상태 */
    unsigned long ffbit[BITS_TO_LONGS(FF_CNT)];    /* Force Feedback */

    /* 절대축 파라미터 (min, max, fuzz, flat, resolution) */
    struct input_absinfo *absinfo;

    /* 콜백 함수 */
    int (*open)(struct input_dev *dev);    /* 첫 핸들러 연결 시 */
    void (*close)(struct input_dev *dev);  /* 마지막 핸들러 해제 시 */
    int (*event)(struct input_dev *dev,
                 unsigned int type,
                 unsigned int code, int value); /* LED/FF 출력 이벤트 */

    struct device dev;            /* 내장 device 구조체 */
    struct list_head h_list;     /* 연결된 input_handle 리스트 */
    struct list_head node;       /* 전역 input_dev 리스트 */
};
/* === struct input_handler — 이벤트 핸들러 (evdev, kbd, mousedev 등) ===
 * Input Core에 등록되어 매칭되는 디바이스의 이벤트를 처리 */
struct input_handler {
    void (*event)(struct input_handle *handle,
                  unsigned int type,
                  unsigned int code, int value);
    void (*events)(struct input_handle *handle,
                   const struct input_value *vals,
                   unsigned int count);         /* 배치 이벤트 (성능 최적화) */
    bool (*filter)(struct input_handle *handle,
                   unsigned int type,
                   unsigned int code, int value); /* 이벤트 필터링 */
    bool (*match)(struct input_handler *handler,
                  struct input_dev *dev);        /* 추가 매칭 조건 */
    int  (*connect)(struct input_handler *handler,
                    struct input_dev *dev,
                    const struct input_device_id *id);
    void (*disconnect)(struct input_handle *handle);

    const char *name;
    const struct input_device_id *id_table;   /* 매칭 테이블 */
    struct list_head h_list;                   /* 연결된 handle 리스트 */
    struct list_head node;                     /* 전역 handler 리스트 */
};
/* === struct input_handle — input_dev와 input_handler 연결 ===
 * 하나의 디바이스는 여러 핸들러에 연결 가능 (예: evdev + kbd 동시) */
struct input_handle {
    void *private;                     /* 핸들러별 개인 데이터 */
    int open;                           /* 열린 파일 디스크립터 수 */
    const char *name;
    struct input_dev *dev;              /* 연결된 디바이스 */
    struct input_handler *handler;      /* 연결된 핸들러 */
    struct list_head d_node;            /* dev->h_list 노드 */
    struct list_head h_node;            /* handler->h_list 노드 */
};

/* === struct input_event — 유저 공간에 전달되는 이벤트 구조 ===
 * /dev/input/eventN에서 read() 시 이 구조체 배열로 전달됨 */
struct input_event {
    struct timeval time;   /* 이벤트 발생 시각 (또는 input_event_usec) */
    __u16 type;             /* EV_KEY, EV_REL, EV_ABS ... */
    __u16 code;             /* KEY_A, REL_X, ABS_MT_POSITION_X ... */
    __s32 value;            /* 키: 1(press)/0(release)/2(repeat), 축: 좌표값 */
};

이벤트 유형 상세

이벤트 유형상수용도주요 코드 예시
동기화EV_SYN0x00이벤트 그룹 경계 표시SYN_REPORT, SYN_MT_REPORT, SYN_DROPPED
키/버튼EV_KEY0x01키보드, 마우스 버튼, 게임패드KEY_A, BTN_LEFT, BTN_TOUCH
상대 이동EV_REL0x02마우스 이동, 스크롤 휠REL_X, REL_Y, REL_WHEEL
절대 위치EV_ABS0x03터치스크린, 태블릿, 조이스틱ABS_X, ABS_MT_POSITION_X, ABS_MT_SLOT
기타EV_MSC0x04스캔코드 등 잡다한 이벤트MSC_SCAN, MSC_TIMESTAMP
스위치EV_SW0x05덮개(lid), 헤드폰 잭, 태블릿 모드SW_LID, SW_HEADPHONE_INSERT
LEDEV_LED0x11키보드 LED 제어LED_CAPSL, LED_NUML, LED_SCROLLL
사운드EV_SND0x12비프, 클릭 사운드SND_BELL, SND_TONE
반복EV_REP0x14키 반복 파라미터REP_DELAY, REP_PERIOD
Force FeedbackEV_FF0x15진동/햅틱/포스 피드백FF_RUMBLE, FF_CONSTANT, FF_PERIODIC
전원EV_PWR0x16전원 버튼 이벤트(예약됨)
FF 상태EV_FF_STATUS0x17FF 효과 재생 상태FF_STATUS_STOPPED, FF_STATUS_PLAYING
EV_SYN과 이벤트 패킷: 입력 이벤트는 패킷 단위로 전달됩니다. 하나의 동작(예: 마우스 대각선 이동)은 EV_REL/REL_X, EV_REL/REL_Y 이벤트 후 EV_SYN/SYN_REPORT로 묶입니다. 유저 공간은 SYN_REPORT를 받을 때까지 버퍼링해야 합니다. SYN_DROPPED를 수신하면 이벤트 누락이 발생한 것이므로 디바이스 상태를 다시 동기화해야 합니다.

Input 디바이스 등록

#include <linux/input.h>
#include <linux/module.h>
#include <linux/platform_device.h>

struct my_kbd_data {
    struct input_dev *idev;
    int irq;
};

/* 인터럽트 핸들러 — 하드웨어에서 키 이벤트 수신 */
static irqreturn_t my_kbd_irq(int irq, void *dev_id)
{
    struct my_kbd_data *data = dev_id;
    u8 scancode;

    /* 하드웨어에서 스캔코드 읽기 (실제로는 MMIO/I2C/SPI 등) */
    scancode = read_hw_scancode();

    /* 이벤트 보고 — Input Core가 연결된 모든 handler에 전파 */
    input_report_key(data->idev, scancode, 1);  /* 키 누름 */
    input_sync(data->idev);                      /* SYN_REPORT */

    /* 키 해제 (간단한 예제 — 실제로는 별도 인터럽트) */
    input_report_key(data->idev, scancode, 0);  /* 키 해제 */
    input_sync(data->idev);

    return IRQ_HANDLED;
}

static int my_kbd_probe(struct platform_device *pdev)
{
    struct my_kbd_data *data;
    struct input_dev *idev;
    int ret, i;

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

    /* === 1. devm_input_allocate_device — managed 할당 ===
     * 드라이버 해제 시 자동으로 input_free_device() 호출됨
     * input_allocate_device()와 달리 수동 free 불필요 */
    idev = devm_input_allocate_device(&pdev->dev);
    if (!idev)
        return -ENOMEM;

    /* === 2. 디바이스 식별 정보 설정 === */
    idev->name = "My Custom Keyboard";
    idev->phys = "my-kbd/input0";
    idev->id.bustype = BUS_HOST;       /* BUS_USB, BUS_I2C, BUS_SPI 등 */
    idev->id.vendor  = 0x1234;
    idev->id.product = 0x5678;
    idev->id.version = 0x0100;

    /* === 3. 이벤트 능력(capability) 선언 ===
     * 이 디바이스가 생성할 수 있는 이벤트 유형과 코드를 선언
     * Input Core가 이 정보로 적절한 handler를 매칭 */
    input_set_capability(idev, EV_KEY, KEY_A);     /* EV_KEY + KEY_A 동시 설정 */
    input_set_capability(idev, EV_KEY, KEY_B);

    /* 또는 set_bit로 개별 설정 */
    set_bit(EV_KEY, idev->evbit);
    for (i = KEY_ESC; i <= KEY_MICMUTE; i++)
        set_bit(i, idev->keybit);

    /* 키 반복(autorepeat) 자동 지원 */
    set_bit(EV_REP, idev->evbit);

    /* === 4. open/close 콜백 (선택) ===
     * 유저 공간에서 디바이스 열기/닫기 시 호출
     * 전력 절약: 열린 핸들러가 없으면 하드웨어 비활성화 */
    idev->open  = my_kbd_open;
    idev->close = my_kbd_close;

    /* === 5. 인터럽트 등록 === */
    data->irq = platform_get_irq(pdev, 0);
    if (data->irq < 0)
        return data->irq;

    ret = devm_request_irq(&pdev->dev, data->irq,
                           my_kbd_irq, IRQF_TRIGGER_FALLING,
                           "my-kbd", data);
    if (ret)
        return ret;

    data->idev = idev;
    platform_set_drvdata(pdev, data);

    /* === 6. 디바이스 등록 ===
     * Input Core에 디바이스 등록 → 매칭되는 handler와 자동 연결
     * 등록 후에는 capability 변경 금지 */
    ret = input_register_device(idev);
    if (ret)
        return ret;  /* devm이므로 idev는 자동 해제 */

    dev_info(&pdev->dev, "keyboard registered\n");
    return 0;
}
input_set_capability() vs set_bit(): input_set_capability(dev, EV_KEY, KEY_A)set_bit(EV_KEY, dev->evbit)set_bit(KEY_A, dev->keybit)를 한 번에 수행합니다. 이벤트 유형(evbit)을 빠뜨리는 실수를 방지하므로 가능하면 input_set_capability()를 사용하세요.

이벤트 보고 API

함수이벤트 유형설명
input_report_key(dev, code, value)EV_KEY키/버튼 누름(1), 해제(0), 반복(2)
input_report_rel(dev, code, value)EV_REL상대 이동 (마우스 X/Y, 스크롤)
input_report_abs(dev, code, value)EV_ABS절대 좌표 (터치, 조이스틱)
input_report_switch(dev, code, value)EV_SW스위치 상태 (lid, 잭 등)
input_event(dev, type, code, value)모든 유형범용 이벤트 보고 (위 함수들의 기반)
input_sync(dev)EV_SYNSYN_REPORT — 이벤트 패킷 완료 표시
input_mt_sync(dev)EV_SYNSYN_MT_REPORT — MT Protocol A 슬롯 구분
/* 마우스 이동 + 클릭 보고 예제 */
input_report_rel(idev, REL_X, dx);
input_report_rel(idev, REL_Y, dy);
input_report_rel(idev, REL_WHEEL, wheel);
input_report_key(idev, BTN_LEFT, left_pressed);
input_sync(idev);   /* 하나의 패킷으로 원자적 전달 */

/* 절대 좌표 + 압력 보고 예제 (태블릿) */
input_report_abs(idev, ABS_X, x);
input_report_abs(idev, ABS_Y, y);
input_report_abs(idev, ABS_PRESSURE, pressure);
input_report_key(idev, BTN_TOUCH, pressure > 0);
input_sync(idev);

절대축 파라미터 (ABS)

/* input_set_abs_params(dev, axis, min, max, fuzz, flat)
 *
 * min/max  : 축의 유효 범위
 * fuzz     : 노이즈 필터링 — |new - old| < fuzz이면 무시 (jitter 제거)
 * flat     : 데드존 — |value| < flat이면 0으로 처리 (조이스틱 중앙)
 * resolution: input_abs_set_res()로 별도 설정 (units/mm 등)
 */

/* 터치스크린: 1920x1080 해상도, 노이즈 필터링 4픽셀 */
input_set_abs_params(idev, ABS_X, 0, 1920, 4, 0);
input_set_abs_params(idev, ABS_Y, 0, 1080, 4, 0);
input_set_abs_params(idev, ABS_PRESSURE, 0, 255, 0, 0);

/* resolution 설정 — libinput이 물리적 크기 계산에 사용 */
input_abs_set_res(idev, ABS_X, 40);   /* 40 units/mm */
input_abs_set_res(idev, ABS_Y, 40);

/* 조이스틱: -32768 ~ 32767, 데드존 4096 */
input_set_abs_params(idev, ABS_X, -32768, 32767, 16, 4096);
input_set_abs_params(idev, ABS_Y, -32768, 32767, 16, 4096);

멀티터치 프로토콜

리눅스 커널은 멀티터치를 위한 두 가지 프로토콜을 정의합니다 (Documentation/input/multi-touch-protocol.rst):

항목Protocol A (Deprecated)Protocol B (현재 표준)
슬롯 관리커널이 관리하지 않음커널이 슬롯 할당·추적
터치 구분SYN_MT_REPORT로 분리ABS_MT_SLOT + ABS_MT_TRACKING_ID
대역폭모든 터치를 매번 전송변경된 슬롯만 전송 (효율적)
유저 공간ID 추적을 직접 해야 함슬롯 기반으로 자연스러운 추적
사용처레거시 터치 컨트롤러최신 터치스크린 (goodix, atmel, etc.)
/* === Multitouch Protocol B — 슬롯 기반 (권장) ===
 * input_mt_init_slots()로 슬롯 수를 미리 선언
 * 각 슬롯에 tracking_id를 할당하여 터치 추적 */

#include <linux/input/mt.h>

static int ts_probe(struct i2c_client *client)
{
    struct input_dev *idev;
    int ret;

    idev = devm_input_allocate_device(&client->dev);

    /* 절대 좌표 설정 */
    input_set_abs_params(idev, ABS_MT_POSITION_X, 0, 1920, 0, 0);
    input_set_abs_params(idev, ABS_MT_POSITION_Y, 0, 1080, 0, 0);
    input_set_abs_params(idev, ABS_MT_TOUCH_MAJOR, 0, 255, 0, 0);
    input_set_abs_params(idev, ABS_MT_PRESSURE, 0, 255, 0, 0);

    /* 최대 10개 동시 터치, DIRECT = 터치스크린 (POINTER = 터치패드) */
    ret = input_mt_init_slots(idev, 10, INPUT_MT_DIRECT | INPUT_MT_DROP_UNUSED);
    if (ret)
        return ret;

    input_set_capability(idev, EV_KEY, BTN_TOUCH);

    return input_register_device(idev);
}

/* 터치 이벤트 보고 (인터럽트 핸들러 또는 threaded irq에서) */
static void ts_report_touches(struct input_dev *idev,
                              struct touch_point *tp, int count)
{
    int i;

    for (i = 0; i < count; i++) {
        /* 슬롯 선택 — 같은 tracking_id를 같은 슬롯에 유지 */
        input_mt_slot(idev, tp[i].slot);

        /* tracking_id 할당: 양수=활성 터치, -1=터치 해제 */
        input_mt_report_slot_state(idev, MT_TOOL_FINGER, tp[i].active);

        if (tp[i].active) {
            input_report_abs(idev, ABS_MT_POSITION_X, tp[i].x);
            input_report_abs(idev, ABS_MT_POSITION_Y, tp[i].y);
            input_report_abs(idev, ABS_MT_TOUCH_MAJOR, tp[i].width);
            input_report_abs(idev, ABS_MT_PRESSURE, tp[i].pressure);
        }
    }

    /* INPUT_MT_DROP_UNUSED: 보고되지 않은 슬롯 자동 해제 */
    input_mt_sync_frame(idev);

    /* 단일 터치 이벤트도 함께 생성 (Protocol B가 자동 처리) */
    input_sync(idev);
}
input_mt_sync_frame() 필수: Protocol B에서 INPUT_MT_DROP_UNUSED 플래그를 사용하면 input_mt_sync_frame()을 반드시 호출해야 합니다. 이 함수가 현재 프레임에서 보고되지 않은 슬롯을 자동으로 비활성화합니다. 이를 빠뜨리면 "유령 터치(ghost touch)"가 발생합니다.
MT 축 코드설명일반적인 범위
ABS_MT_SLOT현재 슬롯 인덱스0 ~ (max_slots - 1)
ABS_MT_TRACKING_ID터치 추적 ID (-1 = 해제)자동 할당
ABS_MT_POSITION_X/Y터치 중심 좌표디바이스 해상도
ABS_MT_TOUCH_MAJOR터치 영역 장축 길이0 ~ 255
ABS_MT_TOUCH_MINOR터치 영역 단축 길이0 ~ 255
ABS_MT_WIDTH_MAJOR접근 도구(손가락) 너비0 ~ 255
ABS_MT_PRESSURE터치 압력0 ~ 255
ABS_MT_ORIENTATION터치 타원 방향-90 ~ 90
ABS_MT_TOOL_TYPEMT_TOOL_FINGER / MT_TOOL_PEN도구 유형
ABS_MT_DISTANCE표면과의 거리 (호버링)0 = 접촉

폴링 Input 디바이스

인터럽트를 지원하지 않는 하드웨어의 경우, input_setup_polling()을 사용하여 커널이 주기적으로 하드웨어를 폴링합니다. 이전의 input_polled_dev 구조체는 deprecated되었으며, v5.12부터 통합 API로 대체되었습니다.

/* === 폴링 Input 디바이스 예제 (v5.12+) === */
static void my_sensor_poll(struct input_dev *idev)
{
    struct my_sensor *sensor = input_get_drvdata(idev);
    int x, y;

    /* 하드웨어에서 현재 값 읽기 */
    x = read_sensor_x(sensor);
    y = read_sensor_y(sensor);

    input_report_abs(idev, ABS_X, x);
    input_report_abs(idev, ABS_Y, y);
    input_sync(idev);
}

static int my_sensor_probe(struct i2c_client *client)
{
    struct input_dev *idev;
    struct my_sensor *sensor;

    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    idev = devm_input_allocate_device(&client->dev);

    idev->name = "My Analog Sensor";
    input_set_abs_params(idev, ABS_X, 0, 4095, 2, 0);
    input_set_abs_params(idev, ABS_Y, 0, 4095, 2, 0);

    input_set_drvdata(idev, sensor);

    /* 폴링 설정: 콜백 + 주기 */
    input_setup_polling(idev, my_sensor_poll);
    input_set_poll_interval(idev, 20);       /* 20ms = 50Hz */
    input_set_min_poll_interval(idev, 10);   /* 최소 10ms */
    input_set_max_poll_interval(idev, 100);  /* 최대 100ms */

    /* 유저 공간에서 poll_interval 조정 가능:
     * /sys/class/input/inputN/device/poll_interval */

    return input_register_device(idev);
}

Device Tree 바인딩

커널은 gpio-keys, gpio-keys-polled, matrix-keypad 등 범용 Input 드라이버를 제공합니다. Device Tree만으로 입력 장치를 정의할 수 있어 별도 드라이버 작성이 불필요합니다.

/* === gpio-keys: GPIO 기반 버튼 (인터럽트 지원) === */
gpio-keys {
    compatible = "gpio-keys";

    power-button {
        label = "Power Button";
        gpios = <&gpio1 5 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_POWER>;        /* input-event-codes.h 참조 */
        wakeup-source;                    /* 이 버튼으로 시스템 깨우기 */
        debounce-interval = <20>;        /* ms 단위 디바운스 */
    };

    volume-up {
        label = "Volume Up";
        gpios = <&gpio1 12 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_VOLUMEUP>;
        autorepeat;                       /* 키 반복 활성화 */
    };

    lid-switch {
        label = "Lid Switch";
        gpios = <&gpio2 3 GPIO_ACTIVE_LOW>;
        linux,input-type = <EV_SW>;      /* 스위치 이벤트 */
        linux,code = <SW_LID>;
    };
};

/* === gpio-keys-polled: 인터럽트 없는 GPIO 폴링 === */
gpio-keys-polled {
    compatible = "gpio-keys-polled";
    poll-interval = <100>;              /* 100ms 간격 폴링 */

    button-0 {
        label = "Reset";
        gpios = <&gpio3 8 GPIO_ACTIVE_LOW>;
        linux,code = <KEY_RESTART>;
    };
};

/* === matrix-keypad: 행/열 매트릭스 키패드 === */
matrix-keypad {
    compatible = "gpio-matrix-keypad";
    row-gpios = <&gpio1 0 0>, <&gpio1 1 0>, <&gpio1 2 0>;
    col-gpios = <&gpio1 3 0>, <&gpio1 4 0>, <&gpio1 5 0>;
    debounce-delay-ms = <5>;
    col-scan-delay-us = <2>;

    linux,keymap = <
        MATRIX_KEY(0, 0, KEY_1)
        MATRIX_KEY(0, 1, KEY_2)
        MATRIX_KEY(0, 2, KEY_3)
        MATRIX_KEY(1, 0, KEY_4)
        MATRIX_KEY(1, 1, KEY_5)
        MATRIX_KEY(1, 2, KEY_6)
        MATRIX_KEY(2, 0, KEY_7)
        MATRIX_KEY(2, 1, KEY_8)
        MATRIX_KEY(2, 2, KEY_9)
    >;
};

내장 Event Handler

핸들러디바이스 노드매칭 조건역할
evdev/dev/input/eventN모든 input_dev범용 이벤트 인터페이스 — libinput, X11, Wayland에서 사용. struct input_event 배열을 read()로 전달
kbd(내부)EV_KEY 디바이스콘솔(VT) 키보드 처리 — scancode→keycode→keysym 변환, 콘솔 스위칭(Alt+Fn), SysRq
mousedev/dev/input/mouseN, /dev/input/miceEV_REL 또는 EV_ABS + BTN레거시 PS/2 마우스 프로토콜 에뮬레이션 (ImPS/2). /dev/input/mice는 모든 마우스의 통합 노드
joydev/dev/input/jsNBTN_JOYSTICK/GAMEPAD 등레거시 조이스틱 API (현재는 evdev 사용 권장)
input-leds(내부)EV_LED 디바이스Input LED를 LED 클래스 디바이스로 연결 — LED 서브시스템 trigger 사용 가능
rfkill-input(내부)KEY_RFKILL 등무선 킬 스위치 이벤트 → rfkill 서브시스템 연동

evdev 유저 공간 인터페이스

evdev는 가장 중요한 핸들러로, /dev/input/eventN 캐릭터 디바이스를 통해 모든 이벤트를 있는 그대로(raw) 유저 공간에 전달합니다.

/* === 유저 공간에서 evdev 이벤트 읽기 === */
#include <linux/input.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    int fd = open("/dev/input/event0", O_RDONLY);
    struct input_event ev;
    char name[256];

    /* 디바이스 이름 조회 */
    ioctl(fd, EVIOCGNAME(sizeof(name)), name);
    printf("Device: %s\n", name);

    /* 이벤트 루프 */
    while (read(fd, &ev, sizeof(ev)) == sizeof(ev)) {
        if (ev.type == EV_KEY)
            printf("Key %d %s\n", ev.code,
                   ev.value ? "pressed" : "released");
    }
    close(fd);
}
ioctl방향설명
EVIOCGNAMER디바이스 이름 문자열
EVIOCGIDRstruct input_id (bus, vendor, product, version)
EVIOCGPHYSR물리적 경로 문자열
EVIOCGUNIQR고유 식별자 문자열
EVIOCGPROPR디바이스 프로퍼티 비트마스크 (INPUT_PROP_*)
EVIOCGBIT(type, size)R지원하는 이벤트 유형/코드 비트마스크
EVIOCGABS(axis)Rstruct input_absinfo (min, max, fuzz, flat, res)
EVIOCSABS(axis)W절대축 파라미터 변경 (캘리브레이션)
EVIOCGKEYR현재 키 상태 비트마스크 (눌린 키 조회)
EVIOCGLEDR현재 LED 상태 비트마스크
EVIOCGSWR현재 스위치 상태 비트마스크
EVIOCGRABW디바이스 독점(grab) — 다른 프로세스/핸들러 차단
EVIOCREVOKEWfd의 접근 권한 철회 (logind 세션 전환 시)
EVIOCSFFWForce Feedback 효과 업로드
EVIOCRMFFWForce Feedback 효과 삭제
EVIOCGEFFECTSR동시 FF 효과 수
EVIOCGRAB — 디바이스 독점: ioctl(fd, EVIOCGRAB, 1)을 호출하면 해당 fd만 이벤트를 수신합니다. 다른 모든 evdev 클라이언트와 kbd/mousedev 핸들러가 이벤트를 받지 못합니다. 게임, 스크린 잠금, 키 매핑 도구에서 사용되며, EVIOCGRAB, 0으로 해제합니다.

Force Feedback (FF)

Force Feedback은 게임패드 진동, 스티어링 휠 저항, 햅틱 피드백 등을 지원합니다. 커널은 FF 효과를 디바이스에 업로드하고 재생/정지를 제어하는 프레임워크를 제공합니다.

/* === 커널 드라이버: FF 지원 등록 === */
#include <linux/input.h>

static int my_ff_upload(struct input_dev *dev,
                        struct ff_effect *effect,
                        struct ff_effect *old)
{
    /* 효과를 하드웨어에 프로그래밍 */
    write_ff_to_hw(effect);
    return 0;
}

static int my_ff_playback(struct input_dev *dev,
                          int effect_id, int value)
{
    /* value: 1=재생, 0=정지 */
    if (value)
        start_hw_effect(effect_id);
    else
        stop_hw_effect(effect_id);
    return 0;
}

static int setup_ff(struct input_dev *idev)
{
    input_set_capability(idev, EV_FF, FF_RUMBLE);
    input_set_capability(idev, EV_FF, FF_PERIODIC);
    input_set_capability(idev, EV_FF, FF_CONSTANT);

    /* 최대 동시 효과 수, 업로드/재생 콜백 */
    return input_ff_create(idev, 16);
    /* input_ff_create() 이후 콜백 설정 */
    idev->ff->upload = my_ff_upload;
    idev->ff->playback = my_ff_playback;
}

/* === 간편 rumble API (많은 게임패드에서 사용) ===
 * upload/playback 콜백 없이 드라이버가 직접 모터 제어 */
static int my_play_effect(struct input_dev *dev,
                          void *data,
                          struct ff_effect *effect)
{
    u16 strong = effect->u.rumble.strong_magnitude;
    u16 weak   = effect->u.rumble.weak_magnitude;

    /* 모터 강도 설정 */
    set_motor_speed(data, strong, weak);
    return 0;
}

/* input_ff_create_memless()로 간편 등록 */
input_ff_create_memless(idev, priv, my_play_effect);

Input 프로퍼티 (INPUT_PROP_*)

Input 프로퍼티는 디바이스의 물리적 특성을 유저 공간에 알려줍니다. input_set_capability()가 아닌 set_bit()으로 dev->propbit에 설정합니다.

프로퍼티설명사용처
INPUT_PROP_DIRECT화면에 직접 맵핑되는 입력 (터치스크린)좌표 변환 없이 화면 좌표로 사용
INPUT_PROP_POINTER간접 포인팅 (터치패드, 트랙볼)가속 커브 적용, 커서 제어
INPUT_PROP_SEMI_MT불완전한 멀티터치 (바운딩 박스만 제공)저가 터치패드
INPUT_PROP_TOPBUTTONPAD상단에 소프트 버튼 영역 (클릭패드)ThinkPad 등 트랙포인트 버튼
INPUT_PROP_BUTTONPAD패드 전체가 버튼 (클릭패드)MacBook, 최신 노트북 터치패드
INPUT_PROP_ACCELEROMETER가속도 센서 (포인팅 아님)화면 회전, 게임 기울기
/* 터치스크린: DIRECT 프로퍼티 설정 */
set_bit(INPUT_PROP_DIRECT, idev->propbit);

/* 터치패드: POINTER + BUTTONPAD */
set_bit(INPUT_PROP_POINTER, idev->propbit);
set_bit(INPUT_PROP_BUTTONPAD, idev->propbit);

키코드 매핑과 스캔코드

Input 서브시스템은 하드웨어 스캔코드(scancode)를 리눅스 키코드(keycode)로 변환하는 2단계 매핑을 지원합니다. 키코드 테이블은 런타임에 변경 가능하여 유저 공간에서 키 재매핑이 가능합니다.

/* 커널 드라이버에서 키코드 테이블 설정 */
static const unsigned short my_keymap[] = {
    [0x01] = KEY_ESC,
    [0x02] = KEY_1,
    [0x03] = KEY_2,
    /* ... */
};

idev->keycode     = my_keymap;
idev->keycodesize = sizeof(my_keymap[0]);
idev->keycodemax  = ARRAY_SIZE(my_keymap);

/* 선택: 커스텀 getkeycode/setkeycode 콜백
 * 기본 구현은 keycode[] 배열을 인덱스로 접근
 * 희소(sparse) 매핑이 필요하면 커스텀 콜백 구현 */
idev->getkeycode = my_getkeycode;
idev->setkeycode = my_setkeycode;
# 유저 공간에서 키 재매핑 (evdev ioctl)
# EVIOCSKEYCODE — scancode → keycode 변경

# udevadm hwdb 기반 자동 매핑 (권장)
# /etc/udev/hwdb.d/70-keyboard.hwdb:
evdev:input:b0003v046DpC52B*
 KEYBOARD_KEY_70039=esc        # CapsLock → Escape
 KEYBOARD_KEY_70029=capslock   # Escape → CapsLock

# hwdb 업데이트 적용
sudo systemd-hwdb update
sudo udevadm trigger

디버깅 도구

# === /proc/bus/input/devices — 등록된 모든 Input 디바이스 === 
cat /proc/bus/input/devices
# I: Bus=0011 Vendor=0001 Product=0001 Version=ab54
# N: Name="AT Translated Set 2 keyboard"
# P: Phys=isa0060/serio0/input0
# S: Sysfs=/devices/platform/i8042/serio0/input/input0
# U: Uniq=
# H: Handlers=sysrq kbd leds event0
# B: PROP=0
# B: EV=120013
# B: KEY=402000000 3803078f800d001 feffffdfffefffff fffffffffffffffe
# B: MSC=10
# B: LED=7

# === /proc/bus/input/handlers — 등록된 핸들러 목록 ===
cat /proc/bus/input/handlers
# N: Number=0 Name=rfkill
# N: Number=1 Name=kbd
# N: Number=2 Name=mousedev Minor=32
# N: Number=3 Name=evdev Minor=64
# N: Number=4 Name=joydev Minor=0

# === sysfs Input 디바이스 정보 ===
ls /sys/class/input/input0/
# capabilities/  device/  event0/  id/  inhibited  name  phys  properties  uevent  uniq

# 디바이스 활성/비활성 (v5.14+)
echo 1 > /sys/class/input/input0/inhibited   # 일시 비활성
echo 0 > /sys/class/input/input0/inhibited   # 재활성

# === evtest — 실시간 이벤트 모니터링 (가장 유용한 도구) ===
sudo evtest /dev/input/event0
# Event: time 1234567890.123456, type 1 (EV_KEY), code 30 (KEY_A), value 1
# Event: time 1234567890.123456, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0
# Event: time 1234567890.234567, type 1 (EV_KEY), code 30 (KEY_A), value 0
# Event: time 1234567890.234567, type 0 (EV_SYN), code 0 (SYN_REPORT), value 0

# === libinput debug-events — 고수준 이벤트 분석 ===
sudo libinput debug-events
# -event2   KEYBOARD_KEY     +3.24s  KEY_A (30) pressed
# -event2   KEYBOARD_KEY     +3.38s  KEY_A (30) released
# -event5   POINTER_MOTION   +5.12s  12.00/ -3.00

# === evemu — 이벤트 녹화/재생 (재현 테스트용) ===
sudo evemu-record /dev/input/event0 > recording.txt   # 녹화
sudo evemu-play /dev/input/event0 < recording.txt     # 재생

# === 커널 디버그: input 이벤트 추적 ===
echo 1 > /sys/module/evdev/parameters/debug            # evdev 디버그 (if available)

# ftrace로 Input 이벤트 추적
echo 1 > /sys/kernel/debug/tracing/events/input/input_event/enable
cat /sys/kernel/debug/tracing/trace_pipe
# irq/18-i8042-18 input_event: dev=input0, type=1, code=30, value=1
Input 디바이스 시뮬레이션 (uinput): /dev/uinput을 통해 유저 공간에서 가상 입력 디바이스를 생성할 수 있습니다. 자동화 테스트, 원격 입력, 매크로 도구에 활용됩니다.
/* === uinput: 유저 공간에서 가상 Input 디바이스 생성 === */
#include <linux/uinput.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

int create_virtual_kbd(void)
{
    int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK);
    struct uinput_setup usetup;

    /* 이벤트 유형 활성화 */
    ioctl(fd, UI_SET_EVBIT, EV_KEY);
    ioctl(fd, UI_SET_KEYBIT, KEY_A);
    ioctl(fd, UI_SET_KEYBIT, KEY_B);

    /* 디바이스 정보 설정 */
    memset(&usetup, 0, sizeof(usetup));
    usetup.id.bustype = BUS_USB;
    usetup.id.vendor  = 0x1234;
    usetup.id.product = 0x5678;
    strcpy(usetup.name, "Virtual Keyboard");

    ioctl(fd, UI_DEV_SETUP, &usetup);
    ioctl(fd, UI_DEV_CREATE);

    /* 키 이벤트 주입 */
    struct input_event ev = { .type = EV_KEY, .code = KEY_A, .value = 1 };
    write(fd, &ev, sizeof(ev));
    ev.type = EV_SYN; ev.code = SYN_REPORT; ev.value = 0;
    write(fd, &ev, sizeof(ev));

    /* 해제 */
    ioctl(fd, UI_DEV_DESTROY);
    close(fd);
    return 0;
}

이벤트 흐름 요약

Hardware IRQ 발생 또는 폴링 Device Driver input_report_*() input_sync() Input Core input_handle_event() handler에 전파 evdev Handler evdev_event() client 버퍼에 저장 User Space read() / poll() /dev/input/eventN input_report_*() → input_handle_event() → handler->event() → evdev client buffer → read() fuzz 필터링은 Input Core에서, 타임스탬프는 input_handle_event()에서 부여
주요 소스 파일:
  • drivers/input/input.c — Input Core: 디바이스/핸들러 등록, 이벤트 라우팅
  • drivers/input/evdev.c — evdev 핸들러: /dev/input/eventN 캐릭터 디바이스
  • drivers/input/mousedev.c — 레거시 PS/2 마우스 에뮬레이션
  • drivers/input/joydev.c — 레거시 조이스틱 인터페이스
  • drivers/input/keyboard/ — 키보드 드라이버 (atkbd, gpio_keys, ...)
  • drivers/input/mouse/ — 마우스/터치패드 드라이버 (psmouse, elantech, ...)
  • drivers/input/touchscreen/ — 터치스크린 드라이버 (goodix, atmel_mxt, ...)
  • drivers/input/misc/ — 기타 (uinput, pwm-beeper, ...)
  • include/linux/input.h — 핵심 헤더 (struct input_dev, 이벤트 보고 API)
  • include/uapi/linux/input-event-codes.h — 모든 이벤트 유형/코드 상수 정의

USB 서브시스템

USB(Universal Serial Bus) 서브시스템은 리눅스 커널에서 가장 크고 복잡한 드라이버 프레임워크 중 하나입니다. 호스트 컨트롤러 드라이버(HCD), USB 코어, 디바이스 드라이버의 3계층으로 구성되며, 핫플러그와 다양한 전송 유형을 지원합니다.

USB 소프트웨어 스택 & 디바이스 토폴로지 User Space libusb / usbfs / /dev/bus/usb/* USB 디바이스 드라이버 (Class / Vendor Specific) usb-storage usbhid cdc-acm uvcvideo custom drv USB Core (drivers/usb/core/) 디바이스 열거 · URB 관리 · 허브 드라이버 · 전원 관리 · sysfs 인터페이스 호스트 컨트롤러 드라이버 (HCD) xhci-hcd ehci-hcd ohci-hcd dwc2/dwc3 musb 하드웨어 토폴로지 Root Hub Hub Device A Device B Device C
USB 소프트웨어 스택 — User Space, 디바이스 드라이버, USB Core, HCD, 하드웨어 토폴로지

USB 프로토콜 버전 비교

버전최대 속도전력 공급커넥터호스트 컨트롤러
USB 1.112 Mbps (Full Speed)500 mA (5V)Type-A/BUHCI / OHCI
USB 2.0480 Mbps (High Speed)500 mA (5V)Type-A/B, Mini, MicroEHCI
USB 3.05 Gbps (SuperSpeed)900 mA (5V)Type-A/B (SS), Type-CxHCI
USB 3.110 Gbps (SS+)최대 100W (USB PD)Type-CxHCI
USB 3.220 Gbps (2×2)최대 100W (USB PD)Type-CxHCI
USB440/80 Gbps최대 240W (USB PD 3.1)Type-CxHCI + USB4 호스트 라우터

USB 디스크립터 계층 구조 — USB 디바이스는 계층적 디스크립터로 자신의 기능을 호스트에 알립니다. 커널은 열거(enumeration) 과정에서 이 디스크립터들을 읽어 적절한 드라이버를 바인딩합니다.

디스크립터커널 구조체주요 필드설명
Device struct usb_device_descriptor idVendor, idProduct, bDeviceClass, bNumConfigurations 디바이스 전체를 식별. VID/PID로 드라이버 매칭
Configuration struct usb_config_descriptor bNumInterfaces, bConfigurationValue, bmAttributes 전력 소비, 인터페이스 개수. 동시에 하나만 활성
Interface struct usb_interface_descriptor bInterfaceClass, bInterfaceSubClass, bNumEndpoints 드라이버 바인딩 단위. 하나의 기능을 나타냄
Endpoint struct usb_endpoint_descriptor bEndpointAddress, bmAttributes, wMaxPacketSize, bInterval 실제 데이터 전송 채널. 방향(IN/OUT)과 전송 유형 정의
드라이버 바인딩 단위: USB 드라이버는 디바이스가 아니라 인터페이스 단위로 바인딩됩니다. 복합 디바이스(예: 웹캠+마이크)는 여러 인터페이스를 가지며, 각 인터페이스에 서로 다른 드라이버가 바인딩될 수 있습니다. probe/disconnect 콜백의 첫 번째 인자가 struct usb_interface *인 이유입니다.

USB 전송 유형 — 4가지 전송 유형은 각각 다른 QoS 특성을 가지며, 디바이스의 엔드포인트 디스크립터에 정의됩니다.

전송 유형용도대역폭 보장에러 재전송최대 패킷 (HS)예시 디바이스
Control 디바이스 설정, 상태 조회 보장 (10%) 있음 64 bytes 모든 디바이스 (EP0)
Bulk 대용량 데이터 전송 없음 (잔여 대역폭) 있음 512 bytes 저장장치, 프린터
Interrupt 소량 주기적 데이터 보장 (지연 한계) 있음 1024 bytes 키보드, 마우스
Isochronous 실시간 스트리밍 보장 (대역폭 예약) 없음 1024 bytes (×3) 오디오, 웹캠

URB(USB Request Block) 라이프사이클 — URB는 USB I/O의 핵심 데이터 구조로, 비동기적으로 USB 코어에 전송 요청을 전달합니다.

/* URB 라이프사이클: 할당 → 초기화(fill) → 제출(submit) → 완료 콜백 → 해제 */

/* 1. URB 할당 */
struct urb *urb = usb_alloc_urb(0, GFP_KERNEL);  /* iso_packets=0 (non-iso) */
if (!urb)
    return -ENOMEM;

/* 2. 전송 유형별 초기화 함수 */
usb_fill_control_urb(urb, udev, pipe, setup, buf, len, callback, context);
usb_fill_bulk_urb(urb, udev, pipe, buf, len, callback, context);
usb_fill_int_urb(urb, udev, pipe, buf, len, callback, context, interval);
/* isochronous는 fill 함수 없음 — 수동으로 urb 필드 설정 필요 */

/* 3. URB 제출 (비동기) */
int ret = usb_submit_urb(urb, GFP_KERNEL);
if (ret) {
    dev_err(&intf->dev, "URB submit failed: %d\n", ret);
    usb_free_urb(urb);
    return ret;
}

/* 4. 완료 콜백 (인터럽트 컨텍스트에서 호출됨!) */
static void my_urb_complete(struct urb *urb)
{
    if (urb->status) {
        if (urb->status == -ENOENT ||     /* usb_kill_urb() */
            urb->status == -ECONNRESET || /* usb_unlink_urb() */
            urb->status == -ESHUTDOWN)    /* 디바이스 제거 */
            return;
        /* 기타 에러 처리 */
    }

    /* urb->actual_length 바이트만큼 데이터 수신 완료 */
    process_data(urb->transfer_buffer, urb->actual_length);

    /* Interrupt/Isochronous: 연속 전송을 위해 재제출 */
    usb_submit_urb(urb, GFP_ATOMIC);  /* 인터럽트 컨텍스트 → GFP_ATOMIC */
}

/* 5. URB 취소 및 해제 (disconnect 시) */
usb_kill_urb(urb);   /* 동기적으로 취소 후 콜백 완료까지 대기 */
usb_free_urb(urb);   /* 참조 카운트 감소, 0이면 해제 */
URB 콜백 컨텍스트: URB 완료 콜백은 인터럽트 컨텍스트(하드/소프트 IRQ)에서 호출됩니다. sleep 가능 함수(kmalloc(GFP_KERNEL), mutex_lock, usb_control_msg 등) 사용이 불가합니다. 메모리 할당 시 GFP_ATOMIC을 사용하고, 복잡한 처리는 workqueue로 지연시키세요.

완전한 USB 드라이버 예제 — probe, disconnect, URB 기반 데이터 수신을 포함하는 실용적인 USB 드라이버 골격입니다.

#include <linux/module.h>
#include <linux/usb.h>
#include <linux/slab.h>

#define MY_VENDOR_ID   0x1234
#define MY_PRODUCT_ID  0x5678
#define MY_BUF_SIZE    512

struct my_usb_dev {
    struct usb_device    *udev;
    struct usb_interface *intf;
    struct urb           *bulk_in_urb;
    unsigned char        *bulk_in_buf;
    size_t                bulk_in_size;
    __u8                  bulk_in_addr;
    __u8                  bulk_out_addr;
};

static void my_bulk_complete(struct urb *urb)
{
    struct my_usb_dev *dev = urb->context;

    if (urb->status) {
        if (urb->status == -ENOENT ||
            urb->status == -ECONNRESET ||
            urb->status == -ESHUTDOWN)
            return;
        dev_err(&dev->intf->dev, "bulk IN error: %d\n", urb->status);
        return;
    }

    dev_dbg(&dev->intf->dev, "received %d bytes\n", urb->actual_length);
    /* 수신 데이터 처리 ... */
}

/* USB 디바이스 ID 테이블 */
static const struct usb_device_id my_usb_ids[] = {
    { USB_DEVICE(MY_VENDOR_ID, MY_PRODUCT_ID) },
    { USB_INTERFACE_INFO(USB_CLASS_VENDOR_SPEC, 0x01, 0x01) },
    { }  /* 종료 엔트리 */
};
MODULE_DEVICE_TABLE(usb, my_usb_ids);

static int my_usb_probe(struct usb_interface *intf,
        const struct usb_device_id *id)
{
    struct usb_device *udev = interface_to_usbdev(intf);
    struct usb_host_interface *iface_desc = intf->cur_altsetting;
    struct usb_endpoint_descriptor *ep;
    struct my_usb_dev *dev;
    int i;

    /* 디바이스 구조체 할당 */
    dev = kzalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->udev = usb_get_dev(udev);
    dev->intf = intf;

    /* 엔드포인트 탐색 — Bulk IN/OUT 찾기 */
    for (i = 0; i < iface_desc->desc.bNumEndpoints; i++) {
        ep = &iface_desc->endpoint[i].desc;

        if (usb_endpoint_is_bulk_in(ep) && !dev->bulk_in_addr) {
            dev->bulk_in_size = usb_endpoint_maxp(ep);
            dev->bulk_in_addr = ep->bEndpointAddress;
        }
        if (usb_endpoint_is_bulk_out(ep) && !dev->bulk_out_addr)
            dev->bulk_out_addr = ep->bEndpointAddress;
    }

    if (!dev->bulk_in_addr || !dev->bulk_out_addr) {
        dev_err(&intf->dev, "required endpoints not found\n");
        goto error;
    }

    /* Bulk IN URB 준비 */
    dev->bulk_in_buf = kmalloc(MY_BUF_SIZE, GFP_KERNEL);
    dev->bulk_in_urb = usb_alloc_urb(0, GFP_KERNEL);
    if (!dev->bulk_in_buf || !dev->bulk_in_urb) {
        dev_err(&intf->dev, "allocation failed\n");
        goto error;
    }

    usb_fill_bulk_urb(dev->bulk_in_urb, udev,
        usb_rcvbulkpipe(udev, dev->bulk_in_addr),
        dev->bulk_in_buf, MY_BUF_SIZE,
        my_bulk_complete, dev);

    /* 인터페이스에 드라이버 데이터 저장 */
    usb_set_intfdata(intf, dev);

    dev_info(&intf->dev, "USB device connected (VID=%04x PID=%04x)\n",
        le16_to_cpu(udev->descriptor.idVendor),
        le16_to_cpu(udev->descriptor.idProduct));
    return 0;

error:
    usb_free_urb(dev->bulk_in_urb);
    kfree(dev->bulk_in_buf);
    usb_put_dev(dev->udev);
    kfree(dev);
    return -ENOMEM;
}

static void my_usb_disconnect(struct usb_interface *intf)
{
    struct my_usb_dev *dev = usb_get_intfdata(intf);

    usb_set_intfdata(intf, NULL);
    usb_kill_urb(dev->bulk_in_urb);  /* 진행 중인 URB 동기적 취소 */
    usb_free_urb(dev->bulk_in_urb);
    kfree(dev->bulk_in_buf);
    usb_put_dev(dev->udev);
    kfree(dev);

    dev_info(&intf->dev, "USB device disconnected\n");
}

static struct usb_driver my_usb_driver = {
    .name       = "my_usb_drv",
    .id_table   = my_usb_ids,
    .probe      = my_usb_probe,
    .disconnect = my_usb_disconnect,
};

module_usb_driver(my_usb_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example USB driver");

호스트 컨트롤러 드라이버(HCD) 비교 — 호스트 컨트롤러는 USB 버스의 물리적 인터페이스를 제어하며, 각 세대별로 다른 HCD 모듈이 필요합니다.

HCDUSB 버전커널 모듈특징
UHCI USB 1.x uhci-hcd Intel 설계. 소프트웨어 스케줄링 (CPU 부하 높음)
OHCI USB 1.x ohci-hcd 하드웨어 스케줄링. 임베디드 SoC에서 여전히 사용
EHCI USB 2.0 ehci-hcd High Speed 전용. FS/LS는 컴패니언(UHCI/OHCI)에 위임
xHCI USB 1.x ~ USB4 xhci-hcd 모든 속도 통합 지원. 스트림, MSI-X, 64비트 DMA
DWC2 USB 2.0 OTG dwc2 Synopsys DesignWare. Raspberry Pi, STM32 등 SoC
DWC3 USB 3.x dwc3 Synopsys DesignWare. 최신 ARM SoC에서 광범위하게 사용
USB Gadget 프레임워크: 위 내용은 USB 호스트 측 드라이버입니다. 리눅스가 USB 디바이스 역할을 할 때(OTG, 임베디드 보드)는 USB Gadget API(drivers/usb/gadget/)를 사용합니다. ConfigFS(/sys/kernel/config/usb_gadget/)를 통해 유저 스페이스에서 USB 기능(mass storage, ethernet, serial 등)을 동적으로 구성할 수 있으며, usb_composite_driver로 복합 Gadget 드라이버를 작성합니다.
USB 디버깅 도구:
  • lsusb -v — 디바이스 디스크립터 전체 덤프 (VID/PID, 클래스, 엔드포인트 등)
  • lsusb -t — 버스 토폴로지를 트리 형태로 표시
  • usbmon — 커널 USB 패킷 모니터. mount -t debugfs none /sys/kernel/debug/sys/kernel/debug/usb/usbmon/에서 캡처
  • Wireshark — usbmon 인터페이스를 직접 캡처하여 USB 프로토콜 분석 가능
  • /sys/bus/usb/devices/ — sysfs를 통한 디바이스 속성 조회 (idVendor, idProduct, speed, manufacturer 등)
  • usb-devices/proc/bus/usb/devices를 파싱하여 사람이 읽기 쉬운 형태로 출력
USB 드라이버 핵심 주의사항:
  • 핫플러그 안전성 — disconnect 콜백에서 모든 URB를 usb_kill_urb()로 동기적 취소 후 리소스 해제. race condition 방지를 위해 usb_set_intfdata(intf, NULL)을 먼저 호출
  • USB 3.x Stream — Bulk 엔드포인트에 여러 스트림을 할당하여 병렬 전송 가능. UAS(USB Attached SCSI)에서 활용. usb_alloc_streams()로 설정
  • 전원 관리usb_autopm_get_interface()/usb_autopm_put_interface()로 autosuspend 관리. I/O 전에 반드시 get, 완료 후 put 호출
  • 대안 API — 단순한 동기 전송은 usb_bulk_msg(), usb_control_msg()로 가능 (내부적으로 URB 사용). 프로토타이핑이나 설정 단계에서 유용

Video 서브시스템 (V4L2 / DRM)

프레임워크용도커널 코드유저 API
V4L2 카메라, 비디오 캡처/출력, 코덱 drivers/media/ /dev/videoN, ioctl 기반
DRM/KMS GPU, 디스플레이 출력, 모드 설정 drivers/gpu/drm/ /dev/dri/cardN, GEM/dumb buffer
fbdev 프레임버퍼 (레거시) drivers/video/fbdev/ /dev/fb0 — DRM으로 대체 추세

Audio 서브시스템 (ALSA)

ALSA(Advanced Linux Sound Architecture)는 리눅스 커널의 사운드 서브시스템으로, sound/ 디렉토리에 위치합니다. OSS(Open Sound System)를 대체하며, PCM 재생/녹음, MIDI, 하드웨어 믹서, 시퀀서 등을 지원합니다.

상세 문서: ALSA 서브시스템의 심층 분석은 ALSA 서브시스템 심화 페이지에서 확인할 수 있습니다.

ALSA 아키텍처

User Space aplay / arecord PulseAudio / PipeWire ALSA lib (libasound) amixer / alsamixer JACK / PortAudio ioctl / read / write / mmap ALSA Core (sound/core/) snd_card (카드 관리) snd_pcm (PCM 스트림) snd_kcontrol (믹서/볼륨) snd_rawmidi (MIDI) snd_timer (타이밍) snd_hwdep (HW 의존) Low-level Drivers snd-hda-intel (HD Audio) snd-usb-audio (USB Audio) snd-soc-* (ASoC 드라이버) ASoC Framework (sound/soc/) Codec Driver (DAC/ADC) Platform DMA (DMA 엔진) Machine Driver (보드 연결) Hardware HD Audio Controller (PCI, MMIO) USB Audio Device (UAC 1.0/2.0/3.0) I2S / TDM Controller (SoC 내장) Audio Codec Chip (I2C/SPI 제어)
커널 디렉토리역할디바이스 노드
sound/core/ALSA 코어 — 카드 관리, PCM, 컨트롤, 타이머, MIDI 프레임워크/dev/snd/controlC*
sound/pci/PCI 사운드 카드 드라이버 (hda-intel, emu10k1, cmipci 등)/dev/snd/pcmC*D*p, c
sound/usb/USB Audio 드라이버 (UAC 1.0/2.0/3.0)/dev/snd/pcmC*D*p
sound/soc/ASoC 프레임워크 — 임베디드/SoC 오디오 (Codec + Platform + Machine)/dev/snd/pcmC*D*p
sound/hda/HD Audio 공통 코어 — HDA 버스, 코덱 공통 로직
sound/firewire/FireWire (IEEE 1394) 오디오 장치 드라이버/dev/snd/hwC*D*
sound/virtio/Virtio 사운드 디바이스 (가상화 환경)/dev/snd/pcmC*D*p

핵심 구조체

/* === snd_card: 사운드 카드 최상위 컨테이너 === */
struct snd_card {
    int                 number;        /* 카드 인덱스 (0, 1, 2, ...) */
    char                id[16];        /* 짧은 이름 ("Intel") */
    char                longname[80];  /* 긴 이름 */
    struct device       *dev;           /* 부모 디바이스 */
    struct list_head    devices;        /* snd_device 연결 리스트 */
    struct list_head    controls;       /* snd_kcontrol 리스트 */
    void                *private_data;  /* 드라이버 전용 데이터 */
    void (*private_free)(struct snd_card *); /* 해제 콜백 */
    ...
};

/* === snd_pcm: PCM 인스턴스 (Playback/Capture 스트림 쌍) === */
struct snd_pcm {
    struct snd_card    *card;          /* 소속 카드 */
    int                device;         /* PCM 디바이스 번호 */
    struct snd_pcm_str streams[2];     /* [0]=PLAYBACK, [1]=CAPTURE */
    ...
};

/* === snd_pcm_substream: 개별 PCM 스트림 === */
struct snd_pcm_substream {
    struct snd_pcm           *pcm;
    int                      stream;        /* PLAYBACK 또는 CAPTURE */
    struct snd_pcm_ops       *ops;           /* 드라이버 오퍼레이션 */
    struct snd_pcm_runtime   *runtime;       /* 런타임 상태 (아래 참조) */
    struct snd_dma_buffer    dma_buffer;     /* DMA 버퍼 정보 */
    ...
};

/* === snd_pcm_runtime: 런타임 파라미터와 상태 === */
struct snd_pcm_runtime {
    struct snd_pcm_hardware  hw;          /* 하드웨어 능력 */
    snd_pcm_format_t        format;      /* 현재 샘플 포맷 */
    unsigned int            rate;        /* 샘플레이트 */
    unsigned int            channels;    /* 채널 수 */
    snd_pcm_uframes_t       buffer_size; /* 전체 버퍼 (프레임) */
    snd_pcm_uframes_t       period_size; /* 인터럽트 주기 (프레임) */
    snd_pcm_state_t         state;       /* OPEN, SETUP, PREPARED, RUNNING, ... */
    unsigned char           *dma_area;   /* DMA 버퍼 가상 주소 */
    dma_addr_t              dma_addr;    /* DMA 버퍼 물리 주소 */
    size_t                  dma_bytes;   /* DMA 버퍼 크기 */
    ...
};

PCM 드라이버 구현

#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>

/* 하드웨어 제약 조건 정의 — open()에서 runtime->hw에 복사됨 */
static const struct snd_pcm_hardware my_pcm_hw = {
    .info           = SNDRV_PCM_INFO_MMAP |
                      SNDRV_PCM_INFO_MMAP_VALID |
                      SNDRV_PCM_INFO_INTERLEAVED |
                      SNDRV_PCM_INFO_RESUME,
    .formats        = SNDRV_PCM_FMTBIT_S16_LE |
                      SNDRV_PCM_FMTBIT_S24_LE |
                      SNDRV_PCM_FMTBIT_S32_LE,      /* 지원 포맷 */
    .rates          = SNDRV_PCM_RATE_44100 |
                      SNDRV_PCM_RATE_48000 |
                      SNDRV_PCM_RATE_96000,          /* 지원 샘플레이트 */
    .rate_min       = 44100,
    .rate_max       = 96000,
    .channels_min   = 2,
    .channels_max   = 8,
    .buffer_bytes_max = 256 * 1024,       /* 최대 DMA 버퍼 (256KB) */
    .period_bytes_min = 64,               /* 최소 period 크기 */
    .period_bytes_max = 128 * 1024,       /* 최대 period 크기 */
    .periods_min    = 2,                  /* 최소 period 개수 (더블 버퍼링) */
    .periods_max    = 32,
};

/* open: 서브스트림이 열릴 때 호출 */
static int my_pcm_open(struct snd_pcm_substream *substream)
{
    struct my_chip *chip = snd_pcm_substream_chip(substream);
    struct snd_pcm_runtime *runtime = substream->runtime;

    runtime->hw = my_pcm_hw;  /* 하드웨어 제약 복사 */
    /* 추가 제약 조건: period 크기를 128 프레임 단위로 정렬 */
    snd_pcm_hw_constraint_step(runtime, 0,
        SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 128);
    return 0;
}

/* hw_params: 유저 공간이 결정한 파라미터로 DMA 버퍼 할당 */
static int my_hw_params(struct snd_pcm_substream *substream,
                        struct snd_pcm_hw_params *params)
{
    /* 커널 관리 DMA 버퍼 할당 (devm 방식과 유사) */
    return snd_pcm_lib_malloc_pages(substream,
                params_buffer_bytes(params));
}

/* hw_free: DMA 버퍼 해제 */
static int my_hw_free(struct snd_pcm_substream *substream)
{
    return snd_pcm_lib_free_pages(substream);
}

/* prepare: 스트림 시작 전 하드웨어 초기화 */
static int my_pcm_prepare(struct snd_pcm_substream *substream)
{
    struct my_chip *chip = snd_pcm_substream_chip(substream);
    struct snd_pcm_runtime *runtime = substream->runtime;

    /* 하드웨어 레지스터에 포맷, 채널, 샘플레이트 설정 */
    my_hw_set_format(chip, runtime->format);
    my_hw_set_rate(chip, runtime->rate);
    my_hw_set_channels(chip, runtime->channels);

    /* DMA 주소와 크기를 하드웨어에 프로그래밍 */
    my_hw_set_dma(chip, runtime->dma_addr, runtime->dma_bytes);
    my_hw_set_period(chip,
        frames_to_bytes(runtime, runtime->period_size));
    return 0;
}

/* trigger: START/STOP 명령 — atomic 컨텍스트에서 호출됨! */
static int my_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
    struct my_chip *chip = snd_pcm_substream_chip(substream);

    switch (cmd) {
    case SNDRV_PCM_TRIGGER_START:
    case SNDRV_PCM_TRIGGER_RESUME:
        my_hw_start_dma(chip);     /* DMA 전송 시작 */
        break;
    case SNDRV_PCM_TRIGGER_STOP:
    case SNDRV_PCM_TRIGGER_SUSPEND:
        my_hw_stop_dma(chip);      /* DMA 전송 중지 */
        break;
    default:
        return -EINVAL;
    }
    return 0;
}

/* pointer: 현재 DMA 하드웨어 위치를 프레임 단위로 반환 */
static snd_pcm_uframes_t my_pcm_pointer(
        struct snd_pcm_substream *substream)
{
    struct my_chip *chip = snd_pcm_substream_chip(substream);
    unsigned int pos = my_hw_get_dma_pos(chip); /* 바이트 위치 */
    return bytes_to_frames(substream->runtime, pos);
}

/* PCM 오퍼레이션 테이블 */
static const struct snd_pcm_ops my_pcm_ops = {
    .open      = my_pcm_open,
    .close     = my_pcm_close,
    .hw_params = my_hw_params,
    .hw_free   = my_hw_free,
    .prepare   = my_pcm_prepare,
    .trigger   = my_pcm_trigger,
    .pointer   = my_pcm_pointer,
};
PCM 오퍼레이션 호출 흐름: 사용자 공간에서 ALSA PCM 디바이스를 사용하면 다음 순서로 콜백이 호출됩니다: openhw_paramspreparetrigger(START) → [pointer 반복 호출] → trigger(STOP)hw_freeclose. preparetrigger(START) 전에 여러 번 호출될 수 있습니다 (underrun 복구 시 포함).

사운드 카드 등록

static int my_snd_probe(struct platform_device *pdev)
{
    struct snd_card *card;
    struct snd_pcm *pcm;
    struct my_chip *chip;
    int err;

    /* 1. 사운드 카드 생성 (-1 = 자동 인덱스 할당) */
    err = snd_card_new(&pdev->dev, -1, NULL, THIS_MODULE,
                       sizeof(*chip), &card);
    if (err < 0)
        return err;

    chip = card->private_data;
    chip->card = card;

    /* 2. PCM 인스턴스 생성 (play=1, capture=1) */
    err = snd_pcm_new(card, "My PCM", 0, 1, 1, &pcm);
    if (err < 0)
        goto err_free;

    pcm->private_data = chip;
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &my_pcm_ops);
    snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &my_capture_ops);

    /* 3. DMA 버퍼 사전 할당 (managed) */
    snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV,
        &pdev->dev, 64 * 1024, 256 * 1024);  /* 기본 64KB, 최대 256KB */

    /* 4. Mixer 컨트롤 등록 (아래 참조) */
    err = my_mixer_new(chip);
    if (err < 0)
        goto err_free;

    /* 5. 카드 정보 설정 */
    strcpy(card->driver, "my_snd");
    strcpy(card->shortname, "My Sound Card");
    snprintf(card->longname, sizeof(card->longname),
             "%s at 0x%lx irq %d", card->shortname,
             chip->iobase, chip->irq);

    /* 6. 카드 등록 → /dev/snd/ 노드 생성, /proc/asound/ 등록 */
    err = snd_card_register(card);
    if (err < 0)
        goto err_free;

    platform_set_drvdata(pdev, card);
    return 0;

err_free:
    snd_card_free(card);
    return err;
}
Managed DMA 버퍼: snd_pcm_set_managed_buffer_all()을 사용하면 hw_params/hw_free에서 수동으로 snd_pcm_lib_malloc_pages()/snd_pcm_lib_free_pages()를 호출할 필요 없이 ALSA 코어가 DMA 버퍼 할당/해제를 자동 관리합니다. 최신 커널(5.2+)에서 권장하는 방식입니다.

Mixer / Control 인터페이스

ALSA Control 인터페이스(/dev/snd/controlC*)는 볼륨, 스위치, 열거형 등의 믹서 컨트롤을 유저 공간에 노출합니다. amixer, alsamixer 등의 도구가 이 인터페이스를 사용합니다.

#include <sound/control.h>

/* 볼륨 컨트롤 — info 콜백: 타입, 범위, 채널 수 알림 */
static int my_vol_info(struct snd_kcontrol *kctl,
                       struct snd_ctl_elem_info *uinfo)
{
    uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
    uinfo->count = 2;              /* 스테레오 (L/R) */
    uinfo->value.integer.min = 0;
    uinfo->value.integer.max = 100; /* 0~100 범위 */
    return 0;
}

/* get 콜백: 현재 값을 유저 공간에 반환 */
static int my_vol_get(struct snd_kcontrol *kctl,
                      struct snd_ctl_elem_value *uval)
{
    struct my_chip *chip = snd_kcontrol_chip(kctl);
    uval->value.integer.value[0] = chip->vol_l; /* Left */
    uval->value.integer.value[1] = chip->vol_r; /* Right */
    return 0;
}

/* put 콜백: 유저가 설정한 값을 하드웨어에 반영 */
static int my_vol_put(struct snd_kcontrol *kctl,
                      struct snd_ctl_elem_value *uval)
{
    struct my_chip *chip = snd_kcontrol_chip(kctl);
    int changed = 0;

    if (chip->vol_l != uval->value.integer.value[0]) {
        chip->vol_l = uval->value.integer.value[0];
        changed = 1;
    }
    if (chip->vol_r != uval->value.integer.value[1]) {
        chip->vol_r = uval->value.integer.value[1];
        changed = 1;
    }
    if (changed)
        my_hw_set_volume(chip, chip->vol_l, chip->vol_r);

    return changed;  /* 1=값 변경됨, 0=변경 없음 */
}

/* 컨트롤 정의 */
static const struct snd_kcontrol_new my_controls[] = {
    {
        .iface  = SNDRV_CTL_ELEM_IFACE_MIXER,
        .name   = "Master Playback Volume",  /* ALSA 표준 이름 규칙 */
        .info   = my_vol_info,
        .get    = my_vol_get,
        .put    = my_vol_put,
    },
    {   /* 스위치(on/off) 컨트롤 예 */
        .iface  = SNDRV_CTL_ELEM_IFACE_MIXER,
        .name   = "Master Playback Switch",
        .info   = snd_ctl_boolean_stereo_info,  /* 헬퍼 함수 */
        .get    = my_switch_get,
        .put    = my_switch_put,
    },
};

/* 카드에 컨트롤 일괄 등록 */
static int my_mixer_new(struct my_chip *chip)
{
    int i, err;
    for (i = 0; i < ARRAY_SIZE(my_controls); i++) {
        err = snd_ctl_add(chip->card,
                  snd_ctl_new1(&my_controls[i], chip));
        if (err < 0)
            return err;
    }
    return 0;
}
ALSA 컨트롤 이름 규칙: 컨트롤 이름은 "[Source] [Direction] [Function]" 형식을 따릅니다.
  • Source: Master, PCM, Line, Mic, CD, Headphone 등
  • Direction: Playback, Capture (생략 시 공통)
  • Function: Volume, Switch, Route, Source
예: "Master Playback Volume", "Capture Switch", "Mic Boost Volume". 이 규칙을 따르면 PulseAudio/PipeWire가 자동으로 컨트롤을 인식합니다.

인터럽트 핸들러와 Period 완료 통지

/* DMA 인터럽트 핸들러 — period_size 만큼 전송 완료 시 하드웨어가 인터럽트 발생 */
static irqreturn_t my_snd_irq(int irq, void *dev_id)
{
    struct my_chip *chip = dev_id;
    unsigned int status = my_hw_read_irq_status(chip);

    if (!(status & MY_IRQ_AUDIO))
        return IRQ_NONE;         /* 이 디바이스 인터럽트 아님 */

    my_hw_clear_irq(chip);          /* 인터럽트 비트 클리어 */

    /* ALSA 코어에 period 완료 통지
     * → 유저 공간의 poll()/select()가 깨어남
     * → pointer() 콜백 호출로 현재 위치 갱신 */
    if (chip->playback_substream)
        snd_pcm_period_elapsed(chip->playback_substream);
    if (chip->capture_substream)
        snd_pcm_period_elapsed(chip->capture_substream);

    return IRQ_HANDLED;
}
snd_pcm_period_elapsed() 주의사항:
  • 인터럽트 컨텍스트에서 호출해도 안전하지만, 내부적으로 snd_pcm_stream_lock()을 잡음. trigger() 콜백 내부에서 직접 호출하면 데드락 발생
  • trigger()에서 period_elapsed 호출이 필요한 경우 tasklet이나 workqueue로 지연 처리
  • DMA가 중지된 상태에서 호출하면 XRUN(underrun/overrun) 처리가 트리거됨

PCM 상태 머신

OPEN SETUP PREPARED RUNNING DRAINING XRUN PAUSED hw_params prepare trigger(START) drain underrun prepare trigger(PAUSE_PUSH) PAUSE_RELEASE 완료

ASoC (ALSA System on Chip) 프레임워크

임베디드/SoC 환경에서는 직접 ALSA 드라이버를 작성하는 대신 ASoC 프레임워크(sound/soc/)를 사용합니다. 오디오 경로를 Codec, Platform(DMA), Machine(보드 연결) 3계층으로 분리하여 코드 재사용성을 극대화합니다.

ASoC 계층역할구현 위치예시
Codec Driver DAC/ADC 칩 제어. I2C/SPI로 레지스터 R/W. DAPM 위젯 정의 sound/soc/codecs/ wm8960, rt5682, cs42l42, tas2781
Platform Driver SoC의 I2S/TDM/PDM 컨트롤러 + DMA 엔진. PCM 데이터 전송 담당 sound/soc/<vendor>/ fsl-sai, stm32-sai, rockchip-i2s
Machine Driver Codec와 Platform을 연결. DAI 링크 정의, 보드별 GPIO/앰프 제어 sound/soc/<vendor>/ 또는 sound/soc/generic/ simple-audio-card, rpi-dac
#include <sound/soc.h>

/* === Codec Driver (예: 간단한 DAC) === */
static const struct snd_soc_dapm_widget my_codec_widgets[] = {
    SND_SOC_DAPM_DAC("DAC", "Playback", MY_REG_POWER, 0, 0),
    SND_SOC_DAPM_OUTPUT("HPOUT"),
    SND_SOC_DAPM_ADC("ADC", "Capture", MY_REG_POWER, 1, 0),
    SND_SOC_DAPM_INPUT("MICIN"),
};

static const struct snd_soc_dapm_route my_codec_routes[] = {
    { "HPOUT", NULL, "DAC" },      /* DAC → 헤드폰 출력 */
    { "ADC",   NULL, "MICIN" },    /* 마이크 입력 → ADC */
};

static const struct snd_soc_component_driver my_codec_comp = {
    .dapm_widgets       = my_codec_widgets,
    .num_dapm_widgets   = ARRAY_SIZE(my_codec_widgets),
    .dapm_routes        = my_codec_routes,
    .num_dapm_routes    = ARRAY_SIZE(my_codec_routes),
};

/* DAI(Digital Audio Interface) 정의 */
static struct snd_soc_dai_driver my_codec_dai = {
    .name = "my-codec-hifi",
    .playback = {
        .stream_name  = "Playback",
        .channels_min = 2,
        .channels_max = 2,
        .rates        = SNDRV_PCM_RATE_48000,
        .formats      = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE,
    },
    .capture = {
        .stream_name  = "Capture",
        .channels_min = 1,
        .channels_max = 2,
        .rates        = SNDRV_PCM_RATE_48000,
        .formats      = SNDRV_PCM_FMTBIT_S16_LE,
    },
    .ops = &my_codec_dai_ops,  /* set_fmt, set_sysclk, hw_params */
};

/* Codec 드라이버 등록 (I2C 디바이스) */
static int my_codec_i2c_probe(struct i2c_client *client)
{
    return devm_snd_soc_register_component(&client->dev,
        &my_codec_comp, &my_codec_dai, 1);
}
/* Device Tree — simple-audio-card로 Machine 정의 */
sound {
    compatible = "simple-audio-card";
    simple-audio-card,name = "My Board Audio";
    simple-audio-card,format = "i2s";

    simple-audio-card,cpu {
        sound-dai = <&i2s0>;    /* Platform: SoC의 I2S 컨트롤러 */
    };
    simple-audio-card,codec {
        sound-dai = <&codec0>;  /* Codec: I2C 연결된 DAC/ADC */
    };
};
DAPM (Dynamic Audio Power Management): ASoC의 DAPM 엔진은 오디오 경로 그래프를 기반으로 사용하지 않는 위젯(DAC, ADC, Mixer, Mux 등)의 전원을 자동으로 차단합니다. Codec 드라이버에서 SND_SOC_DAPM_* 위젯과 라우트를 정확히 정의하면, 유저가 경로를 연결/해제할 때 관련 하드웨어 블록이 자동으로 켜지거나 꺼집니다.

ALSA 디버깅과 진단

도구/파일용도예시
aplay -l 등록된 사운드 카드와 PCM 디바이스 목록 aplay -l
aplay -v 재생 시 상세 하드웨어 파라미터 출력 aplay -v -D hw:0,0 test.wav
arecord 캡처 테스트 arecord -f S16_LE -r 48000 -c 2 out.wav
amixer 컨트롤 값 조회/설정 amixer -c 0 contents
alsamixer TUI 기반 믹서 (볼륨, 스위치 시각적 조작) alsamixer -c 0
/proc/asound/ ALSA proc 파일시스템 (카드 정보, PCM 상태, 코덱 덤프) cat /proc/asound/cards
/proc/asound/card0/pcm0p/sub0/hw_params 현재 재생 중인 PCM의 실제 파라미터 access, format, rate, channels, period_size
/proc/asound/card0/codec#0 HD Audio 코덱 덤프 (HDA 전용) 코덱 위젯, 핀 설정, 연결 정보
viDMA / dmesg DMA 에러, underrun 로그 확인 dmesg | grep -i snd
snd_hda_codec_dbg HDA 코덱 디버그 트레이싱 (CONFIG_SND_HDA_RECONFIG) echo 1 > /sys/class/sound/hwC0D0/reconfig
# 등록된 사운드 카드 확인
cat /proc/asound/cards
# 출력 예:  0 [Intel  ]: HDA-Intel - HDA Intel PCH

# PCM 디바이스 목록
cat /proc/asound/pcm
# 출력 예: 00-00: ALC269 Analog : ALC269 Analog : playback 1 : capture 1

# 현재 재생 중인 스트림의 하드웨어 파라미터
cat /proc/asound/card0/pcm0p/sub0/hw_params
# access: MMAP_INTERLEAVED
# format: S16_LE
# channels: 2
# rate: 48000 (48000/1)
# period_size: 1024
# buffer_size: 16384

# XRUN(underrun) 디버깅 — 타이머 기반 XRUN 덤프 활성화
echo 7 > /proc/asound/card0/pcm0p/xrun_debug

# ASoC DAPM 경로 확인 (ASoC 드라이버 전용)
cat /sys/kernel/debug/asoc/<card-name>/dapm/<widget-name>
ALSA 드라이버 개발 주의사항:
  • trigger() 컨텍스트trigger() 콜백은 atomic 컨텍스트(spinlock 보유)에서 호출됩니다. sleep, mutex, kmalloc(GFP_KERNEL) 사용 불가
  • pointer() 정확도pointer()는 현재 DMA 위치를 정확히 반환해야 합니다. 부정확하면 오디오 글리치, 타이밍 오류, 리샘플링 아티팩트 발생
  • DMA 정렬period_sizebuffer_size는 DMA 전송 단위에 정렬되어야 합니다. snd_pcm_hw_constraint_step()으로 제약 조건 설정
  • XRUN 처리 — Underrun(재생 버퍼 고갈)/Overrun(캡처 버퍼 오버플로) 발생 시 ALSA 코어가 자동으로 XRUN 상태 전이. prepare()가 다시 호출되어 복구
  • 멀티스트림 — 하나의 snd_pcm에 여러 substream을 둘 수 있음(다중 재생). 하드웨어 믹싱 지원 시 유용
  • suspend/resume — PM 지원 시 snd_power_change_state()로 상태 전이. DMA 중지/재시작, 코덱 레지스터 저장/복원 필수

Serial / TTY 서브시스템

TTY(Teletypewriter) 서브시스템은 리눅스 커널에서 가장 오래되고 복잡한 계층 중 하나입니다. 원래 물리적 텔레타이프 단말기를 위해 설계되었지만, 현대 리눅스에서는 시리얼 포트, 가상 콘솔, 의사 터미널(PTY), USB 시리얼 등 다양한 문자 기반 I/O 인터페이스를 통합 관리합니다.

/dev/ttyS0 /dev/ttyUSB0 /dev/pts/N /dev/ttyN /dev/console TTY Core (drivers/tty/tty_io.c) tty_struct · tty_driver · tty_port · tty_operations Line Discipline (N_TTY, SLIP, PPP, ...) tty_ldisc · tty_ldisc_ops · read/write/ioctl serial_core uart_driver / uart_port USB Serial usb_serial_driver PTY master ↔ slave VT Console vt.c / keyboard.c 기타 8250 / PL011 / IMX FTDI / CP210x / CH341 UART 16550A RS-232 / RS-485 ARM PL011 AMBA UART USB-Serial FTDI / CP2102 SoC UART IMX / Samsung Virtual virtio-console

TTY 코어 데이터 구조

TTY 서브시스템의 핵심 구조체들은 include/linux/tty.hinclude/linux/tty_driver.h에 정의되어 있습니다. 각 구조체의 역할과 관계를 이해하는 것이 TTY/Serial 드라이버 개발의 출발점입니다.

#include <linux/tty.h>
#include <linux/tty_driver.h>
#include <linux/tty_flip.h>

/* === tty_struct: 열린 TTY 디바이스 인스턴스 ===
 * 프로세스가 /dev/ttyS0 등을 open()하면 생성됩니다.
 * 하나의 물리 포트에 대해 최대 하나의 tty_struct가 존재합니다.
 */
struct tty_struct {
    int                     magic;       /* TTY_MAGIC — 유효성 검증 */
    struct kref             kref;        /* 참조 카운터 */
    struct device           *dev;        /* sysfs 디바이스 */
    struct tty_driver       *driver;     /* 소속 드라이버 */
    const struct tty_operations *ops;    /* 드라이버 오퍼레이션 */
    int                     index;       /* 드라이버 내 포트 인덱스 */
    struct tty_ldisc        *ldisc;      /* 현재 line discipline */
    struct tty_port         *port;       /* 하드웨어 포트 정보 */
    struct tty_struct       *link;       /* PTY master ↔ slave 연결 */
    struct tty_bufhead      buf;         /* flip buffer 헤드 */
    struct winsize          winsize;     /* 터미널 윈도우 크기 */
    struct ktermios         termios;     /* 현재 termios 설정 */
    unsigned long           flags;       /* TTY_THROTTLED 등 상태 플래그 */
    struct work_struct      hangup_work; /* hangup 지연 처리 */
    struct work_struct      SAK_work;    /* Secure Attention Key */
    /* ... */
};

/* === tty_driver: TTY 드라이버 등록 정보 ===
 * 동일 유형의 여러 포트를 관리하는 드라이버 단위 구조체입니다.
 * 예: 8250 드라이버가 4개 시리얼 포트를 관리할 때 하나의 tty_driver를 등록합니다.
 */
struct tty_driver {
    struct cdev             *cdevs;     /* 문자 디바이스 배열 */
    const char              *driver_name; /* 예: "serial" */
    const char              *name;       /* 디바이스 이름 prefix: "ttyS" */
    int                     name_base;   /* 번호 시작값 (보통 0) */
    int                     major;       /* major 번호 (4=ttyS) */
    int                     minor_start; /* minor 시작 (64=ttyS0) */
    unsigned int            num;         /* 관리 포트 수 */
    short                   type;        /* TTY_DRIVER_TYPE_SERIAL 등 */
    short                   subtype;     /* SERIAL_TYPE_NORMAL 등 */
    struct ktermios         init_termios; /* 초기 termios */
    const struct tty_operations *ops;    /* 드라이버 콜백 */
    struct tty_port         **ports;     /* 포트 배열 */
    /* ... */
};

/* === tty_port: 물리/가상 포트 상태 관리 ===
 * 하드웨어 포트(또는 가상 포트)의 라이프사이클과 상태를 추적합니다.
 * open/close, hangup, carrier detect 등의 동기화를 담당합니다.
 */
struct tty_port {
    struct tty_bufhead     buf;          /* flip buffer */
    struct tty_struct      *tty;         /* 현재 열린 tty */
    const struct tty_port_operations *ops; /* 포트 콜백 */
    struct mutex           mutex;        /* open/close 직렬화 */
    struct mutex           buf_mutex;    /* 버퍼 접근 보호 */
    unsigned long          flags;        /* ASYNC_* 플래그 */
    int                    count;        /* open 참조 카운터 */
    struct wait_queue_head open_wait;    /* carrier detect 대기 */
    struct wait_queue_head close_wait;   /* 닫기 완료 대기 */
    struct wait_queue_head delta_msr_wait; /* 모뎀 상태 변화 대기 */
    unsigned char          console:1;    /* 콘솔 포트 여부 */
    /* ... */
};

TTY 오퍼레이션 — tty_operations

tty_operations는 TTY 드라이버가 TTY 코어에 제공하는 콜백 함수 테이블입니다. VFS의 file_operations와 유사한 패턴으로, user space의 open()/write()/ioctl() 호출이 이 콜백으로 전달됩니다.

struct tty_operations {
    /* 포트 열기/닫기 — 리소스 할당/해제 */
    int  (*open)(struct tty_struct *tty, struct file *filp);
    void (*close)(struct tty_struct *tty, struct file *filp);

    /* 데이터 송신 */
    ssize_t (*write)(struct tty_struct *tty,
                     const u8 *buf, size_t count);
    unsigned int (*write_room)(struct tty_struct *tty); /* 쓰기 가능 바이트 */
    unsigned int (*chars_in_buffer)(struct tty_struct *tty);

    /* termios 설정 변경 (보레이트, 데이터 비트, 패리티 등) */
    void (*set_termios)(struct tty_struct *tty,
                         const struct ktermios *old);

    /* 흐름 제어 */
    void (*throttle)(struct tty_struct *tty);   /* 수신 일시 정지 */
    void (*unthrottle)(struct tty_struct *tty); /* 수신 재개 */
    void (*stop)(struct tty_struct *tty);       /* 송신 정지 (^S) */
    void (*start)(struct tty_struct *tty);      /* 송신 재개 (^Q) */

    /* ioctl 핸들러 */
    int  (*ioctl)(struct tty_struct *tty,
                  unsigned int cmd, unsigned long arg);

    /* 모뎀 제어 신호 (DTR, RTS, CTS, DCD, DSR, RI) */
    int  (*tiocmget)(struct tty_struct *tty);
    int  (*tiocmset)(struct tty_struct *tty,
                     unsigned int set, unsigned int clear);

    /* break 신호 송신 */
    int  (*break_ctl)(struct tty_struct *tty, int state);

    /* 하드웨어 hangup 감지 */
    void (*hangup)(struct tty_struct *tty);

    /* RS485 설정 (half-duplex 산업용 통신) */
    int  (*get_serial)(struct tty_struct *tty,
                       struct serial_struct *p);
    int  (*set_serial)(struct tty_struct *tty,
                       struct serial_struct *p);
    /* ... */
};

Flip Buffer — 수신 데이터 경로

인터럽트 핸들러에서 수신된 데이터를 user space로 전달하는 메커니즘입니다. ISR에서 직접 user space 버퍼에 복사할 수 없으므로, 커널 내부의 flip buffer를 거칩니다. ISR은 flip buffer에 데이터를 채우고, workqueue를 통해 line discipline으로 전달됩니다.

#include <linux/tty_flip.h>

/* 인터럽트 핸들러에서 수신 데이터 처리 */
static irqreturn_t my_uart_irq(int irq, void *dev_id)
{
    struct uart_port *port = dev_id;
    struct tty_port *tport = &port->state->port;
    unsigned int status, ch;

    status = readl(port->membase + REG_STATUS);

    /* 수신 데이터 처리 */
    while (status & RX_DATA_READY) {
        ch = readl(port->membase + REG_DATA);
        unsigned int flag = TTY_NORMAL;

        port->icount.rx++;

        /* 에러 검출 */
        if (status & PARITY_ERR) {
            port->icount.parity++;
            flag = TTY_PARITY;
        } else if (status & FRAME_ERR) {
            port->icount.frame++;
            flag = TTY_FRAME;
        } else if (status & OVERRUN_ERR) {
            port->icount.overrun++;
            flag = TTY_OVERRUN;
        } else if (status & BREAK_DETECT) {
            port->icount.brk++;
            flag = TTY_BREAK;
            if (uart_handle_break(port))
                continue;
        }

        /* sysrq / null char 처리 */
        if (uart_handle_sysrq_char(port, ch))
            continue;

        /* flip buffer에 한 바이트 삽입 */
        tty_insert_flip_char(tport, ch, flag);

        status = readl(port->membase + REG_STATUS);
    }

    /* flip buffer의 데이터를 line discipline으로 push
     * 내부적으로 work를 스케줄하여 ldisc->receive_buf() 호출 */
    tty_flip_buffer_push(tport);

    /* 송신 처리 */
    if (status & TX_EMPTY)
        my_handle_tx(port);

    return IRQ_HANDLED;
}

/* flip buffer API 요약:
 * tty_insert_flip_char(port, ch, flag)   — 1바이트 삽입
 * tty_insert_flip_string(port, str, len) — 문자열 bulk 삽입 (빠름)
 * tty_prepare_flip_string(port, &p, len) — 직접 포인터 획득 (DMA용)
 * tty_flip_buffer_push(port)             — ldisc로 데이터 전달
 */

Line Discipline (회선 규율)

Line discipline은 TTY 코어와 하위 드라이버 사이에서 데이터를 가공하는 중간 계층입니다. 기본 N_TTY는 canonical/non-canonical 모드 처리, 에코, 시그널 문자(^C, ^Z) 등을 담당합니다. 사용자 정의 line discipline으로 교체하면 시리얼 라인 위에 전용 프로토콜을 구현할 수 있습니다.

Line Discipline번호용도커널 소스
N_TTY0기본 터미널 I/O (canonical/raw 모드)drivers/tty/n_tty.c
N_SLIP1Serial Line IP — 시리얼 위 IP 통신drivers/net/slip/
N_PPP3Point-to-Point Protocoldrivers/net/ppp/
N_GSM071021GSM 멀티플렉싱 (모뎀)drivers/tty/n_gsm.c
N_NULL27모든 데이터를 버림 (테스트용)drivers/tty/n_null.c
N_TRACESINK23디버그 트레이스 데이터 싱크drivers/tty/n_tracesink.c
#include <linux/tty_ldisc.h>

/* 사용자 정의 Line Discipline 예제 — 간단한 패킷 프로토콜 */
static struct tty_ldisc_ops my_ldisc_ops = {
    .owner          = THIS_MODULE,
    .num            = N_MY_LDISC,         /* 고유 번호 (29 이상 사용) */
    .name           = "my_proto",

    /* TTY가 이 ldisc로 전환될 때 */
    .open           = my_ldisc_open,
    .close          = my_ldisc_close,

    /* user space → driver 방향: write() 시스템콜에서 호출 */
    .write          = my_ldisc_write,

    /* driver → user space 방향: ISR → flip buffer → 이 콜백 */
    .receive_buf    = my_ldisc_receive,

    /* user space read()에서 호출 — 가공된 데이터 전달 */
    .read           = my_ldisc_read,

    .ioctl          = my_ldisc_ioctl,
};

/* receive_buf 콜백 — 하드웨어에서 수신된 데이터 처리 */
static void my_ldisc_receive(struct tty_struct *tty,
                             const u8 *data, const u8 *flags,
                             size_t count)
{
    struct my_proto *proto = tty->disc_data;
    size_t i;

    for (i = 0; i < count; i++) {
        if (flags && flags[i] != TTY_NORMAL)
            continue;  /* 에러 바이트 건너뛰기 */

        if (data[i] == MY_FRAME_DELIM) {
            my_process_frame(proto);  /* 프레임 완성 → 처리 */
        } else {
            proto->buf[proto->len++] = data[i];
        }
    }
}

/* 모듈 초기화 시 ldisc 등록 */
static int __init my_ldisc_init(void)
{
    return tty_register_ldisc(&my_ldisc_ops);
}

/* user space에서 ldisc 전환:
 *   int ldisc = N_MY_LDISC;
 *   ioctl(fd, TIOCSETD, &ldisc);  // line discipline 변경
 */

serial_core 프레임워크 — UART 드라이버

serial_core(drivers/tty/serial/serial_core.c)는 UART 하드웨어 드라이버를 위한 표준 프레임워크입니다. 드라이버 개발자는 uart_driver를 등록하고, 각 포트에 대해 uart_portuart_ops를 제공하면 됩니다. TTY 코어와의 연동, line discipline 관리, sysfs 노출 등은 serial_core가 자동 처리합니다.

#include <linux/serial_core.h>
#include <linux/platform_device.h>

#define MY_UART_NR       4    /* 지원 포트 수 */
#define MY_UART_FIFO_SZ  64   /* TX/RX FIFO 깊이 */

/* === uart_ops: UART 하드웨어 오퍼레이션 === */
static unsigned int my_tx_empty(struct uart_port *port)
{
    /* TX FIFO가 완전히 비었으면 TIOCSER_TEMT 반환 */
    return (readl(port->membase + REG_STATUS) & TX_FIFO_EMPTY)
           ? TIOCSER_TEMT : 0;
}

static void my_set_mctrl(struct uart_port *port, unsigned int mctrl)
{
    u32 val = readl(port->membase + REG_MCR);

    if (mctrl & TIOCM_RTS) val |= MCR_RTS;  else val &= ~MCR_RTS;
    if (mctrl & TIOCM_DTR) val |= MCR_DTR;  else val &= ~MCR_DTR;

    writel(val, port->membase + REG_MCR);
}

static unsigned int my_get_mctrl(struct uart_port *port)
{
    u32 status = readl(port->membase + REG_MSR);
    unsigned int mctrl = 0;

    if (status & MSR_CTS) mctrl |= TIOCM_CTS;
    if (status & MSR_DCD) mctrl |= TIOCM_CAR;  /* Carrier Detect */
    if (status & MSR_DSR) mctrl |= TIOCM_DSR;
    if (status & MSR_RI)  mctrl |= TIOCM_RNG;  /* Ring Indicator */

    return mctrl;
}

static void my_start_tx(struct uart_port *port)
{
    /* TX 인터럽트 활성화 — ISR에서 실제 전송 수행 */
    u32 ier = readl(port->membase + REG_IER);
    ier |= IER_TX_EMPTY;
    writel(ier, port->membase + REG_IER);
}

static void my_stop_tx(struct uart_port *port)
{
    u32 ier = readl(port->membase + REG_IER);
    ier &= ~IER_TX_EMPTY;
    writel(ier, port->membase + REG_IER);
}

static void my_stop_rx(struct uart_port *port)
{
    u32 ier = readl(port->membase + REG_IER);
    ier &= ~IER_RX_DATA;
    writel(ier, port->membase + REG_IER);
}

static int my_startup(struct uart_port *port)
{
    int ret;

    /* IRQ 등록 */
    ret = request_irq(port->irq, my_uart_irq,
                      IRQF_SHARED, "my-uart", port);
    if (ret)
        return ret;

    /* FIFO 활성화, RX 인터럽트 활성화 */
    writel(FCR_FIFO_EN | FCR_RX_TRIG_HALF,
           port->membase + REG_FCR);
    writel(IER_RX_DATA, port->membase + REG_IER);

    return 0;
}

static void my_shutdown(struct uart_port *port)
{
    /* 모든 인터럽트 비활성화 */
    writel(0, port->membase + REG_IER);
    free_irq(port->irq, port);
}

static void my_set_termios(struct uart_port *port,
                           struct ktermios *termios,
                           const struct ktermios *old)
{
    unsigned int baud, lcr = 0;

    /* 보레이트 계산 — 클램핑 포함 */
    baud = uart_get_baud_rate(port, termios, old,
                              9600, 4000000);
    unsigned int divisor = uart_get_divisor(port, baud);

    /* 데이터 비트 */
    switch (termios->c_cflag & CSIZE) {
    case CS5: lcr |= LCR_WLEN5; break;
    case CS6: lcr |= LCR_WLEN6; break;
    case CS7: lcr |= LCR_WLEN7; break;
    default:  lcr |= LCR_WLEN8; break;
    }

    /* 정지 비트 */
    if (termios->c_cflag & CSTOPB)
        lcr |= LCR_STOP_2;

    /* 패리티 */
    if (termios->c_cflag & PARENB) {
        lcr |= LCR_PARITY;
        if (!(termios->c_cflag & PARODD))
            lcr |= LCR_EVEN_PARITY;
    }

    /* 하드웨어 레지스터 업데이트 */
    spin_lock_irq(&port->lock);
    uart_update_timeout(port, termios->c_cflag, baud);
    writel(divisor, port->membase + REG_BAUD_DIV);
    writel(lcr, port->membase + REG_LCR);

    /* 에러 무시 마스크 설정 */
    port->read_status_mask = OVERRUN_ERR;
    if (termios->c_iflag & INPCK)
        port->read_status_mask |= PARITY_ERR | FRAME_ERR;
    if (termios->c_iflag & (BRKINT | PARMRK))
        port->read_status_mask |= BREAK_DETECT;

    port->ignore_status_mask = 0;
    if (termios->c_iflag & IGNPAR)
        port->ignore_status_mask |= PARITY_ERR | FRAME_ERR;
    if (termios->c_iflag & IGNBRK)
        port->ignore_status_mask |= BREAK_DETECT;

    spin_unlock_irq(&port->lock);
}

static const char *my_type(struct uart_port *port)
{
    return "MY-UART";
}

static void my_config_port(struct uart_port *port, int flags)
{
    if (flags & UART_CONFIG_TYPE)
        port->type = PORT_MY_UART;
}

static const struct uart_ops my_uart_ops = {
    .tx_empty     = my_tx_empty,
    .set_mctrl    = my_set_mctrl,
    .get_mctrl    = my_get_mctrl,
    .start_tx     = my_start_tx,
    .stop_tx      = my_stop_tx,
    .stop_rx      = my_stop_rx,
    .startup      = my_startup,
    .shutdown     = my_shutdown,
    .set_termios  = my_set_termios,
    .type         = my_type,
    .config_port  = my_config_port,
};

/* === uart_driver: 드라이버 등록 구조체 === */
static struct uart_driver my_uart_drv = {
    .owner       = THIS_MODULE,
    .driver_name = "my-uart",      /* /proc/tty/drivers에 표시 */
    .dev_name    = "ttyMY",        /* 디바이스 이름: /dev/ttyMY0, ttyMY1, ... */
    .major       = 0,              /* 0 = 동적 할당 */
    .minor       = 0,
    .nr          = MY_UART_NR,     /* 최대 포트 수 */
    .cons        = &my_console,    /* 콘솔 구조체 (NULL 가능) */
};

/* === Platform Driver 통합 === */
static int my_uart_probe(struct platform_device *pdev)
{
    struct my_uart_priv *priv;
    struct resource *res;
    int ret;

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

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->port.membase = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->port.membase))
        return PTR_ERR(priv->port.membase);

    priv->port.irq      = platform_get_irq(pdev, 0);
    priv->port.ops       = &my_uart_ops;
    priv->port.dev       = &pdev->dev;
    priv->port.type      = PORT_MY_UART;
    priv->port.iotype    = UPIO_MEM32;   /* 32-bit MMIO */
    priv->port.fifosize  = MY_UART_FIFO_SZ;
    priv->port.flags     = UPF_BOOT_AUTOCONF;
    priv->port.line      = pdev->id;      /* 포트 번호 (DT: alias) */

    /* 클럭 설정 */
    priv->clk = devm_clk_get_enabled(&pdev->dev, NULL);
    if (IS_ERR(priv->clk))
        return PTR_ERR(priv->clk);
    priv->port.uartclk = clk_get_rate(priv->clk);

    platform_set_drvdata(pdev, priv);

    /* serial_core에 포트 등록 → /dev/ttyMYn 생성 */
    ret = uart_add_one_port(&my_uart_drv, &priv->port);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "MY-UART at 0x%lx, irq %d, %d Hz\n",
             (unsigned long)res->start, priv->port.irq,
             priv->port.uartclk);
    return 0;
}

static void my_uart_remove(struct platform_device *pdev)
{
    struct my_uart_priv *priv = platform_get_drvdata(pdev);
    uart_remove_one_port(&my_uart_drv, &priv->port);
}

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

static struct platform_driver my_uart_platform_drv = {
    .probe  = my_uart_probe,
    .remove = my_uart_remove,
    .driver = {
        .name = "my-uart",
        .of_match_table = my_uart_of_match,
    },
};

/* 모듈 초기화: uart_driver 등록 → platform_driver 등록 */
static int __init my_uart_init(void)
{
    int ret = uart_register_driver(&my_uart_drv);
    if (ret)
        return ret;

    ret = platform_driver_register(&my_uart_platform_drv);
    if (ret)
        uart_unregister_driver(&my_uart_drv);
    return ret;
}

static void __exit my_uart_exit(void)
{
    platform_driver_unregister(&my_uart_platform_drv);
    uart_unregister_driver(&my_uart_drv);
}

module_init(my_uart_init);
module_exit(my_uart_exit);

시리얼 콘솔과 earlycon

커널 콘솔은 부팅 메시지(printk)를 출력하는 저수준 인터페이스입니다. TTY 서브시스템이 초기화되기 전에도 동작하므로, earlycon으로 부팅 초기부터 디버그 출력이 가능합니다.

#include <linux/console.h>
#include <linux/serial_core.h>

/* === 일반 시리얼 콘솔 === */
static void my_console_write(struct console *co,
                              const char *s, unsigned int count)
{
    struct uart_port *port = &my_ports[co->index];
    unsigned long flags;
    int locked;

    /* 콘솔은 NMI, panic 등에서도 호출될 수 있음
     * trylock 실패 시에도 출력 시도 (디버깅 목적) */
    locked = spin_trylock_irqsave(&port->lock, flags);

    /* uart_console_write()는 '\n' → '\r\n' 변환 포함 */
    uart_console_write(port, s, count, my_console_putchar);

    if (locked)
        spin_unlock_irqrestore(&port->lock, flags);
}

static void my_console_putchar(struct uart_port *port, unsigned char ch)
{
    /* TX FIFO 비어질 때까지 polling (콘솔은 인터럽트 불가) */
    while (!(readl(port->membase + REG_STATUS) & TX_FIFO_EMPTY))
        cpu_relax();
    writel(ch, port->membase + REG_DATA);
}

static int my_console_setup(struct console *co, char *options)
{
    struct uart_port *port = &my_ports[co->index];
    int baud = 115200, bits = 8, parity = 'n', flow = 'n';

    if (options)
        uart_parse_options(options, &baud, &parity, &bits, &flow);

    return uart_set_options(port, co, baud, parity, bits, flow);
}

static struct console my_console = {
    .name    = "ttyMY",      /* console=ttyMY0,115200 */
    .write   = my_console_write,
    .device  = uart_console_device,  /* serial_core 제공 헬퍼 */
    .setup   = my_console_setup,
    .flags   = CON_PRINTBUFFER,       /* 등록 전 버퍼 출력 */
    .index   = -1,                    /* -1 = 커널 파라미터로 결정 */
    .data    = &my_uart_drv,
};

/* === earlycon: 부팅 초기 콘솔 ===
 * TTY/serial_core 초기화 전에 printk 출력 가능.
 * 커널 파라미터: earlycon=my-uart,0x1c28000,115200
 * Device Tree: chosen { stdout-path = "serial0:115200n8"; };
 */
static void my_early_write(struct console *co,
                           const char *s, unsigned int count)
{
    struct earlycon_device *dev = co->data;
    struct uart_port *port = &dev->port;

    uart_console_write(port, s, count, my_early_putchar);
}

static int __init my_early_console_setup(struct earlycon_device *dev,
                                         const char *options)
{
    if (!dev->port.membase)
        return -ENODEV;
    dev->con->write = my_early_write;
    return 0;
}
OF_EARLYCON_DECLARE(my_uart, "vendor,my-uart", my_early_console_setup);
# 커널 부팅 파라미터 예시
console=ttyS0,115200n8         # 표준 시리얼 콘솔
console=ttyAMA0,115200         # ARM PL011
console=tty0                   # VGA 콘솔
console=ttyS0 console=tty0     # 다중 콘솔 (마지막이 /dev/console)

earlycon=uart8250,mmio32,0xfe215040,115200  # earlycon 직접 지정
earlycon                       # DT stdout-path에서 자동 감지

TTY 디바이스 명명 규칙

디바이스경로용도드라이버/서브시스템
ttySN/dev/ttyS08250/16550 호환 시리얼 포트drivers/tty/serial/8250/
ttyAMAN/dev/ttyAMA0ARM AMBA PL011 UARTdrivers/tty/serial/amba-pl011.c
ttyUSBN/dev/ttyUSB0USB-Serial 변환기 (FTDI, CP210x 등)drivers/usb/serial/
ttyACMN/dev/ttyACM0USB CDC ACM (Abstract Control Model)drivers/usb/class/cdc-acm.c
ttyMFDN/dev/ttyMFD0Intel MID (Medfield) UARTdrivers/tty/serial/mfd.c
ttyON/dev/ttyO0TI OMAP UARTdrivers/tty/serial/omap-serial.c
ttySACN/dev/ttySAC0Samsung S3C/S5P UARTdrivers/tty/serial/samsung_tty.c
ttyN/dev/tty1가상 콘솔 (VT)drivers/tty/vt/
pts/N/dev/pts/0의사 터미널 slave (PTY)drivers/tty/pty.c
ptmx/dev/ptmxPTY master 멀티플렉서drivers/tty/pty.c
console/dev/console시스템 콘솔 (마지막 console= 파라미터)커널 코어
ttyGSN/dev/ttyGS0USB Gadget 시리얼 (디바이스 모드)drivers/usb/gadget/function/u_serial.c
ttyLPN/dev/ttyLP0Intel LPSS UART (Low Power)drivers/tty/serial/8250/8250_lpss.c

PTY (Pseudo-Terminal)

의사 터미널은 물리 하드웨어 없이 TTY 인터페이스를 제공합니다. SSH, 터미널 에뮬레이터(xterm, gnome-terminal), screen/tmux 등이 PTY를 사용합니다. master(제어 프로그램 쪽)와 slave(응용 프로세스 쪽)의 쌍으로 동작하며, master에 쓴 데이터가 slave의 입력으로 나타나고, 그 반대도 마찬가지입니다.

/* PTY 동작 원리 (커널 내부):
 *
 *   Terminal Emulator (xterm)     Shell (bash)
 *        |                            |
 *        v                            v
 *   /dev/ptmx (master)  ←→  /dev/pts/N (slave)
 *        |                            |
 *        +----- pty_write() ------→---+
 *        +----- pty_read() ←------→---+
 *
 * master에 write → slave에서 read 가능 (키보드 입력 시뮬레이션)
 * slave에 write → master에서 read 가능 (프로그램 출력 캡처)
 * slave 쪽에 N_TTY line discipline 적용 (에코, ^C 등 처리)
 */

/* PTY 생성 과정 (user space, POSIX API) */
#include <stdlib.h>
#include <fcntl.h>

int master_fd = posix_openpt(O_RDWR | O_NOCTTY);
grantpt(master_fd);          /* slave 소유권/퍼미션 설정 */
unlockpt(master_fd);         /* slave 잠금 해제 */
char *slave_name = ptsname(master_fd); /* "/dev/pts/3" 등 */

int slave_fd = open(slave_name, O_RDWR);
/* 이제 master_fd ↔ slave_fd 양방향 통신 가능 */
/* 커널의 PTY 구현 핵심 (drivers/tty/pty.c) */

/* master의 write → slave의 입력 버퍼로 전달 */
static ssize_t pty_write(struct tty_struct *tty,
                         const u8 *buf, size_t c)
{
    struct tty_struct *to = tty->link;  /* master→slave 또는 slave→master */

    if (!to || tty_io_error(tty))
        return -EIO;

    /* 상대편의 flip buffer에 데이터 삽입 */
    c = tty_insert_flip_string(&to->port, buf, c);

    if (c)
        tty_flip_buffer_push(&to->port);

    return c;
}

termios 설정 상세

termios 구조체는 TTY 디바이스의 동작 모드를 제어합니다. 입력/출력 처리, 제어 문자, 로컬 모드 등 네 가지 플래그 그룹으로 구성됩니다.

struct ktermios {
    tcflag_t c_iflag;   /* 입력 모드: IGNBRK, ICRNL, IXON, IXOFF ... */
    tcflag_t c_oflag;   /* 출력 모드: OPOST, ONLCR ... */
    tcflag_t c_cflag;   /* 제어 모드: CSIZE, CSTOPB, PARENB, CRTSCTS ... */
    tcflag_t c_lflag;   /* 로컬 모드: ECHO, ICANON, ISIG, IEXTEN ... */
    cc_t     c_cc[NCCS]; /* 제어 문자: VINTR(^C), VEOF(^D), VMIN, VTIME ... */
    speed_t  c_ispeed;  /* 입력 보레이트 */
    speed_t  c_ospeed;  /* 출력 보레이트 */
};

/* c_cflag 주요 비트:
 * CSIZE   — CS5/CS6/CS7/CS8 (데이터 비트)
 * CSTOPB  — 정지 비트 2개 (미설정 시 1개)
 * PARENB  — 패리티 활성화
 * PARODD  — 홀수 패리티 (미설정 시 짝수)
 * CRTSCTS — 하드웨어 흐름 제어 (RTS/CTS)
 * CLOCAL  — 모뎀 제어 무시 (DCD 불필요)
 * CREAD   — 수신 활성화
 * CBAUD   — 보레이트 마스크 (B9600, B115200 등)
 */

/* c_lflag 주요 비트:
 * ICANON  — Canonical 모드 (줄 단위 입력, ^D로 EOF)
 * ECHO    — 입력 에코
 * ECHOE   — Backspace 에코 (지우기)
 * ISIG    — 시그널 문자 활성화 (^C→SIGINT, ^Z→SIGTSTP, ^\→SIGQUIT)
 * IEXTEN  — 확장 입력 처리 (^V → literal next)
 */
# stty로 termios 설정 확인/변경
stty -a -F /dev/ttyS0
# speed 115200 baud; rows 0; columns 0; line = 0;
# intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; ...
# -parenb -parodd cs8 -cstopb cread clocal -crtscts
# -ignbrk -brkint ignpar -ignpar -parmrk -inpck -istrip ...
# opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel ...
# -isig -icanon -iexten -echo -echoe -echok -echonl ...

# 보레이트 변경
stty -F /dev/ttyS0 115200

# 8N1 설정 (8 데이터 비트, 패리티 없음, 1 정지 비트)
stty -F /dev/ttyS0 cs8 -parenb -cstopb

# Raw 모드 (line discipline 가공 없이 바이트 그대로)
stty -F /dev/ttyS0 raw

# 하드웨어 흐름 제어 활성화
stty -F /dev/ttyS0 crtscts

# 소프트웨어 흐름 제어 (XON/XOFF)
stty -F /dev/ttyS0 ixon ixoff

RS-485 모드

RS-485는 산업용 half-duplex 직렬 통신 표준으로, 하나의 버스에 여러 디바이스를 연결합니다. 리눅스 커널은 serial_rs485 구조체와 TIOCSRS485 ioctl을 통해 RS-485 모드를 지원합니다.

#include <linux/serial.h>

/* user space에서 RS-485 모드 활성화 */
struct serial_rs485 rs485conf = {
    .flags = SER_RS485_ENABLED | SER_RS485_RTS_ON_SEND,
    .delay_rts_before_send = 0,  /* TX 시작 전 RTS 지연 (ms) */
    .delay_rts_after_send  = 0,  /* TX 완료 후 RTS 지연 (ms) */
};
ioctl(fd, TIOCSRS485, &rs485conf);

/* 커널 UART 드라이버에서 RS-485 지원:
 * uart_port.rs485_config() 콜백 구현 필요.
 * RTS 핀을 TX enable로 사용하여 송신 시 RTS 활성화,
 * 수신 시 RTS 비활성화하여 트랜시버 방향 제어.
 *
 * Device Tree 설정 예:
 *   &uart1 {
 *       linux,rs485-enabled-at-boot-time;
 *       rs485-rts-delay = <0 0>;
 *       rs485-rts-active-low;          // RTS 극성 반전
 *   };
 */

TTY/Serial 디버깅

# ─── 시스템 정보 확인 ───

# 등록된 TTY 드라이버 목록
cat /proc/tty/drivers
# /dev/tty             /dev/tty        5       0 system:/dev/tty
# /dev/console         /dev/console    5       1 system:console
# /dev/ptmx            /dev/ptmx       5       2 system
# serial               /dev/ttyS       4  64-67 serial
# pty_slave            /dev/pts      136   0-... pty:slave
# pty_master           /dev/ptm      128   0-... pty:master

# 활성 TTY 라인 정보
cat /proc/tty/line_disc
# n_tty       0

# 시리얼 포트 하드웨어 정보
cat /proc/tty/driver/serial
# serinfo:1.0 driver revision:
# 0: uart:16550A port:000003F8 irq:4 tx:0 rx:0
# 1: uart:16550A port:000002F8 irq:3 tx:0 rx:0

# setserial로 시리얼 포트 상세 정보
setserial -g /dev/ttyS0
# /dev/ttyS0, UART: 16550A, Port: 0x03f8, IRQ: 4

# ─── 디바이스 테스트 ───

# minicom 또는 picocom으로 시리얼 통신
minicom -D /dev/ttyS0 -b 115200
picocom --baud 115200 /dev/ttyS0

# 간단한 시리얼 송수신 테스트
echo "hello" > /dev/ttyS0             # 데이터 송신
cat /dev/ttyS0                          # 데이터 수신 (blocking)
dd if=/dev/ttyS0 bs=1 count=10          # 10바이트만 수신

# ─── 커널 디버깅 ───

# TTY 관련 커널 로그
dmesg | grep -i -E 'tty|serial|uart'
# [    0.000000] printk: console [tty0] enabled
# [    0.524130] serial8250: ttyS0 at I/O 0x3f8 (irq = 4, base_baud = 115200)
# [    1.234567] usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0

# dynamic debug로 serial_core 트레이싱
echo 'module serial_core +p' > /sys/kernel/debug/dynamic_debug/control
echo 'module 8250_core +p' > /sys/kernel/debug/dynamic_debug/control

# UART 포트 통계 (인터럽트 카운터)
cat /proc/interrupts | grep -i serial
#  4:       128   IO-APIC   4-edge      serial

# sysfs를 통한 UART 정보
ls /sys/class/tty/ttyS0/
# close_delay  closing_wait  custom_divisor  io_type  iomem_base
# iomem_reg_shift  irq  line  port  type  uartclk  xmit_fifo_size

cat /sys/class/tty/ttyS0/uartclk   # UART 기본 클럭
cat /sys/class/tty/ttyS0/type      # UART 타입 (16550A=4)

# ─── PTY 정보 ───

# 현재 열린 PTY 확인
ls /dev/pts/
# 0  1  2  ptmx

# 자신의 터미널 확인
tty
# /dev/pts/0

# 프로세스별 controlling terminal
ps -eo pid,tty,comm | head -20
TX 인터럽트 핸들러 패턴: UART 송신은 circular buffer(uart_state->xmit)를 통해 이루어집니다. start_tx()가 TX empty 인터럽트를 활성화하면, ISR에서 uart_circ_chars_pending()으로 남은 데이터를 확인하고 FIFO에 채워넣습니다. 버퍼가 비면 uart_write_wakeup()을 호출하여 대기 중인 write()를 깨우고, 전송 완료 시 stop_tx()로 인터럽트를 끕니다.
/* TX 인터럽트 핸들러 패턴 */
static void my_handle_tx(struct uart_port *port)
{
    struct tty_port *tport = &port->state->port;
    unsigned int pending;
    u8 ch;

    /* x_char (XON/XOFF) 우선 송신 */
    if (port->x_char) {
        writel(port->x_char, port->membase + REG_DATA);
        port->icount.tx++;
        port->x_char = 0;
        return;
    }

    /* pending 데이터를 FIFO에 채워넣기 */
    pending = kfifo_len(&tport->xmit_fifo);
    if (pending == 0 || uart_tx_stopped(port)) {
        my_stop_tx(port);
        return;
    }

    while (readl(port->membase + REG_STATUS) & TX_FIFO_NOT_FULL) {
        if (!kfifo_get(&tport->xmit_fifo, &ch))
            break;
        writel(ch, port->membase + REG_DATA);
        port->icount.tx++;
    }

    /* 버퍼 여유 생기면 write() 대기 프로세스 깨우기 */
    if (kfifo_len(&tport->xmit_fifo) < WAKEUP_CHARS)
        uart_write_wakeup(port);

    /* 모든 데이터 전송 완료 시 TX 인터럽트 끄기 */
    if (kfifo_is_empty(&tport->xmit_fifo))
        my_stop_tx(port);
}
TTY/Serial 드라이버 개발 주의사항:
  • ISR 컨텍스트 — UART 인터럽트 핸들러는 hard IRQ 컨텍스트에서 실행됩니다. sleep, mutex, GFP_KERNEL 할당 불가. spin_lock(&port->lock)으로 start_tx/stop_tx와의 경쟁 보호
  • flip buffer 크기TTY_BUFFER_PAGE(4KB) 단위로 할당됩니다. 고속 통신에서 ISR이 지연되면 데이터 손실 발생 가능. DMA 전송 사용 권장
  • hangup 경쟁 — USB-Serial 언플러그 시 hangup()write()가 동시에 호출될 수 있음. tty_port_hangup() 사용으로 안전한 처리 보장
  • DMA 전송 — 고속 UART에서는 PIO(Programmed I/O) 대신 DMA 사용 권장. tty_prepare_flip_string()으로 직접 DMA 타겟 버퍼 획득 가능
  • 콘솔 write 경로console_write()는 printk에서 호출되므로 NMI, panic 등 어떤 컨텍스트에서든 안전해야 합니다. spin_trylock() 사용 필수
  • suspend/resumeuart_suspend_port()/uart_resume_port() 사용. 진행 중인 DMA 전송 중지, TX FIFO drain 대기, 클럭 재설정 등 순서 준수 필수

8250/16550 드라이버 — 가장 보편적인 UART

8250/16550 호환 UART는 PC 시리얼 포트의 사실상 표준입니다. 리눅스 커널의 drivers/tty/serial/8250/ 디렉터리에 구현되어 있으며, PCI, ACPI, Device Tree, ISA 등 다양한 열거 방식을 지원합니다.

레지스터오프셋읽기 용도쓰기 용도
RBR/THR0x00수신 데이터 (RBR)송신 데이터 (THR)
IER0x01인터럽트 활성화 (RX, TX, Line Status, Modem Status)
IIR/FCR0x02인터럽트 식별 (IIR)FIFO 제어 (FCR)
LCR0x03Line Control (데이터 비트, 정지 비트, 패리티, DLAB)
MCR0x04Modem Control (DTR, RTS, loopback)
LSR0x05Line Status (Data Ready, Overrun, Parity Err, TX Empty)
MSR0x06Modem Status (CTS, DSR, RI, DCD 변화 감지)
SCR0x07Scratch Register (UART 존재 감지용)
DLL/DLM0x00/0x01Divisor Latch (DLAB=1일 때, 보레이트 설정)
/* 8250 포트를 수동으로 등록하는 예 (레거시/커스텀 보드) */
#include <linux/serial_8250.h>

static struct plat_serial8250_port my_8250_data[] = {
    {
        .mapbase  = 0x3F8,           /* COM1 물리 주소 */
        .irq      = 4,
        .uartclk  = 1843200,         /* 1.8432 MHz 기본 클럭 */
        .iotype   = UPIO_PORT,        /* x86 I/O 포트 접근 */
        .flags    = UPF_SKIP_TEST | UPF_BOOT_AUTOCONF,
        .regshift = 0,               /* 레지스터 간격: 1바이트 */
    },
    { }, /* 터미네이터 */
};

/* 보레이트 계산:
 * divisor = uartclk / (16 × baud_rate)
 * 115200 baud: 1843200 / (16 × 115200) = 1
 * 9600 baud:   1843200 / (16 × 9600) = 12
 */
서브시스템주요 드라이버디버깅 도구
Inputgpio-keys, atkbd, hid-*evtest, libinput debug-events
USBxhci-hcd, ehci-hcd, usb-storagelsusb -v, usbmon
V4L2uvcvideo, vividv4l2-ctl, media-ctl
DRMi915, amdgpu, nouveaumodetest, drm_info
ALSAsnd-hda-intel, snd-usb-audioaplay -l, alsamixer
Serial8250, pl011, imx-uartminicom, stty

전원 관리 (Power Management) 심화

시스템 절전 상태

상태ACPI커널 문자열저장 위치복귀 시간전력
Freeze freeze RAM (프로세스 동결만) ~100ms 높음
Standby S1 standby RAM (CPU 클럭 차단) ~1s 중간
Suspend-to-RAM S3 mem RAM (대부분 H/W 전원 OFF) ~3s 낮음
Hibernate S4 disk 디스크 (swap 파티션) ~15s 0 (완전 차단)
s2idle Modern Standby s2idle RAM (저전력 idle) ~200ms 낮음
# 지원되는 절전 상태 확인
cat /sys/power/state
# freeze mem disk

# Suspend-to-RAM 실행
echo mem > /sys/power/state

# Hibernate 실행
echo disk > /sys/power/state

# systemd를 통한 절전
systemctl suspend
systemctl hibernate

Runtime PM (디바이스 동적 전원 관리)

#include <linux/pm_runtime.h>

/* 드라이버 probe에서 Runtime PM 활성화 */
static int my_probe(struct platform_device *pdev)
{
    /* Runtime PM 콜백 등록 */
    pm_runtime_set_active(&pdev->dev);
    pm_runtime_enable(&pdev->dev);
    pm_runtime_set_autosuspend_delay(&pdev->dev, 1000); /* 1초 */
    pm_runtime_use_autosuspend(&pdev->dev);
    return 0;
}

/* I/O 수행 시 디바이스 깨우기 */
static int my_read(struct device *dev)
{
    pm_runtime_get_sync(dev);  /* 디바이스 깨우고 사용 카운터++ */

    /* ... H/W 접근 ... */

    pm_runtime_mark_last_busy(dev);
    pm_runtime_put_autosuspend(dev); /* 카운터--, 유휴 시 자동 절전 */
    return 0;
}

/* Runtime PM 콜백 */
static int my_runtime_suspend(struct device *dev) {
    clk_disable_unprepare(priv->clk);
    return 0;
}
static int my_runtime_resume(struct device *dev) {
    clk_prepare_enable(priv->clk);
    return 0;
}

static const struct dev_pm_ops my_pm_ops = {
    SET_SYSTEM_SLEEP_PM_OPS(my_suspend, my_resume)
    SET_RUNTIME_PM_OPS(my_runtime_suspend, my_runtime_resume, NULL)
};

cpufreq와 cpuidle

서브시스템기능거버너/드라이버설정
cpufreq CPU 주파수 스케일링 (DVFS) schedutil(기본), performance, powersave, ondemand /sys/devices/system/cpu/cpufreq/
cpuidle CPU 유휴 상태 관리 (C-states) menu, ladder, teo, haltpoll /sys/devices/system/cpu/cpuidle/
intel_pstate Intel HWP 드라이버 active(HWP), passive(cpufreq) /sys/devices/system/cpu/intel_pstate/
thermal 온도 감시 및 쓰로틀링 step_wise, power_allocator /sys/class/thermal/
# CPU 주파수 거버너 설정
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

# C-state 비활성화 (지연 민감 환경)
# 부트 파라미터: processor.max_cstate=1 intel_idle.max_cstate=0
# 또는 런타임:
echo 1 > /sys/devices/system/cpu/cpu0/cpuidle/state2/disable

# 온도 모니터링
cat /sys/class/thermal/thermal_zone0/temp  # 밀리°C 단위
# 47000 → 47°C

Suspend-to-RAM (S3) 상세 흐름

사용자가 echo mem > /sys/power/state 또는 systemctl suspend를 실행하면, 커널은 다단계 절차를 거쳐 시스템을 S3 상태로 진입시킵니다.

/* === Suspend-to-RAM 전체 흐름 ===
 *
 * 소스: kernel/power/suspend.c, drivers/base/power/main.c
 *
 * 사용자 공간 트리거:
 *   echo mem > /sys/power/state
 *     → state_store() [kernel/power/main.c]
 *       → pm_suspend(PM_SUSPEND_MEM)
 *
 * pm_suspend() 내부 단계:
 *
 * [1단계] suspend_prepare() — 준비 작업
 *   ├── pm_notifier_call_chain(PM_SUSPEND_PREPARE)
 *   │     → 등록된 PM notifier들에게 suspend 예고
 *   │       (예: CPU hotplug 비활성화, 드라이버 버퍼 플러시 등)
 *   ├── pm_prepare_console()  → VT 콘솔 전환 (CONFIG_VT_CONSOLE_SLEEP)
 *   └── suspend_freeze_processes()
 *         ├── freeze_processes()  → 모든 사용자 프로세스 동결 (SIGSTOP 유사)
 *         │     → try_to_freeze_tasks(true)
 *         │       → 각 태스크에 TIF_SIGPENDING 설정 → refrigerator() 진입
 *         │       → 동결 타임아웃: 20초 (기본값)
 *         └── freeze_kernel_threads()  → 커널 스레드 동결 (freezable 플래그 설정된 것만)
 *
 * [2단계] suspend_devices_and_enter() — 디바이스 suspend + 플랫폼 진입
 *   ├── platform_suspend_begin()   → ACPI: acpi_suspend_begin()
 *   ├── suspend_console()          → 콘솔 출력 중단
 *   │
 *   ├── dpm_suspend_start(PMSG_SUSPEND)
 *   │     ├── dpm_prepare()        → 모든 디바이스 .prepare() 콜백
 *   │     └── dpm_suspend()        → 모든 디바이스 .suspend() 콜백
 *   │           → 디바이스 트리 역순으로 순회 (leaf → root)
 *   │           → 각 디바이스: bus→class→driver .suspend() 호출
 *   │           → DMA 정지, 레지스터 저장, 클럭 비활성화 등
 *   │
 *   ├── suspend_enter(state)
 *   │     ├── platform_suspend_prepare()  → ACPI: S3 준비
 *   │     ├── dpm_suspend_late()          → .suspend_late() 콜백
 *   │     │     → 인터럽트 활성 상태에서 마지막 디바이스 작업
 *   │     ├── platform_suspend_prepare_late()
 *   │     ├── disable_nonboot_cpus()      → BSP 외 모든 AP CPU 오프라인
 *   │     ├── arch_suspend_disable_irqs() → 인터럽트 전역 비활성화
 *   │     │
 *   │     ├── dpm_suspend_noirq()         → .suspend_noirq() 콜백
 *   │     │     → IRQ 없는 환경에서 최종 디바이스 정리
 *   │     │     → GPIO 상태 저장, 인터럽트 컨트롤러 설정 등
 *   │     ├── syscore_suspend()           → syscore_ops 체인 호출
 *   │     │     → APIC, 타이머, 클럭소스 등 핵심 서브시스템 저장
 *   │     │
 *   │     └── suspend_ops->enter()        ★ 실제 S3 진입 ★
 *   │           → ACPI: acpi_suspend_enter()
 *   │             → acpi_enter_sleep_state(ACPI_STATE_S3)
 *   │               → AML SLP_TYPa/SLP_TYPb 레지스터에 S3 값 기록
 *   │               → PM1a_CNT/PM1b_CNT에 SLP_EN 비트 설정
 *   │               → ★ CPU 정지, RAM만 전원 유지 ★
 *   │
 *   │     === 외부 이벤트(전원 버튼, LAN, RTC 등)로 Wakeup ===
 *   │
 *   │     → 펌웨어가 CPU 리셋 → wakeup 벡터로 점프
 *   │     → 커널 resume 코드로 복귀 (arch/x86/kernel/acpi/wakeup_64.S)
 *   │
 *   │     ├── syscore_resume()            → syscore_ops 역순 복원
 *   │     ├── dpm_resume_noirq()          → .resume_noirq() 콜백
 *   │     ├── arch_suspend_enable_irqs()  → 인터럽트 재활성화
 *   │     ├── enable_nonboot_cpus()       → AP CPU 다시 온라인
 *   │     ├── dpm_resume_early()          → .resume_early() 콜백
 *   │     └── platform_resume_finish()
 *   │
 *   ├── dpm_resume_end(PMSG_RESUME)
 *   │     ├── dpm_resume()       → .resume() 콜백 (디바이스 복원)
 *   │     │     → 디바이스 트리 정순 (root → leaf)
 *   │     │     → 클럭 활성화, 레지스터 복원, DMA 재시작
 *   │     └── dpm_complete()     → .complete() 콜백
 *   │
 *   └── resume_console()         → 콘솔 출력 복원
 *
 * [3단계] suspend_finish() — 정리 작업
 *   ├── thaw_processes()         → 동결된 프로세스 해동
 *   ├── pm_restore_console()     → VT 콘솔 복원
 *   └── pm_notifier_call_chain(PM_POST_SUSPEND) → PM notifier 완료 통보
 */
Wakeup 벡터: S3 진입 전 커널은 FACS(Firmware ACPI Control Structure)의 firmware_waking_vector 필드에 resume 진입점 주소를 기록합니다. CPU가 리셋될 때 펌웨어는 이 주소로 점프하여 커널 resume 코드를 실행합니다. x86_64에서는 리얼 모드 → 보호 모드 → 롱 모드 전환을 다시 수행합니다.
/* === s2idle (Modern Standby / S0ix) ===
 *
 * S3보다 빠른 복귀를 위한 소프트웨어 기반 절전.
 * ACPI S3 없이 커널이 직접 저전력 idle 상태를 관리.
 * Intel: C10 idle state + S0ix 패키지 상태
 *
 * echo s2idle > /sys/power/mem_sleep  → s2idle을 mem의 기본으로 설정
 *
 * 흐름 차이:
 *   S3:     suspend_ops->enter() → 펌웨어가 전원 관리
 *   s2idle: freeze_enter() → cpuidle으로 깊은 idle 진입
 *           → 주기적 tick freeze, 디바이스 Runtime PM 활용
 *
 * /sys/power/mem_sleep:
 *   [s2idle] deep  → 현재 "mem"이 s2idle로 매핑됨
 *   s2idle [deep]  → 현재 "mem"이 S3(deep)으로 매핑됨
 */

Hibernate (S4) 상세 흐름

Hibernate는 전체 시스템 메모리를 디스크에 저장하고 전원을 완전히 차단합니다. 전원이 복구되면 디스크에서 메모리 이미지를 복원하여 suspend 직전 상태로 되돌립니다.

/* === Hibernate (S4) 전체 흐름 ===
 *
 * 소스: kernel/power/hibernate.c, kernel/power/swap.c, kernel/power/snapshot.c
 *
 * 트리거: echo disk > /sys/power/state → hibernate()
 *
 * [1단계] hibernate_prepare() — 준비
 *   ├── pm_notifier_call_chain(PM_HIBERNATION_PREPARE)
 *   ├── freeze_processes()        → 사용자 프로세스 + 커널 스레드 동결
 *   └── hibernation_snapshot()에 사용할 swap 영역 확인
 *
 * [2단계] hibernation_snapshot() — 메모리 스냅샷 생성
 *   ├── create_image()
 *   │     ├── dpm_suspend_start() → 모든 디바이스 suspend
 *   │     ├── disable_nonboot_cpus()
 *   │     ├── syscore_suspend()
 *   │     │
 *   │     ├── swsusp_arch_suspend()  ★ 스냅샷 포인트 ★
 *   │     │     → CPU 레지스터(RSP, RBP, RBX 등) 스택에 저장
 *   │     │     → swsusp_save(): 사용 중인 메모리 페이지를
 *   │     │       "snapshot image"로 복사 (pfn 기반)
 *   │     │     → return 0 (hibernate 경로)
 *   │     │     ※ resume 시 이 지점부터 return 1로 재실행
 *   │     │
 *   │     ├── syscore_resume()
 *   │     ├── enable_nonboot_cpus()
 *   │     └── dpm_resume_end()   → 디바이스 일시적 resume (이미지 기록용)
 *   │
 *   └── 이미지 생성 완료 (nosave 페이지 제외, 클린 페이지 제외)
 *
 * [3단계] hibernation_platform_enter() 또는 power_down()
 *   ├── swsusp_write()            → 스냅샷을 swap 파티션/파일에 기록
 *   │     → save_image(): LZO/LZ4 압축 후 swap에 기록
 *   │     → 헤더에 서명 "S1SUSPEND" + 체크섬 기록
 *   │     → swap의 첫 페이지에 swsusp_info 메타데이터 기록
 *   │
 *   ├── 플랫폼별 전원 차단:
 *   │     hibernate_ops->enter()  → ACPI: S4 진입
 *   │     또는 kernel_power_off() → S5 진입 (기본)
 *   │     또는 kernel_restart()   → 재부팅 (resume= 없을 때 테스트용)
 *   │
 *   └── ★ 전원 완전 차단 ★
 *
 * === Resume 흐름 (다음 부팅 시) ===
 *
 * 부트로더 → 커널 로드 → start_kernel() → ... →
 *
 * [R1] hibernate_resume_setup()
 *   → 커널 커맨드라인 "resume=/dev/sdXN" 파싱
 *
 * [R2] software_resume()  [late_initcall]
 *   ├── swsusp_check()   → swap 파티션에서 "S1SUSPEND" 서명 확인
 *   ├── freeze_processes()
 *   ├── swsusp_read()    → 디스크에서 스냅샷 이미지 읽기
 *   │     → LZO/LZ4 압축 해제 → 메모리 페이지 복원
 *   │
 *   ├── hibernation_restore()
 *   │     ├── dpm_suspend_start()
 *   │     ├── disable_nonboot_cpus()
 *   │     ├── syscore_suspend()
 *   │     │
 *   │     ├── swsusp_arch_resume()  ★ 메모리 이미지 복원 ★
 *   │     │     → 저장된 페이지를 원래 위치에 덮어쓰기
 *   │     │     → CPU 레지스터 복원 → 스택 전환
 *   │     │     → swsusp_arch_suspend()가 return 1로 복귀
 *   │     │
 *   │     ├── syscore_resume()
 *   │     ├── enable_nonboot_cpus()
 *   │     └── dpm_resume_end()     → 디바이스 resume
 *   │
 *   └── thaw_processes()           → 프로세스 해동
 *       → 시스템이 hibernate 직전 상태로 완전 복원
 */
Hibernate 필수 설정:
  • swap 파티션/파일 크기 ≥ 사용 중인 RAM (free -h의 used 기준)
  • 커널 파라미터: resume=/dev/sdXN 또는 resume=UUID=...
  • initramfs에 resume 모듈 포함: /etc/initramfs-tools/conf.d/resume
  • swap 파일 사용 시: resume_offset= 파라미터 추가 필요 (filefrag -v로 오프셋 확인)
/* === Hibernate 이미지 구조 ===
 *
 * swap 파티션에 기록되는 구조:
 *
 * ┌──────────────────────────────────────┐
 * │ swap page 0: swsusp_info             │
 * │   - "S1SUSPEND" 서명 (8바이트)       │
 * │   - 커널 버전, 페이지 수, 이미지 크기│
 * │   - CRC32 체크섬                     │
 * │   - 첫 번째 데이터 페이지 오프셋     │
 * ├──────────────────────────────────────┤
 * │ swap page 1~N: 압축된 메모리 이미지  │
 * │   - LZO 또는 LZ4 압축 (기본: LZO)   │
 * │   - 멀티스레드 압축/해제 지원        │
 * │   - pfn(Page Frame Number) 매핑 포함 │
 * └──────────────────────────────────────┘
 *
 * 이미지 크기 제한:
 *   /sys/power/image_size (기본: RAM의 2/5)
 *   커널이 클린(디스크와 동기화된) 페이지를 제외하여 이미지 축소
 *   → 실제 이미지는 사용 메모리보다 상당히 작음
 *
 * 압축 알고리즘 선택:
 *   echo lz4 > /sys/power/tuxonice/compressor  (TuxOnIce)
 *   또는 CONFIG_HIBERNATION_COMP_LZO / CONFIG_HIBERNATION_COMP_LZ4
 */

Reboot 상세 흐름

커널 재부팅은 reboot 명령 또는 reboot() 시스템 콜로 트리거됩니다. 모든 프로세스를 종료하고 디바이스를 정리한 뒤 하드웨어 리셋을 수행합니다.

/* === Reboot 시스템 콜 ===
 *
 * 소스: kernel/reboot.c
 *
 * #include <linux/reboot.h>
 * #include <sys/reboot.h>   (사용자 공간)
 *
 * reboot() 시스템 콜:
 *   reboot(LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, arg)
 *   - MAGIC1 = 0xfee1dead  ("feel dead")
 *   - MAGIC2 = 0x28121969 | 0x05121996 | 0x16041998 | 0x20112000
 *     (Linus 및 자녀들의 생년월일)
 *   - cmd: LINUX_REBOOT_CMD_RESTART   = 0x01234567  (재부팅)
 *          LINUX_REBOOT_CMD_HALT      = 0xCDEF0123  (정지)
 *          LINUX_REBOOT_CMD_POWER_OFF = 0x4321FEDC  (전원 끄기)
 *          LINUX_REBOOT_CMD_RESTART2  = 0xA1B2C3D4  (문자열 인자 재부팅)
 *          LINUX_REBOOT_CMD_KEXEC     = 0x45584543  (kexec)
 */

/* === kernel_restart() 내부 흐름 ===
 *
 * SYSCALL_DEFINE4(reboot, ...) [kernel/reboot.c]
 *   → cmd == LINUX_REBOOT_CMD_RESTART:
 *     → kernel_restart(NULL)
 *
 * kernel_restart(char *cmd):
 *   ├── kernel_restart_prepare(cmd)
 *   │     ├── blocking_notifier_call_chain(&reboot_notifier_list, ...)
 *   │     │     → 등록된 reboot notifier들 순차 호출
 *   │     │       (예: watchdog 정지, 하드웨어 LED 끄기, IPMI 알림 등)
 *   │     ├── system_state = SYSTEM_RESTART
 *   │     └── device_shutdown()
 *   │           → 모든 디바이스의 .shutdown() 콜백 호출 (역순)
 *   │           → 디스크 캐시 플러시, DMA 정지, NIC 링 해제 등
 *   │
 *   ├── kmsg_dump(KMSG_DUMP_SHUTDOWN) → 로그 덤프 (pstore 등)
 *   ├── migrate_to_reboot_cpu()       → CPU 0(BSP)으로 마이그레이션
 *   ├── syscore_shutdown()            → syscore_ops .shutdown() 체인
 *   │
 *   └── machine_restart(cmd)          ★ 아키텍처별 하드웨어 리셋 ★
 *         → arch/x86/kernel/reboot.c: native_machine_restart()
 */

/* === x86 리셋 메서드 (native_machine_restart) ===
 *
 * 소스: arch/x86/kernel/reboot.c
 *
 * 커널은 여러 리셋 메서드를 순차적으로 시도합니다:
 * (reboot= 커널 파라미터로 순서/방법 지정 가능)
 */
enum reboot_type {
    BOOT_TRIPLE    = 't',  /* Triple Fault — IDT를 0으로 → #GP → CPU 리셋 */
    BOOT_KBD       = 'k',  /* 키보드 컨트롤러 (i8042) — port 0x64에 0xFE 기록 */
    BOOT_BIOS      = 'b',  /* BIOS 리셋 — 리얼 모드 전환 → BIOS warm reboot */
    BOOT_ACPI      = 'a',  /* ACPI RESET_REG — FADT의 리셋 레지스터 사용 */
    BOOT_EFI       = 'e',  /* EFI ResetSystem(EfiResetCold, ...) */
    BOOT_CF9_FORCE = 'p',  /* PCI CF9 리셋 — I/O 포트 0xCF9에 0x06 기록 */
};

/* 기본 시도 순서 (ACPI 리셋 레지스터가 있는 경우):
 *   1. ACPI RESET_REG (FADT Generic Address)
 *   2. 키보드 컨트롤러 (i8042 0xFE)
 *   3. EFI ResetSystem()
 *   4. PCI CF9
 *   5. Triple Fault (최후의 수단)
 *
 * 커널 파라미터 예시:
 *   reboot=acpi       → ACPI 리셋 우선
 *   reboot=efi        → EFI ResetSystem() 우선
 *   reboot=kbd        → 키보드 컨트롤러 우선
 *   reboot=pci        → PCI CF9 리셋 우선
 *   reboot=triple     → Triple Fault
 *   reboot=bios       → BIOS warm reboot
 *   reboot=cold       → 콜드 리셋 (메모리 초기화)
 *   reboot=warm       → 웜 리셋 (BIOS POST 스킵)
 *   reboot=force      → emergency_restart() 사용 (notifier 호출 안 함)
 */

/* 각 리셋 메서드 상세 */

/* 1. ACPI RESET_REG
 * FADT(Fixed ACPI Description Table)에 정의된 리셋 레지스터:
 *   - reset_reg: Generic Address Structure (I/O, MMIO, 또는 PCI Config)
 *   - reset_value: 레지스터에 기록할 값
 *
 * acpi_reboot():
 *   acpi_reset() → FADT.reset_reg에 FADT.reset_value 기록
 *   → 칩셋이 리셋 신호 생성 */

/* 2. 키보드 컨트롤러 (i8042)
 *   outb(0xFE, 0x64)  → CPU 리셋 라인(A20 컨트롤러) 활성화
 *   → 레거시 시스템의 전통적 리셋 방법
 *   → PS/2 컨트롤러 없는 시스템에서는 동작 안 함 */

/* 3. EFI ResetSystem()
 *   efi.reset_system(EFI_RESET_COLD, EFI_SUCCESS, 0, NULL)
 *   → UEFI Runtime Service를 통한 리셋
 *   → 가장 신뢰성 높은 방법 (현대 시스템) */

/* 4. PCI CF9
 *   outb(0x02, 0xCF9)  → CF9 초기화
 *   outb(0x06, 0xCF9)  → 풀 리셋 (bit1=시스템리셋, bit2=콜드리셋)
 *   → Intel PCH/ICH 칩셋의 리셋 컨트롤 레지스터
 *   → 0x0E = 풀 리셋 + CPU 리셋 */

/* 5. Triple Fault
 *   load_idt(&no_idt)  → IDT를 0으로 설정
 *   __asm__ __volatile__("int3")  → #BP → IDT 없음 → #DF → #TF
 *   → CPU가 Triple Fault → 무조건적 리셋
 *   → 다른 모든 방법이 실패했을 때의 최후의 수단 */
Reboot Notifier: 드라이버는 register_reboot_notifier()로 재부팅 알림을 등록합니다. 이 notifier에서 워치독 정지, RAID 배터리 상태 저장, IPMI 시스템 이벤트 로그 기록, 원격 관리 카드(BMC) 알림 등을 수행합니다. notifier에서의 지연은 재부팅 시간에 직접 영향을 줍니다.
/* === Reboot Notifier 등록 예시 === */
#include <linux/reboot.h>

static int my_reboot_handler(struct notifier_block *nb,
                             unsigned long action, void *data)
{
    switch (action) {
    case SYS_RESTART:     /* reboot */
    case SYS_HALT:        /* halt */
    case SYS_POWER_OFF:   /* poweroff */
        my_hardware_shutdown();
        break;
    }
    return NOTIFY_DONE;
}

static struct notifier_block my_reboot_nb = {
    .notifier_call = my_reboot_handler,
    .priority = 0,  /* 높을수록 먼저 호출 */
};

/* 모듈 init에서 등록 */
register_reboot_notifier(&my_reboot_nb);

/* 모듈 exit에서 해제 */
unregister_reboot_notifier(&my_reboot_nb);

Poweroff (S5) 상세 흐름

전원 차단은 ACPI S5 상태 또는 EFI ResetSystem(Shutdown)으로 수행됩니다. poweroff, shutdown -h now, systemctl poweroff 명령이 이 경로를 트리거합니다.

/* === Poweroff 전체 흐름 ===
 *
 * 소스: kernel/reboot.c, kernel/power/poweroff.c
 *
 * 사용자 공간 → 커널:
 *   systemctl poweroff → systemd가 SIGTERM → SIGKILL → reboot(POWER_OFF)
 *   shutdown -h now    → init 프로세스가 runlevel 0 진입
 *   reboot(LINUX_REBOOT_CMD_POWER_OFF)
 *
 * SYSCALL_DEFINE4(reboot, ...) [kernel/reboot.c]
 *   → cmd == LINUX_REBOOT_CMD_POWER_OFF:
 *     → kernel_power_off()
 *
 * kernel_power_off():
 *   ├── kernel_shutdown_prepare(SYSTEM_POWER_OFF)
 *   │     ├── blocking_notifier_call_chain(&reboot_notifier_list,
 *   │     │                                 SYS_POWER_OFF, ...)
 *   │     │     → reboot notifier들 호출 (재부팅과 동일)
 *   │     ├── system_state = SYSTEM_POWER_OFF
 *   │     └── device_shutdown()
 *   │           → 모든 디바이스 .shutdown() 콜백
 *   │           → 디스크 캐시 플러시 ★ (데이터 무결성 핵심)
 *   │           → USB 컨트롤러 정지, NIC 링 해제 등
 *   │
 *   ├── kmsg_dump(KMSG_DUMP_SHUTDOWN)  → 로그 덤프
 *   ├── migrate_to_reboot_cpu()        → CPU 0으로 마이그레이션
 *   ├── syscore_shutdown()
 *   │
 *   └── pm_power_off()               ★ 플랫폼별 전원 차단 ★
 *         → 함수 포인터, 플랫폼 초기화 시 등록됨
 *
 *   ※ pm_power_off가 NULL이거나 실패 시:
 *     → kernel_halt()로 폴백 (CPU 정지, 전원은 계속 공급)
 */

/* === pm_power_off 등록 메커니즘 ===
 *
 * 아키텍처/플랫폼별로 pm_power_off 함수 포인터를 설정:
 *
 * x86 ACPI:
 *   acpi_power_off() [drivers/acpi/sleep.c]
 *     → acpi_enter_sleep_state(ACPI_STATE_S5)
 *       → SLP_TYPa/SLP_TYPb에 S5 값 기록
 *       → PM1a_CNT에 SLP_EN 비트 설정
 *       → ★ 전원 차단 ★
 *
 * x86 EFI:
 *   efi_power_off() [drivers/firmware/efi/reboot.c]
 *     → efi.reset_system(EFI_RESET_SHUTDOWN, ...)
 *       → UEFI Runtime Service로 전원 차단
 *
 * ARM (Device Tree 기반):
 *   gpio-poweroff, syscon-poweroff 등 드라이버가 등록
 *   → GPIO 핀 토글 또는 PMIC 레지스터 기록으로 전원 차단
 *
 * PSCI (ARM64 가상화):
 *   psci_sys_poweroff()
 *     → PSCI SYSTEM_OFF SMC 호출 → 펌웨어가 전원 차단
 */

/* === Halt vs Poweroff ===
 *
 * kernel_halt():
 *   → device_shutdown() + CPU 정지 (hlt 루프)
 *   → 전원은 계속 공급됨 (ATX PSU ON 상태)
 *   → "System halted." 메시지 출력 후 무한 대기
 *   → 수동으로 전원 버튼을 눌러야 함
 *
 * kernel_power_off():
 *   → device_shutdown() + pm_power_off()
 *   → ACPI S5 또는 EFI로 ATX PSU를 OFF
 *   → 전원이 자동으로 차단됨
 *
 * 역사적 배경:
 *   AT 파워서플라이(1990년대): 소프트웨어 전원 차단 불가 → halt만 가능
 *   ATX 파워서플라이(1996~): ACPI로 소프트웨어 전원 차단 가능
 */
Emergency 경로: 시스템이 정상 종료할 수 없는 비상 상황에서는 다른 경로를 사용합니다:
  • SysRq+Oemergency_power_off(): notifier/device_shutdown 없이 즉시 전원 차단
  • SysRq+Bemergency_restart(): notifier/device_shutdown 없이 즉시 재부팅
  • SysRq+S,U,B — 안전한 비상 재부팅: Sync(디스크 동기화) → Umount(파일시스템 읽기전용) → reBoot
  • panic() — 커널 패닉 시: panic_timeout초 후 자동 재부팅 (kernel.panic=10)
  • 하드웨어 워치독/dev/watchdog: 일정 시간 내 리프레시 없으면 칩셋이 강제 리셋
/* === systemd의 종료 과정 (사용자 공간) ===
 *
 * systemctl poweroff / reboot 실행 시:
 *
 * 1. systemd가 shutdown.target 활성화
 *    → 모든 서비스 유닛 역순으로 정지
 *    → ExecStop= 또는 SIGTERM (TimeoutStopSec 후 SIGKILL)
 *
 * 2. 파일시스템 언마운트
 *    → systemd-shutdown이 모든 마운트 해제
 *    → 루트 파일시스템은 읽기 전용으로 리마운트
 *
 * 3. reboot() 시스템 콜 호출
 *    → LINUX_REBOOT_CMD_POWER_OFF (poweroff)
 *    → LINUX_REBOOT_CMD_RESTART  (reboot)
 *
 * 타임아웃 설정 (/etc/systemd/system.conf):
 *   DefaultTimeoutStopSec=90s   → 서비스 정지 타임아웃
 *   FinalKillSignal=SIGKILL     → 타임아웃 후 강제 종료 시그널
 *
 * 종료 지연 디버깅:
 *   systemd-analyze blame       → 서비스별 시작/정지 시간
 *   journalctl -b -1 -e         → 마지막 종료 로그 확인
 */

/* === kexec — 부트로더 없는 빠른 재부팅 ===
 *
 * 소스: kernel/kexec.c, kernel/kexec_core.c
 *
 * 기존 커널에서 새 커널을 직접 로드하여 BIOS/UEFI POST 단계를 건너뜀.
 * 서버 환경에서 재부팅 시간을 수십 초 → 수 초로 단축.
 *
 * 사용 순서:
 *   1. kexec -l /boot/vmlinuz --initrd=/boot/initrd.img \
 *        --command-line="root=/dev/sda1 ..."
 *      → 새 커널을 메모리에 미리 로드 (segments 준비)
 *
 *   2. kexec -e  또는  reboot(LINUX_REBOOT_CMD_KEXEC)
 *      → machine_kexec()
 *        → 모든 디바이스 shutdown
 *        → disable_nonboot_cpus()
 *        → machine_kexec(kexec_image)
 *          → relocate_kernel: 커널 이미지를 최종 위치에 복사
 *          → 새 커널의 startup_64로 점프
 *          → 기존 커널 메모리 해제 → 새 커널 시작
 *
 * kexec와 kdump의 차이:
 *   kexec:  정상 재부팅 대체 (빠른 커널 교체)
 *   kdump:  커널 패닉 시 크래시 덤프 수집용
 *           → 예약된 메모리에 미리 로드된 "capture 커널"로 전환
 *           → /proc/vmcore로 크래시 메모리 덤프 접근
 *           → makedumpfile로 덤프 저장 → crash 도구로 분석
 */
# === Reboot / Poweroff / Suspend 실전 명령어 모음 ===

# 재부팅
reboot                           # systemd → reboot(RESTART)
systemctl reboot                 # 동일
echo b > /proc/sysrq-trigger    # SysRq 즉시 재부팅 (비상용)

# 전원 차단
poweroff                         # systemd → reboot(POWER_OFF)
systemctl poweroff               # 동일
shutdown -h now                  # halt 후 전원 차단
echo o > /proc/sysrq-trigger    # SysRq 즉시 전원 차단 (비상용)

# Suspend-to-RAM
systemctl suspend                # S3 또는 s2idle
echo mem > /sys/power/state      # 직접 제어
pm-suspend                       # pm-utils (레거시)

# Hibernate
systemctl hibernate              # S4
echo disk > /sys/power/state     # 직접 제어

# 절전 상태 확인
cat /sys/power/state             # 지원되는 상태 목록
cat /sys/power/mem_sleep         # s2idle vs deep(S3) 선택
cat /sys/power/disk              # hibernate 모드: platform/shutdown/reboot

# 리셋 메서드 확인/변경
cat /sys/kernel/reboot/mode      # cold / warm
cat /sys/kernel/reboot/type      # kbd / acpi / efi / pci / triple

# kexec 빠른 재부팅
kexec -l /boot/vmlinuz-$(uname -r) \
    --initrd=/boot/initrd.img-$(uname -r) \
    --command-line="$(cat /proc/cmdline)"
kexec -e                         # POST 건너뛰고 새 커널로 직접 점프

# 종료 디버깅
dmesg | grep -i "reboot\|shutdown\|power"   # 커널 종료 메시지
journalctl -b -1 --no-pager | tail -50      # 이전 부팅의 마지막 로그
cat /sys/power/pm_debug_messages             # PM 디버그 메시지 활성화
echo 1 > /sys/power/pm_debug_messages       # suspend 상세 로그 켜기

전원 관리 주의사항

전원 관리 핵심 고려사항:
  • Suspend/Resume 순서 — suspend: 유저 프로세스 동결 → 디바이스 late_suspend → noirq_suspend. resume은 역순. 의존성 있는 디바이스는 device_link로 순서 보장
  • Runtime PM 균형pm_runtime_get/put 쌍이 불균형이면 영원히 잠들거나 깨어나지 못함. pm_runtime_get_sync 실패 시 put 호출 누락 주의
  • IRQ 안전성 — noirq 단계에서는 인터럽트가 비활성. 이 단계의 suspend/resume 콜백에서 인터럽트 의존 코드 사용 금지
  • 클럭/레귤레이터 — suspend 시 클럭 비활성화 후 resume에서 복원 안 하면 디바이스 동작 불능. clk_prepare_enable/clk_disable_unprepare 쌍 필수
  • wakeup 소스device_init_wakeup()으로 등록한 디바이스만 suspend 상태에서 시스템을 깨울 수 있음. cat /sys/power/wakeup_count
  • C-state 지연 트레이드오프 — 깊은 C-state는 전력 절감이 크지만 복귀 지연(exit latency)도 큼. HFT/실시간 환경에서는 C1 이하로 제한
  • Hibernate 메모리 — 전체 RAM 내용을 디스크에 기록. swap 파티션이 RAM보다 작으면 hibernate 실패. resume= 커널 파라미터 필수
  • ACPI 의존성 — x86 전원 관리는 ACPI에 크게 의존. DSDT/SSDT 테이블 버그가 suspend 실패의 주요 원인. acpidumpiasl로 디버깅

디바이스 드라이버 주요 버그 패턴

디바이스 드라이버는 커널 코드의 약 70%를 차지하며, 커널 취약점의 가장 큰 원인입니다. 하드웨어와의 상호작용, 비동기 이벤트 처리, DMA 메모리 관리 등에서 반복적으로 발생하는 버그 패턴을 분석합니다.

DMA 매핑 방향 오류

DMA 방향(direction) 불일치 — 데이터 손상/정보 누출:

DMA 매핑 시 dma_map_single()에 전달하는 방향(DMA_TO_DEVICE, DMA_FROM_DEVICE, DMA_BIDIRECTIONAL)이 실제 데이터 흐름과 일치하지 않으면, 캐시 일관성(cache coherency) 문제로 데이터 손상이 발생하거나 초기화되지 않은 커널 메모리가 장치로 전송되어 정보가 누출될 수 있습니다.

/* DMA 방향 오류 패턴 */

/* 취약: 수신 버퍼를 DMA_TO_DEVICE로 매핑 */
dma_addr = dma_map_single(dev, rx_buf, len, DMA_TO_DEVICE);
/* → 캐시가 무효화되지 않아 장치가 쓴 데이터 대신
 *   CPU 캐시의 오래된 데이터를 읽음 (데이터 손상) */

/* 수정: 올바른 방향 지정 */
dma_addr = dma_map_single(dev, rx_buf, len, DMA_FROM_DEVICE);

/* DMA-API 디버깅으로 방향 오류 탐지 */
CONFIG_DMA_API_DEBUG=y
/* 런타임에 dma_map/unmap 쌍의 방향 일치 여부 검증 */
/* dmesg에 "DMA-API: device driver has a bug" 메시지 출력 */

인터럽트 핸들러 경쟁 조건

IRQ 핸들러와 프로세스 컨텍스트 간 경쟁:

인터럽트 핸들러에서 접근하는 공유 데이터를 프로세스 컨텍스트에서도 접근할 때, spin_lock_irqsave() 대신 spin_lock()을 사용하면 데드락이 발생합니다. 프로세스 컨텍스트에서 lock을 보유한 상태에서 인터럽트가 발생하면, IRQ 핸들러가 같은 lock을 획득하려다 영원히 대기합니다.

/* 드라이버 IRQ 데드락 패턴 */

/* 취약: process context에서 spin_lock() 사용 */
spin_lock(&dev->lock);       /* process context */
dev->status = NEW_STATUS;
spin_unlock(&dev->lock);
/* ↑ 이 사이에 IRQ 발생 시 데드락 */

static irqreturn_t my_irq(int irq, void *data) {
    spin_lock(&dev->lock);   /* IRQ context — 이미 잠겨있으면 데드락 */
    process_data(dev);
    spin_unlock(&dev->lock);
    return IRQ_HANDLED;
}

/* 수정: spin_lock_irqsave()로 IRQ 비활성화 */
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags);  /* IRQ 비활성화 + 잠금 */
dev->status = NEW_STATUS;
spin_unlock_irqrestore(&dev->lock, flags);

devm 리소스 해제 순서 문제

devm (managed) 리소스의 역순 해제 함정:

devm_* API로 할당된 리소스는 드라이버 제거 시 등록 역순으로 자동 해제됩니다. 그러나 리소스 간 의존 관계를 고려하지 않으면 이미 해제된 리소스를 참조하는 문제가 발생합니다. 예를 들어, IRQ 핸들러가 접근하는 메모리 버퍼가 IRQ보다 먼저 해제되면 Use-After-Free가 발생합니다.

/* devm 해제 순서 문제 예시 */

static int my_probe(struct platform_device *pdev) {
    /* 등록 순서: 1→버퍼, 2→IRQ */
    buf = devm_kzalloc(&pdev->dev, BUF_SIZE, GFP_KERNEL); /* #1 */
    devm_request_irq(&pdev->dev, irq, handler, ...);     /* #2 */

    /* 해제 순서 (역순): #2→IRQ 해제, #1→버퍼 해제 ✓ */
    /* → IRQ가 먼저 해제되므로 안전 */

    /* 위험한 등록 순서: 1→IRQ, 2→버퍼 */
    devm_request_irq(&pdev->dev, irq, handler, ...);     /* #1 */
    buf = devm_kzalloc(&pdev->dev, BUF_SIZE, GFP_KERNEL); /* #2 */

    /* 해제 순서 (역순): #2→버퍼 해제, #1→IRQ 해제 ✗ */
    /* → 버퍼가 먼저 해제되는데 IRQ 핸들러가 아직 활성 → UAF */
}

/* 교훈: 의존 관계를 고려하여 devm 등록 순서 결정
 * 의존하는 리소스(데이터)를 먼저 등록, 소비자(IRQ/타이머)를 나중에 등록
 * → 해제 시 소비자가 먼저 해제되어 안전 */

USB/PCI 핫플러그 경쟁 조건

장치 제거 중 접근 (disconnect race):

USB나 PCI 핫플러그 장치에서 사용자가 장치를 물리적으로 제거하는 동안 드라이버가 여전히 장치에 접근하면, 잘못된 메모리 접근이나 커널 패닉이 발생합니다. disconnect 콜백에서 진행 중인 I/O를 모두 취소하고, 이후의 I/O 요청을 거부해야 합니다.

/* USB disconnect race 방어 패턴 */

struct my_usb_dev {
    struct usb_device *udev;
    struct mutex io_mutex;
    bool disconnected;    /* disconnect 발생 여부 */
    struct kref kref;     /* 참조 카운트 */
};

/* I/O 수행 전 disconnect 여부 확인 */
static ssize_t my_write(struct file *file, ...) {
    mutex_lock(&dev->io_mutex);
    if (dev->disconnected) {
        mutex_unlock(&dev->io_mutex);
        return -ENODEV;  /* 이미 제거됨 */
    }
    /* 안전하게 I/O 수행 */
    retval = usb_bulk_msg(dev->udev, ...);
    mutex_unlock(&dev->io_mutex);
    return retval;
}

/* disconnect 콜백 */
static void my_disconnect(struct usb_interface *intf) {
    struct my_usb_dev *dev = usb_get_intfdata(intf);
    mutex_lock(&dev->io_mutex);
    dev->disconnected = true;  /* 이후 I/O 거부 */
    mutex_unlock(&dev->io_mutex);
    usb_kill_anchored_urbs(&dev->submitted); /* 진행 중인 URB 취소 */
    kref_put(&dev->kref, my_delete);
}
드라이버 버그 탐지 도구 요약:

CONFIG_DMA_API_DEBUG: DMA 매핑/해제 쌍, 방향 일치 여부 검증
CONFIG_LOCKDEP: IRQ/프로세스 컨텍스트 간 잠금 순서 위반 탐지
CONFIG_KASAN: Use-After-Free, 버퍼 오버플로우 런타임 탐지
CONFIG_KFENCE: 프로덕션 환경용 저오버헤드 메모리 오류 샘플링
CONFIG_PROVE_LOCKING: 잠금 의존성 그래프 검증으로 데드락 사전 탐지
coccinelle (spatch): 커널 코드 정적 분석 도구, 일반적인 드라이버 버그 패턴 탐지