Skip to main content

device_envoy/
servo.rs

1//! A device abstraction for hobby servos.
2//!
3//! This module provides a simple interface for controlling hobby positional servo motors
4//! like the SG90. See [`Servo`] for usage examples.
5//!
6//! Use the [`servo!`] macro for a keyword-driven constructor with defaults.
7
8use defmt::info;
9use embassy_rp::clocks::clk_sys_freq;
10use embassy_rp::pwm::{Config, Pwm};
11
12const SERVO_PERIOD_US: u16 = 20_000; // 20 ms
13
14/// Default minimum pulse width for hobby servos (microseconds).
15pub const SERVO_MIN_US_DEFAULT: u16 = 500;
16
17/// Default maximum pulse width for hobby servos (microseconds).
18pub const SERVO_MAX_US_DEFAULT: u16 = 2_500;
19
20/// Create a servo with keyword arguments and default pulse widths.
21///
22/// **Syntax:**
23///
24/// ```text
25/// servo! {
26///     pin: <pin_expr>,
27///     slice: <pwm_slice_expr>,
28///     channel: A | B,             // optional
29///     odd: <bool_expr>,           // optional
30///     even: <bool_expr>,          // optional
31///     min_us: <u16_expr>,         // optional
32///     max_us: <u16_expr>,         // optional
33///     max_degrees: <u16_expr>,    // optional
34/// }
35/// ```
36///
37/// Required fields: `pin`, `slice`.
38///
39/// Optional fields: `min_us`, `max_us`, `max_degrees` (defaults to
40/// [`SERVO_MIN_US_DEFAULT`]/[`SERVO_MAX_US_DEFAULT`]/[`Servo::DEFAULT_MAX_DEGREES`]),
41/// plus `channel: A/B` or `odd`/`even` to override the inferred channel.
42///
43/// See [`Servo`] for details and examples.
44#[macro_export]
45#[doc(hidden)]
46macro_rules! servo {
47    ($($tt:tt)*) => { $crate::__servo_impl! { $($tt)* } };
48}
49#[doc(inline)]
50pub use servo;
51
52// Public for macro expansion in downstream crates.
53#[doc(hidden)]
54#[macro_export]
55macro_rules! __servo_impl {
56    (@__fill_defaults
57        pin: $pin:tt,
58        slice: $slice:tt,
59        channel: $channel:tt,
60        min_us: $min_us:expr,
61        max_us: $max_us:expr,
62        max_degrees: $max_degrees:expr,
63        fields: [ ]
64    ) => {
65        $crate::__servo_impl! {
66            @__build
67            pin: $pin,
68            slice: $slice,
69            channel: $channel,
70            min_us: $min_us,
71            max_us: $max_us,
72            max_degrees: $max_degrees
73        }
74    };
75
76    (@__fill_defaults
77        pin: $pin:tt,
78        slice: $slice:tt,
79        channel: $channel:tt,
80        min_us: $min_us:expr,
81        max_us: $max_us:expr,
82        max_degrees: $max_degrees:expr,
83        fields: [ pin: $pin_value:expr, $($rest:tt)* ]
84    ) => {
85        $crate::__servo_impl! {
86            @__fill_defaults
87            pin: $pin_value,
88            slice: $slice,
89            channel: $channel,
90            min_us: $min_us,
91            max_us: $max_us,
92            max_degrees: $max_degrees,
93            fields: [ $($rest)* ]
94        }
95    };
96
97    (@__fill_defaults
98        pin: $pin:tt,
99        slice: $slice:tt,
100        channel: $channel:tt,
101        min_us: $min_us:expr,
102        max_us: $max_us:expr,
103        max_degrees: $max_degrees:expr,
104        fields: [ pin: $pin_value:expr ]
105    ) => {
106        $crate::__servo_impl! {
107            @__fill_defaults
108            pin: $pin_value,
109            slice: $slice,
110            channel: $channel,
111            min_us: $min_us,
112            max_us: $max_us,
113            max_degrees: $max_degrees,
114            fields: [ ]
115        }
116    };
117
118    (@__fill_defaults
119        pin: $pin:tt,
120        slice: $slice:tt,
121        channel: $channel:tt,
122        min_us: $min_us:expr,
123        max_us: $max_us:expr,
124        max_degrees: $max_degrees:expr,
125        fields: [ slice: $slice_value:expr, $($rest:tt)* ]
126    ) => {
127        $crate::__servo_impl! {
128            @__fill_defaults
129            pin: $pin,
130            slice: $slice_value,
131            channel: $channel,
132            min_us: $min_us,
133            max_us: $max_us,
134            max_degrees: $max_degrees,
135            fields: [ $($rest)* ]
136        }
137    };
138
139    (@__fill_defaults
140        pin: $pin:tt,
141        slice: $slice:tt,
142        channel: $channel:tt,
143        min_us: $min_us:expr,
144        max_us: $max_us:expr,
145        max_degrees: $max_degrees:expr,
146        fields: [ slice: $slice_value:expr ]
147    ) => {
148        $crate::__servo_impl! {
149            @__fill_defaults
150            pin: $pin,
151            slice: $slice_value,
152            channel: $channel,
153            min_us: $min_us,
154            max_us: $max_us,
155            max_degrees: $max_degrees,
156            fields: [ ]
157        }
158    };
159
160    (@__fill_defaults
161        pin: $pin:tt,
162        slice: $slice:tt,
163        channel: $channel:tt,
164        min_us: $min_us:expr,
165        max_us: $max_us:expr,
166        max_degrees: $max_degrees:expr,
167        fields: [ min_us: $min_us_value:expr, $($rest:tt)* ]
168    ) => {
169        $crate::__servo_impl! {
170            @__fill_defaults
171            pin: $pin,
172            slice: $slice,
173            channel: $channel,
174            min_us: $min_us_value,
175            max_us: $max_us,
176            max_degrees: $max_degrees,
177            fields: [ $($rest)* ]
178        }
179    };
180
181    (@__fill_defaults
182        pin: $pin:tt,
183        slice: $slice:tt,
184        channel: $channel:tt,
185        min_us: $min_us:expr,
186        max_us: $max_us:expr,
187        max_degrees: $max_degrees:expr,
188        fields: [ min_us: $min_us_value:expr ]
189    ) => {
190        $crate::__servo_impl! {
191            @__fill_defaults
192            pin: $pin,
193            slice: $slice,
194            channel: $channel,
195            min_us: $min_us_value,
196            max_us: $max_us,
197            max_degrees: $max_degrees,
198            fields: [ ]
199        }
200    };
201
202    (@__fill_defaults
203        pin: $pin:tt,
204        slice: $slice:tt,
205        channel: $channel:tt,
206        min_us: $min_us:expr,
207        max_us: $max_us:expr,
208        max_degrees: $max_degrees:expr,
209        fields: [ max_us: $max_us_value:expr, $($rest:tt)* ]
210    ) => {
211        $crate::__servo_impl! {
212            @__fill_defaults
213            pin: $pin,
214            slice: $slice,
215            channel: $channel,
216            min_us: $min_us,
217            max_us: $max_us_value,
218            max_degrees: $max_degrees,
219            fields: [ $($rest)* ]
220        }
221    };
222
223    (@__fill_defaults
224        pin: $pin:tt,
225        slice: $slice:tt,
226        channel: $channel:tt,
227        min_us: $min_us:expr,
228        max_us: $max_us:expr,
229        max_degrees: $max_degrees:expr,
230        fields: [ max_us: $max_us_value:expr ]
231    ) => {
232        $crate::__servo_impl! {
233            @__fill_defaults
234            pin: $pin,
235            slice: $slice,
236            channel: $channel,
237            min_us: $min_us,
238            max_us: $max_us_value,
239            max_degrees: $max_degrees,
240            fields: [ ]
241        }
242    };
243
244    (@__fill_defaults
245        pin: $pin:tt,
246        slice: $slice:tt,
247        channel: $channel:tt,
248        min_us: $min_us:expr,
249        max_us: $max_us:expr,
250        max_degrees: $max_degrees:expr,
251        fields: [ max_degrees: $max_degrees_value:expr, $($rest:tt)* ]
252    ) => {
253        $crate::__servo_impl! {
254            @__fill_defaults
255            pin: $pin,
256            slice: $slice,
257            channel: $channel,
258            min_us: $min_us,
259            max_us: $max_us,
260            max_degrees: $max_degrees_value,
261            fields: [ $($rest)* ]
262        }
263    };
264
265    (@__fill_defaults
266        pin: $pin:tt,
267        slice: $slice:tt,
268        channel: $channel:tt,
269        min_us: $min_us:expr,
270        max_us: $max_us:expr,
271        max_degrees: $max_degrees:expr,
272        fields: [ max_degrees: $max_degrees_value:expr ]
273    ) => {
274        $crate::__servo_impl! {
275            @__fill_defaults
276            pin: $pin,
277            slice: $slice,
278            channel: $channel,
279            min_us: $min_us,
280            max_us: $max_us,
281            max_degrees: $max_degrees_value,
282            fields: [ ]
283        }
284    };
285
286    (@__fill_defaults
287        pin: $pin:tt,
288        slice: $slice:tt,
289        channel: $channel:tt,
290        min_us: $min_us:expr,
291        max_us: $max_us:expr,
292        max_degrees: $max_degrees:expr,
293        fields: [ channel: A, $($rest:tt)* ]
294    ) => {
295        $crate::__servo_impl! {
296            @__fill_defaults
297            pin: $pin,
298            slice: $slice,
299            channel: A,
300            min_us: $min_us,
301            max_us: $max_us,
302            max_degrees: $max_degrees,
303            fields: [ $($rest)* ]
304        }
305    };
306
307    (@__fill_defaults
308        pin: $pin:tt,
309        slice: $slice:tt,
310        channel: $channel:tt,
311        min_us: $min_us:expr,
312        max_us: $max_us:expr,
313        max_degrees: $max_degrees:expr,
314        fields: [ channel: A ]
315    ) => {
316        $crate::__servo_impl! {
317            @__fill_defaults
318            pin: $pin,
319            slice: $slice,
320            channel: A,
321            min_us: $min_us,
322            max_us: $max_us,
323            max_degrees: $max_degrees,
324            fields: [ ]
325        }
326    };
327
328    (@__fill_defaults
329        pin: $pin:tt,
330        slice: $slice:tt,
331        channel: $channel:tt,
332        min_us: $min_us:expr,
333        max_us: $max_us:expr,
334        max_degrees: $max_degrees:expr,
335        fields: [ channel: B, $($rest:tt)* ]
336    ) => {
337        $crate::__servo_impl! {
338            @__fill_defaults
339            pin: $pin,
340            slice: $slice,
341            channel: B,
342            min_us: $min_us,
343            max_us: $max_us,
344            max_degrees: $max_degrees,
345            fields: [ $($rest)* ]
346        }
347    };
348
349    (@__fill_defaults
350        pin: $pin:tt,
351        slice: $slice:tt,
352        channel: $channel:tt,
353        min_us: $min_us:expr,
354        max_us: $max_us:expr,
355        max_degrees: $max_degrees:expr,
356        fields: [ channel: B ]
357    ) => {
358        $crate::__servo_impl! {
359            @__fill_defaults
360            pin: $pin,
361            slice: $slice,
362            channel: B,
363            min_us: $min_us,
364            max_us: $max_us,
365            max_degrees: $max_degrees,
366            fields: [ ]
367        }
368    };
369
370    (@__fill_defaults
371        pin: $pin:tt,
372        slice: $slice:tt,
373        channel: $channel:tt,
374        min_us: $min_us:expr,
375        max_us: $max_us:expr,
376        max_degrees: $max_degrees:expr,
377        fields: [ even, $($rest:tt)* ]
378    ) => {
379        $crate::__servo_impl! {
380            @__fill_defaults
381            pin: $pin,
382            slice: $slice,
383            channel: A,
384            min_us: $min_us,
385            max_us: $max_us,
386            max_degrees: $max_degrees,
387            fields: [ $($rest)* ]
388        }
389    };
390
391    (@__fill_defaults
392        pin: $pin:tt,
393        slice: $slice:tt,
394        channel: $channel:tt,
395        min_us: $min_us:expr,
396        max_us: $max_us:expr,
397        max_degrees: $max_degrees:expr,
398        fields: [ even ]
399    ) => {
400        $crate::__servo_impl! {
401            @__fill_defaults
402            pin: $pin,
403            slice: $slice,
404            channel: A,
405            min_us: $min_us,
406            max_us: $max_us,
407            max_degrees: $max_degrees,
408            fields: [ ]
409        }
410    };
411
412    (@__fill_defaults
413        pin: $pin:tt,
414        slice: $slice:tt,
415        channel: $channel:tt,
416        min_us: $min_us:expr,
417        max_us: $max_us:expr,
418        max_degrees: $max_degrees:expr,
419        fields: [ odd, $($rest:tt)* ]
420    ) => {
421        $crate::__servo_impl! {
422            @__fill_defaults
423            pin: $pin,
424            slice: $slice,
425            channel: B,
426            min_us: $min_us,
427            max_us: $max_us,
428            max_degrees: $max_degrees,
429            fields: [ $($rest)* ]
430        }
431    };
432
433    (@__fill_defaults
434        pin: $pin:tt,
435        slice: $slice:tt,
436        channel: $channel:tt,
437        min_us: $min_us:expr,
438        max_us: $max_us:expr,
439        max_degrees: $max_degrees:expr,
440        fields: [ odd ]
441    ) => {
442        $crate::__servo_impl! {
443            @__fill_defaults
444            pin: $pin,
445            slice: $slice,
446            channel: B,
447            min_us: $min_us,
448            max_us: $max_us,
449            max_degrees: $max_degrees,
450            fields: [ ]
451        }
452    };
453
454    (@__build
455        pin: _UNSET_,
456        slice: $slice:tt,
457        channel: $channel:tt,
458        min_us: $min_us:expr,
459        max_us: $max_us:expr,
460        max_degrees: $max_degrees:expr
461    ) => {
462        compile_error!("servo! requires `pin: ...`");
463    };
464
465    (@__build
466        pin: $pin:expr,
467        slice: _UNSET_,
468        channel: $channel:tt,
469        min_us: $min_us:expr,
470        max_us: $max_us:expr,
471        max_degrees: $max_degrees:expr
472    ) => {
473        compile_error!("servo! requires `slice: ...`");
474    };
475
476    (@__build
477        pin: $pin:expr,
478        slice: $slice:expr,
479        channel: _UNSET_,
480        min_us: $min_us:expr,
481        max_us: $max_us:expr,
482        max_degrees: $max_degrees:expr
483    ) => {
484        $crate::servo::servo_from_pin_slice($pin, $slice, $min_us, $max_us, $max_degrees)
485    };
486
487    (@__build
488        pin: $pin:expr,
489        slice: $slice:expr,
490        channel: A,
491        min_us: $min_us:expr,
492        max_us: $max_us:expr,
493        max_degrees: $max_degrees:expr
494    ) => {
495        $crate::servo::Servo::new_output_a(
496            embassy_rp::pwm::Pwm::new_output_a(
497                $slice,
498                $pin,
499                embassy_rp::pwm::Config::default(),
500            ),
501            $min_us,
502            $max_us,
503            $max_degrees,
504        )
505    };
506
507    (@__build
508        pin: $pin:expr,
509        slice: $slice:expr,
510        channel: B,
511        min_us: $min_us:expr,
512        max_us: $max_us:expr,
513        max_degrees: $max_degrees:expr
514    ) => {
515        $crate::servo::Servo::new_output_b(
516            embassy_rp::pwm::Pwm::new_output_b(
517                $slice,
518                $pin,
519                embassy_rp::pwm::Config::default(),
520            ),
521            $min_us,
522            $max_us,
523            $max_degrees,
524        )
525    };
526
527    (
528        $($fields:tt)*
529    ) => {
530        $crate::__servo_impl! {
531            @__fill_defaults
532            pin: _UNSET_,
533            slice: _UNSET_,
534            channel: _UNSET_,
535            min_us: $crate::servo::SERVO_MIN_US_DEFAULT,
536            max_us: $crate::servo::SERVO_MAX_US_DEFAULT,
537            max_degrees: $crate::servo::Servo::DEFAULT_MAX_DEGREES,
538            fields: [ $($fields)* ]
539        }
540    };
541}
542
543// Public for macro expansion in downstream crates.
544#[doc(hidden)]
545pub trait ServoPwmPin<S: embassy_rp::PeripheralType>: embassy_rp::PeripheralType {
546    const IS_CHANNEL_A: bool;
547    fn new_pwm<'d>(slice: embassy_rp::Peri<'d, S>, pin: embassy_rp::Peri<'d, Self>) -> Pwm<'d>;
548}
549
550// Public for macro expansion in downstream crates.
551#[doc(hidden)]
552pub fn servo_from_pin_slice<'d, P, S>(
553    pin: embassy_rp::Peri<'d, P>,
554    slice: embassy_rp::Peri<'d, S>,
555    min_us: u16,
556    max_us: u16,
557    max_degrees: u16,
558) -> Servo<'d>
559where
560    P: ServoPwmPin<S>,
561    S: embassy_rp::PeripheralType,
562{
563    let pwm = P::new_pwm(slice, pin);
564    if P::IS_CHANNEL_A {
565        Servo::new_output_a(pwm, min_us, max_us, max_degrees)
566    } else {
567        Servo::new_output_b(pwm, min_us, max_us, max_degrees)
568    }
569}
570
571macro_rules! servo_pin_map {
572    ($pin:ident, $slice:ident, A) => {
573        impl ServoPwmPin<embassy_rp::peripherals::$slice> for embassy_rp::peripherals::$pin {
574            const IS_CHANNEL_A: bool = true;
575            fn new_pwm<'d>(
576                slice: embassy_rp::Peri<'d, embassy_rp::peripherals::$slice>,
577                pin: embassy_rp::Peri<'d, Self>,
578            ) -> Pwm<'d> {
579                embassy_rp::pwm::Pwm::new_output_a(slice, pin, Config::default())
580            }
581        }
582    };
583    ($pin:ident, $slice:ident, B) => {
584        impl ServoPwmPin<embassy_rp::peripherals::$slice> for embassy_rp::peripherals::$pin {
585            const IS_CHANNEL_A: bool = false;
586            fn new_pwm<'d>(
587                slice: embassy_rp::Peri<'d, embassy_rp::peripherals::$slice>,
588                pin: embassy_rp::Peri<'d, Self>,
589            ) -> Pwm<'d> {
590                embassy_rp::pwm::Pwm::new_output_b(slice, pin, Config::default())
591            }
592        }
593    };
594}
595
596servo_pin_map!(PIN_0, PWM_SLICE0, A);
597servo_pin_map!(PIN_1, PWM_SLICE0, B);
598servo_pin_map!(PIN_2, PWM_SLICE1, A);
599servo_pin_map!(PIN_3, PWM_SLICE1, B);
600servo_pin_map!(PIN_4, PWM_SLICE2, A);
601servo_pin_map!(PIN_5, PWM_SLICE2, B);
602servo_pin_map!(PIN_6, PWM_SLICE3, A);
603servo_pin_map!(PIN_7, PWM_SLICE3, B);
604servo_pin_map!(PIN_8, PWM_SLICE4, A);
605servo_pin_map!(PIN_9, PWM_SLICE4, B);
606servo_pin_map!(PIN_10, PWM_SLICE5, A);
607servo_pin_map!(PIN_11, PWM_SLICE5, B);
608servo_pin_map!(PIN_12, PWM_SLICE6, A);
609servo_pin_map!(PIN_13, PWM_SLICE6, B);
610servo_pin_map!(PIN_14, PWM_SLICE7, A);
611servo_pin_map!(PIN_15, PWM_SLICE7, B);
612servo_pin_map!(PIN_16, PWM_SLICE0, A);
613servo_pin_map!(PIN_17, PWM_SLICE0, B);
614servo_pin_map!(PIN_18, PWM_SLICE1, A);
615servo_pin_map!(PIN_19, PWM_SLICE1, B);
616servo_pin_map!(PIN_20, PWM_SLICE2, A);
617servo_pin_map!(PIN_21, PWM_SLICE2, B);
618servo_pin_map!(PIN_22, PWM_SLICE3, A);
619servo_pin_map!(PIN_23, PWM_SLICE3, B);
620servo_pin_map!(PIN_24, PWM_SLICE4, A);
621servo_pin_map!(PIN_25, PWM_SLICE4, B);
622servo_pin_map!(PIN_26, PWM_SLICE5, A);
623servo_pin_map!(PIN_27, PWM_SLICE5, B);
624servo_pin_map!(PIN_28, PWM_SLICE6, A);
625servo_pin_map!(PIN_29, PWM_SLICE6, B);
626
627#[cfg(feature = "pico2")]
628servo_pin_map!(PIN_30, PWM_SLICE7, A);
629#[cfg(feature = "pico2")]
630servo_pin_map!(PIN_31, PWM_SLICE7, B);
631#[cfg(feature = "pico2")]
632servo_pin_map!(PIN_32, PWM_SLICE8, A);
633#[cfg(feature = "pico2")]
634servo_pin_map!(PIN_33, PWM_SLICE8, B);
635#[cfg(feature = "pico2")]
636servo_pin_map!(PIN_34, PWM_SLICE9, A);
637#[cfg(feature = "pico2")]
638servo_pin_map!(PIN_35, PWM_SLICE9, B);
639#[cfg(feature = "pico2")]
640servo_pin_map!(PIN_36, PWM_SLICE10, A);
641#[cfg(feature = "pico2")]
642servo_pin_map!(PIN_37, PWM_SLICE10, B);
643#[cfg(feature = "pico2")]
644servo_pin_map!(PIN_38, PWM_SLICE11, A);
645#[cfg(feature = "pico2")]
646servo_pin_map!(PIN_39, PWM_SLICE11, B);
647#[cfg(feature = "pico2")]
648servo_pin_map!(PIN_40, PWM_SLICE8, A);
649#[cfg(feature = "pico2")]
650servo_pin_map!(PIN_41, PWM_SLICE8, B);
651#[cfg(feature = "pico2")]
652servo_pin_map!(PIN_42, PWM_SLICE9, A);
653#[cfg(feature = "pico2")]
654servo_pin_map!(PIN_43, PWM_SLICE9, B);
655#[cfg(feature = "pico2")]
656servo_pin_map!(PIN_44, PWM_SLICE10, A);
657#[cfg(feature = "pico2")]
658servo_pin_map!(PIN_45, PWM_SLICE10, B);
659#[cfg(feature = "pico2")]
660servo_pin_map!(PIN_46, PWM_SLICE11, A);
661#[cfg(feature = "pico2")]
662servo_pin_map!(PIN_47, PWM_SLICE11, B);
663
664/// A device abstraction for hobby servos.
665///
666/// Use `Servo` for direct, immediate control when you want to manually manage servo
667/// positioning. Use [`servo_player`](mod@crate::servo_player) instead when you need
668/// background animation sequences or want motion to continue while your code does other work.
669///
670#[doc = include_str!("../docs/how_servos_work.md")]
671///
672/// # Examples
673/// ```rust,no_run
674/// # #![no_std]
675/// # #![no_main]
676/// use device_envoy::{servo, servo::Servo};
677/// use embassy_time::{Duration, Timer};
678/// # use core::panic::PanicInfo;
679/// # #[panic_handler]
680/// # fn panic(_info: &PanicInfo) -> ! { loop {} }
681/// async fn example(p: embassy_rp::Peripherals) {
682///     // Create a servo on GPIO 11.
683///     // GPIO 11 → (11/2) % 8 = 5 → PWM_SLICE5
684///     let mut servo = servo! {
685///         pin: p.PIN_11,
686///         slice: p.PWM_SLICE5,
687///     };
688///
689///     servo.set_degrees(45);                          // Move to 45 degrees and hold.
690///     Timer::after(Duration::from_secs(1)).await;     // Give servo reasonable time to reach position
691///     servo.set_degrees(90);                          // Move to 90 degrees and hold.
692///     Timer::after(Duration::from_secs(1)).await;     // Give servo reasonable time to reach position
693///     servo.relax();                                  // Let the servo relax. It will re-enable on next set_degrees()
694/// }
695/// ```
696pub struct Servo<'d> {
697    pwm: Pwm<'d>,
698    cfg: Config, // Store config to avoid recreating default (which resets divider)
699    top: u16,
700    min_us: u16,
701    max_us: u16,
702    max_degrees: u16,
703    channel: ServoChannel, // Track which channel (A or B) this servo uses
704    state: ServoState,
705}
706
707#[derive(Debug, Clone, Copy)]
708enum ServoChannel {
709    A,
710    B,
711}
712
713#[derive(Debug, Clone, Copy, Eq, PartialEq)]
714enum ServoState {
715    Disabled,
716    Enabled,
717}
718
719impl<'d> Servo<'d> {
720    /// Default maximum rotation range in degrees (180°).
721    pub const DEFAULT_MAX_DEGREES: u16 = 180;
722
723    /// Create a servo on a PWM output A channel.
724    ///
725    /// See the [`Servo`] example for usage.
726    pub(crate) fn new_output_a(pwm: Pwm<'d>, min_us: u16, max_us: u16, max_degrees: u16) -> Self {
727        Self::init(pwm, ServoChannel::A, min_us, max_us, max_degrees)
728    }
729
730    /// Create a servo on a PWM output B channel.
731    ///
732    /// See the [`Servo`] example for usage.
733    pub(crate) fn new_output_b(pwm: Pwm<'d>, min_us: u16, max_us: u16, max_degrees: u16) -> Self {
734        Self::init(pwm, ServoChannel::B, min_us, max_us, max_degrees)
735    }
736
737    /// Configure PWM and initialize servo. Internal shared logic.
738    fn init(
739        mut pwm: Pwm<'d>,
740        channel: ServoChannel,
741        min_us: u16,
742        max_us: u16,
743        max_degrees: u16,
744    ) -> Self {
745        // TODO: consider if these could/should be checked at compile time.
746        assert!(min_us < max_us, "min_us must be less than max_us");
747        assert!(max_degrees > 0, "max_degrees must be positive");
748        let clk = clk_sys_freq() as u64; // Hz
749        // Aim for tick ≈ 1 µs: divider = clk_sys / 1_000_000 (with /16 fractional)
750        let mut div_int = (clk / 1_000_000).clamp(1, 255) as u16;
751        let rem = clk.saturating_sub(div_int as u64 * 1_000_000);
752        let mut div_frac = ((rem * 16 + 500_000) / 1_000_000).clamp(0, 15) as u8;
753        if div_frac == 16 {
754            div_frac = 0;
755            div_int = (div_int + 1).min(255);
756        }
757
758        let top = SERVO_PERIOD_US - 1; // 19999 -> 20_000 ticks/frame
759        assert!(min_us <= top, "min_us must fit in the PWM frame");
760        assert!(max_us <= top, "max_us must fit in the PWM frame");
761
762        let mut cfg = Config::default();
763        cfg.top = top;
764        cfg.phase_correct = false; // edge-aligned => exact 1 µs steps
765        // Apply divider: use the integer part as u8 which has a From impl
766        cfg.divider = (div_int as u8).into();
767
768        // Set the appropriate compare register based on channel
769        match channel {
770            ServoChannel::A => cfg.compare_a = 1500, // start ~center
771            ServoChannel::B => cfg.compare_b = 1500, // start ~center
772        }
773
774        cfg.enable = true; // Enable PWM output
775        pwm.set_config(&cfg);
776
777        info!(
778            "servo clk={}Hz div={}.{} top={}",
779            clk, div_int, div_frac, top
780        );
781
782        let mut servo = Self {
783            pwm,
784            cfg, // Store config to avoid losing divider on reconfiguration
785            top,
786            min_us,
787            max_us,
788            max_degrees,
789            channel,
790            state: ServoState::Enabled,
791        };
792        let center_us = min_us + (max_us - min_us) / 2;
793        servo.set_pulse_us(center_us);
794        servo
795    }
796
797    /// Set position in degrees 0..=max_degrees mapped into [min_us, max_us].
798    ///
799    /// Automatically enables the servo if it was disabled.
800    ///
801    /// See the [`Servo`] example for usage.
802    pub fn set_degrees(&mut self, degrees: u16) {
803        assert!((0..=self.max_degrees).contains(&degrees));
804        self.ensure_enabled();
805        let us = self.min_us as u32
806            + (u32::from(degrees)) * (u32::from(self.max_us) - u32::from(self.min_us))
807                / u32::from(self.max_degrees);
808        info!("Servo set_degrees({}) -> {}µs", degrees, us);
809        self.set_pulse_us(us as u16);
810    }
811
812    /// Set raw pulse width in microseconds.
813    ///
814    /// See the [`Servo`] example for usage.
815    /// NOTE: only update the *compare* register; do not reconfigure the slice.
816    #[doc(hidden)]
817    pub fn set_pulse_us(&mut self, us: u16) {
818        assert!(us <= self.top, "pulse width must fit in the PWM frame");
819        // One tick ≈ 1 µs, so compare = us.
820        // CRITICAL: Update our stored config and reapply it WITH the divider intact.
821        // This prevents the divider from being reset to default.
822        match self.channel {
823            ServoChannel::A => self.cfg.compare_a = us,
824            ServoChannel::B => self.cfg.compare_b = us,
825        }
826        self.pwm.set_config(&self.cfg);
827    }
828
829    fn ensure_enabled(&mut self) {
830        if self.state == ServoState::Enabled {
831            return;
832        }
833
834        self.cfg.enable = true;
835        self.pwm.set_config(&self.cfg);
836        self.state = ServoState::Enabled;
837    }
838
839    /// Stop sending control signals to the servo.
840    ///
841    /// This allows the servo to relax and move freely, reducing power consumption
842    /// and mechanical stress.
843    ///
844    /// See the [`Servo`] example for usage.
845    pub fn relax(&mut self) {
846        if self.state == ServoState::Disabled {
847            return;
848        }
849
850        self.cfg.enable = false;
851        self.pwm.set_config(&self.cfg);
852        self.state = ServoState::Disabled;
853    }
854
855    /// Resume sending control signals to the servo.
856    ///
857    /// The servo will move back to its last commanded position.
858    pub fn hold(&mut self) {
859        if self.state == ServoState::Enabled {
860            return;
861        }
862
863        self.cfg.enable = true;
864        self.pwm.set_config(&self.cfg);
865        self.state = ServoState::Enabled;
866    }
867}