blob: 63ebe7f6db5905889486bbfd9ca456540462cf58 [file] [edit]
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
*/
#include <linux/debugfs.h>
#include <linux/io.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/soc/qcom/smem.h>
#include <linux/units.h>
#include "smem.h"
#define SMEM_DDR_INFO_ID 603
#define MAX_DDR_FREQ_NUM_V3 13
#define MAX_DDR_FREQ_NUM_V5 14
#define MAX_CHAN_NUM 8
#define MAX_RANK_NUM 2
#define DDR_HBB_MIN 13
#define DDR_HBB_MAX 19
#define MAX_SHUB_ENTRIES 8
static struct smem_dram *__dram;
enum ddr_info_version {
INFO_UNKNOWN,
INFO_V3,
INFO_V3_WITH_14_FREQS,
INFO_V4,
INFO_V5,
INFO_V5_WITH_6_REGIONS,
INFO_V6, /* INFO_V6 seems to only have shipped with 6 DDR regions, unlike V7 */
INFO_V7,
INFO_V7_WITH_6_REGIONS,
};
struct smem_dram {
unsigned long frequencies[MAX_DDR_FREQ_NUM_V5];
u32 num_frequencies;
u8 hbb;
};
enum ddr_type {
DDR_TYPE_NODDR = 0,
DDR_TYPE_LPDDR1 = 1,
DDR_TYPE_LPDDR2 = 2,
DDR_TYPE_PCDDR2 = 3,
DDR_TYPE_PCDDR3 = 4,
DDR_TYPE_LPDDR3 = 5,
DDR_TYPE_LPDDR4 = 6,
DDR_TYPE_LPDDR4X = 7,
DDR_TYPE_LPDDR5 = 8,
DDR_TYPE_LPDDR5X = 9,
};
/* The data structures below are NOT __packed on purpose! */
/* Structs used across multiple versions */
struct ddr_part_details {
__le16 revision_id1;
__le16 revision_id2;
__le16 width;
__le16 density;
};
struct ddr_freq_table {
__le32 freq_khz;
u8 enabled;
};
/* V3 */
struct ddr_freq_plan_v3 {
struct ddr_freq_table ddr_freq[MAX_DDR_FREQ_NUM_V3];
u8 num_ddr_freqs;
phys_addr_t clk_period_address;
};
struct ddr_details_v3 {
u8 manufacturer_id;
u8 device_type;
struct ddr_part_details ddr_params[MAX_CHAN_NUM];
struct ddr_freq_plan_v3 ddr_freq_tbl;
u8 num_channels;
};
/* Some V3 structs have an additional frequency level */
struct ddr_freq_plan_v3_14freqs {
struct ddr_freq_table ddr_freq[MAX_DDR_FREQ_NUM_V3 + 1];
u8 num_ddr_freqs;
phys_addr_t clk_period_address;
};
struct ddr_details_v3_14freqs {
u8 manufacturer_id;
u8 device_type;
struct ddr_part_details ddr_params[MAX_CHAN_NUM];
struct ddr_freq_plan_v3_14freqs ddr_freq_tbl;
u8 num_channels;
};
/* V4 */
struct ddr_details_v4 {
u8 manufacturer_id;
u8 device_type;
struct ddr_part_details ddr_params[MAX_CHAN_NUM];
struct ddr_freq_plan_v3 ddr_freq_tbl;
u8 num_channels;
u8 num_ranks[MAX_CHAN_NUM];
u8 highest_bank_addr_bit[MAX_CHAN_NUM][MAX_RANK_NUM];
};
/* V5 */
struct shub_freq_table {
u8 enable;
__le32 freq_khz;
};
struct shub_freq_plan_entry {
u8 num_shub_freqs;
struct shub_freq_table shub_freq[MAX_SHUB_ENTRIES];
};
struct ddr_xbl2quantum_smem_data {
phys_addr_t ssr_cookie_addr;
__le32 reserved[10];
};
struct ddr_freq_plan_v5 {
struct ddr_freq_table ddr_freq[MAX_DDR_FREQ_NUM_V5];
u8 num_ddr_freqs;
phys_addr_t clk_period_address;
__le32 max_nom_ddr_freq;
};
struct ddr_region_v5 {
__le64 start_address;
__le64 size;
__le64 mem_controller_address;
__le32 granule_size; /* MiB */
u8 ddr_rank;
#define DDR_RANK_0 BIT(0)
#define DDR_RANK_1 BIT(1)
u8 segments_start_index;
__le64 segments_start_offset;
};
struct ddr_regions_v5 {
__le32 ddr_region_num; /* We expect this to always be 4 or 6 */
__le64 ddr_rank0_size;
__le64 ddr_rank1_size;
__le64 ddr_cs0_start_addr;
__le64 ddr_cs1_start_addr;
__le32 highest_bank_addr_bit;
struct ddr_region_v5 ddr_region[] __counted_by_le(ddr_region_num);
};
struct ddr_details_v5 {
u8 manufacturer_id;
u8 device_type;
struct ddr_part_details ddr_params[MAX_CHAN_NUM];
struct ddr_freq_plan_v5 ddr_freq_tbl;
u8 num_channels;
u8 _padding;
struct ddr_regions_v5 ddr_regions;
};
/* V6 */
struct ddr_misc_info_v6 {
__le32 dsf_version;
__le32 reserved[10];
};
/* V7 */
struct ddr_details_v7 {
u8 manufacturer_id;
u8 device_type;
struct ddr_part_details ddr_params[MAX_CHAN_NUM];
struct ddr_freq_plan_v5 ddr_freq_tbl;
u8 num_channels;
u8 sct_config;
struct ddr_regions_v5 ddr_regions;
};
/**
* qcom_smem_dram_get_hbb(): Get the Highest bank address bit
*
* Context: Check qcom_smem_is_available() before calling this function.
* Because __dram * is initialized by smem_dram_parse(), which is in turn
* called from * qcom_smem_probe(), __dram will only be NULL if the data
* couldn't have been found/interpreted correctly.
*
* Return: highest bank bit on success, -ENODATA on failure.
*/
int qcom_smem_dram_get_hbb(void)
{
if (!__dram || !__dram->hbb)
return -ENODATA;
if (__dram->hbb < DDR_HBB_MIN || __dram->hbb > DDR_HBB_MAX)
return -ENODATA;
return __dram->hbb;
}
EXPORT_SYMBOL_GPL(qcom_smem_dram_get_hbb);
static void smem_dram_parse_v3_data(struct smem_dram *dram, void *data)
{
struct ddr_details_v3 *details = data;
for (int i = 0; i < MAX_DDR_FREQ_NUM_V3; i++) {
struct ddr_freq_table *freq_entry = &details->ddr_freq_tbl.ddr_freq[i];
if (freq_entry->freq_khz && freq_entry->enabled) {
u32 freq_khz = le32_to_cpu(freq_entry->freq_khz);
dram->frequencies[dram->num_frequencies++] = 1000 * freq_khz;
}
}
}
static void smem_dram_parse_v3_14freqs_data(struct smem_dram *dram, void *data)
{
struct ddr_details_v3_14freqs *details = data;
for (int i = 0; i < MAX_DDR_FREQ_NUM_V3 + 1; i++) {
struct ddr_freq_table *freq_entry = &details->ddr_freq_tbl.ddr_freq[i];
if (freq_entry->freq_khz && freq_entry->enabled)
dram->frequencies[dram->num_frequencies++] = 1000 * freq_entry->freq_khz;
}
}
static void smem_dram_parse_v4_data(struct smem_dram *dram, void *data)
{
struct ddr_details_v4 *details = data;
/* Rank 0 channel 0 entry holds the correct value */
dram->hbb = details->highest_bank_addr_bit[0][0];
for (int i = 0; i < MAX_DDR_FREQ_NUM_V3; i++) {
struct ddr_freq_table *freq_entry = &details->ddr_freq_tbl.ddr_freq[i];
if (freq_entry->freq_khz && freq_entry->enabled) {
u32 freq_khz = le32_to_cpu(freq_entry->freq_khz);
dram->frequencies[dram->num_frequencies++] = 1000 * freq_khz;
}
}
}
static void smem_dram_parse_v5_data(struct smem_dram *dram, void *data)
{
struct ddr_details_v5 *details = data;
struct ddr_regions_v5 *region = &details->ddr_regions;
dram->hbb = le32_to_cpu(region[0].highest_bank_addr_bit);
for (int i = 0; i < MAX_DDR_FREQ_NUM_V5; i++) {
struct ddr_freq_table *freq_entry = &details->ddr_freq_tbl.ddr_freq[i];
if (freq_entry->freq_khz && freq_entry->enabled) {
u32 freq_khz = le32_to_cpu(freq_entry->freq_khz);
dram->frequencies[dram->num_frequencies++] = 1000 * freq_khz;
}
}
}
static void smem_dram_parse_v7_data(struct smem_dram *dram, void *data)
{
struct ddr_details_v7 *details = data;
struct ddr_regions_v5 *region = &details->ddr_regions;
dram->hbb = le32_to_cpu(region[0].highest_bank_addr_bit);
for (int i = 0; i < MAX_DDR_FREQ_NUM_V5; i++) {
struct ddr_freq_table *freq_entry = &details->ddr_freq_tbl.ddr_freq[i];
if (freq_entry->freq_khz && freq_entry->enabled) {
u32 freq_khz = le32_to_cpu(freq_entry->freq_khz);
dram->frequencies[dram->num_frequencies++] = 1000 * freq_khz;
}
}
}
/* The structure contains no version field, so we have to perform some guesswork.. */
static int smem_dram_infer_struct_version(size_t size)
{
/* Some early versions provided less bytes of less useful data */
if (size < sizeof(struct ddr_details_v3))
return -EINVAL;
if (size == sizeof(struct ddr_details_v3))
return INFO_V3;
if (size == sizeof(struct ddr_details_v3_14freqs))
return INFO_V3_WITH_14_FREQS;
if (size == sizeof(struct ddr_details_v4))
return INFO_V4;
if (size == sizeof(struct ddr_details_v5) +
4 * sizeof(struct ddr_region_v5))
return INFO_V5;
if (size == sizeof(struct ddr_details_v5) +
4 * sizeof(struct ddr_region_v5) +
sizeof(struct ddr_xbl2quantum_smem_data) +
sizeof(struct shub_freq_plan_entry))
return INFO_V5;
if (size == sizeof(struct ddr_details_v5) +
6 * sizeof(struct ddr_region_v5))
return INFO_V5_WITH_6_REGIONS;
if (size == sizeof(struct ddr_details_v5) +
6 * sizeof(struct ddr_region_v5) +
sizeof(struct ddr_xbl2quantum_smem_data) +
sizeof(struct shub_freq_plan_entry))
return INFO_V5_WITH_6_REGIONS;
if (size == sizeof(struct ddr_details_v5) +
6 * sizeof(struct ddr_region_v5) +
sizeof(struct ddr_misc_info_v6) +
sizeof(struct shub_freq_plan_entry))
return INFO_V6;
if (size == sizeof(struct ddr_details_v7) +
4 * sizeof(struct ddr_region_v5) +
sizeof(struct ddr_misc_info_v6) +
sizeof(struct shub_freq_plan_entry))
return INFO_V7;
if (size == sizeof(struct ddr_details_v7) +
6 * sizeof(struct ddr_region_v5) +
sizeof(struct ddr_misc_info_v6) +
sizeof(struct shub_freq_plan_entry))
return INFO_V7_WITH_6_REGIONS;
return INFO_UNKNOWN;
}
static int smem_dram_frequencies_show(struct seq_file *s, void *unused)
{
struct smem_dram *dram = s->private;
for (int i = 0; i < dram->num_frequencies; i++)
seq_printf(s, "%lu\n", dram->frequencies[i]);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(smem_dram_frequencies);
static int smem_hbb_show(struct seq_file *s, void *unused)
{
struct smem_dram *dram = s->private;
if (!dram->hbb)
return -EINVAL;
seq_printf(s, "%d\n", dram->hbb);
return 0;
}
DEFINE_SHOW_ATTRIBUTE(smem_hbb);
struct dentry *smem_dram_parse(struct qcom_smem *smem, struct device *dev)
{
struct dentry *debugfs_dir;
enum ddr_info_version ver;
struct smem_dram *dram;
size_t actual_size;
void *data;
/* No need to check qcom_smem_is_available(), this func is called by the SMEM driver */
data = __qcom_smem_get(smem, QCOM_SMEM_HOST_ANY, SMEM_DDR_INFO_ID, &actual_size);
if (IS_ERR_OR_NULL(data))
return ERR_PTR(-ENODATA);
ver = smem_dram_infer_struct_version(actual_size);
if (ver < 0) {
/* Some SoCs don't provide data that's useful for us */
return ERR_PTR(-ENODATA);
} else if (ver == INFO_UNKNOWN) {
/* In other cases, we may not have added support for a newer struct revision */
dev_err(dev, "Found an unknown type of DRAM info struct (size = %zu)\n",
actual_size);
return ERR_PTR(-EINVAL);
}
dram = devm_kzalloc(dev, sizeof(*dram), GFP_KERNEL);
if (!dram)
return ERR_PTR(-ENOMEM);
switch (ver) {
case INFO_V3:
smem_dram_parse_v3_data(dram, data);
break;
case INFO_V3_WITH_14_FREQS:
smem_dram_parse_v3_14freqs_data(dram, data);
break;
case INFO_V4:
smem_dram_parse_v4_data(dram, data);
break;
case INFO_V5:
case INFO_V5_WITH_6_REGIONS:
case INFO_V6:
smem_dram_parse_v5_data(dram, data);
break;
case INFO_V7:
case INFO_V7_WITH_6_REGIONS:
smem_dram_parse_v7_data(dram, data);
break;
default:
return ERR_PTR(-EINVAL);
}
debugfs_dir = debugfs_create_dir("qcom_smem", NULL);
debugfs_create_file("dram_frequencies", 0444, debugfs_dir, dram,
&smem_dram_frequencies_fops);
debugfs_create_file("hbb", 0444, debugfs_dir, dram, &smem_hbb_fops);
__dram = dram;
return debugfs_dir;
}