Compare commits

...

55 Commits

Author SHA1 Message Date
Heinrich Toews 305190aba7 arch: arm64: dts: ti: Enable slb9670 TPM on k3-am623-pfc-750-8400
TPM support is required for secure key storage and platform integrity
features. Enabling the slb9670 node ensures the TPM is available for
system use on this board variant.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-16 17:53:42 +02:00
Heinrich Toews 5fd796f64e arm64: dts: ti: Move default UART4 RTS GPIO to common board DTSI
When an interaction overlay is loaded after a base overlay, its
inclusion of k3-am623-pfc-rs485.dtsi overrides the baseboard-specific
UART4 rts-gpios config back to GPIO0_38. Moving the default rts-gpios to
the base common DTSI prevents this override, letting the active baseboard
overlay (like p0004224 which uses GPIO0_37) cleanly configure its
specific RTS routing.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-11 10:36:05 +02:00
Heinrich Toews 4988fa5c86 arm64: boot: fitImage: Add missing config for base-p0004224
U-Boot fails to load the baseboard overlay for PCB p0004224 at boot
because the FIT image configuration node is missing. Adding this entry
resolves the configuration name correctly and lets the bootloader
apply the appropriate base overlay.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-11 10:28:50 +02:00
Heinrich Toews c3ed670c91 arm64: dts: ti: Fix UART4 pinmux reference in base overlay p0004224
The omap8250 driver fails to register main_uart4 because the base
devicetree's phandle reference for 'pinctrl-0' is invalidated when
the overlay redefines 'main_uart4_pins_default'. Explicitly assigning
the pinctrl-0 property in the overlay ensures that the resolver
updates the phandle to the newly overlay-defined node.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-11 09:52:59 +02:00
Heinrich Toews 4d1f5a13ae arch: arm64: dts: ti: Add overlay for 750-8400 p0004224 RS485 DE fix
Board revision p0004224 has a hardware bug with RS485 DE routed to the
wrong pin (RS.DE instead of RS-2.DE). This overlay applies a workaround
to enable correct RS485 operation on this revision. Future revision
p0004305 resolves the issue in hardware, making this workaround
unnecessary.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-10 15:59:42 +02:00
Heinrich Toews e0657990ef Revert "arm64: dts: ti: pfc400-rev3: Use rs-2.de for RS-485 direction control"
This reverts commit 9698958e83.
2026-06-10 15:45:11 +02:00
Heinrich Toews 37580c48f9 arm64: dts: ti: Move single LED mode to specific baseboard overlay
The newer p0004305 baseboard utilizes a dual-LED configuration on its
Ethernet ports for link and activity signaling, making the global
single-led-mode property counter-productive.

DTC compiles separate label overrides inside DTBOs into disjoint
fragments, causing dynamic compile-time /delete-property/ directives
not to propagate across fragment boundaries. Decouple the single-led
configuration from the shared dtsi by allocating it exclusively inside
the legacy p0003964 baseboard overlay.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-09 17:16:32 +02:00
Heinrich Toews 9698958e83 arm64: dts: ti: pfc400-rev3: Use rs-2.de for RS-485 direction control
PFC400 hardware revision 3 has a misconfiguration where the
RS-485-2 Driver Enable signal (RS-2.DE) is falsely wired to the RS-1 instance line
instead of RS-485-1 (RS-1.DE).

Override rts-gpios in the revision-specific overlay to select
the rs-2.de GPIO as a workaround for this board errata.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-09 16:29:40 +02:00
Heinrich Toews 1bbcd3bd76 arm64: dts: ti: Update board IDs for PFC400 revision 2 overlays
To match the updated hardware identification numbers assigned to the
revision 2 of the PFC400 board prototypes, the overlay filenames and
their respective configurations in the Makefile and fitImage.its need
to be updated. This ensures consistency between physical revision 2
boards and the compiled device-tree binaries.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-06-09 13:14:52 +02:00
Heinrich Toews 5781a66491 arm64: dts: ti: k3-am623-pfc-750-84xx: fix switch port labels (dirty)
Ethernet switch port labels were incorrectly assigned, causing
mismatch between physical port positions and their logical names.
Swap labels to reflect the correct port-to-interface mapping:
port@0->ethX1, port@2->ethX3, port@3->ethX4, port@4->ethEM.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 18:04:37 +02:00
Heinrich Toews e3fd89f943 arm64: dts: ti: shorten FIT config nodes and add OMS poll interval
The U-Boot extension string generation for overlay configurations
exceeded the libfdt 31-character limit, causing failures during boot.
Shorten the configuration node names in the FIT image to match the
updated U-Boot logic. Additionally, add a poll interval to the OMS
switch overlay to work around missing MCU GPIO interrupt routing.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 17:39:37 +02:00
Heinrich Toews fbb5aac346 arm64: dts: ti: k3-am623-pfc-750-84xx: Fix VDD_CORE min voltage
The minimum voltage for VDD_CORE was set to 750mV, which is below
the actual operating range. Align it with the maximum of 850mV to
reflect the correct and fixed voltage level for this regulator.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews 59f1d87a2c arch: arm64: dts: ti: Remove duplicate TPS65219 PMIC node from wosm
The TPS65219 PMIC node at address 0x30 is already defined in the
base board DTS. Having it redefined in the wosm variant causes
device tree overlay conflicts and redundant regulator registrations.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews 7c8e5ded71 arch/arm64/boot/dts/ti: Fix OPP voltage for AM623 PFC 750-8xxx
Correct the operating performance point voltages from 750mV to
850mV across all frequency points. The previous 750mV setting was
insufficient for stable operation at the defined frequencies.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews ad4a4f96a3 arm64: dts: ti: k3-am62x-pfc: Fix VDD_CORE voltage for 1.25 GHz OPP
The 1.25 GHz operating performance point (OPP) requests a core voltage
of 0.75V, violating the SoC specifications which require 0.85V. Under
heavy load, the insufficient voltage causes core brownouts, leading to
data aborts, undefined instruction exceptions, and severe memory
corruption. Increase opp-microvolt to 850000 to stabilize the system
at peak frequencies.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews 3bbeb76f8d arch: arm64: boot: dts: ti: Add missing address/size-cells
Add missing `#address-cells` and `#size-cells` properties to root
node, `fss`, `ospi0`, and `main_i2c0` nodes in the PFC 750-84xx
WOSM device tree include file.

Without these properties, DT validation warns about missing
address/size specifiers, and child node addresses cannot be
correctly parsed by the kernel.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews 1c9d167f39 arm64: dts: ti: k3-am623: fix missing address and size cells
Relying on implicit default values for #address-cells and #size-cells
triggers avoid_default_addr_size and reg_format warnings during device
tree compilation. These unresolved properties create cascading failures
when parsing reg properties and resolving DTBO prerequisites.

Explicitly defining bus cell constraints for I2C and SPI overlay targets
silences these warnings and satisfies strict boundary requirements for
robust overlay application.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews c2f1a0dbf7 arm64: dts: ti: Remove static overlays from FIT image config
Foundational hardware definitions are now integrated into the base
device tree via a base .dtsi file, resolving previous root node
patching issues. Default FIT configuration no longer requires static
overlays for CPU voltages, RAM, and PMIC regulators. Removing these
entries allows U-Boot to dynamically load hardware-specific overlays
based on EEPROM values and simplifies runtime application.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews f40a482cfa arm64: dts: ti: Extract WOSM core into base dtb and support new overlays
Applying fundamental System-On-Module components like the PMIC, memory
capacity, and CPU voltages via a device tree overlay using the root
node ('&{/}') causes unpredictable structural and dtc compilation
issues (such as register storms on the SDHCI controller).

To bypass this problem, separate the immutable baseline hardware of the
WOSM into a new 'k3-am623-pfc-750-84xx-wosm-base.dtsi' and include it
statically in the main 'k3-am623-pfc-750-8400.dts'. The original
'wosm.dtsi' is now cleanly stripped of root manipulation and serves
only as a framework for true peripheral overlays.

Additionally, introduce the missing new hardware revisions (wosm
p0004263, base p0004305, localbus p0004067, interaction p0004065)
into the Makefile and update the flattened device tree layout inside
'fitImage.its' to assemble them correctly.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews 5a450e9f83 arm64: dts: ti: Add expansion resets, RS485 and CAN pinmux for p0004263
Hardware revision p0004263 introduces the following routing changes:
- nRST-EM2 moves from pad B6 to E18
- nRST-EM3 moves from pad A6 to B18

In addition, the WOSM module pins for a second RS485 interface (UART3)
and a CAN interface (MCAN0) are now explicitly defined for future use.

Define the 'em_reset_pins_default', 'rs485_2_pins_default', and
'can_interface_pins_default' in the p0004263 configuration overlay.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews fc1cce5f76 arm64: dts: ti: Update OMS buttons for p0004067 revision
The newer hardware revision p0004067 removes the combined nSTOP/RUN
switch from pad D24. Instead, it introduces two distinct buttons:
nRUN-BAS on pad A8 (MCU_I2C0_SCL) and nSTOP-BAS on pad D10
(MCU_I2C0_SDA).

Update the localbus p0004067 overlay to reflect these changes by
disabling the old "oms_stop_run" GPIO keys node, omitting pad D24 from
the "oms_pins_default" node, and defining the new individual RUN and
STOP buttons using the MCU GPIO pins. Finally, include the new overlay
in the build system.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews af7d0d54df mtd: spi-nor: core: Restrict blind MRAM reset to mram nodes
The spi_nor_everspin_reboot_fix() was executing unconditionally for any
SPI NOR device lacking a dedicated reset GPIO. This caused unintended
8-8-8 soft reset sequences and an unnecessary boot delay for unrelated
flashes.

While the new WOSM p0004263 hardware revision routes the MRAM nRST to
a GPIO, older revisions (e.g. p0000042) still rely on nPORz and therefore
need this fallback to escape Octal-STR mode during a warm reboot.

Limit the blind reset strictly to nodes named "mram" to avoid breaking
standard SPI NOR flash initializations.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews a6e86cb411 arm64: dts: ti: Add MRAM reset for WOSM p0004263 revision
The new AM6234 WOSM hardware revision (p0004263) disconnects the OSPI
MRAM nRST from nPORz and routes it to pad G25 (GPIO0_1). Define the
pinctrl for G25 and assign it to the MRAM node via the reset-gpios
property to ensure proper initialization. Add the new overlay to the
build system.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:45 +02:00
Heinrich Toews dd08a46be6 arm64: dts: ti: align fitImage configurations with u-boot board names
Update the FIT image base configuration name from 'conf-750-8400'
to 'conf-PFC-750-840x' to correctly map against the runtime U-Boot
evaluation of the '${board_name}' environment variable.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews 8c4152a66a arm64: dts: ti: fix missing root overlay nodes and FIT config
When overlay dtsi files specify root nodes using the standard '/ {'
syntax within a '/plugin/' context, the dtc compiler orphans these
nodes. At runtime, the applied overlays lose critical bindings like
voltage regulators and CPU configuration, leading to SDHCI register
storms and clock initialization failures.

Target the root node explicitly via '&{/} {' to ensure correct overlay
application. Additionally, add all board-specific overlays to the
default FIT configuration to streamline automatic booting.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews dc4f8882fe arch: arm64: dts: ti: add M4 LED wrapper for PFC 750-8400
The PFC 750-8400 board uses an M4 coprocessor to drive system
LEDs. Add the wago,m4-led-wrapper node to expose sys, run, io,
em and four user LEDs via the remoteproc firmware interface.
The sys LED defaults to an orange timer trigger on boot.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews f99c43a2d2 arm64: dts: ti: Extract WAGO board components to overlays
last working

U-Boot@PFC-750-840x:/ setenv boot_conf "conf-750-8400#overlay-750-8400-wosm-p0000042#overlay-750-8400-base-p0000017#overlay-750-8400-localbus-p0000007#overlay-750-8400-interaction-p0000031"
U-Boot@PFC-750-840x:/ setenv fdt_high
U-Boot@PFC-750-840x:/ setenv bootm_size 0x10000000
U-Boot@PFC-750-840x:/ ext4load mmc 1:2 ${loadaddr} /boot/fitImage
23880192 bytes read in 1048 ms (21.7 MiB/s)
U-Boot@PFC-750-840x:/ setenv bootargs "console=ttyS2,115200 root=/dev/mmcblk0p2 rw rootwait"
U-Boot@PFC-750-840x:/ bootm ${loadaddr}#${boot_conf}

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews e4dd81bf6d tools: leds: add m4-led-perf-test.sh
Bash/sh script (BusyBox compatible) to stress-test the WAGO M4 RGB LED
strip driver by activating kernel LED triggers on all ten strip LEDs
(sys, run, io, em, u1-u6).

Three trigger modes selectable via --trigger=<mode>:

  pattern   (default)
    Phase 1 - FADE:       staggered smooth brightness ramp (200 ms
              offset per LED) using the pattern trigger
    Phase 2 - FAST FLASH: 50 ms on/off in distinct colours per LED
    Phase 3 - SLOW FLASH: 400 ms on/off with colour cycling

  timer
    Simple on/off blink with staggered delay_on/delay_off values
    (50 ms .. 500 ms) and per-LED colours.

  heartbeat
    Kernel heartbeat trigger, all LEDs white.

Additional options:
  --duration=<sec>   Total test duration in seconds (default: 30)
  --version          Print version string
  --help             Print usage

Cleanup handler (trap INT/TERM) restores trigger=none + brightness=0
on all LEDs when the test ends or is interrupted.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews da7fae1b03 arm64: defconfig: enable DYNAMIC_DEBUG for am6xxx
Tracing complex interactions between the application and remote
processors (e.g. RPMsg channels, state synchronizations) requires
comprehensive log granularity without flooding the console by default.

CONFIG_DYNAMIC_DEBUG: Allows selectively enabling or disabling dev_dbg()
  and pr_debug() messages at runtime via the debugfs control file.
  This ensures low-overhead operations normally while permitting in-depth
  event tracing when analyzing boot delays or firmware communication.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews cce6dbfb08 arm64: defconfig: enable WAGO M4 RGB LED wrapper support
Operating the RGB LED strips on WAGO PFC AM6xxx-based platforms requires
the wrapper driver alongside specific LED subsystem features to properly
drive colors and sequences.

CONFIG_LEDS_CLASS_MULTICOLOR: Provides the multicolor LED class
  framework, essential to represent the aggregated RGB channels.
CONFIG_LEDS_WAGO_M4_WRAPPER: Enables the dedicated driver translating
  standard LED commands to RPMsg messages for the M4 firmware.
CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH: Serves as a back-door to
  the M4 to execute predefined commands directly, bypassing the Linux LED
  subsystem.
CONFIG_LEDS_TRIGGER_PATTERN: Provides the kernel framework to parse
  and execute complex timing sequences (patterns) for the LEDs.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews 9778b2faa4 arm64: dts: ti: k3-am623-pfc: disable M4F auto-boot
The M4F remote processor lifecycle requires explicit management by the
kernel remoteproc framework to ensure proper synchronization with Linux
drivers. Automatic booting bypasses necessary initialization sequences
and prevents controlled RPMsg channel establishment.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews 32b1b0da35 arm64: dts: ti: k3-am62x-pfc: reserve U-Boot IPC sidecar memory
U-Boot establishes an IPC sidecar mechanism utilizing shared memory
regions that must remain untouched by the kernel. Unrestricted access
corrupts the pre-established communication channels.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Heinrich Toews f28267ee58 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>
2026-05-28 09:54:44 +02:00
Heinrich Toews 44e5bd9b8c remoteproc: ti_k3_m4: add ti,no-auto-boot DT property support
Add a new optional boolean device tree property 'ti,no-auto-boot' for
the TI K3 M4F remoteproc driver. When present, this property disables
automatic firmware loading and booting at probe time by setting the
rproc->auto_boot flag to false.

This is useful in scenarios where the M4F firmware depends on resources
that are only available after the filesystem is mounted (e.g. firmware
files in /lib/firmware). In such cases, an external driver such as
'wago,m4-led-wrapper' can explicitly call rproc_boot() once all
dependencies are satisfied.

The corresponding YAML binding documentation is updated to describe the
new property and its intended use case.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-05-28 09:54:44 +02:00
Maxim Laschinksy 0d9d7d4c5a leds: trigger: pattern: Add per-tuple RGB color
- Extend pattern trigger to parse optional RGB per tuple
- Plumb color data through include/linux/leds.h
- Preserve existing tuple semantics when color omitted

Signed-off-by: Maxim Laschinsky <maxim.laschinsky@wago.com>
2026-05-28 09:54:44 +02:00
Maxim Laschinksy 13361187d2 leds: led-class-multicolor: Accept trailing RGB components
- Allow shorter writes for <=3 sub-LEDs (e.g. "R,G")
- Improve sysfs usability for RGB patterns
- Maintain compatibility with full-length writes

Signed-off-by: Maxim Laschinsky <maxim.laschinsky@wago.com>
2026-05-28 09:54:44 +02:00
Maxim Laschinksy 67e0e9c205 leds: group-multicolor: Respect default-state KEEP
- Skip initial brightness write when DT specifies KEEP
- Use LED core led_init_default_state_get() for defaults
- Add optional mc-allow-subled-writes to keep sub-LED sysfs writable
- Preserve intensity scaling and suspend/resume behavior

Signed-off-by: Maxim Laschinsky <maxim.laschinsky@wago.com>
2026-05-28 09:54:44 +02:00
Peter GALKA 267524ab51 Merge pull request #433 from BU-Automation/topic/gape/fix-gpio-dp_trig_reset
wsysinit: fix gpio 'dp_trig_reset' handling
2026-05-28 09:05:39 +02:00
Peter Galka e93894be4a wsysinit: fix gpio 'dp_trig_reset' handling
Signed-off-by: Peter Galka <peter.galka@wago.com>
2026-05-26 16:19:51 +02:00
Oleg KARFICH 036060f93d Merge pull request #428 from BU-Automation/topic/ht/pfc400/mram-em008lxo-mtd-ram
MTD: Enable mmap() support and diagnostic tooling for MRAM devices
2026-05-15 11:12:59 +02:00
Oleg KARFICH abeb97feae Merge branch 'master' into topic/ht/pfc400/mram-em008lxo-mtd-ram 2026-05-13 14:45:04 +02:00
Oleg KARFICH 38da587d35 Merge pull request #432 from BU-Automation/topic/ok/release_w05.13.00
add new version release_w05.13.00
2026-05-13 14:34:17 +02:00
Oleg Karfich dd0c614012 add new version release_w05.13.00
Signed-off-by: Oleg Karfich <oleg.karfich@wago.com>
2026-05-13 09:36:54 +02:00
Oleg KARFICH fd81267439 Merge branch 'master' into topic/ht/pfc400/mram-em008lxo-mtd-ram 2026-05-12 15:51:54 +02:00
Andrej UNRAU 527d216fa5 Merge pull request #431 from BU-Automation/topic/AUn/revert-mram-optimize-timings
Revert "arm: dts: pfc-750-84xx-wosm: mram: optimize timings"
2026-05-12 12:09:36 +02:00
Andrej Unrau 5c81f0089b Revert "arm: dts: pfc-750-84xx-wosm: mram: optimize timings"
This reverts commit 49f86c67e217f5bc745c203db5ba3c2783f53076.

The optimized MRAM timings are not stable on all devices.

Signed-off-by: Andrej Unrau <andrej.unrau@wago.com>
2026-05-11 10:48:36 +02:00
Oleg KARFICH 03ce0e95a4 Merge branch 'master' into topic/ht/pfc400/mram-em008lxo-mtd-ram 2026-05-07 16:47:03 +02:00
Oleg KARFICH 69818ae138 Merge pull request #430 from BU-Automation/topic/ht/pfc400/pr-ksz
Revert "arm64: configs: am6xxx: enable NET_DSA_MICROCHIP_KSZ9477_SYSFS"
2026-05-07 16:46:45 +02:00
Heinrich Toews 50c4d42a31 Revert "arm64: configs: am6xxx: enable NET_DSA_MICROCHIP_KSZ9477_SYSFS"
This reverts commit f7bef5c22d.
2026-05-06 12:40:02 +02:00
Heinrich Toews e9ad8bc142 tools: mtd: add mram_io byte-wise read/write tool for MTD_RAM devices
mram_io is a small user-space utility that maps a MTD_RAM device
(e.g. Everspin MRAM on Cadence OSPI in DAC mode) into its address
space via mmap() and performs byte-granular reads or writes without
any kernel copy overhead.

Usage:
  mram_io --device <device> --read --offset <off> --length <n>
  mram_io --device <device> --write --offset <off> --data <b0> [b1 b2 ...]

Options:
  --device / -D <n>  MTD character device, e.g. /dev/mtd0
  --offset / -o <n>  Byte offset (hex or decimal)
  --length / -l <n>  Number of bytes to read
  --data   / -d      Remaining arguments are bytes to write

Read output is a classic hex-dump with offset, hex columns (split
8+8) and an ASCII column.

Write mode logs each byte as it is written, then performs an
immediate read-back and reports OK / MISMATCH per byte.

Bytes are written as volatile stores to prevent the compiler from
optimising away individual byte accesses; the mmap region is mapped
noncached by the kernel (pgprot_noncached) so every store goes
straight to the hardware.

Argument parsing uses getopt_long and utilizes the device path as a
named option (--device / -D) to eliminate positional ambiguity when
passing data bytes.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-04-23 12:26:09 +02:00
Heinrich Toews 26cea97562 mtd: spi-nor: log MTD_RAM type and physical address at probe time
The late_init fixup in everspin.c sets mtd->priv to a map_info with
the AHB window's physical base address, but mtd->type/flags were
previously assigned redundantly in the fixup and then overwritten by
spi_nor_set_mtd_info() which always hardcodes MTD_NORFLASH.

Fix the ordering properly:

- spi_nor_set_mtd_info() in core.c now checks mtd->priv before
  assigning type/flags. If a fixup hook has already placed a
  struct map_info there, it uses MTD_RAM / MTD_CAP_RAM and emits
  a dev_info() with the physical base address:

    spi-nor: MTD type set to MTD_RAM (mmap enabled), phys base: 0x60000000

  This makes it easy to verify in the kernel log that the device
  came up correctly and which physical address to use for mmap().

- The now-redundant mtd->type / mtd->flags assignments are removed
  from everspin_mram_late_init().

- core.c gains #include <linux/mtd/map.h> for the struct map_info cast.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-04-23 12:17:29 +02:00
Heinrich Toews 98b840c366 mtd: spi-nor: everspin: enable mmap() for MRAM via Cadence OSPI DAC window
The Cadence OSPI controller (ti,am654-ospi) provides a Direct Access
Controller (DAC) mode: the flash/MRAM address space is visible as a
contiguous window in the CPU's physical address space via the AHB bus
(resource[1] in the platform device, base address ~0x60000000 on AM62x).

When DAC mode is active the driver already uses memcpy_fromio() /
memcpy_toio() against this window for all data transfers. There is
therefore a 1:1 mapping between MTD offsets and physical addresses:

  phys_addr = res_ahb->start + mtd_offset

Wire this up to allow user-space mmap() on /dev/mtd0:

1. Allocate a struct everspin_mram_priv (contains an embedded
   struct map_info) via devm_kzalloc() in the late_init fixup hook.

2. Read the AHB window physical base address from the controller's
   platform_device resource[1] using platform_get_resource().
   This is the same resource that cqspi_probe() uses for ioremap,
   so it is always valid when DAC mode is active.

3. Fill in map_info.phys and map_info.size, store it in nor->priv
   and point mtd->priv at it.

4. Set mtd->type = MTD_RAM and mtd->flags = MTD_CAP_RAM so that
   mtdchar_mmap() takes the vm_iomap_memory() path introduced in the
   companion patch 'mtd: mtdchar: enable mmap() for MTD_RAM devices
   with a physical map'.

If the AHB resource is not found (e.g. non-Cadence controller) the
function logs a warning and continues without mmap support; all other
functionality is unaffected.

After this change user-space can do:

  int fd = open("/dev/mtd0", O_RDWR | O_SYNC);
  void *p = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

and get direct access to the MRAM without any kernel copy overhead.

Tested on AM6232 (PFC-750-8xxx) with Everspin EM008LXO in Octal-STR
(8-8-8) mode at 200 MHz.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-04-22 14:01:11 +02:00
Heinrich Toews 1259dde287 mtd: mtdchar: enable mmap() for MTD_RAM devices with a physical map
The mmap() path in mtdchar has been intentionally dead since commit
9fdca4d ('mtd: kill dead code in mtdchar_mmap') with the comment
'This is broken because it assumes the MTD device is map-based'.

Re-enable mmap() for MTD_RAM devices that explicitly set mtd->priv to
a valid struct map_info with a known physical address (map->phys !=
NO_XIP). The NULL and NO_XIP guards prevent the broken assumptions
that caused the original removal from applying to any other device.

All other MTD types continue to return -ENODEV as before.

This is a prerequisite for Everspin MRAM on Cadence OSPI (DAC mode)
which exposes the memory as a directly CPU-addressable AHB window.

Signed-off-by: Heinrich Toews <ht@twx-software.de>
2026-04-22 14:01:11 +02:00
Christian HOHNSTÄDT c02ca98757 Merge pull request #427 from BU-Automation/topic/cho/YBP-12364-rs485-term
YBP-12364 - RS485: termination-resistor via WDx
2026-04-16 10:53:07 +02:00
Christian Hohnstaedt 83f91a7720 arm64: dts: k3-am623-pfc-rs485: remove term-gpio
Disconnect the gpio from rs485-ioctl

This way the BIAS network and termination-resistor configuration
work the same way via "gpioset" from the commandline.

Now the resistor may be configured without accessing the serial device,
which may be blocked by an other user.

Signed-off-by: Christian Hohnstaedt <christian.hohnstaedt@wago.com>
2026-04-15 15:05:36 +02:00
41 changed files with 2727 additions and 165 deletions
@@ -0,0 +1,60 @@
# 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 "rpmsg-tty" channel using
ASCII text commands (CMD-WLED / CMD-IDL). Each LED is exposed as a Linux LED
multiclass (RGB) device under /sys/class/leds/m4-led<N>/.
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";
};
@@ -50,6 +50,14 @@ properties:
maxItems: 1
description: Name of firmware to load for the M4F core
ti,no-auto-boot:
type: boolean
description:
If present, the remoteproc will not automatically load and boot the
M4F firmware at probe time. Firmware loading is then delegated to an
external driver that calls rproc_boot() explicitly once the
filesystem is available.
mboxes:
description:
OMAP Mailbox specifier denoting the sub-mailbox, to be used for
+1 -1
View File
@@ -89,7 +89,7 @@ static int wsysinit_init_probe_dt(struct platform_device *pdev)
struct device_node *np = pdev->dev.of_node;
struct __kernel_timex tmx = {0};
if (of_property_present(np, "dp,rst")) {
if (of_property_present(np, "dp,rst-gpios")) {
wsysinitset.dp_gpio_rst = devm_gpiod_get(&pdev->dev, "dp,rst", GPIOD_OUT_HIGH);
if (IS_ERR(wsysinitset.dp_gpio_rst)) {
dev_err(&pdev->dev,
+10
View File
@@ -26,6 +26,15 @@ dtb-$(CONFIG_ARCH_K3) += k3-am62-lp-sk-hdmi-audio.dtb
dtb-$(CONFIG_ARCH_K3) += k3-am623-pfc-750-8302.dtb
dtb-$(CONFIG_ARCH_K3) += k3-am623-pfc-750-830x-ems.dtb
dtb-$(CONFIG_ARCH_K3) += k3-am623-pfc-750-8400.dtb
dtb-$(CONFIG_ARCH_K3) += overlay-750-8400-wosm-p0004037.dtbo \
overlay-750-8400-wosm-p0004263.dtbo \
overlay-750-8400-base-p0003964.dtbo \
overlay-750-8400-base-p0004305.dtbo \
overlay-750-8400-base-p0004224.dtbo \
overlay-750-8400-localbus-p0003978.dtbo \
overlay-750-8400-localbus-p0004067.dtbo \
overlay-750-8400-interaction-p0003965.dtbo \
overlay-750-8400-interaction-p0004065.dtbo
# Boards with AM62Ax SoC
dtb-$(CONFIG_ARCH_K3) += k3-am62a7-sk.dtb
@@ -86,3 +95,4 @@ DTC_FLAGS_k3-am6548-iot2050-advanced-m2 += -@
DTC_FLAGS_k3-j721e-common-proc-board += -@
DTC_FLAGS_k3-j721s2-common-proc-board += -@
DTC_FLAGS_k3-am623-pfc-750-8302 += -@
DTC_FLAGS_k3-am623-pfc-750-8400 += -@
+165
View File
@@ -0,0 +1,165 @@
/dts-v1/;
/ {
description = "WAGO AM623 PFC FIT Image";
#address-cells = <1>;
images {
kernel {
description = "Linux Kernel";
data = /incbin/("../../Image");
type = "kernel";
arch = "arm64";
os = "linux";
compression = "none";
load = <0x80080000>;
entry = <0x80080000>;
hash-1 {
algo = "sha256";
};
};
fdt-PFC-750-840x {
description = "Flattened Device Tree blob";
data = /incbin/("k3-am623-pfc-750-8400.dtb");
type = "flat_dt";
arch = "arm64";
compression = "none";
load = <0x88000000>;
hash-1 {
algo = "sha256";
};
};
img-overlay-PFC-750-840x-wosm-p0004037 {
description = "WOSM Overlay";
data = /incbin/("overlay-750-8400-wosm-p0004037.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-wosm-p0004263 {
description = "WOSM Overlay p0004263";
data = /incbin/("overlay-750-8400-wosm-p0004263.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-base-p0003964 {
description = "Base Overlay";
data = /incbin/("overlay-750-8400-base-p0003964.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-base-p0004224 {
description = "Base Overlay p0004224";
data = /incbin/("overlay-750-8400-base-p0004224.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-base-p0004305 {
description = "Base Overlay p0004305";
data = /incbin/("overlay-750-8400-base-p0004305.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-localbus-p0003978 {
description = "Localbus Overlay";
data = /incbin/("overlay-750-8400-localbus-p0003978.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-localbus-p0004067 {
description = "Localbus Overlay p0004067";
data = /incbin/("overlay-750-8400-localbus-p0004067.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-interaction-p0003965 {
description = "Interaction Overlay";
data = /incbin/("overlay-750-8400-interaction-p0003965.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
img-overlay-PFC-750-840x-interaction-p0004065 {
description = "Interaction Overlay p0004065";
data = /incbin/("overlay-750-8400-interaction-p0004065.dtbo");
type = "flat_dt";
arch = "arm64";
compression = "none";
};
};
configurations {
default = "conf-PFC-750-840x";
conf-PFC-750-840x {
description = "Boot Linux kernel with base FDT and overlays";
kernel = "kernel";
fdt = "fdt-PFC-750-840x";
};
ovl-750-840x-wosm-p0004037 {
description = "WOSM Overlay config";
fdt = "img-overlay-PFC-750-840x-wosm-p0004037";
};
ovl-750-840x-wosm-p0004263 {
description = "WOSM Overlay p0004263 config";
fdt = "img-overlay-PFC-750-840x-wosm-p0004263";
};
ovl-750-840x-base-p0003964 {
description = "Base Overlay config";
fdt = "img-overlay-PFC-750-840x-base-p0003964";
};
ovl-750-840x-base-p0004224 {
description = "Base Overlay p0004224 config";
fdt = "img-overlay-PFC-750-840x-base-p0004224";
};
ovl-750-840x-base-p0004305 {
description = "Base Overlay p0004305 config";
fdt = "img-overlay-PFC-750-840x-base-p0004305";
};
ovl-750-840x-localbus-p0003978 {
description = "Localbus Overlay config";
fdt = "img-overlay-PFC-750-840x-localbus-p0003978";
};
ovl-750-840x-localbus-p0004067 {
description = "Localbus Overlay p0004067 config";
fdt = "img-overlay-PFC-750-840x-localbus-p0004067";
};
ovl-750-840x-interaction-p0003965 {
description = "Interaction Overlay config";
fdt = "img-overlay-PFC-750-840x-interaction-p0003965";
};
ovl-750-840x-interaction-p0004065 {
description = "Interaction Overlay p0004065 config";
fdt = "img-overlay-PFC-750-840x-interaction-p0004065";
};
};
};
@@ -6,14 +6,74 @@
* Copyright (C) 2025 WAGO GmbH & Co. KG - https://www.wago.com/
*/
#include "k3-am623-pfc-750-84xx-wosm.dtsi"
#include "k3-am623-pfc-750-84xx-baseboard.dtsi"
#include "k3-am623-pfc-750-84xx-kbus.dtsi"
#include "k3-am623-pfc-750-84xx-interaction.dtsi"
/dts-v1/;
#include "k3-am623-pfc-750-8xxx-common.dtsi"
#include "k3-am623-pfc-750-84xx-wosm-base.dtsi"
#include "k3-am623-pfc-tpm.dtsi"
/ {
compatible = "wago,am623-pfc-750_8400-100d", "wago,am623-pfc", "ti,am625";
model = "PFC400-750-8400";
aliases {
usb1 = &usb1;
ethernet0 = &cpsw_port2;
ethernet1 = &cpsw_port1;
};
leds-m4 {
compatible = "wago,m4-led-wrapper";
remoteproc = <&mcu_m4fss>;
firmware-name = "wago-led-server-app.elf";
wago,reset-on-init;
#address-cells = <1>;
#size-cells = <0>;
led@0 {
reg = <0>;
label = "sys";
linux,default-trigger = "timer";
led-default-intensity = <255 80 0 64>; /* Orange, brightness 64/255 */
};
led@1 {
reg = <1>;
label = "run";
};
led@2 {
reg = <2>;
label = "io";
};
led@3 {
reg = <3>;
label = "em";
};
led@4 {
reg = <4>;
label = "u1";
};
led@5 {
reg = <5>;
label = "u2";
};
led@6 {
reg = <6>;
label = "u3";
};
led@7 {
reg = <7>;
label = "u4";
};
};
};
&wsysinit {
@@ -21,6 +81,10 @@
board,variant = "pfc400";
};
&slb9670 {
status = "okay";
};
&main_spi2 {
ti,spi-num-cs = <4>;
@@ -23,6 +23,9 @@
};
&mcu_spi0 {
#address-cells = <1>;
#size-cells = <0>;
ksz9477@0 {
compatible = "microchip,ksz9477";
pinctrl-names = "default";
@@ -37,15 +40,13 @@
interrupt-parent = <&main_gpio0>;
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
microchip,single-led-mode;
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 {
reg = <0>;
label = "ethX3";
label = "ethX1";
phy-mode = "internal";
};
@@ -57,19 +58,19 @@
port@2 {
reg = <2>;
label = "ethX1";
label = "ethX3";
phy-mode = "internal";
};
port@3 {
reg = <3>;
label = "ethEM";
label = "ethX4";
phy-mode = "internal";
};
port@4 {
reg = <4>;
label = "ethX4";
label = "ethEM";
phy-mode = "internal";
};
@@ -90,6 +91,9 @@
};
&main_i2c1 {
#address-cells = <1>;
#size-cells = <0>;
eeprom_hw_id_bb: at24c02@54 {
status = "disabled";
compatible = "atmel,24c02";
@@ -18,6 +18,9 @@
};
&main_i2c1 {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
eeprom_hw_id_ib: at24c02@54 {
@@ -291,7 +294,7 @@
* Group triplets from PCA9552 into multicolor LEDs to enable
* led-class-multicolor and pattern trigger RGB support.
*/
/ {
&{/} {
sys_led: sys-led {
compatible = "leds-group-multicolor";
label = "sys";
@@ -8,7 +8,7 @@
#include "k3-am623-pfc-kbus.dtsi"
/ {
&{/} {
/* this name of the gpio-keys device is a
* historical heritage from 3.6.11 kernel.
* the device-name is checked in omsd. So we
@@ -71,6 +71,9 @@
};
&main_i2c1 {
#address-cells = <1>;
#size-cells = <0>;
eeprom_hw_id_lb: at24c02@50 {
status = "disabled";
compatible = "atmel,24c02";
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: GPL-2.0
/ {
compatible = "ti,am623-pfc", "ti,am625";
memory@80000000 {
device_type = "memory";
/* 4 GiB RAM */
reg = <0x00000000 0x80000000 0x00000000 0x80000000>,
<0x00000008 0x80000000 0x00000000 0x80000000>;
};
vcc_1v8_sys: regulator-4 {
compatible = "regulator-fixed";
regulator-name = "vcc_1v8_sys";
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <1800000>;
vin-supply = <&vmain_pd>;
regulator-always-on;
regulator-boot-on;
};
ldo1_sd_1v8_reg: ldo1-sd-1v8-reg {
/* Output VLDO1 of tps65219 */
compatible = "regulator-gpio";
regulator-name = "VDDSHV_SD_1V8_IO_PMIC_GPIO";
pinctrl-names = "default";
pinctrl-0 = <&mcu_sd_vsel_pin>;
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <3300000>;
regulator-boot-on;
vin-supply = <&vcc_5v0>;
gpios = <&mcu_gpio0 10 GPIO_ACTIVE_HIGH>;
states = <1800000 0x0>,
<3300000 0x1>;
};
};
&mcu_pmx0 {
mcu_sd_vsel_pin: mcu-sd-vsel-pins {
pinctrl-single,pins = <
AM64X_MCU_IOPAD(0x028, PIN_OUTPUT, 7) /* (C5) WKUP_UART0_TXD.MCU_GPIO0_10 */
>;
};
};
&cpu0 {
cpu-supply = <&buck1_30_reg>;
};
&cpu1 {
cpu-supply = <&buck1_30_reg>;
};
&cpu2 {
cpu-supply = <&buck1_30_reg>;
};
&cpu3 {
cpu-supply = <&buck1_30_reg>;
};
&main_i2c0 {
tps65219_30: pmic@30 {
compatible = "ti,tps65219";
reg = <0x30>;
system-power-controller;
buck1-supply = <&vcc_5v0>;
buck2-supply = <&vcc_5v0>;
buck3-supply = <&vcc_5v0>;
ldo1-supply = <&vcc_3v3_sys>;
ldo2-supply = <&vcc_1v8_sys>;
ldo3-supply = <&vcc_3v3_sys>;
ldo4-supply = <&vcc_3v3_sys>;
pinctrl-names = "default";
pinctrl-0 = <&pmic_irq_pins_default>;
interrupt-parent = <&main_gpio0>;
interrupts = <68 IRQ_TYPE_EDGE_FALLING>;
/* NOTICE:
*
* We could use "tps65219-gpio" here with GPO1 (EN-DCDC-1V8)
* and GPO2 (CARRIER_PWR_EN for SD-Card and I2C1 devices) enabled,
* but will skip it for now and trust the bootloader or TPS-NVM for the
* correct settings correctly.
*
* For now we declare vcc_1v8_sys as being fixed.
*
*/
regulators {
buck1_30_reg: buck1 {
regulator-name = "VDD_CORE";
regulator-min-microvolt = <850000>;
regulator-max-microvolt = <850000>;
regulator-boot-on;
regulator-always-on;
};
buck2_30_reg: buck2 {
regulator-name = "VCC3V3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-boot-on;
regulator-always-on;
};
buck3_30_reg: buck3 {
regulator-name = "VDD_LPDDR4";
regulator-min-microvolt = <1200000>;
regulator-max-microvolt = <1200000>;
regulator-boot-on;
regulator-always-on;
};
ldo2_30_reg: ldo2 {
regulator-name = "VDDAR_CORE";
regulator-min-microvolt = <850000>;
regulator-max-microvolt = <850000>;
regulator-boot-on;
regulator-always-on;
};
ldo3_30_reg: ldo3 {
regulator-name = "VDDA_1V8";
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <1800000>;
regulator-boot-on;
regulator-always-on;
};
ldo4_30_reg: ldo4 {
regulator-name = "VDDA_2V5";
regulator-min-microvolt = <2500000>;
regulator-max-microvolt = <2500000>;
regulator-boot-on;
regulator-always-on;
};
};
};
};
@@ -6,17 +6,12 @@
* Copyright (C) 2025 WAGO GmbH & Co. KG - https://www.wago.com/
*/
#include "k3-am623-pfc-750-8xxx-common.dtsi"
#include "k3-am623-pfc-tpm.dtsi"
/ {
&{/} {
compatible = "ti,am623-pfc", "ti,am625";
aliases {
usb1 = &usb1;
ethernet0 = &cpsw_port2;
ethernet1 = &cpsw_port1;
};
#address-cells = <2>;
#size-cells = <2>;
cpus {
cpu@0 {
@@ -71,6 +66,9 @@
};
&fss {
#address-cells = <2>;
#size-cells = <2>;
uio_mram: uio@500000000 {
compatible = "uio_pdrv_genirq";
linux,uio-name = "UIO_OSPI_MRAM";
@@ -93,12 +91,6 @@
AM64X_MCU_IOPAD(0x010, PIN_OUTPUT, 0) /* (C9) MCU_SPI0_D1 MOSI */
>;
};
mcu_sd_vsel_pin: mcu-sd-vsel-pins {
pinctrl-single,pins = <
AM64X_MCU_IOPAD(0x028, PIN_OUTPUT, 7) /* (C5) WKUP_UART0_TXD.MCU_GPIO0_10 */
>;
};
};
&main_pmx0 {
@@ -178,6 +170,8 @@
};
&ospi0 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "ti,am654-ospi";
pinctrl-names = "default";
pinctrl-0 = <&ospi0_pins_default>;
@@ -208,10 +202,10 @@
cdns,read-delay = <5>;
/* Tighten timings for 200 MHz (T=5ns) */
cdns,tshsl-ns = <30>; /* CS# high pulse width */
cdns,tsd2d-ns = <30>; /* Delay between back-to-back transfers */
cdns,tchsh-ns = <10>; /* CS# hold time */
cdns,tslch-ns = <10>; /* CS# setup time */
cdns,tshsl-ns = <200>; /* CS# high pulse width (increased for stability) */
cdns,tsd2d-ns = <200>; /* Delay between back-to-back transfers */
cdns,tchsh-ns = <3>; /* CS# hold time */
cdns,tslch-ns = <3>; /* CS# setup time */
spi-rx-bus-width = <8>;
spi-tx-bus-width = <8>;
@@ -230,6 +224,8 @@
};
&main_i2c0 {
#address-cells = <1>;
#size-cells = <0>;
status = "okay";
eeprom_hw_id_wosm: at24c02@50 {
@@ -238,86 +234,6 @@
reg = <0x50>;
pagesize = <16>;
};
tps65219_30: pmic@30 {
compatible = "ti,tps65219";
reg = <0x30>;
system-power-controller;
buck1-supply = <&vcc_5v0>;
buck2-supply = <&vcc_5v0>;
buck3-supply = <&vcc_5v0>;
ldo1-supply = <&vcc_3v3_sys>;
ldo2-supply = <&vcc_1v8_sys>;
ldo3-supply = <&vcc_3v3_sys>;
ldo4-supply = <&vcc_3v3_sys>;
pinctrl-names = "default";
pinctrl-0 = <&pmic_irq_pins_default>;
interrupt-parent = <&main_gpio0>;
interrupts = <68 IRQ_TYPE_EDGE_FALLING>;
/* NOTICE:
*
* We could use "tps65219-gpio" here with GPO1 (EN-DCDC-1V8)
* and GPO2 (CARRIER_PWR_EN for SD-Card and I2C1 devices) enabled,
* but will skip it for now and trust the bootloader or TPS-NVM for the
* correct settings correctly.
*
* For now we declare vcc_1v8_sys as being fixed.
*
*/
regulators {
buck1_30_reg: buck1 {
regulator-name = "VDD_CORE";
regulator-min-microvolt = <750000>;
regulator-max-microvolt = <850000>;
regulator-boot-on;
regulator-always-on;
};
buck2_30_reg: buck2 {
regulator-name = "VCC3V3";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-boot-on;
regulator-always-on;
};
buck3_30_reg: buck3 {
regulator-name = "VDD_LPDDR4";
regulator-min-microvolt = <1200000>;
regulator-max-microvolt = <1200000>;
regulator-boot-on;
regulator-always-on;
};
ldo2_30_reg: ldo2 {
regulator-name = "VDDAR_CORE";
regulator-min-microvolt = <850000>;
regulator-max-microvolt = <850000>;
regulator-boot-on;
regulator-always-on;
};
ldo3_30_reg: ldo3 {
regulator-name = "VDDA_1V8";
regulator-min-microvolt = <1800000>;
regulator-max-microvolt = <1800000>;
regulator-boot-on;
regulator-always-on;
};
ldo4_30_reg: ldo4 {
regulator-name = "VDDA_2V5";
regulator-min-microvolt = <2500000>;
regulator-max-microvolt = <2500000>;
regulator-boot-on;
regulator-always-on;
};
};
};
};
&main_i2c1 {
@@ -28,27 +28,27 @@
opp-table {
opp-200000000 {
opp-microvolt = <750000>;
opp-microvolt = <850000>;
};
opp-400000000 {
opp-microvolt = <750000>;
opp-microvolt = <850000>;
};
opp-600000000 {
opp-microvolt = <750000>;
opp-microvolt = <850000>;
};
opp-800000000 {
opp-microvolt = <750000>;
opp-microvolt = <850000>;
};
opp-1000000000 {
opp-microvolt = <750000>;
opp-microvolt = <850000>;
};
opp-1250000000 {
opp-microvolt = <750000>;
opp-microvolt = <850000>;
};
opp-1400000000 {
@@ -110,6 +110,17 @@
no-map;
};
/*
* U-Boot IPC side-door shared memory.
* Written by U-Boot (A53) to send LED commands to the M4.
* Must not be touched by the Linux kernel.
* See UBOOT_IPC_SHMEM_PHYS in wago_m4_led_protocol.h.
*/
uboot_ipc_shm: uboot-ipc-shm@9cbff000 {
reg = <0x00 0x9cbff000 0x00 0x1000>;
no-map;
};
mcu_m4fss_dma_memory_region: m4f-dma-memory@9cb00000 {
compatible = "shared-dma-pool";
reg = <0x00 0x9cb00000 0x00 0x100000>;
@@ -285,6 +296,7 @@
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&main_uart4_pins_default>;
rts-gpios = <&main_gpio0 38 GPIO_ACTIVE_HIGH>;
};
&main_i2c0 { /* type label */
@@ -394,5 +406,6 @@
mboxes = <&mailbox0_cluster0 &mbox_m4_0>;
memory-region = <&mcu_m4fss_dma_memory_region>,
<&mcu_m4fss_memory_region>;
ti,no-auto-boot;
status = "okay";
};
@@ -20,6 +20,9 @@
};
&main_spi0 {
#address-cells = <1>;
#size-cells = <0>;
spi-rt;
spi-rt-prio = <81>;
@@ -7,6 +7,9 @@
*/
&main_i2c1 {
#address-cells = <1>;
#size-cells = <0>;
/*
* PCA9538, 8-Bit I/O Expander
*/
@@ -29,8 +32,7 @@
};
&main_uart4 { /* RS485 Interface */
rts-gpios = <&main_gpio0 38 GPIO_ACTIVE_HIGH>;
rs485-term-gpios = <&io_exp_8bit 0 GPIO_ACTIVE_HIGH>;
/* RS485 termination is controlled via GPIO userspace tools */
rs485-rts-active-high;
linux,rs485-enabled-at-boot-time;
};
+4 -1
View File
@@ -7,11 +7,14 @@
*/
&main_i2c1 {
#address-cells = <1>;
#size-cells = <0>;
rtc@52 {
compatible = "microcrystal,rv3028";
reg = <0x52>;
/*
/*
* The backup goldcap is supplied with a higher voltage (+5V)
* then the RTC (+3,3) itself. It requires a special setup
* to avoid hardware failure.
@@ -0,0 +1,15 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-baseboard.dtsi"
&mcu_spi0 {
ksz9477@0 {
microchip,single-led-mode;
};
};
@@ -0,0 +1,24 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-baseboard.dtsi"
&main_pmx0 {
main_uart4_pins_default: main-uart4-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x0b0, PIN_INPUT, 3) /* (K22) UART4_RXD */
AM62X_IOPAD(0x0b4, PIN_OUTPUT, 3) /* (K24) UART4_TXD */
AM62X_IOPAD(0x0098, PIN_OUTPUT_PULLDOWN, 7) /* (U23) GPIO0_37 (RS-DE) */
>;
};
};
&main_uart4 {
pinctrl-0 = <&main_uart4_pins_default>;
rts-gpios = <&main_gpio0 37 GPIO_ACTIVE_HIGH>;
};
@@ -0,0 +1,9 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-baseboard.dtsi"
@@ -0,0 +1,9 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-interaction.dtsi"
@@ -0,0 +1,9 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-interaction.dtsi"
@@ -0,0 +1,9 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-kbus.dtsi"
@@ -0,0 +1,49 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-kbus.dtsi"
&mcu_pmx0 {
oms_mcu_pins_default: oms-mcu-default-pins {
pinctrl-single,pins = <
AM62X_MCU_IOPAD(0x0044, PIN_INPUT, 7) /* (A8) MCU_I2C0_SCL.MCU_GPIO0_17 nRUN-BAS */
AM62X_MCU_IOPAD(0x0048, PIN_INPUT, 7) /* (D10) MCU_I2C0_SDA.MCU_GPIO0_18 nSTOP-BAS */
>;
};
};
&oms_pins_default {
pinctrl-single,pins = <
AM62X_IOPAD(0x01a4, PIN_INPUT, 7) /* (B20) MCASP0_ACLKX.GPIO1_11 IO-RAB */
>;
};
&oms {
pinctrl-0 = <&oms_pins_default &oms_mcu_pins_default>;
poll-interval = <100>;
oms_run: run {
label = "RUN";
gpios = <&mcu_gpio0 17 GPIO_ACTIVE_LOW>;
linux,code = <1>;
linux,input-type = <5>;
debounce-interval = <1>;
};
oms_stop: stop {
label = "STOP";
gpios = <&mcu_gpio0 18 GPIO_ACTIVE_LOW>;
linux,code = <2>;
linux,input-type = <5>;
debounce-interval = <1>;
};
};
&oms_stop_run {
status = "disabled";
};
@@ -0,0 +1,9 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-wosm.dtsi"
@@ -0,0 +1,44 @@
/dts-v1/;
/plugin/;
#include "k3-pinctrl.h"
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/leds/common.h>
#include "k3-am623-pfc-750-84xx-wosm.dtsi"
&main_pmx0 {
mram_reset_pins_default: mram-reset-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x0004, PIN_OUTPUT, 7) /* (G25) OSPI0_LBCLKO.GPIO0_1 (nRST) */
>;
};
em_reset_pins_default: em-reset-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x01a0, PIN_OUTPUT, 7) /* (E18) MCASP0_AXR0.GPIO1_10 (nRST-EM2) */
AM62X_IOPAD(0x019c, PIN_OUTPUT, 7) /* (B18) MCASP0_AXR1.GPIO1_9 (nRST-EM3) */
>;
};
rs485_2_pins_default: rs485-2-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x0098, PIN_OUTPUT, 7) /* (U23) GPMC0_WAIT0.GPIO0_38 (RS-2.DE) */
AM62X_IOPAD(0x00c0, PIN_INPUT, 4) /* (W25) VOUT0_DATA2.UART3_RXD (RS-2.RXD) */
AM62X_IOPAD(0x00c4, PIN_OUTPUT, 4) /* (W24) VOUT0_DATA3.UART3_TXD (RS-2.TXD) */
>;
};
can_interface_pins_default: can-interface-default-pins {
pinctrl-single,pins = <
AM62X_IOPAD(0x01dc, PIN_INPUT, 0) /* (E15) MCAN0_RX (CAN.RXD) */
AM62X_IOPAD(0x01d8, PIN_OUTPUT, 0) /* (C15) MCAN0_TX (CAN.TXD) */
>;
};
};
&mram {
pinctrl-0 = <&mram_irq_pins_default &mram_reset_pins_default>;
reset-gpios = <&main_gpio0 1 GPIO_ACTIVE_LOW>;
};
+5 -1
View File
@@ -189,7 +189,6 @@ CONFIG_TUN=y
CONFIG_VETH=m
CONFIG_NET_DSA_MICROCHIP_KSZ_COMMON=y
CONFIG_NET_DSA_MICROCHIP_KSZ_SPI=y
CONFIG_NET_DSA_MICROCHIP_KSZ9477_SYSFS=y
# CONFIG_NET_VENDOR_ALACRITECH is not set
# CONFIG_NET_VENDOR_AMAZON is not set
# CONFIG_NET_VENDOR_AMD is not set
@@ -342,6 +341,9 @@ CONFIG_MMC_SDHCI_PLTFM=y
CONFIG_MMC_SDHCI_AM654=y
CONFIG_NEW_LEDS=y
CONFIG_LEDS_CLASS=y
CONFIG_LEDS_CLASS_MULTICOLOR=y
CONFIG_LEDS_WAGO_M4_WRAPPER=y
CONFIG_LEDS_WAGO_M4_WRAPPER_SYSFS_PASSTHROUGH=y
CONFIG_LEDS_GPIO=y
CONFIG_LEDS_PCA955X=y
CONFIG_LEDS_TRIGGERS=y
@@ -349,6 +351,7 @@ CONFIG_LEDS_TRIGGER_TIMER=y
CONFIG_LEDS_TRIGGER_HEARTBEAT=y
CONFIG_LEDS_TRIGGER_DEFAULT_ON=y
CONFIG_LEDS_TRIGGER_PANIC=y
CONFIG_LEDS_TRIGGER_PATTERN=y
CONFIG_EDAC=y
CONFIG_RTC_CLASS=y
CONFIG_RTC_DRV_RV3028=y
@@ -467,6 +470,7 @@ CONFIG_DMA_CMA=y
CONFIG_CMA_SIZE_MBYTES=32
CONFIG_IRQ_POLL=y
CONFIG_PRINTK_TIME=y
CONFIG_DYNAMIC_DEBUG=y
CONFIG_DEBUG_INFO_DWARF_TOOLCHAIN_DEFAULT=y
CONFIG_MAGIC_SYSRQ=y
CONFIG_DEBUG_FS=y
+10 -5
View File
@@ -42,22 +42,27 @@ static ssize_t multi_intensity_store(struct device *dev,
mutex_lock(&led_cdev->led_access);
for (i = 0; i < mcled_cdev->num_colors; i++) {
for (i = 0; i < LED_COLOR_ID_MAX; i++) {
ret = sscanf(buf + offset, "%i%n",
&intensity_value[i], &nrchars);
if (ret != 1) {
ret = -EINVAL;
goto err_out;
}
if (ret != 1)
break;
offset += nrchars;
}
/* Expect newline; reject surplus trailing chars */
offset++;
if (offset < size) {
ret = -EINVAL;
goto err_out;
}
/* Require at least the physical number of colors */
if (i < mcled_cdev->num_colors) {
ret = -EINVAL;
goto err_out;
}
for (i = 0; i < mcled_cdev->num_colors; i++)
mcled_cdev->subled_info[i].intensity = intensity_value[i];
+36
View File
@@ -51,4 +51,40 @@ 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-tty/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" channel using ASCII text commands and exposes each LED
as a Linux LED multiclass (RGB) device under
/sys/class/leds/m4-led<N>/.
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
+24 -12
View File
@@ -70,6 +70,7 @@ static int leds_gmc_probe(struct platform_device *pdev)
struct leds_multicolor *priv;
unsigned int max_brightness = 0;
int i, ret, count = 0;
bool allow_subled_writes = false;
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
@@ -121,28 +122,39 @@ static int leds_gmc_probe(struct platform_device *pdev)
priv->mc_cdev.num_colors = count;
init_data.fwnode = dev_fwnode(dev);
allow_subled_writes = fwnode_property_present(init_data.fwnode, "mc-allow-subled-writes");
ret = devm_led_classdev_multicolor_register_ext(dev, &priv->mc_cdev, &init_data);
if (ret)
return dev_err_probe(dev, ret, "failed to register multicolor LED for %s.\n",
cdev->name);
ret = leds_gmc_set(cdev, cdev->brightness);
if (ret)
return dev_err_probe(dev, ret, "failed to set LED value for %s.", cdev->name);
/* Honor DT default-state using LED core helper */
{
int def = led_init_default_state_get(dev_fwnode(dev));
if (def != LEDS_DEFSTATE_KEEP) {
ret = leds_gmc_set(cdev, cdev->brightness);
if (ret)
return dev_err_probe(dev, ret, "failed to set LED value for %s.", cdev->name);
}
}
for (i = 0; i < count; i++) {
struct led_classdev *led_cdev = priv->monochromatics[i];
/*
* Make the individual LED sysfs interface read-only to prevent the user
* to change the brightness of the individual LEDs of the group.
*/
mutex_lock(&led_cdev->led_access);
led_sysfs_disable(led_cdev);
mutex_unlock(&led_cdev->led_access);
/* Optionally keep sub-LED sysfs writable for legacy userspace */
if (!allow_subled_writes) {
/*
* Make the individual LED sysfs interface read-only to prevent the user
* to change the brightness of the individual LEDs of the group.
*/
mutex_lock(&led_cdev->led_access);
led_sysfs_disable(led_cdev);
mutex_unlock(&led_cdev->led_access);
/* Restore the write access to the LED sysfs when the group is destroyed */
devm_add_action_or_reset(dev, restore_sysfs_write_access, led_cdev);
/* Restore the write access to the LED sysfs when the group is destroyed */
devm_add_action_or_reset(dev, restore_sysfs_write_access, led_cdev);
}
}
return 0;
+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 */
+900
View File
@@ -0,0 +1,900 @@
// 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"
#define WAGO_LED_NUM_LEDS 10
#define WAGO_LED_NUM_CHANNELS 3 /* R, G, B */
/* 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 v2");
MODULE_ALIAS("platform:" DRIVER_NAME);
+114 -15
View File
@@ -13,6 +13,9 @@
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/ctype.h>
#include <linux/string.h>
#include <linux/led-class-multicolor.h>
#define MAX_PATTERNS 1024
/*
@@ -34,8 +37,56 @@ struct pattern_trig_data {
bool is_indefinite;
bool is_hw_pattern;
struct timer_list timer;
#if IS_ENABLED(CONFIG_LEDS_CLASS_MULTICOLOR)
struct led_classdev_mc *mcled;
#endif
};
#if IS_ENABLED(CONFIG_LEDS_CLASS_MULTICOLOR)
static void pattern_trig_apply_color(struct pattern_trig_data *data,
const struct led_pattern *pat)
{
int i;
unsigned int max_intensity;
bool has_red = false, has_green = false, has_blue = false;
if (!data->mcled || !pat->has_color)
return;
max_intensity = data->mcled->led_cdev.max_brightness;
for (i = 0; i < data->mcled->num_colors; i++) {
switch (data->mcled->subled_info[i].color_index) {
case LED_COLOR_ID_RED:
data->mcled->subled_info[i].intensity =
DIV_ROUND_CLOSEST(pat->red * max_intensity, 255);
has_red = true;
break;
case LED_COLOR_ID_GREEN:
data->mcled->subled_info[i].intensity =
DIV_ROUND_CLOSEST(pat->green * max_intensity, 255);
has_green = true;
break;
case LED_COLOR_ID_BLUE:
data->mcled->subled_info[i].intensity =
DIV_ROUND_CLOSEST(pat->blue * max_intensity, 255);
has_blue = true;
break;
default:
break;
}
}
if ((!has_red && pat->red) || (!has_green && pat->green) ||
(!has_blue && pat->blue))
dev_warn_once(data->led_cdev->dev,
"pattern: color component set for absent channel\n");
}
#else
static inline void pattern_trig_apply_color(struct pattern_trig_data *data,
const struct led_pattern *pat) { }
#endif
static void pattern_trig_update_patterns(struct pattern_trig_data *data)
{
data->curr = data->next;
@@ -81,6 +132,7 @@ static void pattern_trig_timer_function(struct timer_list *t)
if (data->curr->brightness == data->next->brightness) {
/* Step change of brightness */
pattern_trig_apply_color(data, data->curr);
led_set_brightness(data->led_cdev,
data->curr->brightness);
mod_timer(&data->timer,
@@ -104,6 +156,8 @@ static void pattern_trig_timer_function(struct timer_list *t)
continue;
}
if (data->delta_t == 0)
pattern_trig_apply_color(data, data->curr);
led_set_brightness(data->led_cdev,
pattern_trig_compute_brightness(data));
mod_timer(&data->timer,
@@ -207,10 +261,18 @@ static ssize_t pattern_trig_show_patterns(struct pattern_trig_data *data,
goto out;
for (i = 0; i < data->npatterns; i++) {
count += scnprintf(buf + count, PAGE_SIZE - count,
"%d %u ",
data->patterns[i].brightness,
data->patterns[i].delta_t);
const struct led_pattern *p = &data->patterns[i];
if (!hw_pattern && p->has_color)
count += scnprintf(buf + count, PAGE_SIZE - count,
"%d:%02x%02x%02x %u ",
p->brightness, p->red, p->green,
p->blue, p->delta_t);
else
count += scnprintf(buf + count, PAGE_SIZE - count,
"%d %u ",
p->brightness,
p->delta_t);
}
buf[count - 1] = '\n';
@@ -223,22 +285,52 @@ out:
static int pattern_trig_store_patterns_string(struct pattern_trig_data *data,
const char *buf, size_t count)
{
int ccount, cr, offset = 0;
int consumed, ccount;
unsigned int brightness, delta_t;
size_t offset = 0;
u8 r, g, b;
while (offset < count - 1 && data->npatterns < MAX_PATTERNS) {
cr = 0;
ccount = sscanf(buf + offset, "%u %u %n",
&data->patterns[data->npatterns].brightness,
&data->patterns[data->npatterns].delta_t, &cr);
while (offset < count && data->npatterns < MAX_PATTERNS) {
while (offset < count && isspace(buf[offset]))
offset++;
if (offset >= count)
break;
if (ccount != 2 ||
data->patterns[data->npatterns].brightness > data->led_cdev->max_brightness) {
if (!data->is_hw_pattern) {
consumed = 0;
ccount = sscanf(buf + offset,
"%u:%2hhx%2hhx%2hhx %u %n",
&brightness, &r, &g, &b,
&delta_t, &consumed);
if (ccount == 5) {
if (brightness > data->led_cdev->max_brightness) {
data->npatterns = 0;
return -EINVAL;
}
data->patterns[data->npatterns].brightness = brightness;
data->patterns[data->npatterns].delta_t = delta_t;
data->patterns[data->npatterns].has_color = true;
data->patterns[data->npatterns].red = r;
data->patterns[data->npatterns].green = g;
data->patterns[data->npatterns].blue = b;
offset += consumed;
data->npatterns++;
continue;
}
}
consumed = 0;
ccount = sscanf(buf + offset, "%u %u %n", &brightness, &delta_t, &consumed);
if (ccount != 2 || brightness > data->led_cdev->max_brightness) {
data->npatterns = 0;
return -EINVAL;
}
offset += cr;
data->patterns[data->npatterns].brightness = brightness;
data->patterns[data->npatterns].delta_t = delta_t;
data->npatterns++;
if (consumed == 0)
break;
offset += consumed;
}
return 0;
@@ -274,6 +366,8 @@ static ssize_t pattern_trig_store_patterns(struct led_classdev *led_cdev,
data->is_hw_pattern = hw_pattern;
data->npatterns = 0;
/* Zero all pattern entries so per-tuple color field clearing is unnecessary */
memset(data->patterns, 0, sizeof(data->patterns));
if (buf)
err = pattern_trig_store_patterns_string(data, buf, count);
@@ -404,6 +498,11 @@ static int pattern_trig_activate(struct led_classdev *led_cdev)
data->last_repeat = -1;
mutex_init(&data->lock);
data->led_cdev = led_cdev;
#if IS_ENABLED(CONFIG_LEDS_CLASS_MULTICOLOR)
if (led_cdev->color == LED_COLOR_ID_MULTI ||
led_cdev->color == LED_COLOR_ID_RGB)
data->mcled = lcdev_to_mccdev(led_cdev);
#endif
led_set_trigger_data(led_cdev, data);
timer_setup(&data->timer, pattern_trig_timer_function, 0);
led_cdev->activated = true;
@@ -459,5 +558,5 @@ module_exit(pattern_trig_exit);
MODULE_AUTHOR("Raphael Teysseyre <rteysseyre@gmail.com>");
MODULE_AUTHOR("Baolin Wang <baolin.wang@linaro.org>");
MODULE_DESCRIPTION("LED Pattern trigger");
MODULE_DESCRIPTION("LED Pattern trigger (optional per-node RGB)");
MODULE_LICENSE("GPL v2");
+6 -5
View File
@@ -1381,11 +1381,12 @@ static int mtdchar_mmap(struct file *file, struct vm_area_struct *vma)
struct mtd_info *mtd = mfi->mtd;
struct map_info *map = mtd->priv;
/* This is broken because it assumes the MTD device is map-based
and that mtd->priv is a valid struct map_info. It should be
replaced with something that uses the mtd_get_unmapped_area()
operation properly. */
if (0 /*mtd->type == MTD_RAM || mtd->type == MTD_ROM*/) {
/*
* Only MTD_RAM devices that have a valid map_info with a physical
* address (e.g. MRAM on a Cadence OSPI DAC window) support mmap().
* All other MTD types return -ENODEV as before.
*/
if (mtd->type == MTD_RAM && map && map->phys != NO_XIP) {
#ifdef pgprot_noncached
if (file->f_flags & O_DSYNC || map->phys >= __pa(high_memory))
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
+26 -2
View File
@@ -14,6 +14,7 @@
#include <linux/math64.h>
#include <linux/module.h>
#include <linux/mtd/mtd.h>
#include <linux/mtd/map.h>
#include <linux/mtd/spi-nor.h>
#include <linux/mutex.h>
#include <linux/of_platform.h>
@@ -3420,8 +3421,24 @@ static void spi_nor_set_mtd_info(struct spi_nor *nor)
mtd->dev.parent = dev;
if (!mtd->name)
mtd->name = dev_name(dev);
mtd->type = MTD_NORFLASH;
mtd->flags = MTD_CAP_NORFLASH;
/*
* If a fixup hook has already populated mtd->priv with a struct
* map_info (e.g. Everspin MRAM on Cadence OSPI DAC window), honour
* that and advertise the device as byte-addressable RAM so that
* mtdchar_mmap() can call vm_iomap_memory() for zero-copy access.
* Otherwise fall back to the standard NOR-flash type.
*/
if (mtd->priv) {
mtd->type = MTD_RAM;
mtd->flags = MTD_CAP_RAM;
dev_info(dev,
"MTD type set to MTD_RAM (mmap enabled), "
"phys base: %pa\n",
&((struct map_info *)mtd->priv)->phys);
} else {
mtd->type = MTD_NORFLASH;
mtd->flags = MTD_CAP_NORFLASH;
}
/* Unset BIT_WRITEABLE to enable JFFS2 write buffer for ECC'd NOR */
if (nor->flags & SNOR_F_ECC)
mtd->flags &= ~MTD_BIT_WRITEABLE;
@@ -3466,6 +3483,13 @@ static void spi_nor_everspin_reboot_fix(struct spi_nor *nor)
if (!nor->spimem || !nor->dev)
return;
/*
* Limit this hack to nodes explicitly named "mram" to prevent
* sending blind 8-8-8 resets to unrelated SPI NOR flashes.
*/
if (!nor->dev->of_node || !of_node_name_eq(nor->dev->of_node, "mram"))
return;
/*
* Logging as dev_info so it appears in dmesg during boot.
* This helps verify if the fix is being executed.
+59
View File
@@ -5,10 +5,18 @@
*/
#include <linux/mtd/spi-nor.h>
#include <linux/mtd/map.h>
#include <linux/delay.h>
#include <linux/platform_device.h>
#include <linux/spi/spi.h>
#include <linux/spi/spi-mem.h>
#include "core.h"
/* Private data stored in nor->priv for MRAM mmap support */
struct everspin_mram_priv {
struct map_info map;
};
/* Optimization for 200 MHz: 20 Dummy Cycles are typically required */
#define EVERSPIN_MRAM_DUMMY_CYCLES 8
#define EVERSPIN_MRAM_DUMMY_CYCLES_FAST 20
@@ -154,6 +162,7 @@ static void everspin_mram_default_init(struct spi_nor *nor)
ret = spi_nor_read_any_reg(nor, &op, SNOR_PROTO_1_1_1);
if (!ret) {
u8 sr1 = nor->bouncebuf[0];
dev_info(nor->dev, "Initial SR1: 0x%02x (BP-Bits: 0x%x)\n", sr1, (sr1 & 0x3c) >> 2);
if (sr1 & GENMASK(5, 2))
everspin_mram_unlock(nor);
@@ -190,6 +199,7 @@ static void everspin_mram_default_init(struct spi_nor *nor)
if (!ret) {
/* Access the result from the DMA-safe bounce buffer */
u8 status = nor->bouncebuf[0];
dev_info(nor->dev, "MRAM Status Register (8s-0-8s): 0x%02x\n", status);
}
@@ -289,6 +299,9 @@ static int everspin_mram_setup(struct spi_nor *nor,
static int everspin_mram_late_init(struct spi_nor *nor)
{
struct spi_nor_flash_parameter *params = nor->params;
struct platform_device *ctlr_pdev;
struct everspin_mram_priv *priv;
struct resource *res_ahb;
dev_info(nor->dev, "Finalizing 8s-8s-8s STR: Write/Read fully functional.\n");
@@ -326,6 +339,52 @@ static int everspin_mram_late_init(struct spi_nor *nor)
nor->params->setup = everspin_mram_setup;
/*
* Set up MTD_RAM so that /dev/mtd0 supports mmap().
*
* The Cadence OSPI controller exposes the flash memory as a directly
* accessible window in the CPU address space (DAC / AHB mode). We
* read the physical base address of that window from the controller's
* platform_device resource[1] the same resource that cqspi_probe()
* uses for ioremap. This lets mtdchar_mmap() call vm_iomap_memory()
* and hand the physical pages directly to user-space.
*/
priv = devm_kzalloc(nor->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/*
* nor->spimem->spi->controller->dev.parent is the platform_device
* of the Cadence QSPI controller. Its IORESOURCE_MEM[1] is the AHB
* (DAC) window index 0 is the APB register block.
*/
ctlr_pdev = to_platform_device(nor->spimem->spi->controller->dev.parent);
res_ahb = platform_get_resource(ctlr_pdev, IORESOURCE_MEM, 1);
if (!res_ahb) {
dev_warn(nor->dev,
"MRAM: cannot find AHB resource mmap() disabled\n");
devm_kfree(nor->dev, priv);
return 0;
}
priv->map.name = nor->mtd.name;
priv->map.phys = res_ahb->start;
priv->map.size = resource_size(res_ahb);
priv->map.virt = NULL; /* not needed for mmap path */
nor->priv = priv;
/*
* mtd->type and mtd->flags will be set to MTD_RAM / MTD_CAP_RAM
* by spi_nor_set_mtd_info() in core.c once it sees mtd->priv != NULL.
* mtdchar_mmap() will then call vm_iomap_memory() via map->phys.
*/
nor->mtd.priv = &priv->map;
dev_info(nor->dev,
"MRAM: AHB window at %pa size %pa mmap() enabled\n",
&res_ahb->start, &priv->map.size);
return 0;
}
+1
View File
@@ -899,6 +899,7 @@ static int k3_m4_rproc_probe(struct platform_device *pdev)
rproc->has_iommu = false;
rproc->recovery_disabled = true;
rproc->auto_boot = !of_property_read_bool(np, "ti,no-auto-boot");
if (data->uses_lreset) {
rproc->ops->prepare = k3_m4_rproc_prepare;
rproc->ops->unprepare = k3_m4_rproc_unprepare;
+14 -3
View File
@@ -706,13 +706,24 @@ static inline void led_classdev_notify_brightness_hw_changed(
#endif
/**
* struct led_pattern - pattern interval settings
* @delta_t: pattern interval delay, in milliseconds
* @brightness: pattern interval brightness
* struct led_pattern - pattern interval settings (optional RGB per node)
* @delta_t: pattern interval delay (ms)
* @brightness: pattern interval brightness (0..max_brightness)
* @has_color: true if this node specifies new RGB intensities
* @red: red component (0..255, valid if has_color)
* @green: green component (0..255, valid if has_color)
* @blue: blue component (0..255, valid if has_color)
*
* If has_color is false the previously applied color (if any) remains
* unchanged; only brightness/duration are processed.
*/
struct led_pattern {
u32 delta_t;
int brightness;
bool has_color;
u8 red;
u8 green;
u8 blue;
};
enum led_audio {
+1 -1
View File
@@ -1 +1 @@
-w05.12.00
-w05.13.01
+383
View File
@@ -0,0 +1,383 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0
#
# m4-led-perf-test.sh - Performance / stress test for WAGO M4 RGB LED strip
#
# Exercises all ten M4 strip LEDs (sys, run, io, em, u1-u6) by activating
# kernel LED triggers at various frequencies and patterns. Designed to run
# on a BusyBox-based target (ash/sh compatible, no bashisms).
#
# Triggers supported:
# pattern - (default) three phases: fade, flash-fast, flash-slow
# timer - simple on/off blink at staggered frequencies per LED
# heartbeat - kernel heartbeat trigger (frequency fixed by kernel)
#
# Usage:
# m4-led-perf-test.sh [--trigger=<pattern|timer|heartbeat>]
# [--duration=<seconds>]
# [--help] [--version]
#
# Author: WAGO GmbH & Co. KG
VERSION="1.0.0"
LED_SYSFS="/sys/class/leds"
# All M4 strip LED names (must match DT 'label' properties)
LEDS="sys run io em u1 u2 u3 u4 u5 u6"
# Default options
OPT_TRIGGER="pattern"
OPT_DURATION=30
# ---------------------------------------------------------------------------
# Logging helpers
# ---------------------------------------------------------------------------
log() { echo "[ m4-led-test ] $*"; }
info() { log "INFO $*"; }
warn() { log "WARN $*"; }
err() { log "ERROR $*" >&2; }
die() { err "$*"; cleanup; exit 1; }
# ---------------------------------------------------------------------------
# Usage / version
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
Usage: $(basename "$0") [OPTIONS]
Performance / stress test for the WAGO M4 RGB LED strip.
Activates kernel LED triggers on all ten strip LEDs at various
frequencies to stress the RPMsg / M4 communication path.
Options:
--trigger=<mode> Trigger mode to use (default: pattern)
pattern - three phases: fade in/out, fast flash,
slow flash with colour cycling
timer - simple on/off blink at staggered
frequencies (10 ms .. 500 ms)
heartbeat - kernel heartbeat trigger
--duration=<sec> How long to run the test in seconds (default: 30)
--version Print version and exit
--help Print this help and exit
Examples:
$(basename "$0")
$(basename "$0") --trigger=timer --duration=60
$(basename "$0") --trigger=heartbeat
EOF
}
# ---------------------------------------------------------------------------
# Argument parsing (BusyBox-compatible, no getopt)
# ---------------------------------------------------------------------------
for arg in "$@"; do
case "$arg" in
--help)
usage
exit 0
;;
--version)
echo "$(basename "$0") version $VERSION"
exit 0
;;
--trigger=*)
OPT_TRIGGER="${arg#--trigger=}"
;;
--duration=*)
OPT_DURATION="${arg#--duration=}"
;;
*)
err "Unknown option: $arg"
usage >&2
exit 1
;;
esac
done
# Validate trigger
case "$OPT_TRIGGER" in
pattern|timer|heartbeat) ;;
*) die "Invalid trigger '$OPT_TRIGGER'. Choose: pattern, timer, heartbeat" ;;
esac
# Validate duration (must be a positive integer)
case "$OPT_DURATION" in
''|*[!0-9]*) die "Invalid duration '$OPT_DURATION': must be a positive integer" ;;
esac
[ "$OPT_DURATION" -gt 0 ] || die "Duration must be > 0"
# ---------------------------------------------------------------------------
# LED sysfs helpers
# ---------------------------------------------------------------------------
# led_path <name> -> /sys/class/leds/<name>
led_path() { echo "${LED_SYSFS}/$1"; }
# led_write <name> <file> <value>
led_write() {
local path
path="$(led_path "$1")/$2"
if [ ! -w "$path" ]; then
warn "Not writable: $path — skipping"
return 1
fi
printf '%s' "$3" > "$path" 2>/dev/null || warn "Write failed: $path <- $3"
}
# led_set_trigger <name> <trigger>
led_set_trigger() { led_write "$1" trigger "$2"; }
# led_set_brightness <name> <value 0-255>
led_set_brightness() { led_write "$1" brightness "$2"; }
# led_set_multi_intensity <name> <R> <G> <B>
led_set_multi_intensity() { led_write "$1" multi_intensity "$2 $3 $4"; }
# led_set_pattern <name> <pattern-string>
led_set_pattern() { led_write "$1" pattern "$2"; }
# Check which LEDs are actually present in sysfs
check_leds() {
local found=0 missing=0 name
for name in $LEDS; do
if [ -d "$(led_path "$name")" ]; then
found=$((found + 1))
else
warn "LED '$name' not found in $LED_SYSFS — skipping"
missing=$((missing + 1))
fi
done
info "Found $found / $(echo $LEDS | wc -w) expected LEDs"
[ "$found" -gt 0 ] || die "No M4 LEDs found in $LED_SYSFS. Is the driver loaded?"
}
# Return only the LEDs that exist in sysfs
present_leds() {
local name
for name in $LEDS; do
[ -d "$(led_path "$name")" ] && printf '%s ' "$name"
done
}
# ---------------------------------------------------------------------------
# Cleanup: restore all LEDs to 'none' trigger and brightness=0
# ---------------------------------------------------------------------------
cleanup() {
info "Restoring all LEDs (trigger=none, brightness=0) ..."
local name
for name in $(present_leds); do
led_set_trigger "$name" "none"
led_set_brightness "$name" 0
done
info "Cleanup done."
}
# Trap SIGINT / SIGTERM so Ctrl-C always restores LEDs
trap 'info "Interrupted — cleaning up ..."; cleanup; exit 130' INT TERM
# ---------------------------------------------------------------------------
# Trigger implementations
# ---------------------------------------------------------------------------
# --- HEARTBEAT --------------------------------------------------------------
run_heartbeat() {
info "Setting trigger=heartbeat on all LEDs ..."
local name
for name in $(present_leds); do
# White at full brightness
led_set_multi_intensity "$name" 255 255 255
led_set_brightness "$name" 255
led_set_trigger "$name" heartbeat
info " $name -> heartbeat"
done
}
# --- TIMER ------------------------------------------------------------------
#
# Stagger delay_on / delay_off across LEDs to create a "running" effect.
# Pairs (on_ms, off_ms) cycle through a range from fast (50/50) to slow
# (500/500) and back.
run_timer() {
info "Setting trigger=timer on all LEDs with staggered frequencies ..."
# Pre-defined (on_ms off_ms R G B) tuples — one per LED
# Colours: red, green, blue, cyan, magenta, yellow, white, orange, lime, teal
set -- \
"50 50 255 0 0" \
"100 100 0 255 0" \
"200 200 0 0 255" \
"300 300 0 255 255" \
"400 400 255 0 255" \
"500 500 255 255 0" \
"150 350 255 255 255" \
"80 420 255 128 0" \
"250 250 128 255 0" \
"350 150 0 128 128"
local name idx on off r g b
idx=1
for name in $(present_leds); do
# Extract the idx-th tuple from positional params
eval "tuple=\$$idx"
on=$(echo "$tuple" | awk '{print $1}')
off=$(echo "$tuple" | awk '{print $2}')
r=$(echo "$tuple" | awk '{print $3}')
g=$(echo "$tuple" | awk '{print $4}')
b=$(echo "$tuple" | awk '{print $5}')
led_set_multi_intensity "$name" "$r" "$g" "$b"
led_set_brightness "$name" 255
led_set_trigger "$name" timer
led_write "$name" delay_on "$on"
led_write "$name" delay_off "$off"
info " $name -> timer on=${on}ms off=${off}ms RGB($r,$g,$b)"
idx=$((idx + 1))
done
}
# --- PATTERN ----------------------------------------------------------------
#
# Three phases, each lasting 1/3 of the total duration:
#
# Phase 1 — FADE: smooth brightness ramp up/down (pattern trigger)
# Phase 2 — FAST FLASH: rapid on/off (50 ms) in different colours per LED
# Phase 3 — SLOW FLASH: slow pulse (400 ms) with colour cycling
#
# Pattern format: "<brightness> <duration_ms> <brightness> <duration_ms> ..."
# brightness 0-255, duration in ms. The pattern trigger loops the sequence.
# Smooth fade: 0->255 in steps then 255->0, total ~2 s per cycle
FADE_PATTERN="0 100 32 100 64 100 96 100 128 100 160 100 192 100 224 100 255 100 224 100 192 100 160 100 128 100 96 100 64 100 32 100"
# Fast flash: full on 50 ms, off 50 ms
FLASH_FAST_PATTERN="255 50 0 50"
# Slow flash: full on 400 ms, off 400 ms
FLASH_SLOW_PATTERN="255 400 0 400"
run_pattern_phase() {
local phase_name="$1"
local pattern="$2"
shift 2
# Remaining args: "name R G B" tuples
info " Phase: $phase_name"
local name r g b
for name in $(present_leds); do
# Consume next R G B from positional args
r="$1"; g="$2"; b="$3"; shift 3 2>/dev/null || true
led_set_trigger "$name" none
led_set_multi_intensity "$name" "$r" "$g" "$b"
led_set_brightness "$name" 255
led_set_trigger "$name" pattern
led_set_pattern "$name" "$pattern"
info " $name -> pattern='$phase_name' RGB($r,$g,$b)"
done
}
run_pattern() {
local phase_dur total_leds phase_sleep
total_leds=$(present_leds | wc -w)
phase_dur=$(( OPT_DURATION / 3 ))
[ "$phase_dur" -lt 2 ] && phase_dur=2
info "Pattern test: 3 phases x ~${phase_dur}s (${total_leds} LEDs)"
# --- Phase 1: FADE (all white, staggered start via offset pattern) ------
# Give each LED a slightly rotated version of the fade pattern so they
# don't all pulse in sync — we pre-build per-LED patterns with an initial
# offset silence of (index * 200 ms).
info "--- Phase 1/3: FADE ---"
local idx=0 name offset_pattern prefix
for name in $(present_leds); do
offset_pattern=""
if [ "$idx" -gt 0 ]; then
# Insert a leading "0 <offset_ms>" step to stagger each LED
prefix=$((idx * 200))
offset_pattern="0 ${prefix} "
fi
led_set_trigger "$name" none
led_set_multi_intensity "$name" 255 255 255
led_set_brightness "$name" 255
led_set_trigger "$name" pattern
led_set_pattern "$name" "${offset_pattern}${FADE_PATTERN}"
info " $name -> fade (offset ${idx}x200ms)"
idx=$((idx + 1))
done
sleep "$phase_dur"
# --- Phase 2: FAST FLASH — different colour per LED --------------------
info "--- Phase 2/3: FAST FLASH ---"
run_pattern_phase "fast-flash" "$FLASH_FAST_PATTERN" \
255 0 0 \
0 255 0 \
0 0 255 \
255 255 0 \
0 255 255 \
255 0 255 \
255 128 0 \
128 0 255 \
0 255 128 \
255 255 255
sleep "$phase_dur"
# --- Phase 3: SLOW FLASH — colour cycling (shift colours between LEDs) --
info "--- Phase 3/3: SLOW FLASH ---"
run_pattern_phase "slow-flash" "$FLASH_SLOW_PATTERN" \
0 255 255 \
255 0 255 \
255 255 0 \
0 0 255 \
0 255 0 \
255 0 0 \
128 255 128 \
255 128 128 \
128 128 255 \
200 200 200
# Let the last phase run for the remainder of the duration
local elapsed=$(( phase_dur * 2 ))
local remaining=$(( OPT_DURATION - elapsed ))
[ "$remaining" -lt 1 ] && remaining=1
sleep "$remaining"
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
info "============================================"
info " WAGO M4 LED Performance Test v${VERSION}"
info "============================================"
info "Trigger : $OPT_TRIGGER"
info "Duration : ${OPT_DURATION}s"
info "LED sysfs: $LED_SYSFS"
info "--------------------------------------------"
check_leds
case "$OPT_TRIGGER" in
pattern)
run_pattern
;;
timer)
run_timer
info "Running for ${OPT_DURATION}s ..."
sleep "$OPT_DURATION"
;;
heartbeat)
run_heartbeat
info "Running for ${OPT_DURATION}s ..."
sleep "$OPT_DURATION"
;;
esac
cleanup
info "Test complete."
exit 0
+33
View File
@@ -0,0 +1,33 @@
# SPDX-License-Identifier: GPL-2.0-only
include ../scripts/Makefile.include
bindir ?= /usr/sbin
ifeq ($(srctree),)
srctree := $(patsubst %/,%,$(dir $(CURDIR)))
srctree := $(patsubst %/,%,$(dir $(srctree)))
endif
# Do not use make's built-in rules
# (this improves performance and avoids hard-to-debug behaviour);
MAKEFLAGS += -r
CFLAGS += -O2 -Wall -Wextra -g -D_GNU_SOURCE
ALL_TARGETS := mram_io
ALL_PROGRAMS := $(patsubst %,$(OUTPUT)%,$(ALL_TARGETS))
all: $(ALL_PROGRAMS)
export srctree OUTPUT CC LD CFLAGS
include $(srctree)/tools/build/Makefile.include
$(OUTPUT)mram_io: mram_io.c
$(CC) $(CFLAGS) -o $@ $<
clean:
$(RM) $(ALL_PROGRAMS)
install: all
install -d $(DESTDIR)$(bindir)
install -m 755 $(ALL_PROGRAMS) $(DESTDIR)$(bindir)
+334
View File
@@ -0,0 +1,334 @@
// SPDX-License-Identifier: GPL-2.0
/*
* mram_io - Byte-wise read/write tool for MTD_RAM devices via mmap()
*
* Requires a MTD device that supports mmap() (e.g. Everspin MRAM on
* Cadence OSPI in DAC mode, registered as MTD_RAM).
*
* Usage:
* mram_io --read --device <dev> --offset <offset> --length <n>
* mram_io --write --device <dev> --offset <offset> --data <b0> [b1 b2 ...]
*
* Examples:
* mram_io --read --device /dev/mtd0 --offset 0x100 --length 16
* mram_io --write --device /dev/mtd0 --offset 0x100 --data 0xde 0xad 0xbe 0xef
*/
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
static void usage(const char *prog)
{
fprintf(stderr,
"Usage:\n"
" %s --read --device <dev> --offset <off> --length <n>\n"
" %s --write --device <dev> --offset <off> --data <b0> [b1 ...]\n"
"\n"
"Options:\n"
" --device, -D <path> MTD character device, e.g. /dev/mtd0\n"
" --offset, -o <n> Byte offset into the device (hex or dec)\n"
" --length, -l <n> Number of bytes to read\n"
" --data, -d Following args are bytes to write (hex or dec)\n"
" --read, -r Read mode\n"
" --write, -w Write mode\n"
" --help, -h Show this help\n"
"\n"
"Output (read): hex-dump with offset, hex columns and ASCII column.\n",
prog, prog);
}
static unsigned long parse_num(const char *s)
{
char *end;
unsigned long v = strtoul(s, &end, 0);
if (*end != '\0') {
fprintf(stderr, "Invalid number: '%s'\n", s);
exit(EXIT_FAILURE);
}
return v;
}
/* Returns the size of the MTD device via lseek(). */
static off_t mtd_size(int fd)
{
off_t sz = lseek(fd, 0, SEEK_END);
if (sz < 0) {
perror("lseek (SEEK_END)");
exit(EXIT_FAILURE);
}
lseek(fd, 0, SEEK_SET);
return sz;
}
/* ------------------------------------------------------------------ */
/* main */
/* ------------------------------------------------------------------ */
enum mode { MODE_NONE, MODE_READ, MODE_WRITE };
int main(int argc, char *argv[])
{
static const struct option long_opts[] = {
{ "read", no_argument, NULL, 'r' },
{ "write", no_argument, NULL, 'w' },
{ "device", required_argument, NULL, 'D' },
{ "offset", required_argument, NULL, 'o' },
{ "length", required_argument, NULL, 'l' },
{ "data", no_argument, NULL, 'd' },
{ "help", no_argument, NULL, 'h' },
{ NULL, 0, NULL, 0 },
};
enum mode mode = MODE_NONE;
const char *device = NULL;
unsigned long offset = 0;
unsigned long length = 0;
int have_offset = 0;
int have_length = 0;
int data_mode = 0;
int opt;
int opt_idx = 0;
/*
* Parse all named options first. Stop at --data/-d: everything
* after it is a raw byte value for --write, not an option.
*/
while (!data_mode &&
(opt = getopt_long(argc, argv, "rwD:o:l:dh",
long_opts, &opt_idx)) != -1) {
switch (opt) {
case 'r':
mode = MODE_READ;
break;
case 'w':
mode = MODE_WRITE;
break;
case 'D':
device = optarg;
break;
case 'o':
offset = parse_num(optarg);
have_offset = 1;
break;
case 'l':
length = parse_num(optarg);
have_length = 1;
break;
case 'd':
data_mode = 1;
break;
case 'h':
usage(argv[0]);
return EXIT_SUCCESS;
default:
usage(argv[0]);
return EXIT_FAILURE;
}
}
/* --- Validate ------------------------------------------------- */
if (!device) {
fprintf(stderr, "Error: --device <path> is required.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
if (mode == MODE_NONE) {
fprintf(stderr, "Error: specify --read or --write.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
if (!have_offset) {
fprintf(stderr, "Error: --offset is required.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
/* Collect write bytes from remaining argv (after --data) */
uint8_t *write_buf = NULL;
size_t write_cnt = 0;
if (mode == MODE_WRITE) {
if (!data_mode) {
fprintf(stderr,
"Error: --write requires --data <b0> [b1 ...].\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
write_cnt = (size_t)(argc - optind);
if (write_cnt == 0) {
fprintf(stderr, "Error: no data bytes given after --data.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
write_buf = malloc(write_cnt);
if (!write_buf) {
perror("malloc");
return EXIT_FAILURE;
}
for (size_t i = 0; i < write_cnt; i++) {
unsigned long v = parse_num(argv[optind + i]);
if (v > 0xff) {
fprintf(stderr,
"Error: byte value 0x%lx out of range.\n", v);
free(write_buf);
return EXIT_FAILURE;
}
write_buf[i] = (uint8_t)v;
}
length = write_cnt;
}
if (mode == MODE_READ && !have_length) {
fprintf(stderr, "Error: --read requires --length.\n\n");
usage(argv[0]);
return EXIT_FAILURE;
}
/* --- Open device ---------------------------------------------- */
int flags = (mode == MODE_WRITE) ? (O_RDWR | O_SYNC) : O_RDONLY;
int fd = open(device, flags);
if (fd < 0) {
fprintf(stderr, "open(%s): %s\n", device, strerror(errno));
free(write_buf);
return EXIT_FAILURE;
}
off_t dev_size = mtd_size(fd);
if ((unsigned long)dev_size == 0) {
fprintf(stderr, "Error: device size is 0.\n");
goto err_close;
}
if (offset >= (unsigned long)dev_size) {
fprintf(stderr,
"Error: offset 0x%lx is beyond device size 0x%lx.\n",
offset, (unsigned long)dev_size);
goto err_close;
}
if (offset + length > (unsigned long)dev_size) {
fprintf(stderr,
"Warning: clamping length from %lu to %lu (end of device).\n",
length, (unsigned long)dev_size - offset);
length = (unsigned long)dev_size - offset;
}
/* --- mmap ----------------------------------------------------- */
/*
* mmap() the whole device. The kernel (mtdchar_mmap) maps the
* physical AHB window directly into our address space; accesses
* go straight to the MRAM without any kernel copy.
*/
int prot = PROT_READ | ((mode == MODE_WRITE) ? PROT_WRITE : 0);
void *map = mmap(NULL, (size_t)dev_size, prot, MAP_SHARED, fd, 0);
if (map == MAP_FAILED) {
fprintf(stderr, "mmap(%s): %s\n", device, strerror(errno));
fprintf(stderr,
"Note: device must be of type MTD_RAM with a physical "
"AHB window (e.g. Everspin MRAM on Cadence OSPI).\n");
goto err_close;
}
uint8_t *base = (uint8_t *)map + offset;
/* --- Read ----------------------------------------------------- */
if (mode == MODE_READ) {
printf("Reading %lu byte(s) from %s at offset 0x%lx:\n\n",
length, device, offset);
printf(" Offset 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f"
" ASCII\n");
printf(" -------- -----------------------------------------------"
" ----------------\n");
for (unsigned long i = 0; i < length; ) {
unsigned long row_off = offset + i;
unsigned long row_start = i;
/* Offset column */
printf(" %08lx ", row_off);
/* Hex bytes: 16 per row, split 8+8 */
for (int col = 0; col < 16; col++) {
if (col == 8)
printf(" ");
if (row_start + col < length)
printf("%02x ", base[row_start + col]);
else
printf(" ");
}
/* ASCII column */
printf(" |");
for (int col = 0; col < 16 && row_start + col < length; col++) {
uint8_t c = base[row_start + col];
printf("%c", (c >= 0x20 && c < 0x7f) ? c : '.');
}
printf("|\n");
i += 16;
}
printf("\n");
}
/* --- Write ---------------------------------------------------- */
if (mode == MODE_WRITE) {
printf("Writing %lu byte(s) to %s at offset 0x%lx:\n",
length, device, offset);
for (size_t i = 0; i < write_cnt; i++) {
/*
* Volatile write: ensure each byte reaches the hardware
* window. The mmap region is pgprot_noncached, but being
* explicit prevents compiler from merging byte stores.
*/
volatile uint8_t *dst = (volatile uint8_t *)base + i;
*dst = write_buf[i];
printf(" [0x%08lx] <- 0x%02x\n",
offset + i, write_buf[i]);
}
/* Read back for verification */
printf("\nVerification (read-back):\n");
for (size_t i = 0; i < write_cnt; i++) {
uint8_t got = *((volatile uint8_t *)base + i);
const char *status = (got == write_buf[i]) ? "OK" : "MISMATCH";
printf(" [0x%08lx] wrote 0x%02x read 0x%02x %s\n",
offset + i, write_buf[i], got, status);
}
}
munmap(map, (size_t)dev_size);
free(write_buf);
close(fd);
return EXIT_SUCCESS;
err_close:
free(write_buf);
close(fd);
return EXIT_FAILURE;
}