Tüm eğitimler
REHBer GÖMÜLÜ LİNUX KERNEL DEVICE TREE

Linux Platform Driver —
Device Tree'den probe'a.

compatible string eşleşmesinden devm_ kaynak yönetimine — tam bir peripheral driver adım adım.

00 Platform bus nedir

Platform bus, keşfedilemeyen (non-discoverable) memory-mapped peripheral'lar için Linux driver modelinin sunduğu sanal bus'tur.

Linux driver modeli, her donanım bileşenini bir bus üzerinden yönetir. I2C, SPI veya USB gibi bus'lar kendi protokolleriyle cihazları otomatik keşfedebilir. Ancak SoC içindeki UART, GPIO, timer gibi memory-mapped peripheral'lar keşfedilemez — bunların tanımlanması gerekir. İşte platform bus bu boşluğu doldurur: Device Tree üzerinden tanımlanan peripheral'lar için platform_device nesneleri oluşturulur ve bu nesneler platform_driver nesneleriyle eşleştirilir.

  Device Tree (.dts)
  ┌──────────────────────────────────────┐
  │  mydev@10000000 {                    │
  │    compatible = "myvendor,mydev";    │
  │    reg = <0x10000000 0x1000>;        │
  │    interrupts = <0 42 4>;            │
  │  };                                  │
  └──────────────┬───────────────────────┘
                 │  of_platform_populate()
                 ▼
  platform_device  ←── platform bus ──→  platform_driver
  (kernel oluşturur)                      (modül yükler)
                 │  compatible string eşleşirse
                 ▼
             probe() çağrılır
    

Bus tipleri karşılaştırması

Bus tipiStructKeşfedilebilir?Tipik kullanım
platformplatform_driverHayır (DT/ACPI)SoC peripheral: UART, GPIO, timer, SPI controller
i2ci2c_driverKısmi (DT)I2C slave: sensör, EEPROM, RTC
spispi_driverKısmi (DT)SPI slave: flash, ADC, display
pcipci_driverEvetPCIe kartlar: NIC, GPU, NVMe
usbusb_driverEvetUSB aygıtlar: kamera, HID, depolama
platform_driver_register()Driver'ı platform bus'a kaydet; mevcut device'larla hemen eşleştirme dener
platform_driver_unregister()Driver'ı bus'tan çıkar; eşleşmiş device'lar için remove() çağrılır
module_platform_driver()register + unregister için tek satır makro — module_init/exit yerine kullan
NOT

Platform bus'ta device → driver eşleşmesi compatible string üzerinden yapılır. DTS'teki compatible = "myvendor,mydev" değeri, driver'ın of_match_table dizisindeki bir .compatible alanıyla örtüşmezse probe() hiç çağrılmaz.

Bu bölümde

  • Platform bus: non-discoverable memory-mapped peripheral'lar için sanal bus
  • of_platform_populate(): DT node → platform_device otomasyonu
  • platform_device + platform_driver çifti; compatible string eşleşmesi
  • I2C/SPI/PCI/USB bus'larıyla karşılaştırma — hangi senaryoda hangisi
  • module_platform_driver() — tek satır register/unregister makrosu

01 Driver kaydı ve of_match_table

platform_driver struct'ının anatomisi ve Device Tree compatible string eşleştirme mekanizması.

Bir platform driver'ın kalbi struct platform_driver'dır. Bu struct içindeki .driver.of_match_table alanı, hangi Device Tree node'larıyla eşleşileceğini belirler. Tablo NULL sentinel ile sonlandırılmalıdır.

mydev_driver.c — skeleton
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_device.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Emirhan Pehlevan");
MODULE_DESCRIPTION("Platform driver skeleton");

/*
 * of_device_id tablosu: DT'deki compatible degerleriyle eslesir
 * Son satir bos olmali (sentinel)
 */
static const struct of_device_id mydev_of_match[] = {
    { .compatible = "myvendor,mydev-v1" },
    { .compatible = "myvendor,mydev-v2", .data = (void *)2 },
    { } /* sentinel */
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

static int  mydev_probe(struct platform_device *pdev);
static int  mydev_remove(struct platform_device *pdev);

static struct platform_driver mydev_driver = {
    .probe  = mydev_probe,
    .remove = mydev_remove,
    .driver = {
        .name           = "mydev",
        .of_match_table = mydev_of_match,
        .owner          = THIS_MODULE,
    },
};

static int mydev_probe(struct platform_device *pdev)
{
    const void *match_data = of_device_get_match_data(&pdev->dev);
    int version = match_data ? (int)(uintptr_t)match_data : 1;
    dev_info(&pdev->dev, "probe çağrıldı, hw versiyon=%d\n", version);
    return 0;
}

static int mydev_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "remove çağrıldı\n");
    return 0;
}

/*
 * module_platform_driver():
 *   module_init → platform_driver_register(&mydev_driver)
 *   module_exit → platform_driver_unregister(&mydev_driver)
 */
module_platform_driver(mydev_driver);

of_match_table ile birden fazla chip versiyonu

Aynı driver farklı chip revizyonlarını destekleyebilir. .data alanına versiyon numarası veya konfigürasyon struct pointer'ı konulur:

multi_compatible.c
struct mydev_config {
    unsigned int  fifo_depth;
    bool          has_dma;
};

static const struct mydev_config cfg_v1 = { .fifo_depth = 16,  .has_dma = false };
static const struct mydev_config cfg_v2 = { .fifo_depth = 128, .has_dma = true  };

static const struct of_device_id mydev_of_match[] = {
    { .compatible = "myvendor,mydev-v1", .data = &cfg_v1 },
    { .compatible = "myvendor,mydev-v2", .data = &cfg_v2 },
    { }
};
MODULE_DEVICE_TABLE(of, mydev_of_match);

static int mydev_probe(struct platform_device *pdev)
{
    const struct mydev_config *cfg =
        of_device_get_match_data(&pdev->dev);

    dev_info(&pdev->dev, "FIFO depth=%u, DMA=%s\n",
             cfg->fifo_depth, cfg->has_dma ? "evet" : "hayir");
    return 0;
}

MODULE_DEVICE_TABLE ve modprobe autoloading

terminal
# depmod çalıştır → modules.alias güncelle
$ sudo depmod -a

# alias içeriğini kontrol et
$ grep myvendor /lib/modules/$(uname -r)/modules.alias
alias of:N*T*Cmyvendor,mydev-v1* mydev_driver
alias of:N*T*Cmyvendor,mydev-v2* mydev_driver

# Driver dosyasını kopyala ve modprobe ile yükle
$ sudo cp mydev_driver.ko /lib/modules/$(uname -r)/extra/
$ sudo modprobe mydev_driver

Bu bölümde

  • struct platform_driver: .probe, .remove, .driver.name, .driver.of_match_table
  • of_device_id tablosu: compatible string eşleştirme; .data ile versiyon bilgisi
  • of_device_get_match_data() — eşleşen entry'nin .data pointer'ını al
  • MODULE_DEVICE_TABLE(of, ...) — modprobe autoloading için modalias kaydı
  • module_platform_driver() — tek satır register/unregister makrosu

02 probe() fonksiyonu

probe(), platform driver'ın kalbidir; device eşleşince çağrılır ve tüm kaynak tahsisi, başlatma ve kayıt işlemleri burada yapılır.

probe() başarıyla tamamlandığında device kullanıma hazırdır. Herhangi bir adım başarısız olursa negatif errno dönülür; kernel bu durumda devm_ cleanup mekanizmasıyla tahsis edilen kaynakları otomatik temizler.

full_probe.c
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/io.h>
#include <linux/interrupt.h>
#include <linux/slab.h>

struct mydev_priv {
    void __iomem         *base;
    int                   irq;
    u32                   reg_shift;
    const char           *label;
};

static irqreturn_t mydev_irq(int irq, void *data)
{
    return IRQ_HANDLED;
}

static int mydev_probe(struct platform_device *pdev)
{
    struct mydev_priv *priv;
    struct resource   *res;
    int                ret;

    /* 1. Private struct — device kaldırılınca otomatik kfree */
    priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv)
        return -ENOMEM;

    /* 2. DT'den opsiyonel property'leri oku */
    of_property_read_u32(pdev->dev.of_node, "reg-shift", &priv->reg_shift);
    of_property_read_string(pdev->dev.of_node, "label", &priv->label);
    if (!priv->label)
        priv->label = "mydev";

    /* 3. Register base address al ve ioremap et */
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return dev_err_probe(&pdev->dev, -ENODEV,
                              "MEM resource bulunamadi\n");

    priv->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->base))
        return PTR_ERR(priv->base);

    dev_info(&pdev->dev, "registers: 0x%08llx mapped @ %p\n",
             (unsigned long long)res->start, priv->base);

    /* 4. IRQ numarası al */
    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0)
        return priv->irq;

    /* 5. IRQ kaydet — device kaldırılınca otomatik free_irq */
    ret = devm_request_irq(&pdev->dev, priv->irq, mydev_irq,
                            IRQF_TRIGGER_RISING, pdev->name, priv);
    if (ret)
        return dev_err_probe(&pdev->dev, ret,
                              "IRQ %d kaydedilemedi\n", priv->irq);

    /* 6. Private data'yı device'a bağla */
    platform_set_drvdata(pdev, priv);

    dev_info(&pdev->dev, "'%s' probe basarili, IRQ=%d\n",
             priv->label, priv->irq);
    return 0;
}
platform_get_resource()IORESOURCE_MEM veya IORESOURCE_IO tipinde resource al; index 0'dan başlar
devm_ioremap_resource()resource_request + ioremap tek adımda; device kaldırılınca otomatik iounmap
platform_get_irq()DT'deki interrupts property'sinden IRQ numarasını al; hata durumunda negatif errno
dev_err_probe()Hata mesajı yaz ve errno döndür; EPROBE_DEFER durumunda sessiz kalır — otomatik retry
platform_set_drvdata()Private struct pointer'ını device'a bağla; platform_get_drvdata() ile diğer callback'lerde al
DİKKAT

-EPROBE_DEFER dönen bir kaynak, henüz hazır olmayan başka bir driver tarafından sağlanıyor demektir. Kernel bu durumda probe'u daha sonra yeniden dener. dev_err_probe() bu errno için mesaj basmaz — log'u gereksiz gürültüden korur.

Bu bölümde

  • probe() imzası: int probe(struct platform_device *pdev)
  • devm_kzalloc → platform_get_resource → devm_ioremap_resource → platform_get_irq sırası
  • of_property_read_u32() / of_property_read_string() ile DT property okuma
  • dev_err_probe() — hata loglama + EPROBE_DEFER farkındalığı
  • platform_set_drvdata() ile private struct'ın driver genelinde taşınması

03 remove() ve cleanup

devm_ kullanan modern driver'larda remove() genellikle boş ya da minimal; devm_ olmayan durumlarda ters sıralı manuel temizleme gerekir.

remove(), şu durumlarda çağrılır: modül kaldırma (rmmod), device tree overlay kaldırma, veya /sys/bus/platform/drivers/mydev/unbind yazma. devm_ kullanılmışsa kernel tüm registered cleanup fonksiyonlarını ters sırada çağırır.

remove_examples.c
/* ─── devm_ kullanan driver: remove neredeyse boş ─── */
static int mydev_remove_devm(struct platform_device *pdev)
{
    /* devm_kzalloc, devm_ioremap_resource, devm_request_irq
     * hepsi kernel tarafından otomatik temizlenir.
     * Sadece devm_ ile yönetilemeyen şeyleri burada temizle. */
    dev_info(&pdev->dev, "kaldirildi\n");
    return 0;
}

/* ─── Manuel cleanup gereken driver ─── */
struct manual_priv {
    void __iomem      *base;
    int                irq;
    struct clk        *clk;
    struct cdev        cdev;
    dev_t              devno;
    struct class      *cls;
};

static int mydev_remove_manual(struct platform_device *pdev)
{
    struct manual_priv *priv = platform_get_drvdata(pdev);

    /* Temizleme sırası: yüklemenin TERSİ */
    device_destroy(priv->cls, priv->devno);
    class_destroy(priv->cls);
    cdev_del(&priv->cdev);
    unregister_chrdev_region(priv->devno, 1);
    free_irq(priv->irq, priv);
    clk_disable_unprepare(priv->clk);
    clk_put(priv->clk);
    iounmap(priv->base);
    kfree(priv);
    return 0;
}

Driver unbind/bind testi

terminal
# Device adını öğren
$ ls /sys/bus/platform/devices/ | grep mydev
10000000.mydev

# Manuel unbind → remove() çağrılır
$ echo "10000000.mydev" | sudo tee /sys/bus/platform/drivers/mydev/unbind

# Manuel bind → probe() çağrılır
$ echo "10000000.mydev" | sudo tee /sys/bus/platform/drivers/mydev/bind
UYARI

devm_ kullandıysan remove()'da o kaynağı manuel temizleme — çift temizleme (double-free) veya use-after-free hatası oluşturur. devm_ ile yönetilen kaynaklar yalnızca kernel tarafından serbest bırakılır.

Bu bölümde

  • devm_ kullanan driver'da remove() boş veya minimal — kernel otomatik temizler
  • Manuel cleanup gerektiğinde ters sıra: device → irq → clk → ioremap → kfree
  • platform_get_drvdata() ile remove'da private struct'a erişim
  • /sys/bus/platform/drivers/mydev/unbind — bind/unbind ile probe/remove test

04 Device tree'den veri okuma

Device Tree property'lerini driver içinde okumak için of_ API'si kullanılır; her property tipinin kendine özgü okuma fonksiyonu vardır.

Device Tree node'una pdev->dev.of_node üzerinden erişilir. Tüm of_property_read_* fonksiyonları property varsa 0, yoksa negatif errno döner. Opsiyonel property'ler için dönüş değeri kontrol edilmeli; yoksa varsayılan değer kullanılmalıdır.

dt_read.c — probe() içi DT okuma örnekleri
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>

static int read_dt_properties(struct platform_device *pdev)
{
    struct device_node *np = pdev->dev.of_node;
    u32         val32;
    u64         val64;
    const char *str;
    int         gpio, irq, ret;

    if (!np) {
        dev_err(&pdev->dev, "device tree node yok\n");
        return -ENODEV;
    }

    /* u32 property oku */
    ret = of_property_read_u32(np, "reg-shift", &val32);
    if (!ret)
        dev_info(&pdev->dev, "reg-shift = %u\n", val32);

    /* u64 property oku */
    ret = of_property_read_u64(np, "clock-frequency", &val64);
    if (!ret)
        dev_info(&pdev->dev, "clock-freq = %llu Hz\n", val64);

    /* string property oku */
    ret = of_property_read_string(np, "label", &str);
    if (!ret)
        dev_info(&pdev->dev, "label = %s\n", str);

    /* boolean property — sadece varlığını kontrol et */
    if (of_property_read_bool(np, "hw-flow-control"))
        dev_info(&pdev->dev, "HW flow control aktif\n");

    /* GPIO numarası al (legacy binding) */
    gpio = of_get_named_gpio(np, "reset-gpios", 0);
    if (gpio_is_valid(gpio))
        dev_info(&pdev->dev, "reset GPIO = %d\n", gpio);

    /* IRQ numarası DT'den al */
    irq = of_irq_get(np, 0);
    if (irq > 0)
        dev_info(&pdev->dev, "DT IRQ = %d\n", irq);

    return 0;
}

DTS örneği

board.dts
/ {
    mydev: mydev@10000000 {
        compatible      = "myvendor,mydev-v2";
        reg             = <0x10000000 0x1000>;
        interrupts      = <GIC_SPI 42 IRQ_TYPE_EDGE_RISING>;
        clocks          = <&clk_apb>;
        clock-names     = "apb_pclk";
        reset-gpios     = <&gpio1 5 GPIO_ACTIVE_LOW>;
        reg-shift       = <2>;
        clock-frequency = <48000000>;
        label           = "mydev0";
        hw-flow-control; /* boolean — değersiz */
        status          = "okay";
    };
};

DT property → driver struct field eşlemesi

DT propertyof_ APIDriver struct field
regplatform_get_resource(IORESOURCE_MEM)void __iomem *base
interruptsplatform_get_irq(pdev, 0)int irq
clocks / clock-namesdevm_clk_get(&dev, "apb_pclk")struct clk *clk
reset-gpiosdevm_gpiod_get(&dev, "reset", GPIOD_OUT_LOW)struct gpio_desc *reset
reg-shiftof_property_read_u32(np, "reg-shift", &v)u32 reg_shift
labelof_property_read_string(np, "label", &s)const char *label
hw-flow-controlof_property_read_bool(np, "hw-flow-control")bool hw_flow

Bu bölümde

  • pdev->dev.of_node — DT node erişim noktası; !np kontrolü yap
  • of_property_read_u32/u64/string() — tipli property okuma, ret kontrolü
  • of_property_read_bool() — boolean/flag property varlık kontrolü
  • of_get_named_gpio() — GPIO numarası; of_irq_get() — IRQ numarası
  • DT property → of_ API → driver struct field mapping tablosu

05 GPIO ile LED driver

GPIO descriptor API'si ve sysfs attribute birleştirilerek DTS'ten yapılandırılan tam çalışan bir LED platform driver örneği.

led_platform.dts
/ {
    my-leds {
        compatible = "myvendor,my-leds";
        gpios      = <&gpio 17 GPIO_ACTIVE_LOW>;
        label      = "status-led";
        status     = "okay";
    };
};
led_platform.c
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/sysfs.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Emirhan Pehlevan");

struct led_priv {
    struct gpio_desc *gpiod;
    const char       *label;
    int               brightness;
};

static ssize_t brightness_show(struct device *dev,
                                struct device_attribute *attr, char *buf)
{
    struct led_priv *priv = dev_get_drvdata(dev);
    return sysfs_emit(buf, "%d\n", priv->brightness);
}

static ssize_t brightness_store(struct device *dev,
                                 struct device_attribute *attr,
                                 const char *buf, size_t count)
{
    struct led_priv *priv = dev_get_drvdata(dev);
    int val, ret;

    ret = kstrtoint(buf, 10, &val);
    if (ret)
        return ret;

    priv->brightness = !!val;
    /* GPIO_ACTIVE_LOW mantığı gpiod API tarafından uygulanır */
    gpiod_set_value(priv->gpiod, priv->brightness);
    dev_info(dev, "LED '%s' = %s\n", priv->label,
             priv->brightness ? "ACIK" : "KAPALI");
    return count;
}

static DEVICE_ATTR_RW(brightness);

static struct attribute *led_attrs[] = {
    &dev_attr_brightness.attr,
    NULL
};
static const struct attribute_group led_group = { .attrs = led_attrs };

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

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

    /*
     * GPIO descriptor al — DTS'teki gpios property'sinden
     * NULL: "gpios" ozelligi (isimsiz)
     * GPIOD_OUT_LOW: cikis olarak yapilandir, baslangic LOW
     */
    priv->gpiod = devm_gpiod_get(&pdev->dev, NULL, GPIOD_OUT_LOW);
    if (IS_ERR(priv->gpiod))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->gpiod),
                              "GPIO alinamadi\n");

    of_property_read_string(pdev->dev.of_node, "label", &priv->label);
    if (!priv->label)
        priv->label = pdev->name;

    platform_set_drvdata(pdev, priv);

    /* sysfs attribute grubunu ekle — device kaldırılınca otomatik çıkar */
    ret = devm_device_add_group(&pdev->dev, &led_group);
    if (ret)
        return ret;

    dev_info(&pdev->dev, "LED '%s' hazir\n", priv->label);
    return 0;
}

static int led_remove(struct platform_device *pdev)
{
    dev_info(&pdev->dev, "LED kaldirildi\n");
    return 0;
}

static const struct of_device_id led_of_match[] = {
    { .compatible = "myvendor,my-leds" },
    { }
};
MODULE_DEVICE_TABLE(of, led_of_match);

static struct platform_driver led_driver = {
    .probe  = led_probe,
    .remove = led_remove,
    .driver = {
        .name          = "my-leds",
        .of_match_table = led_of_match,
    },
};
module_platform_driver(led_driver);

GPIO descriptor API

devm_gpiod_get()DT'den GPIO al; GPIOD_OUT_LOW/HIGH/IN yönü; device kaldırılınca otomatik gpiod_put
gpiod_set_value(gpiod, val)GPIO değerini yaz; ACTIVE_LOW/HIGH mantığını otomatik uygular
gpiod_get_value(gpiod)GPIO değerini oku; polarite düzeltmesi uygulanmış değer döner
gpiod_direction_output()GPIO'yu çıkış yap ve değer ata — probe'dan sonra yön değiştirmek için
gpiod_direction_input()GPIO'yu giriş yap — interrupt kaynağı olarak kullanmadan önce
terminal
# LED'i aç
$ echo 1 | sudo tee /sys/bus/platform/devices/my-leds/brightness

# LED'i kapat
$ echo 0 | sudo tee /sys/bus/platform/devices/my-leds/brightness

# Mevcut durumu oku
$ cat /sys/bus/platform/devices/my-leds/brightness
1
NOT

Kernel'ın yerleşik LED subsystem'i (/sys/class/leds/) aynı işlevi daha zengin bir API ile sunar: CONFIG_LEDS_CLASS + devm_led_classdev_register(). Yeni LED driver yazıyorsan LED class kullanmayı değerlendir; buradaki örnek platform driver mekaniklerini göstermek içindir.

Bu bölümde

  • devm_gpiod_get() — DTS'ten GPIO descriptor; polarite otomatik yönetilir
  • gpiod_set_value() / gpiod_get_value() — ACTIVE_LOW/HIGH soyutlaması
  • DEVICE_ATTR_RW + devm_device_add_group() — sysfs attribute ekleme
  • Tam LED driver: DTS → probe → sysfs → GPIO kontrol döngüsü
  • Kernel LED subsystem (/sys/class/leds/) — üretim kodu için alternatif

06 Interrupt ile register driver

Interrupt tabanlı peripheral okuma, workqueue ile deferred işleme ve wait queue mekanizmasıyla character device read() entegrasyonu.

Birçok peripheral, veri hazır olduğunda interrupt üretir. Driver bu interrupt'ı yakalayıp veriyi bir buffer'a kopyalar; userspace read() çağrısı ise bu buffer'dan okur. Okuma ile interrupt arasında senkronizasyon için wait queue kullanılır.

  Donanım IRQ tetiklenir
        │
        ▼
  irq_handler() [interrupt ctx]
  • Register'dan veri oku  (readl)
  • ring buffer'a yaz      (spinlock)
  • schedule_work()
        │
        ▼  (deferred — process ctx)
  workqueue handler
  • wake_up_interruptible(&wait_q)
        │
  Kullanıcı tarafı (ayrı thread):
  read() → wait_event_interruptible(wait_q, data_ready)
         → buffer kopyala → copy_to_user → geri döndür
    
irq_chardev.c
// SPDX-License-Identifier: GPL-2.0
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#include <linux/workqueue.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h>

MODULE_LICENSE("GPL");

#define BUF_SIZE  256

struct irqdev_priv {
    void __iomem         *base;
    int                   irq;
    char                  buf[BUF_SIZE];
    size_t                buf_len;
    bool                  data_ready;
    spinlock_t            lock;
    wait_queue_head_t     wait_q;
    struct work_struct    work;
    dev_t                 devno;
    struct cdev           cdev;
    struct class         *cls;
};

/* Workqueue handler — process context */
static void irqdev_work(struct work_struct *work)
{
    struct irqdev_priv *priv =
        container_of(work, struct irqdev_priv, work);
    unsigned long flags;

    spin_lock_irqsave(&priv->lock, flags);
    priv->data_ready = true;
    spin_unlock_irqrestore(&priv->lock, flags);
    wake_up_interruptible(&priv->wait_q);
}

/* Interrupt handler — top half */
static irqreturn_t irqdev_handler(int irq, void *data)
{
    struct irqdev_priv *priv = data;
    unsigned long flags;
    u32           reg_val;

    reg_val = readl(priv->base + 0x04);

    spin_lock_irqsave(&priv->lock, flags);
    priv->buf_len = snprintf(priv->buf, BUF_SIZE,
                              "reg=0x%08x\n", reg_val);
    spin_unlock_irqrestore(&priv->lock, flags);

    schedule_work(&priv->work);
    return IRQ_HANDLED;
}

/* character device read() — wait queue ile bloklar */
static ssize_t irqdev_read(struct file *filp, char __user *ubuf,
                            size_t len, loff_t *off)
{
    struct irqdev_priv *priv = filp->private_data;
    unsigned long flags;
    size_t        to_copy;
    int           ret;

    /* data_ready olana kadar uyku — sinyal gelirse ERESTARTSYS */
    ret = wait_event_interruptible(priv->wait_q, priv->data_ready);
    if (ret)
        return -ERESTARTSYS;

    spin_lock_irqsave(&priv->lock, flags);
    to_copy = min(len, priv->buf_len);
    if (copy_to_user(ubuf, priv->buf, to_copy)) {
        spin_unlock_irqrestore(&priv->lock, flags);
        return -EFAULT;
    }
    priv->data_ready = false;
    spin_unlock_irqrestore(&priv->lock, flags);
    return to_copy;
}

static int irqdev_open(struct inode *inode, struct file *filp)
{
    struct irqdev_priv *priv =
        container_of(inode->i_cdev, struct irqdev_priv, cdev);
    filp->private_data = priv;
    return 0;
}

static const struct file_operations irqdev_fops = {
    .owner   = THIS_MODULE,
    .open    = irqdev_open,
    .read    = irqdev_read,
};
DİKKAT

wait_event_interruptible() sadece process context'te çağrılabilir — interrupt handler veya atomic context içinde çağrılamaz. priv->data_ready condition expression'ı spinlock altında yazılmalı ve read edilmelidir; aksi hâlde race condition oluşur.

Bu bölümde

  • Interrupt → workqueue → wait queue → read() veri akış zinciri
  • wake_up_interruptible() + wait_event_interruptible() çifti
  • container_of() — cdev pointer'ından private struct'a ulaşma
  • spinlock ile interrupt handler ve read() arasında data_ready senkronizasyonu
  • readl() — memory-mapped register okuma; interrupt context'te güvenli

07 Clock ve regulator yönetimi

SoC peripheral'larının büyük çoğunluğu clock ve power rail bağımlıdır; Linux clock framework ve regulator framework bu kaynakları soyutlar.

Clock framework

Peripheral register'larına erişmeden önce clock'un etkinleştirilmesi gerekir. clk_prepare_enable() iki adımı birleştirir: clk_prepare() (uyku yapılabilir hazırlık) + clk_enable() (atomik aktifleştirme).

clock_demo.c — probe() içi clock yönetimi
#include <linux/clk.h>

struct clk_priv {
    struct clk *apb_clk;
    struct clk *ref_clk;
};

static int clkdev_probe(struct platform_device *pdev)
{
    struct clk_priv *priv;
    unsigned long   rate;
    int             ret;

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

    /* DTS clock-names = "apb_pclk", "ref_clk" */
    priv->apb_clk = devm_clk_get(&pdev->dev, "apb_pclk");
    if (IS_ERR(priv->apb_clk))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->apb_clk),
                              "APB clock alinamadi\n");

    /* _optional: DT'de yoksa NULL döner, hata vermez */
    priv->ref_clk = devm_clk_get_optional(&pdev->dev, "ref_clk");
    if (IS_ERR(priv->ref_clk))
        return PTR_ERR(priv->ref_clk);

    /* prepare + enable tek adimda */
    ret = clk_prepare_enable(priv->apb_clk);
    if (ret)
        return dev_err_probe(&pdev->dev, ret,
                              "APB clock etkinlestirilemedi\n");

    rate = clk_get_rate(priv->apb_clk);
    dev_info(&pdev->dev, "APB clock: %lu Hz\n", rate);

    /* Clock hizini ayarla — donanim destekliyorsa */
    ret = clk_set_rate(priv->apb_clk, 50000000);
    if (ret)
        dev_warn(&pdev->dev, "clock rate ayarlanamadi: %d\n", ret);

    platform_set_drvdata(pdev, priv);
    return 0;
}

static int clkdev_remove(struct platform_device *pdev)
{
    struct clk_priv *priv = platform_get_drvdata(pdev);
    clk_disable_unprepare(priv->apb_clk);
    return 0; /* devm_clk_get → clk_put otomatik */
}

Regulator framework

regulator_demo.c
#include <linux/regulator/consumer.h>

struct reg_priv { struct regulator *vdd; };

static int regdev_probe(struct platform_device *pdev)
{
    struct reg_priv *priv;
    int             ret;

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

    /* DTS: vdd-supply = <&reg_3v3>; */
    priv->vdd = devm_regulator_get(&pdev->dev, "vdd");
    if (IS_ERR(priv->vdd))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->vdd),
                              "VDD regulator alinamadi\n");

    ret = regulator_enable(priv->vdd);
    if (ret)
        return dev_err_probe(&pdev->dev, ret,
                              "VDD etkinlestirilemedi\n");

    dev_info(&pdev->dev, "VDD: %d mV\n",
             regulator_get_voltage(priv->vdd) / 1000);
    platform_set_drvdata(pdev, priv);
    return 0;
}

static int regdev_remove(struct platform_device *pdev)
{
    struct reg_priv *priv = platform_get_drvdata(pdev);
    regulator_disable(priv->vdd);
    return 0;
}

Runtime Power Management

runtime_pm_snippet.c
/* probe() sonunda runtime PM etkinleştir */
pm_runtime_set_autosuspend_delay(&pdev->dev, 100); /* 100 ms */
pm_runtime_use_autosuspend(&pdev->dev);
pm_runtime_enable(&pdev->dev);

/* driver operasyonu öncesi — device uyandır */
pm_runtime_get_sync(&pdev->dev);

/* operasyon bitti — otomatik uyku planla */
pm_runtime_put_autosuspend(&pdev->dev);

/* remove() içinde */
pm_runtime_disable(&pdev->dev);

Bu bölümde

  • devm_clk_get() + clk_prepare_enable() — clock al ve etkinleştir
  • clk_get_rate() / clk_set_rate() — clock hızı okuma ve ayarlama
  • clk_disable_unprepare() — remove()'da simetrik kapatma
  • devm_regulator_get() + regulator_enable/disable() — güç kaynağı yönetimi
  • Runtime PM: pm_runtime_get_sync / put_autosuspend — dinamik güç tasarrufu

08 Tam driver örneği: UART benzeri cihaz

of_match_table, probe, IRQ handler, workqueue, character device ve sysfs'i tek çatı altında birleştiren production-ready platform driver örneği.

Register haritası ve DTS

OffsetRegisterAçıklama
0x00DATA_REGOkuma: RX FIFO'dan 1 byte; Yazma: TX FIFO'ya gönder
0x04STATUS_REGbit0: RX_READY, bit1: TX_EMPTY, bit2: ERROR
0x08CTRL_REGbit0: RX_IRQ_EN, bit1: TX_IRQ_EN, bit7: RESET
0x0CBAUD_REGDivisor = clk_freq / (16 * baud)
myuart.dts
/ {
    myuart0: myuart@20000000 {
        compatible      = "myvendor,myuart";
        reg             = <0x20000000 0x100>;
        interrupts      = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>;
        clocks          = <&clk_uart>;
        clock-names     = "uart_clk";
        clock-frequency = <48000000>;
        status          = "okay";
    };
};

Tam driver kodu

myuart.c
// SPDX-License-Identifier: GPL-2.0
#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>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/spinlock.h>
#include <linux/workqueue.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Emirhan Pehlevan");
MODULE_DESCRIPTION("myuart — UART benzeri platform driver");

#define DATA_REG        0x00
#define STATUS_REG      0x04
#define CTRL_REG        0x08
#define BAUD_REG        0x0C
#define STATUS_RX_READY BIT(0)
#define STATUS_TX_EMPTY BIT(1)
#define CTRL_RX_IRQ_EN  BIT(0)
#define CTRL_RESET      BIT(7)
#define RX_BUF_SIZE     1024
#define DEVICE_NAME     "myuart"
#define CLASS_NAME      "myuart_class"

struct myuart_priv {
    void __iomem        *base;
    int                  irq;
    struct clk          *clk;
    u32                  clk_freq;
    char                 rx_buf[RX_BUF_SIZE];
    unsigned int         rx_head;
    unsigned int         rx_tail;
    spinlock_t           rx_lock;
    wait_queue_head_t    rx_wait;
    struct work_struct   rx_work;
    dev_t                devno;
    struct cdev          cdev;
    struct class        *cls;
    struct device       *dev_node;
    u32                  baud;
};

static inline u32  myuart_readl(struct myuart_priv *p, u32 o) { return readl(p->base + o); }
static inline void myuart_writel(struct myuart_priv *p, u32 v, u32 o) { writel(v, p->base + o); }

static void myuart_rx_work(struct work_struct *work)
{
    struct myuart_priv *priv = container_of(work, struct myuart_priv, rx_work);
    wake_up_interruptible(&priv->rx_wait);
}

static irqreturn_t myuart_irq(int irq, void *data)
{
    struct myuart_priv *priv = data;
    unsigned long       flags;
    char                ch;

    if (!(myuart_readl(priv, STATUS_REG) & STATUS_RX_READY))
        return IRQ_NONE;

    spin_lock_irqsave(&priv->rx_lock, flags);
    while (myuart_readl(priv, STATUS_REG) & STATUS_RX_READY) {
        unsigned int next;
        ch = (char)myuart_readl(priv, DATA_REG);
        next = (priv->rx_head + 1) & (RX_BUF_SIZE - 1);
        if (next != priv->rx_tail) {
            priv->rx_buf[priv->rx_head] = ch;
            priv->rx_head = next;
        }
    }
    spin_unlock_irqrestore(&priv->rx_lock, flags);
    schedule_work(&priv->rx_work);
    return IRQ_HANDLED;
}

static int myuart_open(struct inode *inode, struct file *filp)
{
    filp->private_data = container_of(inode->i_cdev, struct myuart_priv, cdev);
    return 0;
}

static int myuart_release(struct inode *i, struct file *f) { return 0; }

static ssize_t myuart_read(struct file *filp, char __user *ubuf,
                            size_t len, loff_t *off)
{
    struct myuart_priv *priv = filp->private_data;
    unsigned long  flags;
    size_t         copied = 0;

    if (wait_event_interruptible(priv->rx_wait,
                priv->rx_head != priv->rx_tail))
        return -ERESTARTSYS;

    spin_lock_irqsave(&priv->rx_lock, flags);
    while (copied < len && priv->rx_tail != priv->rx_head) {
        char ch = priv->rx_buf[priv->rx_tail];
        priv->rx_tail = (priv->rx_tail + 1) & (RX_BUF_SIZE - 1);
        spin_unlock_irqrestore(&priv->rx_lock, flags);
        if (put_user(ch, ubuf + copied))
            return copied ? (ssize_t)copied : -EFAULT;
        copied++;
        spin_lock_irqsave(&priv->rx_lock, flags);
    }
    spin_unlock_irqrestore(&priv->rx_lock, flags);
    return (ssize_t)copied;
}

static ssize_t myuart_write(struct file *filp, const char __user *ubuf,
                             size_t len, loff_t *off)
{
    struct myuart_priv *priv = filp->private_data;
    size_t i;
    char   ch;
    for (i = 0; i < len; i++) {
        if (get_user(ch, ubuf + i))
            return i ? (ssize_t)i : -EFAULT;
        while (!(myuart_readl(priv, STATUS_REG) & STATUS_TX_EMPTY))
            cpu_relax();
        myuart_writel(priv, ch, DATA_REG);
    }
    return (ssize_t)len;
}

static const struct file_operations myuart_fops = {
    .owner   = THIS_MODULE,
    .open    = myuart_open,
    .release = myuart_release,
    .read    = myuart_read,
    .write   = myuart_write,
};

static ssize_t baud_show(struct device *dev,
                          struct device_attribute *attr, char *buf)
{
    struct myuart_priv *priv = dev_get_drvdata(dev);
    return sysfs_emit(buf, "%u\n", priv->baud);
}

static ssize_t baud_store(struct device *dev,
                           struct device_attribute *attr,
                           const char *buf, size_t count)
{
    struct myuart_priv *priv = dev_get_drvdata(dev);
    u32 baud, divisor;
    if (kstrtou32(buf, 10, &baud) || !baud)
        return -EINVAL;
    divisor = priv->clk_freq / (16 * baud);
    myuart_writel(priv, divisor, BAUD_REG);
    priv->baud = baud;
    dev_info(dev, "baud=%u divisor=%u\n", baud, divisor);
    return count;
}
static DEVICE_ATTR_RW(baud);
static struct attribute *myuart_attrs[] = { &dev_attr_baud.attr, NULL };
static const struct attribute_group myuart_group = { .attrs = myuart_attrs };

static int myuart_probe(struct platform_device *pdev)
{
    struct myuart_priv *priv;
    struct resource    *res;
    int                 ret;

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

    spin_lock_init(&priv->rx_lock);
    init_waitqueue_head(&priv->rx_wait);
    INIT_WORK(&priv->rx_work, myuart_rx_work);

    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    priv->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(priv->base)) return PTR_ERR(priv->base);

    priv->clk = devm_clk_get(&pdev->dev, "uart_clk");
    if (IS_ERR(priv->clk))
        return dev_err_probe(&pdev->dev, PTR_ERR(priv->clk),
                              "clock alinamadi\n");

    ret = clk_prepare_enable(priv->clk);
    if (ret) return ret;

    of_property_read_u32(pdev->dev.of_node, "clock-frequency",
                         &priv->clk_freq);
    if (!priv->clk_freq)
        priv->clk_freq = clk_get_rate(priv->clk);

    /* Donanımı sıfırla, RX IRQ etkinleştir, baud rate ayarla */
    myuart_writel(priv, CTRL_RESET, CTRL_REG);
    myuart_writel(priv, CTRL_RX_IRQ_EN, CTRL_REG);
    priv->baud = 115200;
    myuart_writel(priv, priv->clk_freq / (16 * 115200), BAUD_REG);

    priv->irq = platform_get_irq(pdev, 0);
    if (priv->irq < 0) { ret = priv->irq; goto err_clk; }

    ret = devm_request_irq(&pdev->dev, priv->irq, myuart_irq,
                            0, "myuart", priv);
    if (ret) goto err_clk;

    ret = alloc_chrdev_region(&priv->devno, 0, 1, DEVICE_NAME);
    if (ret) goto err_clk;

    cdev_init(&priv->cdev, &myuart_fops);
    priv->cdev.owner = THIS_MODULE;
    ret = cdev_add(&priv->cdev, priv->devno, 1);
    if (ret) goto err_unreg;

    priv->cls = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(priv->cls)) { ret = PTR_ERR(priv->cls); goto err_cdev; }

    priv->dev_node = device_create(priv->cls, &pdev->dev,
                                    priv->devno, priv, DEVICE_NAME);
    if (IS_ERR(priv->dev_node)) {
        ret = PTR_ERR(priv->dev_node); goto err_class;
    }

    ret = sysfs_create_group(&priv->dev_node->kobj, &myuart_group);
    if (ret) goto err_devnode;

    platform_set_drvdata(pdev, priv);
    dev_info(&pdev->dev,
             "myuart probe OK — /dev/%s IRQ=%d clk=%u baud=%u\n",
             DEVICE_NAME, priv->irq, priv->clk_freq, priv->baud);
    return 0;

err_devnode: device_destroy(priv->cls, priv->devno);
err_class:   class_destroy(priv->cls);
err_cdev:    cdev_del(&priv->cdev);
err_unreg:   unregister_chrdev_region(priv->devno, 1);
err_clk:     clk_disable_unprepare(priv->clk);
             return ret;
}

static int myuart_remove(struct platform_device *pdev)
{
    struct myuart_priv *priv = platform_get_drvdata(pdev);
    sysfs_remove_group(&priv->dev_node->kobj, &myuart_group);
    device_destroy(priv->cls, priv->devno);
    class_destroy(priv->cls);
    cdev_del(&priv->cdev);
    unregister_chrdev_region(priv->devno, 1);
    cancel_work_sync(&priv->rx_work);
    clk_disable_unprepare(priv->clk);
    /* devm_: ioremap, irq, kzalloc otomatik temizlenir */
    dev_info(&pdev->dev, "myuart kaldirildi\n");
    return 0;
}

static const struct of_device_id myuart_of_match[] = {
    { .compatible = "myvendor,myuart" },
    { }
};
MODULE_DEVICE_TABLE(of, myuart_of_match);

static struct platform_driver myuart_driver = {
    .probe  = myuart_probe,
    .remove = myuart_remove,
    .driver = {
        .name          = "myuart",
        .of_match_table = myuart_of_match,
    },
};
module_platform_driver(myuart_driver);

Makefile ve derleme

Makefile
obj-m := myuart.o
KDIR  := /lib/modules/$(shell uname -r)/build

all:
	make -C $(KDIR) M=$(PWD) modules

clean:
	make -C $(KDIR) M=$(PWD) clean

Test adımları

terminal
# 1. Derle
$ make
  CC [M]  myuart.o
  LD [M]  myuart.ko

# 2. Modülü yükle
$ sudo insmod myuart.ko
[  10.001] myuart: probe OK — /dev/myuart IRQ=55 clk=48000000 baud=115200

# 3. Baud rate'i sysfs üzerinden değiştir
$ echo 9600 | sudo tee /sys/class/myuart_class/myuart/baud
$ cat /sys/class/myuart_class/myuart/baud
9600

# 4. Veri gönder
$ echo "AT+TEST" | sudo tee /dev/myuart

# 5. Veri oku (interrupt tabanlı — veri gelince döner)
$ sudo cat /dev/myuart
OK

# 6. Modülü kaldır
$ sudo rmmod myuart
[  45.112] myuart: myuart kaldirildi
NOT

Bu örnekte character device için devm_ kullanılamamıştır çünkü class_create() ve device_create()'in devm_ versiyonları (devm_class_create()) kernel 6.4'te eklenmiştir. Daha eski kernel'larda goto cleanup pattern veya özel devm_add_action_or_reset() wrapper kullanılmalıdır.

Bu bölümde

  • Tam myuart driver: DTS → probe → IRQ + workqueue + wait queue + chardev + sysfs
  • Ring buffer ile RX FIFO yönetimi; spinlock + wait_event_interruptible entegrasyonu
  • baud rate sysfs attribute: kstrtou32 + register yazma — çalışırken değiştirilebilir
  • probe() goto cleanup pattern: devm_ ile yönetilemeyen chardev kaynakları için
  • remove() ters sıra: sysfs → device → class → cdev → cancel_work → clk