hwmon: add Corsair Commander Pro driver

This is v7 of a driver for the Corsair Commander Pro.
It provides sysfs attributes for:
- Reading fan speed
- Reading temp sensors
- Reading voltage values
- Writing pwm and reading last written pwm
- Reading fan and temp connection status

It is an usb driver, so it needs to be ignored by usbhid.
The Corsair Commander Pro is a fan controller and provides
no means for user interaction.
The two device numbers are there, because there is a slightly
different version of the same device. (Only difference
seem to be in some presets.)

Squashed:
 hwmon: (corsair-cpro) add fan_target

 This adds fan_target entries to the corsair-cpro driver.
 Reading the attribute from the device does not seem possible, so
 it returns the last set value (same as pwm).

 send_usb_cmd now has one more argument, which is needed for the
 fan_target command.

 hwmon: corsair-cpro: Change to HID driver

 This changes corsair-cpro to a hid driver using hid reports.

Signed-off-by: Marius Zachmann <mail@mariuszachmann.de>
Reviewed-by: Guenter Roeck <linux@roeck-us.net>
Link: https://lore.kernel.org/r/20200626055936.4441-1-mail@mariuszachmann.de
Link: https://lore.kernel.org/r/20200709141413.30790-1-mail@mariuszachmann.de
[groeck: Squashed follow-up patches to avoid changes in HID code]
Signed-off-by: Guenter Roeck <linux@roeck-us.net>
This commit is contained in:
Marius Zachmann 2020-06-26 07:59:36 +02:00 committed by Guenter Roeck
parent a686024e18
commit 40c3a44542
6 changed files with 618 additions and 0 deletions

View File

@ -0,0 +1,42 @@
.. SPDX-License-Identifier: GPL-2.0-or-later
Kernel driver corsair-cpro
==========================
Supported devices:
* Corsair Commander Pro
* Corsair Commander Pro (1000D)
Author: Marius Zachmann
Description
-----------
This driver implements the sysfs interface for the Corsair Commander Pro.
The Corsair Commander Pro is a USB device with 6 fan connectors,
4 temperature sensor connectors and 2 Corsair LED connectors.
It can read the voltage levels on the SATA power connector.
Usage Notes
-----------
Since it is a USB device, hotswapping is possible. The device is autodetected.
Sysfs entries
-------------
======================= =====================================================================
in0_input Voltage on SATA 12v
in1_input Voltage on SATA 5v
in2_input Voltage on SATA 3.3v
temp[1-4]_input Temperature on connected temperature sensors
fan[1-6]_input Connected fan rpm.
fan[1-6]_label Shows fan type as detected by the device.
fan[1-6]_target Sets fan speed target rpm.
When reading, it reports the last value if it was set by the driver.
Otherwise returns 0.
pwm[1-6] Sets the fan speed. Values from 0-255.
When reading, it reports the last value if it was set by the driver.
Otherwise returns 0.
======================= =====================================================================

View File

@ -47,6 +47,7 @@ Hardware Monitoring Kernel Drivers
bel-pfe
bt1-pvt
coretemp
corsair-cpro
da9052
da9055
dell-smm-hwmon

View File

@ -4401,6 +4401,12 @@ S: Maintained
F: Documentation/hwmon/coretemp.rst
F: drivers/hwmon/coretemp.c
CORSAIR-CPRO HARDWARE MONITOR DRIVER
M: Marius <mail@mariuszachmann.de>
L: linux-hwmon@vger.kernel.org
S: Maintained
F: drivers/hwmon/corsair-cpro.c
COSA/SRP SYNC SERIAL DRIVER
M: Jan "Yenya" Kasprzak <kas@fi.muni.cz>
S: Maintained

View File

@ -439,6 +439,16 @@ config SENSORS_BT1_PVT_ALARMS
the data conversion will be periodically performed and the data will be
saved in the internal driver cache.
config SENSORS_CORSAIR_CPRO
tristate "Corsair Commander Pro controller"
depends on HID
help
If you say yes here you get support for the Corsair Commander Pro
controller.
This driver can also be built as a module. If so, the module
will be called corsair-cpro.
config SENSORS_DRIVETEMP
tristate "Hard disk drives with temperature sensors"
depends on SCSI && ATA

View File

@ -56,6 +56,7 @@ obj-$(CONFIG_SENSORS_ATXP1) += atxp1.o
obj-$(CONFIG_SENSORS_AXI_FAN_CONTROL) += axi-fan-control.o
obj-$(CONFIG_SENSORS_BT1_PVT) += bt1-pvt.o
obj-$(CONFIG_SENSORS_CORETEMP) += coretemp.o
obj-$(CONFIG_SENSORS_CORSAIR_CPRO) += corsair-cpro.o
obj-$(CONFIG_SENSORS_DA9052_ADC)+= da9052-hwmon.o
obj-$(CONFIG_SENSORS_DA9055)+= da9055-hwmon.o
obj-$(CONFIG_SENSORS_DELL_SMM) += dell-smm-hwmon.o

View File

@ -0,0 +1,558 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* corsair-cpro.c - Linux driver for Corsair Commander Pro
* Copyright (C) 2020 Marius Zachmann <mail@mariuszachmann.de>
*
* This driver uses hid reports to communicate with the device to allow hidraw userspace drivers
* still being used. The device does not use report ids. When using hidraw and this driver
* simultaniously, reports could be switched.
*/
#include <linux/bitops.h>
#include <linux/completion.h>
#include <linux/hid.h>
#include <linux/hwmon.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/types.h>
#define USB_VENDOR_ID_CORSAIR 0x1b1c
#define USB_PRODUCT_ID_CORSAIR_COMMANDERPRO 0x0c10
#define USB_PRODUCT_ID_CORSAIR_1000D 0x1d00
#define OUT_BUFFER_SIZE 63
#define IN_BUFFER_SIZE 16
#define LABEL_LENGTH 11
#define REQ_TIMEOUT 300
#define CTL_GET_TMP_CNCT 0x10 /*
* returns in bytes 1-4 for each temp sensor:
* 0 not connected
* 1 connected
*/
#define CTL_GET_TMP 0x11 /*
* send: byte 1 is channel, rest zero
* rcv: returns temp for channel in centi-degree celsius
* in bytes 1 and 2
* returns 17 in byte 0 if no sensor is connected
*/
#define CTL_GET_VOLT 0x12 /*
* send: byte 1 is rail number: 0 = 12v, 1 = 5v, 2 = 3.3v
* rcv: returns millivolt in bytes 1,2
*/
#define CTL_GET_FAN_CNCT 0x20 /*
* returns in bytes 1-6 for each fan:
* 0 not connected
* 1 3pin
* 2 4pin
*/
#define CTL_GET_FAN_RPM 0x21 /*
* send: byte 1 is channel, rest zero
* rcv: returns rpm in bytes 1,2
*/
#define CTL_SET_FAN_FPWM 0x23 /*
* set fixed pwm
* send: byte 1 is fan number
* send: byte 2 is percentage from 0 - 100
*/
#define CTL_SET_FAN_TARGET 0x24 /*
* set target rpm
* send: byte 1 is fan number
* send: byte 2-3 is target
* device accepts all values from 0x00 - 0xFFFF
*/
#define NUM_FANS 6
#define NUM_TEMP_SENSORS 4
struct ccp_device {
struct hid_device *hdev;
struct device *hwmon_dev;
struct completion wait_input_report;
struct mutex mutex; /* whenever buffer is used, lock before send_usb_cmd */
u8 *buffer;
int pwm[6];
int target[6];
DECLARE_BITMAP(temp_cnct, NUM_TEMP_SENSORS);
DECLARE_BITMAP(fan_cnct, NUM_FANS);
char fan_label[6][LABEL_LENGTH];
};
/* send command, check for error in response, response in ccp->buffer */
static int send_usb_cmd(struct ccp_device *ccp, u8 command, u8 byte1, u8 byte2, u8 byte3)
{
unsigned long t;
int ret;
memset(ccp->buffer, 0x00, OUT_BUFFER_SIZE);
ccp->buffer[0] = command;
ccp->buffer[1] = byte1;
ccp->buffer[2] = byte2;
ccp->buffer[3] = byte3;
reinit_completion(&ccp->wait_input_report);
ret = hid_hw_output_report(ccp->hdev, ccp->buffer, OUT_BUFFER_SIZE);
if (ret < 0)
return ret;
t = wait_for_completion_timeout(&ccp->wait_input_report, msecs_to_jiffies(REQ_TIMEOUT));
if (!t)
return -ETIMEDOUT;
/* first byte of response is error code */
if (ccp->buffer[0] != 0x00) {
hid_dbg(ccp->hdev, "device response error: %d", ccp->buffer[0]);
return -EIO;
}
return 0;
}
static int ccp_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *data, int size)
{
struct ccp_device *ccp = hid_get_drvdata(hdev);
/* only copy buffer when requested */
if (completion_done(&ccp->wait_input_report))
return 0;
memcpy(ccp->buffer, data, min(IN_BUFFER_SIZE, size));
complete(&ccp->wait_input_report);
return 0;
}
/* requests and returns single data values depending on channel */
static int get_data(struct ccp_device *ccp, int command, int channel)
{
int ret;
mutex_lock(&ccp->mutex);
ret = send_usb_cmd(ccp, command, channel, 0, 0);
if (ret)
goto out_unlock;
ret = (ccp->buffer[1] << 8) + ccp->buffer[2];
out_unlock:
mutex_unlock(&ccp->mutex);
return ret;
}
static int set_pwm(struct ccp_device *ccp, int channel, long val)
{
int ret;
if (val < 0 || val > 255)
return -EINVAL;
ccp->pwm[channel] = val;
/* The Corsair Commander Pro uses values from 0-100 */
val = DIV_ROUND_CLOSEST(val * 100, 255);
mutex_lock(&ccp->mutex);
ret = send_usb_cmd(ccp, CTL_SET_FAN_FPWM, channel, val, 0);
mutex_unlock(&ccp->mutex);
return ret;
}
static int set_target(struct ccp_device *ccp, int channel, long val)
{
int ret;
val = clamp_val(val, 0, 0xFFFF);
ccp->target[channel] = val;
mutex_lock(&ccp->mutex);
ret = send_usb_cmd(ccp, CTL_SET_FAN_TARGET, channel, val >> 8, val);
mutex_unlock(&ccp->mutex);
return ret;
}
static int ccp_read_string(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, const char **str)
{
struct ccp_device *ccp = dev_get_drvdata(dev);
switch (type) {
case hwmon_fan:
switch (attr) {
case hwmon_fan_label:
*str = ccp->fan_label[channel];
return 0;
default:
break;
}
break;
default:
break;
}
return -EOPNOTSUPP;
}
static int ccp_read(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long *val)
{
struct ccp_device *ccp = dev_get_drvdata(dev);
int ret;
switch (type) {
case hwmon_temp:
switch (attr) {
case hwmon_temp_input:
ret = get_data(ccp, CTL_GET_TMP, channel);
if (ret < 0)
return ret;
*val = ret * 10;
return 0;
default:
break;
}
break;
case hwmon_fan:
switch (attr) {
case hwmon_fan_input:
ret = get_data(ccp, CTL_GET_FAN_RPM, channel);
if (ret < 0)
return ret;
*val = ret;
return 0;
case hwmon_fan_target:
/* how to read target values from the device is unknown */
/* driver returns last set value or 0 */
*val = ccp->target[channel];
return 0;
default:
break;
}
break;
case hwmon_pwm:
switch (attr) {
case hwmon_pwm_input:
/* how to read pwm values from the device is currently unknown */
/* driver returns last set value or 0 */
*val = ccp->pwm[channel];
return 0;
default:
break;
}
break;
case hwmon_in:
switch (attr) {
case hwmon_in_input:
ret = get_data(ccp, CTL_GET_VOLT, channel);
if (ret < 0)
return ret;
*val = ret;
return 0;
default:
break;
}
break;
default:
break;
}
return -EOPNOTSUPP;
};
static int ccp_write(struct device *dev, enum hwmon_sensor_types type,
u32 attr, int channel, long val)
{
struct ccp_device *ccp = dev_get_drvdata(dev);
switch (type) {
case hwmon_pwm:
switch (attr) {
case hwmon_pwm_input:
return set_pwm(ccp, channel, val);
default:
break;
}
break;
case hwmon_fan:
switch (attr) {
case hwmon_fan_target:
return set_target(ccp, channel, val);
default:
break;
}
default:
break;
}
return -EOPNOTSUPP;
};
static umode_t ccp_is_visible(const void *data, enum hwmon_sensor_types type,
u32 attr, int channel)
{
const struct ccp_device *ccp = data;
switch (type) {
case hwmon_temp:
if (!test_bit(channel, ccp->temp_cnct))
break;
switch (attr) {
case hwmon_temp_input:
return 0444;
case hwmon_temp_label:
return 0444;
default:
break;
}
break;
case hwmon_fan:
if (!test_bit(channel, ccp->fan_cnct))
break;
switch (attr) {
case hwmon_fan_input:
return 0444;
case hwmon_fan_label:
return 0444;
case hwmon_fan_target:
return 0644;
default:
break;
}
break;
case hwmon_pwm:
if (!test_bit(channel, ccp->fan_cnct))
break;
switch (attr) {
case hwmon_pwm_input:
return 0644;
default:
break;
}
break;
case hwmon_in:
switch (attr) {
case hwmon_in_input:
return 0444;
default:
break;
}
break;
default:
break;
}
return 0;
};
static const struct hwmon_ops ccp_hwmon_ops = {
.is_visible = ccp_is_visible,
.read = ccp_read,
.read_string = ccp_read_string,
.write = ccp_write,
};
static const struct hwmon_channel_info *ccp_info[] = {
HWMON_CHANNEL_INFO(chip,
HWMON_C_REGISTER_TZ),
HWMON_CHANNEL_INFO(temp,
HWMON_T_INPUT,
HWMON_T_INPUT,
HWMON_T_INPUT,
HWMON_T_INPUT
),
HWMON_CHANNEL_INFO(fan,
HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_TARGET,
HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_TARGET,
HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_TARGET,
HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_TARGET,
HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_TARGET,
HWMON_F_INPUT | HWMON_F_LABEL | HWMON_F_TARGET
),
HWMON_CHANNEL_INFO(pwm,
HWMON_PWM_INPUT,
HWMON_PWM_INPUT,
HWMON_PWM_INPUT,
HWMON_PWM_INPUT,
HWMON_PWM_INPUT,
HWMON_PWM_INPUT
),
HWMON_CHANNEL_INFO(in,
HWMON_I_INPUT,
HWMON_I_INPUT,
HWMON_I_INPUT
),
NULL
};
static const struct hwmon_chip_info ccp_chip_info = {
.ops = &ccp_hwmon_ops,
.info = ccp_info,
};
/* read fan connection status and set labels */
static int get_fan_cnct(struct ccp_device *ccp)
{
int channel;
int mode;
int ret;
ret = send_usb_cmd(ccp, CTL_GET_FAN_CNCT, 0, 0, 0);
if (ret)
return ret;
for (channel = 0; channel < NUM_FANS; channel++) {
mode = ccp->buffer[channel + 1];
if (mode == 0)
continue;
set_bit(channel, ccp->fan_cnct);
switch (mode) {
case 1:
scnprintf(ccp->fan_label[channel], LABEL_LENGTH,
"fan%d 3pin", channel + 1);
break;
case 2:
scnprintf(ccp->fan_label[channel], LABEL_LENGTH,
"fan%d 4pin", channel + 1);
break;
default:
scnprintf(ccp->fan_label[channel], LABEL_LENGTH,
"fan%d other", channel + 1);
break;
}
}
return 0;
}
/* read temp sensor connection status */
static int get_temp_cnct(struct ccp_device *ccp)
{
int channel;
int mode;
int ret;
ret = send_usb_cmd(ccp, CTL_GET_TMP_CNCT, 0, 0, 0);
if (ret)
return ret;
for (channel = 0; channel < NUM_TEMP_SENSORS; channel++) {
mode = ccp->buffer[channel + 1];
if (mode == 0)
continue;
set_bit(channel, ccp->temp_cnct);
}
return 0;
}
static int ccp_probe(struct hid_device *hdev, const struct hid_device_id *id)
{
struct ccp_device *ccp;
int ret;
ccp = devm_kzalloc(&hdev->dev, sizeof(*ccp), GFP_KERNEL);
if (!ccp)
return -ENOMEM;
ccp->buffer = devm_kmalloc(&hdev->dev, OUT_BUFFER_SIZE, GFP_KERNEL);
if (!ccp->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 out_hw_stop;
ccp->hdev = hdev;
hid_set_drvdata(hdev, ccp);
mutex_init(&ccp->mutex);
init_completion(&ccp->wait_input_report);
hid_device_io_start(hdev);
/* temp and fan connection status only updates when device is powered on */
ret = get_temp_cnct(ccp);
if (ret)
goto out_hw_close;
ret = get_fan_cnct(ccp);
if (ret)
goto out_hw_close;
ccp->hwmon_dev = hwmon_device_register_with_info(&hdev->dev, "corsaircpro",
ccp, &ccp_chip_info, 0);
if (IS_ERR(ccp->hwmon_dev)) {
ret = PTR_ERR(ccp->hwmon_dev);
goto out_hw_close;
}
return 0;
out_hw_close:
hid_hw_close(hdev);
out_hw_stop:
hid_hw_stop(hdev);
return ret;
}
static void ccp_remove(struct hid_device *hdev)
{
struct ccp_device *ccp = hid_get_drvdata(hdev);
hwmon_device_unregister(ccp->hwmon_dev);
hid_hw_close(hdev);
hid_hw_stop(hdev);
}
static const struct hid_device_id ccp_devices[] = {
{ HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, USB_PRODUCT_ID_CORSAIR_COMMANDERPRO) },
{ HID_USB_DEVICE(USB_VENDOR_ID_CORSAIR, USB_PRODUCT_ID_CORSAIR_1000D) },
{ }
};
static struct hid_driver ccp_driver = {
.name = "corsair-cpro",
.id_table = ccp_devices,
.probe = ccp_probe,
.remove = ccp_remove,
.raw_event = ccp_raw_event,
};
MODULE_DEVICE_TABLE(hid, ccp_devices);
MODULE_LICENSE("GPL");
static int __init ccp_init(void)
{
return hid_register_driver(&ccp_driver);
}
static void __exit ccp_exit(void)
{
hid_unregister_driver(&ccp_driver);
}
/*
* When compiling this driver as built-in, hwmon initcalls will get called before the
* hid driver and this driver would fail to register. late_initcall solves this.
*/
late_initcall(ccp_init);
module_exit(ccp_exit);