Wallacoloo/printipi

Add Servo Control

Closed this issue · 3 comments

Servos are controlled by periodically sending a pulse of a specified length.
The length of that pulse determines the position at which the servo should be placed, and the servo will attempt to stay at that location until it receives another pulse
Typical pulse lengths vary from 1ms to 2ms for the full control range and the pulses must occur between 40-200 times per second.

There are multiple ways to generate these pulses:

  • Use PWM
  • Schedule OutputEvents

Each has notable downsides. If we use PWM, it requires that the underlying hardware is capable of sustaining PWM cycles of at least about 10 ms (100 Hz) in length. It also requires a slight rework of the HardwareScheduler API (with a fixed-length buffer-based PWM system like PWM via DMA, the user can't just ask for 100Hz and expect to get it - the period must be an exact divisor of the buffer length).

If we do this via scheduling OutputEvents, then we have to deal with concurrent scheduling of motor steps and servo controls. In a way, we already deal with concurrent scheduling, in the form of scheduling multiple AxisSteppers side-by-side, and that turns out to be rather trivial. This could be generalized to concurrently scheduling all IoDrivers - just implement the method, getNextOutputEvent().

I think using OutputEvents is the better, and more future-proof choice here. The downside is that would require a great deal more refactoring.

In terms of concurrently scheduling multiple sources of OutputEvents, we have a few options:

  • Insert each AxisStepper into the ioDrivers tuple and move the logic in motionPlanner::_nextStep into the state, having it operate over all IoDrivers.
  • Keep the motionPlanner logic as-is, but add additional logic into the State that merges the motionPlanner's OutputEvent stream with streams from each IoDriver.

Advantages of Option 1:

  • Less redundancy (merge all streams in one pass, instead of two)
  • Placing stepper drivers into the ioDrivers tuple allows for easy g-code control of them (selectively enable/disable axis). This can still be done with the other option though.

Disadvantages of Option 1:

  • Requires much more refactoring.
    • Currently, AxisSteppers, ArcSteppers, and HomeSteppers are all separate objects - this implementation would suggest combining them into one object.
    • Now that the AxisSteppers must be mixed in with the rest of the IoDrivers, it's more difficult to query when a move is complete (would likely retain a tuple of references to the AxisSteppers within the IoDrivers list for this purpose).
    • Initializing moves is also more difficult. Would likely just enumerate all IoDrivers and notify them of the move. This does require sharing the ioDrivers tuple with the motionPlanner, which is slightly undesireable. But it also gives other IoDrivers the opportunity to listen for motion events.
  • The reported OutputEvent times from the AxisSteppers must be transformed by the AccelerationProfile, whereas OutputEvent times from the rest of the IoDrivers need not (this transformation is currently performed inside the motionPlanner).

Advantages of Option 2:

  • Easier to implement

Although Option 1 at first seems the cleaner choice to me, that bolded bullet point hints at an inconsistency. The transformation times could be achieved by wrapping the actual AxisSteppers inside another object that abstracts this, but at this point it seems as if I'm trying to make AxisSteppers fit a role they weren't meant to fit.

I will likely proceed with Option 2 sometime within the next 2 weeks.

Servos are now implemented in devel, though M-code control of them has not been added.

To use a servo, you must instantiate it and put it in the ioDrivers tuple. This can be done by returning it directly from Machine::getIoDrivers(), or, the CoordMap can instantiate it as a member variable and return a reference to it in its CoordMap::getDependentIoDrivers() function (useful if it's used in a homing/auto-level routine).

M-code control of them is now implemented in devel (M280 P<index> S<angle in degrees>). The index refers to the Nth Servo in existence, rather than the Nth IoDriver. So if the State's IoDrivers are tuple<Fan, Hotend, Servo0, Fan, Servo1>, then M280 P0 refers to Servo0, and M280 P1 refers to Servo1.

See the current cartesian machine for how to instantiate a Servo (refer to the getIoDrivers() function).

To see what the parameters mean, refer to servo.h

Note that Servos can be created by the CoordMap's getDependentIoDrivers function (useful when the servos are used for bed leveling, homing, etc).