简介

本文分析MTK平台对LED(指示灯)驱动节点部分的创建、处理流程。MTK Android平台遵循Linux的规范惯例,和常规的Linux一样,将发光的二极管等设备抽象为一个sysfs class(驱动里面的一种类),诸如指示灯、LED灯、背光等都属于这一类,因此都在同一个class(/sys/class/leds)下管理。MTK平台在该目录下提供了对LED进行操作的驱动节点。

其中,和常规的Linux一样,也是通过trigger节点来控制LED灯的触发。

android:/ # ls /sys/class/leds/*
/sys/class/leds/blue:
brightness device max_brightness power subsystem trigger uevent 

/sys/class/leds/green:
brightness device max_brightness power subsystem trigger uevent 

/sys/class/leds/lcd-backlight:
brightness device max_brightness power subsystem trigger uevent 

/sys/class/leds/red:
brightness device max_brightness power subsystem trigger uevent 

/sys/class/leds/vibrator:
activate brightness device duration max_brightness power state subsystem trigger uevent

Linux标准规范下对LED设备节点名称的命名格式为:devicename:colour:function,而MTK Android平台不遵循这一规则,通常直接用简单直观的名字命名。

1. LED模式、驱动方式和基本数据结构

对于MTK Android平台各个LED驱动节点而言,设备中各LED均按照一个既定的类型和模式来管理并驱动。对于一个LED设备驱动而言,指定了其类型、模式和输出方式,就是完整的一个LED控制方法的配置了。

MTK Android平台将设备中对LED归类为如下几种类型,主要是RGB三色灯、键盘灯按键灯、LCD背光等。

enum mt65xx_led_type            
{
        MT65XX_LED_TYPE_RED = 0,
        MT65XX_LED_TYPE_GREEN,
        MT65XX_LED_TYPE_BLUE,
        MT65XX_LED_TYPE_JOGBALL,
        MT65XX_LED_TYPE_KEYBOARD,
        MT65XX_LED_TYPE_BUTTON,
        MT65XX_LED_TYPE_LCD,
        MT65XX_LED_TYPE_TOTAL,
};

而驱动这些能发光的器件,主要是通过GPIO输出、PWM输出、借助PMIC(iSink)输出等几种方式。

enum mt65xx_led_mode
{
        MT65XX_LED_MODE_NONE,
        MT65XX_LED_MODE_PWM,
        MT65XX_LED_MODE_GPIO,
        MT65XX_LED_MODE_PMIC,
        MT65XX_LED_MODE_CUST_LCM,
        MT65XX_LED_MODE_CUST_BLS_PWM
};

// PMIC iSink的输出方式
enum mt65xx_led_pmic
{
        MT65XX_LED_PMIC_LCD_ISINK = 0,
        MT65XX_LED_PMIC_NLED_ISINK_MIN = MT65XX_LED_PMIC_LCD_ISINK,
        MT65XX_LED_PMIC_NLED_ISINK0,
        MT65XX_LED_PMIC_NLED_ISINK1,
        MT65XX_LED_PMIC_NLED_ISINK_MAX,
};

// PWM方式的定义
struct PWM_config
{
        int clock_source;
        int div;
        int low_duration;
        int High_duration;
        BOOL pmic_pad;
};

LED设备配置模式的一个完整的描述如下,完整的LED包含名称、模式、输出方式。

/*
 * name : must the same as lights HAL
 * mode : control mode
 * data :
 *    PWM:  pwm number
 *    GPIO: gpio id
 *    PMIC: enum mt65xx_led_pmic
 *    CUST: custom set brightness function pointer
*/
struct cust_mt65xx_led {
        char                 *name;
        enum mt65xx_led_mode  mode;
        int                   data;
        struct PWM_config config_data;
};

对于上述data成员,当mode是MT65XX_LED_MODE_CUST_LCM时,data存储控制屏幕亮度的函数指针(一般这种屏幕是接收BB端的指令来自行调节亮度的,而不是BB端来控制背光,如OLED等自发光的屏幕)。

小结:通过cust_mt65xx_led来描述一个LED类型和驱动模式。

cust_mt65xx_led只是LED类型和驱动模式的封装,还不是真正使用的驱动的数据结构。

2. 驱动节点配置

为了方便不同的主板对自己的LED进行适配,MTK Android平台提供了dts的方式来允许用户方便的配置支持的LED数量和类型,不需要用户手动去创建和初始化cust_mt65xx_led

其中,当dts没有配置时,会从一个数组(列表)中获取LED配置。如果不进行dts配置,仅修改这个列表,效果也是一样的,但不推荐。

2.1. 列表配置

前面讲过,cust_mt65xx_led即是LED模式和驱动方式的完整抽象,cust_leds.c定义了该类型的数组,用来包含保存所有LED配置实例,外部文件通过get_cust_led_list获得这个列表。

没有定义dts时才会获取这个列表

static struct cust_mt65xx_led cust_led_list[MT65XX_LED_TYPE_TOTAL] = {
    {"red",               MT65XX_LED_MODE_PWM, 1,{0,0,0,0,1}},
    {"green",             MT65XX_LED_MODE_PWM, 0,{0,0,0,0,1}},
    {"blue",              MT65XX_LED_MODE_PWM, -1,{0,0,0,0,1}},
    {"jogball-backlight", MT65XX_LED_MODE_NONE, -1,{0,0,0,0,0}},
    {"keyboard-backlight",MT65XX_LED_MODE_NONE, -1,{0,0,0,0,0}},
    {"button-backlight",  MT65XX_LED_MODE_NONE, -1,{0,0,0,0,0}},
    {"lcd-backlight",     MT65XX_LED_MODE_CUST_BLS_PWM, (int)disp_bls_set_backlight,{0}},
};

struct cust_mt65xx_led *get_cust_led_list(void)
{
        return cust_led_list;
}

2.2. dts配置

LED dts的配置如下。由于dts中配置的命名非常直观,且和前述LED数据结构变量名都能刚好对应,不再赘述。由于LED节点的创建流程中会首先判断是否存在dts配置,然后再检查列表配置。因此dts配置优先级高于列表配置。

&odm {
        led0:led@0 {
                compatible = "mediatek,red";
                led_mode = <1>;
                data = <1>;
                pwm_config = <0 0 0 0 0>;
        };

        led1:led@1 {
                compatible = "mediatek,green";
                led_mode = <1>;
                data = <0>;
                pwm_config = <0 0 0 0 0>;
        };
        ...
};

2.3. dts解析

LED配置的解析和加载是懒加载的形式。即运行流程中发生了对LED相关操作(如mt65xx_leds_brightness_set()函数调用)后,检查是否完成了dts的解析,使得LED dts配置是在使用时才解析,且仅进行首次解析。

int mt65xx_leds_brightness_set(enum mt65xx_led_type type, enum led_brightness level)
{
        struct cust_mt65xx_led *cust_led_list = get_cust_led_dtsi();

        // dts没有配置时,加载列表配置
        if (cust_led_list == NULL) {
                cust_led_list = get_cust_led_list();
                LEDS_DEBUG("Cannot not get the LED info from device tree. \n");
        }

        if (type >= MT65XX_LED_TYPE_TOTAL)
                return -1;
        
        ...
}

get_cust_led_dtsi()通过缓存解析结果来实现仅首次解析一次的目的。其中,缓存结果存储到pled_dtsi中。它是以指针类型存储的数组。

struct cust_mt65xx_led pled_dtsi[MT65XX_LED_TYPE_TOTAL];

前面dts的配置可以确定LED dts配置的解析路径,以及相匹配的LED名称。

char *leds_name[MT65XX_LED_TYPE_TOTAL] = {
        "red",
        "green",
        "blue",
        "jogball-backlight",
        "keyboard-backlight",
        "button-backlight",
        "lcd-backlight",
};

char *leds_node[MT65XX_LED_TYPE_TOTAL] = {
        "/odm/led@0",
        "/odm/led@1",
        "/odm/led@2",
        "/odm/led@3",
        "/odm/led@4",
        "/odm/led@5",
        "/odm/led@6",
};

解析函数如下。它的逻辑很简单,就是按照leds_name[]数组来按顺序解析dts,对数据结构初始化。如果是CUST系列的两个mode,则对data赋值为对应函数指针。

struct cust_mt65xx_led *get_cust_led_dtsi(void)
{
        static bool isDTinited = false;
        int i, offset;
        int pwm_config[5] = {0};

#if defined(USE_DTB_NO_DWS)
        if ( isDTinited == true )
                goto out;
        for (i = 0; i < MT65XX_LED_TYPE_TOTAL; i++) {
                pled_dtsi[i].name = leds_name[i];
                offset = fdt_path_offset(get_lk_overlayed_dtb(), leds_node[i]);
                if (offset < 0) {
                        LEDS_DEBUG("[LEDS]LK:Cannot find LED node from dts\n");
                        pled_dtsi[i].mode = 0;
                        pled_dtsi[i].data = -1;
                } else {
                        isDTinited = true;
                        pled_dtsi[i].mode = led_fdt_getprop_u32(get_lk_overlayed_dtb(), offset, "led_mode");
                        pled_dtsi[i].data = led_fdt_getprop_u32(get_lk_overlayed_dtb(), offset, "data");
                        led_fdt_getprop_char_array(get_lk_overlayed_dtb(), offset, "pwm_config", (char *)pwm_config);
                        pled_dtsi[i].config_data.clock_source = pwm_config[0];
                        pled_dtsi[i].config_data.div = pwm_config[1];
                        pled_dtsi[i].config_data.low_duration = pwm_config[2];
                        pled_dtsi[i].config_data.High_duration = pwm_config[3];
                        pled_dtsi[i].config_data.pmic_pad = pwm_config[4];
                        switch (pled_dtsi[i].mode) {
                                case MT65XX_LED_MODE_CUST_LCM:
                                        pled_dtsi[i].data = (long)primary_display_setbacklight;
                                        LEDS_DEBUG("[LEDS]LK:The backlight hw mode is LCM.\n");
                                        break;
                                case MT65XX_LED_MODE_CUST_BLS_PWM:
                                        pled_dtsi[i].data = (long)disp_bls_set_backlight;
                                        LEDS_DEBUG("[LEDS]LK:The backlight hw mode is BLS.\n");
                                        break;
                                default:
                                        break;
                        }
                        LEDS_INFO("[LEDS]LK:led[%d] offset is %d,mode is %d,data is %d .\n",    \
                                  i,offset,pled_dtsi[i].mode,pled_dtsi[i].data);
                }
        }
#endif
        if ( isDTinited == false )
                return NULL;
out:
        return pled_dtsi;
}

3. 驱动的初始化和驱动节点创建

MTK LED驱动是一个独立的module,由CONFIG_MTK_LEDS控制,在系统中注册LED设备驱动,并初始化节点。实际上,完整的LED节点创建过程中,创建LED节点本身是第二步,第一步是Linux原生创建的LED sysfs class。因此Kernel中有一些Linux定义好的用来创建对应class的KConfig配置也和LED驱动节点有关。下表列出相关的一些KConfig配置(这些配置是需要打开以支持LED驱动的),列出的顺序和Module加载顺序是一致的。

除CONFIG_MTK_LEDS是MTK加入的,其他都是Linux原生的

定义 包含模块 加载 简介
CONFIG_NEW_LEDS led-core.o exported symbols 提供LED操作函数的抽象和这些函数的symbol export,定义了leds_list、锁并export。主要是将通用函数和公共全局变量symbol export出来,并挂接到真正的ledclass_cdev实现中。相当于将通用API和实际设备驱动进行胶水层粘合
CONFIG_LEDS_CLASS led-class.o subsys_initcall 加载、初始化LED类(class),让系统中出现这一类的sysfs驱动节点,初始化对应attribute。注册电源管理函数、group attribute等。
CONFIG_LEDS_TRIGGERS led-triggers.o exported symbols 类似led-core,提供并export trigger相关的通用API |
CONFIG_LEDS_TRIGGER_TIMER ledtrig-timer.o module_init 用于trigger实现blink,创建delay_on/off节点并通过它们来运行调用者设置、实现blink
CONFIG_MTK_LEDS mtk_leds_drv.o mtk_leds.o late_initcall,在led-class加载完成后才加载,先加载mtk_leds_drv后加载mtk_leds mtk_leds_drv完成LED dts配置解析,并创建LED在sysfs class的节点,注册了LED brightness、blink等节点设置亮度、blink的处理函数块。其中,mtk_leds_drv注册的各处理函数,都是调用了mtk_leds.o

3.1. sysfs LED Class驱动

在分析LED驱动前,先分析sysfs下LED class。led-class.o在subsys_initcall阶段初始化,主要完成创建class类、注册电源管理函数、注册groups。

3.1.1. 创建sysfs LED class

创建一个class比较简单,调用class_create()就会在对应路径创建class目录了。led-class.o模块通过这个方法在sysfs下创建了leds目录。

3.1.2. 注册sysfs class dev_pm_ops

dev_pm_ops是设备电源管理的操作函数,当LED驱动挂起和恢复时调用,用于LED电源的管理。其中,注册的操作函数包含led_suspendled_resume

其中suspend的过程非常简单,通过dev_get_drvdata(struct device*),根据电源管理子系统传入的device取得存储于device->driver_dataled_classdev指针,然后调用led-core.o定义好的API,把LED的brightness设置为0,关掉了灯即进入了suspend。

如前所述,led class、led core、led device中间通过框架抽象粘合在了一起,这里的suspend过程就是这个框架在发挥作用。led device如何加入到框架中,稍后分析。

resume的流程也是一样的,把之前LED灯的brightness level恢复点亮就行了。其中brightness level(即对应LED device当前生效的亮度)存储在led_classdev->brightness

3.1.3. 注册sysfs class dev_groups,为设备添加属性节点

dev_groups的类型是struct attribute_group,它是class给其包含的设备添加的属性。LED class添加了brightness、max_brightness、trigger等节点,这里仅看看brightness节点。

static ssize_t brightness_show(struct device *dev,                         
                struct device_attribute *attr, char *buf)                  
{                                                                            
        struct led_classdev *led_cdev = dev_get_drvdata(dev);                 

        /* no lock needed for this */
        led_update_brightness(led_cdev);

        return sprintf(buf, "%u\n", led_cdev->brightness);
}

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);
        unsigned long state;
        ssize_t ret;

        mutex_lock(&led_cdev->led_access);

        if (led_sysfs_is_disabled(led_cdev)) {
                ret = -EBUSY;
                goto unlock;
        }

        ret = kstrtoul(buf, 10, &state);
        if (ret)
                goto unlock;

        if (state == LED_OFF)
                led_trigger_remove(led_cdev);
        led_set_brightness(led_cdev, state);

        ret = size;
unlock:
        mutex_unlock(&led_cdev->led_access);
        return ret;
}
static DEVICE_ATTR_RW(brightness);

brightness_show()的逻辑是通过led_update_brightness()调用LED设备驱动,获取brightness。

brightness_store()的逻辑会进行加锁、拷贝用户空间的输入值,调用led_set_brightness(),他是led-core提供的抽象API。

void led_set_brightness(struct led_classdev *led_cdev,
                        enum led_brightness brightness)             
{
        /*                                  
         * If software blink is active, delay brightness setting
         * until the next timer tick.
         */                                                                  
        if (test_bit(LED_BLINK_SW, &led_cdev->work_flags)) {
                /*                         
                 * If we need to disable soft blinking delegate this to the
                 * work queue task to avoid problems in case we are called
                 * from hard irq context.
                 */ 
                if (brightness == LED_OFF) {
                        set_bit(LED_BLINK_DISABLE, &led_cdev->work_flags);
                        schedule_work(&led_cdev->set_brightness_work);
                } else {       
                        set_bit(LED_BLINK_BRIGHTNESS_CHANGE,
                                &led_cdev->work_flags);
                        led_cdev->new_blink_brightness = brightness;
                }
                return;
        }          

        led_set_brightness_nosleep(led_cdev, brightness);
}
EXPORT_SYMBOL_GPL(led_set_brightness);

void led_set_brightness_nopm(struct led_classdev *led_cdev,
                              enum led_brightness value)
{
        /* Use brightness_set op if available, it is guaranteed not to sleep */
        if (!__led_set_brightness(led_cdev, value))
                return;

        /* If brightness setting can sleep, delegate it to a work queue task */
        led_cdev->delayed_set_value = value;
        schedule_work(&led_cdev->set_brightness_work);
}
EXPORT_SYMBOL_GPL(led_set_brightness_nopm);

void led_set_brightness_nosleep(struct led_classdev *led_cdev,
                                enum led_brightness value)
{
        led_cdev->brightness = min(value, led_cdev->max_brightness);

        if (led_cdev->flags & LED_SUSPENDED)
                return;

        led_set_brightness_nopm(led_cdev, led_cdev->brightness);
}
EXPORT_SYMBOL_GPL(led_set_brightness_nosleep);

如果此时LED是软件实现的Blink(通过一个工作队列实现的),则设置对应标志位,由工作队列来更新亮度。否则调用led_set_brightness_nosleep()

led_set_brightness_nosleep()的作用是跳过LED_SUSPENDED状态,设置brightness的操作仅在此之外的状态中生效。实际调用led_set_brightness_nopm()来设置brightness。

led_set_brightness_nopm()首先通过__led_set_brightness()检查LED是否注册了brightness_set函数。有则调用,完成brightness设置。否则调用set_brightness_work工作队列来完成。

static int __led_set_brightness(struct led_classdev *led_cdev,              
                                enum led_brightness value)                 
{                                               
        if (!led_cdev->brightness_set)
                return -ENOTSUPP;                                
                                                                        
        led_cdev->brightness_set(led_cdev, value);
                                                                       
        return 0;                                                                             
}

brightness_set是mtk_leds_drv.o模块在probe时,解析LED dts、创建mt65xx_led_data的时候赋值为mt65xx_led_set()

set_brightness_work是led-class.o在初始化led class时创建的,该延时队列执行的是set_brightness_delayed()。它首先调用和_nopm()相同的__led_set_brightness(),在失败的情况下会再尝试__led_set_brightness_blocking()

3.1.4. 小结

led-class.o创建了LED class,并为即将加入到该class的子节点们准备了brightness、max_brightness、trigger等属性(attribute),并将这些属性与对应的读写函数挂接起来。其中,挂接的函数实际是mtk_leds_drv.o和mtk_leds.o模块实现的。

3.2. LED驱动基础数据结构

我们知道Android启动过程实际上是经历了LK和Kernel两个阶段的。其中Kernel阶段会创建LED驱动节点。Kernel中除了定义了与LK一致的用于抽象LED模式和驱动方式的struct外,主要在此基础上增加了设备驱动模型的抽象,当前亮度等级,以及用于blink的支持。

struct mt65xx_led_data {
        struct led_classdev cdev;
        struct cust_mt65xx_led cust;
        struct work_struct work;
        int level;
        int delay_on;
        int delay_off;
};

其中led_classdev包含了blink、timer、trigger等除brightness等的功能,比较庞大。

struct led_classdev {                                          
        const char              *name;                
        enum led_brightness      brightness;                                   
        enum led_brightness      max_brightness;          
        int                      flags;                        
                                                            
        /* set_brightness_work / blink_timer flags, atomic, private. */    
        unsigned long           work_flags;            
                                                          
        /* Set LED brightness level                             
         * Must not sleep. Use brightness_set_blocking for drivers     
         * that can sleep while setting brightness.                      
         */                                                        
        void            (*brightness_set)(struct led_classdev *led_cdev,  
                                          enum led_brightness brightness);
        /*                                                              
         * Set LED brightness level immediately - it can block the caller for
         * the time required for accessing a LED device register.                    
         */                                                             
        int (*brightness_set_blocking)(struct led_classdev *led_cdev,
                                       enum led_brightness brightness);
        /* Get LED brightness level */
        enum led_brightness (*brightness_get)(struct led_classdev *led_cdev);

        /*
         * Activate hardware accelerated blink, delays are in milliseconds
         * and if both are zero then a sensible default should be chosen.
         * The call should adjust the timings in that case and if it can't
         * match the values specified exactly.
         * Deactivate blinking again when the brightness is set to LED_OFF
         * via the brightness_set() callback.
         */
        int             (*blink_set)(struct led_classdev *led_cdev,
                                     unsigned long *delay_on,
                                     unsigned long *delay_off);

        struct device           *dev;
        const struct attribute_group    **groups;

        struct list_head         node;                  /* LED Device list */
        const char              *default_trigger;       /* Trigger to use */

        unsigned long            blink_delay_on, blink_delay_off;
        struct timer_list        blink_timer;
        int                      blink_brightness;
        int                      new_blink_brightness;
        void                    (*flash_resume)(struct led_classdev *led_cdev);

        struct work_struct      set_brightness_work;
        int                     delayed_set_value;

#ifdef CONFIG_LEDS_TRIGGERS
        /* Protects the trigger data below */
        struct rw_semaphore      trigger_lock;

        struct led_trigger      *trigger;
        struct list_head         trig_list;
        void                    *trigger_data;
        /* true if activated - deactivate routine uses it to do cleanup */
        bool                    activated;
#endif

#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED
        int                      brightness_hw_changed;
        struct kernfs_node      *brightness_hw_changed_kn;
#endif

        /* Ensures consistent access to the LED Flash Class device */
        struct mutex            led_access;

3.3. LED驱动初始化

mtk_leds_drv.o模块的mt65xx_leds_probe完成对LED驱动的初始化。

首先读取LED配置(dts配置或列表配置),为配置的每个LED创建驱动数据结构,设置操作函数指针,初始化工作队列,最后调用LED节点注册函数。

static int mt65xx_leds_probe(struct platform_device *pdev)
{                
        int i;
        int ret;
        struct cust_mt65xx_led *cust_led_list = mt_get_cust_led_list();
                                              
        if (!cust_led_list) {               
                pr_info("[LED] get dts fail! Probe exit.\n");
                ret = -1;                                              
                goto err_dts;                                   
        }                                     
                                              
#ifdef CONFIG_BACKLIGHT_SUPPORT_LP8557
        /*i2c_register_board_info(4, &leds_board_info, 1);*/
        if (i2c_add_driver(&led_i2c_driver)) {
                LEDS_DRV_DEBUG("unable to add led-i2c driver.\n");
                ret = -1;
                goto err_dts;
        }
#endif                                                     
 
        for (i = 0; i < TYPE_TOTAL; i++) {
                if (cust_led_list[i].mode == MT65XX_LED_MODE_NONE) {
                        g_leds_data[i] = NULL;
                        continue;
                }

                g_leds_data[i] =
                    kzalloc(sizeof(struct mt65xx_led_data), GFP_KERNEL);
                if (!g_leds_data[i]) {
                        ret = -ENOMEM;
                        goto err;
                }

                g_leds_data[i]->cust.mode = cust_led_list[i].mode;
                g_leds_data[i]->cust.data = cust_led_list[i].data;
                g_leds_data[i]->cust.name = cust_led_list[i].name;

                g_leds_data[i]->cdev.name = cust_led_list[i].name;
                g_leds_data[i]->cust.config_data = cust_led_list[i].config_data;

                g_leds_data[i]->cdev.brightness_set = mt65xx_led_set;
                g_leds_data[i]->cdev.blink_set = mt65xx_blink_set;

                INIT_WORK(&g_leds_data[i]->work, mt_mt65xx_led_work);

                ret = led_classdev_register(&pdev->dev, &g_leds_data[i]->cdev);

                if (ret)
                        goto err;
        }
        ...
}

接下来通过led_classdev_register()注册LED设备驱动,注册每个LED驱动所需要的同步锁、驱动节点。

其中,每个LED是分别在for循环里面逐个单独地调用led_classdev_register()进行注册的。

函数如下。这个函数较长,包含几个关键步骤。下面先展示这个函数后再分解分析。

#define led_classdev_register(parent, led_cdev)                         \
        of_led_classdev_register(parent, NULL, led_cdev)



/**
 * of_led_classdev_register - register a new object of led_classdev class. 
 *                                                       
 * @parent: parent of LED device                                             
 * @led_cdev: the led_classdev structure for this device.                 
 * @np: DT node describing this LED       
 */                                                     
int of_led_classdev_register(struct device *parent, struct device_node *np,
                            struct led_classdev *led_cdev)                    
{                                                          
        char name[LED_MAX_NAME_SIZE];
        int ret;           
                                                            
        ret = led_classdev_next_name(led_cdev->name, name, sizeof(name));     
        if (ret < 0)                                                          
                return ret;               
                                                         
        led_cdev->dev = device_create_with_groups(leds_class, parent, 0,
                                led_cdev, led_cdev->groups, "%s", name);
        if (IS_ERR(led_cdev->dev))
                return PTR_ERR(led_cdev->dev);
        led_cdev->dev->of_node = np;

        if (ret)
                dev_warn(parent, "Led %s renamed to %s due to name collision",
                                led_cdev->name, dev_name(led_cdev->dev));

        if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) {
                ret = led_add_brightness_hw_changed(led_cdev);
                if (ret) {
                        device_unregister(led_cdev->dev);
                        return ret;
                }
        }

        led_cdev->work_flags = 0;
        #ifdef CONFIG_LEDS_TRIGGERS
        init_rwsem(&led_cdev->trigger_lock);
#endif
#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED
        led_cdev->brightness_hw_changed = -1;
#endif
        mutex_init(&led_cdev->led_access);
        /* add to the list of leds */
        down_write(&leds_list_lock);
        list_add_tail(&led_cdev->node, &leds_list);
        up_write(&leds_list_lock);

        if (!led_cdev->max_brightness)
                led_cdev->max_brightness = LED_FULL;

        led_update_brightness(led_cdev);

        led_init_core(led_cdev);

#ifdef CONFIG_LEDS_TRIGGERS
        led_trigger_set_default(led_cdev);
#endif

        dev_dbg(parent, "Registered led device: %s\n",
                        led_cdev->name);

        return 0;
}
EXPORT_SYMBOL_GPL(of_led_classdev_register);

of_led_classdev_register()可以分解为如下几个最主要的流程。

3.3.1. 查找并移除重名LED节点

led_classdev_next_name()查找LED class内重复注册同一个名称(函数参数里面的name)的节点。class中如果重复注册,第一个节点命名为LED名称(如”red”),其余重复的命名为”red_0”。这个函数通过class_find_device()查找到同名class节点后,通过put_device()将其从class节点集合中移除。

这里传入的参数中len == sizeof(name),因此只会将直接重名的LED class移除(如”red”),而不会移除按序号再命名的class节点如(”red_0”)。

函数返回小于0的时候,即枚举重名节点期间的缓冲区大小不够(注意缓冲区要有空间给字符串后面补’\0’才算满足大小;直接传入sizeof()大小是满足要求的。),此时没有可靠地检查并移除重名函数,因此提前退出,不创建节点。

函数返回值大于等于0时表示发现并移除了对应数量的重名和再命名节点。

static int led_classdev_next_name(const char *init_name, char *name,
                                  size_t len)
{
        unsigned int i = 0;
        int ret = 0;
        struct device *dev;

        strlcpy(name, init_name, len);

        while ((ret < len) &&
               (dev = class_find_device(leds_class, NULL, name, match_name))) {
                put_device(dev);
                ret = snprintf(name, len, "%s_%u", init_name, ++i);
        }

        if (ret >= len)
                return -ENOMEM;

        return i;
}

3.3.2. 创建LED节点

在移除同名节点后,通过device_create_with_groups创建sysfs LED class路径下的节点。它是Linux系统特地为创建sysfs class节点专用的函数。

/**
 * device_create_with_groups - creates a device and registers it with sysfs
 * @class: pointer to the struct class that this device should be registered to
 * @parent: pointer to the parent struct device of this new device, if any
 * @devt: the dev_t for the char device to be added
 * @drvdata: the data to be added to the device for callbacks
 * @groups: NULL-terminated list of attribute groups to be created
 * @fmt: string for the device's name
 *
 * This function can be used by char device classes.  A struct device
 * will be created in sysfs, registered to the specified class.
 * Additional attributes specified in the groups parameter will also
 * be created automatically.
 *
 * A "dev" file will be created, showing the dev_t for the device, if
 * the dev_t is not 0,0.
 * If a pointer to a parent struct device is passed in, the newly created
 * struct device will be a child of that device in sysfs.
 * The pointer to the struct device will be returned from the call.
 * Any further sysfs files that might be required can be created using this
 * pointer.
 *
 * Returns &struct device pointer on success, or ERR_PTR() on error.
 *
 * Note: the struct class passed to this function must have previously
 * been created with a call to class_create().
 */
struct device *device_create_with_groups(struct class *class,
                                         struct device *parent, dev_t devt,
                                         void *drvdata,
                                         const struct attribute_group **groups,
                                         const char *fmt, ...)
{
    ...
}
EXPORT_SYMBOL_GPL(device_create_with_groups);

LED创建它的sysfs LED class子节点的调用方式如下。创建的device指针存储在mt65xx_led_data->led_classdev->dev这一struct device*中。

led_cdev->dev = device_create_with_groups(leds_class, parent, 0,
                    led_cdev, led_cdev->groups, "%s", name);

至此,mtk_leds_drv.o就借助mt65xx_leds_probe()完成了MTK LED驱动的sysfs class注册和初始化了。

总结

MTK LED采用了Linux LED sysfs class,它通过dts进行配置,借助built-in module的方式加载。其中,LED class提供了各LED设备的attribute来实现brightness、trigger等节点的创建,并将store、show等读写函数挂接到mtk_leds_drv.o这一真正的LED class device中,实现了LED节点的创建、读写流程。

类图和流程图