Last month I wrote a series of posts about reducing the power consumption of the ESP8266, showing how I got the power consumption of a weather station down to 54 μAh per 5 minute reporting cycle.
At the end of that, I thought further improvements would be mainly tweaks and fine tuning, but Tobias wrote a comment explaining that it would be possible to avoid the initial network scan happening as part of the WiFi network association. I’m happy to say that it is possible, and it works quite well indeed.
Introducing the RTC and its memory
When the ESP8266 goes into deep sleep, a part of the chip called the RTC remains awake. Its power consumption is extremely low, so the ESP8266 does not use much power when in deep sleep. This is the component responsible for generating the wakeup signal when our deep sleep times out. That’s not the only benefit of the RTC, though. It also has its own memory, which we can actually read from and write to from the main ESP8266 processor.
WiFi quick connect
The WiFi.begin() method has an overload which accepts a WiFi channel number and a BSSID. By passing these, the ESP8266 will attempt to connect to a specific WiFi access point on the channel given, thereby eliminating the need to scan for an access point advertising the requested network.
How much time this will save depends on your network infrastructure, how many WiFi networks are active, how busy the network is and the signal strength. It WILL save time, though, time in which the WiFi radio is active and drawing power from the batteries.
Only problem is: How is the ESP8266 going to know which channel and BSSID to use when it wakes up, without doing a scan?
That’s where we can use the memory in the RTC. As the RTC remains powered during deep sleep, anything we write into its memory will still be there when the ESP wakes up again.
So, when the ESP8266 wakes up, we’ll read the RTC memory and check if we have a valid configuration to use. If we do, we’ll pass the additional parameters to WiFi.begin(). If we don’t, we’ll just connect as normal. As soon as the connection has been established, we can write the channel and BSSID into the RTC memory, ready for next time.
Reading and writing this memory is explained in the RTCUserMemory example in the Arduino board support package, and my code below is based on that example.
How to do it
First we’ll need to define a structure that we can store in the RTC memory. For the WiFi quick connect we only need to know the channel and BSSID. We also need a checksum or signature to help identify good or bad data, and as the RTCUserMemory example is using a CRC32 checksum, I am doing the same. CRC32 may be a bit overkill for such a small structure, and a signature word might have been more suitable. However, having the CRC32 in place now will make it easier to extend the structure in the future.
Define the structure at the global level of your sketch, so it is available from any method or function:
// The ESP8266 RTC memory is arranged into blocks of 4 bytes. The access methods read and write 4 bytes at a time, // so the RTC data structure should be padded to a 4-byte multiple. struct { uint32_t crc32; // 4 bytes uint8_t channel; // 1 byte, 5 in total uint8_t bssid[6]; // 6 bytes, 11 in total uint8_t padding; // 1 byte, 12 in total } rtcData;
The RTC memory is arranged into blocks of 4 bytes, so we pad the structure to a multiple of 4 bytes. We lead with the CRC32 checksum, followed by a single byte for the channel and 6 bytes of BSSID. (The BSSID is the MAC address of the WiFi access point.)
In the setup() method, before enabling the WiFi, add this code to read the settings from the RTC memory and validate the CRC32 checksum. If the settings are valid, we set the rtcValid flag.
// Try to read WiFi settings from RTC memory bool rtcValid = false; if( ESP.rtcUserMemoryRead( 0, (uint32_t*)&rtcData, sizeof( rtcData ) ) ) { // Calculate the CRC of what we just read from RTC memory, but skip the first 4 bytes as that's the checksum itself. uint32_t crc = calculateCRC32( ((uint8_t*)&rtcData) + 4, sizeof( rtcData ) - 4 ); if( crc == rtcData.crc32 ) { rtcValid = true; } }
Next, replace the call to WiFi.begin() with the following:
if( rtcValid ) { // The RTC data was good, make a quick connection WiFi.begin( WLAN_SSID, WLAN_PASSWD, rtcData.channel, rtcData.ap_mac, true ); } else { // The RTC data was not valid, so make a regular connection WiFi.begin( WLAN_SSID, WLAN_PASSWD ); }
Quite simple, we check to see if we have valid data from RTC and if we do, we pass the additional information to WiFi.begin(). Otherwise, just do a normal connection.
The loop waiting for the WiFi connection to be established becomes a little more complicated as we’ll have to consider the possibility that the WiFi access point has changed channels, or that the access point itself has been changed. So, if we haven’t established a connection after a certain number of loops, we’ll reset the WiFi and try again with a normal connection. If, after 30 seconds, we still don’t have a connection we’ll go back to sleep and try again the next time we wake up. After all, it could be that the WiFi network is temporarily unavailable and we don’t want to stay awake until it comes back. Better to go back to sleep and hope things are better in the morning…
int retries = 0; int wifiStatus = WiFi.status(); while( wifiStatus != WL_CONNECTED ) { retries++; if( retries == 100 ) { // Quick connect is not working, reset WiFi and try regular connection WiFi.disconnect(); delay( 10 ); WiFi.forceSleepBegin(); delay( 10 ); WiFi.forceSleepWake(); delay( 10 ); WiFi.begin( WLAN_SSID, WLAN_PASSWD ); } if( retries == 600 ) { // Giving up after 30 seconds and going back to sleep WiFi.disconnect( true ); delay( 1 ); WiFi.mode( WIFI_OFF ); ESP.deepSleep( SLEEPTIME, WAKE_RF_DISABLED ); return; // Not expecting this to be called, the previous call will never return. } delay( 50 ); wifiStatus = WiFi.status(); }
Once the WiFi is connected, we can get the channel and BSSID and stuff it into the RTC memory, ready for the next time we wake up.
// Write current connection info back to RTC rtcData.channel = WiFi.channel(); memcpy( rtcData.ap_mac, WiFi.BSSID(), 6 ); // Copy 6 bytes of BSSID (AP's MAC address) rtcData.crc32 = calculateCRC32( ((uint8_t*)&rtcData) + 4, sizeof( rtcData ) - 4 ); ESP.rtcUserMemoryWrite( 0, (uint32_t*)&rtcData, sizeof( rtcData ) );
And the calculateCRC32() method:
uint32_t calculateCRC32( const uint8_t *data, size_t length ) { uint32_t crc = 0xffffffff; while( length-- ) { uint8_t c = *data++; for( uint32_t i = 0x80; i > 0; i >>= 1 ) { bool bit = crc & 0x80000000; if( c & i ) { bit = !bit; } crc <<= 1; if( bit ) { crc ^= 0x04c11db7; } } } return crc; }
Results
With this change, the wake time per reporting cycle has been reduced by 1 second, and the total power consumption is now down to 42.6μAh, 74% down from the starting point.
Many thanks to Tobias for pointing me in this direction.
Hi,
May I have yours contacts? Both you and Tobias?
I’d like to know if it’s thinkable to have a battery primary cell device that wake up every 5 minutes to send data over wifi?
Which battery life may we expect?
Thanks a lot in advance,
Romain DEQUIDT
Rtone.fr
Hi, there.
I can’t share any contact details on the blog, unfortunately. However I can confirm that I am getting more than 2 months life out of a 3xAA cell device. (It’s been 2 months since I replaced the batteries last time, and the supply voltage is still just a hair under 4.2 volts. This is the first run of this device generation, so I don’t know yet how long it will last, but the way the batteries drain now, I’d expect at least another 2 weeks.
This device wakes up every 5 minutes to read a DHT22 and BMP280, then transmit the readings to a server over MQTT.
I have found that the battery life is mostly affected by how long it takes to transmit the results to the MQTT server, so on a busy network you’re not likely to see results anywhere near 2 weeks. I have a completely separate Wifi network for my IoT devices, mainly to keep them separated from Internet, but it also helps in reducing network traffic and thus power consumption.