Для записи лога я решил использовать микросхему Winbond W25Q80BV. Объем 1M, питание 3V, интерфейс SPI. Приобрел на Aliexpress в магазине Shenzhen LC Technology Co., Ltd. Store No.524881. Здесь мелким оптом дешевле.

На отладочной плате только микросхема и пять подтягивающих вверх резисторов. Для подключения микросхемы я использовал согласователь уровней 5V-3V. Приобрел на Aliexpress в магазине Electronic Gadget World Store No.1512066.

Подключение к Arduino Pro Mini осуществляется как на приведенной схеме.

Вот отдельно плата с памятью, чтобы были видны названия контактов.

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

Память Winbond W25Q80BV состоит из 16 блоков по 64K, блок состоит из 16 секторов по 4K, сектор состоит из 16 страниц по 256B. Для записи данных необходимо память предварительно стереть, при этом во все байты записывается 0xFF. Минимально можно стереть сектор 4K. Одну страницу стереть нельзя. Запись осуществляется постранично (Особенность библиотеки Adafruit_TinyFlash). Чтение побайтно. Datasheet Winbond W2580BL.
Для работы с микросхемой я решил воспользоваться библиотекой Adafruit_TinyFlash. Библиотека очень нетребовательна к ресурсам и обеспечивает весь необходимый мне базовый функционал. Она умеет:
eraseChip — стирать весь чип,
eraseSector — стирать сектор,
writePage — записывать страницу,
readNextByte — читать байт.
Теперь опишу принцип записи и чтения записей лога. Для простоты и скорости работы на каждой странице памяти будет располагаться одна запись лога. Все записи лога будут пронумерованы от 0 и до максимально 2 в 32 степени. Память состоит из 4096 страниц по 256 байт и будет использоваться циклично. При заполнении памяти записи будут добавляться в начало стирая самые старые. Адрес в памяти для строки лога легко рассчитать взяв номер строки лога и проделав две операции сдвига, сначало влево на 20 бит, затем вправо на 12 бит. Это нужно, чтобы умножить номер строки на 256, чтобы найти номер страницы, а затем обнулить слева полтора байта, что равносильно взятию остатка от деления на 1M. Структура строки лога:
4 байта — номер записи в формате unsigned long,
4 байта — время в формате time_t,
Остальные байта страницы для символьной строки в формате C, оканчивающейся 0x0.
Далее приведу файлы программы. Заголовок snFlash.h:
//* (c) Victor Makarov, 2015
#ifndef SNFLASH_H
#define SNFLASH_H
#define FLASH_PAGE_SIZE 256 // Размер страницы
#define FLASH_PAGE_NUM 4096 // Количество страниц
#define DUMP_CHARS_IN_LINE 16 // Количество символов в строке для дампа
#define LOGRECORD_SIZE 80 // Максимальная длина записи в логе (4+4+x = номер + время + строка)
#define INCLUDE_dumpPage_proc // Используется для отладки
#ifdef INCLUDE_dumpPage_proc
void dumpPage(int page); // Дамп страницы по номеру
#endif
void logWrite(char* s); // Запись строки в лог
void logLastLines(int num); // Печать последних num строк из лога
void logPrintLine(uint32_t line); // Печать строки лога по номеру
void eraseLog(void); // Стереть всю память
#endif
Собственно функции:
//* (c) Victor Makarov, 2015
#include <Adafruit_TinyFlash.h>
#include <SPI.h>
#include "snFlash.h"
#include <DS3231RTC.h>
#include <Time.h>
#include <Wire.h>
#include "snTime.h"
Adafruit_TinyFlash flash;
uint32_t logNextLineNum;
boolean isInitLogNextLineNum = false;
//*****************************************************************
//* Стереть лог
//*****************************************************************
void eraseLog(void) {
logNextLineNum = 0;
flash.eraseChip();
}
//*****************************************************************
//* Найти номер следующей страницы для записи
//*****************************************************************
void initLogNextLineNum(void) {
logNextLineNum = 0;
uint32_t num = 0;
boolean isEmpty = true;
for (uint32_t page = 0; page < FLASH_PAGE_NUM; page++) {
//flash.beginRead(addr);
flash.beginRead(page << 8);
uint8_t buf[4];
for (int i = 0; i < 4; i++) {
buf[i] = flash.readNextByte();
}
if (buf[3] != 0xFF) {
isEmpty = false;
memcpy(&num, buf, 4);
if (num > logNextLineNum) {
logNextLineNum = num;
}
}
}
flash.endRead();
if (!isEmpty) logNextLineNum++;
isInitLogNextLineNum = true;
}
//*****************************************************************
//* Дамп страницы по ее номеру
//*****************************************************************
#ifdef INCLUDE_dumpPage_proc
void dumpPage(int page) {
uint32_t addr = page;
addr = addr << 8;
uint8_t buffer [DUMP_CHARS_IN_LINE];
flash.beginRead(addr);
// Вывод страницы в порт построчно
for (int line = 0; line < (FLASH_PAGE_SIZE / DUMP_CHARS_IN_LINE); line++) {
// Чтение в буфер
for (int i = 0; i < DUMP_CHARS_IN_LINE; i++)
buffer[i] = flash.readNextByte();
// Адрес
char buf[9];
ultoa(addr + line * DUMP_CHARS_IN_LINE, buf, 16);
int l = 8 - strlen(buf);
while (l-- > 0) Serial.print('0');
Serial.print(buf);
Serial.print(' ');
// Вывод шестнадцатеричных значений
int b = 0;
for(int i = 0; i < DUMP_CHARS_IN_LINE; i++) {
ultoa(buffer[i], buf, 16);
int l = 2 - strlen(buf);
while (l-- > 0) Serial.print('0');
Serial.print(buf);
if (++b % 4 == 0) Serial.print(' ');
}
// Вывод символов
Serial.print("| ");
b = 0;
for(int i = 0; i < DUMP_CHARS_IN_LINE; i++) {
if (isprint(buffer[i])) Serial.print((char) buffer[i]);
else Serial.print('.');
if (++b % 4 == 0) Serial.print(' ');
}
Serial.print('|');
Serial.println();
}
flash.endRead();
Serial.println();
}
#endif
//*****************************************************************
//* Записать данные в лог.
//*****************************************************************
void logWrite(char* s) {
if (!isInitLogNextLineNum) initLogNextLineNum(); // Была перезагрузка, восстановить номер строки
uint8_t buf [LOGRECORD_SIZE];
// Номер записи лога преобразовать в номер страницы:
// Умножить на 256 - размер страницы, затем взять остаток от
// деления на 0x000FFFFF (1M - размер память).
// Умножение и деление меняю на сдвиги.
uint32_t addr = (logNextLineNum << 20) >> 12;
flash.beginRead(addr + 3);
if (flash.readNextByte() != 0xFF) // Проверить, страница была стерта (старший байт номера = 0xFF).
flash.eraseSector(addr); // Стереть сектор (4K), содержащий эту страницу.
tmElements_t tm; // Структура для хранения времени
RTC.read(tm); // Чтение времени
time_t time = makeTime(tm); // Упаковка времени
memcpy(buf, &logNextLineNum, 4); // Номер записи в буфер
memcpy(buf + 4, &time, 4); // Время в буфер
strlcpy((char*) buf + 8, s, sizeof(buf) - 8); // Строка лога
flash.writePage(addr, buf); // Размеры buf < 256, сделанно намеренно для экономии памяти
logNextLineNum++; // Номер следующей строки
}
//*****************************************************************
//* Печать записи по номеру
//*****************************************************************
void logPrintLine(uint32_t lineNum) {
uint32_t num; // Прочитанный номер записи
time_t time; // Прочитанное время
uint8_t buf [LOGRECORD_SIZE]; // Прочитанная строка
// Чтение записи
flash.beginRead((lineNum << 20) >> 12); // Номер записи преобразован в адрес
for (int i = 0; i < sizeof(buf); i++) {
buf[i] = flash.readNextByte();
}
flash.endRead();
//Serial.println("Read");
if (buf[3] != 0xFF) { // Страница не пустая
memcpy(&num, buf, 4); // Копирование прочитанного номера записи
//Serial.println(num);
if (num == lineNum) { // Прочитанный номер записи совпадает с запрошенным
memcpy(&time, buf + 4, 4); // Копирование прочитанного времени
// Печать номера записи
char bufConv [9]; // Буфер для символьного номера строки
ultoa(num, bufConv, 10);
int l = 8 - strlen(bufConv);
while (l-- > 0) Serial.print('0');
Serial.print(bufConv);
Serial.print(' ');
// Печать времени
tmElements_t tm; // Структура для хранения времени
breakTime(time, tm); // Распаковка времени
char timeStringbufferSMS [TIMESTRING_BUFFER_LENGTH];
Serial.print(tmElementsToString(tm, timeStringbufferSMS, sizeof(timeStringbufferSMS)));
Serial.print(" ");
Serial.print((char*) buf + 8);
Serial.print("<");
Serial.println();
}
}
}
//*****************************************************************
//* Печать последних нескольких записей
//*****************************************************************
void logLastLines(int num) {
if (!isInitLogNextLineNum) initLogNextLineNum(); // Была перезагрузка, восстановить номер строки
uint32_t c = logNextLineNum - num;
if (c > logNextLineNum) c = 0;
for (uint32_t i = c; i < logNextLineNum; i++) {
logPrintLine(i);
}
}