[PATCH v7 3/4] pwm: add microchip soft ip corePWM driver

Conor.Dooley at microchip.com Conor.Dooley at microchip.com
Tue Aug 2 05:34:14 PDT 2022



On 02/08/2022 09:46, Uwe Kleine-König wrote:
> Hello,
> 
> On Thu, Jul 21, 2022 at 06:21:09PM +0100, Conor Dooley wrote:
>> From: Conor Dooley <conor.dooley at microchip.com>
>>
>> Add a driver that supports the Microchip FPGA "soft" PWM IP core.
>>
>> Signed-off-by: Conor Dooley <conor.dooley at microchip.com>
>> ---

>> +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. We must
>> +	 * write these registers and wait for them to be applied before calling
>> +	 * enable().
> 
> What does "calling enable()" mean? There is no such function or callback
> with that name?!

I relocated the comment but forgot to proof read it!
s/calling enable()/considering the channel enabled/
I'm not sure where it comes from, but I keep thinking that there is an
enable() callback...

> 
>> +	 */
>> +	if (mchp_core_pwm->sync_update_mask & (1 << pwm->hwpwm)) {
>> +		writel_relaxed(1U, mchp_core_pwm->base + MCHPCOREPWM_SYNC_UPD);
>> +		usleep_range(period, period * 2);
> 
> So if period = 5000 *ns* you sleep between 5000 and 10000 *us* here?

/facepalm

> 
>> +	}
>> +}
>> +
>> +static u64 mchp_core_pwm_calc_duty(struct pwm_chip *chip, struct pwm_device *pwm,
>> +				   const struct pwm_state *state, u8 prescale, u8 period_steps)
>> +{
>> +	struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
>> +	u64 duty_steps, period, tmp;
>> +	u16 prescale_val = PREG_TO_VAL(prescale);
>> +	u8 period_steps_val = PREG_TO_VAL(period_steps);
> 
> Can it happen that period_steps is 0xff? Then period_steps_val ends up
> being 0.

I guess that register could have a value it in from the bootloader etc and
therefore handling it is a good idea - but not in this function since it
is never used...

> 
>> +
>> +	period = period_steps_val * prescale_val * NSEC_PER_SEC;
>> +	period = DIV64_U64_ROUND_UP(period, clk_get_rate(mchp_core_pwm->clk));
> 
> The value you are calculating for period isn't used?!

huh, I am surprised that this was not caught by a W=1 C=1 build. Or maybe it
was and I just didn't notice - but I am 99% sure I made sure there were none.

> 
>> +
>> +	/*
>> +	 * 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.
>> +	 */
>> +	duty_steps = state->duty_cycle * clk_get_rate(mchp_core_pwm->clk);
>> +	tmp = prescale_val * NSEC_PER_SEC;
>> +	return div64_u64(duty_steps, tmp);
>> +}
>> +
>> +static void mchp_core_pwm_apply_duty(struct pwm_chip *chip, struct pwm_device *pwm,
>> +				     const struct pwm_state *state, u64 duty_steps, u8 period_steps)
>> +{
>> +	struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
>> +	u8 posedge, negedge;
>> +	u8 period_steps_val = PREG_TO_VAL(period_steps);
>> +
>> +	/*
>> +	 * Turn the output on unless posedge == negedge, in which case the
>> +	 * duty is intended to be 0, but limitations of the IP block don't
>> +	 * allow a zero length duty cycle - so just set the max high/low time
>> +	 * respectively.
>> +	 */
> 
> I don't understand that comment. Maybe you mean?:
> 
> 	/*
> 	 * 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.
> 	 */

Yeah, this is a better comment. Thanks.

> 
>> +	if (state->polarity == PWM_POLARITY_INVERSED) {
>> +		negedge = !duty_steps ? period_steps_val : 0u;
>> +		posedge = duty_steps;
>> +	} else {
>> +		posedge = !duty_steps ? period_steps_val : 0u;
>> +		negedge = duty_steps;
>> +	}
>> +
>> +	writel_relaxed(posedge, mchp_core_pwm->base + MCHPCOREPWM_POSEDGE(pwm->hwpwm));
>> +	writel_relaxed(negedge, mchp_core_pwm->base + MCHPCOREPWM_NEGEDGE(pwm->hwpwm));
>> +}
>> +
>> +static int mchp_core_pwm_calc_period(struct pwm_chip *chip, const struct pwm_state *state,
>> +				     u8 *prescale, u8 *period_steps)
>> +{
>> +	struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
>> +	u64 tmp, clk_rate;
>> +
>> +	/*
>> +	 * Calculate the period cycles and prescale values.
>> +	 * The registers are each 8 bits wide & multiplied to compute the period
>> +	 * using the formula:
>> +	 * (clock_period) * (prescale + 1) * (period_steps + 1)
>> +	 * 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.
>> +	 */
>> +	clk_rate = clk_get_rate(mchp_core_pwm->clk);
>> +
>> +	/*
>> +	 * If clk_rate is too big, the following multiplication might overflow.
>> +	 * However this is implausible, as the fabric of current FPGAs cannot
>> +	 * provide clocks at a rate high enough.
>> +	 */
>> +	if (clk_rate >= NSEC_PER_SEC)
>> +		return -EINVAL;
>> +
>> +	tmp = mul_u64_u64_div_u64(state->period, clk_rate, NSEC_PER_SEC);
>> +
>> +	if (tmp >= MCHPCOREPWM_PERIOD_MAX) {
>> +		*prescale = MCHPCOREPWM_PRESCALE_MAX - 1;
> 
> why -1 here?

Because the hardware adds 1 to the register value. I had tried to explain
in the large comment above, but I will reword the comment for v8.

> 
>> +		*period_steps = MCHPCOREPWM_PERIOD_STEPS_MAX - 1;
>> +		return 0;
>> +	}
>> +
>> +	*prescale = div_u64(tmp, MCHPCOREPWM_PERIOD_STEPS_MAX);
>> +	/* PREG_TO_VAL() can produce a value larger than UINT8_MAX */
> 
> That should explain the cast to u32? If this were really necessary
> (hint: it isn't) it would IMHO be better to hide that cast in the macro.
> 
>> +	*period_steps = div_u64(tmp, PREG_TO_VAL((u32)*prescale)) - 1;
>> +
>> +	return 0;
>> +}
>> +
>> +static inline void mchp_core_pwm_apply_period(struct mchp_core_pwm_chip *mchp_core_pwm,
>> +					      u8 prescale, u8 period_steps)
>> +{
>> +	writel_relaxed(prescale, mchp_core_pwm->base + MCHPCOREPWM_PRESCALE);
>> +	writel_relaxed(period_steps, mchp_core_pwm->base + MCHPCOREPWM_PERIOD);
>> +}
>> +
>> +static int mchp_core_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
>> +			       const struct pwm_state *state)
>> +{
>> +	struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
>> +	struct pwm_state current_state = pwm->state;
>> +	bool period_locked;
>> +	u64 duty_steps;
>> +	u8 prescale, period_steps, hw_prescale, hw_period_steps;
>> +	int ret;
>> +
>> +	ret = mutex_lock_interruptible(&mchp_core_pwm->lock);
>> +	if (ret)
>> +		return ret;
> 
> I would have used mutex_lock() here. Why should a signal prevent
> reconfiguration of the PWM?

Cool, willdo.

> 
>> +
>> +	if (!state->enabled) {
>> +		mchp_core_pwm_enable(chip, pwm, false, current_state.period);
>> +		mutex_unlock(&mchp_core_pwm->lock);
>> +		return 0;
>> +	}
>> +
>> +	/*
>> +	 * If the only thing that has changed is the duty cycle or the polarity,
>> +	 * we can shortcut the calculations and just compute/apply the new duty
>> +	 * cycle pos & neg edges
>> +	 * As all the channels share the same period, do not allow it to be
>> +	 * changed if any other channels are enabled.
>> +	 * If the period is locked, it may not be possible to use a period
>> +	 * less than that requested. In that case, we just abort.
>> +	 */
>> +	period_locked = mchp_core_pwm->channel_enabled & ~(1 << pwm->hwpwm);
>> +
>> +	if (period_locked) {
>> +		mchp_core_pwm_calc_period(chip, state, &prescale, &period_steps);
>> +		hw_prescale = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_PRESCALE);
>> +		hw_period_steps = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_PERIOD);
>> +
>> +		if ((period_steps * prescale) < (hw_period_steps * hw_prescale)) {
> 
> You need
> 
> 	if ((period_steps + 1) * (prescale + 1) < (hw_period_steps + 1) * (hw_prescale + 1))
> 
> here, don't you?

Yikes, yeah...

> 
>> +			mutex_unlock(&mchp_core_pwm->lock);
>> +			return -EINVAL;
>> +		}
>> +
>> +		prescale = hw_prescale;
>> +		period_steps = hw_period_steps;
> 
> The two hw_* variables are only used in this branch. So their
> declaration can move into here.
> 
>> +	} else if (!current_state.enabled || current_state.period != state->period) {
>> +		ret = mchp_core_pwm_calc_period(chip, state, &prescale, &period_steps);
>> +		if (ret) {
>> +			mutex_unlock(&mchp_core_pwm->lock);
>> +			return ret;
>> +		}
>> +		mchp_core_pwm_apply_period(mchp_core_pwm, prescale, period_steps);
>> +	} else {
>> +		prescale = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_PRESCALE);
>> +		period_steps = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_PERIOD);
>> +	}
>> +
>> +	duty_steps = mchp_core_pwm_calc_duty(chip, pwm, state, prescale, period_steps);
>> +
>> +	/*
>> +	 * Because the period is per channel, it is possible that the requested
>> +	 * duty cycle is longer than the period, in which case cap it to the
>> +	 * period, IOW a 100% duty cycle.
>> +	 */
>> +	if (duty_steps > period_steps)
>> +		duty_steps = period_steps + 1;
>> +
>> +	mchp_core_pwm_apply_duty(chip, pwm, state, duty_steps, period_steps);
>> +
>> +	mchp_core_pwm_enable(chip, pwm, true, state->period);
>> +
>> +	mutex_unlock(&mchp_core_pwm->lock);
>> +
>> +	return 0;
>> +}
>> +
>> +static void mchp_core_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
>> +				    struct pwm_state *state)
>> +{
>> +	struct mchp_core_pwm_chip *mchp_core_pwm = to_mchp_core_pwm(chip);
>> +	u16 prescale;
>> +	u8 period_steps, duty_steps, posedge, negedge;
>> +	int ret;
>> +
>> +	ret = mutex_lock_interruptible(&mchp_core_pwm->lock);
>> +	if (ret)
>> +		return;
>> +
>> +	if (mchp_core_pwm->channel_enabled & (1 << pwm->hwpwm))
> 
> channel_enabled is initialized to 0 in .probe(), so a PWM is never
> diagnosed to be running when the core initially wants to determine the
> current state.

Good point. I'll initialise it in probe.

> 
>> +		state->enabled = true;
>> +	else
>> +		state->enabled = false;
>> +
>> +	prescale = PREG_TO_VAL(readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_PRESCALE));
>> +
>> +	period_steps = PREG_TO_VAL(readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_PERIOD));
>> +	state->period = period_steps * prescale * NSEC_PER_SEC;
>> +	state->period = DIV64_U64_ROUND_UP(state->period, clk_get_rate(mchp_core_pwm->clk));
>> +
>> +	posedge = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_POSEDGE(pwm->hwpwm));
>> +	negedge = readb_relaxed(mchp_core_pwm->base + MCHPCOREPWM_NEGEDGE(pwm->hwpwm));
>> +
>> +	if (negedge == posedge) {
>> +		state->duty_cycle = state->period / 2;
> 
> I thought that's:
> 
> 		state->duty_cycle = state->period;
> 		state->period *= 2;

Correct, as usual..
Thanks for your review Uwe! I'll fix it all up & submit v8 after -rc1.
Conor.



More information about the linux-riscv mailing list