da74944d3a
Recently 'cur_state' user space 'sysfs' interface 'sysfs' has been deprecated. This interface is used in Nvidia systems for setting fan speed limit. Currently fan speed limit is set from the user space by setting 'sysfs' 'cur_state' attribute to 'max_state + n', where 'n' is required limit, for example: 15 for 50% speed limit, 20 for full fan speed enforcement. The purpose of this feature is to provides ability to limit fan speed according to some system wise considerations, like absence of some replaceable units (PSU or line cards), high system ambient temperature, unreliable transceivers temperature sensing or some other factors which indirectly impacts system's airflow. The motivation is to support fan low limit feature through 'hwmon' interface. Use 'hwmon' 'pwm' attribute for setting low limit for fan speed in case 'thermal' subsystem is configured in kernel. In this case setting fan speed through 'hwmon' will never let the 'thermal' subsystem to select a lower duty cycle than the duty cycle selected with the 'pwm' attribute. From other side, fan speed is to be updated in hardware through 'pwm' only in case the requested fan speed is above last speed set by 'thermal' subsystem, otherwise requested fan speed will be just stored with no PWM update. Signed-off-by: Vadim Pasternak <vadimp@nvidia.com> Link: https://lore.kernel.org/r/20220126141825.13545-1-vadimp@nvidia.com Signed-off-by: Guenter Roeck <linux@roeck-us.net>
615 lines
15 KiB
C
615 lines
15 KiB
C
// SPDX-License-Identifier: (GPL-2.0 OR BSD-3-Clause)
|
|
//
|
|
// Copyright (c) 2018 Mellanox Technologies. All rights reserved.
|
|
// Copyright (c) 2018 Vadim Pasternak <vadimp@mellanox.com>
|
|
|
|
#include <linux/bitops.h>
|
|
#include <linux/device.h>
|
|
#include <linux/hwmon.h>
|
|
#include <linux/module.h>
|
|
#include <linux/platform_data/mlxreg.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/regmap.h>
|
|
#include <linux/thermal.h>
|
|
|
|
#define MLXREG_FAN_MAX_TACHO 14
|
|
#define MLXREG_FAN_MAX_PWM 4
|
|
#define MLXREG_FAN_PWM_NOT_CONNECTED 0xff
|
|
#define MLXREG_FAN_MAX_STATE 10
|
|
#define MLXREG_FAN_MIN_DUTY 51 /* 20% */
|
|
#define MLXREG_FAN_MAX_DUTY 255 /* 100% */
|
|
#define MLXREG_FAN_SPEED_MIN_LEVEL 2 /* 20 percent */
|
|
#define MLXREG_FAN_TACHO_SAMPLES_PER_PULSE_DEF 44
|
|
#define MLXREG_FAN_TACHO_DIV_MIN 283
|
|
#define MLXREG_FAN_TACHO_DIV_DEF (MLXREG_FAN_TACHO_DIV_MIN * 4)
|
|
#define MLXREG_FAN_TACHO_DIV_SCALE_MAX 64
|
|
/*
|
|
* FAN datasheet defines the formula for RPM calculations as RPM = 15/t-high.
|
|
* The logic in a programmable device measures the time t-high by sampling the
|
|
* tachometer every t-sample (with the default value 11.32 uS) and increment
|
|
* a counter (N) as long as the pulse has not change:
|
|
* RPM = 15 / (t-sample * (K + Regval)), where:
|
|
* Regval: is the value read from the programmable device register;
|
|
* - 0xff - represents tachometer fault;
|
|
* - 0xfe - represents tachometer minimum value , which is 4444 RPM;
|
|
* - 0x00 - represents tachometer maximum value , which is 300000 RPM;
|
|
* K: is 44 and it represents the minimum allowed samples per pulse;
|
|
* N: is equal K + Regval;
|
|
* In order to calculate RPM from the register value the following formula is
|
|
* used: RPM = 15 / ((Regval + K) * 11.32) * 10^(-6)), which in the
|
|
* default case is modified to:
|
|
* RPM = 15000000 * 100 / ((Regval + 44) * 1132);
|
|
* - for Regval 0x00, RPM will be 15000000 * 100 / (44 * 1132) = 30115;
|
|
* - for Regval 0xfe, RPM will be 15000000 * 100 / ((254 + 44) * 1132) = 4446;
|
|
* In common case the formula is modified to:
|
|
* RPM = 15000000 * 100 / ((Regval + samples) * divider).
|
|
*/
|
|
#define MLXREG_FAN_GET_RPM(rval, d, s) (DIV_ROUND_CLOSEST(15000000 * 100, \
|
|
((rval) + (s)) * (d)))
|
|
#define MLXREG_FAN_GET_FAULT(val, mask) ((val) == (mask))
|
|
#define MLXREG_FAN_PWM_DUTY2STATE(duty) (DIV_ROUND_CLOSEST((duty) * \
|
|
MLXREG_FAN_MAX_STATE, \
|
|
MLXREG_FAN_MAX_DUTY))
|
|
#define MLXREG_FAN_PWM_STATE2DUTY(stat) (DIV_ROUND_CLOSEST((stat) * \
|
|
MLXREG_FAN_MAX_DUTY, \
|
|
MLXREG_FAN_MAX_STATE))
|
|
|
|
struct mlxreg_fan;
|
|
|
|
/*
|
|
* struct mlxreg_fan_tacho - tachometer data (internal use):
|
|
*
|
|
* @connected: indicates if tachometer is connected;
|
|
* @reg: register offset;
|
|
* @mask: fault mask;
|
|
* @prsnt: present register offset;
|
|
*/
|
|
struct mlxreg_fan_tacho {
|
|
bool connected;
|
|
u32 reg;
|
|
u32 mask;
|
|
u32 prsnt;
|
|
};
|
|
|
|
/*
|
|
* struct mlxreg_fan_pwm - PWM data (internal use):
|
|
*
|
|
* @fan: private data;
|
|
* @connected: indicates if PWM is connected;
|
|
* @reg: register offset;
|
|
* @cooling: cooling device levels;
|
|
* @last_hwmon_state: last cooling state set by hwmon subsystem;
|
|
* @last_thermal_state: last cooling state set by thermal subsystem;
|
|
* @cdev: cooling device;
|
|
*/
|
|
struct mlxreg_fan_pwm {
|
|
struct mlxreg_fan *fan;
|
|
bool connected;
|
|
u32 reg;
|
|
unsigned long last_hwmon_state;
|
|
unsigned long last_thermal_state;
|
|
struct thermal_cooling_device *cdev;
|
|
};
|
|
|
|
/*
|
|
* struct mlxreg_fan - private data (internal use):
|
|
*
|
|
* @dev: basic device;
|
|
* @regmap: register map of parent device;
|
|
* @tacho: tachometer data;
|
|
* @pwm: PWM data;
|
|
* @tachos_per_drwr - number of tachometers per drawer;
|
|
* @samples: minimum allowed samples per pulse;
|
|
* @divider: divider value for tachometer RPM calculation;
|
|
*/
|
|
struct mlxreg_fan {
|
|
struct device *dev;
|
|
void *regmap;
|
|
struct mlxreg_core_platform_data *pdata;
|
|
struct mlxreg_fan_tacho tacho[MLXREG_FAN_MAX_TACHO];
|
|
struct mlxreg_fan_pwm pwm[MLXREG_FAN_MAX_PWM];
|
|
int tachos_per_drwr;
|
|
int samples;
|
|
int divider;
|
|
};
|
|
|
|
static int mlxreg_fan_set_cur_state(struct thermal_cooling_device *cdev,
|
|
unsigned long state);
|
|
|
|
static int
|
|
mlxreg_fan_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
|
|
int channel, long *val)
|
|
{
|
|
struct mlxreg_fan *fan = dev_get_drvdata(dev);
|
|
struct mlxreg_fan_tacho *tacho;
|
|
struct mlxreg_fan_pwm *pwm;
|
|
u32 regval;
|
|
int err;
|
|
|
|
switch (type) {
|
|
case hwmon_fan:
|
|
tacho = &fan->tacho[channel];
|
|
switch (attr) {
|
|
case hwmon_fan_input:
|
|
/*
|
|
* Check FAN presence: FAN related bit in presence register is one,
|
|
* if FAN is physically connected, zero - otherwise.
|
|
*/
|
|
if (tacho->prsnt && fan->tachos_per_drwr) {
|
|
err = regmap_read(fan->regmap, tacho->prsnt, ®val);
|
|
if (err)
|
|
return err;
|
|
|
|
/*
|
|
* Map channel to presence bit - drawer can be equipped with
|
|
* one or few FANs, while presence is indicated per drawer.
|
|
*/
|
|
if (BIT(channel / fan->tachos_per_drwr) & regval) {
|
|
/* FAN is not connected - return zero for FAN speed. */
|
|
*val = 0;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
err = regmap_read(fan->regmap, tacho->reg, ®val);
|
|
if (err)
|
|
return err;
|
|
|
|
*val = MLXREG_FAN_GET_RPM(regval, fan->divider,
|
|
fan->samples);
|
|
break;
|
|
|
|
case hwmon_fan_fault:
|
|
err = regmap_read(fan->regmap, tacho->reg, ®val);
|
|
if (err)
|
|
return err;
|
|
|
|
*val = MLXREG_FAN_GET_FAULT(regval, tacho->mask);
|
|
break;
|
|
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
break;
|
|
|
|
case hwmon_pwm:
|
|
pwm = &fan->pwm[channel];
|
|
switch (attr) {
|
|
case hwmon_pwm_input:
|
|
err = regmap_read(fan->regmap, pwm->reg, ®val);
|
|
if (err)
|
|
return err;
|
|
|
|
*val = regval;
|
|
break;
|
|
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
mlxreg_fan_write(struct device *dev, enum hwmon_sensor_types type, u32 attr,
|
|
int channel, long val)
|
|
{
|
|
struct mlxreg_fan *fan = dev_get_drvdata(dev);
|
|
struct mlxreg_fan_pwm *pwm;
|
|
|
|
switch (type) {
|
|
case hwmon_pwm:
|
|
switch (attr) {
|
|
case hwmon_pwm_input:
|
|
if (val < MLXREG_FAN_MIN_DUTY ||
|
|
val > MLXREG_FAN_MAX_DUTY)
|
|
return -EINVAL;
|
|
pwm = &fan->pwm[channel];
|
|
/* If thermal is configured - handle PWM limit setting. */
|
|
if (IS_REACHABLE(CONFIG_THERMAL)) {
|
|
pwm->last_hwmon_state = MLXREG_FAN_PWM_DUTY2STATE(val);
|
|
/*
|
|
* Update PWM only in case requested state is not less than the
|
|
* last thermal state.
|
|
*/
|
|
if (pwm->last_hwmon_state >= pwm->last_thermal_state)
|
|
return mlxreg_fan_set_cur_state(pwm->cdev,
|
|
pwm->last_hwmon_state);
|
|
return 0;
|
|
}
|
|
return regmap_write(fan->regmap, pwm->reg, val);
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
static umode_t
|
|
mlxreg_fan_is_visible(const void *data, enum hwmon_sensor_types type, u32 attr,
|
|
int channel)
|
|
{
|
|
switch (type) {
|
|
case hwmon_fan:
|
|
if (!(((struct mlxreg_fan *)data)->tacho[channel].connected))
|
|
return 0;
|
|
|
|
switch (attr) {
|
|
case hwmon_fan_input:
|
|
case hwmon_fan_fault:
|
|
return 0444;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case hwmon_pwm:
|
|
if (!(((struct mlxreg_fan *)data)->pwm[channel].connected))
|
|
return 0;
|
|
|
|
switch (attr) {
|
|
case hwmon_pwm_input:
|
|
return 0644;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static char *mlxreg_fan_name[] = {
|
|
"mlxreg_fan",
|
|
"mlxreg_fan1",
|
|
"mlxreg_fan2",
|
|
"mlxreg_fan3",
|
|
};
|
|
|
|
static const struct hwmon_channel_info *mlxreg_fan_hwmon_info[] = {
|
|
HWMON_CHANNEL_INFO(fan,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT,
|
|
HWMON_F_INPUT | HWMON_F_FAULT),
|
|
HWMON_CHANNEL_INFO(pwm,
|
|
HWMON_PWM_INPUT,
|
|
HWMON_PWM_INPUT,
|
|
HWMON_PWM_INPUT,
|
|
HWMON_PWM_INPUT),
|
|
NULL
|
|
};
|
|
|
|
static const struct hwmon_ops mlxreg_fan_hwmon_hwmon_ops = {
|
|
.is_visible = mlxreg_fan_is_visible,
|
|
.read = mlxreg_fan_read,
|
|
.write = mlxreg_fan_write,
|
|
};
|
|
|
|
static const struct hwmon_chip_info mlxreg_fan_hwmon_chip_info = {
|
|
.ops = &mlxreg_fan_hwmon_hwmon_ops,
|
|
.info = mlxreg_fan_hwmon_info,
|
|
};
|
|
|
|
static int mlxreg_fan_get_max_state(struct thermal_cooling_device *cdev,
|
|
unsigned long *state)
|
|
{
|
|
*state = MLXREG_FAN_MAX_STATE;
|
|
return 0;
|
|
}
|
|
|
|
static int mlxreg_fan_get_cur_state(struct thermal_cooling_device *cdev,
|
|
unsigned long *state)
|
|
|
|
{
|
|
struct mlxreg_fan_pwm *pwm = cdev->devdata;
|
|
struct mlxreg_fan *fan = pwm->fan;
|
|
u32 regval;
|
|
int err;
|
|
|
|
err = regmap_read(fan->regmap, pwm->reg, ®val);
|
|
if (err) {
|
|
dev_err(fan->dev, "Failed to query PWM duty\n");
|
|
return err;
|
|
}
|
|
|
|
*state = MLXREG_FAN_PWM_DUTY2STATE(regval);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mlxreg_fan_set_cur_state(struct thermal_cooling_device *cdev,
|
|
unsigned long state)
|
|
|
|
{
|
|
struct mlxreg_fan_pwm *pwm = cdev->devdata;
|
|
struct mlxreg_fan *fan = pwm->fan;
|
|
int err;
|
|
|
|
if (state > MLXREG_FAN_MAX_STATE)
|
|
return -EINVAL;
|
|
|
|
/* Save thermal state. */
|
|
pwm->last_thermal_state = state;
|
|
|
|
state = max_t(unsigned long, state, pwm->last_hwmon_state);
|
|
err = regmap_write(fan->regmap, pwm->reg,
|
|
MLXREG_FAN_PWM_STATE2DUTY(state));
|
|
if (err) {
|
|
dev_err(fan->dev, "Failed to write PWM duty\n");
|
|
return err;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static const struct thermal_cooling_device_ops mlxreg_fan_cooling_ops = {
|
|
.get_max_state = mlxreg_fan_get_max_state,
|
|
.get_cur_state = mlxreg_fan_get_cur_state,
|
|
.set_cur_state = mlxreg_fan_set_cur_state,
|
|
};
|
|
|
|
static int mlxreg_fan_connect_verify(struct mlxreg_fan *fan,
|
|
struct mlxreg_core_data *data)
|
|
{
|
|
u32 regval;
|
|
int err;
|
|
|
|
err = regmap_read(fan->regmap, data->capability, ®val);
|
|
if (err) {
|
|
dev_err(fan->dev, "Failed to query capability register 0x%08x\n",
|
|
data->capability);
|
|
return err;
|
|
}
|
|
|
|
return !!(regval & data->bit);
|
|
}
|
|
|
|
static int mlxreg_pwm_connect_verify(struct mlxreg_fan *fan,
|
|
struct mlxreg_core_data *data)
|
|
{
|
|
u32 regval;
|
|
int err;
|
|
|
|
err = regmap_read(fan->regmap, data->reg, ®val);
|
|
if (err) {
|
|
dev_err(fan->dev, "Failed to query pwm register 0x%08x\n",
|
|
data->reg);
|
|
return err;
|
|
}
|
|
|
|
return regval != MLXREG_FAN_PWM_NOT_CONNECTED;
|
|
}
|
|
|
|
static int mlxreg_fan_speed_divider_get(struct mlxreg_fan *fan,
|
|
struct mlxreg_core_data *data)
|
|
{
|
|
u32 regval;
|
|
int err;
|
|
|
|
err = regmap_read(fan->regmap, data->capability, ®val);
|
|
if (err) {
|
|
dev_err(fan->dev, "Failed to query capability register 0x%08x\n",
|
|
data->capability);
|
|
return err;
|
|
}
|
|
|
|
/*
|
|
* Set divider value according to the capability register, in case it
|
|
* contains valid value. Otherwise use default value. The purpose of
|
|
* this validation is to protect against the old hardware, in which
|
|
* this register can return zero.
|
|
*/
|
|
if (regval > 0 && regval <= MLXREG_FAN_TACHO_DIV_SCALE_MAX)
|
|
fan->divider = regval * MLXREG_FAN_TACHO_DIV_MIN;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mlxreg_fan_config(struct mlxreg_fan *fan,
|
|
struct mlxreg_core_platform_data *pdata)
|
|
{
|
|
int tacho_num = 0, tacho_avail = 0, pwm_num = 0, i;
|
|
struct mlxreg_core_data *data = pdata->data;
|
|
bool configured = false;
|
|
int err;
|
|
|
|
fan->samples = MLXREG_FAN_TACHO_SAMPLES_PER_PULSE_DEF;
|
|
fan->divider = MLXREG_FAN_TACHO_DIV_DEF;
|
|
for (i = 0; i < pdata->counter; i++, data++) {
|
|
if (strnstr(data->label, "tacho", sizeof(data->label))) {
|
|
if (tacho_num == MLXREG_FAN_MAX_TACHO) {
|
|
dev_err(fan->dev, "too many tacho entries: %s\n",
|
|
data->label);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (data->capability) {
|
|
err = mlxreg_fan_connect_verify(fan, data);
|
|
if (err < 0)
|
|
return err;
|
|
else if (!err) {
|
|
tacho_num++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
fan->tacho[tacho_num].reg = data->reg;
|
|
fan->tacho[tacho_num].mask = data->mask;
|
|
fan->tacho[tacho_num].prsnt = data->reg_prsnt;
|
|
fan->tacho[tacho_num++].connected = true;
|
|
tacho_avail++;
|
|
} else if (strnstr(data->label, "pwm", sizeof(data->label))) {
|
|
if (pwm_num == MLXREG_FAN_MAX_TACHO) {
|
|
dev_err(fan->dev, "too many pwm entries: %s\n",
|
|
data->label);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Validate if more then one PWM is connected. */
|
|
if (pwm_num) {
|
|
err = mlxreg_pwm_connect_verify(fan, data);
|
|
if (err < 0)
|
|
return err;
|
|
else if (!err)
|
|
continue;
|
|
}
|
|
|
|
fan->pwm[pwm_num].reg = data->reg;
|
|
fan->pwm[pwm_num].connected = true;
|
|
pwm_num++;
|
|
} else if (strnstr(data->label, "conf", sizeof(data->label))) {
|
|
if (configured) {
|
|
dev_err(fan->dev, "duplicate conf entry: %s\n",
|
|
data->label);
|
|
return -EINVAL;
|
|
}
|
|
/* Validate that conf parameters are not zeros. */
|
|
if (!data->mask && !data->bit && !data->capability) {
|
|
dev_err(fan->dev, "invalid conf entry params: %s\n",
|
|
data->label);
|
|
return -EINVAL;
|
|
}
|
|
if (data->capability) {
|
|
err = mlxreg_fan_speed_divider_get(fan, data);
|
|
if (err)
|
|
return err;
|
|
} else {
|
|
if (data->mask)
|
|
fan->samples = data->mask;
|
|
if (data->bit)
|
|
fan->divider = data->bit;
|
|
}
|
|
configured = true;
|
|
} else {
|
|
dev_err(fan->dev, "invalid label: %s\n", data->label);
|
|
return -EINVAL;
|
|
}
|
|
}
|
|
|
|
if (pdata->capability) {
|
|
int drwr_avail;
|
|
u32 regval;
|
|
|
|
/* Obtain the number of FAN drawers, supported by system. */
|
|
err = regmap_read(fan->regmap, pdata->capability, ®val);
|
|
if (err) {
|
|
dev_err(fan->dev, "Failed to query capability register 0x%08x\n",
|
|
pdata->capability);
|
|
return err;
|
|
}
|
|
|
|
drwr_avail = hweight32(regval);
|
|
if (!tacho_avail || !drwr_avail || tacho_avail < drwr_avail) {
|
|
dev_err(fan->dev, "Configuration is invalid: drawers num %d tachos num %d\n",
|
|
drwr_avail, tacho_avail);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Set the number of tachometers per one drawer. */
|
|
fan->tachos_per_drwr = tacho_avail / drwr_avail;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mlxreg_fan_cooling_config(struct device *dev, struct mlxreg_fan *fan)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < MLXREG_FAN_MAX_PWM; i++) {
|
|
struct mlxreg_fan_pwm *pwm = &fan->pwm[i];
|
|
|
|
if (!pwm->connected)
|
|
continue;
|
|
pwm->fan = fan;
|
|
pwm->cdev = devm_thermal_of_cooling_device_register(dev, NULL, mlxreg_fan_name[i],
|
|
pwm, &mlxreg_fan_cooling_ops);
|
|
if (IS_ERR(pwm->cdev)) {
|
|
dev_err(dev, "Failed to register cooling device\n");
|
|
return PTR_ERR(pwm->cdev);
|
|
}
|
|
|
|
/* Set minimal PWM speed. */
|
|
pwm->last_hwmon_state = MLXREG_FAN_PWM_DUTY2STATE(MLXREG_FAN_MIN_DUTY);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int mlxreg_fan_probe(struct platform_device *pdev)
|
|
{
|
|
struct mlxreg_core_platform_data *pdata;
|
|
struct device *dev = &pdev->dev;
|
|
struct mlxreg_fan *fan;
|
|
struct device *hwm;
|
|
int err;
|
|
|
|
pdata = dev_get_platdata(dev);
|
|
if (!pdata) {
|
|
dev_err(dev, "Failed to get platform data.\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
fan = devm_kzalloc(dev, sizeof(*fan), GFP_KERNEL);
|
|
if (!fan)
|
|
return -ENOMEM;
|
|
|
|
fan->dev = dev;
|
|
fan->regmap = pdata->regmap;
|
|
|
|
err = mlxreg_fan_config(fan, pdata);
|
|
if (err)
|
|
return err;
|
|
|
|
hwm = devm_hwmon_device_register_with_info(dev, "mlxreg_fan",
|
|
fan,
|
|
&mlxreg_fan_hwmon_chip_info,
|
|
NULL);
|
|
if (IS_ERR(hwm)) {
|
|
dev_err(dev, "Failed to register hwmon device\n");
|
|
return PTR_ERR(hwm);
|
|
}
|
|
|
|
if (IS_REACHABLE(CONFIG_THERMAL))
|
|
err = mlxreg_fan_cooling_config(dev, fan);
|
|
|
|
return err;
|
|
}
|
|
|
|
static struct platform_driver mlxreg_fan_driver = {
|
|
.driver = {
|
|
.name = "mlxreg-fan",
|
|
},
|
|
.probe = mlxreg_fan_probe,
|
|
};
|
|
|
|
module_platform_driver(mlxreg_fan_driver);
|
|
|
|
MODULE_AUTHOR("Vadim Pasternak <vadimp@mellanox.com>");
|
|
MODULE_DESCRIPTION("Mellanox FAN driver");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS("platform:mlxreg-fan");
|