1. 程式人生 > >Linux下led子系統 --- 分析篇

Linux下led子系統 --- 分析篇

前言:

什麼叫做驅動框架?
核心中驅動部分維護者針對每個種類的驅動設計一套成熟的、標準的、典型的驅動實現,並把不同廠家的同類硬體驅動中相同的部分抽出來自己實現好,再把不同部分留出介面給具體的驅動開發工程師來實現,這就叫驅動框架。即標準化的驅動實現,統一管理系統資源,維護系統穩定。

概述:

led子系統驅動框架:

所有led共性:
 有和使用者通訊的裝置節點
 亮和滅

不同點:
 有的led可能是接在gpio管腳上,不同的led有不同的gpio來控制
 有的led可能由其他的晶片來控制(節約cpu的pin,或者為了控制led的電流等)
 可以設定亮度
 可以閃爍
所以Linux中led子系統驅動框架把把所有led的共性給實現了,把不同的地方留給驅動工程師去做.
led子系統核心檔案:

driver/leds/led-class.c
driver/leds/led-core.c
driver/leds/led-triggers.c
include/linux/leds.h

輔助檔案(也就是說可以根據需求來決定這部分程式碼是否需要)

driver/leds/led-triggers.c
driver/leds/trigger/led-triggers.c
driver/leds/trigger/ledtrig-oneshot.c
driver/leds/trigger/ledtrig-timer.c
driver/leds/trigger/ledtrig-heartbeat.c

程式碼框架分析

led-class.c(led子系統框架的入口) 和led-core.c

  1. 會在/sys/class/目錄下建立leds類.
  2. 提供不同led裝置往led子系統註冊的介面,並在/sys/class/下建立對應的裝置節點,並在節點下面建立對應的屬性檔案,最後提供給應用統一的訪問介面(read/write).
    當make menuconfig選中LED Class Support這一項(在選擇這個之前需要先選擇device driver中的LED Support),就會呼叫led-class.c中的下面的入口:

subsys_initcall(leds_init);
leds_init模組入口函式負責在/sys/class/目錄下面建立一個leds類目錄,併為基於leds這個類的每個裝置(device)建立對應的屬性檔案同時將led-class中的suspend的指標以及resume的指標初始化了,一般 來說是當系統休眠的時候系統上層會層層通知各個裝置進入睡眠狀態,那麼負責這個裝置的驅動則實際執行睡眠,例如手機的休眠鍵位,喚醒時呼叫的是 resume,恢復裝置的執行狀態,這也是為了省電。即電源管理。

static int __init leds_init(void)
{
    leds_class = class_create(THIS_MODULE, "leds");//會生成/sys/class/leds/目錄
    leds_class->pm = &leds_class_dev_pm_ops;       //suspend的指標以及resume的指標初始化
    leds_class->dev_groups = led_groups;           //建立為基於這個class的所有裝置建立屬性
    return 0;
}
brightness max_brightness等屬性的建立
static const struct attribute_group *led_groups[] = {
    &led_group,
#ifdef CONFIG_LEDS_TRIGGERS                   //只有開啟這個巨集,才會建立對應的trigger屬性(trigger後面分析)
    &led_trigger_group,
#endif
    NULL,
};
static const struct attribute_group led_group = {
    .attrs = led_class_attrs,
};
static const struct attribute_group led_trigger_group = {
    .attrs = led_trigger_attrs,
};

其中DEVICE_ATTR屬性的原型是(Documentation/driver-model/Device.txt中有對DEVICE_ATTR的詳細介紹)
#define DEVICE_ATTR(_name, _mode, _show, _store) \

  struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

_name表示屬性的名字,即在sys中呈現的檔案.

_mode表示這個屬性的讀寫許可權,如0666, 分別表示user/group/other的許可權都是可讀可寫

_show表示的是對此屬性的讀函式,當cat這個屬性的時候被呼叫,_stroe表示的是對此屬性的寫函式,當echo內容到這個屬性的時候被呼叫。

當然_ATTR還有一系列的如__ATTR_RO巨集只有讀方法,__ATTR_RW等等

static DEVICE_ATTR(trigger, 0666, led_trigger_show, led_trigger_store);
static DEVICE_ATTR_RO(max_brightness);
static DEVICE_ATTR_RW(brightness);
static struct attribute *led_class_attrs[] = {
    &dev_attr_brightness.attr,            //&dev_attr_xxx中xxx必須和DEVICE_ATTR中的name一致
    &dev_attr_max_brightness.attr,
    NULL,
};
static struct attribute *led_trigger_attrs[] = {
    &dev_attr_trigger.attr,
    NULL,
};
提供register介面

led-class.c還提供了一個讓驅動開發者在/sys/class/leds這個類下建立led裝置的介面

/**
 * led_classdev_register - register a new object of led_classdev class.
 * @parent: The device to register.
 * @led_cdev: the led_classdev structure for this device.
 */
int led_classdev_register(struct device *parent, struct led_classdev *led_cdev)
{
    char name[64];
    int ret;
    //獲取你要建立的裝置的名字
    ret = led_classdev_next_name(led_cdev->name, name, sizeof(name));
    //基於leds這個類建立對應的裝置
    led_cdev->dev = device_create_with_groups(leds_class, parent, 0,
                led_cdev, led_cdev->groups, "%s", name);          
    //將這個註冊的裝置新增到連結串列中
    list_add_tail(&led_cdev->node, &leds_list);
    //如果裝置驅動在註冊時沒有設定max_brightness,則將max_brightness設定為滿即255,在leds.h中定義
    if (!led_cdev->max_brightness)
        led_cdev->max_brightness = LED_FULL;

    led_cdev->flags |= SET_BRIGHTNESS_ASYNC;
    //如果在註冊led_cdev時,設定了get_brightness方法,則讀出當前的brightness並更新
    led_update_brightness(led_cdev);
    //設定一個定時器,
    led_init_core(led_cdev);

    //如果打開了trigger巨集,則設定預設的trigger
#ifdef CONFIG_LEDS_TRIGGERS
    led_trigger_set_default(led_cdev);
#endif

    return 0;
}
EXPORT_SYMBOL_GPL(led_classdev_register);

當驅動呼叫led_classdev_register註冊了一個裝置,那麼就會在/sys/class/leds目錄下建立xxx裝置,並這個xxx目錄下建立一系列attr屬性檔案,如brightness max_brightness trigger等

cat 或者echo 屬性檔案時

當用戶在檔案系統下讀寫這些屬性檔案時,就會呼叫這些屬性檔案的show和store方法.
如,當用戶cat /sys/class/leds/xxx/brightness時會呼叫led-class.c中的brightness_show函式

static ssize_t brightness_show(struct device *dev, struct device_attribute *attr, char *buf)
{
    //當你操作哪個裝置下的屬性檔案,就會根據這個裝置對應的device結構體獲取leds_classdev結構,裡面有這個裝置的所有資訊
    struct led_classdev *led_cdev = dev_get_drvdata(dev);

    /* no lock needed for this */
    led_update_brightness(led_cdev);
    //如果裝置驅動註冊時傳入了brightness_get函式,則去呼叫他驅動實時的brightness,否則將裝置初始化的brightness值返回給使用者
    return sprintf(buf, "%u\n", led_cdev->brightness);
}
int led_update_brightness(struct led_classdev *led_cdev)
{
    int ret = 0;
    //呼叫驅動註冊時傳入的brightness_get函式,獲取brightness值
    if (led_cdev->brightness_get) {
        ret = led_cdev->brightness_get(led_cdev);
    }

    return ret;
}

當用戶echo 100 > /sys/class/leds/xxx/brightness時會呼叫led-class.c中的brightness_store函式

static ssize_t brightness_store(struct device *dev,
        struct device_attribute *attr, const char *buf, size_t size)
{
    struct led_classdev *led_cdev = dev_get_drvdata(dev);
    //讀取使用者傳入的brightness值,並存入state中
    ret = kstrtoul(buf, 10, &state);
    if (ret)
        goto unlock;
    //如果使用者寫入的brightness是0,即關閉led,則把對應的trigger移除
    if (state == LED_OFF)
        led_trigger_remove(led_cdev);
    //否則呼叫裝置驅動傳入的設定亮度函式,設定相應的亮度
    led_set_brightness(led_cdev, state);

    ret = size;
}


void led_set_brightness(struct led_classdev *led_cdev, enum led_brightness brightness)
{
    int ret = 0;

    /* delay brightness if soft-blink is active */
    if (led_cdev->blink_delay_on || led_cdev->blink_delay_off) {
        led_cdev->delayed_set_value = brightness;
        if (brightness == LED_OFF)
            schedule_work(&led_cdev->set_brightness_work);
        return;
    }

    if (led_cdev->flags & SET_BRIGHTNESS_ASYNC) {
        led_set_brightness_async(led_cdev, brightness);
        return;
    } else if (led_cdev->flags & SET_BRIGHTNESS_SYNC)
        ret = led_set_brightness_sync(led_cdev, brightness);
}
static inline void led_set_brightness_async(struct led_classdev *led_cdev,  enum led_brightness value)
{
    value = min(value, led_cdev->max_brightness);
    led_cdev->brightness = value;

    if (!(led_cdev->flags & LED_SUSPENDED))
        led_cdev->brightness_set(led_cdev, value);   //呼叫具體的led驅動的brighntness_set函式,操作具體的led的亮度
}
trigger操作

當用戶cat /sys/class/leds/xxx/trigger

ssize_t led_trigger_show(struct device *dev, struct device_attribute *attr, char *buf)
{
    struct led_classdev *led_cdev = dev_get_drvdata(dev);
    struct led_trigger *trig;
    int len = 0;

    //如果沒有預設的trigger,則將none這個list加上"[]",當我們cat trigger時會發現,當前的trigger是哪一項,那麼這一項會用"[]"標示
    if (!led_cdev->trigger)
        len += sprintf(buf+len, "[none] ");
    else
        len += sprintf(buf+len, "none ");
    //列出trigger_list中的所有trigger
    list_for_each_entry(trig, &trigger_list, next_trig) {
        if (led_cdev->trigger && !strcmp(led_cdev->trigger->name,
                            trig->name))
            len += sprintf(buf+len, "[%s] ", trig->name);
        else
            len += sprintf(buf+len, "%s ", trig->name);
    }

    len += sprintf(len+buf, "\n");
    return len;
}

當用戶echo timer > /sys/class/leds/xxx/trigger

ssize_t led_trigger_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
    struct led_classdev *led_cdev = dev_get_drvdata(dev);
    char trigger_name[TRIG_NAME_MAX];
    struct led_trigger *trig;

    trigger_name[sizeof(trigger_name) - 1] = '\0';
    strncpy(trigger_name, buf, sizeof(trigger_name) - 1);
    len = strlen(trigger_name);

    if (len && trigger_name[len - 1] == '\n')
        trigger_name[len - 1] = '\0';

    if (!strcmp(trigger_name, "none")) {
        led_trigger_remove(led_cdev);
        goto unlock;
    }

    list_for_each_entry(trig, &trigger_list, next_trig) {
        if (!strcmp(trigger_name, trig->name)) {
            led_trigger_set(led_cdev, trig);         //這個地方會呼叫對應trigger的activated函式,然後啟用此trigger
            goto unlock;
        }
    }

unlock:
    return ret;
}
觸發器 trigger概述

當make menuconfig將Trigger support開啟,並將對應的timer等trigger開啟,那麼就會呼叫對應的ledtri-xxx.c,在/sys/class/leds/xxx/目錄下生成一個trigger檔案,cat trigger可以列出你開啟的這些trigger,例如timer one-short等.

  [*]   LED Trigger support  --->                           //會編譯driver/leds/led-triggers.c
      [*]   LED Timer Trigger                                                        
      [*]   LED One-shot Trigger              
      [*]   LED Heartbeat Trigger          
      [ ]   LED backlight Trigger            
      [ ]   LED CPU Trigger                   
      [ ]   LED GPIO Trigger              
      [ ]   LED Default ON Trigger         
      .....

下面介紹一下常用的trigger - timer
對應ledtri-timer.c,稱之為閃爍定時觸發器,某個led_classdev與之連線後(即echo timer > trigger,或者在驅動程式碼中將timer設定為預設trigger),這個觸發器會在/sys/class/leds//下建立兩個屬性檔案delay_on/delay_off,顧名思義,這兩個檔案分別儲存led燈亮的時間和滅的時間,led會按照這兩個時間來進行閃爍,單位是ms,如果led_classdev註冊了硬體閃爍的介面led_cdev->blink_set就是用硬體控制閃爍,否則用軟體定時器(ledtri-timer.c,即led子系統中的定時器)來控制閃爍。
我第一次在使用led子系統實現觸發器(trigger)的時候,一直在一個很低階的錯誤點糾結:
使用者可以通過echo xxx > delay_on或者off來控制燈閃爍,然而我自己的驅動程式碼中根本沒有讓燈閃爍的程式碼,他是如何閃爍的呢,後來分析了ledtri-timer.c程式碼才發現,ledtri-timer.c中的blink函式會呼叫對應驅動註冊的set_brightness函式,從而來控制對應led的閃爍,所以無論你是什麼平臺,不管你是哪個led,我的timer觸發器都可以通過呼叫你的set_brightness函式從而來控制閃爍.
下面以ledtri-timer.c為例分析各種trigger
當在make menucong中把這個巨集開啟 [*] LED Timer Trigger就會載入module_init
module_init(timer_trig_init);

給timer這個trigger的activate等賦值,在上面介紹過,當用戶echo timer > trigger的時候led_trigger_store會呼叫led_trigger_set(led_cdev, trig); 繼而呼叫對應trigger的activate函式.

static int __init timer_trig_init(void)
{
    return led_trigger_register(&timer_led_trigger);
}
static struct led_trigger timer_led_trigger = {
    .name     = "timer",
    .activate = timer_trig_activate,
    .deactivate = timer_trig_deactivate,
};
/建立delay_on和delay_off這兩個屬性檔案
static void timer_trig_activate(struct led_classdev *led_cdev)
{
    rc = device_create_file(led_cdev->dev, &dev_attr_delay_on);
    rc = device_create_file(led_cdev->dev, &dev_attr_delay_off);

    led_blink_set(led_cdev, &led_cdev->blink_delay_on, &led_cdev->blink_delay_off);
    led_cdev->activated = true;
}
void led_blink_set(struct led_classdev *led_cdev,
           unsigned long *delay_on,
           unsigned long *delay_off)
{
    del_timer_sync(&led_cdev->blink_timer);

    led_cdev->flags &= ~LED_BLINK_ONESHOT;
    led_cdev->flags &= ~LED_BLINK_ONESHOT_STOP;

    led_blink_setup(led_cdev, delay_on, delay_off);
}
static void led_blink_setup(struct led_classdev *led_cdev,
             unsigned long *delay_on,
             unsigned long *delay_off)
{
    if (!(led_cdev->flags & LED_BLINK_ONESHOT) &&
        led_cdev->blink_set &&
        !led_cdev->blink_set(led_cdev, delay_on, delay_off))
        return;

    /* blink with 1 Hz as default if nothing specified */
    if (!*delay_on && !*delay_off)
        *delay_on = *delay_off = 500;

    led_set_software_blink(led_cdev, *delay_on, *delay_off);
}
static void led_set_software_blink(struct led_classdev *led_cdev,
                   unsigned long delay_on,
                   unsigned long delay_off)
{
    led_cdev->blink_delay_on = delay_on;
    led_cdev->blink_delay_off = delay_off;

    /* never on - just set to off */
    if (!delay_on) {
        led_set_brightness_async(led_cdev, LED_OFF);
        return;
    }

    /* never off - just set to brightness */
    if (!delay_off) {
        led_set_brightness_async(led_cdev, led_cdev->blink_brightness);
        return;
    }
    //開啟led_init_core函式初始化的定時器,開始執行led閃爍功能
    mod_timer(&led_cdev->blink_timer, jiffies + 1);
}

在led_classdev_register時,呼叫了led_init_core函式去初始化了一個timer定時器

void led_init_core(struct led_classdev *led_cdev)
{
    setup_timer(&led_cdev->blink_timer, led_timer_function,
            (unsigned long)led_cdev);
}
static void led_timer_function(unsigned long data)
{
    struct led_classdev *led_cdev = (void *)data;
    unsigned long brightness;
    unsigned long delay;
     //如果delay_on和delay_off沒有設定的話,說明沒有設定閃爍功能,則直接退出
    if (!led_cdev->blink_delay_on || !led_cdev->blink_delay_off) {
        led_set_brightness_async(led_cdev, LED_OFF);
        return;
    }

    if (led_cdev->flags & LED_BLINK_ONESHOT_STOP) {
        led_cdev->flags &= ~LED_BLINK_ONESHOT_STOP;
        return;
    }

    //如果程式執行到這個位置,說明已經設定了閃爍功能,則執行閃爍
    brightness = led_get_brightness(led_cdev);
    if (!brightness) {        //如果led在處於關閉狀態,就將brightness設定blink_brightness,最後開啟led
        /* Time to switch the LED on. */
        if (led_cdev->delayed_set_value) {
            led_cdev->blink_brightness =
                    led_cdev->delayed_set_value;
            led_cdev->delayed_set_value = 0;
        }
        brightness = led_cdev->blink_brightness;
        delay = led_cdev->blink_delay_on;     //將delay設定為delay_on的時間,定時器
    } else {      //如果led在處於開啟狀態,就將brightness設定0,最後關閉led
        /* Store the current brightness value to be able
         * to restore it when the delay_off period is over.
         */
        led_cdev->blink_brightness = brightness;
        brightness = LED_OFF;
        delay = led_cdev->blink_delay_off;

led_set_brightness_async(led_cdev, brightness);

    /* Return in next iteration if led is in one-shot mode and we are in
     * the final blink state so that the led is toggled each delay_on +
     * delay_off milliseconds in worst case.
     */
    if (led_cdev->flags & LED_BLINK_ONESHOT) {
        if (led_cdev->flags & LED_BLINK_INVERT) {
            if (brightness)
                led_cdev->flags |= LED_BLINK_ONESHOT_STOP;
        } else {
            if (!brightness)
                led_cdev->flags |= LED_BLINK_ONESHOT_STOP;
        }
    }
    //實現迴圈定時器的功能,根據設定的on和off時間進行LED_ON和LED_OFF
    mod_timer(&led_cdev->blink_timer, jiffies + msecs_to_jiffies(delay));
}

到此為止, 整個led子系統框架分析完畢,當驅動工程師需要寫一個led驅動的時刻,可以按照這個標準,在led子系統中註冊對應的led裝置,建立對應的屬性,選擇對應的觸發方式(trigger).
應用例項請參考下篇 ——- Linux下led子系統 — 例項篇