Положение и скважность
Чтобы управлять сервоприводом с Raspberry Pi Pico, нужно задать частоту ШИМ 50 Гц. Насколько мне известно, ни в embassy, ни в rp-hal нет простого способа задать частоту напрямую.
В embassy-rp ШИМ настраивается через структуру Config с несколькими полями. Для нашей задачи нас в основном интересуют значения top и divider. То же относится и к rp-hal, где можно задавать top, div_int и div_frac отдельно.
Подходящее значение TOP можно найти ручным методом или с помощью формы, которая автоматически вычисляет и TOP, и делитель для целевой частоты 50 Гц.
Ручным методом я получил значение TOP 46 874 при делителе 64. С помощью формы я получил делитель 45,8125 и значение TOP 65 483. Можно использовать любую из этих конфигураций.
Note
В rp-hal целую и дробную части делителя нужно задавать отдельно. Так, делитель 64 становится div_int = 64 и div_frac = 0. Делитель 45,8125 становится div_int = 45 и div_frac = 13.
Расчёт положения на основе TOP
Когда известно значение TOP для ШИМ-сигнала 50 Гц, можно рассчитать значения скважности, необходимые для установки положения сервопривода.
Сервопривод определяет своё положение, измеряя ширину импульса, то есть время, в течение которого сигнал остаётся в высоком состоянии в каждом 20-миллисекундном цикле ШИМ. Точные ширины импульсов не одинаковы для всех сервоприводов и могут немного отличаться в зависимости от конкретной модели.
В моём случае значения были следующими:
-
0° при примерно 0,5 мс, что соответствует скважности 2,5 %, поскольку 0,5 мс составляет 2,5 % периода 20 мс.
-
90° при примерно 1,5 мс, что соответствует скважности 7,5 %, поскольку 1,5 мс составляет 7,5 % периода 20 мс.
-
180° при примерно 2,4 мс, что соответствует скважности 12 %, поскольку 2,4 мс составляет 12 % периода 20 мс.
В главе о плавном изменении яркости светодиода изменять скважность было просто. Нас интересовала только яркость, а не частота, поэтому было достаточно использовать set_duty_cycle_percent. Эта функция принимает значение u8 от 0 до 100, что хорошо подходит для целых процентов.
Для управления сервоприводом такой подход не подходит, потому что нужные скважности включают дробные значения, например 2,5 %, 7,5 % и 12 %.
Поэтому у нас есть две альтернативы. Первый вариант — вычислить значение скважности напрямую из TOP и использовать set_duty_cycle, который принимает u16. Второй вариант — использовать set_duty_cycle_fraction, который позволяет задать скважность в виде числителя и знаменателя.
Вариант 1: ручной расчёт с set_duty_cycle
Сначала переводим ширину импульса в процент от периода. Затем этот процент умножаем на TOP + 1, чтобы получить значение скважности, которое настраивает выход ШИМ.
#![allow(unused)]
fn main() {
const PWM_DIV_INT: u8 = 64;
const PWM_TOP: u16 = 46_874;
const TOP: u16 = PWM_TOP + 1;
// 0,5 мс — это 2,5 % от 20 мс; 0 градусов для сервопривода
const MIN_DUTY: u16 = (TOP as f64 * (2.5 / 100.)) as u16;
// 1,5 мс — это 7,5 % от 20 мс; 90 градусов для сервопривода
const HALF_DUTY: u16 = (TOP as f64 * (7.5 / 100.)) as u16;
// 2,4 мс — это 12 % от 20 мс; 180 градусов для сервопривода
const MAX_DUTY: u16 = (TOP as f64 * (12. / 100.)) as u16;
}
Когда значение скважности вычислено, его можно применить так:
#![allow(unused)]
fn main() {
servo.set_duty_cycle(MIN_DUTY)
.expect("invalid min duty cycle");
}
Вариант 2: использование set_duty_cycle_fraction
Другой вариант — использовать set_duty_cycle_fraction. Он поможет задавать проценты в виде дроби.
На самом деле set_duty_cycle_percent — это удобный метод из embedded-hal, который внутри вызывает set_duty_cycle_fraction. Он просто делит процент на 100 и передаёт результат как дробь.
Из embedded-hal:
#![allow(unused)]
fn main() {
/// Установить скважность в `percent / 100`
///
/// Вызывающий код отвечает за то, что `percent` не превышает 100.
#[inline]
fn set_duty_cycle_percent(&mut self, percent: u8) -> Result<(), Self::Error> {
self.set_duty_cycle_fraction(u16::from(percent), 100)
}
/// Установить скважность в `num / denom`.
///
/// Вызывающий код отвечает за то, что `num` не превышает `denom`,
/// и что `denom` не равен нулю.
fn set_duty_cycle_fraction(&mut self, num: u16, denom: u16) -> Result<(), Self::Error> {
debug_assert!(denom != 0);
debug_assert!(num <= denom);
let duty = u32::from(num) * u32::from(self.max_duty_cycle()) / u32::from(denom);
// Это безопасно, потому что мы знаем, что `num <= denom`, поэтому `duty <= self.max_duty_cycle()` (u16)
#[allow(clippy::cast_possible_truncation)]
{
self.set_duty_cycle(duty as u16)
}
}
}
Эта функция не принимает значения с плавающей запятой. Вместо этого она принимает числитель и знаменатель, оба типа u16. Чтобы представить дробные проценты, мы просто масштабируем их до целых чисел.
Помните, что 2,5 % можно записать как дробь 2,5/100. Поскольку в числителе нельзя использовать десятичные дроби, умножим и числитель, и знаменатель на 10, чтобы получить эквивалентную дробь из целых чисел:
#![allow(unused)]
fn main() {
2.5/100 = (2.5 × 10)/(100 × 10) = 25/1000
}
Теперь у нас есть эквивалентная дробь, использующая только целые числа. Ту же операцию можно применить и к остальным процентам:
Например:
- 2,5 % можно записать как 25 / 1000 (то есть 25 — это 2,5 % от 1000)
- 7,5 % можно записать как 75 / 1000 (то есть 75 — это 7,5 % от 1000)
- 12 % можно записать как 120 / 1000 (то есть 120 — это 12 % от 1000)
Поэтому в нашем коде можно применить это так:
#![allow(unused)]
fn main() {
// Переместить сервопривод в положение 0° (скважность 2,5 % = 25/1000)
servo.set_duty_cycle_fraction(25, 1000)
.expect("invalid duty cycle");
// Положение 90° (скважность 7,5 %)
servo.set_duty_cycle_fraction(75, 1000)
.expect("invalid duty cycle");
// Положение 180° (скважность 12 %)
servo.set_duty_cycle_fraction(120, 1000)
.expect("invalid duty cycle");
}