📜 ⬆️ ⬇️

Stupid Weatherbox on E-Ink



Already a year and a half ago, I bought a pair of E-Ink screens with eBay based on the SSD1606 driver, just for the weather station. And 4 months ago, before the new year, he appeared.

I will say at once that there is no clock in it, since at home there are literally watches everywhere But he can show the following:


Actually, that's all. The necessary minimum and ultimate simplicity!
')
Even there is no such GUI


Principle of operation


The controller should at the touch of a button to display current information. Most of the time the controller sleeps, as does the display, which is in deep sleep.

The controller periodically wakes up on watchDog and once every 5 minutes makes pressure measurements, to build a graph of pressure changes.

With the schedule came out very interesting, since the pressure can change very quickly and strongly (the weather in the northern city is generally unpredictable), then at some point a graph may increase in scale. To do this, once a couple of hours, the average measurement point is recalibrated (the pressure can go both up and down). However, due to this, the visual difference of the previous values ​​simplifies the reading of the graph (example on KPDV).

Iron


The main brain is the ATMega328P microcontroller, the BME280 is used as an all- meter barometer, and for the screen the previously described E-Ink second revision based on SSD1606 from Smart-Prototyping.

This is almost the same screen as waveShare epaper 2.7 “, only older (they have very similar datasheets).

All this works on the battery from a toy helicopter at 120 mAh. The battery is charged using a module with protection against deep discharge and recharging on the basis of TP4056 with a 47 kΩ resistor installed for charging current of about 20mA.

Energy Optimization


Good and healthy sleep is our everything! Therefore, you need to sleep to the maximum!

Since there was no software for working with the screen, only a basic code sample with comments in the language of the Middle Kingdom and datasheet (the screen just appeared a year and a half ago), I had to do most of the work myself, since there was already experience with different screens.

The DeepSleep mode was found in the datasheet, the screen consumes nothing but 1.6mA in it!

The barometer has a metering mode on demand (aka standby), in it the sensor consumes a minimum of energy, while providing sufficient accuracy for simple indication of changes (in the datasheet it is indicated that it is just for weather stations). The inclusion of this mode gave a consumption of 6.2 ÎĽA. Next on the module was the LDO controller with the LM6206N3 (and maybe the XC6206, both of them disguised as 662k) on the MCP1700.



This gave a gain of even 2 µA.

Since you need to achieve minimal power consumption, the LowPower library was used. It has a convenient work with watchDog, on the basis of which the dream is made. However, by itself, it consumes about 4µA. I see a solution to this problem using an external timer based on Texas Instruments TPL5010 or similar.

To reduce power consumption, it was also necessary to flash the tool with other FUSE bits and a bootloader, which was successfully done with USBasp, and was added to the boards.txt file

The following text:
## 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


Also put in the “bootloaders / atmega /” folder the bootloader compiled from 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


Actually, as you most likely guessed, all this was done on the basis of the Arduino, namely pro mini at 8 MHz 3.3V. The mic5203 LDO controller (too voracious at low currents) was dropped from this board and the LED resistor was sealed off to indicate power.

As a result, it was possible to achieve energy consumption of 10 µAh in sleep mode, which gives about 462.96 days of work. It is safe to deduct a third from this number, thereby obtaining about 10 months, which so far corresponds to reality.

I tested the version on ionistors, with a final capacity of 3 mAh, it works no more than 6 days (high self-discharge). The calculation of the capacity of the ionistor was made according to the formula C * V / 3.6 = X mAh. I think that the version with the solar battery and MSP430 will be generally eternal.

Ads:
#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;


Initialization:
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);
}


Main code:
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, ie 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();
}


Housing


Since I do not have a 3D printer, but I have a MyRiwell RP800A 3D pen. It turned out that it was not so easy for her to make planar and even structures. All PLA was painted with plastic, which was at that time, so the case came out multi-colored, which in other matters gives a certain charm (then I will redo it under the tree, when plastic with wood chips comes).

The first parts were drawn directly on paper, after which they came off. It left traces on the plastic. Moreover, the details were crooked and needed to be straightened somehow!



The solution turned out to be trivially simple - to draw on the glass, and under it put "drawings" of the necessary elements of the body.

And that's what happened:



The screen update button just had to be red on a white background!



The back wall is made with a simple pattern, thereby creating air vents.



The button was attached to the horizontal strut inside (in yellow) with the handle that was also made.



The button itself is taken from the old computer case (it has a pleasant sound).



Inside, everything is fixed with hot melt and plastic, so that it’s not easy to disassemble.



Of course, left the connector for charging and firmware upgrades. The case, unfortunately, had to be made monolithic for greater strength.

Conclusion


4 months have passed, and after not fully charging (up to 4V) the voltage on the battery is only up to 3.58V, which guarantees a long service life until the next charge.

People are very accustomed to this thing in the case of headaches or if you need to know the exact weather forecast for the next hour or two, they immediately go to her and see what happened with the pressure. At KPDV, for example, one can see a strong drop in pressure, as a result it began to snow heavily with the wind.

Repository links:

→ screen library
→ library for lowPower
→ library for BME280

Updated:

In connection with the increased interest in the body posted more images. Screen Smart-Prototyping second revision. Analog to him on Ali here .

Click me:




PS KPDV was done in the evening, as a result, very, very much snow fell in St. Petersburg tonight.
PPS Blue tape did not add to the survey for known reasons.

Source: https://habr.com/ru/post/410949/


All Articles