Add driver for TM16xx family LED controllers and compatible chips from
multiple vendors including Titan Micro, Fuda Hisi, i-Core, Princeton, and
Winrise. These controllers drive 7-segment digits and individual LED icons
through either I2C or SPI buses.
Successfully tested on various ARM TV boxes including H96 Max, Magicsee N5,
Tanix TX3 Mini, Tanix TX6, X92, and X96 Max across different SoC platforms
(Rockchip, Amlogic, Allwinner).
Acked-by: Paolo Sabatino <paolo.sabatino@gmail.com> # As primary user, integrated tm16xx into Armbian rockchip64
Acked-by: Christian Hewitt <christianshewitt@gmail.com> # As primary user, integrated tm16xx into LibreElec
Tested-by: Boris Gjenero <boris.gjenero@gmail.com> # Tested on X92
Tested-by: Paolo Sabatino <paolo.sabatino@gmail.com> # Tested on H96 Max (XY_RK3328)
Tested-by: Christian Hewitt <christianshewitt@gmail.com> # Tested on X96 Max, Tanix TX3 Mini
Tested-by: Martin Blumenstingl <martin.blumenstingl@googlemail.com> # Tested on Tanix TX3 Mini
Signed-off-by: Jean-François Lessard <jefflessard3@gmail.com>
---
Notes:
checkpatch reports false positives that are intentionally ignored:
DEVICE_ATTR_FUNCTIONS: Functions are correctly prefixed with driver
name (tm16xx_*) following standard kernel practice for device
attribute functions to avoid namespace conflicts.
BIT_MACRO: bit shifts are used for field values while GENMASK/BIT
are used for bit positions per semantic convention
include <linux/of.h> is required for the default name of the main led
device, excluding the unit address, as implemented in
drivers/leds/led-core.c which relies on of_node->name
LED registration uses non-devm variant on-purpose to allow explicit
unregistration on device removal, ensuring LED triggers are
immediately stopped. This prevents stale LED trigger activity from
continuing after the hardware is gone, avoiding the need for complex
state tracking in brightness callbacks.
.../ABI/testing/sysfs-class-leds-tm16xx | 57 ++
MAINTAINERS | 3 +
drivers/auxdisplay/Kconfig | 20 +-
drivers/auxdisplay/Makefile | 2 +
drivers/auxdisplay/tm16xx.h | 177 +++++
drivers/auxdisplay/tm16xx_core.c | 618 ++++++++++++++++++
6 files changed, 876 insertions(+), 1 deletion(-)
create mode 100644 Documentation/ABI/testing/sysfs-class-leds-tm16xx
create mode 100644 drivers/auxdisplay/tm16xx.h
create mode 100644 drivers/auxdisplay/tm16xx_core.c
diff --git a/Documentation/ABI/testing/sysfs-class-leds-tm16xx b/Documentation/ABI/testing/sysfs-class-leds-tm16xx
new file mode 100644
index 000000000..c01cca251
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-leds-tm16xx
@@ -0,0 +1,57 @@
+What: /sys/class/leds/<led>/value
+Date: August 2025
+KernelVersion: 6.17
+Contact: Jean-François Lessard <jefflessard3@gmail.com>
+Description:
+ Controls the text displayed on the TM16xx 7-segment display.
+
+ Reading returns the current display content as ASCII characters,
+ one character per digit position, followed by a newline.
+
+ Writing sets new display content. Input characters are mapped
+ to 7-segment patterns using the configured character map. The
+ string length should not exceed the number of available digits
+ (see num_digits). Shorter strings will clear remaining digits.
+
+ Example:
+ echo "1234" > value # Display "1234"
+ cat value # Returns "1234\n"
+
+What: /sys/class/leds/<led>/num_digits
+Date: August 2025
+KernelVersion: 6.17
+Contact: Jean-François Lessard <jefflessard3@gmail.com>
+Description:
+ Read-only attribute showing the number of 7-segment digit
+ positions available on this TM16xx display controller.
+
+ The value is determined by the device tree configuration
+ and represents the maximum length for the 'value' attribute.
+
+ Example:
+ cat num_digits # Returns "4\n" for 4-digit display
+
+What: /sys/class/leds/<led>/map_seg7
+Date: August 2025
+KernelVersion: 6.17
+Contact: Jean-François Lessard <jefflessard3@gmail.com>
+Description:
+ Read/write binary blob representing the ASCII-to-7-segment
+ display conversion table used by the TM16xx driver, as defined
+ by struct seg7_conversion_map in <linux/map_to_7segment.h>.
+
+ This attribute is not human-readable. Writes must match the
+ struct size exactly, else -EINVAL is returned; reads return the
+ entire mapping as a binary blob.
+
+ This interface and its implementation match existing conventions
+ used in auxdisplay and segment-mapped display drivers since 2005.
+
+ ABI note: This style of binary sysfs attribute *is an exception*
+ to current "one value per file, text only" sysfs rules, for
+ historical compatibility and driver uniformity. New drivers are
+ discouraged from introducing additional binary sysfs ABIs.
+
+ Reference interface guidance:
+ - include/uapi/linux/map_to_7segment.h
+Users: Display configuration utilities and embedded system scripts/tools.
diff --git a/MAINTAINERS b/MAINTAINERS
index 4e5a7db6d..0ed971881 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25405,7 +25405,10 @@ F: drivers/net/ethernet/ti/tlan.*
TM16XX-COMPATIBLE LED CONTROLLERS DISPLAY DRIVER
M: Jean-François Lessard <jefflessard3@gmail.com>
S: Maintained
+F: Documentation/ABI/testing/sysfs-class-leds-tm16xx
F: Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
+F: drivers/auxdisplay/tm16xx.h
+F: drivers/auxdisplay/tm16xx_core.c
TMIO/SDHI MMC DRIVER
M: Wolfram Sang <wsa+renesas@sang-engineering.com>
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
index bedc6133f..7b58c6cc8 100644
--- a/drivers/auxdisplay/Kconfig
+++ b/drivers/auxdisplay/Kconfig
@@ -316,7 +316,7 @@ endif # PARPORT_PANEL
config PANEL_CHANGE_MESSAGE
bool "Change LCD initialization message ?"
- depends on CHARLCD || LINEDISP
+ depends on CHARLCD || LINEDISP || TM16XX
help
This allows you to replace the boot message indicating the kernel version
and the driver version with a custom message. This is useful on appliances
@@ -526,6 +526,24 @@ config SEG_LED_GPIO
This driver can also be built as a module. If so, the module
will be called seg-led-gpio.
+config TM16XX
+ tristate "TM16XX-compatible 7-segment LED controllers"
+ depends on SPI || I2C
+ select NEW_LEDS
+ select LEDS_CLASS
+ select LEDS_TRIGGERS
+ help
+ This driver supports the following TM16XX compatible
+ I2C and SPI 7-segment led display chips:
+ - Titanmec: TM1618, TM1620, TM1628, TM1638, TM1650
+ - Fuda Hisi: FD620, FD628, FD650, FD655, FD6551
+ - i-Core: AiP650, AiP1618, AiP1628
+ - Princeton: PT6964
+ - Winrise: HBS658
+
+ This driver can also be built as a module. If so, the module
+ will be called tm16xx.
+
#
# Character LCD with non-conforming interface section
#
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
index f5c13ed1c..7ecf3cd4a 100644
--- a/drivers/auxdisplay/Makefile
+++ b/drivers/auxdisplay/Makefile
@@ -16,3 +16,5 @@ obj-$(CONFIG_LINEDISP) += line-display.o
obj-$(CONFIG_MAX6959) += max6959.o
obj-$(CONFIG_PARPORT_PANEL) += panel.o
obj-$(CONFIG_SEG_LED_GPIO) += seg-led-gpio.o
+obj-$(CONFIG_TM16XX) += tm16xx.o
+tm16xx-y += tm16xx_core.o
diff --git a/drivers/auxdisplay/tm16xx.h b/drivers/auxdisplay/tm16xx.h
new file mode 100644
index 000000000..a7ce483d3
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx.h
@@ -0,0 +1,177 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * TM16xx and compatible LED display/keypad controller driver
+ * Supports TM16xx, FD6xx, PT6964, HBS658, AIP16xx and related chips.
+ *
+ * Copyright (C) 2024 Jean-François Lessard
+ */
+
+#ifndef _TM16XX_H
+#define _TM16XX_H
+
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/leds.h>
+#include <linux/workqueue.h>
+
+#define TM16XX_DIGIT_SEGMENTS 7
+
+/* Common bit field definitions */
+
+/* Command type bits (bits 7-6) */
+#define TM16XX_CMD_MASK GENMASK(7, 6)
+#define TM16XX_CMD_MODE (0 << 6)
+#define TM16XX_CMD_DATA (1 << 6)
+#define TM16XX_CMD_CTRL (2 << 6)
+#define TM16XX_CMD_ADDR (3 << 6)
+#define TM16XX_CMD_WRITE (TM16XX_CMD_DATA | TM16XX_DATA_MODE_WRITE)
+#define TM16XX_CMD_READ (TM16XX_CMD_DATA | TM16XX_DATA_MODE_READ)
+
+/* Mode command grid settings (bits 1-0) */
+#define TM16XX_MODE_GRID_MASK GENMASK(1, 0)
+#define TM16XX_MODE_4GRIDS (0 << 0)
+#define TM16XX_MODE_5GRIDS (1 << 0)
+#define TM16XX_MODE_6GRIDS (2 << 0)
+#define TM16XX_MODE_7GRIDS (3 << 0)
+
+/* Data command settings */
+#define TM16XX_DATA_ADDR_MASK BIT(2)
+#define TM16XX_DATA_ADDR_AUTO (0 << 2)
+#define TM16XX_DATA_ADDR_FIXED (1 << 2)
+#define TM16XX_DATA_MODE_MASK GENMASK(1, 0)
+#define TM16XX_DATA_MODE_WRITE (0 << 0)
+#define TM16XX_DATA_MODE_READ (2 << 0)
+
+/* Control command settings */
+#define TM16XX_CTRL_BR_MASK GENMASK(2, 0)
+#define TM16XX_CTRL_ON (1 << 3)
+
+/* TM1618 specific constants */
+#define TM1618_BYTE1_MASK GENMASK(4, 0)
+#define TM1618_BYTE2_MASK GENMASK(7, 5)
+#define TM1618_BYTE2_SHIFT 3
+#define TM1618_KEY_READ_LEN 3
+#define TM1618_KEY_MASK (BIT(4) | BIT(1))
+
+/* TM1628 specific constants */
+#define TM1628_BYTE1_MASK GENMASK(7, 0)
+#define TM1628_BYTE2_MASK GENMASK(13, 8)
+#define TM1628_KEY_READ_LEN 5
+#define TM1628_KEY_MASK (GENMASK(4, 3) | GENMASK(1, 0))
+
+/* TM1638 specific constants */
+#define TM1638_KEY_READ_LEN 4
+#define TM1638_KEY_MASK (GENMASK(6, 4) | GENMASK(2, 0))
+
+/* FD620 specific constants */
+#define FD620_BYTE1_MASK GENMASK(6, 0)
+
+#define FD620_BYTE2_MASK BIT(7)
+#define FD620_BYTE2_SHIFT 5
+#define FD620_KEY_READ_LEN 4
+#define FD620_KEY_MASK (BIT(3) | BIT(0))
+
+/* I2C controller addresses and control settings */
+#define TM1650_CMD_CTRL 0x48
+#define TM1650_CMD_READ 0x4F
+#define TM1650_CMD_ADDR 0x68
+#define TM1650_CTRL_BR_MASK GENMASK(6, 4)
+#define TM1650_CTRL_ON (1 << 0)
+#define TM1650_CTRL_SLEEP (1 << 2)
+#define TM1650_CTRL_SEG_MASK BIT(3)
+#define TM1650_CTRL_SEG8_MODE (0 << 3)
+#define TM1650_CTRL_SEG7_MODE (1 << 3)
+#define TM1650_KEY_ROW_MASK GENMASK(1, 0)
+#define TM1650_KEY_COL_MASK GENMASK(5, 3)
+#define TM1650_KEY_DOWN_MASK BIT(6)
+#define TM1650_KEY_COMBINED GENMASK(5, 3)
+
+#define FD655_CMD_CTRL 0x48
+#define FD655_CMD_ADDR 0x66
+#define FD655_CTRL_BR_MASK GENMASK(6, 5)
+#define FD655_CTRL_ON (1 << 0)
+
+#define FD6551_CMD_CTRL 0x48
+#define FD6551_CTRL_BR_MASK GENMASK(3, 1)
+#define FD6551_CTRL_ON (1 << 0)
+
+#define HBS658_KEY_COL_MASK GENMASK(7, 5)
+
+#define TM16XX_CTRL_BRIGHTNESS(on, val, prfx) \
+ ((on) ? (FIELD_PREP(prfx##_CTRL_BR_MASK, (val)) | prfx##_CTRL_ON) : 0)
+
+/* Forward declarations */
+struct tm16xx_display;
+struct tm16xx_digit;
+struct tm16xx_led;
+
+/**
+ * DOC: struct tm16xx_controller - Controller-specific operations and limits
+ * @max_grids: Maximum number of grids supported by the controller.
+ * @max_segments: Maximum number of segments supported by the controller.
+ * @max_brightness: Maximum brightness level supported by the controller.
+ * @max_key_rows: Maximum number of key input rows supported by the controller.
+ * @max_key_cols: Maximum number of key input columns supported by the controller.
+ * @init: Pointer to controller mode/brightness configuration function.
+ * @data: Pointer to function writing display data to the controller.
+ * @keys: Pointer to function reading controller key state into bitmap.
+ *
+ * Holds function pointers and limits for controller-specific operations.
+ */
+struct tm16xx_controller {
+ const u8 max_grids;
+ const u8 max_segments;
+ const u8 max_brightness;
+ const u8 max_key_rows;
+ const u8 max_key_cols;
+ int (*const init)(struct tm16xx_display *display);
+ int (*const data)(struct tm16xx_display *display, u8 index,
+ unsigned int grid);
+ int (*const keys)(struct tm16xx_display *display);
+};
+
+/**
+ * struct tm16xx_display - Main driver structure for the display
+ * @dev: Pointer to device struct.
+ * @controller: Controller-specific function table and limits.
+ * @client: Union of I2C and SPI client pointers.
+ * @spi_buffer: DMA-safe buffer for SPI transactions, or NULL for I2C.
+ * @num_grids: Number of controller grids in use.
+ * @num_segments: Number of controller segments in use.
+ * @main_led: LED class device for the entire display.
+ * @leds: Array of individual LED icon structures.
+ * @num_leds: Number of individual LED icons.
+ * @digits: Array of 7-segment digit structures.
+ * @num_digits: Number of 7-segment digits.
+ * @flush_init: Work struct for configuration update.
+ * @flush_display: Work struct for display update.
+ * @flush_status: Status/result of last flush work.
+ * @lock: Mutex protecting concurrent access to work operations.
+ * @state: Bitmap holding current raw display state.
+ */
+struct tm16xx_display {
+ struct device *dev;
+ const struct tm16xx_controller *controller;
+ union {
+ struct i2c_client *i2c;
+ struct spi_device *spi;
+ } client;
+ u8 *spi_buffer;
+ u8 num_grids;
+ u8 num_segments;
+ struct led_classdev main_led;
+ struct tm16xx_led *leds;
+ u8 num_leds;
+ struct tm16xx_digit *digits;
+ u8 num_digits;
+ struct work_struct flush_init;
+ struct work_struct flush_display;
+ int flush_status;
+ struct mutex lock; /* prevents concurrent work operations */
+ unsigned long *state;
+};
+
+int tm16xx_probe(struct tm16xx_display *display);
+void tm16xx_remove(struct tm16xx_display *display);
+
+#endif /* _TM16XX_H */
diff --git a/drivers/auxdisplay/tm16xx_core.c b/drivers/auxdisplay/tm16xx_core.c
new file mode 100644
index 000000000..415be7747
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx_core.c
@@ -0,0 +1,618 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TM16xx and compatible LED display/keypad controller driver
+ * Supports TM16xx, FD6xx, PT6964, HBS658, AIP16xx and related chips.
+ *
+ * Copyright (C) 2024 Jean-François Lessard
+ */
+
+#include <linux/map_to_7segment.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/property.h>
+
+#include "tm16xx.h"
+
+static const char *tm16xx_init_value;
+#ifdef CONFIG_PANEL_BOOT_MESSAGE
+tm16xx_init_value = CONFIG_PANEL_BOOT_MESSAGE;
+#endif
+
+static SEG7_CONVERSION_MAP(map_seg7, MAP_ASCII7SEG_ALPHANUM);
+
+/**
+ * struct tm16xx_led - Individual LED icon mapping
+ * @cdev: LED class device for sysfs interface.
+ * @grid: Controller grid index of the LED.
+ * @segment: Controller segment index of the LED.
+ */
+struct tm16xx_led {
+ struct led_classdev cdev;
+ u8 grid;
+ u8 segment;
+};
+
+/**
+ * struct tm16xx_digit_segment - Digit segment mapping to display coordinates
+ * @grid: Controller grid index for this segment.
+ * @segment: Controller segment index for this segment.
+ */
+struct tm16xx_digit_segment {
+ u8 grid;
+ u8 segment;
+};
+
+/**
+ * struct tm16xx_digit - 7-segment digit mapping and value
+ * @segments: Array mapping each 7-segment position to a grid/segment on the controller.
+ * @value: Current character value displayed on this digit.
+ */
+struct tm16xx_digit {
+ struct tm16xx_digit_segment segments[TM16XX_DIGIT_SEGMENTS];
+ char value;
+};
+
+/* state bitmap helpers */
+/**
+ * tm16xx_led_nbits() - Number of bits used for the display state bitmap
+ * @display: pointer to tm16xx_display
+ *
+ * Return: total bits in the display state bitmap (grids * segments)
+ */
+static inline unsigned int tm16xx_led_nbits(const struct tm16xx_display *display)
+{
+ return display->num_grids * display->num_segments;
+}
+
+/**
+ * tm16xx_set_seg() - Set the display state for a specific grid/segment
+ * @display: pointer to tm16xx_display
+ * @grid: grid index
+ * @seg: segment index
+ * @on: true to turn on, false to turn off
+ */
+static inline void tm16xx_set_seg(const struct tm16xx_display *display,
+ const u8 grid, const u8 seg, const bool on)
+{
+ assign_bit(grid * display->num_segments + seg, display->state, on);
+}
+
+/**
+ * tm16xx_get_grid() - Get the current segment pattern for a grid
+ * @display: pointer to tm16xx_display
+ * @index: grid index
+ *
+ * Return: bit pattern of all segments for the given grid
+ */
+static inline unsigned int tm16xx_get_grid(const struct tm16xx_display *display,
+ const unsigned int index)
+{
+ return bitmap_read(display->state, index * display->num_segments,
+ display->num_segments);
+}
+
+/* main display */
+/**
+ * tm16xx_display_flush_init() - Workqueue to configure controller and set brightness
+ * @work: pointer to work_struct
+ */
+static void tm16xx_display_flush_init(struct work_struct *work)
+{
+ struct tm16xx_display *display = container_of(work,
+ struct tm16xx_display,
+ flush_init);
+ int ret;
+
+ if (display->controller->init) {
+ dev_dbg(display->dev, "Configuring controller\n");
+ scoped_guard(mutex, &display->lock) {
+ ret = display->controller->init(display);
+ display->flush_status = ret;
+ }
+ if (ret < 0)
+ dev_err(display->dev,
+ "Failed to configure controller: %d\n", ret);
+ }
+}
+
+/**
+ * tm16xx_display_flush_data() - Workqueue to update display data to controller
+ * @work: pointer to work_struct
+ */
+static void tm16xx_display_flush_data(struct work_struct *work)
+{
+ struct tm16xx_display *display = container_of(work,
+ struct tm16xx_display,
+ flush_display);
+ int i, ret = 0;
+ unsigned int grid;
+
+ dev_dbg(display->dev, "Sending data to controller\n");
+ scoped_guard(mutex, &display->lock) {
+ if (display->controller->data) {
+ for (i = 0; i < display->num_grids; i++) {
+ grid = tm16xx_get_grid(display, i);
+ ret = display->controller->data(display, i,
+ grid);
+ if (ret < 0) {
+ dev_err(display->dev,
+ "Failed to write display data: %d\n",
+ ret);
+ break;
+ }
+ }
+ }
+
+ display->flush_status = ret;
+ }
+}
+
+/**
+ * tm16xx_brightness_set() - Set display main LED brightness
+ * @led_cdev: pointer to led_classdev
+ * @brightness: new brightness value
+ */
+static void tm16xx_brightness_set(struct led_classdev *led_cdev,
+ enum led_brightness brightness)
+{
+ struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent);
+
+ dev_dbg(display->dev, "Setting brightness to %d\n", brightness);
+ led_cdev->brightness = brightness;
+ schedule_work(&display->flush_init);
+}
+
+/**
+ * tm16xx_led_set() - Set state of an individual LED icon
+ * @led_cdev: pointer to led_classdev
+ * @value: new brightness (0/1)
+ */
+static void tm16xx_led_set(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct tm16xx_led *led = container_of(led_cdev, struct tm16xx_led, cdev);
+ struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent);
+
+ dev_dbg(display->dev, "Setting led %u,%u to %d\n", led->grid,
+ led->segment, value);
+
+ tm16xx_set_seg(display, led->grid, led->segment, value);
+ schedule_work(&display->flush_display);
+}
+
+/**
+ * tm16xx_value_show() - Sysfs: show current display digit values
+ * @dev: pointer to device
+ * @attr: device attribute (unused)
+ * @buf: output buffer
+ *
+ * Return: number of bytes written to output buffer
+ */
+static ssize_t tm16xx_value_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent);
+ int i;
+
+ for (i = 0; i < display->num_digits && i < PAGE_SIZE - 1; i++)
+ buf[i] = display->digits[i].value;
+
+ buf[i++] = '\n';
+ return i;
+}
+
+/**
+ * tm16xx_value_store() - Sysfs: set display digit values
+ * @dev: pointer to device
+ * @attr: device attribute (unused)
+ * @buf: new digit values (ASCII chars)
+ * @count: buffer length
+ *
+ * Return: number of bytes written or negative error code
+ */
+static ssize_t tm16xx_value_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent);
+ struct tm16xx_digit *digit;
+ struct tm16xx_digit_segment *ds;
+ int i, j;
+ int seg_pattern;
+ bool val;
+
+ dev_dbg(display->dev, "Setting value to %s\n", buf);
+
+ for (i = 0; i < display->num_digits && i < count; i++) {
+ digit = &display->digits[i];
+ digit->value = buf[i];
+ seg_pattern = map_to_seg7(&map_seg7, digit->value);
+
+ for (j = 0; j < TM16XX_DIGIT_SEGMENTS; j++) {
+ ds = &digit->segments[j];
+ val = seg_pattern & BIT(j);
+ tm16xx_set_seg(display, ds->grid, ds->segment, val);
+ }
+ }
+
+ for (; i < display->num_digits; i++) {
+ digit = &display->digits[i];
+ digit->value = 0;
+ for (j = 0; j < TM16XX_DIGIT_SEGMENTS; j++) {
+ ds = &digit->segments[j];
+ tm16xx_set_seg(display, ds->grid, ds->segment, 0);
+ }
+ }
+
+ schedule_work(&display->flush_display);
+ return count;
+}
+
+/**
+ * tm16xx_num_digits_show() - Sysfs: show number of digits on display
+ * @dev: pointer to device
+ * @attr: device attribute (unused)
+ * @buf: output buffer
+ *
+ * Return: number of bytes written
+ */
+static ssize_t tm16xx_num_digits_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev *led_cdev = dev_get_drvdata(dev);
+ struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent);
+
+ return sprintf(buf, "%u\n", display->num_digits);
+}
+
+/**
+ * tm16xx_map_seg7_show() - Sysfs: show current 7-segment character map (binary blob)
+ * @dev: pointer to device
+ * @attr: device attribute (unused)
+ * @buf: output buffer
+ *
+ * Return: size of map_seg7
+ */
+static ssize_t tm16xx_map_seg7_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ memcpy(buf, &map_seg7, sizeof(map_seg7));
+ return sizeof(map_seg7);
+}
+
+/**
+ * tm16xx_map_seg7_store() - Sysfs: set 7-segment character map (binary blob)
+ * @dev: pointer to device
+ * @attr: device attribute (unused)
+ * @buf: new mapping (must match size of map_seg7)
+ * @cnt: buffer length
+ *
+ * Return: cnt on success, negative errno on failure
+ */
+static ssize_t tm16xx_map_seg7_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t cnt)
+{
+ if (cnt != sizeof(map_seg7))
+ return -EINVAL;
+ memcpy(&map_seg7, buf, cnt);
+ return cnt;
+}
+
+static DEVICE_ATTR(value, 0644, tm16xx_value_show, tm16xx_value_store);
+static DEVICE_ATTR(num_digits, 0444, tm16xx_num_digits_show, NULL);
+static DEVICE_ATTR(map_seg7, 0644, tm16xx_map_seg7_show, tm16xx_map_seg7_store);
+
+static struct attribute *tm16xx_main_led_attrs[] = {
+ &dev_attr_value.attr,
+ &dev_attr_num_digits.attr,
+ &dev_attr_map_seg7.attr,
+ NULL,
+};
+ATTRIBUTE_GROUPS(tm16xx_main_led);
+
+/**
+ * tm16xx_display_init() - Initialize display hardware and state
+ * @display: pointer to tm16xx_display
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int tm16xx_display_init(struct tm16xx_display *display)
+{
+ unsigned int nbits = tm16xx_led_nbits(display);
+
+ dev_dbg(display->dev, "Initializing display\n");
+ schedule_work(&display->flush_init);
+ flush_work(&display->flush_init);
+ if (display->flush_status < 0)
+ return display->flush_status;
+
+ if (tm16xx_init_value) {
+ tm16xx_value_store(display->main_led.dev, NULL,
+ tm16xx_init_value,
+ strlen(tm16xx_init_value));
+ } else {
+ bitmap_fill(display->state, nbits);
+ schedule_work(&display->flush_display);
+ flush_work(&display->flush_display);
+ bitmap_zero(display->state, nbits);
+ if (display->flush_status < 0)
+ return display->flush_status;
+ }
+
+ dev_info(display->dev, "Display turned on\n");
+
+ return 0;
+}
+
+/**
+ * tm16xx_parse_dt() - Parse device tree for digit and LED mapping
+ * @dev: pointer to struct device
+ * @display: pointer to tm16xx_display
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+static int tm16xx_parse_dt(struct device *dev, struct tm16xx_display *display)
+{
+ struct fwnode_handle *leds_node, *digits_node, *child;
+ struct tm16xx_led *led;
+ struct tm16xx_digit *digit;
+ int max_grid = 0, max_segment = 0;
+ int ret, i, j;
+ u32 segments[TM16XX_DIGIT_SEGMENTS * 2];
+ u32 reg[2];
+
+ /* parse digits */
+ digits_node = device_get_named_child_node(dev, "digits");
+ if (digits_node) {
+ display->num_digits = 0;
+ fwnode_for_each_child_node(digits_node, child)
+ display->num_digits++;
+
+ dev_dbg(dev, "Number of digits: %u\n", display->num_digits);
+
+ if (display->num_digits) {
+ display->digits = devm_kcalloc(dev, display->num_digits,
+ sizeof(*display->digits),
+ GFP_KERNEL);
+ if (!display->digits) {
+ fwnode_handle_put(digits_node);
+ return -ENOMEM;
+ }
+
+ i = 0;
+ fwnode_for_each_child_node(digits_node, child) {
+ digit = &display->digits[i];
+
+ ret = fwnode_property_read_u32(child, "reg",
+ reg);
+ if (ret < 0) {
+ fwnode_handle_put(child);
+ fwnode_handle_put(digits_node);
+ return ret;
+ }
+
+ ret = fwnode_property_read_u32_array(child,
+ "segments",
+ segments,
+ TM16XX_DIGIT_SEGMENTS * 2);
+ if (ret < 0) {
+ fwnode_handle_put(child);
+ fwnode_handle_put(digits_node);
+ return ret;
+ }
+
+ for (j = 0; j < TM16XX_DIGIT_SEGMENTS; ++j) {
+ digit->segments[j].grid = segments[2 * j];
+ digit->segments[j].segment = segments[2 * j + 1];
+ max_grid = umax(max_grid,
+ digit->segments[j].grid);
+ max_segment = umax(max_segment,
+ digit->segments[j].segment);
+ }
+ digit->value = 0;
+ i++;
+ }
+
+ fwnode_handle_put(digits_node);
+ }
+ }
+
+ /* parse leds */
+ leds_node = device_get_named_child_node(dev, "leds");
+ if (leds_node) {
+ display->num_leds = 0;
+ fwnode_for_each_child_node(leds_node, child)
+ display->num_leds++;
+
+ dev_dbg(dev, "Number of LEDs: %u\n", display->num_leds);
+
+ if (display->num_leds) {
+ display->leds = devm_kcalloc(dev, display->num_leds,
+ sizeof(*display->leds),
+ GFP_KERNEL);
+ if (!display->leds) {
+ fwnode_handle_put(leds_node);
+ return -ENOMEM;
+ }
+
+ i = 0;
+ fwnode_for_each_child_node(leds_node, child) {
+ led = &display->leds[i];
+ ret = fwnode_property_read_u32_array(child,
+ "reg", reg,
+ 2);
+ if (ret < 0) {
+ fwnode_handle_put(child);
+ fwnode_handle_put(leds_node);
+ return ret;
+ }
+
+ led->grid = reg[0];
+ led->segment = reg[1];
+ max_grid = umax(max_grid, led->grid);
+ max_segment = umax(max_segment, led->segment);
+ i++;
+ }
+ }
+
+ fwnode_handle_put(leds_node);
+ }
+
+ if (max_grid >= display->controller->max_grids) {
+ dev_err(dev, "grid %u exceeds controller max_grids %u\n",
+ max_grid, display->controller->max_grids);
+ return -EINVAL;
+ }
+
+ if (max_segment >= display->controller->max_segments) {
+ dev_err(dev, "segment %u exceeds controller max_segments %u\n",
+ max_segment, display->controller->max_segments);
+ return -EINVAL;
+ }
+
+ display->num_grids = max_grid + 1;
+ display->num_segments = max_segment + 1;
+
+ dev_dbg(dev, "Number of grids: %u\n", display->num_grids);
+ dev_dbg(dev, "Number of segments: %u\n", display->num_segments);
+
+ return 0;
+}
+
+/**
+ * tm16xx_probe() - Probe and initialize display device, register LEDs
+ * @display: pointer to tm16xx_display
+ *
+ * Return: 0 on success, negative error code on failure
+ */
+int tm16xx_probe(struct tm16xx_display *display)
+{
+ struct device *dev = display->dev;
+ struct led_classdev *main = &display->main_led;
+ struct fwnode_handle *leds_node, *child;
+ unsigned int nbits;
+ int ret, i;
+
+ dev_dbg(dev, "Probing device\n");
+ ret = tm16xx_parse_dt(dev, display);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to parse device tree\n");
+
+ nbits = tm16xx_led_nbits(display);
+ display->state = devm_bitmap_zalloc(dev, nbits, GFP_KERNEL);
+ if (!display->state)
+ return -ENOMEM;
+
+ mutex_init(&display->lock);
+ INIT_WORK(&display->flush_init, tm16xx_display_flush_init);
+ INIT_WORK(&display->flush_display, tm16xx_display_flush_data);
+
+ /* Initialize main LED properties */
+ if (dev->of_node)
+ main->name = dev->of_node->name;
+ if (!main->name)
+ main->name = "display";
+ device_property_read_string(dev, "label", &main->name);
+
+ main->max_brightness = display->controller->max_brightness;
+ device_property_read_u32(dev, "max-brightness", &main->max_brightness);
+ main->max_brightness = umin(main->max_brightness,
+ display->controller->max_brightness);
+
+ main->brightness = main->max_brightness;
+ device_property_read_u32(dev, "default-brightness", &main->brightness);
+ main->brightness = umin(main->brightness, main->max_brightness);
+
+ main->brightness_set = tm16xx_brightness_set;
+ main->groups = tm16xx_main_led_groups;
+ main->flags = LED_RETAIN_AT_SHUTDOWN | LED_CORE_SUSPENDRESUME;
+
+ ret = led_classdev_register(dev, main);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Failed to register main LED\n");
+
+ i = 0;
+ leds_node = device_get_named_child_node(dev, "leds");
+ fwnode_for_each_child_node(leds_node, child) {
+ struct tm16xx_led *led = &display->leds[i];
+ struct led_init_data led_init = {
+ .fwnode = child,
+ .devicename = dev_name(main->dev),
+ .devname_mandatory = true,
+ .default_label = "led",
+ };
+ led->cdev.max_brightness = 1;
+ led->cdev.brightness_set = tm16xx_led_set;
+ led->cdev.flags = LED_RETAIN_AT_SHUTDOWN |
+ LED_CORE_SUSPENDRESUME;
+
+ ret = led_classdev_register_ext(dev, &led->cdev, &led_init);
+ if (ret < 0) {
+ fwnode_handle_put(child);
+ dev_err_probe(dev, ret, "Failed to register LED %s\n",
+ led->cdev.name);
+ goto unregister_leds;
+ }
+
+ i++;
+ }
+
+ ret = tm16xx_display_init(display);
+ if (ret < 0) {
+ dev_err_probe(dev, ret, "Failed to initialize display\n");
+ goto unregister_leds;
+ }
+
+ return 0;
+
+unregister_leds:
+ while (i--)
+ led_classdev_unregister(&display->leds[i].cdev);
+
+ led_classdev_unregister(main);
+ return ret;
+}
+EXPORT_SYMBOL_NS(tm16xx_probe, "TM16XX");
+
+/**
+ * tm16xx_remove() - Remove display, unregister LEDs, blank output
+ * @display: pointer to tm16xx_display
+ */
+void tm16xx_remove(struct tm16xx_display *display)
+{
+ unsigned int nbits = tm16xx_led_nbits(display);
+ struct tm16xx_led *led;
+
+ dev_dbg(display->dev, "Removing display\n");
+
+ /*
+ * Unregister LEDs first to immediately stop trigger activity.
+ * This prevents LED triggers from attempting to access hardware
+ * after it's been disconnected or driver unloaded.
+ */
+ for (int i = 0; i < display->num_leds; i++) {
+ led = &display->leds[i];
+ led_classdev_unregister(&led->cdev);
+ }
+ led_classdev_unregister(&display->main_led);
+
+ /* Clear display state */
+ bitmap_zero(display->state, nbits);
+ schedule_work(&display->flush_display);
+ flush_work(&display->flush_display);
+
+ /* Turn off display */
+ display->main_led.brightness = LED_OFF;
+ schedule_work(&display->flush_init);
+ flush_work(&display->flush_init);
+
+ dev_info(display->dev, "Display turned off\n");
+}
+EXPORT_SYMBOL_NS(tm16xx_remove, "TM16XX");
+
+MODULE_AUTHOR("Jean-François Lessard");
+MODULE_DESCRIPTION("TM16xx LED Display Controllers");
+MODULE_LICENSE("GPL");
--
2.43.0
On Sun, Aug 24, 2025 at 11:32:29PM -0400, Jean-François Lessard wrote: > Add driver for TM16xx family LED controllers and compatible chips from > multiple vendors including Titan Micro, Fuda Hisi, i-Core, Princeton, and > Winrise. These controllers drive 7-segment digits and individual LED icons > through either I2C or SPI buses. > > Successfully tested on various ARM TV boxes including H96 Max, Magicsee N5, > Tanix TX3 Mini, Tanix TX6, X92, and X96 Max across different SoC platforms > (Rockchip, Amlogic, Allwinner). ... > Notes: > checkpatch reports false positives that are intentionally ignored: > DEVICE_ATTR_FUNCTIONS: Functions are correctly prefixed with driver > name (tm16xx_*) following standard kernel practice for device > attribute functions to avoid namespace conflicts. > BIT_MACRO: bit shifts are used for field values while GENMASK/BIT > are used for bit positions per semantic convention > include <linux/of.h> is required for the default name of the main led > device, excluding the unit address, as implemented in > drivers/leds/led-core.c which relies on of_node->name Sorry, but I do not see how of.h is related to all this... Please, drop it. > LED registration uses non-devm variant on-purpose to allow explicit > unregistration on device removal, ensuring LED triggers are > immediately stopped. This prevents stale LED trigger activity from > continuing after the hardware is gone, avoiding the need for complex > state tracking in brightness callbacks. ... > +What: /sys/class/leds/<led>/value > +Date: August 2025 > +KernelVersion: 6.17 The Date should be approximate date of the kernel release (alternatively, -rc1 of that). The version is estimated version where ABI can be found first. Both of these need to be changed. > +Contact: Jean-François Lessard <jefflessard3@gmail.com> > +Description: > + Controls the text displayed on the TM16xx 7-segment display. > + > + Reading returns the current display content as ASCII characters, > + one character per digit position, followed by a newline. > + > + Writing sets new display content. Input characters are mapped > + to 7-segment patterns using the configured character map. The > + string length should not exceed the number of available digits > + (see num_digits). Shorter strings will clear remaining digits. > + > + Example: > + echo "1234" > value # Display "1234" > + cat value # Returns "1234\n" > + > +What: /sys/class/leds/<led>/num_digits > +Date: August 2025 > +KernelVersion: 6.17 > +Contact: Jean-François Lessard <jefflessard3@gmail.com> > +Description: > + Read-only attribute showing the number of 7-segment digit > + positions available on this TM16xx display controller. > + > + The value is determined by the device tree configuration > + and represents the maximum length for the 'value' attribute. > + > + Example: > + cat num_digits # Returns "4\n" for 4-digit display > + > +What: /sys/class/leds/<led>/map_seg7 > +Date: August 2025 > +KernelVersion: 6.17 > +Contact: Jean-François Lessard <jefflessard3@gmail.com> > +Description: > + Read/write binary blob representing the ASCII-to-7-segment > + display conversion table used by the TM16xx driver, as defined > + by struct seg7_conversion_map in <linux/map_to_7segment.h>. > + > + This attribute is not human-readable. Writes must match the > + struct size exactly, else -EINVAL is returned; reads return the > + entire mapping as a binary blob. > + > + This interface and its implementation match existing conventions > + used in auxdisplay and segment-mapped display drivers since 2005. > + > + ABI note: This style of binary sysfs attribute *is an exception* > + to current "one value per file, text only" sysfs rules, for > + historical compatibility and driver uniformity. New drivers are > + discouraged from introducing additional binary sysfs ABIs. > + > + Reference interface guidance: > + - include/uapi/linux/map_to_7segment.h So, the driver is under auxdisplay, but at the same time it completely relies on LED subsystem... What's going on here? Btw, have you seen https://lore.kernel.org/linux-leds/20231011190017.1230898-1-wse@tuxedocomputers.com/ ? And if so, what're your takeaways? (Yes, I know that's about different HW) > +Users: Display configuration utilities and embedded system scripts/tools. ... > + * Copyright (C) 2024 Jean-François Lessard My calendar shows something different. > +#include <linux/bitfield.h> > +#include <linux/bitmap.h> Is this used? > +#include <linux/leds.h> > +#include <linux/workqueue.h> ... > +#define TM16XX_DIGIT_SEGMENTS 7 Why do we even need this? ... > +#define TM16XX_CTRL_BRIGHTNESS(on, val, prfx) \ > + ((on) ? (FIELD_PREP(prfx##_CTRL_BR_MASK, (val)) | prfx##_CTRL_ON) : 0) prefix can be spelled fully, going slightly over 80 is not a crime. ... > +struct tm16xx_display { > + struct device *dev; > + const struct tm16xx_controller *controller; > + union { > + struct i2c_client *i2c; > + struct spi_device *spi; > + } client; Why? Just drop it. struct device *dev is enough and I can't see the need in this at all. > + u8 *spi_buffer; > + u8 num_grids; > + u8 num_segments; > + struct led_classdev main_led; > + struct tm16xx_led *leds; > + u8 num_leds; > + struct tm16xx_digit *digits; > + u8 num_digits; > + struct work_struct flush_init; > + struct work_struct flush_display; > + int flush_status; > + struct mutex lock; /* prevents concurrent work operations */ > + unsigned long *state; > +}; ... > + * Copyright (C) 2024 Jean-François Lessard Year? ... > +#include <linux/map_to_7segment.h> > +#include <linux/module.h> > +#include <linux/of.h> > +#include <linux/property.h> Please, follow IWYU principle. ... > +static ssize_t tm16xx_num_digits_show(struct device *dev, > + struct device_attribute *attr, char *buf) > +{ > + struct led_classdev *led_cdev = dev_get_drvdata(dev); > + struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent); > + > + return sprintf(buf, "%u\n", display->num_digits); Should be sysfs_emit(). > +} ... > +static ssize_t tm16xx_map_seg7_show(struct device *dev, > + struct device_attribute *attr, char *buf) > +{ > + memcpy(buf, &map_seg7, sizeof(map_seg7)); > + return sizeof(map_seg7); > +} Can we use LINEDISP library? ... > +static struct attribute *tm16xx_main_led_attrs[] = { > + &dev_attr_value.attr, > + &dev_attr_num_digits.attr, > + &dev_attr_map_seg7.attr, > + NULL, No comma in the terminator entry. > +}; > +ATTRIBUTE_GROUPS(tm16xx_main_led); ... > +static int tm16xx_display_init(struct tm16xx_display *display) > +{ > + unsigned int nbits = tm16xx_led_nbits(display); > + dev_dbg(display->dev, "Initializing display\n"); Please, drop all these dev_dbg() over the code as they are close to useless, use tracers and other mechanisms to debug if required. Also drop unneeded kernel-doc for the (esp. static) functions that have well established meaning (e.g., no need a kernel-doc for device attributes as they should be documented in the ABI). > + schedule_work(&display->flush_init); > + flush_work(&display->flush_init); > + if (display->flush_status < 0) > + return display->flush_status; > + > + if (tm16xx_init_value) { > + tm16xx_value_store(display->main_led.dev, NULL, > + tm16xx_init_value, > + strlen(tm16xx_init_value)); > + } else { > + bitmap_fill(display->state, nbits); > + schedule_work(&display->flush_display); > + flush_work(&display->flush_display); > + bitmap_zero(display->state, nbits); > + if (display->flush_status < 0) > + return display->flush_status; > + } > + > + dev_info(display->dev, "Display turned on\n"); > + > + return 0; > +} ... > +static int tm16xx_parse_dt(struct device *dev, struct tm16xx_display *display) Why DT only? No support for other platforms? Why? I think this is just matter of naming the function properly. > +{ > + struct fwnode_handle *leds_node, *digits_node, *child; > + struct tm16xx_led *led; > + struct tm16xx_digit *digit; > + int max_grid = 0, max_segment = 0; Why signed? > + int ret, i, j; Why are i and j signed? > + u32 segments[TM16XX_DIGIT_SEGMENTS * 2]; > + u32 reg[2]; > + > + /* parse digits */ > + digits_node = device_get_named_child_node(dev, "digits"); > + if (digits_node) { > + display->num_digits = 0; > + fwnode_for_each_child_node(digits_node, child) > + display->num_digits++; Don't we have a _count API for this? > + dev_dbg(dev, "Number of digits: %u\n", display->num_digits); > + > + if (display->num_digits) { > + display->digits = devm_kcalloc(dev, display->num_digits, > + sizeof(*display->digits), > + GFP_KERNEL); > + if (!display->digits) { > + fwnode_handle_put(digits_node); Use RAII instead, we have defined __free() method for this. > + return -ENOMEM; > + } > + > + i = 0; > + fwnode_for_each_child_node(digits_node, child) { Ditto. Use _scoped variant. > + digit = &display->digits[i]; > + ret = fwnode_property_read_u32(child, "reg", > + reg); One line. > + if (ret < 0) { Can it be positive? Here and everywhere else, if there is no positive return, use 'if (ret)'. > + fwnode_handle_put(child); > + fwnode_handle_put(digits_node); > + return ret; > + } > + > + ret = fwnode_property_read_u32_array(child, > + "segments", > + segments, > + TM16XX_DIGIT_SEGMENTS * 2); > + if (ret < 0) { > + fwnode_handle_put(child); > + fwnode_handle_put(digits_node); > + return ret; > + } > + > + for (j = 0; j < TM16XX_DIGIT_SEGMENTS; ++j) { > + digit->segments[j].grid = segments[2 * j]; > + digit->segments[j].segment = segments[2 * j + 1]; > + max_grid = umax(max_grid, Firstly, the variables made signed and then specifically force them to be unsigned in the macro. Weird. Can we make them to be a proper type and use max()? > + digit->segments[j].grid); One line > + max_segment = umax(max_segment, > + digit->segments[j].segment); As per above comments. > + } > + digit->value = 0; > + i++; > + } > + > + fwnode_handle_put(digits_node); > + } > + } > + > + /* parse leds */ > + leds_node = device_get_named_child_node(dev, "leds"); > + if (leds_node) { > + display->num_leds = 0; > + fwnode_for_each_child_node(leds_node, child) > + display->num_leds++; As per above. > + dev_dbg(dev, "Number of LEDs: %u\n", display->num_leds); > + > + if (display->num_leds) { > + display->leds = devm_kcalloc(dev, display->num_leds, > + sizeof(*display->leds), > + GFP_KERNEL); > + if (!display->leds) { > + fwnode_handle_put(leds_node); > + return -ENOMEM; > + } > + > + i = 0; > + fwnode_for_each_child_node(leds_node, child) { > + led = &display->leds[i]; > + ret = fwnode_property_read_u32_array(child, > + "reg", reg, > + 2); Make it one line. > + if (ret < 0) { > + fwnode_handle_put(child); > + fwnode_handle_put(leds_node); > + return ret; > + } > + > + led->grid = reg[0]; > + led->segment = reg[1]; > + max_grid = umax(max_grid, led->grid); > + max_segment = umax(max_segment, led->segment); > + i++; > + } > + } > + > + fwnode_handle_put(leds_node); > + } > + > + if (max_grid >= display->controller->max_grids) { > + dev_err(dev, "grid %u exceeds controller max_grids %u\n", > + max_grid, display->controller->max_grids); > + return -EINVAL; > + } > + > + if (max_segment >= display->controller->max_segments) { > + dev_err(dev, "segment %u exceeds controller max_segments %u\n", > + max_segment, display->controller->max_segments); > + return -EINVAL; > + } > + > + display->num_grids = max_grid + 1; > + display->num_segments = max_segment + 1; > + dev_dbg(dev, "Number of grids: %u\n", display->num_grids); > + dev_dbg(dev, "Number of segments: %u\n", display->num_segments); I didn't get this. You mean that they are not strictly 7-segment ones? > + > + return 0; > +} ... > +int tm16xx_probe(struct tm16xx_display *display) > +{ > + struct device *dev = display->dev; > + struct led_classdev *main = &display->main_led; > + struct fwnode_handle *leds_node, *child; > + unsigned int nbits; > + int ret, i; Why is i signed? > + dev_dbg(dev, "Probing device\n"); > + ret = tm16xx_parse_dt(dev, display); > + if (ret < 0) > + return dev_err_probe(dev, ret, "Failed to parse device tree\n"); > + > + nbits = tm16xx_led_nbits(display); > + display->state = devm_bitmap_zalloc(dev, nbits, GFP_KERNEL); > + if (!display->state) > + return -ENOMEM; > + mutex_init(&display->lock); devm_mutex_init() > + INIT_WORK(&display->flush_init, tm16xx_display_flush_init); > + INIT_WORK(&display->flush_display, tm16xx_display_flush_data); > + /* Initialize main LED properties */ > + if (dev->of_node) > + main->name = dev->of_node->name; > + if (!main->name) > + main->name = "display"; > + device_property_read_string(dev, "label", &main->name); My gosh. This is done in the LED core if we even need this... > + main->max_brightness = display->controller->max_brightness; > + device_property_read_u32(dev, "max-brightness", &main->max_brightness); > + main->max_brightness = umin(main->max_brightness, > + display->controller->max_brightness); > + > + main->brightness = main->max_brightness; > + device_property_read_u32(dev, "default-brightness", &main->brightness); > + main->brightness = umin(main->brightness, main->max_brightness); > + > + main->brightness_set = tm16xx_brightness_set; > + main->groups = tm16xx_main_led_groups; > + main->flags = LED_RETAIN_AT_SHUTDOWN | LED_CORE_SUSPENDRESUME; > + > + ret = led_classdev_register(dev, main); > + if (ret < 0) > + return dev_err_probe(dev, ret, "Failed to register main LED\n"); > + i = 0; > + leds_node = device_get_named_child_node(dev, "leds"); > + fwnode_for_each_child_node(leds_node, child) { > + struct tm16xx_led *led = &display->leds[i]; > + struct led_init_data led_init = { > + .fwnode = child, > + .devicename = dev_name(main->dev), > + .devname_mandatory = true, > + .default_label = "led", > + }; > + led->cdev.max_brightness = 1; > + led->cdev.brightness_set = tm16xx_led_set; > + led->cdev.flags = LED_RETAIN_AT_SHUTDOWN | > + LED_CORE_SUSPENDRESUME; > + > + ret = led_classdev_register_ext(dev, &led->cdev, &led_init); > + if (ret < 0) { > + fwnode_handle_put(child); > + dev_err_probe(dev, ret, "Failed to register LED %s\n", > + led->cdev.name); > + goto unregister_leds; > + } > + > + i++; > + } > + > + ret = tm16xx_display_init(display); > + if (ret < 0) { > + dev_err_probe(dev, ret, "Failed to initialize display\n"); > + goto unregister_leds; > + } > + > + return 0; > + > +unregister_leds: > + while (i--) > + led_classdev_unregister(&display->leds[i].cdev); > + > + led_classdev_unregister(main); > + return ret; > +} ... > +void tm16xx_remove(struct tm16xx_display *display) > +{ > + unsigned int nbits = tm16xx_led_nbits(display); > + struct tm16xx_led *led; > + dev_dbg(display->dev, "Removing display\n"); Unneeded noise. > + /* > + * Unregister LEDs first to immediately stop trigger activity. > + * This prevents LED triggers from attempting to access hardware > + * after it's been disconnected or driver unloaded. > + */ > + for (int i = 0; i < display->num_leds; i++) { Why iterator is signed? > + led = &display->leds[i]; > + led_classdev_unregister(&led->cdev); > + } > + led_classdev_unregister(&display->main_led); > + > + /* Clear display state */ > + bitmap_zero(display->state, nbits); > + schedule_work(&display->flush_display); > + flush_work(&display->flush_display); > + > + /* Turn off display */ > + display->main_led.brightness = LED_OFF; > + schedule_work(&display->flush_init); > + flush_work(&display->flush_init); > + dev_info(display->dev, "Display turned off\n"); Unneeded noise. > +} -- With Best Regards, Andy Shevchenko
Le 25 août 2025 11 h 14 min 21 s HAE, Andy Shevchenko <andriy.shevchenko@intel.com> a écrit : >On Sun, Aug 24, 2025 at 11:32:29PM -0400, Jean-François Lessard wrote: >> Add driver for TM16xx family LED controllers and compatible chips from >> multiple vendors including Titan Micro, Fuda Hisi, i-Core, Princeton, and >> Winrise. These controllers drive 7-segment digits and individual LED icons >> through either I2C or SPI buses. >> >> Successfully tested on various ARM TV boxes including H96 Max, Magicsee N5, >> Tanix TX3 Mini, Tanix TX6, X92, and X96 Max across different SoC platforms >> (Rockchip, Amlogic, Allwinner). > >... > >> Notes: >> checkpatch reports false positives that are intentionally ignored: >> DEVICE_ATTR_FUNCTIONS: Functions are correctly prefixed with driver >> name (tm16xx_*) following standard kernel practice for device >> attribute functions to avoid namespace conflicts. >> BIT_MACRO: bit shifts are used for field values while GENMASK/BIT >> are used for bit positions per semantic convention > >> include <linux/of.h> is required for the default name of the main led >> device, excluding the unit address, as implemented in >> drivers/leds/led-core.c which relies on of_node->name > >Sorry, but I do not see how of.h is related to all this... Please, drop it. > This relates to the LED subsystem integration question below. I'll address both together and remove the include if not needed. >> LED registration uses non-devm variant on-purpose to allow explicit >> unregistration on device removal, ensuring LED triggers are >> immediately stopped. This prevents stale LED trigger activity from >> continuing after the hardware is gone, avoiding the need for complex >> state tracking in brightness callbacks. > >... > >> +What: /sys/class/leds/<led>/value > >> +Date: August 2025 >> +KernelVersion: 6.17 > >The Date should be approximate date of the kernel release (alternatively, -rc1 >of that). The version is estimated version where ABI can be found first. > >Both of these need to be changed. > > Given that 6.17-rc3 was just released, should I target 6.18 for the kernel version and use a March 2025 date for the estimated release timeframe? >> +Contact: Jean-François Lessard <jefflessard3@gmail.com> >> +Description: >> + Controls the text displayed on the TM16xx 7-segment display. >> + >> + Reading returns the current display content as ASCII characters, >> + one character per digit position, followed by a newline. >> + >> + Writing sets new display content. Input characters are mapped >> + to 7-segment patterns using the configured character map. The >> + string length should not exceed the number of available digits >> + (see num_digits). Shorter strings will clear remaining digits. >> + >> + Example: >> + echo "1234" > value # Display "1234" >> + cat value # Returns "1234\n" >> + >> +What: /sys/class/leds/<led>/num_digits >> +Date: August 2025 >> +KernelVersion: 6.17 >> +Contact: Jean-François Lessard <jefflessard3@gmail.com> >> +Description: >> + Read-only attribute showing the number of 7-segment digit >> + positions available on this TM16xx display controller. >> + >> + The value is determined by the device tree configuration >> + and represents the maximum length for the 'value' attribute. >> + >> + Example: >> + cat num_digits # Returns "4\n" for 4-digit display >> + >> +What: /sys/class/leds/<led>/map_seg7 >> +Date: August 2025 >> +KernelVersion: 6.17 >> +Contact: Jean-François Lessard <jefflessard3@gmail.com> >> +Description: >> + Read/write binary blob representing the ASCII-to-7-segment >> + display conversion table used by the TM16xx driver, as defined >> + by struct seg7_conversion_map in <linux/map_to_7segment.h>. >> + >> + This attribute is not human-readable. Writes must match the >> + struct size exactly, else -EINVAL is returned; reads return the >> + entire mapping as a binary blob. >> + >> + This interface and its implementation match existing conventions >> + used in auxdisplay and segment-mapped display drivers since 2005. >> + >> + ABI note: This style of binary sysfs attribute *is an exception* >> + to current "one value per file, text only" sysfs rules, for >> + historical compatibility and driver uniformity. New drivers are >> + discouraged from introducing additional binary sysfs ABIs. >> + >> + Reference interface guidance: >> + - include/uapi/linux/map_to_7segment.h > >So, the driver is under auxdisplay, but at the same time it completely relies >on LED subsystem... What's going on here? > The design integrates with the LED subsystem for two main reasons: 1. Brightness control: The entire display brightness is controlled at the display level (individual LED icons can only be on/off via their brightness attributes). The LED subsystem provides established mechanisms for this. 2. Coherent sysfs interface: This provides consistent /sys/class/leds/display for display-level controls and /sys/class/leds/display::{function} for individual icons. I'm seeking your guidance on the best design for the auxdisplay subsystem. >Btw, have you seen >https://lore.kernel.org/linux-leds/20231011190017.1230898-1-wse@tuxedocomputers.com/ >? And if so, what're your takeaways? (Yes, I know that's about different HW) > I've read the thread but I'm not clear on the specific point you're making. Could you clarify what aspect I should focus on? (Though, my personal opinion is that using auxdisplay for keyboard LEDs doesn't really make sense. I think it would be better to properly implement it the required mechanism into input subsystem, with maybe some integration with the leds subsystem. Just a quick opinion, I do not master all aspects of this question at all.) >> +Users: Display configuration utilities and embedded system scripts/tools. > >... > >> + * Copyright (C) 2024 Jean-François Lessard > >My calendar shows something different. > > The original code was developed in 2024, though it's being submitted in 2025. >> +#include <linux/bitfield.h> > >> +#include <linux/bitmap.h> > >Is this used? > Yes, display->state is a bitmap. I'll move this include to tm16xx_core.c since it's not used in the header itself. >> +#include <linux/leds.h> >> +#include <linux/workqueue.h> > >... > >> +#define TM16XX_DIGIT_SEGMENTS 7 > >Why do we even need this? > You're right. I'll move it to tm16xx_core.c. >... > >> +#define TM16XX_CTRL_BRIGHTNESS(on, val, prfx) \ >> + ((on) ? (FIELD_PREP(prfx##_CTRL_BR_MASK, (val)) | prfx##_CTRL_ON) : 0) > >prefix can be spelled fully, going slightly over 80 is not a crime. > Acknowledged, I'll use the full prefix name. >... > >> +struct tm16xx_display { >> + struct device *dev; >> + const struct tm16xx_controller *controller; > >> + union { >> + struct i2c_client *i2c; >> + struct spi_device *spi; >> + } client; > >Why? Just drop it. struct device *dev is enough and I can't see the need >in this at all. > I'll remove this union and use container_of(dev, struct i2c_client, dev) or container_of(dev, struct spi_device, dev) where the specific client type is needed. >> + u8 *spi_buffer; >> + u8 num_grids; >> + u8 num_segments; >> + struct led_classdev main_led; >> + struct tm16xx_led *leds; >> + u8 num_leds; >> + struct tm16xx_digit *digits; >> + u8 num_digits; >> + struct work_struct flush_init; >> + struct work_struct flush_display; >> + int flush_status; >> + struct mutex lock; /* prevents concurrent work operations */ >> + unsigned long *state; >> +}; > >... > >> + * Copyright (C) 2024 Jean-François Lessard > >Year? > Same as above. >... > >> +#include <linux/map_to_7segment.h> >> +#include <linux/module.h> >> +#include <linux/of.h> >> +#include <linux/property.h> > >Please, follow IWYU principle. > I'll explicitly include all required headers in each source file instead of relying on transitive includes from the header. >... > >> +static ssize_t tm16xx_num_digits_show(struct device *dev, >> + struct device_attribute *attr, char *buf) >> +{ >> + struct led_classdev *led_cdev = dev_get_drvdata(dev); >> + struct tm16xx_display *display = dev_get_drvdata(led_cdev->dev->parent); >> + >> + return sprintf(buf, "%u\n", display->num_digits); > >Should be sysfs_emit(). > Well received. >> +} > >... > >> +static ssize_t tm16xx_map_seg7_show(struct device *dev, >> + struct device_attribute *attr, char *buf) >> +{ >> + memcpy(buf, &map_seg7, sizeof(map_seg7)); >> + return sizeof(map_seg7); >> +} > >Can we use LINEDISP library? > I considered this but have two concerns: 1. It creates attributes under a virtual "linedisp.{n}" device, which conflicts with the coherent LED sysfs design 2. Messages scroll indefinitely. There should be control for single-pass scrolling I'm willing to contribute improvements to line-display if needed, but this depends on resolving the main LED design question above. >... > >> +static struct attribute *tm16xx_main_led_attrs[] = { >> + &dev_attr_value.attr, >> + &dev_attr_num_digits.attr, >> + &dev_attr_map_seg7.attr, > >> + NULL, > >No comma in the terminator entry. > Well received. >> +}; >> +ATTRIBUTE_GROUPS(tm16xx_main_led); > >... > >> +static int tm16xx_display_init(struct tm16xx_display *display) >> +{ >> + unsigned int nbits = tm16xx_led_nbits(display); > >> + dev_dbg(display->dev, "Initializing display\n"); > >Please, drop all these dev_dbg() over the code as they are close to useless, >use tracers and other mechanisms to debug if required. > Understood, I'll remove the debug noise. >Also drop unneeded kernel-doc for the (esp. static) functions that have well >established meaning (e.g., no need a kernel-doc for device attributes as they >should be documented in the ABI). > Understood, I'll remove these kernel-doc. >> + schedule_work(&display->flush_init); >> + flush_work(&display->flush_init); >> + if (display->flush_status < 0) >> + return display->flush_status; >> + >> + if (tm16xx_init_value) { >> + tm16xx_value_store(display->main_led.dev, NULL, >> + tm16xx_init_value, >> + strlen(tm16xx_init_value)); >> + } else { >> + bitmap_fill(display->state, nbits); >> + schedule_work(&display->flush_display); >> + flush_work(&display->flush_display); >> + bitmap_zero(display->state, nbits); >> + if (display->flush_status < 0) >> + return display->flush_status; >> + } >> + >> + dev_info(display->dev, "Display turned on\n"); >> + >> + return 0; >> +} > >... > >> +static int tm16xx_parse_dt(struct device *dev, struct tm16xx_display *display) > >Why DT only? No support for other platforms? Why? >I think this is just matter of naming the function properly. > You're right, I'll rename it to tm16xx_parse_fwnode since it uses fwnode APIs. >> +{ >> + struct fwnode_handle *leds_node, *digits_node, *child; >> + struct tm16xx_led *led; >> + struct tm16xx_digit *digit; > >> + int max_grid = 0, max_segment = 0; > >Why signed? > My oversight - I'll change these to appropriate unsigned types. >> + int ret, i, j; > >Why are i and j signed? > Standard kernel practice uses int for simple loop counters. But I will change to unsigned types for consistency. >> + u32 segments[TM16XX_DIGIT_SEGMENTS * 2]; >> + u32 reg[2]; >> + >> + /* parse digits */ >> + digits_node = device_get_named_child_node(dev, "digits"); >> + if (digits_node) { > >> + display->num_digits = 0; >> + fwnode_for_each_child_node(digits_node, child) >> + display->num_digits++; > >Don't we have a _count API for this? > I'll use device_get_child_node_count() instead of manual counting loops. >> + dev_dbg(dev, "Number of digits: %u\n", display->num_digits); >> + >> + if (display->num_digits) { >> + display->digits = devm_kcalloc(dev, display->num_digits, >> + sizeof(*display->digits), >> + GFP_KERNEL); >> + if (!display->digits) { > >> + fwnode_handle_put(digits_node); > >Use RAII instead, we have defined __free() method for this. > >> + return -ENOMEM; >> + } >> + >> + i = 0; >> + fwnode_for_each_child_node(digits_node, child) { > >Ditto. Use _scoped variant. > Well received. >> + digit = &display->digits[i]; > >> + ret = fwnode_property_read_u32(child, "reg", >> + reg); > >One line. > Well received. >> + if (ret < 0) { > >Can it be positive? Here and everywhere else, if there is no positive return, >use 'if (ret)'. > I'll change error checks to if (ret) where functions only return 0 on success or negative on error. >> + fwnode_handle_put(child); >> + fwnode_handle_put(digits_node); >> + return ret; >> + } >> + >> + ret = fwnode_property_read_u32_array(child, >> + "segments", >> + segments, >> + TM16XX_DIGIT_SEGMENTS * 2); >> + if (ret < 0) { >> + fwnode_handle_put(child); >> + fwnode_handle_put(digits_node); >> + return ret; >> + } >> + >> + for (j = 0; j < TM16XX_DIGIT_SEGMENTS; ++j) { >> + digit->segments[j].grid = segments[2 * j]; >> + digit->segments[j].segment = segments[2 * j + 1]; > >> + max_grid = umax(max_grid, > >Firstly, the variables made signed and then specifically force them to be >unsigned in the macro. Weird. Can we make them to be a proper type and use max()? > Will change to unsigned types per above. >> + digit->segments[j].grid); > >One line > Well received. >> + max_segment = umax(max_segment, >> + digit->segments[j].segment); > >As per above comments. > Will change to unsigned types per above. >> + } >> + digit->value = 0; >> + i++; >> + } >> + >> + fwnode_handle_put(digits_node); >> + } >> + } >> + >> + /* parse leds */ >> + leds_node = device_get_named_child_node(dev, "leds"); >> + if (leds_node) { >> + display->num_leds = 0; >> + fwnode_for_each_child_node(leds_node, child) >> + display->num_leds++; > >As per above. > I'll use device_get_child_node_count() instead of manual counting loops. >> + dev_dbg(dev, "Number of LEDs: %u\n", display->num_leds); >> + >> + if (display->num_leds) { >> + display->leds = devm_kcalloc(dev, display->num_leds, >> + sizeof(*display->leds), >> + GFP_KERNEL); >> + if (!display->leds) { >> + fwnode_handle_put(leds_node); >> + return -ENOMEM; >> + } >> + >> + i = 0; >> + fwnode_for_each_child_node(leds_node, child) { >> + led = &display->leds[i]; > >> + ret = fwnode_property_read_u32_array(child, >> + "reg", reg, >> + 2); > >Make it one line. > Well received. >> + if (ret < 0) { >> + fwnode_handle_put(child); >> + fwnode_handle_put(leds_node); >> + return ret; >> + } >> + >> + led->grid = reg[0]; >> + led->segment = reg[1]; >> + max_grid = umax(max_grid, led->grid); >> + max_segment = umax(max_segment, led->segment); >> + i++; >> + } >> + } >> + >> + fwnode_handle_put(leds_node); >> + } >> + >> + if (max_grid >= display->controller->max_grids) { >> + dev_err(dev, "grid %u exceeds controller max_grids %u\n", >> + max_grid, display->controller->max_grids); >> + return -EINVAL; >> + } >> + >> + if (max_segment >= display->controller->max_segments) { >> + dev_err(dev, "segment %u exceeds controller max_segments %u\n", >> + max_segment, display->controller->max_segments); >> + return -EINVAL; >> + } >> + >> + display->num_grids = max_grid + 1; >> + display->num_segments = max_segment + 1; > >> + dev_dbg(dev, "Number of grids: %u\n", display->num_grids); >> + dev_dbg(dev, "Number of segments: %u\n", display->num_segments); > >I didn't get this. You mean that they are not strictly 7-segment ones? > The terminology is confusing - "segment" is used both for 7-segment digits (which are indeed 7-segment) and for controller matrix coordinates (grid,segment) from datasheets. Controllers support varying numbers of segments For individual LED icons, not necessarily related to 7-segment displays. I'll add a comment to clarify this distinction. >> + >> + return 0; >> +} > >... > >> +int tm16xx_probe(struct tm16xx_display *display) >> +{ >> + struct device *dev = display->dev; >> + struct led_classdev *main = &display->main_led; >> + struct fwnode_handle *leds_node, *child; >> + unsigned int nbits; >> + int ret, i; > >Why is i signed? > Will change to unsigned types per above. >> + dev_dbg(dev, "Probing device\n"); >> + ret = tm16xx_parse_dt(dev, display); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "Failed to parse device tree\n"); >> + >> + nbits = tm16xx_led_nbits(display); >> + display->state = devm_bitmap_zalloc(dev, nbits, GFP_KERNEL); >> + if (!display->state) >> + return -ENOMEM; > >> + mutex_init(&display->lock); > >devm_mutex_init() > Well received. >> + INIT_WORK(&display->flush_init, tm16xx_display_flush_init); >> + INIT_WORK(&display->flush_display, tm16xx_display_flush_data); > >> + /* Initialize main LED properties */ >> + if (dev->of_node) >> + main->name = dev->of_node->name; >> + if (!main->name) >> + main->name = "display"; >> + device_property_read_string(dev, "label", &main->name); > >My gosh. This is done in the LED core if we even need this... > This relates to the LED subsystem integration question. If my design approach is acceptable, I'll review the LED core implementation to avoid duplicating this logic if possible. >> + main->max_brightness = display->controller->max_brightness; >> + device_property_read_u32(dev, "max-brightness", &main->max_brightness); >> + main->max_brightness = umin(main->max_brightness, >> + display->controller->max_brightness); >> + >> + main->brightness = main->max_brightness; >> + device_property_read_u32(dev, "default-brightness", &main->brightness); >> + main->brightness = umin(main->brightness, main->max_brightness); >> + >> + main->brightness_set = tm16xx_brightness_set; >> + main->groups = tm16xx_main_led_groups; >> + main->flags = LED_RETAIN_AT_SHUTDOWN | LED_CORE_SUSPENDRESUME; >> + >> + ret = led_classdev_register(dev, main); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "Failed to register main LED\n"); > >> + i = 0; >> + leds_node = device_get_named_child_node(dev, "leds"); >> + fwnode_for_each_child_node(leds_node, child) { >> + struct tm16xx_led *led = &display->leds[i]; >> + struct led_init_data led_init = { >> + .fwnode = child, >> + .devicename = dev_name(main->dev), >> + .devname_mandatory = true, >> + .default_label = "led", >> + }; >> + led->cdev.max_brightness = 1; >> + led->cdev.brightness_set = tm16xx_led_set; >> + led->cdev.flags = LED_RETAIN_AT_SHUTDOWN | >> + LED_CORE_SUSPENDRESUME; >> + >> + ret = led_classdev_register_ext(dev, &led->cdev, &led_init); >> + if (ret < 0) { >> + fwnode_handle_put(child); >> + dev_err_probe(dev, ret, "Failed to register LED %s\n", >> + led->cdev.name); >> + goto unregister_leds; >> + } >> + >> + i++; >> + } >> + >> + ret = tm16xx_display_init(display); >> + if (ret < 0) { >> + dev_err_probe(dev, ret, "Failed to initialize display\n"); >> + goto unregister_leds; >> + } >> + >> + return 0; >> + >> +unregister_leds: >> + while (i--) >> + led_classdev_unregister(&display->leds[i].cdev); >> + >> + led_classdev_unregister(main); >> + return ret; >> +} > >... > >> +void tm16xx_remove(struct tm16xx_display *display) >> +{ >> + unsigned int nbits = tm16xx_led_nbits(display); >> + struct tm16xx_led *led; > >> + dev_dbg(display->dev, "Removing display\n"); > >Unneeded noise. > Well received. >> + /* >> + * Unregister LEDs first to immediately stop trigger activity. >> + * This prevents LED triggers from attempting to access hardware >> + * after it's been disconnected or driver unloaded. >> + */ >> + for (int i = 0; i < display->num_leds; i++) { > >Why iterator is signed? > Will change to unsigned types per above. >> + led = &display->leds[i]; >> + led_classdev_unregister(&led->cdev); >> + } >> + led_classdev_unregister(&display->main_led); >> + >> + /* Clear display state */ >> + bitmap_zero(display->state, nbits); >> + schedule_work(&display->flush_display); >> + flush_work(&display->flush_display); >> + >> + /* Turn off display */ >> + display->main_led.brightness = LED_OFF; >> + schedule_work(&display->flush_init); >> + flush_work(&display->flush_init); > >> + dev_info(display->dev, "Display turned off\n"); > >Unneeded noise. > Well received. >> +} > Thank you for the detailed review. The main question remains about the LED subsystem integration approach. I'd appreciate your guidance on the best design direction.
Le 25 août 2025 13 h 48 min 45 s HAE, "Jean-François Lessard" <jefflessard3@gmail.com> a écrit : >Le 25 août 2025 11 h 14 min 21 s HAE, Andy Shevchenko <andriy.shevchenko@intel.com> a écrit : >>On Sun, Aug 24, 2025 at 11:32:29PM -0400, Jean-François Lessard wrote: ... >>> + fwnode_for_each_child_node(digits_node, child) >>> + display->num_digits++; >> >>Don't we have a _count API for this? >> > >I'll use device_get_child_node_count() instead of manual counting loops. > >>> + dev_dbg(dev, "Number of digits: %u\n", display->num_digits); >>> + >>> + if (display->num_digits) { >>> + display->digits = devm_kcalloc(dev, display->num_digits, >>> + sizeof(*display->digits), >>> + GFP_KERNEL); >>> + if (!display->digits) { >> >>> + fwnode_handle_put(digits_node); >> >>Use RAII instead, we have defined __free() method for this. >> >>> + return -ENOMEM; >>> + } >>> + >>> + i = 0; >>> + fwnode_for_each_child_node(digits_node, child) { >> >>Ditto. Use _scoped variant. >> > >Well received. > After further investigation, _scoped variant exists for device_for_each_child_node_scoped() but not for fwnode_for_each_child_node(). I suggest to include an additional patch in next submission to add to include/linux/property.h: #define fwnode_for_each_child_node_scoped(fwnode, child) \ for (struct fwnode_handle *child __free(fwnode_handle) = \ fwnode_get_next_child_node(fwnode, NULL); \ child; child = fwnode_get_next_child_node(fwnode, child)) #define fwnode_for_each_named_child_node_scoped(fwnode, child, name) \ fwnode_for_each_child_node_scoped(fwnode, child) \ for_each_if(fwnode_name_eq(child, name)) #define fwnode_for_each_available_child_node_scoped(fwnode, child) \ for (struct fwnode_handle *child __free(fwnode_handle) = \ fwnode_get_next_available_child_node(fwnode, NULL); \ child; child = fwnode_get_next_available_child_node(fwnode, child)) ...
On Wed, Aug 27, 2025 at 02:37:47PM -0400, Jean-François Lessard wrote: > Le 25 août 2025 13 h 48 min 45 s HAE, "Jean-François Lessard" <jefflessard3@gmail.com> a écrit : > >Le 25 août 2025 11 h 14 min 21 s HAE, Andy Shevchenko <andriy.shevchenko@intel.com> a écrit : > >>On Sun, Aug 24, 2025 at 11:32:29PM -0400, Jean-François Lessard wrote: ... > >>> + fwnode_for_each_child_node(digits_node, child) > >>> + display->num_digits++; > >> > >>Don't we have a _count API for this? > >> > > > >I'll use device_get_child_node_count() instead of manual counting loops. > > > >>> + dev_dbg(dev, "Number of digits: %u\n", display->num_digits); > >>> + > >>> + if (display->num_digits) { > >>> + display->digits = devm_kcalloc(dev, display->num_digits, > >>> + sizeof(*display->digits), > >>> + GFP_KERNEL); > >>> + if (!display->digits) { > >> > >>> + fwnode_handle_put(digits_node); > >> > >>Use RAII instead, we have defined __free() method for this. > >> > >>> + return -ENOMEM; > >>> + } > >>> + > >>> + i = 0; > >>> + fwnode_for_each_child_node(digits_node, child) { > >> > >>Ditto. Use _scoped variant. > > > >Well received. > > After further investigation, _scoped variant exists for > device_for_each_child_node_scoped() but not for fwnode_for_each_child_node(). > > I suggest to include an additional patch in next submission to add to > include/linux/property.h: > > #define fwnode_for_each_child_node_scoped(fwnode, child) \ > for (struct fwnode_handle *child __free(fwnode_handle) = \ > fwnode_get_next_child_node(fwnode, NULL); \ > child; child = fwnode_get_next_child_node(fwnode, child)) > > #define fwnode_for_each_named_child_node_scoped(fwnode, child, name) \ > fwnode_for_each_child_node_scoped(fwnode, child) \ > for_each_if(fwnode_name_eq(child, name)) > > #define fwnode_for_each_available_child_node_scoped(fwnode, child) \ > for (struct fwnode_handle *child __free(fwnode_handle) = \ > fwnode_get_next_available_child_node(fwnode, NULL); \ > child; child = fwnode_get_next_available_child_node(fwnode, child)) LGTM! -- With Best Regards, Andy Shevchenko
On Mon, Aug 25, 2025 at 01:48:45PM -0400, Jean-François Lessard wrote: > Le 25 août 2025 11 h 14 min 21 s HAE, Andy Shevchenko <andriy.shevchenko@intel.com> a écrit : > >On Sun, Aug 24, 2025 at 11:32:29PM -0400, Jean-François Lessard wrote: ... > >> +Date: August 2025 > >> +KernelVersion: 6.17 > > > >The Date should be approximate date of the kernel release (alternatively, -rc1 > >of that). The version is estimated version where ABI can be found first. > >Both of these need to be changed. > > Given that 6.17-rc3 was just released, should I target 6.18 for the kernel > version and use a March 2025 date for the estimated release timeframe? 6.18 The date is not in the past, obviously. You can consult with this site: https://hansen.beer/~dave/phb/ ... > >So, the driver is under auxdisplay, but at the same time it completely relies > >on LED subsystem... What's going on here? > > The design integrates with the LED subsystem for two main reasons: > > 1. Brightness control: > The entire display brightness is controlled at the display level > (individual LED icons can only be on/off via their brightness attributes). > The LED subsystem provides established mechanisms for this. > > 2. Coherent sysfs interface: > This provides consistent /sys/class/leds/display for display-level controls > and /sys/class/leds/display::{function} for individual icons. > > I'm seeking your guidance on the best design for the auxdisplay subsystem. > > >Btw, have you seen > >https://lore.kernel.org/linux-leds/20231011190017.1230898-1-wse@tuxedocomputers.com/ > >? And if so, what're your takeaways? (Yes, I know that's about different HW) > > I've read the thread but I'm not clear on the specific point you're making. > Could you clarify what aspect I should focus on? If you have a LED matrix, perhaps we can consider different approaches as well. (It's all about the current HW, is it a 7-segment or arbitrary display, if the former, that discussion is unrelated) > (Though, my personal opinion is that using auxdisplay for keyboard LEDs > doesn't really make sense. I think it would be better to properly implement > it the required mechanism into input subsystem, with maybe some > integration with the leds subsystem. Just a quick opinion, I do not > master all aspects of this question at all.) ... > >> + * Copyright (C) 2024 Jean-François Lessard > > > >My calendar shows something different. > > The original code was developed in 2024, though it's being submitted in 2025. But haven't you changed it in 2025? ... > >> +#include <linux/bitmap.h> > > > >Is this used? > > Yes, display->state is a bitmap. I'll move this include to tm16xx_core.c > since it's not used in the header itself. Yes, that's what I meant "used by this header file". ... > >> + union { > >> + struct i2c_client *i2c; > >> + struct spi_device *spi; > >> + } client; > > > >Why? Just drop it. struct device *dev is enough and I can't see the need > >in this at all. > > I'll remove this union and use container_of(dev, struct i2c_client, dev) > or container_of(dev, struct spi_device, dev) where the specific client type > is needed. This is in correlation with the regmap proposal. ... > >> +static ssize_t tm16xx_map_seg7_show(struct device *dev, > >> + struct device_attribute *attr, char *buf) > >> +{ > >> + memcpy(buf, &map_seg7, sizeof(map_seg7)); > >> + return sizeof(map_seg7); > >> +} > > > >Can we use LINEDISP library? > > I considered this but have two concerns: > > 1. It creates attributes under a virtual "linedisp.{n}" device, > which conflicts with the coherent LED sysfs design It creates the specific attributes for the 7-segment HW, So look at it from this angle. We have well established library and we expect 7-seg drivers will use it to make sure that user space may be written in uniform way. > 2. Messages scroll indefinitely. There should be control for single-pass scrolling If we miss that, add it to linedisp. I wouldn't mind, actually I will be in favour of the development of that library. > I'm willing to contribute improvements to line-display if needed, > but this depends on resolving the main LED design question above. ... > >> + display->num_digits = 0; > >> + fwnode_for_each_child_node(digits_node, child) > >> + display->num_digits++; > > > >Don't we have a _count API for this? > > I'll use device_get_child_node_count() instead of manual counting loops. fwnode_get_child_node_count() I assume you meant. ... > >> + dev_dbg(dev, "Number of grids: %u\n", display->num_grids); > >> + dev_dbg(dev, "Number of segments: %u\n", display->num_segments); > > > >I didn't get this. You mean that they are not strictly 7-segment ones? > > The terminology is confusing - "segment" is used both for 7-segment digits > (which are indeed 7-segment) and for controller matrix coordinates > (grid,segment) from datasheets. Controllers support varying numbers of segments > For individual LED icons, not necessarily related to 7-segment displays. > I'll add a comment to clarify this distinction. Hmm... Maybe try to rename these 'segments' to something else, like 'hwseg' (find a better name). ... > >> + /* Initialize main LED properties */ > >> + if (dev->of_node) > >> + main->name = dev->of_node->name; > >> + if (!main->name) > >> + main->name = "display"; > >> + device_property_read_string(dev, "label", &main->name); > > > >My gosh. This is done in the LED core if we even need this... > > This relates to the LED subsystem integration question. If my design approach > is acceptable, I'll review the LED core implementation to avoid duplicating > this logic if possible. I think if you integrate LED for special LED icons and linedisp for 7-segment into a single driver, it's fine. I just can't speak about LED icons case. The 7-seg LGTM (assuming linedisp and other existing APIs/ABIs to use, if required). -- With Best Regards, Andy Shevchenko
Le 26 août 2025 11 h 22 min 56 s HAE, Andy Shevchenko <andriy.shevchenko@intel.com> a écrit : >On Mon, Aug 25, 2025 at 01:48:45PM -0400, Jean-François Lessard wrote: >> Le 25 août 2025 11 h 14 min 21 s HAE, Andy Shevchenko <andriy.shevchenko@intel.com> a écrit : >> >On Sun, Aug 24, 2025 at 11:32:29PM -0400, Jean-François Lessard wrote: > >... > >> >> +Date: August 2025 >> >> +KernelVersion: 6.17 >> > >> >The Date should be approximate date of the kernel release (alternatively, -rc1 >> >of that). The version is estimated version where ABI can be found first. > >> >Both of these need to be changed. >> >> Given that 6.17-rc3 was just released, should I target 6.18 for the kernel >> version and use a March 2025 date for the estimated release timeframe? > >6.18 >The date is not in the past, obviously. You can consult with this site: >https://hansen.beer/~dave/phb/ > Thanks for the correction and link. I'll update to 6.18 with appropriate future date. >... > >> >So, the driver is under auxdisplay, but at the same time it completely relies >> >on LED subsystem... What's going on here? >> >> The design integrates with the LED subsystem for two main reasons: >> >> 1. Brightness control: >> The entire display brightness is controlled at the display level >> (individual LED icons can only be on/off via their brightness attributes). >> The LED subsystem provides established mechanisms for this. >> >> 2. Coherent sysfs interface: >> This provides consistent /sys/class/leds/display for display-level controls >> and /sys/class/leds/display::{function} for individual icons. >> >> I'm seeking your guidance on the best design for the auxdisplay subsystem. >> >> >Btw, have you seen >> >https://lore.kernel.org/linux-leds/20231011190017.1230898-1-wse@tuxedocomputers.com/ >> >? And if so, what're your takeaways? (Yes, I know that's about different HW) >> >> I've read the thread but I'm not clear on the specific point you're making. >> Could you clarify what aspect I should focus on? > >If you have a LED matrix, perhaps we can consider different approaches as well. >(It's all about the current HW, is it a 7-segment or arbitrary display, if the > former, that discussion is unrelated) > TM16xx devices are primarily 7-segment displays with additional LED icon and key scanning capabilities. >> (Though, my personal opinion is that using auxdisplay for keyboard LEDs >> doesn't really make sense. I think it would be better to properly implement >> it the required mechanism into input subsystem, with maybe some >> integration with the leds subsystem. Just a quick opinion, I do not >> master all aspects of this question at all.) > >... > >> >> + * Copyright (C) 2024 Jean-François Lessard >> > >> >My calendar shows something different. >> >> The original code was developed in 2024, though it's being submitted in 2025. > >But haven't you changed it in 2025? > You're right, I'll update the copyright to 2025. >... > >> >> +#include <linux/bitmap.h> >> > >> >Is this used? >> >> Yes, display->state is a bitmap. I'll move this include to tm16xx_core.c >> since it's not used in the header itself. > >Yes, that's what I meant "used by this header file". > I'll move bitmap.h to the source file. >... > >> >> + union { >> >> + struct i2c_client *i2c; >> >> + struct spi_device *spi; >> >> + } client; >> > >> >Why? Just drop it. struct device *dev is enough and I can't see the need >> >in this at all. >> >> I'll remove this union and use container_of(dev, struct i2c_client, dev) >> or container_of(dev, struct spi_device, dev) where the specific client type >> is needed. > >This is in correlation with the regmap proposal. > I'll address the regmap evaluation in the cover letter of the next submission as you requested. The union removal will be addressed too. >... > >> >> +static ssize_t tm16xx_map_seg7_show(struct device *dev, >> >> + struct device_attribute *attr, char *buf) >> >> +{ >> >> + memcpy(buf, &map_seg7, sizeof(map_seg7)); >> >> + return sizeof(map_seg7); >> >> +} >> > >> >Can we use LINEDISP library? >> >> I considered this but have two concerns: >> >> 1. It creates attributes under a virtual "linedisp.{n}" device, >> which conflicts with the coherent LED sysfs design > >It creates the specific attributes for the 7-segment HW, So look at it >from this angle. We have well established library and we expect 7-seg >drivers will use it to make sure that user space may be written in uniform >way. > To provide a coherent user interface, I'd like to propose extending linedisp to optionally attach attributes to an existing device rather than always creating virtual devices. This would create a unified /sys/class/leds/{label}/ interface for content, brightness, and icons while maintaining linedisp's established APIs. If device attachment isn't feasible, could linedisp at minimum accept a device name parameter to create /sys/virtual/linedisp-{label} instead of generic linedisp.{n}? I'm happy to implement these linedisp enhancements as part of the TM16xx integration if you find the approach acceptable. >> 2. Messages scroll indefinitely. There should be control for single-pass scrolling > >If we miss that, add it to linedisp. I wouldn't mind, actually I will be in >favour of the development of that library. > I'll submit patches to extend linedisp with missing functionality (like single-pass scrolling control) as part of this integration. >> I'm willing to contribute improvements to line-display if needed, >> but this depends on resolving the main LED design question above. > >... > >> >> + display->num_digits = 0; >> >> + fwnode_for_each_child_node(digits_node, child) >> >> + display->num_digits++; >> > >> >Don't we have a _count API for this? >> >> I'll use device_get_child_node_count() instead of manual counting loops. > >fwnode_get_child_node_count() I assume you meant. > Correct, my oversight. I'll use the fwnode variant. >... > >> >> + dev_dbg(dev, "Number of grids: %u\n", display->num_grids); >> >> + dev_dbg(dev, "Number of segments: %u\n", display->num_segments); >> > >> >I didn't get this. You mean that they are not strictly 7-segment ones? >> >> The terminology is confusing - "segment" is used both for 7-segment digits >> (which are indeed 7-segment) and for controller matrix coordinates >> (grid,segment) from datasheets. Controllers support varying numbers of segments >> For individual LED icons, not necessarily related to 7-segment displays. >> I'll add a comment to clarify this distinction. > >Hmm... Maybe try to rename these 'segments' to something else, like 'hwseg' >(find a better name). > Good point. I'll use a different name to distinguish from 7-segment terminology. >... > >> >> + /* Initialize main LED properties */ >> >> + if (dev->of_node) >> >> + main->name = dev->of_node->name; >> >> + if (!main->name) >> >> + main->name = "display"; >> >> + device_property_read_string(dev, "label", &main->name); >> > >> >My gosh. This is done in the LED core if we even need this... >> >> This relates to the LED subsystem integration question. If my design approach >> is acceptable, I'll review the LED core implementation to avoid duplicating >> this logic if possible. > >I think if you integrate LED for special LED icons and linedisp for 7-segment >into a single driver, it's fine. I just can't speak about LED icons case. >The 7-seg LGTM (assuming linedisp and other existing APIs/ABIs to use, if required). > I think linedisp is the way to go if we can provide a consistent user space API.
© 2016 - 2025 Red Hat, Inc.