All of lore.kernel.org
 help / color / mirror / Atom feed
* [RFC] ledtrig-dither: A Poor man's adjustable LED brightness.
@ 2017-08-15 21:35 Paulo Costa
  2017-08-15 22:34 ` Pavel Machek
  0 siblings, 1 reply; 6+ messages in thread
From: Paulo Costa @ 2017-08-15 21:35 UTC (permalink / raw)
  To: linux-leds; +Cc: Paulo Costa

While many LED drivers support adjustable brightness levels, usually
via PWM hardware, most can only be set to ON or OFF.

Well, I wish I could adjust the brightness of every led.
What if we can fake PWMs with kernel timers?

This led trigger tries to do that, in hacky way:
- Assign 'dither' to myled/trigger
- A new led device 'myled:dither' is created
- The brightness of myled will quickly switch between 0 and max_brightness, with the time average value defined by 'myled:dither/brightness'
- Human is fooled and thinks the brightness is adjustable.

There are a number of issues in that:
- The underlying LED has to be fast, since it will be ajusted many times per second.
  E.g., GPIO leds are OK, USB Keyboard capslock led aren't.
- kernel timers have low time resolution (dependent on HZ), so:
  - Timing variances has to be accounter for and compensated.
  - Since the the smallest between time_on and time_off is limited to a jiffie,
    blinking slows down and flickering becomes visible when brightness is near-zero and near-max.
  - Setting kernel to 1000Hz is much better than 100Hz

Despite being hacky, it works surprisingly well. You can see a demo on https://youtu.be/PIyMW8uwOmE

A better integration that bypasses using a trigger and a new led would be even better, but I'm leaving that in the future.
---
 drivers/leds/trigger/Kconfig          |   9 ++
 drivers/leds/trigger/Makefile         |   1 +
 drivers/leds/trigger/ledtrig-dither.c | 202 ++++++++++++++++++++++++++++++++++
 3 files changed, 212 insertions(+)
 create mode 100644 drivers/leds/trigger/ledtrig-dither.c

diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig
index 3f9ddb9f..5a23ab7a 100644
--- a/drivers/leds/trigger/Kconfig
+++ b/drivers/leds/trigger/Kconfig
@@ -127,3 +127,12 @@ config LEDS_TRIGGER_PANIC
 	  If unsure, say Y.
 
 endif # LEDS_TRIGGERS
+
+config LEDS_TRIGGER_DITHER
+	tristate "LED Dithering Trigger"
+	depends on LEDS_TRIGGERS
+	help
+	  The poor man's PWM led. It uses kernel timers to quickly switch leds ON
+	  and OFF, simulating adjustable brightness. Some flickering may be visible,
+	  but the overall effect is convincing.
+	  If unsure, say Y.
diff --git a/drivers/leds/trigger/Makefile b/drivers/leds/trigger/Makefile
index a72c43cf..5a4d7039 100644
--- a/drivers/leds/trigger/Makefile
+++ b/drivers/leds/trigger/Makefile
@@ -10,3 +10,4 @@ obj-$(CONFIG_LEDS_TRIGGER_DEFAULT_ON)	+= ledtrig-default-on.o
 obj-$(CONFIG_LEDS_TRIGGER_TRANSIENT)	+= ledtrig-transient.o
 obj-$(CONFIG_LEDS_TRIGGER_CAMERA)	+= ledtrig-camera.o
 obj-$(CONFIG_LEDS_TRIGGER_PANIC)	+= ledtrig-panic.o
+obj-$(CONFIG_LEDS_TRIGGER_DITHER)	+= ledtrig-dither.o
diff --git a/drivers/leds/trigger/ledtrig-dither.c b/drivers/leds/trigger/ledtrig-dither.c
new file mode 100644
index 00000000..cf8d8baf
--- /dev/null
+++ b/drivers/leds/trigger/ledtrig-dither.c
@@ -0,0 +1,202 @@
+/*
+ * Dithering LED trigger
+ *
+ * Author: Paulo Costa <me@paulo.costa.nom.br>
+ *
+ * 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/module.h>
+#include <linux/kernel.h>
+#include <linux/init.h>
+#include <linux/leds.h>
+#include <linux/slab.h>
+#include <linux/printk.h>
+#include <linux/ktime.h>
+#include <linux/timer.h>
+#include "../leds.h"
+
+#define WRAPPER_LED_SUFIX ":dither"
+
+// The target blink period.
+// But since timers have a pretty low resolution, the actual blink period is likely to be bigger
+#define DITHER_MIN_PERIOD_MS 20
+// And this is the maximum period we can tolerate
+#define DITHER_MAX_PERIOD_MS 1000
+
+struct ledtrig_dither_data {
+	struct led_classdev *actual_led;
+	struct led_classdev wrapper_led;
+	struct timer_list timer;
+	ktime_t updated_at;
+	enum led_brightness target_value;
+	enum led_brightness actual_value;
+
+	int error;
+
+    int min_time_on;
+    int min_time_off;
+	int max_time_on;
+    int max_time_off;
+	int min_error;
+	int max_error;
+};
+
+static inline struct ledtrig_dither_data * led_to_data(struct led_classdev *led)
+{
+	return container_of(led, struct ledtrig_dither_data, wrapper_led);
+}
+
+static void dither_update_error(struct ledtrig_dither_data* dither)
+{
+	ktime_t now = ktime_get();
+	int elapsed =  ktime_to_us(ktime_sub(now, dither->updated_at));
+
+	dither->updated_at = now;
+
+	dither->error += elapsed * (dither->actual_value - dither->target_value);
+	if (dither->error < dither->min_error) {
+		dither->error = dither->min_error;
+	} else if (dither->error > dither->max_error) {
+		dither->error = dither->max_error;
+	}
+}
+
+
+static void dither_update_brightness(struct ledtrig_dither_data* dither)
+{
+	int error_speed;
+	int timeout;
+	if ((dither->error < 0) || ( (dither->error == 0) && (dither->target_value > dither->wrapper_led.max_brightness / 2) ) ) {
+		led_set_brightness_nosleep(dither->actual_led, dither->actual_led->max_brightness);
+		dither->actual_value = dither->wrapper_led.max_brightness;
+	} else {
+		led_set_brightness_nosleep(dither->actual_led, LED_OFF);
+		dither->actual_value = LED_OFF;
+	}
+
+	//Calculate how long it takes to cross error=0, and schedules the timer accordingly
+	error_speed = dither->target_value - dither->actual_value;
+	if (error_speed == 0) {
+		// We're stable, no need to reschedule the timer until the value has changed.
+		return;
+	} else {
+		timeout = DIV_ROUND_UP(dither->error, error_speed);
+
+		//We don't want to blink too fast and waster CPU -- Ensures a minimum time before calling the timer.
+		if (dither->actual_value && (dither->min_time_on >= dither->min_time_off) && (timeout < dither->min_time_on)) {
+			timeout = dither->min_time_on;
+		}
+		if (!dither->actual_value && (dither->min_time_off >= dither->min_time_on) && (timeout < dither->min_time_off)) {
+			timeout = dither->min_time_off;
+		}
+
+		mod_timer(&dither->timer, jiffies + usecs_to_jiffies(timeout));
+	}
+}
+
+static void dither_set(struct led_classdev *led, enum led_brightness value)
+{
+	struct ledtrig_dither_data *dither = led_to_data(led);
+
+	del_timer_sync(&dither->timer);
+
+	dither->min_time_on  = 1000 * DITHER_MIN_PERIOD_MS * value / led->max_brightness;
+	dither->min_time_off = 1000 * DITHER_MIN_PERIOD_MS * (led->max_brightness - value) / led->max_brightness;
+
+	dither->max_time_on  = 1000 * DITHER_MAX_PERIOD_MS * value / led->max_brightness;
+	dither->max_time_off = 1000 * DITHER_MAX_PERIOD_MS * (led->max_brightness - value) / led->max_brightness;
+	dither->max_error = dither->max_time_on  * (led->max_brightness - value);
+	dither->min_error = dither->max_time_off * -value;
+
+	dither_update_error(dither);
+	dither->target_value = value;
+	dither_update_brightness(dither);
+}
+
+static void dither_timer(unsigned long data)
+{
+	struct ledtrig_dither_data* dither = (struct ledtrig_dither_data*)data;
+	dither_update_error(dither);
+	dither_update_brightness(dither);
+}
+
+static void ledtrig_dither_activate(struct led_classdev *led)
+{
+	char* wrapper_name;
+	struct ledtrig_dither_data* dither_data;
+
+	wrapper_name = kzalloc(strlen(led->name) + strlen(WRAPPER_LED_SUFIX) + 1, GFP_KERNEL);
+	if (!wrapper_name)
+		goto err_wrapper_name;
+	strcpy(wrapper_name, led->name);
+	strcat(wrapper_name, WRAPPER_LED_SUFIX);
+
+	dither_data = kzalloc(sizeof(*dither_data), GFP_KERNEL);
+	if (!dither_data)
+		goto err_dither_data;
+	dither_data->actual_led = led;
+	dither_data->wrapper_led.name = wrapper_name;
+	dither_data->wrapper_led.brightness_set = dither_set;
+	dither_data->wrapper_led.max_brightness = LED_FULL;
+	dither_data->wrapper_led.flags = LED_CORE_SUSPENDRESUME;
+
+	if (led_classdev_register(led->dev, &dither_data->wrapper_led))
+		goto err_register_wrapper;
+
+	setup_timer(&dither_data->timer, dither_timer, (unsigned long)dither_data);
+
+	led->trigger_data = dither_data;
+	led->activated = true;
+
+	dither_data->updated_at = ktime_get();
+	dither_data->error = 0;
+	dither_set(&dither_data->wrapper_led, 0);
+	return;
+
+err_register_wrapper:
+	kfree(dither_data);
+err_dither_data:
+	kfree(wrapper_name);
+err_wrapper_name:
+	return;
+}
+
+static void ledtrig_dither_deactivate(struct led_classdev *led)
+{
+	struct ledtrig_dither_data *dither_data = led->trigger_data;
+
+	if (led->activated) {
+		led_classdev_unregister(&dither_data->wrapper_led);
+		del_timer_sync(&dither_data->timer);
+		kfree(dither_data->wrapper_led.name);
+		kfree(dither_data);
+		led->activated = false;
+	}
+}
+
+static struct led_trigger ledtrig_dither = {
+	.name = "dither",
+	.activate = ledtrig_dither_activate,
+	.deactivate = ledtrig_dither_deactivate
+};
+
+static int __init ledtrig_dither_init(void)
+{
+	led_trigger_register(&ledtrig_dither);
+	return 0;
+}
+module_init(ledtrig_dither_init);
+
+static void __exit ledtrig_dither_exit(void)
+{
+	led_trigger_unregister(&ledtrig_dither);
+}
+module_exit(ledtrig_dither_exit);
+
+MODULE_DESCRIPTION("LED Trigger for fake brightness adjustment via dithering");
+MODULE_AUTHOR("Paulo Costa");
+MODULE_LICENSE("GPL");
-- 
2.11.0

^ permalink raw reply related	[flat|nested] 6+ messages in thread

* Re: [RFC] ledtrig-dither: A Poor man's adjustable LED brightness.
  2017-08-15 21:35 [RFC] ledtrig-dither: A Poor man's adjustable LED brightness Paulo Costa
@ 2017-08-15 22:34 ` Pavel Machek
  2017-08-16  3:55   ` Paulo Costa
  0 siblings, 1 reply; 6+ messages in thread
From: Pavel Machek @ 2017-08-15 22:34 UTC (permalink / raw)
  To: Paulo Costa; +Cc: linux-leds

[-- Attachment #1: Type: text/plain, Size: 500 bytes --]

On Tue 2017-08-15 18:35:41, Paulo Costa wrote:
> While many LED drivers support adjustable brightness levels, usually
> via PWM hardware, most can only be set to ON or OFF.
> 
> Well, I wish I could adjust the brightness of every led.
> What if we can fake PWMs with kernel timers?

Hmm. Can we simply use blinking trigger to do that?

									Pavel
-- 
(english) http://www.livejournal.com/~pavelmachek
(cesky, pictures) http://atrey.karlin.mff.cuni.cz/~pavel/picture/horses/blog.html

[-- Attachment #2: Digital signature --]
[-- Type: application/pgp-signature, Size: 181 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC] ledtrig-dither: A Poor man's adjustable LED brightness.
  2017-08-15 22:34 ` Pavel Machek
@ 2017-08-16  3:55   ` Paulo Costa
  2017-08-16 22:30     ` Pavel Machek
  0 siblings, 1 reply; 6+ messages in thread
From: Paulo Costa @ 2017-08-16  3:55 UTC (permalink / raw)
  To: Pavel Machek; +Cc: linux-leds

> Hmm. Can we simply use blinking trigger to do that?

To some extent...

If you just want to have 50% brightness, set timeon=1, timeoff=1 and
you are done.

But, accurately seting the brightness to weird numbers is harder.
E.g., To get 37% brightness your only chance is to set timeon=37,
timeoff=63 (Or multiples of it).

The larger delay causes very visible flickering, which sucks.



This trigger uses error propagation to continously adjusts the delay,
getting a higher blink frequency and accurate average brightness.

E.g., For 37% it will alternate between 33% (timeon=1,timeoff=2) and
50% (timeon=1,timeoff=1)



Unfortunately, it doesn't really help when the brightness is near 0%
or near 100%.
E.g., for 1% brightness, this trigger it is equivalent to
timeon=1,timeoff=99 -- Flickering is very visible.

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC] ledtrig-dither: A Poor man's adjustable LED brightness.
  2017-08-16  3:55   ` Paulo Costa
@ 2017-08-16 22:30     ` Pavel Machek
  2017-08-17  4:21       ` Paulo Costa
  0 siblings, 1 reply; 6+ messages in thread
From: Pavel Machek @ 2017-08-16 22:30 UTC (permalink / raw)
  To: Paulo Costa; +Cc: linux-leds

[-- Attachment #1: Type: text/plain, Size: 1378 bytes --]

On Wed 2017-08-16 00:55:04, Paulo Costa wrote:
> > Hmm. Can we simply use blinking trigger to do that?
> 
> To some extent...
> 
> If you just want to have 50% brightness, set timeon=1, timeoff=1 and
> you are done.
> 
> But, accurately seting the brightness to weird numbers is harder.
> E.g., To get 37% brightness your only chance is to set timeon=37,
> timeoff=63 (Or multiples of it).
> 
> The larger delay causes very visible flickering, which sucks.

Well, to set 37% brightness you just set timeon=1 timeoff=2 ;-).

> This trigger uses error propagation to continously adjusts the delay,
> getting a higher blink frequency and accurate average brightness.
> 
> E.g., For 37% it will alternate between 33% (timeon=1,timeoff=2) and
> 50% (timeon=1,timeoff=1)

Hmm, ok, clever.

> Unfortunately, it doesn't really help when the brightness is near 0%
> or near 100%.
> E.g., for 1% brightness, this trigger it is equivalent to
> timeon=1,timeoff=99 -- Flickering is very visible.

Hmm. You could do led=on, udelay(100), led=off. Do that 100 times a
second, and I believe you'll get quite nice low levels.

Now... nice hack. Probably can be improved. But is it suitable for
mainline?

									Pavel
-- 
(english) http://www.livejournal.com/~pavelmachek
(cesky, pictures) http://atrey.karlin.mff.cuni.cz/~pavel/picture/horses/blog.html

[-- Attachment #2: Digital signature --]
[-- Type: application/pgp-signature, Size: 181 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC] ledtrig-dither: A Poor man's adjustable LED brightness.
  2017-08-16 22:30     ` Pavel Machek
@ 2017-08-17  4:21       ` Paulo Costa
  2017-08-28  9:10         ` Pavel Machek
  0 siblings, 1 reply; 6+ messages in thread
From: Paulo Costa @ 2017-08-17  4:21 UTC (permalink / raw)
  To: Pavel Machek; +Cc: linux-leds

> Hmm. You could do led=on, udelay(100), led=off. Do that 100 times a
> second, and I believe you'll get quite nice low levels.

I don't want to keep the CPU busy on a delay() just for blinking led,
even if only for a few micros.

> Now... nice hack. Probably can be improved. But is it suitable for
> mainline?

Thank you =)

It's a proof of concept for now, definitely not ready for mainline.

Instead of a LED trigger + wrapper LED entry, I think this should kick in
if we set 'myled/dither=1' or something like that.

Now, do you think it would be a nice addition / Should I put more work on it?

Thanks for looking into it

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: [RFC] ledtrig-dither: A Poor man's adjustable LED brightness.
  2017-08-17  4:21       ` Paulo Costa
@ 2017-08-28  9:10         ` Pavel Machek
  0 siblings, 0 replies; 6+ messages in thread
From: Pavel Machek @ 2017-08-28  9:10 UTC (permalink / raw)
  To: Paulo Costa; +Cc: linux-leds

[-- Attachment #1: Type: text/plain, Size: 1100 bytes --]

On Thu 2017-08-17 01:21:37, Paulo Costa wrote:
> > Hmm. You could do led=on, udelay(100), led=off. Do that 100 times a
> > second, and I believe you'll get quite nice low levels.
> 
> I don't want to keep the CPU busy on a delay() just for blinking led,
> even if only for a few micros.

Well... it will enable you to have nice low brigtness. Few
microseconds should not be a problem.

> > Now... nice hack. Probably can be improved. But is it suitable for
> > mainline?
> 
> Thank you =)
> 
> It's a proof of concept for now, definitely not ready for mainline.
> 
> Instead of a LED trigger + wrapper LED entry, I think this should kick in
> if we set 'myled/dither=1' or something like that.
> 
> Now, do you think it would be a nice addition / Should I put more work on it?

Actually, I'm not sure. Sounds a bit too hacky to me... Your
choice. If your goal is to learn about kernel, you are on the right
track :-).

									Pavel
-- 
(english) http://www.livejournal.com/~pavelmachek
(cesky, pictures) http://atrey.karlin.mff.cuni.cz/~pavel/picture/horses/blog.html

[-- Attachment #2: Digital signature --]
[-- Type: application/pgp-signature, Size: 181 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2017-08-28  9:10 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2017-08-15 21:35 [RFC] ledtrig-dither: A Poor man's adjustable LED brightness Paulo Costa
2017-08-15 22:34 ` Pavel Machek
2017-08-16  3:55   ` Paulo Costa
2017-08-16 22:30     ` Pavel Machek
2017-08-17  4:21       ` Paulo Costa
2017-08-28  9:10         ` Pavel Machek

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.