mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access
@ 2026-04-23  6:04 Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 1/5] pci_ids: Add Hygon Family 0x18 DF device IDs Lin Wang
                   ` (4 more replies)
  0 siblings, 5 replies; 8+ messages in thread
From: Lin Wang @ 2026-04-23  6:04 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

== Background ==

The "AMD NB and SMN rework" series [1][2] restructured AMD northbridge
and SMN support into amd_nb.c and amd_node.c, providing a clean
framework for discovering and accessing AMD Data Fabric (DF) instances
via PCI config space and SMN.

Hygon Family 0x18 implements a Data Fabric topology that is structurally
comparable to AMD Zen systems at the register level, but diverges from
several platform conventions that amd_nb.c and amd_node.c rely on. This
series adds the missing Hygon DF support while keeping all Hygon
enablement in Hygon-specific files.

== Overview ==

This series adds kernel infrastructure for two closely related functions
on Hygon Family 0x18:

  1. DF node enumeration -- discovering and identifying each Data Fabric
     (DF) instance on the system through its PCI config registers, so
     that the NB and SMN subsystems can access them correctly.

  2. CPU-to-node mapping -- establishing a canonical logical node ID that
     bridges the DF register world (socket_id, DFID) and the CPU
     topology world (phys_node_id from CPUID), so that EDAC, MCE, and
     ATL consumers can translate a CPU reference to the correct DF node
     in O(1).

== Problem ==

AMD Zen-based systems enumerate DF nodes at fixed PCI slots 00:18.x
through 00:1f.x on segment 0 bus 0. The slot-minus-0x18 identity is a
platform guarantee that amd_nb.c and amd_node.c rely on throughout.

Hygon Family 0x18 processors expose DF instances at platform-assigned
PCI slots with no fixed relationship to node identity. As a result:

  - amd_nb.c cannot populate amd_northbridges[] for Hygon systems.
  - amd_smn_init() cannot assign SMN root devices to the correct nodes.
  - There is no kernel mechanism to map a CPU to its DF node. The
    phys_node_id values from CPUID 8000001Eh[7:0] are globally unique
    but sparse (socket 0: 0..3, socket 1: 16..19, etc.) and cannot be
    used directly as indices.

== Solution ==

v2 follows the architectural direction from v1 review: all Hygon
enablement goes into Hygon-specific files. Rather than adding Hygon
branches into amd_nb.c and amd_node.c, v2 introduces two small generic
registration interfaces -- amd_nb_set_cache() and smn_set_roots() -- so
that a vendor-specific module can build its data structures independently
and hand them to the shared infrastructure.

hygon_node.c is the Hygon-specific DF enumeration module. It reads
hardware identity directly from each DF instance, builds a correctly
ordered node array, provides the CPU-to-node mapping as an O(1) lookup,
and registers the results with the AMD NB and SMN layers through the
generic interfaces above.

The existing AMD code paths remain unchanged: amd_cache_northbridges()
and amd_smn_init() continue to run exactly as before on AMD hardware.
On Hygon systems, the AMD paths early-return and Hygon's own
fs_initcall handles all registration.

== Hardware Background ==

Each DF instance exposes identity through PCI config registers on its
function siblings:

  F1x200 (SystemCfg), all models:
    [30:28]  MySocketId  -- hardware socket ID
    [23:20]  MyDieId     -- die ID (equals DFID on most models)

  F5x180 (FabricId), Model 0x06-0x08 only:
    [19:16]  DFID        -- real Data Fabric ID (MyDieId != DFID here)

DFID classifies each DF instance:
  DFID >= 4: Compute Die (CDD) -- hosts CPU cores and UMC controllers.
             Each CDD has a platform-unique phys_node_id from
             CPUID leaf 8000001Eh[7:0].
  DFID <  4: I/O Die (IOD)     -- interconnect and I/O; no CPUs, no UMC.

The phys_node_id is globally unique but sparse across sockets:

  4-socket, 4 CDD/socket (Model 0x04/0x05):
    Socket 0: DFID=4,5,6,7   phys_nid=0,1,2,3
    Socket 1: DFID=4,5,6,7   phys_nid=16,17,18,19
    ...

  2-socket, sparse DFID from F5x180 (Model 0x06):
    Socket 0: DFID=4,5,8,9   phys_nid=0,1,2,3
    Socket 1: DFID=4,5,8,9   phys_nid=16,17,18,19

== Design ==

=== DF Node Enumeration ===

hygon_build_cache() runs lazily on the first API call, under a mutex,
and publishes the completed cache via smp_store_release(ready).

Phase 1 -- Collect:
  Walk all DF misc (F3) devices matching hygon_nb_misc_ids[]. For each:
    - Find the F4 (link) sibling on the same PCI slot.
    - Read F1x200 via the F1 sibling: socket_id and die ID.
    - On Model 0x06-0x08: read F5x180 via the F5 sibling for the real
      DFID (MyDieId != DFID on these models).
    - A model-to-device-ID table (hygon_df_table[]) maps boot CPU model
      to the expected F1/F5 device IDs, avoiding per-model switch-case
      logic and making new-model support a one-row table addition.
  Validate that socket IDs are dense (0..N-1).

Phase 2 -- Sort and classify:
  Sort all collected entries by (is_cdd DESC, socket_id ASC, dfid ASC).
  Count num_cdd. Validate: num_cdd > 0, fits in u8, divisible by
  num_sockets, each socket contributes the same CDD count.

After sorting, the node array is partitioned as:

  nodes[]
  +----------------------------------------------------------+
  | CDD region: indices 0 .. num_cdd-1                       |
  | sorted by (socket_id ASC, dfid ASC)                      |
  | index = logical_node_id                                  |
  +----------------------------------------------------------+
  | IOD region: indices num_cdd .. num_nodes-1               |
  | socket_id used for SMN root assignment only              |
  +----------------------------------------------------------+

=== CPU-to-Node Mapping ===

The array index in the CDD region serves as the logical_node_id.
Consumers such as EDAC, MCE, and ATL need to translate a CPU reference
to this index. The challenge is bridging two independent hardware worlds:

  DF world:   socket_id and DFID from PCI config registers (F1x200, F5x180)
  CPU world:  phys_node_id from CPUID 8000001Eh[7:0] per core

Both are hardware-fixed values. Neither depends on software enumeration
order. A third hardware property connects them:

    Within a socket, CDDs with ascending DFID are assigned ascending
    phys_node_id values. Lower socket IDs always occupy lower phys_nid
    ranges.

Phase 3 of the cache build exploits this:
  - Collect unique phys_node_id values from online CPUs (one per CDD)
    via topology_amd_node_id(), using a bitmap for de-duplication.
  - Sort the collected values globally ascending.
  - Map: nid_to_logical[nids[i]] = i  for i in 0..num_cdd-1.

The mapping is stored as a 256-byte direct-mapped array for O(1) lookup.

If the hardware property is violated on a future platform, Phase 3
validation detects it (collected phys_nid count != num_cdd) and fails
loudly rather than producing a silently wrong mapping.

=== NB Cache and SMN Root Registration ===

Once enumeration is complete, two independent fs_initcalls perform
registration that plugs Hygon into the shared AMD infrastructure:

  1. hygon_nb_init() -- builds a struct amd_northbridge[] array from the
     enumerated nodes and registers it via amd_nb_set_cache(), so
     amd_nb consumers (EDAC, MCE decode, etc.) work transparently.

  2. hygon_smn_init() -- Hygon shares SMN root devices per-socket (all
     nodes on the same socket use the same host-bridge root). Discovers
     host-bridge roots by PCI class, reserves config regions exclusively,
     expands the per-socket roots into a per-node array and registers it
     via smn_set_roots(). Properly releases reserved config regions and
     allocated memory on any failure.

Both registration functions follow the same pattern: the caller builds
and owns the data, hands it to the generic layer, and the generic layer
checks for double-registration (-EBUSY).

== API ==

Exported (for loadable module consumers: EDAC, MCE, ATL):

  hygon_f18h_model()              model byte, 0 if not Hygon Family 0x18
  hygon_cdd_num()                 CDD count (EDAC instance sizing)
  hygon_get_dfid(misc, &dfid)     DFID for a DF misc device
  hygon_cpu_to_df_node(cpu)       DF node index (0..N-1), or -errno

Inline helper in the header:
  hygon_f18h_model_in_range(first, last)  -- true if model in [first, last]

asm/hygon/node.h provides static inline stubs returning 0/NULL/-ENODEV
for all functions when CONFIG_HYGON_NODE=n; no #ifdef guards are needed
in consumer code.

== Series ==

  [1/5] pci_ids: Add Hygon Family 0x18 DF device IDs
        Hygon-specific PCI device IDs for DF misc (F3) and link (F4)
        functions across all supported Hygon Family 0x18 models.

  [2/5] x86/hygon: Add Family 0x18 node enumeration API header
        asm/hygon/node.h: API declarations, CONFIG_HYGON_NODE=n stubs,
        and inline model-check helpers.

  [3/5] x86/amd_nb: Add amd_nb_set_cache() for external NB cache
        registration
        Vendor-neutral data registration interface: the caller builds
        a struct amd_northbridge[] array, populates the mandatory misc
        and link device pointers, and transfers ownership to the NB
        layer. Guard amd_cache_northbridges() to AMD-only.

  [4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for
        external SMN registration
        Vendor-neutral interface for registering a pre-built per-node
        SMN root array. Guard amd_smn_init() to AMD-only and remove the
        Hygon vendor check from get_next_root().

  [5/5] x86/hygon: Add Fam18h node enumeration, NB cache and SMN init
        Core implementation in hygon_node.c: CONFIG_HYGON_NODE Kconfig,
        three-phase cache build, hardware-anchored CPU-to-node mapping,
        NB cache registration via hygon_nb_init() and amd_nb_set_cache(),
        SMN root discovery and registration via hygon_smn_init() and
        smn_set_roots(). Both as independent fs_initcalls. All exported
        API functions.

== Testing ==

Verified boot, SMN access, and CPU-to-node mapping correctness on:
  - Hygon Family 0x18 Model 0x04, 4-socket (16 CDD + 4 IOD)
  - Hygon Family 0x18 Model 0x05, 2-socket (8 CDD + 2 IOD)
  - Hygon Family 0x18 Model 0x06, 2-socket, sparse DFID (8 CDD + 2 IOD)

== Feedback Requested ==

  - Is the generic registration approach (amd_nb_set_cache / smn_set_roots)
    the right way for non-AMD vendors to plug into the existing NB/SMN
    infrastructure, or would a different integration point be preferred?

  - Is the three-phase enumeration approach the right structure for
    Hygon DF node discovery?

  - Is the hardware-anchored bijection (Phase 3) a sound way to
    establish the CPU-to-CDD mapping, or is there a cleaner mechanism
    that does not rely on the DFID-ASC <-> phys_nid-ASC property?

  - Does the exported API (four functions) provide the right abstraction
    for downstream consumers?

== Changes since v1 [5] ==

The main change addresses Boris's feedback [3] and Mario's suggestions [4]:
all Hygon enablement is now in Hygon-specific files, with no Hygon
branches added to AMD code paths.

Specifically:

  - v1 patches 4/5 added Hygon branches directly into amd_nb.c
    (amd_cache_northbridges) and amd_node.c (amd_smn_init).
    v2 replaces this with two generic registration interfaces:
    amd_nb_set_cache() and smn_set_roots(). The AMD code paths
    themselves are unchanged; Hygon builds and registers its own
    data from hygon_node.c.

  - v1 reused AMD-prefixed PCI device IDs for Hygon devices.
    v2 defines Hygon-specific IDs (PCI_DEVICE_ID_HYGON_18H_M04H_*)
    in the Hygon section of pci_ids.h, per Mario's suggestion.

  - v2 introduces CONFIG_HYGON_NODE (depends on CPU_SUP_HYGON, PCI,
    AMD_NB, AMD_NODE) to gate all Hygon-specific code, per Mario's
    suggestion.

  - v1 left the Hygon vendor ID check in amd_node.c get_next_root().
    v2 removes it so that Hygon host bridges are no longer enumerated
    by the AMD path; Hygon enumerates its own roots in hygon_node.c.

Link: https://lore.kernel.org/all/20241206161210.163701-1-yazen.ghannam@amd.com/ # [1]
Link: https://lore.kernel.org/all/20250107222847.3300430-1-yazen.ghannam@amd.com/ # [2]
Link: https://lore.kernel.org/all/20260402235036.GDac8AzDPVVq-tBeG-@fat_crate.local/ # [3]
Link: https://lore.kernel.org/all/ab6a8335-48ab-4800-be27-761a91264ad7@amd.com/ # [4]
Link: https://lore.kernel.org/all/20260402111515.1155505-1-wanglin@open-hieco.net/ # [5] RFC v1

Lin Wang (5):
  pci_ids: Add Hygon Family 0x18 DF device IDs
  x86/hygon: Add Family 0x18 node enumeration API header
  x86/amd_nb: Add amd_nb_set_cache() for external NB cache registration
  x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration
  x86/hygon: Add Fam18h node enumeration, NB cache and SMN init

 MAINTAINERS                       |    3 +
 arch/x86/Kconfig                  |    4 +
 arch/x86/include/asm/amd/nb.h     |    7 +
 arch/x86/include/asm/amd/node.h   |    4 +
 arch/x86/include/asm/hygon/node.h |   85 +++
 arch/x86/kernel/Makefile          |    1 +
 arch/x86/kernel/amd_nb.c          |   22 +
 arch/x86/kernel/amd_node.c        |  106 ++-
 arch/x86/kernel/hygon_node.c      | 1008 +++++++++++++++++++++++++++++
 include/linux/pci_ids.h           |    6 +
 10 files changed, 1223 insertions(+), 23 deletions(-)
 create mode 100644 arch/x86/include/asm/hygon/node.h
 create mode 100644 arch/x86/kernel/hygon_node.c

-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [RFC PATCH v2 1/5] pci_ids: Add Hygon Family 0x18 DF device IDs
  2026-04-23  6:04 [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access Lin Wang
@ 2026-04-23  6:04 ` Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 2/5] x86/hygon: Add Family 0x18 node enumeration API header Lin Wang
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-04-23  6:04 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Add the PCI device IDs required to discover Hygon Family 0x18 Data Fabric
functions.

Hygon systems expose DF-related PCI functions using PCI_VENDOR_ID_HYGON.
All device IDs are defined with Hygon-specific names and explicit numeric
values.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 include/linux/pci_ids.h | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/include/linux/pci_ids.h b/include/linux/pci_ids.h
index 24cb42f66e4b..7b99708779d8 100644
--- a/include/linux/pci_ids.h
+++ b/include/linux/pci_ids.h
@@ -2615,6 +2615,12 @@
 #define PCI_VENDOR_ID_ROCKCHIP		0x1d87
 
 #define PCI_VENDOR_ID_HYGON		0x1d94
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3	0x1463
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3B	0x1493
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4	0x1464
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4B	0x1494
+#define PCI_DEVICE_ID_HYGON_18H_M05H_DF_F3	0x14b3
+#define PCI_DEVICE_ID_HYGON_18H_M10H_DF_F3	0x14d3
 
 #define PCI_VENDOR_ID_META		0x1d9b
 
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [RFC PATCH v2 2/5] x86/hygon: Add Family 0x18 node enumeration API header
  2026-04-23  6:04 [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 1/5] pci_ids: Add Hygon Family 0x18 DF device IDs Lin Wang
@ 2026-04-23  6:04 ` Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 3/5] x86/amd_nb: Add amd_nb_set_cache() for external NB cache registration Lin Wang
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-04-23  6:04 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Introduce asm/hygon/node.h, a Hygon-only header that provides the public
API for Hygon Family 0x18 Data Fabric node support.

Exported function prototypes (under CONFIG_HYGON_NODE):

  hygon_f18h_model()              model byte, 0 if not Hygon Fam18h
  hygon_cdd_num()                 compute die count for EDAC sizing
  hygon_get_dfid()                DF ID for a DF misc device
  hygon_cpu_to_df_node()          phys_node_id to dense DF node index

Inline model-range helper (usable regardless of CONFIG_HYGON_NODE):

  hygon_f18h_model_in_range()     true if model is within [first, last]

All functions have static inline stubs returning 0/NULL/-ENODEV when
CONFIG_HYGON_NODE=n, so consumers need no #ifdef guards.

Also add the MAINTAINERS entry for Hygon node support header.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 MAINTAINERS                       |  2 +
 arch/x86/include/asm/hygon/node.h | 85 +++++++++++++++++++++++++++++++
 2 files changed, 87 insertions(+)
 create mode 100644 arch/x86/include/asm/hygon/node.h

diff --git a/MAINTAINERS b/MAINTAINERS
index c9b7b6f9828e..3d5ef8a344f3 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11941,8 +11941,10 @@ F:	drivers/input/touchscreen/hycon-hy46xx.c
 
 HYGON PROCESSOR SUPPORT
 M:	Pu Wen <puwen@hygon.cn>
+M:	Lin Wang <wanglin@open-hieco.net>
 L:	linux-kernel@vger.kernel.org
 S:	Maintained
+F:	arch/x86/include/asm/hygon/
 F:	arch/x86/kernel/cpu/hygon.c
 
 HYNIX HI556 SENSOR DRIVER
diff --git a/arch/x86/include/asm/hygon/node.h b/arch/x86/include/asm/hygon/node.h
new file mode 100644
index 000000000000..18c4c7a3ea6e
--- /dev/null
+++ b/arch/x86/include/asm/hygon/node.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+#ifndef _ASM_X86_HYGON_NODE_H
+#define _ASM_X86_HYGON_NODE_H
+
+#include <linux/errno.h>
+#include <linux/types.h>
+
+struct pci_dev;
+
+#define HYGON_MAX_SOCKETS	8
+
+#ifdef CONFIG_HYGON_NODE
+
+/**
+ * hygon_cdd_num() - number of compute dies (CDD)
+ *
+ * DF IDs >= 4 represent compute dies (CDD). DF IDs < 4 represent IO dies
+ * (IOD). EDAC instances must be sized by CDD count, not total node count,
+ * because IOD nodes have no UMC.
+ */
+u16 hygon_cdd_num(void);
+
+/**
+ * hygon_f18h_model() - return Hygon Fam18h model byte, or 0 if not Hygon Fam18h
+ */
+u8 hygon_f18h_model(void);
+
+/**
+ * hygon_get_dfid() - read DF ID for a Hygon DF misc device
+ * @misc: PCI dev for DF misc (function 3)
+ * @dfid: output DF ID
+ */
+int hygon_get_dfid(struct pci_dev *misc, u8 *dfid);
+
+/**
+ * hygon_cpu_to_df_node() - map CPU to dense DF node index (O(1))
+ * @cpu: CPU index
+ *
+ * Hygon Fam18h exposes sparse physical node IDs (CPUID 8000001E[7:0]).
+ * This function translates the per-CPU physical node ID into a contiguous
+ * DF node index (0..hygon_cdd_num()-1) that aligns with EDAC instance
+ * numbering and amd_nb[] indexing.
+ *
+ * Return: DF node index on success, negative errno on failure.
+ */
+int hygon_cpu_to_df_node(unsigned int cpu);
+
+#else /* !CONFIG_HYGON_NODE */
+
+static inline u16 hygon_cdd_num(void)
+{
+	return 0;
+}
+
+static inline u8 hygon_f18h_model(void)
+{
+	return 0;
+}
+
+static inline int hygon_get_dfid(struct pci_dev *misc, u8 *dfid)
+{
+	return -ENODEV;
+}
+
+static inline int hygon_cpu_to_df_node(unsigned int cpu)
+{
+	return -ENODEV;
+}
+#endif /* CONFIG_HYGON_NODE */
+
+/**
+ * hygon_f18h_model_in_range() - check if Hygon Fam18h model is within [first, last]
+ * @first: lower bound (inclusive)
+ * @last:  upper bound (inclusive)
+ *
+ * Returns false on non-Hygon-Fam18h systems (hygon_f18h_model() returns 0).
+ */
+static inline bool hygon_f18h_model_in_range(u8 first, u8 last)
+{
+	u8 m = hygon_f18h_model();
+
+	return m >= first && m <= last;
+}
+
+#endif /* _ASM_X86_HYGON_NODE_H */
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [RFC PATCH v2 3/5] x86/amd_nb: Add amd_nb_set_cache() for external NB cache registration
  2026-04-23  6:04 [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 1/5] pci_ids: Add Hygon Family 0x18 DF device IDs Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 2/5] x86/hygon: Add Family 0x18 node enumeration API header Lin Wang
@ 2026-04-23  6:04 ` Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration Lin Wang
  2026-04-23  6:04 ` [RFC PATCH v2 5/5] x86/hygon: Add Fam18h node enumeration, NB cache and SMN init Lin Wang
  4 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-04-23  6:04 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Several subsystems (EDAC, MCE decode, ATL) consume the NB cache through
node_to_amd_nb() and amd_nb_num().  On Hygon Family 0x18, the AMD
enumeration path in amd_cache_northbridges() cannot discover DF nodes
because Hygon uses platform-assigned PCI slots rather than the fixed
00:18..1f layout.

Hygon therefore needs to populate the NB cache from its own enumeration
module (hygon_node.c) without adding Hygon-specific code or includes
into amd_nb.c.

Add amd_nb_set_cache() as a vendor-neutral data registration interface:
the caller builds a struct amd_northbridge[] array, populates the
mandatory misc and link device pointers, and transfers ownership to the
NB layer.  AMD-only legacy features (GART, L3 Index Disable, flags)
remain inactive for non-AMD callers because amd_cache_northbridges()
-- which sets those features -- is now guarded to AMD-only.

This keeps amd_nb.c free of any vendor-specific headers, defines, or
branch logic, while allowing non-AMD vendors to satisfy the existing
amd_nb consumer contracts.

No functional change for AMD systems.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 arch/x86/include/asm/amd/nb.h |  7 +++++++
 arch/x86/kernel/amd_nb.c      | 22 ++++++++++++++++++++++
 2 files changed, 29 insertions(+)

diff --git a/arch/x86/include/asm/amd/nb.h b/arch/x86/include/asm/amd/nb.h
index ddb5108cf46c..0f10911c08d5 100644
--- a/arch/x86/include/asm/amd/nb.h
+++ b/arch/x86/include/asm/amd/nb.h
@@ -47,6 +47,7 @@ struct amd_northbridge_info {
 u16 amd_nb_num(void);
 bool amd_nb_has_feature(unsigned int feature);
 struct amd_northbridge *node_to_amd_nb(int node);
+int amd_nb_set_cache(struct amd_northbridge *nb, u16 num);
 
 static inline bool amd_gart_present(void)
 {
@@ -69,6 +70,12 @@ static inline struct amd_northbridge *node_to_amd_nb(int node)
 {
 	return NULL;
 }
+
+static inline int amd_nb_set_cache(struct amd_northbridge *nb, u16 num)
+{
+	return -ENODEV;
+}
+
 #define amd_gart_present(x)	false
 
 #endif
diff --git a/arch/x86/kernel/amd_nb.c b/arch/x86/kernel/amd_nb.c
index 5d364540673d..8a066cf0f747 100644
--- a/arch/x86/kernel/amd_nb.c
+++ b/arch/x86/kernel/amd_nb.c
@@ -58,6 +58,25 @@ struct amd_northbridge *node_to_amd_nb(int node)
 }
 EXPORT_SYMBOL_GPL(node_to_amd_nb);
 
+/*
+ * Register a pre-built NB cache from a vendor-specific module (e.g. Hygon).
+ * Must be called at most once, before any amd_nb consumer is active.
+ * Returns -EBUSY if the cache is already populated.
+ */
+int amd_nb_set_cache(struct amd_northbridge *nb, u16 num)
+{
+	if (!nb || !num)
+		return -EINVAL;
+
+	if (amd_northbridges.num)
+		return -EBUSY;
+
+	amd_northbridges.nb  = nb;
+	amd_northbridges.num = num;
+	return 0;
+}
+EXPORT_SYMBOL_GPL(amd_nb_set_cache);
+
 static int amd_cache_northbridges(void)
 {
 	struct amd_northbridge *nb;
@@ -66,6 +85,9 @@ static int amd_cache_northbridges(void)
 	if (amd_northbridges.num)
 		return 0;
 
+	if (boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
+		return -ENODEV;
+
 	amd_northbridges.num = amd_num_nodes();
 
 	nb = kzalloc_objs(struct amd_northbridge, amd_northbridges.num);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration
  2026-04-23  6:04 [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access Lin Wang
                   ` (2 preceding siblings ...)
  2026-04-23  6:04 ` [RFC PATCH v2 3/5] x86/amd_nb: Add amd_nb_set_cache() for external NB cache registration Lin Wang
@ 2026-04-23  6:04 ` Lin Wang
  2026-04-23 19:12   ` Mario Limonciello
  2026-04-23  6:04 ` [RFC PATCH v2 5/5] x86/hygon: Add Fam18h node enumeration, NB cache and SMN init Lin Wang
  4 siblings, 1 reply; 8+ messages in thread
From: Lin Wang @ 2026-04-23  6:04 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Guard the AMD SMN init (amd_smn_init) to X86_VENDOR_AMD only, removing
the PCI_VENDOR_ID_HYGON check from get_next_root() so that Hygon host
bridges are no longer enumerated by the AMD path.

Split the original monolithic root registration into two explicit phases:

  smn_set_roots()  -- pure data registration: saves the per-node root
                      array and node count, with no side effects.

  smn_activate()   -- enables the SMN access layer: creates the optional
                      debugfs interface (gated by amd_smn_debugfs_enable)
                      and sets smn_exclusive so that amd_smn_read/write()
                      begin accepting requests.  Takes a debugfs_name
                      parameter so each vendor can use its own directory
                      name (e.g. "amd_smn", "hygon_smn").

This two-phase design keeps the API names honest (set_roots is a pure
setter, activate is an explicit state transition) and lets each vendor
control when the shared SMN layer becomes live.

Export both functions so that any vendor-specific init module can hand a
pre-built per-node root array to the shared SMN layer.

No functional change for AMD systems.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 arch/x86/include/asm/amd/node.h |   4 ++
 arch/x86/kernel/amd_node.c      | 106 +++++++++++++++++++++++++-------
 2 files changed, 87 insertions(+), 23 deletions(-)

diff --git a/arch/x86/include/asm/amd/node.h b/arch/x86/include/asm/amd/node.h
index a672b8765fa8..2705b4f59361 100644
--- a/arch/x86/include/asm/amd/node.h
+++ b/arch/x86/include/asm/amd/node.h
@@ -32,12 +32,16 @@ static inline u16 amd_num_nodes(void)
 #ifdef CONFIG_AMD_NODE
 int __must_check amd_smn_read(u16 node, u32 address, u32 *value);
 int __must_check amd_smn_write(u16 node, u32 address, u32 value);
+int smn_set_roots(struct pci_dev **roots, u16 num_nodes);
+int smn_activate(const char *debugfs_name);
 
 /* Should only be used by the HSMP driver. */
 int __must_check amd_smn_hsmp_rdwr(u16 node, u32 address, u32 *value, bool write);
 #else
 static inline int __must_check amd_smn_read(u16 node, u32 address, u32 *value) { return -ENODEV; }
 static inline int __must_check amd_smn_write(u16 node, u32 address, u32 value) { return -ENODEV; }
+static inline int smn_set_roots(struct pci_dev **roots, u16 num_nodes) { return -ENODEV; }
+static inline int smn_activate(const char *debugfs_name) { return -ENODEV; }
 
 static inline int __must_check amd_smn_hsmp_rdwr(u16 node, u32 address, u32 *value, bool write)
 {
diff --git a/arch/x86/kernel/amd_node.c b/arch/x86/kernel/amd_node.c
index 0be01725a2a4..76dbf847e567 100644
--- a/arch/x86/kernel/amd_node.c
+++ b/arch/x86/kernel/amd_node.c
@@ -9,6 +9,7 @@
  */
 
 #include <linux/debugfs.h>
+#include <asm/processor.h>
 #include <asm/amd/node.h>
 
 /*
@@ -34,7 +35,8 @@ struct pci_dev *amd_node_get_func(u16 node, u8 func)
 	return pci_get_domain_bus_and_slot(0, 0, PCI_DEVFN(AMD_NODE0_PCI_SLOT + node, func));
 }
 
-static struct pci_dev **amd_roots;
+static struct pci_dev **smn_roots;
+static u16 smn_num_nodes;
 
 /* Protect the PCI config register pairs used for SMN. */
 static DEFINE_MUTEX(smn_mutex);
@@ -88,10 +90,10 @@ static int __amd_smn_rw(u8 i_off, u8 d_off, u16 node, u32 address, u32 *value, b
 	struct pci_dev *root;
 	int err = -ENODEV;
 
-	if (node >= amd_num_nodes())
+	if (node >= smn_num_nodes)
 		return err;
 
-	root = amd_roots[node];
+	root = smn_roots[node];
 	if (!root)
 		return err;
 
@@ -151,7 +153,7 @@ static ssize_t smn_node_write(struct file *file, const char __user *userbuf,
 	if (ret)
 		return ret;
 
-	if (node >= amd_num_nodes())
+	if (node >= smn_num_nodes)
 		return -ENODEV;
 
 	debug_node = node;
@@ -225,8 +227,7 @@ static struct pci_dev *get_next_root(struct pci_dev *root)
 		if (root->devfn)
 			continue;
 
-		if (root->vendor != PCI_VENDOR_ID_AMD &&
-		    root->vendor != PCI_VENDOR_ID_HYGON)
+		if (root->vendor != PCI_VENDOR_ID_AMD)
 			continue;
 
 		break;
@@ -244,17 +245,78 @@ static int __init amd_smn_enable_dfs(char *str)
 }
 __setup("amd_smn_debugfs_enable", amd_smn_enable_dfs);
 
+/*
+ * Register a pre-built per-node SMN root array.  The caller allocates
+ * @roots and transfers ownership to the SMN layer; the array must remain
+ * valid for the lifetime of the system.
+ *
+ * This is a pure data registration; the SMN access layer is not yet
+ * usable until smn_activate() is called.
+ *
+ * Not concurrency-safe.  Intended to be called exactly once during boot
+ * (e.g. from an fs_initcall).  The caller is responsible for any
+ * serialization required by its own init path.
+ */
+int smn_set_roots(struct pci_dev **roots, u16 num_nodes)
+{
+	if (!roots || !num_nodes)
+		return -EINVAL;
+
+	if (smn_roots)
+		return -EBUSY;
+
+	smn_roots = roots;
+	smn_num_nodes = num_nodes;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(smn_set_roots);
+
+/*
+ * Activate the SMN access layer after roots have been registered via
+ * smn_set_roots().  Creates the optional debugfs interface (gated by
+ * the amd_smn_debugfs_enable boot parameter) and sets smn_exclusive so
+ * that amd_smn_read()/amd_smn_write() begin accepting requests.
+ *
+ * @debugfs_name: directory name under arch_debugfs_dir (e.g. "amd_smn")
+ *
+ * Same serialization rules as smn_set_roots(): must be called exactly
+ * once, at boot, after smn_set_roots() succeeds.
+ */
+int smn_activate(const char *debugfs_name)
+{
+	if (!smn_roots || !smn_num_nodes)
+		return -EINVAL;
+
+	if (smn_exclusive)
+		return -EBUSY;
+
+	if (enable_dfs) {
+		debugfs_dir = debugfs_create_dir(debugfs_name, arch_debugfs_dir);
+
+		debugfs_create_file("node",	0600, debugfs_dir, NULL, &smn_node_fops);
+		debugfs_create_file("address",	0600, debugfs_dir, NULL, &smn_address_fops);
+		debugfs_create_file("value",	0600, debugfs_dir, NULL, &smn_value_fops);
+	}
+
+	smn_exclusive = true;
+	return 0;
+}
+EXPORT_SYMBOL_GPL(smn_activate);
+
 static int __init amd_smn_init(void)
 {
 	u16 count, num_roots, roots_per_node, node, num_nodes;
-	struct pci_dev *root;
+	struct pci_dev **roots, *root;
+	int ret;
 
-	if (!cpu_feature_enabled(X86_FEATURE_ZEN))
+	if (!cpu_feature_enabled(X86_FEATURE_ZEN) ||
+	    boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
 		return 0;
 
 	guard(mutex)(&smn_mutex);
 
-	if (amd_roots)
+	if (smn_roots)
 		return 0;
 
 	num_roots = 0;
@@ -268,7 +330,9 @@ static int __init amd_smn_init(void)
 		 * entire PCI config space for simplicity rather than covering
 		 * specific registers piecemeal.
 		 */
-		if (!pci_request_config_region_exclusive(root, 0, PCI_CFG_SPACE_SIZE, NULL)) {
+		if (!pci_request_config_region_exclusive(root, 0,
+							 PCI_CFG_SPACE_SIZE,
+							 NULL)) {
 			pci_err(root, "Failed to reserve config space\n");
 			return -EEXIST;
 		}
@@ -276,14 +340,14 @@ static int __init amd_smn_init(void)
 		num_roots++;
 	}
 
-	pr_debug("Found %d AMD root devices\n", num_roots);
+	pr_debug("Found %d SMN root devices\n", num_roots);
 
 	if (!num_roots)
 		return -ENODEV;
 
 	num_nodes = amd_num_nodes();
-	amd_roots = kzalloc_objs(*amd_roots, num_nodes);
-	if (!amd_roots)
+	roots = kzalloc_objs(*roots, num_nodes);
+	if (!roots)
 		return -ENOMEM;
 
 	roots_per_node = num_roots / num_nodes;
@@ -297,20 +361,16 @@ static int __init amd_smn_init(void)
 			continue;
 
 		pci_dbg(root, "is root for AMD node %u\n", node);
-		amd_roots[node++] = root;
+		roots[node++] = root;
 	}
 
-	if (enable_dfs) {
-		debugfs_dir = debugfs_create_dir("amd_smn", arch_debugfs_dir);
-
-		debugfs_create_file("node",	0600, debugfs_dir, NULL, &smn_node_fops);
-		debugfs_create_file("address",	0600, debugfs_dir, NULL, &smn_address_fops);
-		debugfs_create_file("value",	0600, debugfs_dir, NULL, &smn_value_fops);
+	ret = smn_set_roots(roots, num_nodes);
+	if (ret) {
+		kfree(roots);
+		return ret;
 	}
 
-	smn_exclusive = true;
-
-	return 0;
+	return smn_activate("amd_smn");
 }
 
 fs_initcall(amd_smn_init);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [RFC PATCH v2 5/5] x86/hygon: Add Fam18h node enumeration, NB cache and SMN init
  2026-04-23  6:04 [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access Lin Wang
                   ` (3 preceding siblings ...)
  2026-04-23  6:04 ` [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration Lin Wang
@ 2026-04-23  6:04 ` Lin Wang
  4 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-04-23  6:04 UTC (permalink / raw)
  To: yazen.ghannam, mario.limonciello, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Add hygon_node.c, the Hygon Family 0x18 Data Fabric node enumeration
and registration module.

DF node enumeration:

  AMD systems enumerate DF nodes at fixed PCI slots 00:18..1f on bus 0.
  Hygon Fam18h places DF instances at platform-assigned slots with no
  fixed relationship to node identity.

  hygon_build_cache() walks DF misc (F3) devices by PCI ID, reads
  hardware identity from per-instance registers (F1x200 SystemCfg,
  F5x180 FabricId), and builds a sorted node cache that classifies
  each node as CDD (compute, DFID >= 4) or IOD (I/O, DFID < 4).

CPU-to-node mapping:

  Establishes a hardware-anchored bijection between per-CPU phys_node_id
  (CPUID 8000001E[7:0]) and dense DF node indices (0..num_cdd-1) for
  O(1) lookup by EDAC, MCE decode, and ATL consumers.

NB cache and SMN root registration:

  hygon_nb_init() registers a struct amd_northbridge[] array via
  amd_nb_set_cache(), making the PCI config-space view of each DF node
  available to amd_nb consumers independently of SMN.

  hygon_smn_init() registers a per-node SMN root array via
  smn_set_roots(). Hygon shares SMN root devices per-socket; all nodes
  on the same socket use the same host-bridge root.

Exported API for loadable module consumers (EDAC, MCE, ATL):
  hygon_f18h_model(), hygon_cdd_num(), hygon_get_dfid(),
  hygon_cpu_to_df_node()

Introduce CONFIG_HYGON_NODE (depends on CPU_SUP_HYGON, PCI, AMD_NB,
AMD_NODE) to gate all Hygon-specific code.

Also add the hygon_node.c entry to MAINTAINERS.

Signed-off-by: Lin Wang <wanglin@open-hieco.net>
---
 MAINTAINERS                  |    1 +
 arch/x86/Kconfig             |    4 +
 arch/x86/kernel/Makefile     |    1 +
 arch/x86/kernel/hygon_node.c | 1008 ++++++++++++++++++++++++++++++++++
 4 files changed, 1014 insertions(+)
 create mode 100644 arch/x86/kernel/hygon_node.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 3d5ef8a344f3..8d39878fd0ab 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11945,6 +11945,7 @@ M:	Lin Wang <wanglin@open-hieco.net>
 L:	linux-kernel@vger.kernel.org
 S:	Maintained
 F:	arch/x86/include/asm/hygon/
+F:	arch/x86/kernel/hygon_node.c
 F:	arch/x86/kernel/cpu/hygon.c
 
 HYNIX HI556 SENSOR DRIVER
diff --git a/arch/x86/Kconfig b/arch/x86/Kconfig
index 99bb5217649a..4a40eec7d784 100644
--- a/arch/x86/Kconfig
+++ b/arch/x86/Kconfig
@@ -3118,6 +3118,10 @@ config AMD_NODE
 	def_bool y
 	depends on CPU_SUP_AMD && PCI
 
+config HYGON_NODE
+	def_bool y
+	depends on CPU_SUP_HYGON && PCI && AMD_NB && AMD_NODE
+
 endmenu
 
 menu "Binary Emulations"
diff --git a/arch/x86/kernel/Makefile b/arch/x86/kernel/Makefile
index 47a32f583930..b104391cf460 100644
--- a/arch/x86/kernel/Makefile
+++ b/arch/x86/kernel/Makefile
@@ -136,6 +136,7 @@ obj-$(CONFIG_HPET_TIMER) 	+= hpet.o
 
 obj-$(CONFIG_AMD_NB)		+= amd_nb.o
 obj-$(CONFIG_AMD_NODE)		+= amd_node.o
+obj-$(CONFIG_HYGON_NODE)	+= hygon_node.o
 obj-$(CONFIG_DEBUG_NMI_SELFTEST) += nmi_selftest.o
 
 obj-$(CONFIG_KVM_GUEST)		+= kvm.o kvmclock.o
diff --git a/arch/x86/kernel/hygon_node.c b/arch/x86/kernel/hygon_node.c
new file mode 100644
index 000000000000..fd39bf83ad0b
--- /dev/null
+++ b/arch/x86/kernel/hygon_node.c
@@ -0,0 +1,1008 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Hygon Family 0x18 Data Fabric node enumeration and registration
+ *
+ * AMD systems enumerate DF nodes at fixed PCI slots 00:18..1f on bus 0.
+ * Hygon Fam18h places DF instances at platform-assigned slots with no
+ * fixed relationship to node identity, so the AMD enumeration path in
+ * amd_nb.c / amd_node.c cannot be used.
+ *
+ * This module provides:
+ *
+ *  - DF node enumeration: walk DF misc (F3) devices by PCI ID, read
+ *    hardware identity from per-instance registers (F1x200 SystemCfg,
+ *    F5x180 FabricId), and build a sorted node cache that classifies
+ *    each node as CDD (compute, DFID >= 4) or IOD (I/O, DFID < 4).
+ *
+ *  - CPU-to-node mapping: establish a hardware-anchored bijection between
+ *    per-CPU phys_node_id (CPUID 8000001E[7:0]) and dense logical node
+ *    IDs (0..num_cdd-1) for O(1) lookup by EDAC, MCE decode, and ATL.
+ *
+ *  - NB cache and SMN root registration: register the per-node PCI
+ *    config-space view via amd_nb_set_cache(), and register the
+ *    per-node SMN root array independently via smn_set_roots().
+ */
+
+#define pr_fmt(fmt) "hygon_node: " fmt
+
+#include <linux/bitmap.h>
+#include <linux/bitops.h>
+#include <linux/cleanup.h>
+#include <linux/cpu.h>
+#include <linux/export.h>
+#include <linux/init.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <linux/pci_ids.h>
+#include <linux/slab.h>
+#include <linux/sort.h>
+
+#include <asm/amd/nb.h>
+#include <asm/amd/node.h>
+#include <asm/hygon/node.h>
+#include <asm/processor.h>
+#include <asm/topology.h>
+
+/* CPUID 8000001E[7:0] is 8-bit and globally unique. */
+#define HYGON_MAX_PHYS_NID	256
+/* Sentinel for unmapped entries in nid_to_logical[]. */
+#define HYGON_NID_INVALID	U8_MAX
+
+/*
+ * DF register offsets used for node identity discovery.
+ *
+ * F1x200 (SystemCfg) -- present on all models:
+ *   [30:28]  MySocketId    - hardware socket ID
+ *   [23:20]  MyDieId       - die ID (equals DFID on some models, see below)
+ *
+ * F5x180 (FabricBlockInstanceInformation3_CS) -- Model 06h-08h only:
+ *   [19:16]  DFID          - real Data Fabric ID for UMC/SMN addressing
+ *
+ * DFID source by model:
+ *   Model 04h/05h:  F1x200[23:20] (MyDieId == DFID)
+ *   Model 06h-08h:  F5x180[19:16] (MyDieId != DFID, different numbering)
+ *   Model 10h+:     F1x200[23:20] (MyDieId == DFID, same as Model 04h/05h)
+ */
+#define DF_F1_SYSTEM_CFG	0x200
+#define DF_F5_FABRIC_ID		0x180
+
+/* DF function numbers for sibling device access. */
+#define HYGON_DF_F1	1	/* SystemCfg: socket and die identity */
+#define HYGON_DF_F3	3	/* Miscellaneous (misc) */
+#define HYGON_DF_F4	4	/* Link */
+#define HYGON_DF_F5	5	/* FabricId: real DFID on Model 06h-08h */
+
+/* DF sibling device IDs used only within this file for identity reads. */
+#define PCI_DEVICE_ID_HYGON_18H_M04H_DF_F1	0x1491
+#define PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1	0x14b1
+#define PCI_DEVICE_ID_HYGON_18H_M05H_DF_F4	0x14b4
+#define PCI_DEVICE_ID_HYGON_18H_M06H_DF_F5	0x14b5
+#define PCI_DEVICE_ID_HYGON_18H_M10H_DF_F4	0x14d4
+
+/*
+ * Cached identity for one DF instance.  After sorting, CDDs occupy
+ * nodes[0..num_cdd-1].
+ *
+ * socket_id + dfid together uniquely identify a DF node in hardware.
+ * Unlike AMD (fixed PCI slot == node ID), Hygon DF nodes sit at
+ * platform-assigned slots, so EDAC / MCE consumers that need to map an
+ * error to a physical location will eventually need both fields exposed
+ * (e.g. "socket=1 dfid=5" in ECC error logs).
+ */
+struct hygon_node {
+	struct pci_dev	*misc;		/* DF function 3 */
+	struct pci_dev	*link;		/* DF function 4 */
+	u8		socket_id;	/* F1x200[30:28] */
+	u8		dfid;		/* model-dependent DFID */
+	bool		is_cdd;		/* DFID >= 4 */
+};
+
+struct hygon_node_cache {
+	struct hygon_node	*nodes;		/* sorted: CDD first, then IOD */
+	u16			num_nodes;	/* CDD + IOD = total logical nodes */
+	u16			num_cdd;	/* CDD only = EDAC instance count */
+	u16			num_sockets;
+
+	/*
+	 * Direct sparse-to-dense mapping. nid_to_logical[phys_nid] gives the
+	 * dense logical_node_id (0..num_cdd-1), or HYGON_NID_INVALID.
+	 */
+	u8			nid_to_logical[HYGON_MAX_PHYS_NID];
+
+	bool			ready;
+	bool			failed;
+};
+
+struct hygon_df_id {
+	u8	socket_id;
+	u8	dfid;
+};
+
+/* Model-specific DF sibling device IDs for reading node identity. */
+struct hygon_df_func_ids {
+	u8	model_start;
+	u8	model_end;
+	u16	f1_id;
+	u16	f5_id;			/* 0 = not available */
+};
+
+/* DF misc (F3) device IDs for all supported Hygon Family 0x18 models. */
+static const struct pci_device_id hygon_nb_misc_ids[] = {
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3) },  /* M04h */
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F3B) }, /* M04h variant */
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F3) },  /* M05h-08h */
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M10H_DF_F3) },  /* M10h+ */
+	{}
+};
+
+/* DF link (F4) device IDs, parallel to hygon_nb_misc_ids[]. */
+static const struct pci_device_id hygon_nb_link_ids[] = {
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4) },  /* M04h */
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F4B) }, /* M04h variant */
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F4) }, /* M05h-08h */
+	{ PCI_DEVICE(PCI_VENDOR_ID_HYGON, PCI_DEVICE_ID_HYGON_18H_M10H_DF_F4) }, /* M10h+ */
+	{}
+};
+
+/*
+ * Map Hygon Fam18h model ranges onto the DF sibling functions used to read
+ * node identity. Model 5 may still expose the Model 4 F1 device id on mixed
+ * silicon, which is handled separately in hygon_read_df_reg().
+ */
+static const struct hygon_df_func_ids hygon_df_table[] = {
+	{ 0x04, 0x04, PCI_DEVICE_ID_HYGON_18H_M04H_DF_F1, 0 },
+	{ 0x05, 0x05, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1, 0 },
+	{ 0x06, 0x08, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1,
+		      PCI_DEVICE_ID_HYGON_18H_M06H_DF_F5 },
+	{ 0x10, 0x1f, PCI_DEVICE_ID_HYGON_18H_M05H_DF_F1, 0 },
+	{}
+};
+
+static struct hygon_node_cache hygon_cache;
+static DEFINE_MUTEX(hygon_mutex);
+
+static void hygon_dump_nodes(const struct hygon_node_cache *cache, const char *phase)
+{
+	u16 i;
+
+	pr_debug("%s: %u nodes\n", phase, cache->num_nodes);
+
+	for (i = 0; i < cache->num_nodes; i++) {
+		const struct hygon_node *node = &cache->nodes[i];
+
+		pr_debug("%s: node[%u] %04x:%02x:%02x.%u socket=%u dfid=%u type=%s\n",
+			 phase, i, pci_domain_nr(node->misc->bus),
+			 node->misc->bus->number,
+			 PCI_SLOT(node->misc->devfn),
+			 PCI_FUNC(node->misc->devfn),
+			 node->socket_id, node->dfid,
+			 node->is_cdd ? "CDD" : "IOD");
+	}
+}
+
+/*
+ * Iterate Hygon PCI devices, returning the next one that matches @ids.
+ * Follows the pci_get_device() convention: @from is consumed (its reference
+ * is dropped) and the returned device has an elevated reference count.
+ */
+static struct pci_dev *next_hygon_dev(struct pci_dev *from,
+				      const struct pci_device_id *ids)
+{
+	while ((from = pci_get_device(PCI_VENDOR_ID_HYGON, PCI_ANY_ID, from))) {
+		if (pci_match_id(ids, from))
+			return from;
+	}
+
+	return NULL;
+}
+
+/*
+ * Find the DF link (function 4) sibling of a DF misc (function 3) device.
+ * Both functions share the same PCI bus and slot.
+ */
+static struct pci_dev *hygon_get_link(struct pci_dev *misc)
+{
+	struct pci_dev *link;
+
+	link = pci_get_domain_bus_and_slot(pci_domain_nr(misc->bus),
+					   misc->bus->number,
+					   PCI_DEVFN(PCI_SLOT(misc->devfn), HYGON_DF_F4));
+	if (!link)
+		return NULL;
+
+	if (!pci_match_id(hygon_nb_link_ids, link)) {
+		pci_dev_put(link);
+		return NULL;
+	}
+
+	return link;
+}
+
+/* Look up the DF sibling device IDs for the current boot CPU model. */
+static const struct hygon_df_func_ids *hygon_get_df_func_ids(void)
+{
+	const struct hygon_df_func_ids *entry;
+	u8 model = boot_cpu_data.x86_model;
+
+	for (entry = hygon_df_table; entry->f1_id; entry++) {
+		if (model >= entry->model_start && model <= entry->model_end)
+			return entry;
+	}
+
+	pr_warn_once("unsupported Hygon Fam18h model 0x%x, Hygon node support disabled\n",
+		     model);
+	return NULL;
+}
+
+/*
+ * Read a config register from a DF sibling function on the same PCI slot as
+ * @misc.  Only functions 1 (F1, SystemCfg) and 5 (F5, FabricId) are
+ * supported.
+ */
+static int hygon_read_df_reg(struct pci_dev *misc, u8 func, int offset,
+			     u32 *value)
+{
+	const struct hygon_df_func_ids *ids;
+	struct pci_dev *sibling;
+	u16 expected_device;
+	int err;
+
+	ids = hygon_get_df_func_ids();
+	if (!ids)
+		return -ENODEV;
+
+	if (func == HYGON_DF_F1) {
+		expected_device = ids->f1_id;
+
+		/*
+		 * Model 5 can expose an older mixed-silicon variant where the
+		 * F1 sibling still uses the M04H device ID.
+		 */
+		if (boot_cpu_data.x86_model == 0x5 &&
+		    misc->device != PCI_DEVICE_ID_HYGON_18H_M05H_DF_F3)
+			expected_device = PCI_DEVICE_ID_HYGON_18H_M04H_DF_F1;
+	} else if (func == HYGON_DF_F5) {
+		expected_device = ids->f5_id;
+	} else {
+		return -EINVAL;
+	}
+
+	if (!expected_device)
+		return -ENODEV;
+
+	sibling = pci_get_domain_bus_and_slot(pci_domain_nr(misc->bus),
+					      misc->bus->number,
+					      PCI_DEVFN(PCI_SLOT(misc->devfn),
+							func));
+	if (!sibling)
+		return -ENODEV;
+
+	if (sibling->vendor != PCI_VENDOR_ID_HYGON ||
+	    sibling->device != expected_device) {
+		pci_dev_put(sibling);
+		return -ENODEV;
+	}
+
+	err = pci_read_config_dword(sibling, offset, value);
+	pci_dev_put(sibling);
+
+	if (err)
+		pr_warn("error reading %04x:%02x:%02x.%u offset 0x%x\n",
+			pci_domain_nr(misc->bus), misc->bus->number,
+			PCI_SLOT(misc->devfn), func, offset);
+
+	return pcibios_err_to_errno(err);
+}
+
+/*
+ * Read the hardware identity for one DF misc device from its sibling
+ * functions.
+ *
+ * All models expose F1x200 (SystemCfg): socket_id from [30:28] and a die
+ * identifier (MyDieId) from [23:20].
+ * On Model 06h-08h MyDieId differs from the DFID used by UMC and SMN
+ * addressing, so an additional F5x180 (FabricId) read is required to
+ * obtain the real DFID from [19:16].
+ */
+static int hygon_read_df_id(struct pci_dev *misc, struct hygon_df_id *id)
+{
+	u32 reg;
+	int ret;
+
+	ret = hygon_read_df_reg(misc, HYGON_DF_F1, DF_F1_SYSTEM_CFG, &reg);
+	if (ret)
+		return ret;
+
+	id->socket_id = (reg >> 28) & 0x7;
+	id->dfid      = (reg >> 20) & 0xf;
+
+	/*
+	 * All DF instances on a Hygon system are the same model, so
+	 * boot_cpu_data.x86_model is representative for all devices.
+	 */
+	if (boot_cpu_data.x86_model >= 0x6 &&
+	    boot_cpu_data.x86_model <= 0x8) {
+		ret = hygon_read_df_reg(misc, HYGON_DF_F5, DF_F5_FABRIC_ID, &reg);
+		if (ret)
+			return ret;
+		id->dfid = (reg >> 16) & 0xf;
+	}
+
+	return 0;
+}
+
+static void hygon_release_nodes(struct hygon_node *nodes, u16 count)
+{
+	u16 i;
+
+	for (i = 0; i < count; i++) {
+		pci_dev_put(nodes[i].misc);
+		pci_dev_put(nodes[i].link);
+	}
+
+	kfree(nodes);
+}
+
+/*
+ * Walk all DF misc (F3) devices, read hardware identity (socket_id, dfid)
+ * from each instance's F1/F5 siblings, and collect them into a flat array.
+ * Validate that socket IDs are dense (0..N-1).
+ */
+static int hygon_collect_nodes(struct hygon_node_cache *cache)
+{
+	struct hygon_node *nodes;
+	struct pci_dev *misc;
+	u16 capacity = 0, count = 0;
+	u8 socket_mask = 0;
+	int ret;
+
+	misc = NULL;
+	while ((misc = next_hygon_dev(misc, hygon_nb_misc_ids)))
+		capacity++;
+
+	if (!capacity)
+		return -ENODEV;
+
+	nodes = kcalloc(capacity, sizeof(*nodes), GFP_KERNEL);
+	if (!nodes)
+		return -ENOMEM;
+
+	misc = NULL;
+	while ((misc = next_hygon_dev(misc, hygon_nb_misc_ids))) {
+		struct hygon_df_id id;
+		struct pci_dev *link;
+
+		link = hygon_get_link(misc);
+		if (!link) {
+			pci_dev_put(misc);
+			ret = -ENODEV;
+			goto fail;
+		}
+
+		ret = hygon_read_df_id(misc, &id);
+		if (ret) {
+			pci_dev_put(link);
+			pci_dev_put(misc);
+			goto fail;
+		}
+
+		if (count >= capacity) {
+			pci_dev_put(link);
+			pci_dev_put(misc);
+			ret = -ENODEV;
+			goto fail;
+		}
+
+		pr_debug("collect: %04x:%02x:%02x.%u socket=%u dfid=%u\n",
+			 pci_domain_nr(misc->bus), misc->bus->number,
+			 PCI_SLOT(misc->devfn), PCI_FUNC(misc->devfn),
+			 id.socket_id, id.dfid);
+
+		nodes[count].misc      = pci_dev_get(misc);
+		nodes[count].link      = link;
+		nodes[count].socket_id = id.socket_id;
+		nodes[count].dfid      = id.dfid;
+		nodes[count].is_cdd    = (id.dfid >= 4);
+		socket_mask |= BIT(id.socket_id);
+		count++;
+	}
+
+	if (!count) {
+		ret = -ENODEV;
+		goto fail;
+	}
+
+	cache->nodes       = nodes;
+	cache->num_nodes   = count;
+	cache->num_sockets = hweight8(socket_mask);
+
+	if (!cache->num_sockets ||
+	    socket_mask != GENMASK(cache->num_sockets - 1, 0)) {
+		pr_warn("sparse socket IDs not supported (mask=0x%x)\n",
+			socket_mask);
+		ret = -EINVAL;
+		goto fail;
+	}
+
+	return 0;
+
+fail:
+	hygon_release_nodes(nodes, count);
+	cache->nodes = NULL;
+	cache->num_nodes = 0;
+	return ret;
+}
+
+/* Sort CDD nodes before IOD nodes, then order by hardware (socket_id, dfid). */
+static int hygon_node_cmp(const void *a, const void *b)
+{
+	const struct hygon_node *left = a;
+	const struct hygon_node *right = b;
+
+	if (left->is_cdd != right->is_cdd)
+		return right->is_cdd - left->is_cdd;
+
+	if (left->socket_id != right->socket_id)
+		return (int)left->socket_id - right->socket_id;
+
+	return (int)left->dfid - (int)right->dfid;
+}
+
+/*
+ * Classify the sorted node array and validate the CDD layout.
+ *
+ * Hygon EDAC sizing assumes each socket contributes the same number of compute
+ * dies, so reject topologies that do not satisfy that invariant.
+ */
+static int hygon_sort_and_classify(struct hygon_node_cache *cache)
+{
+	u16 cdd_per_socket;
+	u16 i;
+	u8 per_sock_count[HYGON_MAX_SOCKETS] = { 0 };
+
+	hygon_dump_nodes(cache, "before-sort");
+
+	sort(cache->nodes, cache->num_nodes, sizeof(*cache->nodes),
+	     hygon_node_cmp, NULL);
+
+	for (i = 0; i < cache->num_nodes; i++) {
+		if (!cache->nodes[i].is_cdd)
+			break;
+	}
+
+	cache->num_cdd = i;
+
+	if (!cache->num_cdd)
+		return -ENODEV;
+
+	if (cache->num_cdd > HYGON_NID_INVALID) {
+		pr_warn("CDD count %u exceeds u8 logical ID range\n",
+			cache->num_cdd);
+		return -EOVERFLOW;
+	}
+
+	if (cache->num_cdd % cache->num_sockets) {
+		pr_warn("CDD count %u not divisible by %u sockets\n",
+			cache->num_cdd, cache->num_sockets);
+		return -EINVAL;
+	}
+
+	cdd_per_socket = cache->num_cdd / cache->num_sockets;
+	if (!cdd_per_socket)
+		return -EINVAL;
+
+	for (i = 0; i < cache->num_cdd; i++) {
+		u8 socket_id = cache->nodes[i].socket_id;
+
+		if (socket_id >= cache->num_sockets)
+			return -EINVAL;
+		per_sock_count[socket_id]++;
+	}
+
+	for (i = 0; i < cache->num_sockets; i++) {
+		if (per_sock_count[i] != cdd_per_socket) {
+			pr_warn("socket %u: %u CDDs, expected %u\n",
+				i, per_sock_count[i], cdd_per_socket);
+			return -EINVAL;
+		}
+	}
+
+	hygon_dump_nodes(cache, "after-sort");
+
+	return 0;
+}
+
+static int hygon_u8_cmp(const void *a, const void *b)
+{
+	return (int)*(const u8 *)a - (int)*(const u8 *)b;
+}
+
+/*
+ * Build nid_to_logical[]: collect unique phys_node_id values from online CPUs,
+ * sort them globally ascending, then write nid_to_logical[phys_nids[i]] = i.
+ *
+ * This works because both orderings follow the same physical progression:
+ *   nodes[] CDD region: (socket_id ASC, dfid ASC) from DF registers
+ *   phys_nids[]:        globally ascending; lower socket always occupies a
+ *                       lower phys_nid range, and within a socket ascending
+ *                       DFID order corresponds to ascending phys_nid order.
+ *
+ * A local bitmap (nid_seen) de-duplicates CPUs that share a node; each
+ * unique phys_nid is collected exactly once into phys_nids[]. The resulting
+ * runtime lookup is O(1): a direct array access by phys_nid.
+ */
+static int hygon_build_nid_map(struct hygon_node_cache *cache)
+{
+	DECLARE_BITMAP(nid_seen, HYGON_MAX_PHYS_NID) = { };
+	u8 *phys_nids;
+	u16 count = 0;
+	u16 i;
+	int cpu;
+
+	phys_nids = kcalloc(cache->num_cdd, sizeof(*phys_nids), GFP_KERNEL);
+	if (!phys_nids)
+		return -ENOMEM;
+
+	for_each_online_cpu(cpu) {
+		unsigned int phys_nid = topology_amd_node_id(cpu);
+
+		if (phys_nid >= HYGON_MAX_PHYS_NID) {
+			pr_warn("cpu %u: phys_node_id %u out of range\n",
+				cpu, phys_nid);
+			kfree(phys_nids);
+			return -ERANGE;
+		}
+
+		if (__test_and_set_bit(phys_nid, nid_seen))
+			continue;
+
+		if (count >= cache->num_cdd) {
+			pr_warn("more unique phys_node_ids than CDD nodes\n");
+			kfree(phys_nids);
+			return -EINVAL;
+		}
+
+		phys_nids[count++] = (u8)phys_nid;
+	}
+
+	if (count != cache->num_cdd) {
+		pr_warn("collected %u phys_node_ids, expected %u CDDs\n",
+			count, cache->num_cdd);
+		kfree(phys_nids);
+		return -EINVAL;
+	}
+
+	sort(phys_nids, count, sizeof(*phys_nids), hygon_u8_cmp, NULL);
+
+	memset(cache->nid_to_logical, HYGON_NID_INVALID,
+	       sizeof(cache->nid_to_logical));
+
+	for (i = 0; i < count; i++)
+		cache->nid_to_logical[phys_nids[i]] = i;
+
+	kfree(phys_nids);
+	return 0;
+}
+
+static void hygon_destroy_cache(struct hygon_node_cache *cache)
+{
+	if (cache->nodes)
+		hygon_release_nodes(cache->nodes, cache->num_nodes);
+
+	cache->nodes = NULL;
+	cache->num_nodes = 0;
+	cache->num_cdd = 0;
+	cache->num_sockets = 0;
+	memset(cache->nid_to_logical, HYGON_NID_INVALID,
+	       sizeof(cache->nid_to_logical));
+	cache->ready = false;
+}
+
+/*
+ * Build and publish the global Hygon node cache.
+ *
+ * Uses a double-checked locking pattern: the first smp_load_acquire() on
+ * @ready provides a fast lockless path for all calls after the initial build.
+ * The second check inside the mutex prevents duplicate construction if two
+ * callers race through the first check simultaneously.
+ *
+ * On success, smp_store_release() on @ready ensures all cache writes are
+ * visible to subsequent smp_load_acquire() readers without a lock.
+ *
+ * On failure, @failed is set under the mutex so that subsequent callers
+ * return immediately without repeating the expensive PCI walk.
+ */
+static int hygon_build_cache(void)
+{
+	struct hygon_node_cache tmp;
+	int err;
+
+	/* Pairs with smp_store_release() below; fast path once cache is built. */
+	if (smp_load_acquire(&hygon_cache.ready))
+		return 0;
+
+	if (boot_cpu_data.x86_vendor != X86_VENDOR_HYGON ||
+	    boot_cpu_data.x86 != 0x18)
+		return -ENODEV;
+
+	guard(mutex)(&hygon_mutex);
+
+	/* Re-check under mutex to handle concurrent builders. */
+	if (smp_load_acquire(&hygon_cache.ready))
+		return 0;
+
+	if (hygon_cache.failed)
+		return -ENODEV;
+
+	memset(&tmp, 0, sizeof(tmp));
+	memset(tmp.nid_to_logical, HYGON_NID_INVALID,
+	       sizeof(tmp.nid_to_logical));
+
+	err = hygon_collect_nodes(&tmp);
+	if (err)
+		goto fail;
+
+	err = hygon_sort_and_classify(&tmp);
+	if (err)
+		goto fail;
+
+	err = hygon_build_nid_map(&tmp);
+	if (err)
+		goto fail;
+
+	hygon_cache = tmp;
+	/* Pairs with smp_load_acquire() above; ensures cache is visible to all CPUs. */
+	smp_store_release(&hygon_cache.ready, true);
+	return 0;
+
+fail:
+	hygon_destroy_cache(&tmp);
+	hygon_cache.failed = true;
+	return err;
+}
+
+u8 hygon_f18h_model(void)
+{
+	if (boot_cpu_data.x86_vendor == X86_VENDOR_HYGON &&
+	    boot_cpu_data.x86 == 0x18)
+		return boot_cpu_data.x86_model;
+
+	return 0;
+}
+EXPORT_SYMBOL_GPL(hygon_f18h_model);
+
+int hygon_get_dfid(struct pci_dev *misc, u8 *dfid)
+{
+	struct hygon_df_id id;
+	int ret;
+
+	ret = hygon_read_df_id(misc, &id);
+	if (ret)
+		return ret;
+
+	*dfid = id.dfid;
+	return 0;
+}
+EXPORT_SYMBOL_GPL(hygon_get_dfid);
+
+static u16 hygon_node_num(void)
+{
+	return hygon_build_cache() ? 0 : hygon_cache.num_nodes;
+}
+
+u16 hygon_cdd_num(void)
+{
+	return hygon_build_cache() ? 0 : hygon_cache.num_cdd;
+}
+EXPORT_SYMBOL_GPL(hygon_cdd_num);
+
+static u16 hygon_socket_num(void)
+{
+	return hygon_build_cache() ? 0 : hygon_cache.num_sockets;
+}
+
+/*
+ * Return the DF function device for a logical Hygon node.
+ * The caller must call pci_dev_put() on the returned pointer when done.
+ * Only functions 3 (misc) and 4 (link) are supported.
+ */
+static struct pci_dev *hygon_node_get_func(u16 node, u8 func)
+{
+	if (hygon_build_cache())
+		return NULL;
+
+	if (node >= hygon_cache.num_nodes)
+		return NULL;
+
+	switch (func) {
+	case HYGON_DF_F3:
+		return pci_dev_get(hygon_cache.nodes[node].misc);
+	case HYGON_DF_F4:
+		return pci_dev_get(hygon_cache.nodes[node].link);
+	default:
+		return NULL;
+	}
+}
+
+static u8 hygon_node_socket(u16 node)
+{
+	if (hygon_build_cache())
+		return U8_MAX;
+
+	if (node >= hygon_cache.num_nodes)
+		return U8_MAX;
+
+	return hygon_cache.nodes[node].socket_id;
+}
+
+/*
+ * Translate a CPU's sparse physical node ID (CPUID 8000001E[7:0]) into a
+ * dense DF node index (0..hygon_cdd_num()-1) used by NB, EDAC, MCE, and
+ * ATL.  The lookup is O(1) via direct array access on nid_to_logical[].
+ *
+ * Returns the DF node index on success, or a negative errno on failure.
+ */
+int hygon_cpu_to_df_node(unsigned int cpu)
+{
+	unsigned int phys_nid;
+	u8 logical_id;
+
+	if (hygon_build_cache())
+		return -ENODEV;
+
+	phys_nid = topology_amd_node_id(cpu);
+	if (phys_nid >= HYGON_MAX_PHYS_NID)
+		return -ENODEV;
+
+	logical_id = hygon_cache.nid_to_logical[phys_nid];
+	return logical_id == HYGON_NID_INVALID ? -ENODEV : logical_id;
+}
+EXPORT_SYMBOL_GPL(hygon_cpu_to_df_node);
+
+/*
+ * Walk PCI host-bridge devices matching the Hygon vendor.  The SMN
+ * index/data registers live in function 0 of each root complex (class
+ * PCI_CLASS_BRIDGE_HOST, devfn 0).
+ */
+static struct pci_dev *hygon_get_next_root(struct pci_dev *root)
+{
+	while ((root = pci_get_class(PCI_CLASS_BRIDGE_HOST << 8, root))) {
+		if (root->devfn)
+			continue;
+		if (root->vendor != PCI_VENDOR_ID_HYGON)
+			continue;
+		break;
+	}
+	return root;
+}
+
+/*
+ * Build and register the NB cache for amd_nb consumers (EDAC, MCE, etc.).
+ *
+ * Allocates a struct amd_northbridge[] array, fills in the misc (F3) and
+ * link (F4) PCI devices for each logical node, and hands it to
+ * amd_nb_set_cache(). On success the array is owned by amd_nb; on failure
+ * all PCI references are released.
+ */
+static int __init hygon_nb_cache_init(u16 num_nodes)
+{
+	struct amd_northbridge *nb;
+	u16 node;
+	int ret;
+
+	nb = kzalloc_objs(struct amd_northbridge, num_nodes);
+	if (!nb)
+		return -ENOMEM;
+
+	for (node = 0; node < num_nodes; node++) {
+		nb[node].misc = hygon_node_get_func(node, 3);
+		if (!nb[node].misc) {
+			ret = -ENODEV;
+			goto err_put;
+		}
+
+		nb[node].link = hygon_node_get_func(node, 4);
+		if (!nb[node].link) {
+			ret = -ENODEV;
+			pci_dev_put(nb[node].misc);
+			nb[node].misc = NULL;
+			goto err_put;
+		}
+	}
+
+	ret = amd_nb_set_cache(nb, num_nodes);
+	if (ret)
+		goto err_put;
+
+	return 0;
+
+err_put:
+	while (node--) {
+		pci_dev_put(nb[node].link);
+		pci_dev_put(nb[node].misc);
+	}
+	kfree(nb);
+	return ret;
+}
+
+/*
+ * Register the Hygon NB cache independently of SMN root setup.
+ *
+ * The NB cache exposes the per-node misc/link PCI devices and does not
+ * rely on SMN.  A successful NB registration therefore remains useful
+ * even if SMN root setup later fails.
+ *
+ * Both hygon_nb_init() and hygon_smn_init() depend only on
+ * hygon_build_cache(), so their relative fs_initcall order does not
+ * matter.
+ */
+static int __init hygon_nb_init(void)
+{
+	u16 num_nodes;
+
+	if (boot_cpu_data.x86_vendor != X86_VENDOR_HYGON ||
+	    boot_cpu_data.x86 != 0x18)
+		return 0;
+
+	num_nodes = hygon_node_num();
+	if (!num_nodes)
+		return -ENODEV;
+
+	return hygon_nb_cache_init(num_nodes);
+}
+fs_initcall(hygon_nb_init);
+
+static void hygon_release_root_regions(struct pci_dev **roots, u16 count)
+{
+	u16 i;
+
+	for (i = 0; i < count; i++)
+		pci_release_config_region(roots[i], 0, PCI_CFG_SPACE_SIZE);
+}
+
+/*
+ * Hygon SMN initialisation.
+ *
+ * On Hygon Fam18h, SMN root devices are shared per-socket: all nodes (CDD
+ * and IOD) on the same socket use the same host-bridge root for SMN access.
+ * These root devices are transport endpoints, not topology identifiers:
+ * socket_id / DFID come from DF registers, while the selected root only
+ * determines which socket-local SMN index/data pair is used by
+ * amd_smn_read() / amd_smn_write(). Any root within a socket-equivalent
+ * group is sufficient, but binding a node to a root from another socket
+ * would route the transaction through the wrong SMN ingress.
+ *
+ * We therefore collect one representative root per socket and expand that
+ * per-socket selection into a per-node array for smn_set_roots().
+ */
+static int __init hygon_smn_init(void)
+{
+	struct pci_dev *socket_roots[HYGON_MAX_SOCKETS] = { };
+	struct pci_dev **reserved_roots, **roots, *root;
+	u16 count, num_roots, roots_per_socket, node, num_nodes;
+	u16 num_sockets, reserved, socket, socket_id;
+	int ret;
+
+	if (boot_cpu_data.x86_vendor != X86_VENDOR_HYGON ||
+	    boot_cpu_data.x86 != 0x18)
+		return 0;
+
+	num_roots = 0;
+	root = NULL;
+	while ((root = hygon_get_next_root(root)))
+		num_roots++;
+
+	pr_debug("Found %u Hygon SMN root devices\n", num_roots);
+
+	if (!num_roots)
+		return -ENODEV;
+
+	/*
+	 * hygon_node_num() triggers hygon_build_cache(), which reads
+	 * F1x200 / F5x180 identity registers from every DF misc device
+	 * and validates the socket topology.
+	 */
+	num_nodes = hygon_node_num();
+	if (!num_nodes)
+		return -ENODEV;
+
+	num_sockets = hygon_socket_num();
+	if (!num_sockets)
+		return -ENODEV;
+
+	if (num_sockets > ARRAY_SIZE(socket_roots)) {
+		pr_err("Socket count %u exceeds maximum %zu\n",
+		       num_sockets, ARRAY_SIZE(socket_roots));
+		return -EINVAL;
+	}
+
+	if (num_roots % num_sockets) {
+		pr_err("Root count %u not divisible by socket count %u\n",
+		       num_roots, num_sockets);
+		return -ENODEV;
+	}
+
+	roots = kzalloc_objs(*roots, num_nodes);
+	if (!roots)
+		return -ENOMEM;
+
+	reserved_roots = kcalloc(num_roots, sizeof(*reserved_roots), GFP_KERNEL);
+	if (!reserved_roots) {
+		kfree(roots);
+		return -ENOMEM;
+	}
+
+	/*
+	 * Collect one representative root per socket and skip the rest.
+	 * Root devices within the same socket are redundant SMN ingress points.
+	 */
+	roots_per_socket = num_roots / num_sockets;
+	socket = 0;
+	reserved = 0;
+	count = 0;
+	root = NULL;
+	while ((root = hygon_get_next_root(root))) {
+		pci_dbg(root, "Reserving PCI config space\n");
+
+		/*
+		 * Reserve the entire PCI config space so user space cannot
+		 * race with SMN index/data register access.
+		 */
+		if (!pci_request_config_region_exclusive(root, 0,
+							 PCI_CFG_SPACE_SIZE,
+							 NULL)) {
+			pci_err(root, "Failed to reserve config space\n");
+			ret = -EEXIST;
+			goto err_release;
+		}
+
+		reserved_roots[reserved++] = root;
+
+		if (count++ % roots_per_socket)
+			continue;
+
+		if (socket >= num_sockets) {
+			ret = -ENODEV;
+			goto err_release;
+		}
+
+		pci_dbg(root, "is root for Hygon socket %u\n", socket);
+		socket_roots[socket++] = root;
+	}
+
+	if (socket != num_sockets) {
+		ret = -ENODEV;
+		goto err_release;
+	}
+
+	/* Expand socket roots to a per-node array for the SMN layer. */
+	for (node = 0; node < num_nodes; node++) {
+		socket_id = hygon_node_socket(node);
+
+		if (socket_id >= num_sockets) {
+			ret = -ENODEV;
+			goto err_release;
+		}
+
+		pci_dbg(socket_roots[socket_id],
+			"is root for Hygon node %u (socket %u)\n",
+			node, socket_id);
+		roots[node] = socket_roots[socket_id];
+	}
+
+	ret = smn_set_roots(roots, num_nodes);
+	if (ret)
+		goto err_release;
+
+	/* roots[] is now owned by the SMN layer; do not free on error below. */
+	ret = smn_activate("hygon_smn");
+	kfree(reserved_roots);
+	return ret;
+
+err_release:
+	hygon_release_root_regions(reserved_roots, reserved);
+	kfree(reserved_roots);
+	kfree(roots);
+	return ret;
+}
+fs_initcall(hygon_smn_init);
-- 
2.43.0


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration
  2026-04-23  6:04 ` [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration Lin Wang
@ 2026-04-23 19:12   ` Mario Limonciello
  2026-04-24  1:43     ` Lin Wang
  0 siblings, 1 reply; 8+ messages in thread
From: Mario Limonciello @ 2026-04-23 19:12 UTC (permalink / raw)
  To: Lin Wang, yazen.ghannam, Borislav Petkov; +Cc: tglx, mingo, x86, linux-kernel



On 4/23/26 01:04, Lin Wang wrote:
> Guard the AMD SMN init (amd_smn_init) to X86_VENDOR_AMD only, removing
> the PCI_VENDOR_ID_HYGON check from get_next_root() so that Hygon host
> bridges are no longer enumerated by the AMD path.
> 
> Split the original monolithic root registration into two explicit phases:
> 
>    smn_set_roots()  -- pure data registration: saves the per-node root
>                        array and node count, with no side effects.
> 
>    smn_activate()   -- enables the SMN access layer: creates the optional
>                        debugfs interface (gated by amd_smn_debugfs_enable)
>                        and sets smn_exclusive so that amd_smn_read/write()
>                        begin accepting requests.  Takes a debugfs_name
>                        parameter so each vendor can use its own directory
>                        name (e.g. "amd_smn", "hygon_smn").
> 
> This two-phase design keeps the API names honest (set_roots is a pure
> setter, activate is an explicit state transition) and lets each vendor
> control when the shared SMN layer becomes live.
> 
> Export both functions so that any vendor-specific init module can hand a
> pre-built per-node root array to the shared SMN layer.
> 
> No functional change for AMD systems.
> 
> Signed-off-by: Lin Wang <wanglin@open-hieco.net>
> ---
>   arch/x86/include/asm/amd/node.h |   4 ++
>   arch/x86/kernel/amd_node.c      | 106 +++++++++++++++++++++++++-------
>   2 files changed, 87 insertions(+), 23 deletions(-)
> 
> diff --git a/arch/x86/include/asm/amd/node.h b/arch/x86/include/asm/amd/node.h
> index a672b8765fa8..2705b4f59361 100644
> --- a/arch/x86/include/asm/amd/node.h
> +++ b/arch/x86/include/asm/amd/node.h
> @@ -32,12 +32,16 @@ static inline u16 amd_num_nodes(void)
>   #ifdef CONFIG_AMD_NODE
>   int __must_check amd_smn_read(u16 node, u32 address, u32 *value);
>   int __must_check amd_smn_write(u16 node, u32 address, u32 value);
> +int smn_set_roots(struct pci_dev **roots, u16 num_nodes);
> +int smn_activate(const char *debugfs_name);
>   
>   /* Should only be used by the HSMP driver. */
>   int __must_check amd_smn_hsmp_rdwr(u16 node, u32 address, u32 *value, bool write);
>   #else
>   static inline int __must_check amd_smn_read(u16 node, u32 address, u32 *value) { return -ENODEV; }
>   static inline int __must_check amd_smn_write(u16 node, u32 address, u32 value) { return -ENODEV; }
> +static inline int smn_set_roots(struct pci_dev **roots, u16 num_nodes) { return -ENODEV; }
> +static inline int smn_activate(const char *debugfs_name) { return -ENODEV; }
>   
>   static inline int __must_check amd_smn_hsmp_rdwr(u16 node, u32 address, u32 *value, bool write)
>   {
> diff --git a/arch/x86/kernel/amd_node.c b/arch/x86/kernel/amd_node.c
> index 0be01725a2a4..76dbf847e567 100644
> --- a/arch/x86/kernel/amd_node.c
> +++ b/arch/x86/kernel/amd_node.c
> @@ -9,6 +9,7 @@
>    */
>   
>   #include <linux/debugfs.h>
> +#include <asm/processor.h>
>   #include <asm/amd/node.h>
>   
>   /*
> @@ -34,7 +35,8 @@ struct pci_dev *amd_node_get_func(u16 node, u8 func)
>   	return pci_get_domain_bus_and_slot(0, 0, PCI_DEVFN(AMD_NODE0_PCI_SLOT + node, func));
>   }
>   
> -static struct pci_dev **amd_roots;
> +static struct pci_dev **smn_roots;
> +static u16 smn_num_nodes;
>   
>   /* Protect the PCI config register pairs used for SMN. */
>   static DEFINE_MUTEX(smn_mutex);
> @@ -88,10 +90,10 @@ static int __amd_smn_rw(u8 i_off, u8 d_off, u16 node, u32 address, u32 *value, b
>   	struct pci_dev *root;
>   	int err = -ENODEV;
>   
> -	if (node >= amd_num_nodes())
> +	if (node >= smn_num_nodes)
>   		return err;
>   
> -	root = amd_roots[node];
> +	root = smn_roots[node];
>   	if (!root)
>   		return err;
>   
> @@ -151,7 +153,7 @@ static ssize_t smn_node_write(struct file *file, const char __user *userbuf,
>   	if (ret)
>   		return ret;
>   
> -	if (node >= amd_num_nodes())
> +	if (node >= smn_num_nodes)
>   		return -ENODEV;
>   
>   	debug_node = node;
> @@ -225,8 +227,7 @@ static struct pci_dev *get_next_root(struct pci_dev *root)
>   		if (root->devfn)
>   			continue;
>   
> -		if (root->vendor != PCI_VENDOR_ID_AMD &&
> -		    root->vendor != PCI_VENDOR_ID_HYGON)
> +		if (root->vendor != PCI_VENDOR_ID_AMD)
>   			continue;
>   
>   		break;
> @@ -244,17 +245,78 @@ static int __init amd_smn_enable_dfs(char *str)
>   }
>   __setup("amd_smn_debugfs_enable", amd_smn_enable_dfs);
>   
> +/*
> + * Register a pre-built per-node SMN root array.  The caller allocates
> + * @roots and transfers ownership to the SMN layer; the array must remain
> + * valid for the lifetime of the system.
> + *
> + * This is a pure data registration; the SMN access layer is not yet
> + * usable until smn_activate() is called.
> + *
> + * Not concurrency-safe.  Intended to be called exactly once during boot
> + * (e.g. from an fs_initcall).  The caller is responsible for any
> + * serialization required by its own init path.
> + */
> +int smn_set_roots(struct pci_dev **roots, u16 num_nodes)
> +{
> +	if (!roots || !num_nodes)
> +		return -EINVAL;
> +
> +	if (smn_roots)
> +		return -EBUSY;
> +
> +	smn_roots = roots;
> +	smn_num_nodes = num_nodes;
> +
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(smn_set_roots);
> +
> +/*
> + * Activate the SMN access layer after roots have been registered via
> + * smn_set_roots().  Creates the optional debugfs interface (gated by
> + * the amd_smn_debugfs_enable boot parameter) and sets smn_exclusive so
> + * that amd_smn_read()/amd_smn_write() begin accepting requests.
> + *
> + * @debugfs_name: directory name under arch_debugfs_dir (e.g. "amd_smn")
> + *
> + * Same serialization rules as smn_set_roots(): must be called exactly
> + * once, at boot, after smn_set_roots() succeeds.
> + */
> +int smn_activate(const char *debugfs_name)
> +{
> +	if (!smn_roots || !smn_num_nodes)
> +		return -EINVAL;

You don't need the !smn_num_nodes check here.  smn_set_roots() already 
has it.

> +
> +	if (smn_exclusive)
> +		return -EBUSY;
> +
> +	if (enable_dfs) {
> +		debugfs_dir = debugfs_create_dir(debugfs_name, arch_debugfs_dir);
> +
> +		debugfs_create_file("node",	0600, debugfs_dir, NULL, &smn_node_fops);
> +		debugfs_create_file("address",	0600, debugfs_dir, NULL, &smn_address_fops);
> +		debugfs_create_file("value",	0600, debugfs_dir, NULL, &smn_value_fops);
> +	}
> +
> +	smn_exclusive = true;
> +	return 0;
> +}
> +EXPORT_SYMBOL_GPL(smn_activate);
> +
>   static int __init amd_smn_init(void)
>   {
>   	u16 count, num_roots, roots_per_node, node, num_nodes;
> -	struct pci_dev *root;
> +	struct pci_dev **roots, *root;
> +	int ret;
>   
> -	if (!cpu_feature_enabled(X86_FEATURE_ZEN))
> +	if (!cpu_feature_enabled(X86_FEATURE_ZEN) ||
> +	    boot_cpu_data.x86_vendor != X86_VENDOR_AMD)
>   		return 0;
>   
>   	guard(mutex)(&smn_mutex);
>   
> -	if (amd_roots)
> +	if (smn_roots)
>   		return 0;
>   
>   	num_roots = 0;
> @@ -268,7 +330,9 @@ static int __init amd_smn_init(void)
>   		 * entire PCI config space for simplicity rather than covering
>   		 * specific registers piecemeal.
>   		 */
> -		if (!pci_request_config_region_exclusive(root, 0, PCI_CFG_SPACE_SIZE, NULL)) {
> +		if (!pci_request_config_region_exclusive(root, 0,
> +							 PCI_CFG_SPACE_SIZE,
> +							 NULL)) {
>   			pci_err(root, "Failed to reserve config space\n");
>   			return -EEXIST;
>   		}
> @@ -276,14 +340,14 @@ static int __init amd_smn_init(void)
>   		num_roots++;
>   	}
>   
> -	pr_debug("Found %d AMD root devices\n", num_roots);
> +	pr_debug("Found %d SMN root devices\n", num_roots);
>   
>   	if (!num_roots)
>   		return -ENODEV;
>   
>   	num_nodes = amd_num_nodes();
> -	amd_roots = kzalloc_objs(*amd_roots, num_nodes);
> -	if (!amd_roots)
> +	roots = kzalloc_objs(*roots, num_nodes);
> +	if (!roots)
>   		return -ENOMEM;
>   
>   	roots_per_node = num_roots / num_nodes;
> @@ -297,20 +361,16 @@ static int __init amd_smn_init(void)
>   			continue;
>   
>   		pci_dbg(root, "is root for AMD node %u\n", node);
> -		amd_roots[node++] = root;
> +		roots[node++] = root;
>   	}
>   
> -	if (enable_dfs) {
> -		debugfs_dir = debugfs_create_dir("amd_smn", arch_debugfs_dir);
> -
> -		debugfs_create_file("node",	0600, debugfs_dir, NULL, &smn_node_fops);
> -		debugfs_create_file("address",	0600, debugfs_dir, NULL, &smn_address_fops);
> -		debugfs_create_file("value",	0600, debugfs_dir, NULL, &smn_value_fops);
> +	ret = smn_set_roots(roots, num_nodes);
> +	if (ret) {
> +		kfree(roots);
> +		return ret;
>   	}
>   
> -	smn_exclusive = true;
> -
> -	return 0;
> +	return smn_activate("amd_smn");
>   }
>   
>   fs_initcall(amd_smn_init);


^ permalink raw reply	[flat|nested] 8+ messages in thread

* Re: [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration
  2026-04-23 19:12   ` Mario Limonciello
@ 2026-04-24  1:43     ` Lin Wang
  0 siblings, 0 replies; 8+ messages in thread
From: Lin Wang @ 2026-04-24  1:43 UTC (permalink / raw)
  To: Mario Limonciello, yazen.ghannam, Borislav Petkov
  Cc: tglx, mingo, x86, linux-kernel

Hi Mario,

You're right. After a successful smn_set_roots(), smn_num_nodes is 
guaranteed to be non-zero because smn_set_roots() already rejects 
(!roots || !num_nodes). So checking !smn_roots here is sufficient.

Will fix in v3.

Thanks & Br,

Lin

On 4/24/2026 3:12 AM, Mario Limonciello wrote:
>> +/*
>> + * Activate the SMN access layer after roots have been registered via
>> + * smn_set_roots().  Creates the optional debugfs interface (gated by
>> + * the amd_smn_debugfs_enable boot parameter) and sets smn_exclusive so
>> + * that amd_smn_read()/amd_smn_write() begin accepting requests.
>> + *
>> + * @debugfs_name: directory name under arch_debugfs_dir (e.g. "amd_smn")
>> + *
>> + * Same serialization rules as smn_set_roots(): must be called exactly
>> + * once, at boot, after smn_set_roots() succeeds.
>> + */
>> +int smn_activate(const char *debugfs_name)
>> +{
>> +    if (!smn_roots || !smn_num_nodes)
>> +        return -EINVAL;
> 
> You don't need the !smn_num_nodes check here.  smn_set_roots() already 
> has it.


^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-04-24  1:43 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-04-23  6:04 [RFC PATCH v2 0/5] x86/hygon: Add Family 0x18 node enumeration and SMN access Lin Wang
2026-04-23  6:04 ` [RFC PATCH v2 1/5] pci_ids: Add Hygon Family 0x18 DF device IDs Lin Wang
2026-04-23  6:04 ` [RFC PATCH v2 2/5] x86/hygon: Add Family 0x18 node enumeration API header Lin Wang
2026-04-23  6:04 ` [RFC PATCH v2 3/5] x86/amd_nb: Add amd_nb_set_cache() for external NB cache registration Lin Wang
2026-04-23  6:04 ` [RFC PATCH v2 4/5] x86/amd_node: Add smn_set_roots() and smn_activate() for external SMN registration Lin Wang
2026-04-23 19:12   ` Mario Limonciello
2026-04-24  1:43     ` Lin Wang
2026-04-23  6:04 ` [RFC PATCH v2 5/5] x86/hygon: Add Fam18h node enumeration, NB cache and SMN init Lin Wang

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

all inboxes | Powered by JetHome®