Re: how to gracefully unload an i2c driver if chip not detected?

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



----- Mail original -----
> De: "Wolfram Sang" <wsa@xxxxxxxxxxxxx>
> À: "Émeric Vigier" <emeric.vigier@xxxxxxxxxxxxxxxxxxxx>
> Cc: linux-i2c@xxxxxxxxxxxxxxx
> Envoyé: Mardi 9 Avril 2013 05:26:25
> Objet: Re: how to gracefully unload an i2c driver if chip not detected?
> 
> 
> > I recently changed the sensor on my board. Leading to kernel crash
> > when entering suspend. Thanks to "no_console_suspend" cmdline
> > argument, I found out that the suspend function of the "absent"
> > chip
> > gets called. It tries to take a mutex which has been freed in
> > probe's
> > "device not found" fallback code. Leading to kernel panic.
> 
> Can we have the full driver? The snipplet is not enough.

Here you go:

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/interrupt.h>
#include <linux/delay.h>
#include <linux/input.h>
#include <linux/pm.h>
#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/workqueue.h>
#include <linux/i2c.h>
#include <linux/i2c/vcnl4010.h>
#include <linux/gpio.h>

#define VCNL4010_DEBUG 1

#define DEVICE_NAME "vcnl4010"
#define DRIVER_NAME "vcnl4010"

#define VCNL4010_CMD_REG 0x80
#define VCNL4010_PROD_ID_VER 0x81
#define VCNL4010_PROX_RATE 0X82
#define VCNL4010_IR_LED_CURR 0x83
#define VCNL4010_AMB_LIGHT_PARAM 0x84
#define VCNL4010_MSB_AMB_LIGHT_RESULT 0x85
#define VCNL4010_LSB_AMB_LIGHT_RESULT 0x86
#define VCNL4010_MSB_PROX_MEASURE_RESULT 0x87
#define VCNL4010_LSB_PROX_MEASURE_RESULT 0x88
#define VCNL4010_INTERRUPT_CTRL 0x89
#define VCNL4010_MSB_LOW_THRESHOLD 0x8A
#define VCNL4010_LSB_LOW_THRESHOLD 0x8B
#define VCNL4010_MSB_HIGH_THRESHOLD 0x8C
#define VCNL4010_LSB_HIGH_THRESHOLD 0x8D
#define VCNL4010_INTERRUPT_STATUS 0x8E
#define VCNL4010_PROX_MODU_TIMING_ADJ 0x8F

typedef enum {eNear, eFar, eNone} vcnl4010InterruptState;
#define VCNL4010_INT_PROX_READY 0x08
#define VCNL4010_INT_ALS_READY  0x04
#define VCNL4010_INT_THRES_LOW  0x02
#define VCNL4010_INT_THRES_HI   0x01

#define VCNL4010_FAR_DISTANCE 10

struct vcnl4010_data
{
    struct vcnl4010_platform_data *pdata;
    struct i2c_client *client;
    struct input_dev *input_dev;
    struct delayed_work wq;
    struct mutex lock;
    uint32_t self_refresh_poll_rate;
    vcnl4010InterruptState last_interrupt_state;
    uint16_t prox_offset;
    uint16_t threshold_low;
    uint16_t threshold_hi;
};

static struct task_struct *proximity_intr;

//static uint32_t prox_debug;
//module_param(prox_debug, uint, 0664);
//module_param_named(vcnl4010_debug, prox_debug, uint, 0664);
//MODULE_PARM_DESC(vcnl4010_debug, "VCNL4010 Debug");

#ifdef VCNL4010_DEBUG
struct vcnl4010_reg
{
    const char *name;
    uint8_t reg;
    int writeable;
} vcnl4010_regs[] = {
    { "CMD_REG",                VCNL4010_CMD_REG, 0 },
    { "VERSION",                VCNL4010_PROD_ID_VER, 0 },
    { "VCNL4010_PROX_RATE",            VCNL4010_PROX_RATE, 0 },
    { "IR_LED_CURR",            VCNL4010_IR_LED_CURR, 0 },
    { "AMB_LIGHT_PARAM",            VCNL4010_AMB_LIGHT_PARAM, 0 },
    { "MSB_AMB_LIGHT_RESULT",        VCNL4010_MSB_AMB_LIGHT_RESULT, 0 },
    { "LSB_AMB_LIGHT_RESULT",        VCNL4010_LSB_AMB_LIGHT_RESULT, 0 },
    { "MSB_PROX_MEASURE_RESULT",        VCNL4010_MSB_PROX_MEASURE_RESULT, 0 },
    { "LSB_PROX_MEASURE_RESULT",        VCNL4010_LSB_PROX_MEASURE_RESULT, 0 },
    { "VCNL4010_INTERRUPT_CTRL",        VCNL4010_INTERRUPT_CTRL, 0 },
    { "VCNL4010_MSB_LOW_THRESHOLD",        VCNL4010_MSB_LOW_THRESHOLD, 0 },
    { "VCNL4010_LSB_LOW_THRESHOLD",        VCNL4010_LSB_LOW_THRESHOLD, 0 },
    { "VCNL4010_MSB_HIGH_THRESHOLD",    VCNL4010_MSB_HIGH_THRESHOLD, 0 },
    { "VCNL4010_LSB_HIGH_THRESHOLD",    VCNL4010_LSB_HIGH_THRESHOLD, 0 },
    { "VCNL4010_INTERRUPT_STATUS",        VCNL4010_INTERRUPT_STATUS, 0 },
    { "VCNL4010_PROX_MODU_TIMING_ADJ",    VCNL4010_PROX_MODU_TIMING_ADJ, 0 },
};
#endif

static int vcnl4010_write(struct vcnl4010_data *data, u8 reg, u8 val)
{
    int ret = 0;

    ret = i2c_smbus_write_byte_data(data->client, reg, val);
    if (ret < 0)
        dev_err(&data->client->dev,
            "i2c_smbus_write_byte_data failed\n");
    return ret;
}

static int vcnl4010_read_transfer(struct vcnl4010_data *data,
        unsigned short data_addr, char *data_buf, int count)
{
    int ret;
    int counter = 5;
    char *data_buffer = data_buf;

    struct i2c_msg msgs[] = {
        {
        .addr = data->client->addr,
        .flags = data->client->flags,
        .len = 1,
        .buf = data_buffer,
        },
        {
        .addr = data->client->addr,
        .flags = (data->client->flags) | I2C_M_RD,
        .len = count,
        .buf = data_buffer,
        },
    };

    data_buffer[0] = data_addr;
    msgs->buf = data_buffer;

    do {
        ret = i2c_transfer(data->client->adapter, msgs, 2);
        if (ret != 2)
        {
            dev_err(&data->client->dev,
                "i2c_transfer failed\n");
            counter--;
            msleep(1);
        }
        else
        {
            return 0;
        }
    } while (counter >= 0);

    return -1;
}

static void vcnl4010_enter_self_timed_mode(struct vcnl4010_data *data)
{
    int ret = 0;

    pr_info("vcnl4010: Enter self refresh mode");

    mutex_lock(&data->lock);
    ret = ret || vcnl4010_write(data, VCNL4010_CMD_REG,              0x00);
    ret = ret || vcnl4010_write(data, VCNL4010_IR_LED_CURR,            20);
    ret = ret || vcnl4010_write(data, VCNL4010_INTERRUPT_CTRL,       0x62);
    ret = ret || vcnl4010_write(data, VCNL4010_PROX_MODU_TIMING_ADJ, 0x01);
    ret = ret || vcnl4010_write(data, VCNL4010_PROX_RATE,            0x04);
    ret = ret || vcnl4010_write(data, VCNL4010_CMD_REG,              0x03);
    mutex_unlock(&data->lock);

    if(ret != 0)
        pr_err("vcnl4010: Failed to enter self refresh mode");
}

static uint32_t vcnl4010_poll_prox_value(struct vcnl4010_data *data)
{
    uint8_t reg_val, timeout;
    uint32_t data_val = 0;

    //device refreshs itself 31 times per secs, if value not ready, wait a little bit and try again
    for(timeout = 0; timeout < 10; timeout++)
    {
        mutex_lock(&data->lock);
        vcnl4010_read_transfer(data, VCNL4010_CMD_REG, &reg_val, 1);
        if(reg_val & 0x20)
        {
            vcnl4010_read_transfer(data, VCNL4010_MSB_PROX_MEASURE_RESULT, &reg_val, 1);
            data_val = reg_val << 8;

            vcnl4010_read_transfer(data, VCNL4010_LSB_PROX_MEASURE_RESULT, &reg_val, 1);
            data_val |= reg_val;
            mutex_unlock(&data->lock);
            break;
        }
        mutex_unlock(&data->lock);
        msleep_interruptible(8);
    }

    if(timeout == 10)
        pr_err("vcnl4010: get prox value failed");

    return data_val;
}

static vcnl4010InterruptState vcnl4010_poll_interrupt_value(struct vcnl4010_data *data)
{
    vcnl4010InterruptState retVal = eNone;
    uint8_t reg_val = 0;

    mutex_lock(&data->lock);
    if(vcnl4010_read_transfer(data, VCNL4010_INTERRUPT_STATUS, &reg_val, 1) == 0)
    {
        if(reg_val & VCNL4010_INT_THRES_HI)
            retVal = eNear;
        if(reg_val & VCNL4010_INT_THRES_LOW)
            retVal = eFar;

        if((reg_val & (VCNL4010_INT_THRES_HI | VCNL4010_INT_THRES_LOW)) != 0)
        {
            vcnl4010_write(data, VCNL4010_INTERRUPT_STATUS, VCNL4010_INT_THRES_HI || VCNL4010_INT_THRES_LOW);
        }
    }

    mutex_unlock(&data->lock);
    return retVal;
}

static void vcnl4010_set_int_threshold(struct vcnl4010_data *data, vcnl4010InterruptState type)
{
    uint16_t threshold_hi;
    uint16_t threshold_low;

    if(type == eNear)
    {
        threshold_hi = data->prox_offset + data->threshold_hi;
        threshold_low = 0;
    }
    else
    {
        threshold_hi = 0xffff;
        threshold_low = data->prox_offset + data->threshold_low;
    }

    mutex_lock(&data->lock);
    vcnl4010_write(data, VCNL4010_LSB_LOW_THRESHOLD, threshold_low & 0xff);
    vcnl4010_write(data, VCNL4010_MSB_LOW_THRESHOLD, (threshold_low >> 8) & 0xff);
    vcnl4010_write(data, VCNL4010_LSB_HIGH_THRESHOLD, threshold_hi & 0xff);
    vcnl4010_write(data, VCNL4010_MSB_HIGH_THRESHOLD, (threshold_hi >> 8) & 0xff);
    mutex_unlock(&data->lock);
}

static int proximity_thread(void *data)
{
    struct vcnl4010_data *prox_data = data;

    while (!kthread_should_stop())
    {
        vcnl4010InterruptState lState = vcnl4010_poll_interrupt_value(prox_data);
        if(lState != eNone && lState != prox_data->last_interrupt_state)
        {
            prox_data->last_interrupt_state = lState;
            if(lState == eFar)
            {
                input_report_abs (prox_data->input_dev, ABS_DISTANCE, VCNL4010_FAR_DISTANCE);
                input_sync(prox_data->input_dev);
                vcnl4010_set_int_threshold(prox_data, eNear);
            }
            else
            {
                input_report_abs (prox_data->input_dev, ABS_DISTANCE, 0);
                input_sync(prox_data->input_dev);
                vcnl4010_set_int_threshold(prox_data, eFar);
            }
        }
        msleep_interruptible (prox_data->self_refresh_poll_rate); //250ms
    }
    return 0;
}

static ssize_t vcnl4010_get_raw_prox(struct device *dev, struct device_attribute *attr, char *buf)
{
    uint32_t data_val;
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);

    data_val = vcnl4010_poll_prox_value(data);

    return sprintf(buf, "%d\n", data_val);
}

static ssize_t vcnl4010_get_offset(struct device *dev,
                  struct device_attribute *attr, char *buf)
{
    unsigned n;
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    n = scnprintf(buf, PAGE_SIZE, "%d", data->prox_offset);

    return n;
}

static ssize_t vcnl4010_set_offset(struct device *dev,
                   struct device_attribute *attr,
                   const char *buf, size_t count)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);

    int value;
    if (sscanf(buf, "%d", &value) != 1) {
        pr_err("%s:unable to parse input\n", __func__);
        return -1;
    }
    if(value > 0xffff || value < 0)
    {
        pr_err("%s:invalid offset value\n", __func__);
        return -1;
    }

    data->prox_offset = value;

    if(data->last_interrupt_state == eFar)
        vcnl4010_set_int_threshold(data, eNear);
    else
        vcnl4010_set_int_threshold(data, eFar);

    return count;
}

static ssize_t vcnl4010_get_threshold_hi(struct device *dev,
                  struct device_attribute *attr, char *buf)
{
    unsigned n;
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    n = scnprintf(buf, PAGE_SIZE, "%d", data->threshold_hi);

    return n;
}

static ssize_t vcnl4010_set_threshold_hi(struct device *dev,
                   struct device_attribute *attr,
                   const char *buf, size_t count)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);

    int value;
    if (sscanf(buf, "%d", &value) != 1) {
        pr_err("%s:unable to parse input\n", __func__);
        return -1;
    }
    if(value > 0xffff || value < 0)
    {
        pr_err("%s:invalid threshold value\n", __func__);
        return -1;
    }

    data->threshold_hi = value;

    if(data->last_interrupt_state == eFar)
        vcnl4010_set_int_threshold(data, eNear);
    else
        vcnl4010_set_int_threshold(data, eFar);

    return count;
}

static ssize_t vcnl4010_get_threshold_low(struct device *dev,
                  struct device_attribute *attr, char *buf)
{
    unsigned n;
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    n = scnprintf(buf, PAGE_SIZE, "%d", data->threshold_low);

    return n;
}

static ssize_t vcnl4010_set_threshold_low(struct device *dev,
                   struct device_attribute *attr,
                   const char *buf, size_t count)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);

    int value;
    if (sscanf(buf, "%d", &value) != 1) {
        pr_err("%s:unable to parse input\n", __func__);
        return -1;
    }
    if(value > 0xffff || value < 0)
    {
        pr_err("%s:invalid threshold value\n", __func__);
        return -1;
    }

    data->threshold_low = value;

    if(data->last_interrupt_state == eFar)
        vcnl4010_set_int_threshold(data, eNear);
    else
        vcnl4010_set_int_threshold(data, eFar);

    return count;
}

#ifdef VCNL4010_DEBUG
static ssize_t vcnl4010_registers_show(struct device *dev,
                  struct device_attribute *attr, char *buf)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    unsigned i, n, reg_count;
    uint8_t value;

    mutex_lock(&data->lock);
    reg_count = sizeof(vcnl4010_regs) / sizeof(vcnl4010_regs[0]);
    for (i = 0, n = 0; i < reg_count; i++)
    {
        vcnl4010_read_transfer(data, vcnl4010_regs[i].reg, &value, 1);
        n += scnprintf(buf + n, PAGE_SIZE - n, "%-20s = 0x%02X\n",
                   vcnl4010_regs[i].name,
                   value);
    }
    mutex_unlock(&data->lock);

    return n;
}

static ssize_t vcnl4010_registers_store(struct device *dev,
                   struct device_attribute *attr,
                   const char *buf, size_t count)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    unsigned i, reg_count, value;
    int error = 0;
    char name[30];

    if (count >= 30) {
        pr_err("%s:input too long\n", __func__);
        return -1;
    }

    if (sscanf(buf, "%s %x", name, &value) != 2) {
        pr_err("%s:unable to parse input\n", __func__);
        return -1;
    }

    mutex_lock(&data->lock);
    reg_count = sizeof(vcnl4010_regs) / sizeof(vcnl4010_regs[0]);
    for (i = 0; i < reg_count; i++) {
        if (!strcmp(name, vcnl4010_regs[i].name)) {
            if (vcnl4010_regs[i].writeable) {
                error = vcnl4010_write(data,
                    vcnl4010_regs[i].reg,
                    value);
                if (error) {
                    pr_err("%s:Failed to write %s\n",
                        __func__, name);
                    mutex_unlock(&data->lock);
                    return -1;
                }
            } else {
                pr_err("%s:Register %s is not writeable\n",
                        __func__, name);
                mutex_unlock(&data->lock);
                return -1;
            }
            mutex_unlock(&data->lock);
            return count;
        }
    }
    mutex_unlock(&data->lock);

    pr_err("%s:no such register %s\n", __func__, name);
    return -1;
}

static DEVICE_ATTR(registers, S_IWUSR | S_IRUGO,
        vcnl4010_registers_show, vcnl4010_registers_store);
#endif


//only root
//#define FILE_PERMS S_IWUSR | S_IRUGO
//all users
#define FILE_PERMS S_IWUGO | S_IRUGO

static DEVICE_ATTR(prox_value, FILE_PERMS, vcnl4010_get_raw_prox, NULL);
static DEVICE_ATTR(prox_offset, FILE_PERMS, vcnl4010_get_offset, vcnl4010_set_offset);
static DEVICE_ATTR(threshold_low, FILE_PERMS, vcnl4010_get_threshold_low, vcnl4010_set_threshold_low);
static DEVICE_ATTR(threshold_hi, FILE_PERMS, vcnl4010_get_threshold_hi, vcnl4010_set_threshold_hi);

static struct attribute *vcnl4010_attrs[] =
{
    &dev_attr_prox_value.attr,
    &dev_attr_prox_offset.attr,
    &dev_attr_threshold_low.attr,
    &dev_attr_threshold_hi.attr,
#ifdef VCNL4010_DEBUG
    &dev_attr_registers.attr,
#endif
    NULL
};

static const struct attribute_group vcnl4010_attr_group = {
    .attrs = vcnl4010_attrs,
};

static int __devinit vcnl4010_driver_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct vcnl4010_platform_data *pdata = client->dev.platform_data;
    struct vcnl4010_data *data;
    int ret = 0;
    uint8_t reg_val = 0;

    pr_info("%s: Enter\n", __func__);

    if (pdata == NULL) {
        pr_err("%s: Platform data not found\n", __func__);
        return -ENODEV;
    }

    if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) {
        pr_err("%s: need I2C_FUNC_I2C\n", __func__);
        return -ENODEV;
    }

    /* alloc memory for data structure */
    data = kzalloc(sizeof(struct vcnl4010_data), GFP_KERNEL);
    if (!data) {
        ret = -ENOMEM;
        goto error;
    }

    mutex_init(&data->lock);

    data->input_dev = input_allocate_device();

    if (!data->input_dev)
    {
        ret = -ENOMEM;
        printk("%s: input device allocate failed: %d\n", __func__, ret);
        goto dev_allocate_error;
    }

    data->input_dev->name = "proximity";
    set_bit(EV_ABS, data->input_dev->evbit);
    set_bit(ABS_DISTANCE, data->input_dev->absbit);
    data->input_dev->phys = "proximity/input";

    input_set_abs_params( data->input_dev, ABS_DISTANCE, 0, VCNL4010_FAR_DISTANCE, 0, 0 );

    ret = input_register_device(data->input_dev);
    if (ret) {
        printk("%s: input device register failed:%d\n", __func__, ret);
        goto dev_register_error;
    }

    data->pdata = pdata;
    data->client = client;
    data->self_refresh_poll_rate = pdata->def_poll_rate;
    data->last_interrupt_state = eFar;  //Start as far
    data->prox_offset = 17000;  //todo set correct default value
    data->threshold_low = 105;
    data->threshold_hi =  110;

    i2c_set_clientdata(client, data);

    //test for device presence
    if(vcnl4010_read_transfer(data, VCNL4010_PROD_ID_VER, &reg_val, 1) !=0 )
    {
        pr_err("vcnl4010: Device not found!");
        goto nochip;
    }

    if(reg_val != 0x21)
    {
        pr_err("vcnl4010: Found device isn't a vcnl4010, is a vcnl4000 installed?");
        goto badchip;
    }

    pr_info("vcnl4010: Found device!");

    vcnl4010_set_int_threshold(data, eNear);  //Generate an interrupt when become near
    vcnl4010_enter_self_timed_mode(data);

    input_report_abs (data->input_dev, ABS_DISTANCE, VCNL4010_FAR_DISTANCE);
    input_sync(data->input_dev);

    ret = sysfs_create_group(&client->dev.kobj, &vcnl4010_attr_group);
    if (ret)
        goto sysfs_error;

    /* start the kthread */
    proximity_intr = kthread_run(proximity_thread, data, "proximity_thread");

    return 0;

sysfs_error:
badchip:
nochip:
    input_unregister_device(data->input_dev);
dev_register_error:
    input_free_device(data->input_dev);
dev_allocate_error:
    mutex_destroy(&data->lock);
    kfree(data);
error:
    return ret;
}

static int __devexit vcnl4010_driver_remove(struct i2c_client *client)
{
    struct vcnl4010_data *data = i2c_get_clientdata(client);
    int ret = 0;

    sysfs_remove_group(&client->dev.kobj, &vcnl4010_attr_group);

    i2c_set_clientdata(client, NULL);

    input_free_device(data->input_dev);
    mutex_destroy(&data->lock);
    kfree(data);

    return ret;
}


#ifdef CONFIG_PM
static int vcnl4010_driver_suspend(struct device *dev)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    mutex_lock(&data->lock);
    vcnl4010_write(data, VCNL4010_CMD_REG,              0x00);
    vcnl4010_write(data, VCNL4010_IR_LED_CURR,             0);
    mutex_unlock(&data->lock);

    return 0;
}

static int vcnl4010_driver_resume(struct device *dev)
{
    struct platform_device *pdev = to_platform_device(dev);
    struct vcnl4010_data *data = platform_get_drvdata(pdev);
    vcnl4010_enter_self_timed_mode(data);
    if(data->last_interrupt_state == eFar)
        vcnl4010_set_int_threshold(data, eNear);
    else
        vcnl4010_set_int_threshold(data, eFar);

    return 0;
}

static const struct dev_pm_ops vcnl4010_pm_ops = {
    .suspend = vcnl4010_driver_suspend,
    .resume = vcnl4010_driver_resume,
};
#endif

static const struct i2c_device_id vcnl4010_idtable[] = {
    { DRIVER_NAME, 0 },
    { },
};

MODULE_DEVICE_TABLE(i2c, vcnl4010_idtable);

static struct i2c_driver vcnl4010_driver = {
    .probe        = vcnl4010_driver_probe,
    .remove        = vcnl4010_driver_remove,
    .id_table    = vcnl4010_idtable,
    .driver = {
        .name = DRIVER_NAME,
#ifdef CONFIG_PM
        .pm = &vcnl4010_pm_ops,
#endif
    },
};

static int __init vcnl4010_driver_init(void)
{
    return i2c_add_driver(&vcnl4010_driver);
}

static void __exit vcnl4010_driver_exit(void)
{
    i2c_del_driver(&vcnl4010_driver);
}

module_init(vcnl4010_driver_init);
module_exit(vcnl4010_driver_exit);

MODULE_DESCRIPTION("VCNL4010 Proximity Driver");
MODULE_LICENSE("GPL");
--
To unsubscribe from this list: send the line "unsubscribe linux-i2c" in
the body of a message to majordomo@xxxxxxxxxxxxxxx
More majordomo info at  http://vger.kernel.org/majordomo-info.html




[Index of Archives]     [Linux GPIO]     [Linux SPI]     [Linux Hardward Monitoring]     [LM Sensors]     [Linux USB Devel]     [Linux Media]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]

  Powered by Linux