[PATCH v3 4/4] hwmon: Support Aspeed AST2600 PWM/Fan tachometer
Troy Lee
troy_lee at aspeedtech.com
Tue Jan 26 21:05:25 AEDT 2021
Hi Andrew,
The 01/22/2021 12:38, Andrew Jeffery wrote:
> Hi Troy,
>
> On Mon, 18 Jan 2021, at 17:20, Troy Lee wrote:
> > Add Aspeed AST2600 PWM/Fan tacho driver. AST2600 has 16 PWM channel and
> > 16 FAN tacho channel.
> >
> > Changes since v2:
> > - declare local function as static function
> >
> > Changes since v1:
> > - fixed review comments
> > - fixed double-looped calculation of div_h and div_l
> > - moving configuration to device tree
> > - register hwmon driver with devm_hwmon_device_register_with_info()
> >
> > Signed-off-by: Troy Lee <troy_lee at aspeedtech.com>
> > Reported-by: kernel test robot <lkp at intel.com>
> > ---
...
> > diff --git a/drivers/hwmon/aspeed2600-pwm-tacho.c
> > b/drivers/hwmon/aspeed2600-pwm-tacho.c
> > new file mode 100644
> > index 000000000000..00ff100db92f
> > --- /dev/null
> > +++ b/drivers/hwmon/aspeed2600-pwm-tacho.c
> > @@ -0,0 +1,756 @@
> > +// SPDX-License-Identifier: GPL-2.0-or-later
> > +/*
> > + * Copyright (C) ASPEED Technology Inc.
> > + *
> > + * This program is free software; you can redistribute it and/or modify
> > + * it under the terms of the GNU General Public License version 2 or
> > later as
> > + * published by the Free Software Foundation.
> > + */
>
> The license is captured by the SPDX marker above. This summary text should be dropped (though retain the copyright line).
>
> > +
> > +#include <linux/clk.h>
> > +#include <linux/errno.h>
> > +#include <linux/gpio/consumer.h>
> > +#include <linux/delay.h>
> > +#include <linux/hwmon.h>
> > +#include <linux/hwmon-sysfs.h>
> > +#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/sysfs.h>
> > +#include <linux/reset.h>
> > +#include <linux/regmap.h>
> > +#include <linux/thermal.h>
> > +/**********************************************************
> > + * PWM HW register offset define
> > + *********************************************************/
>
> Banner comments like this generally aren't preferred. If you think the comment text itself is necessary, just do:
>
> /* PWM HW register offset define */
>
> However, the macro names should indicate what we're defining anyway, so I feel we can remove it entirely without too much loss
>
I'll remove the redundant description and comments.
> > +/* PWM Control Register */
> > +#define ASPEED_PWM_CTRL_CH(ch) (((ch) * 0x10) + 0x00)
> > +/* PWM Duty Cycle Register */
> > +#define ASPEED_PWM_DUTY_CYCLE_CH(ch) (((ch) * 0x10) + 0x04)
> > +/* TACH Control Register */
> > +#define ASPEED_TACHO_CTRL_CH(ch) (((ch) * 0x10) + 0x08)
> > +/* TACH Status Register */
> > +#define ASPEED_TACHO_STS_CH(x) (((x) * 0x10) + 0x0C)
> > +
> > +/**********************************************************
> > + * PWM register Bit field
> > + *********************************************************/
> > +/*PWM_CTRL */
> > +#define PWM_LOAD_SEL_AS_WDT_BIT (19) /* load selection as WDT */
>
> The trailing comments don't really add any further information in the context of the macro name. Let's remove these as well.
>
> > +#define PWM_DUTY_LOAD_AS_WDT_EN BIT(18) /* enable PWM duty load as
> > WDT */
> > +#define PWM_DUTY_SYNC_DIS BIT(17) /* disable PWM duty sync */
> > +#define PWM_CLK_ENABLE BIT(16) /* enable PWM clock */
> > +#define PWM_LEVEL_OUTPUT BIT(15) /* output PWM level */
> > +#define PWM_INVERSE BIT(14) /* inverse PWM pin */
> > +#define PWM_OPEN_DRAIN_EN BIT(13) /* enable open-drain */
> > +#define PWM_PIN_EN BIT(12) /* enable PWM pin */
> > +#define PWM_CLK_DIV_H_MASK (0xf << 8) /* PWM clock division H bit
> > [3:0] */
> > +#define PWM_CLK_DIV_L_MASK (0xff) /* PWM clock division H bit [3:0] */
> > +/* [19] */
> > +#define LOAD_SEL_FALLING 0
> > +#define LOAD_SEL_RIGING 1
>
> It's much easier to track these macros if you place them alongside the associated register, and in a way that makes them self-documenting (i.e. we remove the need for the banner comments). Often the bit/mask macros are indented to help with visuals, for example:
>
> #define ASPEED_PWM_CTRL_CH(ch) (((ch) * 0x10) + 0x00)
> #define PWM_DUTY_LOAD_AS_WDT_EN BIT(18)
> #define PWM_DUTY_SYNC_DIS BIT(17)
> ...
>
> #define ASPEED_PWM_DUTY_CYCLE_CH(ch) (((ch) * 0x10) + 0x04)
> #define PWM_PERIOD_BIT (24)
> #define PWM_PERIOD_BIT_MASK (0xff << 24)
> ...
>
> etcetera. It also helps to make the name of the bitfield macros use a similar prefix to name of the associated register macro.
>
Yes, this will increase readibility, I'll re-order these in v4.
> > +
> > +/*PWM_DUTY_CYCLE */
> > +#define PWM_PERIOD_BIT (24) /* pwm period bit [7:0] */
> > +#define PWM_PERIOD_BIT_MASK (0xff << 24) /* pwm period bit [7:0] */
> > +#define PWM_RISING_FALLING_AS_WDT_BIT (16)
> > +#define PWM_RISING_FALLING_AS_WDT_MASK (0xff << 16) /* rising/falling
> > point bit [7:0] as WDT */
> > +#define PWM_RISING_FALLING_MASK (0xffff)
> > +#define PWM_FALLING_POINT_BIT (8) /* pwm falling point bit [7:0] */
> > +#define PWM_RISING_POINT_BIT (0) /* pwm rising point bit [7:0] */
> > +/* [31:24] */
> > +#define DEFAULT_PWM_PERIOD 0xff
> > +
> > +/*PWM_TACHO_CTRL */
> > +#define TACHO_IER BIT(31) /* enable tacho interrupt */
> > +#define TACHO_INVERS_LIMIT BIT(30) /* inverse tacho limit comparison
> > */
> > +#define TACHO_LOOPBACK BIT(29) /* tacho loopback */
> > +#define TACHO_ENABLE BIT(28) /* enable tacho */
> > +#define TACHO_DEBOUNCE_MASK (0x3 << 26) /* tacho de-bounce */
> > +#define TACHO_DEBOUNCE_BIT (26) /* tacho de-bounce */
> > +#define TECHIO_EDGE_MASK (0x3 << 24) /* tacho edge */
> > +#define TECHIO_EDGE_BIT (24) /* tacho edge */
> > +#define TACHO_CLK_DIV_T_MASK (0xf << 20)
> > +#define TACHO_CLK_DIV_BIT (20)
> > +#define TACHO_THRESHOLD_MASK (0xfffff) /* tacho threshold bit */
> > +/* [27:26] */
> > +#define DEBOUNCE_3_CLK 0x00 /* 10b */
> > +#define DEBOUNCE_2_CLK 0x01 /* 10b */
> > +#define DEBOUNCE_1_CLK 0x02 /* 10b */
> > +#define DEBOUNCE_0_CLK 0x03 /* 10b */
> > +/* [25:24] */
> > +#define F2F_EDGES 0x00 /* 10b */
> > +#define R2R_EDGES 0x01 /* 10b */
> > +#define BOTH_EDGES 0x02 /* 10b */
> > +/* [23:20] */
> > +/* Cover rpm range 5~5859375 */
> > +#define DEFAULT_TACHO_DIV 5
> > +
> > +/*PWM_TACHO_STS */
> > +#define TACHO_ISR BIT(31) /* interrupt status and clear */
> > +#define PWM_OUT BIT(25) /* pwm_out */
> > +#define PWM_OEN BIT(24) /* pwm_oeN */
> > +#define TACHO_DEB_INPUT BIT(23) /* tacho deB input */
> > +#define TACHO_RAW_INPUT BIT(22) /* tacho raw input */
> > +#define TACHO_VALUE_UPDATE BIT(21) /* tacho value updated since the
> > last read */
> > +#define TACHO_FULL_MEASUREMENT BIT(20) /* tacho full measurement */
> > +#define TACHO_VALUE_MASK 0xfffff /* tacho value bit [19:0] */
> > +/**********************************************************
> > + * Software setting
> > + *********************************************************/
> > +#define DEFAULT_TARGET_PWM_FREQ 25000
> > +#define DEFAULT_FAN_PULSE_PR 2
> > +#define DEFAULT_WDT_RISING_FALLING_PT 0x10
> > +#define DEFAULT_RISING_PT 0x00
> > +#define DEFAULT_FALLING_PT 0x0a
> > +#define MAX_CDEV_NAME_LEN 16
> > +
> > +struct aspeed_pwm_channel_params {
> > + int target_freq;
> > + int pwm_freq;
> > + int load_wdt_rising_falling_pt;
> > + int load_wdt_selection; /* 0: rising , 1: falling */
> > + int load_wdt_enable;
> > + int duty_sync_disable;
> > + int inverse_pin;
> > + u8 falling;
> > +};
> > +
> > +static struct aspeed_pwm_channel_params default_pwm_params[16];
> > +
> > +struct aspeed_tacho_channel_params {
> > + int limited_inverse;
> > + u16 threshold;
> > + u8 tacho_edge;
> > + u8 tacho_debounce;
> > + u8 pulse_pr;
> > + u32 divide;
> > +};
> > +
> > +static struct aspeed_tacho_channel_params default_tacho_params[16];
> > +
> > +struct aspeed_pwm_tachometer_data {
> > + struct regmap *regmap;
>
> What's the reason for using a regmap? For MMIO the benefit is that the API provides some locking for when regmap is used as a syscon, but that doesn't appear to be how it's used here? regmap generates a bunch of overhead compared to readl()/writel().
>
Nope, the readl()/writel() will work as well.
> > + unsigned long clk_freq;
> > + struct reset_control *reset;
> > + bool pwm_present[16];
> > + bool fan_tach_present[16];
> > + struct aspeed_pwm_channel_params *pwm_channel;
> > + struct aspeed_tacho_channel_params *tacho_channel;
> > + /* for thermal */
> > + struct aspeed_cooling_device *cdev[8];
> > +};
> > +
> > +struct aspeed_cooling_device {
> > + char name[16];
> > + struct aspeed_pwm_tachometer_data *priv;
> > + struct thermal_cooling_device *tcdev;
> > + int pwm_channel;
> > + u8 *cooling_levels;
> > + u8 max_state;
> > + u8 cur_state;
> > +};
> > +
> > +static int regmap_aspeed_pwm_tachometer_reg_write(void *context,
> > unsigned int reg,
> > + unsigned int val)
> > +{
> > + void __iomem *regs = (void __iomem *)context;
> > +
> > + writel(val, regs + reg);
> > + return 0;
> > +}
> > +
> > +static int regmap_aspeed_pwm_tachometer_reg_read(void *context,
> > unsigned int reg,
> > + unsigned int *val)
> > +{
> > + void __iomem *regs = (void __iomem *)context;
> > +
> > + *val = readl(regs + reg);
> > + return 0;
> > +}
> > +
> > +static const struct regmap_config aspeed_pwm_tachometer_regmap_config
> > = {
> > + .reg_bits = 32,
> > + .val_bits = 32,
> > + .reg_stride = 4,
> > + .max_register = 0x100,
> > + .reg_write = regmap_aspeed_pwm_tachometer_reg_write,
> > + .reg_read = regmap_aspeed_pwm_tachometer_reg_read,
>
> These just do an MMIO access, do we really need our own read/write definitions?
>
> > + .fast_io = true,
> > +};
> > +
> > +static void aspeed_set_pwm_channel_enable(struct regmap *regmap, u8
> > pwm_channel,
> > + bool enable)
> > +{
> > + regmap_update_bits(regmap, ASPEED_PWM_CTRL_CH(pwm_channel),
> > + PWM_CLK_ENABLE | PWM_PIN_EN,
> > + enable ? PWM_CLK_ENABLE | PWM_PIN_EN : 0);
>
> If the motivation for regmap is something like regmap_update_bits(), you can alternatively use the FIELD_*() macros from linux/bitfield.h
>
Got it.
> > +}
> > +
> > +static void aspeed_set_fan_tach_ch_enable(struct
> > aspeed_pwm_tachometer_data *priv, u8 fan_tach_ch,
> > + bool enable, u32 tacho_div)
> > +{
> > + u32 reg_value;
> > +
> > + if (enable) {
> > + /* divide = 2^(tacho_div*2) */
> > + priv->tacho_channel[fan_tach_ch].divide = 1 << (tacho_div << 1);
> > +
> > + reg_value = TACHO_ENABLE |
> > + (priv->tacho_channel[fan_tach_ch].tacho_edge << TECHIO_EDGE_BIT) |
> > + (tacho_div << TACHO_CLK_DIV_BIT) |
> > + (priv->tacho_channel[fan_tach_ch].tacho_debounce <<
> > TACHO_DEBOUNCE_BIT);
> > +
> > + if (priv->tacho_channel[fan_tach_ch].limited_inverse)
> > + reg_value |= TACHO_INVERS_LIMIT;
> > +
> > + if (priv->tacho_channel[fan_tach_ch].threshold)
> > + reg_value |= (TACHO_IER |
> > priv->tacho_channel[fan_tach_ch].threshold);
>
> I feel like this could be cleaned up a bit with:
>
> const struct aspeed_tacho_channel_params *tacho = &priv->tacho_channel[fan_tach_ch];
>
> and replacing the expression with tacho throughout.
>
After replacing the expression, the code looks cleaner.
> > +
> > + regmap_write(priv->regmap, ASPEED_TACHO_CTRL_CH(fan_tach_ch),
> > reg_value);
> > + } else
> > + regmap_update_bits(priv->regmap, ASPEED_TACHO_CTRL_CH(fan_tach_ch),
> > TACHO_ENABLE, 0);
> > +}
> > +
> > +/*
> > + * The PWM frequency = HCLK(200Mhz) / (clock division L bit *
> > + * clock division H bit * (period bit + 1))
> > + */
> > +static void aspeed_set_pwm_channel_fan_ctrl(struct device *dev,
> > + struct aspeed_pwm_tachometer_data *priv,
> > + u8 index, u8 fan_ctrl)
> > +{
> > + u32 duty_value, ctrl_value;
> > + u32 target_div, div_h, div_l, cal_freq;
> > + u32 tmp_div_h, tmp_div_l, diff, min_diff = INT_MAX;
> > +
> > + if (fan_ctrl == 0) {
> > + aspeed_set_pwm_channel_enable(priv->regmap, index, false);
> > + return;
> > + }
> > +
> > + cal_freq = priv->clk_freq / (DEFAULT_PWM_PERIOD + 1);
>
> Is this always correct if we're relying on a default value? Or are we using DEFAULT_PWM_PERIOD because its value is the maximum period? Using a macro with DEFAULT in the name certainly makes me feel less sure that the code is right.
>
I'll check again about this.
> > + target_div = DIV_ROUND_UP(cal_freq,
> > priv->pwm_channel[index].target_freq);
> > +
> > + /* calculate for target frequence */
> > + for (tmp_div_h = 0; tmp_div_h < 0x10; tmp_div_h++) {
> > + tmp_div_l = target_div / BIT(tmp_div_h) - 1;
> > +
> > + if (tmp_div_l < 0 || tmp_div_l > 255)
> > + continue;
> > +
> > + diff = priv->pwm_channel[index].target_freq - cal_freq /
> > (BIT(tmp_div_h) * (tmp_div_l + 1));
> > + if (abs(diff) < abs(min_diff)) {
> > + min_diff = diff;
> > + div_l = tmp_div_l;
> > + div_h = tmp_div_h;
> > +
> > + if (diff == 0)
> > + break;
> > + }
> > + }
>
> I've spent some time thinking about this and haven't come up with anything that's concretely better. It wasn't fun to read though :(
>
Yep, me too. The intention of this segment is finding a valid pair of DIV_H and DIV_L, and it will be used as clock prescaler.
> > +
> > + priv->pwm_channel[index].pwm_freq = cal_freq / (BIT(div_h) * (div_l +
> > 1));
> > + dev_info(dev, "div h %x, l : %x pwm out clk %d\n", div_h, div_l,
> > + priv->pwm_channel[index].pwm_freq);
> > + dev_info(dev, "hclk %ld, target pwm freq %d, real pwm freq %d\n",
> > priv->clk_freq,
> > + priv->pwm_channel[index].target_freq,
> > priv->pwm_channel[index].pwm_freq);
>
> These should be dev_dbg()
>
Updated.
> > +
> > + ctrl_value = (div_h << 8) | div_l;
> > +
> > + duty_value = (DEFAULT_PWM_PERIOD << PWM_PERIOD_BIT) |
> > + (0 << PWM_RISING_POINT_BIT) | (fan_ctrl << PWM_FALLING_POINT_BIT);
> > +
> > + if (priv->pwm_channel[index].load_wdt_enable) {
> > + ctrl_value |= PWM_DUTY_LOAD_AS_WDT_EN;
> > + ctrl_value |= priv->pwm_channel[index].load_wdt_selection <<
> > PWM_LOAD_SEL_AS_WDT_BIT;
> > + duty_value |= (priv->pwm_channel[index].load_wdt_rising_falling_pt
> > << PWM_RISING_FALLING_AS_WDT_BIT);
> > + }
> > +
> > + if (priv->pwm_channel[index].inverse_pin)
> > + ctrl_value |= PWM_INVERSE;
> > + if (priv->pwm_channel[index].duty_sync_disable)
> > + ctrl_value |= PWM_DUTY_SYNC_DIS;
>
> Similar to my comment above about tacho, I think this can be cleaned up a bit with:
>
> const struct aspeed_pwm_channel_params *pwm = &priv->pwm_channel[index];
>
> and replacing the expression with pwm throughout.
>
Updated.
> > +
> > + regmap_write(priv->regmap, ASPEED_PWM_DUTY_CYCLE_CH(index),
> > duty_value);
> > + regmap_write(priv->regmap, ASPEED_PWM_CTRL_CH(index), ctrl_value);
> > +
> > + aspeed_set_pwm_channel_enable(priv->regmap, index, true);
> > +}
> > +
> > +static int aspeed_get_fan_tach_ch_rpm(struct device *dev, struct
> > aspeed_pwm_tachometer_data *priv,
> > + u8 fan_tach_ch)
> > +{
> > + u32 raw_data, tach_div, clk_source, val;
> > + int ret;
> > +
> > + ret = regmap_read_poll_timeout(priv->regmap,
> > + ASPEED_TACHO_STS_CH(fan_tach_ch),
> > + val,
> > + (val & TACHO_FULL_MEASUREMENT),
> > + 1000,
> > + 10000);
>
> Could just use readl_poll_timeout()?
>
Updated accordingly.
> > +
> > + /* return -ETIMEDOUT if we didn't get an answer. */
> > + if (ret)
> > + return ret;
> > +
> > + raw_data = val & TACHO_VALUE_MASK;
> > + if (raw_data == 0xfffff)
> > + return 0;
> > +
> > + raw_data += 1;
> > +
> > + /*
> > + * We need the mode to determine if the raw_data is double (from
> > + * counting both edges).
> > + */
> > + tach_div = raw_data * (priv->tacho_channel[fan_tach_ch].divide) *
> > (priv->tacho_channel[fan_tach_ch].pulse_pr);
>
> Again, lets extract the channel pointer to clean this up a little.
>
Updated.
> > +
> > + dev_dbg(dev, "clk %ld, raw_data %d , tach_div %d\n", priv->clk_freq,
> > raw_data, tach_div);
> > + clk_source = priv->clk_freq;
> > +
> > + return ((clk_source / tach_div) * 60);
> > +
> > +}
> > +
> > +static int set_pwm(struct device *dev, int index, long fan_ctrl)
> > +{
> > + struct aspeed_pwm_tachometer_data *priv = dev_get_drvdata(dev);
> > + u8 org_falling = priv->pwm_channel[index].falling;
> > +
> > + if (fan_ctrl > DEFAULT_PWM_PERIOD || fan_ctrl < 0)
> > + return -EINVAL;
> > +
> > + if (priv->pwm_channel[index].falling == fan_ctrl)
> > + return 0;
> > +
> > + priv->pwm_channel[index].falling = fan_ctrl;
>
> My understanding is the value of fan_ctrl is the value that userspace has written to the pwm[1-*] attribute in sysfs. The name 'falling' in struct aspeed_pwm_channel_params is not at all intuitive to me. I understand the hardware calls it something along these lines, but is there a better name? "period"? "duty"?
>
Good suggestion, I'll update accordingly.
> > +
> > + if (fan_ctrl == 0)
> > + aspeed_set_pwm_channel_enable(priv->regmap, index, false);
> > + else {
> > + if (fan_ctrl == DEFAULT_PWM_PERIOD)
> > + regmap_update_bits(priv->regmap,
> > + ASPEED_PWM_DUTY_CYCLE_CH(index),
> > + GENMASK(15, 0), 0);
> > + else
> > + regmap_update_bits(priv->regmap,
> > + ASPEED_PWM_DUTY_CYCLE_CH(index),
> > + GENMASK(15, 8),
>
> The asymmetry between these masks caught my eye, and I guess I understand what's going on a bit more with 'falling' at least. The hardware is definitely flexible :)
>
> > + (fan_ctrl << PWM_FALLING_POINT_BIT));
> > + }
> > +
> > + if (org_falling == 0)
>
> Do we have to assign .falling member before this point? Can we get rid of org_falling by assigning .falling after this test?
>
Yes, good suggestion.
> > + aspeed_set_pwm_channel_enable(priv->regmap, index, true);
> > +
> > + return 0;
> > +}
> > +
> > +static void aspeed_create_pwm_channel(struct device *dev, struct
> > aspeed_pwm_tachometer_data *priv,
> > + u8 pwm_channel)
> > +{
> > + priv->pwm_present[pwm_channel] = true;
>
> Is there a reason we can't have the present state as a member of the params struct?
>
Nope. I'll move the present into param struct.
> > +
> > + /* use default */
> > + aspeed_set_pwm_channel_fan_ctrl(dev,
> > + priv,
> > + pwm_channel,
> > + priv->pwm_channel[pwm_channel].falling);
> > +}
> > +
> > +static void aspeed_create_fan_tach_channel(struct
> > aspeed_pwm_tachometer_data *priv,
> > + u8 *fan_tach_ch, int count,
> > + u32 fan_pulse_pr, u32 tacho_div)
> > +{
> > + u8 val, index;
> > +
> > + for (val = 0; val < count; val++) {
> > + index = fan_tach_ch[val];
> > + priv->fan_tach_present[index] = true;
>
> Same query as pwm_present: Can we not have this as member of the params struct?
>
Same as pwm.
> > + priv->tacho_channel[index].pulse_pr = fan_pulse_pr;
> > + priv->tacho_channel[index].limited_inverse = 0;
> > + priv->tacho_channel[index].threshold = 0;
> > + priv->tacho_channel[index].tacho_edge = F2F_EDGES;
> > + priv->tacho_channel[index].tacho_debounce = DEBOUNCE_3_CLK;
> > + aspeed_set_fan_tach_ch_enable(priv, index, true, tacho_div);
> > + }
> > +}
> > +
> > +static int
> > +aspeed_pwm_cz_get_max_state(struct thermal_cooling_device *tcdev,
> > + unsigned long *state)
> > +{
> > + struct aspeed_cooling_device *cdev = tcdev->devdata;
> > +
> > + *state = cdev->max_state;
> > +
> > + return 0;
> > +}
> > +
> > +static int
> > +aspeed_pwm_cz_get_cur_state(struct thermal_cooling_device *tcdev,
> > + unsigned long *state)
> > +{
> > + struct aspeed_cooling_device *cdev = tcdev->devdata;
> > +
> > + *state = cdev->cur_state;
> > +
> > + return 0;
> > +}
> > +
> > +static int
> > +aspeed_pwm_cz_set_cur_state(struct thermal_cooling_device *tcdev,
> > + unsigned long state)
> > +{
> > + struct aspeed_cooling_device *cdev = tcdev->devdata;
> > +
> > + if (state > cdev->max_state)
> > + return -EINVAL;
> > +
> > + cdev->cur_state = state;
> > + cdev->priv->pwm_channel[cdev->pwm_channel].falling =
> > + cdev->cooling_levels[cdev->cur_state];
> > + aspeed_set_pwm_channel_fan_ctrl(&tcdev->device, cdev->priv,
> > cdev->pwm_channel,
> > + cdev->cooling_levels[cdev->cur_state]);
> > +
> > + return 0;
> > +}
> > +
> > +static const struct thermal_cooling_device_ops aspeed_pwm_cool_ops = {
> > + .get_max_state = aspeed_pwm_cz_get_max_state,
> > + .get_cur_state = aspeed_pwm_cz_get_cur_state,
> > + .set_cur_state = aspeed_pwm_cz_set_cur_state,
> > +};
> > +
> > +static int aspeed_create_pwm_cooling(struct device *dev,
> > + struct device_node *child,
> > + struct aspeed_pwm_tachometer_data *priv,
> > + u32 pwm_channel, u8 num_levels)
> > +{
> > + int ret;
> > + struct aspeed_cooling_device *cdev;
> > +
> > + cdev = devm_kzalloc(dev, sizeof(*cdev), GFP_KERNEL);
> > + if (!cdev)
> > + return -ENOMEM;
> > +
> > + cdev->cooling_levels = devm_kzalloc(dev, num_levels, GFP_KERNEL);
> > + if (!cdev->cooling_levels)
> > + return -ENOMEM;
> > +
> > + cdev->max_state = num_levels - 1;
> > + ret = of_property_read_u8_array(child, "cooling-levels",
> > + cdev->cooling_levels,
> > + num_levels);
> > + if (ret) {
> > + dev_err(dev, "Property 'cooling-levels' cannot be read.\n");
> > + return ret;
> > + }
> > + snprintf(cdev->name, MAX_CDEV_NAME_LEN, "%s%d", child->name,
> > pwm_channel);
> > +
> > + cdev->tcdev = thermal_of_cooling_device_register(child,
> > + cdev->name,
> > + cdev,
> > + &aspeed_pwm_cool_ops);
> > + if (IS_ERR(cdev->tcdev))
> > + return PTR_ERR(cdev->tcdev);
> > +
> > + cdev->priv = priv;
> > + cdev->pwm_channel = pwm_channel;
> > +
> > + priv->cdev[pwm_channel] = cdev;
> > +
> > + return 0;
> > +}
>
> I'm not super familiar with the cooling zones stuff, so I'll leave that for someone else to comment on :)
>
> > +
> > +static int aspeed_pwm_create_fan(struct device *dev,
> > + struct device_node *child,
> > + struct aspeed_pwm_tachometer_data *priv)
> > +{
> > + u8 *fan_tach_ch;
> > + u32 fan_pulse_pr;
> > + u32 tacho_div;
> > + u32 pwm_channel;
> > + u32 target_pwm_freq = 0;
> > + u8 val;
> > + int ret, count;
> > +
> > + ret = of_property_read_u32(child, "reg", &pwm_channel);
> > + if (ret)
> > + return ret;
> > + else if (pwm_channel > 0x0f)
> > + return -EINVAL;
> > +
> > + ret = of_property_read_u32(child, "aspeed,pwm-freq-hz",
> > &target_pwm_freq);
> > + if (ret)
> > + target_pwm_freq = DEFAULT_TARGET_PWM_FREQ;
> > +
> > + priv->pwm_channel[pwm_channel].target_freq = target_pwm_freq;
>
> I made some comments on the binding patch, but I'll repeat some of that here.
>
> What are your thoughts on reusing a property like 'bus-frequency' here?
>
> > +
> > + priv->pwm_channel[pwm_channel].load_wdt_enable =
> > of_property_read_bool(child,
> > + "aspeed,enable-pwm-duty-load-wdt");
>
> This isn't documented in the binding.
>
> > + priv->pwm_channel[pwm_channel].load_wdt_selection =
> > of_property_read_bool(child,
> > + "aspeed,wdt-selection-rising");
>
> This isn't documented in the binding.
>
> > + priv->pwm_channel[pwm_channel].duty_sync_disable =
> > of_property_read_bool(child,
> > + "aspeed,disable-duty-instant-change");
>
> This isn't documented in the binding.
>
> However, does slowly increase/decrease the duty instead? Might it be better to name the property in terms of the positive case (the property name doesn't have to directly reflect the name of the hardware setting), e.g. 'aspeed,ramp-pwm-duty'?
>
I'll check about the functionaility.
> > + priv->pwm_channel[pwm_channel].inverse_pin =
> > of_property_read_bool(child,
> > + "aspeed,inverse-pin");
> > +
> > + ret = of_property_read_u8(child, "aspeed,wdt-rising-falling-point",
> > &val);
>
> This isn't documented in the binding.
>
> > + if (ret)
> > + val = DEFAULT_WDT_RISING_FALLING_PT;
> > + priv->pwm_channel[pwm_channel].load_wdt_rising_falling_pt = val;
> > +
> > + ret = of_property_read_u8(child, "aspeed,falling-point", &val);
> > + if (ret)
> > + val = DEFAULT_FALLING_PT;
> > + priv->pwm_channel[pwm_channel].falling = val;
> > +
> > + aspeed_create_pwm_channel(dev, priv, (u8)pwm_channel);
> > +
> > + ret = of_property_count_u8_elems(child, "cooling-levels");
> > + if (ret > 0) {
> > + if (IS_ENABLED(CONFIG_THERMAL)) {
> > + ret = aspeed_create_pwm_cooling(dev, child, priv, pwm_channel,
> > + ret);
> > + if (ret)
> > + return ret;
> > + }
> > + }
> > +
> > + count = of_property_count_u8_elems(child, "fan-tach-ch");
> > + if (count < 1)
> > + return -EINVAL;
> > +
> > + fan_tach_ch = devm_kzalloc(dev, sizeof(*fan_tach_ch) * count,
> > + GFP_KERNEL);
> > + if (!fan_tach_ch)
> > + return -ENOMEM;
> > + ret = of_property_read_u8_array(child, "fan-tach-ch",
> > + fan_tach_ch, count);
> > + if (ret)
> > + return ret;
> > +
> > + ret = of_property_read_u32(child, "pulses-per-revolution",
> > &fan_pulse_pr);
> > + if (ret)
> > + fan_pulse_pr = DEFAULT_FAN_PULSE_PR;
> > +
> > + ret = of_property_read_u32(child, "aspeed,tacho-div", &tacho_div);
>
> This isn't documented in the binding.
>
Missing properties will be added in v4 document.
> > + if (ret)
> > + tacho_div = DEFAULT_TACHO_DIV;
> > +
> > + aspeed_create_fan_tach_channel(priv, fan_tach_ch, count,
> > fan_pulse_pr, tacho_div);
> > +
> > + return 0;
> > +}
> > +
> > +static umode_t aspeed_pwm_tacho_is_visible(const void *data, enum
> > hwmon_sensor_types type,
> > + u32 attr, int channel)
> > +{
> > + const struct aspeed_pwm_tachometer_data *priv = data;
> > +
> > + switch (type) {
> > + case hwmon_pwm:
> > + switch (attr) {
> > + case hwmon_pwm_input:
> > + if (!priv->pwm_present[channel])
> > + return 0;
> > + return 0644;
> > + default:
> > + return 0;
> > + }
> > + break;
> > + case hwmon_fan:
> > + switch (attr) {
> > + case hwmon_fan_input:
> > + if (!priv->fan_tach_present[channel])
> > + return 0;
> > + return 0444;
> > + default:
> > + return 0;
> > + }
> > + default:
> > + return 0;
> > + }
> > +}
> > +
> > +static int aspeed_pwm_tacho_read(struct device *dev, enum
> > hwmon_sensor_types type,
> > + u32 attr, int channel, long *val)
> > +{
> > + struct aspeed_pwm_tachometer_data *priv = dev_get_drvdata(dev);
> > + long rpm;
> > +
> > + switch (type) {
> > + case hwmon_pwm:
> > + switch (attr) {
> > + case hwmon_pwm_input:
> > + *val = priv->pwm_channel[channel].falling;
> > + break;
> > + default:
> > + return -EOPNOTSUPP;
> > + }
> > + break;
> > + case hwmon_fan:
> > + switch (attr) {
> > + case hwmon_fan_input:
> > + rpm = aspeed_get_fan_tach_ch_rpm(dev, priv, channel);
> > + if (rpm < 0)
> > + return rpm;
> > + *val = rpm;
> > + break;
> > + default:
> > + return -EOPNOTSUPP;
> > + }
> > + break;
> > + default:
> > + return -EOPNOTSUPP;
> > + }
> > + return 0;
> > +}
> > +
> > +static int aspeed_pwm_tacho_write(struct device *dev, enum
> > hwmon_sensor_types type,
> > + u32 attr, int channel, long val)
> > +{
> > + switch (type) {
> > + case hwmon_pwm:
> > + switch (attr) {
> > + case hwmon_pwm_input:
> > + return set_pwm(dev, channel, val);
> > + default:
> > + return -EOPNOTSUPP;
> > + }
> > + break;
> > + default:
> > + return -EOPNOTSUPP;
> > + }
> > + return -EOPNOTSUPP;
> > +}
> > +
> > +static const struct hwmon_ops aspeed_pwm_hwmon_ops = {
> > + .is_visible = aspeed_pwm_tacho_is_visible,
> > + .read = aspeed_pwm_tacho_read,
> > + .write = aspeed_pwm_tacho_write,
> > +};
> > +
> > +static const struct hwmon_channel_info *aspeed_pwm_hwmon_info[] = {
> > + HWMON_CHANNEL_INFO(pwm,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT,
> > + HWMON_PWM_INPUT),
> > + HWMON_CHANNEL_INFO(fan,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT,
> > + HWMON_F_INPUT),
> > + NULL,
> > +};
> > +
> > +static const struct hwmon_chip_info aspeed_pwm_chip_info = {
> > + .ops = &aspeed_pwm_hwmon_ops,
> > + .info = aspeed_pwm_hwmon_info,
> > +};
> > +
> > +static int aspeed_pwm_tachometer_probe(struct platform_device *pdev)
> > +{
> > + struct device *dev = &pdev->dev;
> > + struct device_node *np, *child;
> > + struct aspeed_pwm_tachometer_data *priv;
> > + void __iomem *regs;
> > + struct device *hwmon;
> > + struct clk *clk;
> > + int ret;
> > +
> > + np = dev->of_node;
> > +
> > + regs = devm_platform_ioremap_resource(pdev, 0);
> > + if (IS_ERR(regs))
> > + return PTR_ERR(regs);
> > + priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
> > + if (!priv)
> > + return -ENOMEM;
> > +
> > + priv->pwm_channel = default_pwm_params;
> > + priv->tacho_channel = default_tacho_params;
> > + priv->regmap = devm_regmap_init(dev, NULL, (__force void *)regs,
> > + &aspeed_pwm_tachometer_regmap_config);
> > + if (IS_ERR(priv->regmap))
> > + return PTR_ERR(priv->regmap);
> > +
> > + clk = devm_clk_get(dev, NULL);
> > + if (IS_ERR(clk))
> > + return -ENODEV;
> > + priv->clk_freq = clk_get_rate(clk);
> > +
> > + priv->reset = devm_reset_control_get(&pdev->dev, NULL);
> > + if (IS_ERR(priv->reset)) {
> > + dev_err(&pdev->dev, "can't get aspeed_pwm_tacho reset\n");
> > + return PTR_ERR(priv->reset);
> > + }
> > +
> > + /* scu init */
> > + reset_control_assert(priv->reset);
> > + reset_control_deassert(priv->reset);
> > +
> > + for_each_child_of_node(np, child) {
> > + ret = aspeed_pwm_create_fan(dev, child, priv);
> > + if (ret) {
> > + of_node_put(child);
> > + return ret;
>
> Should you reassert the reset here? Or is that handled by the devm infra?
>
I don't think we need to reassert here. It should reset once at the
begining.
> > + }
> > + }
> > +
> > + dev_info(dev, "pwm tach probe done\n");
>
> This is a bit misleading because you're yet to register with hwmon :) I'd put it after the call below.
>
> Anyway, I think it should probably print something more informative if we're going to keep it. Print the number of PWMs/tachos configured as well?
Good catch. I think this message can be removed. How many pwm/tacho channels configured can be found in sysfs.
>
> > + hwmon = devm_hwmon_device_register_with_info(dev,
> > "aspeed_pwm_tachometer",
> > + priv, &aspeed_pwm_chip_info, NULL);
> > +
> > + return PTR_ERR_OR_ZERO(hwmon);
> > +}
> > +
> > +static const struct of_device_id of_pwm_tachometer_match_table[] = {
> > + { .compatible = "aspeed,ast2600-pwm-tachometer", },
> > + {},
> > +};
> > +MODULE_DEVICE_TABLE(of, of_pwm_tachometer_match_table);
> > +
> > +static struct platform_driver aspeed_pwm_tachometer_driver = {
> > + .probe = aspeed_pwm_tachometer_probe,
> > + .driver = {
> > + .name = "aspeed_pwm_tachometer",
> > + .of_match_table = of_pwm_tachometer_match_table,
> > + },
> > +};
> > +
> > +module_platform_driver(aspeed_pwm_tachometer_driver);
> > +
> > +MODULE_AUTHOR("Ryan Chen <ryan_chen at aspeedtech.com>");
> > +MODULE_DESCRIPTION("ASPEED PWM and Fan Tachometer device driver");
>
> Given it's AST2600-specific:
>
> "AST2600 PWM and Fan Tachometer device driver"
>
> I've run out of steam, so that's enough for now :)
>
> Andrew
Thanks for reviewing, I'll update and submit a v4 patch.
Troy Lee
More information about the Linux-aspeed
mailing list