Re: [PATCH RFC v7 7/9] PM / devfreq: Introduce the QCOM SCMI Memlat devfreq driver
Pragnesh Papaniya <[email protected]> Tue, 14 Jul 2026 01:14:23 +0530
| Newsgroups | org.kernel.vger.arm-scmi,org.infradead.lists.linux-arm-kernel,org.kernel.vger.linux-arm-msm,org.kernel.vger.linux-devicetree,org.kernel.vger.linux-kernel,org.kernel.vger.linux-pm,org.kernel.vger.linux-tegra |
|---|---|
| Message-ID | <[email protected]> |
On 02-Jul-26 10:51 PM, Bjorn Andersson wrote: > On Wed, Jun 10, 2026 at 02:21:34PM +0530, Pragnesh Papaniya wrote: >> From: Sibi Sankar <[email protected]> >> >> On Qualcomm Glymur, Mahua and X1E/X1P (Hamoa) SoCs, the memlat governor and >> the mechanism to control the various caches and RAM is hosted on the CPU >> Control Processor (CPUCP), and configuration and control of this governor >> is exposed through the QCOM SCMI Generic Extension Protocol, addressed via >> the "MEMLAT" algorithm string. >> > > This explains that there's a bunch of functionality running on CPUCP and > there's a "MEMLAT" string. > CPUCP does all the real work: it samples CPU perf counters, computes IPM/stall, and votes the DDR/LLCC/DDR_QOS buses on its own timer. The Linux driver only pushes static configuration (freq maps, ceilings) once at probe and starts/stops the CPUCP timer. I'll rewrite the message to say this plainly. >> Introduce a devfreq SCMI client driver that uses the MEMLAT algorithm >> string to detect memory-latency-bound workloads and control the >> frequency/level of the memory buses (DDR, LLCC and DDR_QOS). > > You established that there's stuff running in the firmware, now we're > introducing a client driver to control memory buses. > > But where did you explain how these two "facts" are related? Why is > there a client driver, what is the actual distribution of roles in this > dance? > At runtime the driver is not in the control loop, CPUCP is. devfreq is used so each bus shows up as a real device with trans_stat and the remote governor's parameters like sample_ms and ipm_ceil are user-configurable. I'll make that reasoning explicit in the commit text. >> Model each bus >> as a devfreq device using the remote devfreq governor. This provides basic >> insight into device operation via trans_stat and allows further tuning of >> the remote governor's parameters from userspace. >> > > Does this mean that the driver is "optional", and only exists to give > insight and a way to tune the firmware operation? > > If that's the case, why is it a devfreq driver? > > Please write your commit messages (and patches) such that people outside > your immediate team can understand why the patches are needed and what > the code is supposed to do. > Ack >> Co-developed-by: Amir Vajid <[email protected]> >> Signed-off-by: Amir Vajid <[email protected]> >> Co-developed-by: Ramakrishna Gottimukkula <[email protected]> >> Signed-off-by: Ramakrishna Gottimukkula <[email protected]> >> Signed-off-by: Sibi Sankar <[email protected]> >> Co-developed-by: Pragnesh Papaniya <[email protected]> >> Signed-off-by: Pragnesh Papaniya <[email protected]> >> --- >> drivers/devfreq/Kconfig | 13 + >> drivers/devfreq/Makefile | 1 + >> drivers/devfreq/scmi-qcom-memlat-cfg.h | 573 +++++++++++++++++++++++++++ >> drivers/devfreq/scmi-qcom-memlat-devfreq.c | 616 +++++++++++++++++++++++++++++ >> 4 files changed, 1203 insertions(+) >> >> diff --git a/drivers/devfreq/Kconfig b/drivers/devfreq/Kconfig >> index 2caa87554914..98b5a50d3189 100644 >> --- a/drivers/devfreq/Kconfig >> +++ b/drivers/devfreq/Kconfig >> @@ -169,6 +169,19 @@ config ARM_SUN8I_A33_MBUS_DEVFREQ >> This adds the DEVFREQ driver for the MBUS controller in some >> Allwinner sun8i (A33 through H3) and sun50i (A64 and H5) SoCs. >> >> +config SCMI_QCOM_MEMLAT_DEVFREQ >> + tristate "Qualcomm Technologies Inc. SCMI client driver" >> + depends on QCOM_SCMI_GENERIC_EXT || COMPILE_TEST >> + select DEVFREQ_GOV_REMOTE >> + help >> + This driver uses the MEMLAT (memory latency) algorithm string > > Is "driver uses X algorithm string" idiomatic SCMI terms? > No, "algorithm string" is an internal term. I'll drop the jargon and describe it in plain SCMI vendor-protocol terms. >> + hosted on QCOM SCMI Vendor Protocol to detect memory latency >> + workloads and control frequency/level of the various memory >> + buses (DDR/LLCC/DDR_QOS). >> + >> + This driver defines/documents the parameter IDs used while configuring >> + the memory buses. > > Imagine an person outside your team, sitting there in menuconfig > wondering if they should enable this driver or not. > > There's a sentence in the middle ("control frequency/level of various > memory buses" - that sounds like something I want. But "detect memory > latency", is it just monitoring or does that part relate to the > controlling part? "This driver defines" so what are those parameters > used for, do I need some other driver for the control part? Is this last > paragraph adding value to my understanding for that > CONFIG_SCMI_QCOM_MEMLAT_DEVFREQ does? > I'll rewrite it to say what you get (memory-bus scaling on these Qualcomm SoCs), that CPUCP does the actual scaling, and that nothing else is required to enable it. The parameter-ID paragraph will go. >> + >> source "drivers/devfreq/event/Kconfig" >> >> endif # PM_DEVFREQ >> diff --git a/drivers/devfreq/Makefile b/drivers/devfreq/Makefile >> index cde57c8cda76..b11f94e2f485 100644 >> --- a/drivers/devfreq/Makefile >> +++ b/drivers/devfreq/Makefile >> @@ -17,6 +17,7 @@ obj-$(CONFIG_ARM_MEDIATEK_CCI_DEVFREQ) += mtk-cci-devfreq.o >> obj-$(CONFIG_ARM_RK3399_DMC_DEVFREQ) += rk3399_dmc.o >> obj-$(CONFIG_ARM_SUN8I_A33_MBUS_DEVFREQ) += sun8i-a33-mbus.o >> obj-$(CONFIG_ARM_TEGRA_DEVFREQ) += tegra30-devfreq.o >> +obj-$(CONFIG_SCMI_QCOM_MEMLAT_DEVFREQ) += scmi-qcom-memlat-devfreq.o >> >> # DEVFREQ Event Drivers >> obj-$(CONFIG_PM_DEVFREQ_EVENT) += event/ >> diff --git a/drivers/devfreq/scmi-qcom-memlat-cfg.h b/drivers/devfreq/scmi-qcom-memlat-cfg.h >> new file mode 100644 >> index 000000000000..1ab8b61ea271 >> --- /dev/null >> +++ b/drivers/devfreq/scmi-qcom-memlat-cfg.h > > Are the entities declared in this header file used by anything other > than scmi-qcom-memlat-devfreq.c? If not why is it a separate header file? > No, only scmi-qcom-memlat-devfreq.c uses it. I split it out just to keep the large config tables out of the driver logic. Happy either way: do you prefer I fold it back into the .c, or keep it as a header? >> @@ -0,0 +1,573 @@ >> +/* SPDX-License-Identifier: GPL-2.0-only */ >> +/* >> + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. >> + */ >> + >> +#ifndef __DRIVERS_DEVFREQ_SCMI_QCOM_MEMLAT_CONFIG_H__ >> +#define __DRIVERS_DEVFREQ_SCMI_QCOM_MEMLAT_CONFIG_H__ >> + >> +/* >> + * Memlat Effective Frequency Calculation Method >> + * CPUCP_EFFECTIVE_FREQ_METHOD_0 - Uses CPU Cycles and CONST Cycles to calculate >> + * CPUCP_EFFECTIVE_FREQ_METHOD_1 - Uses CPU Cycles and time period >> + */ >> +#define CPUCP_EFFECTIVE_FREQ_CALC_METHOD_0 0 >> +#define CPUCP_EFFECTIVE_FREQ_CALC_METHOD_1 1 > > #define ZERO 0 > #define ONE 1 > > Can these be given real names, or could the struct member even be a > boolean? > > That said, "ZERO" isn't used in your patch... > I'll make cpucp_freq_method a bool. Method 0 is a valid firmware method and will be used too, so I'll keep both and select via the bool. >> + >> +#define EV_CPU_CYCLES 0 >> +#define EV_CNT_CYCLES 1 >> +#define EV_INST_RETIRED 2 >> +#define EV_STALL_BACKEND_MEM 3 >> +#define EV_L2_D_RFILL 5 >> +#define INVALID_IDX 0xff > > The names of these defines aren't awesome names to put in include > files... > Ack, I'll namespace them (MEMLAT_EV_*) and align INVALID_IDX with the firmware's 0xFF. >> + >> +#define MEMLAT_ALGO_STR 0x4D454D4C4154ULL /* MEMLAT */ > > As you've seen a thousand times on LKML already, we want lower-case hex > digits... > Firmware expects upper-case string only. >> + >> +struct scmi_qcom_map_table { >> + unsigned int cpu_freq; >> + unsigned int mem_freq; > > Unit soup > I'll suffix them (cpu_freq_mhz / mem_freq_mhz). >> +}; >> + >> +struct scmi_qcom_opp_data { >> + unsigned long freq; >> + unsigned int level; > > The arrays of opp_data is all static const, and you seem to only define > "level" for ddr_qos, which if I read the code correctly you later > treat in some special way anyways. > Yes, level is only set for DDR_QOS and unused elsewhere. I'll drop the level field entirely and document that the DDR_QOS OPP frequencies (1 and 100) are not real frequencies; they stand in for the 0/1 level. >> +}; >> + >> +struct scmi_qcom_memory_range { >> + unsigned int min_freq; >> + unsigned int max_freq; >> +}; >> + >> +enum common_ev_idx { >> + INST_IDX, >> + CYC_IDX, >> + CONST_CYC_IDX, >> + FE_STALL_IDX, >> + BE_STALL_IDX, >> + NUM_COMMON_EVS >> +}; >> + >> +enum grp_ev_idx { >> + MISS_IDX, >> + WB_IDX, >> + ACC_IDX, >> + NUM_GRP_EVS >> +}; >> + >> +/* >> + * CPUCP firmware identifies memory groups by a small integer (the hw_type >> + * carried in node_msg / scalar_param_msg / map_param_msg / ev_map_msg). > > Ok, sounds reasonable. > >> The >> + * encoding is shared between the cfg tables below and scmi_qcom_devfreq_get_cur_freq() >> + * which special-cases DDR_QOS as a level-based bus rather than a frequency-scaled one. > > The constants are shared between A and B, both being described as > in-driver users. So is this constants shared between the first (as you > said above) or an enumeration within the driver? > It's a protocol ABI, shared with the first (firmware). I'll reword the comment to say it's a firmware-defined encoding, not a driver-internal enum. >> + */ >> +enum scmi_qcom_memlat_hw_type { >> + MEMLAT_HW_DDR = 0, >> + MEMLAT_HW_LLCC = 1, >> + MEMLAT_HW_DDR_QOS_COMPUTE = 2, > > Please use "enum" for enumerations and #define for constants. (These > aren't enumerations, as they are defined constants) > Ack, will keep it as enum (as you suggested below). >> +}; >> + >> +struct scmi_qcom_monitor_cfg { >> + const struct scmi_qcom_map_table *table; >> + const char *name; >> + u32 be_stall_floor; > > What is a "be stall floor"? Also, it seems to be 1 in all your cases. Is > it boolean? Is it constant? > It's a back-end-stall percentage threshold. It happens to be 1 in all current configs (meaning almost any stall qualifies). I'll document it as a percent. >> + u32 cpu_mask; >> + u32 ipm_ceil; >> + int table_len; > > Why is this signed? > Ack >> +}; >> + >> +struct scmi_qcom_memory_cfg { >> + const struct scmi_qcom_monitor_cfg *monitor_cfg; >> + const struct scmi_qcom_opp_data *mem_table; >> + struct scmi_qcom_memory_range memory_range; >> + const u32 *grp_ev; >> + const char *name; >> + u32 memory_type; > > As the memory type isn't an enumeration, I asked you above to make it > constants. But in the form you wrote it, there's no reason for this not > to have its enum type. > Ack, will keep it as enum. >> + int monitor_cnt; >> + int num_opps; > > Aren't these unsigned? > Ack >> +}; >> + >> +struct scmi_qcom_memlat_cfg_data { >> + const struct scmi_qcom_memory_cfg *memory_cfg; >> + const u32 *common_ev; >> + u32 cpucp_freq_method; >> + u32 cpucp_sample_ms; >> + int memory_cnt; > > Unsigned... > Ack >> +}; >> + >> +static const u32 glymur_common_ev[NUM_COMMON_EVS] = { >> + [INST_IDX] = EV_INST_RETIRED, >> + [CYC_IDX] = EV_CPU_CYCLES, >> + [CONST_CYC_IDX] = EV_CNT_CYCLES, >> + [FE_STALL_IDX] = INVALID_IDX, >> + [BE_STALL_IDX] = EV_STALL_BACKEND_MEM, >> +}; >> + >> +static const u32 glymur_ddr_grp_ev[NUM_GRP_EVS] = { >> + [MISS_IDX] = EV_L2_D_RFILL, >> + [WB_IDX] = INVALID_IDX, >> + [ACC_IDX] = INVALID_IDX, >> +}; >> + >> +static const u32 glymur_llcc_grp_ev[NUM_GRP_EVS] = { >> + [MISS_IDX] = EV_L2_D_RFILL, >> + [WB_IDX] = INVALID_IDX, >> + [ACC_IDX] = INVALID_IDX, >> +}; >> + >> +static const u32 glymur_ddr_qos_grp_ev[NUM_GRP_EVS] = { >> + [MISS_IDX] = EV_L2_D_RFILL, >> + [WB_IDX] = INVALID_IDX, >> + [ACC_IDX] = INVALID_IDX, >> +}; >> + >> +static const u32 hamoa_common_ev[NUM_COMMON_EVS] = { >> + [INST_IDX] = EV_INST_RETIRED, >> + [CYC_IDX] = EV_CPU_CYCLES, >> + [CONST_CYC_IDX] = EV_CNT_CYCLES, >> + [FE_STALL_IDX] = INVALID_IDX, >> + [BE_STALL_IDX] = EV_STALL_BACKEND_MEM, >> +}; >> + >> +static const u32 hamoa_ddr_grp_ev[NUM_GRP_EVS] = { >> + [MISS_IDX] = EV_L2_D_RFILL, >> + [WB_IDX] = INVALID_IDX, >> + [ACC_IDX] = INVALID_IDX, >> +}; >> + >> +static const u32 hamoa_llcc_grp_ev[NUM_GRP_EVS] = { >> + [MISS_IDX] = EV_L2_D_RFILL, >> + [WB_IDX] = INVALID_IDX, >> + [ACC_IDX] = INVALID_IDX, >> +}; >> + >> +static const u32 hamoa_ddr_qos_grp_ev[NUM_GRP_EVS] = { >> + [MISS_IDX] = EV_L2_D_RFILL, >> + [WB_IDX] = INVALID_IDX, >> + [ACC_IDX] = INVALID_IDX, >> +}; >> + >> +static const struct scmi_qcom_opp_data glymur_llcc_table[] = { >> + { .freq = 315000000 }, >> + { .freq = 479000000 }, >> + { .freq = 545000000 }, >> + { .freq = 725000000 }, >> + { .freq = 840000000 }, >> + { .freq = 959000000 }, >> + { .freq = 1090000000 }, >> + { .freq = 1211000000 }, >> +}; >> + >> +static const struct scmi_qcom_opp_data hamoa_llcc_table[] = { >> + { .freq = 300000000 }, >> + { .freq = 466000000 }, >> + { .freq = 600000000 }, >> + { .freq = 806000000 }, >> + { .freq = 933000000 }, >> + { .freq = 1066000000 }, >> +}; >> + >> +static const struct scmi_qcom_opp_data glymur_ddr_table[] = { >> + { .freq = 200000000 }, >> + { .freq = 547000000 }, >> + { .freq = 1353000000 }, >> + { .freq = 1555000000 }, >> + { .freq = 1708000000 }, >> + { .freq = 2092000000 }, >> + { .freq = 2736000000 }, >> + { .freq = 3187000000 }, >> + { .freq = 3686000000 }, >> + { .freq = 4224000000 }, >> + { .freq = 4761000000 }, >> +}; >> + >> +static const struct scmi_qcom_opp_data hamoa_ddr_table[] = { >> + { .freq = 200000000 }, >> + { .freq = 547000000 }, >> + { .freq = 768000000 }, >> + { .freq = 1555000000 }, >> + { .freq = 1708000000 }, >> + { .freq = 2092000000 }, >> + { .freq = 2736000000 }, >> + { .freq = 3187000000 }, >> + { .freq = 3686000000 }, >> + { .freq = 4224000000 }, >> +}; >> + >> +/* >> + * DDR_QOS is a level-based bus (0 = nominal, 1 = boost), not a >> + * frequency-scaled one. > > "level-based"? Looks more like "boolean"? > > So I presume "not a frequency-scaled one" should be interpreted as the > .freq is bogus? > Yes, the .freq values there are not real frequencies. Removing the level field and rewording the comment will make that clear. >> The OPP entries below use synthetic frequencies >> + * (1 / 100) purely as distinct devfreq keys so trans_stat can show >> + * level transitions. > > 1/100? 0.01? > Its not division, its an 'or'. Will drop '/'. > Why are the frequencies 1 and 100, why not 0 and 1, or 0 and 100? > 0 can't be used, dev_pm_opp_add() rejects a 0 frequency. The two keys just need to be non-zero and distinct, hence 1 and 100. I'll spell this out in the comment. >> scmi_qcom_devfreq_get_cur_freq() maps the firmware >> + * level back to the matching key. > > What is the "key" here? > "key" means the OPP/devfreq frequency used for lookup. I'll say "OPP frequency" instead of "key". >> + */ >> +static const struct scmi_qcom_opp_data glymur_ddr_qos_table[] = { >> + { .freq = 1, .level = 0 }, >> + { .freq = 100, .level = 1 }, >> +}; >> + >> +static const struct scmi_qcom_memory_cfg glymur_memory_cfg[] = { >> + { >> + .memory_type = MEMLAT_HW_DDR, >> + .name = "ddr", >> + .mem_table = glymur_ddr_table, >> + .num_opps = ARRAY_SIZE(glymur_ddr_table), >> + .grp_ev = glymur_ddr_grp_ev, >> + .monitor_cnt = 4, >> + .memory_range = { .min_freq = 547000, .max_freq = 4761000}, >> + .monitor_cfg = (const struct scmi_qcom_monitor_cfg[]) { >> + { >> + .name = "mon_0", >> + .cpu_mask = 0x3f, >> + .ipm_ceil = 60000000, >> + .be_stall_floor = 1, >> + .table_len = 8, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 960, .mem_freq = 547000 }, >> + { .cpu_freq = 1133, .mem_freq = 1353000 }, >> + { .cpu_freq = 1594, .mem_freq = 1555000 }, >> + { .cpu_freq = 1920, .mem_freq = 1708000 }, >> + { .cpu_freq = 2228, .mem_freq = 2736000 }, >> + { .cpu_freq = 2362, .mem_freq = 3187000 }, >> + { .cpu_freq = 2650, .mem_freq = 3686000 }, >> + { .cpu_freq = 2938, .mem_freq = 4761000 }, > > Why are these tables hard coded in the driver? Are they constant? > These tables can be either in DT (like in earlier re-spins of the series) or in the driver. For the former to work well with the existing OPP framework, we would need a clock provider created for DDR/LLCC/DDR-QOS just to derive the cpufreq to memfreq map tables. Having it in the driver simplifies the overall implementation. >> + } >> + }, >> + { >> + .name = "mon_1", >> + .cpu_mask = 0xfc0, >> + .ipm_ceil = 60000000, >> + .be_stall_floor = 1, >> + .table_len = 8, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 356, .mem_freq = 547000 }, >> + { .cpu_freq = 1018, .mem_freq = 1353000 }, >> + { .cpu_freq = 1536, .mem_freq = 1555000 }, >> + { .cpu_freq = 1748, .mem_freq = 1708000 }, >> + { .cpu_freq = 2324, .mem_freq = 2736000 }, >> + { .cpu_freq = 2496, .mem_freq = 3187000 }, >> + { .cpu_freq = 2900, .mem_freq = 3686000 }, >> + { .cpu_freq = 3514, .mem_freq = 4761000 }, >> + } >> + }, >> + { >> + .name = "mon_2", >> + .cpu_mask = 0x3f000, >> + .ipm_ceil = 60000000, >> + .be_stall_floor = 1, >> + .table_len = 8, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 356, .mem_freq = 547000 }, >> + { .cpu_freq = 1018, .mem_freq = 1353000 }, >> + { .cpu_freq = 1536, .mem_freq = 1555000 }, >> + { .cpu_freq = 1748, .mem_freq = 1708000 }, >> + { .cpu_freq = 2324, .mem_freq = 2736000 }, >> + { .cpu_freq = 2496, .mem_freq = 3187000 }, >> + { .cpu_freq = 2900, .mem_freq = 3686000 }, >> + { .cpu_freq = 3514, .mem_freq = 4761000 }, >> + } >> + }, >> + { >> + .name = "mon_3", >> + .cpu_mask = 0x3ffff, >> + .table_len = 4, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2823, .mem_freq = 547000 }, >> + { .cpu_freq = 3034, .mem_freq = 1555000 }, >> + { .cpu_freq = 3226, .mem_freq = 1708000 }, >> + { .cpu_freq = 5012, .mem_freq = 2092000 }, >> + } >> + }, >> + }, >> + }, >> + { >> + .memory_type = MEMLAT_HW_LLCC, >> + .name = "llcc", >> + .mem_table = glymur_llcc_table, >> + .num_opps = ARRAY_SIZE(glymur_llcc_table), >> + .grp_ev = glymur_llcc_grp_ev, >> + .monitor_cnt = 3, >> + .memory_range = { .min_freq = 315000, .max_freq = 1211000}, >> + .monitor_cfg = (const struct scmi_qcom_monitor_cfg[]) { >> + { >> + .name = "mon_0", >> + .cpu_mask = 0x3f, >> + .ipm_ceil = 60000000, >> + .be_stall_floor = 1, >> + .table_len = 7, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 960, .mem_freq = 315000 }, >> + { .cpu_freq = 1113, .mem_freq = 479000 }, >> + { .cpu_freq = 1594, .mem_freq = 545000 }, >> + { .cpu_freq = 1920, .mem_freq = 725000 }, >> + { .cpu_freq = 2362, .mem_freq = 840000 }, >> + { .cpu_freq = 2650, .mem_freq = 959000 }, >> + { .cpu_freq = 2938, .mem_freq = 1211000 }, >> + } >> + }, >> + { >> + .name = "mon_1", >> + .cpu_mask = 0xfc0, >> + .ipm_ceil = 60000000, >> + .be_stall_floor = 1, >> + .table_len = 7, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 356, .mem_freq = 315000 }, >> + { .cpu_freq = 1018, .mem_freq = 479000 }, >> + { .cpu_freq = 1536, .mem_freq = 545000 }, >> + { .cpu_freq = 1748, .mem_freq = 725000 }, >> + { .cpu_freq = 2496, .mem_freq = 840000 }, >> + { .cpu_freq = 2900, .mem_freq = 959000 }, >> + { .cpu_freq = 3514, .mem_freq = 1211000 }, >> + } >> + }, >> + { >> + .name = "mon_2", >> + .cpu_mask = 0x3f000, >> + .ipm_ceil = 60000000, >> + .be_stall_floor = 1, >> + .table_len = 7, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 356, .mem_freq = 315000 }, >> + { .cpu_freq = 1018, .mem_freq = 479000 }, >> + { .cpu_freq = 1536, .mem_freq = 545000 }, >> + { .cpu_freq = 1748, .mem_freq = 725000 }, >> + { .cpu_freq = 2496, .mem_freq = 840000 }, >> + { .cpu_freq = 2900, .mem_freq = 959000 }, >> + { .cpu_freq = 3514, .mem_freq = 1211000 }, >> + } >> + }, >> + }, >> + }, >> + { >> + .memory_type = MEMLAT_HW_DDR_QOS_COMPUTE, >> + .name = "ddr-qos", >> + .monitor_cnt = 3, >> + .mem_table = glymur_ddr_qos_table, >> + .num_opps = ARRAY_SIZE(glymur_ddr_qos_table), >> + .grp_ev = glymur_ddr_qos_grp_ev, >> + .memory_range = { .min_freq = 0, .max_freq = 1}, >> + .monitor_cfg = (const struct scmi_qcom_monitor_cfg[]) { >> + { >> + .name = "mon_0", >> + .cpu_mask = 0x3f, >> + .ipm_ceil = 80000000, >> + .be_stall_floor = 1, >> + .table_len = 2, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2362, .mem_freq = 0 }, >> + { .cpu_freq = 2938, .mem_freq = 1 }, >> + } >> + }, >> + { >> + .name = "mon_1", >> + .cpu_mask = 0xfc0, >> + .ipm_ceil = 80000000, >> + .be_stall_floor = 1, >> + .table_len = 2, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2496, .mem_freq = 0 }, >> + { .cpu_freq = 3514, .mem_freq = 1 }, >> + } >> + }, >> + { >> + .name = "mon_2", >> + .cpu_mask = 0x3f000, >> + .ipm_ceil = 80000000, >> + .be_stall_floor = 1, >> + .table_len = 2, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2496, .mem_freq = 0 }, >> + { .cpu_freq = 3514, .mem_freq = 1 }, >> + } >> + }, >> + }, >> + }, >> +}; >> + >> +static const struct scmi_qcom_memory_cfg hamoa_memory_cfg[] = { >> + { >> + .memory_type = MEMLAT_HW_DDR, >> + .name = "ddr", >> + .mem_table = hamoa_ddr_table, >> + .num_opps = ARRAY_SIZE(hamoa_ddr_table), >> + .grp_ev = hamoa_ddr_grp_ev, >> + .monitor_cnt = 4, >> + .memory_range = { .min_freq = 200000, .max_freq = 4224000}, >> + .monitor_cfg = (const struct scmi_qcom_monitor_cfg[]) { >> + { >> + .name = "mon_0", >> + .cpu_mask = 0xf, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 6, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 999, .mem_freq = 547000 }, >> + { .cpu_freq = 1440, .mem_freq = 768000 }, >> + { .cpu_freq = 1671, .mem_freq = 1555000 }, >> + { .cpu_freq = 2189, .mem_freq = 2092000 }, >> + { .cpu_freq = 2516, .mem_freq = 3187000 }, >> + { .cpu_freq = 3860, .mem_freq = 4224000 }, >> + } >> + }, >> + { >> + .name = "mon_1", >> + .cpu_mask = 0xf0, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 6, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 999, .mem_freq = 547000 }, >> + { .cpu_freq = 1440, .mem_freq = 768000 }, >> + { .cpu_freq = 1671, .mem_freq = 1555000 }, >> + { .cpu_freq = 2189, .mem_freq = 2092000 }, >> + { .cpu_freq = 2516, .mem_freq = 3187000 }, >> + { .cpu_freq = 3860, .mem_freq = 4224000 }, >> + } >> + }, >> + { >> + .name = "mon_2", >> + .cpu_mask = 0xf00, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 6, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 999, .mem_freq = 547000 }, >> + { .cpu_freq = 1440, .mem_freq = 768000 }, >> + { .cpu_freq = 1671, .mem_freq = 1555000 }, >> + { .cpu_freq = 2189, .mem_freq = 2092000 }, >> + { .cpu_freq = 2516, .mem_freq = 3187000 }, >> + { .cpu_freq = 3860, .mem_freq = 4224000 }, >> + } >> + }, >> + { >> + .name = "mon_3", >> + .cpu_mask = 0xfff, >> + .table_len = 4, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 1440, .mem_freq = 547000 }, >> + { .cpu_freq = 2189, .mem_freq = 768000 }, >> + { .cpu_freq = 2516, .mem_freq = 1555000 }, >> + { .cpu_freq = 3860, .mem_freq = 2092000 }, >> + } >> + }, >> + }, >> + }, >> + { >> + .memory_type = MEMLAT_HW_LLCC, >> + .name = "llcc", >> + .mem_table = hamoa_llcc_table, >> + .num_opps = ARRAY_SIZE(hamoa_llcc_table), >> + .grp_ev = hamoa_llcc_grp_ev, >> + .monitor_cnt = 3, >> + .memory_range = { .min_freq = 300000, .max_freq = 1066000}, >> + .monitor_cfg = (const struct scmi_qcom_monitor_cfg[]) { >> + { >> + .name = "mon_0", >> + .cpu_mask = 0xf, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 6, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 999, .mem_freq = 300000 }, >> + { .cpu_freq = 1440, .mem_freq = 466000 }, >> + { .cpu_freq = 1671, .mem_freq = 600000 }, >> + { .cpu_freq = 2189, .mem_freq = 806000 }, >> + { .cpu_freq = 2516, .mem_freq = 933000 }, >> + { .cpu_freq = 3860, .mem_freq = 1066000 }, >> + } >> + }, >> + { >> + .name = "mon_1", >> + .cpu_mask = 0xf0, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 6, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 999, .mem_freq = 300000 }, >> + { .cpu_freq = 1440, .mem_freq = 466000 }, >> + { .cpu_freq = 1671, .mem_freq = 600000 }, >> + { .cpu_freq = 2189, .mem_freq = 806000 }, >> + { .cpu_freq = 2516, .mem_freq = 933000 }, >> + { .cpu_freq = 3860, .mem_freq = 1066000 }, >> + } >> + }, >> + { >> + .name = "mon_2", >> + .cpu_mask = 0xf00, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 6, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 999, .mem_freq = 300000 }, >> + { .cpu_freq = 1440, .mem_freq = 466000 }, >> + { .cpu_freq = 1671, .mem_freq = 600000 }, >> + { .cpu_freq = 2189, .mem_freq = 806000 }, >> + { .cpu_freq = 2516, .mem_freq = 933000 }, >> + { .cpu_freq = 3860, .mem_freq = 1066000 }, >> + } >> + }, >> + }, >> + }, >> + { >> + .memory_type = MEMLAT_HW_DDR_QOS_COMPUTE, >> + .name = "ddr-qos", >> + .monitor_cnt = 3, >> + .mem_table = glymur_ddr_qos_table, >> + .num_opps = ARRAY_SIZE(glymur_ddr_qos_table), >> + .grp_ev = hamoa_ddr_qos_grp_ev, >> + .memory_range = { .min_freq = 0, .max_freq = 1}, >> + .monitor_cfg = (const struct scmi_qcom_monitor_cfg[]) { >> + { >> + .name = "mon_0", >> + .cpu_mask = 0xf, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 2, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2189, .mem_freq = 0 }, >> + { .cpu_freq = 3860, .mem_freq = 1 }, >> + } >> + }, >> + { >> + .name = "mon_1", >> + .cpu_mask = 0xf0, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 2, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2189, .mem_freq = 0 }, >> + { .cpu_freq = 3860, .mem_freq = 1 }, >> + } >> + }, >> + { >> + .name = "mon_2", >> + .cpu_mask = 0xf00, >> + .ipm_ceil = 20000000, >> + .be_stall_floor = 1, >> + .table_len = 2, >> + .table = (const struct scmi_qcom_map_table[]) { >> + { .cpu_freq = 2189, .mem_freq = 0 }, >> + { .cpu_freq = 3860, .mem_freq = 1 }, >> + } >> + }, >> + }, >> + }, >> +}; >> + >> +static const struct scmi_qcom_memlat_cfg_data glymur_memlat_data = { >> + .memory_cfg = glymur_memory_cfg, >> + .common_ev = glymur_common_ev, >> + .cpucp_freq_method = CPUCP_EFFECTIVE_FREQ_CALC_METHOD_1, >> + .cpucp_sample_ms = 4, >> + .memory_cnt = ARRAY_SIZE(glymur_memory_cfg), >> +}; >> + >> +static const struct scmi_qcom_memlat_cfg_data hamoa_memlat_data = { >> + .memory_cfg = hamoa_memory_cfg, >> + .common_ev = hamoa_common_ev, >> + .cpucp_freq_method = CPUCP_EFFECTIVE_FREQ_CALC_METHOD_1, >> + .cpucp_sample_ms = 4, >> + .memory_cnt = ARRAY_SIZE(hamoa_memory_cfg), >> +}; > > It would be wonderful to declare scmi_qcom_memlat_configs[] in the > vicinity of these two definitions. > Ack >> + >> +#endif >> diff --git a/drivers/devfreq/scmi-qcom-memlat-devfreq.c b/drivers/devfreq/scmi-qcom-memlat-devfreq.c >> new file mode 100644 >> index 000000000000..99ae229acfdc >> --- /dev/null >> +++ b/drivers/devfreq/scmi-qcom-memlat-devfreq.c >> @@ -0,0 +1,616 @@ >> +// SPDX-License-Identifier: GPL-2.0-only >> +/* >> + * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries. >> + */ >> + >> +#include <linux/cpu.h> >> +#include <linux/devfreq.h> >> +#include <linux/err.h> >> +#include <linux/errno.h> >> +#include <linux/init.h> >> +#include <linux/kernel.h> >> +#include <linux/module.h> >> +#include <linux/of.h> >> +#include <linux/platform_device.h> >> +#include <linux/pm_opp.h> >> +#include <linux/scmi_protocol.h> >> +#include <linux/scmi_qcom_protocol.h> >> +#include <linux/units.h> >> + >> +#define MAX_NAME_LEN 20 >> +#define MAX_MAP_ENTRIES 10 > > The names used for these defines are a bit generic... > I'll prefix them (MEMLAT_MAX_*). >> + >> +#include "scmi-qcom-memlat-cfg.h" >> + >> +/** >> + * enum scmi_memlat_protocol_cmd - parameter_ids supported by the "MEMLAT" algo_str hosted >> + * by the Qualcomm Generic Vendor Protocol on the SCMI controller. > > This is supposed to be the short description, yet it's the same length > as the long description on the very next line. > Ack, will make it single line. >> + * >> + * MEMLAT (Memory Latency) monitors the counters to detect memory latency bound workloads >> + * and scales the frequency/levels of the memory buses accordingly. >> + * >> + * @MEMLAT_SET_MEM_GROUP: initializes the frequency/level scaling functions for the memory bus. >> + * @MEMLAT_SET_MONITOR: configures the monitor to work on a specific memory bus. >> + * @MEMLAT_SET_COMMON_EV_MAP: set up common counters used to monitor the cpu frequency. >> + * @MEMLAT_SET_GRP_EV_MAP: set up any specific counters used to monitor the memory bus. >> + * @MEMLAT_IPM_CEIL: set the IPM (Instruction Per Misses) ceiling per monitor. >> + * @MEMLAT_BE_STALL_FLOOR: set the back-end stall floor per monitor. >> + * @MEMLAT_SAMPLE_MS: set the sampling period for all the monitors. >> + * @MEMLAT_MON_FREQ_MAP: setup the cpufreq to memfreq map. >> + * @MEMLAT_SET_MIN_FREQ: set the min frequency of the memory bus. >> + * @MEMLAT_SET_MAX_FREQ: set the max frequency of the memory bus. >> + * @MEMLAT_GET_CUR_FREQ: query the current frequency/level of the memory bus. >> + * @MEMLAT_START_TIMER: start all the monitors with the requested sampling period. >> + * @MEMLAT_STOP_TIMER: stop all the running monitors. >> + * @MEMLAT_SET_EFFECTIVE_FREQ_METHOD: set the method used to determine cpu frequency. >> + */ >> +enum scmi_memlat_protocol_cmd { > > Don't use "enum" when you don't mean "enumeration". > Ack, these are fixed SCMI parameter IDs with explicit values and gaps that must match the firmware, so I'll switch them to #define. >> + MEMLAT_SET_MEM_GROUP = 16, >> + MEMLAT_SET_MONITOR, >> + MEMLAT_SET_COMMON_EV_MAP, >> + MEMLAT_SET_GRP_EV_MAP, >> + MEMLAT_IPM_CEIL = 23, >> + MEMLAT_BE_STALL_FLOOR = 25, >> + MEMLAT_SAMPLE_MS = 31, >> + MEMLAT_MON_FREQ_MAP, >> + MEMLAT_SET_MIN_FREQ, >> + MEMLAT_SET_MAX_FREQ, >> + MEMLAT_GET_CUR_FREQ, >> + MEMLAT_START_TIMER = 36, >> + MEMLAT_STOP_TIMER, >> + MEMLAT_SET_EFFECTIVE_FREQ_METHOD = 39, >> +}; >> + >> +struct cpucp_map_table { >> + __le16 v1; >> + __le16 v2; > > v1 v2? > > As far as I can tell these two numbers are cpu and memory frequency. > v1 is CPU freq and v2 is memory freq (both in MHz). I'll rename them accordingly. >> +}; >> + >> +struct map_param_msg { >> + __le32 hw_type; >> + __le32 mon_idx; >> + __le32 nr_rows; >> + struct cpucp_map_table tbl[MAX_MAP_ENTRIES]; >> +} __packed; > > Does this really need the __packed? And if so, why is the only message > that you explicitly mark as packed? > It doesn't. I'll drop the __packed so all the message structs are consistent. >> + >> +struct node_msg { >> + __le32 cpumask; >> + __le32 hw_type; >> + __le32 mon_type; >> + __le32 mon_idx; >> + char mon_name[MAX_NAME_LEN]; >> +}; >> + >> +struct scalar_param_msg { >> + __le32 hw_type; >> + __le32 mon_idx; >> + __le32 val; >> +}; >> + >> +struct ev_map_msg { >> + __le32 num_evs; >> + __le32 hw_type; >> + __le32 cid[NUM_COMMON_EVS]; >> +}; >> + >> +struct scmi_qcom_memlat_map { >> + unsigned int cpufreq_mhz; >> + unsigned int memfreq_khz; > > unit soup > Same fix as above, will make both MHz. >> +}; >> + >> +struct scmi_qcom_monitor_info { >> + struct scmi_qcom_memlat_map *freq_map; >> + char name[MAX_NAME_LEN]; >> + u32 mon_idx; >> + u32 mon_type; >> + u32 ipm_ceil; >> + u32 be_stall_floor; >> + u32 mask; >> + u32 freq_map_len; > > Most of these values are merely copied from const tables to heap on > probe - verbatim - and then put in messages. Would it be possible to > just reference the static configuration, or are the any dynamic aspects > that I'm missing? > Apart from sample_ms, there's nothing dynamic here, it really is just a copy. I'll build the messages directly from the const config instead of staging it in heap structs, which also removes most of the allocation and cleanup below. >> +}; >> + >> +struct scmi_qcom_memory_info { >> + struct scmi_qcom_monitor_info **monitor; >> + u32 hw_type; >> + int monitor_cnt; > > Why signed? > Ack, will make unsigned. >> + u32 min_freq; >> + u32 max_freq; > > Throughout the driver we see frequencies expressed in mhz, khz, and hz. > Suffix these to help the reader. > Ack >> + struct devfreq_dev_profile profile; >> + struct devfreq *devfreq; >> + struct platform_device *pdev; >> + struct scmi_protocol_handle *ph; >> + const struct qcom_generic_ext_ops *ops; >> +}; >> + >> +struct scmi_qcom_memlat_info { >> + struct scmi_protocol_handle *ph; >> + const struct qcom_generic_ext_ops *ops; >> + const struct scmi_qcom_memlat_cfg_data *cfg_data; >> + struct scmi_qcom_memory_info **memory; >> + u32 cpucp_freq_method; >> + u32 cpucp_sample_ms; >> + int memory_cnt; > > Sounds like an unsigned to me > Ack >> +}; >> + >> +static int configure_cpucp_common_events(struct scmi_qcom_memlat_info *info, >> + const struct scmi_qcom_memlat_cfg_data *cfg_data) >> +{ >> + const struct qcom_generic_ext_ops *ops = info->ops; >> + struct ev_map_msg msg = {}; >> + int i; >> + >> + msg.num_evs = cpu_to_le32(NUM_COMMON_EVS); >> + /* Common events apply to all groups; INVALID_IDX flags "no specific group". */ > > Does hw_type of INVALID_IDX mean "all groups", or "any (no specific) > group"? > > Is the protocol defined that you pass "invalid index" for both hw_type > and cid? Or do we just reuse the INVALID_IDX define because it happens > to have the right value? > > Why not "#define HW_TYPE_ALL 0xff"? Then you can skip the second half of > the comment above, which only purpose is to clarify why the code looks > "wrong". > Common events apply to every group, so hw_type is a don't-care here. Reusing INVALID_IDX was just convenient. I'll add a named #define HW_TYPE_ALL 0xff and drop the apologetic comment. >> + msg.hw_type = cpu_to_le32(INVALID_IDX); >> + for (i = 0; i < NUM_COMMON_EVS; i++) >> + msg.cid[i] = cpu_to_le32(cfg_data->common_ev[i]); >> + >> + return ops->set_param(info->ph, &msg, sizeof(msg), MEMLAT_ALGO_STR, > > This is always info->ops->set_param(info->ph, ..., ..., MEMLAT_ALGO_STR, > ...), why not create a helper unction that takes info, the command, > message and size instead? > > Would make the code less noisy. > Ack, I'll add a small wrapper (memlat_set_param(info, cmd, msg, size)) and use it throughout. >> + MEMLAT_SET_COMMON_EV_MAP); >> +} >> + >> +static int configure_cpucp_grp(struct device *dev, struct scmi_qcom_memlat_info *info, >> + const struct scmi_qcom_memlat_cfg_data *cfg_data, >> + int memory_index) >> +{ >> + const u32 *grp_ev = cfg_data->memory_cfg[memory_index].grp_ev; >> + struct scmi_qcom_memory_info *memory = info->memory[memory_index]; >> + const struct qcom_generic_ext_ops *ops = info->ops; >> + struct ev_map_msg ev_msg = {}; >> + struct node_msg msg = {}; >> + int ret; >> + int i; >> + >> + msg.cpumask = cpu_to_le32(*cpumask_bits(cpu_possible_mask)); >> + msg.hw_type = cpu_to_le32(memory->hw_type); >> + msg.mon_type = 0; >> + msg.mon_idx = 0; >> + ret = ops->set_param(info->ph, &msg, sizeof(msg), MEMLAT_ALGO_STR, MEMLAT_SET_MEM_GROUP); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to configure mem type %d\n", >> + memory->hw_type); >> + >> + ev_msg.num_evs = cpu_to_le32(NUM_GRP_EVS); >> + ev_msg.hw_type = cpu_to_le32(memory->hw_type); >> + for (i = 0; i < NUM_GRP_EVS; i++) >> + ev_msg.cid[i] = cpu_to_le32(grp_ev[i]); >> + >> + ret = ops->set_param(info->ph, &ev_msg, sizeof(ev_msg), MEMLAT_ALGO_STR, >> + MEMLAT_SET_GRP_EV_MAP); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to configure event map for mem type %d\n", >> + memory->hw_type); >> + >> + return ret; >> +} >> + >> +static int configure_cpucp_mon(struct device *dev, struct scmi_qcom_memlat_info *info, >> + int memory_index, int monitor_index) >> +{ >> + const struct qcom_generic_ext_ops *ops = info->ops; >> + struct scmi_qcom_memory_info *memory = info->memory[memory_index]; >> + struct scmi_qcom_monitor_info *monitor = memory->monitor[monitor_index]; >> + struct scalar_param_msg scalar_msg = {}; >> + struct map_param_msg map_msg = {}; >> + struct node_msg msg = {}; > > So you have scalar_msg, map_msg, and msg... Why is "msg" just "msg" and > not "node_msg", is it special? > > If you made scalar_param_msg the "msg" instead, this function would be > significantly less characters :) > > In fact, is it significant that these local variables are denoted to be > messages? If you name them "scalar", "map" and "node" you will reduce > the noise below quite a bit. > > Same goes for all other functions that has multiple messages - where the > message type is the more significant fact, and you choose to name the > variable "this is a message". > Ack, I'll rename to scalar/map/node (dropping the _msg suffix) here and in the other functions that hold several or maybe drop this entirely. >> + int ret; >> + int i; >> + >> + msg.cpumask = cpu_to_le32(monitor->mask); >> + msg.hw_type = cpu_to_le32(memory->hw_type); >> + msg.mon_type = cpu_to_le32(monitor->mon_type); >> + msg.mon_idx = cpu_to_le32(monitor->mon_idx); >> + strscpy(msg.mon_name, monitor->name, sizeof(msg.mon_name)); >> + ret = ops->set_param(info->ph, &msg, sizeof(msg), MEMLAT_ALGO_STR, MEMLAT_SET_MONITOR); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to configure monitor %s\n", >> + monitor->name); >> + >> + scalar_msg.hw_type = cpu_to_le32(memory->hw_type); >> + scalar_msg.mon_idx = cpu_to_le32(monitor->mon_idx); >> + scalar_msg.val = cpu_to_le32(monitor->ipm_ceil); >> + ret = ops->set_param(info->ph, &scalar_msg, sizeof(scalar_msg), MEMLAT_ALGO_STR, >> + MEMLAT_IPM_CEIL); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to set ipm ceil for %s\n", >> + monitor->name); >> + >> + scalar_msg.hw_type = cpu_to_le32(memory->hw_type); >> + scalar_msg.mon_idx = cpu_to_le32(monitor->mon_idx); >> + scalar_msg.val = cpu_to_le32(monitor->be_stall_floor); >> + ret = ops->set_param(info->ph, &scalar_msg, sizeof(scalar_msg), MEMLAT_ALGO_STR, >> + MEMLAT_BE_STALL_FLOOR); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to set be_stall_floor for %s\n", >> + monitor->name); >> + >> + map_msg.hw_type = cpu_to_le32(memory->hw_type); >> + map_msg.mon_idx = cpu_to_le32(monitor->mon_idx); >> + map_msg.nr_rows = cpu_to_le32(monitor->freq_map_len); >> + for (i = 0; i < monitor->freq_map_len; i++) { >> + map_msg.tbl[i].v1 = cpu_to_le16(monitor->freq_map[i].cpufreq_mhz); >> + >> + /* >> + * Wire format v2 is u16 in MHz; convert from kHz. > > Why is the freq_map cpufreq defined in MHz, but the memfreq in kHz - and > then we convert the memory frequency to MHz before we write it? > > Why are they not both defined in MHz? > Ack, Firmware expects both values in MHz so I'll make memfreq as MHz in header as well. >> For DDR_QOS >> + * the table holds level indices (0 / 1) rather than real >> + * frequencies, so pass them through unchanged. > > "holds level indices", "pass them through unchanged"? What are you > trying to say? > I meant: for DDR_QOS these are 0/1 levels, not frequencies, so don't divide them. Won't need this anyway. >> + */ >> + if (monitor->freq_map[i].memfreq_khz > 1) > > If you change memfreq_khz to memfreq_mhz you can remove the whole > comment, the condition, as well as the divide. There might be a reason > for you to do this, but the comment is poor, so I simply can't tell. > Ack >> + map_msg.tbl[i].v2 = cpu_to_le16(monitor->freq_map[i].memfreq_khz / 1000); >> + else >> + map_msg.tbl[i].v2 = cpu_to_le16(monitor->freq_map[i].memfreq_khz); >> + } >> + ret = ops->set_param(info->ph, &map_msg, sizeof(map_msg), MEMLAT_ALGO_STR, >> + MEMLAT_MON_FREQ_MAP); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to configure freq_map for %s\n", >> + monitor->name); >> + >> + scalar_msg.hw_type = cpu_to_le32(memory->hw_type); >> + scalar_msg.mon_idx = cpu_to_le32(monitor->mon_idx); >> + scalar_msg.val = cpu_to_le32(memory->min_freq); >> + ret = ops->set_param(info->ph, &scalar_msg, sizeof(scalar_msg), MEMLAT_ALGO_STR, >> + MEMLAT_SET_MIN_FREQ); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to set min_freq for %s\n", >> + monitor->name); >> + >> + scalar_msg.hw_type = cpu_to_le32(memory->hw_type); >> + scalar_msg.mon_idx = cpu_to_le32(monitor->mon_idx); >> + scalar_msg.val = cpu_to_le32(memory->max_freq); >> + ret = ops->set_param(info->ph, &scalar_msg, sizeof(scalar_msg), MEMLAT_ALGO_STR, >> + MEMLAT_SET_MAX_FREQ); >> + if (ret < 0) >> + return dev_err_probe(dev, ret, "failed to set max_freq for %s\n", monitor->name); >> + >> + return ret; >> +} >> + >> +static int scmi_qcom_devfreq_get_cur_freq(struct device *dev, unsigned long *freq) >> +{ >> + struct scmi_qcom_memory_info *memory = dev_get_drvdata(dev); >> + const struct qcom_generic_ext_ops *ops = memory->ops; >> + struct scalar_param_msg scalar_msg = {}; >> + u32 max_freq_khz = 0; >> + __le32 cur_freq; >> + int ret, i; >> + >> + /* >> + * MEMLAT_GET_CUR_FREQ returns target_freq for a single (hw_type, >> + * mon_idx) tuple. The bus's actual voted frequency is the max across >> + * all configured monitors in the group, so query each one and pick >> + * the highest vote. > > You can say this with fewer words. > Ack >> + */ >> + for (i = 0; i < memory->monitor_cnt; i++) { >> + scalar_msg.hw_type = cpu_to_le32(memory->hw_type); >> + scalar_msg.mon_idx = cpu_to_le32(memory->monitor[i]->mon_idx); >> + scalar_msg.val = 0; >> + >> + ret = ops->get_param(memory->ph, &scalar_msg, sizeof(scalar_msg), >> + MEMLAT_ALGO_STR, MEMLAT_GET_CUR_FREQ, >> + sizeof(cur_freq)); >> + if (ret < 0) { >> + dev_err(dev, "failed to get current frequency for %s\n", >> + memory->monitor[i]->name); >> + return ret; > > We're grabbing a bunch of these, is it really fatal if one fails? > No, it's a poll callback, so bailing on one failed monitor query is too harsh. I'll skip the failing monitor and keep aggregating the rest. >> + } >> + >> + /* qcom_scmi_common_xfer() returns the response into the same tx buffer. */ > > What is qcom_scmi_common_xfer()? > > You can express in C the fact that msg is a in/out - by declaring it as > a union. > > That way you don't need a comment to describe why you're reinterpreting > the first 4 bytes of msg as the result of the query. > It's the internal transport that reuses the tx buffer for the response. I'll try to express the in/out with a union (request struct plus a __le32 response) so the reinterpret is self-documenting and the memcpy and comment can go. >> + memcpy(&cur_freq, &scalar_msg, sizeof(cur_freq)); >> + if (le32_to_cpu(cur_freq) > max_freq_khz) >> + max_freq_khz = le32_to_cpu(cur_freq); > > And you could then le32_to_cpu() out of the union to a local variable > once, to avoid the double le32_to_cpu(). > Ack >> + } >> + >> + /* >> + * Frequency-scaled buses (DDR/LLCC) report cur_freq in kHz; convert >> + * to Hz to match the devfreq OPP table units. Level-based buses >> + * (e.g. DDR_QOS_COMPUTE) configure max_freq == 1 because the firmware >> + * reports a 0/1 level rather than a frequency, and the matching >> + * synthetic OPP keys (1 / 100) live in glymur_ddr_qos_table. > > You surely don't need 5 lines of solid text to express this. > Ack, will cut it down to a line or two. >> + */ >> + if (memory->max_freq > 1) >> + *freq = max_freq_khz * 1000UL; >> + else >> + *freq = max_freq_khz ? 100 : 1; >> + >> + return 0; >> +} >> + >> +static void scmi_qcom_memlat_unwind(struct scmi_qcom_memlat_info *info, int count) >> +{ >> + for (int i = 0; i < count; i++) { >> + struct scmi_qcom_memory_info *memory = info->memory[i]; >> + >> + if (IS_ERR_OR_NULL(memory) || IS_ERR_OR_NULL(memory->pdev)) > > I don't get it, how can "memory" be IS_ERR()? Isn't it the result of the > devm_kcalloc() in scmi_qcom_memlat_parse_cfg()? > Ack, I'll use a plain !memory check (or drop it once the cleanup is restructured). >> + continue; >> + >> + dev_pm_opp_remove_all_dynamic(&memory->pdev->dev); >> + platform_device_unregister(memory->pdev); >> + } >> +} >> + >> +static int scmi_qcom_memlat_configure_events(struct scmi_device *sdev, >> + struct scmi_qcom_memlat_info *info) >> +{ >> + const struct qcom_generic_ext_ops *ops = info->ops; >> + struct scmi_protocol_handle *ph = info->ph; >> + __le32 sample_ms, freq_method; >> + int i, j, ret; >> + >> + /* Configure common events ids */ >> + ret = configure_cpucp_common_events(info, info->cfg_data); >> + if (ret < 0) >> + return dev_err_probe(&sdev->dev, ret, "failed to configure common events\n"); >> + >> + for (i = 0; i < info->memory_cnt; i++) { >> + /* Configure per group parameters */ >> + ret = configure_cpucp_grp(&sdev->dev, info, info->cfg_data, i); >> + if (ret < 0) >> + return ret; >> + >> + for (j = 0; j < info->memory[i]->monitor_cnt; j++) { >> + /* Configure per monitor parameters */ >> + ret = configure_cpucp_mon(&sdev->dev, info, i, j); >> + if (ret < 0) >> + return ret; >> + } >> + } >> + >> + /* Set loop sampling time */ >> + sample_ms = cpu_to_le32(info->cpucp_sample_ms); >> + ret = ops->set_param(ph, &sample_ms, sizeof(sample_ms), >> + MEMLAT_ALGO_STR, MEMLAT_SAMPLE_MS); >> + if (ret < 0) >> + return dev_err_probe(&sdev->dev, ret, "failed to set sample_ms\n"); >> + >> + /* Set the effective cpu frequency calculation method */ >> + freq_method = cpu_to_le32(info->cpucp_freq_method); >> + ret = ops->set_param(ph, &freq_method, sizeof(freq_method), >> + MEMLAT_ALGO_STR, MEMLAT_SET_EFFECTIVE_FREQ_METHOD); >> + if (ret < 0) >> + return dev_err_probe(&sdev->dev, ret, >> + "failed to set effective frequency calc method\n"); >> + >> + /* Start sampling and voting timer */ >> + ret = ops->start_activity(ph, NULL, 0, MEMLAT_ALGO_STR, MEMLAT_START_TIMER); >> + if (ret < 0) >> + return dev_err_probe(&sdev->dev, ret, "failed to start memory group timer\n"); >> + >> + for (i = 0; i < info->memory_cnt; i++) { >> + struct scmi_qcom_memory_info *memory = info->memory[i]; >> + struct platform_device *pdev = memory->pdev; >> + struct devfreq_dev_profile *profile = &memory->profile; >> + >> + /* sampling time should be double the devfreq observing time */ > > That's interesting, tell me more... > This follows Lukasz's earlier point on Nyquist criterion: sample about 2x faster than the changes you want to observe. CPUCP updates every cpucp_sample_ms, so the devfreq poll runs at half that (sample_ms / 2) to actually catch the transitions in trans_stat. >> + profile->polling_ms = max(1U, info->cpucp_sample_ms / 2); >> + profile->get_cur_freq = scmi_qcom_devfreq_get_cur_freq; >> + profile->initial_freq = memory->min_freq > 1 ? >> + (memory->min_freq * 1000UL) : memory->min_freq; > > What is the purpose of this conditional? > > You support memories with an actual frequency, and what you call > "level". But for any memory with a "level" resource other than a boolean > this logic fails. > > For min_freq == 0 we get initial_freq of 0, both for frequency-based and > level-based memories. > > So the only case where this actually does anything is if you have a > boolean resource with minimum value of 1 (i.e. a constant). Do we have > those? > The guard just separates DDR_QOS (level-based) from the frequency buses: for DDR/LLCC, devfreq wants Hz, so initial_freq = min_freq * 1000, while DDR_QOS passes its value through as-is. >> + >> + platform_set_drvdata(pdev, memory); >> + >> + memory->devfreq = devm_devfreq_add_device(&pdev->dev, profile, >> + DEVFREQ_GOV_REMOTE, NULL); >> + if (IS_ERR(memory->devfreq)) { >> + dev_err(&sdev->dev, "failed to add devfreq device\n"); > > I personally don't like all the dev_err_probe() that you have sprinkled > throughout the driver, but I know that many others do - and this one > isn't consistent. > Ack, I'll use dev_err_probe uniformly on the probe paths. >> + /* Stop sampling and voting timer */ >> + ret = ops->stop_activity(ph, NULL, 0, MEMLAT_ALGO_STR, MEMLAT_STOP_TIMER); >> + if (ret < 0) >> + dev_err_probe(&sdev->dev, ret, >> + "failed to stop memory group timer\n"); > > The significant event is that adding the devfreq device failed, but the > error message that you retain is "failed to stop memory group timer"... > I am already capturing devfreq failure just above. This is inside that scope. Do you want me to drop this? >> + return PTR_ERR(memory->devfreq); >> + } >> + } >> + >> + return 0; >> +} >> + >> +static struct scmi_qcom_memlat_map * >> +scmi_qcom_parse_memlat_map(struct device *dev, const struct scmi_qcom_monitor_cfg *mon_cfg) >> +{ >> + struct scmi_qcom_memlat_map *map_table; >> + const struct scmi_qcom_map_table *table; >> + >> + if (mon_cfg->table_len > MAX_MAP_ENTRIES) >> + return ERR_PTR(-EINVAL); >> + >> + map_table = devm_kcalloc(dev, mon_cfg->table_len, sizeof(*map_table), >> + GFP_KERNEL); >> + if (!map_table) >> + return ERR_PTR(-ENOMEM); >> + >> + for (int i = 0; i < mon_cfg->table_len; i++) { >> + table = &mon_cfg->table[i]; >> + >> + map_table[i].cpufreq_mhz = table->cpu_freq; >> + map_table[i].memfreq_khz = table->mem_freq; >> + } >> + >> + return map_table; >> +} >> + >> +static const struct of_device_id scmi_qcom_memlat_configs[] = { >> + { .compatible = "qcom,glymur", .data = &glymur_memlat_data}, >> + { .compatible = "qcom,mahua", .data = &glymur_memlat_data}, >> + { .compatible = "qcom,x1e80100", .data = &hamoa_memlat_data}, >> + { .compatible = "qcom,x1p42100", .data = &hamoa_memlat_data}, >> + { } >> +}; >> + >> +static int scmi_qcom_memlat_parse_cfg(struct scmi_device *sdev, struct scmi_qcom_memlat_info *info) >> +{ >> + const struct scmi_qcom_memlat_cfg_data *cfg_data; >> + struct scmi_qcom_monitor_info *monitor; >> + struct scmi_qcom_memory_info *memory; >> + int ret, i, j; >> + >> + cfg_data = of_machine_get_match_data(scmi_qcom_memlat_configs); >> + if (!cfg_data) { >> + /* >> + * The SCMI generic-ext protocol can bind on Qualcomm SoCs that >> + * do not ship CPUCP memlat. Bail out quietly in that case rather >> + * than printing an error on every such system. >> + */ >> + dev_dbg(&sdev->dev, "no memlat config data for this platform\n"); >> + return -ENODEV; >> + } >> + >> + info->memory = devm_kcalloc(&sdev->dev, cfg_data->memory_cnt, >> + sizeof(*info->memory), GFP_KERNEL); >> + if (!info->memory) >> + return -ENOMEM; >> + >> + for (i = 0; i < cfg_data->memory_cnt; i++) { >> + const struct scmi_qcom_memory_cfg *memory_cfg = &cfg_data->memory_cfg[i]; >> + struct platform_device_info pdevinfo = { 0 }; >> + >> + pdevinfo.parent = &sdev->dev; >> + pdevinfo.name = memory_cfg->name; >> + pdevinfo.id = PLATFORM_DEVID_NONE; >> + >> + memory = devm_kzalloc(&sdev->dev, sizeof(*memory), GFP_KERNEL); >> + if (!memory) >> + return -ENOMEM; >> + >> + memory->ops = info->ops; >> + memory->ph = info->ph; >> + memory->hw_type = memory_cfg->memory_type; >> + memory->monitor_cnt = memory_cfg->monitor_cnt; >> + memory->min_freq = memory_cfg->memory_range.min_freq; >> + memory->max_freq = memory_cfg->memory_range.max_freq; >> + >> + memory->pdev = platform_device_register_full(&pdevinfo); > > Why are there platform_devices in here? > > I'm guessing that you want the struct devices to hang the opp tables > off? But why a whole platform_device? > Yes, I'll try to explore a lighter alternative. >> + if (IS_ERR(memory->pdev)) >> + return dev_err_probe(&sdev->dev, PTR_ERR(memory->pdev), >> + "failed to register platform device\n"); >> + >> + info->memory[i] = memory; >> + /* Track progress so probe() can unwind on a later failure. */ > > Please clean up your own mess. If this function becomes too long for you > to be able to get things in order, that's just another indication that > you need to refactor this function. > Ack, I'll refactor so each function unwinds its own partial work on error, and drop the external progress counter. >> + info->memory_cnt = i + 1; >> + >> + for (j = 0; j < memory_cfg->num_opps; j++) { >> + const struct scmi_qcom_opp_data *table = &memory_cfg->mem_table[j]; >> + struct platform_device *pdev = memory->pdev; >> + struct dev_pm_opp_data data = {}; >> + >> + data.freq = table->freq; >> + data.level = table->level; >> + >> + ret = dev_pm_opp_add_dynamic(&pdev->dev, &data); >> + if (ret) >> + return dev_err_probe(&sdev->dev, ret, "failed to add OPP\n"); >> + } >> + >> + memory->monitor = devm_kcalloc(&sdev->dev, memory_cfg->monitor_cnt, >> + sizeof(*memory->monitor), GFP_KERNEL); >> + if (!memory->monitor) >> + return -ENOMEM; >> + >> + for (j = 0; j < memory_cfg->monitor_cnt; j++) { >> + const struct scmi_qcom_monitor_cfg *mon_cfg = &memory_cfg->monitor_cfg[j]; >> + >> + monitor = devm_kzalloc(&sdev->dev, sizeof(*monitor), GFP_KERNEL); >> + if (!monitor) >> + return -ENOMEM; >> + >> + monitor->ipm_ceil = mon_cfg->ipm_ceil; >> + /* mon_type 0 = IPM-based latency monitor; 1 = stall-only (compute) */ >> + monitor->mon_type = monitor->ipm_ceil ? 0 : 1; > > So the actual mon_type in the message is just a 0 or 1? Is this 0 the > same as the 0 in configure_cpucp_grp()? Should we have a couple of > defines for these - instead of the comment? > It's just latency(0)/compute(1). I'll make it a bool is_compute in the driver too and drop the magic 0/1 and the comment. The mon_type/mon_idx in configure_cpucp_grp is a don't-care, I'll stop reusing a bare 0 there. >> + monitor->be_stall_floor = mon_cfg->be_stall_floor; >> + monitor->mask = mon_cfg->cpu_mask; >> + monitor->freq_map_len = mon_cfg->table_len; >> + >> + monitor->freq_map = scmi_qcom_parse_memlat_map(&sdev->dev, mon_cfg); > > It might be a good idea to do this before you're starting to register > platform_devices and opps, as it would simplify the cleanup. > Ack >> + if (IS_ERR(monitor->freq_map)) >> + return dev_err_probe(&sdev->dev, PTR_ERR(monitor->freq_map), >> + "failed to populate cpufreq-memfreq map\n"); > > There are two ways scmi_qcom_parse_memlat_map() can fail, either the > driver data is wrong and you get EINVAL (a developer BUG) or you get > ENOMEM, which is already printed... > Ack, will drop this. >> + >> + strscpy(monitor->name, mon_cfg->name, sizeof(monitor->name)); > > Do you really need a copy? As far as I can see the next access is > another copy out of this struct. > No, it's redundant. Once messages are built from the const config directly, I'll reference mon_cfg->name and drop the intermediate copy. >> + monitor->mon_idx = j; >> + memory->monitor[j] = monitor; >> + } >> + } >> + >> + info->cfg_data = cfg_data; >> + info->cpucp_freq_method = cfg_data->cpucp_freq_method; >> + info->cpucp_sample_ms = cfg_data->cpucp_sample_ms; > > So much code just moving data between two representations... > Ack, Building messages straight from the const config collapses the two representations and removes most of this function. >> + >> + return 0; >> +} >> + >> +static int scmi_qcom_devfreq_memlat_probe(struct scmi_device *sdev) >> +{ >> + const struct scmi_handle *handle = sdev->handle; >> + const struct qcom_generic_ext_ops *ops; >> + struct scmi_qcom_memlat_info *info; >> + struct scmi_protocol_handle *ph; >> + int ret; >> + >> + if (!handle) >> + return -ENODEV; >> + >> + info = devm_kzalloc(&sdev->dev, sizeof(*info), GFP_KERNEL); >> + if (!info) >> + return -ENOMEM; >> + >> + ops = handle->devm_protocol_get(sdev, SCMI_PROTOCOL_QCOM_GENERIC, &ph); >> + if (IS_ERR(ops)) >> + return PTR_ERR(ops); >> + >> + info->ops = ops; >> + info->ph = ph; >> + >> + ret = scmi_qcom_memlat_parse_cfg(sdev, info); >> + if (ret) { >> + scmi_qcom_memlat_unwind(info, info->memory_cnt); > > scmi_qcom_memlat_unwind() unwinds the work done by > scmi_qcom_memlat_parse_cfg(), this isn't really idiomatic. Please have > scmi_qcom_memlat_parse_cfg() clean up its own mess on failure. > Ack. The non-devm bits (platform_device_register_full and dev_pm_opp_add_dynamic) are why the external unwind exists, I'll handle those inside the function's own error paths. >> + return ret; >> + } >> + >> + ret = scmi_qcom_memlat_configure_events(sdev, info); >> + if (ret) { >> + scmi_qcom_memlat_unwind(info, info->memory_cnt); >> + return ret; >> + } >> + >> + dev_set_drvdata(&sdev->dev, info); >> + >> + return ret; >> +} >> + >> +static void scmi_qcom_devfreq_memlat_remove(struct scmi_device *sdev) >> +{ >> + struct scmi_qcom_memlat_info *info = dev_get_drvdata(&sdev->dev); >> + struct scmi_protocol_handle *ph; >> + const struct qcom_generic_ext_ops *ops; >> + int ret; >> + >> + if (!info) > > How is that possible? > It isn't, probe always sets drvdata to a valid info on success. I'll drop the !info check. >> + return; >> + >> + ph = info->ph; >> + ops = info->ops; >> + >> + ret = ops->stop_activity(ph, NULL, 0, MEMLAT_ALGO_STR, MEMLAT_STOP_TIMER); >> + if (ret < 0) >> + dev_err(&sdev->dev, "failed to stop memory group timer\n"); >> + >> + scmi_qcom_memlat_unwind(info, info->memory_cnt); >> +} >> + >> +static const struct scmi_device_id scmi_id_table[] = { >> + { SCMI_PROTOCOL_QCOM_GENERIC, "qcom-generic-ext" }, >> + { }, >> +}; >> +MODULE_DEVICE_TABLE(scmi, scmi_id_table); >> + >> +static struct scmi_driver scmi_qcom_devfreq_memlat_driver = { >> + .name = "scmi-qcom-devfreq-memlat", >> + .probe = scmi_qcom_devfreq_memlat_probe, >> + .remove = scmi_qcom_devfreq_memlat_remove, >> + .id_table = scmi_id_table, >> +}; >> +module_scmi_driver(scmi_qcom_devfreq_memlat_driver); >> + >> +MODULE_AUTHOR("Pragnesh Papaniya <[email protected]>"); >> +MODULE_DESCRIPTION("SCMI QCOM DEVFREQ MEMLAT driver"); > > Let go of the shift key! > > s/QCOM/Qualcomm/ at least. > > Regards, > Bjorn > Ack, I'll change it to sentence case: "Qualcomm SCMI memlat devfreq driver". Thanks for reviewing this! -Pragnesh >> +MODULE_LICENSE("GPL"); >> >> -- >> 2.34.1 >>