Сборка счетчика топлива завершена. Ура!

Устройство собрал, вот фото в корпусе без крышки:

IMG_0514

 

С закрытой крышкой:

IMG_0531Слева направо: кабель к датчику топлива, антенна GSM, индикация, кабель к датчику напряжения, питание 220V. Слева от кабеля к датчику напряжения внутри корпуса датчик температуры.

При сборке внес изменения: датчик температуры поместил на плату, добавил подтягивающий вниз резистор 10k на второй пин Arduino.

Счетчик топлива перенесен на платы

Счетчик топлива со всеми компонентами перенесен на платы и готов к размещению в корпусе.

Это блок контроллера, флэш памяти и часов. Версия 2

Слева часы. Вверху справа Arduino. Внизу справа преобразователь уровней и флэш память.

Разъемы:

Слева: питание модема 3,3V.

Справа внизу от блока питания: черный — земля, красный +5V, оранжевый +3,3V.

Справа вверху для отладки — USB TTL UART. Скорость 9600.

Вверху слева для блока сенсоров: коричневый — земля, красный +5V, оранжевый — напряжение,  желтый — не используется, зеленый — температура.

Затем для датчика топлива: коричневый — земля, красный — +5V, оранжевый — данные.

Затем для блока индикации: черный — земля, серый +5V, коричневый — DS, фиолетовый — ST,  синий — SH.

Затем для модема: коричневый — SIMR, красный — SIMT.

Блок индикации:

IMG_0488

Индикаторы сверху вниз:

критический остаток топлива,

критическая температура,

нет напряжения в сети,

модем не готов,

ошибка флэш памяти,

устройство работает.

Блок датчиков:

IMG_0509

Слева подключен датчик напряжения. Над ним датчик температуры. Справа разъем для блока микроконтроллера.

Подключение GSM модема SIM900A mini v3.4.1

Для управления устройством по SMS я купил на Aliexpress GSM модем SIM900A mini v3.4.1.

SIM900A mini v3.4.1

Документация: SIM900_AT Command Manual_V1.03 SIM900_download procedure_V0.20 SIM900_Hardware Design_V2.00 SIM900A_schematic

Оказалось, что этот модем не предназначен для работы в России. Пришлось повозиться. В конце концов мне  удалось его перепрошить, так что он заработал.

Процесс перепрошивки описан здесь: http://alex-exe.ru/radio/wireless/gsm-sim900-firmwar-all-in-one/ и здесь http://arduino.ru/forum/apparatnye-voprosy/gsm-sim900a-delaem-iz-dvukh-diapazonov-chetyre.

Я использовал эту прошивку 1137B02SIM900M64_ST_ENHANCE и эту программу для ее загрузки SIM900 Series download Tools Develop 1.9. Подключение для загрузки прошивки:

Снимок экрана 2015-04-25 в 14.44.08

К сожалению на плате не было возможности легко подключиться к пинам PWRKEY и DEBUG. Они просто не были выведены. К тому же у меня почему-то нужно было пин PWRKEY не на землю сажать, а подавать на него 1. Плату паять не хотелось, поэтому я выкрутился следующим образом: положил под плату магнит от жесткого диска и пины от кабелей прилипли к нужным разъемам. На фотографиях ниже показано, как я это сделал.

IMG_0244

 

IMG_0241

IMG_0243

Подключение датчика расхода топлива

В качестве датчика я намереваюсь использовать следующее купленное на Aliexpress устройство:

IMG_0088

Модель: LP-GF24
RED — VCC
WHITE — GND
BLUE — DATA
2174 импульсов на 1 литр

Скорее всего оно было скопировано с этого устройства:

Снимок экрана 2015-02-01 в 23.29.15

В этом блоге ругают копию. На ebay можно купить оригинал. Попробую пока с тем датчиком, который уже купил. Импульсы он честно выдает, а как себя будет вести, когда будет подключен к топливопроводу — посмотрим.

Я подключил датчик ко второму пину Arduino Pro Mini, чтобы считать импульсы, приходящие от него. Для этого я буду обрабатывать прерывание 0. Уже в процессе сборки добавил подтягивающий вниз резистор 10k на второй пин, на схеме его нет.

Кабель к датчику нужен экранированный, я использовал FTP патч корд.

Снимок экрана 2015-04-25 в 14.19.43

//*****************************************************************
//* Инициализация подсистемы подсчета расхода топлива
//*****************************************************************
void setupFuelMeter(void) {
attachInterrupt(fuelMeterInt, ISRfuelMeter, RISING); // Разрешения прерывания
}

//*****************************************************************
//* Обработчик прерывания - импульс счетчика топлива
//*****************************************************************
void ISRfuelMeter(void) {
app.fuelPulsesAll.number++;
app.fuelPulsesLate.number++;
}

Переменные были определены как volatile:

volatile UnsignedLong_Bytes fuelPulsesAll; // Расход топлива в импульсах с момента установки счетчика
volatile UnsignedLong_Bytes fuelPulsesLate; // Расход топлива в импульсах с момента заправки

Подключение часов DS3231

Часы DS3231 обладают высокой точностью. Они подключаются к Arduino по протоколу I2C двумя проводами, плюс питание и земля. Подключение к Arduino Pro Mini: A4 — SDA, A5 — SCL, VCC — VCC, GND — GND. Datasheet можно посмотреть здесь DS3231.

Снимок экрана 2015-04-25 в 13.14.52

Я использовал библиотеку — DS3231RTC.h, скачать ее можно здесь https://github.com/trunet/DS3231RTC. Она используется совместно с библиотекой Time.h, последнюю версию можно взять здесь: https://github.com/PaulStoffregen/Time.

Примеры использования:

tmElements_t tm; // Структура для хранения времени
RTC.read(tm); // Чтение времени
RTC.write(tm); // Установка времени

Структура tmElements_t описана в Time.h:

typedef struct {
uint8_t Second;
uint8_t Minute;
uint8_t Hour;
uint8_t Wday; // day of week, sunday is day 1
uint8_t Day;
uint8_t Month;
uint8_t Year; // offset from 1970;
} tmElements_t, TimeElements, *tmElementsPtr_t;

Там же находятся полезные константы и функции. Среди них:

//convenience macros to convert to and from tm years 
#define  tmYearToCalendar(Y) ((Y) + 1970)  // full four digit year 
#define  CalendarYrToTm(Y)   ((Y) - 1970)
#define  tmYearToY2k(Y)      ((Y) - 30)    // offset is from 2000
#define  y2kYearToTm(Y)      ((Y) + 30)  

void breakTime(time_t time, tmElements_t &tm); // break time_t into elements
time_t makeTime(tmElements_t &tm); // convert time elements into time_t

Датчик наличия напряжения в сети 220V

Вчера собрал и подключил к макету датчик наличия напряжения в сети. Он был собран по следующей схеме. Схема нарисована на онлай ресурсе Scheme-It.
Снимок экрана 2015-04-05 в 18.41.44

 

U1 и U2 — оптопара TLP504A (AOT101AC)
Q1 — KT3107A (BC308A)
R1 — 470K
R2 — 20K
R3 — 30K
R4 — 10K
C1 — 470mF

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

IMG_0360

 

На следующей фотографии вилка в сборе.

IMG_0363

Обработка данных в программе.

#define LED_220 9 // HIGH - есть напряжение 220, LOW - нет

void setup() {
  pinMode(LED_220, INPUT);  // Установка пина для проверки напряжения сети
}

// В процедуре проверки
if (digitalRead(LED_220) == LOW) {
  // Обработка пропадания напряжения
}

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. Когда дойдет очередь напишу подробнее. Также в планах изучить и собрать датчик напряжения сети.