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}