[PATCH] leds: pca955x: Add HW blink support
Eddie James
eajames at linux.ibm.com
Thu Mar 31 07:33:18 AEDT 2022
Support blinking using the PCA955x chip. Use PWM0 for blinking
instead of LED_HALF brightness. Since there is only one frequency
and brightness register for any blinking LED, all blinked LEDs on
the chip will have the same frequency and brightness.
Signed-off-by: Eddie James <eajames at linux.ibm.com>
---
drivers/leds/leds-pca955x.c | 175 ++++++++++++++++++++++++------------
1 file changed, 120 insertions(+), 55 deletions(-)
diff --git a/drivers/leds/leds-pca955x.c b/drivers/leds/leds-pca955x.c
index 81aaf21212d7..aeddc64e8ecf 100644
--- a/drivers/leds/leds-pca955x.c
+++ b/drivers/leds/leds-pca955x.c
@@ -74,6 +74,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 +82,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,
},
};
@@ -163,7 +169,7 @@ static inline u8 pca955x_ledsel(u8 oldval, int led_num, int state)
/*
* 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) / <38 or 44, chip dependent>
*/
static int pca955x_write_psc(struct i2c_client *client, int n, u8 val)
{
@@ -273,13 +279,16 @@ 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->client, 0, &pwm);
+ if (ret)
+ return ret;
+ ret = 256 - pwm;
break;
case PCA955X_LS_BLINK1:
ret = pca955x_read_pwm(pca955x->client, 1, &pwm);
if (ret)
return ret;
- ret = 255 - pwm;
+ ret = 256 - pwm;
break;
}
@@ -308,32 +317,98 @@ static int pca955x_led_set(struct led_classdev *led_cdev,
if (ret)
goto out;
- switch (value) {
- case LED_FULL:
- ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_LED_ON);
- break;
- case LED_OFF:
+ if (value == LED_OFF) {
ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_LED_OFF);
- break;
- case LED_HALF:
+ } else {
+ u8 tls = (ls >> (ls_led << 1)) & 0x3;
+
+ if (tls == PCA955X_LS_BLINK0) {
+ ret = pca955x_write_pwm(pca955x->client, 0,
+ 256 - value);
+ goto out;
+ } else {
+ if (value == LED_FULL) {
+ ls = pca955x_ledsel(ls, ls_led,
+ PCA955X_LS_LED_ON);
+ } else {
+ /*
+ * 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->client, 1,
+ 256 - value);
+ if (ret || tls == PCA955X_LS_BLINK1)
+ goto out;
+ ls = pca955x_ledsel(ls, ls_led,
+ PCA955X_LS_BLINK1);
+ }
+ }
+ }
+
+ ret = pca955x_write_ls(pca955x->client, chip_ls, ls);
+
+out:
+ mutex_unlock(&pca955x->lock);
+
+ return ret;
+}
+
+static int pca955x_led_blink(struct led_classdev *led_cdev,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ int chip_ls;
+ int ls_led;
+ int ret;
+ u8 ls;
+ struct pca955x_led *pca955x_led = container_of(led_cdev,
+ struct pca955x_led,
+ led_cdev);
+ struct pca955x *pca955x = pca955x_led->pca955x;
+ unsigned long p = *delay_on + *delay_off;
+
+ /* 1 Hz default */
+ if (!p)
+ p = 1000;
+
+ p *= (unsigned long)pca955x->chipdef->blink_div;
+ p /= 1000;
+ p -= 1;
+
+ chip_ls = pca955x_led->led_num / 4;
+ ls_led = pca955x_led->led_num % 4;
+
+ mutex_lock(&pca955x->lock);
+
+ ret = pca955x_read_ls(pca955x->client, chip_ls, &ls);
+ if (ret)
+ goto out;
+
+ /*
+ * All blinking leds on the PCA955x chip will use the same period and
+ * brightness.
+ */
+ ret = pca955x_write_psc(pca955x->client, 0, (u8)p);
+ if (ret)
+ goto out;
+
+ if (((ls >> (ls_led << 1)) & 0x3) != PCA955X_LS_BLINK0) {
ls = pca955x_ledsel(ls, ls_led, 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->client, 1, 255 - value);
+ ret = pca955x_write_ls(pca955x->client, chip_ls, ls);
if (ret)
goto out;
- ls = pca955x_ledsel(ls, ls_led, PCA955X_LS_BLINK1);
- break;
}
- ret = pca955x_write_ls(pca955x->client, chip_ls, ls);
+ p += 1;
+ p *= 1000;
+ p /= (unsigned long)pca955x->chipdef->blink_div;
+ p /= 2;
+
+ *delay_on = p;
+ *delay_off = p;
out:
mutex_unlock(&pca955x->lock);
@@ -495,7 +570,6 @@ static int pca955x_probe(struct i2c_client *client)
int i, err;
struct pca955x_platform_data *pdata;
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);
@@ -577,6 +651,7 @@ 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) {
@@ -585,9 +660,28 @@ static int pca955x_probe(struct i2c_client *client)
return err;
} else if (pdata->leds[i].default_state ==
LEDS_GPIO_DEFSTATE_ON) {
- err = pca955x_led_set(led, LED_FULL);
+ /*
+ * handle this case specially in order to turn
+ * off blinking, which pca955x_led_set won't do
+ */
+ u8 ls;
+ int chip_ls = i / 4;
+ int ls_led = i % 4;
+
+ err = pca955x_read_ls(pca955x->client, chip_ls,
+ &ls);
if (err)
return err;
+
+ if (((ls >> (ls_led << 1)) & 0x3) !=
+ PCA955X_LS_LED_ON) {
+ ls = pca955x_ledsel(ls, ls_led,
+ PCA955X_LS_LED_ON);
+ err = pca955x_write_ls(pca955x->client,
+ chip_ls, ls);
+ if (err)
+ return err;
+ }
}
init_data.fwnode = pdata->leds[i].fwnode;
@@ -616,39 +710,10 @@ 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;
- }
}
}
- /* PWM0 is used for half brightness or 50% duty cycle */
- err = pca955x_write_pwm(client, 0, 255 - LED_HALF);
- if (err)
- return err;
-
- if (!keep_pwm) {
- /* PWM1 is used for variable brightness, default to OFF */
- err = pca955x_write_pwm(client, 1, 0);
- if (err)
- return err;
- }
-
- /* Set to fast frequency so we do not see flashing */
- err = pca955x_write_psc(client, 0, 0);
- if (err)
- return err;
+ /* Set PWM1 to fast frequency so we do not see flashing */
err = pca955x_write_psc(client, 1, 0);
if (err)
return err;
--
2.27.0
More information about the openbmc
mailing list