All of lore.kernel.org
 help / color / mirror / Atom feed
From: Eddie James <eajames@linux.ibm.com>
To: linux-leds@vger.kernel.org
Cc: linux-kernel@vger.kernel.org, pavel@ucw.cz,
	openbmc@lists.ozlabs.org, joel@jms.id.au,
	Eddie James <eajames@linux.ibm.com>
Subject: [PATCH] leds: pca955x: Add HW blink support
Date: Wed, 30 Mar 2022 15:33:18 -0500	[thread overview]
Message-ID: <20220330203318.19225-1-eajames@linux.ibm.com> (raw)

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@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


WARNING: multiple messages have this Message-ID (diff)
From: Eddie James <eajames@linux.ibm.com>
To: linux-leds@vger.kernel.org
Cc: openbmc@lists.ozlabs.org, Eddie James <eajames@linux.ibm.com>,
	linux-kernel@vger.kernel.org, pavel@ucw.cz, joel@jms.id.au
Subject: [PATCH] leds: pca955x: Add HW blink support
Date: Wed, 30 Mar 2022 15:33:18 -0500	[thread overview]
Message-ID: <20220330203318.19225-1-eajames@linux.ibm.com> (raw)

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@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


             reply	other threads:[~2022-03-30 20:33 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2022-03-30 20:33 Eddie James [this message]
2022-03-30 20:33 ` [PATCH] leds: pca955x: Add HW blink support Eddie James
2022-03-31 15:39 ` Patrick Williams
2022-03-31 15:39   ` Patrick Williams
2022-04-01 14:17   ` Eddie James
2022-04-01 14:17     ` Eddie James

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20220330203318.19225-1-eajames@linux.ibm.com \
    --to=eajames@linux.ibm.com \
    --cc=joel@jms.id.au \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-leds@vger.kernel.org \
    --cc=openbmc@lists.ozlabs.org \
    --cc=pavel@ucw.cz \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.