Руководство по памяти Arduino

Узнайте о встроенных блоках памяти плат Arduino® в этой статье.

Авторы: Arduino, José Bagur, Taddy Chung. Последнее обновление: 29.12.2023

Микроконтроллер (также известный как MCU) — это интегральная схема (ИС), как правило используемая для выполнения конкретных приложений или задач. Обычно такая ИС собирает информацию или данные из окружающей среды, обрабатывает их и генерирует конкретные выходные сигналы в соответствии с полученными данными. Сегодня микроконтроллеры повсюду: они являются неотъемлемой частью современных встроенных систем, которые можно найти практически везде в нашем мире — от умных часов до электромобилей; они даже присутствуют на поверхности Марса прямо сейчас.

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

Что такое память?

Блоки памяти являются важнейшими компонентами современных встроенных систем, особенно основанных на микроконтроллерах. Блоки памяти — это полупроводниковые устройства, которые хранят и извлекают информацию или данные; центральный процессор (CPU) микроконтроллера использует и обрабатывает данные, хранящиеся в блоках памяти, для выполнения конкретных задач.

Как показано на изображении ниже, блоки памяти в микроконтроллерах обычно описываются как массивы. Массивы памяти разделены на ячейки, которые могут хранить данные и доступны через уникальный идентификатор, представляющий их адрес или позицию относительно массива памяти. Информация в ячейках памяти хранится в виде двоичных цифр (битов), обычно организованных в байты (8 бит); она также может быть впоследствии извлечена MCU или другими компонентами микроконтроллерной системы.

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

Архитектуры памяти: введение

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

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

Архитектура фон Неймана

Архитектура фон Неймана, названная в честь математика, физика и учёного в области вычислительной техники Джона фон Неймана, была впервые представлена в середине 40-х годов. Она также известна как Принстонская архитектура. Эта архитектура хранит программные данные и инструкции в одном блоке памяти.

Архитектура фон Неймана

Архитектура фон Неймана.

Оба компонента доступны для CPU через одну коммуникационную шину, как показано ниже. Архитектура фон Неймана является фундаментальной, поскольку практически все проекты цифровых компьютеров основаны на этой архитектуре.

Гарвардская архитектура

Гарвардская архитектура, названная в честь релейного компьютера Harvard Mark I, была впервые представлена в середине 40-х годов. Главная особенность этой архитектуры заключается в том, что она использует два отдельных блока памяти: один для хранения программных инструкций и один для хранения программных данных. Оба блока памяти в Гарвардской архитектуре доступны для CPU через разные коммуникационные шины.

Гарвардская архитектура

Гарвардская архитектура.

Современные архитектуры: гибридные решения

Современные вычислительные системы используют гибридные архитектурные модели, которые максимизируют производительность, используя лучшее из обоих миров — моделей фон Неймана и Гарварда.

Микроконтроллеры обычно используются во встроенных приложениях. Они должны надёжно и эффективно выполнять определённые задачи при ограниченных ресурсах; именно поэтому Гарвардская архитектурная модель в основном используется в микроконтроллерах: микроконтроллеры имеют небольшую память программ и данных, к которой необходим одновременный доступ. Однако Гарвардская архитектура используется не всегда; некоторые семейства микроконтроллеров используют гибридные архитектурные модели или архитектуру фон Неймана.

Архитектуры плат Arduino®

Платы Arduino® в основном основаны на двух семействах микроконтроллеров: AVR® и ARM®. Хотя микроконтроллеры семейства AVR® основаны на Гарвардской архитектуре, микроконтроллеры семейства ARM® могут основываться как на архитектуре фон Неймана, так и на Гарвардской. Следующая таблица обобщает архитектуры микроконтроллеров плат Arduino:

Архитектуры микроконтроллеров плат Arduino

Плата

Микроконтроллер

Семейство

Архитектура

UNO Mini

ATmega328P

AVR

Harvard

UNO Rev3

ATmega328P

AVR

Harvard

UNO WiFi Rev2

ATmega4809

AVR

Harvard

UNO Rev3 SMD

ATmega328P

AVR

Harvard

Leonardo

ATmega32u4

AVR

Harvard

Mega 2560 Rev3

ATmega2560

AVR

Harvard

Micro

ATmega32u4

AVR

Harvard

Zero

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

Portenta H7

STM32H747

Arm® Cortex®-M4/M7

Harvard

Nicla Sense ME

nRF52832

Arm® Cortex®-M4

Harvard

Nano RP2040 Connect

RP2040

Arm® Cortex®-M0+

Von Neumann

MKR FOX 1200

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

MKR NB 1500

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

MKR Vidor 4000

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

MKR WiFi 1010

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

MKR Zero

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

MKR1000 WIFI

ATSAMW25H18

Arm® Cortex®-M0+

Von Neumann

MKR WAN 1300

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

MKR WAN 1310

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

Nano

ATmega328P

AVR

Harvard

Nano Every

ATmega4809

AVR

Harvard

Nano 33 IoT

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

Nano 33 BLE

nRF52840

Arm® Cortex®-M4

Harvard

Nano 33 BLE Sense

nRF52840

Arm® Cortex®-M4

Harvard

Типы памяти

Все различные блоки памяти внутри микроконтроллера можно разделить на два основных типа: RAM и ROM. RAM (от Random-Access Memory, память с произвольным доступом) в микроконтроллерных системах является энергозависимой памятью, используемой для хранения временных данных, например переменных встроенного программного обеспечения системы. ROM (от Read-Only Memory, постоянное запоминающее устройство) в микроконтроллерных системах является энергонезависимой памятью, используемой для хранения постоянных данных, например встроенного программного обеспечения системы.

RAM и ROM в микроконтроллерных системах организованы в три основные категории:

  • Flash

  • RAM

  • EEPROM

Flash-память

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

Blink.ino

: когда мы компилируем этот скетч, мы создаём бинарный файл, который затем сохраняется во Flash-памяти платы Arduino. Скетч выполняется при включении платы.

RAM

RAM в микроконтроллерных системах — это место, где хранятся временные данные или данные времени выполнения; например, переменные, созданные функциями программы. RAM в микроконтроллерах обычно представляет собой SRAM — тип RAM, использующий триггер (flip-flop) для хранения одного бита данных. Существует также другой тип RAM, встречающийся в микроконтроллерах: DRAM.

EEPROM

В микроконтроллерных системах EEPROM (Erasable Programmable Read-Only Memory — электрически стираемое программируемое постоянное запоминающее устройство) также является частью ROM; фактически, Flash-память является разновидностью EEPROM. Основное отличие между Flash-памятью и EEPROM заключается в способе управления ими: EEPROM может управляться на уровне байта (запись или стирание), тогда как Flash-памятью управляют на уровне блока.

Распределение памяти в платах Arduino®

Как уже говорилось, платы Arduino® в основном основаны на двух семействах микроконтроллеров — AVR® и ARM®; важно знать, что распределение памяти в обеих архитектурах различается. В архитектуре Гарварда на базе AVR память организована, как показано на изображении ниже:

Карта памяти AVR

Карта памяти AVR.

Важно упомянуть об организации SRAM в платах Arduino на базе AVR — она разделена на различные секции:

  • Text

  • Data

  • BSS

  • Stack

  • Heap

Секция text содержит инструкции, загружаемые во Flash-память; секция data содержит переменные, инициализированные в скетче; секция BSS содержит неинициализированные данные; секция stack хранит данные функций и прерываний, а секция heap хранит переменные, создаваемые во время выполнения.

В гибридных архитектурах ARM реализована так называемая карта памяти с различными конфигурациями адресного пространства — 32-битной, 36-битной и 40-битной — в зависимости от требований адресного пространства системы на кристалле (SoC) с дополнительной DRAM. Карта памяти обеспечивает интерфейс с архитектурой SoC, при этом сохраняя большинство элементов управления системой на уровне кода высокого уровня. Инструкции доступа к памяти могут использоваться в коде высокого уровня для управления модулями прерываний и встроенными периферийными устройствами. Всё это контролируется блоком управления памятью (MMU).

Ресурс памяти управляется MMU. Основная роль MMU — позволить процессору выполнять несколько задач независимо в их собственном виртуальном адресном пространстве памяти; MMU затем использует таблицы трансляции для установления связи между виртуальными и физическими адресами памяти. Виртуальный адрес управляется программно с помощью инструкций памяти, а физический адрес — это система памяти, которая управляется в зависимости от входных данных таблицы трансляции, предоставляемых виртуальным адресом.

Пример организации памяти в микроконтроллерах на базе ARM — виртуальной и физической — показан на изображении ниже:

Организация памяти в микроконтроллерах на базе ARM

Организация памяти в микроконтроллерах на базе ARM.

Память микроконтроллера на базе ARM организована по следующим секциям в рамках упомянутых ранее типов адресов:

  • Виртуальный адрес:

    • Kernel code and data (код и данные ядра)

    • Application code and data (код и данные приложения)

  • Физический адрес:

    • ROM

    • RAM

    • Flash

    • Peripherals (периферийные устройства)

Следующая таблица обобщает распределение памяти конкретных плат Arduino®:

Распределение памяти плат Arduino

Плата

Микроконтроллер

Семейство

Архитектура

Flash

SRAM

EEPROM

UNO Mini

ATmega328P

AVR

Harvard

32kB

2kB

1kB

UNO Rev3

ATmega328P

AVR

Harvard

32kB

2kB

1kB

UNO WiFi Rev2

ATmega4809

AVR

Harvard

48kB

6kB

256B

UNO Rev3 SMD

ATmega328P

AVR

Harvard

32kB

2kB

1kB

Leonardo

ATmega32u4

AVR

Harvard

32kB

2.5kB

1kB

Mega 2560 Rev3

ATmega2560

AVR

Harvard

256kB

8kB

4kB

Micro

ATmega32u4

AVR

Harvard

32kB

2.5kB

1kB

Zero

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

Portenta H7 (базовая конфигурация)

STM32H747

Arm® Cortex®-M4/M7

Harvard

16MB

8MB

Nicla Sense ME

nRF52832

Arm® Cortex®-M4

Harvard

512kB

64kB

Nano RP2040 Connect

RP2040

Arm® Cortex®-M0+

Von Neumann

264kB

MKR FOX 1200

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR NB 1500

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR Vidor 4000

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR WiFi 1010

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR Zero

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR1000 WIFI

ATSAMW25H18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR WAN 1300

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

MKR WAN 1310

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

Nano

ATmega328P

AVR

Harvard

32kB

2kB

1kB

Nano Every

ATmega4809

AVR

Harvard

48kB

6kB

256B

Nano 33 IoT

ATSAMD21G18

Arm® Cortex®-M0+

Von Neumann

256kB

32kB

Nano 33 BLE

nRF52840

Arm® Cortex®-M4

Harvard

1MB

256kB

Nano 33 BLE Sense

nRF52840

Arm® Cortex®-M4

Harvard

1MB

256kB

Примечание

Объём SDRAM и Flash-памяти для профессиональных плат (Pro) в значительной степени настраиваем. Для получения дополнительной информации посетите сайт Arduino Pro.

Измерение использования памяти в платах Arduino®

Статистика использования памяти помогает понять особенности управления ресурсами, на которое влияет спроектированная структура кода. Нагрузка на память — это одна из характеристик, которая даст вам представление о том, насколько эффективно спроектирован код. Это критически важный элемент разработки, поскольку ресурсы внутри микроконтроллерной системы ограничены; программное обеспечение всегда должно работать, не достигая максимальной нагрузочной способности, чтобы избежать проблем. Нагрузку на память можно наблюдать либо как доступную RAM для конкретных задач, либо как оставшуюся ёмкость Flash-памяти для необходимого запаса.

Важно

Чтобы избежать проблем во время выполнения, микроконтроллерные системы всегда должны работать, не достигая максимальной ёмкости памяти.

Давайте подробнее поговорим об измерении использования памяти в платах Arduino®.

Измерение Flash-памяти

Flash-память на платах Arduino® можно измерить с помощью Arduino IDE. Как уже говорилось, Flash-память — это место, где хранится код приложения; Arduino IDE сообщает об использовании Flash-памяти через консоль вывода компилятора, чтобы разработчики знали, какой объём Flash-памяти используется.

Например, вывод консоли компилятора IDE для платы Arduino® на базе AVR — Nano — показан на изображении ниже:

Измерение Flash-памяти в плате Arduino® на базе AVR

Измерение Flash-памяти в плате Arduino® на базе AVR.

Вывод консоли компилятора IDE для платы Arduino® на базе ARM — MKR WAN 1310 — показан на изображении ниже:

Измерение Flash-памяти в плате Arduino® на базе ARM

Измерение Flash-памяти в плате Arduino® на базе ARM.

Вывод консоли компилятора IDE для другой платы Arduino® на базе ARM — Portenta H7 — показан на изображении ниже:

Измерение Flash-памяти в плате Arduino® на базе ARM (Portenta H7)

Измерение Flash-памяти в плате Arduino® на базе ARM (Portenta H7).

Обратите внимание, что вывод компилятора меняется в зависимости от того, является ли плата AVR-based или ARM-based.

Измерение SRAM

Иногда возникают ситуации, когда даже при успешной компиляции и загрузке кода IDE в плату происходят внезапные остановки. Эти проблемы, вероятно, связаны с чрезмерным потреблением памяти или недостаточным объёмом памяти для выделения. Необходимо понять, в каком секторе кода потребление памяти выходит за пределы доступных ресурсов, чтобы решить эту проблему. Следующий пример кода можно использовать для измерения использования SRAM в платах Arduino® на базе AVR:

void display_freeram() {
  Serial.print(F("- SRAM left: "));
  Serial.println(freeRam());
}

int freeRam() {
  extern int __heap_start,*__brkval;
  int v;
  return (int)&v - (__brkval == 0
    ? (int)&__heap_start : (int) __brkval);
}

Помните, что секция heap — это место, где хранятся переменные, создаваемые во время выполнения. В коде:

  • __heap_start — начало секции heap.

  • __brkval — последний использованный указатель адреса памяти в секции heap.

Следующий пример кода можно использовать для измерения использования SRAM в платах Arduino® на базе ARM:

extern "C" char* sbrk(int incr);

void display_freeram(){
  Serial.print(F("- SRAM left: "));
  Serial.println(freeRam());
}

int freeRam() {
  char top;
  return &top - reinterpret_cast<char*>(sbrk(0));
}

Приведённый выше код взят из библиотеки Arduino-MemoryFree от Michael P. Flaga.

Измерение EEPROM

Управление памятью EEPROM можно легко осуществить с помощью встроенных библиотек, уже установленных в Arduino IDE. Библиотека EEPROM может использоваться для чтения, записи и стирания памяти EEPROM. Следующий код показывает, как байт информации можно сохранить в памяти EEPROM и затем считать с помощью функций write и read:

#include <EEPROM.h>

void setup() {
}

void loop() {
  // Запись данных по конкретному адресу памяти EEPROM
  EEPROM.write(address, value);

  // Чтение данных по конкретному адресу памяти EEPROM
  EEPROM.read(address);
}

Также можно очистить всю память EEPROM, установив её в 0, как показано в коде ниже:

#include <EEPROM.h>

void setup() {
}

void loop() {
  for (int i = 0 ; i < EEPROM.length() ; i++) {
    // Очистка памяти EEPROM
    EEPROM.write(i, 0);
  }
}

Совет

Для получения дополнительной информации об управлении памятью EEPROM вы можете обратиться к руководству по EEPROM.

Оптимизация использования памяти в системах на базе Arduino

Знание того, как код использует ресурсы памяти системы, — это лишь первая рекомендуемая задача в процессе разработки; совершенно другая задача — оптимизация использования памяти. Как может подразумевать термин «разработка», требования могут изменяться или корректироваться в зависимости от внешних факторов, таких как снижение ёмкости устройства из-за недоступности компонентов. Таким образом, архитектура кода может потребовать оптимизации для работы на сниженных ограниченных ресурсах памяти.

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

Рассмотрим некоторые методы оптимизации использования памяти.

Оптимизация Flash-памяти

Оптимизация Flash-памяти — это, пожалуй, наиболее очевидный потенциальный источник оптимизации. Flash-память — это место, где ёмкость, используемая скомпилированным кодом, может быть значительно сокращена при учёте некоторых деталей.

Удаление неиспользуемых ресурсов

Удаление новых источников включает неиспользуемые библиотеки и остатки кода. Остатки кода могут состоять из больше не используемых функций и «плавающих» переменных, которые занимают ненужное место в памяти. Это значительно улучшит размер скомпилированного кода и сделает процесс компиляции более понятным.

Модульные задачи

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

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

Оптимизация SRAM

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

Идеальный способ использования команды вывода данных — использование обёртки строк F() вокруг литералов. Смотрите пример ниже:

String Wrapper (обёртка строк)

Инструкции Serial.print() или Serial.println() используют пространство SRAM, что может быть удобно, но нежелательно. Идеальный способ использования инструкции Serial.print() или Serial.println() — с применением обёртки строк F() вокруг литералов. Например:

Serial.println(F("Something"));

Оборачивание строки Something с помощью обёртки F() перемещает строки только во Flash-память, не используя при этом пространство SRAM. Использование обёртки F() можно рассматривать как выгрузку таких данных во Flash-память вместо SRAM. Flash-память значительно объёмнее SRAM, поэтому лучше использовать пространство Flash-памяти, чем SRAM, которая будет использовать секцию heap. Это не означает, что пространство памяти всегда будет доступно, поскольку Flash-память также имеет ограниченный объём. Не рекомендуется засорять код инструкциями Serial.print() или Serial.println(), используйте их там, где они наиболее важны в коде.

PROGMEM

Не только строки занимают пространство SRAM — глобальные переменные также занимают немало пространства SRAM. Глобальные и статические переменные передаются в пространство SRAM и сдвигают секцию памяти heap в сторону stack. Пространство, занятое этими переменными, переданными в SRAM, сохраняется на своём месте и не изменяется; это означает, что чем больше таких переменных создаётся, тем больше места они занимают, и, следовательно, система испытывает проблемы из-за неудовлетворительного управления памятью.

PROGMEM, что расшифровывается как Program Memory (Программная память), можно использовать для хранения данных переменных в пространстве Flash-памяти, точно так же как описанная ранее обёртка F(), но использование PROGMEM имеет один недостаток: скорость чтения данных. Использование RAM обеспечит значительно более высокую скорость чтения данных, но PROGMEM, поскольку использует Flash-память, будет медленнее RAM при том же объёме данных. Таким образом, важно проектировать код, зная, какие переменные являются критически важными, а какие нет или имеют более низкий приоритет.

Использование PROGMEM в плате Arduino® на базе AVR показано в примере кода ниже:

#include <avr/pgmspace.h>

// Базовая структура PROGMEM
const PROGMEM DataType Variable_Name[] = {var0, var1, var2 ...};

// Хранение беззнакового 16-битного целого числа
const PROGMEM uint16_t NumSet[] = {0, 1, 1, 2, 3, 5, 8 ...};

// Хранение символа в PROGMEM
const char greetMessage[] PROGMEM = {"Something"};

Совет

Подробнее о PROGMEM можно прочитать в справочнике языка Arduino.

Для плат Arduino® на базе ARM для реализации аналогичного решения необходимо использовать static const для переменных.

static const int Variable = Data;

Использование различается на разных уровнях, которые резюмируются следующим образом:

  • Уровень пространства имён (Namespace Level)

    • На уровне пространства имён мы указываем на переменные, и различие состоит в том, объявлено ли static или нет. Если объявлено, это означает явное статическое объявление переменной; в противном случае — неявное статическое объявление.

  • Уровень функции (Function Level)

    • Если объявлено внутри static, любые применимые данные будут управляться между вызовами функций.

  • Уровень класса (Class Level)

    • На уровне класса объявление static означает, что любые применимые данные будут общими между экземплярами.

Нединамическое выделение памяти

Динамическое выделение памяти обычно является подходящим методом, если размер RAM системы достаточно велик; однако для микроконтроллерных систем, таких как встроенные системы, не рекомендуется подсчитывать каждый байт RAM.

Динамическое выделение памяти вызывает фрагментацию heap. При фрагментации heap многие затронутые ею области RAM не могут быть повторно использованы, оставляя мёртвые байты, которые могли бы быть использованы для других задач. Кроме того, когда динамическое выделение памяти освобождает занятое пространство, это не обязательно уменьшает размер heap. Поэтому, чтобы по возможности избежать фрагментации heap или RAM, можно следовать следующим правилам:

  • Приоритет стека перед heap:

    • Память стека (stack) не подвержена фрагментации и может быть полностью освобождена при возврате из функции. Heap, напротив, может не освобождать пространство, даже если ему было дано такое указание. Использование локальных переменных поможет в этом; старайтесь не использовать динамическое выделение памяти, которое состоит из различных вызовов: malloc, calloc, realloc.

  • Сокращение глобальных и статических данных (по возможности):

    • Пока код выполняется, область памяти, занятая этими данными, не будет освобождена. Данные не будут изменены, поскольку константные данные занимают ценное пространство.

  • Используйте короткие строки/литералы:

    • Рекомендуется сохранять строки/литералы как можно короче. Один символ (char) занимает один байт RAM, поэтому чем короче, тем лучше использование пространства памяти. Это не означает, что можно хранить их короткими и использовать в нескольких разных местах кода. Используйте их там, где это необходимо, и сохраняйте их как можно короче, чтобы освободить RAM для других задач.

    • Массивы также рекомендуется делать минимального размера. Если потребуется изменить размер массива, вы всегда можете переустановить его размер в коде. Это может быть утомительным, а также неэффективным методом — жёстко задавать размеры массивов. Однако если код использует небольшие размеры массивов и менее трёх массивов, ручного изменения размера может быть достаточно, если требования известны. Интеллектуальный способ — создать массив с изменяемым размером, но с ограничением. Задачи будут использовать массив, не выходя за границу размера. Таким образом, это подходит для обширного кода. При этом предел размера массива должен быть проанализирован и сохранён как можно меньшим.

Функция Reserve

Для задач в коде, работающих со строками, которые меняют размер в зависимости от результата операции, функция reserve() — то, что нужно. Эта функция поможет зарезервировать буферное пространство и предварительно выделить его для строковой переменной, изменяя её размер и избегая фрагментации памяти. Строковая переменная, меняющая свой размер, может быть результатом, например, переменной типа int, обёрнутой для использования в качестве строки.

Следующий код показывает, как использовать инструкцию reserve():

// String_Variable — переменная типа String
// Alloc_Size — объём памяти для предварительного выделения в байтах, тип unsigned int
String_Variable.reserve(Alloc_Size);

Совет

Для получения дополнительной информации о функции reserve() посетите справочник языка Arduino.

Управление размером буфера

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

Рассмотрим пример: последовательная связь в Arduino. Последовательная связь — это регулярно используемый сервис в системах на базе Arduino; последовательная связь в Arduino работает с использованием предустановленной библиотеки Serial (внешние библиотеки также могут эмулировать последовательную связь с помощью программного обеспечения). Среди фоновых сервисов последовательная связь определяет необходимый пул памяти как буфер заданного размера. Если высокоскоростная последовательная связь не входит в требования, размер последовательного буфера можно переопределить для экономии памяти. Это можно легко сделать, изменив следующую строку кода в файле HardwareSerial.h, который можно найти в папке установки Arduino IDE:

#define SERIAL_TX_BUFFER_SIZE 64

#define SERIAL_RX_BUFFER_SIZE 64

Примечание

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

Корректное использование типов данных

Реализация подходящих типов данных ведёт к хорошей общей архитектуре кода. Разработчикам может быть желательно использовать простейший или наиболее доступный тип данных для работы с данными в коде. Однако важно учитывать объём памяти, занимаемый при использовании определённых типов данных.

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

Следующая таблица показывает базовые типы данных значений в Arduino:

Базовые типы данных в Arduino

Тип

Длина (байт)

Диапазон значений

boolean

1

Ограничен логическими true и false

char

1

-128 до 127

unsigned char

1

0 до 255

byte

1

0 до 255

int

2

-32 768 до 32 767

unsigned int

2

0 до 65 535

word

2

0 до 65 535

long

4

-2 147 483 648 до 2 147 483 647

unsigned long

4

0 до 4 294 967 295

float

4

-3.4028235E+38 до 3.4028235E+38

double

4

-3.4028235E+38 до 3.4028235E+38

Оптимизация EEPROM

Оптимизация памяти EEPROM, как правило, не требуется; данные, которые должны использоваться пространством EEPROM, не нуждаются во Flash-памяти в качестве источника хранения. Кроме того, не является хорошей практикой выгружать данные SRAM в EEPROM. Данные SRAM размещаются с учётом энергозависимости, поэтому выгрузка в пространство EEPROM, которое является энергонезависимой памятью, означает, что выгруженные данные будут «выгравированы» в пространстве EEPROM.

При работе с EEPROM крайне важно знать, что операция write ограничена. Операция read для EEPROM неограничена; однако операция write конечна и обычно ограничена 100 000 циклами. Таким образом, важно сохранять только необходимые параметры для датчиков или модулей для работы с преимущественно неизменными данными. Кроме того, следует избегать реализации операций write в циклах, чтобы не допустить постоянных операций write; эти операции должны быть минимизированы во время работы системы.

Эмуляция EEPROM с помощью Flash-памяти

Поскольку EEPROM ограничена циклами операции записи, это также относится и к Flash-памяти. Обе они подвержены потере данных по истечении определённого производителем жизненного цикла. EEPROM основана на памяти типа NOR, тогда как Flash-память имеет тип NAND, что делает EEPROM более дорогостоящей, чем Flash-память. EEPROM работает с данными побайтово, тогда как Flash-память обращается к данным блоками.

Иногда разработчику приходится использовать EEPROM в качестве альтернативного хранилища для операций задач, но мы знаем, что это будет нецелесообразным кодированием из-за её размера и свойств. Для решения этой проблемы можно использовать Flash-память для эмуляции EEPROM. Благодаря библиотеке FlashStorage, созданной Кристианом Мальи (Christian Maglie), можно эмулировать EEPROM с помощью Flash-памяти.

Предупреждение

Библиотека FlashStorage поможет вам использовать Flash-память для эмуляции EEPROM, но, конечно, помните о свойствах EEPROM при использовании библиотеки. Как и EEPROM, Flash-память также ограничена в циклах write. С двумя новыми дополнительными функциями, указанными в библиотеке, EEPROM.commit() не следует вызывать внутри функции loop, иначе это исчерпает циклы операции write Flash-памяти, что приведёт к потере способности хранить данные.

Дополнительные материалы и ресурсы

Архитектуры памяти в микроконтроллерных системах — весьма обширная тема; если вы хотите узнать больше по этой теме, ознакомьтесь со следующими ссылками:

  • Документация по 8-битному ядру AVR® на сайте Microchip® Developer. Здесь вы найдёте подробную информацию о 8-битном центральном процессоре (CPU) AVR®.

  • Сайт документации по архитектуре ARM. Здесь вы найдёте подробную информацию о различных процессорах ARM. Обратите внимание на технические справочные руководства Cortex-M0+ и Cortex-M4.

Список литературы

[1] S. F. Barrett and D. J. Pack, Microchip AVR® Microcontroller Primer: Programming and Interfacing, Third Edition (Synthesis Lectures on Digital Circuits and Systems), Morgan & Claypool, 2019.

[2] J. Y. Yiu, The Definitive Guide to Arm® Cortex®-M0 and Cortex-M0+ Processors, Second ed., Newnes, 2015.

[3] J. Yiu, The Definitive Guide to ARM® Cortex®-M3 and Cortex®-M4 Processors, Third ed., Newnes, 2014.