[PATCH v2 2/2] mfd: Add initial synology microp driver
From: Markus Probst
Date: Sun Mar 08 2026 - 14:41:39 EST
Add a initial synology microp driver, written in Rust.
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.
Signed-off-by: Markus Probst <markus.probst@xxxxxxxxx>
---
MAINTAINERS | 6 +
drivers/mfd/Kconfig | 2 +
drivers/mfd/Makefile | 2 +
drivers/mfd/synology_microp/Kconfig | 14 ++
drivers/mfd/synology_microp/Makefile | 2 +
drivers/mfd/synology_microp/TODO | 7 +
drivers/mfd/synology_microp/command.rs | 50 +++++
drivers/mfd/synology_microp/led.rs | 275 +++++++++++++++++++++++++
drivers/mfd/synology_microp/synology_microp.rs | 82 ++++++++
rust/uapi/uapi_helper.h | 2 +
10 files changed, 442 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index e9e83ab552c7..092cd9e8a730 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -25550,6 +25550,12 @@ F: drivers/dma-buf/sync_*
F: include/linux/sync_file.h
F: include/uapi/linux/sync_file.h
+SYNOLOGY MICROP DRIVER
+M: Markus Probst <markus.probst@xxxxxxxxx>
+S: Maintained
+F: Documentation/devicetree/bindings/mfd/synology,microp.yaml
+F: drivers/mfd/synology_microp/
+
SYNOPSYS ARC ARCHITECTURE
M: Vineet Gupta <vgupta@xxxxxxxxxx>
L: linux-snps-arc@xxxxxxxxxxxxxxxxxxx
diff --git a/drivers/mfd/Kconfig b/drivers/mfd/Kconfig
index 7192c9d1d268..bc269719749f 100644
--- a/drivers/mfd/Kconfig
+++ b/drivers/mfd/Kconfig
@@ -2580,5 +2580,7 @@ config MFD_MAX7360
additional drivers must be enabled in order to use the functionality
of the device.
+source "drivers/mfd/synology_microp/Kconfig"
+
endmenu
endif
diff --git a/drivers/mfd/Makefile b/drivers/mfd/Makefile
index e75e8045c28a..0a6fa33d5c35 100644
--- a/drivers/mfd/Makefile
+++ b/drivers/mfd/Makefile
@@ -304,3 +304,5 @@ obj-$(CONFIG_MFD_RSMU_SPI) += rsmu_spi.o rsmu_core.o
obj-$(CONFIG_MFD_UPBOARD_FPGA) += upboard-fpga.o
obj-$(CONFIG_MFD_LOONGSON_SE) += loongson-se.o
+
+obj-$(CONFIG_MFD_SYNOLOGY_MICROP) += synology_microp/
diff --git a/drivers/mfd/synology_microp/Kconfig b/drivers/mfd/synology_microp/Kconfig
new file mode 100644
index 000000000000..4bbbcf0b6e94
--- /dev/null
+++ b/drivers/mfd/synology_microp/Kconfig
@@ -0,0 +1,14 @@
+
+config MFD_SYNOLOGY_MICROP
+ tristate "Synology Microp driver"
+ depends on RUST
+ depends on SERIAL_DEV_BUS
+ depends on LEDS_CLASS && LEDS_CLASS_MULTICOLOR
+ default n
+ help
+ Enable support for the MCU found in Synology NAS devices.
+
+ This is needed to properly shutdown and reboot the device, as well as
+ additional functionality like fan and LED control.
+
+ This driver is work in progress and may not be fully functional.
diff --git a/drivers/mfd/synology_microp/Makefile b/drivers/mfd/synology_microp/Makefile
new file mode 100644
index 000000000000..d762cada20c9
--- /dev/null
+++ b/drivers/mfd/synology_microp/Makefile
@@ -0,0 +1,2 @@
+
+obj-y += synology_microp.o
diff --git a/drivers/mfd/synology_microp/TODO b/drivers/mfd/synology_microp/TODO
new file mode 100644
index 000000000000..1961a33115db
--- /dev/null
+++ b/drivers/mfd/synology_microp/TODO
@@ -0,0 +1,7 @@
+TODO:
+- 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/mfd/synology_microp/command.rs b/drivers/mfd/synology_microp/command.rs
new file mode 100644
index 000000000000..78f82a86f1b2
--- /dev/null
+++ b/drivers/mfd/synology_microp/command.rs
@@ -0,0 +1,50 @@
+// SPDX-License-Identifier: GPL-2.0
+
+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/mfd/synology_microp/led.rs b/drivers/mfd/synology_microp/led.rs
new file mode 100644
index 000000000000..28a765b1e6c2
--- /dev/null
+++ b/drivers/mfd/synology_microp/led.rs
@@ -0,0 +1,275 @@
+// SPDX-License-Identifier: GPL-2.0
+
+use core::sync::atomic::{
+ AtomicBool,
+ Ordering, //
+};
+
+use kernel::{
+ device::{
+ property::FwNode,
+ Bound, //
+ },
+ devres::Devres,
+ error::Error,
+ led::{self, MultiColorSubLed},
+ macros::vtable,
+ prelude::*,
+ serdev,
+ types::ARef, //
+};
+
+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_by_fwnode<'a>(
+ parent: &'a serdev::Device<Bound>,
+ default_trigger: &'static CStr,
+ brightness: u32,
+ color: led::Color,
+ map: fn(State) -> Command,
+ fwnode: Option<ARef<FwNode>>,
+ ) -> impl PinInit<Devres<led::Device<Self>>, Error> + 'a {
+ led::DeviceBuilder::new()
+ .fwnode(fwnode)
+ .default_trigger(default_trigger)
+ .initial_brightness(brightness)
+ .devicename(c"synology-microp")
+ .color(color)
+ .build(
+ parent,
+ Ok(Self {
+ blink: AtomicBool::new(true),
+ map,
+ }),
+ )
+ }
+
+ 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 {
+ Self::register_by_fwnode(
+ parent,
+ default_trigger,
+ brightness,
+ color,
+ map,
+ parent
+ .as_ref()
+ .fwnode()
+ .and_then(|fwnode| fwnode.get_child_by_name(fwnode_child_name)),
+ )
+ }
+
+ fn register_optional<'a>(
+ parent: &'a serdev::Device<Bound>,
+ fwnode_child_name: &'static CStr,
+ default_trigger: &'static CStr,
+ brightness: u32,
+ color: led::Color,
+ map: fn(State) -> Command,
+ ) -> Option<impl PinInit<Devres<led::Device<Self>>, Error> + 'a> {
+ parent
+ .as_ref()
+ .fwnode()
+ .and_then(|fwnode| fwnode.get_child_by_name(fwnode_child_name))
+ .map(|fwnode| {
+ Self::register_by_fwnode(
+ parent,
+ default_trigger,
+ brightness,
+ color,
+ map,
+ Some(fwnode),
+ )
+ })
+ }
+
+ 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>,
+ ) -> Option<impl PinInit<Devres<led::Device<Self>>, Error> + 'a> {
+ Self::register_optional(
+ parent,
+ c"alert-led",
+ c"none",
+ 0,
+ led::Color::Orange,
+ Command::AlertLed,
+ )
+ }
+
+ pub(crate) fn register_usb<'a>(
+ parent: &'a serdev::Device<Bound>,
+ ) -> Option<impl PinInit<Devres<led::Device<Self>>, Error> + 'a> {
+ Self::register_optional(
+ 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/mfd/synology_microp/synology_microp.rs b/drivers/mfd/synology_microp/synology_microp.rs
new file mode 100644
index 000000000000..e92de7da3e46
--- /dev/null
+++ b/drivers/mfd/synology_microp/synology_microp.rs
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Synology Microp driver
+
+use kernel::{
+ device,
+ devres::{self, 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",
+ params: {
+ check_fan: i32 {
+ default: 1,
+ description: "Check for cpu fan failures",
+ },
+ },
+}
+
+#[pin_data]
+struct SynologyMicropDriver {
+ #[pin]
+ power_led: Devres<kernel::led::Device<SynologyMicropLedHandler>>,
+ #[pin]
+ status_led: Devres<kernel::led::MultiColorDevice<SynologyMicropStatusLedHandler>>,
+}
+
+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)?;
+
+ // TODO: Replace with Option field on SynologyMicropDriver once
+ // https://github.com/Rust-for-Linux/pin-init/issues/59 has been resolved.
+ if let Some(alert_led) = SynologyMicropLedHandler::register_alert(dev) {
+ devres::register(dev.as_ref(), alert_led, GFP_KERNEL)?;
+ }
+ if let Some(usb_led) = SynologyMicropLedHandler::register_usb(dev) {
+ devres::register(dev.as_ref(), usb_led, GFP_KERNEL)?;
+ }
+
+ Ok(try_pin_init!(Self {
+ power_led <- SynologyMicropLedHandler::register_power(dev),
+ status_led <- SynologyMicropStatusLedHandler::register(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