leds: rgb: wago-m4-led-wrapper: switch to binary RPMsg protocol

Replace the ASCII text-based command protocol with a compact binary
frame format defined in wago-m4-led-protocol.h. The Zephyr firmware
no longer exposes a generic rpmsg-tty channel; instead it announces
a dedicated "wago-led" RPMsg endpoint.

The ASCII protocol required up to three CMD-WLED messages plus a
CMD-IDL per brightness_set call and encoded colour values as decimal
strings. The binary protocol replaces this with fixed-size packed
structs (CMD_SET_LED: 5 bytes, CMD_IDL: 1 byte), reducing per-update
overhead and eliminating string formatting from the hot path.

CMD_SET_LED (0x01) addresses a single LED by index and carries R/G/B
values directly, allowing the M4 to update one pixel without
disturbing the rest of the strip buffer it maintains internally.
New frame types CMD_SET_STRIP (0x02) and CMD_SET_ALL (0x03) are
defined for future strip-wide updates.

The sysfs passthrough path is preserved for legacy ASCII animation
commands (CMD-CYC, CMD-BLK, CMD-FAD, etc.) via a thin
wago_send_ascii() wrapper around the new wago_send() helper.

The RX callback comment is updated to reflect that no ACKs are
expected in the binary protocol, and the rx log is adjusted to print
byte count rather than attempting to print binary data as a string.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
This commit is contained in:
Heinrich Toews 2026-04-02 17:22:45 +02:00
parent 92dc0f78ab
commit 270935a1cd

@ -8,16 +8,19 @@
*
* Protocol
* --------
* Communication uses the existing rpmsg-tty channel ("rpmsg-tty") that the
* Zephyr app creates. The driver sends ASCII commands fire-and-forget style
* ACK replies from the M4 are intentionally ignored. Commands used:
* 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.
*
* CMD-WLED-<id>-<R|G|B>-<brightness> set a single colour channel of LED <id>
* CMD-IDL turn off all LEDs (idle)
* Binary frame formats (little-endian, packed):
*
* Since one LED-multiclass write may set R, G and B simultaneously, the driver
* always sends up to three CMD-WLED messages per brightness_set call (one per
* non-zero channel) followed by a single CMD-IDL when all channels are zero.
* 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
* -------------------------
@ -70,6 +73,8 @@
#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.
@ -79,7 +84,7 @@
#define WAGO_LED_NUM_LEDS 10
#define WAGO_LED_NUM_CHANNELS 3 /* R, G, B */
/* Maximum ASCII command length: "CMD-WLED-9-R-255\n\0" */
/* Maximum ASCII command length for sysfs passthrough */
#define WAGO_CMD_MAX_LEN 32
/* First boot attempt this many ms after probe() */
@ -101,7 +106,10 @@
#define CH_GREEN 1
#define CH_BLUE 2
static const char ch_names[WAGO_LED_NUM_CHANNELS] = { 'R', 'G', 'B' };
/* 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
@ -158,29 +166,27 @@ struct wago_m4_led_priv {
};
/* -------------------------------------------------------------------------
* RPMsg callback: M4 -> Linux ("ACK: <cmd>")
* RPMsg callback: M4 -> Linux
*
* ACK messages are silently dropped we operate fire-and-forget.
* The callback must still be registered so the rpmsg core does not
* flag unexpected inbound messages as errors.
* 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)
{
/* ACKs intentionally ignored — fire-and-forget mode */
dev_dbg(&rpdev->dev, "rx (ignored): %.*s\n", len, (char *)data);
dev_dbg(&rpdev->dev, "rx (ignored, %d bytes)\n", len);
return 0;
}
/* -------------------------------------------------------------------------
* IPC helper: send one ASCII command, fire-and-forget
* IPC helper: fire-and-forget binary send
*
* Uses rpmsg_trysend() to avoid blocking when the vring TX ring is full.
* Retries up to WAGO_SEND_RETRIES times with a short udelay back-off.
* 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_cmd(struct wago_m4_led_priv *priv, const char *cmd)
static int wago_send(struct wago_m4_led_priv *priv,
const void *msg, size_t len)
{
int ret, tries;
@ -189,36 +195,41 @@ static int wago_send_cmd(struct wago_m4_led_priv *priv, const char *cmd)
return -ENODEV;
}
dev_dbg(priv->dev, "tx: %s", cmd);
for (tries = 0; tries < WAGO_SEND_RETRIES; tries++) {
ret = rpmsg_trysend(priv->rpdev->ept, (void *)cmd, strlen(cmd));
ret = rpmsg_trysend(priv->rpdev->ept, (void *)msg, len);
if (ret != -ENOMEM)
break;
/* vring full — give the M4 a moment to drain the ring */
udelay(WAGO_SEND_RETRY_US);
}
if (ret)
dev_warn_ratelimited(priv->dev,
"rpmsg_trysend failed after %d tries: %d (cmd: %s)\n",
tries, ret, cmd);
"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
* echo "CMD-WLED-3-B-128" > /sys/bus/platform/devices/leds-m4/wago_led_cmd
* ---------------------------------------------------------------------- */
#ifdef CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH
@ -247,7 +258,7 @@ static ssize_t wago_led_cmd_store(struct device *dev,
cmd[len] = '\0';
mutex_lock(&priv->send_lock);
ret = wago_send_cmd(priv, cmd);
ret = wago_send_ascii(priv, cmd);
mutex_unlock(&priv->send_lock);
return ret ? ret : count;
@ -283,35 +294,23 @@ static void wago_led_set(struct led_classdev *led_cdev,
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;
char cmd[WAGO_CMD_MAX_LEN];
bool any_nonzero = false;
int ch;
struct wago_msg_set_led msg;
/* Scale each sub-channel intensity by the master brightness */
led_mc_calc_color_components(mc_cdev, brightness);
/*
* 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 = (u8)mc_cdev->subled_info[CH_RED].brightness;
msg.g = (u8)mc_cdev->subled_info[CH_GREEN].brightness;
msg.b = (u8)mc_cdev->subled_info[CH_BLUE].brightness;
mutex_lock(&priv->send_lock);
for (ch = 0; ch < WAGO_LED_NUM_CHANNELS; ch++) {
unsigned int val = mc_cdev->subled_info[ch].brightness;
if (val == 0)
continue;
any_nonzero = true;
snprintf(cmd, sizeof(cmd), "CMD-WLED-%u-%c-%u\n",
led->index, ch_names[ch], val);
wago_send_cmd(priv, cmd);
}
/* All channels zero → turn LED off */
if (!any_nonzero) {
snprintf(cmd, sizeof(cmd), "CMD-IDL\n");
wago_send_cmd(priv, cmd);
}
wago_send(priv, &msg, sizeof(msg));
mutex_unlock(&priv->send_lock);
}