Re: [PATCH v14 1/2] pwm: add microchip soft ip corePWM driver

From: Uwe Kleine-König
Date: Wed Mar 22 2023 - 06:55:55 EST


Hello,

On Mon, Mar 06, 2023 at 09:48:58AM +0000, Conor Dooley wrote:
> Add a driver that supports the Microchip FPGA "soft" PWM IP core.
>
> Signed-off-by: Conor Dooley <conor.dooley@xxxxxxxxxxxxx>
> ---
> drivers/pwm/Kconfig | 10 +
> drivers/pwm/Makefile | 1 +
> drivers/pwm/pwm-microchip-core.c | 441 +++++++++++++++++++++++++++++++
> 3 files changed, 452 insertions(+)
> create mode 100644 drivers/pwm/pwm-microchip-core.c
>
> diff --git a/drivers/pwm/Kconfig b/drivers/pwm/Kconfig
> index dae023d783a22..f42756a014ed4 100644
> --- a/drivers/pwm/Kconfig
> +++ b/drivers/pwm/Kconfig
> @@ -393,6 +393,16 @@ config PWM_MEDIATEK
> To compile this driver as a module, choose M here: the module
> will be called pwm-mediatek.
>
> +config PWM_MICROCHIP_CORE
> + tristate "Microchip corePWM PWM support"
> + depends on SOC_MICROCHIP_POLARFIRE || COMPILE_TEST
> + depends on HAS_IOMEM && OF
> + help
> + PWM driver for Microchip FPGA soft IP core.
> +
> + To compile this driver as a module, choose M here: the module
> + will be called pwm-microchip-core.
> +
> config PWM_MXS
> tristate "Freescale MXS PWM support"
> depends on ARCH_MXS || COMPILE_TEST
> diff --git a/drivers/pwm/Makefile b/drivers/pwm/Makefile
> index 7bf1a29f02b84..a65625359ece4 100644
> --- a/drivers/pwm/Makefile
> +++ b/drivers/pwm/Makefile
> @@ -34,6 +34,7 @@ obj-$(CONFIG_PWM_LPSS_PCI) += pwm-lpss-pci.o
> obj-$(CONFIG_PWM_LPSS_PLATFORM) += pwm-lpss-platform.o
> obj-$(CONFIG_PWM_MESON) += pwm-meson.o
> obj-$(CONFIG_PWM_MEDIATEK) += pwm-mediatek.o
> +obj-$(CONFIG_PWM_MICROCHIP_CORE) += pwm-microchip-core.o
> obj-$(CONFIG_PWM_MTK_DISP) += pwm-mtk-disp.o
> obj-$(CONFIG_PWM_MXS) += pwm-mxs.o
> obj-$(CONFIG_PWM_NTXEC) += pwm-ntxec.o
> diff --git a/drivers/pwm/pwm-microchip-core.c b/drivers/pwm/pwm-microchip-core.c
> new file mode 100644
> index 0000000000000..a2a2e28f39031
> --- /dev/null
> +++ b/drivers/pwm/pwm-microchip-core.c
> @@ -0,0 +1,441 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * corePWM driver for Microchip "soft" FPGA IP cores.
> + *
> + * Copyright (c) 2021-2023 Microchip Corporation. All rights reserved.
> + * Author: Conor Dooley <conor.dooley@xxxxxxxxxxxxx>
> + * Documentation:
> + * https://www.microsemi.com/document-portal/doc_download/1245275-corepwm-hb
> + *
> + * Limitations:
> + * - If the IP block is configured without "shadow registers", all register
> + * writes will take effect immediately, causing glitches on the output.
> + * If shadow registers *are* enabled, a write to the "SYNC_UPDATE" register
> + * notifies the core that it needs to update the registers defining the
> + * waveform from the contents of the "shadow registers".
> + * - The IP block has no concept of a duty cycle, only rising/falling edges of
> + * the waveform. Unfortunately, if the rising & falling edges registers have
> + * the same value written to them the IP block will do whichever of a rising
> + * or a falling edge is possible. I.E. a 50% waveform at twice the requested
> + * period. Therefore to get a 0% waveform, the output is set the max high/low
> + * time depending on polarity.
> + * - The PWM period is set for the whole IP block not per channel. The driver
> + * will only change the period if no other PWM output is enabled.
> + */
> +
> +#include <linux/clk.h>
> +#include <linux/delay.h>
> +#include <linux/err.h>
> +#include <linux/io.h>
> +#include <linux/ktime.h>
> +#include <linux/math.h>
> +#include <linux/module.h>
> +#include <linux/mutex.h>
> +#include <linux/of_device.h>
> +#include <linux/platform_device.h>
> +#include <linux/pwm.h>
> +
> +#define PREG_TO_VAL(PREG) ((PREG) + 1)
> +
> +#define MCHPCOREPWM_PRESCALE_MAX 0x100
> +#define MCHPCOREPWM_PERIOD_STEPS_MAX 0xff
> +#define MCHPCOREPWM_PERIOD_MAX 0xff00
> +
> +#define MCHPCOREPWM_PRESCALE 0x00
> +#define MCHPCOREPWM_PERIOD 0x04
> +#define MCHPCOREPWM_EN(i) (0x08 + 0x04 * (i)) /* 0x08, 0x0c */
> +#define MCHPCOREPWM_POSEDGE(i) (0x10 + 0x08 * (i)) /* 0x10, 0x18, ..., 0x88 */
> +#define MCHPCOREPWM_NEGEDGE(i) (0x14 + 0x08 * (i)) /* 0x14, 0x1c, ..., 0x8c */
> +#define MCHPCOREPWM_SYNC_UPD 0xe4
> +#define MCHPCOREPWM_TIMEOUT_MS 100u
> +
> +struct mchp_core_pwm_chip {
> + struct pwm_chip chip;
> + struct clk *clk;
> + void __iomem *base;
> + struct mutex lock; /* protects the shared period */
> + ktime_t update_timestamp;
> + u32 sync_update_mask;
> + u16 channel_enabled;
> +};
> +
> +static inline struct mchp_core_pwm_chip *to_mchp_core_pwm(struct pwm_chip *chip)
> +{
> + return container_of(chip, struct mchp_core_pwm_chip, chip);
> +}
> +
> +static void mchp_core_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm,
> + bool enable, u64 period)
> +{
> + struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
> + u8 channel_enable, reg_offset, shift;
> +
> + /*
> + * There are two adjacent 8 bit control regs, the lower reg controls
> + * 0-7 and the upper reg 8-15. Check if the pwm is in the upper reg
> + * and if so, offset by the bus width.
> + */
> + reg_offset = MCHPCOREPWM_EN(pwm->hwpwm >> 3);
> + shift = pwm->hwpwm & 7;
> +
> + channel_enable = readb_relaxed(mchp_core_pwm->base + reg_offset);
> + channel_enable &= ~(1 << shift);
> + channel_enable |= (enable << shift);
> +
> + writel_relaxed(channel_enable, mchp_core_pwm->base + reg_offset);
> + mchp_core_pwm->channel_enabled &= ~BIT(pwm->hwpwm);
> + mchp_core_pwm->channel_enabled |= enable << pwm->hwpwm;
> +
> + /*
> + * Notify the block to update the waveform from the shadow registers.
> + * The updated values will not appear on the bus until they have been
> + * applied to the waveform at the beginning of the next period.
> + * This is a NO-OP if the channel does not have shadow registers.
> + */
> + if (mchp_core_pwm->sync_update_mask & (1 << pwm->hwpwm))
> + mchp_core_pwm->update_timestamp = ktime_add_ns(ktime_get(), period);
> +}
> +
> +static void mchp_core_pwm_wait_for_sync_update(struct mchp_core_pwm_chip *mchp_core_pwm,
> + unsigned int channel)
> +{
> + /*
> + * If a shadow register is used for this PWM channel, and iff there is
> + * a pending update to the waveform, we must wait for it to be applied
> + * before attempting to read its state. Reading the registers yields
> + * the currently implemented settings & the new ones are only readable
> + * once the current period has ended.
> + */
> +
> + if (mchp_core_pwm->sync_update_mask & (1 << channel)) {
> + ktime_t current_time = ktime_get();
> + s64 remaining_ns;
> + u32 delay_us;
> +
> + remaining_ns = ktime_to_ns(ktime_sub(mchp_core_pwm->update_timestamp,
> + current_time));
> +
> + /*
> + * If the update has gone through, don't bother waiting for
> + * obvious reasons. Otherwise wait around for an appropriate
> + * amount of time for the update to go through.
> + */
> + if (remaining_ns <= 0)
> + return;
> +
> + delay_us = DIV_ROUND_UP_ULL(remaining_ns, NSEC_PER_USEC);
> + fsleep(delay_us);
> + }
> +}
> +
> +static u64 mchp_core_pwm_calc_duty(const struct pwm_state *state, u64 clk_rate,
> + u8 prescale, u8 period_steps)
> +{
> + u64 duty_steps, tmp;
> + u16 prescale_val = PREG_TO_VAL(prescale);
> +
> + /*
> + * Calculate the duty cycle in multiples of the prescaled period:
> + * duty_steps = duty_in_ns / step_in_ns
> + * step_in_ns = (prescale * NSEC_PER_SEC) / clk_rate
> + * The code below is rearranged slightly to only divide once.
> + */
> + tmp = prescale_val * NSEC_PER_SEC;
> + duty_steps = mul_u64_u64_div_u64(state->duty_cycle, clk_rate, tmp);
> +
> + return duty_steps;
> +}
> +
> +static void mchp_core_pwm_apply_duty(struct pwm_chip *chip, struct pwm_device *pwm,
> + const struct pwm_state *state, u64 duty_steps,
> + u16 period_steps)
> +{
> + struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
> + u8 posedge, negedge;
> + u8 first_edge = 0, second_edge = duty_steps;
> +
> + /*
> + * Setting posedge == negedge doesn't yield a constant output,
> + * so that's an unsuitable setting to model duty_steps = 0.
> + * In that case set the unwanted edge to a value that never
> + * triggers.
> + */
> + if (duty_steps == 0)
> + first_edge = PREG_TO_VAL(period_steps);
> +
> + if (state->polarity == PWM_POLARITY_INVERSED) {
> + negedge = first_edge;
> + posedge = second_edge;
> + } else {
> + posedge = first_edge;
> + negedge = second_edge;
> + }
> +
> + writel_relaxed(posedge, mchp_core_pwm->base + MCHPCOREPWM_POSEDGE(pwm->hwpwm));
> + writel_relaxed(negedge, mchp_core_pwm->base + MCHPCOREPWM_NEGEDGE(pwm->hwpwm));
> +}
> +
> +static void mchp_core_pwm_calc_period(const struct pwm_state *state, unsigned long clk_rate,
> + u16 *prescale, u16 *period_steps)
> +{
> + u64 tmp;
> +
> + /*
> + * Calculate the period cycles and prescale values.
> + * The registers are each 8 bits wide & multiplied to compute the period
> + * using the formula:
> + * (prescale + 1) * (period_steps + 1)
> + * period = -------------------------------------
> + * clk_rate
> + * so the maximum period that can be generated is 0x10000 times the
> + * period of the input clock.
> + * However, due to the design of the "hardware", it is not possible to
> + * attain a 100% duty cycle if the full range of period_steps is used.
> + * Therefore period_steps is restricted to 0xFE and the maximum multiple
> + * of the clock period attainable is 0xFF00.
> + */
> + tmp = mul_u64_u64_div_u64(state->period, clk_rate, NSEC_PER_SEC);
> +
> + /*
> + * The hardware adds one to the register value, so decrement by one to
> + * account for the offset
> + */
> + if (tmp >= MCHPCOREPWM_PERIOD_MAX) {
> + *prescale = MCHPCOREPWM_PRESCALE_MAX - 1;
> + *period_steps = MCHPCOREPWM_PERIOD_STEPS_MAX - 1;
> +
> + return;
> + }
> +
> + /*
> + * The optimal value for prescale can be calculated using the maximum
> + * permitted value of period_steps, 0xff.

I had to think about that one for a while. The maximal value for
(period_steps + 1) is 0xff with the reasoning above?! That's also what
the code uses.

Also as the comment is written here, it's wrong (or misleading)
depending on the semantic of "optimal". If you want to achive

(prescale + 1) * (period_steps + 1) <= 64009

you should pick prescale == period_steps == 252 to get optimally near
64009.
However the idea is to pick a set of values with period_steps being big
to allow a finegrained selection for the duty cycle, right?

Consider

clk_rate = 1000000
period = 64009000

then your code gives:

period * clk_rate
tmp = ----------------- = 64009
NSEC_PER_SEC

and so *prescale = 251 and *period_steps = 253.

Wasn't the intention to pick *prescale = 250 and then
*period_steps = 255?

Depending on your semantics of "optimal", either (252, 252) or (250,
255) is better than (251, 253). I think that means you shouldn't ignore
the -1?

One thing I think is strange is that with clk_rate = 1000001 and your
algorithm we get:

requested period = 1274998 ns -> real period = 1269998.73000127 (prescale = 4, period_steps = 253)
requested period = 1274999 ns -> real period = 1271998.728001272 (prescale = 5, period_steps = 211)

while 1271998.728001272 would be a better match for a request with
period = 1274998 than 1269998.73000127.

I spend too much time to think about that now. I'm unsure if this is
because the -1 is missing, or if there is a bug in the idea to pick a
small prescale to allow a big period_steps value (in combination with
the request to pick the biggest possible period).

Hmm, maybe you understand that better than me? I'll have to think about
it.

> + *
> + * period * clk_rate
> + * prescale = ------------------- - 1
> + * NSEC_PER_SEC * 0xff
> + *
> + * However, we are purely interested in the integer upper bound of this
> + * calculation, so ignore the subtraction & rely on the truncation done
> + * by the division.
> + *
> + * period * clk_rate
> + * ------------------- was precomputed as `tmp`
> + * NSEC_PER_SEC
> + *
> + * period_steps is then computed using the result:
> + * period * clk_rate
> + * period_steps = ----------------------------- - 1
> + * NSEC_PER_SEC * (prescale + 1)
> + */
> + *prescale = div_u64(tmp, MCHPCOREPWM_PERIOD_STEPS_MAX);
> + *period_steps = div_u64(tmp, PREG_TO_VAL(*prescale)) - 1;
> +}
> +
> [..]
> +static int mchp_core_pwm_probe(struct platform_device *pdev)
> +{
> + struct mchp_core_pwm_chip *mchp_core_pwm;
> + struct resource *regs;
> +
> + mchp_core_pwm = devm_kzalloc(&pdev->dev, sizeof(*mchp_core_pwm), GFP_KERNEL);
> + if (!mchp_core_pwm)
> + return -ENOMEM;
> +
> + mchp_core_pwm->base = devm_platform_get_and_ioremap_resource(pdev, 0, &regs);
> + if (IS_ERR(mchp_core_pwm->base))
> + return PTR_ERR(mchp_core_pwm->base);
> +
> + mchp_core_pwm->clk = devm_clk_get_enabled(&pdev->dev, NULL);
> + if (IS_ERR(mchp_core_pwm->clk))
> + return dev_err_probe(&pdev->dev, PTR_ERR(mchp_core_pwm->clk),
> + "failed to get PWM clock\n");
> +
> + if (of_property_read_u32(pdev->dev.of_node, "microchip,sync-update-mask",
> + &mchp_core_pwm->sync_update_mask))
> + mchp_core_pwm->sync_update_mask = 0;
> +
> + mutex_init(&mchp_core_pwm->lock);
> +
> + mchp_core_pwm->chip.dev = &pdev->dev;
> + mchp_core_pwm->chip.ops = &mchp_core_pwm_ops;
> + mchp_core_pwm->chip.npwm = 16;
> +
> + mchp_core_pwm->channel_enabled = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_EN(0));
> + mchp_core_pwm->channel_enabled |=
> + readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_EN(1)) << 8;
> +
> + writel_relaxed(1U, mchp_core_pwm->base + MCHPCOREPWM_SYNC_UPD);

This one is just for the case where there is an unapplied configuration
in the registers, right?

> + mchp_core_pwm->update_timestamp = ktime_get();
> +
> + return devm_pwmchip_add(&pdev->dev, &mchp_core_pwm->chip);

An error message if devm_pwmchip_add() fails would be nice.

> +}
> +
> +static struct platform_driver mchp_core_pwm_driver = {
> + .driver = {
> + .name = "mchp-core-pwm",
> + .of_match_table = mchp_core_of_match,
> + },
> + .probe = mchp_core_pwm_probe,
> +};
> +module_platform_driver(mchp_core_pwm_driver);
> +
> +MODULE_LICENSE("GPL");
> +MODULE_AUTHOR("Conor Dooley <conor.dooley@xxxxxxxxxxxxx>");
> +MODULE_DESCRIPTION("corePWM driver for Microchip FPGAs");
> --
> 2.39.2
>
>

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

Attachment: signature.asc
Description: PGP signature