In this post I try to make my Arduino based TTN Nodes more power efficient.
Try to save energy
An Arduino Pro Mini with 3.3V consumes power. However, many do not know that you can already save a lot of electricity here!
Today I try to drastically reduce power consumption.
How to basically reduce power consumption, can be found under “Arduino Pro Mini 3.3V Low Power” at Google. I have already done everything. So unsolder LED and Regulator.
From 5mA normal you can reduce a lot:
- 1mA through the LED
- 0.5mA through the power regulator
Now we still have 3.5mA. This is too much.
Through the LowPower library we can put the Arduino into deep sleep, which is really impressive!
Theoretically we come down to 5uA, so 0.005mA!
Measurements
Today my new multimeter arrived 🙂
UNI-T UT61E, really nice!
So let me take the first measurement.
First I tried to use the built-in measurement with uAmA. However, it only show me errors and I quickly dodged on the already more accurate shunt method. For this one, take a resistance between battery and consumer.
So that the consumer still gets enough electricity, take as small as possible. If one takes too small, however, the voltage drop is too low and the multimeter gets only fractions of mV, which are then also inaccurate.
I first tried 10 ohms. However, the node was then in a permanent reset.
I then turned four 10 ohm resistors together so that I get to 2.5 ohms, in my case 2.6 ohms. Now it works fine!
If we measure 100mV, we divide it by 2.6 ohms. So 38mA.
Without LowPower-Mode
Without the LowPower library, the result is not so good:
9.88mV / 2.6 Ohm = 3.8mA
1 2 3 4 5 6 7 8 9 10 11 12 |
case EV_TXCOMPLETE: Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)")); if (LMIC.txrxFlags & TXRX_ACK) Serial.println(F("Received ack")); if (LMIC.dataLen) { Serial.println(F("Received ")); Serial.print(LMIC.dataLen); Serial.println(F(" bytes of payload")); } // Schedule next transmission os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send); break; |
With LowPower-Mode
0.1mV / 2.6 Ohm = 0.038mA = 38uA
We saved 100 times alone with this step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
case EV_TXCOMPLETE: Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)")); if (LMIC.txrxFlags & TXRX_ACK) Serial.println(F("Received ack")); if (LMIC.dataLen) { Serial.println(F("Received ")); Serial.print(LMIC.dataLen); Serial.println(F(" bytes of payload")); } // Schedule next transmission //os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send); do_send(&sendjob); for (int i=0; i<int(TX_INTERVAL/8); i++) { // Use library from https://github.com/rocketscream/Low-Power LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); } break; |
So, what’s the problem? Or do we have a problem at all?
3200mA / 0.01mAh = 320.000h = 36 years!!
Conclusion:
So all right!
Very satisfied 🙂
LowPower Test Node Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 |
/******************************************************************************* Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman Permission is hereby granted, free of charge, to anyone obtaining a copy of this document and accompanying files, to do whatever they want with them without any restriction, including, but not limited to, copying, modification and redistribution. NO WARRANTY OF ANY KIND IS PROVIDED. This example sends a valid LoRaWAN packet with payload "Hello, world!", using frequency and encryption settings matching those of the The Things Network. This uses ABP (Activation-by-personalisation), where a DevAddr and Session keys are preconfigured (unlike OTAA, where a DevEUI and application key is configured, while the DevAddr and session keys are assigned/generated in the over-the-air-activation procedure). Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in g1, 0.1% in g2), but not the TTN fair usage policy (which is probably violated by this sketch when left running for longer)! To use this sketch, first register your application and device with the things network, to set or generate a DevAddr, NwkSKey and AppSKey. Each device should have their own unique values for these fields. Do not forget to define the radio type correctly in config.h. *******************************************************************************/ #include <Arduino.h> #include <lmic.h> #include <hal/hal.h> #include <SPI.h> #include <Wire.h> #include <LowPower.h> // Enable debug prints to serial monitor #define MY_DEBUG int BATTERY_SENSE_PIN = A0; int BATTERY_FULL = 4.2; // LoRaWAN NwkSKey, network session key // This is the default Semtech key, which is used by the early prototype TTN // network. // DEVEUI und AppEUI LSB static const PROGMEM u1_t NWKSKEY[16] = { }; // LoRaWAN AppSKey, application session key // This is the default Semtech key, which is used by the early prototype TTN // network. // MSB static const u1_t PROGMEM APPSKEY[16] = { }; // LoRaWAN end-device address (DevAddr) static const u4_t DEVADDR = 0x00 // These callbacks are only used in over-the-air activation, so they are // left empty here (we cannot leave them out completely unless // DISABLE_JOIN is set in config.h, otherwise the linker will complain). void os_getArtEui (u1_t* buf) { } void os_getDevEui (u1_t* buf) { } void os_getDevKey (u1_t* buf) { } static uint8_t mydata[] = "Hello, world!"; static osjob_t sendjob; // Schedule TX every this many seconds (might become longer due to duty // cycle limitations). const unsigned TX_INTERVAL_MINUTES = 1; const unsigned TX_INTERVAL = 60 * TX_INTERVAL_MINUTES; // Pin mapping const lmic_pinmap lmic_pins = { .nss = 10, .rxtx = LMIC_UNUSED_PIN, .rst = 9, .dio = {4, 5, 7}, //DIO0, DIO1 and DIO2 connected }; void onEvent (ev_t ev) { Serial.print(os_getTime()); Serial.print(": "); switch (ev) { case EV_SCAN_TIMEOUT: Serial.println(F("EV_SCAN_TIMEOUT")); break; case EV_BEACON_FOUND: Serial.println(F("EV_BEACON_FOUND")); break; case EV_BEACON_MISSED: Serial.println(F("EV_BEACON_MISSED")); break; case EV_BEACON_TRACKED: Serial.println(F("EV_BEACON_TRACKED")); break; case EV_JOINING: Serial.println(F("EV_JOINING")); break; case EV_JOINED: Serial.println(F("EV_JOINED")); break; case EV_RFU1: Serial.println(F("EV_RFU1")); break; case EV_JOIN_FAILED: Serial.println(F("EV_JOIN_FAILED")); break; case EV_REJOIN_FAILED: Serial.println(F("EV_REJOIN_FAILED")); break; case EV_TXCOMPLETE: Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)")); if (LMIC.txrxFlags & TXRX_ACK) Serial.println(F("Received ack")); if (LMIC.dataLen) { Serial.println(F("Received ")); Serial.print(LMIC.dataLen); Serial.println(F(" bytes of payload")); } // Schedule next transmission //os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send); do_send(&sendjob); for (int i=0; i<int(TX_INTERVAL/8); i++) { // Use library from https://github.com/rocketscream/Low-Power LowPower.powerDown(SLEEP_8S, ADC_OFF, BOD_OFF); } break; case EV_LOST_TSYNC: Serial.println(F("EV_LOST_TSYNC")); break; case EV_RESET: Serial.println(F("EV_RESET")); break; case EV_RXCOMPLETE: // data received in ping slot Serial.println(F("EV_RXCOMPLETE")); break; case EV_LINK_DEAD: Serial.println(F("EV_LINK_DEAD")); break; case EV_LINK_ALIVE: Serial.println(F("EV_LINK_ALIVE")); break; default: Serial.println(F("Unknown event")); break; } } void do_send(osjob_t* j) { // Read sensor values and multiply by 100 to effectively keep 2 decimals // Signed 16 bits integer, -32767 up to +32767 int16_t t = 0; // Unsigned 16 bits integer, 0 up to 65535 uint16_t h = 0; // Unsigned 16 bits integer, 0 up to 65535 uint16_t b = readBat() * 100; byte buffer[6]; buffer[0] = t >> 8; buffer[1] = t; buffer[2] = h >> 8; buffer[3] = h; buffer[4] = b >> 8; buffer[5] = b; LMIC_setTxData2(1, buffer, sizeof(buffer), 0); Serial.println(""); Serial.print("Sending - temperature: "); Serial.print(t); Serial.print(", humidity: "); Serial.print(h); Serial.print(", battery: "); Serial.print(b); Serial.println(""); // Check if there is not a current TX/RX job running if (LMIC.opmode & OP_TXRXPEND) { Serial.println(F("OP_TXRXPEND, not sending")); } else { // Prepare upstream data transmission at the next possible time. LMIC_setTxData2(1, buffer, sizeof(buffer), 0); Serial.println(F("Packet queued")); } // Next TX is scheduled after TX_COMPLETE event. } void setup() { Serial.begin(115200); Serial.println(F("Starting")); // Fuer A0 Battery: analogReference(INTERNAL); #ifdef VCC_ENABLE // For Pinoccio Scout boards pinMode(VCC_ENABLE, OUTPUT); digitalWrite(VCC_ENABLE, HIGH); delay(1000); #endif // LMIC init os_init(); // Reset the MAC state. Session and pending data transfers will be discarded. LMIC_reset(); // Set static session parameters. Instead of dynamically establishing a session // by joining the network, precomputed session parameters are be provided. #ifdef PROGMEM // On AVR, these values are stored in flash and only copied to RAM // once. Copy them to a temporary buffer here, LMIC_setSession will // copy them into a buffer of its own again. uint8_t appskey[sizeof(APPSKEY)]; uint8_t nwkskey[sizeof(NWKSKEY)]; memcpy_P(appskey, APPSKEY, sizeof(APPSKEY)); memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY)); LMIC_setSession (0x1, DEVADDR, nwkskey, appskey); #else // If not running an AVR with PROGMEM, just use the arrays directly LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY); #endif #if defined(CFG_eu868) // Set up the channels used by the Things Network, which corresponds // to the defaults of most gateways. Without this, only three base // channels from the LoRaWAN specification are used, which certainly // works, so it is good for debugging, but can overload those // frequencies, so be sure to configure the full frequency range of // your network here (unless your network autoconfigures them). // Setting up channels should happen after LMIC_setSession, as that // configures the minimal channel set. // NA-US channels 0-71 are configured automatically // LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band // LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band // LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2-band LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(1, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI); // g-band LMIC_setupChannel(2, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(3, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(4, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(5, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(6, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(7, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7), BAND_CENTI); // g-band LMIC_setupChannel(8, 868100000, DR_RANGE_MAP(DR_FSK, DR_FSK), BAND_MILLI); // g2- // TTN defines an additional channel at 869.525Mhz using SF9 for class B // devices' ping slots. LMIC does not have an easy way to define set this // frequency and support for class B is spotty and untested, so this // frequency is not configured here. #elif defined(CFG_us915) // NA-US channels 0-71 are configured automatically // but only one group of 8 should (a subband) should be active // TTN recommends the second sub band, 1 in a zero based count. // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json LMIC_selectSubBand(1); #endif // Disable link check validation LMIC_setLinkCheckMode(0); // TTN uses SF9 for its RX2 window. LMIC.dn2Dr = DR_SF9; // Set data rate and transmit power for uplink (note: txpow seems to be ignored by the library) LMIC_setDrTxpow(DR_SF7, 14); // Start job do_send(&sendjob); } float readBat() { int sensorValue = analogRead(BATTERY_SENSE_PIN); #ifdef MY_DEBUG Serial.println(sensorValue); #endif // 1M, 470K divider across battery and using internal ADC ref of 1.1V // Sense point is bypassed with 0.1 uF cap to reduce noise at that point // ((1e6+470e3)/470e3)*1.1 = Vmax = 3.44 Volts // 3.44/1023 = Volts per bit = 0.003363075 // 2M, 470K 1.1V ref => 5,78V Max! // 5,78/1023 = 0.00565 float batteryV = sensorValue * 0.00565; #ifdef MY_DEBUG Serial.print("Battery Voltage: "); Serial.print(batteryV); Serial.println(" V"); #endif return batteryV; } void loop() { os_runloop_once(); } |
Hi Mario, this is great stuff! Thanks for posting. A lot of people are going to be really happy with this.
BTW: I love your 2.5 Ohm resistor 😉
Thx 🙂
Hi Mario, great article!
How did you connect the battery to the Arduino Pro mini 3.3v?
The battery, full charge, has 4.6V, removing the regulator
do you risk damaging the RFM95 module?
Thanks
I hava a LDO (MCP1700-330) between BAT out of Protection Circuit and Arduino, so i have perfect 3.3V. This LDO has only 1 uA Current instead of the Original Converter from the Arduino RAW Pin!
Nice day!
Thanks for the reply
but I still have a doubt …
also in your article “My first own TTN node with self designed PCB”,
in the components list there is “MCP1700-330 LDO (1.6uA, 250mAh max, 2.3-6V input)”, but in the “Affiliate links” it does not appear
sorry, but I do not know the component well
looking on the internet, I find the component in the TO-92-3 format
use the component individually, have you bought a small card that contains it?
do you have a reference link?
Sorry for the late reply. Here is the link to the product:
http://s.click.aliexpress.com/e/Zr33nAI
I also wrote it in the article on the PCB. Thank you 😉
So, how much power used your whole board in sleep mode ? (include LDO)
Hi, arround 40uA in Deepsleep 😉
Hello.
Thank a lot for that great tuto. It’s exactely what I need as I work with Arduino-lmci and a Feather MO Adaloger
Howerver,I sadly can not make it working.
When I compile my code, I got the following error:
‘class LowPowerClass’ has no member named ‘powerDown’
I double checked your example and I can not where is my error.
No object need to be ceated for LowPower?
I also read the following:
####Notes: External interrupt during standby on ATSAMD21G18A requires a patch to the Arduino SAMD Core in order for it to work.
This is not really clear for me as you have not specify it. Did you imported via the Board Manager and did nothing else??
Many thank
hello, I’ve never worked with the Adafruit boards before.
maybe the following library will help you:
https://github.com/rocketscream/Low-Power
Hi Mario,
Thank’s for the great work!
Hi have a doubt!
I tried to run the code on an Arduino Uno but the consumption remains high: 20mA in PowerDown and 40mA in transmission.
The only thing that I changed is the pin mapping:
// Pin mapping
const lmic_pinmap lmic_pins = {
.nss = 4,
.rxtx = LMIC_UNUSED_PIN,
.rst = 1,
.dio = {2, 3}, // DIO0, DIO1 and DIO2 connected
};
In your opinion, what can these high consumption depend on?
Hello and thank you again.
Silvano
Hello,
I think you did everything right.
Unfortunately the Arduino Uno is not very energy saving.
It consumes 50mA on average.
Your 20mA are relatively good.
Take an Arduino Pro Mini 3.3V
Greetings
Compared with the original code by 2015 Thomas Telkamp and Matthijs Kooijman, you seem to use different KEY names, why is that?
You use:
static const PROGMEM u1_t NWKSKEY[16], is this lsb static const u1_t DEVEUI[8] + static const u1_t APPEUI[8]?
Doing so my ProMini + RFM95 did not seem to function. In the original code there was an indicator LED to see when the joining process was going on. That would seem usefull.
Hello,
is it possible that you’re using an old version of the library?
I didn’t change anything.
Hi Mario,
I used https://github.com/matthijskooijman/arduino-lmic
Look at the example ttn-otaa.ino
There you see the keys for APPEUI[8] and DEVEUI[8] , while you seem to combine them into NWKSKEY[16]
Also in your code you use APPSKEY[16] instead of APPKEY[16]
Which library did you use?
I don’t see any attempts to lower the power of the LoRa circuitry. Is it powered down enough anyhow between transmissions?
Isn’t there a risk EV_TXCOMPLETE never occurs, and the Arduino therefore never enters power save?
Hello,
the radio chip doesn’t consume as much when it’s not transmitting or receiving.
You can also put the chip into sleep mode.
LMIC_shutdown();
After that the RFM95 would have to consume only 0.7uA. (0.2uA for the radio chip, 0.5uA for the rest on the HopeRF).
The case “EV_TXCOMPLETE” is completed.
It is regularly triggered. They wait for a reception. If this is not the case, it should send (do_send) and then sleep. Then EV_TXCOMPLETE is ready and starts again.
Greetings
Dear Mario
Your wrote:
You can also put the chip into sleep mode.
LMIC_shutdown();
For the next do_send, do we need to reinitialize the RFM95 module? or we just need to lunch the do_send function as if we would not have ran LMIC_shutdown()
Read here (2.5.19): https://www.developpez.net/forums/attachments/p195381d1450200851/environnements-developpement/delphi/web-reseau/reseau-objet-connecte-lorawan-delphi/lmic-v1.5.pdf/
“void LMIC_shutdown ()
Stop all MAC activity. Subsequently, the MAC needs to be reset via a call to LMIC_reset() and new
protocol actions need to be initiated.”
Hi, whats the lowest power it consumes when in RX mode? I read that you can set a receiver in a Duty-Cycle Receive Mode so it doenst constantly needs a lot of power: sleep -> wake up when rx signal -> sleep again etc
I need it for a battery powered receiver
Read here:
https://mariozwiers.de/2018/09/14/first-lora-node-the-real-power-consumption/
Hi Mario,
thanks for the great article! However I have one question: In your low-power example you commented out the rescheduling of your send job with (os_setTimedCallback(…)) and triggered it manually.
Did you get errors with this in the long run or does it send stable?
Richard
With some nodes i had problems on long run time. But whether that has something to do with it, I hardly think so.
If necessary, you build a software reset after x days…
Would you mind and share the decode for this data you are sending?
Sure,
function Decoder(bytes, port) {
var t = (bytes[0] & 0x80 ? 0xFFFF<<16 : 0) | bytes[0]<<8 | bytes[1]; var h = bytes[2]<<8 | bytes[3]; var b = bytes[4]<<8 | bytes[5]; return { temperature: t / 100, humidity: h / 100, battery: b / 100 } }