devicetree.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver
@ 2025-11-21 14:59 Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 1/7] dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton, winrise, wxicore Jean-François Lessard
                   ` (8 more replies)
  0 siblings, 9 replies; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree

This series adds mainline kernel support for TM16xx family LED matrix
controllers and compatible chips, widely used in auxiliary displays on TV
boxes and embedded devices.

Many consumer devices, particularly TV boxes, use auxiliary displays based
on TM16xx controllers to show status information such as time, network
connectivity and system state. Currently, there is no mainline kernel
support for these displays, forcing users to rely on out-of-tree drivers
or userspace solutions that access hardware interfaces directly.

This driver provides unified TM16xx support through the LED subsystem with
both I2C and SPI communication protocols. It integrates with the LED class
framework, enabling control via standard sysfs interfaces and LED triggers,
while supporting keypad input when hardware connections are available.

The driver supports multiple controller families from various vendors:
- Titan Micro Electronics: TM1618, TM1620, TM1628, TM1638, TM1650
- Fuda Hisi Microelectronics: FD620, FD628, FD650, FD655, FD6551
- i-Core Electronics: AiP650, AiP1618, AiP1628
- Princeton Technology: PT6964
- Winrise Technology: HBS658

Key features:
- 7-segment display support with flexible digit/segment mapping
- Individual LED icon control through LED class devices
- Optional keypad scanning with configurable key mapping
- Device tree configuration for board-specific wiring layouts
- LED trigger integration for automatic system event indication
- I2C and SPI protocol support depending on controller interface

Device tree bindings describe board-specific display wiring since
controllers are layout-agnostic. The bindings use separate 'digits' and
'leds' containers with specific addressing schemes to accommodate the
hardware's grid/segment matrix organization.

Tested on multiple ARM TV boxes (H96 Max, Magicsee N5, Tanix TX3 Mini,
Tanix TX6, X92, X96 Max) across different SoC platforms (Rockchip, Amlogic,
Allwinner) in both I2C and SPI configurations.

User space utilities available at:
https://github.com/jefflessard/tm16xx-display

Dependencies:
- linedisp_attach()/_detach() infrastructure introduced in patch series:
 "auxdisplay: linedisp: support attribute attachment to auxdisplay devices"
- fwnode_for_each_available_child_node_scoped() from patch series:
 "device property: Add scoped fwnode child node iterators"

Note: This driver is placed in drivers/auxdisplay rather than drivers/leds
based on previous maintainer guidance. LED maintainer Pavel Machek
recommended auxdisplay for TM1628-based display drivers:
https://lore.kernel.org/linux-devicetree/20200226130300.GB2800@duo.ucw.cz/

Regmap Evaluation:
TM16xx controllers use command-based 2-wire/3-wire protocols that share
sufficient commonalities with I2C/SPI to leverage their subsystems, but
are not fully compliant with standard register-based access patterns:
- TM1650 example: 0x48 is a control command while 0x4F is a keyscan
  command. These appear as adjacent I2C "addresses" but are distinct
  commands with different data directions and payloads, not read/write
  pairs of the same register.
- TM1628 example: Initialization requires coordinated sequences followed
  by indexed data writes. Single regmap read/write calls cannot express
  these multi-step transactions and timing constraints.
- Protocol requirements: I2C read operations require I2C_M_NO_RD_ACK flags;
  SPI write-then-read operations require mandatory inter-transfer delays
  and CS assertion across phases.

While regmap provides valuable synchronization, debugfs, and abstraction
benefits, standard I2C/SPI regmap buses cannot handle these requirements.

Custom regmap implementation is technically possible via IO accessors, but
demands complex command routing logic and only partially supports paging.
It would essentially recreate the existing controller functions while
forcing them into register semantics they don't naturally fit.

The current explicit I2C/SPI approach directly expresses the hardware's
actual command structure and maintains proper controller abstraction.

Changes in v6:
- core: Reduce indent level of fwnode children parsing
- core: Comment brightness properties handling
- core: Document concurrency model and non-devm resource management
- core: Remove apply label property comment
- core: Remove dev_err_probe for mutex init
- core: remove '0' from led_init_data initialization
- core: Merge tm16xx_display_value loops with embedded conditional
- core: Document flush_status error handling to flush operations
- core: Change scoped_guard to guard() in flush operations
- core: Return early on flush operations
- core: Format to single line within 100 char limit
- core: Drop tm16xx_probe/_remove kernel-doc
- core: Use %true/%false formatting in kernel-doc
- i2c: Consolidate FD655/FD6551 CMD_CTRL definitions
- all: Ensure RCS declarations
- all: Change EXPORT_SYMBOL_NS to _GPL
- all: Add missing header includes
- header: Add forward declaration for struct device
- header: Remove const qualifiers from controller fields
- Kconfig: Expand help text to ~3 lines minimum
- Kconfig: Add COMPILE_TEST for compile test coverage
- dt-bindings: Change units to hex pattern
- dt-bindings: Add led node description

Changes in v5:
- dt-bindings: set $ref: /schemas/leds/common.yaml# at the node level
- dt-bindings: add constraints to max_/default_brightness properties
- dt-bindings: clarify digit positions are numbered left-to-right
- dt-bindings: reorder the schema sections to 'dependencies',
              'required', 'allOf'
- dt-bindings: leds: add default-brightness to leds/common.yaml
- core: rename prfx to prefix in TM16XX_CTRL_BRIGHTNESS macro
- core: drop i2c/spi client union in favor of to_i2c_client/to_spi_device
- core: rename controller grids/segments to avoid 7-seg confusion
- core: remove tm16xx_digit_segment and simplify tm16xx_digit structs
- core: drop tm16xx sysfs attributes in favor of line-display library
- core: rename tm16xx_parse_dt to tm16xx_parse_fwnode
- core: replace manual child count with fwnode_get_child_node_count
- core: use __free(fwnode_handle) instead of fwnode_handle_put
- core: remove of.h include and duplicated logic of main led label
- core: use devm_ variant of mutex_init
- core: drop kernel-doc for well-established meaning functions
- i2c/spi: remove redundant NULL initializers
- i2c/spi: remove CONFIG_OF preprocessor conditions
- i2c/spi: drop usage of of_match_ptr
- i2c/spi: fix CONFIG_I2C=m, CONFIG_SPI=y, CONFIG_TM16XX=y edge case
           reported by kernel test robot (late v3 feedback)
- all: rely on explicit rather than transitive includes
- all: review signed types usage consistency
- all: use 'if (ret)' where there is no positive return
- all: apply relaxed line wrap, allowing over 80 column width
- all: remove info and debug messages
- all: update copyright year to 2025

Changes in v4:
- Split MAINTAINERS patch into each specific patch
- Document ABI of sysfs driver attributes
- Remove kernel-doc of obvious Linux core driver model APIs
- dt-bindings: Drop obvious comments that schema tells by itself
- dt-bindings: Gather canonical compatible strings in a single enum
- dt-bindings: Clarify top-level logical led DT node name/label property
- dt:bindings: Replace refs to input properties with allOf
- Split driver patch and code file for better reviewability
- Refactor into separate i2c and spi glue driver modules
- Drop driver name macro constant in favor of explicit string literals
- Revise to use bit shifts for values and GENMASK/BIT for bit positions
- Format TM16XX_CTRL_BRIGHTNESS on one line
- Drop default_value module param in favor of Kconfig compile time option
- Fix for_each_key name and expressions
- Replace manual mutex locking with scoped_guard
- Move scancode declaration to avoid mix with code
- Remove unnecessary ret initialization
- Remove ENOMEM error message
- Replace probe error messages by dev_err_probe
- Remove keypad failed probe cleanup to avoid devm anti-pattern confusion
- Switch to non-devm led registration to avoid anti-pattern confusion
- Replace u16 in favor of unsigned int for controller data

Changes in v3:
- Update vendor prefixes with documented rationale, in a single patch,
  per maintainer feedback
- Refine device tree bindings per maintainer feedback:
  * Update compatible string ordering and fallback logic
  * Improve YAML descriptions for clarity and 80-column wrapping
  * Replace digit-specific properties with clearer digits container node
  * Add required constraints for properties in container nodes
  * Clarify addressing schemes for LED icons and digits
  * Fix conditional SPI properties handling
  * Document rationale for spi-3wire property
  * Expand DT examples to cover typical and transposed display layouts
- Code reformat from clang-format to kernel style per maintainer feedback
- Fix conditional CONFIG_I2C/_SPI compilation issues per kernel test robot
- Add keypad scanning with configurable keymap (new feature)
- Add support for TM1638 controller extending hardware compatibility
- Add support for default and maximum brightness properties
- Fix multi-instance device handling and add optional label property
- Allocate DMA-safe SPI buffer for hardware compatibility
- Enhance error handling with comprehensive kernel-doc documentation
- Remove sysfs runtime reconfiguration, enforce device tree-only

Changes in v2:
- Fix duplicate label in dt-bindings examples
- Rename device tree property prefixes to use titanmec vendor prefix

Jean-François Lessard (7):
  dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton,
    winrise, wxicore
  dt-bindings: leds: add default-brightness property to common.yaml
  dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx
  auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver
  auxdisplay: TM16xx: Add keypad support for scanning matrix keys
  auxdisplay: TM16xx: Add support for I2C-based controllers
  auxdisplay: TM16xx: Add support for SPI-based controllers

 .../bindings/auxdisplay/titanmec,tm16xx.yaml  | 465 +++++++++++++++++
 .../devicetree/bindings/leds/common.yaml      |   6 +
 .../devicetree/bindings/vendor-prefixes.yaml  |  10 +
 MAINTAINERS                                   |  10 +
 drivers/auxdisplay/Kconfig                    |  53 ++
 drivers/auxdisplay/Makefile                   |   5 +
 drivers/auxdisplay/tm16xx.h                   | 200 +++++++
 drivers/auxdisplay/tm16xx_core.c              | 488 ++++++++++++++++++
 drivers/auxdisplay/tm16xx_i2c.c               | 333 ++++++++++++
 drivers/auxdisplay/tm16xx_keypad.c            | 192 +++++++
 drivers/auxdisplay/tm16xx_spi.c               | 398 ++++++++++++++
 11 files changed, 2160 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
 create mode 100644 drivers/auxdisplay/tm16xx.h
 create mode 100644 drivers/auxdisplay/tm16xx_core.c
 create mode 100644 drivers/auxdisplay/tm16xx_i2c.c
 create mode 100644 drivers/auxdisplay/tm16xx_keypad.c
 create mode 100644 drivers/auxdisplay/tm16xx_spi.c

-- 
2.43.0


^ permalink raw reply	[flat|nested] 14+ messages in thread

* [PATCH v6 1/7] dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton, winrise, wxicore
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 2/7] dt-bindings: leds: add default-brightness property to common.yaml Jean-François Lessard
                   ` (7 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree, Conor Dooley,
	Andreas Färber

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 Färber.

Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Jean-François Lessard <jefflessard3@gmail.com>
---
CC: Andreas Färber <afaerber@suse.de>

 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 f1d1882009ba..6dd77c9ef111 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,.*":
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v6 2/7] dt-bindings: leds: add default-brightness property to common.yaml
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 1/7] dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton, winrise, wxicore Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 3/7] dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx Jean-François Lessard
                   ` (6 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree

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.

Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Jean-François 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 274f83288a92..f4e44b33f56d 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,
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v6 3/7] dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 1/7] dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton, winrise, wxicore Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 2/7] dt-bindings: leds: add default-brightness property to common.yaml Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-12-04 15:42   ` Rob Herring
  2025-11-21 14:59 ` [PATCH v6 4/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (5 subsequent siblings)
  8 siblings, 1 reply; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree

Add documentation for TM16xx-compatible 7-segment LED display controllers
with keyscan.

Signed-off-by: Jean-François Lessard <jefflessard3@gmail.com>
---

Notes:
    The 'segments' property is intentionally not vendor-prefixed as it
    defines a generic hardware description concept applicable to any
    7-segment display controller. The property describes the fundamental
    grid/segment coordinate mapping that is controller-agnostic and could
    be reused by other LED matrix display bindings. Similar to how 'gpios'
    describes GPIO connections generically, 'segments' describes segment
    connections in a standardized way using uint32-matrix format.
    
    The property uses explicit coordinate pairs to handle real-world
    hardware variations. Some board manufacturers use standard layouts
    (same grid, different segments per digit) while others use transposed
    layouts (same segment, different grids per digit). The coordinate-pair
    approach accommodates both patterns without requiring separate arrays
    or boolean flags, as confirmed acceptable by DT maintainers.

    Retained 'properties:' wrapper for spi-3wire in conditional block.
    Rob Herring suggested removing it, but dt_binding_check requires
    explicit 'properties:' context when referencing peripheral
    properties within allOf conditional sections to satisfy
    unevaluatedProperties validation. This follows the pattern in
    spi-controller.yaml itself.

 .../bindings/auxdisplay/titanmec,tm16xx.yaml  | 465 ++++++++++++++++++
 MAINTAINERS                                   |   5 +
 2 files changed, 470 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml

diff --git a/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml b/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
new file mode 100644
index 000000000000..a852d8b8882c
--- /dev/null
+++ b/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
@@ -0,0 +1,465 @@
+# 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-9a-f]+$":
+        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-9a-f]+,[0-9a-f]+$":
+        type: object
+        description:
+          Individual LED icon addressed by <grid>,<segment> matrix coordinates.
+        $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";
+          };
+        };
+      };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index e64b94e6b5a9..8ccf02ca2544 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25914,6 +25914,11 @@ W:	http://sourceforge.net/projects/tlan/
 F:	Documentation/networking/device_drivers/ethernet/ti/tlan.rst
 F:	drivers/net/ethernet/ti/tlan.*
 
+TM16XX-COMPATIBLE LED CONTROLLERS DISPLAY DRIVER
+M:	Jean-François Lessard <jefflessard3@gmail.com>
+S:	Maintained
+F:	Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
+
 TMIO/SDHI MMC DRIVER
 M:	Wolfram Sang <wsa+renesas@sang-engineering.com>
 L:	linux-mmc@vger.kernel.org
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v6 4/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (2 preceding siblings ...)
  2025-11-21 14:59 ` [PATCH v6 3/7] dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-11-24 11:05   ` Andy Shevchenko
  2025-11-21 14:59 ` [PATCH v6 5/7] auxdisplay: TM16xx: Add keypad support for scanning matrix keys Jean-François Lessard
                   ` (4 subsequent siblings)
  8 siblings, 1 reply; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree, Paolo Sabatino,
	Christian Hewitt, Martin Blumenstingl

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: Paolo Sabatino <paolo.sabatino@gmail.com> # Tested on H96 Max (XY_RK3328)
Tested-by: Christian Hewitt <christianshewitt@gmail.com> # Tested on X96 Max, Tanix TX3 Mini
Tested-by: Martin Blumenstingl <martin.blumenstingl@googlemail.com> # Tested on Tanix TX3 Mini
Signed-off-by: Jean-François Lessard <jefflessard3@gmail.com>
---

Notes:
    checkpatch reports false positives that are intentionally ignored:
    BIT_MACRO: bit shifts are used for field values while GENMASK/BIT
    are used for bit positions per semantic convention
    
    LED registration uses non-devm variant on-purpose to allow explicit
    unregistration on device removal, ensuring LED triggers are
    immediately stopped. This prevents stale LED trigger activity from
    continuing after the hardware is gone, avoiding the need for complex
    state tracking in brightness callbacks.

 MAINTAINERS                      |   2 +
 drivers/auxdisplay/Kconfig       |  12 +
 drivers/auxdisplay/Makefile      |   2 +
 drivers/auxdisplay/tm16xx.h      | 175 +++++++++++
 drivers/auxdisplay/tm16xx_core.c | 484 +++++++++++++++++++++++++++++++
 5 files changed, 675 insertions(+)
 create mode 100644 drivers/auxdisplay/tm16xx.h
 create mode 100644 drivers/auxdisplay/tm16xx_core.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 8ccf02ca2544..d9badf2c24ba 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25918,6 +25918,8 @@ TM16XX-COMPATIBLE LED CONTROLLERS DISPLAY DRIVER
 M:	Jean-François Lessard <jefflessard3@gmail.com>
 S:	Maintained
 F:	Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
+F:	drivers/auxdisplay/tm16xx.h
+F:	drivers/auxdisplay/tm16xx_core.c
 
 TMIO/SDHI MMC DRIVER
 M:	Wolfram Sang <wsa+renesas@sang-engineering.com>
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
index bedc6133f970..6b7c04902649 100644
--- a/drivers/auxdisplay/Kconfig
+++ b/drivers/auxdisplay/Kconfig
@@ -526,6 +526,18 @@ config SEG_LED_GPIO
 	  This driver can also be built as a module. If so, the module
 	  will be called seg-led-gpio.
 
+config TM16XX
+	tristate "TM16xx LED matrix display controllers" if COMPILE_TEST
+	select LEDS_CLASS
+	select LEDS_TRIGGERS
+	select LINEDISP
+	select NEW_LEDS
+	help
+	  Core support for TM16xx-compatible 7-segment LED matrix display
+	  controllers from multiple vendors (Titan Micro, Fuda Hisi, i-Core,
+	  Princeton, Winrise). Provides LED class integration for display
+	  control and optional keypad scanning support.
+
 #
 # Character LCD with non-conforming interface section
 #
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
index f5c13ed1cd4f..7ecf3cd4a0d3 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..ef6c004f9d89
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx.h
@@ -0,0 +1,175 @@
+/* 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/bits.h>
+#include <linux/bitfield.h>
+#include <linux/leds.h>
+#include <linux/mutex.h>
+#include <linux/types.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_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 device;
+struct tm16xx_display;
+struct tm16xx_digit;
+struct tm16xx_led;
+
+/**
+ * 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 {
+	u8 max_grids;
+	u8 max_segments;
+	u8 max_brightness;
+	u8 max_key_rows;
+	u8 max_key_cols;
+	int (*init)(struct tm16xx_display *display);
+	int (*data)(struct tm16xx_display *display, u8 index, unsigned int grid);
+	int (*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..9c29b7fb1635
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx_core.c
@@ -0,0 +1,484 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * TM16xx and compatible LED display/keypad controller driver
+ * Supports TM16xx, FD6xx, PT6964, HBS658, AIP16xx and related chips.
+ *
+ * Concurrency model:
+ * - Atomic display state bitmap writes for LED triggers in atomic context
+ * - Non-atomic display state reads in flush work provide eventual consistency
+ * - Mutex serializes hardware I2C/SPI transactions (sleeping context)
+ * - Workqueue prevents same work item running concurrently
+ *
+ * Uses explicit resource management (non-devm) for LEDs and workqueues
+ * to enforce removal ordering: unregister LEDs first to stop triggers
+ * before hardware cleanup, preventing use-after-free.
+ *
+ * Copyright (C) 2025 Jean-François Lessard
+ */
+
+#include <linux/bits.h>
+#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/mutex.h>
+#include <linux/property.h>
+#include <linux/sysfs.h>
+#include <linux/types.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
+ *
+ * Atomic display state bitmap writes. May execute in atomic context.
+ */
+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
+ *
+ * Non-atomic display state reads. Flush work provide eventual consistency.
+ *
+ * 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
+ *
+ * Configures controller and sets brightness. If an error occurs the error code
+ * is stored in flush_status for upper layers to handle.
+ *
+ * Flush operations use mutex to serialize hardware transactions. Workqueue
+ * allows non-atomic context and ensures the same work never runs concurrently.
+ */
+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)
+		return;
+
+	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
+ *
+ * Updates all hardware grids with current display state. If an error occurs
+ * during any grid write, the operation is interrupted and the error code is
+ * stored in flush_status for upper layers to handle.
+ *
+ * Flush operations use mutex to serialize hardware transactions. Workqueue
+ * allows non-atomic context and ensures the same work never runs concurrently.
+ */
+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;
+
+	if (!display->controller->data)
+		return;
+
+	guard(mutex)(&display->lock);
+
+	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
+ *
+ * Cannot sleep. Display brightness can be set by LED trigger in atomic context.
+ */
+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)
+ *
+ * Cannot sleep. LED brightness can be set by LED trigger in atomic context.
+ */
+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;
+	int seg_pattern, ret = 0;
+	unsigned int i, j;
+	bool val;
+
+	for (i = 0; i < display->num_digits; i++) {
+		digit = &display->digits[i];
+
+		if (i < count) {
+			seg_pattern = map_to_seg7(&map->map.seg7, buf[i]);
+			if (seg_pattern < 0) {
+				dev_err(display->dev,
+					"Invalid mapping to 7 segment at position %u: %c",
+					i, buf[i]);
+				ret = -EINVAL;
+				seg_pattern = 0;
+			}
+		} else {
+			seg_pattern = 0;
+		}
+
+		for (j = 0; j < TM16XX_DIGIT_SEGMENTS; j++) {
+			val = seg_pattern & BIT(j);
+			tm16xx_set_seg(display, digit->hwgrids[j], digit->hwsegs[j], val);
+		}
+	}
+
+	schedule_work(&display->flush_display);
+	return ret;
+}
+
+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)
+{
+	unsigned int max_hwgrid = 0, max_hwseg = 0;
+	u32 segments[TM16XX_DIGIT_SEGMENTS * 2];
+	struct tm16xx_digit *digit;
+	struct tm16xx_led *led;
+	unsigned int i, j;
+	u32 reg[2];
+	int ret;
+
+	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 */
+	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 */
+	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;
+}
+
+int tm16xx_probe(struct tm16xx_display *display)
+{
+	struct led_classdev *main = &display->main_led;
+	struct led_init_data led_init = {};
+	struct device *dev = display->dev;
+	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 ret;
+
+	/*
+	 * Explicit (non-devm) resource management and specific order shutdown sequence
+	 * required to prevent hardware access races when triggers attempt to update
+	 * the display during removal:
+	 * 1. unregister LEDs to stop triggers
+	 * 2. clear display
+	 * 3. turn off display
+	 */
+
+	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);
+	/* max_brightness: handle default value and enforce hardware ceiling */
+	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);
+
+	/* brightness: handle default value and enforce max ceiling */
+	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];
+		/* Individual leds are hardware-constrained to on/off */
+		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_GPL(tm16xx_probe, "TM16XX");
+
+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_GPL(tm16xx_remove, "TM16XX");
+
+MODULE_AUTHOR("Jean-François Lessard");
+MODULE_DESCRIPTION("TM16xx LED Display Controllers");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("LINEDISP");
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v6 5/7] auxdisplay: TM16xx: Add keypad support for scanning matrix keys
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (3 preceding siblings ...)
  2025-11-21 14:59 ` [PATCH v6 4/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-11-21 14:59 ` [PATCH v6 6/7] auxdisplay: TM16xx: Add support for I2C-based controllers Jean-François Lessard
                   ` (3 subsequent siblings)
  8 siblings, 0 replies; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree

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-François Lessard <jefflessard3@gmail.com>
---

Notes:
    checkpatch reports false positives that are intentionally ignored:
    COMPLEX_MACRO/MACRO_ARG_REUSE for tm16xx_for_each_key(): This is a
    standard iterator pattern following kernel conventions (similar to
    for_each_* macros throughout the kernel). The nested for loops are
    the correct implementation for matrix iteration.

 MAINTAINERS                        |   1 +
 drivers/auxdisplay/Kconfig         |   9 ++
 drivers/auxdisplay/Makefile        |   1 +
 drivers/auxdisplay/tm16xx.h        |  25 ++++
 drivers/auxdisplay/tm16xx_core.c   |   4 +
 drivers/auxdisplay/tm16xx_keypad.c | 192 +++++++++++++++++++++++++++++
 6 files changed, 232 insertions(+)
 create mode 100644 drivers/auxdisplay/tm16xx_keypad.c

diff --git a/MAINTAINERS b/MAINTAINERS
index d9badf2c24ba..21ba2a99b581 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25920,6 +25920,7 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
 F:	drivers/auxdisplay/tm16xx.h
 F:	drivers/auxdisplay/tm16xx_core.c
+F:	drivers/auxdisplay/tm16xx_keypad.c
 
 TMIO/SDHI MMC DRIVER
 M:	Wolfram Sang <wsa+renesas@sang-engineering.com>
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
index 6b7c04902649..afd8ce05c668 100644
--- a/drivers/auxdisplay/Kconfig
+++ b/drivers/auxdisplay/Kconfig
@@ -528,16 +528,25 @@ config SEG_LED_GPIO
 
 config TM16XX
 	tristate "TM16xx LED matrix display controllers" if COMPILE_TEST
+	depends on INPUT
+	select INPUT_MATRIXKMAP
 	select LEDS_CLASS
 	select LEDS_TRIGGERS
 	select LINEDISP
 	select NEW_LEDS
+	select TM16XX_KEYPAD if (INPUT)
 	help
 	  Core support for TM16xx-compatible 7-segment LED matrix display
 	  controllers from multiple vendors (Titan Micro, Fuda Hisi, i-Core,
 	  Princeton, Winrise). Provides LED class integration for display
 	  control and optional keypad scanning support.
 
+config TM16XX_KEYPAD
+	bool
+	depends on TM16XX
+	help
+	  Enable optional keyscan support for TM16XX driver.
+
 #
 # Character LCD with non-conforming interface section
 #
diff --git a/drivers/auxdisplay/Makefile b/drivers/auxdisplay/Makefile
index 7ecf3cd4a0d3..a9b9c8ff05e8 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 ef6c004f9d89..af0ed889ab1a 100644
--- a/drivers/auxdisplay/tm16xx.h
+++ b/drivers/auxdisplay/tm16xx.h
@@ -106,6 +106,7 @@ struct device;
 struct tm16xx_display;
 struct tm16xx_digit;
 struct tm16xx_led;
+struct tm16xx_keypad;
 
 /**
  * struct tm16xx_controller - Controller-specific operations and limits
@@ -136,6 +137,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.
@@ -153,6 +155,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;
@@ -172,4 +175,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 9c29b7fb1635..03e9484235b0 100644
--- a/drivers/auxdisplay/tm16xx_core.c
+++ b/drivers/auxdisplay/tm16xx_core.c
@@ -437,6 +437,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..be867b250da5
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx_keypad.c
@@ -0,0 +1,192 @@
+// 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/mutex.h>
+#include <linux/property.h>
+#include <linux/types.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_GPL(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);
+	int row, col, scancode;
+	unsigned int bit;
+	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);
+}
+
+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;
+	unsigned int poll_interval, nbits;
+	struct tm16xx_keypad *keypad;
+	struct input_dev *input;
+	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;
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v6 6/7] auxdisplay: TM16xx: Add support for I2C-based controllers
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (4 preceding siblings ...)
  2025-11-21 14:59 ` [PATCH v6 5/7] auxdisplay: TM16xx: Add keypad support for scanning matrix keys Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-11-24 16:53   ` Andy Shevchenko
  2025-11-21 14:59 ` [PATCH v6 7/7] auxdisplay: TM16xx: Add support for SPI-based controllers Jean-François Lessard
                   ` (2 subsequent siblings)
  8 siblings, 1 reply; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree

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-François Lessard <jefflessard3@gmail.com>
---
 MAINTAINERS                     |   1 +
 drivers/auxdisplay/Kconfig      |  16 ++
 drivers/auxdisplay/Makefile     |   1 +
 drivers/auxdisplay/tm16xx_i2c.c | 333 ++++++++++++++++++++++++++++++++
 4 files changed, 351 insertions(+)
 create mode 100644 drivers/auxdisplay/tm16xx_i2c.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 21ba2a99b581..17b3f101a0c6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25920,6 +25920,7 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
 F:	drivers/auxdisplay/tm16xx.h
 F:	drivers/auxdisplay/tm16xx_core.c
+F:	drivers/auxdisplay/tm16xx_i2c.c
 F:	drivers/auxdisplay/tm16xx_keypad.c
 
 TMIO/SDHI MMC DRIVER
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
index afd8ce05c668..104a43b5baf4 100644
--- a/drivers/auxdisplay/Kconfig
+++ b/drivers/auxdisplay/Kconfig
@@ -547,6 +547,22 @@ config TM16XX_KEYPAD
 	help
 	  Enable optional 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 a9b9c8ff05e8..ba7b310f5667 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..29031cea4d07
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx_i2c.c
@@ -0,0 +1,333 @@
+// 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 <linux/types.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] = FD655_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");
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* [PATCH v6 7/7] auxdisplay: TM16xx: Add support for SPI-based controllers
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (5 preceding siblings ...)
  2025-11-21 14:59 ` [PATCH v6 6/7] auxdisplay: TM16xx: Add support for I2C-based controllers Jean-François Lessard
@ 2025-11-21 14:59 ` Jean-François Lessard
  2025-11-24 17:00   ` Andy Shevchenko
  2025-11-24  7:33 ` [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Andy Shevchenko
  2025-11-24 17:02 ` Andy Shevchenko
  8 siblings, 1 reply; 14+ messages in thread
From: Jean-François Lessard @ 2025-11-21 14:59 UTC (permalink / raw)
  To: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley
  Cc: linux-kernel, linux-leds, devicetree

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-François Lessard <jefflessard3@gmail.com>
---
 MAINTAINERS                     |   1 +
 drivers/auxdisplay/Kconfig      |  16 ++
 drivers/auxdisplay/Makefile     |   1 +
 drivers/auxdisplay/tm16xx_spi.c | 398 ++++++++++++++++++++++++++++++++
 4 files changed, 416 insertions(+)
 create mode 100644 drivers/auxdisplay/tm16xx_spi.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 17b3f101a0c6..b32ccca2b7f6 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25922,6 +25922,7 @@ F:	drivers/auxdisplay/tm16xx.h
 F:	drivers/auxdisplay/tm16xx_core.c
 F:	drivers/auxdisplay/tm16xx_i2c.c
 F:	drivers/auxdisplay/tm16xx_keypad.c
+F:	drivers/auxdisplay/tm16xx_spi.c
 
 TMIO/SDHI MMC DRIVER
 M:	Wolfram Sang <wsa+renesas@sang-engineering.com>
diff --git a/drivers/auxdisplay/Kconfig b/drivers/auxdisplay/Kconfig
index 104a43b5baf4..c7409df72d85 100644
--- a/drivers/auxdisplay/Kconfig
+++ b/drivers/auxdisplay/Kconfig
@@ -563,6 +563,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 ba7b310f5667..2485a3a6769d 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..9bd3882ca051
--- /dev/null
+++ b/drivers/auxdisplay/tm16xx_spi.c
@@ -0,0 +1,398 @@
+// 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/types.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 *codes = display->spi_buffer;
+	u8 *cmd = 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 *codes = display->spi_buffer;
+	u8 *cmd = 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 *codes = display->spi_buffer;
+	u8 *cmd = 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 *codes = display->spi_buffer;
+	u8 *cmd = 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");
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 14+ messages in thread

* Re: [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (6 preceding siblings ...)
  2025-11-21 14:59 ` [PATCH v6 7/7] auxdisplay: TM16xx: Add support for SPI-based controllers Jean-François Lessard
@ 2025-11-24  7:33 ` Andy Shevchenko
  2025-11-24 17:02 ` Andy Shevchenko
  8 siblings, 0 replies; 14+ messages in thread
From: Andy Shevchenko @ 2025-11-24  7:33 UTC (permalink / raw)
  To: Jean-François Lessard
  Cc: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-kernel, linux-leds,
	devicetree

On Fri, Nov 21, 2025 at 09:59:00AM -0500, Jean-François Lessard wrote:
> This series adds mainline kernel support for TM16xx family LED matrix
> controllers and compatible chips, widely used in auxiliary displays on TV
> boxes and embedded devices.
> 
> Many consumer devices, particularly TV boxes, use auxiliary displays based
> on TM16xx controllers to show status information such as time, network
> connectivity and system state. Currently, there is no mainline kernel
> support for these displays, forcing users to rely on out-of-tree drivers
> or userspace solutions that access hardware interfaces directly.
> 
> This driver provides unified TM16xx support through the LED subsystem with
> both I2C and SPI communication protocols. It integrates with the LED class
> framework, enabling control via standard sysfs interfaces and LED triggers,
> while supporting keypad input when hardware connections are available.
> 
> The driver supports multiple controller families from various vendors:
> - Titan Micro Electronics: TM1618, TM1620, TM1628, TM1638, TM1650
> - Fuda Hisi Microelectronics: FD620, FD628, FD650, FD655, FD6551
> - i-Core Electronics: AiP650, AiP1618, AiP1628
> - Princeton Technology: PT6964
> - Winrise Technology: HBS658
> 
> Key features:
> - 7-segment display support with flexible digit/segment mapping
> - Individual LED icon control through LED class devices
> - Optional keypad scanning with configurable key mapping
> - Device tree configuration for board-specific wiring layouts
> - LED trigger integration for automatic system event indication
> - I2C and SPI protocol support depending on controller interface
> 
> Device tree bindings describe board-specific display wiring since
> controllers are layout-agnostic. The bindings use separate 'digits' and
> 'leds' containers with specific addressing schemes to accommodate the
> hardware's grid/segment matrix organization.
> 
> Tested on multiple ARM TV boxes (H96 Max, Magicsee N5, Tanix TX3 Mini,
> Tanix TX6, X92, X96 Max) across different SoC platforms (Rockchip, Amlogic,
> Allwinner) in both I2C and SPI configurations.
> 
> User space utilities available at:
> https://github.com/jefflessard/tm16xx-display
> 
> Dependencies:
> - linedisp_attach()/_detach() infrastructure introduced in patch series:
>  "auxdisplay: linedisp: support attribute attachment to auxdisplay devices"
> - fwnode_for_each_available_child_node_scoped() from patch series:
>  "device property: Add scoped fwnode child node iterators"
> 
> Note: This driver is placed in drivers/auxdisplay rather than drivers/leds
> based on previous maintainer guidance. LED maintainer Pavel Machek
> recommended auxdisplay for TM1628-based display drivers:
> https://lore.kernel.org/linux-devicetree/20200226130300.GB2800@duo.ucw.cz/
> 
> Regmap Evaluation:
> TM16xx controllers use command-based 2-wire/3-wire protocols that share
> sufficient commonalities with I2C/SPI to leverage their subsystems, but
> are not fully compliant with standard register-based access patterns:
> - TM1650 example: 0x48 is a control command while 0x4F is a keyscan
>   command. These appear as adjacent I2C "addresses" but are distinct
>   commands with different data directions and payloads, not read/write
>   pairs of the same register.
> - TM1628 example: Initialization requires coordinated sequences followed
>   by indexed data writes. Single regmap read/write calls cannot express
>   these multi-step transactions and timing constraints.
> - Protocol requirements: I2C read operations require I2C_M_NO_RD_ACK flags;
>   SPI write-then-read operations require mandatory inter-transfer delays
>   and CS assertion across phases.
> 
> While regmap provides valuable synchronization, debugfs, and abstraction
> benefits, standard I2C/SPI regmap buses cannot handle these requirements.
> 
> Custom regmap implementation is technically possible via IO accessors, but
> demands complex command routing logic and only partially supports paging.
> It would essentially recreate the existing controller functions while
> forcing them into register semantics they don't naturally fit.
> 
> The current explicit I2C/SPI approach directly expresses the hardware's
> actual command structure and maintains proper controller abstraction.

Sorry, it seems I forgot to send a review of the input (sub)driver, I think
most of the comments there are still applicable to this series. I'll also
look into the rest.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v6 4/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver
  2025-11-21 14:59 ` [PATCH v6 4/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
@ 2025-11-24 11:05   ` Andy Shevchenko
  0 siblings, 0 replies; 14+ messages in thread
From: Andy Shevchenko @ 2025-11-24 11:05 UTC (permalink / raw)
  To: Jean-François Lessard
  Cc: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-kernel, linux-leds,
	devicetree, Paolo Sabatino, Christian Hewitt, Martin Blumenstingl

On Fri, Nov 21, 2025 at 09:59:04AM -0500, Jean-François Lessard wrote:
> Add driver for TM16xx family LED controllers and compatible chips from
> multiple vendors including Titan Micro, Fuda Hisi, i-Core, Princeton, and
> Winrise. These controllers drive 7-segment digits and individual LED icons
> through either I2C or SPI buses.
> 
> Successfully tested on various ARM TV boxes including H96 Max, Magicsee N5,
> Tanix TX3 Mini, Tanix TX6, X92, and X96 Max across different SoC platforms
> (Rockchip, Amlogic, Allwinner).

...

> +/* 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_CTRL_BR_MASK	GENMASK(3, 1)
> +#define FD6551_CTRL_ON		(1 << 0)
> +
> +#define HBS658_KEY_COL_MASK	GENMASK(7, 5)

I'm wondering if splitting adding these chips to the separate patches gives us
better first one and the rest from the review / understanding perspective?

> +#define TM16XX_CTRL_BRIGHTNESS(on, val, prefix) \
> +	((on) ? (FIELD_PREP(prefix##_CTRL_BR_MASK, (val)) | prefix##_CTRL_ON) : 0)
> +
> +/* Forward declarations */
> +struct device;

+ blank line.

> +struct tm16xx_display;
> +struct tm16xx_digit;
> +struct tm16xx_led;

...

> +struct tm16xx_display {

Run `pahole` and try to find a compromise to group and save a few bytes here
and there.

> +	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;
> +};

...

> +#include <linux/bits.h>
> +#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/mutex.h>
> +#include <linux/property.h>
> +#include <linux/sysfs.h>
> +#include <linux/types.h>
> +#include <linux/workqueue.h>

...

> +static void tm16xx_display_flush_data(struct work_struct *work)
> +{
> +	struct tm16xx_display *display = container_of(work, struct tm16xx_display, flush_display);

What about...

	struct device *dev = display->dev;

> +	unsigned int grid, i;

> +	int ret = 0;

Drop.


> +	if (!display->controller->data)
> +		return;
> +
> +	guard(mutex)(&display->lock);

	display->flush_status = 0; // do we even need this?

> +	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 = display->controller->data(display, i, grid);
		if (display->flush_status) {
			dev_err(dev, "Failed to write display data: %d\n", display->flush_status);
			return;
		}

> +	}

> +	display->flush_status = ret;

Drop.


?

> +}

...

> +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 device *dev = display->dev;

> +	struct tm16xx_digit *digit;
> +	int seg_pattern, ret = 0;
> +	unsigned int i, j;
> +	bool val;
> +
> +	for (i = 0; i < display->num_digits; i++) {
> +		digit = &display->digits[i];
> +
> +		if (i < count) {
> +			seg_pattern = map_to_seg7(&map->map.seg7, buf[i]);
> +			if (seg_pattern < 0) {
> +				dev_err(display->dev,
> +					"Invalid mapping to 7 segment at position %u: %c",
> +					i, buf[i]);

> +				ret = -EINVAL;

Why shadowing error code?

> +				seg_pattern = 0;


			ret = map_to_seg7(&map->map.seg7, buf[i]);
			if (ret < 0) {
				dev_err(dev, "Invalid mapping to 7 segment at position %u: %c",
					i, buf[i]);
				seg_pattern = 0;
			} else {
				seg_pattern = ret;
			}

?

> +			}
> +		} else {
> +			seg_pattern = 0;
> +		}
> +
> +		for (j = 0; j < TM16XX_DIGIT_SEGMENTS; j++) {
> +			val = seg_pattern & BIT(j);
> +			tm16xx_set_seg(display, digit->hwgrids[j], digit->hwsegs[j], val);
> +		}
> +	}
> +
> +	schedule_work(&display->flush_display);
> +	return ret;
> +}

...

> +static int tm16xx_parse_fwnode(struct device *dev, struct tm16xx_display *display)
> +{
> +	unsigned int max_hwgrid = 0, max_hwseg = 0;
> +	u32 segments[TM16XX_DIGIT_SEGMENTS * 2];
> +	struct tm16xx_digit *digit;
> +	struct tm16xx_led *led;
> +	unsigned int i, j;
> +	u32 reg[2];
> +	int ret;
> +
> +	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 */
> +	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);

		display->digits = devm_kcalloc(dev, display->num_digits, sizeof(*display->digits),
					       GFP_KERNEL);

I prefer logical split.

> +		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);

ARRAY_SIZE() ?

> +			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 */
> +	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);

Ditto (logical split).

> +		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);

ARRAY_SIZE() ?

> +			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;
> +}

...

> +int tm16xx_probe(struct tm16xx_display *display)
> +{
> +	struct led_classdev *main = &display->main_led;
> +	struct led_init_data led_init = {};
> +	struct device *dev = display->dev;
> +	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 ret;

> +	/*
> +	 * Explicit (non-devm) resource management and specific order shutdown sequence
> +	 * required to prevent hardware access races when triggers attempt to update
> +	 * the display during removal:
> +	 * 1. unregister LEDs to stop triggers
> +	 * 2. clear display
> +	 * 3. turn off display
> +	 */

Can we drop using devm at all, since this is misleading now that we need to
call a ->remove() while the ->probe() is _partially_ managed.

> +	INIT_WORK(&display->flush_init, tm16xx_display_flush_init);
> +	INIT_WORK(&display->flush_display, tm16xx_display_flush_data);

This can be moved down, right?

> +	/* Initialize main LED properties */
> +	led_init.fwnode = dev_fwnode(dev);
> +	/* max_brightness: handle default value and enforce hardware ceiling */
> +	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);
> +
> +	/* brightness: handle default value and enforce max ceiling */
> +	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;

At least somewhere here.

> +	/* 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");

Does this actually allow already to have a work queued? If not, then move
initialisation even further.

> +	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];
> +		/* Individual leds are hardware-constrained to on/off */
> +		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_GPL(tm16xx_probe, "TM16XX");

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v6 6/7] auxdisplay: TM16xx: Add support for I2C-based controllers
  2025-11-21 14:59 ` [PATCH v6 6/7] auxdisplay: TM16xx: Add support for I2C-based controllers Jean-François Lessard
@ 2025-11-24 16:53   ` Andy Shevchenko
  0 siblings, 0 replies; 14+ messages in thread
From: Andy Shevchenko @ 2025-11-24 16:53 UTC (permalink / raw)
  To: Jean-François Lessard
  Cc: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-kernel, linux-leds,
	devicetree

On Fri, Nov 21, 2025 at 09:59:06AM -0500, Jean-François Lessard wrote:
> 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.

...

+ array_size.h

> +#include <linux/bitfield.h>

> +#include <linux/device.h>

Isn't it simply device/devres.h


+ errno.h

> +#include <linux/i2c.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/types.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;

	return tm16xx_probe();

> +}

...

> +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,
> +	}};

No array is needed.

> +	int ret;
> +
> +	ret = i2c_transfer(i2c->adapter, msgs, ARRAY_SIZE(msgs));
> +	if (ret < 0)
> +		return ret;
> +
> +	return (ret == ARRAY_SIZE(msgs)) ? 0 : -EIO;
> +}

...

> +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));

For u8 / char it's okay to use simple sizeof(). But it's up to you.
Ditto for the rest similar cases.

> +}

...

> +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);
> +}

Perhaps make it part of swab.h?

...

> +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;

	return tm16xx_i2c_write(display, &cmd, 1);

> +}

...

> +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) {

Perhaps

	if (keycode == 0xFF) // consider defining 0xFF with useful name
		return;

?

> +		col = FIELD_GET(HBS658_KEY_COL_MASK, keycode);
> +		tm16xx_set_key(display, 0, col, true);
> +	}
> +
> +	return 0;
> +}

...

Probably it is better to split out the additional HW enablement
to separate changes.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v6 7/7] auxdisplay: TM16xx: Add support for SPI-based controllers
  2025-11-21 14:59 ` [PATCH v6 7/7] auxdisplay: TM16xx: Add support for SPI-based controllers Jean-François Lessard
@ 2025-11-24 17:00   ` Andy Shevchenko
  0 siblings, 0 replies; 14+ messages in thread
From: Andy Shevchenko @ 2025-11-24 17:00 UTC (permalink / raw)
  To: Jean-François Lessard
  Cc: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-kernel, linux-leds,
	devicetree

On Fri, Nov 21, 2025 at 09:59:07AM -0500, Jean-François Lessard wrote:
> 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.

...

Seems like same/similar comments as per I2C glue driver are applicable here.
Please, address accordingly.

Additional comments below.

...

> +	tm16xx_for_each_key(display, row, col) {
> +		byte = col >> 1;

> +		bit = (2 - row) + ((col & 1) << 2);

If you do something like

		byte = col / 2;
		... = col % 2;

it may be optimised to a single assembly instruction on some architectures
by a compiler (and yes, I saw it in real life that `idiv` on x86 has been
chosen over other approaches by GCC).


> +		value = !!(codes[byte] & BIT(bit));

Seems unneeded

> +		tm16xx_set_key(display, row, col, value);

		tm16xx_set_key(display, row, col, codes[byte] & BIT(bit));


> +	}
> +
> +	return 0;
> +}

...

> +	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)));

Do we really need !!() ?

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver
  2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
                   ` (7 preceding siblings ...)
  2025-11-24  7:33 ` [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Andy Shevchenko
@ 2025-11-24 17:02 ` Andy Shevchenko
  8 siblings, 0 replies; 14+ messages in thread
From: Andy Shevchenko @ 2025-11-24 17:02 UTC (permalink / raw)
  To: Jean-François Lessard
  Cc: Andy Shevchenko, Geert Uytterhoeven, Rob Herring,
	Krzysztof Kozlowski, Conor Dooley, linux-kernel, linux-leds,
	devicetree

On Fri, Nov 21, 2025 at 09:59:00AM -0500, Jean-François Lessard wrote:
> This series adds mainline kernel support for TM16xx family LED matrix
> controllers and compatible chips, widely used in auxiliary displays on TV
> boxes and embedded devices.
> 
> Many consumer devices, particularly TV boxes, use auxiliary displays based
> on TM16xx controllers to show status information such as time, network
> connectivity and system state. Currently, there is no mainline kernel
> support for these displays, forcing users to rely on out-of-tree drivers
> or userspace solutions that access hardware interfaces directly.
> 
> This driver provides unified TM16xx support through the LED subsystem with
> both I2C and SPI communication protocols. It integrates with the LED class
> framework, enabling control via standard sysfs interfaces and LED triggers,
> while supporting keypad input when hardware connections are available.
> 
> The driver supports multiple controller families from various vendors:
> - Titan Micro Electronics: TM1618, TM1620, TM1628, TM1638, TM1650
> - Fuda Hisi Microelectronics: FD620, FD628, FD650, FD655, FD6551
> - i-Core Electronics: AiP650, AiP1618, AiP1628
> - Princeton Technology: PT6964
> - Winrise Technology: HBS658
> 
> Key features:
> - 7-segment display support with flexible digit/segment mapping
> - Individual LED icon control through LED class devices
> - Optional keypad scanning with configurable key mapping
> - Device tree configuration for board-specific wiring layouts
> - LED trigger integration for automatic system event indication
> - I2C and SPI protocol support depending on controller interface
> 
> Device tree bindings describe board-specific display wiring since
> controllers are layout-agnostic. The bindings use separate 'digits' and
> 'leds' containers with specific addressing schemes to accommodate the
> hardware's grid/segment matrix organization.
> 
> Tested on multiple ARM TV boxes (H96 Max, Magicsee N5, Tanix TX3 Mini,
> Tanix TX6, X92, X96 Max) across different SoC platforms (Rockchip, Amlogic,
> Allwinner) in both I2C and SPI configurations.
> 
> User space utilities available at:
> https://github.com/jefflessard/tm16xx-display
> 
> Dependencies:
> - linedisp_attach()/_detach() infrastructure introduced in patch series:
>  "auxdisplay: linedisp: support attribute attachment to auxdisplay devices"
> - fwnode_for_each_available_child_node_scoped() from patch series:
>  "device property: Add scoped fwnode child node iterators"
> 
> Note: This driver is placed in drivers/auxdisplay rather than drivers/leds
> based on previous maintainer guidance. LED maintainer Pavel Machek
> recommended auxdisplay for TM1628-based display drivers:
> https://lore.kernel.org/linux-devicetree/20200226130300.GB2800@duo.ucw.cz/
> 
> Regmap Evaluation:
> TM16xx controllers use command-based 2-wire/3-wire protocols that share
> sufficient commonalities with I2C/SPI to leverage their subsystems, but
> are not fully compliant with standard register-based access patterns:
> - TM1650 example: 0x48 is a control command while 0x4F is a keyscan
>   command. These appear as adjacent I2C "addresses" but are distinct
>   commands with different data directions and payloads, not read/write
>   pairs of the same register.
> - TM1628 example: Initialization requires coordinated sequences followed
>   by indexed data writes. Single regmap read/write calls cannot express
>   these multi-step transactions and timing constraints.
> - Protocol requirements: I2C read operations require I2C_M_NO_RD_ACK flags;
>   SPI write-then-read operations require mandatory inter-transfer delays
>   and CS assertion across phases.
> 
> While regmap provides valuable synchronization, debugfs, and abstraction
> benefits, standard I2C/SPI regmap buses cannot handle these requirements.
> 
> Custom regmap implementation is technically possible via IO accessors, but
> demands complex command routing logic and only partially supports paging.
> It would essentially recreate the existing controller functions while
> forcing them into register semantics they don't naturally fit.
> 
> The current explicit I2C/SPI approach directly expresses the hardware's
> actual command structure and maintains proper controller abstraction.

I think I have reviewed everything (except DT bindings, I will rely on
the respective tags from DT people). There are some comments also given
recently against previous round, please consider them as well.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply	[flat|nested] 14+ messages in thread

* Re: [PATCH v6 3/7] dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx
  2025-11-21 14:59 ` [PATCH v6 3/7] dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx Jean-François Lessard
@ 2025-12-04 15:42   ` Rob Herring
  0 siblings, 0 replies; 14+ messages in thread
From: Rob Herring @ 2025-12-04 15:42 UTC (permalink / raw)
  To: Jean-François Lessard
  Cc: Andy Shevchenko, Geert Uytterhoeven, Krzysztof Kozlowski,
	Conor Dooley, linux-kernel, linux-leds, devicetree

On Fri, Nov 21, 2025 at 09:59:03AM -0500, Jean-François Lessard wrote:
> Add documentation for TM16xx-compatible 7-segment LED display controllers
> with keyscan.
> 
> Signed-off-by: Jean-François Lessard <jefflessard3@gmail.com>
> ---
> 
> Notes:
>     The 'segments' property is intentionally not vendor-prefixed as it
>     defines a generic hardware description concept applicable to any
>     7-segment display controller. The property describes the fundamental
>     grid/segment coordinate mapping that is controller-agnostic and could
>     be reused by other LED matrix display bindings. Similar to how 'gpios'
>     describes GPIO connections generically, 'segments' describes segment
>     connections in a standardized way using uint32-matrix format.
>     
>     The property uses explicit coordinate pairs to handle real-world
>     hardware variations. Some board manufacturers use standard layouts
>     (same grid, different segments per digit) while others use transposed
>     layouts (same segment, different grids per digit). The coordinate-pair
>     approach accommodates both patterns without requiring separate arrays
>     or boolean flags, as confirmed acceptable by DT maintainers.
> 
>     Retained 'properties:' wrapper for spi-3wire in conditional block.
>     Rob Herring suggested removing it, but dt_binding_check requires
>     explicit 'properties:' context when referencing peripheral
>     properties within allOf conditional sections to satisfy
>     unevaluatedProperties validation. This follows the pattern in
>     spi-controller.yaml itself.
> 
>  .../bindings/auxdisplay/titanmec,tm16xx.yaml  | 465 ++++++++++++++++++
>  MAINTAINERS                                   |   5 +
>  2 files changed, 470 insertions(+)
>  create mode 100644 Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
> 
> diff --git a/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml b/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
> new file mode 100644
> index 000000000000..a852d8b8882c
> --- /dev/null
> +++ b/Documentation/devicetree/bindings/auxdisplay/titanmec,tm16xx.yaml
> @@ -0,0 +1,465 @@
> +# 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-9a-f]+$":
> +        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-9a-f]+,[0-9a-f]+$":
> +        type: object
> +        description:
> +          Individual LED icon addressed by <grid>,<segment> matrix coordinates.
> +        $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

You can drop properties here.

Otherwise,

Reviewed-by: Rob Herring (Arm) <robh@kernel.org>

> +      required:
> +        - spi-3wire

^ permalink raw reply	[flat|nested] 14+ messages in thread

end of thread, other threads:[~2025-12-04 15:42 UTC | newest]

Thread overview: 14+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-11-21 14:59 [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
2025-11-21 14:59 ` [PATCH v6 1/7] dt-bindings: vendor-prefixes: Add fdhisi, titanmec, princeton, winrise, wxicore Jean-François Lessard
2025-11-21 14:59 ` [PATCH v6 2/7] dt-bindings: leds: add default-brightness property to common.yaml Jean-François Lessard
2025-11-21 14:59 ` [PATCH v6 3/7] dt-bindings: auxdisplay: add Titan Micro Electronics TM16xx Jean-François Lessard
2025-12-04 15:42   ` Rob Herring
2025-11-21 14:59 ` [PATCH v6 4/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Jean-François Lessard
2025-11-24 11:05   ` Andy Shevchenko
2025-11-21 14:59 ` [PATCH v6 5/7] auxdisplay: TM16xx: Add keypad support for scanning matrix keys Jean-François Lessard
2025-11-21 14:59 ` [PATCH v6 6/7] auxdisplay: TM16xx: Add support for I2C-based controllers Jean-François Lessard
2025-11-24 16:53   ` Andy Shevchenko
2025-11-21 14:59 ` [PATCH v6 7/7] auxdisplay: TM16xx: Add support for SPI-based controllers Jean-François Lessard
2025-11-24 17:00   ` Andy Shevchenko
2025-11-24  7:33 ` [PATCH v6 0/7] auxdisplay: Add TM16xx 7-segment LED matrix display controllers driver Andy Shevchenko
2025-11-24 17:02 ` Andy Shevchenko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).