Arduino — основы последовательного ввода

(перевод)

Введение

Новички часто испытывают трудности с процессом получения последовательных данных на Arduino, особенно когда им нужно ввести больше одного символа. Тот факт, что на странице Serial reference перечислено 18 различных функций, вероятно, не помогает.

Вы могли бы написать небольшую книгу, но все равно не смогли бы охватить все возможные ситуации, связанные с получением данных. Вместо того, чтобы писать множество страниц, которые мало кто прочтет, я подумал, что было бы полезнее привести несколько примеров, которые, вероятно, охватят все потребности новичка. И когда вы поймете эти примеры, вы сможете найти решения для других странных случаев.

Почти все данные последовательного ввода могут быть обработаны в трех простых ситуациях

  • А — когда требуется ввести только один символ
  • Б — когда требуется только простой ручной ввод с помощью последовательного монитора
  • В — другие

Последовательная передача данных медленная по меркам Arduino

Когда что-либо отправляет последовательные данные на Arduino, они поступают во входной буфер Arduino со скоростью, определяемой скоростью передачи данных. При скорости 9600 бод в секунду поступает около 960 символов, то есть между символами проходит чуть больше 1 миллисекунды. За 1 миллисекунду Arduino может многое сделать, поэтому приведенный ниже код не тратит время на ожидание, если во входном буфере ничего нет, даже если все данные еще не поступили. Даже при скорости 115200 бод между символами проходит 86 микросекунд или 1376 инструкций Arduino.

Поскольку данные поступают относительно медленно, Arduino может легко опустошить буфер последовательного ввода, даже если все данные еще не поступили. Многие новички ошибочно полагают, что конструкция while (Serial.available() > 0) { соберет все отправленные данные. Но гораздо более вероятно, что цикл WHILE опустошит буфер, даже если поступила только часть данных.

Пример 1. Получение единичных символов

Во многих случаях требуется отправить на Arduino один символ. Между прописными и строчными буквами, а также цифровыми символами есть 62 варианта. Например, можно использовать 'F' для движения вперед, 'R' для движения назад и 'S' для остановки.

Код для получения одного символа очень прост:

sample1.ino
// Пример 1 - Получение единичных символов
 
char receivedChar;
boolean newData = false;
 
void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}
 
void loop() {
    recvOneChar();
    showNewData();
}
 
void recvOneChar() {
    if (Serial.available() > 0) {
        receivedChar = Serial.read();
        newData = true;
    }
}
 
void showNewData() {
    if (newData == true) {
        Serial.print("This just in ... ");
        Serial.println(receivedChar);
        newData = false;
    }
}

Несмотря на то, что этот пример короткий и простой, я намеренно вынес код для получения символа в отдельную функцию под названием recvOneChar(), чтобы его можно было легко добавить в любую другую программу. Код для отображения символа я вынес в функцию showNewData(), потому что его можно изменить по своему усмотрению, не нарушая работу остального кода.

Если вы хотите использовать код любого из примеров в своей программе, я предлагаю вам просто скопировать функции из соответствующего примера и создать необходимые глобальные переменные в начале своей программы.

Пример 2. Получение нескольких символов из монитора порта

Если вам нужно получить из последовательного монитора не один символ (например, если вы хотите ввести имена людей), вам понадобится какой-то способ сообщить Arduino, что сообщение получено полностью. Самый простой способ — установить в качестве окончания строки символ новой строки.

Это делается с помощью поля в нижней части окна «Монитор порта». Вы можете выбрать один из вариантов: «Без окончания строки», «Новая строка», «Возврат каретки» и «И новая строка, и возврат каретки». При выборе опции «Новая строка» в конце отправляемых данных добавляется символ новой строки (\n).

sample2.ino
// Пример 2 - Получение с использованием маркера конца
 
const byte numChars = 32;
char receivedChars[numChars];   // массив для хранения полученных данных
 
boolean newData = false;
 
void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}
 
void loop() {
    recvWithEndMarker();
    showNewData();
}
 
void recvWithEndMarker() {
    static byte ndx = 0;
    char endMarker = '\n';
    char rc;
 
    while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();
 
        if (rc != endMarker) {
            receivedChars[ndx] = rc;
            ndx++;
            if (ndx >= numChars) {
                ndx = numChars - 1;
            }
        }
        else {
            receivedChars[ndx] = '\0'; // ограничитель строки
            ndx = 0;
            newData = true;
        }
    }
}
 
void showNewData() {
    if (newData == true) {
        Serial.print("This just in ... ");
        Serial.println(receivedChars);
        newData = false;
    }
}

Эта версия программы считывает все символы в массив до тех пор, пока не обнаружит символ новой строки в качестве конечного маркера.

Пример 3. Более полная система

Простая программа из примера 2 будет хорошо работать, если за ней следит человек, который не пытается ее испортить. Но если компьютер или человек, отправляющий данные, не может определить, когда Arduino будет готова их принять, существует реальный риск того, что Arduino не поймет, где начинаются данные.

Если хотите попробовать, измените конечный маркер в предыдущей программе с \n на >, чтобы можно было включить конечный маркер в текст для наглядности. (Нельзя вручную ввести символ новой строки в текст, отправляемый с последовательного монитора). И верните в настройках окончания строки значение «Без окончания строки».

Теперь, когда код отредактирован, отправьте команду qwert> и убедитесь, что она работает точно так же, как при использовании символа новой строки в качестве конечного маркера.

Но если вы попробуете ввести asdfg>zxcvb, то увидите только первую часть «asdfg». А если после этого вы введёте qwert>, то увидите «zxcvbqwert», потому что Arduino запутался и не понимает, что «zxcvb» нужно проигнорировать.

Чтобы решить эту проблему, нужно добавить начальный маркер, так же как и конечный.

sample3.ino
// Пример 3 - Приём данных с использованием начальных и конечных маркеров
 
const byte numChars = 32;
char receivedChars[numChars];
 
boolean newData = false;
 
void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}
 
void loop() {
    recvWithStartEndMarkers();
    showNewData();
}
 
void recvWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    char startMarker = '<';
    char endMarker = '>';
    char rc;
 
    while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();
 
        if (recvInProgress == true) {
            if (rc != endMarker) {
                receivedChars[ndx] = rc;
                ndx++;
                if (ndx >= numChars) {
                    ndx = numChars - 1;
                }
            }
            else {
                receivedChars[ndx] = '\0'; // терминатор строки
                recvInProgress = false;
                ndx = 0;
                newData = true;
            }
        }
 
        else if (rc == startMarker) {
            recvInProgress = true;
        }
    }
}
 
void showNewData() {
    if (newData == true) {
        Serial.print("This just in ... ");
        Serial.println(receivedChars);
        newData = false;
    }
}

Чтобы понять, как это работает, попробуйте отправить qwerty<asdfg>zxcvb, и вы увидите, что система игнорирует все символы, кроме «asdfg».

В этой программе вы увидите новую переменную под названием recvInProgress. Она необходима для того, чтобы различать нежелательные символы, которые приходят до начального маркера, и допустимые символы, которые приходят после него.

Эта версия программы очень похожа на код Arduino из этого руководства: Python - Arduino demo.

Важно отметить, что при каждом вызове функции recvWithEndMarker() или recvWithStartEndMarker() она считывает все символы, которые могли поступить в буфер последовательного ввода, и помещает их в массив receivedChars.

Если в буфере ничего нет, функция recvWithEndMarker() не тратит время на ожидание.

В случае использования recvWithStartEndMarker() все символы отбрасываются до тех пор, пока не будет обнаружен начальный маркер.

Если конечный маркер еще не достигнут, функция повторит попытку при следующем вызове loop().

Для достижения наилучших результатов важно, чтобы функция loop() выполнялась как можно быстрее — сотни или даже тысячи раз в секунду.

Сколько символов может быть принято?

В примерах я исходил из того, что вам не понадобится получать более 32 байт. Это легко изменить, изменив значение константы numChars.

Обратите внимание, что 64-байтовый размер буфера последовательного ввода Arduino не ограничивает количество получаемых символов, поскольку код в примерах может очищать буфер быстрее, чем поступают новые данные.

Вещи, которые не используются в примерах

Как вы могли заметить, в приведенных здесь примерах не используются ни одна из этих функций Arduino:

  • Serial.parseInt()
  • Serial.parseFloat()
  • Serial.readBytes()
  • Serial.readBytesUntil()

Все эти функции блокируют работу Arduino до тех пор, пока не будут выполнены или пока не истечет время ожидания. Приведенные здесь примеры выполняют ту же задачу, но без блокировки. Это позволяет Arduino выполнять другие действия, пока она ожидает поступления данных.

serialEvent()

Я не рекомендую использовать эту функцию — я предпочитаю работать с последовательными данными, когда мне удобно. Она работает так же, как если бы этот код был последним в цикле loop().

if (Serial.available() > 0) {
    mySerialEvent();
}

Очистка входного буфера

Вероятно, стоит упомянуть, что функция Serial.flush() с неудачным названием не очищает входной буфер. Она актуальна только в том случае, если Arduino отправляет данные, и ее цель — блокировать Arduino до тех пор, пока все исходящие данные не будут отправлены.

Если вам нужно убедиться, что буфер последовательного ввода пуст, сделайте это следующим образом:

while (Serial.available() > 0) {
    Serial.read();
}

Получение чисел вместо текста

В приведенных примерах предполагается, что вы хотите получить текст. Но, возможно, вы хотите отправить число или комбинацию текста и чисел.

Пример 4. Получение одного числа через Serial Monitor

Самый простой случай — когда вы хотите ввести число в последовательный монитор (предполагаю, что в качестве окончания строки у вас используется символ новой строки). Допустим, вы хотите отправить число 234. Это вариация примера 2, которая будет работать с любым целочисленным значением. Обратите внимание, что если вы введёте недопустимое число, оно отобразится как 0 (ноль).

sample4.ino
// Пример 4 - Получение числа в виде текста и преобразование его в целое число
 
const byte numChars = 32;
char receivedChars[numChars];   // массив для хранения принятых данных
 
boolean newData = false;
 
int dataNumber = 0;             // новое для этой версии
 
void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}
 
void loop() {
    recvWithEndMarker();
    showNewNumber();
}
 
void recvWithEndMarker() {
    static byte ndx = 0;
    char endMarker = '\n';
    char rc;
 
    if (Serial.available() > 0) {
        rc = Serial.read();
 
        if (rc != endMarker) {
            receivedChars[ndx] = rc;
            ndx++;
            if (ndx >= numChars) {
                ndx = numChars - 1;
            }
        }
        else {
            receivedChars[ndx] = '\0'; // терминатор строки
            ndx = 0;
            newData = true;
        }
    }
}
 
void showNewNumber() {
    if (newData == true) {
        dataNumber = 0;             // новое для этой версии
        dataNumber = atoi(receivedChars);   // новое для этой версии
        Serial.print("This just in ... ");
        Serial.println(receivedChars);
        Serial.print("Data as Number ... ");    // новое для этой версии
        Serial.println(dataNumber);     // новое для этой версии
        newData = false;
    }
}

Пример 5. Получение и анализ нескольких фрагментов данных

Также можно легко получить несколько фрагментов данных в одном сообщении и проанализировать их, чтобы присвоить отдельным переменным. В этом примере предполагается, что вы отправляете что-то вроде <HelloWorld, 12, 24.7>. Это продолжение примера 3.

Добавлена функция parseData(), а функция showParsedData() заменяет showNewData() из предыдущего примера.

sample5.ino
// Пример 5 - Получение с использованием начальных и конечных маркеров в сочетании с синтаксическим анализом
 
const byte numChars = 32;
char receivedChars[numChars];
char tempChars[numChars];        // временный массив для использования при синтаксическом анализе
 
// переменные для хранения обработанных данных
char messageFromPC[numChars] = {0};
int integerFromPC = 0;
float floatFromPC = 0.0;
 
boolean newData = false;
 
//============
 
void setup() {
    Serial.begin(9600);
    Serial.println("This demo expects 3 pieces of data - text, an integer and a floating point value");
    Serial.println("Enter data in this style <HelloWorld, 12, 24.7>  ");
    Serial.println();
}
 
//============
 
void loop() {
    recvWithStartEndMarkers();
    if (newData == true) {
        strcpy(tempChars, receivedChars);
            // Эта временная копия необходима для защиты исходных данных
            //   потому что функция strtok(), используемая в parseData(), заменяет запятые на \0
        parseData();
        showParsedData();
        newData = false;
    }
}
 
//============
 
void recvWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    char startMarker = '<';
    char endMarker = '>';
    char rc;
 
    while (Serial.available() > 0 && newData == false) {
        rc = Serial.read();
 
        if (recvInProgress == true) {
            if (rc != endMarker) {
                receivedChars[ndx] = rc;
                ndx++;
                if (ndx >= numChars) {
                    ndx = numChars - 1;
                }
            }
            else {
                receivedChars[ndx] = '\0'; // терминатор строки
                recvInProgress = false;
                ndx = 0;
                newData = true;
            }
        }
 
        else if (rc == startMarker) {
            recvInProgress = true;
        }
    }
}
 
//============
 
void parseData() {      // делим данные на части
 
    char * strtokIndx; // используется функцией strtok() в качестве индекса
 
    strtokIndx = strtok(tempChars,",");      // получаем первую часть — строку
    strcpy(messageFromPC, strtokIndx); // копируем её в messageFromPC
 
    strtokIndx = strtok(NULL, ","); // продолжение
    integerFromPC = atoi(strtokIndx);     // конвертируем эту часть в целое число
 
    strtokIndx = strtok(NULL, ",");
    floatFromPC = atof(strtokIndx);     // преобразуем эту часть в число с плавающей запятой
 
}
 
//============
 
void showParsedData() {
    Serial.print("Message ");
    Serial.println(messageFromPC);
    Serial.print("Integer ");
    Serial.println(integerFromPC);
    Serial.print("Float ");
    Serial.println(floatFromPC);
}

Двоичные данные

До сих пор мы получали символьные данные — например, число 121 представлено символами '1', '2' и '1'. Это значение также можно отправить в виде двоичных данных в одном байте — это значение ASCII для символа 'y'. Обратите внимание, что 121 в десятичной системе счисления — это то же самое, что 0x79 в шестнадцатеричной.

Обратите внимание: если вы отправляете двоичные данные, то, скорее всего, вам придется отправлять в качестве данных так же и значения, которые используются в качестве начальных и конечных маркеров. Это выходит за рамки данного руководства, и один из способов сделать это показан в демке: here.

В приведенных ниже примерах предполагается, что двоичные данные НИКОГДА не будут содержать байтовые значения, используемые в качестве начальных и конечных маркеров. Для простоты я продолжу использовать в качестве маркеров символы < и >. Байтовые значения этих символов — 0x3C и 0x3E. Это позволит вам протестировать программу с помощью последовательного монитора, отправив, например, <24y>, что будет интерпретировано принимающей программой как двоичные значения 0x32, 0x34 и 0x79. Это коды ASCII для цифр 2, 4 и y.

Конечно, обычно двоичные данные отправляются другой компьютерной программой — на другом Arduino или на ПК.

Пример 6. Программа для приёма двоичных данных

Адаптировано на основе примера 3.

sample6.ino
// Пример 6 - Приём двоичных данных
 
const byte numBytes = 32;
byte receivedBytes[numBytes];
byte numReceived = 0;
 
boolean newData = false;
 
void setup() {
    Serial.begin(9600);
    Serial.println("<Arduino is ready>");
}
 
void loop() {
    recvBytesWithStartEndMarkers();
    showNewData();
}
 
void recvBytesWithStartEndMarkers() {
    static boolean recvInProgress = false;
    static byte ndx = 0;
    byte startMarker = 0x3C;
    byte endMarker = 0x3E;
    byte rb;
 
 
    while (Serial.available() > 0 && newData == false) {
        rb = Serial.read();
 
        if (recvInProgress == true) {
            if (rb != endMarker) {
                receivedBytes[ndx] = rb;
                ndx++;
                if (ndx >= numBytes) {
                    ndx = numBytes - 1;
                }
            }
            else {
                receivedBytes[ndx] = '\0'; // терминатор строки
                recvInProgress = false;
                numReceived = ndx;
                ndx = 0;
                newData = true;
            }
        }
 
        else if (rb == startMarker) {
            recvInProgress = true;
        }
    }
}
 
void showNewData() {
    if (newData == true) {
        Serial.print("This just in (HEX values)... ");
        for (byte n = 0; n < numReceived; n++) {
            Serial.print(receivedBytes[n], HEX);
            Serial.print(' ');
        }
        Serial.println();
        newData = false;
    }
}
  • arduino/arduino_osnovy_posledovatelnogo_vvoda.txt
  • Последнее изменение: 21.05.2026 21:48
  • r0wbh