fn start_duty_fade(
        &self,
        start_duty_pct: u8,
        end_duty_pct: u8,
        duration_ms: u16,
    ) -> Result<(), Error> {
        let duty_exp;
        let frequency;
        if start_duty_pct > 100u8 {
            return Err(Error::Fade(FadeError::StartDuty));
        }
        if end_duty_pct > 100u8 {
            return Err(Error::Fade(FadeError::EndDuty));
        }
        if let Some(timer) = self.timer {
            if let Some(timer_duty) = timer.duty() {
                if timer.frequency() > 0 {
                    duty_exp = timer_duty as u32;
                    frequency = timer.frequency();
                } else {
                    return Err(Error::Timer);
                }
            } else {
                return Err(Error::Timer);
            }
        } else {
            return Err(Error::Channel);
        }

        let duty_range = (1u32 << duty_exp) - 1;
        let start_duty_value = (duty_range * start_duty_pct as u32) / 100;
        let end_duty_value = (duty_range * end_duty_pct as u32) / 100;

        // NB: since we do the multiplication first here, there's no loss of
        // precision from using milliseconds instead of (e.g.) nanoseconds.
        let pwm_cycles = (duration_ms as u32) * frequency / 1000;

        let abs_duty_diff = end_duty_value.abs_diff(start_duty_value);
        let duty_steps: u32 = u16::try_from(abs_duty_diff).unwrap_or(65535).into();
        // This conversion may fail if duration_ms is too big, and if either
        // duty_steps gets truncated, or the fade is over a short range of duty
        // percentages, so it's too small.  Returning an Err in either case is
        // fine: shortening the duration_ms will sort things out.
        let cycles_per_step: u16 = (pwm_cycles / duty_steps)
            .try_into()
            .map_err(|_| Error::Fade(FadeError::Duration))
            .and_then(|res| {
                if res > 1023 {
                    Err(Error::Fade(FadeError::Duration))
                } else {
                    Ok(res)
                }
            })?;
        // This can't fail unless abs_duty_diff is bigger than 65536*65535-1,
        // and so duty_steps gets truncated.  But that requires duty_exp to be
        // at least 32, and the hardware only supports up to 20.  Still, handle
        // it in case something changes in the future.
        let duty_per_cycle: u16 = (abs_duty_diff / duty_steps)
            .try_into()
            .map_err(|_| Error::Fade(FadeError::DutyRange))?;

        self.start_duty_fade_hw(
            start_duty_value,
            end_duty_value > start_duty_value,
            duty_steps.try_into().unwrap(),
            cycles_per_step,
            duty_per_cycle,
        );

        Ok(())
    }
Start a duty fade from one % to another.

There's a constraint on the combination of timer frequency, timer PWM
duty resolution (the bit count), the fade "range" (abs(start-end)), and
the duration:

frequency * duration / ((1<<bit_count) * abs(start-end)) < 1024

Small percentage changes, long durations, coarse PWM resolutions (that
is, low bit counts), and high timer frequencies will all be more likely
to fail this requirement.  If it does fail, this function will return
an error Result.
esp-hal/src/ledc/channel.rs