Книга: Программируем Arduino. Профессиональная работа со скетчами.

6. Память

6. Память

Объем памяти в большинстве компьютеров исчисляется гигабайтами, но в Arduino Uno ее всего 2 Кбайт. То есть более чем в миллион раз меньше, чем в обычном компьютере. Однако ограниченный объем памяти удивительным образом способствует концентрации мысли в процессе программирования. Здесь нет места для расточительства, которым страдает большинство компьютеров.

Писать эффективный код, конечно, важно, но необязательно делать это за счет усложнения чтения и сопровождения. Даже при таких ограниченных ресурсах, как в Arduino, большинство скетчей оказываются далеки от использования всего объема оперативного запоминающего устройства (ОЗУ). Беспокоиться о нехватке памяти приходится, только когда создается действительно очень сложный скетч, использующий массу данных.

Память в Arduino

Сравнивать объем памяти в Arduino и в обычных компьютерах не совсем корректно, так как в них память ОЗУ используется для разных целей. На рис. 6.1 показано, как используется память в компьютере, когда запускается программа.

Когда компьютер запускает программу, он сначала копирует ее целиком с жесткого диска в ОЗУ, а затем запускает эту копию. Переменные в программе занимают дополнительный объем ОЗУ. Для сравнения на рис. 6.2 показано, как используется память в Arduino, когда запускается программа. Сама программа действует, находясь во флеш-памяти. Она не копируется в ОЗУ.


Рис. 6.1. Как используется память в компьютере


Рис. 6.2. Как используется память в Arduino

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

При приближении к границам возможностей Arduino придется позаботиться о рациональном использовании ОЗУ и, в меньшей степени, о размере программы внутри флеш-памяти. Так как в Arduino Uno имеется 32 Кбайт флеш-памяти, этот предел достигается нечасто.

Уменьшение используемого объема ОЗУ

Как вы уже видели, чтобы уменьшить используемый объем ОЗУ, следует уменьшить объем памяти, занимаемой переменными.

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

Самым широко используемым типом данных в Arduino C, бесспорно, является тип int. Каждая переменная типа int занимает 2 байта, но часто такие переменные используются для представления чисел из намного более узкого диапазона, чем –32 768…+32 767, и нередко типа byte с его диапазоном 0…255 для них оказывается вполне достаточно. Большинство встроенных методов, принимающих аргументы типа int, с таким же успехом могут принимать однобайтовые аргументы.

Типичным примером могут служить переменные с номерами контактов. Они часто объявляются с типом int, как показано в следующем примере:

// sketch_06_01_int

int ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};

void setup()

{

  for (int i = 0; i < 12; i++)

  {

    pinMode(ledPins[i], OUTPUT);

    digitalWrite(ledPins[i], HIGH);

  }

}

void loop()

{

}

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

По-настоящему отличный способ экономии ОЗУ — объявление неизменяемых переменных константами. Для этого достаточно добавить слово const в начало объявления переменной. Зная, что значение никогда не изменится, компилятор сможет подставлять значение переменной в местах обращения к ней и тем самым экономить ОЗУ. Например, массив из предыдущего примера можно объявить так:

const byte ledPins[] = {2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13};

Не злоупотребляйте рекурсией

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

Когда происходит вызов функции, в области памяти, называемой стеком, выделяется фрагмент. Представьте подпружиненный дозатор для леденцов, например Pez™, но позволяющий вталкивать леденцы и выталкивать их сверху (рис. 6.3). Под термином «вталкивать» понимается добавление чего-то на стек, а под термином «выталкивать» — извлечение со стека.

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

Первоначально стек пуст, но, когда скетч вызовет функцию (пусть это будет функция А), на стеке выделяется пространство под кадр. Если функция А вызовет другую функцию (функцию Б), на вершину стека будет добавлен еще один кадр и теперь в стеке будет храниться две записи. Когда функция Б завершится, ее кадр будет вытолкнут со стека. Затем, когда завершится функция А, ее кадр также будет вытолкнут со стека. Поскольку локальные переменные функции находятся в кадре стека, они не сохраняются между вызовами функции.


Рис. 6.3. Стек

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

Например, математическая функция вычисления факториала находит произведение всех целых чисел, предшествующих указанному числу, включая его. Факториал числа 6 равен 6 х 5 х 4 х 3 х 2 х 1 = 720.

Рекурсивный алгоритм вычисления факториала определяется так.

• Если n = 0, факториал числа n равен 1.

• Иначе факториал числа n равен произведению n на факториал (n – 1).

Далее показана реализация этого алгоритма на языке Arduino C:

long factorial(long n)

{

  if (n == 0)

  {

    return 1;

  }

  else

  {

    return n* factorial(n — 1);

  }

}

Полную версию кода, который вычисляет факториалы чисел и выводит результаты, вы найдете в скетче sketch_06_02_factorial. Люди с математическим складом ума находят такую реализацию весьма искусной. Но обратите внимание на то, что глубина стека в вызове такой функции равна числу, факториал которого требуется найти. Совсем нетрудно догадаться, как реализовать нерекурсивную версию функции factorial:

long factorial(long n)

{

  long result = 1;

  while (n > 0)

  {

    result = result * n;

    n--;

  }

  return result;

}

С точки зрения удобочитаемости этот код, возможно, выглядит понятнее, а кроме того, он расходует меньше памяти и работает быстрее. Вообще старайтесь избегать рекурсии или хотя бы ограничивайтесь высокоэффективными рекурсивными алгоритмами, такими как Quicksort (http://ru.wikipedia.org/wiki/Быстрая_сортировка), который очень эффективно упорядочивает массив чисел.

Сохраняйте строковые константы во флеш-памяти

По умолчанию строковые константы, как в следующем примере, сохраняются в ОЗУ и во флеш-памяти — один экземпляр хранится в коде программы, а второй экземпляр создается в ОЗУ во время выполнения скетча:

Serial.println("Program Started");

Но если использовать код, как показано далее, строковая константа будет храниться только во флеш-памяти:

Serial.println(F("Program Started"));

В разделе «Использование флеш-памяти» далее в этой главе вы познакомитесь с другими способами использования флеш-памяти.

Типичные заблуждения

Многие заблуждаются, полагая, что использование более коротких имен переменных позволяет экономить память. В действительности это не так. Компилятор сам заботится об этом и не включает имена переменных в скомпилированный скетч. Другое распространенное заблуждение: комментарии увеличивают размер программы или объем потребляемой ею оперативной памяти. Это не так.

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

Измерение объема свободной памяти

Узнать, какой объем ОЗУ занимает скетч во время выполнения, можно с помощью библиотеки MemoryFree, доступной по адресу http://playground.arduino.cc/Code/AvailableMemory.

Пользоваться этой библиотекой совсем не сложно: в ней имеется функция free­Me­mory, возвращающая число доступных байтов. Следующий скетч иллюстрирует ее использование:

#include <MemoryFree.h>

void setup()

{

  Serial.begin(115200);

}

void loop()

{

  Serial.print("freeMemory()=");

  Serial.println(freeMemory());

  delay(1000);

}

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

Уменьшение используемого объема флеш-памяти

По окончании процедуры компиляции скетча в нижней части окна Arduino IDE появится примерно такое сообщение:

Скетч использует 1344 байт (4%) памяти устройства. Всего доступно 32 256 байт.

Эта строка сообщает точный объем флеш-памяти в Arduino, который будет занят скетчем, благодаря чему вы всегда будете знать, насколько близко подошли к пределу в 32 Кбайт. Оказавшись близко к предельному значению, нужно позаботиться об оптимизации использования флеш-памяти. В этом вам помогут рассматриваемые далее рекомендации.

Используйте константы

Многие, стараясь дать имена контактам, определяют для этого переменные, как показано ниже:

int ledPin = 13;

Если вы не собираетесь изменять номер контакта с именем ledPin в процессе выполнения скетча, то вместо переменной можно использовать константу. Просто добавьте слово const в начало объявления:

const int ledPin = 13;

Это поможет сэкономить 2 байта ОЗУ плюс 2 байта флеш-памяти при каждом использовании константы. Для часто используемых переменных экономия может достигать нескольких десятков байтов.

Удалите ненужные трассировочные вызовы

В процессе отладки скетчей для Arduino принято вставлять в код команды Serial.println, помогающие увидеть значения переменных в разных точках программы и определить источники ошибок. Эти команды потреб­ляют значительный объем флеш-памяти. Любое использование Serial.println требует включения в скетч примерно 500 байт библиотечного кода. Поэтому, убедившись в безупречной работе скетча, удалите или закомментируйте все такие команды.

Откажитесь от использования загрузчика

В главе 2 рассказывалось, как запрограммировать микроконтроллер непосредственно через контакты ICSP на плате Arduino с применением аппаратных программаторов. Такой подход поможет сэкономить пару килобайт, так как не требует установки загрузчика.

Статическое и динамическое размещение в памяти

Если вы, подобно автору книги, имеете опыт разработки крупномасштабных систем на таких языках, как Java или C#, вам наверняка приходилось создавать объекты во время выполнения и позволять сборщику мусора освобождать занимаемую ими память без вашего участия. Этот подход к программированию в принципе непригоден для программ, выполняющихся на микропроцессорах, которые имеют всего 2 Кбайт памяти. Ведь в Arduino просто нет никакого сборщика мусора, и, что более важно, в программах, которые пишутся для Arduino, выделение и освобождение памяти во время выполнения редко бывают необходимы.

Ниже приводится пример объявления статического массива, как это обычно делается в скетчах:

// sketch_06_04_static

int array[100];

void setup()

{

  array[0] = 1;

  array[50] = 2;

  Serial.begin(9600);

  Serial.println(array[50]);

}

void loop()

{

}

Объем памяти, занимаемой массивом, известен уже на этапе компиляции скетча, поэтому компилятор может зарезервировать для массива необходимый объем памяти. Второй пример, приведенный ниже, также создает массив того же размера, но выделяет память для него во время выполнения из пула доступной памяти. Обратите внимание на то, что версии Arduino IDE ниже 1.0.4 не поддерживают malloc.

// sketch_06_03_dynamic

int *array;

void setup()

{

  array = (int *)malloc(sizeof(int) * 100);

  array[0] = 1;

  array[50] = 2;

  Serial.begin(9600);

  Serial.println(array[50]);

}

void loop()

{

}

В начале скетча определяется переменная int *array. Символ * сообщает, что это указатель на целочисленное значение (или в данном случае массив целых чисел), а не простое значение. Объем памяти, занимаемой массивом, неизвестен, пока не будет выполнена следующая строка в функции setup:

  array = (int *)malloc(sizeof(int) * 100);

Команда malloc (memory allocate — выделить память) выделяет память в области ОЗУ, которую называют кучей (heap). В качестве аргумента ей передается объем памяти в байтах, который следует выделить. Так как массив хранит 100 значений типа int, требуется выполнить некоторые расчеты, чтобы определить размер массива в байтах. В действительности можно было бы просто передать функции malloc число 200 в аргументе, потому что известно, что каждое значение типа int занимает 2 байта памяти, но использование функции sizeof гарантирует получение правильного числа в любом случае.

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

Однако, используя прием динамического распределения памяти, легко оказаться в ситуации, когда память выделяется, но не освобождается, из-за чего скетч может быстро исчерпать имеющуюся память. Исчерпание памяти может вызвать зависание Arduino. Но если вся память выделяется статически, такого не происходит.

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

Строки

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

Многие программы для Arduino вообще не нуждаются в текстовом представлении данных или используют его только в командах Serial.println для нужд отладки.

В Arduino поддерживаются два основных метода использования строк: старый метод — массивы элементов типа char и новый метод с применением библиотеки String Object.

Массивы элементов типа char

Когда в скетче определяется строковая константа, такая как

char message[] = "Hello World";

создается статический массив элементов типа char, содержащий 12 символов. Именно 12, а не 11, по числу букв в строке «Hello World», потому что в конец добавляется заключительный нулевой символ (), отмечающий конец строки. Такое соглашение для строк символов, принятое в языке C, позволяет использовать массивы символов большего размера, чем предполагалось вначале (рис. 6.4). Каждая буква, цифра или другой символ имеет код, который называют значением ASCII.


Рис. 6.4. Массив элементов типа char в стиле языка C с завершающим нулевым символом

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

char *message = "Hello World";

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

Форматированный вывод строк несколькими командами print

Часто строки необходимы, только чтобы вывести сообщение на жидкокристаллический дисплей или в качестве параметра Serial.println. Многие могут подумать, что в основном требуется только возможность объединения строк и преобразования чисел в строки. Например, рассмотрим конкретную проблему — как на жидкокристаллическом дисплее отобразить сообщение «Temp: 32 C». Вы могли бы предположить, что для этого нужно объединить число 32 со строкой "Temp: " и затем добавить в конец строку " C". И действительно, программисты с опытом использования языка Java могли бы попытаться написать на C следующий код:

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, выполняющую форматирование массивов символов. Она вставляет значения переменных в строку шаблона, как показано в следующем примере:

char line1[17];

int tempC = 30;

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

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

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

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

char line2[17];

int h = 12;

int m = 30;

int s = 5;

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

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

Time: 12:30:05

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

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

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

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

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

strlen("abc");

вернет число 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")[0] Переменная ch получит значение "a"
trim String s = " abc "; s.trim(); Удалит пробелы с обеих сторон от группы символов abc. Переменная s получит значение "abc"
toInt String s = "123"; int x = s.toInt(); Преобразует строковое представление числа в значение типа int или long
substring String s = "abcdefg"; String s2 = s.substring(1, 3); Возвращает фрагмент исходной строки. Переменная s2 получит значение "bc". В параметрах передаются: индекс первого символа фрагмента и индекс символа, следующего за последним символом фрагмента
replace String s = "abcdefg"; s.replace("de", "DE"); Заменит все вхождения "de" в строке на "DE". Переменная s2 получит значение "abcDEfg"

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

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

ПРИМЕЧАНИЕ

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

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

#include <EEPROM.h>

void setup()

{

  byte valueToSave = 123

  EEPROM.write(0, valueToSave);

}

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

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

EEPROM.read(0);

где 0 — это адрес в ЭСППЗУ.

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

Следующий пример демонстрирует типичный сценарий записи значения в процессе нормального выполнения программы и его чтения в момент запуска. Приложение реализует кодовый замок двери и дает возможность вводить и изменять шифр с помощью монитора последовательного порта. Шифр хранится в ЭСППЗУ, поэтому его можно менять. Если бы шифр должен был сбрасываться при каждом запуске Arduino, не было бы смысла давать пользователю возможность изменять его.

В дискуссии, приведенной далее, будут обсуждаться отдельные фрагменты скетча. Желающие увидеть полный код скетча могут открыть скетч sketch_06_06_EEPROM_example в Arduino IDE, доступный в пакете примеров для этой книги на сайте www.simonmonk.org. Опробуйте этот скетч у себя, чтобы получить более полное представление о его работе. Он не требует подключения дополнительного аппаратного обеспечения к Arduino.

Функция setup содержит вызов функции initializeCode.

void initializeCode()

{

  byte codeSetMarker = EEPROM.read(0);

  if (codeSetMarker == codeSetMarkerValue)

  {

    code = readSecretCodeFromEEPROM();

  }

  else

  {

    code = defaultCode;

  }

}

Задача этой функции — записать значение в переменную code (шифр). Это значение обычно читается из ЭСППЗУ, но при этом возникает несколько сложностей.

Содержимое ЭСППЗУ может быть не очищено в момент выгрузки нового скетча; значение, однажды записанное в ЭСППЗУ, может измениться только в результате записи нового значения поверх старого. То есть при первом запуске скетча нет никакой возможности узнать, не было ли значение оставлено в ЭСППЗУ предыдущим скетчем. В результате можно оказаться перед закрытой дверью, не зная, какой шифр хранится в ЭСППЗУ.

Для решения этой проблемы можно написать отдельный скетч, устанавливающий шифр по умолчанию. Этот скетч потребовалось бы установить в плату Arduino перед основным скетчем.

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

Функция initializeCode читает первый байт из ЭСППЗУ, и, если он равен переменной codeMarkerValue, которой где-то в другом месте присваивается значение 123, она считает, что ЭСППЗУ содержит установленный пользователем шифр, и вызывает функцию readSecretCodeFromEEPROM:

int readSecretCodeFromEEPROM()

{

  byte high = EEPROM.read(1);

  byte low = EEPROM.read(2);

  return (high << 8) + low;

}

Эта функция читает двухбайтный шифр типа int из байтов с адресами 1 и 2 в ЭСППЗУ (рис. 6.5).


Рис. 6.5. Хранение значения типа int в ЭСППЗУ

Чтобы из двух отдельных байтов получить одно значение int, нужно сдвинуть старший байт влево на 8 двоичных разрядов (high << 8) и затем прибавить младший байт.

Чтение хранимого кода из ЭСППЗУ выполняется только в случае сброса платы Arduino. Но запись шифра в ЭСППЗУ должна выполняться при каждом его изменении, чтобы после выключения или сброса Arduino шифр сохранился в ЭСППЗУ и мог быть прочитан в момент запуска скетча.

За запись отвечает функция saveSecretCodeToEEPROM:

void saveSecretCodeToEEPROM()

{

  EEPROM.write(0, codeSetMarkerValue);

  EEPROM.write(1, highByte(code));

  EEPROM.write(2, lowByte(code));

}

Она записывает признак в ячейку ЭСППЗУ с адресом 0, указывающим, что в ЭСППЗУ хранится действительный шифр, и затем записывает два байта шифра. Для получения старшего и младшего байтов шифра типа int используются вспомогательные функции highByte и lowByte из стандартной библиотеки Arduino.

Использование библиотеки avr/eeprom.h

Библиотека EEPROM позволяет писать и читать данные только по одному байту. В предыдущем разделе мы обошли это ограничение, разбивая значение int на два байта перед сохранением и объединяя два байта в значение int после чтения. В качестве альтернативы, однако, можно использовать библиотеку EEPROM, предоставляемую компанией AVR, производящей микроконтроллеры. Она обладает более широкими возможностями, включая чтение и запись целых слов (16 бит) и даже блоков памяти произвольного размера.

Следующий скетч использует эту библиотеку для сохранения и чтения значения int непосредственно, увеличивая его при каждом перезапуске Arduino:

// sketch_06_07_avr_eeprom_int

#include <avr/eeprom.h>

void setup()

{

  int i = eeprom_read_word((uint16_t*)10);

  i++;

  eeprom_write_word((uint16_t*)10, i);

  Serial.begin(9600);

  Serial.println(i);

}

void loop()

{

}

Аргумент в вызове eeprom_read_word (10) и первый аргумент в вызове eeprom_write_word — это начальный адрес слова. Обратите внимание на то, что слово состоит из двух байтов, поэтому, если понадобится записать еще одно значение int, нужно будет указать адрес 12, а не 11. Конструкция (uint16_t*) перед 10 необходима, чтобы привести адрес (или индекс) к типу, ожидаемому библиотечной функцией.

Еще одна полезная пара функций в этой библиотеке — eeprom_read_block и eeprom_write_block. Эти функции позволяют сохранять и извлекать произвольные структуры данных (допустимого размера).

Например, далее приводится скетч, записывающий строку символов в ЭСППЗУ, начиная с адреса 100:

// sketch_06_07_avr_eeprom_string

#include <avr/eeprom.h>

void setup()

{

  char message[] = "I am written in EEPROM";

  eeprom_write_block(message, (void *)100,

         strlen(message) + 1);

}

void loop()

{

}

В первом аргументе функции eeprom_write_block передается указатель на массив символов, подлежащий записи, во втором — адрес первой ячейки в ЭСППЗУ (100). В последнем аргументе передается число байтов, которые требуется записать. Здесь это число вычисляется как длина строки плюс один байт для завершающего нулевого символа.

Ниже демонстрируется скетч, который читает строку из ЭСППЗУ и выводит ее в монитор последовательного порта вместе с числом, обозначающим длину строки:

// sketch_06_07_avr_eeprom_string_read

#include <avr/eeprom.h>

void setup()

{

char message[50]; // буфер достаточно большого размера

eeprom_read_block(&message, (void *)100, 50);

Serial.begin(9600);

Serial.println(message);

Serial.println(strlen(message));

}

void loop()

{

}

Для чтения строки создается массив емкостью 50 символов. Затем вызывается функция eeprom_read_block, которая читает 50 символов в message. Знак & перед message указывает, что функции передается адрес массива message в ОЗУ.

Так как текст завершается нулевым символом, в монитор последовательного порта выводится только ожидаемый текст, а не все 50 символов.

Ограничения ЭСППЗУ

Операции чтения/записи с памятью ЭСППЗУ выполняются очень медленно — около 3 мс. Кроме того, надежность хранения гарантируется только для 100 000 циклов записи, после чего появляется вероятность искажения записанных данных. По этой причине старайтесь не выполнять запись в цикле.

Использование флеш-памяти

Объем флеш-памяти в Arduino намного больше, чем объем любой другой памяти. В Arduino Uno, например, объем флеш-памяти составляет 32 Кбайт против 2 Кбайт ОЗУ. Это делает флеш-память привлекательным местом для хранения данных, особенно если учесть, что она сохраняет данные после выключения питания.

Однако есть несколько препятствий, мешающих использованию флеш-памяти для хранения данных.

• Флеш-память в Arduino гарантирует сохранность данных только для 100 000 циклов записи, после чего она становится бесполезной.

• Флеш-память хранит программу, поэтому, если в расчеты вкрадется ошибка и часть программы окажется затертой данными, в скетче могут произойти невероятные события.

• Флеш-память содержит также загрузчик, уничтожение или искажение которого может превратить плату Arduino в «кирпич», после чего восстановить ее можно будет только с помощью аппаратного программатора (как описывалось в главе 2).

• Записывать данные во флеш-память можно только блоками по 64 байта.

Несмотря на все сказанное, в целом довольно безопасно использовать флеш-память для хранения постоянных данных, не изменяющихся в процессе выполнения скетча.

Для платы Arduino Due была создана сторонняя библиотека, позволяющая выполнять операции чтения/записи с флеш-памятью, чтобы компенсировать отсутствие ЭСППЗУ в этой модели. Более полную информацию об этом проекте можно получить по адресу http://pansenti.wordpress.com/2013/04/19/simple-flash-library-for-arduino-due/.

Самый простой способ создать строковую константу, хранящуюся во флеш-памяти, — использовать функцию F, упоминавшуюся в одном из предыдущих разделов. Напомню ее синтаксис:

Serial.println(F("Program Started"));

Этот прием работает только при использовании строковых констант непосредственно в вызове функции вывода. Нельзя, например, присвоить результат указателю на тип char.

Более гибкий, но более сложный способ заключается в использовании директивы PROGMEM (Program Memory — память программы) для сохранения любых структур данных. Однако данные должны быть постоянными — они не могут изменяться в процессе выполнения сценария.

Следующий пример иллюстрирует, как можно определить массив целых чисел (int), хранящийся во флеш-памяти:

// sketch_06_10_PROGMEM_array

#include <avr/pgmspace.h>

PROGMEM int value[] = {10, 20, 25, 25, 20, 10};

void setup()

{

  Serial.begin(9600);

  for (int i = 0; i < 6; i++)

  {

    int x = pgm_read_word(&value[i]);

    Serial.println(x);

  }

}

void loop()

{

}

Директива PROGMEM перед объявлением массива гарантирует, что он будет храниться только во флеш-памяти. Но прочитать значение элемента из такого массива можно только с помощью функции pgm_read_word из биб­лиотеки avr/pgmspace:

int x = pgm_read_word(&value[i]);

Символ & перед именем массива в параметре указывает, что функции передается адрес данного элемента массива во флеш-памяти, а не его значение.

Функция pgm_read_word читает из флеш-памяти слово (2 байта). В библио­теке имеются также функции pgm_read_byte и pgm_read_dword, возвращающие 1 и 4 байта соответственно.

Использование SD-карты

Несмотря на то что сами платы Arduino не имеют слота для SD-карт, некоторые платы расширения, включая Ethernet и MP3 (рис. 6.6), имеют слоты для карт SD или microSD.

Для подключения карт SD используется интерфейс SPI (обсуждается в главе 9). К счастью, чтобы использовать карту SD с платой Arduino, не требуется писать низкоуровневый код для взаимодействия с интерфейсом SPI, так как в состав Arduino IDE входит специализированная библиотека с простым названием SD.


Рис. 6.6. Плата расширения MP3 со слотом для карты microSD


Рис. 6.7. Результат работы примера Cardinfo

Эта библиотека включает множество примеров скетчей, выполняющих разные операции с картой SD, включая поиск информации о карте SD и ее вывод в монитор последовательного порта (рис. 6.7).

Запись на карту SD выполняется очень просто, как показано в следующем фрагменте кода:

File dataFile = SD.open("datalog.txt", FILE_WRITE);

// Если файл существует, записать в него

if(dataFile) {

  dataFile.println(dataString);

  dataFile.close();

  // вывести также в монитор последовательного порта

  Serial.println(dataString);

}

В заключение

В этой главе вы познакомились со всеми аспектами использования памяти и хранения данных в Arduino. В следующих главах мы займемся исследованием приемов программирования различных последовательных интерфейсов в Arduino, начав с шины I2C.

Оглавление книги


Генерация: 0.031. Запросов К БД/Cache: 0 / 2
поделиться
Вверх Вниз