mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2024-12-28 16:53:49 +00:00
b9c15c96cc
Add the usb id of the HX1200i Series 2023. Update the documentation accordingly. Also fix the version comments, there are no Series 2022 products. That are legacy or first version products going back many many years. Signed-off-by: Wilken Gottwalt <wilken.gottwalt@posteo.net> Link: https://lore.kernel.org/r/ZlAZs4u0dU7JxtDf@monster.localdomain Signed-off-by: Guenter Roeck <linux@roeck-us.net>
924 lines
24 KiB
C
924 lines
24 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* corsair-psu.c - Linux driver for Corsair power supplies with HID sensors interface
|
|
* Copyright (C) 2020 Wilken Gottwalt <wilken.gottwalt@posteo.net>
|
|
*/
|
|
|
|
#include <linux/completion.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/errno.h>
|
|
#include <linux/hid.h>
|
|
#include <linux/hwmon.h>
|
|
#include <linux/hwmon-sysfs.h>
|
|
#include <linux/jiffies.h>
|
|
#include <linux/kernel.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/types.h>
|
|
|
|
/*
|
|
* Corsair protocol for PSUs
|
|
*
|
|
* message size = 64 bytes (request and response, little endian)
|
|
* request:
|
|
* [length][command][param0][param1][paramX]...
|
|
* reply:
|
|
* [echo of length][echo of command][data0][data1][dataX]...
|
|
*
|
|
* - commands are byte sized opcodes
|
|
* - length is the sum of all bytes of the commands/params
|
|
* - the micro-controller of most of these PSUs support concatenation in the request and reply,
|
|
* but it is better to not rely on this (it is also hard to parse)
|
|
* - the driver uses raw events to be accessible from userspace (though this is not really
|
|
* supported, it is just there for convenience, may be removed in the future)
|
|
* - a reply always starts with the length and command in the same order the request used it
|
|
* - length of the reply data is specific to the command used
|
|
* - some of the commands work on a rail and can be switched to a specific rail (0 = 12v,
|
|
* 1 = 5v, 2 = 3.3v)
|
|
* - the format of the init command 0xFE is swapped length/command bytes
|
|
* - parameter bytes amount and values are specific to the command (rail setting is the only
|
|
* one for now that uses non-zero values)
|
|
* - the driver supports debugfs for values not fitting into the hwmon class
|
|
* - not every device class (HXi or RMi) supports all commands
|
|
* - if configured wrong the PSU resets or shuts down, often before actually hitting the
|
|
* reported critical temperature
|
|
* - new models like HX1500i Series 2023 have changes in the reported vendor and product
|
|
* strings, both are slightly longer now, report vendor and product in one string and are
|
|
* the same now
|
|
*/
|
|
|
|
#define DRIVER_NAME "corsair-psu"
|
|
|
|
#define REPLY_SIZE 24 /* max length of a reply to a single command */
|
|
#define CMD_BUFFER_SIZE 64
|
|
#define CMD_TIMEOUT_MS 250
|
|
#define SECONDS_PER_HOUR (60 * 60)
|
|
#define SECONDS_PER_DAY (SECONDS_PER_HOUR * 24)
|
|
#define RAIL_COUNT 3 /* 3v3 + 5v + 12v */
|
|
#define TEMP_COUNT 2
|
|
#define OCP_MULTI_RAIL 0x02
|
|
|
|
#define PSU_CMD_SELECT_RAIL 0x00 /* expects length 2 */
|
|
#define PSU_CMD_FAN_PWM 0x3B /* the rest of the commands expect length 3 */
|
|
#define PSU_CMD_RAIL_VOLTS_HCRIT 0x40
|
|
#define PSU_CMD_RAIL_VOLTS_LCRIT 0x44
|
|
#define PSU_CMD_RAIL_AMPS_HCRIT 0x46
|
|
#define PSU_CMD_TEMP_HCRIT 0x4F
|
|
#define PSU_CMD_IN_VOLTS 0x88
|
|
#define PSU_CMD_IN_AMPS 0x89
|
|
#define PSU_CMD_RAIL_VOLTS 0x8B
|
|
#define PSU_CMD_RAIL_AMPS 0x8C
|
|
#define PSU_CMD_TEMP0 0x8D
|
|
#define PSU_CMD_TEMP1 0x8E
|
|
#define PSU_CMD_FAN 0x90
|
|
#define PSU_CMD_RAIL_WATTS 0x96
|
|
#define PSU_CMD_VEND_STR 0x99
|
|
#define PSU_CMD_PROD_STR 0x9A
|
|
#define PSU_CMD_TOTAL_UPTIME 0xD1
|
|
#define PSU_CMD_UPTIME 0xD2
|
|
#define PSU_CMD_OCPMODE 0xD8
|
|
#define PSU_CMD_TOTAL_WATTS 0xEE
|
|
#define PSU_CMD_FAN_PWM_ENABLE 0xF0
|
|
#define PSU_CMD_INIT 0xFE
|
|
|
|
#define L_IN_VOLTS "v_in"
|
|
#define L_OUT_VOLTS_12V "v_out +12v"
|
|
#define L_OUT_VOLTS_5V "v_out +5v"
|
|
#define L_OUT_VOLTS_3_3V "v_out +3.3v"
|
|
#define L_IN_AMPS "curr in"
|
|
#define L_AMPS_12V "curr +12v"
|
|
#define L_AMPS_5V "curr +5v"
|
|
#define L_AMPS_3_3V "curr +3.3v"
|
|
#define L_FAN "psu fan"
|
|
#define L_TEMP0 "vrm temp"
|
|
#define L_TEMP1 "case temp"
|
|
#define L_WATTS "power total"
|
|
#define L_WATTS_12V "power +12v"
|
|
#define L_WATTS_5V "power +5v"
|
|
#define L_WATTS_3_3V "power +3.3v"
|
|
|
|
static const char *const label_watts[] = {
|
|
L_WATTS,
|
|
L_WATTS_12V,
|
|
L_WATTS_5V,
|
|
L_WATTS_3_3V
|
|
};
|
|
|
|
static const char *const label_volts[] = {
|
|
L_IN_VOLTS,
|
|
L_OUT_VOLTS_12V,
|
|
L_OUT_VOLTS_5V,
|
|
L_OUT_VOLTS_3_3V
|
|
};
|
|
|
|
static const char *const label_amps[] = {
|
|
L_IN_AMPS,
|
|
L_AMPS_12V,
|
|
L_AMPS_5V,
|
|
L_AMPS_3_3V
|
|
};
|
|
|
|
struct corsairpsu_data {
|
|
struct hid_device *hdev;
|
|
struct device *hwmon_dev;
|
|
struct dentry *debugfs;
|
|
struct completion wait_completion;
|
|
struct mutex lock; /* for locking access to cmd_buffer */
|
|
u8 *cmd_buffer;
|
|
char vendor[REPLY_SIZE];
|
|
char product[REPLY_SIZE];
|
|
long temp_crit[TEMP_COUNT];
|
|
long in_crit[RAIL_COUNT];
|
|
long in_lcrit[RAIL_COUNT];
|
|
long curr_crit[RAIL_COUNT];
|
|
u8 temp_crit_support;
|
|
u8 in_crit_support;
|
|
u8 in_lcrit_support;
|
|
u8 curr_crit_support;
|
|
bool in_curr_cmd_support; /* not all commands are supported on every PSU */
|
|
};
|
|
|
|
/* some values are SMBus LINEAR11 data which need a conversion */
|
|
static int corsairpsu_linear11_to_int(const u16 val, const int scale)
|
|
{
|
|
const int exp = ((s16)val) >> 11;
|
|
const int mant = (((s16)(val & 0x7ff)) << 5) >> 5;
|
|
const int result = mant * scale;
|
|
|
|
return (exp >= 0) ? (result << exp) : (result >> -exp);
|
|
}
|
|
|
|
/* the micro-controller uses percentage values to control pwm */
|
|
static int corsairpsu_dutycycle_to_pwm(const long dutycycle)
|
|
{
|
|
const int result = (256 << 16) / 100;
|
|
|
|
return (result * dutycycle) >> 16;
|
|
}
|
|
|
|
static int corsairpsu_usb_cmd(struct corsairpsu_data *priv, u8 p0, u8 p1, u8 p2, void *data)
|
|
{
|
|
unsigned long time;
|
|
int ret;
|
|
|
|
memset(priv->cmd_buffer, 0, CMD_BUFFER_SIZE);
|
|
priv->cmd_buffer[0] = p0;
|
|
priv->cmd_buffer[1] = p1;
|
|
priv->cmd_buffer[2] = p2;
|
|
|
|
reinit_completion(&priv->wait_completion);
|
|
|
|
ret = hid_hw_output_report(priv->hdev, priv->cmd_buffer, CMD_BUFFER_SIZE);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
time = wait_for_completion_timeout(&priv->wait_completion,
|
|
msecs_to_jiffies(CMD_TIMEOUT_MS));
|
|
if (!time)
|
|
return -ETIMEDOUT;
|
|
|
|
/*
|
|
* at the start of the reply is an echo of the send command/length in the same order it
|
|
* was send, not every command is supported on every device class, if a command is not
|
|
* supported, the length value in the reply is okay, but the command value is set to 0
|
|
*/
|
|
if (p0 != priv->cmd_buffer[0] || p1 != priv->cmd_buffer[1])
|
|
return -EOPNOTSUPP;
|
|
|
|
if (data)
|
|
memcpy(data, priv->cmd_buffer + 2, REPLY_SIZE);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int corsairpsu_init(struct corsairpsu_data *priv)
|
|
{
|
|
/*
|
|
* PSU_CMD_INIT uses swapped length/command and expects 2 parameter bytes, this command
|
|
* actually generates a reply, but we don't need it
|
|
*/
|
|
return corsairpsu_usb_cmd(priv, PSU_CMD_INIT, 3, 0, NULL);
|
|
}
|
|
|
|
static int corsairpsu_fwinfo(struct corsairpsu_data *priv)
|
|
{
|
|
int ret;
|
|
|
|
ret = corsairpsu_usb_cmd(priv, 3, PSU_CMD_VEND_STR, 0, priv->vendor);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
ret = corsairpsu_usb_cmd(priv, 3, PSU_CMD_PROD_STR, 0, priv->product);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int corsairpsu_request(struct corsairpsu_data *priv, u8 cmd, u8 rail, void *data)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock(&priv->lock);
|
|
switch (cmd) {
|
|
case PSU_CMD_RAIL_VOLTS_HCRIT:
|
|
case PSU_CMD_RAIL_VOLTS_LCRIT:
|
|
case PSU_CMD_RAIL_AMPS_HCRIT:
|
|
case PSU_CMD_RAIL_VOLTS:
|
|
case PSU_CMD_RAIL_AMPS:
|
|
case PSU_CMD_RAIL_WATTS:
|
|
ret = corsairpsu_usb_cmd(priv, 2, PSU_CMD_SELECT_RAIL, rail, NULL);
|
|
if (ret < 0)
|
|
goto cmd_fail;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
ret = corsairpsu_usb_cmd(priv, 3, cmd, 0, data);
|
|
|
|
cmd_fail:
|
|
mutex_unlock(&priv->lock);
|
|
return ret;
|
|
}
|
|
|
|
static int corsairpsu_get_value(struct corsairpsu_data *priv, u8 cmd, u8 rail, long *val)
|
|
{
|
|
u8 data[REPLY_SIZE];
|
|
long tmp;
|
|
int ret;
|
|
|
|
ret = corsairpsu_request(priv, cmd, rail, data);
|
|
if (ret < 0)
|
|
return ret;
|
|
|
|
/*
|
|
* the biggest value here comes from the uptime command and to exceed MAXINT total uptime
|
|
* needs to be about 68 years, the rest are u16 values and the biggest value coming out of
|
|
* the LINEAR11 conversion are the watts values which are about 1500 for the strongest psu
|
|
* supported (HX1500i)
|
|
*/
|
|
tmp = ((long)data[3] << 24) + (data[2] << 16) + (data[1] << 8) + data[0];
|
|
switch (cmd) {
|
|
case PSU_CMD_RAIL_VOLTS_HCRIT:
|
|
case PSU_CMD_RAIL_VOLTS_LCRIT:
|
|
case PSU_CMD_RAIL_AMPS_HCRIT:
|
|
case PSU_CMD_TEMP_HCRIT:
|
|
case PSU_CMD_IN_VOLTS:
|
|
case PSU_CMD_IN_AMPS:
|
|
case PSU_CMD_RAIL_VOLTS:
|
|
case PSU_CMD_RAIL_AMPS:
|
|
case PSU_CMD_TEMP0:
|
|
case PSU_CMD_TEMP1:
|
|
*val = corsairpsu_linear11_to_int(tmp & 0xFFFF, 1000);
|
|
break;
|
|
case PSU_CMD_FAN:
|
|
*val = corsairpsu_linear11_to_int(tmp & 0xFFFF, 1);
|
|
break;
|
|
case PSU_CMD_FAN_PWM_ENABLE:
|
|
*val = corsairpsu_linear11_to_int(tmp & 0xFFFF, 1);
|
|
/*
|
|
* 0 = automatic mode, means the micro-controller controls the fan using a plan
|
|
* which can be modified, but changing this plan is not supported by this
|
|
* driver, the matching PWM mode is automatic fan speed control = PWM 2
|
|
* 1 = fixed mode, fan runs at a fixed speed represented by a percentage
|
|
* value 0-100, this matches the PWM manual fan speed control = PWM 1
|
|
* technically there is no PWM no fan speed control mode, it would be a combination
|
|
* of 1 at 100%
|
|
*/
|
|
if (*val == 0)
|
|
*val = 2;
|
|
break;
|
|
case PSU_CMD_FAN_PWM:
|
|
*val = corsairpsu_linear11_to_int(tmp & 0xFFFF, 1);
|
|
*val = corsairpsu_dutycycle_to_pwm(*val);
|
|
break;
|
|
case PSU_CMD_RAIL_WATTS:
|
|
case PSU_CMD_TOTAL_WATTS:
|
|
*val = corsairpsu_linear11_to_int(tmp & 0xFFFF, 1000000);
|
|
break;
|
|
case PSU_CMD_TOTAL_UPTIME:
|
|
case PSU_CMD_UPTIME:
|
|
case PSU_CMD_OCPMODE:
|
|
*val = tmp;
|
|
break;
|
|
default:
|
|
ret = -EOPNOTSUPP;
|
|
break;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void corsairpsu_get_criticals(struct corsairpsu_data *priv)
|
|
{
|
|
long tmp;
|
|
int rail;
|
|
|
|
for (rail = 0; rail < TEMP_COUNT; ++rail) {
|
|
if (!corsairpsu_get_value(priv, PSU_CMD_TEMP_HCRIT, rail, &tmp)) {
|
|
priv->temp_crit_support |= BIT(rail);
|
|
priv->temp_crit[rail] = tmp;
|
|
}
|
|
}
|
|
|
|
for (rail = 0; rail < RAIL_COUNT; ++rail) {
|
|
if (!corsairpsu_get_value(priv, PSU_CMD_RAIL_VOLTS_HCRIT, rail, &tmp)) {
|
|
priv->in_crit_support |= BIT(rail);
|
|
priv->in_crit[rail] = tmp;
|
|
}
|
|
|
|
if (!corsairpsu_get_value(priv, PSU_CMD_RAIL_VOLTS_LCRIT, rail, &tmp)) {
|
|
priv->in_lcrit_support |= BIT(rail);
|
|
priv->in_lcrit[rail] = tmp;
|
|
}
|
|
|
|
if (!corsairpsu_get_value(priv, PSU_CMD_RAIL_AMPS_HCRIT, rail, &tmp)) {
|
|
priv->curr_crit_support |= BIT(rail);
|
|
priv->curr_crit[rail] = tmp;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void corsairpsu_check_cmd_support(struct corsairpsu_data *priv)
|
|
{
|
|
long tmp;
|
|
|
|
priv->in_curr_cmd_support = !corsairpsu_get_value(priv, PSU_CMD_IN_AMPS, 0, &tmp);
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_temp_is_visible(const struct corsairpsu_data *priv, u32 attr,
|
|
int channel)
|
|
{
|
|
umode_t res = 0444;
|
|
|
|
switch (attr) {
|
|
case hwmon_temp_input:
|
|
case hwmon_temp_label:
|
|
case hwmon_temp_crit:
|
|
if (channel > 0 && !(priv->temp_crit_support & BIT(channel - 1)))
|
|
res = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_fan_is_visible(const struct corsairpsu_data *priv, u32 attr,
|
|
int channel)
|
|
{
|
|
switch (attr) {
|
|
case hwmon_fan_input:
|
|
case hwmon_fan_label:
|
|
return 0444;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_pwm_is_visible(const struct corsairpsu_data *priv, u32 attr,
|
|
int channel)
|
|
{
|
|
switch (attr) {
|
|
case hwmon_pwm_input:
|
|
case hwmon_pwm_enable:
|
|
return 0444;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_power_is_visible(const struct corsairpsu_data *priv, u32 attr,
|
|
int channel)
|
|
{
|
|
switch (attr) {
|
|
case hwmon_power_input:
|
|
case hwmon_power_label:
|
|
return 0444;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_in_is_visible(const struct corsairpsu_data *priv, u32 attr,
|
|
int channel)
|
|
{
|
|
umode_t res = 0444;
|
|
|
|
switch (attr) {
|
|
case hwmon_in_input:
|
|
case hwmon_in_label:
|
|
case hwmon_in_crit:
|
|
if (channel > 0 && !(priv->in_crit_support & BIT(channel - 1)))
|
|
res = 0;
|
|
break;
|
|
case hwmon_in_lcrit:
|
|
if (channel > 0 && !(priv->in_lcrit_support & BIT(channel - 1)))
|
|
res = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_curr_is_visible(const struct corsairpsu_data *priv, u32 attr,
|
|
int channel)
|
|
{
|
|
umode_t res = 0444;
|
|
|
|
switch (attr) {
|
|
case hwmon_curr_input:
|
|
if (channel == 0 && !priv->in_curr_cmd_support)
|
|
res = 0;
|
|
break;
|
|
case hwmon_curr_label:
|
|
case hwmon_curr_crit:
|
|
if (channel > 0 && !(priv->curr_crit_support & BIT(channel - 1)))
|
|
res = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static umode_t corsairpsu_hwmon_ops_is_visible(const void *data, enum hwmon_sensor_types type,
|
|
u32 attr, int channel)
|
|
{
|
|
const struct corsairpsu_data *priv = data;
|
|
|
|
switch (type) {
|
|
case hwmon_temp:
|
|
return corsairpsu_hwmon_temp_is_visible(priv, attr, channel);
|
|
case hwmon_fan:
|
|
return corsairpsu_hwmon_fan_is_visible(priv, attr, channel);
|
|
case hwmon_pwm:
|
|
return corsairpsu_hwmon_pwm_is_visible(priv, attr, channel);
|
|
case hwmon_power:
|
|
return corsairpsu_hwmon_power_is_visible(priv, attr, channel);
|
|
case hwmon_in:
|
|
return corsairpsu_hwmon_in_is_visible(priv, attr, channel);
|
|
case hwmon_curr:
|
|
return corsairpsu_hwmon_curr_is_visible(priv, attr, channel);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int corsairpsu_hwmon_temp_read(struct corsairpsu_data *priv, u32 attr, int channel,
|
|
long *val)
|
|
{
|
|
int err = -EOPNOTSUPP;
|
|
|
|
switch (attr) {
|
|
case hwmon_temp_input:
|
|
return corsairpsu_get_value(priv, channel ? PSU_CMD_TEMP1 : PSU_CMD_TEMP0,
|
|
channel, val);
|
|
case hwmon_temp_crit:
|
|
*val = priv->temp_crit[channel];
|
|
err = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int corsairpsu_hwmon_pwm_read(struct corsairpsu_data *priv, u32 attr, int channel, long *val)
|
|
{
|
|
switch (attr) {
|
|
case hwmon_pwm_input:
|
|
return corsairpsu_get_value(priv, PSU_CMD_FAN_PWM, 0, val);
|
|
case hwmon_pwm_enable:
|
|
return corsairpsu_get_value(priv, PSU_CMD_FAN_PWM_ENABLE, 0, val);
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
static int corsairpsu_hwmon_power_read(struct corsairpsu_data *priv, u32 attr, int channel,
|
|
long *val)
|
|
{
|
|
if (attr == hwmon_power_input) {
|
|
switch (channel) {
|
|
case 0:
|
|
return corsairpsu_get_value(priv, PSU_CMD_TOTAL_WATTS, 0, val);
|
|
case 1 ... 3:
|
|
return corsairpsu_get_value(priv, PSU_CMD_RAIL_WATTS, channel - 1, val);
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
static int corsairpsu_hwmon_in_read(struct corsairpsu_data *priv, u32 attr, int channel, long *val)
|
|
{
|
|
int err = -EOPNOTSUPP;
|
|
|
|
switch (attr) {
|
|
case hwmon_in_input:
|
|
switch (channel) {
|
|
case 0:
|
|
return corsairpsu_get_value(priv, PSU_CMD_IN_VOLTS, 0, val);
|
|
case 1 ... 3:
|
|
return corsairpsu_get_value(priv, PSU_CMD_RAIL_VOLTS, channel - 1, val);
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case hwmon_in_crit:
|
|
*val = priv->in_crit[channel - 1];
|
|
err = 0;
|
|
break;
|
|
case hwmon_in_lcrit:
|
|
*val = priv->in_lcrit[channel - 1];
|
|
err = 0;
|
|
break;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int corsairpsu_hwmon_curr_read(struct corsairpsu_data *priv, u32 attr, int channel,
|
|
long *val)
|
|
{
|
|
int err = -EOPNOTSUPP;
|
|
|
|
switch (attr) {
|
|
case hwmon_curr_input:
|
|
switch (channel) {
|
|
case 0:
|
|
return corsairpsu_get_value(priv, PSU_CMD_IN_AMPS, 0, val);
|
|
case 1 ... 3:
|
|
return corsairpsu_get_value(priv, PSU_CMD_RAIL_AMPS, channel - 1, val);
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
case hwmon_curr_crit:
|
|
*val = priv->curr_crit[channel - 1];
|
|
err = 0;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int corsairpsu_hwmon_ops_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
|
|
int channel, long *val)
|
|
{
|
|
struct corsairpsu_data *priv = dev_get_drvdata(dev);
|
|
|
|
switch (type) {
|
|
case hwmon_temp:
|
|
return corsairpsu_hwmon_temp_read(priv, attr, channel, val);
|
|
case hwmon_fan:
|
|
if (attr == hwmon_fan_input)
|
|
return corsairpsu_get_value(priv, PSU_CMD_FAN, 0, val);
|
|
return -EOPNOTSUPP;
|
|
case hwmon_pwm:
|
|
return corsairpsu_hwmon_pwm_read(priv, attr, channel, val);
|
|
case hwmon_power:
|
|
return corsairpsu_hwmon_power_read(priv, attr, channel, val);
|
|
case hwmon_in:
|
|
return corsairpsu_hwmon_in_read(priv, attr, channel, val);
|
|
case hwmon_curr:
|
|
return corsairpsu_hwmon_curr_read(priv, attr, channel, val);
|
|
default:
|
|
return -EOPNOTSUPP;
|
|
}
|
|
}
|
|
|
|
static int corsairpsu_hwmon_ops_read_string(struct device *dev, enum hwmon_sensor_types type,
|
|
u32 attr, int channel, const char **str)
|
|
{
|
|
if (type == hwmon_temp && attr == hwmon_temp_label) {
|
|
*str = channel ? L_TEMP1 : L_TEMP0;
|
|
return 0;
|
|
} else if (type == hwmon_fan && attr == hwmon_fan_label) {
|
|
*str = L_FAN;
|
|
return 0;
|
|
} else if (type == hwmon_power && attr == hwmon_power_label && channel < 4) {
|
|
*str = label_watts[channel];
|
|
return 0;
|
|
} else if (type == hwmon_in && attr == hwmon_in_label && channel < 4) {
|
|
*str = label_volts[channel];
|
|
return 0;
|
|
} else if (type == hwmon_curr && attr == hwmon_curr_label && channel < 4) {
|
|
*str = label_amps[channel];
|
|
return 0;
|
|
}
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
static const struct hwmon_ops corsairpsu_hwmon_ops = {
|
|
.is_visible = corsairpsu_hwmon_ops_is_visible,
|
|
.read = corsairpsu_hwmon_ops_read,
|
|
.read_string = corsairpsu_hwmon_ops_read_string,
|
|
};
|
|
|
|
static const struct hwmon_channel_info *const corsairpsu_info[] = {
|
|
HWMON_CHANNEL_INFO(chip,
|
|
HWMON_C_REGISTER_TZ),
|
|
HWMON_CHANNEL_INFO(temp,
|
|
HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_CRIT,
|
|
HWMON_T_INPUT | HWMON_T_LABEL | HWMON_T_CRIT),
|
|
HWMON_CHANNEL_INFO(fan,
|
|
HWMON_F_INPUT | HWMON_F_LABEL),
|
|
HWMON_CHANNEL_INFO(pwm,
|
|
HWMON_PWM_INPUT | HWMON_PWM_ENABLE),
|
|
HWMON_CHANNEL_INFO(power,
|
|
HWMON_P_INPUT | HWMON_P_LABEL,
|
|
HWMON_P_INPUT | HWMON_P_LABEL,
|
|
HWMON_P_INPUT | HWMON_P_LABEL,
|
|
HWMON_P_INPUT | HWMON_P_LABEL),
|
|
HWMON_CHANNEL_INFO(in,
|
|
HWMON_I_INPUT | HWMON_I_LABEL,
|
|
HWMON_I_INPUT | HWMON_I_LABEL | HWMON_I_LCRIT | HWMON_I_CRIT,
|
|
HWMON_I_INPUT | HWMON_I_LABEL | HWMON_I_LCRIT | HWMON_I_CRIT,
|
|
HWMON_I_INPUT | HWMON_I_LABEL | HWMON_I_LCRIT | HWMON_I_CRIT),
|
|
HWMON_CHANNEL_INFO(curr,
|
|
HWMON_C_INPUT | HWMON_C_LABEL,
|
|
HWMON_C_INPUT | HWMON_C_LABEL | HWMON_C_CRIT,
|
|
HWMON_C_INPUT | HWMON_C_LABEL | HWMON_C_CRIT,
|
|
HWMON_C_INPUT | HWMON_C_LABEL | HWMON_C_CRIT),
|
|
NULL
|
|
};
|
|
|
|
static const struct hwmon_chip_info corsairpsu_chip_info = {
|
|
.ops = &corsairpsu_hwmon_ops,
|
|
.info = corsairpsu_info,
|
|
};
|
|
|
|
#ifdef CONFIG_DEBUG_FS
|
|
|
|
static void print_uptime(struct seq_file *seqf, u8 cmd)
|
|
{
|
|
struct corsairpsu_data *priv = seqf->private;
|
|
long val;
|
|
int ret;
|
|
|
|
ret = corsairpsu_get_value(priv, cmd, 0, &val);
|
|
if (ret < 0) {
|
|
seq_puts(seqf, "N/A\n");
|
|
return;
|
|
}
|
|
|
|
if (val > SECONDS_PER_DAY) {
|
|
seq_printf(seqf, "%ld day(s), %02ld:%02ld:%02ld\n", val / SECONDS_PER_DAY,
|
|
val % SECONDS_PER_DAY / SECONDS_PER_HOUR, val % SECONDS_PER_HOUR / 60,
|
|
val % 60);
|
|
return;
|
|
}
|
|
|
|
seq_printf(seqf, "%02ld:%02ld:%02ld\n", val % SECONDS_PER_DAY / SECONDS_PER_HOUR,
|
|
val % SECONDS_PER_HOUR / 60, val % 60);
|
|
}
|
|
|
|
static int uptime_show(struct seq_file *seqf, void *unused)
|
|
{
|
|
print_uptime(seqf, PSU_CMD_UPTIME);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SHOW_ATTRIBUTE(uptime);
|
|
|
|
static int uptime_total_show(struct seq_file *seqf, void *unused)
|
|
{
|
|
print_uptime(seqf, PSU_CMD_TOTAL_UPTIME);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SHOW_ATTRIBUTE(uptime_total);
|
|
|
|
static int vendor_show(struct seq_file *seqf, void *unused)
|
|
{
|
|
struct corsairpsu_data *priv = seqf->private;
|
|
|
|
seq_printf(seqf, "%s\n", priv->vendor);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SHOW_ATTRIBUTE(vendor);
|
|
|
|
static int product_show(struct seq_file *seqf, void *unused)
|
|
{
|
|
struct corsairpsu_data *priv = seqf->private;
|
|
|
|
seq_printf(seqf, "%s\n", priv->product);
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SHOW_ATTRIBUTE(product);
|
|
|
|
static int ocpmode_show(struct seq_file *seqf, void *unused)
|
|
{
|
|
struct corsairpsu_data *priv = seqf->private;
|
|
long val;
|
|
int ret;
|
|
|
|
/*
|
|
* The rail mode is switchable on the fly. The RAW interface can be used for this. But it
|
|
* will not be included here, because I consider it somewhat dangerous for the health of the
|
|
* PSU. The returned value can be a bogus one, if the PSU is in the process of switching and
|
|
* getting of the value itself can also fail during this. Because of this every other value
|
|
* than OCP_MULTI_RAIL can be considered as "single rail".
|
|
*/
|
|
ret = corsairpsu_get_value(priv, PSU_CMD_OCPMODE, 0, &val);
|
|
if (ret < 0)
|
|
seq_puts(seqf, "N/A\n");
|
|
else
|
|
seq_printf(seqf, "%s\n", (val == OCP_MULTI_RAIL) ? "multi rail" : "single rail");
|
|
|
|
return 0;
|
|
}
|
|
DEFINE_SHOW_ATTRIBUTE(ocpmode);
|
|
|
|
static void corsairpsu_debugfs_init(struct corsairpsu_data *priv)
|
|
{
|
|
char name[32];
|
|
|
|
scnprintf(name, sizeof(name), "%s-%s", DRIVER_NAME, dev_name(&priv->hdev->dev));
|
|
|
|
priv->debugfs = debugfs_create_dir(name, NULL);
|
|
debugfs_create_file("uptime", 0444, priv->debugfs, priv, &uptime_fops);
|
|
debugfs_create_file("uptime_total", 0444, priv->debugfs, priv, &uptime_total_fops);
|
|
debugfs_create_file("vendor", 0444, priv->debugfs, priv, &vendor_fops);
|
|
debugfs_create_file("product", 0444, priv->debugfs, priv, &product_fops);
|
|
debugfs_create_file("ocpmode", 0444, priv->debugfs, priv, &ocpmode_fops);
|
|
}
|
|
|
|
#else
|
|
|
|
static void corsairpsu_debugfs_init(struct corsairpsu_data *priv)
|
|
{
|
|
}
|
|
|
|
#endif
|
|
|
|
static int corsairpsu_probe(struct hid_device *hdev, const struct hid_device_id *id)
|
|
{
|
|
struct corsairpsu_data *priv;
|
|
int ret;
|
|
|
|
priv = devm_kzalloc(&hdev->dev, sizeof(struct corsairpsu_data), GFP_KERNEL);
|
|
if (!priv)
|
|
return -ENOMEM;
|
|
|
|
priv->cmd_buffer = devm_kmalloc(&hdev->dev, CMD_BUFFER_SIZE, GFP_KERNEL);
|
|
if (!priv->cmd_buffer)
|
|
return -ENOMEM;
|
|
|
|
ret = hid_parse(hdev);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = hid_hw_start(hdev, HID_CONNECT_HIDRAW);
|
|
if (ret)
|
|
return ret;
|
|
|
|
ret = hid_hw_open(hdev);
|
|
if (ret)
|
|
goto fail_and_stop;
|
|
|
|
priv->hdev = hdev;
|
|
hid_set_drvdata(hdev, priv);
|
|
mutex_init(&priv->lock);
|
|
init_completion(&priv->wait_completion);
|
|
|
|
hid_device_io_start(hdev);
|
|
|
|
ret = corsairpsu_init(priv);
|
|
if (ret < 0) {
|
|
dev_err(&hdev->dev, "unable to initialize device (%d)\n", ret);
|
|
goto fail_and_stop;
|
|
}
|
|
|
|
ret = corsairpsu_fwinfo(priv);
|
|
if (ret < 0) {
|
|
dev_err(&hdev->dev, "unable to query firmware (%d)\n", ret);
|
|
goto fail_and_stop;
|
|
}
|
|
|
|
corsairpsu_get_criticals(priv);
|
|
corsairpsu_check_cmd_support(priv);
|
|
|
|
priv->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "corsairpsu", priv,
|
|
&corsairpsu_chip_info, NULL);
|
|
|
|
if (IS_ERR(priv->hwmon_dev)) {
|
|
ret = PTR_ERR(priv->hwmon_dev);
|
|
goto fail_and_close;
|
|
}
|
|
|
|
corsairpsu_debugfs_init(priv);
|
|
|
|
return 0;
|
|
|
|
fail_and_close:
|
|
hid_hw_close(hdev);
|
|
fail_and_stop:
|
|
hid_hw_stop(hdev);
|
|
return ret;
|
|
}
|
|
|
|
static void corsairpsu_remove(struct hid_device *hdev)
|
|
{
|
|
struct corsairpsu_data *priv = hid_get_drvdata(hdev);
|
|
|
|
debugfs_remove_recursive(priv->debugfs);
|
|
hwmon_device_unregister(priv->hwmon_dev);
|
|
hid_hw_close(hdev);
|
|
hid_hw_stop(hdev);
|
|
}
|
|
|
|
static int corsairpsu_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data,
|
|
int size)
|
|
{
|
|
struct corsairpsu_data *priv = hid_get_drvdata(hdev);
|
|
|
|
if (completion_done(&priv->wait_completion))
|
|
return 0;
|
|
|
|
memcpy(priv->cmd_buffer, data, min(CMD_BUFFER_SIZE, size));
|
|
complete(&priv->wait_completion);
|
|
|
|
return 0;
|
|
}
|
|
|
|
#ifdef CONFIG_PM
|
|
static int corsairpsu_resume(struct hid_device *hdev)
|
|
{
|
|
struct corsairpsu_data *priv = hid_get_drvdata(hdev);
|
|
|
|
/* some PSUs turn off the microcontroller during standby, so a reinit is required */
|
|
return corsairpsu_init(priv);
|
|
}
|
|
#endif
|
|
|
|
static const struct hid_device_id corsairpsu_idtable[] = {
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c03) }, /* Corsair HX550i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c04) }, /* Corsair HX650i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c05) }, /* Corsair HX750i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c06) }, /* Corsair HX850i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c07) }, /* Corsair HX1000i Legacy */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c08) }, /* Corsair HX1200i Legacy */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c09) }, /* Corsair RM550i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c0a) }, /* Corsair RM650i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c0b) }, /* Corsair RM750i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c0c) }, /* Corsair RM850i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c0d) }, /* Corsair RM1000i */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c1e) }, /* Corsair HX1000i Series 2023 */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c1f) }, /* Corsair HX1500i Legacy and Series 2023 */
|
|
{ HID_USB_DEVICE(0x1b1c, 0x1c23) }, /* Corsair HX1200i Series 2023 */
|
|
{ },
|
|
};
|
|
MODULE_DEVICE_TABLE(hid, corsairpsu_idtable);
|
|
|
|
static struct hid_driver corsairpsu_driver = {
|
|
.name = DRIVER_NAME,
|
|
.id_table = corsairpsu_idtable,
|
|
.probe = corsairpsu_probe,
|
|
.remove = corsairpsu_remove,
|
|
.raw_event = corsairpsu_raw_event,
|
|
#ifdef CONFIG_PM
|
|
.resume = corsairpsu_resume,
|
|
.reset_resume = corsairpsu_resume,
|
|
#endif
|
|
};
|
|
|
|
static int __init corsair_init(void)
|
|
{
|
|
return hid_register_driver(&corsairpsu_driver);
|
|
}
|
|
|
|
static void __exit corsair_exit(void)
|
|
{
|
|
hid_unregister_driver(&corsairpsu_driver);
|
|
}
|
|
|
|
/*
|
|
* With module_init() the driver would load before the HID bus when
|
|
* built-in, so use late_initcall() instead.
|
|
*/
|
|
late_initcall(corsair_init);
|
|
module_exit(corsair_exit);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_AUTHOR("Wilken Gottwalt <wilken.gottwalt@posteo.net>");
|
|
MODULE_DESCRIPTION("Linux driver for Corsair power supplies with HID sensors interface");
|