Введение
Серво-привод — устройство, поворачивающееся в заданное угловое положение (обычно от 0 до 180 градусов) и удерживающее его, в отличие от обычного DC-мотора, который просто вращается с заданной скоростью. В статье разберём прошивку управления серво через стандартную библиотеку Servo.h и реализацию плавного, отзывчивого управления углом из Qt-интерфейса.
Концепция
Серво управляется специфичным ШИМ-сигналом с периодом около 20 мс, где длительность импульса (обычно от 1 до 2 мс) определяет угол поворота — библиотека Servo.h скрывает генерацию этого сигнала за простым методом write(angle), принимающим угол в градусах напрямую. Для плавного, не «скачкообразного» движения серво при управлении через слайдер можно либо отправлять промежуточные значения угла с небольшим шагом и задержкой непосредственно с Qt-стороны, либо реализовать плавную интерполяцию прямо в прошивке, получая только конечное целевое значение.
Пример кода
// firmware/src/main.cpp — серво с плавной интерполяцией к целевому углу на стороне прошивки
#include <Servo.h>
Servo myServo;
int currentAngle = 90;
int targetAngle = 90;
void setup()
{
Serial.begin(9600);
myServo.attach(9); // подключение к ШИМ-совместимому пину
myServo.write(currentAngle);
delay(100);
Serial.println("READY");
}
void loop()
{
static String inputBuffer = "";
while (Serial.available() > 0) {
char incomingChar = Serial.read();
if (incomingChar == 'n') {
processCommand(inputBuffer);
inputBuffer = "";
} else {
inputBuffer += incomingChar;
}
}
// Плавное приближение текущего угла к целевому — без блокирующих delay()
static unsigned long lastStepTime = 0;
if (millis() - lastStepTime >= 15 && currentAngle != targetAngle) {
currentAngle += (targetAngle > currentAngle) ? 1 : -1;
myServo.write(currentAngle);
lastStepTime = millis();
}
}
void processCommand(String command)
{
command.trim();
if (command.startsWith("SERVO:ANGLE:")) {
int angle = command.substring(12).toInt();
targetAngle = constrain(angle, 0, 180);
Serial.print("ACK:SERVO:ANGLE:");
Serial.println(targetAngle);
}
}
// servocontrolwidget.h — слайдер угла с троттлингом
#pragma once
#include <QWidget>
#include <QSlider>
#include <QLabel>
#include <QVBoxLayout>
#include <QTimer>
#include "arduinoconnection.h"
class ServoControlWidget : public QWidget
{
Q_OBJECT
public:
explicit ServoControlWidget(ArduinoConnection *connection, QWidget *parent = nullptr)
: QWidget(parent), m_connection(connection)
{
auto *angleSlider = new QSlider(Qt::Horizontal, this);
angleSlider->setRange(0, 180);
angleSlider->setValue(90);
m_angleLabel = new QLabel("Угол: 90°", this);
auto *layout = new QVBoxLayout(this);
layout->addWidget(m_angleLabel);
layout->addWidget(angleSlider);
connect(angleSlider, &QSlider::valueChanged, this, [this](int value) {
m_angleLabel->setText(QString("Угол: %1°").arg(value));
m_pendingAngle = value;
if (!m_throttleTimer.isActive()) {
m_throttleTimer.start(50);
}
});
m_throttleTimer.setSingleShot(true);
connect(&m_throttleTimer, &QTimer::timeout, this, [this]() {
m_connection->sendCommand(QString("SERVO:ANGLE:%1").arg(m_pendingAngle));
});
}
private:
ArduinoConnection *m_connection;
QLabel *m_angleLabel;
QTimer m_throttleTimer;
int m_pendingAngle = 90;
};
Пояснения к коду
Прошивка хранит два значения — currentAngle (текущее, реальное положение серво) и targetAngle (желаемое положение, получаемое от Qt) — и в loop() плавно, шаг за шагом, приближает текущий угол к целевому с фиксированным интервалом 15 мс между шагами, без использования блокирующего delay(). Это даёт плавное, «механически естественное» движение серво даже при резком изменении целевого угла, и одновременно не блокирует обработку входящих команд, в отличие от наивной реализации с циклом for и delay() внутри обработки одной команды.
ServoControlWidget использует тот же паттерн троттлинга через QTimer, для управления скоростью мотора — поскольку оба случая (непрерывно изменяемый слайдером параметр) структурно идентичны, стоит заметить, что троттлинг отправки команд — переиспользуемый паттерн, применимый к любому параметру, управляемому слайдером, а не специфичный только для серво или мотора.
Подводные камни
- Резкое присвоение
myServo.write(angle)напрямую из обработчика команды без промежуточной интерполяции заставляет серво двигаться максимально быстро к новому положению, что создаёт резкие, дёргающиеся движения и повышенный механический износ редуктора серво при частых изменениях угла — плавная интерполяция, как в примере, не только выглядит лучше визуально, но и физически менее нагружает механику серво-привода. - Серво-приводы потребляют заметный ток, особенно в момент начала движения или при механической нагрузке, и питание серво напрямую от пина 5V самой платы Arduino (а не от отдельного источника питания) может вызывать нестабильность напряжения, сбросы платы или непредсказуемое поведение других частей схемы — для серво, особенно более мощных моделей, рекомендуется отдельное питание, не идущее через сам Arduino.
- Интервал шага интерполяции (15 мс в примере) жёстко закодирован без возможности настройки скорости движения для разных сценариев использования — для одних задач может требоваться очень быстрое движение серво (быстрая реакция), а для других — подчёркнуто плавное, медленное; для более универсальной прошивки стоит сделать скорость интерполяции отдельным, настраиваемым через протокол параметром, а не фиксированной константой.
- Отсутствие физических концевых упоров или программных ограничений, соответствующих реальному диапазону движения конкретного механического узла, к которому подключено серво — программное ограничение
constrain(angle, 0, 180)защищает только от значений за пределами возможностей самого серво, но не от значений, недопустимых для конкретной механической конструкции (например, если серво поворачивает рычаг, который физически может двигаться только в более узком диапазоне) — это нужно явно учитывать в логике конкретного проекта, а не предполагать, что весь диапазон 0–180 градусов всегда безопасен механически.