Обрабатываем строки на Arduino. По мотивам «Обрабатываем строки на Arduino

Текстовые строки могут быть объявлены двумя способами: можно использовать тип данных String, который входит в ядро, начиная с версии 0019; либо объявить строку как массив символов char с нулевым символом в конце. На этой странице описан второй способ. Для получения более подробной информации об объекте String, предоставляющем больше возможностей ценой большего расхода памяти, см. страницу String - объект .

Примеры

Ниже представлены примеры правильного объявления строк.

Char Str1; char Str2 = {"a", "r", "d", "u", "i", "n", "o"}; char Str3 = {"a", "r", "d", "u", "i", "n", "o", "\0"}; char Str4 = "arduino"; char Str5 = "arduino"; char Str6 = "arduino";

Допускаемые операции при объявлении строк

  • Объявить массив символов без его инициализации (Str1)
  • Объявить массив символов с одним избыточным элементом, компилятор сам добавит требуемый нулевой символ (Str2)
  • Добавить нулевой символ явно (Str3)
  • Инициализировать массив с помощью строковой константы, заключенной в кавычки; компилятор создаст массив необходимого размера с нулевым символом в конце (Str4)
  • Инициализировать массив с помощью строковой константы, явно указав его размер (Str5)
  • Инициализировать массив избыточного размера, оставив место для более длинных строк (Str6)

Нулевой завершающий символ

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

По сути, это означает, что длина вашей строки должна быть на 1 символ больше, чем текст, который вы хотели бы в ней хранить. Именно поэтому Str2 и Str5 должны быть длиной 8 символов, несмотря на то, что слово "arduino" занимает всего 7 - последняя позиция автоматически заполняется нулевым символом. Размер Str4 автоматически станет равным 8 - один символ требуется для завершающего нуля. В строке Str3 мы самостоятельно указали нулевой символ (обозначается "\0").

Следует иметь в виду, что в целом можно объявить строку и без завершающего нулевого символа (например, если задать длину Str2 равной 7, а не 8). Однако это приведет к неработоспособности большинства строковых функций, поэтому не следует намеренно так делать. Такая ошибка может быть причиной странного поведения или появления сторонних символов при работе со строками.

Одинарные или двойные кавычки?

Строки всегда объявляются в двойных кавычках ("Abc"), а символы всегда объявляются в одинарных кавычках ("A").

Перенос длинных строк

Длинные строки можно переносить так:

Char myString = "This is the first line" " this is the second line" " etcetera";

Массивы строк

При работе с большими объемами текста (например, в проектах, работающих с LCD-экраном) часто удобно использовать массивы строк. Поскольку строки сами по себе являются массивами, то фактически, это - пример двумерного массива.

В нижеследующей программе звездочка после указания типа данных char "char*" показывает, что переменная является массивом "указателей". Все имена массивов фактически являются указателями, поэтому звездочка необходима для создания массива массивов. Указатели в C - одна из наиболее сложных вещей для начинающих, но в данном случае глубокого понимания указателей для их эффективного использования вовсе не требуется.

Пример

char* myStrings={"This is string 1", "This is string 2", "This is string 3", "This is string 4", "This is string 5","This is string 6"}; void setup(){ Serial.begin(9600); } void loop(){ for (int i = 0; i < 6; i++){ Serial.println(myStrings[i]); delay(500); } }

Что требовалось? Вывод информации и обработка введённых пользователем строк. Например:

Ethernet controller - ok
STATIC mode
>time
2015-11-16 22:35:27

Собственно, надо сравнить стоки. Нет, сначала надо разбить текст на фрагменты разделителем (например пробел), но потом всё равно сравнить строки. Поскольку команд было «раз, два - и обчёлся», то разбивку текста на фрагменты убрал. Из-за указанной выше ошибки класс String использовать не получалось, то как можно по другому? Arduino использует библиотеку AVR-libc , то резонно в первую очередь обратиться к ней.
Что имеем?

  1. stdlib.h - функции взаимного преобразования чисел и строк (в обе стороны).
  2. string.h - функции работы со строками. Основной наш интерес.
  3. stdio.h - функции стандартного ввода-вывода.

Этим не ограничивается функционал. Упомянуто то, что связано с задачей.

№2 - используем функции memset для заполнения или очистки буфера, memcmp - для сравнения. strcmp не использую, так как нужно явно ограничивать длину сравниваемого фрагмента. №3 - для форматного чтения и вывода: sprintf , sprint_P , sscanf , sscanf_P . Функции с суффиксом _P отличаются тем, что строку форматирования берут из памяти программ PROGMEM, он же макрос F() в библиотеках Arduino.

Кстати

Кстати, если полноценно реализовать функции ввода-вывода отдельного символа getc и putc , то получите стандартные потоки ввода, вывода, ошибок и для работы с файлами, если таковые у вас есть. Часто можно обойтись, переопределив макросы putchar() и getchar() , работающие со стандартным вводом и выводом.

У меня сравнение строк выглядит так:

If (memcmp(str ,"statlist" ,8)==0) { // your code here }

Пожалуй, стоит оговориться, что сравниваются начала строк. Для поиска фрагментов можно использовать memmem .

строки для Си

строки для Си str , они же char * - это ссылка на начало последовательности char , последняя из которых имеет значение 0x00 . А значит, их надо где-то разместить. Например, в массиве. Или использовать malloc , calloc , free . Что не даёт делать ошибок подразумевает переложение ответственности на программиста за их размещение и контроль длинны .

То есть поиск команды может выглядеть так:

If (memcmp(str ,"statclear", 9)==0) { memset(journal, 0, sizeof(jrn_rec_t)*JRN_REC_NUM); Serial.println(F("ok")); }else if (memcmp(str ,"statlist" ,8)==0) { funcStatlist(); }else if (memcmp(str ,"cfgshow", 7)==0) { funcCfgShow(); }else if (memcmp(str ,"timeset", 7)==0) { funcTimeSet(str); // setup date and time YYYY-MM-DD hh:mm:ss }else if (memcmp(str ,"cfgset", 6)==0) { funcCfgSet(str); //funcPingdel(str); }else if (memcmp(str ,"time", 4)==0) { funcTime(); // print date and time from RTC }else if (memcmp(str ,"help", 4)==0) { // print short help Serial.println(F(" helprn statlist statclearrn time timesetrn cfgshow cfgset")); }else{ Serial.print(F("unknow cmd> ")); Serial.println(str); }

Неочевидный момент

Команды, они же строки, с большей длинной должны идти первыми в приведённом фрагменте. Задумайтесь, почему?

Строки «собираю» следующим образом: читаю байты с порта, пока не превышена допустимая длинна строки или пока не встречен один из символов перевода строки r или n.

чтение строки

Лучше доработать бы… Пока как есть. Вызывается всё время в основном кольце. Если нет работы - максимально быстро на выход, возвращаем false . Если набрали новую строку - true . bool readln(HardwareSerial &uart, char *outbuf) // return true when find CR, LF or both and if size limit { static char mybuf = { 0 }; static char idx = 0; while (uart.available()) { if (uart.peek()!= "r" && uart.peek()!= "n") { mybuf[ idx++ ] = uart.read(); } else {// если CR uart.read(); if (uart.peek()=="n" || uart.peek()=="r") uart.read(); if (idx == 0) { return 0; } mybuf[ idx++ ] = ""; // дописать 0 memcpy(outbuf, mybuf, idx); // скопировать idx = 0; return 1; } if (idx >=(SBUF_SZ-1)) { // проверяем на длину внутреннего буфера mybuf[ SBUF_SZ-1 ] = ""; // дописать 0 memcpy(outbuf, mybuf, 32); // скопировать idx = 0; return 1; } } return 0; }

Ещё очень полезен форматный ввод-вывод. Например, разбор строки с ведённой датой и временем выглядит так:

Sscanf_P(str, (const char *)F("%*s %d-%d-%d %d:%d:%d"), &y, &m, &d, &hh, &mm, &ss)

Получение строки для вывода IP:

Sprintf_P(buff, (const char *)F("Your IP: %d.%d.%d.%d"), ip, ip, ip, ip);

Вот собственно и всё. Надеюсь, кому-то данный материал поможет «отвязаться» от Arduino или просто лучше и за меньшее время писать свои программы. Но более типичная ситуация - обойти ограничения Wiring .


String text = "Temp: " + tempC + " C";

Увы, в C этот прием не работает. В данном случае сообщение можно вывести несколькими инструкциями print, как показано далее:

lcd.print("Temp: "); lcd.print(tempC); lcd.print(" C");

Этот подход устраняет необходимость закулисного копирования данных в процессе конкатенации (объединения) строк, как происходит в других современных языках.

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

Форматирование строк с помощью sprintf

Стандартная библиотека строковых функций для языка C (не путайте с библиотекой Arduino String Object, которая обсуждается в следующем разделе) включает очень удобную функцию sprintf, выполняющую форматирование массивов символов. Она вставляет значения переменных в строку шаблона, как показано в следующем примере:

sprint(line1, "Temp: %d C", tempC);

Массив символов line1 - это строковый буфер, содержащий форматированный текст. Как указано в примере, он имеет емкость 17 символов, включая дополнительный нулевой символ в конце. Имя line1 я выбрал потому, что собираюсь показать, как сформировать содержимое верхней строки для жидкокристаллического дисплея с двумя строками по 16 символов в каждой.

В первом параметре команде sprintf передается массив символов, в который должен быть записан результат. Следующий аргумент - строка формата, содержащая смесь простого текста, такого как Temp:, и команд форматирования, например %d. В данном случае %d означает «десятичное целое со знаком». Остальные параметры будут подставлены в строку формата в порядке их следования на место команд форматирования.

Чтобы во вторую строку на жидкокристаллическом дисплее вывести время, его можно сформировать из отдельных значений часов, минут и секунд, как показано далее:

sprintf(line2, "Time: %2d:%02d:%02d", h, m, s);

Если попробовать вывести строку line2 в монитор последовательного порта или на экран жидкокристаллического дисплея, вы увидите текст

Команда sprintf не только подставила числа в нужные места, но и добавила ведущий ноль перед цифрой 5. В примере между символами: находятся команды форматирования трех компонентов времени. Часам соответствует команда %2d, которая выводит двузначное десятичное число. Команды форматирования для минут и секунд немного отличаются (%02d). Эти команды также выводят двузначные десятичные числа, но добавляют ведущий ноль, если это необходимо.

Однако имейте в виду, что этот прием предназначен для значений типа int. К сожалению, разработчики Arduino не реализовали в стандартной библиотеке C поддержку других типов, таких как float.

Определение длины строки

Так как строки, хранящиеся в массивах символов, часто оказываются короче самих массивов, в библиотеке предусмотрена удобная функция с именем strlen. Эта функция подсчитывает число символов в массиве, предшествующих нулевому символу, отмечающему конец строки.

Функция принимает массив символов в своем единственном параметре и возвращает размер строки (исключая пустой символ), хранящейся в нем, например, команда

вернет число 3.

Библиотека Arduino String Object

В Arduino IDE, начиная с версии 019, вышедшей несколько лет тому назад, включается библиотека String, более понятная и дружественная разработчикам, использующим Java, Ruby, Python и другие языки, где конкатенацию строк допускается выполнять простым оператором +. Эта библиотека также предлагает массу вспомогательных функций для работы со строками.

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

Эта библиотека удивительно проста в использовании, и, если вам приходилось работать со строками в Java, благодаря библиотеке Arduino String Object вы будете чувствовать себя как дома.

Создание строк

Создать строку можно из массива элементов типа char, а также из значения типа int или float, как показано в следующем примере:

String message = "Temp: ";

String temp = String(123);

Конкатенация строк

Строки типа String можно объединять друг с другом и с данными других типов с помощью оператора +. Попробуйте добавить следующий код в функцию setup пустого скетча:

Serial.begin(9600);

String message = "Temp: ";

String temp = String(123);

Serial.println(message + temp + " C");

Обратите внимание на то, что последнее значение, добавляемое в строку, в действительности является массивом символов. Если первый элемент в последовательности значений между операторами + является строкой, остальные элементы автоматически будут преобразованы в строки перед объединением.

Другие строковые функции

В табл. 6.1 перечислены еще несколько удобных функций из библиотеки String. Полный список доступных функций можно найти по адресу http://arduino.cc/en/Reference/StringObject.

Таблица 6.1. Некоторые полезные функции в библиотеке String

Функция

Пример

Описание

char ch = String("abc")

Переменная ch получит значение "a"

String s = " abc ";

Удалит пробелы с обеих сторон от группы символов abc. Переменная s получит значение "abc"

String s = "123";

int x = s.toInt();

Преобразует строковое представление числа в значение типа int или long

String s = "abcdefg";

String s2 = s.substring(1, 3);

Возвращает фрагмент исходной строки. Переменная s2 получит значение "bc". В параметрах передаются: индекс первого символа фрагмента и индекс символа, следующего за последним символом фрагмента

String s = "abcdefg";

s.replace("de", "DE");

Заменит все вхождения "de" в строке на "DE". Переменная s2 получит значение "abcDEfg"

Использование ЭСППЗУ

Содержимое всех переменных, используемых в скетче Arduino, теряется при выключении питания или выполнении сброса. Чтобы сохранить значения, их нужно записать байт за байтом в память ЭСППЗУ. В Arduino Uno имеется 1 Кбайт памяти ЭСППЗУ.

ПРИМЕЧАНИЕ

Это не относится к плате Arduino Due, не имеющей ЭСППЗУ. В этой модели данные следует сохранять на карту microSD.

Для чтения и записи данных в ЭСППЗУ требуется использовать библиотеку, входящую в состав Arduino IDE. Следующий пример демонстрирует, как записать единственный байт в ЭСППЗУ, в данном случае операция выполняется в функции setup:

#include

byte valueToSave = 123

EEPROM.write(0, valueToSave);

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

18 ноября 2015 в 14:39

По мотивам «Обрабатываем строки на Arduino»

  • Разработка под Arduino

Что требовалось? Вывод информации и обработка введённых пользователем строк. Например:

Ethernet controller - ok
STATIC mode
>time
2015-11-16 22:35:27

Собственно, надо сравнить стоки. Нет, сначала надо разбить текст на фрагменты разделителем (например пробел), но потом всё равно сравнить строки. Поскольку команд было «раз, два - и обчёлся», то разбивку текста на фрагменты убрал. Из-за указанной выше ошибки класс String использовать не получалось, то как можно по другому? Arduino использует библиотеку AVR-libc , то резонно в первую очередь обратиться к ней.
Что имеем?
  1. stdlib.h - функции взаимного преобразования чисел и строк (в обе стороны).
  2. string.h - функции работы со строками. Основной наш интерес.
  3. stdio.h - функции стандартного ввода-вывода.
Этим не ограничивается функционал. Упомянуто то, что связано с задачей.

№2 - используем функции memset для заполнения или очистки буфера, memcmp - для сравнения. strcmp не использую, так как нужно явно ограничивать длину сравниваемого фрагмента. №3 - для форматного чтения и вывода: sprintf , sprint_P , sscanf , sscanf_P . Функции с суффиксом _P отличаются тем, что строку форматирования берут из памяти программ PROGMEM, он же макрос F() в библиотеках Arduino.

Кстати

Кстати, если полноценно реализовать функции ввода-вывода отдельного символа getc и putc , то получите стандартные потоки ввода, вывода, ошибок и для работы с файлами, если таковые у вас есть. Часто можно обойтись, переопределив макросы putchar() и getchar() , работающие со стандартным вводом и выводом.


У меня сравнение строк выглядит так:

If (memcmp(str ,"statlist" ,8)==0) { // your code here }
Пожалуй, стоит оговориться, что сравниваются начала строк. Для поиска фрагментов можно использовать memmem .

строки для Си

строки для Си str , они же char * - это ссылка на начало последовательности char , последняя из которых имеет значение 0x00 . А значит, их надо где-то разместить. Например, в массиве. Или использовать malloc , calloc , free . Что не даёт делать ошибок подразумевает переложение ответственности на программиста за их размещение и контроль длинны .


То есть поиск команды может выглядеть так:

If (memcmp(str ,"statclear", 9)==0) { memset(journal, 0, sizeof(jrn_rec_t)*JRN_REC_NUM); Serial.println(F("ok")); }else if (memcmp(str ,"statlist" ,8)==0) { funcStatlist(); }else if (memcmp(str ,"cfgshow", 7)==0) { funcCfgShow(); }else if (memcmp(str ,"timeset", 7)==0) { funcTimeSet(str); // setup date and time YYYY-MM-DD hh:mm:ss }else if (memcmp(str ,"cfgset", 6)==0) { funcCfgSet(str); //funcPingdel(str); }else if (memcmp(str ,"time", 4)==0) { funcTime(); // print date and time from RTC }else if (memcmp(str ,"help", 4)==0) { // print short help Serial.println(F(" help\r\n statlist statclear\r\n time timeset\r\n cfgshow cfgset")); }else{ Serial.print(F("unknow cmd> ")); Serial.println(str); }

Неочевидный момент

Команды, они же строки, с большей длинной должны идти первыми в приведённом фрагменте. Задумайтесь, почему?


Строки «собираю» следующим образом: читаю байты с порта, пока не превышена допустимая длинна строки или пока не встречен один из символов перевода строки \r или \n.

чтение строки

Лучше доработать бы… Пока как есть. Вызывается всё время в основном кольце. Если нет работы - максимально быстро на выход, возвращаем false . Если набрали новую строку - true .

Bool readln(HardwareSerial &uart, char *outbuf) // return true when find CR, LF or both and if size limit { static char mybuf = { 0 }; static char idx = 0; while (uart.available()) { if (uart.peek()!= "\r" && uart.peek()!= "\n") { mybuf[ idx++ ] = uart.read(); } else {// если CR uart.read(); if (uart.peek()=="\n" || uart.peek()=="\r") uart.read(); if (idx == 0) { return 0; } mybuf[ idx++ ] = "\0"; // дописать 0 memcpy(outbuf, mybuf, idx); // скопировать idx = 0; return 1; } if (idx >=(SBUF_SZ-1)) { // проверяем на длину внутреннего буфера mybuf[ SBUF_SZ-1 ] = "\0"; // дописать 0 memcpy(outbuf, mybuf, 32); // скопировать idx = 0; return 1; } } return 0; }


Ещё очень полезен форматный ввод-вывод. Например, разбор строки с ведённой датой и временем выглядит так: sscanf_P(str, (const char *)F("%*s %d-%d-%d %d:%d:%d"), &y, &m, &d, &hh, &mm, &ss)
Получение строки для вывода IP:

Sprintf_P(buff, (const char *)F("Your IP: %d.%d.%d.%d"), ip, ip, ip, ip);
Подробней о строке формата можно почитать, например,

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

Итак. Вот примерный алгоритм, которому я следовал:

  1. Идем на arduino.ru и высматриваем в колонке типов все, связанное с символами.
  2. Решаем, какую форму представления будем использовать (Я остановился на классе String, т.к. имел неприятный опыт с месивом массивом).
  3. Судорожно пытаемся написать свою функцию с преферансом и профурсетками
  4. Ищем класса.
  5. Ищем нужные операторы.
  6. Пишем!
А алгоритм работы основного тела программы прост:
  1. Циклично проверяем, есть ли в буфере com порта доступный для чтения байт, если есть, читаем.
  2. Если принятый байт - символ переноса строки ("\n"), то вызываем самописную функцию парсинга, если же нет, то добавляем принятый байт в созданную переменную типа String.
  3. Парсим, наконец, строку.

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

  4. В зависимости от принятого результата с помощью switch case выбираем нужный.
  5. Обнуляем принятую строку, чтобы потом начать собирать ее заново.

А вот, наконец-таки, код:

#define led 13 String input_string = ""; const String Led_off = "switch led off"; const String Led_on = "switch led on"; bool led_running; void setup() { Serial.begin(9600); } void loop() { while (Serial.available() > 0) { char c = Serial.read(); if (c == "\n") { Serial.print("Input_string is: "); Serial.println(input_string); switch (parse(input_string, Led_off, Led_on)) { case 10: led_running=false; Serial.println("Switching off is done"); break; case 11: led_running=true; Serial.println("Switching on is done"); break; case 0: Serial.println("invalid String"); break; } input_string = ""; digitalWrite(led, led_running); } else { input_string += c; } } } byte parse(String input_string, const String Led_off, const String Led_on) { if (input_string.equals(Led_off) == true) { return 10; } else if (input_string.equals(Led_on) == true) { return 11; } else return 0; }


Так, я не понял, что за дела? Почему не загорается светодиод? Ах да, как же это я запамятовал, в void setup нужно добавить:

PinMode(led, OUTPUT);

P.S.: Немаловажно установить монитор com порта в Arduino IDE в режим «Новая строка», т.к. в любом другом посылаемая строка не будет сопровождаться символом ее окончания "\n".

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

P.P.P.S.: Если статья будет принята адекватно, напишу следующую о том, что у меня вышло с улучшением функционала функции парсинга. Ну, с богом! .