Полгода я вёл журнал наблюдений за теплицей на даче: термометр на веранде, блокнот на гвоздике и моя рука с карандашом каждые два часа. Через неделю выяснилось, что половина строк кривая: то время пропущено, то температура записана «на глаз», то чернила размокли от дождя. Однажды я назначил замер на 14:00, вспомнил о нём в 19:00 — и график за день превратился в решето с вычерками. Абсурд дошёл до меня не сразу: плата на веранде каждые две секунды честно меряла датчиком, а запись вёл человек с карандашом, у которого своя жизнь. Развязка случилась в гостях: знакомый открыл монитор порта, скопировал столбик чисел через запятую и вставил в таблицу — значения разложились по ячейкам, и график построился сам. В тот вечер мой блокнот уехал на полку, а место журнала заняла одна строка Serial.print.
Здесь мы соберём такой журнал сами. Сложим несколько переменных в одну строку через Serial.print() и поймём, что за зверь такой CSV. Разберём три ходовых разделителя — запятую, точку с запятой и табуляцию — и почему русская локаль Excel переворачивает правила игры. Накормим данными Serial Plotter: числа в строке превратятся в живые кривые, а затесавшийся текст строку сломает. И заменим delay() на millis-таймер, чтобы интервал между строками был стабильным, а loop() не замирал. Одиночные значения мы уже печатали в уроке про первый разговор с платой, CSV-строку мельком пробовали в уроке про отладку через Serial — теперь разберём формат по-взрослому.
CSV: одна строка — одна запись
CSV — договор до смешного простой: comma-separated values, «значения через запятую». Каждая строка текста — одна запись, каждый кусок между разделителями — ячейка. Никакой магии: обычный текст, который открывается и блокнотом, и Excel, и Google Таблицами. Первая строка — шапка со списком столбцов, дальше — данные. Отсюда правило разметки: шапку печатаем один раз в setup(), данные — в loop(); печатайте шапку в цикле, и таблица превратится в гребёнку из повторяющихся заголовков, которую потом чистить вручную. Второе правило: одна строка — один момент времени. Не «за день было 14 замеров», а строка на каждый замер: время, значение, ещё значение. Машина прозу не читает, зато строки с одинаковой структурой ест без запинки.
Собирается запись цепочкой: значение, разделитель, значение, разделитель, значение — и Serial.println() в конце, чтобы строка закрылась переводом. println здесь не украшение, а замыкающий символ: забудете его — следующее измерение приклеится к предыдущему, третье к ним обоим, и к утру в мониторе будет одна бесконечная строка. Прикинем цену по времени: на 9600 бод UART выдаёт примерно 960 байт в секунду, строка из двадцати символов — около двадцати миллисекунд передачи. Печатать каждые 300 мс — значит порт занят примерно семь процентов времени, смехотворно мало; каждые 30 мс — почти непрерывно, скетч начнёт задыхаться. А чтобы интервал был честным, вместо delay(300) ставим millis-таймер: if (millis() - lastLog >= 300). loop() не замирает, кнопки и датчики живут своей жизнью, а строки выходят ровно через 300 мс. Кстати, сырые показания термистора из урока про NTC-термистор — готовые жильцы такой таблицы: время, сырые единицы, температура в градусах.
Пара слов о столбцах. Время — почти всегда первый: на Uno настоящих часов нет, поэтому меткой служит millis() — миллисекунды с включения; для отладки этого достаточно, а если нужен именно час дня — придётся добавить модуль часов реального времени, у него будет свой разговор. Единицы держите в шапке, а не в данных: столбец temp_c честнее столбца с «25.4 C», потому что ячейка должна остаться числом. И не бойтесь пропусков: если датчик временно не подключён, печатайте 0 или -1 — структура строки останется ровной, и таблица не поедет на первом же пустом месте. Ровная структура важнее красивых значений: парсер прощает нули, но не прощает лишний столбец.
unsigned long lastLog = 0;const unsigned long INTERVAL = 300; void setup() { Serial.begin(9600); Serial.println("time_ms,raw,percent"); // шапка печатается один раз} void loop() { unsigned long now = millis(); if (now - lastLog >= INTERVAL) { // millis-таймер вместо delay lastLog = now; int raw = analogRead(A0); int percent = map(raw, 0, 1023, 0, 100); Serial.print(now); Serial.print(","); Serial.print(raw); Serial.print(","); Serial.println(percent); // println закрывает строку }}Запятая, точка с запятой, табуляция: три диалекта
Казалось бы, выбирай любой разделитель и живи. Но у каждого — свой фан-клуб и свои грабли. Запятая — мировой стандарт: её ждут Serial Plotter, англоязычный Excel, почти все онлайн-парсеры. Точка с запятой — европейская классика, и вот почему: во многих странах, включая Россию, десятичный разделитель — запятая, и Excel с русской локалью читает «3,42» как одно число. Суньте такому Excel строку «12,3,42» — и он запутается, свалив всё в первый столбец. Табуляция — третий диалект: скопируйте столбик через таб из монитора и вставьте в таблицу — значения разложатся по ячейкам без всякого импорта. В логе таб ещё и выравнивает колонки, если значения разной длины: глазу заметно легче, чем в сплошной каше.
Мой выбор после пары вечеров с русским Excel: в скетчах я печатаю через запятую и храню числа с точкой — это диалект плоттера и остального мира; когда файл нужен в таблице на русскоязычном компьютере, пользуюсь импортом с указанием разделителя вручную, а не подстраиваю скетч под локаль. Но это вкусовщина: железное правило одно — не смешивать диалекты в одном логе. Запятая-разделитель плюс десятичная запятая в числах — гарантированная каша. И второе: не выдумывайте собственных разделителей. Вертикальная черта, двоеточие, три тире подряд — выглядят солидно, но ни один парсер их не ждёт, и перед каждым импортом вы будете делать поиск и замену руками. Если же однажды понадобится читать такую таблицу обратно — числами, по одному за вызов, — это делает parseInt(): механика разобрана в уроке про Serial.parseInt и parseFloat. Шпаргалка ниже — то, к чему я возвращаюсь до сих пор.
| Разделитель | Где принят | Подводный камень |
|---|---|---|
| запятая , | Serial Plotter, английские Excel и Google Таблицы | русская локаль разваливает столбцы: запятая там — десятичный знак |
| точка с запятой ; | Excel и Google Таблицы с русской/европейской локалью | плоттер и классические парсеры ждут запятую — ряды слипнутся |
| табуляция \t | копипаст из монитора прямо в ячейки таблицы | невидима в бумажном протоколе: перепечатать руками не угадаешь |
| пробел | короткие человеческие логи вида «t=12 raw=512» | для таблиц не годится: число пробелов гуляет — столбцы едут |
void setup() { Serial.begin(9600);} void loop() { int raw = analogRead(A0); float volt = raw * 5.0 / 1023.0; // десятичная точка — для машин int prc = map(raw, 0, 1023, 0, 100); Serial.print("raw="); Serial.print(raw); Serial.print(";volt="); Serial.print(volt, 2); // два знака после точки Serial.print(";prc="); Serial.println(prc); delay(300);}Плоттер: числа в строке — ряды на графике
Serial Plotter в Arduino IDE и вкладка «Плоттер» в нашем симуляторе едят те же строки, что и таблица. Правило простое: сколько чисел в строке — столько точек на графике в этом кадре. Печатаете «12,3,42» каждые 300 мс — получаете три кривые, обновляющиеся три раза в секунду. Если снабдить каждое число меткой вида «raw=512», плоттер ещё и подпишет ряды именами — а три безымянных ряда «1, 2, 3» через минуту превращаются в игру «угадай, какая линия про температуру». Метки не обязательны, но легенда на экране живёт дольше, чем легенда в голове. Интервал задаёт гладкость: 300 мс — около трёх точек в секунду, за минуту набегает двести — любому графику хватает с запасом.
Теперь тёмная сторона: текст среди чисел ломает строку. Строка данных — это или только голые числа, или только пары «имя=значение», и ничего третьего. Смесь вроде «raw=512 болт 3.42» плоттер разбирает криво: пары и числа вперемешку не сходятся по счёту, разбор падает в запасной режим — и ряды от строки к строке перепутываются, график пляшет, как безумный осциллограф. Совсем без цифр строка не страшна: сообщение «Podklyuchite datchik» плоттер просто пропустит — но монитор его напечатает, и лог замусорится. Поэтому у меня правило: данные — в строках данных, слова — в отдельных событиях. Захотелось прокомментировать — печатайте комментарий отдельной строкой до или после, а в строке таблицы держите только то, что должно стать ячейкой.
Практическая мелочь: в симуляторе график можно заморозить кнопкой «пауза» и рассмотреть значения в спокойствии, а «сброс графика» начинает кривые заново, не трогая монитор. В Arduino IDE 2 у плоттера есть своя пауза — приём тот же. Если кривые уехали за край экрана, это не поломка: величины разного порядка — сырые единицы 0–1023 и вольты 0–5 — делят один вертикальный масштаб, и мелкая прижалась к полу. Лечится либо печатью величин одного порядка, либо двумя отдельными запусками с одним рядом. На этом передача исчерпана: дальше по разделу — обратная сторона, плата слушает строки: readStringUntil() и команды словами.
- CSV: одна строка — одна запись; значения через разделитель; println() в конце закрывает строку
- Шапку столбцов печатайте один раз в setup(), данные — в loop()
- Запятая — для плоттера и англоязычных таблиц; точка с запятой — для русской локали Excel; табуляция — для копипаста в ячейки
- В строке данных только числа (или только пары «имя=значение»): лишний текст сбивает разбор, и ряды перепутаются
- millis-таймер вместо delay() даёт стабильный интервал и не блокирует loop()
- 1. Что означает одна строка CSV-лога с данными?
- 2. Почему Excel с русской локалью открывает CSV с запятыми «всё в один столбец»?
- 3. В строку данных затесалось слово без метки: «raw=512 bolt 3.42». Что увидит плоттер?
- 4. Зачем в логгере millis-таймер вместо delay(300)?
Частые вопросы по теме урока
- Что означает одна строка CSV-лога с данными?
- Строка — срез состояния в один момент времени: значения через разделитель, println() закрывает запись. Шапка печатается один раз и к данным не относится. Одну запись: один момент времени и набор значений всех столбцов
- Почему Excel с русской локалью открывает CSV с запятыми «всё в один столбец»?
- В русской локали десятичная запятая — часть числа, поэтому разделителем служит точка с запятой. CSV с запятыми требует импорта с указанием разделителя вручную. В этой локали запятая — десятичный разделитель, и Excel путается в числах
- В строку данных затесалось слово без метки: «raw=512 bolt 3.42». Что увидит плоттер?
- Пары «имя=значение» и голые числа в одной строке не сходятся по счёту — разбор падает в запасной режим, и ряды пляшут. Данные и слова держите в разных строках. Разбор собьётся, и ряды между строками перепутаются
- Зачем в логгере millis-таймер вместо delay(300)?
- delay() замораживает весь скетч на 300 мс — кнопки и датчики отстают от жизни. millis-таймер держит интервал, не блокируя loop(). loop() не замирает, а интервал между строками остаётся стабильным