SPI Flash memory Winbond W25Q80BV

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

IMG_0357

 

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

 

Снимок экрана 2015-03-30 в 20.36.23

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

Снимок экрана 2015-03-29 в 22.37.40

 

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

Снимок экрана 2015-03-29 в 22.37.26

 

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

Снимок экрана 2015-03-30 в 20.42.51

 

Память 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);
    }
}

Микроконтроллер, watchdog и bootloader

Для управления приложением я использую плату Arduino Pro Mini с микроконтроллером ATMega328P (5V, 16MHz). Купил на Aliexpress в магазине Consumer Electronics Store  Store No.336447.

Arduino Pro Mini 328P 5V 16MHz

Удобная диаграмма !ProMini. Datasheet контроллера Atmel-8271-8-bit-AVR-Microcontroller-ATmega48A-48PA-88A-88PA-168A-168PA-328-328P_datasheet_Complete.

Программирование осуществляется при помощи Arduino IDE. Подключение к компьютеру осуществляется через USB порт при помощи USB to TTL UART модуля, например, на микросхеме CP2102 (CP2102 datasheet). Купил на Aliexpress в магазине ModuleFans Store No.612195.

USB TTL UART

 

Подключение к USB осуществляется по схеме:

ArduinoUSBTTL

Поскольку предполагается автономная работа в течение длительного времени, необходимо решить задачу перезагрузки контроллера в случае его зависания. Для этого используется watchdog timer. Идея простая: в таймер записывается время (максимально 8 секунд) и таймер начинает его уменьшать. Нужно до достижении 0 сбросить таймер. Если таймер не сброшен, то это означает, что контроллер завис. При достижении 0 контроллер перезагружается. Вот код, реализующий эту функцию:

//* (c) Victor Makarov, 2015
#include <avr/wdt.h>

void setup() {
    wdt_enable(WDTO_8S); // Установка таймера Watchdog на 8 секунд
}

void loop() {

    //  Какие-либо полезные действия

    // Сброс таймера Watchdog. Если управление не получено в течение
    // 8 секунд от предыдущего вызова - будет осуществлена перезагрузка.
    wdt_reset();
}

Однако существует проблема в стандартном bootloader Arduino. Он неправильно работает с этим таймером. При перезагрузке этот таймер устанавливается на 15мс и загрузчик должен его выключить. Однако загрузчик Arduino этого не делает и контроллер не успев запустить прикладную программу снова уходит в перезагрузку.

Для решения этой проблемы я установил загрузчик Optiboot. Сам загрузчик и инструкции по установке можно скачать здесь. Я использовал одну плату Arduino Pro Mini для установки Optiboot на другую плату. Шаги по установке Optiboot:

1) Скачать загрузчик, скопировать папку optiboot в /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/avr/bootloaders. Старую папку предварительно переименовать в optiboot.bak (на всякий случай).

2) Отредактировать файл /Applications/Arduino.app/Contents/Resources/Java/hardware/arduino/avr/boards.txt. Предварительно сделать его резервную копию boards.txt.bak. Я удалил из него разделы всех плат, которыми не пользуюсь и добавил новый раздел для  Arduino Pro Mini с загрузчиком Optiboot:

pro328o.name=[Optiboot] Arduino Pro Mini (5V, 16 MHz) w/ ATmega328p
pro328o.upload.tool=avrdude
pro328o.upload.protocol=arduino
pro328o.upload.maximum_size=32256
pro328o.upload.speed=115200
pro328o.bootloader.tool=avrdude
pro328o.bootloader.low_fuses=0xff
pro328o.bootloader.high_fuses=0xd6
pro328o.bootloader.extended_fuses=0x05
pro328o.bootloader.file=optiboot/optiboot_atmega328.hex
pro328o.bootloader.unlock_bits=0x3F
pro328o.bootloader.lock_bits=0x0F
pro328o.build.mcu=atmega328p
pro328o.build.f_cpu=16000000L
pro328o.build.core=arduino:arduino
pro328o.build.variant=arduino:standard

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

3) Использовать другую Arduino Pro Mini как программатор. Скетч программатора находится в примерах Arduino IDE.

Первый учебный проект — счетчик топлива

Для того чтобы изучение Arduino было интересным и полезным я решил начать со сборки счетчика топлива для загородного дома с управлением по SMS.

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

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

IMG_0354

 

На фотографии верхняя макетная плата (breadboard) предназначена для GSM модема, нижняя для всего остального. Справа налево вверху: DC-DC преобразователь напряжения для питания модема, сам модем, вольметр, антенна модема, датчик расхода топлива. Внизу: часы, плата Arduino Pro Mini. Слева провода уходят к раздельным источникам питания, справа к USB TTL UART модулю и далее к компьютеру. Черный маленький элемент слева от Arduino — датчик температуры.

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

IMG_0355

 

На фотографии вверху Arduino, внизу память и согласователь уровней 5V-3V3. Когда дойдет очередь напишу подробнее. Также в планах изучить и собрать датчик напряжения сети.