Re: [PATCH v7 06/13] pwm: add support for sl28cpld PWM controller
From: Uwe Kleine-König <hidden>
Date: 2020-08-06 11:13:15
Also in:
linux-devicetree, linux-gpio, linux-hwmon, linux-pwm, linux-watchdog, lkml
Hello Michael, I'm nearly happy now; see below. On Mon, Aug 03, 2020 at 11:35:52AM +0200, Michael Walle wrote:
quoted hunk ↗ jump to hunk
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 .
quoted hunk ↗ jump to hunk
+ 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_TESTdiff --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.odiff --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, ®);
+
+ 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, ®);
+ 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/ |