@@ -0,0 +1,464 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * ACPI RAS2 memory driver
+ *
+ * Copyright (c) 2024-2025 HiSilicon Limited.
+ *
+ */
+
+#define pr_fmt(fmt) "ACPI RAS2 MEMORY: " fmt
+
+#include <linux/bitfield.h>
+#include <linux/edac.h>
+#include <linux/platform_device.h>
+#include <acpi/ras2.h>
+
+#define RAS2_SUPPORT_HW_PARTOL_SCRUB BIT(0)
+#define RAS2_TYPE_PATROL_SCRUB 0x0000
+
+#define RAS2_GET_PATROL_PARAMETERS 0x01
+#define RAS2_START_PATROL_SCRUBBER 0x02
+#define RAS2_STOP_PATROL_SCRUBBER 0x03
+
+/*
+ * RAS2 patrol scrub
+ */
+#define RAS2_PS_SC_HRS_IN_MASK GENMASK(15, 8)
+#define RAS2_PS_EN_BACKGROUND BIT(0)
+#define RAS2_PS_SC_HRS_OUT_MASK GENMASK(7, 0)
+#define RAS2_PS_MIN_SC_HRS_OUT_MASK GENMASK(15, 8)
+#define RAS2_PS_MAX_SC_HRS_OUT_MASK GENMASK(23, 16)
+#define RAS2_PS_FLAG_SCRUB_RUNNING BIT(0)
+
+#define RAS2_SCRUB_NAME_LEN 128
+#define RAS2_HOUR_IN_SECS 3600
+
+#define TO_RAS2_MEM_CTX(_pxm_domain, inst) \
+ ((struct ras2_mem_ctx *)((u64)(_pxm_domain) - \
+ ((u64)((inst) * (u64)sizeof(struct ras2_pxm_domain)) \
+ + (u64)sizeof(struct ras2_mem_ctx_hdr))))
+
+enum ras2_od_scrub_status {
+ OD_SCRUB_STS_IDLE,
+ OD_SCRUB_STS_INIT,
+ OD_SCRUB_STS_ACTIVE,
+};
+
+struct acpi_ras2_ps_shared_mem {
+ struct acpi_ras2_shmem common;
+ struct acpi_ras2_patrol_scrub_param params;
+};
+
+#define TO_ACPI_RAS2_PS_SHMEM(_addr) \
+ container_of(_addr, struct acpi_ras2_ps_shared_mem, common)
+
+static int ras2_is_patrol_scrub_support(struct ras2_mem_ctx_hdr *ras2_ctx_hdr)
+{
+ struct acpi_ras2_shmem __iomem *common = (void *)ras2_ctx_hdr->comm_addr;
+
+ guard(mutex)(ras2_ctx_hdr->pcc_lock);
+ common->set_caps[0] = 0;
+
+ return common->features[0] & RAS2_SUPPORT_HW_PARTOL_SCRUB;
+}
+
+static int ras2_update_patrol_scrub_params_cache(struct ras2_mem_ctx *ras2_ctx,
+ u16 pxm_inst)
+{
+ struct ras2_pxm_domain *pxm_domain = &ras2_ctx->pxm_domains[pxm_inst];
+ struct acpi_ras2_ps_shared_mem __iomem *ps_sm =
+ TO_ACPI_RAS2_PS_SHMEM(ras2_ctx->hdr.comm_addr);
+ int ret;
+
+ ps_sm->common.set_caps[0] = RAS2_SUPPORT_HW_PARTOL_SCRUB;
+ ps_sm->params.command = RAS2_GET_PATROL_PARAMETERS;
+ ps_sm->params.req_addr_range[0] = pxm_domain->mem_base_addr;
+ ps_sm->params.req_addr_range[1] = pxm_domain->mem_size;
+ ret = ras2_send_pcc_cmd(ras2_ctx, PCC_CMD_EXEC_RAS2);
+ if (ret) {
+ dev_err(ras2_ctx->hdr.dev, "failed to read parameters\n");
+ return ret;
+ }
+
+ pxm_domain->min_scrub_cycle = FIELD_GET(RAS2_PS_MIN_SC_HRS_OUT_MASK,
+ ps_sm->params.scrub_params_out);
+ pxm_domain->max_scrub_cycle = FIELD_GET(RAS2_PS_MAX_SC_HRS_OUT_MASK,
+ ps_sm->params.scrub_params_out);
+ pxm_domain->scrub_cycle_hrs = FIELD_GET(RAS2_PS_SC_HRS_OUT_MASK,
+ ps_sm->params.scrub_params_out);
+ if (pxm_domain->bg_scrub) {
+ pxm_domain->base = 0;
+ pxm_domain->size = 0;
+ pxm_domain->od_scrub_sts = OD_SCRUB_STS_IDLE;
+ return 0;
+ }
+
+ if (ps_sm->params.flags & RAS2_PS_FLAG_SCRUB_RUNNING) {
+ pxm_domain->base = ps_sm->params.actl_addr_range[0];
+ pxm_domain->size = ps_sm->params.actl_addr_range[1];
+ } else if (pxm_domain->od_scrub_sts != OD_SCRUB_STS_INIT) {
+ /*
+ * When demand scrubbing is finished driver resets actual
+ * address range to 0 when readback. Otherwise userspace
+ * assumes demand scrubbing is in progress.
+ */
+ pxm_domain->base = 0;
+ pxm_domain->size = 0;
+ pxm_domain->od_scrub_sts = OD_SCRUB_STS_IDLE;
+ }
+
+ return 0;
+}
+
+/* Context - PCC lock must be held */
+static int ras2_get_patrol_scrub_running(struct ras2_mem_ctx *ras2_ctx,
+ u16 pxm_inst, bool *running)
+{
+ struct ras2_pxm_domain *pxm_domain = &ras2_ctx->pxm_domains[pxm_inst];
+ struct acpi_ras2_ps_shared_mem __iomem *ps_sm =
+ TO_ACPI_RAS2_PS_SHMEM(ras2_ctx->hdr.comm_addr);
+ int ret;
+
+ ps_sm->common.set_caps[0] = RAS2_SUPPORT_HW_PARTOL_SCRUB;
+ ps_sm->params.command = RAS2_GET_PATROL_PARAMETERS;
+ ps_sm->params.req_addr_range[0] = pxm_domain->mem_base_addr;
+ ps_sm->params.req_addr_range[1] = pxm_domain->mem_size;
+
+ ret = ras2_send_pcc_cmd(ras2_ctx, PCC_CMD_EXEC_RAS2);
+ if (ret) {
+ dev_err(ras2_ctx->hdr.dev, "failed to read parameters\n");
+ return ret;
+ }
+
+ *running = ps_sm->params.flags & RAS2_PS_FLAG_SCRUB_RUNNING;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_read_min_scrub_cycle(struct device *dev, void *drv_data,
+ u32 *min)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+
+ *min = pxm_domain->min_scrub_cycle * RAS2_HOUR_IN_SECS;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_read_max_scrub_cycle(struct device *dev, void *drv_data,
+ u32 *max)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+
+ *max = pxm_domain->max_scrub_cycle * RAS2_HOUR_IN_SECS;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_cycle_read(struct device *dev, void *drv_data,
+ u32 *scrub_cycle_secs)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+
+ *scrub_cycle_secs = pxm_domain->scrub_cycle_hrs * RAS2_HOUR_IN_SECS;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_cycle_write(struct device *dev, void *drv_data,
+ u32 scrub_cycle_secs)
+{
+ u8 scrub_cycle_hrs = scrub_cycle_secs / RAS2_HOUR_IN_SECS;
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+ struct ras2_mem_ctx *ras2_ctx = TO_RAS2_MEM_CTX(pxm_domain,
+ pxm_domain->feat_instance);
+ bool running;
+ int ret;
+
+ guard(mutex)(ras2_ctx->hdr.pcc_lock);
+ ret = ras2_get_patrol_scrub_running(ras2_ctx, pxm_domain->feat_instance,
+ &running);
+ if (ret)
+ return ret;
+
+ if (running)
+ return -EBUSY;
+
+ if (scrub_cycle_hrs < pxm_domain->min_scrub_cycle ||
+ scrub_cycle_hrs > pxm_domain->max_scrub_cycle)
+ return -EINVAL;
+
+ pxm_domain->scrub_cycle_hrs = scrub_cycle_hrs;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_read_addr(struct device *dev, void *drv_data, u64 *base)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+ struct ras2_mem_ctx *ras2_ctx = TO_RAS2_MEM_CTX(pxm_domain,
+ pxm_domain->feat_instance);
+ int ret;
+
+ /*
+ * When BG scrubbing is enabled the actual address range is not valid.
+ * Return -EBUSY now unless find out a method to retrieve actual
+ * full PA range.
+ */
+ if (pxm_domain->bg_scrub)
+ return -EBUSY;
+
+ ret = ras2_update_patrol_scrub_params_cache(ras2_ctx,
+ pxm_domain->feat_instance);
+ if (ret)
+ return ret;
+
+ *base = pxm_domain->base;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_read_size(struct device *dev, void *drv_data, u64 *size)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+ struct ras2_mem_ctx *ras2_ctx = TO_RAS2_MEM_CTX(pxm_domain,
+ pxm_domain->feat_instance);
+ int ret;
+
+ if (pxm_domain->bg_scrub)
+ return -EBUSY;
+
+ ret = ras2_update_patrol_scrub_params_cache(ras2_ctx,
+ pxm_domain->feat_instance);
+ if (ret)
+ return ret;
+
+ *size = pxm_domain->size;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_write_addr(struct device *dev, void *drv_data, u64 base)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+ struct ras2_mem_ctx *ras2_ctx = TO_RAS2_MEM_CTX(pxm_domain,
+ pxm_domain->feat_instance);
+ struct ras2_mem_ctx_hdr *ras2_ctx_hdr = &ras2_ctx->hdr;
+ struct acpi_ras2_ps_shared_mem __iomem *ps_sm =
+ TO_ACPI_RAS2_PS_SHMEM(ras2_ctx_hdr->comm_addr);
+ bool running;
+ int ret;
+
+ if (pxm_domain->bg_scrub)
+ return -EBUSY;
+
+ guard(mutex)(ras2_ctx_hdr->pcc_lock);
+ ps_sm->common.set_caps[0] = RAS2_SUPPORT_HW_PARTOL_SCRUB;
+
+ if (!pxm_domain->size || pxm_domain->size > pxm_domain->mem_size ||
+ base < pxm_domain->mem_base_addr ||
+ (base + pxm_domain->size) >
+ (pxm_domain->mem_base_addr + pxm_domain->mem_size)) {
+ dev_warn(dev,
+ "%s: Invalid address range, base=0x%llx size=0x%llx\n",
+ __func__, base, pxm_domain->size);
+ pxm_domain->size = 0;
+ return -ERANGE;
+ }
+
+ ret = ras2_get_patrol_scrub_running(ras2_ctx, pxm_domain->feat_instance,
+ &running);
+ if (ret)
+ return ret;
+
+ if (running)
+ return -EBUSY;
+
+ pxm_domain->base = base;
+ ps_sm->params.scrub_params_in &= ~RAS2_PS_SC_HRS_IN_MASK;
+ ps_sm->params.scrub_params_in |= FIELD_PREP(RAS2_PS_SC_HRS_IN_MASK,
+ pxm_domain->scrub_cycle_hrs);
+ ps_sm->params.req_addr_range[0] = pxm_domain->base;
+ ps_sm->params.req_addr_range[1] = pxm_domain->size;
+ ps_sm->params.scrub_params_in &= ~RAS2_PS_EN_BACKGROUND;
+ ps_sm->params.command = RAS2_START_PATROL_SCRUBBER;
+
+ ret = ras2_send_pcc_cmd(ras2_ctx, PCC_CMD_EXEC_RAS2);
+ if (ret) {
+ dev_err(dev, "Failed to start demand scrubbing rc(%d)\n", ret);
+ if (ret != -EBUSY) {
+ pxm_domain->base = 0;
+ pxm_domain->size = 0;
+ pxm_domain->od_scrub_sts = OD_SCRUB_STS_IDLE;
+ }
+ return ret;
+ }
+ pxm_domain->od_scrub_sts = OD_SCRUB_STS_ACTIVE;
+
+ return ras2_update_patrol_scrub_params_cache(ras2_ctx,
+ pxm_domain->feat_instance);
+}
+
+static int ras2_hw_scrub_write_size(struct device *dev, void *drv_data, u64 size)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+ struct ras2_mem_ctx *ras2_ctx = TO_RAS2_MEM_CTX(pxm_domain,
+ pxm_domain->feat_instance);
+ bool running;
+ int ret;
+
+ if (!size) {
+ dev_warn(dev, "%s: Invalid address range size=0x%llx\n",
+ __func__, size);
+ return -EINVAL;
+ }
+
+ if (pxm_domain->bg_scrub)
+ return -EBUSY;
+
+ guard(mutex)(ras2_ctx->hdr.pcc_lock);
+ ret = ras2_get_patrol_scrub_running(ras2_ctx, pxm_domain->feat_instance,
+ &running);
+ if (ret)
+ return ret;
+
+ if (running)
+ return -EBUSY;
+
+ pxm_domain->size = size;
+ pxm_domain->od_scrub_sts = OD_SCRUB_STS_INIT;
+
+ return 0;
+}
+
+static int ras2_hw_scrub_set_enabled_bg(struct device *dev, void *drv_data, bool enable)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+ struct ras2_mem_ctx *ras2_ctx = TO_RAS2_MEM_CTX(pxm_domain,
+ pxm_domain->feat_instance);
+ struct ras2_mem_ctx_hdr *ras2_ctx_hdr = &ras2_ctx->hdr;
+ struct acpi_ras2_ps_shared_mem __iomem *ps_sm =
+ TO_ACPI_RAS2_PS_SHMEM(ras2_ctx_hdr->comm_addr);
+ bool running;
+ int ret;
+
+ guard(mutex)(ras2_ctx_hdr->pcc_lock);
+ ps_sm->common.set_caps[0] = RAS2_SUPPORT_HW_PARTOL_SCRUB;
+ ret = ras2_get_patrol_scrub_running(ras2_ctx, pxm_domain->feat_instance,
+ &running);
+ if (ret)
+ return ret;
+
+ if (enable) {
+ if (pxm_domain->bg_scrub || running)
+ return -EBUSY;
+ ps_sm->params.req_addr_range[0] = 0;
+ ps_sm->params.req_addr_range[1] = 0;
+ ps_sm->params.scrub_params_in &= ~RAS2_PS_SC_HRS_IN_MASK;
+ ps_sm->params.scrub_params_in |= FIELD_PREP(RAS2_PS_SC_HRS_IN_MASK,
+ pxm_domain->scrub_cycle_hrs);
+ ps_sm->params.command = RAS2_START_PATROL_SCRUBBER;
+ } else {
+ if (!pxm_domain->bg_scrub)
+ return -EPERM;
+ ps_sm->params.command = RAS2_STOP_PATROL_SCRUBBER;
+ }
+
+ ps_sm->params.scrub_params_in &= ~RAS2_PS_EN_BACKGROUND;
+ ps_sm->params.scrub_params_in |= FIELD_PREP(RAS2_PS_EN_BACKGROUND,
+ enable);
+ ret = ras2_send_pcc_cmd(ras2_ctx, PCC_CMD_EXEC_RAS2);
+ if (ret) {
+ dev_err(dev, "Failed to %s background scrubbing\n",
+ str_enable_disable(enable));
+ return ret;
+ }
+
+ if (enable) {
+ pxm_domain->bg_scrub = true;
+ /* Update the cache to account for rounding of supplied parameters and similar */
+ ret = ras2_update_patrol_scrub_params_cache(ras2_ctx,
+ pxm_domain->feat_instance);
+ } else {
+ ret = ras2_update_patrol_scrub_params_cache(ras2_ctx,
+ pxm_domain->feat_instance);
+ pxm_domain->bg_scrub = false;
+ }
+
+ return ret;
+}
+
+static int ras2_hw_scrub_get_enabled_bg(struct device *dev, void *drv_data, bool *enabled)
+{
+ struct ras2_pxm_domain *pxm_domain = drv_data;
+
+ *enabled = pxm_domain->bg_scrub;
+
+ return 0;
+}
+
+static const struct edac_scrub_ops ras2_scrub_ops = {
+ .read_addr = ras2_hw_scrub_read_addr,
+ .read_size = ras2_hw_scrub_read_size,
+ .write_addr = ras2_hw_scrub_write_addr,
+ .write_size = ras2_hw_scrub_write_size,
+ .get_enabled_bg = ras2_hw_scrub_get_enabled_bg,
+ .set_enabled_bg = ras2_hw_scrub_set_enabled_bg,
+ .get_min_cycle = ras2_hw_scrub_read_min_scrub_cycle,
+ .get_max_cycle = ras2_hw_scrub_read_max_scrub_cycle,
+ .get_cycle_duration = ras2_hw_scrub_cycle_read,
+ .set_cycle_duration = ras2_hw_scrub_cycle_write,
+};
+
+static int ras2_probe(struct auxiliary_device *auxdev,
+ const struct auxiliary_device_id *id)
+{
+ struct ras2_mem_ctx_hdr *ras2_ctx_hdr = container_of(auxdev, struct ras2_mem_ctx_hdr, adev);
+ struct ras2_mem_ctx *ras2_ctx = container_of(ras2_ctx_hdr, struct ras2_mem_ctx, hdr);
+ struct edac_dev_feature *ras_features;
+ char scrub_name[RAS2_SCRUB_NAME_LEN];
+ u16 i;
+ int ret;
+
+ if (!ras2_is_patrol_scrub_support(ras2_ctx_hdr))
+ return -EOPNOTSUPP;
+
+ sprintf(scrub_name, "acpi_ras_mem_id%d", ras2_ctx_hdr->id);
+
+ ras_features = kzalloc(ras2_ctx_hdr->num_pxm_domains *
+ sizeof(*ras_features), GFP_KERNEL);
+ if (!ras_features)
+ return -ENOMEM;
+
+ for (i = 0; i < ras2_ctx_hdr->num_pxm_domains; i++) {
+ ras_features[i].ft_type = RAS_FEAT_SCRUB;
+ ras_features[i].instance = i;
+ ras_features[i].scrub_ops = &ras2_scrub_ops;
+ ras_features[i].ctx = &ras2_ctx->pxm_domains[i];
+ ret = ras2_update_patrol_scrub_params_cache(ras2_ctx, i);
+ if (ret)
+ return ret;
+ }
+
+ return edac_dev_register(&auxdev->dev, scrub_name, NULL,
+ ras2_ctx_hdr->num_pxm_domains,
+ ras_features);
+}
+
+static const struct auxiliary_device_id ras2_mem_dev_id_table[] = {
+ { .name = RAS2_AUX_DEV_NAME "." RAS2_MEM_DEV_ID_NAME, },
+ { }
+};
+
+MODULE_DEVICE_TABLE(auxiliary, ras2_mem_dev_id_table);
+
+static struct auxiliary_driver ras2_mem_driver = {
+ .name = RAS2_MEM_DEV_ID_NAME,
+ .probe = ras2_probe,
+ .id_table = ras2_mem_dev_id_table,
+};
+module_auxiliary_driver(ras2_mem_driver);
+
+MODULE_IMPORT_NS("ACPI_RAS2");
+MODULE_DESCRIPTION("ACPI RAS2 memory driver");
+MODULE_LICENSE("GPL");