[<prev] [next>] [<thread-prev] [day] [month] [year] [list]
Message-ID: <20251008181027.662616-5-markus.probst@posteo.de>
Date: Wed, 08 Oct 2025 18:10:49 +0000
From: Markus Probst <markus.probst@...teo.de>
To: Lee Jones <lee@...nel.org>,
Pavel Machek <pavel@...nel.org>,
Danilo Krummrich <dakr@...nel.org>,
Miguel Ojeda <ojeda@...nel.org>,
Alex Gaynor <alex.gaynor@...il.com>,
Igor Korotin <igor.korotin.linux@...il.com>
Cc: Markus Probst <markus.probst@...teo.de>,
Lorenzo Stoakes <lorenzo.stoakes@...cle.com>,
Vlastimil Babka <vbabka@...e.cz>,
"Liam R. Howlett" <Liam.Howlett@...cle.com>,
Uladzislau Rezki <urezki@...il.com>,
Boqun Feng <boqun.feng@...il.com>,
Gary Guo <gary@...yguo.net>,
bjorn3_gh@...tonmail.com,
Benno Lossin <lossin@...nel.org>,
Andreas Hindborg <a.hindborg@...nel.org>,
Alice Ryhl <aliceryhl@...gle.com>,
Trevor Gross <tmgross@...ch.edu>,
Daniel Almeida <daniel.almeida@...labora.com>,
linux-leds@...r.kernel.org,
rust-for-linux@...r.kernel.org,
linux-kernel@...r.kernel.org
Subject: [PATCH 4/4] leds: add driver for synology atmega1608 controlled LEDs
The Atmega1608 is a microcontroller used by synology devices to control
leds via the i2c bus. It can handle up to 24 leds.
Successfully tested on a Synology DS923+.
Signed-off-by: Markus Probst <markus.probst@...teo.de>
---
MAINTAINERS | 6 +
drivers/leds/Kconfig | 9 +
drivers/leds/Makefile | 1 +
drivers/leds/leds_atmega1608.rs | 377 ++++++++++++++++++++++++++++++++
4 files changed, 393 insertions(+)
create mode 100644 drivers/leds/leds_atmega1608.rs
diff --git a/MAINTAINERS b/MAINTAINERS
index 5881ef69c1d7..c6065751fa15 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -24401,6 +24401,12 @@ F: drivers/dma-buf/sync_*
F: include/linux/sync_file.h
F: include/uapi/linux/sync_file.h
+SYNOLOGY ATMEGA1608 LED CONTROLLER DRIVER
+M: Markus Probst <markus.probst@...teo.de>
+L: linux-leds@...r.kernel.org
+S: Maintained
+F: drivers/leds/leds_atmega1608.rs
+
SYNOPSYS ARC ARCHITECTURE
M: Vineet Gupta <vgupta@...nel.org>
L: linux-snps-arc@...ts.infradead.org
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 6e3dce7e35a4..d04914896235 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -323,6 +323,15 @@ config LEDS_WRAP
help
This option enables support for the PCEngines WRAP programmable LEDs.
+config LEDS_ATMEGA1608
+ tristate "LED Support for Atmega1608 used in Synology devices"
+ depends on LEDS_CLASS
+ depends on I2C
+ depends on RUST
+ help
+ This option enables support for the Atmega1608 microcontroller used
+ as led controller in synology devices.
+
config LEDS_COBALT_QUBE
tristate "LED Support for the Cobalt Qube series front LED"
depends on LEDS_CLASS
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 9a0333ec1a86..5c1a91344980 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -98,6 +98,7 @@ obj-$(CONFIG_LEDS_UPBOARD) += leds-upboard.o
obj-$(CONFIG_LEDS_WM831X_STATUS) += leds-wm831x-status.o
obj-$(CONFIG_LEDS_WM8350) += leds-wm8350.o
obj-$(CONFIG_LEDS_WRAP) += leds-wrap.o
+obj-$(CONFIG_LEDS_ATMEGA1608) += leds_atmega1608.o
# Kinetic ExpressWire Protocol
obj-$(CONFIG_LEDS_EXPRESSWIRE) += leds-expresswire.o
diff --git a/drivers/leds/leds_atmega1608.rs b/drivers/leds/leds_atmega1608.rs
new file mode 100644
index 000000000000..495b6105de04
--- /dev/null
+++ b/drivers/leds/leds_atmega1608.rs
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0
+
+//! Led Driver for Synology devices using Atmega1608 as Led Controller.
+//!
+//! Atmega1608 is a microcontroller from Microchip Technology, it is used
+//! as a led controller in synology nas devices.
+//!
+//! It can handle a total of 24 leds. Each led has 4 modes:
+//! * OFF - the led is turned off
+//! * ON - the led is turned on at full brightness
+//! * DIM0 - custom brightness and optional hardware accelerated blinking
+//!
+//! there should also be DIM1 (which should do the same as DIM0), but
+//! according to my own testing it doesn't actually work, so it won't be used.
+//!
+//!
+//! # Limitations
+//!
+//! DIM0 (and DIM1) are shared across all leds, meaning if DIM0 is used by
+//! multiple leds, these multiple leds cannot have different brightness
+//! levels. The same does apply for the hardware accelerated blinking.
+//!
+//! In other words, for all 24 leds there can either only be one brightness other
+//! than 0 and 255, or one hardware accelerated blinking delay.
+//!
+//! Furthermore the off and on delay in hardware accelerated blinking cannot
+//! have different values and have to be equal. We solve this by calculating
+//! the average of those numbers and use it as delay for both. The delay
+//! cannot be larger than 765 ms (255*3).
+//!
+//! While hardware accelerated blinking is on, the led cycles between
+//! the current brightness for the mode and full brightness. Because of this
+//! behaviour, we hardcode the brightness value of 128 if hardware accelerated
+//! blinking is used.
+//!
+//!
+//! # Technical details
+//!
+//! The controller has 6 8-bit i2c registers for led modes. See [`Atmega1608LedAddress`].
+//! Each led has its own 2 bits:
+//!
+//! +=====================================+
+//! | Register | Leds (2 bits each) |
+//! +=====================================+
+//! | 0x06 | LED3 LED2 LED1 LED0 |
+//! |-----------+-------------------------+
+//! | 0x07 | LED7 LED6 LED5 LED4 |
+//! |-----------+-------------------------+
+//! | 0x08 | LED11 LED10 LED9 LED8 |
+//! |-----------+-------------------------+
+//! | 0x09 | LED15 LED14 LED13 LED12 |
+//! |-----------+-------------------------+
+//! | 0x0A | LED19 LED18 LED17 LED16 |
+//! |-----------+-------------------------+
+//! | 0x0B | LED23 LED22 LED21 LED20 |
+//! |=====================================+
+//!
+//! See [`Atmega1608LedMode`] for the value of each mode.
+//!
+//!
+//! +===============================================+
+//! | Register | Mode / Meaning |
+//! +===============================================+
+//! | 0x01 | turns all leds off if set to 1 |
+//! +-----------------------------------------------+
+//! | 0x02 | blinking delay for Dim0, 1 = 3 ms |
+//! +-----------------------------------------------+
+//! | 0x03 | brightness for Dim0, inverted |
+//! +-----------------------------------------------+
+//! | 0x04 | blinking delay for Dim1, 1 = 3 ms |
+//! +-----------------------------------------------+
+//! | 0x05 | brightness for Dim1, inverted |
+//! +===============================================+
+//!
+
+use kernel::{
+ alloc::kvec::KPinnedVec,
+ c_str,
+ error::code::EINVAL,
+ i2c::{self, I2cClient},
+ led::{LedClassDev, LedHandler, LedInitData},
+ new_mutex,
+ prelude::*,
+ str::CString,
+ sync::{Arc, Mutex},
+ types::ARef,
+};
+
+struct Atmega1608LedDriver {
+ client: ARef<I2cClient>,
+ #[expect(
+ dead_code,
+ reason = "leds should be unregistered on device/driver removal"
+ )]
+ leds: KPinnedVec<LedClassDev>,
+}
+
+kernel::module_i2c_driver! {
+ type: Atmega1608LedDriver,
+ name: "atmega1608",
+ authors: ["Markus Probst <markus.probst@...teo.de>"],
+ description: "Led Driver for Synology devices using Atmega1608 as Led Controller",
+ license: "GPL v2",
+}
+
+#[repr(u8)]
+#[derive(Copy, Clone)]
+enum Atmega1608LedAddress {
+ Ls0 = 0x06,
+ Ls1 = 0x07,
+ Ls2 = 0x08,
+ Ls3 = 0x09,
+ Ls4 = 0x0A,
+ Ls5 = 0x0B,
+}
+
+#[repr(u8)]
+#[derive(Copy, Clone)]
+enum Atmega1608LedId {
+ Sel0 = 0,
+ Sel1 = 1,
+ Sel2 = 2,
+ Sel3 = 3,
+}
+
+#[repr(u8)]
+#[derive(Copy, Clone, PartialEq, Eq)]
+enum Atmega1608LedMode {
+ Off = 0b00,
+ On = 0b01,
+ Dim0 = 0b10,
+ #[allow(dead_code)]
+ Dim1 = 0b11,
+}
+
+struct Atmega1608Led {
+ addr: Atmega1608LedAddress,
+ id: Atmega1608LedId,
+
+ client: ARef<I2cClient>,
+
+ mode_lock: Arc<Mutex<()>>,
+}
+
+kernel::of_device_table! {
+ OF_TABLE,
+ MODULE_OF_TABLE,
+ <Atmega1608LedDriver as i2c::Driver>::IdInfo,
+ [(kernel::of::DeviceId::new(c_str!("synology,atmega1608")), 0)]
+}
+
+impl i2c::Driver for Atmega1608LedDriver {
+ type IdInfo = u32;
+
+ const OF_ID_TABLE: Option<kernel::of::IdTable<Self::IdInfo>> = Some(&OF_TABLE);
+
+ fn probe(
+ pdev: &I2cClient<kernel::device::Core>,
+ _id_info: Option<&Self::IdInfo>,
+ ) -> Result<Pin<KBox<Self>>> {
+ let idev = pdev.as_ref();
+
+ let Some(fwnode) = idev.fwnode() else {
+ return Err(EINVAL);
+ };
+
+ let client: ARef<I2cClient> = pdev.into();
+
+ client
+ .write_byte_data(1, 0)
+ .inspect_err(|err| dev_err!(idev, "unable to remove led mask: {err:?}\n"))?;
+
+ let mut leds = KPinnedVec::with_capacity(
+ Atmega1608LedAddress::VALUES.len() * Atmega1608LedId::VALUES.len(),
+ GFP_KERNEL,
+ )?;
+
+ let mut i = 0;
+ for addr in Atmega1608LedAddress::VALUES {
+ let mode_lock = Arc::pin_init(new_mutex!(()), GFP_KERNEL)?;
+
+ for id in Atmega1608LedId::VALUES {
+ let Some(child) =
+ fwnode.get_child_by_name(&CString::try_from_fmt(fmt!("led@{i}"))?)
+ else {
+ continue;
+ };
+
+ let client = ARef::clone(&client);
+ let mode_lock = Arc::clone(&mode_lock);
+
+ leds.push_pin_init(LedClassDev::new(
+ Some(idev),
+ None,
+ LedInitData::new().fwnode(&child),
+ Atmega1608Led {
+ addr,
+ id,
+ client,
+
+ mode_lock,
+ },
+ ))?;
+ i += 1;
+ }
+ }
+ Ok(KBox::new(Self { client, leds }, GFP_KERNEL)?.into())
+ }
+}
+
+impl Drop for Atmega1608LedDriver {
+ fn drop(&mut self) {
+ let _ = self
+ .client
+ .write_byte_data(1, 1)
+ .inspect_err(|err| dev_err!(self.client.as_ref(), "unable to set led mask: {err:?}\n"));
+
+ for addr in Atmega1608LedAddress::VALUES {
+ let _ = self
+ .client
+ .write_byte_data(addr as u8, 0)
+ .inspect_err(|err| {
+ dev_err!(
+ self.client.as_ref(),
+ "Unable to turn of leds at address {:#2x}: {err:?}\n",
+ addr as u8
+ )
+ });
+ }
+ }
+}
+
+impl Atmega1608LedAddress {
+ const VALUES: [Self; 6] = [
+ Self::Ls0,
+ Self::Ls1,
+ Self::Ls2,
+ Self::Ls3,
+ Self::Ls4,
+ Self::Ls5,
+ ];
+}
+
+impl Atmega1608LedId {
+ const VALUES: [Self; 4] = [Self::Sel0, Self::Sel1, Self::Sel2, Self::Sel3];
+
+ const fn mask(self) -> u8 {
+ match self {
+ Self::Sel0 => 0x03,
+ Self::Sel1 => 0x0C,
+ Self::Sel2 => 0x30,
+ Self::Sel3 => 0xC0,
+ }
+ }
+
+ const fn shift(self) -> u8 {
+ match self {
+ Self::Sel0 => 0,
+ Self::Sel1 => 2,
+ Self::Sel2 => 4,
+ Self::Sel3 => 6,
+ }
+ }
+}
+
+impl LedHandler for Atmega1608Led {
+ const BLOCKING: bool = true;
+ const BLINK: bool = true;
+ const MAX_BRIGHTNESS: u32 = 255;
+
+ fn brightness_set(&self, brightness: u32) -> Result<()> {
+ let brightness = u8::try_from(brightness).unwrap_or(255);
+
+ let mode = self.update_mode(match brightness {
+ 0 => Atmega1608LedMode::Off,
+ 255 | 254 => Atmega1608LedMode::On,
+ _ => Atmega1608LedMode::Dim0,
+ })?;
+
+ mode.update_prescale(&self.client, 0)?;
+ mode.update_pwm(&self.client, !brightness)?;
+
+ Ok(())
+ }
+
+ fn blink_set(&self, delay_on: &mut usize, delay_off: &mut usize) -> Result<()> {
+ if *delay_on == 0 && *delay_off == 0 {
+ *delay_on = 90;
+ *delay_off = 90;
+ }
+ let avg = (*delay_on + *delay_off) / 2;
+ let prescale = avg / 3;
+ let prescale = u8::try_from(prescale).unwrap_or(255);
+
+ let delay = prescale as usize * 3;
+ *delay_on = delay;
+ *delay_off = delay;
+
+ let mode = self.update_mode(Atmega1608LedMode::Dim0)?;
+
+ mode.update_prescale(&self.client, prescale)?;
+ mode.update_pwm(&self.client, 127)?;
+
+ Ok(())
+ }
+}
+
+impl Atmega1608Led {
+ fn update_mode(&self, mode: Atmega1608LedMode) -> Result<Atmega1608LedMode> {
+ let _guard = self.mode_lock.lock();
+
+ let mut current = self
+ .client
+ .read_byte_data(self.addr as u8)
+ .inspect_err(|err| {
+ dev_err!(
+ self.client.as_ref(),
+ "failed to read {:#2x}: {err:?}\n",
+ self.addr as u8
+ );
+ })?;
+
+ current =
+ (current & !self.id.mask()) | (((mode as u8) << self.id.shift()) & self.id.mask());
+
+ self.client
+ .write_byte_data(self.addr as u8, current)
+ .inspect_err(|err| {
+ dev_err!(
+ self.client.as_ref(),
+ "failed to write {:#2x}: {err:?}",
+ self.addr as u8
+ );
+ })?;
+
+ Ok(mode)
+ }
+}
+
+impl Atmega1608LedMode {
+ fn update_prescale(self, client: &I2cClient, prescale: u8) -> Result<()> {
+ let addr = match self {
+ Self::Dim0 => 0x02,
+ Self::Dim1 => 0x04,
+ Self::Off | Self::On => return Ok(()),
+ };
+ client.write_byte_data(addr, prescale).inspect_err(|err| {
+ dev_err!(client.as_ref(), "failed to write {addr:#2x}: {err:?}",);
+ })
+ }
+
+ fn update_pwm(self, client: &I2cClient, pwm: u8) -> Result<()> {
+ let addr = match self {
+ Self::Dim0 => 0x03,
+ Self::Dim1 => 0x05,
+ Self::Off | Self::On => return Ok(()),
+ };
+ client.write_byte_data(addr, pwm).inspect_err(|err| {
+ dev_err!(client.as_ref(), "failed to write {addr:#2x}: {err:?}",);
+ })
+ }
+}
+
+impl From<u8> for Atmega1608LedMode {
+ fn from(value: u8) -> Self {
+ const ON: u8 = Atmega1608LedMode::On as u8;
+ const DIM0: u8 = Atmega1608LedMode::Dim0 as u8;
+ const DIM1: u8 = Atmega1608LedMode::Dim1 as u8;
+
+ match value {
+ ON => Self::On,
+ DIM0 => Self::Dim0,
+ DIM1 => Self::Dim1,
+ _ => Self::Off,
+ }
+ }
+}
--
2.49.1
Powered by blists - more mailing lists