[RFC PATCH 00/05] x86/hygon: Add Family 0x18 node enumeration and SMN access

From: Lin Wang

Date: Thu Apr 02 2026 - 07:24:12 EST


== 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. The Hygon vendor ID is already recognized
in amd_node.c (get_next_root()) and early_is_amd_nb(), but the mainline
kernel currently lacks support for Hygon Data Fabric node enumeration.

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 by building on the existing AMD
node framework through a set of Hygon-specific hooks, rather than forking
the code paths. The goal is to keep changes to existing AMD code minimal
and self-contained.

== Overview ==

This series adds the missing 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. The fundamental challenge, however, is not
merely compacting a sparse range -- it is establishing a reliable
mapping between two independent hardware domains: the DF register
world (socket_id and DFID from PCI config space) and the CPU topology
world (phys_node_id from CPUID). Neither domain references the other
directly. A correct mapping must be grounded in hardware-fixed
properties, not software enumeration order. EDAC, MCE decode, and
address translation all depend on this correspondence.

== Solution ==

hygon_node.c is a Hygon-specific DF enumeration module that reads
hardware identity directly from each DF instance, builds a correctly
ordered node array, and provides the CPU-to-node mapping as an O(1)
lookup. It integrates with the existing AMD subsystems through a narrow
set of hooks rather than forking the code paths.

== 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 |
| |
| [0] sock=0 dfid=4 logical 0 -> amd_nb[0] |
| [1] sock=0 dfid=5 logical 1 -> amd_nb[1] |
| ... |
| [7] sock=1 dfid=7 logical 7 -> amd_nb[7] |
+----------------------------------------------------------+
| IOD region: indices num_cdd .. num_nodes-1 |
| socket_id used for SMN root assignment only |
| |
| [8] sock=0 dfid=0 IOD -> amd_roots[8] |
| [9] sock=1 dfid=0 IOD -> amd_roots[9] |
+----------------------------------------------------------+

IOD nodes must be present because amd_roots[] is indexed by the full
node count (CDD + IOD), and SMN access via amd_smn_read() indexes
amd_roots[] directly by node number.

=== 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.

Since sorted_nodes[] CDD and sorted_nids[] both follow the same physical
ordering, nids[i] and nodes[i] correspond to the same physical die. The
result is a stable, hardware-anchored bijection that any consumer can
use by calling hygon_cpu_to_logical_node(cpu).

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

index: 0 1 2 3 4 ... 15 16 17 18 19 20 ... 255
+---+---+---+---+---+ +---+---+---+---+---+---+ +---+
value: | 0 | 1 | 2 | 3 |FF | .. |FF | 4 | 5 | 6 | 7 |FF | .. |FF |
+---+---+---+---+---+ +---+---+---+---+---+---+ +---+
^ ^ ^ ^ ^ ^ ^ ^
sock0 logical 0-3 sock1 logical 4-7

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.

=== SMN Access ===

SMN root devices (PCI class 0x0600, vendor 0x1d94) are already
enumerated and reserved by amd_smn_init() in amd_node.c -- the existing
get_next_root() already matches the Hygon vendor ID. The only difference
from AMD is root-to-node assignment:

AMD: one root per node (roots_per_node grouping)
Hygon: one root per socket, shared across all nodes on that socket
(roots_per_socket grouping, expanded via hygon_node_socket())

Three internal functions hook into amd_node.c (not exported; used only
by built-in x86 code):
hygon_node_num() -- total node count, sizes amd_nb[] and amd_roots[]
hygon_socket_num() -- socket count, for roots_per_socket
hygon_node_socket(n) -- socket ID for node n, for amd_roots[] fill

Two further internal functions hook into amd_nb.c:
hygon_node_get_func(n, 3/4) -- PCI dev for DF misc/link of node n

== 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_logical_node(cpu) logical node ID (0..N-1), or -errno

Inline helpers in the header (outside CONFIG_AMD_NODE guard):
hygon_f18h_m4h() -- model in 0x04..0x0f
hygon_f18h_m10h() -- model >= 0x10

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

== Series ==

[1/5] pci_ids: Add Hygon Family 0x18 DF device IDs
PCI device IDs for DF function 1, 3, 4, and 5 siblings 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_AMD_NODE=n stubs, and
inline model-check helpers.

[3/5] x86/hygon: Add DF misc-based node enumeration helpers
Core implementation: three-phase cache build, hardware-anchored
CPU-to-node mapping, and all public/internal API functions.

[4/5] x86/amd_nb: Use Hygon DF misc enumeration for Family 0x18
amd_cache_northbridges() uses hygon_node_num() and
hygon_node_get_func() on Hygon systems to size and populate
amd_northbridges[].

[5/5] x86/amd_node: Support Hygon SMN roots by socket
amd_smn_init() gains a Hygon branch: per-socket root grouping
and per-node expansion via hygon_node_socket(). AMD behavior is
unchanged.

== 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 three-phase enumeration approach the right structure, or
should Hygon node discovery be folded into amd_node.c more directly?

- 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 EDAC/MCE/ATL consumers?

- Is the per-socket SMN root assignment by PCI enumeration order safe
to rely on, given that root devices carry no hardware socket ID?

- This series intentionally minimises changes to existing AMD code
(amd_nb.c, amd_node.c), preferring narrow hooks over deeper
integration. If the community sees a cleaner or more maintainable
approach to integrating Hygon DF support into the AMD node framework,
suggestions are welcome.

Lin Wang (5):
pci_ids: Add Hygon Family 0x18 DF device IDs
x86/hygon: Add Family 0x18 node enumeration API header
x86/hygon: Add DF misc-based node enumeration helpers
x86/amd_nb: Use Hygon DF misc enumeration for Family 0x18
x86/amd_node: Support Hygon SMN roots by socket

MAINTAINERS | 2 +
arch/x86/include/asm/hygon/node.h | 148 ++++++
arch/x86/kernel/Makefile | 2 +-
arch/x86/kernel/amd_nb.c | 17 +-
arch/x86/kernel/amd_node.c | 122 ++++-
arch/x86/kernel/hygon_node.c | 721 ++++++++++++++++++++++++++++++
include/linux/pci_ids.h | 4 +
7 files changed, 997 insertions(+), 19 deletions(-)
create mode 100644 arch/x86/include/asm/hygon/node.h
create mode 100644 arch/x86/kernel/hygon_node.c

Link: https://lore.kernel.org/all/20241206161210.163701-1-yazen.ghannam@xxxxxxx/ # [1]
Link: https://lore.kernel.org/all/20250107222847.3300430-1-yazen.ghannam@xxxxxxx/ # [2]
--
2.43.0