Меню для термостата у меня занимало девять строк на русском — и на макетке всё работало, ровно до момента, когда я добавил вывод справки по кнопке. Плата стала перезагружаться при попытке показать текст. Не брак, не питание — кончилось ОЗУ: у Uno её 2 КБ, и строки на русском в UTF-8 весят как маленькая война.
Это урок про три вида памяти в ATmega328P и про то, почему длинные строки надо класть во Flash через PROGMEM, а не в SRAM. Плюс маленький трюк, которым я пользуюсь в каждом среднем проекте — функция подсчёта свободного ОЗУ.
Три памяти платы: Flash, SRAM, EEPROM
У ATmega328P (Uno, Nano) три независимых хранилища, и путать их — самая дорогая ошибка новичка в средних проектах.
Flash, 32 КБ — программа и константы. Не теряется при выключении. Сюда компилятор кладёт ваш скетч; сюда же умеет класть данные через PROGMEM. SRAM, 2 КБ — переменные, стек, куча. Быстрая, но исчезает при выключении питания — и её мало. EEPROM, 1 КБ — медленная энергонезависимая память для настроек, о ней отдельный урок.
Загвоздка в том, что по умолчанию все строки попадают в SRAM, даже если они «константные». Компилятор кладёт их во Flash И копирует в ОЗУ при старте — потому что по-настоящему старый AVR не умеет читать константы из Flash обычным кодом. 20 строк по 30 символов — это 600 байт ОЗУ, почти треть всей памяти, ещё до того, как вы завели первую переменную.
| Память | Объём на Uno | Назначение | Пропадает при выключении |
|---|---|---|---|
| Flash | 32 КБ | код + константы (PROGMEM) | нет |
| SRAM | 2 КБ | переменные, стек, Serial-буферы | да |
| EEPROM | 1 КБ | настройки, счётчики | нет |
PROGMEM: строки живут во Flash
Квалификатор PROGMEM говорит компилятору: держи эти данные только во Flash и не копируй в ОЗУ. Читать такие строки можно лишь специальными функциями — strcpy_P, memcpy_P, F() для Serial-печати. Код выглядит чуть многословнее, зато SRAM остаётся живой.
Макрос F("текст") — самый частый случай: обёртка, которая заставляет Serial.print читать строку прямо из Flash. Если в вашем скетче есть десять Serial.println("Длинный текст..."), простая замена на Serial.println(F("...")) экономит сотни байт ОЗУ и стоит ровно три буквы.
#include <avr/pgmspace.h> // Меню живёт во Flash — ОЗУ не тратитсяconst char menu1[] PROGMEM = "1 - Temperature";const char menu2[] PROGMEM = "2 - Humidity";const char menu3[] PROGMEM = "3 - Settings";const char* const menu[] PROGMEM = { menu1, menu2, menu3 }; void setup() { Serial.begin(9600); for (int i = 0; i < 3; i++) { char buf[32]; strcpy_P(buf, menu[i]); // чтение из Flash Serial.println(buf); } Serial.println(F("Ready")); // F() — строка прямо из Flash} void loop() {}freeRam: датчик бедности ОЗУ
Классическая функция для настоящей платы — измерение свободной ОЗУ по расстоянию между указателем стека и краем кучи. Не наукоёмко, работает везде и спасала мне отладку не раз: печатали freeRam раз в секунду — и видели, как память утекает при каждой итерации.
Нормы такие: у спокойного проекта до 70% занятой SRAM всё стабильно. Как только freeRam опускается ниже ~300 байт, начинаются странности: Serial глючит, строки печатаются кракозябрами, плата перезагружается «сама» — это стек наехал на данные.
int freeRam() { extern int __heap_start, *__brkval; int v; return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval);} void setup() { Serial.begin(9600);} void loop() { Serial.print(F("freeRam: ")); Serial.println(freeRam()); delay(2000);}String() — главный пожиратель ОЗУ
Класс String удобен, но каждый + создаёт новый кусок памяти, а старый ждёт сборщика фрагментов, которого на AVR фактически нет. Лог, собранный конкатенацией String раз в минуту, за несколько дней фрагментирует ОЗУ в решето — и плата зависает без видимых причин. У меня такой «случайный» зависающий логгер на даче жил две недели ровно.
Правило, которое я вывел для себя: String — только для прототипов и десктопа; в долгоживущем проекте — char-массивы фиксированного размера и функции из avr/pgmspace. Если без String никак — хотя бы избегайте конкатенаций в loop(), переиспользуя один буфер.
- Длинные константные тексты —
F()для печати иPROGMEMдля массивов строк - Переменные —
char buf[N]вместо String, где возможно - Каждый новый String в loop() — потенциальная утечка фрагментации
- freeRam() раз в секунду в debug-сборке — ранняя сигнализация
- Таблица памяти в datasheet: Uno 2 КБ SRAM, Nano — столько же, Mega — 8 КБ
Эксперименты
- В sim-блоке добавьте пятую строку меню и посмотрите, как пересчитывается бюджет.
- Поменяйте строки на русские длинные описания (40+ символов) — почувствуйте, как быстро съедается ОЗУ.
- На настоящей плате: скомпилийте скетч с 5 строками и с 50 строками и сравните «Global variables» в логе компилятора — разница и есть цена отсутствия PROGMEM.
- Добавьте freeRam() в свой прошлый проект с датчиками и узнайте, сколько у него запаса.
- Uno: Flash 32 КБ (код+константы), SRAM 2 КБ (жизнь), EEPROM 1 КБ (настройки)
- Строки по умолчанию копируются в SRAM — длинные тексты душат память
- F("...") и PROGMEM держат строки во Flash; читать — через *_P-функции
- freeRam() — ваш манометр; ниже ~300 байт начинается странное
- String в долгоживущих проектах заменять на char-буферы
- 1. Сколько SRAM у Arduino Uno (ATmega328P)?
- 2. Что делает F("текст") в Serial.println?
- 3. Плата перезагружается при выводе длинного меню. Первая версия?
- 4. Почему String опасен в многодневных проектах?
Память — второй навык после неблокирующего кода, отличающий уверенного ардуинщика от новичка. Дальше по разделу: таймеры и watchdog, сон и батарейные проекты. А если ваш проект уже упирается в 2 КБ — посмотрите в сторону ESP32: 520 КБ ОЗУ меняют правила игры, о нём целый раздел.
Частые вопросы по теме урока
- Сколько SRAM у Arduino Uno (ATmega328P)?
- 32 КБ — это Flash под код, 1 КБ — EEPROM, 8 КБ — SRAM у Mega. У Uno ОЗУ ровно 2 КБ. 2 КБ
- Что делает F("текст") в Serial.println?
- F() — макрос PROGMEM-строки: текст лежит во Flash и печатается оттуда, SRAM не тратится. Читает строку из Flash, не копируя её в SRAM
- Плата перезагружается при выводе длинного меню. Первая версия?
- Классический симптом исчерпания ОЗУ: строки ушли в SRAM, стек столкнулся с данными — плата сбрасывается. Лечение: F()/PROGMEM и freeRam(). Кончилась SRAM — строки заняли ОЗУ, стек наехал на данные
- Почему String опасен в многодневных проектах?
- Каждый + выделяет новый блок, старые дыры не переиспользуются — ОЗУ фрагментируется, через дни/недели плате «нечем дышать». Фрагментация ОЗУ от постоянных конкатенаций