Instrumenting Contiki NG applications with energy usage estimation
See also: the documentation of the Energest API at doc:energest.
See also: the performance benchmark and visualization example at examples/benchmarks/result-visualization/run-analysis.py
Contiki-NG includes Energest module which can be used to implement lightweight, software-based energy estimation approach for resource-constrained IoT devices. The Energest module accounts the time a system has spent in various states. Using this information together with a system’s hardware power consumption model, the developer can estimate the energy consumption of the system.
Using the Energest Module
Contiki-NG comes with a system service called simple-energest
. Using it is the fastest way to get started.
To enable the simple-energest
service, add this line to an application’s Makefile
:
MODULES += os/services/simple-energest
Energest Module capabilities
There are five predefined Energest states:
Type |
Purpose |
---|---|
|
The CPU is active. |
|
The CPU is in low power mode. |
|
The CPU is in deep low power mode. |
|
The radio is transmitting. |
|
The radio is listening. |
Most Contiki-NG platforms support tracking of all of these state. However, there are exceptions, for example, the Texas Instruments simplelink
account all low-power modes under ENERGEST_TYPE_LPM
because its not possible to distinguish between the low-power modes without modifying the Texas Instruments core SDK.
Interpreting Simple Energest output
Once the Simple Energest service is enabled by setting the MODULES
variable in the Makefile
, it will print a summary message once per minute. An example message on an emulated Zolertia Z1 node:
[INFO: Energest ] --- Period summary #2 (60 seconds)
[INFO: Energest ] Total time : 1966080
[INFO: Energest ] CPU : 10374/ 1966080 (5 permil)
[INFO: Energest ] LPM : 1955706/ 1966080 (994 permil)
[INFO: Energest ] Deep LPM : 0/ 1966080 (0 permil)
[INFO: Energest ] Radio Tx : 106/ 1966080 (0 permil)
[INFO: Energest ] Radio Rx : 104802/ 1966080 (53 permil)
[INFO: Energest ] Radio total : 104908/ 1966080 (53 permil)
The fields are:
Period summary #2 (60 seconds)
- #2 is the sequence number of the accounting period i.e. since the last printout. 60 seconds is the duration of the period.Total time : 1966080
- the number here refers to the total number of ticks in the accounting period (60 seconds). As the value of theRTIMER_ARCH_SECOND
constant on the Z1 platform is equal to 32768, we can expect 32768 multiplied by the number of seconds. 32768 * 60 = 1966080.CPU : 10374/ 1966080 (5 permil)
- the first number10374
here refers to the number of ticks spent in the CPU active mode.5 permil
is the approximate proportion of time spent in this state. It is equal to 10374 divided by 1966080, subject to rounding errors. 1 permil is equal to 0.1 percent.
The rest of the fields follow the same template.
Platform’s current consumption model
In order to convert these ticks into useful values, a platform-specific current consumption model is necessary. In short, if you know how much the platform is consuming in each state, you can use the Energest output to convert the ticks to useful numbers.
Luckily, the Zolertia Z1 platform provides current consumption values of its main components in its datasheet. (Caveat: datasheet values not always are trustworthy, it is better to do your own measurements!)
The low-power mode on this platform in Contiki-NG corresponds to CPU being in the standby mode (0.5 uA current consumption from the datasheet) and the various other components being in Power Down or standby modes (20 uA for radio, >2 uA for all other components together). Summing up, we can approximate that with 23 uA current consumption. The rest of modes can be taken directly from the datasheet.
State |
Current consumption |
---|---|
CPU active |
10 mA |
CPU low power mode |
23 uA |
Radio Rx |
18.8 mA |
Radio Tx |
17.4 mA |
Estimating charge consumption and energy consumption
Let’s assume that these variable are already known:
ticks
- the number of ticks spent in a stateRTIMER_ARCH_SECOND
- the number of ticks per secondcurrent_mA
- the current consumption in that state in mAvoltage
- the voltage provided by the system to the component (radio or CPU)period_sec
- the duration of the accounting period in secondsperiod_ticks
- the duration of the accounting period in period_ticks
The following metrics can now be computed for the state during the measurement period:
average current consumption (in milliamperes, mA)
state_avg_current_mA = (ticks * current_mA) / (RTIMER_ARCH_SECOND * period_sec) = (ticks * current_mA) / period_ticks
charge consumption (in millicoulumbs, mC)
state_charge_mC = (ticks * current_mA) / RTIMER_ARCH_SECOND
average power consumption (in milliwats, mW)
state_power_mW = avg_current_mA * voltage
energy consumption (in millijoules, mJ)
state_energy_mJ = state_charge_mC * voltage
or, alternatively:
state_energy_mJ = state_power_mW * period_sec
Total values for the whole system can be obtained by simply summing the values of all tracked states.
Tip: that despite the name “Energest”, the most useful metric is in fact the charge consumption. The energy consumption depends on the voltage, which may change over time (for example, when the battery discharges) and depends on platform-specific design decisions not under control of Contiki-NG application developer (for example, the operating voltage, which may be different from the voltage provided by the battery).
Tip: it’s often more useful to express charge consumption in milliampere hours (mAh), as batteries typically report this metric. A millicoulumb (mC) is simply a milliampere second. To convert between the values:
value_mAh = value_mC / 3600.0
value_mC = value_mAh * 3600.0
Code example
One code example that interprets the Simple Energest output can be found in https://github.com/contiki-ng/contiki-ng/blob/develop/examples/benchmarks/result-visualization/run-analysis.py Here we show how to construct another one.
Let’s assume that you have a COOJA.testlog
file formatted like this:
1107305 2 [INFO: Main ] Starting Contiki-NG-release/v4.5-149-g1c0b472-dirty
1110081 2 [INFO: Main ] - Routing: RPL Lite
1112715 2 [INFO: Main ] - Net: sicslowpan
1114921 2 [INFO: Main ] - MAC: TSCH
1118219 2 [INFO: Main ] - 802.15.4 PANID: 0x81a5
1123248 2 [INFO: Main ] - 802.15.4 TSCH default hopping sequence length: 4
1125408 2 [INFO: Main ] Node ID: 2
Here is a Python code that parses the log output, computes the summary metrics for each node and prints them:
INPUT_FILE = "COOJA.testlog"
# From Z1 node datasheet
CURRENT_MA = {
"CPU" : 10,
"LPM" : 0.023,
"Deep LPM" : 0, # not used by Z1 nodes
"Radio Rx" : 18.8,
"Radio Tx" : 17.4,
}
STATES = list(CURRENT_MA.keys())
VOLTAGE = 3.0 # assume 3 volt batteries
RTIMER_ARCH_SECOND = 32768
def main():
node_ticks = {}
node_total_ticks = {}
with open(INPUT_FILE, "r") as f:
for line in f:
if "INFO: Energest" not in line:
continue
fields = line.split()
try:
node = int(fields[1])
except:
continue
if node not in node_ticks:
# initialize to zero
node_ticks[node] = { u : 0 for u in STATES }
node_total_ticks[node] = 0
try:
state_index = 5
state = fields[state_index]
tick_index = state_index + 2
if state not in STATES:
state = fields[state_index] + " " + fields[state_index+1]
tick_index += 1
if state not in STATES:
# add to the total time
if state == "Total time":
node_total_ticks[node] += int(fields[tick_index])
continue
# add to the time spent in specific state
ticks = int(fields[tick_index][:-1])
node_ticks[node][state] += ticks
except Exception as ex:
print("Failed to process line '{}': {}".format(line, ex))
nodes = sorted(node_ticks.keys())
for node in nodes:
total_avg_current_mA = 0
period_ticks = node_total_ticks[node]
period_seconds = period_ticks / RTIMER_ARCH_SECOND
for state in STATES:
ticks = node_ticks[node].get(state, 0)
current_mA = CURRENT_MA[state]
state_avg_current_mA = ticks * current_mA / period_ticks
total_avg_current_mA += state_avg_current_mA
total_charge_mC = period_ticks * total_avg_current_mA / RTIMER_ARCH_SECOND
total_energy_mJ = total_charge_mC * VOLTAGE
print("Node {}: {:.2f} mC ({:.3f} mAh) charge consumption, {:.2f} mJ energy consumption in {:.2f} seconds".format(
node, total_charge_mC, total_charge_mC / 3600.0, total_energy_mJ, period_seconds))
if __name__ == "__main__":
main()
Example output:
Node 1: 3917.93 mC (1.088 mAh) charge consumption, 11753.79 mJ energy consumption in 3599.99 seconds
Node 2: 5699.00 mC (1.583 mAh) charge consumption, 17097.01 mJ energy consumption in 3599.99 seconds
The code can be easily adapted to log files generated by real nodes in a testbed.