디바이스 드라이버 (Device Drivers)
Linux 커널 디바이스 드라이버: character/block/network 디바이스, platform driver, DMA 종합 가이드.
디바이스 드라이버 개요
디바이스 드라이버는 커널과 하드웨어 사이의 인터페이스입니다. 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 Tree | DTS/DTB 파일에 하드웨어 기술, 부트로더가 전달 | ARM, RISC-V, PowerPC |
| ACPI | ACPI 테이블(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_node | Device Tree에서 생성된 경우, 해당 DT 노드 (struct device_node *) |
dev.fwnode | 통합 firmware 노드. DT와 ACPI를 추상화하는 struct fwnode_handle * |
resource | MMIO 주소 범위, IRQ 번호, DMA 채널 등 하드웨어 리소스 |
driver_override | sysfs에서 수동으로 특정 드라이버를 강제 바인딩할 때 사용 |
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_table | Device Tree compatible 매칭 테이블 |
driver.acpi_match_table | ACPI HID/CID 매칭 테이블 |
driver.pm | struct dev_pm_ops * — 최신 PM 콜백 (전원 관리 참고) |
driver.owner | THIS_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_override | sysfs에서 수동 설정된 드라이버 이름과 driver.name 비교 |
| 2 | of_match_table | DT 노드의 compatible과 드라이버의 of_device_id 배열 비교 |
| 3 | acpi_match_table | ACPI 디바이스 HID/CID와 acpi_device_id 배열 비교 |
| 4 | id_table | platform_device.name과 platform_device_id.name 비교 |
| 5 (최저) | driver.name | platform_device.name과 driver.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 호출 조건:
- 디바이스가 등록될 때 이미 드라이버가 로드되어 있으면 즉시 호출
- 드라이버가 등록될 때 이미 디바이스가 존재하면 즉시 호출
- 둘 다 존재하고
platform_match()가 성공해야 함
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)
#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_type | sysfs 경로 | match 방식 | 디바이스 열거 방식 |
|---|---|---|---|
platform_bus_type | /sys/bus/platform/ | DT compatible, ACPI HID, name | DT/ACPI/정적 등록 |
pci_bus_type | /sys/bus/pci/ | vendor:device:class ID | PCI 설정 공간 스캔 |
usb_bus_type | /sys/bus/usb/ | vendor:product:class ID | USB 열거 프로토콜 |
i2c_bus_type | /sys/bus/i2c/ | DT compatible, i2c_device_id | DT/보드 파일/ACPI |
spi_bus_type | /sys/bus/spi/ | DT compatible, spi_device_id | DT/보드 파일/ACPI |
virtio_bus | /sys/bus/virtio/ | device ID + feature bits | virtio 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)되는 과정은 디바이스 모델의 핵심 메커니즘입니다:
/* 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", ... */
디바이스 모델 전체 계층 구조
커스텀 버스 구현 예제
드라이버 서브시스템 개발자를 위한 커스텀 버스 타입 구현 예제입니다:
/* 커스텀 버스: 간단한 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);
콜백 함수의 매개변수:
nb: 현재 호출 중인notifier_block포인터 (container_of로 상위 구조체 접근 가능)action: 이벤트 유형 (예:NETDEV_UP,NETDEV_DOWN)data: 이벤트별 추가 데이터 (예:struct net_device *)
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 동작 흐름
실전 구현 예제
예제 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 (소스) → dtc (컴파일러) → .dtb (바이너리 블롭) → 부트로더가 메모리에 로드 → 커널이 파싱하여 struct device_node 트리 구축 → 드라이버가 of_* API로 프로퍼티 조회
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;
};
};
};
};
};
};
표준 프로퍼티 레퍼런스
| 프로퍼티 | 타입 | 설명 | 예시 |
|---|---|---|---|
compatible | string-list | 드라이버 매칭 키. 구체적→일반적 순서 | "vendor,exact", "vendor,fallback" |
reg | prop-encoded | 주소/크기 쌍. 해석은 부모의 #address-cells/#size-cells에 의존 | <0x10000 0x1000> |
interrupts | prop-encoded | 인터럽트 지정자. 해석은 인터럽트 컨트롤러의 #interrupt-cells에 의존 | <GIC_SPI 42 IRQ_TYPE_LEVEL_HIGH> |
interrupt-parent | phandle | 인터럽트 컨트롤러 참조 (생략 시 부모 노드에서 상속) | <&gic> |
clocks | phandle+args | 클럭 소스 참조 | <&ccu CLK_UART0> |
clock-names | string-list | 클럭 이름 (clocks와 순서 대응) | "apb", "mod" |
resets | phandle+args | 리셋 컨트롤러 참조 | <&ccu RST_UART0> |
status | string | "okay"=활성, "disabled"=비활성 | "okay" |
#address-cells | u32 | 자식 reg의 주소 u32 개수 | <2> |
#size-cells | u32 | 자식 reg의 크기 u32 개수 (0이면 크기 없음) | <1> |
ranges | prop-encoded | 자식→부모 주소 변환. 빈 값이면 1:1 매핑 | <0x0 0x0 0x10000000 0x1000000> |
dma-ranges | prop-encoded | DMA 주소 변환 (CPU 주소 ≠ DMA 주소일 때) | <0x0 0x0 0x80000000 0x80000000> |
pinctrl-0 | phandle-list | 핀 설정 참조 (상태 0=default) | <&uart0_pins> |
pinctrl-names | string-list | 핀 설정 상태 이름 | "default", "sleep" |
*-gpios | phandle+args | GPIO 참조 (접두사가 이름) | reset-gpios = <&gpio1 5 GPIO_ACTIVE_LOW> |
*-supply | phandle | 전원 레귤레이터 참조 | 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 직접 비교 (폴백)
*/
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 # 경고 활성화 빌드
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 트리를 구축합니다.
/* ===== 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 프로퍼티가 자식 주소 공간 → 부모 주소 공간으로의 변환 규칙을 정의합니다.
/* ===== 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/SPARC | Intel, x86 서버/데스크톱 |
| 주요 플랫폼 | ARM, RISC-V, PowerPC, MIPS | x86, 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 리소스 패킹 |
| 커널 API | of_*() (DT 전용) | acpi_*() (ACPI 전용) |
| 통합 API | device_property_*() / fwnode_*() — DT/ACPI 양쪽 지원 | |
| 바인딩 문서 | Documentation/devicetree/bindings/ (YAML) | ACPI Spec + DSDT/SSDT (벤더 구현) |
| 검증 도구 | dt_binding_check, dtbs_check | iasl (Intel ASL Compiler), acpidump |
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>;
};
};
arch/arm64/boot/dts/ 디렉토리에서 실제 SoC의 .dtsi 파일을 읽어보면 DT 구조를 빠르게 이해할 수 있습니다. 특히 compatible 문자열로 커널에서 대응하는 드라이버(drivers/)를 검색하면 DT↔드라이버 연결 관계를 파악할 수 있습니다: git grep "allwinner,sun50i-h6-emac" drivers/
Managed Device Resources (devm)
devm_* API는 디바이스 해제 시 리소스를 자동으로 정리합니다. 에러 경로에서의 수동 해제가 불필요합니다.
| 일반 API | Managed 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_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_mapping_error()를 항상 호출. IOMMU 공간 소진 시 실패 가능 - bounce buffer 인지 — SWIOTLB 사용 시 실제 복사가 발생하여 성능 저하.
dma_set_mask()로 64비트 지원 확인 - DMA 주소 수명 — map과 unmap 사이에서만 유효. unmap 후 DMA 주소 사용 금지
- 캐시 라인 공유 금지 — DMA 버퍼가 다른 데이터와 같은 캐시 라인을 공유하면 false sharing 발생.
____cacheline_aligned사용 - Scatter-Gather 병합 — IOMMU는 물리적으로 불연속인 페이지를 DMA 주소 공간에서 연속으로 매핑 가능.
dma_map_sg()후sg_dma_len()이 원래 세그먼트와 다를 수 있음 - DMA coherent 메모리의 성능 — ARM 등 non-x86에서 coherent 메모리는 uncached로 할당되어 CPU 접근이 느림. 대용량 데이터는 streaming DMA 선호
- 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);
- BAR 매핑 —
ioremap()반환값은__iomem포인터.readl()/writel()로만 접근 (직접 포인터 역참조 금지) - Config Space 접근 —
pci_read_config_dword()로 구성 공간 읽기. 잘못된 접근은 시스템 행(hang) 유발 가능 - MSI vs MSI-X — MSI는 최대 32개, MSI-X는 2048개 벡터. 멀티큐 디바이스에서는 MSI-X 필수
- SR-IOV —
pci_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
- 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) 서브시스템
| 특성 | I2C | SMBus 3.0 | I3C (Basic) | I3C (HDR) |
|---|---|---|---|---|
| 최대 속도 | 3.4 Mbps (Hs) | 1 Mbps | 12.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, SCL | SDA, SCL | SDA, 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
* };
* };
*/
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
- 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-gpiosDT 속성). GPIO CS는 소프트웨어 제어라 속도 저하 가능 - Quad/Dual SPI —
spi-rx-bus-width/spi-tx-bus-widthDT 속성 설정 필수. 컨트롤러 드라이버가SPI_RX_QUAD/SPI_TX_QUAD지원해야 함 - 전이중 vs 반이중 — 대부분의 SPI Flash는 명령/주소 전송 후 데이터 수신 (반이중 방식).
spi_transfer에서tx_buf와rx_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]
- 레거시 API 사용 금지 —
gpio_request()/gpio_set_value()는 deprecated. 반드시gpiod_*descriptor API 사용. sysfs/sys/class/gpio/export도 deprecated →/dev/gpiochipNchardev 사용 - 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-intervalDT 속성) - GPIO hog — 특정 GPIO를 부팅 시 고정 상태로 설정: DT에서
gpio-hog; gpios = <5 0>; output-high;→ 드라이버 없이도 GPIO 상태 보장
&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/lane | PCI enumeration (자동) | GPU, NIC, NVMe, QAT |
| I2C | 멀티마스터 직렬 버스 | 3.4 Mbps | Device Tree / ACPI | 센서, EEPROM, PMIC |
| SPI | 마스터-슬레이브 (CS 라인) | 100+ Mbps | Device Tree | Flash, ADC, 디스플레이 |
| GPIO | 점대점 디지털 라인 | MHz급 토글 | Device Tree / ACPI | 리셋, LED, 인터럽트 |
| SATA | 점대점 (포트별 디바이스) | 6 Gbps (SATA III) | AHCI 자동 감지 | HDD, SSD (레거시) |
| NVMe | PCIe 직접 연결 | PCIe 대역폭 | PCI enumeration | 고성능 SSD |
| USB | 트리 (Hub 계층) | 20 Gbps (USB 3.2) | USB enumeration (핫플러그) | 주변장치 범용 |
| MDIO | 직렬 버스 (PHY 전용) | 2.5 MHz | Device 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 서브시스템은 3계층으로 구성됩니다:
- Device Drivers: 하드웨어와 직접 통신하여
input_event를 생성 (input_report_*API) - Input Core (
drivers/input/input.c): 디바이스와 핸들러 사이의 매칭·라우팅 —input_register_device()/input_register_handler() - Event Handlers: 이벤트를 유저 공간에 전달 —
evdev(범용),kbd(콘솔 키보드),mousedev(/dev/input/mice),joydev(조이스틱)
핵심 데이터 구조
/* === 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_SYN | 0x00 | 이벤트 그룹 경계 표시 | SYN_REPORT, SYN_MT_REPORT, SYN_DROPPED |
| 키/버튼 | EV_KEY | 0x01 | 키보드, 마우스 버튼, 게임패드 | KEY_A, BTN_LEFT, BTN_TOUCH |
| 상대 이동 | EV_REL | 0x02 | 마우스 이동, 스크롤 휠 | REL_X, REL_Y, REL_WHEEL |
| 절대 위치 | EV_ABS | 0x03 | 터치스크린, 태블릿, 조이스틱 | ABS_X, ABS_MT_POSITION_X, ABS_MT_SLOT |
| 기타 | EV_MSC | 0x04 | 스캔코드 등 잡다한 이벤트 | MSC_SCAN, MSC_TIMESTAMP |
| 스위치 | EV_SW | 0x05 | 덮개(lid), 헤드폰 잭, 태블릿 모드 | SW_LID, SW_HEADPHONE_INSERT |
| LED | EV_LED | 0x11 | 키보드 LED 제어 | LED_CAPSL, LED_NUML, LED_SCROLLL |
| 사운드 | EV_SND | 0x12 | 비프, 클릭 사운드 | SND_BELL, SND_TONE |
| 반복 | EV_REP | 0x14 | 키 반복 파라미터 | REP_DELAY, REP_PERIOD |
| Force Feedback | EV_FF | 0x15 | 진동/햅틱/포스 피드백 | FF_RUMBLE, FF_CONSTANT, FF_PERIODIC |
| 전원 | EV_PWR | 0x16 | 전원 버튼 이벤트 | (예약됨) |
| FF 상태 | EV_FF_STATUS | 0x17 | FF 효과 재생 상태 | FF_STATUS_STOPPED, FF_STATUS_PLAYING |
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(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_SYN | SYN_REPORT — 이벤트 패킷 완료 표시 |
input_mt_sync(dev) | EV_SYN | SYN_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_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_TYPE | MT_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/mice | EV_REL 또는 EV_ABS + BTN | 레거시 PS/2 마우스 프로토콜 에뮬레이션 (ImPS/2). /dev/input/mice는 모든 마우스의 통합 노드 |
| joydev | /dev/input/jsN | BTN_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 | 방향 | 설명 |
|---|---|---|
EVIOCGNAME | R | 디바이스 이름 문자열 |
EVIOCGID | R | struct input_id (bus, vendor, product, version) |
EVIOCGPHYS | R | 물리적 경로 문자열 |
EVIOCGUNIQ | R | 고유 식별자 문자열 |
EVIOCGPROP | R | 디바이스 프로퍼티 비트마스크 (INPUT_PROP_*) |
EVIOCGBIT(type, size) | R | 지원하는 이벤트 유형/코드 비트마스크 |
EVIOCGABS(axis) | R | struct input_absinfo (min, max, fuzz, flat, res) |
EVIOCSABS(axis) | W | 절대축 파라미터 변경 (캘리브레이션) |
EVIOCGKEY | R | 현재 키 상태 비트마스크 (눌린 키 조회) |
EVIOCGLED | R | 현재 LED 상태 비트마스크 |
EVIOCGSW | R | 현재 스위치 상태 비트마스크 |
EVIOCGRAB | W | 디바이스 독점(grab) — 다른 프로세스/핸들러 차단 |
EVIOCREVOKE | W | fd의 접근 권한 철회 (logind 세션 전환 시) |
EVIOCSFF | W | Force Feedback 효과 업로드 |
EVIOCRMFF | W | Force Feedback 효과 삭제 |
EVIOCGEFFECTS | R | 동시 FF 효과 수 |
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
/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;
}
이벤트 흐름 요약
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 프로토콜 버전 비교
| 버전 | 최대 속도 | 전력 공급 | 커넥터 | 호스트 컨트롤러 |
|---|---|---|---|---|
| USB 1.1 | 12 Mbps (Full Speed) | 500 mA (5V) | Type-A/B | UHCI / OHCI |
| USB 2.0 | 480 Mbps (High Speed) | 500 mA (5V) | Type-A/B, Mini, Micro | EHCI |
| USB 3.0 | 5 Gbps (SuperSpeed) | 900 mA (5V) | Type-A/B (SS), Type-C | xHCI |
| USB 3.1 | 10 Gbps (SS+) | 최대 100W (USB PD) | Type-C | xHCI |
| USB 3.2 | 20 Gbps (2×2) | 최대 100W (USB PD) | Type-C | xHCI |
| USB4 | 40/80 Gbps | 최대 240W (USB PD 3.1) | Type-C | xHCI + 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)과 전송 유형 정의 |
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이면 해제 */
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 모듈이 필요합니다.
| HCD | USB 버전 | 커널 모듈 | 특징 |
|---|---|---|---|
| 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에서 광범위하게 사용 |
drivers/usb/gadget/)를 사용합니다. ConfigFS(/sys/kernel/config/usb_gadget/)를 통해 유저 스페이스에서 USB 기능(mass storage, ethernet, serial 등)을 동적으로 구성할 수 있으며, usb_composite_driver로 복합 Gadget 드라이버를 작성합니다.
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를 파싱하여 사람이 읽기 쉬운 형태로 출력
- 핫플러그 안전성 — 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 아키텍처
| 커널 디렉토리 | 역할 | 디바이스 노드 |
|---|---|---|
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,
};
open → hw_params → prepare → trigger(START) →
[pointer 반복 호출] → trigger(STOP) → hw_free → close.
prepare는 trigger(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;
}
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;
}
"[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_stream_lock()을 잡음.trigger()콜백 내부에서 직접 호출하면 데드락 발생 trigger()에서 period_elapsed 호출이 필요한 경우 tasklet이나 workqueue로 지연 처리- DMA가 중지된 상태에서 호출하면 XRUN(underrun/overrun) 처리가 트리거됨
PCM 상태 머신
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 */
};
};
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>
- trigger() 컨텍스트 —
trigger()콜백은 atomic 컨텍스트(spinlock 보유)에서 호출됩니다. sleep, mutex, kmalloc(GFP_KERNEL) 사용 불가 - pointer() 정확도 —
pointer()는 현재 DMA 위치를 정확히 반환해야 합니다. 부정확하면 오디오 글리치, 타이밍 오류, 리샘플링 아티팩트 발생 - DMA 정렬 —
period_size와buffer_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 인터페이스를 통합 관리합니다.
TTY 코어 데이터 구조
TTY 서브시스템의 핵심 구조체들은 include/linux/tty.h와 include/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_TTY | 0 | 기본 터미널 I/O (canonical/raw 모드) | drivers/tty/n_tty.c |
| N_SLIP | 1 | Serial Line IP — 시리얼 위 IP 통신 | drivers/net/slip/ |
| N_PPP | 3 | Point-to-Point Protocol | drivers/net/ppp/ |
| N_GSM0710 | 21 | GSM 멀티플렉싱 (모뎀) | drivers/tty/n_gsm.c |
| N_NULL | 27 | 모든 데이터를 버림 (테스트용) | drivers/tty/n_null.c |
| N_TRACESINK | 23 | 디버그 트레이스 데이터 싱크 | 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_port와 uart_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/ttyS0 | 8250/16550 호환 시리얼 포트 | drivers/tty/serial/8250/ |
| ttyAMAN | /dev/ttyAMA0 | ARM AMBA PL011 UART | drivers/tty/serial/amba-pl011.c |
| ttyUSBN | /dev/ttyUSB0 | USB-Serial 변환기 (FTDI, CP210x 등) | drivers/usb/serial/ |
| ttyACMN | /dev/ttyACM0 | USB CDC ACM (Abstract Control Model) | drivers/usb/class/cdc-acm.c |
| ttyMFDN | /dev/ttyMFD0 | Intel MID (Medfield) UART | drivers/tty/serial/mfd.c |
| ttyON | /dev/ttyO0 | TI OMAP UART | drivers/tty/serial/omap-serial.c |
| ttySACN | /dev/ttySAC0 | Samsung S3C/S5P UART | drivers/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/ptmx | PTY master 멀티플렉서 | drivers/tty/pty.c |
| console | /dev/console | 시스템 콘솔 (마지막 console= 파라미터) | 커널 코어 |
| ttyGSN | /dev/ttyGS0 | USB Gadget 시리얼 (디바이스 모드) | drivers/usb/gadget/function/u_serial.c |
| ttyLPN | /dev/ttyLP0 | Intel 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
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);
}
- 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/resume —
uart_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/THR | 0x00 | 수신 데이터 (RBR) | 송신 데이터 (THR) |
| IER | 0x01 | 인터럽트 활성화 (RX, TX, Line Status, Modem Status) | |
| IIR/FCR | 0x02 | 인터럽트 식별 (IIR) | FIFO 제어 (FCR) |
| LCR | 0x03 | Line Control (데이터 비트, 정지 비트, 패리티, DLAB) | |
| MCR | 0x04 | Modem Control (DTR, RTS, loopback) | |
| LSR | 0x05 | Line Status (Data Ready, Overrun, Parity Err, TX Empty) | |
| MSR | 0x06 | Modem Status (CTS, DSR, RI, DCD 변화 감지) | |
| SCR | 0x07 | Scratch Register (UART 존재 감지용) | |
| DLL/DLM | 0x00/0x01 | Divisor 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
*/
| 서브시스템 | 주요 드라이버 | 디버깅 도구 |
|---|---|---|
| Input | gpio-keys, atkbd, hid-* | evtest, libinput debug-events |
| USB | xhci-hcd, ehci-hcd, usb-storage | lsusb -v, usbmon |
| V4L2 | uvcvideo, vivid | v4l2-ctl, media-ctl |
| DRM | i915, amdgpu, nouveau | modetest, drm_info |
| ALSA | snd-hda-intel, snd-usb-audio | aplay -l, alsamixer |
| Serial | 8250, pl011, imx-uart | minicom, 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 완료 통보
*/
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 직전 상태로 완전 복원
*/
- 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 → 무조건적 리셋
* → 다른 모든 방법이 실패했을 때의 최후의 수단 */
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로 소프트웨어 전원 차단 가능
*/
- SysRq+O —
emergency_power_off(): notifier/device_shutdown 없이 즉시 전원 차단 - SysRq+B —
emergency_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 실패의 주요 원인.
acpidump와iasl로 디버깅
디바이스 드라이버 주요 버그 패턴
디바이스 드라이버는 커널 코드의 약 70%를 차지하며, 커널 취약점의 가장 큰 원인입니다. 하드웨어와의 상호작용, 비동기 이벤트 처리, DMA 메모리 관리 등에서 반복적으로 발생하는 버그 패턴을 분석합니다.
DMA 매핑 방향 오류
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" 메시지 출력 */
인터럽트 핸들러 경쟁 조건
인터럽트 핸들러에서 접근하는 공유 데이터를 프로세스 컨텍스트에서도 접근할 때, 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_* 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 핫플러그 경쟁 조건
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): 커널 코드 정적 분석 도구, 일반적인 드라이버 버그 패턴 탐지