17 Apr 2023
A comparison of wireless protocols for battery powered embedded devices
Trying to sleep as much as possible
We've established that deep sleep on ESP32 boards gives us a runtime of ~4 up to ~15 years on a modest battery charge. This is plenty for my applications. So now we need to turn our attention towards the awake time of our sensor devices. This will mostly consist of collecting a measurment, sending it off to some system collecting the metrics and then going back to sleep.
To wirelessly transmit sensor readings to either a WiFi router or some custom base-station device, I can think of the following protocols: WiFi, Bluetooth low energy (BLE) and ESP-NOW.
WiFi is great because it allows the devices to connect directly to my WiFi router, no middle-man required. On a 2.4 GHz network it also has considerable range and can get signal through multiple walls. BLE on the other hand could get away with much less power consumption (although from what I read this is not particularly impressive on ESP32, and I'm too lazy to test it myself), at the expense of shorter range and less wall penetration.
The dark horse in the lineup is ESP-NOW. It's a proprietary protocol by espressif with some desirable properties for my use-cases: It's connectionless, which hopefully will shorten the time until a packet is sent, it allows broadcasting messages and it runs on the normal WiFi band meaning we inherit the range capabilities.
Both BLE and ESP-NOW would require a gateway device that receives messages and sends them off via WiFi to my monitoring setup in the cloud.
Boards under test
We measured power draw of four different development boards. Now that we know their characteristics (and I'm lazy), we will proceed to only consider two boards from here on out: The DFRobot FireBeetle ESP32 and the Seeed Studio XIAO ESP32C3. They represent a nice selection of the boards I own. The FireBeetle is the fully featured ESP32 board, with an ARM based chip with two cores, plenty of GPIO pins and a built-in battery connector. The XIAO on the other hand is tiny, more affordable and ships with an external antenna presumably due to a lack of space for a PCB antenna. It features the ESP32C3, a RISV-V based single core CPU.
Awake time is essential
For the sake of a rough estimation, we will assume that while awake the device consumes the amout of power we measured during WiFi setup. There is some overlap between the different boards at around 60 mA, so let's go with that.
Comparing this number to a deep sleep current of 10 - 40 uA it's easy to see that it's essential for battery life to spend as little time as possible awake. An additional second of active processing can shorten the possible sleep time by almost 2 hours!
For this reason we are going to measure how quickly a device can wake up from light or deep sleep, establish a connection (if necessary), send a small message with sensor readings and go back to sleep. Since the boot process of the chip is hard to time exactly, I figure the most effective way to measure the full cycle is to sleep only very briefly in-between cycles and check how many messages are received over time. If e.g. 2 messages reach the monitoring setup per second, then one full cycle takes 500 miliseconds. Or if only 1 message every 7 seconds arrives, clearly the cycles takes 7 second.
Boot time
First it would be interesting to time the boot time of a board in isolation. That way we will have a reference to know how much time we actually spend in a transmission and how much is just waking up.
To measure this I simply log some serial output, go to deep sleep and immediately wake up again and repeat. This shows around 7.5 wakeups per second on the Firebeetle ESP32 board, that gives a boot time of 133 ms. For wakeup after light sleep I'm getting around 1.4 ms. I couldn't test this on a Seeed XIAO because the serial monitor doesn't automatically reconnect, which makes it hard to observe the output. I'm sure this could be fixed if one wasn't a complete novice. I would assume it's not much different considering it has a chip in the same family. Once we get to the protocols we'll see if this assumption makes sense.
This is quite promising for deep sleep wakeup! If we can keep the Modem startup and transmission time on a reasonable scale we'll have pretty good battery life.
When we add the above snippet to the test script we measure a total cycle time of 312 ms. Starting the modem (at least I suspect this doesn't just set the mode considering the time increase) makes the startup process quite a bit slower.
But enough groundwork, let's get to trialing the first protocol.
WiFi
First let's test the obvious choice. We want to resume from deep sleep, start the modem, connect to the Access Point, send a packet to my metric collection system which is hosted in the cloud and then finally go back to sleep.
In the above snippet we send an incrementing counter to a
Prometheus PushGateway, that's where
this weird #TYPE some_metric gauge
syntax comes from. By letting this run for a couple of
minutes and then checking what count has been reached in Prometheus, we get the following timings
for a single wake-up cycle:
Mode | FireBeetle ESP32 | Seeed XIAO |
---|---|---|
Deep sleep | 4.5 s | 1.3 s |
Deep sleep + static IP | 4.4 s | 1.4 s |
Interestingly the XIAO is significantly faster in acquiring a connection to the access point and sending the data. I'm not sure if this is due to the ESP32-C3 chip or the external antenna that comes with this board. Either way, this distinctly shorter time to get data off device should result in some juicy battery savings in the long run.
I also ran these tests in my laundry room to check if these numbers are affected by physical distance to the AP and walls in-between. It turns out the numbers look exactly the same.
Static IP address (no DHCP)
I had read that we can cut the WiFi connection time short by providing a static IP, thus skipping DHCP entirely. To do this one simply configures values in the WiFi setup like so:
Interestingly this didn't do much at all, arguably just noise in the measurements. I'm not sure if I did something wrong or if there are other factors, such as the WiFi router being used etc.
Bluetooth Low Energy
Next, I was going to look at BLE. However, it quickly became apparent that this protocol is not a good fit for my purposes due to multiple reasons.
First, its range is very limited. No chance to reach my laundry room if it can't even penetrate one wall in my apartment. That's quite limiting and means for some applications I would need to reach for a different protocol. In that case I'd rather stick to one that fits all my needs.
Next, it's clearly designed with sensors in mind that are running continuously and regularly advertise a new reading, say a smart watch. When instead the beacon is sleeping most of the time, and then spins up a BLE server just to send one value, one can feel the resistance of the protocol. There is a lot more code required to make this happen than in the other protocols I tested.
Third, I found the reconnection logic to be fragile. Once the server boots and starts advertising, the client (in my case that would be the gateway which will relay a message to the internet) has to find it and connect to it, at which point the server can notify of a new value. This did not work as robustly as I would like with the client sometimes just not finding the server. This may well be my codes shortcoming of course, but I will count it against the protocol nonetheless, since WiFi and ESP-NOW put up less resistance.
So ultimately I did not proceed with timing the code, below you will find my server and client scripts anyway if you need inspiration.
BLE server code
BLE client code
ESP-NOW
If you're like me you know nothing about ESP-NOW, so let's explore it a bit first
ESP-NOW is designed for small messages up to 250 bytes. It can send messages to specific receivers or broadcast to anyone in listening range, without the need to establish a connection first. This is bound to give us an advantage when we try to minimize the time to ship data off device.
The first thing I was interested to know after getting a proof of concept to work was how high the message throughput is. The very first test when sending from the XIAO to the FireBeetle showed just 3 messages per second. Very disappointing! Curiously, when the device is connected to a serial monitor, it's much faster. It turns out that there is a bug in Arduino regarding buffering serial output. The fix is simple:
With this, I see around 800 messages per second at a message size of 16 bytes.
Another gotcha I encountered is how sensitive the antennas are to interference when the signal gets weak. For instance, when the sender or receiver is in my laundry room (1 floor down and a couple walls in between) the devices need to be kept well away of any metal object, otherwise no transmission will succeed. Even a metal baseplate that attaches legs to my dining room table are enough to block the signal from a device laying on the table. Ask me how I know.
In circumstances with weak signal strength it is also noticeable how sometimes a couple of retries are required. I would theorize this is due to WiFi interference on the 2.4 GHz band.
Let's get to testing ESP-NOW. The following script starts up, sets everything up to be able to
transmit data via ESP-NOW and then attempts to resend data until the recipient (in this case
the device with the mac address described in the receiver[]
array) acknowledges it. Then
the device enters deep sleep for a microsecond, wakes up and reruns the procedure.
The results are very promising:
Sleep mode | FireBeetle ESP32 | Seeed XIAO |
---|---|---|
Deep sleep | 416 ms | 172 ms |
Light sleep | 104 ms | 102 ms |
Now we have some end-to-end timings to gauge how much time we should expect for a full cycle of waking up, sending data and resuming sleep in this protocol. Remember that assumption above, that the XIAO probably takes the same amount of time to wake from sleep? Clearly that didn't pan out if we consider the numbers here.
Instead, the XIAO, or rather the ESP32-C3, is remarkably fast to wake from deep sleep and get that data out the door.
Beware some pitfalls of light and deep sleep
The loop is useless for the deep sleep test as upon wake up, the device starts executing
from the start, rerunning the setup. This is unintuitive if one expects the code to resume
from the invocation of esp_deep_sleep_start()
. However, it makes sense once we
consider that in this sleep mode just about everything is turned off to save power,
including the memory that stores program state. That's why you'll notice the counter to be
stored in RTC memory (via RTC_DATA_ATTR
), this makes sure it survives the deep sleep.
It took me some time to get this test to work with light sleep. The problem here is that
while the program state is persisted and the code resumes as you would expect, the modem is
powered down to save power. To be able to start it up properly, we need to call
esp_wifi_stop()
prior to starting sleep, otherwise you will not be able to send ESP-NOW
messages.
Conclusion
From the above tests it is clear that ESP-NOW is the winner. It's by far the fastest, which should allow us to conserve battery power. Further, it has as good a range as we can expect. While I initially thought of a gateway device that receives a measurement and sends it on towards the internet as a negative, I've since come around: The gateway saves time as the sensor device does not need to wait the round trip time to the cloud service, it can buffer results in case my access point is down and I could even augment the gateway device with a screen and show the freshest sensor values for my viewing pleasure.
It will be interesting to consider in a next post which scenario is a particularly good fit for each of these boards. The FireBeetle has particularly good deep sleep current, but is slower to wake up and send data, whereas the XIAO has 4 times higher deep sleep current but is significantly faster to wake up and send data. Naturally, the XIAO should do better the higher the required frequency of measurments.