[PATCH v9 7/7] x86/hygon: Add Family 0x18 SMN access
From: Lin Wang
Date: Thu Oct 29 2026 - 01:35:25 EST
All DF nodes in a Hygon socket share one SMN ingress, so SMN accesses
for a node have to go through a root of the node's socket rather than a
root per node.
After DF enumeration, map each DF node to the first root of its socket.
The roots do not expose their socket IDs, so group them by socket in
PCI enumeration order. Each DF node holds its own reference to its
socket's root. Keep config space reserved on all roots for the lifetime
of the node layer and free the temporary root table after setup.
Serialize each index/data transaction. A setup failure releases all
acquired resources without invalidating the DF node cache.
Hygon drivers such as EDAC need SMN access. Keep the SMN read and write
helpers local to hygon_node.c until such drivers are added.
Signed-off-by: Lin Wang <wanglin@xxxxxxxxxxxxxx>
---
arch/x86/kernel/hygon_node.c | 200 ++++++++++++++++++++++++++++++++++-
1 file changed, 199 insertions(+), 1 deletion(-)
diff --git a/arch/x86/kernel/hygon_node.c b/arch/x86/kernel/hygon_node.c
index 03c723aab97e..5dc766197d4e 100644
--- a/arch/x86/kernel/hygon_node.c
+++ b/arch/x86/kernel/hygon_node.c
@@ -1,6 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
- * Hygon Family 0x18 Data Fabric node enumeration
+ * Hygon Family 0x18 Data Fabric node enumeration and SMN access
*
* A DF instance exposes sibling PCI functions in one slot. Function 3
* is the misc device used for enumeration, function 4 is the link
@@ -17,8 +17,10 @@
#include <linux/bitfield.h>
#include <linux/bitops.h>
+#include <linux/cleanup.h>
#include <linux/cpufeature.h>
#include <linux/init.h>
+#include <linux/mutex.h>
#include <linux/pci.h>
#include <linux/pci_ids.h>
#include <linux/processor.h>
@@ -77,6 +79,7 @@
struct df_node {
struct pci_dev *misc;
struct pci_dev *link;
+ struct pci_dev *smn_root;
u8 socket_id;
u8 dfid;
bool is_cdd;
@@ -629,6 +632,197 @@ static int __maybe_unused hygon_pci_dev_to_df_node(struct pci_dev *pdev)
return -ENODEV;
}
+/*
+ * SMN index/data register pair offsets in the host-bridge PCI config
+ * space. Reads and writes to a (node, address) pair are issued as a
+ * two-step transaction: write the SMN address to the index register,
+ * then read or write the value at the data register.
+ */
+#define SMN_INDEX_OFFSET 0x60
+#define SMN_DATA_OFFSET 0x64
+
+/* Protect the SMN index/data transaction. */
+static DEFINE_MUTEX(smn_mutex);
+
+/*
+ * Walk PCI host-bridge devices matching the Hygon vendor. The SMN
+ * index/data registers live in function 0 of each root complex. This
+ * follows pci_get_class() iterator semantics: @root is consumed and the
+ * returned device has an iterator reference. A retained device needs an
+ * independent reference before the iterator advances.
+ */
+static struct pci_dev * __init 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;
+}
+
+/*
+ * Select one root from each contiguous per-socket enumeration group,
+ * then map every DF node to the root for its socket. The roots do not
+ * expose their socket IDs, so the grouping follows PCI enumeration order.
+ * DF enumeration guarantees dense socket IDs starting at zero.
+ *
+ * Cached roots own independent PCI references. Keep the config regions
+ * reserved after setup; release them only when setup fails.
+ */
+static int __init smn_setup(void)
+{
+ u16 num_roots, roots_per_socket, node, num_nodes, num_sockets, reserved;
+ struct pci_dev **reserved_roots;
+ struct pci_dev *root = NULL;
+ int ret;
+
+ num_roots = 0;
+ while ((root = get_next_root(root)))
+ num_roots++;
+
+ pr_debug("Found %u Hygon SMN root devices\n", num_roots);
+
+ if (!num_roots)
+ return -ENODEV;
+
+ num_nodes = df_cache.num_nodes;
+ if (!num_nodes)
+ return -ENODEV;
+
+ num_sockets = df_cache.num_sockets;
+ if (!num_sockets)
+ return -ENODEV;
+
+ if (num_roots % num_sockets) {
+ pr_err("Root count %u not divisible by socket count %u\n",
+ num_roots, num_sockets);
+ return -ENODEV;
+ }
+
+ reserved_roots = kzalloc_objs(*reserved_roots, num_roots);
+ if (!reserved_roots)
+ return -ENOMEM;
+
+ /* Reserve every root before publishing the node-to-root mapping. */
+ roots_per_socket = num_roots / num_sockets;
+ reserved = 0;
+ while ((root = get_next_root(root))) {
+ if (reserved >= num_roots) {
+ ret = -ENODEV;
+ goto out;
+ }
+
+ pci_dbg(root, "Reserving PCI config space\n");
+
+ /*
+ * Mark the entire PCI config space kernel-exclusive because it
+ * contains the SMN index/data registers.
+ */
+ 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 out;
+ }
+
+ reserved_roots[reserved++] = pci_dev_get(root);
+ }
+
+ if (reserved != num_roots) {
+ pr_err("Root enumeration changed: expected %u roots, got %u\n",
+ num_roots, reserved);
+ ret = -ENODEV;
+ goto out;
+ }
+
+ for (node = 0; node < num_nodes; node++) {
+ struct df_node *n = &df_cache.nodes[node];
+
+ n->smn_root = pci_dev_get(reserved_roots[n->socket_id * roots_per_socket]);
+
+ pci_dbg(n->smn_root,
+ "is root for Hygon node %u (socket %u)\n",
+ node, n->socket_id);
+ }
+
+ ret = 0;
+
+out:
+ pci_dev_put(root);
+ while (reserved) {
+ root = reserved_roots[--reserved];
+ if (ret)
+ pci_release_config_region(root, 0, PCI_CFG_SPACE_SIZE);
+ pci_dev_put(root);
+ }
+ kfree(reserved_roots);
+ return ret;
+}
+
+/*
+ * Serialize the PCI index/data pair between in-kernel SMN users.
+ * The access sequence follows amd_smn_read() and amd_smn_write().
+ * The Hygon node-to-root mapping comes from smn_setup().
+ */
+static int smn_rw(u16 node, u32 address, u32 *value, bool write)
+{
+ struct pci_dev *root;
+ int err;
+
+ if (!df_cache.nodes || node >= df_cache.num_nodes)
+ return -ENODEV;
+
+ root = df_cache.nodes[node].smn_root;
+ if (!root)
+ return -ENODEV;
+
+ guard(mutex)(&smn_mutex);
+
+ err = pci_write_config_dword(root, SMN_INDEX_OFFSET, address);
+ if (err) {
+ pr_warn("SMN index write failed (addr 0x%x)\n", address);
+ return pcibios_err_to_errno(err);
+ }
+
+ err = write ? pci_write_config_dword(root, SMN_DATA_OFFSET, *value)
+ : pci_read_config_dword(root, SMN_DATA_OFFSET, value);
+
+ return pcibios_err_to_errno(err);
+}
+
+static int __must_check __maybe_unused hygon_smn_read(u16 node, u32 address, u32 *value)
+{
+ int err;
+
+ if (!value)
+ return -EINVAL;
+
+ err = smn_rw(node, address, value, false);
+
+ /* Clear the output so callers do not consume a stale value. */
+ if (err) {
+ *value = 0;
+ return err;
+ }
+
+ /* Treat the PCI all-ones value as a missing device. */
+ if (PCI_POSSIBLE_ERROR(*value)) {
+ *value = 0;
+ return -ENODEV;
+ }
+
+ return 0;
+}
+
+static int __must_check __maybe_unused hygon_smn_write(u16 node, u32 address, u32 value)
+{
+ return smn_rw(node, address, &value, true);
+}
+
static int __init df_node_init(void)
{
int ret;
@@ -648,6 +842,10 @@ static int __init df_node_init(void)
df_cache.num_nodes - df_cache.num_cdd,
df_cache.num_sockets);
+ ret = smn_setup();
+ if (ret)
+ pr_warn("SMN setup failed: %d\n", ret);
+
return 0;
}
fs_initcall(df_node_init);
--
2.43.0