* Re: [PATCH v7 20/42] KVM: SEV: Make 'uaddr' parameter optional for KVM_SEV_SNP_LAUNCH_UPDATE
From: Suzuki K Poulose @ 2026-06-04 15:29 UTC (permalink / raw)
To: ackerleytng, aik, andrew.jones, binbin.wu, brauner, chao.p.peng,
david, ira.weiny, jmattson, jthoughton, michael.roth, oupton,
pankaj.gupta, qperret, rick.p.edgecombe, rientjes, shivankg,
steven.price, tabba, willy, wyihan, yan.y.zhao, forkloop,
pratyush, aneesh.kumar, liam, Paolo Bonzini, Sean Christopherson,
Thomas Gleixner, Ingo Molnar, Borislav Petkov, Dave Hansen, x86,
H. Peter Anvin, Steven Rostedt, Masami Hiramatsu,
Mathieu Desnoyers, Jonathan Corbet, Shuah Khan, Shuah Khan,
Vishal Annapurve, Andrew Morton, Chris Li, Kairui Song,
Kemeng Shi, Nhat Pham, Baoquan He, Barry Song, Axel Rasmussen,
Yuanchu Xie, Wei Xu, Youngjun Park, Qi Zheng, Shakeel Butt,
Kiryl Shutsemau, Jason Gunthorpe, Vlastimil Babka
Cc: kvm, linux-kernel, linux-trace-kernel, linux-doc, linux-kselftest,
linux-mm, linux-coco
In-Reply-To: <20260522-gmem-inplace-conversion-v7-20-2f0fae496530@google.com>
On 23/05/2026 01:18, Ackerley Tng via B4 Relay wrote:
> From: Michael Roth <michael.roth@amd.com>
>
> For vm_memory_attributes=1, in-place conversion/population is not
> supported, so the initial contents necessarily must need to come
> from a separate src address, which is enforced by the current
> implementation. However, for vm_memory_attributes=0, it is possible for
> guest memory to be initialized directly from userspace by mmap()'ing the
> guest_memfd and writing to it while the corresponding GPA ranges are in
> a 'shared' state before converting them to the 'private' state expected
> by KVM_SEV_SNP_LAUNCH_UPDATE.
>
> Update the handling/documentation for KVM_SEV_SNP_LAUNCH_UPDATE to allow
> for 'uaddr' to be set to NULL when vm_memory_attributes=0, which
> SNP_LAUNCH_UPDATE will then use to determine when it should/shouldn't
> copy in data from a separate memory location. Continue to enforce
> non-NULL for the original vm_memory_attributes=1 case.
>
> Signed-off-by: Michael Roth <michael.roth@amd.com>
> [Added src_page check in error handling path when the firmware command fails]
> [Dropped ifdef CONFIG_KVM_VM_MEMORY_ATTRIBUTES]
> Signed-off-by: Ackerley Tng <ackerleytng@google.com>
> ---
> Documentation/virt/kvm/x86/amd-memory-encryption.rst | 15 +++++++++++----
> arch/x86/kvm/svm/sev.c | 18 +++++++++++++-----
> virt/kvm/kvm_main.c | 1 +
> 3 files changed, 25 insertions(+), 9 deletions(-)
>
> diff --git a/Documentation/virt/kvm/x86/amd-memory-encryption.rst b/Documentation/virt/kvm/x86/amd-memory-encryption.rst
> index b2395dd4769de..43085f65b2d85 100644
> --- a/Documentation/virt/kvm/x86/amd-memory-encryption.rst
> +++ b/Documentation/virt/kvm/x86/amd-memory-encryption.rst
> @@ -503,7 +503,8 @@ secrets.
>
> It is required that the GPA ranges initialized by this command have had the
> KVM_MEMORY_ATTRIBUTE_PRIVATE attribute set in advance. See the documentation
> -for KVM_SET_MEMORY_ATTRIBUTES for more details on this aspect.
> +for KVM_SET_MEMORY_ATTRIBUTES/KVM_SET_MEMORY_ATTRIBUTES2 for more details on
> +this aspect.
>
> Upon success, this command is not guaranteed to have processed the entire
> range requested. Instead, the ``gfn_start``, ``uaddr``, and ``len`` fields of
> @@ -511,9 +512,15 @@ range requested. Instead, the ``gfn_start``, ``uaddr``, and ``len`` fields of
> remaining range that has yet to be processed. The caller should continue
> calling this command until those fields indicate the entire range has been
> processed, e.g. ``len`` is 0, ``gfn_start`` is equal to the last GFN in the
> -range plus 1, and ``uaddr`` is the last byte of the userspace-provided source
> -buffer address plus 1. In the case where ``type`` is KVM_SEV_SNP_PAGE_TYPE_ZERO,
> -``uaddr`` will be ignored completely.
> +range plus 1, and ``uaddr`` (if specified) is the last byte of the
> +userspace-provided source buffer address plus 1.
> +
> +In the case where ``type`` is KVM_SEV_SNP_PAGE_TYPE_ZERO, ``uaddr`` will be
> +ignored completely. Otherwise, ``uaddr`` is required if
> +kvm.vm_memory_attributes=1 and optional if kvm.vm_memory_attributes=0, since
> +in the latter case guest memory can be initialized directly from userspace
> +prior to converting it to private and passing the GPA range on to this
> +interface.
Just to confirm, so the sev_gmem_prepare doesn't destroy the contents in
the process of making it "private" ? i.e., the contents of a SNP shared
page are preserved while transitioning to "SNP Private" (via RMP
update).
Suzuki
>
> Parameters (in): struct kvm_sev_snp_launch_update
>
> diff --git a/arch/x86/kvm/svm/sev.c b/arch/x86/kvm/svm/sev.c
> index 1a361f08c7a3d..e1dbc827c2807 100644
> --- a/arch/x86/kvm/svm/sev.c
> +++ b/arch/x86/kvm/svm/sev.c
> @@ -2343,7 +2343,15 @@ static int sev_gmem_post_populate(struct kvm *kvm, gfn_t gfn, kvm_pfn_t pfn,
> int level;
> int ret;
>
> - if (WARN_ON_ONCE(sev_populate_args->type != KVM_SEV_SNP_PAGE_TYPE_ZERO && !src_page))
> + /*
> + * For vm_memory_attributes=1, in-place conversion/population is not
> + * supported, so the initial contents necessarily need to come from a
> + * separate src address. For vm_memory_attributes=0, this isn't
> + * necessarily the case, since the pages may have been populated
> + * directly from userspace before calling KVM_SEV_SNP_LAUNCH_UPDATE.
> + */
> + if (vm_memory_attributes &&
> + sev_populate_args->type != KVM_SEV_SNP_PAGE_TYPE_ZERO && !src_page)
> return -EINVAL;
>
> ret = snp_lookup_rmpentry((u64)pfn, &assigned, &level);
> @@ -2390,7 +2398,7 @@ static int sev_gmem_post_populate(struct kvm *kvm, gfn_t gfn, kvm_pfn_t pfn,
> */
> if (ret && !snp_page_reclaim(kvm, pfn) &&
> sev_populate_args->type == KVM_SEV_SNP_PAGE_TYPE_CPUID &&
> - sev_populate_args->fw_error == SEV_RET_INVALID_PARAM) {
> + sev_populate_args->fw_error == SEV_RET_INVALID_PARAM && src_page) {
> void *src_vaddr = kmap_local_page(src_page);
> void *dst_vaddr = kmap_local_pfn(pfn);
>
> @@ -2423,8 +2431,8 @@ static int snp_launch_update(struct kvm *kvm, struct kvm_sev_cmd *argp)
> if (copy_from_user(¶ms, u64_to_user_ptr(argp->data), sizeof(params)))
> return -EFAULT;
>
> - pr_debug("%s: GFN start 0x%llx length 0x%llx type %d flags %d\n", __func__,
> - params.gfn_start, params.len, params.type, params.flags);
> + pr_debug("%s: GFN start 0x%llx length 0x%llx type %d flags %d src %llx\n", __func__,
> + params.gfn_start, params.len, params.type, params.flags, params.uaddr);
>
> if (!params.len || !PAGE_ALIGNED(params.len) || params.flags ||
> (params.type != KVM_SEV_SNP_PAGE_TYPE_NORMAL &&
> @@ -2481,7 +2489,7 @@ static int snp_launch_update(struct kvm *kvm, struct kvm_sev_cmd *argp)
>
> params.gfn_start += count;
> params.len -= count * PAGE_SIZE;
> - if (params.type != KVM_SEV_SNP_PAGE_TYPE_ZERO)
> + if (src && params.type != KVM_SEV_SNP_PAGE_TYPE_ZERO)
> params.uaddr += count * PAGE_SIZE;
>
> if (copy_to_user(u64_to_user_ptr(argp->data), ¶ms, sizeof(params)))
> diff --git a/virt/kvm/kvm_main.c b/virt/kvm/kvm_main.c
> index ba195bb239aaa..3bf212fd99193 100644
> --- a/virt/kvm/kvm_main.c
> +++ b/virt/kvm/kvm_main.c
> @@ -105,6 +105,7 @@ module_param(allow_unsafe_mappings, bool, 0444);
> #ifdef CONFIG_KVM_VM_MEMORY_ATTRIBUTES
> bool vm_memory_attributes = true;
> module_param(vm_memory_attributes, bool, 0444);
> +EXPORT_SYMBOL_FOR_KVM_INTERNAL(vm_memory_attributes);
> #endif
> DEFINE_STATIC_CALL_RET0(__kvm_get_memory_attributes, kvm_get_memory_attributes_t);
> EXPORT_SYMBOL_FOR_KVM_INTERNAL(STATIC_CALL_KEY(__kvm_get_memory_attributes));
>
^ permalink raw reply
* Re: [PATCH v3] docs: pt_BR: update "Purpose of Defconfigs" section in maintainer-soc.rst
From: Jonathan Corbet @ 2026-06-04 15:37 UTC (permalink / raw)
To: Amanda Corrêa, Daniel Pereira; +Cc: linux-doc, Amanda Corrêa
In-Reply-To: <20260604031840.17236-1-amandacorreasilvax@gmail.com>
Amanda Corrêa <amandacorreasilvax@gmail.com> writes:
> This update includes the "Purpose of Defconfigs" section translated
> to Brazilian Portuguese.
>
> Signed-off-by: Amanda Corrêa <amandacorreasilvax@gmail.com>
> ---
> v2:
> - Adjust translation of section title to "Propósito dos Defconfigs"
> for better clarity in Portuguese.
> v3:
>
> - Fix plural agreement in section title
> - Clarify that the referenced device must be supported by upstream
>
>
> .../translations/pt_BR/process/maintainer-soc.rst | 12 +++++++++++-
> 1 file changed, 11 insertions(+), 1 deletion(-)
Some requests:
- Use scripts/get_maintainer.pl to create the CC list for your patches.
If you don't include the maintainer, your work may fall through the
cracks.
- Three versions of a patch in three hours is far too fast; wait for
review comments to come in before reposting, please.
Thanks,
jon
^ permalink raw reply
* Re: [PATCH v3 08/15] riscv: Add Zic64b to cpufeature and hwprobe
From: Andrew Jones @ 2026-06-04 15:48 UTC (permalink / raw)
To: Guodong Xu
Cc: Jonathan Corbet, Shuah Khan, Paul Walmsley, Palmer Dabbelt,
Albert Ou, Alexandre Ghiti, Zong Li, Deepak Gupta, Anup Patel,
Atish Patra, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Chen Wang, Inochi Amaoto, Conor Dooley, Shuah Khan,
Christian Brauner, linux-doc, linux-riscv, linux-kernel, kvm,
kvm-riscv, Paul Walmsley, Palmer Dabbelt, Conor Dooley,
devicetree, spacemit, sophgo, linux-kselftest, Charles Jenkins,
Samuel Holland
In-Reply-To: <20260603-rva23u64-hwprobe-v2-v3-8-5529a7b28384@gmail.com>
On Wed, Jun 03, 2026 at 07:12:03AM -0400, Guodong Xu wrote:
> Zic64b mandates 64-byte naturally aligned cache blocks and is a
> mandatory extension of the RVA22 and RVA23 profiles. Allocate a
> RISCV_ISA_EXT_ZIC64B id, parse "zic64b" from the ISA string with a
> validate callback that requires cbom/cbop/cboz cache block sizes of 64
> bytes, and export it through hwprobe.
>
> Signed-off-by: Guodong Xu <docular.xu@gmail.com>
> ---
> v3: New patch.
> ---
> Documentation/arch/riscv/hwprobe.rst | 3 +++
> arch/riscv/include/asm/hwcap.h | 1 +
> arch/riscv/include/uapi/asm/hwprobe.h | 1 +
> arch/riscv/kernel/cpufeature.c | 18 ++++++++++++++++++
> arch/riscv/kernel/sys_hwprobe.c | 1 +
> 5 files changed, 24 insertions(+)
>
> diff --git a/Documentation/arch/riscv/hwprobe.rst b/Documentation/arch/riscv/hwprobe.rst
> index 002d5046ab689..601e81f561421 100644
> --- a/Documentation/arch/riscv/hwprobe.rst
> +++ b/Documentation/arch/riscv/hwprobe.rst
> @@ -425,3 +425,6 @@ The following keys are defined:
> * :c:macro:`RISCV_HWPROBE_EXT_B`: The B extension is supported, as defined
> in version 1.0 of the Bit-Manipulation ISA extensions, and implies the
> presence of the Zba, Zbb, and Zbs sub-extensions.
Need a blank line here.
> + * :c:macro:`RISCV_HWPROBE_EXT_ZIC64B`: The Zic64b extension is supported,
> + as defined in the RISC-V Profiles specification starting from commit
> + b1d80660 ("Updated to ratified state.")
> diff --git a/arch/riscv/include/asm/hwcap.h b/arch/riscv/include/asm/hwcap.h
> index 58523b3a1998a..36572c1ff438a 100644
> --- a/arch/riscv/include/asm/hwcap.h
> +++ b/arch/riscv/include/asm/hwcap.h
> @@ -117,6 +117,7 @@
> #define RISCV_ISA_EXT_ZICCAMOA 107
> #define RISCV_ISA_EXT_ZICCIF 108
> #define RISCV_ISA_EXT_ZA64RS 109
> +#define RISCV_ISA_EXT_ZIC64B 110
>
> #define RISCV_ISA_EXT_XLINUXENVCFG 127
>
> diff --git a/arch/riscv/include/uapi/asm/hwprobe.h b/arch/riscv/include/uapi/asm/hwprobe.h
> index 430dc49a82863..36ec8ab470423 100644
> --- a/arch/riscv/include/uapi/asm/hwprobe.h
> +++ b/arch/riscv/include/uapi/asm/hwprobe.h
> @@ -122,6 +122,7 @@ struct riscv_hwprobe {
> #define RISCV_HWPROBE_EXT_ZICCRSE (1ULL << 4)
> #define RISCV_HWPROBE_EXT_ZA64RS (1ULL << 5)
> #define RISCV_HWPROBE_EXT_B (1ULL << 6)
> +#define RISCV_HWPROBE_EXT_ZIC64B (1ULL << 7)
>
> /* Increase RISCV_HWPROBE_MAX_KEY when adding items. */
>
> diff --git a/arch/riscv/kernel/cpufeature.c b/arch/riscv/kernel/cpufeature.c
> index e0197160af6dd..79ff431768139 100644
> --- a/arch/riscv/kernel/cpufeature.c
> +++ b/arch/riscv/kernel/cpufeature.c
> @@ -154,6 +154,23 @@ static int riscv_ext_zicbop_validate(const struct riscv_isa_ext_data *data,
> return 0;
> }
>
> +static int riscv_ext_zic64b_validate(const struct riscv_isa_ext_data *data,
> + const unsigned long *isa_bitmap)
> +{
> + /*
> + * Zic64b mandates 64-byte naturally aligned cache blocks; cross-check the
> + * cbom/cbop/cboz block-size device-tree properties to avoid inconsistency.
> + */
> + if (riscv_cbom_block_size != 64 ||
> + riscv_cbop_block_size != 64 ||
> + riscv_cboz_block_size != 64) {
> + pr_err("Zic64b detected in ISA string, disabling as the cache block size is not 64 bytes\n");
> + return -EINVAL;
> + }
> +
> + return 0;
> +}
> +
> static int riscv_ext_f_validate(const struct riscv_isa_ext_data *data,
> const unsigned long *isa_bitmap)
> {
> @@ -524,6 +541,7 @@ const struct riscv_isa_ext_data riscv_isa_ext[] = {
> __RISCV_ISA_EXT_SUPERSET(b, RISCV_ISA_EXT_B, riscv_b_exts),
> __RISCV_ISA_EXT_SUPERSET_VALIDATE(v, RISCV_ISA_EXT_V, riscv_v_exts, riscv_ext_vector_float_validate),
> __RISCV_ISA_EXT_DATA(h, RISCV_ISA_EXT_H),
> + __RISCV_ISA_EXT_DATA_VALIDATE(zic64b, RISCV_ISA_EXT_ZIC64B, riscv_ext_zic64b_validate),
> __RISCV_ISA_EXT_SUPERSET_VALIDATE(zicbom, RISCV_ISA_EXT_ZICBOM, riscv_xlinuxenvcfg_exts, riscv_ext_zicbom_validate),
> __RISCV_ISA_EXT_DATA_VALIDATE(zicbop, RISCV_ISA_EXT_ZICBOP, riscv_ext_zicbop_validate),
> __RISCV_ISA_EXT_SUPERSET_VALIDATE(zicboz, RISCV_ISA_EXT_ZICBOZ, riscv_xlinuxenvcfg_exts, riscv_ext_zicboz_validate),
> diff --git a/arch/riscv/kernel/sys_hwprobe.c b/arch/riscv/kernel/sys_hwprobe.c
> index dcc102bf8f183..3e80e5551ae0d 100644
> --- a/arch/riscv/kernel/sys_hwprobe.c
> +++ b/arch/riscv/kernel/sys_hwprobe.c
> @@ -211,6 +211,7 @@ static void hwprobe_isa_ext1(struct riscv_hwprobe *pair,
> EXT_KEY(isainfo->isa, ZICCRSE, pair->value, missing);
> EXT_KEY(isainfo->isa, ZA64RS, pair->value, missing);
> EXT_KEY(isainfo->isa, B, pair->value, missing);
> + EXT_KEY(isainfo->isa, ZIC64B, pair->value, missing);
> }
>
> /* Now turn off reporting features if any CPU is missing it. */
>
> --
> 2.43.0
>
Other than the missing blank line,
Reviewed-by: Andrew Jones <andrew.jones@oss.qualcomm.com>
^ permalink raw reply
* Re: [PATCH v3 02/15] riscv: hwprobe.rst: Document EXT_ZICFISS and EXT_ZICFILP
From: Andrew Jones @ 2026-06-04 16:01 UTC (permalink / raw)
To: Guodong Xu
Cc: Jonathan Corbet, Shuah Khan, Paul Walmsley, Palmer Dabbelt,
Albert Ou, Alexandre Ghiti, Zong Li, Deepak Gupta, Anup Patel,
Atish Patra, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Chen Wang, Inochi Amaoto, Conor Dooley, Shuah Khan,
Christian Brauner, linux-doc, linux-riscv, linux-kernel, kvm,
kvm-riscv, Paul Walmsley, Palmer Dabbelt, Conor Dooley,
devicetree, spacemit, sophgo, linux-kselftest, Charles Jenkins,
Samuel Holland
In-Reply-To: <20260603-rva23u64-hwprobe-v2-v3-2-5529a7b28384@gmail.com>
On Wed, Jun 03, 2026 at 07:11:57AM -0400, Guodong Xu wrote:
> Commit 30c3099036a9 ("riscv/hwprobe: add zicfilp / zicfiss
> enumeration in hwprobe") added RISCV_HWPROBE_EXT_ZICFISS and
> RISCV_HWPROBE_EXT_ZICFILP, but did not add matching entries to
> Documentation/arch/riscv/hwprobe.rst. Add them now.
>
> Fixes: 30c3099036a9 ("riscv/hwprobe: add zicfilp / zicfiss enumeration in hwprobe")
> Signed-off-by: Guodong Xu <docular.xu@gmail.com>
> ---
> v3:
> - Also document RISCV_HWPROBE_EXT_ZICFILP (bit 63 of IMA_EXT_0), the
> sibling enumeration added by the same commit (Andrew).
> v2: New patch.
> ---
> Documentation/arch/riscv/hwprobe.rst | 10 ++++++++++
> 1 file changed, 10 insertions(+)
>
> diff --git a/Documentation/arch/riscv/hwprobe.rst b/Documentation/arch/riscv/hwprobe.rst
> index a09a8f16bd16f..3cedaaa53f331 100644
> --- a/Documentation/arch/riscv/hwprobe.rst
> +++ b/Documentation/arch/riscv/hwprobe.rst
> @@ -289,6 +289,11 @@ The following keys are defined:
> defined in the RISC-V ISA manual starting from commit f88abf1 ("Integrating
> load/store pair for RV32 with the main manual") of the riscv-isa-manual.
>
> + * :c:macro:`RISCV_HWPROBE_EXT_ZICFILP`: The Zicfilp extension is supported,
> + as defined in version 1.0 of the RISC-V Control-flow Integrity (CFI)
> + extensions specification, ratified in commit ff03d8485a04 ("Update to
Can we use commit 302a2d45c243 instead since that one has the v1.0 tag?
> + ratified state") of riscv-cfi.
> +
> * :c:macro:`RISCV_HWPROBE_KEY_CPUPERF_0`: Deprecated. Returns similar values to
> :c:macro:`RISCV_HWPROBE_KEY_MISALIGNED_SCALAR_PERF`, but the key was
> mistakenly classified as a bitmask rather than a value.
> @@ -391,3 +396,8 @@ The following keys are defined:
> * :c:macro:`RISCV_HWPROBE_KEY_IMA_EXT_1`: A bitmask containing additional
> extensions that are compatible with the
> :c:macro:`RISCV_HWPROBE_BASE_BEHAVIOR_IMA`: base system behavior.
> +
> + * :c:macro:`RISCV_HWPROBE_EXT_ZICFISS`: The Zicfiss extension is supported,
> + as defined in version 1.0 of the RISC-V Control-flow Integrity (CFI)
> + extensions specification, ratified in commit ff03d8485a04 ("Update to
> + ratified state") of riscv-cfi.
>
> --
> 2.43.0
>
Otherwise,
Reviewed-by: Andrew Jones <andrew.jones@oss.qualcomm.com>
^ permalink raw reply
* Re: [PATCH v3 00/15] riscv: hwprobe: Expose RVA23U64 base behavior
From: Andrew Jones @ 2026-06-04 16:03 UTC (permalink / raw)
To: Guodong Xu
Cc: Jonathan Corbet, Shuah Khan, Paul Walmsley, Palmer Dabbelt,
Albert Ou, Alexandre Ghiti, Zong Li, Deepak Gupta, Anup Patel,
Atish Patra, Rob Herring, Krzysztof Kozlowski, Conor Dooley,
Yixun Lan, Chen Wang, Inochi Amaoto, Conor Dooley, Shuah Khan,
Christian Brauner, linux-doc, linux-riscv, linux-kernel, kvm,
kvm-riscv, Paul Walmsley, Palmer Dabbelt, Conor Dooley,
devicetree, spacemit, sophgo, linux-kselftest, Charles Jenkins,
Samuel Holland, Charlie Jenkins, Jesse Taube, Andy Chiu
In-Reply-To: <20260603-rva23u64-hwprobe-v2-v3-0-5529a7b28384@gmail.com>
On Wed, Jun 03, 2026 at 07:11:55AM -0400, Guodong Xu wrote:
> This series builds on Andrew Jones's earlier RFC [1]. It lets
> userspace check for RVA23U64 conformance in one call, instead of
> walking hwprobe + prctl across every mandatory extension.
>
> The series adds a small framework that resolves profile-class
> bases (IMA and RVA23U64) from the kernel's ISA extension bitmap at
> init time, and surfaces the result through both /proc/cpuinfo and
> hwprobe. Later patches can add RVA23S64, and backward RVA22 / RVA20
> detection, to riscv_set_isa_bases() without changes to the
> surrounding code.
>
Hi Guodong,
The series looks good to me. Thanks a lot for picking up this work!
drew
^ permalink raw reply
* Re: (subset) [PATCH v7 00/10] Support for Samsung S2MU005 PMIC and its sub-devices
From: Sebastian Reichel @ 2026-06-04 16:05 UTC (permalink / raw)
To: Lee Jones, Pavel Machek, Rob Herring, Krzysztof Kozlowski,
Conor Dooley, MyungJoo Ham, Chanwoo Choi, Sebastian Reichel,
Krzysztof Kozlowski, André Draszik, Alexandre Belloni,
Jonathan Corbet, Shuah Khan, Nam Tran,
Łukasz Lebiedziński, Yassine Oudjana,
Kaustabh Chakraborty
Cc: linux-leds, devicetree, linux-kernel, linux-pm, linux-samsung-soc,
linux-rtc, linux-doc, Conor Dooley, Krzysztof Kozlowski
In-Reply-To: <20260516-s2mu005-pmic-v7-0-73f9702fb461@disroot.org>
On Sat, 16 May 2026 03:08:32 +0530, Kaustabh Chakraborty wrote:
> S2MU005 is an MFD chip manufactured by Samsung Electronics. This is
> found in various devices manufactured by Samsung and others, including
> all Exynos 7870 devices. It is known to have the following features:
>
> 1. Two LED channels with adjustable brightness for use as a torch, or a
> flash strobe.
> 2. An RGB LED with 8-bit channels. Usually programmed as a notification
> indicator.
> 3. An MUIC, which works with USB micro-B (and USB-C?). For the micro-B
> variant though, it measures the ID-GND resistance using an internal
> ADC.
> 4. A charger device, which reports if charger is online, voltage,
> resistance, etc.
>
> [...]
Applied, thanks!
[10/10] power: supply: add support for Samsung S2M series PMIC charger device
commit: 7e541f6dbd05921d0bbb99646028cb9982535707
Best regards,
--
Sebastian Reichel <sebastian.reichel@collabora.com>
^ permalink raw reply
* Re: [PATCH v6 01/11] x86/virt/tdx: Simplify tdmr_get_pamt_sz()
From: Kiryl Shutsemau @ 2026-06-04 16:05 UTC (permalink / raw)
To: Rick Edgecombe
Cc: bp, dave.hansen, hpa, kvm, linux-coco, linux-doc, linux-kernel,
mingo, nik.borisov, pbonzini, seanjc, tglx, vannapurve, x86,
chao.gao, yan.y.zhao, kai.huang, Binbin Wu
In-Reply-To: <20260526023515.288829-2-rick.p.edgecombe@intel.com>
On Mon, May 25, 2026 at 07:35:05PM -0700, Rick Edgecombe wrote:
> For each memory region that the TDX module might use (called TDMR), three
> separate traditional PAMT allocations are needed. One for each supported
> page size (1GB, 2MB, 4KB). These store information on each page in the
> TDMR. In Linux, they are allocated out of one physically contiguous block,
> in order to more efficiently use some internal TDX module book keeping
> resources. So some simple math is needed to break the single large
> allocation into three smaller allocations for each page size.
>
> There are some commonalities in the math needed to calculate the base and
> size for each smaller allocation, and so an effort was made to share logic
> across the three. Unfortunately doing this turned out unnaturally tortured,
> with a loop iterating over the three page sizes, only to call into a
> function with cases statement for each page size. In the future Dynamic
> PAMT will add more logic that is special to the 4KB page size, making the
> benefit of the math sharing even more questionable.
>
> Three is not a very high number, so get rid of the loop and just duplicate
> the small calculation three times. In doing so, setup for future Dynamic
> PAMT changes.
>
> Since the loop that iterates over it is gone, further simplify the code by
> dropping the array of intermediate size and base storage. Just store the
> values to their final locations. Accept the small complication of having
> to clear tdmr->pamt_4k_base in the error path, so that tdmr_do_pamt_func()
> will not try to operate on the TDMR struct when attempting to free it.
>
> Assisted-by: GitHub Copilot:claude-opus-4-6 Claude:claude-opus-4-7
> Reviewed-by: Binbin Wu <binbin.wu@linux.intel.com>
> Signed-off-by: Rick Edgecombe <rick.p.edgecombe@intel.com>
Reviewed-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
Couple of nits below.
> ---
> v6:
> - Drop {} by moving a comment (Binbin)
> - Log tweaks
>
> v4:
> - Just refer to global var instead of passing pamt_entry_size around
> (Xiaoyao)
> - Remove setting pamt_4k_base to zero, because it already is zero.
> Adjust the comment appropriately (Kai)
>
> v3:
> - New patch
> ---
> arch/x86/virt/vmx/tdx/tdx.c | 93 ++++++++++++-------------------------
> 1 file changed, 29 insertions(+), 64 deletions(-)
>
> diff --git a/arch/x86/virt/vmx/tdx/tdx.c b/arch/x86/virt/vmx/tdx/tdx.c
> index 967482ae3c801..487f389f52f4b 100644
> --- a/arch/x86/virt/vmx/tdx/tdx.c
> +++ b/arch/x86/virt/vmx/tdx/tdx.c
> @@ -516,31 +516,21 @@ static __init int fill_out_tdmrs(struct list_head *tmb_list,
> * Calculate PAMT size given a TDMR and a page size. The returned
> * PAMT size is always aligned up to 4K page boundary.
> */
> -static __init unsigned long tdmr_get_pamt_sz(struct tdmr_info *tdmr, int pgsz,
> - u16 pamt_entry_size)
> +static __init unsigned long tdmr_get_pamt_sz(struct tdmr_info *tdmr, int pgsz)
> {
> unsigned long pamt_sz, nr_pamt_entries;
> + const int tdx_pg_size_shift[] = { PAGE_SHIFT, PMD_SHIFT, PUD_SHIFT };
> + const u16 pamt_entry_size[TDX_PS_NR] = {
> + tdx_sysinfo.tdmr.pamt_4k_entry_size,
> + tdx_sysinfo.tdmr.pamt_2m_entry_size,
> + tdx_sysinfo.tdmr.pamt_1g_entry_size,
> + };
>
> - switch (pgsz) {
> - case TDX_PS_4K:
> - nr_pamt_entries = tdmr->size >> PAGE_SHIFT;
> - break;
> - case TDX_PS_2M:
> - nr_pamt_entries = tdmr->size >> PMD_SHIFT;
> - break;
> - case TDX_PS_1G:
> - nr_pamt_entries = tdmr->size >> PUD_SHIFT;
> - break;
> - default:
> - WARN_ON_ONCE(1);
> - return 0;
> - }
> + nr_pamt_entries = tdmr->size >> tdx_pg_size_shift[pgsz];
> + pamt_sz = nr_pamt_entries * pamt_entry_size[pgsz];
>
> - pamt_sz = nr_pamt_entries * pamt_entry_size;
> /* TDX requires PAMT size must be 4K aligned */
> - pamt_sz = ALIGN(pamt_sz, PAGE_SIZE);
> -
> - return pamt_sz;
> + return PAGE_ALIGN(pamt_sz);
> }
>
> /*
> @@ -578,28 +568,21 @@ static __init int tdmr_get_nid(struct tdmr_info *tdmr, struct list_head *tmb_lis
> * within @tdmr, and set up PAMTs for @tdmr.
> */
> static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,
> - struct list_head *tmb_list,
> - u16 pamt_entry_size[])
> + struct list_head *tmb_list)
> {
> - unsigned long pamt_base[TDX_PS_NR];
> - unsigned long pamt_size[TDX_PS_NR];
> - unsigned long tdmr_pamt_base;
> unsigned long tdmr_pamt_size;
> struct page *pamt;
> - int pgsz, nid;
> -
> + int nid;
Add a newline here?
> nid = tdmr_get_nid(tdmr, tmb_list);
>
> /*
> * Calculate the PAMT size for each TDX supported page size
> * and the total PAMT size.
> */
> - tdmr_pamt_size = 0;
> - for (pgsz = TDX_PS_4K; pgsz < TDX_PS_NR; pgsz++) {
> - pamt_size[pgsz] = tdmr_get_pamt_sz(tdmr, pgsz,
> - pamt_entry_size[pgsz]);
> - tdmr_pamt_size += pamt_size[pgsz];
> - }
> + tdmr->pamt_4k_size = tdmr_get_pamt_sz(tdmr, TDX_PS_4K);
> + tdmr->pamt_2m_size = tdmr_get_pamt_sz(tdmr, TDX_PS_2M);
> + tdmr->pamt_1g_size = tdmr_get_pamt_sz(tdmr, TDX_PS_1G);
> + tdmr_pamt_size = tdmr->pamt_4k_size + tdmr->pamt_2m_size + tdmr->pamt_1g_size;
>
> /*
> * Allocate one chunk of physically contiguous memory for all
> @@ -607,26 +590,18 @@ static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,
> * in overlapped TDMRs.
> */
> pamt = alloc_contig_pages(tdmr_pamt_size >> PAGE_SHIFT, GFP_KERNEL,
> - nid, &node_online_map);
> + nid, &node_online_map);
> +
Looks like unrelated whitespace change. Is it intentional?
> + /*
> + * tdmr->pamt_4k_base is still zero so the error
> + * path of the caller will skip freeing the pamt.
> + */
> if (!pamt)
> return -ENOMEM;
>
> - /*
> - * Break the contiguous allocation back up into the
> - * individual PAMTs for each page size.
> - */
> - tdmr_pamt_base = page_to_pfn(pamt) << PAGE_SHIFT;
> - for (pgsz = TDX_PS_4K; pgsz < TDX_PS_NR; pgsz++) {
> - pamt_base[pgsz] = tdmr_pamt_base;
> - tdmr_pamt_base += pamt_size[pgsz];
> - }
> -
> - tdmr->pamt_4k_base = pamt_base[TDX_PS_4K];
> - tdmr->pamt_4k_size = pamt_size[TDX_PS_4K];
> - tdmr->pamt_2m_base = pamt_base[TDX_PS_2M];
> - tdmr->pamt_2m_size = pamt_size[TDX_PS_2M];
> - tdmr->pamt_1g_base = pamt_base[TDX_PS_1G];
> - tdmr->pamt_1g_size = pamt_size[TDX_PS_1G];
> + tdmr->pamt_4k_base = page_to_phys(pamt);
> + tdmr->pamt_2m_base = tdmr->pamt_4k_base + tdmr->pamt_4k_size;
> + tdmr->pamt_1g_base = tdmr->pamt_2m_base + tdmr->pamt_2m_size;
>
> return 0;
> }
> @@ -657,10 +632,7 @@ static __init void tdmr_do_pamt_func(struct tdmr_info *tdmr,
> tdmr_get_pamt(tdmr, &pamt_base, &pamt_size);
>
> /* Do nothing if PAMT hasn't been allocated for this TDMR */
> - if (!pamt_size)
> - return;
> -
> - if (WARN_ON_ONCE(!pamt_base))
> + if (!pamt_base)
> return;
>
> pamt_func(pamt_base, pamt_size);
> @@ -686,14 +658,12 @@ static __init void tdmrs_free_pamt_all(struct tdmr_info_list *tdmr_list)
>
> /* Allocate and set up PAMTs for all TDMRs */
> static __init int tdmrs_set_up_pamt_all(struct tdmr_info_list *tdmr_list,
> - struct list_head *tmb_list,
> - u16 pamt_entry_size[])
> + struct list_head *tmb_list)
> {
> int i, ret = 0;
>
> for (i = 0; i < tdmr_list->nr_consumed_tdmrs; i++) {
> - ret = tdmr_set_up_pamt(tdmr_entry(tdmr_list, i), tmb_list,
> - pamt_entry_size);
> + ret = tdmr_set_up_pamt(tdmr_entry(tdmr_list, i), tmb_list);
> if (ret)
> goto err;
> }
> @@ -970,18 +940,13 @@ static __init int construct_tdmrs(struct list_head *tmb_list,
> struct tdmr_info_list *tdmr_list,
> struct tdx_sys_info_tdmr *sysinfo_tdmr)
> {
> - u16 pamt_entry_size[TDX_PS_NR] = {
> - sysinfo_tdmr->pamt_4k_entry_size,
> - sysinfo_tdmr->pamt_2m_entry_size,
> - sysinfo_tdmr->pamt_1g_entry_size,
> - };
> int ret;
>
> ret = fill_out_tdmrs(tmb_list, tdmr_list);
> if (ret)
> return ret;
>
> - ret = tdmrs_set_up_pamt_all(tdmr_list, tmb_list, pamt_entry_size);
> + ret = tdmrs_set_up_pamt_all(tdmr_list, tmb_list);
> if (ret)
> return ret;
>
> --
> 2.54.0
>
--
Kiryl Shutsemau / Kirill A. Shutemov
^ permalink raw reply
* Re: [PATCH v3 1/4] mm/zswap: Make shrink_worker writeback cursor per-memcg
From: Yosry Ahmed @ 2026-06-04 16:10 UTC (permalink / raw)
To: Hao Jia
Cc: akpm, tj, hannes, shakeel.butt, mhocko, mkoutny, nphamcs,
chengming.zhou, muchun.song, roman.gushchin, cgroups, linux-mm,
linux-kernel, linux-doc, Hao Jia
In-Reply-To: <a60eedb6-f3fd-4092-b726-04a17a695ace@gmail.com>
On Thu, Jun 4, 2026 at 6:06 AM Hao Jia <jiahao.kernel@gmail.com> wrote:
>
>
>
> On 2026/6/4 13:34, Yosry Ahmed wrote:
> >>>> For instance, suppose a parent memcg has two children, memcg1 and memcg2,
> >>>> each with 200MB of zswap (100MB inactive). Triggering proactive writeback on
> >>>> the parent memcg will exhaust memcg1's inactive zswap pages. After that,
> >>>> even though memcg2 still has plenty of inactive zswap pages, it will
> >>>> continue to write back memcg1's active zswap pages. Writing back active
> >>>> zswap pages causes the user-space agent to prematurely abort the writeback
> >>>> because it detects that certain memcg metrics have exceeded predefined
> >>>> thresholds.
> >>>
> >>> This will only happen if the reclaim size is smaller than the batch
> >>> size, right? Otherwise the kernel should reclaim more or less equally
> >>> from both memcgs?
> >>>
> >>
> >> I gave it some thought. Not using a cursor could lead to unfairness
> >> issues with certain writeback sizes:
> >>
> >> - If the writeback size is an odd multiple of WB_BATCH (e.g.,
> >> triggering a writeback of 3 * WB_BATCH), with 2 child cgroups, the
> >> writeback ratio might end up being 2:1.
> >> - If a memcg has 5 child cgroups and a writeback of 2 * WB_BATCH is
> >> triggered, it might repeatedly write back from only the first 2 child
> >> cgroups.
> >>
> >> Although setting a smaller WB_BATCH might mitigate this unfairness, it
> >> could hurt writeback efficiency. Let's just use per-memcg cursors to
> >> completely fix these corner cases.
> >
> > Exactly, the batch size should be small enough that any unfairness is
> > not a problem. I would honestly just do batching without a per-memcg
> > cursor, unless we have numbers to prove that the efficiency is
> > affected when we use a small batch size. Let's only introduce
> > complexity when needed please.
>
>
> If you prefer not to use per-cgroup cursors, do we still need to keep
> the global cursor (i.e., the root cgroup's cursor) zswap_next_shrink?
> I found this part to be quite tricky when trying to reuse the main logic
> of shrink_worker() in zswap_proactive_writeback().
>
> Of course, I think we could also keep zswap_next_shrink and write a
> small helper to check if it's the root cgroup, allowing us to use
> different memcg iteration methods.
I think we want to keep the global cursor, at least for now.
^ permalink raw reply
* Re: [PATCH v6 02/11] x86/virt/tdx: Allocate page bitmap for Dynamic PAMT
From: Kiryl Shutsemau @ 2026-06-04 16:14 UTC (permalink / raw)
To: Rick Edgecombe
Cc: bp, dave.hansen, hpa, kvm, linux-coco, linux-doc, linux-kernel,
mingo, nik.borisov, pbonzini, seanjc, tglx, vannapurve, x86,
chao.gao, yan.y.zhao, kai.huang, Kirill A. Shutemov, Binbin Wu
In-Reply-To: <20260526023515.288829-3-rick.p.edgecombe@intel.com>
On Mon, May 25, 2026 at 07:35:06PM -0700, Rick Edgecombe wrote:
> @@ -579,7 +591,12 @@ static __init int tdmr_set_up_pamt(struct tdmr_info *tdmr,
> * Calculate the PAMT size for each TDX supported page size
> * and the total PAMT size.
> */
> - tdmr->pamt_4k_size = tdmr_get_pamt_sz(tdmr, TDX_PS_4K);
> + if (tdx_supports_dynamic_pamt(&tdx_sysinfo)) {
> + /* With Dynamic PAMT, PAMT_4K is replaced with a bitmap */
> + tdmr->pamt_4k_size = tdmr_get_pamt_bitmap_sz(tdmr);
> + } else {
> + tdmr->pamt_4k_size = tdmr_get_pamt_sz(tdmr, TDX_PS_4K);
> + }
> tdmr->pamt_2m_size = tdmr_get_pamt_sz(tdmr, TDX_PS_2M);
> tdmr->pamt_1g_size = tdmr_get_pamt_sz(tdmr, TDX_PS_1G);
> tdmr_pamt_size = tdmr->pamt_4k_size + tdmr->pamt_2m_size + tdmr->pamt_1g_size;
Maybe it would more readable if we reverse the size order:
/*
* Calculate the PAMT size for each TDX supported page size
* and the total PAMT size.
*/
tdmr->pamt_1g_size = tdmr_get_pamt_sz(tdmr, TDX_PS_1G);
tdmr->pamt_2m_size = tdmr_get_pamt_sz(tdmr, TDX_PS_2M);
if (tdx_supports_dynamic_pamt(&tdx_sysinfo)) {
/* With Dynamic PAMT, PAMT_4K is replaced with a bitmap */
tdmr->pamt_4k_size = tdmr_get_pamt_bitmap_sz(tdmr);
} else {
tdmr->pamt_4k_size = tdmr_get_pamt_sz(tdmr, TDX_PS_4K);
}
tdmr_pamt_size = tdmr->pamt_1g_size + tdmr->pamt_2m_size + tdmr->pamt_4k_size;
It allows split it into logical blocks while keeping the comment attached.
--
Kiryl Shutsemau / Kirill A. Shutemov
^ permalink raw reply
* Re: [PATCH v2 08/10] riscv: cpufeature: Introduce ISA bases bitmap and rva23u64 detection
From: Conor Dooley @ 2026-06-04 16:22 UTC (permalink / raw)
To: Andrew Jones
Cc: Guodong Xu, Jonathan Corbet, Paul Walmsley, Palmer Dabbelt,
Conor Dooley, Albert Ou, Alexandre Ghiti, Shuah Khan, Anup Patel,
Atish Patra, Shuah Khan, Deepak Gupta, Zong Li, Christian Brauner,
Charlie Jenkins, Samuel Holland, linux-doc, linux-riscv,
linux-kernel, linux-kselftest, kvm, kvm-riscv, Guodong Xu
In-Reply-To: <ug2cipududshcbv24ruicqpugzinujnamvdcuy6pubkwh67bfp@2fysrplz3nr4>
[-- Attachment #1: Type: text/plain, Size: 5378 bytes --]
On Thu, Jun 04, 2026 at 10:29:19AM -0500, Andrew Jones wrote:
> On Sat, May 30, 2026 at 08:42:29AM +0800, Guodong Xu wrote:
> > On Thu, May 28, 2026 at 12:35 AM Andrew Jones
> > > Sashiko points out a few things about this patch which I think I
> > > agree with
> > >
> > > https://sashiko.dev/#/patchset/20260511-rva23u64-hwprobe-v2-v2-0-21c5a544f1dc%40riscstar.com?part=8
> >
> > Quote the following from Sashiko.dev:
> > > Should this mask specify the individual subset extensions required by the
> > > profile instead of the superset extensions like RISCV_ISA_EXT_B,
> > > RISCV_ISA_EXT_C, and RISCV_ISA_EXT_V?
> >
> > My preference is to leave the mask on B/C/V (and A) as-is. I'd prefer to
> > keep matching on the single-letter, rather than expanding them. Here is why:
> >
> > - The RVA23 profile lists A, B, C and V as single-letter mandatory
> > extensions; it doesn't enumerate Zaamo/Zalrsc, Zba/Zbb/Zbs, Zc* or the
> > Zve*/Zvl* subsets in the mandatory set.
> >
> > - In current merged code, hwprobe_isa_ext0() is already using
> > riscv_isa_extension_available() signle letter checking for C and V.
> >
> > PS:
> > B maybe a special one, just in case anybody raise it. As the community
> > discussed when I adding it into the bindings, because B comes later than
> > its sub-components zba/zbb/zbs, so, when I added B, I cleaned up all
> > in-tree dts files which declared zba/zbb/zbs but not B and made them declare
> > both.
> >
> > Link: https://lore.kernel.org/linux-riscv/20260115-adding-b-dtsi-v2-0-254dd61cf947@riscstar.com/
> > [1]
> >
> > Also, in the bindings: extensions.yaml, a schema rule is added which requires
> > a node listing zba, zbb and zbs to also list b (and the reverse). Moving on,
> > new dtsi/dts fils, a node with only the subsets fails dtbs_check.
> >
> > One may argue that the schema check doesn't cover ACPI path. But again,
> > shouldn't the vendor who publishs RVA23 hardware be conformant to the
> > extensions wording in RVA23 v1.0 spec?
> >
> > What do you think?
> >
>
> I certainly see a case for the kernel staying out of the extension
> dependency validation game. I think it makes sense for an ISA string
> validation tool to exist for vendors to do sanity checks on their
> ISA strings, but that's not the kernel's role. OTOH, whether or not
> the kernel wants to try and detect inconsistencies with the ISA
> string in order to build confidence in using what it sees there
> and publishing what it sees there to usermode, through hwprobe, might
> still be worth debating.
On the dt front, our policy is that it is not the kernel's job to
check that the dt matches the hardware. It is the firmware's job to
accurately report what the hardware is. There's definitely some
exceptions to that already (*cough thead vector cough*) but that's in
part because we failed to come up with concrete descriptions in the
first place.
Yesterday I commented on a patch that was adding this kind of thing:
| The point of validate callbacks is twofold: checking that the kernel
| configuration supports the extension and that extensions that the kernel
| depends on to support the one in question are present.
The latter is because you could depend on an extension that is affected
by the former.
Looking at the list of validate callbacks, I think there are some that
actually don't follow this policy (I saw riscv_ext_zca_depends() that
has no real purpose, but didn't look much further).
I'm not even sure that the validate callback added in this series for
zic64b should be added, it's borderline for me. I'm only really not
objecting to it because it outputs a warning if someone has the
retroactively added extension without the relevant dt/acpi properties
and expects things to work.
> Without CPUID / ID_* registers for Linux to be able to check an
> authoritative source of truth about what is and isn't supported by
> the CPU, riscv Linux has to decide to either blindly trust the
> hardware description or do sanity checks / probes in order to
> confirm what it sees there. Maybe we can assume that any extension
> used by the kernel will trip over itself quickly, alerting vendors
> to fix their ISA strings, but I'm not sure we can make that assumption
> for usermode extensions that Linux doesn't use, but does expose
> through hwprobe. What apps need to run in testing to exercise them?
> How long will those apps need to run before they trip over something?
>
> The more I think I about it, the more I think the lack of CPUID / ID_*
> registers puts Linux in a tight spot. If Linux trustingly publishes
> what firmware tells it to to userspace and userspace blows up, Linux
> will have to share some of the blame for having misled it. So, should
> Linux validate everything it publishes somehow? Or, should it at least
> do relatively cheap sanity checks on everything it publishes in order
> to build some confidence?
Part of me wants to know why we should be going to a bunch of effort to
cover up for inaccurate devicetrees etc. Garbage in -> garbage out and let
them keep the pieces.
That said, if we were to do something with checks, we can't actually
even check everything and my opinion is that either we certify that
everything is VerifiedTM or we don't certify at all.
Cheers,
Conor.
[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]
^ permalink raw reply
* Re: [PATCH mm-unstable v18 06/14] mm/khugepaged: generalize collapse_huge_page for mTHP collapse
From: Nico Pache @ 2026-06-04 16:28 UTC (permalink / raw)
To: Lorenzo Stoakes
Cc: linux-doc, linux-kernel, linux-mm, linux-trace-kernel, aarcange,
akpm, anshuman.khandual, apopple, baohua, baolin.wang, byungchul,
catalin.marinas, cl, corbet, dave.hansen, david, dev.jain, gourry,
hannes, hughd, jack, jackmanb, jannh, jglisse, joshua.hahnjy, kas,
lance.yang, liam, mathieu.desnoyers, matthew.brost, mhiramat,
mhocko, peterx, pfalcato, rakie.kim, raquini, rdunlap,
richard.weiyang, rientjes, rostedt, rppt, ryan.roberts, shivankg,
sunnanyong, surenb, thomas.hellstrom, tiwai, usamaarif642, vbabka,
vishal.moola, wangkefeng.wang, will, willy, yang, ying.huang, ziy,
zokeefe, Usama Arif
In-Reply-To: <aiFz-VSKSQ-zBfN7@lucifer>
On Thu, Jun 4, 2026 at 6:56 AM Lorenzo Stoakes <ljs@kernel.org> wrote:
>
> On Thu, Jun 04, 2026 at 06:45:58AM -0600, Nico Pache wrote:
> > On Thu, Jun 4, 2026 at 6:40 AM Lorenzo Stoakes <ljs@kernel.org> wrote:
> > >
> > > On Thu, Jun 04, 2026 at 12:38:30PM +0100, Lorenzo Stoakes wrote:
> > > > I will go review the thread about the cache maintenance separately and
> > > > respond about that.
> > > >
> > > > On Fri, May 22, 2026 at 09:00:01AM -0600, Nico Pache wrote:
> > > > > Pass an order and offset to collapse_huge_page to support collapsing anon
> > > > > memory to arbitrary orders within a PMD. order indicates what mTHP size we
> > > > > are attempting to collapse to, and offset indicates were in the PMD to
> > > > > start the collapse attempt.
> > > > >
> > > > > For non-PMD collapse we must leave the anon VMA write locked until after
> > > > > we collapse the mTHP-- in the PMD case all the pages are isolated, but in
> > > > > the mTHP case this is not true, and we must keep the lock to prevent
> > > > > access/changes to the page tables. This can happen if the rmap walkers hit
> > > > > a pmd_none while the PMD entry is currently unavailable due to being
> > > > > temporarily removed during the collapse phase.
> > > > >
> > > > > Acked-by: Usama Arif <usama.arif@linux.dev>
> > > > > Signed-off-by: Nico Pache <npache@redhat.com>
> > > >
> > > > The logic LGTM generally, some questions for understanding below, and of
> > > > course as per above I want to review the Lance/David subthread.
> > > >
> > > > Thanks!
> > > >
> > > > > ---
> > > > > mm/khugepaged.c | 93 +++++++++++++++++++++++++++++--------------------
> > > > > 1 file changed, 55 insertions(+), 38 deletions(-)
> > > > >
> > > > > diff --git a/mm/khugepaged.c b/mm/khugepaged.c
> > > > > index fab35d318641..d64f42f66236 100644
> > > > > --- a/mm/khugepaged.c
> > > > > +++ b/mm/khugepaged.c
> > > > > @@ -1214,34 +1214,36 @@ static enum scan_result alloc_charge_folio(struct folio **foliop, struct mm_stru
> > > > > * while allocating a THP, as that could trigger direct reclaim/compaction.
> > > > > * Note that the VMA must be rechecked after grabbing the mmap_lock again.
> > > > > */
> > > > > -static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long address,
> > > > > - int referenced, int unmapped, struct collapse_control *cc)
> > > > > +static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long start_addr,
> > > > > + int referenced, int unmapped, struct collapse_control *cc,
> > > > > + unsigned int order)
> > > > > {
> > > > > + const unsigned long pmd_addr = start_addr & HPAGE_PMD_MASK;
> > > > > + const unsigned long end_addr = start_addr + (PAGE_SIZE << order);
> > > > > LIST_HEAD(compound_pagelist);
> > > > > pmd_t *pmd, _pmd;
> > > > > - pte_t *pte;
> > > > > + pte_t *pte = NULL;
> > > >
> > > > As mentioned elsewhere for some reason this was dropped in
> > > > mm-unstable. Maybe a bad conflict resolution?
> > > >
> > > > > pgtable_t pgtable;
> > > > > struct folio *folio;
> > > > > spinlock_t *pmd_ptl, *pte_ptl;
> > > > > enum scan_result result = SCAN_FAIL;
> > > > > struct vm_area_struct *vma;
> > > > > struct mmu_notifier_range range;
> > > > > + bool anon_vma_locked = false;
> > > > >
> > > > > - VM_BUG_ON(address & ~HPAGE_PMD_MASK);
> > > > > -
> > > > > - result = alloc_charge_folio(&folio, mm, cc, HPAGE_PMD_ORDER);
> > > > > + result = alloc_charge_folio(&folio, mm, cc, order);
> > > > > if (result != SCAN_SUCCEED)
> > > > > goto out_nolock;
> > > > >
> > > > > mmap_read_lock(mm);
> > > > > - result = hugepage_vma_revalidate(mm, address, true, &vma, cc,
> > > > > - HPAGE_PMD_ORDER);
> > > > > + result = hugepage_vma_revalidate(mm, pmd_addr, /*expect_anon=*/ true,
> > > > > + &vma, cc, order);
> > > > > if (result != SCAN_SUCCEED) {
> > > > > mmap_read_unlock(mm);
> > > > > goto out_nolock;
> > > > > }
> > > > >
> > > > > - result = find_pmd_or_thp_or_none(mm, address, &pmd);
> > > > > + result = find_pmd_or_thp_or_none(mm, pmd_addr, &pmd);
> > > > > if (result != SCAN_SUCCEED) {
> > > > > mmap_read_unlock(mm);
> > > > > goto out_nolock;
> > > > > @@ -1253,8 +1255,8 @@ static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long a
> > > > > * released when it fails. So we jump out_nolock directly in
> > > > > * that case. Continuing to collapse causes inconsistency.
> > > > > */
> > > > > - result = __collapse_huge_page_swapin(mm, vma, address, pmd,
> > > > > - referenced, HPAGE_PMD_ORDER);
> > > > > + result = __collapse_huge_page_swapin(mm, vma, start_addr, pmd,
> > > > > + referenced, order);
> > > > > if (result != SCAN_SUCCEED)
> > > > > goto out_nolock;
> > > > > }
> > > > > @@ -1269,20 +1271,21 @@ static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long a
> > > > > * mmap_lock.
> > > > > */
> > > > > mmap_write_lock(mm);
> > > > > - result = hugepage_vma_revalidate(mm, address, true, &vma, cc,
> > > > > - HPAGE_PMD_ORDER);
> > > > > + result = hugepage_vma_revalidate(mm, pmd_addr, /*expect_anon=*/ true,
> > > > > + &vma, cc, order);
> > > > > if (result != SCAN_SUCCEED)
> > > > > goto out_up_write;
> > > > > /* check if the pmd is still valid */
> > > > > vma_start_write(vma);
> > >
> > > Hmm actually I think we have another problem here.
> > >
> > > For PMD THP this is fine. Only a single VMA can span the range we need, and it
> > > will span the entire PMD.
> > >
> > > But for mTHP we have an issue...
> > >
> > > See below...
> > >
> > > > > - result = check_pmd_still_valid(mm, address, pmd);
> > > > > + result = check_pmd_still_valid(mm, pmd_addr, pmd);
> > > > > if (result != SCAN_SUCCEED)
> > > > > goto out_up_write;
> > > > >
> > > > > anon_vma_lock_write(vma->anon_vma);
> > > > > + anon_vma_locked = true;
> > > >
> > > > I worry that we hold this lock a lot longer now? Maybe the algorithmic
> > > > change alters that, but Claude did suggest on the s390 bug that longer lock
> > > > hold might be an issue.
> > > >
> > > > I wonder if we'll observe lock contention as a result?
> > > >
> > > > Correct me if I'm wrong and we're not holding longer than previously,
> > > > however. Just appears that we do.
> > > >
> > > > >
> > > > > - mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm, address,
> > > > > - address + HPAGE_PMD_SIZE);
> > > > > + mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, mm, start_addr,
> > > > > + end_addr);
> > > > > mmu_notifier_invalidate_range_start(&range);
> > > > >
> > > > > pmd_ptl = pmd_lock(mm, pmd); /* probably unnecessary */
> > > > > @@ -1294,26 +1297,23 @@ static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long a
> > > > > * Parallel GUP-fast is fine since GUP-fast will back off when
> > > > > * it detects PMD is changed.
> > > > > */
> > > > > - _pmd = pmdp_collapse_flush(vma, address, pmd);
> > > > > + _pmd = pmdp_collapse_flush(vma, pmd_addr, pmd);
> > >
> > > ...So we exclude VMA locked faults faulting in a new PMD entry for PMD-sized THP
> > > but for mTHP we might have _another_ VMA that spans another part of the range
> > > mapped by the same PMD entry.
> > >
> > > So we clear this, but we do not have a write lock on any other VMA, and so
> > > racing VMA read locks can install a new PMD entry.
> > >
> > > > > spin_unlock(pmd_ptl);
> > >
> > > Especially since you unlock this :)
> > >
> > > And...
> > >
> > > > > mmu_notifier_invalidate_range_end(&range);
> > > > > tlb_remove_table_sync_one();
> > > > >
> > > > > - pte = pte_offset_map_lock(mm, &_pmd, address, &pte_ptl);
> > > > > + pte = pte_offset_map_lock(mm, &_pmd, start_addr, &pte_ptl);
> > > > > if (pte) {
> > > > > - result = __collapse_huge_page_isolate(vma, address, pte, cc,
> > > > > - HPAGE_PMD_ORDER,
> > > > > - &compound_pagelist);
> > > > > + result = __collapse_huge_page_isolate(vma, start_addr, pte, cc,
> > > > > + order, &compound_pagelist);
> > > > > spin_unlock(pte_ptl);
> > > > > } else {
> > > > > result = SCAN_NO_PTE_TABLE;
> > > > > }
> > > > >
> > > > > if (unlikely(result != SCAN_SUCCEED)) {
> > > > > - if (pte)
> > > > > - pte_unmap(pte);
> > > >
> > > > OK I seem to remember this is because we're holding the anon_vma lock
> > > > longer. That does imply that on e.g. x86-64 the RCU lock is being held a
> > > > bit longer also as well as the anon_vma loc.
> > > >
> > > > I guess it's also because we need to hold anon_vma and pte lock because
> > > > we're fiddling around at PTE level for mTHP not just PMD level as 'classic'
> > > > THP did.
> > > >
> > > > (Rememberings going on here :)
> > > >
> > > > > spin_lock(pmd_ptl);
> > > > > - BUG_ON(!pmd_none(*pmd));
> > > > > + WARN_ON_ONCE(!pmd_none(*pmd));
> > >
> > > ...this will get triggered.
> > >
> > > I don't know whether we can safely hold the PMD lock across everything here for
> > > mTHP?
> > >
> > > Maybe the solution would have to be to scan through VMAs in the range of the PMD
> > > and VMA write lock each of them?
> >
> > I believe we've spoken about this before, but because we always make
>
> Maybe worth a comment then...? Ah how rewarding review is :)
I'll expand the commit message and comment in commit 1 of the series! thanks
>
> This is something that somebody else might very well wonder about and
> forget that it happens to be covered there.
>
> Also:
>
> /* Always check the PMD order to ensure its not shared by another VMA */
>
> Is pretty lightweight there. Something about avoiding racing page faults
> would be helpful.
yeah fair enough the commit message of patch 1 also doesnt really do
it justice on the *why*
>
> > sure the VMA spans the full PMD we won't ever hit this issue. If we
> > wanted to support mTHP collapse on regions smaller than a PMD, the
> > locking gets tricky (hence the design choice to not do that for now).
> >
> > This is handled by the HPAGE_ORDER in hugepage_vma_revalidate().
>
> The existing code is atrocious, and sticking this on top has added to the
> pile of assumptions and conventions and having to go check a bunch of
> functions to 'just know' you're safe for X, Y, Z.
>
> We really need to see some cleanup series coming after this and I'm going
> to get pretty grumpy(ier) if we don't.
Many more to come :) Improvements too but cleanups first!
Cheers,
-- Nico
>
> >
> > /* Always check the PMD order to ensure its not shared by another VMA */
> > if (!thp_vma_suitable_order(vma, address, PMD_ORDER))
> >
> > -- Nico
> >
> > >
> > > That could cause some 'interesting' lock contention issues though? Then again,
> > > we will be releasing the mmap write lock soon enough which will drop the VMA
> > > write locks.
> > >
> > > > > /*
> > > > > * We can only use set_pmd_at when establishing
> > > > > * hugepmds and never for establishing regular pmds that
> > > > > @@ -1321,21 +1321,24 @@ static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long a
> > > > > */
> > > > > pmd_populate(mm, pmd, pmd_pgtable(_pmd));
> > > > > spin_unlock(pmd_ptl);
> > > > > - anon_vma_unlock_write(vma->anon_vma);
> > > > > goto out_up_write;
> > > > > }
> > > > >
> > > > > /*
> > > > > - * All pages are isolated and locked so anon_vma rmap
> > > > > - * can't run anymore.
> > > > > + * For PMD collapse all pages are isolated and locked so anon_vma
> > > > > + * rmap can't run anymore. For mTHP collapse the PMD entry has been
> > > > > + * removed and not all pages are isolated and locked, so we must hold
> > > >
> > > > Right because some PTE entries be unaffected by the change.
> > > >
> > > > > + * the lock to prevent neighboring folios from attempting to access
> > > > > + * this PMD until its reinstalled.
> > > >
> > > > OK. This is slightly annoying for my CoW context work as it means there's
> > > > another case where we need to explicitly hold an anon_vma lock for
> > > > correctness :)
> > > >
> > > > Anyway I will think about that separately, is what it is. And in fact
> > > > motivates to want this merged earlier so I can work against it :)
> > > >
> > > >
> > > > > */
> > > > > - anon_vma_unlock_write(vma->anon_vma);
> > > > > + if (is_pmd_order(order)) {
> > > > > + anon_vma_unlock_write(vma->anon_vma);
> > > > > + anon_vma_locked = false;
> > > > > + }
> > > > >
> > > > > result = __collapse_huge_page_copy(pte, folio, pmd, _pmd,
> > > > > - vma, address, pte_ptl,
> > > > > - HPAGE_PMD_ORDER,
> > > > > - &compound_pagelist);
> > > > > - pte_unmap(pte);
> > > > > + vma, start_addr, pte_ptl,
> > > > > + order, &compound_pagelist);
> > > > > if (unlikely(result != SCAN_SUCCEED))
> > > > > goto out_up_write;
> > > > >
> > > > > @@ -1345,18 +1348,32 @@ static enum scan_result collapse_huge_page(struct mm_struct *mm, unsigned long a
> > > > > * write.
> > > > > */
> > > > > __folio_mark_uptodate(folio);
> > > > > - pgtable = pmd_pgtable(_pmd);
> > > > > -
> > > > > spin_lock(pmd_ptl);
> > > > > - BUG_ON(!pmd_none(*pmd));
> > > > > - pgtable_trans_huge_deposit(mm, pmd, pgtable);
> > > > > - map_anon_folio_pmd_nopf(folio, pmd, vma, address);
> > > > > + WARN_ON_ONCE(!pmd_none(*pmd));
> > > > > + if (is_pmd_order(order)) {
> > > > > + pgtable = pmd_pgtable(_pmd);
> > > > > + pgtable_trans_huge_deposit(mm, pmd, pgtable);
> > > > > + map_anon_folio_pmd_nopf(folio, pmd, vma, pmd_addr);
> > > > > + } else {
> > > > > + /*
> > > > > + * set_ptes is called in map_anon_folio_pte_nopf with the
> > > > > + * pmd_ptl lock still held; this is safe as the PMD is expected
> > > >
> > > > PMD entry you mean?
> > > >
> > > > > + * to be none. The pmd entry is then repopulated below.
> > > > > + */
> > > > > + map_anon_folio_pte_nopf(folio, pte, vma, start_addr, /*uffd_wp=*/ false);
> > > >
> > > > So here we populate entries in the existing PTE _table_ to point at the new
> > > > order>0 folio? With arm64 of course doing transparent contpte stuff?
> > > >
> > > > > + smp_wmb(); /* make PTEs visible before PMD. See pmd_install() */
> > > > > + pmd_populate(mm, pmd, pmd_pgtable(_pmd));
> > > >
> > > > And then we reinstall the pre-existing PMD _entry_ from none -> what it was
> > > > before?
> > > >
> > > > > + }
> > > > > spin_unlock(pmd_ptl);
> > > > >
> > > > > folio = NULL;
> > > > >
> > > > > result = SCAN_SUCCEED;
> > > > > out_up_write:
> > > > > + if (anon_vma_locked)
> > > > > + anon_vma_unlock_write(vma->anon_vma);
> > > > > + if (pte)
> > > > > + pte_unmap(pte);
> > > > > mmap_write_unlock(mm);
> > > > > out_nolock:
> > > > > if (folio)
> > > > > @@ -1536,7 +1553,7 @@ static enum scan_result collapse_scan_pmd(struct mm_struct *mm,
> > > > > /* collapse_huge_page expects the lock to be dropped before calling */
> > > > > mmap_read_unlock(mm);
> > > > > result = collapse_huge_page(mm, start_addr, referenced,
> > > > > - unmapped, cc);
> > > > > + unmapped, cc, HPAGE_PMD_ORDER);
> > > > > /* collapse_huge_page will return with the mmap_lock released */
> > > > > *lock_dropped = true;
> > > > > }
> > > > > --
> > > > > 2.54.0
> > > > >
> > >
> > > Thanks, Lorenzo
> > >
> >
>
^ permalink raw reply
* [PATCH net-next v3 02/13] net: ethernet: oa_tc6: Handle the OA TC6 SPI protected mode
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Implement the OA TC6 standard defined protected mode for control (register
access) transactions. In addition to the current register access formats
the oa_tc6 driver handles, 1's complement values of the data field
are included (by both the host and the MACPHY) in the SPI transfer frames.
This feature acts as an integrity check.
Control write transactions look like this:
|<- 32 bits ->|<--- data_size --->|<- 32 bits ->|
MOSI: | ctrl header | reg write data | ignored |
MISO: | (discard) | echoed ctrl hdr | echoed data |
data_size (LEN = number of registers to read in a sequence):
Unprotected: 32 x (LEN + 1) bits
Protected: 2 x 32 x (LEN + 1) bits
Control read transaction:
|<- 32 bits ->|<--- 32 bits --> |<- data_size ->|
MOSI: | ctrl header | ignored ... |
MISO: | (discard) | echoed ctrl hdr | reg read data |
data_size (LEN = number of registers to read in a sequence):
Unprotected: 32 x (LEN + 1) bits
Protected: 2 x 32 x (LEN + 1) bits
Register data format ("reg write data" and "reg read data"):
Unprotected:
| W1 (normal) | W2 (normal) | ... | Wx (normal) |
Protected:
| W1 (normal) | W1 (complement) | ... | Wx (normal) | Wx (complement)|
The protected mode state can be read from the bit 5 of CONFIG0 (0x4)
register, and this setting is usually only configured during the
MACPHY's reset (depending on the device it can be done by setting the
state of a pin). We can read the protected mode configuration before any
other register access and since the SPI transfer is initially sized for an
unprotected read, the MACPHY's complement words are never clocked out
and no checking is required. The data transactions (Ethernet frames)
remain unchanged.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- no change
v2 changelog:
- Updated OA_TC6_CTRL_SPI_BUF_SIZE to always alloc the control
transaction buffer size required by the protected mode, instead of
calling krealloc if the PROTE bit is set.
- Formatting to fit the 80 character column limit.
---
drivers/net/ethernet/oa_tc6.c | 93 +++++++++++++++++++++++++++++++++++--------
1 file changed, 76 insertions(+), 17 deletions(-)
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 91a906a7918a..baba5aad84df 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -24,6 +24,7 @@
#define OA_TC6_REG_CONFIG0 0x0004
#define CONFIG0_SYNC BIT(15)
#define CONFIG0_ZARFE_ENABLE BIT(12)
+#define CONFIG0_PROTE BIT(5)
/* Status Register #0 */
#define OA_TC6_REG_STATUS0 0x0008
@@ -87,14 +88,17 @@
#define OA_TC6_PHY_C45_AUTO_NEG_MMS5 5 /* MMD 7 */
#define OA_TC6_PHY_C45_POWER_UNIT_MMS6 6 /* MMD 13 */
+#define OA_TC6_CTRL_PROT_REPLY_SIZE 4
#define OA_TC6_CTRL_HEADER_SIZE 4
#define OA_TC6_CTRL_REG_VALUE_SIZE 4
#define OA_TC6_CTRL_IGNORED_SIZE 4
#define OA_TC6_CTRL_MAX_REGISTERS 128
-#define OA_TC6_CTRL_SPI_BUF_SIZE (OA_TC6_CTRL_HEADER_SIZE +\
- (OA_TC6_CTRL_MAX_REGISTERS *\
- OA_TC6_CTRL_REG_VALUE_SIZE) +\
- OA_TC6_CTRL_IGNORED_SIZE)
+#define OA_TC6_CTRL_SPI_BUF_SIZE (OA_TC6_CTRL_HEADER_SIZE +\
+ (OA_TC6_CTRL_MAX_REGISTERS *\
+ (OA_TC6_CTRL_REG_VALUE_SIZE +\
+ OA_TC6_CTRL_PROT_REPLY_SIZE)) +\
+ OA_TC6_CTRL_IGNORED_SIZE)
+
#define OA_TC6_CHUNK_PAYLOAD_SIZE 64
#define OA_TC6_DATA_HEADER_SIZE 4
#define OA_TC6_CHUNK_SIZE (OA_TC6_DATA_HEADER_SIZE +\
@@ -129,6 +133,7 @@ struct oa_tc6 {
u8 rx_chunks_available;
bool rx_buf_overflow;
bool int_flag;
+ bool prot_ctrl;
};
enum oa_tc6_header_type {
@@ -212,25 +217,36 @@ static void oa_tc6_update_ctrl_write_data(struct oa_tc6 *tc6, u32 value[],
{
__be32 *tx_buf = tc6->spi_ctrl_tx_buf + OA_TC6_CTRL_HEADER_SIZE;
- for (int i = 0; i < length; i++)
+ for (int i = 0; i < length; i++) {
*tx_buf++ = cpu_to_be32(value[i]);
+ if (tc6->prot_ctrl)
+ *tx_buf++ = cpu_to_be32(~value[i]);
+ }
}
-static u16 oa_tc6_calculate_ctrl_buf_size(u8 length)
+static u16 oa_tc6_calculate_ctrl_buf_size(u8 length, bool ctrl_prot)
{
+ u32 reply_size = OA_TC6_CTRL_REG_VALUE_SIZE;
+
+ if (ctrl_prot)
+ reply_size += OA_TC6_CTRL_PROT_REPLY_SIZE;
+
/* Control command consists 4 bytes header + 4 bytes register value for
- * each register + 4 bytes ignored value.
+ * each register (+ 4 bytes for the register value complement in case
+ * protected mode is used) + 4 bytes ignored value.
*/
- return OA_TC6_CTRL_HEADER_SIZE + OA_TC6_CTRL_REG_VALUE_SIZE * length +
+ return OA_TC6_CTRL_HEADER_SIZE + reply_size * length +
OA_TC6_CTRL_IGNORED_SIZE;
}
static void oa_tc6_prepare_ctrl_spi_buf(struct oa_tc6 *tc6, u32 address,
u32 value[], u8 length,
- enum oa_tc6_register_op reg_op)
+ enum oa_tc6_register_op reg_op,
+ u16 buf_size)
{
__be32 *tx_buf = tc6->spi_ctrl_tx_buf;
+ memset(tx_buf, 0, buf_size);
*tx_buf = oa_tc6_prepare_ctrl_header(address, length, reg_op);
if (reg_op == OA_TC6_CTRL_REG_WRITE)
@@ -253,10 +269,12 @@ static int oa_tc6_check_ctrl_write_reply(struct oa_tc6 *tc6, u8 size)
return 0;
}
-static int oa_tc6_check_ctrl_read_reply(struct oa_tc6 *tc6, u8 size)
+static int oa_tc6_check_ctrl_read_reply(struct oa_tc6 *tc6, u8 length)
{
- u32 *rx_buf = tc6->spi_ctrl_rx_buf + OA_TC6_CTRL_IGNORED_SIZE;
- u32 *tx_buf = tc6->spi_ctrl_tx_buf;
+ __be32 *rx_buf = tc6->spi_ctrl_rx_buf + OA_TC6_CTRL_IGNORED_SIZE;
+ __be32 *tx_buf = tc6->spi_ctrl_tx_buf;
+ u32 complement;
+ u32 reply;
/* The echoed control read header must match with the one that was
* transmitted.
@@ -264,6 +282,20 @@ static int oa_tc6_check_ctrl_read_reply(struct oa_tc6 *tc6, u8 size)
if (*tx_buf != *rx_buf)
return -EPROTO;
+ if (tc6->prot_ctrl) {
+ /* Skip past the echoed header to the value/complement pairs */
+ rx_buf += 1;
+ for (int i = 0; i < length; i++) {
+ reply = be32_to_cpu(rx_buf[0]);
+ complement = be32_to_cpu(rx_buf[1]);
+
+ if (complement != ~reply)
+ return -EPROTO;
+
+ rx_buf += 2;
+ }
+ }
+
return 0;
}
@@ -273,8 +305,13 @@ static void oa_tc6_copy_ctrl_read_data(struct oa_tc6 *tc6, u32 value[],
__be32 *rx_buf = tc6->spi_ctrl_rx_buf + OA_TC6_CTRL_IGNORED_SIZE +
OA_TC6_CTRL_HEADER_SIZE;
- for (int i = 0; i < length; i++)
+ for (int i = 0; i < length; i++) {
value[i] = be32_to_cpu(*rx_buf++);
+
+ /* skip complement word */
+ if (tc6->prot_ctrl)
+ rx_buf++;
+ }
}
static int oa_tc6_perform_ctrl(struct oa_tc6 *tc6, u32 address, u32 value[],
@@ -283,10 +320,10 @@ static int oa_tc6_perform_ctrl(struct oa_tc6 *tc6, u32 address, u32 value[],
u16 size;
int ret;
- /* Prepare control command and copy to SPI control buffer */
- oa_tc6_prepare_ctrl_spi_buf(tc6, address, value, length, reg_op);
+ size = oa_tc6_calculate_ctrl_buf_size(length, tc6->prot_ctrl);
- size = oa_tc6_calculate_ctrl_buf_size(length);
+ /* Prepare control command and copy to SPI control buffer */
+ oa_tc6_prepare_ctrl_spi_buf(tc6, address, value, length, reg_op, size);
/* Perform SPI transfer */
ret = oa_tc6_spi_transfer(tc6, OA_TC6_CTRL_HEADER, size);
@@ -301,7 +338,7 @@ static int oa_tc6_perform_ctrl(struct oa_tc6 *tc6, u32 address, u32 value[],
return oa_tc6_check_ctrl_write_reply(tc6, size);
/* Check echoed/received control read command reply for errors */
- ret = oa_tc6_check_ctrl_read_reply(tc6, size);
+ ret = oa_tc6_check_ctrl_read_reply(tc6, length);
if (ret)
return ret;
@@ -1224,6 +1261,20 @@ netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, struct sk_buff *skb)
}
EXPORT_SYMBOL_GPL(oa_tc6_start_xmit);
+static int oa_tc6_check_ctrl_protection(struct oa_tc6 *tc6)
+{
+ u32 regval;
+ int ret;
+
+ ret = oa_tc6_read_register(tc6, OA_TC6_REG_CONFIG0, ®val);
+ if (ret)
+ return ret;
+
+ tc6->prot_ctrl = FIELD_GET(CONFIG0_PROTE, regval);
+
+ return 0;
+}
+
/**
* oa_tc6_init - allocates and initializes oa_tc6 structure.
* @spi: device with which data will be exchanged.
@@ -1276,6 +1327,14 @@ struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev)
if (!tc6->spi_data_rx_buf)
return NULL;
+ /* Check the PROTE bit status so that we can reset the device */
+ ret = oa_tc6_check_ctrl_protection(tc6);
+ if (ret) {
+ dev_err(&tc6->spi->dev,
+ "Failed to check the protection mode: %d\n", ret);
+ return NULL;
+ }
+
ret = oa_tc6_sw_reset_macphy(tc6);
if (ret) {
dev_err(&tc6->spi->dev,
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 00/13] net: Add ADIN1140 support
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
This series introduces support for the ADIN1140 (also called AD3306)
10BASE-T1S single port MACPHY. The device integrates the MAC and PHY in
the same package. The communication with the host CPU is done through an
SPI interface, using the Open Alliance TC6 protocol for control and data
transactions. As a result, the oa_tc6 framework is used to implement
the communication with the device (register accesses and Ethernet frame
RX/TX).
The MAC and PHY are connected internally using an MII and MDIO bus.
The PHY is a half duplex 10Mbps device, which implements both the PLCA
RS (IEEE 802.3 clause 148) and CSMA/CD methods of accessing the Ethernet
medium. The 10BASE-T1S standard allows multiple PHY devices to be
connected (in parallel) on the same single twisted pair network segment,
so PLCA can be configured in order to provide a fair access scheme to
all the nodes and reduce the jitter introduced by the unordered CSMA/CD
transmits. The PHY's internal register map can be accessed using the
direct MDIO mode of the OA TC6. The control, status, phy id 1 & 2 C22
registers are mapped to the 0xFF00 - 0xFF03 range. As for C45
addressable devices, the PHY has PCS, PMA and PLCA blocks.
The oa_tc6 framework patches are changes that would make the library
usable by the subsequent ADIN1140 MAC driver.
The protected mode patch is required because the ADIN1140 only allows
protected mode OA TC6 control transactions, which the oa_tc6 framework
doesn't currently implement.
The OA_TC6_BROKEN_PHY quirk patch is required in order to allow the MAC
driver to have a custom implementation for the mii_bus access methods as a
workaround for hardware issues:
1. The OA TC6 standard defines the direct and indirect access modes for
MDIO transactions. The ADIN1140 incorrectly advertises indirect mode
only (supported capabilities register - 0x2, bit 9), while actually
implementing just the direct mode. We cannot rely on the CAP register
to choose an access method (which oa_tc6 does by default, even though
it only implements the direct mode), so the driver has to use its
own.
2. The ADIN1140 cannot access the C22 register space of the internal
PHY, while the PHY is busy receiving frames. If that happens, the
CONFIG0 and CONFIG2 registers of the MAC will get corrupted and the
data transfer will stop. Those two registers configure settings for
the transfer protocol between the MAC and host, so the value for some
of their subfields shouldn't be changed while the netdev is up.
Since we know the PHY is internal, the MAC driver can implement a
custom mii_bus, which can intercept C22 accesses. Most of the
registers mapped in the 0x0 - 0x3 range (the only ones the PHY offers)
are read only, and their value can be read from somewhere else (e.g
the PHYID 1 & 2 have the same value as 0x1 in the MAC memory map).
C45 accesses do not cause this issue, so we can properly implement
them.
Even though they have different driver, the MAC one cannot function
without the PHY driver, since the PHY is not compatible with the generic
c22 driver. As such CONFIG_ADIN1140 selects CONFIG_ADIN1140_PHY.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
Changes in v3:
- Added adi,ad3306 as a fallback compatible in the DT schema.
- Keep the current spi_device and net_device parameters for
oa_tc6_init() and add a new struct parameter for passing the quirk
flags, insted of changing the function to take a single config
parameter.
- Add the oa_tc6_write_register_mms() and oa_tc6_read_register_mms()
functions instead of defining a new macro for MMS and register address
formatting.
- Split the OA TC6 register address macro exports patch into two
different commits (export + rename).
- Use the devres API for mii_bus and net_device management in the
ADIN1140 driver.
- Fix a bug related to the destination MAC filter mask in the ADIN1140
driver.
- Link to v2: https://lore.kernel.org/r/20260527-adin1140-driver-v2-0-37e5c8d4e0a0@analog.com
Changes in v2:
- Add the OA_TC6_BROKEN_PHY quirk flag to the oa_tc6 framework in order
to allow ethernet drivers to manage their own mii_bus struct and PHY.
- Move the OA TC6 standard register definitions in the oa_tc6.h header
so other drivers can use them. Do the same for the C45 access
functions
- Add the genphy_read_mmd_c45 and genphy_write_mmd_c45 functions to
genphy, which allow drivers that have been discovered over C22 to make
direct C45 operations, without using the 0xD and 0xE register. The
change was implemented based on the suggestion in this thread:
https://lore.kernel.org/all/CY8PR02MB9249CDA8F2C560FDD0F662D883382@CY8PR02MB9249.namprd02.prod.outlook.com/
- Link to v1: https://lore.kernel.org/r/20260503-adin1140-driver-v1-0-dd043cdd88f0@analog.com
---
Ciprian Regus (13):
dt-bindings: net: Add ADIN1140
net: ethernet: oa_tc6: Handle the OA TC6 SPI protected mode
net: ethernet: oa_tc6: add OA_TC6_BROKEN_PHY quirk flag
net: ethernet: oa_tc6: Export the C45 access functions
net: ethernet: oa_tc6: Export standard defined registers
net: ethernet: oa_tc6: Add the OA_TC6_ prefix to standard registers
net: ethernet: oa_tc6: Add read_mms/write_mms register access functions
net: ethernet: oa_tc6: Use the read_mms/write_mms functions for C45
net: ethernet: oa_tc6: Add new register address defines
net: phy: add generic helpers for direct C45 MMD access
net: phy: microchip-t1s: use generic C45 MMD access helpers
net: phy: Add support for the ADIN1140 PHY
net: ethernet: adi: Add a driver for the ADIN1140 MACPHY
.../devicetree/bindings/net/adi,adin1140.yaml | 71 ++
Documentation/networking/oa-tc6-framework.rst | 3 +-
MAINTAINERS | 15 +
drivers/net/ethernet/adi/Kconfig | 12 +
drivers/net/ethernet/adi/Makefile | 1 +
drivers/net/ethernet/adi/adin1140.c | 815 +++++++++++++++++++++
drivers/net/ethernet/microchip/lan865x/lan865x.c | 2 +-
drivers/net/ethernet/oa_tc6.c | 262 ++++---
drivers/net/phy/Kconfig | 6 +
drivers/net/phy/Makefile | 1 +
drivers/net/phy/adin1140-phy.c | 72 ++
drivers/net/phy/microchip_t1s.c | 32 +-
drivers/net/phy/phy_device.c | 25 +
include/linux/oa_tc6.h | 73 +-
include/linux/phy.h | 3 +
15 files changed, 1266 insertions(+), 127 deletions(-)
---
base-commit: b217a5c25fe4a5fdb63f57d028a88da470601f57
change-id: 20260429-adin1140-driver-93ae0d376318
Best regards,
--
Ciprian Regus <ciprian.regus@analog.com>
^ permalink raw reply
* [PATCH net-next v3 01/13] dt-bindings: net: Add ADIN1140
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
The ADIN1140 is a single port 10BASE-T1S Ethernet controller that
includes both the MAC and a PHY in the same package.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- set adi,ad3306 as a fallback compatible.
v2 changelog:
- Reorder the compatible entries in the dt schema (ad3306, adin1140).
- Removed "dt-bindings" from the commit title and message.
- Updated the DT example to use IRQ_TYPE_LEVEL_LOW instead of
IRQ_TYPE_EDGE_FALLING for the interrupt trigger condition.
- "implements" -> "tries to implement" in the description.
- Removed the MAINTAINERS entry, as it will be added in a later patch
in the series.
- Reordered as the first patch of the series
---
.../devicetree/bindings/net/adi,adin1140.yaml | 71 ++++++++++++++++++++++
1 file changed, 71 insertions(+)
diff --git a/Documentation/devicetree/bindings/net/adi,adin1140.yaml b/Documentation/devicetree/bindings/net/adi,adin1140.yaml
new file mode 100644
index 000000000000..739429c46253
--- /dev/null
+++ b/Documentation/devicetree/bindings/net/adi,adin1140.yaml
@@ -0,0 +1,71 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/net/adi,adin1140.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: ADI ADIN1140 10BASE-T1S MAC-PHY
+
+maintainers:
+ - Ciprian Regus <ciprian.regus@analog.com>
+
+description: |
+ The ADIN1140 (also called AD3306) is a low power single port
+ 10BASE-T1S MAC-PHY. It integrates an Ethernet PHY with a MAC
+ and all the associated analog circuitry.
+ The device tries to implement the Open Alliance TC6 10BASE-T1x MAC-PHY
+ Serial Interface specification and is compliant with the
+ IEEE 802.3cg-2019 Ethernet standard for 10 Mbps single pair
+ Ethernet (SPE). The device has a 4-wire SPI interface for
+ communication between the MAC and host processor.
+
+allOf:
+ - $ref: /schemas/net/ethernet-controller.yaml#
+ - $ref: /schemas/spi/spi-peripheral-props.yaml#
+
+properties:
+ compatible:
+ oneOf:
+ - items:
+ - const: adi,adin1140
+ - const: adi,ad3306
+ - const: adi,ad3306
+
+ reg:
+ maxItems: 1
+
+ spi-max-frequency:
+ maximum: 25000000
+
+ interrupts:
+ maxItems: 1
+ description: Interrupt from the MAC-PHY for receive data available
+ and error conditions
+
+required:
+ - compatible
+ - reg
+ - interrupts
+ - spi-max-frequency
+
+unevaluatedProperties: false
+
+examples:
+ - |
+ #include <dt-bindings/interrupt-controller/irq.h>
+
+ spi {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ ethernet@0 {
+ compatible = "adi,ad3306";
+ reg = <0>;
+ spi-max-frequency = <23000000>;
+
+ interrupt-parent = <&gpio>;
+ interrupts = <6 IRQ_TYPE_LEVEL_LOW>;
+
+ local-mac-address = [ 00 11 22 33 44 55 ];
+ };
+ };
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 03/13] net: ethernet: oa_tc6: add OA_TC6_BROKEN_PHY quirk flag
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Some MAC-PHY devices need custom MDIO bus access functions to work
around hardware issues. Add the OA_TC6_BROKEN_PHY quirk flag so drivers
can opt in to skip oa_tc6's internal PHY init and manage the PHY
themselves. When the flag is set, oa_tc6 skips MDIO bus registration,
PHY discovery and PHY connection, leaving these to the driver.
Drivers that do not set the flag retain the existing behavior. Update
lan865x and the framework documentation accordingly.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- add the oa_tc6_quirks struct to the oa_tc6_init() parameters (along
the spi_device and net_device), instead of adding everything in a
single struct.
v2 changelog:
- Added the quirk flag field in the oa_tc6_config struct and a first
value entry (OA_TC6_BROKEN_PHY) instead of the mii_bus struct.
---
Documentation/networking/oa-tc6-framework.rst | 3 ++-
drivers/net/ethernet/microchip/lan865x/lan865x.c | 2 +-
drivers/net/ethernet/oa_tc6.c | 14 +++++++++++++-
include/linux/oa_tc6.h | 11 ++++++++++-
4 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/Documentation/networking/oa-tc6-framework.rst b/Documentation/networking/oa-tc6-framework.rst
index fe2aabde923a..013824078cea 100644
--- a/Documentation/networking/oa-tc6-framework.rst
+++ b/Documentation/networking/oa-tc6-framework.rst
@@ -454,7 +454,8 @@ Device drivers API
The include/linux/oa_tc6.h defines the following functions:
.. c:function:: struct oa_tc6 *oa_tc6_init(struct spi_device *spi, \
- struct net_device *netdev)
+ struct net_device *netdev, \
+ struct oa_tc6_quirks *quirks)
Initialize OA TC6 lib.
diff --git a/drivers/net/ethernet/microchip/lan865x/lan865x.c b/drivers/net/ethernet/microchip/lan865x/lan865x.c
index 0277d9737369..26a2761332a5 100644
--- a/drivers/net/ethernet/microchip/lan865x/lan865x.c
+++ b/drivers/net/ethernet/microchip/lan865x/lan865x.c
@@ -346,7 +346,7 @@ static int lan865x_probe(struct spi_device *spi)
spi_set_drvdata(spi, priv);
INIT_WORK(&priv->multicast_work, lan865x_multicast_work_handler);
- priv->tc6 = oa_tc6_init(spi, netdev);
+ priv->tc6 = oa_tc6_init(spi, netdev, NULL);
if (!priv->tc6) {
ret = -ENODEV;
goto free_netdev;
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index baba5aad84df..2a72f0c4b009 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -134,6 +134,7 @@ struct oa_tc6 {
bool rx_buf_overflow;
bool int_flag;
bool prot_ctrl;
+ enum oa_tc6_quirk_flag quirk_flags;
};
enum oa_tc6_header_type {
@@ -580,6 +581,9 @@ static int oa_tc6_phy_init(struct oa_tc6 *tc6)
{
int ret;
+ if (tc6->quirk_flags & OA_TC6_BROKEN_PHY)
+ return 0;
+
ret = oa_tc6_check_phy_reg_direct_access_capability(tc6);
if (ret) {
netdev_err(tc6->netdev,
@@ -616,6 +620,9 @@ static int oa_tc6_phy_init(struct oa_tc6 *tc6)
static void oa_tc6_phy_exit(struct oa_tc6 *tc6)
{
+ if (tc6->quirk_flags & OA_TC6_BROKEN_PHY)
+ return;
+
phy_disconnect(tc6->phydev);
oa_tc6_mdiobus_unregister(tc6);
}
@@ -1279,11 +1286,13 @@ static int oa_tc6_check_ctrl_protection(struct oa_tc6 *tc6)
* oa_tc6_init - allocates and initializes oa_tc6 structure.
* @spi: device with which data will be exchanged.
* @netdev: network device interface structure.
+ * @quirks: device specific modifiers for the OA TC6 protocol.
*
* Return: pointer reference to the oa_tc6 structure if the MAC-PHY
* initialization is successful otherwise NULL.
*/
-struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev)
+struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev,
+ struct oa_tc6_quirks *quirks)
{
struct oa_tc6 *tc6;
int ret;
@@ -1298,6 +1307,9 @@ struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev)
mutex_init(&tc6->spi_ctrl_lock);
spin_lock_init(&tc6->tx_skb_lock);
+ if (quirks)
+ tc6->quirk_flags = quirks->quirk_flags;
+
/* Set the SPI controller to pump at realtime priority */
tc6->spi->rt = true;
if (spi_setup(tc6->spi) < 0)
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index 15f58e3c56c7..62e3d89f80ed 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -12,7 +12,16 @@
struct oa_tc6;
-struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev);
+enum oa_tc6_quirk_flag {
+ OA_TC6_BROKEN_PHY = BIT(0),
+};
+
+struct oa_tc6_quirks {
+ enum oa_tc6_quirk_flag quirk_flags;
+};
+
+struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev,
+ struct oa_tc6_quirks *quirks);
void oa_tc6_exit(struct oa_tc6 *tc6);
int oa_tc6_write_register(struct oa_tc6 *tc6, u32 address, u32 value);
int oa_tc6_write_registers(struct oa_tc6 *tc6, u32 address, u32 value[],
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 04/13] net: ethernet: oa_tc6: Export the C45 access functions
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
The C45 access functions can still be used by some Ethernet drivers
which set the OA_TC6_BROKEN_PHY flag. Export them.
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- no change
v2 changelog:
- New patch
---
drivers/net/ethernet/oa_tc6.c | 10 ++++++----
include/linux/oa_tc6.h | 4 ++++
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 2a72f0c4b009..b37e398e30e3 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -499,8 +499,8 @@ static int oa_tc6_get_phy_c45_mms(int devnum)
}
}
-static int oa_tc6_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum,
- int regnum)
+int oa_tc6_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum,
+ int regnum)
{
struct oa_tc6 *tc6 = bus->priv;
u32 regval;
@@ -516,9 +516,10 @@ static int oa_tc6_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum,
return regval;
}
+EXPORT_SYMBOL_GPL(oa_tc6_mdiobus_read_c45);
-static int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
- int regnum, u16 val)
+int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
+ int regnum, u16 val)
{
struct oa_tc6 *tc6 = bus->priv;
int ret;
@@ -529,6 +530,7 @@ static int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
return oa_tc6_write_register(tc6, (ret << 16) | regnum, val);
}
+EXPORT_SYMBOL_GPL(oa_tc6_mdiobus_write_c45);
static int oa_tc6_mdiobus_register(struct oa_tc6 *tc6)
{
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index 62e3d89f80ed..2660eefa3504 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -31,3 +31,7 @@ int oa_tc6_read_registers(struct oa_tc6 *tc6, u32 address, u32 value[],
u8 length);
netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, struct sk_buff *skb);
int oa_tc6_zero_align_receive_frame_enable(struct oa_tc6 *tc6);
+int oa_tc6_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum,
+ int regnum);
+int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
+ int regnum, u16 val);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 05/13] net: ethernet: oa_tc6: Export standard defined registers
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Move defines for standard Open Alliance TC6 register addresses and
subfields in the oa_tc6's header and add entries for the PHYID and
CONFIG2. As such, other ethernet drivers that rely on oa_tc6 can use
them directly.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- Only move the register definitions, without adding the OA_TC6_ prefix
v2 changelog:
- New patch
---
drivers/net/ethernet/oa_tc6.c | 48 -------------------------------------------
include/linux/oa_tc6.h | 48 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+), 48 deletions(-)
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index b37e398e30e3..97df38207827 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -11,45 +11,6 @@
#include <linux/phy.h>
#include <linux/oa_tc6.h>
-/* OPEN Alliance TC6 registers */
-/* Standard Capabilities Register */
-#define OA_TC6_REG_STDCAP 0x0002
-#define STDCAP_DIRECT_PHY_REG_ACCESS BIT(8)
-
-/* Reset Control and Status Register */
-#define OA_TC6_REG_RESET 0x0003
-#define RESET_SWRESET BIT(0) /* Software Reset */
-
-/* Configuration Register #0 */
-#define OA_TC6_REG_CONFIG0 0x0004
-#define CONFIG0_SYNC BIT(15)
-#define CONFIG0_ZARFE_ENABLE BIT(12)
-#define CONFIG0_PROTE BIT(5)
-
-/* Status Register #0 */
-#define OA_TC6_REG_STATUS0 0x0008
-#define STATUS0_RESETC BIT(6) /* Reset Complete */
-#define STATUS0_HEADER_ERROR BIT(5)
-#define STATUS0_LOSS_OF_FRAME_ERROR BIT(4)
-#define STATUS0_RX_BUFFER_OVERFLOW_ERROR BIT(3)
-#define STATUS0_TX_PROTOCOL_ERROR BIT(0)
-
-/* Buffer Status Register */
-#define OA_TC6_REG_BUFFER_STATUS 0x000B
-#define BUFFER_STATUS_TX_CREDITS_AVAILABLE GENMASK(15, 8)
-#define BUFFER_STATUS_RX_CHUNKS_AVAILABLE GENMASK(7, 0)
-
-/* Interrupt Mask Register #0 */
-#define OA_TC6_REG_INT_MASK0 0x000C
-#define INT_MASK0_HEADER_ERR_MASK BIT(5)
-#define INT_MASK0_LOSS_OF_FRAME_ERR_MASK BIT(4)
-#define INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK BIT(3)
-#define INT_MASK0_TX_PROTOCOL_ERR_MASK BIT(0)
-
-/* PHY Clause 22 registers base address and mask */
-#define OA_TC6_PHY_STD_REG_ADDR_BASE 0xFF00
-#define OA_TC6_PHY_STD_REG_ADDR_MASK 0x1F
-
/* Control command header */
#define OA_TC6_CTRL_HEADER_DATA_NOT_CTRL BIT(31)
#define OA_TC6_CTRL_HEADER_WRITE_NOT_READ BIT(29)
@@ -79,15 +40,6 @@
#define OA_TC6_DATA_FOOTER_END_BYTE_OFFSET GENMASK(13, 8)
#define OA_TC6_DATA_FOOTER_TX_CREDITS GENMASK(5, 1)
-/* PHY – Clause 45 registers memory map selector (MMS) as per table 6 in the
- * OPEN Alliance specification.
- */
-#define OA_TC6_PHY_C45_PCS_MMS2 2 /* MMD 3 */
-#define OA_TC6_PHY_C45_PMA_PMD_MMS3 3 /* MMD 1 */
-#define OA_TC6_PHY_C45_VS_PLCA_MMS4 4 /* MMD 31 */
-#define OA_TC6_PHY_C45_AUTO_NEG_MMS5 5 /* MMD 7 */
-#define OA_TC6_PHY_C45_POWER_UNIT_MMS6 6 /* MMD 13 */
-
#define OA_TC6_CTRL_PROT_REPLY_SIZE 4
#define OA_TC6_CTRL_HEADER_SIZE 4
#define OA_TC6_CTRL_REG_VALUE_SIZE 4
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index 2660eefa3504..bbc42758a313 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -10,6 +10,54 @@
#include <linux/etherdevice.h>
#include <linux/spi/spi.h>
+/* OPEN Alliance TC6 registers */
+/* Standard Capabilities Register */
+#define OA_TC6_REG_STDCAP 0x0002
+#define STDCAP_DIRECT_PHY_REG_ACCESS BIT(8)
+
+/* Reset Control and Status Register */
+#define OA_TC6_REG_RESET 0x0003
+#define RESET_SWRESET BIT(0) /* Software Reset */
+
+/* Configuration Register #0 */
+#define OA_TC6_REG_CONFIG0 0x0004
+#define CONFIG0_SYNC BIT(15)
+#define CONFIG0_ZARFE_ENABLE BIT(12)
+#define CONFIG0_PROTE BIT(5)
+
+/* Status Register #0 */
+#define OA_TC6_REG_STATUS0 0x0008
+#define STATUS0_RESETC BIT(6) /* Reset Complete */
+#define STATUS0_HEADER_ERROR BIT(5)
+#define STATUS0_LOSS_OF_FRAME_ERROR BIT(4)
+#define STATUS0_RX_BUFFER_OVERFLOW_ERROR BIT(3)
+#define STATUS0_TX_PROTOCOL_ERROR BIT(0)
+
+/* Buffer Status Register */
+#define OA_TC6_REG_BUFFER_STATUS 0x000B
+#define BUFFER_STATUS_TX_CREDITS_AVAILABLE GENMASK(15, 8)
+#define BUFFER_STATUS_RX_CHUNKS_AVAILABLE GENMASK(7, 0)
+
+/* Interrupt Mask Register #0 */
+#define OA_TC6_REG_INT_MASK0 0x000C
+#define INT_MASK0_HEADER_ERR_MASK BIT(5)
+#define INT_MASK0_LOSS_OF_FRAME_ERR_MASK BIT(4)
+#define INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK BIT(3)
+#define INT_MASK0_TX_PROTOCOL_ERR_MASK BIT(0)
+
+/* PHY Clause 22 registers base address and mask */
+#define OA_TC6_PHY_STD_REG_ADDR_BASE 0xFF00
+#define OA_TC6_PHY_STD_REG_ADDR_MASK 0x1F
+
+/* PHY – Clause 45 registers memory map selector (MMS) as per table 6 in the
+ * OPEN Alliance specification.
+ */
+#define OA_TC6_PHY_C45_PCS_MMS2 2 /* MMD 3 */
+#define OA_TC6_PHY_C45_PMA_PMD_MMS3 3 /* MMD 1 */
+#define OA_TC6_PHY_C45_VS_PLCA_MMS4 4 /* MMD 31 */
+#define OA_TC6_PHY_C45_AUTO_NEG_MMS5 5 /* MMD 7 */
+#define OA_TC6_PHY_C45_POWER_UNIT_MMS6 6 /* MMD 13 */
+
struct oa_tc6;
enum oa_tc6_quirk_flag {
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 06/13] net: ethernet: oa_tc6: Add the OA_TC6_ prefix to standard registers
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
The OA TC6 standard registers are currently exported in a header file.
Add the OA_TC6_ prefix to the register address and subfield mask macros
to avoid future naming conflicts.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- New patch
---
drivers/net/ethernet/oa_tc6.c | 35 ++++++++++++++++++-----------------
include/linux/oa_tc6.h | 36 ++++++++++++++++++------------------
2 files changed, 36 insertions(+), 35 deletions(-)
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 97df38207827..92da5bb74cc7 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -397,7 +397,7 @@ static int oa_tc6_check_phy_reg_direct_access_capability(struct oa_tc6 *tc6)
if (ret)
return ret;
- if (!(regval & STDCAP_DIRECT_PHY_REG_ACCESS))
+ if (!(regval & OA_TC6_STDCAP_DIRECT_PHY_REG_ACCESS))
return -ENODEV;
return 0;
@@ -598,7 +598,7 @@ static int oa_tc6_read_status0(struct oa_tc6 *tc6)
static int oa_tc6_sw_reset_macphy(struct oa_tc6 *tc6)
{
- u32 regval = RESET_SWRESET;
+ u32 regval = OA_TC6_RESET_SWRESET;
int ret;
ret = oa_tc6_write_register(tc6, OA_TC6_REG_RESET, regval);
@@ -607,7 +607,7 @@ static int oa_tc6_sw_reset_macphy(struct oa_tc6 *tc6)
/* Poll for soft reset complete for every 1ms until 1s timeout */
ret = readx_poll_timeout(oa_tc6_read_status0, tc6, regval,
- regval & STATUS0_RESETC,
+ regval & OA_TC6_STATUS0_RESETC,
STATUS0_RESETC_POLL_DELAY,
STATUS0_RESETC_POLL_TIMEOUT);
if (ret)
@@ -626,10 +626,10 @@ static int oa_tc6_unmask_macphy_error_interrupts(struct oa_tc6 *tc6)
if (ret)
return ret;
- regval &= ~(INT_MASK0_TX_PROTOCOL_ERR_MASK |
- INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK |
- INT_MASK0_LOSS_OF_FRAME_ERR_MASK |
- INT_MASK0_HEADER_ERR_MASK);
+ regval &= ~(OA_TC6_INT_MASK0_TX_PROTOCOL_ERR_MASK |
+ OA_TC6_INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK |
+ OA_TC6_INT_MASK0_LOSS_OF_FRAME_ERR_MASK |
+ OA_TC6_INT_MASK0_HEADER_ERR_MASK);
return oa_tc6_write_register(tc6, OA_TC6_REG_INT_MASK0, regval);
}
@@ -644,7 +644,7 @@ static int oa_tc6_enable_data_transfer(struct oa_tc6 *tc6)
return ret;
/* Enable configuration synchronization for data transfer */
- value |= CONFIG0_SYNC;
+ value |= OA_TC6_CONFIG0_SYNC;
return oa_tc6_write_register(tc6, OA_TC6_REG_CONFIG0, value);
}
@@ -687,25 +687,25 @@ static int oa_tc6_process_extended_status(struct oa_tc6 *tc6)
return ret;
}
- if (FIELD_GET(STATUS0_RX_BUFFER_OVERFLOW_ERROR, value)) {
+ if (FIELD_GET(OA_TC6_STATUS0_RX_BUFFER_OVERFLOW_ERROR, value)) {
tc6->rx_buf_overflow = true;
oa_tc6_cleanup_ongoing_rx_skb(tc6);
net_err_ratelimited("%s: Receive buffer overflow error\n",
tc6->netdev->name);
return -EAGAIN;
}
- if (FIELD_GET(STATUS0_TX_PROTOCOL_ERROR, value)) {
+ if (FIELD_GET(OA_TC6_STATUS0_TX_PROTOCOL_ERROR, value)) {
netdev_err(tc6->netdev, "Transmit protocol error\n");
return -ENODEV;
}
/* TODO: Currently loss of frame and header errors are treated as
* non-recoverable errors. They will be handled in the next version.
*/
- if (FIELD_GET(STATUS0_LOSS_OF_FRAME_ERROR, value)) {
+ if (FIELD_GET(OA_TC6_STATUS0_LOSS_OF_FRAME_ERROR, value)) {
netdev_err(tc6->netdev, "Loss of frame error\n");
return -ENODEV;
}
- if (FIELD_GET(STATUS0_HEADER_ERROR, value)) {
+ if (FIELD_GET(OA_TC6_STATUS0_HEADER_ERROR, value)) {
netdev_err(tc6->netdev, "Header error\n");
return -ENODEV;
}
@@ -1141,9 +1141,10 @@ static int oa_tc6_update_buffer_status_from_register(struct oa_tc6 *tc6)
if (ret)
return ret;
- tc6->tx_credits = FIELD_GET(BUFFER_STATUS_TX_CREDITS_AVAILABLE, value);
- tc6->rx_chunks_available = FIELD_GET(BUFFER_STATUS_RX_CHUNKS_AVAILABLE,
- value);
+ tc6->tx_credits = FIELD_GET(OA_TC6_BUFFER_STATUS_TX_CREDITS_AVAILABLE,
+ value);
+ tc6->rx_chunks_available =
+ FIELD_GET(OA_TC6_BUFFER_STATUS_RX_CHUNKS_AVAILABLE, value);
return 0;
}
@@ -1183,7 +1184,7 @@ int oa_tc6_zero_align_receive_frame_enable(struct oa_tc6 *tc6)
return ret;
/* Set Zero-Align Receive Frame Enable */
- regval |= CONFIG0_ZARFE_ENABLE;
+ regval |= OA_TC6_CONFIG0_ZARFE_ENABLE;
return oa_tc6_write_register(tc6, OA_TC6_REG_CONFIG0, regval);
}
@@ -1231,7 +1232,7 @@ static int oa_tc6_check_ctrl_protection(struct oa_tc6 *tc6)
if (ret)
return ret;
- tc6->prot_ctrl = FIELD_GET(CONFIG0_PROTE, regval);
+ tc6->prot_ctrl = FIELD_GET(OA_TC6_CONFIG0_PROTE, regval);
return 0;
}
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index bbc42758a313..bd369aac9c3b 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -13,37 +13,37 @@
/* OPEN Alliance TC6 registers */
/* Standard Capabilities Register */
#define OA_TC6_REG_STDCAP 0x0002
-#define STDCAP_DIRECT_PHY_REG_ACCESS BIT(8)
+#define OA_TC6_STDCAP_DIRECT_PHY_REG_ACCESS BIT(8)
/* Reset Control and Status Register */
#define OA_TC6_REG_RESET 0x0003
-#define RESET_SWRESET BIT(0) /* Software Reset */
+#define OA_TC6_RESET_SWRESET BIT(0) /* Software Reset */
/* Configuration Register #0 */
#define OA_TC6_REG_CONFIG0 0x0004
-#define CONFIG0_SYNC BIT(15)
-#define CONFIG0_ZARFE_ENABLE BIT(12)
-#define CONFIG0_PROTE BIT(5)
+#define OA_TC6_CONFIG0_SYNC BIT(15)
+#define OA_TC6_CONFIG0_ZARFE_ENABLE BIT(12)
+#define OA_TC6_CONFIG0_PROTE BIT(5)
/* Status Register #0 */
#define OA_TC6_REG_STATUS0 0x0008
-#define STATUS0_RESETC BIT(6) /* Reset Complete */
-#define STATUS0_HEADER_ERROR BIT(5)
-#define STATUS0_LOSS_OF_FRAME_ERROR BIT(4)
-#define STATUS0_RX_BUFFER_OVERFLOW_ERROR BIT(3)
-#define STATUS0_TX_PROTOCOL_ERROR BIT(0)
+#define OA_TC6_STATUS0_RESETC BIT(6) /* Reset Complete */
+#define OA_TC6_STATUS0_HEADER_ERROR BIT(5)
+#define OA_TC6_STATUS0_LOSS_OF_FRAME_ERROR BIT(4)
+#define OA_TC6_STATUS0_RX_BUFFER_OVERFLOW_ERROR BIT(3)
+#define OA_TC6_STATUS0_TX_PROTOCOL_ERROR BIT(0)
/* Buffer Status Register */
-#define OA_TC6_REG_BUFFER_STATUS 0x000B
-#define BUFFER_STATUS_TX_CREDITS_AVAILABLE GENMASK(15, 8)
-#define BUFFER_STATUS_RX_CHUNKS_AVAILABLE GENMASK(7, 0)
+#define OA_TC6_REG_BUFFER_STATUS 0x000B
+#define OA_TC6_BUFFER_STATUS_TX_CREDITS_AVAILABLE GENMASK(15, 8)
+#define OA_TC6_BUFFER_STATUS_RX_CHUNKS_AVAILABLE GENMASK(7, 0)
/* Interrupt Mask Register #0 */
-#define OA_TC6_REG_INT_MASK0 0x000C
-#define INT_MASK0_HEADER_ERR_MASK BIT(5)
-#define INT_MASK0_LOSS_OF_FRAME_ERR_MASK BIT(4)
-#define INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK BIT(3)
-#define INT_MASK0_TX_PROTOCOL_ERR_MASK BIT(0)
+#define OA_TC6_REG_INT_MASK0 0x000C
+#define OA_TC6_INT_MASK0_HEADER_ERR_MASK BIT(5)
+#define OA_TC6_INT_MASK0_LOSS_OF_FRAME_ERR_MASK BIT(4)
+#define OA_TC6_INT_MASK0_RX_BUFFER_OVERFLOW_ERR_MASK BIT(3)
+#define OA_TC6_INT_MASK0_TX_PROTOCOL_ERR_MASK BIT(0)
/* PHY Clause 22 registers base address and mask */
#define OA_TC6_PHY_STD_REG_ADDR_BASE 0xFF00
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 07/13] net: ethernet: oa_tc6: Add read_mms/write_mms register access functions
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
The Open Alliance TC6 standard defines multiple memory maps for the
MAC-PHY's register space. These are used to separate standard, vendor
and PHY MMD specific registers. Define register access functions that
allow the caller to specify the MMS.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- replace the OA_TC6_MMS_REG() macro with the register access functions
that allow passing an mms parameter.
v2 changelog:
- New patch
---
drivers/net/ethernet/oa_tc6.c | 47 +++++++++++++++++++++++++++++++++++++++++++
include/linux/oa_tc6.h | 4 ++++
2 files changed, 51 insertions(+)
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 92da5bb74cc7..3807265bf0b5 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -61,6 +61,9 @@
#define STATUS0_RESETC_POLL_DELAY 1000
#define STATUS0_RESETC_POLL_TIMEOUT 1000000
+#define OA_TC6_REG_MMS_MASK GENMASK(19, 16)
+#define OA_TC6_REG_ADDR_MASK GENMASK(15, 0)
+
/* Internal structure for MAC-PHY drivers */
struct oa_tc6 {
struct device *dev;
@@ -344,6 +347,28 @@ int oa_tc6_read_register(struct oa_tc6 *tc6, u32 address, u32 *value)
}
EXPORT_SYMBOL_GPL(oa_tc6_read_register);
+/**
+ * oa_tc6_read_register_mms - function for reading a MAC-PHY register in a
+ * memory map other than 0.
+ * @tc6: oa_tc6 struct.
+ * @mms: Memory map selector for the register.
+ * @address: register address of the MAC-PHY to be read.
+ * @value: value read from the @address register address of the MAC-PHY.
+ *
+ * Return: 0 on success otherwise failed.
+ */
+int oa_tc6_read_register_mms(struct oa_tc6 *tc6, u8 mms, u32 address,
+ u32 *value)
+{
+ u32 mms_reg;
+
+ mms_reg = FIELD_PREP(OA_TC6_REG_MMS_MASK, mms) |
+ FIELD_PREP(OA_TC6_REG_ADDR_MASK, address);
+
+ return oa_tc6_read_registers(tc6, mms_reg, value, 1);
+}
+EXPORT_SYMBOL_GPL(oa_tc6_read_register_mms);
+
/**
* oa_tc6_write_registers - function for writing multiple consecutive registers.
* @tc6: oa_tc6 struct.
@@ -388,6 +413,28 @@ int oa_tc6_write_register(struct oa_tc6 *tc6, u32 address, u32 value)
}
EXPORT_SYMBOL_GPL(oa_tc6_write_register);
+/**
+ * oa_tc6_write_register_mms - function for writing a MAC-PHY register in a
+ * memory map other than 0.
+ * @tc6: oa_tc6 struct.
+ * @mms: Memory map selector for the register.
+ * @address: register address of the MAC-PHY to be written.
+ * @value: value to be written in the @address register address of the MAC-PHY.
+ *
+ * Return: 0 on success otherwise failed.
+ */
+int oa_tc6_write_register_mms(struct oa_tc6 *tc6, u8 mms, u32 address,
+ u32 value)
+{
+ u32 mms_reg;
+
+ mms_reg = FIELD_PREP(OA_TC6_REG_MMS_MASK, mms) |
+ FIELD_PREP(OA_TC6_REG_ADDR_MASK, address);
+
+ return oa_tc6_write_registers(tc6, mms_reg, &value, 1);
+}
+EXPORT_SYMBOL_GPL(oa_tc6_write_register_mms);
+
static int oa_tc6_check_phy_reg_direct_access_capability(struct oa_tc6 *tc6)
{
u32 regval;
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index bd369aac9c3b..9fa4397303d1 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -72,9 +72,13 @@ struct oa_tc6 *oa_tc6_init(struct spi_device *spi, struct net_device *netdev,
struct oa_tc6_quirks *quirks);
void oa_tc6_exit(struct oa_tc6 *tc6);
int oa_tc6_write_register(struct oa_tc6 *tc6, u32 address, u32 value);
+int oa_tc6_write_register_mms(struct oa_tc6 *tc6, u8 mms, u32 address,
+ u32 value);
int oa_tc6_write_registers(struct oa_tc6 *tc6, u32 address, u32 value[],
u8 length);
int oa_tc6_read_register(struct oa_tc6 *tc6, u32 address, u32 *value);
+int oa_tc6_read_register_mms(struct oa_tc6 *tc6, u8 mms, u32 address,
+ u32 *value);
int oa_tc6_read_registers(struct oa_tc6 *tc6, u32 address, u32 value[],
u8 length);
netdev_tx_t oa_tc6_start_xmit(struct oa_tc6 *tc6, struct sk_buff *skb);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 08/13] net: ethernet: oa_tc6: Use the read_mms/write_mms functions for C45
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Accessing PHY MMD devices requires control transactions to registers in
a memory map other than 0. Replace the current formatting of the
register addresses with the oa_tc6_{read,write}_register_mms()
functions. While we're here, introduce the mms variable to store the
memory map returned by oa_tc6_get_phy_c45_mms() instead of ret, in order
to improve the code readability.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- New patch
---
drivers/net/ethernet/oa_tc6.c | 19 ++++++++++---------
1 file changed, 10 insertions(+), 9 deletions(-)
diff --git a/drivers/net/ethernet/oa_tc6.c b/drivers/net/ethernet/oa_tc6.c
index 3807265bf0b5..691d293b8ee2 100644
--- a/drivers/net/ethernet/oa_tc6.c
+++ b/drivers/net/ethernet/oa_tc6.c
@@ -503,13 +503,14 @@ int oa_tc6_mdiobus_read_c45(struct mii_bus *bus, int addr, int devnum,
{
struct oa_tc6 *tc6 = bus->priv;
u32 regval;
+ int mms;
int ret;
- ret = oa_tc6_get_phy_c45_mms(devnum);
- if (ret < 0)
- return ret;
+ mms = oa_tc6_get_phy_c45_mms(devnum);
+ if (mms < 0)
+ return mms;
- ret = oa_tc6_read_register(tc6, (ret << 16) | regnum, ®val);
+ ret = oa_tc6_read_register_mms(tc6, mms, regnum, ®val);
if (ret)
return ret;
@@ -521,13 +522,13 @@ int oa_tc6_mdiobus_write_c45(struct mii_bus *bus, int addr, int devnum,
int regnum, u16 val)
{
struct oa_tc6 *tc6 = bus->priv;
- int ret;
+ int mms;
- ret = oa_tc6_get_phy_c45_mms(devnum);
- if (ret < 0)
- return ret;
+ mms = oa_tc6_get_phy_c45_mms(devnum);
+ if (mms < 0)
+ return mms;
- return oa_tc6_write_register(tc6, (ret << 16) | regnum, val);
+ return oa_tc6_write_register_mms(tc6, mms, regnum, val);
}
EXPORT_SYMBOL_GPL(oa_tc6_mdiobus_write_c45);
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 10/13] net: phy: add generic helpers for direct C45 MMD access
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Some PHYs support direct C45 register access but not C22 indirect MMD
access (registers 0xD and 0xE). When discovered via C22, phylib routes
MMD access through the indirect path, which won't work on these
devices.
Add genphy_read_mmd_c45() and genphy_write_mmd_c45() as read_mmd/
write_mmd callbacks that bypass the C22 indirect path and use the bus
C45 accessors directly.
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- no change
v2 changelog:
- New patch
---
drivers/net/phy/phy_device.c | 25 +++++++++++++++++++++++++
include/linux/phy.h | 3 +++
2 files changed, 28 insertions(+)
diff --git a/drivers/net/phy/phy_device.c b/drivers/net/phy/phy_device.c
index 3370eb822017..d33d096e700d 100644
--- a/drivers/net/phy/phy_device.c
+++ b/drivers/net/phy/phy_device.c
@@ -2764,6 +2764,31 @@ int genphy_read_abilities(struct phy_device *phydev)
}
EXPORT_SYMBOL(genphy_read_abilities);
+/* Some PHYs support direct C45 register access but not C22 indirect
+ * MMD access (registers 13 and 14). When discovered via C22, phylib
+ * routes MMD access through the indirect path, which won't work on
+ * these devices. These helpers bypass indirect access and use the bus
+ * C45 accessors directly.
+ */
+int genphy_read_mmd_c45(struct phy_device *phydev, int devnum, u16 regnum)
+{
+ struct mii_bus *bus = phydev->mdio.bus;
+ int addr = phydev->mdio.addr;
+
+ return __mdiobus_c45_read(bus, addr, devnum, regnum);
+}
+EXPORT_SYMBOL(genphy_read_mmd_c45);
+
+int genphy_write_mmd_c45(struct phy_device *phydev, int devnum, u16 regnum,
+ u16 val)
+{
+ struct mii_bus *bus = phydev->mdio.bus;
+ int addr = phydev->mdio.addr;
+
+ return __mdiobus_c45_write(bus, addr, devnum, regnum, val);
+}
+EXPORT_SYMBOL(genphy_write_mmd_c45);
+
/* This is used for the phy device which doesn't support the MMD extended
* register access, but it does have side effect when we are trying to access
* the MMD register via indirect method.
diff --git a/include/linux/phy.h b/include/linux/phy.h
index 199a7aaa341b..432d44188dbc 100644
--- a/include/linux/phy.h
+++ b/include/linux/phy.h
@@ -2297,6 +2297,9 @@ static inline int genphy_no_config_intr(struct phy_device *phydev)
{
return 0;
}
+int genphy_read_mmd_c45(struct phy_device *phydev, int devnum, u16 regnum);
+int genphy_write_mmd_c45(struct phy_device *phydev, int devnum, u16 regnum,
+ u16 val);
int genphy_read_mmd_unsupported(struct phy_device *phdev, int devad,
u16 regnum);
int genphy_write_mmd_unsupported(struct phy_device *phdev, int devnum,
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 11/13] net: phy: microchip-t1s: use generic C45 MMD access helpers
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Replace the driver specific lan865x_phy_read_mmd() and
lan865x_phy_write_mmd() with the shared genphy_read_mmd_c45() and
genphy_write_mmd_c45() helpers.
No functional change.
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- no change
v2 changelog:
- New patch
---
drivers/net/phy/microchip_t1s.c | 32 ++------------------------------
1 file changed, 2 insertions(+), 30 deletions(-)
diff --git a/drivers/net/phy/microchip_t1s.c b/drivers/net/phy/microchip_t1s.c
index e601d56b2507..73c23d311d72 100644
--- a/drivers/net/phy/microchip_t1s.c
+++ b/drivers/net/phy/microchip_t1s.c
@@ -506,34 +506,6 @@ static int lan86xx_read_status(struct phy_device *phydev)
return 0;
}
-/* OPEN Alliance 10BASE-T1x compliance MAC-PHYs will have both C22 and
- * C45 registers space. If the PHY is discovered via C22 bus protocol it assumes
- * it uses C22 protocol and always uses C22 registers indirect access to access
- * C45 registers. This is because, we don't have a clean separation between
- * C22/C45 register space and C22/C45 MDIO bus protocols. Resulting, PHY C45
- * registers direct access can't be used which can save multiple SPI bus access.
- * To support this feature, set .read_mmd/.write_mmd in the PHY driver to call
- * .read_c45/.write_c45 in the OPEN Alliance framework
- * drivers/net/ethernet/oa_tc6.c
- */
-static int lan865x_phy_read_mmd(struct phy_device *phydev, int devnum,
- u16 regnum)
-{
- struct mii_bus *bus = phydev->mdio.bus;
- int addr = phydev->mdio.addr;
-
- return __mdiobus_c45_read(bus, addr, devnum, regnum);
-}
-
-static int lan865x_phy_write_mmd(struct phy_device *phydev, int devnum,
- u16 regnum, u16 val)
-{
- struct mii_bus *bus = phydev->mdio.bus;
- int addr = phydev->mdio.addr;
-
- return __mdiobus_c45_write(bus, addr, devnum, regnum, val);
-}
-
static struct phy_driver microchip_t1s_driver[] = {
{
PHY_ID_MATCH_EXACT(PHY_ID_LAN867X_REVB1),
@@ -584,8 +556,8 @@ static struct phy_driver microchip_t1s_driver[] = {
.features = PHY_BASIC_T1S_P2MP_FEATURES,
.config_init = lan865x_revb_config_init,
.read_status = lan86xx_read_status,
- .read_mmd = lan865x_phy_read_mmd,
- .write_mmd = lan865x_phy_write_mmd,
+ .read_mmd = genphy_read_mmd_c45,
+ .write_mmd = genphy_write_mmd_c45,
.get_plca_cfg = genphy_c45_plca_get_cfg,
.set_plca_cfg = lan86xx_plca_set_cfg,
.get_plca_status = genphy_c45_plca_get_status,
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 09/13] net: ethernet: oa_tc6: Add new register address defines
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Add macro defines for the CONFIG2 register and the MMS1 memory map.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- New patch
---
include/linux/oa_tc6.h | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/include/linux/oa_tc6.h b/include/linux/oa_tc6.h
index 9fa4397303d1..e6eca352b2c0 100644
--- a/include/linux/oa_tc6.h
+++ b/include/linux/oa_tc6.h
@@ -25,6 +25,9 @@
#define OA_TC6_CONFIG0_ZARFE_ENABLE BIT(12)
#define OA_TC6_CONFIG0_PROTE BIT(5)
+/* Configuration Register #2 */
+#define OA_TC6_REG_CONFIG2 0x0006
+
/* Status Register #0 */
#define OA_TC6_REG_STATUS0 0x0008
#define OA_TC6_STATUS0_RESETC BIT(6) /* Reset Complete */
@@ -49,6 +52,9 @@
#define OA_TC6_PHY_STD_REG_ADDR_BASE 0xFF00
#define OA_TC6_PHY_STD_REG_ADDR_MASK 0x1F
+/* Vendor specific memory map. */
+#define OA_TC6_VEND_MMS1 1
+
/* PHY – Clause 45 registers memory map selector (MMS) as per table 6 in the
* OPEN Alliance specification.
*/
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 13/13] net: ethernet: adi: Add a driver for the ADIN1140 MACPHY
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Add a driver for ADIN1140. The device is a 10BASE-T1S MAC-PHY
(integrated in the same package) that connects to a CPU over an SPI bus,
and implements the Open Alliance TC6 protocol for control and frame
transfers. As such, this driver relies on oa_tc6 for the communication
with the device. The device has an alternative name (AD3306), so the
driver can be probed using one of the two compatible strings.
For control transactions, ADIN1140 only implements the protected mode.
The driver has a custom implementation for the mii_bus access methods as a
workaround for hardware issues:
1. The OA TC6 standard defines the direct and indirect access modes for
MDIO transactions. The ADIN1140 incorrectly advertises indirect mode
only (supported capabilities register - 0x2, bit 9), while actually
implementing just the direct mode. We cannot rely on the CAP register
to choose an access method (which oa_tc6 does by default, even though
it only implements the direct mode), so the driver has to use its
own.
2. The ADIN1140 cannot access the C22 register space of the internal
PHY, while the PHY is busy receiving frames. If that happens, the
CONFIG0 and CONFIG2 registers of the MAC will get corrupted and the
data transfer will stop. Those two registers configure settings for
the transfer protocol between the MAC and host, so the value for some
of their subfields shouldn't be changed while the netdev is up.
Since we know the PHY is internal, the MAC driver can implement a
custom mii_bus, which can intercept C22 accesses. Most of the
registers mapped in the 0x0 - 0x3 range (the only ones the PHY offers)
are read only, and their value can be read from somewhere else (e.g
the PHYID 1 & 2 have the same value as 0x1 in the MAC memory map).
For the fields that are R/W (loopback and AN/reset) in the control
register, the PHY driver already implements the set_loopback() and
config_aneg() functions. The C22 write function of the driver is a
no-op and is used to protect against the ioctl MDIO access path.
C45 accesses do not cause this issue, so we can properly implement
them.
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- Clear the unused destination address filter slots in
adin1140_rx_mode_work(). This is required in case we remove
multicast or unicast addresses from a netdev.
- The device only allows destination MAC masks for the first 2 slots.
Fix adin1140_mac_filter_set() to take this into account.
- use oa_tc6_{write,read}_register_mms, since the OA_TC6_MMS_REG()
macro was removed. Update the register address defines accordingly.
- use the devres API for mdio and netdev alloc/register instead of
manually managing those.
- use dev_err_probe() in several places to simplify logging during
probe.
- use scoped_guard() instead of spin_lock/unlock(). Had to break some
sequences in their own function to fit the 80 character limit.
- fix the comment describing the reason for skb padding in the TX path.
v2 changelog:
- Exported statistics that match the ethtool_stats entries as such and
kept the other ones custom, using ethtool strings.
- Used phy_do_ioctl_running() for ndo_eth_ioctl.
- Adapted the mii_bus and PHY handling to the newly added
OA_TC6_BROKEN_PHY flag for oa_tc6.
- Used the oa_tc6_mdiobus_read_c45/oa_tc6_mdiobus_write_c45 functions
for the C45 read/write mii_bus operations.
- Removed OA TC6 register definitions (e.g CONFIG2) from the adin1140
driver and instead used the ones exported from oa_tc6.h
- Used OA_TC6_MMS_REG to define MMS registers instead of
ADIN1140_MMS_REG.
- Returned default values for the MII_PHYSID1/MII_PHYSID2.
- Set the mii_bus->phy_mask, since the the same PHY will be registered
32 times otherwise.
- Updated the MAINTAINERS entry to include the dt-bindings.
---
MAINTAINERS | 8 +
drivers/net/ethernet/adi/Kconfig | 12 +
drivers/net/ethernet/adi/Makefile | 1 +
drivers/net/ethernet/adi/adin1140.c | 815 ++++++++++++++++++++++++++++++++++++
4 files changed, 836 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index eda74f3154dc..3d6da16c4312 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1857,6 +1857,14 @@ S: Supported
W: https://ez.analog.com/linux-software-drivers
F: drivers/dma/dma-axi-dmac.c
+ANALOG DEVICES INC ETHERNET DRIVERS
+M: Ciprian Regus <ciprian.regus@analog.com>
+L: netdev@vger.kernel.org
+S: Maintained
+W: https://ez.analog.com/linux-software-drivers
+F: Documentation/devicetree/bindings/net/adi,adin1140.yaml
+F: drivers/net/ethernet/adi/adin1140.c
+
ANALOG DEVICES INC ETHERNET PHY DRIVERS
M: Ciprian Regus <ciprian.regus@analog.com>
L: netdev@vger.kernel.org
diff --git a/drivers/net/ethernet/adi/Kconfig b/drivers/net/ethernet/adi/Kconfig
index 760a9a60bc15..bdb8ff7d15da 100644
--- a/drivers/net/ethernet/adi/Kconfig
+++ b/drivers/net/ethernet/adi/Kconfig
@@ -26,4 +26,16 @@ config ADIN1110
Say yes here to build support for Analog Devices ADIN1110
Low Power 10BASE-T1L Ethernet MAC-PHY.
+config ADIN1140
+ tristate "Analog Devices ADIN1140 MAC-PHY"
+ depends on SPI
+ select ADIN1140_PHY
+ select OA_TC6
+ help
+ Say yes here to build support for Analog Devices, Inc. ADIN1140
+ 10BASE-T1S Ethernet MAC-PHY.
+
+ To compile this driver as a module, choose M here. The module will be
+ called adin1140.
+
endif # NET_VENDOR_ADI
diff --git a/drivers/net/ethernet/adi/Makefile b/drivers/net/ethernet/adi/Makefile
index d0383d94303c..0390ca8ccc49 100644
--- a/drivers/net/ethernet/adi/Makefile
+++ b/drivers/net/ethernet/adi/Makefile
@@ -4,3 +4,4 @@
#
obj-$(CONFIG_ADIN1110) += adin1110.o
+obj-$(CONFIG_ADIN1140) += adin1140.o
diff --git a/drivers/net/ethernet/adi/adin1140.c b/drivers/net/ethernet/adi/adin1140.c
new file mode 100644
index 000000000000..358e9a11c993
--- /dev/null
+++ b/drivers/net/ethernet/adi/adin1140.c
@@ -0,0 +1,815 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Driver for Analog Devices, Inc. ADIN1140 10BASE-T1S MAC-PHY
+ *
+ * Copyright 2026 Analog Devices Inc.
+ */
+
+#include <linux/cleanup.h>
+#include <linux/etherdevice.h>
+#include <linux/kernel.h>
+#include <linux/mdio.h>
+#include <linux/module.h>
+#include <linux/oa_tc6.h>
+#include <linux/phy.h>
+
+#define ADIN1140_CONFIG2_FWD_UNK2HOST BIT(2)
+
+#define ADIN1140_MAC_P1_LOOP_ADDR_REG 0xC4
+
+#define ADIN1140_MAC_ADDR_FILT_UPR_REG 0x50
+#define ADIN1140_MAC_ADDR_FILT_APPLY2PORT1 BIT(30)
+#define ADIN1140_MAC_ADDR_FILT_TO_HOST BIT(16)
+
+#define ADIN1140_MAC_ADDR_FILT_LWR_REG 0x51
+
+#define ADIN1140_MAC_ADDR_MASK_UPR_REG 0x70
+#define ADIN1140_MAC_ADDR_MASK_LWR_REG 0x71
+
+#define ADIN1140_MAC_FILT_MC_SLOT 0U
+#define ADIN1140_MAC_FILT_BC_SLOT 1U
+#define ADIN1140_MAC_FILT_UC_SLOT 2U
+#define ADIN1140_MAC_FILT_MAX_SLOT 16U
+#define ADIN1140_MAC_FILT_MASK_LIMIT 2U
+
+#define ADIN1140_RX_FRAME_CNT 0xA1
+#define ADIN1140_RX_BC_FRAME_CNT 0xA2
+#define ADIN1140_RX_MC_FRAME_CNT 0xA3
+#define ADIN1140_RX_UC_FRAME_CNT 0xA4
+#define ADIN1140_RX_CRC_ERR_CNT 0xA5
+#define ADIN1140_RX_ALIGN_ERR_CNT 0xA6
+#define ADIN1140_RX_PREAMBLE_ERR_CNT 0xA7
+#define ADIN1140_RX_SHORT_ERR_CNT 0xA8
+#define ADIN1140_RX_LONG_ERR_CNT 0xA9
+#define ADIN1140_RX_PHY_ERR_CNT 0xAA
+#define ADIN1140_RX_DRP_FULL_CNT 0xAB
+#define ADIN1140_RX_DRP_FILTER_CNT 0xAD
+#define ADIN1140_RX_IFG_ERR_CNT 0xAE
+#define ADIN1140_TX_FRAME_CNT 0xB1
+#define ADIN1140_TX_BC_FRAME_CNT 0xB2
+#define ADIN1140_TX_MC_FRAME_CNT 0xB3
+#define ADIN1140_TX_UC_FRAME_CNT 0xB4
+#define ADIN1140_TX_SINGLE_COL_CNT 0xB5
+#define ADIN1140_TX_MULTI_COL_CNT 0xB6
+#define ADIN1140_TX_DEFERRED_CNT 0xB7
+#define ADIN1140_TX_LATE_COL_CNT 0xB8
+#define ADIN1140_TX_EXCESS_COL_CNT 0xB9
+#define ADIN1140_TX_UNDERRUN_CNT 0xBA
+
+/* ADIN1140_MAC_FILT_MAX_SLOT - 3 (multicast, broadcast and unicast
+ * reserved slots)
+ */
+#define ADIN1140_MAC_FILT_AVAIL 13U
+
+#define ADIN1140_PHY_CTRL_DEFAULT 0x1000
+#define ADIN1140_PHY_STATUS_DEFAULT 0x082D
+#define ADIN1140_PHY_ID1 0x0283
+#define ADIN1140_PHY_ID2 0xBE00
+
+#define ADIN1140_STATS_CHECK_DELAY (3 * HZ)
+
+enum adin1140_statistics_entry {
+ rx_frames,
+ rx_bc_frames,
+ rx_mc_frames,
+ rx_uc_frames,
+ rx_crc_errors,
+ rx_align_errors,
+ rx_preamble_errors,
+ rx_short_frame_errors,
+ rx_long_frame_errors,
+ rx_phy_errors,
+ rx_fifo_full_dropped,
+ rx_addr_filter_dropped,
+ rx_ifg_errors,
+ tx_frames,
+ tx_bc_frames,
+ tx_mc_frames,
+ tx_uc_frames,
+ tx_single_collision,
+ tx_multi_collision,
+ tx_deferred,
+ tx_late_collision,
+ tx_excess_collision,
+ tx_underrun,
+ ADIN1140_STATS_CNT,
+};
+
+struct adin1140_statistics_reg {
+ const char *name;
+ enum adin1140_statistics_entry idx;
+};
+
+struct adin1140_priv {
+ struct net_device *netdev;
+ struct oa_tc6 *tc6;
+ struct mii_bus *mdiobus;
+ struct phy_device *phydev;
+ struct work_struct rx_mode_work;
+ struct delayed_work stats_work;
+
+ /* Protects stats[] from concurrent updates in adin1140_stats_work
+ * and reads in the get_stats functions
+ */
+ spinlock_t stat_lock;
+ u64 stats[ADIN1140_STATS_CNT];
+};
+
+static const u32 adin1140_stat_regs[] = {
+ [rx_frames] = ADIN1140_RX_FRAME_CNT,
+ [rx_bc_frames] = ADIN1140_RX_BC_FRAME_CNT,
+ [rx_mc_frames] = ADIN1140_RX_MC_FRAME_CNT,
+ [rx_uc_frames] = ADIN1140_RX_UC_FRAME_CNT,
+ [rx_crc_errors] = ADIN1140_RX_CRC_ERR_CNT,
+ [rx_align_errors] = ADIN1140_RX_ALIGN_ERR_CNT,
+ [rx_preamble_errors] = ADIN1140_RX_PREAMBLE_ERR_CNT,
+ [rx_short_frame_errors] = ADIN1140_RX_SHORT_ERR_CNT,
+ [rx_long_frame_errors] = ADIN1140_RX_LONG_ERR_CNT,
+ [rx_phy_errors] = ADIN1140_RX_PHY_ERR_CNT,
+ [rx_fifo_full_dropped] = ADIN1140_RX_DRP_FULL_CNT,
+ [rx_addr_filter_dropped] = ADIN1140_RX_DRP_FILTER_CNT,
+ [rx_ifg_errors] = ADIN1140_RX_IFG_ERR_CNT,
+ [tx_frames] = ADIN1140_TX_FRAME_CNT,
+ [tx_bc_frames] = ADIN1140_TX_BC_FRAME_CNT,
+ [tx_mc_frames] = ADIN1140_TX_MC_FRAME_CNT,
+ [tx_uc_frames] = ADIN1140_TX_UC_FRAME_CNT,
+ [tx_single_collision] = ADIN1140_TX_SINGLE_COL_CNT,
+ [tx_multi_collision] = ADIN1140_TX_MULTI_COL_CNT,
+ [tx_deferred] = ADIN1140_TX_DEFERRED_CNT,
+ [tx_late_collision] = ADIN1140_TX_LATE_COL_CNT,
+ [tx_excess_collision] = ADIN1140_TX_EXCESS_COL_CNT,
+ [tx_underrun] = ADIN1140_TX_UNDERRUN_CNT,
+};
+
+static const struct adin1140_statistics_reg adin1140_stats[] = {
+ {.name = "rx_unicast_frames", .idx = rx_uc_frames},
+ {.name = "rx_preamble_errors", .idx = rx_preamble_errors},
+ {.name = "rx_ifg_errors", .idx = rx_ifg_errors},
+ {.name = "rx_addr_filter_dropped", .idx = rx_addr_filter_dropped},
+ {.name = "tx_unicast_frames", .idx = tx_uc_frames},
+};
+
+static int adin1140_mac_filter_set(struct adin1140_priv *priv,
+ const u8 *addr, const u8 *mask,
+ u8 slot)
+{
+ u32 reg_address;
+ u32 val;
+ int ret;
+
+ if (slot >= ADIN1140_MAC_FILT_MAX_SLOT)
+ return -ENOSPC;
+
+ reg_address = ADIN1140_MAC_ADDR_FILT_UPR_REG + 2 * slot;
+
+ ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_VEND_MMS1,
+ reg_address,
+ get_unaligned_be16(&addr[0]) |
+ ADIN1140_MAC_ADDR_FILT_APPLY2PORT1 |
+ ADIN1140_MAC_ADDR_FILT_TO_HOST);
+ if (ret)
+ return ret;
+
+ reg_address = ADIN1140_MAC_ADDR_FILT_LWR_REG + 2 * slot;
+ ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_VEND_MMS1,
+ reg_address,
+ get_unaligned_be32(&addr[2]));
+ if (ret)
+ return ret;
+
+ /* Only the first 2 destination MAC filter slots support masking.
+ * For the other entries, the destination address in the received
+ * frame must match exactly.
+ */
+ if (slot >= ADIN1140_MAC_FILT_MASK_LIMIT)
+ return 0;
+
+ val = get_unaligned_be16(&mask[0]);
+ reg_address = ADIN1140_MAC_ADDR_MASK_UPR_REG + (2 * slot);
+
+ ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_VEND_MMS1,
+ reg_address, val);
+ if (ret)
+ return ret;
+
+ val = get_unaligned_be32(&mask[2]);
+ reg_address = ADIN1140_MAC_ADDR_MASK_LWR_REG + (2 * slot);
+
+ return oa_tc6_write_register_mms(priv->tc6, OA_TC6_VEND_MMS1,
+ reg_address, val);
+}
+
+static int adin1140_mac_filter_clear(struct adin1140_priv *priv, u8 slot)
+{
+ u8 mask[ETH_ALEN];
+ u8 addr[ETH_ALEN];
+
+ memset(mask, 0xFF, ETH_ALEN);
+ memset(addr, 0x0, ETH_ALEN);
+
+ return adin1140_mac_filter_set(priv, addr, mask, slot);
+}
+
+static int adin1140_filter_unicast(struct adin1140_priv *priv)
+{
+ /* Only the first 2 filter slots support masking, so no unicast
+ * address will ever need a mask. The first slots are used for the
+ * all multicast and broadcast filter.
+ */
+ return adin1140_mac_filter_set(priv, priv->netdev->dev_addr, NULL,
+ ADIN1140_MAC_FILT_UC_SLOT);
+}
+
+static int adin1140_filter_all_multicast(struct adin1140_priv *priv, bool en)
+{
+ u8 multicast_addr[ETH_ALEN] = {1, 0, 0, 0, 0, 0};
+
+ if (en)
+ return adin1140_mac_filter_set(priv, multicast_addr,
+ multicast_addr,
+ ADIN1140_MAC_FILT_MC_SLOT);
+
+ return adin1140_mac_filter_clear(priv, ADIN1140_MAC_FILT_MC_SLOT);
+}
+
+static int adin1140_filter_broadcast(struct adin1140_priv *priv, bool enabled)
+{
+ u8 mask[ETH_ALEN];
+
+ if (enabled) {
+ memset(mask, 0xFF, ETH_ALEN);
+ return adin1140_mac_filter_set(priv, mask, mask,
+ ADIN1140_MAC_FILT_BC_SLOT);
+ }
+
+ return adin1140_mac_filter_clear(priv, ADIN1140_MAC_FILT_BC_SLOT);
+}
+
+static int adin1140_default_filter_config(struct adin1140_priv *priv)
+{
+ int ret;
+
+ ret = adin1140_filter_broadcast(priv, true);
+ if (ret)
+ return ret;
+
+ return adin1140_filter_unicast(priv);
+}
+
+static int adin1140_promiscuous_mode(struct adin1140_priv *priv, bool enabled)
+{
+ int ret;
+ u32 val;
+
+ ret = oa_tc6_read_register(priv->tc6, OA_TC6_REG_CONFIG2, &val);
+ if (ret)
+ return ret;
+
+ if (enabled)
+ val |= ADIN1140_CONFIG2_FWD_UNK2HOST;
+ else
+ val &= ~ADIN1140_CONFIG2_FWD_UNK2HOST;
+
+ return oa_tc6_write_register(priv->tc6, OA_TC6_REG_CONFIG2, val);
+}
+
+static void adin1140_rx_mode_work(struct work_struct *work)
+{
+ struct adin1140_priv *priv = container_of(work, struct adin1140_priv,
+ rx_mode_work);
+ struct netdev_hw_addr *ha;
+ bool all_multi, promisc;
+ u8 mask[ETH_ALEN];
+ u8 start, end;
+ u32 mac_addrs;
+ u8 slot, i;
+ int ret;
+
+ /* The ADIN1140 has 16 dest MAC address filter slots:
+ * 0 - reserved for all multicast filter.
+ * 1 - reserved for broadcast filter.
+ * 2 - reserved for the device's own unicast MAC.
+ * 3 -> 15 - available for other unicast/multicast filters.
+ */
+
+ mac_addrs = netdev_uc_count(priv->netdev) +
+ netdev_mc_count(priv->netdev);
+
+ if (priv->netdev->flags & IFF_PROMISC) {
+ promisc = true;
+ all_multi = false;
+ } else if (priv->netdev->flags & IFF_ALLMULTI) {
+ promisc = false;
+ all_multi = true;
+ } else if (mac_addrs <= ADIN1140_MAC_FILT_AVAIL) {
+ promisc = false;
+ all_multi = false;
+
+ slot = ADIN1140_MAC_FILT_UC_SLOT + 1;
+ memset(mask, 0xFF, ETH_ALEN);
+
+ netdev_for_each_uc_addr(ha, priv->netdev) {
+ ret = adin1140_mac_filter_set(priv, ha->addr, mask,
+ slot);
+ if (ret)
+ return;
+
+ slot++;
+ }
+
+ netdev_for_each_mc_addr(ha, priv->netdev) {
+ ret = adin1140_mac_filter_set(priv, ha->addr, mask,
+ slot);
+ if (ret)
+ return;
+
+ slot++;
+ }
+
+ for (i = slot; i < ADIN1140_MAC_FILT_MAX_SLOT; i++) {
+ ret = adin1140_mac_filter_clear(priv, i);
+ if (ret)
+ return;
+ }
+
+ } else {
+ /* The filter table is full. Enable promisc mode. */
+ promisc = true;
+ all_multi = false;
+
+ start = ADIN1140_MAC_FILT_UC_SLOT + 1;
+ end = ADIN1140_MAC_FILT_MAX_SLOT;
+ for (i = start; i < end; i++) {
+ ret = adin1140_mac_filter_clear(priv, i);
+ if (ret)
+ return;
+ }
+ }
+
+ ret = adin1140_promiscuous_mode(priv, promisc);
+ if (ret)
+ return;
+
+ adin1140_filter_all_multicast(priv, all_multi);
+}
+
+static void adin1140_rx_mode(struct net_device *netdev)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+
+ schedule_work(&priv->rx_mode_work);
+}
+
+static void adin1140_stats_work(struct work_struct *work)
+{
+ struct delayed_work *dwork = to_delayed_work(work);
+ u64 stat_buff[ADIN1140_STATS_CNT] = {};
+ struct adin1140_priv *priv;
+ u32 reg_val;
+ int ret;
+ u32 i;
+
+ priv = container_of(dwork, struct adin1140_priv, stats_work);
+
+ for (i = 0; i < ARRAY_SIZE(adin1140_stat_regs); i++) {
+ ret = oa_tc6_read_register_mms(priv->tc6, OA_TC6_VEND_MMS1,
+ adin1140_stat_regs[i],
+ ®_val);
+ if (ret)
+ break;
+
+ stat_buff[i] = reg_val;
+ }
+
+ scoped_guard(spinlock, &priv->stat_lock)
+ memcpy(&priv->stats, stat_buff, sizeof(priv->stats));
+
+ schedule_delayed_work(dwork, ADIN1140_STATS_CHECK_DELAY);
+}
+
+static int adin1140_configure(struct adin1140_priv *priv)
+{
+ int ret;
+
+ ret = oa_tc6_zero_align_receive_frame_enable(priv->tc6);
+ if (ret)
+ return ret;
+
+ /* Disable MAC loopback */
+ ret = oa_tc6_write_register_mms(priv->tc6, OA_TC6_VEND_MMS1,
+ ADIN1140_MAC_P1_LOOP_ADDR_REG, 0x0);
+ if (ret)
+ return ret;
+
+ return adin1140_default_filter_config(priv);
+}
+
+static int adin1140_open(struct net_device *netdev)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+
+ schedule_delayed_work(&priv->stats_work, ADIN1140_STATS_CHECK_DELAY);
+
+ phy_start(netdev->phydev);
+ netif_start_queue(netdev);
+
+ return 0;
+}
+
+static int adin1140_close(struct net_device *netdev)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+
+ cancel_delayed_work_sync(&priv->stats_work);
+
+ netif_stop_queue(netdev);
+ phy_stop(netdev->phydev);
+
+ return 0;
+}
+
+static netdev_tx_t adin1140_start_xmit(struct sk_buff *skb,
+ struct net_device *netdev)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+
+ /* The MAC doesn't automatically pad the frame to a 60 byte minimum
+ * size in case the host sends a shorter skb, so we have to do it in
+ * the driver. The FCS will be added by the MAC.
+ */
+ if (skb_put_padto(skb, ETH_ZLEN))
+ return NETDEV_TX_OK;
+
+ return oa_tc6_start_xmit(priv->tc6, skb);
+}
+
+static int adin1140_set_mac_address(struct net_device *netdev, void *addr)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+ struct sockaddr *address = addr;
+ u8 mask[ETH_ALEN];
+ int ret;
+
+ ret = eth_prepare_mac_addr_change(netdev, addr);
+ if (ret < 0)
+ return ret;
+
+ if (ether_addr_equal(address->sa_data, netdev->dev_addr))
+ return 0;
+
+ memset(mask, 0xFF, ETH_ALEN);
+ ret = adin1140_mac_filter_set(priv, address->sa_data, mask,
+ ADIN1140_MAC_FILT_UC_SLOT);
+ if (ret)
+ return ret;
+
+ eth_commit_mac_addr_change(netdev, addr);
+
+ return 0;
+}
+
+static void __adin1140_ndo_get_stats64(struct adin1140_priv *priv,
+ struct rtnl_link_stats64 *storage)
+{
+ storage->rx_errors = priv->stats[rx_crc_errors] +
+ priv->stats[rx_align_errors] +
+ priv->stats[rx_preamble_errors] +
+ priv->stats[rx_short_frame_errors] +
+ priv->stats[rx_long_frame_errors] +
+ priv->stats[rx_phy_errors] +
+ priv->stats[rx_ifg_errors];
+
+ storage->tx_errors = priv->stats[tx_excess_collision] +
+ priv->stats[tx_underrun];
+
+ storage->rx_dropped = priv->stats[rx_fifo_full_dropped] +
+ priv->stats[rx_addr_filter_dropped];
+
+ storage->multicast = priv->stats[rx_mc_frames];
+
+ storage->collisions = priv->stats[tx_single_collision] +
+ priv->stats[tx_multi_collision];
+
+ storage->rx_length_errors = priv->stats[rx_short_frame_errors] +
+ priv->stats[rx_long_frame_errors];
+ storage->rx_over_errors = priv->stats[rx_fifo_full_dropped];
+ storage->rx_crc_errors = priv->stats[rx_crc_errors];
+ storage->rx_frame_errors = priv->stats[rx_align_errors];
+ storage->rx_missed_errors = priv->stats[rx_fifo_full_dropped];
+
+ storage->tx_aborted_errors = priv->stats[tx_excess_collision];
+ storage->tx_fifo_errors = priv->stats[tx_underrun];
+ storage->tx_window_errors = priv->stats[tx_late_collision];
+}
+
+static void adin1140_ndo_get_stats64(struct net_device *dev,
+ struct rtnl_link_stats64 *storage)
+{
+ struct adin1140_priv *priv = netdev_priv(dev);
+
+ storage->rx_packets = priv->netdev->stats.rx_packets;
+ storage->tx_packets = priv->netdev->stats.tx_packets;
+
+ storage->rx_bytes = priv->netdev->stats.rx_bytes;
+ storage->tx_bytes = priv->netdev->stats.tx_bytes;
+
+ scoped_guard(spinlock, &priv->stat_lock)
+ __adin1140_ndo_get_stats64(priv, storage);
+}
+
+static void adin1140_get_drvinfo(struct net_device *netdev,
+ struct ethtool_drvinfo *info)
+{
+ strscpy(info->driver, "ADIN1140", sizeof(info->driver));
+ strscpy(info->bus_info, dev_name(netdev->dev.parent),
+ sizeof(info->bus_info));
+}
+
+static void adin1140_get_ethtool_stats(struct net_device *netdev,
+ struct ethtool_stats *stats, u64 *data)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+ u32 i;
+
+ scoped_guard(spinlock, &priv->stat_lock) {
+ for (i = 0; i < ARRAY_SIZE(adin1140_stats); i++)
+ data[i] = priv->stats[adin1140_stats[i].idx];
+ }
+}
+
+static void adin1140_get_ethtool_strings(struct net_device *netdev, u32 sset,
+ u8 *p)
+{
+ u32 i;
+
+ switch (sset) {
+ case ETH_SS_STATS:
+ for (i = 0; i < ARRAY_SIZE(adin1140_stats); i++)
+ ethtool_puts(&p, adin1140_stats[i].name);
+
+ break;
+ }
+}
+
+static int adin1140_get_sset_count(struct net_device *netdev, int sset)
+{
+ switch (sset) {
+ case ETH_SS_STATS:
+ return ARRAY_SIZE(adin1140_stats);
+ default:
+ return -EOPNOTSUPP;
+ }
+}
+
+static void __adin1140_eth_mac_stats(struct adin1140_priv *priv,
+ struct ethtool_eth_mac_stats *mac_stats)
+{
+ mac_stats->FramesReceivedOK = priv->stats[rx_frames];
+ mac_stats->BroadcastFramesReceivedOK = priv->stats[rx_bc_frames];
+ mac_stats->MulticastFramesReceivedOK = priv->stats[rx_mc_frames];
+ mac_stats->FrameCheckSequenceErrors = priv->stats[rx_crc_errors];
+ mac_stats->AlignmentErrors = priv->stats[rx_align_errors];
+ mac_stats->FrameTooLongErrors = priv->stats[rx_long_frame_errors];
+ mac_stats->FramesLostDueToIntMACRcvError =
+ priv->stats[rx_fifo_full_dropped];
+ mac_stats->FramesTransmittedOK = priv->stats[tx_frames];
+ mac_stats->BroadcastFramesXmittedOK = priv->stats[tx_bc_frames];
+ mac_stats->MulticastFramesXmittedOK = priv->stats[tx_mc_frames];
+ mac_stats->SingleCollisionFrames = priv->stats[tx_single_collision];
+ mac_stats->MultipleCollisionFrames = priv->stats[tx_multi_collision];
+ mac_stats->FramesWithDeferredXmissions = priv->stats[tx_deferred];
+ mac_stats->LateCollisions = priv->stats[tx_late_collision];
+ mac_stats->FramesAbortedDueToXSColls =
+ priv->stats[tx_excess_collision];
+ mac_stats->FramesLostDueToIntMACXmitError = priv->stats[tx_underrun];
+}
+
+static void adin1140_get_eth_mac_stats(struct net_device *netdev,
+ struct ethtool_eth_mac_stats *mac_stats)
+{
+ struct adin1140_priv *priv = netdev_priv(netdev);
+
+ scoped_guard(spinlock, &priv->stat_lock)
+ __adin1140_eth_mac_stats(priv, mac_stats);
+}
+
+static int adin1140_mdiobus_read(struct mii_bus *bus, int addr, int regnum)
+{
+ /* The ADIN1140's standard PHY C22 register map (OA TC6 0xFF00 -
+ * 0xFF1F), of which only 0xFF00 - 0xFF03 are implemented) cannot be
+ * accessed while frames are being received by the PHY. In case this
+ * happens the CONFIG0 and CONFIG2 register values will get corrupted,
+ * getting a random value. Both reads and writes cause the same
+ * behavior. This is a workaround that avoids MDIO accesses all
+ * together. Since this is a 10BASE-T1S PHY, only the loopback and
+ * reset (AN) bits in the control register (0x0) can be written.
+ * These functionalities have custom implementations in the PHY
+ * driver. C45 accesses do not cause this issue.
+ */
+
+ switch (regnum) {
+ case MII_BMCR:
+ return ADIN1140_PHY_CTRL_DEFAULT;
+ case MII_BMSR:
+ return ADIN1140_PHY_STATUS_DEFAULT;
+ case MII_PHYSID1:
+ return ADIN1140_PHY_ID1;
+ case MII_PHYSID2:
+ return ADIN1140_PHY_ID2;
+ default:
+ return 0xFFFF;
+ }
+}
+
+static int adin1140_mdiobus_write(struct mii_bus *bus, int addr, int regnum,
+ u16 val)
+{
+ return -EIO;
+}
+
+static int adin1140_mdio_register(struct adin1140_priv *priv,
+ struct spi_device *spidev)
+{
+ priv->mdiobus = devm_mdiobus_alloc(&spidev->dev);
+ if (!priv->mdiobus)
+ return dev_err_probe(&spidev->dev, -ENOMEM,
+ "MDIO bus alloc failed\n");
+
+ priv->mdiobus->name = "adin1140-mdiobus";
+ priv->mdiobus->priv = priv->tc6;
+ priv->mdiobus->parent = &spidev->dev;
+ priv->mdiobus->phy_mask = GENMASK(31, 1);
+ priv->mdiobus->read = adin1140_mdiobus_read;
+ priv->mdiobus->write = adin1140_mdiobus_write;
+ priv->mdiobus->read_c45 = oa_tc6_mdiobus_read_c45;
+ priv->mdiobus->write_c45 = oa_tc6_mdiobus_write_c45;
+
+ snprintf(priv->mdiobus->id, MII_BUS_ID_SIZE, "adin1140-%s.%u",
+ dev_name(&spidev->dev), spi_get_chipselect(spidev, 0));
+
+ return devm_mdiobus_register(&spidev->dev, priv->mdiobus);
+}
+
+static void adin1140_handle_link_change(struct net_device *netdev)
+{
+ phy_print_status(netdev->phydev);
+}
+
+static void adin1140_phy_remove(void *data)
+{
+ phy_disconnect(data);
+}
+
+static int adin1140_phy_init(struct adin1140_priv *priv,
+ struct spi_device *spidev)
+{
+ int ret;
+
+ ret = adin1140_mdio_register(priv, spidev);
+ if (ret)
+ return ret;
+
+ priv->phydev = phy_find_first(priv->mdiobus);
+ if (!priv->phydev)
+ return dev_err_probe(&spidev->dev, -ENODEV, "No PHY found\n");
+
+ priv->phydev->is_internal = true;
+ ret = phy_connect_direct(priv->netdev, priv->phydev,
+ &adin1140_handle_link_change,
+ PHY_INTERFACE_MODE_INTERNAL);
+ if (ret)
+ return dev_err_probe(&spidev->dev, ret,
+ "Can't attach PHY to %s\n",
+ priv->mdiobus->id);
+
+ ret = devm_add_action_or_reset(&spidev->dev, adin1140_phy_remove,
+ priv->phydev);
+ if (ret)
+ return ret;
+
+ phy_attached_info(priv->phydev);
+
+ return 0;
+}
+
+static const struct ethtool_ops adin1140_ethtool_ops = {
+ .get_drvinfo = adin1140_get_drvinfo,
+ .get_link = ethtool_op_get_link,
+ .get_ethtool_stats = adin1140_get_ethtool_stats,
+ .get_sset_count = adin1140_get_sset_count,
+ .get_strings = adin1140_get_ethtool_strings,
+ .get_link_ksettings = phy_ethtool_get_link_ksettings,
+ .set_link_ksettings = phy_ethtool_set_link_ksettings,
+ .get_eth_mac_stats = adin1140_get_eth_mac_stats,
+};
+
+static const struct net_device_ops adin1140_netdev_ops = {
+ .ndo_open = adin1140_open,
+ .ndo_stop = adin1140_close,
+ .ndo_start_xmit = adin1140_start_xmit,
+ .ndo_set_mac_address = adin1140_set_mac_address,
+ .ndo_validate_addr = eth_validate_addr,
+ .ndo_set_rx_mode = adin1140_rx_mode,
+ .ndo_eth_ioctl = phy_do_ioctl_running,
+ .ndo_get_stats64 = adin1140_ndo_get_stats64,
+};
+
+static void adin1140_oa_tc6_remove(void *data)
+{
+ oa_tc6_exit(data);
+}
+
+static void adin1140_cancel_rx_mode_work(void *data)
+{
+ cancel_work_sync(data);
+}
+
+static int adin1140_probe(struct spi_device *spi)
+{
+ struct oa_tc6_quirks tc6_quirks = {};
+ struct net_device *netdev;
+ struct adin1140_priv *priv;
+ int ret;
+
+ netdev = devm_alloc_etherdev(&spi->dev, sizeof(struct adin1140_priv));
+ if (!netdev)
+ return -ENOMEM;
+
+ priv = netdev_priv(netdev);
+ priv->netdev = netdev;
+ spi_set_drvdata(spi, priv);
+ spin_lock_init(&priv->stat_lock);
+
+ tc6_quirks.quirk_flags = OA_TC6_BROKEN_PHY;
+
+ priv->tc6 = oa_tc6_init(spi, netdev, &tc6_quirks);
+ if (!priv->tc6)
+ return -ENODEV;
+
+ ret = devm_add_action_or_reset(&spi->dev, adin1140_oa_tc6_remove,
+ priv->tc6);
+ if (ret)
+ return ret;
+
+ ret = adin1140_phy_init(priv, spi);
+ if (ret)
+ return ret;
+
+ if (device_get_ethdev_address(&spi->dev, netdev))
+ eth_hw_addr_random(netdev);
+
+ ret = adin1140_configure(priv);
+ if (ret)
+ return ret;
+
+ INIT_WORK(&priv->rx_mode_work, adin1140_rx_mode_work);
+ INIT_DELAYED_WORK(&priv->stats_work, adin1140_stats_work);
+
+ ret = devm_add_action_or_reset(&spi->dev, adin1140_cancel_rx_mode_work,
+ &priv->rx_mode_work);
+ if (ret)
+ return ret;
+
+ netdev->if_port = IF_PORT_10BASET;
+ netdev->irq = spi->irq;
+ netdev->netdev_ops = &adin1140_netdev_ops;
+ netdev->ethtool_ops = &adin1140_ethtool_ops;
+ netdev->netns_immutable = true;
+ netdev->priv_flags |= IFF_LIVE_ADDR_CHANGE |
+ IFF_UNICAST_FLT;
+
+ ret = devm_register_netdev(&spi->dev, netdev);
+ if (ret)
+ return dev_err_probe(&spi->dev, ret,
+ "Failed to register netdev");
+
+ return 0;
+}
+
+static const struct spi_device_id adin1140_spi_id[] = {
+ { .name = "ad3306" },
+ { .name = "adin1140" },
+ {},
+};
+MODULE_DEVICE_TABLE(spi, adin1140_spi_id);
+
+static const struct of_device_id adin1140_match_table[] = {
+ { .compatible = "adi,ad3306" },
+ { .compatible = "adi,adin1140" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, adin1140_match_table);
+
+static struct spi_driver adin1140_driver = {
+ .driver = {
+ .name = "adin1140",
+ .of_match_table = adin1140_match_table,
+ },
+ .probe = adin1140_probe,
+ .id_table = adin1140_spi_id,
+};
+module_spi_driver(adin1140_driver);
+
+MODULE_DESCRIPTION("Analog Devices, Inc. ADIN1140 10BASE-T1S MAC-PHY");
+MODULE_AUTHOR("Ciprian Regus <ciprian.regus@analog.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
* [PATCH net-next v3 12/13] net: phy: Add support for the ADIN1140 PHY
From: Ciprian Regus via B4 Relay @ 2026-06-04 16:32 UTC (permalink / raw)
To: Parthiban Veerasooran, Andrew Lunn, David S. Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Simon Horman, Jonathan Corbet,
Shuah Khan, Andrew Lunn, Heiner Kallweit, Russell King,
Rob Herring, Krzysztof Kozlowski, Conor Dooley
Cc: netdev, linux-kernel, linux-doc, devicetree, Ciprian Regus
In-Reply-To: <20260604-adin1140-driver-v3-0-5debdb3173c4@analog.com>
From: Ciprian Regus <ciprian.regus@analog.com>
Add a driver for the ADIN1140's internal 10BASE-T1S PHY. The device
doesn't implement autonegotiation, so the link is always reported as
being up.
The device implements both C22 and C45 MDIO access methods, but can only
be discovered over C22, since the C45 MMD devices lack the MDIO_DEVID1 and
MDIO_DEVID2 registers. The indirect C45 over C22 feature is not
supported.
Reviewed-by: Andrew Lunn <andrew@lunn.ch>
Signed-off-by: Ciprian Regus <ciprian.regus@analog.com>
---
v3 changelog:
- no change
v2 changelog:
- No longer setting PHY_MAC_INTERRUPT in order to avoid state polling.
- Replace the driver specific .read/write_mmd() functions with the ones
exported from genphy.
- Renamed the file to adin1140-phy.c in order to avoid module name
conflicts with the adin1140 ethernet driver.
---
MAINTAINERS | 7 ++++
drivers/net/phy/Kconfig | 6 ++++
drivers/net/phy/Makefile | 1 +
drivers/net/phy/adin1140-phy.c | 72 ++++++++++++++++++++++++++++++++++++++++++
4 files changed, 86 insertions(+)
diff --git a/MAINTAINERS b/MAINTAINERS
index ca6c7425b45f..eda74f3154dc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -1857,6 +1857,13 @@ S: Supported
W: https://ez.analog.com/linux-software-drivers
F: drivers/dma/dma-axi-dmac.c
+ANALOG DEVICES INC ETHERNET PHY DRIVERS
+M: Ciprian Regus <ciprian.regus@analog.com>
+L: netdev@vger.kernel.org
+S: Maintained
+W: https://ez.analog.com/linux-software-drivers
+F: drivers/net/phy/adin1140-phy.c
+
ANALOG DEVICES INC IIO DRIVERS
M: Lars-Peter Clausen <lars@metafoo.de>
M: Michael Hennerich <Michael.Hennerich@analog.com>
diff --git a/drivers/net/phy/Kconfig b/drivers/net/phy/Kconfig
index 8b51bdc2e945..bd21a5dad366 100644
--- a/drivers/net/phy/Kconfig
+++ b/drivers/net/phy/Kconfig
@@ -124,6 +124,12 @@ config ADIN1100_PHY
Currently supports the:
- ADIN1100 - Robust,Industrial, Low Power 10BASE-T1L Ethernet PHY
+config ADIN1140_PHY
+ tristate "Analog Devices ADIN1140 10BASE-T1S PHY"
+ help
+ Adds support for the Analog Devices, Inc. ADIN1140's internal
+ 10BASE-T1S PHY.
+
config AMCC_QT2025_PHY
tristate "AMCC QT2025 PHY"
depends on RUST_PHYLIB_ABSTRACTIONS
diff --git a/drivers/net/phy/Makefile b/drivers/net/phy/Makefile
index 05e4878af27a..73152845b0b2 100644
--- a/drivers/net/phy/Makefile
+++ b/drivers/net/phy/Makefile
@@ -29,6 +29,7 @@ obj-y += $(sfp-obj-y) $(sfp-obj-m)
obj-$(CONFIG_ADIN_PHY) += adin.o
obj-$(CONFIG_ADIN1100_PHY) += adin1100.o
+obj-$(CONFIG_ADIN1140_PHY) += adin1140-phy.o
obj-$(CONFIG_AIR_EN8811H_PHY) += air_en8811h.o
obj-$(CONFIG_AMD_PHY) += amd.o
obj-$(CONFIG_AMCC_QT2025_PHY) += qt2025.o
diff --git a/drivers/net/phy/adin1140-phy.c b/drivers/net/phy/adin1140-phy.c
new file mode 100644
index 000000000000..d35da4ad680d
--- /dev/null
+++ b/drivers/net/phy/adin1140-phy.c
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Driver for Analog Devices, Inc. ADIN1140 10BASE-T1S PHY
+ *
+ * Copyright 2026 Analog Devices Inc.
+ */
+
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/phy.h>
+
+#define ADIN1140_PHY_ID 0x0283be00
+
+#define ADIN1140_PCS_CTRL 0x08f3
+#define ADIN1140_PCS_CTRL_LOOPBACK BIT(14)
+
+static int adin1140_config_aneg(struct phy_device *phydev)
+{
+ /* phylib tries to clear BIT(12) in MDIO_CTRL1, since AN is disabled.
+ * However, on the ADIN1140, that field is non-standard, being used
+ * to control the reset status of the PHY (thus it needs to remain set).
+ */
+ return 0;
+}
+
+static int adin1140_loopback(struct phy_device *phydev, bool enable, int speed)
+{
+ if (enable && speed)
+ return -EOPNOTSUPP;
+
+ return phy_modify_mmd(phydev, MDIO_MMD_PCS, ADIN1140_PCS_CTRL,
+ ADIN1140_PCS_CTRL_LOOPBACK,
+ enable ? ADIN1140_PCS_CTRL_LOOPBACK : 0);
+}
+
+static int adin1140_read_status(struct phy_device *phydev)
+{
+ phydev->link = 1;
+ phydev->duplex = DUPLEX_HALF;
+ phydev->speed = SPEED_10;
+ phydev->autoneg = AUTONEG_DISABLE;
+
+ return 0;
+}
+
+static struct phy_driver adin1140_driver[] = {
+ {
+ PHY_ID_MATCH_EXACT(ADIN1140_PHY_ID),
+ .name = "ADIN1140_PHY",
+ .features = PHY_BASIC_T1S_P2MP_FEATURES,
+ .read_status = adin1140_read_status,
+ .config_aneg = adin1140_config_aneg,
+ .set_loopback = adin1140_loopback,
+ .read_mmd = genphy_read_mmd_c45,
+ .write_mmd = genphy_write_mmd_c45,
+ .get_plca_cfg = genphy_c45_plca_get_cfg,
+ .set_plca_cfg = genphy_c45_plca_set_cfg,
+ .get_plca_status = genphy_c45_plca_get_status,
+ },
+};
+module_phy_driver(adin1140_driver);
+
+static const struct mdio_device_id __maybe_unused adin1140_tbl[] = {
+ { PHY_ID_MATCH_EXACT(ADIN1140_PHY_ID) },
+ { }
+};
+
+MODULE_DEVICE_TABLE(mdio, adin1140_tbl);
+
+MODULE_DESCRIPTION("Analog Devices, Inc. ADIN1140 10BASE-T1S PHY");
+MODULE_AUTHOR("Ciprian Regus <ciprian.regus@analog.com>");
+MODULE_LICENSE("GPL");
--
2.43.0
^ permalink raw reply related
page: next (older) | prev (newer) | latest
- recent:[subjects (threaded)|topics (new)|topics (active)]
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox