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}