
Hace ya un año y medio, compré un par de pantallas E-Ink con eBay basadas en el controlador SSD1606, solo para la estación meteorológica. Y hace 4 meses, antes del año nuevo, apareció.
Diré de inmediato que no hay reloj, ¡ya que hay relojes en casa literalmente en todas partes! Pero él sabe cómo mostrar lo siguiente:
- temperatura actual en grados Celsius;
- humedad actual en porcentaje;
- presión actual en mmHg;
- historial de presión de las últimas 15 horas en un gráfico;
- voltaje de la batería
En realidad eso es todo. Necesaria mínima y máxima simplicidad!
Principio de funcionamiento
El controlador debe, con solo tocar un botón, mostrar información relevante en la pantalla. La mayoría de las veces el controlador duerme, al igual que la pantalla, que está en reposo profundo.
El controlador se activa periódicamente con watchDog y toma una medición de presión cada 5 minutos para generar un gráfico de los cambios de presión.
Resultó ser muy interesante con el cronograma, ya que la presión puede cambiar muy rápida y fuertemente (el clima en la ciudad del norte es generalmente impredecible), entonces, en algún momento, el gráfico puede salir de escala. Para hacer esto, una vez cada dos horas, se recalibra el punto medio de las mediciones (la presión puede subir y bajar). Sin embargo, debido a esto, una clara diferencia entre los valores anteriores simplifica la lectura del gráfico (un ejemplo en CPDV).
Hierro
El cerebro principal es el microcontrolador ATMega328P, el
BME280 se usa como el medidor completo del barómetro y para la pantalla es el E-Ink de la segunda revisión basada en SSD1606 de Smart-Prototyping, que ya se describió anteriormente.
Esta es casi la misma pantalla que el WaveShare epaper 2.7 ", solo que más antiguo (las hojas de datos son muy similares a ellas).
Todo esto funciona con una batería de un helicóptero de
juguete de 120 mAh. La batería se carga utilizando un módulo con protección contra descargas profundas y sobrecargas basadas en TP4056 con una resistencia de 47 kΩ instalada para cargar con una corriente de aproximadamente 20 mA.
Optimización de energía
¡Un sueño sano y saludable es nuestro todo! Por lo tanto, ¡debes dormir al máximo!
Como no había software para trabajar con la pantalla, solo un ejemplo básico de código con comentarios en el lenguaje celeste y la hoja de datos (la pantalla solo apareció hace un año y medio), la mayor parte de todo tenía que hacerlo yo mismo, ya que ya tenía experiencia trabajando con diferentes pantallas.
El modo DeepSleep se encontró en la hoja de datos, en ella la pantalla no consume nada: ¡1,6 mkA!
El barómetro tiene un modo de medición bajo demanda (también conocido como en espera), en el que el sensor consume un mínimo de energía, al tiempo que proporciona una precisión suficiente para una simple indicación de cambios (la hoja de datos indica que es solo para estaciones meteorológicas). La inclusión de este modo dio un consumo de 6.2 μA. Más adelante en el módulo, el regulador LDO fue soldado desde LM6206N3 (o tal vez XC6206, ambos están disfrazados como 662k) en el MCP1700.

Esto dio una ganancia de 2 μA más.
Como es necesario lograr un consumo mínimo de energía, se utilizó la biblioteca LowPower. Tiene un trabajo conveniente con watchDog, en base al cual se hace realidad el sueño de atmega. Sin embargo, por sí solo consume alrededor de 4 μA. Veo una solución a este problema usando un temporizador externo basado en Texas Instruments TPL5010 o similar.
Además, para reducir el consumo de energía, era necesario flashear el atme con otros bits FUSE y un gestor de arranque, que se realizó con éxito con USBasp, y se agregó al archivo boards.txt
El siguiente texto:## 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
También coloque el gestor de arranque compilado desde optiboot en la carpeta "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 realidad, como probablemente haya adivinado, todo esto se hizo sobre la base de Arduino, es decir, pro mini a 8MHz 3.3V. El regulador mic5203 LDO fue soldado de esta placa (demasiado glotón a bajas corrientes) y la resistencia LED fue soldada para indicar potencia.
Como resultado, fue posible lograr un consumo de energía de 10 μAh en modo de reposo, lo que da unos 462,96 días de funcionamiento. De este número, puede restar de manera segura un tercio, obteniendo así unos 10 meses, que hasta ahora corresponde a la realidad.
Probé la versión con ionistores, con una capacidad final de 3 mAh, no dura más de 6 días (alta autodescarga). El cálculo de la capacitancia del ionistor se realizó de acuerdo con la fórmula C * V / 3.6 = X mAh. Creo que la versión con batería solar y MSP430 será generalmente eterna.
Anuncios:#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;
Inicializacion: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();
}
Vivienda
Como no tengo una impresora 3D, tengo un bolígrafo 3D MyRiwell RP800A. Resultó que no era tan fácil hacer estructuras planas e incluso estructuras. Todo fue dibujado con plástico PLA, que era en ese momento, por lo que el caso salió multicolor, lo que además le da un cierto encanto (luego lo rehaceré debajo del árbol cuando llegue el plástico con astillas de madera).
Las primeras partes fueron dibujadas directamente en papel, y luego se desprenden. Esto dejó marcas en el plástico. ¡Además, los detalles estaban torcidos y debían enderezarse de alguna manera!

La solución resultó ser simple: dibuje sobre vidrio y coloque "dibujos" de los elementos necesarios del estuche debajo.
Y esto es lo que sucedió:

¡El botón de actualización de pantalla solo tenía que ser rojo sobre un fondo blanco!

La pared posterior está hecha con el patrón más simple, creando así orificios de ventilación.

El botón se fijó en un puntal horizontal en el interior (en amarillo) con el mismo mango.

El botón en sí está tomado de una caja de computadora vieja (tiene un sonido agradable).

En el interior, todo se fija con adhesivo termofusible y plástico, por lo que no es fácil desmontarlo.

Por supuesto, se deja el conector para cargar y actualizar el firmware. El caso, desafortunadamente, tuvo que hacerse monolítico para una mayor resistencia.
Conclusión
Pasaron 4 meses, y después de no cargarse completamente (hasta 4V), el voltaje de la batería bajó a solo 3.58V, lo que garantiza una vida útil aún más larga hasta la próxima carga.
Los trabajadores a domicilio están muy acostumbrados a este artilugio en caso de dolores de cabeza o si necesita conocer el pronóstico del tiempo exacto para la próxima hora o dos, luego acuda inmediatamente a ella y vea qué sucedió con la presión. En KPDV, por ejemplo, se observa una fuerte caída de presión, como resultado, comenzaron a caer fuertes nevadas y vientos.
Enlaces a repositorios:
→
biblioteca de pantalla→
biblioteca para lowPower→
biblioteca para BME280Actualizado:
Debido al mayor interés en el cuerpo, publicó más imágenes. Pantalla de prototipos inteligentes de la segunda revisión. Un análogo a él en Ali está
aquí .
PD: El CPDV se hizo por la noche, como resultado de la caída de nieve en San Petersburgo esta noche.
PPS Blue tape no se hizo conocido por agregar a la encuesta.