Введение
Помимо чтения данных датчиков, частая задача — управление исполнительными устройствами: реле (включение/выключение мощных нагрузок), DC-моторы через драйверы типа L298N. В статье разберём прошивку и Qt-интерфейс для управления реле и простым DC-мотором с регулировкой скорости через ШИМ (PWM).
Концепция
Реле управляется простым цифровым сигналом (включено/выключено) через один пин, тогда как DC-мотор через драйвер обычно требует как минимум двух сигналов — направление (цифровой пин) и скорость (ШИМ-сигнал через analogWrite(), на Arduino реализующий аппаратную широтно-импульсную модуляцию на определённых, специально поддерживающих эту функцию пинах). Расширим протокол команд из статьи 282 для поддержки параметризованных команд (не просто «включить», но и «установить скорость на конкретное значение»), и добавим в Qt-интерфейс соответствующие элементы управления — переключатель для реле и слайдер для скорости мотора.
Пример кода
// firmware/src/main.cpp — управление реле и DC-мотором через драйвер L298N
#define RELAY_PIN 7
#define MOTOR_DIRECTION_PIN 5
#define MOTOR_SPEED_PIN 6 // должен быть ШИМ-совместимым пином (отмечен ~ на платах Arduino)
void setup()
{
Serial.begin(9600);
pinMode(RELAY_PIN, OUTPUT);
pinMode(MOTOR_DIRECTION_PIN, OUTPUT);
pinMode(MOTOR_SPEED_PIN, OUTPUT);
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;
}
}
}
void processCommand(String command)
{
command.trim();
if (command == "RELAY:ON") {
digitalWrite(RELAY_PIN, HIGH);
Serial.println("ACK:RELAY:ON");
} else if (command == "RELAY:OFF") {
digitalWrite(RELAY_PIN, LOW);
Serial.println("ACK:RELAY:OFF");
} else if (command.startsWith("MOTOR:SPEED:")) {
// Параметризованная команда — значение скорости извлекается из самой строки команды
int speed = command.substring(12).toInt();
speed = constrain(speed, 0, 255); // защита от выхода за допустимый диапазон ШИМ
digitalWrite(MOTOR_DIRECTION_PIN, speed >= 0 ? HIGH : LOW);
analogWrite(MOTOR_SPEED_PIN, speed);
Serial.print("ACK:MOTOR:SPEED:");
Serial.println(speed);
} else {
Serial.print("ERROR:UNKNOWN_COMMAND:");
Serial.println(command);
}
}
// motorcontrolwidget.h — Qt-интерфейс управления реле и мотором
#pragma once
#include <QWidget>
#include <QPushButton>
#include <QSlider>
#include <QVBoxLayout>
#include "arduinoconnection.h"
class MotorControlWidget : public QWidget
{
Q_OBJECT
public:
explicit MotorControlWidget(ArduinoConnection *connection, QWidget *parent = nullptr)
: QWidget(parent), m_connection(connection)
{
auto *relayButton = new QPushButton("Включить реле", this);
relayButton->setCheckable(true);
auto *speedSlider = new QSlider(Qt::Horizontal, this);
speedSlider->setRange(0, 255);
auto *layout = new QVBoxLayout(this);
layout->addWidget(relayButton);
layout->addWidget(speedSlider);
connect(relayButton, &QPushButton::toggled, this, [this, relayButton](bool checked) {
relayButton->setText(checked ? "Выключить реле" : "Включить реле");
m_connection->sendCommand(checked ? "RELAY:ON" : "RELAY:OFF");
});
// Троттлинг отправки команд скорости мотора — слайдер может генерировать
// десятки сигналов valueChanged в секунду при перетаскивании
connect(speedSlider, &QSlider::valueChanged, this, [this](int value) {
m_pendingSpeed = value;
if (!m_sendThrottleTimer.isActive()) {
m_sendThrottleTimer.start(50); // не чаще 20 раз в секунду
}
});
connect(&m_sendThrottleTimer, &QTimer::timeout, this, [this]() {
m_connection->sendCommand(QString("MOTOR:SPEED:%1").arg(m_pendingSpeed));
});
m_sendThrottleTimer.setSingleShot(true);
}
private:
ArduinoConnection *m_connection;
QTimer m_sendThrottleTimer;
int m_pendingSpeed = 0;
};
Пояснения к коду
Прошивка добавляет обработку параметризованной команды MOTOR:SPEED:N, где N — числовое значение скорости — command.substring(12) отрезает известную длину префикса, оставляя только числовую часть для разбора через toInt(). constrain() — встроенная функция Arduino, ограничивающая значение в заданном диапазоне, что защищает от потенциально опасных значений ШИМ за пределами допустимого диапазона 0–255, если на Qt-стороне произошла ошибка или была отправлена некорректная команда.
MotorControlWidget показывает важный практический паттерн — троттлинг частоты отправки команд при работе со слайдером: поскольку valueChanged слайдера может срабатывать очень часто при активном перетаскивании ползунка пользователем, отправка отдельной команды через последовательный порт при каждом срабатывании создала бы избыточную нагрузку на канал связи (особенно учитывая относительно невысокую скорость передачи данных типичного последовательного соединения с Arduino) — QTimer с интервалом 50 мс ограничивает реальную частоту отправки команд, сохраняя только самое последнее запрошенное значение скорости (m_pendingSpeed) для отправки при следующем срабатывании таймера.
Подводные камни
- Прямое управление мощными нагрузками (моторы, мощные реле) без понимания требований к источнику питания и развязке цепей — Arduino сам не может напрямую управлять мощными нагрузками через свои цифровые пины (максимальный ток на пине ограничен несколькими десятками миллиампер), и для реального управления реле/моторами обязательно нужны промежуточные компоненты (драйвер реле с собственным транзистором, драйвер мотора типа L298N) с отдельным, достаточным по мощности источником питания — попытка подключить мощную нагрузку напрямую к пину Arduino может повредить плату.
- Отсутствие троттлинга при отправке частых команд изменения скорости через последовательный порт создаёт избыточную нагрузку на канал связи и может привести к заметным задержкам или даже потере команд при переполнении буфера приёма на стороне Arduino — паттерн троттлинга через
QTimer, показанный в примере, существенно важен именно для непрерывно изменяемых параметров (слайдеры), в отличие от разовых, дискретных команд (кнопки включения/выключения). constrain()на стороне прошивки — последняя линия защиты, но не замена валидации на стороне Qt-интерфейса. Хотя прошивка защищена от выхода значения за допустимый диапазон, полноценная пользовательская обратная связь (например, явное ограничение диапазона самогоQSliderчерезsetRange(0, 255), как в примере) должна происходить на уровне интерфейса, чтобы пользователь не мог даже попытаться запросить недопустимое значение, а не только полагаться на молчаливое ограничение значения уже на стороне устройства.- Отсутствие отдельного механизма экстренной остановки (emergency stop) для проектов, где управляемое оборудование физически может представлять опасность (быстро вращающийся мотор, мощная нагрузка) — для серьёзных, не чисто учебных проектов управления реальным оборудованием стоит явно предусмотреть отдельную, высокоприоритетную команду немедленной остановки, обрабатываемую прошивкой с максимальным приоритетом, а не полагаться только на обычный, последовательный протокол команд для критичной по безопасности функциональности.