hannobraun / stepper

Universal Stepper Motor Interface
Other
107 stars 17 forks source link

Software-based motion control implementation requires custom numeric type #105

Closed hannobraun closed 3 years ago

hannobraun commented 3 years ago

The software-based motion control implementation (SoftwareMotionControl) needs to convert the delay values it gets from the RampMaker MotionProfile implementation, whose units are user-defined, to nanoseconds (to figure out how long it should wait after the step signal has finished) and to timer ticks (to then wait for that long). Since Step/Dir is an abstract library that know nothing about the units the user uses, or the frequency of the timer, all it can do is require that those required conversions can be made, by specifying the appropriate trait bounds.

The user can't implement conversion traits, if both types that need to be converted between come from foreign crates, which they likely are (the delay value is likely a floating point number or something from fixed; the timer tick type is defined in a HAL). Therefore the only solution is for the user to implement their own numeric type, wrapping the numeric type they want to use. However, since all kinds of operations need to be supported for this numeric type, there's a whole lot of trait implementations that the user needs to provide.

This is non-obvious, and even if you realize it, highly inconvenient. I'm going to mark this as a bug. It is not one in the strict sense, as in the motion control API works as designed, but this quirk is so limiting, it might as well be a bug.

I'm not sure how to address this. The best solution would be to eliminate the need for this custom type, but I don't see how. Maybe we should provide a macro to generate this type. This would make the whole thing manageable, but seems a bit hacky. We can provide a built-in type that does conversion based on conversion factor (provided at runtime or, soon, via const generics), but this might be too simplistic (for example when using fixed-point numbers, multiplying by a factor might just cause an overflow).

In any case, feedback is highly welcome.

For the record, here's my own numeric type implementation from an internal test application:

use core::{
    convert::{Infallible, TryInto},
    ops,
};

use embedded_time::duration::Nanoseconds;
use fixed::traits::ToFixed;
use step_dir::ramp_maker;

#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct Num(Inner);

impl Num {
    pub fn from_num(num: impl ToFixed) -> Self {
        Self(Inner::from_num(num))
    }
}

impl az::Cast<u32> for Num {
    fn cast(self) -> u32 {
        self.0.cast()
    }
}

impl num_traits::Num for Num {
    type FromStrRadixErr = <Inner as num_traits::Num>::FromStrRadixErr;

    fn from_str_radix(
        str: &str,
        radix: u32,
    ) -> Result<Self, Self::FromStrRadixErr> {
        let inner = Inner::from_str_radix(str, radix)?;
        Ok(Self(inner))
    }
}

impl num_traits::Zero for Num {
    fn zero() -> Self {
        Self(Inner::zero())
    }

    fn is_zero(&self) -> bool {
        self.0.is_zero()
    }
}

impl num_traits::One for Num {
    fn one() -> Self {
        Self(Inner::one())
    }
}

impl num_traits::Signed for Num {
    fn abs(&self) -> Self {
        Self(self.0.abs())
    }

    fn abs_sub(&self, other: &Self) -> Self {
        Self(self.0.abs_sub(&other.0))
    }

    fn signum(&self) -> Self {
        Self(self.0.signum())
    }

    fn is_positive(&self) -> bool {
        self.0.is_positive()
    }

    fn is_negative(&self) -> bool {
        self.0.is_negative()
    }
}

impl num_traits::Inv for Num {
    type Output = Self;

    fn inv(self) -> Self::Output {
        Self(self.0.inv())
    }
}

impl ops::Add for Num {
    type Output = Self;

    fn add(self, rhs: Self) -> Self::Output {
        Self(self.0.add(rhs.0))
    }
}

impl ops::Sub for Num {
    type Output = Self;

    fn sub(self, rhs: Self) -> Self::Output {
        Self(self.0.sub(rhs.0))
    }
}

impl ops::Neg for Num {
    type Output = Self;

    fn neg(self) -> Self::Output {
        Self(self.0.neg())
    }
}

impl ops::Mul for Num {
    type Output = Self;

    fn mul(self, rhs: Self) -> Self::Output {
        Self(self.0.mul(rhs.0))
    }
}

impl ops::Div for Num {
    type Output = Self;

    fn div(self, rhs: Self) -> Self::Output {
        Self(self.0.div(rhs.0))
    }
}

impl ops::Rem for Num {
    type Output = Self;

    fn rem(self, rhs: Self) -> Self::Output {
        Self(self.0.rem(rhs.0))
    }
}

impl ramp_maker::util::traits::Ceil for Num {
    fn ceil(self) -> Self {
        Self(self.0.ceil())
    }
}

impl ramp_maker::util::traits::Sqrt for Num {
    fn sqrt(self) -> Self {
        Self(self.0.sqrt())
    }
}

impl TryInto<Nanoseconds> for Num {
    type Error = Infallible;

    fn try_into(self) -> Result<Nanoseconds, Self::Error> {
        let mut delay = Nanoseconds::new(0);

        let delay_s = self.0;
        delay.0 += delay_s.int().to_num::<u32>() * 1_000_000_000;

        let delay_ms = delay_s.frac() * 1000;
        delay.0 += delay_ms.int().to_num::<u32>() * 1_000_000;

        let delay_us = delay_ms.frac() * 1000;
        delay.0 += delay_us.int().to_num::<u32>() * 1_000;

        let delay_ns = delay_us.frac() * 1000;
        delay.0 += delay_ns.int().to_num::<u32>();

        Ok(delay)
    }
}

type Inner = fixed::FixedI64<typenum::U32>;