| // SPDX-License-Identifier: GPL-2.0+ | |
| /* | |
| * Surface Platform Profile / Performance Mode driver for Surface System | |
| * Aggregator Module (thermal and fan subsystem). | |
| * | |
| * Copyright (C) 2021-2022 Maximilian Luz <luzmaximilian@gmail.com> | |
| */ | |
| #include <linux/unaligned.h> | |
| #include <linux/kernel.h> | |
| #include <linux/module.h> | |
| #include <linux/platform_profile.h> | |
| #include <linux/types.h> | |
| #include <linux/surface_aggregator/device.h> | |
| // Enum for the platform performance profile sent to the TMP module. | |
| enum ssam_tmp_profile { | |
| SSAM_TMP_PROFILE_NORMAL = 1, | |
| SSAM_TMP_PROFILE_BATTERY_SAVER = 2, | |
| SSAM_TMP_PROFILE_BETTER_PERFORMANCE = 3, | |
| SSAM_TMP_PROFILE_BEST_PERFORMANCE = 4, | |
| }; | |
| // Enum for the fan profile sent to the FAN module. This fan profile is | |
| // only sent to the EC if the 'has_fan' property is set. The integers are | |
| // not a typo, they differ from the performance profile indices. | |
| enum ssam_fan_profile { | |
| SSAM_FAN_PROFILE_NORMAL = 2, | |
| SSAM_FAN_PROFILE_BATTERY_SAVER = 1, | |
| SSAM_FAN_PROFILE_BETTER_PERFORMANCE = 3, | |
| SSAM_FAN_PROFILE_BEST_PERFORMANCE = 4, | |
| }; | |
| struct ssam_tmp_profile_info { | |
| __le32 profile; | |
| __le16 unknown1; | |
| __le16 unknown2; | |
| } __packed; | |
| struct ssam_platform_profile_device { | |
| struct ssam_device *sdev; | |
| struct device *ppdev; | |
| bool has_fan; | |
| }; | |
| SSAM_DEFINE_SYNC_REQUEST_CL_R(__ssam_tmp_profile_get, struct ssam_tmp_profile_info, { | |
| .target_category = SSAM_SSH_TC_TMP, | |
| .command_id = 0x02, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_CL_W(__ssam_tmp_profile_set, __le32, { | |
| .target_category = SSAM_SSH_TC_TMP, | |
| .command_id = 0x03, | |
| }); | |
| SSAM_DEFINE_SYNC_REQUEST_W(__ssam_fan_profile_set, u8, { | |
| .target_category = SSAM_SSH_TC_FAN, | |
| .target_id = SSAM_SSH_TID_SAM, | |
| .command_id = 0x0e, | |
| .instance_id = 0x01, | |
| }); | |
| static int ssam_tmp_profile_get(struct ssam_device *sdev, enum ssam_tmp_profile *p) | |
| { | |
| struct ssam_tmp_profile_info info; | |
| int status; | |
| status = ssam_retry(__ssam_tmp_profile_get, sdev, &info); | |
| if (status < 0) | |
| return status; | |
| *p = le32_to_cpu(info.profile); | |
| return 0; | |
| } | |
| static int ssam_tmp_profile_set(struct ssam_device *sdev, enum ssam_tmp_profile p) | |
| { | |
| const __le32 profile_le = cpu_to_le32(p); | |
| return ssam_retry(__ssam_tmp_profile_set, sdev, &profile_le); | |
| } | |
| static int ssam_fan_profile_set(struct ssam_device *sdev, enum ssam_fan_profile p) | |
| { | |
| const u8 profile = p; | |
| return ssam_retry(__ssam_fan_profile_set, sdev->ctrl, &profile); | |
| } | |
| static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_profile p) | |
| { | |
| switch (p) { | |
| case SSAM_TMP_PROFILE_NORMAL: | |
| return PLATFORM_PROFILE_BALANCED; | |
| case SSAM_TMP_PROFILE_BATTERY_SAVER: | |
| return PLATFORM_PROFILE_LOW_POWER; | |
| case SSAM_TMP_PROFILE_BETTER_PERFORMANCE: | |
| return PLATFORM_PROFILE_BALANCED_PERFORMANCE; | |
| case SSAM_TMP_PROFILE_BEST_PERFORMANCE: | |
| return PLATFORM_PROFILE_PERFORMANCE; | |
| default: | |
| dev_err(&sdev->dev, "invalid performance profile: %d", p); | |
| return -EINVAL; | |
| } | |
| } | |
| static int convert_profile_to_ssam_tmp(struct ssam_device *sdev, enum platform_profile_option p) | |
| { | |
| switch (p) { | |
| case PLATFORM_PROFILE_LOW_POWER: | |
| return SSAM_TMP_PROFILE_BATTERY_SAVER; | |
| case PLATFORM_PROFILE_BALANCED: | |
| return SSAM_TMP_PROFILE_NORMAL; | |
| case PLATFORM_PROFILE_BALANCED_PERFORMANCE: | |
| return SSAM_TMP_PROFILE_BETTER_PERFORMANCE; | |
| case PLATFORM_PROFILE_PERFORMANCE: | |
| return SSAM_TMP_PROFILE_BEST_PERFORMANCE; | |
| default: | |
| /* This should have already been caught by platform_profile_store(). */ | |
| WARN(true, "unsupported platform profile"); | |
| return -EOPNOTSUPP; | |
| } | |
| } | |
| static int convert_profile_to_ssam_fan(struct ssam_device *sdev, enum platform_profile_option p) | |
| { | |
| switch (p) { | |
| case PLATFORM_PROFILE_LOW_POWER: | |
| return SSAM_FAN_PROFILE_BATTERY_SAVER; | |
| case PLATFORM_PROFILE_BALANCED: | |
| return SSAM_FAN_PROFILE_NORMAL; | |
| case PLATFORM_PROFILE_BALANCED_PERFORMANCE: | |
| return SSAM_FAN_PROFILE_BETTER_PERFORMANCE; | |
| case PLATFORM_PROFILE_PERFORMANCE: | |
| return SSAM_FAN_PROFILE_BEST_PERFORMANCE; | |
| default: | |
| /* This should have already been caught by platform_profile_store(). */ | |
| WARN(true, "unsupported platform profile"); | |
| return -EOPNOTSUPP; | |
| } | |
| } | |
| static int ssam_platform_profile_get(struct device *dev, | |
| enum platform_profile_option *profile) | |
| { | |
| struct ssam_platform_profile_device *tpd; | |
| enum ssam_tmp_profile tp; | |
| int status; | |
| tpd = dev_get_drvdata(dev); | |
| status = ssam_tmp_profile_get(tpd->sdev, &tp); | |
| if (status) | |
| return status; | |
| status = convert_ssam_tmp_to_profile(tpd->sdev, tp); | |
| if (status < 0) | |
| return status; | |
| *profile = status; | |
| return 0; | |
| } | |
| static int ssam_platform_profile_set(struct device *dev, | |
| enum platform_profile_option profile) | |
| { | |
| struct ssam_platform_profile_device *tpd; | |
| int tp; | |
| tpd = dev_get_drvdata(dev); | |
| tp = convert_profile_to_ssam_tmp(tpd->sdev, profile); | |
| if (tp < 0) | |
| return tp; | |
| tp = ssam_tmp_profile_set(tpd->sdev, tp); | |
| if (tp < 0) | |
| return tp; | |
| if (tpd->has_fan) { | |
| tp = convert_profile_to_ssam_fan(tpd->sdev, profile); | |
| if (tp < 0) | |
| return tp; | |
| tp = ssam_fan_profile_set(tpd->sdev, tp); | |
| } | |
| return tp; | |
| } | |
| static int ssam_platform_profile_probe(void *drvdata, unsigned long *choices) | |
| { | |
| set_bit(PLATFORM_PROFILE_LOW_POWER, choices); | |
| set_bit(PLATFORM_PROFILE_BALANCED, choices); | |
| set_bit(PLATFORM_PROFILE_BALANCED_PERFORMANCE, choices); | |
| set_bit(PLATFORM_PROFILE_PERFORMANCE, choices); | |
| return 0; | |
| } | |
| static const struct platform_profile_ops ssam_platform_profile_ops = { | |
| .probe = ssam_platform_profile_probe, | |
| .profile_get = ssam_platform_profile_get, | |
| .profile_set = ssam_platform_profile_set, | |
| }; | |
| static int surface_platform_profile_probe(struct ssam_device *sdev) | |
| { | |
| struct ssam_platform_profile_device *tpd; | |
| tpd = devm_kzalloc(&sdev->dev, sizeof(*tpd), GFP_KERNEL); | |
| if (!tpd) | |
| return -ENOMEM; | |
| tpd->sdev = sdev; | |
| ssam_device_set_drvdata(sdev, tpd); | |
| tpd->has_fan = device_property_read_bool(&sdev->dev, "has_fan"); | |
| tpd->ppdev = devm_platform_profile_register(&sdev->dev, "Surface Platform Profile", | |
| tpd, &ssam_platform_profile_ops); | |
| return PTR_ERR_OR_ZERO(tpd->ppdev); | |
| } | |
| static const struct ssam_device_id ssam_platform_profile_match[] = { | |
| { SSAM_SDEV(TMP, SAM, 0x00, 0x01) }, | |
| { }, | |
| }; | |
| MODULE_DEVICE_TABLE(ssam, ssam_platform_profile_match); | |
| static struct ssam_device_driver surface_platform_profile = { | |
| .probe = surface_platform_profile_probe, | |
| .match_table = ssam_platform_profile_match, | |
| .driver = { | |
| .name = "surface_platform_profile", | |
| .probe_type = PROBE_PREFER_ASYNCHRONOUS, | |
| }, | |
| }; | |
| module_ssam_device_driver(surface_platform_profile); | |
| MODULE_AUTHOR("Maximilian Luz <luzmaximilian@gmail.com>"); | |
| MODULE_DESCRIPTION("Platform Profile Support for Surface System Aggregator Module"); | |
| MODULE_LICENSE("GPL"); |