Введение
Со стороны Arduino нужна прошивка (sketch), которая слушает последовательный порт, разбирает приходящие от Qt-приложения команды и отправляет обратно данные датчиков. В статье разберём базовую структуру скетча и спроектируем простой, текстовый протокол обмена, на основе которого будут построены все последующие статьи цикла.
Концепция
Скетч Arduino состоит из двух обязательных функций: setup(), выполняемой один раз при включении/перезагрузке платы (инициализация последовательного порта, пинов, датчиков), и loop(), выполняемой бесконечно повторно — именно внутри loop() происходит проверка наличия входящих команд через Serial.available() и периодическая отправка данных датчиков. Текстовый протокол (в противоположность бинарному) проще отлаживать вручную (можно просто читать обмен в текстовом виде через Serial Monitor) за счёт несколько большего объёма передаваемых данных по сравнению с компактным бинарным форматом — для большинства учебных и небольших проектов с Arduino эта компактность не критична, и читаемость протокола важнее.
Пример кода
// firmware/src/main.cpp — простая прошивка с текстовым протоколом команд
const int LED_PIN = 13;
String inputBuffer = "";
void setup()
{
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
// Небольшая пауза после инициализации — даёт время плате "устояться"
// после автоматического сброса при открытии последовательного порта
delay(100);
Serial.println("READY"); // сигнал готовности, который Qt-приложение может ожидать
}
void loop()
{
// Обработка входящих команд от Qt-приложения
while (Serial.available() > 0) {
char incomingChar = Serial.read();
if (incomingChar == 'n') {
processCommand(inputBuffer);
inputBuffer = "";
} else {
inputBuffer += incomingChar;
}
}
// Периодическая отправка данных — здесь просто значение со встроенного аналогового пина
static unsigned long lastSendTime = 0;
if (millis() - lastSendTime >= 1000) { // раз в секунду, без блокирующего delay()
int sensorValue = analogRead(A0);
Serial.print("SENSOR:A0:");
Serial.println(sensorValue);
lastSendTime = millis();
}
}
void processCommand(String command)
{
command.trim();
if (command == "LED:ON") {
digitalWrite(LED_PIN, HIGH);
Serial.println("ACK:LED:ON");
} else if (command == "LED:OFF") {
digitalWrite(LED_PIN, LOW);
Serial.println("ACK:LED:OFF");
} else if (command == "PING") {
Serial.println("PONG");
} else {
Serial.print("ERROR:UNKNOWN_COMMAND:");
Serial.println(command);
}
}
Пояснения к коду
setup() инициализирует последовательный порт на той же скорости (9600), что и в Qt-коде из предыдущей статьи, настраивает пин светодиода как выход, и после небольшой задержки отправляет строку "READY" — это позволяет Qt-приложению дождаться явного сигнала готовности платы, а не пытаться отправлять команды немедленно после открытия порта, когда плата ещё может быть в процессе перезагрузки.
Цикл loop() построен на принципиально важном для Arduino паттерне — отказе от блокирующего delay() для периодических задач, в пользу проверки millis() - lastSendTime >= 1000, что позволяет одновременно и оперативно обрабатывать входящие команды (через Serial.available()), и периодически отправлять данные датчика, не блокируя друг друга — если бы вместо этого использовался delay(1000) перед отправкой данных, входящие команды от Qt не обрабатывались бы вообще в течение этой секунды задержки. processCommand() реализует простой текстовый протокол команд: LED:ON/LED:OFF для управления, PING/PONG для проверки связи, и явный ACK-ответ на каждую команду — что позволяет Qt-стороне быть уверенной, что команда была действительно получена и обработана, а не просто отправлена в пустоту.
Подводные камни
- Использование блокирующего
delay()внутриloop()для периодических задач (как было бы при наивной реализации без проверкиmillis()) полностью останавливает выполнение всего остального кодаloop(), включая обработку входящих команд, на время этой задержки — это одна из самых частых ошибок новичков в программировании Arduino, и переход на неблокирующий паттерн черезmillis()критичен для любой прошивки, которая должна одновременно реагировать на команды и выполнять периодические действия. - Накопление символов в
String inputBufferбез ограничения максимальной длины — если по какой-то причине символ конца строки (n) не приходит вообще (повреждённые данные, рассинхронизация протокола), буфер будет расти неограниченно, что на Arduino с очень ограниченным объёмом RAM (например, 2 КБ у классической Uno) быстро приводит к переполнению памяти и неопределённому, часто катастрофическому поведению прошивки; для надёжности стоит ограничивать максимальную длину буфера явной проверкой. - Использование класса
StringArduino (а не C-style массивов char) в небольших, ограниченных по памяти прошивках может приводить к фрагментации динамически выделяемой памяти при долгой работе устройства без перезагрузки — для простых учебных прошивок и коротких сессий работы это обычно не проблема, но для устройств, работающих месяцами без перезагрузки, более опытные Arduino-разработчики предпочитают C-style буферы фиксированного размера вместоString. - Отсутствие версионирования или какой-либо проверки совместимости протокола между прошивкой и Qt-приложением. Если прошивка обновляется (добавляются новые команды, меняется формат данных) без соответствующего обновления Qt-стороны (или наоборот), рассинхронизация протокола может привести к молчаливому игнорированию команд или неверной интерпретации полученных данных — для проектов, выходящих за рамки чисто учебных, стоит явно предусмотреть простую механику проверки версии протокола (например, через
PING/PONGс указанием версии) ещё на этапе первоначального проектирования.