2022-09-14 05:33:21

by Mathew McBride

[permalink] [raw]
Subject: [PATCH 0/3] hwmon: add Microchip EMC230X fan controller driver

The Microchip EMC230X (formerly made by SMSC) family of fan controllers
provide PWM control for up to 5 fans (in the EMC2305). The EMC230X is
capable of maintaining (closed-loop) a target RPM speed through PWM.

This driver has been tested with the EMC2301 (on our Traverse Ten64
appliance) and with the EMC2305 demo board (ADM00879).

The driver is by no means complete, for example, further work would
be required to support the different PWM output frequencies for
voltage-based fan speed control. (So far this driver has only been
tested with direct PWM capable fans, like the 4 pin fans found
in recent PCs)

The emc230x driver also has thermal subsystem integration which allows
the emc230x-controlled fan(s) to be used as cooling devices.

Mathew McBride (3):
hwmon: (emc230x) add Microchip (SMSC) EMC230X fan controller support
dt-bindings: add binding for Microchip EMC230X fan controller family
arm64: dts: ten64: add configuration for fan controller

.../bindings/hwmon/microchip,emc2301.yaml | 83 +++
MAINTAINERS | 7 +
.../boot/dts/freescale/fsl-ls1088a-ten64.dts | 43 ++
drivers/hwmon/Kconfig | 13 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/emc230x.c | 587 ++++++++++++++++++
6 files changed, 734 insertions(+)
create mode 100644 Documentation/devicetree/bindings/hwmon/microchip,emc2301.yaml
create mode 100644 drivers/hwmon/emc230x.c

--
2.30.1


2022-09-14 05:41:41

by Mathew McBride

[permalink] [raw]
Subject: [PATCH 2/3] dt-bindings: add binding for Microchip EMC230X fan controller family

Add a binding for the Microchip EMC230X fan controller family,
which is supported by the new "emc230x" hwmon driver.

Signed-off-by: Mathew McBride <[email protected]>
---
.../bindings/hwmon/microchip,emc2301.yaml | 83 +++++++++++++++++++
MAINTAINERS | 1 +
2 files changed, 84 insertions(+)
create mode 100644 Documentation/devicetree/bindings/hwmon/microchip,emc2301.yaml

diff --git a/Documentation/devicetree/bindings/hwmon/microchip,emc2301.yaml b/Documentation/devicetree/bindings/hwmon/microchip,emc2301.yaml
new file mode 100644
index 000000000000..1e5c7072caee
--- /dev/null
+++ b/Documentation/devicetree/bindings/hwmon/microchip,emc2301.yaml
@@ -0,0 +1,83 @@
+# SPDX-License-Identifier: GPL-2.0-only or BSD-2-Clause
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/hwmon/microchip,emc2301.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: Microchip EMC230X family RPM fan controller
+
+maintainers:
+ - Mathew McBride <[email protected]>
+
+description: |
+ The Microchip EMC230X family (formerly produced by SMSC) is a family
+ of fan controllers which can drive via a set PWM period or
+ to a target RPM speed. They are available in variants from 1 to 5
+ independent channels.
+
+ Product information:
+ https://www.microchip.com/en-us/product/EMC2301
+ https://www.microchip.com/en-us/product/EMC2302
+ https://www.microchip.com/en-us/product/EMC2303
+ https://www.microchip.com/en-us/product/EMC2305
+
+
+properties:
+ compatible:
+ enum:
+ - microchip,emc2301
+ - microchip,emc2302
+ - microchip,emc2303
+ - microchip,emc2305
+ reg:
+ maxItems: 1
+
+ "#address-cells": true
+
+ "#size-cells": true
+
+required:
+ - compatible
+ - reg
+
+patternProperties:
+ "fan@[0-9]+$":
+ type: object
+ description: Fan channel properties for use as a thermal cooling device
+
+ properties:
+ min-rpm:
+ description: Minimum fan RPM when used as a cooling device
+ maxItems: 1
+ max-rpm:
+ description: Maximum fan RPM when used as a cooling device
+ maxItems: 1
+ reg:
+ description: Channel number on EMC230X device the fan is attached to
+ maxItems: 1
+ "#cooling-cells":
+ const: 2
+ required:
+ - reg
+ - min-rpm
+ - max-rpm
+ - "#cooling-cells"
+additionalProperties: false
+
+examples:
+ - |
+ i2c {
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ fanctrl@2f {
+ compatible = "microchip,emc2301";
+ reg = <0x2f>;
+
+ fan@0 {
+ min-rpm = /bits/ 16 <3500>;
+ max-rpm = /bits/ 16 <5000>;
+ reg = <0>;
+ }
+ };
+ };
diff --git a/MAINTAINERS b/MAINTAINERS
index bf3c6c3c0492..5938780abe20 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8046,6 +8046,7 @@ MICROCHIP EMC230X PWM-FAN CONTROLLER DRIVERS
M: Mathew McBride <[email protected]>
L: [email protected]
S: Maintained
+F: Documentation/devicetree/bindings/hwmon/microchip,emc2301.yaml
F: drivers/hwmon/emc230x.c

MICROCHIP POLARFIRE FPGA DRIVERS
--
2.30.1

2022-09-14 05:57:21

by Mathew McBride

[permalink] [raw]
Subject: [PATCH 3/3] arm64: dts: ten64: add configuration for fan controller

The Ten64 desktop appliance has a Noctua NF-A4x20 PWM fan,
controlled by a Microchip EMC2301 PWM fan controller.

This binding allows the fan speed to be slowed to a
quieter level when the system is not busy.

Reference: https://ten64doc.traverse.com.au/hardware/fan-control/

Signed-off-by: Mathew McBride <[email protected]>
---
.../boot/dts/freescale/fsl-ls1088a-ten64.dts | 43 +++++++++++++++++++
1 file changed, 43 insertions(+)

diff --git a/arch/arm64/boot/dts/freescale/fsl-ls1088a-ten64.dts b/arch/arm64/boot/dts/freescale/fsl-ls1088a-ten64.dts
index ef6c8967533e..fd8261c9a186 100644
--- a/arch/arm64/boot/dts/freescale/fsl-ls1088a-ten64.dts
+++ b/arch/arm64/boot/dts/freescale/fsl-ls1088a-ten64.dts
@@ -87,6 +87,35 @@ sfp_xg1: dpmac1-sfp {
los-gpios = <&sfpgpio 7 GPIO_ACTIVE_HIGH>;
maximum-power-milliwatt = <2000>;
};
+
+ thermal-zones {
+ soc {
+ trips {
+ fanmid0: fanmid0 {
+ temperature = <60000>;
+ hysteresis = <2000>;
+ type = "active";
+ };
+
+ fanmax0: fanmax0 {
+ temperature = <70000>;
+ hysteresis = <2000>;
+ type = "active";
+ };
+ };
+
+ cooling-maps {
+ map2 {
+ trip = <&fanmid0>;
+ cooling-device = <&case_fan 0 6>;
+ };
+ map3 {
+ trip = <&fanmax0>;
+ cooling-device = <&case_fan 7 THERMAL_NO_LIMIT>;
+ };
+ };
+ };
+ };
};

/* XG1 - Upper SFP */
@@ -231,6 +260,20 @@ at97sc: tpm@29 {
compatible = "atmel,at97sc3204t";
reg = <0x29>;
};
+
+ fanctrl: emc2301@2f {
+ reg = <0x2f>;
+ compatible = "microchip,emc2301";
+ #address-cells = <1>;
+ #size-cells = <0>;
+
+ case_fan: fan@0 {
+ reg = <0>;
+ min-rpm = /bits/ 16 <3000>;
+ max-rpm = /bits/ 16 <5500>;
+ #cooling-cells = <2>;
+ };
+ };
};

&i2c2 {
--
2.30.1

2022-09-14 06:03:16

by Mathew McBride

[permalink] [raw]
Subject: [PATCH 1/3] hwmon: (emc230x) add Microchip (SMSC) EMC230X fan controller support

The EMC230X is a family of RPM-based RPM fan controllers from
Microchip (and SMSC before that) available in 1 (EMC2301), 2,
3 and 5 (EMC2305) channel variants.

This driver supports being used as a thermal cooling device,
with RPM control when configured from a device tree.

At the moment this driver assumes the 'default' PWM configuration
in the controller (open-drain, 20kHz PWM), other configurations
will require further development.

Signed-off-by: Mathew McBride <[email protected]>
---
MAINTAINERS | 6 +
drivers/hwmon/Kconfig | 13 +
drivers/hwmon/Makefile | 1 +
drivers/hwmon/emc230x.c | 587 ++++++++++++++++++++++++++++++++++++++++
4 files changed, 607 insertions(+)
create mode 100644 drivers/hwmon/emc230x.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 936490dcc97b..bf3c6c3c0492 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -8042,6 +8042,12 @@ S: Maintained
F: Documentation/ABI/testing/sysfs-driver-intel-m10-bmc-sec-update
F: drivers/fpga/intel-m10-bmc-sec-update.c

+MICROCHIP EMC230X PWM-FAN CONTROLLER DRIVERS
+M: Mathew McBride <[email protected]>
+L: [email protected]
+S: Maintained
+F: drivers/hwmon/emc230x.c
+
MICROCHIP POLARFIRE FPGA DRIVERS
M: Conor Dooley <[email protected]>
R: Ivan Bornyakov <[email protected]>
diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
index e70d9614bec2..e8da7793367c 100644
--- a/drivers/hwmon/Kconfig
+++ b/drivers/hwmon/Kconfig
@@ -1775,6 +1775,19 @@ config SENSORS_EMC1403
Threshold values can be configured using sysfs.
Data from the different diodes are accessible via sysfs.

+config SENSORS_EMC230X
+ tristate "Microchip EMC230X fan controllers"
+ depends on I2C
+ help
+ If you say yes here you get support for the Microchip (SMSC)
+ EMC230x family of RPM-PWM fan controllers.
+
+ When the thermal subsystem is enabled, this driver can
+ be used as a cooling-device for a thermal zone.
+
+ This driver can also be built as a module. If so, the module
+ will be called emc230x.
+
config SENSORS_EMC2103
tristate "SMSC EMC2103"
depends on I2C
diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
index 007e829d1d0d..f87f8323493b 100644
--- a/drivers/hwmon/Makefile
+++ b/drivers/hwmon/Makefile
@@ -69,6 +69,7 @@ obj-$(CONFIG_SENSORS_DRIVETEMP) += drivetemp.o
obj-$(CONFIG_SENSORS_DS620) += ds620.o
obj-$(CONFIG_SENSORS_DS1621) += ds1621.o
obj-$(CONFIG_SENSORS_EMC1403) += emc1403.o
+obj-$(CONFIG_SENSORS_EMC230X) += emc230x.o
obj-$(CONFIG_SENSORS_EMC2103) += emc2103.o
obj-$(CONFIG_SENSORS_EMC6W201) += emc6w201.o
obj-$(CONFIG_SENSORS_F71805F) += f71805f.o
diff --git a/drivers/hwmon/emc230x.c b/drivers/hwmon/emc230x.c
new file mode 100644
index 000000000000..1207acf62d2c
--- /dev/null
+++ b/drivers/hwmon/emc230x.c
@@ -0,0 +1,587 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Microchip (formerly SMSC) EMC230X family RPM fan controller driver
+ * For EMC2301/2/5 controllers
+ * Copyright (C) 2021-2022 Traverse Technologies
+ */
+
+/*
+ * This driver has two components:
+ * - hwmon (read/write fan rpm values)
+ * - thermal (set fan rpm speeds for cooling purposes)
+ */
+
+#include <linux/err.h>
+#include <linux/hwmon.h>
+#include <linux/i2c.h>
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/of_device.h>
+#include <linux/thermal.h>
+
+#define TACH_HIGH_MASK GENMASK(12, 5)
+#define TACH_LOW_MASK GENMASK(4, 0)
+
+#define EMC230X_FAN_STALL_STATUS_REG 0x25
+#define EMC230X_FAN_SPIN_FAIL_STATUS_REG 0x26
+#define EMC230X_FAN_DRIVE_FAIL_STATUS_REG 0x27
+
+#define EMC230X_PWM_FAN_DRIVE_REG(channel) (0x30 + (channel * 0x10))
+#define EMC230X_FAN_CONFIG_REG(channel) (0x32 + (channel * 0x10))
+#define EMC230X_FAN_CONFIG_ENABLE_CLOSED_LOOP BIT(7)
+#define EMC230X_TACH_TARGET_LOW_REG(channel) (0x3C + (channel * 0x10))
+#define EMC230X_TACH_READ_HIGH_REG(channel) (0x3E + (channel * 0x10))
+
+#define THERMAL_DEVICE_COOLING_MAX_STEPS 7
+#define MAX_COOLING_DEV_NAME_LEN 16
+/*
+ * Factor by equations [2] and [3] from data sheet; valid for fans where the
+ * number of edges equals (poles * 2 + 1).
+ */
+#define FAN_RPM_FACTOR 3932160
+
+struct emc230x_cooling_device {
+ struct emc230x_data *parent;
+ char name[MAX_COOLING_DEV_NAME_LEN];
+ struct thermal_cooling_device *tcdev;
+ u8 channel;
+ u8 cur_state;
+ u16 cooling_step;
+ u16 min_rpm;
+ u16 max_rpm;
+};
+
+struct emc230x_data {
+ struct device *hwmon_dev;
+ struct i2c_client *i2c_client;
+ u8 device_max_channels;
+};
+
+static const struct i2c_device_id emc230x_ids[] = {
+ { "emc2301", 1 },
+ { "emc2302", 2 },
+ { "emc2303", 3 },
+ { "emc2305", 5 },
+ { /* sentinel */ }
+};
+
+MODULE_DEVICE_TABLE(i2c, emc230x_ids);
+
+static int emc2301_read_fan_tach(struct device *dev,
+ int channel,
+ u16 *tach)
+{
+ struct emc230x_data *priv = dev_get_drvdata(dev);
+ u8 channel_high_register_addr;
+ u16 combined_reading;
+ u8 channel_bytes[2];
+ int bytes_read;
+
+ if ((channel + 1) > priv->device_max_channels)
+ return -EINVAL;
+
+ channel_high_register_addr = EMC230X_TACH_READ_HIGH_REG(channel);
+
+ bytes_read = i2c_smbus_read_i2c_block_data(priv->i2c_client,
+ channel_high_register_addr,
+ 2, &channel_bytes[0]);
+ if (bytes_read < 2) {
+ dev_err(dev,
+ "%s: error %d reading channel %d tach register",
+ __func__, bytes_read, channel);
+ return bytes_read;
+ }
+
+ /* First byte (register) is the high byte, second low byte */
+ combined_reading = ((u16)(channel_bytes[0]) << 5) | (channel_bytes[1] >> 3);
+
+ *tach = combined_reading;
+
+ return 0;
+}
+
+static int emc230x_show_fan_rpm(struct device *dev, int channel, long *val)
+{
+ long fan_rpm;
+ u16 channel_tach;
+ int ret;
+
+ ret = emc2301_read_fan_tach(dev, channel, &channel_tach);
+ if (ret)
+ return ret;
+
+ fan_rpm = (FAN_RPM_FACTOR * 2) / channel_tach;
+ *val = fan_rpm;
+
+ return 0;
+}
+
+static int emc230x_write_fan_rpm_target(struct i2c_client *client,
+ int channel, long target_rpm)
+{
+ int ret;
+ u16 target_tach;
+ u8 tach_channel_reg_addr;
+ u8 tach_bytes[2];
+
+ tach_channel_reg_addr = EMC230X_TACH_TARGET_LOW_REG(channel);
+ /* 0 RPM is a special case - writing 0xFFF0 will turn off the fan */
+ if (target_rpm == 0) {
+ tach_bytes[0] = 0xF0;
+ tach_bytes[1] = 0xFF;
+ } else {
+ target_tach = (FAN_RPM_FACTOR * 2) / target_rpm;
+ tach_bytes[0] = (target_tach & TACH_LOW_MASK) << 3;
+ tach_bytes[1] = (target_tach & TACH_HIGH_MASK) >> 5;
+ }
+
+ ret = i2c_smbus_write_i2c_block_data(client,
+ tach_channel_reg_addr,
+ 2, &tach_bytes[0]);
+ if (ret != 2)
+ return ret;
+
+ return 0;
+}
+
+static int emc230x_set_fan_rpm(struct device *dev, int channel, long target_rpm)
+{
+ struct emc230x_data *priv = dev_get_drvdata(dev);
+
+ if ((channel + 1) > priv->device_max_channels)
+ return -ENODEV;
+
+ dev_dbg(dev,
+ "%s: setting channel %d rpm to %ld\n",
+ __func__, channel, target_rpm);
+
+ return emc230x_write_fan_rpm_target(priv->i2c_client, channel, target_rpm);
+}
+
+static int emc230x_read_fan_rpm_target_value(struct i2c_client *client,
+ int channel,
+ long *target_rpm_value)
+{
+ int ret;
+ u8 tach_channel_reg_addr;
+ u8 tach_bytes[2];
+ u16 combined_reading;
+ long rpm_target;
+
+ tach_channel_reg_addr = EMC230X_TACH_TARGET_LOW_REG(channel);
+ ret = i2c_smbus_read_i2c_block_data(client,
+ tach_channel_reg_addr,
+ 2, &tach_bytes[0]);
+ if (ret != 2)
+ return ret;
+
+ /* This is different from the RPM speed registers,
+ * the low byte is the first register read
+ */
+ if (tach_bytes[1] == 0xFF && tach_bytes[0] == 0xF0) {
+ rpm_target = 0;
+ } else {
+ combined_reading = ((u16)(tach_bytes[1]) << 5) | (tach_bytes[0] >> 3);
+ rpm_target = (FAN_RPM_FACTOR * 2) / combined_reading;
+ }
+
+ *target_rpm_value = rpm_target;
+ return 0;
+}
+
+static int emc230x_show_fan_rpm_target(struct device *dev, int channel,
+ long *value)
+{
+ struct emc230x_data *priv = dev_get_drvdata(dev);
+
+ if ((channel + 1) > priv->device_max_channels)
+ return -EINVAL;
+
+ return emc230x_read_fan_rpm_target_value(priv->i2c_client,
+ channel,
+ value);
+}
+
+static int emc230x_enable_rpm_control(struct device *dev,
+ struct i2c_client *client,
+ int channel, bool enable)
+{
+ u8 fan_config_reg_addr;
+ u8 fan_config_reg_val;
+ int ret;
+
+ fan_config_reg_addr = EMC230X_FAN_CONFIG_REG(channel);
+
+ fan_config_reg_val = i2c_smbus_read_byte_data(client, fan_config_reg_addr);
+ if (enable)
+ fan_config_reg_val |= EMC230X_FAN_CONFIG_ENABLE_CLOSED_LOOP;
+ else
+ fan_config_reg_val &= ~(EMC230X_FAN_CONFIG_ENABLE_CLOSED_LOOP);
+
+ ret = i2c_smbus_write_byte_data(client, fan_config_reg_addr, fan_config_reg_val);
+ if (ret) {
+ dev_err(dev,
+ "Unable to write fan configuration register %02X\n",
+ fan_config_reg_addr);
+ return ret;
+ }
+
+ /* If RPM drive not enabled, set PWM cycle (non-closed loop) to 100% */
+ if (!enable)
+ ret = i2c_smbus_write_byte_data(client,
+ EMC230X_PWM_FAN_DRIVE_REG(channel),
+ 0xFF);
+
+ return ret;
+};
+
+static int emc230x_write_rpm_control(struct device *dev,
+ int channel,
+ long value)
+{
+ struct emc230x_data *priv = dev_get_drvdata(dev);
+ bool enable_rpm_control = (value == 1) ? true : false;
+
+ return emc230x_enable_rpm_control(dev, priv->i2c_client,
+ channel,
+ enable_rpm_control);
+}
+
+static int emc230x_read_is_rpm_control(struct device *dev,
+ int channel,
+ long *value)
+{
+ struct emc230x_data *priv = dev_get_drvdata(dev);
+ u8 fan_config_reg_addr;
+ u32 fan_config_reg_val;
+
+ fan_config_reg_addr = EMC230X_FAN_CONFIG_REG(channel);
+
+ fan_config_reg_val = i2c_smbus_read_byte_data(priv->i2c_client,
+ fan_config_reg_addr);
+ if (fan_config_reg_val < 0)
+ return fan_config_reg_val;
+
+ if (fan_config_reg_val & EMC230X_FAN_CONFIG_ENABLE_CLOSED_LOOP)
+ *value = 1;
+ else
+ *value = 0;
+
+ return 0;
+}
+
+
+static int emc230x_read_fault(struct i2c_client *client,
+ int channel,
+ long *retval)
+{
+ int ret;
+ u8 fan_mask;
+ u8 fan_status_registers[3];
+ long fault_value = 0;
+
+ /* Read the three fan fault registers (stall,spin,drive)
+ * consecutively
+ */
+ ret = i2c_smbus_read_i2c_block_data(client,
+ EMC230X_FAN_STALL_STATUS_REG,
+ 3, &fan_status_registers[0]);
+ if (ret != 3)
+ return ret;
+
+ fan_mask = (1 << channel);
+
+ if (fan_status_registers[0] & fan_mask) /* Stall */
+ fault_value = 1;
+ if (fan_status_registers[1] & fan_mask) /* Spin */
+ fault_value |= (1<<1);
+ if (fan_status_registers[2] & fan_mask) /* Drive */
+ fault_value |= (1<<2);
+ *retval = fault_value;
+
+ return 0;
+}
+
+static int emc230x_show_faults(struct device *dev, int channel,
+ long *value)
+{
+ struct emc230x_data *priv = dev_get_drvdata(dev);
+
+ return emc230x_read_fault(priv->i2c_client, channel, value);
+}
+
+static const struct hwmon_channel_info *emc2301_hwmon_info[] = {
+ HWMON_CHANNEL_INFO(fan,
+ HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_TARGET,
+ HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_TARGET,
+ HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_TARGET,
+ HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_TARGET,
+ HWMON_F_INPUT | HWMON_F_FAULT | HWMON_F_TARGET),
+ HWMON_CHANNEL_INFO(pwm,
+ HWMON_PWM_ENABLE,
+ HWMON_PWM_ENABLE,
+ HWMON_PWM_ENABLE,
+ HWMON_PWM_ENABLE,
+ HWMON_PWM_ENABLE),
+ NULL
+};
+
+static umode_t emc230x_hwmon_is_visible(const void *data,
+ enum hwmon_sensor_types type,
+ u32 attr, int channel)
+{
+ struct emc230x_data *priv = (struct emc230x_data *)data;
+
+ if ((channel + 1) > priv->device_max_channels)
+ return 0;
+
+ switch (type) {
+ case hwmon_fan:
+ switch (attr) {
+ case hwmon_fan_input:
+ case hwmon_fan_fault:
+ return 0444;
+ case hwmon_fan_target:
+ return 0644;
+ default:
+ return 0;
+ }
+ case hwmon_pwm:
+ switch (attr) {
+ case hwmon_pwm_enable:
+ return 0644;
+ default:
+ return 0;
+ }
+ default:
+ return 0;
+ }
+}
+
+static int emc230x_hwmon_read(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long *value)
+{
+ switch (type) {
+ case hwmon_fan:
+ switch (attr) {
+ case hwmon_fan_input:
+ return emc230x_show_fan_rpm(dev, channel, value);
+ case hwmon_fan_target:
+ return emc230x_show_fan_rpm_target(dev, channel, value);
+ case hwmon_fan_fault:
+ return emc230x_show_faults(dev, channel, value);
+ default:
+ return 0;
+ }
+ case hwmon_pwm:
+ switch (attr) {
+ case hwmon_pwm_enable:
+ return emc230x_read_is_rpm_control(dev, channel, value);
+ default:
+ return 0;
+ }
+ default:
+ return 0;
+ }
+}
+
+static int emc230x_hwmon_write(struct device *dev, enum hwmon_sensor_types type,
+ u32 attr, int channel, long value)
+{
+ if (type == hwmon_fan && attr == hwmon_fan_target)
+ return emc230x_set_fan_rpm(dev, channel, value);
+ else if (type == hwmon_pwm && attr == hwmon_pwm_enable)
+ return emc230x_write_rpm_control(dev, channel, value);
+
+ return -EINVAL;
+}
+
+static int emc230x_thermal_get_max_state(struct thermal_cooling_device *tcdev,
+ unsigned long *state)
+{
+ *state = THERMAL_DEVICE_COOLING_MAX_STEPS;
+ return 0;
+}
+
+static int emc230x_thermal_get_cur_state(struct thermal_cooling_device *tcdev,
+ unsigned long *state)
+{
+ struct emc230x_cooling_device *cdev = tcdev->devdata;
+
+ *state = cdev->cur_state;
+
+ return 0;
+}
+
+static int emc230x_thermal_set_cur_state(struct thermal_cooling_device *tcdev,
+ unsigned long state)
+{
+ struct emc230x_cooling_device *cdev = tcdev->devdata;
+ struct emc230x_data *parent_device = cdev->parent;
+ struct i2c_client *client = parent_device->i2c_client;
+ int ret;
+ u16 fan_rpm;
+
+ if (state > 0)
+ fan_rpm = cdev->min_rpm + ((state + 1) * cdev->cooling_step);
+ else
+ fan_rpm = cdev->min_rpm;
+
+ ret = emc230x_write_fan_rpm_target(client, cdev->channel, fan_rpm);
+ if (!ret)
+ cdev->cur_state = state;
+
+ return ret;
+}
+
+static const struct thermal_cooling_device_ops emc230x_fan_cooling_ops = {
+ .get_max_state = emc230x_thermal_get_max_state,
+ .get_cur_state = emc230x_thermal_get_cur_state,
+ .set_cur_state = emc230x_thermal_set_cur_state
+};
+
+/*
+ * emc230x_create_fan() - create a thermal device from FDT configuration
+ */
+static int emc230x_create_fan(struct device *dev,
+ struct device_node *child,
+ struct emc230x_data *priv)
+{
+ int ret;
+ u32 channel;
+ u16 min_rpm, max_rpm, cooling_range;
+ struct emc230x_cooling_device *faninfo;
+
+ ret = of_property_read_u32(child, "reg", &channel);
+ if (ret)
+ return ret;
+
+ if ((channel + 1) > priv->device_max_channels)
+ return -EINVAL;
+
+ /* If there are any DT attributes introduced that are not
+ * directly related to thermal subsystem integration
+ * (such as configuring PWM modes, or fan pole settings),
+ * they should be processed here before the #cooling-cells
+ * check
+ */
+ ret = of_property_count_u32_elems(child, "#cooling-cells");
+ if (ret < 1)
+ return 0;
+
+ ret = of_property_read_u16(child, "min-rpm", &min_rpm);
+ if (ret) {
+ dev_err(dev,
+ "%s: missing or invalid \"min-rpm\" property (channel=%d,err=%d)\n",
+ __func__, channel, ret);
+ return ret;
+ }
+
+ ret = of_property_read_u16(child, "max-rpm", &max_rpm);
+ if (ret) {
+ dev_err(dev,
+ "%s: missing or invalid \"min-rpm\" property (channel=%d,err=%d)\n",
+ __func__, channel, ret);
+ return ret;
+ }
+
+ faninfo = devm_kzalloc(dev, sizeof(*faninfo), GFP_KERNEL);
+
+ faninfo->parent = priv;
+ faninfo->channel = (u8)channel;
+ faninfo->min_rpm = min_rpm;
+ faninfo->max_rpm = max_rpm;
+
+ cooling_range = max_rpm - min_rpm;
+ faninfo->cooling_step = cooling_range / (THERMAL_DEVICE_COOLING_MAX_STEPS+1);
+ faninfo->cur_state = THERMAL_DEVICE_COOLING_MAX_STEPS;
+
+ /* Set the maximum fan rpm */
+ ret = emc230x_write_fan_rpm_target(priv->i2c_client, channel, max_rpm);
+ if (ret)
+ return ret;
+
+ /* Enable RPM closed-loop control */
+ ret = emc230x_enable_rpm_control(dev, priv->i2c_client, channel, true);
+ if (ret)
+ return ret;
+
+ snprintf(faninfo->name, MAX_COOLING_DEV_NAME_LEN, "%pOFn%d", child, channel);
+
+ faninfo->tcdev = devm_thermal_of_cooling_device_register(dev, child,
+ faninfo->name,
+ faninfo,
+ &emc230x_fan_cooling_ops);
+ dev_info(dev,
+ "channel %d registered as cooling device %d, min/max RPM %d/%d step %d\n",
+ channel, faninfo->tcdev->id, min_rpm, max_rpm, faninfo->cooling_step);
+
+ return 0;
+}
+
+static const struct hwmon_ops emc230x_hwmon_ops = {
+ .is_visible = emc230x_hwmon_is_visible,
+ .read = emc230x_hwmon_read,
+ .write = emc230x_hwmon_write,
+};
+
+static const struct hwmon_chip_info emc230x_chip_info = {
+ .ops = &emc230x_hwmon_ops,
+ .info = emc2301_hwmon_info
+};
+
+static int emc230x_probe(struct i2c_client *i2c)
+{
+ struct device *dev = &i2c->dev;
+ struct device *hwmon_dev;
+ struct device_node *np, *child;
+ struct emc230x_data *priv;
+ const struct i2c_device_id *dev_id;
+ int ret;
+
+ if (!i2c_check_functionality(i2c->adapter, I2C_FUNC_SMBUS_BYTE_DATA |
+ I2C_FUNC_SMBUS_WORD_DATA))
+ return -ENODEV;
+
+ priv = devm_kzalloc(dev, sizeof(struct emc230x_data), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->i2c_client = i2c;
+
+ dev_id = i2c_match_id(emc230x_ids, i2c);
+ if (!dev_id)
+ return -ENODEV;
+
+ priv->device_max_channels = (u32)dev_id->driver_data;
+
+ if (IS_REACHABLE(CONFIG_THERMAL) && dev->of_node) {
+ np = dev->of_node;
+ for_each_child_of_node(np, child) {
+ ret = emc230x_create_fan(dev, child, priv);
+ if (ret)
+ of_node_put(child);
+ }
+ }
+
+ hwmon_dev = devm_hwmon_device_register_with_info(dev, i2c->name,
+ priv,
+ &emc230x_chip_info,
+ NULL);
+ return PTR_ERR_OR_ZERO(hwmon_dev);
+}
+
+static struct i2c_driver emc230x_driver = {
+ .class = I2C_CLASS_HWMON,
+ .driver = {
+ .name = "emc230x",
+ },
+ .probe_new = emc230x_probe,
+ .id_table = emc230x_ids,
+};
+
+module_i2c_driver(emc230x_driver);
+
+MODULE_AUTHOR("Mathew McBride <[email protected]>");
+MODULE_DESCRIPTION("Microchip EMC230X family PWM RPM fan controller driver");
+MODULE_LICENSE("GPL");
--
2.30.1

2022-09-14 10:40:41

by Mathew McBride

[permalink] [raw]
Subject: Re: [PATCH 0/3] hwmon: add Microchip EMC230X fan controller driver

On Wed, Sep 14, 2022, at 8:12 PM, Guenter Roeck wrote:
> On Wed, Sep 14, 2022 at 05:30:27AM +0000, Mathew McBride wrote:
> > The Microchip EMC230X (formerly made by SMSC) family of fan controllers
> > provide PWM control for up to 5 fans (in the EMC2305). The EMC230X is
> > capable of maintaining (closed-loop) a target RPM speed through PWM.
> >
> > This driver has been tested with the EMC2301 (on our Traverse Ten64
> > appliance) and with the EMC2305 demo board (ADM00879).
> >
> > The driver is by no means complete, for example, further work would
> > be required to support the different PWM output frequencies for
> > voltage-based fan speed control. (So far this driver has only been
> > tested with direct PWM capable fans, like the 4 pin fans found
> > in recent PCs)
> >
> > The emc230x driver also has thermal subsystem integration which allows
> > the emc230x-controlled fan(s) to be used as cooling devices.
>
> I just accepted a driver or emc2301/2/3/5. Please submit improvements
> on top of that driver if needed; we won't have competing drivers
> for the same chip in the kernel, and replacing a just accepted driver
> smply does not make any sense.

My apologies, I was unaware of the separate effort!
Mine has been out of tree for a long time and I finally had time to work on it recently.

There are several changes I can see that need to be made to the accepted one to work for our usecase.

> Guenter
>

2022-09-14 10:53:01

by Guenter Roeck

[permalink] [raw]
Subject: Re: [PATCH 0/3] hwmon: add Microchip EMC230X fan controller driver

On Wed, Sep 14, 2022 at 05:30:27AM +0000, Mathew McBride wrote:
> The Microchip EMC230X (formerly made by SMSC) family of fan controllers
> provide PWM control for up to 5 fans (in the EMC2305). The EMC230X is
> capable of maintaining (closed-loop) a target RPM speed through PWM.
>
> This driver has been tested with the EMC2301 (on our Traverse Ten64
> appliance) and with the EMC2305 demo board (ADM00879).
>
> The driver is by no means complete, for example, further work would
> be required to support the different PWM output frequencies for
> voltage-based fan speed control. (So far this driver has only been
> tested with direct PWM capable fans, like the 4 pin fans found
> in recent PCs)
>
> The emc230x driver also has thermal subsystem integration which allows
> the emc230x-controlled fan(s) to be used as cooling devices.

I just accepted a driver or emc2301/2/3/5. Please submit improvements
on top of that driver if needed; we won't have competing drivers
for the same chip in the kernel, and replacing a just accepted driver
smply does not make any sense.

Guenter