diff --git a/doc/doxygen/src/porting-boards.md b/doc/doxygen/src/porting-boards.md index 5c81b7d83f..09613e1b79 100644 --- a/doc/doxygen/src/porting-boards.md +++ b/doc/doxygen/src/porting-boards.md @@ -49,7 +49,9 @@ configurations. e.g: specific pin connections to a LCD screen, radio, etc.). Some boards might also define optimized `XTIMER_%` values (e.g. @ref XTIMER_BACKOFF). - `gpio_params.h`: if the board supports @ref drivers_saul "SAUL" then its - @ref saul_gpio_params_t is defined here. + @ref saul_gpio_params_t is defined here. (Analogously, a `adc_params.h` can + contain a @ref saul_adc_params_t, and `pwm_params.h` a @ref + saul_pwm_rgb_params_t and a @ref saul_pwm_dimmer_params_t). - other: other specific headers needed by one `BOARD` @note Header files do not need to be defined in `include/`, but if defined diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index 0d8da2b51d..d0c97ea903 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -154,6 +154,10 @@ ifneq (,$(filter saul_gpio,$(USEMODULE))) FEATURES_REQUIRED += periph_gpio endif +ifneq (,$(filter saul_pwm,$(USEMODULE))) + FEATURES_REQUIRED += periph_pwm +endif + ifneq (,$(filter saul,$(USEMODULE))) USEMODULE += phydat endif diff --git a/drivers/include/saul/periph.h b/drivers/include/saul/periph.h index 74f24c68f1..51d7a34be0 100644 --- a/drivers/include/saul/periph.h +++ b/drivers/include/saul/periph.h @@ -27,6 +27,10 @@ #include "periph/adc.h" #endif /* MODULE_SAUL_ADC */ +#if MODULE_SAUL_PWM || DOXYGEN +#include "periph/pwm.h" +#endif /* MODULE_SAUL_PWM */ + #ifdef __cplusplus extern "C" { #endif @@ -63,6 +67,115 @@ typedef struct { } saul_adc_params_t; #endif /* MODULE_SAUL_ADC */ +#if MODULE_SAUL_PWM || DOXYGEN +/** + * @brief Resolution of SAUL mapped PWMs + */ +static const uint16_t saul_pwm_resolution = 255; + +/** + * @brief SAUL PWM parameters + */ +typedef enum { + SAUL_PWM_REGULAR = (0 << 0), /**< Physical values are proportional to + average voltage levels (ie. LEDs are in + active-high, anode driven) */ + SAUL_PWM_INVERTED = (1 << 0), /**< Physical values are inverted from + average voltage levels (ie. LEDs are in + active-low, cathode driven) */ +} saul_pwm_flags_t; + +/** + * @brief Single PWM channel exposed via SAUL + * + * This does never need to be declared on its own, but is used inisde @ref + * saul_pwm_dimmer_params_t and @ref saul_pwm_rgb_params_t structs. + */ +typedef struct { + pwm_t dev; /**< PWM device backing this entry */ + uint8_t channel; /**< Channel on the PWM device */ + saul_pwm_flags_t flags; /**< Configuration flags */ +} saul_pwm_channel_t; + +/** @brief Default value for @ref SAUL_PWM_FREQ */ +#define SAUL_PWM_FREQ_DEFAULT 1000 + +/** + * @brief Define the PWM frequency for LEDs + * + * This frequency is requested from the PWM driver. As the per @ref pwm_init, + * the actual frequency may be lower, and the SAUL wrapper does not place a + * limit there. + * + * Frequencies of above 200Hz usually give a smooth visual experience. The + * higher 1kHz is picked as a default as some devices can't go that low with + * their timer. + * + * This is typically set in the board's `pwm_params.h`. + */ +/* This is not applied here as it would later need to be undef'd; actual + * application of the default happens in auto_init_saul_pwm.c */ +#if DOXYGEN +#define SAUL_PWM_FREQ SAUL_PWM_FREQ_DEFAULT +#endif + +/** + * @brief Suppress saul_pwm's dimmer generation + * + * This can be defined in `pwm_params.h` if the saul_pwm module is used, but no + * dimmers (and only RGB LEDs) are in use. Then, no @ref saul_pwm_dimmer_params + * needs to be set. + */ +#if DOXYGEN +#define SAUL_PWM_NO_DIMMER +#endif + +/** + * @brief PWM channels mapped to dimmer-style registration entries + * + * This is used to define a `static const saul_pwm_dimmer_params_t + * saul_pwm_dimer_params[]` in a board's `pwm_params.h` for use by the saul_pwm + * module. If the module is used but only RGB LEDs are present, a @ref + * SAUL_PWM_NO_DIMMER define can be set instead. + */ +typedef struct { + const char *name; /**< Name of the device connected to this + channel */ + saul_pwm_channel_t channel; /**< Full channel description (device, channel) + along with flags that indicate whether high + PWM values are dark or bright*/ +} saul_pwm_dimmer_params_t; + +/** + * @brief Suppress saul_pwm's RGB LED generation + * + * This can be defined in `pwm_params.h` if the saul_pwm module is used, but no + * RGB LEDs (and only dimmers) are in use. Then, no @ref saul_pwm_rgb_params_t + * needs to be set. + */ +#if DOXYGEN +#define SAUL_PWM_NO_RGB +#endif + +/** + * @brief PWM channels mapped to RGB LED registration entries + * + * This is used to define a `static const saul_pwm_rgb_params_t + * saul_pwm_rgb_params[]` in a board's `pwm_params.h` for use by the saul_pwm + * module. If the module is used but only dimmers are present, a @ref + * SAUL_PWM_NO_RGB define can be set instead. + */ +typedef struct { + const char *name; /**< Name of the device connected to these + channels */ + saul_pwm_channel_t channels[3]; /**< Full channel description (device, channel) + along with flags that indicate whether high + PWM values are dark or bright*/ +} saul_pwm_rgb_params_t; + + +#endif /* MODULE_SAUL_PWM */ + #ifdef __cplusplus } #endif diff --git a/drivers/saul/Makefile b/drivers/saul/Makefile index 39dc655787..0e9946400c 100644 --- a/drivers/saul/Makefile +++ b/drivers/saul/Makefile @@ -6,5 +6,8 @@ endif ifneq (,$(filter saul_adc,$(USEMODULE))) SRC += adc_saul.c endif +ifneq (,$(filter saul_pwm,$(USEMODULE))) + SRC += pwm_saul.c +endif include $(RIOTBASE)/Makefile.base diff --git a/drivers/saul/init_devs/auto_init_saul_pwm.c b/drivers/saul/init_devs/auto_init_saul_pwm.c new file mode 100644 index 0000000000..1fe8b4a1c5 --- /dev/null +++ b/drivers/saul/init_devs/auto_init_saul_pwm.c @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2015 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + * + */ + +/* + * @ingroup sys_auto_init_saul + * @{ + * + * @file + * @brief Auto initialization of PWM pins directly mapped to SAUL reg + * + * @author Christian Amsüss + * + * When this module is used, any PWM device assigned inside the configuration + * structs inside `pwm_params.h` in the @ref saul_pwm_dimmer_params_t and @ref + * saul_pwm_rgb_params_t is initialized at startup for 8-bit dimming at about + * 1kHz, and the indicated channels are exposed via SAUL. + * + * @} + */ + +#include "log.h" +#include "saul_reg.h" +#include "saul/periph.h" +#include "pwm_params.h" +#include "periph/pwm.h" + +#if !defined(SAUL_PWM_FREQ) +#define SAUL_PWM_FREQ SAUL_PWM_FREQ_DEFAULT +#endif + +/** + * @brief Define the number of configured dimmers + */ +#ifndef SAUL_PWM_NO_DIMMER +#define SAUL_PWM_DIMMER_NUMOF ARRAY_SIZE(saul_pwm_dimmer_params) +#else +#define SAUL_PWM_DIMMER_NUMOF 0 +static const saul_pwm_dimmer_params_t saul_pwm_dimmer_params[0]; +#endif + +/** + * @brief Define the number of configured RGB LEDs + */ +#ifndef SAUL_PWM_NO_RGB +#define SAUL_PWM_RGB_NUMOF ARRAY_SIZE(saul_pwm_rgb_params) +#else +#define SAUL_PWM_RGB_NUMOF 0 +static const saul_pwm_rgb_params_t saul_pwm_rgb_params[0]; +#endif + +/** + * @brief Memory for the registry RGB LED entries + */ +/* The static variable will be unused in the 0 case and thus not emitted. */ +static saul_reg_t saul_reg_entries_rgb[SAUL_PWM_RGB_NUMOF]; + +/** + * @brief Memory for the registry dimmer entries + */ +/* The static variable will be unused in the 0 case and thus not emitted. */ +static saul_reg_t saul_reg_entries_dimmer[SAUL_PWM_DIMMER_NUMOF]; + +/** + * @brief Reference to the driver for single-channel dimmers + */ +extern saul_driver_t dimmer_saul_driver; + +/** + * @brief Reference to the driver for RGB LEDs + */ +extern saul_driver_t rgb_saul_driver; + +/** + * Configure a PWM driver for LED output (1kHz, 8bit) + */ +static int configure(pwm_t dev) +{ + LOG_DEBUG("[auto_init_saul] initializing PWM %u for LED operation,", dev); + uint32_t freq = pwm_init(dev, PWM_LEFT, SAUL_PWM_FREQ, saul_pwm_resolution); + LOG_DEBUG(" actual frequency %lu,\n", freq); + return freq != 0 ? 0 : -ENOTSUP; +} + +/** + * Configure the PWM driver at the given index (inside saul_pwm_dimmer_params, + * overflowing into saul_pwm_rgb_params) unless that device came up previously, + * in which case the function returns without any action. + * */ +static int configure_on_first_use(pwm_t dev, unsigned index) +{ + /* Work around -Werror=type-limits that would otherwise trigger */ + unsigned dimmer_numof = SAUL_PWM_DIMMER_NUMOF; + for (unsigned i = 0; i < dimmer_numof; ++i) { + pwm_t currentdev = saul_pwm_dimmer_params[i].channel.dev; + if (currentdev == dev) { + if (i == index) { + return configure(dev); + } + return 0; + } + } + + /* Work around -Werror=type-limits that would otherwise trigger */ + unsigned rgb_numof = SAUL_PWM_RGB_NUMOF; + for (unsigned i = 0; i < rgb_numof; ++i) { + for (int j = 0; j < 3; ++j) { + unsigned index = SAUL_PWM_DIMMER_NUMOF + i * 3 + j; + pwm_t currentdev = saul_pwm_rgb_params[i].channels[j].dev; + if (currentdev == dev) { + if (i == index) { + return configure(dev); + } + return 0; + } + } + } + return -ENOENT; +} + +void auto_init_saul_pwm(void) +{ + /* Work around -Werror=type-limits that would otherwise trigger */ + unsigned dimmer_numof = SAUL_PWM_DIMMER_NUMOF; + for (unsigned i = 0; i < dimmer_numof; i++) { + const saul_pwm_dimmer_params_t *p = &saul_pwm_dimmer_params[i]; + + LOG_DEBUG("[auto_init_saul] initializing dimmer #%u\n", i); + + saul_reg_entries_dimmer[i].dev = (void*)p; + saul_reg_entries_dimmer[i].name = p->name; + saul_reg_entries_dimmer[i].driver = &dimmer_saul_driver; + + int err = configure_on_first_use(p->channel.dev, i); + if (err != 0) { + LOG_ERROR( + "[auto_init_saul] Error initializing device for dimmer #%u\n", + i); + /* not `continue`ing: we could run into this on a non-first use and + * then we couldn't break either */ + } + /* set initial dark state */ + phydat_t s; + s.val[0] = 0; + saul_reg_entries_dimmer[i].driver->write(p, &s); + /* add to registry */ + saul_reg_add(&(saul_reg_entries_dimmer[i])); + } + + /* Work around -Werror=type-limits that would otherwise trigger */ + unsigned rgb_numof = SAUL_PWM_RGB_NUMOF; + for (unsigned i = 0; i < rgb_numof; i++) { + const saul_pwm_rgb_params_t *p = &saul_pwm_rgb_params[i]; + + LOG_DEBUG("[auto_init_saul] initializing RGB #%u\n", i); + + saul_reg_entries_rgb[i].dev = (void*)p; + saul_reg_entries_rgb[i].name = p->name; + saul_reg_entries_rgb[i].driver = &rgb_saul_driver; + + for (int j = 0; j < 3; ++j) { + unsigned index = SAUL_PWM_DIMMER_NUMOF + i * 3 + j; + int err = configure_on_first_use(p->channels[j].dev, index); + if (err != 0) { + LOG_ERROR( + "[auto_init_saul] Error initializing device for RGB #%u/%u\n", + i, j); + /* not `continue`ing: we could run into this on a non-first use and + * then we couldn't break either */ + } + } + /* set initial dark state */ + phydat_t s; + s.val[0] = 0; + s.val[1] = 0; + s.val[2] = 0; + saul_reg_entries_rgb[i].driver->write(p, &s); + /* add to registry */ + saul_reg_add(&(saul_reg_entries_rgb[i])); + } +} diff --git a/drivers/saul/init_devs/init.c b/drivers/saul/init_devs/init.c index db50b81e85..f5aa152ca9 100644 --- a/drivers/saul/init_devs/init.c +++ b/drivers/saul/init_devs/init.c @@ -35,6 +35,10 @@ void saul_init_devs(void) extern void auto_init_gpio(void); auto_init_gpio(); } + if (IS_USED(MODULE_SAUL_PWM)) { + extern void auto_init_saul_pwm(void); + auto_init_saul_pwm(); + } if (IS_USED(MODULE_SAUL_NRF_TEMPERATURE)) { extern void auto_init_nrf_temperature(void); auto_init_nrf_temperature(); diff --git a/drivers/saul/pwm_saul.c b/drivers/saul/pwm_saul.c new file mode 100644 index 0000000000..ca2db36859 --- /dev/null +++ b/drivers/saul/pwm_saul.c @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup drivers_saul + * @{ + * + * @file + * @brief SAUL wrapper for PWM pins + * + * @author Christian Amsüss + * + * @} + */ + +#include "saul.h" +#include "phydat.h" +#include "periph/pwm.h" +#include "saul/periph.h" + +static inline void setchan(const saul_pwm_channel_t *chan, uint16_t value) +{ + pwm_set(chan->dev, + chan->channel, + (chan->flags & SAUL_PWM_INVERTED) ? saul_pwm_resolution - value : value); +} + +static int write_dimmer(const void *dev, phydat_t *state) +{ + const saul_pwm_dimmer_params_t *p = dev; + + setchan(&p->channel, state->val[0]); + return 3; +} + +const saul_driver_t dimmer_saul_driver = { + .read = saul_notsup, + .write = write_dimmer, + .type = SAUL_ACT_DIMMER +}; + +static int write_rgb(const void *dev, phydat_t *state) +{ + const saul_pwm_rgb_params_t *p = dev; + + for (int i = 0; i < 3; ++i) { + setchan(&p->channels[i], state->val[i]); + } + return 3; +} + +const saul_driver_t rgb_saul_driver = { + .read = saul_notsup, + .write = write_rgb, + .type = SAUL_ACT_LED_RGB +}; diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 41295be5f5..9decda7511 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -104,6 +104,7 @@ PSEUDOMODULES += saul_adc PSEUDOMODULES += saul_default PSEUDOMODULES += saul_gpio PSEUDOMODULES += saul_nrf_temperature +PSEUDOMODULES += saul_pwm PSEUDOMODULES += scanf_float PSEUDOMODULES += sched_cb PSEUDOMODULES += semtech_loramac_rx