| // 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; | |
| } |