Linux Framebuffer Layer development
 help / color / mirror / Atom feed
* [PATCH] staging: sm750fb: fix mono image source stride mismatch in lynxfb_ops_imageblit()
From: Muhammad Bilal @ 2026-07-18  4:56 UTC (permalink / raw)
  To: Sudip Mukherjee, Teddy Wang
  Cc: Greg Kroah-Hartman, linux-fbdev, linux-staging, linux-kernel,
	Muhammad Bilal

sm750_hw_imageblit() advances its monochrome source pointer by
src_delta per scanline, and computes the correct rounded-up stride
internally as:

	bytes_per_scan = (width + start_bit + 7) / 8;

Its only caller, lynxfb_ops_imageblit(), instead passes src_delta as
image->width >> 3. For widths not a multiple of 8 this under-counts
the stride, so the source pointer falls further behind the real
per-scanline layout on every line, corrupting the rendered image.

Use DIV_ROUND_UP() so the stride passed in matches what
sm750_hw_imageblit() already assumes.

Signed-off-by: Muhammad Bilal <meatuni001@gmail.com>
---
 drivers/staging/sm750fb/sm750.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/staging/sm750fb/sm750.c b/drivers/staging/sm750fb/sm750.c
index 89c811e0806c..69e803f4f175 100644
--- a/drivers/staging/sm750fb/sm750.c
+++ b/drivers/staging/sm750fb/sm750.c
@@ -261,7 +261,7 @@ static void lynxfb_ops_imageblit(struct fb_info *info,
 	spin_lock(&sm750_dev->slock);
 
 	sm750_dev->accel.de_imageblit(&sm750_dev->accel,
-				      image->data, image->width >> 3, 0,
+				      image->data, DIV_ROUND_UP(image->width, 8), 0,
 				      base, pitch, bpp,
 				      image->dx, image->dy,
 				      image->width, image->height,
-- 
2.55.0


^ permalink raw reply related

* Re: [PATCH 00/32] x86/msr: Drop 32-bit MSR interfaces
From: Ingo Molnar @ 2026-07-17  9:38 UTC (permalink / raw)
  To: Juergen Gross
  Cc: Sean Christopherson, Arnd Bergmann, linux-kernel, linux-pm,
	linux-edac@vger.kernel.org, x86, linux-acpi, kvm, linux-coco,
	linux-pci, virtualization, linux-ide, dri-devel, linux-fbdev,
	linux-crypto, open list:GPIO SUBSYSTEM, linux-hyperv, linux-hwmon,
	linux-perf-users, linux-mtd, platform-driver-x86,
	Rafael J . Wysocki, Daniel Lezcano, Zhang Rui,
	lukasz.luba@arm.com, Jason Baron, Borislav Petkov, Tony Luck,
	Yazen Ghannam, Len Brown, Pavel Machek, Thomas Gleixner,
	Ingo Molnar, Dave Hansen, H. Peter Anvin, Paolo Bonzini,
	Kirill A. Shutemov, Rick Edgecombe, Pu Wen, Bjorn Helgaas,
	Ajay Kaher, Alexey Makhalov, Broadcom internal kernel review list,
	Viresh Kumar, Reinette Chatre, Dave Martin, James Morse,
	Babu Moger, Tony W Wang-oc, Damien Le Moal, Niklas Cassel,
	Dave Airlie, Helge Deller, linux-geode, Olivia Mackall,
	Herbert Xu, Linus Walleij, Bartosz Golaszewski,
	Greg Kroah-Hartman, K. Y. Srinivasan, Haiyang Zhang, Wei Liu,
	Dexuan Cui, Long Li, Guenter Roeck, Peter Zijlstra,
	Arnaldo Carvalho de Melo, Namhyung Kim, Mark Rutland,
	Alexander Shishkin, Jiri Olsa, Ian Rogers, Adrian Hunter,
	James Clark, Josh Poimboeuf, Pawan Gupta, Vitaly Kuznetsov,
	Andy Lutomirski, Boris Ostrovsky, Huang Rui, Mario Limonciello,
	Perry Yuan, K Prateek Nayak, srinivas.pandruvada@linux.intel.com,
	Artem Bityutskiy, Artem Bityutskiy, Miquel Raynal,
	Richard Weinberger, Vignesh Raghavendra, Ashok Raj, Hans de Goede,
	Ilpo Järvinen, Rajneesh Bhardwaj, David E Box, xen-devel
In-Reply-To: <99228803-b8b7-47a3-b77c-6fdf3b785730@suse.com>


* Juergen Gross <jgross@suse.com> wrote:

> On 02.07.26 12:07, Ingo Molnar wrote:
> > 
> > * Sean Christopherson <seanjc@google.com> wrote:
> > 
> > > > Note that the individual patches are IMO significantly easier to review
> > > > through the actual 32-bit => 64-bit variable assignment changes done
> > > > in isolation (which sometimes include minor cleanups), while
> > > > the Coccinelle semantic patch:
> > > > 
> > > >     { a(b,c) => c = a(b) }
> > > > 
> > > > which changes both the function signature and the order of terms as
> > > > well, is just a single add-on treewide patch.
> > > 
> > > Is the plan for subsystem maintainers to pick up the relevant patches,
> > > and then do the treewide change one release cycle later?
> > 
> > I'll try to keep the patches in a single tree (tip:x86/msr)
> > in the hope of not prolonging the pain two cycles - but it's
> > of course fine for maintainers to pick up the patches too
> > (most of them are standalone), we'll sort it all out in the end.
> 
> Ingo, would you be fine with me posting patch updates just as replies to the
> original patch emails? This would speed things up, as I wouldn't need to wait
> for more review input of all the patches before sending out new versions.

Sure, that works for me too. I've picked up a couple of -v2 patches
already.

Thanks,

	Ingo

^ permalink raw reply

* Re: [PATCH] staging: fbtft: fb_ra8875: replace udelay with usleep_range
From: Andy Shevchenko @ 2026-07-16 18:37 UTC (permalink / raw)
  To: mashikiselby
  Cc: Andy Shevchenko, Greg Kroah-Hartman, dri-devel, linux-fbdev,
	linux-staging, linux-kernel
In-Reply-To: <20260716-staging-testing-v1-1-2ebc0c6bb27a@gmail.com>

On Thu, Jul 16, 2026 at 01:08:33PM +0200, Selby Mashiki via B4 Relay wrote:

> udelay() busy-waits for the specified duration, consuming CPU cycles
> unnecessarily. usleep_range() is preferred for delays that can sleep,
> as it allows the scheduler to run other tasks during the wait.
> 
> Replace two instances of udelay(100) with usleep_range(100, 200) to
> use the preferred sleeping delay function.

NAK. Read mailing list archives why.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply

* Re: [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver
From: Guenter Roeck @ 2026-07-16 11:36 UTC (permalink / raw)
  To: Ramiro Manuel Silva Oliveira, Lee Jones, Linus Walleij,
	Bartosz Golaszewski, Andi Shyti, Daniel Thompson, Jingoo Han,
	Helge Deller, Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano,
	Zhang Rui, Lukasz Luba
  Cc: linux-kernel@vger.kernel.org, mfd@lists.linux.dev,
	linux-gpio@vger.kernel.org, linux-hwmon@vger.kernel.org,
	linux-i2c@vger.kernel.org, dri-devel@lists.freedesktop.org,
	linux-fbdev@vger.kernel.org, linux-watchdog@vger.kernel.org,
	linux-pm@vger.kernel.org, Wenkai.Chung, Francisco Aragon-Trivino,
	Hongzhi Wang, Mikhail Tsukerman, Thomas.Kastner
In-Reply-To: <PSAPR02MB4502BBD659D7C4FCDDCABB6294C72@PSAPR02MB4502.apcprd02.prod.outlook.com>

On 7/16/26 01:10, Ramiro Manuel Silva Oliveira wrote:
>> On 7/14/26 08:54, Ramiro Oliveira wrote:
>>> This driver controls the Hardware Monitor block of the Advantech EIO chip.
>>>
>>> There is a separate driver for the thermal device driver since it's a
>>> use case we support currently, but if it's really necessary we can
>>> move it to the same driver.
>>>
>>> Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
>>
>> Normally there would be a hwmon driver instantiating the thermal zone(s)
>> using HWMON_C_REGISTER_TZ. If the driver actively controls thermal zones
>> since it also handles fans, that would be instantiated from the hwmon driver.
>>
>> Since you have both thermal temperature and thermal fan drivers, I am very
>> concerned that there is functionality overlap andor even conflicts. You should
>> drop this driver and configure the thermal temperature driver to instantiate a
>> hwmon device from it if so desired.
> 
> But this hwmon driver doesn't control any setting, it just displays values. Fan
> and Thermal Zone control is handled in the thermal fan and thermal
> temperature drivers.
> 
> Also this hwmon shows values not related to temperature, like voltage,
> current and intrusion.
> 
> Would it be ok if I kept the HW Monitor since it only displays values, and if
> not can I keep it to show the other non-temperature values?
> 

All other similar drivers implement a hwmon driver and instantiate the thermal
subsystem from it. You decided to implement thermal drivers. You should only
have thermal or hwmon drivers, not both.

Having both would essentially implement two drivers with overlapping functionality.
Since you decided that you want separate thermal drivers, and two of them, you
should drop the hwmon driver.

Guenter


^ permalink raw reply

* [PATCH] staging: fbtft: fb_ra8875: replace udelay with usleep_range
From: Selby Mashiki via B4 Relay @ 2026-07-16 11:08 UTC (permalink / raw)
  To: Andy Shevchenko, Greg Kroah-Hartman
  Cc: dri-devel, linux-fbdev, linux-staging, linux-kernel,
	Selby Mashiki

From: Selby Mashiki <mashikiselby@gmail.com>

udelay() busy-waits for the specified duration, consuming CPU cycles
unnecessarily. usleep_range() is preferred for delays that can sleep,
as it allows the scheduler to run other tasks during the wait.

Replace two instances of udelay(100) with usleep_range(100, 200) to
use the preferred sleeping delay function.

Signed-off-by: Selby Mashiki <mashikiselby@gmail.com>
---
 drivers/staging/fbtft/fb_ra8875.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/drivers/staging/fbtft/fb_ra8875.c b/drivers/staging/fbtft/fb_ra8875.c
index 0ab1de664..d2400bb44 100644
--- a/drivers/staging/fbtft/fb_ra8875.c
+++ b/drivers/staging/fbtft/fb_ra8875.c
@@ -210,7 +210,7 @@ static void write_reg8_bus8(struct fbtft_par *par, int len, ...)
 	}
 	len--;
 
-	udelay(100);
+	usleep_range(100, 200);
 
 	if (len) {
 		buf = (u8 *)par->buf;
@@ -231,7 +231,7 @@ static void write_reg8_bus8(struct fbtft_par *par, int len, ...)
 
 	/* restore user spi-speed */
 	par->fbtftops.write = fbtft_write_spi;
-	udelay(100);
+	usleep_range(100, 200);
 }
 
 static int write_vmem16_bus8(struct fbtft_par *par, size_t offset, size_t len)

---
base-commit: 64276d9bfe4d1fdd5cf2636f1065f7ea55c2defb
change-id: 20260716-staging-testing-b1b81d63d0d1

Best regards,
--  
Selby Mashiki <mashikiselby@gmail.com>



^ permalink raw reply related

* RE: [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver
From: Ramiro Manuel Silva Oliveira @ 2026-07-16  8:10 UTC (permalink / raw)
  To: Guenter Roeck, Lee Jones, Linus Walleij, Bartosz Golaszewski,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel@vger.kernel.org, mfd@lists.linux.dev,
	linux-gpio@vger.kernel.org, linux-hwmon@vger.kernel.org,
	linux-i2c@vger.kernel.org, dri-devel@lists.freedesktop.org,
	linux-fbdev@vger.kernel.org, linux-watchdog@vger.kernel.org,
	linux-pm@vger.kernel.org, Wenkai.Chung, Francisco Aragon-Trivino,
	Hongzhi Wang, Mikhail Tsukerman, Thomas.Kastner
In-Reply-To: <b7171173-aa64-4393-b9cf-77d92f2ec932@roeck-us.net>

> On 7/14/26 08:54, Ramiro Oliveira wrote:
> > This driver controls the Hardware Monitor block of the Advantech EIO chip.
> >
> > There is a separate driver for the thermal device driver since it's a
> > use case we support currently, but if it's really necessary we can
> > move it to the same driver.
> >
> > Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
> 
> Normally there would be a hwmon driver instantiating the thermal zone(s)
> using HWMON_C_REGISTER_TZ. If the driver actively controls thermal zones
> since it also handles fans, that would be instantiated from the hwmon driver.
> 
> Since you have both thermal temperature and thermal fan drivers, I am very
> concerned that there is functionality overlap andor even conflicts. You should
> drop this driver and configure the thermal temperature driver to instantiate a
> hwmon device from it if so desired.

But this hwmon driver doesn't control any setting, it just displays values. Fan
and Thermal Zone control is handled in the thermal fan and thermal 
temperature drivers.

Also this hwmon shows values not related to temperature, like voltage,
current and intrusion.

Would it be ok if I kept the HW Monitor since it only displays values, and if
not can I keep it to show the other non-temperature values?

> 
> Thanks,
> Guenter


^ permalink raw reply

* [PATCH] staging: sm750fb: remove unused set_all_eng_off field
From: Hungyu Lin @ 2026-07-16  4:46 UTC (permalink / raw)
  To: Sudip Mukherjee, Teddy Wang, Greg Kroah-Hartman
  Cc: Dan Carpenter, linux-fbdev, linux-staging, linux-kernel,
	Hungyu Lin

The set_all_eng_off field is only initialized to 0 and is never set by
any caller.

Remove the unused field together with the dead cleanup path guarded by
it.

No functional change intended.

Signed-off-by: Hungyu Lin <dennylin0707@gmail.com>
---
 drivers/staging/sm750fb/ddk750_chip.c | 27 ---------------------------
 drivers/staging/sm750fb/ddk750_chip.h |  7 -------
 drivers/staging/sm750fb/sm750.c       |  1 -
 3 files changed, 35 deletions(-)

diff --git a/drivers/staging/sm750fb/ddk750_chip.c b/drivers/staging/sm750fb/ddk750_chip.c
index 0bb56bbec43f..aa78d0bc1d87 100644
--- a/drivers/staging/sm750fb/ddk750_chip.c
+++ b/drivers/staging/sm750fb/ddk750_chip.c
@@ -263,33 +263,6 @@ int ddk750_init_hw(struct initchip_param *p_init_param)
 		poke32(MISC_CTRL, reg);
 	}
 
-	if (p_init_param->set_all_eng_off == 1) {
-		sm750_enable_2d_engine(0);
-
-		/* Disable Overlay, if a former application left it on */
-		reg = peek32(VIDEO_DISPLAY_CTRL);
-		reg &= ~DISPLAY_CTRL_PLANE;
-		poke32(VIDEO_DISPLAY_CTRL, reg);
-
-		/* Disable video alpha, if a former application left it on */
-		reg = peek32(VIDEO_ALPHA_DISPLAY_CTRL);
-		reg &= ~DISPLAY_CTRL_PLANE;
-		poke32(VIDEO_ALPHA_DISPLAY_CTRL, reg);
-
-		/* Disable alpha plane, if a former application left it on */
-		reg = peek32(ALPHA_DISPLAY_CTRL);
-		reg &= ~DISPLAY_CTRL_PLANE;
-		poke32(ALPHA_DISPLAY_CTRL, reg);
-
-		/* Disable DMA Channel, if a former application left it on */
-		reg = peek32(DMA_ABORT_INTERRUPT);
-		reg |= DMA_ABORT_INTERRUPT_ABORT_1;
-		poke32(DMA_ABORT_INTERRUPT, reg);
-
-		/* Disable DMA Power, if a former application left it on */
-		sm750_enable_dma(0);
-	}
-
 	/* We can add more initialization as needed. */
 
 	return 0;
diff --git a/drivers/staging/sm750fb/ddk750_chip.h b/drivers/staging/sm750fb/ddk750_chip.h
index ee2e9d90f7dd..2a13debc179f 100644
--- a/drivers/staging/sm750fb/ddk750_chip.h
+++ b/drivers/staging/sm750fb/ddk750_chip.h
@@ -76,13 +76,6 @@ struct initchip_param {
 	 */
 	unsigned short master_clock;
 
-	/*
-	 * 0 = leave all engine state untouched.
-	 * 1 = make sure they are off: 2D, Overlay,
-	 * video alpha, alpha, hardware cursors
-	 */
-	unsigned short set_all_eng_off;
-
 	/*
 	 * 0 = Do not reset the memory controller
 	 * 1 = Reset the memory controller
diff --git a/drivers/staging/sm750fb/sm750.c b/drivers/staging/sm750fb/sm750.c
index a2f244878f85..039e2033f84e 100644
--- a/drivers/staging/sm750fb/sm750.c
+++ b/drivers/staging/sm750fb/sm750.c
@@ -837,7 +837,6 @@ static void sm750fb_setup(struct sm750_dev *sm750_dev, char *src)
 	sm750_dev->init_parm.mem_clock = 0;
 	sm750_dev->init_parm.master_clock = 0;
 	sm750_dev->init_parm.power_mode = 0;
-	sm750_dev->init_parm.set_all_eng_off = 0;
 	sm750_dev->init_parm.reset_memory = 1;
 
 	/* defaultly turn g_hwcursor on for both view */
-- 
2.43.0


^ permalink raw reply related

* Re: [PATCH v2 3/8] Add Advantech EIO Hardware Monitor driver
From: Guenter Roeck @ 2026-07-16  0:29 UTC (permalink / raw)
  To: Ramiro Oliveira, Lee Jones, Linus Walleij, Bartosz Golaszewski,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner
In-Reply-To: <20260714-upstream-v2-v2-3-76e5e41026db@advantech.com>

On 7/14/26 08:54, Ramiro Oliveira wrote:
> This driver controls the Hardware Monitor block of the Advantech EIO chip.
> 
> There is a separate driver for the thermal device driver
> since it's a use case we support currently, but if it's
> really necessary we can move it to the same driver.
> 
> Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>

Normally there would be a hwmon driver instantiating the thermal zone(s)
using HWMON_C_REGISTER_TZ. If the driver actively controls thermal zones
since it also handles fans, that would be instantiated from the hwmon driver.

Since you have both thermal temperature and thermal fan drivers, I am very
concerned that there is functionality overlap andor even conflicts. You
should drop this driver and configure the thermal temperature driver
to instantiate a hwmon device from it if so desired.

Thanks,
Guenter


^ permalink raw reply

* Re: [PATCH v2 2/8] Add Advantech EIO GPIO driver
From: Bartosz Golaszewski @ 2026-07-15 12:24 UTC (permalink / raw)
  To: Ramiro Oliveira
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner, Lee Jones, Linus Walleij, Bartosz Golaszewski,
	Guenter Roeck, Andi Shyti, Daniel Thompson, Jingoo Han,
	Helge Deller, Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano,
	Zhang Rui, Lukasz Luba
In-Reply-To: <20260714-upstream-v2-v2-2-76e5e41026db@advantech.com>

On Tue, 14 Jul 2026 17:54:16 +0200, Ramiro Oliveira
<ramiro.oliveira@advantech.com> said:
> This driver controls the GPIO component of the Advantech EIO chip.
>

The subject should be: "gpio: add a driver for the Advantech EIO GPIO
controller"
or similar.

> Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
> ---
>  MAINTAINERS             |   7 ++
>  drivers/gpio/Kconfig    |   6 ++
>  drivers/gpio/Makefile   |   1 +
>  drivers/gpio/gpio-eio.c | 252 ++++++++++++++++++++++++++++++++++++++++++++++++
>  4 files changed, 266 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 53b5f7412966..a7da47393815 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -609,6 +609,13 @@ S:	Maintained
>  F:	Documentation/scsi/advansys.rst
>  F:	drivers/scsi/advansys.c
>
> +ADVANTECH EIO DRIVER
> +M:	Ramiro Oliveira <ramiro.oliveira@advantech.com>
> +S:	Maintained
> +F:	drivers/gpio/gpio-eio.c
> +F:	drivers/mfd/eio_core.c
> +F:	include/linux/mfd/eio.h

Shouldn't this be part of the MFD entry?

> +
>  ADVANTECH SWBTN DRIVER
>  M:	Andrea Ho <Andrea.Ho@advantech.com.tw>
>  L:	platform-driver-x86@vger.kernel.org
> diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
> index f03c05288376..696065d77235 100644
> --- a/drivers/gpio/Kconfig
> +++ b/drivers/gpio/Kconfig
> @@ -295,6 +295,12 @@ config GPIO_DWAPB
>  	  Say Y or M here to build support for the Synopsys DesignWare APB
>  	  GPIO block.
>
> +config GPIO_EIO
> +	tristate "Advantech EIO GPIO"
> +	depends on MFD_EIO
> +	help
> +	  Say Y or M to build support for Advantech EIO GPIO block.
> +
>  config GPIO_EIC_SPRD
>  	tristate "Spreadtrum EIC support"
>  	depends on ARCH_SPRD || COMPILE_TEST
> diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
> index fa14581e3995..628596705c21 100644
> --- a/drivers/gpio/Makefile
> +++ b/drivers/gpio/Makefile
> @@ -64,6 +64,7 @@ obj-$(CONFIG_GPIO_DLN2)			+= gpio-dln2.o
>  obj-$(CONFIG_GPIO_DS4520)		+= gpio-ds4520.o
>  obj-$(CONFIG_GPIO_DWAPB)		+= gpio-dwapb.o
>  obj-$(CONFIG_GPIO_EIC_SPRD)		+= gpio-eic-sprd.o
> +obj-$(CONFIG_GPIO_EIO)			+= gpio-eio.o
>  obj-$(CONFIG_GPIO_ELKHARTLAKE)		+= gpio-elkhartlake.o
>  obj-$(CONFIG_GPIO_EM)			+= gpio-em.o
>  obj-$(CONFIG_GPIO_EN7523)		+= gpio-en7523.o
> diff --git a/drivers/gpio/gpio-eio.c b/drivers/gpio/gpio-eio.c
> new file mode 100644
> index 000000000000..34e1aefd0716
> --- /dev/null
> +++ b/drivers/gpio/gpio-eio.c
> @@ -0,0 +1,252 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * GPIO driver for Advantech EIO Embedded controller.
> + *
> + * Copyright (C) 2025 Advantech Corporation. All rights reserved.
> + */
> +
> +#include <linux/errno.h>
> +#include <linux/gpio/driver.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mfd/eio.h>
> +#include <linux/module.h>
> +
> +#define EIO_GPIO_MAX_PINS	48
> +#define EIO_GPIO_WRITE		0x18
> +#define EIO_GPIO_READ		0x19
> +
> +struct eio_gpio_dev {
> +	u64 avail;
> +	int max;
> +	struct gpio_chip chip;
> +	struct device *dev;
> +};
> +
> +static struct {
> +	int size;
> +	bool write;
> +} ctrl_para[] = {

Please use a common prefix for all symbols. "eio_gpio" like above is fine.

> +	{ 0x01, false }, { 0x00, false }, { 0x00, false }, { 0x02, false },
> +	{ 0x01, false }, { 0x00, false }, { 0x00, false }, { 0x00, false },
> +	{ 0x00, false }, { 0x00, false }, { 0x00, false }, { 0x00, false },
> +	{ 0x00, false }, { 0x00, false }, { 0x00, false }, { 0x00, false },
> +	{ 0x01, true  }, { 0x01, true  }, { 0x02, true  }, { 0x02, true  },
> +	{ 0x02, false }, { 0x10, false }
> +};
> +
> +enum gpio_ctrl {
> +	EIO_GPIO_STATUS = 0x0,
> +	EIO_GPIO_GROUP_AVAIL = 0x3,
> +	EIO_GPIO_ERROR = 0x04,
> +	EIO_GPIO_PIN_DIR = 0x10,
> +	EIO_GPIO_PIN_LEVEL = 0x11,
> +	EIO_GPIO_GROUP_DIR = 0x12,
> +	EIO_GPIO_GROUP_LEVEL = 0x13,
> +	EIO_GPIO_MAPPING = 0x14,
> +	EIO_GPIO_NAME = 0x15
> +};
> +
> +static struct {
> +	int group;
> +	int port;
> +} group_map[] = {
> +	{ 0, 0 }, { 0, 1 },
> +	{ 1, 0 }, { 1, 1 },
> +	{ 2, 0 }, { 2, 1 },
> +	{ 3, 0 }, { 3, 1 },
> +	{ 3, 2 }, { 3, 3 },
> +	{ 3, 4 }, { 3, 5 },
> +	{ 3, 6 }, { 3, 7 }
> +};
> +
> +static int pmc_write(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
> +{
> +	struct pmc_op op = {
> +		 .cmd       = EIO_GPIO_WRITE,
> +		 .control   = ctrl,
> +		 .device_id = dev_id,
> +		 .payload   = (u8 *)data,
> +	};
> +
> +	if (ctrl >= ARRAY_SIZE(ctrl_para))
> +		return -ENOMEM;
> +
> +	if (!ctrl_para[ctrl].write)
> +		return -EINVAL;
> +
> +	op.size = ctrl_para[ctrl].size;
> +
> +	return eio_core_pmc_operation(mfd_dev, &op);
> +}
> +
> +static int pmc_read(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
> +{
> +	struct pmc_op op = {
> +		 .cmd       = EIO_GPIO_READ,
> +		 .control   = ctrl,
> +		 .device_id = dev_id,
> +		 .payload   = (u8 *)data,
> +	};
> +
> +	if (ctrl > ARRAY_SIZE(ctrl_para))
> +		return -ENOMEM;
> +
> +	op.size = ctrl_para[ctrl].size;
> +
> +	return eio_core_pmc_operation(mfd_dev, &op);
> +}
> +
> +static int get_dir(struct gpio_chip *chip, unsigned int offset)
> +{
> +	u8 dir;
> +	int ret;
> +
> +	ret = pmc_read(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
> +	if (ret)
> +		return ret;
> +
> +	return dir ? 0 : 1;
> +}
> +
> +static int dir_input(struct gpio_chip *chip, unsigned int offset)
> +{
> +	u8 dir = 0;
> +
> +	return pmc_write(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
> +}
> +
> +static int dir_output(struct gpio_chip *chip, unsigned int offset, int value)
> +{
> +	u8 dir = 1;
> +	u8 val = value;
> +
> +	pmc_write(chip->parent, EIO_GPIO_PIN_DIR, offset, &dir);
> +
> +	return pmc_write(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &val);
> +}
> +
> +static int gpio_get(struct gpio_chip *chip, unsigned int offset)
> +{
> +	u8 level;
> +	int ret;
> +
> +	ret = pmc_read(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &level);
> +	if (ret)
> +		return ret;
> +
> +	return level;
> +}
> +
> +static int gpio_set(struct gpio_chip *chip, unsigned int offset, int value)
> +{
> +	u8 val = value;
> +
> +	return pmc_write(chip->parent, EIO_GPIO_PIN_LEVEL, offset, &val);
> +}
> +
> +static int check_support(struct device *dev)
> +{
> +	u8  data;
> +	int ret;
> +
> +	ret = pmc_read(dev, EIO_GPIO_STATUS, 0, &data);
> +	if (ret)
> +		return ret;
> +
> +	if ((data & 0x01) == 0)
> +		return -EOPNOTSUPP;
> +
> +	return 0;
> +}
> +
> +static int check_pin(struct device *dev, int pin)
> +{
> +	int ret;
> +	int group, bit;

Why not put all ints on the same line? Same elsewhere.

> +	u16 data;
> +
> +	/* Get pin mapping */
> +	ret = pmc_read(dev, EIO_GPIO_MAPPING, pin, &data);
> +	if (ret)
> +		return ret;
> +
> +	if ((data & 0xFF) > ARRAY_SIZE(group_map))
> +		return -EINVAL;
> +
> +	group = group_map[data & 0xFF].group;
> +	bit   = data >> 8;
> +
> +	/* Check mapped pin */
> +	ret = pmc_read(dev, EIO_GPIO_GROUP_AVAIL, group, &data);
> +	if (ret)
> +		return ret;
> +
> +	return data & BIT(bit) ? 0 : -EOPNOTSUPP;
> +}
> +
> +static int gpio_init(struct device *mfd, struct eio_gpio_dev *eio_gpio)
> +{
> +	int ret, i;
> +
> +	ret = check_support(mfd);
> +	if (ret)

This warrants an -ENODEV. We don't want to retry probe here, do we?

> +		return dev_err_probe(eio_gpio->dev, ret, "GPIO not supported\n");
> +
> +	eio_gpio->avail = 0;
> +
> +	for (i = 0 ; i < EIO_GPIO_MAX_PINS ; i++) {
> +		ret = check_pin(mfd, i);
> +		if (ret)
> +			continue;
> +
> +		eio_gpio->avail |= BIT(i);
> +		eio_gpio->max = i + 1;
> +	}
> +
> +	return eio_gpio->max ? 0 : -EOPNOTSUPP;
> +}
> +
> +static int gpio_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct eio_gpio_dev *eio_gpio;
> +	struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
> +
> +	if (!eio_dev)
> +		return dev_err_probe(dev, -ENODEV, "Error contact eio_core\n");

-ENODEV is used for silently bailing out of probe and telling the driver core
to not defer. Basically it means: this device cannot be enabled on this system
or is not present at all. This means: don't use dev_err_probe() and don't emit
any logs. Just return -ENODEV.

Though it makes me think - can this even happen at all? MFD cells won't get
created if parent's probe() failed.

> +
> +	eio_gpio = devm_kzalloc(dev, sizeof(*eio_gpio), GFP_KERNEL);
> +	if (!eio_gpio)
> +		return -ENOMEM;
> +
> +	eio_gpio->dev = dev;
> +
> +	if (gpio_init(dev->parent, eio_gpio))

Can you use the ret = gpio_init() pattern for consistency with the rest of the
code?

> +		return -EIO;
> +
> +	eio_gpio->chip.parent = dev->parent;
> +	eio_gpio->chip.ngpio = eio_gpio->max;
> +	eio_gpio->chip.label = KBUILD_MODNAME;
> +	eio_gpio->chip.owner = THIS_MODULE;
> +	eio_gpio->chip.direction_input = dir_input;
> +	eio_gpio->chip.get = gpio_get;
> +	eio_gpio->chip.direction_output = dir_output;
> +	eio_gpio->chip.set = gpio_set;
> +	eio_gpio->chip.get_direction = get_dir;
> +	eio_gpio->chip.base = -1;
> +	eio_gpio->chip.can_sleep = true;
> +
> +	return devm_gpiochip_add_data(dev, &eio_gpio->chip, eio_gpio);
> +}
> +
> +static struct platform_driver gpio_driver = {
> +	.probe  = gpio_probe,
> +	.driver = { .name = KBUILD_MODNAME, },
> +};
> +
> +module_platform_driver(gpio_driver);
> +
> +MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
> +MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
> +MODULE_DESCRIPTION("GPIO driver for Advantech EIO embedded controller");
> +MODULE_LICENSE("GPL");
>
> --
> 2.43.0
>
>

Bart

^ permalink raw reply

* Re: [PATCH 16/16] arm: omap2: remove remaining pdata-quirks for pandora legacy devices
From: Andreas Kemnade @ 2026-07-14 18:46 UTC (permalink / raw)
  To: H. Nikolaus Schaller
  Cc: Grond, Stefan Leichter, Grazvydas Ignotas, Tony Lindgren,
	Ethan Nelson-Moore, Jarkko Nikula, Sascha Hauer, Lee Jones,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Liam Girdwood,
	Mark Brown, Aaro Koskinen, Kevin Hilman, Roger Quadros,
	Russell King, Daniel Thompson, Jingoo Han, Helge Deller,
	Jaroslav Kysela, Takashi Iwai, Sen Wang, Richard Fitzgerald,
	Arnd Bergmann, Srinivas Kandagatla, Kuninori Morimoto,
	Charles Keepax, Niranjan H Y, letux-kernel, devicetree,
	linux-kernel, linux-sound, linux-omap, linux-arm-kernel,
	dri-devel, linux-fbdev, kernel, mfd
In-Reply-To: <d380941287c655a3d205ee8b08516d364c0ddec9.1783749722.git.hns@goldelico.com>

On Sat, 11 Jul 2026 08:02:03 +0200
"H. Nikolaus Schaller" <hns@goldelico.com> wrote:

> After updating the pandora-backligt setup and removing the
> omap3pandora sound system and defining related gpios by device tree,
> we can remove omap3_pandora_legacy_init() and the related legacy
> devices completely.
> 
> Signed-off-by: H. Nikolaus Schaller <hns@goldelico.com>

regarding bisectabily: Do we create mess, if the platform devices via
quirks are disabled?

I am wondering how we handle this. If this patch goes together with the
removal of the legacy drivers, then there might be a situation where
neither the new one nor the old one is available? Split this all up?

If sound discussion takes longer, maybe do the backlight thing alone?

Regards,
Andreas
> ---
>  arch/arm/mach-omap2/pdata-quirks.c | 23 -----------------------
>  1 file changed, 23 deletions(-)
> 
> diff --git a/arch/arm/mach-omap2/pdata-quirks.c
> b/arch/arm/mach-omap2/pdata-quirks.c index
> b947bacf23a37..aca7097a692ed 100644 ---
> a/arch/arm/mach-omap2/pdata-quirks.c +++
> b/arch/arm/mach-omap2/pdata-quirks.c @@ -268,27 +268,6 @@ static void
> __init omap3_logicpd_torpedo_init(void) omap3_gpio126_127_129();
>  }
>  
> -/* omap3pandora legacy devices */
> -
> -static struct platform_device pandora_backlight = {
> -	.name	= "pandora-backlight",
> -	.id	= -1,
> -};
> -
> -static struct gpiod_lookup_table pandora_soc_audio_gpios = {
> -	.dev_id = "soc-audio",
> -	.table = {
> -		GPIO_LOOKUP("gpio-112-127", 6, "dac",
> GPIO_ACTIVE_HIGH),
> -		GPIO_LOOKUP("gpio-0-15", 14, "amp",
> GPIO_ACTIVE_HIGH),
> -		{ }
> -	},
> -};
> -
> -static void __init omap3_pandora_legacy_init(void)
> -{
> -	platform_device_register(&pandora_backlight);
> -	gpiod_add_lookup_table(&pandora_soc_audio_gpios);
> -}
>  #endif /* CONFIG_ARCH_OMAP3 */
>  
>  #ifdef CONFIG_SOC_DRA7XX
> @@ -514,8 +493,6 @@ static struct pdata_init pdata_quirks[]
> __initdata = { { "ti,omap3-evm-37xx", omap3_evm_legacy_init, },
>  	{ "ti,am3517-evm", am3517_evm_legacy_init, },
>  	{ "technexion,omap3-tao3530", omap3_tao3530_legacy_init, },
> -	{ "openpandora,omap3-pandora-600mhz",
> omap3_pandora_legacy_init, },
> -	{ "openpandora,omap3-pandora-1ghz",
> omap3_pandora_legacy_init, }, #endif
>  	{ /* sentinel */ },
>  };


^ permalink raw reply

* Re: [PATCH 06/16] ASoC: twl4030-codec: Allow setting APLL rate through the .set_sysclk() interface
From: Andreas Kemnade @ 2026-07-14 18:36 UTC (permalink / raw)
  To: H. Nikolaus Schaller
  Cc: Grond, Stefan Leichter, Grazvydas Ignotas, Tony Lindgren,
	Ethan Nelson-Moore, Jarkko Nikula, Sascha Hauer, Lee Jones,
	Rob Herring, Krzysztof Kozlowski, Conor Dooley, Liam Girdwood,
	Mark Brown, Aaro Koskinen, Kevin Hilman, Roger Quadros,
	Russell King, Daniel Thompson, Jingoo Han, Helge Deller,
	Jaroslav Kysela, Takashi Iwai, Sen Wang, Richard Fitzgerald,
	Arnd Bergmann, Srinivas Kandagatla, Kuninori Morimoto,
	Charles Keepax, Niranjan H Y, letux-kernel, devicetree,
	linux-kernel, linux-sound, linux-omap, linux-arm-kernel,
	dri-devel, linux-fbdev, kernel, mfd
In-Reply-To: <7ea589f951baf1e217c7169ae21c0bf510672ecf.1783749722.git.hns@goldelico.com>

On Sat, 11 Jul 2026 08:01:53 +0200
"H. Nikolaus Schaller" <hns@goldelico.com> wrote:

> From: Grond <grond66@riseup.net>
> 
> On the Pandora, the APLL is actually what drives all of the audio
> clocks on the board. Since the TWL4030 is only used for capture, we
> need some way of setting the correct rate for playback streams, since
> they will not go through the TWL4030's .hw_params() code path (which
> is normally how APLL rate is set).
> 
> Signed-off-by: Grond <grond66@riseup.net>
> Signed-off-by: H. Nikolaus Schaller <hns@goldelico.com>

As a general comment: might we model this as clock provider (and maybe
that enables the usage of a generic audio-graph-card)?

Regards,
Andreas


^ permalink raw reply

* Re: [PATCH 0/3] staging/fbtft: Minor cleanups.
From: Dan Carpenter @ 2026-07-15  7:28 UTC (permalink / raw)
  To: Panagiotis Gravias
  Cc: andy, gregkh, namcao, tomasz.unger, dri-devel, linux-fbdev,
	linux-staging, linux-kernel
In-Reply-To: <20260714190958.219437-2-graviaspanagiotis1@gmail.com>

On Tue, Jul 14, 2026 at 10:09:38PM +0300, Panagiotis Gravias wrote:
> Solved some style errors,
> parenthesis spacing, aligning arguments, replacing udelay() with
> usleep_range().
> 
> I hope this makes future checks with the style script cleaner and more
> helpfull.
> 

With old code you should search lore for why it still exists.

regards,
dan carpenter


^ permalink raw reply

* Re: [PATCH 0/3] staging/fbtft: Minor cleanups.
From: Andy Shevchenko @ 2026-07-14 19:18 UTC (permalink / raw)
  To: Panagiotis Gravias
  Cc: andy, gregkh, namcao, tomasz.unger, dri-devel, linux-fbdev,
	linux-staging, linux-kernel
In-Reply-To: <20260714190958.219437-2-graviaspanagiotis1@gmail.com>

On Tue, Jul 14, 2026 at 10:09:38PM +0300, Panagiotis Gravias wrote:
> Solved some style errors,
> parenthesis spacing, aligning arguments, replacing udelay() with
> usleep_range().
> 
> I hope this makes future checks with the style script cleaner and more
> helpfull.

To understand the resolution, read the mailing list archives for
the last ~6-9 months against this driver.

Also read and understand README in the folder of the driver.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply

* Re: [PATCH 3/3] staging: fbtft: Replace udelay with usleep_range
From: Andy Shevchenko @ 2026-07-14 19:17 UTC (permalink / raw)
  To: Panagiotis Gravias
  Cc: andy, gregkh, namcao, tomasz.unger, dri-devel, linux-fbdev,
	linux-staging, linux-kernel
In-Reply-To: <20260714190958.219437-6-graviaspanagiotis1@gmail.com>

On Tue, Jul 14, 2026 at 10:09:42PM +0300, Panagiotis Gravias wrote:
> Fix checkpatch warnings preferring usleep_range over udelay.
> Replaced udelay() calls with usleep_range() providing a reasonable
> upper bound to allow the scheduler to coalesce timer interrupts.

NAK.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply

* Re: [PATCH 2/3] staging/fbtft: Align arguments to match open parenthesis
From: Andy Shevchenko @ 2026-07-14 19:16 UTC (permalink / raw)
  To: Panagiotis Gravias
  Cc: andy, gregkh, namcao, tomasz.unger, dri-devel, linux-fbdev,
	linux-staging, linux-kernel
In-Reply-To: <20260714190958.219437-5-graviaspanagiotis1@gmail.com>

On Tue, Jul 14, 2026 at 10:09:41PM +0300, Panagiotis Gravias wrote:
> Fix checkpatch warning for alignment in write_reg() call to match
> the open parenthesis, improving code readability.

NAK.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply

* Re: [PATCH 1/3] staging/fbtft: Remove prohibited space before close parenthesis
From: Andy Shevchenko @ 2026-07-14 19:16 UTC (permalink / raw)
  To: Panagiotis Gravias
  Cc: andy, gregkh, namcao, tomasz.unger, dri-devel, linux-fbdev,
	linux-staging, linux-kernel
In-Reply-To: <20260714190958.219437-4-graviaspanagiotis1@gmail.com>

On Tue, Jul 14, 2026 at 10:09:40PM +0300, Panagiotis Gravias wrote:
> Fix checkpatch error: "space prohibited before that close parenthesis"
> in the define_fbtft_write_reg macro.

NAK.

-- 
With Best Regards,
Andy Shevchenko



^ permalink raw reply

* [PATCH 3/3] staging: fbtft: Replace udelay with usleep_range
From: Panagiotis Gravias @ 2026-07-14 19:09 UTC (permalink / raw)
  To: andy, gregkh
  Cc: namcao, tomasz.unger, dri-devel, linux-fbdev, linux-staging,
	linux-kernel, Panagiotis Gravias
In-Reply-To: <20260714190958.219437-2-graviaspanagiotis1@gmail.com>

Fix checkpatch warnings preferring usleep_range over udelay.
Replaced udelay() calls with usleep_range() providing a reasonable
upper bound to allow the scheduler to coalesce timer interrupts.

Signed-off-by: Panagiotis Gravias <graviaspanagiotis1@gmail.com>
---
 drivers/staging/fbtft/fb_ra8875.c    |  4 ++--
 drivers/staging/fbtft/fb_upd161704.c | 18 +++++++++---------
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/drivers/staging/fbtft/fb_ra8875.c b/drivers/staging/fbtft/fb_ra8875.c
index 0ab1de6647d0..d2400bb44f1c 100644
--- a/drivers/staging/fbtft/fb_ra8875.c
+++ b/drivers/staging/fbtft/fb_ra8875.c
@@ -210,7 +210,7 @@ static void write_reg8_bus8(struct fbtft_par *par, int len, ...)
 	}
 	len--;
 
-	udelay(100);
+	usleep_range(100, 200);
 
 	if (len) {
 		buf = (u8 *)par->buf;
@@ -231,7 +231,7 @@ static void write_reg8_bus8(struct fbtft_par *par, int len, ...)
 
 	/* restore user spi-speed */
 	par->fbtftops.write = fbtft_write_spi;
-	udelay(100);
+	usleep_range(100, 200);
 }
 
 static int write_vmem16_bus8(struct fbtft_par *par, size_t offset, size_t len)
diff --git a/drivers/staging/fbtft/fb_upd161704.c b/drivers/staging/fbtft/fb_upd161704.c
index c680160d6380..e26766f58a50 100644
--- a/drivers/staging/fbtft/fb_upd161704.c
+++ b/drivers/staging/fbtft/fb_upd161704.c
@@ -32,27 +32,27 @@ static int init_display(struct fbtft_par *par)
 
 	/* oscillator start */
 	write_reg(par, 0x003A, 0x0001);	/*Oscillator 0: stop, 1: operation */
-	udelay(100);
+	usleep_range(100, 200);
 
 	/* y-setting */
 	write_reg(par, 0x0024, 0x007B);	/* amplitude setting */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0025, 0x003B);	/* amplitude setting */
 	write_reg(par, 0x0026, 0x0034);	/* amplitude setting */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0027, 0x0004);	/* amplitude setting */
 	write_reg(par, 0x0052, 0x0025);	/* circuit setting 1 */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0053, 0x0033);	/* circuit setting 2 */
 	write_reg(par, 0x0061, 0x001C);	/* adjustment V10 positive polarity */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0062, 0x002C);	/* adjustment V9 negative polarity */
 	write_reg(par, 0x0063, 0x0022);	/* adjustment V34 positive polarity */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0064, 0x0027);	/* adjustment V31 negative polarity */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0065, 0x0014);	/* adjustment V61 negative polarity */
-	udelay(10);
+	usleep_range(10, 20);
 	write_reg(par, 0x0066, 0x0010);	/* adjustment V61 negative polarity */
 
 	/* Basical clock for 1 line (BASECOUNT[7:0]) number specified */
@@ -60,7 +60,7 @@ static int init_display(struct fbtft_par *par)
 
 	/* Power supply setting */
 	write_reg(par, 0x0019, 0x0000);	/* DC/DC output setting */
-	udelay(200);
+	usleep_range(200, 300);
 	write_reg(par, 0x001A, 0x1000);	/* DC/DC frequency setting */
 	write_reg(par, 0x001B, 0x0023);	/* DC/DC rising setting */
 	write_reg(par, 0x001C, 0x0C01);	/* Regulator voltage setting */
-- 
2.55.0


^ permalink raw reply related

* [PATCH 2/3] staging/fbtft: Align arguments to match open parenthesis
From: Panagiotis Gravias @ 2026-07-14 19:09 UTC (permalink / raw)
  To: andy, gregkh
  Cc: namcao, tomasz.unger, dri-devel, linux-fbdev, linux-staging,
	linux-kernel, Panagiotis Gravias
In-Reply-To: <20260714190958.219437-2-graviaspanagiotis1@gmail.com>

Fix checkpatch warning for alignment in write_reg() call to match
the open parenthesis, improving code readability.

Signed-off-by: Panagiotis Gravias <graviaspanagiotis1@gmail.com>
---
 drivers/staging/fbtft/fb_tinylcd.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/drivers/staging/fbtft/fb_tinylcd.c b/drivers/staging/fbtft/fb_tinylcd.c
index afa8f1c740c5..d58b1247262b 100644
--- a/drivers/staging/fbtft/fb_tinylcd.c
+++ b/drivers/staging/fbtft/fb_tinylcd.c
@@ -38,7 +38,7 @@ static int init_display(struct fbtft_par *par)
 	write_reg(par, 0xE5, 0x00);
 	write_reg(par, 0xF0, 0x36, 0xA5, 0x53);
 	write_reg(par, 0xE0, 0x00, 0x35, 0x33, 0x00, 0x00, 0x00,
-		       0x00, 0x35, 0x33, 0x00, 0x00, 0x00);
+		  0x00, 0x35, 0x33, 0x00, 0x00, 0x00);
 	write_reg(par, MIPI_DCS_SET_PIXEL_FORMAT, 0x55);
 	write_reg(par, MIPI_DCS_EXIT_SLEEP_MODE);
 	fsleep(250);
-- 
2.55.0


^ permalink raw reply related

* [PATCH 1/3] staging/fbtft: Remove prohibited space before close parenthesis
From: Panagiotis Gravias @ 2026-07-14 19:09 UTC (permalink / raw)
  To: andy, gregkh
  Cc: namcao, tomasz.unger, dri-devel, linux-fbdev, linux-staging,
	linux-kernel, Panagiotis Gravias
In-Reply-To: <20260714190958.219437-2-graviaspanagiotis1@gmail.com>

Fix checkpatch error: "space prohibited before that close parenthesis"
in the define_fbtft_write_reg macro.

Signed-off-by: Panagiotis Gravias <graviaspanagiotis1@gmail.com>
---
 drivers/staging/fbtft/fbtft-bus.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/drivers/staging/fbtft/fbtft-bus.c b/drivers/staging/fbtft/fbtft-bus.c
index 30e436ff19e4..38697992f5e1 100644
--- a/drivers/staging/fbtft/fbtft-bus.c
+++ b/drivers/staging/fbtft/fbtft-bus.c
@@ -62,9 +62,9 @@ out:									      \
 }                                                                             \
 EXPORT_SYMBOL(func);
 
-define_fbtft_write_reg(fbtft_write_reg8_bus8, u8, u8, )
+define_fbtft_write_reg(fbtft_write_reg8_bus8, u8, u8,)
 define_fbtft_write_reg(fbtft_write_reg16_bus8, __be16, u16, cpu_to_be16)
-define_fbtft_write_reg(fbtft_write_reg16_bus16, u16, u16, )
+define_fbtft_write_reg(fbtft_write_reg16_bus16, u16, u16,)
 
 void fbtft_write_reg8_bus9(struct fbtft_par *par, int len, ...)
 {
-- 
2.55.0


^ permalink raw reply related

* [PATCH 0/3] staging/fbtft: Minor cleanups.
From: Panagiotis Gravias @ 2026-07-14 19:09 UTC (permalink / raw)
  To: andy, gregkh
  Cc: namcao, tomasz.unger, dri-devel, linux-fbdev, linux-staging,
	linux-kernel, Panagiotis Gravias

Solved some style errors,
parenthesis spacing, aligning arguments, replacing udelay() with
usleep_range().

I hope this makes future checks with the style script cleaner and more
helpfull.

Panagiotis Gravias (3):
  staging/fbtft: Remove prohibited space before close parenthesis
  staging/fbtft: Align arguments to match open parenthesis
  staging: fbtft: Replace udelay with usleep_range

 drivers/staging/fbtft/fb_ra8875.c    |  4 ++--
 drivers/staging/fbtft/fb_tinylcd.c   |  2 +-
 drivers/staging/fbtft/fb_upd161704.c | 18 +++++++++---------
 drivers/staging/fbtft/fbtft-bus.c    |  4 ++--
 4 files changed, 14 insertions(+), 14 deletions(-)

-- 
2.55.0


^ permalink raw reply

* [PATCH v2 8/8] Add Advantech EIO Fan driver
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
  To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner, Ramiro Oliveira
In-Reply-To: <20260714-upstream-v2-v2-0-76e5e41026db@advantech.com>

This commit adds the driver to control the Advantech EIO Fan block,
which is included in the Advantech EIO Embedded Controller.

Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
 MAINTAINERS               |   1 +
 drivers/thermal/Kconfig   |   8 +
 drivers/thermal/Makefile  |   1 +
 drivers/thermal/eio_fan.c | 478 ++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 488 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index ded89e70d6cc..5d5d4ac54030 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -616,6 +616,7 @@ F:	drivers/gpio/gpio-eio.c
 F:	drivers/hwmon/eio-hwmon.c
 F:	drivers/i2c/busses/i2c-eio.c
 F:	drivers/mfd/eio_core.c
+F:	drivers/thermal/eio_fan.c
 F:	drivers/thermal/eio_thermal.c
 F:	drivers/video/backlight/eio_bl.c
 F:	drivers/watchdog/eio_wdt.c
diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig
index fc078012a989..a52ec50c8cc5 100644
--- a/drivers/thermal/Kconfig
+++ b/drivers/thermal/Kconfig
@@ -436,6 +436,14 @@ config EIO_THERMAL
 	  the Linux thermal framework. It communicates with the EC through the
 	  EIO MFD core.
 
+config EIO_FAN
+        tristate "Advantech EIO Fan cooling device"
+        depends on MFD_EIO && THERMAL
+        help
+          Fan cooling device for the Advantech EIO. This driver exposes a
+          thermal cooling device with controllable states (e.g. Auto/Manual/PWM).
+          It communicates with the EC through the EIO MFD core.
+
 menu "Mediatek thermal drivers"
 depends on ARCH_MEDIATEK || COMPILE_TEST
 source "drivers/thermal/mediatek/Kconfig"
diff --git a/drivers/thermal/Makefile b/drivers/thermal/Makefile
index 6abce8933e34..872f733b6ad4 100644
--- a/drivers/thermal/Makefile
+++ b/drivers/thermal/Makefile
@@ -55,6 +55,7 @@ obj-$(CONFIG_IMX91_THERMAL)	+= imx91_thermal.o
 obj-$(CONFIG_MAX77620_THERMAL)	+= max77620_thermal.o
 obj-$(CONFIG_QORIQ_THERMAL)	+= qoriq_thermal.o
 obj-$(CONFIG_DA9062_THERMAL)	+= da9062-thermal.o
+obj-$(CONFIG_EIO_FAN)		+= eio_fan.o
 obj-$(CONFIG_EIO_THERMAL)	+= eio_thermal.o
 obj-y				+= intel/
 obj-$(CONFIG_TI_SOC_THERMAL)	+= ti-soc-thermal/
diff --git a/drivers/thermal/eio_fan.c b/drivers/thermal/eio_fan.c
new file mode 100644
index 000000000000..e07b18c2ec44
--- /dev/null
+++ b/drivers/thermal/eio_fan.c
@@ -0,0 +1,478 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * eio_fan
+ * ============
+ * Thermal zone driver for Advantech EIO embedded controller's smart
+ * fan mechanism.
+ *
+ * We create a sysfs 'name' of the zone, point out where the fan is. Such as
+ * CPU0, SYS3, etc.
+ *
+ * The sysfs 'fan_mode' can be one of 'Stop', 'Full', 'Manual' or 'Auto'.
+ * If 'Manual'. You can control fan speed via sysfs 'PWM'.
+ * If it is 'Auto'. It enables the smart fan mechanism as below.
+ *
+ * In EIO chip. The smart fan has 3 trips. When the temperature is:
+ * - Over Temp High(trip0), the Fan runs at the fan PWM High.
+ * - Between Temp Low and Temp High(trip1 - trip0), the fan PWM value slopes
+ *   from PWM Low to PWM High.
+ * - Between Temp Stop and Temp Low(trip2 - trip1), the fan PWM is PWM low.
+ * - Below Temp Stop, the fan stopped.
+ *
+ * (PWM)|
+ *	|
+ * High |............................. ______________
+ * (Max)|			      /:
+ *	|			     / :
+ *	|			    /  :
+ *	|			   /   :
+ *	|			  /    :
+ *	|			 /     :
+ *	|			/      :
+ *	|		       /       :
+ *  Low	|.......... __________/	       :
+ *	|	    |	      :	       :
+ *	|	    |	      :	       :
+ *    0	+===========+---------+--------+-------------
+ *	0	   Stop	     Low      High	(Temp)
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/ctype.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/sysfs.h>
+#include <linux/thermal.h>
+
+#define CMD_FAN_WRITE		0x24
+#define CMD_FAN_READ		0x25
+#define FAN_MAX			0x04
+
+#define CMD_THERM_WRITE		0x10
+#define CMD_THERM_READ		0x11
+#define THERM_MAX		0x04
+#define THERM_MULTI		100
+
+#define CTRL_STATE		0x00
+#define CTRL_TYPE		0x01
+#define CTRL_CTRL		0x02
+#define CTRL_ERROR		0x04
+#define CTRL_VALUE		0x10
+#define CTRL_INVERT		0x11
+#define CTRL_FREQ		0x12
+#define CTRL_THERM_HIGH		0x13
+#define CTRL_THERM_LOW		0x14
+#define CTRL_THERM_STOP		0x15
+#define CTRL_PWM_HIGH		0x16
+#define CTRL_PWM_LOW		0x17
+#define CTRL_THERM_SRC		0x20
+
+#define CTRLMODE_STOP		0x00
+#define CTRLMODE_FULL		0x01
+#define CTRLMODE_MANUAL		0x02
+#define CTRLMODE_AUTO		0x03
+
+#define DUTY_MAX		100
+#define UNIT_PER_TEMP		10
+#define NAME_SIZE		4
+
+#define TRIP_HIGH		0
+#define TRIP_LOW		1
+#define TRIP_STOP		2
+#define TRIP_NUM		3
+
+/* Bitfields inside CTRL_CTRL */
+#define FAN_MODE_MASK           GENMASK(1, 0)
+#define FAN_SCM_BIT             BIT(2)
+#define FAN_FRAME_BIT           BIT(3)
+#define FAN_SRC_MASK            GENMASK(7, 4)
+
+#define FAN_SRC(val)            (((int)(val)) >> 4)
+
+#ifndef DECI_KELVIN_TO_MILLI_CELSIUS
+#define DECI_KELVIN_TO_MILLI_CELSIUS(t)  ((((t) - 2731) * 100))
+#endif
+
+#ifndef MILLI_CELSIUS_TO_DECI_KELVIN
+#define MILLI_CELSIUS_TO_DECI_KELVIN(t)  ((((t) / 100) + 2731))
+#endif
+
+static const u8 pmc_len[CTRL_THERM_SRC + 1] = {
+/*      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f */
+	1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+	1, 1, 4, 2, 2, 2, 1, 1, 2, 2, 2, 0, 0, 0, 0, 0,
+	1,
+};
+
+static const char fan_name[0x20][NAME_SIZE + 1] = {
+	"CPU0", "CPU1", "CPU2", "CPU3", "SYS0", "SYS1", "SYS2", "SYS3",
+	"", "", "", "", "", "", "", "",
+	"", "", "", "", "", "", "", "",
+	"", "", "", "", "OEM0", "OEM1", "OEM2", "OEM3",
+};
+
+struct eio_fan_trip {
+	u8 trip_ctl;
+};
+
+struct eio_fan_dev {
+	struct device *mfd;
+	struct device *dev;
+	u8 id;
+	struct thermal_zone_device *tzd;
+	struct eio_fan_trip trip_priv[TRIP_NUM];
+};
+
+static int pmc_write(struct device *mfd, u8 ctrl, u8 id, void *data)
+{
+	if (ctrl >= ARRAY_SIZE(pmc_len))
+		return -EINVAL;
+
+	struct pmc_op op = {
+		.cmd       = CMD_FAN_WRITE,
+		.control   = ctrl,
+		.device_id = id,
+		.size	   = pmc_len[ctrl],
+		.payload   = (u8 *)data,
+	};
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read(struct device *mfd, u8 ctrl, u8 id, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = CMD_FAN_READ,
+		.control   = ctrl,
+		.device_id = id,
+		.size	   = pmc_len[ctrl],
+		.payload   = (u8 *)data,
+	};
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read_therm(struct device *mfd, u8 ctrl, u8 id, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = CMD_THERM_READ,
+		.control   = ctrl,
+		.device_id = id,
+		.size	   = 2,
+		.payload   = (u8 *)data,
+	};
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int eio_fan_get_temp(struct thermal_zone_device *tzd, int *temp)
+{
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	struct device *mfd = fan->mfd;
+	u8 ch = fan->id;
+	int sensor = 0;
+	u16 val = 0;
+	int ret;
+
+	ret = pmc_read(mfd, CTRL_CTRL, ch, &sensor);
+	if (ret)
+		return ret;
+
+	ret = pmc_read_therm(mfd, CTRL_VALUE, (u8)FAN_SRC(sensor), &val);
+	if (ret)
+		return ret;
+
+	*temp = DECI_KELVIN_TO_MILLI_CELSIUS(val);
+	return 0;
+}
+
+static int eio_fan_set_trip_temp(struct thermal_zone_device *tzd,
+				 const struct thermal_trip *trip, int temp)
+{
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	const struct eio_fan_trip *fan_trip = trip->priv;
+	u8 ctl = CTRL_THERM_HIGH + fan_trip->trip_ctl;
+	u16 val;
+
+	if (temp < 1000)
+		return -EINVAL;
+
+	val = MILLI_CELSIUS_TO_DECI_KELVIN(temp);
+	return pmc_write(fan->mfd, ctl, fan->id, &val);
+}
+
+static bool eio_fan_should_bind(struct thermal_zone_device *tzd,
+				const struct thermal_trip *trip,
+				struct thermal_cooling_device *cdev,
+				struct cooling_spec *spec)
+{
+	struct eio_fan_dev *tz_fan  = thermal_zone_device_priv(tzd);
+	struct eio_fan_dev *cd_fan  = cdev->devdata;
+
+	if (!tz_fan || !cd_fan)
+		return false;
+
+	if (tz_fan->mfd != cd_fan->mfd || tz_fan->id != cd_fan->id)
+		return false;
+
+	return true;
+}
+
+static const struct thermal_zone_device_ops zone_ops = {
+	.get_temp = eio_fan_get_temp,
+	.set_trip_temp = eio_fan_set_trip_temp,
+	.should_bind   = eio_fan_should_bind,
+};
+
+static int eio_fan_get_max_state(struct thermal_cooling_device *cdev,
+				 unsigned long *state)
+{
+	*state = 100;
+	return 0;
+}
+
+static int eio_fan_get_cur_state(struct thermal_cooling_device *cdev,
+				 unsigned long *state)
+{
+	struct eio_fan_dev *fan = cdev->devdata;
+	int fan_mode = 0;
+	u8 duty = 0;
+	int ret = 0;
+
+	*state = 0;
+	ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &fan_mode);
+	if (ret)
+		return ret;
+
+	switch (fan_mode & FAN_MODE_MASK) {
+	case CTRLMODE_STOP:
+		*state = 0;
+		break;
+	case CTRLMODE_FULL:
+		*state = 100;
+		break;
+	case CTRLMODE_AUTO:
+		*state = 0;
+		ret = 0;
+		break;
+	case CTRLMODE_MANUAL:
+		ret = pmc_read(fan->mfd, CTRL_VALUE, fan->id, &duty);
+		if (ret)
+			return ret;
+		duty = (u8)clamp_val(duty, 0, 100);
+		*state = duty;
+		break;
+	default:
+		*state = 0;
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int eio_fan_set_cur_state(struct thermal_cooling_device *cdev,
+				 unsigned long state)
+{
+	struct eio_fan_dev *fan = cdev->devdata;
+	u8 ctrl = 0;
+	u8 duty;
+	int ret;
+
+	ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &ctrl);
+	if (ret)
+		return ret;
+
+	if ((ctrl & FAN_MODE_MASK) != CTRLMODE_MANUAL)
+		return -EOPNOTSUPP;
+
+	duty = (u8)clamp_val(state, 0, 100);
+
+	ret = pmc_write(fan->mfd, CTRL_VALUE, fan->id, &duty);
+
+	return ret;
+}
+
+static const struct thermal_cooling_device_ops cooling_ops = {
+	.get_max_state = eio_fan_get_max_state,
+	.get_cur_state = eio_fan_get_cur_state,
+	.set_cur_state = eio_fan_set_cur_state,
+};
+
+static ssize_t fan_mode_show(struct device *dev, struct device_attribute *attr,
+			     char *buf)
+{
+	static const char * const names[] = { "Stop", "Full", "Manual", "Auto" };
+	struct thermal_zone_device *tzd = dev_get_drvdata(dev);
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	u8 mode = 0;
+
+	int ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &mode);
+
+	if (ret)
+		return ret;
+
+	return sysfs_emit(buf, "%s\n", names[mode & 0x03]);
+}
+
+static ssize_t fan_mode_store(struct device *dev, struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	static const char * const names[] = { "Stop", "Full", "Manual", "Auto" };
+	struct thermal_zone_device *tzd = dev_get_drvdata(dev);
+	struct eio_fan_dev *fan = thermal_zone_device_priv(tzd);
+	u8 ctrl, newc;
+	int mode_idx, ret;
+
+	for (mode_idx = 0; mode_idx < ARRAY_SIZE(names); mode_idx++) {
+		if (strncasecmp(buf, names[mode_idx], strlen(names[mode_idx])))
+			continue;
+
+		ret = pmc_read(fan->mfd, CTRL_CTRL, fan->id, &ctrl);
+		if (ret)
+			return -EIO;
+
+		newc  = ctrl & FAN_SRC_MASK;
+
+		switch (mode_idx) {
+		case CTRLMODE_AUTO:
+			newc |= FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_AUTO;
+			break;
+		case CTRLMODE_MANUAL:
+			newc &= ~FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_MANUAL;
+			break;
+		case CTRLMODE_FULL:
+			newc &= ~FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_FULL;
+			break;
+		case CTRLMODE_STOP:
+		default:
+			newc &= ~FAN_FRAME_BIT;
+			newc &= ~FAN_SCM_BIT;
+			newc |= CTRLMODE_STOP;
+			break;
+		}
+
+		ret = pmc_write(fan->mfd, CTRL_CTRL, fan->id, &newc);
+		return ret ? ret : count;
+	}
+
+	return -EINVAL;
+}
+
+static DEVICE_ATTR_RW(fan_mode);
+
+static int eio_fan_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	unsigned int fan_id;
+	int ret;
+
+	if (!dev_get_drvdata(dev->parent)) {
+		dev_err(dev, "eio_core not present\n");
+		return -ENODEV;
+	}
+
+	for (fan_id = 0; fan_id < FAN_MAX; fan_id++) {
+		u8 state = 0, name = 0;
+		int trip_hi = 0, trip_lo = 0, trip_stop = 0;
+		int pwm_hi = 0, pwm_lo = 0;
+		struct eio_fan_dev *fan;
+		struct thermal_zone_device *tzd;
+		struct thermal_cooling_device *cdev;
+
+		if (pmc_read(dev->parent, CTRL_STATE, fan_id, &state) ||
+		    pmc_read(dev->parent, CTRL_TYPE, fan_id, &name) ||
+		    pmc_read(dev->parent, CTRL_THERM_HIGH, fan_id, &trip_hi) ||
+		    pmc_read(dev->parent, CTRL_THERM_LOW, fan_id, &trip_lo) ||
+		    pmc_read(dev->parent, CTRL_THERM_STOP, fan_id, &trip_stop) ||
+		    pmc_read(dev->parent, CTRL_PWM_HIGH, fan_id, &pwm_hi) ||
+		    pmc_read(dev->parent, CTRL_PWM_LOW, fan_id, &pwm_lo)) {
+			dev_info(dev, "fan%u: pmc read error, skipping\n", fan_id);
+			continue;
+		}
+
+		if (!(state & 0x1)) {
+			dev_info(dev, "fan%u: firmware reports disabled\n", fan_id);
+			continue;
+		}
+
+		if (!fan_name[name][0]) {
+			dev_info(dev, "fan%u: unknown name index %u\n", fan_id, name);
+			continue;
+		}
+
+		fan = devm_kzalloc(dev, sizeof(*fan), GFP_KERNEL);
+		if (!fan)
+			return -ENOMEM;
+
+		fan->mfd = dev->parent;
+		fan->id  = (u8)fan_id;
+
+		fan->trip_priv[TRIP_HIGH].trip_ctl = CTRL_THERM_HIGH;
+		fan->trip_priv[TRIP_LOW].trip_ctl  = CTRL_THERM_LOW;
+		fan->trip_priv[TRIP_STOP].trip_ctl = CTRL_THERM_STOP;
+
+		struct thermal_trip trips[TRIP_NUM] = {
+			[TRIP_HIGH] = {
+				.type = THERMAL_TRIP_ACTIVE,
+				.temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_hi),
+				.flags = THERMAL_TRIP_FLAG_RW_TEMP,
+				.priv = &fan->trip_priv[TRIP_HIGH],
+			},
+			[TRIP_LOW] = {
+				.type = THERMAL_TRIP_ACTIVE,
+				.temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_lo),
+				.flags = THERMAL_TRIP_FLAG_RW_TEMP,
+				.priv = &fan->trip_priv[TRIP_LOW],
+			},
+			[TRIP_STOP] = {
+				.type = THERMAL_TRIP_ACTIVE,
+				.temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_stop),
+				.flags = THERMAL_TRIP_FLAG_RW_TEMP,
+				.priv = &fan->trip_priv[TRIP_STOP],
+			},
+		};
+
+		tzd = thermal_zone_device_register_with_trips(fan_name[name],
+							      trips, TRIP_NUM,
+							      fan,
+							      &zone_ops,
+							      NULL,
+							      0, 0);
+		if (IS_ERR(tzd))
+			return PTR_ERR(tzd);
+
+		cdev = thermal_cooling_device_register(fan_name[name], fan, &cooling_ops);
+		if (IS_ERR(cdev)) {
+			thermal_zone_device_unregister(tzd);
+			dev_err(dev, "fan%u: cdev register failed: %ld\n",
+				fan_id, PTR_ERR(cdev));
+			return PTR_ERR(cdev);
+		}
+
+		dev_set_drvdata(thermal_zone_device(tzd), tzd);
+		ret = device_create_file(thermal_zone_device(tzd), &dev_attr_fan_mode);
+		if (ret)
+			dev_warn(dev, "Error create thermal zone fan_mode sysfs\n");
+	}
+	return 0;
+}
+
+static struct platform_driver eio_fan_driver = {
+	.probe  = eio_fan_probe,
+	.driver = {
+		.name = "eio_fan",
+	},
+};
+
+module_platform_driver(eio_fan_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Fan driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");

-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 7/8] Add Advantech EIO Thermal driver
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
  To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner, Ramiro Oliveira
In-Reply-To: <20260714-upstream-v2-v2-0-76e5e41026db@advantech.com>

This commit adds the driver to control the Advantech EIO Thermal block,
this block is included in the Advantech EIO Embedded Controller.

Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
 MAINTAINERS                   |   1 +
 drivers/thermal/Kconfig       |   9 ++
 drivers/thermal/Makefile      |   1 +
 drivers/thermal/eio_thermal.c | 346 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 357 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 8d35fd7c5599..ded89e70d6cc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -616,6 +616,7 @@ F:	drivers/gpio/gpio-eio.c
 F:	drivers/hwmon/eio-hwmon.c
 F:	drivers/i2c/busses/i2c-eio.c
 F:	drivers/mfd/eio_core.c
+F:	drivers/thermal/eio_thermal.c
 F:	drivers/video/backlight/eio_bl.c
 F:	drivers/watchdog/eio_wdt.c
 F:	include/linux/mfd/eio.h
diff --git a/drivers/thermal/Kconfig b/drivers/thermal/Kconfig
index 810eeccedfba..fc078012a989 100644
--- a/drivers/thermal/Kconfig
+++ b/drivers/thermal/Kconfig
@@ -427,6 +427,15 @@ config DA9062_THERMAL
 	  zone.
 	  Compatible with the DA9062 and DA9061 PMICs.
 
+config EIO_THERMAL
+	tristate "Advantech EIO Thermal zone"
+	depends on MFD_EIO && THERMAL
+	help
+	  Thermal zone support for the Advantech EIO. This driver exposes
+	  temperature readings, trip points and protection enable/disable via
+	  the Linux thermal framework. It communicates with the EC through the
+	  EIO MFD core.
+
 menu "Mediatek thermal drivers"
 depends on ARCH_MEDIATEK || COMPILE_TEST
 source "drivers/thermal/mediatek/Kconfig"
diff --git a/drivers/thermal/Makefile b/drivers/thermal/Makefile
index 3b249195c088..6abce8933e34 100644
--- a/drivers/thermal/Makefile
+++ b/drivers/thermal/Makefile
@@ -55,6 +55,7 @@ obj-$(CONFIG_IMX91_THERMAL)	+= imx91_thermal.o
 obj-$(CONFIG_MAX77620_THERMAL)	+= max77620_thermal.o
 obj-$(CONFIG_QORIQ_THERMAL)	+= qoriq_thermal.o
 obj-$(CONFIG_DA9062_THERMAL)	+= da9062-thermal.o
+obj-$(CONFIG_EIO_THERMAL)	+= eio_thermal.o
 obj-y				+= intel/
 obj-$(CONFIG_TI_SOC_THERMAL)	+= ti-soc-thermal/
 obj-y				+= st/
diff --git a/drivers/thermal/eio_thermal.c b/drivers/thermal/eio_thermal.c
new file mode 100644
index 000000000000..ad923acaf138
--- /dev/null
+++ b/drivers/thermal/eio_thermal.c
@@ -0,0 +1,346 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * eio_thermal
+ * ================
+ * Thermal zone driver for Advantech EIO embedded controller's thermal
+ * protect mechanism.
+ *
+ * In EIO chip. The smart fan has 3 trips. While the temperature:
+ * - Touch Trip0: Shutdown --> Cut off the power.
+ * - Touch Trip1: Poweroff --> Send the power button signal.
+ * - between Trip2 and Trip1: Throttle --> Intermittently hold the CPU.
+ *
+ *			  PowerOff    Shutdown
+ *			      ^	         ^
+ *	      Throttle	      |		 |
+ *		 |	      |	         |
+ *	+--------+------------+----------+---------
+ *	0       trip2	     trip1      trip0  (Temp)
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/slab.h>
+#include <linux/sysfs.h>
+#include <linux/thermal.h>
+
+#define CMD_THERM_WRITE		 0x10
+#define CMD_THERM_READ		 0x11
+#define THERM_NUM		 0x04
+#define UNIT_PER_TEMP		 100
+
+#define CTRL_STATE		 0x00
+#define CTRL_TYPE		 0x01
+#define CTRL_ERROR		 0x04
+#define CTRL_VALUE		 0x10
+#define CTRL_MAX		 0x11
+#define CTRL_MIN		 0x12
+#define CTRL_THROTTLE		 0x20
+#define CTRL_THROTTLE_HI	 0x21
+#define CTRL_THROTTLE_LO	 0x22
+#define CTRL_THROTTLE_DEFAULT	 0x28
+#define CTRL_THROTTLE_HI_DEFAULT 0x29
+#define CTRL_THROTTLE_LO_DEFAULT 0x2A
+#define CTRL_POWEROFF		 0x30
+#define CTRL_POWEROFF_HI	 0x31
+#define CTRL_POWEROFF_LO	 0x32
+#define CTRL_POWEROFF_DEFAULT	 0x38
+#define CTRL_POWEROFF_HI_DEFAULT 0x39
+#define CTRL_POWEROFF_LO_DEFAULT 0x3A
+#define CTRL_SHUTDOWN		 0x40
+#define CTRL_SHUTDOWN_HI	 0x41
+#define CTRL_SHUTDOWN_LO	 0x42
+#define CTRL_SHUTDOWN_DEFAULT	 0x48
+#define CTRL_SHUTDOWN_HI_DEFAULT 0x49
+#define CTRL_SHUTDOWN_LO_DEFAULT 0x4A
+#define CTRL_SB_TSI_STATUS	 0x80
+#define CTRL_SB_TSI_ACCESS	 0x81
+#define CTRL_WARN_STATUS	 0x90
+#define CTRL_WARN_BEEP		 0x91
+#define CTRL_WARN_TEMP		 0x92
+
+#define THERM_ERR_NO		 0x00
+#define THERM_ERR_CHANNEL	 0x01
+#define THERM_ERR_HI		 0x02
+#define THERM_ERR_LO		 0x03
+
+#define NAME_SIZE		 5
+
+#define TRIP_NUM		 3
+#define TRIP_SHUTDOWN		 0
+#define TRIP_POWEROFF		 1
+#define TRIP_THROTTLE		 2
+/* Beep mechanism no stable. Not supported, yet. */
+#define TRIP_BEEP		 3
+
+#define THERMAL_POLLING_DELAY		2000 /* millisecond */
+#define THERMAL_PASSIVE_DELAY		1000
+
+#define DECI_KELVIN_TO_MILLI_CELSIUS(t) (((t) - 2731) * 100)
+#define MILLI_CELSIUS_TO_DECI_KELVIN(t) (((t) / 100) + 2731)
+
+#define THERM_STS_AVAIL           BIT(0)
+#define THERM_STS_THROTTLE_AVAIL  BIT(1)
+#define THERM_STS_POWEROFF_AVAIL  BIT(2)
+#define THERM_STS_SHUTDOWN_AVAIL  BIT(3)
+#define THERM_STS_THROTTLE_EVT    BIT(4)
+#define THERM_STS_POWEROFF_EVT    BIT(5)
+#define THERM_STS_SHUTDOWN_EVT    BIT(6)
+/* BIT(7) reserved */
+#define THERM_STS_THROTTLE_ON     BIT(8)
+#define THERM_STS_POWEROFF_ON     BIT(9)
+#define THERM_STS_SHUTDOWN_ON     BIT(10)
+/* BIT(11) reserved */
+#define THERM_STS_THROTTLE_LOG    BIT(12)
+#define THERM_STS_POWEROFF_LOG    BIT(13)
+#define THERM_STS_SHUTDOWN_LOG    BIT(14)
+
+static u8 pmc_len[] = {
+/*      0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f */
+/* 0 */	2, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 1 */	2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 2 */	1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 3 */	1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 4 */	1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 5 */	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 6 */	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 7 */	0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 8 */	1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+/* 9 */	2, 1, 2,
+};
+
+static char therm_name[0x20][NAME_SIZE + 1] = {
+	"CPU0", "CPU1", "CPU2", "CPU3", "SYS0", "SYS1", "SYS2", "SYS3",
+	"AUX0", "AUX1", "AUX2", "AUX3", "DIMM0", "DIMM1", "DIMM2", "DIMM3",
+	"PCH", "VGA", "", "", "", "", "", "",
+	"", "", "", "", "OEM0", "OEM1", "OEM2", "OEM3",
+};
+
+static const u8 ctrl_map[] = {
+	CTRL_SHUTDOWN, CTRL_POWEROFF, CTRL_THROTTLE
+};
+
+struct eio_thermal_dev {
+	struct device *mfd;
+	struct device *dev;
+	u8 ch;
+	u8 name;
+};
+
+struct eio_trip_dev {
+	struct device *mfd;
+	u8 ch;
+	u8 idx;
+};
+
+static int pmc_write(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+	if (ctrl >= ARRAY_SIZE(pmc_len))
+		return -EINVAL;
+
+	struct pmc_op op = {
+		.cmd       = CMD_THERM_WRITE,
+		.control   = ctrl,
+		.device_id = dev_id,
+		.payload   = (u8 *)data,
+		.size      = pmc_len[ctrl],
+	};
+
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+	if (ctrl >= ARRAY_SIZE(pmc_len))
+		return -EINVAL;
+
+	struct pmc_op op = {
+		.cmd       = CMD_THERM_READ,
+		.control   = ctrl,
+		.device_id = dev_id,
+		.payload   = (u8 *)data,
+		.size      = pmc_len[ctrl],
+	};
+
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int eio_tz_get_temp(struct thermal_zone_device *tzd, int *temp)
+{
+	struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
+	u16 val = 0;
+	int ret;
+
+	ret = pmc_read(eio_thermal->mfd, CTRL_VALUE, eio_thermal->ch, &val);
+	if (ret)
+		return ret;
+
+	*temp = DECI_KELVIN_TO_MILLI_CELSIUS(val);
+	return 0;
+}
+
+static int eio_tz_set_trip_temp(struct thermal_zone_device *tzd,
+				const struct thermal_trip *trip, int temp)
+{
+	struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
+	const u8 ctl = (uintptr_t)trip->priv;
+	u16 val;
+
+	if (temp < 1000)
+		return -EINVAL;
+
+	val = MILLI_CELSIUS_TO_DECI_KELVIN(temp);
+	return pmc_write(eio_thermal->mfd, ctl, eio_thermal->ch, &val);
+}
+
+static int eio_tz_change_mode(struct thermal_zone_device *tzd,
+			      enum thermal_device_mode mode)
+{
+	struct eio_thermal_dev *eio_thermal = thermal_zone_device_priv(tzd);
+	int trip;
+	int ret = 0;
+
+	for (trip = 0; trip < TRIP_NUM; trip++) {
+		ret = pmc_write(eio_thermal->mfd, ctrl_map[trip], eio_thermal->ch, &mode);
+		if (ret)
+			dev_err(eio_thermal->dev, "Error when %s trip num %d\n",
+				mode == THERMAL_DEVICE_ENABLED ? "enabling" : "disabling",
+				trip);
+	}
+
+	return ret;
+}
+
+static struct thermal_zone_device_ops zone_ops = {
+	.get_temp = eio_tz_get_temp,
+	.set_trip_temp = eio_tz_set_trip_temp,
+	.change_mode   = eio_tz_change_mode,
+};
+
+static struct thermal_zone_params zone_params = {
+	.no_hwmon      = true,
+};
+
+static int eio_thermal_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	int ch;
+
+	if (!dev_get_drvdata(dev->parent)) {
+		dev_err(dev, "eio_core not present\n");
+		return -ENODEV;
+	}
+
+	for (ch = 0; ch < THERM_NUM; ch++) {
+		u16 state = 0;
+		u8  name  = 0;
+		u16 hi_shutdown = 0, hi_poweroff = 0, hi_throttle = 0;
+		int t_shutdown = 0, t_poweroff = 0, t_throttle = 0;
+		struct thermal_trip trips[TRIP_NUM];
+		int ntrips = 0;
+		struct eio_thermal_dev *eio_th;
+		struct thermal_zone_device *tzd;
+
+		if (pmc_read(dev->parent, CTRL_STATE, (u8)ch, &state) ||
+		    pmc_read(dev->parent, CTRL_TYPE,  (u8)ch, &name)) {
+			dev_info(dev, "thermal%d: PMC read error\n", ch);
+			continue;
+		}
+
+		if (!(state & THERM_STS_AVAIL) ||
+		    !((state & THERM_STS_THROTTLE_AVAIL) ||
+		      (state & THERM_STS_POWEROFF_AVAIL) ||
+		      (state & THERM_STS_SHUTDOWN_AVAIL))) {
+			dev_info(dev, "thermal%d: firmware not activated\n", ch);
+			continue;
+		}
+
+		if (name >= ARRAY_SIZE(therm_name) || !therm_name[name][0]) {
+			dev_info(dev, "thermal%d: unknown sensor name idx=%u\n", ch, name);
+			continue;
+		}
+
+		/* Throttle starts a 1C increase it */
+		int throttle_temp = MILLI_CELSIUS_TO_DECI_KELVIN(60000);
+
+		pmc_write(dev->parent, CTRL_THROTTLE_HI, (u8)ch, &throttle_temp);
+
+		pmc_read(dev->parent, CTRL_SHUTDOWN_HI, (u8)ch, &hi_shutdown);
+		pmc_read(dev->parent, CTRL_POWEROFF_HI, (u8)ch, &hi_poweroff);
+		pmc_read(dev->parent, CTRL_THROTTLE_HI, (u8)ch, &hi_throttle);
+
+		t_shutdown = DECI_KELVIN_TO_MILLI_CELSIUS(hi_shutdown);
+		t_poweroff = DECI_KELVIN_TO_MILLI_CELSIUS(hi_poweroff);
+		t_throttle = DECI_KELVIN_TO_MILLI_CELSIUS(hi_throttle);
+
+		ntrips = 0;
+		if (hi_shutdown) {
+			trips[ntrips].type = THERMAL_TRIP_CRITICAL;
+			trips[ntrips].temperature = t_shutdown;
+			trips[ntrips].flags = THERMAL_TRIP_FLAG_RW_TEMP;
+			trips[ntrips].priv  = THERMAL_INT_TO_TRIP_PRIV(TRIP_SHUTDOWN),
+			ntrips++;
+		}
+		if (hi_poweroff) {
+			trips[ntrips].type = THERMAL_TRIP_HOT;
+			trips[ntrips].temperature = t_poweroff;
+			trips[ntrips].flags = THERMAL_TRIP_FLAG_RW_TEMP;
+			trips[ntrips].priv  = THERMAL_INT_TO_TRIP_PRIV(TRIP_POWEROFF),
+			ntrips++;
+		}
+		if (hi_throttle) {
+			trips[ntrips].type = THERMAL_TRIP_PASSIVE;
+			trips[ntrips].temperature = t_throttle;
+			trips[ntrips].flags = THERMAL_TRIP_FLAG_RW_TEMP;
+			trips[ntrips].priv  = THERMAL_INT_TO_TRIP_PRIV(TRIP_THROTTLE),
+			ntrips++;
+		}
+		if (!ntrips) {
+			dev_info(dev, "thermal%d: no valid trips\n", ch);
+			continue;
+		}
+
+		eio_th = devm_kzalloc(dev, sizeof(*eio_th), GFP_KERNEL);
+		if (!eio_th)
+			return -ENOMEM;
+		eio_th->ch = (u8)ch;
+		eio_th->mfd = dev->parent;
+		eio_th->dev = dev;
+
+		tzd = thermal_zone_device_register_with_trips(therm_name[name],
+							      trips,
+							      ntrips,
+							      eio_th,
+							      &zone_ops,
+							      &zone_params,
+							      THERMAL_PASSIVE_DELAY,
+							      THERMAL_POLLING_DELAY);
+		if (IS_ERR(tzd))
+			return PTR_ERR(tzd);
+		/* Make sure zones start disabled */
+		thermal_zone_device_disable(tzd);
+
+		dev_info(dev, "%s thermal up (ch=%d)\n", therm_name[name], ch);
+	}
+
+	return 0;
+}
+
+static struct platform_driver eio_thermal_driver = {
+	.probe  = eio_thermal_probe,
+	.driver = {
+		.name = "eio_thermal",
+	},
+};
+module_platform_driver(eio_thermal_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Thermal driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");

-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 6/8] Add Advantech EIO Watchdog driver
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
  To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner, Ramiro Oliveira
In-Reply-To: <20260714-upstream-v2-v2-0-76e5e41026db@advantech.com>

This commit adds the driver to control the Advantech EIO Watchdog block,
this block is included in the Advantech EIO Embedded Controller.

Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
 MAINTAINERS                |   1 +
 drivers/watchdog/Kconfig   |   7 +
 drivers/watchdog/Makefile  |   1 +
 drivers/watchdog/eio_wdt.c | 641 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 650 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index d2d7d815ed44..8d35fd7c5599 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -617,6 +617,7 @@ F:	drivers/hwmon/eio-hwmon.c
 F:	drivers/i2c/busses/i2c-eio.c
 F:	drivers/mfd/eio_core.c
 F:	drivers/video/backlight/eio_bl.c
+F:	drivers/watchdog/eio_wdt.c
 F:	include/linux/mfd/eio.h
 
 ADVANTECH SWBTN DRIVER
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index 9f013d774897..bc0f13f4f79c 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -265,6 +265,13 @@ config DA9062_WATCHDOG
 
 	  This driver can be built as a module. The module name is da9062_wdt.
 
+config EIO_WATCHDOG
+	tristate "Advantech EIO Watchdog"
+	depends on MFD_EIO
+	help
+	  Watchdog timer driver for the Advantech EIO.
+	  If unsure, say N.
+
 config GPIO_WATCHDOG
 	tristate "Watchdog device controlled through GPIO-line"
 	depends on GPIOLIB && (ACPI || OF)
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index cb78e9932eae..eee059cfe1da 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -229,6 +229,7 @@ obj-$(CONFIG_DA9052_WATCHDOG) += da9052_wdt.o
 obj-$(CONFIG_DA9055_WATCHDOG) += da9055_wdt.o
 obj-$(CONFIG_DA9062_WATCHDOG) += da9062_wdt.o
 obj-$(CONFIG_DA9063_WATCHDOG) += da9063_wdt.o
+obj-$(CONFIG_EIO_WATCHDOG) += eio_wdt.o
 obj-$(CONFIG_GPIO_WATCHDOG)	+= gpio_wdt.o
 obj-$(CONFIG_WDAT_WDT) += wdat_wdt.o
 obj-$(CONFIG_WM831X_WATCHDOG) += wm831x_wdt.o
diff --git a/drivers/watchdog/eio_wdt.c b/drivers/watchdog/eio_wdt.c
new file mode 100644
index 000000000000..b3c036a004b3
--- /dev/null
+++ b/drivers/watchdog/eio_wdt.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Advantech EIO Watchdog Driver
+ *
+ * Copyright (C) 2025 Advantech Co., Ltd.
+ */
+
+#include <linux/interrupt.h>
+#include <linux/jiffies.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/reboot.h>
+#include <linux/uaccess.h>
+#include <linux/watchdog.h>
+
+#define WATCHDOG_TIMEOUT	60
+#define WATCHDOG_PRETIMEOUT	10
+
+/* Support Flags */
+#define SUPPORT_AVAILABLE	BIT(0)
+#define SUPPORT_PWRBTN		BIT(3)
+#define SUPPORT_IRQ		BIT(4)
+#define SUPPORT_SCI		BIT(5)
+#define SUPPORT_PIN		BIT(6)
+#define SUPPORT_RESET		BIT(7)
+
+/* PMC registers */
+#define REG_STATUS		0x00
+#define REG_CONTROL		0x02
+#define REG_EVENT		0x10
+#define REG_PWR_EVENT_TIME	0x12
+#define REG_IRQ_EVENT_TIME	0x13
+#define REG_RESET_EVENT_TIME	0x14
+#define REG_PIN_EVENT_TIME	0x15
+#define REG_SCI_EVENT_TIME	0x16
+#define REG_IRQ_NUMBER		0x17
+
+/* PMC command and control */
+#define CMD_WDT_WRITE		0x2A
+#define CMD_WDT_READ		0x2B
+#define CTRL_STOP		0x00
+#define CTRL_START		0x01
+#define CTRL_TRIGGER		0x02
+
+/* I/O register and its flags */
+#define IOREG_UNLOCK		0x87
+#define IOREG_LOCK		0xAA
+#define IOREG_LDN		0x07
+#define IOREG_LDN_PMCIO		0x0F
+#define IOREG_IRQ		0x70
+#define IOREG_WDT_STATUS	0x30
+
+/* Flags */
+#define FLAG_WDT_ENABLED	0x01
+#define FLAG_TRIGGER_IRQ	BIT(4)
+
+/* Mapping event type to supported bit */
+#define EVENT_BIT(type)	BIT(type + 2)
+
+enum event_type {
+	EVENT_NONE,
+	EVENT_PWRBTN,
+	EVENT_IRQ,
+	EVENT_SCI,
+	EVENT_PIN
+};
+
+struct eio_wdt_dev {
+	u32 event_type;
+	u32 support;
+	int irq;
+	unsigned long last_time;
+	struct regmap *iomap;
+	struct device *mfd;
+	struct device *dev;
+	struct watchdog_device wdd;
+	struct eio_dev *core;
+};
+
+static char * const type_strs[] = {
+	"NONE",
+	"PWRBTN",
+	"IRQ",
+	"SCI",
+	"PIN",
+};
+
+static u32 type_regs[] = {
+	REG_RESET_EVENT_TIME,
+	REG_PWR_EVENT_TIME,
+	REG_IRQ_EVENT_TIME,
+	REG_SCI_EVENT_TIME,
+	REG_PIN_EVENT_TIME,
+};
+
+/* Specify the pin triggered on pretimeout or timeout */
+static char *event_type = "NONE";
+module_param(event_type, charp, 0);
+MODULE_PARM_DESC(event_type, "Watchdog timeout event type (NONE, PWRBTN, IRQ, SCI, PIN)");
+
+static int pmc_write(struct device *dev, u8 ctrl, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = CMD_WDT_WRITE,
+		.control   = ctrl,
+		.payload   = data,
+		.size     = (ctrl <= REG_EVENT) ? 1 :
+			    (ctrl >= REG_IRQ_NUMBER) ? 1 : 4,
+	};
+	return eio_core_pmc_operation(dev, &op);
+}
+
+static int pmc_read(struct device *dev, u8 ctrl, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = CMD_WDT_READ,
+		.control   = ctrl,
+		.payload   = data,
+		.size     = (ctrl <= REG_EVENT) ? 1 :
+			    (ctrl >= REG_IRQ_NUMBER) ? 1 : 4,
+	};
+	return eio_core_pmc_operation(dev, &op);
+}
+
+static int wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout)
+{
+	wdd->timeout = timeout;
+	return 0;
+}
+
+static int wdt_set_pretimeout(struct watchdog_device *wdd, unsigned int pretimeout)
+{
+	wdd->pretimeout = pretimeout;
+	return 0;
+}
+
+static int wdt_get_type(struct eio_wdt_dev *eio_wdt)
+{
+	int i;
+
+	for (i = 1; i < ARRAY_SIZE(type_strs); i++) {
+		if (strcasecmp(event_type, type_strs[i]) == 0) {
+			if ((eio_wdt->support & EVENT_BIT(i)) == 0) {
+				dev_err(eio_wdt->dev,
+					"This board doesn't support %s trigger type\n",
+					event_type);
+				return -EINVAL;
+			}
+			eio_wdt->event_type = i;
+			return 0;
+		}
+	}
+	return 0;
+}
+
+static int get_time(struct eio_wdt_dev *eio_wdt, u8 ctrl, u32 *val)
+{
+	int ret;
+
+	ret = pmc_read(eio_wdt->mfd, ctrl, val);
+	if (ret)
+		return ret;
+
+	/* ms to sec */
+	*val /= 1000;
+
+	return 0;
+}
+
+static int set_time(struct eio_wdt_dev *eio_wdt, u8 ctrl, u32 time)
+{
+	/* sec to ms */
+	time *= 1000;
+
+	return pmc_write(eio_wdt->mfd, ctrl, &time);
+}
+
+static int wdt_set_config(struct eio_wdt_dev *eio_wdt)
+{
+	int ret, type;
+	u32 event_time = 0;
+	u32 reset_time = 0;
+
+	if (eio_wdt->event_type > EVENT_PIN)
+		return -EFAULT;
+
+	/* Calculate event time and reset time */
+	if (eio_wdt->wdd.pretimeout && eio_wdt->wdd.timeout) {
+		if (eio_wdt->wdd.timeout < eio_wdt->wdd.pretimeout)
+			return -EINVAL;
+
+		reset_time = eio_wdt->wdd.timeout;
+		event_time = eio_wdt->wdd.timeout - eio_wdt->wdd.pretimeout;
+
+	} else if (eio_wdt->wdd.timeout) {
+		reset_time = eio_wdt->event_type ?	0 : eio_wdt->wdd.timeout;
+		event_time = eio_wdt->event_type ? eio_wdt->wdd.timeout : 0;
+	}
+
+	/* Set reset time */
+	ret = set_time(eio_wdt, REG_RESET_EVENT_TIME, reset_time);
+	if (ret)
+		return ret;
+
+	/* Set every other times */
+	for (type = 1; type < ARRAY_SIZE(type_regs); type++) {
+		ret = set_time(eio_wdt, type_regs[type],
+			       (eio_wdt->event_type == type) ? event_time : 0);
+		if (ret)
+			return ret;
+	}
+
+	dev_dbg(eio_wdt->dev, "Config wdt reset time %u\n", reset_time);
+	dev_dbg(eio_wdt->dev, "Config wdt event time %u\n", event_time);
+	dev_dbg(eio_wdt->dev, "Config wdt event type %s\n",
+		type_strs[eio_wdt->event_type]);
+
+	return 0;
+}
+
+static int wdt_get_config(struct eio_wdt_dev *eio_wdt)
+{
+	int ret, type;
+	u32 event_time = 0, reset_time = 0;
+
+	/* Get Reset Time */
+	ret = get_time(eio_wdt, REG_RESET_EVENT_TIME, &reset_time);
+	if (ret)
+		return ret;
+
+	dev_dbg(eio_wdt->dev, "Timeout H/W default timeout: %u secs\n", reset_time);
+
+	/* Get every other times */
+	for (type = 1; type < ARRAY_SIZE(type_regs); type++) {
+		if ((eio_wdt->support & EVENT_BIT(type)) == 0)
+			continue;
+
+		ret = get_time(eio_wdt, type_regs[type], &event_time);
+		if (ret)
+			return ret;
+
+		if (event_time == 0)
+			continue;
+
+		if (reset_time) {
+			if (reset_time < event_time)
+				continue;
+
+			eio_wdt->wdd.timeout    = reset_time;
+			eio_wdt->wdd.pretimeout = reset_time - event_time;
+
+			dev_dbg(eio_wdt->dev,
+				"Pretimeout H/W enabled with event %s of %u secs\n",
+				type_strs[type], eio_wdt->wdd.pretimeout);
+		} else {
+			eio_wdt->wdd.timeout    = event_time;
+			eio_wdt->wdd.pretimeout = 0;
+		}
+
+		eio_wdt->event_type = type;
+
+		dev_dbg(eio_wdt->dev, "Timeout H/W enabled of %u secs\n",
+			eio_wdt->wdd.timeout);
+		return 0;
+	}
+
+	eio_wdt->event_type         = EVENT_NONE;
+	eio_wdt->wdd.pretimeout     = reset_time ? 0 : WATCHDOG_PRETIMEOUT;
+	eio_wdt->wdd.timeout        = reset_time ? reset_time : WATCHDOG_TIMEOUT;
+
+	return 0;
+}
+
+static int set_ctrl(struct eio_wdt_dev *eio_wdt, u8 ctrl)
+{
+	return pmc_write(eio_wdt->mfd, REG_CONTROL, &ctrl);
+}
+
+static int wdt_start(struct watchdog_device *wdd)
+{
+	struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+	int ret;
+
+	ret = wdt_set_config(eio_wdt);
+	if (ret)
+		return ret;
+
+	ret = set_ctrl(eio_wdt, CTRL_START);
+	if (!ret) {
+		eio_wdt->last_time = jiffies;
+		dev_dbg(eio_wdt->dev, "Watchdog started\n");
+	}
+
+	return ret;
+}
+
+static int wdt_stop(struct watchdog_device *wdd)
+{
+	struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+	int ret;
+
+	dev_dbg(eio_wdt->dev, "Watchdog stopped\n");
+	eio_wdt->last_time = 0;
+
+	ret = set_ctrl(eio_wdt, CTRL_STOP);
+	return ret;
+}
+
+static int wdt_ping(struct watchdog_device *wdd)
+{
+	struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+	int ret;
+
+	dev_dbg(eio_wdt->dev, "Watchdog ping\n");
+
+	ret = set_ctrl(eio_wdt, CTRL_TRIGGER);
+	if (!ret)
+		eio_wdt->last_time = jiffies;
+
+	return ret;
+}
+
+static int wdt_support(struct eio_wdt_dev *eio_wdt)
+{
+	u8 support;
+
+	if (pmc_read(eio_wdt->mfd, REG_STATUS, &support))
+		return -EIO;
+
+	if (!(support & SUPPORT_AVAILABLE))
+		return -ENODEV;
+
+	if ((support & SUPPORT_RESET) != SUPPORT_RESET)
+		return -ENODEV;
+
+	eio_wdt->support = support;
+
+	return 0;
+}
+
+static int wdt_get_irq_io(struct eio_wdt_dev *eio_wdt)
+{
+	int ret  = 0;
+	int idx  = EIO_PNP_INDEX;
+	int data = EIO_PNP_DATA;
+	struct regmap *map = eio_wdt->iomap;
+
+	mutex_lock(&eio_wdt->core->mutex);
+
+	/* Unlock EC IO port */
+	ret |= regmap_write(map, idx, IOREG_UNLOCK);
+	ret |= regmap_write(map, idx, IOREG_UNLOCK);
+
+	/* Select logical device to PMC */
+	ret |= regmap_write(map, idx,  IOREG_LDN);
+	ret |= regmap_write(map, data, IOREG_LDN_PMCIO);
+
+	/* Get IRQ number */
+	ret |= regmap_write(map, idx,  IOREG_IRQ);
+	ret |= regmap_read(map, data, &eio_wdt->irq);
+
+	/* Lock back */
+	ret |= regmap_write(map, idx, IOREG_LOCK);
+
+	mutex_unlock(&eio_wdt->core->mutex);
+
+	return ret ? -EIO : 0;
+}
+
+static int wdt_get_irq_pmc(struct eio_wdt_dev *eio_wdt)
+{
+	return pmc_read(eio_wdt->mfd, REG_IRQ_NUMBER, &eio_wdt->irq);
+}
+
+static int wdt_get_irq(struct eio_wdt_dev *eio_wdt)
+{
+	int ret;
+
+	if (!(eio_wdt->support & BIT(EVENT_IRQ)))
+		return -ENODEV;
+
+	ret = wdt_get_irq_pmc(eio_wdt);
+	if (ret) {
+		dev_err(eio_wdt->dev, "Error get irq by pmc\n");
+		return ret;
+	}
+
+	if (eio_wdt->irq)
+		return 0;
+
+	/* Fallback: get IRQ number from EC IO space */
+	ret = wdt_get_irq_io(eio_wdt);
+	if (ret) {
+		dev_err(eio_wdt->dev, "Error get irq by io\n");
+		return ret;
+	}
+
+	if (!eio_wdt->irq) {
+		dev_err(eio_wdt->dev, "Error IRQ number = 0\n");
+		return -EIO;
+	}
+
+	return 0;
+}
+
+static int wdt_set_irq_io(struct eio_wdt_dev *eio_wdt)
+{
+	int ret  = 0;
+	int idx  = EIO_PNP_INDEX;
+	int data = EIO_PNP_DATA;
+	struct regmap *map = eio_wdt->iomap;
+
+	mutex_lock(&eio_wdt->core->mutex);
+
+	/* Unlock EC IO port */
+	ret = regmap_write(map, idx, IOREG_UNLOCK);
+	if (ret)
+		goto unlock;
+	ret = regmap_write(map, idx, IOREG_UNLOCK);
+	if (ret)
+		goto unlock;
+
+	/* Select logical device to PMC */
+	ret = regmap_write(map, idx, IOREG_LDN);
+	if (ret)
+		goto unlock;
+	ret = regmap_write(map, data, IOREG_LDN_PMCIO);
+	if (ret)
+		goto unlock;
+
+	/* Enable WDT */
+	ret = regmap_write(map, idx, IOREG_WDT_STATUS);
+	if (ret)
+		goto unlock;
+	ret = regmap_write(map, data, FLAG_WDT_ENABLED);
+	if (ret)
+		goto unlock;
+
+	/* Set IRQ number */
+	ret = regmap_write(map, idx, IOREG_IRQ);
+	if (ret)
+		goto unlock;
+	ret = regmap_write(map, data, eio_wdt->irq);
+	if (ret)
+		goto unlock;
+
+	/* Lock back */
+	ret = regmap_write(map, idx, IOREG_LOCK);
+
+unlock:
+	mutex_unlock(&eio_wdt->core->mutex);
+	return ret;
+}
+
+static int wdt_set_irq_pmc(struct eio_wdt_dev *eio_wdt)
+{
+	return pmc_write(eio_wdt->mfd, REG_IRQ_NUMBER, &eio_wdt->irq);
+}
+
+static int wdt_set_irq(struct eio_wdt_dev *eio_wdt)
+{
+	int ret;
+
+	if (!(eio_wdt->support & BIT(EVENT_IRQ)))
+		return -ENODEV;
+
+	ret = wdt_set_irq_io(eio_wdt);
+	if (ret) {
+		dev_err(eio_wdt->dev, "Error set irq by io\n");
+		return ret;
+	}
+
+	ret = wdt_set_irq_pmc(eio_wdt);
+	if (ret) {
+		dev_err(eio_wdt->dev, "Error set irq by pmc\n");
+		return ret;
+	}
+
+	return 0;
+}
+
+static int wdt_get_irq_event(struct eio_wdt_dev *eio_wdt)
+{
+	u8 status;
+
+	if (pmc_read(eio_wdt->mfd, REG_EVENT, &status))
+		return 0;
+
+	return status;
+}
+
+static irqreturn_t wdt_isr(int irq, void *arg)
+{
+	return IRQ_WAKE_THREAD;
+}
+
+static irqreturn_t wdt_threaded_isr(int irq, void *arg)
+{
+	struct eio_wdt_dev *eio_wdt = arg;
+	u8 status = wdt_get_irq_event(eio_wdt) & FLAG_TRIGGER_IRQ;
+
+	if (!status)
+		return IRQ_NONE;
+
+	if (eio_wdt->wdd.pretimeout) {
+		watchdog_notify_pretimeout(&eio_wdt->wdd);
+	} else {
+		dev_crit(eio_wdt->dev, "Watchdog expired, rebooting\n");
+		emergency_restart();
+	}
+
+	return IRQ_HANDLED;
+}
+
+static int query_irq(struct eio_wdt_dev *eio_wdt)
+{
+	int ret = 0;
+
+	ret = wdt_get_irq(eio_wdt);
+	if (ret)
+		return ret;
+
+	dev_dbg(eio_wdt->dev, "IRQ = %d\n", eio_wdt->irq);
+
+	return wdt_set_irq(eio_wdt);
+}
+
+static int wdt_init(struct eio_wdt_dev *eio_wdt)
+{
+	int ret;
+
+	ret = wdt_support(eio_wdt);
+	if (ret)
+		return ret;
+
+	ret = wdt_get_config(eio_wdt);
+	if (ret)
+		return ret;
+
+	ret = wdt_get_type(eio_wdt);
+	if (ret)
+		return ret;
+
+	if (eio_wdt->event_type == EVENT_IRQ)
+		ret = query_irq(eio_wdt);
+
+	return ret;
+}
+
+static const struct watchdog_ops wdt_ops = {
+	.owner		= THIS_MODULE,
+	.start		= wdt_start,
+	.stop		= wdt_stop,
+	.ping		= wdt_ping,
+	.set_timeout	= wdt_set_timeout,
+	.set_pretimeout = wdt_set_pretimeout,
+};
+
+static struct watchdog_info wdinfo = {
+	.identity = KBUILD_MODNAME,
+	.options  = WDIOF_SETTIMEOUT | WDIOF_KEEPALIVEPING |
+		    WDIOF_PRETIMEOUT | WDIOF_MAGICCLOSE,
+};
+
+static int eio_wdt_probe(struct platform_device *pdev)
+{
+	struct device *dev = &pdev->dev;
+	struct eio_wdt_dev *eio_wdt;
+	struct watchdog_device *wdd;
+	int ret = 0;
+
+	eio_wdt = devm_kzalloc(dev, sizeof(*eio_wdt), GFP_KERNEL);
+	if (!eio_wdt)
+		return -ENOMEM;
+
+	eio_wdt->dev = dev;
+	eio_wdt->mfd = dev->parent;
+	eio_wdt->iomap = dev_get_regmap(dev->parent, NULL);
+	if (!eio_wdt->iomap)
+		return dev_err_probe(dev, -ENODEV, "parent regmap missing\n");
+
+	eio_wdt->core = dev_get_drvdata(dev->parent);
+	if (!eio_wdt->core)
+		return dev_err_probe(dev, -ENODEV, "eio_core not present\n");
+
+	ret = wdt_init(eio_wdt);
+	if (ret) {
+		dev_err(dev, "wdt_init fail\n");
+		return -EIO;
+	}
+
+	if (eio_wdt->event_type == EVENT_IRQ) {
+		ret = devm_request_threaded_irq(dev, eio_wdt->irq,
+						wdt_isr, wdt_threaded_isr,
+						IRQF_SHARED | IRQF_ONESHOT, pdev->name,
+						eio_wdt);
+		if (ret) {
+			dev_err_probe(dev, ret, "IRQ %u request fail: Disabled.\n",
+				eio_wdt->irq);
+			return ret;
+		}
+	}
+
+	wdd = &eio_wdt->wdd;
+	wdd->info        = &wdinfo;
+	wdd->ops         = &wdt_ops;
+	wdd->parent      = dev;
+	wdd->min_timeout = 1;
+	wdd->max_timeout = 0x7FFF;
+
+	if (ret) {
+		dev_err(dev, "Init timeout fail\n");
+		return ret;
+	}
+
+	watchdog_stop_on_reboot(&eio_wdt->wdd);
+	watchdog_stop_on_unregister(&eio_wdt->wdd);
+
+	watchdog_set_drvdata(&eio_wdt->wdd, eio_wdt);
+	platform_set_drvdata(pdev, eio_wdt);
+
+	ret = devm_watchdog_register_device(dev, &eio_wdt->wdd);
+	if (ret)
+		dev_err(dev, "Cannot register watchdog device (err: %d)\n", ret);
+
+	return ret;
+}
+
+static struct platform_driver eio_wdt_driver = {
+	.probe  = eio_wdt_probe,
+	.driver = {
+		.name = "eio_wdt",
+	},
+};
+module_platform_driver(eio_wdt_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Watchdog interface for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");

-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 5/8] Add Advantech EIO Backlight driver
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
  To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner, Ramiro Oliveira
In-Reply-To: <20260714-upstream-v2-v2-0-76e5e41026db@advantech.com>

This driver controls the Video Backlight block of the Advantech EIO chip.

Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
 MAINTAINERS                      |   1 +
 drivers/video/backlight/Kconfig  |   6 +
 drivers/video/backlight/Makefile |   1 +
 drivers/video/backlight/eio_bl.c | 243 +++++++++++++++++++++++++++++++++++++++
 4 files changed, 251 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index e228d12a2326..d2d7d815ed44 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -616,6 +616,7 @@ F:	drivers/gpio/gpio-eio.c
 F:	drivers/hwmon/eio-hwmon.c
 F:	drivers/i2c/busses/i2c-eio.c
 F:	drivers/mfd/eio_core.c
+F:	drivers/video/backlight/eio_bl.c
 F:	include/linux/mfd/eio.h
 
 ADVANTECH SWBTN DRIVER
diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig
index 7c66b8840d88..a5559a7db75d 100644
--- a/drivers/video/backlight/Kconfig
+++ b/drivers/video/backlight/Kconfig
@@ -522,6 +522,12 @@ config BACKLIGHT_RAVE_SP
 	help
 	  Support for backlight control on RAVE SP device.
 
+config BACKLIGHT_EIO
+	tristate "Advantech EIO Backlight"
+	depends on MFD_EIO && BACKLIGHT_CLASS_DEVICE
+	help
+	  Backlight driver for Advantech EIO.
+
 config BACKLIGHT_LED
 	tristate "Generic LED based Backlight Driver"
 	depends on LEDS_CLASS && OF
diff --git a/drivers/video/backlight/Makefile b/drivers/video/backlight/Makefile
index 34469711c6cd..724246242526 100644
--- a/drivers/video/backlight/Makefile
+++ b/drivers/video/backlight/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_BACKLIGHT_CGBC)		+= cgbc_bl.o
 obj-$(CONFIG_BACKLIGHT_CLASS_DEVICE)	+= backlight.o
 obj-$(CONFIG_BACKLIGHT_DA903X)		+= da903x_bl.o
 obj-$(CONFIG_BACKLIGHT_DA9052)		+= da9052_bl.o
+obj-$(CONFIG_BACKLIGHT_EIO)		+= eio_bl.o
 obj-$(CONFIG_BACKLIGHT_EP93XX)		+= ep93xx_bl.o
 obj-$(CONFIG_BACKLIGHT_GPIO)		+= gpio_backlight.o
 obj-$(CONFIG_BACKLIGHT_HP680)		+= hp680_bl.o
diff --git a/drivers/video/backlight/eio_bl.c b/drivers/video/backlight/eio_bl.c
new file mode 100644
index 000000000000..c2826400de45
--- /dev/null
+++ b/drivers/video/backlight/eio_bl.c
@@ -0,0 +1,243 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Backlight driver for Advantech EIO Embedded controller.
+ *
+ * Copyright (C) 2025 Advantech Corporation. All rights reserved.
+ */
+
+#include <linux/backlight.h>
+#include <linux/errno.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/uaccess.h>
+
+#define PMC_BL_WRITE		0x20
+#define PMC_BL_READ		0x21
+
+#define BL_CTRL_STATUS		0x00
+#define BL_CTRL_ENABLE		0x12
+#define BL_CTRL_ENABLE_INVERT	0x13
+#define BL_CTRL_DUTY		0x14
+#define BL_CTRL_INVERT		0x15
+#define BL_CTRL_FREQ		0x16
+
+#define BL_MAX			2
+
+#define BL_STATUS_AVAIL		0x01
+#define BL_ENABLE_OFF		0x00
+#define BL_ENABLE_ON		0x01
+#define BL_ENABLE_AUTO		BIT(1)
+
+#define USE_DEFAULT		-1
+#define THERMAL_MAX		100
+
+static uint bri_freq = USE_DEFAULT;
+module_param(bri_freq, uint, 0444);
+MODULE_PARM_DESC(bri_freq, "Setup backlight PWM frequency.\n");
+
+static int bri_invert = USE_DEFAULT;
+module_param(bri_invert, int, 0444);
+MODULE_PARM_DESC(bri_invert, "Setup backlight PWM polarity.\n");
+
+static int bl_power_invert = USE_DEFAULT;
+module_param(bl_power_invert, int, 0444);
+MODULE_PARM_DESC(bl_power_invert, "Setup backlight enable pin polarity.\n");
+
+struct eio_bl_dev {
+	struct device *mfd;
+	u8 id;
+};
+
+static int pmc_write(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = PMC_BL_WRITE,
+		.control   = ctrl,
+		.device_id = dev_id,
+		.payload   = (u8 *)data,
+		.size      = (ctrl == BL_CTRL_FREQ) ? 4 : 1,
+	};
+
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int pmc_read(struct device *mfd, u8 ctrl, u8 dev_id, void *data)
+{
+	struct pmc_op op = {
+		.cmd       = PMC_BL_READ,
+		.control   = ctrl,
+		.device_id = dev_id,
+		.payload   = (u8 *)data,
+		.size      = (ctrl == BL_CTRL_FREQ) ? 4 : 1,
+	};
+
+	return eio_core_pmc_operation(mfd, &op);
+}
+
+static int bl_update_status(struct backlight_device *bl)
+{
+	struct eio_bl_dev *eio_bl = bl_get_data(bl);
+	u8 duty = clamp_val(backlight_get_brightness(bl), 0, THERMAL_MAX);
+	u8 sw = backlight_is_blank(bl);
+	int ret;
+
+	/* Setup PWM duty */
+	ret = pmc_write(eio_bl->mfd, BL_CTRL_DUTY, eio_bl->id, &duty);
+	if (ret)
+		return ret;
+
+	/* Setup backlight enable pin */
+	return pmc_write(eio_bl->mfd, BL_CTRL_ENABLE, eio_bl->id, &sw);
+}
+
+static int bl_get_brightness(struct backlight_device *bl)
+{
+	struct eio_bl_dev *eio_bl = bl_get_data(bl);
+	u8 duty = 0;
+	int ret;
+
+	ret = pmc_read(eio_bl->mfd, BL_CTRL_DUTY, eio_bl->id, &duty);
+
+	if (ret)
+		return ret;
+
+	return duty;
+}
+
+static const struct backlight_ops bl_ops = {
+	.get_brightness = bl_get_brightness,
+	.update_status	= bl_update_status,
+	.options	= BL_CORE_SUSPENDRESUME,
+};
+
+static int bl_init(struct device *dev, int id,
+		   struct backlight_properties *props)
+{
+	int ret;
+	u8 enabled = 0;
+	u8 status = 0;
+
+	/* Check EC-supported backlight */
+	ret = pmc_read(dev, BL_CTRL_STATUS, id, &status);
+	if (ret)
+		return ret;
+
+	if (!(status & BL_STATUS_AVAIL)) {
+		dev_dbg(dev, "eio_bl%d hardware report disabled.\n", id);
+		return -ENODEV;
+	}
+
+	ret = pmc_read(dev, BL_CTRL_DUTY, id, &props->brightness);
+	if (ret)
+		return ret;
+
+	/* Invert PWM */
+	if (bri_invert > USE_DEFAULT) {
+		ret = pmc_write(dev, BL_CTRL_INVERT, id, &bri_invert);
+		if (ret)
+			return ret;
+	}
+
+	ret = pmc_read(dev, BL_CTRL_INVERT, id, &bri_invert);
+	if (ret)
+		return ret;
+
+	if (bri_freq != USE_DEFAULT) {
+		ret = pmc_write(dev, BL_CTRL_FREQ, id, &bri_freq);
+		if (ret)
+			return ret;
+	}
+
+	ret = pmc_read(dev, BL_CTRL_FREQ, id, &bri_freq);
+	if (ret)
+		return ret;
+
+	if (bl_power_invert >= USE_DEFAULT) {
+		ret = pmc_write(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
+		if (ret)
+			return ret;
+	}
+
+	ret = pmc_read(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
+	if (ret)
+		return ret;
+
+	/* Read power state */
+	ret = pmc_read(dev, BL_CTRL_ENABLE, id, &enabled);
+	if (ret)
+		return ret;
+
+	props->power = enabled ? BACKLIGHT_POWER_OFF : BACKLIGHT_POWER_ON;
+
+	return 0;
+}
+
+static int bl_probe(struct platform_device *pdev)
+{
+	u8 id;
+	int ret;
+	struct device *dev = &pdev->dev;
+	struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
+
+	if (!eio_dev) {
+		dev_err(dev, "eio_core not present\n");
+		return -ENODEV;
+	}
+
+	for (id = 0; id < BL_MAX; id++) {
+		char name[32];
+		struct backlight_properties props;
+		struct eio_bl_dev *eio_bl;
+		struct backlight_device *bl;
+
+		memset(&props, 0, sizeof(props));
+		props.type           = BACKLIGHT_RAW;
+		props.max_brightness = THERMAL_MAX;
+		props.power          = BACKLIGHT_POWER_OFF;
+		props.brightness     = props.max_brightness;
+		props.scale	     = BACKLIGHT_SCALE_NON_LINEAR;
+
+		eio_bl = devm_kzalloc(dev, sizeof(*eio_bl), GFP_KERNEL);
+		if (!eio_bl)
+			return -ENOMEM;
+
+		eio_bl->mfd = dev->parent;
+		eio_bl->id  = id;
+
+		ret = bl_init(eio_bl->mfd, id, &props);
+		if (ret) {
+			dev_info(dev, "%d No Backlight %u enabled!\n", ret, id);
+			continue;
+		}
+
+		snprintf(name, sizeof(name), "%s%u", pdev->name, id);
+
+		bl = devm_backlight_device_register(dev, name, dev, eio_bl,
+						    &bl_ops, &props);
+
+		if (IS_ERR(bl)) {
+			ret = PTR_ERR(bl);
+			if (ret == -EPROBE_DEFER)
+				return ret;
+
+			dev_err(dev, "register %s failed: %d\n", name, ret);
+			continue;
+		}
+	}
+	return ret;
+}
+
+static struct platform_driver bl_driver = {
+	.probe  = bl_probe,
+	.driver = {
+		.name = "eio_bl",
+	},
+};
+
+module_platform_driver(bl_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Backlight driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");

-- 
2.43.0


^ permalink raw reply related

* [PATCH v2 4/8] Add Advantech EIO I2C driver
From: Ramiro Oliveira @ 2026-07-14 15:54 UTC (permalink / raw)
  To: Lee Jones, Linus Walleij, Bartosz Golaszewski, Guenter Roeck,
	Andi Shyti, Daniel Thompson, Jingoo Han, Helge Deller,
	Wim Van Sebroeck, Rafael J. Wysocki, Daniel Lezcano, Zhang Rui,
	Lukasz Luba
  Cc: linux-kernel, mfd, linux-gpio, linux-hwmon, linux-i2c, dri-devel,
	linux-fbdev, linux-watchdog, linux-pm, Wenkai Chung,
	Francisco Aragon-Trivino, Hongzhi Wang, Mikhail Tsukerman,
	Thomas Kastner, Ramiro Oliveira
In-Reply-To: <20260714-upstream-v2-v2-0-76e5e41026db@advantech.com>

This commit adds the driver to control the Advantech EIO I2C block, this
block is included in the Advantech EIO MFD.

Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
 MAINTAINERS                  |    1 +
 drivers/i2c/busses/Kconfig   |    6 +
 drivers/i2c/busses/Makefile  |    1 +
 drivers/i2c/busses/i2c-eio.c | 1135 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1143 insertions(+)

diff --git a/MAINTAINERS b/MAINTAINERS
index 6d727d5182e4..e228d12a2326 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -614,6 +614,7 @@ M:	Ramiro Oliveira <ramiro.oliveira@advantech.com>
 S:	Maintained
 F:	drivers/gpio/gpio-eio.c
 F:	drivers/hwmon/eio-hwmon.c
+F:	drivers/i2c/busses/i2c-eio.c
 F:	drivers/mfd/eio_core.c
 F:	include/linux/mfd/eio.h
 
diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig
index d7b89508311f..ba982bb00cf9 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -649,6 +649,12 @@ config I2C_DIGICOLOR
 	  This driver can also be built as a module.  If so, the module
 	  will be called i2c-digicolor.
 
+config I2C_EIO
+	tristate "Advantech EIO I2C bus"
+	depends on MFD_EIO
+	help
+	  Say Y or M to build support for Advantech EIO I2C block.
+
 config I2C_EG20T
 	tristate "Intel EG20T PCH/LAPIS Semicon IOH(ML7213/ML7223/ML7831) I2C"
 	depends on PCI && (X86_32 || MIPS || COMPILE_TEST)
diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile
index 3755c54b3d82..2d8d4f37838c 100644
--- a/drivers/i2c/busses/Makefile
+++ b/drivers/i2c/busses/Makefile
@@ -62,6 +62,7 @@ obj-$(CONFIG_I2C_DESIGNWARE_AMDISP) += i2c-designware-amdisp.o
 obj-$(CONFIG_I2C_DESIGNWARE_PCI)			+= i2c-designware-pci.o
 i2c-designware-pci-y					:= i2c-designware-pcidrv.o
 obj-$(CONFIG_I2C_DIGICOLOR)	+= i2c-digicolor.o
+obj-$(CONFIG_I2C_EIO)		+= i2c-eio.o
 obj-$(CONFIG_I2C_EG20T)		+= i2c-eg20t.o
 obj-$(CONFIG_I2C_EMEV2)		+= i2c-emev2.o
 obj-$(CONFIG_I2C_EXYNOS5)	+= i2c-exynos5.o
diff --git a/drivers/i2c/busses/i2c-eio.c b/drivers/i2c/busses/i2c-eio.c
new file mode 100644
index 000000000000..56a89ca18f32
--- /dev/null
+++ b/drivers/i2c/busses/i2c-eio.c
@@ -0,0 +1,1135 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * I2C and SMBus driver of EIO embedded driver
+ *
+ * Copyright (C) 2025 Advantech Co., Ltd.
+ */
+
+#include <linux/device.h>
+#include <linux/delay.h>
+#include <linux/errno.h>
+#include <linux/i2c.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#define SUPPORTED_COMMON (I2C_FUNC_I2C | \
+			  I2C_FUNC_SMBUS_QUICK | \
+			  I2C_FUNC_SMBUS_BYTE | \
+			  I2C_FUNC_SMBUS_BYTE_DATA | \
+			  I2C_FUNC_SMBUS_WORD_DATA | \
+			  I2C_FUNC_SMBUS_I2C_BLOCK)
+#define SUPPORTED_SMB	(SUPPORTED_COMMON | I2C_FUNC_SMBUS_BLOCK_DATA)
+#define SUPPORTED_I2C	(SUPPORTED_COMMON | I2C_FUNC_10BIT_ADDR)
+
+#define MAX_I2C_SMB		4
+
+#define REG_PNP_INDEX		0x299
+#define REG_PNP_DATA		0x29A
+#define REG_SUB_PNP_INDEX	0x499
+#define REG_SUB_PNP_DATA	0x49A
+#define REG_EXT_MODE_ENTER	0x87
+#define REG_EXT_MODE_EXIT	0xAA
+#define REG_LDN			0x07
+
+#define LDN_I2C0		0x20
+#define LDN_I2C1		0x21
+#define LDN_SMBUS0		0x22
+#define LDN_SMBUS1		0x23
+
+#define REG_BASE_HI		0x60
+#define REG_BASE_LO		0x61
+
+#define I2C_REG_CTRL		0x00
+#define I2C_CTRL_STOP		BIT(1)
+
+#define I2C_REG_STAT		0x01
+#define I2C_STAT_RXREADY	BIT(6)
+#define I2C_STAT_TXDONE		BIT(5)
+#define I2C_STAT_NAK_ERR	BIT(4)
+#define I2C_STAT_ARL_ERR	BIT(3)
+#define I2C_STAT_SLV_STP	BIT(2)
+#define I2C_STAT_BUSY		BIT(1)
+#define I2C_STAT_MST_SLV	BIT(0)
+
+#define I2C_REG_MYADDR		0x02
+#define I2C_REG_ADDR		0x03
+#define I2C_REG_DATA		0x04
+#define I2C_REG_PRESCALE1	0x05
+#define I2C_REG_PRESCALE2	0x06
+
+#define I2C_REG_ECTRL		0x07
+#define I2C_ECTRL_RST		BIT(7)
+
+#define I2C_REG_SEM		0x08
+#define I2C_SEM_INUSE		BIT(1)
+
+#define SMB_REG_HC2		0x0C
+
+#define SMB_REG_HS		0x00
+#define SMB_HS_BUSY		BIT(0)
+#define SMB_HS_FINISH		BIT(1)
+#define SMB_HS_ARL_ERR		BIT(3)
+#define SMB_HS_FAILED		BIT(4)
+#define SMB_HS_RX_READY		BIT(5)
+#define SMB_HS_INUSE		BIT(6)
+#define SMB_HS_TX_DONE		BIT(7)
+
+#define SMB_REG_HS2		0x01
+#define SMB_HS2_HNOTIFY		BIT(0)
+#define SMB_HS2_PEC_ERR		BIT(1)
+#define SMB_HS2_NACK_ERR	BIT(2)
+#define SMB_HS2_ALERT_STS	BIT(3)
+#define SMB_HS2_TO_ERR		BIT(4)
+#define SMB_HS2_SSTOP_STS	BIT(5)
+#define SMB_HS2_STX_REQ		BIT(6)
+#define SMB_HS2_SMODE		BIT(7)
+
+#define SMB_REG_HC		0x02
+#define SMB_HC_I2C_NACKEN	BIT(0)
+#define SMB_HC_KILL		BIT(1)
+#define SMB_HC_CMD_SHIFT	2
+#define SMB_HC_LAST_BYTE	BIT(5)
+#define SMB_HC_START		BIT(6)
+#define SMB_HC_PEC_EN		BIT(7)
+
+#define SMB_REG_HCMD		0x03
+#define SMB_REG_HADDR		0x04
+#define SMB_REG_HD0		0x05
+#define SMB_REG_HD1		0x06
+#define SMB_REG_HBLOCK		0x07
+#define SMB_REG_HPEC		0x08
+#define SMB_REG_SADDR		0x09
+#define SMB_REG_SD0		0x0A
+#define SMB_REG_SD1		0x0B
+
+#define SMB_REG_HC2		0x0C
+#define SMB_HC2_HNOTIFY_DIS	BIT(0)
+#define SMB_HC2_I2C_EN		BIT(1)
+#define SMB_HC2_AAPEC		BIT(2)
+#define SMB_HC2_E32B		BIT(3)
+#define SMB_HC2_SRESET		BIT(7)
+
+#define SMB_REG_HPIN		0x0D
+#define SMB_REG_HC3		0x0E
+#define SMB_REG_HC4		0x0F
+#define SMB_REG_NOTIFY_D0	0x11
+#define SMB_REG_NOTIFY_D1	0x12
+#define SMB_REG_HPRESCALE1	0x13
+#define SMB_REG_HPRESCALE2	0x14
+#define SMB_REG_HEXTRA		0x15
+
+#define I2C_TIMEOUT		(10 * USEC_PER_MSEC)
+#define USE_DEFAULT		-1
+
+#define CHIP_CLK		50000
+#define I2C_SCLH_HIGH		2500
+#define I2C_SCLH_LOW		1000
+#define I2C_SCL_FAST_MODE	0x80
+#define I2C_THRESHOLD_SPEED	100
+#define I2C_THRESHOLD_SCLH	30
+#define I2C_FREQ_MAX		400
+#define I2C_FREQ_MIN		8
+
+enum eio_chan_id {
+	EIO_I2C0 = 0,
+	EIO_I2C1,
+	EIO_SMB0,
+	EIO_SMB1,
+};
+
+struct eio_i2c_dev {
+	struct device *dev;
+	struct device *mfd;
+	struct regmap *regmap;
+	struct mutex pnp_mutex; /* Mutex for PNP acces */
+	struct eio_i2c_chan *chan[MAX_I2C_SMB];
+};
+
+struct eio_i2c_chan {
+	u16 base;
+	enum eio_chan_id id;
+	struct eio_i2c_dev *parent;
+	struct i2c_adapter adap;
+	struct mutex lock; /* Mutex for regmap writes */
+	int freq_override;                  /* kHz or USE_DEFAULT */
+};
+
+static int i2c0_freq = USE_DEFAULT;
+module_param(i2c0_freq, int, 0444);
+MODULE_PARM_DESC(i2c0_freq, "Set EIO's I2C0 freq.\n");
+
+static int i2c1_freq = USE_DEFAULT;
+module_param(i2c1_freq, int, 0444);
+MODULE_PARM_DESC(i2c1_freq, "Set EIO's I2C1 freq.\n");
+
+static int smb0_freq = USE_DEFAULT;
+module_param(smb0_freq, int, 0444);
+MODULE_PARM_DESC(smb0_freq, "Set EIO's SMB0 freq.\n");
+
+static int smb1_freq = USE_DEFAULT;
+module_param(smb1_freq, int, 0444);
+MODULE_PARM_DESC(smb1_freq, "Set EIO's SMB1 freq.\n");
+
+static inline u16 eio_enc_7bit_addr(u16 x)
+{
+	return ((x & 0x07F) << 1);
+}
+
+static inline u16 eio_enc_10bit_addr(u16 x)
+{
+	return ((x & 0xFF) | ((x & 0x0300) << 1) | 0xF000);
+}
+
+static inline bool is_i2c(const struct eio_i2c_chan *i2c_chan)
+{
+	return i2c_chan->id == EIO_I2C0 || i2c_chan->id == EIO_I2C1;
+}
+
+static inline struct device *eio_dev(const struct eio_i2c_chan *i2c_chan)
+{
+	return i2c_chan->parent->dev;
+}
+
+static inline struct regmap *eio_map(const struct eio_i2c_chan *i2c_chan)
+{
+	return i2c_chan->parent->regmap;
+}
+
+static inline int eio_reg_write(struct eio_i2c_chan *i2c_chan,
+				unsigned int reg_off, unsigned int val)
+{
+	return regmap_write(eio_map(i2c_chan), i2c_chan->base + reg_off, val);
+}
+
+static inline int eio_reg_read(const struct eio_i2c_chan *chan,
+			       unsigned int reg, unsigned int *val)
+{
+	int ret;
+
+	ret = regmap_read(chan->parent->regmap, chan->base + reg, val);
+	return ret;
+}
+
+static inline int eio_reg_set_bits(const struct eio_i2c_chan *chan,
+				   unsigned int reg, unsigned int mask)
+{
+	return regmap_update_bits(chan->parent->regmap, reg, mask, mask);
+}
+
+static inline int eio_reg_clear_bits(const struct eio_i2c_chan *chan,
+				     unsigned int reg, unsigned int mask)
+{
+	return regmap_update_bits(chan->parent->regmap, reg, mask, 0);
+}
+
+static inline int eio_reg_or(struct eio_i2c_chan *chan,
+			     unsigned int reg, unsigned int mask)
+{
+	return eio_reg_set_bits(chan, reg, mask);
+}
+
+static inline int eio_reg_and(struct eio_i2c_chan *chan,
+			      unsigned int reg, unsigned int mask)
+{
+	return eio_reg_clear_bits(chan, reg, ~mask);
+}
+
+static inline unsigned int eio_chan_reg(const struct eio_i2c_chan *i2c_chan,
+					unsigned int i2c_reg,
+					unsigned int smb_reg)
+{
+	return is_i2c(i2c_chan) ? i2c_reg : smb_reg;
+}
+
+static inline int eio_trigger_read(struct eio_i2c_chan *i2c_chan, u32 *data)
+{
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_DATA, SMB_REG_HD0);
+
+	return eio_reg_read(i2c_chan, reg, data);
+}
+
+static int wait_busy(struct eio_i2c_chan *i2c_chan)
+{
+	ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
+	unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_BUSY, SMB_HS_BUSY);
+	unsigned int val;
+	int cnt = 0;
+
+	do {
+		fsleep(cnt++);
+
+		if (ktime_after(ktime_get(), time_end)) {
+			dev_err(eio_dev(i2c_chan), "Wait I2C bus busy timeout\n");
+			return -ETIME;
+		}
+
+		if (eio_reg_read(i2c_chan, reg, &val))
+			return -EIO;
+
+	} while (val & target);
+
+	return 0;
+}
+
+static void reset_bus(struct eio_i2c_chan *i2c_chan)
+{
+	ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_ECTRL, SMB_REG_HC2);
+	unsigned int target = eio_chan_reg(i2c_chan, I2C_ECTRL_RST, SMB_HC2_SRESET);
+	unsigned int val = 0;
+	unsigned int cnt = 0;
+
+	dev_dbg(eio_dev(i2c_chan), "i2c[%d] bus reset\n", i2c_chan->id);
+
+	if (is_i2c(i2c_chan))
+		eio_reg_write(i2c_chan, I2C_REG_ECTRL, I2C_ECTRL_RST);
+	else
+		eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_SRESET);
+
+	do {
+		fsleep(cnt++);
+
+		if (ktime_after(ktime_get(), time_end)) {
+			dev_err(eio_dev(i2c_chan), "bus reset timeout\n");
+			return;
+		}
+
+		if (eio_reg_read(i2c_chan, reg, &val))
+			return;
+
+	} while (val & target);
+
+	wait_busy(i2c_chan);
+}
+
+static int wait_bus_free(struct eio_i2c_chan *i2c_chan)
+{
+	ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+	unsigned int val;
+	int cnt = 1;
+
+	/* Wait if channel is resetting */
+	do {
+		fsleep(cnt);
+
+		if (ktime_after(ktime_get(), time_end)) {
+			dev_err(eio_dev(i2c_chan), "Wait bus reset timeout\n");
+			return -ETIME;
+		}
+
+		if (eio_reg_read(i2c_chan,
+				 eio_chan_reg(i2c_chan, I2C_REG_ECTRL, SMB_REG_HC2),
+				 &val))
+			return -EIO;
+
+	} while (val & eio_chan_reg(i2c_chan, I2C_ECTRL_RST, SMB_HC2_SRESET));
+
+	/* Wait INUSE */
+	time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+
+	do {
+		fsleep(cnt);
+
+		if (ktime_after(ktime_get(), time_end)) {
+			dev_err(eio_dev(i2c_chan), "Timeout: I2C bus in use\n");
+			return -ETIME;
+		}
+
+		if (eio_reg_read(i2c_chan,
+				 eio_chan_reg(i2c_chan, I2C_REG_SEM, SMB_REG_HS),
+				 &val))
+			return -EIO;
+
+	} while (val & eio_chan_reg(i2c_chan, I2C_SEM_INUSE, SMB_HS_INUSE));
+
+	return 0;
+}
+
+static int let_stop(struct eio_i2c_chan *i2c_chan)
+{
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_CTRL, SMB_REG_HC);
+	unsigned int target = eio_chan_reg(i2c_chan, I2C_CTRL_STOP, SMB_HC_LAST_BYTE);
+
+	return eio_reg_or(i2c_chan, reg, target);
+}
+
+static int clr_inuse(struct eio_i2c_chan *i2c_chan)
+{
+	if (is_i2c(i2c_chan))
+		return eio_reg_write(i2c_chan, I2C_REG_SEM, I2C_SEM_INUSE);
+
+	return eio_reg_or(i2c_chan, SMB_REG_HS, SMB_HS_INUSE);
+}
+
+static int bus_stop(struct eio_i2c_chan *i2c_chan)
+{
+	ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_CTRL, SMB_REG_HC);
+	unsigned int target = eio_chan_reg(i2c_chan, I2C_CTRL_STOP, SMB_HC_LAST_BYTE);
+	unsigned int val = 0;
+	int cnt = 0;
+
+	/* Set STOP bit */
+	eio_reg_or(i2c_chan, reg, target);
+
+	/* Wait until STOP bit clears */
+	do {
+		fsleep(cnt++);
+
+		if (ktime_after(ktime_get(), time_end))
+			return -ETIME;
+
+		if (eio_reg_read(i2c_chan, reg, &val))
+			return -EIO;
+
+	} while (val & target);
+
+	return 0;
+}
+
+static void switch_i2c_mode(struct eio_i2c_chan *i2c_chan, bool on)
+{
+	u32 tmp;
+
+	if (is_i2c(i2c_chan))
+		return;
+
+	if (eio_reg_read(i2c_chan, SMB_REG_HC2, &tmp))
+		return;
+
+	eio_reg_write(i2c_chan, SMB_REG_HC2,
+		      on ? (tmp | SMB_HC2_I2C_EN | SMB_HC2_SRESET)
+		      : (tmp & ~SMB_HC2_I2C_EN));
+}
+
+static void i2c_clear(struct eio_i2c_chan *i2c_chan)
+{
+	if (is_i2c(i2c_chan)) {
+		eio_reg_write(i2c_chan, I2C_REG_STAT, 0xFF);
+	} else {
+		eio_reg_or(i2c_chan, SMB_REG_HS,  0xA9);
+		eio_reg_or(i2c_chan, SMB_REG_HS2, 0x4C);
+	}
+}
+
+static int wait_write_done(struct eio_i2c_chan *i2c_chan, bool no_ack)
+{
+	ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+	unsigned int val = 0;
+	int cnt = 0;
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
+	unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_TXDONE, SMB_HS_TX_DONE);
+
+	do {
+		fsleep(cnt++);
+		if (ktime_after(ktime_get(), time_end)) {
+			if (is_i2c(i2c_chan)) {
+				eio_reg_or(i2c_chan, I2C_REG_STAT, 0);
+			} else {
+				eio_reg_or(i2c_chan, SMB_REG_HS,  0);
+				eio_reg_or(i2c_chan, SMB_REG_HS2, 0);
+			}
+			dev_err(eio_dev(i2c_chan), "wait write complete timeout %X %X\n",
+				val, target);
+			return -ETIME;
+		}
+		if (eio_reg_read(i2c_chan, reg, &val))
+			return -EIO;
+
+	} while ((val & target) == 0);
+
+	if (no_ack)
+		return 0;
+
+	if (is_i2c(i2c_chan)) {
+		eio_reg_or(i2c_chan, I2C_REG_STAT, 0);
+		return (val & I2C_STAT_NAK_ERR) ? -EIO : 0;
+	}
+
+	eio_reg_or(i2c_chan, SMB_REG_HS, 0);
+	if (eio_reg_read(i2c_chan, SMB_REG_HS2, &val))
+		return -EIO;
+	eio_reg_write(i2c_chan, SMB_REG_HS2, val);
+
+	return (val & SMB_HS2_NACK_ERR) ? -EIO : 0;
+}
+
+static int wait_ready(struct eio_i2c_chan *i2c_chan)
+{
+	int ret;
+
+	ret = wait_bus_free(i2c_chan);
+	if (ret)
+		return ret;
+
+	if (wait_busy(i2c_chan) == 0)
+		return 0;
+
+	reset_bus(i2c_chan);
+
+	return wait_busy(i2c_chan);
+}
+
+static int write_addr(struct eio_i2c_chan *i2c_chan, int addr, bool no_ack)
+{
+	eio_reg_write(i2c_chan, eio_chan_reg(i2c_chan, I2C_REG_ADDR, SMB_REG_HADDR),
+		      addr);
+
+	return wait_write_done(i2c_chan, no_ack);
+}
+
+static int write_data(struct eio_i2c_chan *i2c_chan, int data, bool no_ack)
+{
+	eio_reg_write(i2c_chan, eio_chan_reg(i2c_chan, I2C_REG_DATA, SMB_REG_HD0),
+		      data);
+
+	return wait_write_done(i2c_chan, no_ack);
+}
+
+static int read_data(struct eio_i2c_chan *i2c_chan, u8 *data)
+{
+	unsigned int val = 0, tmp;
+	int cnt = 0;
+	ktime_t time_end = ktime_add_us(ktime_get(), I2C_TIMEOUT);
+	unsigned int stat = eio_chan_reg(i2c_chan, I2C_REG_STAT, SMB_REG_HS);
+	unsigned int target = eio_chan_reg(i2c_chan, I2C_STAT_RXREADY, SMB_HS_RX_READY);
+	unsigned int reg = eio_chan_reg(i2c_chan, I2C_REG_DATA, SMB_REG_HD0);
+
+	do {
+		fsleep(cnt++);
+
+		if (ktime_after(ktime_get(), time_end)) {
+			eio_reg_or(i2c_chan, stat, 0);
+			dev_err(eio_dev(i2c_chan), "read data timeout\n");
+			return -ETIME;
+		}
+
+		if (eio_reg_read(i2c_chan, stat, &val))
+			return -EIO;
+
+	} while ((val & target) != target);
+
+	/* clear status */
+	eio_reg_write(i2c_chan, stat, val);
+
+	/* Must read data after clearing status */
+	if (eio_reg_read(i2c_chan, reg, &tmp))
+		return -EIO;
+	*data = (u8)tmp;
+
+	return 0;
+}
+
+static int set_freq(struct eio_i2c_chan *i2c_chan, int freq)
+{
+	u8 pre1, pre2;
+	u16 speed;
+	unsigned int reg1 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE1, SMB_REG_HPRESCALE1);
+	unsigned int reg2 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE2, SMB_REG_HPRESCALE2);
+
+	dev_dbg(eio_dev(i2c_chan), "set freq: %dkHz\n", freq);
+	if (freq > I2C_FREQ_MAX || freq < I2C_FREQ_MIN) {
+		dev_err(eio_dev(i2c_chan), "Invalid i2c freq: %d\n", freq);
+		return -EINVAL;
+	}
+
+	speed = (freq < I2C_THRESHOLD_SCLH) ? I2C_SCLH_LOW : I2C_SCLH_HIGH;
+
+	pre1 = (u8)(CHIP_CLK / speed);
+	pre2 = (u8)((speed / freq) - 1);
+
+	if (freq > I2C_THRESHOLD_SCLH)
+		pre2 |= I2C_SCL_FAST_MODE;
+
+	eio_reg_write(i2c_chan, reg1, pre1);
+	eio_reg_write(i2c_chan, reg2, pre2);
+
+	return 0;
+}
+
+static int get_freq(struct eio_i2c_chan *i2c_chan, int *freq)
+{
+	int clk;
+	unsigned int pre1 = 0, pre2 = 0;
+	unsigned int reg1 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE1, SMB_REG_HPRESCALE1);
+	unsigned int reg2 = eio_chan_reg(i2c_chan, I2C_REG_PRESCALE2, SMB_REG_HPRESCALE2);
+
+	if (eio_reg_read(i2c_chan, reg1, &pre1))
+		return -EIO;
+	if (eio_reg_read(i2c_chan, reg2, &pre2))
+		return -EIO;
+
+	clk = (pre2 & I2C_SCL_FAST_MODE) ? I2C_SCLH_HIGH : I2C_SCLH_LOW;
+	pre2 &= ~I2C_SCL_FAST_MODE;
+
+	*freq = clk / ((int)pre2 + 1);
+
+	return 0;
+}
+
+static int smb_access(struct eio_i2c_chan *i2c_chan, u8 addr, bool is_read, u8 cmd,
+		      int size, union i2c_smbus_data *data)
+{
+	int i, tmp, ret = 0;
+	unsigned int st1, st2;
+	int len = 0;
+
+	mutex_lock(&i2c_chan->lock);
+
+	ret = wait_ready(i2c_chan);
+	if (ret)
+		goto exit;
+
+	/* Force SMBus mode */
+	switch_i2c_mode(i2c_chan, false);
+
+	addr = eio_enc_7bit_addr(addr) | (is_read ? 1 : 0);
+	eio_reg_write(i2c_chan, SMB_REG_HADDR, addr);
+	eio_reg_write(i2c_chan, SMB_REG_HCMD,  cmd);
+
+	dev_dbg(eio_dev(i2c_chan), "SMB[%d], addr:0x%02X, cmd:0x%02X size=%d\n",
+		i2c_chan->id, addr, cmd, size);
+
+	switch (size) {
+	case I2C_SMBUS_QUICK:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK\n");
+		break;
+
+	case I2C_SMBUS_BYTE:
+		if (!is_read) {
+			dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE\n");
+			eio_reg_write(i2c_chan, SMB_REG_HCMD, cmd);
+		}
+		break;
+
+	case I2C_SMBUS_BYTE_DATA:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE_DATA\n");
+		if (!is_read) {
+			eio_reg_write(i2c_chan, SMB_REG_HD0, data->byte);
+			dev_dbg(eio_dev(i2c_chan), "write %X\n", data->byte);
+		}
+		break;
+
+	case I2C_SMBUS_WORD_DATA:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA\n");
+		if (!is_read) {
+			eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+			eio_reg_write(i2c_chan, SMB_REG_HD1, data->block[1]);
+		}
+		break;
+
+	case I2C_SMBUS_PROC_CALL:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL\n");
+		eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+		eio_reg_write(i2c_chan, SMB_REG_HD1, data->block[1]);
+		break;
+
+	case I2C_SMBUS_BLOCK_DATA:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA\n");
+		if (is_read)
+			break;
+
+		/* Program command type */
+		eio_reg_read(i2c_chan, SMB_REG_HC, (unsigned int *)&tmp);
+		tmp &= ~(0x07 << SMB_HC_CMD_SHIFT);
+		tmp |=  (size << SMB_HC_CMD_SHIFT);
+		eio_reg_write(i2c_chan, SMB_REG_HC, tmp);
+
+		/* Force write for payload stage */
+		eio_reg_write(i2c_chan, SMB_REG_HADDR, addr & ~0x01);
+
+		/* Reset internal buffer index pointer */
+		eio_reg_and(i2c_chan, SMB_REG_HC2, (int)~SMB_HC2_E32B);
+		eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_E32B);
+
+		/* Write length + data */
+		eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+		for (i = 1; i <= data->block[0]; i++)
+			eio_reg_write(i2c_chan, SMB_REG_HBLOCK, data->block[i]);
+		break;
+
+	case I2C_SMBUS_BLOCK_PROC_CALL:
+		/* Set command type field */
+		eio_reg_and(i2c_chan, SMB_REG_HC, (0x07 << SMB_HC_CMD_SHIFT));
+		eio_reg_write(i2c_chan, SMB_REG_HD0, data->block[0]);
+
+		/* Reset buffer index */
+		eio_reg_and(i2c_chan, SMB_REG_HC2, (int)~SMB_HC2_E32B);
+		eio_reg_or(i2c_chan, SMB_REG_HC2, SMB_HC2_E32B);
+
+		for (i = 1; i <= data->block[0]; i++)
+			eio_reg_write(i2c_chan, SMB_REG_HBLOCK, data->block[i]);
+		break;
+
+	default:
+		ret = -EINVAL;
+		goto exit;
+	}
+
+	/* Launch transaction */
+	eio_reg_read(i2c_chan, SMB_REG_HC, (unsigned int *)&tmp);
+	tmp &= ~(0x07 << SMB_HC_CMD_SHIFT);
+	tmp |=  (size << SMB_HC_CMD_SHIFT) | SMB_HC_START;
+	tmp &= ~(SMB_HC_I2C_NACKEN | SMB_HC_KILL | SMB_HC_PEC_EN);
+	eio_reg_write(i2c_chan, SMB_REG_HC, tmp);
+
+	ret = wait_busy(i2c_chan);
+	if (ret)
+		goto exit;
+
+	eio_reg_read(i2c_chan, SMB_REG_HS,  &st1);
+	eio_reg_read(i2c_chan, SMB_REG_HS2, &st2);
+
+	if (st1 & SMB_HS_FAILED) {
+		dev_err(eio_dev(i2c_chan), "HS FAILED\n");
+		ret = -EIO;
+	} else if (st1 & SMB_HS_ARL_ERR) {
+		dev_err(eio_dev(i2c_chan), "ARL FAILED\n");
+		ret = -EIO;
+	} else if (st2 & SMB_HS2_TO_ERR) {
+		dev_err(eio_dev(i2c_chan), "timeout\n");
+		ret = -ETIME;
+	} else if (st2 & SMB_HS2_NACK_ERR) {
+		dev_err(eio_dev(i2c_chan), "NACK err\n");
+		ret = -EIO;
+	} else if (st2 & SMB_HS2_PEC_ERR) {
+		dev_err(eio_dev(i2c_chan), "PEC err\n");
+		ret = -EIO;
+	}
+	if (ret)
+		goto exit;
+
+	switch (size) {
+	case I2C_SMBUS_QUICK:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK\n");
+		break;
+
+	case I2C_SMBUS_BYTE:
+	case I2C_SMBUS_BYTE_DATA:
+		if (is_read) {
+			unsigned int v;
+
+			dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE/I2C_SMBUS_BYTE_DATA\n");
+			eio_reg_read(i2c_chan, SMB_REG_HD0, &v);
+			data->block[0] = (u8)v;
+			dev_dbg(eio_dev(i2c_chan), "read %X\n", data->block[0]);
+		}
+		break;
+
+	case I2C_SMBUS_WORD_DATA: {
+		unsigned int v0, v1;
+
+		if (is_read) {
+			dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA\n");
+			eio_reg_read(i2c_chan, SMB_REG_HD0, &v0);
+			eio_reg_read(i2c_chan, SMB_REG_HD1, &v1);
+			data->block[0] = (u8)v0;
+			data->block[1] = (u8)v1;
+		}
+		break;
+	}
+
+	case I2C_SMBUS_PROC_CALL: {
+		unsigned int v0, v1;
+
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL\n");
+		eio_reg_read(i2c_chan, SMB_REG_HD0, &v0);
+		eio_reg_read(i2c_chan, SMB_REG_HD1, &v1);
+		data->block[0] = (u8)v0;
+		data->block[1] = (u8)v1;
+		break;
+	}
+
+	case I2C_SMBUS_BLOCK_DATA:
+		if (!is_read)
+			break;
+
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA\n");
+		eio_reg_read(i2c_chan, SMB_REG_HD0, (unsigned int *)&len);
+		len = min(len, I2C_SMBUS_BLOCK_MAX);
+		data->block[0] = len;
+
+		for (i = 1; i <= len; i++)
+			eio_reg_read(i2c_chan, SMB_REG_HBLOCK,
+				     (unsigned int *)&data->block[i]);
+		break;
+
+	default:
+		ret = -EINVAL;
+		goto exit;
+	}
+
+exit:
+	/* Clear latched status */
+	eio_reg_write(i2c_chan, SMB_REG_HS,  0xFF);
+	eio_reg_write(i2c_chan, SMB_REG_HS2, 0xFF);
+
+	mutex_unlock(&i2c_chan->lock);
+	return ret;
+}
+
+static int i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int nmsgs)
+{
+	int msg, data;
+	int addr = 0;
+	int dummy;
+	int ret = 0;
+	struct eio_i2c_chan *i2c_chan = i2c_get_adapdata(adap);
+
+	mutex_lock(&i2c_chan->lock);
+
+	ret = wait_ready(i2c_chan);
+	if (ret)
+		goto exit;
+
+	switch_i2c_mode(i2c_chan, true);
+
+	dev_dbg(eio_dev(i2c_chan), "Transmit %d I2C messages\n", nmsgs);
+	for (msg = 0; msg < nmsgs; msg++) {
+		int is_read = msgs[msg].flags & I2C_M_RD;
+		bool no_ack = msgs[msg].flags & I2C_M_IGNORE_NAK;
+
+		dev_dbg(eio_dev(i2c_chan), "message %d len=%d\n", msg, msgs[msg].len);
+
+		if (!msgs[msg].len)
+			let_stop(i2c_chan);
+
+		if (msgs[msg].flags & I2C_M_TEN) {
+			addr = eio_enc_10bit_addr(msgs[msg].addr);
+			addr |= is_read;
+			dev_dbg(eio_dev(i2c_chan), "10-bit addr: %X\n", addr);
+
+			ret = write_addr(i2c_chan, addr >> 8, no_ack);
+			if (!ret)
+				ret = write_data(i2c_chan, addr & 0x7F, no_ack);
+		} else {
+			addr = eio_enc_7bit_addr(msgs[msg].addr);
+			addr |= is_read;
+			dev_dbg(eio_dev(i2c_chan), "7-bit addr: %X\n", addr);
+
+			ret = write_addr(i2c_chan, addr, no_ack);
+		}
+
+		if (ret)
+			goto exit;
+
+		if (!msgs[msg].len)
+			goto exit;
+
+		if (is_read)
+			ret = eio_trigger_read(i2c_chan, (u32 *)&dummy);
+
+		/* Transmit all messages */
+		for (data = 0; data < msgs[msg].len; data++) {
+			if (msgs[msg].flags & I2C_M_RD) {
+				bool last = (msgs[msg].len == data + 1);
+
+				if (last)
+					let_stop(i2c_chan);
+
+				ret = read_data(i2c_chan, &msgs[msg].buf[data]);
+				dev_dbg(eio_dev(i2c_chan), "I2C read[%d] = %x\n",
+					data, msgs[msg].buf[data]);
+
+				/* Don't stop twice */
+				if (last && ret == 0)
+					goto exit;
+			} else {
+				ret = write_data(i2c_chan, msgs[msg].buf[data], no_ack);
+				dev_dbg(eio_dev(i2c_chan), "I2C write[%d] = %x\n",
+					data, msgs[msg].buf[data]);
+			}
+			if (ret)
+				goto exit;
+		}
+	}
+
+	if (!ret)
+		ret = bus_stop(i2c_chan);
+
+	if (!ret)
+		goto exit;
+
+exit:
+	if (ret)
+		reset_bus(i2c_chan);
+
+	i2c_clear(i2c_chan);
+	clr_inuse(i2c_chan);
+
+	mutex_unlock(&i2c_chan->lock);
+	return ret ? ret : nmsgs;
+}
+
+static int smbus_xfer(struct i2c_adapter *adap, u16 addr,
+		      u16 flags, char is_read, u8 cmd,
+		      int size, union i2c_smbus_data *data)
+{
+	int ret;
+	struct eio_i2c_chan *i2c_chan = i2c_get_adapdata(adap);
+	int nmsgs = is_read ? 2 : 1;
+	u8 buf[I2C_SMBUS_BLOCK_MAX + sizeof(u32)] = { cmd, };
+	struct i2c_msg msgs[2] = {
+		{ .addr = addr, .flags = flags & ~I2C_M_RD, .buf = buf + 0 },
+		{ .addr = addr, .flags = flags |  I2C_M_RD, .buf = buf + 1 },
+	};
+
+	/* Non-I2C channels use the SMB engine, except I2C block variants we emulate */
+	if (!is_i2c(i2c_chan) && size != I2C_SMBUS_I2C_BLOCK_DATA)
+		return smb_access(i2c_chan, addr, is_read, cmd, size, data);
+
+	if (data) {
+		buf[0] = cmd;
+		/* FIX: preserve other flags; only toggle I2C_M_RD */
+		msgs[0].flags = is_read ? (flags | I2C_M_RD) : (flags & ~I2C_M_RD);
+		msgs[1].buf   = data->block;
+	}
+
+	switch (size) {
+	case I2C_SMBUS_QUICK:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_QUICK on I2C\n");
+		nmsgs = 1;
+		break;
+
+	case I2C_SMBUS_BYTE:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE on I2C\n");
+		nmsgs = 1;
+		msgs[0].len   = 1;
+		msgs[0].buf   = is_read ? data->block : buf;
+		msgs[0].flags = is_read ? (flags | I2C_M_RD) : (flags & ~I2C_M_RD);
+		break;
+
+	case I2C_SMBUS_BYTE_DATA:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BYTE_DATA on I2C\n");
+		if (!data)
+			return -EINVAL;
+		msgs[0].len = is_read ? 1 : 2;
+		buf[1]      = data->block[0];
+		msgs[1].len = 1;
+		break;
+
+	case I2C_SMBUS_WORD_DATA:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_WORD_DATA on I2C\n");
+		if (!data)
+			return -EINVAL;
+		msgs[0].len = is_read ? 1 : 3;
+		msgs[1].len = 2;
+		buf[1]      = data->block[0];
+		buf[2]      = data->block[1];
+		msgs[1].buf = data->block;
+		break;
+
+	case I2C_SMBUS_I2C_BLOCK_DATA:
+	case I2C_SMBUS_I2C_BLOCK_BROKEN:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_I2C_BLOCK_(DATA/BROKEN) on I2C len=%d\n",
+			data->block[0]);
+		if (!data)
+			return -EINVAL;
+		msgs[0].len = is_read ? 1 : data->block[0] + 1;
+		msgs[1].len = data->block[0];
+		msgs[1].buf = data->block + 1;
+		if (msgs[0].len >= I2C_SMBUS_BLOCK_MAX ||
+		    msgs[1].len >= I2C_SMBUS_BLOCK_MAX)
+			return -EINVAL;
+		if (!is_read)
+			memcpy(buf + 1, data->block + 1, msgs[0].len);
+		break;
+
+	case I2C_SMBUS_PROC_CALL:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_PROC_CALL on I2C\n");
+		if (!data)
+			return -EINVAL;
+		nmsgs = 2;
+		msgs[0].flags = flags & ~I2C_M_RD;
+		msgs[0].len   = 3;
+		buf[1]        = data->block[0];
+		buf[2]        = data->block[1];
+		msgs[1].len   = 2;
+		break;
+
+	case I2C_SMBUS_BLOCK_DATA:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_DATA on I2C not supported\n");
+		return -EINVAL;
+
+	case I2C_SMBUS_BLOCK_PROC_CALL:
+		dev_dbg(eio_dev(i2c_chan), "I2C_SMBUS_BLOCK_PROC_CALL on I2C not supported\n");
+		return -EINVAL;
+
+	default:
+		return -EINVAL;
+	}
+
+	ret = i2c_xfer(adap, msgs, nmsgs);
+	return ret < 0 ? ret : 0;
+}
+
+static int load_i2c(struct device *dev, enum eio_chan_id id,
+		    struct eio_i2c_chan *i2c_chan)
+{
+	u32 base_lo, base_hi, base;
+	int ldn = LDN_I2C0 + id;
+	struct eio_i2c_dev *eio_i2c = i2c_chan->parent;
+	struct regmap *map;
+
+	if (!eio_i2c || !eio_i2c->regmap)
+		return dev_err_probe(dev, -ENODEV, "missing parent/regmap\n");
+
+	map = eio_i2c->regmap;
+
+	/* Read channel I/O base via shared PNP window */
+	mutex_lock(&eio_i2c->pnp_mutex);
+	if (regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_ENTER) ||
+	    regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_ENTER) ||
+	    regmap_write(map, REG_PNP_INDEX, REG_LDN) ||
+	    regmap_write(map, REG_PNP_DATA,  ldn) ||
+	    regmap_write(map, REG_PNP_INDEX, REG_BASE_HI) ||
+	    regmap_read(map,  REG_PNP_DATA,  &base_hi) ||
+	    regmap_write(map, REG_PNP_INDEX, REG_BASE_LO) ||
+	    regmap_read(map,  REG_PNP_DATA,  &base_lo) ||
+	    regmap_write(map, REG_PNP_INDEX, REG_EXT_MODE_EXIT)) {
+		mutex_unlock(&eio_i2c->pnp_mutex);
+		dev_err(dev, "error read/write I2C[%d] IO port\n", id);
+		return -EIO;
+	}
+	mutex_unlock(&eio_i2c->pnp_mutex);
+
+	base = (base_hi << 8) | base_lo;
+	if (base == 0xFFFF || base == 0) {
+		dev_dbg(dev, "i2c[%d] base addr=%#x (not in-use)\n", id, base);
+		return -ENODEV;
+	}
+
+	dev_dbg(dev, "i2c[%d] base addr=%#x\n", id, base);
+
+	/* Bind channel (no per-chan dev) */
+	i2c_chan->base = (u16)base;
+	i2c_chan->id   = id;
+
+	/* Per-channel frequency policy */
+	if (i2c_chan->freq_override != USE_DEFAULT)
+		set_freq(i2c_chan, i2c_chan->freq_override);
+
+	get_freq(i2c_chan, &i2c_chan->freq_override);
+
+	return 0;
+}
+
+static u32 functionality(struct i2c_adapter *adap)
+{
+	struct eio_i2c_chan *i2c_chan = i2c_get_adapdata(adap);
+
+	return is_i2c(i2c_chan) ? SUPPORTED_I2C : SUPPORTED_SMB;
+}
+
+static const struct i2c_algorithm algo = {
+	.smbus_xfer	= smbus_xfer,
+	.master_xfer	= i2c_xfer,
+	.functionality	= functionality,
+};
+
+static int eio_i2c_probe(struct platform_device *pdev)
+{
+	static const char * const names[] = { "i2c0", "i2c1", "smb0", "smb1" };
+	struct device *dev = &pdev->dev;
+	struct eio_i2c_dev *eio_i2c;
+	struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
+	int ret = 0;
+	enum eio_chan_id ch;
+
+	if (!eio_dev) {
+		dev_err(dev, "Error contact eio_core\n");
+		return -ENODEV;
+	}
+
+	eio_i2c = devm_kzalloc(dev, sizeof(*eio_i2c), GFP_KERNEL);
+	if (!eio_i2c)
+		return -ENOMEM;
+
+	eio_i2c->dev = dev;
+	eio_i2c->mfd = dev->parent;
+	eio_i2c->regmap = dev_get_regmap(dev->parent, NULL);
+	if (!eio_i2c->regmap)
+		return dev_err_probe(dev, -ENODEV, "parent regmap not found\n");
+
+	mutex_init(&eio_i2c->pnp_mutex);
+	platform_set_drvdata(pdev, eio_i2c);
+
+	for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
+		struct eio_i2c_chan *i2c_chan;
+
+		i2c_chan = devm_kzalloc(dev, sizeof(*i2c_chan), GFP_KERNEL);
+		if (!i2c_chan) {
+			ret = -ENOMEM;
+			break;
+		}
+
+		i2c_chan->parent = eio_i2c;
+		i2c_chan->freq_override = USE_DEFAULT;
+		mutex_init(&i2c_chan->lock);
+
+		if (load_i2c(dev, ch, i2c_chan)) {
+			dev_info(dev, "No %s%d!\n", (ch < 2) ? "I2C" : "SMBus", ch & 1);
+			continue;
+		}
+
+		i2c_chan->adap.owner      = THIS_MODULE;
+		i2c_chan->adap.class      = I2C_CLASS_HWMON;
+		i2c_chan->adap.algo       = &algo;
+		i2c_chan->adap.dev.parent = dev;
+		snprintf(i2c_chan->adap.name, sizeof(i2c_chan->adap.name), "eio-%s",
+			 names[ch]);
+
+		i2c_set_adapdata(&i2c_chan->adap, i2c_chan);
+
+		ret = i2c_add_adapter(&i2c_chan->adap);
+		dev_info(dev, "Add %s%d %s. %d\n", (ch < 2) ? "I2C" : "SMBus",
+			 ch, ret ? "Error" : "Success", ret);
+		if (ret)
+			break;
+
+		eio_i2c->chan[ch] = i2c_chan;
+	}
+
+	if (ret) {
+		for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
+			if (eio_i2c->chan[ch]) {
+				i2c_del_adapter(&eio_i2c->chan[ch]->adap);
+				eio_i2c->chan[ch] = NULL;
+			}
+		}
+	}
+
+	return ret;
+}
+
+static void eio_i2c_remove(struct platform_device *pdev)
+{
+	struct eio_i2c_dev *eio_i2c = platform_get_drvdata(pdev);
+	enum eio_chan_id ch;
+
+	for (ch = EIO_I2C0; ch < MAX_I2C_SMB; ch++) {
+		if (eio_i2c->chan[ch]) {
+			i2c_del_adapter(&eio_i2c->chan[ch]->adap);
+			eio_i2c->chan[ch] = NULL;
+		}
+	}
+}
+
+static struct platform_driver eio_i2c_driver = {
+	.probe  = eio_i2c_probe,
+	.remove = eio_i2c_remove,
+	.driver = {
+		.name = "i2c_eio",
+	},
+};
+
+module_platform_driver(eio_i2c_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("I2C driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
+MODULE_SOFTDEP("pre: eio_core");

-- 
2.43.0


^ permalink raw reply related


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox