diff --git a/Documentation/devicetree/bindings/leds/wago,m4-led-wrapper.yaml b/Documentation/devicetree/bindings/leds/wago,m4-led-wrapper.yaml new file mode 100644 index 000000000000..26fca5b2b290 --- /dev/null +++ b/Documentation/devicetree/bindings/leds/wago,m4-led-wrapper.yaml @@ -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 + +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 0x01–0x04 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"; + }; diff --git a/drivers/leds/rgb/Kconfig b/drivers/leds/rgb/Kconfig index 183bccc06cf3..2a6ca4d78988 100644 --- a/drivers/leds/rgb/Kconfig +++ b/drivers/leds/rgb/Kconfig @@ -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 diff --git a/drivers/leds/rgb/Makefile b/drivers/leds/rgb/Makefile index c11cc56384e7..5412115a3c52 100644 --- a/drivers/leds/rgb/Makefile +++ b/drivers/leds/rgb/Makefile @@ -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 diff --git a/drivers/leds/rgb/wago-m4-led-protocol.h b/drivers/leds/rgb/wago-m4-led-protocol.h new file mode 100644 index 000000000000..b0427f39e011 --- /dev/null +++ b/drivers/leds/rgb/wago-m4-led-protocol.h @@ -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 + +#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 */ diff --git a/drivers/leds/rgb/wago-m4-led-wrapper.c b/drivers/leds/rgb/wago-m4-led-wrapper.c new file mode 100644 index 000000000000..8f2ff5bab023 --- /dev/null +++ b/drivers/leds/rgb/wago-m4-led-wrapper.c @@ -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 1–3 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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: (three u32 values, each 0-255) + * or: (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", ®)) { + 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);