1066 lines
26 KiB
C
1066 lines
26 KiB
C
|
/*
|
||
|
* drivers/leds/is31fl3736.c
|
||
|
* Driver for ISSI is31fl3736 of I2C LED controllers
|
||
|
*
|
||
|
* Copyright (C) 2018 Allwinner Technology Limited. All rights reserved.
|
||
|
*
|
||
|
*
|
||
|
* This program is free software; you can redistribute it and/or modify
|
||
|
* it under the terms of the GNU General Public License version 2 as
|
||
|
* published by the Free Software Foundation.
|
||
|
*
|
||
|
*/
|
||
|
|
||
|
#include <linux/device.h>
|
||
|
#include <linux/i2c.h>
|
||
|
#include <linux/kernel.h>
|
||
|
#include <linux/leds.h>
|
||
|
#include <linux/module.h>
|
||
|
#include <linux/delay.h>
|
||
|
#include <linux/of.h>
|
||
|
#include <linux/of_device.h>
|
||
|
#include <linux/gpio.h>
|
||
|
#include <linux/sunxi-gpio.h>
|
||
|
#include <linux/of_gpio.h>
|
||
|
#include <linux/io.h>
|
||
|
|
||
|
#include "leds-is31fl3736.h"
|
||
|
|
||
|
#define IS31FL3736_WRITE_BIT 0x00
|
||
|
#define IS31FL3736_READ_BIT 0x01
|
||
|
#define IS31FL3736_ACK_CHECK_EN 1
|
||
|
#define IS31FL3736_READ_ACK 0x0 /*!< I2C ack value */
|
||
|
#define IS31FL3736_READ_NACK 0x1 /*!< I2C nack value */
|
||
|
#define IS31FL3736_CMD_WRITE_EN 0xC5 /*!< magic num */
|
||
|
#define IS31FL3736_I2C_ID 0xA0 /*!< I2C Addr,up to ADDR1/ADDR2 pin*/
|
||
|
|
||
|
/* Used to indicate a device has no such register */
|
||
|
#define IS31FL37XX_REG_NONE 0xFF
|
||
|
|
||
|
/*!< max channel num in the PWM mode */
|
||
|
#define IS31FL3736_PWM_CHANNEL_MAX (0XBE)
|
||
|
/*!< in LED matrix mode, there are 8 current sources channel */
|
||
|
#define IS31FL3736_CSX_MAX (8)
|
||
|
#define IS31FL3736_CSX_MAX_MASK (0xff)
|
||
|
/*!< in LED matrix mode, there are 12 switch channel */
|
||
|
#define IS31FL3736_SWY_MAX (12)
|
||
|
#define IS31FL3736_SWY_MAX_MASK (0xfff)
|
||
|
|
||
|
#define IS31FL3736_PAGE(i) (i) /*!< range 0 ~ 3 */
|
||
|
|
||
|
enum is31fl3736_t1_t3_time {
|
||
|
IS31FL3736_t1_t3_time_0 = 0,/*!< t1/t3, 0.21s */
|
||
|
IS31FL3736_t1_t3_time_1 = 0x01, /*!< t1/t3, 0.42s */
|
||
|
IS31FL3736_t1_t3_time_2 = 0x02, /*!< t1/t3, 0.84s */
|
||
|
IS31FL3736_t1_t3_time_3 = 0x03, /*!< t1/t3, 1.68s*/
|
||
|
IS31FL3736_t1_t3_time_4 = 0x04, /*!< t1/t3, 3.36s */
|
||
|
IS31FL3736_t1_t3_time_5 = 0x05, /*!< t1/t3, 6.72s */
|
||
|
IS31FL3736_t1_t3_time_6 = 0x06, /*!< t1/t3, 13.44s */
|
||
|
IS31FL3736_t1_t3_time_7 = 0x07, /*!< t1/t3, 26.88s */
|
||
|
IS31FL3736_t1_t3_time_MAX,
|
||
|
};
|
||
|
|
||
|
enum is31fl3736_t2_time {
|
||
|
IS31FL3736_t2_time_0 = 0,/*!< t2, 0s */
|
||
|
IS31FL3736_t2_time_1 = 0x01, /*!< t2, 0.21s */
|
||
|
IS31FL3736_t2_time_2 = 0x02, /*!< t2, 0.42s */
|
||
|
IS31FL3736_t2_time_3 = 0x03, /*!< t2, 0.84s */
|
||
|
IS31FL3736_t2_time_4 = 0x04, /*!< t2, 1.68s */
|
||
|
IS31FL3736_t2_time_5 = 0x05, /*!< t2, 3.36s */
|
||
|
IS31FL3736_t2_time_6 = 0x06, /*!< t2, 6.72s */
|
||
|
IS31FL3736_t2_time_7 = 0x07, /*!< t2, 13.44s */
|
||
|
IS31FL3736_t2_time_8 = 0x08, /*!< t2, 26.88s */
|
||
|
IS31FL3736_t2_time_MAX,
|
||
|
};
|
||
|
|
||
|
enum is31fl3736_t4_time {
|
||
|
IS31FL3736_t4_time_0 = 0,/*!< t4, 0s */
|
||
|
IS31FL3736_t4_time_1 = 0x01, /*!< t4, 0.21s */
|
||
|
IS31FL3736_t4_time_2 = 0x02, /*!< t4, 0.42s */
|
||
|
IS31FL3736_t4_time_3 = 0x03, /*!< t4, 0.84s */
|
||
|
IS31FL3736_t4_time_4 = 0x04, /*!< t4, 1.68s */
|
||
|
IS31FL3736_t4_time_5 = 0x05, /*!< t4, 3.36s */
|
||
|
IS31FL3736_t4_time_6 = 0x06, /*!< t4, 6.72s */
|
||
|
IS31FL3736_t4_time_7 = 0x07, /*!< t4, 13.44s */
|
||
|
IS31FL3736_t4_time_8 = 0x08, /*!< t4, 26.88s */
|
||
|
IS31FL3736_t4_time_9 = 0x09, /*!< t4, 53.76s */
|
||
|
IS31FL3736_t4_time_10 = 0x10, /*!< t4, 107.52s */
|
||
|
IS31FL3736_t4_time_MAX,
|
||
|
};
|
||
|
|
||
|
enum is31fl3736_led_state {
|
||
|
IS31FL3736_LED_OFF = 0, /*!< The resistor value */
|
||
|
IS31FL3736_LED_ON = 1, /*!< The resistor value */
|
||
|
IS31FL3736_LED_MAX,
|
||
|
};
|
||
|
|
||
|
enum is31fl3736_auto_breath_mode {
|
||
|
IS31FL3736_PWM_MODE = 0, /*!< The resistor value */
|
||
|
IS31FL3736_ABM1 = 1, /*!< The resistor value */
|
||
|
IS31FL3736_ABM2 = 2, /*!< The resistor value */
|
||
|
IS31FL3736_ABM3 = 3, /*!< The resistor value */
|
||
|
IS31FL3736_ABM_MAX,
|
||
|
};
|
||
|
|
||
|
struct is31fl3736_led_data {
|
||
|
struct led_classdev cdev;
|
||
|
u8 channel; /* 1-based, max */
|
||
|
u8 sw_ch;
|
||
|
u8 cs_ch;
|
||
|
/*Auto breath control time set*/
|
||
|
u8 rise_time; /*t1*/
|
||
|
u8 hold_time; /*t2*/
|
||
|
u8 fall_time; /*t3*/
|
||
|
u8 off_time; /*t4*/
|
||
|
u8 cur_duty; /*pwm duty*/
|
||
|
u32 loop;/*blink time, if 0, endless blink*/
|
||
|
enum is31fl3736_led_state cur_state;
|
||
|
enum is31fl3736_auto_breath_mode cur_mode;
|
||
|
struct is31fl3736_priv *priv;
|
||
|
};
|
||
|
|
||
|
struct is31fl3736_priv {
|
||
|
u32 power_gpio;
|
||
|
u32 shdn_gpio;
|
||
|
u32 int_gpio;
|
||
|
u16 sw_y[IS31FL3736_SWY_MAX];
|
||
|
u16 num_leds;
|
||
|
u8 cs_max;
|
||
|
u8 sw_max;
|
||
|
struct i2c_client *client;
|
||
|
struct mutex mutex;
|
||
|
struct is31fl3736_led_data leds[0];
|
||
|
};
|
||
|
|
||
|
#define IS31Fl3736_ATTR_RW(_name) \
|
||
|
DEVICE_ATTR(_name, 0644, is31fl3736_led_show_##_name, \
|
||
|
is31fl3736_led_store_##_name)
|
||
|
|
||
|
static int is31fl3736_read_byte(struct is31fl3736_priv *priv,
|
||
|
u8 reg, u8 *rt_value)
|
||
|
{
|
||
|
int ret;
|
||
|
u8 read_cmd[3] = { 0 };
|
||
|
u8 cmd_len = 0;
|
||
|
|
||
|
read_cmd[0] = reg;
|
||
|
cmd_len = 1;
|
||
|
|
||
|
if (priv->client->adapter == NULL)
|
||
|
pr_err("is31fl3736_read client->adapter==NULL\n");
|
||
|
|
||
|
ret = i2c_master_send(priv->client, read_cmd, cmd_len);
|
||
|
if (ret != cmd_len) {
|
||
|
pr_err("is31fl3736_read error1\n");
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
ret = i2c_master_recv(priv->client, rt_value, 1);
|
||
|
if (ret != 1) {
|
||
|
pr_err("is31fl3736_read error2, ret = %d.\n", ret);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int is31fl3736_write_byte(struct is31fl3736_priv *priv,
|
||
|
u8 reg, u8 value)
|
||
|
{
|
||
|
int ret = 0;
|
||
|
u8 write_cmd[2] = { 0 };
|
||
|
|
||
|
write_cmd[0] = reg;
|
||
|
write_cmd[1] = value;
|
||
|
|
||
|
ret = i2c_master_send(priv->client, write_cmd, 2);
|
||
|
if (ret != 2) {
|
||
|
pr_err("is31fl3736_write error->[REG-0x%02x,val-0x%02x]\n",
|
||
|
reg, value);
|
||
|
return -1;
|
||
|
}
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief change led channels on/off state
|
||
|
*/
|
||
|
static int is31fl3736_set_led_state(struct is31fl3736_priv *priv,
|
||
|
u8 cs_x, u8 sw_y, enum is31fl3736_led_state state)
|
||
|
{
|
||
|
u8 reg, reg_val;
|
||
|
int ret = 0;
|
||
|
|
||
|
pr_debug("%s: cs: %d, sw: %d, state: %d\n",
|
||
|
__func__, cs_x, sw_y, state);
|
||
|
|
||
|
if (cs_x > IS31FL3736_CSX_MAX || cs_x < 1) {
|
||
|
pr_err("%s: cs channel error, cs_x: %d\n", __func__, cs_x);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (sw_y > IS31FL3736_SWY_MAX || sw_y < 1) {
|
||
|
pr_err("%s: sw channel error, sw_y: %d\n", __func__, sw_y);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(0));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
reg = (sw_y - 1) * 2 + ((cs_x - 1) / 4);
|
||
|
|
||
|
if (state == IS31FL3736_LED_ON)
|
||
|
*(priv->sw_y + sw_y - 1) |= state << (cs_x - 1) * 2;
|
||
|
else
|
||
|
*(priv->sw_y + sw_y - 1) &= state << (cs_x - 1) * 2;
|
||
|
|
||
|
if (cs_x > 4)
|
||
|
reg_val = (*(priv->sw_y + sw_y - 1) >> 8) & 0xff;//high 8 bit
|
||
|
else
|
||
|
reg_val = *(priv->sw_y + sw_y - 1) & 0xff;//low 8 bit
|
||
|
|
||
|
pr_debug("%s: reg 0x%02x, val 0x%02x\n", __func__, reg, reg_val);
|
||
|
return is31fl3736_write_byte(priv, reg, reg_val);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief change channels pwm duty register
|
||
|
*/
|
||
|
static int is31fl3736_set_pwm_duty(struct is31fl3736_priv *priv,
|
||
|
u8 cs_x, u8 sw_y, u8 duty)
|
||
|
{
|
||
|
u8 reg;
|
||
|
int ret = 0;
|
||
|
|
||
|
if (cs_x > IS31FL3736_CSX_MAX || cs_x < 1) {
|
||
|
pr_err("%s: cs channel error, cs_x: %d\n", __func__, cs_x);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (sw_y > IS31FL3736_SWY_MAX || sw_y < 1) {
|
||
|
pr_err("%s: sw channel error, sw_y: %d\n", __func__, sw_y);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(1));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
reg = 0x10 * (sw_y - 1) + 0x02 * (cs_x - 1);
|
||
|
pr_debug("%s: reg 0x%02x, duty %d\n", __func__, reg, duty);
|
||
|
|
||
|
return is31fl3736_write_byte(priv, reg, duty); //pwm-duty
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief change the breath mode for each led bit
|
||
|
*/
|
||
|
static int is31fl3736_set_breath_mode(struct is31fl3736_priv *priv,
|
||
|
u8 cs_x, u8 sw_y, enum is31fl3736_auto_breath_mode mode)
|
||
|
{
|
||
|
u8 reg;
|
||
|
int ret = 0;
|
||
|
|
||
|
if (cs_x > IS31FL3736_CSX_MAX || cs_x < 1) {
|
||
|
pr_err("%s: cs channel error, cs_x: %d\n", __func__, cs_x);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
if (sw_y > IS31FL3736_SWY_MAX || sw_y < 1) {
|
||
|
pr_err("%s: sw channel error, sw_y: %d\n", __func__, sw_y);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(2));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
reg = 0x10 * (sw_y - 1) + 0x02 * (cs_x - 1);
|
||
|
pr_debug("%s: reg 0x%02x, mode %d\n", __func__, reg, mode);
|
||
|
|
||
|
return is31fl3736_write_byte(priv, reg, mode); //ABM-x
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief set led time delay for fade and holdtime
|
||
|
*/
|
||
|
static int is31fl3736_set_breath_time(struct is31fl3736_priv *priv,
|
||
|
u8 rise, u8 hold, u8 fall, u8 off,
|
||
|
enum is31fl3736_auto_breath_mode mode)
|
||
|
{
|
||
|
int ret;
|
||
|
u8 regval;
|
||
|
|
||
|
pr_debug("%s: set led breah mode:%d time: t1: %d, t2: %d, t3: %d, t3: %d\n",
|
||
|
__func__, mode, rise, hold, fall, off);
|
||
|
|
||
|
if (mode == IS31FL3736_PWM_MODE)
|
||
|
return -EINVAL;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(3));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
regval = ((rise & IS31FL3736_ABM_T1_V) << IS31FL3736_ABM_T1_S) |
|
||
|
((hold & IS31FL3736_ABM_T2_V) << IS31FL3736_ABM_T2_S);
|
||
|
pr_debug("%s: reg: 0x%0x, val: %d\n",
|
||
|
__func__, IS31FL3736_REG_PG3_FADE_IN(mode), regval);
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_PG3_FADE_IN(mode), regval);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
regval = ((fall & IS31FL3736_ABM_T3_V) << IS31FL3736_ABM_T3_S) |
|
||
|
((off & IS31FL3736_ABM_T4_V) << IS31FL3736_ABM_T4_S);
|
||
|
|
||
|
pr_debug("%s: reg: 0x%0x, val: %d\n",
|
||
|
__func__, IS31FL3736_REG_PG3_FADE_OUT(mode), regval);
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_PG3_FADE_OUT(mode), regval);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief set led loop time for brath mode
|
||
|
*/
|
||
|
static int is31fl3736_set_breath_loop(struct is31fl3736_priv *priv,
|
||
|
u32 loop, enum is31fl3736_auto_breath_mode mode)
|
||
|
{
|
||
|
int ret;
|
||
|
|
||
|
pr_debug("%s: set led blink loop times, mode: %d, loop: %d\n",
|
||
|
__func__, mode, loop);
|
||
|
|
||
|
if (mode == IS31FL3736_PWM_MODE)
|
||
|
return -EINVAL;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(3));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
/*LTA, high 4 bit*/
|
||
|
ret = is31fl3736_write_byte(priv, IS31FL3736_REG_PG3_LOOP1(mode),
|
||
|
(loop >> 8) & IS31FL3736_ABM_LTA_V);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
/*LTB, low 8 bits, total loop time: LTA*256 + LTB*/
|
||
|
ret = is31fl3736_write_byte(priv, IS31FL3736_REG_PG3_LOOP2(mode),
|
||
|
loop & IS31FL3736_ABM_LTB_V);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief set led blink for brath mode, begin or stop blink,
|
||
|
just influent the ARMx mode led
|
||
|
*/
|
||
|
static int is31fl3736_set_breath_blink(struct is31fl3736_priv *priv, int enable)
|
||
|
{
|
||
|
int ret;
|
||
|
|
||
|
pr_debug("%s: set led blink enable: %d\n", __func__, enable);
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv, IS31FL3736_RET_CMD_LOCK,
|
||
|
IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(3));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
if (enable) {
|
||
|
is31fl3736_write_byte(priv, IS31FL3736_REG_PG3_CONFIG, 0x03);
|
||
|
//update registers
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_PG3_UPDATE, 0x00);
|
||
|
} else {
|
||
|
//disable auto breath mode
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_PG3_CONFIG, 0x01);
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief reset all regs to default
|
||
|
*/
|
||
|
static int is31fl3736_reset_regs(struct is31fl3736_priv *priv)
|
||
|
{
|
||
|
int ret;
|
||
|
u8 reg;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(3));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_read_byte(priv,
|
||
|
IS31FL3736_REG_PG3_RESET, ®);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief software shutdown the chip
|
||
|
*/
|
||
|
static int is31fl3736_software_shutdown(struct is31fl3736_priv *priv,
|
||
|
bool enable)
|
||
|
{
|
||
|
int ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_CMD, IS31FL3736_PAGE(3));
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
if (enable) {
|
||
|
//software shutdown
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_PG3_CONFIG, 0x00);
|
||
|
} else {
|
||
|
//normal state
|
||
|
ret = is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_REG_PG3_CONFIG, 0x03);
|
||
|
}
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief init the chip to normal state
|
||
|
*/
|
||
|
static int is31fl3736_init_regs(struct is31fl3736_priv *priv)
|
||
|
{
|
||
|
is31fl3736_write_byte(priv,
|
||
|
IS31FL3736_RET_CMD_LOCK, IS31FL3736_CMD_WRITE_EN);
|
||
|
is31fl3736_write_byte(priv, IS31FL3736_REG_PG3_CONFIG, 0x03);
|
||
|
is31fl3736_write_byte(priv, IS31FL3736_REG_PG3_CURR, 0x0ff);
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @brief set the led chanel brightness
|
||
|
*/
|
||
|
static int is31fl3736_brightness_set(struct led_classdev *led_cdev,
|
||
|
enum led_brightness brightness)
|
||
|
{
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
int ret;
|
||
|
enum is31fl3736_led_state state;
|
||
|
|
||
|
dev_dbg(led_cdev->dev, "%s: %d, cs: %d, sw: %d\n",
|
||
|
__func__, brightness,
|
||
|
led_data->cs_ch, led_data->sw_ch);
|
||
|
|
||
|
if (led_data->cur_mode != IS31FL3736_PWM_MODE) {
|
||
|
dev_info(led_cdev->dev,
|
||
|
"%s: current mode is not PWM mode\n", __func__);
|
||
|
return -EPERM;
|
||
|
}
|
||
|
|
||
|
if (brightness < 0 || brightness > 0xff) {
|
||
|
dev_err(led_cdev->dev,
|
||
|
"%s: brightness: %d is invalid\n",
|
||
|
__func__, brightness);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
state = brightness ? IS31FL3736_LED_ON : IS31FL3736_LED_OFF;
|
||
|
is31fl3736_set_led_state(led_data->priv,
|
||
|
led_data->cs_ch, led_data->sw_ch, IS31FL3736_LED_ON);
|
||
|
led_data->cur_state = state;
|
||
|
|
||
|
ret = is31fl3736_set_pwm_duty(led_data->priv,
|
||
|
led_data->cs_ch, led_data->sw_ch,
|
||
|
brightness);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_show_mode(struct device *dev,
|
||
|
struct device_attribute *attr, char *buf)
|
||
|
{
|
||
|
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
ssize_t ret = 0;
|
||
|
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
switch (led_data->cur_mode) {
|
||
|
case IS31FL3736_PWM_MODE:
|
||
|
ret += sprintf(buf, "PWM\n");
|
||
|
break;
|
||
|
case IS31FL3736_ABM1:
|
||
|
ret += sprintf(buf, "ABM1\n");
|
||
|
break;
|
||
|
case IS31FL3736_ABM2:
|
||
|
ret += sprintf(buf, "ABM2\n");
|
||
|
break;
|
||
|
case IS31FL3736_ABM3:
|
||
|
ret += sprintf(buf, "ABM3\n");
|
||
|
break;
|
||
|
default:
|
||
|
ret += sprintf(buf, "NONE\n");
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_store_mode(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
enum is31fl3736_auto_breath_mode mode;
|
||
|
int ret;
|
||
|
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
if (!strncmp(buf, "PWM", 3))
|
||
|
mode = IS31FL3736_PWM_MODE;
|
||
|
else if (!strncmp(buf, "ABM1", 4))
|
||
|
mode = IS31FL3736_ABM1;
|
||
|
else if (!strncmp(buf, "ABM2", 4))
|
||
|
mode = IS31FL3736_ABM2;
|
||
|
else if (!strncmp(buf, "ABM3", 4))
|
||
|
mode = IS31FL3736_ABM3;
|
||
|
else
|
||
|
mode = IS31FL3736_PWM_MODE;
|
||
|
|
||
|
ret = is31fl3736_set_breath_mode(led_data->priv,
|
||
|
led_data->cs_ch,
|
||
|
led_data->sw_ch, mode);
|
||
|
if (!ret)
|
||
|
led_data->cur_mode = mode;
|
||
|
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return size;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_show_state(struct device *dev,
|
||
|
struct device_attribute *attr, char *buf)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
ssize_t ret = 0;
|
||
|
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
switch (led_data->cur_state) {
|
||
|
case IS31FL3736_LED_OFF:
|
||
|
ret += sprintf(buf, "OFF\n");
|
||
|
break;
|
||
|
case IS31FL3736_LED_ON:
|
||
|
ret += sprintf(buf, "ON\n");
|
||
|
break;
|
||
|
default:
|
||
|
ret += sprintf(buf, "NONE\n");
|
||
|
break;
|
||
|
}
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_store_state(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
enum is31fl3736_led_state state;
|
||
|
int ret;
|
||
|
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
if (!strncmp(buf, "OFF", 3) || !strncmp(buf, "off", 3))
|
||
|
state = IS31FL3736_LED_OFF;
|
||
|
else if (!strncmp(buf, "ON", 2) || !strncmp(buf, "on", 2))
|
||
|
state = IS31FL3736_LED_ON;
|
||
|
else
|
||
|
state = IS31FL3736_LED_OFF;
|
||
|
|
||
|
ret = is31fl3736_set_led_state(led_data->priv,
|
||
|
led_data->cs_ch, led_data->sw_ch, state);
|
||
|
if (!ret)
|
||
|
led_data->cur_state = state;
|
||
|
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return size;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_show_time(struct device *dev,
|
||
|
struct device_attribute *attr, char *buf)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
return snprintf(buf, PAGE_SIZE, "%d %d %d %d\n",
|
||
|
led_data->rise_time, led_data->hold_time,
|
||
|
led_data->fall_time, led_data->off_time);
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_store_time(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
int ret, rise_time, hold_time, fall_time, off_time;
|
||
|
|
||
|
ret = sscanf(buf, "%d %d %d %d",
|
||
|
&rise_time, &hold_time,
|
||
|
&fall_time, &off_time);
|
||
|
|
||
|
pr_debug("%s, rise: %d, hold: %d, fall: %d, off: %d\n",
|
||
|
__func__, rise_time, hold_time, fall_time, off_time);
|
||
|
|
||
|
if (led_data->cur_mode == IS31FL3736_PWM_MODE) {
|
||
|
dev_info(led_cdev->dev,
|
||
|
"%s: current mode is PWM mode\n", __func__);
|
||
|
return -EPERM;
|
||
|
}
|
||
|
|
||
|
if ((rise_time < 0 || rise_time >= IS31FL3736_t1_t3_time_MAX) ||
|
||
|
(hold_time < 0 || hold_time >= IS31FL3736_t2_time_MAX) ||
|
||
|
(fall_time < 0 || fall_time >= IS31FL3736_t1_t3_time_MAX) ||
|
||
|
(off_time < 0 || off_time >= IS31FL3736_t4_time_MAX)) {
|
||
|
dev_info(led_cdev->dev,
|
||
|
"%s: breath time paras invalid\n", __func__);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
ret = is31fl3736_set_breath_time(led_data->priv,
|
||
|
rise_time, hold_time,
|
||
|
fall_time, off_time,
|
||
|
led_data->cur_mode);
|
||
|
if (!ret) {
|
||
|
led_data->rise_time = rise_time;/*t1, fade in*/
|
||
|
led_data->hold_time = hold_time;/*t2, on*/
|
||
|
led_data->fall_time = fall_time;/*t3, fade out*/
|
||
|
led_data->off_time = off_time; /*t4, off*/
|
||
|
}
|
||
|
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return size;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_show_loop(struct device *dev,
|
||
|
struct device_attribute *attr, char *buf)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
return sprintf(buf, "%d\n", led_data->loop);
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_store_loop(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
unsigned long loop;
|
||
|
int ret;
|
||
|
|
||
|
if (kstrtoul(buf, 10, &loop))
|
||
|
return -EINVAL;
|
||
|
|
||
|
if (loop < 0)
|
||
|
loop = 0;
|
||
|
|
||
|
pr_debug("%s, set breath loop time: %ld", __func__, loop);
|
||
|
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
ret = is31fl3736_set_breath_loop(led_data->priv,
|
||
|
(u32)loop, led_data->cur_mode);
|
||
|
if (!ret)
|
||
|
led_data->loop = (u32)loop;
|
||
|
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return size;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_show_blink(struct device *dev,
|
||
|
struct device_attribute *attr, char *buf)
|
||
|
{
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static ssize_t is31fl3736_led_store_blink(struct device *dev,
|
||
|
struct device_attribute *attr,
|
||
|
const char *buf, size_t size)
|
||
|
{
|
||
|
struct led_classdev *led_cdev = dev_get_drvdata(dev);
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
container_of(led_cdev, struct is31fl3736_led_data, cdev);
|
||
|
|
||
|
unsigned long blink;
|
||
|
int ret;
|
||
|
|
||
|
ret = kstrtoul(buf, 10, &blink);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
pr_debug("%s, blink time: %ld", __func__, blink);
|
||
|
mutex_lock(&led_data->priv->mutex);
|
||
|
|
||
|
is31fl3736_set_breath_blink(led_data->priv, (int)blink);
|
||
|
|
||
|
mutex_unlock(&led_data->priv->mutex);
|
||
|
|
||
|
return size;
|
||
|
}
|
||
|
|
||
|
static IS31Fl3736_ATTR_RW(mode);
|
||
|
static IS31Fl3736_ATTR_RW(state);
|
||
|
static IS31Fl3736_ATTR_RW(time);
|
||
|
static IS31Fl3736_ATTR_RW(loop);
|
||
|
static IS31Fl3736_ATTR_RW(blink);
|
||
|
|
||
|
static struct attribute *is31fl3736_led_attributes[] = {
|
||
|
&dev_attr_mode.attr,
|
||
|
&dev_attr_state.attr,
|
||
|
&dev_attr_time.attr,
|
||
|
&dev_attr_loop.attr,
|
||
|
&dev_attr_blink.attr,
|
||
|
NULL,
|
||
|
};
|
||
|
|
||
|
static struct attribute_group is31fl3736_led_attribute_group = {
|
||
|
.attrs = is31fl3736_led_attributes
|
||
|
};
|
||
|
|
||
|
static const struct attribute_group *is31fl3736_led_attribute_groups[] = {
|
||
|
&is31fl3736_led_attribute_group,
|
||
|
NULL
|
||
|
};
|
||
|
|
||
|
static inline size_t sizeof_is31fl3736_priv(int num_leds)
|
||
|
{
|
||
|
return sizeof(struct is31fl3736_priv) +
|
||
|
(sizeof(struct is31fl3736_led_data) * num_leds);
|
||
|
}
|
||
|
|
||
|
static int is31fl3736_parse_child_dt(const struct device *dev,
|
||
|
const struct device_node *child,
|
||
|
struct is31fl3736_led_data *led_data)
|
||
|
{
|
||
|
struct led_classdev *cdev = &led_data->cdev;
|
||
|
int ret = 0;
|
||
|
u32 reg;
|
||
|
u32 sw, cs;
|
||
|
|
||
|
if (of_property_read_string(child, "label", &cdev->name))
|
||
|
cdev->name = child->name;
|
||
|
|
||
|
ret = of_property_read_u32(child, "reg", ®);
|
||
|
if (ret || reg < 1) {
|
||
|
dev_err(dev,
|
||
|
"Child node %s does not have a valid reg property\n",
|
||
|
child->full_name);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
led_data->channel = reg;
|
||
|
pr_debug("%s, line:%d, led channel: %d, name: %s\n",
|
||
|
__func__, __LINE__, reg, child->name);
|
||
|
|
||
|
ret = of_property_read_u32(child, "sw_location", &sw);
|
||
|
if (ret || sw < 1 || sw > led_data->priv->sw_max) {
|
||
|
dev_err(dev,
|
||
|
"Child node %s does not have a valid sw location property\n",
|
||
|
child->full_name);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
led_data->sw_ch = sw;
|
||
|
|
||
|
ret = of_property_read_u32(child, "cs_location", &cs);
|
||
|
if (ret || cs < 1 || cs > led_data->priv->cs_max) {
|
||
|
dev_err(dev,
|
||
|
"Child node %s does not have a valid cs location property\n",
|
||
|
child->full_name);
|
||
|
return -EINVAL;
|
||
|
}
|
||
|
led_data->cs_ch = cs;
|
||
|
|
||
|
of_property_read_string(child, "linux,default-trigger",
|
||
|
&cdev->default_trigger);
|
||
|
|
||
|
cdev->brightness_set_blocking = is31fl3736_brightness_set;
|
||
|
cdev->brightness = LED_OFF;
|
||
|
cdev->groups = is31fl3736_led_attribute_groups;
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static struct is31fl3736_led_data *is31fl3736_find_led_data(
|
||
|
struct is31fl3736_priv *priv,
|
||
|
u8 channel)
|
||
|
{
|
||
|
size_t i;
|
||
|
|
||
|
for (i = 0; i < priv->num_leds; i++) {
|
||
|
if (priv->leds[i].channel == channel)
|
||
|
return &priv->leds[i];
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
static int is31fl3736_parse_dt(struct device *dev,
|
||
|
struct is31fl3736_priv *priv)
|
||
|
{
|
||
|
struct device_node *child;
|
||
|
struct gpio_config config;
|
||
|
int ret = 0;
|
||
|
|
||
|
/*gpio power enable*/
|
||
|
priv->power_gpio = of_get_named_gpio_flags(dev->of_node,
|
||
|
"gpio-power", 0,
|
||
|
(enum of_gpio_flags *)&config);
|
||
|
|
||
|
pr_debug("%s, line:%d, power_gpio: %d!\n",
|
||
|
__func__, __LINE__, priv->power_gpio);
|
||
|
|
||
|
/*gpio shdn enable*/
|
||
|
priv->shdn_gpio = of_get_named_gpio_flags(dev->of_node,
|
||
|
"gpio-shdn", 0,
|
||
|
(enum of_gpio_flags *)&config);
|
||
|
|
||
|
pr_debug("%s, line:%d, shdn_gpio: %d!\n",
|
||
|
__func__, __LINE__, priv->shdn_gpio);
|
||
|
|
||
|
for_each_child_of_node(dev->of_node, child) {
|
||
|
struct is31fl3736_led_data *led_data =
|
||
|
&priv->leds[priv->num_leds];
|
||
|
|
||
|
const struct is31fl3736_led_data *other_led_data;
|
||
|
|
||
|
led_data->priv = priv;
|
||
|
|
||
|
ret = is31fl3736_parse_child_dt(dev, child, led_data);
|
||
|
if (ret)
|
||
|
goto err;
|
||
|
|
||
|
/* Detect if channel is already in use by another child */
|
||
|
other_led_data = is31fl3736_find_led_data(priv,
|
||
|
led_data->channel);
|
||
|
if (other_led_data) {
|
||
|
dev_err(dev,
|
||
|
"%s and %s both attempting to use channel %d\n",
|
||
|
led_data->cdev.name,
|
||
|
other_led_data->cdev.name,
|
||
|
led_data->channel);
|
||
|
goto err;
|
||
|
}
|
||
|
|
||
|
ret = devm_led_classdev_register(dev, &led_data->cdev);
|
||
|
if (ret) {
|
||
|
dev_err(dev, "failed to register PWM led for %s: %d\n",
|
||
|
led_data->cdev.name, ret);
|
||
|
goto err;
|
||
|
}
|
||
|
priv->num_leds++;
|
||
|
}
|
||
|
return 0;
|
||
|
|
||
|
err:
|
||
|
of_node_put(child);
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
static const struct of_device_id of_is31fl3736_match[] = {
|
||
|
{ .compatible = "issi,is31fl3736", },
|
||
|
{},
|
||
|
};
|
||
|
|
||
|
MODULE_DEVICE_TABLE(of, of_is31fl3736_match);
|
||
|
|
||
|
static int is31fl3736_probe(struct i2c_client *client,
|
||
|
const struct i2c_device_id *id)
|
||
|
{
|
||
|
const struct of_device_id *of_dev_id;
|
||
|
struct device *dev = &client->dev;
|
||
|
struct is31fl3736_priv *priv;
|
||
|
int count;
|
||
|
int ret = 0;
|
||
|
|
||
|
pr_debug("%s, line:%d, i2c device id: %s!\n",
|
||
|
__func__, __LINE__, id->name);
|
||
|
|
||
|
of_dev_id = of_match_device(of_is31fl3736_match, dev);
|
||
|
if (!of_dev_id)
|
||
|
return -EINVAL;
|
||
|
|
||
|
count = of_get_child_count(dev->of_node);
|
||
|
if (!count)
|
||
|
return -EINVAL;
|
||
|
|
||
|
pr_debug("%s, line:%d, led count: %d!\n", __func__, __LINE__, count);
|
||
|
|
||
|
priv = devm_kzalloc(dev, sizeof_is31fl3736_priv(count),
|
||
|
GFP_KERNEL);
|
||
|
if (!priv)
|
||
|
return -ENOMEM;
|
||
|
|
||
|
priv->client = client;
|
||
|
priv->cs_max = IS31FL3736_CSX_MAX;
|
||
|
priv->sw_max = IS31FL3736_SWY_MAX;
|
||
|
|
||
|
i2c_set_clientdata(client, priv);
|
||
|
|
||
|
ret = is31fl3736_parse_dt(dev, priv);
|
||
|
if (ret)
|
||
|
return ret;
|
||
|
|
||
|
if (gpio_is_valid(priv->shdn_gpio)) {
|
||
|
ret = gpio_request(priv->shdn_gpio, "shdn gpio");
|
||
|
if (!ret) {
|
||
|
gpio_direction_output(priv->shdn_gpio, 1);
|
||
|
gpio_set_value(priv->shdn_gpio, 1);
|
||
|
pr_info("%s, line:%d, enable shdn gpio: %d!\n",
|
||
|
__func__, __LINE__, priv->shdn_gpio);
|
||
|
msleep(20);
|
||
|
} else {
|
||
|
pr_err("%s, line:%d, failed request shdn gpio: %d!\n",
|
||
|
__func__, __LINE__,
|
||
|
priv->shdn_gpio);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (gpio_is_valid(priv->power_gpio)) {
|
||
|
ret = gpio_request(priv->power_gpio, "power gpio");
|
||
|
if (!ret) {
|
||
|
gpio_direction_output(priv->power_gpio, 1);
|
||
|
gpio_set_value(priv->power_gpio, 1);
|
||
|
pr_info("%s, line:%d, power gpio: %d!\n",
|
||
|
__func__, __LINE__, priv->power_gpio);
|
||
|
msleep(20);
|
||
|
} else {
|
||
|
pr_err("%s, line:%d, failed request power gpio: %d!\n",
|
||
|
__func__, __LINE__,
|
||
|
priv->power_gpio);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
mutex_init(&priv->mutex);
|
||
|
|
||
|
is31fl3736_reset_regs(priv);
|
||
|
is31fl3736_init_regs(priv);
|
||
|
|
||
|
pr_info("%s, line:%d, is31fl3736_probe succeed!\n",
|
||
|
__func__, __LINE__);
|
||
|
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
static int is31fl3736_remove(struct i2c_client *client)
|
||
|
{
|
||
|
struct is31fl3736_priv *priv = i2c_get_clientdata(client);
|
||
|
|
||
|
return is31fl3736_reset_regs(priv);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* i2c-core (and modalias) requires that id_table be properly filled,
|
||
|
* even though it is not used for DeviceTree based instantiation.
|
||
|
*/
|
||
|
static const struct i2c_device_id is31fl3736_id[] = {
|
||
|
{ "is31fl3736" },
|
||
|
{},
|
||
|
};
|
||
|
|
||
|
MODULE_DEVICE_TABLE(i2c, is31fl3736_id);
|
||
|
|
||
|
static struct i2c_driver is31fl3736_driver = {
|
||
|
.driver = {
|
||
|
.name = "is31fl3736",
|
||
|
.of_match_table = of_is31fl3736_match,
|
||
|
},
|
||
|
.probe = is31fl3736_probe,
|
||
|
.remove = is31fl3736_remove,
|
||
|
.id_table = is31fl3736_id,
|
||
|
};
|
||
|
|
||
|
module_i2c_driver(is31fl3736_driver);
|
||
|
|
||
|
MODULE_AUTHOR("xudong<xudong@allwinnertech.com>");
|
||
|
MODULE_DESCRIPTION("ISSI IS31FL3736 LED driver");
|
||
|
MODULE_LICENSE("GPL v2");
|