Re: [PATCH v7 06/13] pwm: add support for sl28cpld PWM controller

Uwe Kleine-König
2020-08-06T11:13:15Z

[-- Attachment #1: Type: text/plain, Size: 6872 bytes --]

Hello Michael,

I'm nearly happy now; see below.

On Mon, Aug 03, 2020 at 11:35:52AM +0200, Michael Walle wrote:
> diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> index 7dbcf6973d33..a0d50d70c3b9 100644
> --- a/drivers/pwm/Kconfig
> +++ b/drivers/pwm/Kconfig
> @@ -428,6 +428,16 @@ config PWM_SIFIVE
> To compile this driver as a module, choose M here: the module
> will be called pwm-sifive.
>
> +config PWM_SL28CPLD
> + tristate "Kontron sl28cpld PWM support"
> + select MFD_SIMPLE_MFD_I2C

Is it sensible to present this option to everyone? Maybe

depends on SOME_SYMBOL_ONLY_TRUE_ON_SL28CPLD || COMPILE_TEST

.

> + help
> + Generic PWM framework driver for board management controller
> + found on the Kontron sl28 CPLD.
> +
> + To compile this driver as a module, choose M here: the module
> + will be called pwm-sl28cpld.
> +
> config PWM_SPEAR
> tristate "STMicroelectronics SPEAr PWM support"
> depends on PLAT_SPEAR || COMPILE_TEST
> diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
> index 2c2ba0a03557..cbdcd55d69ee 100644
> --- a/drivers/pwm/Makefile
> +++ b/drivers/pwm/Makefile
> @@ -40,6 +40,7 @@ obj-$(CONFIG_PWM_RENESAS_TPU) += pwm-renesas-tpu.o
> obj-$(CONFIG_PWM_ROCKCHIP) += pwm-rockchip.o
> obj-$(CONFIG_PWM_SAMSUNG) += pwm-samsung.o
> obj-$(CONFIG_PWM_SIFIVE) += pwm-sifive.o
> +obj-$(CONFIG_PWM_SL28CPLD) += pwm-sl28cpld.o
> obj-$(CONFIG_PWM_SPEAR) += pwm-spear.o
> obj-$(CONFIG_PWM_SPRD) += pwm-sprd.o
> obj-$(CONFIG_PWM_STI) += pwm-sti.o
> diff --git a/drivers/pwm/pwm-sl28cpld.c b/drivers/pwm/pwm-sl28cpld.c
> new file mode 100644
> index 000000000000..bb298af36f0b
> --- /dev/null
> +++ b/drivers/pwm/pwm-sl28cpld.c
> @@ -0,0 +1,235 @@
> +// SPDX-License-Identifier: GPL-2.0-only
> +/*
> + * sl28cpld PWM driver
> + *
> + * Copyright (c) 2020 Michael Walle <michael@walle.cc>
> + *
> + * There is no public datasheet available for this PWM core. But it is easy
> + * enough to be briefly explained. It consists of one 8-bit counter. The PWM
> + * supports four distinct frequencies by selecting when to reset the counter.
> + * With the prescaler setting you can select which bit of the counter is used
> + * to reset it. This implies that the higher the frequency the less remaining
> + * bits are available for the actual counter.
> + *
> + * Let cnt[7:0] be the counter, clocked at 32kHz:
> + * +-----------+--------+--------------+-----------+---------------+
> + * | prescaler | reset | counter bits | frequency | period length |
> + * +-----------+--------+--------------+-----------+---------------+
> + * | 0 | cnt[7] | cnt[6:0] | 250 Hz | 4000000 ns |
> + * | 1 | cnt[6] | cnt[5:0] | 500 Hz | 2000000 ns |
> + * | 2 | cnt[5] | cnt[4:0] | 1 kHz | 1000000 ns |
> + * | 3 | cnt[4] | cnt[3:0] | 2 kHz | 500000 ns |
> + * +-----------+--------+--------------+-----------+---------------+
> + *
> + * Limitations:
> + * - The hardware cannot generate a 100% duty cycle if the prescaler is 0.
> + * - The hardware cannot atomically set the prescaler and the counter value,
> + * which might lead to glitches and inconsistent states if a write fails.
> + * - The counter is not reset if you switch the prescaler which leads
> + * to glitches, too.
> + * - The duty cycle will switch immediately and not after a complete cycle.
> + * - Depending on the actual implementation, disabling the PWM might have
> + * side effects. For example, if the output pin is shared with a GPIO pin
> + * it will automatically switch back to GPIO mode.

Very nice.

> + */
> +
> +#include <linux/bitfield.h>
> +#include <linux/kernel.h>
> +#include <linux/mod_devicetable.h>
> +#include <linux/module.h>
> +#include <linux/platform_device.h>
> +#include <linux/pwm.h>
> +#include <linux/regmap.h>
> +
> +/*
> + * PWM timer block registers.
> + */
> +#define SL28CPLD_PWM_CTRL 0x00
> +#define SL28CPLD_PWM_CTRL_ENABLE BIT(7)
> +#define SL28CPLD_PWM_CTRL_PRESCALER_MASK GENMASK(1, 0)
> +#define SL28CPLD_PWM_CYCLE 0x01
> +#define SL28CPLD_PWM_CYCLE_MAX GENMASK(6, 0)
> +
> +#define SL28CPLD_PWM_CLK 32000 /* 32 kHz */
> +#define SL28CPLD_PWM_MAX_DUTY_CYCLE(prescaler) (1 << (7 - (prescaler)))
> +#define SL28CPLD_PWM_PERIOD(prescaler) \
> + (NSEC_PER_SEC / SL28CPLD_PWM_CLK * SL28CPLD_PWM_MAX_DUTY_CYCLE(prescaler))
> +
> +/*
> + * We calculate the duty cycle like this:
> + * duty_cycle_ns = pwm_cycle_reg * max_period_ns / max_duty_cycle
> + *
> + * With
> + * max_period_ns = 1 << (7 - prescaler) / pwm_clk * NSEC_PER_SEC
> + * max_duty_cycle = 1 << (7 - prescaler)
> + * this then simplifies to:
> + * duty_cycle_ns = pwm_cycle_reg / pwm_clk * NSEC_PER_SEC
> + *
> + * NSEC_PER_SEC and SL28CPLD_PWM_CLK is integer here, so we're not losing
> + * precision by doing the divison first.

Apart from the grammatical issue (s/is/are/) this is not the relevant
fact. The relevant thing is that NSEC_PER_SEC / SL28CPLD_PWM_CLK is
integer.

(In case this is not clear, assume SL28CPLD_PWM_CLK to be 30000 and reg
0x12345.

Then we have:

NSEC_PER_SEC / SL28CPLD_PWM_CLK * (reg) -> 0x94255749
NSEC_PER_SEC * (reg) / SL28CPLD_PWM_CLK -> 0x9425b860

.)

> + */
> +#define SL28CPLD_PWM_TO_DUTY_CYCLE(reg) \
> + (NSEC_PER_SEC / SL28CPLD_PWM_CLK * (reg))
> +#define SL28CPLD_PWM_FROM_DUTY_CYCLE(duty_cycle) \
> + (DIV_ROUND_DOWN_ULL((duty_cycle), NSEC_PER_SEC / SL28CPLD_PWM_CLK))
> +
> +#define sl28cpld_pwm_read(priv, reg, val) \
> + regmap_read((priv)->regmap, (priv)->offset + (reg), (val))
> +#define sl28cpld_pwm_write(priv, reg, val) \
> + regmap_write((priv)->regmap, (priv)->offset + (reg), (val))
> +
> +struct sl28cpld_pwm {
> + struct pwm_chip pwm_chip;
> + struct regmap *regmap;
> + u32 offset;
> +};
> +
> +static void sl28cpld_pwm_get_state(struct pwm_chip *chip,
> + struct pwm_device *pwm,
> + struct pwm_state *state)
> +{
> + struct sl28cpld_pwm *priv = dev_get_drvdata(chip->dev);
> + unsigned int reg;
> + int prescaler;
> +
> + sl28cpld_pwm_read(priv, SL28CPLD_PWM_CTRL, &reg);
> +
> + state->enabled = reg & SL28CPLD_PWM_CTRL_ENABLE;
> +
> + prescaler = FIELD_GET(SL28CPLD_PWM_CTRL_PRESCALER_MASK, reg);
> + state->period = SL28CPLD_PWM_PERIOD(prescaler);
> +
> + sl28cpld_pwm_read(priv, SL28CPLD_PWM_CYCLE, &reg);
> + state->duty_cycle = SL28CPLD_PWM_TO_DUTY_CYCLE(reg);

Should reg be masked to SL28CPLD_PWM_CYCLE_MAX, or is it guaranteed that
the upper bits are zero?

> + state->polarity = PWM_POLARITY_NORMAL;
> +}

Best regards
Uwe

--
Pengutronix e.K. | Uwe Kleine-König |
Industrial Linux Solutions | https://www.pengutronix.de/ |

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 488 bytes --]