mbox series

[v2,0/3] upport pwm/tach driver for aspeed ast26xx

Message ID 20221101095156.30591-1-billy_tsai@aspeedtech.com
Headers show
Series upport pwm/tach driver for aspeed ast26xx | expand

Message

Billy Tsai Nov. 1, 2022, 9:51 a.m. UTC
Unlike the old design that the register setting of the TACH should based
on the configure of the PWM. In ast26xx, the dependency between pwm and
tach controller is eliminated and becomes a separate hardware block. One
is used to provide pwm output and another is used to monitor the frequency
of the input. Therefore, this patch serials implements them by writing the
two driver "pwm-aspeed-ast2600.c" and "tach-aspeed-ast2600.c". The former
is following the pwm subsystem which can apply the existed driver to
controller the fan(pwm-fan.c), beeper(pwm-beeper.c) and so on. The latter
is following the sysfs interface of hwmon to creat the node for fan
monitor.

Changes since v1:
- tach:
  - Add the document tach-aspeed-ast2600.rst
  - Use devm_* api to simplify the error cleanup.
  - Change hwmon register api to devm_hwmon_device_register_with_info

Billy Tsai (3):
  dt-bindings: Add bindings for aspeed pwm-tach.
  pwm: Add Aspeed ast2600 PWM support
  hwmon: Add Aspeed ast2600 TACH support

 .../bindings/hwmon/aspeed,ast2600-tach.yaml   |  48 ++
 .../bindings/mfd/aspeed,ast2600-pwm-tach.yaml |  76 +++
 .../bindings/pwm/aspeed,ast2600-pwm.yaml      |  64 +++
 Documentation/hwmon/tach-aspeed-ast2600.rst   |  28 +
 drivers/hwmon/Kconfig                         |   9 +
 drivers/hwmon/Makefile                        |   1 +
 drivers/hwmon/tach-aspeed-ast2600.c           | 484 ++++++++++++++++++
 drivers/pwm/Kconfig                           |  10 +
 drivers/pwm/Makefile                          |   1 +
 drivers/pwm/pwm-aspeed-ast2600.c              | 325 ++++++++++++
 10 files changed, 1046 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/hwmon/aspeed,ast2600-tach.yaml
 create mode 100644 Documentation/devicetree/bindings/mfd/aspeed,ast2600-pwm-tach.yaml
 create mode 100644 Documentation/devicetree/bindings/pwm/aspeed,ast2600-pwm.yaml
 create mode 100644 Documentation/hwmon/tach-aspeed-ast2600.rst
 create mode 100644 drivers/hwmon/tach-aspeed-ast2600.c
 create mode 100644 drivers/pwm/pwm-aspeed-ast2600.c

Comments

Christophe JAILLET Nov. 1, 2022, 10:03 a.m. UTC | #1
Le 01/11/2022 à 10:51, Billy Tsai a écrit :
> This patch add the support of PWM controller which can be found at aspeed
> ast2600 soc. The pwm supoorts up to 16 channels and it's part function
> of multi-function device "pwm-tach controller".
> 
> Signed-off-by: Billy Tsai <billy_tsai@aspeedtech.com>

[...]

> +static int aspeed_pwm_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	int ret;
> +	struct aspeed_pwm_data *priv;
> +	struct device_node *np;
> +	struct platform_device *parent_dev;
> +
> +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +
> +	np = pdev->dev.parent->of_node;
> +	if (!of_device_is_compatible(np, "aspeed,ast2600-pwm-tach"))
> +		return dev_err_probe(dev, -ENODEV,
> +				     "Unsupported pwm device binding\n");
> +
> +	priv->regmap = syscon_node_to_regmap(np);
> +	if (IS_ERR(priv->regmap))
> +		return dev_err_probe(dev, PTR_ERR(priv->regmap),
> +				     "Couldn't get regmap\n");
> +
> +	parent_dev = of_find_device_by_node(np);
> +	priv->clk = devm_clk_get(&parent_dev->dev, 0);

Hi,

if this helps, using devm_clk_get_enabled() would save a few lines of code.

CJ

> +	if (IS_ERR(priv->clk))
> +		return dev_err_probe(dev, PTR_ERR(priv->clk),
> +				     "Couldn't get clock\n");
> +

[...]
Christophe JAILLET Nov. 1, 2022, 10:16 a.m. UTC | #2
Le 01/11/2022 à 10:51, Billy Tsai a écrit :
> This patch add the support of Tachometer which can use to monitor the
> frequency of the input. The tach supports up to 16 channels and it's part
> function of multi-function device "pwm-tach controller".
> 
> Signed-off-by: Billy Tsai <billy_tsai@aspeedtech.com>

Hi,
a few nits below,

[...]

> +
> +	if (ret) {
> +		/* return 0 if we didn't get an answer because of timeout*/

Missing space at the end of the comment

> +		if (ret == -ETIMEDOUT)
> +			return 0;
> +		else
> +			return ret;

[...]

> +static int aspeed_tach_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct device_node *np, *child;
> +	struct aspeed_tach_data *priv;
> +	struct device *hwmon;
> +	struct platform_device *parent_dev;
> +	int ret;
> +
> +	np = dev->parent->of_node;
> +	if (!of_device_is_compatible(np, "aspeed,ast2600-pwm-tach"))
> +		return dev_err_probe(dev, -ENODEV,
> +				     "Unsupported tach device binding\n");
> +
> +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +	priv->dev = &pdev->dev;
> +	priv->tach_channel =
> +		devm_kzalloc(dev,
> +			     TACH_ASPEED_NR_TACHS * sizeof(*priv->tach_channel),
> +			     GFP_KERNEL);

use devm_kcalloc() instead of devm_kzalloc()?
Error handling?

> +
> +	priv->regmap = syscon_node_to_regmap(np);
> +	if (IS_ERR(priv->regmap)) {
> +		dev_err(priv->dev, "Couldn't get regmap\n");

In order to be conistent with the other error handling paths:
return dev_err_probe()?

> +		return -ENODEV;
> +	}

[...]
Guenter Roeck Nov. 1, 2022, 1:14 p.m. UTC | #3
On Tue, Nov 01, 2022 at 05:51:56PM +0800, Billy Tsai wrote:
> This patch add the support of Tachometer which can use to monitor the
> frequency of the input. The tach supports up to 16 channels and it's part
> function of multi-function device "pwm-tach controller".
> 
> Signed-off-by: Billy Tsai <billy_tsai@aspeedtech.com>
> ---
>  Documentation/hwmon/tach-aspeed-ast2600.rst |  28 ++

Also needs to be added to Documentation/hwmon/index.rst.

>  drivers/hwmon/Kconfig                       |   9 +
>  drivers/hwmon/Makefile                      |   1 +
>  drivers/hwmon/tach-aspeed-ast2600.c         | 484 ++++++++++++++++++++
>  4 files changed, 522 insertions(+)
>  create mode 100644 Documentation/hwmon/tach-aspeed-ast2600.rst
>  create mode 100644 drivers/hwmon/tach-aspeed-ast2600.c
> 
> diff --git a/Documentation/hwmon/tach-aspeed-ast2600.rst b/Documentation/hwmon/tach-aspeed-ast2600.rst
> new file mode 100644
> index 000000000000..8967f60672dc
> --- /dev/null
> +++ b/Documentation/hwmon/tach-aspeed-ast2600.rst
> @@ -0,0 +1,28 @@
> +Kernel driver tach-aspeed-ast2600
> +==============================
> +
> +Supported chips:
> +	ASPEED AST2600
> +
> +Authors:
> +	<billy_tsai@aspeedtech.com>
> +
> +Description:
> +------------
> +This driver implements support for ASPEED AST2600 Fan Tacho controller.
> +The controller supports up to 16 tachometer inputs.
> +
> +The driver provides the following sensor accesses in sysfs:
> +=============== ======= =====================================================
> +fanX_input	ro	provide current fan rotation value in RPM as reported
> +			by the fan to the device.
> +fanX_div	rw	Fan divisor: Supported value are power of 4 (1, 4, 16
> +                        64, ... 4194304)
> +                        The larger divisor, the less rpm accuracy and the less
> +                        affected by fan signal glitch.
> +fanX_min	rw      Fan minimum RPM which can used to change the timeout
> +                        value for controller polling the result.
> +fanX_max	rw      Fan maximum RPM which can used to change the polling
> +                        period of the driver.
> +fanX_pulses	rw      Fan pulses per resolution.
> +=============== ======= ======================================================
> diff --git a/drivers/hwmon/Kconfig b/drivers/hwmon/Kconfig
> index fa2356398744..a84c15b73aa6 100644
> --- a/drivers/hwmon/Kconfig
> +++ b/drivers/hwmon/Kconfig
> @@ -397,6 +397,15 @@ config SENSORS_ASPEED
>  	  This driver can also be built as a module. If so, the module
>  	  will be called aspeed_pwm_tacho.
>  
> +config SENSORS_TACH_ASPEED_AST2600
> +	tristate "ASPEED ast2600 Tachometer support"
> +	select REGMAP
> +	help
> +	  This driver provides support for Aspeed ast2600 Tachometer.
> +
> +	  To compile this driver as a module, choose M here: the module
> +	  will be called tach-aspeed-ast2600.
> +
>  config SENSORS_ATXP1
>  	tristate "Attansic ATXP1 VID controller"
>  	depends on I2C
> diff --git a/drivers/hwmon/Makefile b/drivers/hwmon/Makefile
> index d2497b2644e6..7e2d708e93b8 100644
> --- a/drivers/hwmon/Makefile
> +++ b/drivers/hwmon/Makefile
> @@ -51,6 +51,7 @@ obj-$(CONFIG_SENSORS_ARM_SCMI)	+= scmi-hwmon.o
>  obj-$(CONFIG_SENSORS_ARM_SCPI)	+= scpi-hwmon.o
>  obj-$(CONFIG_SENSORS_AS370)	+= as370-hwmon.o
>  obj-$(CONFIG_SENSORS_ASC7621)	+= asc7621.o
> +obj-$(CONFIG_SENSORS_TACH_ASPEED_AST2600) += tach-aspeed-ast2600.o
>  obj-$(CONFIG_SENSORS_ASPEED)	+= aspeed-pwm-tacho.o
>  obj-$(CONFIG_SENSORS_ATXP1)	+= atxp1.o
>  obj-$(CONFIG_SENSORS_AXI_FAN_CONTROL) += axi-fan-control.o
> diff --git a/drivers/hwmon/tach-aspeed-ast2600.c b/drivers/hwmon/tach-aspeed-ast2600.c
> new file mode 100644
> index 000000000000..599ed5db17a8
> --- /dev/null
> +++ b/drivers/hwmon/tach-aspeed-ast2600.c
> @@ -0,0 +1,484 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (C) ASPEED Technology Inc.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/errno.h>
> +#include <linux/delay.h>
> +#include <linux/hwmon.h>
> +#include <linux/hwmon-sysfs.h>

Not needed. Please check if there are other unnecessary includes.

> +#include <linux/io.h>
> +#include <linux/kernel.h>
> +#include <linux/module.h>
> +#include <linux/of_platform.h>
> +#include <linux/of_device.h>
> +#include <linux/platform_device.h>
> +#include <linux/mfd/syscon.h>
> +#include <linux/sysfs.h>
> +#include <linux/reset.h>
> +#include <linux/regmap.h>
> +#include <linux/bitfield.h>

Please order include files in alphabetic order.

> +
> +/* The channel number of Aspeed tach controller */
> +#define TACH_ASPEED_NR_TACHS 16

#define<space>NAME<tab>value, please, and align the value.

> +/* TACH Control Register */
> +#define TACH_ASPEED_CTRL(ch) (((ch) * 0x10) + 0x08)
> +#define TACH_ASPEED_IER BIT(31)
> +#define TACH_ASPEED_INVERS_LIMIT BIT(30)
> +#define TACH_ASPEED_LOOPBACK BIT(29)
> +#define TACH_ASPEED_ENABLE BIT(28)
> +#define TACH_ASPEED_DEBOUNCE_MASK GENMASK(27, 26)
> +#define TACH_ASPEED_DEBOUNCE_BIT (26)

() around plain integers is not needed.

> +#define TACH_ASPEED_IO_EDGE_MASK GENMASK(25, 24)
> +#define TACH_ASPEED_IO_EDGE_BIT (24)
> +#define TACH_ASPEED_CLK_DIV_T_MASK GENMASK(23, 20)
> +#define TACH_ASPEED_CLK_DIV_BIT (20)
> +#define TACH_ASPEED_THRESHOLD_MASK GENMASK(19, 0)
> +/* [27:26] */
> +#define DEBOUNCE_3_CLK 0x00
> +#define DEBOUNCE_2_CLK 0x01
> +#define DEBOUNCE_1_CLK 0x02
> +#define DEBOUNCE_0_CLK 0x03
> +/* [25:24] */
> +#define F2F_EDGES 0x00
> +#define R2R_EDGES 0x01
> +#define BOTH_EDGES 0x02
> +/* [23:20] */
> +/* divisor = 4 to the nth power, n = register value */
> +#define DEFAULT_TACH_DIV 1024
> +#define DIV_TO_REG(divisor) (ilog2(divisor) >> 1)
> +
> +/* TACH Status Register */
> +#define TACH_ASPEED_STS(ch) (((ch) * 0x10) + 0x0C)
> +
> +/*PWM_TACH_STS */
> +#define TACH_ASPEED_ISR BIT(31)
> +#define TACH_ASPEED_PWM_OUT BIT(25)
> +#define TACH_ASPEED_PWM_OEN BIT(24)
> +#define TACH_ASPEED_DEB_INPUT BIT(23)
> +#define TACH_ASPEED_RAW_INPUT BIT(22)
> +#define TACH_ASPEED_VALUE_UPDATE BIT(21)
> +#define TACH_ASPEED_FULL_MEASUREMENT BIT(20)
> +#define TACH_ASPEED_VALUE_MASK GENMASK(19, 0)
> +/**********************************************************
> + * Software setting
> + *********************************************************/
> +#define DEFAULT_FAN_MIN_RPM 1000
> +#define DEFAULT_FAN_PULSE_PR 2
> +/*
> + * Add this value to avoid CPU consuming a lot of resources in waiting rpm
> + * updating. Assume the max rpm of fan is 60000, the fastest period of updating
> + * tach value will be equal to (1000000 * 2 * 60) / (2 * max_rpm) = 1000us.
> + */
> +#define DEFAULT_FAN_MAX_RPM 60000
> +
> +struct aspeed_tach_channel_params {
> +	int limited_inverse;
> +	u16 threshold;
> +	u8 tach_edge;
> +	u8 tach_debounce;
> +	u8 pulse_pr;
> +	u32 min_rpm;
> +	u32 max_rpm;
> +	u32 divisor;
> +	u32 sample_period; /* unit is us */
> +	u32 polling_period; /* unit is us */
> +};
> +
> +struct aspeed_tach_data {
> +	struct device *dev;
> +	struct regmap *regmap;
> +	struct clk *clk;
> +	struct reset_control *reset;
> +	bool tach_present[TACH_ASPEED_NR_TACHS];
> +	struct aspeed_tach_channel_params *tach_channel;
> +};
> +
> +static void aspeed_update_tach_sample_period(struct aspeed_tach_data *priv,
> +					     u8 fan_tach_ch)
> +{
> +	u32 tach_period_us;
> +	u8 pulse_pr = priv->tach_channel[fan_tach_ch].pulse_pr;
> +	u32 min_rpm = priv->tach_channel[fan_tach_ch].min_rpm;
> +
> +	/*
> +	 * min(Tach input clock) = (PulsePR * minRPM) / 60
> +	 * max(Tach input period) = 60 / (PulsePR * minRPM)
> +	 * Tach sample period > 2 * max(Tach input period) = (2*60) / (PulsePR * minRPM)
> +	 */
> +	tach_period_us = (USEC_PER_SEC * 2 * 60) / (pulse_pr * min_rpm);
> +	/* Add the margin (about 1.5) of tach sample period to avoid sample miss */
> +	tach_period_us = (tach_period_us * 1500) >> 10;
> +	dev_dbg(priv->dev, "tach%d sample period = %dus", fan_tach_ch, tach_period_us);
> +	priv->tach_channel[fan_tach_ch].sample_period = tach_period_us;
> +}
> +
> +static void aspeed_update_tach_polling_period(struct aspeed_tach_data *priv,
> +					     u8 fan_tach_ch)
> +{
> +	u32 tach_period_us;
> +	u8 pulse_pr = priv->tach_channel[fan_tach_ch].pulse_pr;
> +	u32 max_rpm = priv->tach_channel[fan_tach_ch].max_rpm;
> +
> +	tach_period_us = (USEC_PER_SEC * 2 * 60) / (pulse_pr * max_rpm);
> +	dev_dbg(priv->dev, "tach%d polling period = %dus", fan_tach_ch, tach_period_us);
> +	priv->tach_channel[fan_tach_ch].polling_period = tach_period_us;
> +}
> +
> +static void aspeed_tach_ch_enable(struct aspeed_tach_data *priv, u8 tach_ch,
> +				  bool enable)
> +{
> +	if (enable)
> +		regmap_set_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +				TACH_ASPEED_ENABLE);
> +	else
> +		regmap_clear_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +				   TACH_ASPEED_ENABLE);
> +}
> +
> +static int aspeed_get_fan_tach_ch_rpm(struct aspeed_tach_data *priv,
> +				      u8 fan_tach_ch)
> +{
> +	u32 raw_data, tach_div, val;
> +	unsigned long clk_source;
> +	u64 rpm;
> +	int ret;
> +
> +	/* Restart the Tach channel to guarantee the value is fresh */
> +	aspeed_tach_ch_enable(priv, fan_tach_ch, false);
> +	aspeed_tach_ch_enable(priv, fan_tach_ch, true);

Is that really needed ? Doesn't the controller measure values continuously ?

> +	ret = regmap_read_poll_timeout(
> +		priv->regmap, TACH_ASPEED_STS(fan_tach_ch), val,
> +		(val & TACH_ASPEED_FULL_MEASUREMENT) &&
> +			(val & TACH_ASPEED_VALUE_UPDATE),
> +		priv->tach_channel[fan_tach_ch].polling_period,
> +		priv->tach_channel[fan_tach_ch].sample_period);

Please fix multi-line alignment everywhere. Go up to 100 columns if needed.

> +
> +	if (ret) {
> +		/* return 0 if we didn't get an answer because of timeout*/
> +		if (ret == -ETIMEDOUT)
> +			return 0;
> +		else
> +			return ret;

else after return is unnecessary, and why would a timeout be be ignored ?

> +	}
> +
> +	raw_data = val & TACH_ASPEED_VALUE_MASK;
> +	/*
> +	 * We need the mode to determine if the raw_data is double (from
> +	 * counting both edges).
> +	 */
> +	if (priv->tach_channel[fan_tach_ch].tach_edge == BOTH_EDGES)
> +		raw_data <<= 1;
> +
> +	tach_div = raw_data * (priv->tach_channel[fan_tach_ch].divisor) *
> +		   (priv->tach_channel[fan_tach_ch].pulse_pr);
> +
> +	clk_source = clk_get_rate(priv->clk);
> +	dev_dbg(priv->dev, "clk %ld, raw_data %d , tach_div %d\n", clk_source,
> +		raw_data, tach_div);
> +
> +	if (tach_div == 0)
> +		return -EDOM;

If the fan is off or not connected, would that return an error ?
If so, that would be inappropriate; it should return a speed
of 0 in that case.

> +
> +	rpm = (u64)clk_source * 60;
> +	do_div(rpm, tach_div);
> +
> +	return rpm;
> +}
> +
> +static int aspeed_tach_hwmon_read(struct device *dev, enum hwmon_sensor_types type, u32 attr,
> +			int channel, long *val)
> +{
> +	struct aspeed_tach_data *priv = dev_get_drvdata(dev);
> +	u32 reg_val;
> +	int ret;
> +
> +	switch (attr) {
> +	case hwmon_fan_input:
> +		ret = aspeed_get_fan_tach_ch_rpm(priv, channel);
> +		if (ret < 0)
> +			return ret;
> +		*val = ret;
> +		break;
> +	case hwmon_fan_min:
> +		*val = priv->tach_channel[channel].min_rpm;
> +		break;
> +	case hwmon_fan_max:
> +		*val = priv->tach_channel[channel].max_rpm;
> +		break;
> +	case hwmon_fan_div:
> +		regmap_read(priv->regmap, TACH_ASPEED_CTRL(channel), &reg_val);
> +		reg_val = FIELD_GET(TACH_ASPEED_CLK_DIV_T_MASK, reg_val);
> +		*val = 1 << (reg_val << 1);

BIT() ?

> +		break;
> +	case hwmon_fan_pulses:
> +		*val = priv->tach_channel[channel].pulse_pr;
> +		break;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +	return 0;
> +}
> +
> +static int aspeed_tach_hwmon_write(struct device *dev, enum hwmon_sensor_types type, u32 attr,
> +			 int channel, long val)
> +{
> +	struct aspeed_tach_data *priv = dev_get_drvdata(dev);
> +
> +	switch (attr) {
> +	case hwmon_fan_min:
> +		priv->tach_channel[channel].min_rpm = val;
> +		aspeed_update_tach_sample_period(priv, channel);
> +		break;
> +	case hwmon_fan_max:
> +		priv->tach_channel[channel].max_rpm = val;
> +		aspeed_update_tach_polling_period(priv, channel);
> +		break;
> +	case hwmon_fan_div:
> +		if ((is_power_of_2(val) && !(ilog2(val) % 2))) {
> +			priv->tach_channel[channel].divisor = val;
> +			regmap_write_bits(
> +				priv->regmap, TACH_ASPEED_CTRL(channel),
> +				TACH_ASPEED_CLK_DIV_T_MASK,
> +				DIV_TO_REG(priv->tach_channel[channel].divisor)
> +					<< TACH_ASPEED_CLK_DIV_BIT);
> +		} else {
> +			dev_err(dev,
> +				"fan_div value %ld not supported. Only support power of 4\n",
> +				val);
> +			return -EINVAL;
> +		}

Error case should be handled first, making else unnecessary.

> +		break;
> +	case hwmon_fan_pulses:
> +		priv->tach_channel[channel].pulse_pr = val;
> +		aspeed_update_tach_sample_period(priv, channel);
> +		break;
> +	default:
> +		return -EOPNOTSUPP;
> +	}
> +
> +	return 0;
> +}
> +
> +static umode_t aspeed_tach_dev_is_visible(const void *drvdata, enum hwmon_sensor_types type,
> +				  u32 attr, int channel)
> +{
> +	const struct aspeed_tach_data *priv = drvdata;
> +
> +	if (!priv->tach_present[channel])
> +		return 0;
> +	switch (attr) {
> +	case hwmon_fan_input:
> +		return 0444;
> +	case hwmon_fan_min:
> +	case hwmon_fan_max:
> +	case hwmon_fan_div:
> +	case hwmon_fan_pulses:
> +		return 0644;
> +	}
> +	return 0;
> +}
> +
> +static const struct hwmon_ops aspeed_tach_ops = {
> +	.is_visible = aspeed_tach_dev_is_visible,
> +	.read = aspeed_tach_hwmon_read,
> +	.write = aspeed_tach_hwmon_write,
> +};
> +
> +static const struct hwmon_channel_info *aspeed_tach_info[] = {
> +	HWMON_CHANNEL_INFO(fan,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES,
> +			   HWMON_F_INPUT | HWMON_F_MIN | HWMON_F_MAX |
> +				   HWMON_F_DIV | HWMON_F_PULSES),
> +	NULL
> +};
> +
> +static const struct hwmon_chip_info aspeed_tach_chip_info = {
> +	.ops = &aspeed_tach_ops,
> +	.info = aspeed_tach_info,
> +};
> +
> +static void aspeed_create_fan_tach_channel(struct aspeed_tach_data *priv,
> +					   u32 tach_ch)
> +{
> +	priv->tach_present[tach_ch] = true;
> +	priv->tach_channel[tach_ch].limited_inverse = 0;
> +	regmap_write_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +			  TACH_ASPEED_INVERS_LIMIT,
> +			  priv->tach_channel[tach_ch].limited_inverse ?
> +				  TACH_ASPEED_INVERS_LIMIT :
> +				  0);
> +
> +	priv->tach_channel[tach_ch].tach_debounce = DEBOUNCE_3_CLK;
> +	regmap_write_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +			  TACH_ASPEED_DEBOUNCE_MASK,
> +			  priv->tach_channel[tach_ch].tach_debounce
> +				  << TACH_ASPEED_DEBOUNCE_BIT);
> +
> +	priv->tach_channel[tach_ch].tach_edge = F2F_EDGES;
> +	regmap_write_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +			  TACH_ASPEED_IO_EDGE_MASK,
> +			  priv->tach_channel[tach_ch].tach_edge
> +				  << TACH_ASPEED_IO_EDGE_BIT);
> +
> +	priv->tach_channel[tach_ch].divisor = DEFAULT_TACH_DIV;
> +	regmap_write_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +			  TACH_ASPEED_CLK_DIV_T_MASK,
> +			  DIV_TO_REG(priv->tach_channel[tach_ch].divisor)
> +				  << TACH_ASPEED_CLK_DIV_BIT);
> +
> +	priv->tach_channel[tach_ch].threshold = 0;
> +	regmap_write_bits(priv->regmap, TACH_ASPEED_CTRL(tach_ch),
> +			  TACH_ASPEED_THRESHOLD_MASK,
> +			  priv->tach_channel[tach_ch].threshold);
> +
> +	priv->tach_channel[tach_ch].pulse_pr = DEFAULT_FAN_PULSE_PR;
> +	priv->tach_channel[tach_ch].min_rpm = DEFAULT_FAN_MIN_RPM;
> +	aspeed_update_tach_sample_period(priv, tach_ch);
> +
> +	priv->tach_channel[tach_ch].max_rpm = DEFAULT_FAN_MAX_RPM;
> +	aspeed_update_tach_polling_period(priv, tach_ch);
> +
> +	aspeed_tach_ch_enable(priv, tach_ch, true);
> +}
> +
> +static int aspeed_tach_create_fan(struct device *dev, struct device_node *child,
> +				  struct aspeed_tach_data *priv)
> +{
> +	u32 tach_channel;
> +	int ret;
> +
> +	ret = of_property_read_u32(child, "reg", &tach_channel);
> +	if (ret)
> +		return ret;
> +
> +	aspeed_create_fan_tach_channel(priv, tach_channel);
> +
> +	return 0;
> +}
> +
> +static void aspeed_tach_reset_assert(void *data)
> +{
> +	struct reset_control *rst = data;
> +
> +	reset_control_assert(rst);
> +}
> +
> +static int aspeed_tach_probe(struct platform_device *pdev)
> +{
> +	struct device *dev = &pdev->dev;
> +	struct device_node *np, *child;
> +	struct aspeed_tach_data *priv;
> +	struct device *hwmon;
> +	struct platform_device *parent_dev;
> +	int ret;
> +
> +	np = dev->parent->of_node;
> +	if (!of_device_is_compatible(np, "aspeed,ast2600-pwm-tach"))
> +		return dev_err_probe(dev, -ENODEV,
> +				     "Unsupported tach device binding\n");
> +
> +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> +	if (!priv)
> +		return -ENOMEM;
> +	priv->dev = &pdev->dev;
> +	priv->tach_channel =
> +		devm_kzalloc(dev,
> +			     TACH_ASPEED_NR_TACHS * sizeof(*priv->tach_channel),
> +			     GFP_KERNEL);
> +
> +	priv->regmap = syscon_node_to_regmap(np);
> +	if (IS_ERR(priv->regmap)) {
> +		dev_err(priv->dev, "Couldn't get regmap\n");
> +		return -ENODEV;

Why replace the reported error ?

> +	}
> +	parent_dev = of_find_device_by_node(np);
> +	priv->clk = devm_clk_get_enabled(&parent_dev->dev, 0);
> +	if (IS_ERR(priv->clk))
> +		return dev_err_probe(dev, PTR_ERR(priv->clk),
> +				     "Couldn't get clock\n");
> +
> +	priv->reset = devm_reset_control_get_shared(&parent_dev->dev, NULL);
> +	if (IS_ERR(priv->reset))
> +		return dev_err_probe(dev, PTR_ERR(priv->reset),
> +				     "Couldn't get reset control\n");
> +
> +	ret = reset_control_deassert(priv->reset);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "Couldn't deassert reset control\n");
> +
> +	ret = devm_add_action_or_reset(dev, aspeed_tach_reset_assert,
> +				       priv->reset);
> +	if (ret)
> +		return ret;
> +
> +	for_each_child_of_node(dev->of_node, child) {
> +		ret = aspeed_tach_create_fan(dev, child, priv);
> +		if (ret) {
> +			of_node_put(child);
> +			return ret;
> +		}
> +	}
> +
> +	hwmon = devm_hwmon_device_register_with_info(
> +		dev, "aspeed_tach", priv, &aspeed_tach_chip_info, NULL);

Please watch out for multi-line alignment (checkpatch --strict); go up to 100
columns if needed.

> +	ret = PTR_ERR_OR_ZERO(hwmon);
> +	if (ret)
> +		return dev_err_probe(dev, ret,
> +				     "Failed to register hwmon device\n");
> +	return 0;
> +}
> +
> +static const struct of_device_id of_stach_match_table[] = {
> +	{
> +		.compatible = "aspeed,ast2600-tach",
> +	},
> +	{},
> +};
> +MODULE_DEVICE_TABLE(of, of_stach_match_table);
> +
> +static struct platform_driver aspeed_tach_driver = {
> +	.probe		= aspeed_tach_probe,
> +	.driver		= {
> +		.name	= "aspeed_tach",
> +		.of_match_table = of_stach_match_table,
> +	},
> +};
> +
> +module_platform_driver(aspeed_tach_driver);
> +
> +MODULE_AUTHOR("Billy Tsai <billy_tsai@aspeedtech.com>");
> +MODULE_DESCRIPTION("Aspeed ast2600 TACH device driver");
> +MODULE_LICENSE("GPL v2");
> +
> -- 
> 2.25.1
>
Billy Tsai Nov. 2, 2022, 6:54 a.m. UTC | #4
Hi Guenter,

On 2022/11/1, 9:15 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:

    On Tue, Nov 01, 2022 at 05:51:56PM +0800, Billy Tsai wrote:
    > > +
    > > +	/* Restart the Tach channel to guarantee the value is fresh */
    > > +	aspeed_tach_ch_enable(priv, fan_tach_ch, false);
    > > +	aspeed_tach_ch_enable(priv, fan_tach_ch, true);

    > Is that really needed ? Doesn't the controller measure values continuously ?

Yes, the controller will measure values continuously by hardware. I will remove it. 
If the user want to get the fresh value, it should be done by the application layer
(e.g. read two times).

    > > +
    > > +	if (ret) {
    > > +		/* return 0 if we didn't get an answer because of timeout*/
    > > +		if (ret == -ETIMEDOUT)
    > > +			return 0;
    > > +		else
    > > +			return ret;

    > else after return is unnecessary, and why would a timeout be be ignored ?

When the user sets the correct fan information (i.e., min_rpm, max_rpm), the read
poll timeout will only occur if the tach pin does not get any signal (i.e. rpm=0).

    > > +	}
    > > +
    > > +	raw_data = val & TACH_ASPEED_VALUE_MASK;
    > > +	/*
    > > +	 * We need the mode to determine if the raw_data is double (from
    > > +	 * counting both edges).
    > > +	 */
    > > +	if (priv->tach_channel[fan_tach_ch].tach_edge == BOTH_EDGES)
    > > +		raw_data <<= 1;
    > > +
    > > +	tach_div = raw_data * (priv->tach_channel[fan_tach_ch].divisor) *
    > > +		   (priv->tach_channel[fan_tach_ch].pulse_pr);
    > > +
    > > +	clk_source = clk_get_rate(priv->clk);
    > > +	dev_dbg(priv->dev, "clk %ld, raw_data %d , tach_div %d\n", clk_source,
    > > +		raw_data, tach_div);
    > > +
    > > +	if (tach_div == 0)
    > > +		return -EDOM;

    > If the fan is off or not connected, would that return an error ?
    > If so, that would be inappropriate; it should return a speed
    > of 0 in that case.

It will be handled by the regmap_read_poll_timeout.

Thanks

Best Regards,
Billy Tsai
Guenter Roeck Nov. 2, 2022, 5:01 p.m. UTC | #5
On Wed, Nov 02, 2022 at 06:54:43AM +0000, Billy Tsai wrote:
> Hi Guenter,
> 
> On 2022/11/1, 9:15 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:
> 
>     On Tue, Nov 01, 2022 at 05:51:56PM +0800, Billy Tsai wrote:
>     > > +
>     > > +	/* Restart the Tach channel to guarantee the value is fresh */
>     > > +	aspeed_tach_ch_enable(priv, fan_tach_ch, false);
>     > > +	aspeed_tach_ch_enable(priv, fan_tach_ch, true);
> 
>     > Is that really needed ? Doesn't the controller measure values continuously ?
> 
> Yes, the controller will measure values continuously by hardware. I will remove it. 
> If the user want to get the fresh value, it should be done by the application layer
> (e.g. read two times).
> 
>     > > +
>     > > +	if (ret) {
>     > > +		/* return 0 if we didn't get an answer because of timeout*/
>     > > +		if (ret == -ETIMEDOUT)
>     > > +			return 0;
>     > > +		else
>     > > +			return ret;
> 
>     > else after return is unnecessary, and why would a timeout be be ignored ?
> 
> When the user sets the correct fan information (i.e., min_rpm, max_rpm), the read
> poll timeout will only occur if the tach pin does not get any signal (i.e. rpm=0).
> 

In that case it would be appropriate to return -ETIMEDOUT to the caller.

Anyway, that should really not happen. Sysfs attributes such as minimum/maximum fan
speed, the number of fan pulses per revolution, or a divider value should only exist
if the chip needs that information, for example to report a fan error/alarm if the
measured speed is out of range or if the chip actually calculates RPM and provides
the result to the driver. Those values should not be necessary (and should not be
used) to calculate some timeout.

>     > > +	}
>     > > +
>     > > +	raw_data = val & TACH_ASPEED_VALUE_MASK;
>     > > +	/*
>     > > +	 * We need the mode to determine if the raw_data is double (from
>     > > +	 * counting both edges).
>     > > +	 */
>     > > +	if (priv->tach_channel[fan_tach_ch].tach_edge == BOTH_EDGES)
>     > > +		raw_data <<= 1;
>     > > +
>     > > +	tach_div = raw_data * (priv->tach_channel[fan_tach_ch].divisor) *
>     > > +		   (priv->tach_channel[fan_tach_ch].pulse_pr);
>     > > +
>     > > +	clk_source = clk_get_rate(priv->clk);
>     > > +	dev_dbg(priv->dev, "clk %ld, raw_data %d , tach_div %d\n", clk_source,
>     > > +		raw_data, tach_div);
>     > > +
>     > > +	if (tach_div == 0)
>     > > +		return -EDOM;
> 
>     > If the fan is off or not connected, would that return an error ?
>     > If so, that would be inappropriate; it should return a speed
>     > of 0 in that case.
> 
> It will be handled by the regmap_read_poll_timeout.

This would only happen if raw_data is 0, or if any of
priv->tach_channel[fan_tach_ch].divisor or priv->tach_channel[fan_tach_ch].pulse_pr
are 0. The latter should never happen, leaving raw_data. If that is 0, I would assume
that there was no fan pulse. That would indicate that the fan isn't running (or maybe
that no fan is connected). Either case that would not warrant returning -EDOM.
If the fan isn't running (no pulse was reported), the reported fan speed should be 0.
If that is an error, the fanX_alarm (or, if available, fanX_min_alarm) should report 1.
Reading the fan speed should never return an error to the caller unless there was
an actual error when reading the value from the hardware.

Thanks,
Guenter
Billy Tsai Nov. 3, 2022, 3:52 a.m. UTC | #6
On 2022/11/3, 1:01 AM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:

    On Wed, Nov 02, 2022 at 06:54:43AM +0000, Billy Tsai wrote:
    >   > Hi Guenter,
    >   > 
    >   > On 2022/11/1, 9:15 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:
    >   > 
    >   >     On Tue, Nov 01, 2022 at 05:51:56PM +0800, Billy Tsai wrote:
    >   >     > > +
    >   >     > > +	/* Restart the Tach channel to guarantee the value is fresh */
    >   >     > > +	aspeed_tach_ch_enable(priv, fan_tach_ch, false);
    >   >     > > +	aspeed_tach_ch_enable(priv, fan_tach_ch, true);
    >   > 
    >   >     > Is that really needed ? Doesn't the controller measure values continuously ?
    >   > 
    >   > Yes, the controller will measure values continuously by hardware. I will remove it. 
    >   > If the user want to get the fresh value, it should be done by the application layer
    >   > (e.g. read two times).
    >   > 
    >   >     > > +
    >   >     > > +	if (ret) {
    >   >     > > +		/* return 0 if we didn't get an answer because of timeout*/
    >   >     > > +		if (ret == -ETIMEDOUT)
    >   >     > > +			return 0;
    >   >     > > +		else
    >   >     > > +			return ret;
    >   > 
    >   >     > else after return is unnecessary, and why would a timeout be be ignored ?
    >   > 
    >   > When the user sets the correct fan information (i.e., min_rpm, max_rpm), the read
    >   > poll timeout will only occur if the tach pin does not get any signal (i.e. rpm=0).
    >   > 

    >   In that case it would be appropriate to return -ETIMEDOUT to the caller.

    >   Anyway, that should really not happen. Sysfs attributes such as minimum/maximum fan
    >   speed, the number of fan pulses per revolution, or a divider value should only exist
    >   if the chip needs that information, for example to report a fan error/alarm if the
    >   measured speed is out of range or if the chip actually calculates RPM and provides
    >   the result to the driver. Those values should not be necessary (and should not be
    >   used) to calculate some timeout.

Can't I use a min/max RPM to let the driver know a reasonable timeout/polling period when
the driver is trying to get RPM?
Beacause that our tach controller have the falg to indicates the hardware detected the change
in the input signal. I need the proper timout to rule out slow RPMs.

Thanks

Best Regards,
Billy Tsai
Guenter Roeck Nov. 3, 2022, 4:30 a.m. UTC | #7
On Thu, Nov 03, 2022 at 03:52:59AM +0000, Billy Tsai wrote:
> 
> Can't I use a min/max RPM to let the driver know a reasonable timeout/polling period when
> the driver is trying to get RPM?
> Beacause that our tach controller have the falg to indicates the hardware detected the change
> in the input signal. I need the proper timout to rule out slow RPMs.
> 

If the chip measures the fan speed continuously, why would that ever be a
problem, and why wait in the first place instead of just taking the most
recent result ?

Pretty much every other driver is doing that, so I really don't understand
why that would not work here.

Guenter
Billy Tsai Nov. 3, 2022, 5:40 a.m. UTC | #8
On 2022/11/3, 12:30 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:

    On Thu, Nov 03, 2022 at 03:52:59AM +0000, Billy Tsai wrote:
    > > 
    > > Can't I use a min/max RPM to let the driver know a reasonable timeout/polling period when
    > > the driver is trying to get RPM?
    > > Beacause that our tach controller have the falg to indicates the hardware detected the change
    > > in the input signal. I need the proper timout to rule out slow RPMs.

    > If the chip measures the fan speed continuously, why would that ever be a
    > problem, and why wait in the first place instead of just taking the most
    > recent result ?

    > Pretty much every other driver is doing that, so I really don't understand
    > why that would not work here.

When the fan speed drop from a very fast RPM to a very slow RPM. Especially when it is close to stopping.
The most recent result will be no meaningful value. The slower RPM needs more time to sample it. E.g., If
we want to measure the fan with 600 RPM, the controller needs at least 100ms. During this time period, we
will always get the wrong value. So, our tach controller have the flag to avoid this problem:
TACH_ASPEED_VALUE_UPDATE: tach value updated since last read
This flag will be set when the controller detected the change of the signal and clear by read it.
In order to use this flag, the controller needs the proper timeout based on minimum RPM to avoid waiting forever.

Best Regards,
Billy Tsai
Guenter Roeck Nov. 3, 2022, 2:30 p.m. UTC | #9
On Thu, Nov 03, 2022 at 05:40:44AM +0000, Billy Tsai wrote:
> On 2022/11/3, 12:30 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:
> 
>     On Thu, Nov 03, 2022 at 03:52:59AM +0000, Billy Tsai wrote:
>     > > 
>     > > Can't I use a min/max RPM to let the driver know a reasonable timeout/polling period when
>     > > the driver is trying to get RPM?
>     > > Beacause that our tach controller have the falg to indicates the hardware detected the change
>     > > in the input signal. I need the proper timout to rule out slow RPMs.
> 
>     > If the chip measures the fan speed continuously, why would that ever be a
>     > problem, and why wait in the first place instead of just taking the most
>     > recent result ?
> 
>     > Pretty much every other driver is doing that, so I really don't understand
>     > why that would not work here.
> 
> When the fan speed drop from a very fast RPM to a very slow RPM. Especially when it is close to stopping.
> The most recent result will be no meaningful value. The slower RPM needs more time to sample it. E.g., If
> we want to measure the fan with 600 RPM, the controller needs at least 100ms. During this time period, we
> will always get the wrong value. So, our tach controller have the flag to avoid this problem:
> TACH_ASPEED_VALUE_UPDATE: tach value updated since last read
> This flag will be set when the controller detected the change of the signal and clear by read it.
> In order to use this flag, the controller needs the proper timeout based on minimum RPM to avoid waiting forever.
> 

I am not going to accept this patch as-is. If userspace wants to have
values accurate down to ms, this kind of approach is just wrong. Users
will have to live with the fact that measurements may be a bit (in the 
< 1 second range) out of date. Many older drivers even implement code
which avoids reading registers again for a second or longer. Older
temperature sensors may take several seconds to provide new readings.
That is not a reason to block userspace until a new value is available.
I do not see that as a problem. In my opinion it is much more of a
problem if the driver returns a completely bad value such as 0 or even
an error code because its software parameters did not match reality and
the driver didn't wait long enough for a new value. That would be _much_
worse than providing a value which is a few 100 ms out of date, and your
code is vulnerable to that problem.

Besides, for a fan to reduce its speed that quickly, it has to be manually
stopped. Normally fans take several seconds to stop if power is taken away
completely. Your code is adding a lot of complexity (and unnecessary
attributes) for no good reason.

Guenter
Billy Tsai Nov. 8, 2022, 7:17 a.m. UTC | #10
On 2022/11/3, 10:31 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:

    On Thu, Nov 03, 2022 at 05:40:44AM +0000, Billy Tsai wrote:
    > > On 2022/11/3, 12:30 PM, "Guenter Roeck" <groeck7@gmail.com on behalf of linux@roeck-us.net> wrote:
    > > 
    > >     On Thu, Nov 03, 2022 at 03:52:59AM +0000, Billy Tsai wrote:
    > >     > > 
    > >     > > Can't I use a min/max RPM to let the driver know a reasonable timeout/polling period when
    > >     > > the driver is trying to get RPM?
    > >     > > Beacause that our tach controller have the falg to indicates the hardware detected the change
    > >     > > in the input signal. I need the proper timout to rule out slow RPMs.
    > > 
    > >     > If the chip measures the fan speed continuously, why would that ever be a
    > >     > problem, and why wait in the first place instead of just taking the most
    > >     > recent result ?
    > > 
    > >     > Pretty much every other driver is doing that, so I really don't understand
    > >     > why that would not work here.
    > > 
    > > When the fan speed drop from a very fast RPM to a very slow RPM. Especially when it is close to stopping.
    > > The most recent result will be no meaningful value. The slower RPM needs more time to sample it. E.g., If
    > > we want to measure the fan with 600 RPM, the controller needs at least 100ms. During this time period, we
    > > will always get the wrong value. So, our tach controller have the flag to avoid this problem:
    > > TACH_ASPEED_VALUE_UPDATE: tach value updated since last read
    > > This flag will be set when the controller detected the change of the signal and clear by read it.
    > > In order to use this flag, the controller needs the proper timeout based on minimum RPM to avoid waiting forever.
    > > 

    > I am not going to accept this patch as-is. If userspace wants to have
    > values accurate down to ms, this kind of approach is just wrong. Users
    > will have to live with the fact that measurements may be a bit (in the 
    > < 1 second range) out of date. Many older drivers even implement code
    > which avoids reading registers again for a second or longer. Older
    > temperature sensors may take several seconds to provide new readings.
    > That is not a reason to block userspace until a new value is available.
    > I do not see that as a problem. In my opinion it is much more of a
    > problem if the driver returns a completely bad value such as 0 or even
    > an error code because its software parameters did not match reality and
    > the driver didn't wait long enough for a new value. That would be _much_
    > worse than providing a value which is a few 100 ms out of date, and your
    > code is vulnerable to that problem.

    > Besides, for a fan to reduce its speed that quickly, it has to be manually
    > stopped. Normally fans take several seconds to stop if power is taken away
    > completely. Your code is adding a lot of complexity (and unnecessary
    > attributes) for no good reason.

Thanks for your detailed explanations.
I will remove the min and max RPMs and return the tach value directly without any polling or delay.

Best Regards,
Billy Tsai