* Re: [PATCH v2] docs: fix typo in uniwill-laptop.rst
From: Ilpo Järvinen @ 2026-05-19 11:09 UTC (permalink / raw)
To: Sakurai Shun
Cc: Armin Wolf, Jonathan Corbet, Shuah Khan, platform-driver-x86,
linux-doc, linux-kernel
In-Reply-To: <20260517024148.9642-1-ssh1326@icloud.com>
On Sun, 17 May 2026, Sakurai Shun wrote:
> Replace "benifit" with "benefit".
>
> Signed-off-by: Sakurai Shun <ssh1326@icloud.com>
Thanks for the patch.
When sending an update, you should collect the tags from the earlier
version.
No need to send another version because of it, I've added Armin's
Reviewed-by while applying to review-ilpo-next (it will appear there later
once I push the local changes into the public repo).
--
i.
> ---
> Documentation/wmi/devices/uniwill-laptop.rst | 2 +-
> 1 file changed, 1 insertion(+), 1 deletion(-)
>
> diff --git a/Documentation/wmi/devices/uniwill-laptop.rst b/Documentation/wmi/devices/uniwill-laptop.rst
> index e246bf293..65583b239 100644
> --- a/Documentation/wmi/devices/uniwill-laptop.rst
> +++ b/Documentation/wmi/devices/uniwill-laptop.rst
> @@ -189,7 +189,7 @@ Indexed IO
>
> Indexed IO with IO ports with a granularity of a single byte can be performed using the ``RIOP``
> (read) and ``WIOP`` (write) ACPI control methods. Those ACPI methods are unused because they
> -provide no benifit when compared to the native IO port access functions provided by the kernel.
> +provide no benefit when compared to the native IO port access functions provided by the kernel.
>
> Special thanks go to github user `pobrn` which developed the
> `qc71_laptop <https://github.com/pobrn/qc71_laptop>`_ driver on which this driver is partly based.
>
^ permalink raw reply
* Re: [PATCH] Documentation: KVM: Document guest-visible compatibility expectations
From: Will Deacon @ 2026-05-19 11:11 UTC (permalink / raw)
To: David Woodhouse
Cc: Paolo Bonzini, Marc Zyngier, Jonathan Corbet, Shuah Khan, kvm,
Linux Doc Mailing List, Kernel Mailing List, Linux,
Sean Christopherson, Jim Mattson, Oliver Upton, Joey Gouly,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas,
Raghavendra Rao Ananta, Eric Auger, Kees Cook, Arnd Bergmann,
Nathan Chancellor, linux-arm-kernel, kvmarm, linux-kselftest
In-Reply-To: <3f9d731c3d26b0367600f1069e6425099bc34eac.camel@infradead.org>
On Tue, May 19, 2026 at 11:41:26AM +0100, David Woodhouse wrote:
> On Wed, 2026-05-13 at 18:24 +0200, Paolo Bonzini wrote:
> >
> > > See commit https://git.kernel.org/torvalds/c/49a1a2c70a7f which adds a
> > > new guest-visible feature in revision 3, but allowed userspace to
> > > restore the old behaviour by setting it to revision 2. All my patch
> > > above does, is make it possible to set it to revision 1 as
> > > well. Because https://git.kernel.org/torvalds/c/d53c2c29ae0d previously
> > > changed the behaviour and bumped the default to 2 *without* allowing
> > > userspace to restore the prior behaviour, and we've been carrying a
> > > *revert* of that patch.
> > >
> > > Why would we *not* accept such a patch?
> >
> > Agreed. Even ignoring your revert, there's no reason why any upgrade
> > past 49a1a2c70a7f has to be from after d53c2c29ae0d.
>
> So where do we go from here?
>
> I assume you'll be taking this Documentation patch via the KVM tree?
>
> But what about the actual fix at
> https://lore.kernel.org/all/20260511113558.3325004-2-dwmw2@infradead.org/
>
> This is a simple and unintrusive bug fix to make KVM/arm64 follow the
> "common sense" requirement that the doc patch codifies, apparently
> being rejected with the rather bizarre claim that KVM has no *need* to
> maintain guest-visible compatibility across host kernel changes.
>
> So... what next? Is one of the other KVM/arm64 maintainers going to
> speak up? Paolo would you consider taking the fixes through your tree
> directly?
>
> Does Arm not actually *care* whether AArch64 is considered a stable and
> mature platform for KVM hosting?
Hey, come on. Marc cares more about this stuff than anybody else on the
planet. He's been single-handedly maintaining the tree for the past
couple of releases while Oliver was out and he's on the end of a _lot_
of patches. I'm only cc'd on a fraction of the KVM/arm64 changes and
it's bedlam.
Will
^ permalink raw reply
* Re: [Linaro-mm-sig] Re: [PATCH 4/8] drm/panthor: Add support for protected memory allocation in panthor
From: Boris Brezillon @ 2026-05-19 11:37 UTC (permalink / raw)
To: Maxime Ripard
Cc: Chia-I Wu, Liviu Dudau, Marcin Ślusarz, Ketil Johnsen,
David Airlie, Simona Vetter, Maarten Lankhorst, Thomas Zimmermann,
Jonathan Corbet, Shuah Khan, Sumit Semwal, Benjamin Gaignard,
Brian Starkey, John Stultz, T.J. Mercier, Christian König,
Steven Price, Daniel Almeida, Alice Ryhl, Matthias Brugger,
AngeloGioacchino Del Regno, dri-devel, linux-doc, linux-kernel,
linux-media, linaro-mm-sig, linux-arm-kernel, linux-mediatek,
Florent Tomasin, nd
In-Reply-To: <20260519-loutish-beautiful-trogon-67453f@houat>
On Tue, 19 May 2026 11:52:13 +0200
Maxime Ripard <mripard@kernel.org> wrote:
> Hi Boris,
>
> On Mon, May 18, 2026 at 09:16:50AM +0200, Boris Brezillon wrote:
> > On Wed, 13 May 2026 12:31:32 -0700
> > Chia-I Wu <olvaffe@gmail.com> wrote:
> >
> > > On Tue, May 12, 2026 at 8:39 AM Liviu Dudau <liviu.dudau@arm.com> wrote:
> > > >
> > > > On Tue, May 12, 2026 at 04:11:11PM +0200, Boris Brezillon wrote:
> > > > > On Tue, 12 May 2026 14:47:27 +0100
> > > > > Liviu Dudau <liviu.dudau@arm.com> wrote:
> > > > >
> > > > > > On Thu, May 07, 2026 at 01:53:56PM +0200, Boris Brezillon wrote:
> > > > > > > On Thu, 7 May 2026 11:02:26 +0200
> > > > > > > Marcin Ślusarz <marcin.slusarz@arm.com> wrote:
> > > > > > >
> > > > > > > > On Tue, May 05, 2026 at 06:15:23PM +0200, Boris Brezillon wrote:
> > > > > > > > > > @@ -277,9 +286,21 @@ int panthor_device_init(struct panthor_device *ptdev)
> > > > > > > > > > return ret;
> > > > > > > > > > }
> > > > > > > > > >
> > > > > > > > > > + /* If a protected heap name is specified but not found, defer the probe until created */
> > > > > > > > > > + if (protected_heap_name && strlen(protected_heap_name)) {
> > > > > > > > >
> > > > > > > > > Do we really need this strlen() > 0? Won't dma_heap_find() fail is the
> > > > > > > > > name is "" already?
> > > > > > > >
> > > > > > > > If dma_heap_find() will fail, then the whole probe with fail too.
> > > > > > > > This check prevents that.
> > > > > > >
> > > > > > > Yeah, that's also a questionable design choice. I mean, we can
> > > > > > > currently probe and boot the FW even though we never setup the
> > > > > > > protected FW sections, so why should we defer the probe here? Can't we
> > > > > > > just retry the next time a group with the protected bit is created and
> > > > > > > fail if we can find a protected heap?
> > > > > >
> > > > > > The problem we have with the current firmware is that it does a number of setup steps at "boot"
> > > > > > time only. One of the steps is preparing its internal structures for when it enters protected
> > > > > > mode and it stores them in the buffer passed in at firmware loading. We cannot later run the
> > > > > > process when we have a group with protected mode set.
> > > > >
> > > > > No, but we can force a full/slow reset and have that thing
> > > > > re-initialized, can't we? I mean, that's basically what we do when a
> > > > > fast reset fails: we re-initialize all the sections and reset again, at
> > > > > which point the FW should start from a fresh state, and be able to
> > > > > properly initialize the protected-related stuff if protected sections
> > > > > are populated. Am I missing something?
> > > >
> > > > Right, we can do that. For some reason I keep associating the reset with the
> > > > error handling and not with "normal" operations.
> > > I kind of hope we end up with either
> > >
> > > - panthor knows the exact heap to use and fails with EPROBE_DEFER if
> > > the heap is missing, or
> > > - panthor gets a dma-buf from userspace and does the full reset
> > > - userspace also needs to provide a dma-buf for each protected
> > > group for the suspend buffer
> > >
> > > than something in-between. The latter is more ad-hoc and basically
> > > kicks the issue to the userspace.
> >
> > Indeed, the second option is more ad-hoc, but when you think about it,
> > userspace has to have this knowledge, because it needs to know the
> > dma-heap to use for buffer allocation that cross a device boundary
> > anyway. Think about frames produced by a video decoder, and composited
> > by the GPU into a protected scanout buffer that's passed to the KMS
> > device. Why would the GPU driver be source of truth when it comes to
> > choosing the heap to use to allocate protected buffers for the video
> > decoder or those used for the display?
>
> Just fyi, the trend is to go to devices listing the heaps userspace
> should allocate from
Devices listing the heaps they are able to import buffers from
(with the list being different based on the buffer properties, I
guess) is a good thing. This way the central allocator is in a position
where it can intersect the devices lists and decide which heap to
allocate from based on its buffer sharing knowledge.
> and/or using the heaps internally to allocate their
> buffers,
Yes, that too. For internal buffers (especially the device-wide ones,
like the protected FW sections we were discussing), it makes sense to
leave that up to the driver.
> so that last part is where we're headed, and feels totally
> reasonable to me.
Just to be clear, my main concern right now is not the long term plan,
but how realistic it is to assume we'll have all the DT/dma_heap pieces
in place in a reasonable amount of time. Looking at the current state
of affairs (based on this patchset), it feels like we're a long way
till we can have a robust way of exposing dma_heaps to in-kernel users
(refcounting, lifetime issues, describing allowed heaps, ensuring heaps
truly provide buffers with the expected properties, ...). I'm certainly
not saying these are not valid concerns, but I'd like to have a
temporary solution to support protected rendering in the meantime...
>
> Maxime
^ permalink raw reply
* Re: [PATCH] nios2: remove the architecture
From: Dinh Nguyen @ 2026-05-19 11:40 UTC (permalink / raw)
To: Wolfram Sang, Simon Schuster
Cc: Ethan Nelson-Moore, Peter Zijlstra, Arnd Bergmann, linux-doc,
devicetree, workflows, Linux-Arch, dmaengine, linux-i2c,
linux-iio, Netdev, linux-pci, linux-pwm, linux-hardening,
linux-kbuild, linux-csky@vger.kernel.org, Jonathan Corbet,
Shuah Khan, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Daniel Lezcano, Thomas Gleixner, Alex Shi, Yanteng Si,
Dongliang Mu, Hu Haowen, Kees Cook, Oleg Nesterov, Will Deacon,
Aneesh Kumar K.V (Arm), Andrew Morton, Nicholas Piggin,
Vinod Koul, Frank Li, Dave Penkler, Andi Shyti, Jonathan Cameron,
David Lechner, Nuno Sá, Andy Shevchenko, Andrew Lunn,
David S . Miller, Eric Dumazet, Jakub Kicinski, Paolo Abeni,
Lorenzo Pieralisi, Krzysztof WilczyDski, Andreas Oetken
In-Reply-To: <agxBqd-ubOL2_i-j@shikoro>
Hi Simon,
On 5/19/26 05:55, Wolfram Sang wrote:
> Hi Simon,
>
>>> ... but given this, you might want to get added in MAINTAINERS as
>>> reviewer (or even maintainer) for nios2? Besides that your efforts are
>>> already worth it in my book, it would also ensure you get CCed on
>>> patches like this. Then, you are not depending on people like Arnd
>>> putting you in the loop manually.
>>
>> Sure, I'd be glad to do so, but so far I refrained from it as I was a bit
>> unsure about the netiquette (can I simply do so by self-proclamation? At
>> least the git history seems to suggest so...).
>
> In your case, you can do so, I'd say. You explained your very reasonable
> interest in the architecture and have already shown efforts to keep it,
> as we can see from the git history. The final call will be done by Dinh
> Nguyen obviously with whom you probably need to sort out details. But I
> can't imagine your offer for help will be rejected, quite the contrary.
>
I 100% support adding you as a maintainer. Please send a patch.
Thanks,
Dinh
^ permalink raw reply
* Re: [PATCH v2 0/6] io_uring/zcrx: add CQE based notifications and stats reporting
From: Pavel Begunkov @ 2026-05-19 11:43 UTC (permalink / raw)
To: Clément Léger, io-uring, Jens Axboe
Cc: linux-doc, linux-kernel, linux-kselftest, netdev, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Jonathan Corbet, Shuah Khan, Vishwanath Seshagiri
In-Reply-To: <20260518153532.2835502-1-cleger@meta.com>
On 5/18/26 16:35, Clément Léger wrote:
> The zcrx path can encounter various conditions that lead to internal
> fallbacks or errors. These errors can have a large impact on performance
> and functionality but are not yet not being reported to the user which
> is then unable to take action.>
> This series addresses this problem by adding a new notification system
> paired with a statistics structure. The notification system currently
> report out of buffer and packets that fallback to copy. The statistics
> structure report the number and total size of packets that were copied
> rather than received via the zero-copy path.
>
> The out of buffer notification allows the user to actually adjust the
> buffer sizing when registering zcrx support for the ifq. Some future
> work could allow the user to add more memory on the fly to the pool so
> the page allocator doesn't run out of memory.
Looks good, I'm going to take the first 4 and send out with other
zcrx patches.
--
Pavel Begunkov
^ permalink raw reply
* Re: [PATCH] Documentation: KVM: Document guest-visible compatibility expectations
From: David Woodhouse @ 2026-05-19 11:44 UTC (permalink / raw)
To: Will Deacon
Cc: Paolo Bonzini, Marc Zyngier, Jonathan Corbet, Shuah Khan, kvm,
Linux Doc Mailing List, Kernel Mailing List, Linux,
Sean Christopherson, Jim Mattson, Oliver Upton, Joey Gouly,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas,
Raghavendra Rao Ananta, Eric Auger, Kees Cook, Arnd Bergmann,
Nathan Chancellor, linux-arm-kernel, kvmarm, linux-kselftest
In-Reply-To: <agxFbniU_6eQ98t2@willie-the-truck>
[-- Attachment #1: Type: text/plain, Size: 3074 bytes --]
On Tue, 2026-05-19 at 12:11 +0100, Will Deacon wrote:
> On Tue, May 19, 2026 at 11:41:26AM +0100, David Woodhouse wrote:
> > On Wed, 2026-05-13 at 18:24 +0200, Paolo Bonzini wrote:
> > >
> > > > See commit https://git.kernel.org/torvalds/c/49a1a2c70a7f which adds a
> > > > new guest-visible feature in revision 3, but allowed userspace to
> > > > restore the old behaviour by setting it to revision 2. All my patch
> > > > above does, is make it possible to set it to revision 1 as
> > > > well. Because https://git.kernel.org/torvalds/c/d53c2c29ae0d previously
> > > > changed the behaviour and bumped the default to 2 *without* allowing
> > > > userspace to restore the prior behaviour, and we've been carrying a
> > > > *revert* of that patch.
> > > >
> > > > Why would we *not* accept such a patch?
> > >
> > > Agreed. Even ignoring your revert, there's no reason why any upgrade
> > > past 49a1a2c70a7f has to be from after d53c2c29ae0d.
> >
> > So where do we go from here?
> >
> > I assume you'll be taking this Documentation patch via the KVM tree?
> >
> > But what about the actual fix at
> > https://lore.kernel.org/all/20260511113558.3325004-2-dwmw2@infradead.org/
> >
> > This is a simple and unintrusive bug fix to make KVM/arm64 follow the
> > "common sense" requirement that the doc patch codifies, apparently
> > being rejected with the rather bizarre claim that KVM has no *need* to
> > maintain guest-visible compatibility across host kernel changes.
> >
> > So... what next? Is one of the other KVM/arm64 maintainers going to
> > speak up? Paolo would you consider taking the fixes through your tree
> > directly?
> >
> > Does Arm not actually *care* whether AArch64 is considered a stable and
> > mature platform for KVM hosting?
>
> Hey, come on. Marc cares more about this stuff than anybody else on the
> planet. He's been single-handedly maintaining the tree for the past
> couple of releases while Oliver was out and he's on the end of a _lot_
> of patches. I'm only cc'd on a fraction of the KVM/arm64 changes and
> it's bedlam.
I certainly wouldn't disagree with any of that. The depth of knowledge
and the amount of energy that Marc displays through this work is
impressive, and I'm sure we all have an enormous amount of respect for
it, and for him. I know I do.
Nevertheless, the specific technical decision to reject the simple bug
fix linked above is dead wrong.
Because the principle under which it was rejected — the idea that KVM
has no responsibility to maintain compatibility of guest-visible
behaviour from one kernel version to the next — is also dead wrong.
If KVM on arm64 doesn't aspire to maintain guest compatibility across
host kernel changes — regardless of whether the previous kernel's
behaviour was "blessed" by the architecture specification or not — then
it does not meet the expectation that we have of KVM implementations in
the Linux kernel.
Or indeed the standards that we've held for Linux kernel ABIs for the
last 35 years.
[-- Attachment #2: smime.p7s --]
[-- Type: application/pkcs7-signature, Size: 5069 bytes --]
^ permalink raw reply
* [PATCH v2 0/2] selftests/mm: separate GUP microbenchmarking from functional testing
From: Sarthak Sharma @ 2026-05-19 12:05 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Jonathan Corbet, Jason Gunthorpe, John Hubbard, Peter Xu,
Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, linux-mm,
linux-kselftest, linux-kernel, linux-doc, Sarthak Sharma
gup_test.c currently serves two distinct purposes: microbenchmarking
(GUP_FAST_BENCHMARK, PIN_FAST_BENCHMARK, PIN_LONGTERM_BENCHMARK) and
functional correctness testing (GUP_BASIC_TEST, PIN_BASIC_TEST,
DUMP_USER_PAGES_TEST). Mixing these in a single binary means functional
tests cannot be run or reported individually, and run_vmtests.sh must
invoke the binary multiple times with different flag combinations to
cover all configurations. This patch series separates the two concerns:
tools/mm/gup_bench for benchmarking and tools/testing/selftests/mm/gup_test
for functional testing.
Patch 1 adds tools/mm/gup_bench.c, a standalone microbenchmark for
GUP_FAST, PIN_FAST and PIN_LONGTERM via the CONFIG_GUP_TEST debugfs
interface. It runs the same matrix of configurations as the old
run_gup_matrix() shell function (all three commands, read/write,
private/shared, four page counts, THP on/off, hugetlb), but as a
standalone C program under tools/mm with no dependency on kselftest.
Patch 2 rewrites gup_test.c as a kselftest harness-based selftest. It
covers all five GUP kernel functions (get_user_pages, get_user_pages_fast,
pin_user_pages, pin_user_pages_fast, pin_user_pages with FOLL_LONGTERM)
plus DUMP_USER_PAGES_TEST, across 12 mapping configurations (THP on,
THP off and hugetlb, each across private/shared and read/write variants)
and four batch sizes (1, 512, 123, all pages). Results are reported as
standard TAP output with no command-line arguments required.
---
These patches apply on top of mm/mm-new.
Changes in v2:
- Address v1 feedback from Sashiko
- Add fast and longterm GUP/PUP coverage
- Sweep nr_pages_per_call over 1, 512, 123, and all pages
- Call madvise(MADV_NOHUGEPAGE) in non-THP variants
- Use 256 MB for hugetlb fixtures
- Use hugetlb_restore_settings() in FIXTURE_TEARDOWN instead of atexit()
- Add TH_LOG to report nr_pages_per_call for each iteration
- Update Documentation/core-api/pin_user_pages.rst unit testing section
Sarthak Sharma (2):
tools/mm: add a standalone GUP microbenchmark
selftests/mm: rewrite gup_test as a standalone harness-based selftest
Documentation/core-api/pin_user_pages.rst | 12 +-
MAINTAINERS | 1 +
tools/mm/.gitignore | 2 +
tools/mm/Makefile | 6 +-
tools/mm/gup_bench.c | 491 ++++++++++++++++++++
tools/testing/selftests/mm/gup_test.c | 536 +++++++++++++---------
tools/testing/selftests/mm/run_vmtests.sh | 37 +-
7 files changed, 822 insertions(+), 263 deletions(-)
create mode 100644 tools/mm/gup_bench.c
base-commit: 2c3f468717231305523ddcd94d91c0d5e4a72419
--
2.39.5
^ permalink raw reply
* [PATCH v2 1/2] tools/mm: add a standalone GUP microbenchmark
From: Sarthak Sharma @ 2026-05-19 12:05 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Jonathan Corbet, Jason Gunthorpe, John Hubbard, Peter Xu,
Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, linux-mm,
linux-kselftest, linux-kernel, linux-doc, Sarthak Sharma
In-Reply-To: <20260519120506.184512-1-sarthak.sharma@arm.com>
Add a command-line tool for benchmarking get_user_pages fast-path
(GUP_FAST), pin_user_pages fast-path (PIN_FAST), and pin_user_pages
longterm (PIN_LONGTERM) via the CONFIG_GUP_TEST debugfs interface.
When invoked without arguments, gup_bench runs the same matrix of
configurations as run_gup_matrix() in run_vmtests.sh: all three GUP
commands across read/write, private/shared mappings, and a range of
page counts, with THP on/off for regular mappings and hugetlb for huge
page mappings.
This tool is a mix of reused and new logic. The mapping/setup path comes
from selftests/mm/gup_test.c, while the default benchmark matrix matches
run_gup_matrix() in run_vmtests.sh. The standalone CLI and tools/mm
integration are added here so tools/mm does not depend on kselftest.
Add gup_bench to BUILD_TARGETS and INSTALL_TARGETS in tools/mm/Makefile,
and ignore the resulting binary in tools/mm/.gitignore. While here, also
add the missing thp_swap_allocator_test entry to .gitignore.
Add tools/mm/gup_bench.c to the GUP entry in MAINTAINERS.
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Sarthak Sharma <sarthak.sharma@arm.com>
---
MAINTAINERS | 1 +
tools/mm/.gitignore | 2 +
tools/mm/Makefile | 6 +-
tools/mm/gup_bench.c | 491 +++++++++++++++++++++++++++++++++++++++++++
4 files changed, 497 insertions(+), 3 deletions(-)
create mode 100644 tools/mm/gup_bench.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 98d0a7a1c689..c91165b9280e 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -16830,6 +16830,7 @@ T: git git://git.kernel.org/pub/scm/linux/kernel/git/akpm/mm
F: mm/gup.c
F: mm/gup_test.c
F: mm/gup_test.h
+F: tools/mm/gup_bench.c
F: tools/testing/selftests/mm/gup_longterm.c
F: tools/testing/selftests/mm/gup_test.c
diff --git a/tools/mm/.gitignore b/tools/mm/.gitignore
index 922879f93fc8..154d740be02e 100644
--- a/tools/mm/.gitignore
+++ b/tools/mm/.gitignore
@@ -2,3 +2,5 @@
slabinfo
page-types
page_owner_sort
+thp_swap_allocator_test
+gup_bench
diff --git a/tools/mm/Makefile b/tools/mm/Makefile
index f5725b5c23aa..8e4db797a17a 100644
--- a/tools/mm/Makefile
+++ b/tools/mm/Makefile
@@ -3,13 +3,13 @@
#
include ../scripts/Makefile.include
-BUILD_TARGETS=page-types slabinfo page_owner_sort thp_swap_allocator_test
+BUILD_TARGETS=page-types slabinfo page_owner_sort thp_swap_allocator_test gup_bench
INSTALL_TARGETS = $(BUILD_TARGETS) thpmaps
LIB_DIR = ../lib/api
LIBS = $(LIB_DIR)/libapi.a
-CFLAGS += -Wall -Wextra -I../lib/ -pthread
+CFLAGS += -Wall -Wextra -I../lib/ -I../.. -pthread
LDFLAGS += $(LIBS) -pthread
all: $(BUILD_TARGETS)
@@ -23,7 +23,7 @@ $(LIBS):
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
clean:
- $(RM) page-types slabinfo page_owner_sort thp_swap_allocator_test
+ $(RM) page-types slabinfo page_owner_sort thp_swap_allocator_test gup_bench
make -C $(LIB_DIR) clean
sbindir ?= /usr/sbin
diff --git a/tools/mm/gup_bench.c b/tools/mm/gup_bench.c
new file mode 100644
index 000000000000..2806ee0d7453
--- /dev/null
+++ b/tools/mm/gup_bench.c
@@ -0,0 +1,491 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Microbenchmark for get_user_pages (GUP) kernel interfaces.
+ *
+ * Exercises GUP_FAST_BENCHMARK, PIN_FAST_BENCHMARK, and
+ * PIN_LONGTERM_BENCHMARK via the CONFIG_GUP_TEST debugfs interface.
+ *
+ * Example use:
+ * # Run the full matrix (all commands, access modes, page counts):
+ * ./gup_bench
+ *
+ * # Single run: pin_user_pages_fast, 512 pages, write access, hugetlb:
+ * ./gup_bench -a -n 512 -w -H
+ *
+ * Requires CONFIG_GUP_TEST=y and debugfs mounted at /sys/kernel/debug.
+ * Must be run as root.
+ */
+
+#define __SANE_USERSPACE_TYPES__ // Use ll64
+#include <fcntl.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <pthread.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <stdatomic.h>
+#include <limits.h>
+#include <mm/gup_test.h>
+#include <string.h>
+
+#define MB (1UL << 20)
+
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+#endif
+
+/* Just the flags we need, copied from the kernel internals. */
+#define FOLL_WRITE 0x01 /* check pte is writable */
+
+#define GUP_TEST_FILE "/sys/kernel/debug/gup_test"
+
+/*
+ * Local HugeTLB setup helpers for gup_bench.
+ *
+ * These helpers were copied from tools/testing/selftests/mm/ and adjusted to
+ * remove the ksft formatting. Keep this copy local so tools/mm does not
+ * depend on ksft output behavior.
+ */
+
+static unsigned int psize(void)
+{
+ static unsigned int __page_size;
+
+ if (!__page_size)
+ __page_size = sysconf(_SC_PAGESIZE);
+ return __page_size;
+}
+
+static unsigned long default_huge_page_size(void)
+{
+ FILE *f = fopen("/proc/meminfo", "r");
+ unsigned long hpage_size = 0;
+ char buf[256];
+
+ if (!f)
+ return 0;
+ while (fgets(buf, sizeof(buf), f)) {
+ if (sscanf(buf, "Hugepagesize: %lu kB", &hpage_size) == 1)
+ break;
+ }
+ fclose(f);
+ hpage_size <<= 10;
+ return hpage_size;
+}
+
+static void hugetlb_sysfs_path(char *buf, size_t buflen,
+ unsigned long size, const char *attr)
+{
+ snprintf(buf, buflen, "/sys/kernel/mm/hugepages/hugepages-%lukB/%s",
+ size / 1024, attr);
+}
+
+static unsigned long hugetlb_read_num(const char *path)
+{
+ char buf[32];
+ FILE *f = fopen(path, "r");
+ unsigned long val = 0;
+
+ if (!f)
+ return 0;
+ if (fgets(buf, sizeof(buf), f))
+ val = strtoul(buf, NULL, 10);
+ fclose(f);
+ return val;
+}
+
+static void hugetlb_write_num(const char *path, unsigned long num)
+{
+ FILE *f = fopen(path, "w");
+
+ if (!f)
+ return;
+ fprintf(f, "%lu\n", num);
+ fclose(f);
+}
+
+static unsigned long hugetlb_nr_pages(unsigned long size)
+{
+ char path[PATH_MAX];
+
+ hugetlb_sysfs_path(path, sizeof(path), size, "nr_hugepages");
+ return hugetlb_read_num(path);
+}
+
+static void hugetlb_set_nr_pages(unsigned long size, unsigned long nr)
+{
+ char path[PATH_MAX];
+
+ hugetlb_sysfs_path(path, sizeof(path), size, "nr_hugepages");
+ hugetlb_write_num(path, nr);
+}
+
+static unsigned long hugetlb_free_pages(unsigned long size)
+{
+ char path[PATH_MAX];
+
+ hugetlb_sysfs_path(path, sizeof(path), size, "free_hugepages");
+ return hugetlb_read_num(path);
+}
+
+/* Saved pool size to restore on exit */
+static unsigned long hugetlb_saved_nr;
+static unsigned long hugetlb_saved_size;
+
+static void hugetlb_restore_atexit(void)
+{
+ if (hugetlb_saved_size)
+ hugetlb_set_nr_pages(hugetlb_saved_size, hugetlb_saved_nr);
+}
+
+static bool __hugetlb_setup(unsigned long size, unsigned long nr)
+{
+ unsigned long free = hugetlb_free_pages(size);
+ unsigned long total = hugetlb_nr_pages(size);
+
+ if (free >= nr)
+ return true;
+
+ hugetlb_set_nr_pages(size, total + (nr - free));
+
+ return hugetlb_free_pages(size) >= nr;
+}
+
+static bool hugetlb_setup_default(unsigned long nr)
+{
+ unsigned long hsize = default_huge_page_size();
+
+ if (!hsize)
+ return false;
+
+ /* Save current pool so we can restore it on exit (only on first call) */
+ if (!hugetlb_saved_size) {
+ hugetlb_saved_size = hsize;
+ hugetlb_saved_nr = hugetlb_nr_pages(hsize);
+ atexit(hugetlb_restore_atexit);
+ }
+
+ return __hugetlb_setup(hsize, nr);
+}
+
+static unsigned long cmd;
+static const char *bench_label;
+static int gup_fd, repeats = 1;
+static unsigned long size = 128 * MB;
+static atomic_int bench_error;
+/* Serialize prints */
+static pthread_mutex_t print_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+static const unsigned long bench_cmds[] = {
+ GUP_FAST_BENCHMARK,
+ PIN_FAST_BENCHMARK,
+ PIN_LONGTERM_BENCHMARK,
+};
+static const int bench_thp_modes[] = { 1, 0 }; /* on, off */
+static const int bench_nr_pages_list[] = { 1, 512, 123, -1 };
+
+static const char *cmd_to_str(unsigned long cmd)
+{
+ switch (cmd) {
+ case GUP_FAST_BENCHMARK:
+ return "GUP_FAST_BENCHMARK";
+ case PIN_FAST_BENCHMARK:
+ return "PIN_FAST_BENCHMARK";
+ case PIN_LONGTERM_BENCHMARK:
+ return "PIN_LONGTERM_BENCHMARK";
+ }
+ return "Unknown command";
+}
+
+struct bench_run {
+ unsigned long cmd;
+ int thp; /* -1: default, 0: off, 1: on */
+ bool hugetlb;
+ bool write;
+ bool shared;
+ int nr_pages; /* -1 means all pages (size / psize()) */
+ unsigned long size;
+ char *file;
+ int nthreads;
+ unsigned int gup_flags;
+};
+
+void *gup_thread(void *data)
+{
+ struct gup_test gup = *(struct gup_test *)data;
+ int i, status;
+
+ for (i = 0; i < repeats; i++) {
+ gup.size = size;
+ status = ioctl(gup_fd, cmd, &gup);
+ if (status) {
+ bench_error = 1;
+ break;
+ }
+
+ pthread_mutex_lock(&print_mutex);
+ printf("%s time: get:%lld put:%lld us",
+ bench_label, gup.get_delta_usec,
+ gup.put_delta_usec);
+ if (gup.size != size)
+ printf(", truncated (size: %lld)", gup.size);
+ printf("\n");
+ pthread_mutex_unlock(&print_mutex);
+ }
+
+ return NULL;
+}
+
+static int run_bench(struct bench_run *run)
+{
+ struct gup_test gup = { 0 };
+ int zero_fd, i, ret, started_threads = 0;
+ int flags = MAP_PRIVATE;
+ pthread_t *tid;
+ char label[128];
+ char *p;
+
+ /* Set globals consumed by gup_thread */
+ cmd = run->cmd;
+ size = run->size;
+ bench_error = 0;
+
+ if (run->hugetlb) {
+ unsigned long hp_size = default_huge_page_size();
+
+ if (!hp_size) {
+ fprintf(stderr, "Could not determine huge page size\n");
+ return 1;
+ }
+ size = (size + hp_size - 1) & ~(hp_size - 1);
+ if (!hugetlb_setup_default(size / hp_size)) {
+ fprintf(stderr, "Not enough huge pages\n");
+ return 1;
+ }
+ flags |= (MAP_HUGETLB | MAP_ANONYMOUS);
+ }
+
+ if (run->shared) {
+ flags &= ~MAP_PRIVATE;
+ flags |= MAP_SHARED;
+ }
+
+ gup.nr_pages_per_call = run->nr_pages < 0 ? size / psize() :
+ (unsigned long)run->nr_pages;
+
+ gup.gup_flags = run->gup_flags;
+ if (run->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ snprintf(label, sizeof(label), "%s (nr_pages=%-4u %s %s %s %s)",
+ cmd_to_str(run->cmd),
+ gup.nr_pages_per_call,
+ run->write ? "write" : "read",
+ run->shared ? "shared" : "private",
+ run->hugetlb ? "hugetlb=on" : "hugetlb=off",
+ run->hugetlb ? "thp=off" :
+ (run->thp == 1 ? "thp=on" :
+ (run->thp == 0 ? "thp=off" : "thp=default")));
+ bench_label = label;
+
+ zero_fd = open(run->file, O_RDWR);
+ if (zero_fd < 0) {
+ fprintf(stderr, "Unable to open %s: %s\n", run->file, strerror(errno));
+ return 1;
+ }
+
+ p = mmap(NULL, size, PROT_READ | PROT_WRITE, flags, zero_fd, 0);
+ close(zero_fd);
+ if (p == MAP_FAILED) {
+ fprintf(stderr, "mmap: %s\n", strerror(errno));
+ return 1;
+ }
+ gup.addr = (unsigned long)p;
+
+ if (run->thp == 1)
+ madvise(p, size, MADV_HUGEPAGE);
+ else if (run->thp == 0)
+ madvise(p, size, MADV_NOHUGEPAGE);
+
+ /* Fault them in here, from user space. */
+ for (; (unsigned long)p < gup.addr + size; p += psize())
+ p[0] = 0;
+
+ tid = malloc(sizeof(pthread_t) * run->nthreads);
+ assert(tid);
+ for (i = 0; i < run->nthreads; i++) {
+ ret = pthread_create(&tid[i], NULL, gup_thread, &gup);
+ if (ret) {
+ fprintf(stderr, "pthread_create failed: %s\n", strerror(ret));
+ bench_error = 1;
+ break;
+ }
+ started_threads++;
+ }
+ for (i = 0; i < started_threads; i++) {
+ ret = pthread_join(tid[i], NULL);
+ if (ret) {
+ fprintf(stderr, "pthread_join failed: %s\n", strerror(ret));
+ bench_error = 1;
+ }
+ }
+
+ free(tid);
+ munmap((void *)gup.addr, size);
+
+ return bench_error ? 1 : 0;
+}
+
+static int run_matrix(void)
+{
+ unsigned int c, t, w, s, n;
+ int ret = 0;
+
+ for (c = 0; c < ARRAY_SIZE(bench_cmds); c++) {
+ for (w = 0; w <= 1; w++) {
+ for (s = 0; s <= 1; s++) {
+ for (t = 0; t < ARRAY_SIZE(bench_thp_modes); t++) {
+ for (n = 0; n < ARRAY_SIZE(bench_nr_pages_list); n++) {
+ struct bench_run run = {
+ .cmd = bench_cmds[c],
+ .thp = bench_thp_modes[t],
+ .hugetlb = false,
+ .write = w,
+ .shared = s,
+ .nr_pages = bench_nr_pages_list[n],
+ .size = 128 * MB,
+ .file = "/dev/zero",
+ .nthreads = 1,
+ };
+ ret |= run_bench(&run);
+ }
+ }
+ /* hugetlb: 256M to match run_gup_matrix() in run_vmtests.sh */
+ for (n = 0; n < ARRAY_SIZE(bench_nr_pages_list); n++) {
+ struct bench_run run = {
+ .cmd = bench_cmds[c],
+ .thp = -1,
+ .hugetlb = true,
+ .write = w,
+ .shared = s,
+ .nr_pages = bench_nr_pages_list[n],
+ .size = 256 * MB,
+ .file = "/dev/zero",
+ .nthreads = 1,
+ };
+ ret |= run_bench(&run);
+ }
+ }
+ }
+ }
+ return ret;
+}
+
+int main(int argc, char **argv)
+{
+ struct bench_run run = {
+ .cmd = GUP_FAST_BENCHMARK,
+ .thp = -1,
+ .hugetlb = false,
+ .write = true,
+ .shared = false,
+ .nr_pages = 1,
+ .size = 128 * MB,
+ .file = "/dev/zero",
+ .nthreads = 1,
+ };
+ int opt, result;
+
+ while ((opt = getopt(argc, argv, "m:r:n:F:f:aj:tTLuwWSH")) != -1) {
+ switch (opt) {
+
+ /* Command selection */
+ case 'u':
+ run.cmd = GUP_FAST_BENCHMARK;
+ break;
+ case 'a':
+ run.cmd = PIN_FAST_BENCHMARK;
+ break;
+ case 'L':
+ run.cmd = PIN_LONGTERM_BENCHMARK;
+ break;
+
+ /* Memory type */
+ case 'H':
+ run.hugetlb = true;
+ break;
+ case 't':
+ run.thp = 1;
+ break;
+ case 'T':
+ run.thp = 0;
+ break;
+
+ /* Access mode */
+ case 'w':
+ run.write = true;
+ break;
+ case 'W':
+ run.write = false;
+ break;
+ case 'S':
+ run.shared = true;
+ break;
+
+ /* Mapping */
+ case 'f':
+ run.file = optarg;
+ break;
+
+ /* Sizing and iteration */
+ case 'm':
+ run.size = atoi(optarg) * MB;
+ break;
+ case 'n':
+ run.nr_pages = atoi(optarg);
+ break;
+ case 'r':
+ repeats = atoi(optarg);
+ break;
+ case 'j':
+ run.nthreads = atoi(optarg);
+ break;
+
+ /* Advanced */
+ case 'F':
+ /* strtol, so you can pass flags in hex form */
+ run.gup_flags = strtol(optarg, 0, 0);
+ break;
+
+ default:
+ fprintf(stderr, "Wrong argument\n");
+ exit(1);
+ }
+ }
+
+ gup_fd = open(GUP_TEST_FILE, O_RDWR);
+ if (gup_fd == -1) {
+ if (errno == EACCES) {
+ fprintf(stderr, "Please run as root\n");
+ } else if (errno == ENOENT) {
+ if (opendir("/sys/kernel/debug") == NULL)
+ fprintf(stderr, "Mount debugfs at /sys/kernel/debug\n");
+ else
+ fprintf(stderr, "Check CONFIG_GUP_TEST in kernel config\n");
+ } else {
+ fprintf(stderr, "Failed to open %s: %s\n", GUP_TEST_FILE,
+ strerror(errno));
+ }
+ exit(1);
+ }
+
+ result = (argc == 1) ? run_matrix() : run_bench(&run);
+ close(gup_fd);
+ return result;
+}
--
2.39.5
^ permalink raw reply related
* [PATCH v2 2/2] selftests/mm: rewrite gup_test as a standalone harness-based selftest
From: Sarthak Sharma @ 2026-05-19 12:05 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Jonathan Corbet, Jason Gunthorpe, John Hubbard, Peter Xu,
Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, linux-mm,
linux-kselftest, linux-kernel, linux-doc, Sarthak Sharma
In-Reply-To: <20260519120506.184512-1-sarthak.sharma@arm.com>
Rewrite gup_test.c using kselftest_harness.h. The new test covers 12
mapping configurations: THP on, THP off and hugetlb, each across
private/shared and read/write variants. It runs seven test cases per
variant: get_user_pages, get_user_pages_fast, pin_user_pages,
pin_user_pages_fast, pin_user_pages_longterm, and DUMP_USER_PAGES_TEST
via both get and pin.
Each test case sweeps four nr_pages_per_call values: 1, 512, 123, and
all pages. This preserves the old run_gup_matrix() sweep: 12 mapping
combinations x 5 GUP/PUP operations x 4 batch sizes = 240 ioctl sweeps.
It also expands DUMP_USER_PAGES_TEST coverage from one standalone
invocation to 12 variants x 2 dump modes x 4 batch sizes = 96
additional sweeps, for 336 total ioctl sweeps and 84 TAP-reported cases.
On a Radxa Orion O6 board, ./gup_test completes in 5.07s on average
over 10 runs (range: 4.94s - 5.18s).
Update run_vmtests.sh: remove run_gup_matrix() and the multiple flagged
invocations of gup_test, replacing them with a single unconditional
invocation. Benchmark functionality is handled by tools/mm/gup_bench
introduced in the previous patch.
Update Documentation/core-api/pin_user_pages.rst to reflect the new
harness-based gup_test interface rather than command-line flag
invocations.
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Sarthak Sharma <sarthak.sharma@arm.com>
---
Documentation/core-api/pin_user_pages.rst | 12 +-
tools/testing/selftests/mm/gup_test.c | 536 +++++++++++++---------
tools/testing/selftests/mm/run_vmtests.sh | 37 +-
3 files changed, 325 insertions(+), 260 deletions(-)
diff --git a/Documentation/core-api/pin_user_pages.rst b/Documentation/core-api/pin_user_pages.rst
index c16ca163b55e..ea722adf22cc 100644
--- a/Documentation/core-api/pin_user_pages.rst
+++ b/Documentation/core-api/pin_user_pages.rst
@@ -230,10 +230,16 @@ This file::
tools/testing/selftests/mm/gup_test.c
-has the following new calls to exercise the new pin*() wrapper functions:
+contains the following test cases to exercise pin_user_pages*():
-* PIN_FAST_BENCHMARK (./gup_test -a)
-* PIN_BASIC_TEST (./gup_test -b)
+* pin_user_pages via PIN_BASIC_TEST
+* pin_user_pages_fast via PIN_FAST_BENCHMARK
+* pin_user_pages_longterm via PIN_LONGTERM_BENCHMARK
+
+Run with::
+
+ make -C tools/testing/selftests/mm
+ ./tools/testing/selftests/mm/gup_test
You can monitor how many total dma-pinned pages have been acquired and released
since the system was booted, via two new /proc/vmstat entries: ::
diff --git a/tools/testing/selftests/mm/gup_test.c b/tools/testing/selftests/mm/gup_test.c
index 3f841a96f870..d60d48bb9126 100644
--- a/tools/testing/selftests/mm/gup_test.c
+++ b/tools/testing/selftests/mm/gup_test.c
@@ -9,267 +9,361 @@
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
-#include <pthread.h>
-#include <assert.h>
#include <mm/gup_test.h>
#include "kselftest.h"
#include "vm_util.h"
#include "hugepage_settings.h"
+#include "kselftest_harness.h"
#define MB (1UL << 20)
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof((arr)[0]))
+#endif
+
/* Just the flags we need, copied from the kernel internals. */
#define FOLL_WRITE 0x01 /* check pte is writable */
+/* Page counts exercising single, THP-batch, partial, and full-mapping GUP. */
+static const int nr_pages_list[] = { 1, 512, 123, -1 };
+
#define GUP_TEST_FILE "/sys/kernel/debug/gup_test"
-static unsigned long cmd = GUP_FAST_BENCHMARK;
-static int gup_fd, repeats = 1;
-static unsigned long size = 128 * MB;
-/* Serialize prints */
-static pthread_mutex_t print_mutex = PTHREAD_MUTEX_INITIALIZER;
+FIXTURE(gup_test) {
+ int gup_fd;
+ char *addr;
+ unsigned long size;
+};
+
+FIXTURE_VARIANT(gup_test) {
+ bool thp;
+ bool hugetlb;
+ bool write;
+ bool shared;
+};
+
+FIXTURE_VARIANT_ADD(gup_test, private_write)
+{
+ .thp = false,
+ .hugetlb = false,
+ .write = true,
+ .shared = false,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, private_readonly)
+{
+ .thp = false,
+ .hugetlb = false,
+ .write = false,
+ .shared = false,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, private_write_thp)
+{
+ .thp = true,
+ .hugetlb = false,
+ .write = true,
+ .shared = false,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, private_readonly_thp)
+{
+ .thp = true,
+ .hugetlb = false,
+ .write = false,
+ .shared = false,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, private_write_hugetlb)
+{
+ .thp = false,
+ .hugetlb = true,
+ .write = true,
+ .shared = false,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, private_readonly_hugetlb)
+{
+ .thp = false,
+ .hugetlb = true,
+ .write = false,
+ .shared = false,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, shared_write)
+{
+ .thp = false,
+ .hugetlb = false,
+ .write = true,
+ .shared = true,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, shared_readonly)
+{
+ .thp = false,
+ .hugetlb = false,
+ .write = false,
+ .shared = true,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, shared_write_thp)
+{
+ .thp = true,
+ .hugetlb = false,
+ .write = true,
+ .shared = true,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, shared_readonly_thp)
+{
+ .thp = true,
+ .hugetlb = false,
+ .write = false,
+ .shared = true,
+};
+
+FIXTURE_VARIANT_ADD(gup_test, shared_write_hugetlb)
+{
+ .thp = false,
+ .hugetlb = true,
+ .write = true,
+ .shared = true,
+};
-static char *cmd_to_str(unsigned long cmd)
+FIXTURE_VARIANT_ADD(gup_test, shared_readonly_hugetlb)
{
- switch (cmd) {
- case GUP_FAST_BENCHMARK:
- return "GUP_FAST_BENCHMARK";
- case PIN_FAST_BENCHMARK:
- return "PIN_FAST_BENCHMARK";
- case PIN_LONGTERM_BENCHMARK:
- return "PIN_LONGTERM_BENCHMARK";
- case GUP_BASIC_TEST:
- return "GUP_BASIC_TEST";
- case PIN_BASIC_TEST:
- return "PIN_BASIC_TEST";
- case DUMP_USER_PAGES_TEST:
- return "DUMP_USER_PAGES_TEST";
+ .thp = false,
+ .hugetlb = true,
+ .write = false,
+ .shared = true,
+};
+
+FIXTURE_SETUP(gup_test) {
+ int mmap_flags = MAP_PRIVATE;
+ int zero_fd;
+ char *p;
+
+ self->size = variant->hugetlb ? 256 * MB : 128 * MB;
+
+ /* Check for hugetlb */
+ if (variant->hugetlb) {
+ unsigned long hp_size = default_huge_page_size();
+
+ if (!hp_size)
+ SKIP(return, "HugeTLB not available\n");
+
+ self->size = (self->size + hp_size - 1) & ~(hp_size - 1);
+ if (!hugetlb_setup_default(self->size / hp_size))
+ SKIP(return, "Not enough huge pages\n");
+
+ mmap_flags |= (MAP_HUGETLB | MAP_ANONYMOUS);
}
- return "Unknown command";
+
+ /* zero_fd has to be >= 0. Already checked in main() */
+ zero_fd = open("/dev/zero", O_RDWR);
+ ASSERT_GE(zero_fd, 0);
+
+ /* gup_fd has to be >= 0. Already checked in main() */
+ self->gup_fd = open(GUP_TEST_FILE, O_RDWR);
+ ASSERT_GE(self->gup_fd, 0);
+
+ if (variant->shared)
+ mmap_flags = (mmap_flags & ~MAP_PRIVATE) | MAP_SHARED;
+
+ self->addr = mmap(NULL, self->size, PROT_READ | PROT_WRITE,
+ mmap_flags, zero_fd, 0);
+ close(zero_fd);
+ ASSERT_NE(self->addr, MAP_FAILED);
+
+ if (variant->thp)
+ madvise(self->addr, self->size, MADV_HUGEPAGE);
+ else
+ madvise(self->addr, self->size, MADV_NOHUGEPAGE);
+
+ for (p = self->addr; (unsigned long)p < (unsigned long)self->addr
+ + self->size; p += psize())
+ p[0] = 0;
}
-void *gup_thread(void *data)
-{
- struct gup_test gup = *(struct gup_test *)data;
- int i, status;
-
- /* Only report timing information on the *_BENCHMARK commands: */
- if ((cmd == PIN_FAST_BENCHMARK) || (cmd == GUP_FAST_BENCHMARK) ||
- (cmd == PIN_LONGTERM_BENCHMARK)) {
- for (i = 0; i < repeats; i++) {
- gup.size = size;
- status = ioctl(gup_fd, cmd, &gup);
- if (status)
- break;
-
- pthread_mutex_lock(&print_mutex);
- ksft_print_msg("%s: Time: get:%lld put:%lld us",
- cmd_to_str(cmd), gup.get_delta_usec,
- gup.put_delta_usec);
- if (gup.size != size)
- ksft_print_msg(", truncated (size: %lld)", gup.size);
- ksft_print_msg("\n");
- pthread_mutex_unlock(&print_mutex);
- }
- } else {
- gup.size = size;
- status = ioctl(gup_fd, cmd, &gup);
- if (status)
- goto return_;
-
- pthread_mutex_lock(&print_mutex);
- ksft_print_msg("%s: done\n", cmd_to_str(cmd));
- if (gup.size != size)
- ksft_print_msg("Truncated (size: %lld)\n", gup.size);
- pthread_mutex_unlock(&print_mutex);
+FIXTURE_TEARDOWN(gup_test) {
+ munmap(self->addr, self->size);
+ close(self->gup_fd);
+
+ if (variant->hugetlb)
+ hugetlb_restore_settings();
+}
+
+TEST_F(gup_test, get_user_pages) {
+ /* Tests the get_user_pages path */
+ int i;
+
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
+
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
+
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, GUP_BASIC_TEST, &gup), 0);
}
+}
+
+TEST_F(gup_test, pin_user_pages) {
+ /* Tests the pin_user_pages path */
+ int i;
+
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
+
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
-return_:
- ksft_test_result(!status, "ioctl status %d\n", status);
- return NULL;
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, PIN_BASIC_TEST, &gup), 0);
+ }
}
-int main(int argc, char **argv)
-{
- struct gup_test gup = { 0 };
- int filed, i, opt, nr_pages = 1, thp = -1, write = 1, nthreads = 1, ret;
- int flags = MAP_PRIVATE;
- char *file = "/dev/zero";
- bool hugetlb = false;
- pthread_t *tid;
- char *p;
+TEST_F(gup_test, dump_user_pages_with_get) {
+ /* Tests DUMP_USER_PAGES_TEST using get_user_pages */
+ int i;
- while ((opt = getopt(argc, argv, "m:r:n:F:f:abcj:tTLUuwWSHpz")) != -1) {
- switch (opt) {
- case 'a':
- cmd = PIN_FAST_BENCHMARK;
- break;
- case 'b':
- cmd = PIN_BASIC_TEST;
- break;
- case 'L':
- cmd = PIN_LONGTERM_BENCHMARK;
- break;
- case 'c':
- cmd = DUMP_USER_PAGES_TEST;
- /*
- * Dump page 0 (index 1). May be overridden later, by
- * user's non-option arguments.
- *
- * .which_pages is zero-based, so that zero can mean "do
- * nothing".
- */
- gup.which_pages[0] = 1;
- break;
- case 'p':
- /* works only with DUMP_USER_PAGES_TEST */
- gup.test_flags |= GUP_TEST_FLAG_DUMP_PAGES_USE_PIN;
- break;
- case 'F':
- /* strtol, so you can pass flags in hex form */
- gup.gup_flags = strtol(optarg, 0, 0);
- break;
- case 'j':
- nthreads = atoi(optarg);
- break;
- case 'm':
- size = atoi(optarg) * MB;
- break;
- case 'r':
- repeats = atoi(optarg);
- break;
- case 'n':
- nr_pages = atoi(optarg);
- if (nr_pages < 0)
- nr_pages = size / psize();
- break;
- case 't':
- thp = 1;
- break;
- case 'T':
- thp = 0;
- break;
- case 'U':
- cmd = GUP_BASIC_TEST;
- break;
- case 'u':
- cmd = GUP_FAST_BENCHMARK;
- break;
- case 'w':
- write = 1;
- break;
- case 'W':
- write = 0;
- break;
- case 'f':
- file = optarg;
- break;
- case 'S':
- flags &= ~MAP_PRIVATE;
- flags |= MAP_SHARED;
- break;
- case 'H':
- flags |= (MAP_HUGETLB | MAP_ANONYMOUS);
- hugetlb = true;
- break;
- default:
- ksft_exit_fail_msg("Wrong argument\n");
- }
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
+
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
+
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ gup.which_pages[0] = 1;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, DUMP_USER_PAGES_TEST, &gup), 0);
}
+}
- if (optind < argc) {
- int extra_arg_count = 0;
- /*
- * For example:
- *
- * ./gup_test -c 0 1 0x1001
- *
- * ...to dump pages 0, 1, and 4097
- */
-
- while ((optind < argc) &&
- (extra_arg_count < GUP_TEST_MAX_PAGES_TO_DUMP)) {
- /*
- * Do the 1-based indexing here, so that the user can
- * use normal 0-based indexing on the command line.
- */
- long page_index = strtol(argv[optind], 0, 0) + 1;
-
- gup.which_pages[extra_arg_count] = page_index;
- extra_arg_count++;
- optind++;
- }
+TEST_F(gup_test, dump_user_pages_with_pin) {
+ /* Tests DUMP_USER_PAGES_TEST using pin_user_pages */
+ int i;
+
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
+
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
+
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ gup.which_pages[0] = 1;
+ gup.test_flags |= GUP_TEST_FLAG_DUMP_PAGES_USE_PIN;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, DUMP_USER_PAGES_TEST, &gup), 0);
}
+}
- ksft_print_header();
+TEST_F(gup_test, get_user_pages_fast) {
+ /* Tests the lockless get_user_pages_fast() path */
+ int i;
- if (hugetlb) {
- unsigned long hp_size = default_huge_page_size();
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
- if (!hp_size)
- ksft_exit_skip("HugeTLB is unavailable\n");
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
- size = (size + hp_size - 1) & ~(hp_size - 1);
- if (!hugetlb_setup_default(size / hp_size))
- ksft_exit_skip("Not enough huge pages\n");
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, GUP_FAST_BENCHMARK, &gup), 0);
}
+}
- ksft_set_plan(nthreads);
+TEST_F(gup_test, pin_user_pages_fast) {
+ /* Tests the lockless pin_user_pages_fast() path */
+ int i;
- filed = open(file, O_RDWR|O_CREAT, 0664);
- if (filed < 0)
- ksft_exit_fail_msg("Unable to open %s: %s\n", file, strerror(errno));
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
- gup.nr_pages_per_call = nr_pages;
- if (write)
- gup.gup_flags |= FOLL_WRITE;
-
- gup_fd = open(GUP_TEST_FILE, O_RDWR);
- if (gup_fd == -1) {
- switch (errno) {
- case EACCES:
- if (getuid())
- ksft_print_msg("Please run this test as root\n");
- break;
- case ENOENT:
- if (opendir("/sys/kernel/debug") == NULL)
- ksft_print_msg("mount debugfs at /sys/kernel/debug\n");
- ksft_print_msg("check if CONFIG_GUP_TEST is enabled in kernel config\n");
- break;
- default:
- ksft_print_msg("failed to open %s: %s\n", GUP_TEST_FILE, strerror(errno));
- break;
- }
- ksft_test_result_skip("Please run this test as root\n");
- ksft_exit_pass();
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
+
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, PIN_FAST_BENCHMARK, &gup), 0);
}
+}
- p = mmap(NULL, size, PROT_READ | PROT_WRITE, flags, filed, 0);
- if (p == MAP_FAILED)
- ksft_exit_fail_msg("mmap: %s\n", strerror(errno));
- gup.addr = (unsigned long)p;
+TEST_F(gup_test, pin_user_pages_longterm) {
+ /* Tests pin_user_pages() with FOLL_LONGTERM */
+ int i;
- if (thp == 1)
- madvise(p, size, MADV_HUGEPAGE);
- else if (thp == 0)
- madvise(p, size, MADV_NOHUGEPAGE);
+ for (i = 0; i < (int)ARRAY_SIZE(nr_pages_list); i++) {
+ struct gup_test gup = { 0 };
- /* Fault them in here, from user space. */
- for (; (unsigned long)p < gup.addr + size; p += psize())
- p[0] = 0;
+ gup.addr = (unsigned long)self->addr;
+ gup.size = self->size;
+ gup.nr_pages_per_call = nr_pages_list[i] < 0 ?
+ self->size / psize() : nr_pages_list[i];
- tid = malloc(sizeof(pthread_t) * nthreads);
- assert(tid);
- for (i = 0; i < nthreads; i++) {
- ret = pthread_create(&tid[i], NULL, gup_thread, &gup);
- assert(ret == 0);
+ if (variant->write)
+ gup.gup_flags |= FOLL_WRITE;
+
+ TH_LOG("nr_pages_per_call=%u", gup.nr_pages_per_call);
+ ASSERT_EQ(ioctl(self->gup_fd, PIN_LONGTERM_BENCHMARK, &gup), 0);
}
- for (i = 0; i < nthreads; i++) {
- ret = pthread_join(tid[i], NULL);
- assert(ret == 0);
+}
+
+int main(int argc, char **argv)
+{
+ int fd;
+ char *file = "/dev/zero";
+
+ fd = open(file, O_RDWR);
+ if (fd < 0) {
+ ksft_print_header();
+ ksft_exit_fail_msg("Unable to open %s: %s\n", file, strerror(errno));
}
+ close(fd);
- free(tid);
+ fd = open(GUP_TEST_FILE, O_RDWR);
+ if (fd == -1) {
+ ksft_print_header();
+ if (errno == EACCES)
+ ksft_exit_skip("Please run this test as root\n");
+ if (errno == ENOENT) {
+ if (opendir("/sys/kernel/debug") == NULL)
+ ksft_exit_skip("Mount debugfs at /sys/kernel/debug\n");
+ else
+ ksft_exit_skip("Check CONFIG_GUP_TEST in kernel config\n");
+ }
+ ksft_exit_skip("failed to open %s: %s\n", GUP_TEST_FILE, strerror(errno));
+ }
+ close(fd);
- ksft_exit_pass();
+ return test_harness_run(argc, argv);
}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 043aa3ed2596..65a4ef0f3748 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -130,30 +130,6 @@ test_selected() {
fi
}
-run_gup_matrix() {
- # -t: thp=on, -T: thp=off, -H: hugetlb=on
- local hugetlb_mb=256
-
- for huge in -t -T "-H -m $hugetlb_mb"; do
- # -u: gup-fast, -U: gup-basic, -a: pin-fast, -b: pin-basic, -L: pin-longterm
- for test_cmd in -u -U -a -b -L; do
- # -w: write=1, -W: write=0
- for write in -w -W; do
- # -S: shared
- for share in -S " "; do
- # -n: How many pages to fetch together? 512 is special
- # because it's default thp size (or 2M on x86), 123 to
- # just test partial gup when hit a huge in whatever form
- for num in "-n 1" "-n 512" "-n 123" "-n -1"; do
- CATEGORY="gup_test" run_test ./gup_test \
- $huge $test_cmd $write $share $num
- done
- done
- done
- done
- done
-}
-
# filter 64bit architectures
ARCH64STR="arm64 mips64 parisc64 ppc64 ppc64le riscv64 s390x sparc64 x86_64"
if [ -z "$ARCH" ]; then
@@ -239,18 +215,7 @@ fi
CATEGORY="mmap" run_test ./map_fixed_noreplace
-if $RUN_ALL; then
- run_gup_matrix
-else
- # get_user_pages_fast() benchmark
- CATEGORY="gup_test" run_test ./gup_test -u -n 1
- CATEGORY="gup_test" run_test ./gup_test -u -n -1
- # pin_user_pages_fast() benchmark
- CATEGORY="gup_test" run_test ./gup_test -a -n 1
- CATEGORY="gup_test" run_test ./gup_test -a -n -1
-fi
-# Dump pages 0, 19, and 4096, using pin_user_pages:
-CATEGORY="gup_test" run_test ./gup_test -ct -F 0x1 0 19 0x1000
+CATEGORY="gup_test" run_test ./gup_test
CATEGORY="gup_test" run_test ./gup_longterm
CATEGORY="userfaultfd" run_test ./uffd-unit-tests
--
2.39.5
^ permalink raw reply related
* Re: [PATCH v6 03/11] dt-bindings: mfd: add documentation for S2MU005 PMIC
From: Kaustabh Chakraborty @ 2026-05-19 12:07 UTC (permalink / raw)
To: Krzysztof Kozlowski, Conor Dooley, 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: <0240eb13-6c56-4879-8db7-b990a220a78f@kernel.org>
On 2026-05-18 12:23 +02:00, Krzysztof Kozlowski wrote:
> On 18/05/2026 11:45, Conor Dooley wrote:
>> On Mon, May 18, 2026 at 09:15:11AM +0200, Krzysztof Kozlowski wrote:
>>> On 17/05/2026 22:52, Conor Dooley wrote:
>>>> On Sun, May 17, 2026 at 06:39:37PM +0530, Kaustabh Chakraborty wrote:
>>>>>>>>>> +
>>>>>>>>> + properties:
>>>>>>>>> + compatible:
>>>>>>>>> + const: samsung,s2mu005-rgb
>>>>>>>>> +
>>>>>>>>> + required:
>>>>>>>>> + - compatible
>>>>>>>>> +
>>>>>>>>> + unevaluatedProperties: false
>>>>>>>>> +
>>>>>>>>> + reg:
>>>>>>>>> + maxItems: 1
>>>>>>>>
>>>>>>>> Move this above the child nodes please.
>>>>>>>
>>>>>>> But properties are sorted in lex order?
>>>>>>
>>>>>> Typically the binding is sorted in the same order as properties go in
>>>>>> nodes. Common stuff like reg/clocks/interrupts therefore send up above
>>>>>> child nodes.
>>>>>
>>>>> So, do I change this? For one, I don't see the same being followed in
>>>>> other schemas of samsung in the same dir (not that I'm trying to pose it
>>>>> as an argument against your suggestion), and this was reviewed by
>>>>> Krzysztof and is adderssed in v7.
>>>>
>>>> If Krzysztof doesn't care, then I won't ask you to change it.
>>>
>>> This builds on top of bindings for previous Samsung PMIC devices, so
>>> that's why it keeps the compatibles for children, I guess. No one
>>> complained about this at v1-v2 reviews, so when I joined reviewing in v3
>>> I did not, either.
>>>
>>> I don't think the compatible should be here, but I also don't want to
>>> stall that patchset. I understand that it is inconsistent review from my
>>> side, because other similar patchsets receive comment to drop the
>>> compatible. But I don't think we will be fair asking to drop the
>>> compatible now, when we did not ask for that in the early versions at all.
>>
>>
>> I think you misunderstood, we were talking about the ordering of the
>> properties in the binding file being alphanumerical, rather than the
>> more typical approach of approximately following the order of
>> dts-coding-style.
>
>
> Ah, then I misunderstood and, even though it is a nit, I do care because
> old code is then used for new patches. Bindings follow DTS rules, thus
> should be:
> 1. compatible
> 2. reg
> 3. core properties
> 4. vendor properties
>
> Kaustabh, can you change it please?
Ack, will do that in v8 then.
While at it, do you also want me to drop the multi-led compatible string?
So it would be:
multi-led:
$ref: /schemas/leds/leds-class-multicolor.yaml#
>
> Best regards,
> Krzysztof
^ permalink raw reply
* Re: [PATCH v3] killswitch: add per-function short-circuit mitigation primitive
From: Daniel Borkmann @ 2026-05-19 12:13 UTC (permalink / raw)
To: Song Liu, Sasha Levin
Cc: linux-kernel, linux-doc, linux-kselftest, bpf, live-patching,
Greg Kroah-Hartman, Andrew Morton, Jonathan Corbet,
Mathieu Desnoyers, Joshua Peisach, Florian Weimer, Breno Leitao,
Anthony Iliopoulos, Michal Hocko, Jiri Olsa, John Fastabend,
Christian Brauner
In-Reply-To: <CAPhsuW44UX663Au=WwHz8MVwnQgLkjxOqpJSCKxNiv3=RpZvqw@mail.gmail.com>
On 5/19/26 1:59 AM, Song Liu wrote:
> On Mon, May 18, 2026 at 6:33 AM Sasha Levin <sashal@kernel.org> wrote:
>> On Sun, May 17, 2026 at 11:37:36PM -0700, Song Liu wrote:
>>> On Sun, May 17, 2026 at 6:49 AM Sasha Levin <sashal@kernel.org> wrote:
>>>> * fail_function (CONFIG_FUNCTION_ERROR_INJECTION) is disabled in
>>>> most production kernels. Even where enabled, it only works on
>>>> functions pre-annotated with ALLOW_ERROR_INJECTION() in source -
>>>> no help for a freshly-disclosed CVE. The debugfs UI is blocked by
>>>> lockdown=integrity and the override is probabilistic.
>>>>
>>>> * BPF override (bpf_override_return) honors the same
>>>> ALLOW_ERROR_INJECTION() whitelist, and BPF itself is off in many
>>>> production kernels. Even where on, the operator interface is
>>>> "load a verified BPF program," not a one-line write.
>>>
>>> If it is OK for killswitch to attach to any kernel functions, do we still
>>> need ALLOW_ERROR_INJECTION() for fail_function and BPF
>>> override? Shall we instead also allow fail_function and BPF override
>>> to attach to any kernel functions?
>>
>> I don't think so. ALLOW_ERROR_INJECTION is not a security mechanism, it's an
>> integrity/safety mechanism for both bpf and fault injection.
>>
>> It protects against a "developer or CI script doing legitimate fault injection
>> accidentally panics the box" scenario, not an "attacker gets in" one.
>
> There really isn't a clear boundary between "security mechanism" and
> "non-security mechanism". As we are making killswitch available
> everywhere under root, users will soon learn to use it to do fault injection,
> and potentially much more scary things. (Think about agents with sudo
> access).
Fully agree with Song here that there is no clear boundary, and that the
killswitch could lead to arbitrary, hard to debug breakage if applied to
the wrong function.. introducing worse bugs than the one being mitigated
or even /short-circuit LSM enforcement/ (engage security_file_open 0,
engage cap_capable 0, engage apparmor_* etc).
The ALLOW_ERROR_INJECTION() provides a curated white-list where you may
return with an error without causing more severe damage (assuming the
error handling code is right). The right thing would be to more widely
apply ALLOW_ERROR_INJECTION() or to figure out a better way to safely
enable the latter without explicit function annotation.
Wrt BPF:
>>>> * BPF override (bpf_override_return) honors the same
>>>> ALLOW_ERROR_INJECTION() whitelist, and BPF itself is off in many
>>>> production kernels. Even where on, the operator interface is
>>>> "load a verified BPF program," not a one-line write.
The claim that BPF itself is off in many production kernels is not really
true, where did you get that from? All the major distros and cloud providers
have BPF enabled these days, and even systemd ships BPF programs for
custom service firewalling etc.
The operator interface is to load a program vs. one-line write.. so we're
disregarding existing infra where you can already achieve the same for a
less safe one-liner convenience? (similarly for the livepatch infra..)
If you need a one-liner: bpftrace -e 'kprobe:FUNC { override(RETVAL); }'
Alternatively, add an extension to systemd where you can just deploy a
list of functions, and it does the necessary work in the background and
persistently.
Also, what about other classes of bugs, like OOB access, UAFs, locking
issues, etc which then could be used as a means for privilege escalations?
It feels like this proposal is a quick'n'dirty prototype via Claude as a
reaction to copy fail bug, but the right solution would be to improve
the user space tooling as mentioned and existing infra we have in kernel.
^ permalink raw reply
* Re: [PATCH] Documentation: KVM: Document guest-visible compatibility expectations
From: Paolo Bonzini @ 2026-05-19 12:13 UTC (permalink / raw)
To: David Woodhouse
Cc: Will Deacon, Marc Zyngier, Jonathan Corbet, Shuah Khan, kvm,
Linux Doc Mailing List, Kernel Mailing List, Linux,
Sean Christopherson, Jim Mattson, Oliver Upton, Joey Gouly,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas,
Raghavendra Rao Ananta, Eric Auger, Kees Cook, Arnd Bergmann,
Nathan Chancellor, linux-arm-kernel, kvmarm, linux-kselftest
In-Reply-To: <cf429f2082e863571595f74d1d3dedc3e6a82964.camel@infradead.org>
On Tue, May 19, 2026 at 1:44 PM David Woodhouse <dwmw2@infradead.org> wrote:
> > > So... what next? Is one of the other KVM/arm64 maintainers going to
> > > speak up? Paolo would you consider taking the fixes through your tree
> > > directly?
I admit that my knowledge of Arm is really limited, and I do not
understand which IIDR values have architecturally allowed behaviors
and which (if any) were made up by KVM; but even if I cannot honestly
remark on the code or even the approach, a compatibility knob is the
right thing to have. That's a userspace API design matter, not an Arm
or GIC matter.
I hope that Marc provides a better explanation of why he believes
https://lore.kernel.org/all/20260511113558.3325004-2-dwmw2@infradead.org/
shouldn't be accepted, because I am more than a bit puzzled about
*why* that patch is being rejected or (in v3) so far ignored. Marc in
this thread wrote: "If userspace is not a total joke, it will read all
the ID registers, and configure what it wants to see, assuming it is a
feature that can be configured (not everything can, because the
architecture itself is not fully backward compatible)". But in this
case there's an ID register that tells KVM if userspace wants the old
or the new behavior, independent of whether that old behavior is
architecturally valid or not.
I will certainly take this patch, but I won't override Marc. However
I'd like to better understand his point of view, because right now I
just don't get it.
> If KVM on arm64 doesn't aspire to maintain guest compatibility across
> host kernel changes — regardless of whether the previous kernel's
> behaviour was "blessed" by the architecture specification or not — then
> it does not meet the expectation that we have of KVM implementations in
> the Linux kernel.
I agree with the "aspire" wording. Even if it's not going to be 100%
achievable, KVM *needs* to aspire to maintain both guest compatibility
and architecture precision. Sometimes it's impossible, sometimes there
are constraints that require you to trade off one for another (e.g.
via quirks, or by breaking behavior that no sane guest would have
cared about). But in general as a maintainer you don't *get* to
choose.
Paolo
> Or indeed the standards that we've held for Linux kernel ABIs for the
> last 35 years.
^ permalink raw reply
* Re: [PATCH 09/15] accel/qda: Add DMA-backed GEM objects and memory manager integration
From: Markus Elfring @ 2026-05-19 12:14 UTC (permalink / raw)
To: Ekansh Gupta, dri-devel, iommu, linux-media, linux-arm-msm,
linaro-mm-sig, Christian König, David Airlie,
Jörg Rödel, Jonathan Corbet, Maarten Lankhorst,
Maxime Ripard, Oded Gabbay, Robin Murphy, Shuah Khan,
Simona Vetter, Sumit Semwal, Thomas Zimmermann
Cc: linux-doc, linux-kernel, Bharath Kumar, Bjorn Andersson,
Chenna Kesava Raju, Dmitry Baryshkov, Konrad Dybcio, Rob Clark,
Srinivas Kandagatla, Will Deacon
In-Reply-To: <20260519-qda-series-v1-9-b2d984c297f8@oss.qualcomm.com>
…
> Assisted-by: Claude:claude-4-6-sonnet
…
Did such an information source gather the knowledge to benefit more
from the application of scope-based resource management?
…
> +++ b/drivers/accel/qda/qda_drv.c
…
> @@ -32,6 +33,18 @@ static void qda_postclose(struct drm_device *dev, struct drm_file *file)
> {
…
> + if (refcount_dec_and_test(&iommu_dev->refcount)) {
> + spin_lock_irqsave(&iommu_dev->lock, flags);
> + iommu_dev->assigned_pid = 0;
> + iommu_dev->assigned_file_priv = NULL;
> + spin_unlock_irqrestore(&iommu_dev->lock, flags);
> + }
…
Under which circumstances would you become interested to apply a statement
like “guard(spinlock_irqsave)(&iommu_dev->lock);”?
https://elixir.bootlin.com/linux/v7.1-rc4/source/include/linux/spinlock.h#L619-L622
Regards,
Markus
^ permalink raw reply
* [PATCH v12 0/6] iio: adc: ad4691: add driver for AD4691 multichannel SAR ADC family
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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 v12:
- IIO_DEV_ACQUIRE_DIRECT_MODE moved from set_sampling_freq into write_raw
- enum ad4691_ref_ctrl: remove explicit values
- .sign = 'u' → .format = 'u' throughout — the field has a new name
- device_property_present(dev, "vdd-supply") / device_property_present(dev, "ref-supply")
pattern instead of handling -ENODEV
- AD4691_STATE_RESET_ALL 0x01 → BIT(0)
- spi_device_id: use named initializers instead of (kernel_ulong_t)&... cast
- ad4691_reset: 300 µs sleep belongs after reset_control_deassert, not
between assert and deassert — actual RESETL minimum time (~10 ns) is
covered by overhead; add post-deassert sleep
- Add guard(mutex)(&st->lock) in get_sampling_freq
- Don't register or attach iio_trigger in manual mode
- Remove the if (i >= indio_dev->num_channels - 1) break guards
- ad4691_read_scan return type → void
- Remove (u16) cast: st->scan_tx[k] = AD4691_ADC_CHAN(bit) << 8
- sizeof(st->scan_tx[k]) → sizeof(*st->scan_tx)
- Remove & from iio_buffer_setup_ops in offload structs
- Add args[0] <= 3 check in ad4691_offload_trigger_request
- Embed offload, offload_trigger, trigger_hz directly in struct ad4691_state
- Link to v11: https://lore.kernel.org/r/20260515-ad4692-multichannel-sar-adc-driver-v11-0-eab27d852ac2@analog.com
Changes in v11:
- initial driver: fix commit message — IIO_CHAN_INFO_SAMP_FREQ is
info_mask_separate throughout the series, not info_mask_shared_by_all
- initial driver: readable_reg / volatile_reg: replace open switch ranges
for multi-byte sparse arrays with stride checks; intermediate (unaligned)
addresses are now excluded so debugfs cannot trigger cross-boundary reads
- initial driver: add comment in ad4691_get_sampling_freq noting that
AD4691_OSC_FREQ_REG is non-volatile and served from regcache; no lock
is needed
- triggered buffer: restore .endianness = IIO_BE on AD4691_CHANNEL
scan_type; accidentally dropped in v10
- triggered buffer: add early break in both iio_for_each_active_channel
loops to skip the soft timestamp scan index; prevents out-of-bounds
writes into scan_tx[] and scan_xfers[]
- triggered buffer: fix DMA aliasing in manual mode preenable — set
rx_buf = NULL for the first transfer (pipeline residual) instead of
aliasing it to vals[0] alongside the second transfer
- triggered buffer: add cs_change_delay of 430 ns on channel transfers
to satisfy the minimum CNV high time requirement
- triggered buffer: remove cs_change=1 from the state-reset transfer;
must not be set on the final transfer of a SPI message
- triggered buffer: move enable_irq() from the trigger handler into a
reenable callback on ad4691_trigger_ops, closing the race between
enable_irq and iio_trigger_notify_done; fix reenable return type
(void, not int)
- triggered buffer: use two separate iio_info structs so that
validate_trigger (iio_validate_own_trigger) is enforced only in CNV
burst mode; manual mode must accept external triggers
- triggered buffer: add comment explaining STATE_RESET_ALL sequencing
in CNV burst mode
- triggered buffer: fix STD_SEQ_CONFIG write in both preenable paths —
apply & GENMASK(15, 0) to strip the soft timestamp bit before writing,
matching the existing acc_mask computation
- oversampling: fix commit message — writing oversampling_ratio snaps
target_osc_freq_Hz to preserve integer sampling_frequency read-back;
the two attributes are not orthogonal
- docs: add missing Buffer data format section covering the __be16
software path and the CPU-native offload DMA path
- Link to v10: https://lore.kernel.org/r/20260511-ad4692-multichannel-sar-adc-driver-v10-0-e1fbb1744e38@analog.com
Changes in v10:
- initial driver: depends on REGULATOR || COMPILE_TEST
- triggered buffer: fix vals[] layout — index vals[] with slot counter k,
not channel index i; fixes sparse active_scan_mask producing garbage in
userspace buffer
- triggered buffer: add comment to cnv_burst_buffer_postenable explaining
why sampling_enable()/enable_irq() cannot be called from preenable
- triggered buffer + offload: scan_tx changed from __be16 to u16;
non-offload path uses put_unaligned_be16() (bits_per_word=8); offload
path uses plain native u16 assignments (bits_per_word=16); also fixes
byte-order bug in manual preenable: command byte was in the low byte,
now correctly shifted to the high byte
- oversampling: remove incorrect iio_for_each_active_channel() timestamp
guards; active_scan_mask never includes the timestamp channel
- Link to v9: https://lore.kernel.org/r/20260430-ad4692-multichannel-sar-adc-driver-v9-0-33e439e4fb87@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 | 226 +++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 9 +
drivers/iio/adc/Kconfig | 16 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 2099 ++++++++++++++++++++
7 files changed, 2532 insertions(+)
---
base-commit: 5200f5f493f79f14bbdc349e402a40dfb32f23c8
change-id: 20260302-ad4692-multichannel-sar-adc-driver-78e4d44d24b2
Best regards,
--
Radu Sabau <radu.sabau@analog.com>
^ permalink raw reply
* [PATCH v12 1/6] dt-bindings: iio: adc: add AD4691 family
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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: <20260519-ad4692-multichannel-sar-adc-driver-v12-0-5b335162aa51@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 c2c6d79275c6..7d31c38921e8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1482,6 +1482,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 v12 2/6] iio: adc: ad4691: add initial driver for AD4691 family
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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: <20260519-ad4692-multichannel-sar-adc-driver-v12-0-5b335162aa51@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 by asserting
the reset-gpios GPIO line for at least 300 µs then deasserting it;
a software reset via SPI_CONFIG_A is used as fallback when no reset
GPIO is provided.
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.
IIO_CHAN_INFO_SAMP_FREQ is exposed as info_mask_separate. The oscillator
is shared hardware — writing any channel's sampling_frequency attribute
sets it for all others — but per-channel attributes are used throughout
the series to avoid an ABI change when per-channel oversampling ratios
are introduced in a later commit, at which point the effective output
rate (osc_freq / osr[N]) becomes genuinely per-channel.
Reviewed-by: David Lechner <dlechner@baylibre.com>
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
MAINTAINERS | 1 +
drivers/iio/adc/Kconfig | 12 +
drivers/iio/adc/Makefile | 1 +
drivers/iio/adc/ad4691.c | 796 +++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 810 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index 7d31c38921e8..020c1ffae31b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1488,6 +1488,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 a9dedbb8eb46..c9aca0d08e41 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -144,6 +144,18 @@ 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
+ depends on REGULATOR || COMPILE_TEST
+ 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 097357d146ba..707dd708912f 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..2d58df862142
--- /dev/null
+++ b/drivers/iio/adc/ad4691.c
@@ -0,0 +1,796 @@
+// 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/bitmap.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/types.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_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))
+
+#define AD4691_STATUS_REG 0x014
+#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)
+#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_SEQ_ALL_CHANNELS_OFF 0x00
+#define AD4691_SPARE_CONTROL 0x02A
+
+#define AD4691_MAX_CHANNELS 16
+
+#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 BIT(0)
+#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
+ * 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_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
+#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,
+ AD4691_VREF_3P0,
+ AD4691_VREF_3P3,
+ AD4691_VREF_4P096,
+ AD4691_VREF_5P0
+};
+
+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_type = { \
+ .realbits = 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),
+};
+
+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),
+};
+
+/*
+ * 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_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;
+ struct spi_device *spi;
+
+ 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_MASK_REG - 1:
+ case AD4691_ACC_MASK_REG + 1 ... 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_ACC_MASK_REG:
+ 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):
+ return true;
+ default:
+ break;
+ }
+
+ /*
+ * Multi-byte registers have non-unit strides; only accept base
+ * addresses to prevent debugfs from triggering reads that cross
+ * register boundaries.
+ */
+ if (reg >= AD4691_AVG_IN(0) && reg <= AD4691_AVG_IN(15))
+ return (reg - AD4691_AVG_IN(0)) % 2 == 0;
+ if (reg >= AD4691_AVG_STS_IN(0) && reg <= AD4691_AVG_STS_IN(15))
+ return (reg - AD4691_AVG_STS_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_IN(0) && reg <= AD4691_ACC_IN(15))
+ return (reg - AD4691_ACC_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_STS_DATA(0) && reg <= AD4691_ACC_STS_DATA(15))
+ return (reg - AD4691_ACC_STS_DATA(0)) % 4 == 0;
+
+ 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:
+ return true;
+ default:
+ break;
+ }
+
+ /* Multi-byte registers: only accept base addresses (see volatile_reg). */
+ if (reg >= AD4691_AVG_IN(0) && reg <= AD4691_AVG_IN(15))
+ return (reg - AD4691_AVG_IN(0)) % 2 == 0;
+ if (reg >= AD4691_AVG_STS_IN(0) && reg <= AD4691_AVG_STS_IN(15))
+ return (reg - AD4691_AVG_STS_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_IN(0) && reg <= AD4691_ACC_IN(15))
+ return (reg - AD4691_ACC_IN(0)) % 3 == 0;
+ if (reg >= AD4691_ACC_STS_DATA(0) && reg <= AD4691_ACC_STS_DATA(15))
+ return (reg - AD4691_ACC_STS_DATA(0)) % 4 == 0;
+
+ 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;
+
+ guard(mutex)(&st->lock);
+
+ 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 ad4691_state *st, int freq)
+{
+ unsigned int start = ad4691_samp_freq_start(st->info);
+
+ guard(mutex)(&st->lock);
+
+ 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)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ IIO_DEV_ACQUIRE_DIRECT_MODE(indio_dev, claim);
+ if (IIO_DEV_ACQUIRE_FAILED(claim))
+ return -EBUSY;
+
+ switch (mask) {
+ case IIO_CHAN_INFO_SAMP_FREQ:
+ return ad4691_set_sampling_freq(st, 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.
+ */
+ if (device_property_present(dev, "vdd-supply")) {
+ ret = devm_regulator_get_enable(dev, "vdd");
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Failed to get and enable VDD\n");
+ } else if (device_property_present(dev, "ldo-in-supply")) {
+ 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 {
+ return dev_err_probe(dev, -EINVAL,
+ "missing one of vdd-supply, ldo-in-supply\n");
+ }
+
+ if (device_property_present(dev, "ref-supply")) {
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "ref");
+ if (st->vref_uV < 0)
+ return dev_err_probe(dev, st->vref_uV,
+ "Failed to get REF supply voltage\n");
+ } else if (device_property_present(dev, "refin-supply")) {
+ st->vref_uV = devm_regulator_get_enable_read_voltage(dev, "refin");
+ if (st->vref_uV < 0)
+ return dev_err_probe(dev, st->vref_uV,
+ "Failed to get REFIN supply voltage\n");
+ st->refbuf_en = true;
+ } else {
+ return dev_err_probe(dev, -EINVAL,
+ "missing one of ref-supply, refin-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;
+ int ret;
+
+ 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) {
+ /*
+ * Assert the reset line to guarantee a clean reset pulse on
+ * every probe, including driver reloads where the line may
+ * already be deasserted (reset_control_put() does not
+ * re-assert on release). tRESETL (minimum pulse width) = 10 ns
+ * (Table 5); kernel function-call overhead alone exceeds this,
+ * so no explicit delay is needed between assert and deassert.
+ */
+ reset_control_assert(rst);
+ ret = reset_control_deassert(rst);
+ if (ret)
+ return ret;
+ /*
+ * Wait tHWR = 300 µs (Table 5) for the device to complete its
+ * internal reset sequence before accepting SPI commands.
+ */
+ fsleep(300);
+ return 0;
+ }
+
+ /* No hardware reset available, fall back to software reset. */
+ ret = regmap_write(st->regmap, AD4691_SPI_CONFIG_A_REG, AD4691_SW_RESET);
+ if (ret)
+ return ret;
+ /*
+ * Wait tSWR = 300 µs (Table 5) for the device to complete its
+ * internal reset sequence before accepting SPI commands.
+ */
+ fsleep(300);
+ return 0;
+}
+
+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->spi = spi;
+ st->info = spi_get_device_match_data(spi);
+ if (!st->info)
+ return -ENODEV;
+
+ 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[] = {
+ { .name = "ad4691", .driver_data = (kernel_ulong_t)&ad4691_chip_info },
+ { .name = "ad4692", .driver_data = (kernel_ulong_t)&ad4692_chip_info },
+ { .name = "ad4693", .driver_data = (kernel_ulong_t)&ad4693_chip_info },
+ { .name = "ad4694", .driver_data = (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 v12 3/6] iio: adc: ad4691: add triggered buffer support
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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: <20260519-ad4692-multichannel-sar-adc-driver-v12-0-5b335162aa51@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 big-endian 16-bit (__be16) slot per active channel, densely packed
in scan_index order, followed by a timestamp.
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 | 559 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 557 insertions(+), 4 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index c9aca0d08e41..1d7afde108c0 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -148,6 +148,8 @@ config AD4691
tristate "Analog Devices AD4691 Family ADC Driver"
depends on SPI
depends on REGULATOR || COMPILE_TEST
+ 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 2d58df862142..ed60ed5b488c 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -11,19 +11,29 @@
#include <linux/dev_printk.h>
#include <linux/device/devres.h>
#include <linux/err.h>
+#include <linux/interrupt.h>
+#include <linux/kstrtox.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>
+#include <linux/string.h>
#include <linux/spi/spi.h>
#include <linux/types.h>
#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
@@ -120,8 +130,12 @@ struct ad4691_chip_info {
BIT(IIO_CHAN_INFO_SAMP_FREQ), \
.info_mask_shared_by_all = BIT(IIO_CHAN_INFO_SCALE), \
.channel = ch, \
+ .scan_index = ch, \
.scan_type = { \
+ .format = 'u', \
.realbits = 16, \
+ .storagebits = 16, \
+ .endianness = IIO_BE, \
}, \
}
@@ -142,6 +156,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[] = {
@@ -153,6 +168,7 @@ 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 = {
@@ -189,6 +205,8 @@ static const int ad4691_osc_freqs_Hz[] = {
[0xF] = 1250,
};
+static const char * const ad4691_gp_names[] = { "gp0", "gp1", "gp2", "gp3" };
+
static const struct ad4691_chip_info ad4691_chip_info = {
.name = "ad4691",
.max_rate = 500 * HZ_PER_KHZ,
@@ -218,8 +236,13 @@ struct ad4691_state {
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 irq_enabled;
bool refbuf_en;
bool ldo_en;
/*
@@ -227,8 +250,56 @@ 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 + 1 state-reset (CNV burst).
+ */
+ struct spi_transfer scan_xfers[34];
+ /*
+ * CNV burst: 16 AVG_IN addresses = 16. Manual: 16 channel cmds +
+ * 1 NOOP = 17. Stored as native u16; put_unaligned_be16() fills each
+ * slot so the SPI controller (bits_per_word=8) sends bytes MSB-first.
+ */
+ u16 scan_tx[17] __aligned(IIO_DMA_MINALIGN);
+ /*
+ * CNV burst state-reset: 4-byte write [addr_hi, addr_lo,
+ * STATE_RESET_ALL, OSC_EN=1]. CS is asserted throughout, so
+ * ADDR_DESCENDING writes byte[3]=1 to OSC_EN_REG (0x180) as a
+ * deliberate side-write, keeping the oscillator enabled. Shared
+ * with the offload path (mutually exclusive at probe).
+ */
+ u8 scan_tx_reset[4] __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;
@@ -548,13 +619,397 @@ static int ad4691_reg_access(struct iio_dev *indio_dev, unsigned int reg,
return regmap_write(st->regmap, reg, writeval);
}
-static const struct iio_info ad4691_info = {
+static int ad4691_set_pwm_freq(struct ad4691_state *st, unsigned 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 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);
+
+ k = 0;
+ iio_for_each_active_channel(indio_dev, i) {
+ /*
+ * Channel-select command occupies the first (high) byte of the
+ * 16-bit DIN frame; the second byte is a don't-care zero pad.
+ * put_unaligned_be16() writes [cmd, 0x00] in memory so the
+ * SPI controller sends the command byte first on the wire.
+ */
+ put_unaligned_be16((u16)(AD4691_ADC_CHAN(i) << 8), &st->scan_tx[k]);
+ 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. Discard it (rx_buf=NULL)
+ * to avoid aliasing vals[0] across two concurrent DMA mappings.
+ * xfer[1] (or the NOOP when only one channel is active) writes
+ * the real ch[0] result to vals[0]. Subsequent transfers write
+ * into vals[k-1] so each result lands at the next dense slot.
+ */
+ st->scan_xfers[k].rx_buf = (k == 0) ? NULL : &st->vals[k - 1];
+ st->scan_xfers[k].len = sizeof(*st->scan_tx);
+ 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;
+ spi_message_add_tail(&st->scan_xfers[k], &st->scan_msg);
+ 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[k - 1];
+ st->scan_xfers[k].len = sizeof(*st->scan_tx);
+ 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 acc_mask, std_seq_config;
+ 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) {
+ put_unaligned_be16(0x8000 | AD4691_AVG_IN(i), &st->scan_tx[k]);
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(*st->scan_tx);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+ st->scan_xfers[2 * k + 1].rx_buf = &st->vals[k];
+ st->scan_xfers[2 * k + 1].len = sizeof(*st->scan_tx);
+ st->scan_xfers[2 * k + 1].cs_change = 1;
+ spi_message_add_tail(&st->scan_xfers[2 * k + 1], &st->scan_msg);
+ k++;
+ }
+
+ /*
+ * Append a 4-byte state-reset transfer [addr_hi, addr_lo,
+ * STATE_RESET_ALL, OSC_EN=1]. CS is asserted throughout, so
+ * ADDR_DESCENDING writes byte[3]=1 to OSC_EN_REG (0x180) as a
+ * deliberate side-write, keeping the oscillator enabled.
+ * STATE_RESET_ALL starts the next burst; the hardware does not
+ * accumulate new conversions until after a STATE_RESET pulse, so
+ * no in-progress data is lost. No cs_change here — CS must
+ * deassert normally at end of message to frame the next command.
+ */
+ put_unaligned_be16(AD4691_STATE_RESET_REG, st->scan_tx_reset);
+ st->scan_tx_reset[2] = AD4691_STATE_RESET_ALL;
+ st->scan_tx_reset[3] = 1;
+ st->scan_xfers[2 * k].tx_buf = st->scan_tx_reset;
+ st->scan_xfers[2 * k].len = sizeof(st->scan_tx_reset);
+ spi_message_add_tail(&st->scan_xfers[2 * k], &st->scan_msg);
+
+ ret = spi_optimize_message(st->spi, &st->scan_msg);
+ if (ret)
+ return ret;
+
+ std_seq_config = bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG, std_seq_config);
+ if (ret)
+ goto err_unoptimize;
+
+ acc_mask = ~std_seq_config & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_ACC_MASK_REG, acc_mask);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_enter_conversion_mode(st);
+ if (ret)
+ goto err_unoptimize;
+
+ return 0;
+
+err_unoptimize:
+ spi_unoptimize_message(&st->scan_msg);
+ return ret;
+}
+
+static int ad4691_cnv_burst_buffer_postenable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+ int ret;
+
+ /*
+ * Start the PWM and unmask the IRQ here in postenable, not in
+ * preenable. The IIO core attaches the trigger poll function between
+ * preenable and postenable; enabling sampling or unmasking the IRQ
+ * before that point risks a DATA_READY assertion landing before the
+ * poll function is registered. iio_trigger_poll() would drop the
+ * event, disable_irq_nosync() would fire, and enable_irq() would
+ * never be called, leaving the IRQ permanently masked.
+ */
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ return ret;
+
+ enable_irq(st->irq);
+ st->irq_enabled = true;
+ return 0;
+}
+
+static int ad4691_cnv_burst_buffer_predisable(struct iio_dev *indio_dev)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ if (st->irq_enabled) {
+ disable_irq(st->irq);
+ st->irq_enabled = false;
+ }
+ return ad4691_sampling_enable(st, false);
+}
+
+static int ad4691_cnv_burst_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_cnv_burst_buffer_setup_ops = {
+ .preenable = ad4691_cnv_burst_buffer_preenable,
+ .postenable = ad4691_cnv_burst_buffer_postenable,
+ .predisable = ad4691_cnv_burst_buffer_predisable,
+ .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);
+ unsigned int freq;
+ int ret;
+
+ ret = kstrtouint(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);
+
+ /*
+ * Disable the IRQ before calling iio_trigger_poll(). The IRQ is
+ * re-enabled via the trigger .reenable callback, which the IIO core
+ * calls inside iio_trigger_notify_done() once use_count reaches zero.
+ * Re-enabling here (before notify_done) would race: a DATA_READY
+ * between enable_irq() and notify_done() calls iio_trigger_poll()
+ * while use_count > 0, dropping the event and permanently masking
+ * the IRQ.
+ */
+ disable_irq_nosync(st->irq);
+ iio_trigger_poll(indio_dev->trig);
+
+ return IRQ_HANDLED;
+}
+
+static void ad4691_trigger_reenable(struct iio_trigger *trig)
+{
+ struct ad4691_state *st = iio_trigger_get_drvdata(trig);
+
+ enable_irq(st->irq);
+}
+
+static const struct iio_trigger_ops ad4691_trigger_ops = {
+ .reenable = ad4691_trigger_reenable,
+ .validate_device = iio_trigger_validate_own_device,
+};
+
+static void ad4691_read_scan(struct iio_dev *indio_dev, s64 ts)
+{
+ struct ad4691_state *st = iio_priv(indio_dev);
+
+ guard(mutex)(&st->lock);
+
+ spi_sync(st->spi, &st->scan_msg);
+
+ /*
+ * 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), ts);
+}
+
+static irqreturn_t ad4691_trigger_handler(int irq, void *p)
+{
+ struct iio_poll_func *pf = p;
+ struct iio_dev *indio_dev = pf->indio_dev;
+
+ ad4691_read_scan(indio_dev, pf->timestamp);
+ iio_trigger_notify_done(indio_dev->trig);
+ return IRQ_HANDLED;
+}
+
+/*
+ * CNV burst mode: only allow our own trigger (driven by DATA_READY IRQ).
+ * Manual mode: external triggers (e.g. iio-trig-hrtimer) must be allowed
+ * because manual mode has no DATA_READY IRQ to fire the internal trigger.
+ * iio_trigger_ops.validate_device = iio_trigger_validate_own_device is
+ * correct in both modes — it prevents other devices from hijacking our
+ * internal trigger; the distinction here is only for iio_info.validate_trigger.
+ */
+static const struct iio_info ad4691_cnv_burst_info = {
+ .read_raw = ad4691_read_raw,
+ .write_raw = ad4691_write_raw,
+ .read_avail = ad4691_read_avail,
+ .debugfs_reg_access = ad4691_reg_access,
+ .validate_trigger = iio_validate_own_trigger,
+};
+
+static const struct iio_info ad4691_manual_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_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);
@@ -662,6 +1117,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;
@@ -715,6 +1186,86 @@ 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;
+
+ indio_dev->channels = st->info->sw_info->channels;
+ indio_dev->num_channels = st->info->sw_info->num_channels;
+ indio_dev->info = st->manual_mode ? &ad4691_manual_info : &ad4691_cnv_burst_info;
+
+ /*
+ * Manual mode relies on an external trigger (e.g. iio-trig-hrtimer);
+ * no internal trigger is needed or registered.
+ */
+ 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);
+
+ /*
+ * CNV burst mode: allocate an internal trigger driven by the
+ * DATA_READY IRQ on the GP pin.
+ */
+ 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);
+
+ /*
+ * The GP pin named in interrupt-names asserts at end-of-conversion.
+ * The IRQ handler 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 || irq == -EPROBE_DEFER)
+ 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;
+
+ /*
+ * The handler only calls disable_irq_nosync() and iio_trigger_poll(),
+ * both safe in hardirq context, so register as a hard IRQ handler.
+ * IRQF_NO_AUTOEN keeps it disabled until the buffer is enabled.
+ */
+ ret = devm_request_irq(dev, irq, ad4691_irq, 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;
@@ -754,11 +1305,11 @@ static int ad4691_probe(struct spi_device *spi)
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;
+ 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 v12 4/6] iio: adc: ad4691: add SPI offload support
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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: <20260519-ad4692-multichannel-sar-adc-driver-v12-0-5b335162aa51@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 SPI Engine assembles received bits into native 16-bit words before
DMA, so offload samples land in CPU-native byte order (IIO_CPU).
Dedicated channel arrays (AD4691_OFFLOAD_CHANNEL) reflect this: they
omit IIO_BE and carry no soft timestamp (DMA delivers data directly to
userspace). The software triggered-buffer path retains its IIO_BE
channels because bits_per_word=8 causes SPI to deliver bytes MSB-first
into memory, making the on-disk layout big-endian. Both paths use
storagebits=16 as transfers are 16 bits wide in both cases.
IIO_BUFFER_DMAENGINE is selected because the offload path uses
devm_iio_dmaengine_buffer_setup_with_handle() to allocate and
attach the DMA RX buffer to the IIO device.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/Kconfig | 2 +
drivers/iio/adc/ad4691.c | 444 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 443 insertions(+), 3 deletions(-)
diff --git a/drivers/iio/adc/Kconfig b/drivers/iio/adc/Kconfig
index 1d7afde108c0..4d12eeb08020 100644
--- a/drivers/iio/adc/Kconfig
+++ b/drivers/iio/adc/Kconfig
@@ -149,8 +149,10 @@ config AD4691
depends on SPI
depends on REGULATOR || COMPILE_TEST
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 ed60ed5b488c..a6588292f3c1 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -10,6 +10,7 @@
#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/kstrtox.h>
@@ -24,11 +25,15 @@
#include <linux/reset.h>
#include <linux/string.h>
#include <linux/spi/spi.h>
+#include <linux/spi/offload/consumer.h>
+#include <linux/spi/offload/provider.h>
#include <linux/types.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>
@@ -44,6 +49,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))
@@ -118,6 +128,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) \
@@ -139,6 +150,30 @@ struct ad4691_chip_info {
}, \
}
+/*
+ * Offload path (bits_per_word=16): the SPI Engine assembles received
+ * bits into native 16-bit words before DMA, so samples are in
+ * CPU-native byte order (IIO_CPU). storagebits=16 matches the 16-bit
+ * DMA word size.
+ */
+#define AD4691_OFFLOAD_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 = { \
+ .format = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
static const struct iio_chan_spec ad4691_channels[] = {
AD4691_CHANNEL(0),
AD4691_CHANNEL(1),
@@ -171,6 +206,40 @@ static const struct iio_chan_spec ad4693_channels[] = {
IIO_CHAN_SOFT_TIMESTAMP(8),
};
+/*
+ * Offload channel arrays: no IIO_CHAN_SOFT_TIMESTAMP because DMA delivers
+ * data directly to userspace without a software timestamp.
+ */
+static const struct iio_chan_spec ad4691_offload_channels[] = {
+ AD4691_OFFLOAD_CHANNEL(0),
+ AD4691_OFFLOAD_CHANNEL(1),
+ AD4691_OFFLOAD_CHANNEL(2),
+ AD4691_OFFLOAD_CHANNEL(3),
+ AD4691_OFFLOAD_CHANNEL(4),
+ AD4691_OFFLOAD_CHANNEL(5),
+ AD4691_OFFLOAD_CHANNEL(6),
+ AD4691_OFFLOAD_CHANNEL(7),
+ AD4691_OFFLOAD_CHANNEL(8),
+ AD4691_OFFLOAD_CHANNEL(9),
+ AD4691_OFFLOAD_CHANNEL(10),
+ AD4691_OFFLOAD_CHANNEL(11),
+ AD4691_OFFLOAD_CHANNEL(12),
+ AD4691_OFFLOAD_CHANNEL(13),
+ AD4691_OFFLOAD_CHANNEL(14),
+ AD4691_OFFLOAD_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_offload_channels[] = {
+ AD4691_OFFLOAD_CHANNEL(0),
+ AD4691_OFFLOAD_CHANNEL(1),
+ AD4691_OFFLOAD_CHANNEL(2),
+ AD4691_OFFLOAD_CHANNEL(3),
+ AD4691_OFFLOAD_CHANNEL(4),
+ AD4691_OFFLOAD_CHANNEL(5),
+ AD4691_OFFLOAD_CHANNEL(6),
+ AD4691_OFFLOAD_CHANNEL(7),
+};
+
static const struct ad4691_channel_info ad4691_sw_info = {
.channels = ad4691_channels,
.num_channels = ARRAY_SIZE(ad4691_channels),
@@ -181,6 +250,16 @@ 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_offload_channels,
+ .num_channels = ARRAY_SIZE(ad4691_offload_channels),
+};
+
+static const struct ad4691_channel_info ad4693_offload_info = {
+ .channels = ad4693_offload_channels,
+ .num_channels = ARRAY_SIZE(ad4693_offload_channels),
+};
+
/*
* 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
@@ -211,24 +290,28 @@ 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_state {
@@ -250,6 +333,10 @@ struct ad4691_state {
* atomicity of consecutive SPI operations.
*/
struct mutex lock;
+ /* NULL when no SPI offload hardware is present. */
+ struct spi_offload *offload;
+ struct spi_offload_trigger *offload_trigger;
+ u64 trigger_hz;
/*
* Per-buffer-enable lifetime resources:
* Manual Mode - a pre-built SPI message that clocks out N+1
@@ -264,8 +351,11 @@ struct ad4691_state {
struct spi_transfer scan_xfers[34];
/*
* CNV burst: 16 AVG_IN addresses = 16. Manual: 16 channel cmds +
- * 1 NOOP = 17. Stored as native u16; put_unaligned_be16() fills each
- * slot so the SPI controller (bits_per_word=8) sends bytes MSB-first.
+ * 1 NOOP = 17. Stored as native u16. The non-offload path fills slots
+ * with put_unaligned_be16() (bits_per_word=8, bytes go out in memory
+ * order). The offload path assigns native values directly
+ * (bits_per_word=bpw, SPI reads each slot as a native 16-bit word and
+ * shifts it out MSB-first).
*/
u16 scan_tx[17] __aligned(IIO_DMA_MINALIGN);
/*
@@ -300,6 +390,45 @@ 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 || args[0] > 3)
+ 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;
@@ -876,6 +1005,218 @@ 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 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));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ /*
+ * 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.
+ * scan_tx is reused for TX commands (mutually exclusive with the
+ * non-offload triggered-buffer path).
+ *
+ * bits_per_word=bpw: the SPI controller reads tx_buf as a native
+ * 16-bit word and shifts it out MSB-first. Store the exact 16-bit
+ * value we want on the wire as a plain native u16 — no endianness
+ * macro — so the wire bytes are correct on both LE and BE hosts.
+ * The channel-select command is a single byte; shift it to the MSB
+ * position so SPI sends it first, with a zero pad in the LSB.
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ st->scan_tx[k] = AD4691_ADC_CHAN(bit) << 8;
+ st->scan_xfers[k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[k].len = sizeof(*st->scan_tx);
+ 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 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].len = sizeof(*st->scan_tx);
+ 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 = st->offload;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ config.periodic.frequency_hz = st->trigger_hz;
+ ret = spi_offload_trigger_enable(st->offload, st->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);
+
+ spi_offload_trigger_disable(st->offload, st->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 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, std_seq_config;
+ unsigned int bit, k;
+ int ret;
+
+ std_seq_config = bitmap_read(indio_dev->active_scan_mask, 0,
+ iio_get_masklength(indio_dev)) & GENMASK(15, 0);
+ ret = regmap_write(st->regmap, AD4691_STD_SEQ_CONFIG, std_seq_config);
+ if (ret)
+ return ret;
+
+ acc_mask = ~std_seq_config & 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));
+ memset(st->scan_tx, 0, sizeof(st->scan_tx));
+
+ /*
+ * Each AVG_IN register read uses two transfers:
+ * TX: [reg_hi | 0x80, reg_lo] (address phase, CS stays asserted)
+ * RX: [data_hi, data_lo] (bpw-wide data phase, storagebits=16)
+ * Both TX and RX use bits_per_word=bpw: the SPI controller reads tx_buf
+ * as a native 16-bit word and shifts it out MSB-first. Store the exact
+ * 16-bit wire value as a plain native u16 — no endianness macro — so the
+ * wire bytes are correct on both LE and BE hosts. The read-address
+ * (0x8000 | reg) is already the 16-bit value we want on the wire.
+ * scan_tx is reused for TX addresses (mutually exclusive with the
+ * non-offload triggered-buffer path).
+ */
+ k = 0;
+ iio_for_each_active_channel(indio_dev, bit) {
+ st->scan_tx[k] = 0x8000 | AD4691_AVG_IN(bit);
+
+ /* TX: address phase, CS stays asserted into data phase */
+ st->scan_xfers[2 * k].tx_buf = &st->scan_tx[k];
+ st->scan_xfers[2 * k].len = sizeof(*st->scan_tx);
+ 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(*st->scan_tx);
+ 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: single 4-byte write [addr_hi, addr_lo, STATE_RESET_ALL,
+ * OSC_EN=1]. ADDR_DESCENDING writes byte[3]=1 to OSC_EN_REG (0x180) as
+ * a deliberate side-write, keeping the oscillator enabled.
+ * scan_tx_reset is shared with the non-offload path (len=4 here vs
+ * len=3 there) since the two paths are mutually exclusive at probe.
+ */
+ put_unaligned_be16(AD4691_STATE_RESET_REG, st->scan_tx_reset);
+ st->scan_tx_reset[2] = AD4691_STATE_RESET_ALL;
+ st->scan_tx_reset[3] = 1;
+ st->scan_xfers[2 * k].tx_buf = st->scan_tx_reset;
+ st->scan_xfers[2 * k].len = sizeof(st->scan_tx_reset);
+ /*
+ * 4-byte u8 buffer assembled with put_unaligned_be16(); leave
+ * bits_per_word at the default (8) so bytes go out in memory order.
+ */
+
+ spi_message_init_with_transfers(&st->scan_msg, st->scan_xfers, 2 * k + 1);
+ st->scan_msg.offload = st->offload;
+
+ ret = spi_optimize_message(spi, &st->scan_msg);
+ if (ret)
+ goto err_exit_conversion;
+
+ ret = spi_offload_trigger_enable(st->offload, st->offload_trigger, &config);
+ if (ret)
+ goto err_unoptimize;
+
+ ret = ad4691_sampling_enable(st, true);
+ if (ret)
+ goto err_disable_trigger;
+
+ return 0;
+
+err_disable_trigger:
+ spi_offload_trigger_disable(st->offload, st->offload_trigger);
+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);
+
+ ad4691_sampling_enable(st, false);
+ spi_offload_trigger_disable(st->offload, st->offload_trigger);
+ 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)
@@ -883,6 +1224,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", READ_ONCE(st->trigger_hz));
+
return sysfs_emit(buf, "%lu\n", NSEC_PER_SEC / st->cnv_period_ns);
}
@@ -903,6 +1247,20 @@ 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;
+
+ WRITE_ONCE(st->trigger_hz, config.periodic.frequency_hz);
+ return len;
+ }
+
ret = ad4691_set_pwm_freq(st, freq);
if (ret)
return ret;
@@ -1266,9 +1624,77 @@ 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 dma_chan *rx_dma;
+ int ret;
+
+ st->offload = spi_offload;
+
+ indio_dev->channels = st->info->offload_info->channels;
+ indio_dev->num_channels = st->info->offload_info->num_channels;
+ /*
+ * Offload path uses DMA directly; no IIO trigger is involved, so
+ * external triggers are not restricted (no validate_trigger).
+ */
+ indio_dev->info = &ad4691_manual_info;
+
+ if (st->manual_mode) {
+ st->offload_trigger =
+ devm_spi_offload_trigger_get(dev, st->offload,
+ SPI_OFFLOAD_TRIGGER_PERIODIC);
+ if (IS_ERR(st->offload_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->offload_trigger),
+ "Failed to get periodic offload trigger\n");
+
+ st->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");
+
+ st->offload_trigger =
+ devm_spi_offload_trigger_get(dev, st->offload,
+ SPI_OFFLOAD_TRIGGER_DATA_READY);
+ if (IS_ERR(st->offload_trigger))
+ return dev_err_probe(dev, PTR_ERR(st->offload_trigger),
+ "Failed to get DATA_READY offload trigger\n");
+ }
+
+ rx_dma = devm_spi_offload_rx_stream_request_dma_chan(dev, st->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;
@@ -1304,10 +1730,20 @@ 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->modes = INDIO_DIRECT_MODE;
- ret = ad4691_setup_triggered_buffer(indio_dev, st);
+ if (spi_offload)
+ ret = ad4691_setup_offload(indio_dev, st, spi_offload);
+ else
+ ret = ad4691_setup_triggered_buffer(indio_dev, st);
if (ret)
return ret;
@@ -1345,3 +1781,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 v12 5/6] iio: adc: ad4691: add oversampling support
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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: <20260519-ad4692-multichannel-sar-adc-driver-v12-0-5b335162aa51@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.
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 precomputed at probe for
each OSR value, listing only oscillator table entries that divide
evenly by that OSR, expressed as effective rates (osc_freq / osr[N]).
The list becomes sparser as OSR increases, capping at max_rate / osr[N].
read_avail picks the precomputed list for the channel's current OSR,
making the returned pointer stable and race-free.
Writing oversampling_ratio stores the new OSR for that channel and snaps
target_osc_freq_Hz to the largest oscillator table entry that is both
<= old_effective_rate * new_osr and evenly divisible by new_osr. This
preserves an integer read-back of in_voltageN_sampling_frequency after
the OSR change while keeping the oscillator as close as possible to the
previous effective rate.
OSR defaults to 1 (no accumulation) for all channels.
Signed-off-by: Radu Sabau <radu.sabau@analog.com>
---
drivers/iio/adc/ad4691.c | 388 ++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 351 insertions(+), 37 deletions(-)
diff --git a/drivers/iio/adc/ad4691.c b/drivers/iio/adc/ad4691.c
index a6588292f3c1..4b32f50d2176 100644
--- a/drivers/iio/adc/ad4691.c
+++ b/drivers/iio/adc/ad4691.c
@@ -121,6 +121,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;
};
@@ -131,12 +132,39 @@ 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_SAMP_FREQ), \
+ .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 = { \
+ .format = '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, \
+ .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), \
@@ -155,8 +183,33 @@ struct ad4691_chip_info {
* bits into native 16-bit words before DMA, so samples are in
* CPU-native byte order (IIO_CPU). storagebits=16 matches the 16-bit
* DMA word size.
+ *
+ * CNV burst offload configures ACC_DEPTH_IN per channel, so the
+ * oversampling_ratio attribute is exposed. Manual offload does not;
+ * use AD4691_OFFLOAD_MANUAL_CHANNEL for that path.
*/
#define AD4691_OFFLOAD_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 = { \
+ .format = 'u', \
+ .realbits = 16, \
+ .storagebits = 16, \
+ }, \
+ }
+
+/* Manual offload — same IIO_CPU layout but no oversampling_ratio attribute. */
+#define AD4691_OFFLOAD_MANUAL_CHANNEL(ch) \
{ \
.type = IIO_VOLTAGE, \
.indexed = 1, \
@@ -240,23 +293,91 @@ static const struct iio_chan_spec ad4693_offload_channels[] = {
AD4691_OFFLOAD_CHANNEL(7),
};
+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 struct iio_chan_spec ad4691_offload_manual_channels[] = {
+ AD4691_OFFLOAD_MANUAL_CHANNEL(0),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(1),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(2),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(3),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(4),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(5),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(6),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(7),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(8),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(9),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(10),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(11),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(12),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(13),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(14),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(15),
+};
+
+static const struct iio_chan_spec ad4693_offload_manual_channels[] = {
+ AD4691_OFFLOAD_MANUAL_CHANNEL(0),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(1),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(2),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(3),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(4),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(5),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(6),
+ AD4691_OFFLOAD_MANUAL_CHANNEL(7),
+};
+
+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_offload_channels,
+ .manual_channels = ad4691_offload_manual_channels,
.num_channels = ARRAY_SIZE(ad4691_offload_channels),
};
static const struct ad4691_channel_info ad4693_offload_info = {
.channels = ad4693_offload_channels,
+ .manual_channels = ad4693_offload_manual_channels,
.num_channels = ARRAY_SIZE(ad4693_offload_channels),
};
@@ -323,6 +444,25 @@ 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[AD4691_MAX_CHANNELS];
+ /*
+ * Precomputed effective-rate lists, one row per entry in
+ * ad4691_oversampling_ratios[]. Populated at probe; read_avail picks
+ * the row for the channel's current OSR. The tables are stable after
+ * probe so returning a pointer into them from read_avail is race-free.
+ */
+ int samp_freq_avail[ARRAY_SIZE(ad4691_oversampling_ratios)][ARRAY_SIZE(ad4691_osc_freqs_Hz)];
+ int samp_freq_avail_len[ARRAY_SIZE(ad4691_oversampling_ratios)];
bool manual_mode;
bool irq_enabled;
@@ -588,50 +728,131 @@ 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;
-
- guard(mutex)(&st->lock);
-
- 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 ad4691_state *st, int freq)
+/*
+ * 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 start = ad4691_samp_freq_start(st->info);
- guard(mutex)(&st->lock);
-
for (unsigned int i = start; i < ARRAY_SIZE(ad4691_osc_freqs_Hz); i++) {
- if (ad4691_osc_freqs_Hz[i] != freq)
+ if ((unsigned int)ad4691_osc_freqs_Hz[i] > needed_osc)
continue;
- return regmap_update_bits(st->regmap, AD4691_OSC_FREQ_REG,
- AD4691_OSC_FREQ_MASK, i);
+ if (ad4691_osc_freqs_Hz[i] % osr)
+ continue;
+ return ad4691_osc_freqs_Hz[i];
}
+ return 0;
+}
+/* Write target_osc_freq_Hz to OSC_FREQ_REG. Called at use time. */
+static int ad4691_write_osc_freq(struct ad4691_state *st)
+{
+ for (unsigned int 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;
}
+/* Return the index of osr in ad4691_oversampling_ratios[], defaulting to 0. */
+static unsigned int ad4691_osr_index(unsigned int osr)
+{
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_oversampling_ratios) - 1; i++) {
+ if ((unsigned int)ad4691_oversampling_ratios[i] == osr)
+ return i;
+ }
+ return ARRAY_SIZE(ad4691_oversampling_ratios) - 1;
+}
+
+/*
+ * Precompute samp_freq_avail[][]: for each OSR value, list the oscillator
+ * table entries that divide evenly by that OSR, expressed as effective rates
+ * (osc_freq / osr). Called once at probe after st->info is set.
+ */
+static void ad4691_precompute_samp_freq_avail(struct ad4691_state *st)
+{
+ unsigned int start = ad4691_samp_freq_start(st->info);
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_oversampling_ratios); i++) {
+ unsigned int osr = ad4691_oversampling_ratios[i];
+ int n = 0;
+
+ for (unsigned int j = start; j < ARRAY_SIZE(ad4691_osc_freqs_Hz); j++) {
+ if (ad4691_osc_freqs_Hz[j] % osr)
+ continue;
+ st->samp_freq_avail[i][n++] = ad4691_osc_freqs_Hz[j] / osr;
+ }
+ st->samp_freq_avail_len[i] = n;
+ }
+}
+
+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 ad4691_state *st,
+ struct iio_chan_spec const *chan, int freq)
+{
+ unsigned int osr, found;
+
+ /*
+ * Read osr under st->lock: osr[chan] and target_osc_freq_Hz are
+ * modified together under the lock; reading after acquiring it ensures
+ * we see a consistent snapshot with no concurrent write racing us.
+ */
+ guard(mutex)(&st->lock);
+ osr = st->osr[chan->channel];
+
+ if (freq <= 0 || (unsigned int)freq > st->info->max_rate / osr)
+ 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,
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];
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ unsigned int osr_idx;
+
+ /*
+ * The precomputed tables are stable after probe; only the
+ * channel's current OSR needs to be read under the lock to
+ * pick the right row atomically.
+ */
+ guard(mutex)(&st->lock);
+ osr_idx = ad4691_osr_index(st->osr[chan->channel]);
+ *vals = st->samp_freq_avail[osr_idx];
*type = IIO_VAL_INT;
- *length = ARRAY_SIZE(ad4691_osc_freqs_Hz) - start;
+ *length = st->samp_freq_avail_len[osr_idx];
+ 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;
@@ -642,7 +863,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);
@@ -662,7 +883,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;
@@ -670,9 +896,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((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);
@@ -706,8 +935,21 @@ 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);
+ case IIO_CHAN_INFO_SAMP_FREQ: {
+ /*
+ * Read target_osc_freq_Hz and osr[chan] under st->lock to get a
+ * consistent snapshot: write_raw for SAMP_FREQ or OSR modifies
+ * both fields under the lock, so a concurrent read without the
+ * lock could observe a new oscillator frequency with the old OSR.
+ */
+ guard(mutex)(&st->lock);
+ return ad4691_get_sampling_freq(st, st->osr[chan->channel], val);
+ }
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
+ guard(mutex)(&st->lock);
+ *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;
@@ -729,7 +971,40 @@ static int ad4691_write_raw(struct iio_dev *indio_dev,
switch (mask) {
case IIO_CHAN_INFO_SAMP_FREQ:
- return ad4691_set_sampling_freq(st, val);
+ return ad4691_set_sampling_freq(st, chan, val);
+ case IIO_CHAN_INFO_OVERSAMPLING_RATIO: {
+ unsigned int old_effective, found;
+ bool valid = false;
+
+ for (unsigned int i = 0; i < ARRAY_SIZE(ad4691_oversampling_ratios); i++) {
+ if (ad4691_oversampling_ratios[i] == val) {
+ valid = true;
+ break;
+ }
+ }
+ if (!valid)
+ return -EINVAL;
+
+ /*
+ * Hold st->lock while computing the new oscillator frequency
+ * and updating both target_osc_freq_Hz and osr[chan] atomically:
+ * read_raw for SAMP_FREQ reads both fields under the lock and
+ * must see a consistent pair (new osc ↔ new osr).
+ *
+ * Snap target_osc_freq_Hz to the largest table entry that is
+ * both <= old_effective * new_osr and evenly divisible by
+ * new_osr, preserving an integer read-back of
+ * in_voltageN_sampling_frequency after the OSR change.
+ */
+ guard(mutex)(&st->lock);
+ old_effective = st->target_osc_freq_Hz / st->osr[chan->channel];
+ found = ad4691_find_osc_freq(st, old_effective * (unsigned int)val, val);
+ if (!found)
+ return -EINVAL;
+ st->target_osc_freq_Hz = found;
+ st->osr[chan->channel] = val;
+ return 0;
+ }
default:
return -EINVAL;
}
@@ -784,6 +1059,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)
@@ -943,6 +1222,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;
@@ -1122,6 +1407,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;
@@ -1536,11 +1827,15 @@ 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)
return dev_err_probe(dev, ret, "Failed to write ADC_SETUP\n");
+ ad4691_precompute_samp_freq_avail(st);
+
return 0;
}
@@ -1552,7 +1847,14 @@ static int ad4691_setup_triggered_buffer(struct iio_dev *indio_dev,
unsigned int i;
int irq, ret;
- indio_dev->channels = st->info->sw_info->channels;
+ /*
+ * Manual mode exposes channels without the oversampling_ratio attribute
+ * because ACC_DEPTH_IN is not configured in manual mode.
+ */
+ 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;
indio_dev->info = st->manual_mode ? &ad4691_manual_info : &ad4691_cnv_burst_info;
@@ -1634,7 +1936,18 @@ static int ad4691_setup_offload(struct iio_dev *indio_dev,
st->offload = spi_offload;
- indio_dev->channels = st->info->offload_info->channels;
+ /*
+ * CNV burst offload exposes oversampling_ratio (ACC_DEPTH_IN is
+ * configured per channel at buffer enable). Manual offload does not
+ * configure ACC_DEPTH_IN, so it uses a separate channel array
+ * without the oversampling_ratio attribute. Both paths use IIO_CPU
+ * (no .endianness annotation) because bits_per_word=16 causes the
+ * SPI Engine to produce native 16-bit DMA words.
+ */
+ 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;
/*
* Offload path uses DMA directly; no IIO trigger is involved, so
@@ -1708,6 +2021,7 @@ static int ad4691_probe(struct spi_device *spi)
st->info = spi_get_device_match_data(spi);
if (!st->info)
return -ENODEV;
+ memset(st->osr, 1, sizeof(st->osr));
ret = devm_mutex_init(dev, &st->lock);
if (ret)
--
2.43.0
^ permalink raw reply related
* [PATCH v12 6/6] docs: iio: adc: ad4691: add driver documentation
From: Radu Sabau via B4 Relay @ 2026-05-19 12:20 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: <20260519-ad4692-multichannel-sar-adc-driver-v12-0-5b335162aa51@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 | 226 +++++++++++++++++++++++++++++++++++++++++++
Documentation/iio/index.rst | 1 +
MAINTAINERS | 1 +
3 files changed, 228 insertions(+)
diff --git a/Documentation/iio/ad4691.rst b/Documentation/iio/ad4691.rst
new file mode 100644
index 000000000000..5ec77846e317
--- /dev/null
+++ b/Documentation/iio/ad4691.rst
@@ -0,0 +1,226 @@
+.. 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
+a 16-bit mean, so the buffer data type (shown in ``buffer0/in_voltageN_type``)
+is 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 and
+snaps the internal oscillator to the largest valid table entry that is both
+less than or equal to ``old_effective_rate × new_osr`` and evenly divisible
+by ``new_osr``. This preserves an integer read-back of
+``in_voltageN_sampling_frequency`` after the change and keeps the oscillator
+as close as possible to the previous effective rate.
+
+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): the GPIO line is
+ asserted for at least 300 µs then deasserted at probe.
+* **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";
+ ...
+ interrupt-parent = <&gpio0>;
+ interrupts = <17 IRQ_TYPE_LEVEL_HIGH>;
+ 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.
+
+
+Buffer data format
+==================
+
+The sample format in the IIO buffer depends on whether SPI offload is in use.
+
+Software triggered-buffer path (no SPI offload)
+------------------------------------------------
+
+Each active channel occupies one 16-bit big-endian slot (``storagebits=16``,
+``endianness=be``). Active channels are packed densely in scan-index order,
+followed by a 64-bit software timestamp appended by the IIO core.
+
+SPI offload path
+----------------
+
+Each active channel occupies one 16-bit CPU-native slot (``storagebits=16``,
+``endianness=cpu``). The SPI offload engine streams 16-bit words directly from
+the SPI Engine into the DMA buffer; no software timestamp is appended.
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 020c1ffae31b..3fbac296b667 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1488,6 +1488,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: Documentation/iio/ad4691.rst
F: drivers/iio/adc/ad4691.c
ANALOG DEVICES INC AD4695 DRIVER
--
2.43.0
^ permalink raw reply related
* Re: [PATCH] docs/filesystems/9p: fix broken external links
From: Dominique Martinet @ 2026-05-19 12:26 UTC (permalink / raw)
To: ericvh, Aayush Patil
Cc: lucho, linux_oss, corbet, skhan, v9fs, linux-doc, linux-kernel
In-Reply-To: <20260510182856.17569-1-aayushpatilsch@gmail.com>
Aayush Patil wrote on Sun, May 10, 2026 at 11:58:56PM +0530:
> The xcpu.org links for xcpu-talk, kvmfs, and cellfs-talk are dead
> with no archived snapshots available on the Wayback Machine, so
> remove them. The PROSE I/O link redirects to a dead server; replace
> it with an archived version from web.archive.org.S
(I assume the final S is a typo here)
Eric, it looks like you're the one who added these links, would you
happen to have a copy around if you care about keeping these?
Otherwise I'm not sure of the value of listing the papers without the
actual files available, but I don't mind either way.
I agree dead links are of little value though so will pick this up if
there's no reply in a while
>
> Signed-off-by: Aayush Patil <aayushpatilsch@gmail.com>
> ---
> Documentation/filesystems/9p.rst | 5 +----
> 1 file changed, 1 insertion(+), 4 deletions(-)
>
> diff --git a/Documentation/filesystems/9p.rst b/Documentation/filesystems/9p.rst
> index be3504ca034a..65809a1dad21 100644
> --- a/Documentation/filesystems/9p.rst
> +++ b/Documentation/filesystems/9p.rst
> @@ -23,13 +23,10 @@ the 9p client is available in the form of a USENIX paper:
> Other applications are described in the following papers:
>
> * XCPU & Clustering
> - http://xcpu.org/papers/xcpu-talk.pdf
I found http://mirtchovski.postnix.pw/p9/xcpu-talk.pdf but I'm not sure
if it's the same file
> * KVMFS: control file system for KVM
> - http://xcpu.org/papers/kvmfs.pdf
Looks close but perhaps not the same as
https://www.kernel.org/doc/ols/2007/ols2007v2-pages-59-64.pdf ?
> * CellFS: A New Programming Model for the Cell BE
> - http://xcpu.org/papers/cellfs-talk.pdf
Couldn't find anything fo this one
> * PROSE I/O: Using 9p to enable Application Partitions
> - http://plan9.escet.urjc.es/iwp9/cready/PROSE_iwp9_2006.pdf
> + http://web.archive.org/web/20110101152020/http://plan9.escet.urjc.es/iwp9/cready/PROSE_iwp9_2006.pdf
> * VirtFS: A Virtualization Aware File System pass-through
> https://kernel.org/doc/ols/2010/ols2010-pages-109-120.pdf
>
--
Dominique Martinet | Asmadeus
^ permalink raw reply
* Re: [PATCH 09/15] accel/qda: Add DMA-backed GEM objects and memory manager integration
From: Matthew Wilcox @ 2026-05-19 12:28 UTC (permalink / raw)
To: Markus Elfring
Cc: Ekansh Gupta, dri-devel, iommu, linux-media, linux-arm-msm,
linaro-mm-sig, Christian König, David Airlie,
Jörg Rödel, Jonathan Corbet, Maarten Lankhorst,
Maxime Ripard, Oded Gabbay, Robin Murphy, Shuah Khan,
Simona Vetter, Sumit Semwal, Thomas Zimmermann, linux-doc,
linux-kernel, Bharath Kumar, Bjorn Andersson, Chenna Kesava Raju,
Dmitry Baryshkov, Konrad Dybcio, Rob Clark, Srinivas Kandagatla,
Will Deacon
In-Reply-To: <5e0d72fa-929a-4905-9066-6648892bef4a@web.de>
Feel free to ignore everything Markus says.
On Tue, May 19, 2026 at 02:14:34PM +0200, Markus Elfring wrote:
> …
> > Assisted-by: Claude:claude-4-6-sonnet
> …
>
> Did such an information source gather the knowledge to benefit more
> from the application of scope-based resource management?
>
>
> …
> > +++ b/drivers/accel/qda/qda_drv.c
> …
> > @@ -32,6 +33,18 @@ static void qda_postclose(struct drm_device *dev, struct drm_file *file)
> > {
> …
> > + if (refcount_dec_and_test(&iommu_dev->refcount)) {
> > + spin_lock_irqsave(&iommu_dev->lock, flags);
> > + iommu_dev->assigned_pid = 0;
> > + iommu_dev->assigned_file_priv = NULL;
> > + spin_unlock_irqrestore(&iommu_dev->lock, flags);
> > + }
> …
>
> Under which circumstances would you become interested to apply a statement
> like “guard(spinlock_irqsave)(&iommu_dev->lock);”?
> https://elixir.bootlin.com/linux/v7.1-rc4/source/include/linux/spinlock.h#L619-L622
>
> Regards,
> Markus
>
^ permalink raw reply
* Re: [PATCH 09/15] accel/qda: Add DMA-backed GEM objects and memory manager integration
From: Markus Elfring @ 2026-05-19 12:32 UTC (permalink / raw)
To: Matthew Wilcox, dri-devel, iommu, linux-media, linux-arm-msm,
linaro-mm-sig
Cc: Ekansh Gupta, Christian König, David Airlie,
Jörg Rödel, Jonathan Corbet, Maarten Lankhorst,
Maxime Ripard, Oded Gabbay, Robin Murphy, Shuah Khan,
Simona Vetter, Sumit Semwal, Thomas Zimmermann, linux-doc,
linux-kernel, Bharath Kumar, Bjorn Andersson, Chenna Kesava Raju,
Dmitry Baryshkov, Konrad Dybcio, Rob Clark, Srinivas Kandagatla,
Will Deacon
In-Reply-To: <agxXc8ttEzBFOlE2@casper.infradead.org>
> Feel free to ignore everything Markus says.
Will any contributors care more for linked information sources
(in constructive ways)?
Regards,
Markus
^ permalink raw reply
* Re: [PATCH] Documentation: KVM: Document guest-visible compatibility expectations
From: Marc Zyngier @ 2026-05-19 12:38 UTC (permalink / raw)
To: Paolo Bonzini
Cc: David Woodhouse, Will Deacon, Jonathan Corbet, Shuah Khan, kvm,
Linux Doc Mailing List, Kernel Mailing List, Linux,
Sean Christopherson, Jim Mattson, Oliver Upton, Joey Gouly,
Suzuki K Poulose, Zenghui Yu, Catalin Marinas,
Raghavendra Rao Ananta, Eric Auger, Kees Cook, Arnd Bergmann,
Nathan Chancellor, linux-arm-kernel, kvmarm, linux-kselftest
In-Reply-To: <CABgObfacAYexR25SMi1kSZMRnHx3EDGj8=E84V1DumER66ibnQ@mail.gmail.com>
On Tue, 19 May 2026 13:13:41 +0100,
Paolo Bonzini <pbonzini@redhat.com> wrote:
>
> On Tue, May 19, 2026 at 1:44 PM David Woodhouse <dwmw2@infradead.org> wrote:
> > > > So... what next? Is one of the other KVM/arm64 maintainers going to
> > > > speak up? Paolo would you consider taking the fixes through your tree
> > > > directly?
>
> I admit that my knowledge of Arm is really limited, and I do not
> understand which IIDR values have architecturally allowed behaviors
> and which (if any) were made up by KVM; but even if I cannot honestly
> remark on the code or even the approach, a compatibility knob is the
> right thing to have. That's a userspace API design matter, not an Arm
> or GIC matter.
I agree that we can have the knob -- not having it is a userspace
issue, and I have said that I was OK with preserving the userspace
interface.
>
> I hope that Marc provides a better explanation of why he believes
> https://lore.kernel.org/all/20260511113558.3325004-2-dwmw2@infradead.org/
> shouldn't be accepted, because I am more than a bit puzzled about
> *why* that patch is being rejected or (in v3) so far ignored. Marc in
> this thread wrote: "If userspace is not a total joke, it will read all
> the ID registers, and configure what it wants to see, assuming it is a
> feature that can be configured (not everything can, because the
> architecture itself is not fully backward compatible)".
This was a more general comment on the full mechanism that we use to
save/restore the state and at the same time configure the feature
set. Which is what the GICD_IIDR does to some extent for the GIC.
> But in this case there's an ID register that tells KVM if userspace
> wants the old or the new behavior, independent of whether that old
> behavior is architecturally valid or not.
But the "old behaviour" makes no sense, and cannot be used by a guest:
- either the guest doesn't use the alternative interrupt groups, then
it wasn't affected by the bug. That's 100% of the guests.
- or the guest did try to use the alternative groups, and it *NEVER*
worked, as it wouldn't get any interrupt at all. What is the point
of preserving a "feature" that only results in a non-working guest?
Given that, re-introducing a behaviour that cannot be used makes zero
sense to me.
> I will certainly take this patch, but I won't override Marc. However
> I'd like to better understand his point of view, because right now I
> just don't get it.
I don't get it either, but for different reasons.
>
> > If KVM on arm64 doesn't aspire to maintain guest compatibility across
> > host kernel changes — regardless of whether the previous kernel's
> > behaviour was "blessed" by the architecture specification or not — then
> > it does not meet the expectation that we have of KVM implementations in
> > the Linux kernel.
>
> I agree with the "aspire" wording. Even if it's not going to be 100%
> achievable, KVM *needs* to aspire to maintain both guest compatibility
> and architecture precision. Sometimes it's impossible, sometimes there
> are constraints that require you to trade off one for another (e.g.
> via quirks, or by breaking behavior that no sane guest would have
> cared about). But in general as a maintainer you don't *get* to
> choose.
>
> Paolo
>
> > Or indeed the standards that we've held for Linux kernel ABIs for the
> > last 35 years.
As I said before, I'd be OK with something that would restore IIDR to
REV1. But not something that actively breaks the GIC emulation by
reintroducing a bug. That's, by construction, dead code that will only
bitrot, because there is no SW that can make use of this nonsense.
M.
--
Without deviation from the norm, progress is not possible.
^ permalink raw reply
* Re: [PATCH v13 04/15] arm64: kexec_file: Fix potential buffer overflow in prepare_elf_headers()
From: Jinjie Ruan @ 2026-05-19 12:42 UTC (permalink / raw)
To: Breno Leitao
Cc: corbet, skhan, catalin.marinas, will, chenhuacai, kernel, maddy,
mpe, npiggin, chleroy, pjw, palmer, aou, alex, tglx, mingo, bp,
dave.hansen, hpa, robh, saravanak, akpm, bhe, rppt,
pasha.tatashin, pratyush, ruirui.yang, rdunlap, pmladek,
dapeng1.mi, kees, elver, kuba, ebiggers, lirongqing, paulmck,
sourabhjain, coxu, jbohac, ryan.roberts, osandov, cfsworks,
tangyouling, ritesh.list, adityag, guoren, songshuaishuai,
kevin.brodsky, vishal.moola, junhui.liu, wangruikang, namcao,
chao.gao, seanjc, fuqiang.wang, ardb, chenjiahao16, hbathini,
takahiro.akashi, james.morse, lizhengyu3, x86, linux-doc,
linux-kernel, linux-arm-kernel, loongarch, linuxppc-dev,
linux-riscv, devicetree, kexec
In-Reply-To: <agGkvrg06KNDNfDi@gmail.com>
On 5/11/2026 5:46 PM, Breno Leitao wrote:
> On Mon, May 11, 2026 at 11:04:43AM +0800, Jinjie Ruan wrote:
>> There is a race condition between the kexec_load() system call
>> (crash kernel loading path) and memory hotplug operations that can
>> lead to buffer overflow and potential kernel crash.
>>
>> During prepare_elf_headers(), the following steps occur:
>> 1. The first for_each_mem_range() queries current System RAM memory ranges
>> 2. Allocates buffer based on queried count
>> 3. The 2st for_each_mem_range() populates ranges from memblock
>>
>> If memory hotplug occurs between step 1 and step 3, the number of ranges
>> can increase, causing out-of-bounds write when populating cmem->ranges[].
>>
>> This happens because kexec_load() uses kexec_trylock (atomic_t) while
>> memory hotplug uses device_hotplug_lock (mutex), so they don't serialize
>> with each other.
>>
>> Add the explicit bounds checking to prevent out-of-bounds access.
>
> It seems you have a TOCTOU type of issue, and this seems to be shrinking
> the window, but not fully solving it?
I plan to fix this issue as follows, and would appreciate your feedback
on whether this is reasonable.
Sashiko AI code review pointed out there is a TOCTOU (Time-of-Check to
Time-of-Use) race condition in prepare_elf_headers() between the initial
pass that counts System RAM ranges and the second pass that populates them.
If a memory hotplug event occurs between these two steps, the number of
memory regions may increase, causing an out-of-bounds write to
the cmem->ranges[] array.
To resolve this and ensure data consistency, this patch:
1. Wraps the counting and population passes with get_online_mems() and
crash_hotplug_lock(). This serializes the kexec_file_load() path
with concurrent memory hotplug operations, ensuring the memory
map remains consistent throughout the header preparation.
2. Adds an explicit boundary check in prepare_elf64_ram_headers_callback().
If the number of ranges exceeds the allocated maximum, it now returns
-EAGAIN, which indicates a transient race, signaling userspace
kexec-tools to retry the syscall instead of leaving the system
without a loaded crash kernel.
index daf81a873bbd..546be6261177 100644
--- a/arch/arm64/kernel/machine_kexec_file.c
+++ b/arch/arm64/kernel/machine_kexec_file.c
@@ -15,6 +15,7 @@
#include <linux/kexec.h>
#include <linux/libfdt.h>
#include <linux/memblock.h>
+#include <linux/memory_hotplug.h>
#include <linux/of.h>
#include <linux/of_fdt.h>
#include <linux/slab.h>
@@ -40,7 +41,7 @@ int arch_kimage_file_post_load_cleanup(struct kimage
*image)
}
#ifdef CONFIG_CRASH_DUMP
-int prepare_elf_headers(void **addr, unsigned long *sz)
+static int __prepare_elf_headers(void **addr, unsigned long *sz)
{
struct crash_mem *cmem;
unsigned int nr_ranges;
@@ -59,6 +60,11 @@ int prepare_elf_headers(void **addr, unsigned long *sz)
cmem->max_nr_ranges = nr_ranges;
cmem->nr_ranges = 0;
for_each_mem_range(i, &start, &end) {
+ if (cmem->nr_ranges >= cmem->max_nr_ranges) {
+ ret = -EAGAIN;
+ goto out;
+ }
+
cmem->ranges[cmem->nr_ranges].start = start;
cmem->ranges[cmem->nr_ranges].end = end - 1;
cmem->nr_ranges++;
@@ -81,6 +87,21 @@ int prepare_elf_headers(void **addr, unsigned long *sz)
kfree(cmem);
return ret;
}
+
+int prepare_elf_headers(void **addr, unsigned long *sz)
+{
+ int ret;
+
+ crash_hotplug_lock();
+ get_online_mems();
+
+ ret = __prepare_elf_headers(addr, sz);
+
+ put_online_mems();
+ crash_hotplug_unlock();
+
+ return ret;
+}
#endif
>
>> Cc: Catalin Marinas <catalin.marinas@arm.com>
>> Cc: Will Deacon <will.deacon@arm.com>
>> Cc: Andrew Morton <akpm@linux-foundation.org>
>> Cc: Baoquan He <bhe@redhat.com>
>> Cc: Breno Leitao <leitao@debian.org>
>> Cc: stable@vger.kernel.org
>> Fixes: 3751e728cef2 ("arm64: kexec_file: add crash dump support")
>> Closes: https://sashiko.dev/#/patchset/20260323072745.2481719-1-ruanjinjie%40huawei.com
>> Signed-off-by: Jinjie Ruan <ruanjinjie@huawei.com>
>> ---
>> arch/arm64/kernel/machine_kexec_file.c | 5 +++++
>> 1 file changed, 5 insertions(+)
>>
>> diff --git a/arch/arm64/kernel/machine_kexec_file.c b/arch/arm64/kernel/machine_kexec_file.c
>> index e31fabed378a..a67e7b1abbab 100644
>> --- a/arch/arm64/kernel/machine_kexec_file.c
>> +++ b/arch/arm64/kernel/machine_kexec_file.c
>> @@ -59,6 +59,11 @@ static int prepare_elf_headers(void **addr, unsigned long *sz)
>> cmem->max_nr_ranges = nr_ranges;
>> cmem->nr_ranges = 0;
>> for_each_mem_range(i, &start, &end) {
>> + if (cmem->nr_ranges >= cmem->max_nr_ranges) {
>> + ret = -ENOMEM;
>
> -ENOMEM seems to be the the wrong errno. This isn't an allocation
> failure; it's a transient race. -EBUSY or -EAGAIN would be more honest
^ 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