leds: rgb: add WAGO M4 RGB LED wrapper driver

The WAGO PFC controllers use a dedicated M4 remote processor to manage
RGB LED strips. Communication with the M4 core is handled via a binary
RPMsg protocol. A specialized wrapper is required to translate standard
Linux LED class and pattern trigger events into RPMsg commands understood
by the remote processor firmware.

Support includes device tree based LED naming, initial default intensity
configuration, and dynamic RGB pattern triggers. The driver registers
LED devices at probe time for immediate trigger availability and polls
the remote processor to minimize boot delays. An optional sysfs
passthrough mechanism allows direct command execution for advanced
diagnostics.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
This commit is contained in:
Heinrich Toews
2026-04-29 14:14:18 +02:00
parent 3fb6184c57
commit e9cdbb93f3
5 changed files with 1094 additions and 0 deletions
@@ -0,0 +1,62 @@
# SPDX-License-Identifier: GPL-2.0-only OR BSD-2-Clause
%YAML 1.2
---
$id: http://devicetree.org/schemas/leds/wago,m4-led-wrapper.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#
title: WAGO M4 RGB LED Strip Wrapper
maintainers:
- WAGO GmbH & Co. KG <linux@wago.com>
description: |
Driver for a WS2812 RGB LED strip (10 LEDs) controlled by the TI AM62x M4
coprocessor running the Zephyr wago-led-server-app.
The driver communicates with the M4 via the RPMsg "wago-led" endpoint using
a binary protocol of packed byte frames (command ID 0x010x04 defined in
wago-m4-led-protocol.h). Each LED is exposed as a Linux LED multicolor (RGB)
device. ASCII command injection is available only through the optional sysfs
passthrough (CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH).
The M4 firmware may either be pre-loaded by the bootloader — in which case
the driver attaches to the already-running core via remoteproc — or it may be
loaded by the driver itself from /lib/firmware.
properties:
compatible:
const: wago,m4-led-wrapper
remoteproc:
$ref: /schemas/types.yaml#/definitions/phandle
description:
Phandle to the remoteproc node that manages the M4 coprocessor.
firmware-name:
$ref: /schemas/types.yaml#/definitions/string
description: |
File name of the M4 ELF firmware image, looked up in /lib/firmware.
Only required when the bootloader has not already started the M4.
default: "wago-led-server.elf"
wago,reset-on-init:
type: boolean
description: |
If present, the driver will reset (turn off) all LEDs when the RPMsg
channel is first established. This is useful for clearing any colors
inherited from the bootloader.
required:
- compatible
- remoteproc
additionalProperties: false
examples:
- |
/* Board .dts snippet */
leds-m4 {
compatible = "wago,m4-led-wrapper";
remoteproc = <&mcu_m4fss>;
firmware-name = "wago-led-server.elf";
};
+35
View File
@@ -51,4 +51,39 @@ config LEDS_MT6370_RGB
This driver can also be built as a module. If so, the module
will be called "leds-mt6370-rgb".
config LEDS_WAGO_M4_WRAPPER
tristate "WAGO M4 RGB LED strip wrapper (RPMsg/remoteproc)"
depends on RPMSG
depends on REMOTEPROC
depends on OF
help
Driver for a 10-LED WS2812 RGB strip on the TI AM62x M4 coprocessor
running the Zephyr wago-led-server-app. Communicates over the RPMsg
"wago-led" endpoint using a binary packed-frame protocol and exposes
each LED as a Linux LED multicolor (RGB) device.
The M4 firmware can be pre-loaded by the bootloader (the driver
will attach) or loaded by this driver from /lib/firmware.
Say M or Y for WAGO boards with an M4-driven WS2812 LED strip.
config LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH
bool "WAGO M4 LED wrapper: sysfs raw command passthrough"
depends on LEDS_WAGO_M4_WRAPPER
default n
help
Adds a wago_led_cmd sysfs attribute to the platform device that
forwards raw ASCII commands directly to the M4 via RPMsg, bypassing
the LED multiclass framework.
Useful for development, testing and triggering M4-side effects
(animations, blink patterns) that have no LED class equivalent:
echo "CMD-IDL" > /sys/.../leds-m4/wago_led_cmd
echo "CMD-CYC-50-128" > /sys/.../leds-m4/wago_led_cmd
echo "CMD-BLK-0-R-200-255" > /sys/.../leds-m4/wago_led_cmd
Say N for production builds where direct M4 access should be
restricted to the LED class interface only.
endif # LEDS_CLASS_MULTICOLOR
+1
View File
@@ -4,3 +4,4 @@ obj-$(CONFIG_LEDS_GROUP_MULTICOLOR) += leds-group-multicolor.o
obj-$(CONFIG_LEDS_PWM_MULTICOLOR) += leds-pwm-multicolor.o
obj-$(CONFIG_LEDS_QCOM_LPG) += leds-qcom-lpg.o
obj-$(CONFIG_LEDS_MT6370_RGB) += leds-mt6370-rgb.o
obj-$(CONFIG_LEDS_WAGO_M4_WRAPPER) += wago-m4-led-wrapper.o
+84
View File
@@ -0,0 +1,84 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* WAGO M4 LED Wrapper IPC Protocol Definition
*
* Binary protocol between the Linux wago-m4-led-wrapper driver and the
* Zephyr wago-led-server-app running on the TI AM62x M4 coprocessor.
*
* !! Must match the command IDs and frame layouts in the Zephyr app !!
*
* Author: WAGO GmbH & Co. KG
*/
#ifndef _WAGO_M4_LED_PROTOCOL_H
#define _WAGO_M4_LED_PROTOCOL_H
#include <linux/types.h>
#define WAGO_LED_NUM_LEDS 10
#define WAGO_LED_NUM_CHANNELS 3 /* R, G, B */
/**
* enum wago_led_cmd - Binary command IDs (first byte of every RPMsg frame)
*
* @WAGO_CMD_SET_LED: Set a single LED, all others unchanged. 5 bytes total.
* @WAGO_CMD_SET_STRIP: Set all LEDs atomically with a master brightness.
* 2 + WAGO_LED_NUM_LEDS * 3 bytes total.
* @WAGO_CMD_SET_ALL: Set every LED to the same color. 4 bytes total.
* @WAGO_CMD_IDL: Turn off all LEDs. 1 byte total.
*/
enum wago_led_cmd {
WAGO_CMD_SET_LED = 0x01,
WAGO_CMD_SET_STRIP = 0x02,
WAGO_CMD_SET_ALL = 0x03,
WAGO_CMD_IDL = 0x04,
};
/**
* struct wago_msg_set_led - WAGO_CMD_SET_LED frame
* @cmd: WAGO_CMD_SET_LED
* @led_idx: LED index [0 .. WAGO_LED_NUM_LEDS-1]
* @r: Red intensity [0..255]
* @g: Green intensity [0..255]
* @b: Blue intensity [0..255]
*/
struct wago_msg_set_led {
__u8 cmd;
__u8 led_idx;
__u8 r;
__u8 g;
__u8 b;
} __packed;
/**
* struct wago_msg_set_strip - WAGO_CMD_SET_STRIP frame
* @cmd: WAGO_CMD_SET_STRIP
* @brightness: Master brightness scale [0..255], applied to every channel
* @leds: RGB values for all LEDs; each channel scaled by brightness
* on the M4 side: out = (in * brightness) / 255
*/
struct wago_msg_set_strip {
__u8 cmd;
__u8 brightness;
struct {
__u8 r;
__u8 g;
__u8 b;
} leds[WAGO_LED_NUM_LEDS];
} __packed;
/**
* struct wago_msg_set_all - WAGO_CMD_SET_ALL frame
* @cmd: WAGO_CMD_SET_ALL
* @r: Red intensity [0..255]
* @g: Green intensity [0..255]
* @b: Blue intensity [0..255]
*/
struct wago_msg_set_all {
__u8 cmd;
__u8 r;
__u8 g;
__u8 b;
} __packed;
#endif /* _WAGO_M4_LED_PROTOCOL_H */
+912
View File
@@ -0,0 +1,912 @@
// SPDX-License-Identifier: GPL-2.0
/*
* WAGO M4 LED Wrapper Driver
*
* Controls a WS2812 RGB LED strip (10 LEDs) running on the TI AM62x M4
* coprocessor via the Zephyr wago-led-server-app. Exposes each LED as a
* Linux LED multiclass (RGB) device.
*
* Protocol
* --------
* Communication uses the "wago-led" RPMsg endpoint announced by the Zephyr app.
* The driver sends binary frames fire-and-forget style ACK replies from the
* M4 are intentionally ignored.
*
* Binary frame formats (little-endian, packed):
*
* CMD_SET_LED (0x01) [u8 cmd][u8 idx][u8 r][u8 g][u8 b] 5 bytes
* CMD_SET_STRIP (0x02) [u8 cmd][u8 bri][r0][g0][b0]...[r9][g9][b9] 32 bytes
* CMD_SET_ALL (0x03) [u8 cmd][u8 r][u8 g][u8 b] 4 bytes
* CMD_IDL (0x04) [u8 cmd] 1 byte
*
* The LED multiclass brightness_set callback uses CMD_SET_LED to update
* only the addressed LED without disturbing others on the strip.
*
* Fire-and-forget rationale
* -------------------------
* Waiting for an ACK after every command costs 13 ms per roundtrip (IPM
* interrupt + Zephyr thread wake-up). LED strip updates are idempotent a
* missed frame is harmless. If the vring TX ring is full, rpmsg_trysend()
* returns -ENOMEM; the driver retries up to WAGO_SEND_RETRIES times.
*
* Firmware loading and boot sequencing
* -------------------------------------
* Two boot paths are supported:
*
* a) U-Boot / SPL path (default for production):
* The M4 firmware is loaded by SPL before Linux starts. The rproc is
* already in RPROC_DETACHED state when the driver probes.
*
* In this path the driver registers all LED class devices immediately
* at probe() time so that kernel LED triggers (e.g. timer, pattern)
* start working without any delay. wago_led_set() silently drops
* frames while rpdev == NULL; once the RPMsg channel is announced
* (typically within a few hundred ms) real hardware updates flow.
*
* The boot_work is scheduled with zero delay to call rproc_boot()
* (attach) as early as possible.
*
* b) Linux-boot path (development / fallback):
* The driver loads the firmware from /lib/firmware itself via rproc_boot().
* Because the rootfs may not yet be mounted at probe() time, boot_work
* retries with WAGO_BOOT_RETRY_MS until the file appears.
*
* In this path LED class devices are also registered at probe() time
* so triggers work immediately, but the M4 will not actually render
* colours until rproc_boot() completes and the RPMsg channel appears.
*
* probe()
* -> register LED class devices immediately (triggers start at once)
* -> register RPMsg driver (waits for M4 channel announcement)
* -> schedule wago_boot_work:
* RPROC_DETACHED -> delay=0 (attach right away)
* otherwise -> delay=WAGO_BOOT_INITIAL_DELAY_MS
*
* wago_boot_work
* -> rproc_get + rproc_set_firmware + rproc_boot
* success -> RPMsg channel appears, wago_rpmsg_probe() sets rpdev
* -ENOENT -> filesystem not ready, reschedule after WAGO_BOOT_RETRY_MS
* other -> fatal, stop retrying
*
* Sysfs example
* -------------
* echo "0 128 128" > /sys/class/leds/sys/multi_intensity
* echo 255 > /sys/class/leds/sys/brightness
*
* # Send raw commands directly to the M4 for testing
* # (requires CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH=y):
* echo "CMD-IDL" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
* echo "CMD-CYC-50-128" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
*
* Author: WAGO GmbH & Co. KG
*/
#include <linux/delay.h>
#include <linux/led-class-multicolor.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/property.h>
#include <linux/platform_device.h>
#include <linux/remoteproc.h>
#include <linux/rpmsg.h>
#include <linux/slab.h>
#include <linux/workqueue.h>
#include "wago-m4-led-protocol.h"
#define DRIVER_NAME "wago-m4-led-wrapper"
/* Endpoint name announced by the Zephyr app via RPMsg name-service.
* Must match RPMSG_TTY_NAME in the Zephyr wago-led-server-app/src/main.c
*/
#define WAGO_RPMSG_EPT_NAME "wago-led"
/* Maximum ASCII command length for sysfs passthrough */
#define WAGO_CMD_MAX_LEN 32
/* First boot attempt this many ms after probe() — Linux-boot path only,
* gives the rootfs time to mount before loading firmware from /lib/firmware.
* Not used in the SPL path (M4 already running).
*/
#define WAGO_BOOT_INITIAL_DELAY_MS 5000
/* Retry interval when rproc is not yet registered or firmware not found */
#define WAGO_BOOT_RETRY_MS 200
/* Maximum number of boot attempts before giving up */
#define WAGO_BOOT_MAX_RETRIES 150
/*
* vring TX ring retry policy for rpmsg_trysend().
* Total worst-case wait: WAGO_SEND_RETRIES x WAGO_SEND_RETRY_US = 1 ms
*/
#define WAGO_SEND_RETRIES 5
#define WAGO_SEND_RETRY_US 200
/* colour channel indices matching subled_info[] order */
#define CH_RED 0
#define CH_GREEN 1
#define CH_BLUE 2
/* Maximum binary frame size for CMD_SET_STRIP:
* 1 (cmd) + 1 (brightness) + WAGO_LED_NUM_LEDS * 3 (RGB)
*/
#define WAGO_SET_STRIP_LEN \
(2 + WAGO_LED_NUM_LEDS * WAGO_LED_NUM_CHANNELS)
/* -------------------------------------------------------------------------
* Data structures
* ----------------------------------------------------------------------
*/
struct wago_m4_led_priv;
/**
* struct wago_led - per-LED state
* @mc_cdev: multicolor LED class device (wraps led_classdev)
* @subled_info: R/G/B sub-LED descriptors
* @priv: back-pointer to driver private data
* @index: LED index on the strip [0 .. WAGO_LED_NUM_LEDS-1]
*/
struct wago_led {
struct led_classdev_mc mc_cdev;
struct mc_subled subled_info[WAGO_LED_NUM_CHANNELS];
struct wago_m4_led_priv *priv;
unsigned int index;
};
/**
* struct wago_m4_led_priv - driver private data
* @dev: underlying platform device
* @rproc: remoteproc handle for the M4 core
* @rproc_booted_by_us: true when we called rproc_boot() ourselves
* @fw_name: firmware filename (from DT or default)
* @boot_work: delayed work for filesystem-ready retry loop
* @boot_retries: number of boot attempts made so far
* @rpdev: RPMsg device (populated when the channel appears)
* @send_lock: serialises concurrent rpmsg_trysend() calls
* @leds: per-LED state array
*/
struct wago_m4_led_priv {
struct device *dev;
/* remoteproc */
struct rproc *rproc;
bool rproc_booted_by_us;
bool reset_on_init;
const char *fw_name;
/* boot retry workqueue */
struct delayed_work boot_work;
int boot_retries;
/* RPMsg */
struct rpmsg_device *rpdev;
/* serialises concurrent send calls */
struct mutex send_lock;
/* LED strip */
struct wago_led leds[WAGO_LED_NUM_LEDS];
};
/* -------------------------------------------------------------------------
* RPMsg callback: M4 -> Linux
*
* No ACKs in binary protocol callback registered to satisfy rpmsg core.
* ----------------------------------------------------------------------
*/
static int wago_rpmsg_cb(struct rpmsg_device *rpdev, void *data,
int len, void *priv_data, u32 src)
{
dev_dbg(&rpdev->dev, "rx (ignored, %d bytes)\n", len);
return 0;
}
/* -------------------------------------------------------------------------
* IPC helper: fire-and-forget binary send
*
* Uses rpmsg_trysend() to avoid blocking. Retries up to WAGO_SEND_RETRIES
* times with a short udelay back-off when the vring TX ring is full.
* ----------------------------------------------------------------------
*/
static int wago_send(struct wago_m4_led_priv *priv,
const void *msg, size_t len)
{
int ret, tries;
if (!priv->rpdev) {
dev_dbg(priv->dev, "RPMsg channel not yet available, dropping frame\n");
return -ENODEV;
}
for (tries = 0; tries < WAGO_SEND_RETRIES; tries++) {
ret = rpmsg_trysend(priv->rpdev->ept, (void *)msg, len);
if (ret != -ENOMEM)
break;
udelay(WAGO_SEND_RETRY_US);
}
if (ret)
dev_warn_ratelimited(priv->dev,
"rpmsg_trysend failed after %d tries: %d\n",
tries, ret);
return ret;
}
/* Thin wrapper for sysfs passthrough (ASCII strings) */
static int wago_send_ascii(struct wago_m4_led_priv *priv, const char *cmd)
{
dev_dbg(priv->dev, "tx ascii: %s", cmd);
return wago_send(priv, cmd, strlen(cmd));
}
/* -------------------------------------------------------------------------
* Sysfs attribute: wago_led_cmd
* Only compiled in when CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH=y
*
* Allows sending raw ASCII commands to the M4 directly from the shell.
* The command string is forwarded as-is via RPMsg (fire-and-forget).
* Note: only legacy animation commands are handled as ASCII by the M4;
* LED colour updates from the kernel use the binary protocol.
*
* Usage:
* echo "CMD-IDL" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
* echo "CMD-CYC-50-128" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
* echo "CMD-BLK-0-R-200-255" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
* echo "CMD-FAD-0-G-20-5" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
* ----------------------------------------------------------------------
*/
#ifdef CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH
/*
* wago_all_leds_off - set brightness=0 and cancel triggers on all LEDs.
*
* Called when CMD-IDL is sent via sysfs passthrough. Without this, any
* active kernel trigger (e.g. "heartbeat") would keep calling
* brightness_set() and immediately re-light LEDs that CMD-IDL just cleared.
*/
static void wago_all_leds_off(struct wago_m4_led_priv *priv)
{
int i;
for (i = 0; i < WAGO_LED_NUM_LEDS; i++) {
struct led_classdev *lcdev =
&priv->leds[i].mc_cdev.led_cdev;
/* led_set_brightness() cancels any active trigger and sets
* brightness=0 through the normal LED core path.
*/
led_set_brightness(lcdev, LED_OFF);
}
}
static ssize_t wago_led_cmd_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
struct wago_m4_led_priv *priv = dev_get_drvdata(dev);
char cmd[WAGO_CMD_MAX_LEN];
size_t len;
int ret;
/* Strip trailing newline added by echo and copy into local buffer */
len = min(count, sizeof(cmd) - 2);
memcpy(cmd, buf, len);
/* Remove trailing whitespace / newline */
while (len > 0 && (cmd[len - 1] == '\n' ||
cmd[len - 1] == '\r' ||
cmd[len - 1] == ' '))
len--;
/* Re-add a single newline — the Zephyr app expects it */
cmd[len++] = '\n';
cmd[len] = '\0';
/* If this is CMD-IDL, silence all kernel-side triggers first.
* Otherwise an active trigger (e.g. "heartbeat") would keep calling
* brightness_set() and immediately re-light the strip after the M4
* has cleared it.
*/
if (strncmp(cmd, "CMD-IDL", 7) == 0)
wago_all_leds_off(priv);
mutex_lock(&priv->send_lock);
ret = wago_send_ascii(priv, cmd);
mutex_unlock(&priv->send_lock);
return ret ? ret : count;
}
static ssize_t wago_led_cmd_show(struct device *dev,
struct device_attribute *attr, char *buf)
{
struct wago_m4_led_priv *priv = dev_get_drvdata(dev);
return sysfs_emit(buf, "%s\n",
priv->rpdev ? "online" : "offline");
}
static DEVICE_ATTR_RW(wago_led_cmd);
static struct attribute *wago_led_attrs[] = {
&dev_attr_wago_led_cmd.attr,
NULL,
};
static const struct attribute_group wago_led_attr_group = {
.attrs = wago_led_attrs,
};
#endif /* CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH */
static void wago_led_set(struct led_classdev *led_cdev,
enum led_brightness brightness)
{
struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(led_cdev);
struct wago_led *led = container_of(mc_cdev, struct wago_led, mc_cdev);
struct wago_m4_led_priv *priv = led->priv;
struct wago_msg_set_led msg;
/*
* The pattern trigger updates the 'intensity' values in mc_cdev->subled_info
* directly (via pattern_trig_apply_color). We must use these current
* intensities instead of recalculating them from the static multi_intensity.
*
* Note: when the pattern trigger does gradual dimming, it updates the
* brightness of the led_cdev while keeping the sub-led intensities constant
* (at the target color). This correctly results in a fading effect.
*/
int i;
u8 components[WAGO_LED_NUM_CHANNELS] = {0};
for (i = 0; i < mc_cdev->num_colors && i < WAGO_LED_NUM_CHANNELS; i++)
components[i] = (u8)((brightness * mc_cdev->subled_info[i].intensity) / LED_FULL);
dev_dbg(priv->dev, "LED%d: bri=%d R=%d G=%d B=%d (ints: %d %d %d)\n",
led->index, brightness,
components[CH_RED], components[CH_GREEN], components[CH_BLUE],
mc_cdev->subled_info[CH_RED].intensity,
mc_cdev->subled_info[CH_GREEN].intensity,
mc_cdev->subled_info[CH_BLUE].intensity);
/*
* Use CMD_SET_LED to update only this LED without touching the
* others. The M4 maintains the full pixel[] buffer internally.
*/
msg.cmd = WAGO_CMD_SET_LED;
msg.led_idx = (u8)led->index;
msg.r = components[CH_RED];
msg.g = components[CH_GREEN];
msg.b = components[CH_BLUE];
mutex_lock(&priv->send_lock);
wago_send(priv, &msg, sizeof(msg));
mutex_unlock(&priv->send_lock);
}
/* -------------------------------------------------------------------------
* LED registration
* ----------------------------------------------------------------------
*/
/*
* wago_led_init_subled - initialise sub-LED descriptors common to every strip LED.
*/
static void wago_led_init_subled(struct wago_led *led)
{
led->subled_info[CH_RED].color_index = LED_COLOR_ID_RED;
led->subled_info[CH_RED].intensity = 255;
led->subled_info[CH_RED].channel = CH_RED;
led->subled_info[CH_GREEN].color_index = LED_COLOR_ID_GREEN;
led->subled_info[CH_GREEN].intensity = 255;
led->subled_info[CH_GREEN].channel = CH_GREEN;
led->subled_info[CH_BLUE].color_index = LED_COLOR_ID_BLUE;
led->subled_info[CH_BLUE].intensity = 255;
led->subled_info[CH_BLUE].channel = CH_BLUE;
led->mc_cdev.subled_info = led->subled_info;
led->mc_cdev.num_colors = WAGO_LED_NUM_CHANNELS;
led->mc_cdev.led_cdev.brightness_set = wago_led_set;
led->mc_cdev.led_cdev.max_brightness = LED_FULL;
led->mc_cdev.led_cdev.flags = LED_CORE_SUSPENDRESUME;
led->mc_cdev.led_cdev.color = LED_COLOR_ID_MULTI;
}
/*
* wago_led_apply_default_intensity - read optional 'led-default-intensity'
* DT property and apply R/G/B values and optional brightness to the LED.
*
* Property format: <R G B> (three u32 values, each 0-255)
* or: <R G B brightness> (four u32 values, each 0-255)
* If the property is absent or malformed the subled defaults (255/255/255)
* set by wago_led_init_subled() are kept unchanged.
*/
static void wago_led_apply_default_intensity(struct device *dev,
struct wago_led *led,
struct fwnode_handle *fwnode)
{
u32 rgba[4] = { 255, 255, 255, LED_FULL };
int count;
/* Accept both 3-cell (R G B) and 4-cell (R G B brightness) */
count = fwnode_property_count_u32(fwnode, "led-default-intensity");
if (count != 3 && count != 4)
return; /* property absent or wrong size — keep defaults */
if (fwnode_property_read_u32_array(fwnode, "led-default-intensity",
rgba, count))
return;
led->subled_info[CH_RED].intensity = clamp_val(rgba[0], 0, 255);
led->subled_info[CH_GREEN].intensity = clamp_val(rgba[1], 0, 255);
led->subled_info[CH_BLUE].intensity = clamp_val(rgba[2], 0, 255);
if (count == 4)
led->mc_cdev.led_cdev.brightness = clamp_val(rgba[3], 0, LED_FULL);
dev_dbg(dev, "LED %u: default intensity RGB(%u,%u,%u) brightness=%u from DT\n",
led->index, rgba[0], rgba[1], rgba[2],
led->mc_cdev.led_cdev.brightness);
}
/*
* wago_led_register_leds - register all strip LEDs with the LED core.
*
* When the DT node contains child nodes (led@N with a 'reg' property),
* they are used to drive naming via led_init_data.fwnode the LED core
* will read the 'label' and 'linux,default-trigger' properties from the
* child node automatically.
*
* When no child nodes are present the driver falls back to the legacy
* hard-coded names "m4-led0" "m4-led9".
*/
static int wago_led_register_leds(struct wago_m4_led_priv *priv)
{
struct device *dev = priv->dev;
struct fwnode_handle *child;
char default_label[16];
int count, ret;
count = device_get_child_node_count(dev);
if (count > 0) {
/*
* DT child-node path: iterate child nodes ordered by their
* 'reg' property which encodes the LED index [0..9].
*/
device_for_each_child_node(dev, child) {
struct led_init_data init_data = {};
struct wago_led *led;
u32 reg;
if (fwnode_property_read_u32(child, "reg", &reg)) {
dev_warn(dev,
"LED child node missing 'reg', skipping\n");
continue;
}
if (reg >= WAGO_LED_NUM_LEDS) {
dev_warn(dev,
"LED child reg %u out of range [0..%d], skipping\n",
reg, WAGO_LED_NUM_LEDS - 1);
continue;
}
led = &priv->leds[reg];
led->priv = priv;
led->index = reg;
wago_led_init_subled(led);
wago_led_apply_default_intensity(dev, led, child);
init_data.fwnode = child;
init_data.devicename = NULL;
init_data.devname_mandatory = false;
ret = devm_led_classdev_multicolor_register_ext(dev,
&led->mc_cdev,
&init_data);
if (ret) {
dev_err(dev,
"Failed to register LED %u: %d\n",
reg, ret);
fwnode_handle_put(child);
return ret;
}
dev_dbg(dev, "Registered LED %u from DT child node\n", reg);
}
} else {
/*
* Fallback: no child nodes use legacy "m4-ledN" names.
*/
int i;
for (i = 0; i < WAGO_LED_NUM_LEDS; i++) {
struct led_init_data init_data = {};
struct wago_led *led = &priv->leds[i];
led->priv = priv;
led->index = i;
wago_led_init_subled(led);
snprintf(default_label, sizeof(default_label), "m4-led%d", i);
init_data.fwnode = NULL;
init_data.devicename = DRIVER_NAME;
init_data.default_label = default_label;
init_data.devname_mandatory = false;
ret = devm_led_classdev_multicolor_register_ext(dev,
&led->mc_cdev,
&init_data);
if (ret) {
dev_err(dev,
"Failed to register LED %d: %d\n",
i, ret);
return ret;
}
dev_dbg(dev, "Registered %s (fallback name)\n",
led->mc_cdev.led_cdev.name);
}
}
return 0;
}
/* -------------------------------------------------------------------------
* RPMsg driver probe / remove
* Called by the rpmsg core when the M4 announces the "rpmsg-tty" service.
* ----------------------------------------------------------------------
*/
static int wago_rpmsg_probe(struct rpmsg_device *rpdev)
{
/*
* Walk up the device parent chain to find our priv pointer.
*
* The chain built by the remoteproc / virtio stack is:
*
* rpdev->dev rpmsg device (this device)
* .parent virtio_device (vrp->vdev)
* .parent rproc_vdev platform_device (rvdev->pdev)
* .parent rproc->dev <- dev_set_drvdata set here
*
* We set dev_set_drvdata on rproc->dev in wago_boot_work() as soon
* as we obtained the rproc handle, so three levels up is correct.
*/
struct device *rproc_dev = rpdev->dev.parent->parent->parent;
struct wago_m4_led_priv *priv;
/* Try three levels up first, then four (rproc->dev.parent layout
* may differ across kernel versions).
*/
priv = dev_get_drvdata(rproc_dev);
if (!priv) {
/* One more level up: rproc->dev.parent = ti_k3_m4 pdev */
priv = dev_get_drvdata(rproc_dev->parent);
}
if (!priv) {
dev_err(&rpdev->dev,
"No driver private data found in parent chain\n");
return -ENODEV;
}
/*
* RPMsg channel is up LED class devices are already registered
* (done at platform probe time). Just store the channel handle so
* that wago_led_set() can start sending real frames to the M4.
*/
priv->rpdev = rpdev;
dev_set_drvdata(&rpdev->dev, priv);
if (priv->reset_on_init) {
struct wago_msg_set_all msg = {
.cmd = WAGO_CMD_SET_ALL,
.r = 0, .g = 0, .b = 0
};
dev_info(priv->dev, "Resetting all LEDs to black (reset-on-init)\n");
mutex_lock(&priv->send_lock);
wago_send(priv, &msg, sizeof(msg));
mutex_unlock(&priv->send_lock);
}
dev_info(&rpdev->dev,
"WAGO M4 RPMsg channel up — LED strip active (%d LEDs)\n",
WAGO_LED_NUM_LEDS);
return 0;
}
static void wago_rpmsg_remove(struct rpmsg_device *rpdev)
{
struct wago_m4_led_priv *priv = dev_get_drvdata(&rpdev->dev);
if (priv)
priv->rpdev = NULL;
dev_info(&rpdev->dev, "WAGO M4 LED RPMsg channel removed\n");
}
static const struct rpmsg_device_id wago_rpmsg_id_table[] = {
{ .name = WAGO_RPMSG_EPT_NAME },
{}
};
MODULE_DEVICE_TABLE(rpmsg, wago_rpmsg_id_table);
static struct rpmsg_driver wago_rpmsg_driver = {
.drv = {
.name = DRIVER_NAME "-rpmsg",
.owner = THIS_MODULE,
},
.id_table = wago_rpmsg_id_table,
.probe = wago_rpmsg_probe,
.callback = wago_rpmsg_cb,
.remove = wago_rpmsg_remove,
};
/* -------------------------------------------------------------------------
* remoteproc boot (delayed-work retry loop)
* ----------------------------------------------------------------------
*/
/*
* wago_boot_work - try to get the rproc handle and attach/boot the M4.
*
* Called from a workqueue, so blocking operations are safe.
* Reschedules itself with WAGO_BOOT_RETRY_MS if the firmware file is not
* yet available (-ENOENT / -EAGAIN), giving the rootfs time to appear.
*
* When the M4 is already running (RPROC_DETACHED, U-Boot/SPL path) this
* work is scheduled with zero delay from probe() so the RPMsg channel
* comes up as fast as possible.
*/
static void wago_boot_work(struct work_struct *work)
{
struct wago_m4_led_priv *priv =
container_of(work, struct wago_m4_led_priv, boot_work.work);
struct device *dev = priv->dev;
int ret;
/* Lazily obtain the rproc handle on the first run */
if (!priv->rproc) {
struct device_node *np = dev->of_node;
struct device_node *rproc_np;
rproc_np = of_parse_phandle(np, "remoteproc", 0);
if (!rproc_np) {
dev_err(dev, "Missing 'remoteproc' phandle in DT\n");
return;
}
priv->rproc = rproc_get_by_phandle(rproc_np->phandle);
of_node_put(rproc_np);
if (IS_ERR_OR_NULL(priv->rproc)) {
priv->rproc = NULL;
dev_warn(dev, "rproc not yet available, retrying...\n");
goto retry;
}
/*
* Store priv on the rproc device so wago_rpmsg_probe() can
* retrieve it by walking up the parent chain from the rpmsg
* device. This must be done before rproc_boot() triggers the
* virtio / RPMsg stack.
*/
dev_set_drvdata(&priv->rproc->dev, priv);
}
/*
* U-Boot / SPL path: M4 is already powered (RPROC_DETACHED).
* Just attach no firmware file needed.
*/
if (priv->rproc->state == RPROC_DETACHED) {
dev_info(dev, "M4 already running (SPL path) — attaching\n");
ret = rproc_boot(priv->rproc);
if (ret) {
dev_err(dev, "rproc attach failed: %d\n", ret);
goto fatal;
}
priv->rproc_booted_by_us = true;
dev_info(dev, "M4 attached, RPMsg channel expected shortly\n");
return;
}
/* Linux-boot path: load firmware from filesystem */
ret = rproc_set_firmware(priv->rproc, priv->fw_name);
if (ret) {
dev_err(dev, "rproc_set_firmware failed: %d\n", ret);
goto fatal;
}
dev_dbg(dev, "Boot attempt %d/%d: loading %s\n",
priv->boot_retries + 1, WAGO_BOOT_MAX_RETRIES, priv->fw_name);
ret = rproc_boot(priv->rproc);
if (ret == 0) {
priv->rproc_booted_by_us = true;
dev_info(dev, "M4 booted with %s, waiting for RPMsg channel\n",
priv->fw_name);
return;
}
/*
* -ENOENT: firmware file not found rootfs not yet mounted.
* Switch to the longer retry interval for filesystem polling.
*/
if (ret == -ENOENT) {
dev_dbg(dev, "Firmware not found yet, retrying in %d ms\n",
WAGO_BOOT_INITIAL_DELAY_MS);
schedule_delayed_work(&priv->boot_work,
msecs_to_jiffies(WAGO_BOOT_INITIAL_DELAY_MS));
return;
}
dev_err(dev, "rproc_boot failed: %d\n", ret);
fatal:
rproc_put(priv->rproc);
priv->rproc = NULL;
return;
retry:
if (++priv->boot_retries >= WAGO_BOOT_MAX_RETRIES) {
dev_err(dev,
"Firmware '%s' not found after %d attempts, giving up\n",
priv->fw_name, WAGO_BOOT_MAX_RETRIES);
if (priv->rproc) {
rproc_put(priv->rproc);
priv->rproc = NULL;
}
return;
}
schedule_delayed_work(&priv->boot_work,
msecs_to_jiffies(WAGO_BOOT_RETRY_MS));
}
static void wago_rproc_stop(struct wago_m4_led_priv *priv)
{
if (!priv->rproc)
return;
if (priv->rproc_booted_by_us) {
rproc_shutdown(priv->rproc);
priv->rproc_booted_by_us = false;
}
rproc_put(priv->rproc);
priv->rproc = NULL;
}
/* -------------------------------------------------------------------------
* Platform driver probe / remove
* ----------------------------------------------------------------------
*/
static int wago_m4_led_probe(struct platform_device *pdev)
{
struct wago_m4_led_priv *priv;
const char *fw_name;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->dev = &pdev->dev;
mutex_init(&priv->send_lock);
/* Read firmware name from DT, fall back to default */
if (of_property_read_string(pdev->dev.of_node, "firmware-name", &fw_name))
fw_name = "wago-led-server.elf";
priv->fw_name = fw_name;
priv->reset_on_init = device_property_read_bool(&pdev->dev, "wago,reset-on-init");
platform_set_drvdata(pdev, priv);
/*
* Register LED class devices immediately so that kernel triggers
* (timer, pattern, ...) start working right away regardless of
* whether the RPMsg channel is up yet.
* wago_led_set() drops frames silently while rpdev == NULL.
*/
ret = wago_led_register_leds(priv);
if (ret) {
dev_err(&pdev->dev, "Failed to register LEDs: %d\n", ret);
return ret;
}
/*
* Register the RPMsg driver. The rpmsg core will call
* wago_rpmsg_probe() once the M4 announces the endpoint,
* regardless of whether the boot was done by SPL or by us.
*/
ret = register_rpmsg_driver(&wago_rpmsg_driver);
if (ret) {
dev_err(&pdev->dev,
"Failed to register rpmsg driver: %d\n", ret);
return ret;
}
#ifdef CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH
ret = sysfs_create_group(&pdev->dev.kobj, &wago_led_attr_group);
if (ret) {
dev_err(&pdev->dev,
"Failed to create sysfs group: %d\n", ret);
unregister_rpmsg_driver(&wago_rpmsg_driver);
return ret;
}
#endif /* CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH */
/*
* Schedule boot_work immediately (delay=0).
*
* boot_work polls for the rproc handle with WAGO_BOOT_RETRY_MS
* intervals (200 ms). Once the rproc is registered by the remoteproc
* subsystem it checks the state:
* RPROC_DETACHED -> attach right away (SPL path, ~1 retry needed)
* otherwise -> load firmware from /lib/firmware (Linux-boot path,
* retries until rootfs is mounted)
*/
INIT_DELAYED_WORK(&priv->boot_work, wago_boot_work);
schedule_delayed_work(&priv->boot_work, 0);
dev_info(&pdev->dev,
"WAGO M4 LED wrapper probed, %d LEDs active\n",
WAGO_LED_NUM_LEDS);
return 0;
}
static int wago_m4_led_remove(struct platform_device *pdev)
{
struct wago_m4_led_priv *priv = platform_get_drvdata(pdev);
/* Cancel any pending boot retry before tearing down */
cancel_delayed_work_sync(&priv->boot_work);
#ifdef CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH
sysfs_remove_group(&pdev->dev.kobj, &wago_led_attr_group);
#endif
unregister_rpmsg_driver(&wago_rpmsg_driver);
wago_rproc_stop(priv);
dev_info(&pdev->dev, "WAGO M4 LED wrapper removed\n");
return 0;
}
/* -------------------------------------------------------------------------
* Device Tree match table / module boilerplate
* ----------------------------------------------------------------------
*/
static const struct of_device_id wago_m4_led_of_match[] = {
{ .compatible = "wago,m4-led-wrapper" },
{}
};
MODULE_DEVICE_TABLE(of, wago_m4_led_of_match);
static struct platform_driver wago_m4_led_driver = {
.probe = wago_m4_led_probe,
.remove = wago_m4_led_remove,
.driver = {
.name = DRIVER_NAME,
.of_match_table = wago_m4_led_of_match,
},
};
module_platform_driver(wago_m4_led_driver);
MODULE_DESCRIPTION("WAGO M4 RGB LED strip wrapper (rpmsg-tty / remoteproc, fire-and-forget)");
MODULE_AUTHOR("WAGO GmbH & Co. KG");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:" DRIVER_NAME);