diff --git a/Documentation/ABI/testing/sysfs-class-led b/Documentation/ABI/testing/sysfs-class-led index 491cdeedc195..5f67f7ab277b 100644 --- a/Documentation/ABI/testing/sysfs-class-led +++ b/Documentation/ABI/testing/sysfs-class-led @@ -23,6 +23,23 @@ Description: If the LED does not support different brightness levels, this should be 1. +What: /sys/class/leds//brightness_hw_changed +Date: January 2017 +KernelVersion: 4.11 +Description: + Last hardware set brightness level for this LED. Some LEDs + may be changed autonomously by hardware/firmware. Only LEDs + where this happens and the driver can detect this, will have + this file. + + This file supports poll() to detect when the hardware changes + the brightness. + + Reading this file will return the last brightness level set + by the hardware, this may be different from the current + brightness. Reading this file when no hw brightness change + event has happened will return an ENODATA error. + What: /sys/class/leds//trigger Date: March 2006 KernelVersion: 2.6.17 diff --git a/Documentation/devicetree/bindings/leds/common.txt b/Documentation/devicetree/bindings/leds/common.txt index 696be5792625..24b656014089 100644 --- a/Documentation/devicetree/bindings/leds/common.txt +++ b/Documentation/devicetree/bindings/leds/common.txt @@ -61,16 +61,24 @@ property can be omitted. Examples: -system-status { - label = "Status"; - linux,default-trigger = "heartbeat"; - ... +gpio-leds { + compatible = "gpio-leds"; + + system-status { + label = "Status"; + linux,default-trigger = "heartbeat"; + gpios = <&gpio0 0 GPIO_ACTIVE_HIGH>; + }; }; -camera-flash { - label = "Flash"; - led-sources = <0>, <1>; - led-max-microamp = <50000>; - flash-max-microamp = <320000>; - flash-max-timeout-us = <500000>; +max77693-led { + compatible = "maxim,max77693-led"; + + camera-flash { + label = "Flash"; + led-sources = <0>, <1>; + led-max-microamp = <50000>; + flash-max-microamp = <320000>; + flash-max-timeout-us = <500000>; + }; }; diff --git a/Documentation/leds/leds-class.txt b/Documentation/leds/leds-class.txt index f1f7ec9f5cc5..836cb16d6f09 100644 --- a/Documentation/leds/leds-class.txt +++ b/Documentation/leds/leds-class.txt @@ -65,6 +65,21 @@ LED subsystem core exposes following API for setting brightness: blinking, returns -EBUSY if software blink fallback is enabled. +LED registration API +==================== + +A driver wanting to register a LED classdev for use by other drivers / +userspace needs to allocate and fill a led_classdev struct and then call +[devm_]led_classdev_register. If the non devm version is used the driver +must call led_classdev_unregister from its remove function before +free-ing the led_classdev struct. + +If the driver can detect hardware initiated brightness changes and thus +wants to have a brightness_hw_changed attribute then the LED_BRIGHT_HW_CHANGED +flag must be set in flags before registering. Calling +led_classdev_notify_brightness_hw_changed on a classdev not registered with +the LED_BRIGHT_HW_CHANGED flag is a bug and will trigger a WARN_ON. + Hardware accelerated blink of LEDs ================================== diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig index c621cbbb5768..275f467956ee 100644 --- a/drivers/leds/Kconfig +++ b/drivers/leds/Kconfig @@ -29,6 +29,15 @@ config LEDS_CLASS_FLASH for the flash related features of a LED device. It can be built as a module. +config LEDS_BRIGHTNESS_HW_CHANGED + bool "LED Class brightness_hw_changed attribute support" + depends on LEDS_CLASS + help + This option enables support for the brightness_hw_changed attribute + for led sysfs class devices under /sys/class/leds. + + See Documentation/ABI/testing/sysfs-class-led for details. + comment "LED drivers" config LEDS_88PM860X diff --git a/drivers/leds/led-class.c b/drivers/leds/led-class.c index 326ee6e925a2..f2b0a80a62b4 100644 --- a/drivers/leds/led-class.c +++ b/drivers/leds/led-class.c @@ -103,6 +103,68 @@ static const struct attribute_group *led_groups[] = { NULL, }; +#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED +static ssize_t brightness_hw_changed_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct led_classdev *led_cdev = dev_get_drvdata(dev); + + if (led_cdev->brightness_hw_changed == -1) + return -ENODATA; + + return sprintf(buf, "%u\n", led_cdev->brightness_hw_changed); +} + +static DEVICE_ATTR_RO(brightness_hw_changed); + +static int led_add_brightness_hw_changed(struct led_classdev *led_cdev) +{ + struct device *dev = led_cdev->dev; + int ret; + + ret = device_create_file(dev, &dev_attr_brightness_hw_changed); + if (ret) { + dev_err(dev, "Error creating brightness_hw_changed\n"); + return ret; + } + + led_cdev->brightness_hw_changed_kn = + sysfs_get_dirent(dev->kobj.sd, "brightness_hw_changed"); + if (!led_cdev->brightness_hw_changed_kn) { + dev_err(dev, "Error getting brightness_hw_changed kn\n"); + device_remove_file(dev, &dev_attr_brightness_hw_changed); + return -ENXIO; + } + + return 0; +} + +static void led_remove_brightness_hw_changed(struct led_classdev *led_cdev) +{ + sysfs_put(led_cdev->brightness_hw_changed_kn); + device_remove_file(led_cdev->dev, &dev_attr_brightness_hw_changed); +} + +void led_classdev_notify_brightness_hw_changed(struct led_classdev *led_cdev, + enum led_brightness brightness) +{ + if (WARN_ON(!led_cdev->brightness_hw_changed_kn)) + return; + + led_cdev->brightness_hw_changed = brightness; + sysfs_notify_dirent(led_cdev->brightness_hw_changed_kn); +} +EXPORT_SYMBOL_GPL(led_classdev_notify_brightness_hw_changed); +#else +static int led_add_brightness_hw_changed(struct led_classdev *led_cdev) +{ + return 0; +} +static void led_remove_brightness_hw_changed(struct led_classdev *led_cdev) +{ +} +#endif + /** * led_classdev_suspend - suspend an led_classdev. * @led_cdev: the led_classdev to suspend. @@ -204,9 +266,20 @@ int led_classdev_register(struct device *parent, struct led_classdev *led_cdev) dev_warn(parent, "Led %s renamed to %s due to name collision", led_cdev->name, dev_name(led_cdev->dev)); + if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) { + ret = led_add_brightness_hw_changed(led_cdev); + if (ret) { + device_unregister(led_cdev->dev); + return ret; + } + } + led_cdev->work_flags = 0; #ifdef CONFIG_LEDS_TRIGGERS init_rwsem(&led_cdev->trigger_lock); +#endif +#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED + led_cdev->brightness_hw_changed = -1; #endif mutex_init(&led_cdev->led_access); /* add to the list of leds */ @@ -256,6 +329,9 @@ void led_classdev_unregister(struct led_classdev *led_cdev) flush_work(&led_cdev->set_brightness_work); + if (led_cdev->flags & LED_BRIGHT_HW_CHANGED) + led_remove_brightness_hw_changed(led_cdev); + device_unregister(led_cdev->dev); down_write(&leds_list_lock); diff --git a/drivers/leds/leds-ktd2692.c b/drivers/leds/leds-ktd2692.c index bf23ba191ad0..45296aaca9da 100644 --- a/drivers/leds/leds-ktd2692.c +++ b/drivers/leds/leds-ktd2692.c @@ -270,15 +270,15 @@ static int ktd2692_parse_dt(struct ktd2692_context *led, struct device *dev, return -ENXIO; led->ctrl_gpio = devm_gpiod_get(dev, "ctrl", GPIOD_ASIS); - if (IS_ERR(led->ctrl_gpio)) { - ret = PTR_ERR(led->ctrl_gpio); + ret = PTR_ERR_OR_ZERO(led->ctrl_gpio); + if (ret) { dev_err(dev, "cannot get ctrl-gpios %d\n", ret); return ret; } led->aux_gpio = devm_gpiod_get(dev, "aux", GPIOD_ASIS); - if (IS_ERR(led->aux_gpio)) { - ret = PTR_ERR(led->aux_gpio); + ret = PTR_ERR_OR_ZERO(led->aux_gpio); + if (ret) { dev_err(dev, "cannot get aux-gpios %d\n", ret); return ret; } diff --git a/drivers/leds/trigger/ledtrig-heartbeat.c b/drivers/leds/trigger/ledtrig-heartbeat.c index c9f386213e9e..e6f2f8b9f09a 100644 --- a/drivers/leds/trigger/ledtrig-heartbeat.c +++ b/drivers/leds/trigger/ledtrig-heartbeat.c @@ -43,6 +43,9 @@ static void led_heartbeat_function(unsigned long data) return; } + if (test_and_clear_bit(LED_BLINK_BRIGHTNESS_CHANGE, &led_cdev->work_flags)) + led_cdev->blink_brightness = led_cdev->new_blink_brightness; + /* acts like an actual heart beat -- ie thump-thump-pause... */ switch (heartbeat_data->phase) { case 0: @@ -59,26 +62,26 @@ static void led_heartbeat_function(unsigned long data) delay = msecs_to_jiffies(70); heartbeat_data->phase++; if (!heartbeat_data->invert) - brightness = led_cdev->max_brightness; + brightness = led_cdev->blink_brightness; break; case 1: delay = heartbeat_data->period / 4 - msecs_to_jiffies(70); heartbeat_data->phase++; if (heartbeat_data->invert) - brightness = led_cdev->max_brightness; + brightness = led_cdev->blink_brightness; break; case 2: delay = msecs_to_jiffies(70); heartbeat_data->phase++; if (!heartbeat_data->invert) - brightness = led_cdev->max_brightness; + brightness = led_cdev->blink_brightness; break; default: delay = heartbeat_data->period - heartbeat_data->period / 4 - msecs_to_jiffies(70); heartbeat_data->phase = 0; if (heartbeat_data->invert) - brightness = led_cdev->max_brightness; + brightness = led_cdev->blink_brightness; break; } @@ -133,7 +136,10 @@ static void heartbeat_trig_activate(struct led_classdev *led_cdev) setup_timer(&heartbeat_data->timer, led_heartbeat_function, (unsigned long) led_cdev); heartbeat_data->phase = 0; + if (!led_cdev->blink_brightness) + led_cdev->blink_brightness = led_cdev->max_brightness; led_heartbeat_function(heartbeat_data->timer.data); + set_bit(LED_BLINK_SW, &led_cdev->work_flags); led_cdev->activated = true; } @@ -145,6 +151,7 @@ static void heartbeat_trig_deactivate(struct led_classdev *led_cdev) del_timer_sync(&heartbeat_data->timer); device_remove_file(led_cdev->dev, &dev_attr_invert); kfree(heartbeat_data); + clear_bit(LED_BLINK_SW, &led_cdev->work_flags); led_cdev->activated = false; } } diff --git a/include/linux/leds.h b/include/linux/leds.h index 569cb531094c..38c0bd7ca107 100644 --- a/include/linux/leds.h +++ b/include/linux/leds.h @@ -13,6 +13,7 @@ #define __LINUX_LEDS_H_INCLUDED #include +#include #include #include #include @@ -27,6 +28,7 @@ struct device; enum led_brightness { LED_OFF = 0, + LED_ON = 1, LED_HALF = 127, LED_FULL = 255, }; @@ -46,6 +48,7 @@ struct led_classdev { #define LED_DEV_CAP_FLASH (1 << 18) #define LED_HW_PLUGGABLE (1 << 19) #define LED_PANIC_INDICATOR (1 << 20) +#define LED_BRIGHT_HW_CHANGED (1 << 21) /* set_brightness_work / blink_timer flags, atomic, private. */ unsigned long work_flags; @@ -110,6 +113,11 @@ struct led_classdev { bool activated; #endif +#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED + int brightness_hw_changed; + struct kernfs_node *brightness_hw_changed_kn; +#endif + /* Ensures consistent access to the LED Flash Class device */ struct mutex led_access; }; @@ -422,4 +430,12 @@ static inline void ledtrig_cpu(enum cpu_led_event evt) } #endif +#ifdef CONFIG_LEDS_BRIGHTNESS_HW_CHANGED +extern void led_classdev_notify_brightness_hw_changed( + struct led_classdev *led_cdev, enum led_brightness brightness); +#else +static inline void led_classdev_notify_brightness_hw_changed( + struct led_classdev *led_cdev, enum led_brightness brightness) { } +#endif + #endif /* __LINUX_LEDS_H_INCLUDED */ diff --git a/tools/leds/Makefile b/tools/leds/Makefile index c03a79ebf9c8..078b666fd78b 100644 --- a/tools/leds/Makefile +++ b/tools/leds/Makefile @@ -3,11 +3,11 @@ CC = $(CROSS_COMPILE)gcc CFLAGS = -Wall -Wextra -g -I../../include/uapi -all: uledmon +all: uledmon led_hw_brightness_mon %: %.c $(CC) $(CFLAGS) -o $@ $^ clean: - $(RM) uledmon + $(RM) uledmon led_hw_brightness_mon .PHONY: all clean diff --git a/tools/leds/led_hw_brightness_mon.c b/tools/leds/led_hw_brightness_mon.c new file mode 100644 index 000000000000..64642ccfe442 --- /dev/null +++ b/tools/leds/led_hw_brightness_mon.c @@ -0,0 +1,84 @@ +/* + * led_hw_brightness_mon.c + * + * This program monitors LED brightness level changes having its origin + * in hardware/firmware, i.e. outside of kernel control. + * A timestamp and brightness value is printed each time the brightness changes. + * + * Usage: led_hw_brightness_mon + * + * is the name of the LED class device to be monitored. Pressing + * CTRL+C will exit. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +int main(int argc, char const *argv[]) +{ + int fd, ret; + char brightness_file_path[LED_MAX_NAME_SIZE + 11]; + struct pollfd pollfd; + struct timespec ts; + char buf[11]; + + if (argc != 2) { + fprintf(stderr, "Requires argument\n"); + return 1; + } + + snprintf(brightness_file_path, LED_MAX_NAME_SIZE, + "/sys/class/leds/%s/brightness_hw_changed", argv[1]); + + fd = open(brightness_file_path, O_RDONLY); + if (fd == -1) { + printf("Failed to open %s file\n", brightness_file_path); + return 1; + } + + /* + * read may fail if no hw brightness change has occurred so far, + * but it is required to avoid spurious poll notifications in + * the opposite case. + */ + read(fd, buf, sizeof(buf)); + + pollfd.fd = fd; + pollfd.events = POLLPRI; + + while (1) { + ret = poll(&pollfd, 1, -1); + if (ret == -1) { + printf("Failed to poll %s file (%d)\n", + brightness_file_path, ret); + ret = 1; + break; + } + + clock_gettime(CLOCK_MONOTONIC, &ts); + + ret = read(fd, buf, sizeof(buf)); + if (ret < 0) + break; + + ret = lseek(pollfd.fd, 0, SEEK_SET); + if (ret < 0) { + printf("lseek failed (%d)\n", ret); + break; + } + + printf("[%ld.%09ld] %d\n", ts.tv_sec, ts.tv_nsec, atoi(buf)); + } + + close(fd); + + return ret; +}