Device Tree와 Linux 플랫폼 드라이버 작성 입문
Device Tree를 단순한 설정 파일로 외우지 않고, 실제 드라이버가 하드웨어를 발견하고 자원을 준비하는 과정까지 연결해 보자. 예제는 메모리 맵 레지스터와 IRQ, 클록, 전원을 가진 가상의 보드 장치다.
읽는 방법 / How to read this article
각 절에서 먼저 “하드웨어 관점에서 무엇이 연결되어 있는가”를 보고, 이어서 DTS와 C 드라이버가 그 연결을 어떻게 표현하는지 확인한다. 예제 주소와 IRQ 번호는 설명을 위한 값이며, 실제 보드의 데이터시트 값으로 바꿔야 한다.
1. Device Tree는 왜 필요한가? / Why do we need Device Tree?
커널 드라이버가 보드마다 다음처럼 주소를 직접 들고 있다고 가정해 보자.
/* board-a.c */
#define MY_DEVICE_BASE 0x40000000
#define MY_DEVICE_IRQ 42
/* board-b.c */
#define MY_DEVICE_BASE 0x50000000
#define MY_DEVICE_IRQ 73
장치가 같은 종류인데 보드가 바뀔 때마다 드라이버 소스를 복사하거나 #define을 바꿔야 한다. 유지보수가 어려워지고, 커널과 보드의 결합도도 높아진다.
Device Tree는 이 하드웨어 차이를 데이터로 분리한다. 드라이버는 “나는 이런 종류의 장치를 지원한다”고 선언하고, 보드의 DTS는 “이 장치는 이 주소·IRQ·클록·전원을 사용한다”고 설명한다.
하드웨어 설계
├─ register base / size
├─ interrupt line
├─ clock source
└─ power rail
↓ Device Tree
platform device
↓ compatible matching
platform driver
↓ probe()
실제 장치 초기화
2. DTS, DTB, binding / DTS, DTB, and bindings
DTS(Device Tree Source)
: 사람이 읽고 수정하는 텍스트 소스다. 공통 SoC 설명은 .dtsi로, 보드별 연결은 .dts로 나누는 경우가 많다.
DTB(Device Tree Blob)
: dtc가 DTS를 컴파일한 바이너리다. 부트로더가 커널에 전달하고, 커널은 초기 부팅 때 이를 메모리 구조로 펼친다.
Binding
: 노드와 속성의 의미를 정한 계약서다. 어떤 compatible 문자열을 쓰고, reg·clocks·interrupts를 어떤 형식으로 적는지 정의한다.
최근 커널은 binding을 YAML 스키마로 관리하고 dtbs_check로 DTS가 규칙을 지키는지 검증한다. “컴파일만 된다”는 것과 “binding에 맞는다”는 것은 다르다.
3. 예제 하드웨어를 DTS로 표현하기 / Describing the hardware in DTS
가상의 장치가 다음 자원을 가진다고 하자.
| 자원 / Resource | 예시 값 / Example | 용도 / Purpose |
|---|---|---|
| MMIO register | base 0x40000000, size 0x1000 | 제어·상태 레지스터 접근 |
| IRQ | 42 | 장치 이벤트를 CPU에 알림 |
| clock | &clk 3 | 장치 동작 클록 공급 |
| power | ®_3v3 | 전원 레일 제어 |
mydev: sensor@40000000 {
compatible = "example,my-sensor-v1";
reg = <0x40000000 0x1000>;
interrupts = <42>;
clocks = <&clk 3>;
clock-names = "bus";
vdd-supply = <®_3v3>;
status = "okay";
};
노드 이름의 @40000000은 unit address다. 보통 reg의 첫 주소와 맞춰 적는다. 단, 실제 주소 셀과 크기 셀의 개수는 부모 버스의 #address-cells와 #size-cells가 결정한다.
reg를 단순히 두 숫자로 외우지 말 것 / Do not memorize reg as two numbers
부모가 64비트 주소를 요구하면 reg = <0x0 0x40000000 0x0 0x1000>;처럼 네 셀이 될 수 있다. 어떤 형식이 맞는지는 해당 버스의 binding과 상위 노드를 확인해야 한다.
4. compatible가 드라이버를 찾는 과정 / How compatible finds a driver
Device Tree 노드가 있다고 자동으로 C 코드의 probe()가 호출되는 것은 아니다. 대략 다음 두 목록이 맞아야 한다.
DTS node:
compatible = "example,my-sensor-v1";
driver match table:
{ .compatible = "example,my-sensor-v1" }
match 성공 → platform device와 platform driver 연결 → probe(pdev)
Platform device는 CPU에 직접 연결된 메모리 맵 장치를 Linux 장치 모델 안에 표현한 객체다. Platform driver는 이런 장치를 제어하는 드라이버다. PCI처럼 버스가 장치를 열거해 주는 방식과 달리, 플랫폼 장치는 Device Tree나 ACPI가 장치 정보를 제공한다.
compatible는 가능한 한 구체적으로 작성한다. 새 하드웨어가 이전 하드웨어의 상위 호환이라면 드라이버가 여러 문자열을 매칭하도록 fallback을 둘 수 있지만, 단순히 제품군 이름을 와일드카드처럼 쓰면 안 된다.
5. 드라이버의 기본 생명주기 / The driver lifecycle
module load / built-in init
↓
driver registration
↓
Device Tree node appears as platform_device
↓ compatible match
probe(pdev)
├─ allocate private data
├─ map MMIO
├─ enable clock / regulator
├─ request IRQ
├─ initialize registers
└─ publish interface
↓
normal runtime: read/write/IRQ/workqueue
↓
remove() or device-managed cleanup
**probe()**는 “드라이버 파일이 로드됐다”는 함수가 아니라, 특정 장치 하나를 실제로 사용할 준비를 하는 함수다. 자원을 하나씩 준비하다가 실패하면 이미 얻은 자원을 되돌리고, 아직 준비되지 않은 공급자가 있으면 -EPROBE_DEFER를 반환해 나중에 다시 시도하도록 할 수 있다.
6. 실제 플랫폼 드라이버 뼈대 / A practical platform-driver skeleton
아래 코드는 설명을 위한 최소 뼈대다. 실제 제품에서는 레지스터 정의, 전원 시퀀스, 오류 복구, locking, suspend/resume, ABI 설계를 더 추가해야 한다.
#include <linux/clk.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/regulator/consumer.h>
struct my_sensor {
void __iomem *base;
struct clk *bus_clk;
struct regulator *vdd;
int irq;
};
static irqreturn_t my_sensor_irq(int irq, void *data)
{
struct my_sensor *sensor = data;
u32 status = readl(sensor->base + 0x20);
/* acknowledge only the bits defined by the hardware manual */
writel(status, sensor->base + 0x24);
return IRQ_HANDLED;
}
static int my_sensor_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct my_sensor *sensor;
int ret;
sensor = devm_kzalloc(dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
sensor->base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(sensor->base))
return PTR_ERR(sensor->base);
sensor->bus_clk = devm_clk_get(dev, "bus");
if (IS_ERR(sensor->bus_clk))
return dev_err_probe(dev, PTR_ERR(sensor->bus_clk),
"failed to get bus clock\\n");
sensor->vdd = devm_regulator_get(dev, "vdd");
if (IS_ERR(sensor->vdd))
return dev_err_probe(dev, PTR_ERR(sensor->vdd),
"failed to get vdd\\n");
ret = regulator_enable(sensor->vdd);
if (ret)
return dev_err_probe(dev, ret, "failed to enable vdd\\n");
ret = clk_prepare_enable(sensor->bus_clk);
if (ret)
goto disable_vdd;
sensor->irq = platform_get_irq(pdev, 0);
if (sensor->irq < 0) {
ret = sensor->irq;
goto disable_clk;
}
ret = devm_request_irq(dev, sensor->irq, my_sensor_irq,
0, dev_name(dev), sensor);
if (ret)
goto disable_clk;
platform_set_drvdata(pdev, sensor);
writel(0x1, sensor->base + 0x00); /* enable, per the datasheet */
return 0;
disable_clk:
clk_disable_unprepare(sensor->bus_clk);
disable_vdd:
regulator_disable(sensor->vdd);
return ret;
}
static void my_sensor_remove(struct platform_device *pdev)
{
struct my_sensor *sensor = platform_get_drvdata(pdev);
writel(0x0, sensor->base + 0x00);
clk_disable_unprepare(sensor->bus_clk);
regulator_disable(sensor->vdd);
}
static const struct of_device_id my_sensor_of_match[] = {
{ .compatible = "example,my-sensor-v1" },
{ }
};
MODULE_DEVICE_TABLE(of, my_sensor_of_match);
static struct platform_driver my_sensor_driver = {
.probe = my_sensor_probe,
.remove = my_sensor_remove,
.driver = {
.name = "my-sensor",
.of_match_table = my_sensor_of_match,
},
};
module_platform_driver(my_sensor_driver);
MODULE_LICENSE("GPL");
이 코드는 그대로 복사해 제품에 넣는 코드가 아니다 / The skeleton is not production-ready
커널 버전에 따라 .remove의 반환형이나 clock/regulator API가 달라질 수 있다. 실제 트리의 header와 기존 드라이버 스타일을 기준으로 맞춰야 한다. 특히 IRQ status를 무조건 다시 쓰는 동작은 장치 데이터시트의 acknowledge 방식과 다를 수 있다.
7. 코드 한 덩어리씩 해석하기 / Reading the skeleton line by line
7-1. private data와 devm 자원 / Private state and device-managed resources
struct my_sensor는 이 장치 인스턴스 하나의 상태를 담는다. devm_kzalloc(), devm_platform_ioremap_resource(), devm_request_irq()처럼 devm_ 접두사가 붙은 함수는 장치가 제거될 때 커널이 자원을 자동으로 정리하도록 연결한다.
단, clk_prepare_enable()과 regulator_enable()은 자동으로 짝을 맞춰 주지 않으므로 성공한 경로와 실패 경로에서 직접 disable해야 한다. “devm이면 모든 cleanup이 자동”이라고 생각하면 안 된다.
7-2. MMIO 접근 / Accessing MMIO
devm_platform_ioremap_resource(pdev, 0)는 Device Tree의 첫 번째 reg 리소스를 매핑한다. 반환값은 일반 RAM 포인터가 아니라 I/O 메모리를 가리키는 __iomem 포인터다.
u32 status = readl(sensor->base + STATUS_OFFSET);
writel(value, sensor->base + CONTROL_OFFSET);
커널에서는 MMIO를 일반 포인터 역참조나 임의의 volatile 변수로 처리하지 않는다. readl()/writel() 같은 접근자가 아키텍처에 필요한 접근 의미와 순서를 표현한다. 레지스터 offset과 write-one-to-clear 같은 동작은 반드시 데이터시트 기준으로 작성한다.
7-3. Register bit, mask, RMW, W1C / Register bits, masks, RMW, and W1C
하드웨어 레지스터 하나에 여러 기능이 들어 있으므로, 기능 하나만 바꾸려면 bit mask를 사용한 **Read-Modify-Write(RMW)**가 필요하다. 예를 들어 START bit만 켜면서 IRQ_EN을 보존하려면 레지스터 전체에 1을 덮어쓰면 안 된다.
#define CTRL_START BIT(0)
#define CTRL_RESET BIT(1)
#define CTRL_IRQ_EN BIT(2)
u32 val = readl(sensor->base + CTRL_OFFSET);
val |= CTRL_START; /* only set START */
writel(val, sensor->base + CTRL_OFFSET);
반대로 모든 레지스터가 RMW에 안전한 것은 아니다. W1C(Write One to Clear) 레지스터는 “1을 써서 해당 상태 bit를 지운다”는 의미이므로, 읽은 값을 그대로 다시 쓰면 의도하지 않은 bit를 지울 수 있다. RO, WO, RW, W1C, W1S, RC 같은 접근 속성을 데이터시트에서 확인하고 각각의 acknowledge 방법을 따로 구현한다.
u32 irq = readl(sensor->base + IRQ_STATUS);
if (irq & IRQ_DONE)
writel(IRQ_DONE, sensor->base + IRQ_STATUS); /* W1C: write 1, not irq */
7-4. MMIO ordering과 DMA descriptor / MMIO ordering and DMA descriptors
NPU·Ethernet·스토리지 드라이버는 보통 일반 RAM에 descriptor를 채운 다음 MMIO doorbell을 써서 장치에 “읽어도 된다”고 알린다. 장치가 doorbell을 먼저 관찰하면 아직 완성되지 않은 descriptor를 읽을 수 있으므로, CPU·캐시·DMA 관점의 순서를 명시해야 한다.
desc->addr = dma_addr;
desc->len = length;
/* exact ordering depends on the DMA API and device protocol */
dma_wmb();
writel(QUEUE_START, sensor->base + DOORBELL_OFFSET);
readl()/writel()은 MMIO 접근을 표현하는 architecture-aware accessor다. writel_relaxed()는 더 약한 ordering을 의도적으로 사용하므로 성능만 보고 바꾸면 안 된다. wmb(), dma_wmb(), acquire/release 연산 중 무엇이 필요한지는 “누가 어떤 데이터를 언제 관찰해야 하는가”와 DMA API 문서를 기준으로 결정한다. Cache coherency가 있다고 해서 ordering 문제가 자동으로 해결되는 것은 아니다.
7-5. regmap: register 접근의 추상화 / regmap as a register-access abstraction
regmap은 register read/write와 bit update를 공통 API로 감싼다. MMIO뿐 아니라 SPI·I2C peripheral에서도 같은 드라이버 코드를 유지하기 쉽고, register cache·trace·lock 같은 공통 기능도 활용할 수 있다.
/* control path: change only selected bits */
regmap_update_bits(sensor->regmap,
CTRL_OFFSET,
CTRL_START | CTRL_IRQ_EN,
CTRL_START | CTRL_IRQ_EN);
/* fast path: a doorbell may remain direct MMIO */
writel(queue_id, sensor->base + DOORBELL_OFFSET);
regmap_update_bits(map, reg, mask, value)는 mask에 포함된 bit만 바꾼다. 그러나 W1C나 WO doorbell처럼 읽기 자체가 의미 없거나 RMW가 위험한 register에는 무작정 적용하지 않는다. 느린 설정·전원·reset 경로에는 regmap이 편리하고, 고성능 queue 제출 경로에는 직접 MMIO가 더 적합할 수 있다. 이것은 “어느 API가 더 좋은가”가 아니라 장치 프로토콜과 경로의 성격을 구분하는 문제다.
7-6. clock와 regulator / Clock and power
DTS의 clock-names = "bus"와 vdd-supply가 드라이버의 devm_clk_get(dev, "bus"), devm_regulator_get(dev, "vdd")와 연결된다. 문자열 하나가 다르면 -ENOENT 또는 공급자 준비 지연이 발생할 수 있다.
dev_err_probe()는 오류 코드와 장치 이름을 함께 기록하고 -EPROBE_DEFER 같은 재시도 가능한 상태를 지나치게 시끄럽게 출력하지 않도록 돕는다.
7-7. IRQ와 실행 맥락 / IRQ context
devm_request_irq()로 등록한 핸들러는 인터럽트 컨텍스트에서 실행될 수 있다. 이 안에서는 sleep할 수 없는 경우가 많으므로, 오래 걸리는 작업은 workqueue나 threaded IRQ로 넘긴다.
인터럽트 핸들러에서는 먼저 장치의 상태 레지스터를 읽어 정말 내 장치가 발생시킨 IRQ인지 확인하고, 필요한 acknowledge를 한 뒤 최소한의 일을 한다. 공유 큐를 만진다면 IRQ와 다른 CPU의 동시 접근을 고려해 spinlock 또는 적절한 lock을 사용한다.
8. 오류 경로가 실력을 만든다 / Error paths are part of the driver
드라이버는 성공 경로보다 실패 경로를 더 자주 만난다. 클록이 없거나 regulator가 아직 준비되지 않았거나, IRQ 번호가 잘못됐거나, Device Tree가 비활성화되어 있을 수 있다.
| 증상 / Symptom | 먼저 볼 것 / First checks |
|---|---|
| probe가 호출되지 않음 | status, compatible, driver가 빌드됐는지, match table이 등록됐는지 |
-EPROBE_DEFER | clock/regulator/GPIO/PHY 공급자가 나중에 준비되는지 |
-EINVAL 또는 MMIO fault | reg cell, bus address translation, resource size, 접근 폭 |
| IRQ가 계속 발생 | status clear/ack 방식, level·edge 설정, interrupt-parent |
| 장치가 켜지지 않음 | 전원 순서, reset, pinctrl, clock rate, enable bit |
로그는 “에러가 났다”보다 “어느 자원을 얻는 단계에서 어떤 errno가 났는가”를 남겨야 한다. dev_err_probe()와 장치 이름을 활용하면 여러 보드 장치를 동시에 볼 때도 추적하기 쉽다.
9. 드라이버가 사용자 공간과 만나는 방법 / Choosing a userspace interface
하드웨어를 초기화했다고 사용자 프로그램이 바로 접근할 수 있는 것은 아니다. 어떤 기능을 외부에 공개할지 인터페이스를 설계해야 한다.
기존 subsystem 사용 : GPIO, IIO, RTC, input, hwmon, MMC 등 이미 맞는 커널 subsystem이 있으면 그 모델에 맞추는 것이 우선이다.
character device : 장치 고유 명령과 read/write/ioctl이 필요할 때 사용한다. ioctl은 장기 ABI가 되므로 구조체 크기·호환성·권한을 신중하게 설계한다.
sysfs : 간단한 상태·설정 속성을 노출하는 표준 경로다. 바이너리 스트림이나 복잡한 명령 프로토콜을 억지로 넣는 곳은 아니다.
debugfs : 디버깅용이며 안정적인 제품 ABI로 약속하면 안 된다.
GPIO처럼 이미 표준 subsystem이 있는 기능을 임의의 character device로 다시 만들면 사용자 공간과 커널 생태계의 장점을 잃는다. 반대로 새 장치의 고유 기능을 subsystem에 억지로 끼워 넣어도 안 된다.
10. Device Tree와 드라이버를 함께 디버깅하기 / Debugging the pair
한쪽만 보면 원인을 놓친다. 아래 순서로 양쪽을 같이 확인한다.
- DTS 소스와 실제 부트된 DTB가 같은가?
status = "okay"인가? 상위 버스가 활성화되어 있는가?compatible문자열이 driver match table과 정확히 일치하는가?reg,interrupts,clocks,*-supply, pinctrl의 이름·순서·cell 형식이 binding과 맞는가?- 커널 설정에서 driver가
y또는 필요한m으로 들어갔는가? - probe 로그의 첫 실패 errno가 무엇인가?
- probe 이후 실제 레지스터와 핀 신호가 데이터시트 기대와 같은가?
# 실행 중인 보드에서 확인할 때의 예
ls /sys/bus/platform/devices
ls /sys/bus/platform/drivers/my-sensor
dmesg | grep -i -E "my-sensor|probe|defer|irq"
cat /proc/interrupts
# 빌드 트리에서 설정과 DT 검증을 확인할 때의 예
grep CONFIG_MY_SENSOR .config
make dtbs_check
make dt_binding_check
위 명령의 경로와 대상은 커널 버전·빌드 시스템에 따라 달라질 수 있다. 중요한 것은 “DTS를 고쳤다”에서 멈추지 않고, 부트된 DTB와 커널 로그가 실제로 바뀌었는지 확인하는 것이다.
11. 다음 단계: 더 큰 드라이버로 확장하기 / Where to go next
이 예제는 단순한 platform driver지만, 실제 BSP 장치에도 같은 질문이 반복된다.
- USB: regulator/VBUS, PHY, reset, role switch, hub 전원
- eMMC: bus width, pinctrl, clock, DMA, tuning, power sequence
- CAN/SPI/UART: subsystem API, FIFO, IRQ, DMA, locking
- NPU/PCIe: MMIO와 IRQ에 더해 DMA/IOMMU, firmware, power·thermal 관리
드라이버가 실행되는 CPU 쪽의 System Call, task, scheduler, IRQ context, spinlock, atomic, memory ordering을 함께 복습하려면 Linux Kernel 핵심 흐름 글을 이어서 읽으면 된다.
따라서 새로운 장치를 만났을 때 “어떤 함수부터 외울까?”보다 아래 순서를 먼저 세우면 좋다.
회로도 / 데이터시트
→ binding과 DTS
→ compatible matching
→ probe에서 자원 획득
→ register / IRQ 초기화
→ subsystem 또는 userspace ABI
→ 로그·파형·성능 측정으로 검증
핵심 정리 / Key takeaways
- Device Tree는 드라이버 코드와 보드별 하드웨어 차이를 분리하는 데이터다.
compatible매칭이 성공해야 platform driver의probe()가 특정 장치에 대해 실행된다.reg·IRQ·clock·regulator·GPIO는 DTS와 드라이버의 이름·cell·순서가 함께 맞아야 한다.- MMIO는
readl()/writel()로 접근하고, IRQ 컨텍스트에서는 sleep 가능 여부를 반드시 구분한다. - 좋은 드라이버는 성공 경로뿐 아니라
-EPROBE_DEFER, 전원 실패, IRQ 오류, remove 경로까지 설계한다.
공식 참고 자료 / Official references
- Linux and the Devicetree
- Writing Devicetree Bindings
- Linux platform devices and drivers
- Regmap API
- Dynamic DMA mapping Guide
- Why the volatile type class should not be used
#DeviceTree
#LinuxKernel
#PlatformDriver
#BSP
#DeviceDriver
#MMIO