16 апреля 2014

Работа с энергонезависимой памятью на STM32

Введение

Во встраиваемых системах часто возникает необходимость хранить данные во время отключенного питания. Это могут быть настройки программы, журналы событий, логи работы устройства и многое другое. Для этих целей можно использовать средства, встроенные в контроллер, а можно взять внешние микросхемы памяти. Каждый из вариантов имеет свои положительные и отрицательные стороны.
Внутренняя энергонезависимая память контроллера, пригодная для наших целей может быть как EEPROM, так и память программ (FLASH). EEPROM есть ни у каждого чипа, и ее объемы часто бывают недостаточны. FLASH, в силу своей особенности имеет ощутимые ограничения по количеству циклов стирания/записи. Плюс ко всему, стирание возможно только постранично. То есть, если нужно переписать один байт, то необходимо стереть всю страницу, затем заново записать нужные данные. Есть методики использования FLASH для этих целей, которые позволяют использовать ее более эффективно, но данная статья не об этом.

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

За основу был выбран микроконтроллер STM32F100RBT6, установленный на STM32VLDISCOVERY. Но данный проект может быть легко перенесен на любые другие платформы.

В качестве пациента воспользуемся микросхемой FM24CL64. Питается от 2.7 до 3.6 вольт, хранит данные до 45 лет, количество циклов перезаписи устремляется к бесконечности. Данная модель имеет 64 килобита памяти (8 килобайт). Запись данных производится со скоростью шины: с какой скоростью данные для записи передаются в микросхему - с той скоростью они и будут записываться. Например, при использовании классических EEPROM необходимо выжидать довольно длительную паузу после каждой отправки байт на запись. Это, в свою очередь, увеличивает риск потерять данные, если отключение питания произойдет в критический момент. У FRAM длительность этого момента настолько незначительно, что простым контролем напряжения со стороны микроконтроллера можно полностью избавиться от этой проблемы.

Запись данных

Как было сказано выше общение с микросхемой происходит с помощью последовательного интерфейса I2C.
Рассмотрим схему записи одного байта, взятую из даташита:


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

В прошлой статье мы написали модуль софтового I2C, который, в качестве функционала, предоставляет нам две функции.
int i2cSoft_ReadBuffer (uint8_t chipAddress, uint8_t *buffer, uint32_t sizeOfBuffer);
int i2cSoft_WriteBuffer (uint8_t chipAddress, uint8_t *buffer, uint32_t sizeOfBuffer);
Их нам будет достаточно, чтобы организовать связь в микросхемой памяти. Применительно к рисунку выше, функция i2cSoft_WriteBuffer записывает произвольный буфер данных, выделенный на картинке красным в чип, адрес которого выделен на картинке синим. Эта функция берет на себя все действия, необходимые для отправки пакета. Нам же необходимо только сформировать правильный буфер.
Примечание. Вся энергонезависимая память поделена на ячейки, размером 1 байт каждая. Мы можем обратиться непосредственно к каждой ячейке, используя ее адрес.
Первые два байта отправляемого буфера - это адрес ячейки, в который будет производится запись. Обратите внимание, что адрес ячейки двухбайтный, и старший байт идет первым. Третий байт - это байт информации, который необходимо записать.

Можно уже написать тестовый код, который будет что-то делать.
uint8_t buf[3];
buf[0] = 0x01;
buf[1] = 0x23;
buf[2] = 144;
i2cSoft_WriteBuffer( FRAM_ADDRES, buf, 3);
Мы создаем буфер на три элемента, и вручную заполняем его. В первый элемент записываем старший байт адреса ячейки, во второй - младший, в третий - данные, которые будем записывать. То есть на этом этапе мы формируем буфер, выделенный на рисунке выше красным. Затем мы вызываем функцию, в которую передаем адрес нашего подчиненного на шине (он фиксированный и указан в даташите - 0xA0), указатель на буфер, который мы сформировали, и количество элементов в буфере для отправки. Таким образом мы записали число 144 в ячейку с адресом 0x0123.

Часто бывает необходимо записывать несколько байт информации за раз, причем эти байты должны идти в памяти друг за другом. Это нужно для размещения в энергонезависимой памяти многобайтных переменных.
Эта возможность тоже предусмотрена, и реализуется следующим образом.
Тут мы действуем примерно также, как в прошлом примере, только не ограничиваемся одним байтом информации. Дело в том, что после передачи каждого байта, в момент, обозначенный на рисунке зеленым цветом, микросхема перемещает свой указатель на следующую ячейку, и второй байт записывается уже следом за прошлой ячейкой. Напишем простейший код:
uint8_t buf[5];
buf[0] = 0x02;
buf[1] = 0x01;
buf[2] = 5;
buf[3] = 10;
buf[4] = 15;
i2cSoft_WriteBuffer( FRAM_ADDRES, buf, 5);
Тут мы помещаем в буфер адрес ячейки, начиная с которой будет производится запись. Затем последовательно помещаем элементы, которые нужно записать. В результате в память запишется следующее:
Адрес    данные
0x0201 - 5
0x0202 - 10
0x0203 - 15
Примечание. Обратите внимание, что при вызове функции i2cSoft_WriteBuffer третьим параметром мы передаем число 5, которое обозначает что нам нужно передать в шину буфер из 5 элементов.

Чтение данных

Прежде чем поговорить о чтении данных из энергонезависимой памяти, остановимся на таком понятии, как указатель не текущую ячейку. Этот указатель находится в микросхеме памяти и обозначает куда мы будем записывать наши данные и откуда мы будем их читать. Как уже было сказано выше, при операции записи, мы непосредственно передаем адрес ячейки, куда будет производится запись. А при записи нескольких байт, мы знаем, что указатель на текущую ячейку будет автоматически инкрементироваться. При чтении логика работы аналогичная: сначала нам нужно установить указатель на нужную ячейку, затем ее прочитать. Причем установка указателя на нужную ячейку довольно схожа с записью данных в память. Рассмотрим это подробнее:
В отличии от первого рисунка, где мы записываем один байт информации по указанному адресу, тут мы устанавливаем текущий адрес и прерываем передачу. Напишем простую функцию, которая будет устанавливать указатель энергонезависимой памяти на нужный адрес:
/**
 *  @brief  Установить указатель на считываемые данные в энергонезависимой памяти
 *  @param  uint16_t blockAdr - адрес в энергонезависимой памяти
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
static bool prv_SetAddress ( uint16_t adr )
{
    int result;
    uint8_t buf[2];
    buf[0] = (uint8_t) (adr >> 8);
    buf[1] = (uint8_t) (adr & 0x00FF);
    result = i2cSoft_WriteBuffer( FRAM_ADDRESS, buf, 2 );
    return result;
}

Стоит отметить, что это уже не механический пример, а работоспособная функция, которая будет использоваться в коде.
После этого мы можем приступить к чтению данных.
Обратите внимание на единицу, выделенную зеленым: это часть стандарта I2C. Мастер этой единицей сообщает слейву, что он собирается читать данные. В предыдущих коммуникациях, рассмотренных нами, мастер только записывал данные. После того, как слейв принял свой адрес (Slave Address) и увидел единицу, он начинает выдавать данные из ячейки, на которую установлен его указатель (а этот указатель мы установили в предыдущей передаче). Тоесть область, выделенная на рисунке красным, будет сгенерирована микросхемой памяти. Рассмотрим это на примере кода:
prv_SetAddress(0x0123);
uint8_t data;
i2cSoft_ReadBuffer( FRAM_ADDRES, &data, 1);
Все довольно просто: сначала мы устанавливаем указатель на ячейку 0x0123, и затем читаем один байт.
Примечание. Обратите внимание, что в функцию ReadBuffer вторым параметром мы передаем не переменную data, а ее адрес.
Легко догадаться, как будет выглядеть чтение нескольких байт. Инкремент указателя на ячейку происходит и при чтении.
Тут мы читаем из слейва уже два байта, причем чтение будет происходить начиная с той ячейки, на которую мы заранее установили указатель. И чтение каждого следующего байта будет происходить из следующей ячейки памяти. И снова простейший пример:
prv_SetAddress(0x0123);
uint8_t data[2];
i2cSoft_ReadBuffer( FRAM_ADDRES, data, 2);
Примечание. Я не рассматриваю тут варианты, когда чтение данных происходит сразу же после установки адреса и формирования сигнала «повторный старт». Многие микросхемы памяти поддерживают эту возможность и это позволяет сократить время чтения. Но данная информация выходит за рамки этой статьи. Тем более что мой модуль программного I2C не поддерживает эту возможность. Подробнее об этом можно прочитать тут.

Пишем код

Теорию общения с энергонезависимой памятью мы рассмотрели, приступим к написанию кода. Одну функцию установки указателя в памяти я уже написал, поэтому повторять ее не буду. Напишем функцию, которая будет писать блок данных заданного размера в память начиная с указанного ядреса ячейки:
/**
 *  @brief  Запись в энергонезависимую память по указанному адресу
 *          блока данных. Размер блока данных не более 4 байт.
 *  @param  void* buf - указатель на данные
 *          size_t blockSize - размер блока данных в байтах
 *          uint16_t blockAdr - адрес в энергонезависимой памяти, начиная
 *                              с которого будет производится запись
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
static bool prv_WriteBlock ( void* buf, size_t blockSize, uint16_t blockAdr )
{
    assert_param(blockSize <= MAX_BLOCK_SIZE);          // максимальный размер блока - 4 байта
                                                        // ибо больше не требуется
    bool result;
    uint8_t data[MAX_BLOCK_SIZE + 2];                   // буфер на два байта больше.
                                                        // Два байта для адреса ячейки

    memcpy( (void*) data + 2, (void*) buf, blockSize ); // подготавливаем буфер

    data[0] = (uint8_t) (blockAdr >> 8);
    data[1] = (uint8_t) (blockAdr & 0xFF);
                                                        // и отправляем его
    result = i2cSoft_WriteBuffer( FRAM_ADDRESS, data, blockSize + 2 );
    return result;
}
Примечание. Конструкция assert_param(blockSize <= MAX_BLOCK_SIZE) не обязательна в нашем случае. Во время выполнения программы она проверяет выражение в скобках на истинность. Если выражение ложно – вызовется функция void assert_failed(uint8_t* file, uint32_t line), которая у меня объявлена в файле main.c. Благодаря этому можно отследить некорректное выполнение программы (попытка отправки блока больше MAX_BLOCK_SIZE) и устранить проблему на этапе тестирования.
 И еще одна функция, которая будет читать данные по тому же принципу.
/**
 *  @brief  Чтение из энергонезависимой памяти по указанному адресу
 *          блока данных. Размер блока данных не более 4 байт.
 *  @param  void* buf - указатель на данные
 *          size_t blockSize - размер блока данных в байтах
 *          uint16_t blockAdr - адрес в энергонезависимой памяти, начиная
 *                              с которого будет производится чтение
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
static bool prv_ReadBlock ( void* buf, size_t blockSize, uint16_t blockAdr )
{
    bool result;
    result = prv_SetAddress( blockAdr );                // сначала устанавливаем адрес чтения
    if ( result == false )
        return false;
    result = i2cSoft_ReadBuffer( FRAM_ADDRESS, buf, blockSize );    // читаем данные
    return result;
}
Эти функции являются основными, с помощью которых можно прочитать, либо записать любое количество байт (у меня ограничение не больше 4 байт) по любому адресу. Но нам нужно читать и записывать стандартные переменные, такие как uint8_t, int16_t, float и прочее. Поэтому мы создадим «обертки» для этих рабочих функций, которые будут выполнять всю работу.

Запись однобайтной переменной будет выглядеть так:
/**
 *  @brief  Запись в энергонезависимую память по указанному адресу одного байта.
 *  @param  uint8_t data - записываемый байт
 *          uint16_t adr - адрес в энергонезависимой памяти
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
bool Fram_WriteByte(uint8_t data, uint16_t adr)
{
    return prv_WriteBlock(&data, sizeof(uint8_t), adr);
}
Здесь мы передаем в функцию prv_WriteBlock адрес переменной, которую нужно записать, ее размер в байтах, вычисленный как sizeof(uint8_t) и адрес ячейки. Все остальные функции выполнены по тому же принципу:
/**
 *  @brief  Запись в энергонезависимую память по указанному адресу двух байт.
 */
bool Fram_WriteDoubleByte(uint16_t data, uint16_t adr)
{
    return prv_WriteBlock(&data, sizeof(uint16_t), adr);
}

/**
 *  @brief  Запись в энергонезависимую память по указанному адресу четырех байт.
 */
bool Fram_WriteFourByte(uint32_t data, uint16_t adr)
{
    return prv_WriteBlock(&data, sizeof(uint32_t), adr);
}

/**
 *  @brief  Запись в энергонезависимую память по указанному адресу переменной
 *          типа float.
 */
bool Fram_WriteFloat(float data, uint16_t adr)
{
    return prv_WriteBlock(&data, sizeof(float), adr);
}

/**
 *  @brief  Запись в энергонезависимую память по указанному адресу переменной
 *          типа double.
 */
bool Fram_WriteDouble(double data, uint16_t adr)
{
    return prv_WriteBlock(&data, sizeof(double), adr);
}

/**
 *  @brief  Чтение из энергонезависимой памяти по указанному адресу одного байта.
 *  @param  uint8_t *data - указатель на переменную, в которую будет помещен
 *                          результат чтения
 *          uint16_t adr - адрес в энергонезависимой памяти
 *  @return bool - результат выполнения функции:
 *          true в случае успеха
 *          false в случае ошибки
 */
bool Fram_ReadByte(uint8_t *data, uint16_t adr)
{
    return prv_ReadBlock(data, sizeof(uint8_t), adr);
}

/**
 *  @brief  Чтение из энергонезависимой памяти по указанному адресу двух байт.
 */
bool Fram_ReadDoubleByte(uint16_t *data, uint16_t adr)
{
    return prv_ReadBlock((uint8_t*)data, sizeof(uint16_t), adr);
}

/**
 *  @brief  Чтение из энергонезависимой памяти по указанному адресу четырех байт.
 */
bool Fram_ReadFourByte(uint32_t *data, uint16_t adr)
{
    return prv_ReadBlock((uint8_t*)data, sizeof(uint32_t), adr);
}

/**
 *  @brief  Чтение из энергонезависимой памяти по указанному адресу переменной
 *          типа float
 */
bool Fram_ReadFloat(float *data, uint16_t adr)
{
    return prv_ReadBlock((uint8_t*)data, sizeof(float), adr);
}

/**
 *  @brief  Чтение из энергонезависимой памяти по указанному адресу переменной
 *          типа double
 */
bool Fram_ReadDouble(double *data, uint16_t adr)
{
    return prv_ReadBlock((uint8_t*)data, sizeof(double), adr);
}
Вот и все, библиотека готова. Теперь набросаем небольшой рабочий пример, который позволит протестировать нашу библиотеку. Пример механический, не несет в себе никакой полезной функциональности. Имеется смысл его запускать только под отладчиком. Шагая по коду, можно посмотреть как нужные данные читаются и записываются из/в энергонезависимой памяти корректно.
int main ()
{
    Fram_Init();                // инициализация модуля
    float data = 0;             // объявляем переменную и присваиваем ее нулю
    Fram_ReadFloat(&data, 32);  // читаем в эту переменную данные из ячейки 32
    data = 56.4343f;
    Fram_WriteFloat(data, 32);  // записываем число 56.4343f в ячейку
    data = 0;
    Fram_ReadFloat(&data, 32);  // читаем число и убеждаемся что оно совпадает
    data = 66.4343f;
    Fram_WriteFloat(data, 32);  // пишем другое число, чтоб при следующем запуске
                                // программы прочитать его и убедится что оно
                                // сохранилось во время отключенного питания.
    while ( 1 ) {

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

Готовый пример можно скачать отсюда. Проект сделан в эклипсе. Пользователи других IDE могут просто добавить следующие файлы к себе в проект:
  • i2cSoft.h
  • i2cSoft.c
  • fram_driver.h
  • fram_driver.c
  • main.c

2 комментария:

  1. Хреновая статья! Весь код в статье разбросан как на "мусорнике". А эклипсом не все пользуются.

    ОтветитьУдалить
    Ответы
    1. Этот комментарий тоже должен быть удален автором.

      Удалить