Уже как полтора года назад я купил пару E-Ink экранов с eBay на базе драйвера SSD1606, как раз для метеостанции. И вот 4 месяца назад, перед новыми годом, появился он.
Скажу сразу, что часов в ней нет, поскольку дома часы есть буквально везде!
Но умеет он показывать следующее:
- текущую температуру по Цельсию;
- текущую влажность в процентах;
- текущее давление в мм.рт.ст;
- историю давления за последние 15 часов в виде графика;
- напряжение батареи.
Собственно и все. Необходимый минимум и предельная простота!
Принцип работы
Контроллер должен по нажатию кнопки выводить на экран актуальную информацию. Большую часть времени контроллер спит, как и дисплей, находящийся в глубоком сне.
Контроллер периодически просыпается по 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:
: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
#include
#include
#include
//#include // local optimisation
#include
#include
#include
#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
Источник