Skip to main content

device_envoy_esp/
servo.rs

1//! A device abstraction for hobby servos on ESP LEDC PWM.
2//!
3//! This module provides both direct servo control ([`servo!`](macro@crate::servo::servo)) and
4//! servo animationl ([`servo_player!`](macro@crate::servo::servo_player)).
5//!
6//! Use [`servo!`] for a keyword-driven typed constructor.
7//!
8//! **After reading the examples below, see also:**
9//!
10//! - [`servo!`](macro@crate::servo::servo) — Direct servo control without animation support.
11//! - [`servo_player!`](macro@crate::servo::servo_player) — Macro to generate a servo player struct
12//!   type (includes syntax details). See [`ServoPlayerGenerated`](servo_player_generated::ServoPlayerGenerated)
13//!   for a sample of a generated type.
14//! - [`combine!`](macro@crate::servo::combine) & [`linear`] — Macro and function for creating
15//!   complex motion sequences.
16//! - [`Servo`] — Trait defining core methods and constants for direct servo control.
17//! - [`ServoPlayer`] — Trait defining core methods and constants for animatable servos.
18//!
19#![doc = include_str!("../docs/how_servos_work.md")]
20//!
21//! This device abstraction, `servo_player`, adds a background software task around the hardware
22//! control signal.
23//!
24//! # Controlling Multiple Servos
25//!
26//! Supports multiple servos, where each servo consumes one [LEDC](crate#glossary) timer resource and
27//! one [LEDC](crate#glossary) channel resource. The macro-generated link-time ownership claims enforce
28//! this exclusivity so duplicate timer/channel selections fail at link time.
29//!
30//! # Example: Basic Servo Control
31//!
32//! This example demonstrates basic servo control: moving to a position, relaxing,
33//! and using animation. Here, the generated struct type is named `Servo11`.
34//!
35//! ```rust,no_run
36//! # #![no_std]
37//! # #![no_main]
38//! # use esp_backtrace as _;
39//! # use core::convert::Infallible;
40//! use device_envoy_esp::{Result, init_and_start, servo::{Servo as _, servo}};
41//! use embassy_time::{Duration, Timer};
42//!
43//! servo! {
44//!     Servo11 {
45//!         pin: GPIO11,
46//!         timer: Timer0,
47//!         channel: Channel0,
48//!     }
49//! }
50//!
51//! # #[esp_rtos::main]
52//! # async fn main(_spawner: embassy_executor::Spawner) -> ! {
53//! #     let err = inner_main().await.unwrap_err();
54//! #     panic!("{err:?}");
55//! # }
56//! async fn inner_main() -> Result<Infallible> {
57//!     init_and_start!(p, ledc: ledc);
58//!     let servo11 = Servo11::new(&ledc, p.GPIO11)?;
59//!     servo11.set_degrees(45);
60//!     Timer::after(Duration::from_secs(1)).await;
61//!     servo11.relax();
62//!     core::future::pending().await
63//! }
64//! ```
65//!
66//! # Example: Multi-Step Animation
67//!
68//! This example combines 40 animation steps using `linear` and `combine!` to
69//! sweep up, hold, sweep down, hold pattern. Here, the generated struct type is named
70//! `ServoSweep`.
71//!
72//! ```rust,no_run
73//! # #![no_std]
74//! # #![no_main]
75//! # use esp_backtrace as _;
76//! # use core::convert::Infallible;
77//! use device_envoy_esp::{Result, init_and_start, servo::{AtEnd, Servo as _, ServoPlayer as _, combine, linear, servo_player}};
78//! use embassy_time::Duration;
79//!
80//! servo_player! {
81//!     ServoSweep {
82//!         pin: GPIO12,
83//!         timer: Timer1,
84//!         channel: Channel1,
85//!         max_steps: 40,
86//!     }
87//! }
88//!
89//! # #[esp_rtos::main]
90//! # async fn main(spawner: embassy_executor::Spawner) -> ! {
91//! #     let err = inner_main(spawner).await.unwrap_err();
92//! #     panic!("{err:?}");
93//! # }
94//! async fn inner_main(spawner: embassy_executor::Spawner) -> Result<Infallible> {
95//!     init_and_start!(p, ledc: ledc);
96//!     let servo_sweep = ServoSweep::new(&ledc, p.GPIO12, spawner)?;
97//!     const STEPS: [(u16, Duration); 40] = combine!(
98//!         linear::<19>(0, 180, Duration::from_secs(2)),
99//!         [(180, Duration::from_millis(400))],
100//!         linear::<19>(180, 0, Duration::from_secs(2)),
101//!         [(0, Duration::from_millis(400))]
102//!     );
103//!     servo_sweep.animate(STEPS, AtEnd::Loop);
104//!     core::future::pending().await
105//! }
106//! ```
107
108use crate::Result;
109use core::cell::RefCell;
110pub use device_envoy_core::servo::Servo;
111use esp_hal::gpio::{interconnect::PeripheralOutput, DriveMode};
112use esp_hal::ledc::{channel, timer, LowSpeed};
113use esp_hal::ledc::{channel::ChannelIFace, timer::TimerIFace};
114use esp_hal::time::Rate;
115use static_cell::StaticCell;
116
117#[doc(inline)]
118pub use crate::combine;
119#[doc(inline)]
120pub use crate::servo_player::servo_player;
121#[doc(hidden)]
122pub use device_envoy_core::servo::{
123    __servo_player_animate, __servo_player_hold, __servo_player_relax, __servo_player_set_degrees,
124    device_loop,
125};
126pub use device_envoy_core::servo::{
127    combine, linear, AtEnd, ServoPlayer, ServoPlayerHandle, ServoPlayerStatic,
128};
129
130/// Sample generated servo-player type documentation.
131pub mod servo_player_generated {
132    #[cfg(doc)]
133    pub use crate::servo_player::servo_player_generated::*;
134}
135
136const SERVO_PERIOD_US: u32 = 20_000;
137
138/// Default minimum pulse width for hobby servos (microseconds).
139pub const SERVO_MIN_US_DEFAULT: u32 = 500;
140
141/// Default maximum pulse width for hobby servos (microseconds).
142pub const SERVO_MAX_US_DEFAULT: u32 = 2_500;
143
144/// LEDC-backed servo static resources and motion configuration.
145pub struct ServoStatic {
146    timer: StaticCell<timer::Timer<'static, LowSpeed>>,
147    channel: StaticCell<channel::Channel<'static, LowSpeed>>,
148    timer_number: timer::Number,
149    channel_number: channel::Number,
150    min_us: u32,
151    max_us: u32,
152    max_degrees: u16,
153}
154
155impl ServoStatic {
156    /// Create static resources for one servo output.
157    #[must_use]
158    pub const fn new_static(
159        timer_number: timer::Number,
160        channel_number: channel::Number,
161        min_us: u32,
162        max_us: u32,
163        max_degrees: u16,
164    ) -> Self {
165        assert!(min_us < max_us, "min_us must be less than max_us");
166        assert!(max_degrees > 0, "max_degrees must be positive");
167        Self {
168            timer: StaticCell::new(),
169            channel: StaticCell::new(),
170            timer_number,
171            channel_number,
172            min_us,
173            max_us,
174            max_degrees,
175        }
176    }
177}
178
179/// A direct servo output using one LEDC timer and one LEDC channel.
180///
181/// This type is public only because `servo!` and `servo_player!` macro
182/// expansions in downstream crates reference it.
183#[doc(hidden)]
184pub struct ServoEsp {
185    channel: RefCell<&'static mut channel::Channel<'static, LowSpeed>>,
186    min_us: u32,
187    max_us: u32,
188    max_degrees: u16,
189}
190
191impl ServoEsp {
192    /// Default maximum rotation range in degrees.
193    pub const DEFAULT_MAX_DEGREES: u16 = <Self as Servo>::DEFAULT_MAX_DEGREES;
194
195    /// Create a servo from static resources and a GPIO output pin.
196    pub fn new(
197        servo_static: &'static ServoStatic,
198        ledc: &esp_hal::ledc::Ledc<'static>,
199        pin: impl PeripheralOutput<'static>,
200    ) -> Result<Self> {
201        let timer = servo_static
202            .timer
203            .init(ledc.timer::<LowSpeed>(servo_static.timer_number));
204        timer.configure(timer::config::Config {
205            duty: timer::config::Duty::Duty14Bit,
206            clock_source: timer::LSClockSource::APBClk,
207            frequency: Rate::from_hz(50),
208        })?;
209
210        let channel = servo_static
211            .channel
212            .init(ledc.channel(servo_static.channel_number, pin));
213        channel.configure(channel::config::Config {
214            timer,
215            duty_pct: 0,
216            drive_mode: DriveMode::PushPull,
217        })?;
218
219        Ok(Self {
220            channel: RefCell::new(channel),
221            min_us: servo_static.min_us,
222            max_us: servo_static.max_us,
223            max_degrees: servo_static.max_degrees,
224        })
225    }
226
227    fn pulse_for_degrees(&self, degrees: u16) -> u32 {
228        let pulse_span = self.max_us - self.min_us;
229        self.min_us
230            + (u32::from(degrees) * pulse_span + u32::from(self.max_degrees / 2))
231                / u32::from(self.max_degrees)
232    }
233
234    fn degrees_to_duty_pct(&self, degrees: u16) -> u8 {
235        let pulse_us = self.pulse_for_degrees(degrees);
236        let duty_pct = ((pulse_us * 100) + (SERVO_PERIOD_US / 2)) / SERVO_PERIOD_US;
237        assert!(duty_pct <= u8::MAX as u32);
238        duty_pct as u8
239    }
240}
241
242impl Servo for ServoEsp {
243    const DEFAULT_MAX_DEGREES: u16 = 180;
244
245    /// Set position in degrees `0..=max_degrees`.
246    fn set_degrees(&self, degrees: u16) {
247        assert!(degrees <= self.max_degrees);
248        let duty_pct = self.degrees_to_duty_pct(degrees);
249        self.channel
250            .borrow_mut()
251            .set_duty(duty_pct)
252            .expect("LEDC set_duty failed in Servo::set_degrees");
253    }
254
255    /// Keep driving pulses at the last commanded angle.
256    fn hold(&self) {}
257
258    /// Stop driving pulses.
259    fn relax(&self) {
260        self.channel
261            .borrow_mut()
262            .set_duty(0)
263            .expect("LEDC set_duty failed in Servo::relax");
264    }
265}
266
267#[doc(hidden)]
268pub use paste;
269
270/// Macro to generate a direct-servo struct type (includes syntax details).
271///
272/// **See the [servo module documentation](mod@crate::servo) for usage examples.**
273///
274/// **Syntax:**
275///
276/// ```text
277/// servo! {
278///     <Name> {
279///         pin: <pin_ident>,
280///         timer: <timer_ident>,
281///         channel: <channel_ident>,
282///         min_us: <u32_expr>,         // optional
283///         max_us: <u32_expr>,         // optional
284///         max_degrees: <u16_expr>,    // optional
285///     }
286/// }
287/// ```
288///
289/// **Required fields:**
290///
291/// - `pin` - GPIO pin for servo output
292/// - `timer` - [LEDC](crate#glossary) timer resource
293/// - `channel` - [LEDC](crate#glossary) channel resource
294///
295/// **Optional fields:**
296///
297/// - `min_us` - Minimum pulse width in microseconds for 0° (default: `500`)
298/// - `max_us` - Maximum pulse width in microseconds for `max_degrees` (default: `2500`)
299/// - `max_degrees` - Maximum servo angle in degrees (default: `180`)
300///
301/// See the [servo module documentation](mod@crate::servo) for details and examples.
302#[macro_export]
303#[doc(hidden)]
304macro_rules! servo {
305    ($($tt:tt)*) => { $crate::__servo_impl! { $($tt)* } };
306}
307#[doc(inline)]
308pub use servo;
309
310/// Public for macro expansion in downstream crates.
311#[doc(hidden)]
312#[macro_export]
313macro_rules! __servo_impl {
314    (
315        $name:ident {
316            $($fields:tt)*
317        }
318    ) => {
319        $crate::__servo_impl! {
320            @__parse
321            name: $name,
322            pin: [],
323            timer: [],
324            channel: [],
325            min_us: [],
326            max_us: [],
327            max_degrees: [],
328            fields: [ $($fields)* ]
329        }
330    };
331
332    (@__parse
333        name: $name:ident,
334        pin: [],
335        timer: [$($timer:ident)?],
336        channel: [$($channel:ident)?],
337        min_us: [$($min_us:expr)?],
338        max_us: [$($max_us:expr)?],
339        max_degrees: [$($max_degrees:expr)?],
340        fields: [ pin: $pin:ident $(, $($rest:tt)*)? ]
341    ) => {
342        $crate::__servo_impl! {
343            @__parse
344            name: $name,
345            pin: [$pin],
346            timer: [$($timer)?],
347            channel: [$($channel)?],
348            min_us: [$($min_us)?],
349            max_us: [$($max_us)?],
350            max_degrees: [$($max_degrees)?],
351            fields: [ $($($rest)*)? ]
352        }
353    };
354    (@__parse
355        name: $name:ident,
356        pin: [$_pin_seen:ident],
357        timer: [$($timer:ident)?],
358        channel: [$($channel:ident)?],
359        min_us: [$($min_us:expr)?],
360        max_us: [$($max_us:expr)?],
361        max_degrees: [$($max_degrees:expr)?],
362        fields: [ pin: $pin:ident $(, $($rest:tt)*)? ]
363    ) => {
364        compile_error!("servo! duplicate `pin` field");
365    };
366
367    (@__parse
368        name: $name:ident,
369        pin: [$($pin:ident)?],
370        timer: [],
371        channel: [$($channel:ident)?],
372        min_us: [$($min_us:expr)?],
373        max_us: [$($max_us:expr)?],
374        max_degrees: [$($max_degrees:expr)?],
375        fields: [ timer: $timer:ident $(, $($rest:tt)*)? ]
376    ) => {
377        $crate::__servo_impl! {
378            @__parse
379            name: $name,
380            pin: [$($pin)?],
381            timer: [$timer],
382            channel: [$($channel)?],
383            min_us: [$($min_us)?],
384            max_us: [$($max_us)?],
385            max_degrees: [$($max_degrees)?],
386            fields: [ $($($rest)*)? ]
387        }
388    };
389    (@__parse
390        name: $name:ident,
391        pin: [$($pin:ident)?],
392        timer: [$_timer_seen:ident],
393        channel: [$($channel:ident)?],
394        min_us: [$($min_us:expr)?],
395        max_us: [$($max_us:expr)?],
396        max_degrees: [$($max_degrees:expr)?],
397        fields: [ timer: $timer:ident $(, $($rest:tt)*)? ]
398    ) => {
399        compile_error!("servo! duplicate `timer` field");
400    };
401
402    (@__parse
403        name: $name:ident,
404        pin: [$($pin:ident)?],
405        timer: [$($timer:ident)?],
406        channel: [],
407        min_us: [$($min_us:expr)?],
408        max_us: [$($max_us:expr)?],
409        max_degrees: [$($max_degrees:expr)?],
410        fields: [ channel: $channel:ident $(, $($rest:tt)*)? ]
411    ) => {
412        $crate::__servo_impl! {
413            @__parse
414            name: $name,
415            pin: [$($pin)?],
416            timer: [$($timer)?],
417            channel: [$channel],
418            min_us: [$($min_us)?],
419            max_us: [$($max_us)?],
420            max_degrees: [$($max_degrees)?],
421            fields: [ $($($rest)*)? ]
422        }
423    };
424    (@__parse
425        name: $name:ident,
426        pin: [$($pin:ident)?],
427        timer: [$($timer:ident)?],
428        channel: [$_channel_seen:ident],
429        min_us: [$($min_us:expr)?],
430        max_us: [$($max_us:expr)?],
431        max_degrees: [$($max_degrees:expr)?],
432        fields: [ channel: $channel:ident $(, $($rest:tt)*)? ]
433    ) => {
434        compile_error!("servo! duplicate `channel` field");
435    };
436
437    (@__parse
438        name: $name:ident,
439        pin: [$($pin:ident)?],
440        timer: [$($timer:ident)?],
441        channel: [$($channel:ident)?],
442        min_us: [],
443        max_us: [$($max_us:expr)?],
444        max_degrees: [$($max_degrees:expr)?],
445        fields: [ min_us: $min_us:expr $(, $($rest:tt)*)? ]
446    ) => {
447        $crate::__servo_impl! {
448            @__parse
449            name: $name,
450            pin: [$($pin)?],
451            timer: [$($timer)?],
452            channel: [$($channel)?],
453            min_us: [$min_us],
454            max_us: [$($max_us)?],
455            max_degrees: [$($max_degrees)?],
456            fields: [ $($($rest)*)? ]
457        }
458    };
459    (@__parse
460        name: $name:ident,
461        pin: [$($pin:ident)?],
462        timer: [$($timer:ident)?],
463        channel: [$($channel:ident)?],
464        min_us: [$_min_us_seen:expr],
465        max_us: [$($max_us:expr)?],
466        max_degrees: [$($max_degrees:expr)?],
467        fields: [ min_us: $min_us:expr $(, $($rest:tt)*)? ]
468    ) => {
469        compile_error!("servo! duplicate `min_us` field");
470    };
471
472    (@__parse
473        name: $name:ident,
474        pin: [$($pin:ident)?],
475        timer: [$($timer:ident)?],
476        channel: [$($channel:ident)?],
477        min_us: [$($min_us:expr)?],
478        max_us: [],
479        max_degrees: [$($max_degrees:expr)?],
480        fields: [ max_us: $max_us:expr $(, $($rest:tt)*)? ]
481    ) => {
482        $crate::__servo_impl! {
483            @__parse
484            name: $name,
485            pin: [$($pin)?],
486            timer: [$($timer)?],
487            channel: [$($channel)?],
488            min_us: [$($min_us)?],
489            max_us: [$max_us],
490            max_degrees: [$($max_degrees)?],
491            fields: [ $($($rest)*)? ]
492        }
493    };
494    (@__parse
495        name: $name:ident,
496        pin: [$($pin:ident)?],
497        timer: [$($timer:ident)?],
498        channel: [$($channel:ident)?],
499        min_us: [$($min_us:expr)?],
500        max_us: [$_max_us_seen:expr],
501        max_degrees: [$($max_degrees:expr)?],
502        fields: [ max_us: $max_us:expr $(, $($rest:tt)*)? ]
503    ) => {
504        compile_error!("servo! duplicate `max_us` field");
505    };
506
507    (@__parse
508        name: $name:ident,
509        pin: [$($pin:ident)?],
510        timer: [$($timer:ident)?],
511        channel: [$($channel:ident)?],
512        min_us: [$($min_us:expr)?],
513        max_us: [$($max_us:expr)?],
514        max_degrees: [],
515        fields: [ max_degrees: $max_degrees:expr $(, $($rest:tt)*)? ]
516    ) => {
517        $crate::__servo_impl! {
518            @__parse
519            name: $name,
520            pin: [$($pin)?],
521            timer: [$($timer)?],
522            channel: [$($channel)?],
523            min_us: [$($min_us)?],
524            max_us: [$($max_us)?],
525            max_degrees: [$max_degrees],
526            fields: [ $($($rest)*)? ]
527        }
528    };
529    (@__parse
530        name: $name:ident,
531        pin: [$($pin:ident)?],
532        timer: [$($timer:ident)?],
533        channel: [$($channel:ident)?],
534        min_us: [$($min_us:expr)?],
535        max_us: [$($max_us:expr)?],
536        max_degrees: [$_max_degrees_seen:expr],
537        fields: [ max_degrees: $max_degrees:expr $(, $($rest:tt)*)? ]
538    ) => {
539        compile_error!("servo! duplicate `max_degrees` field");
540    };
541
542    (@__parse
543        name: $name:ident,
544        pin: [$($pin:ident)?],
545        timer: [$($timer:ident)?],
546        channel: [$($channel:ident)?],
547        min_us: [$($min_us:expr)?],
548        max_us: [$($max_us:expr)?],
549        max_degrees: [$($max_degrees:expr)?],
550        fields: [ ]
551    ) => {
552        $crate::__servo_impl! {
553            @__finish
554            name: $name,
555            pin: [$($pin)?],
556            timer: [$($timer)?],
557            channel: [$($channel)?],
558            min_us: [$($min_us)?],
559            max_us: [$($max_us)?],
560            max_degrees: [$($max_degrees)?]
561        }
562    };
563
564    (@__parse
565        name: $name:ident,
566        pin: [$($pin:ident)?],
567        timer: [$($timer:ident)?],
568        channel: [$($channel:ident)?],
569        min_us: [$($min_us:expr)?],
570        max_us: [$($max_us:expr)?],
571        max_degrees: [$($max_degrees:expr)?],
572        fields: [ $field:ident : $($value:tt)+ ]
573    ) => {
574        compile_error!(
575            "servo! unknown field; expected `pin`, `timer`, `channel`, `min_us`, `max_us`, or `max_degrees`"
576        );
577    };
578
579    (@__finish
580        name: $name:ident,
581        pin: [],
582        timer: [$($timer:ident)?],
583        channel: [$($channel:ident)?],
584        min_us: [$($min_us:expr)?],
585        max_us: [$($max_us:expr)?],
586        max_degrees: [$($max_degrees:expr)?]
587    ) => {
588        compile_error!("servo! missing required `pin` field");
589    };
590    (@__finish
591        name: $name:ident,
592        pin: [$pin:ident],
593        timer: [],
594        channel: [$($channel:ident)?],
595        min_us: [$($min_us:expr)?],
596        max_us: [$($max_us:expr)?],
597        max_degrees: [$($max_degrees:expr)?]
598    ) => {
599        compile_error!("servo! missing required `timer` field");
600    };
601    (@__finish
602        name: $name:ident,
603        pin: [$pin:ident],
604        timer: [$timer:ident],
605        channel: [],
606        min_us: [$($min_us:expr)?],
607        max_us: [$($max_us:expr)?],
608        max_degrees: [$($max_degrees:expr)?]
609    ) => {
610        compile_error!("servo! missing required `channel` field");
611    };
612    (@__finish
613        name: $name:ident,
614        pin: [$pin:ident],
615        timer: [$timer:ident],
616        channel: [$channel:ident],
617        min_us: [$($min_us:expr)?],
618        max_us: [$($max_us:expr)?],
619        max_degrees: [$($max_degrees:expr)?]
620    ) => {
621        $crate::servo::paste::paste! {
622            pub struct $name;
623
624            // Link-time ownership claims: duplicate timer or channel selection across the
625            // final binary should fail the link with duplicate symbol errors.
626            #[used]
627            #[unsafe(no_mangle)]
628            static [<__device_envoy_esp_ledc_timer_claim_ $timer:lower>]: u8 = 0;
629
630            #[used]
631            #[unsafe(no_mangle)]
632            static [<__device_envoy_esp_ledc_channel_claim_ $channel:lower>]: u8 = 0;
633
634            static [<$name:upper _SERVO_STATIC>]: [<$name Static>] = $name::new_static();
635
636            pub struct [<$name Static>] {
637                servo_static: $crate::servo::ServoStatic,
638            }
639
640            impl $name {
641                #[must_use]
642                pub const fn new_static() -> [<$name Static>] {
643                    [<$name Static>] {
644                        servo_static: $crate::servo::ServoStatic::new_static(
645                            ::esp_hal::ledc::timer::Number::$timer,
646                            ::esp_hal::ledc::channel::Number::$channel,
647                            $crate::__servo_impl!(@min_us $($min_us)?),
648                            $crate::__servo_impl!(@max_us $($max_us)?),
649                            $crate::__servo_impl!(@max_degrees $($max_degrees)?),
650                        ),
651                    }
652                }
653
654                pub fn new(
655                    ledc: &::esp_hal::ledc::Ledc<'static>,
656                    pin: ::esp_hal::peripherals::$pin<'static>,
657                ) -> $crate::Result<$crate::servo::ServoEsp> {
658                    $crate::servo::ServoEsp::new(&[<$name:upper _SERVO_STATIC>].servo_static, ledc, pin)
659                }
660            }
661        }
662    };
663
664    (@min_us $min_us:expr) => { $min_us };
665    (@min_us) => { $crate::servo::SERVO_MIN_US_DEFAULT };
666    (@max_us $max_us:expr) => { $max_us };
667    (@max_us) => { $crate::servo::SERVO_MAX_US_DEFAULT };
668    (@max_degrees $max_degrees:expr) => { $max_degrees };
669    (@max_degrees) => { $crate::servo::ServoEsp::DEFAULT_MAX_DEGREES };
670}