Arduino

Прошивка Arduino (sketch): структура setup()/loop(), простой текстовый протокол для Qt

Введение

Со стороны 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) быстро приводит к переполнению памяти и неопределённому, часто катастрофическому поведению прошивки; для надёжности стоит ограничивать максимальную длину буфера явной проверкой.
  • Использование класса String Arduino (а не C-style массивов char) в небольших, ограниченных по памяти прошивках может приводить к фрагментации динамически выделяемой памяти при долгой работе устройства без перезагрузки — для простых учебных прошивок и коротких сессий работы это обычно не проблема, но для устройств, работающих месяцами без перезагрузки, более опытные Arduino-разработчики предпочитают C-style буферы фиксированного размера вместо String.
  • Отсутствие версионирования или какой-либо проверки совместимости протокола между прошивкой и Qt-приложением. Если прошивка обновляется (добавляются новые команды, меняется формат данных) без соответствующего обновления Qt-стороны (или наоборот), рассинхронизация протокола может привести к молчаливому игнорированию команд или неверной интерпретации полученных данных — для проектов, выходящих за рамки чисто учебных, стоит явно предусмотреть простую механику проверки версии протокола (например, через PING/PONG с указанием версии) ещё на этапе первоначального проектирования.