电子墨水上的愚蠢天气盒



大约一年半以前,我购买了一对基于eBay的,基于SSD1606驱动程序的电子墨水屏,仅用于气象站。 4个月前,在新的一年之前,他出现了。

我会马上说里面没有手表,因为实际上家里到处都有手表! 但是他知道如何显示以下内容:

  • 当前摄氏温度;
  • 当前湿度百分比
  • 当前压力,单位:mmHg;
  • 图表中最近15个小时的压力历史记录;
  • 电池电压。

其实仅此而已。 必要的最低限度和终极简单性!

即使没有这样的GUI


工作原理


控制器应该通过触摸按钮显示相关信息。 在大多数情况下,控制器都处于睡眠状态,显示屏也处于深度睡眠状态。

控制器会定期通过watchDog唤醒,并每5分钟进行一次压力测量,以建立压力变化图。

事实证明,该时间表非常有趣,因为压力会迅速而强烈地变化(北部城市的天气通常是不可预测的),因此该图表有时可能会超出比例。 为此,每隔几个小时要重新校准一次测量的中点(压力可以上下波动)。 但是,由于这个原因,先前值之间的明显差异简化了图形的读取(CPDV上的示例)。


主要的大脑是ATMega328P微控制器, BME280被用作晴雨表的整个仪表,而屏幕则是基于Smart-Prototyping的SSD1606的第二版电子墨水,这已经在前面进行了介绍。

该屏幕与WaveShare epaper 2.7“几乎是同一屏幕,只是屏幕更旧(数据表与它们非常相似)。

所有这些都可以使用120 mAh 玩具直升机的电池供电。 电池使用基于TP4056的具有深度放电和过充电保护功能的模块进行充电,该模块安装了47kΩ电阻器,用于以约20 mA的电流充电。

功率优化


健全而健康的睡眠是我们的一切! 因此,您需要最大程度地睡眠!

由于没有用于屏幕的软件,只有带有天体语言和数据表的注释的基本代码示例(屏幕仅在一年半前才出现),所以大多数事情都必须由我自己完成,因为我已经有使用不同屏幕的经验。

在数据表中找到了DeepSleep模式,在该模式下,屏幕完全不消耗任何东西-1.6mkA!

晴雨表具有按需计量模式(又称待机模式),传感器消耗的能量最少,同时提供了足够的精度以简单指示变化(数据表表明它仅用于气象站)。 包含此模式的功耗为6.2μA。 进一步在模块上,LDO稳压器由MCP1700上的LM6206N3(或XC6206,都伪装成662k)焊接而成。



这样可以增加2μA的增益。

由于必须实现最低功耗,因此使用了LowPower库。 它与watchDog配合使用非常方便,从而实现了atmega的梦想。 但是,它本身消耗约4μA的电流。 我发现使用基于Texas Instruments TPL5010或类似产品的外部计时器可以解决此问题。

另外,为减少功耗,有必要用其他FUSE位和引导加载程序刷新atme,这已通过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


还将从optiboot编译的引导程序放入“ 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


实际上,正如您可能猜到的那样,所有这些都是在Arduino的基础上完成的,即8MHz 3.3V的pro mini。 mic5203 LDO稳压器从该板上焊接(在低电流下太粘),而LED电阻器则焊接以指示功率。

结果,有可能在睡眠模式下实现10μAh的能耗,这可以提供约462.96天的运行时间。 从这个数字中,您可以放心地减去三分之一,从而获得大约10个月的时间,这与实际情况相符。

我在离子电阻器上测试了该版本,最终容量为3 mAh,持续时间不超过6天(高自放电)。 根据公式C * V / 3.6 = X mAh计算离子电阻的电容。 我认为带有太阳能电池和MSP430的版本通常是永恒的。

公告:
#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;


初始化:
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, 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();
}


房屋


由于我没有3D打印机,但是我有3D笔MyRiwell RP800A。 事实证明,制造平面且均匀的结构并非易事。 一切都是用当时的PLA塑料绘制的,所以表壳是彩色的,另外还具有一定的魅力(然后,当有木屑的塑料到达时,我将在树下重新制作)。

第一部分直接画在纸上,然后脱落。 这在塑料上留下了痕迹。 而且,细节是弯曲的,需要以某种方式弄直!



事实证明,解决方案很简单-在玻璃上画图,然后在其下放下案件必要元素的“图”。

这是发生了什么:



屏幕刷新按钮只需要在白色背景上为红色即可!



后壁用最简单的图案制成,从而形成通风孔。



将按钮固定在具有相同手柄的水平支柱内部(黄色)中。



该按钮本身取自旧的计算机机箱(声音不错)。



在内部,所有部件都用热熔胶和塑料固定,因此拆卸起来不容易。



当然,剩下用于充电和更新固件的连接器了。 不幸的是,为了增强强度,必须将外壳做成整体。

结论


经过4个月的时间,并且在未完全充电(最高4V)之后,电池上的电压下降到仅3.58V,这保证了下一次充电之前的使用寿命更长。

在头痛的情况下,或者如果您需要了解接下来一两个小时的确切天气预报,家庭作业人员会非常习惯这种情况,然后立即去找她,看看压力如何了。 例如,在KPDV上,看到了很大的压降,结果,大雪和大风开始降落。

链接到存储库:

屏幕库
低功耗库
BME280的库

更新时间:

由于对身体的兴趣增加,他发布了更多图片。 第二修订版的智能原型屏幕。 这里有一个与他类似的关于阿里的故事

点击我:




PS CPDV是在傍晚制作的,这是由于今晚圣彼得堡发生了非常大的积雪。
PPS Blue胶带并未因加入调查而闻名。

Source: https://habr.com/ru/post/zh-CN410949/


All Articles