Question about "itemised list" of sfoc functionalityies (SFOC-rs rust rewrite)

So I’m doing a rust rewrite of sfoc in rust, and I’m trying to encapsulate the different elements purely in terms of interfaces. My question "What traits, methods, compile-time settings, and runtime-controlable configurations, etc are needed to provide the prerequisites of "a thing that can run an foc_loop forever?

The Rust code at present:

After using a platform specific implementation to create a struct (let’s name it driver), you can then implement:

  • PhasePwmPins: giving you the PhasePwmPins::set_pwms(&mut driver, ....) method
  • FocController: Giving the ``FocController::set_phase_angle(&mut driver, …)` method
  • PosSensor: Giving let pos = PosSensor::read_elec_angle(&driver);
  • With PosSensor, and TimeSource implemented, you can then implement MotionTracker, giving access to the that traits interface, which would include methods such as:
    • push_pos_time
    • latest_velocity
    • latest_accel
    • latest_jerk
    • telemtetry_dump

So if you want to just set foo_pwms, you do this in rust code:

#[esp_hal::entry]
fn main() -> ! {
    // esp32 boilerplate to get access/control of relevant peripherals
    let peripherals = Peripherals::take();
    let system = peripherals.SYSTEM.split();
    let clock_ctrl = ClockControl::boot_defaults(system.clock_control);
    let clocks: Clocks = clock_ctrl.freeze();

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    let pins = io.pins;

    // Pass peripherals into driver constructor
    let mut driver = Esp3PWM::new(
        &clocks,
        peripherals.TIMG0,
        peripherals.MCPWM0,
        peripherals.PCNT,
        (pins.gpio1, pins.gpio2, pins.gpio3),
        (pins.gpio4, pins.gpio5),
    );

    // set duty cycles to "meaning of life, universe and everything"
    let foo_a = DutyCycle(0.42f32);
    let foo_b = DutyCycle(0.42f32);
    let foo_c = DutyCycle(0.42f32);
    MotorHiPins::set_pwms(&mut driver, foo_a, foo_b, foo_c);

    // and let's not burn things out?
    MotorHiPins::set_zero(&mut driver);

    loop {}
}

…you could, of course, use the different elements to bring together the different things that make up FOC to create a true FOC driver.

// If something implements MotorHiPins, PosSensor, TimeSorce, etc..., then it can expose the
// FOCMotor interface
pub trait FOCMotor: MotorHiPins + PosSensor + TimeSource + etc... {
    pub fn default_foc_loop(self) -> ! {
        ...
    }
}

So back to my original question: What are some of the pre-requisites and interfaces needed in order to provide the end user with a “primary playground” looking a bit like this?:

// Pass in a thing with all the resources needed to make a motor...
fn portable_main(mut driver_uninit: impl UninitFOCMotor) -> ! {
    // Let's adjust the default initial config a touch...
    let foc_initial_config = ...;
    // And add some "extras"...
    let driver = driver_uninit
        // Pass something in to define the nature of this listener. spi? usb? tx/rx channel?
        .with_listen_commands(...);
        // suppose we want to cache telemetry such as movement, phase-energisation, etc...
        .with_telemetry_dump_channel(...);
        // Probably pass in a UART thing of some sort here
        .with_debug_output(...)
        .set_psu_decivoltage_limit::<U120>()
        .set_some_other_compiletime_limit::<U42>()
        // ...and point of no-return. 
        // Before: - An interface backed by a platform specific struct that has cached things 
        //         like gpio pins, pulse-count peripherals, etc.
        //         - Compile-time configurations baked into the generics of the contructor.
        // After:  - An interface backed by a platform spicific struct that owns initialised
        //         hardware such as pwm pins, pulse counters, etc.
        //         - Compile-time configurations such as voltage limit, speed limit, etc.
        //           optimised and baked directly into the compiled binary.
        .init(foc_initial_config);

    // jump into the infinate foc-loop 
    driver.foc_loop()
}

Hey,

I think a primary question to ask is what objective you are trying to achieve?

I’m of course quite partial to SimpleFOC’s style of abstraction based on Objects - it follows the Arduino convention and is easy for users to understand. The principal objects used follow the hardware the user can touch: motor, driver, sensor. In addition there are few more easy to understand objects like “current sense”, “Commander” and “SimpleFOCDebug”.
Of course it also has some disadvantages, but I think it is consistent with our goal to make FOC easy and accessible.

An implementation whose primary focus was performance would probably choose a different API.

Another way to look at it would be from the control theory standpoint, in the sense that what is actually implemented is (for example) this:

But more advanced users could have different ideas on how to implement each of the boxes in the diagram above. So an API that would allow you to compose a control process out of “control primitives” would also make a lot of sense (to me at least :smiley: ).

In terms of the sample code you show, this makes sense and looks quite clean, although seeing “decivolts” makes me cringe - I can’t think of any reason in the world why one would want to make the user convert anything into decivolts in order to use the code, it’s almost like an invitation to make mistakes. Use simple units like Volts, Amps, Ohms to make it easy for the user.

Regarding compile-time configurations, note that the only compile-time constants are things like the pin-numbers used for the driver, etc, since these really can’t conceivably change during runtime. But any kind of limit like the PSU voltage could certainly change – if using a battery for example voltage would gradually drop as the battery discharges. It’s also worth considering the case that the code starts with no configuration except the pins because the configuration is set at runtime via a GUI or external controller.

As someone who doesn’t know much about Rust, but has looked at the low level code for the timers for some different MCU support crates in the embedded rust world, I would say that I would prioritise actually making a motor move, and then worry about the API afterwards. As I understand Rust implementing external traits involves dynamic dispatch, and I’ll be really interested to hear what the performance will be like… I have no concept at all what to expect there.

1 Like

Decivolts was due to a limitation of something under the hood. I didn’t like it myself, but if it’s not a compile time thing, that can easily be resolved. I’ll probably go for millivolts, so as to avoid floats.

rust can very easy not use dynamic dispatch. dyn dispatch happens when you use trait-objects:

// the argument can point upwards in the stack to a struct + vtable fat-pointer
// the Box is a smart-pointer onto the heap. the `dyn TraitThing`, is same style fat-pointer
fn foo(trait_obj: impl SomeTrait) -> Box<dyn SomeOtherTrait>;

// not dynamic dispatch: compiler "re-writes" the function to use the actual object:
// If `Bar` and `Foo` implements `SomeTrait`, thing would effectively become two functions
// at compile time, one with a function signature taking a `Foo` argument, one taking a `Bar`, and its
// content would be the trait-impls content.
fn bar<T: SomeTrait, O: OtherTrait>(non_dyn: T) -> O;