3202 lines
95 KiB
Diff
3202 lines
95 KiB
Diff
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sun, 31 Aug 2025 00:23:51 -0400
|
|
Subject: docs: ABI: auxdisplay: document linedisp library sysfs attributes
|
|
|
|
Add ABI documentation for sysfs attributes provided by the line-display
|
|
auxdisplay library module. These attributes enable text message display and
|
|
configuration on character-based auxdisplay devices.
|
|
|
|
Documents previously undocumented attributes:
|
|
- message, scroll_step_ms (introduced in v5.16)
|
|
- map_seg7, map_seg14 (introduced in v6.9)
|
|
|
|
The line-display library is used by multiple auxdisplay drivers and
|
|
can expose these attributes either on linedisp.N child devices or
|
|
directly on parent auxdisplay devices.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
Documentation/ABI/testing/sysfs-auxdisplay-linedisp | 79 ++++++++++
|
|
1 file changed, 79 insertions(+)
|
|
|
|
diff --git a/Documentation/ABI/testing/sysfs-auxdisplay-linedisp b/Documentation/ABI/testing/sysfs-auxdisplay-linedisp
|
|
new file mode 100644
|
|
index 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/Documentation/ABI/testing/sysfs-auxdisplay-linedisp
|
|
@@ -0,0 +1,79 @@
|
|
+What: /sys/.../message
|
|
+Date: October 2021
|
|
+KernelVersion: 5.16
|
|
+Description:
|
|
+ Controls the text message displayed on character line displays.
|
|
+
|
|
+ Reading returns the current message with a trailing newline.
|
|
+ Writing updates the displayed message. Messages longer than the
|
|
+ display width will automatically scroll. Trailing newlines in
|
|
+ input are automatically trimmed.
|
|
+
|
|
+ Writing an empty string clears the display.
|
|
+
|
|
+ Example:
|
|
+ echo "Hello World" > message
|
|
+ cat message # Returns "Hello World\n"
|
|
+
|
|
+What: /sys/.../scroll_step_ms
|
|
+Date: October 2021
|
|
+KernelVersion: 5.16
|
|
+Description:
|
|
+ Controls the scrolling speed for messages longer than the display
|
|
+ width, specified in milliseconds per scroll step.
|
|
+
|
|
+ Setting to 0 disables scrolling. Default is 500ms.
|
|
+
|
|
+ Example:
|
|
+ echo "250" > scroll_step_ms # 4Hz scrolling
|
|
+ cat scroll_step_ms # Returns "250\n"
|
|
+
|
|
+What: /sys/.../map_seg7
|
|
+Date: January 2024
|
|
+KernelVersion: 6.9
|
|
+Description:
|
|
+ Read/write binary blob representing the ASCII-to-7-segment
|
|
+ display conversion table used by the linedisp driver, as defined
|
|
+ by struct seg7_conversion_map in <linux/map_to_7segment.h>.
|
|
+
|
|
+ Only visible on displays with 7-segment capability.
|
|
+
|
|
+ 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 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
|
|
+
|
|
+What: /sys/.../map_seg14
|
|
+Date: January 2024
|
|
+KernelVersion: 6.9
|
|
+Description:
|
|
+ Read/write binary blob representing the ASCII-to-14-segment
|
|
+ display conversion table used by the linedisp driver, as defined
|
|
+ by struct seg14_conversion_map in <linux/map_to_14segment.h>.
|
|
+
|
|
+ Only visible on displays with 14-segment capability.
|
|
+
|
|
+ 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 by 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_14segment.h
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 30 Aug 2025 20:31:38 -0400
|
|
Subject: auxdisplay: linedisp: encapsulate container_of usage within
|
|
to_linedisp
|
|
|
|
Replace direct container_of() calls with a to_linedisp() helper function
|
|
throughout the line-display auxdisplay library module. This abstraction
|
|
prepares for upcoming dual-mode support where linedisp context retrieval
|
|
will need to handle both dedicated child devices and attached parent
|
|
auxdisplay devices.
|
|
|
|
No functional changes in this patch.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/line-display.c | 21 ++++++----
|
|
1 file changed, 13 insertions(+), 8 deletions(-)
|
|
|
|
diff --git a/drivers/auxdisplay/line-display.c b/drivers/auxdisplay/line-display.c
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/line-display.c
|
|
+++ b/drivers/auxdisplay/line-display.c
|
|
@@ -31,6 +31,11 @@
|
|
|
|
#define DEFAULT_SCROLL_RATE (HZ / 2)
|
|
|
|
+static struct linedisp *to_linedisp(struct device *dev)
|
|
+{
|
|
+ return container_of(dev, struct linedisp, dev);
|
|
+}
|
|
+
|
|
/**
|
|
* linedisp_scroll() - scroll the display by a character
|
|
* @t: really a pointer to the private data structure
|
|
@@ -133,7 +138,7 @@ static int linedisp_display(struct linedisp *linedisp, const char *msg,
|
|
static ssize_t message_show(struct device *dev, struct device_attribute *attr,
|
|
char *buf)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
|
|
return sysfs_emit(buf, "%s\n", linedisp->message);
|
|
}
|
|
@@ -152,7 +157,7 @@ static ssize_t message_show(struct device *dev, struct device_attribute *attr,
|
|
static ssize_t message_store(struct device *dev, struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
int err;
|
|
|
|
err = linedisp_display(linedisp, buf, count);
|
|
@@ -164,7 +169,7 @@ static DEVICE_ATTR_RW(message);
|
|
static ssize_t scroll_step_ms_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
|
|
return sysfs_emit(buf, "%u\n", jiffies_to_msecs(linedisp->scroll_rate));
|
|
}
|
|
@@ -173,7 +178,7 @@ static ssize_t scroll_step_ms_store(struct device *dev,
|
|
struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
unsigned int ms;
|
|
int err;
|
|
|
|
@@ -195,7 +200,7 @@ static DEVICE_ATTR_RW(scroll_step_ms);
|
|
|
|
static ssize_t map_seg_show(struct device *dev, struct device_attribute *attr, char *buf)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
struct linedisp_map *map = linedisp->map;
|
|
|
|
memcpy(buf, &map->map, map->size);
|
|
@@ -205,7 +210,7 @@ static ssize_t map_seg_show(struct device *dev, struct device_attribute *attr, c
|
|
static ssize_t map_seg_store(struct device *dev, struct device_attribute *attr,
|
|
const char *buf, size_t count)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
struct linedisp_map *map = linedisp->map;
|
|
|
|
if (count != map->size)
|
|
@@ -232,7 +237,7 @@ static struct attribute *linedisp_attrs[] = {
|
|
static umode_t linedisp_attr_is_visible(struct kobject *kobj, struct attribute *attr, int n)
|
|
{
|
|
struct device *dev = kobj_to_dev(kobj);
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
struct linedisp_map *map = linedisp->map;
|
|
umode_t mode = attr->mode;
|
|
|
|
@@ -263,7 +268,7 @@ static DEFINE_IDA(linedisp_id);
|
|
|
|
static void linedisp_release(struct device *dev)
|
|
{
|
|
- struct linedisp *linedisp = container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
|
|
kfree(linedisp->map);
|
|
kfree(linedisp->message);
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sun, 31 Aug 2025 13:53:01 -0400
|
|
Subject: auxdisplay: linedisp: display static message when length <= display
|
|
size
|
|
|
|
Currently, when a message shorter than the display size is written, the
|
|
content wraps around (e.g., "123" on a 4-digit display shows "1231")
|
|
without scrolling, which is confusing and unintuitive.
|
|
|
|
Change behavior to display short messages statically with space padding
|
|
(e.g. "123 ") while only scrolling messages longer than the display width.
|
|
This provides more natural behavior that aligns with user expectations
|
|
and current linedisp_display() kernel-doc.
|
|
|
|
The scroll logic is also consolidated into a helper function for clarity.
|
|
|
|
No API changes are introduced.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/line-display.c | 30 +++++++---
|
|
1 file changed, 21 insertions(+), 9 deletions(-)
|
|
|
|
diff --git a/drivers/auxdisplay/line-display.c b/drivers/auxdisplay/line-display.c
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/line-display.c
|
|
+++ b/drivers/auxdisplay/line-display.c
|
|
@@ -36,6 +36,11 @@ static struct linedisp *to_linedisp(struct device *dev)
|
|
return container_of(dev, struct linedisp, dev);
|
|
}
|
|
|
|
+static inline bool should_scroll(struct linedisp *linedisp)
|
|
+{
|
|
+ return linedisp->message_len > linedisp->num_chars && linedisp->scroll_rate;
|
|
+}
|
|
+
|
|
/**
|
|
* linedisp_scroll() - scroll the display by a character
|
|
* @t: really a pointer to the private data structure
|
|
@@ -67,8 +72,7 @@ static void linedisp_scroll(struct timer_list *t)
|
|
linedisp->scroll_pos %= linedisp->message_len;
|
|
|
|
/* rearm the timer */
|
|
- if (linedisp->message_len > num_chars && linedisp->scroll_rate)
|
|
- mod_timer(&linedisp->timer, jiffies + linedisp->scroll_rate);
|
|
+ mod_timer(&linedisp->timer, jiffies + linedisp->scroll_rate);
|
|
}
|
|
|
|
/**
|
|
@@ -118,8 +122,16 @@ static int linedisp_display(struct linedisp *linedisp, const char *msg,
|
|
linedisp->message_len = count;
|
|
linedisp->scroll_pos = 0;
|
|
|
|
- /* update the display */
|
|
- linedisp_scroll(&linedisp->timer);
|
|
+ if (should_scroll(linedisp)) {
|
|
+ /* display scrolling message */
|
|
+ linedisp_scroll(&linedisp->timer);
|
|
+ } else {
|
|
+ /* display static message */
|
|
+ memset(linedisp->buf, ' ', linedisp->num_chars);
|
|
+ memcpy(linedisp->buf, linedisp->message,
|
|
+ umin(linedisp->num_chars, linedisp->message_len));
|
|
+ linedisp->ops->update(linedisp);
|
|
+ }
|
|
|
|
return 0;
|
|
}
|
|
@@ -186,12 +198,12 @@ static ssize_t scroll_step_ms_store(struct device *dev,
|
|
if (err)
|
|
return err;
|
|
|
|
+ timer_delete_sync(&linedisp->timer);
|
|
+
|
|
linedisp->scroll_rate = msecs_to_jiffies(ms);
|
|
- if (linedisp->message && linedisp->message_len > linedisp->num_chars) {
|
|
- timer_delete_sync(&linedisp->timer);
|
|
- if (linedisp->scroll_rate)
|
|
- linedisp_scroll(&linedisp->timer);
|
|
- }
|
|
+
|
|
+ if (should_scroll(linedisp))
|
|
+ linedisp_scroll(&linedisp->timer);
|
|
|
|
return count;
|
|
}
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 30 Aug 2025 20:35:34 -0400
|
|
Subject: auxdisplay: linedisp: add num_chars sysfs attribute
|
|
|
|
Add a read-only 'num_chars' sysfs attribute to report display digit count.
|
|
|
|
The num_chars attribute provides essential capability information to
|
|
userspace applications that need to know display dimensions before writing
|
|
messages, complementing the existing message and scroll controls.
|
|
|
|
No functional changes to existing behavior.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
Documentation/ABI/testing/sysfs-auxdisplay-linedisp | 11 ++++++++++
|
|
drivers/auxdisplay/line-display.c | 11 ++++++++++
|
|
2 files changed, 22 insertions(+)
|
|
|
|
diff --git a/Documentation/ABI/testing/sysfs-auxdisplay-linedisp b/Documentation/ABI/testing/sysfs-auxdisplay-linedisp
|
|
index 111111111111..222222222222 100644
|
|
--- a/Documentation/ABI/testing/sysfs-auxdisplay-linedisp
|
|
+++ b/Documentation/ABI/testing/sysfs-auxdisplay-linedisp
|
|
@@ -15,6 +15,17 @@ Description:
|
|
echo "Hello World" > message
|
|
cat message # Returns "Hello World\n"
|
|
|
|
+What: /sys/.../num_chars
|
|
+Date: November 2025
|
|
+KernelVersion: 6.18
|
|
+Contact: Jean-François Lessard <jefflessard3@gmail.com>
|
|
+Description:
|
|
+ Read-only attribute showing the character width capacity of
|
|
+ the line display device. Messages longer than this will scroll.
|
|
+
|
|
+ Example:
|
|
+ cat num_chars # Returns "16\n" for 16-char display
|
|
+
|
|
What: /sys/.../scroll_step_ms
|
|
Date: October 2021
|
|
KernelVersion: 5.16
|
|
diff --git a/drivers/auxdisplay/line-display.c b/drivers/auxdisplay/line-display.c
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/line-display.c
|
|
+++ b/drivers/auxdisplay/line-display.c
|
|
@@ -178,6 +178,16 @@ static ssize_t message_store(struct device *dev, struct device_attribute *attr,
|
|
|
|
static DEVICE_ATTR_RW(message);
|
|
|
|
+static ssize_t num_chars_show(struct device *dev, struct device_attribute *attr,
|
|
+ char *buf)
|
|
+{
|
|
+ struct linedisp *linedisp = to_linedisp(dev);
|
|
+
|
|
+ return sysfs_emit(buf, "%u\n", linedisp->num_chars);
|
|
+}
|
|
+
|
|
+static DEVICE_ATTR_RO(num_chars);
|
|
+
|
|
static ssize_t scroll_step_ms_show(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
@@ -240,6 +250,7 @@ static DEVICE_ATTR(map_seg14, 0644, map_seg_show, map_seg_store);
|
|
|
|
static struct attribute *linedisp_attrs[] = {
|
|
&dev_attr_message.attr,
|
|
+ &dev_attr_num_chars.attr,
|
|
&dev_attr_scroll_step_ms.attr,
|
|
&dev_attr_map_seg7.attr,
|
|
&dev_attr_map_seg14.attr,
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 30 Aug 2025 20:41:39 -0400
|
|
Subject: auxdisplay: linedisp: support attribute attachment to auxdisplay
|
|
devices
|
|
|
|
Enable linedisp library integration into existing kernel devices (like LED
|
|
class) to provide a uniform 7-segment userspace API without creating
|
|
separate child devices, meeting the consistent interface while maintaining
|
|
coherent device hierarchies.
|
|
|
|
This allows uniform 7-segment API across all drivers while solving device
|
|
proliferation and fragmented userspace interfaces.
|
|
|
|
The sysfs attributes appear in one of the two locations depending on usage:
|
|
1. On linedisp.N child devices (legacy linedisp_register())
|
|
2. On the parent auxdisplay device (new linedisp_attach())
|
|
Functionality is identical in both modes.
|
|
|
|
Existing consumers of linedisp_register() are unaffected. The new API
|
|
enables drivers like TM16XX to integrate 7-segment display functionality
|
|
seamlessly within their LED class device hierarchy.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/line-display.c | 180 +++++++++-
|
|
drivers/auxdisplay/line-display.h | 4 +
|
|
2 files changed, 178 insertions(+), 6 deletions(-)
|
|
|
|
diff --git a/drivers/auxdisplay/line-display.c b/drivers/auxdisplay/line-display.c
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/line-display.c
|
|
+++ b/drivers/auxdisplay/line-display.c
|
|
@@ -6,20 +6,23 @@
|
|
* Author: Paul Burton <paul.burton@mips.com>
|
|
*
|
|
* Copyright (C) 2021 Glider bv
|
|
+ * Copyright (C) 2025 Jean-François Lessard
|
|
*/
|
|
|
|
#ifndef CONFIG_PANEL_BOOT_MESSAGE
|
|
#include <generated/utsrelease.h>
|
|
#endif
|
|
|
|
-#include <linux/container_of.h>
|
|
+#include <linux/cleanup.h>
|
|
#include <linux/device.h>
|
|
#include <linux/export.h>
|
|
#include <linux/idr.h>
|
|
#include <linux/jiffies.h>
|
|
#include <linux/kstrtox.h>
|
|
+#include <linux/list.h>
|
|
#include <linux/module.h>
|
|
#include <linux/slab.h>
|
|
+#include <linux/spinlock.h>
|
|
#include <linux/string.h>
|
|
#include <linux/sysfs.h>
|
|
#include <linux/timer.h>
|
|
@@ -31,9 +34,80 @@
|
|
|
|
#define DEFAULT_SCROLL_RATE (HZ / 2)
|
|
|
|
+/**
|
|
+ * struct linedisp_attachment - Holds the device to linedisp mapping
|
|
+ * @list: List entry for the linedisp_attachments list
|
|
+ * @device: Pointer to the device where linedisp attributes are added
|
|
+ * @linedisp: Pointer to the linedisp mapped to the device
|
|
+ * @direct: true for directly attached device using linedisp_attach(),
|
|
+ * false for child registered device using linedisp_register()
|
|
+ */
|
|
+struct linedisp_attachment {
|
|
+ struct list_head list;
|
|
+ struct device *device;
|
|
+ struct linedisp *linedisp;
|
|
+ bool direct;
|
|
+};
|
|
+
|
|
+static LIST_HEAD(linedisp_attachments);
|
|
+static DEFINE_SPINLOCK(linedisp_attachments_lock);
|
|
+
|
|
+static int create_attachment(struct device *dev, struct linedisp *linedisp, bool direct)
|
|
+{
|
|
+ struct linedisp_attachment *attachment;
|
|
+
|
|
+ attachment = kzalloc(sizeof(*attachment), GFP_KERNEL);
|
|
+ if (!attachment)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ attachment->device = dev;
|
|
+ attachment->linedisp = linedisp;
|
|
+ attachment->direct = direct;
|
|
+
|
|
+ guard(spinlock)(&linedisp_attachments_lock);
|
|
+ list_add(&attachment->list, &linedisp_attachments);
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static struct linedisp *delete_attachment(struct device *dev, bool direct)
|
|
+{
|
|
+ struct linedisp_attachment *attachment;
|
|
+ struct linedisp *linedisp;
|
|
+
|
|
+ guard(spinlock)(&linedisp_attachments_lock);
|
|
+
|
|
+ list_for_each_entry(attachment, &linedisp_attachments, list) {
|
|
+ if (attachment->device == dev &&
|
|
+ attachment->direct == direct)
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (list_entry_is_head(attachment, &linedisp_attachments, list))
|
|
+ return NULL;
|
|
+
|
|
+ linedisp = attachment->linedisp;
|
|
+ list_del(&attachment->list);
|
|
+ kfree(attachment);
|
|
+
|
|
+ return linedisp;
|
|
+}
|
|
+
|
|
static struct linedisp *to_linedisp(struct device *dev)
|
|
{
|
|
- return container_of(dev, struct linedisp, dev);
|
|
+ struct linedisp_attachment *attachment;
|
|
+
|
|
+ guard(spinlock)(&linedisp_attachments_lock);
|
|
+
|
|
+ list_for_each_entry(attachment, &linedisp_attachments, list) {
|
|
+ if (attachment->device == dev)
|
|
+ break;
|
|
+ }
|
|
+
|
|
+ if (list_entry_is_head(attachment, &linedisp_attachments, list))
|
|
+ return NULL;
|
|
+
|
|
+ return attachment->linedisp;
|
|
}
|
|
|
|
static inline bool should_scroll(struct linedisp *linedisp)
|
|
@@ -348,6 +422,90 @@ static int linedisp_init_map(struct linedisp *linedisp)
|
|
#define LINEDISP_INIT_TEXT "Linux " UTS_RELEASE " "
|
|
#endif
|
|
|
|
+/**
|
|
+ * linedisp_attach - attach a character line display
|
|
+ * @linedisp: pointer to character line display structure
|
|
+ * @dev: pointer of the device to attach to
|
|
+ * @num_chars: the number of characters that can be displayed
|
|
+ * @ops: character line display operations
|
|
+ *
|
|
+ * Directly attach the line-display sysfs attributes to the passed device.
|
|
+ * The caller is responsible for calling linedisp_detach() to release resources
|
|
+ * after use.
|
|
+ *
|
|
+ * Return: zero on success, else a negative error code.
|
|
+ */
|
|
+int linedisp_attach(struct linedisp *linedisp, struct device *dev,
|
|
+ unsigned int num_chars, const struct linedisp_ops *ops)
|
|
+{
|
|
+ int err;
|
|
+
|
|
+ memset(linedisp, 0, sizeof(*linedisp));
|
|
+ linedisp->ops = ops;
|
|
+ linedisp->num_chars = num_chars;
|
|
+ linedisp->scroll_rate = DEFAULT_SCROLL_RATE;
|
|
+
|
|
+ linedisp->buf = kzalloc(linedisp->num_chars, GFP_KERNEL);
|
|
+ if (!linedisp->buf)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ /* initialise a character mapping, if required */
|
|
+ err = linedisp_init_map(linedisp);
|
|
+ if (err)
|
|
+ goto out_free_buf;
|
|
+
|
|
+ /* initialise a timer for scrolling the message */
|
|
+ timer_setup(&linedisp->timer, linedisp_scroll, 0);
|
|
+
|
|
+ err = create_attachment(dev, linedisp, true);
|
|
+ if (err)
|
|
+ goto out_del_timer;
|
|
+
|
|
+ /* display a default message */
|
|
+ err = linedisp_display(linedisp, LINEDISP_INIT_TEXT, -1);
|
|
+ if (err)
|
|
+ goto out_del_attach;
|
|
+
|
|
+ /* add attribute groups to target device */
|
|
+ err = device_add_groups(dev, linedisp_groups);
|
|
+ if (err)
|
|
+ goto out_del_attach;
|
|
+
|
|
+ return 0;
|
|
+
|
|
+out_del_attach:
|
|
+ delete_attachment(dev, true);
|
|
+out_del_timer:
|
|
+ timer_delete_sync(&linedisp->timer);
|
|
+out_free_buf:
|
|
+ kfree(linedisp->buf);
|
|
+ return err;
|
|
+}
|
|
+EXPORT_SYMBOL_NS_GPL(linedisp_attach, "LINEDISP");
|
|
+
|
|
+/**
|
|
+ * linedisp_detach - detach a character line display
|
|
+ * @dev: pointer of the device to detach from, that was previously
|
|
+ * attached with linedisp_attach()
|
|
+ */
|
|
+void linedisp_detach(struct device *dev)
|
|
+{
|
|
+ struct linedisp *linedisp;
|
|
+
|
|
+ linedisp = delete_attachment(dev, true);
|
|
+ if (!linedisp)
|
|
+ return;
|
|
+
|
|
+ timer_delete_sync(&linedisp->timer);
|
|
+
|
|
+ device_remove_groups(dev, linedisp_groups);
|
|
+
|
|
+ kfree(linedisp->map);
|
|
+ kfree(linedisp->message);
|
|
+ kfree(linedisp->buf);
|
|
+}
|
|
+EXPORT_SYMBOL_NS_GPL(linedisp_detach, "LINEDISP");
|
|
+
|
|
/**
|
|
* linedisp_register - register a character line display
|
|
* @linedisp: pointer to character line display structure
|
|
@@ -355,6 +513,11 @@ static int linedisp_init_map(struct linedisp *linedisp)
|
|
* @num_chars: the number of characters that can be displayed
|
|
* @ops: character line display operations
|
|
*
|
|
+ * Register the line-display sysfs attributes to a new device named
|
|
+ * "linedisp.N" added to the passed parent device.
|
|
+ * The caller is responsible for calling linedisp_unregister() to release
|
|
+ * resources after use.
|
|
+ *
|
|
* Return: zero on success, else a negative error code.
|
|
*/
|
|
int linedisp_register(struct linedisp *linedisp, struct device *parent,
|
|
@@ -390,19 +553,23 @@ int linedisp_register(struct linedisp *linedisp, struct device *parent,
|
|
/* initialise a timer for scrolling the message */
|
|
timer_setup(&linedisp->timer, linedisp_scroll, 0);
|
|
|
|
- err = device_add(&linedisp->dev);
|
|
+ err = create_attachment(&linedisp->dev, linedisp, false);
|
|
if (err)
|
|
goto out_del_timer;
|
|
|
|
/* display a default message */
|
|
err = linedisp_display(linedisp, LINEDISP_INIT_TEXT, -1);
|
|
if (err)
|
|
- goto out_del_dev;
|
|
+ goto out_del_attach;
|
|
+
|
|
+ err = device_add(&linedisp->dev);
|
|
+ if (err)
|
|
+ goto out_del_attach;
|
|
|
|
return 0;
|
|
|
|
-out_del_dev:
|
|
- device_del(&linedisp->dev);
|
|
+out_del_attach:
|
|
+ delete_attachment(&linedisp->dev, false);
|
|
out_del_timer:
|
|
timer_delete_sync(&linedisp->timer);
|
|
out_put_device:
|
|
@@ -419,6 +586,7 @@ EXPORT_SYMBOL_NS_GPL(linedisp_register, "LINEDISP");
|
|
void linedisp_unregister(struct linedisp *linedisp)
|
|
{
|
|
device_del(&linedisp->dev);
|
|
+ delete_attachment(&linedisp->dev, false);
|
|
timer_delete_sync(&linedisp->timer);
|
|
put_device(&linedisp->dev);
|
|
}
|
|
diff --git a/drivers/auxdisplay/line-display.h b/drivers/auxdisplay/line-display.h
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/line-display.h
|
|
+++ b/drivers/auxdisplay/line-display.h
|
|
@@ -6,6 +6,7 @@
|
|
* Author: Paul Burton <paul.burton@mips.com>
|
|
*
|
|
* Copyright (C) 2021 Glider bv
|
|
+ * Copyright (C) 2025 Jean-François Lessard
|
|
*/
|
|
|
|
#ifndef _LINEDISP_H
|
|
@@ -81,6 +82,9 @@ struct linedisp {
|
|
unsigned int id;
|
|
};
|
|
|
|
+int linedisp_attach(struct linedisp *linedisp, struct device *dev,
|
|
+ unsigned int num_chars, const struct linedisp_ops *ops);
|
|
+void linedisp_detach(struct device *dev);
|
|
int linedisp_register(struct linedisp *linedisp, struct device *parent,
|
|
unsigned int num_chars, const struct linedisp_ops *ops);
|
|
void linedisp_unregister(struct linedisp *linedisp);
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 16 Aug 2025 16:17:06 -0400
|
|
Subject: dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton,
|
|
winrise, wxicore
|
|
|
|
Add vendor prefixes of chip manufacturers supported by the TM16xx 7-segment
|
|
LED matrix display controllers driver:
|
|
- fdhisi: Fuzhou Fuda Hisi Microelectronics Co., Ltd.
|
|
- titanmec: Shenzhen Titan Micro Electronics Co., Ltd.
|
|
- princeton: Princeton Technology Corp.
|
|
- winrise: Shenzhen Winrise Technology Co., Ltd.
|
|
- wxicore: Wuxi i-Core Electronics Co., Ltd.
|
|
|
|
The titanmec prefix is based on the company's domain name titanmec.com.
|
|
The remaining prefixes are based on company names, as these manufacturers
|
|
either lack active .com domains or their .com domains are occupied by
|
|
unrelated businesses.
|
|
|
|
The fdhisi and titanmec prefixes were originally identified by
|
|
Andreas Farber.
|
|
|
|
CC: Andreas Farber <afaerber@suse.de>
|
|
Acked-by: Conor Dooley <conor.dooley@microchip.com>
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
Documentation/devicetree/bindings/vendor-prefixes.yaml | 10 ++++++++++
|
|
1 file changed, 10 insertions(+)
|
|
|
|
diff --git a/Documentation/devicetree/bindings/vendor-prefixes.yaml b/Documentation/devicetree/bindings/vendor-prefixes.yaml
|
|
index 111111111111..222222222222 100644
|
|
--- a/Documentation/devicetree/bindings/vendor-prefixes.yaml
|
|
+++ b/Documentation/devicetree/bindings/vendor-prefixes.yaml
|
|
@@ -562,6 +562,8 @@ patternProperties:
|
|
description: Fastrax Oy
|
|
"^fcs,.*":
|
|
description: Fairchild Semiconductor
|
|
+ "^fdhisi,.*":
|
|
+ description: Fuzhou Fuda Hisi Microelectronics Co., Ltd.
|
|
"^feixin,.*":
|
|
description: Shenzhen Feixin Photoelectic Co., Ltd
|
|
"^feiyang,.*":
|
|
@@ -1279,6 +1281,8 @@ patternProperties:
|
|
description: Prime View International (PVI)
|
|
"^primux,.*":
|
|
description: Primux Trading, S.L.
|
|
+ "^princeton,.*":
|
|
+ description: Princeton Technology Corp.
|
|
"^probox2,.*":
|
|
description: PROBOX2 (by W2COMP Co., Ltd.)
|
|
"^pri,.*":
|
|
@@ -1632,6 +1636,8 @@ patternProperties:
|
|
description: Texas Instruments
|
|
"^tianma,.*":
|
|
description: Tianma Micro-electronics Co., Ltd.
|
|
+ "^titanmec,.*":
|
|
+ description: Shenzhen Titan Micro Electronics Co., Ltd.
|
|
"^tlm,.*":
|
|
description: Trusted Logic Mobility
|
|
"^tmt,.*":
|
|
@@ -1791,6 +1797,8 @@ patternProperties:
|
|
description: Wingtech Technology Co., Ltd.
|
|
"^winlink,.*":
|
|
description: WinLink Co., Ltd
|
|
+ "^winrise,.*":
|
|
+ description: Shenzhen Winrise Technology Co., Ltd.
|
|
"^winsen,.*":
|
|
description: Winsen Corp.
|
|
"^winstar,.*":
|
|
@@ -1807,6 +1815,8 @@ patternProperties:
|
|
description: Wobo
|
|
"^wolfvision,.*":
|
|
description: WolfVision GmbH
|
|
+ "^wxicore,.*":
|
|
+ description: Wuxi i-Core Electronics Co., Ltd.
|
|
"^x-powers,.*":
|
|
description: X-Powers
|
|
"^xen,.*":
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Mon, 1 Sep 2025 11:57:15 -0400
|
|
Subject: dt-bindings: leds: add default-brightness property to common.yaml
|
|
|
|
Add default-brightness property to leds/common.yaml to establish a single
|
|
canonical definition for LED brightness initialization.
|
|
|
|
The property is currently defined locally in leds/leds-pwm.yaml and is
|
|
needed by auxdisplay/titanmec,tm16xx.yaml. Properties should be defined
|
|
in only one location to avoid type inconsistencies across bindings.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
Documentation/devicetree/bindings/leds/common.yaml | 6 ++++++
|
|
1 file changed, 6 insertions(+)
|
|
|
|
diff --git a/Documentation/devicetree/bindings/leds/common.yaml b/Documentation/devicetree/bindings/leds/common.yaml
|
|
index 111111111111..222222222222 100644
|
|
--- a/Documentation/devicetree/bindings/leds/common.yaml
|
|
+++ b/Documentation/devicetree/bindings/leds/common.yaml
|
|
@@ -173,6 +173,12 @@ properties:
|
|
led-max-microamp.
|
|
$ref: /schemas/types.yaml#/definitions/uint32
|
|
|
|
+ default-brightness:
|
|
+ description:
|
|
+ Brightness to be set if LED's default state is on. Used only during
|
|
+ initialization. If the option is not set then max brightness is used.
|
|
+ $ref: /schemas/types.yaml#/definitions/uint32
|
|
+
|
|
panic-indicator:
|
|
description:
|
|
This property specifies that the LED should be used, if at all possible,
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Mon, 1 Sep 2025 12:03:37 -0400
|
|
Subject: dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx
|
|
|
|
Add documentation for TM16xx-compatible 7-segment LED display controllers
|
|
with keyscan.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml | 463 ++++++++++
|
|
1 file changed, 463 insertions(+)
|
|
|
|
diff --git a/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml b/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
|
|
new file mode 100644
|
|
index 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
|
|
@@ -0,0 +1,463 @@
|
|
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
|
|
+%YAML 1.2
|
|
+---
|
|
+$id: http://devicetree.org/schemas/auxdisplay/titanmec,tm16xx.yaml#
|
|
+$schema: http://devicetree.org/meta-schemas/core.yaml#
|
|
+
|
|
+title: Auxiliary displays based on TM16xx and compatible LED controllers
|
|
+
|
|
+maintainers:
|
|
+ - Jean-François Lessard <jefflessard3@gmail.com>
|
|
+
|
|
+description: |
|
|
+ LED matrix controllers used in auxiliary display devices that drive individual
|
|
+ LED icons and 7-segment digit groups through a grid/segment addressing scheme.
|
|
+ Controllers manage a matrix of LEDs organized as grids (columns/banks in
|
|
+ vendor datasheets) and segments (rows/bit positions in vendor datasheets).
|
|
+ Maximum brightness and grid/segment indices are controller-specific.
|
|
+ Controller-specific maximum are validated in the driver.
|
|
+
|
|
+ The controller is agnostic of the display layout. Board-specific LED wiring is
|
|
+ described through child nodes that specify grid/segment coordinates for
|
|
+ individual icons and segment mapping for 7-segment digits.
|
|
+
|
|
+ The bindings use separate 'leds' and 'digits' containers to accommodate
|
|
+ different addressing schemes:
|
|
+ - LEDs use 2-cell addressing (grid, segment) for matrix coordinates
|
|
+ - Digits use 1-cell addressing with explicit segment mapping
|
|
+
|
|
+ The controller node exposes a logical LED-like control for the aggregate
|
|
+ display brightness. Child nodes describe individual icons and 7-seg digits.
|
|
+ The top-level control supports only label and brightness-related properties
|
|
+ and does not support other common LED properties such as color or function.
|
|
+ Child LED nodes use the standard LED binding.
|
|
+
|
|
+ Optional keypad scanning is supported when both 'linux,keymap' and
|
|
+ 'poll-interval' properties are specified.
|
|
+
|
|
+properties:
|
|
+ compatible:
|
|
+ oneOf:
|
|
+ - items:
|
|
+ - enum:
|
|
+ - fdhisi,fd628
|
|
+ - princeton,pt6964
|
|
+ - wxicore,aip1628
|
|
+ - const: titanmec,tm1628
|
|
+ - items:
|
|
+ - enum:
|
|
+ - wxicore,aip1618
|
|
+ - const: titanmec,tm1618
|
|
+ - items:
|
|
+ - enum:
|
|
+ - fdhisi,fd650
|
|
+ - wxicore,aip650
|
|
+ - const: titanmec,tm1650
|
|
+ - enum:
|
|
+ - fdhisi,fd620
|
|
+ - fdhisi,fd655
|
|
+ - fdhisi,fd6551
|
|
+ - titanmec,tm1618
|
|
+ - titanmec,tm1620
|
|
+ - titanmec,tm1628
|
|
+ - titanmec,tm1638
|
|
+ - titanmec,tm1650
|
|
+ - winrise,hbs658
|
|
+
|
|
+ reg:
|
|
+ maxItems: 1
|
|
+
|
|
+ label:
|
|
+ description:
|
|
+ The label for the top-level LED. If omitted, the label is taken from the
|
|
+ node name (excluding the unit address). It has to uniquely identify a
|
|
+ device, i.e. no other LED class device can be assigned the same label.
|
|
+
|
|
+ max-brightness:
|
|
+ minimum: 0 # 0=off
|
|
+ maximum: 8 # Maximum across all TM16xx controllers
|
|
+ description:
|
|
+ Normally the maximum brightness is determined by the hardware and this
|
|
+ property is not required. This property is used to put a software limit
|
|
+ on the brightness apart from what the driver says, as it could happen
|
|
+ that a LED can be made so bright that it gets damaged or causes damage
|
|
+ due to restrictions in a specific system, such as mounting conditions.
|
|
+
|
|
+ default-brightness:
|
|
+ minimum: 0 # 0=off
|
|
+ maximum: 8 # Maximum across all TM16xx controllers
|
|
+ description:
|
|
+ Brightness to be set if LED's default state is on. Used only during
|
|
+ initialization. If the option is not set then max brightness is used.
|
|
+
|
|
+ digits:
|
|
+ type: object
|
|
+ description: Container for 7-segment digit group definitions
|
|
+ additionalProperties: false
|
|
+
|
|
+ properties:
|
|
+ "#address-cells":
|
|
+ const: 1
|
|
+ "#size-cells":
|
|
+ const: 0
|
|
+
|
|
+ patternProperties:
|
|
+ "^digit@[0-9]+$":
|
|
+ type: object
|
|
+ unevaluatedProperties: false
|
|
+
|
|
+ properties:
|
|
+ reg:
|
|
+ description:
|
|
+ Digit position identifier numbered sequentially left-to-right,
|
|
+ with reg=0 representing the leftmost digit position as displayed
|
|
+ to the user.
|
|
+ maxItems: 1
|
|
+
|
|
+ segments:
|
|
+ $ref: /schemas/types.yaml#/definitions/uint32-matrix
|
|
+ description: |
|
|
+ Array of grid/segment coordinate pairs for each 7-segment position.
|
|
+ Each entry is <grid segment> mapping to standard 7-segment positions
|
|
+ in order: a, b, c, d, e, f, g
|
|
+
|
|
+ Standard 7-segment layout:
|
|
+ aaa
|
|
+ f b
|
|
+ f b
|
|
+ ggg
|
|
+ e c
|
|
+ e c
|
|
+ ddd
|
|
+ items:
|
|
+ items:
|
|
+ - description: Grid index
|
|
+ - description: Segment index
|
|
+ minItems: 7
|
|
+ maxItems: 7
|
|
+
|
|
+ required:
|
|
+ - reg
|
|
+ - segments
|
|
+
|
|
+ leds:
|
|
+ type: object
|
|
+ description: Container for individual LED icon definitions
|
|
+ additionalProperties: false
|
|
+
|
|
+ properties:
|
|
+ "#address-cells":
|
|
+ const: 2
|
|
+ "#size-cells":
|
|
+ const: 0
|
|
+
|
|
+ patternProperties:
|
|
+ "^led@[0-9]+,[0-9]+$":
|
|
+ type: object
|
|
+ $ref: /schemas/leds/common.yaml#
|
|
+ unevaluatedProperties: false
|
|
+
|
|
+ properties:
|
|
+ reg:
|
|
+ description:
|
|
+ Grid and segment indices as <grid segment> of this individual LED icon
|
|
+
|
|
+ required:
|
|
+ - reg
|
|
+
|
|
+dependencies:
|
|
+ poll-interval:
|
|
+ - linux,keymap
|
|
+ linux,keymap:
|
|
+ - poll-interval
|
|
+ autorepeat:
|
|
+ - linux,keymap
|
|
+ - poll-interval
|
|
+
|
|
+required:
|
|
+ - compatible
|
|
+ - reg
|
|
+
|
|
+allOf:
|
|
+ - $ref: /schemas/leds/common.yaml#
|
|
+ properties:
|
|
+ color: false
|
|
+ function: false
|
|
+ function-enumerator: false
|
|
+ - $ref: /schemas/input/input.yaml#
|
|
+ - $ref: /schemas/input/matrix-keymap.yaml#
|
|
+ # SPI controllers require 3-wire (combined MISO/MOSI line)
|
|
+ - if:
|
|
+ properties:
|
|
+ compatible:
|
|
+ contains:
|
|
+ enum:
|
|
+ - fdhisi,fd620
|
|
+ - fdhisi,fd628
|
|
+ - princeton,pt6964
|
|
+ - titanmec,tm1618
|
|
+ - titanmec,tm1620
|
|
+ - titanmec,tm1628
|
|
+ - titanmec,tm1638
|
|
+ - wxicore,aip1618
|
|
+ - wxicore,aip1628
|
|
+ then:
|
|
+ $ref: /schemas/spi/spi-peripheral-props.yaml#
|
|
+ properties:
|
|
+ spi-3wire: true
|
|
+ required:
|
|
+ - spi-3wire
|
|
+
|
|
+unevaluatedProperties: false
|
|
+
|
|
+examples:
|
|
+ - |
|
|
+ #include <dt-bindings/leds/common.h>
|
|
+
|
|
+ // I2C example: Magicsee N5 TV box with fd655 controller
|
|
+ i2c {
|
|
+ #address-cells = <1>;
|
|
+ #size-cells = <0>;
|
|
+
|
|
+ display@24 {
|
|
+ reg = <0x24>;
|
|
+ compatible = "fdhisi,fd655";
|
|
+
|
|
+ digits {
|
|
+ #address-cells = <1>;
|
|
+ #size-cells = <0>;
|
|
+ digit@0 {
|
|
+ reg = <0>;
|
|
+ segments = <1 3>, <1 4>, <1 5>, <1 0>, <1 1>, <1 2>, <1 6>;
|
|
+ };
|
|
+ digit@1 {
|
|
+ reg = <1>;
|
|
+ segments = <2 3>, <2 4>, <2 5>, <2 0>, <2 1>, <2 2>, <2 6>;
|
|
+ };
|
|
+ digit@2 {
|
|
+ reg = <2>;
|
|
+ segments = <3 3>, <3 4>, <3 5>, <3 0>, <3 1>, <3 2>, <3 6>;
|
|
+ };
|
|
+ digit@3 {
|
|
+ reg = <3>;
|
|
+ segments = <4 3>, <4 4>, <4 5>, <4 0>, <4 1>, <4 2>, <4 6>;
|
|
+ };
|
|
+ };
|
|
+
|
|
+ leds {
|
|
+ #address-cells = <2>;
|
|
+ #size-cells = <0>;
|
|
+ led@0,0 {
|
|
+ reg = <0 0>;
|
|
+ function = LED_FUNCTION_ALARM;
|
|
+ };
|
|
+ led@0,1 {
|
|
+ reg = <0 1>;
|
|
+ function = LED_FUNCTION_USB;
|
|
+ };
|
|
+ led@0,2 {
|
|
+ reg = <0 2>;
|
|
+ function = "play";
|
|
+ };
|
|
+ led@0,3 {
|
|
+ reg = <0 3>;
|
|
+ function = "pause";
|
|
+ };
|
|
+ led@0,4 {
|
|
+ reg = <0 4>;
|
|
+ function = "colon";
|
|
+ };
|
|
+ led@0,5 {
|
|
+ reg = <0 5>;
|
|
+ function = LED_FUNCTION_LAN;
|
|
+ };
|
|
+ led@0,6 {
|
|
+ reg = <0 6>;
|
|
+ function = LED_FUNCTION_WLAN;
|
|
+ };
|
|
+ };
|
|
+ };
|
|
+ };
|
|
+
|
|
+ - |
|
|
+ #include <dt-bindings/input/input.h>
|
|
+
|
|
+ // SPI example: TM1638 module with keypad support
|
|
+ spi {
|
|
+ #address-cells = <1>;
|
|
+ #size-cells = <0>;
|
|
+
|
|
+ display@0 {
|
|
+ reg = <0>;
|
|
+ compatible = "titanmec,tm1638";
|
|
+ spi-3wire;
|
|
+ spi-lsb-first;
|
|
+ spi-max-frequency = <500000>;
|
|
+
|
|
+ label = "tm1638";
|
|
+ default-brightness = <2>;
|
|
+ max-brightness = <4>;
|
|
+ poll-interval = <100>;
|
|
+ linux,keymap = <MATRIX_KEY(2, 0, KEY_F1)
|
|
+ MATRIX_KEY(2, 2, KEY_F2)
|
|
+ MATRIX_KEY(2, 4, KEY_F3)
|
|
+ MATRIX_KEY(2, 6, KEY_F4)
|
|
+ MATRIX_KEY(2, 1, KEY_F5)
|
|
+ MATRIX_KEY(2, 3, KEY_F6)
|
|
+ MATRIX_KEY(2, 5, KEY_F7)
|
|
+ MATRIX_KEY(2, 7, KEY_F8)>;
|
|
+
|
|
+ digits {
|
|
+ #address-cells = <1>;
|
|
+ #size-cells = <0>;
|
|
+
|
|
+ digit@0 {
|
|
+ reg = <0>;
|
|
+ segments = <7 0>, <7 1>, <7 2>, <7 3>, <7 4>, <7 5>, <7 6>;
|
|
+ };
|
|
+
|
|
+ digit@1 {
|
|
+ reg = <1>;
|
|
+ segments = <6 0>, <6 1>, <6 2>, <6 3>, <6 4>, <6 5>, <6 6>;
|
|
+ };
|
|
+
|
|
+ digit@2 {
|
|
+ reg = <2>;
|
|
+ segments = <5 0>, <5 1>, <5 2>, <5 3>, <5 4>, <5 5>, <5 6>;
|
|
+ };
|
|
+
|
|
+ digit@3 {
|
|
+ reg = <3>;
|
|
+ segments = <4 0>, <4 1>, <4 2>, <4 3>, <4 4>, <4 5>, <4 6>;
|
|
+ };
|
|
+
|
|
+ digit@4 {
|
|
+ reg = <4>;
|
|
+ segments = <3 0>, <3 1>, <3 2>, <3 3>, <3 4>, <3 5>, <3 6>;
|
|
+ };
|
|
+
|
|
+ digit@5 {
|
|
+ reg = <5>;
|
|
+ segments = <2 0>, <2 1>, <2 2>, <2 3>, <2 4>, <2 5>, <2 6>;
|
|
+ };
|
|
+
|
|
+ digit@6 {
|
|
+ reg = <6>;
|
|
+ segments = <1 0>, <1 1>, <1 2>, <1 3>, <1 4>, <1 5>, <1 6>;
|
|
+ };
|
|
+
|
|
+ digit@7 {
|
|
+ reg = <7>;
|
|
+ segments = <0 0>, <0 1>, <0 2>, <0 3>, <0 4>, <0 5>, <0 6>;
|
|
+ };
|
|
+ };
|
|
+
|
|
+ leds {
|
|
+ #address-cells = <2>;
|
|
+ #size-cells = <0>;
|
|
+
|
|
+ led@0,7 {
|
|
+ reg = <0 7>;
|
|
+ };
|
|
+
|
|
+ led@1,7 {
|
|
+ reg = <1 7>;
|
|
+ };
|
|
+
|
|
+ led@2,7 {
|
|
+ reg = <2 7>;
|
|
+ };
|
|
+
|
|
+ led@3,7 {
|
|
+ reg = <3 7>;
|
|
+ };
|
|
+
|
|
+ led@4,7 {
|
|
+ reg = <4 7>;
|
|
+ };
|
|
+
|
|
+ led@5,7 {
|
|
+ reg = <5 7>;
|
|
+ };
|
|
+
|
|
+ led@6,7 {
|
|
+ reg = <6 7>;
|
|
+ };
|
|
+
|
|
+ led@7,7 {
|
|
+ reg = <7 7>;
|
|
+ };
|
|
+ };
|
|
+ };
|
|
+ };
|
|
+
|
|
+ - |
|
|
+ #include <dt-bindings/leds/common.h>
|
|
+
|
|
+ // SPI example: X96 Max with transposed layout (fd628 with tm1628 fallback)
|
|
+ spi {
|
|
+ #address-cells = <1>;
|
|
+ #size-cells = <0>;
|
|
+
|
|
+ display@0 {
|
|
+ reg = <0>;
|
|
+ compatible = "fdhisi,fd628", "titanmec,tm1628";
|
|
+ spi-3wire;
|
|
+ spi-lsb-first;
|
|
+ spi-max-frequency = <500000>;
|
|
+
|
|
+ digits {
|
|
+ #address-cells = <1>;
|
|
+ #size-cells = <0>;
|
|
+ digit@0 {
|
|
+ reg = <0>;
|
|
+ segments = <0 7>, <1 7>, <2 7>, <3 7>, <4 7>, <5 7>, <6 7>;
|
|
+ };
|
|
+ digit@1 {
|
|
+ reg = <1>;
|
|
+ segments = <0 6>, <1 6>, <2 6>, <3 6>, <4 6>, <5 6>, <6 6>;
|
|
+ };
|
|
+ digit@2 {
|
|
+ reg = <2>;
|
|
+ segments = <0 5>, <1 5>, <2 5>, <3 5>, <4 5>, <5 5>, <6 5>;
|
|
+ };
|
|
+ digit@3 {
|
|
+ reg = <3>;
|
|
+ segments = <0 4>, <1 4>, <2 4>, <3 4>, <4 4>, <5 4>, <6 4>;
|
|
+ };
|
|
+ };
|
|
+
|
|
+ leds {
|
|
+ #address-cells = <2>;
|
|
+ #size-cells = <0>;
|
|
+ led@0,3 {
|
|
+ reg = <0 3>;
|
|
+ function = "apps";
|
|
+ };
|
|
+ led@1,3 {
|
|
+ reg = <1 3>;
|
|
+ function = "setup";
|
|
+ };
|
|
+ led@2,3 {
|
|
+ reg = <2 3>;
|
|
+ function = LED_FUNCTION_USB;
|
|
+ };
|
|
+ led@3,3 {
|
|
+ reg = <3 3>;
|
|
+ function = LED_FUNCTION_SD;
|
|
+ };
|
|
+ led@4,3 {
|
|
+ reg = <4 3>;
|
|
+ function = "colon";
|
|
+ };
|
|
+ led@5,3 {
|
|
+ reg = <5 3>;
|
|
+ function = "hdmi";
|
|
+ };
|
|
+ led@6,3 {
|
|
+ reg = <6 3>;
|
|
+ function = "video";
|
|
+ };
|
|
+ };
|
|
+ };
|
|
+ };
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 16 Aug 2025 20:52:21 -0400
|
|
Subject: auxdisplay: Add TM16xx 7-segment LED matrix display controllers
|
|
driver
|
|
|
|
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-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/Kconfig | 9 +
|
|
drivers/auxdisplay/Makefile | 2 +
|
|
drivers/auxdisplay/tm16xx.h | 172 ++++
|
|
drivers/auxdisplay/tm16xx_core.c | 459 ++++++++++
|
|
4 files changed, 642 insertions(+)
|
|
|
|
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Kconfig
|
|
+++ b/drivers/auxdisplay/Kconfig
|
|
@@ -526,6 +526,15 @@ 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
|
|
+ select LEDS_CLASS
|
|
+ select LEDS_TRIGGERS
|
|
+ select LINEDISP
|
|
+ select NEW_LEDS
|
|
+ help
|
|
+ Core TM16XX-compatible 7-segment LED controllers module
|
|
+
|
|
#
|
|
# Character LCD with non-conforming interface section
|
|
#
|
|
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
|
|
index 111111111111..222222222222 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 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/drivers/auxdisplay/tm16xx.h
|
|
@@ -0,0 +1,172 @@
|
|
+/* 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) 2025 Jean-François Lessard
|
|
+ */
|
|
+
|
|
+#ifndef _TM16XX_H
|
|
+#define _TM16XX_H
|
|
+
|
|
+#include <linux/bitfield.h>
|
|
+#include <linux/leds.h>
|
|
+#include <linux/workqueue.h>
|
|
+
|
|
+#include "line-display.h"
|
|
+
|
|
+/* 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, prefix) \
|
|
+ ((on) ? (FIELD_PREP(prefix##_CTRL_BR_MASK, (val)) | prefix##_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.
|
|
+ * @linedisp: character line display structure
|
|
+ * @spi_buffer: DMA-safe buffer for SPI transactions, or NULL for I2C.
|
|
+ * @num_hwgrid: Number of controller grids in use.
|
|
+ * @num_hwseg: 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;
|
|
+ struct linedisp linedisp;
|
|
+ u8 *spi_buffer;
|
|
+ u8 num_hwgrid;
|
|
+ u8 num_hwseg;
|
|
+ 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 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/drivers/auxdisplay/tm16xx_core.c
|
|
@@ -0,0 +1,459 @@
|
|
+// 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) 2025 Jean-François Lessard
|
|
+ */
|
|
+
|
|
+#include <linux/bitfield.h>
|
|
+#include <linux/bitmap.h>
|
|
+#include <linux/cleanup.h>
|
|
+#include <linux/container_of.h>
|
|
+#include <linux/device.h>
|
|
+#include <linux/leds.h>
|
|
+#include <linux/map_to_7segment.h>
|
|
+#include <linux/module.h>
|
|
+#include <linux/property.h>
|
|
+#include <linux/sysfs.h>
|
|
+#include <linux/workqueue.h>
|
|
+
|
|
+#include "line-display.h"
|
|
+#include "tm16xx.h"
|
|
+
|
|
+#define TM16XX_DIGIT_SEGMENTS 7
|
|
+
|
|
+#define linedisp_to_tm16xx(display) \
|
|
+ container_of(display, struct tm16xx_display, linedisp)
|
|
+
|
|
+/**
|
|
+ * struct tm16xx_led - Individual LED icon mapping
|
|
+ * @cdev: LED class device for sysfs interface.
|
|
+ * @hwgrid: Controller grid index of the LED.
|
|
+ * @hwseg: Controller segment index of the LED.
|
|
+ */
|
|
+struct tm16xx_led {
|
|
+ struct led_classdev cdev;
|
|
+ u8 hwgrid;
|
|
+ u8 hwseg;
|
|
+};
|
|
+
|
|
+/**
|
|
+ * struct tm16xx_digit - 7-segment digit mapping and value
|
|
+ * @hwgrids: Array mapping each 7-segment position to a grid on the controller.
|
|
+ * @hwsegs: Array mapping each 7-segment position to a segment on the controller.
|
|
+ * @value: Current character value displayed on this digit.
|
|
+ */
|
|
+struct tm16xx_digit {
|
|
+ u8 hwgrids[TM16XX_DIGIT_SEGMENTS];
|
|
+ u8 hwsegs[TM16XX_DIGIT_SEGMENTS];
|
|
+};
|
|
+
|
|
+/* 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_hwgrid * display->num_hwseg;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_set_seg() - Set the display state for a specific grid/segment
|
|
+ * @display: pointer to tm16xx_display
|
|
+ * @hwgrid: grid index
|
|
+ * @hwseg: segment index
|
|
+ * @on: true to turn on, false to turn off
|
|
+ */
|
|
+static inline void tm16xx_set_seg(const struct tm16xx_display *display,
|
|
+ const u8 hwgrid, const u8 hwseg, const bool on)
|
|
+{
|
|
+ assign_bit(hwgrid * display->num_hwseg + hwseg, 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_hwseg,
|
|
+ display->num_hwseg);
|
|
+}
|
|
+
|
|
+/* 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) {
|
|
+ scoped_guard(mutex, &display->lock) {
|
|
+ ret = display->controller->init(display);
|
|
+ display->flush_status = ret;
|
|
+ }
|
|
+ if (ret)
|
|
+ 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);
|
|
+ unsigned int grid, i;
|
|
+ int ret = 0;
|
|
+
|
|
+ scoped_guard(mutex, &display->lock) {
|
|
+ if (display->controller->data) {
|
|
+ for (i = 0; i < display->num_hwgrid; i++) {
|
|
+ grid = tm16xx_get_grid(display, i);
|
|
+ ret = display->controller->data(display, i, grid);
|
|
+ if (ret) {
|
|
+ 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);
|
|
+
|
|
+ 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);
|
|
+
|
|
+ tm16xx_set_seg(display, led->hwgrid, led->hwseg, value);
|
|
+ schedule_work(&display->flush_display);
|
|
+}
|
|
+
|
|
+static int tm16xx_display_value(struct tm16xx_display *display, const char *buf, size_t count)
|
|
+{
|
|
+ struct linedisp *linedisp = &display->linedisp;
|
|
+ struct linedisp_map *map = linedisp->map;
|
|
+ struct tm16xx_digit *digit;
|
|
+ unsigned int i, j;
|
|
+ int seg_pattern;
|
|
+ bool val;
|
|
+
|
|
+ for (i = 0; i < display->num_digits && i < count; i++) {
|
|
+ digit = &display->digits[i];
|
|
+ seg_pattern = map_to_seg7(&map->map.seg7, buf[i]);
|
|
+
|
|
+ for (j = 0; j < TM16XX_DIGIT_SEGMENTS; j++) {
|
|
+ val = seg_pattern & BIT(j);
|
|
+ tm16xx_set_seg(display, digit->hwgrids[j], digit->hwsegs[j], val);
|
|
+ }
|
|
+ }
|
|
+
|
|
+ for (; i < display->num_digits; i++) {
|
|
+ digit = &display->digits[i];
|
|
+ for (j = 0; j < TM16XX_DIGIT_SEGMENTS; j++)
|
|
+ tm16xx_set_seg(display, digit->hwgrids[j], digit->hwsegs[j], 0);
|
|
+ }
|
|
+
|
|
+ schedule_work(&display->flush_display);
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int tm16xx_linedisp_get_map_type(struct linedisp *linedisp)
|
|
+{
|
|
+ return LINEDISP_MAP_SEG7;
|
|
+}
|
|
+
|
|
+static void tm16xx_linedisp_update(struct linedisp *linedisp)
|
|
+{
|
|
+ struct tm16xx_display *display = linedisp_to_tm16xx(linedisp);
|
|
+
|
|
+ tm16xx_display_value(display, linedisp->buf, linedisp->num_chars);
|
|
+}
|
|
+
|
|
+static const struct linedisp_ops tm16xx_linedisp_ops = {
|
|
+ .get_map_type = tm16xx_linedisp_get_map_type,
|
|
+ .update = tm16xx_linedisp_update,
|
|
+};
|
|
+
|
|
+static int tm16xx_display_init(struct tm16xx_display *display)
|
|
+{
|
|
+ schedule_work(&display->flush_init);
|
|
+ flush_work(&display->flush_init);
|
|
+ if (display->flush_status)
|
|
+ return display->flush_status;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int tm16xx_parse_fwnode(struct device *dev, struct tm16xx_display *display)
|
|
+{
|
|
+ struct tm16xx_led *led;
|
|
+ struct tm16xx_digit *digit;
|
|
+ unsigned int max_hwgrid = 0, max_hwseg = 0;
|
|
+ unsigned int i, j;
|
|
+ int ret;
|
|
+ u32 segments[TM16XX_DIGIT_SEGMENTS * 2];
|
|
+ u32 reg[2];
|
|
+
|
|
+ struct fwnode_handle *digits_node __free(fwnode_handle) =
|
|
+ device_get_named_child_node(dev, "digits");
|
|
+ struct fwnode_handle *leds_node __free(fwnode_handle) =
|
|
+ device_get_named_child_node(dev, "leds");
|
|
+
|
|
+ /* parse digits */
|
|
+ if (digits_node) {
|
|
+ display->num_digits = fwnode_get_child_node_count(digits_node);
|
|
+
|
|
+ if (display->num_digits) {
|
|
+ display->digits = devm_kcalloc(dev, display->num_digits,
|
|
+ sizeof(*display->digits),
|
|
+ GFP_KERNEL);
|
|
+ if (!display->digits)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ i = 0;
|
|
+ fwnode_for_each_available_child_node_scoped(digits_node, child) {
|
|
+ digit = &display->digits[i];
|
|
+
|
|
+ ret = fwnode_property_read_u32(child, "reg", reg);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ ret = fwnode_property_read_u32_array(child,
|
|
+ "segments", segments,
|
|
+ TM16XX_DIGIT_SEGMENTS * 2);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ for (j = 0; j < TM16XX_DIGIT_SEGMENTS; ++j) {
|
|
+ digit->hwgrids[j] = segments[2 * j];
|
|
+ digit->hwsegs[j] = segments[2 * j + 1];
|
|
+ max_hwgrid = umax(max_hwgrid, digit->hwgrids[j]);
|
|
+ max_hwseg = umax(max_hwseg, digit->hwsegs[j]);
|
|
+ }
|
|
+ i++;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /* parse leds */
|
|
+ if (leds_node) {
|
|
+ display->num_leds = fwnode_get_child_node_count(leds_node);
|
|
+
|
|
+ if (display->num_leds) {
|
|
+ display->leds = devm_kcalloc(dev, display->num_leds,
|
|
+ sizeof(*display->leds),
|
|
+ GFP_KERNEL);
|
|
+ if (!display->leds)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ i = 0;
|
|
+ fwnode_for_each_available_child_node_scoped(leds_node, child) {
|
|
+ led = &display->leds[i];
|
|
+ ret = fwnode_property_read_u32_array(child, "reg", reg, 2);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ led->hwgrid = reg[0];
|
|
+ led->hwseg = reg[1];
|
|
+ max_hwgrid = umax(max_hwgrid, led->hwgrid);
|
|
+ max_hwseg = umax(max_hwseg, led->hwseg);
|
|
+ i++;
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+
|
|
+ if (max_hwgrid >= display->controller->max_grids) {
|
|
+ dev_err(dev, "grid %u exceeds controller max_grids %u\n",
|
|
+ max_hwgrid, display->controller->max_grids);
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ if (max_hwseg >= display->controller->max_segments) {
|
|
+ dev_err(dev, "segment %u exceeds controller max_segments %u\n",
|
|
+ max_hwseg, display->controller->max_segments);
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ display->num_hwgrid = max_hwgrid + 1;
|
|
+ display->num_hwseg = max_hwseg + 1;
|
|
+
|
|
+ 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 led_init_data led_init = {0};
|
|
+ struct fwnode_handle *leds_node;
|
|
+ struct tm16xx_led *led;
|
|
+ unsigned int nbits, i;
|
|
+ int ret;
|
|
+
|
|
+ ret = tm16xx_parse_fwnode(dev, display);
|
|
+ if (ret)
|
|
+ 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;
|
|
+
|
|
+ ret = devm_mutex_init(display->dev, &display->lock);
|
|
+ if (ret)
|
|
+ return dev_err_probe(dev, ret, "Failed to initialize mutex\n");
|
|
+
|
|
+ INIT_WORK(&display->flush_init, tm16xx_display_flush_init);
|
|
+ INIT_WORK(&display->flush_display, tm16xx_display_flush_data);
|
|
+
|
|
+ /* Initialize main LED properties */
|
|
+ led_init.fwnode = dev_fwnode(dev); /* apply label property */
|
|
+ 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->flags = LED_RETAIN_AT_SHUTDOWN | LED_CORE_SUSPENDRESUME;
|
|
+
|
|
+ /* Register individual LEDs from device tree */
|
|
+ ret = led_classdev_register_ext(dev, main, &led_init);
|
|
+ if (ret)
|
|
+ return dev_err_probe(dev, ret, "Failed to register main LED\n");
|
|
+
|
|
+ i = 0;
|
|
+ led_init.devicename = dev_name(main->dev);
|
|
+ led_init.devname_mandatory = true;
|
|
+ led_init.default_label = "led";
|
|
+ leds_node = device_get_named_child_node(dev, "leds");
|
|
+ fwnode_for_each_available_child_node_scoped(leds_node, child) {
|
|
+ led_init.fwnode = child;
|
|
+ led = &display->leds[i];
|
|
+ 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) {
|
|
+ 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) {
|
|
+ dev_err_probe(dev, ret, "Failed to initialize display\n");
|
|
+ goto unregister_leds;
|
|
+ }
|
|
+
|
|
+ ret = linedisp_attach(&display->linedisp, display->main_led.dev,
|
|
+ display->num_digits, &tm16xx_linedisp_ops);
|
|
+ if (ret) {
|
|
+ dev_err_probe(dev, ret, "Failed to initialize line-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;
|
|
+
|
|
+ linedisp_detach(display->main_led.dev);
|
|
+
|
|
+ /*
|
|
+ * 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);
|
|
+}
|
|
+EXPORT_SYMBOL_NS(tm16xx_remove, "TM16XX");
|
|
+
|
|
+MODULE_AUTHOR("Jean-François Lessard");
|
|
+MODULE_DESCRIPTION("TM16xx LED Display Controllers");
|
|
+MODULE_LICENSE("GPL");
|
|
+MODULE_IMPORT_NS("LINEDISP");
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Fri, 29 Aug 2025 22:28:35 -0400
|
|
Subject: auxdisplay: TM16xx: Add keypad support for scanning matrix keys
|
|
|
|
Add support for keypad scanning on TM16xx-compatible auxiliary display
|
|
controllers. It handles keypad initialization, scanning, and input
|
|
reporting for matrix keys managed by the TM16xx devices.
|
|
|
|
Key features include:
|
|
- Input device registration configured by device properties
|
|
(poll-interval, linux,keymap, autorepeat)
|
|
- Key state tracking using managed bitmaps
|
|
- Matrix scanning and event reporting integrated with Linux input
|
|
subsystem
|
|
|
|
This code is separated from main core driver to improve maintainability
|
|
and reviewability.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/Kconfig | 9 +
|
|
drivers/auxdisplay/Makefile | 1 +
|
|
drivers/auxdisplay/tm16xx.h | 25 ++
|
|
drivers/auxdisplay/tm16xx_core.c | 4 +
|
|
drivers/auxdisplay/tm16xx_keypad.c | 196 ++++++++++
|
|
5 files changed, 235 insertions(+)
|
|
|
|
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Kconfig
|
|
+++ b/drivers/auxdisplay/Kconfig
|
|
@@ -528,13 +528,22 @@ config SEG_LED_GPIO
|
|
|
|
config TM16XX
|
|
tristate
|
|
+ depends on INPUT
|
|
+ select INPUT_MATRIXKMAP
|
|
select LEDS_CLASS
|
|
select LEDS_TRIGGERS
|
|
select LINEDISP
|
|
select NEW_LEDS
|
|
+ select TM16XX_KEYPAD if (INPUT)
|
|
help
|
|
Core TM16XX-compatible 7-segment LED controllers module
|
|
|
|
+config TM16XX_KEYPAD
|
|
+ bool
|
|
+ depends on TM16XX
|
|
+ help
|
|
+ Enable keyscan support for TM16XX driver.
|
|
+
|
|
#
|
|
# Character LCD with non-conforming interface section
|
|
#
|
|
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Makefile
|
|
+++ b/drivers/auxdisplay/Makefile
|
|
@@ -18,3 +18,4 @@ 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
|
|
+tm16xx-$(CONFIG_TM16XX_KEYPAD) += tm16xx_keypad.o
|
|
diff --git a/drivers/auxdisplay/tm16xx.h b/drivers/auxdisplay/tm16xx.h
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/tm16xx.h
|
|
+++ b/drivers/auxdisplay/tm16xx.h
|
|
@@ -103,6 +103,7 @@
|
|
struct tm16xx_display;
|
|
struct tm16xx_digit;
|
|
struct tm16xx_led;
|
|
+struct tm16xx_keypad;
|
|
|
|
/**
|
|
* DOC: struct tm16xx_controller - Controller-specific operations and limits
|
|
@@ -133,6 +134,7 @@ struct tm16xx_controller {
|
|
* @dev: Pointer to device struct.
|
|
* @controller: Controller-specific function table and limits.
|
|
* @linedisp: character line display structure
|
|
+ * @keypad: Opaque pointer to tm16xx_keypad struct.
|
|
* @spi_buffer: DMA-safe buffer for SPI transactions, or NULL for I2C.
|
|
* @num_hwgrid: Number of controller grids in use.
|
|
* @num_hwseg: Number of controller segments in use.
|
|
@@ -150,6 +152,7 @@ struct tm16xx_controller {
|
|
struct tm16xx_display {
|
|
struct device *dev;
|
|
const struct tm16xx_controller *controller;
|
|
+ struct tm16xx_keypad *keypad;
|
|
struct linedisp linedisp;
|
|
u8 *spi_buffer;
|
|
u8 num_hwgrid;
|
|
@@ -169,4 +172,26 @@ struct tm16xx_display {
|
|
int tm16xx_probe(struct tm16xx_display *display);
|
|
void tm16xx_remove(struct tm16xx_display *display);
|
|
|
|
+/* keypad support */
|
|
+#if IS_ENABLED(CONFIG_TM16XX_KEYPAD)
|
|
+int tm16xx_keypad_probe(struct tm16xx_display *display);
|
|
+void tm16xx_set_key(const struct tm16xx_display *display, const int row,
|
|
+ const int col, const bool pressed);
|
|
+#else
|
|
+static inline int tm16xx_keypad_probe(struct tm16xx_display *display)
|
|
+{
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static inline void tm16xx_set_key(const struct tm16xx_display *display,
|
|
+ const int row, const int col,
|
|
+ const bool pressed)
|
|
+{
|
|
+}
|
|
+#endif
|
|
+
|
|
+#define tm16xx_for_each_key(display, _r, _c) \
|
|
+ for (int _r = 0; _r < (display)->controller->max_key_rows; _r++) \
|
|
+ for (int _c = 0; _c < (display)->controller->max_key_cols; _c++)
|
|
+
|
|
#endif /* _TM16XX_H */
|
|
diff --git a/drivers/auxdisplay/tm16xx_core.c b/drivers/auxdisplay/tm16xx_core.c
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/tm16xx_core.c
|
|
+++ b/drivers/auxdisplay/tm16xx_core.c
|
|
@@ -408,6 +408,10 @@ int tm16xx_probe(struct tm16xx_display *display)
|
|
goto unregister_leds;
|
|
}
|
|
|
|
+ ret = tm16xx_keypad_probe(display);
|
|
+ if (ret)
|
|
+ dev_warn(dev, "Failed to initialize keypad: %d\n", ret);
|
|
+
|
|
return 0;
|
|
|
|
unregister_leds:
|
|
diff --git a/drivers/auxdisplay/tm16xx_keypad.c b/drivers/auxdisplay/tm16xx_keypad.c
|
|
new file mode 100644
|
|
index 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/drivers/auxdisplay/tm16xx_keypad.c
|
|
@@ -0,0 +1,196 @@
|
|
+// 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) 2025 Jean-François Lessard
|
|
+ */
|
|
+
|
|
+#include <linux/bitmap.h>
|
|
+#include <linux/cleanup.h>
|
|
+#include <linux/device.h>
|
|
+#include <linux/input.h>
|
|
+#include <linux/input/matrix_keypad.h>
|
|
+#include <linux/property.h>
|
|
+
|
|
+#include "tm16xx.h"
|
|
+
|
|
+/**
|
|
+ * struct tm16xx_keypad - Keypad matrix state and input device
|
|
+ * @input: Input device for reporting key events.
|
|
+ * @state: Current bitmap of key states.
|
|
+ * @last_state: Previous bitmap of key states for change detection.
|
|
+ * @changes: Bitmap of key state changes since last poll.
|
|
+ * @row_shift: Row shift for keymap encoding.
|
|
+ */
|
|
+struct tm16xx_keypad {
|
|
+ struct input_dev *input;
|
|
+ unsigned long *state;
|
|
+ unsigned long *last_state;
|
|
+ unsigned long *changes;
|
|
+ int row_shift;
|
|
+};
|
|
+
|
|
+/**
|
|
+ * tm16xx_key_nbits() - Number of bits for the keypad state bitmap
|
|
+ * @display: pointer to tm16xx_display
|
|
+ *
|
|
+ * Return: total bits in keypad state bitmap (max_key_rows * max_key_cols)
|
|
+ */
|
|
+static inline unsigned int tm16xx_key_nbits(const struct tm16xx_display *display)
|
|
+{
|
|
+ return display->controller->max_key_rows *
|
|
+ display->controller->max_key_cols;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_get_key_row() - Get row index from keypad bit index
|
|
+ * @display: pointer to tm16xx_display
|
|
+ * @bit: bit index in state bitmap
|
|
+ *
|
|
+ * Return: row index
|
|
+ */
|
|
+static inline int tm16xx_get_key_row(const struct tm16xx_display *display,
|
|
+ const unsigned int bit)
|
|
+{
|
|
+ return bit / display->controller->max_key_cols;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_get_key_col() - Get column index from keypad bit index
|
|
+ * @display: pointer to tm16xx_display
|
|
+ * @bit: bit index in state bitmap
|
|
+ *
|
|
+ * Return: column index
|
|
+ */
|
|
+static inline int tm16xx_get_key_col(const struct tm16xx_display *display,
|
|
+ const unsigned int bit)
|
|
+{
|
|
+ return bit % display->controller->max_key_cols;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_set_key() - Set the keypad state for a key
|
|
+ * @display: pointer to tm16xx_display
|
|
+ * @row: row index
|
|
+ * @col: column index
|
|
+ * @pressed: true if pressed, false otherwise
|
|
+ */
|
|
+void tm16xx_set_key(const struct tm16xx_display *display, const int row,
|
|
+ const int col, const bool pressed)
|
|
+{
|
|
+ __assign_bit(row * display->controller->max_key_cols + col,
|
|
+ display->keypad->state, pressed);
|
|
+}
|
|
+EXPORT_SYMBOL_NS(tm16xx_set_key, "TM16XX");
|
|
+
|
|
+/**
|
|
+ * tm16xx_keypad_poll() - Polls the keypad, reports events
|
|
+ * @input: pointer to input_dev
|
|
+ *
|
|
+ * Reads the matrix keypad state, compares with previous state, and
|
|
+ * reports key events to the input subsystem.
|
|
+ */
|
|
+static void tm16xx_keypad_poll(struct input_dev *input)
|
|
+{
|
|
+ struct tm16xx_display *display = input_get_drvdata(input);
|
|
+ struct tm16xx_keypad *keypad = display->keypad;
|
|
+ const unsigned short *keycodes = keypad->input->keycode;
|
|
+ unsigned int nbits = tm16xx_key_nbits(display);
|
|
+ unsigned int bit;
|
|
+ int row, col, scancode;
|
|
+ bool pressed;
|
|
+ int ret;
|
|
+
|
|
+ bitmap_zero(keypad->state, nbits);
|
|
+ bitmap_zero(keypad->changes, nbits);
|
|
+
|
|
+ scoped_guard(mutex, &display->lock) {
|
|
+ ret = display->controller->keys(display);
|
|
+ }
|
|
+
|
|
+ if (ret) {
|
|
+ dev_err(display->dev, "Reading failed: %d\n", ret);
|
|
+ return;
|
|
+ }
|
|
+
|
|
+ bitmap_xor(keypad->changes, keypad->state, keypad->last_state, nbits);
|
|
+
|
|
+ for_each_set_bit(bit, keypad->changes, nbits) {
|
|
+ row = tm16xx_get_key_row(display, bit);
|
|
+ col = tm16xx_get_key_col(display, bit);
|
|
+ pressed = _test_bit(bit, keypad->state);
|
|
+ scancode = MATRIX_SCAN_CODE(row, col, keypad->row_shift);
|
|
+
|
|
+ input_event(keypad->input, EV_MSC, MSC_SCAN, scancode);
|
|
+ input_report_key(keypad->input, keycodes[scancode], pressed);
|
|
+ }
|
|
+ input_sync(keypad->input);
|
|
+
|
|
+ bitmap_copy(keypad->last_state, keypad->state, nbits);
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_keypad_probe() - Initialize keypad/input device
|
|
+ * @display: pointer to tm16xx_display
|
|
+ *
|
|
+ * Return: 0 on success, negative error code on failure
|
|
+ */
|
|
+int tm16xx_keypad_probe(struct tm16xx_display *display)
|
|
+{
|
|
+ const unsigned int rows = display->controller->max_key_rows;
|
|
+ const unsigned int cols = display->controller->max_key_cols;
|
|
+ struct tm16xx_keypad *keypad;
|
|
+ struct input_dev *input;
|
|
+ unsigned int poll_interval, nbits;
|
|
+ int ret;
|
|
+
|
|
+ if (!display->controller->keys || !rows || !cols)
|
|
+ return 0; /* keypad not supported */
|
|
+
|
|
+ if (!device_property_present(display->dev, "poll-interval") ||
|
|
+ !device_property_present(display->dev, "linux,keymap"))
|
|
+ return 0; /* keypad disabled */
|
|
+
|
|
+ ret = device_property_read_u32(display->dev, "poll-interval", &poll_interval);
|
|
+ if (ret)
|
|
+ return dev_err_probe(display->dev, ret,
|
|
+ "Failed to read poll-interval\n");
|
|
+
|
|
+ keypad = devm_kzalloc(display->dev, sizeof(*keypad), GFP_KERNEL);
|
|
+ if (!keypad)
|
|
+ return -ENOMEM;
|
|
+ display->keypad = keypad;
|
|
+
|
|
+ nbits = tm16xx_key_nbits(display);
|
|
+ keypad->state = devm_bitmap_zalloc(display->dev, nbits, GFP_KERNEL);
|
|
+ keypad->last_state = devm_bitmap_zalloc(display->dev, nbits, GFP_KERNEL);
|
|
+ keypad->changes = devm_bitmap_zalloc(display->dev, nbits, GFP_KERNEL);
|
|
+ if (!keypad->state || !keypad->last_state || !keypad->changes)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ input = devm_input_allocate_device(display->dev);
|
|
+ if (!input)
|
|
+ return -ENOMEM;
|
|
+ input->name = "tm16xx-keypad";
|
|
+ keypad->input = input;
|
|
+ input_set_drvdata(input, display);
|
|
+
|
|
+ keypad->row_shift = get_count_order(cols); /* !cols already checked */
|
|
+ ret = matrix_keypad_build_keymap(NULL, "linux,keymap", rows, cols, NULL, input);
|
|
+ if (ret)
|
|
+ return dev_err_probe(display->dev, ret,
|
|
+ "Failed to build keymap\n");
|
|
+
|
|
+ if (device_property_read_bool(display->dev, "autorepeat"))
|
|
+ __set_bit(EV_REP, input->evbit);
|
|
+
|
|
+ input_setup_polling(input, tm16xx_keypad_poll);
|
|
+ input_set_poll_interval(input, poll_interval);
|
|
+ ret = input_register_device(input);
|
|
+ if (ret)
|
|
+ return dev_err_probe(display->dev, ret,
|
|
+ "Failed to register input device\n");
|
|
+
|
|
+ return 0;
|
|
+}
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 23 Aug 2025 21:26:59 -0400
|
|
Subject: auxdisplay: TM16xx: Add support for I2C-based controllers
|
|
|
|
Add support for TM16xx-compatible auxiliary display controllers connected
|
|
via the I2C bus.
|
|
|
|
The implementation includes:
|
|
- I2C driver registration and initialization
|
|
- Probe/remove logic for I2C devices
|
|
- Controller-specific handling and communication sequences
|
|
- Integration with the TM16xx core driver for common functionality
|
|
|
|
This allows platforms using TM16xx or compatible controllers over I2C to be
|
|
managed by the TM16xx driver infrastructure.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/Kconfig | 16 +
|
|
drivers/auxdisplay/Makefile | 1 +
|
|
drivers/auxdisplay/tm16xx_i2c.c | 332 ++++++++++
|
|
3 files changed, 349 insertions(+)
|
|
|
|
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Kconfig
|
|
+++ b/drivers/auxdisplay/Kconfig
|
|
@@ -544,6 +544,22 @@ config TM16XX_KEYPAD
|
|
help
|
|
Enable keyscan support for TM16XX driver.
|
|
|
|
+config TM16XX_I2C
|
|
+ tristate "TM16XX-compatible I2C 7-segment LED controllers with keyscan"
|
|
+ depends on I2C
|
|
+ select TM16XX
|
|
+ help
|
|
+ This driver supports the following TM16XX compatible
|
|
+ I2C (2-wire) 7-segment led display chips:
|
|
+ - Titanmec: TM1650
|
|
+ - Fuda Hisi: FD650, FD655, FD6551
|
|
+ - i-Core: AiP650
|
|
+ - Winrise: HBS658
|
|
+
|
|
+ To compile this driver as a module, choose M here: the module
|
|
+ will be called tm16xx_i2c and you will also get tm16xx for the
|
|
+ core module.
|
|
+
|
|
#
|
|
# Character LCD with non-conforming interface section
|
|
#
|
|
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Makefile
|
|
+++ b/drivers/auxdisplay/Makefile
|
|
@@ -19,3 +19,4 @@ obj-$(CONFIG_SEG_LED_GPIO) += seg-led-gpio.o
|
|
obj-$(CONFIG_TM16XX) += tm16xx.o
|
|
tm16xx-y += tm16xx_core.o
|
|
tm16xx-$(CONFIG_TM16XX_KEYPAD) += tm16xx_keypad.o
|
|
+obj-$(CONFIG_TM16XX_I2C) += tm16xx_i2c.o
|
|
diff --git a/drivers/auxdisplay/tm16xx_i2c.c b/drivers/auxdisplay/tm16xx_i2c.c
|
|
new file mode 100644
|
|
index 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/drivers/auxdisplay/tm16xx_i2c.c
|
|
@@ -0,0 +1,332 @@
|
|
+// 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) 2025 Jean-François Lessard
|
|
+ */
|
|
+
|
|
+#include <linux/bitfield.h>
|
|
+#include <linux/device.h>
|
|
+#include <linux/i2c.h>
|
|
+#include <linux/mod_devicetable.h>
|
|
+#include <linux/module.h>
|
|
+
|
|
+#include "tm16xx.h"
|
|
+
|
|
+static int tm16xx_i2c_probe(struct i2c_client *client)
|
|
+{
|
|
+ const struct tm16xx_controller *controller;
|
|
+ struct tm16xx_display *display;
|
|
+ int ret;
|
|
+
|
|
+ controller = i2c_get_match_data(client);
|
|
+ if (!controller)
|
|
+ return -EINVAL;
|
|
+
|
|
+ display = devm_kzalloc(&client->dev, sizeof(*display), GFP_KERNEL);
|
|
+ if (!display)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ display->dev = &client->dev;
|
|
+ display->controller = controller;
|
|
+
|
|
+ i2c_set_clientdata(client, display);
|
|
+
|
|
+ ret = tm16xx_probe(display);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void tm16xx_i2c_remove(struct i2c_client *client)
|
|
+{
|
|
+ struct tm16xx_display *display = i2c_get_clientdata(client);
|
|
+
|
|
+ tm16xx_remove(display);
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_i2c_write() - I2C write helper for controller
|
|
+ * @display: pointer to tm16xx_display structure
|
|
+ * @data: command and data bytes to send
|
|
+ * @len: number of bytes in @data
|
|
+ *
|
|
+ * Return: 0 on success, negative error code on failure
|
|
+ */
|
|
+static int tm16xx_i2c_write(struct tm16xx_display *display, u8 *data, size_t len)
|
|
+{
|
|
+ struct i2c_client *i2c = to_i2c_client(display->dev);
|
|
+
|
|
+ /* expected sequence: S Command [A] Data [A] P */
|
|
+ struct i2c_msg msg = {
|
|
+ .addr = data[0] >> 1,
|
|
+ .flags = 0,
|
|
+ .len = len - 1,
|
|
+ .buf = &data[1],
|
|
+ };
|
|
+ int ret;
|
|
+
|
|
+ ret = i2c_transfer(i2c->adapter, &msg, 1);
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ return (ret == 1) ? 0 : -EIO;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_i2c_read() - I2C read helper for controller
|
|
+ * @display: pointer to tm16xx_display structure
|
|
+ * @cmd: command/address byte to send before reading
|
|
+ * @data: buffer to receive data
|
|
+ * @len: number of bytes to read into @data
|
|
+ *
|
|
+ * Return: 0 on success, negative error code on failure
|
|
+ */
|
|
+static int tm16xx_i2c_read(struct tm16xx_display *display, u8 cmd, u8 *data, size_t len)
|
|
+{
|
|
+ struct i2c_client *i2c = to_i2c_client(display->dev);
|
|
+
|
|
+ /* expected sequence: S Command [A] [Data] [A] P */
|
|
+ struct i2c_msg msgs[1] = {{
|
|
+ .addr = cmd >> 1,
|
|
+ .flags = I2C_M_RD | I2C_M_NO_RD_ACK,
|
|
+ .len = len,
|
|
+ .buf = data,
|
|
+ }};
|
|
+ int ret;
|
|
+
|
|
+ ret = i2c_transfer(i2c->adapter, msgs, ARRAY_SIZE(msgs));
|
|
+ if (ret < 0)
|
|
+ return ret;
|
|
+
|
|
+ return (ret == ARRAY_SIZE(msgs)) ? 0 : -EIO;
|
|
+}
|
|
+
|
|
+/* I2C controller-specific functions */
|
|
+static int tm1650_init(struct tm16xx_display *display)
|
|
+{
|
|
+ const enum led_brightness brightness = display->main_led.brightness;
|
|
+ u8 cmds[2];
|
|
+
|
|
+ cmds[0] = TM1650_CMD_CTRL;
|
|
+ cmds[1] = TM16XX_CTRL_BRIGHTNESS(brightness, brightness, TM1650) |
|
|
+ TM1650_CTRL_SEG8_MODE;
|
|
+
|
|
+ return tm16xx_i2c_write(display, cmds, ARRAY_SIZE(cmds));
|
|
+}
|
|
+
|
|
+static int tm1650_data(struct tm16xx_display *display, u8 index,
|
|
+ unsigned int grid)
|
|
+{
|
|
+ u8 cmds[2];
|
|
+
|
|
+ cmds[0] = TM1650_CMD_ADDR + index * 2;
|
|
+ cmds[1] = grid; /* SEG 1 to 8 */
|
|
+
|
|
+ return tm16xx_i2c_write(display, cmds, ARRAY_SIZE(cmds));
|
|
+}
|
|
+
|
|
+static int tm1650_keys(struct tm16xx_display *display)
|
|
+{
|
|
+ int row, col;
|
|
+ bool pressed;
|
|
+ u8 keycode;
|
|
+ int ret;
|
|
+
|
|
+ ret = tm16xx_i2c_read(display, TM1650_CMD_READ, &keycode, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ if (keycode == 0x00 || keycode == 0xFF)
|
|
+ return -EINVAL;
|
|
+
|
|
+ row = FIELD_GET(TM1650_KEY_ROW_MASK, keycode);
|
|
+ pressed = FIELD_GET(TM1650_KEY_DOWN_MASK, keycode) != 0;
|
|
+ if ((keycode & TM1650_KEY_COMBINED) == TM1650_KEY_COMBINED) {
|
|
+ tm16xx_set_key(display, row, 0, pressed);
|
|
+ tm16xx_set_key(display, row, 1, pressed);
|
|
+ } else {
|
|
+ col = FIELD_GET(TM1650_KEY_COL_MASK, keycode);
|
|
+ tm16xx_set_key(display, row, col, pressed);
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int fd655_init(struct tm16xx_display *display)
|
|
+{
|
|
+ const enum led_brightness brightness = display->main_led.brightness;
|
|
+ u8 cmds[2];
|
|
+
|
|
+ cmds[0] = FD655_CMD_CTRL;
|
|
+ cmds[1] = TM16XX_CTRL_BRIGHTNESS(brightness, brightness % 3, FD655);
|
|
+
|
|
+ return tm16xx_i2c_write(display, cmds, ARRAY_SIZE(cmds));
|
|
+}
|
|
+
|
|
+static int fd655_data(struct tm16xx_display *display, u8 index,
|
|
+ unsigned int grid)
|
|
+{
|
|
+ u8 cmds[2];
|
|
+
|
|
+ cmds[0] = FD655_CMD_ADDR + index * 2;
|
|
+ cmds[1] = grid; /* SEG 1 to 8 */
|
|
+
|
|
+ return tm16xx_i2c_write(display, cmds, ARRAY_SIZE(cmds));
|
|
+}
|
|
+
|
|
+static int fd6551_init(struct tm16xx_display *display)
|
|
+{
|
|
+ const enum led_brightness brightness = display->main_led.brightness;
|
|
+ u8 cmds[2];
|
|
+
|
|
+ cmds[0] = FD6551_CMD_CTRL;
|
|
+ cmds[1] = TM16XX_CTRL_BRIGHTNESS(brightness, ~(brightness - 1), FD6551);
|
|
+
|
|
+ return tm16xx_i2c_write(display, cmds, ARRAY_SIZE(cmds));
|
|
+}
|
|
+
|
|
+static void hbs658_swap_nibbles(u8 *data, size_t len)
|
|
+{
|
|
+ for (size_t i = 0; i < len; i++)
|
|
+ data[i] = (data[i] << 4) | (data[i] >> 4);
|
|
+}
|
|
+
|
|
+static int hbs658_init(struct tm16xx_display *display)
|
|
+{
|
|
+ const enum led_brightness brightness = display->main_led.brightness;
|
|
+ u8 cmd;
|
|
+ int ret;
|
|
+
|
|
+ /* Set data command */
|
|
+ cmd = TM16XX_CMD_WRITE | TM16XX_DATA_ADDR_AUTO;
|
|
+ hbs658_swap_nibbles(&cmd, 1);
|
|
+ ret = tm16xx_i2c_write(display, &cmd, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* Set control command with brightness */
|
|
+ cmd = TM16XX_CMD_CTRL |
|
|
+ TM16XX_CTRL_BRIGHTNESS(brightness, brightness - 1, TM16XX);
|
|
+ hbs658_swap_nibbles(&cmd, 1);
|
|
+ ret = tm16xx_i2c_write(display, &cmd, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int hbs658_data(struct tm16xx_display *display, u8 index,
|
|
+ unsigned int grid)
|
|
+{
|
|
+ u8 cmds[2];
|
|
+
|
|
+ cmds[0] = TM16XX_CMD_ADDR + index * 2;
|
|
+ cmds[1] = grid;
|
|
+
|
|
+ hbs658_swap_nibbles(cmds, ARRAY_SIZE(cmds));
|
|
+ return tm16xx_i2c_write(display, cmds, ARRAY_SIZE(cmds));
|
|
+}
|
|
+
|
|
+static int hbs658_keys(struct tm16xx_display *display)
|
|
+{
|
|
+ u8 cmd, keycode;
|
|
+ int col;
|
|
+ int ret;
|
|
+
|
|
+ cmd = TM16XX_CMD_READ;
|
|
+ hbs658_swap_nibbles(&cmd, 1);
|
|
+ ret = tm16xx_i2c_read(display, cmd, &keycode, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ hbs658_swap_nibbles(&keycode, 1);
|
|
+
|
|
+ if (keycode != 0xFF) {
|
|
+ col = FIELD_GET(HBS658_KEY_COL_MASK, keycode);
|
|
+ tm16xx_set_key(display, 0, col, true);
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+/* I2C controller definitions */
|
|
+static const struct tm16xx_controller tm1650_controller = {
|
|
+ .max_grids = 4,
|
|
+ .max_segments = 8,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 4,
|
|
+ .max_key_cols = 7,
|
|
+ .init = tm1650_init,
|
|
+ .data = tm1650_data,
|
|
+ .keys = tm1650_keys,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller fd655_controller = {
|
|
+ .max_grids = 5,
|
|
+ .max_segments = 7,
|
|
+ .max_brightness = 3,
|
|
+ .max_key_rows = 5,
|
|
+ .max_key_cols = 7,
|
|
+ .init = fd655_init,
|
|
+ .data = fd655_data,
|
|
+ .keys = tm1650_keys,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller fd6551_controller = {
|
|
+ .max_grids = 5,
|
|
+ .max_segments = 7,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 0,
|
|
+ .max_key_cols = 0,
|
|
+ .init = fd6551_init,
|
|
+ .data = fd655_data,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller hbs658_controller = {
|
|
+ .max_grids = 5,
|
|
+ .max_segments = 8,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 1,
|
|
+ .max_key_cols = 8,
|
|
+ .init = hbs658_init,
|
|
+ .data = hbs658_data,
|
|
+ .keys = hbs658_keys,
|
|
+};
|
|
+
|
|
+static const struct of_device_id tm16xx_i2c_of_match[] = {
|
|
+ { .compatible = "titanmec,tm1650", .data = &tm1650_controller },
|
|
+ { .compatible = "fdhisi,fd6551", .data = &fd6551_controller },
|
|
+ { .compatible = "fdhisi,fd655", .data = &fd655_controller },
|
|
+ { .compatible = "winrise,hbs658", .data = &hbs658_controller },
|
|
+ { /* sentinel */ }
|
|
+};
|
|
+MODULE_DEVICE_TABLE(of, tm16xx_i2c_of_match);
|
|
+
|
|
+static const struct i2c_device_id tm16xx_i2c_id[] = {
|
|
+ { "tm1650", (kernel_ulong_t)&tm1650_controller },
|
|
+ { "fd6551", (kernel_ulong_t)&fd6551_controller },
|
|
+ { "fd655", (kernel_ulong_t)&fd655_controller },
|
|
+ { "hbs658", (kernel_ulong_t)&hbs658_controller },
|
|
+ { /* sentinel */ }
|
|
+};
|
|
+MODULE_DEVICE_TABLE(i2c, tm16xx_i2c_id);
|
|
+
|
|
+static struct i2c_driver tm16xx_i2c_driver = {
|
|
+ .driver = {
|
|
+ .name = "tm16xx-i2c",
|
|
+ .of_match_table = tm16xx_i2c_of_match,
|
|
+ },
|
|
+ .probe = tm16xx_i2c_probe,
|
|
+ .remove = tm16xx_i2c_remove,
|
|
+ .shutdown = tm16xx_i2c_remove,
|
|
+ .id_table = tm16xx_i2c_id,
|
|
+};
|
|
+module_i2c_driver(tm16xx_i2c_driver);
|
|
+
|
|
+MODULE_AUTHOR("Jean-François Lessard");
|
|
+MODULE_DESCRIPTION("TM16xx-i2c LED Display Controllers");
|
|
+MODULE_LICENSE("GPL");
|
|
+MODULE_IMPORT_NS("TM16XX");
|
|
--
|
|
Armbian
|
|
|
|
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
|
From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Lessard?= <jefflessard3@gmail.com>
|
|
Date: Sat, 23 Aug 2025 21:55:14 -0400
|
|
Subject: auxdisplay: TM16xx: Add support for SPI-based controllers
|
|
|
|
Add support for TM16xx-compatible auxiliary display controllers connected
|
|
via the SPI bus.
|
|
|
|
The implementation includes:
|
|
- SPI driver registration and initialization
|
|
- Probe/remove logic for SPI devices
|
|
- Controller-specific handling and communication sequences
|
|
- Integration with the TM16xx core driver for common functionality
|
|
|
|
This allows platforms using TM16xx or compatible controllers over SPI to be
|
|
managed by the TM16xx driver infrastructure.
|
|
|
|
Signed-off-by: Jean-Francois Lessard <jefflessard3@gmail.com>
|
|
---
|
|
drivers/auxdisplay/Kconfig | 16 +
|
|
drivers/auxdisplay/Makefile | 1 +
|
|
drivers/auxdisplay/tm16xx_spi.c | 397 ++++++++++
|
|
3 files changed, 414 insertions(+)
|
|
|
|
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Kconfig
|
|
+++ b/drivers/auxdisplay/Kconfig
|
|
@@ -560,6 +560,22 @@ config TM16XX_I2C
|
|
will be called tm16xx_i2c and you will also get tm16xx for the
|
|
core module.
|
|
|
|
+config TM16XX_SPI
|
|
+ tristate "TM16XX-compatible SPI 7-segment LED controllers with keyscan"
|
|
+ depends on SPI
|
|
+ select TM16XX
|
|
+ help
|
|
+ This driver supports the following TM16XX compatible
|
|
+ SPI (3-wire) 7-segment led display chips:
|
|
+ - Titanmec: TM1618, TM1620, TM1628, TM1638
|
|
+ - Fuda Hisi: FD620, FD628
|
|
+ - i-Core: AiP1618, AiP1628
|
|
+ - Princeton: PT6964
|
|
+
|
|
+ To compile this driver as a module, choose M here: the module
|
|
+ will be called tm16xx_spi and you will also get tm16xx for the
|
|
+ core module.
|
|
+
|
|
#
|
|
# Character LCD with non-conforming interface section
|
|
#
|
|
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
|
|
index 111111111111..222222222222 100644
|
|
--- a/drivers/auxdisplay/Makefile
|
|
+++ b/drivers/auxdisplay/Makefile
|
|
@@ -20,3 +20,4 @@ obj-$(CONFIG_TM16XX) += tm16xx.o
|
|
tm16xx-y += tm16xx_core.o
|
|
tm16xx-$(CONFIG_TM16XX_KEYPAD) += tm16xx_keypad.o
|
|
obj-$(CONFIG_TM16XX_I2C) += tm16xx_i2c.o
|
|
+obj-$(CONFIG_TM16XX_SPI) += tm16xx_spi.o
|
|
diff --git a/drivers/auxdisplay/tm16xx_spi.c b/drivers/auxdisplay/tm16xx_spi.c
|
|
new file mode 100644
|
|
index 000000000000..111111111111
|
|
--- /dev/null
|
|
+++ b/drivers/auxdisplay/tm16xx_spi.c
|
|
@@ -0,0 +1,397 @@
|
|
+// 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) 2025 Jean-François Lessard
|
|
+ */
|
|
+
|
|
+#include <linux/bitfield.h>
|
|
+#include <linux/device.h>
|
|
+#include <linux/mod_devicetable.h>
|
|
+#include <linux/module.h>
|
|
+#include <linux/spi/spi.h>
|
|
+
|
|
+#include "tm16xx.h"
|
|
+
|
|
+#define TM16XX_SPI_BUFFER_SIZE 8
|
|
+#define TM16XX_SPI_TWAIT_US 2
|
|
+
|
|
+static int tm16xx_spi_probe(struct spi_device *spi)
|
|
+{
|
|
+ const struct tm16xx_controller *controller;
|
|
+ struct tm16xx_display *display;
|
|
+ int ret;
|
|
+
|
|
+ controller = spi_get_device_match_data(spi);
|
|
+ if (!controller)
|
|
+ return -EINVAL;
|
|
+
|
|
+ display = devm_kzalloc(&spi->dev, sizeof(*display), GFP_KERNEL);
|
|
+ if (!display)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ /* Allocate DMA-safe buffer */
|
|
+ display->spi_buffer = devm_kzalloc(&spi->dev, TM16XX_SPI_BUFFER_SIZE, GFP_KERNEL);
|
|
+ if (!display->spi_buffer)
|
|
+ return -ENOMEM;
|
|
+
|
|
+ display->dev = &spi->dev;
|
|
+ display->controller = controller;
|
|
+
|
|
+ spi_set_drvdata(spi, display);
|
|
+
|
|
+ ret = tm16xx_probe(display);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static void tm16xx_spi_remove(struct spi_device *spi)
|
|
+{
|
|
+ struct tm16xx_display *display = spi_get_drvdata(spi);
|
|
+
|
|
+ tm16xx_remove(display);
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_spi_read() - SPI read helper for controller
|
|
+ * @display: pointer to tm16xx_display
|
|
+ * @cmd: command to send
|
|
+ * @cmd_len: length of command
|
|
+ * @data: buffer for received data
|
|
+ * @data_len: length of data to read
|
|
+ *
|
|
+ * Return: 0 on success, negative error code on failure
|
|
+ */
|
|
+static int tm16xx_spi_read(struct tm16xx_display *display, u8 *cmd,
|
|
+ size_t cmd_len, u8 *data, size_t data_len)
|
|
+{
|
|
+ struct spi_device *spi = to_spi_device(display->dev);
|
|
+ struct spi_message msg;
|
|
+ int ret;
|
|
+
|
|
+ /* If STB is high during transmission, command is invalid.
|
|
+ * Reading requires a minimum 2 microseconds wait (Twait)
|
|
+ * after the 8th CLK rising edge before reading on falling edge.
|
|
+ */
|
|
+ struct spi_transfer xfers[2] = {
|
|
+ {
|
|
+ .tx_buf = cmd,
|
|
+ .len = cmd_len,
|
|
+ .cs_change = 0, /* NO CS toggle */
|
|
+ .delay.value = TM16XX_SPI_TWAIT_US,
|
|
+ .delay.unit = SPI_DELAY_UNIT_USECS,
|
|
+ }, {
|
|
+ .rx_buf = data,
|
|
+ .len = data_len,
|
|
+ }
|
|
+ };
|
|
+
|
|
+ spi_message_init_with_transfers(&msg, xfers, ARRAY_SIZE(xfers));
|
|
+
|
|
+ ret = spi_sync(spi, &msg);
|
|
+
|
|
+ return ret;
|
|
+}
|
|
+
|
|
+/**
|
|
+ * tm16xx_spi_write() - SPI write helper for controller
|
|
+ * @display: pointer to tm16xx_display
|
|
+ * @data: data to write
|
|
+ * @len: number of bytes to write
|
|
+ *
|
|
+ * Return: 0 on success, negative error code on failure
|
|
+ */
|
|
+static int tm16xx_spi_write(struct tm16xx_display *display, u8 *data, size_t len)
|
|
+{
|
|
+ struct spi_device *spi = to_spi_device(display->dev);
|
|
+
|
|
+ return spi_write(spi, data, len);
|
|
+}
|
|
+
|
|
+/* SPI controller-specific functions */
|
|
+static int tm1628_init(struct tm16xx_display *display)
|
|
+{
|
|
+ const enum led_brightness brightness = display->main_led.brightness;
|
|
+ const u8 num_hwgrid = display->num_hwgrid;
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+ int ret;
|
|
+
|
|
+ /* Set mode command based on grid count */
|
|
+ cmd[0] = TM16XX_CMD_MODE;
|
|
+ if (num_hwgrid <= 4)
|
|
+ cmd[0] |= TM16XX_MODE_4GRIDS;
|
|
+ else if (num_hwgrid == 5)
|
|
+ cmd[0] |= TM16XX_MODE_5GRIDS;
|
|
+ else if (num_hwgrid == 6)
|
|
+ cmd[0] |= TM16XX_MODE_6GRIDS;
|
|
+ else
|
|
+ cmd[0] |= TM16XX_MODE_7GRIDS;
|
|
+
|
|
+ ret = tm16xx_spi_write(display, cmd, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* Set data command */
|
|
+ cmd[0] = TM16XX_CMD_WRITE | TM16XX_DATA_ADDR_AUTO;
|
|
+ ret = tm16xx_spi_write(display, cmd, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* Set control command with brightness */
|
|
+ cmd[0] = TM16XX_CMD_CTRL |
|
|
+ TM16XX_CTRL_BRIGHTNESS(brightness, brightness - 1, TM16XX);
|
|
+ ret = tm16xx_spi_write(display, cmd, 1);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int tm1618_data(struct tm16xx_display *display, u8 index,
|
|
+ unsigned int grid)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_ADDR + index * 2;
|
|
+ cmd[1] = FIELD_GET(TM1618_BYTE1_MASK, grid);
|
|
+ cmd[2] = FIELD_GET(TM1618_BYTE2_MASK, grid) << TM1618_BYTE2_SHIFT;
|
|
+
|
|
+ return tm16xx_spi_write(display, cmd, 3);
|
|
+}
|
|
+
|
|
+static int tm1628_data(struct tm16xx_display *display, u8 index,
|
|
+ unsigned int grid)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_ADDR + index * 2;
|
|
+ cmd[1] = FIELD_GET(TM1628_BYTE1_MASK, grid);
|
|
+ cmd[2] = FIELD_GET(TM1628_BYTE2_MASK, grid);
|
|
+
|
|
+ return tm16xx_spi_write(display, cmd, 3);
|
|
+}
|
|
+
|
|
+static int tm1628_keys(struct tm16xx_display *display)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+ u8 *codes = display->spi_buffer;
|
|
+ unsigned int i;
|
|
+ int bit, byte;
|
|
+ bool value;
|
|
+ int ret;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_READ;
|
|
+ ret = tm16xx_spi_read(display, cmd, 1, codes, TM1628_KEY_READ_LEN);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* prevent false readings */
|
|
+ for (i = 0; i < TM1628_KEY_READ_LEN; i++) {
|
|
+ if (codes[i] & ~TM1628_KEY_MASK)
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ tm16xx_for_each_key(display, row, col) {
|
|
+ byte = col >> 1;
|
|
+ bit = row + ((col & 1) * 3);
|
|
+ value = !!(codes[byte] & BIT(bit));
|
|
+
|
|
+ tm16xx_set_key(display, row, col, value);
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int tm1638_keys(struct tm16xx_display *display)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+ u8 *codes = display->spi_buffer;
|
|
+ unsigned int i;
|
|
+ int bit, byte;
|
|
+ bool value;
|
|
+ int ret;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_READ;
|
|
+ ret = tm16xx_spi_read(display, cmd, 1, codes, TM1638_KEY_READ_LEN);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* prevent false readings */
|
|
+ for (i = 0; i < TM1638_KEY_READ_LEN; i++) {
|
|
+ if (codes[i] & ~TM1638_KEY_MASK)
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ tm16xx_for_each_key(display, row, col) {
|
|
+ byte = col >> 1;
|
|
+ bit = (2 - row) + ((col & 1) << 2);
|
|
+ value = !!(codes[byte] & BIT(bit));
|
|
+
|
|
+ tm16xx_set_key(display, row, col, value);
|
|
+ }
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int tm1618_keys(struct tm16xx_display *display)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+ u8 *codes = display->spi_buffer;
|
|
+ unsigned int i;
|
|
+ int ret;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_READ;
|
|
+ ret = tm16xx_spi_read(display, cmd, 1, codes, TM1618_KEY_READ_LEN);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* prevent false readings */
|
|
+ for (i = 0; i < TM1618_KEY_READ_LEN; i++) {
|
|
+ if (codes[i] & ~TM1618_KEY_MASK)
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ tm16xx_set_key(display, 0, 0, !!(codes[0] & BIT(1)));
|
|
+ tm16xx_set_key(display, 0, 1, !!(codes[0] & BIT(4)));
|
|
+ tm16xx_set_key(display, 0, 2, !!(codes[1] & BIT(1)));
|
|
+ tm16xx_set_key(display, 0, 3, !!(codes[1] & BIT(4)));
|
|
+ tm16xx_set_key(display, 0, 4, !!(codes[2] & BIT(1)));
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+static int fd620_data(struct tm16xx_display *display, u8 index,
|
|
+ unsigned int grid)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_ADDR + index * 2;
|
|
+ cmd[1] = FIELD_GET(FD620_BYTE1_MASK, grid);
|
|
+ cmd[2] = FIELD_GET(FD620_BYTE2_MASK, grid) << FD620_BYTE2_SHIFT;
|
|
+
|
|
+ return tm16xx_spi_write(display, cmd, 3);
|
|
+}
|
|
+
|
|
+static int fd620_keys(struct tm16xx_display *display)
|
|
+{
|
|
+ u8 *cmd = display->spi_buffer;
|
|
+ u8 *codes = display->spi_buffer;
|
|
+ unsigned int i;
|
|
+ int ret;
|
|
+
|
|
+ cmd[0] = TM16XX_CMD_READ;
|
|
+ ret = tm16xx_spi_read(display, cmd, 1, codes, FD620_KEY_READ_LEN);
|
|
+ if (ret)
|
|
+ return ret;
|
|
+
|
|
+ /* prevent false readings */
|
|
+ for (i = 0; i < FD620_KEY_READ_LEN; i++) {
|
|
+ if (codes[i] & ~FD620_KEY_MASK)
|
|
+ return -EINVAL;
|
|
+ }
|
|
+
|
|
+ tm16xx_set_key(display, 0, 0, codes[0] & BIT(0));
|
|
+ tm16xx_set_key(display, 0, 1, codes[0] & BIT(3));
|
|
+ tm16xx_set_key(display, 0, 2, codes[1] & BIT(0));
|
|
+ tm16xx_set_key(display, 0, 3, codes[1] & BIT(3));
|
|
+ tm16xx_set_key(display, 0, 4, codes[2] & BIT(0));
|
|
+ tm16xx_set_key(display, 0, 5, codes[2] & BIT(3));
|
|
+ tm16xx_set_key(display, 0, 6, codes[3] & BIT(0));
|
|
+
|
|
+ return 0;
|
|
+}
|
|
+
|
|
+/* SPI controller definitions */
|
|
+static const struct tm16xx_controller tm1618_controller = {
|
|
+ .max_grids = 7,
|
|
+ .max_segments = 8,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 1,
|
|
+ .max_key_cols = 5,
|
|
+ .init = tm1628_init,
|
|
+ .data = tm1618_data,
|
|
+ .keys = tm1618_keys,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller tm1620_controller = {
|
|
+ .max_grids = 6,
|
|
+ .max_segments = 10,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 0,
|
|
+ .max_key_cols = 0,
|
|
+ .init = tm1628_init,
|
|
+ .data = tm1628_data,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller tm1628_controller = {
|
|
+ .max_grids = 7,
|
|
+ .max_segments = 14, /* seg 11 unused */
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 2,
|
|
+ .max_key_cols = 10,
|
|
+ .init = tm1628_init,
|
|
+ .data = tm1628_data,
|
|
+ .keys = tm1628_keys,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller tm1638_controller = {
|
|
+ .max_grids = 8,
|
|
+ .max_segments = 10,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 3,
|
|
+ .max_key_cols = 8,
|
|
+ .init = tm1628_init,
|
|
+ .data = tm1628_data,
|
|
+ .keys = tm1638_keys,
|
|
+};
|
|
+
|
|
+static const struct tm16xx_controller fd620_controller = {
|
|
+ .max_grids = 5,
|
|
+ .max_segments = 8,
|
|
+ .max_brightness = 8,
|
|
+ .max_key_rows = 1,
|
|
+ .max_key_cols = 7,
|
|
+ .init = tm1628_init,
|
|
+ .data = fd620_data,
|
|
+ .keys = fd620_keys,
|
|
+};
|
|
+
|
|
+static const struct of_device_id tm16xx_spi_of_match[] = {
|
|
+ { .compatible = "titanmec,tm1618", .data = &tm1618_controller },
|
|
+ { .compatible = "titanmec,tm1620", .data = &tm1620_controller },
|
|
+ { .compatible = "titanmec,tm1628", .data = &tm1628_controller },
|
|
+ { .compatible = "titanmec,tm1638", .data = &tm1638_controller },
|
|
+ { .compatible = "fdhisi,fd620", .data = &fd620_controller },
|
|
+ { /* sentinel */ }
|
|
+};
|
|
+MODULE_DEVICE_TABLE(of, tm16xx_spi_of_match);
|
|
+
|
|
+static const struct spi_device_id tm16xx_spi_id[] = {
|
|
+ { "tm1618", (kernel_ulong_t)&tm1618_controller },
|
|
+ { "tm1620", (kernel_ulong_t)&tm1620_controller },
|
|
+ { "tm1628", (kernel_ulong_t)&tm1628_controller },
|
|
+ { "tm1638", (kernel_ulong_t)&tm1638_controller },
|
|
+ { "fd620", (kernel_ulong_t)&fd620_controller },
|
|
+ { /* sentinel */ }
|
|
+};
|
|
+MODULE_DEVICE_TABLE(spi, tm16xx_spi_id);
|
|
+
|
|
+static struct spi_driver tm16xx_spi_driver = {
|
|
+ .driver = {
|
|
+ .name = "tm16xx-spi",
|
|
+ .of_match_table = tm16xx_spi_of_match,
|
|
+ },
|
|
+ .probe = tm16xx_spi_probe,
|
|
+ .remove = tm16xx_spi_remove,
|
|
+ .shutdown = tm16xx_spi_remove,
|
|
+ .id_table = tm16xx_spi_id,
|
|
+};
|
|
+module_spi_driver(tm16xx_spi_driver);
|
|
+
|
|
+MODULE_AUTHOR("Jean-François Lessard");
|
|
+MODULE_DESCRIPTION("TM16xx-spi LED Display Controllers");
|
|
+MODULE_LICENSE("GPL");
|
|
+MODULE_IMPORT_NS("TM16XX");
|
|
--
|
|
Armbian
|
|
|