-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Add generic gpio leds driver with sample #21578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||||||
| # Copyright (c) 2019 Nordic Semiconductor ASA | ||||||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||||||
|
|
||||||||||
| config LED_GPIO | ||||||||||
| bool "LED GPIO driver" | ||||||||||
| depends on GPIO | ||||||||||
| help | ||||||||||
| This option enables support for the LEDs connected to GPIO | ||||||||||
| outputs. To be useful the particular board must have LEDs | ||||||||||
| and they must be connected to the GPIO lines. | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
|
|
||||||||||
| if LED_GPIO | ||||||||||
|
|
||||||||||
| config LED_SOFTWARE_BLINK | ||||||||||
| bool "Software blink" | ||||||||||
| help | ||||||||||
| Support for an LED software blink based on | ||||||||||
| system timers | ||||||||||
|
|
||||||||||
| module = LED_GPIO | ||||||||||
| module-str = LED GPIO | ||||||||||
| source "${ZEPHYR_BASE}/subsys/logging/Kconfig.template.log_config" | ||||||||||
|
|
||||||||||
| endif # LED_GPIO | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,264 @@ | ||
| /* | ||
| * Copyright (c) 2019 Nordic Semiconductor ASA | ||
| * | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| #include <drivers/gpio.h> | ||
| #include <drivers/led.h> | ||
| #include <logging/log.h> | ||
|
|
||
| LOG_MODULE_REGISTER(led_gpio, CONFIG_LED_GPIO_LOG_LEVEL); | ||
|
|
||
| #define DT_DRV_COMPAT gpio_leds | ||
| #define LED_DATA(node_id) \ | ||
| { \ | ||
| { \ | ||
| DT_GPIO_LABEL(node_id, gpios), \ | ||
| DT_GPIO_PIN(node_id, gpios), \ | ||
| DT_GPIO_FLAGS(node_id, gpios) \ | ||
mbolivar-nordic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, \ | ||
| DT_LABEL(node_id), \ | ||
| DT_PROP(node_id, id) \ | ||
| }, | ||
| #define LED_SW_BLINK BIT(0) | ||
| #define LED_BLINK_DISABLE BIT(1) | ||
|
|
||
| struct gpio_cfg { | ||
|
||
| const char *port; | ||
| gpio_pin_t pin; | ||
| gpio_dt_flags_t flags; | ||
| }; | ||
|
|
||
| struct led_dts_cfg { | ||
| struct gpio_cfg gpio; | ||
| const char *label; | ||
| uint32_t id; | ||
| }; | ||
|
|
||
| struct led_gpio_data { | ||
| struct device *gpio_dev; | ||
| const struct led_dts_cfg *cfg; | ||
| #if defined(CONFIG_LED_SOFTWARE_BLINK) | ||
|
||
| struct k_timer timer; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is that OK to use a timer here ? Behind gpio_pin_set(), we can have an I2C GPIO expander which may need to sleep and to wait for interrupts. Is that OK to sleep in a timer handler in Zephyr ? In Linux, when a LED driver can't configure a LED without sleeping a work queue is used. Note that the same context issue may apply to all the LED callbacks. AFAIK there is nothing preventing from calling a LED API function from an interrupt context.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be a problem. Maybe it was not possible to sleep in the I will check, how Linux handle LEDs. I would like to have a possibility to call LED API from irq context. BTW There is no any word in
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, we have SPI/I2C GPIO expander driver which works synchronously
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked the Linux. Just as I thought it used combination of software timer and workqueue to handle software blink.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So since I know
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
They would not be synchronous. Using work queue adds much more jitter and latency compared to timer. From on chip gpios this can be seen as unnecessary limitation.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the GPIO API can return the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bluetooth should not be affected by items in the system work queue unless those items misbehave. I don't think this driver should define a workqueue, but agree we need better control of which work queue is to be used by drivers that need them.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, I think that software LED blinking should be a fallback for all the LED devices/drivers without hardware blinking capabilities, and not only GPIO LEDs. Moreover context should be handled from the led_set_{on,off,brightness} functions. Software LED blinking is only a user among others. To make a decision between calling the LED driver's callback directly or defer it to a workqueue, this functions need to be aware of the context (interrupt, thread) and also if the driver may sleep. I don't think that calling the driver's callback and handling a -EWOULDBLOCK error is correct. GPIO is not the only backend we have to deal with. It would be better to have a "can_sleep" flag advertised by the LED drivers. But to be efficient this needs some support from all the possible backends (including GPIO)...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
GPIO is not the only API where an operation may not be isr-ok. The standard way of nicely supporting code that tries something that can't be done in the calling (interrupt) context is to return I understand that this seems a little odd, but it is the most flexible/least impact solution we have available. Since this LED driver can't tell whether a particular GPIO has to support can change pin state from interrupt context, so there is no way it could advertise a "can sleep" flag. |
||
| atomic_t flags; | ||
| uint32_t blink_delay_on; | ||
| uint32_t blink_delay_off; | ||
| bool next_state; | ||
| #endif | ||
| bool state; | ||
| }; | ||
|
|
||
| struct led_gpio_cfg { | ||
| const struct led_dts_cfg *led; | ||
| uint32_t led_cnt; | ||
| }; | ||
|
|
||
| static inline struct led_gpio_data *get_dev_data(struct device *dev) | ||
| { | ||
| return dev->driver_data; | ||
| } | ||
|
|
||
| static inline const struct led_gpio_cfg *get_dev_config(struct device *dev) | ||
| { | ||
| return dev->config_info; | ||
| } | ||
|
|
||
| static struct led_gpio_data *led_parameters_get(struct device *dev, | ||
| uint32_t led) | ||
| { | ||
| const struct led_gpio_cfg *cfg = get_dev_config(dev); | ||
| struct led_gpio_data *data = get_dev_data(dev); | ||
|
|
||
| for (size_t i = 0; i < cfg->led_cnt; i++) { | ||
| if (data[i].cfg->id == led) { | ||
| return &data[i]; | ||
| } | ||
| } | ||
|
|
||
| return NULL; | ||
| } | ||
|
|
||
| static int gpio_led_set_no_delay(struct led_gpio_data *data, bool state) | ||
| { | ||
| int err; | ||
|
|
||
| data->state = state; | ||
|
|
||
| err = gpio_pin_set(data->gpio_dev, data->cfg->gpio.pin, | ||
| state); | ||
| if (err) { | ||
| LOG_DBG("%s turn %s error %d", | ||
| data->cfg->label, | ||
| state ? "on" : "off", | ||
| err); | ||
| } else { | ||
| LOG_DBG("%s turn %s", data->cfg->label, | ||
| state ? "on" : "off"); | ||
| } | ||
|
|
||
| return err; | ||
| } | ||
|
|
||
| static int gpio_led_set(struct led_gpio_data *data, bool state) | ||
| { | ||
| #if defined(CONFIG_LED_SOFTWARE_BLINK) | ||
| if (atomic_test_bit(&data->flags, LED_SW_BLINK)) { | ||
| data->next_state = state; | ||
|
|
||
| atomic_set_bit(&data->flags, LED_BLINK_DISABLE); | ||
| k_timer_start(&data->timer, K_NO_WAIT, K_NO_WAIT); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am sure there is a good reason for not stopping the timer here and for handling the set from the timer handler. But since I don't see, I am asking :)
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is here to avoid some race conditions when during the software blink is used, the led migth be set to different state from a higher priority interrupt or thread than a timer handler also the set function can be called when timer is pending. So setting the LEDs from one context and using the atomic flags eliminates this problem. |
||
|
|
||
| return 0; | ||
|
|
||
| } | ||
| #endif /* defined(CONFIG_LED_SOFTWARE_BLINK) */ | ||
|
|
||
| return gpio_led_set_no_delay(data, state); | ||
| } | ||
|
|
||
| static int gpio_led_on(struct device *dev, uint32_t led) | ||
| { | ||
| struct led_gpio_data *data = led_parameters_get(dev, led); | ||
|
|
||
| if (!data) { | ||
| return -EINVAL; | ||
| } | ||
|
|
||
| return gpio_led_set(data, true); | ||
| } | ||
|
|
||
| static int gpio_led_off(struct device *dev, uint32_t led) | ||
| { | ||
| struct led_gpio_data *data = led_parameters_get(dev, led); | ||
|
|
||
| if (!data) { | ||
| return -EINVAL; | ||
| } | ||
|
|
||
| return gpio_led_set(data, false); | ||
| } | ||
|
|
||
| #if defined(CONFIG_LED_SOFTWARE_BLINK) | ||
| static void stop_sw_blink(struct led_gpio_data *data) | ||
| { | ||
| k_timer_stop(&data->timer); | ||
| data->blink_delay_on = 0; | ||
| data->blink_delay_off = 0; | ||
| atomic_clear_bit(&data->flags, LED_SW_BLINK); | ||
| } | ||
|
|
||
| static void led_timer_handler(struct k_timer *timer) | ||
| { | ||
| struct led_gpio_data *data; | ||
| uint32_t delay; | ||
| bool state; | ||
| int err; | ||
|
|
||
| data = CONTAINER_OF(timer, struct led_gpio_data, timer); | ||
|
|
||
| if (atomic_test_and_clear_bit(&data->flags, LED_BLINK_DISABLE)) { | ||
| stop_sw_blink(data); | ||
| gpio_led_set_no_delay(data, data->next_state); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (data->state) { | ||
| delay = data->blink_delay_off; | ||
| state = 0; | ||
| } else { | ||
| delay = data->blink_delay_on; | ||
| state = 1; | ||
| } | ||
|
|
||
| err = gpio_led_set_no_delay(data, state); | ||
| if (err) { | ||
| LOG_DBG("%s blink error: %d", data->cfg->label, err); | ||
| } | ||
|
|
||
| k_timer_start(&data->timer, K_MSEC(delay), K_NO_WAIT); | ||
| } | ||
|
|
||
| static int gpio_led_blink(struct device *dev, uint32_t led, | ||
| uint32_t delay_on, uint32_t delay_off) | ||
| { | ||
| struct led_gpio_data *data = led_parameters_get(dev, led); | ||
|
|
||
| if (!delay_on) { | ||
| return gpio_led_off(dev, led); | ||
| } | ||
|
|
||
| if (!delay_off) { | ||
| return gpio_led_on(dev, led); | ||
| } | ||
|
|
||
| data->blink_delay_on = delay_on; | ||
| data->blink_delay_off = delay_off; | ||
|
|
||
| atomic_set_bit(&data->flags, LED_SW_BLINK); | ||
|
|
||
| LOG_DBG("%s blink start, delay on: %d, delay off: %d", | ||
| data->cfg->label, delay_on, delay_off); | ||
|
|
||
| k_timer_start(&data->timer, K_NO_WAIT, K_NO_WAIT); | ||
|
|
||
| return 0; | ||
| } | ||
| #endif /* defined(CONFIG_LED_SOFTWARE_BLINK) */ | ||
|
|
||
| static const struct led_driver_api led_api = { | ||
| #if defined(CONFIG_LED_SOFTWARE_BLINK) | ||
| .blink = gpio_led_blink, | ||
| #endif | ||
| .on = gpio_led_on, | ||
| .off = gpio_led_off, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe you should define the set_brightness callback as well ? It is not related with this patch but I don't see the purpose of this on/off callbacks. It seems to me that the set_brightness callback is enough to turn a LED off or on.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can define it but I would not remove this on/off callback I rather prefer to use it instead of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course. I am just saying you could add the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe just add a callback that returns -ENOTSUP? The led.h API, at least as it is currently defined, requires set_brightness to be defined. So this is technically an API violation, even though I agree with @KAGA164 that it doesn't make sense for an LED connected to a GPIO to do anything with set_brightness. |
||
| }; | ||
|
|
||
| static int gpio_led_init(struct device *dev) | ||
| { | ||
| int err; | ||
| const struct led_gpio_cfg *cfg = get_dev_config(dev); | ||
| struct led_gpio_data *data = get_dev_data(dev); | ||
|
|
||
| for (size_t i = 0; i < cfg->led_cnt; i++) { | ||
| data[i].gpio_dev = device_get_binding(cfg->led[i].gpio.port); | ||
| if (!data[i].gpio_dev) { | ||
| LOG_DBG("Failed to get GPIO device for port %s pin %d", | ||
| cfg->led[i].gpio.port, cfg->led[i].gpio.pin); | ||
| return -EINVAL; | ||
| } | ||
|
|
||
| /* Set all leds pin as output */ | ||
| err = gpio_pin_configure(data[i].gpio_dev, | ||
| cfg->led[i].gpio.pin, | ||
| (GPIO_OUTPUT_INACTIVE | | ||
| cfg->led[i].gpio.flags)); | ||
| if (err) { | ||
| LOG_DBG("Failed to set GPIO port %s pin %d as output", | ||
| cfg->led[i].gpio.port, cfg->led[i].gpio.pin); | ||
| return err; | ||
| } | ||
|
|
||
|
|
||
| data[i].cfg = &cfg->led[i]; | ||
|
|
||
| #if defined(CONFIG_LED_SOFTWARE_BLINK) | ||
| k_timer_init(&data[i].timer, led_timer_handler, NULL); | ||
| #endif | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| static const struct led_dts_cfg led_dts[] = {DT_INST_FOREACH_CHILD(0, | ||
| LED_DATA)}; | ||
| static struct led_gpio_data led_data[ARRAY_SIZE(led_dts)]; | ||
| static const struct led_gpio_cfg led_cfg = { | ||
| .led_cnt = ARRAY_SIZE(led_dts), | ||
| .led = led_dts | ||
| }; | ||
|
|
||
| DEVICE_AND_API_INIT(gpio_led, DT_PROP(DT_DRV_INST(0), driver), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason for not supporting several device/driver instances ? I think we should allow a board to define several GPIO LED nodes.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't mind many instances of this driver. I just wonder if it's really needed. I can add support for it if you want to. Is it possible to have many
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is not super useful but I don't see any reason for not supporting it. As an example, let's imagine a board with two distinct arrays of LEDs. If each array can have its own led-gpio node, then the LED indexes (or IDs) will reflect their hardware position in the array. It could be more meaningful for a user than having a big single node with the LED indexes not related with the hardware.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my opinion we can just support one instance to start with and make the driver multi-instance later on if we need to, since we don't have a use case for more right now. Nothing will break and it will be totally backwards compatible as long as we access the devices using label properties as discussed elsewhere in these comments. |
||
| gpio_led_init, led_data, | ||
| &led_cfg, POST_KERNEL, CONFIG_LED_INIT_PRIORITY, | ||
| &led_api); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,13 @@ description: GPIO LEDs parent node | |
|
|
||
| compatible: "gpio-leds" | ||
|
|
||
| properties: | ||
|
||
| driver: | ||
| required: false | ||
| type: string | ||
| description: GPIO LED driver name | ||
| default: "LED_GPIO_0" | ||
|
|
||
| child-binding: | ||
| description: GPIO LED child node | ||
| properties: | ||
|
|
@@ -15,3 +22,7 @@ child-binding: | |
| required: true | ||
| type: string | ||
| description: Human readable string describing the device (used as device_get_binding() argument) | ||
| id: | ||
|
||
| required: false | ||
| type: int | ||
| description: Id needed for the GPIO LED driver to identify an LED | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,7 +73,7 @@ __subsystem struct led_driver_api { | |
| * This routine starts blinking an LED forever with the given time period | ||
| * | ||
| * @param dev LED device | ||
| * @param led LED channel/pin | ||
| * @param led LED channel/pin/id | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| * @param delay_on Time period (in milliseconds) an LED should be ON | ||
| * @param delay_off Time period (in milliseconds) an LED should be OFF | ||
| * @return 0 on success, negative on error | ||
|
|
@@ -97,7 +97,7 @@ static inline int z_impl_led_blink(struct device *dev, uint32_t led, | |
| * Calling this function after led_blink() won't affect blinking. | ||
| * | ||
| * @param dev LED device | ||
| * @param led LED channel/pin | ||
| * @param led LED channel/pin/id | ||
| * @param value Brightness value to set in percent | ||
| * @return 0 on success, negative on error | ||
| */ | ||
|
|
@@ -119,7 +119,7 @@ static inline int z_impl_led_set_brightness(struct device *dev, uint32_t led, | |
| * This routine turns on an LED | ||
| * | ||
| * @param dev LED device | ||
| * @param led LED channel/pin | ||
| * @param led LED channel/pin/id | ||
| * @return 0 on success, negative on error | ||
| */ | ||
| __syscall int led_on(struct device *dev, uint32_t led); | ||
|
|
@@ -138,7 +138,7 @@ static inline int z_impl_led_on(struct device *dev, uint32_t led) | |
| * This routine turns off an LED | ||
| * | ||
| * @param dev LED device | ||
| * @param led LED channel/pin | ||
| * @param led LED channel/pin/id | ||
| * @return 0 on success, negative on error | ||
| */ | ||
| __syscall int led_off(struct device *dev, uint32_t led); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| cmake_minimum_required(VERSION 3.13.1) | ||
|
|
||
| include($ENV{ZEPHYR_BASE}/cmake/app/boilerplate.cmake NO_POLICY_SCOPE) | ||
| project(led_gpio) | ||
|
|
||
| FILE(GLOB app_sources src/*.c) | ||
| target_sources(app PRIVATE ${app_sources}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.