This article discusses storage of sensor data (a BME280) in an FRAM MB85RC256V and only send the stored over WiFi when the 32k FRAM is full. that would save on WiFi time and thus battery life. It is a conceptual discussion as it might not save any energy on account of having to add extra hardware.
The FRAM uses about 200uA when in use and 27uA when in standby. It can store about 2048 BME280 records so that would save quite some energy on WiFi connections that do not need to be made. The catch however is that storing sensor data is only relevant if you know the time they were read, so you would need to for instance make connection to a time server to read the time, which means having to make a relatively power hungry WiFi connection, which is what we tried to avoid to begin with. So that doesn’t seem to be the best option. Nevertheless, below I will provide a program that does such:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <Adafruit_FRAM_I2C.h>
#include <ESP8266WiFi.h>
#include <FirebaseESP8266.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
// Replace with your Firebase credentials
#define FIREBASE_HOST "your-firebase-project-id.firebaseio.com"
#define FIREBASE_AUTH "your-firebase-auth-token"
// Replace with your Wi-Fi credentials
const char *ssid = "your-SSID";
const char *password = "your-PASSWORD";
// Define the sleep duration in seconds (4 hours)*
// max sleeptime on ESP8266 is 71 min. To make it sleep longer:
//https://arduinodiy.wordpress.com/2023/12/07/making-the-esp8266-sleep-for-longer-than-72-minutes/
const uint32_t SLEEP_DURATION = 4 * 60 * 60;
// Create an instance of the BME280 sensor
Adafruit_BME280 bme;
// Create an instance of the Adafruit FRAM library
Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();
// Create an instance of the NTP client
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
void setup() {
Serial.begin(115200);
Wire.begin();
// Initialize Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
// Initialize BME280 sensor
if (!bme.begin()) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
// Initialize FRAM
if (!fram.begin()) {
Serial.println("Could not find a valid FRAM, check wiring!");
while (1);
}
// Connect to Firebase
Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
}
void loop() {
// Get current time from NTP server
updateTime();
// Read sensor data
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0; // Pressure in hPa
// Store data in FRAM
writeDataToFram(timeClient.getEpochTime(), temperature, humidity, pressure);
// Check if FRAM is full
if (framUsedBytes() >= framSize()) {
// Upload data to Firebase
uploadDataToFirebase();
}
// Deep sleep
ESP.deepSleep(SLEEP_DURATION * 1e6);
}
void writeDataToFram(uint32_t timestamp, float temperature, float humidity, float pressure) {
// Calculate the number of records stored
uint16_t recordNumber = framUsedBytes() / sizeof(SensorData);
// Create a data structure to hold sensor data
SensorData data;
data.timestamp = timestamp;
data.temperature = temperature;
data.humidity = humidity;
data.pressure = pressure;
// Write data to FRAM
fram.write((uint8_t*)&data, recordNumber * sizeof(SensorData), sizeof(SensorData));
}
void uploadDataToFirebase() {
// Calculate the number of records stored
uint16_t recordNumber = framUsedBytes() / sizeof(SensorData);
// Read data from FRAM and upload to Firebase
for (uint16_t i = 0; i < recordNumber; i++) {
SensorData data;
fram.read((uint8_t*)&data, i * sizeof(SensorData), sizeof(SensorData));
// Upload data to Firebase
uploadRecordToFirebase(data);
}
// Clear FRAM
fram.erase();
Serial.println("Data uploaded to Firebase.");
}
void uploadRecordToFirebase(SensorData data) {
// Create a unique path for each record using the timestamp
String path = "/sensor_data/" + String(data.timestamp);
// Create a JSON object with the sensor data
FirebaseJson json;
json.set("timestamp", data.timestamp);
json.set("temperature", data.temperature);
json.set("humidity", data.humidity);
json.set("pressure", data.pressure);
// Upload data to Firebase
Firebase.set(path, json);
}
// Function to get the used bytes in FRAM
uint16_t framUsedBytes() {
uint16_t address = 0;
while (fram.read8(address) == 0xFF && address < framSize()) {
address++;
}
return address;
}
// Function to get the size of FRAM
uint16_t framSize() {
return fram.size();
}
// Data structure to hold sensor data
struct SensorData {
uint32_t timestamp;
float temperature;
float humidity;
float pressure;
};
Another possibility is to avoid the timeserver and to use an RTC such as the DS3231. At 3.6 Volt it uses 200uA when active and 110uA when not active. The Vcc range is +2.3 to +5.5Volt so at 3 Volt it will likely use a bit less. A program could look something like this:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <RTClib.h> // Library for DS3231 RTC
#include <Adafruit_FRAM_I2C.h>
#include <ESP8266WiFi.h>
#include <FirebaseESP8266.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
// Replace with your Firebase credentials
#define FIREBASE_HOST "your-firebase-project-id.firebaseio.com"
#define FIREBASE_AUTH "your-firebase-auth-token"
// Replace with your Wi-Fi credentials
const char *ssid = "your-SSID";
const char *password = "your-PASSWORD";
// Define the sleep duration in seconds (4 hours) Max sleep time is 72 min
// to make it sleep longer, check:
//https://arduinodiy.wordpress.com/2023/12/07/making-the-esp8266-sleep-for-longer-than-72-minutes/
const uint32_t SLEEP_DURATION = 4 * 60 * 60;
// Create an instance of the BME280 sensor
Adafruit_BME280 bme;
// Create an instance of the Adafruit FRAM library
Adafruit_FRAM_I2C fram = Adafruit_FRAM_I2C();
// Create an instance of the RTClib library for DS3231
RTC_DS3231 rtc;
// Create an instance of the NTP client
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org");
void setup() {
Serial.begin(115200);
Wire.begin();
// Initialize Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
// Initialize BME280 sensor
if (!bme.begin()) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
// Initialize DS3231 RTC
if (!rtc.begin()) {
Serial.println("Couldn't find RTC");
while (1);
}
// Initialize FRAM
if (!fram.begin()) {
Serial.println("Could not find a valid FRAM, check wiring!");
while (1);
}
// Connect to Firebase
Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
}
void loop() {
// Get current time from DS3231 RTC
DateTime now = rtc.now();
// Read sensor data
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0; // Pressure in hPa
// Store data in FRAM
writeDataToFram(now.unixtime(), temperature, humidity, pressure);
// Check if FRAM is full
if (framUsedBytes() >= framSize()) {
// Upload data to Firebase
uploadDataToFirebase();
}
// Deep sleep
ESP.deepSleep(SLEEP_DURATION * 1e6);
}
void writeDataToFram(uint32_t timestamp, float temperature, float humidity, float pressure) {
// Calculate the number of records stored
uint16_t recordNumber = framUsedBytes() / sizeof(SensorData);
// Create a data structure to hold sensor data
SensorData data;
data.timestamp = timestamp;
data.temperature = temperature;
data.humidity = humidity;
data.pressure = pressure;
// Write data to FRAM
fram.write((uint8_t*)&data, recordNumber * sizeof(SensorData), sizeof(SensorData));
}
void uploadDataToFirebase() {
// Calculate the number of records stored
uint16_t recordNumber = framUsedBytes() / sizeof(SensorData);
// Read data from FRAM and upload to Firebase
for (uint16_t i = 0; i < recordNumber; i++) {
SensorData data;
fram.read((uint8_t*)&data, i * sizeof(SensorData), sizeof(SensorData));
// Upload data to Firebase
uploadRecordToFirebase(data);
}
// Clear FRAM
fram.erase();
Serial.println("Data uploaded to Firebase.");
}
void uploadRecordToFirebase(SensorData data) {
// Create a unique path for each record using the timestamp
String path = "/sensor_data/" + String(data.timestamp);
// Create a JSON object with the sensor data
FirebaseJson json;
json.set("timestamp", data.timestamp);
json.set("temperature", data.temperature);
json.set("humidity", data.humidity);
json.set("pressure", data.pressure);
// Upload data to Firebase
Firebase.set(path, json);
}
// Function to get the used bytes in FRAM
uint16_t framUsedBytes() {
uint16_t address = 0;
while (fram.read8(address) == 0xFF && address < framSize()) {
address++;
}
return address;
}
// Function to get the size of FRAM
uint16_t framSize() {
return fram.size();
}
// Data structure to hold sensor data
struct SensorData {
uint32_t timestamp;
float temperature;
float humidity;
float pressure;
};
Mind you though that the two programs above are not as foolproof as they could be. The position to write a new record to after a wake up, is determined by the function ‘framUsedBytes()’. This function simply looks for code 0xFF that is stored in memory that is unused or after a clean up. It is also possible to store a recordnumber , either in the FRAM or in the ESP8266 NVRAM and retrieve that after a wake up and calculate the new position
Is it useful?
I think instinctively one can already say that having the ESP8266 make contact with a time server every to get the time, but not send the data yet, doesnt seem to make much sense.
That leaves the option with the RTC: So if the RTC is using <100uA in rest and the FRAM 27uA (lets say the total is 120uA) extra for 4 hrs in order to save ca 300mA for a short burst (say 2 secs), that is 480uAh vs 600mAs = 480uAh vs 0.167 mAh= 480uAh vs 167uAh. And this did not even include the extra energy used during the active phase of the added hardware.
That hardly seems to make sense from a battery saving point of view, it could be an interesting option though if no internet is around and one brings the device to an available internet from time to time
So what can we do
in order to make this more energy efficient (and thus useful) there are possibilities: Commenter Craig Larson describes a system he had on a pro mini in which a counter was indexed to work out the time.
it is also possible to use the internal clock of the ESP8266 and or store millis(), but that is all not very secure/accurate.
So, let’s try something that is a bit akin to what Craig did on the pro-mini (that had an ESP attached to it):
We only attach the FRAM memory to the ESP8266, no RTC.
On it’s start, we let the ESP8266 contact a time server only once and store that time in FRAM.
We then let the ESP8266 read the BME and store the values in FRAM with a recordnumber of ‘0’ attached to it. Then go to sleep for 4 hours.
On wake up, we do not read the time server again, but immediately read the BME280 and store the values as a new record, but now with the previous recordnumber increased by ‘1’.
Then when the FRAM is full (based on calculating the amount of records multiplied with the length of the records) the records are sent off to Firebase, but before sending, the recordnumber is replaced by the originally stored time from the server (which is in Unix format) and then 4 hours, multiplied by the recordnumber, is added to it.
Such a program should look something like this:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <MB85RC256V.h>
#define BME_SDA D2
#define BME_SCL D1
MB85RC256V fram;
Adafruit_BME280 bme;
const char* ssid = "your-ssid";
const char* password = "your-password";
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 0; // Your timezone offset in seconds
const int daylightOffset_sec = 0; // Daylight saving time offset in seconds
void connectWiFi() {
Serial.println("Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi...");
}
Serial.println("Connected to WiFi");
}
void sendToFirebase(int recordNumber, float temperature, float humidity, float pressure, time_t timestamp) {
// Implement Firebase data sending logic here
// Use the Firebase API or library of your choice
// Include the necessary headers and set up the connection
// Send the data to Firebase with the adjusted timestamp
}
void setup() {
//Serial.begin(115200);
Wire.begin(BME_SDA, BME_SCL);
if (!bme.begin()) {
Serial.println("Could not find a valid BME280 sensor, check wiring!");
while (1);
}
fram.begin();
if (!fram.begin()) {
//Serial.println("Could not find a valid FRAM chip, check wiring!");
while (1);
}
int recordNumber = fram.read(0);
if (recordNumber == -1) {
// First start, contact NTP server
connectWiFi();
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, ntpServer, gmtOffset_sec, daylightOffset_sec);
timeClient.begin();
time_t currentTime = timeClient.getEpochTime();
timeClient.end();
fram.write(0, currentTime);
// Read BME280 and store the initial record
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F; // Convert Pa to hPa
fram.write(4, recordNumber);
fram.writeFloat(8, temperature);
fram.writeFloat(12, humidity);
fram.writeFloat(16, pressure);
Serial.println("Initial record stored.");
}
// Enter deep sleep for 4 hours
//Serial.println("Going to sleep for 4 hours");// See Note *
ESP.deepSleep(4 * 60 * 60 * 1000000);
}
void loop() {
// This part won't be executed as the ESP8266 is in deep sleep
}
void wakeUpAndSendData() {
// Waking up from deep sleep
connectWiFi();
int recordNumber = fram.read(4);
recordNumber++;
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F; // Convert Pa to hPa
fram.write(4, recordNumber);
fram.writeFloat(8 + recordNumber * 16, temperature);
fram.writeFloat(12 + recordNumber * 16, humidity);
fram.writeFloat(16 + recordNumber * 16, pressure);
//Serial.println("Data recorded.");
if (recordNumber * 4 * 60 * 60 >= fram.capacity()) {
// FRAM memory full, send data to Firebase
time_t initialTime = fram.read(0);
for (int i = 0; i <= recordNumber; i++) {
float temp = fram.readFloat(8 + i * 16);
float hum = fram.readFloat(12 + i * 16);
float pres = fram.readFloat(16 + i * 16);
time_t timestamp = initialTime + i * 4 * 60 * 60;
sendToFirebase(i, temp, hum, pres, timestamp);
}
// Serial.println("Data sent to Firebase.");
// Clear FRAM memory after sending data
fram.format();
}
// Enter deep sleep for 4 hours
//Serial.println("Going to sleep for 4 hours");
ESP.deepSleep(4 * 60 * 60 * 1000000);// This is a simplification. See the note below
}
This program assumes you have a routine called ‘sendToFirebase()’ which may be different for various people (e.g. you may want to use the firebase library or not), or you may want to send it to another database (e.g. SQL or maybe just googlesheet).
So is the continuous use of 27uA worth the savings? 27uh over 4 hrs is 108uAh vs 167uAh, so roughly speaking one would save 60uAh. The downside is that you would have to wait long for the data. As the FRAM will be able to store some 2048 records, in which 6 records form a day, it will take 341 days before the data is sent. That may be a downside, but in some instances it might be an upside :-).
There are caveats with this program. What if the power suddenly fails? you may find your data over written. So you would have to build in some safeguards:
One could have the program on initial start check if there already is a valid unix date in the FRAM and then check till howfar the FRAM is already filled (going by the fact an empty memory is usually filled with 0xFF) and then take it from there.
Another option would be to use the MB85RS64V FRAM chip, as that uses only 10uA in standby. I’s memory capacity is only 8kByte so you would only be able to store 85 days of data, so your energy savings in the long run would be less, but you would have your data sooner.
Whether it is worth doing it is up to you.
* the max sleeptime on the ESP8266 is 72 minutes.(although some will still work with 3 hrs sleep time, but will have a drift). To make it sleep for 4 hrs, look here:
https://arduinodiy.wordpress.com/2023/12/07/making-the-esp8266-sleep-for-longer-than-72-minutes/