Caixa estúpida do tempo no E-Ink



Já há um ano e meio, comprei um par de telas E-Ink no eBay, baseadas no driver SSD1606, apenas para a estação meteorológica. E há 4 meses, antes do ano novo, ele apareceu.

Direi imediatamente que não há relógio, pois há relógios em casa literalmente em todo lugar! Mas ele sabe como mostrar o seguinte:

  • temperatura atual em graus Celsius;
  • umidade atual em porcentagem;
  • pressão atual em mmHg;
  • histórico de pressão nas últimas 15 horas em um gráfico;
  • voltagem da bateria.

Na verdade isso é tudo. Simplicidade mínima e final necessária!

Mesmo não existe tal GUI


Princípio de funcionamento


O controlador deve, com o toque de um botão, exibir informações relevantes na tela. Na maioria das vezes o controlador dorme, assim como a tela, que está em sono profundo.

O controlador acorda periodicamente com o watchDog e realiza uma medição de pressão a cada 5 minutos para criar um gráfico das alterações de pressão.

Acabou sendo muito interessante com o cronograma, uma vez que a pressão pode mudar muito rápida e fortemente (o clima na cidade do norte é geralmente imprevisível), então, em algum momento, o gráfico pode ficar fora de escala. Para fazer isso, uma vez a cada duas horas, o ponto médio das medições é recalibrado (a pressão pode subir e descer). No entanto, devido a isso, uma clara diferença entre os valores anteriores simplifica a leitura do gráfico (um exemplo no CPDV).

Ferro


O cérebro principal é o microcontrolador ATMega328P, o BME280 é usado como o medidor inteiro do barômetro e para a tela está o E-Ink da segunda revisão baseada no SSD1606 do Smart-Prototyping, que já foi descrito anteriormente.

Essa tela é quase a mesma do epaper WaveShare 2.7 ", apenas mais antiga (as planilhas de dados são muito semelhantes a elas).

Tudo isso funciona com uma bateria de um helicóptero de brinquedo de 120 mAh. A bateria é carregada usando um módulo com proteção contra descarga profunda e sobrecarga com base no TP4056 com um resistor de 47 kΩ instalado para carregar com uma corrente de cerca de 20 mA.

Otimização de energia


Um sono saudável e saudável é o nosso tudo! Portanto, você precisa dormir ao máximo!

Como não havia software para trabalhar com a tela, apenas um exemplo básico de código com comentários na linguagem celestial e na folha de dados (a tela apareceu apenas um ano e meio atrás), quase tudo tinha que ser feito por mim, pois eu já tinha experiência em trabalhar com telas diferentes.

O modo DeepSleep foi encontrado na folha de dados; nela, a tela não consome nada - 1.6mkA!

O barômetro possui um modo de medição sob demanda (também conhecido como espera), nele o sensor consome um mínimo de energia, fornecendo precisão suficiente para uma simples indicação de alterações (a folha de dados indica que é apenas para estações meteorológicas). A inclusão desse modo deu um consumo de 6,2 μA. Mais adiante no módulo, o regulador LDO foi soldado do LM6206N3 (ou talvez do XC6206, ambos disfarçados de 662k) no MCP1700.



Isso deu um ganho de 2 μA a mais.

Como é necessário atingir o consumo mínimo de energia, a biblioteca LowPower foi usada. Possui um trabalho conveniente com o watchDog, com base no qual o sonho do atmega é realizado. No entanto, por si só consome cerca de 4 μA. Vejo uma solução para esse problema usando um timer externo baseado no Texas Instruments TPL5010 ou similar.

Além disso, para reduzir o consumo de energia, era necessário atualizar o atme com outros bits do FUSE e um gerenciador de inicialização, que foi realizado com sucesso com o USBasp, e foi adicionado ao arquivo boards.txt

O texto a seguir:
## 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


Coloque também o gerenciador de inicialização compilado a partir do optiboot na pasta “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


Na verdade, como você provavelmente adivinhou, tudo isso foi feito com base no Arduino, ou seja, pro mini a 8MHz 3.3V. O regulador mic5203 LDO foi soldado a partir desta placa (muito glutão em correntes baixas) e o resistor de LED foi soldado para indicar energia.

Como resultado, foi possível obter um consumo de energia de 10 μAh no modo de suspensão, o que proporciona cerca de 462,96 dias de operação. Deste número, você pode subtrair com segurança um terço, obtendo assim cerca de 10 meses, o que até agora corresponde à realidade.

Testei a versão em ionistores, com capacidade final de 3 mAh, não dura mais de 6 dias (alta autodescarga). O cálculo da capacitância do ionistor foi realizado de acordo com a fórmula C * V / 3,6 = X mAh. Eu acho que a versão com a bateria solar e o MSP430 será geralmente eterna.

Anúncios:
#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;


Inicialização:
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);
}


Código 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();
}


Habitação


Desde que eu não tenho uma impressora 3D, mas tenho uma caneta 3D MyRiwell RP800A. Descobriu-se que não era tão fácil criar estruturas planas e uniformes. Tudo foi desenhado com plástico PLA, que era naquela época, então a caixa saiu multicolorida, o que além disso dá um certo charme (então vou refazê-la embaixo da árvore quando o plástico com lascas de madeira chegar).

As primeiras partes foram desenhadas diretamente no papel e depois saem. Isso deixou marcas no plástico. Além disso, os detalhes estavam torcidos e precisavam ser corrigidos de alguma forma!



A solução acabou sendo banal simples - desenhe no vidro e coloque “desenhos” dos elementos necessários do estojo embaixo dele.

E aqui está o que aconteceu:



O botão de atualização da tela só precisava estar vermelho sobre fundo branco!



A parede traseira é feita com o padrão mais simples, criando, assim, orifícios de ventilação.



O botão foi fixado em um suporte horizontal no interior (em amarelo) com a mesma alça.



O botão em si é retirado de um gabinete antigo de computador (possui um som agradável).



No interior, tudo é fixado com adesivo hot-melt e plástico, para que não seja fácil desmontá-lo.



Obviamente, resta o conector para carregar e atualizar o firmware. Infelizmente, o caso teve que ser monolítico para maior força.

Conclusão


Passados ​​4 meses e depois de não carregar totalmente (até 4V), a voltagem da bateria caiu para apenas 3,58V, o que garante uma vida útil ainda mais longa até a próxima carga.

Os trabalhadores da casa estão muito acostumados com isso no caso de dores de cabeça ou se você precisar descobrir a previsão exata do tempo para a próxima hora ou duas, então vá imediatamente até ela e veja o que aconteceu com a pressão. No KPDV, por exemplo, uma forte queda de pressão é vista, como resultado, neve e vento fortes começaram a cair.

Links para repositórios:

biblioteca de telas
biblioteca para lowPower
biblioteca para BME280

Atualizado:

Devido ao crescente interesse no corpo, ele postou mais imagens. Tela de prototipagem inteligente da segunda revisão. Um análogo para ele em Ali está aqui .

Clique em mim:




PS O CPDV foi feito à noite, como resultado de muita, muita neve caiu em São Petersburgo hoje à noite.
A fita azul PPS não ficou conhecida por ser adicionada à pesquisa.

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


All Articles