Глупый метеокороб на E-Ink / Geektimes

Уже как полтора года назад я купил пару E-Ink экранов с eBay на базе драйвера SSD1606, как раз для метеостанции. И вот 4 месяца назад, перед новыми годом, появился он.

Скажу сразу, что часов в ней нет, поскольку дома часы есть буквально везде!

Но умеет он показывать следующее:

Собственно и все. Необходимый минимум и предельная простота!

Принцип работы


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

Контроллер периодически просыпается по watchDog и раз в 5 минут делает замеры давления, для построения графика изменения давления.

С графиком вышло очень интересно, так как давление может меняться очень быстро и сильно (погода в северном городе вообще непредсказуема), то в какой-то момент может возникнуть зашкаливание графа. Для этого раз в пару часов происходит перекалибровка средней точки измерений (давление может идти как вверх, так и вниз). Однако благодаря этому, наглядная разница предыдущих значений упрощает чтение графа (пример на КПДВ).

Железо


Основным мозгом является микроконтроллер ATMega328P, в качестве всеметра барометра используется BME280, а для экрана уже описанный ранее E-Ink второй ревизии на базе SSD1606 от Smart-Prototyping.
Это почти тот же экран, что и waveShare epaper 2,7“, только старее (даташиты у них ну очень похожи).

Все это работает на аккумуляторе от игрушечного вертолета на 120 мАч.

Заряжается аккумулятор при помощи модуля с защитой от глубокого разряда и перезаряда на базе TP4056 с установленным резистором на 47 кОм для зарядки током около 20мА.

Оптимизация энергопотребления


Крепкий и здоровый сон наше все! Поэтому нужно спать по максимуму!

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

В даташите был найден режим DeepSleep, в нем экран потребляет всего ничего — 1.6мкА!

Барометр имеет режим замера по требованию (ака standby), в нем датчик потребляет минимум энергии, при этом предоставляя достаточную точность для простой индикации изменений (в даташите указано, что он как раз для метеостанций). Включение этого режима дало потребление на уровне 6,2 мкА. Далее на модуле был перепаян LDO регулятор с LM6206N3 (а может и XC6206, они оба маскируются как 662k) на MCP1700.


Это дало выигрыш еше на 2 мкА.

Поскольку нужно добиться минимального энергопотребления, то была использована библиотека LowPower. В ней есть удобная работа с watchDog, на основе чего и сделан сон атмеги. Однако, сам по себе он потребляет около 4мкА. Решение этой проблемы мне видится использованием внешнего таймера на основе Texas Instruments TPL5010 или аналогичным.

Таже для уменьшения энергопотребления нужно было прошить атмегу другими FUSE битами и загрузчиком, что и было успешно сделано с USBasp, а в файл boards.txt был добавлен

следующий текст:

## Arduino Pro or Pro Mini (1.8V, 1 MHz Int.) w/ ATmega328p

## internal osc div8, also now watchdog, no LED on boot

## bootloader size: 402 bytes

## http://homes-smart.ru/index.php/oborudovanie/arduino/avr-zagruzchik

## http://homes-smart.ru/fusecalc/?prog=avrstudio&part=ATmega328P

## http://www.engbedded.com/fusecalc

## -------------------------------------------------

pro.menu.cpu.1MHzIntatmega328=ATmega328 (1.8V, 1 MHz Int., BOD off)

pro.menu.cpu.1MHzIntatmega328.upload.maximum_size=32256

pro.menu.cpu.1MHzIntatmega328.upload.maximum_data_size=2048

pro.menu.cpu.1MHzIntatmega328.upload.speed=9600

pro.menu.cpu.1MHzIntatmega328.bootloader.low_fuses=0x62

pro.menu.cpu.1MHzIntatmega328.bootloader.high_fuses=0xD6

pro.menu.cpu.1MHzIntatmega328.bootloader.extended_fuses=0x07

pro.menu.cpu.1MHzIntatmega328.bootloader.file=atmega/a328p_1MHz_62_d6_5.hex

pro.menu.cpu.1MHzIntatmega328.build.mcu=atmega328p

pro.menu.cpu.1MHzIntatmega328.build.f_cpu=1000000L

Также положить в папку «bootloaders/atmega/» загрузчик собранный из optiboot:

a328p_1MHz_62_d6_5.hex

:107E0000F894112484B714BE81FFDDD082E0809302

:107E1000C00088E18093C10086E08093C2008CE0BE

:107E20008093C4008EE0B9D0CC24DD2488248394D0

:107E3000B5E0AB2EA1E19A2EF3E0BF2EA2D08134A3

:107E400061F49FD0082FAFD0023811F0013811F43F

:107E500084E001C083E08DD089C0823411F484E1D4

:107E600003C0853419F485E0A6D080C0853579F447

:107E700088D0E82EFF2485D0082F10E0102F00278F

:107E80000E291F29000F111F8ED068016FC0863583

:107E900021F484E090D080E0DECF843609F040C049

:107EA00070D06FD0082F6DD080E0C81680E7D8065C

:107EB00018F4F601B7BEE895C0E0D1E062D089932E

:107EC0000C17E1F7F0E0CF16F0E7DF0618F0F60147

:107ED000B7BEE89568D007B600FCFDCFA601A0E0CC

:107EE000B1E02C9130E011968C91119790E0982F91

:107EF0008827822B932B1296FA010C0187BEE895F6

:107F000011244E5F5F4FF1E0A038BF0751F7F60133

:107F1000A7BEE89507B600FCFDCF97BEE89526C042

:107F20008437B1F42ED02DD0F82E2BD03CD0F601D2

:107F3000EF2C8F010F5F1F4F84911BD0EA94F80143

:107F4000C1F70894C11CD11CFA94CF0CD11C0EC0EF

:107F5000853739F428D08EE10CD085E90AD08FE03E

:107F60007ACF813511F488E018D01DD080E101D09E

:107F700065CF982F8091C00085FFFCCF9093C600FD

:107F800008958091C00087FFFCCF8091C00084FDE0

:107F900001C0A8958091C6000895E0E6F0E098E160

:107FA000908380830895EDDF803219F088E0F5DF5B

:107FB000FFCF84E1DECF1F93182FE3DF1150E9F7E5

:107FC000F2DF1F91089580E0E8DFEE27FF27099494

:0400000300007E007B

:00000001FF

Собственно как вы, скорее всего, догадались, все это делалось на базе Arduino, а именно pro mini на 8МГц 3.3В. С этой платы был выпаян LDO-регулятор mic5203 (слишком прожорлив при малых токах) и отпаян резистор светодиода для индикации питания.

В итоге удалось добиться энергопотребления в 10 мкАч в спящем режиме, что дает около 462,96 дней работы. От этого числа смело можно вычесть треть, получив тем самым около 10 месяцев, что пока соответствует реальности.

Версию на ионисторах тестировал, при конечной емкости 3мАч работает не более 6 дней (высокий саморазряд). Расчет емкости ионистора делался по формуле C*V/3,6 = X мАч. Думаю, что версия с солнечной батареей и MSP430 будет вообще вечной.

Объявления:

#include <SPI.h>

#include <Wire.h>

#include <ssd1606.h>

#include <Adafruit_BME280.h>

//#include <BME280_2.h> // local optimisation

#include <LowPower.h>

#include <avr/sleep.h>

#include <avr/power.h>

#define TIME_X_POS 0

#define TIME_Y_POS 12

#define DATE_X_POS 2

#define DATE_Y_POS 9

#define WEECK_X_POS 65

#define WEECK_Y_POS 9

// ====================================== //

#define TEMP_X_POS 105

#define TEMP_Y_POS 15

#define PRESURE_X_POS 105

#define PRESURE_Y_POS 12

#define HUMIDITY_X_POS 105

#define HUMIDITY_Y_POS 9

// ====================================== //

#define BATT_X_POS 65

#define BATT_Y_POS 15

#define ONE_PASCAL 133.322

// ==== for presure history in graph ==== //

#define MAX_MESURES 171

#define BAR_GRAPH_X_POS 0

#define BAR_GRAPH_Y_POS 0

#define PRESURE_PRECISION_RANGE 4.0 // -/+ 4 mm

#define PRESURE_GRAPH_MIN 30 // vertical line graph for every N minutes

#define PRESURE_PRECISION_VAL 10 // max val 100

#define PRESURE_CONST_VALUE 700.0 // const val what unneed in graph calculations

#define PRESURE_ERROR -1000 // calibrated value

// ====================================== //

#define VCC_CALIBRATED_VAL 0.027085714285714 // == 3.792 V / 140 (real / mesured)

//#define VCC_CALIBRATED_VAL 0.024975369458128 // == 5.070 V / 203 (real / mesured)

#define VCC_MIN_VALUE 2.95 // min value to refresh screen

#define CALIBRATE_VCC 1 // need for battery mesure calibration

// 37 ~296 sec or 5 min * MAX_MESURES = 14,33(3) hours for full screen

#define SLEEP_SIZE 37

#ifdef BME280_ADDRESS

#undef BME280_ADDRESS

#define BME280_ADDRESS 0x76

#endif

#define ISR_PIN 3 // other mega328-based 2, 3

#define POWER_OFF_PIN 4 // also DONEPIN

#define E_CS 6 // CS ~ D6

#define E_DC 5 // D/C ~ D5

#define E_BSY 7 // BUSY ~ D7

#define E_RST 2 // RST ~ D2

#define E_BS 8 // BS ~ D8

/*

MOSI ~ D11

MISO ~ D12

CLK ~ D13

*/

EPD_SSD1606 Eink(E_CS, E_DC, E_BSY, E_RST);

Adafruit_BME280 bme;

volatile bool adcDone;

bool updateSreen = true;

bool normalWakeup = false;

float battVal =0;

uint8_t battValcV =0;

uint8_t timeToSleep = 0;

float presure =0;

float temperature =0;

float humidity =0;

float presure_mmHg =0;

unsigned long presureMin =0;

unsigned long presureMax =0;

uint8_t currentMesure = MAX_MESURES;

uint8_t presureValHistoryArr[MAX_MESURES] = {0};

typedef struct {

uint8_t *pData;

uint8_t pos;

uint8_t size;

unsigned long valMax;

unsigned long valMin;

} history_t;

Инициализация:

void setup()

{

saveExtraPower();

Eink.begin();

initBME();

// https://www.arduino.cc/en/Reference/attachInterrupt

pinMode(ISR_PIN, INPUT_PULLUP);

attachInterrupt(digitalPinToInterrupt(ISR_PIN), ISRwakeupPin, RISING);

//drawDefaultGUI();

drawDefaultScreen();

// tiiiiny fix....

checkBME280();

updatePresureHistory();

}

void saveExtraPower(void)

{

power_timer1_disable();

power_timer2_disable();

// Disable digital input buffers:

DIDR0 = 0x3F; // on ADC0-ADC5 pins

DIDR1 = (1 << AIN1D) | (1 << AIN0D); // on AIN1/0

}

void initBME(void)

{

bme.begin(BME280_ADDRESS); // I2C addr

LowPower.powerDown(SLEEP_250MS, ADC_OFF, BOD_OFF); // wait for chip to wake up.

while(bme.isReadingCalibration()) { // if chip is still reading calibration, delay

LowPower.powerDown(SLEEP_120MS, ADC_OFF, BOD_OFF);

}

bme.readCoefficients();

bme.setSampling(Adafruit_BME280::MODE_FORCED,

Adafruit_BME280::SAMPLING_X1, // temperature

Adafruit_BME280::SAMPLING_X1, // pressure

Adafruit_BME280::SAMPLING_X1, // humidity

Adafruit_BME280::FILTER_OFF);

}

Основной код:

void loop()

{

for(;;) { // i hate func jumps when it's unneed!

checkVCC();

if(normalWakeup) {

checkBME280();

updatePresureHistory();

} else {

normalWakeup = true;

}

updateEinkData();

enterSleep();

}

}

// func to exec in pin ISR

void ISRwakeupPin(void)

{

// Keep this as short as possible. Possibly avoid using function calls

normalWakeup = false;

updateSreen = true;

timeToSleep = 1;

}

ISR(ADC_vect)

{

adcDone = true;

}

void debounceFix(void)

{

normalWakeup = true;

updateSreen = false;

}

//https://github.com/jcw/jeelib/blob/master/examples/Ports/bandgap/bandgap.ino

uint8_t vccRead(void)

{

uint8_t count = 4;

set_sleep_mode(SLEEP_MODE_ADC);

ADMUX = bit(REFS0) | 14; // use VCC and internal bandgap

bitSet(ADCSRA, ADIE);

do {

adcDone = false;

while(!adcDone) sleep_mode();

} while (--count);

bitClear(ADCSRA, ADIE);

// convert ADC readings to fit in one byte, i.e. 20 mV steps:

// 1.0V = 0, 1.8V = 40, 3.3V = 115, 5.0V = 200, 6.0V = 250

return (55U * 1023U) / (ADC + 1) - 50;

}

unsigned long getHiPrecision(double number)

{

// what if presure will be more 800 or less 700? ...

number -= PRESURE_CONST_VALUE; // remove constant value

number *= PRESURE_PRECISION_VAL; // increase precision by PRESURE_PRECISION_VAL

return (unsigned long)number; // Extract the integer part of the number

}

void checkVCC(void)

{

// reconstruct human readable value

battValcV = vccRead();

battVal = battValcV * VCC_CALIBRATED_VAL;

if(battVal <= VCC_MIN_VALUE) { // not enought power to drive E-Ink or work propetly

detachInterrupt(digitalPinToInterrupt(ISR_PIN));

// to prevent full discharge: just sleep

bme.setSampling(Adafruit_BME280::MODE_SLEEP);

LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);

Eink.sleep(true);

LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);

}

}

void checkBME280(void)

{

bme.takeForcedMeasurement(); // wakeup, make new mesure and sleep

temperature = bme.readTemperature();

humidity = bme.readHumidity();

presure = bme.readPressure();

}

void updatePresureHistory(void)

{

// convert Pa to mmHg; 1 mmHg == 133.322 Pa

presure_mmHg = (presure + PRESURE_ERROR)/ONE_PASCAL;

// === calc presure history in graph === //

if((++currentMesure) >= (MAX_MESURES/3)) { // each 4,75 hours

currentMesure =0;

presureMin = getHiPrecision(presure_mmHg - PRESURE_PRECISION_RANGE);

presureMax = getHiPrecision(presure_mmHg + PRESURE_PRECISION_RANGE);

}

// 36 == 4 pixels in sector * 9 sectors

presureValHistoryArr[MAX_MESURES-1] = map(getHiPrecision(presure_mmHg), presureMin, presureMax, 0, 35);

for(uint8_t i=0; i < MAX_MESURES; i++) {

presureValHistoryArr[i] = presureValHistoryArr[i+1];

}

}

void updateEinkData(void)

{

if(updateSreen) {

updateSreen = false;

Eink.sleep(false);

// bar history

Eink.fillRect(BAR_GRAPH_X_POS, BAR_GRAPH_Y_POS, MAX_MESURES, 9, COLOR_WHITE);

for(uint8_t i=1; i <= (MAX_MESURES/PRESURE_GRAPH_MIN); i++) {

Eink.drawVLine(BAR_GRAPH_X_POS+i*PRESURE_GRAPH_MIN, BAR_GRAPH_Y_POS, 35, COLOR_DARKGREY);

}

for(uint8_t i=0; i <= MAX_MESURES; i++) {

Eink.drawPixel(i, BAR_GRAPH_Y_POS+presureValHistoryArr[i], COLOR_BLACK);

}

#if CALIBRATE_VCC

Eink.setCursor(BATT_X_POS, BATT_Y_POS);

Eink.print(battVal);

Eink.setCursor(BATT_X_POS, BATT_Y_POS-3);

Eink.print(battValcV);

#endif

Eink.setCursor(TEMP_X_POS, TEMP_Y_POS);

Eink.print(temperature);

Eink.setCursor(PRESURE_X_POS, PRESURE_Y_POS);

Eink.print(presure_mmHg);

Eink.setCursor(HUMIDITY_X_POS, HUMIDITY_Y_POS);

Eink.print(humidity);

updateEinkSreen();

Eink.sleep(true);

}

}

void updateEinkSreen(void)

{

Eink.display(); // update Eink RAM to screen

LowPower.idle(SLEEP_15MS, ADC_OFF, TIMER2_OFF, TIMER1_OFF, TIMER0_OFF, SPI_OFF, USART0_OFF, TWI_OFF);

Eink.closeChargePump();

// as Eink display acts not like in DS, then just sleep for 2 seconds

LowPower.powerDown(SLEEP_2S, ADC_OFF, BOD_OFF);

}

void effectiveIdle(void)

{

LowPower.idle(SLEEP_30MS, ADC_OFF, TIMER2_OFF, TIMER1_OFF, TIMER0_OFF, SPI_OFF, USART0_OFF, TWI_OFF);

}

void drawDefaultScreen(void)

{

Eink.fillScreen(COLOR_WHITE);

Eink.printAt(TEMP_X_POS, TEMP_Y_POS, F("00.00 C"));

Eink.printAt(PRESURE_X_POS, PRESURE_Y_POS, F("000.00 mm"));

Eink.printAt(HUMIDITY_X_POS, HUMIDITY_Y_POS, F("00.00 %"));

#if CALIBRATE_VCC

Eink.printAt(BATT_X_POS, BATT_Y_POS, F("0.00V"));

// just show speed in some kart racing game in mushr... kingdom (^_^ )/

Eink.printAt(BATT_X_POS, BATT_Y_POS-3, F("000cc"));

#endif

}

void drawDefaultGUI(void)

{

Eink.drawHLine(0, 60, 171, COLOR_BLACK); // split 2 areas

// draw window

Eink.drawRect(0, 0, 171, 71, COLOR_BLACK);

// frame for text

Eink.drawRect(BATT_X_POS, BATT_Y_POS, 102, 32, COLOR_BLACK);

}

void snooze(void)

{

do {

LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF);

} while(--timeToSleep);

}

void disablePower(void)

{

digitalWrite(POWER_OFF_PIN, HIGH);

delay(1);

digitalWrite(POWER_OFF_PIN, LOW);

LowPower.powerDown(SLEEP_FOREVER, ADC_OFF, BOD_OFF);

}

void enterSleep(void)

{

// wakeup after ISR signal;

timeToSleep = SLEEP_SIZE;

debounceFix();

snooze();

}

Корпус


Поскольку 3D принтера не имею, но имею 3D ручку MyRiwell RP800A. Оказалось, что делать планарные и ровные структуры ей не так-то просто. Рисовалось все PLA пластиком, который был на тот момент, поэтому корпус вышел разноцветным, что в прочем придает некий шарм (потом переделаю под дерево, когда приедет пластик с древесной крошкой).

Первые части рисовались напрямую на бумаге, после чего отрывались. Это оставляло следы на пластике. Более того детали были кривыми и их было нужно как-то выпрямлять!

Решение оказалось до банального простым — рисовать на стекле, а под него положить «чертежи» нужных элементов корпуса.

И вот что вышло:

Кнопка обновления экрана просто обязана была быть красной на белом фоне!

Задняя стенка сделана с простейшим узором, создавая тем самым вентиляционные отверстия.

Кнопка была закреплена на горизонтальной распорке внутри (желтым цветом) так же сделанной ручкой.

Сама кнопка взята от старого компьютерного корпуса (у нее приятный звук).

Внутри все закреплено термоклеем и пластиком, так, что разобрать это все непросто.

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

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

Заключение


Прошло 4 месяца, а после не полной зарядки (до 4В) напряжение на батарее село всего до 3.58В, что гарантирует еще долгий срок службы до следующей зарядки.

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

Ссылки на репозитории:
библиотека для экрана
библиотека для lowPower
библиотека для BME280

Источник