읽는 방법
각 절에서 먼저 "하드웨어 관점에서 무엇이 연결되어 있는가"를 살펴보고, 이어서 DTS와 C 드라이버가 그 연결을 어떻게 표현하는지 확인합니다. 예제 주소와 IRQ 번호는 설명을 위한 값이며, 실제 보드의 데이터시트 값으로 바꿔야 합니다.
Device Tree를 단순한 설정 파일로 외우지 않고, 실제 드라이버가 하드웨어를 발견하고 자원을 준비하는 과정까지 연결해 보겠습니다. 예제는 메모리 맵 레지스터와 IRQ, 클록, 전원을 가진 가상의 보드 장치입니다.
1. 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(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로 표현하기
가상의 장치가 다음 자원을 가진다고 하겠습니다.
| 자원 | 예시 값 | 용도 |
|---|---|---|
| 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를 단순히 두 숫자로 외우지 마십시오
부모가 64비트 주소를 요구하면 reg = <0x0 0x40000000 0x0 0x1000>;처럼 네 셀이 될 수 있습니다. 어떤 형식이 맞는지는 해당 버스의 binding과 상위 노드를 확인해야 합니다.
4. compatible가 드라이버를 찾는 과정
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. 드라이버의 기본 생명주기
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. 실제 플랫폼 드라이버 뼈대
아래 코드는 설명을 위한 최소 뼈대입니다. 실제 제품에서는 레지스터 정의, 전원 시퀀스, 오류 복구, 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");
이 코드는 그대로 복사해 제품에 넣는 코드가 아닙니다
커널 버전에 따라 .remove의 반환형이나 clock/regulator API가 달라질 수 있습니다. 실제 트리의 header와 기존 드라이버 스타일을 기준으로 맞춰야 합니다. 특히 IRQ status를 무조건 다시 쓰는 동작은 장치 데이터시트의 acknowledge 방식과 다를 수 있습니다.
7. 코드 한 덩어리씩 해석하기
7-1. private data와 devm 자원
struct my_sensor는 이 장치 인스턴스 하나의 상태를 담습니다. devm_kzalloc(), devm_platform_ioremap_resource(), devm_request_irq()처럼 devm_ 접두사가 붙은 함수는 장치가 제거될 때 커널이 자원을 자동으로 정리하도록 연결합니다.
단, clk_prepare_enable()과 regulator_enable()은 자동으로 짝을 맞춰 주지 않으므로 성공한 경로와 실패 경로에서 직접 disable해야 합니다. "devm이면 모든 cleanup이 자동"이라고 생각하면 안 됩니다.
7-2. 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
하드웨어 레지스터 하나에 여러 기능이 들어 있으므로, 기능 하나만 바꾸려면 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
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은 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
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와 실행 맥락
devm_request_irq()로 등록한 핸들러는 인터럽트 컨텍스트에서 실행될 수 있습니다. 이 안에서는 sleep할 수 없는 경우가 많으므로, 오래 걸리는 작업은 workqueue나 threaded IRQ로 넘깁니다.
인터럽트 핸들러에서는 먼저 장치의 상태 레지스터를 읽어 정말 내 장치가 발생시킨 IRQ인지 확인하고, 필요한 acknowledge를 한 뒤 최소한의 일을 합니다. 공유 큐를 만진다면 IRQ와 다른 CPU의 동시 접근을 고려해 spinlock 또는 적절한 lock을 사용합니다.
8. 오류 경로가 실력을 만든다
드라이버는 성공 경로보다 실패 경로를 더 자주 만납니다. 클록이 없거나 regulator가 아직 준비되지 않았거나, IRQ 번호가 잘못됐거나, Device Tree가 비활성화되어 있을 수 있습니다.
| 증상 | 먼저 볼 것 |
|---|---|
| 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. 드라이버가 사용자 공간과 만나는 방법
하드웨어를 초기화했다고 사용자 프로그램이 바로 접근할 수 있는 것은 아닙니다. 어떤 기능을 외부에 공개할지 인터페이스를 설계해야 합니다.
- 기존 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와 드라이버를 함께 디버깅하기
한쪽만 보면 원인을 놓칩니다. 아래 순서로 양쪽을 같이 확인합니다.
- 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. 다음 단계: 더 큰 드라이버로 확장하기
이 예제는 단순한 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
→ 로그·파형·성능 측정으로 검증
핵심 정리
- Device Tree는 드라이버 코드와 보드별 하드웨어 차이를 분리하는 데이터입니다.
compatible매칭이 성공해야 platform driver의probe()가 특정 장치에 대해 실행됩니다.reg·IRQ·clock·regulator·GPIO는 DTS와 드라이버의 이름·cell·순서가 함께 맞아야 합니다.- MMIO는
readl()/writel()로 접근하고, IRQ 컨텍스트에서는 sleep 가능 여부를 반드시 구분합니다. - 좋은 드라이버는 성공 경로뿐 아니라
-EPROBE_DEFER, 전원 실패, IRQ 오류, remove 경로까지 설계합니다.
공식 참고 자료
How to read this article
Each section opens with the hardware relationship, then shows how the DTS and C driver express it. The addresses and IRQ numbers used here are illustrative; replace them with values from your board's actual datasheet.
Rather than treating Device Tree as a configuration file to memorize, this article connects it to the real driver lifecycle: discovery, resource acquisition, and hardware access. The example device has a memory-mapped register bank, an IRQ, a clock, and a power supply.
1. Why Do We Need Device Tree?
Imagine a kernel driver that hard-codes board-specific values like this:
/* 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
If the driver hard-codes addresses and IRQs, every board variation requires a source change. That increases maintenance cost and tightly couples the kernel to one specific board.
Device Tree moves those board differences into data. The driver declares what it supports; the DTS describes where the device is and which IRQ, clock, and power resources it uses.
Hardware design
├─ register base / size
├─ interrupt line
├─ clock source
└─ power rail
↓ Device Tree
platform device
↓ compatible matching
platform driver
↓ probe()
Real device initialization
2. DTS, DTB, and Bindings
- DTS (Device Tree Source)
- Human-readable source text. It is commonly split into a reusable SoC description (
.dtsi) and a board-specific overlay (.dts). - DTB (Device Tree Blob)
- The binary produced by
dtcfrom the DTS. The bootloader hands it to the kernel, which unpacks it into an in-memory structure early in boot. - Binding
- The contract that defines the meaning of node properties: which
compatiblestring to use, and how to formatreg,clocks,interrupts, and other properties.
Modern kernels manage bindings as YAML schemas and can validate DTS files with dtbs_check. A DTS that compiles is not necessarily a DTS that conforms to the binding.
3. Describing the Hardware in DTS
Suppose our fictional device has the following resources:
| Resource | Example value | Purpose |
|---|---|---|
| MMIO register | base 0x40000000, size 0x1000 |
Control and status registers |
| IRQ | 42 | Notify the CPU of a device event |
| clock | &clk 3 |
Supply the operating clock |
| power | ®_3v3 |
Control the power rail |
mydev: sensor@40000000 {
compatible = "example,my-sensor-v1";
reg = <0x40000000 0x1000>;
interrupts = <42>;
clocks = <&clk 3>;
clock-names = "bus";
vdd-supply = <®_3v3>;
status = "okay";
};
The @40000000 suffix is the unit address and normally matches the first address in reg. The number of address and size cells is determined by the parent bus's #address-cells and #size-cells properties.
Do not memorize reg as always two numbers
A 64-bit parent bus may require four cells: reg = <0x0 0x40000000 0x0 0x1000>;. The binding and the parent node define the correct format for your specific bus.
4. How compatible Finds a Driver
A Device Tree node does not automatically cause probe() to be called. Two lists must agree:
DTS node:
compatible = "example,my-sensor-v1";
driver match table:
{ .compatible = "example,my-sensor-v1" }
match → platform device + platform driver linked → probe(pdev)
A platform device represents a processor-attached, memory-mapped device in the Linux device model. A platform driver controls it. Unlike PCI, where the bus enumerates devices automatically, platform devices rely on firmware—Device Tree or ACPI—to supply device descriptions.
Keep compatible strings specific. A newer device may list a fallback string for an older implementation, but a broad family name should never be used as a wildcard.
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() prepares one concrete device instance—it is not a generic "driver loaded" notification. If a resource acquisition fails, already-obtained resources must be released. If a supplier is not yet available, returning -EPROBE_DEFER asks the core to retry later.
6. A Practical Platform-Driver Skeleton
The following is a teaching skeleton, not production-ready code. A real driver also needs register definitions, power sequencing, error recovery, locking, suspend/resume, and a carefully designed userspace 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");
This skeleton is not production-ready
API details such as the .remove return type and clock/regulator APIs can vary by kernel version. Always follow the headers and existing drivers in the target tree. Never assume that writing back the IRQ status register value is the correct acknowledge operation for real hardware.
7. Reading the Skeleton Line by Line
7-1. Private State and Device-Managed Resources
struct my_sensor holds the state for one device instance. Functions prefixed with devm_—such as devm_kzalloc(), devm_platform_ioremap_resource(), and devm_request_irq()—tie allocations, mappings, and IRQ registrations to the device lifetime so they are released automatically when the device is removed.
Clock and regulator enable operations are not covered by this mechanism, however. clk_prepare_enable() and regulator_enable() require explicit matching disable calls on both the success path (in remove()) and every failure path in probe(). Do not assume that device-managed allocation means every state transition is automatically undone.
7-2. Accessing MMIO
devm_platform_ioremap_resource(pdev, 0) maps the first reg resource from Device Tree. The returned value is a __iomem pointer—not an ordinary RAM pointer.
u32 status = readl(sensor->base + STATUS_OFFSET);
writel(value, sensor->base + CONTROL_OFFSET);
Do not access MMIO by dereferencing a plain pointer or using an arbitrary volatile variable. Accessors such as readl() and writel() carry the architecture-specific access semantics and ordering required for I/O memory. Register offsets and special behaviors such as write-one-to-clear must come directly from the datasheet.
7-3. Register Bits, Masks, RMW, and W1C
Hardware registers often pack multiple controls into a single value. To change one field without disturbing others, use a masked Read-Modify-Write (RMW) sequence. Writing the literal value 1 to a control register, for example, could silently clear or alter unrelated bits.
#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);
Not every register is safe for RMW, however. A W1C (Write One to Clear) status register clears a bit when a one is written to it; writing back a value you just read can inadvertently clear unrelated events. Check access attributes—RO, WO, RW, W1C, W1S, RC—in the datasheet and implement each acknowledge operation accordingly.
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 and DMA Descriptors
NPU, Ethernet, and storage drivers commonly fill a descriptor in normal RAM, then ring an MMIO doorbell to tell the device the descriptor is ready. If the device observes the doorbell before the descriptor write has propagated, it may fetch an incomplete or stale descriptor. CPU, cache, and DMA ordering must therefore be made explicit.
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() are architecture-aware MMIO accessors. writel_relaxed() intentionally provides weaker ordering; do not choose it purely for performance. Deciding among wmb(), dma_wmb(), or acquire/release semantics requires understanding who must observe which data and consulting the DMA API documentation. Cache coherency alone does not guarantee the ordering you need.
7-5. regmap as a Register-Access Abstraction
regmap wraps register reads, writes, and bit-field updates in a common API. It makes it easier to share driver logic across MMIO, SPI, and I2C peripherals, and provides built-in facilities such as register caching, tracing, and locking.
/* 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) changes only bits covered by the mask. Do not apply it to W1C or write-only doorbell registers where a read or RMW would be unsafe or meaningless. Regmap is convenient for slow configuration, power, and reset paths; direct MMIO may be more appropriate on a performance-critical queue submission path. The choice follows the device protocol and path characteristics, not a universal rule about which API is "better."
7-6. Clock and Power
The DTS property clock-names = "bus" connects to devm_clk_get(dev, "bus") in the driver, and vdd-supply connects to devm_regulator_get(dev, "vdd"). A single-character name mismatch can produce -ENOENT or a deferred probe.
dev_err_probe() logs the device name and error code together, and avoids excessive noise for retryable states such as -EPROBE_DEFER.
7-7. IRQ Context
A handler registered with devm_request_irq() may run in interrupt context, where sleeping is generally not allowed. Move any lengthy processing to a workqueue or use a threaded IRQ.
The handler should read the device's status register to confirm the IRQ came from this device, acknowledge the interrupt according to the hardware manual, and do the minimum necessary work. When accessing a shared queue from both interrupt and process context, use a spinlock or another appropriate locking primitive to guard against concurrent access from multiple CPUs.
8. Error Paths Are Part of the Driver
Drivers encounter failure paths frequently: a clock may be missing, a regulator may not yet be ready, an IRQ number may be invalid, or the Device Tree node may be disabled.
| Symptom | First checks |
|---|---|
| probe is never called | status, compatible, build configuration, and match-table registration |
-EPROBE_DEFER |
Whether a clock, regulator, GPIO, or PHY supplier will become available later |
-EINVAL or MMIO fault |
reg cells, bus address translation, resource size, and access width |
| IRQ fires continuously | Status clear/ack method, level vs. edge configuration, and interrupt-parent |
| Device does not power up | Power sequence, reset, pinctrl, clock rate, and enable bit |
Useful logs identify the specific resource and errno, not merely "an error occurred." Device-aware logging with dev_err_probe() makes it much easier to trace failures when multiple board devices are probing simultaneously.
9. Choosing a Userspace Interface
Initializing hardware does not automatically give applications an interface. The driver must deliberately expose the required functionality.
- Use an existing subsystem
- Prefer an established kernel subsystem—GPIO, IIO, RTC, input, hwmon, MMC—when one fits the device's function.
- Character device
- Use a character device when the hardware requires device-specific read/write/ioctl operations. Design the ioctl ABI carefully: structure sizes, compatibility, and permissions become long-term commitments.
- sysfs
- The standard path for small, textual state and configuration attributes. It is not appropriate for binary streams or complex command protocols.
- debugfs
- Intended for debugging and development. Do not expose it as a stable product ABI.
Reimplementing GPIO-like functionality as a private character device wastes the benefits of the kernel's established subsystems and userspace ecosystem. Equally, genuinely device-specific behavior should not be forced into an unrelated subsystem just to avoid writing a new interface.
10. Debugging the DTS and Driver Together
Looking at only one side often hides the root cause. Work through both sides in order:
- Does the booted DTB match the DTS source that was edited?
- Is
status = "okay"set? Is the parent bus node also enabled? - Does the
compatiblestring exactly match an entry in the driver's match table? - Do the names, cell counts, and ordering for
reg,interrupts,clocks,*-supply, and pinctrl all conform to the binding? - Is the driver built into the kernel (
y) or as a loadable module (m) in the kernel configuration? - What is the first errno logged during probe?
- After a successful probe, do the actual register values and pin signals match datasheet expectations?
# Commands to run on a live board
ls /sys/bus/platform/devices
ls /sys/bus/platform/drivers/my-sensor
dmesg | grep -i -E "my-sensor|probe|defer|irq"
cat /proc/interrupts
# Build-tree checks
grep CONFIG_MY_SENSOR .config
make dtbs_check
make dt_binding_check
Exact paths and build targets vary by kernel version and build system. The key discipline is not stopping at "I edited the DTS," but verifying that the booted DTB and kernel logs actually reflect the change.
11. Where to Go Next
This example is a simple platform driver, but the same questions surface in every real BSP device:
- USB: regulator/VBUS, PHY, reset, role switch, hub power
- eMMC: bus width, pinctrl, clock, DMA, tuning, power sequence
- CAN/SPI/UART: subsystem API, FIFO, IRQ, DMA, locking
- NPU/PCIe: MMIO and IRQ plus DMA/IOMMU, firmware, power and thermal management
For the CPU-side foundations—system calls, tasks, scheduling, IRQ context, spinlocks, atomics, and memory ordering—continue with the Linux Kernel Foundations article.
When you encounter a new device, rather than asking "which function should I memorize first?", build the picture in this order:
Schematic / datasheet
→ binding and DTS
→ compatible matching
→ resource acquisition in probe()
→ register / IRQ initialization
→ subsystem or userspace ABI
→ verify with logs, signals, and performance measurement
Key Takeaways
- Device Tree separates board-specific hardware data from driver code.
- A successful
compatiblematch is what triggers a platform driver'sprobe()for a specific device. - The names, cell counts, and ordering of
reg, IRQ, clock, regulator, and GPIO must agree on both the DTS side and the driver side. - Access MMIO with
readl()/writel(), and always distinguish between sleepable and non-sleepable contexts in IRQ handlers. - A good driver plans for
-EPROBE_DEFER, power failures, IRQ errors, and the remove path—not just the success path.
Official References
'임베디드' 카테고리의 다른 글
| Kconfig, 커널 옵션을 메뉴에서 고르는 법 (0) | 2026.09.16 |
|---|---|
| UUU 명령 하나로 eMMC 파형과 드라이버 코드까지 연결해 보기 (0) | 2026.09.14 |
| Linux Kernel 핵심 흐름: System Call부터 동기화까지 (0) | 2026.09.12 |
| Device Tree와 Linux 플랫폼 드라이버 작성 입문 (0) | 2026.09.11 |
| UUU 명령 하나로 eMMC 파형과 드라이버 코드까지 따라가기 (0) | 2026.09.11 |
