filogic: Add LED driver for GCA230718
authorRoland Reinl <reinlroland+github@gmail.com>
Tue, 26 Dec 2023 07:31:15 +0000 (08:31 +0100)
committerRafał Miłecki <rafal@milecki.pl>
Mon, 8 Jul 2024 20:51:11 +0000 (22:51 +0200)
Add basic support for the LED driver for GCA230718.

 - I didn't find any documentation or driver for it, so the information below is purely based on my investigations
 - If there is already I driver for it, please tell me. Maybe I didn't search enough
 - I implemented a kernel module (leds-gca230718) to access the LEDs via DTS
 - The LED controller supports PWM for brightness control and ramp control for smooth blinking. This is not implemented in the driver
 - The LED controller supports toggling (on -> off -> on -> off) where the brightness of the LEDs can be set individually for each on cycle
 - Until now, only simple active/inactive control is implemented (like when the LEDs would have been connected via GPIO)
 - Controlling the LEDs requires three sequences sent to the chip. Each sequence consists of
   - A reset command (0x81 0xE4) written to register 0x00
   - A control command (for example 0x0C 0x02 0x01 0x00 0x00 0x00 0xFF 0x01 0x00 0x00 0x00 0xFF 0x87 written to register 0x03)
 - The reset command is always the same
 - In the control command
   - byte 0 is always the same
   - byte 1 (0x02 in the example above) must be changed in every sequence: 0x02 -> 0x01 -> 0x03)
   - byte 2 is set to 0x01 which disables toggling. 0x02 would be LED toggling without ramp control, 0x03 would be toggling with ramp control
   - byte 3 to 6 define the brightness values for the LEDs (R,G,B,W) for the first on cycle when toggling
   - byte 7 defines the toggling frequency (if toggling enabled)
   - byte 8 to 11 define the brightness values for the LEDs (R,G,B,W) for the second on cycle when toggling
   - byte 12 is constant 0x87

Signed-off-by: Roland Reinl <reinlroland+github@gmail.com>
(cherry picked from commit 0682974aa8da4855ca373d195cb6365913fc1f8e)

package/kernel/leds-gca230718/Makefile [new file with mode: 0644]
package/kernel/leds-gca230718/src/Makefile [new file with mode: 0644]
package/kernel/leds-gca230718/src/leds-gca230718.c [new file with mode: 0644]

diff --git a/package/kernel/leds-gca230718/Makefile b/package/kernel/leds-gca230718/Makefile
new file mode 100644 (file)
index 0000000..a1ae3bc
--- /dev/null
@@ -0,0 +1,34 @@
+#
+# Copyright (C) 2008-2010 OpenWrt.org
+#
+# This is free software, licensed under the GNU General Public License v2.
+# See /LICENSE for more information.
+#
+
+include $(TOPDIR)/rules.mk
+include $(INCLUDE_DIR)/kernel.mk
+
+PKG_NAME:=leds-gca230718
+PKG_RELEASE:=1
+PKG_LICENSE:=GPL-2.0
+
+include $(INCLUDE_DIR)/package.mk
+
+define KernelPackage/leds-gca230718
+  SUBMENU:=LED modules
+  TITLE:=GCA230718 LED support (e.g. for D-Link M30)
+  FILES:= \
+       $(PKG_BUILD_DIR)/leds-gca230718.ko
+  AUTOLOAD:=$(call AutoProbe,leds-gca230718,1)
+  DEPENDS:=@TARGET_mediatek_filogic
+endef
+
+define KernelPackage/leds-gca230718/description
+  GCA230718 LED support (e.g. for D-Link M30) using I2C.
+endef
+
+define Build/Compile
+       $(KERNEL_MAKE) M="$(PKG_BUILD_DIR)" modules
+endef
+
+$(eval $(call KernelPackage,leds-gca230718))
diff --git a/package/kernel/leds-gca230718/src/Makefile b/package/kernel/leds-gca230718/src/Makefile
new file mode 100644 (file)
index 0000000..eaa4eef
--- /dev/null
@@ -0,0 +1 @@
+obj-m := leds-gca230718.o
diff --git a/package/kernel/leds-gca230718/src/leds-gca230718.c b/package/kernel/leds-gca230718/src/leds-gca230718.c
new file mode 100644 (file)
index 0000000..d759453
--- /dev/null
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * GCA230718 LED support (e.g. for D-Link M30) using I2C
+ *
+ * Copyright 2022 Roland Reinl <reinlroland+github@gmail.com>
+ *
+ * This driver can control RGBW LEDs which are connected to a GCA230718.
+ */
+
+#include <linux/delay.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/property.h>
+#include <linux/i2c.h>
+#include <linux/mutex.h>
+#include <linux/version.h>
+
+#define GCA230718_MAX_LEDS                                      (4u)
+
+#define GCA230718_OPMODE_DISABLED                               (0x00u)
+#define GCA230718_OPMODE_NO_TOGGLE                              (0x01u)
+#define GCA230718_OPMODE_TOGGLE_RAMP_CONTROL_DISABLED           (0x02u)
+#define GCA230718_OPMODE_TOGGLE_RAMP_CONTROL_ENSABLED           (0x03u)
+
+#define GCA230718_1ST_SEQUENCE_BYTE_1                           (0x02u)
+#define GCA230718_2ND_SEQUENCE_BYTE_1                           (0x01u)
+#define GCA230718_3RD_SEQUENCE_BYTE_1                           (0x03u)
+
+struct gca230718_led
+{
+       enum led_brightness brightness;
+       struct i2c_client *client;
+       struct led_classdev ledClassDev;
+};
+
+struct gca230718_private
+{
+       struct mutex lock;
+       struct gca230718_led leds[GCA230718_MAX_LEDS];
+};
+
+static void gca230718_init_private_led_data(struct gca230718_private* data)
+{
+       u8 ledIndex;
+       for (ledIndex = 0; ledIndex < GCA230718_MAX_LEDS; ledIndex++)
+       {
+               data->leds[ledIndex].client = NULL;
+       }
+}
+
+static void gca230718_send_sequence(struct i2c_client *client, u8 byte0, struct gca230718_private* gca230718_privateData)
+{
+       int status = 0;
+       u8 ledIndex;
+       const u8 resetCommand[2] = { 0x81, 0xE4 };
+       const u8 resetCommandRegister = 0x00;
+
+       u8 controlCommand[13];
+       const u8 controlCommandRegister = 0x03;
+
+       controlCommand[0] = 0x0C; /* Unknown */
+       controlCommand[1] = byte0;
+       controlCommand[2] = GCA230718_OPMODE_NO_TOGGLE;
+       /* Byte 3-6 are set below to the brighness value of the individual LEDs */
+       controlCommand[7] = 0x01; /* Frequency, doesn't care as long as GCA230718_OPMODE_NO_TOGGLE is used above */
+       /* Byte 8-11 are set below to the brighness value of the individual LEDs */
+       controlCommand[12] = 0x87;
+
+       for (ledIndex = 0; ledIndex < GCA230718_MAX_LEDS; ledIndex++)
+       {
+               controlCommand[3 + ledIndex] = gca230718_privateData->leds[ledIndex].brightness;
+               controlCommand[8 + ledIndex] = gca230718_privateData->leds[ledIndex].brightness;
+       }
+
+       mutex_lock(&(gca230718_privateData->lock));
+
+       if ((status = i2c_smbus_write_i2c_block_data(client, resetCommandRegister, sizeof(resetCommand), resetCommand)) != 0)
+       {
+               pr_info("Error %i during call of i2c_smbus_write_i2c_block_data for reset command\n", status);
+       }
+       else if ((status = i2c_smbus_write_i2c_block_data(client, controlCommandRegister, sizeof(controlCommand), controlCommand)) != 0)
+       {
+               pr_info("Error %i during call of i2c_smbus_write_i2c_block_data for control command\n", status);
+       }
+
+       mutex_unlock(&(gca230718_privateData->lock));
+}
+
+static int gca230718_set_brightness(struct led_classdev *led_cdev, enum led_brightness value)
+{
+       struct gca230718_led* led;
+       struct i2c_client* client;
+
+       led = container_of(led_cdev, struct gca230718_led, ledClassDev);
+       client = led->client;
+
+       if (client != NULL)
+       {
+               struct gca230718_private* gca230718_privateData;
+
+               led->brightness = value;
+               gca230718_privateData = i2c_get_clientdata(client);
+
+               gca230718_send_sequence(client, GCA230718_2ND_SEQUENCE_BYTE_1, gca230718_privateData);
+       }
+
+       return 0;
+}
+
+static int gca230718_probe(struct i2c_client *client, const struct i2c_device_id *id)
+{
+       int status = 0;
+       struct gca230718_private* gca230718_privateData;
+
+       pr_info("Enter gca230718_probe for device address %u\n", client->addr);
+       gca230718_privateData = devm_kzalloc(&(client->dev), sizeof(struct gca230718_private), GFP_KERNEL);
+
+       if (gca230718_privateData == NULL)
+       {
+               pr_info("Error during allocating memory for private data\n");
+               status = -ENOMEM;
+       }
+       else
+       {
+               struct device_node* ledNode;
+               mutex_init(&gca230718_privateData->lock);
+               gca230718_init_private_led_data(gca230718_privateData);
+               i2c_set_clientdata(client, gca230718_privateData);
+
+               for_each_child_of_node(client->dev.of_node, ledNode)
+               {
+                       u32 regValue = 0;
+                       if (of_property_read_u32(ledNode, "reg", &regValue) != 0)
+                       {
+                               pr_info("Missing entry \"reg\" in node %s\n", ledNode->name); 
+                       }
+                       else if (regValue >= GCA230718_MAX_LEDS)
+                       {
+                               pr_info("Invalid entry \"reg\" in node %s (%u)\n", ledNode->name, regValue);
+                       }
+                       else
+                       {
+                               struct led_classdev* ledClassDev = &(gca230718_privateData->leds[regValue].ledClassDev);
+                               struct led_init_data init_data = {};
+
+                               gca230718_privateData->leds[regValue].client = client;
+                               init_data.fwnode = of_fwnode_handle(ledNode);
+
+                               pr_info("Creating LED for node %s: reg=%u\n", ledNode->name, regValue); 
+
+                               ledClassDev->name = of_get_property(ledNode, "label", NULL);
+                               if (ledClassDev->name == NULL)
+                               {
+                                       ledClassDev->name = ledNode->name;
+                               }
+
+                               ledClassDev->brightness = LED_OFF;
+                               ledClassDev->max_brightness = LED_FULL;
+                               ledClassDev->brightness_set_blocking = gca230718_set_brightness;
+       
+                               if (devm_led_classdev_register_ext(&(client->dev), ledClassDev, &init_data) != 0)
+                               {
+                                       pr_info("Error during call of devm_led_classdev_register_ext");
+                               }
+                       }
+               }
+       }
+
+       if (status == 0)
+       {
+               /* 
+               Send full initialization sequence.
+               Afterwards only GCA230718_2ND_SEQUENCE_BYTE_1 must be send to upddate the brightness values.
+               */
+               gca230718_send_sequence(client, GCA230718_1ST_SEQUENCE_BYTE_1, gca230718_privateData);
+               gca230718_send_sequence(client, GCA230718_2ND_SEQUENCE_BYTE_1, gca230718_privateData);
+               gca230718_send_sequence(client, GCA230718_3RD_SEQUENCE_BYTE_1, gca230718_privateData);
+       }
+
+       return status;
+}
+
+#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,18,0)
+static void gca230718_remove(struct i2c_client *client)
+#else
+static int gca230718_remove(struct i2c_client *client)
+#endif
+{
+       struct gca230718_private* gca230718_privateData;
+       gca230718_privateData = i2c_get_clientdata(client);
+       mutex_destroy(&gca230718_privateData->lock);
+       gca230718_init_private_led_data(gca230718_privateData);
+
+#if LINUX_VERSION_CODE < KERNEL_VERSION(5,18,0)
+       return 0;
+#endif
+}
+
+static const struct i2c_device_id gca230718_i2c_ids[] = {
+       { "gca230718", 0 },
+       {},
+};
+MODULE_DEVICE_TABLE(i2c, gca230718_i2c_ids);
+
+static const struct of_device_id gca230718_dt_ids[] = {
+       { .compatible = "unknown,gca230718" },
+       {},
+};
+MODULE_DEVICE_TABLE(of, gca230718_dt_ids);
+
+static struct i2c_driver gca230718_driver = {
+       .probe          = gca230718_probe,
+       .remove         = gca230718_remove,
+       .id_table       = gca230718_i2c_ids,
+       .driver = {
+               .name           = KBUILD_MODNAME,
+               .of_match_table = gca230718_dt_ids,
+       },
+};
+
+module_i2c_driver(gca230718_driver);
+
+MODULE_AUTHOR("Roland Reinl <reinlroland+github@gmail.com>");
+MODULE_DESCRIPTION("GCA230718 LED support (e.g. for D-Link M30) using I2C");
+MODULE_LICENSE("GPL");