| /* |
| * Copyright 2022-2023 Daniel DeGrasse <daniel@degrasse.com> |
| * |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| #define DT_DRV_COMPAT issi_is31fl3733 |
| |
| #include <zephyr/drivers/gpio.h> |
| #include <zephyr/drivers/i2c.h> |
| #include <zephyr/drivers/led.h> |
| #include <zephyr/kernel.h> |
| |
| #include <zephyr/drivers/led/is31fl3733.h> |
| |
| #include <zephyr/logging/log.h> |
| LOG_MODULE_REGISTER(is31fl3733, CONFIG_LED_LOG_LEVEL); |
| |
| /* IS31FL3733 register definitions */ |
| #define CMD_SEL_REG 0xFD /* Command/page selection reg */ |
| #define CMD_SEL_LED 0x0 /* LED configuration page */ |
| #define CMD_SEL_PWM 0x1 /* PWM configuration page */ |
| #define CMD_SEL_FUNC 0x3 /* Function configuration page */ |
| |
| #define CMD_LOCK_REG 0xFE /* Command selection lock reg */ |
| #define CMD_LOCK_UNLOCK 0xC5 /* Command sel unlock value */ |
| |
| /* IS31FL3733 page specific register definitions */ |
| |
| /* Function configuration page */ |
| #define CONF_REG 0x0 /* configuration register */ |
| #define CONF_REG_SSD_MASK 0x1 /* Software shutdown mask */ |
| #define CONF_REG_SSD_SHIFT 0x0 /* Software shutdown shift */ |
| #define CONF_REG_SYNC_SHIFT 0x6 /* Sync mode shift */ |
| #define CONF_REG_SYNC_MASK 0xC /* Sync mode mask */ |
| |
| #define GLOBAL_CURRENT_CTRL_REG 0x1 /* global current control register */ |
| |
| #define RESET_REG 0x11 /* Reset all registers to POR state */ |
| |
| /* Matrix Layout definitions */ |
| #define IS31FL3733_ROW_COUNT 12 |
| #define IS31FL3733_COL_COUNT 16 |
| #define IS31FL3733_MAX_LED (IS31FL3733_ROW_COUNT * IS31FL3733_COL_COUNT) |
| |
| /* Max brightness */ |
| #define IS31FL3733_MAX_BRIGHTNESS 100 |
| |
| struct is31fl3733_config { |
| struct i2c_dt_spec bus; |
| struct gpio_dt_spec sdb; |
| uint8_t current_limit; |
| uint8_t sync; |
| }; |
| |
| struct is31fl3733_data { |
| /* Active configuration page */ |
| uint32_t selected_page; |
| /* Scratch buffer, used for bulk controller writes */ |
| uint8_t scratch_buf[IS31FL3733_MAX_LED + 1]; |
| /* LED config reg state, IS31FL3733 conf reg is write only */ |
| uint8_t conf_reg; |
| }; |
| |
| /* Selects target register page for IS31FL3733. After setting the |
| * target page, all I2C writes will use the selected page until the selected |
| * page is changed. |
| */ |
| static int is31fl3733_select_page(const struct device *dev, uint8_t page) |
| { |
| const struct is31fl3733_config *config = dev->config; |
| struct is31fl3733_data *data = dev->data; |
| int ret = 0U; |
| |
| if (data->selected_page == page) { |
| /* No change necessary */ |
| return 0; |
| } |
| |
| /* Unlock page selection register */ |
| ret = i2c_reg_write_byte_dt(&config->bus, CMD_LOCK_REG, CMD_LOCK_UNLOCK); |
| if (ret < 0) { |
| LOG_ERR("Could not unlock page selection register"); |
| return ret; |
| } |
| |
| /* Write to function select to select active page */ |
| ret = i2c_reg_write_byte_dt(&config->bus, CMD_SEL_REG, page); |
| if (ret < 0) { |
| LOG_ERR("Could not select active page"); |
| return ret; |
| } |
| data->selected_page = page; |
| |
| return ret; |
| } |
| |
| static int is31fl3733_led_set_brightness(const struct device *dev, uint32_t led, uint8_t value) |
| { |
| const struct is31fl3733_config *config = dev->config; |
| int ret; |
| uint8_t led_brightness = (uint8_t)(((uint32_t)value * 255) / 100); |
| |
| if (led >= IS31FL3733_MAX_LED) { |
| return -EINVAL; |
| } |
| |
| /* Configure PWM mode */ |
| ret = is31fl3733_select_page(dev, CMD_SEL_PWM); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| return i2c_reg_write_byte_dt(&config->bus, led, led_brightness); |
| } |
| |
| static int is31fl3733_led_on(const struct device *dev, uint32_t led) |
| { |
| return is31fl3733_led_set_brightness(dev, led, IS31FL3733_MAX_BRIGHTNESS); |
| } |
| |
| static int is31fl3733_led_off(const struct device *dev, uint32_t led) |
| { |
| return is31fl3733_led_set_brightness(dev, led, 0); |
| } |
| |
| static int is31fl3733_led_write_channels(const struct device *dev, uint32_t start_channel, |
| uint32_t num_channels, const uint8_t *buf) |
| { |
| const struct is31fl3733_config *config = dev->config; |
| struct is31fl3733_data *data = dev->data; |
| int ret = 0U; |
| uint8_t *pwm_start; |
| |
| if ((start_channel + num_channels) > IS31FL3733_MAX_LED) { |
| return -EINVAL; |
| } |
| pwm_start = data->scratch_buf + start_channel; |
| /* Set PWM and LED target registers as first byte of each transfer */ |
| *pwm_start = start_channel; |
| memcpy((pwm_start + 1), buf, num_channels); |
| |
| /* Write LED PWM states */ |
| ret = is31fl3733_select_page(dev, CMD_SEL_PWM); |
| if (ret < 0) { |
| return ret; |
| } |
| LOG_HEXDUMP_DBG(pwm_start, (num_channels + 1), "PWM states"); |
| |
| return i2c_write_dt(&config->bus, pwm_start, num_channels + 1); |
| } |
| |
| static int is31fl3733_init(const struct device *dev) |
| { |
| const struct is31fl3733_config *config = dev->config; |
| struct is31fl3733_data *data = dev->data; |
| int ret = 0U; |
| uint8_t dummy; |
| |
| if (!i2c_is_ready_dt(&config->bus)) { |
| LOG_ERR("I2C device not ready"); |
| return -ENODEV; |
| } |
| if (config->sdb.port != NULL) { |
| if (!gpio_is_ready_dt(&config->sdb)) { |
| LOG_ERR("GPIO SDB pin not ready"); |
| return -ENODEV; |
| } |
| /* Set SDB pin high to exit hardware shutdown */ |
| ret = gpio_pin_configure_dt(&config->sdb, GPIO_OUTPUT_ACTIVE); |
| if (ret < 0) { |
| return ret; |
| } |
| } |
| |
| ret = is31fl3733_select_page(dev, CMD_SEL_FUNC); |
| if (ret < 0) { |
| return ret; |
| } |
| /* |
| * read reset reg to reset all registers to POR state, |
| * in case we are booting from a warm reset. |
| */ |
| ret = i2c_reg_read_byte_dt(&config->bus, RESET_REG, &dummy); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Select function page after LED controller reset */ |
| ret = is31fl3733_select_page(dev, CMD_SEL_FUNC); |
| if (ret < 0) { |
| return ret; |
| } |
| /* Set global current control register based off devicetree value */ |
| ret = i2c_reg_write_byte_dt(&config->bus, GLOBAL_CURRENT_CTRL_REG, |
| config->current_limit); |
| if (ret < 0) { |
| return ret; |
| } |
| /* As a final step, we exit software shutdown, disabling display |
| * blanking. We also set the LED controller sync mode here. |
| */ |
| data->conf_reg = (config->sync << CONF_REG_SYNC_SHIFT) | CONF_REG_SSD_MASK; |
| ret = i2c_reg_write_byte_dt(&config->bus, CONF_REG, data->conf_reg); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Enable all LEDs. We only control LED brightness in this driver. */ |
| data->scratch_buf[0] = 0x0; |
| memset(data->scratch_buf + 1, 0xFF, (IS31FL3733_MAX_LED / 8)); |
| ret = is31fl3733_select_page(dev, CMD_SEL_LED); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| return i2c_write_dt(&config->bus, data->scratch_buf, |
| (IS31FL3733_MAX_LED / 8) + 1); |
| } |
| |
| /* Custom IS31FL3733 specific APIs */ |
| |
| /** |
| * @brief Blanks IS31FL3733 LED display. |
| * |
| * When blank_en is set, the LED display will be disabled. This can be used for |
| * flicker-free display updates, or power saving. |
| * |
| * @param dev: LED device structure |
| * @param blank_en: should blanking be enabled |
| * @return 0 on success, or negative value on error. |
| */ |
| int is31fl3733_blank(const struct device *dev, bool blank_en) |
| { |
| const struct is31fl3733_config *config = dev->config; |
| struct is31fl3733_data *data = dev->data; |
| int ret; |
| |
| ret = is31fl3733_select_page(dev, CMD_SEL_FUNC); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| if (blank_en) { |
| data->conf_reg &= ~CONF_REG_SSD_MASK; |
| } else { |
| data->conf_reg |= CONF_REG_SSD_MASK; |
| } |
| |
| return i2c_reg_write_byte_dt(&config->bus, CONF_REG, data->conf_reg); |
| } |
| |
| /** |
| * @brief Sets led current limit |
| * |
| * Sets the current limit for the LED driver. This is a separate value |
| * from per-led brightness, and applies to all LEDs. |
| * This value sets the output current limit according |
| * to the following formula: (840/R_ISET) * (limit/256) |
| * See table 14 of the datasheet for additional details. |
| * @param dev: LED device structure |
| * @param limit: current limit to apply |
| * @return 0 on success, or negative value on error. |
| */ |
| int is31fl3733_current_limit(const struct device *dev, uint8_t limit) |
| { |
| const struct is31fl3733_config *config = dev->config; |
| int ret; |
| |
| ret = is31fl3733_select_page(dev, CMD_SEL_FUNC); |
| if (ret < 0) { |
| return ret; |
| } |
| |
| /* Set global current control register */ |
| return i2c_reg_write_byte_dt(&config->bus, GLOBAL_CURRENT_CTRL_REG, limit); |
| } |
| |
| static const struct led_driver_api is31fl3733_api = { |
| .on = is31fl3733_led_on, |
| .off = is31fl3733_led_off, |
| .set_brightness = is31fl3733_led_set_brightness, |
| .write_channels = is31fl3733_led_write_channels, |
| }; |
| |
| #define IS31FL3733_DEVICE(n) \ |
| static const struct is31fl3733_config is31fl3733_config_##n = { \ |
| .bus = I2C_DT_SPEC_INST_GET(n), \ |
| .sdb = GPIO_DT_SPEC_INST_GET_OR(n, sdb_gpios, {}), \ |
| .current_limit = DT_INST_PROP(n, current_limit), \ |
| .sync = DT_INST_ENUM_IDX(n, sync_mode), \ |
| }; \ |
| \ |
| static struct is31fl3733_data is31fl3733_data_##n = { \ |
| .selected_page = CMD_SEL_LED, \ |
| }; \ |
| \ |
| DEVICE_DT_INST_DEFINE(n, &is31fl3733_init, NULL, &is31fl3733_data_##n, \ |
| &is31fl3733_config_##n, POST_KERNEL, CONFIG_LED_INIT_PRIORITY, \ |
| &is31fl3733_api); |
| |
| DT_INST_FOREACH_STATUS_OKAY(IS31FL3733_DEVICE) |