---
drivers/leds/leds-pca955x.c | 222 +++++++++++++++++++++++++++---------
1 file changed, 168 insertions(+), 54 deletions(-)
diff --git a/drivers/leds/leds-pca955x.c b/drivers/leds/leds-pca955x.c
index 61f3cb84a945..7c156de215d7 100644
--- a/drivers/leds/leds-pca955x.c
+++ b/drivers/leds/leds-pca955x.c
@@ -62,6 +62,8 @@
#define PCA955X_GPIO_HIGH LED_OFF
#define PCA955X_GPIO_LOW LED_FULL
+#define PCA955X_BLINK_DEFAULT_MS 1000
+
enum pca955x_type {
pca9550,
pca9551,
@@ -74,6 +76,7 @@ struct pca955x_chipdef {
int bits;
u8 slv_addr; /* 7-bit slave address mask */
int slv_addr_shift; /* Number of bits to ignore */
+ int blink_div; /* PSC divider */
};
static struct pca955x_chipdef pca955x_chipdefs[] = {
@@ -81,26 +84,31 @@ static struct pca955x_chipdef pca955x_chipdefs[] = {
.bits = 2,
.slv_addr = /* 110000x */ 0x60,
.slv_addr_shift = 1,
+ .blink_div = 44,
},
[pca9551] = {
.bits = 8,
.slv_addr = /* 1100xxx */ 0x60,
.slv_addr_shift = 3,
+ .blink_div = 38,
},
[pca9552] = {
.bits = 16,
.slv_addr = /* 1100xxx */ 0x60,
.slv_addr_shift = 3,
+ .blink_div = 44,
},
[ibm_pca9552] = {
.bits = 16,
.slv_addr = /* 0110xxx */ 0x30,
.slv_addr_shift = 3,
+ .blink_div = 44,
},
[pca9553] = {
.bits = 4,
.slv_addr = /* 110001x */ 0x62,
.slv_addr_shift = 1,
+ .blink_div = 44,
},
};
@@ -119,7 +127,9 @@ struct pca955x {
struct pca955x_led *leds;
struct pca955x_chipdef *chipdef;
struct i2c_client *client;
+ unsigned long active_blink;
unsigned long active_pins;
+ unsigned long blink_period;
#ifdef CONFIG_LEDS_PCA955X_GPIO
struct gpio_chip gpio;
#endif
@@ -170,7 +180,8 @@ static inline int pca955x_ledstate(u8 ls, int led_num)
/*
* Write to frequency prescaler register, used to program the
- * period of the PWM output. period = (PSCx + 1) / 38
+ * period of the PWM output. period = (PSCx + 1) / coeff
+ * Where for pca9551 chips coeff = 38 and for all other chips coeff = 44
*/
static int pca955x_write_psc(struct pca955x *pca955x, int n, u8 val)
{
@@ -251,6 +262,20 @@ static int pca955x_read_pwm(struct pca955x *pca955x, int n, u8 *val)
return 0;
}
+static int pca955x_read_psc(struct pca955x *pca955x, int n, u8 *val)
+{
+ u8 cmd = pca955x_num_input_regs(pca955x->chipdef->bits) + (2 * n);
+ int ret;
+
+ ret = i2c_smbus_read_byte_data(pca955x->client, cmd);
+ if (ret < 0) {
+ dev_err(&pca955x->client->dev, "%s: reg 0x%x, err %d\n", __func__, n, ret);
+ return ret;
+ }
+ *val = (u8)ret;
+ return 0;
+}
+
static enum led_brightness pca955x_led_get(struct led_classdev *led_cdev)
{
struct pca955x_led *pca955x_led = led_to_pca955x(led_cdev);
@@ -270,7 +295,10 @@ static enum led_brightness pca955x_led_get(struct led_classdev *led_cdev)
ret = LED_OFF;
break;
case PCA955X_LS_BLINK0:
- ret = LED_HALF;
+ ret = pca955x_read_pwm(pca955x, 0, &pwm);
+ if (ret)
+ return ret;
+ ret = 256 - pwm;
break;
case PCA955X_LS_BLINK1:
ret = pca955x_read_pwm(pca955x, 1, &pwm);
@@ -299,29 +327,36 @@ static int pca955x_led_set(struct led_classdev *led_cdev,
if (ret)
goto out;
- switch (value) {
- case LED_FULL:
- ls = pca955x_ledsel(ls, bit, PCA955X_LS_LED_ON);
- break;
- case LED_OFF:
- ls = pca955x_ledsel(ls, bit, PCA955X_LS_LED_OFF);
- break;
- case LED_HALF:
- ls = pca955x_ledsel(ls, bit, PCA955X_LS_BLINK0);
- break;
- default:
- /*
- * Use PWM1 for all other values. This has the unwanted
- * side effect of making all LEDs on the chip share the
- * same brightness level if set to a value other than
- * OFF, HALF, or FULL. But, this is probably better than
- * just turning off for all other values.
- */
- ret = pca955x_write_pwm(pca955x, 1, 255 - value);
- if (ret)
+ if (test_bit(pca955x_led->led_num, &pca955x->active_blink)) {
+ if (value == LED_OFF) {
+ clear_bit(pca955x_led->led_num, &pca955x->active_blink);
+ ls = pca955x_ledsel(ls, bit, PCA955X_LS_LED_OFF);
+ } else {
+ ret = pca955x_write_pwm(pca955x, 0, 256 - value);
goto out;
- ls = pca955x_ledsel(ls, bit, PCA955X_LS_BLINK1);
- break;
+ }
+ } else {
+ switch (value) {
+ case LED_FULL:
+ ls = pca955x_ledsel(ls, bit, PCA955X_LS_LED_ON);
+ break;
+ case LED_OFF:
+ ls = pca955x_ledsel(ls, bit, PCA955X_LS_LED_OFF);
+ break;
+ default:
+ /*
+ * Use PWM1 for all other values. This has the unwanted
+ * side effect of making all LEDs on the chip share the
+ * same brightness level if set to a value other than
+ * OFF or FULL. But, this is probably better than just
+ * turning off for all other values.
+ */
+ ret = pca955x_write_pwm(pca955x, 1, 255 - value);
+ if (ret)
+ goto out;
+ ls = pca955x_ledsel(ls, bit, PCA955X_LS_BLINK1);
+ break;
+ }
}
ret = pca955x_write_ls(pca955x, reg, ls);
@@ -332,6 +367,94 @@ static int pca955x_led_set(struct led_classdev *led_cdev,
return ret;
}
+static u8 pca955x_period_to_psc(struct pca955x *pca955x, unsigned long p)
+{
+ p *= pca955x->chipdef->blink_div;
+ p /= MSEC_PER_SEC;
+ p -= 1;
+
+ return p;
+}
+
+static unsigned long pca955x_psc_to_period(struct pca955x *pca955x, u8 psc)
+{
+ unsigned long p = psc;
+
+ p += 1;
+ p *= MSEC_PER_SEC;
+ p /= pca955x->chipdef->blink_div;
+
+ return p;
+}
+
+static int pca955x_led_blink(struct led_classdev *led_cdev,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ struct pca955x_led *pca955x_led = led_to_pca955x(led_cdev);
+ struct pca955x *pca955x = pca955x_led->pca955x;
+ unsigned long p = *delay_on + *delay_off;
+ int ret;
+
+ mutex_lock(&pca955x->lock);
+
+ if (p) {
+ if (*delay_on != *delay_off) {
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (p < pca955x_psc_to_period(pca955x, 0) ||
+ p > pca955x_psc_to_period(pca955x, 0xff)) {
+ ret = -EINVAL;
+ goto out;
+ }
+ } else {
+ p = pca955x->active_blink ? pca955x->blink_period :
+ PCA955X_BLINK_DEFAULT_MS;
+ }
+
+ if (!pca955x->active_blink ||
+ pca955x->active_blink == BIT(pca955x_led->led_num) ||
+ pca955x->blink_period == p) {
+ u8 psc = pca955x_period_to_psc(pca955x, p);
+
+ if (!test_and_set_bit(pca955x_led->led_num,
+ &pca955x->active_blink)) {
+ u8 ls;
+ int reg = pca955x_led->led_num / 4;
+ int bit = pca955x_led->led_num % 4;
+
+ ret = pca955x_read_ls(pca955x, reg, &ls);
+ if (ret)
+ goto out;
+
+ ls = pca955x_ledsel(ls, bit, PCA955X_LS_BLINK0);
+ ret = pca955x_write_ls(pca955x, reg, ls);
+ if (ret)
+ goto out;
+ }
+
+ if (pca955x->blink_period != p) {
+ pca955x->blink_period = p;
+ ret = pca955x_write_psc(pca955x, 0, psc);
+ if (ret)
+ goto out;
+ }
+
+ p = pca955x_psc_to_period(pca955x, psc);
+ p /= 2;
+ *delay_on = p;
+ *delay_off = p;
+ } else {
+ ret = -EBUSY;
+ }
+
+out:
+ mutex_unlock(&pca955x->lock);
+
+ return ret;
+}
+
#ifdef CONFIG_LEDS_PCA955X_GPIO
/*
* Read the INPUT register, which contains the state of LEDs.
@@ -487,8 +610,9 @@ static int pca955x_probe(struct i2c_client *client)
u8 ls1[4];
u8 ls2[4];
struct pca955x_platform_data *pdata;
+ u8 psc0;
+ bool keep_psc0 = false;
bool set_default_label = false;
- bool keep_pwm = false;
char default_label[8];
enum pca955x_type chip_type;
const void *md = device_get_match_data(&client->dev);
@@ -552,6 +676,7 @@ static int pca955x_probe(struct i2c_client *client)
mutex_init(&pca955x->lock);
pca955x->client = client;
pca955x->chipdef = chip;
+ pca955x->blink_period = PCA955X_BLINK_DEFAULT_MS;
init_data.devname_mandatory = false;
init_data.devicename = "pca955x";
@@ -581,15 +706,21 @@ static int pca955x_probe(struct i2c_client *client)
led = &pca955x_led->led_cdev;
led->brightness_set_blocking = pca955x_led_set;
led->brightness_get = pca955x_led_get;
+ led->blink_set = pca955x_led_blink;
if (pdata->leds[i].default_state ==
- LEDS_GPIO_DEFSTATE_OFF)
+ LEDS_GPIO_DEFSTATE_OFF) {
ls2[reg] = pca955x_ledsel(ls2[reg], bit,
PCA955X_LS_LED_OFF);
- else if (pdata->leds[i].default_state ==
- LEDS_GPIO_DEFSTATE_ON)
+ } else if (pdata->leds[i].default_state ==
+ LEDS_GPIO_DEFSTATE_ON) {
ls2[reg] = pca955x_ledsel(ls2[reg], bit,
PCA955X_LS_LED_ON);
+ } else if (pca955x_ledstate(ls2[reg], bit) ==
+ PCA955X_LS_BLINK0) {
+ keep_psc0 = true;
+ set_bit(i, &pca955x->active_blink);
+ }
init_data.fwnode = pdata->leds[i].fwnode;
@@ -617,20 +748,6 @@ static int pca955x_probe(struct i2c_client *client)
return err;
set_bit(i, &pca955x->active_pins);
-
- /*
- * For default-state == "keep", let the core update the
- * brightness from the hardware, then check the
- * brightness to see if it's using PWM1. If so, PWM1
- * should not be written below.
- */
- if (pdata->leds[i].default_state ==
- LEDS_GPIO_DEFSTATE_KEEP) {
- if (led->brightness != LED_FULL &&
- led->brightness != LED_OFF &&
- led->brightness != LED_HALF)
- keep_pwm = true;
- }
}
}
@@ -642,22 +759,19 @@ static int pca955x_probe(struct i2c_client *client)
}
}
- /* PWM0 is used for half brightness or 50% duty cycle */
- err = pca955x_write_pwm(pca955x, 0, 255 - LED_HALF);
- if (err)
- return err;
-
- if (!keep_pwm) {
- /* PWM1 is used for variable brightness, default to OFF */
- err = pca955x_write_pwm(pca955x, 1, 0);
- if (err)
- return err;
+ if (keep_psc0) {
+ err = pca955x_read_psc(pca955x, 0, &psc0);
+ } else {
+ psc0 = pca955x_period_to_psc(pca955x, pca955x->blink_period);
+ err = pca955x_write_psc(pca955x, 0, psc0);
}
- /* Set to fast frequency so we do not see flashing */
- err = pca955x_write_psc(pca955x, 0, 0);
if (err)
return err;
+
+ pca955x->blink_period = pca955x_psc_to_period(pca955x, psc0);
+
+ /* Set PWM1 to fast frequency so we do not see flashing */
err = pca955x_write_psc(pca955x, 1, 0);
if (err)
return err;
--
2.27.0