Логгер температуры в гараже писал всё время, пока было питание. За две недели на карте накопилось шесть с половиной тысяч строк, из которых мне нужны были четыре часа одного конкретного вечера — когда в подкапотном пространстве что-то щёлкало. Искал я их, листая CSV в блокноте, и клялся, что сделаю кнопку. Второй мотив банальнее: скетч стартует вместе с питанием, и лог начинается с момента подачи пяти вольт, а не с момента, когда стало интересно.
Первое, что приходит в голову — останавливать запись кнопкой reset на плате. Я так делал, и это грабля: reset перезапускает скетч, close() при этом не вызывается, и всё, что сидело в буфере 512 байт, в файл не попадает. Каждая последняя сессия перед выдёргиванием карты оказывалась обрезанной. Честный стоп — программный: нажали кнопку, скетч сам закрыл файл, диод погас, карту можно вынимать.
Здесь соберём полный цикл управления записью: фронт кнопки с антидребезгом, открытие сессии новым файлом с номером, запись с индикатором на светодиоде и закрытие по второму нажатию. Кнопку вешаем на D2, индикатор — на D12. Заодно разберём, почему на каждую сессию заводится отдельный файл и что происходит с текущей сессией, если всё-таки нажать reset. Кнопку я ставлю внешнюю, на проводах: панелька у двери дороже микропереключателя на макетке, зато нажимается без разборки корпуса — и это уже половина пользы всей схемы.
Фронт кнопки: от нажатия к сессии
Кнопка старт-стоп — это не «нажата или нет», а переход. Скетч опрашивает пин D2 каждый оборот loop, и интересен ему только момент, когда значение сменилось с HIGH на LOW: было отпущено — стало нажато. Такой переход называется фронтом. Пока скетч реагирует на уровень, одно нажатие даст несколько переключений сессии подряд: механические контакты дребезжат 5-20 мс, и вместо одного чистого LOW пин выдаёт пачку импульсов.
Защита двухслойная. Первая — подтяжка: pinMode(BTN, INPUT_PULLUP) держит пин в HIGH через внутренний резистор платы, кнопка на нажатие замыкает его на GND, внешних резисторов не нужно. Вторая — антидребезг: после зарегистрированного фронта я игнорирую изменения пина 200 мс, и дребезг, и случайное второе нажатие отсекаются одним сравнением millis(). Для старт-стопа это нормальная цена: 200 мс между сессиями никто не заметит, а вот сессия, открытая-закрытая пять раз за одно нажатие, — заметит. Почему именно 200 мс: дребезг укладывается в первые 20 мс, а осознанное повторное нажатие быстрее 150-200 мс человек не делает — окно короче сотни от дребезга защищает слабо, длиннее полусекунды — уже тормозит.
Состояний у логгера ровно два: покой и запись. В покое скетч только опрашивает кнопку — карта не трогается вовсе. По фронту — открытие файла с шапкой и переход в запись; по второму фронту — close и обратно в покой. Отдельная деталь: SD.begin() я делаю один раз в setup, а не при каждом старте сессии — полсекунды инициализации в ответ на кнопку выглядят как зависание. Правила этой инициализации разобраны в уроке про SD.begin и диагностику.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;const int BTN = 2; // кнопка между пином и GNDconst int LED = 12; // индикатор сессии bool logging = false;bool prevBtn = HIGH;unsigned long lastPress = 0;unsigned long lastLine = 0;unsigned long sessionStart = 0;int session = 0; // номер сессии, растёт за жизнь платыint lines = 0;String fileName = ""; void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); pinMode(LED, OUTPUT); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } }} void loop() { bool nowBtn = digitalRead(BTN); // фронт: отпущена -> нажата, плюс окно 200 мс от дребезга if (prevBtn == HIGH && nowBtn == LOW && millis() - lastPress > 200) { lastPress = millis(); if (!logging) { session = session + 1; fileName = "run" + String(session) + ".csv"; File f = SD.open(fileName.c_str(), FILE_WRITE); if (f) { f.println("sec,v"); // шапка один раз за сессию f.close(); logging = true; lines = 0; sessionStart = millis(); digitalWrite(LED, HIGH); // горит, пока файл открыт Serial.print("start run"); Serial.println(session); } else { Serial.println("open failed"); } } else { logging = false; digitalWrite(LED, LOW); // погас — карту можно вынимать Serial.print("stop run"); Serial.print(session); Serial.print(" lines="); Serial.println(lines); } } prevBtn = nowBtn; // в сессии: строка раз в секунду, открыл - дописал - закрыл if (logging && millis() - lastLine >= 1000) { lastLine = millis(); File f = SD.open(fileName.c_str(), FILE_WRITE); if (f) { f.print((millis() - sessionStart) / 1000); f.print(","); f.println(analogRead(A0)); f.close(); lines = lines + 1; } }}Обратите внимание на имя файла: run1.csv, run2.csv — счётчик растёт с каждой сессией, пока плата жива. Новая сессия получает новый файл по трём причинам. Первая — шапка: sec,v в первой строке каждого файла, и любой из них открывается в таблицу сам по себе. Вторая — границы: если дописывать в один log.csv, за сутки получится каша без разделения на сессии, и выделить нужную будет нечем. Третья — цена: на карте 8 ГБ кластер 8 КБ, недозаполненный хвост каждой сессии стоит копейки; сотни файлов в год — норма, тысячи — уже трата места, и этот подсчёт мы делали в уроке про кластеры и размеры.
Запись внутри сессии я веду схемой открыл — дописал — закрыл на каждую строку: дороже открытого файла, зато при выдёргивании питания теряется максимум одна строка, а не вся сессия с прошлого flush. Если строк в секунду больше десяти, переходите на открытый файл со сбросом буфера каждые 2-5 секунд — эта развилка разобрана в уроке про println и close. Цена моей схемы открыл-закрыл-на-строку измерена: 10-30 мс раз в секунду, один-три процента времени, а потеря при сбое ограничена одной строкой. Номер сессии при перезагрузке обнуляется; если файлы после выключения должны не перезаписываться, перенесите счётчик в EEPROM тем же приёмом, каким в уроке про счётчик включений переживает питание число пусков.
| Чем останавливают запись | close() выполняется | Что остаётся на карте |
|---|---|---|
| Кнопка старт-стоп в скетче | Да — по второму нажатию | Сессия зафиксирована целиком, размер файла актуален |
| Кнопка reset на плате | Нет | Хвост буфера потерян, файл обрезан до прошлой фиксации |
| Вынуть карту на живой плате | Нет, и хуже: под током | То же плюс шанс развалить FAT при физической записи |
| Таймер простоя в скетче | Да — автоматически | Сессия закрывается сама, журнал цел |
Имена файлов и длинная сессия
Имена я держу короткими, в формате восемь-точка-три: run1.csv, d2512.csv — день и месяц прямо в имени, когда сессий много и счётчик не помогает. Длинные имена на FAT съедают по несколько записей каталога и ломаются в старых кардридерах — механика дескриптора и режимов открытия разобрана в уроке про File и режимы. Шапку пишу в момент открытия сессии: sec,v — файл открывается в таблицу без ручных настроек; полный формат CSV-лога с примером чтения — в уроке про CSV и конфиг.
Длинная сессия добавляет две страховки. Первая — счётчик неудачных open подряд: дошёл до трёх — гасим диод, уходим в покой, печатаем причину в Serial, иначе логгер честно моргает диодом, а строки идут в никуда. Вторая — таймер простоя: если данных нет час, сессию стоит закрыть автоматически, потому что закрытая сессия переживает что угодно — таблица выше это и показывает. Страховка от зависшей карты: если open три раза подряд вернул ложь, логгер уходит в покой с сообщением в Serial вместо вечного крутящегося цикла — вы узнаете о карте раньше, чем она молча съест сутки наблюдений. И помните про распиновку: кнопке нужен любой цифровой пин, а SD-модуль занимает шину SPI целиком — как она делится с другими устройствами, разобрано в уроке про подключение по SPI.
На плате прогоните сценарий из жизни: старт — минута записи — стоп — вынуть карту — посмотреть файл в компьютере. Потом то же, но с reset вместо стопа, и увидите обрезанный хвост: это живой учебник по таблице выше. Дребезг в браузерном симуляторе не воспроизводится — контакты там идеальные, поэтому антидребезг в модели чистый ритуал; на живой плате убирать его нельзя. Файлы после проверок удаляйте SD.remove-ом или держите отдельную карту под опыты, чтобы имена run1, run2 не засоряли рабочий журнал.
Имена проговорите вслух до монтажа: run1-run9 — на картах, которые переформатируются, d2512.csv — день и месяц — там, где сессии живут годами. И подпишите маркером саму карту с номером логгера: через полгода три одинаковые microSD без надписей неразличимы, а загадку «чей где лог» дешевле предупредить, чем разгадывать.
- 1. Что такое фронт кнопки в коде логгера?
- 2. Сколько раз стартует сессия при одном нажатии без антидребезга?
- 3. Почему на сессию заводят новый файл run1, run2, а не общий log.csv?
- 4. Что станет с текущей сессией, если во время записи нажать reset?
- 5. Зачем LED на D12 в скетче старт-стоп?
- Фронт кнопки — переход с HIGH на LOW между чтениями; реакция на уровень даёт пачку срабатываний
- INPUT_PULLUP и окно 200 мс после фронта закрывают и дребезг, и случайное двойное нажатие
- Сессия = новый файл runN.csv с шапкой; close обязателен по стопу, иначе хвост буфера не попадёт на карту
- LED на D12 горит ровно пока файл открыт: погас — можно вынимать карту
- reset во время записи сессию не закрывает: файл обрезан до прошлой фиксации — останавливайте программно
Частые вопросы по теме урока
- Что такое фронт кнопки в коде логгера?
- Скетч сравнивает текущее чтение с предыдущим: было HIGH, стало LOW — значит, нажали. Реакция на уровень даёт несколько срабатываний за одно нажатие. Переход с HIGH на LOW между двумя соседними чтениями
- Сколько раз стартует сессия при одном нажатии без антидребезга?
- Контакты дребезжат 5-20 мс, пин выдаёт серию LOW-HIGH-LOW. Без окна игнорирования каждое переключение выглядит как новое нажатие. Может и пять: дребезг даёт пачку фронтов за 5-20 мс
- Почему на сессию заводят новый файл run1, run2, а не общий log.csv?
- Новый файл — собственная шапка и чёткие границы сессии. Дозапись в один журнал за сутки превращается в кашу, где сессии не отделить. Каждая сессия начинается с шапки и не подмешивается к чужим строкам
- Что станет с текущей сессией, если во время записи нажать reset?
- Скетч останавливается мгновенно, close не вызывается: каталог FAT показывает размер на момент прошлого close, всё после — в никуда. Файл останется на размере прошлой фиксации, хвост буфера потерян
- Зачем LED на D12 в скетче старт-стоп?
- Индикатор сессии — свой диод: горит = файл открыт, погас после close = карту можно вынимать. Огонёк модуля показывает питание и ничего больше. Горит ровно пока дескриптор сессии открыт