The Servo library lets you drive 3-pin hobby servo motors from a Raspberry Pi Pico or any other RP2040-based board, such as the [Pimoroni Servo 2040](https://pimoroni.com/servo2040).
* a `Servo` class that uses hardware PWM to drive a single servo, with support for up to 16 servos.
* a `ServoCluster` class that uses Programmable IO (PIO) hardware to drive up to 30 servos.
There is also a `Calibration` class for performing advanced tweaking of each servo's movement behaviour.
### PWM Limitations
Although the RP2040 is capable of outputting up to 16 PWM signals, there are limitations of which pins can be used together:
* The PWMs output from pins 0 to 15 are repeated for pins 16 to 29, meaning that those pins share the same signals if PWM is enabled on both. For example if you used pin 3 for PWM and then tried to use pin 19, they would both output the same signal and it would not be possible to control them independently.
* The 16 PWM channels are grouped into 8 PWM slices, each containing A and B sub channels that are synchronised with each other. This means that parameters such as frequency are shared, which can cause issues if you want one servo to operate at a different frequency to it's pin neighbour or to drive an LED with PWM at a high frequency.
The official [RP2040 datasheet](https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf), includes the handy _Table 525_ that details the pwm channel for each GPIO pin. This is shown below for convenience:
The RP2040 features two PIOs with four state machines each. This places a hard limit on how many ServoClusters can be created. As this class is capable of driving all 30 GPIO pins, the only time this limit will be of concern is when servos with different frequencies are wanted, as all the outputs a ServoCluster controls share the same frequency. Relating this to the hardware PWM, think of it as a single PWM slice with up to 30 sub channels, A, B, C, D etc.
When creating a ServoCluster, in most cases you'll use `0` for PIO and `0` for PIO state-machine. You should change these though if you plan on running multiple clusters, or using a cluster alongside something else that uses PIO, such as our [Plasma library](https://github.com/pimoroni/pimoroni-pico/tree/main/micropython/modules/plasma).
Value is a way to control servos using numbers that have a real-world meaning, rather than with pulse widths. For instance, -90 to +90 degrees for an angular servo, or -1 to +1 for a continous rotation servo. See [Calibration](#calibration) for more details.
To read the current value the servo is at:
```python
value() # returns float: the value
```
If the servo is disabled, this will be the last value that was provided when enabled.
To set the servo to a new value:
```python
value(
value # float: the value
)
```
If the servo is disabled, this will also enable it. The resulting pulse width will also be stored.
The vast majority of Servos expect to receive pulses with a frequency of 50Hz, so this library uses that as its default. However, there may be cases where this value needs to be changed, such as when using servos that operate up to frequencies of 333Hz.
Sometimes there are projects where you want a servo to move based on the reading from a sensor or another device, but the numbers given out are not easy to convert to values the servo accepts. To overcome this the library lets you move the servo to a percent between its minimum and maximum values, or two values you provided, based on that input.
With an input between `-1.0` and `1.0`, move the servo to a percent between its minimum and maximum values:
```python
to_percent(
in # float: the input, from -1.0 to +1.0
)
```
With an input between a provided min and max, move the servo to a percent between its minimum and maximum values:
```python
to_percent(
in, # float: the input, from in_min to in_max
in_min, # float: the minimum expected input
in_max # float: the maximum expected input
)
```
With an input between a provided min and max, move the servo to a percent between two provided values:
```python
to_percent(
in, # float: the input, from in_min to in_max
in_min, # float: the minimum expected input
in_max # float: the maximum expected input
value_min, # float: the value the servo will go to when receiving a minimum input
value_max # float: the value the servo will go to when receiving a maximum input
There are different types of servos, with angular, linear, and continuous being common. To support these different types, each Servo class contains a calibration object that stores the specific value to pulse mapping needed for its type. This object can be accessed for each servo as well as updated on the fly.
To get the calibration of the servo:
```python
calibration() # returns Calibration: a copy of the servo's calibration
```
To update the calibration of the servo.
```python
calibration(
calibration # Calibration: the object to update the servo's calibration with
Value is a way to control servos using numbers that have a real-world meaning, rather than with pulse widths. For instance, -90 to +90 degrees for an angular servo, or -1 to +1 for a continous rotation servo. See [Calibration](#calibration) for more details.
To read the current value the servo is at:
```python
value(
servo # int: the servo to read the value of
) # returns float: the value
```
If the servo is disabled, this will be the last value that was provided when enabled.
To set the servo to a new value:
```python
value(
servo, # int: the servo to set the value of
value, # float: the value
load=True # bool: whether to load the change immediately
)
```
If the servo is disabled, this will also enable it. The resulting pulse width will also be stored.
When dealing with many servos, there can often be large current draw spikes caused by them all responding to pulses at the same time. To minimise this, a servo cluster allows for the start time of a servo's pulses to be delayed by a percentage of the available time period. This is called their phase.
To read the current phase of a servo on the cluster:
```python
phase(
servo # int: the servo to read the phase of
) # returns float: the phase, between 0.0 and 1.0
```
To set the phase of a servo on the cluster:
```python
phase(
servo, # int: the servo to set the phase of
phase, # float: the phase, between 0.0 and 1.0
load=True # bool: whether to load the change immediately
The vast majority of Servos expect to receive pulses with a frequency of 50Hz, so this library uses that as its default. However, there may be cases where this value needs to be changed, such as when using servos that operate up to frequencies of 333Hz.
All servos on a cluster share the same frequency.
To read the current servo cluster pulse frequency:
```python
frequency() # returns float: the frequency in Hz
```
To set a new servo cluster pulse frequency:
```python
frequency(
freq # float: the frequency between 10 and 350Hz
)
```
Note, currently the frequency changes immediately, even if part-way through a pulse. It is recommended to disable all servos first before changing the frequency.
Sometimes there are projects where you want a servo to move based on the reading from a sensor or another device, but the numbers given out are not easy to convert to values the servo accepts. To overcome this the library lets you move the servo to a percent between its minimum and maximum values, or two values you provided, based on that input.
With an input between `-1.0` and `1.0`, move a servo on the cluster to a percent between its minimum and maximum values:
```python
to_percent(
servo, # int: the servo to move to the percent
in # float: the input, from -1.0 to +1.0
load=True # bool: whether to load the change immediately
)
```
With an input between a provided min and max, move a servo on the cluster to a percent between its minimum and maximum values:
```python
to_percent(
servo, # int: the servo to move to the percent
in, # float: the input, from in_min to in_max
in_min, # float: the minimum expected input
in_max # float: the maximum expected input
load=True # bool: whether to load the change immediately
)
```
With an input between a provided min and max, move a servo on the cluster to a percent between two provided values:
```python
to_percent(
servo, # int: the servo to move to the percent
in, # float: the input, from in_min to in_max
in_min, # float: the minimum expected input
in_max # float: the maximum expected input
value_min, # float: the value the servo will go to when receiving a minimum input
value_max # float: the value the servo will go to when receiving a maximum input
load=True # bool: whether to load the change immediately
There are different types of servos, with angular, linear, and continuous being common. To support these different types, each Servo class contains a calibration object that stores the specific value to pulse mapping needed for its type. This object can be accessed for each servo as well as updated on the fly.
To get the calibration of a servo on the cluster:
```python
calibration(
servo, # int: the servo to get the calibration of
) # returns Calibration: a copy of the servo's calibration
```
To update the calibration of a servo on the cluster.
```python
calibration(
servo, # int: the servo to set the calibration of
calibration # Calibration: the object to update the servo's calibration with
To match behaviour with the regular Servo class, the ServoCluster automatically applies each change to a servo's state immediately. However, sometimes this may not be wanted, and instead you want all servos to receive updated pulses at the same time, regardless of how long the code ran that calculated the update.
For this purpose, all the functions that modify a servo state include an optional parameter `load`, which by default is `True`. To avoid this "loading" include `load=False` in the relevant function calls. Then either the last call can include `load=True`, or the following function can be called: