Skip to main content

pamoja_actuators/
stepper.rs

1//! Coil sequencing for four-wire stepper motors.
2//!
3//! A stepper turns by energising its coils in a repeating pattern; stepping through
4//! the pattern in one direction or the other advances or reverses the shaft. This
5//! module holds the three standard drive patterns and a sequencer that walks them, so
6//! a caller toggles the four coil lines (directly, or through a darlington array like
7//! the ULN2003) without hand-maintaining the sequence. It also models the
8//! step-and-direction interface of driver chips such as the A4988 and DRV8825, which
9//! take a step pulse and a direction level and track position as a signed count.
10//!
11//! Coil patterns are four bits, the most significant being the first coil (IN1 on a
12//! ULN2003 board) down to the least significant (IN4).
13
14/// Which way to step.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum Direction {
17    /// Advance the sequence, turning the shaft one way.
18    Forward,
19    /// Reverse the sequence, turning the shaft the other way.
20    Backward,
21}
22
23/// A stepper drive pattern.
24///
25/// The three patterns trade torque, smoothness, and resolution. Wave drive energises
26/// one coil at a time (least torque, least power); full-step energises two at a time
27/// (most torque); half-step alternates between them to double the resolution at the
28/// cost of uneven torque.
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30pub enum Drive {
31    /// One coil energised at a time: four steps.
32    Wave,
33    /// Two adjacent coils energised at a time: four steps, more torque.
34    FullStep,
35    /// Alternating one and two coils: eight steps, double resolution.
36    HalfStep,
37}
38
39// The four wave-drive coil patterns, one coil at a time.
40const WAVE: [u8; 4] = [0b1000, 0b0100, 0b0010, 0b0001];
41// The four full-step patterns, two adjacent coils at a time.
42const FULL_STEP: [u8; 4] = [0b1100, 0b0110, 0b0011, 0b1001];
43// The eight half-step patterns, interleaving wave and full-step.
44const HALF_STEP: [u8; 8] = [
45    0b1000, 0b1100, 0b0100, 0b0110, 0b0010, 0b0011, 0b0001, 0b1001,
46];
47
48impl Drive {
49    /// Returns the coil patterns for this drive, in forward order.
50    ///
51    /// # Returns
52    ///
53    /// A slice of four-bit coil patterns: four for wave and full-step, eight for
54    /// half-step.
55    pub fn pattern(self) -> &'static [u8] {
56        match self {
57            Drive::Wave => &WAVE,
58            Drive::FullStep => &FULL_STEP,
59            Drive::HalfStep => &HALF_STEP,
60        }
61    }
62
63    /// Returns how many steps make up one full electrical cycle of this drive.
64    ///
65    /// # Returns
66    ///
67    /// `4` for wave and full-step, `8` for half-step.
68    pub fn step_count(self) -> usize {
69        self.pattern().len()
70    }
71}
72
73/// A position in a drive sequence, walked one step at a time.
74///
75/// Holding the index into the pattern, a sequencer turns each [`step`](Sequencer::step)
76/// into the next coil pattern to apply, wrapping around the cycle so it can run
77/// indefinitely in either direction.
78///
79/// # Examples
80///
81/// ```
82/// use pamoja_actuators::stepper::{Direction, Drive, Sequencer};
83///
84/// let mut sequencer = Sequencer::new(Drive::HalfStep);
85/// assert_eq!(sequencer.coils(), 0b1000);
86/// assert_eq!(sequencer.step(Direction::Forward), 0b1100);
87///
88/// // One full electrical cycle returns to the start.
89/// for _ in 1..Drive::HalfStep.step_count() {
90///     sequencer.step(Direction::Forward);
91/// }
92/// assert_eq!(sequencer.coils(), 0b1000);
93/// ```
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub struct Sequencer {
96    drive: Drive,
97    index: usize,
98}
99
100impl Sequencer {
101    /// Creates a sequencer at the start of a drive pattern.
102    ///
103    /// # Arguments
104    ///
105    /// * `drive` - the drive pattern to walk.
106    ///
107    /// # Returns
108    ///
109    /// A sequencer whose current pattern is the first in the drive.
110    pub fn new(drive: Drive) -> Sequencer {
111        Sequencer { drive, index: 0 }
112    }
113
114    /// Returns the coil pattern at the current position.
115    ///
116    /// # Returns
117    ///
118    /// The four-bit coil pattern to apply now.
119    pub fn coils(&self) -> u8 {
120        self.drive.pattern()[self.index]
121    }
122
123    /// Advances one step in `direction` and returns the new coil pattern.
124    ///
125    /// The index wraps around the cycle, so stepping forever in either direction is
126    /// well defined.
127    ///
128    /// # Arguments
129    ///
130    /// * `direction` - which way to step.
131    ///
132    /// # Returns
133    ///
134    /// The coil pattern to apply after the step.
135    pub fn step(&mut self, direction: Direction) -> u8 {
136        let count = self.drive.step_count();
137        self.index = match direction {
138            Direction::Forward => (self.index + 1) % count,
139            Direction::Backward => (self.index + count - 1) % count,
140        };
141        self.coils()
142    }
143
144    /// Returns the drive pattern this sequencer walks.
145    pub fn drive(&self) -> Drive {
146        self.drive
147    }
148}
149
150/// A signed step counter for step-and-direction driver chips.
151///
152/// Driver chips like the A4988 and DRV8825 move one (micro)step per pulse in the
153/// direction set on a level pin, so the host just counts. This tracks that count, so
154/// position is known without reading the hardware.
155#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
156pub struct Position {
157    steps: i32,
158}
159
160impl Position {
161    /// Creates a position at zero.
162    ///
163    /// # Returns
164    ///
165    /// A position whose step count is zero.
166    pub fn new() -> Position {
167        Position { steps: 0 }
168    }
169
170    /// Records one step in `direction` and returns the new step count.
171    ///
172    /// # Arguments
173    ///
174    /// * `direction` - which way the pulse moved the motor.
175    ///
176    /// # Returns
177    ///
178    /// The updated step count, increasing for [`Direction::Forward`].
179    pub fn step(&mut self, direction: Direction) -> i32 {
180        self.steps += match direction {
181            Direction::Forward => 1,
182            Direction::Backward => -1,
183        };
184        self.steps
185    }
186
187    /// Returns the current step count.
188    pub fn steps(self) -> i32 {
189        self.steps
190    }
191}
192
193/// Converts an angle to a whole number of steps for a given motor.
194///
195/// # Arguments
196///
197/// * `degrees` - the angle to turn; negative turns the other way.
198/// * `steps_per_revolution` - the motor's steps per full turn, for example 200 for a
199///   1.8-degree motor.
200///
201/// # Returns
202///
203/// The nearest whole number of steps to that angle.
204pub fn steps_for_degrees(degrees: f32, steps_per_revolution: u32) -> i32 {
205    // `f32::round` lives in `std`; casting to `i32` truncates toward zero, so adding a
206    // signed half first rounds to nearest without pulling in a floating-point library.
207    let scaled = degrees / 360.0 * steps_per_revolution as f32;
208    let half = if scaled >= 0.0 { 0.5 } else { -0.5 };
209    (scaled + half) as i32
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn drive_patterns_match_the_standard_sequences() {
218        assert_eq!(Drive::Wave.pattern(), &[0b1000, 0b0100, 0b0010, 0b0001]);
219        assert_eq!(Drive::FullStep.pattern(), &[0b1100, 0b0110, 0b0011, 0b1001]);
220        assert_eq!(
221            Drive::HalfStep.pattern(),
222            &[0b1000, 0b1100, 0b0100, 0b0110, 0b0010, 0b0011, 0b0001, 0b1001]
223        );
224    }
225
226    #[test]
227    fn half_step_interleaves_wave_and_full_step() {
228        let half = Drive::HalfStep.pattern();
229        for i in 0..4 {
230            assert_eq!(half[2 * i], Drive::Wave.pattern()[i]);
231            assert_eq!(half[2 * i + 1], Drive::FullStep.pattern()[i]);
232        }
233    }
234
235    #[test]
236    fn a_full_cycle_returns_to_the_start() {
237        for drive in [Drive::Wave, Drive::FullStep, Drive::HalfStep] {
238            let mut sequencer = Sequencer::new(drive);
239            let start = sequencer.coils();
240            for _ in 0..drive.step_count() {
241                sequencer.step(Direction::Forward);
242            }
243            assert_eq!(sequencer.coils(), start);
244        }
245    }
246
247    #[test]
248    fn stepping_back_undoes_a_step_forward() {
249        let mut sequencer = Sequencer::new(Drive::FullStep);
250        let start = sequencer.coils();
251        sequencer.step(Direction::Forward);
252        assert_eq!(sequencer.step(Direction::Backward), start);
253    }
254
255    #[test]
256    fn backward_from_the_start_wraps_to_the_last_pattern() {
257        let mut sequencer = Sequencer::new(Drive::Wave);
258        assert_eq!(sequencer.step(Direction::Backward), 0b0001);
259    }
260
261    #[test]
262    fn position_counts_signed_steps() {
263        let mut position = Position::new();
264        assert_eq!(position.step(Direction::Forward), 1);
265        assert_eq!(position.step(Direction::Forward), 2);
266        assert_eq!(position.step(Direction::Backward), 1);
267        assert_eq!(position.steps(), 1);
268    }
269
270    #[test]
271    fn degrees_convert_to_steps() {
272        assert_eq!(steps_for_degrees(360.0, 200), 200);
273        assert_eq!(steps_for_degrees(90.0, 200), 50);
274        assert_eq!(steps_for_degrees(-90.0, 200), -50);
275        // A 1.8-degree motor: one step is 1.8 degrees.
276        assert_eq!(steps_for_degrees(1.8, 200), 1);
277    }
278}