[RFC 07/25] leds: Add Titan Micro Electronics TM1628
From: Andreas FÃrber
Date: Wed Dec 11 2019 - 22:40:12 EST
Add a driver for TM1628 LED controller.
Cc: zypeng@xxxxxxxxxxxx
Signed-off-by: Andreas FÃrber <afaerber@xxxxxxx>
---
drivers/leds/Kconfig | 11 ++
drivers/leds/Makefile | 1 +
drivers/leds/leds-tm1628.c | 420 +++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 432 insertions(+)
create mode 100644 drivers/leds/leds-tm1628.c
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 4b68520ac251..f3afb419a9a1 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -836,6 +836,17 @@ config LEDS_LM36274
Say Y to enable the LM36274 LED driver for TI LMU devices.
This supports the LED device LM36274.
+config LEDS_TM1628
+ tristate "LED driver for TM1628"
+ depends on LEDS_CLASS
+ depends on SPI
+ depends on OF || COMPILE_TEST
+ help
+ Say Y to enable support for Titan Micro Electronics TM1628
+ LED controllers.
+ They are 3-wire SPI devices controlling a two-dimensional grid of
+ LEDs. Dimming is applied to all outputs through an internal PWM.
+
comment "LED Triggers"
source "drivers/leds/trigger/Kconfig"
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 2da39e896ce8..4c931002ef44 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -90,6 +90,7 @@ obj-$(CONFIG_LEDS_LM36274) += leds-lm36274.o
obj-$(CONFIG_LEDS_CR0014114) += leds-cr0014114.o
obj-$(CONFIG_LEDS_DAC124S085) += leds-dac124s085.o
obj-$(CONFIG_LEDS_EL15203000) += leds-el15203000.o
+obj-$(CONFIG_LEDS_TM1628) += leds-tm1628.o
# LED Userspace Drivers
obj-$(CONFIG_LEDS_USER) += uleds.o
diff --git a/drivers/leds/leds-tm1628.c b/drivers/leds/leds-tm1628.c
new file mode 100644
index 000000000000..319bf34ce835
--- /dev/null
+++ b/drivers/leds/leds-tm1628.c
@@ -0,0 +1,420 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Titan Micro Electronics TM1628 LED controller
+ *
+ * Copyright (c) 2019 Andreas FÃrber
+ */
+
+#include <linux/bitops.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/property.h>
+#include <linux/pwm.h>
+#include <linux/spi/spi.h>
+
+#define TM1628_CMD_MASK GENMASK(7, 6)
+#define TM1628_CMD_DISPLAY_MODE (0x0 << 6)
+#define TM1628_CMD_DATA_SETTING (0x1 << 6)
+#define TM1628_CMD_DISPLAY_CTRL (0x2 << 6)
+#define TM1628_CMD_ADDRESS_SETTING (0x3 << 6)
+
+#define TM1628_DISPLAY_MODE_MODE_MASK GENMASK(1, 0)
+
+#define TM1628_DATA_SETTING_MODE_MASK GENMASK(1, 0)
+#define TM1628_DATA_SETTING_WRITE_DATA 0x0
+#define TM1628_DATA_SETTING_READ_DATA 0x2
+#define TM1628_DATA_SETTING_FIXED_ADDR BIT(2)
+#define TM1628_DATA_SETTING_TEST_MODE BIT(3)
+
+#define TM1628_DISPLAY_CTRL_PW_MASK GENMASK(2, 0)
+
+#define TM1628_DISPLAY_CTRL_DISPLAY_ON BIT(3)
+
+struct tm1628_mode {
+ u8 grid_mask;
+ u16 seg_mask;
+};
+
+struct tm1628_info {
+ u8 grid_mask;
+ u16 seg_mask;
+ const struct tm1628_mode *modes;
+ int default_mode;
+ const struct pwm_capture *pwm_map;
+ int default_pwm;
+};
+
+struct tm1628_led {
+ struct led_classdev leddev;
+ struct tm1628 *ctrl;
+ u32 grid;
+ u32 seg;
+};
+
+struct tm1628 {
+ struct spi_device *spi;
+ const struct tm1628_info *info;
+ u32 grids;
+ unsigned int segments;
+ int mode_index;
+ int pwm_index;
+ u8 data[14];
+ unsigned int num_leds;
+ struct tm1628_led leds[];
+};
+
+/* Command 1: Display Mode Setting */
+static int tm1628_set_display_mode(struct spi_device *spi, u8 grid_mode)
+{
+ u8 cmd = TM1628_CMD_DISPLAY_MODE;
+
+ if (unlikely(grid_mode & ~TM1628_DISPLAY_MODE_MODE_MASK))
+ return -EINVAL;
+
+ cmd |= grid_mode;
+
+ return spi_write(spi, &cmd, 1);
+}
+
+/* Command 2: Data Setting */
+static int tm1628_write_data(struct spi_device *spi, const u8 *data, unsigned int len)
+{
+ u8 cmd = TM1628_CMD_DATA_SETTING | TM1628_DATA_SETTING_WRITE_DATA;
+ struct spi_transfer xfers[] = {
+ {
+ .tx_buf = &cmd,
+ .len = 1,
+ },
+ {
+ .tx_buf = data,
+ .len = len,
+ },
+ };
+
+ if (len > 14)
+ return -EINVAL;
+
+ return spi_sync_transfer(spi, xfers, ARRAY_SIZE(xfers));
+}
+
+/* Command 3: Address Setting */
+static int tm1628_set_address(struct spi_device *spi, u8 addr)
+{
+ u8 cmd = TM1628_CMD_ADDRESS_SETTING;
+
+ cmd |= (addr & GENMASK(3, 0));
+
+ return spi_write(spi, &cmd, 1);
+}
+
+/* Command 4: Display Control */
+static int tm1628_set_display_ctrl(struct spi_device *spi, bool on, u8 pwm_index)
+{
+ u8 cmd = TM1628_CMD_DISPLAY_CTRL;
+
+ if (on)
+ cmd |= TM1628_DISPLAY_CTRL_DISPLAY_ON;
+
+ if (pwm_index & ~TM1628_DISPLAY_CTRL_PW_MASK)
+ return -EINVAL;
+
+ cmd |= pwm_index;
+
+ return spi_write(spi, &cmd, 1);
+}
+
+static inline bool tm1628_is_valid_grid(struct tm1628 *s, unsigned int grid)
+{
+ return s->info->modes[s->mode_index].grid_mask & BIT(grid);
+}
+
+static inline bool tm1628_is_valid_seg(struct tm1628 *s, unsigned int seg)
+{
+ return s->info->modes[s->mode_index].seg_mask & BIT(seg);
+}
+
+static int tm1628_get_led_offset(struct tm1628 *s,
+ unsigned int grid, unsigned int seg, int *poffset, int *pbit)
+{
+ int offset, bit;
+
+ if (grid == 0 || grid > 7 || seg == 0 || seg > 16)
+ return -EINVAL;
+
+ offset = (grid - 1) * 2;
+ bit = seg - 1;
+ if (bit >= 8) {
+ bit -= 8;
+ offset++;
+ }
+
+ *poffset = offset;
+ if (pbit)
+ *pbit = bit;
+
+ return 0;
+}
+
+static int tm1628_get_led(struct tm1628 *s,
+ unsigned int grid, unsigned int seg, bool *on)
+{
+ int offset, bit;
+ int ret;
+
+ ret = tm1628_get_led_offset(s, grid, seg, &offset, &bit);
+ if (ret)
+ return ret;
+
+ *on = !!(s->data[offset] & BIT(bit));
+
+ return 0;
+}
+
+static int tm1628_set_led(struct tm1628 *s,
+ unsigned int grid, unsigned int seg, bool on)
+{
+ int offset, bit;
+ int ret;
+
+ ret = tm1628_get_led_offset(s, grid, seg, &offset, &bit);
+ if (ret)
+ return ret;
+
+ if (on)
+ s->data[offset] |= BIT(bit);
+ else
+ s->data[offset] &= ~BIT(bit);
+
+ return 0;
+}
+
+static int tm1628_led_set_brightness(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct tm1628_led *led = container_of(led_cdev, struct tm1628_led, leddev);
+ struct tm1628 *s = led->ctrl;
+ int ret, offset;
+
+ ret = tm1628_set_led(s, led->grid, led->seg, brightness != LED_OFF);
+ if (ret)
+ return ret;
+
+ ret = tm1628_get_led_offset(s, led->grid, led->seg, &offset, NULL);
+ if (unlikely(ret))
+ return ret;
+
+ ret = tm1628_set_address(s->spi, offset);
+ if (ret)
+ return ret;
+
+ return tm1628_write_data(s->spi, s->data + offset, 1);
+}
+
+static enum led_brightness tm1628_led_get_brightness(struct led_classdev *led_cdev)
+{
+ struct tm1628_led *led = container_of(led_cdev, struct tm1628_led, leddev);
+ struct tm1628 *s = led->ctrl;
+ bool on;
+ int ret;
+
+ ret = tm1628_get_led(s, led->grid, led->seg, &on);
+ if (ret)
+ return ret;
+
+ return on ? LED_ON : LED_OFF;
+}
+
+static int tm1628_register_led(struct tm1628 *s,
+ struct fwnode_handle *node, u32 grid, u32 seg, struct tm1628_led *led)
+{
+ struct device *dev = &s->spi->dev;
+ struct led_init_data init_data = {0};
+
+ if (!tm1628_is_valid_grid(s, grid) || !tm1628_is_valid_seg(s, seg)) {
+ dev_warn(dev, "%s reg out of range\n", fwnode_get_name(node));
+ return -EINVAL;
+ }
+
+ led->ctrl = s;
+ led->grid = grid;
+ led->seg = seg;
+ led->leddev.max_brightness = LED_ON;
+ led->leddev.brightness_set_blocking = tm1628_led_set_brightness;
+ led->leddev.brightness_get = tm1628_led_get_brightness;
+
+ fwnode_property_read_string(node, "linux,default-trigger", &led->leddev.default_trigger);
+
+ init_data.fwnode = node;
+ init_data.devicename = "tm1628";
+
+ return devm_led_classdev_register_ext(dev, &led->leddev, &init_data);
+}
+
+/* Work around __builtin_popcount() */
+static u32 tm1628_grid_popcount(u8 grid_mask)
+{
+ int i, n = 0;
+
+ while (grid_mask) {
+ i = __ffs(grid_mask);
+ grid_mask &= ~BIT(i);
+ n++;
+ }
+
+ return n;
+}
+
+static int tm1628_spi_probe(struct spi_device *spi)
+{
+ struct tm1628 *s;
+ struct fwnode_handle *child;
+ u32 grids;
+ u32 reg[2];
+ size_t leds;
+ int ret, i;
+
+ leds = device_get_child_node_count(&spi->dev);
+
+ s = devm_kzalloc(&spi->dev, struct_size(s, leds, leds), GFP_KERNEL);
+ if (!s)
+ return -ENOMEM;
+
+ s->spi = spi;
+
+ s->info = device_get_match_data(&spi->dev);
+ if (!s->info)
+ return -EINVAL;
+
+ s->pwm_index = s->info->default_pwm;
+
+ ret = tm1628_set_display_ctrl(spi, false, s->pwm_index);
+ if (ret) {
+ dev_err(&spi->dev, "Turning display off failed (%d)\n", ret);
+ return ret;
+ }
+
+ ret = device_property_read_u32(&spi->dev, "#grids", &grids);
+ if (ret && ret != -EINVAL) {
+ dev_err(&spi->dev, "Error reading #grids property (%d)\n", ret);
+ return ret;
+ }
+
+ s->mode_index = -1;
+ for (i = 0; i < 4; i++) {
+ if (tm1628_grid_popcount(s->info->modes[i].grid_mask) != grids)
+ continue;
+ s->mode_index = i;
+ break;
+ }
+ if (s->mode_index == -1) {
+ dev_err(&spi->dev, "#grids out of range (%u)\n", grids);
+ return -EINVAL;
+ }
+
+ spi_set_drvdata(spi, s);
+
+ device_for_each_child_node(&spi->dev, child) {
+ ret = fwnode_property_read_u32_array(child, "reg", reg, 2);
+ if (ret) {
+ dev_err(&spi->dev, "Reading %s reg property failed (%d)\n",
+ fwnode_get_name(child), ret);
+ fwnode_handle_put(child);
+ return ret;
+ }
+
+ if (fwnode_property_count_u32(child, "reg") == 2) {
+ ret = tm1628_register_led(s, child, reg[0], reg[1], &s->leds[i++]);
+ if (ret && ret != -EINVAL) {
+ dev_err(&spi->dev, "Failed to register LED %s (%d)\n",
+ fwnode_get_name(child), ret);
+ fwnode_handle_put(child);
+ return ret;
+ }
+ s->num_leds++;
+ }
+ }
+
+ ret = tm1628_set_address(spi, 0x0);
+ if (ret) {
+ dev_err(&spi->dev, "Setting address failed (%d)\n", ret);
+ return ret;
+ }
+
+ ret = tm1628_write_data(spi, s->data, sizeof(s->data));
+ if (ret) {
+ dev_err(&spi->dev, "Writing data failed (%d)\n", ret);
+ return ret;
+ }
+
+ ret = tm1628_set_display_mode(spi, s->mode_index);
+ if (ret) {
+ dev_err(&spi->dev, "Setting display mode failed (%d)\n", ret);
+ return ret;
+ }
+
+ ret = tm1628_set_display_ctrl(spi, true, s->pwm_index);
+ if (ret) {
+ dev_err(&spi->dev, "Turning display on failed (%d)\n", ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+static const struct pwm_capture tm1628_pwm_map[8] = {
+ { .duty_cycle = 1, .period = 16 },
+ { .duty_cycle = 2, .period = 16 },
+ { .duty_cycle = 4, .period = 16 },
+ { .duty_cycle = 10, .period = 16 },
+ { .duty_cycle = 11, .period = 16 },
+ { .duty_cycle = 12, .period = 16 },
+ { .duty_cycle = 13, .period = 16 },
+ { .duty_cycle = 14, .period = 16 },
+};
+
+static const struct tm1628_mode tm1628_modes[4] = {
+ {
+ .grid_mask = GENMASK(4, 1),
+ .seg_mask = GENMASK(14, 12) | GENMASK(10, 1),
+ },
+ {
+ .grid_mask = GENMASK(5, 1),
+ .seg_mask = GENMASK(13, 12) | GENMASK(10, 1),
+ },
+ {
+ .grid_mask = GENMASK(6, 1),
+ .seg_mask = BIT(12) | GENMASK(10, 1),
+ },
+ {
+ .grid_mask = GENMASK(7, 1),
+ .seg_mask = GENMASK(10, 1),
+ },
+};
+
+static const struct tm1628_info tm1628_info = {
+ .grid_mask = GENMASK(7, 1),
+ .seg_mask = GENMASK(14, 12) | GENMASK(10, 1),
+ .modes = tm1628_modes,
+ .default_mode = 3,
+ .pwm_map = tm1628_pwm_map,
+ .default_pwm = 0,
+};
+
+static const struct of_device_id tm1628_spi_of_matches[] = {
+ { .compatible = "titanmec,tm1628", .data = &tm1628_info },
+ {}
+};
+MODULE_DEVICE_TABLE(of, tm1628_spi_of_matches);
+
+static struct spi_driver tm1628_spi_driver = {
+ .probe = tm1628_spi_probe,
+ .driver = {
+ .name = "tm1628",
+ .of_match_table = tm1628_spi_of_matches,
+ },
+};
+module_spi_driver(tm1628_spi_driver);
+
+MODULE_DESCRIPTION("TM1628 LED controller driver");
+MODULE_AUTHOR("Andreas FÃrber");
+MODULE_LICENSE("GPL");
--
2.16.4