mirror of
git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2025-08-05 16:54:27 +00:00

Introduce ACQUIRE() and ACQUIRE_ERR() for conditional locks. Convert CXL subsystem to use the new macros.
2109 lines
56 KiB
C
2109 lines
56 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* CXL EDAC memory feature driver.
|
|
*
|
|
* Copyright (c) 2024-2025 HiSilicon Limited.
|
|
*
|
|
* - Supports functions to configure EDAC features of the
|
|
* CXL memory devices.
|
|
* - Registers with the EDAC device subsystem driver to expose
|
|
* the features sysfs attributes to the user for configuring
|
|
* CXL memory RAS feature.
|
|
*/
|
|
|
|
#include <linux/cleanup.h>
|
|
#include <linux/edac.h>
|
|
#include <linux/limits.h>
|
|
#include <linux/unaligned.h>
|
|
#include <linux/xarray.h>
|
|
#include <cxl/features.h>
|
|
#include <cxl.h>
|
|
#include <cxlmem.h>
|
|
#include "core.h"
|
|
#include "trace.h"
|
|
|
|
#define CXL_NR_EDAC_DEV_FEATURES 7
|
|
|
|
#define CXL_SCRUB_NO_REGION -1
|
|
|
|
struct cxl_patrol_scrub_context {
|
|
u8 instance;
|
|
u16 get_feat_size;
|
|
u16 set_feat_size;
|
|
u8 get_version;
|
|
u8 set_version;
|
|
u16 effects;
|
|
struct cxl_memdev *cxlmd;
|
|
struct cxl_region *cxlr;
|
|
};
|
|
|
|
/*
|
|
* See CXL spec rev 3.2 @8.2.10.9.11.1 Table 8-222 Device Patrol Scrub Control
|
|
* Feature Readable Attributes.
|
|
*/
|
|
struct cxl_scrub_rd_attrbs {
|
|
u8 scrub_cycle_cap;
|
|
__le16 scrub_cycle_hours;
|
|
u8 scrub_flags;
|
|
} __packed;
|
|
|
|
/*
|
|
* See CXL spec rev 3.2 @8.2.10.9.11.1 Table 8-223 Device Patrol Scrub Control
|
|
* Feature Writable Attributes.
|
|
*/
|
|
struct cxl_scrub_wr_attrbs {
|
|
u8 scrub_cycle_hours;
|
|
u8 scrub_flags;
|
|
} __packed;
|
|
|
|
#define CXL_SCRUB_CONTROL_CHANGEABLE BIT(0)
|
|
#define CXL_SCRUB_CONTROL_REALTIME BIT(1)
|
|
#define CXL_SCRUB_CONTROL_CYCLE_MASK GENMASK(7, 0)
|
|
#define CXL_SCRUB_CONTROL_MIN_CYCLE_MASK GENMASK(15, 8)
|
|
#define CXL_SCRUB_CONTROL_ENABLE BIT(0)
|
|
|
|
#define CXL_GET_SCRUB_CYCLE_CHANGEABLE(cap) \
|
|
FIELD_GET(CXL_SCRUB_CONTROL_CHANGEABLE, cap)
|
|
#define CXL_GET_SCRUB_CYCLE(cycle) \
|
|
FIELD_GET(CXL_SCRUB_CONTROL_CYCLE_MASK, cycle)
|
|
#define CXL_GET_SCRUB_MIN_CYCLE(cycle) \
|
|
FIELD_GET(CXL_SCRUB_CONTROL_MIN_CYCLE_MASK, cycle)
|
|
#define CXL_GET_SCRUB_EN_STS(flags) FIELD_GET(CXL_SCRUB_CONTROL_ENABLE, flags)
|
|
|
|
#define CXL_SET_SCRUB_CYCLE(cycle) \
|
|
FIELD_PREP(CXL_SCRUB_CONTROL_CYCLE_MASK, cycle)
|
|
#define CXL_SET_SCRUB_EN(en) FIELD_PREP(CXL_SCRUB_CONTROL_ENABLE, en)
|
|
|
|
static int cxl_mem_scrub_get_attrbs(struct cxl_mailbox *cxl_mbox, u8 *cap,
|
|
u16 *cycle, u8 *flags, u8 *min_cycle)
|
|
{
|
|
size_t rd_data_size = sizeof(struct cxl_scrub_rd_attrbs);
|
|
size_t data_size;
|
|
struct cxl_scrub_rd_attrbs *rd_attrbs __free(kfree) =
|
|
kzalloc(rd_data_size, GFP_KERNEL);
|
|
if (!rd_attrbs)
|
|
return -ENOMEM;
|
|
|
|
data_size = cxl_get_feature(cxl_mbox, &CXL_FEAT_PATROL_SCRUB_UUID,
|
|
CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs,
|
|
rd_data_size, 0, NULL);
|
|
if (!data_size)
|
|
return -EIO;
|
|
|
|
*cap = rd_attrbs->scrub_cycle_cap;
|
|
*cycle = le16_to_cpu(rd_attrbs->scrub_cycle_hours);
|
|
*flags = rd_attrbs->scrub_flags;
|
|
if (min_cycle)
|
|
*min_cycle = CXL_GET_SCRUB_MIN_CYCLE(*cycle);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_scrub_get_attrbs(struct cxl_patrol_scrub_context *cxl_ps_ctx,
|
|
u8 *cap, u16 *cycle, u8 *flags, u8 *min_cycle)
|
|
{
|
|
struct cxl_mailbox *cxl_mbox;
|
|
struct cxl_region_params *p;
|
|
struct cxl_memdev *cxlmd;
|
|
struct cxl_region *cxlr;
|
|
u8 min_scrub_cycle = 0;
|
|
int i, ret;
|
|
|
|
if (!cxl_ps_ctx->cxlr) {
|
|
cxl_mbox = &cxl_ps_ctx->cxlmd->cxlds->cxl_mbox;
|
|
return cxl_mem_scrub_get_attrbs(cxl_mbox, cap, cycle,
|
|
flags, min_cycle);
|
|
}
|
|
|
|
ACQUIRE(rwsem_read_intr, rwsem)(&cxl_rwsem.region);
|
|
if ((ret = ACQUIRE_ERR(rwsem_read_intr, &rwsem)))
|
|
return ret;
|
|
|
|
cxlr = cxl_ps_ctx->cxlr;
|
|
p = &cxlr->params;
|
|
|
|
for (i = 0; i < p->nr_targets; i++) {
|
|
struct cxl_endpoint_decoder *cxled = p->targets[i];
|
|
|
|
cxlmd = cxled_to_memdev(cxled);
|
|
cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
ret = cxl_mem_scrub_get_attrbs(cxl_mbox, cap, cycle, flags,
|
|
min_cycle);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* The min_scrub_cycle of a region is the max of minimum scrub
|
|
* cycles supported by memdevs that back the region.
|
|
*/
|
|
if (min_cycle)
|
|
min_scrub_cycle = max(*min_cycle, min_scrub_cycle);
|
|
}
|
|
|
|
if (min_cycle)
|
|
*min_cycle = min_scrub_cycle;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_scrub_set_attrbs_region(struct device *dev,
|
|
struct cxl_patrol_scrub_context *cxl_ps_ctx,
|
|
u8 cycle, u8 flags)
|
|
{
|
|
struct cxl_scrub_wr_attrbs wr_attrbs;
|
|
struct cxl_mailbox *cxl_mbox;
|
|
struct cxl_region_params *p;
|
|
struct cxl_memdev *cxlmd;
|
|
struct cxl_region *cxlr;
|
|
int ret, i;
|
|
|
|
ACQUIRE(rwsem_read_intr, rwsem)(&cxl_rwsem.region);
|
|
if ((ret = ACQUIRE_ERR(rwsem_read_intr, &rwsem)))
|
|
return ret;
|
|
|
|
cxlr = cxl_ps_ctx->cxlr;
|
|
p = &cxlr->params;
|
|
wr_attrbs.scrub_cycle_hours = cycle;
|
|
wr_attrbs.scrub_flags = flags;
|
|
|
|
for (i = 0; i < p->nr_targets; i++) {
|
|
struct cxl_endpoint_decoder *cxled = p->targets[i];
|
|
|
|
cxlmd = cxled_to_memdev(cxled);
|
|
cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
ret = cxl_set_feature(cxl_mbox, &CXL_FEAT_PATROL_SCRUB_UUID,
|
|
cxl_ps_ctx->set_version, &wr_attrbs,
|
|
sizeof(wr_attrbs),
|
|
CXL_SET_FEAT_FLAG_DATA_SAVED_ACROSS_RESET,
|
|
0, NULL);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (cycle != cxlmd->scrub_cycle) {
|
|
if (cxlmd->scrub_region_id != CXL_SCRUB_NO_REGION)
|
|
dev_info(dev,
|
|
"Device scrub rate(%d hours) set by region%d rate overwritten by region%d scrub rate(%d hours)\n",
|
|
cxlmd->scrub_cycle,
|
|
cxlmd->scrub_region_id, cxlr->id,
|
|
cycle);
|
|
|
|
cxlmd->scrub_cycle = cycle;
|
|
cxlmd->scrub_region_id = cxlr->id;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_scrub_set_attrbs_device(struct device *dev,
|
|
struct cxl_patrol_scrub_context *cxl_ps_ctx,
|
|
u8 cycle, u8 flags)
|
|
{
|
|
struct cxl_scrub_wr_attrbs wr_attrbs;
|
|
struct cxl_mailbox *cxl_mbox;
|
|
struct cxl_memdev *cxlmd;
|
|
int ret;
|
|
|
|
wr_attrbs.scrub_cycle_hours = cycle;
|
|
wr_attrbs.scrub_flags = flags;
|
|
|
|
cxlmd = cxl_ps_ctx->cxlmd;
|
|
cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
ret = cxl_set_feature(cxl_mbox, &CXL_FEAT_PATROL_SCRUB_UUID,
|
|
cxl_ps_ctx->set_version, &wr_attrbs,
|
|
sizeof(wr_attrbs),
|
|
CXL_SET_FEAT_FLAG_DATA_SAVED_ACROSS_RESET, 0,
|
|
NULL);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (cycle != cxlmd->scrub_cycle) {
|
|
if (cxlmd->scrub_region_id != CXL_SCRUB_NO_REGION)
|
|
dev_info(dev,
|
|
"Device scrub rate(%d hours) set by region%d rate overwritten with device local scrub rate(%d hours)\n",
|
|
cxlmd->scrub_cycle, cxlmd->scrub_region_id,
|
|
cycle);
|
|
|
|
cxlmd->scrub_cycle = cycle;
|
|
cxlmd->scrub_region_id = CXL_SCRUB_NO_REGION;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_scrub_set_attrbs(struct device *dev,
|
|
struct cxl_patrol_scrub_context *cxl_ps_ctx,
|
|
u8 cycle, u8 flags)
|
|
{
|
|
if (cxl_ps_ctx->cxlr)
|
|
return cxl_scrub_set_attrbs_region(dev, cxl_ps_ctx, cycle, flags);
|
|
|
|
return cxl_scrub_set_attrbs_device(dev, cxl_ps_ctx, cycle, flags);
|
|
}
|
|
|
|
static int cxl_patrol_scrub_get_enabled_bg(struct device *dev, void *drv_data,
|
|
bool *enabled)
|
|
{
|
|
struct cxl_patrol_scrub_context *ctx = drv_data;
|
|
u8 cap, flags;
|
|
u16 cycle;
|
|
int ret;
|
|
|
|
ret = cxl_scrub_get_attrbs(ctx, &cap, &cycle, &flags, NULL);
|
|
if (ret)
|
|
return ret;
|
|
|
|
*enabled = CXL_GET_SCRUB_EN_STS(flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_patrol_scrub_set_enabled_bg(struct device *dev, void *drv_data,
|
|
bool enable)
|
|
{
|
|
struct cxl_patrol_scrub_context *ctx = drv_data;
|
|
u8 cap, flags, wr_cycle;
|
|
u16 rd_cycle;
|
|
int ret;
|
|
|
|
if (!capable(CAP_SYS_RAWIO))
|
|
return -EPERM;
|
|
|
|
ret = cxl_scrub_get_attrbs(ctx, &cap, &rd_cycle, &flags, NULL);
|
|
if (ret)
|
|
return ret;
|
|
|
|
wr_cycle = CXL_GET_SCRUB_CYCLE(rd_cycle);
|
|
flags = CXL_SET_SCRUB_EN(enable);
|
|
|
|
return cxl_scrub_set_attrbs(dev, ctx, wr_cycle, flags);
|
|
}
|
|
|
|
static int cxl_patrol_scrub_get_min_scrub_cycle(struct device *dev,
|
|
void *drv_data, u32 *min)
|
|
{
|
|
struct cxl_patrol_scrub_context *ctx = drv_data;
|
|
u8 cap, flags, min_cycle;
|
|
u16 cycle;
|
|
int ret;
|
|
|
|
ret = cxl_scrub_get_attrbs(ctx, &cap, &cycle, &flags, &min_cycle);
|
|
if (ret)
|
|
return ret;
|
|
|
|
*min = min_cycle * 3600;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_patrol_scrub_get_max_scrub_cycle(struct device *dev,
|
|
void *drv_data, u32 *max)
|
|
{
|
|
*max = U8_MAX * 3600; /* Max set by register size */
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_patrol_scrub_get_scrub_cycle(struct device *dev, void *drv_data,
|
|
u32 *scrub_cycle_secs)
|
|
{
|
|
struct cxl_patrol_scrub_context *ctx = drv_data;
|
|
u8 cap, flags;
|
|
u16 cycle;
|
|
int ret;
|
|
|
|
ret = cxl_scrub_get_attrbs(ctx, &cap, &cycle, &flags, NULL);
|
|
if (ret)
|
|
return ret;
|
|
|
|
*scrub_cycle_secs = CXL_GET_SCRUB_CYCLE(cycle) * 3600;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_patrol_scrub_set_scrub_cycle(struct device *dev, void *drv_data,
|
|
u32 scrub_cycle_secs)
|
|
{
|
|
struct cxl_patrol_scrub_context *ctx = drv_data;
|
|
u8 scrub_cycle_hours = scrub_cycle_secs / 3600;
|
|
u8 cap, wr_cycle, flags, min_cycle;
|
|
u16 rd_cycle;
|
|
int ret;
|
|
|
|
if (!capable(CAP_SYS_RAWIO))
|
|
return -EPERM;
|
|
|
|
ret = cxl_scrub_get_attrbs(ctx, &cap, &rd_cycle, &flags, &min_cycle);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (!CXL_GET_SCRUB_CYCLE_CHANGEABLE(cap))
|
|
return -EOPNOTSUPP;
|
|
|
|
if (scrub_cycle_hours < min_cycle) {
|
|
dev_dbg(dev, "Invalid CXL patrol scrub cycle(%d) to set\n",
|
|
scrub_cycle_hours);
|
|
dev_dbg(dev,
|
|
"Minimum supported CXL patrol scrub cycle in hour %d\n",
|
|
min_cycle);
|
|
return -EINVAL;
|
|
}
|
|
wr_cycle = CXL_SET_SCRUB_CYCLE(scrub_cycle_hours);
|
|
|
|
return cxl_scrub_set_attrbs(dev, ctx, wr_cycle, flags);
|
|
}
|
|
|
|
static const struct edac_scrub_ops cxl_ps_scrub_ops = {
|
|
.get_enabled_bg = cxl_patrol_scrub_get_enabled_bg,
|
|
.set_enabled_bg = cxl_patrol_scrub_set_enabled_bg,
|
|
.get_min_cycle = cxl_patrol_scrub_get_min_scrub_cycle,
|
|
.get_max_cycle = cxl_patrol_scrub_get_max_scrub_cycle,
|
|
.get_cycle_duration = cxl_patrol_scrub_get_scrub_cycle,
|
|
.set_cycle_duration = cxl_patrol_scrub_set_scrub_cycle,
|
|
};
|
|
|
|
static int cxl_memdev_scrub_init(struct cxl_memdev *cxlmd,
|
|
struct edac_dev_feature *ras_feature,
|
|
u8 scrub_inst)
|
|
{
|
|
struct cxl_patrol_scrub_context *cxl_ps_ctx;
|
|
struct cxl_feat_entry *feat_entry;
|
|
u8 cap, flags;
|
|
u16 cycle;
|
|
int rc;
|
|
|
|
feat_entry = cxl_feature_info(to_cxlfs(cxlmd->cxlds),
|
|
&CXL_FEAT_PATROL_SCRUB_UUID);
|
|
if (IS_ERR(feat_entry))
|
|
return -EOPNOTSUPP;
|
|
|
|
if (!(le32_to_cpu(feat_entry->flags) & CXL_FEATURE_F_CHANGEABLE))
|
|
return -EOPNOTSUPP;
|
|
|
|
cxl_ps_ctx = devm_kzalloc(&cxlmd->dev, sizeof(*cxl_ps_ctx), GFP_KERNEL);
|
|
if (!cxl_ps_ctx)
|
|
return -ENOMEM;
|
|
|
|
*cxl_ps_ctx = (struct cxl_patrol_scrub_context){
|
|
.get_feat_size = le16_to_cpu(feat_entry->get_feat_size),
|
|
.set_feat_size = le16_to_cpu(feat_entry->set_feat_size),
|
|
.get_version = feat_entry->get_feat_ver,
|
|
.set_version = feat_entry->set_feat_ver,
|
|
.effects = le16_to_cpu(feat_entry->effects),
|
|
.instance = scrub_inst,
|
|
.cxlmd = cxlmd,
|
|
};
|
|
|
|
rc = cxl_mem_scrub_get_attrbs(&cxlmd->cxlds->cxl_mbox, &cap, &cycle,
|
|
&flags, NULL);
|
|
if (rc)
|
|
return rc;
|
|
|
|
cxlmd->scrub_cycle = CXL_GET_SCRUB_CYCLE(cycle);
|
|
cxlmd->scrub_region_id = CXL_SCRUB_NO_REGION;
|
|
|
|
ras_feature->ft_type = RAS_FEAT_SCRUB;
|
|
ras_feature->instance = cxl_ps_ctx->instance;
|
|
ras_feature->scrub_ops = &cxl_ps_scrub_ops;
|
|
ras_feature->ctx = cxl_ps_ctx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_region_scrub_init(struct cxl_region *cxlr,
|
|
struct edac_dev_feature *ras_feature,
|
|
u8 scrub_inst)
|
|
{
|
|
struct cxl_patrol_scrub_context *cxl_ps_ctx;
|
|
struct cxl_region_params *p = &cxlr->params;
|
|
struct cxl_feat_entry *feat_entry = NULL;
|
|
struct cxl_memdev *cxlmd;
|
|
u8 cap, flags;
|
|
u16 cycle;
|
|
int i, rc;
|
|
|
|
/*
|
|
* The cxl_region_rwsem must be held if the code below is used in a context
|
|
* other than when the region is in the probe state, as shown here.
|
|
*/
|
|
for (i = 0; i < p->nr_targets; i++) {
|
|
struct cxl_endpoint_decoder *cxled = p->targets[i];
|
|
|
|
cxlmd = cxled_to_memdev(cxled);
|
|
feat_entry = cxl_feature_info(to_cxlfs(cxlmd->cxlds),
|
|
&CXL_FEAT_PATROL_SCRUB_UUID);
|
|
if (IS_ERR(feat_entry))
|
|
return -EOPNOTSUPP;
|
|
|
|
if (!(le32_to_cpu(feat_entry->flags) &
|
|
CXL_FEATURE_F_CHANGEABLE))
|
|
return -EOPNOTSUPP;
|
|
|
|
rc = cxl_mem_scrub_get_attrbs(&cxlmd->cxlds->cxl_mbox, &cap,
|
|
&cycle, &flags, NULL);
|
|
if (rc)
|
|
return rc;
|
|
|
|
cxlmd->scrub_cycle = CXL_GET_SCRUB_CYCLE(cycle);
|
|
cxlmd->scrub_region_id = CXL_SCRUB_NO_REGION;
|
|
}
|
|
|
|
cxl_ps_ctx = devm_kzalloc(&cxlr->dev, sizeof(*cxl_ps_ctx), GFP_KERNEL);
|
|
if (!cxl_ps_ctx)
|
|
return -ENOMEM;
|
|
|
|
*cxl_ps_ctx = (struct cxl_patrol_scrub_context){
|
|
.get_feat_size = le16_to_cpu(feat_entry->get_feat_size),
|
|
.set_feat_size = le16_to_cpu(feat_entry->set_feat_size),
|
|
.get_version = feat_entry->get_feat_ver,
|
|
.set_version = feat_entry->set_feat_ver,
|
|
.effects = le16_to_cpu(feat_entry->effects),
|
|
.instance = scrub_inst,
|
|
.cxlr = cxlr,
|
|
};
|
|
|
|
ras_feature->ft_type = RAS_FEAT_SCRUB;
|
|
ras_feature->instance = cxl_ps_ctx->instance;
|
|
ras_feature->scrub_ops = &cxl_ps_scrub_ops;
|
|
ras_feature->ctx = cxl_ps_ctx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
struct cxl_ecs_context {
|
|
u16 num_media_frus;
|
|
u16 get_feat_size;
|
|
u16 set_feat_size;
|
|
u8 get_version;
|
|
u8 set_version;
|
|
u16 effects;
|
|
struct cxl_memdev *cxlmd;
|
|
};
|
|
|
|
/*
|
|
* See CXL spec rev 3.2 @8.2.10.9.11.2 Table 8-225 DDR5 ECS Control Feature
|
|
* Readable Attributes.
|
|
*/
|
|
struct cxl_ecs_fru_rd_attrbs {
|
|
u8 ecs_cap;
|
|
__le16 ecs_config;
|
|
u8 ecs_flags;
|
|
} __packed;
|
|
|
|
struct cxl_ecs_rd_attrbs {
|
|
u8 ecs_log_cap;
|
|
struct cxl_ecs_fru_rd_attrbs fru_attrbs[];
|
|
} __packed;
|
|
|
|
/*
|
|
* See CXL spec rev 3.2 @8.2.10.9.11.2 Table 8-226 DDR5 ECS Control Feature
|
|
* Writable Attributes.
|
|
*/
|
|
struct cxl_ecs_fru_wr_attrbs {
|
|
__le16 ecs_config;
|
|
} __packed;
|
|
|
|
struct cxl_ecs_wr_attrbs {
|
|
u8 ecs_log_cap;
|
|
struct cxl_ecs_fru_wr_attrbs fru_attrbs[];
|
|
} __packed;
|
|
|
|
#define CXL_ECS_LOG_ENTRY_TYPE_MASK GENMASK(1, 0)
|
|
#define CXL_ECS_REALTIME_REPORT_CAP_MASK BIT(0)
|
|
#define CXL_ECS_THRESHOLD_COUNT_MASK GENMASK(2, 0)
|
|
#define CXL_ECS_COUNT_MODE_MASK BIT(3)
|
|
#define CXL_ECS_RESET_COUNTER_MASK BIT(4)
|
|
#define CXL_ECS_RESET_COUNTER 1
|
|
|
|
enum {
|
|
ECS_THRESHOLD_256 = 256,
|
|
ECS_THRESHOLD_1024 = 1024,
|
|
ECS_THRESHOLD_4096 = 4096,
|
|
};
|
|
|
|
enum {
|
|
ECS_THRESHOLD_IDX_256 = 3,
|
|
ECS_THRESHOLD_IDX_1024 = 4,
|
|
ECS_THRESHOLD_IDX_4096 = 5,
|
|
};
|
|
|
|
static const u16 ecs_supp_threshold[] = {
|
|
[ECS_THRESHOLD_IDX_256] = 256,
|
|
[ECS_THRESHOLD_IDX_1024] = 1024,
|
|
[ECS_THRESHOLD_IDX_4096] = 4096,
|
|
};
|
|
|
|
enum {
|
|
ECS_LOG_ENTRY_TYPE_DRAM = 0x0,
|
|
ECS_LOG_ENTRY_TYPE_MEM_MEDIA_FRU = 0x1,
|
|
};
|
|
|
|
enum cxl_ecs_count_mode {
|
|
ECS_MODE_COUNTS_ROWS = 0,
|
|
ECS_MODE_COUNTS_CODEWORDS = 1,
|
|
};
|
|
|
|
static int cxl_mem_ecs_get_attrbs(struct device *dev,
|
|
struct cxl_ecs_context *cxl_ecs_ctx,
|
|
int fru_id, u8 *log_cap, u16 *config)
|
|
{
|
|
struct cxl_memdev *cxlmd = cxl_ecs_ctx->cxlmd;
|
|
struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
struct cxl_ecs_fru_rd_attrbs *fru_rd_attrbs;
|
|
size_t rd_data_size;
|
|
size_t data_size;
|
|
|
|
rd_data_size = cxl_ecs_ctx->get_feat_size;
|
|
|
|
struct cxl_ecs_rd_attrbs *rd_attrbs __free(kvfree) =
|
|
kvzalloc(rd_data_size, GFP_KERNEL);
|
|
if (!rd_attrbs)
|
|
return -ENOMEM;
|
|
|
|
data_size = cxl_get_feature(cxl_mbox, &CXL_FEAT_ECS_UUID,
|
|
CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs,
|
|
rd_data_size, 0, NULL);
|
|
if (!data_size)
|
|
return -EIO;
|
|
|
|
fru_rd_attrbs = rd_attrbs->fru_attrbs;
|
|
*log_cap = rd_attrbs->ecs_log_cap;
|
|
*config = le16_to_cpu(fru_rd_attrbs[fru_id].ecs_config);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_mem_ecs_set_attrbs(struct device *dev,
|
|
struct cxl_ecs_context *cxl_ecs_ctx,
|
|
int fru_id, u8 log_cap, u16 config)
|
|
{
|
|
struct cxl_memdev *cxlmd = cxl_ecs_ctx->cxlmd;
|
|
struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
struct cxl_ecs_fru_rd_attrbs *fru_rd_attrbs;
|
|
struct cxl_ecs_fru_wr_attrbs *fru_wr_attrbs;
|
|
size_t rd_data_size, wr_data_size;
|
|
u16 num_media_frus, count;
|
|
size_t data_size;
|
|
|
|
num_media_frus = cxl_ecs_ctx->num_media_frus;
|
|
rd_data_size = cxl_ecs_ctx->get_feat_size;
|
|
wr_data_size = cxl_ecs_ctx->set_feat_size;
|
|
struct cxl_ecs_rd_attrbs *rd_attrbs __free(kvfree) =
|
|
kvzalloc(rd_data_size, GFP_KERNEL);
|
|
if (!rd_attrbs)
|
|
return -ENOMEM;
|
|
|
|
data_size = cxl_get_feature(cxl_mbox, &CXL_FEAT_ECS_UUID,
|
|
CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs,
|
|
rd_data_size, 0, NULL);
|
|
if (!data_size)
|
|
return -EIO;
|
|
|
|
struct cxl_ecs_wr_attrbs *wr_attrbs __free(kvfree) =
|
|
kvzalloc(wr_data_size, GFP_KERNEL);
|
|
if (!wr_attrbs)
|
|
return -ENOMEM;
|
|
|
|
/*
|
|
* Fill writable attributes from the current attributes read
|
|
* for all the media FRUs.
|
|
*/
|
|
fru_rd_attrbs = rd_attrbs->fru_attrbs;
|
|
fru_wr_attrbs = wr_attrbs->fru_attrbs;
|
|
wr_attrbs->ecs_log_cap = log_cap;
|
|
for (count = 0; count < num_media_frus; count++)
|
|
fru_wr_attrbs[count].ecs_config =
|
|
fru_rd_attrbs[count].ecs_config;
|
|
|
|
fru_wr_attrbs[fru_id].ecs_config = cpu_to_le16(config);
|
|
|
|
return cxl_set_feature(cxl_mbox, &CXL_FEAT_ECS_UUID,
|
|
cxl_ecs_ctx->set_version, wr_attrbs,
|
|
wr_data_size,
|
|
CXL_SET_FEAT_FLAG_DATA_SAVED_ACROSS_RESET,
|
|
0, NULL);
|
|
}
|
|
|
|
static u8 cxl_get_ecs_log_entry_type(u8 log_cap, u16 config)
|
|
{
|
|
return FIELD_GET(CXL_ECS_LOG_ENTRY_TYPE_MASK, log_cap);
|
|
}
|
|
|
|
static u16 cxl_get_ecs_threshold(u8 log_cap, u16 config)
|
|
{
|
|
u8 index = FIELD_GET(CXL_ECS_THRESHOLD_COUNT_MASK, config);
|
|
|
|
return ecs_supp_threshold[index];
|
|
}
|
|
|
|
static u8 cxl_get_ecs_count_mode(u8 log_cap, u16 config)
|
|
{
|
|
return FIELD_GET(CXL_ECS_COUNT_MODE_MASK, config);
|
|
}
|
|
|
|
#define CXL_ECS_GET_ATTR(attrb) \
|
|
static int cxl_ecs_get_##attrb(struct device *dev, void *drv_data, \
|
|
int fru_id, u32 *val) \
|
|
{ \
|
|
struct cxl_ecs_context *ctx = drv_data; \
|
|
u8 log_cap; \
|
|
u16 config; \
|
|
int ret; \
|
|
\
|
|
ret = cxl_mem_ecs_get_attrbs(dev, ctx, fru_id, &log_cap, \
|
|
&config); \
|
|
if (ret) \
|
|
return ret; \
|
|
\
|
|
*val = cxl_get_ecs_##attrb(log_cap, config); \
|
|
\
|
|
return 0; \
|
|
}
|
|
|
|
CXL_ECS_GET_ATTR(log_entry_type)
|
|
CXL_ECS_GET_ATTR(count_mode)
|
|
CXL_ECS_GET_ATTR(threshold)
|
|
|
|
static int cxl_set_ecs_log_entry_type(struct device *dev, u8 *log_cap,
|
|
u16 *config, u32 val)
|
|
{
|
|
if (val != ECS_LOG_ENTRY_TYPE_DRAM &&
|
|
val != ECS_LOG_ENTRY_TYPE_MEM_MEDIA_FRU)
|
|
return -EINVAL;
|
|
|
|
*log_cap = FIELD_PREP(CXL_ECS_LOG_ENTRY_TYPE_MASK, val);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_set_ecs_threshold(struct device *dev, u8 *log_cap, u16 *config,
|
|
u32 val)
|
|
{
|
|
*config &= ~CXL_ECS_THRESHOLD_COUNT_MASK;
|
|
|
|
switch (val) {
|
|
case ECS_THRESHOLD_256:
|
|
*config |= FIELD_PREP(CXL_ECS_THRESHOLD_COUNT_MASK,
|
|
ECS_THRESHOLD_IDX_256);
|
|
break;
|
|
case ECS_THRESHOLD_1024:
|
|
*config |= FIELD_PREP(CXL_ECS_THRESHOLD_COUNT_MASK,
|
|
ECS_THRESHOLD_IDX_1024);
|
|
break;
|
|
case ECS_THRESHOLD_4096:
|
|
*config |= FIELD_PREP(CXL_ECS_THRESHOLD_COUNT_MASK,
|
|
ECS_THRESHOLD_IDX_4096);
|
|
break;
|
|
default:
|
|
dev_dbg(dev, "Invalid CXL ECS threshold count(%u) to set\n",
|
|
val);
|
|
dev_dbg(dev, "Supported ECS threshold counts: %u, %u, %u\n",
|
|
ECS_THRESHOLD_256, ECS_THRESHOLD_1024,
|
|
ECS_THRESHOLD_4096);
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_set_ecs_count_mode(struct device *dev, u8 *log_cap, u16 *config,
|
|
u32 val)
|
|
{
|
|
if (val != ECS_MODE_COUNTS_ROWS && val != ECS_MODE_COUNTS_CODEWORDS) {
|
|
dev_dbg(dev, "Invalid CXL ECS scrub mode(%d) to set\n", val);
|
|
dev_dbg(dev,
|
|
"Supported ECS Modes: 0: ECS counts rows with errors,"
|
|
" 1: ECS counts codewords with errors\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
*config &= ~CXL_ECS_COUNT_MODE_MASK;
|
|
*config |= FIELD_PREP(CXL_ECS_COUNT_MODE_MASK, val);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_set_ecs_reset_counter(struct device *dev, u8 *log_cap,
|
|
u16 *config, u32 val)
|
|
{
|
|
if (val != CXL_ECS_RESET_COUNTER)
|
|
return -EINVAL;
|
|
|
|
*config &= ~CXL_ECS_RESET_COUNTER_MASK;
|
|
*config |= FIELD_PREP(CXL_ECS_RESET_COUNTER_MASK, val);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define CXL_ECS_SET_ATTR(attrb) \
|
|
static int cxl_ecs_set_##attrb(struct device *dev, void *drv_data, \
|
|
int fru_id, u32 val) \
|
|
{ \
|
|
struct cxl_ecs_context *ctx = drv_data; \
|
|
u8 log_cap; \
|
|
u16 config; \
|
|
int ret; \
|
|
\
|
|
if (!capable(CAP_SYS_RAWIO)) \
|
|
return -EPERM; \
|
|
\
|
|
ret = cxl_mem_ecs_get_attrbs(dev, ctx, fru_id, &log_cap, \
|
|
&config); \
|
|
if (ret) \
|
|
return ret; \
|
|
\
|
|
ret = cxl_set_ecs_##attrb(dev, &log_cap, &config, val); \
|
|
if (ret) \
|
|
return ret; \
|
|
\
|
|
return cxl_mem_ecs_set_attrbs(dev, ctx, fru_id, log_cap, \
|
|
config); \
|
|
}
|
|
CXL_ECS_SET_ATTR(log_entry_type)
|
|
CXL_ECS_SET_ATTR(count_mode)
|
|
CXL_ECS_SET_ATTR(reset_counter)
|
|
CXL_ECS_SET_ATTR(threshold)
|
|
|
|
static const struct edac_ecs_ops cxl_ecs_ops = {
|
|
.get_log_entry_type = cxl_ecs_get_log_entry_type,
|
|
.set_log_entry_type = cxl_ecs_set_log_entry_type,
|
|
.get_mode = cxl_ecs_get_count_mode,
|
|
.set_mode = cxl_ecs_set_count_mode,
|
|
.reset = cxl_ecs_set_reset_counter,
|
|
.get_threshold = cxl_ecs_get_threshold,
|
|
.set_threshold = cxl_ecs_set_threshold,
|
|
};
|
|
|
|
static int cxl_memdev_ecs_init(struct cxl_memdev *cxlmd,
|
|
struct edac_dev_feature *ras_feature)
|
|
{
|
|
struct cxl_ecs_context *cxl_ecs_ctx;
|
|
struct cxl_feat_entry *feat_entry;
|
|
int num_media_frus;
|
|
|
|
feat_entry =
|
|
cxl_feature_info(to_cxlfs(cxlmd->cxlds), &CXL_FEAT_ECS_UUID);
|
|
if (IS_ERR(feat_entry))
|
|
return -EOPNOTSUPP;
|
|
|
|
if (!(le32_to_cpu(feat_entry->flags) & CXL_FEATURE_F_CHANGEABLE))
|
|
return -EOPNOTSUPP;
|
|
|
|
num_media_frus = (le16_to_cpu(feat_entry->get_feat_size) -
|
|
sizeof(struct cxl_ecs_rd_attrbs)) /
|
|
sizeof(struct cxl_ecs_fru_rd_attrbs);
|
|
if (!num_media_frus)
|
|
return -EOPNOTSUPP;
|
|
|
|
cxl_ecs_ctx =
|
|
devm_kzalloc(&cxlmd->dev, sizeof(*cxl_ecs_ctx), GFP_KERNEL);
|
|
if (!cxl_ecs_ctx)
|
|
return -ENOMEM;
|
|
|
|
*cxl_ecs_ctx = (struct cxl_ecs_context){
|
|
.get_feat_size = le16_to_cpu(feat_entry->get_feat_size),
|
|
.set_feat_size = le16_to_cpu(feat_entry->set_feat_size),
|
|
.get_version = feat_entry->get_feat_ver,
|
|
.set_version = feat_entry->set_feat_ver,
|
|
.effects = le16_to_cpu(feat_entry->effects),
|
|
.num_media_frus = num_media_frus,
|
|
.cxlmd = cxlmd,
|
|
};
|
|
|
|
ras_feature->ft_type = RAS_FEAT_ECS;
|
|
ras_feature->ecs_ops = &cxl_ecs_ops;
|
|
ras_feature->ctx = cxl_ecs_ctx;
|
|
ras_feature->ecs_info.num_media_frus = num_media_frus;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Perform Maintenance CXL 3.2 Spec 8.2.10.7.1
|
|
*/
|
|
|
|
/*
|
|
* Perform Maintenance input payload
|
|
* CXL rev 3.2 section 8.2.10.7.1 Table 8-117
|
|
*/
|
|
struct cxl_mbox_maintenance_hdr {
|
|
u8 op_class;
|
|
u8 op_subclass;
|
|
} __packed;
|
|
|
|
static int cxl_perform_maintenance(struct cxl_mailbox *cxl_mbox, u8 class,
|
|
u8 subclass, void *data_in,
|
|
size_t data_in_size)
|
|
{
|
|
struct cxl_memdev_maintenance_pi {
|
|
struct cxl_mbox_maintenance_hdr hdr;
|
|
u8 data[];
|
|
} __packed;
|
|
struct cxl_mbox_cmd mbox_cmd;
|
|
size_t hdr_size;
|
|
|
|
struct cxl_memdev_maintenance_pi *pi __free(kvfree) =
|
|
kvzalloc(cxl_mbox->payload_size, GFP_KERNEL);
|
|
if (!pi)
|
|
return -ENOMEM;
|
|
|
|
pi->hdr.op_class = class;
|
|
pi->hdr.op_subclass = subclass;
|
|
hdr_size = sizeof(pi->hdr);
|
|
/*
|
|
* Check minimum mbox payload size is available for
|
|
* the maintenance data transfer.
|
|
*/
|
|
if (hdr_size + data_in_size > cxl_mbox->payload_size)
|
|
return -ENOMEM;
|
|
|
|
memcpy(pi->data, data_in, data_in_size);
|
|
mbox_cmd = (struct cxl_mbox_cmd){
|
|
.opcode = CXL_MBOX_OP_DO_MAINTENANCE,
|
|
.size_in = hdr_size + data_in_size,
|
|
.payload_in = pi,
|
|
};
|
|
|
|
return cxl_internal_send_cmd(cxl_mbox, &mbox_cmd);
|
|
}
|
|
|
|
/*
|
|
* Support for finding a memory operation attributes
|
|
* are from the current boot or not.
|
|
*/
|
|
|
|
struct cxl_mem_err_rec {
|
|
struct xarray rec_gen_media;
|
|
struct xarray rec_dram;
|
|
};
|
|
|
|
enum cxl_mem_repair_type {
|
|
CXL_PPR,
|
|
CXL_CACHELINE_SPARING,
|
|
CXL_ROW_SPARING,
|
|
CXL_BANK_SPARING,
|
|
CXL_RANK_SPARING,
|
|
CXL_REPAIR_MAX,
|
|
};
|
|
|
|
/**
|
|
* struct cxl_mem_repair_attrbs - CXL memory repair attributes
|
|
* @dpa: DPA of memory to repair
|
|
* @nibble_mask: nibble mask, identifies one or more nibbles on the memory bus
|
|
* @row: row of memory to repair
|
|
* @column: column of memory to repair
|
|
* @channel: channel of memory to repair
|
|
* @sub_channel: sub channel of memory to repair
|
|
* @rank: rank of memory to repair
|
|
* @bank_group: bank group of memory to repair
|
|
* @bank: bank of memory to repair
|
|
* @repair_type: repair type. For eg. PPR, memory sparing etc.
|
|
*/
|
|
struct cxl_mem_repair_attrbs {
|
|
u64 dpa;
|
|
u32 nibble_mask;
|
|
u32 row;
|
|
u16 column;
|
|
u8 channel;
|
|
u8 sub_channel;
|
|
u8 rank;
|
|
u8 bank_group;
|
|
u8 bank;
|
|
enum cxl_mem_repair_type repair_type;
|
|
};
|
|
|
|
static struct cxl_event_gen_media *
|
|
cxl_find_rec_gen_media(struct cxl_memdev *cxlmd,
|
|
struct cxl_mem_repair_attrbs *attrbs)
|
|
{
|
|
struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
|
|
struct cxl_event_gen_media *rec;
|
|
|
|
if (!array_rec)
|
|
return NULL;
|
|
|
|
rec = xa_load(&array_rec->rec_gen_media, attrbs->dpa);
|
|
if (!rec)
|
|
return NULL;
|
|
|
|
if (attrbs->repair_type == CXL_PPR)
|
|
return rec;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static struct cxl_event_dram *
|
|
cxl_find_rec_dram(struct cxl_memdev *cxlmd,
|
|
struct cxl_mem_repair_attrbs *attrbs)
|
|
{
|
|
struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
|
|
struct cxl_event_dram *rec;
|
|
u16 validity_flags;
|
|
|
|
if (!array_rec)
|
|
return NULL;
|
|
|
|
rec = xa_load(&array_rec->rec_dram, attrbs->dpa);
|
|
if (!rec)
|
|
return NULL;
|
|
|
|
validity_flags = get_unaligned_le16(rec->media_hdr.validity_flags);
|
|
if (!(validity_flags & CXL_DER_VALID_CHANNEL) ||
|
|
!(validity_flags & CXL_DER_VALID_RANK))
|
|
return NULL;
|
|
|
|
switch (attrbs->repair_type) {
|
|
case CXL_PPR:
|
|
if (!(validity_flags & CXL_DER_VALID_NIBBLE) ||
|
|
get_unaligned_le24(rec->nibble_mask) == attrbs->nibble_mask)
|
|
return rec;
|
|
break;
|
|
case CXL_CACHELINE_SPARING:
|
|
if (!(validity_flags & CXL_DER_VALID_BANK_GROUP) ||
|
|
!(validity_flags & CXL_DER_VALID_BANK) ||
|
|
!(validity_flags & CXL_DER_VALID_ROW) ||
|
|
!(validity_flags & CXL_DER_VALID_COLUMN))
|
|
return NULL;
|
|
|
|
if (rec->media_hdr.channel == attrbs->channel &&
|
|
rec->media_hdr.rank == attrbs->rank &&
|
|
rec->bank_group == attrbs->bank_group &&
|
|
rec->bank == attrbs->bank &&
|
|
get_unaligned_le24(rec->row) == attrbs->row &&
|
|
get_unaligned_le16(rec->column) == attrbs->column &&
|
|
(!(validity_flags & CXL_DER_VALID_NIBBLE) ||
|
|
get_unaligned_le24(rec->nibble_mask) ==
|
|
attrbs->nibble_mask) &&
|
|
(!(validity_flags & CXL_DER_VALID_SUB_CHANNEL) ||
|
|
rec->sub_channel == attrbs->sub_channel))
|
|
return rec;
|
|
break;
|
|
case CXL_ROW_SPARING:
|
|
if (!(validity_flags & CXL_DER_VALID_BANK_GROUP) ||
|
|
!(validity_flags & CXL_DER_VALID_BANK) ||
|
|
!(validity_flags & CXL_DER_VALID_ROW))
|
|
return NULL;
|
|
|
|
if (rec->media_hdr.channel == attrbs->channel &&
|
|
rec->media_hdr.rank == attrbs->rank &&
|
|
rec->bank_group == attrbs->bank_group &&
|
|
rec->bank == attrbs->bank &&
|
|
get_unaligned_le24(rec->row) == attrbs->row &&
|
|
(!(validity_flags & CXL_DER_VALID_NIBBLE) ||
|
|
get_unaligned_le24(rec->nibble_mask) ==
|
|
attrbs->nibble_mask))
|
|
return rec;
|
|
break;
|
|
case CXL_BANK_SPARING:
|
|
if (!(validity_flags & CXL_DER_VALID_BANK_GROUP) ||
|
|
!(validity_flags & CXL_DER_VALID_BANK))
|
|
return NULL;
|
|
|
|
if (rec->media_hdr.channel == attrbs->channel &&
|
|
rec->media_hdr.rank == attrbs->rank &&
|
|
rec->bank_group == attrbs->bank_group &&
|
|
rec->bank == attrbs->bank &&
|
|
(!(validity_flags & CXL_DER_VALID_NIBBLE) ||
|
|
get_unaligned_le24(rec->nibble_mask) ==
|
|
attrbs->nibble_mask))
|
|
return rec;
|
|
break;
|
|
case CXL_RANK_SPARING:
|
|
if (rec->media_hdr.channel == attrbs->channel &&
|
|
rec->media_hdr.rank == attrbs->rank &&
|
|
(!(validity_flags & CXL_DER_VALID_NIBBLE) ||
|
|
get_unaligned_le24(rec->nibble_mask) ==
|
|
attrbs->nibble_mask))
|
|
return rec;
|
|
break;
|
|
default:
|
|
return NULL;
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
#define CXL_MAX_STORAGE_DAYS 10
|
|
#define CXL_MAX_STORAGE_TIME_SECS (CXL_MAX_STORAGE_DAYS * 24 * 60 * 60)
|
|
|
|
static void cxl_del_expired_gmedia_recs(struct xarray *rec_xarray,
|
|
struct cxl_event_gen_media *cur_rec)
|
|
{
|
|
u64 cur_ts = le64_to_cpu(cur_rec->media_hdr.hdr.timestamp);
|
|
struct cxl_event_gen_media *rec;
|
|
unsigned long index;
|
|
u64 delta_ts_secs;
|
|
|
|
xa_for_each(rec_xarray, index, rec) {
|
|
delta_ts_secs = (cur_ts -
|
|
le64_to_cpu(rec->media_hdr.hdr.timestamp)) / 1000000000ULL;
|
|
if (delta_ts_secs >= CXL_MAX_STORAGE_TIME_SECS) {
|
|
xa_erase(rec_xarray, index);
|
|
kfree(rec);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void cxl_del_expired_dram_recs(struct xarray *rec_xarray,
|
|
struct cxl_event_dram *cur_rec)
|
|
{
|
|
u64 cur_ts = le64_to_cpu(cur_rec->media_hdr.hdr.timestamp);
|
|
struct cxl_event_dram *rec;
|
|
unsigned long index;
|
|
u64 delta_secs;
|
|
|
|
xa_for_each(rec_xarray, index, rec) {
|
|
delta_secs = (cur_ts -
|
|
le64_to_cpu(rec->media_hdr.hdr.timestamp)) / 1000000000ULL;
|
|
if (delta_secs >= CXL_MAX_STORAGE_TIME_SECS) {
|
|
xa_erase(rec_xarray, index);
|
|
kfree(rec);
|
|
}
|
|
}
|
|
}
|
|
|
|
#define CXL_MAX_REC_STORAGE_COUNT 200
|
|
|
|
static void cxl_del_overflow_old_recs(struct xarray *rec_xarray)
|
|
{
|
|
void *err_rec;
|
|
unsigned long index, count = 0;
|
|
|
|
xa_for_each(rec_xarray, index, err_rec)
|
|
count++;
|
|
|
|
if (count <= CXL_MAX_REC_STORAGE_COUNT)
|
|
return;
|
|
|
|
count -= CXL_MAX_REC_STORAGE_COUNT;
|
|
xa_for_each(rec_xarray, index, err_rec) {
|
|
xa_erase(rec_xarray, index);
|
|
kfree(err_rec);
|
|
count--;
|
|
if (!count)
|
|
break;
|
|
}
|
|
}
|
|
|
|
int cxl_store_rec_gen_media(struct cxl_memdev *cxlmd, union cxl_event *evt)
|
|
{
|
|
struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
|
|
struct cxl_event_gen_media *rec;
|
|
void *old_rec;
|
|
|
|
if (!IS_ENABLED(CONFIG_CXL_EDAC_MEM_REPAIR) || !array_rec)
|
|
return 0;
|
|
|
|
rec = kmemdup(&evt->gen_media, sizeof(*rec), GFP_KERNEL);
|
|
if (!rec)
|
|
return -ENOMEM;
|
|
|
|
old_rec = xa_store(&array_rec->rec_gen_media,
|
|
le64_to_cpu(rec->media_hdr.phys_addr), rec,
|
|
GFP_KERNEL);
|
|
if (xa_is_err(old_rec)) {
|
|
kfree(rec);
|
|
return xa_err(old_rec);
|
|
}
|
|
|
|
kfree(old_rec);
|
|
|
|
cxl_del_expired_gmedia_recs(&array_rec->rec_gen_media, rec);
|
|
cxl_del_overflow_old_recs(&array_rec->rec_gen_media);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(cxl_store_rec_gen_media, "CXL");
|
|
|
|
int cxl_store_rec_dram(struct cxl_memdev *cxlmd, union cxl_event *evt)
|
|
{
|
|
struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
|
|
struct cxl_event_dram *rec;
|
|
void *old_rec;
|
|
|
|
if (!IS_ENABLED(CONFIG_CXL_EDAC_MEM_REPAIR) || !array_rec)
|
|
return 0;
|
|
|
|
rec = kmemdup(&evt->dram, sizeof(*rec), GFP_KERNEL);
|
|
if (!rec)
|
|
return -ENOMEM;
|
|
|
|
old_rec = xa_store(&array_rec->rec_dram,
|
|
le64_to_cpu(rec->media_hdr.phys_addr), rec,
|
|
GFP_KERNEL);
|
|
if (xa_is_err(old_rec)) {
|
|
kfree(rec);
|
|
return xa_err(old_rec);
|
|
}
|
|
|
|
kfree(old_rec);
|
|
|
|
cxl_del_expired_dram_recs(&array_rec->rec_dram, rec);
|
|
cxl_del_overflow_old_recs(&array_rec->rec_dram);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(cxl_store_rec_dram, "CXL");
|
|
|
|
static bool cxl_is_memdev_memory_online(const struct cxl_memdev *cxlmd)
|
|
{
|
|
struct cxl_port *port = cxlmd->endpoint;
|
|
|
|
if (port && cxl_num_decoders_committed(port))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* CXL memory sparing control
|
|
*/
|
|
enum cxl_mem_sparing_granularity {
|
|
CXL_MEM_SPARING_CACHELINE,
|
|
CXL_MEM_SPARING_ROW,
|
|
CXL_MEM_SPARING_BANK,
|
|
CXL_MEM_SPARING_RANK,
|
|
CXL_MEM_SPARING_MAX
|
|
};
|
|
|
|
struct cxl_mem_sparing_context {
|
|
struct cxl_memdev *cxlmd;
|
|
uuid_t repair_uuid;
|
|
u16 get_feat_size;
|
|
u16 set_feat_size;
|
|
u16 effects;
|
|
u8 instance;
|
|
u8 get_version;
|
|
u8 set_version;
|
|
u8 op_class;
|
|
u8 op_subclass;
|
|
bool cap_safe_when_in_use;
|
|
bool cap_hard_sparing;
|
|
bool cap_soft_sparing;
|
|
u8 channel;
|
|
u8 rank;
|
|
u8 bank_group;
|
|
u32 nibble_mask;
|
|
u64 dpa;
|
|
u32 row;
|
|
u16 column;
|
|
u8 bank;
|
|
u8 sub_channel;
|
|
enum edac_mem_repair_type repair_type;
|
|
bool persist_mode;
|
|
};
|
|
|
|
#define CXL_SPARING_RD_CAP_SAFE_IN_USE_MASK BIT(0)
|
|
#define CXL_SPARING_RD_CAP_HARD_SPARING_MASK BIT(1)
|
|
#define CXL_SPARING_RD_CAP_SOFT_SPARING_MASK BIT(2)
|
|
|
|
#define CXL_SPARING_WR_DEVICE_INITIATED_MASK BIT(0)
|
|
|
|
#define CXL_SPARING_QUERY_RESOURCE_FLAG BIT(0)
|
|
#define CXL_SET_HARD_SPARING_FLAG BIT(1)
|
|
#define CXL_SPARING_SUB_CHNL_VALID_FLAG BIT(2)
|
|
#define CXL_SPARING_NIB_MASK_VALID_FLAG BIT(3)
|
|
|
|
#define CXL_GET_SPARING_SAFE_IN_USE(flags) \
|
|
(FIELD_GET(CXL_SPARING_RD_CAP_SAFE_IN_USE_MASK, \
|
|
flags) ^ 1)
|
|
#define CXL_GET_CAP_HARD_SPARING(flags) \
|
|
FIELD_GET(CXL_SPARING_RD_CAP_HARD_SPARING_MASK, \
|
|
flags)
|
|
#define CXL_GET_CAP_SOFT_SPARING(flags) \
|
|
FIELD_GET(CXL_SPARING_RD_CAP_SOFT_SPARING_MASK, \
|
|
flags)
|
|
|
|
#define CXL_SET_SPARING_QUERY_RESOURCE(val) \
|
|
FIELD_PREP(CXL_SPARING_QUERY_RESOURCE_FLAG, val)
|
|
#define CXL_SET_HARD_SPARING(val) \
|
|
FIELD_PREP(CXL_SET_HARD_SPARING_FLAG, val)
|
|
#define CXL_SET_SPARING_SUB_CHNL_VALID(val) \
|
|
FIELD_PREP(CXL_SPARING_SUB_CHNL_VALID_FLAG, val)
|
|
#define CXL_SET_SPARING_NIB_MASK_VALID(val) \
|
|
FIELD_PREP(CXL_SPARING_NIB_MASK_VALID_FLAG, val)
|
|
|
|
/*
|
|
* See CXL spec rev 3.2 @8.2.10.7.2.3 Table 8-134 Memory Sparing Feature
|
|
* Readable Attributes.
|
|
*/
|
|
struct cxl_memdev_repair_rd_attrbs_hdr {
|
|
u8 max_op_latency;
|
|
__le16 op_cap;
|
|
__le16 op_mode;
|
|
u8 op_class;
|
|
u8 op_subclass;
|
|
u8 rsvd[9];
|
|
} __packed;
|
|
|
|
struct cxl_memdev_sparing_rd_attrbs {
|
|
struct cxl_memdev_repair_rd_attrbs_hdr hdr;
|
|
u8 rsvd;
|
|
__le16 restriction_flags;
|
|
} __packed;
|
|
|
|
/*
|
|
* See CXL spec rev 3.2 @8.2.10.7.1.4 Table 8-120 Memory Sparing Input Payload.
|
|
*/
|
|
struct cxl_memdev_sparing_in_payload {
|
|
u8 flags;
|
|
u8 channel;
|
|
u8 rank;
|
|
u8 nibble_mask[3];
|
|
u8 bank_group;
|
|
u8 bank;
|
|
u8 row[3];
|
|
__le16 column;
|
|
u8 sub_channel;
|
|
} __packed;
|
|
|
|
static int
|
|
cxl_mem_sparing_get_attrbs(struct cxl_mem_sparing_context *cxl_sparing_ctx)
|
|
{
|
|
size_t rd_data_size = sizeof(struct cxl_memdev_sparing_rd_attrbs);
|
|
struct cxl_memdev *cxlmd = cxl_sparing_ctx->cxlmd;
|
|
struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
u16 restriction_flags;
|
|
size_t data_size;
|
|
u16 return_code;
|
|
struct cxl_memdev_sparing_rd_attrbs *rd_attrbs __free(kfree) =
|
|
kzalloc(rd_data_size, GFP_KERNEL);
|
|
if (!rd_attrbs)
|
|
return -ENOMEM;
|
|
|
|
data_size = cxl_get_feature(cxl_mbox, &cxl_sparing_ctx->repair_uuid,
|
|
CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs,
|
|
rd_data_size, 0, &return_code);
|
|
if (!data_size)
|
|
return -EIO;
|
|
|
|
cxl_sparing_ctx->op_class = rd_attrbs->hdr.op_class;
|
|
cxl_sparing_ctx->op_subclass = rd_attrbs->hdr.op_subclass;
|
|
restriction_flags = le16_to_cpu(rd_attrbs->restriction_flags);
|
|
cxl_sparing_ctx->cap_safe_when_in_use =
|
|
CXL_GET_SPARING_SAFE_IN_USE(restriction_flags);
|
|
cxl_sparing_ctx->cap_hard_sparing =
|
|
CXL_GET_CAP_HARD_SPARING(restriction_flags);
|
|
cxl_sparing_ctx->cap_soft_sparing =
|
|
CXL_GET_CAP_SOFT_SPARING(restriction_flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct cxl_event_dram *
|
|
cxl_mem_get_rec_dram(struct cxl_memdev *cxlmd,
|
|
struct cxl_mem_sparing_context *ctx)
|
|
{
|
|
struct cxl_mem_repair_attrbs attrbs = { 0 };
|
|
|
|
attrbs.dpa = ctx->dpa;
|
|
attrbs.channel = ctx->channel;
|
|
attrbs.rank = ctx->rank;
|
|
attrbs.nibble_mask = ctx->nibble_mask;
|
|
switch (ctx->repair_type) {
|
|
case EDAC_REPAIR_CACHELINE_SPARING:
|
|
attrbs.repair_type = CXL_CACHELINE_SPARING;
|
|
attrbs.bank_group = ctx->bank_group;
|
|
attrbs.bank = ctx->bank;
|
|
attrbs.row = ctx->row;
|
|
attrbs.column = ctx->column;
|
|
attrbs.sub_channel = ctx->sub_channel;
|
|
break;
|
|
case EDAC_REPAIR_ROW_SPARING:
|
|
attrbs.repair_type = CXL_ROW_SPARING;
|
|
attrbs.bank_group = ctx->bank_group;
|
|
attrbs.bank = ctx->bank;
|
|
attrbs.row = ctx->row;
|
|
break;
|
|
case EDAC_REPAIR_BANK_SPARING:
|
|
attrbs.repair_type = CXL_BANK_SPARING;
|
|
attrbs.bank_group = ctx->bank_group;
|
|
attrbs.bank = ctx->bank;
|
|
break;
|
|
case EDAC_REPAIR_RANK_SPARING:
|
|
attrbs.repair_type = CXL_RANK_SPARING;
|
|
break;
|
|
default:
|
|
return NULL;
|
|
}
|
|
|
|
return cxl_find_rec_dram(cxlmd, &attrbs);
|
|
}
|
|
|
|
static int
|
|
cxl_mem_perform_sparing(struct device *dev,
|
|
struct cxl_mem_sparing_context *cxl_sparing_ctx)
|
|
{
|
|
struct cxl_memdev *cxlmd = cxl_sparing_ctx->cxlmd;
|
|
struct cxl_memdev_sparing_in_payload sparing_pi;
|
|
struct cxl_event_dram *rec = NULL;
|
|
u16 validity_flags = 0;
|
|
int ret;
|
|
|
|
ACQUIRE(rwsem_read_intr, region_rwsem)(&cxl_rwsem.region);
|
|
if ((ret = ACQUIRE_ERR(rwsem_read_intr, ®ion_rwsem)))
|
|
return ret;
|
|
|
|
ACQUIRE(rwsem_read_intr, dpa_rwsem)(&cxl_rwsem.dpa);
|
|
if ((ret = ACQUIRE_ERR(rwsem_read_intr, &dpa_rwsem)))
|
|
return ret;
|
|
|
|
if (!cxl_sparing_ctx->cap_safe_when_in_use) {
|
|
/* Memory to repair must be offline */
|
|
if (cxl_is_memdev_memory_online(cxlmd))
|
|
return -EBUSY;
|
|
} else {
|
|
if (cxl_is_memdev_memory_online(cxlmd)) {
|
|
rec = cxl_mem_get_rec_dram(cxlmd, cxl_sparing_ctx);
|
|
if (!rec)
|
|
return -EINVAL;
|
|
|
|
if (!get_unaligned_le16(rec->media_hdr.validity_flags))
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
memset(&sparing_pi, 0, sizeof(sparing_pi));
|
|
sparing_pi.flags = CXL_SET_SPARING_QUERY_RESOURCE(0);
|
|
if (cxl_sparing_ctx->persist_mode)
|
|
sparing_pi.flags |= CXL_SET_HARD_SPARING(1);
|
|
|
|
if (rec)
|
|
validity_flags = get_unaligned_le16(rec->media_hdr.validity_flags);
|
|
|
|
switch (cxl_sparing_ctx->repair_type) {
|
|
case EDAC_REPAIR_CACHELINE_SPARING:
|
|
sparing_pi.column = cpu_to_le16(cxl_sparing_ctx->column);
|
|
if (!rec || (validity_flags & CXL_DER_VALID_SUB_CHANNEL)) {
|
|
sparing_pi.flags |= CXL_SET_SPARING_SUB_CHNL_VALID(1);
|
|
sparing_pi.sub_channel = cxl_sparing_ctx->sub_channel;
|
|
}
|
|
fallthrough;
|
|
case EDAC_REPAIR_ROW_SPARING:
|
|
put_unaligned_le24(cxl_sparing_ctx->row, sparing_pi.row);
|
|
fallthrough;
|
|
case EDAC_REPAIR_BANK_SPARING:
|
|
sparing_pi.bank_group = cxl_sparing_ctx->bank_group;
|
|
sparing_pi.bank = cxl_sparing_ctx->bank;
|
|
fallthrough;
|
|
case EDAC_REPAIR_RANK_SPARING:
|
|
sparing_pi.rank = cxl_sparing_ctx->rank;
|
|
fallthrough;
|
|
default:
|
|
sparing_pi.channel = cxl_sparing_ctx->channel;
|
|
if ((rec && (validity_flags & CXL_DER_VALID_NIBBLE)) ||
|
|
(!rec && (!cxl_sparing_ctx->nibble_mask ||
|
|
(cxl_sparing_ctx->nibble_mask & 0xFFFFFF)))) {
|
|
sparing_pi.flags |= CXL_SET_SPARING_NIB_MASK_VALID(1);
|
|
put_unaligned_le24(cxl_sparing_ctx->nibble_mask,
|
|
sparing_pi.nibble_mask);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return cxl_perform_maintenance(&cxlmd->cxlds->cxl_mbox,
|
|
cxl_sparing_ctx->op_class,
|
|
cxl_sparing_ctx->op_subclass,
|
|
&sparing_pi, sizeof(sparing_pi));
|
|
}
|
|
|
|
static int cxl_mem_sparing_get_repair_type(struct device *dev, void *drv_data,
|
|
const char **repair_type)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
|
|
switch (ctx->repair_type) {
|
|
case EDAC_REPAIR_CACHELINE_SPARING:
|
|
case EDAC_REPAIR_ROW_SPARING:
|
|
case EDAC_REPAIR_BANK_SPARING:
|
|
case EDAC_REPAIR_RANK_SPARING:
|
|
*repair_type = edac_repair_type[ctx->repair_type];
|
|
break;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#define CXL_SPARING_GET_ATTR(attrb, data_type) \
|
|
static int cxl_mem_sparing_get_##attrb( \
|
|
struct device *dev, void *drv_data, data_type *val) \
|
|
{ \
|
|
struct cxl_mem_sparing_context *ctx = drv_data; \
|
|
\
|
|
*val = ctx->attrb; \
|
|
\
|
|
return 0; \
|
|
}
|
|
CXL_SPARING_GET_ATTR(persist_mode, bool)
|
|
CXL_SPARING_GET_ATTR(dpa, u64)
|
|
CXL_SPARING_GET_ATTR(nibble_mask, u32)
|
|
CXL_SPARING_GET_ATTR(bank_group, u32)
|
|
CXL_SPARING_GET_ATTR(bank, u32)
|
|
CXL_SPARING_GET_ATTR(rank, u32)
|
|
CXL_SPARING_GET_ATTR(row, u32)
|
|
CXL_SPARING_GET_ATTR(column, u32)
|
|
CXL_SPARING_GET_ATTR(channel, u32)
|
|
CXL_SPARING_GET_ATTR(sub_channel, u32)
|
|
|
|
#define CXL_SPARING_SET_ATTR(attrb, data_type) \
|
|
static int cxl_mem_sparing_set_##attrb(struct device *dev, \
|
|
void *drv_data, data_type val) \
|
|
{ \
|
|
struct cxl_mem_sparing_context *ctx = drv_data; \
|
|
\
|
|
ctx->attrb = val; \
|
|
\
|
|
return 0; \
|
|
}
|
|
CXL_SPARING_SET_ATTR(nibble_mask, u32)
|
|
CXL_SPARING_SET_ATTR(bank_group, u32)
|
|
CXL_SPARING_SET_ATTR(bank, u32)
|
|
CXL_SPARING_SET_ATTR(rank, u32)
|
|
CXL_SPARING_SET_ATTR(row, u32)
|
|
CXL_SPARING_SET_ATTR(column, u32)
|
|
CXL_SPARING_SET_ATTR(channel, u32)
|
|
CXL_SPARING_SET_ATTR(sub_channel, u32)
|
|
|
|
static int cxl_mem_sparing_set_persist_mode(struct device *dev, void *drv_data,
|
|
bool persist_mode)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
|
|
if ((persist_mode && ctx->cap_hard_sparing) ||
|
|
(!persist_mode && ctx->cap_soft_sparing))
|
|
ctx->persist_mode = persist_mode;
|
|
else
|
|
return -EOPNOTSUPP;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_get_mem_sparing_safe_when_in_use(struct device *dev,
|
|
void *drv_data, bool *safe)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
|
|
*safe = ctx->cap_safe_when_in_use;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_mem_sparing_get_min_dpa(struct device *dev, void *drv_data,
|
|
u64 *min_dpa)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
*min_dpa = cxlds->dpa_res.start;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_mem_sparing_get_max_dpa(struct device *dev, void *drv_data,
|
|
u64 *max_dpa)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
*max_dpa = cxlds->dpa_res.end;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_mem_sparing_set_dpa(struct device *dev, void *drv_data, u64 dpa)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
if (!cxl_resource_contains_addr(&cxlds->dpa_res, dpa))
|
|
return -EINVAL;
|
|
|
|
ctx->dpa = dpa;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_do_mem_sparing(struct device *dev, void *drv_data, u32 val)
|
|
{
|
|
struct cxl_mem_sparing_context *ctx = drv_data;
|
|
|
|
if (val != EDAC_DO_MEM_REPAIR)
|
|
return -EINVAL;
|
|
|
|
return cxl_mem_perform_sparing(dev, ctx);
|
|
}
|
|
|
|
#define RANK_OPS \
|
|
.get_repair_type = cxl_mem_sparing_get_repair_type, \
|
|
.get_persist_mode = cxl_mem_sparing_get_persist_mode, \
|
|
.set_persist_mode = cxl_mem_sparing_set_persist_mode, \
|
|
.get_repair_safe_when_in_use = cxl_get_mem_sparing_safe_when_in_use, \
|
|
.get_min_dpa = cxl_mem_sparing_get_min_dpa, \
|
|
.get_max_dpa = cxl_mem_sparing_get_max_dpa, \
|
|
.get_dpa = cxl_mem_sparing_get_dpa, \
|
|
.set_dpa = cxl_mem_sparing_set_dpa, \
|
|
.get_nibble_mask = cxl_mem_sparing_get_nibble_mask, \
|
|
.set_nibble_mask = cxl_mem_sparing_set_nibble_mask, \
|
|
.get_rank = cxl_mem_sparing_get_rank, \
|
|
.set_rank = cxl_mem_sparing_set_rank, \
|
|
.get_channel = cxl_mem_sparing_get_channel, \
|
|
.set_channel = cxl_mem_sparing_set_channel, \
|
|
.do_repair = cxl_do_mem_sparing
|
|
|
|
#define BANK_OPS \
|
|
RANK_OPS, .get_bank_group = cxl_mem_sparing_get_bank_group, \
|
|
.set_bank_group = cxl_mem_sparing_set_bank_group, \
|
|
.get_bank = cxl_mem_sparing_get_bank, \
|
|
.set_bank = cxl_mem_sparing_set_bank
|
|
|
|
#define ROW_OPS \
|
|
BANK_OPS, .get_row = cxl_mem_sparing_get_row, \
|
|
.set_row = cxl_mem_sparing_set_row
|
|
|
|
#define CACHELINE_OPS \
|
|
ROW_OPS, .get_column = cxl_mem_sparing_get_column, \
|
|
.set_column = cxl_mem_sparing_set_column, \
|
|
.get_sub_channel = cxl_mem_sparing_get_sub_channel, \
|
|
.set_sub_channel = cxl_mem_sparing_set_sub_channel
|
|
|
|
static const struct edac_mem_repair_ops cxl_rank_sparing_ops = {
|
|
RANK_OPS,
|
|
};
|
|
|
|
static const struct edac_mem_repair_ops cxl_bank_sparing_ops = {
|
|
BANK_OPS,
|
|
};
|
|
|
|
static const struct edac_mem_repair_ops cxl_row_sparing_ops = {
|
|
ROW_OPS,
|
|
};
|
|
|
|
static const struct edac_mem_repair_ops cxl_cacheline_sparing_ops = {
|
|
CACHELINE_OPS,
|
|
};
|
|
|
|
struct cxl_mem_sparing_desc {
|
|
const uuid_t repair_uuid;
|
|
enum edac_mem_repair_type repair_type;
|
|
const struct edac_mem_repair_ops *repair_ops;
|
|
};
|
|
|
|
static const struct cxl_mem_sparing_desc mem_sparing_desc[] = {
|
|
{
|
|
.repair_uuid = CXL_FEAT_CACHELINE_SPARING_UUID,
|
|
.repair_type = EDAC_REPAIR_CACHELINE_SPARING,
|
|
.repair_ops = &cxl_cacheline_sparing_ops,
|
|
},
|
|
{
|
|
.repair_uuid = CXL_FEAT_ROW_SPARING_UUID,
|
|
.repair_type = EDAC_REPAIR_ROW_SPARING,
|
|
.repair_ops = &cxl_row_sparing_ops,
|
|
},
|
|
{
|
|
.repair_uuid = CXL_FEAT_BANK_SPARING_UUID,
|
|
.repair_type = EDAC_REPAIR_BANK_SPARING,
|
|
.repair_ops = &cxl_bank_sparing_ops,
|
|
},
|
|
{
|
|
.repair_uuid = CXL_FEAT_RANK_SPARING_UUID,
|
|
.repair_type = EDAC_REPAIR_RANK_SPARING,
|
|
.repair_ops = &cxl_rank_sparing_ops,
|
|
},
|
|
};
|
|
|
|
static int cxl_memdev_sparing_init(struct cxl_memdev *cxlmd,
|
|
struct edac_dev_feature *ras_feature,
|
|
const struct cxl_mem_sparing_desc *desc,
|
|
u8 repair_inst)
|
|
{
|
|
struct cxl_mem_sparing_context *cxl_sparing_ctx;
|
|
struct cxl_feat_entry *feat_entry;
|
|
int ret;
|
|
|
|
feat_entry = cxl_feature_info(to_cxlfs(cxlmd->cxlds),
|
|
&desc->repair_uuid);
|
|
if (IS_ERR(feat_entry))
|
|
return -EOPNOTSUPP;
|
|
|
|
if (!(le32_to_cpu(feat_entry->flags) & CXL_FEATURE_F_CHANGEABLE))
|
|
return -EOPNOTSUPP;
|
|
|
|
cxl_sparing_ctx = devm_kzalloc(&cxlmd->dev, sizeof(*cxl_sparing_ctx),
|
|
GFP_KERNEL);
|
|
if (!cxl_sparing_ctx)
|
|
return -ENOMEM;
|
|
|
|
*cxl_sparing_ctx = (struct cxl_mem_sparing_context){
|
|
.get_feat_size = le16_to_cpu(feat_entry->get_feat_size),
|
|
.set_feat_size = le16_to_cpu(feat_entry->set_feat_size),
|
|
.get_version = feat_entry->get_feat_ver,
|
|
.set_version = feat_entry->set_feat_ver,
|
|
.effects = le16_to_cpu(feat_entry->effects),
|
|
.cxlmd = cxlmd,
|
|
.repair_type = desc->repair_type,
|
|
.instance = repair_inst++,
|
|
};
|
|
uuid_copy(&cxl_sparing_ctx->repair_uuid, &desc->repair_uuid);
|
|
|
|
ret = cxl_mem_sparing_get_attrbs(cxl_sparing_ctx);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if ((cxl_sparing_ctx->cap_soft_sparing &&
|
|
cxl_sparing_ctx->cap_hard_sparing) ||
|
|
cxl_sparing_ctx->cap_soft_sparing)
|
|
cxl_sparing_ctx->persist_mode = 0;
|
|
else if (cxl_sparing_ctx->cap_hard_sparing)
|
|
cxl_sparing_ctx->persist_mode = 1;
|
|
else
|
|
return -EOPNOTSUPP;
|
|
|
|
ras_feature->ft_type = RAS_FEAT_MEM_REPAIR;
|
|
ras_feature->instance = cxl_sparing_ctx->instance;
|
|
ras_feature->mem_repair_ops = desc->repair_ops;
|
|
ras_feature->ctx = cxl_sparing_ctx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* CXL memory soft PPR & hard PPR control
|
|
*/
|
|
struct cxl_ppr_context {
|
|
uuid_t repair_uuid;
|
|
u8 instance;
|
|
u16 get_feat_size;
|
|
u16 set_feat_size;
|
|
u8 get_version;
|
|
u8 set_version;
|
|
u16 effects;
|
|
u8 op_class;
|
|
u8 op_subclass;
|
|
bool cap_dpa;
|
|
bool cap_nib_mask;
|
|
bool media_accessible;
|
|
bool data_retained;
|
|
struct cxl_memdev *cxlmd;
|
|
enum edac_mem_repair_type repair_type;
|
|
bool persist_mode;
|
|
u64 dpa;
|
|
u32 nibble_mask;
|
|
};
|
|
|
|
/*
|
|
* See CXL rev 3.2 @8.2.10.7.2.1 Table 8-128 sPPR Feature Readable Attributes
|
|
*
|
|
* See CXL rev 3.2 @8.2.10.7.2.2 Table 8-131 hPPR Feature Readable Attributes
|
|
*/
|
|
|
|
#define CXL_PPR_OP_CAP_DEVICE_INITIATED BIT(0)
|
|
#define CXL_PPR_OP_MODE_DEV_INITIATED BIT(0)
|
|
|
|
#define CXL_PPR_FLAG_DPA_SUPPORT_MASK BIT(0)
|
|
#define CXL_PPR_FLAG_NIB_SUPPORT_MASK BIT(1)
|
|
#define CXL_PPR_FLAG_MEM_SPARING_EV_REC_SUPPORT_MASK BIT(2)
|
|
#define CXL_PPR_FLAG_DEV_INITED_PPR_AT_BOOT_CAP_MASK BIT(3)
|
|
|
|
#define CXL_PPR_RESTRICTION_FLAG_MEDIA_ACCESSIBLE_MASK BIT(0)
|
|
#define CXL_PPR_RESTRICTION_FLAG_DATA_RETAINED_MASK BIT(2)
|
|
|
|
#define CXL_PPR_SPARING_EV_REC_EN_MASK BIT(0)
|
|
#define CXL_PPR_DEV_INITED_PPR_AT_BOOT_EN_MASK BIT(1)
|
|
|
|
#define CXL_PPR_GET_CAP_DPA(flags) \
|
|
FIELD_GET(CXL_PPR_FLAG_DPA_SUPPORT_MASK, flags)
|
|
#define CXL_PPR_GET_CAP_NIB_MASK(flags) \
|
|
FIELD_GET(CXL_PPR_FLAG_NIB_SUPPORT_MASK, flags)
|
|
#define CXL_PPR_GET_MEDIA_ACCESSIBLE(restriction_flags) \
|
|
(FIELD_GET(CXL_PPR_RESTRICTION_FLAG_MEDIA_ACCESSIBLE_MASK, \
|
|
restriction_flags) ^ 1)
|
|
#define CXL_PPR_GET_DATA_RETAINED(restriction_flags) \
|
|
(FIELD_GET(CXL_PPR_RESTRICTION_FLAG_DATA_RETAINED_MASK, \
|
|
restriction_flags) ^ 1)
|
|
|
|
struct cxl_memdev_ppr_rd_attrbs {
|
|
struct cxl_memdev_repair_rd_attrbs_hdr hdr;
|
|
u8 ppr_flags;
|
|
__le16 restriction_flags;
|
|
u8 ppr_op_mode;
|
|
} __packed;
|
|
|
|
/*
|
|
* See CXL rev 3.2 @8.2.10.7.1.2 Table 8-118 sPPR Maintenance Input Payload
|
|
*
|
|
* See CXL rev 3.2 @8.2.10.7.1.3 Table 8-119 hPPR Maintenance Input Payload
|
|
*/
|
|
struct cxl_memdev_ppr_maintenance_attrbs {
|
|
u8 flags;
|
|
__le64 dpa;
|
|
u8 nibble_mask[3];
|
|
} __packed;
|
|
|
|
static int cxl_mem_ppr_get_attrbs(struct cxl_ppr_context *cxl_ppr_ctx)
|
|
{
|
|
size_t rd_data_size = sizeof(struct cxl_memdev_ppr_rd_attrbs);
|
|
struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd;
|
|
struct cxl_mailbox *cxl_mbox = &cxlmd->cxlds->cxl_mbox;
|
|
u16 restriction_flags;
|
|
size_t data_size;
|
|
u16 return_code;
|
|
|
|
struct cxl_memdev_ppr_rd_attrbs *rd_attrbs __free(kfree) =
|
|
kmalloc(rd_data_size, GFP_KERNEL);
|
|
if (!rd_attrbs)
|
|
return -ENOMEM;
|
|
|
|
data_size = cxl_get_feature(cxl_mbox, &cxl_ppr_ctx->repair_uuid,
|
|
CXL_GET_FEAT_SEL_CURRENT_VALUE, rd_attrbs,
|
|
rd_data_size, 0, &return_code);
|
|
if (!data_size)
|
|
return -EIO;
|
|
|
|
cxl_ppr_ctx->op_class = rd_attrbs->hdr.op_class;
|
|
cxl_ppr_ctx->op_subclass = rd_attrbs->hdr.op_subclass;
|
|
cxl_ppr_ctx->cap_dpa = CXL_PPR_GET_CAP_DPA(rd_attrbs->ppr_flags);
|
|
cxl_ppr_ctx->cap_nib_mask =
|
|
CXL_PPR_GET_CAP_NIB_MASK(rd_attrbs->ppr_flags);
|
|
|
|
restriction_flags = le16_to_cpu(rd_attrbs->restriction_flags);
|
|
cxl_ppr_ctx->media_accessible =
|
|
CXL_PPR_GET_MEDIA_ACCESSIBLE(restriction_flags);
|
|
cxl_ppr_ctx->data_retained =
|
|
CXL_PPR_GET_DATA_RETAINED(restriction_flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_mem_perform_ppr(struct cxl_ppr_context *cxl_ppr_ctx)
|
|
{
|
|
struct cxl_memdev_ppr_maintenance_attrbs maintenance_attrbs;
|
|
struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd;
|
|
struct cxl_mem_repair_attrbs attrbs = { 0 };
|
|
int ret;
|
|
|
|
ACQUIRE(rwsem_read_intr, region_rwsem)(&cxl_rwsem.region);
|
|
if ((ret = ACQUIRE_ERR(rwsem_read_intr, ®ion_rwsem)))
|
|
return ret;
|
|
|
|
ACQUIRE(rwsem_read_intr, dpa_rwsem)(&cxl_rwsem.dpa);
|
|
if ((ret = ACQUIRE_ERR(rwsem_read_intr, &dpa_rwsem)))
|
|
return ret;
|
|
|
|
if (!cxl_ppr_ctx->media_accessible || !cxl_ppr_ctx->data_retained) {
|
|
/* Memory to repair must be offline */
|
|
if (cxl_is_memdev_memory_online(cxlmd))
|
|
return -EBUSY;
|
|
} else {
|
|
if (cxl_is_memdev_memory_online(cxlmd)) {
|
|
/* Check memory to repair is from the current boot */
|
|
attrbs.repair_type = CXL_PPR;
|
|
attrbs.dpa = cxl_ppr_ctx->dpa;
|
|
attrbs.nibble_mask = cxl_ppr_ctx->nibble_mask;
|
|
if (!cxl_find_rec_dram(cxlmd, &attrbs) &&
|
|
!cxl_find_rec_gen_media(cxlmd, &attrbs))
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
memset(&maintenance_attrbs, 0, sizeof(maintenance_attrbs));
|
|
maintenance_attrbs.flags = 0;
|
|
maintenance_attrbs.dpa = cpu_to_le64(cxl_ppr_ctx->dpa);
|
|
put_unaligned_le24(cxl_ppr_ctx->nibble_mask,
|
|
maintenance_attrbs.nibble_mask);
|
|
|
|
return cxl_perform_maintenance(&cxlmd->cxlds->cxl_mbox,
|
|
cxl_ppr_ctx->op_class,
|
|
cxl_ppr_ctx->op_subclass,
|
|
&maintenance_attrbs,
|
|
sizeof(maintenance_attrbs));
|
|
}
|
|
|
|
static int cxl_ppr_get_repair_type(struct device *dev, void *drv_data,
|
|
const char **repair_type)
|
|
{
|
|
*repair_type = edac_repair_type[EDAC_REPAIR_PPR];
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_get_persist_mode(struct device *dev, void *drv_data,
|
|
bool *persist_mode)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
|
|
*persist_mode = cxl_ppr_ctx->persist_mode;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_get_ppr_safe_when_in_use(struct device *dev, void *drv_data,
|
|
bool *safe)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
|
|
*safe = cxl_ppr_ctx->media_accessible & cxl_ppr_ctx->data_retained;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_get_min_dpa(struct device *dev, void *drv_data, u64 *min_dpa)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
*min_dpa = cxlds->dpa_res.start;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_get_max_dpa(struct device *dev, void *drv_data, u64 *max_dpa)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
*max_dpa = cxlds->dpa_res.end;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_get_dpa(struct device *dev, void *drv_data, u64 *dpa)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
|
|
*dpa = cxl_ppr_ctx->dpa;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_set_dpa(struct device *dev, void *drv_data, u64 dpa)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
if (!cxl_resource_contains_addr(&cxlds->dpa_res, dpa))
|
|
return -EINVAL;
|
|
|
|
cxl_ppr_ctx->dpa = dpa;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_get_nibble_mask(struct device *dev, void *drv_data,
|
|
u32 *nibble_mask)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
|
|
*nibble_mask = cxl_ppr_ctx->nibble_mask;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_ppr_set_nibble_mask(struct device *dev, void *drv_data,
|
|
u32 nibble_mask)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
|
|
cxl_ppr_ctx->nibble_mask = nibble_mask;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cxl_do_ppr(struct device *dev, void *drv_data, u32 val)
|
|
{
|
|
struct cxl_ppr_context *cxl_ppr_ctx = drv_data;
|
|
struct cxl_memdev *cxlmd = cxl_ppr_ctx->cxlmd;
|
|
struct cxl_dev_state *cxlds = cxlmd->cxlds;
|
|
|
|
if (val != EDAC_DO_MEM_REPAIR ||
|
|
!cxl_resource_contains_addr(&cxlds->dpa_res, cxl_ppr_ctx->dpa))
|
|
return -EINVAL;
|
|
|
|
return cxl_mem_perform_ppr(cxl_ppr_ctx);
|
|
}
|
|
|
|
static const struct edac_mem_repair_ops cxl_sppr_ops = {
|
|
.get_repair_type = cxl_ppr_get_repair_type,
|
|
.get_persist_mode = cxl_ppr_get_persist_mode,
|
|
.get_repair_safe_when_in_use = cxl_get_ppr_safe_when_in_use,
|
|
.get_min_dpa = cxl_ppr_get_min_dpa,
|
|
.get_max_dpa = cxl_ppr_get_max_dpa,
|
|
.get_dpa = cxl_ppr_get_dpa,
|
|
.set_dpa = cxl_ppr_set_dpa,
|
|
.get_nibble_mask = cxl_ppr_get_nibble_mask,
|
|
.set_nibble_mask = cxl_ppr_set_nibble_mask,
|
|
.do_repair = cxl_do_ppr,
|
|
};
|
|
|
|
static int cxl_memdev_soft_ppr_init(struct cxl_memdev *cxlmd,
|
|
struct edac_dev_feature *ras_feature,
|
|
u8 repair_inst)
|
|
{
|
|
struct cxl_ppr_context *cxl_sppr_ctx;
|
|
struct cxl_feat_entry *feat_entry;
|
|
int ret;
|
|
|
|
feat_entry = cxl_feature_info(to_cxlfs(cxlmd->cxlds),
|
|
&CXL_FEAT_SPPR_UUID);
|
|
if (IS_ERR(feat_entry))
|
|
return -EOPNOTSUPP;
|
|
|
|
if (!(le32_to_cpu(feat_entry->flags) & CXL_FEATURE_F_CHANGEABLE))
|
|
return -EOPNOTSUPP;
|
|
|
|
cxl_sppr_ctx =
|
|
devm_kzalloc(&cxlmd->dev, sizeof(*cxl_sppr_ctx), GFP_KERNEL);
|
|
if (!cxl_sppr_ctx)
|
|
return -ENOMEM;
|
|
|
|
*cxl_sppr_ctx = (struct cxl_ppr_context){
|
|
.get_feat_size = le16_to_cpu(feat_entry->get_feat_size),
|
|
.set_feat_size = le16_to_cpu(feat_entry->set_feat_size),
|
|
.get_version = feat_entry->get_feat_ver,
|
|
.set_version = feat_entry->set_feat_ver,
|
|
.effects = le16_to_cpu(feat_entry->effects),
|
|
.cxlmd = cxlmd,
|
|
.repair_type = EDAC_REPAIR_PPR,
|
|
.persist_mode = 0,
|
|
.instance = repair_inst,
|
|
};
|
|
uuid_copy(&cxl_sppr_ctx->repair_uuid, &CXL_FEAT_SPPR_UUID);
|
|
|
|
ret = cxl_mem_ppr_get_attrbs(cxl_sppr_ctx);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ras_feature->ft_type = RAS_FEAT_MEM_REPAIR;
|
|
ras_feature->instance = cxl_sppr_ctx->instance;
|
|
ras_feature->mem_repair_ops = &cxl_sppr_ops;
|
|
ras_feature->ctx = cxl_sppr_ctx;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int devm_cxl_memdev_edac_register(struct cxl_memdev *cxlmd)
|
|
{
|
|
struct edac_dev_feature ras_features[CXL_NR_EDAC_DEV_FEATURES];
|
|
int num_ras_features = 0;
|
|
u8 repair_inst = 0;
|
|
int rc;
|
|
|
|
if (IS_ENABLED(CONFIG_CXL_EDAC_SCRUB)) {
|
|
rc = cxl_memdev_scrub_init(cxlmd, &ras_features[num_ras_features], 0);
|
|
if (rc < 0 && rc != -EOPNOTSUPP)
|
|
return rc;
|
|
|
|
if (rc != -EOPNOTSUPP)
|
|
num_ras_features++;
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_CXL_EDAC_ECS)) {
|
|
rc = cxl_memdev_ecs_init(cxlmd, &ras_features[num_ras_features]);
|
|
if (rc < 0 && rc != -EOPNOTSUPP)
|
|
return rc;
|
|
|
|
if (rc != -EOPNOTSUPP)
|
|
num_ras_features++;
|
|
}
|
|
|
|
if (IS_ENABLED(CONFIG_CXL_EDAC_MEM_REPAIR)) {
|
|
for (int i = 0; i < CXL_MEM_SPARING_MAX; i++) {
|
|
rc = cxl_memdev_sparing_init(cxlmd,
|
|
&ras_features[num_ras_features],
|
|
&mem_sparing_desc[i], repair_inst);
|
|
if (rc == -EOPNOTSUPP)
|
|
continue;
|
|
if (rc < 0)
|
|
return rc;
|
|
|
|
repair_inst++;
|
|
num_ras_features++;
|
|
}
|
|
|
|
rc = cxl_memdev_soft_ppr_init(cxlmd, &ras_features[num_ras_features],
|
|
repair_inst);
|
|
if (rc < 0 && rc != -EOPNOTSUPP)
|
|
return rc;
|
|
|
|
if (rc != -EOPNOTSUPP) {
|
|
repair_inst++;
|
|
num_ras_features++;
|
|
}
|
|
|
|
if (repair_inst) {
|
|
struct cxl_mem_err_rec *array_rec =
|
|
devm_kzalloc(&cxlmd->dev, sizeof(*array_rec),
|
|
GFP_KERNEL);
|
|
if (!array_rec)
|
|
return -ENOMEM;
|
|
|
|
xa_init(&array_rec->rec_gen_media);
|
|
xa_init(&array_rec->rec_dram);
|
|
cxlmd->err_rec_array = array_rec;
|
|
}
|
|
}
|
|
|
|
if (!num_ras_features)
|
|
return -EINVAL;
|
|
|
|
char *cxl_dev_name __free(kfree) =
|
|
kasprintf(GFP_KERNEL, "cxl_%s", dev_name(&cxlmd->dev));
|
|
if (!cxl_dev_name)
|
|
return -ENOMEM;
|
|
|
|
return edac_dev_register(&cxlmd->dev, cxl_dev_name, NULL,
|
|
num_ras_features, ras_features);
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(devm_cxl_memdev_edac_register, "CXL");
|
|
|
|
int devm_cxl_region_edac_register(struct cxl_region *cxlr)
|
|
{
|
|
struct edac_dev_feature ras_features[CXL_NR_EDAC_DEV_FEATURES];
|
|
int num_ras_features = 0;
|
|
int rc;
|
|
|
|
if (!IS_ENABLED(CONFIG_CXL_EDAC_SCRUB))
|
|
return 0;
|
|
|
|
rc = cxl_region_scrub_init(cxlr, &ras_features[num_ras_features], 0);
|
|
if (rc < 0)
|
|
return rc;
|
|
|
|
num_ras_features++;
|
|
|
|
char *cxl_dev_name __free(kfree) =
|
|
kasprintf(GFP_KERNEL, "cxl_%s", dev_name(&cxlr->dev));
|
|
if (!cxl_dev_name)
|
|
return -ENOMEM;
|
|
|
|
return edac_dev_register(&cxlr->dev, cxl_dev_name, NULL,
|
|
num_ras_features, ras_features);
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(devm_cxl_region_edac_register, "CXL");
|
|
|
|
void devm_cxl_memdev_edac_release(struct cxl_memdev *cxlmd)
|
|
{
|
|
struct cxl_mem_err_rec *array_rec = cxlmd->err_rec_array;
|
|
struct cxl_event_gen_media *rec_gen_media;
|
|
struct cxl_event_dram *rec_dram;
|
|
unsigned long index;
|
|
|
|
if (!IS_ENABLED(CONFIG_CXL_EDAC_MEM_REPAIR) || !array_rec)
|
|
return;
|
|
|
|
xa_for_each(&array_rec->rec_dram, index, rec_dram)
|
|
kfree(rec_dram);
|
|
xa_destroy(&array_rec->rec_dram);
|
|
|
|
xa_for_each(&array_rec->rec_gen_media, index, rec_gen_media)
|
|
kfree(rec_gen_media);
|
|
xa_destroy(&array_rec->rec_gen_media);
|
|
}
|
|
EXPORT_SYMBOL_NS_GPL(devm_cxl_memdev_edac_release, "CXL");
|