While performing pretty well in terms of battery consumption (60µA), the old firmware was very buggy and only reacted poorly to the buttons, so I started working on the new Firmware.
It is available on GitHub, see the main project page.
Flaws of the old version:
- processor is woken up from deep sleep every second by the RTC to look for button presses
- menu completely blocks the processor, is very sluggish and hard to expand
- battery voltage is determined with a voltage divider
- no display timeout -> accidentaly drained the batteries in under a day
- peripherals are accessed during interrupts (not necessarily a bad thing, but only when timing is critical)
The main difference is that now, interrupts only set flags in the global flag struct which get processed in the main loop. Also the processor goes to deep sleep after it has finished drawing the display, while it is sending data via UART or while it is waiting for the HYT939 to finish it's measurement.
Instead of polling the buttons in a loop, the buttons now trigger a pin change interrupt. Every time this interrupt fires, a timer is reset back to zero, or started if it wasn't running already. When this timer hits a predetermined value, it fires an interrupt that stops the timer and sets the appropriate flags for the buttons.
This ensures that the inputs are only read when they didn't change their state for a while, which is a very effective way (both in responsiveness and required processing power) of debouncing buttons.
When the button flags are set, the main loop executes a function pointer which holds the currently selected button handler. This allows submenus to borrow the button events instead of having all the code in one ridiculously huge method.
When the LCD is turned off, the button handler gets reset to a method which either launches the menu or shows the current measurements.
All menu entries are now stored in an array that holds
- it's title
- a pointer to an optional value
- a pointer to a method that gets executed when the entry is selected
- a flag that determines whether the value points to a static variable or to a method that returns the value to be displayed
When an entry is executed, the method can reassign the button handler to whatever method it wants to allow for submenus.
After a button handler is executed, the processor returns to deep sleep.
Also the display now has a configurable timeout to prevent accidentally draining the battery.
When the RTC triggers it's interrupt and the countdown for setting the interval overflows, it sets a flag. In the main loop this triggers a measurement request to be sent to the HYT939. It also starts the watchdog in interrupt mode to trigger the post-measurement processing. The watchdog was chosen over a normal timer because it runs very often and a normal timer will leave the main system clock running (only supports idle mode vs power down mode) which ultimately used too much power.
Of the 14 bits that the sensor reports for each temperature and humidity, twelve are fitted into three bytes for each sample. Forty of these samples are stored in a buffer in RAM. Each of those packets also contains the time and date of the first sample, the battery voltage at the first sample, the currently selected sample interval, and the amount of actually used samples in the packet.
This makes each packet 128 bytes long, which coincides with the physical layout of the pages in the EEPROM. Writing one page at a time has many advantages, because the EEPROM can only erase it's data in entire pages. So even if you just stored a single byte, a whole page gets erased and newly written to, which wears out the EEPROM faster and takes more time and power.
The collected data can be sent via UART at a baudrate of 250,000. The controller reads an entire page from the EEPROM into a buffer and enables the UDRE interrupt which fires when there is room for another byte in the transceiver. In this interrupt , new data is put into the tranceiver. If it detects that the buffer is empty, it will again set a flag to tell the main loop to refill the buffer. This allows the core to go into idle mode to save power.
The heavy use of interrupts and sleep modes (not a single polling loop) along with the use of <power.h> to turn off the ATMega's unused modules drastically reduces the power consumption of the device.
To measure such a small power, I used a 10k resistor as a shunt and measured the voltage across it with my oscilloscope. This allowed me to integrate the current to get a precise average.
Blue Waveform: Current (10µA/div) Black Waveform: ∫ Current dt (100µAs/div) Timescale: 10s/div
This works out to a phenomenal power consumption of just 3.1µA this value even decreases with lower voltages. After loosing power, it will continue to measure for 8 more minutes just on the 220µF capacitor.
Doing some very basic calculations, the theoretical battery life would be over seventy years. In practice, the device also needs a fair bit of power to store the buffer into the EEPROM though I never measured how much exactly.
Further possible improvements
- find current leaks (probably the 220µF capacitor and the GoldCap)
- use a lithium ion cell instead of NiMhs (lower self discharge rate)
For now, I think the current setup is more than sufficient, so I will leave further improvements to readers who want to build one for themselves.