[PATCH 2/2] staging: Add initial synology microp driver
From: Markus Probst
Date: Fri Mar 06 2026 - 14:42:46 EST
Add a initial synology microp driver, written in Rust, to the staging tree.
The driver targets a microcontroller found in Synology NAS devices. It
currently only supports controlling of the power led, status led, alert
led and usb led. Other components such as fan control or handling
on-device buttons will be added once the required rust abstractions are
there.
---
MAINTAINERS | 6 +
drivers/staging/Kconfig | 2 +
drivers/staging/Makefile | 1 +
drivers/staging/synology_microp/Kconfig | 4 +
drivers/staging/synology_microp/Makefile | 2 +
drivers/staging/synology_microp/TODO | 8 +
drivers/staging/synology_microp/command.rs | 48 +++++
drivers/staging/synology_microp/led.rs | 229 +++++++++++++++++++++
drivers/staging/synology_microp/synology_microp.rs | 73 +++++++
rust/uapi/uapi_helper.h | 2 +
10 files changed, 375 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index e9e83ab552c7..a1f8dec31db2 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25143,6 +25143,12 @@ L: linux-fbdev@xxxxxxxxxxxxxxx
S: Maintained
F: drivers/staging/sm750fb/
+STAGING - SYNOLOGY MICROP DRIVER
+M: Markus Probst <markus.probst@xxxxxxxxx>
+S: Maintained
+F: Documentation/devicetree/bindings/mfd/synology,microp.yaml
+F: drivers/staging/synology_microp/
+
STAGING SUBSYSTEM
M: Greg Kroah-Hartman <gregkh@xxxxxxxxxxxxxxxxxxx>
L: linux-staging@xxxxxxxxxxxxxxx
diff --git a/drivers/staging/Kconfig b/drivers/staging/Kconfig
index 2f92cd698bef..193d4a5d7f56 100644
--- a/drivers/staging/Kconfig
+++ b/drivers/staging/Kconfig
@@ -48,4 +48,6 @@ source "drivers/staging/axis-fifo/Kconfig"
source "drivers/staging/vme_user/Kconfig"
+source "drivers/staging/synology_microp/Kconfig"
+
endif # STAGING
diff --git a/drivers/staging/Makefile b/drivers/staging/Makefile
index f5b8876aa536..cb0cadd08ae2 100644
--- a/drivers/staging/Makefile
+++ b/drivers/staging/Makefile
@@ -13,3 +13,4 @@ obj-$(CONFIG_MOST) += most/
obj-$(CONFIG_GREYBUS) += greybus/
obj-$(CONFIG_BCM2835_VCHIQ) += vc04_services/
obj-$(CONFIG_XIL_AXIS_FIFO) += axis-fifo/
+obj-$(CONFIG_MFD_SYNOLOGY_MICROP) += synology_microp/
diff --git a/drivers/staging/synology_microp/Kconfig b/drivers/staging/synology_microp/Kconfig
new file mode 100644
index 000000000000..8d315d2576d8
--- /dev/null
+++ b/drivers/staging/synology_microp/Kconfig
@@ -0,0 +1,4 @@
+
+config MFD_SYNOLOGY_MICROP
+ tristate "Synology Microp driver"
+ depends on RUST && SERIAL_DEV_BUS && LEDS_CLASS && LEDS_CLASS_MULTICOLOR
diff --git a/drivers/staging/synology_microp/Makefile b/drivers/staging/synology_microp/Makefile
new file mode 100644
index 000000000000..d762cada20c9
--- /dev/null
+++ b/drivers/staging/synology_microp/Makefile
@@ -0,0 +1,2 @@
+
+obj-y += synology_microp.o
diff --git a/drivers/staging/synology_microp/TODO b/drivers/staging/synology_microp/TODO
new file mode 100644
index 000000000000..d432ddd8403c
--- /dev/null
+++ b/drivers/staging/synology_microp/TODO
@@ -0,0 +1,8 @@
+TODO:
+- not all devices have a alert or usb led. Only register them, if a fwnode entry is present.
+- add missing components:
+ - handle on-device buttons (Power, Factory reset, "USB Copy")
+ - handle fan failure
+ - beeper
+ - fan speed control
+ - correctly perform device power-off and restart on Synology devices
diff --git a/drivers/staging/synology_microp/command.rs b/drivers/staging/synology_microp/command.rs
new file mode 100644
index 000000000000..e98e46423e2d
--- /dev/null
+++ b/drivers/staging/synology_microp/command.rs
@@ -0,0 +1,48 @@
+use kernel::{
+ device::Bound,
+ error::Result,
+ serdev, //
+};
+
+use crate::led;
+
+#[derive(Copy, Clone)]
+#[expect(
+ clippy::enum_variant_names,
+ reason = "future variants will not end with Led"
+)]
+pub(crate) enum Command {
+ PowerLed(led::State),
+ StatusLed(led::StatusLedColor, led::State),
+ AlertLed(led::State),
+ UsbLed(led::State),
+}
+
+impl Command {
+ pub(crate) fn write(self, dev: &serdev::Device<Bound>) -> Result<()> {
+ dev.write_all(
+ match self {
+ Command::PowerLed(led::State::On) => &[0x34],
+ Command::PowerLed(led::State::Blink) => &[0x35],
+ Command::PowerLed(led::State::Off) => &[0x36],
+
+ Command::StatusLed(_, led::State::Off) => &[0x37],
+ Command::StatusLed(led::StatusLedColor::Green, led::State::On) => &[0x38],
+ Command::StatusLed(led::StatusLedColor::Green, led::State::Blink) => &[0x39],
+ Command::StatusLed(led::StatusLedColor::Orange, led::State::On) => &[0x3A],
+ Command::StatusLed(led::StatusLedColor::Orange, led::State::Blink) => &[0x3B],
+
+ Command::AlertLed(led::State::On) => &[0x4C, 0x41, 0x31],
+ Command::AlertLed(led::State::Blink) => &[0x4C, 0x41, 0x32],
+ Command::AlertLed(led::State::Off) => &[0x4C, 0x41, 0x33],
+
+ Command::UsbLed(led::State::On) => &[0x40],
+ Command::UsbLed(led::State::Blink) => &[0x41],
+ Command::UsbLed(led::State::Off) => &[0x42],
+ },
+ serdev::Timeout::Max,
+ )?;
+ dev.wait_until_sent(serdev::Timeout::Max);
+ Ok(())
+ }
+}
diff --git a/drivers/staging/synology_microp/led.rs b/drivers/staging/synology_microp/led.rs
new file mode 100644
index 000000000000..deeece661937
--- /dev/null
+++ b/drivers/staging/synology_microp/led.rs
@@ -0,0 +1,229 @@
+use core::sync::atomic::{
+ AtomicBool,
+ Ordering, //
+};
+
+use kernel::{
+ device::Bound,
+ devres::Devres,
+ error::Error,
+ led::{self, MultiColorSubLed},
+ macros::vtable,
+ prelude::*,
+ serdev, //
+};
+
+use crate::command::Command;
+
+pub(crate) struct SynologyMicropLedHandler {
+ blink: AtomicBool,
+ map: fn(State) -> Command,
+}
+
+pub(crate) struct SynologyMicropStatusLedHandler {
+ blink: AtomicBool,
+}
+
+#[derive(Copy, Clone)]
+pub(crate) enum State {
+ On,
+ Blink,
+ Off,
+}
+
+#[derive(Copy, Clone)]
+pub(crate) enum StatusLedColor {
+ Green,
+ Orange,
+}
+
+impl SynologyMicropLedHandler {
+ fn register<'a>(
+ parent: &'a serdev::Device<Bound>,
+ fwnode_child_name: &'static CStr,
+ default_trigger: &'static CStr,
+ brightness: u32,
+ color: led::Color,
+ map: fn(State) -> Command,
+ ) -> impl PinInit<Devres<led::Device<Self>>, Error> + 'a {
+ led::DeviceBuilder::new()
+ .fwnode(
+ parent
+ .as_ref()
+ .fwnode()
+ .and_then(|fwnode| fwnode.get_child_by_name(fwnode_child_name)),
+ )
+ .default_trigger(default_trigger)
+ .initial_brightness(brightness)
+ .devicename(c"synology-microp")
+ .color(color)
+ .build(
+ parent,
+ Ok(Self {
+ blink: AtomicBool::new(true),
+ map,
+ }),
+ )
+ }
+
+ pub(crate) fn register_power<'a>(
+ parent: &'a serdev::Device<Bound>,
+ ) -> impl PinInit<Devres<led::Device<Self>>, Error> + 'a {
+ Self::register(
+ parent,
+ c"power-led",
+ c"timer",
+ 1,
+ led::Color::Blue,
+ Command::PowerLed,
+ )
+ }
+
+ pub(crate) fn register_alert<'a>(
+ parent: &'a serdev::Device<Bound>,
+ ) -> impl PinInit<Devres<led::Device<Self>>, Error> + 'a {
+ Self::register(
+ parent,
+ c"alert-led",
+ c"none",
+ 0,
+ led::Color::Orange,
+ Command::AlertLed,
+ )
+ }
+
+ pub(crate) fn register_usb<'a>(
+ parent: &'a serdev::Device<Bound>,
+ ) -> impl PinInit<Devres<led::Device<Self>>, Error> + 'a {
+ Self::register(
+ parent,
+ c"usb-led",
+ c"none",
+ 0,
+ led::Color::Green,
+ Command::UsbLed,
+ )
+ }
+}
+
+#[vtable]
+impl led::LedOps for SynologyMicropLedHandler {
+ type Bus = serdev::Device<Bound>;
+ type Mode = led::Normal;
+ const BLOCKING: bool = true;
+ const MAX_BRIGHTNESS: u32 = 1;
+
+ fn brightness_set(
+ &self,
+ dev: &Self::Bus,
+ _classdev: &led::Device<Self>,
+ brightness: u32,
+ ) -> Result<()> {
+ (self.map)(if brightness == 0 {
+ self.blink.store(false, Ordering::Relaxed);
+ State::Off
+ } else if self.blink.load(Ordering::Relaxed) {
+ State::Blink
+ } else {
+ State::On
+ })
+ .write(dev)
+ }
+
+ fn blink_set(
+ &self,
+ dev: &Self::Bus,
+ _classdev: &led::Device<Self>,
+ delay_on: &mut usize,
+ delay_off: &mut usize,
+ ) -> Result<()> {
+ *delay_on = 167;
+ *delay_off = 167;
+
+ self.blink.store(true, Ordering::Relaxed);
+ (self.map)(State::Blink).write(dev)
+ }
+}
+
+impl SynologyMicropStatusLedHandler {
+ pub(crate) fn register(
+ parent: &serdev::Device<Bound>,
+ ) -> impl PinInit<Devres<led::MultiColorDevice<Self>>, Error> + '_ {
+ const SUBLEDS: &[MultiColorSubLed] = &[
+ MultiColorSubLed::new(led::Color::Green).initial_intensity(1),
+ MultiColorSubLed::new(led::Color::Orange),
+ ];
+
+ led::DeviceBuilder::new()
+ .fwnode(
+ parent
+ .as_ref()
+ .fwnode()
+ .and_then(|fwnode| fwnode.get_child_by_name(c"status-led")),
+ )
+ .devicename(c"synology-microp")
+ .color(led::Color::Multi)
+ .build_multicolor(
+ parent,
+ Ok(SynologyMicropStatusLedHandler {
+ blink: AtomicBool::new(false),
+ }),
+ SUBLEDS,
+ )
+ }
+}
+
+#[vtable]
+impl led::LedOps for SynologyMicropStatusLedHandler {
+ type Bus = serdev::Device<Bound>;
+ type Mode = led::MultiColor;
+ const BLOCKING: bool = true;
+ const MAX_BRIGHTNESS: u32 = 1;
+
+ fn brightness_set(
+ &self,
+ dev: &Self::Bus,
+ classdev: &led::MultiColorDevice<Self>,
+ brightness: u32,
+ ) -> Result<()> {
+ if brightness == 0 {
+ self.blink.store(false, Ordering::Relaxed);
+ }
+
+ let (color, subled_brightness) = if classdev.subleds()[1].brightness == 0 {
+ (StatusLedColor::Green, classdev.subleds()[0].brightness)
+ } else {
+ (StatusLedColor::Orange, classdev.subleds()[1].brightness)
+ };
+
+ if subled_brightness == 0 {
+ Command::StatusLed(color, State::Off)
+ } else if self.blink.load(Ordering::Relaxed) {
+ Command::StatusLed(color, State::Blink)
+ } else {
+ Command::StatusLed(color, State::On)
+ }
+ .write(dev)
+ }
+
+ fn blink_set(
+ &self,
+ dev: &Self::Bus,
+ classdev: &led::MultiColorDevice<Self>,
+ delay_on: &mut usize,
+ delay_off: &mut usize,
+ ) -> Result<()> {
+ *delay_on = 167;
+ *delay_off = 167;
+
+ self.blink.store(true, Ordering::Relaxed);
+
+ let color = if classdev.subleds()[1].brightness == 0 {
+ StatusLedColor::Green
+ } else {
+ StatusLedColor::Orange
+ };
+
+ Command::StatusLed(color, State::Blink).write(dev)
+ }
+}
diff --git a/drivers/staging/synology_microp/synology_microp.rs b/drivers/staging/synology_microp/synology_microp.rs
new file mode 100644
index 000000000000..abc513edc590
--- /dev/null
+++ b/drivers/staging/synology_microp/synology_microp.rs
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Synology Microp driver
+
+use kernel::{
+ device,
+ devres::Devres,
+ of,
+ prelude::*,
+ serdev, //
+};
+use pin_init::pin_init_scope;
+
+use crate::{
+ led::{
+ SynologyMicropLedHandler,
+ SynologyMicropStatusLedHandler, //
+ }, //
+};
+
+pub(crate) mod command;
+mod led;
+
+kernel::module_serdev_device_driver! {
+ type: SynologyMicropDriver,
+ name: "synology_microp",
+ authors: ["Markus Probst <markus.probst@xxxxxxxxx>"],
+ description: "Synology Microp driver",
+ license: "GPL v2",
+}
+
+#[pin_data]
+struct SynologyMicropDriver {
+ #[pin]
+ power_led: Devres<kernel::led::Device<SynologyMicropLedHandler>>,
+ #[pin]
+ status_led: Devres<kernel::led::MultiColorDevice<SynologyMicropStatusLedHandler>>,
+ #[pin]
+ alert_led: Devres<kernel::led::Device<SynologyMicropLedHandler>>,
+ #[pin]
+ usb_led: Devres<kernel::led::Device<SynologyMicropLedHandler>>,
+}
+
+kernel::of_device_table!(
+ OF_TABLE,
+ MODULE_OF_TABLE,
+ <SynologyMicropDriver as serdev::Driver>::IdInfo,
+ [(of::DeviceId::new(c"synology,microp"), ()),]
+);
+
+#[vtable]
+impl serdev::Driver for SynologyMicropDriver {
+ type IdInfo = ();
+ const OF_ID_TABLE: Option<kernel::of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
+
+ fn probe(
+ dev: &serdev::Device<device::Core>,
+ _id_info: Option<&Self::IdInfo>,
+ ) -> impl PinInit<Self, kernel::error::Error> {
+ pin_init_scope(move || {
+ let _ = dev.set_baudrate(9600);
+ dev.set_flow_control(false);
+ dev.set_parity(serdev::Parity::None)?;
+
+ Ok(try_pin_init!(Self {
+ power_led <- SynologyMicropLedHandler::register_power(dev),
+ status_led <- SynologyMicropStatusLedHandler::register(dev),
+ alert_led <- SynologyMicropLedHandler::register_alert(dev),
+ usb_led <- SynologyMicropLedHandler::register_usb(dev),
+ }))
+ })
+ }
+}
diff --git a/rust/uapi/uapi_helper.h b/rust/uapi/uapi_helper.h
index 06d7d1a2e8da..94b6c1b59e56 100644
--- a/rust/uapi/uapi_helper.h
+++ b/rust/uapi/uapi_helper.h
@@ -14,3 +14,5 @@
#include <uapi/linux/mdio.h>
#include <uapi/linux/mii.h>
#include <uapi/linux/ethtool.h>
+#include <uapi/linux/serial_reg.h>
+
--
2.52.0