* [PATCH] fbcon: Add check for return value
From: Ваторопин Андрей @ 2025-12-17 9:11 UTC (permalink / raw)
To: Simona Vetter
Cc: Ваторопин Андрей,
Helge Deller, Thomas Zimmermann, Ville Syrjälä,
Sam Ravnborg, Shixiong Ou, Kees Cook, Zsolt Kajtar, Andrew Morton,
Antonino A. Daplas, linux-fbdev@vger.kernel.org,
dri-devel@lists.freedesktop.org, linux-kernel@vger.kernel.org,
lvc-project@linuxtesting.org, stable@vger.kernel.org
From: Andrey Vatoropin <a.vatoropin@crpt.ru>
If fbcon_open() fails when called from con2fb_acquire_newinfo() then
info->fbcon_par pointer remains NULL which is later dereferenced.
Add check for return value of the function con2fb_acquire_newinfo() to
avoid it.
Found by Linux Verification Center (linuxtesting.org) with SVACE.
Fixes: d1baa4ffa677 ("fbcon: set_con2fb_map fixes")
Cc: stable@vger.kernel.org
Signed-off-by: Andrey Vatoropin <a.vatoropin@crpt.ru>
---
drivers/video/fbdev/core/fbcon.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/drivers/video/fbdev/core/fbcon.c b/drivers/video/fbdev/core/fbcon.c
index e7e07eb2142e..7453377f3433 100644
--- a/drivers/video/fbdev/core/fbcon.c
+++ b/drivers/video/fbdev/core/fbcon.c
@@ -1047,7 +1047,8 @@ static void fbcon_init(struct vc_data *vc, bool init)
return;
if (!info->fbcon_par)
- con2fb_acquire_newinfo(vc, info, vc->vc_num);
+ if (con2fb_acquire_newinfo(vc, info, vc->vc_num))
+ return;
/* If we are not the first console on this
fb, copy the font from that console */
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v3 7/9] efi: Refactor init_primary_display() helpers
From: Ard Biesheuvel @ 2025-12-16 13:40 UTC (permalink / raw)
To: Thomas Zimmermann
Cc: javierm, arnd, richard.lyu, helgaas, x86, linux-arm-kernel,
linux-kernel, linux-efi, loongarch, linux-riscv, dri-devel,
linux-hyperv, linux-pci, linux-fbdev
In-Reply-To: <20251126160854.553077-8-tzimmermann@suse.de>
On Wed, 26 Nov 2025 at 17:09, Thomas Zimmermann <tzimmermann@suse.de> wrote:
>
> Rework the kernel's init_primary_display() helpers to allow for later
> support of additional config-table entries and EDID information. No
> functional changes.
>
> Signed-off-by: Thomas Zimmermann <tzimmermann@suse.de>
> ---
> arch/loongarch/kernel/efi.c | 22 +++++++++++-----------
> drivers/firmware/efi/efi-init.c | 19 ++++++++++---------
> 2 files changed, 21 insertions(+), 20 deletions(-)
>
This patch seems unnecessary now that we've replace one table with another.
I've dropped it for now - let me know if you really want to keep it.
> diff --git a/arch/loongarch/kernel/efi.c b/arch/loongarch/kernel/efi.c
> index 638a392d2cd2..1ef38036e8ae 100644
> --- a/arch/loongarch/kernel/efi.c
> +++ b/arch/loongarch/kernel/efi.c
> @@ -81,19 +81,19 @@ EXPORT_SYMBOL_GPL(sysfb_primary_display);
>
> static void __init init_primary_display(void)
> {
> - struct screen_info *si;
> -
> - if (screen_info_table == EFI_INVALID_TABLE_ADDR)
> - return;
> -
> - si = early_memremap(screen_info_table, sizeof(*si));
> - if (!si) {
> - pr_err("Could not map screen_info config table\n");
> + if (screen_info_table == EFI_INVALID_TABLE_ADDR) {
> + struct screen_info *si = early_memremap(screen_info_table, sizeof(*si));
> +
> + if (!si) {
> + pr_err("Could not map screen_info config table\n");
> + return;
> + }
> + sysfb_primary_display.screen = *si;
> + memset(si, 0, sizeof(*si));
> + early_memunmap(si, sizeof(*si));
> + } else {
> return;
> }
> - sysfb_primary_display.screen = *si;
> - memset(si, 0, sizeof(*si));
> - early_memunmap(si, sizeof(*si));
>
> memblock_reserve(__screen_info_lfb_base(&sysfb_primary_display.screen),
> sysfb_primary_display.screen.lfb_size);
> diff --git a/drivers/firmware/efi/efi-init.c b/drivers/firmware/efi/efi-init.c
> index d1d418a34407..ca697d485116 100644
> --- a/drivers/firmware/efi/efi-init.c
> +++ b/drivers/firmware/efi/efi-init.c
> @@ -67,10 +67,9 @@ EXPORT_SYMBOL_GPL(sysfb_primary_display);
>
> static void __init init_primary_display(void)
> {
> - struct screen_info *si;
> -
> if (screen_info_table != EFI_INVALID_TABLE_ADDR) {
> - si = early_memremap(screen_info_table, sizeof(*si));
> + struct screen_info *si = early_memremap(screen_info_table, sizeof(*si));
> +
> if (!si) {
> pr_err("Could not map screen_info config table\n");
> return;
> @@ -78,14 +77,16 @@ static void __init init_primary_display(void)
> sysfb_primary_display.screen = *si;
> memset(si, 0, sizeof(*si));
> early_memunmap(si, sizeof(*si));
> + } else {
> + return;
> + }
>
> - if (memblock_is_map_memory(sysfb_primary_display.screen.lfb_base))
> - memblock_mark_nomap(sysfb_primary_display.screen.lfb_base,
> - sysfb_primary_display.screen.lfb_size);
> + if (memblock_is_map_memory(sysfb_primary_display.screen.lfb_base))
> + memblock_mark_nomap(sysfb_primary_display.screen.lfb_base,
> + sysfb_primary_display.screen.lfb_size);
>
> - if (IS_ENABLED(CONFIG_EFI_EARLYCON))
> - efi_earlycon_reprobe();
> - }
> + if (IS_ENABLED(CONFIG_EFI_EARLYCON))
> + efi_earlycon_reprobe();
> }
>
> static int __init uefi_init(u64 efi_system_table)
> --
> 2.51.1
>
^ permalink raw reply
* Re: [PATCH v3 9/9] efi: libstub: Simplify interfaces for primary_display
From: Ard Biesheuvel @ 2025-12-16 13:23 UTC (permalink / raw)
To: Thomas Zimmermann
Cc: javierm, arnd, richard.lyu, helgaas, x86, linux-arm-kernel,
linux-kernel, linux-efi, loongarch, linux-riscv, dri-devel,
linux-hyperv, linux-pci, linux-fbdev
In-Reply-To: <20251126160854.553077-10-tzimmermann@suse.de>
Hi Thomas
On Wed, 26 Nov 2025 at 17:09, Thomas Zimmermann <tzimmermann@suse.de> wrote:
>
> Rename alloc_primary_display() and __alloc_primary_display(), clarify
> free semantics to make interfaces easier to understand.
>
> Rename alloc_primary_display() to lookup_primary_display() as it
> does not necessarily allocate. Then rename __alloc_primary_display()
> to the new alloc_primary_display(). The helper belongs to
> free_primary_display), so it should be named without underscores.
>
> The lookup helper does not necessarily allocate, so the output
> parameter needs_free to indicate when free should be called.
I don't understand why we need this. Whether or not the helper
allocates is a compile time decision, and in builds where it doesn't,
the free helper doesn't do anything.
I'm all for making things simpler, but I don't think this patch
achieves that tbh.
I've queued up this series now up until this patch - once we converge
on the simplification, I'm happy to apply it on top.
Thanks,
> Pass
> an argument through the calls to track this state. Put the free
> handling into release_primary_display() for simplificy.
>
> Also move the comment fro primary_display.c to efi-stub-entry.c,
> where it now describes lookup_primary_display().
>
> Signed-off-by: Thomas Zimmermann <tzimmermann@suse.de>
> ---
> drivers/firmware/efi/libstub/efi-stub-entry.c | 23 +++++++++++++++++--
> drivers/firmware/efi/libstub/efi-stub.c | 22 ++++++++++++------
> drivers/firmware/efi/libstub/efistub.h | 2 +-
> .../firmware/efi/libstub/primary_display.c | 17 +-------------
> drivers/firmware/efi/libstub/zboot.c | 6 +++--
> 5 files changed, 42 insertions(+), 28 deletions(-)
>
> diff --git a/drivers/firmware/efi/libstub/efi-stub-entry.c b/drivers/firmware/efi/libstub/efi-stub-entry.c
> index aa85e910fe59..3077b51fe0b2 100644
> --- a/drivers/firmware/efi/libstub/efi-stub-entry.c
> +++ b/drivers/firmware/efi/libstub/efi-stub-entry.c
> @@ -14,10 +14,29 @@ static void *kernel_image_addr(void *addr)
> return addr + kernel_image_offset;
> }
>
> -struct sysfb_display_info *alloc_primary_display(void)
> +/*
> + * There are two ways of populating the core kernel's sysfb_primary_display
> + * via the stub:
> + *
> + * - using a configuration table, which relies on the EFI init code to
> + * locate the table and copy the contents; or
> + *
> + * - by linking directly to the core kernel's copy of the global symbol.
> + *
> + * The latter is preferred because it makes the EFIFB earlycon available very
> + * early, but it only works if the EFI stub is part of the core kernel image
> + * itself. The zboot decompressor can only use the configuration table
> + * approach.
> + */
> +
> +struct sysfb_display_info *lookup_primary_display(bool *needs_free)
> {
> + *needs_free = true;
> +
> if (IS_ENABLED(CONFIG_ARM))
> - return __alloc_primary_display();
> + return alloc_primary_display();
> +
> + *needs_free = false;
>
> if (IS_ENABLED(CONFIG_X86) ||
> IS_ENABLED(CONFIG_EFI_EARLYCON) ||
> diff --git a/drivers/firmware/efi/libstub/efi-stub.c b/drivers/firmware/efi/libstub/efi-stub.c
> index 42d6073bcd06..dc545f62c62b 100644
> --- a/drivers/firmware/efi/libstub/efi-stub.c
> +++ b/drivers/firmware/efi/libstub/efi-stub.c
> @@ -51,14 +51,14 @@ static bool flat_va_mapping = (EFI_RT_VIRTUAL_OFFSET != 0);
> void __weak free_primary_display(struct sysfb_display_info *dpy)
> { }
>
> -static struct sysfb_display_info *setup_primary_display(void)
> +static struct sysfb_display_info *setup_primary_display(bool *dpy_needs_free)
> {
> struct sysfb_display_info *dpy;
> struct screen_info *screen = NULL;
> struct edid_info *edid = NULL;
> efi_status_t status;
>
> - dpy = alloc_primary_display();
> + dpy = lookup_primary_display(dpy_needs_free);
> if (!dpy)
> return NULL;
> screen = &dpy->screen;
> @@ -68,15 +68,22 @@ static struct sysfb_display_info *setup_primary_display(void)
>
> status = efi_setup_graphics(screen, edid);
> if (status != EFI_SUCCESS)
> - goto err_free_primary_display;
> + goto err___free_primary_display;
>
> return dpy;
>
> -err_free_primary_display:
> - free_primary_display(dpy);
> +err___free_primary_display:
> + if (*dpy_needs_free)
> + free_primary_display(dpy);
> return NULL;
> }
>
> +static void release_primary_display(struct sysfb_display_info *dpy, bool dpy_needs_free)
> +{
> + if (dpy && dpy_needs_free)
> + free_primary_display(dpy);
> +}
> +
> static void install_memreserve_table(void)
> {
> struct linux_efi_memreserve *rsv;
> @@ -156,13 +163,14 @@ efi_status_t efi_stub_common(efi_handle_t handle,
> char *cmdline_ptr)
> {
> struct sysfb_display_info *dpy;
> + bool dpy_needs_free;
> efi_status_t status;
>
> status = check_platform_features();
> if (status != EFI_SUCCESS)
> return status;
>
> - dpy = setup_primary_display();
> + dpy = setup_primary_display(&dpy_needs_free);
>
> efi_retrieve_eventlog();
>
> @@ -182,7 +190,7 @@ efi_status_t efi_stub_common(efi_handle_t handle,
>
> status = efi_boot_kernel(handle, image, image_addr, cmdline_ptr);
>
> - free_primary_display(dpy);
> + release_primary_display(dpy, dpy_needs_free);
>
> return status;
> }
> diff --git a/drivers/firmware/efi/libstub/efistub.h b/drivers/firmware/efi/libstub/efistub.h
> index 979a21818cc1..1503ffb82903 100644
> --- a/drivers/firmware/efi/libstub/efistub.h
> +++ b/drivers/firmware/efi/libstub/efistub.h
> @@ -1176,8 +1176,8 @@ efi_enable_reset_attack_mitigation(void) { }
>
> void efi_retrieve_eventlog(void);
>
> +struct sysfb_display_info *lookup_primary_display(bool *needs_free);
> struct sysfb_display_info *alloc_primary_display(void);
> -struct sysfb_display_info *__alloc_primary_display(void);
> void free_primary_display(struct sysfb_display_info *dpy);
>
> void efi_cache_sync_image(unsigned long image_base,
> diff --git a/drivers/firmware/efi/libstub/primary_display.c b/drivers/firmware/efi/libstub/primary_display.c
> index cdaebab26514..34c54ac1e02a 100644
> --- a/drivers/firmware/efi/libstub/primary_display.c
> +++ b/drivers/firmware/efi/libstub/primary_display.c
> @@ -7,24 +7,9 @@
>
> #include "efistub.h"
>
> -/*
> - * There are two ways of populating the core kernel's sysfb_primary_display
> - * via the stub:
> - *
> - * - using a configuration table, which relies on the EFI init code to
> - * locate the table and copy the contents; or
> - *
> - * - by linking directly to the core kernel's copy of the global symbol.
> - *
> - * The latter is preferred because it makes the EFIFB earlycon available very
> - * early, but it only works if the EFI stub is part of the core kernel image
> - * itself. The zboot decompressor can only use the configuration table
> - * approach.
> - */
> -
> static efi_guid_t primary_display_guid = LINUX_EFI_PRIMARY_DISPLAY_TABLE_GUID;
>
> -struct sysfb_display_info *__alloc_primary_display(void)
> +struct sysfb_display_info *alloc_primary_display(void)
> {
> struct sysfb_display_info *dpy;
> efi_status_t status;
> diff --git a/drivers/firmware/efi/libstub/zboot.c b/drivers/firmware/efi/libstub/zboot.c
> index 4b76f74c56da..c1fd1fdbcb08 100644
> --- a/drivers/firmware/efi/libstub/zboot.c
> +++ b/drivers/firmware/efi/libstub/zboot.c
> @@ -26,9 +26,11 @@ void __weak efi_cache_sync_image(unsigned long image_base,
> // executable code loaded into memory to be safe for execution.
> }
>
> -struct sysfb_display_info *alloc_primary_display(void)
> +struct sysfb_display_info *lookup_primary_display(bool *needs_free)
> {
> - return __alloc_primary_display();
> + *needs_free = true;
> +
> + return alloc_primary_display();
> }
>
> asmlinkage efi_status_t __efiapi
> --
> 2.51.1
>
^ permalink raw reply
* Re: [PATCH v6 1/4] dt-bindings: backlight: Add max25014 support
From: Maud Spierings @ 2025-12-16 8:19 UTC (permalink / raw)
To: Rob Herring
Cc: Frank Li, Lee Jones, Daniel Thompson, Jingoo Han, Pavel Machek,
Krzysztof Kozlowski, Conor Dooley, Helge Deller, Shawn Guo,
Sascha Hauer, Pengutronix Kernel Team, Fabio Estevam,
Liam Girdwood, Mark Brown, dri-devel, linux-leds, devicetree,
linux-kernel, linux-fbdev, imx, linux-arm-kernel
In-Reply-To: <20251209190722.GA945742-robh@kernel.org>
On 12/9/25 20:07, Rob Herring wrote:
> On Mon, Dec 08, 2025 at 02:56:50PM +0100, Maud Spierings wrote:
>> On 12/2/25 15:53, Frank Li wrote:
>>> On Tue, Dec 02, 2025 at 08:46:21AM +0100, Maud Spierings wrote:
>>>> On 12/1/25 17:52, Frank Li wrote:
>>>>> On Mon, Dec 01, 2025 at 12:53:20PM +0100, Maud Spierings via B4 Relay wrote:
>>>>>> From: Maud Spierings <maudspierings@gocontroll.com>
>>>>>>
>>>>>> The Maxim MAX25014 is a 4-channel automotive grade backlight driver IC
>>>>>> with integrated boost controller.
>>>>>>
>>>>>> Signed-off-by: Maud Spierings <maudspierings@gocontroll.com>
>>>>>>
>>>>>> ---
>>>>>>
>>>>>> In the current implementation the control registers for channel 1,
>>>>>> control all channels. So only one led subnode with led-sources is
>>>>>> supported right now. If at some point the driver functionality is
>>>>>> expanded the bindings can be easily extended with it.
>>>>>> ---
>>>>>> .../bindings/leds/backlight/maxim,max25014.yaml | 107 +++++++++++++++++++++
>>>>>> MAINTAINERS | 5 +
>>>>>> 2 files changed, 112 insertions(+)
>>>>>>
>>>>>> diff --git a/Documentation/devicetree/bindings/leds/backlight/maxim,max25014.yaml b/Documentation/devicetree/bindings/leds/backlight/maxim,max25014.yaml
>>>>>> new file mode 100644
>>>>>> index 000000000000..e83723224b07
>>>>>> --- /dev/null
>>>>>> +++ b/Documentation/devicetree/bindings/leds/backlight/maxim,max25014.yaml
>>>>>> @@ -0,0 +1,107 @@
>>>>>> +# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
>>>>>> +%YAML 1.2
>>>>>> +---
>>>>>> +$id: http://devicetree.org/schemas/leds/backlight/maxim,max25014.yaml#
>>>>>> +$schema: http://devicetree.org/meta-schemas/core.yaml#
>>>>>> +
>>>>>> +title: Maxim max25014 backlight controller
>>>>>> +
>>>>>> +maintainers:
>>>>>> + - Maud Spierings <maudspierings@gocontroll.com>
>>>>>> +
>>>>>> +properties:
>>>>>> + compatible:
>>>>>> + enum:
>>>>>> + - maxim,max25014
>>>>>> +
>>>>>> + reg:
>>>>>> + maxItems: 1
>>>>>> +
>>>>>> + "#address-cells":
>>>>>> + const: 1
>>>>>> +
>>>>>> + "#size-cells":
>>>>>> + const: 0
>>>>>> +
>>>>>> + enable-gpios:
>>>>>> + maxItems: 1
>>>>>> +
>>>>>> + interrupts:
>>>>>> + maxItems: 1
>>>>>> +
>>>>>> + power-supply:
>>>>>> + description: Regulator which controls the boost converter input rail.
>>>>>> +
>>>>>> + pwms:
>>>>>> + maxItems: 1
>>>>>> +
>>>>>> + maxim,iset:
>>>>>> + $ref: /schemas/types.yaml#/definitions/uint32
>>>>>> + maximum: 15
>>>>>> + default: 11
>>>>>> + description:
>>>>>> + Value of the ISET field in the ISET register. This controls the current
>>>>>> + scale of the outputs, a higher number means more current.
>>>>>> +
>>>>>> + led@0:
>>>>>
>>>>> define whole binding, allow 0-3. binding is not related with driver's
>>>>> implement.
>>>>>
>>>>> it'd better put unders leds.
>>>>>
>>>>
>>>> so like:
>>>>
>>>> backlight: backlight@6f {
>>>> compatible = "maxim,max25014";
>>>> reg = <0x6f>;
>>>> enable-gpios = <&gpio1 4 GPIO_ACTIVE_HIGH>;
>>>> pinctrl-names = "default";
>>>> pinctrl-0 = <&pinctrl_backlight>;
>>>> maxim,iset = <7>;
>>>>
>>>> leds {
>>>> #address-cells = <1>;
>>>> #size-cells = <0>;
>>>>
>>>> led@0 {
>>>> reg = <0>;
>>>> led-sources = <0 1 2>;
>>>> default-brightness = <50>;
>>>> };
>>>>
>>>> optional led@#....
>>>> };
>>>> };
>>>>
>>>> right?
>>>
>>> yes.
>>>
>>
>> I am feeling a bit weird about these led sub nodes, because it is not
>> programmed as a led driver, it is programmed as a backlight. I am trying to
>> figure out how this would be used later when the led strings are
>> individually controllable.
>>
>> it isn't possible to link the seperate strings to different displays because
>> it is only one backlight device, so I don't seen any reason why it would
>> ever be used in another way than what it is now, were all strings are
>> programmed by one register.
>>
>> The only way I can make sense of it is if instead I program this device as a
>> led driver and then use the led_bl driver as the actual backlight.
>>
>> Thats a pretty big step in a different direction, but then the led subnodes
>> at least can be properly used I feel.
>
> If you don't have any use for anything other than driving a single
> backlight, then I'd just drop the led nodes completely.
Theoretically with how the registers are laid out, it should be able to
control 4 led strings individually. But as I said when I configure led
string 1 it will also affect all the others seemingly. I am not sure if
with some other configuration you can indeed do individual control.
Before I start converting stuff back to how it was several versions ago.
Frank, do you agree with removing the led nodes in this case? I don't
want to get stuck between two different paths.
Kind regards,
Maud
^ permalink raw reply
* [syzbot] Monthly fbdev report (Dec 2025)
From: syzbot @ 2025-12-16 1:32 UTC (permalink / raw)
To: deller, dri-devel, linux-fbdev, linux-kernel, syzkaller-bugs
Hello fbdev maintainers/developers,
This is a 31-day syzbot report for the fbdev subsystem.
All related reports/information can be found at:
https://syzkaller.appspot.com/upstream/s/fbdev
During the period, 0 new issues were detected and 0 were fixed.
In total, 6 issues are still open and 29 have already been fixed.
Some of the still happening issues:
Ref Crashes Repro Title
<1> 920 Yes KASAN: slab-out-of-bounds Read in fbcon_prepare_logo
https://syzkaller.appspot.com/bug?extid=0c815b25cdb3678e7083
<2> 381 Yes KASAN: vmalloc-out-of-bounds Write in imageblit (6)
https://syzkaller.appspot.com/bug?extid=5a40432dfe8f86ee657a
<3> 134 No KASAN: vmalloc-out-of-bounds Write in fillrect
https://syzkaller.appspot.com/bug?extid=7a63ce155648954e749b
<4> 7 No KASAN: slab-out-of-bounds Read in soft_cursor (2)
https://syzkaller.appspot.com/bug?extid=ae44b38396335bd847cd
---
This report is generated by a bot. It may contain errors.
See https://goo.gl/tpsmEJ for more information about syzbot.
syzbot engineers can be reached at syzkaller@googlegroups.com.
To disable reminders for individual bugs, reply with the following command:
#syz set <Ref> no-reminders
To change bug's subsystems, reply with:
#syz set <Ref> subsystems: new-subsystem
You may send multiple commands in a single email message.
^ permalink raw reply
* [PATCH] fbdev: xilinxfb: request memory region before mapping framebuffer
From: patdiviyam @ 2025-12-15 22:53 UTC (permalink / raw)
To: dri-devel; +Cc: tzimmermann, deller, linux-fbdev, DiviyamPathak
From: DiviyamPathak <patdiviyam@gmail.com>
The xilinxfb driver maps a physical framebuffer address with ioremap()
without first reserving the memory region. This can conflict with other
drivers accessing the same resource.
Request the memory region with devm_request_mem_region() before mapping
the framebuffer and use managed mappings for proper lifetime handling.
This addresses the fbdev TODO about requesting memory regions and avoids
potential resource conflicts.
Signed-off-by: DiviyamPathak <patdiviyam@gmail.com>
---
drivers/video/fbdev/xilinxfb.c | 30 +++++++++++++++++-------------
1 file changed, 17 insertions(+), 13 deletions(-)
diff --git a/drivers/video/fbdev/xilinxfb.c b/drivers/video/fbdev/xilinxfb.c
index 0a6e05cd155a..f18437490de8 100644
--- a/drivers/video/fbdev/xilinxfb.c
+++ b/drivers/video/fbdev/xilinxfb.c
@@ -280,19 +280,27 @@ static int xilinxfb_assign(struct platform_device *pdev,
/* Allocate the framebuffer memory */
if (pdata->fb_phys) {
drvdata->fb_phys = pdata->fb_phys;
- drvdata->fb_virt = ioremap(pdata->fb_phys, fbsize);
+ /* Request the memory region before mapping */
+ if (!devm_request_mem_region(dev, pdata->fb_phys, fbsize,
+ DRIVER_NAME)) {
+ dev_err(dev, "Cannot request framebuffer memory region\n");
+ return -EBUSY;
+ }
+ drvdata->fb_virt = devm_ioremap(dev, pdata->fb_phys, fbsize);
+ if (!drvdata->fb_virt) {
+ dev_err(dev, "Could not map framebuffer memory\n");
+ return -ENOMEM;
+ }
} else {
drvdata->fb_alloced = 1;
drvdata->fb_virt = dma_alloc_coherent(dev, PAGE_ALIGN(fbsize),
- &drvdata->fb_phys,
- GFP_KERNEL);
- }
-
- if (!drvdata->fb_virt) {
- dev_err(dev, "Could not allocate frame buffer memory\n");
- return -ENOMEM;
+ &drvdata->fb_phys,
+ GFP_KERNEL);
+ if (!drvdata->fb_virt) {
+ dev_err(dev, "Could not allocate frame buffer memory\n");
+ return -ENOMEM;
+ }
}
-
/* Clear (turn to black) the framebuffer */
memset_io((void __iomem *)drvdata->fb_virt, 0, fbsize);
@@ -362,8 +370,6 @@ static int xilinxfb_assign(struct platform_device *pdev,
if (drvdata->fb_alloced)
dma_free_coherent(dev, PAGE_ALIGN(fbsize), drvdata->fb_virt,
drvdata->fb_phys);
- else
- iounmap(drvdata->fb_virt);
/* Turn off the display */
xilinx_fb_out32(drvdata, REG_CTRL, 0);
@@ -386,8 +392,6 @@ static void xilinxfb_release(struct device *dev)
if (drvdata->fb_alloced)
dma_free_coherent(dev, PAGE_ALIGN(drvdata->info.fix.smem_len),
drvdata->fb_virt, drvdata->fb_phys);
- else
- iounmap(drvdata->fb_virt);
/* Turn off the display */
xilinx_fb_out32(drvdata, REG_CTRL, 0);
--
2.43.0
^ permalink raw reply related
* Re: [PATCH v2] fbdev: arkfb: Request legacy VGA I/O region
From: Swaraj Gaikwad @ 2025-12-14 11:19 UTC (permalink / raw)
To: sam
Cc: Frank.Li, akpm, david.hunter.linux, deller, donettom, dri-devel,
hverkuil+cisco, kees, konrad.dybcio, linux-fbdev, linux-kernel,
nicolas.dufresne, rongqianfeng, skhan, swarajgaikwad1925,
vivek.kasireddy, ydirson, ziy
In-Reply-To: <20251213223850.GA419250@ravnborg.org>
Hi Sam,
Thanks for the review.
You are right that the cast to (void __iomem *) makes it look like memory,
but the resource is explicitly initialized as I/O ports a few lines earlier:
vga_res.flags = IORESOURCE_IO;
Since the resource flag is IORESOURCE_IO (targeting the legacy VGA ports),
I used devm_request_region() instead of devm_request_mem_region().
Best regards,
Swaraj
^ permalink raw reply
* Re: [PATCH 2/8] Add Advantech EIO GPIO driver
From: Bartosz Golaszewski @ 2025-12-14 0:54 UTC (permalink / raw)
To: Ramiro Oliveira
Cc: linux-kernel, 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: <20251212-upstream-v1-v1-2-d50d40ec8d8a@advantech.com>
On Fri, 12 Dec 2025 17:40:53 +0100, Ramiro Oliveira
<ramiro.oliveira@advantech.com> said:
> This driver controls the GPIO component of the Advantech EIO chip.
>
> Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
> ---
> MAINTAINERS | 1 +
> drivers/gpio/Kconfig | 6 ++
> drivers/gpio/Makefile | 1 +
> drivers/gpio/gpio-eio.c | 273 ++++++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 281 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index bd9279796c2f..359d4a13f212 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -619,6 +619,7 @@ F: drivers/platform/x86/adv_swbutton.c
> ADVANTECH EIO DRIVER
> M: Ramiro Oliveira <ramiro.oliveira@advantech.com>
> S: Maintained
> +F: drivers/gpio/gpio-eio.c
Instead of churning MAINTAINERS in every patch of the series, I suggest you
add a separate patch adding the full entry at the end.
> F: drivers/mfd/eio_core.c
> F: include/linux/mfd/eio.h
>
> diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
> index bd185482a7fd..628a914842bd 100644
> --- a/drivers/gpio/Kconfig
> +++ b/drivers/gpio/Kconfig
> @@ -277,6 +277,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 2421a8fd3733..ba3883d5e4a0 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..50f66a325e8f
> --- /dev/null
> +++ b/drivers/gpio/gpio-eio.c
> @@ -0,0 +1,273 @@
> +// 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.h>
Don't include this, it's a legacy header as per one of the first lines in this
file.
> +#include <linux/gpio/driver.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mfd/eio.h>
> +#include <linux/module.h>
> +
> +#define GPIO_MAX_PINS 48
> +#define GPIO_WRITE 0x18
> +#define GPIO_READ 0x19
> +
> +struct eio_gpio_dev {
> + u64 avail;
> + int max;
> + struct gpio_chip chip;
> + struct device *dev;
> +};
> +
> +struct {
> + int size;
> + bool write;
> +} ctrl_para[] = {
> + { 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 }
> +};
This should be static.
> +
> +enum {
> + GPIO_STATUS = 0,
> + GPIO_GROUP_AVAIL = 3,
> + GPIO_ERROR = 0x04,
> + GPIO_PIN_DIR = 0x10,
> + GPIO_PIN_LEVEL = 0x11,
> + GPIO_GROUP_DIR = 0x12,
> + GPIO_GROUP_LEVEL = 0x13,
> + GPIO_MAPPING = 0x14,
> + GPIO_NAME = 0x15
> +} gpio_ctrl;
Do enum gpio_ctrl {. But also use a common prefix for all symbols in
this driver.
> +
> +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 timeout;
> +module_param(timeout, int, 0444);
> +MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
> +
> +static int pmc_write(struct device *mfd_dev, u8 ctrl, u8 dev_id, void *data)
> +{
> + struct pmc_op op = {
> + .cmd = GPIO_WRITE,
> + .control = ctrl,
> + .device_id = dev_id,
> + .payload = (u8 *)data,
> + .timeout = timeout,
> + };
> +
> + 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 = GPIO_READ,
> + .control = ctrl,
> + .device_id = dev_id,
> + .payload = (u8 *)data,
> + .timeout = timeout,
> + };
> +
> + 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, 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, 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, GPIO_PIN_DIR, offset, &dir);
> +
> + return pmc_write(chip->parent, 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, 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;
> +
> + pmc_write(chip->parent, GPIO_PIN_LEVEL, offset, &val);
> +
> + return 0;
> +}
> +
> +static int check_support(struct device *dev)
> +{
> + u8 data;
> + int ret;
> +
> + ret = pmc_read(dev, 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;
> + u16 data;
> +
> + /* Get pin mapping */
> + ret = pmc_read(dev, 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, 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;
> + int i;
Keep these on the same line.
> + char str[GPIO_MAX_PINS + 1];
> +
> + memset(str, 0x30, sizeof(str));
> +
> + ret = check_support(mfd);
> + if (ret) {
> + dev_err(eio_gpio->dev, "GPIO not supported (%d)\n", ret);
return dev_err_probe()
> + return ret;
> + }
> +
> + eio_gpio->avail = 0;
> +
> + for (i = 0 ; i < GPIO_MAX_PINS ; i++) {
> + ret = check_pin(mfd, i);
> + if (ret)
> + continue;
> +
> + eio_gpio->avail |= BIT(i);
> + eio_gpio->max = i + 1;
> + str[GPIO_MAX_PINS - i] = '1';
> + }
> +
> + dev_info(eio_gpio->dev, "GPIO pins=%s\n", str);
> +
No need to print anything here.
> + return eio_gpio->max ? 0 : -EOPNOTSUPP;
> +}
> +
> +static const struct gpio_chip eio_gpio_chip = {
> + .label = KBUILD_MODNAME,
> + .owner = THIS_MODULE,
> + .direction_input = dir_input,
> + .get = gpio_get,
> + .direction_output = dir_output,
> + .set = gpio_set,
> + .get_direction = get_dir,
> + .base = -1,
> + .can_sleep = true,
> +};
Instead of having an unnecessary copy of the chip in .rodata, just use compound
literals when initiating it in probe().
> +
> +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) {
> + dev_err(dev, "Error contact eio_core\n");
> + return -ENODEV;
> + }
return dev_err_probe()
> +
> + eio_gpio = devm_kzalloc(dev, sizeof(*eio_gpio), GFP_KERNEL);
This can fail, please check the return value.
> + eio_gpio->dev = dev;
> +
> + if (gpio_init(dev->parent, eio_gpio))
> + return -EIO;
> +
> + eio_gpio->chip = eio_gpio_chip;
Don't use tabs like that please. Just stick to single spaces.
> + eio_gpio->chip.parent = dev->parent;
> + eio_gpio->chip.ngpio = eio_gpio->max;
> +
> + 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
>
>
^ permalink raw reply
* Re: [PATCH v2] fbdev: arkfb: Request legacy VGA I/O region
From: Sam Ravnborg @ 2025-12-13 22:38 UTC (permalink / raw)
To: Swaraj Gaikwad
Cc: Helge Deller, Andrew Morton, Hans Verkuil, Nicolas Dufresne,
Frank Li, Zi Yan, Donet Tom, Yann Dirson, Konrad Dybcio,
Vivek Kasireddy, Qianfeng Rong, Kees Cook,
open list:FRAMEBUFFER LAYER, open list:FRAMEBUFFER LAYER,
open list, skhan, david.hunter.linux
In-Reply-To: <20251213202239.8772-1-swarajgaikwad1925@gmail.com>
Hi Swaraj,
On Sat, Dec 13, 2025 at 08:22:34PM +0000, Swaraj Gaikwad wrote:
> The arkfb driver uses the legacy VGA I/O range (0x3c0+) but does not
> request it. This can cause conflicts with other drivers that try to
> reserve these ports.
>
> Fix this by using devm_request_region() during the probe function.
> This ensures the region is properly reserved and automatically released
> on driver detach.
>
> v1: https://lore.kernel.org/lkml/20251213154937.104301-1-swarajgaikwad1925@gmail.com/
> Signed-off-by: Swaraj Gaikwad <swarajgaikwad1925@gmail.com>
> ---
> v2:
> - Use resource_size(&vga_res) instead of hardcoded 64 * 1024.
> - (Feedback from Kees Cook)
>
> Compile-tested only on x86_64.
>
> drivers/video/fbdev/arkfb.c | 6 ++++++
> 1 file changed, 6 insertions(+)
>
> diff --git a/drivers/video/fbdev/arkfb.c b/drivers/video/fbdev/arkfb.c
> index ec084323115f..24e4c20d1a32 100644
> --- a/drivers/video/fbdev/arkfb.c
> +++ b/drivers/video/fbdev/arkfb.c
> @@ -1018,6 +1018,12 @@ static int ark_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
>
> pcibios_bus_to_resource(dev->bus, &vga_res, &bus_reg);
>
> + if (!devm_request_region(&dev->dev, vga_res.start, resource_size(&vga_res), "arkfb-vga")) {
> + dev_err(info->device, "cannot reserve legacy VGA ports\n");
> + rc = -EBUSY;
> + goto err_find_mode;
> + }
> +
> par->state.vgabase = (void __iomem *) (unsigned long) vga_res.start;
Any explanation why devm_request_region() is the right choice here?
As per the line above vga_res.start is iomem, and I had expected to see
devm_request_mem_region() used.
I looked only briefly, so I may be wrong.
Sam
^ permalink raw reply
* [PATCH] fbdev: geode: lxfb: Use devm_request_mem_region
From: Swaraj Gaikwad @ 2025-12-14 1:20 UTC (permalink / raw)
To: Andres Salomon, Helge Deller,
moderated list:AMD GEODE PROCESSOR/CHIPSET SUPPORT,
open list:FRAMEBUFFER LAYER, open list:FRAMEBUFFER LAYER,
open list
Cc: skhan, david.hunter.linux, Swaraj Gaikwad
The lxfb driver currently uses pci_request_region() for memory
reservation, which requires manual error handling and cleanup using
pci_release_region().
Simplify the driver by migrating to the managed helper
devm_request_mem_region(). This ensures that resources are automatically
released on driver detach, allowing the removal of explicit cleanup code
in the probe error path and the remove function.
This addresses the TODO item "Request memory regions in all fbdev
drivers" in Documentation/gpu/todo.rst.
Signed-off-by: Swaraj Gaikwad <swarajgaikwad1925@gmail.com>
---
Compile-tested only on x86_64.
drivers/video/fbdev/geode/lxfb_core.c | 36 +++++++++------------------
1 file changed, 12 insertions(+), 24 deletions(-)
diff --git a/drivers/video/fbdev/geode/lxfb_core.c b/drivers/video/fbdev/geode/lxfb_core.c
index cad99f5b7fe8..8189d6a13c5d 100644
--- a/drivers/video/fbdev/geode/lxfb_core.c
+++ b/drivers/video/fbdev/geode/lxfb_core.c
@@ -335,25 +335,21 @@ static int lxfb_map_video_memory(struct fb_info *info, struct pci_dev *dev)
if (ret)
return ret;
- ret = pci_request_region(dev, 0, "lxfb-framebuffer");
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 0),
+ pci_resource_len(dev, 0), "lxfb-framebuffer"))
+ return -EBUSY;
- if (ret)
- return ret;
-
- ret = pci_request_region(dev, 1, "lxfb-gp");
-
- if (ret)
- return ret;
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 1),
+ pci_resource_len(dev, 1), "lxfb-gp"))
+ return -EBUSY;
- ret = pci_request_region(dev, 2, "lxfb-vg");
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 2),
+ pci_resource_len(dev, 2), "lxfb-vg"))
+ return -EBUSY;
- if (ret)
- return ret;
-
- ret = pci_request_region(dev, 3, "lxfb-vp");
-
- if (ret)
- return ret;
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 3),
+ pci_resource_len(dev, 3), "lxfb-vp"))
+ return -EBUSY;
info->fix.smem_start = pci_resource_start(dev, 0);
info->fix.smem_len = vram ? vram : lx_framebuffer_size();
@@ -546,19 +542,15 @@ static int lxfb_probe(struct pci_dev *pdev, const struct pci_device_id *id)
err:
if (info->screen_base) {
iounmap(info->screen_base);
- pci_release_region(pdev, 0);
}
if (par->gp_regs) {
iounmap(par->gp_regs);
- pci_release_region(pdev, 1);
}
if (par->dc_regs) {
iounmap(par->dc_regs);
- pci_release_region(pdev, 2);
}
if (par->vp_regs) {
iounmap(par->vp_regs);
- pci_release_region(pdev, 3);
}
fb_dealloc_cmap(&info->cmap);
@@ -575,16 +567,12 @@ static void lxfb_remove(struct pci_dev *pdev)
unregister_framebuffer(info);
iounmap(info->screen_base);
- pci_release_region(pdev, 0);
iounmap(par->gp_regs);
- pci_release_region(pdev, 1);
iounmap(par->dc_regs);
- pci_release_region(pdev, 2);
iounmap(par->vp_regs);
- pci_release_region(pdev, 3);
fb_dealloc_cmap(&info->cmap);
framebuffer_release(info);
base-commit: a859eca0e4cc96f63ff125dbe5388d961558b0e9
--
2.52.0
^ permalink raw reply related
* [PATCH] fbdev: geode: gxfb: Use devm_request_mem_region
From: Swaraj Gaikwad @ 2025-12-14 1:13 UTC (permalink / raw)
To: Andres Salomon, Helge Deller,
moderated list:AMD GEODE PROCESSOR/CHIPSET SUPPORT,
open list:FRAMEBUFFER LAYER, open list:FRAMEBUFFER LAYER,
open list
Cc: skhan, david.hunter.linux, Swaraj Gaikwad
The gxfb driver currently uses pci_request_region() for memory
reservation, which requires manual error handling and cleanup using
pci_release_region().
Simplify the driver by migrating to the managed helper
devm_request_mem_region(). This ensures that resources are automatically
released on driver detach, allowing the removal of explicit cleanup code
in the probe error path and the remove function.
This addresses the TODO item "Request memory regions in all fbdev
drivers" in Documentation/gpu/todo.rst.
Signed-off-by: Swaraj Gaikwad <swarajgaikwad1925@gmail.com>
---
Compile-tested only on x86_64.
drivers/video/fbdev/geode/gxfb_core.c | 36 +++++++++++----------------
1 file changed, 15 insertions(+), 21 deletions(-)
diff --git a/drivers/video/fbdev/geode/gxfb_core.c b/drivers/video/fbdev/geode/gxfb_core.c
index 8d69be7c9d31..05af546c8c92 100644
--- a/drivers/video/fbdev/geode/gxfb_core.c
+++ b/drivers/video/fbdev/geode/gxfb_core.c
@@ -223,31 +223,33 @@ static int gxfb_map_video_memory(struct fb_info *info, struct pci_dev *dev)
if (ret < 0)
return ret;
- ret = pci_request_region(dev, 3, "gxfb (video processor)");
- if (ret < 0)
- return ret;
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 3),
+ pci_resource_len(dev, 3), "gxfb (video processor)"))
+ return -EBUSY;
+
par->vid_regs = pci_ioremap_bar(dev, 3);
if (!par->vid_regs)
return -ENOMEM;
- ret = pci_request_region(dev, 2, "gxfb (display controller)");
- if (ret < 0)
- return ret;
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 2),
+ pci_resource_len(dev, 2), "gxfb (display controller)"))
+ return -EBUSY;
+
par->dc_regs = pci_ioremap_bar(dev, 2);
if (!par->dc_regs)
return -ENOMEM;
- ret = pci_request_region(dev, 1, "gxfb (graphics processor)");
- if (ret < 0)
- return ret;
- par->gp_regs = pci_ioremap_bar(dev, 1);
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 1),
+ pci_resource_len(dev, 1), "gxfb (graphics processor)"))
+ return -EBUSY;
+ par->gp_regs = pci_ioremap_bar(dev, 1);
if (!par->gp_regs)
return -ENOMEM;
- ret = pci_request_region(dev, 0, "gxfb (framebuffer)");
- if (ret < 0)
- return ret;
+ if (!devm_request_mem_region(&dev->dev, pci_resource_start(dev, 0),
+ pci_resource_len(dev, 0), "gxfb (framebuffer)"))
+ return -EBUSY;
info->fix.smem_start = pci_resource_start(dev, 0);
info->fix.smem_len = vram ? vram : gx_frame_buffer_size();
@@ -414,19 +416,15 @@ static int gxfb_probe(struct pci_dev *pdev, const struct pci_device_id *id)
err:
if (info->screen_base) {
iounmap(info->screen_base);
- pci_release_region(pdev, 0);
}
if (par->vid_regs) {
iounmap(par->vid_regs);
- pci_release_region(pdev, 3);
}
if (par->dc_regs) {
iounmap(par->dc_regs);
- pci_release_region(pdev, 2);
}
if (par->gp_regs) {
iounmap(par->gp_regs);
- pci_release_region(pdev, 1);
}
fb_dealloc_cmap(&info->cmap);
@@ -442,16 +440,12 @@ static void gxfb_remove(struct pci_dev *pdev)
unregister_framebuffer(info);
iounmap((void __iomem *)info->screen_base);
- pci_release_region(pdev, 0);
iounmap(par->vid_regs);
- pci_release_region(pdev, 3);
iounmap(par->dc_regs);
- pci_release_region(pdev, 2);
iounmap(par->gp_regs);
- pci_release_region(pdev, 1);
fb_dealloc_cmap(&info->cmap);
base-commit: a859eca0e4cc96f63ff125dbe5388d961558b0e9
--
2.52.0
^ permalink raw reply related
* Re: [PATCH 8/8] Add Advantech EIO Fan driver
From: kernel test robot @ 2025-12-13 17:33 UTC (permalink / raw)
To: Ramiro Oliveira, 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: oe-kbuild-all, linux-kernel, 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: <20251212-upstream-v1-v1-8-d50d40ec8d8a@advantech.com>
Hi Ramiro,
kernel test robot noticed the following build warnings:
[auto build test WARNING on d9771d0dbe18dd643760431870a6abf9b0866bb0]
url: https://github.com/intel-lab-lkp/linux/commits/Ramiro-Oliveira/Add-Advantech-EIO-MFD-driver/20251213-004905
base: d9771d0dbe18dd643760431870a6abf9b0866bb0
patch link: https://lore.kernel.org/r/20251212-upstream-v1-v1-8-d50d40ec8d8a%40advantech.com
patch subject: [PATCH 8/8] Add Advantech EIO Fan driver
config: nios2-allmodconfig (https://download.01.org/0day-ci/archive/20251214/202512140153.dNgpAKJt-lkp@intel.com/config)
compiler: nios2-linux-gcc (GCC) 11.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251214/202512140153.dNgpAKJt-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202512140153.dNgpAKJt-lkp@intel.com/
All warnings (new ones prefixed by >>):
drivers/thermal/eio_fan.c: In function 'eio_fan_probe':
>> drivers/thermal/eio_fan.c:391:21: warning: variable 'temps_mc' set but not used [-Wunused-but-set-variable]
391 | int temps_mc[TRIP_NUM];
| ^~~~~~~~
vim +/temps_mc +391 drivers/thermal/eio_fan.c
375
376 static int eio_fan_probe(struct platform_device *pdev)
377 {
378 struct device *dev = &pdev->dev;
379 unsigned int fan_id;
380 int ret;
381
382 if (!dev_get_drvdata(dev->parent)) {
383 dev_err(dev, "eio_core not present\n");
384 return -ENODEV;
385 }
386
387 for (fan_id = 0; fan_id < FAN_MAX; fan_id++) {
388 u8 state = 0, name = 0;
389 int trip_hi = 0, trip_lo = 0, trip_stop = 0;
390 int pwm_hi = 0, pwm_lo = 0;
> 391 int temps_mc[TRIP_NUM];
392 struct eio_fan_dev *fan;
393 struct thermal_zone_device *tzd;
394 struct thermal_cooling_device *cdev;
395
396 if (pmc_read(dev->parent, CTRL_STATE, fan_id, &state) ||
397 pmc_read(dev->parent, CTRL_TYPE, fan_id, &name) ||
398 pmc_read(dev->parent, CTRL_THERM_HIGH, fan_id, &trip_hi) ||
399 pmc_read(dev->parent, CTRL_THERM_LOW, fan_id, &trip_lo) ||
400 pmc_read(dev->parent, CTRL_THERM_STOP, fan_id, &trip_stop) ||
401 pmc_read(dev->parent, CTRL_PWM_HIGH, fan_id, &pwm_hi) ||
402 pmc_read(dev->parent, CTRL_PWM_LOW, fan_id, &pwm_lo)) {
403 dev_info(dev, "fan%u: pmc read error, skipping\n", fan_id);
404 continue;
405 }
406
407 if (!(state & 0x1)) {
408 dev_info(dev, "fan%u: firmware reports disabled\n", fan_id);
409 continue;
410 }
411
412 if (!fan_name[name][0]) {
413 dev_info(dev, "fan%u: unknown name index %u\n", fan_id, name);
414 continue;
415 }
416
417 temps_mc[TRIP_HIGH] = DECI_KELVIN_TO_MILLI_CELSIUS(trip_hi);
418 temps_mc[TRIP_LOW] = DECI_KELVIN_TO_MILLI_CELSIUS(trip_lo);
419 temps_mc[TRIP_STOP] = DECI_KELVIN_TO_MILLI_CELSIUS(trip_stop);
420
421 fan = devm_kzalloc(dev, sizeof(*fan), GFP_KERNEL);
422 if (!fan)
423 return -ENOMEM;
424
425 fan->mfd = dev->parent;
426 fan->id = (u8)fan_id;
427
428 fan->trip_priv[TRIP_HIGH].trip_ctl = CTRL_THERM_HIGH;
429 fan->trip_priv[TRIP_LOW].trip_ctl = CTRL_THERM_LOW;
430 fan->trip_priv[TRIP_STOP].trip_ctl = CTRL_THERM_STOP;
431
432 struct thermal_trip trips[TRIP_NUM] = {
433 [TRIP_HIGH] = {
434 .type = THERMAL_TRIP_ACTIVE,
435 .temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_hi),
436 .flags = THERMAL_TRIP_FLAG_RW_TEMP,
437 .priv = &fan->trip_priv[TRIP_HIGH],
438 },
439 [TRIP_LOW] = {
440 .type = THERMAL_TRIP_ACTIVE,
441 .temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_lo),
442 .flags = THERMAL_TRIP_FLAG_RW_TEMP,
443 .priv = &fan->trip_priv[TRIP_LOW],
444 },
445 [TRIP_STOP] = {
446 .type = THERMAL_TRIP_ACTIVE,
447 .temperature = DECI_KELVIN_TO_MILLI_CELSIUS(trip_stop),
448 .flags = THERMAL_TRIP_FLAG_RW_TEMP,
449 .priv = &fan->trip_priv[TRIP_STOP],
450 },
451 };
452
453 tzd = thermal_zone_device_register_with_trips(fan_name[name],
454 trips, TRIP_NUM,
455 fan,
456 &zone_ops,
457 NULL,
458 0, 0);
459 if (IS_ERR(tzd))
460 return PTR_ERR(tzd);
461
462 cdev = thermal_cooling_device_register(fan_name[name], fan, &cooling_ops);
463 if (IS_ERR(cdev)) {
464 thermal_zone_device_unregister(tzd);
465 dev_err(dev, "fan%u: cdev register failed: %ld\n",
466 fan_id, PTR_ERR(cdev));
467 return PTR_ERR(cdev);
468 }
469
470 dev_set_drvdata(thermal_zone_device(tzd), tzd);
471 ret = device_create_file(thermal_zone_device(tzd), &dev_attr_fan_mode);
472 if (ret)
473 dev_warn(dev, "Error create thermal zone fan_mode sysfs\n");
474 }
475 return 0;
476 }
477
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* Re: [PATCH 1/8] Add Advantech EIO MFD driver
From: kernel test robot @ 2025-12-13 15:19 UTC (permalink / raw)
To: Ramiro Oliveira, 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: oe-kbuild-all, linux-kernel, 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: <20251212-upstream-v1-v1-1-d50d40ec8d8a@advantech.com>
Hi Ramiro,
kernel test robot noticed the following build warnings:
[auto build test WARNING on d9771d0dbe18dd643760431870a6abf9b0866bb0]
url: https://github.com/intel-lab-lkp/linux/commits/Ramiro-Oliveira/Add-Advantech-EIO-MFD-driver/20251213-004905
base: d9771d0dbe18dd643760431870a6abf9b0866bb0
patch link: https://lore.kernel.org/r/20251212-upstream-v1-v1-1-d50d40ec8d8a%40advantech.com
patch subject: [PATCH 1/8] Add Advantech EIO MFD driver
config: nios2-allmodconfig (https://download.01.org/0day-ci/archive/20251213/202512132239.HrAPSw6z-lkp@intel.com/config)
compiler: nios2-linux-gcc (GCC) 11.5.0
reproduce (this is a W=1 build): (https://download.01.org/0day-ci/archive/20251213/202512132239.HrAPSw6z-lkp@intel.com/reproduce)
If you fix the issue in a separate patch/commit (i.e. not just a new version of
the same patch/commit), kindly add following tags
| Reported-by: kernel test robot <lkp@intel.com>
| Closes: https://lore.kernel.org/oe-kbuild-all/202512132239.HrAPSw6z-lkp@intel.com/
All warnings (new ones prefixed by >>):
>> Warning: drivers/mfd/eio_core.c:37 cannot understand function prototype: 'uint timeout = DEFAULT_TIMEOUT;'
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH v2] fbdev: arkfb: Request legacy VGA I/O region
From: Swaraj Gaikwad @ 2025-12-13 20:22 UTC (permalink / raw)
To: Helge Deller, Andrew Morton, Hans Verkuil, Nicolas Dufresne,
Frank Li, Zi Yan, Donet Tom, Yann Dirson, Konrad Dybcio,
Vivek Kasireddy, Qianfeng Rong, Kees Cook,
open list:FRAMEBUFFER LAYER, open list:FRAMEBUFFER LAYER,
open list
Cc: skhan, david.hunter.linux, Swaraj Gaikwad
The arkfb driver uses the legacy VGA I/O range (0x3c0+) but does not
request it. This can cause conflicts with other drivers that try to
reserve these ports.
Fix this by using devm_request_region() during the probe function.
This ensures the region is properly reserved and automatically released
on driver detach.
v1: https://lore.kernel.org/lkml/20251213154937.104301-1-swarajgaikwad1925@gmail.com/
Signed-off-by: Swaraj Gaikwad <swarajgaikwad1925@gmail.com>
---
v2:
- Use resource_size(&vga_res) instead of hardcoded 64 * 1024.
- (Feedback from Kees Cook)
Compile-tested only on x86_64.
drivers/video/fbdev/arkfb.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/video/fbdev/arkfb.c b/drivers/video/fbdev/arkfb.c
index ec084323115f..24e4c20d1a32 100644
--- a/drivers/video/fbdev/arkfb.c
+++ b/drivers/video/fbdev/arkfb.c
@@ -1018,6 +1018,12 @@ static int ark_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
pcibios_bus_to_resource(dev->bus, &vga_res, &bus_reg);
+ if (!devm_request_region(&dev->dev, vga_res.start, resource_size(&vga_res), "arkfb-vga")) {
+ dev_err(info->device, "cannot reserve legacy VGA ports\n");
+ rc = -EBUSY;
+ goto err_find_mode;
+ }
+
par->state.vgabase = (void __iomem *) (unsigned long) vga_res.start;
/* FIXME get memsize */
base-commit: a859eca0e4cc96f63ff125dbe5388d961558b0e9
--
2.52.0
^ permalink raw reply related
* Re: [PATCH] fbdev: arkfb: Request legacy VGA I/O region
From: Kees Cook @ 2025-12-13 14:22 UTC (permalink / raw)
To: Swaraj Gaikwad
Cc: Helge Deller, Andrew Morton, Hans Verkuil, Zi Yan, Frank Li,
Bjorn Andersson, Laurent Pinchart, Qianfeng Rong, Vivek Kasireddy,
Konrad Dybcio, Bartosz Golaszewski, open list:FRAMEBUFFER LAYER,
open list:FRAMEBUFFER LAYER, open list, skhan, david.hunter.linux
In-Reply-To: <20251213154937.104301-1-swarajgaikwad1925@gmail.com>
On Sat, Dec 13, 2025 at 03:49:32PM +0000, Swaraj Gaikwad wrote:
> The arkfb driver uses the legacy VGA I/O range (0x3c0+) but does not
> request it. This can cause conflicts with other drivers that try to
> reserve these ports.
Eek, nice catch!
>
> Fix this by using devm_request_region() during the probe function.
> This ensures the region is properly reserved and automatically released
> on driver detach.
>
> Signed-off-by: Swaraj Gaikwad <swarajgaikwad1925@gmail.com>
> ---
> Compile-tested only on x86_64.
>
> drivers/video/fbdev/arkfb.c | 6 ++++++
> 1 file changed, 6 insertions(+)
>
> diff --git a/drivers/video/fbdev/arkfb.c b/drivers/video/fbdev/arkfb.c
> index ec084323115f..24e4c20d1a32 100644
> --- a/drivers/video/fbdev/arkfb.c
> +++ b/drivers/video/fbdev/arkfb.c
> @@ -1018,6 +1018,12 @@ static int ark_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
>
> pcibios_bus_to_resource(dev->bus, &vga_res, &bus_reg);
>
> + if (!devm_request_region(&dev->dev, vga_res.start, 64 * 1024, "arkfb-vga")) {
I was expecting to see vga_res.end as the third argument instead of
repeating the open-coded value.
-Kees
> + dev_err(info->device, "cannot reserve legacy VGA ports\n");
> + rc = -EBUSY;
> + goto err_find_mode;
> + }
> +
> par->state.vgabase = (void __iomem *) (unsigned long) vga_res.start;
>
> /* FIXME get memsize */
>
> base-commit: a859eca0e4cc96f63ff125dbe5388d961558b0e9
> --
> 2.52.0
>
--
Kees Cook
^ permalink raw reply
* [PATCH] fbdev: arkfb: Request legacy VGA I/O region
From: Swaraj Gaikwad @ 2025-12-13 15:49 UTC (permalink / raw)
To: Helge Deller, Andrew Morton, Hans Verkuil, Zi Yan, Frank Li,
Bjorn Andersson, Laurent Pinchart, Qianfeng Rong, Vivek Kasireddy,
Konrad Dybcio, Bartosz Golaszewski, Kees Cook,
open list:FRAMEBUFFER LAYER, open list:FRAMEBUFFER LAYER,
open list
Cc: skhan, david.hunter.linux, Swaraj Gaikwad
The arkfb driver uses the legacy VGA I/O range (0x3c0+) but does not
request it. This can cause conflicts with other drivers that try to
reserve these ports.
Fix this by using devm_request_region() during the probe function.
This ensures the region is properly reserved and automatically released
on driver detach.
Signed-off-by: Swaraj Gaikwad <swarajgaikwad1925@gmail.com>
---
Compile-tested only on x86_64.
drivers/video/fbdev/arkfb.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/drivers/video/fbdev/arkfb.c b/drivers/video/fbdev/arkfb.c
index ec084323115f..24e4c20d1a32 100644
--- a/drivers/video/fbdev/arkfb.c
+++ b/drivers/video/fbdev/arkfb.c
@@ -1018,6 +1018,12 @@ static int ark_pci_probe(struct pci_dev *dev, const struct pci_device_id *id)
pcibios_bus_to_resource(dev->bus, &vga_res, &bus_reg);
+ if (!devm_request_region(&dev->dev, vga_res.start, 64 * 1024, "arkfb-vga")) {
+ dev_err(info->device, "cannot reserve legacy VGA ports\n");
+ rc = -EBUSY;
+ goto err_find_mode;
+ }
+
par->state.vgabase = (void __iomem *) (unsigned long) vga_res.start;
/* FIXME get memsize */
base-commit: a859eca0e4cc96f63ff125dbe5388d961558b0e9
--
2.52.0
^ permalink raw reply related
* Re: [PATCH 6/8] Add Advantech EIO Watchdog driver
From: Guenter Roeck @ 2025-12-12 18:43 UTC (permalink / raw)
To: Ramiro Oliveira
Cc: 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,
linux-kernel, 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: <20251212-upstream-v1-v1-6-d50d40ec8d8a@advantech.com>
On Fri, Dec 12, 2025 at 05:40:57PM +0100, Ramiro Oliveira wrote:
> 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 | 672 +++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 681 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index df4b4cc31257..dfdf4f39c14b 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -624,6 +624,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
>
> ADXL313 THREE-AXIS DIGITAL ACCELEROMETER DRIVER
> diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
> index d3b9df7d466b..2f8508e51634 100644
> --- a/drivers/watchdog/Kconfig
> +++ b/drivers/watchdog/Kconfig
> @@ -248,6 +248,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 OF_GPIO
> diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
> index ba52099b1253..59b5ec0246d6 100644
> --- a/drivers/watchdog/Makefile
> +++ b/drivers/watchdog/Makefile
> @@ -230,6 +230,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..a81f005d82d2
> --- /dev/null
> +++ b/drivers/watchdog/eio_wdt.c
> @@ -0,0 +1,672 @@
> +// 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/module.h>
> +#include <linux/reboot.h>
> +#include <linux/uaccess.h>
> +#include <linux/watchdog.h>
> +#include <linux/mfd/eio.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)");
> +
> +/* Specify the IRQ number when the IRQ event is triggered */
> +static int irq;
> +module_param(irq, int, 0);
> +MODULE_PARM_DESC(irq, "The IRQ number for IRQ event");
> +
> +static int timeout;
> +module_param(timeout, int, 0444);
> +MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
> +
Those module parameters are unusual and (for timeout) even misleading.
The "timeout" module parameter in a watchdog driver, if it exists,
is usually the watchdog timeout. This is likely to be confused.
On top of that, expecting the user to know the PMC timeout or an irq
number is just not reasonable, much less giving the user an opportunity
to write such values into the chip.
> +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,
> + .timeout = timeout,
> + };
> + 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,
> + .timeout = timeout,
> + };
> + return eio_core_pmc_operation(dev, &op);
> +}
Those functions are almost the same. Only CMD_WDT_READ/CMD_WDT_WRITE
is different. Please fold into a single function.
> +
> +static int wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout)
> +{
> + struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
> +
> + wdd->timeout = timeout;
> + dev_info(eio_wdt->dev, "Set timeout: %u\n", timeout);
> +
> + return 0;
> +}
> +
> +static int wdt_set_pretimeout(struct watchdog_device *wdd, unsigned int pretimeout)
> +{
> + struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
> +
> + wdd->pretimeout = pretimeout;
> + dev_info(eio_wdt->dev, "Set pretimeout: %u\n", pretimeout);
> +
> + return 0;
> +}
The above two functions to nothing that isn't already done by the watchdog
core, except to add logging noise. That is not acceptable.
> +
> +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;
> + }
> +
> + dev_info(eio_wdt->dev, "Trigger type is %d:%s\n",
> + i, type_strs[i]);
> + eio_wdt->event_type = i;
> + return 0;
> + }
> + }
> +
> + dev_info(eio_wdt->dev, "Event type: %s\n",
> + type_strs[eio_wdt->event_type]);
Drop all that logging noise.
> + 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;
> +
> + dev_dbg(eio_wdt->dev, "Pretimeout H/W disabled\n");
Is it ? Then why set it to a value != 0 above ?
> + 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 unsigned int wdt_get_timeleft(struct watchdog_device *wdd)
> +{
> + struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
> + unsigned int timeleft = 0;
> +
> + if (eio_wdt->last_time && wdd->timeout) {
> + unsigned long delta = jiffies - eio_wdt->last_time;
> + unsigned int elapsed = (unsigned int)(delta / HZ);
> +
> + if (elapsed < wdd->timeout)
> + timeleft = wdd->timeout - elapsed;
> + }
> + return timeleft;
No, that is not what the timeleft function is supposed to do. It is supposed
to read the remaining time from the chip. If the chip does not support reading
the remaining time, do not simulate it in software.
> +}
> +
> +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);
> + 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);
> +
> + /* Enable WDT */
> + ret |= regmap_write(map, idx, IOREG_WDT_STATUS);
> + ret |= regmap_write(map, data, FLAG_WDT_ENABLED);
> +
> + /* Set IRQ number */
> + ret |= regmap_write(map, idx, IOREG_IRQ);
> + ret |= regmap_write(map, data, eio_wdt->irq);
> +
> + /* Lock back */
> + ret |= regmap_write(map, idx, IOREG_LOCK);
While it may be convenient, it is unacceptable to bundle return codes
this way. The behavior of this code is completely undefined if any
of the accesses above fails.
> +
> + mutex_unlock(&eio_wdt->core->mutex);
> +
> + return ret ? -EIO : 0;
> +}
> +
> +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;
> +
> + if (irq) {
> + eio_wdt->irq = irq;
> + } else {
> + 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,
> + .get_timeleft = wdt_get_timeleft,
> + .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,
This would end up requesting an irq provided through the module parameter.
This is completely unacceptable.
> + wdt_isr, wdt_threaded_isr,
> + IRQF_SHARED | IRQF_ONESHOT, pdev->name,
> + eio_wdt);
> + if (ret) {
> + dev_err(dev, "IRQ %d request fail:%d. Disabled.\n",
> + eio_wdt->irq, ret);
You might want to consider using dev_err_probe().
> + return ret;
> + }
> + }
> +
> + wdd = &eio_wdt->wdd;
> + wdd->info = &wdinfo;
> + wdd->ops = &wdt_ops;
> + wdd->parent = dev;
> + wdd->min_timeout = 1;
> + wdd->max_timeout = 0x7FFF;
> +
> + ret = watchdog_init_timeout(wdd, wdd->timeout, dev);
This is pretty pointless. The function is normally used to overwrite
the default timeout (which is already in wdd->timeout) with a module
parameter or a timeout from devicetree. Calling it with the already
configured timeout does not add any value. Either call it with "0"
as timeout parameter to let it read any timeout from devicetree if
provided, or drop the call.
> + 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
* Re: [PATCH 3/8] Add Advantech EIO Hardware Monitor driver
From: Guenter Roeck @ 2025-12-12 18:21 UTC (permalink / raw)
To: Ramiro Oliveira
Cc: 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,
linux-kernel, 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: <20251212-upstream-v1-v1-3-d50d40ec8d8a@advantech.com>
On Fri, Dec 12, 2025 at 05:40:54PM +0100, Ramiro Oliveira wrote:
> This driver controls the Hardware Monitor block of the Advantech EIO chip.
>
> Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
> ---
> MAINTAINERS | 1 +
> drivers/hwmon/Kconfig | 10 ++
> drivers/hwmon/Makefile | 1 +
> drivers/hwmon/eio-hwmon.c | 344 ++++++++++++++++++++++++++++++++++++++++++++++
> 4 files changed, 356 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index 359d4a13f212..fdd39b152f41 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -620,6 +620,7 @@ ADVANTECH EIO DRIVER
> M: Ramiro Oliveira <ramiro.oliveira@advantech.com>
> S: Maintained
> F: drivers/gpio/gpio-eio.c
> +F: drivers/hwmon/eio-hwmon.c
> F: drivers/mfd/eio_core.c
> F: include/linux/mfd/eio.h
>
> diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
> index 157678b821fc..08993b993596 100644
> --- a/drivers/hwmon/Kconfig
> +++ b/drivers/hwmon/Kconfig
> @@ -2043,6 +2043,16 @@ config SENSORS_DME1737
> This driver can also be built as a module. If so, the module
> will be called dme1737.
>
> +config SENSORS_EIO
> + tristate "Advantech EIO HWMON"
> + depends on MFD_EIO
> + help
> + If you say yes here you get support for the Advantech EIO
> + temperature, voltage and fan speed monitoring block.
> +
> + This driver can also be built as a module. If so, the module
> + will be called eio-hwmon
> +
> config SENSORS_EMC1403
> tristate "SMSC EMC1403/23 thermal sensor"
> depends on I2C
> diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
> index eade8e3b1bde..e69f03b41fae 100644
> --- a/drivers/hwmon/Makefile
> +++ b/drivers/hwmon/Makefile
> @@ -72,6 +72,7 @@ obj-$(CONFIG_SENSORS_DME1737) += dme1737.o
> obj-$(CONFIG_SENSORS_DRIVETEMP) += drivetemp.o
> obj-$(CONFIG_SENSORS_DS620) += ds620.o
> obj-$(CONFIG_SENSORS_DS1621) += ds1621.o
> +obj-$(CONFIG_SENSORS_EIO) += eio-hwmon.o
> obj-$(CONFIG_SENSORS_EMC1403) += emc1403.o
> obj-$(CONFIG_SENSORS_EMC2103) += emc2103.o
> obj-$(CONFIG_SENSORS_EMC2305) += emc2305.o
> diff --git a/drivers/hwmon/eio-hwmon.c b/drivers/hwmon/eio-hwmon.c
> new file mode 100644
> index 000000000000..164591aa31a7
> --- /dev/null
> +++ b/drivers/hwmon/eio-hwmon.c
> @@ -0,0 +1,344 @@
> +// 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/hwmon.h>
> +#include <linux/hwmon-sysfs.h>
> +#include <linux/mfd/core.h>
> +#include <linux/mfd/eio.h>
> +#include <linux/module.h>
> +
> +#define MAX_DEV 128
> +#define MAX_NAME 32
> +
> +static uint timeout;
> +module_param(timeout, uint, 0444);
> +MODULE_PARM_DESC(timeout,
> + "Default pmc command timeout in micro-seconds.\n");
> +
It does not make sense to me to have this as module parameter for each of
the drivers.
> +struct eio_hwmon_dev {
> + struct device *mfd;
> +};
> +
> +enum _sen_type {
> + NONE,
> + VOLTAGE,
> + CURRENT,
> + TEMP,
> + PWM,
> + TACHO,
> + FAN,
> + CASEOPEN,
> +};
> +
> +struct eio_key {
> + enum _sen_type type;
> + u8 chan;
> + u8 item;
> + u8 label_idx;
> +};
> +
> +struct eio_attr {
> + struct sensor_device_attribute sda;
> + struct eio_key key;
> +};
> +
> +static struct {
> + u8 cmd;
> + u8 max;
> + signed int shift;
> + char name[32];
> + u8 ctrl[16];
> + u16 multi[16];
> + char item[16][32];
> + char labels[32][32];
> +
> +} sen_info[] = {
> + { 0x00, 0, 0, "none" },
> + { 0x12, 8, 0, "in",
> + { 0xFF, 0x10, 0x11, 0x12 },
> + { 1, 10, 10, 10 },
> + { "label", "input", "max", "min" },
> + { "5V", "5Vs5", "12V", "12Vs5",
> + "3V3", "3V3", "5Vsb", "3Vsb",
> + "Vcmos", "Vbat", "Vdc", "Vstb",
> + "Vcore_a", "Vcore_b", "", "",
> + "Voem0", "Voem1", "Voem2", "Voem3"
> + },
> + },
> + { 0x1a, 2, 0, "curr",
> + { 0xFF, 0x10, 0x11, 0x12 },
> + { 1, 10, 10, 10 },
> + { "label", "input", "max", "min" },
> + { "dc", "oem0" },
> + },
> + { 0x10, 4, -2731, "temp",
> + { 0xFF, 0x10, 0x11, 0x12, 0x21, 0x41 },
> + { 1, 100, 100, 100, 100, 100 },
> + { "label", "input", "max", "min", "crit", "emergency" },
> + { "cpu0", "cpu1", "cpu2", "cpu3",
> + "sys0", "sys1", "sys2", "sys3",
> + "aux0", "aux1", "aux2", "aux3",
> + "dimm0", "dimm1", "dimm2", "dimm3",
> + "pch", "gpu", "", "",
> + "", "", "", "",
> + "", "", "", "",
> + "oem0", "oem1", "oem", "oem3" },
> + },
> + { 0x14, 0, 0, "pwm",
> + { 0xFF, 0x11, 0x12 },
> + { 1, 1, 1 },
> + { "label", "polarity", "freq" },
> + { "pwm0", "pwm0", "pwm0", "pwm0" },
> + },
> + { 0x16, 2, 0, "tacho",
> + { 0xFF, 0x10 },
> + { 1, 1 },
> + { "label", "input"},
> + { "cpu0", "cpu1", "cpu2", "cpu3",
> + "sys0", "sys1", "sys2", "sys3",
> + "", "", "", "", "", "", "", "",
> + "", "", "", "", "", "", "", "",
> + "", "", "", "",
> + "oem0", "oem1", "oem2", "oem3"
> + },
> + },
> + { 0x24, 4, 0, "fan",
> + { 0xFF, 0x1A },
> + { 1, 1 },
> + { "label", "input"},
> + { "cpu0", "cpu1", "cpu2", "cpu3",
> + "sys0", "sys1", "sys2", "sys3",
> + "", "", "", "", "", "", "", "",
> + "", "", "", "", "", "", "", "",
> + "", "", "", "",
> + "oem0", "oem1", "oem2", "oem3",
> + },
> + },
> + { 0x28, 1, 0, "intrusion",
> + { 0xFF, 0x02 },
> + { 1, 1 },
> + { "label", "input" },
> + { "case_open" }
> + }
> +};
> +
> +static struct {
> + enum _sen_type type;
> + u8 ctrl;
> + int size;
> + bool write;
> +
> +} ctrl_para[] = {
> + { NONE, 0x00, 0, false },
> +
> + { VOLTAGE, 0x00, 1, false }, { VOLTAGE, 0x01, 1, false },
> + { VOLTAGE, 0x10, 2, false }, { VOLTAGE, 0x11, 2, false },
> + { VOLTAGE, 0x12, 2, false },
> +
> + { CURRENT, 0x00, 1, false }, { CURRENT, 0x01, 1, false },
> + { CURRENT, 0x10, 2, false }, { CURRENT, 0x11, 2, false },
> + { CURRENT, 0x12, 2, false },
> +
> + { TEMP, 0x00, 2, false }, { TEMP, 0x01, 1, false },
> + { TEMP, 0x04, 1, false }, { TEMP, 0x10, 2, false },
> + { TEMP, 0x11, 2, false }, { TEMP, 0x12, 2, false },
> + { TEMP, 0x21, 2, false }, { TEMP, 0x41, 2, false },
> +
> + { PWM, 0x00, 1, false }, { PWM, 0x10, 1, true },
> + { PWM, 0x11, 1, true }, { PWM, 0x12, 4, true },
> +
> + { TACHO, 0x00, 1, false }, { TACHO, 0x01, 1, false },
> + { TACHO, 0x10, 4, true },
> +
> + { FAN, 0x00, 1, false }, { FAN, 0x01, 1, false },
> + { FAN, 0x03, 1, true }, { FAN, 0x1A, 2, false },
> +
> + { CASEOPEN, 0x00, 1, false }, { CASEOPEN, 0x02, 1, true },
> +};
> +
> +static int para_idx(enum _sen_type type, u8 ctrl)
> +{
> + int i;
> +
> + for (i = 1 ; i < ARRAY_SIZE(ctrl_para) ; i++)
> + if (type == ctrl_para[i].type &&
> + ctrl == ctrl_para[i].ctrl)
> + return i;
> +
> + return 0;
> +}
> +
> +static int pmc_read(struct device *mfd, enum _sen_type type, u8 dev_id, u8 ctrl, void *data)
> +{
> + int idx = para_idx(type, ctrl);
> + int ret = 0;
> +
> + if (idx == 0)
> + return -EINVAL;
> +
> + if (WARN_ON(!data))
> + return -EINVAL;
> +
> + struct pmc_op op = {
> + .cmd = sen_info[type].cmd | EIO_FLAG_PMC_READ,
> + .control = ctrl,
> + .device_id = dev_id,
> + .size = ctrl_para[idx].size,
> + .payload = (u8 *)data,
> + .timeout = timeout,
> + };
> +
> + ret = eio_core_pmc_operation(mfd, &op);
> + return ret;
> +}
> +
> +static ssize_t eio_show(struct device *dev, struct device_attribute *attr,
> + char *buf)
> +{
> + struct eio_hwmon_dev *eio_hwmon = dev_get_drvdata(dev);
> + struct eio_attr *eio_attr =
> + container_of(attr, struct eio_attr, sda.dev_attr);
> + const struct eio_key *eio_key = &eio_attr->key;
> + int ret;
> + u8 data[2];
> + u32 temp_val;
> + signed int final_val;
> +
> + switch (eio_key->item) {
> + case 0:
> + return sysfs_emit(buf, "%s\n",
> + sen_info[eio_key->type].labels[eio_key->label_idx]);
> +
> + default:
> + ret = pmc_read(eio_hwmon->mfd, eio_key->type, eio_key->chan,
> + sen_info[eio_key->type].ctrl[eio_key->item],
> + &data);
> + if (ret)
> + return ret;
> +
> + temp_val = data[0] | data[1] << 8;
> +
> + final_val = (signed int)temp_val + (signed int)(sen_info[eio_key->type].shift);
> + final_val = final_val * (signed int)sen_info[eio_key->type].multi[eio_key->item];
> +
> + return sysfs_emit(buf, "%d\n", final_val);
> + }
> +
> + return -EINVAL;
> +}
> +
> +static char devname[MAX_DEV][MAX_NAME];
> +static struct eio_attr devattrs[MAX_DEV];
> +static struct attribute *attrs[MAX_DEV];
> +
> +static struct attribute_group group = {
> + .attrs = attrs,
> +};
> +
> +static const struct attribute_group *groups[] = {
> + &group,
> + NULL
> +};
> +
> +static int hwmon_init(struct device *mfd, struct eio_hwmon_dev *eio_hwmon)
> +{
> + enum _sen_type type;
> + u8 i, j, data[16];
> + int sum = 0;
> + int ret;
> +
> + for (type = VOLTAGE ; type <= CASEOPEN ; type++) {
> + int cnt = 1;
> +
> + for (i = 0 ; i < sen_info[type].max ; i++) {
> + if (pmc_read(mfd, type, i, 0x00, data) ||
> + (data[0] & 0x01) == 0)
> + continue;
> +
> + memset(data, 0, sizeof(data));
> + ret = pmc_read(mfd, type, i, 0x01, data);
> + if (ret != 0 && ret != -EINVAL) {
> + dev_info(mfd, "read type id error\n");
> + continue;
> + }
> +
> + for (j = 0 ; j < ARRAY_SIZE(sen_info->item) ; j++) {
> + struct eio_attr *eio_attr;
> +
> + if (sen_info[type].item[j][0] == 0)
> + continue;
> +
> + eio_attr = &devattrs[sum];
> +
> + eio_attr->key.type = type;
> + eio_attr->key.chan = i;
> + eio_attr->key.item = j;
> + eio_attr->key.label_idx = data[0];
> +
> + snprintf(devname[sum], sizeof(devname[sum]),
> + "%s%d_%s", sen_info[type].name, cnt,
> + sen_info[type].item[j]);
> +
> + eio_attr->sda.dev_attr.attr.name = devname[sum];
> + eio_attr->sda.dev_attr.attr.mode = 0444;
> + eio_attr->sda.dev_attr.show = eio_show;
> +
> + attrs[sum] = &eio_attr->sda.dev_attr.attr;
> +
> + if (++sum >= MAX_DEV)
> + break;
> + }
> + cnt++;
> + }
> + }
> +
> + return sum;
> +}
> +
> +static int hwmon_probe(struct platform_device *pdev)
> +{
> + struct device *dev = &pdev->dev;
> + struct eio_hwmon_dev *eio_hwmon;
> + struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
> + struct device *hwmon;
> +
> + if (!eio_dev) {
> + dev_err(dev, "Error contact eio_core\n");
> + return -ENODEV;
> + }
> +
> + eio_hwmon = devm_kzalloc(dev, sizeof(*eio_hwmon), GFP_KERNEL);
> + if (!eio_hwmon)
> + return -ENOMEM;
> +
> + eio_hwmon->mfd = dev->parent;
> + platform_set_drvdata(pdev, eio_hwmon);
> +
> + if (hwmon_init(dev->parent, eio_hwmon) <= 0)
> + return -ENODEV;
> +
> + hwmon = devm_hwmon_device_register_with_groups(dev, KBUILD_MODNAME,
> + eio_hwmon,
> + groups);
This API is deprecated. Please rework to use
devm_hwmon_device_register_with_info().
Also, it is highly unusual to have both a hardware monitoring driver
and a thermal driver (instead of instantiating the thermal device
from the hardware monitoring driver). This warrants a detailed
explanation.
Thanks,
Guenter
> + return PTR_ERR_OR_ZERO(hwmon);
> +}
> +
> +static struct platform_driver eio_hwmon_driver = {
> + .probe = hwmon_probe,
> + .driver = {
> + .name = "eio_hwmon",
> + },
> +};
> +
> +module_platform_driver(eio_hwmon_driver);
> +
> +MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
> +MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
> +MODULE_DESCRIPTION("Hardware monitor driver for Advantech EIO embedded controller");
> +MODULE_LICENSE("GPL");
> +
>
> --
> 2.43.0
>
>
^ permalink raw reply
* Re: [PATCH 5/8] Add Advantech EIO Backlight driver
From: Daniel Thompson @ 2025-12-12 17:59 UTC (permalink / raw)
To: Ramiro Oliveira
Cc: 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, linux-kernel, 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: <20251212-upstream-v1-v1-5-d50d40ec8d8a@advantech.com>
On Fri, Dec 12, 2025 at 05:40:56PM +0100, Ramiro Oliveira wrote:
> This driver controls the Video Backlight block of the Advantech EIO chip.
>
> Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
Thanks for the patch.
Review below...
> ---
> MAINTAINERS | 1 +
> drivers/video/backlight/Kconfig | 6 +
> drivers/video/backlight/Makefile | 1 +
> drivers/video/backlight/eio_bl.c | 268 +++++++++++++++++++++++++++++++++++++++
> 4 files changed, 276 insertions(+)
>
> diff --git a/MAINTAINERS b/MAINTAINERS
> index be9d3c4e1ce1..df4b4cc31257 100644
> --- a/MAINTAINERS
> +++ b/MAINTAINERS
> @@ -623,6 +623,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
>
> ADXL313 THREE-AXIS DIGITAL ACCELEROMETER DRIVER
> diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig
> index a1422ddd1c22..ddd3d6922553 100644
> --- a/drivers/video/backlight/Kconfig
> +++ b/drivers/video/backlight/Kconfig
> @@ -496,6 +496,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 a5d62b018102..4601b644b6d4 100644
> --- a/drivers/video/backlight/Makefile
> +++ b/drivers/video/backlight/Makefile
> @@ -30,6 +30,7 @@ obj-$(CONFIG_BACKLIGHT_BD6107) += bd6107.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..2b9fd4d48d30
> --- /dev/null
> +++ b/drivers/video/backlight/eio_bl.c
> @@ -0,0 +1,268 @@
> +// 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
> +
> +#define BL_AVAIL BIT(0)
> +#define BL_PWM_DC BIT(1)
> +#define BL_PWM_SRC BIT(2)
> +#define BL_BRI_INVERT BIT(3)
> +#define BL_ENABLE_PIN_SUPP BIT(4)
> +#define BL_POWER_INVERT BIT(5)
> +#define BL_ENABLE_PIN_EN BIT(6)
> +#define BL_FIRMWARE_ERROR BIT(7)
These appear to be unused.
> +
> +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");
> +
> +static int timeout;
> +module_param(timeout, int, 0444);
> +MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
Module parameters are not really expected these days and are
pretty user hostile.
Are they really needed? AFAICT this is a firmware based device. Why
doesn't the firmware provide this information if the drivers need it
(either directly or via PNP ID and a lookup table)?
> +
> +struct eio_bl_dev {
> + struct device *mfd;
> + u8 id;
> + u8 max;
The value in max is never read.
> +};
> +
> +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,
> + .timeout = timeout,
> + };
> +
> + 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,
> + .timeout = timeout,
> + };
> +
> + 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);
> + u32 max = bl->props.max_brightness;
> + u8 duty = clamp_val(bl->props.brightness, 0, max);
> + u8 sw = bl->props.power == BACKLIGHT_POWER_OFF;
You shouldn't need to read anything from bl->props directly.
Please use the helper functions to get the duty value and power
state.
> + 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 -ENXIO;
Is -ENODEV more appropriate here?
> + }
> +
> + ret = pmc_read(dev, BL_CTRL_DUTY, id, &props->brightness);
> + if (ret)
> + return ret;
> +
> + /* Invert PWM */
> + dev_dbg(dev, "bri_invert=%d\n", bri_invert);
Let's drop the dev_dbg() messages, printing module parameter values
isn't very useful.
> + if (bri_invert > USE_DEFAULT) {
> + ret = pmc_write(dev, BL_CTRL_INVERT, id, &bri_invert);
> + if (ret)
> + return ret;
> + }
> +
> + bri_invert = 0;
> + ret = pmc_read(dev, BL_CTRL_INVERT, id, &bri_invert);
> + if (ret)
> + return ret;
Writing back to module parameters during probe is rather unusual. Is it
really needed?
> +
> + dev_dbg(dev, "bri_freq=%u\n", bri_freq);
> + 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;
> +
> + dev_dbg(dev, "bl_power_invert=%d\n", bl_power_invert);
> + if (bl_power_invert >= USE_DEFAULT) {
> + ret = pmc_write(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
> + if (ret)
> + return ret;
> + }
> +
> + bl_power_invert = 0;
> + 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;
> + 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;
> + int ret;
> +
> + memset(&props, 0, sizeof(props));
> + props.type = BACKLIGHT_RAW;
> + props.max_brightness = THERMAL_MAX;
> + props.power = BACKLIGHT_POWER_OFF;
> + props.brightness = props.max_brightness;
New drivers should not initialize props.scale as UNKNOWN. Please
set to match the hardware behaviour (if brightness 50% looks roughly
half as bright as 100% then the scale is 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;
> + eio_bl->max = props.max_brightness;
> +
> + ret = bl_init(eio_bl->mfd, id, &props);
> + if (ret) {
> + dev_info(dev, "%d No Backlight %u enabled!\n", ret, id);
> + continue;
> + }
If neither backlight is enabled if would be good to propagate the return
value (to prevent the probe from spuriously succeeding).
> +
> + 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;
> + }
> +
> + dev_info(dev, "%s registered (max=%u)\n", name, props.max_brightness);
Silence (on success) is golden. Please remove this.
> + }
> +
> + return 0;
> +}
> +
> +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");
Thanks
Daniel.
^ permalink raw reply
* [PATCH 8/8] Add Advantech EIO Fan driver
From: Ramiro Oliveira @ 2025-12-12 16:40 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, 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: <20251212-upstream-v1-v1-0-d50d40ec8d8a@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 | 490 ++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 500 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 770b2f82d01a..b227a9d28191 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -623,6 +623,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 7309f7e7a1c1..ba4958ff0962 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 3740540d8a18..2633e8ed9fdc 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..7f0529a1907e
--- /dev/null
+++ b/drivers/thermal/eio_fan.c
@@ -0,0 +1,490 @@
+// 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 timeout;
+module_param(timeout, int, 0444);
+MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
+
+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,
+ .timeout = timeout,
+ };
+ 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,
+ .timeout = timeout,
+ };
+ 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,
+ .timeout = timeout,
+ };
+ 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;
+ int temps_mc[TRIP_NUM];
+ 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;
+ }
+
+ temps_mc[TRIP_HIGH] = DECI_KELVIN_TO_MILLI_CELSIUS(trip_hi);
+ temps_mc[TRIP_LOW] = DECI_KELVIN_TO_MILLI_CELSIUS(trip_lo);
+ temps_mc[TRIP_STOP] = DECI_KELVIN_TO_MILLI_CELSIUS(trip_stop);
+
+ 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 7/8] Add Advantech EIO Thermal driver
From: Ramiro Oliveira @ 2025-12-12 16:40 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, 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: <20251212-upstream-v1-v1-0-d50d40ec8d8a@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 | 352 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 363 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index dfdf4f39c14b..770b2f82d01a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -623,6 +623,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 b10080d61860..7309f7e7a1c1 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 bb21e7ea7fc6..3740540d8a18 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..2d82bd9d7855
--- /dev/null
+++ b/drivers/thermal/eio_thermal.c
@@ -0,0 +1,352 @@
+// 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 timeout;
+module_param(timeout, int, 0444);
+MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
+
+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],
+ .timeout = timeout,
+ };
+
+ 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],
+ .timeout = timeout,
+ };
+
+ 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 6/8] Add Advantech EIO Watchdog driver
From: Ramiro Oliveira @ 2025-12-12 16:40 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, 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: <20251212-upstream-v1-v1-0-d50d40ec8d8a@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 | 672 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 681 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index df4b4cc31257..dfdf4f39c14b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -624,6 +624,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
ADXL313 THREE-AXIS DIGITAL ACCELEROMETER DRIVER
diff --git a/drivers/watchdog/Kconfig b/drivers/watchdog/Kconfig
index d3b9df7d466b..2f8508e51634 100644
--- a/drivers/watchdog/Kconfig
+++ b/drivers/watchdog/Kconfig
@@ -248,6 +248,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 OF_GPIO
diff --git a/drivers/watchdog/Makefile b/drivers/watchdog/Makefile
index ba52099b1253..59b5ec0246d6 100644
--- a/drivers/watchdog/Makefile
+++ b/drivers/watchdog/Makefile
@@ -230,6 +230,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..a81f005d82d2
--- /dev/null
+++ b/drivers/watchdog/eio_wdt.c
@@ -0,0 +1,672 @@
+// 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/module.h>
+#include <linux/reboot.h>
+#include <linux/uaccess.h>
+#include <linux/watchdog.h>
+#include <linux/mfd/eio.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)");
+
+/* Specify the IRQ number when the IRQ event is triggered */
+static int irq;
+module_param(irq, int, 0);
+MODULE_PARM_DESC(irq, "The IRQ number for IRQ event");
+
+static int timeout;
+module_param(timeout, int, 0444);
+MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
+
+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,
+ .timeout = timeout,
+ };
+ 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,
+ .timeout = timeout,
+ };
+ return eio_core_pmc_operation(dev, &op);
+}
+
+static int wdt_set_timeout(struct watchdog_device *wdd, unsigned int timeout)
+{
+ struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+
+ wdd->timeout = timeout;
+ dev_info(eio_wdt->dev, "Set timeout: %u\n", timeout);
+
+ return 0;
+}
+
+static int wdt_set_pretimeout(struct watchdog_device *wdd, unsigned int pretimeout)
+{
+ struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+
+ wdd->pretimeout = pretimeout;
+ dev_info(eio_wdt->dev, "Set pretimeout: %u\n", 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;
+ }
+
+ dev_info(eio_wdt->dev, "Trigger type is %d:%s\n",
+ i, type_strs[i]);
+ eio_wdt->event_type = i;
+ return 0;
+ }
+ }
+
+ dev_info(eio_wdt->dev, "Event type: %s\n",
+ type_strs[eio_wdt->event_type]);
+ 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;
+
+ dev_dbg(eio_wdt->dev, "Pretimeout H/W disabled\n");
+ 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 unsigned int wdt_get_timeleft(struct watchdog_device *wdd)
+{
+ struct eio_wdt_dev *eio_wdt = watchdog_get_drvdata(wdd);
+ unsigned int timeleft = 0;
+
+ if (eio_wdt->last_time && wdd->timeout) {
+ unsigned long delta = jiffies - eio_wdt->last_time;
+ unsigned int elapsed = (unsigned int)(delta / HZ);
+
+ if (elapsed < wdd->timeout)
+ timeleft = wdd->timeout - elapsed;
+ }
+ return timeleft;
+}
+
+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);
+ 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);
+
+ /* Enable WDT */
+ ret |= regmap_write(map, idx, IOREG_WDT_STATUS);
+ ret |= regmap_write(map, data, FLAG_WDT_ENABLED);
+
+ /* Set IRQ number */
+ ret |= regmap_write(map, idx, IOREG_IRQ);
+ ret |= regmap_write(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_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;
+
+ if (irq) {
+ eio_wdt->irq = irq;
+ } else {
+ 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,
+ .get_timeleft = wdt_get_timeleft,
+ .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(dev, "IRQ %d request fail:%d. Disabled.\n",
+ eio_wdt->irq, ret);
+ return ret;
+ }
+ }
+
+ wdd = &eio_wdt->wdd;
+ wdd->info = &wdinfo;
+ wdd->ops = &wdt_ops;
+ wdd->parent = dev;
+ wdd->min_timeout = 1;
+ wdd->max_timeout = 0x7FFF;
+
+ ret = watchdog_init_timeout(wdd, wdd->timeout, dev);
+ 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 5/8] Add Advantech EIO Backlight driver
From: Ramiro Oliveira @ 2025-12-12 16:40 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, 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: <20251212-upstream-v1-v1-0-d50d40ec8d8a@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 | 268 +++++++++++++++++++++++++++++++++++++++
4 files changed, 276 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index be9d3c4e1ce1..df4b4cc31257 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -623,6 +623,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
ADXL313 THREE-AXIS DIGITAL ACCELEROMETER DRIVER
diff --git a/drivers/video/backlight/Kconfig b/drivers/video/backlight/Kconfig
index a1422ddd1c22..ddd3d6922553 100644
--- a/drivers/video/backlight/Kconfig
+++ b/drivers/video/backlight/Kconfig
@@ -496,6 +496,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 a5d62b018102..4601b644b6d4 100644
--- a/drivers/video/backlight/Makefile
+++ b/drivers/video/backlight/Makefile
@@ -30,6 +30,7 @@ obj-$(CONFIG_BACKLIGHT_BD6107) += bd6107.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..2b9fd4d48d30
--- /dev/null
+++ b/drivers/video/backlight/eio_bl.c
@@ -0,0 +1,268 @@
+// 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
+
+#define BL_AVAIL BIT(0)
+#define BL_PWM_DC BIT(1)
+#define BL_PWM_SRC BIT(2)
+#define BL_BRI_INVERT BIT(3)
+#define BL_ENABLE_PIN_SUPP BIT(4)
+#define BL_POWER_INVERT BIT(5)
+#define BL_ENABLE_PIN_EN BIT(6)
+#define BL_FIRMWARE_ERROR BIT(7)
+
+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");
+
+static int timeout;
+module_param(timeout, int, 0444);
+MODULE_PARM_DESC(timeout, "Set PMC command timeout value.\n");
+
+struct eio_bl_dev {
+ struct device *mfd;
+ u8 id;
+ u8 max;
+};
+
+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,
+ .timeout = timeout,
+ };
+
+ 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,
+ .timeout = timeout,
+ };
+
+ 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);
+ u32 max = bl->props.max_brightness;
+ u8 duty = clamp_val(bl->props.brightness, 0, max);
+ u8 sw = bl->props.power == BACKLIGHT_POWER_OFF;
+ 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 -ENXIO;
+ }
+
+ ret = pmc_read(dev, BL_CTRL_DUTY, id, &props->brightness);
+ if (ret)
+ return ret;
+
+ /* Invert PWM */
+ dev_dbg(dev, "bri_invert=%d\n", bri_invert);
+ if (bri_invert > USE_DEFAULT) {
+ ret = pmc_write(dev, BL_CTRL_INVERT, id, &bri_invert);
+ if (ret)
+ return ret;
+ }
+
+ bri_invert = 0;
+ ret = pmc_read(dev, BL_CTRL_INVERT, id, &bri_invert);
+ if (ret)
+ return ret;
+
+ dev_dbg(dev, "bri_freq=%u\n", bri_freq);
+ 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;
+
+ dev_dbg(dev, "bl_power_invert=%d\n", bl_power_invert);
+ if (bl_power_invert >= USE_DEFAULT) {
+ ret = pmc_write(dev, BL_CTRL_ENABLE_INVERT, id, &bl_power_invert);
+ if (ret)
+ return ret;
+ }
+
+ bl_power_invert = 0;
+ 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;
+ 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;
+ int ret;
+
+ memset(&props, 0, sizeof(props));
+ props.type = BACKLIGHT_RAW;
+ props.max_brightness = THERMAL_MAX;
+ props.power = BACKLIGHT_POWER_OFF;
+ props.brightness = props.max_brightness;
+
+ eio_bl = devm_kzalloc(dev, sizeof(*eio_bl), GFP_KERNEL);
+ if (!eio_bl)
+ return -ENOMEM;
+
+ eio_bl->mfd = dev->parent;
+ eio_bl->id = id;
+ eio_bl->max = props.max_brightness;
+
+ 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;
+ }
+
+ dev_info(dev, "%s registered (max=%u)\n", name, props.max_brightness);
+ }
+
+ return 0;
+}
+
+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 4/8] Add Advantech EIO I2C driver
From: Ramiro Oliveira @ 2025-12-12 16:40 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, 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: <20251212-upstream-v1-v1-0-d50d40ec8d8a@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 | 1142 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 1150 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index fdd39b152f41..be9d3c4e1ce1 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -621,6 +621,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 09ba55bae1fa..e597c08414e4 100644
--- a/drivers/i2c/busses/Kconfig
+++ b/drivers/i2c/busses/Kconfig
@@ -656,6 +656,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 fb985769f5ff..b65bb06b14c6 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..a867f24a4809
--- /dev/null
+++ b/drivers/i2c/busses/i2c-eio.c
@@ -0,0 +1,1142 @@
+// 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 timeout = I2C_TIMEOUT;
+module_param(timeout, int, 0444);
+MODULE_PARM_DESC(timeout, "Set IO timeout value.\n");
+
+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(), 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(), 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(), 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(), 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(), 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(), 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(), 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;
+ }
+
+ timeout = clamp_t(int, timeout, I2C_TIMEOUT / 100, I2C_TIMEOUT * 100);
+ dev_info(dev, "Timeout value %d\n", timeout);
+
+ 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
* [PATCH 3/8] Add Advantech EIO Hardware Monitor driver
From: Ramiro Oliveira @ 2025-12-12 16:40 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, 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: <20251212-upstream-v1-v1-0-d50d40ec8d8a@advantech.com>
This driver controls the Hardware Monitor block of the Advantech EIO chip.
Signed-off-by: Ramiro Oliveira <ramiro.oliveira@advantech.com>
---
MAINTAINERS | 1 +
drivers/hwmon/Kconfig | 10 ++
drivers/hwmon/Makefile | 1 +
drivers/hwmon/eio-hwmon.c | 344 ++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 356 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 359d4a13f212..fdd39b152f41 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -620,6 +620,7 @@ ADVANTECH EIO DRIVER
M: Ramiro Oliveira <ramiro.oliveira@advantech.com>
S: Maintained
F: drivers/gpio/gpio-eio.c
+F: drivers/hwmon/eio-hwmon.c
F: drivers/mfd/eio_core.c
F: include/linux/mfd/eio.h
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 157678b821fc..08993b993596 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -2043,6 +2043,16 @@ config SENSORS_DME1737
This driver can also be built as a module. If so, the module
will be called dme1737.
+config SENSORS_EIO
+ tristate "Advantech EIO HWMON"
+ depends on MFD_EIO
+ help
+ If you say yes here you get support for the Advantech EIO
+ temperature, voltage and fan speed monitoring block.
+
+ This driver can also be built as a module. If so, the module
+ will be called eio-hwmon
+
config SENSORS_EMC1403
tristate "SMSC EMC1403/23 thermal sensor"
depends on I2C
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index eade8e3b1bde..e69f03b41fae 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -72,6 +72,7 @@ obj-$(CONFIG_SENSORS_DME1737) += dme1737.o
obj-$(CONFIG_SENSORS_DRIVETEMP) += drivetemp.o
obj-$(CONFIG_SENSORS_DS620) += ds620.o
obj-$(CONFIG_SENSORS_DS1621) += ds1621.o
+obj-$(CONFIG_SENSORS_EIO) += eio-hwmon.o
obj-$(CONFIG_SENSORS_EMC1403) += emc1403.o
obj-$(CONFIG_SENSORS_EMC2103) += emc2103.o
obj-$(CONFIG_SENSORS_EMC2305) += emc2305.o
diff --git a/drivers/hwmon/eio-hwmon.c b/drivers/hwmon/eio-hwmon.c
new file mode 100644
index 000000000000..164591aa31a7
--- /dev/null
+++ b/drivers/hwmon/eio-hwmon.c
@@ -0,0 +1,344 @@
+// 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/hwmon.h>
+#include <linux/hwmon-sysfs.h>
+#include <linux/mfd/core.h>
+#include <linux/mfd/eio.h>
+#include <linux/module.h>
+
+#define MAX_DEV 128
+#define MAX_NAME 32
+
+static uint timeout;
+module_param(timeout, uint, 0444);
+MODULE_PARM_DESC(timeout,
+ "Default pmc command timeout in micro-seconds.\n");
+
+struct eio_hwmon_dev {
+ struct device *mfd;
+};
+
+enum _sen_type {
+ NONE,
+ VOLTAGE,
+ CURRENT,
+ TEMP,
+ PWM,
+ TACHO,
+ FAN,
+ CASEOPEN,
+};
+
+struct eio_key {
+ enum _sen_type type;
+ u8 chan;
+ u8 item;
+ u8 label_idx;
+};
+
+struct eio_attr {
+ struct sensor_device_attribute sda;
+ struct eio_key key;
+};
+
+static struct {
+ u8 cmd;
+ u8 max;
+ signed int shift;
+ char name[32];
+ u8 ctrl[16];
+ u16 multi[16];
+ char item[16][32];
+ char labels[32][32];
+
+} sen_info[] = {
+ { 0x00, 0, 0, "none" },
+ { 0x12, 8, 0, "in",
+ { 0xFF, 0x10, 0x11, 0x12 },
+ { 1, 10, 10, 10 },
+ { "label", "input", "max", "min" },
+ { "5V", "5Vs5", "12V", "12Vs5",
+ "3V3", "3V3", "5Vsb", "3Vsb",
+ "Vcmos", "Vbat", "Vdc", "Vstb",
+ "Vcore_a", "Vcore_b", "", "",
+ "Voem0", "Voem1", "Voem2", "Voem3"
+ },
+ },
+ { 0x1a, 2, 0, "curr",
+ { 0xFF, 0x10, 0x11, 0x12 },
+ { 1, 10, 10, 10 },
+ { "label", "input", "max", "min" },
+ { "dc", "oem0" },
+ },
+ { 0x10, 4, -2731, "temp",
+ { 0xFF, 0x10, 0x11, 0x12, 0x21, 0x41 },
+ { 1, 100, 100, 100, 100, 100 },
+ { "label", "input", "max", "min", "crit", "emergency" },
+ { "cpu0", "cpu1", "cpu2", "cpu3",
+ "sys0", "sys1", "sys2", "sys3",
+ "aux0", "aux1", "aux2", "aux3",
+ "dimm0", "dimm1", "dimm2", "dimm3",
+ "pch", "gpu", "", "",
+ "", "", "", "",
+ "", "", "", "",
+ "oem0", "oem1", "oem", "oem3" },
+ },
+ { 0x14, 0, 0, "pwm",
+ { 0xFF, 0x11, 0x12 },
+ { 1, 1, 1 },
+ { "label", "polarity", "freq" },
+ { "pwm0", "pwm0", "pwm0", "pwm0" },
+ },
+ { 0x16, 2, 0, "tacho",
+ { 0xFF, 0x10 },
+ { 1, 1 },
+ { "label", "input"},
+ { "cpu0", "cpu1", "cpu2", "cpu3",
+ "sys0", "sys1", "sys2", "sys3",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "",
+ "oem0", "oem1", "oem2", "oem3"
+ },
+ },
+ { 0x24, 4, 0, "fan",
+ { 0xFF, 0x1A },
+ { 1, 1 },
+ { "label", "input"},
+ { "cpu0", "cpu1", "cpu2", "cpu3",
+ "sys0", "sys1", "sys2", "sys3",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "", "", "", "", "",
+ "", "", "", "",
+ "oem0", "oem1", "oem2", "oem3",
+ },
+ },
+ { 0x28, 1, 0, "intrusion",
+ { 0xFF, 0x02 },
+ { 1, 1 },
+ { "label", "input" },
+ { "case_open" }
+ }
+};
+
+static struct {
+ enum _sen_type type;
+ u8 ctrl;
+ int size;
+ bool write;
+
+} ctrl_para[] = {
+ { NONE, 0x00, 0, false },
+
+ { VOLTAGE, 0x00, 1, false }, { VOLTAGE, 0x01, 1, false },
+ { VOLTAGE, 0x10, 2, false }, { VOLTAGE, 0x11, 2, false },
+ { VOLTAGE, 0x12, 2, false },
+
+ { CURRENT, 0x00, 1, false }, { CURRENT, 0x01, 1, false },
+ { CURRENT, 0x10, 2, false }, { CURRENT, 0x11, 2, false },
+ { CURRENT, 0x12, 2, false },
+
+ { TEMP, 0x00, 2, false }, { TEMP, 0x01, 1, false },
+ { TEMP, 0x04, 1, false }, { TEMP, 0x10, 2, false },
+ { TEMP, 0x11, 2, false }, { TEMP, 0x12, 2, false },
+ { TEMP, 0x21, 2, false }, { TEMP, 0x41, 2, false },
+
+ { PWM, 0x00, 1, false }, { PWM, 0x10, 1, true },
+ { PWM, 0x11, 1, true }, { PWM, 0x12, 4, true },
+
+ { TACHO, 0x00, 1, false }, { TACHO, 0x01, 1, false },
+ { TACHO, 0x10, 4, true },
+
+ { FAN, 0x00, 1, false }, { FAN, 0x01, 1, false },
+ { FAN, 0x03, 1, true }, { FAN, 0x1A, 2, false },
+
+ { CASEOPEN, 0x00, 1, false }, { CASEOPEN, 0x02, 1, true },
+};
+
+static int para_idx(enum _sen_type type, u8 ctrl)
+{
+ int i;
+
+ for (i = 1 ; i < ARRAY_SIZE(ctrl_para) ; i++)
+ if (type == ctrl_para[i].type &&
+ ctrl == ctrl_para[i].ctrl)
+ return i;
+
+ return 0;
+}
+
+static int pmc_read(struct device *mfd, enum _sen_type type, u8 dev_id, u8 ctrl, void *data)
+{
+ int idx = para_idx(type, ctrl);
+ int ret = 0;
+
+ if (idx == 0)
+ return -EINVAL;
+
+ if (WARN_ON(!data))
+ return -EINVAL;
+
+ struct pmc_op op = {
+ .cmd = sen_info[type].cmd | EIO_FLAG_PMC_READ,
+ .control = ctrl,
+ .device_id = dev_id,
+ .size = ctrl_para[idx].size,
+ .payload = (u8 *)data,
+ .timeout = timeout,
+ };
+
+ ret = eio_core_pmc_operation(mfd, &op);
+ return ret;
+}
+
+static ssize_t eio_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct eio_hwmon_dev *eio_hwmon = dev_get_drvdata(dev);
+ struct eio_attr *eio_attr =
+ container_of(attr, struct eio_attr, sda.dev_attr);
+ const struct eio_key *eio_key = &eio_attr->key;
+ int ret;
+ u8 data[2];
+ u32 temp_val;
+ signed int final_val;
+
+ switch (eio_key->item) {
+ case 0:
+ return sysfs_emit(buf, "%s\n",
+ sen_info[eio_key->type].labels[eio_key->label_idx]);
+
+ default:
+ ret = pmc_read(eio_hwmon->mfd, eio_key->type, eio_key->chan,
+ sen_info[eio_key->type].ctrl[eio_key->item],
+ &data);
+ if (ret)
+ return ret;
+
+ temp_val = data[0] | data[1] << 8;
+
+ final_val = (signed int)temp_val + (signed int)(sen_info[eio_key->type].shift);
+ final_val = final_val * (signed int)sen_info[eio_key->type].multi[eio_key->item];
+
+ return sysfs_emit(buf, "%d\n", final_val);
+ }
+
+ return -EINVAL;
+}
+
+static char devname[MAX_DEV][MAX_NAME];
+static struct eio_attr devattrs[MAX_DEV];
+static struct attribute *attrs[MAX_DEV];
+
+static struct attribute_group group = {
+ .attrs = attrs,
+};
+
+static const struct attribute_group *groups[] = {
+ &group,
+ NULL
+};
+
+static int hwmon_init(struct device *mfd, struct eio_hwmon_dev *eio_hwmon)
+{
+ enum _sen_type type;
+ u8 i, j, data[16];
+ int sum = 0;
+ int ret;
+
+ for (type = VOLTAGE ; type <= CASEOPEN ; type++) {
+ int cnt = 1;
+
+ for (i = 0 ; i < sen_info[type].max ; i++) {
+ if (pmc_read(mfd, type, i, 0x00, data) ||
+ (data[0] & 0x01) == 0)
+ continue;
+
+ memset(data, 0, sizeof(data));
+ ret = pmc_read(mfd, type, i, 0x01, data);
+ if (ret != 0 && ret != -EINVAL) {
+ dev_info(mfd, "read type id error\n");
+ continue;
+ }
+
+ for (j = 0 ; j < ARRAY_SIZE(sen_info->item) ; j++) {
+ struct eio_attr *eio_attr;
+
+ if (sen_info[type].item[j][0] == 0)
+ continue;
+
+ eio_attr = &devattrs[sum];
+
+ eio_attr->key.type = type;
+ eio_attr->key.chan = i;
+ eio_attr->key.item = j;
+ eio_attr->key.label_idx = data[0];
+
+ snprintf(devname[sum], sizeof(devname[sum]),
+ "%s%d_%s", sen_info[type].name, cnt,
+ sen_info[type].item[j]);
+
+ eio_attr->sda.dev_attr.attr.name = devname[sum];
+ eio_attr->sda.dev_attr.attr.mode = 0444;
+ eio_attr->sda.dev_attr.show = eio_show;
+
+ attrs[sum] = &eio_attr->sda.dev_attr.attr;
+
+ if (++sum >= MAX_DEV)
+ break;
+ }
+ cnt++;
+ }
+ }
+
+ return sum;
+}
+
+static int hwmon_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct eio_hwmon_dev *eio_hwmon;
+ struct eio_dev *eio_dev = dev_get_drvdata(dev->parent);
+ struct device *hwmon;
+
+ if (!eio_dev) {
+ dev_err(dev, "Error contact eio_core\n");
+ return -ENODEV;
+ }
+
+ eio_hwmon = devm_kzalloc(dev, sizeof(*eio_hwmon), GFP_KERNEL);
+ if (!eio_hwmon)
+ return -ENOMEM;
+
+ eio_hwmon->mfd = dev->parent;
+ platform_set_drvdata(pdev, eio_hwmon);
+
+ if (hwmon_init(dev->parent, eio_hwmon) <= 0)
+ return -ENODEV;
+
+ hwmon = devm_hwmon_device_register_with_groups(dev, KBUILD_MODNAME,
+ eio_hwmon,
+ groups);
+ return PTR_ERR_OR_ZERO(hwmon);
+}
+
+static struct platform_driver eio_hwmon_driver = {
+ .probe = hwmon_probe,
+ .driver = {
+ .name = "eio_hwmon",
+ },
+};
+
+module_platform_driver(eio_hwmon_driver);
+
+MODULE_AUTHOR("Wenkai Chung <wenkai.chung@advantech.com.tw>");
+MODULE_AUTHOR("Ramiro Oliveira <ramiro.oliveira@advantech.com>");
+MODULE_DESCRIPTION("Hardware monitor driver for Advantech EIO embedded controller");
+MODULE_LICENSE("GPL");
+
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox