esp_hal_servo/lib.rs
1//! A library for controlling servo motors using LEDC from [`esp-hal`](https://docs.rs/esp-hal/1.0.0/esp_hal/).
2//!
3//! This library provides two approaches for controlling servo motors:
4//!
5//! ## 1. Direct Angle Control
6//! Simply specify the desired angle from the servo's range and wait for it to reach the position.
7//! This is the simplest approach when you know the exact angle you want.
8//!
9//! ```no_run
10//! # use esp_hal_servo::*;
11//! # let mut servo = todo!();
12//! // Set servo to 42 degrees and wait for it to reach the position
13//! servo.set_angle(42.0);
14//! ```
15//!
16//! ## 2. Step-by-Step Control with Direction
17//! Specify the direction of movement and make a step. This approach gives you fine-grained
18//! control over the servo movement, allowing you to move it incrementally.
19//!
20//! ```no_run
21//! # use esp_hal_servo::*;
22//! # let mut servo = todo!();
23//! // Set direction to clockwise
24//! servo.set_dir(Dir::CW);
25//! // Make a step of 10 duty units
26//! servo.step(10.0)?;
27//! // Or make a step as a percentage of the total range
28//! servo.step_pct(5)?; // 5% of the range
29//! ```
30
31#![no_std]
32
33pub mod utils;
34
35use core::{
36 marker::PhantomData,
37 ops::{Neg, Range},
38};
39use esp_hal::{
40 gpio::DriveMode,
41 ledc::{
42 LSGlobalClkSource, Ledc,
43 channel::{self, Channel, ChannelHW, ChannelIFace},
44 timer::{self, Timer, TimerHW, TimerIFace, TimerSpeed, config::Duty},
45 },
46 time::Rate,
47};
48use log::{info, trace};
49
50#[derive(Debug, Clone)]
51pub struct ServoConfig {
52 /// Max angle that servo can be turned, mostly 180, 360.
53 pub max_angle: f32,
54 /// What frequency expect servo (ex. 50Hz for SG90).
55 pub frequency: Rate,
56 /// What pulse width in nanos servo supports (ex. 500000-2400000ns for SG90).
57 pub pulse_width_ns: Range<u32>,
58 /// PWM resolution in bits. Higher bits means more precise control.
59 pub duty: Duty,
60}
61
62impl ServoConfig {
63 /// Default servo configuration with 50Hz frequency and
64 /// pulse width range of 500000-2500000 ns (0.5-2.5ms).
65 pub fn default_servo(duty: Duty, max_angle: f32) -> Self {
66 ServoConfig {
67 max_angle,
68 frequency: Rate::from_hz(50),
69 // Standard servo pulse width range: 500-2500 us
70 pulse_width_ns: 500_000..2_500_000,
71 duty,
72 }
73 }
74
75 /// Config for [SG90](https://www.friendlywire.com/projects/ne555-servo-safe/SG90-datasheet.pdf).
76 /// Can be used for SG90s as well.
77 pub fn sg90(duty: Duty) -> Self {
78 Self {
79 pulse_width_ns: 500_000..2_400_000,
80 ..Self::default_servo(duty, 180.0)
81 }
82 }
83
84 /// Config for [MG995](https://www.electronicoscaldas.com/datasheet/MG995_Tower-Pro.pdf).
85 /// High-torque servo motor with metal gears.
86 /// Can be used for MG996, MG996R as well.
87 pub fn mg995(duty: Duty) -> Self {
88 Self::default_servo(duty, 180.0)
89 }
90
91 /// Helper function to configure a timer with this servo's configuration.
92 pub fn configure_timer<'a, S: TimerSpeed>(
93 &self,
94 ledc: &mut Ledc<'a>,
95 timer_num: timer::Number,
96 clock_source: S::ClockSourceType,
97 ) -> Result<Timer<'a, S>, timer::Error>
98 where
99 Timer<'a, S>: TimerHW<S>,
100 {
101 ledc.set_global_slow_clock(LSGlobalClkSource::APBClk);
102 let mut timer = ledc.timer::<S>(timer_num);
103 timer.configure(timer::config::Config {
104 duty: self.duty,
105 clock_source,
106 frequency: self.frequency,
107 })?;
108 Ok(timer)
109 }
110
111 /// Calculates duty range in absolute values for this servo configuration.
112 /// Returns absolute duty values (0..max_duty), not percentages.
113 pub fn calc_duty_range(&self, max_duty: f32) -> Range<f32> {
114 utils::calc_duty_range(
115 self.pulse_width_ns.clone(),
116 self.frequency.as_hz() as f32,
117 max_duty,
118 )
119 }
120
121 /// Transforms absolute duty value to angle in degrees.
122 /// Returns angle in degrees (0.0..max_angle).
123 pub fn duty_to_angle(&self, duty: f32, max_duty: f32, duty_range: &Range<f32>) -> f32 {
124 utils::duty_to_angle(duty, max_duty, duty_range)
125 }
126
127 /// Transforms angle in degrees to absolute duty value.
128 pub fn angle_to_duty(&self, angle: f32, duty_range: &Range<f32>) -> f32 {
129 utils::angle_to_duty(angle, self.max_angle, duty_range)
130 }
131}
132
133pub struct Servo<'a, S: TimerSpeed> {
134 name: &'static str,
135 channel: Channel<'a, S>,
136 /// Valid duty cycle range in absolute values (e.g., 102..491 for SG90 with 12-bit).
137 /// This corresponds to the pulse width range of the servo.
138 pub duty_range: Range<f32>,
139 config: ServoConfig,
140 /// Cached max duty value for further calculations.
141 max_duty: f32,
142 /// Current direction. Clockwise or counter-clockwise.
143 direction: Dir,
144 /// Current duty in absolute value (0..max_duty).
145 current_duty: f32,
146 _p: PhantomData<&'a mut ()>,
147}
148
149impl<'d, S: TimerSpeed> Servo<'d, S> {
150 /// Creates new servo driver instance for LEDC channel.
151 ///
152 /// # Arguments
153 ///
154 /// * `name` - Name identifier for the servo (for logging)
155 /// * `config` - Servo configuration
156 /// * `ledc` - LEDC peripheral instance
157 /// * `timer` - Configured timer instance (use `ServoConfig::configure_timer` to create it)
158 /// * `channel_num` - Channel number (e.g., `channel::Number::Channel0`)
159 /// * `pin` - GPIO pin to use for PWM output
160 pub fn new<'a>(
161 name: &'static str,
162 config: ServoConfig,
163 ledc: &mut Ledc<'a>,
164 timer: &'a Timer<'a, S>,
165 channel_num: channel::Number,
166 pin: impl esp_hal::gpio::OutputPin + 'a,
167 ) -> Result<Servo<'a, S>, channel::Error>
168 where
169 Timer<'a, S>: TimerHW<S>,
170 {
171 // Calculate max duty before configuring channel
172 let max_duty = match timer.duty() {
173 Some(duty) => (1u32 << duty as u32) - 1,
174 None => 4095, // Default to 12-bit if not configured
175 } as f32;
176
177 // Calculate duty range in absolute values
178 let duty_range = config.calc_duty_range(max_duty);
179
180 let mut channel = ledc.channel(channel_num, pin);
181 channel.configure(channel::config::Config {
182 timer,
183 duty_pct: 0,
184 drive_mode: DriveMode::PushPull,
185 })?;
186
187 let center_duty = duty_range.start + (duty_range.end - duty_range.start) / 2.0;
188 channel.set_duty_hw(center_duty as u32);
189
190 info!(
191 "{name} servo: duty_range={duty_range:?}, center_duty={center_duty}",
192 name = name,
193 duty_range = duty_range,
194 center_duty = center_duty,
195 );
196
197 Ok(Servo::<'a> {
198 name,
199 channel,
200 duty_range,
201 config,
202 direction: Dir::CW,
203 current_duty: center_duty,
204 max_duty,
205 _p: PhantomData,
206 })
207 }
208
209 /// Makes step in absolute duty units should be lesser than [`duty_range()`](Self::duty_range).
210 /// Return false if servo reaches min or max position.
211 /// See also [`step_pct()`](Self::step_pct) for percentage-based stepping.
212 /// Note: Step takes some time depending on servo speed.
213 pub fn step(&mut self, step_size: f32) -> Result<bool, channel::Error> {
214 let new_duty = self.calc_duty(step_size);
215
216 // Compare with epsilon to avoid floating point precision issues
217 if utils::approx_eq(new_duty, self.current_duty) {
218 return Ok(false);
219 }
220
221 // hardware method has better resolution
222 self.channel.set_duty_hw(new_duty as u32);
223 self.current_duty = new_duty;
224 trace!(
225 "{} servo step({}) to duty={}/{}",
226 &self.name, step_size, new_duty, self.max_duty
227 );
228 Ok(true)
229 }
230
231 /// Makes step in percentage of total range.
232 /// Returns false if servo reaches min or max position.
233 /// See also [`step()`](Self::step) for absolute duty-based stepping.
234 /// Note: Step takes some time depending on servo speed.
235 pub fn step_pct(&mut self, step_pct: u8) -> Result<bool, channel::Error> {
236 let step = (step_pct as f32 / 100.0) * self.duty_range();
237 self.step(step)
238 }
239
240 /// Set servo to move new direction.
241 /// Returns old direction if direction was actually changes.
242 pub fn set_dir(&mut self, dir: Dir) -> Option<Dir> {
243 if self.direction != dir {
244 let old = self.direction;
245 self.direction = dir;
246 Some(old)
247 } else {
248 None
249 }
250 }
251
252 /// Returns current direction value.
253 pub fn get_dir(&self) -> Dir {
254 self.direction
255 }
256
257 /// Returns current angle value in degrees.
258 pub fn get_angle(&self) -> f32 {
259 self.config
260 .duty_to_angle(self.current_duty, self.max_duty, &self.duty_range)
261 }
262
263 /// Sets servo to specified angle in degrees.
264 /// Note: turn to angle takes some time depending on servo speed.
265 pub fn set_angle(&mut self, angle: f32) -> bool {
266 let new_duty = self.config.angle_to_duty(angle, &self.duty_range);
267
268 let delta = new_duty - self.current_duty;
269 if delta > utils::EPSILON {
270 self.direction = Dir::CW;
271 } else if delta < utils::EPSILON.neg() {
272 self.direction = Dir::CCW;
273 } else {
274 return false;
275 }
276
277 self.channel.set_duty_hw(new_duty as u32);
278 self.current_duty = new_duty;
279 true
280 }
281
282 /// Returns the size of the duty range (difference between max and min duty values).
283 /// This is not the number of steps, but the range size in duty units.
284 pub fn duty_range(&self) -> f32 {
285 self.duty_range.end - self.duty_range.start
286 }
287
288 /// Calculates new duty based on current direction and step size.
289 /// Returns clamped duty value within valid range.
290 fn calc_duty(&self, step: f32) -> f32 {
291 let new_duty = match self.direction {
292 Dir::CW => {
293 // Move clockwise (increase duty)
294 self.current_duty + step
295 }
296 Dir::CCW => {
297 // Move counter-clockwise (decrease duty)
298 self.current_duty - step
299 }
300 };
301
302 let min_duty = self.duty_range.start;
303 let max_duty = self.duty_range.end - utils::EPSILON;
304 new_duty.clamp(min_duty, max_duty)
305 }
306}
307
308#[derive(PartialEq, Eq, Clone, Copy, Debug)]
309pub enum Dir {
310 /// Clockwise, increases angle.
311 CW,
312 /// Counter-clockwise, decreases angle.
313 CCW,
314}