* [RFC PATCH v2.1 0/2] mm/damon/stat: add kdamond_pid parameter
From: SeongJae Park @ 2026-04-30 14:20 UTC (permalink / raw)
Cc: SeongJae Park, Liam R. Howlett, Andrew Morton, David Hildenbrand,
Jonathan Corbet, Lorenzo Stoakes, Michal Hocko, Mike Rapoport,
Shuah Khan, Suren Baghdasaryan, Vlastimil Babka, damon, linux-doc,
linux-kernel, linux-mm
DAMON_STAT doesn't provide the pid of its kdamond, unlike DAMON_RECLAIM
and DAMON_LRU_SORT. This makes user-space management of DAMON_STAT
unnecessarily complicated. Provide the information via a new parameter,
namely kdamond_pid, and document it.
Changes from RFC v2
- v2: https://lore.kernel.org/20260425203309.108879-1-sj@kernel.org
- Rebase to latest mm-new.
Changes from RFC v1.2
- rfc v1.2: https://lore.kernel.org/20260416002149.87090-1-sj@kernel.org
- Detect and use fresh kdamond pid.
Changes from RFC v1.1
- rfc v1.1: https://lore.kernel.org/20260414235912.98174-1-sj@kernel.org
- Close the parentheses of error handling block.
Changes from RFC
- rfc: https://lore.kernel.org/20260414053742.90296-1-sj@kernel.org
- Fix damon_kdamond_pid() failure handling.
SeongJae Park (2):
mm/damon/stat: add a parameter for reading kdamond pid
Docs/admin-guide/mm/damon/stat: document kdamond_pid parameter
Documentation/admin-guide/mm/damon/stat.rst | 7 ++++
mm/damon/stat.c | 38 +++++++++++++++++++++
2 files changed, 45 insertions(+)
base-commit: b8e00b62c650b2aa471195f663301118fde889c2
--
2.47.3
^ permalink raw reply
* Re: [PATCH v3 2/2] slab: fix kernel-docs for mm-api
From: Marco Elver @ 2026-04-30 13:59 UTC (permalink / raw)
To: Vlastimil Babka (SUSE)
Cc: Andrew Morton, Jonathan Corbet, Nathan Chancellor, Nicolas Schier,
Dennis Zhou, Tejun Heo, Christoph Lameter, Harry Yoo, Hao Li,
David Rientjes, Roman Gushchin, Kees Cook, Gustavo A. R. Silva,
David Hildenbrand, Lorenzo Stoakes, Liam R. Howlett,
Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Alexander Potapenko, Dmitry Vyukov, Nick Desaulniers,
Bill Wendling, Justin Stitt, Miguel Ojeda, linux-kbuild,
linux-kernel, linux-mm, linux-hardening, kasan-dev, llvm,
linux-doc@vger.kernel.org
In-Reply-To: <9c321184-9080-4d5c-bd1a-a16cd0bbaed3@kernel.org>
On Thu, 30 Apr 2026 at 15:40, Vlastimil Babka (SUSE) <vbabka@kernel.org> wrote:
>
> On 4/24/26 15:24, Marco Elver wrote:
> > The mm-api kernel-doc comments have been broken for a while, as many
> > documented symbols shifted from being direct function definitions to
> > macros wrapping _noprof implementations during the introduction of
> > allocation tagging (starting with commit 7bd230a26648 "mm/slab: enable
> > slab allocation tagging for kmalloc and friends").
> >
> > When the kernel-doc block remains above the internal implementation
> > function but uses the public API name, the documentation generator fails
> > to associate the documented symbol and generates warnings and fails to
> > emit the documentation.
> >
> > Fix this by:
> >
> > 1. Moving the kernel-doc comment blocks from slub.c to slab.h, placing
> > them directly above the user-facing macros.
> >
> > 2. Converting the variadic macros for the documented APIs to use
> > explicit arguments.
> >
> > No functional change intended.
> >
> > Signed-off-by: Marco Elver <elver@google.com>
>
> +Cc Jon
>
> I thought it was supposed to work because the kernel-doc scripts were at the
> time taught by commit 51a7bf0238c2 ("scripts/kernel-doc: drop "_noprof" on
> function prototypes") to handle _noprof. In the current form git grep finds:
>
> tools/lib/python/kdoc/kdoc_parser.py: suffixes = [ '_noprof' ]
> tools/lib/python/kdoc/xforms_lists.py: (KernRe("_noprof"), ""),
>
> Doesn't it work for you then?
Ah, I see. So it doesn't work anymore because we add the '_' prefix, too.
I guess the question is if we want to proliferate more kdoc parser
special cases, or just move the docs to the macros. The downside of
macros is that they lose the types in the displayed function
signature.
Preferences?
^ permalink raw reply
* Re: [PATCH v3 2/2] slab: fix kernel-docs for mm-api
From: Vlastimil Babka (SUSE) @ 2026-04-30 13:40 UTC (permalink / raw)
To: Marco Elver, Andrew Morton, Jonathan Corbet
Cc: Nathan Chancellor, Nicolas Schier, Dennis Zhou, Tejun Heo,
Christoph Lameter, Harry Yoo, Hao Li, David Rientjes,
Roman Gushchin, Kees Cook, Gustavo A. R. Silva, David Hildenbrand,
Lorenzo Stoakes, Liam R. Howlett, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Alexander Potapenko,
Dmitry Vyukov, Nick Desaulniers, Bill Wendling, Justin Stitt,
Miguel Ojeda, linux-kbuild, linux-kernel, linux-mm,
linux-hardening, kasan-dev, llvm, linux-doc@vger.kernel.org
In-Reply-To: <20260424132427.2703076-2-elver@google.com>
On 4/24/26 15:24, Marco Elver wrote:
> The mm-api kernel-doc comments have been broken for a while, as many
> documented symbols shifted from being direct function definitions to
> macros wrapping _noprof implementations during the introduction of
> allocation tagging (starting with commit 7bd230a26648 "mm/slab: enable
> slab allocation tagging for kmalloc and friends").
>
> When the kernel-doc block remains above the internal implementation
> function but uses the public API name, the documentation generator fails
> to associate the documented symbol and generates warnings and fails to
> emit the documentation.
>
> Fix this by:
>
> 1. Moving the kernel-doc comment blocks from slub.c to slab.h, placing
> them directly above the user-facing macros.
>
> 2. Converting the variadic macros for the documented APIs to use
> explicit arguments.
>
> No functional change intended.
>
> Signed-off-by: Marco Elver <elver@google.com>
+Cc Jon
I thought it was supposed to work because the kernel-doc scripts were at the
time taught by commit 51a7bf0238c2 ("scripts/kernel-doc: drop "_noprof" on
function prototypes") to handle _noprof. In the current form git grep finds:
tools/lib/python/kdoc/kdoc_parser.py: suffixes = [ '_noprof' ]
tools/lib/python/kdoc/xforms_lists.py: (KernRe("_noprof"), ""),
Doesn't it work for you then?
^ permalink raw reply
* Re: [PATCH v13 3/4] gpio: rpmsg: add generic rpmsg GPIO driver
From: Beleswar Prasad Padhi @ 2026-04-30 12:56 UTC (permalink / raw)
To: Arnaud POULIQUEN, Mathieu Poirier
Cc: Shenwei Wang, Andrew Lunn, Linus Walleij, Bartosz Golaszewski,
Jonathan Corbet, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Bjorn Andersson, Frank Li, Sascha Hauer, Shuah Khan,
linux-gpio@vger.kernel.org, linux-doc@vger.kernel.org,
linux-kernel@vger.kernel.org, Pengutronix Kernel Team,
Fabio Estevam, Peng Fan, devicetree@vger.kernel.org,
linux-remoteproc@vger.kernel.org, imx@lists.linux.dev,
linux-arm-kernel@lists.infradead.org, dl-linux-imx,
Bartosz Golaszewski
In-Reply-To: <f7ef3417-eb84-4467-ac72-a9bc8b0c81e8@foss.st.com>
Hello Arnaud,
On 30/04/26 13:05, Arnaud POULIQUEN wrote:
> Hello,
>
> On 4/29/26 21:20, Mathieu Poirier wrote:
>> On Wed, 29 Apr 2026 at 12:07, Padhi, Beleswar <b-padhi@ti.com> wrote:
>>>
>>> Hi Mathieu,
>>>
>>> On 4/29/2026 11:03 PM, Mathieu Poirier wrote:
>>>> On Wed, 29 Apr 2026 at 10:53, Shenwei Wang <shenwei.wang@nxp.com> wrote:
>>>>>
>>>>>
>>>>>> -----Original Message-----
>>>>>> From: Mathieu Poirier <mathieu.poirier@linaro.org>
>>>>>> Sent: Wednesday, April 29, 2026 10:42 AM
>>>>>> To: Shenwei Wang <shenwei.wang@nxp.com>
>>>>>> Cc: Andrew Lunn <andrew@lunn.ch>; Padhi, Beleswar <b-padhi@ti.com>; Linus
>>>>>> Walleij <linusw@kernel.org>; Bartosz Golaszewski <brgl@kernel.org>; Jonathan
>>>>>> Corbet <corbet@lwn.net>; Rob Herring <robh@kernel.org>; Krzysztof Kozlowski
>>>>>> <krzk+dt@kernel.org>; Conor Dooley <conor+dt@kernel.org>; Bjorn Andersson
>>>>>> <andersson@kernel.org>; Frank Li <frank.li@nxp.com>; Sascha Hauer
>>>>>> <s.hauer@pengutronix.de>; Shuah Khan <skhan@linuxfoundation.org>; linux-
>>>>>> gpio@vger.kernel.org; linux-doc@vger.kernel.org; linux-kernel@vger.kernel.org;
>>>>>> Pengutronix Kernel Team <kernel@pengutronix.de>; Fabio Estevam
>>>>>> <festevam@gmail.com>; Peng Fan <peng.fan@nxp.com>;
>>>>>> devicetree@vger.kernel.org; linux-remoteproc@vger.kernel.org;
>>>>>> imx@lists.linux.dev; linux-arm-kernel@lists.infradead.org; dl-linux-imx <linux-
>>>>>> imx@nxp.com>; Bartosz Golaszewski <brgl@bgdev.pl>
>>>>>> Subject: [EXT] Re: [PATCH v13 3/4] gpio: rpmsg: add generic rpmsg GPIO driver
>>>>>> On Tue, Apr 28, 2026 at 03:24:59PM +0000, Shenwei Wang wrote:
>>>>>>>
>>>>>>>> -----Original Message-----
>>>>>>>> From: Andrew Lunn <andrew@lunn.ch>
>>>>>>>> Sent: Monday, April 27, 2026 3:49 PM
>>>>>>>> To: Shenwei Wang <shenwei.wang@nxp.com>
>>>>>>>> Cc: Padhi, Beleswar <b-padhi@ti.com>; Linus Walleij
>>>>>>>> <linusw@kernel.org>; Bartosz Golaszewski <brgl@kernel.org>; Jonathan
>>>>>>>> Corbet <corbet@lwn.net>; Rob Herring <robh@kernel.org>; Krzysztof
>>>>>>>> Kozlowski <krzk+dt@kernel.org>; Conor Dooley <conor+dt@kernel.org>;
>>>>>>>> Bjorn Andersson <andersson@kernel.org>; Mathieu Poirier
>>>>>>>> <mathieu.poirier@linaro.org>; Frank Li <frank.li@nxp.com>; Sascha
>>>>>>>> Hauer <s.hauer@pengutronix.de>; Shuah Khan
>>>>>>>> <skhan@linuxfoundation.org>; linux-gpio@vger.kernel.org; linux-
>>>>>>>> doc@vger.kernel.org; linux-kernel@vger.kernel.org; Pengutronix
>>>>>>>> Kernel Team <kernel@pengutronix.de>; Fabio Estevam
>>>>>>>> <festevam@gmail.com>; Peng Fan <peng.fan@nxp.com>;
>>>>>>>> devicetree@vger.kernel.org; linux- remoteproc@vger.kernel.org;
>>>>>>>> imx@lists.linux.dev; linux-arm- kernel@lists.infradead.org;
>>>>>>>> dl-linux-imx <linux-imx@nxp.com>; Bartosz Golaszewski
>>>>>>>> <brgl@bgdev.pl>
>>>>>>>> Subject: [EXT] Re: [PATCH v13 3/4] gpio: rpmsg: add generic rpmsg
>>>>>>>> GPIO driver
>>>>>>>>>> struct virtio_gpio_response {
>>>>>>>>>> __u8 status;
>>>>>>>>>> __u8 value;
>>>>>>>>>> };
>>>>>>>>> It is the same message format. Please see the message definition
>>>>>>>> (GET_DIRECTION) below:
>>>>>>>>
>>>>>>>>> + +-----+-----+-----+-----+-----+----+
>>>>>>>>> + |0x00 |0x01 |0x02 |0x03 |0x04 |0x05|
>>>>>>>>> + | 1 | 2 |port |line | err | dir|
>>>>>>>>> + +-----+-----+-----+-----+-----+----+
>>>>>>>> Sorry, but i don't see how two u8 vs six u8 are the same message format.
>>>>>>>>
>>>>>>> Some changes to the message format are necessary.
>>>>>>>
>>>>>>> Virtio uses two communication channels (virtqueues): one for requests and
>>>>>> replies, and a second one for events.
>>>>>>> In contrast, rpmsg provides only a single communication channel, so a
>>>>>>> type field is required to distinguish between different kinds of messages.
>>>>>>>
>>>>>>> Since rpmsg replies and events share the same message format, an additional
>>>>>> line is introduced to handle both cases.
>>>>>>> Finally, rpmsg supports multiple GPIO controllers, so a port field is added to
>>>>>> uniquely identify the target controller.
>>>>>>
>>>>>> I have commented on this before - RPMSG is already providing multiplexing
>>>>>> capability by way of endpoints. There is no need for a port field. One endpoint,
>>>>>> one GPIO controller.
>>>>>>
>>>>> You still need a way to let the remote side know which port the endpoint maps to, either
>>>>> by embedding the port information in the message (the current way), or by sending it
>>>>> separately.
>>>>>
>>>> An endpoint is created with every namespace request. There should be
>>>> one namespace request for every GPIO controller, which yields a unique
>>>> endpoint for each controller and eliminates the need for an extra
>>>> field to identify them.
>>>
>>>
>>> Right, but this can still be done by just having one namespace request.
>>> We can create new endpoints bound to an existing namespace/channel by
>>> invoking rpmsg_create_ept(). This is what I suggested here too:
>>> https://lore.kernel.org/all/29485742-6e49-482e-b73d-228295daaeec@ti.com/
>>>
>>
>> I will look at your suggestion (i.e link above) later this week or next week.
>>
>>> My mental model looks like this for the complete picture:
>>>
>>> 1. namespace/channel#1 = rpmsg-io
>>> a. ept1 -> gpio-controller@1
>>> b. ept2 -> gpio-controller@2
>>>
>>
>> I've asked for one endpoint per GPIO controller since the very
>> beginning. I don't yet have a strong opinion on whether to use one
>> namespace request per GPIO controller or a single request that spins
>> off multiple endpoints. I'll have to look at your link and reflect on
>> that. Regardless of how we proceed on that front, multiplexing needs
>> to happen at the endpoint level rather than the packet level. This is
>> the only way this work can move forward.
>>
>
> I would be more in favor of Mathieu’s proposal: “An endpoint is created with every namespace request.”
>
> If the endpoint is created only on the Linux side, how do we match the Linux endpoint address with the local port field on the remote side?
Simply by sending a message to the remote containing the newly created
endpoint and the port idx. Note that is this done just one time, after this
Linux need not have the port field in the message everytime its sending
a message.
>
> With a multi-namespace approach, the namespace could be rpmsg-io-[addr], where [addr] corresponds to the GPIO controller address in the DT. This would:
You will face the same problem in this case also that you asked above:
"how do we match the Linux endpoint address with the local port field
on the remote side?"
Because the endpoint that is created on a namespace request is also
dynamic in nature. How will the remote know which endpoint addr
Linux allocated for a namespace that it announced?
As an example/PoC, I created a firmware example which announces
2 name services to Linux, one is the standard "rpmsg_chrdev" and
the other is a TI specific name service "ti.ipc4.ping-pong". You can
see it created 2 different addresses (0x400 and 0x401) for each of
the name service request from the same firmware:
root@j784s4-evm:~# dmesg | grep virtio0 | grep -i channel
[ 9.290275] virtio_rpmsg_bus virtio0: creating channel ti.ipc4.ping-pong addr 0xd
[ 9.311230] virtio_rpmsg_bus virtio0: creating channel rpmsg_chrdev addr 0xe
[ 9.496645] rpmsg_chrdev virtio0.rpmsg_chrdev.-1.14: DEBUG: Channel formed from src = 0x400 to dst = 0xe
[ 9.707255] rpmsg_client_sample virtio0.ti.ipc4.ping-pong.-1.13: new channel: 0x401 -> 0xd!
So in this case, rpmsg-io-1 can have different ept addr than rpmsg-io-2
Back to same problem. Simple solution is to reply to remote with the
created ept addr and the index.
>
> - match the RPMsg probe with the DT,
We can probe from all controllers with a single name service
announcement too.
> - provide a simple mapping between the port and the endpoint on both sides,
We are trying to get rid of this mapping from Linux side to adapt
the gpio-virtio design.
> - allow multiple endpoints on the remote side,
We can support this as well with single nameservice model.
There is no limitation. Remote has to send a message with
its newly created ept that's all.
> - provide a simple discovery mechanism for remote capabilities.
A single announcement: "rpmsg-io" is also discovery mechanism.
Feel free to let me know if you have concerns with any of the
suggestions!
Thanks,
Beleswar
>
> Regards,
> Arnaud
>
>>> 2. namespace/channel#2 = rpmsg-i2c
>>> a. ept1 -> i2c@1
>>> b. ept2 -> i2c@2
>>> c. ept3 -> i2c@3
>>>
>>> etc...
>>>
>>> This way device groups are isolated with each channel/namespace, and
>>> instances within each device groups are also respected with specific
>>> endpoints.
>>>
>>> Thanks,
>>> Beleswar
>>>
>>
>
^ permalink raw reply
* htmldocs: Documentation/networking/device_drivers/ethernet/index.rst:10: WARNING: toctree contains reference to nonexisting document 'networking/device_drivers/ethernet/cirrus/cs89x0' [toc.not_readable]
From: kernel test robot @ 2026-04-30 12:18 UTC (permalink / raw)
To: Andrew Lunn; +Cc: oe-kbuild-all, 0day robot, linux-doc
tree: https://github.com/intel-lab-lkp/linux/commits/Andrew-Lunn/drivers-net-3com-3c509-Remove-this-driver/20260424-104110
head: 9223eb1499a6a614ff3744386dc2b772907adc68
commit: ece15657f74a8fe0c3cf05e2837534bb570a85bc drivers: net: cirrus: cs89x0: Remove this driver
date: 6 days ago
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
docutils: docutils (Docutils 0.21.2, Python 3.13.5, on linux)
reproduce: (https://download.01.org/0day-ci/archive/20260430/202604301418.xWBPHv08-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/202604301418.xWBPHv08-lkp@intel.com/
All warnings (new ones prefixed by >>):
Non-Preserved Properties
======================== [docutils]
Documentation/networking/device_drivers/ethernet/index.rst:10: WARNING: toctree contains reference to nonexisting document 'networking/device_drivers/ethernet/3com/3c509' [toc.not_readable]
>> Documentation/networking/device_drivers/ethernet/index.rst:10: WARNING: toctree contains reference to nonexisting document 'networking/device_drivers/ethernet/cirrus/cs89x0' [toc.not_readable]
Documentation/networking/device_drivers/ethernet/index.rst:10: WARNING: toctree contains reference to nonexisting document 'networking/device_drivers/ethernet/smsc/smc9' [toc.not_readable]
Documentation/networking/skbuff:36: ./include/linux/skbuff.h:48: ERROR: Unexpected section title.
vim +10 Documentation/networking/device_drivers/ethernet/index.rst
132db93572821e Jakub Kicinski 2020-06-26 7
132db93572821e Jakub Kicinski 2020-06-26 8 Contents:
132db93572821e Jakub Kicinski 2020-06-26 9
132db93572821e Jakub Kicinski 2020-06-26 @10 .. toctree::
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* Re: [PATCH net-next 2/2] dpll: zl3073x: implement pin operational state reporting
From: Petr Oros @ 2026-04-30 11:58 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Arkadiusz Kubalewski, David S. Miller, Donald Hunter,
Eric Dumazet, Jakub Kicinski, Jiri Pirko, Jonathan Corbet,
Michal Schmidt, Paolo Abeni, Pasi Vaananen, Prathosh Satish,
Shuah Khan, Simon Horman, Vadim Fedorenko, linux-doc,
linux-kernel
In-Reply-To: <20260428154907.2820654-3-ivecera@redhat.com>
On 4/28/26 17:49, Ivan Vecera wrote:
> Implement operstate_on_dpll_get callback for input pins to report
> the actual hardware status:
>
> - active: pin is the currently locked reference
> - standby: signal is valid but pin is not actively used
> - no-signal: reference monitor reports Loss of Signal (LOS)
> - qual-failed: reference monitor reports a qualification failure
> (SCM, CFM, GST, PFM, eSync or Split-XO)
>
> Separate administrative state (state_on_dpll_get) from operational
> state: admin state now reports purely the user-requested intent
> (connected in reflock mode, selectable in auto mode).
>
> Switch periodic monitoring to track operstate changes instead of
> the mixed admin/oper state that was previously reported.
>
> Add ref_mon_status bit definitions to regs.h.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> drivers/dpll/zl3073x/dpll.c | 108 ++++++++++++++++++++++++------------
> drivers/dpll/zl3073x/regs.h | 9 ++-
> 2 files changed, 79 insertions(+), 38 deletions(-)
>
> diff --git a/drivers/dpll/zl3073x/dpll.c b/drivers/dpll/zl3073x/dpll.c
> index c95e93ef3ab04..6fd718696de0d 100644
> --- a/drivers/dpll/zl3073x/dpll.c
> +++ b/drivers/dpll/zl3073x/dpll.c
> @@ -38,7 +38,7 @@
> * @prio: pin priority <0, 14>
> * @esync_control: embedded sync is controllable
> * @phase_gran: phase adjustment granularity
> - * @pin_state: last saved pin state
> + * @operstate: last saved operational state
> * @phase_offset: last saved pin phase offset
> * @freq_offset: last saved fractional frequency offset
> * @measured_freq: last saved measured frequency
> @@ -55,7 +55,7 @@ struct zl3073x_dpll_pin {
> u8 prio;
> bool esync_control;
> s32 phase_gran;
> - enum dpll_pin_state pin_state;
> + enum dpll_pin_operstate operstate;
> s64 phase_offset;
> s64 freq_offset;
> u32 measured_freq;
> @@ -500,46 +500,41 @@ zl3073x_dpll_input_pin_phase_adjust_set(const struct dpll_pin *dpll_pin,
> }
>
> /**
> - * zl3073x_dpll_ref_state_get - get status for given input pin
> + * zl3073x_dpll_ref_operstate_get - get operational state for input pin
> * @pin: pointer to pin
> - * @state: place to store status
> + * @operstate: place to store operational state
> *
> - * Checks current status for the given input pin and stores the value
> - * to @state.
> + * Returns the actual hardware state of the pin: whether it is actively
> + * used by the DPLL, has no signal, failed qualification, or is simply
> + * not in use.
> *
> * Return: 0 on success, <0 on error
> */
> static int
> -zl3073x_dpll_ref_state_get(struct zl3073x_dpll_pin *pin,
> - enum dpll_pin_state *state)
> +zl3073x_dpll_ref_operstate_get(struct zl3073x_dpll_pin *pin,
> + enum dpll_pin_operstate *operstate)
> {
> struct zl3073x_dpll *zldpll = pin->dpll;
> struct zl3073x_dev *zldev = zldpll->dev;
> - const struct zl3073x_chan *chan;
> - u8 ref;
> -
> - chan = zl3073x_chan_state_get(zldev, zldpll->id);
> - ref = zl3073x_input_pin_ref_get(pin->id);
> + const struct zl3073x_ref *ref;
> + u8 ref_id;
>
> - /* Check if the pin reference is connected */
> - if (ref == zl3073x_dpll_connected_ref_get(zldpll)) {
> - *state = DPLL_PIN_STATE_CONNECTED;
> - return 0;
> - }
> + ref_id = zl3073x_input_pin_ref_get(pin->id);
>
> - /* If the DPLL is running in automatic mode and the reference is
> - * selectable and its monitor does not report any error then report
> - * pin as selectable.
> - */
> - if (zl3073x_chan_mode_get(chan) == ZL_DPLL_MODE_REFSEL_MODE_AUTO &&
> - zl3073x_dev_ref_is_status_ok(zldev, ref) &&
> - zl3073x_chan_ref_is_selectable(chan, ref)) {
> - *state = DPLL_PIN_STATE_SELECTABLE;
> + /* Check if this pin is the currently locked reference */
> + if (ref_id == zl3073x_dpll_connected_ref_get(zldpll)) {
> + *operstate = DPLL_PIN_OPERSTATE_ACTIVE;
> return 0;
> }
>
> - /* Otherwise report the pin as disconnected */
> - *state = DPLL_PIN_STATE_DISCONNECTED;
> + /* Check reference monitor status */
> + ref = zl3073x_ref_state_get(zldev, ref_id);
> + if (ref->mon_status & ZL_REF_MON_STATUS_LOS)
> + *operstate = DPLL_PIN_OPERSTATE_NO_SIGNAL;
> + else if (!zl3073x_ref_is_status_ok(ref))
> + *operstate = DPLL_PIN_OPERSTATE_QUAL_FAILED;
> + else
> + *operstate = DPLL_PIN_OPERSTATE_STANDBY;
>
> return 0;
> }
> @@ -551,10 +546,48 @@ zl3073x_dpll_input_pin_state_on_dpll_get(const struct dpll_pin *dpll_pin,
> void *dpll_priv,
> enum dpll_pin_state *state,
> struct netlink_ext_ack *extack)
> +{
> + struct zl3073x_dpll *zldpll = dpll_priv;
> + struct zl3073x_dpll_pin *pin = pin_priv;
> + const struct zl3073x_chan *chan;
> + u8 mode, ref;
> +
> + chan = zl3073x_chan_state_get(zldpll->dev, zldpll->id);
> + ref = zl3073x_input_pin_ref_get(pin->id);
> + mode = zl3073x_chan_mode_get(chan);
> +
> + switch (mode) {
> + case ZL_DPLL_MODE_REFSEL_MODE_REFLOCK:
> + if (ref == zl3073x_chan_ref_get(chan))
> + *state = DPLL_PIN_STATE_CONNECTED;
> + else
> + *state = DPLL_PIN_STATE_DISCONNECTED;
> + break;
> + case ZL_DPLL_MODE_REFSEL_MODE_AUTO:
> + if (zl3073x_chan_ref_is_selectable(chan, ref))
> + *state = DPLL_PIN_STATE_SELECTABLE;
> + else
> + *state = DPLL_PIN_STATE_DISCONNECTED;
> + break;
> + default:
> + *state = DPLL_PIN_STATE_DISCONNECTED;
> + break;
> + }
> +
> + return 0;
> +}
> +
> +static int
> +zl3073x_dpll_input_pin_operstate_on_dpll_get(const struct dpll_pin *dpll_pin,
> + void *pin_priv,
> + const struct dpll_device *dpll,
> + void *dpll_priv,
> + enum dpll_pin_operstate *operstate,
> + struct netlink_ext_ack *extack)
> {
> struct zl3073x_dpll_pin *pin = pin_priv;
>
> - return zl3073x_dpll_ref_state_get(pin, state);
> + return zl3073x_dpll_ref_operstate_get(pin, operstate);
> }
>
> static int
> @@ -1248,6 +1281,7 @@ static const struct dpll_pin_ops zl3073x_dpll_input_pin_ops = {
> .frequency_get = zl3073x_dpll_input_pin_frequency_get,
> .frequency_set = zl3073x_dpll_input_pin_frequency_set,
> .measured_freq_get = zl3073x_dpll_input_pin_measured_freq_get,
> + .operstate_on_dpll_get = zl3073x_dpll_input_pin_operstate_on_dpll_get,
> .phase_offset_get = zl3073x_dpll_input_pin_phase_offset_get,
> .phase_adjust_get = zl3073x_dpll_input_pin_phase_adjust_get,
> .phase_adjust_set = zl3073x_dpll_input_pin_phase_adjust_set,
> @@ -1663,7 +1697,7 @@ zl3073x_dpll_pin_phase_offset_check(struct zl3073x_dpll_pin *pin)
> * 2) For other pins use appropriate ref_phase register if the phase
> * monitor feature is enabled.
> */
> - if (pin->pin_state == DPLL_PIN_STATE_CONNECTED)
> + if (pin->operstate == DPLL_PIN_OPERSTATE_ACTIVE)
> reg = ZL_REG_DPLL_PHASE_ERR_DATA(zldpll->id);
> else if (zldpll->phase_monitor)
> reg = ZL_REG_REF_PHASE(ref_id);
> @@ -1828,7 +1862,7 @@ zl3073x_dpll_changes_check(struct zl3073x_dpll *zldpll)
> }
>
> list_for_each_entry(pin, &zldpll->pins, list) {
> - enum dpll_pin_state state;
> + enum dpll_pin_operstate operstate;
> bool pin_changed = false;
>
> /* Output pins change checks are not necessary because output
> @@ -1837,18 +1871,18 @@ zl3073x_dpll_changes_check(struct zl3073x_dpll *zldpll)
> if (!zl3073x_dpll_is_input_pin(pin))
> continue;
>
> - rc = zl3073x_dpll_ref_state_get(pin, &state);
> + rc = zl3073x_dpll_ref_operstate_get(pin, &operstate);
> if (rc) {
> dev_err(dev,
> - "Failed to get %s on DPLL%u state: %pe\n",
> + "Failed to get %s on DPLL%u oper state: %pe\n",
> pin->label, zldpll->id, ERR_PTR(rc));
> return;
> }
>
> - if (state != pin->pin_state) {
> - dev_dbg(dev, "%s state changed: %u->%u\n", pin->label,
> - pin->pin_state, state);
> - pin->pin_state = state;
> + if (operstate != pin->operstate) {
> + dev_dbg(dev, "%s oper state changed: %u->%u\n",
> + pin->label, pin->operstate, operstate);
> + pin->operstate = operstate;
> pin_changed = true;
> }
>
> diff --git a/drivers/dpll/zl3073x/regs.h b/drivers/dpll/zl3073x/regs.h
> index d425dc67250fe..8015808bdf548 100644
> --- a/drivers/dpll/zl3073x/regs.h
> +++ b/drivers/dpll/zl3073x/regs.h
> @@ -98,7 +98,14 @@
>
> #define ZL_REG_REF_MON_STATUS(_idx) \
> ZL_REG_IDX(_idx, 2, 0x02, 1, ZL3073X_NUM_REFS, 1)
> -#define ZL_REF_MON_STATUS_OK 0 /* all bits zeroed */
> +#define ZL_REF_MON_STATUS_OK 0
> +#define ZL_REF_MON_STATUS_LOS BIT(0)
> +#define ZL_REF_MON_STATUS_SCM BIT(1)
> +#define ZL_REF_MON_STATUS_CFM BIT(2)
> +#define ZL_REF_MON_STATUS_GST BIT(3)
> +#define ZL_REF_MON_STATUS_PFM BIT(4)
> +#define ZL_REF_MON_STATUS_ESYNC BIT(6)
> +#define ZL_REF_MON_STATUS_SPLIT_XO BIT(7)
>
> #define ZL_REG_DPLL_MON_STATUS(_idx) \
> ZL_REG_IDX(_idx, 2, 0x10, 1, ZL3073X_MAX_CHANNELS, 1)
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply
* Re: [PATCH net-next 1/2] dpll: add pin operational state
From: Petr Oros @ 2026-04-30 11:58 UTC (permalink / raw)
To: Ivan Vecera, netdev
Cc: Arkadiusz Kubalewski, David S. Miller, Donald Hunter,
Eric Dumazet, Jakub Kicinski, Jiri Pirko, Jonathan Corbet,
Michal Schmidt, Paolo Abeni, Pasi Vaananen, Prathosh Satish,
Shuah Khan, Simon Horman, Vadim Fedorenko, linux-doc,
linux-kernel
In-Reply-To: <20260428154907.2820654-2-ivecera@redhat.com>
On 4/28/26 17:49, Ivan Vecera wrote:
> Add pin-operstate enum and operstate_on_dpll_get callback to report
> the actual hardware status of a pin with respect to its parent DPLL
> device. Unlike pin-state (which reflects administrative intent set
> by the user), operstate reflects what the hardware is actually doing.
>
> Defined operational states:
> - active: pin is qualified and actively used by the DPLL
> - standby: pin is qualified but not actively used by the DPLL
> - no-signal: pin does not have a valid signal
> - qual-failed: pin signal failed qualification
>
> The operstate is reported inside the pin-parent-device nested
> attribute alongside the existing state and phase-offset attributes.
>
> Signed-off-by: Ivan Vecera <ivecera@redhat.com>
> ---
> Documentation/driver-api/dpll.rst | 38 ++++++++++++++++-----------
> Documentation/netlink/specs/dpll.yaml | 31 ++++++++++++++++++++++
> drivers/dpll/dpll_netlink.c | 27 +++++++++++++++++++
> drivers/dpll/dpll_nl.c | 3 ++-
> drivers/dpll/dpll_nl.h | 2 +-
> include/linux/dpll.h | 6 +++++
> include/uapi/linux/dpll.h | 23 ++++++++++++++++
> 7 files changed, 113 insertions(+), 17 deletions(-)
>
> diff --git a/Documentation/driver-api/dpll.rst b/Documentation/driver-api/dpll.rst
> index 93c191b2d0898..37eaef785e304 100644
> --- a/Documentation/driver-api/dpll.rst
> +++ b/Documentation/driver-api/dpll.rst
> @@ -65,35 +65,43 @@ request, where user provides attributes that result in single pin match.
> Pin selection
> =============
>
> -In general, selected pin (the one which signal is driving the dpll
> -device) can be obtained from ``DPLL_A_PIN_STATE`` attribute, and only
> -one pin shall be in ``DPLL_PIN_STATE_CONNECTED`` state for any dpll
> -device.
> +Pin state (``DPLL_A_PIN_STATE``) reflects the administrative intent set
> +by the user. Pin operational state (``DPLL_A_PIN_OPERSTATE``) reflects
> +what the hardware is actually doing with the pin.
>
> Pin selection can be done either manually or automatically, depending
> on hardware capabilities and active dpll device work mode
> (``DPLL_A_MODE`` attribute). The consequence is that there are
> -differences for each mode in terms of available pin states, as well as
> -for the states the user can request for a dpll device.
> +differences for each mode in terms of available pin states the user can
> +request for a dpll device.
>
> -In manual mode (``DPLL_MODE_MANUAL``) the user can request or receive
> -one of following pin states:
> +In manual mode (``DPLL_MODE_MANUAL``) the user can request one of
> +following pin states:
>
> -- ``DPLL_PIN_STATE_CONNECTED`` - the pin is used to drive dpll device
> -- ``DPLL_PIN_STATE_DISCONNECTED`` - the pin is not used to drive dpll
> +- ``DPLL_PIN_STATE_CONNECTED`` - the pin is selected to drive dpll
> device
> +- ``DPLL_PIN_STATE_DISCONNECTED`` - the pin is not selected to drive
> + dpll device
>
> -In automatic mode (``DPLL_MODE_AUTOMATIC``) the user can request or
> -receive one of following pin states:
> +In automatic mode (``DPLL_MODE_AUTOMATIC``) the user can request one of
> +following pin states:
>
> - ``DPLL_PIN_STATE_SELECTABLE`` - the pin shall be considered as valid
> input for automatic selection algorithm
> - ``DPLL_PIN_STATE_DISCONNECTED`` - the pin shall be not considered as
> a valid input for automatic selection algorithm
>
> -In automatic mode (``DPLL_MODE_AUTOMATIC``) the user can only receive
> -pin state ``DPLL_PIN_STATE_CONNECTED`` once automatic selection
> -algorithm locks a dpll device with one of the inputs.
> +The actual hardware status of a pin is reported via the operational
> +state (``DPLL_A_PIN_OPERSTATE``) attribute nested under the parent
> +device:
> +
> +- ``DPLL_PIN_OPERSTATE_ACTIVE`` - pin is qualified and actively used
> + by the DPLL
> +- ``DPLL_PIN_OPERSTATE_STANDBY`` - pin is qualified but not actively
> + used by the DPLL
> +- ``DPLL_PIN_OPERSTATE_NO_SIGNAL`` - pin does not have a valid signal
> +- ``DPLL_PIN_OPERSTATE_QUAL_FAILED`` - pin signal failed qualification
> + checks
>
> Shared pins
> ===========
> diff --git a/Documentation/netlink/specs/dpll.yaml b/Documentation/netlink/specs/dpll.yaml
> index 40465a3d7fc20..c45de70a47ce6 100644
> --- a/Documentation/netlink/specs/dpll.yaml
> +++ b/Documentation/netlink/specs/dpll.yaml
> @@ -212,6 +212,27 @@ definitions:
> name: selectable
> doc: pin enabled for automatic input selection
> render-max: true
> + -
> + type: enum
> + name: pin-operstate
> + doc: |
> + defines possible operational states of a pin with respect to its
> + parent DPLL device, valid values for DPLL_A_PIN_OPERSTATE attribute
> + entries:
> + -
> + name: active
> + doc: pin is qualified and actively used by the DPLL
> + value: 1
> + -
> + name: standby
> + doc: pin is qualified but not actively used by the DPLL
> + -
> + name: no-signal
> + doc: pin does not have a valid signal
> + -
> + name: qual-failed
> + doc: pin signal failed qualification (e.g. frequency or phase monitor)
> + render-max: true
> -
> type: flags
> name: pin-capabilities
> @@ -488,6 +509,14 @@ attribute-sets:
> Value of (DPLL_A_PIN_MEASURED_FREQUENCY %
> DPLL_PIN_MEASURED_FREQUENCY_DIVIDER) is a fractional part
> of a measured frequency value.
> + -
> + name: operstate
> + type: u32
> + enum: pin-operstate
> + doc: |
> + Operational state of the pin with respect to its parent DPLL
> + device. Unlike state (which reflects the administrative intent),
> + operstate reflects the actual hardware status.
>
> -
> name: pin-parent-device
> @@ -501,6 +530,8 @@ attribute-sets:
> name: prio
> -
> name: state
> + -
> + name: operstate
> -
> name: phase-offset
> -
> diff --git a/drivers/dpll/dpll_netlink.c b/drivers/dpll/dpll_netlink.c
> index af7ce62ec55ca..05cf946b4be5e 100644
> --- a/drivers/dpll/dpll_netlink.c
> +++ b/drivers/dpll/dpll_netlink.c
> @@ -324,6 +324,30 @@ dpll_msg_add_pin_on_dpll_state(struct sk_buff *msg, struct dpll_pin *pin,
> return 0;
> }
>
> +static int
> +dpll_msg_add_pin_operstate(struct sk_buff *msg, struct dpll_pin *pin,
> + struct dpll_pin_ref *ref,
> + struct netlink_ext_ack *extack)
> +{
> + const struct dpll_pin_ops *ops = dpll_pin_ops(ref);
> + struct dpll_device *dpll = ref->dpll;
> + enum dpll_pin_operstate operstate;
> + int ret;
> +
> + if (!ops->operstate_on_dpll_get)
> + return 0;
> + ret = ops->operstate_on_dpll_get(pin,
> + dpll_pin_on_dpll_priv(dpll, pin),
> + dpll, dpll_priv(dpll),
> + &operstate, extack);
> + if (ret)
> + return ret;
> + if (nla_put_u32(msg, DPLL_A_PIN_OPERSTATE, operstate))
> + return -EMSGSIZE;
> +
> + return 0;
> +}
> +
> static int
> dpll_msg_add_pin_direction(struct sk_buff *msg, struct dpll_pin *pin,
> struct dpll_pin_ref *ref,
> @@ -650,6 +674,9 @@ dpll_msg_add_pin_dplls(struct sk_buff *msg, struct dpll_pin *pin,
> if (ret)
> goto nest_cancel;
> ret = dpll_msg_add_pin_on_dpll_state(msg, pin, ref, extack);
> + if (ret)
> + goto nest_cancel;
> + ret = dpll_msg_add_pin_operstate(msg, pin, ref, extack);
> if (ret)
> goto nest_cancel;
> ret = dpll_msg_add_pin_prio(msg, pin, ref, extack);
> diff --git a/drivers/dpll/dpll_nl.c b/drivers/dpll/dpll_nl.c
> index 1e652340a5d73..58235845fa3d5 100644
> --- a/drivers/dpll/dpll_nl.c
> +++ b/drivers/dpll/dpll_nl.c
> @@ -12,11 +12,12 @@
> #include <uapi/linux/dpll.h>
>
> /* Common nested types */
> -const struct nla_policy dpll_pin_parent_device_nl_policy[DPLL_A_PIN_PHASE_OFFSET + 1] = {
> +const struct nla_policy dpll_pin_parent_device_nl_policy[DPLL_A_PIN_OPERSTATE + 1] = {
> [DPLL_A_PIN_PARENT_ID] = { .type = NLA_U32, },
> [DPLL_A_PIN_DIRECTION] = NLA_POLICY_RANGE(NLA_U32, 1, 2),
> [DPLL_A_PIN_PRIO] = { .type = NLA_U32, },
> [DPLL_A_PIN_STATE] = NLA_POLICY_RANGE(NLA_U32, 1, 3),
> + [DPLL_A_PIN_OPERSTATE] = NLA_POLICY_RANGE(NLA_U32, 1, 4),
> [DPLL_A_PIN_PHASE_OFFSET] = { .type = NLA_S64, },
> };
>
> diff --git a/drivers/dpll/dpll_nl.h b/drivers/dpll/dpll_nl.h
> index 7419679b69779..fa8280e3dd14c 100644
> --- a/drivers/dpll/dpll_nl.h
> +++ b/drivers/dpll/dpll_nl.h
> @@ -13,7 +13,7 @@
> #include <uapi/linux/dpll.h>
>
> /* Common nested types */
> -extern const struct nla_policy dpll_pin_parent_device_nl_policy[DPLL_A_PIN_PHASE_OFFSET + 1];
> +extern const struct nla_policy dpll_pin_parent_device_nl_policy[DPLL_A_PIN_OPERSTATE + 1];
> extern const struct nla_policy dpll_pin_parent_pin_nl_policy[DPLL_A_PIN_STATE + 1];
> extern const struct nla_policy dpll_reference_sync_nl_policy[DPLL_A_PIN_STATE + 1];
>
> diff --git a/include/linux/dpll.h b/include/linux/dpll.h
> index b7277a8b484d2..b6f16c884b99e 100644
> --- a/include/linux/dpll.h
> +++ b/include/linux/dpll.h
> @@ -85,6 +85,12 @@ struct dpll_pin_ops {
> const struct dpll_device *dpll,
> void *dpll_priv, enum dpll_pin_state *state,
> struct netlink_ext_ack *extack);
> + int (*operstate_on_dpll_get)(const struct dpll_pin *pin,
> + void *pin_priv,
> + const struct dpll_device *dpll,
> + void *dpll_priv,
> + enum dpll_pin_operstate *operstate,
> + struct netlink_ext_ack *extack);
> int (*state_on_pin_set)(const struct dpll_pin *pin, void *pin_priv,
> const struct dpll_pin *parent_pin,
> void *parent_pin_priv,
> diff --git a/include/uapi/linux/dpll.h b/include/uapi/linux/dpll.h
> index 871685f7c353b..cb363cccf2e2a 100644
> --- a/include/uapi/linux/dpll.h
> +++ b/include/uapi/linux/dpll.h
> @@ -178,6 +178,28 @@ enum dpll_pin_state {
> DPLL_PIN_STATE_MAX = (__DPLL_PIN_STATE_MAX - 1)
> };
>
> +/**
> + * enum dpll_pin_operstate - defines possible operational states of a pin with
> + * respect to its parent DPLL device, valid values for DPLL_A_PIN_OPERSTATE
> + * attribute
> + * @DPLL_PIN_OPERSTATE_ACTIVE: pin is qualified and actively used by the DPLL
> + * @DPLL_PIN_OPERSTATE_STANDBY: pin is qualified but not actively used by the
> + * DPLL
> + * @DPLL_PIN_OPERSTATE_NO_SIGNAL: pin does not have a valid signal
> + * @DPLL_PIN_OPERSTATE_QUAL_FAILED: pin signal failed qualification (e.g.
> + * frequency or phase monitor)
> + */
> +enum dpll_pin_operstate {
> + DPLL_PIN_OPERSTATE_ACTIVE = 1,
> + DPLL_PIN_OPERSTATE_STANDBY,
> + DPLL_PIN_OPERSTATE_NO_SIGNAL,
> + DPLL_PIN_OPERSTATE_QUAL_FAILED,
> +
> + /* private: */
> + __DPLL_PIN_OPERSTATE_MAX,
> + DPLL_PIN_OPERSTATE_MAX = (__DPLL_PIN_OPERSTATE_MAX - 1)
> +};
> +
> /**
> * enum dpll_pin_capabilities - defines possible capabilities of a pin, valid
> * flags on DPLL_A_PIN_CAPABILITIES attribute
> @@ -257,6 +279,7 @@ enum dpll_a_pin {
> DPLL_A_PIN_PHASE_ADJUST_GRAN,
> DPLL_A_PIN_FRACTIONAL_FREQUENCY_OFFSET_PPT,
> DPLL_A_PIN_MEASURED_FREQUENCY,
> + DPLL_A_PIN_OPERSTATE,
>
> __DPLL_A_PIN_MAX,
> DPLL_A_PIN_MAX = (__DPLL_A_PIN_MAX - 1)
Reviewed-by: Petr Oros <poros@redhat.com>
^ permalink raw reply
* Re: [PATCH v13 net-next 05/11] net/nebula-matrix: add channel layer
From: Paolo Abeni @ 2026-04-30 10:51 UTC (permalink / raw)
To: illusion.wang, dimon.zhao, alvin.wang, sam.chen, netdev
Cc: andrew+netdev, corbet, kuba, linux-doc, lorenzo, horms,
vadim.fedorenko, lukas.bulwahn, edumazet, enelsonmoore, skhan,
hkallweit1, open list
In-Reply-To: <20260428114910.2616-6-illusion.wang@nebula-matrix.com>
On 4/28/26 1:48 PM, illusion.wang wrote:
> a channel management layer provides structured approach to handle
> communication between different components and drivers. Here's a summary
> of its key functionalities:
>
> 1. Message Handling Framework
> Message Registration/Unregistration: Functions (nbl_chan_register_msg,
> nbl_chan_unregister_msg) allow dynamic registration of message handlers
> for specific message types, enabling extensible communication protocols.
> Message Sending/Acknowledgment: Core functions (nbl_chan_send_msg,
> nbl_chan_send_ack) handle message transmission, including asynchronous
> operations with acknowledgment (ACK) support.
> Received ACKs are processed via nbl_chan_recv_ack_msg.
> Hash-Based Handler Lookup: A hash table (handle_hash_tbl) stores message
> handlers for efficient O(1) lookup by message type.
>
> 2. Channel Types and Queue Management
> Mailbox Channel: For direct communication between PF0 and Other PF.
> Queue Initialization/Teardown: Functions (nbl_chan_init_queue,
> nbl_chan_teardown_queue) manage transmit (TX) and receive (RX) queues.
>
> Queue Configuration: Hardware-specific queue parameters (e.g., buffer
> sizes, entry counts) are set via nbl_chan_config_queue, with hardware
> interactions delegated to hw_ops.
>
> 3. Hardware Abstraction Layer (HW Ops)
> Hardware-Specific Operations: The nbl_hw_ops structure abstracts
> hardware interactions: queue configuration (config_mailbox_txq/rxq),
> tail pointer updates(update_mailbox_queue_tail_ptr).
>
> Signed-off-by: illusion.wang <illusion.wang@nebula-matrix.com>
> ---
> .../net/ethernet/nebula-matrix/nbl/Makefile | 3 +-
> .../nbl/nbl_channel/nbl_channel.c | 771 +++++++++++++++++-
> .../nbl/nbl_channel/nbl_channel.h | 133 +++
> .../nebula-matrix/nbl/nbl_common/nbl_common.c | 212 +++++
> .../nebula-matrix/nbl/nbl_common/nbl_common.h | 33 +
> .../nbl/nbl_hw/nbl_hw_leonis/nbl_hw_leonis.c | 143 ++++
> .../nbl/nbl_include/nbl_def_channel.h | 87 ++
> .../nbl/nbl_include/nbl_def_common.h | 30 +
> .../nbl/nbl_include/nbl_def_hw.h | 28 +
> .../nbl/nbl_include/nbl_include.h | 6 +
> 10 files changed, 1442 insertions(+), 4 deletions(-)
> create mode 100644 drivers/net/ethernet/nebula-matrix/nbl/nbl_common/nbl_common.c
> create mode 100644 drivers/net/ethernet/nebula-matrix/nbl/nbl_common/nbl_common.h
>
> diff --git a/drivers/net/ethernet/nebula-matrix/nbl/Makefile b/drivers/net/ethernet/nebula-matrix/nbl/Makefile
> index 63116d1d7043..c9bc060732e7 100644
> --- a/drivers/net/ethernet/nebula-matrix/nbl/Makefile
> +++ b/drivers/net/ethernet/nebula-matrix/nbl/Makefile
> @@ -3,7 +3,8 @@
>
> obj-$(CONFIG_NBL) := nbl.o
>
> -nbl-objs += nbl_channel/nbl_channel.o \
> +nbl-objs += nbl_common/nbl_common.o \
> + nbl_channel/nbl_channel.o \
> nbl_hw/nbl_hw_leonis/nbl_hw_leonis.o \
> nbl_hw/nbl_hw_leonis/nbl_resource_leonis.o \
> nbl_hw/nbl_hw_leonis/nbl_hw_leonis_regs.o \
> diff --git a/drivers/net/ethernet/nebula-matrix/nbl/nbl_channel/nbl_channel.c b/drivers/net/ethernet/nebula-matrix/nbl/nbl_channel/nbl_channel.c
> index c1b724a8b92d..810f5f03adc0 100644
> --- a/drivers/net/ethernet/nebula-matrix/nbl/nbl_channel/nbl_channel.c
> +++ b/drivers/net/ethernet/nebula-matrix/nbl/nbl_channel/nbl_channel.c
> @@ -2,12 +2,757 @@
> /*
> * Copyright (c) 2025 Nebula Matrix Limited.
> */
> -
> +#include <linux/delay.h>
> #include <linux/device.h>
> #include <linux/pci.h>
> +#include <linux/bits.h>
> +#include <linux/dma-mapping.h>
> #include "nbl_channel.h"
>
> +static int nbl_chan_add_msg_handler(struct nbl_channel_mgt *chan_mgt,
> + u16 msg_type, nbl_chan_resp func,
> + void *priv)
> +{
> + struct nbl_chan_msg_node_data handler = { 0 };
> + int ret;
> +
> + handler.func = func;
> + handler.priv = priv;
> + ret = nbl_common_alloc_hash_node(chan_mgt->handle_hash_tbl, &msg_type,
> + &handler, NULL);
> +
> + return ret;
> +}
> +
> +static int nbl_chan_init_msg_handler(struct nbl_channel_mgt *chan_mgt)
> +{
> + struct nbl_common_info *common = chan_mgt->common;
> + struct nbl_hash_tbl_key tbl_key;
> +
> + tbl_key.dev = common->dev;
> + tbl_key.key_size = sizeof(u16);
> + tbl_key.data_size = sizeof(struct nbl_chan_msg_node_data);
> + tbl_key.bucket_size = NBL_CHAN_HANDLER_TBL_BUCKET_SIZE;
> +
> + chan_mgt->handle_hash_tbl = nbl_common_init_hash_table(&tbl_key);
> + if (!chan_mgt->handle_hash_tbl)
> + return -ENOMEM;
> +
> + return 0;
> +}
> +
> +static void nbl_chan_remove_msg_handler(struct nbl_channel_mgt *chan_mgt)
> +{
> + nbl_common_remove_hash_table(chan_mgt->handle_hash_tbl, NULL);
> +
> + chan_mgt->handle_hash_tbl = NULL;
> +}
> +
> +static void nbl_chan_init_queue_param(struct nbl_chan_info *chan_info,
> + u16 num_txq_entries, u16 num_rxq_entries,
> + u16 txq_buf_size, u16 rxq_buf_size)
> +{
> + mutex_init(&chan_info->txq_lock);
> + chan_info->num_txq_entries = num_txq_entries;
> + chan_info->num_rxq_entries = num_rxq_entries;
> + chan_info->txq_buf_size = txq_buf_size;
> + chan_info->rxq_buf_size = rxq_buf_size;
> +}
> +
> +static int nbl_chan_init_tx_queue(struct nbl_common_info *common,
> + struct nbl_chan_info *chan_info)
> +{
> + struct nbl_chan_ring *txq = &chan_info->txq;
> + struct device *dev = common->dev;
> + size_t size =
> + chan_info->num_txq_entries * sizeof(struct nbl_chan_tx_desc);
> +
> + txq->desc.tx_desc = dmam_alloc_coherent(dev, size, &txq->dma,
> + GFP_KERNEL);
> + if (!txq->desc.tx_desc)
> + return -ENOMEM;
Sashiko says:
These setup functions use dmam_alloc_coherent() and devm_kcalloc(), but
nbl_chan_teardown_queue() does not free them. If the queues are torn down
and set up multiple times during the device's lifetime, does this leak
memory since devm_ resources are only freed when the device is unbound?
> +static int nbl_chan_teardown_queue(struct nbl_channel_mgt *chan_mgt,
> + u8 chan_type)
> +{
> + struct nbl_chan_info *chan_info = chan_mgt->chan_info[chan_type];
> +
> + nbl_chan_stop_queue(chan_mgt, chan_info);
sashiko says:
The driver registers a background work item for clean_task, but neither
nbl_chan_teardown_queue() nor nbl_chan_remove_common() calls
cancel_work_sync(). If the queues are destroyed while this work is pending,
will it eventually execute and access unmapped DMA regions or freed memory?
> +static int nbl_chan_kick_tx_ring(struct nbl_channel_mgt *chan_mgt,
> + struct nbl_chan_info *chan_info)
> +{
> + struct nbl_hw_ops *hw_ops = chan_mgt->hw_ops_tbl->ops;
> + struct nbl_chan_ring *txq = &chan_info->txq;
> + struct device *dev = chan_mgt->common->dev;
> + struct nbl_chan_tx_desc *tx_desc;
> + int i = 0;
> +
> + NBL_UPDATE_QUEUE_TAIL_PTR(chan_info, hw_ops, chan_mgt, txq->tail_ptr,
> + NBL_MB_TX_QID);
> +
> + tx_desc = NBL_CHAN_TX_RING_TO_DESC(txq, txq->next_to_clean);
> +
> + while (!(tx_desc->flags & BIT(NBL_CHAN_TX_DESC_USED))) {
> + udelay(NBL_CHAN_TX_WAIT_US);
> + i++;
Sashiko says:
This loop executes udelay(100) for up to 30,000 iterations while holding the
txq_lock mutex. Will this busy-wait for up to 3 seconds with a mutex held
and preemption disabled, potentially triggering soft lockups? Could a
sleepable function like usleep_range() be used instead, or the overall
timeout reduced?
> +
> + if (!(i % NBL_CHAN_TX_REKICK_WAIT_TIMES))
> + NBL_UPDATE_QUEUE_TAIL_PTR(chan_info, hw_ops, chan_mgt,
> + txq->tail_ptr, NBL_MB_TX_QID);
> +
> + if (i == NBL_CHAN_TX_WAIT_TIMES) {
> + dev_err(dev, "chan send message type: %d timeout\n",
> + tx_desc->msg_type);
> + return -ETIMEDOUT;
> + }
> + }
> +
> + txq->next_to_clean = txq->next_to_use;
> +
> + return 0;
> +}
> +
> +static void nbl_chan_recv_ack_msg(void *priv, u16 srcid, u16 msgid, void *data,
> + u32 data_len)
> +{
> + struct nbl_channel_mgt *chan_mgt = (struct nbl_channel_mgt *)priv;
> + struct nbl_chan_waitqueue_head *wait_head = NULL;
> + union nbl_chan_msg_id ack_msgid = { { 0 } };
> + struct device *dev = chan_mgt->common->dev;
> + struct nbl_chan_info *chan_info =
> + chan_mgt->chan_info[NBL_CHAN_TYPE_MAILBOX];
> + u32 *payload = (u32 *)data;
> + u32 ack_datalen;
> + u32 copy_len;
> +
> + if (data_len < NBL_CHAN_ACK_HEAD_LEN * sizeof(u32)) {
> + dev_err(dev, "Invalid ACK data_len: %u\n", data_len);
> + return;
> + }
> + ack_datalen = data_len - NBL_CHAN_ACK_HEAD_LEN * sizeof(u32);
> + ack_msgid.id = *(u16 *)(payload + NBL_CHAN_MSG_ID_POS);
This code extracts the message ID by casting a u32 pointer to u16. On
big-endian architectures, will this read the upper 16 bytes of the
32-bit word, which are zero?
> + if (ack_msgid.info.loc >= NBL_CHAN_QUEUE_LEN) {
> + dev_err(dev, "chan recv msg loc: %d err\n", ack_msgid.info.loc);
> + return;
> + }
> + wait_head = &chan_info->wait[ack_msgid.info.loc];
> + wait_head->ack_err = *(payload + NBL_CHAN_ACK_RET_POS);
> +
> + copy_len = min_t(u32, wait_head->ack_data_len, ack_datalen);
> + if (wait_head->ack_err >= 0 && copy_len > 0)
> + memcpy((char *)wait_head->ack_data,
> + payload + NBL_CHAN_ACK_HEAD_LEN, copy_len);
Sashiko says:
If wait_event_timeout() times out in nbl_chan_send_msg(), the function
returns and the caller's stack frame is destroyed. Since the wait slot is
never cleared, if a delayed ACK arrives later, will nbl_chan_recv_ack_msg()
execute memcpy() and overwrite the now-freed stack memory?
Should this function verify if ack_msgid.info.index matches
wait_head->msg_index and the state is NBL_MBX_STATUS_WAITING before copying?
Sashiko has more comments, I reported only those that looked more
impactful to me, please have a careful read of them:
https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20260428114910.2616-1-illusion.wang%40nebula-matrix.com
https://sashiko.dev/#/patchset/20260428114910.2616-1-illusion.wang%40nebula-matrix.com
Also note that submitters should thread AI-generated reviews as any
other kind of feedback. Specifically you should procatively reply on the
ML discussing invalid comments.
Thanks,
Paolo
^ permalink raw reply
* Re: [PATCH v5 03/11] dt-bindings: mfd: add documentation for S2MU005 PMIC
From: Krzysztof Kozlowski @ 2026-04-30 10:50 UTC (permalink / raw)
To: Kaustabh Chakraborty
Cc: Lee Jones, Pavel Machek, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, MyungJoo Ham, Chanwoo Choi, Sebastian Reichel,
André Draszik, Alexandre Belloni, Jonathan Corbet,
Shuah Khan, Nam Tran, Łukasz Lebiedziński, linux-leds,
devicetree, linux-kernel, linux-pm, linux-samsung-soc, linux-rtc,
linux-doc
In-Reply-To: <DI5NXS3PQ53R.L9JZEBHW5EGI@disroot.org>
On 29/04/2026 15:12, Kaustabh Chakraborty wrote:
> Hi Krzysztof,
>
> This are no review comments here. Did you happen to miss anything?
Thanks for noticing. I mistyped.
>
> On 2026-04-28 08:01 +02:00, Krzysztof Kozlowski wrote:
>> On Fri, Apr 24, 2026 at 01:09:02AM +0530, Kaustabh Chakraborty wrote:
>>> Samsung's S2MU005 PMIC includes subdevices for a charger, an MUIC (Micro
>>> USB Interface Controller), and flash and RGB LED controllers.
>>>
>>> Add the compatible and documentation for the S2MU005 PMIC. Also, add an
>>> example for nodes for supported sub-devices, i.e. MUIC, flash LEDs, and
>>> RGB LEDs. Charger sub-device uses the node of the parent.
>>>
>>> Signed-off-by: Kaustabh Chakraborty <kauschluss@disroot.org>
>>> ---
>>> .../bindings/mfd/samsung,s2mu005-pmic.yaml | 120 +++++++++++++++++++++
>>> 1 file changed, 120 insertions(+)
Patch looks correct:
Reviewed-by: Krzysztof Kozlowski <krzysztof.kozlowski@oss.qualcomm.com>
Best regards,
Krzysztof
^ permalink raw reply
* Re: [PATCH v13 net-next 03/11] net/nebula-matrix: add chip related definitions
From: Paolo Abeni @ 2026-04-30 10:41 UTC (permalink / raw)
To: illusion.wang, dimon.zhao, alvin.wang, sam.chen, netdev
Cc: andrew+netdev, corbet, kuba, linux-doc, lorenzo, horms,
vadim.fedorenko, lukas.bulwahn, edumazet, enelsonmoore, skhan,
hkallweit1, open list
In-Reply-To: <20260428114910.2616-4-illusion.wang@nebula-matrix.com>
On 4/28/26 1:48 PM, illusion.wang wrote:
> diff --git a/drivers/net/ethernet/nebula-matrix/nbl/nbl_hw/nbl_hw_leonis/nbl_hw_leonis.h b/drivers/net/ethernet/nebula-matrix/nbl/nbl_hw/nbl_hw_leonis/nbl_hw_leonis.h
> index 77c67b67ba31..8831394ed11b 100644
> --- a/drivers/net/ethernet/nebula-matrix/nbl/nbl_hw/nbl_hw_leonis/nbl_hw_leonis.h
> +++ b/drivers/net/ethernet/nebula-matrix/nbl/nbl_hw/nbl_hw_leonis/nbl_hw_leonis.h
> @@ -11,4 +11,473 @@
> #include "../../nbl_include/nbl_include.h"
> #include "../nbl_hw_reg.h"
>
> +#define NBL_DRIVER_STATUS_REG 0x1300444
> +#define NBL_DRIVER_STATUS_BIT 16
> +
> +#pragma pack(1)
The kernel style for packed layouts is the __packed attribute; #pragma
pack is a non-portable compiler directive.
> +
> +/* ---------- REG BASE ADDR ---------- */
> +/* Interface modules base addr */
> +#define NBL_INTF_HOST_PCOMPLETER_BASE 0x00f08000
> +#define NBL_INTF_HOST_PADPT_BASE 0x00f4c000
> +#define NBL_INTF_HOST_MAILBOX_BASE 0x00fb0000
> +#define NBL_INTF_HOST_PCIE_BASE 0X01504000
> +/* DP modules base addr */
> +#define NBL_DP_USTORE_BASE 0x00104000
> +#define NBL_DP_UQM_BASE 0x00114000
> +#define NBL_DP_UPED_BASE 0x0015c000
> +#define NBL_DP_UVN_BASE 0x00244000
> +#define NBL_DP_DSCH_BASE 0x00404000
> +#define NBL_DP_SHAPING_BASE 0x00504000
> +#define NBL_DP_DVN_BASE 0x00514000
> +#define NBL_DP_DSTORE_BASE 0x00704000
> +#define NBL_DP_DQM_BASE 0x00714000
> +#define NBL_DP_DPED_BASE 0x0075c000
> +#define NBL_DP_DDMUX_BASE 0x00984000
> +/* -------- MAILBOX BAR2 ----- */
> +#define NBL_MAILBOX_NOTIFY_ADDR 0x00000000
> +#define NBL_MAILBOX_BAR_REG 0x00000000
> +#define NBL_MAILBOX_QINFO_CFG_RX_TABLE_ADDR 0x10
> +#define NBL_MAILBOX_QINFO_CFG_TX_TABLE_ADDR 0x20
> +#define NBL_MAILBOX_QINFO_CFG_DBG_TABLE_ADDR 0x30
> +
> +/* -------- MAILBOX -------- */
> +
> +/* mailbox BAR qinfo_cfg_table */
> +struct nbl_mailbox_qinfo_cfg_table {
> + u32 queue_base_addr_l;
> + u32 queue_base_addr_h;
> + u32 queue_size_bwind:4;
> + u32 rsv1:28;
> + u32 queue_rst:1;
> + u32 queue_en:1;
> + u32 dif_err:1;
> + u32 ptr_err:1;
> + u32 rsv2:28;
Sashiko says:
Can these bitfield register layouts work correctly on big-endian
hosts?
The C standard leaves allocation of bitfields within a storage unit
implementation-defined, and with GCC the order flips between LE and
BE targets (LSB-first on little-endian, MSB-first on big-endian).
Because nbl_hw_wr32() ultimately uses writel(), which only does
byte-level LE conversion, the hardware-visible bit positions produced
by these structs will differ between LE and BE builds.
The same question applies to every other bitfield struct added in
this header, e.g. nbl_mailbox_qinfo_map_table, nbl_host_msix_info,
ped_hw_edit_profile, dstore_disc_bp_th, nbl_shaping_net, ustore_pkt_len,
uvn_queue_err_mask, board_cfg_dw3, etc.
Would the explicit shift/mask helpers in <linux/bitfield.h>
(FIELD_PREP/FIELD_GET with GENMASK) be a better match here?
[...]
> +void nbl_write_all_regs(struct nbl_hw_mgt *hw_mgt)
> +{
> + struct nbl_common_info *common = hw_mgt->common;
> + u8 eth_mode = common->eth_mode;
> + const u32 *nbl_sec046_data;
> + const u32 *nbl_sec071_data;
> + u32 i;
> +
> + switch (eth_mode) {
> + case 1:
> + nbl_sec046_data = nbl_sec046_1p_data;
> + nbl_sec071_data = nbl_sec071_1p_data;
> + break;
> + case 2:
> + nbl_sec046_data = nbl_sec046_2p_data;
> + nbl_sec071_data = nbl_sec071_2p_data;
> + break;
> + case 4:
> + nbl_sec046_data = nbl_sec046_4p_data;
> + nbl_sec071_data = nbl_sec071_4p_data;
> + break;
> + default:
> + nbl_sec046_data = nbl_sec046_2p_data;
> + nbl_sec071_data = nbl_sec071_2p_data;
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC006_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC006_REGI(i), nbl_sec006_data[i]);
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC007_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC007_REGI(i), nbl_sec007_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC008_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC008_REGI(i), nbl_sec008_data[i]);
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC009_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC009_REGI(i), nbl_sec009_data[i]);
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC010_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC010_REGI(i), nbl_sec010_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC011_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC011_REGI(i), nbl_sec011_data[i]);
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC012_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC012_REGI(i), nbl_sec012_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC013_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC013_REGI(i), nbl_sec013_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC014_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC014_REGI(i), nbl_sec014_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC022_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC022_REGI(i), nbl_sec022_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC023_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC023_REGI(i), nbl_sec023_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC024_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC024_REGI(i), nbl_sec024_data[i]);
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC025_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC025_REGI(i), nbl_sec025_data[i]);
> + }
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC026_SIZE; i++)
> + nbl_hw_wr32(hw_mgt, NBL_SEC026_REGI(i), nbl_sec026_data[i]);
> +
> + nbl_flush_writes(hw_mgt);
> + for (i = 0; i < NBL_SEC027_SIZE; i++) {
> + if ((i + 1) % NBL_SEC_BLOCK_SIZE == 0)
> + nbl_hw_rd32(hw_mgt, NBL_HW_DUMMY_REG);
> +
> + nbl_hw_wr32(hw_mgt, NBL_SEC027_REGI(i), nbl_sec027_data[i]);
Sashiko says:
Could this loop read past the end of the nbl_sec009_data array?
The macro NBL_SEC009_SIZE is defined as 2048, but the nbl_sec009_data array
contains significantly fewer elements (around 754). This appears to cause
sequential out-of-bounds reads into the .rodata section, writing unrelated
memory to the device registers.
Similar size mismatches exist for nbl_sec025_data (262 elements vs size
1024)
and nbl_sec022_data (506 elements vs size 256).
Would it be safer to use ARRAY_SIZE() to bound these iterations?
/P
^ permalink raw reply
* [bcain:bcain/glink 54/56] htmldocs: Documentation/hexagon/qemu-glink.rst: WARNING: document isn't included in any toctree [toc.not_included]
From: kernel test robot @ 2026-04-30 10:25 UTC (permalink / raw)
To: Brian Cain; +Cc: oe-kbuild-all, linux-doc
tree: https://git.kernel.org/pub/scm/linux/kernel/git/bcain/linux.git bcain/glink
head: 5d897d0f6b0bbd170ef332eeae9a8329ec812a5e
commit: dd12099bfe857f1c7f5cd0915e38f655e8da5449 [54/56] Documentation: hexagon: add GLINK dual-QEMU testing guide
compiler: clang version 20.1.8 (https://github.com/llvm/llvm-project 87f0227cb60147a26a1eeb4fb06e3b505e9c7261)
docutils: docutils (Docutils 0.21.2, Python 3.13.5, on linux)
reproduce: (https://download.01.org/0day-ci/archive/20260430/202604301208.e4oGQJcF-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/202604301208.e4oGQJcF-lkp@intel.com/
All warnings (new ones prefixed by >>):
Documentation/userspace-api/landlock:550: ./include/uapi/linux/landlock.h:45: ERROR: Unknown target name: "network flags". [docutils]
Documentation/userspace-api/landlock:550: ./include/uapi/linux/landlock.h:50: ERROR: Unknown target name: "scope flags". [docutils]
Documentation/userspace-api/landlock:550: ./include/uapi/linux/landlock.h:24: ERROR: Unknown target name: "filesystem flags". [docutils]
Documentation/userspace-api/landlock:559: ./include/uapi/linux/landlock.h:168: ERROR: Unknown target name: "filesystem flags". [docutils]
Documentation/userspace-api/landlock:559: ./include/uapi/linux/landlock.h:191: ERROR: Unknown target name: "network flags". [docutils]
>> Documentation/hexagon/qemu-glink.rst: WARNING: document isn't included in any toctree [toc.not_included]
Documentation/networking/skbuff:36: ./include/linux/skbuff.h:181: WARNING: Failed to create a cross reference. A title or caption not found: 'crc' [ref.ref]
--
0-DAY CI Kernel Test Service
https://github.com/intel/lkp-tests/wiki
^ permalink raw reply
* [PATCH v9 4/6] iio: adc: ad4691: add SPI offload support
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add SPI offload support to enable DMA-based, CPU-independent data
acquisition using the SPI Engine offload framework.
When an SPI offload is available (devm_spi_offload_get() succeeds),
the driver registers a DMA engine IIO buffer and uses dedicated buffer
setup operations. If no offload is available the existing software
triggered buffer path is used unchanged.
Both CNV Burst Mode and Manual Mode support offload, but use different
trigger mechanisms:
CNV Burst Mode: the SPI Engine is triggered by the ADC's DATA_READY
signal on the GP pin specified by the trigger-source consumer reference
in the device tree (one cell = GP pin number 0-3). For this mode the
driver acts as both an SPI offload consumer (DMA RX stream, message
optimization) and a trigger source provider: it registers the
GP/DATA_READY output via devm_spi_offload_trigger_register() so the
offload framework can match the '#trigger-source-cells' phandle and
automatically fire the SPI Engine DMA transfer at end-of-conversion.
Manual Mode: the SPI Engine is triggered by a periodic trigger at
the configured sampling frequency. The pre-built SPI message uses
the pipelined CNV-on-CS protocol: N+1 16-bit transfers are issued
for N active channels (the first result is discarded as garbage from
the pipeline flush) and the remaining N results are captured by DMA.
All offload transfers use 16-bit frames (bits_per_word=16, len=2).
The channel scan_type (storagebits=16, shift=0, IIO_BE) is shared
between the software triggered-buffer and offload paths; no separate
scan_type or channel array is needed for the offload case. The
ad4691_manual_channels[] array introduced in the triggered-buffer
commit is reused here: it hides the IIO_CHAN_INFO_OVERSAMPLING_RATIO
attribute, which is not applicable in Manual Mode.
Kconfig gains a dependency on IIO_BUFFER_DMAENGINE.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 392 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 389 insertions(+), 5 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index d498f16c0816..fdc6565933c5 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -143,8 +143,10 @@ config AD4691
tristate "Analog Devices AD4691 Family ADC Driver"
depends on SPI
select IIO_BUFFER
+ select IIO_BUFFER_DMAENGINE
select IIO_TRIGGERED_BUFFER
select REGMAP
+ select SPI_OFFLOAD
help
Say yes here to build support for Analog Devices AD4691 Family MuxSAR
SPI analog to digital converters (ADC).
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index c1e3406fef18..50d81d87d4a0 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -23,10 +23,14 @@
#include <linux/regulator/consumer.h>
#include <linux/reset.h>
#include <linux/spi/spi.h>
+#include <linux/spi/offload/consumer.h>
+#include <linux/spi/offload/provider.h>
#include <linux/units.h>
#include <linux/unaligned.h>
#include <linux/iio/buffer.h>
+#include <linux/iio/buffer-dma.h>
+#include <linux/iio/buffer-dmaengine.h>
#include <linux/iio/iio.h>
#include <linux/iio/sysfs.h>
#include <linux/iio/trigger.h>
@@ -42,6 +46,11 @@
#define AD4691_CNV_DUTY_CYCLE_NS 380
#define AD4691_CNV_HIGH_TIME_NS 430
+/*
+ * Conservative default for the manual offload periodic trigger. Low enough
+ * to work safely out of the box across all OSR and channel count combinations.
+ */
+#define AD4691_OFFLOAD_INITIAL_TRIGGER_HZ (100 * HZ_PER_KHZ)
#define AD4691_SPI_CONFIG_A_REG 0x000
#define AD4691_SW_RESET (BIT(7) | BIT(0))
@@ -113,6 +122,7 @@ struct ad4691_chip_info {
const char *name;
unsigned int max_rate;
const struct ad4691_channel_info *sw_info;
+ const struct ad4691_channel_info *offload_info;
};
#define AD4691_CHANNEL(ch) \
@@ -176,6 +186,18 @@ static const struct ad4691_channel_info ad4693_sw_info = {
.num_channels = ARRAY_SIZE(ad4693_channels),
};
+static const struct ad4691_channel_info ad4691_offload_info = {
+ .channels = ad4691_channels,
+ /* Exclude the soft timestamp entry; num_channels caps access. */
+ .num_channels = ARRAY_SIZE(ad4691_channels) - 1,
+};
+
+static const struct ad4691_channel_info ad4693_offload_info = {
+ .channels = ad4693_channels,
+ /* Exclude the soft timestamp entry; num_channels caps access. */
+ .num_channels = ARRAY_SIZE(ad4693_channels) - 1,
+};
+
/*
* Internal oscillator frequency table. Index is the OSC_FREQ_REG[3:0] value.
* Index 0 (1 MHz) is only valid for AD4692/AD4694; AD4691/AD4693 support
@@ -206,24 +228,36 @@ static const struct ad4691_chip_info ad4691_chip_info = {
.name = "ad4691",
.max_rate = 500 * HZ_PER_KHZ,
.sw_info = &ad4691_sw_info,
+ .offload_info = &ad4691_offload_info,
};
static const struct ad4691_chip_info ad4692_chip_info = {
.name = "ad4692",
.max_rate = 1 * HZ_PER_MHZ,
.sw_info = &ad4691_sw_info,
+ .offload_info = &ad4691_offload_info,
};
static const struct ad4691_chip_info ad4693_chip_info = {
.name = "ad4693",
.max_rate = 500 * HZ_PER_KHZ,
.sw_info = &ad4693_sw_info,
+ .offload_info = &ad4693_offload_info,
};
static const struct ad4691_chip_info ad4694_chip_info = {
.name = "ad4694",
.max_rate = 1 * HZ_PER_MHZ,
.sw_info = &ad4693_sw_info,
+ .offload_info = &ad4693_offload_info,
+};
+
+struct ad4691_offload_state {
+ struct spi_offload *offload;
+ struct spi_offload_trigger *trigger;
+ u64 trigger_hz;
+ u8 tx_cmd[17][2];
+ u8 tx_reset[4];
};
struct ad4691_state {
@@ -252,7 +286,10 @@ struct ad4691_state {
* transfers in one go.
*/
struct spi_message scan_msg;
- /* max 16 + 1 NOOP (manual) or 2*16 + 2 (CNV burst). */
+ /*
+ * Non-offload: max 16 + 1 NOOP (manual) or 2*16 + 2 (CNV burst).
+ * Offload reuses this array — both paths are mutually exclusive.
+ */
struct spi_transfer scan_xfers[34];
/*
* CNV burst: 16 AVG_IN addresses + state-reset address + state-reset
@@ -264,6 +301,8 @@ struct ad4691_state {
* DMA-aligned because scan_xfers point rx_buf directly into vals[].
*/
IIO_DECLARE_DMA_BUFFER_WITH_TS(__be16, vals, 16);
+ /* NULL when no SPI offload hardware is present */
+ struct ad4691_offload_state *offload;
};
/*
@@ -283,6 +322,46 @@ static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
AD4691_GP_MODE_DATA_READY << shift);
}
+static const struct spi_offload_config ad4691_offload_config = {
+ .capability_flags = SPI_OFFLOAD_CAP_TRIGGER |
+ SPI_OFFLOAD_CAP_RX_STREAM_DMA,
+};
+
+static bool ad4691_offload_trigger_match(struct spi_offload_trigger *trigger,
+ enum spi_offload_trigger_type type,
+ u64 *args, u32 nargs)
+{
+ return type == SPI_OFFLOAD_TRIGGER_DATA_READY &&
+ nargs == 1 && args[0] <= 3;
+}
+
+static int ad4691_offload_trigger_request(struct spi_offload_trigger *trigger,
+ enum spi_offload_trigger_type type,
+ u64 *args, u32 nargs)
+{
+ struct ad4691_state *st = spi_offload_trigger_get_priv(trigger);
+
+ if (nargs != 1)
+ return -EINVAL;
+
+ return ad4691_gpio_setup(st, args[0]);
+}
+
+static int ad4691_offload_trigger_validate(struct spi_offload_trigger *trigger,
+ struct spi_offload_trigger_config *config)
+{
+ if (config->type != SPI_OFFLOAD_TRIGGER_DATA_READY)
+ return -EINVAL;
+
+ return 0;
+}
+
+static const struct spi_offload_trigger_ops ad4691_offload_trigger_ops = {
+ .match = ad4691_offload_trigger_match,
+ .request = ad4691_offload_trigger_request,
+ .validate = ad4691_offload_trigger_validate,
+};
+
static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct spi_device *spi = context;
@@ -712,6 +791,7 @@ static const struct iio_buffer_setup_ops ad4691_manual_buffer_setup_ops = {
static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
{
struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int acc_mask;
unsigned int k, i;
int ret;
@@ -758,9 +838,9 @@ static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
if (ret)
goto err_unoptimize;
- ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
- ~bitmap_read(indio_dev->active_scan_mask, 0,
- iio_get_masklength(indio_dev)) & GENMASK(15, 0));
+ acc_mask = ~bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG, acc_mask);
if (ret)
goto err_unoptimize;
@@ -803,6 +883,209 @@ static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
.postdisable = &ad4691_cnv_burst_buffer_postdisable,
};
+static int ad4691_manual_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ struct device *dev = regmap_get_device(st->regmap);
+ struct spi_device *spi = to_spi_device(dev);
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_PERIODIC,
+ };
+ unsigned int bpw = indio_dev->channels[0].scan_type.realbits;
+ unsigned int bit, k;
+ int ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+
+ /*
+ * N+1 transfers for N channels. Each CS-low period triggers
+ * a conversion AND returns the previous result (pipelined).
+ * TX: [AD4691_ADC_CHAN(n), 0x00]
+ * RX: [data_hi, data_lo] (storagebits=16, shift=0)
+ * Transfer 0 RX is garbage; transfers 1..N carry real data.
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ offload->tx_cmd[k][0] = AD4691_ADC_CHAN(bit);
+ st->scan_xfers[k].tx_buf = offload->tx_cmd[k];
+ st->scan_xfers[k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[k].bits_per_word = bpw;
+ st->scan_xfers[k].cs_change = 1;
+ st->scan_xfers[k].cs_change_delay.value = AD4691_CNV_HIGH_TIME_NS;
+ st->scan_xfers[k].cs_change_delay.unit = SPI_DELAY_UNIT_NSECS;
+ /* First transfer RX is garbage — skip it. */
+ if (k > 0)
+ st->scan_xfers[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ k++;
+ }
+
+ /* Final NOOP to flush pipeline and capture last channel. */
+ offload->tx_cmd[k][0] = AD4691_NOOP;
+ st->scan_xfers[k].tx_buf = offload->tx_cmd[k];
+ st->scan_xfers[k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[k].bits_per_word = bpw;
+ st->scan_xfers[k].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ k++;
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, k);
+ st->scan_msg.offload = offload->offload;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ config.periodic.frequency_hz = offload->trigger_hz;
+ ret = spi_offload_trigger_enable(offload->offload, offload->trigger, &config);
+ if (ret)
+ goto err_unoptimize;
+
+ return 0;
+
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+err_exit_conversion:
+ ad4691_exit_conversion_mode(st);
+ return ret;
+}
+
+static int ad4691_manual_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+
+ spi_offload_trigger_disable(offload->offload, offload->trigger);
+ spi_unoptimize_message(&st->scan_msg);
+
+ return ad4691_exit_conversion_mode(st);
+}
+
+static const struct iio_buffer_setup_ops ad4691_manual_offload_buffer_setup_ops = {
+ .postenable = &ad4691_manual_offload_buffer_postenable,
+ .predisable = &ad4691_manual_offload_buffer_predisable,
+};
+
+static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ struct device *dev = regmap_get_device(st->regmap);
+ struct spi_device *spi = to_spi_device(dev);
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_DATA_READY,
+ };
+ unsigned int bpw = indio_dev->channels[0].scan_type.realbits;
+ unsigned int acc_mask;
+ unsigned int bit, k;
+ int ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)));
+ if (ret)
+ return ret;
+
+ acc_mask = ~bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG, acc_mask);
+ if (ret)
+ return ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ return ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+
+ /*
+ * Each AVG_IN register read uses two 16-bit transfers:
+ * TX: [reg_hi | 0x80, reg_lo] (address, CS stays asserted)
+ * RX: [data_hi, data_lo] (data, storagebits=16, shift=0)
+ * The state reset is also split into two 16-bit transfers
+ * (address then value) to keep bits_per_word uniform throughout.
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ put_unaligned_be16(0x8000 | AD4691_AVG_IN(bit), offload->tx_cmd[k]);
+
+ /* TX: address phase, CS stays asserted into data phase */
+ st->scan_xfers[2 * k].tx_buf = offload->tx_cmd[k];
+ st->scan_xfers[2 * k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k].bits_per_word = bpw;
+
+ /* RX: data phase, CS toggles after to delimit the next register op */
+ st->scan_xfers[2 * k + 1].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k + 1].bits_per_word = bpw;
+ st->scan_xfers[2 * k + 1].offload_flags = SPI_OFFLOAD_XFER_RX_STREAM;
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ k++;
+ }
+
+ /* State reset to re-arm DATA_READY for the next scan. */
+ put_unaligned_be16(AD4691_STATE_RESET_REG, offload->tx_reset);
+ offload->tx_reset[2] = AD4691_STATE_RESET_ALL;
+
+ st->scan_xfers[2 * k].tx_buf = offload->tx_reset;
+ st->scan_xfers[2 * k].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k].bits_per_word = bpw;
+
+ st->scan_xfers[2 * k + 1].tx_buf = &offload->tx_reset[2];
+ st->scan_xfers[2 * k + 1].len = sizeof(offload->tx_cmd[k]);
+ st->scan_xfers[2 * k + 1].bits_per_word = bpw;
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, 2 * k + 2);
+ st->scan_msg.offload = offload->offload;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = spi_offload_trigger_enable(offload->offload, offload->trigger, &config);
+ if (ret)
+ goto err_sampling_disable;
+
+ return 0;
+
+err_sampling_disable:
+ ad4691_sampling_enable(st, false);
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+err_exit_conversion:
+ ad4691_exit_conversion_mode(st);
+ return ret;
+}
+
+static int ad4691_cnv_burst_offload_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ struct ad4691_offload_state *offload = st->offload;
+ int ret;
+
+ spi_offload_trigger_disable(offload->offload, offload->trigger);
+
+ ret = ad4691_sampling_enable(st, false);
+ if (ret)
+ return ret;
+
+ spi_unoptimize_message(&st->scan_msg);
+
+ return ad4691_exit_conversion_mode(st);
+}
+
+static const struct iio_buffer_setup_ops ad4691_cnv_burst_offload_buffer_setup_ops = {
+ .postenable = &ad4691_cnv_burst_offload_buffer_postenable,
+ .predisable = &ad4691_cnv_burst_offload_buffer_predisable,
+};
+
static ssize_t sampling_frequency_show(struct device *dev,
struct device_attribute *attr,
char *buf)
@@ -810,6 +1093,9 @@ static ssize_t sampling_frequency_show(struct device *dev,
struct iio_dev *indio_dev = dev_to_iio_dev(dev);
struct ad4691_state *st = iio_priv(indio_dev);
+ if (st->manual_mode && st->offload)
+ return sysfs_emit(buf, "%llu\n", st->offload->trigger_hz);
+
return sysfs_emit(buf, "%lu\n", NSEC_PER_SEC / st->cnv_period_ns);
}
@@ -829,6 +1115,19 @@ static ssize_t sampling_frequency_store(struct device *dev,
if (IIO_DEV_ACQUIRE_FAILED(claim))
return -EBUSY;
+ if (st->manual_mode && st->offload) {
+ struct spi_offload_trigger_config config = {
+ .type = SPI_OFFLOAD_TRIGGER_PERIODIC,
+ .periodic = { .frequency_hz = freq },
+ };
+
+ ret = spi_offload_trigger_validate(st->offload->trigger, &config);
+ if (ret)
+ return ret;
+
+ st->offload->trigger_hz = config.periodic.frequency_hz;
+ return len;
+ }
ret = ad4691_set_pwm_freq(st, freq);
if (ret)
@@ -1139,9 +1438,75 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
ad4691_buffer_attrs);
}
+static int ad4691_setup_offload(struct iio_dev *indio_dev,
+ struct ad4691_state *st,
+ struct spi_offload *spi_offload)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct ad4691_offload_state *offload;
+ struct dma_chan *rx_dma;
+ int ret;
+
+ offload = devm_kzalloc(dev, sizeof(*offload), GFP_KERNEL);
+ if (!offload)
+ return -ENOMEM;
+
+ offload->offload = spi_offload;
+ st->offload = offload;
+
+ if (st->manual_mode) {
+ offload->trigger =
+ devm_spi_offload_trigger_get(dev, offload->offload,
+ SPI_OFFLOAD_TRIGGER_PERIODIC);
+ if (IS_ERR(offload->trigger))
+ return dev_err_probe(dev, PTR_ERR(offload->trigger),
+ "Failed to get periodic offload trigger\n");
+
+ offload->trigger_hz = AD4691_OFFLOAD_INITIAL_TRIGGER_HZ;
+ } else {
+ struct spi_offload_trigger_info trigger_info = {
+ .fwnode = dev_fwnode(dev),
+ .ops = &ad4691_offload_trigger_ops,
+ .priv = st,
+ };
+
+ ret = devm_spi_offload_trigger_register(dev, &trigger_info);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to register offload trigger\n");
+
+ offload->trigger =
+ devm_spi_offload_trigger_get(dev, offload->offload,
+ SPI_OFFLOAD_TRIGGER_DATA_READY);
+ if (IS_ERR(offload->trigger))
+ return dev_err_probe(dev, PTR_ERR(offload->trigger),
+ "Failed to get DATA_READY offload trigger\n");
+ }
+
+ rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, offload->offload);
+ if (IS_ERR(rx_dma))
+ return dev_err_probe(dev, PTR_ERR(rx_dma),
+ "Failed to get offload RX DMA channel\n");
+
+ if (st->manual_mode)
+ indio_dev->setup_ops = &ad4691_manual_offload_buffer_setup_ops;
+ else
+ indio_dev->setup_ops = &ad4691_cnv_burst_offload_buffer_setup_ops;
+
+ ret = devm_iio_dmaengine_buffer_setup_with_handle(dev, indio_dev, rx_dma,
+ IIO_BUFFER_DIRECTION_IN);
+ if (ret)
+ return ret;
+
+ indio_dev->buffer->attrs = ad4691_buffer_attrs;
+
+ return 0;
+}
+
static int ad4691_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
+ struct spi_offload *spi_offload;
struct iio_dev *indio_dev;
struct ad4691_state *st;
int ret;
@@ -1175,11 +1540,26 @@ static int ad4691_probe(struct spi_device *spi)
if (ret)
return ret;
+ spi_offload = devm_spi_offload_get(dev, spi, &ad4691_offload_config);
+ ret = PTR_ERR_OR_ZERO(spi_offload);
+ if (ret == -ENODEV)
+ spi_offload = NULL;
+ else if (ret)
+ return dev_err_probe(dev, ret, "Failed to get SPI offload\n");
+
indio_dev->name = st->info->name;
indio_dev->info = &ad4691_info;
indio_dev->modes = INDIO_DIRECT_MODE;
- ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (spi_offload) {
+ indio_dev->channels = st->info->offload_info->channels;
+ indio_dev->num_channels = st->info->offload_info->num_channels;
+ ret = ad4691_setup_offload(indio_dev, st, spi_offload);
+ } else {
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ }
if (ret)
return ret;
@@ -1217,3 +1597,5 @@ module_spi_driver(ad4691_driver);
MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("IIO_DMA_BUFFER");
+MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER");
--
2.43.0
^ permalink raw reply related
* [PATCH v9 6/6] docs: iio: adc: ad4691: add driver documentation
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add RST documentation for the AD4691 family ADC driver covering
supported devices, IIO channels, operating modes, oversampling,
reference voltage, LDO supply, reset, GP pins, SPI offload support,
and buffer data format.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
Documentation/iio/ad4691.rst | 205 +++++++++++++++++++++++++++++++++++++++++++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 207 insertions(+)
diff --git a/Documentation/iio/ad4691.rst b/Documentation/iio/ad4691.rst
new file mode 100644
index 000000000000..38e2ad28a713
--- /dev/null
+++ b/Documentation/iio/ad4691.rst
@@ -0,0 +1,205 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+=============
+AD4691 driver
+=============
+
+ADC driver for Analog Devices Inc. AD4691 family of multichannel SAR ADCs.
+The module name is ``ad4691``.
+
+
+Supported devices
+=================
+
+The following chips are supported by this driver:
+
+* `AD4691 <https://www.analog.com/en/products/ad4691.html>`_ — 16-channel, 500 kSPS
+* `AD4692 <https://www.analog.com/en/products/ad4692.html>`_ — 16-channel, 1 MSPS
+* `AD4693 <https://www.analog.com/en/products/ad4693.html>`_ — 8-channel, 500 kSPS
+* `AD4694 <https://www.analog.com/en/products/ad4694.html>`_ — 8-channel, 1 MSPS
+
+
+IIO channels
+============
+
+Each physical ADC input maps to one IIO voltage channel. The AD4691 and AD4692
+expose 16 channels (``voltage0`` through ``voltage15``); the AD4693 and AD4694
+expose 8 channels (``voltage0`` through ``voltage7``).
+
+All channels share a common scale (``in_voltage_scale``), derived from the
+reference voltage. Each channel independently exposes:
+
+* ``in_voltageN_raw`` — single-shot ADC result
+* ``in_voltageN_sampling_frequency`` — per-channel effective output rate,
+ defined as the internal oscillator frequency divided by the channel's
+ oversampling ratio. Writing this attribute selects the nearest achievable
+ rate for the current OSR; the value read back reflects the actual rate after
+ snapping to the closest valid oscillator entry.
+* ``in_voltageN_sampling_frequency_available`` — list of achievable effective
+ rates for the channel's current oversampling ratio. The list updates
+ dynamically when the oversampling ratio changes.
+
+The following attributes are only available in CNV Burst Mode:
+
+* ``in_voltageN_oversampling_ratio`` — per-channel hardware oversampling depth;
+ see `Oversampling`_ below.
+* ``in_voltageN_oversampling_ratio_available`` — valid ratios: 1, 2, 4, 8, 16,
+ 32.
+
+
+Operating modes
+===============
+
+The driver supports two operating modes, selected automatically from the
+device tree at probe time.
+
+Manual Mode
+-----------
+
+Selected when no ``pwms`` property is present in the device tree. The CNV pin
+is tied to the SPI chip-select: every CS assertion triggers a conversion and
+returns the previous result. A user-defined IIO trigger (e.g. hrtimer trigger)
+drives the buffer.
+
+Oversampling is not supported in Manual Mode.
+
+CNV Burst Mode
+--------------
+
+Selected when a ``pwms`` property is present in the device tree. A PWM drives
+the CNV pin at the configured conversion rate. A GP pin wired to the SoC and
+declared in the device tree signals DATA_READY at the end of each burst,
+triggering a readout of all active channel results into the IIO buffer.
+
+The buffer output rate is controlled by the ``sampling_frequency`` attribute
+on the IIO buffer. In practice the PWM rate should be set low enough to allow
+the SPI readout to complete before the next conversion burst begins.
+
+Autonomous Mode (idle / single-shot)
+-------------------------------------
+
+When the IIO buffer is disabled, ``in_voltageN_raw`` reads perform a single
+conversion on the requested channel using the internal oscillator. The
+oscillator is started and stopped around each read to save power.
+
+
+Oversampling
+============
+
+In CNV Burst Mode each channel has an independent hardware accumulator that
+averages a configurable number of successive conversions. The result is always
+returned as a 16-bit mean, so ``realbits`` and ``storagebits`` are unaffected
+by the oversampling ratio. Valid ratios are 1, 2, 4, 8, 16 and 32; the default
+is 1 (no averaging). Oversampling is not supported in Manual Mode.
+
+.. code-block:: bash
+
+ # Set oversampling ratio to 16 on channel 0
+ echo 16 > /sys/bus/iio/devices/iio:device0/in_voltage0_oversampling_ratio
+
+ # Read the resulting effective sampling frequency
+ cat /sys/bus/iio/devices/iio:device0/in_voltage0_sampling_frequency
+
+Writing ``oversampling_ratio`` stores the new depth for that channel;
+the internal oscillator is unaffected. The effective rate read back via
+``in_voltageN_sampling_frequency`` becomes ``osc_freq / new_osr``
+automatically. ``oversampling_ratio`` and ``sampling_frequency`` are
+orthogonal: one controls averaging depth, the other controls the oscillator.
+
+All channels share one internal oscillator. Writing ``sampling_frequency`` for
+any channel updates the oscillator and therefore affects the effective rate
+read back from all other channels.
+
+
+Reference voltage
+=================
+
+The driver supports two reference configurations, mutually exclusive:
+
+* **External reference** (``ref-supply``): a voltage between 2.4 V and 5.25 V
+ supplied externally.
+* **Buffered internal reference** (``refin-supply``): an internal reference
+ buffer is enabled by the driver.
+
+Exactly one of ``ref-supply`` or ``refin-supply`` must be present in the
+device tree. The reference voltage determines the full-scale range reported
+via ``in_voltage_scale``.
+
+
+LDO supply
+==========
+
+The chip contains an internal LDO that powers part of the analog front-end.
+The supply configuration is mutually exclusive:
+
+* **External VDD** (``vdd-supply``): an external 1.8 V supply is used directly;
+ the internal LDO is disabled.
+* **Internal LDO** (``ldo-in-supply``): the internal LDO is enabled and fed
+ from the ``ldo-in`` regulator. Use this when no external 1.8 V VDD is present.
+
+Exactly one of ``vdd-supply`` or ``ldo-in-supply`` must be provided.
+
+
+Reset
+=====
+
+The driver supports two reset mechanisms:
+
+* **Hardware reset** (``reset-gpios`` in device tree): asserted at probe by
+ the reset controller framework.
+* **Software reset** (fallback when ``reset-gpios`` is absent): written
+ automatically at probe.
+
+
+GP pins and interrupts
+======================
+
+The chip exposes up to four general-purpose (GP) pins. In CNV Burst Mode
+(non-offload), one GP pin must be wired to an interrupt-capable SoC input and
+declared in the device tree using the ``interrupts`` and ``interrupt-names``
+properties. The ``interrupt-names`` value identifies which GP pin is used
+(``"gp0"`` through ``"gp3"``).
+
+Example device tree fragment::
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ ...
+ interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
+ interrupt-parent = <&gpio0>;
+ interrupt-names = "gp0";
+ };
+
+
+SPI offload support
+===================
+
+When a SPI offload engine (e.g. the AXI SPI Engine) is present, the driver
+uses DMA-backed transfers for CPU-independent, high-throughput data capture.
+SPI offload is detected automatically at probe; if no offload hardware is
+available the driver falls back to the software triggered-buffer path.
+
+Two SPI offload sub-modes exist:
+
+CNV Burst offload
+-----------------
+
+Used when a ``pwms`` property is present and SPI offload is available. The PWM
+drives CNV at the configured rate; on DATA_READY the offload engine reads all
+active channel results and streams them directly to the IIO DMA buffer with no
+CPU involvement. The GP pin used as DATA_READY trigger is supplied by the
+trigger-source consumer at buffer enable time; no ``interrupt-names`` entry is
+required.
+
+Manual offload
+--------------
+
+Used when no ``pwms`` property is present and SPI offload is available. A
+periodic SPI offload trigger controls the conversion rate and the offload engine
+streams results directly to the IIO DMA buffer.
+
+The ``sampling_frequency`` attribute on the IIO buffer controls the trigger
+rate (in Hz). The initial rate is 100 kHz.
+
+Oversampling is not supported in Manual Mode.
+
diff --git a/Documentation/iio/index.rst b/Documentation/iio/index.rst
index ba3e609c6a13..007e0a1fcc5a 100644
--- a/Documentation/iio/index.rst
+++ b/Documentation/iio/index.rst
@@ -23,6 +23,7 @@ Industrial I/O Kernel Drivers
ad4000
ad4030
ad4062
+ ad4691
ad4695
ad7191
ad7380
diff --git a/MAINTAINERS b/MAINTAINERS
index 24e4502b8292..819d8b6eb6bb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1491,6 +1491,7 @@ S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
F: drivers/iio/adc/ad4691.c
+F: drivers/iio/adc/ad4691.rst
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
--
2.43.0
^ permalink raw reply related
* [PATCH v9 5/6] iio: adc: ad4691: add oversampling support
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add per-channel oversampling ratio (OSR) support for CNV burst mode.
The accumulator depth register (ACC_DEPTH_IN) is programmed with the
selected OSR at buffer enable time and before each single-shot read.
Supported OSR values: 1, 2, 4, 8, 16, 32.
Introduce AD4691_MANUAL_CHANNEL() for manual mode channels, which do
not expose the oversampling ratio attribute since OSR is not applicable
in that mode. A separate manual_channels array is added to
struct ad4691_channel_info and selected at probe time; offload paths
reuse the same arrays with num_channels capping access before the soft
timestamp entry.
in_voltageN_sampling_frequency represents the effective output rate for
channel N, defined as osc_freq / osr[N]. The chip has one internal
oscillator shared by all channels; each channel independently
accumulates osr[N] oscillator cycles before producing a result.
Writing sampling_frequency computes needed_osc = freq * osr[N] and
snaps down to the largest oscillator table entry that satisfies both
osc <= needed_osc and osc % osr[N] == 0, guaranteeing an exact integer
read-back. The result is stored in target_osc_freq_Hz and written to
OSC_FREQ_REG at buffer enable and single-shot time, so sampling_frequency
and oversampling_ratio can be set in any order.
in_voltageN_sampling_frequency_available is computed dynamically from
the channel's current OSR, listing only oscillator table entries that
divide evenly by osr[N], expressed as effective rates. The list becomes
sparser as OSR increases, capping at max_rate / osr[N].
Writing oversampling_ratio stores the new OSR for that channel;
target_osc_freq_Hz is left unchanged. The effective rate read back via
in_voltageN_sampling_frequency becomes target_osc_freq_Hz / new_osr
automatically. The two attributes are orthogonal: sampling_frequency
controls the oscillator, oversampling_ratio controls the averaging depth.
OSR defaults to 1 (no accumulation) for all channels.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/ad4691.c | 251 +++++++++++++++++++++++++++++++++++++++++------
1 file changed, 221 insertions(+), 30 deletions(-)
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index 50d81d87d4a0..45605a97a403 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -115,6 +115,7 @@ enum ad4691_ref_ctrl {
struct ad4691_channel_info {
const struct iio_chan_spec *channels __counted_by_ptr(num_channels);
+ const struct iio_chan_spec *manual_channels __counted_by_ptr(num_channels);
unsigned int num_channels;
};
@@ -125,7 +126,34 @@ struct ad4691_chip_info {
const struct ad4691_channel_info *offload_info;
};
+/* CNV burst mode channel — exposes oversampling ratio. */
#define AD4691_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_OVERSAMPLING_RATIO) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ .endianness = IIO_BE, \
+ }, \
+ }
+
+/*
+ * Manual mode channel — no oversampling ratio attribute. OSR is not
+ * supported in manual mode; ACC_DEPTH_IN is not configured during manual
+ * buffer enable.
+ */
+#define AD4691_MANUAL_CHANNEL(ch) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
@@ -176,25 +204,65 @@ static const struct iio_chan_spec ad4693_channels[] = {
IIO_CHAN_SOFT_TIMESTAMP(8),
};
+static const struct iio_chan_spec ad4691_manual_channels[] = {
+ AD4691_MANUAL_CHANNEL(0),
+ AD4691_MANUAL_CHANNEL(1),
+ AD4691_MANUAL_CHANNEL(2),
+ AD4691_MANUAL_CHANNEL(3),
+ AD4691_MANUAL_CHANNEL(4),
+ AD4691_MANUAL_CHANNEL(5),
+ AD4691_MANUAL_CHANNEL(6),
+ AD4691_MANUAL_CHANNEL(7),
+ AD4691_MANUAL_CHANNEL(8),
+ AD4691_MANUAL_CHANNEL(9),
+ AD4691_MANUAL_CHANNEL(10),
+ AD4691_MANUAL_CHANNEL(11),
+ AD4691_MANUAL_CHANNEL(12),
+ AD4691_MANUAL_CHANNEL(13),
+ AD4691_MANUAL_CHANNEL(14),
+ AD4691_MANUAL_CHANNEL(15),
+ IIO_CHAN_SOFT_TIMESTAMP(16),
+};
+
+static const struct iio_chan_spec ad4693_manual_channels[] = {
+ AD4691_MANUAL_CHANNEL(0),
+ AD4691_MANUAL_CHANNEL(1),
+ AD4691_MANUAL_CHANNEL(2),
+ AD4691_MANUAL_CHANNEL(3),
+ AD4691_MANUAL_CHANNEL(4),
+ AD4691_MANUAL_CHANNEL(5),
+ AD4691_MANUAL_CHANNEL(6),
+ AD4691_MANUAL_CHANNEL(7),
+ IIO_CHAN_SOFT_TIMESTAMP(8),
+};
+
+static const int ad4691_oversampling_ratios[] = { 1, 2, 4, 8, 16, 32 };
+
static const struct ad4691_channel_info ad4691_sw_info = {
.channels = ad4691_channels,
+ .manual_channels = ad4691_manual_channels,
.num_channels = ARRAY_SIZE(ad4691_channels),
};
static const struct ad4691_channel_info ad4693_sw_info = {
.channels = ad4693_channels,
+ .manual_channels = ad4693_manual_channels,
.num_channels = ARRAY_SIZE(ad4693_channels),
};
static const struct ad4691_channel_info ad4691_offload_info = {
.channels = ad4691_channels,
- /* Exclude the soft timestamp entry; num_channels caps access. */
+ /*
+ * Offload paths share the SW channel arrays. num_channels caps access
+ * before the soft timestamp entry, so no separate array is needed.
+ */
+ .manual_channels = ad4691_manual_channels,
.num_channels = ARRAY_SIZE(ad4691_channels) - 1,
};
static const struct ad4691_channel_info ad4693_offload_info = {
.channels = ad4693_channels,
- /* Exclude the soft timestamp entry; num_channels caps access. */
+ .manual_channels = ad4693_manual_channels,
.num_channels = ARRAY_SIZE(ad4693_channels) - 1,
};
@@ -269,6 +337,19 @@ struct ad4691_state {
int irq;
int vref_uV;
u32 cnv_period_ns;
+ /*
+ * Snapped oscillator frequency (Hz) shared by all channels. Set when
+ * sampling_frequency or oversampling_ratio is written; written to
+ * OSC_FREQ_REG at buffer enable and single-shot time so both attributes
+ * can be set in any order. Reading in_voltageN_sampling_frequency
+ * returns target_osc_freq_Hz / osr[N] — the effective rate for that
+ * channel given its oversampling ratio.
+ */
+ u32 target_osc_freq_Hz;
+ /* Per-channel oversampling ratio; always 1 in manual mode. */
+ u8 osr[16];
+ /* Scratch buffer for read_avail SAMP_FREQ; content is OSR-dependent. */
+ int samp_freq_avail[ARRAY_SIZE(ad4691_osc_freqs_Hz)];
bool manual_mode;
bool refbuf_en;
@@ -489,6 +570,18 @@ static const struct regmap_config ad4691_regmap_config = {
.cache_type = REGCACHE_MAPLE,
};
+/* Write target_osc_freq_Hz to OSC_FREQ_REG. Called at use time. */
+static int ad4691_write_osc_freq(struct ad4691_state *st)
+{
+ unsigned int i;
+
+ for (i = 0; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] == st->target_osc_freq_Hz)
+ return regmap_write(st->regmap, AD4691_OSC_FREQ_REG, i);
+ }
+ return -EINVAL;
+}
+
/*
* Index 0 in ad4691_osc_freqs_Hz is 1 MHz — valid only for AD4692/AD4694
* (max_rate == 1 MHz). AD4691/AD4693 cap at 500 kHz so their valid range
@@ -499,36 +592,58 @@ static unsigned int ad4691_samp_freq_start(const struct ad4691_chip_info *info)
return (info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1;
}
-static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
+/*
+ * Find the largest oscillator table entry that is both <= needed_osc and
+ * evenly divisible by osr (guaranteeing an integer effective rate on
+ * read-back). Returns 0 if no such entry exists in the chip's valid range.
+ */
+static unsigned int ad4691_find_osc_freq(struct ad4691_state *st,
+ unsigned int needed_osc,
+ unsigned int osr)
{
- unsigned int reg_val;
- int ret;
+ unsigned int start = ad4691_samp_freq_start(st->info);
- ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
- if (ret)
- return ret;
+ for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if ((unsigned int)ad4691_osc_freqs_Hz[i] > needed_osc)
+ continue;
+ if (ad4691_osc_freqs_Hz[i] % osr != 0)
+ continue;
+ return ad4691_osc_freqs_Hz[i];
+ }
+ return 0;
+}
- *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)];
+static int ad4691_get_sampling_freq(struct ad4691_state *st, u8 osr, int *val)
+{
+ *val = st->target_osc_freq_Hz / osr;
return IIO_VAL_INT;
}
-static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
+static int ad4691_set_sampling_freq(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int freq)
{
struct ad4691_state *st = iio_priv(indio_dev);
- unsigned int start = ad4691_samp_freq_start(st->info);
+ unsigned int osr = st->osr[chan->channel];
+ unsigned int found;
IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
if (IIO_DEV_ACQUIRE_FAILED(claim))
return -EBUSY;
- for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
- if (ad4691_osc_freqs_Hz[i] != freq)
- continue;
- return regmap_update_bits(st->regmap, AD4691_OSC_FREQ_REG,
- AD4691_OSC_FREQ_MASK, i);
- }
+ if (freq <= 0 || (unsigned int)freq > st->info->max_rate / osr)
+ return -EINVAL;
- return -EINVAL;
+ found = ad4691_find_osc_freq(st, (unsigned int)freq * osr, osr);
+ if (!found)
+ return -EINVAL;
+
+ /*
+ * Store the snapped oscillator frequency; OSC_FREQ_REG is written at
+ * buffer enable and single-shot time so that sampling_frequency and
+ * oversampling_ratio can be set in any order.
+ */
+ st->target_osc_freq_Hz = found;
+ return 0;
}
static int ad4691_read_avail(struct iio_dev *indio_dev,
@@ -540,10 +655,30 @@ static int ad4691_read_avail(struct iio_dev *indio_dev,
unsigned int start = ad4691_samp_freq_start(st->info);
switch (mask) {
- case IIO_CHAN_INFO_SAMP_FREQ:
- *vals = &ad4691_osc_freqs_Hz[start];
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ unsigned int osr = st->osr[chan->channel];
+ int n = 0;
+
+ /*
+ * Only oscillator frequencies evenly divisible by the channel's
+ * OSR yield an integer effective rate; expose those as effective
+ * rates (osc / osr) so the user works entirely in output-sample
+ * space.
+ */
+ for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] % osr != 0)
+ continue;
+ st->samp_freq_avail[n++] = ad4691_osc_freqs_Hz[i] / osr;
+ }
+ *vals = st->samp_freq_avail;
*type = IIO_VAL_INT;
- *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
+ *length = n;
+ return IIO_AVAIL_LIST;
+ }
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ *vals = ad4691_oversampling_ratios;
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(ad4691_oversampling_ratios);
return IIO_AVAIL_LIST;
default:
return -EINVAL;
@@ -554,7 +689,7 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan, int *val)
{
struct ad4691_state *st = iio_priv(indio_dev);
- unsigned int reg_val, osc_idx, period_us;
+ unsigned int reg_val, period_us;
int ret;
guard(mutex)(&st->lock);
@@ -575,7 +710,12 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
if (ret)
return ret;
- ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(chan->channel),
+ st->osr[chan->channel]);
+ if (ret)
+ return ret;
+
+ ret = ad4691_write_osc_freq(st);
if (ret)
return ret;
@@ -583,9 +723,12 @@ static int ad4691_single_shot_read(struct iio_dev *indio_dev,
if (ret)
return ret;
- osc_idx = FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val);
- /* Wait 2 oscillator periods for the conversion to complete. */
- period_us = DIV_ROUND_UP(2UL * USEC_PER_SEC, ad4691_osc_freqs_Hz[osc_idx]);
+ /*
+ * Wait osr + 1 oscillator periods: osr for accumulation, +1 for the
+ * pipeline margin (one extra period ensures the final result is ready).
+ */
+ period_us = DIV_ROUND_UP((unsigned long)(st->osr[chan->channel] + 1) * USEC_PER_SEC,
+ st->target_osc_freq_Hz);
fsleep(period_us);
ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 0);
@@ -620,7 +763,10 @@ static int ad4691_read_raw(struct iio_dev *indio_dev,
return ad4691_single_shot_read(indio_dev, chan, val);
}
case IIO_CHAN_INFO_SAMP_FREQ:
- return ad4691_get_sampling_freq(st, val);
+ return ad4691_get_sampling_freq(st, st->osr[chan->channel], val);
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO:
+ *val = st->osr[chan->channel];
+ return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
*val = st->vref_uV / (MICRO / MILLI);
*val2 = chan->scan_type.realbits;
@@ -634,9 +780,29 @@ static int ad4691_write_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int val, int val2, long mask)
{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
- return ad4691_set_sampling_freq(indio_dev, val);
+ return ad4691_set_sampling_freq(indio_dev, chan, val);
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_oversampling_ratios); i++) {
+ if (ad4691_oversampling_ratios[i] != val)
+ continue;
+ /*
+ * Store the new OSR; target_osc_freq_Hz is unchanged.
+ * The effective rate read back via in_voltageN_sampling_frequency
+ * becomes target_osc_freq_Hz / new_osr automatically.
+ */
+ st->osr[chan->channel] = val;
+ return 0;
+ }
+ return -EINVAL;
+ }
default:
return -EINVAL;
}
@@ -691,6 +857,10 @@ static int ad4691_enter_conversion_mode(struct ad4691_state *st)
return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
+ ret = ad4691_write_osc_freq(st);
+ if (ret)
+ return ret;
+
ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
if (ret)
@@ -844,6 +1014,12 @@ static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
if (ret)
goto err_unoptimize;
+ iio_for_each_active_channel(indio_dev, i) {
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(i), st->osr[i]);
+ if (ret)
+ goto err_unoptimize;
+ }
+
ret = ad4691_enter_conversion_mode(st);
if (ret)
goto err_unoptimize;
@@ -995,6 +1171,12 @@ static int ad4691_cnv_burst_offload_buffer_postenable(struct iio_dev *indio_dev)
if (ret)
return ret;
+ iio_for_each_active_channel(indio_dev, bit) {
+ ret = regmap_write(st->regmap, AD4691_ACC_DEPTH_IN(bit), st->osr[bit]);
+ if (ret)
+ return ret;
+ }
+
ret = ad4691_enter_conversion_mode(st);
if (ret)
return ret;
@@ -1361,6 +1543,8 @@ static int ad4691_config(struct ad4691_state *st)
if (ret)
return dev_err_probe(dev, ret, "Failed to write OSC_FREQ\n");
+ st->target_osc_freq_Hz = ad4691_osc_freqs_Hz[ad4691_samp_freq_start(st->info)];
+
ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
if (ret)
@@ -1518,6 +1702,7 @@ static int ad4691_probe(struct spi_device *spi)
st = iio_priv(indio_dev);
st->spi = spi;
st->info = spi_get_device_match_data(spi);
+ memset(st->osr, 1, sizeof(st->osr));
ret = devm_mutex_init(dev, &st->lock);
if (ret)
@@ -1552,11 +1737,17 @@ static int ad4691_probe(struct spi_device *spi)
indio_dev->modes = INDIO_DIRECT_MODE;
if (spi_offload) {
- indio_dev->channels = st->info->offload_info->channels;
+ if (st->manual_mode)
+ indio_dev->channels = st->info->offload_info->manual_channels;
+ else
+ indio_dev->channels = st->info->offload_info->channels;
indio_dev->num_channels = st->info->offload_info->num_channels;
ret = ad4691_setup_offload(indio_dev, st, spi_offload);
} else {
- indio_dev->channels = st->info->sw_info->channels;
+ if (st->manual_mode)
+ indio_dev->channels = st->info->sw_info->manual_channels;
+ else
+ indio_dev->channels = st->info->sw_info->channels;
indio_dev->num_channels = st->info->sw_info->num_channels;
ret = ad4691_setup_triggered_buffer(indio_dev, st);
}
--
2.43.0
^ permalink raw reply related
* [PATCH v9 3/6] iio: adc: ad4691: add triggered buffer support
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add buffered capture support using the IIO triggered buffer framework.
CNV Burst Mode: the GP pin identified by interrupt-names in the device
tree is configured as DATA_READY output. The IRQ handler stops
conversions and fires the IIO trigger; the trigger handler executes a
pre-built SPI message that reads all active channels from the AVG_IN
accumulator registers and then resets accumulator state and restarts
conversions for the next cycle.
Manual Mode: CNV is tied to SPI CS so each transfer simultaneously
reads the previous result and starts the next conversion (pipelined
N+1 scheme). At preenable time a pre-built, optimised SPI message of
N+1 transfers is constructed (N channel reads plus one NOOP to drain
the pipeline). The trigger handler executes the message in a single
spi_sync() call and collects the results. An external trigger (e.g.
iio-trig-hrtimer) is required to drive the trigger at the desired
sample rate.
Both modes share the same trigger handler and push a complete scan —
one u16 slot per channel at its scan_index position, followed by a
timestamp — to the IIO buffer via iio_push_to_buffers_with_ts().
The CNV Burst Mode sampling frequency (PWM period) is exposed as a
buffer-level attribute via IIO_DEVICE_ATTR.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 523 +++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 511 insertions(+), 14 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 3685a03aa8dc..d498f16c0816 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -142,6 +142,8 @@ config AD4170_4
config AD4691
tristate "Analog Devices AD4691 Family ADC Driver"
depends on SPI
+ select IIO_BUFFER
+ select IIO_TRIGGERED_BUFFER
select REGMAP
help
Say yes here to build support for Analog Devices AD4691 Family MuxSAR
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index 05826b762c7f..c1e3406fef18 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -5,16 +5,20 @@
*/
#include <linux/array_size.h>
#include <linux/bitfield.h>
-#include <linux/bitops.h>
+#include <linux/bitmap.h>
#include <linux/cleanup.h>
#include <linux/delay.h>
#include <linux/dev_printk.h>
#include <linux/device/devres.h>
+#include <linux/dmaengine.h>
#include <linux/err.h>
+#include <linux/interrupt.h>
#include <linux/limits.h>
#include <linux/math.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
+#include <linux/property.h>
+#include <linux/pwm.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#include <linux/reset.h>
@@ -22,7 +26,12 @@
#include <linux/units.h>
#include <linux/unaligned.h>
+#include <linux/iio/buffer.h>
#include <linux/iio/iio.h>
+#include <linux/iio/sysfs.h>
+#include <linux/iio/trigger.h>
+#include <linux/iio/triggered_buffer.h>
+#include <linux/iio/trigger_consumer.h>
#define AD4691_VREF_uV_MIN 2400000
#define AD4691_VREF_uV_MAX 5250000
@@ -31,6 +40,9 @@
#define AD4691_VREF_3P3_uV_MAX 3750000
#define AD4691_VREF_4P096_uV_MAX 4500000
+#define AD4691_CNV_DUTY_CYCLE_NS 380
+#define AD4691_CNV_HIGH_TIME_NS 430
+
#define AD4691_SPI_CONFIG_A_REG 0x000
#define AD4691_SW_RESET (BIT(7) | BIT(0))
@@ -38,6 +50,7 @@
#define AD4691_CLAMP_STATUS1_REG 0x01A
#define AD4691_CLAMP_STATUS2_REG 0x01B
#define AD4691_DEVICE_SETUP 0x020
+#define AD4691_MANUAL_MODE BIT(2)
#define AD4691_LDO_EN BIT(4)
#define AD4691_REF_CTRL 0x021
#define AD4691_REF_CTRL_MASK GENMASK(4, 2)
@@ -45,13 +58,18 @@
#define AD4691_OSC_FREQ_REG 0x023
#define AD4691_OSC_FREQ_MASK GENMASK(3, 0)
#define AD4691_STD_SEQ_CONFIG 0x025
+#define AD4691_SEQ_ALL_CHANNELS_OFF 0x00
#define AD4691_SPARE_CONTROL 0x02A
+#define AD4691_NOOP 0x00
+#define AD4691_ADC_CHAN(ch) ((0x10 + (ch)) << 3)
+
#define AD4691_OSC_EN_REG 0x180
#define AD4691_STATE_RESET_REG 0x181
#define AD4691_STATE_RESET_ALL 0x01
#define AD4691_ADC_SETUP 0x182
#define AD4691_ADC_MODE_MASK GENMASK(1, 0)
+#define AD4691_CNV_BURST_MODE 0x01
#define AD4691_AUTONOMOUS_MODE 0x02
/*
* ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
@@ -61,6 +79,8 @@
#define AD4691_ACC_DEPTH_IN(n) (0x186 + (n))
#define AD4691_GPIO_MODE1_REG 0x196
#define AD4691_GPIO_MODE2_REG 0x197
+#define AD4691_GP_MODE_MASK GENMASK(3, 0)
+#define AD4691_GP_MODE_DATA_READY 0x06
#define AD4691_GPIO_READ 0x1A0
#define AD4691_ACC_STATUS_FULL1_REG 0x1B0
#define AD4691_ACC_STATUS_FULL2_REG 0x1B1
@@ -99,8 +119,8 @@ struct ad4691_chip_info {
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
- .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
- | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_separate_available = \
BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
@@ -110,6 +130,7 @@ struct ad4691_chip_info {
.sign = 'u', \
.realbits = 16, \
.storagebits = 16, \
+ .endianness = IIO_BE, \
}, \
}
@@ -130,6 +151,7 @@ static const struct iio_chan_spec ad4691_channels[] = {
AD4691_CHANNEL(13),
AD4691_CHANNEL(14),
AD4691_CHANNEL(15),
+ IIO_CHAN_SOFT_TIMESTAMP(16),
};
static const struct iio_chan_spec ad4693_channels[] = {
@@ -141,6 +163,17 @@ static const struct iio_chan_spec ad4693_channels[] = {
AD4691_CHANNEL(5),
AD4691_CHANNEL(6),
AD4691_CHANNEL(7),
+ IIO_CHAN_SOFT_TIMESTAMP(8),
+};
+
+static const struct ad4691_channel_info ad4691_sw_info = {
+ .channels = ad4691_channels,
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+};
+
+static const struct ad4691_channel_info ad4693_sw_info = {
+ .channels = ad4693_channels,
+ .num_channels = ARRAY_SIZE(ad4693_channels),
};
/*
@@ -167,15 +200,7 @@ static const int ad4691_osc_freqs_Hz[] = {
[0xF] = 1250,
};
-static const struct ad4691_channel_info ad4691_sw_info = {
- .channels = ad4691_channels,
- .num_channels = ARRAY_SIZE(ad4691_channels),
-};
-
-static const struct ad4691_channel_info ad4693_sw_info = {
- .channels = ad4693_channels,
- .num_channels = ARRAY_SIZE(ad4693_channels),
-};
+static const char * const ad4691_gp_names[] = { "gp0", "gp1", "gp2", "gp3" };
static const struct ad4691_chip_info ad4691_chip_info = {
.name = "ad4691",
@@ -204,7 +229,14 @@ static const struct ad4691_chip_info ad4694_chip_info = {
struct ad4691_state {
const struct ad4691_chip_info *info;
struct regmap *regmap;
+ struct spi_device *spi;
+
+ struct pwm_device *conv_trigger;
+ int irq;
int vref_uV;
+ u32 cnv_period_ns;
+
+ bool manual_mode;
bool refbuf_en;
bool ldo_en;
/*
@@ -212,8 +244,45 @@ struct ad4691_state {
* atomicity of consecutive SPI operations.
*/
struct mutex lock;
+ /*
+ * Per-buffer-enable lifetime resources:
+ * Manual Mode - a pre-built SPI message that clocks out N+1
+ * transfers in one go.
+ * CNV Burst Mode - a pre-built SPI message that clocks out 2*N
+ * transfers in one go.
+ */
+ struct spi_message scan_msg;
+ /* max 16 + 1 NOOP (manual) or 2*16 + 2 (CNV burst). */
+ struct spi_transfer scan_xfers[34];
+ /*
+ * CNV burst: 16 AVG_IN addresses + state-reset address + state-reset
+ * value = 18. Manual: 16 channel cmds + 1 NOOP = 17.
+ */
+ __be16 scan_tx[18] __aligned(IIO_DMA_MINALIGN);
+ /*
+ * Scan buffer: one BE16 slot per active channel, plus timestamp.
+ * DMA-aligned because scan_xfers point rx_buf directly into vals[].
+ */
+ IIO_DECLARE_DMA_BUFFER_WITH_TS(__be16, vals, 16);
};
+/*
+ * Configure the given GP pin (0-3) as DATA_READY output.
+ * GP0/GP1 → GPIO_MODE1_REG, GP2/GP3 → GPIO_MODE2_REG.
+ * Even pins occupy bits [3:0], odd pins bits [7:4].
+ */
+static int ad4691_gpio_setup(struct ad4691_state *st, unsigned int gp_num)
+{
+ unsigned int bit_off = gp_num % 2;
+ unsigned int reg_off = gp_num / 2;
+ unsigned int shift = 4 * bit_off;
+
+ return regmap_update_bits(st->regmap,
+ AD4691_GPIO_MODE1_REG + reg_off,
+ AD4691_GP_MODE_MASK << shift,
+ AD4691_GP_MODE_DATA_READY << shift);
+}
+
static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
{
struct spi_device *spi = context;
@@ -507,6 +576,333 @@ static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
return regmap_write(st->regmap, reg, writeval);
}
+static int ad4691_set_pwm_freq(struct ad4691_state *st, int freq)
+{
+ if (!freq)
+ return -EINVAL;
+
+ st->cnv_period_ns = DIV_ROUND_UP(NSEC_PER_SEC, freq);
+ return 0;
+}
+
+static int ad4691_sampling_enable(struct ad4691_state *st, bool enable)
+{
+ struct pwm_state conv_state = {
+ .period = st->cnv_period_ns,
+ .duty_cycle = AD4691_CNV_DUTY_CYCLE_NS,
+ .polarity = PWM_POLARITY_NORMAL,
+ .enabled = enable,
+ };
+
+ return pwm_apply_might_sleep(st->conv_trigger, &conv_state);
+}
+
+/*
+ * ad4691_enter_conversion_mode - Switch the chip to its buffer conversion mode.
+ *
+ * Configures the ADC hardware registers for the mode selected at probe
+ * (CNV_BURST or MANUAL). Called from buffer preenable before starting
+ * sampling. The chip is in AUTONOMOUS mode during idle (for read_raw).
+ */
+static int ad4691_enter_conversion_mode(struct ad4691_state *st)
+{
+ int ret;
+
+ if (st->manual_mode)
+ return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_MANUAL_MODE, AD4691_MANUAL_MODE);
+
+ ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_CNV_BURST_MODE);
+ if (ret)
+ return ret;
+
+ return regmap_write(st->regmap, AD4691_STATE_RESET_REG,
+ AD4691_STATE_RESET_ALL);
+}
+
+/*
+ * ad4691_exit_conversion_mode - Return the chip to AUTONOMOUS mode.
+ *
+ * Called from buffer postdisable to restore the chip to the
+ * idle state used by read_raw. Clears the sequencer and resets state.
+ */
+static int ad4691_exit_conversion_mode(struct ad4691_state *st)
+{
+ if (st->manual_mode)
+ return regmap_update_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_MANUAL_MODE, 0);
+
+ return regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
+}
+
+static int ad4691_manual_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int prev_i, k, i;
+ bool first;
+ int ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ spi_message_init(&st->scan_msg);
+
+ first = true;
+ prev_i = 0;
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ st->scan_tx[k] = cpu_to_be16(AD4691_ADC_CHAN(i));
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k];
+ /*
+ * The pipeline means xfer[0] receives the residual from the
+ * previous sequence, not a valid sample for channel i. Point
+ * it at vals[i] anyway; xfer[1] (or the NOOP when only one
+ * channel is active) will overwrite that slot with the real
+ * result, so no separate dummy buffer is needed.
+ */
+ if (first) {
+ st->scan_xfers[k].rx_buf = &st->vals[i];
+ first = false;
+ } else {
+ st->scan_xfers[k].rx_buf = &st->vals[prev_i];
+ }
+ st->scan_xfers[k].len = sizeof(__be16);
+ st->scan_xfers[k].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+ prev_i = i;
+ k++;
+ }
+
+ /* Final NOOP transfer retrieves the last channel's result. */
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k]; /* scan_tx[k] == 0 == NOOP */
+ st->scan_xfers[k].rx_buf = &st->vals[prev_i];
+ st->scan_xfers[k].len = sizeof(__be16);
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret) {
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ad4691_manual_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ ret = ad4691_exit_conversion_mode(st);
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static const struct iio_buffer_setup_ops ad4691_manual_buffer_setup_ops = {
+ .preenable = &ad4691_manual_buffer_preenable,
+ .postdisable = &ad4691_manual_buffer_postdisable,
+};
+
+static int ad4691_cnv_burst_buffer_preenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int k, i;
+ int ret;
+
+ memset(st->scan_xfers, 0, sizeof(st->scan_xfers));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ spi_message_init(&st->scan_msg);
+
+ /*
+ * Each AVG_IN read needs two transfers: a 2-byte address write phase
+ * followed by a 2-byte data read phase. CS toggles between channels
+ * (cs_change=1 on the read phase of all but the last channel).
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ st->scan_tx[k] = cpu_to_be16(0x8000 | AD4691_AVG_IN(i));
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(__be16);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+ st->scan_xfers[2 * k + 1].rx_buf = &st->vals[i];
+ st->scan_xfers[2 * k + 1].len = sizeof(__be16);
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
+ k++;
+ }
+
+ st->scan_tx[k] = cpu_to_be16(AD4691_STATE_RESET_REG);
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(__be16);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+ st->scan_tx[k + 1] = cpu_to_be16(AD4691_STATE_RESET_ALL << 8);
+ st->scan_xfers[2 * k + 1].tx_buf = &st->scan_tx[k + 1];
+ st->scan_xfers[2 * k + 1].len = sizeof(__be16);
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)));
+ if (ret)
+ goto err_unoptimize;
+
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
+ ~bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0));
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ goto err_exit_conv;
+
+ enable_irq(st->irq);
+ return 0;
+
+err_exit_conv:
+ ad4691_exit_conversion_mode(st);
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static int ad4691_cnv_burst_buffer_postdisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ disable_irq(st->irq);
+
+ ret = ad4691_sampling_enable(st, false);
+ if (ret)
+ return ret;
+
+ ret = ad4691_exit_conversion_mode(st);
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static const struct iio_buffer_setup_ops ad4691_cnv_burst_buffer_setup_ops = {
+ .preenable = &ad4691_cnv_burst_buffer_preenable,
+ .postdisable = &ad4691_cnv_burst_buffer_postdisable,
+};
+
+static ssize_t sampling_frequency_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ return sysfs_emit(buf, "%lu\n", NSEC_PER_SEC / st->cnv_period_ns);
+}
+
+static ssize_t sampling_frequency_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t len)
+{
+ struct iio_dev *indio_dev = dev_to_iio_dev(dev);
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int freq, ret;
+
+ ret = kstrtoint(buf, 10, &freq);
+ if (ret)
+ return ret;
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+
+ ret = ad4691_set_pwm_freq(st, freq);
+ if (ret)
+ return ret;
+
+ return len;
+}
+
+static IIO_DEVICE_ATTR_RW(sampling_frequency, 0);
+
+static const struct iio_dev_attr *ad4691_buffer_attrs[] = {
+ &iio_dev_attr_sampling_frequency,
+ NULL
+};
+
+static irqreturn_t ad4691_irq(int irq, void *private)
+{
+ struct iio_dev *indio_dev = private;
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ iio_trigger_poll(indio_dev->trig);
+ /*
+ * Keep the DATA_READY IRQ disabled until the trigger handler has
+ * finished reading the scan, to prevent a new assertion mid-transfer.
+ * The PWM continues running uninterrupted; the IRQ is re-enabled in
+ * ad4691_trigger_handler once spi_sync completes.
+ *
+ * IRQF_ONESHOT already masks the hardware line during this threaded
+ * handler, so disable_irq_nosync here ensures the IRQ stays disabled
+ * even after IRQF_ONESHOT unmasks on return.
+ */
+ disable_irq_nosync(st->irq);
+
+ return IRQ_HANDLED;
+}
+
+static const struct iio_trigger_ops ad4691_trigger_ops = {
+ .validate_device = iio_trigger_validate_own_device,
+};
+
+static int ad4691_read_scan(struct iio_dev *indio_dev, s64 timestamp)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ guard(mutex)(&st->lock);
+
+ ret = spi_sync(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ /*
+ * rx_buf pointers in scan_xfers point directly into scan.vals, so no
+ * copy is needed. The scan_msg already includes a STATE_RESET at the
+ * end (appended in preenable), so no explicit reset is needed here.
+ */
+ iio_push_to_buffers_with_ts(indio_dev, st->vals, sizeof(st->vals),
+ timestamp);
+ return 0;
+}
+
+static irqreturn_t ad4691_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ ad4691_read_scan(indio_dev, pf->timestamp);
+ if (!st->manual_mode)
+ enable_irq(st->irq);
+ iio_trigger_notify_done(indio_dev->trig);
+ return IRQ_HANDLED;
+}
+
static const struct iio_info ad4691_info = {
.read_raw = &ad4691_read_raw,
.write_raw = &ad4691_write_raw,
@@ -514,6 +910,18 @@ static const struct iio_info ad4691_info = {
.debugfs_reg_access = &ad4691_reg_access,
};
+static int ad4691_pwm_setup(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+
+ st->conv_trigger = devm_pwm_get(dev, "cnv");
+ if (IS_ERR(st->conv_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->conv_trigger),
+ "Failed to get cnv pwm\n");
+
+ return ad4691_set_pwm_freq(st, st->info->max_rate);
+}
+
static int ad4691_regulator_setup(struct ad4691_state *st)
{
struct device *dev = regmap_get_device(st->regmap);
@@ -593,6 +1001,22 @@ static int ad4691_config(struct ad4691_state *st)
unsigned int val;
int ret;
+ /*
+ * Determine buffer conversion mode from DT: if a PWM is provided it
+ * drives the CNV pin (CNV_BURST_MODE); otherwise CNV is tied to CS
+ * and each SPI transfer triggers a conversion (MANUAL_MODE).
+ * Both modes idle in AUTONOMOUS mode so that read_raw can use the
+ * internal oscillator without disturbing the hardware configuration.
+ */
+ if (device_property_present(dev, "pwms")) {
+ st->manual_mode = false;
+ ret = ad4691_pwm_setup(st);
+ if (ret)
+ return ret;
+ } else {
+ st->manual_mode = true;
+ }
+
switch (st->vref_uV) {
case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
ref_val = AD4691_VREF_2P5;
@@ -646,6 +1070,75 @@ static int ad4691_config(struct ad4691_state *st)
return 0;
}
+static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
+ struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct iio_trigger *trig;
+ unsigned int i;
+ int irq, ret;
+
+ trig = devm_iio_trigger_alloc(dev, "%s-dev%d", indio_dev->name,
+ iio_device_id(indio_dev));
+ if (!trig)
+ return -ENOMEM;
+
+ trig->ops = &ad4691_trigger_ops;
+ iio_trigger_set_drvdata(trig, st);
+
+ ret = devm_iio_trigger_register(dev, trig);
+ if (ret)
+ return dev_err_probe(dev, ret, "IIO trigger register failed\n");
+
+ indio_dev->trig = iio_trigger_get(trig);
+
+ if (st->manual_mode)
+ return devm_iio_triggered_buffer_setup(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ &ad4691_trigger_handler,
+ &ad4691_manual_buffer_setup_ops);
+
+ /*
+ * The GP pin named in interrupt-names asserts at end-of-conversion.
+ * The IRQ handler stops conversions and fires the IIO trigger so
+ * the trigger handler can read and push the sample to the buffer.
+ * The IRQ is kept disabled until the buffer is enabled.
+ */
+ irq = -ENXIO;
+ for (i = 0; i < ARRAY_SIZE(ad4691_gp_names); i++) {
+ irq = fwnode_irq_get_byname(dev_fwnode(dev),
+ ad4691_gp_names[i]);
+ if (irq > 0)
+ break;
+ }
+ if (irq < 0)
+ return dev_err_probe(dev, irq, "failed to get GP interrupt\n");
+
+ st->irq = irq;
+
+ ret = ad4691_gpio_setup(st, i);
+ if (ret)
+ return ret;
+
+ /*
+ * IRQ is kept disabled until the buffer is enabled to prevent
+ * spurious DATA_READY events before the SPI message is set up.
+ */
+ ret = devm_request_threaded_irq(dev, irq, NULL,
+ &ad4691_irq,
+ IRQF_ONESHOT | IRQF_NO_AUTOEN,
+ indio_dev->name, indio_dev);
+ if (ret)
+ return ret;
+
+ return devm_iio_triggered_buffer_setup_ext(dev, indio_dev,
+ &iio_pollfunc_store_time,
+ &ad4691_trigger_handler,
+ IIO_BUFFER_DIRECTION_IN,
+ &ad4691_cnv_burst_buffer_setup_ops,
+ ad4691_buffer_attrs);
+}
+
static int ad4691_probe(struct spi_device *spi)
{
struct device *dev = &spi->dev;
@@ -658,6 +1151,7 @@ static int ad4691_probe(struct spi_device *spi)
return -ENOMEM;
st = iio_priv(indio_dev);
+ st->spi = spi;
st->info = spi_get_device_match_data(spi);
ret = devm_mutex_init(dev, &st->lock);
@@ -685,8 +1179,9 @@ static int ad4691_probe(struct spi_device *spi)
indio_dev->info = &ad4691_info;
indio_dev->modes = INDIO_DIRECT_MODE;
- indio_dev->channels = st->info->sw_info->channels;
- indio_dev->num_channels = st->info->sw_info->num_channels;
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (ret)
+ return ret;
return devm_iio_device_register(dev, indio_dev);
}
--
2.43.0
^ permalink raw reply related
* [PATCH v9 2/6] iio: adc: ad4691: add initial driver for AD4691 family
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau
In-Reply-To: <20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add support for the Analog Devices AD4691 family of high-speed,
low-power multichannel SAR ADCs: AD4691 (16-ch, 500 kSPS),
AD4692 (16-ch, 1 MSPS), AD4693 (8-ch, 500 kSPS) and
AD4694 (8-ch, 1 MSPS).
The driver implements a custom regmap layer over raw SPI to handle the
device's mixed 1/2/3/4-byte register widths and uses the standard IIO
read_raw/write_raw interface for single-channel reads.
The chip idles in Autonomous Mode so that single-shot read_raw can use
the internal oscillator without disturbing the hardware configuration.
Three voltage supply domains are managed: avdd (required), vio, and a
reference supply on either the REF pin (ref-supply, external buffer)
or the REFIN pin (refin-supply, uses the on-chip reference buffer;
REFBUF_EN is set accordingly). Hardware reset is performed via
the reset controller framework; a software reset through SPI_CONFIG_A
is used as fallback when no hardware reset is available.
Accumulator channel masking for single-shot reads uses ACC_MASK_REG via
an ADDR_DESCENDING SPI write, which covers both mask bytes in a single
16-bit transfer.
Reviewed-by: David Lechner <dlechner@baylibre.com>
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 11 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 724 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 737 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 438ca850fa1c..24e4502b8292 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1490,6 +1490,7 @@ L: linux-iio@vger.kernel.org
S: Supported
W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
+F: drivers/iio/adc/ad4691.c
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 60038ae8dfc4..3685a03aa8dc 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -139,6 +139,17 @@ config AD4170_4
To compile this driver as a module, choose M here: the module will be
called ad4170-4.
+config AD4691
+ tristate "Analog Devices AD4691 Family ADC Driver"
+ depends on SPI
+ select REGMAP
+ help
+ Say yes here to build support for Analog Devices AD4691 Family MuxSAR
+ SPI analog to digital converters (ADC).
+
+ To compile this driver as a module, choose M here: the module will be
+ called ad4691.
+
config AD4695
tristate "Analog Device AD4695 ADC Driver"
depends on SPI
diff --git a/drivers/iio/adc/Makefile b/drivers/iio/adc/Makefile
index c76550415ff1..4ac1ea09d773 100644
--- a/drivers/iio/adc/Makefile
+++ b/drivers/iio/adc/Makefile
@@ -16,6 +16,7 @@ obj-$(CONFIG_AD4080) += ad4080.o
obj-$(CONFIG_AD4130) += ad4130.o
obj-$(CONFIG_AD4134) += ad4134.o
obj-$(CONFIG_AD4170_4) += ad4170-4.o
+obj-$(CONFIG_AD4691) += ad4691.o
obj-$(CONFIG_AD4695) += ad4695.o
obj-$(CONFIG_AD4851) += ad4851.o
obj-$(CONFIG_AD7091R) += ad7091r-base.o
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
new file mode 100644
index 000000000000..05826b762c7f
--- /dev/null
+++ b/drivers/iio/adc/ad4691.c
@@ -0,0 +1,724 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024-2026 Analog Devices, Inc.
+ * Author: Radu Sabau <radu.sabau@analog.com>
+ */
+#include <linux/array_size.h>
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/delay.h>
+#include <linux/dev_printk.h>
+#include <linux/device/devres.h>
+#include <linux/err.h>
+#include <linux/limits.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/mod_devicetable.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+#include <linux/reset.h>
+#include <linux/spi/spi.h>
+#include <linux/units.h>
+#include <linux/unaligned.h>
+
+#include <linux/iio/iio.h>
+
+#define AD4691_VREF_uV_MIN 2400000
+#define AD4691_VREF_uV_MAX 5250000
+#define AD4691_VREF_2P5_uV_MAX 2750000
+#define AD4691_VREF_3P0_uV_MAX 3250000
+#define AD4691_VREF_3P3_uV_MAX 3750000
+#define AD4691_VREF_4P096_uV_MAX 4500000
+
+#define AD4691_SPI_CONFIG_A_REG 0x000
+#define AD4691_SW_RESET (BIT(7) | BIT(0))
+
+#define AD4691_STATUS_REG 0x014
+#define AD4691_CLAMP_STATUS1_REG 0x01A
+#define AD4691_CLAMP_STATUS2_REG 0x01B
+#define AD4691_DEVICE_SETUP 0x020
+#define AD4691_LDO_EN BIT(4)
+#define AD4691_REF_CTRL 0x021
+#define AD4691_REF_CTRL_MASK GENMASK(4, 2)
+#define AD4691_REFBUF_EN BIT(0)
+#define AD4691_OSC_FREQ_REG 0x023
+#define AD4691_OSC_FREQ_MASK GENMASK(3, 0)
+#define AD4691_STD_SEQ_CONFIG 0x025
+#define AD4691_SPARE_CONTROL 0x02A
+
+#define AD4691_OSC_EN_REG 0x180
+#define AD4691_STATE_RESET_REG 0x181
+#define AD4691_STATE_RESET_ALL 0x01
+#define AD4691_ADC_SETUP 0x182
+#define AD4691_ADC_MODE_MASK GENMASK(1, 0)
+#define AD4691_AUTONOMOUS_MODE 0x02
+/*
+ * ACC_MASK_REG covers both mask bytes via ADDR_DESCENDING SPI: writing a
+ * 16-bit BE value to 0x185 auto-decrements to 0x184 for the second byte.
+ */
+#define AD4691_ACC_MASK_REG 0x185
+#define AD4691_ACC_DEPTH_IN(n) (0x186 + (n))
+#define AD4691_GPIO_MODE1_REG 0x196
+#define AD4691_GPIO_MODE2_REG 0x197
+#define AD4691_GPIO_READ 0x1A0
+#define AD4691_ACC_STATUS_FULL1_REG 0x1B0
+#define AD4691_ACC_STATUS_FULL2_REG 0x1B1
+#define AD4691_ACC_STATUS_OVERRUN1_REG 0x1B2
+#define AD4691_ACC_STATUS_OVERRUN2_REG 0x1B3
+#define AD4691_ACC_STATUS_SAT1_REG 0x1B4
+#define AD4691_ACC_STATUS_SAT2_REG 0x1BE
+#define AD4691_ACC_SAT_OVR_REG(n) (0x1C0 + (n))
+#define AD4691_AVG_IN(n) (0x201 + (2 * (n)))
+#define AD4691_AVG_STS_IN(n) (0x222 + (3 * (n)))
+#define AD4691_ACC_IN(n) (0x252 + (3 * (n)))
+#define AD4691_ACC_STS_DATA(n) (0x283 + (4 * (n)))
+
+static const char * const ad4691_supplies[] = { "avdd", "vio" };
+
+enum ad4691_ref_ctrl {
+ AD4691_VREF_2P5 = 0,
+ AD4691_VREF_3P0 = 1,
+ AD4691_VREF_3P3 = 2,
+ AD4691_VREF_4P096 = 3,
+ AD4691_VREF_5P0 = 4,
+};
+
+struct ad4691_channel_info {
+ const struct iio_chan_spec *channels __counted_by_ptr(num_channels);
+ unsigned int num_channels;
+};
+
+struct ad4691_chip_info {
+ const char *name;
+ unsigned int max_rate;
+ const struct ad4691_channel_info *sw_info;
+};
+
+#define AD4691_CHANNEL(ch) \
+ { \
+ .type = IIO_VOLTAGE, \
+ .indexed = 1, \
+ .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) \
+ | BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_separate_available = \
+ BIT(IIO_CHAN_INFO_SAMP_FREQ), \
+ .info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
+ .channel = ch, \
+ .scan_index = ch, \
+ .scan_type = { \
+ .sign = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
+static const struct iio_chan_spec ad4691_channels[] = {
+ AD4691_CHANNEL(0),
+ AD4691_CHANNEL(1),
+ AD4691_CHANNEL(2),
+ AD4691_CHANNEL(3),
+ AD4691_CHANNEL(4),
+ AD4691_CHANNEL(5),
+ AD4691_CHANNEL(6),
+ AD4691_CHANNEL(7),
+ AD4691_CHANNEL(8),
+ AD4691_CHANNEL(9),
+ AD4691_CHANNEL(10),
+ AD4691_CHANNEL(11),
+ AD4691_CHANNEL(12),
+ AD4691_CHANNEL(13),
+ AD4691_CHANNEL(14),
+ AD4691_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_channels[] = {
+ AD4691_CHANNEL(0),
+ AD4691_CHANNEL(1),
+ AD4691_CHANNEL(2),
+ AD4691_CHANNEL(3),
+ AD4691_CHANNEL(4),
+ AD4691_CHANNEL(5),
+ AD4691_CHANNEL(6),
+ AD4691_CHANNEL(7),
+};
+
+/*
+ * Internal oscillator frequency table. Index is the OSC_FREQ_REG[3:0] value.
+ * Index 0 (1 MHz) is only valid for AD4692/AD4694; AD4691/AD4693 support
+ * up to 500 kHz and use index 1 as their highest valid rate.
+ */
+static const int ad4691_osc_freqs_Hz[] = {
+ [0x0] = 1000000,
+ [0x1] = 500000,
+ [0x2] = 400000,
+ [0x3] = 250000,
+ [0x4] = 200000,
+ [0x5] = 167000,
+ [0x6] = 133000,
+ [0x7] = 125000,
+ [0x8] = 100000,
+ [0x9] = 50000,
+ [0xA] = 25000,
+ [0xB] = 12500,
+ [0xC] = 10000,
+ [0xD] = 5000,
+ [0xE] = 2500,
+ [0xF] = 1250,
+};
+
+static const struct ad4691_channel_info ad4691_sw_info = {
+ .channels = ad4691_channels,
+ .num_channels = ARRAY_SIZE(ad4691_channels),
+};
+
+static const struct ad4691_channel_info ad4693_sw_info = {
+ .channels = ad4693_channels,
+ .num_channels = ARRAY_SIZE(ad4693_channels),
+};
+
+static const struct ad4691_chip_info ad4691_chip_info = {
+ .name = "ad4691",
+ .max_rate = 500 * HZ_PER_KHZ,
+ .sw_info = &ad4691_sw_info,
+};
+
+static const struct ad4691_chip_info ad4692_chip_info = {
+ .name = "ad4692",
+ .max_rate = 1 * HZ_PER_MHZ,
+ .sw_info = &ad4691_sw_info,
+};
+
+static const struct ad4691_chip_info ad4693_chip_info = {
+ .name = "ad4693",
+ .max_rate = 500 * HZ_PER_KHZ,
+ .sw_info = &ad4693_sw_info,
+};
+
+static const struct ad4691_chip_info ad4694_chip_info = {
+ .name = "ad4694",
+ .max_rate = 1 * HZ_PER_MHZ,
+ .sw_info = &ad4693_sw_info,
+};
+
+struct ad4691_state {
+ const struct ad4691_chip_info *info;
+ struct regmap *regmap;
+ int vref_uV;
+ bool refbuf_en;
+ bool ldo_en;
+ /*
+ * Synchronize access to members of the driver state, and ensure
+ * atomicity of consecutive SPI operations.
+ */
+ struct mutex lock;
+};
+
+static int ad4691_reg_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct spi_device *spi = context;
+ u8 tx[2], rx[4];
+ int ret;
+
+ /* Set bit 15 to mark the operation as READ. */
+ put_unaligned_be16(0x8000 | reg, tx);
+
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_SAT_OVR_REG(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 1);
+ if (ret)
+ return ret;
+ *val = rx[0];
+ return 0;
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 2);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be16(rx);
+ return 0;
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 3);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be24(rx);
+ return 0;
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ ret = spi_write_then_read(spi, tx, sizeof(tx), rx, 4);
+ if (ret)
+ return ret;
+ *val = get_unaligned_be32(rx);
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_reg_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct spi_device *spi = context;
+ u8 tx[4];
+
+ put_unaligned_be16(reg, tx);
+
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_MASK_REG - 1:
+ case AD4691_ACC_MASK_REG + 1 ... AD4691_GPIO_MODE2_REG:
+ if (val > U8_MAX)
+ return -EINVAL;
+ tx[2] = val;
+ return spi_write_then_read(spi, tx, 3, NULL, 0);
+ case AD4691_ACC_MASK_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ if (val > U16_MAX)
+ return -EINVAL;
+ put_unaligned_be16(val, &tx[2]);
+ return spi_write_then_read(spi, tx, 4, NULL, 0);
+ default:
+ return -EINVAL;
+ }
+}
+
+static bool ad4691_volatile_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case AD4691_STATUS_REG:
+ case AD4691_CLAMP_STATUS1_REG:
+ case AD4691_CLAMP_STATUS2_REG:
+ case AD4691_GPIO_READ:
+ case AD4691_ACC_STATUS_FULL1_REG ... AD4691_ACC_STATUS_SAT2_REG:
+ case AD4691_ACC_SAT_OVR_REG(0) ... AD4691_ACC_SAT_OVR_REG(15):
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool ad4691_readable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_SPARE_CONTROL ... AD4691_ACC_SAT_OVR_REG(15):
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_AVG_IN(0) ... AD4691_AVG_IN(15):
+ case AD4691_AVG_STS_IN(0) ... AD4691_AVG_STS_IN(15):
+ case AD4691_ACC_IN(0) ... AD4691_ACC_IN(15):
+ case AD4691_ACC_STS_DATA(0) ... AD4691_ACC_STS_DATA(15):
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool ad4691_writeable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case 0 ... AD4691_OSC_FREQ_REG:
+ case AD4691_STD_SEQ_CONFIG:
+ case AD4691_SPARE_CONTROL ... AD4691_GPIO_MODE2_REG:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static const struct regmap_config ad4691_regmap_config = {
+ .reg_bits = 16,
+ .val_bits = 32,
+ .reg_read = ad4691_reg_read,
+ .reg_write = ad4691_reg_write,
+ .volatile_reg = ad4691_volatile_reg,
+ .readable_reg = ad4691_readable_reg,
+ .writeable_reg = ad4691_writeable_reg,
+ .max_register = AD4691_ACC_STS_DATA(15),
+ .cache_type = REGCACHE_MAPLE,
+};
+
+/*
+ * Index 0 in ad4691_osc_freqs_Hz is 1 MHz — valid only for AD4692/AD4694
+ * (max_rate == 1 MHz). AD4691/AD4693 cap at 500 kHz so their valid range
+ * starts at index 1.
+ */
+static unsigned int ad4691_samp_freq_start(const struct ad4691_chip_info *info)
+{
+ return (info->max_rate == 1 * HZ_PER_MHZ) ? 0 : 1;
+}
+
+static int ad4691_get_sampling_freq(struct ad4691_state *st, int *val)
+{
+ unsigned int reg_val;
+ int ret;
+
+ ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ if (ret)
+ return ret;
+
+ *val = ad4691_osc_freqs_Hz[FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val)];
+ return IIO_VAL_INT;
+}
+
+static int ad4691_set_sampling_freq(struct iio_dev *indio_dev, int freq)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int start = ad4691_samp_freq_start(st->info);
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
+ if (ad4691_osc_freqs_Hz[i] != freq)
+ continue;
+ return regmap_update_bits(st->regmap, AD4691_OSC_FREQ_REG,
+ AD4691_OSC_FREQ_MASK, i);
+ }
+
+ return -EINVAL;
+}
+
+static int ad4691_read_avail(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ const int **vals, int *type,
+ int *length, long mask)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int start = ad4691_samp_freq_start(st->info);
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ *vals = &ad4691_osc_freqs_Hz[start];
+ *type = IIO_VAL_INT;
+ *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
+ return IIO_AVAIL_LIST;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_single_shot_read(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ unsigned int reg_val, osc_idx, period_us;
+ int ret;
+
+ guard(mutex)(&st->lock);
+
+ /* Use AUTONOMOUS mode for single-shot reads. */
+ ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG,
+ AD4691_STATE_RESET_ALL);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG,
+ BIT(chan->channel));
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG,
+ ~BIT(chan->channel) & GENMASK(15, 0));
+ if (ret)
+ return ret;
+
+ ret = regmap_read(st->regmap, AD4691_OSC_FREQ_REG, ®_val);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 1);
+ if (ret)
+ return ret;
+
+ osc_idx = FIELD_GET(AD4691_OSC_FREQ_MASK, reg_val);
+ /* Wait 2 oscillator periods for the conversion to complete. */
+ period_us = DIV_ROUND_UP(2UL * USEC_PER_SEC, ad4691_osc_freqs_Hz[osc_idx]);
+ fsleep(period_us);
+
+ ret = regmap_write(st->regmap, AD4691_OSC_EN_REG, 0);
+ if (ret)
+ return ret;
+
+ ret = regmap_read(st->regmap, AD4691_AVG_IN(chan->channel), ®_val);
+ if (ret)
+ return ret;
+
+ *val = reg_val;
+
+ ret = regmap_write(st->regmap, AD4691_STATE_RESET_REG, AD4691_STATE_RESET_ALL);
+ if (ret)
+ return ret;
+
+ return IIO_VAL_INT;
+}
+
+static int ad4691_read_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan, int *val,
+ int *val2, long info)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ switch (info) {
+ case IIO_CHAN_INFO_RAW: {
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ return ad4691_single_shot_read(indio_dev, chan, val);
+ }
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_get_sampling_freq(st, val);
+ case IIO_CHAN_INFO_SCALE:
+ *val = st->vref_uV / (MICRO / MILLI);
+ *val2 = chan->scan_type.realbits;
+ return IIO_VAL_FRACTIONAL_LOG2;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_write_raw(struct iio_dev *indio_dev,
+ struct iio_chan_spec const *chan,
+ int val, int val2, long mask)
+{
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_set_sampling_freq(indio_dev, val);
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
+ unsigned int writeval, unsigned int *readval)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ guard(mutex)(&st->lock);
+
+ if (readval)
+ return regmap_read(st->regmap, reg, readval);
+
+ return regmap_write(st->regmap, reg, writeval);
+}
+
+static const struct iio_info ad4691_info = {
+ .read_raw = &ad4691_read_raw,
+ .write_raw = &ad4691_write_raw,
+ .read_avail = &ad4691_read_avail,
+ .debugfs_reg_access = &ad4691_reg_access,
+};
+
+static int ad4691_regulator_setup(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ int ret;
+
+ ret = devm_regulator_bulk_get_enable(dev, ARRAY_SIZE(ad4691_supplies),
+ ad4691_supplies);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to get and enable supplies\n");
+
+ /*
+ * vdd-supply and ldo-in-supply are mutually exclusive:
+ * vdd-supply present → external 1.8V VDD; disable internal LDO.
+ * vdd-supply absent → enable internal LDO fed from ldo-in-supply.
+ * Having both simultaneously is strongly inadvisable per the datasheet.
+ */
+ ret = devm_regulator_get_enable_optional(dev, "vdd");
+ if (ret == -ENODEV) {
+ ret = devm_regulator_get_enable(dev, "ldo-in");
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to get and enable LDO-IN\n");
+ st->ldo_en = true;
+ } else if (ret) {
+ return dev_err_probe(dev, ret, "Failed to get and enable VDD\n");
+ }
+
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "ref");
+ if (st->vref_uV == -ENODEV) {
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "refin");
+ st->refbuf_en = true;
+ }
+ if (st->vref_uV < 0)
+ return dev_err_probe(dev, st->vref_uV,
+ "Failed to get reference supply\n");
+
+ if (st->vref_uV < AD4691_VREF_uV_MIN || st->vref_uV > AD4691_VREF_uV_MAX)
+ return dev_err_probe(dev, -EINVAL,
+ "vref(%d) must be in the range [%u...%u]\n",
+ st->vref_uV, AD4691_VREF_uV_MIN,
+ AD4691_VREF_uV_MAX);
+
+ return 0;
+}
+
+static int ad4691_reset(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ struct reset_control *rst;
+
+ rst = devm_reset_control_get_optional_exclusive(dev, NULL);
+ if (IS_ERR(rst))
+ return dev_err_probe(dev, PTR_ERR(rst), "Failed to get reset\n");
+
+ if (rst) {
+ /*
+ * reset_gpio_probe() already drives the pin asserted, so the
+ * device is held in reset before we get here.
+ * devm_reset_control_get_optional_exclusive_deasserted() cannot
+ * be used because it deasserts immediately without delay; the
+ * datasheet (Table 5) requires a ≥300 µs reset pulse width
+ * before deassertion.
+ */
+ fsleep(300);
+ return reset_control_deassert(rst);
+ }
+
+ /* No hardware reset available, fall back to software reset. */
+ return regmap_write(st->regmap, AD4691_SPI_CONFIG_A_REG,
+ AD4691_SW_RESET);
+}
+
+static int ad4691_config(struct ad4691_state *st)
+{
+ struct device *dev = regmap_get_device(st->regmap);
+ enum ad4691_ref_ctrl ref_val;
+ unsigned int val;
+ int ret;
+
+ switch (st->vref_uV) {
+ case AD4691_VREF_uV_MIN ... AD4691_VREF_2P5_uV_MAX:
+ ref_val = AD4691_VREF_2P5;
+ break;
+ case AD4691_VREF_2P5_uV_MAX + 1 ... AD4691_VREF_3P0_uV_MAX:
+ ref_val = AD4691_VREF_3P0;
+ break;
+ case AD4691_VREF_3P0_uV_MAX + 1 ... AD4691_VREF_3P3_uV_MAX:
+ ref_val = AD4691_VREF_3P3;
+ break;
+ case AD4691_VREF_3P3_uV_MAX + 1 ... AD4691_VREF_4P096_uV_MAX:
+ ref_val = AD4691_VREF_4P096;
+ break;
+ case AD4691_VREF_4P096_uV_MAX + 1 ... AD4691_VREF_uV_MAX:
+ ref_val = AD4691_VREF_5P0;
+ break;
+ default:
+ return dev_err_probe(dev, -EINVAL,
+ "Unsupported vref voltage: %d uV\n",
+ st->vref_uV);
+ }
+
+ val = FIELD_PREP(AD4691_REF_CTRL_MASK, ref_val);
+ if (st->refbuf_en)
+ val |= AD4691_REFBUF_EN;
+
+ ret = regmap_write(st->regmap, AD4691_REF_CTRL, val);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write REF_CTRL\n");
+
+ ret = regmap_assign_bits(st->regmap, AD4691_DEVICE_SETUP,
+ AD4691_LDO_EN, st->ldo_en);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write DEVICE_SETUP\n");
+
+ /*
+ * Set the internal oscillator to the highest rate this chip supports.
+ * Index 0 (1 MHz) exceeds the 500 kHz max of AD4691/AD4693, so those
+ * chips start at index 1 (500 kHz).
+ */
+ ret = regmap_write(st->regmap, AD4691_OSC_FREQ_REG,
+ ad4691_samp_freq_start(st->info));
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write OSC_FREQ\n");
+
+ ret = regmap_update_bits(st->regmap, AD4691_ADC_SETUP,
+ AD4691_ADC_MODE_MASK, AD4691_AUTONOMOUS_MODE);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to write ADC_SETUP\n");
+
+ return 0;
+}
+
+static int ad4691_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct iio_dev *indio_dev;
+ struct ad4691_state *st;
+ int ret;
+
+ indio_dev = devm_iio_device_alloc(dev, sizeof(*st));
+ if (!indio_dev)
+ return -ENOMEM;
+
+ st = iio_priv(indio_dev);
+ st->info = spi_get_device_match_data(spi);
+
+ ret = devm_mutex_init(dev, &st->lock);
+ if (ret)
+ return ret;
+
+ st->regmap = devm_regmap_init(dev, NULL, spi, &ad4691_regmap_config);
+ if (IS_ERR(st->regmap))
+ return dev_err_probe(dev, PTR_ERR(st->regmap),
+ "Failed to initialize regmap\n");
+
+ ret = ad4691_regulator_setup(st);
+ if (ret)
+ return ret;
+
+ ret = ad4691_reset(st);
+ if (ret)
+ return ret;
+
+ ret = ad4691_config(st);
+ if (ret)
+ return ret;
+
+ indio_dev->name = st->info->name;
+ indio_dev->info = &ad4691_info;
+ indio_dev->modes = INDIO_DIRECT_MODE;
+
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+
+ return devm_iio_device_register(dev, indio_dev);
+}
+
+static const struct of_device_id ad4691_of_match[] = {
+ { .compatible = "adi,ad4691", .data = &ad4691_chip_info },
+ { .compatible = "adi,ad4692", .data = &ad4692_chip_info },
+ { .compatible = "adi,ad4693", .data = &ad4693_chip_info },
+ { .compatible = "adi,ad4694", .data = &ad4694_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ad4691_of_match);
+
+static const struct spi_device_id ad4691_id[] = {
+ { "ad4691", (kernel_ulong_t)&ad4691_chip_info },
+ { "ad4692", (kernel_ulong_t)&ad4692_chip_info },
+ { "ad4693", (kernel_ulong_t)&ad4693_chip_info },
+ { "ad4694", (kernel_ulong_t)&ad4694_chip_info },
+ { }
+};
+MODULE_DEVICE_TABLE(spi, ad4691_id);
+
+static struct spi_driver ad4691_driver = {
+ .driver = {
+ .name = "ad4691",
+ .of_match_table = ad4691_of_match,
+ },
+ .probe = ad4691_probe,
+ .id_table = ad4691_id,
+};
+module_spi_driver(ad4691_driver);
+
+MODULE_AUTHOR("Radu Sabau <radu.sabau@analog.com>");
+MODULE_DESCRIPTION("Analog Devices AD4691 Family ADC Driver");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
* [PATCH v9 1/6] dt-bindings: iio: adc: add AD4691 family
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau, Conor Dooley
In-Reply-To: <20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@analog.com>
From: Radu Sabau <radu.sabau@analog.com>
Add DT bindings for the Analog Devices AD4691 family of multichannel
SAR ADCs (AD4691, AD4692, AD4693, AD4694).
The binding describes the hardware connections:
- Power domains: avdd-supply (required), vio-supply, ref-supply or
refin-supply (external reference; the REFIN path enables the
internal reference buffer). Digital core VDD is supplied either
externally via vdd-supply, or generated by the on-chip LDO fed
from ldo-in-supply; the two are mutually exclusive and one must
be present.
- Optional PWM on the CNV pin selects CNV Burst Mode; when absent,
Manual Mode is assumed with CNV tied to SPI CS.
- An optional reset GPIO (reset-gpios) for hardware reset.
- Up to four GP pins (gp0..gp3) usable as interrupt sources,
identified in firmware via interrupt-names "gp0".."gp3".
- gpio-controller with #gpio-cells = <2> for GP pin GPIO usage.
- #trigger-source-cells = <1>: one cell selecting the GP pin number
(0-3) used as the SPI offload trigger source.
Two binding examples are provided: CNV Burst Mode with SPI offload
(DMA data acquisition driven by DATA_READY on a GP pin), and Manual
Mode for CPU-driven triggered-buffer or single-shot capture.
The four variants are not compatible with each other: AD4691/AD4692 have
16 analog input channels while AD4693/AD4694 have 8, and AD4691/AD4693
top out at 500 kSPS while AD4692/AD4694 reach 1 MSPS. These differences
in channel count and maximum sample rate require distinct compatible
strings so the driver can select the correct channel configuration and
rate limits.
Acked-by: Conor Dooley <conor.dooley@microchip.com>
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
.../devicetree/bindings/iio/adc/adi,ad4691.yaml | 180 +++++++++++++++++++++
MAINTAINERS | 7 +
2 files changed, 187 insertions(+)
diff --git a/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml b/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
new file mode 100644
index 000000000000..af28a0c1cfa9
--- /dev/null
+++ b/Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
@@ -0,0 +1,180 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/iio/adc/adi,ad4691.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Analog Devices AD4691 Family Multichannel SAR ADCs
+
+maintainers:
+ - Radu Sabau <radu.sabau@analog.com>
+
+description: |
+ The AD4691 family are high-speed, low-power, multichannel successive
+ approximation register (SAR) analog-to-digital converters (ADCs) with
+ an SPI-compatible serial interface. The ADC supports CNV Burst Mode,
+ where an external PWM drives the CNV pin, and Manual Mode, where CNV
+ is directly tied to the SPI chip-select.
+
+ Datasheets:
+ * https://www.analog.com/en/products/ad4691.html
+ * https://www.analog.com/en/products/ad4692.html
+ * https://www.analog.com/en/products/ad4693.html
+ * https://www.analog.com/en/products/ad4694.html
+
+$ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+ compatible:
+ enum:
+ - adi,ad4691
+ - adi,ad4692
+ - adi,ad4693
+ - adi,ad4694
+
+ reg:
+ maxItems: 1
+
+ spi-max-frequency:
+ maximum: 40000000
+
+ spi-cpol: true
+ spi-cpha: true
+
+ avdd-supply:
+ description: Analog power supply (4.5V to 5.5V).
+
+ vdd-supply:
+ description:
+ External 1.8V digital core supply. When present, the internal LDO is
+ disabled (LDO_EN = 0). Mutually exclusive with ldo-in-supply.
+
+ ldo-in-supply:
+ description:
+ LDO input supply (2.4V to 5.5V). When present and vdd-supply is absent,
+ the internal LDO generates 1.8V VDD from this input (LDO_EN = 1).
+ Mutually exclusive with vdd-supply.
+
+ vio-supply:
+ description: I/O voltage supply (1.71V to 1.89V or VDD).
+
+ ref-supply:
+ description: External reference voltage supply (2.4V to 5.25V).
+
+ refin-supply:
+ description: Internal reference buffer input supply.
+
+ reset-gpios:
+ description:
+ GPIO line controlling the hardware reset pin (active-low).
+ maxItems: 1
+
+ pwms:
+ description:
+ PWM connected to the CNV pin. When present, selects CNV Burst Mode where
+ the PWM drives the conversion rate. When absent, Manual Mode is used
+ (CNV tied to SPI CS).
+ maxItems: 1
+
+ interrupts:
+ description:
+ Interrupt lines connected to the ADC GP pins. Each GP pin can be
+ physically wired to an interrupt-capable input on the SoC.
+ maxItems: 4
+
+ interrupt-names:
+ description: Names of the interrupt lines, matching the GP pin names.
+ minItems: 1
+ maxItems: 4
+ items:
+ enum:
+ - gp0
+ - gp1
+ - gp2
+ - gp3
+
+ gpio-controller: true
+
+ '#gpio-cells':
+ const: 2
+
+ '#trigger-source-cells':
+ description:
+ This node can act as a trigger source. The single cell in a consumer
+ reference specifies the GP pin number (0-3) used as the trigger output.
+ const: 1
+
+required:
+ - compatible
+ - reg
+ - avdd-supply
+ - vio-supply
+
+allOf:
+ # vdd-supply and ldo-in-supply are mutually exclusive, one is required:
+ # either an external 1.8V VDD is provided or the internal LDO is fed from
+ # ldo-in-supply to generate VDD.
+ - oneOf:
+ - required:
+ - vdd-supply
+ - required:
+ - ldo-in-supply
+ # ref-supply and refin-supply are mutually exclusive, one is required
+ - oneOf:
+ - required:
+ - ref-supply
+ - required:
+ - refin-supply
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ /* AD4692 in CNV Burst Mode with SPI offload */
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ reg = <0>;
+ spi-cpol;
+ spi-cpha;
+ spi-max-frequency = <40000000>;
+
+ avdd-supply = <&avdd_supply>;
+ ldo-in-supply = <&avdd_supply>;
+ vio-supply = <&vio_supply>;
+ ref-supply = <&ref_5v>;
+
+ reset-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
+
+ pwms = <&pwm_gen 0 0>;
+
+ #trigger-source-cells = <1>;
+ };
+ };
+
+ - |
+ #include <dt-bindings/gpio/gpio.h>
+ /* AD4692 in Manual Mode (CNV tied to SPI CS) */
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ adc@0 {
+ compatible = "adi,ad4692";
+ reg = <0>;
+ spi-cpol;
+ spi-cpha;
+ spi-max-frequency = <31250000>;
+
+ avdd-supply = <&avdd_supply>;
+ ldo-in-supply = <&avdd_supply>;
+ vio-supply = <&vio_supply>;
+ refin-supply = <&refin_supply>;
+
+ reset-gpios = <&gpio0 15 GPIO_ACTIVE_LOW>;
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c..438ca850fa1c 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1484,6 +1484,13 @@ W: https://ez.analog.com/linux-software-drivers
F: Documentation/devicetree/bindings/iio/adc/adi,ad4170-4.yaml
F: drivers/iio/adc/ad4170-4.c
+ANALOG DEVICES INC AD4691 DRIVER
+M: Radu Sabau <radu.sabau@analog.com>
+L: linux-iio@vger.kernel.org
+S: Supported
+W: https://ez.analog.com/linux-software-drivers
+F: Documentation/devicetree/bindings/iio/adc/adi,ad4691.yaml
+
ANALOG DEVICES INC AD4695 DRIVER
M: Michael Hennerich <michael.hennerich@analog.com>
M: Nuno Sá <nuno.sa@analog.com>
--
2.43.0
^ permalink raw reply related
* [PATCH v9 0/6] iio: adc: ad4691: add driver for AD4691 multichannel SAR ADC family
From: Radu Sabau via B4 Relay @ 2026-04-30 10:16 UTC (permalink / raw)
To: Lars-Peter Clausen, Michael Hennerich, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Rob Herring,
Krzysztof Kozlowski, Conor Dooley, Uwe Kleine-König,
Liam Girdwood, Mark Brown, Linus Walleij, Bartosz Golaszewski,
Philipp Zabel, Jonathan Corbet, Shuah Khan
Cc: linux-iio, devicetree, linux-kernel, linux-pwm, linux-gpio,
linux-doc, Radu Sabau, Conor Dooley
This series adds support for the Analog Devices AD4691 family of
high-speed, low-power multichannel successive approximation register
(SAR) ADCs with an SPI-compatible serial interface.
The family includes:
- AD4691: 16-channel, 500 kSPS
- AD4692: 16-channel, 1 MSPS
- AD4693: 8-channel, 500 kSPS
- AD4694: 8-channel, 1 MSPS
The devices support two operating modes, auto-detected from the device
tree:
- CNV Burst Mode: external PWM drives CNV independently of SPI;
DATA_READY on a GP pin signals end of conversion
- Manual Mode: CNV tied to SPI CS; each SPI transfer reads
the previous conversion result and starts the
next (pipelined N+1 scheme)
A new driver is warranted rather than extending ad4695: the AD4691
data path uses an accumulator-register model — results are read from
AVG_IN registers, with ACC_MASK, ADC_SETUP, DEVICE_SETUP, and
GPIO_MODE registers controlling the sequencer — none of which exist
in AD4695. CNV Burst Mode (PWM drives CNV independently of SPI) and
Manual Mode (pipelined N+1 transfers) also have no equivalent in
AD4695's command-embedded single-cycle protocol.
The series is structured as follows:
1/6 - DT bindings (YAML schema) and MAINTAINERS entry
2/6 - Initial driver: register map via custom regmap callbacks,
IIO read_raw/write_raw, both operating modes, single-channel
reads via internal oscillator (Autonomous Mode)
3/6 - Triggered buffer support: IRQ-driven (DATA_READY on a GP pin
selected via interrupt-names) for CNV Burst Mode; external IIO
trigger for Manual Mode to handle the pipelined N+1 SPI protocol
4/6 - SPI Engine offload support: DMA-backed high-throughput
capture path using the SPI offload subsystem
5/6 - Per-channel oversampling ratio support for CNV Burst Mode
6/6 - Driver documentation (Documentation/iio/ad4691.rst)
Datasheets:
https://www.analog.com/en/products/ad4691.html
https://www.analog.com/en/products/ad4692.html
https://www.analog.com/en/products/ad4693.html
https://www.analog.com/en/products/ad4694.html
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
Changes in v9:
- devm_regulator_get_enable() → devm_regulator_get_enable_optional() for
vdd-supply. The non-optional variant silently returns a dummy regulator
(ret=0) when the supply is absent from DT, so st->ldo_en was never set
and the internal LDO was never enabled when only ldo-in-supply was provided.
- struct ad4691_channel_info (factoring channels + num_channels out of
struct ad4691_chip_info into a sw_info pointer) is now introduced in
commit 1 instead of commit 2. It is a pure struct cleanup with no
relation to triggered buffers.
- channels and manual_channels fields in struct ad4691_channel_info
are now annotated with __counted_by_ptr(num_channels).
- Link to v8: https://lore.kernel.org/r/20260416-ad4692-multichannel-sar-adc-driver-v8-0-c415bd048fa3@analog.com
Changes in v8:
- dt-bindings: add commit message note explaining why four separate
compatible strings are required (channel count and max rate both
differ between variants);
- initial driver: sizeof(tx) instead of literal 2 in ad4691_reg_read;
U8_MAX/U16_MAX instead of 0xFF/0xFFFF in ad4691_reg_write
- initial driver: extract ad4691_samp_freq_start() helper
- initial driver: fix regulator model — vdd-supply (external 1.8V,
internal LDO disabled) and ldo-in-supply (feeds internal LDO) are
mutually exclusive; add vdd-supply to binding and driver
- initial driver: add comment in ad4691_reset explaining why
devm_reset_control_get_optional_exclusive_deasserted() cannot be
used (datasheet requires ≥300 µs reset pulse)
- initial driver: REF_CTRL and OSC_FREQ_REG: regmap_update_bits /
regmap_assign_bits → regmap_write (reserved bits are 0 at reset)
- initial driver: use dev instead of &spi->dev in devm_iio_device_alloc
- triggered buffer: scan_tx: add __aligned(IIO_DMA_MINALIGN);
scan struct: IIO_DECLARE_DMA_BUFFER_WITH_TS(__be16, vals, 16)
- triggered buffer: full memset of scan_xfers and scan_tx in both
preenable functions; move buffer-dma.h / buffer-dmaengine.h to
commit 4; spi_optimize_message fail path: return ret directly in
cnv_burst_buffer_preenable; reduce devm_iio_trigger_alloc wrapping
- SPI offload: drop AD4691_OFFLOAD_BITS_PER_WORD; use local
bpw = channels[0].scan_type.realbits; num_channels: ARRAY_SIZE - 1
- SPI offload: rename offload_state.spi → .offload; remove spurious
STD_SEQ_CONFIG write from cnv_burst_offload predisable; extract
local acc_mask variable for ACC_MASK_REG write
- SPI offload: sampling_frequency_store: IIO_DEV_ACQUIRE_DIRECT_MODE
for auto-release; remove explicit iio_device_release_direct calls
- oversampling: in_voltageN_sampling_frequency now represents the
effective output rate (osc_freq / osr[N]), matching ad4695
- oversampling: in_voltageN_sampling_frequency_available computed
dynamically from the channel's current OSR; only oscillator entries
divisible by osr[N] shown as effective rates; list becomes sparser
as OSR increases, capping at max_rate / osr[N]
- oversampling: writing sampling_frequency snaps down to the largest
oscillator entry ≤ freq * osr[N] that is divisible by osr[N],
guaranteeing integer read-back; writing oversampling_ratio stores
the new depth only — target_osc_freq_Hz unchanged; the two
attributes are orthogonal
- oversampling: ad4691_write_osc_freq() called from
ad4691_enter_conversion_mode() after manual mode early return,
covering all CNV burst buffer enable paths
- oversampling: (osr + 1) oscillator period wait in single_shot_read
(osr for accumulation, +1 pipeline margin)
- docs: new commit — Documentation/iio/ad4691.rst, userspace-facing
only; oversampling section describes effective-rate SF semantics;
LDO supply section corrected (vdd-supply vs ldo-in-supply)
- Link to v7: https://lore.kernel.org/r/20260409-ad4692-multichannel-sar-adc-driver-v7-0-be375d4df2c5@analog.com
Changes in v7:
- Fix CNV burst triggered-buffer preenable: the state-reset value
transfer had tx_buf assigned the return value of cpu_to_be16()
(an integer) instead of a pointer to a buffer, which would cause
a kernel oops on buffer enable; extend scan_tx[] from 17 to 18
entries to hold the extra slot and fix the pointer assignment
- Extend memset in ad4691_cnv_burst_buffer_preenable to cover the
two state-reset transfer slots (previously left with stale data
across buffer enable/disable cycles if the active channel count
changed)
- Fix format specifier %u -> %lu for NSEC_PER_SEC in
sampling_frequency_show (NSEC_PER_SEC is unsigned long on 32-bit)
- Fix missing iio_device_release_direct() on spi_offload_trigger_-
validate() error path in sampling_frequency_store
- Correct SPI offload commit message: the implementation uses 16-bit
SPI frames (bits_per_word=16, len=2), not 32-bit; storagebits
remains 16 (not promoted to 32); there is no shift=16 for manual
mode; ad4691_manual_channels[] hides IIO_CHAN_INFO_OVERSAMPLING_-
RATIO (not applicable in manual mode), not encodes shift=16
- Link to v6: https://lore.kernel.org/r/20260403-ad4692-multichannel-sar-adc-driver-v6-0-fa2a01a57c4e@analog.com
Changes in v6:
- Replace device.h with dev_printk.h + device/devres.h; add array_size.h
- Rename osc_freqs[] → osc_freqs_Hz[] with explicit [0xN] index designators
- Move loop variable into for() declaration in set_sampling_freq
- Convert multi-line block comment to single-line in single_shot_read
- Replace (u16)~ cast with ~BIT() & GENMASK(15, 0) for ACC_MASK_REG write;
GENMASK(15, 0) is still needed, otherwise maximum value condition line
in reg_write() would fail.
- Extract osc_idx/period_us temporaries in single_shot_read; add comment
- Use devm_regulator_bulk_get_enable() for avdd + vio supplies
- Reformat reset_gpio_probe() comment; remove (GPIOD_OUT_HIGH) detail
- Extract REF_CTRL value into temporary before regmap_update_bits
- Use regmap_assign_bits for OSC_FREQ_REG in config
- Remove ad4691_free_scan_bufs NULL assignments; they are not checked.
- Replace indio_dev->masklength with iio_get_masklength() throughout
- Fix spi_optimize_message error path to use goto err in preenable
- Add iio_buffer_enabled() guard in sampling_frequency_store and
set_oversampling_ratio
- Move ad4691_gpio_setup call from ad4691_config into
setup_triggered_buffer after IRQ lookup; remove duplicate
fwnode_irq_get_byname loop
- Replace oversampling ratio search loop with is_power_of_2 + ilog2
- Link to v5: https://lore.kernel.org/r/20260327-ad4692-multichannel-sar-adc-driver-v5-0-11f789de47b8@analog.com
Changes in v5:
- Reorder datasheets numerically
- Fix interrupt-names: use enum with minItems/maxItems
- Remove if/then block requiring interrupts — driver detail, not hardware constraint
- Remove redundant .shift = 0 from channel macro
- Write max_rate comparison as 1 * HZ_PER_MHZ
- Invert set_sampling_freq loop to use continue
- Fix fsleep() line break; remove blank line in read_raw
- Reorder supply init: vio immediately after avdd
- Move comment rewrites and OSC_FREQ_REG condition into the base driver patch
- Add bit-15 READ comment in reg_read
- Rewrite ldo-in handling with cleaner if/else-if pattern
- Drop redundant refbuf_en = false; invert if (!rst) in reset
- Drop reset_control_assert() — GPIO already asserted at probe
- Use regmap_update_bits/assign_bits in config
- Remove tab-column alignment of state struct members
- Declare osc_freqs[] as const int, eliminating explicit casts
- Drop obvious AUTONOMOUS mode comment
- Rename ACC_COUNT_LIMIT → ACC_DEPTH_IN to match datasheet
- Use bitmap_weight()/bitmap_read() for active_scan_mask access;
add #include <linux/bitmap.h>
- Fix channel macro line-continuation tab alignment
- Use IIO_CHAN_SOFT_TIMESTAMP(8) for 8-channel variants
- Use aligned_s64 ts in scan struct
- Add comment explaining start-index removal in set_sampling_freq
- Remove trailing comma after NULL in buffer_attrs[]
- Add IRQF_NO_AUTOEN rationale comment
- Remove unreachable manual_mode guards in sampling_frequency_show/store
- Remove st->trig; use indio_dev->trig directly
- Move max_speed_hz param to the offload patch where it is used
- Use DIV_ROUND_UP for CNV period; use compound pwm_state initializer
- Move offload fields into a separately allocated sub-struct
- Build TX words via u8* byte-fill; fixes sparse __be32 warnings
- Add three scan types (NORMAL/OFFLOAD_CNV/OFFLOAD_MANUAL) with
get_current_scan_type; triggered buffer path uses storagebits=16
- Fix IIO_CHAN_INFO_SCALE: use iio_get_current_scan_type() for realbits
- Add MODULE_IMPORT_NS("IIO_DMAENGINE_BUFFER")
- Add Documentation/iio/ad4691.rst
- Link to v4: https://lore.kernel.org/r/20260320-ad4692-multichannel-sar-adc-driver-v4-0-052c1050507a@analog.com
Changes in v4:
- dt-bindings: add avdd-supply (required) and ldo-in-supply (optional);
rename vref-supply → ref-supply, vrefin-supply → refin-supply;
corrected reset-gpios polarity (active-high → active-low); remove
clocks and pwm-names; extend interrupts to up to 4 GP pins with
interrupt-names "gp0".."gp3"; reduce #trigger-source-cells to
const: 1 (GP pin number); add gpio-controller / #gpio-cells = <2>;
drop adi,ad4691.h header; update binding examples
- driver: rename CNV Clock Mode → CNV Burst Mode throughout
- driver: add avdd-supply (required) and ldo-in-supply; track ref vs.
refin supply for REFBUF_EN; set LDO_EN in DEVICE_SETUP when ldo-in
is present; add software reset fallback via SPI_CONFIG_A register
- driver: merge ACC_MASK1_REG / ACC_MASK2_REG into ACC_MASK_REG with
a single ADDR_DESCENDING 16-bit SPI write
- driver: remove clocks usage; set PWM rate directly without ref clock
- driver: rename chip info structs (ad4691_chip_info etc.); rename
*chip → *info in state struct; replace adc_mode enum with manual_mode
bool; replace ktime sampling_period with u32 cnv_period_ns
- driver: move IIO_CHAN_INFO_SAMP_FREQ to info_mask_separate with an
available list for the internal oscillator frequency
- driver: use regcache MAPLE instead of RBTREE
- triggered buffer: derive DATA_READY GP pin from interrupt-names in
firmware ("gp0".."gp3") instead of assuming GP0
- triggered buffer: use regmap_update_bits for DEVICE_SETUP mode toggle
to avoid clobbering LDO_EN when toggling MANUAL_MODE bit
- triggered buffer: split buffer setup ops into separate Manual and
CNV Burst variants (mirrors offload path structure)
- SPI offload: promote channel storagebits from 16 to 32 to match DMA
word size; introduce ad4691_manual_channels[] with shift=16 (data in
upper 16 bits of the 32-bit word); update triggered-buffer paths to
the same layout for consistency
- SPI offload: derive GP pin from trigger-source args[0] instead of
hardcoding GP0; split offload buffer setup ops per mode
- replace put_unaligned_be32() + FIELD_PREP() with cpu_to_be32() and
plain bit-shift ops for SPI offload message construction
- multiple reviewer-requested code style and correctness fixes
(Andy Shevchenko, Nuno Sá, Uwe Kleine-König, David Lechner)
- Link to v3: https://lore.kernel.org/r/20260313-ad4692-multichannel-sar-adc-driver-v3-0-b4d14d81a181@analog.com
Changes in v3:
- Replace GPIO reset handling with reset controller framework
- Replace two regmap_write() calls for ACC_MASK1/ACC_MASK2 with regmap_bulk_write()
- Move conv_us declaration closer to its first use
- Derive spi_device/dev from regmap instead of storing st->spi
- ad4691_trigger_handler(): use guard(mutex)() and iio_for_each_active_channel()
- ad4691_setup_triggered_buffer(): return -ENOMEM/-ENOENT directly instead of
wrapping in dev_err_probe(); fix fwnode_irq_get() check (irq <= 0 → irq < 0)
- Add GENMASK defines for SPI offload 32-bit message layout; replace manual
bit-shifts with put_unaligned_be32() + FIELD_PREP()
- Use DIV_ROUND_CLOSEST_ULL() instead of div64_u64()
- ad4691_set_sampling_freq(): fix indentation; drop unnecessary else after return
- ad4691_probe(): use PTR_ERR_OR_ZERO() for devm_spi_offload_get()
- Link to v2: https://lore.kernel.org/r/20260310-ad4692-multichannel-sar-adc-driver-v2-0-d9bb8aeb5e17@analog.com
Changes in v2:
- Drop adi,spi-mode DT property; operating mode now auto-detected
from pwms presence (CNV Clock Mode if present, Manual Mode if not)
- Reduce from 5 operating modes to 2 (CNV Clock Mode, Manual Mode);
Autonomous, SPI Burst and CNV Burst modes removed as user-selectable
modes; Autonomous Mode is now the internal idle/single-shot state
- Single-shot read_raw always uses internal oscillator (Autonomous
Mode), independent of the configured buffer mode
- Replace bulk regulator API with devm_regulator_get_enable() and
devm_regulator_get_enable_read_voltage()
- Use guard(mutex) and IIO_DEV_ACQUIRE_DIRECT_MODE scoped helpers
- Replace enum + indexed chip_info array with named chip_info structs
- Remove product_id field and hardware ID check from probe
- Factor IIO_CHAN_INFO_RAW body into ad4691_single_shot_read() helper
- Use fwnode_irq_get(dev_fwnode(dev), 0); drop interrupt-names from
DT binding
- Use devm_clk_get_enabled(dev, NULL); drop clock-names from DT
binding
- Use spi_write_then_read() for DMA-safe register writes
- Use put_unaligned_be16() for SPI header construction
- fsleep() instead of usleep_range() in single-shot path
- storagebits 24->32 for manual-mode channels (uniform DMA layout)
- Collect full scan into vals[16], single iio_push_to_buffers_with_ts()
- Use pf->timestamp instead of iio_get_time_ns() in trigger handler
- Remove IRQF_TRIGGER_FALLING (comes from firmware/DT)
- Fix offload xfer array size ([17]: N channels + 1 state reset)
- Drop third DT binding example per reviewer request
- Link to v1: https://lore.kernel.org/r/20260305-ad4692-multichannel-sar-adc-driver-v1-0-336229a8dcc7@analog.com
---
Radu Sabau (6):
dt-bindings: iio: adc: add AD4691 family
iio: adc: ad4691: add initial driver for AD4691 family
iio: adc: ad4691: add triggered buffer support
iio: adc: ad4691: add SPI offload support
iio: adc: ad4691: add oversampling support
docs: iio: adc: ad4691: add driver documentation
.../devicetree/bindings/iio/adc/adi,ad4691.yaml | 180 ++
Documentation/iio/ad4691.rst | 205 +++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 9 +
drivers/iio/adc/Kconfig | 15 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 1792 ++++++++++++++++++++
7 files changed, 2203 insertions(+)
---
base-commit: 11439c4635edd669ae435eec308f4ab8a0804808
change-id: 20260302-ad4692-multichannel-sar-adc-driver-78e4d44d24b2
Best regards,
--
Radu Sabau <radu.sabau@analog.com>
^ permalink raw reply
* [PATCH] docs: improve formatting and readability of the main README
From: Darío Ortega Leyva @ 2026-04-30 9:55 UTC (permalink / raw)
To: linux-doc; +Cc: corbet, linux-kernel
Restructured the main documentation file to use standard Markdown
headings instead of underlines for better rendering. Added anchor links
to the "Who Are You?" section to improve navigability for new readers,
and highlighted the AI Assistant warning for better visibility.
Signed-off-by: Darío Ortega Leyva <darioortegaleyva@gmail.com>
Signed-off-by: Dario Ortega Leyva <darioortegaleyva@gmail.com>
---
README | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 143 insertions(+)
create mode 100644 README
diff --git a/README b/README
new file mode 100644
index 000000000..bc2ce650c
--- /dev/null
+++ b/README
@@ -0,0 +1,143 @@
+# Linux Kernel
+
+The Linux kernel is the core of any Linux operating system. It manages
hardware, system resources, and provides the fundamental services for
all other software.
+
+## Quick Start
+
+* **Report a bug:** See `Documentation/admin-guide/reporting-issues.rst`
+* **Get the latest kernel:** Visit [kernel.org](https://kernel.org)
+* **Build the kernel:** See
`Documentation/admin-guide/quickly-build-trimmed-linux.rst`
+* **Join the community:** Visit [lore.kernel.org](https://lore.kernel.org/)
+
+## Essential Documentation
+
+All users should be familiar with the following core documents:
+
+* **Building requirements:** `Documentation/process/changes.rst`
+* **Code of Conduct:** `Documentation/process/code-of-conduct.rst`
+* **License:** See the `COPYING` file.
+
+> **Note:** Documentation can be built locally with `make htmldocs` or
viewed online at
[kernel.org/doc/html/latest/](https://www.kernel.org/doc/html/latest/).
+
+---
+
+## Who Are You?
+
+Find your role below to jump to the most relevant resources:
+
+* [New Kernel Developer](#new-kernel-developer) - Getting started with
kernel development
+* [Academic Researcher](#academic-researcher) - Studying kernel
internals and architecture
+* [Security Expert](#security-expert) - Hardening and vulnerability
analysis
+* [Backport/Maintenance Engineer](#backportmaintenance-engineer) -
Maintaining stable kernels
+* [System Administrator](#system-administrator) - Configuring and
troubleshooting
+* [Maintainer](#maintainer) - Leading subsystems and reviewing patches
+* [Hardware Vendor](#hardware-vendor) - Writing drivers for new hardware
+* [Distribution Maintainer](#distribution-maintainer) - Packaging
kernels for distros
+* [AI Coding Assistant](#ai-coding-assistant) - LLMs and AI-powered
development tools
+
+---
+
+## For Specific Users
+
+### New Kernel Developer
+Welcome! Start your kernel development journey here:
+
+* **Getting Started:** `Documentation/process/development-process.rst`
+* **Your First Patch:** `Documentation/process/submitting-patches.rst`
+* **Coding Style:** `Documentation/process/coding-style.rst`
+* **Build System:** `Documentation/kbuild/index.rst`
+* **Development Tools:** `Documentation/dev-tools/index.rst`
+* **Kernel Hacking Guide:** `Documentation/kernel-hacking/hacking.rst`
+* **Core APIs:** `Documentation/core-api/index.rst`
+
+### Academic Researcher
+Explore the kernel's architecture and internals:
+
+* **Researcher Guidelines:**
`Documentation/process/researcher-guidelines.rst`
+* **Memory Management:** `Documentation/mm/index.rst`
+* **Scheduler:** `Documentation/scheduler/index.rst`
+* **Networking Stack:** `Documentation/networking/index.rst`
+* **Filesystems:** `Documentation/filesystems/index.rst`
+* **RCU (Read-Copy Update):** `Documentation/RCU/index.rst`
+* **Locking Primitives:** `Documentation/locking/index.rst`
+* **Power Management:** `Documentation/power/index.rst`
+
+### Security Expert
+Security documentation and hardening guides:
+
+* **Security Documentation:** `Documentation/security/index.rst`
+* **LSM Development:** `Documentation/security/lsm-development.rst`
+* **Self Protection:** `Documentation/security/self-protection.rst`
+* **Reporting Vulnerabilities:** `Documentation/process/security-bugs.rst`
+* **CVE Procedures:** `Documentation/process/cve.rst`
+* **Embargoed Hardware Issues:**
`Documentation/process/embargoed-hardware-issues.rst`
+* **Security Features:** `Documentation/userspace-api/seccomp_filter.rst`
+
+### Backport/Maintenance Engineer
+Maintain and stabilize kernel versions:
+
+* **Stable Kernel Rules:** `Documentation/process/stable-kernel-rules.rst`
+* **Backporting Guide:** `Documentation/process/backporting.rst`
+* **Applying Patches:** `Documentation/process/applying-patches.rst`
+* **Subsystem Profile:**
`Documentation/maintainer/maintainer-entry-profile.rst`
+* **Git for Maintainers:** `Documentation/maintainer/configure-git.rst`
+
+### System Administrator
+Configure, tune, and troubleshoot Linux systems:
+
+* **Admin Guide:** `Documentation/admin-guide/index.rst`
+* **Kernel Parameters:** `Documentation/admin-guide/kernel-parameters.rst`
+* **Sysctl Tuning:** `Documentation/admin-guide/sysctl/index.rst`
+* **Tracing/Debugging:** `Documentation/trace/index.rst`
+* **Performance Security:** `Documentation/admin-guide/perf-security.rst`
+* **Hardware Monitoring:** `Documentation/hwmon/index.rst`
+
+### Maintainer
+Lead kernel subsystems and manage contributions:
+
+* **Maintainer Handbook:** `Documentation/maintainer/index.rst`
+* **Pull Requests:** `Documentation/maintainer/pull-requests.rst`
+* **Managing Patches:** `Documentation/maintainer/modifying-patches.rst`
+* **Rebasing and Merging:**
`Documentation/maintainer/rebasing-and-merging.rst`
+* **Development Process:** `Documentation/process/maintainer-handbooks.rst`
+* **Maintainer Entry Profile:**
`Documentation/maintainer/maintainer-entry-profile.rst`
+* **Git Configuration:** `Documentation/maintainer/configure-git.rst`
+
+### Hardware Vendor
+Write drivers and support new hardware:
+
+* **Driver API Guide:** `Documentation/driver-api/index.rst`
+* **Driver Model:** `Documentation/driver-api/driver-model/driver.rst`
+* **Device Drivers:** `Documentation/driver-api/infrastructure.rst`
+* **Bus Types:** `Documentation/driver-api/driver-model/bus.rst`
+* **Device Tree Bindings:** `Documentation/devicetree/bindings/`
+* **Power Management:** `Documentation/driver-api/pm/index.rst`
+* **DMA API:** `Documentation/core-api/dma-api.rst`
+
+### Distribution Maintainer
+Package and distribute the kernel:
+
+* **Stable Kernel Rules:** `Documentation/process/stable-kernel-rules.rst`
+* **ABI Documentation:** `Documentation/ABI/README`
+* **Kernel Configuration:** `Documentation/kbuild/kconfig.rst`
+* **Module Signing:** `Documentation/admin-guide/module-signing.rst`
+* **Kernel Parameters:** `Documentation/admin-guide/kernel-parameters.rst`
+* **Tainted Kernels:** `Documentation/admin-guide/tainted-kernels.rst`
+
+### AI Coding Assistant
+
+> **CRITICAL WARNING:** If you are an LLM or AI-powered coding
assistant, you **MUST** read and follow the AI coding assistants
documentation before contributing to the Linux kernel:
+>
+> `Documentation/process/coding-assistants.rst`
+>
+> This documentation contains essential requirements about licensing,
attribution, and the Developer Certificate of Origin that all AI tools
must comply with.
+
+---
+
+## Communication and Support
+
+* **Mailing Lists:** [lore.kernel.org](https://lore.kernel.org/)
+* **IRC:** `#kernelnewbies` on `irc.oftc.net`
+* **Bugzilla:** [bugzilla.kernel.org](https://bugzilla.kernel.org/)
+* **MAINTAINERS file:** Lists subsystem maintainers and mailing lists
(See `MAINTAINERS`)
+* **Email Clients:** `Documentation/process/email-clients.rst`
--
2.43.0
^ permalink raw reply related
* Re: [PATCH 1/1] Documentation: leds: leds-class: Document keyboard backlight LED class naming
From: Lee Jones @ 2026-04-30 9:25 UTC (permalink / raw)
To: Hans de Goede
Cc: Pavel Machek, Jonathan Corbet, Shuah Khan, Rishit Bansal,
Carlos Ferreira, Edip Hazuri, Mustafa Ekşi, Xavier Bestel,
linux-leds, linux-doc
In-Reply-To: <20260406174638.320135-2-johannes.goede@oss.qualcomm.com>
On Mon, 06 Apr 2026, Hans de Goede wrote:
> From: Carlos Ferreira <carlosmiguelferreira.2003@gmail.com>
>
> Document the existing practice of always using 'kbd_backlight' for
> the function part of LED class device names for LED class devices which
> control single-zone keyboard backlights.
>
> Also extend this existing practice with a new naming scheme for keyboards
> with zoned backlight control. There are several drivers in the works (see
> the Link:tags below) which offer backlight control for keyboards where
> the keyboard backlight is divided in a limited number of zones, e.g.
> "main", "cursor" and "numpad" zones.
>
> It is important to agree on a consistent naming scheme for these now,
> so that userspace can support multiple different models / vendors through
> a single unified naming scheme.
>
> Link: https://lore.kernel.org/platform-driver-x86/20230131235027.36304-1-rishitbansal0@gmail.com/
> Link: https://lore.kernel.org/platform-driver-x86/20240719100011.16656-1-carlosmiguelferreira.2003@gmail.com/
> Link: https://lore.kernel.org/platform-driver-x86/20260304105831.119349-3-edip@medip.dev/
> Link: https://lore.kernel.org/platform-driver-x86/20240806205001.191551-2-mustafa.eskieksi@gmail.com/
> Link: https://lore.kernel.org/linux-input/20260402075239.3829699-1-xav@bes.tel/
> Signed-off-by: Carlos Ferreira <carlosmiguelferreira.2003@gmail.com>
> Co-authored-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
> Signed-off-by: Hans de Goede <johannes.goede@oss.qualcomm.com>
The premise is fine I think.
> ---
> Documentation/leds/leds-class.rst | 63 +++++++++++++++++++++++++++++++
> 1 file changed, 63 insertions(+)
>
> diff --git a/Documentation/leds/leds-class.rst b/Documentation/leds/leds-class.rst
> index 5db620ed27aa..d2b042519a66 100644
> --- a/Documentation/leds/leds-class.rst
> +++ b/Documentation/leds/leds-class.rst
> @@ -116,6 +116,69 @@ above leaves scope for further attributes should they be needed. If sections
> of the name don't apply, just leave that section blank.
>
>
> +Keyboard backlight control LED Device Naming
> +============================================
> +
> +For backlit keyboards with a single brightness / color settings a single
> +(multicolor) LED class device should be used to allow userspace to change
> +the backlight brightness (and if possible the color). This LED class device
> +must use "kbd_backlight" for the function part of the LED class device name.
> +IOW the name must end with ":kbd_backlight".
> +
> +For backlit keyboards with multiple control zones, one (multicolor) LED class
> +device should be used per zone. These LED class devices' name must follow:
> +
> + "<devicename>:<color>:kbd_zoned_backlight-<zone_name>"
> +
> +and <devicename> must be the same for all zones of the same keyboard.
> +
> +<zone_name> should be descriptive of which part of the keyboard backlight
> +the zone covers and should be suitable for userspace to show to an end user
> +in an UI for controlling the zones.
> +
> +Where possible <zone_name> should be a value already used by other
> +zoned keyboards with a similar or identical zone layout, e.g.:
> +
> +<devicename>:<color>:kbd_zoned_backlight-right
> +<devicename>:<color>:kbd_zoned_backlight-middle
> +<devicename>:<color>:kbd_zoned_backlight-left
> +<devicename>:<color>:kbd_zoned_backlight-corners
> +<devicename>:<color>:kbd_zoned_backlight-wasd
> +
> +or:
> +
> +<devicename>:<color>:kbd_zoned_backlight-main
> +<devicename>:<color>:kbd_zoned_backlight-cursor
> +<devicename>:<color>:kbd_zoned_backlight-numpad
> +<devicename>:<color>:kbd_zoned_backlight-corners
> +<devicename>:<color>:kbd_zoned_backlight-wasd
> +
> +Note that this is intended for keyboards with a limited number of zones,
> +keyboards with per key addressable backlighting must not use LED class devices
> +since the sysfs API is not suitable for rapidly change multiple LEDs in one
> +"commit" as is necessary to do animations / special effects on such keyboards.
> +
> +An exception to the rule that all zones must follow:
> +
> + "<devicename>:<color>:kbd_zoned_backlight-<zone_name>"
> +
> +is made for the special case where there is a single big zone which controls
> +the backlighting of almost all of the keyboard and there are some small areas
> +with separate control, like just the 4 cursor keys, or the WASD keys. In this
> +case the main zone should use 'kbd_backlight' for the function part of the name
> +for compatiblity with (older) userspace code which is not aware of
Nit: compatibility
There may be others. Please run it through a spell checker.
> +the "kbd_zoned_backlight-<zone_name>" function naming scheme.
> +
> +While the smaller zones should use the new zoned naming scheme. Such a setup
> +would result in e.g.:
> +
> +<devicename>:<color>:kbd_backlight
> +<devicename>:<color>:kbd_zoned_backlight-wasd
> +
> +"kbd_zoned_backlight-<zone_name>" aware userspace should be aware of this
> +exception and check for a main zone with a "kbd_backlight" function-name.
> +
> +
> Brightness setting API
> ======================
>
> --
> 2.53.0
>
--
Lee Jones
^ permalink raw reply
* [PATCH net-next 3/3] docs: update ppp_generic.rst for API changes
From: Qingfang Deng @ 2026-04-30 9:05 UTC (permalink / raw)
To: David S. Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Simon Horman, Jonathan Corbet, Shuah Khan, netdev, linux-doc,
linux-kernel
Cc: linux-ppp, Qingfang Deng
In-Reply-To: <20260430090532.244758-1-qingfang.deng@linux.dev>
Document the new ppp_channel_conf struct and ppp_channel lifecycle
management changes.
Assisted-by: Gemini:gemini-3-flash
Signed-off-by: Qingfang Deng <qingfang.deng@linux.dev>
---
Documentation/networking/ppp_generic.rst | 33 ++++++++++--------------
1 file changed, 13 insertions(+), 20 deletions(-)
diff --git a/Documentation/networking/ppp_generic.rst b/Documentation/networking/ppp_generic.rst
index 5a10abce5964..8d63f997fb3f 100644
--- a/Documentation/networking/ppp_generic.rst
+++ b/Documentation/networking/ppp_generic.rst
@@ -124,18 +124,19 @@ presented to the start_xmit() function contain only the 2-byte
protocol number and the data, and the skbuffs presented to ppp_input()
must be in the same format.
-The channel must provide an instance of a ppp_channel struct to
-represent the channel. The channel is free to use the ``private`` field
-however it wishes. The channel should initialize the ``mtu`` and
-``hdrlen`` fields before calling ppp_register_channel() and not change
-them until after ppp_unregister_channel() returns. The ``mtu`` field
-represents the maximum size of the data part of the PPP frames, that
-is, it does not include the 2-byte protocol number.
+The channel must provide an instance of a ppp_channel_conf struct to
+describe the channel during registration. The generic layer will
+allocate a ppp_channel struct and return a pointer to it. The
+ppp_channel struct is opaque to the channel driver. The ``mtu`` field
+(if multilink is enabled) represents the maximum size of the data part
+of the PPP frames, that is, it does not include the 2-byte protocol
+number. ppp_channel_update_mtu() can be called by the channel driver
+to update the ``mtu`` field once LCP MRU negotiation is complete.
If the channel needs some headroom in the skbuffs presented to it for
transmission (i.e., some space free in the skbuff data area before the
start of the PPP frame), it should set the ``hdrlen`` field of the
-ppp_channel struct to the amount of headroom required. The generic
+ppp_channel_conf struct to the amount of headroom required. The generic
PPP layer will attempt to provide that much headroom but the channel
should still check if there is sufficient headroom and copy the skbuff
if there isn't.
@@ -199,20 +200,12 @@ The PPP generic layer has been designed to be SMP-safe. Locks are
used around accesses to the internal data structures where necessary
to ensure their integrity. As part of this, the generic layer
requires that the channels adhere to certain requirements and in turn
-provides certain guarantees to the channels. Essentially the channels
-are required to provide the appropriate locking on the ppp_channel
-structures that form the basis of the communication between the
-channel and the generic layer. This is because the channel provides
-the storage for the ppp_channel structure, and so the channel is
-required to provide the guarantee that this storage exists and is
-valid at the appropriate times.
+provides certain guarantees to the channels. The generic layer manages
+the ppp_channel object, ensuring it exists and is valid while the
+channel is registered.
The generic layer requires these guarantees from the channel:
-* The ppp_channel object must exist from the time that
- ppp_register_channel() is called until after the call to
- ppp_unregister_channel() returns.
-
* No thread may be in a call to any of ppp_input(), ppp_input_error(),
ppp_output_wakeup(), ppp_channel_index() or ppp_unit_number() for a
channel at the time that ppp_unregister_channel() is called for that
@@ -453,4 +446,4 @@ an interface unit are:
fragments is disabled. This ioctl is only available if the
CONFIG_PPP_MULTILINK option is selected.
-Last modified: 7-feb-2002
+Last modified: 16-apr-2026
--
2.43.0
^ permalink raw reply related
* [PATCH v12 2/3] hwmon: ltc4283: Add support for the LTC4283 Swap Controller
From: Nuno Sá via B4 Relay @ 2026-04-30 8:52 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski
In-Reply-To: <20260430-ltc4283-support-v12-0-5dc9901f2567@analog.com>
From: Nuno Sá <nuno.sa@analog.com>
Support the LTC4283 Hot Swap Controller. The device features programmable
current limit with foldback and independently adjustable inrush current to
optimize the MOSFET safe operating area (SOA). The SOA timer limits MOSFET
temperature rise for reliable protection against overstresses.
An I2C interface and onboard ADC allow monitoring of board current,
voltage, power, energy, and fault status.
Signed-off-by: Nuno Sá <nuno.sa@analog.com>
---
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/ltc4283.rst | 267 ++++++
MAINTAINERS | 1 +
drivers/hwmon/Kconfig | 12 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/ltc4283.c | 1795 +++++++++++++++++++++++++++++++++++++++
6 files changed, 2077 insertions(+)
diff --git a/Documentation/hwmon/index.rst b/Documentation/hwmon/index.rst
index 8b655e5d6b68..b09e6abdd275 100644
--- a/Documentation/hwmon/index.rst
+++ b/Documentation/hwmon/index.rst
@@ -145,6 +145,7 @@ Hardware Monitoring Kernel Drivers
ltc4260
ltc4261
ltc4282
+ ltc4283
ltc4286
macsmc-hwmon
max127
diff --git a/Documentation/hwmon/ltc4283.rst b/Documentation/hwmon/ltc4283.rst
new file mode 100644
index 000000000000..a650c595bc8f
--- /dev/null
+++ b/Documentation/hwmon/ltc4283.rst
@@ -0,0 +1,267 @@
+.. SPDX-License-Identifier: GPL-2.0-only
+
+Kernel drivers ltc4283
+==========================================
+
+Supported chips:
+
+ * Analog Devices LTC4283
+
+ Prefix: 'ltc4283'
+
+ Addresses scanned: -
+
+ Datasheet:
+
+ https://www.analog.com/media/en/technical-documentation/data-sheets/ltc4283.pdf
+
+Author: Nuno Sá <nuno.sa@analog.com>
+
+Description
+___________
+
+The LTC4283 negative voltage hot swap controller drives an external N-channel
+MOSFET to allow a board to be safely inserted and removed from a live backplane.
+The device features programmable current limit with foldback and independently
+adjustable inrush current to optimize the MOSFET safe operating area (SOA). The
+SOA timer limits MOSFET temperature rise for reliable protection against
+overstresses. An I2C interface and onboard gear-shift ADC allow monitoring of
+board current, voltage, power, energy, and fault status. Additional features
+respond to input UV/OV, interrupt the host when a fault has occurred, notify
+when output power is good, detect insertion of a board, turn off the MOSFET
+if an external supply monitor fails to indicate power good within a timeout
+period, and auto-reboot after a programmable delay following a host commanded
+turn-off.
+
+Sysfs entries
+_____________
+
+The following attributes are supported. Limits are read-write and all the other
+attributes are read-only. Note that the VADIOx channels might not be available
+if the ADIO pins are used as GPIOs (naturally also affects the respective
+differential channels).
+
+======================= ==========================================
+in0_lcrit_alarm Critical Undervoltage alarm
+in0_crit_alarm Critical Overvoltage alarm
+in0_reset_history Clears Under and Overvoltage fault logs.
+in0_label Channel label (VIN)
+
+in1_input Output voltage (mV).
+in1_min Undervoltage threshold
+in1_max Overvoltage threshold
+in1_lowest Lowest measured voltage
+in1_highest Highest measured voltage
+in1_reset_history Write 1 to reset history.
+in1_min_alarm Undervoltage alarm
+in1_max_alarm Overvoltage alarm
+in1_label Channel label (VPWR)
+
+in2_input Output voltage (mV).
+in2_min Undervoltage threshold
+in2_max Overvoltage threshold
+in2_lowest Lowest measured voltage
+in2_highest Highest measured voltage
+in2_reset_history Write 1 to reset history.
+in2_min_alarm Undervoltage alarm
+in2_max_alarm Overvoltage alarm
+in2_enable Enable/Disable monitoring.
+in2_label Channel label (VADI1)
+
+in3_input Output voltage (mV).
+in3_min Undervoltage threshold
+in3_max Overvoltage threshold
+in3_lowest Lowest measured voltage
+in3_highest Highest measured voltage
+in3_reset_history Write 1 to reset history.
+in3_min_alarm Undervoltage alarm
+in3_max_alarm Overvoltage alarm
+in3_enable Enable/Disable monitoring.
+in3_label Channel label (VADI2)
+
+in4_input Output voltage (mV).
+in4_min Undervoltage threshold
+in4_max Overvoltage threshold
+in4_lowest Lowest measured voltage
+in4_highest Highest measured voltage
+in4_reset_history Write 1 to reset history.
+in4_min_alarm Undervoltage alarm
+in4_max_alarm Overvoltage alarm
+in4_enable Enable/Disable monitoring.
+in4_label Channel label (VADI3)
+
+in5_input Output voltage (mV).
+in5_min Undervoltage threshold
+in5_max Overvoltage threshold
+in5_lowest Lowest measured voltage
+in5_highest Highest measured voltage
+in5_reset_history Write 1 to reset history.
+in5_min_alarm Undervoltage alarm
+in5_max_alarm Overvoltage alarm
+in5_enable Enable/Disable monitoring.
+in5_label Channel label (VADI4)
+
+in6_input Output voltage (mV).
+in6_min Undervoltage threshold
+in6_max Overvoltage threshold
+in6_lowest Lowest measured voltage
+in6_highest Highest measured voltage
+in6_reset_history Write 1 to reset history.
+in6_min_alarm Undervoltage alarm
+in6_max_alarm Overvoltage alarm
+in6_enable Enable/Disable monitoring.
+in6_label Channel label (VADIO1)
+
+in7_input Output voltage (mV).
+in7_min Undervoltage threshold
+in7_max Overvoltage threshold
+in7_lowest Lowest measured voltage
+in7_highest Highest measured voltage
+in7_reset_history Write 1 to reset history.
+in7_min_alarm Undervoltage alarm
+in7_max_alarm Overvoltage alarm
+in7_enable Enable/Disable monitoring.
+in7_label Channel label (VADIO2)
+
+in8_input Output voltage (mV).
+in8_min Undervoltage threshold
+in8_max Overvoltage threshold
+in8_lowest Lowest measured voltage
+in8_highest Highest measured voltage
+in8_reset_history Write 1 to reset history.
+in8_min_alarm Undervoltage alarm
+in8_max_alarm Overvoltage alarm
+in8_enable Enable/Disable monitoring.
+in8_label Channel label (VADIO3)
+
+in9_input Output voltage (mV).
+in9_min Undervoltage threshold
+in9_max Overvoltage threshold
+in9_lowest Lowest measured voltage
+in9_highest Highest measured voltage
+in9_reset_history Write 1 to reset history.
+in9_min_alarm Undervoltage alarm
+in9_max_alarm Overvoltage alarm
+in9_enable Enable/Disable monitoring.
+in9_label Channel label (VADIO4)
+
+in10_input Output voltage (mV).
+in10_min Undervoltage threshold
+in10_max Overvoltage threshold
+in10_lowest Lowest measured voltage
+in10_highest Highest measured voltage
+in10_reset_history Write 1 to reset history.
+in10_min_alarm Undervoltage alarm
+in10_max_alarm Overvoltage alarm
+in10_enable Enable/Disable monitoring.
+in10_label Channel label (DRNS)
+
+in11_input Output voltage (mV).
+in11_min Undervoltage threshold
+in11_max Overvoltage threshold
+in11_lowest Lowest measured voltage
+in11_highest Highest measured voltage
+in11_reset_history Write 1 to reset history.
+ Also clears fet bad and short fault logs.
+in11_min_alarm Undervoltage alarm
+in11_max_alarm Overvoltage alarm
+in11_enable Enable/Disable monitoring
+in11_fault Failure in the MOSFET. Either bad or shorted FET.
+in11_label Channel label (DRAIN)
+
+in12_input Output voltage (mV).
+in12_min Undervoltage threshold
+in12_max Overvoltage threshold
+in12_lowest Lowest measured voltage
+in12_highest Highest measured voltage
+in12_reset_history Write 1 to reset history.
+in12_min_alarm Undervoltage alarm
+in12_max_alarm Overvoltage alarm
+in12_enable Enable/Disable monitoring.
+in12_label Channel label (ADIN2-ADIN1)
+
+in13_input Output voltage (mV).
+in13_min Undervoltage threshold
+in13_max Overvoltage threshold
+in13_lowest Lowest measured voltage
+in13_highest Highest measured voltage
+in13_reset_history Write 1 to reset history.
+in13_min_alarm Undervoltage alarm
+in13_max_alarm Overvoltage alarm
+in13_enable Enable/Disable monitoring.
+in13_label Channel label (ADIN4-ADIN3)
+
+in14_input Output voltage (mV).
+in14_min Undervoltage threshold
+in14_max Overvoltage threshold
+in14_lowest Lowest measured voltage
+in14_highest Highest measured voltage
+in14_reset_history Write 1 to reset history.
+in14_min_alarm Undervoltage alarm
+in14_max_alarm Overvoltage alarm
+in14_enable Enable/Disable monitoring.
+in14_label Channel label (ADIO2-ADIO1)
+
+in15_input Output voltage (mV).
+in15_min Undervoltage threshold
+in15_max Overvoltage threshold
+in15_lowest Lowest measured voltage
+in15_highest Highest measured voltage
+in15_reset_history Write 1 to reset history.
+in15_min_alarm Undervoltage alarm
+in15_max_alarm Overvoltage alarm
+in15_enable Enable/Disable monitoring.
+in15_label Channel label (ADIO4-ADIO3)
+
+curr1_input Sense current (mA)
+curr1_min Undercurrent threshold
+curr1_max Overcurrent threshold
+curr1_lowest Lowest measured current
+curr1_highest Highest measured current
+curr1_reset_history Write 1 to reset curr1 history.
+ Also clears overcurrent fault logs.
+curr1_min_alarm Undercurrent alarm
+curr1_max_alarm Overcurrent alarm
+curr1_crit_alarm Critical Overcurrent alarm
+curr1_label Channel label (ISENSE)
+
+power1_input Power (in uW)
+power1_min Low power threshold
+power1_max High power threshold
+power1_input_lowest Historical minimum power use
+power1_input_highest Historical maximum power use
+power1_reset_history Write 1 to reset power1 history.
+ Also clears power fault logs.
+power1_min_alarm Low power alarm
+power1_max_alarm High power alarm
+power1_label Channel label (Power)
+
+energy1_input Measured energy over time (in microJoule)
+energy1_enable Enable/Disable Energy accumulation
+======================= ==========================================
+
+DebugFs entries
+_______________
+
+The chip also has a fault log register where failures can be logged. Hence,
+as these are logging events, we give access to them in debugfs. Note that
+even if some failure is detected in these logs, it does necessarily mean
+that the failure is still present. As mentioned in the proper Sysfs entries,
+these logs can be cleared by writing in the proper reset_history attribute.
+
+.. warning:: The debugfs interface is subject to change without notice
+ and is only available when the kernel is compiled with
+ ``CONFIG_DEBUG_FS`` defined.
+
+``/sys/kernel/debug/i2c/i2c-[X]/[X]-addr/``
+contains the following attributes:
+
+======================= ==========================================
+power1_failed_fault_log Set to 1 by a power1 fault occurring.
+power1_good_input_fault_log Set to 1 by a power1 good input fault occurring at PGIO3.
+in11_fet_short_fault_log Set to 1 when a FET-short fault occurs.
+in11_fet_bad_fault_log Set to 1 when a FET-BAD fault occurs.
+in0_lcrit_fault_log Set to 1 by a VIN undervoltage fault occurring.
+in0_crit_fault_log Set to 1 by a VIN overvoltage fault occurring.
+curr1_crit_fault_log Set to 1 by an overcurrent fault occurring.
+======================= ==========================================
diff --git a/MAINTAINERS b/MAINTAINERS
index 76dd9edca7d5..c657ca0a4652 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15243,6 +15243,7 @@ M: Nuno Sá <nuno.sa@analog.com>
L: linux-hwmon@vger.kernel.org
S: Supported
F: Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
+F: drivers/hwmon/ltc4283.c
LTC4286 HARDWARE MONITOR DRIVER
M: Delphine CC Chiu <Delphine_CC_Chiu@Wiwynn.com>
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index 14e4cea48acc..0c1cdd12f71e 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1157,6 +1157,18 @@ config SENSORS_LTC4282
This driver can also be built as a module. If so, the module will
be called ltc4282.
+config SENSORS_LTC4283
+ tristate "Analog Devices LTC4283"
+ depends on I2C
+ select REGMAP_I2C
+ select AUXILIARY_BUS
+ help
+ If you say yes here you get support for Analog Devices LTC4283
+ Negative Voltage Hot Swap Controller I2C interface.
+
+ This driver can also be built as a module. If so, the module will
+ be called ltc4283.
+
config SENSORS_LTQ_CPUTEMP
bool "Lantiq cpu temperature sensor driver"
depends on SOC_XWAY
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 4788996aa137..9e75e9f70375 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -147,6 +147,7 @@ obj-$(CONFIG_SENSORS_LTC4245) += ltc4245.o
obj-$(CONFIG_SENSORS_LTC4260) += ltc4260.o
obj-$(CONFIG_SENSORS_LTC4261) += ltc4261.o
obj-$(CONFIG_SENSORS_LTC4282) += ltc4282.o
+obj-$(CONFIG_SENSORS_LTC4283) += ltc4283.o
obj-$(CONFIG_SENSORS_LTQ_CPUTEMP) += ltq-cputemp.o
obj-$(CONFIG_SENSORS_MACSMC_HWMON) += macsmc-hwmon.o
obj-$(CONFIG_SENSORS_MAX1111) += max1111.o
diff --git a/drivers/hwmon/ltc4283.c b/drivers/hwmon/ltc4283.c
new file mode 100644
index 000000000000..d66815bfb134
--- /dev/null
+++ b/drivers/hwmon/ltc4283.c
@@ -0,0 +1,1795 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Analog Devices LTC4283 I2C Negative Voltage Hot Swap Controller (HWMON)
+ *
+ * Copyright 2025 Analog Devices Inc.
+ */
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/bits.h>
+
+#include <linux/debugfs.h>
+#include <linux/device.h>
+#include <linux/device/devres.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/math.h>
+#include <linux/math64.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+
+#include <linux/mod_devicetable.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/unaligned.h>
+#include <linux/units.h>
+
+#define LTC4283_SYSTEM_STATUS 0x00
+#define LTC4283_FAULT_STATUS 0x03
+#define LTC4283_OV_MASK BIT(0)
+#define LTC4283_UV_MASK BIT(1)
+#define LTC4283_OC_MASK BIT(2)
+#define LTC4283_FET_BAD_MASK BIT(3)
+#define LTC4283_FET_SHORT_MASK BIT(6)
+#define LTC4283_FAULT_LOG 0x04
+#define LTC4283_OV_FAULT_MASK BIT(0)
+#define LTC4283_UV_FAULT_MASK BIT(1)
+#define LTC4283_OC_FAULT_MASK BIT(2)
+#define LTC4283_FET_BAD_FAULT_MASK BIT(3)
+#define LTC4283_PGI_FAULT_MASK BIT(4)
+#define LTC4283_PWR_FAIL_FAULT_MASK BIT(5)
+#define LTC4283_FET_SHORT_FAULT_MASK BIT(6)
+#define LTC4283_ADC_ALM_LOG_1 0x05
+#define LTC4283_POWER_LOW_ALM BIT(0)
+#define LTC4283_POWER_HIGH_ALM BIT(1)
+#define LTC4283_SENSE_LOW_ALM BIT(4)
+#define LTC4283_SENSE_HIGH_ALM BIT(5)
+#define LTC4283_ADC_ALM_LOG_2 0x06
+#define LTC4283_ADC_ALM_LOG_3 0x07
+#define LTC4283_ADC_ALM_LOG_4 0x08
+#define LTC4283_ADC_ALM_LOG_5 0x09
+#define LTC4283_CONTROL_1 0x0a
+#define LTC4283_RW_PAGE_MASK BIT(0)
+#define LTC4283_PIGIO2_ACLB_MASK BIT(2)
+#define LTC4283_PWRGD_RST_CTRL_MASK BIT(3)
+#define LTC4283_FET_BAD_OFF_MASK BIT(4)
+#define LTC4283_THERM_TMR_MASK BIT(5)
+#define LTC4283_DVDT_MASK BIT(6)
+#define LTC4283_CONTROL_2 0x0b
+#define LTC4283_OV_RETRY_MASK BIT(0)
+#define LTC4283_UV_RETRY_MASK BIT(1)
+#define LTC4283_OC_RETRY_MASK GENMASK(3, 2)
+#define LTC4283_FET_BAD_RETRY_MASK GENMASK(5, 4)
+#define LTC4283_EXT_FAULT_RETRY_MASK BIT(7)
+#define LTC4283_RESERVED_OC 0x0c
+#define LTC4283_CONFIG_1 0x0d
+#define LTC4283_FB_MASK GENMASK(3, 2)
+#define LTC4283_ILIM_MASK GENMASK(7, 4)
+#define LTC4283_CONFIG_2 0x0e
+#define LTC4283_COOLING_DL_MASK GENMASK(3, 1)
+#define LTC4283_FTBD_DL_MASK GENMASK(5, 4)
+#define LTC4283_CONFIG_3 0x0f
+#define LTC4283_VPWR_DRNS_MASK BIT(6)
+#define LTC4283_EXTFLT_TURN_OFF_MASK BIT(7)
+#define LTC4283_PGIO_CONFIG 0x10
+#define LTC4283_PGIO1_CFG_MASK GENMASK(1, 0)
+#define LTC4283_PGIO2_CFG_MASK GENMASK(3, 2)
+#define LTC4283_PGIO3_CFG_MASK GENMASK(5, 4)
+#define LTC4283_PGIO4_CFG_MASK GENMASK(7, 6)
+#define LTC4283_PGIO_CONFIG_2 0x11
+#define LTC4283_ADC_MASK GENMASK(2, 0)
+#define LTC4283_ADC_SELECT(c) (0x13 + (c) / 8)
+#define LTC4283_ADC_SELECT_MASK(c) BIT((c) % 8)
+#define LTC4283_SENSE_MIN_TH 0x1b
+#define LTC4283_SENSE_MAX_TH 0x1c
+#define LTC4283_VPWR_MIN_TH 0x1d
+#define LTC4283_VPWR_MAX_TH 0x1e
+#define LTC4283_POWER_MIN_TH 0x1f
+#define LTC4283_POWER_MAX_TH 0x20
+#define LTC4283_ADC_2_MIN_TH(c) (0x21 + (c) * 2)
+#define LTC4283_ADC_2_MAX_TH(c) (0x22 + (c) * 2)
+#define LTC4283_ADC_2_MIN_TH_DIFF(c) (0x39 + (c) * 2)
+#define LTC4283_ADC_2_MAX_TH_DIFF(c) (0x3a + (c) * 2)
+#define LTC4283_SENSE 0x41
+#define LTC4283_SENSE_MIN 0x42
+#define LTC4283_SENSE_MAX 0x43
+#define LTC4283_VPWR 0x44
+#define LTC4283_VPWR_MIN 0x45
+#define LTC4283_VPWR_MAX 0x46
+#define LTC4283_POWER 0x47
+#define LTC4283_POWER_MIN 0x48
+#define LTC4283_POWER_MAX 0x49
+#define LTC4283_RESERVED_68 0x68
+#define LTC4283_RESERVED_6D 0x6D
+/* get channels from ADC 2 */
+#define LTC4283_ADC_2(c) (0x4a + (c) * 3)
+#define LTC4283_ADC_2_MIN(c) (0x4b + (c) * 3)
+#define LTC4283_ADC_2_MAX(c) (0x4c + (c) * 3)
+#define LTC4283_ADC_2_DIFF(c) (0x6e + (c) * 3)
+#define LTC4283_ADC_2_MIN_DIFF(c) (0x6f + (c) * 3)
+#define LTC4283_ADC_2_MAX_DIFF(c) (0x70 + (c) * 3)
+#define LTC4283_ENERGY 0x7a
+#define LTC4283_METER_CONTROL 0x84
+#define LTC4283_INTEGRATE_I_MASK BIT(0)
+#define LTC4283_METER_HALT_MASK BIT(6)
+#define LTC4283_RESERVED_86 0x86
+#define LTC4283_RESERVED_8F 0x8F
+#define LTC4283_FAULT_LOG_CTRL 0x90
+#define LTC4283_FAULT_LOG_EN_MASK BIT(7)
+#define LTC4283_RESERVED_91 0x91
+#define LTC4283_RESERVED_A1 0xA1
+#define LTC4283_RESERVED_A3 0xA3
+#define LTC4283_RESERVED_AC 0xAC
+#define LTC4283_POWER_PLAY_MSB 0xE7
+#define LTC4283_POWER_PLAY_LSB 0xE8
+#define LTC4283_RESERVED_F1 0xF1
+#define LTC4283_RESERVED_FF 0xFF
+
+/* also applies for differential channels */
+#define LTC4283_ADC1_FS_uV 32768
+#define LTC4283_ADC2_FS_mV 2048
+#define LTC4283_TCONV_uS 64103
+#define LTC4283_VILIM_MIN_uV 15000
+#define LTC4283_VILIM_MAX_uV 30000
+#define LTC4283_VILIM_RANGE \
+ (LTC4283_VILIM_MAX_uV - LTC4283_VILIM_MIN_uV + 1)
+
+#define LTC4283_PGIO_FUNC_GPIO 2
+#define LTC4283_PGIO2_FUNC_ACLB 3
+
+/*
+ * Maximum value for rsense in nano ohms. The reasoning for this value is that
+ * it's the max value for which multiplying by 256 does not overflow long on
+ * 32bits. For the minimum value, is a sane minimum rsense for which power_max
+ * does not overflow 32bits.
+ */
+#define LTC4283_MAX_RSENSE 1677721599
+#define LTC4283_MIN_RSENSE 50000
+
+/* voltage channels */
+enum {
+ LTC4283_CHAN_VIN,
+ LTC4283_CHAN_VPWR,
+ LTC4283_CHAN_ADI_1,
+ LTC4283_CHAN_ADI_2,
+ LTC4283_CHAN_ADI_3,
+ LTC4283_CHAN_ADI_4,
+ LTC4283_CHAN_ADIO_1,
+ LTC4283_CHAN_ADIO_2,
+ LTC4283_CHAN_ADIO_3,
+ LTC4283_CHAN_ADIO_4,
+ LTC4283_CHAN_DRNS,
+ LTC4283_CHAN_DRAIN,
+ /* differential channels */
+ LTC4283_CHAN_ADIN12,
+ LTC4283_CHAN_ADIN34,
+ LTC4283_CHAN_ADIO12,
+ LTC4283_CHAN_ADIO34,
+ LTC4283_CHAN_MAX
+};
+
+/* Just for ease of use on the regmap */
+#define LTC4283_ADIO34_MAX \
+ LTC4283_ADC_2_MAX_DIFF(LTC4283_CHAN_ADIO34 - LTC4283_CHAN_ADIN12)
+
+struct ltc4283_hwmon {
+ struct regmap *map;
+ struct i2c_client *client;
+ unsigned long gpio_mask;
+ unsigned long ch_enable_mask;
+ /* in microwatt */
+ unsigned long power_max;
+ /* in millivolt */
+ u32 vsense_max;
+ /* in tenths of microohm*/
+ u32 rsense;
+ bool energy_en;
+ bool ext_fault;
+};
+
+static int ltc4283_read_voltage_word(const struct ltc4283_hwmon *st,
+ u32 reg, u32 fs, long *val)
+{
+ unsigned int __raw;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &__raw);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST(__raw * fs, BIT(16));
+ return 0;
+}
+
+static int ltc4283_read_voltage_byte(const struct ltc4283_hwmon *st,
+ u32 reg, u32 fs, long *val)
+{
+ int ret;
+ u32 in;
+
+ ret = regmap_read(st->map, reg, &in);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST(in * fs, BIT(8));
+ return 0;
+}
+
+static u32 ltc4283_in_reg(u32 attr, u32 channel)
+{
+ switch (attr) {
+ case hwmon_in_input:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_DIFF(channel - LTC4283_CHAN_ADIN12);
+ case hwmon_in_highest:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MAX;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MAX(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MAX_DIFF(channel - LTC4283_CHAN_ADIN12);
+ case hwmon_in_lowest:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MIN;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MIN(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MIN_DIFF(channel - LTC4283_CHAN_ADIN12);
+ case hwmon_in_max:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MAX_TH;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MAX_TH(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MAX_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ default:
+ if (channel == LTC4283_CHAN_VPWR)
+ return LTC4283_VPWR_MIN_TH;
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN)
+ return LTC4283_ADC_2_MIN_TH(channel - LTC4283_CHAN_ADI_1);
+ return LTC4283_ADC_2_MIN_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ }
+}
+
+static int ltc4283_read_in_vals(const struct ltc4283_hwmon *st,
+ u32 attr, u32 channel, long *val)
+{
+ u32 reg = ltc4283_in_reg(attr, channel);
+ int ret;
+
+ if (channel < LTC4283_CHAN_ADIN12) {
+ if (attr != hwmon_in_max && attr != hwmon_in_min)
+ return ltc4283_read_voltage_word(st, reg,
+ LTC4283_ADC2_FS_mV,
+ val);
+
+ return ltc4283_read_voltage_byte(st, reg,
+ LTC4283_ADC2_FS_mV, val);
+ }
+
+ if (attr != hwmon_in_max && attr != hwmon_in_min)
+ ret = ltc4283_read_voltage_word(st, reg,
+ LTC4283_ADC1_FS_uV, val);
+ else
+ ret = ltc4283_read_voltage_byte(st, reg,
+ LTC4283_ADC1_FS_uV, val);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST(*val, MILLI);
+ return 0;
+}
+
+static int ltc4283_read_alarm(struct ltc4283_hwmon *st, u32 reg,
+ u32 mask, long *val)
+{
+ u32 alarm;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &alarm);
+ if (ret)
+ return ret;
+
+ *val = !!(alarm & mask);
+
+ /* If not status/fault logs, clear the alarm after reading it. */
+ if (reg != LTC4283_FAULT_STATUS && reg != LTC4283_FAULT_LOG)
+ return regmap_write(st->map, reg, alarm & ~mask);
+
+ return 0;
+}
+
+static int ltc4283_read_in_alarm(struct ltc4283_hwmon *st, u32 channel,
+ bool max_alm, long *val)
+{
+ if (channel == LTC4283_CHAN_VPWR)
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ BIT(2 + max_alm), val);
+
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_ADI_4) {
+ u32 bit = (channel - LTC4283_CHAN_ADI_1) * 2;
+ /*
+ * Lower channels go to higher bits. We also want to go +1 down
+ * in the min_alarm case.
+ */
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_2,
+ BIT(7 - bit - !max_alm), val);
+ }
+
+ if (channel >= LTC4283_CHAN_ADIO_1 && channel <= LTC4283_CHAN_ADIO_4) {
+ u32 bit = (channel - LTC4283_CHAN_ADIO_1) * 2;
+
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_3,
+ BIT(7 - bit - !max_alm), val);
+ }
+
+ if (channel >= LTC4283_CHAN_ADIN12 && channel <= LTC4283_CHAN_ADIO34) {
+ u32 bit = (channel - LTC4283_CHAN_ADIN12) * 2;
+
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_5,
+ BIT(7 - bit - !max_alm), val);
+ }
+
+ if (channel == LTC4283_CHAN_DRNS)
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_4,
+ BIT(6 + max_alm), val);
+
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_4, BIT(4 + max_alm),
+ val);
+}
+
+static int ltc4283_read_in(struct ltc4283_hwmon *st, u32 attr, u32 channel,
+ long *val)
+{
+ switch (attr) {
+ case hwmon_in_input:
+ if (!test_bit(channel, &st->ch_enable_mask))
+ return -ENODATA;
+
+ return ltc4283_read_in_vals(st, attr, channel, val);
+ case hwmon_in_highest:
+ case hwmon_in_lowest:
+ case hwmon_in_max:
+ case hwmon_in_min:
+ return ltc4283_read_in_vals(st, attr, channel, val);
+ case hwmon_in_max_alarm:
+ return ltc4283_read_in_alarm(st, channel, true, val);
+ case hwmon_in_min_alarm:
+ return ltc4283_read_in_alarm(st, channel, false, val);
+ case hwmon_in_crit_alarm:
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_OV_MASK, val);
+ case hwmon_in_lcrit_alarm:
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_UV_MASK, val);
+ case hwmon_in_fault:
+ /*
+ * We report failure if we detect either a fer_bad or a
+ * fet_short in the status register.
+ */
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_FET_BAD_MASK | LTC4283_FET_SHORT_MASK, val);
+ case hwmon_in_enable:
+ *val = test_bit(channel, &st->ch_enable_mask);
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+ return 0;
+}
+
+static int ltc4283_read_current_word(const struct ltc4283_hwmon *st, u32 reg,
+ long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * DECA * MILLI;
+ unsigned int __raw;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &__raw);
+ if (ret)
+ return ret;
+
+ *val = DIV64_U64_ROUND_CLOSEST(__raw * temp,
+ BIT_ULL(16) * st->rsense);
+
+ return 0;
+}
+
+static int ltc4283_read_current_byte(const struct ltc4283_hwmon *st, u32 reg,
+ long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * DECA * MILLI;
+ u32 curr;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &curr);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST_ULL(curr * temp, BIT(8) * st->rsense);
+ return 0;
+}
+
+static int ltc4283_read_curr(struct ltc4283_hwmon *st, u32 attr, long *val)
+{
+ switch (attr) {
+ case hwmon_curr_input:
+ return ltc4283_read_current_word(st, LTC4283_SENSE, val);
+ case hwmon_curr_highest:
+ return ltc4283_read_current_word(st, LTC4283_SENSE_MAX, val);
+ case hwmon_curr_lowest:
+ return ltc4283_read_current_word(st, LTC4283_SENSE_MIN, val);
+ case hwmon_curr_max:
+ return ltc4283_read_current_byte(st, LTC4283_SENSE_MAX_TH, val);
+ case hwmon_curr_min:
+ return ltc4283_read_current_byte(st, LTC4283_SENSE_MIN_TH, val);
+ case hwmon_curr_max_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_SENSE_HIGH_ALM, val);
+ case hwmon_curr_min_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_SENSE_LOW_ALM, val);
+ case hwmon_curr_crit_alarm:
+ return ltc4283_read_alarm(st, LTC4283_FAULT_STATUS,
+ LTC4283_OC_MASK, val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_read_power_word(const struct ltc4283_hwmon *st,
+ u32 reg, long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ unsigned int __raw;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &__raw);
+ if (ret)
+ return ret;
+
+ /*
+ * Power is given by:
+ * P = CODE(16b) * 32.768mV * 2.048V / (2^16 * Rsense)
+ */
+ *val = DIV64_U64_ROUND_CLOSEST(temp * __raw, BIT_ULL(16) * st->rsense);
+
+ return 0;
+}
+
+static int ltc4283_read_power_byte(const struct ltc4283_hwmon *st,
+ u32 reg, long *val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ u32 power;
+ int ret;
+
+ ret = regmap_read(st->map, reg, &power);
+ if (ret)
+ return ret;
+
+ *val = DIV_ROUND_CLOSEST_ULL(power * temp, BIT(8) * st->rsense);
+
+ return 0;
+}
+
+static int ltc4283_read_power(struct ltc4283_hwmon *st, u32 attr, long *val)
+{
+ switch (attr) {
+ case hwmon_power_input:
+ return ltc4283_read_power_word(st, LTC4283_POWER, val);
+ case hwmon_power_input_highest:
+ return ltc4283_read_power_word(st, LTC4283_POWER_MAX, val);
+ case hwmon_power_input_lowest:
+ return ltc4283_read_power_word(st, LTC4283_POWER_MIN, val);
+ case hwmon_power_max_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_POWER_HIGH_ALM, val);
+ case hwmon_power_min_alarm:
+ return ltc4283_read_alarm(st, LTC4283_ADC_ALM_LOG_1,
+ LTC4283_POWER_LOW_ALM, val);
+ case hwmon_power_max:
+ return ltc4283_read_power_byte(st, LTC4283_POWER_MAX_TH, val);
+ case hwmon_power_min:
+ return ltc4283_read_power_byte(st, LTC4283_POWER_MIN_TH, val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_read_energy(struct ltc4283_hwmon *st, u32 attr, s64 *val)
+{
+ u64 temp = LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV, energy;
+ u8 raw[8] = {};
+ int ret;
+
+ if (!st->energy_en)
+ return -ENODATA;
+
+ ret = i2c_smbus_read_i2c_block_data(st->client, LTC4283_ENERGY, 6, raw);
+ if (ret < 0)
+ return ret;
+ if (ret != 6)
+ return -EIO;
+
+ energy = get_unaligned_be64(raw) >> 16;
+
+ /*
+ * The formula for energy is given by:
+ * E = CODE(48b) * 32.768mV * 2.048V * Tconv / 2^24 * Rsense
+ *
+ * As Rsense can have tenths of micro-ohm resolution, we need to
+ * multiply by DECA to get microjoule.
+ */
+
+ /*
+ * Use mul_u64_u64_div_u64() to handle the 128-bit intermediate
+ * product of energy (up to 48 bits) * temp * Tconv without overflow.
+ * Multiply rsense by CENTI to convert from tenths-of-microohm back
+ * to nanoohm so the result comes out in microjoule.
+ */
+ energy = mul_u64_u64_div_u64(energy, temp * LTC4283_TCONV_uS,
+ BIT_ULL(24) * st->rsense * CENTI);
+
+ *val = energy;
+ return 0;
+}
+
+static int ltc4283_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *val)
+{
+ struct ltc4283_hwmon *st = dev_get_drvdata(dev);
+
+ switch (type) {
+ case hwmon_in:
+ return ltc4283_read_in(st, attr, channel, val);
+ case hwmon_curr:
+ return ltc4283_read_curr(st, attr, val);
+ case hwmon_power:
+ return ltc4283_read_power(st, attr, val);
+ case hwmon_energy:
+ *val = st->energy_en;
+ return 0;
+ case hwmon_energy64:
+ return ltc4283_read_energy(st, attr, (s64 *)val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_write_power_byte(const struct ltc4283_hwmon *st, u32 reg,
+ long val)
+{
+ u64 temp = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ u32 __raw;
+
+ val = clamp_val(val, 0, st->power_max);
+ __raw = DIV64_U64_ROUND_CLOSEST(val * BIT_ULL(8) * st->rsense, temp);
+
+ return regmap_write(st->map, reg, __raw);
+}
+
+static int ltc4283_write_power_word(const struct ltc4283_hwmon *st,
+ u32 reg, unsigned long val)
+{
+ u64 divisor = (u64)LTC4283_ADC1_FS_uV * LTC4283_ADC2_FS_mV * DECA * MILLI;
+ u16 __raw;
+
+ __raw = mul_u64_u64_div_u64(val, st->rsense * BIT_ULL(16), divisor);
+
+ return regmap_write(st->map, reg, __raw);
+}
+
+static int ltc4283_reset_power_hist(struct ltc4283_hwmon *st)
+{
+ int ret;
+
+ ret = ltc4283_write_power_word(st, LTC4283_POWER_MIN, st->power_max);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_write_power_word(st, LTC4283_POWER_MAX, 0);
+ if (ret)
+ return ret;
+
+ /* Clear possible power faults. */
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_PWR_FAIL_FAULT_MASK | LTC4283_PGI_FAULT_MASK);
+}
+
+static int ltc4283_write_power(struct ltc4283_hwmon *st, u32 attr, long val)
+{
+ switch (attr) {
+ case hwmon_power_max:
+ return ltc4283_write_power_byte(st, LTC4283_POWER_MAX_TH, val);
+ case hwmon_power_min:
+ return ltc4283_write_power_byte(st, LTC4283_POWER_MIN_TH, val);
+ case hwmon_power_reset_history:
+ return ltc4283_reset_power_hist(st);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_write_in_history(struct ltc4283_hwmon *st, u32 reg,
+ long lowest, u32 fs)
+{
+ u32 __raw;
+ int ret;
+
+ __raw = DIV_ROUND_CLOSEST(BIT(16) * lowest, fs);
+ if (__raw == BIT(16))
+ __raw = U16_MAX;
+
+ ret = regmap_write(st->map, reg, __raw);
+ if (ret)
+ return ret;
+
+ return regmap_write(st->map, reg + 1, 0);
+}
+
+static int ltc4283_write_in_byte(const struct ltc4283_hwmon *st,
+ u32 reg, u32 fs, long val)
+{
+ u32 __raw;
+
+ val = clamp_val(val, 0, fs);
+ __raw = DIV_ROUND_CLOSEST(val * BIT(8), fs);
+ if (__raw == BIT(8))
+ __raw = U8_MAX;
+
+ return regmap_write(st->map, reg, __raw);
+}
+
+static int ltc4283_reset_in_hist(struct ltc4283_hwmon *st, u32 channel)
+{
+ u32 reg, fs;
+ int ret;
+
+ /*
+ * Make sure to clear possible under/over voltage faults. Otherwise the
+ * chip won't latch on again.
+ */
+ if (channel == LTC4283_CHAN_VIN)
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_OV_FAULT_MASK | LTC4283_UV_FAULT_MASK);
+
+ if (channel == LTC4283_CHAN_VPWR)
+ return ltc4283_write_in_history(st, LTC4283_VPWR_MIN,
+ LTC4283_ADC2_FS_mV,
+ LTC4283_ADC2_FS_mV);
+
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN) {
+ fs = LTC4283_ADC2_FS_mV;
+ reg = LTC4283_ADC_2_MIN(channel - LTC4283_CHAN_ADI_1);
+ } else {
+ fs = LTC4283_ADC1_FS_uV;
+ reg = LTC4283_ADC_2_MIN_DIFF(channel - LTC4283_CHAN_ADIN12);
+ }
+
+ ret = ltc4283_write_in_history(st, reg, fs, fs);
+ if (ret)
+ return ret;
+ if (channel != LTC4283_CHAN_DRAIN)
+ return 0;
+
+ /* Then, let's also clear possible fet faults. Same as above. */
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_FET_BAD_FAULT_MASK | LTC4283_FET_SHORT_FAULT_MASK);
+}
+
+static int ltc4283_write_in_en(struct ltc4283_hwmon *st, u32 channel, bool en)
+{
+ unsigned int bit, adc_idx = channel - LTC4283_CHAN_ADI_1;
+ unsigned int reg = LTC4283_ADC_SELECT(adc_idx);
+ int ret;
+
+ bit = LTC4283_ADC_SELECT_MASK(adc_idx);
+ if (channel > LTC4283_CHAN_DRAIN)
+ /* Account for two reserved fields after DRAIN. */
+ bit <<= 2;
+
+ if (en)
+ ret = regmap_set_bits(st->map, reg, bit);
+ else
+ ret = regmap_clear_bits(st->map, reg, bit);
+ if (ret)
+ return ret;
+
+ __assign_bit(channel, &st->ch_enable_mask, en);
+ return 0;
+}
+
+static int ltc4283_write_minmax(struct ltc4283_hwmon *st, long val,
+ u32 channel, bool is_max)
+{
+ u32 reg;
+
+ if (channel == LTC4283_CHAN_VPWR) {
+ if (is_max)
+ return ltc4283_write_in_byte(st, LTC4283_VPWR_MAX_TH,
+ LTC4283_ADC2_FS_mV, val);
+
+ return ltc4283_write_in_byte(st, LTC4283_VPWR_MIN_TH,
+ LTC4283_ADC2_FS_mV, val);
+ }
+
+ if (channel >= LTC4283_CHAN_ADI_1 && channel <= LTC4283_CHAN_DRAIN) {
+ if (is_max) {
+ reg = LTC4283_ADC_2_MAX_TH(channel - LTC4283_CHAN_ADI_1);
+ return ltc4283_write_in_byte(st, reg,
+ LTC4283_ADC2_FS_mV, val);
+ }
+
+ reg = LTC4283_ADC_2_MIN_TH(channel - LTC4283_CHAN_ADI_1);
+ return ltc4283_write_in_byte(st, reg, LTC4283_ADC2_FS_mV, val);
+ }
+
+ /* Clamp before multiplying to avoid overflow on any arch. */
+ val = clamp_val(val, 0, LONG_MAX / MILLI);
+
+ if (is_max) {
+ reg = LTC4283_ADC_2_MAX_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ return ltc4283_write_in_byte(st, reg, LTC4283_ADC1_FS_uV,
+ val * MILLI);
+ }
+
+ reg = LTC4283_ADC_2_MIN_TH_DIFF(channel - LTC4283_CHAN_ADIN12);
+ return ltc4283_write_in_byte(st, reg, LTC4283_ADC1_FS_uV, val * MILLI);
+}
+
+static int ltc4283_write_in(struct ltc4283_hwmon *st, u32 attr, long val,
+ int channel)
+{
+ switch (attr) {
+ case hwmon_in_max:
+ return ltc4283_write_minmax(st, val, channel, true);
+ case hwmon_in_min:
+ return ltc4283_write_minmax(st, val, channel, false);
+ case hwmon_in_reset_history:
+ return ltc4283_reset_in_hist(st, channel);
+ case hwmon_in_enable:
+ return ltc4283_write_in_en(st, channel, !!val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_write_curr_byte(const struct ltc4283_hwmon *st,
+ u32 reg, long val)
+{
+ u32 temp = LTC4283_ADC1_FS_uV * DECA * MILLI;
+ u32 reg_val, isense_max;
+
+ isense_max = DIV_ROUND_CLOSEST(st->vsense_max * MICRO * DECA, st->rsense);
+ val = clamp_val(val, 0, isense_max);
+ reg_val = DIV_ROUND_CLOSEST_ULL(val * BIT_ULL(8) * st->rsense, temp);
+
+ return regmap_write(st->map, reg, reg_val);
+}
+
+static int ltc4283_write_curr_history(struct ltc4283_hwmon *st)
+{
+ int ret;
+
+ ret = ltc4283_write_in_history(st, LTC4283_SENSE_MIN,
+ st->vsense_max * MILLI,
+ LTC4283_ADC1_FS_uV);
+ if (ret)
+ return ret;
+
+ /* Now, let's also clear possible overcurrent logs. */
+ return regmap_clear_bits(st->map, LTC4283_FAULT_LOG,
+ LTC4283_OC_FAULT_MASK);
+}
+
+static int ltc4283_write_curr(struct ltc4283_hwmon *st, u32 attr, long val)
+{
+ switch (attr) {
+ case hwmon_curr_max:
+ return ltc4283_write_curr_byte(st, LTC4283_SENSE_MAX_TH, val);
+ case hwmon_curr_min:
+ return ltc4283_write_curr_byte(st, LTC4283_SENSE_MIN_TH, val);
+ case hwmon_curr_reset_history:
+ return ltc4283_write_curr_history(st);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static int ltc4283_energy_enable_set(struct ltc4283_hwmon *st, long val)
+{
+ int ret;
+
+ /* Setting the bit halts the meter. */
+ val = !!val;
+ ret = regmap_update_bits(st->map, LTC4283_METER_CONTROL,
+ LTC4283_METER_HALT_MASK,
+ FIELD_PREP(LTC4283_METER_HALT_MASK, !val));
+ if (ret)
+ return ret;
+
+ st->energy_en = val;
+
+ return 0;
+}
+
+static int ltc4283_write(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long val)
+{
+ struct ltc4283_hwmon *st = dev_get_drvdata(dev);
+
+ switch (type) {
+ case hwmon_power:
+ return ltc4283_write_power(st, attr, val);
+ case hwmon_in:
+ return ltc4283_write_in(st, attr, val, channel);
+ case hwmon_curr:
+ return ltc4283_write_curr(st, attr, val);
+ case hwmon_energy:
+ return ltc4283_energy_enable_set(st, val);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static umode_t ltc4283_in_is_visible(const struct ltc4283_hwmon *st,
+ u32 attr, int channel)
+{
+ /* If ADIO is set as a GPIO, don´t make it visible. */
+ if (channel >= LTC4283_CHAN_ADIO_1 && channel <= LTC4283_CHAN_ADIO_4) {
+ /* ADIOX pins come at index 0 in the gpio mask. */
+ channel -= LTC4283_CHAN_ADIO_1;
+ if (test_bit(channel, &st->gpio_mask))
+ return 0;
+ }
+
+ /* Also take care of differential channels. */
+ if (channel >= LTC4283_CHAN_ADIO12 && channel <= LTC4283_CHAN_ADIO34) {
+ channel -= LTC4283_CHAN_ADIO12;
+ /* If one channel in the pair is used, make it invisible. */
+ if (test_bit(channel * 2, &st->gpio_mask) ||
+ test_bit(channel * 2 + 1, &st->gpio_mask))
+ return 0;
+ }
+
+ switch (attr) {
+ case hwmon_in_input:
+ case hwmon_in_highest:
+ case hwmon_in_lowest:
+ case hwmon_in_max_alarm:
+ case hwmon_in_min_alarm:
+ case hwmon_in_label:
+ case hwmon_in_lcrit_alarm:
+ case hwmon_in_crit_alarm:
+ case hwmon_in_fault:
+ return 0444;
+ case hwmon_in_max:
+ case hwmon_in_min:
+ case hwmon_in_enable:
+ return 0644;
+ case hwmon_in_reset_history:
+ return 0200;
+ default:
+ return 0;
+ }
+}
+
+static umode_t ltc4283_curr_is_visible(u32 attr)
+{
+ switch (attr) {
+ case hwmon_curr_input:
+ case hwmon_curr_highest:
+ case hwmon_curr_lowest:
+ case hwmon_curr_max_alarm:
+ case hwmon_curr_min_alarm:
+ case hwmon_curr_crit_alarm:
+ case hwmon_curr_label:
+ return 0444;
+ case hwmon_curr_max:
+ case hwmon_curr_min:
+ return 0644;
+ case hwmon_curr_reset_history:
+ return 0200;
+ default:
+ return 0;
+ }
+}
+
+static umode_t ltc4283_power_is_visible(u32 attr)
+{
+ switch (attr) {
+ case hwmon_power_input:
+ case hwmon_power_input_highest:
+ case hwmon_power_input_lowest:
+ case hwmon_power_label:
+ case hwmon_power_max_alarm:
+ case hwmon_power_min_alarm:
+ return 0444;
+ case hwmon_power_max:
+ case hwmon_power_min:
+ return 0644;
+ case hwmon_power_reset_history:
+ return 0200;
+ default:
+ return 0;
+ }
+}
+
+static umode_t ltc4283_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ switch (type) {
+ case hwmon_in:
+ return ltc4283_in_is_visible(data, attr, channel);
+ case hwmon_curr:
+ return ltc4283_curr_is_visible(attr);
+ case hwmon_power:
+ return ltc4283_power_is_visible(attr);
+ case hwmon_energy:
+ /* hwmon_energy_enable */
+ return 0644;
+ case hwmon_energy64:
+ /* hwmon_energy_input */
+ return 0444;
+ default:
+ return 0;
+ }
+}
+
+static const char * const ltc4283_in_strs[] = {
+ "VIN", "VPWR", "VADI1", "VADI2", "VADI3", "VADI4", "VADIO1", "VADIO2",
+ "VADIO3", "VADIO4", "DRNS", "DRAIN", "ADIN2-ADIN1", "ADIN4-ADIN3",
+ "ADIO2-ADIO1", "ADIO4-ADIO3"
+};
+
+static int ltc4283_read_labels(struct device *dev,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel, const char **str)
+{
+ switch (type) {
+ case hwmon_in:
+ *str = ltc4283_in_strs[channel];
+ return 0;
+ case hwmon_curr:
+ *str = "ISENSE";
+ return 0;
+ case hwmon_power:
+ *str = "Power";
+ return 0;
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+/*
+ * Set max limits for ISENSE and Power as that depends on the max voltage on
+ * rsense that is defined in ILIM_ADJUST. This is specially important for power
+ * because for some rsense and vfsout values, if we allow the default raw 255
+ * value, that would overflow long in 32bit archs when reading back the max
+ * power limit.
+ */
+static int ltc4283_set_max_limits(struct ltc4283_hwmon *st, struct device *dev)
+{
+ u32 temp = st->vsense_max * DECA * MICRO;
+ int ret;
+
+ ret = ltc4283_write_in_byte(st, LTC4283_SENSE_MAX_TH, LTC4283_ADC1_FS_uV,
+ st->vsense_max * MILLI);
+ if (ret)
+ return ret;
+
+ /* Power is given by ISENSE * Vout. */
+ st->power_max = DIV_ROUND_CLOSEST(temp, st->rsense) * LTC4283_ADC2_FS_mV;
+ return ltc4283_write_power_byte(st, LTC4283_POWER_MAX_TH, st->power_max);
+}
+
+static int ltc4283_parse_array_prop(const struct ltc4283_hwmon *st,
+ struct device *dev, const char *prop,
+ const u32 *vals, u32 n_vals)
+{
+ u32 prop_val;
+ int ret;
+ u32 i;
+
+ ret = device_property_read_u32(dev, prop, &prop_val);
+ if (ret)
+ return n_vals;
+
+ for (i = 0; i < n_vals; i++) {
+ if (prop_val != vals[i])
+ continue;
+
+ return i;
+ }
+
+ return dev_err_probe(dev, -EINVAL,
+ "Invalid %s property value %u\n", prop, prop_val);
+}
+
+static int ltc4283_get_defaults(struct ltc4283_hwmon *st)
+{
+ u32 reg_val, ilm_adjust, c;
+ int ret;
+
+ ret = regmap_read(st->map, LTC4283_METER_CONTROL, ®_val);
+ if (ret)
+ return ret;
+
+ st->energy_en = !FIELD_GET(LTC4283_METER_HALT_MASK, reg_val);
+
+ ret = regmap_read(st->map, LTC4283_CONFIG_1, ®_val);
+ if (ret)
+ return ret;
+
+ ilm_adjust = FIELD_GET(LTC4283_ILIM_MASK, reg_val);
+ st->vsense_max = LTC4283_VILIM_MIN_uV / MILLI + ilm_adjust;
+
+ ret = regmap_read(st->map, LTC4283_PGIO_CONFIG, ®_val);
+ if (ret)
+ return ret;
+
+ /* Can be latter overwritten in ltc4283_pgio_config() */
+ if (FIELD_GET(LTC4283_PGIO4_CFG_MASK, reg_val) < LTC4283_PGIO_FUNC_GPIO)
+ st->ext_fault = true;
+
+ /* VPWR and VIN are always enabled */
+ __set_bit(LTC4283_CHAN_VIN, &st->ch_enable_mask);
+ __set_bit(LTC4283_CHAN_VPWR, &st->ch_enable_mask);
+ for (c = LTC4283_CHAN_ADI_1; c < LTC4283_CHAN_MAX; c++) {
+ u32 chan = c - LTC4283_CHAN_ADI_1, bit;
+
+ ret = regmap_read(st->map, LTC4283_ADC_SELECT(chan), ®_val);
+ if (ret)
+ return ret;
+
+ bit = LTC4283_ADC_SELECT_MASK(chan);
+ if (c > LTC4283_CHAN_DRAIN)
+ /* account for two reserved fields after DRAIN */
+ bit <<= 2;
+
+ if (!(bit & reg_val))
+ continue;
+
+ __set_bit(c, &st->ch_enable_mask);
+ }
+
+ return 0;
+}
+
+static const char * const ltc4283_pgio1_funcs[] = {
+ "inverted_power_good", "power_good", "gpio"
+};
+
+static const char * const ltc4283_pgio2_funcs[] = {
+ "inverted_power_good", "power_good", "gpio", "active_current_limiting"
+};
+
+static const char * const ltc4283_pgio3_funcs[] = {
+ "inverted_power_good_input", "power_good_input", "gpio"
+};
+
+static const char * const ltc4283_pgio4_funcs[] = {
+ "inverted_external_fault", "external_fault", "gpio"
+};
+
+enum {
+ LTC4283_PIN_ADIO1,
+ LTC4283_PIN_ADIO2,
+ LTC4283_PIN_ADIO3,
+ LTC4283_PIN_ADIO4,
+ LTC4283_PIN_PGIO1,
+ LTC4283_PIN_PGIO2,
+ LTC4283_PIN_PGIO3,
+ LTC4283_PIN_PGIO4,
+};
+
+static int ltc4283_pgio_config(struct ltc4283_hwmon *st, struct device *dev)
+{
+ int ret, func;
+
+ func = device_property_match_property_string(dev, "adi,pgio1-func",
+ ltc4283_pgio1_funcs,
+ ARRAY_SIZE(ltc4283_pgio1_funcs));
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio1-func property\n");
+ if (func >= 0) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO1, &st->gpio_mask);
+ /* If GPIO, default to an input pin. */
+ func++;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO1_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO1_CFG_MASK, func));
+ if (ret)
+ return ret;
+ }
+
+ func = device_property_match_property_string(dev, "adi,pgio2-func",
+ ltc4283_pgio2_funcs,
+ ARRAY_SIZE(ltc4283_pgio2_funcs));
+
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio2-func property\n");
+ if (func >= 0) {
+ if (func != LTC4283_PGIO2_FUNC_ACLB) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO2, &st->gpio_mask);
+ func++;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO2_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO2_CFG_MASK, func));
+ } else {
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_PIGIO2_ACLB_MASK);
+ }
+
+ if (ret)
+ return ret;
+ }
+
+ func = device_property_match_property_string(dev, "adi,pgio3-func",
+ ltc4283_pgio3_funcs,
+ ARRAY_SIZE(ltc4283_pgio3_funcs));
+
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio3-func property\n");
+ if (func >= 0) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO3, &st->gpio_mask);
+ func++;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO3_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO3_CFG_MASK, func));
+ if (ret)
+ return ret;
+ }
+
+ func = device_property_match_property_string(dev, "adi,pgio4-func",
+ ltc4283_pgio4_funcs,
+ ARRAY_SIZE(ltc4283_pgio4_funcs));
+
+ if (func < 0 && func != -EINVAL)
+ return dev_err_probe(dev, func,
+ "Invalid adi,pgio4-func property\n");
+ if (func >= 0) {
+ if (func == LTC4283_PGIO_FUNC_GPIO) {
+ __set_bit(LTC4283_PIN_PGIO4, &st->gpio_mask);
+ func++;
+ st->ext_fault = false;
+ } else {
+ st->ext_fault = true;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO4_CFG_MASK,
+ FIELD_PREP(LTC4283_PGIO4_CFG_MASK, func));
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int ltc4283_adio_config(struct ltc4283_hwmon *st, struct device *dev,
+ const char *prop, u32 pin)
+{
+ u32 adc_idx;
+ int ret;
+
+ if (!device_property_read_bool(dev, prop))
+ return 0;
+
+ adc_idx = LTC4283_CHAN_ADIO_1 - LTC4283_CHAN_ADI_1 + pin;
+ ret = regmap_clear_bits(st->map, LTC4283_ADC_SELECT(adc_idx),
+ LTC4283_ADC_SELECT_MASK(adc_idx));
+ if (ret)
+ return ret;
+
+ __set_bit(pin, &st->gpio_mask);
+ return 0;
+}
+
+static int ltc4283_pin_config(struct ltc4283_hwmon *st, struct device *dev)
+{
+ int ret;
+
+ ret = ltc4283_pgio_config(st, dev);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_adio_config(st, dev, "adi,gpio-on-adio1", LTC4283_PIN_ADIO1);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_adio_config(st, dev, "adi,gpio-on-adio2", LTC4283_PIN_ADIO2);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_adio_config(st, dev, "adi,gpio-on-adio3", LTC4283_PIN_ADIO3);
+ if (ret)
+ return ret;
+
+ return ltc4283_adio_config(st, dev, "adi,gpio-on-adio4", LTC4283_PIN_ADIO4);
+}
+
+static const char * const ltc4283_oc_fet_retry[] = {
+ "latch-off", "1", "7", "unlimited"
+};
+
+static const u32 ltc4283_fb_factor[] = {
+ 100, 50, 20, 10
+};
+
+static const u32 ltc4283_cooling_dl[] = {
+ 512, 1002, 2005, 4100, 8190, 16400, 32800, 65600
+};
+
+static const u32 ltc4283_fet_bad_delay[] = {
+ 256, 512, 1002, 2005
+};
+
+static int ltc4283_setup(struct ltc4283_hwmon *st, struct device *dev)
+{
+ u32 val;
+ int ret;
+
+ /* The part has an eeprom so let's get the needed defaults from it */
+ ret = ltc4283_get_defaults(st);
+ if (ret)
+ return ret;
+
+ /*
+ * Default to LTC4283_MIN_RSENSE so we can probe without FW properties.
+ */
+ st->rsense = LTC4283_MIN_RSENSE;
+ ret = device_property_read_u32(dev, "adi,rsense-nano-ohms",
+ &st->rsense);
+ if (!ret) {
+ if (st->rsense < LTC4283_MIN_RSENSE || st->rsense > LTC4283_MAX_RSENSE)
+ return dev_err_probe(dev, -EINVAL,
+ "adi,rsense-nano-ohms(%u) too small or too large [%u %u]\n",
+ st->rsense, LTC4283_MIN_RSENSE, LTC4283_MAX_RSENSE);
+ }
+
+ /*
+ * The resolution for rsense is tenths of micro (eg: 62.5 uOhm) which
+ * means we need nano in the bindings. However, to make things easier to
+ * handle (with respect to overflows) we divide it by 100 as we don't
+ * really need the last two digits.
+ */
+ st->rsense /= CENTI;
+
+ ret = device_property_read_u32(dev, "adi,current-limit-sense-microvolt",
+ &st->vsense_max);
+ if (!ret) {
+ u32 reg_val;
+
+ if (!in_range(st->vsense_max, LTC4283_VILIM_MIN_uV,
+ LTC4283_VILIM_RANGE)) {
+ return dev_err_probe(dev, -EINVAL,
+ "adi,current-limit-sense-microvolt (%u) out of range [%u %u]\n",
+ st->vsense_max, LTC4283_VILIM_MIN_uV,
+ LTC4283_VILIM_MAX_uV);
+ }
+
+ st->vsense_max /= MILLI;
+ reg_val = FIELD_PREP(LTC4283_ILIM_MASK,
+ st->vsense_max - LTC4283_VILIM_MIN_uV / MILLI);
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_1,
+ LTC4283_ILIM_MASK, reg_val);
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_parse_array_prop(st, dev, "adi,current-limit-foldback-factor",
+ ltc4283_fb_factor, ARRAY_SIZE(ltc4283_fb_factor));
+ if (ret < 0)
+ return ret;
+ if (ret < ARRAY_SIZE(ltc4283_fb_factor)) {
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_1, LTC4283_FB_MASK,
+ FIELD_PREP(LTC4283_FB_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_parse_array_prop(st, dev, "adi,cooling-delay-ms",
+ ltc4283_cooling_dl, ARRAY_SIZE(ltc4283_cooling_dl));
+ if (ret < 0)
+ return ret;
+ if (ret < ARRAY_SIZE(ltc4283_cooling_dl)) {
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_2, LTC4283_COOLING_DL_MASK,
+ FIELD_PREP(LTC4283_COOLING_DL_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_parse_array_prop(st, dev, "adi,fet-bad-timer-delay-ms",
+ ltc4283_fet_bad_delay, ARRAY_SIZE(ltc4283_fet_bad_delay));
+ if (ret < 0)
+ return ret;
+ if (ret < ARRAY_SIZE(ltc4283_fet_bad_delay)) {
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_2, LTC4283_FTBD_DL_MASK,
+ FIELD_PREP(LTC4283_FTBD_DL_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = ltc4283_set_max_limits(st, dev);
+ if (ret)
+ return ret;
+
+ ret = ltc4283_pin_config(st, dev);
+ if (ret)
+ return ret;
+
+ if (device_property_read_bool(dev, "adi,power-good-reset-on-fet")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_PWRGD_RST_CTRL_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,fet-turn-off-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_FET_BAD_OFF_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,tmr-pull-down-disable")) {
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_THERM_TMR_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,dvdt-inrush-control-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_1,
+ LTC4283_DVDT_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,undervoltage-retry-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_UV_RETRY_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,overvoltage-retry-disable")) {
+ ret = regmap_clear_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_OV_RETRY_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,external-fault-retry-enable")) {
+ if (!st->ext_fault)
+ return dev_err_probe(dev, -EINVAL,
+ "adi,external-fault-retry-enable set but PGIO4 not configured\n");
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_EXT_FAULT_RETRY_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,fault-log-enable")) {
+ ret = regmap_set_bits(st->map, LTC4283_FAULT_LOG_CTRL,
+ LTC4283_FAULT_LOG_EN_MASK);
+ if (ret)
+ return ret;
+ }
+
+ ret = device_property_match_property_string(dev, "adi,overcurrent-retries",
+ ltc4283_oc_fet_retry,
+ ARRAY_SIZE(ltc4283_oc_fet_retry));
+ /* We still want to catch when an invalid string is given. */
+ if (ret < 0 && ret != -EINVAL)
+ return dev_err_probe(dev, ret,
+ "adi,overcurrent-retries invalid value\n");
+ if (ret >= 0) {
+ ret = regmap_update_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_OC_RETRY_MASK,
+ FIELD_PREP(LTC4283_OC_RETRY_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ ret = device_property_match_property_string(dev, "adi,fet-bad-retries",
+ ltc4283_oc_fet_retry,
+ ARRAY_SIZE(ltc4283_oc_fet_retry));
+ if (ret < 0 && ret != -EINVAL)
+ return dev_err_probe(dev, ret,
+ "adi,fet-bad-retries invalid value\n");
+ if (ret >= 0) {
+ ret = regmap_update_bits(st->map, LTC4283_CONTROL_2,
+ LTC4283_FET_BAD_RETRY_MASK,
+ FIELD_PREP(LTC4283_FET_BAD_RETRY_MASK, ret));
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,external-fault-fet-off-enable")) {
+ if (!st->ext_fault)
+ return dev_err_probe(dev, -EINVAL,
+ "adi,external-fault-fet-off-enable set but PGIO4 not configured\n");
+ ret = regmap_set_bits(st->map, LTC4283_CONFIG_3,
+ LTC4283_EXTFLT_TURN_OFF_MASK);
+ if (ret)
+ return ret;
+ }
+
+ if (device_property_read_bool(dev, "adi,vpower-drns-enable")) {
+ u32 chan = LTC4283_CHAN_DRNS - LTC4283_CHAN_ADI_1;
+
+ __clear_bit(LTC4283_CHAN_DRNS, &st->ch_enable_mask);
+ /*
+ * Then, let's by default disable DRNS from ADC2 given that it
+ * is already being monitored by the VPWR channel. One can still
+ * enable it later on if needed.
+ */
+ ret = regmap_clear_bits(st->map, LTC4283_ADC_SELECT(chan),
+ LTC4283_ADC_SELECT_MASK(chan));
+ if (ret)
+ return ret;
+
+ val = 1;
+ } else {
+ val = 0;
+ }
+
+ ret = regmap_update_bits(st->map, LTC4283_CONFIG_3,
+ LTC4283_VPWR_DRNS_MASK,
+ FIELD_PREP(LTC4283_VPWR_DRNS_MASK, val));
+ if (ret)
+ return ret;
+
+ /* Make sure the ADC has 12bit resolution since we're assuming that. */
+ ret = regmap_update_bits(st->map, LTC4283_PGIO_CONFIG_2,
+ LTC4283_ADC_MASK,
+ FIELD_PREP(LTC4283_ADC_MASK, 3));
+ if (ret)
+ return ret;
+
+ /* Energy reads (which are 6 byte block reads) rely on page access */
+ ret = regmap_set_bits(st->map, LTC4283_CONTROL_1, LTC4283_RW_PAGE_MASK);
+ if (ret)
+ return ret;
+
+ /*
+ * Make sure we are integrating power as we only support reporting
+ * consumed energy.
+ */
+ return regmap_clear_bits(st->map, LTC4283_METER_CONTROL,
+ LTC4283_INTEGRATE_I_MASK);
+}
+
+static const struct hwmon_channel_info * const ltc4283_info[] = {
+ HWMON_CHANNEL_INFO(in,
+ HWMON_I_LCRIT_ALARM | HWMON_I_CRIT_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_MAX_ALARM | HWMON_I_RESET_HISTORY |
+ HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_FAULT | HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL,
+ HWMON_I_INPUT | HWMON_I_LOWEST | HWMON_I_HIGHEST |
+ HWMON_I_MAX | HWMON_I_MIN | HWMON_I_MIN_ALARM |
+ HWMON_I_RESET_HISTORY | HWMON_I_MAX_ALARM |
+ HWMON_I_ENABLE | HWMON_I_LABEL),
+ HWMON_CHANNEL_INFO(curr,
+ HWMON_C_INPUT | HWMON_C_LOWEST | HWMON_C_HIGHEST |
+ HWMON_C_MAX | HWMON_C_MIN | HWMON_C_MIN_ALARM |
+ HWMON_C_MAX_ALARM | HWMON_C_CRIT_ALARM |
+ HWMON_C_RESET_HISTORY | HWMON_C_LABEL),
+ HWMON_CHANNEL_INFO(power,
+ HWMON_P_INPUT | HWMON_P_INPUT_LOWEST |
+ HWMON_P_INPUT_HIGHEST | HWMON_P_MAX | HWMON_P_MIN |
+ HWMON_P_MAX_ALARM | HWMON_P_MIN_ALARM |
+ HWMON_P_RESET_HISTORY | HWMON_P_LABEL),
+ HWMON_CHANNEL_INFO(energy,
+ HWMON_E_ENABLE),
+ HWMON_CHANNEL_INFO(energy64,
+ HWMON_E_INPUT),
+ NULL
+};
+
+static const struct hwmon_ops ltc4283_ops = {
+ .read = ltc4283_read,
+ .write = ltc4283_write,
+ .is_visible = ltc4283_is_visible,
+ .read_string = ltc4283_read_labels,
+};
+
+static const struct hwmon_chip_info ltc4283_chip_info = {
+ .ops = <c4283_ops,
+ .info = ltc4283_info,
+};
+
+static int ltc4283_show_fault_log(void *arg, u64 *val, u32 mask)
+{
+ struct ltc4283_hwmon *st = arg;
+ long alarm;
+ int ret;
+
+ ret = ltc4283_read_alarm(st, LTC4283_FAULT_LOG, mask, &alarm);
+ if (ret)
+ return ret;
+
+ *val = alarm;
+
+ return 0;
+}
+
+static int ltc4283_show_in0_lcrit_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_UV_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_in0_lcrit_fault_log,
+ ltc4283_show_in0_lcrit_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_in0_crit_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_OV_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_in0_crit_fault_log,
+ ltc4283_show_in0_crit_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_fet_bad_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_FET_BAD_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_fet_bad_fault_log,
+ ltc4283_show_fet_bad_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_fet_short_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_FET_SHORT_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_fet_short_fault_log,
+ ltc4283_show_fet_short_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_curr1_crit_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_OC_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_curr1_crit_fault_log,
+ ltc4283_show_curr1_crit_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_power1_failed_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_PWR_FAIL_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_power1_failed_fault_log,
+ ltc4283_show_power1_failed_fault_log, NULL, "%llu\n");
+
+static int ltc4283_show_power1_good_input_fault_log(void *arg, u64 *val)
+{
+ return ltc4283_show_fault_log(arg, val, LTC4283_PGI_FAULT_MASK);
+}
+DEFINE_DEBUGFS_ATTRIBUTE(ltc4283_power1_good_input_fault_log,
+ ltc4283_show_power1_good_input_fault_log, NULL, "%llu\n");
+
+static void ltc4283_debugfs_init(struct ltc4283_hwmon *st, struct i2c_client *i2c)
+{
+ debugfs_create_file_unsafe("in0_crit_fault_log", 0400, i2c->debugfs, st,
+ <c4283_in0_crit_fault_log);
+ debugfs_create_file_unsafe("in0_lcrit_fault_log", 0400, i2c->debugfs, st,
+ <c4283_in0_lcrit_fault_log);
+ debugfs_create_file_unsafe("in0_fet_bad_fault_log", 0400, i2c->debugfs, st,
+ <c4283_fet_bad_fault_log);
+ debugfs_create_file_unsafe("in0_fet_short_fault_log", 0400, i2c->debugfs, st,
+ <c4283_fet_short_fault_log);
+ debugfs_create_file_unsafe("curr1_crit_fault_log", 0400, i2c->debugfs, st,
+ <c4283_curr1_crit_fault_log);
+ debugfs_create_file_unsafe("power1_failed_fault_log", 0400, i2c->debugfs, st,
+ <c4283_power1_failed_fault_log);
+ debugfs_create_file_unsafe("power1_good_input_fault_log", 0400, i2c->debugfs,
+ st, <c4283_power1_good_input_fault_log);
+}
+
+static bool ltc4283_is_word_reg(unsigned int reg)
+{
+ return reg >= LTC4283_SENSE && reg <= LTC4283_ADIO34_MAX;
+}
+
+static int ltc4283_reg_read(void *context, unsigned int reg, unsigned int *val)
+{
+ struct i2c_client *client = context;
+ int ret;
+
+ if (ltc4283_is_word_reg(reg))
+ ret = i2c_smbus_read_word_swapped(client, reg);
+ else
+ ret = i2c_smbus_read_byte_data(client, reg);
+
+ if (ret < 0)
+ return ret;
+
+ *val = ret;
+ return 0;
+}
+
+static int ltc4283_reg_write(void *context, unsigned int reg, unsigned int val)
+{
+ struct i2c_client *client = context;
+
+ if (ltc4283_is_word_reg(reg))
+ return i2c_smbus_write_word_swapped(client, reg, val);
+
+ return i2c_smbus_write_byte_data(client, reg, val);
+}
+
+static const struct regmap_bus ltc4283_regmap_bus = {
+ .reg_read = ltc4283_reg_read,
+ .reg_write = ltc4283_reg_write,
+};
+
+static bool ltc4283_writable_reg(struct device *dev, unsigned int reg)
+{
+ switch (reg) {
+ case LTC4283_SYSTEM_STATUS ... LTC4283_FAULT_STATUS:
+ return false;
+ case LTC4283_RESERVED_OC:
+ return false;
+ case LTC4283_RESERVED_86 ... LTC4283_RESERVED_8F:
+ return false;
+ case LTC4283_RESERVED_91 ... LTC4283_RESERVED_A1:
+ return false;
+ case LTC4283_RESERVED_A3:
+ return false;
+ case LTC4283_RESERVED_AC:
+ return false;
+ case LTC4283_POWER_PLAY_MSB ... LTC4283_POWER_PLAY_LSB:
+ return false;
+ case LTC4283_RESERVED_F1 ... LTC4283_RESERVED_FF:
+ return false;
+ default:
+ return true;
+ }
+}
+
+static const struct regmap_config ltc4283_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 16,
+ .max_register = 0xFF,
+ .writeable_reg = ltc4283_writable_reg,
+};
+
+static int ltc4283_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev, *hwmon;
+ struct auxiliary_device *adev;
+ struct ltc4283_hwmon *st;
+ int ret, id;
+
+ st = devm_kzalloc(dev, sizeof(*st), GFP_KERNEL);
+ if (!st)
+ return -ENOMEM;
+
+ if (!i2c_check_functionality(client->adapter,
+ I2C_FUNC_SMBUS_BYTE_DATA |
+ I2C_FUNC_SMBUS_WORD_DATA |
+ I2C_FUNC_SMBUS_READ_I2C_BLOCK))
+ return -EOPNOTSUPP;
+
+ st->client = client;
+ st->map = devm_regmap_init(dev, <c4283_regmap_bus, client,
+ <c4283_regmap_config);
+ if (IS_ERR(st->map))
+ return dev_err_probe(dev, PTR_ERR(st->map),
+ "Failed to create regmap\n");
+
+ ret = ltc4283_setup(st, dev);
+ if (ret)
+ return ret;
+
+ hwmon = devm_hwmon_device_register_with_info(dev, "ltc4283", st,
+ <c4283_chip_info, NULL);
+
+ if (IS_ERR(hwmon))
+ return PTR_ERR(hwmon);
+
+ ltc4283_debugfs_init(st, client);
+
+ if (!st->gpio_mask)
+ return 0;
+
+ id = (client->adapter->nr << 10) | client->addr;
+ adev = __devm_auxiliary_device_create(dev, KBUILD_MODNAME, "gpio",
+ &st->gpio_mask, id);
+ if (!adev)
+ return dev_err_probe(dev, -ENODEV, "Failed to add GPIO device\n");
+
+ return 0;
+}
+
+static const struct of_device_id ltc4283_of_match[] = {
+ { .compatible = "adi,ltc4283" },
+ { }
+};
+
+static const struct i2c_device_id ltc4283_i2c_id[] = {
+ { "ltc4283" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, ltc4283_i2c_id);
+
+static struct i2c_driver ltc4283_driver = {
+ .driver = {
+ .name = "ltc4283",
+ .of_match_table = ltc4283_of_match,
+ },
+ .probe = ltc4283_probe,
+ .id_table = ltc4283_i2c_id,
+};
+module_i2c_driver(ltc4283_driver);
+
+MODULE_AUTHOR("Nuno Sá <nuno.sa@analog.com>");
+MODULE_DESCRIPTION("LTC4283 Hot Swap Controller driver");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related
* [PATCH v12 3/3] gpio: gpio-ltc4283: Add support for the LTC4283 Swap Controller
From: Nuno Sá via B4 Relay @ 2026-04-30 8:52 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski,
Bartosz Golaszewski
In-Reply-To: <20260430-ltc4283-support-v12-0-5dc9901f2567@analog.com>
From: Nuno Sá <nuno.sa@analog.com>
The LTC4283 device has up to 8 pins that can be configured as GPIOs.
Note that PGIO pins are not set as GPIOs by default so if they are
configured to be used as GPIOs we need to make sure to initialize them
to a sane default. They are set as inputs by default.
Acked-by: Bartosz Golaszewski <bartosz.golaszewski@linaro.org>
Reviewed-by: Linus Walleij <linusw@kernel.org>
Signed-off-by: Nuno Sá <nuno.sa@analog.com>
---
MAINTAINERS | 2 +
drivers/gpio/Kconfig | 15 +++
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-ltc4283.c | 218 ++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 236 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index c657ca0a4652..de6bc7828f28 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15240,9 +15240,11 @@ F: drivers/hwmon/ltc4282.c
LTC4283 HARDWARE MONITOR AND GPIO DRIVER
M: Nuno Sá <nuno.sa@analog.com>
+L: linux-gpio@vger.kernel.org
L: linux-hwmon@vger.kernel.org
S: Supported
F: Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
+F: drivers/gpio/gpio-ltc4283.c
F: drivers/hwmon/ltc4283.c
LTC4286 HARDWARE MONITOR DRIVER
diff --git a/drivers/gpio/Kconfig b/drivers/gpio/Kconfig
index 020e51e30317..9b6e0a34eff6 100644
--- a/drivers/gpio/Kconfig
+++ b/drivers/gpio/Kconfig
@@ -1783,6 +1783,21 @@ config GPIO_WM8994
endmenu
+menu "Auxiliary Bus GPIO drivers"
+ depends on AUXILIARY_BUS
+
+config GPIO_LTC4283
+ tristate "Analog Devices LTC4283 GPIO support"
+ depends on SENSORS_LTC4283
+ help
+ If you say yes here you want the GPIO function available in Analog
+ Devices LTC4283 Negative Voltage Hot Swap Controller.
+
+ This driver can also be built as a module. If so, the module will
+ be called gpio-ltc4283.
+
+endmenu
+
menu "PCI GPIO expanders"
depends on PCI
diff --git a/drivers/gpio/Makefile b/drivers/gpio/Makefile
index b267598b517d..6e2367cab94f 100644
--- a/drivers/gpio/Makefile
+++ b/drivers/gpio/Makefile
@@ -100,6 +100,7 @@ obj-$(CONFIG_GPIO_LP873X) += gpio-lp873x.o
obj-$(CONFIG_GPIO_LP87565) += gpio-lp87565.o
obj-$(CONFIG_GPIO_LPC18XX) += gpio-lpc18xx.o
obj-$(CONFIG_GPIO_LPC32XX) += gpio-lpc32xx.o
+obj-$(CONFIG_GPIO_LTC4283) += gpio-ltc4283.o
obj-$(CONFIG_GPIO_MACSMC) += gpio-macsmc.o
obj-$(CONFIG_GPIO_MADERA) += gpio-madera.o
obj-$(CONFIG_GPIO_MAX3191X) += gpio-max3191x.o
diff --git a/drivers/gpio/gpio-ltc4283.c b/drivers/gpio/gpio-ltc4283.c
new file mode 100644
index 000000000000..6609443c5d62
--- /dev/null
+++ b/drivers/gpio/gpio-ltc4283.c
@@ -0,0 +1,218 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Analog Devices LTC4283 GPIO driver
+ *
+ * Copyright 2025 Analog Devices Inc.
+ */
+
+#include <linux/auxiliary_bus.h>
+#include <linux/bitfield.h>
+#include <linux/bitmap.h>
+#include <linux/bits.h>
+#include <linux/device.h>
+#include <linux/gpio/driver.h>
+#include <linux/mod_devicetable.h>
+#include <linux/module.h>
+#include <linux/regmap.h>
+
+#define LTC4283_PINS_MAX 8
+#define LTC4283_PGIOX_START_NR 4
+#define LTC4283_INPUT_STATUS 0x02
+#define LTC4283_PGIO_CONFIG 0x10
+#define LTC4283_PGIO_CFG_MASK(pin) \
+ GENMASK(((pin) - LTC4283_PGIOX_START_NR) * 2 + 1, (((pin) - LTC4283_PGIOX_START_NR) * 2))
+#define LTC4283_PGIO_CONFIG_2 0x11
+
+#define LTC4283_ADIO_CONFIG 0x12
+/* starts at bit 4 */
+#define LTC4283_ADIOX_CONFIG_MASK(pin) BIT((pin) + 4)
+#define LTC4283_PGIO_DIR_IN 3
+#define LTC4283_PGIO_DIR_OUT 2
+
+struct ltc4283_gpio {
+ struct gpio_chip gpio_chip;
+ struct regmap *regmap;
+};
+
+static int ltc4283_pgio_get_direction(const struct ltc4283_gpio *st, unsigned int off)
+{
+ unsigned int val;
+ int ret;
+
+ ret = regmap_read(st->regmap, LTC4283_PGIO_CONFIG, &val);
+ if (ret)
+ return ret;
+
+ val = field_get(LTC4283_PGIO_CFG_MASK(off), val);
+ if (val == LTC4283_PGIO_DIR_IN)
+ return GPIO_LINE_DIRECTION_IN;
+
+ return GPIO_LINE_DIRECTION_OUT;
+}
+
+static int ltc4283_gpio_get_direction(struct gpio_chip *gc, unsigned int off)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+ unsigned int val;
+ int ret;
+
+ if (off >= LTC4283_PGIOX_START_NR)
+ return ltc4283_pgio_get_direction(st, off);
+
+ ret = regmap_read(st->regmap, LTC4283_ADIO_CONFIG, &val);
+ if (ret)
+ return ret;
+
+ if (val & LTC4283_ADIOX_CONFIG_MASK(off))
+ return GPIO_LINE_DIRECTION_IN;
+
+ return GPIO_LINE_DIRECTION_OUT;
+}
+
+static int ltc4283_gpio_direction_set(const struct ltc4283_gpio *st,
+ unsigned int off, bool input)
+{
+ if (off >= LTC4283_PGIOX_START_NR) {
+ unsigned int val = LTC4283_PGIO_DIR_OUT;
+
+ if (input)
+ val = LTC4283_PGIO_DIR_IN;
+
+ val = field_prep(LTC4283_PGIO_CFG_MASK(off), val);
+ return regmap_update_bits(st->regmap, LTC4283_PGIO_CONFIG,
+ LTC4283_PGIO_CFG_MASK(off), val);
+ }
+
+ return regmap_update_bits(st->regmap, LTC4283_ADIO_CONFIG,
+ LTC4283_ADIOX_CONFIG_MASK(off),
+ field_prep(LTC4283_ADIOX_CONFIG_MASK(off), input));
+}
+
+static int __ltc4283_gpio_set_value(const struct ltc4283_gpio *st,
+ unsigned int off, int val)
+{
+ u32 reg = off < LTC4283_PGIOX_START_NR ? LTC4283_ADIO_CONFIG : LTC4283_PGIO_CONFIG_2;
+
+ return regmap_update_bits(st->regmap, reg, BIT(off),
+ field_prep(BIT(off), !!val));
+}
+
+static int ltc4283_gpio_direction_input(struct gpio_chip *gc, unsigned int off)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+
+ return ltc4283_gpio_direction_set(st, off, true);
+}
+
+static int ltc4283_gpio_direction_output(struct gpio_chip *gc, unsigned int off, int val)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+ int ret;
+
+ ret = ltc4283_gpio_direction_set(st, off, false);
+ if (ret)
+ return ret;
+
+ return __ltc4283_gpio_set_value(st, off, val);
+}
+
+static int ltc4283_gpio_get_value(struct gpio_chip *gc, unsigned int off)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+ unsigned int val, reg;
+ int ret, dir;
+
+ dir = ltc4283_gpio_get_direction(gc, off);
+ if (dir < 0)
+ return dir;
+
+ if (dir == GPIO_LINE_DIRECTION_IN) {
+ ret = regmap_read(st->regmap, LTC4283_INPUT_STATUS, &val);
+ if (ret)
+ return ret;
+
+ /* ADIO1 is at bit 3. */
+ if (off < LTC4283_PGIOX_START_NR)
+ return !!(val & BIT(3 - off));
+
+ /* PGIO1 is at bit 7. */
+ return !!(val & BIT(7 - (off - LTC4283_PGIOX_START_NR)));
+ }
+
+ if (off < LTC4283_PGIOX_START_NR)
+ reg = LTC4283_ADIO_CONFIG;
+ else
+ reg = LTC4283_PGIO_CONFIG_2;
+
+ ret = regmap_read(st->regmap, reg, &val);
+ if (ret)
+ return ret;
+
+ return !!(val & BIT(off));
+}
+
+static int ltc4283_gpio_set_value(struct gpio_chip *gc, unsigned int off, int val)
+{
+ struct ltc4283_gpio *st = gpiochip_get_data(gc);
+
+ return __ltc4283_gpio_set_value(st, off, val);
+}
+
+static int ltc4283_init_valid_mask(struct gpio_chip *gc, unsigned long *valid_mask,
+ unsigned int ngpios)
+{
+ unsigned long *mask = dev_get_platdata(gc->parent);
+
+ bitmap_copy(valid_mask, mask, ngpios);
+ return 0;
+}
+
+static int ltc4283_gpio_probe(struct auxiliary_device *adev,
+ const struct auxiliary_device_id *id)
+{
+ struct device *dev = &adev->dev;
+ struct ltc4283_gpio *st;
+ struct gpio_chip *gc;
+
+ st = devm_kzalloc(dev, sizeof(*st), GFP_KERNEL);
+ if (!st)
+ return -ENOMEM;
+
+ st->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!st->regmap)
+ return dev_err_probe(dev, -ENODEV,
+ "Failed to get regmap\n");
+
+ gc = &st->gpio_chip;
+ gc->parent = dev;
+ gc->get_direction = ltc4283_gpio_get_direction;
+ gc->direction_input = ltc4283_gpio_direction_input;
+ gc->direction_output = ltc4283_gpio_direction_output;
+ gc->get = ltc4283_gpio_get_value;
+ gc->set = ltc4283_gpio_set_value;
+ gc->init_valid_mask = ltc4283_init_valid_mask;
+ gc->can_sleep = true;
+
+ gc->base = -1;
+ gc->ngpio = LTC4283_PINS_MAX;
+ gc->label = adev->name;
+ gc->owner = THIS_MODULE;
+
+ return devm_gpiochip_add_data(dev, &st->gpio_chip, st);
+}
+
+static const struct auxiliary_device_id ltc4283_aux_id_table[] = {
+ { "ltc4283.gpio" },
+ { }
+};
+MODULE_DEVICE_TABLE(auxiliary, ltc4283_aux_id_table);
+
+static struct auxiliary_driver ltc4283_gpio_driver = {
+ .probe = ltc4283_gpio_probe,
+ .id_table = ltc4283_aux_id_table,
+};
+module_auxiliary_driver(ltc4283_gpio_driver);
+
+MODULE_AUTHOR("Nuno Sá <nuno.sa@analog.com>");
+MODULE_DESCRIPTION("GPIO LTC4283 Driver");
+MODULE_LICENSE("GPL");
--
2.54.0
^ permalink raw reply related
* [PATCH v12 0/3] hwmon: Add support for the LTC4283 Hot Swap Controller
From: Nuno Sá via B4 Relay @ 2026-04-30 8:52 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski,
Bartosz Golaszewski
This is v8 for the LTC4283 how swap controller.
Similar to the LTC4282 device, we're clearing some fault logs in the
reset_history attributes.
Guenter, for my last email worrying about rsense low values, this is
what I got internally:
"10uOhm at the smallest sense voltage of 15mV would be 1500A and 72kW, which
seems a tad excessive. The highest currents I’ve seen are around 200A, and
the -48V market 4283 serves is generally a lot lower than that. Normal values
are around 200uOhm. I’d say the resolution should be around 1uohm and if a
minimum is needed, 50uOhm is probably safe."
For the resolution, I'm pretty sure I got the tenths of micro
resolution for ltc4282 so I just kept it in here. So, if you don't mind
I would prefer to keep it this way to be safer and changing that now would
require me to change some formulas and I would prefer not to do that at
this stage.
---
Changes in v12:
- Patch 2:
* Add missing in0_reset_history in docs;
* Make sure to pass st->gpio_mask in __devm_auxiliary_device_create().
- Link to v11: https://patch.msgid.link/20260429-ltc4283-support-v11-0-27ccde619dad@analog.com
---
Nuno Sá (3):
dt-bindings: hwmon: Document the LTC4283 Swap Controller
hwmon: ltc4283: Add support for the LTC4283 Swap Controller
gpio: gpio-ltc4283: Add support for the LTC4283 Swap Controller
.../devicetree/bindings/hwmon/adi,ltc4283.yaml | 272 +++
Documentation/hwmon/index.rst | 1 +
Documentation/hwmon/ltc4283.rst | 267 +++
MAINTAINERS | 9 +
drivers/gpio/Kconfig | 15 +
drivers/gpio/Makefile | 1 +
drivers/gpio/gpio-ltc4283.c | 218 +++
drivers/hwmon/Kconfig | 12 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/ltc4283.c | 1795 ++++++++++++++++++++
10 files changed, 2591 insertions(+)
---
base-commit: 992920ad25f27f41521c1bb905d0e1062ecb9e93
change-id: 20260303-ltc4283-support-063f78acc5a4
--
Thanks!
- Nuno Sá
^ permalink raw reply
* [PATCH v12 1/3] dt-bindings: hwmon: Document the LTC4283 Swap Controller
From: Nuno Sá via B4 Relay @ 2026-04-30 8:52 UTC (permalink / raw)
To: linux-gpio, linux-hwmon, devicetree, linux-doc
Cc: Guenter Roeck, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Jonathan Corbet, Linus Walleij, Bartosz Golaszewski
In-Reply-To: <20260430-ltc4283-support-v12-0-5dc9901f2567@analog.com>
From: Nuno Sá <nuno.sa@analog.com>
The LTC4283 is a negative voltage hot swap controller that drives an
external N-channel MOSFET to allow a board to be safely inserted and
removed from a live backplane.
Special note for the "adi,vpower-drns-enable" property. It allows to choose
between the attenuated MOSFET drain voltage or the attenuated input
voltage at the RTNS pin (effectively choosing between input or output
power). This is a system level decision not really intended to change at
runtime and hence is being added as a Firmware property.
Reviewed-by: Rob Herring (Arm) <robh@kernel.org>
Signed-off-by: Nuno Sá <nuno.sa@analog.com>
---
.../devicetree/bindings/hwmon/adi,ltc4283.yaml | 272 +++++++++++++++++++++
MAINTAINERS | 6 +
2 files changed, 278 insertions(+)
diff --git a/Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml b/Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
new file mode 100644
index 000000000000..05e2132ad4d8
--- /dev/null
+++ b/Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
@@ -0,0 +1,272 @@
+# SPDX-License-Identifier: (GPL-2.0 OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/hwmon/adi,ltc4283.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: LTC4283 Negative Voltage Hot Swap Controller
+
+maintainers:
+ - Nuno Sá <nuno.sa@analog.com>
+
+description: |
+ The LTC4283 negative voltage hot swap controller drives an external N-channel
+ MOSFET to allow a board to be safely inserted and removed from a live
+ backplane.
+
+ https://www.analog.com/media/en/technical-documentation/data-sheets/ltc4283.pdf
+
+properties:
+ compatible:
+ enum:
+ - adi,ltc4283
+
+ reg:
+ maxItems: 1
+
+ adi,rsense-nano-ohms:
+ description: Value of the sense resistor.
+
+ adi,current-limit-sense-microvolt:
+ description:
+ The current limit sense voltage of the chip is adjustable between
+ 15mV and 30mV in 1mV steps. This effectively limits the current
+ on the load.
+ minimum: 15000
+ maximum: 30000
+ default: 15000
+
+ adi,current-limit-foldback-factor:
+ description:
+ Specifies the foldback factor for the current limit. The current limit
+ can be reduced (folded back) to one of four preset levels. The value
+ represents the percentage of the current limit sense voltage to use
+ during foldback. A value of 100 means no foldback.
+ $ref: /schemas/types.yaml#/definitions/uint32
+ enum: [10, 20, 50, 100]
+ default: 100
+
+ adi,cooling-delay-ms:
+ description:
+ Cooling time to apply after an overcurrent fault, FET bad or
+ external fault.
+ enum: [512, 1002, 2005, 4100, 8190, 16400, 32800, 65600]
+ default: 512
+
+ adi,fet-bad-timer-delay-ms:
+ description:
+ FET bad timer delay. After a FET bad status condition is detected,
+ this timer is started. If the condition persists for the
+ specified time, the FET is turned off and a fault is logged.
+ enum: [256, 512, 1002, 2005]
+ default: 256
+
+ adi,power-good-reset-on-fet:
+ description:
+ If set, resets the power good status when the MOSFET is turned off.
+ Otherwise, it resets when a low output voltage is detected.
+ type: boolean
+
+ adi,fet-turn-off-disable:
+ description:
+ If set, the MOSFET is not turned off when a FET fault is detected.
+ type: boolean
+
+ adi,tmr-pull-down-disable:
+ description: Disables 2uA pull-down current on the TMR pin.
+ type: boolean
+
+ adi,dvdt-inrush-control-disable:
+ description:
+ Disables dV/dt inrush control during startup. In dV/dt mode, the inrush
+ current is limited by controlling a constant output voltage ramp rate.
+ When disabled, the inrush control mechanism is active current limiting.
+ type: boolean
+
+ adi,fault-log-enable:
+ description:
+ If set, enables logging fault registers and ADC data into EEPROM upon a
+ fault.
+ type: boolean
+
+ adi,vpower-drns-enable:
+ description:
+ If set, enables the attenuated MOSFET drain voltage to be monitored. This
+ effectively means that the MOSFET power is monitored. If not set, the
+ attenuated input voltage (and hence input power) is monitored.
+ type: boolean
+
+ adi,external-fault-fet-off-enable:
+ description: Turns MOSFET off following an external fault.
+ type: boolean
+
+ adi,undervoltage-retry-disable:
+ description: Do not retry to turn on the MOSFET after an undervoltage fault.
+ type: boolean
+
+ adi,overvoltage-retry-disable:
+ description: Do not retry to turn on the MOSFET after an overvoltage fault.
+ type: boolean
+
+ adi,external-fault-retry-enable:
+ description: Retry to turn on the MOSFET after an external fault.
+ type: boolean
+
+ adi,overcurrent-retries:
+ description: Configures auto-retry following an Overcurrent fault.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [latch-off, "1", "7", unlimited]
+ default: latch-off
+
+ adi,fet-bad-retries:
+ description:
+ Configures auto-retry following a FET bad fault and a consequent MOSFET
+ turn off.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [latch-off, "1", "7", unlimited]
+ default: latch-off
+
+ adi,pgio1-func:
+ description: Configures the function of the PGIO1 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_power_good, power_good, gpio]
+ default: inverted_power_good
+
+ adi,pgio2-func:
+ description: Configures the function of the PGIO2 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_power_good, power_good, gpio, active_current_limiting]
+ default: inverted_power_good
+
+ adi,pgio3-func:
+ description: Configures the function of the PGIO3 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_power_good_input, power_good_input, gpio]
+ default: inverted_power_good_input
+
+ adi,pgio4-func:
+ description: Configures the function of the PGIO4 pin.
+ $ref: /schemas/types.yaml#/definitions/string
+ enum: [inverted_external_fault, external_fault, gpio]
+ default: inverted_external_fault
+
+ adi,gpio-on-adio1:
+ description: If set, the ADIO1 pin is used as a GPIO.
+ type: boolean
+
+ adi,gpio-on-adio2:
+ description: If set, the ADIO2 pin is used as a GPIO.
+ type: boolean
+
+ adi,gpio-on-adio3:
+ description: If set, the ADIO3 pin is used as a GPIO.
+ type: boolean
+
+ adi,gpio-on-adio4:
+ description: If set, the ADIO4 pin is used as a GPIO.
+ type: boolean
+
+ gpio-controller: true
+
+ '#gpio-cells':
+ const: 2
+
+dependencies:
+ adi,gpio-on-adio1:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,gpio-on-adio2:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,gpio-on-adio3:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,gpio-on-adio4:
+ - gpio-controller
+ - '#gpio-cells'
+ adi,external-fault-retry-enable:
+ - adi,pgio4-func
+ adi,external-fault-fet-off-enable:
+ - adi,pgio4-func
+
+required:
+ - compatible
+ - reg
+ - adi,rsense-nano-ohms
+
+allOf:
+ - if:
+ properties:
+ adi,pgio1-func:
+ const: gpio
+ required:
+ - adi,pgio1-func
+ then:
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+ - if:
+ properties:
+ adi,pgio2-func:
+ const: gpio
+ required:
+ - adi,pgio2-func
+ then:
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+ - if:
+ properties:
+ adi,pgio3-func:
+ const: gpio
+ required:
+ - adi,pgio3-func
+ then:
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+ - if:
+ properties:
+ adi,pgio4-func:
+ const: gpio
+ required:
+ - adi,pgio4-func
+ then:
+ properties:
+ adi,external-fault-retry-enable: false
+ adi,external-fault-fet-off-enable: false
+ required:
+ - gpio-controller
+ - '#gpio-cells'
+
+additionalProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ swap-controller@15 {
+ compatible = "adi,ltc4283";
+ reg = <0x15>;
+
+ adi,rsense-nano-ohms = <500>;
+ adi,current-limit-sense-microvolt = <25000>;
+ adi,current-limit-foldback-factor = <10>;
+ adi,cooling-delay-ms = <8190>;
+ adi,fet-bad-timer-delay-ms = <512>;
+
+ adi,external-fault-fet-off-enable;
+ adi,pgio4-func = "external_fault";
+
+ adi,gpio-on-adio1;
+ adi,pgio1-func = "gpio";
+ gpio-controller;
+ #gpio-cells = <2>;
+ };
+ };
+...
diff --git a/MAINTAINERS b/MAINTAINERS
index c77860210bd5..76dd9edca7d5 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15238,6 +15238,12 @@ F: Documentation/devicetree/bindings/hwmon/adi,ltc4282.yaml
F: Documentation/hwmon/ltc4282.rst
F: drivers/hwmon/ltc4282.c
+LTC4283 HARDWARE MONITOR AND GPIO DRIVER
+M: Nuno Sá <nuno.sa@analog.com>
+L: linux-hwmon@vger.kernel.org
+S: Supported
+F: Documentation/devicetree/bindings/hwmon/adi,ltc4283.yaml
+
LTC4286 HARDWARE MONITOR DRIVER
M: Delphine CC Chiu <Delphine_CC_Chiu@Wiwynn.com>
L: linux-hwmon@vger.kernel.org
--
2.54.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