diff --git a/kernel-open/nvidia/hwmon-device.c b/kernel-open/nvidia/hwmon-device.c new file mode 100644 index 000000000..cff442c92 --- /dev/null +++ b/kernel-open/nvidia/hwmon-device.c @@ -0,0 +1,372 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include "hwmon-fan.h" +#include "hwmon-device.h" +#include "hwmon-rusd.h" +#include "hwmon-temp-limit.h" +#include "hwmon-thermal.h" + +#define TEMP_CONFIG \ + (HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_MAX | HWMON_T_CRIT | \ + HWMON_T_EMERGENCY | HWMON_T_RATED_MIN | HWMON_T_RATED_MAX) +#define POWER_CONFIG \ + (HWMON_P_INPUT | HWMON_P_LABEL | HWMON_P_AVERAGE | HWMON_P_CAP) +#define FAN_CONFIG (HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_MAX) +#define PWM_CONFIG (HWMON_PWM_INPUT | HWMON_PWM_ENABLE | HWMON_PWM_MODE) + +/* + * hwmon needs static channel tables. Per-GPU detection is enforced later by + * is_visible(), so unsupported files never appear in sysfs. + */ +static const u32 temp_config[NVHWMON_TEMP_CHANNELS + 1] = { + [0 ... NVHWMON_TEMP_CHANNELS - 1] = TEMP_CONFIG, +}; + +static const u32 power_config[NVHWMON_POWER_CHANNELS + 1] = { + [0 ... NVHWMON_POWER_CHANNELS - 1] = POWER_CONFIG, +}; + +static const u32 fan_config[NVHWMON_MAX_FANS + 1] = { + [0 ... NVHWMON_MAX_FANS - 1] = FAN_CONFIG, +}; + +static const u32 pwm_config[NVHWMON_MAX_FANS + 1] = { + [0 ... NVHWMON_MAX_FANS - 1] = PWM_CONFIG, +}; + +static const struct hwmon_channel_info temp_channel_info = { + .type = hwmon_temp, + .config = temp_config, +}; + +static const struct hwmon_channel_info power_channel_info = { + .type = hwmon_power, + .config = power_config, +}; + +static const struct hwmon_channel_info fan_channel_info = { + .type = hwmon_fan, + .config = fan_config, +}; + +static const struct hwmon_channel_info pwm_channel_info = { + .type = hwmon_pwm, + .config = pwm_config, +}; + +static const struct hwmon_channel_info *const channel_info[] = { + &temp_channel_info, &power_channel_info, &fan_channel_info, + &pwm_channel_info, NULL +}; + +static umode_t is_visible(const void *drvdata, enum hwmon_sensor_types type, + u32 attr, int channel) +{ + const struct nvhwmon_gpu *gpu = drvdata; + u32 temp_channel; + + if (channel < 0) + return 0; + + switch (type) { + case hwmon_temp: + if (channel < NVHWMON_RUSD_TEMP_CHANNELS) { + if (attr == hwmon_temp_input || + attr == hwmon_temp_label) + return 0444; + if (nvhwmon_temp_limit_has(gpu, channel, attr)) + return 0444; + return 0; + } + channel -= NVHWMON_RUSD_TEMP_CHANNELS; + if (!nvhwmon_thermal_has_sensor(gpu, channel)) + return 0; + if (attr == hwmon_temp_input || attr == hwmon_temp_label) + return 0444; + if (attr == hwmon_temp_rated_min && + nvhwmon_thermal_has_rated_min(gpu, channel)) + return 0444; + if (attr == hwmon_temp_rated_max && + nvhwmon_thermal_has_rated_max(gpu, channel)) + return 0444; + temp_channel = NVHWMON_RUSD_TEMP_CHANNELS + channel; + if (nvhwmon_temp_limit_has(gpu, temp_channel, attr)) + return 0444; + return 0; + case hwmon_power: + if (channel >= NVHWMON_POWER_CHANNELS) + return 0; + if (attr == hwmon_power_input || attr == hwmon_power_label) + return 0444; + if (attr == hwmon_power_average && + nvhwmon_rusd_power_has_average(channel)) + return 0444; + if (attr == hwmon_power_cap && + nvhwmon_rusd_power_has_cap(channel)) + return 0444; + return 0; + case hwmon_fan: + if (channel >= gpu->fan_count) + return 0; + if (attr == hwmon_fan_input || attr == hwmon_fan_label) + return 0444; + if (attr == hwmon_fan_max && + nvhwmon_fan_has_max_rpm(gpu, channel)) + return 0444; + return 0; + case hwmon_pwm: + if (!nvhwmon_fan_has_pwm(gpu, channel)) + return 0; + if (attr == hwmon_pwm_input || attr == hwmon_pwm_enable || + attr == hwmon_pwm_mode) + return 0644; + return 0; + default: + return 0; + } +} + +/* + * Attributes backed by probe-time state avoid taking gpu->lock. Return -EAGAIN + * for dynamic RM/RUSD reads so the common read path can lock once. + */ +static int read_static(struct nvhwmon_gpu *gpu, enum hwmon_sensor_types type, + u32 attr, int channel, long *val) +{ + switch (type) { + case hwmon_temp: + if (channel < 0 || channel >= NVHWMON_TEMP_CHANNELS) + return -EINVAL; + if (attr == hwmon_temp_max || attr == hwmon_temp_crit || + attr == hwmon_temp_emergency) + return nvhwmon_temp_limit_read(gpu, channel, attr, val); + if (channel < NVHWMON_RUSD_TEMP_CHANNELS) + return -EAGAIN; + + channel -= NVHWMON_RUSD_TEMP_CHANNELS; + if (attr == hwmon_temp_rated_min) + return nvhwmon_thermal_read_rated_min(gpu, channel, + val); + if (attr == hwmon_temp_rated_max) + return nvhwmon_thermal_read_rated_max(gpu, channel, + val); + return -EAGAIN; + case hwmon_fan: + if (attr == hwmon_fan_max) + return nvhwmon_fan_read_max_rpm(gpu, channel, val); + return -EAGAIN; + default: + return -EAGAIN; + } +} + +static int read_temp(struct nvhwmon_gpu *gpu, u32 attr, int channel, long *val) +{ + int thermal_channel; + + if (channel < 0 || channel >= NVHWMON_TEMP_CHANNELS) + return -EINVAL; + + if (attr == hwmon_temp_max || attr == hwmon_temp_crit || + attr == hwmon_temp_emergency) + return nvhwmon_temp_limit_read(gpu, channel, attr, val); + + if (channel < NVHWMON_RUSD_TEMP_CHANNELS) + return attr == hwmon_temp_input ? + nvhwmon_rusd_read_temp(gpu, channel, val) : + -EOPNOTSUPP; + + thermal_channel = channel - NVHWMON_RUSD_TEMP_CHANNELS; + switch (attr) { + case hwmon_temp_input: + return nvhwmon_thermal_read_temp(gpu, thermal_channel, val); + case hwmon_temp_rated_min: + return nvhwmon_thermal_read_rated_min(gpu, thermal_channel, + val); + case hwmon_temp_rated_max: + return nvhwmon_thermal_read_rated_max(gpu, thermal_channel, + val); + default: + return -EOPNOTSUPP; + } +} + +static int read(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long *val) +{ + struct nvhwmon_gpu *gpu = dev_get_drvdata(dev); + int ret; + + ret = read_static(gpu, type, attr, channel, val); + if (ret != -EAGAIN) + return ret; + + mutex_lock(&gpu->lock); + + switch (type) { + case hwmon_temp: + ret = read_temp(gpu, attr, channel, val); + break; + case hwmon_power: + if (attr == hwmon_power_input) + ret = nvhwmon_rusd_read_power(gpu, channel, val); + else if (attr == hwmon_power_average) + ret = nvhwmon_rusd_read_power_average(gpu, channel, val); + else if (attr == hwmon_power_cap) + ret = nvhwmon_rusd_read_power_cap(gpu, channel, val); + else + ret = -EOPNOTSUPP; + break; + case hwmon_fan: + if (attr == hwmon_fan_input) + ret = nvhwmon_rusd_read_fan(gpu, channel, val); + else if (attr == hwmon_fan_max) + ret = nvhwmon_fan_read_max_rpm(gpu, channel, val); + else + ret = -EOPNOTSUPP; + break; + case hwmon_pwm: + if (attr == hwmon_pwm_input) + ret = nvhwmon_fan_read_pwm(gpu, channel, val); + else if (attr == hwmon_pwm_enable) + ret = nvhwmon_fan_read_pwm_enable(gpu, channel, val); + else if (attr == hwmon_pwm_mode) + ret = nvhwmon_fan_read_pwm_mode(gpu, channel, val); + else + ret = -EOPNOTSUPP; + break; + default: + ret = -EOPNOTSUPP; + break; + } + + mutex_unlock(&gpu->lock); + return ret; +} + +/* Writes use the same per-GPU RM lock as dynamic reads. */ +static int write(struct device *dev, enum hwmon_sensor_types type, u32 attr, + int channel, long val) +{ + struct nvhwmon_gpu *gpu = dev_get_drvdata(dev); + int ret; + + if (channel < 0) + return -EINVAL; + + mutex_lock(&gpu->lock); + + if (type == hwmon_pwm && attr == hwmon_pwm_input) + ret = nvhwmon_fan_write_pwm(gpu, channel, val); + else if (type == hwmon_pwm && attr == hwmon_pwm_enable) + ret = nvhwmon_fan_write_pwm_enable(gpu, channel, val); + else if (type == hwmon_pwm && attr == hwmon_pwm_mode) + ret = nvhwmon_fan_write_pwm_mode(gpu, channel, val); + else + ret = -EOPNOTSUPP; + + mutex_unlock(&gpu->lock); + return ret; +} + +static int read_string(struct device *dev, enum hwmon_sensor_types type, + u32 attr, int channel, const char **str) +{ + struct nvhwmon_gpu *gpu = dev_get_drvdata(dev); + int thermal_channel; + + if (channel < 0) + return -EINVAL; + + switch (type) { + case hwmon_temp: + if (attr != hwmon_temp_label) + return -EOPNOTSUPP; + if (channel < NVHWMON_RUSD_TEMP_CHANNELS) { + *str = nvhwmon_rusd_temp_label(channel); + } else { + thermal_channel = channel - NVHWMON_RUSD_TEMP_CHANNELS; + *str = nvhwmon_thermal_label(gpu, thermal_channel); + } + return 0; + case hwmon_power: + if (attr != hwmon_power_label) + return -EOPNOTSUPP; + *str = nvhwmon_rusd_power_label(channel); + return 0; + case hwmon_fan: + if (attr != hwmon_fan_label || channel >= gpu->fan_count) + return -EOPNOTSUPP; + *str = gpu->fans[channel].label; + return 0; + default: + return -EOPNOTSUPP; + } +} + +static const struct hwmon_ops ops = { + .is_visible = is_visible, + .read = read, + .write = write, + .read_string = read_string, +}; + +static const struct hwmon_chip_info chip_info = { + .ops = &ops, + .info = channel_info, +}; + +int nvhwmon_register_device(struct nvhwmon_gpu *gpu) +{ + const struct hwmon_chip_info *chip = &chip_info; + struct device *dev = gpu->parent; + struct device *nvhwmon_dev; + + BUILD_BUG_ON(ARRAY_SIZE(temp_config) != NVHWMON_TEMP_CHANNELS + 1); + BUILD_BUG_ON(ARRAY_SIZE(power_config) != NVHWMON_POWER_CHANNELS + 1); + BUILD_BUG_ON(ARRAY_SIZE(fan_config) != NVHWMON_MAX_FANS + 1); + BUILD_BUG_ON(ARRAY_SIZE(pwm_config) != NVHWMON_MAX_FANS + 1); + + nvhwmon_dev = hwmon_device_register_with_info(dev, NVHWMON_HWMON_NAME, gpu, chip, NULL); + if (IS_ERR(nvhwmon_dev)) + return PTR_ERR(nvhwmon_dev); + + gpu->nvhwmon_dev = nvhwmon_dev; + return 0; +} + +void nvhwmon_unregister_device(struct nvhwmon_gpu *gpu) +{ + if (IS_ERR_OR_NULL(gpu->nvhwmon_dev)) + return; + + hwmon_device_unregister(gpu->nvhwmon_dev); + gpu->nvhwmon_dev = NULL; +} diff --git a/kernel-open/nvidia/hwmon-device.h b/kernel-open/nvidia/hwmon-device.h new file mode 100644 index 000000000..464879f70 --- /dev/null +++ b/kernel-open/nvidia/hwmon-device.h @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_DEVICE_H +#define NVHWMON_DEVICE_H + +#include "hwmon-main.h" + +/* Register/unregister the Linux hwmon core device for one GPU. */ +int nvhwmon_register_device(struct nvhwmon_gpu *gpu); +void nvhwmon_unregister_device(struct nvhwmon_gpu *gpu); + +#endif diff --git a/kernel-open/nvidia/hwmon-entry.h b/kernel-open/nvidia/hwmon-entry.h new file mode 100644 index 000000000..deb9c0b72 --- /dev/null +++ b/kernel-open/nvidia/hwmon-entry.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_ENTRY_H +#define NVHWMON_ENTRY_H + +#include "nvtypes.h" +#include "nv-gpu-info.h" + +/* Called from nvidia.ko module init/exit and per-GPU probe/remove hooks. */ +int nvhwmon_driver_init(void); +void nvhwmon_driver_exit(void); +void nvhwmon_driver_gpu_add(const nv_gpu_info_t *info); +void nvhwmon_driver_gpu_remove(NvU32 gpu_id); + +#endif diff --git a/kernel-open/nvidia/hwmon-fan.c b/kernel-open/nvidia/hwmon-fan.c new file mode 100644 index 000000000..a43510f5a --- /dev/null +++ b/kernel-open/nvidia/hwmon-fan.c @@ -0,0 +1,642 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "hwmon-fan.h" +#include "hwmon-rm.h" + +#define CTRL_CMD_CLIENT_FAN_COOLERS_GET_INFO 0x2080852eU +#define CTRL_CMD_CLIENT_FAN_COOLERS_GET_STATUS 0x2080852fU +#define CTRL_CMD_CLIENT_FAN_COOLERS_GET_CONTROL 0x20808530U +#define CTRL_CMD_CLIENT_FAN_COOLERS_SET_CONTROL 0x2080c531U + +#define FAN_INFO_PARAMS_SIZE 596 +#define FAN_STATUS_PARAMS_SIZE 844 +#define FAN_CONTROL_PARAMS_SIZE 980 +#define FAN_INFO_ENTRY_COUNT NVHWMON_MAX_FANS +#define FAN_STATUS_ENTRY_COUNT NVHWMON_MAX_FANS +#define FAN_CONTROL_ENTRY_COUNT NVHWMON_MAX_FANS +#define FAN_CONTROL_CACHE_MS 1000 +#define FAN_CONTROL_GET_ALL 3 +#define FAN_CONTROL_RESTORE_DEFAULT 1 +#define FAN_CONTROL_PWM_MODE 1 +#define FAN_CONTROL_AUTO 2 +#define FAN_CONTROL_MANUAL 1 + +/* + * These payloads model only fields used by hwmon. BUILD_BUG_ON checks keep the + * total sizes aligned with the RM control ABI. + */ +struct fan_info_entry { + u32 unknown0; + u32 unknown1; + u32 unknown2; + u32 unknown3; + u32 unknown4; + u32 unknown5; + u32 unknown6; + u32 unknown7; + u32 max_rpm; +}; + +struct fan_info_params { + u32 version; + u32 unknown0; + u32 unknown1; + u32 unknown2; + u32 unknown3; + struct fan_info_entry entries[FAN_INFO_ENTRY_COUNT]; +}; + +struct fan_status_entry { + u32 flags; + u32 unknown0; + u32 unknown1; + u32 min_percent_q16; + u32 max_percent_q16; + u32 unknown2; + u32 unknown3; + u32 unknown4; + u32 unknown5; + u32 unknown6; + u32 unknown7; + u32 unknown8; + u32 unknown9; +}; + +struct fan_status_params { + u32 version; + u32 selector; + u32 unknown0; + struct fan_status_entry entries[FAN_STATUS_ENTRY_COUNT]; +}; + +struct fan_control_entry { + u32 flags; + u32 unknown0; + u32 unknown1; + u32 unknown2; + u32 manual; + u32 target_percent_q16; + u32 unknown3; + u32 unknown4; + u32 unknown5; + u32 unknown6; + u32 unknown7; + u32 unknown8; + u32 unknown9; + u32 unknown10; + u32 unknown11; +}; + +struct fan_control_params { + u32 version; + u32 selector; + u32 unknown0; + u32 request; + u32 index_or_restore; + struct fan_control_entry entries[FAN_CONTROL_ENTRY_COUNT]; +}; + +static u32 q16_to_percent(u32 q16) +{ + u32 percent; + + percent = DIV_ROUND_CLOSEST_ULL((u64)q16 * 100, 65536); + return min(percent, 100U); +} + +static u32 percent_to_q16(u32 percent) +{ + return DIV_ROUND_CLOSEST(percent * 65536U, 100U); +} + +static long percent_to_pwm(u32 percent) +{ + return DIV_ROUND_CLOSEST(percent * 255U, 100U); +} + +static u32 pwm_to_percent(long pwm) +{ + return DIV_ROUND_CLOSEST((u32)pwm * 100U, 255U); +} + +static bool status_entry_present(const struct fan_status_entry *entry) +{ + return entry->flags || entry->min_percent_q16 || entry->max_percent_q16; +} + +static bool status_entry_limits(const struct fan_status_entry *entry, + u32 *min_percent, u32 *max_percent) +{ + u32 min_value; + u32 max_value; + + if (!status_entry_present(entry)) + return false; + + min_value = q16_to_percent(entry->min_percent_q16); + max_value = q16_to_percent(entry->max_percent_q16); + if (min_value > max_value) + return false; + + *min_percent = min_value; + *max_percent = max_value; + return true; +} + +static bool control_entry_present(const struct fan_control_entry *entry) +{ + return entry->flags || entry->target_percent_q16 || entry->manual; +} + +static int control_read_all(struct nvhwmon_gpu *gpu, + struct fan_control_params *control, + struct fan_status_params *status) +{ + u32 rm_status; + + BUILD_BUG_ON(sizeof(*control) != FAN_CONTROL_PARAMS_SIZE); + BUILD_BUG_ON(sizeof(*status) != FAN_STATUS_PARAMS_SIZE); + + memset(control, 0, sizeof(*control)); + memset(status, 0, sizeof(*status)); + + control->request = FAN_CONTROL_GET_ALL; + rm_status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_GET_CONTROL, + control, sizeof(*control)); + if (rm_status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(rm_status); + + status->selector = 1; + rm_status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_GET_STATUS, + status, sizeof(*status)); + if (rm_status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(rm_status); + + return 0; +} + +static void control_apply(struct nvhwmon_gpu *gpu, + const struct fan_control_params *control, + const struct fan_status_params *status) +{ + bool global_status_present; + u32 global_max_percent = 0; + u32 global_min_percent = 0; + u32 fan; + + /* + * Status entry 0 can describe global fan limits. Per-cooler entries + * override those limits when populated. + */ + global_status_present = status_entry_limits(&status->entries[0], + &global_min_percent, + &global_max_percent); + + for (fan = 0; fan < gpu->fan_count; fan++) { + struct nvhwmon_fan *state = &gpu->fans[fan]; + const struct fan_control_entry *control_entry; + bool status_present = global_status_present; + u32 max_percent = global_max_percent; + u32 min_percent = global_min_percent; + u8 rm_index = state->rm_index; + + if (rm_index >= ARRAY_SIZE(control->entries)) { + state->pwm_native = false; + continue; + } + + if (rm_index < ARRAY_SIZE(status->entries) && + status_entry_limits(&status->entries[rm_index], + &min_percent, &max_percent)) + status_present = true; + + control_entry = &control->entries[rm_index]; + state->pwm_native = + status_present && + control_entry_present(control_entry); + if (!state->pwm_native) + continue; + + state->pwm_manual = control_entry->manual != 0; + if (control_entry->target_percent_q16) + state->target_percent = + q16_to_percent(control_entry->target_percent_q16); + else + state->target_percent = min_percent; + + state->min_percent = min_percent; + state->max_percent = max_percent; + } + + gpu->fan_control_valid = true; + gpu->fan_control_expires = + jiffies + msecs_to_jiffies(FAN_CONTROL_CACHE_MS); +} + +static int control_refresh(struct nvhwmon_gpu *gpu, bool force) +{ + struct fan_control_params control; + struct fan_status_params status; + int ret; + + if (!force && gpu->fan_control_valid && + time_before(jiffies, gpu->fan_control_expires)) + return 0; + + ret = control_read_all(gpu, &control, &status); + if (ret) + return ret; + + control_apply(gpu, &control, &status); + return 0; +} + +static void control_invalidate(struct nvhwmon_gpu *gpu) +{ + gpu->fan_control_valid = false; + gpu->fan_control_expires = 0; +} + +static int control_restore_default(struct nvhwmon_gpu *gpu, u32 channel); + +static void fan_set_present(struct nvhwmon_gpu *gpu, u8 channel, u32 rm_index) +{ + struct nvhwmon_fan *state = &gpu->fans[channel]; + + state->rm_index = rm_index; + scnprintf(state->label, sizeof(state->label), "fan%u", channel); +} + +static int control_set_manual(struct nvhwmon_gpu *gpu, u32 channel, u32 percent) +{ + struct fan_control_params params = { 0 }; + bool restore_on_set_failure; + u8 rm_index; + u32 status; + + BUILD_BUG_ON(sizeof(params) != FAN_CONTROL_PARAMS_SIZE); + + if (channel >= gpu->fan_count) + return -EINVAL; + + rm_index = gpu->fans[channel].rm_index; + if (rm_index >= ARRAY_SIZE(params.entries)) + return -EINVAL; + + params.request = FAN_CONTROL_GET_ALL; + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_GET_CONTROL, + ¶ms, sizeof(params)); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + + if (!control_entry_present(¶ms.entries[rm_index])) + return -ENODEV; + + /* + * If a write fails after touching manual state, try to put the cooler + * back into automatic control before reporting the error. + */ + restore_on_set_failure = gpu->fans[channel].manual_touched || + !params.entries[rm_index].manual; + params.entries[rm_index].manual = 1; + params.entries[rm_index].target_percent_q16 = + percent_to_q16(percent); + + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_SET_CONTROL, + ¶ms, sizeof(params)); + if (status != NVOS_STATUS_SUCCESS) { + if (restore_on_set_failure) { + gpu->fans[channel].manual_touched = true; + control_restore_default(gpu, channel); + } + return nvhwmon_rm_status_to_errno(status); + } + + gpu->fans[channel].pwm_manual = true; + gpu->fans[channel].manual_touched = true; + gpu->fans[channel].target_percent = percent; + control_invalidate(gpu); + return 0; +} + +static int control_restore_default(struct nvhwmon_gpu *gpu, u32 channel) +{ + struct fan_control_params params = { 0 }; + u8 rm_index; + u32 status; + + BUILD_BUG_ON(sizeof(params) != FAN_CONTROL_PARAMS_SIZE); + + if (channel >= gpu->fan_count) + return -EINVAL; + + rm_index = gpu->fans[channel].rm_index; + if (rm_index >= FAN_CONTROL_ENTRY_COUNT) + return -EINVAL; + + params.request = rm_index + 1; + params.index_or_restore = FAN_CONTROL_RESTORE_DEFAULT; + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_GET_CONTROL, + ¶ms, sizeof(params)); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_SET_CONTROL, + ¶ms, sizeof(params)); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + + gpu->fans[channel].pwm_manual = false; + gpu->fans[channel].manual_touched = false; + control_invalidate(gpu); + return 0; +} + +static void control_probe(struct nvhwmon_gpu *gpu) +{ + struct fan_control_params control; + struct fan_status_params status; + u32 fan; + u8 count = 0; + + if (control_read_all(gpu, &control, &status)) + return; + + for (fan = 0; fan < ARRAY_SIZE(control.entries); fan++) { + bool control_present = + control_entry_present(&control.entries[fan]); + bool status_present = + fan > 0 && fan < ARRAY_SIZE(status.entries) && + status_entry_present(&status.entries[fan]); + + /* + * Status entry 0 can be global limits, so it does not prove + * cooler 0 exists unless its control entry is also populated. + */ + if (!control_present && !status_present) + continue; + + fan_set_present(gpu, count, fan); + count++; + + if (count == NVHWMON_MAX_FANS) + break; + } + + gpu->fan_count = count; + if (gpu->fan_count) + control_apply(gpu, &control, &status); +} + +void nvhwmon_fan_probe(struct nvhwmon_gpu *gpu) +{ + struct fan_info_params params = { 0 }; + u32 status; + u32 fan; + u8 count = 0; + + BUILD_BUG_ON(sizeof(params) != FAN_INFO_PARAMS_SIZE); + + /* Empty cooler entries are skipped; rm_index preserves the RM slot id. */ + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_CLIENT_FAN_COOLERS_GET_INFO, + ¶ms, sizeof(params)); + if (status != NVOS_STATUS_SUCCESS) { + control_probe(gpu); + return; + } + + for (fan = 0; fan < ARRAY_SIZE(params.entries); fan++) { + if (!memchr_inv(¶ms.entries[fan], 0, + sizeof(params.entries[fan]))) + continue; + + fan_set_present(gpu, count, fan); + gpu->fans[count].max_rpm = params.entries[fan].max_rpm; + count++; + + if (count == NVHWMON_MAX_FANS) + break; + } + + gpu->fan_count = count; + + if (gpu->fan_count) + control_refresh(gpu, true); + else + control_probe(gpu); +} + +/* Called on teardown and on failed writes that may have changed fan mode. */ +void nvhwmon_fan_restore_all(struct nvhwmon_gpu *gpu) +{ + u32 fan; + + mutex_lock(&gpu->lock); + + for (fan = 0; fan < gpu->fan_count; fan++) { + int ret; + + if (!gpu->fans[fan].manual_touched) + continue; + + ret = control_restore_default(gpu, fan); + if (ret) + dev_warn(gpu->parent, + "%s: failed to restore fan %u automatic control: %d\n", + NVHWMON_DRIVER_NAME, fan, ret); + } + + mutex_unlock(&gpu->lock); +} + +static void restore_after_failure(struct nvhwmon_gpu *gpu, u32 channel) +{ + int ret; + + if (channel >= gpu->fan_count || !gpu->fans[channel].manual_touched) + return; + + ret = control_restore_default(gpu, channel); + if (ret) + dev_warn(gpu->parent, + "%s: failed to restore fan %u after fan-control failure: %d\n", + NVHWMON_DRIVER_NAME, channel, ret); +} + +bool nvhwmon_fan_has_max_rpm(const struct nvhwmon_gpu *gpu, u32 channel) +{ + if (channel >= gpu->fan_count) + return false; + + return gpu->fans[channel].max_rpm != 0; +} + +int nvhwmon_fan_read_max_rpm(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + if (channel >= gpu->fan_count || !gpu->fans[channel].max_rpm) + return -EINVAL; + + *val = gpu->fans[channel].max_rpm; + return 0; +} + +bool nvhwmon_fan_has_pwm(const struct nvhwmon_gpu *gpu, u32 channel) +{ + if (channel >= gpu->fan_count) + return false; + + return gpu->fans[channel].pwm_native; +} + +int nvhwmon_fan_read_pwm(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + int ret; + + if (channel >= gpu->fan_count || !gpu->fans[channel].pwm_native) + return -EINVAL; + + ret = control_refresh(gpu, false); + if (ret) + return ret; + + *val = percent_to_pwm(gpu->fans[channel].target_percent); + return 0; +} + +int nvhwmon_fan_write_pwm(struct nvhwmon_gpu *gpu, u32 channel, long val) +{ + struct nvhwmon_fan *fan; + u32 percent; + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (channel >= gpu->fan_count || !gpu->fans[channel].pwm_native) + return -EINVAL; + + if (val < 0 || val > 255) + return -EINVAL; + + ret = control_refresh(gpu, false); + if (ret) { + restore_after_failure(gpu, channel); + return ret; + } + + fan = &gpu->fans[channel]; + percent = pwm_to_percent(val); + if (percent < fan->min_percent || percent > fan->max_percent) + return -EINVAL; + + ret = control_set_manual(gpu, channel, percent); + if (ret) + restore_after_failure(gpu, channel); + + return ret; +} + +int nvhwmon_fan_read_pwm_enable(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + int ret; + + if (channel >= gpu->fan_count || !gpu->fans[channel].pwm_native) + return -EINVAL; + + ret = control_refresh(gpu, false); + if (ret) + return ret; + + *val = gpu->fans[channel].pwm_manual ? FAN_CONTROL_MANUAL + : FAN_CONTROL_AUTO; + return 0; +} + +int nvhwmon_fan_write_pwm_enable(struct nvhwmon_gpu *gpu, u32 channel, long val) +{ + int ret; + + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (channel >= gpu->fan_count || !gpu->fans[channel].pwm_native) + return -EINVAL; + + if (val == FAN_CONTROL_AUTO) + return control_restore_default(gpu, channel); + + if (val != FAN_CONTROL_MANUAL) + return -EINVAL; + + ret = control_refresh(gpu, false); + if (ret) { + restore_after_failure(gpu, channel); + return ret; + } + + ret = control_set_manual(gpu, channel, + gpu->fans[channel].target_percent); + if (ret) + restore_after_failure(gpu, channel); + + return ret; +} + +int nvhwmon_fan_read_pwm_mode(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + if (channel >= gpu->fan_count || !gpu->fans[channel].pwm_native) + return -EINVAL; + + *val = FAN_CONTROL_PWM_MODE; + return 0; +} + +int nvhwmon_fan_write_pwm_mode(struct nvhwmon_gpu *gpu, u32 channel, long val) +{ + if (!capable(CAP_SYS_ADMIN)) + return -EPERM; + + if (channel >= gpu->fan_count || !gpu->fans[channel].pwm_native) + return -EINVAL; + + if (val != FAN_CONTROL_PWM_MODE) + return -EINVAL; + + return 0; +} diff --git a/kernel-open/nvidia/hwmon-fan.h b/kernel-open/nvidia/hwmon-fan.h new file mode 100644 index 000000000..f4dda16e5 --- /dev/null +++ b/kernel-open/nvidia/hwmon-fan.h @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_FAN_H +#define NVHWMON_FAN_H + +#include "hwmon-main.h" + +/* Probe caches static fan topology; read/write helpers assume gpu->lock. */ +void nvhwmon_fan_probe(struct nvhwmon_gpu *gpu); +void nvhwmon_fan_restore_all(struct nvhwmon_gpu *gpu); + +bool nvhwmon_fan_has_max_rpm(const struct nvhwmon_gpu *gpu, u32 channel); +int nvhwmon_fan_read_max_rpm(struct nvhwmon_gpu *gpu, u32 channel, long *val); +bool nvhwmon_fan_has_pwm(const struct nvhwmon_gpu *gpu, u32 channel); +int nvhwmon_fan_read_pwm(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_fan_write_pwm(struct nvhwmon_gpu *gpu, u32 channel, long val); +int nvhwmon_fan_read_pwm_enable(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_fan_write_pwm_enable(struct nvhwmon_gpu *gpu, u32 channel, long val); +int nvhwmon_fan_read_pwm_mode(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_fan_write_pwm_mode(struct nvhwmon_gpu *gpu, u32 channel, long val); + +#endif diff --git a/kernel-open/nvidia/hwmon-main.c b/kernel-open/nvidia/hwmon-main.c new file mode 100644 index 000000000..4a351882e --- /dev/null +++ b/kernel-open/nvidia/hwmon-main.c @@ -0,0 +1,265 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include "hwmon-main.h" +#include "hwmon-fan.h" +#include "hwmon-device.h" +#include "hwmon-rm.h" +#include "hwmon-rusd.h" +#include "hwmon-temp-limit.h" +#include "hwmon-thermal.h" + +static LIST_HEAD(gpus); +static DEFINE_MUTEX(gpus_lock); +static bool active; + +/* The list is keyed by RM GPU id; index is only for local RM handle slots. */ +static struct nvhwmon_gpu *gpu_find_locked(u32 gpu_id) +{ + struct nvhwmon_gpu *gpu; + + list_for_each_entry(gpu, &gpus, node) { + if (gpu->gpu_id == gpu_id) + return gpu; + } + + return NULL; +} + +static int gpu_index_locked(void) +{ + bool used[NV_MAX_GPUS] = { 0 }; + struct nvhwmon_gpu *gpu; + u32 i; + + list_for_each_entry(gpu, &gpus, node) { + if (gpu->index < NV_MAX_GPUS) + used[gpu->index] = true; + } + + for (i = 0; i < NV_MAX_GPUS; i++) { + if (!used[i]) + return i; + } + + return -ENOSPC; +} + +/* + * Unregister sysfs first so no new hwmon callbacks can enter while fan state + * and RM objects are being torn down. + */ +static void gpu_destroy(struct nvhwmon_gpu *gpu) +{ + if (!list_empty(&gpu->node)) + list_del_init(&gpu->node); + + nvhwmon_unregister_device(gpu); + nvhwmon_fan_restore_all(gpu); + nvhwmon_rusd_fini(gpu); + nvhwmon_rm_free_gpu_objects(gpu); + nvhwmon_rm_close_gpu(gpu); + mutex_destroy(&gpu->lock); + kfree(gpu); +} + +static int gpu_create(const nv_gpu_info_t *info, u32 index, + struct nvhwmon_gpu **gpu_out) +{ + struct nvhwmon_gpu *gpu; + int ret; + + gpu = kzalloc(sizeof(*gpu), GFP_KERNEL); + if (!gpu) + return -ENOMEM; + + INIT_LIST_HEAD(&gpu->node); + mutex_init(&gpu->lock); + gpu->index = index; + gpu->gpu_id = info->gpu_id; + gpu->parent = info->os_device_ptr; + if (!gpu->parent) { + ret = -ENODEV; + goto fail; + } + + /* Pins the NVIDIA device until destroy releases it before PCI removal. */ + ret = nvhwmon_rm_open_gpu(gpu); + if (ret) + goto fail; + + ret = nvhwmon_rm_alloc_gpu_objects(gpu); + if (ret) + goto fail; + + nvhwmon_fan_probe(gpu); + nvhwmon_thermal_probe(gpu); + nvhwmon_temp_limit_probe(gpu); + + ret = nvhwmon_rusd_init(gpu); + if (ret) + goto fail; + + ret = nvhwmon_register_device(gpu); + if (ret) + goto fail; + + *gpu_out = gpu; + return 0; + +fail: + nvhwmon_unregister_device(gpu); + nvhwmon_fan_restore_all(gpu); + nvhwmon_rusd_fini(gpu); + nvhwmon_rm_free_gpu_objects(gpu); + nvhwmon_rm_close_gpu(gpu); + mutex_destroy(&gpu->lock); + kfree(gpu); + return ret; +} + +static int gpu_add_locked(const nv_gpu_info_t *info) +{ + struct nvhwmon_gpu *gpu; + int index; + int ret; + + if (!active) + return -ENODEV; + + if (!info || !info->os_device_ptr) + return -ENODEV; + + if (gpu_find_locked(info->gpu_id)) + return -EEXIST; + + index = gpu_index_locked(); + if (index < 0) + return index; + + ret = gpu_create(info, index, &gpu); + if (ret) + return ret; + + list_add_tail(&gpu->node, &gpus); + return 0; +} + +/* Probe/remove hooks may race with module exit, so both share gpus_lock. */ +void nvhwmon_driver_gpu_add(const nv_gpu_info_t *info) +{ + mutex_lock(&gpus_lock); + gpu_add_locked(info); + mutex_unlock(&gpus_lock); +} + +void nvhwmon_driver_gpu_remove(NvU32 gpu_id) +{ + struct nvhwmon_gpu *gpu; + + mutex_lock(&gpus_lock); + gpu = gpu_find_locked(gpu_id); + if (gpu) + gpu_destroy(gpu); + mutex_unlock(&gpus_lock); +} + +static void driver_shutdown(void) +{ + struct nvhwmon_gpu *gpu; + + mutex_lock(&gpus_lock); + /* Blocks late hotplug adds before RM ops are unbound below. */ + active = false; + while (!list_empty(&gpus)) { + gpu = list_first_entry(&gpus, struct nvhwmon_gpu, node); + gpu_destroy(gpu); + } + mutex_unlock(&gpus_lock); + + nvhwmon_rm_unbind(); +} + +int __init nvhwmon_driver_init(void) +{ + nv_gpu_info_t *gpu_info; + u32 count; + u32 i; + int ret; + + ret = nvhwmon_rm_bind(); + if (ret) + return ret; + + mutex_lock(&gpus_lock); + active = true; + mutex_unlock(&gpus_lock); + + gpu_info = kcalloc(NV_MAX_GPUS, sizeof(*gpu_info), GFP_KERNEL); + if (!gpu_info) { + ret = -ENOMEM; + goto fail; + } + + count = nvhwmon_rm_enumerate_gpus(gpu_info); + if (!count) { + ret = -ENODEV; + goto fail_info; + } + if (count > NV_MAX_GPUS) + count = NV_MAX_GPUS; + + for (i = 0; i < count; i++) { + mutex_lock(&gpus_lock); + gpu_add_locked(&gpu_info[i]); + mutex_unlock(&gpus_lock); + } + + kfree(gpu_info); + + mutex_lock(&gpus_lock); + ret = list_empty(&gpus) ? -ENODEV : 0; + mutex_unlock(&gpus_lock); + if (ret) + goto fail; + + return 0; + +fail_info: + kfree(gpu_info); +fail: + driver_shutdown(); + return ret; +} + +void __exit nvhwmon_driver_exit(void) +{ + driver_shutdown(); +} diff --git a/kernel-open/nvidia/hwmon-main.h b/kernel-open/nvidia/hwmon-main.h new file mode 100644 index 000000000..50dc5ef77 --- /dev/null +++ b/kernel-open/nvidia/hwmon-main.h @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_MAIN_H +#define NVHWMON_MAIN_H + +#include +#include +#include +#include +#include + +#include "hwmon-entry.h" +#include "hwmon-nvidia.h" + +#define NVHWMON_DRIVER_NAME "nvidia-hwmon" +#define NVHWMON_HWMON_NAME "nvidia_hwmon" +/* Static hwmon config sizes; is_visible() hides channels not present on a GPU. */ +#define NVHWMON_MAX_FANS RUSD_FAN_COOLER_MAX_COOLERS +#define NVHWMON_FAN_LABEL_LEN 8 +#define NVHWMON_RUSD_TEMP_CHANNELS RUSD_TEMPERATURE_TYPE_MAX +#define NVHWMON_THERMAL_MAX_SENSORS 16 +#define NVHWMON_THERMAL_LABEL_LEN 32 +#define NVHWMON_TEMP_CHANNELS \ + (NVHWMON_RUSD_TEMP_CHANNELS + NVHWMON_THERMAL_MAX_SENSORS) +#define NVHWMON_POWER_CHANNELS 9 + +struct nvhwmon_fan { + bool pwm_native; + bool pwm_manual; + /* Tracks only writes made through hwmon so teardown can restore them. */ + bool manual_touched; + u8 rm_index; + char label[NVHWMON_FAN_LABEL_LEN]; + u32 max_rpm; + u32 min_percent; + u32 max_percent; + u32 target_percent; +}; + +struct nvhwmon_rusd_cache { + unsigned long temp_expires; + unsigned long power_expires; + unsigned long fan_expires; + + bool temp_valid[NVHWMON_RUSD_TEMP_CHANNELS]; + bool avg_power_valid; + bool inst_power_valid; + bool power_policy_valid; + bool power_limit_valid; + bool fan_valid; + + RUSD_TEMPERATURE temperatures[NVHWMON_RUSD_TEMP_CHANNELS]; + RUSD_AVG_POWER_USAGE avg_power; + RUSD_INST_POWER_USAGE inst_power; + RUSD_POWER_POLICY_STATUS power_policy; + RUSD_POWER_LIMITS power_limit; + RUSD_FAN_COOLER_STATUS fan_status; +}; + +struct nvhwmon_thermal_sensor { + bool input_valid; + bool min_valid; + bool max_valid; + u32 sensor_index; + u32 target_type; + s32 input_c; + s32 min_c; + s32 max_c; + char label[NVHWMON_THERMAL_LABEL_LEN]; +}; + +struct nvhwmon_temp_limits { + bool max_valid; + bool crit_valid; + bool emergency_valid; + s32 max_mc; + s32 crit_mc; + s32 emergency_mc; +}; + +struct nvhwmon_gpu { + struct list_head node; + /* Compact local slot used to derive non-overlapping RM object handles. */ + u32 index; + /* Stable RM GPU id passed through nvidia_modeset_probe/remove. */ + u32 gpu_id; + struct device *parent; + struct device *nvhwmon_dev; + /* Serializes RM-backed telemetry and fan control access. */ + struct mutex lock; + /* True while nvidia_dev_get() holds a device usage reference. */ + bool opened; + + NvHandle h_client; + NvHandle h_device; + NvHandle h_subdevice; + NvHandle h_rusd; + void *rusd_map; + /* RUSD is mmap-backed telemetry; thermal and fan controls use RM calls. */ + struct nvhwmon_rusd_cache rusd_cache; + unsigned long thermal_expires; + u8 thermal_count; + struct nvhwmon_thermal_sensor + thermal_sensors[NVHWMON_THERMAL_MAX_SENSORS]; + struct nvhwmon_temp_limits temp_limits[NVHWMON_TEMP_CHANNELS]; + unsigned long fan_control_expires; + bool fan_control_valid; + + u8 fan_count; + struct nvhwmon_fan fans[NVHWMON_MAX_FANS]; +}; + +#endif diff --git a/kernel-open/nvidia/hwmon-nvidia.h b/kernel-open/nvidia/hwmon-nvidia.h new file mode 100644 index 000000000..9f1b3c7f2 --- /dev/null +++ b/kernel-open/nvidia/hwmon-nvidia.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_NVIDIA_H +#define NVHWMON_NVIDIA_H + +/* Centralized include point for NVIDIA private RMAPI and class definitions. */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#endif diff --git a/kernel-open/nvidia/hwmon-rm.c b/kernel-open/nvidia/hwmon-rm.c new file mode 100644 index 000000000..07f395aa9 --- /dev/null +++ b/kernel-open/nvidia/hwmon-rm.c @@ -0,0 +1,336 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include + +#include "hwmon-rm.h" + +/* Private handle namespace for objects allocated by this in-tree hwmon path. */ +#define RM_HANDLE_BASE 0x5a000000U +#define RM_HANDLE_STRIDE 0x00000100U +#define RM_HANDLE_DEVICE 0x00000001U +#define RM_HANDLE_SUBDEVICE 0x00000002U +#define RM_HANDLE_RUSD 0x00000003U + +static nvidia_modeset_rm_ops_t rm_ops; + +static NvHandle rm_handle(const struct nvhwmon_gpu *gpu, u32 slot) +{ + return RM_HANDLE_BASE + gpu->index * RM_HANDLE_STRIDE + + slot; +} + +NvHandle nvhwmon_rm_rusd_handle(const struct nvhwmon_gpu *gpu) +{ + return rm_handle(gpu, RM_HANDLE_RUSD); +} + +int nvhwmon_rm_status_to_errno(NvU32 status) +{ + if (status == NVOS_STATUS_SUCCESS) + return 0; + + if (status == NV_ERR_NOT_SUPPORTED) + return -EOPNOTSUPP; + + if (status == NV_ERR_INVALID_ARGUMENT || + status == NV_ERR_INVALID_PARAM_STRUCT) + return -EINVAL; + + if (status == NV_ERR_INSUFFICIENT_PERMISSIONS) + return -EPERM; + + if (status == NV_ERR_NO_MEMORY) + return -ENOMEM; + + return -EIO; +} + +int nvhwmon_rm_bind(void) +{ + NV_STATUS status; + + memset(&rm_ops, 0, sizeof(rm_ops)); + rm_ops.version_string = NV_VERSION_STRING; + + status = nvidia_get_rm_ops(&rm_ops); + if (status != NV_OK) { + memset(&rm_ops, 0, sizeof(rm_ops)); + return -EINVAL; + } + + if (!rm_ops.alloc_stack || !rm_ops.free_stack || + !rm_ops.enumerate_gpus || !rm_ops.open_gpu || + !rm_ops.close_gpu || !rm_ops.op) { + memset(&rm_ops, 0, sizeof(rm_ops)); + return -ENODEV; + } + + return 0; +} + +void nvhwmon_rm_unbind(void) +{ + memset(&rm_ops, 0, sizeof(rm_ops)); +} + +u32 nvhwmon_rm_enumerate_gpus(nv_gpu_info_t *gpu_info) +{ + if (!rm_ops.enumerate_gpus) + return 0; + + return rm_ops.enumerate_gpus(gpu_info); +} + +/* RMAPI transport failure is separate from the op-specific status field. */ +static NvU32 rm_call(void *ops) +{ + nvidia_modeset_stack_ptr stack = NULL; + + if (rm_ops.alloc_stack(&stack) != 0) + return NV_ERR_NO_MEMORY; + + rm_ops.op(stack, ops); + rm_ops.free_stack(stack); + + return NVOS_STATUS_SUCCESS; +} + +/* This increments the NVIDIA device usage count until nvhwmon_rm_close_gpu(). */ +int nvhwmon_rm_open_gpu(struct nvhwmon_gpu *gpu) +{ + nvidia_modeset_stack_ptr stack = NULL; + int ret; + + ret = rm_ops.alloc_stack(&stack); + if (ret != 0) + return ret; + + ret = rm_ops.open_gpu(gpu->gpu_id, stack, NV_FALSE); + rm_ops.free_stack(stack); + if (ret != 0) + return ret; + + gpu->opened = true; + return 0; +} + +/* Must run before PCI remove checks usage_count, otherwise removal can wait. */ +void nvhwmon_rm_close_gpu(struct nvhwmon_gpu *gpu) +{ + nvidia_modeset_stack_ptr stack = NULL; + + if (!gpu->opened) + return; + + if (rm_ops.alloc_stack(&stack) == 0) { + rm_ops.close_gpu(gpu->gpu_id, stack, NV_FALSE); + rm_ops.free_stack(stack); + gpu->opened = false; + } else { + pr_err("%s: failed to allocate NVIDIA stack while closing GPU 0x%x\n", + NVHWMON_DRIVER_NAME, gpu->gpu_id); + /* + * close_gpu() requires an RM stack. If allocation fails here, the + * device reference cannot be released, so keep local state from + * advertising a future close that can no longer be guaranteed. + */ + gpu->opened = false; + } +} + +NvU32 nvhwmon_rm_alloc(NvHandle h_client, NvHandle h_parent, NvHandle h_object, + NvU32 h_class, void *params, NvU32 params_size) +{ + nvidia_kernel_rmapi_ops_t ops = { 0 }; + + ops.op = NV04_ALLOC; + ops.params.alloc.hRoot = h_client; + ops.params.alloc.hObjectParent = h_parent; + ops.params.alloc.hObjectNew = h_object; + ops.params.alloc.hClass = h_class; + ops.params.alloc.pAllocParms = NV_PTR_TO_NvP64(params); + ops.params.alloc.paramsSize = params_size; + + if (rm_call(&ops) != NVOS_STATUS_SUCCESS) + return NV_ERR_NO_MEMORY; + + return ops.params.alloc.status; +} + +NvU32 nvhwmon_rm_free(NvHandle h_client, NvHandle h_parent, NvHandle h_object) +{ + nvidia_kernel_rmapi_ops_t ops = { 0 }; + + ops.op = NV01_FREE; + ops.params.free.hRoot = h_client; + ops.params.free.hObjectParent = h_parent; + ops.params.free.hObjectOld = h_object; + + if (rm_call(&ops) != NVOS_STATUS_SUCCESS) + return NV_ERR_NO_MEMORY; + + return ops.params.free.status; +} + +NvU32 nvhwmon_rm_control(NvHandle h_client, NvHandle h_object, NvU32 cmd, + void *params, NvU32 params_size) +{ + nvidia_kernel_rmapi_ops_t ops = { 0 }; + + ops.op = NV04_CONTROL; + ops.params.control.hClient = h_client; + ops.params.control.hObject = h_object; + ops.params.control.cmd = cmd; + ops.params.control.params = NV_PTR_TO_NvP64(params); + ops.params.control.paramsSize = params_size; + + if (rm_call(&ops) != NVOS_STATUS_SUCCESS) + return NV_ERR_NO_MEMORY; + + return ops.params.control.status; +} + +NvU32 nvhwmon_rm_map_memory(NvHandle h_client, NvHandle h_device, + NvHandle h_memory, NvU64 offset, NvU64 length, + void **linear_address, NvU32 flags) +{ + nvidia_kernel_rmapi_ops_t ops = { 0 }; + + *linear_address = NULL; + + ops.op = NV04_MAP_MEMORY; + ops.params.mapMemory.hClient = h_client; + ops.params.mapMemory.hDevice = h_device; + ops.params.mapMemory.hMemory = h_memory; + ops.params.mapMemory.offset = offset; + ops.params.mapMemory.length = length; + ops.params.mapMemory.flags = flags; + + if (rm_call(&ops) != NVOS_STATUS_SUCCESS) + return NV_ERR_NO_MEMORY; + + if (ops.params.mapMemory.status == NVOS_STATUS_SUCCESS) + *linear_address = + NvP64_VALUE(ops.params.mapMemory.pLinearAddress); + + return ops.params.mapMemory.status; +} + +NvU32 nvhwmon_rm_unmap_memory(NvHandle h_client, NvHandle h_device, + NvHandle h_memory, const void *linear_address, + NvU32 flags) +{ + nvidia_kernel_rmapi_ops_t ops = { 0 }; + + ops.op = NV04_UNMAP_MEMORY; + ops.params.unmapMemory.hClient = h_client; + ops.params.unmapMemory.hDevice = h_device; + ops.params.unmapMemory.hMemory = h_memory; + ops.params.unmapMemory.pLinearAddress = NV_PTR_TO_NvP64(linear_address); + ops.params.unmapMemory.flags = flags; + + if (rm_call(&ops) != NVOS_STATUS_SUCCESS) + return NV_ERR_NO_MEMORY; + + return ops.params.unmapMemory.status; +} + +int nvhwmon_rm_alloc_gpu_objects(struct nvhwmon_gpu *gpu) +{ + NV0000_CTRL_GPU_GET_ID_INFO_V2_PARAMS id_info = { 0 }; + NV0080_ALLOC_PARAMETERS device_params = { 0 }; + NV2080_ALLOC_PARAMETERS subdevice_params = { 0 }; + NvU32 status; + + /* + * Client/device/subdevice handles are the common parent chain for all + * telemetry controls and RUSD mapping below. + */ + gpu->h_client = NV01_NULL_OBJECT; + gpu->h_device = NV01_NULL_OBJECT; + gpu->h_subdevice = NV01_NULL_OBJECT; + gpu->h_rusd = NV01_NULL_OBJECT; + + status = nvhwmon_rm_alloc(NV01_NULL_OBJECT, NV01_NULL_OBJECT, + NV01_NULL_OBJECT, NV01_ROOT, &gpu->h_client, + sizeof(gpu->h_client)); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + if (gpu->h_client == NV01_NULL_OBJECT) + return -ENODEV; + + id_info.gpuId = gpu->gpu_id; + status = nvhwmon_rm_control(gpu->h_client, gpu->h_client, + NV0000_CTRL_CMD_GPU_GET_ID_INFO_V2, + &id_info, sizeof(id_info)); + if (status != NVOS_STATUS_SUCCESS) + goto fail; + + gpu->h_device = rm_handle(gpu, RM_HANDLE_DEVICE); + device_params.deviceId = id_info.deviceInstance; + device_params.hClientShare = gpu->h_client; + + status = nvhwmon_rm_alloc(gpu->h_client, gpu->h_client, gpu->h_device, + NV01_DEVICE_0, &device_params, + sizeof(device_params)); + if (status != NVOS_STATUS_SUCCESS) + goto fail; + + gpu->h_subdevice = rm_handle(gpu, RM_HANDLE_SUBDEVICE); + subdevice_params.subDeviceId = id_info.subDeviceInstance; + + status = nvhwmon_rm_alloc(gpu->h_client, gpu->h_device, + gpu->h_subdevice, NV20_SUBDEVICE_0, + &subdevice_params, sizeof(subdevice_params)); + if (status != NVOS_STATUS_SUCCESS) + goto fail; + + return 0; + +fail: + nvhwmon_rm_free_gpu_objects(gpu); + return nvhwmon_rm_status_to_errno(status); +} + +/* Free children before parents; each handle is nulled so cleanup is idempotent. */ +void nvhwmon_rm_free_gpu_objects(struct nvhwmon_gpu *gpu) +{ + if (gpu->h_subdevice != NV01_NULL_OBJECT) { + nvhwmon_rm_free(gpu->h_client, gpu->h_device, gpu->h_subdevice); + gpu->h_subdevice = NV01_NULL_OBJECT; + } + + if (gpu->h_device != NV01_NULL_OBJECT) { + nvhwmon_rm_free(gpu->h_client, gpu->h_client, gpu->h_device); + gpu->h_device = NV01_NULL_OBJECT; + } + + if (gpu->h_client != NV01_NULL_OBJECT) { + nvhwmon_rm_free(gpu->h_client, NV01_NULL_OBJECT, gpu->h_client); + gpu->h_client = NV01_NULL_OBJECT; + } +} diff --git a/kernel-open/nvidia/hwmon-rm.h b/kernel-open/nvidia/hwmon-rm.h new file mode 100644 index 000000000..4e5470bba --- /dev/null +++ b/kernel-open/nvidia/hwmon-rm.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_RM_H +#define NVHWMON_RM_H + +#include "hwmon-main.h" + +/* Bind to RM callbacks exported inside nvidia.ko and clear them on shutdown. */ +int nvhwmon_rm_bind(void); +void nvhwmon_rm_unbind(void); + +u32 nvhwmon_rm_enumerate_gpus(nv_gpu_info_t *gpu_info); +/* Open/close pins the underlying NVIDIA device lifetime. */ +int nvhwmon_rm_open_gpu(struct nvhwmon_gpu *gpu); +void nvhwmon_rm_close_gpu(struct nvhwmon_gpu *gpu); + +int nvhwmon_rm_alloc_gpu_objects(struct nvhwmon_gpu *gpu); +void nvhwmon_rm_free_gpu_objects(struct nvhwmon_gpu *gpu); +NvHandle nvhwmon_rm_rusd_handle(const struct nvhwmon_gpu *gpu); + +NvU32 nvhwmon_rm_alloc(NvHandle h_client, NvHandle h_parent, NvHandle h_object, + NvU32 h_class, void *params, NvU32 params_size); +NvU32 nvhwmon_rm_free(NvHandle h_client, NvHandle h_parent, NvHandle h_object); +NvU32 nvhwmon_rm_control(NvHandle h_client, NvHandle h_object, NvU32 cmd, + void *params, NvU32 params_size); +NvU32 nvhwmon_rm_map_memory(NvHandle h_client, NvHandle h_device, + NvHandle h_memory, NvU64 offset, NvU64 length, + void **linear_address, NvU32 flags); +NvU32 nvhwmon_rm_unmap_memory(NvHandle h_client, NvHandle h_device, + NvHandle h_memory, const void *linear_address, + NvU32 flags); + +int nvhwmon_rm_status_to_errno(NvU32 status); + +#endif diff --git a/kernel-open/nvidia/hwmon-rusd.c b/kernel-open/nvidia/hwmon-rusd.c new file mode 100644 index 000000000..7b15972d0 --- /dev/null +++ b/kernel-open/nvidia/hwmon-rusd.c @@ -0,0 +1,451 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#include "hwmon-rm.h" +#include "hwmon-rusd.h" + +#define RUSD_READ_ATTEMPTS 10 +#define RUSD_POLL_INTERVAL_MS 1000 +#define RUSD_CACHE_MS RUSD_POLL_INTERVAL_MS +#define RUSD_ERROR_RETRY_MS 100 +#define RUSD_BASE_POLL_MASK \ + (NV00DE_RUSD_POLL_POWER | NV00DE_RUSD_POLL_THERMAL) + +enum power_channel { + NVHWMON_POWER_AVG_GPU, + NVHWMON_POWER_AVG_MODULE, + NVHWMON_POWER_AVG_MEMORY, + NVHWMON_POWER_INST_GPU, + NVHWMON_POWER_INST_MODULE, + NVHWMON_POWER_INST_CPU, + NVHWMON_POWER_POLICY_TOTAL_GPU, + NVHWMON_POWER_LIMIT_REQUESTED, + NVHWMON_POWER_LIMIT_ENFORCED, +}; + +static bool timestamp_valid(NvU64 timestamp) +{ + if (timestamp == RUSD_TIMESTAMP_INVALID || + timestamp == RUSD_TIMESTAMP_WRITE_IN_PROGRESS) + return false; + + if (timestamp >= RUSD_SEQ_START && (timestamp & 1)) + return false; + + return true; +} + +/* Every RUSD record starts with a timestamp used as a lightweight sequence. */ +struct field_header { + NvU64 lastModifiedTimestamp; +}; + +/* + * read_field() casts RUSD records to field_header, so every record type used + * below must keep lastModifiedTimestamp as its first member. + */ +static_assert(offsetof(RUSD_TEMPERATURE, lastModifiedTimestamp) == 0); +static_assert(offsetof(RUSD_AVG_POWER_USAGE, lastModifiedTimestamp) == 0); +static_assert(offsetof(RUSD_INST_POWER_USAGE, lastModifiedTimestamp) == 0); +static_assert(offsetof(RUSD_POWER_POLICY_STATUS, lastModifiedTimestamp) == 0); +static_assert(offsetof(RUSD_POWER_LIMITS, lastModifiedTimestamp) == 0); +static_assert(offsetof(RUSD_FAN_COOLER_STATUS, lastModifiedTimestamp) == 0); + +static int read_field(const void *src, void *dst, size_t size) +{ + const struct field_header *header = src; + u32 attempt; + + for (attempt = 0; attempt < RUSD_READ_ATTEMPTS; attempt++) { + NvU64 seq1 = READ_ONCE(header->lastModifiedTimestamp); + + if (seq1 == RUSD_TIMESTAMP_INVALID) + return -ENODATA; + + if (!timestamp_valid(seq1)) + continue; + + /* Pairs with RM/GSP timestamp publication around the payload. */ + smp_rmb(); + memcpy(dst, src, size); + /* Re-read the timestamp after the payload copy. */ + smp_rmb(); + + if (seq1 == READ_ONCE(header->lastModifiedTimestamp)) { + ((struct field_header *)dst) + ->lastModifiedTimestamp = seq1; + return 0; + } + } + + return -EAGAIN; +} + +static unsigned long cache_expires(bool retry_soon) +{ + u32 cache_ms = retry_soon ? RUSD_ERROR_RETRY_MS : + RUSD_CACHE_MS; + + return jiffies + msecs_to_jiffies(cache_ms); +} + +static void refresh_temps(struct nvhwmon_gpu *gpu) +{ + struct nvhwmon_rusd_cache *cache = &gpu->rusd_cache; + NV00DE_SHARED_DATA *shared = gpu->rusd_map; + bool retry_soon = false; + u32 channel; + + if (cache->temp_expires && time_before(jiffies, cache->temp_expires)) + return; + + /* Missing fields are marked invalid; racing writes retry sooner. */ + for (channel = 0; channel < NVHWMON_RUSD_TEMP_CHANNELS; channel++) { + int ret; + + ret = read_field(&shared->temperatures[channel], + &cache->temperatures[channel], + sizeof(cache->temperatures[channel])); + if (!ret) + cache->temp_valid[channel] = true; + else if (ret == -ENODATA) + cache->temp_valid[channel] = false; + else + retry_soon = true; + } + + cache->temp_expires = cache_expires(retry_soon); +} + +static void refresh_power(struct nvhwmon_gpu *gpu) +{ + struct nvhwmon_rusd_cache *cache = &gpu->rusd_cache; + NV00DE_SHARED_DATA *shared = gpu->rusd_map; + bool retry_soon = false; + int ret; + + if (cache->power_expires && time_before(jiffies, cache->power_expires)) + return; + + ret = read_field(&shared->avgPowerUsage, &cache->avg_power, + sizeof(cache->avg_power)); + if (!ret) + cache->avg_power_valid = true; + else if (ret == -ENODATA) + cache->avg_power_valid = false; + else + retry_soon = true; + + ret = read_field(&shared->instPowerUsage, &cache->inst_power, + sizeof(cache->inst_power)); + if (!ret) + cache->inst_power_valid = true; + else if (ret == -ENODATA) + cache->inst_power_valid = false; + else + retry_soon = true; + + ret = read_field(&shared->powerPolicyStatus, &cache->power_policy, + sizeof(cache->power_policy)); + if (!ret) + cache->power_policy_valid = true; + else if (ret == -ENODATA) + cache->power_policy_valid = false; + else + retry_soon = true; + + ret = read_field(&shared->powerLimitGpu, &cache->power_limit, + sizeof(cache->power_limit)); + if (!ret) + cache->power_limit_valid = true; + else if (ret == -ENODATA) + cache->power_limit_valid = false; + else + retry_soon = true; + + cache->power_expires = cache_expires(retry_soon); +} + +bool nvhwmon_rusd_power_has_average(u32 channel) +{ + return channel == NVHWMON_POWER_AVG_GPU || + channel == NVHWMON_POWER_AVG_MODULE || + channel == NVHWMON_POWER_AVG_MEMORY; +} + +bool nvhwmon_rusd_power_has_cap(u32 channel) +{ + return channel == NVHWMON_POWER_POLICY_TOTAL_GPU; +} + +static void refresh_fans(struct nvhwmon_gpu *gpu) +{ + struct nvhwmon_rusd_cache *cache = &gpu->rusd_cache; + NV00DE_SHARED_DATA *shared = gpu->rusd_map; + bool retry_soon = false; + int ret; + + if (cache->fan_expires && time_before(jiffies, cache->fan_expires)) + return; + + ret = read_field(&shared->fanCoolerStatus, &cache->fan_status, + sizeof(cache->fan_status)); + if (!ret) + cache->fan_valid = true; + else if (ret == -ENODATA) + cache->fan_valid = false; + else + retry_soon = true; + + cache->fan_expires = cache_expires(retry_soon); +} + +int nvhwmon_rusd_init(struct nvhwmon_gpu *gpu) +{ + NV00DE_ALLOC_PARAMETERS alloc_params = { 0 }; + NV00DE_CTRL_REQUEST_POLL_INTERVAL_PARAM interval_params = { 0 }; + NvU64 poll_mask = RUSD_BASE_POLL_MASK; + NvU32 status; + + /* Fan polling is requested only when native fan coolers were detected. */ + if (gpu->fan_count) + poll_mask |= NV00DE_RUSD_POLL_FAN; + + gpu->h_rusd = nvhwmon_rm_rusd_handle(gpu); + alloc_params.polledDataMask = poll_mask; + + status = nvhwmon_rm_alloc(gpu->h_client, gpu->h_subdevice, gpu->h_rusd, + RM_USER_SHARED_DATA, &alloc_params, + sizeof(alloc_params)); + if (status != NVOS_STATUS_SUCCESS) { + gpu->h_rusd = NV01_NULL_OBJECT; + return nvhwmon_rm_status_to_errno(status); + } + + status = nvhwmon_rm_map_memory(gpu->h_client, gpu->h_device, + gpu->h_rusd, 0, + sizeof(NV00DE_SHARED_DATA), + &gpu->rusd_map, + NV04_MAP_MEMORY_FLAGS_NONE); + if (status != NVOS_STATUS_SUCCESS || !gpu->rusd_map) { + nvhwmon_rusd_fini(gpu); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + return -EFAULT; + } + + /* Best effort: default polling still works if RM rejects this request. */ + interval_params.pollingIntervalMs = RUSD_POLL_INTERVAL_MS; + nvhwmon_rm_control(gpu->h_client, gpu->h_rusd, + NV00DE_CTRL_CMD_REQUEST_POLL_INTERVAL, + &interval_params, sizeof(interval_params)); + + return 0; +} + +void nvhwmon_rusd_fini(struct nvhwmon_gpu *gpu) +{ + memset(&gpu->rusd_cache, 0, sizeof(gpu->rusd_cache)); + + if (gpu->rusd_map) { + nvhwmon_rm_unmap_memory(gpu->h_client, gpu->h_device, + gpu->h_rusd, gpu->rusd_map, + NV04_MAP_MEMORY_FLAGS_NONE); + gpu->rusd_map = NULL; + } + + if (gpu->h_rusd != NV01_NULL_OBJECT) { + nvhwmon_rm_free(gpu->h_client, gpu->h_subdevice, gpu->h_rusd); + gpu->h_rusd = NV01_NULL_OBJECT; + } +} + +int nvhwmon_rusd_read_temp(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + NV00DE_SHARED_DATA *shared = gpu->rusd_map; + RUSD_TEMPERATURE temp; + + if (!shared) + return -ENODEV; + + if (channel >= NVHWMON_RUSD_TEMP_CHANNELS) + return -EINVAL; + + refresh_temps(gpu); + if (!gpu->rusd_cache.temp_valid[channel]) + return -ENODATA; + + temp = gpu->rusd_cache.temperatures[channel]; + *val = div_s64((s64)temp.temperature * 1000, 256); + return 0; +} + +int nvhwmon_rusd_read_power(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + NV00DE_SHARED_DATA *shared = gpu->rusd_map; + u32 mw; + + if (!shared) + return -ENODEV; + + if (channel >= NVHWMON_POWER_CHANNELS) + return -EINVAL; + + refresh_power(gpu); + + switch (channel) { + case NVHWMON_POWER_AVG_GPU: + if (!gpu->rusd_cache.avg_power_valid) + return -ENODATA; + mw = gpu->rusd_cache.avg_power.info.averageGpuPower; + break; + case NVHWMON_POWER_AVG_MODULE: + if (!gpu->rusd_cache.avg_power_valid) + return -ENODATA; + mw = gpu->rusd_cache.avg_power.info.averageModulePower; + break; + case NVHWMON_POWER_AVG_MEMORY: + if (!gpu->rusd_cache.avg_power_valid) + return -ENODATA; + mw = gpu->rusd_cache.avg_power.info.averageMemoryPower; + break; + case NVHWMON_POWER_INST_GPU: + if (!gpu->rusd_cache.inst_power_valid) + return -ENODATA; + mw = gpu->rusd_cache.inst_power.info.instGpuPower; + break; + case NVHWMON_POWER_INST_MODULE: + if (!gpu->rusd_cache.inst_power_valid) + return -ENODATA; + mw = gpu->rusd_cache.inst_power.info.instModulePower; + break; + case NVHWMON_POWER_INST_CPU: + if (!gpu->rusd_cache.inst_power_valid) + return -ENODATA; + mw = gpu->rusd_cache.inst_power.info.instCpuPower; + break; + case NVHWMON_POWER_POLICY_TOTAL_GPU: + if (!gpu->rusd_cache.power_policy_valid) + return -ENODATA; + mw = gpu->rusd_cache.power_policy.info.tgpmW; + break; + case NVHWMON_POWER_LIMIT_REQUESTED: + if (!gpu->rusd_cache.power_limit_valid) + return -ENODATA; + mw = gpu->rusd_cache.power_limit.info.requestedmW; + break; + case NVHWMON_POWER_LIMIT_ENFORCED: + if (!gpu->rusd_cache.power_limit_valid) + return -ENODATA; + mw = gpu->rusd_cache.power_limit.info.enforcedmW; + break; + default: + return -EINVAL; + } + + *val = (long)mw * 1000; + return 0; +} + +int nvhwmon_rusd_read_power_average(struct nvhwmon_gpu *gpu, u32 channel, + long *val) +{ + if (!nvhwmon_rusd_power_has_average(channel)) + return -EINVAL; + + return nvhwmon_rusd_read_power(gpu, channel, val); +} + +int nvhwmon_rusd_read_power_cap(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + if (!nvhwmon_rusd_power_has_cap(channel)) + return -EINVAL; + + return nvhwmon_rusd_read_power(gpu, NVHWMON_POWER_LIMIT_REQUESTED, val); +} + +int nvhwmon_rusd_read_fan(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + NV00DE_SHARED_DATA *shared = gpu->rusd_map; + u8 rm_index; + + if (!shared) + return -ENODEV; + + if (channel >= gpu->fan_count) + return -EINVAL; + + rm_index = gpu->fans[channel].rm_index; + if (rm_index >= RUSD_FAN_COOLER_MAX_COOLERS) + return -EINVAL; + + refresh_fans(gpu); + if (!gpu->rusd_cache.fan_valid) + return -ENODATA; + + *val = gpu->rusd_cache.fan_status.info.rpmCurr[rm_index]; + return 0; +} + +const char *nvhwmon_rusd_temp_label(u32 channel) +{ + static const char *const labels[] = { + [RUSD_TEMPERATURE_TYPE_GPU] = "gpu", + [RUSD_TEMPERATURE_TYPE_MEMORY] = "memory", + [RUSD_TEMPERATURE_TYPE_BOARD] = "board", + [RUSD_TEMPERATURE_TYPE_POWER_SUPPLY] = "power_supply", + [RUSD_TEMPERATURE_TYPE_HBM] = "hbm", + }; + + if (channel >= ARRAY_SIZE(labels) || !labels[channel]) + return "unknown"; + + return labels[channel]; +} + +const char *nvhwmon_rusd_power_label(u32 channel) +{ + static const char *const labels[] = { + [NVHWMON_POWER_AVG_GPU] = "average_gpu", + [NVHWMON_POWER_AVG_MODULE] = "average_module", + [NVHWMON_POWER_AVG_MEMORY] = "average_memory", + [NVHWMON_POWER_INST_GPU] = "instant_gpu", + [NVHWMON_POWER_INST_MODULE] = "instant_module", + [NVHWMON_POWER_INST_CPU] = "instant_cpu", + [NVHWMON_POWER_POLICY_TOTAL_GPU] = "policy_total_gpu", + [NVHWMON_POWER_LIMIT_REQUESTED] = "limit_requested", + [NVHWMON_POWER_LIMIT_ENFORCED] = "limit_enforced", + }; + + if (channel >= ARRAY_SIZE(labels) || !labels[channel]) + return "unknown"; + + return labels[channel]; +} diff --git a/kernel-open/nvidia/hwmon-rusd.h b/kernel-open/nvidia/hwmon-rusd.h new file mode 100644 index 000000000..7930b0c4a --- /dev/null +++ b/kernel-open/nvidia/hwmon-rusd.h @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_RUSD_H +#define NVHWMON_RUSD_H + +#include "hwmon-main.h" + +/* RUSD init maps shared telemetry; read helpers are serialized by gpu->lock. */ +int nvhwmon_rusd_init(struct nvhwmon_gpu *gpu); +void nvhwmon_rusd_fini(struct nvhwmon_gpu *gpu); + +int nvhwmon_rusd_read_temp(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_rusd_read_power(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_rusd_read_power_average(struct nvhwmon_gpu *gpu, u32 channel, + long *val); +int nvhwmon_rusd_read_power_cap(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_rusd_read_fan(struct nvhwmon_gpu *gpu, u32 channel, long *val); + +bool nvhwmon_rusd_power_has_average(u32 channel); +bool nvhwmon_rusd_power_has_cap(u32 channel); + +const char *nvhwmon_rusd_temp_label(u32 channel); +const char *nvhwmon_rusd_power_label(u32 channel); + +#endif diff --git a/kernel-open/nvidia/hwmon-temp-limit.c b/kernel-open/nvidia/hwmon-temp-limit.c new file mode 100644 index 000000000..af2dd9f47 --- /dev/null +++ b/kernel-open/nvidia/hwmon-temp-limit.c @@ -0,0 +1,263 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "hwmon-rm.h" +#include "hwmon-temp-limit.h" + +#define CTRL_CMD_THERMAL_GET_THRESHOLD 0x20808546U +#define THERMAL_THRESHOLD_SLOWDOWN_FIRST 0x00000009U +#define THERMAL_THRESHOLD_SLOWDOWN_LAST 0x00000014U +#define THERMAL_THRESHOLD_SHUTDOWN 0x00000015U + +#define CTRL_CMD_PWR_POLICY_GET_INFO 0x2080852aU +#define CTRL_CMD_PWR_POLICY_GET_STATUS 0x2080852cU +#define PWR_POLICY_INFO_SIZE 1784U +#define PWR_POLICY_STATUS_SIZE 1440U +#define PWR_POLICY_THERM_POLICY_OFFSET 0x08U +#define PWR_POLICY_GPU_MAX_TEMP_OFFSET 0x24U +#define TEMP_LIMIT_MAX_NV_TEMP (200 * 256) + +struct threshold_params { + u32 threshold_id; + s32 threshold; + u32 reserved; + u32 flags; + u32 policy_mask; + u32 target_mask; +}; + +static s32 nvtemp_to_mc(s32 nvtemp) +{ + return div_s64((s64)nvtemp * 1000, 256); +} + +static bool valid_nvtemp(s32 nvtemp) +{ + return nvtemp > 0 && nvtemp <= TEMP_LIMIT_MAX_NV_TEMP; +} + +static int get_threshold(struct nvhwmon_gpu *gpu, u32 threshold_id, s32 *mc) +{ + struct threshold_params params = { + .threshold_id = threshold_id, + }; + u32 status; + + BUILD_BUG_ON(sizeof(params) != 24); + + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_THERMAL_GET_THRESHOLD, + ¶ms, sizeof(params)); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + + if (!valid_nvtemp(params.threshold)) + return -ENODATA; + + *mc = nvtemp_to_mc(params.threshold); + return 0; +} + +static int get_slowdown(struct nvhwmon_gpu *gpu, s32 *mc) +{ + bool found = false; + s32 min_mc = S32_MAX; + u32 threshold_id; + + /* Multiple slowdown thresholds may exist; expose the most conservative. */ + for (threshold_id = THERMAL_THRESHOLD_SLOWDOWN_FIRST; + threshold_id <= THERMAL_THRESHOLD_SLOWDOWN_LAST; + threshold_id++) { + s32 candidate_mc; + int ret; + + ret = get_threshold(gpu, threshold_id, &candidate_mc); + if (ret) + continue; + + found = true; + min_mc = min(min_mc, candidate_mc); + } + + if (!found) + return -ENODATA; + + *mc = min_mc; + return 0; +} + +/* + * The power policy status blob is keyed by policy id. Read the id from the + * info blob first, then request the matching status blob. + */ +static int get_gpu_max(struct nvhwmon_gpu *gpu, s32 *mc) +{ + u8 *params; + s32 nvtemp; + u32 policy_id; + u32 status; + int ret = 0; + + BUILD_BUG_ON(PWR_POLICY_THERM_POLICY_OFFSET + sizeof(u32) > + PWR_POLICY_INFO_SIZE); + BUILD_BUG_ON(PWR_POLICY_THERM_POLICY_OFFSET + sizeof(u32) > + PWR_POLICY_STATUS_SIZE); + BUILD_BUG_ON(PWR_POLICY_GPU_MAX_TEMP_OFFSET + sizeof(u32) > + PWR_POLICY_STATUS_SIZE); + + params = kzalloc(max(PWR_POLICY_INFO_SIZE, + PWR_POLICY_STATUS_SIZE), + GFP_KERNEL); + if (!params) + return -ENOMEM; + + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_PWR_POLICY_GET_INFO, + params, PWR_POLICY_INFO_SIZE); + if (status != NVOS_STATUS_SUCCESS) { + ret = nvhwmon_rm_status_to_errno(status); + goto out; + } + + policy_id = get_unaligned_le32(params + + PWR_POLICY_THERM_POLICY_OFFSET); + memset(params, 0, PWR_POLICY_STATUS_SIZE); + put_unaligned_le32(policy_id, + params + PWR_POLICY_THERM_POLICY_OFFSET); + + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + CTRL_CMD_PWR_POLICY_GET_STATUS, + params, PWR_POLICY_STATUS_SIZE); + if (status != NVOS_STATUS_SUCCESS) { + ret = nvhwmon_rm_status_to_errno(status); + goto out; + } + + nvtemp = (s32)get_unaligned_le32(params + + PWR_POLICY_GPU_MAX_TEMP_OFFSET); + if (!valid_nvtemp(nvtemp)) { + ret = -ENODATA; + goto out; + } + + *mc = nvtemp_to_mc(nvtemp); + +out: + kfree(params); + return ret; +} + +static void apply_gpu_limits(struct nvhwmon_gpu *gpu, u32 channel, + const struct nvhwmon_temp_limits *limits) +{ + if (channel >= NVHWMON_TEMP_CHANNELS) + return; + + gpu->temp_limits[channel] = *limits; +} + +void nvhwmon_temp_limit_probe(struct nvhwmon_gpu *gpu) +{ + struct nvhwmon_temp_limits limits = { 0 }; + u32 channel; + int ret; + + ret = get_gpu_max(gpu, &limits.max_mc); + limits.max_valid = !ret; + + ret = get_slowdown(gpu, &limits.crit_mc); + limits.crit_valid = !ret; + + ret = get_threshold(gpu, THERMAL_THRESHOLD_SHUTDOWN, + &limits.emergency_mc); + limits.emergency_valid = !ret; + + if (!limits.max_valid && !limits.crit_valid && !limits.emergency_valid) + return; + + /* Apply GPU limits to both the RUSD GPU channel and GPU target sensors. */ + apply_gpu_limits(gpu, RUSD_TEMPERATURE_TYPE_GPU, &limits); + + for (channel = 0; channel < gpu->thermal_count; channel++) { + if (gpu->thermal_sensors[channel].target_type != + NV2080_CTRL_THERMAL_SYSTEM_TARGET_GPU) + continue; + + apply_gpu_limits(gpu, NVHWMON_RUSD_TEMP_CHANNELS + channel, + &limits); + } +} + +bool nvhwmon_temp_limit_has(const struct nvhwmon_gpu *gpu, u32 channel, u32 attr) +{ + const struct nvhwmon_temp_limits *limits; + + if (channel >= NVHWMON_TEMP_CHANNELS) + return false; + + limits = &gpu->temp_limits[channel]; + + switch (attr) { + case hwmon_temp_max: + return limits->max_valid; + case hwmon_temp_crit: + return limits->crit_valid; + case hwmon_temp_emergency: + return limits->emergency_valid; + default: + return false; + } +} + +int nvhwmon_temp_limit_read(const struct nvhwmon_gpu *gpu, u32 channel, u32 attr, + long *val) +{ + const struct nvhwmon_temp_limits *limits; + + if (!nvhwmon_temp_limit_has(gpu, channel, attr)) + return -EINVAL; + + limits = &gpu->temp_limits[channel]; + + switch (attr) { + case hwmon_temp_max: + *val = limits->max_mc; + return 0; + case hwmon_temp_crit: + *val = limits->crit_mc; + return 0; + case hwmon_temp_emergency: + *val = limits->emergency_mc; + return 0; + default: + return -EOPNOTSUPP; + } +} diff --git a/kernel-open/nvidia/hwmon-temp-limit.h b/kernel-open/nvidia/hwmon-temp-limit.h new file mode 100644 index 000000000..961aadb38 --- /dev/null +++ b/kernel-open/nvidia/hwmon-temp-limit.h @@ -0,0 +1,36 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_TEMP_LIMIT_H +#define NVHWMON_TEMP_LIMIT_H + +#include "hwmon-main.h" + +/* Temperature limits are discovered once and exported only when valid. */ +void nvhwmon_temp_limit_probe(struct nvhwmon_gpu *gpu); + +bool nvhwmon_temp_limit_has(const struct nvhwmon_gpu *gpu, u32 channel, u32 attr); +int nvhwmon_temp_limit_read(const struct nvhwmon_gpu *gpu, u32 channel, + u32 attr, long *val); + +#endif diff --git a/kernel-open/nvidia/hwmon-thermal.c b/kernel-open/nvidia/hwmon-thermal.c new file mode 100644 index 000000000..faf3a400a --- /dev/null +++ b/kernel-open/nvidia/hwmon-thermal.c @@ -0,0 +1,340 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include "hwmon-rm.h" +#include "hwmon-thermal.h" + +#define THERMAL_CACHE_MS 1000 +#define THERMAL_ERROR_RETRY_MS 100 +#define THERMAL_TARGET_SLOTS 5 + +/* Thermal-system queries batch small instructions into one RM control call. */ +static int execute(struct nvhwmon_gpu *gpu, + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_V2_PARAMS *params) +{ + u32 status; + + params->clientAPIVersion = THERMAL_SYSTEM_API_VER; + params->clientAPIRevision = THERMAL_SYSTEM_API_REV; + params->clientInstructionSizeOf = + sizeof(NV2080_CTRL_THERMAL_SYSTEM_INSTRUCTION); + + status = nvhwmon_rm_control(gpu->h_client, gpu->h_subdevice, + NV2080_CTRL_CMD_THERMAL_SYSTEM_EXECUTE_V2, + params, sizeof(*params)); + if (status != NVOS_STATUS_SUCCESS) + return nvhwmon_rm_status_to_errno(status); + + return 0; +} + +static bool +instruction_ok(const NV2080_CTRL_THERMAL_SYSTEM_INSTRUCTION *instruction) +{ + return instruction->executed && + instruction->result == NVOS_STATUS_SUCCESS; +} + +static int get_sensor_count(struct nvhwmon_gpu *gpu, u32 *count) +{ + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_V2_PARAMS params = { 0 }; + NV2080_CTRL_THERMAL_SYSTEM_INSTRUCTION *instruction; + int ret; + + params.instructionListSize = 1; + instruction = ¶ms.instructionList[0]; + instruction->opcode = + NV2080_CTRL_THERMAL_SYSTEM_GET_INFO_SENSORS_AVAILABLE_OPCODE; + + ret = execute(gpu, ¶ms); + if (ret) + return ret; + + if (!instruction_ok(instruction)) + return -ENODATA; + + *count = instruction->operands.getInfoSensorsAvailable.availableSensors; + return 0; +} + +static const char *target_name(u32 target_type) +{ + switch (target_type) { + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_GPU: + return "gpu_hw"; + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_MEMORY: + return "memory_hw"; + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_POWER_SUPPLY: + return "power_supply_hw"; + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_BOARD: + return "board_hw"; + default: + return "thermal_hw"; + } +} + +static u32 target_slot(u32 target_type) +{ + switch (target_type) { + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_GPU: + return 0; + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_MEMORY: + return 1; + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_POWER_SUPPLY: + return 2; + case NV2080_CTRL_THERMAL_SYSTEM_TARGET_BOARD: + return 3; + default: + return 4; + } +} + +static void set_label(struct nvhwmon_thermal_sensor *sensor, u32 target_seen) +{ + const char *base = target_name(sensor->target_type); + + if (target_seen) + scnprintf(sensor->label, sizeof(sensor->label), "%s%u", base, + target_seen + 1); + else + strscpy(sensor->label, base, sizeof(sensor->label)); +} + +static void probe_sensor(struct nvhwmon_gpu *gpu, u32 sensor_index, + u32 *target_counts) +{ + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_V2_PARAMS params = { 0 }; + struct nvhwmon_thermal_sensor *sensor; + NV2080_CTRL_THERMAL_SYSTEM_INSTRUCTION *instruction; + u32 target_index; + u32 slot; + int ret; + + if (gpu->thermal_count >= NVHWMON_THERMAL_MAX_SENSORS) + return; + + /* Target index is required for stable labels; reading range is optional. */ + params.executeFlags = + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_FLAGS_IGNORE_FAIL; + params.instructionListSize = 2; + + instruction = ¶ms.instructionList[0]; + instruction->opcode = + NV2080_CTRL_THERMAL_SYSTEM_GET_INFO_SENSOR_TARGET_OPCODE; + instruction->operands.getInfoSensorTarget.sensorIndex = sensor_index; + + instruction = ¶ms.instructionList[1]; + instruction->opcode = + NV2080_CTRL_THERMAL_SYSTEM_GET_INFO_SENSOR_READING_RANGE_OPCODE; + instruction->operands.getInfoSensorReadingRange.sensorIndex = + sensor_index; + + ret = execute(gpu, ¶ms); + if (ret) + return; + + if (!instruction_ok(¶ms.instructionList[0])) + return; + + sensor = &gpu->thermal_sensors[gpu->thermal_count]; + memset(sensor, 0, sizeof(*sensor)); + sensor->sensor_index = sensor_index; + target_index = params.instructionList[0] + .operands.getInfoSensorTarget.targetIndex; + + if (instruction_ok(¶ms.instructionList[1])) { + sensor->min_valid = true; + sensor->max_valid = true; + sensor->min_c = + params.instructionList[1] + .operands.getInfoSensorReadingRange.minimum; + sensor->max_c = + params.instructionList[1] + .operands.getInfoSensorReadingRange.maximum; + } + + memset(¶ms, 0, sizeof(params)); + params.executeFlags = + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_FLAGS_IGNORE_FAIL; + params.instructionListSize = 1; + + instruction = ¶ms.instructionList[0]; + instruction->opcode = + NV2080_CTRL_THERMAL_SYSTEM_GET_INFO_TARGET_TYPE_OPCODE; + instruction->operands.getInfoTargetType.targetIndex = target_index; + + /* Keep the sensor visible even if target type is unavailable. */ + ret = execute(gpu, ¶ms); + if (!ret && instruction_ok(¶ms.instructionList[0])) + sensor->target_type = params.instructionList[0] + .operands.getInfoTargetType.type; + else + sensor->target_type = NV2080_CTRL_THERMAL_SYSTEM_TARGET_UNKNOWN; + + slot = target_slot(sensor->target_type); + set_label(sensor, target_counts[slot]); + target_counts[slot]++; + gpu->thermal_count++; +} + +void nvhwmon_thermal_probe(struct nvhwmon_gpu *gpu) +{ + u32 target_counts[THERMAL_TARGET_SLOTS] = { 0 }; + u32 count; + u32 sensor; + int ret; + + BUILD_BUG_ON(NVHWMON_THERMAL_MAX_SENSORS > + NV2080_CTRL_THERMAL_SYSTEM_INSTRUCTION_MAX_COUNT); + + ret = get_sensor_count(gpu, &count); + if (ret) + return; + + count = min_t(u32, count, NVHWMON_THERMAL_MAX_SENSORS); + for (sensor = 0; sensor < count; sensor++) + probe_sensor(gpu, sensor, target_counts); +} + +static unsigned long cache_expires(bool retry_soon) +{ + u32 cache_ms = retry_soon ? THERMAL_ERROR_RETRY_MS : + THERMAL_CACHE_MS; + + return jiffies + msecs_to_jiffies(cache_ms); +} + +static void refresh(struct nvhwmon_gpu *gpu) +{ + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_V2_PARAMS params = { 0 }; + bool retry_soon = false; + u32 channel; + int ret; + + if (gpu->thermal_expires && time_before(jiffies, gpu->thermal_expires)) + return; + + /* Read all probed sensors in one batch and retry sooner on partial data. */ + params.executeFlags = + NV2080_CTRL_THERMAL_SYSTEM_EXECUTE_FLAGS_IGNORE_FAIL; + params.instructionListSize = gpu->thermal_count; + + for (channel = 0; channel < gpu->thermal_count; channel++) { + NV2080_CTRL_THERMAL_SYSTEM_INSTRUCTION *instruction; + + instruction = ¶ms.instructionList[channel]; + instruction->opcode = + NV2080_CTRL_THERMAL_SYSTEM_GET_STATUS_SENSOR_READING_OPCODE; + instruction->operands.getStatusSensorReading.sensorIndex = + gpu->thermal_sensors[channel].sensor_index; + } + + ret = execute(gpu, ¶ms); + if (ret) { + for (channel = 0; channel < gpu->thermal_count; channel++) + gpu->thermal_sensors[channel].input_valid = false; + gpu->thermal_expires = cache_expires(true); + return; + } + + for (channel = 0; channel < gpu->thermal_count; channel++) { + struct nvhwmon_thermal_sensor *sensor; + + sensor = &gpu->thermal_sensors[channel]; + if (instruction_ok(¶ms.instructionList[channel])) { + sensor->input_valid = true; + sensor->input_c = + params.instructionList[channel] + .operands.getStatusSensorReading.value; + } else { + sensor->input_valid = false; + retry_soon = true; + } + } + + gpu->thermal_expires = cache_expires(retry_soon); +} + +bool nvhwmon_thermal_has_sensor(const struct nvhwmon_gpu *gpu, u32 channel) +{ + return channel < gpu->thermal_count; +} + +bool nvhwmon_thermal_has_rated_min(const struct nvhwmon_gpu *gpu, u32 channel) +{ + return nvhwmon_thermal_has_sensor(gpu, channel) && + gpu->thermal_sensors[channel].min_valid; +} + +bool nvhwmon_thermal_has_rated_max(const struct nvhwmon_gpu *gpu, u32 channel) +{ + return nvhwmon_thermal_has_sensor(gpu, channel) && + gpu->thermal_sensors[channel].max_valid; +} + +int nvhwmon_thermal_read_temp(struct nvhwmon_gpu *gpu, u32 channel, long *val) +{ + if (!nvhwmon_thermal_has_sensor(gpu, channel)) + return -EINVAL; + + refresh(gpu); + if (!gpu->thermal_sensors[channel].input_valid) + return -ENODATA; + + *val = (long)gpu->thermal_sensors[channel].input_c * 1000; + return 0; +} + +int nvhwmon_thermal_read_rated_min(struct nvhwmon_gpu *gpu, u32 channel, + long *val) +{ + if (!nvhwmon_thermal_has_rated_min(gpu, channel)) + return -EINVAL; + + *val = (long)gpu->thermal_sensors[channel].min_c * 1000; + return 0; +} + +int nvhwmon_thermal_read_rated_max(struct nvhwmon_gpu *gpu, u32 channel, + long *val) +{ + if (!nvhwmon_thermal_has_rated_max(gpu, channel)) + return -EINVAL; + + *val = (long)gpu->thermal_sensors[channel].max_c * 1000; + return 0; +} + +const char *nvhwmon_thermal_label(const struct nvhwmon_gpu *gpu, u32 channel) +{ + if (!nvhwmon_thermal_has_sensor(gpu, channel)) + return "unknown"; + + return gpu->thermal_sensors[channel].label; +} diff --git a/kernel-open/nvidia/hwmon-thermal.h b/kernel-open/nvidia/hwmon-thermal.h new file mode 100644 index 000000000..8fdf0eb19 --- /dev/null +++ b/kernel-open/nvidia/hwmon-thermal.h @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 Jihong Min. All rights reserved. + * SPDX-License-Identifier: MIT + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#ifndef NVHWMON_THERMAL_H +#define NVHWMON_THERMAL_H + +#include "hwmon-main.h" + +/* Probe builds the channel list; runtime reads refresh cached temperatures. */ +void nvhwmon_thermal_probe(struct nvhwmon_gpu *gpu); + +bool nvhwmon_thermal_has_sensor(const struct nvhwmon_gpu *gpu, u32 channel); +bool nvhwmon_thermal_has_rated_min(const struct nvhwmon_gpu *gpu, u32 channel); +bool nvhwmon_thermal_has_rated_max(const struct nvhwmon_gpu *gpu, u32 channel); + +int nvhwmon_thermal_read_temp(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_thermal_read_rated_min(struct nvhwmon_gpu *gpu, u32 channel, long *val); +int nvhwmon_thermal_read_rated_max(struct nvhwmon_gpu *gpu, u32 channel, long *val); +const char *nvhwmon_thermal_label(const struct nvhwmon_gpu *gpu, u32 channel); + +#endif diff --git a/kernel-open/nvidia/nv-modeset-interface.c b/kernel-open/nvidia/nv-modeset-interface.c index 9c18c3ff1..72fb7fcec 100644 --- a/kernel-open/nvidia/nv-modeset-interface.c +++ b/kernel-open/nvidia/nv-modeset-interface.c @@ -27,6 +27,7 @@ #include "nv-linux.h" #include "nvstatus.h" #include "nv.h" +#include "hwmon-entry.h" static const nvidia_modeset_callbacks_t *nv_modeset_callbacks; @@ -73,6 +74,8 @@ void nvidia_modeset_resume(NvU32 gpuId) void nvidia_modeset_remove(NvU32 gpuId) { + nvhwmon_driver_gpu_remove(gpuId); + if (nv_modeset_callbacks && nv_modeset_callbacks->remove) { nv_modeset_callbacks->remove(gpuId); @@ -103,13 +106,20 @@ static void nvidia_modeset_get_gpu_info(nv_gpu_info_t *gpu_info, void nvidia_modeset_probe(const nv_linux_state_t *nvl) { + nv_gpu_info_t gpu_info; + + /* + * hwmon also consumes gpu_info, so collect it even when modeset has no + * probe callback registered. + */ + nvidia_modeset_get_gpu_info(&gpu_info, nvl); + if (nv_modeset_callbacks && nv_modeset_callbacks->probe) { - nv_gpu_info_t gpu_info; - - nvidia_modeset_get_gpu_info(&gpu_info, nvl); nv_modeset_callbacks->probe(&gpu_info); } + + nvhwmon_driver_gpu_add(&gpu_info); } static NvU32 nvidia_modeset_enumerate_gpus(nv_gpu_info_t *gpu_info) diff --git a/kernel-open/nvidia/nv.c b/kernel-open/nvidia/nv.c index b771a001e..9cf86a0d9 100644 --- a/kernel-open/nvidia/nv.c +++ b/kernel-open/nvidia/nv.c @@ -54,6 +54,7 @@ #include "nv-kthread-q.h" #include "nv-dmabuf.h" #include "nv-caps-imex.h" +#include "hwmon-entry.h" /* * Commit aefb2f2e619b ("x86/bugs: Rename CONFIG_RETPOLINE => @@ -203,6 +204,8 @@ struct semaphore nv_linux_devices_lock; // Assigned at device probe (module init) time NvBool nv_ats_supported; +static bool hwmon_initialized; + // allow an easy way to convert all debug printfs related to events // back and forth between 'info' and 'errors' #if defined(NV_DBG_EVENTS) @@ -992,6 +995,17 @@ static int __init nvidia_init_module(void) goto partial_chrdev_exit; } + rc = nvhwmon_driver_init(); + if (rc < 0) + { + nv_printf(NV_DBG_WARNINGS, + "NVRM: hwmon initialization failed: %d\n", rc); + } + else + { + hwmon_initialized = true; + } + __nv_init_sp = sp; return 0; @@ -1035,6 +1049,12 @@ static void __exit nvidia_exit_module(void) { nvidia_stack_t *sp = __nv_init_sp; + if (hwmon_initialized) + { + nvhwmon_driver_exit(); + hwmon_initialized = false; + } + nv_unregister_chrdev(NV_MINOR_DEVICE_NUMBER_CONTROL_DEVICE, 1, &nv_linux_control_device_cdev); nv_unregister_chrdev(0, NV_MINOR_DEVICE_NUMBER_REGULAR_MAX + 1, diff --git a/kernel-open/nvidia/nvidia-sources.Kbuild b/kernel-open/nvidia/nvidia-sources.Kbuild index 4fcf01296..ad8d11b77 100644 --- a/kernel-open/nvidia/nvidia-sources.Kbuild +++ b/kernel-open/nvidia/nvidia-sources.Kbuild @@ -1,5 +1,6 @@ NVIDIA_SOURCES ?= NVIDIA_SOURCES_CXX ?= +NVHWMON_SOURCES ?= NVIDIA_SOURCES += nvidia/nv-platform.c NVIDIA_SOURCES += nvidia/nv-dsi-parse-panel-props.c @@ -59,3 +60,10 @@ NVIDIA_SOURCES += nvidia/nvlink_caps.c NVIDIA_SOURCES += nvidia/linux_nvswitch.c NVIDIA_SOURCES += nvidia/procfs_nvswitch.c NVIDIA_SOURCES += nvidia/i2c_nvswitch.c +NVHWMON_SOURCES += nvidia/hwmon-main.c +NVHWMON_SOURCES += nvidia/hwmon-rm.c +NVHWMON_SOURCES += nvidia/hwmon-rusd.c +NVHWMON_SOURCES += nvidia/hwmon-thermal.c +NVHWMON_SOURCES += nvidia/hwmon-temp-limit.c +NVHWMON_SOURCES += nvidia/hwmon-fan.c +NVHWMON_SOURCES += nvidia/hwmon-device.c diff --git a/kernel-open/nvidia/nvidia.Kbuild b/kernel-open/nvidia/nvidia.Kbuild index d8c108fc2..6311ef989 100644 --- a/kernel-open/nvidia/nvidia.Kbuild +++ b/kernel-open/nvidia/nvidia.Kbuild @@ -8,9 +8,11 @@ include $(src)/nvidia/nvidia-sources.Kbuild NVIDIA_OBJECTS = $(patsubst %.c,%.o,$(NVIDIA_SOURCES)) +NVHWMON_OBJECTS = $(patsubst %.c,%.o,$(NVHWMON_SOURCES)) obj-m += nvidia.o nvidia-y := $(NVIDIA_OBJECTS) +nvidia-y += $(NVHWMON_OBJECTS) NVIDIA_KO = nvidia/nvidia.ko @@ -70,6 +72,11 @@ endif $(call ASSIGN_PER_OBJ_CFLAGS, $(NVIDIA_OBJECTS), $(NVIDIA_CFLAGS)) +NVHWMON_CFLAGS += -I$(src)/../src/common/sdk/nvidia/inc +NVHWMON_CFLAGS += -I$(src)/../src/nvidia/arch/nvalloc/unix/include + +$(call ASSIGN_PER_OBJ_CFLAGS, $(NVHWMON_OBJECTS), $(NVHWMON_CFLAGS)) + # # nv-procfs.c requires nv-compiler.h @@ -100,7 +107,10 @@ NVIDIA_INTERFACE := nvidia/nv-interface.o always += $(NVIDIA_INTERFACE) always-y += $(NVIDIA_INTERFACE) -$(obj)/$(NVIDIA_INTERFACE): $(addprefix $(obj)/,$(NVIDIA_OBJECTS)) +# nv-interface.o is a separate relocatable interface object, not an input to +# nvidia-y. Keep NVHWMON_OBJECTS here so the interface object exports the same +# in-tree hwmon additions without linking them twice into nvidia.ko. +$(obj)/$(NVIDIA_INTERFACE): $(addprefix $(obj)/,$(NVIDIA_OBJECTS) $(NVHWMON_OBJECTS)) $(LD) -r -o $@ $^ @@ -109,6 +119,7 @@ $(obj)/$(NVIDIA_INTERFACE): $(addprefix $(obj)/,$(NVIDIA_OBJECTS)) # NV_OBJECTS_DEPEND_ON_CONFTEST += $(NVIDIA_OBJECTS) +NV_OBJECTS_DEPEND_ON_CONFTEST += $(NVHWMON_OBJECTS) NV_CONFTEST_FUNCTION_COMPILE_TESTS += set_pages_uc NV_CONFTEST_FUNCTION_COMPILE_TESTS += set_memory_uc