Skip to main content

pamoja_actuators/
stepper.rs

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