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