Boîte météo stupide sur E-Ink



Il y a déjà un an et demi, j'ai acheté une paire d'écrans E-Ink avec eBay sur la base du pilote SSD1606, juste pour la station météo. Et il y a 4 mois, avant la nouvelle année, il est apparu.

Je dirai tout de suite qu'il n'y a pas de montre dedans, car il y a des montres chez soi littéralement partout! Mais il sait montrer ce qui suit:

  • température actuelle en degrés Celsius;
  • humidité actuelle en pourcentage;
  • pression actuelle en mmHg;
  • historique de la pression des 15 dernières heures dans un graphique;
  • tension de la batterie.

En fait, c'est tout. Simplicité minimale et ultime nécessaire!

Même il n'y a pas une telle interface graphique


Principe de fonctionnement


Le contrôleur doit, sur simple pression d'un bouton, afficher les informations pertinentes à l'écran. La plupart du temps, le contrôleur dort, tout comme l'écran, qui est en veille profonde.

Le contrôleur se réveille périodiquement avec watchDog et prend une mesure de pression toutes les 5 minutes pour construire un graphique des changements de pression.

Cela s'est avéré très intéressant avec le calendrier, car la pression peut changer très rapidement et fortement (la météo dans la ville du nord est généralement imprévisible), puis à un moment donné, le graphique peut sortir de l'échelle. Pour ce faire, une fois toutes les deux heures, le milieu des mesures est recalibré (la pression peut monter et descendre). Cependant, pour cette raison, une nette différence entre les valeurs précédentes simplifie la lecture du graphique (un exemple sur CPDV).

Le fer


Le cerveau principal est le microcontrôleur ATMega328P, le BME280 est utilisé comme compteur entier du baromètre, et pour l'écran est l'E-Ink de la deuxième révision basée sur SSD1606 de Smart-Prototyping, qui a déjà été décrite précédemment.

Il s'agit presque du même écran que le WaveShare epaper 2.7 ", mais plus ancien (les fiches techniques leur sont très similaires).

Tout cela fonctionne sur une batterie d'un hélicoptère jouet de 120 mAh. La batterie est chargée à l'aide d'un module avec protection contre les décharges profondes et les surcharges basé sur TP4056 avec une résistance de 47 kΩ installée pour la charge avec un courant d'environ 20 mA.

Optimisation de puissance


Un sommeil sain et sain est notre tout! Par conséquent, vous devez dormir au maximum!

Puisqu'il n'y avait pas de logiciel pour travailler avec l'écran, seulement un exemple basique de code avec des commentaires en langage céleste et une fiche technique (l'écran n'est apparu qu'il y a un an et demi), la plupart de tout devait être fait par moi-même, car j'avais déjà une expérience de travail avec différents écrans.

Le mode DeepSleep a été trouvé dans la fiche technique, l'écran n'y consomme rien du tout - 1,6 mkA!

Le baromètre dispose d'un mode de mesure à la demande (alias veille), le capteur consomme un minimum d'énergie, tout en fournissant une précision suffisante pour une simple indication des changements (la fiche technique indique qu'il ne concerne que les stations météorologiques). L'inclusion de ce mode a donné une consommation de 6,2 μA. Plus loin sur le module, le régulateur LDO a été soudé à partir de LM6206N3 (ou peut-être XC6206, les deux sont déguisés en 662k) sur le MCP1700.



Cela a donné un gain de 2 μA de plus.

Puisqu'il est nécessaire d'atteindre une consommation électrique minimale, la bibliothèque LowPower a été utilisée. Il a un travail pratique avec watchDog, sur la base duquel le rêve d’atmega est réalisé. Cependant, en soi, il consomme environ 4 μA. Je vois une solution à ce problème en utilisant une minuterie externe basée sur Texas Instruments TPL5010 ou similaire.

De plus, pour réduire la consommation d'énergie, il était nécessaire de flasher l'atme avec d'autres bits FUSE et un chargeur de démarrage, ce qui a été fait avec succès avec USBasp, et a été ajouté au fichier boards.txt

Le texte suivant:
## 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


Placez également le chargeur de démarrage compilé à partir d’optiboot dans le dossier «bootloaders / atmega /»:

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


En fait, comme vous l'avez probablement deviné, tout cela a été fait sur la base d'Arduino, à savoir pro mini à 8 MHz 3,3V. Le régulateur LDO mic5203 a été soudé à partir de cette carte (trop gluant à faible courant) et la résistance LED a été soudée pour indiquer la puissance.

En conséquence, il a été possible d'atteindre une consommation d'énergie de 10 μAh en mode veille, ce qui donne environ 462,96 jours de fonctionnement. De ce nombre, vous pouvez soustraire un tiers en toute sécurité, obtenant ainsi environ 10 mois, ce qui correspond jusqu'à présent à la réalité.

J'ai testé la version sur ionistors, avec une capacité finale de 3 mAh, elle ne dure pas plus de 6 jours (auto-décharge élevée). Le calcul de la capacité de l'ionistor a été effectué selon la formule C * V / 3,6 = X mAh. Je pense que la version avec la batterie solaire et le MSP430 sera généralement éternelle.

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


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


Code principal:
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();
}


Logement


Depuis je n'ai pas d'imprimante 3D, mais j'ai un stylo 3D MyRiwell RP800A. Il s'est avéré qu'il n'était pas si facile de réaliser des structures planes et uniformes. Tout a été dessiné avec du plastique PLA, ce qui était à ce moment-là, donc le boîtier est sorti multicolore, ce qui donne en plus un certain charme (puis je le refaire sous l'arbre lorsque le plastique avec des copeaux de bois arrivera).

Les premières parties ont été dessinées directement sur papier, puis se sont décollées. Cela a laissé des traces sur le plastique. De plus, les détails étaient tordus et ils devaient être redressés d'une manière ou d'une autre!



La solution s'est avérée être simple et banale - dessiner sur du verre et y placer des «dessins» des éléments nécessaires de l'affaire.

Et voici ce qui s'est passé:



Le bouton de rafraîchissement de l'écran devait juste être rouge sur fond blanc!



La paroi arrière est réalisée avec le motif le plus simple, créant ainsi des trous de ventilation.



Le bouton était fixé sur une entretoise horizontale à l'intérieur (en jaune) avec la même poignée.



Le bouton lui-même est tiré d'un vieux boîtier d'ordinateur (il a un joli son).



À l'intérieur, tout est fixé avec un adhésif thermofusible et du plastique, de sorte qu'il n'est pas facile de le démonter.



Bien sûr, le connecteur pour charger et mettre à jour le firmware est laissé. Le boîtier devait malheureusement être monolithique pour une plus grande résistance.

Conclusion


4 mois se sont écoulés et après une charge incomplète (jusqu'à 4 V), la tension de la batterie est descendue à seulement 3,58 V, ce qui garantit une durée de vie encore plus longue jusqu'à la prochaine charge.

Les travailleurs à domicile sont très habitués à cet engin en cas de maux de tête ou si vous avez besoin de connaître les prévisions météorologiques exactes pour la prochaine heure ou deux, alors allez immédiatement chez elle et voyez ce qui s'est passé avec la pression. Sur KPDV, par exemple, une forte chute de pression est observée, en conséquence, de fortes chutes de neige et du vent ont commencé à tomber.

Liens vers les référentiels:

bibliothèque d'écran
bibliothèque pour lowPower
bibliothèque pour BME280

Mise à jour:

En raison de l'intérêt accru pour le corps, il a publié plus d'images. Écran Smart-Prototyping de la deuxième révision. Un analogue de lui sur Ali est ici .

Cliquez moi:




PS Le CPDV a été fait dans la soirée, à la suite de très, très fortes chutes de neige à Saint-Pétersbourg ce soir.
Le ruban bleu PPS n'est pas devenu connu pour avoir été ajouté à l'enquête.

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


All Articles