Skip to content

Commit 829c4df

Browse files
committed
Low power version release 0.1
1 parent 23a60e2 commit 829c4df

File tree

4 files changed

+279
-113
lines changed

4 files changed

+279
-113
lines changed

fast_io/core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ def get_event_loop(runq_len=16, waitq_len=16, ioq_len=0):
264264
_event_loop = _event_loop_class(runq_len, waitq_len, ioq_len)
265265
return _event_loop
266266

267+
# Allow user classes to determine prior event loop instantiation.
268+
def got_event_loop():
269+
return _event_loop is not None
270+
267271
def sleep(secs):
268272
yield int(secs * 1000)
269273

lowpower/README.md

Lines changed: 195 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
1-
# An experimental low power usayncio adaptation
1+
# A low power usayncio adaptation
2+
3+
Release 0.1 25th July 2018
24

35
# 1. Introduction
46

57
This adaptation is specific to the Pyboard and compatible platforms, namely
6-
those capable of running the `pyb` module. This module supports two low power
7-
modes `standby` and `stop`
8+
those capable of running the `pyb` module; this supports two low power modes
9+
`standby` and `stop`
810
[see docs](http://docs.micropython.org/en/latest/pyboard/library/pyb.html).
911

1012
Use of `standby` is simple in concept: the application runs and issues
1113
`standby`. The board goes into a very low power mode until it is woken by one
12-
of a limited set of external events, when it behaves similarly as after a hard
14+
of a limited set of external events, when it behaves similarly to after a hard
1315
reset. In that respect a `uasyncio` application is no different from any other.
1416
If the application can cope with the fact that execution state is lost during
1517
the delay, it will correctly resume.
1618

1719
This adaptation modifies `uasyncio` such that it can enter `stop` mode for much
18-
of the time, reducing power consumption. The two approaches can be combined,
19-
with a device waking from `shutdown` to run a low power `uasyncio` application
20-
before again entering `shutdown`.
20+
of the time, minimising power consumption while retaining state. The two
21+
approaches can be combined, with a device waking from `shutdown` to run a low
22+
power `uasyncio` application before again entering `shutdown`.
2123

2224
The adaptation trades a reduction in scheduling performance for a substantial
23-
reduction in power consumption.
25+
reduction in power consumption. This tradeoff can be dynamically altered at
26+
runtime. An application can wait with low power consumption for a trigger such
27+
as a button push. Or it could periodically self-trigger by issuing
28+
`await ayncio.sleep(long_time)`. For the duration of running the scheduler
29+
latency can be reduced to improve performance at the cost of temporarily
30+
higher power consumption, with the code reverting to low power mode while
31+
waiting for a new trigger.
2432

2533
Some general notes on low power Pyboard applications may be found
2634
[here](https://github.com/peterhinch/micropython-micropower).
@@ -30,70 +38,83 @@ Some general notes on low power Pyboard applications may be found
3038
Ensure that the version of `uasyncio` in this repository is installed and
3139
tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`.
3240

41+
## 2.1 Files
42+
43+
* `rtc_time.py` Low power library.
44+
* `lpdemo.py` A basic application which waits for a pushbutton to be pressed
45+
before running. A second button press terminates it. While "off" and waiting
46+
very low power is consumed. A normally open pushbutton should be connected
47+
between `X1` and `Gnd`. This program is intended as a basic template for
48+
similar applications.
49+
* `lowpower.py` Send and receive messages on UART4, echoing received messages
50+
to UART2 at a different baudrate. This consumes about 1.4mA and serves to
51+
demonstrate that interrupt-driven devices operate correctly.
52+
3353
The test program `lowpower.py` requires a link between pins X1 and X2 to enable
34-
UART 4 to operate via a loopback.
54+
UART 4 to receive data via a loopback.
3555

3656
# 3 Low power uasyncio operation
3757

3858
## 3.1 The official uasyncio package
3959

4060
The official `uasyncio` library is unsuited to low power operation for two
41-
reasons. Firstly because of its method of I/O polling. In periods when no coro
42-
is ready for execution, it determines the time when the most current coro will
61+
reasons. Firstly because of its method of I/O polling. In periods when no task
62+
is ready for execution, it determines the time when the most current task will
4363
be ready to run. It then calls `select.poll`'s `ipoll` method with a timeout
4464
calculated on that basis. This consumes power.
4565

4666
The second issue is that it uses `utime`'s millisecond timing utilities for
4767
timing. This ensures portability across MicroPython platforms. Unfortunately on
4868
the Pyboard the clock responsible for `utime` stops for the duration of
49-
`pyb.stop()`. This would cause all `uasyncio` timing to become highly
50-
inaccurate.
69+
`pyb.stop()`. An application-level scheme using `pyb.stop` to conserve power
70+
would cause all `uasyncio` timing to become highly inaccurate.
5171

5272
## 3.2 The low power adaptation
5373

5474
If running on a Pyboard the version of `uasyncio` in this repo attempts to
5575
import the file `rtc_time.py`. If this succeeds and there is no USB connection
5676
to the board it derives its millisecond timing from the RTC; this continues to
57-
run through `stop`.
77+
run through `stop`. Libraries using `uasyncio` will run unmodified, barring any
78+
timing issues if user code increases scheduler latency.
5879

5980
To avoid the power drain caused by `select.poll` the user code must issue the
6081
following:
6182

6283
```python
63-
loop = asyncio.get_event_loop()
64-
loop.create_task(rtc_time.lo_power(t))
65-
```
66-
67-
This coro has a continuously running loop that executes `pyb.stop` before
68-
yielding with a zero delay:
69-
70-
```python
71-
def lo_power(t_ms):
72-
rtc.wakeup(t_ms)
73-
while True:
74-
pyb.stop()
75-
yield
84+
try:
85+
if asyncio.version != 'fast_io':
86+
raise AttributeError
87+
except AttributeError:
88+
raise OSError('This requires fast_io fork of uasyncio.')
89+
import rtc_time
90+
# Instantiate event loop with any args before running code that uses it
91+
loop = asyncio.get_event_loop()
92+
rtc_time.Latency(100) # Define latency in ms
7693
```
7794

78-
The design of the scheduler is such that, if at least one coro is pending with
79-
a zero delay, polling will occur with a zero delay. This minimises power draw.
80-
The significance of the `t` argument is detailed below.
95+
The `Latency` class has a continuously running loop that executes `pyb.stop`
96+
before yielding with a zero delay. The duration of the `stop` condition
97+
(`latency`) can be dynamically varied. If zeroed the scheduler will run at
98+
full speed. The `yield` allows each pending task to run once before the
99+
scheduler is again paused (if `latency` > 0).
81100

82101
### 3.2.1 Consequences of pyb.stop
83102

84-
#### 3.2.1.1 Timing Accuracy
103+
#### 3.2.1.1 Timing Accuracy and rollover
85104

86105
A minor limitation is that the Pyboard RTC cannot resolve times of less than
87106
4ms so there is a theoretical reduction in the accuracy of delays. In practice,
88107
as explained in the [tutorial](../TUTORIAL.md), issuing
89108

90109
```python
91-
await asyncio.sleep_ms(t)
110+
await asyncio.sleep_ms(t)
92111
```
93112

94113
specifies a minimum delay: the maximum may be substantially higher depending on
95-
the behaviour of other coroutines. The latency implicit in the `lo_power` coro
96-
(see section 5.2) makes this issue largely academic.
114+
the behaviour of other tasks.
115+
116+
RTC time rollover is at 7 days. The maximum supported `asyncio.sleep()` value
117+
is 302399999 seconds (3.5 days - 1s).
97118

98119
#### 3.2.1.2 USB
99120

@@ -106,41 +127,109 @@ from a separate power source for power measurements.
106127
Applications can detect which timebase is in use by issuing:
107128

108129
```python
130+
try:
131+
if asyncio.version != 'fast_io':
132+
raise AttributeError
133+
except AttributeError:
134+
raise OSError('This requires fast_io fork of uasyncio.')
109135
import rtc_time
110136
if rtc_time.use_utime:
111137
# Timebase is utime: either a USB connection exists or not a Pyboard
112138
else:
113139
# Running on RTC timebase with no USB connection
114140
```
115141

116-
# 4. rtc_time.py
142+
Debugging at low power is facilitated by using `pyb.repl_uart` with an FTDI
143+
adaptor.
144+
145+
### 3.2.2 Measured results
146+
147+
The `lpdemo.py` script consumes a mean current of 980μA with 100ms latency, and
148+
730μA with 200ms latency, while awaiting a button press.
149+
150+
The following script consumes about 380μA between wakeups (usb is disabled in
151+
`boot.py`):
152+
153+
```python
154+
import pyb
155+
for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XY']:
156+
pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP)
157+
rtc = pyb.RTC()
158+
rtc.wakeup(10000)
159+
while True:
160+
pyb.stop()
161+
```
162+
163+
This accords with the 500μA maximum specification for `stop`. So current
164+
consumption can be estimated by `i = ib + n/latency` where `ib` is the stopped
165+
current (in my case 380μA) and `n` is a factor dependent on the amount of code
166+
which runs when the latency period expires. A data logging application might
167+
tolerate latencies of many seconds while waiting for a long delay to expire:
168+
getting close to `ib` may be practicable for such applications during their
169+
waiting period.
170+
171+
# 4. The rtc_time module
117172

118173
This provides the following.
119174

120175
Variable:
121176
* `use_utime` `True` if the `uasyncio` timebase is `utime`, `False` if it is
122-
the RTC.
177+
the RTC. Treat as read-only.
123178

124179
Functions:
125180
If the timebase is `utime` these are references to the corresponding `utime`
126181
functions. Otherwise they are direct replacements but using the RTC as their
127-
timebase. See the `utime` official documentation for these.
182+
timebase. See the `utime` official
183+
[documentation](http://docs.micropython.org/en/latest/pyboard/library/utime.html)
184+
for these.
128185
* `ticks_ms`
129186
* `ticks_add`
130187
* `ticks_diff`
131-
* `sleep_ms` This should not be used if the RTC timebase is in use as its
132-
usage of the RTC will conflict with the `lo_power` coro.
133188

134-
Coroutine:
135-
* `lo_power` Argument: `t_ms`. This coro repeatedly issues `pyb.stop`, waking
136-
after `t_ms` ms. The higher `t_ms` is, the greater the latency experienced by
137-
other coros and by I/O. Smaller values may result in higher power consumption
138-
with other coros being scheduled more frequently.
189+
It also exposes `sleep_ms`. This is always a reference to `utime.sleep_ms`. The
190+
reason is explained in the code comments. It is recommended to use the `utime`
191+
method explicitly if needed.
192+
193+
Latency Class:
194+
* Constructor: Positional arg `t_ms=100`. Period for which the scheduler
195+
enters `stop` i.e. initial latency period.
196+
* Method: `value` Arg `val=None`. Controls period for which scheduler stops.
197+
It returns the period in ms. If the default `None` is passed the value is
198+
unchanged. If 0 is passed the scheduler runs at full speed. A value > 0 sets
199+
the stop period in ms.
200+
201+
The higher the value, the greater the latency experienced by other tasks and
202+
by I/O. Smaller values will result in higher power consumption with other tasks
203+
being scheduled more frequently.
204+
205+
The class is a singleton consequently there is no need to pass an instance
206+
around or to make it global. Once instantiated, latency may be changed by
207+
208+
```python
209+
rtc_time.Latency().value(t)
210+
```
139211

140212
# 5. Application design
141213

142214
Attention to detail is required to minimise power consumption, both in terms of
143-
hardware and code.
215+
hardware and code. The only *required* change to application code is to add
216+
217+
```python
218+
try:
219+
if asyncio.version != 'fast_io':
220+
raise AttributeError
221+
except AttributeError:
222+
raise OSError('This requires fast_io fork of uasyncio.')
223+
# Do this import before configuring any pins or I/O:
224+
import rtc_time
225+
# Instantiate event loop with any args before running code that uses it:
226+
loop = asyncio.get_event_loop()
227+
lp = rtc_time.Latency(100) # Define latency in ms
228+
# Run application code
229+
```
230+
231+
However optimising the power draw/performance tradeoff benefits from further
232+
optimisations.
144233

145234
## 5.1 Hardware
146235

@@ -150,24 +239,38 @@ consumption use the onboard flash memory. Peripherals usually consume power
150239
even when not in use: consider switching their power source under program
151240
control.
152241

242+
Floating Pyboard I/O pins can consume power. Further there are 4.7KΩ pullups on
243+
the I2C pins. The `rtc_time` module sets all pins as inputs with internal
244+
pullups. The application should then reconfigure any pins which are to be used.
245+
If I2C is to be used there are further implications: see the above reference.
246+
153247
## 5.2 Application Code
154248

155-
Issuing `pyb.stop` directly in code is unwise; also, when using the RTC
156-
timebase, calling `rtc_time.sleep_ms`. This is because there is only one RTC,
157-
and hence there is potential conflict with different routines issuing
158-
`rtc.wakeup`. The coro `rtc_time.lo_power` should be the only one issuing this
159-
call.
249+
The Pyboard has only one RTC and the `Latency` class needs sole use of
250+
`pyb.stop` and `rtc.wakeup`; these functions should not be used in application
251+
code. Setting the RTC at runtime is likely to be problematic: the effect on
252+
scheduling can be assumed to be malign. If required, the RTC should be set
253+
prior to instantiating the event loop.
254+
255+
For short delays use `utime.sleep_ms` or `utime.sleep_us`. Such delays use
256+
power and hog execution preventing other tasks from running.
257+
258+
A task only consumes power when it runs: power may be reduced by using larger
259+
values of `t` in
260+
261+
```python
262+
await asyncio.sleep(t)
263+
```
160264

161-
The implications of the `t_ms` argument to `rtc_time.lo_power` should be
162-
considered. During periods when the Pyboard is in a `stop` state, other coros
265+
The implications of the time value of the `Latency` instance should be
266+
considered. During periods when the Pyboard is in a `stop` state, other tasks
163267
will not be scheduled. I/O from interrupt driven devices such as UARTs will be
164268
buffered for processing when stream I/O is next scheduled. The size of buffers
165-
needs to be determined in conjunction with data rates and with this latency
166-
period.
269+
needs to be determined in conjunction with data rates and the latency period.
167270

168-
Long values of `t_ms` will affect the minimum time delays which can be expected
169-
of `await asyncio.sleep_ms`. Such values will affect the aggregate amount of
170-
CPU time any coro will acquire. If `t_ms == 200` the coro
271+
Long values of latency affect the minimum time delays which can be expected of
272+
`await asyncio.sleep_ms`. Such values will affect the aggregate amount of CPU
273+
time any task will acquire in any period. If latency is 200ms the task
171274

172275
```python
173276
async def foo():
@@ -176,8 +279,8 @@ async def foo():
176279
await asyncio.sleep(0)
177280
```
178281

179-
will execute (at best) at a rate of 5Hz. And possibly considerably less
180-
frequently depending on the behaviour of competing coros. Likewise
282+
will execute (at best) at a rate of 5Hz; possibly considerably less frequently
283+
depending on the behaviour of competing tasks. Likewise
181284

182285
```python
183286
async def bar():
@@ -186,5 +289,36 @@ async def bar():
186289
await asyncio.sleep_ms(10)
187290
```
188291

189-
the 10ms sleep may be 200ms or longer, again dependent on other application
190-
code.
292+
the 10ms sleep will be >=200ms dependent on other application tasks.
293+
294+
Latency may be changed dynamically by using the `value` method of the `Latency`
295+
instance. A typical application (as in `lpdemo.py`) might wait on a "Start"
296+
button with a high latency value, before running the application code with a
297+
lower (or zero) latency. On completion it could revert to waiting for "Start"
298+
with high latency to conserve battery.
299+
300+
# 6. Note on the design
301+
302+
The `rtc_time` module represents a compromise designed to minimise changes to
303+
`uasyncio`. The aim is to have zero effect on the performance of normal
304+
applications or code running on non-Pyboard hardware.
305+
306+
An alternative approach is to modify the `PollEventLoop` `wait` method to
307+
invoke `stop` conditions when required. It would have the advantage of removing
308+
the impact of latency on `sleep_ms` times. It proved rather involved and was
309+
abandoned on the grounds of its impact on performance of normal applications.
310+
Despite its name, `.wait` is time-critical in the common case of a zero delay;
311+
increased code is best avoided.
312+
313+
The approach used ensures that there is always at least one task waiting on a
314+
zero delay. This guarantees that `PollEventLoop` `wait` is always called with a
315+
zero delay: consequently `self.poller.ipoll(delay, 1)` will always return
316+
immediately minimising power consumption. Consequently there is no change to
317+
the design of the scheduler beyond the use of a different timebase. It does,
318+
however, rely on the fact that the scheduler algorithm behaves as described
319+
above.
320+
321+
The `rtc_time` module ensures that `uasyncio` uses `utime` for timing if the
322+
module is present in the path but is unused. This can occur because of an
323+
active USB connection or if running on an an incompatible platform. This
324+
ensures that under such conditions performance is unaffected.

0 commit comments

Comments
 (0)