Skip to content

Commit 23a60e2

Browse files
committed
Low power version added.
1 parent 366f16f commit 23a60e2

File tree

7 files changed

+356
-8
lines changed

7 files changed

+356
-8
lines changed

FASTPOLL.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ This behaviour may be desired where short bursts of fast data are handled.
225225
Otherwise drivers of such hardware should be designed to avoid hogging, using
226226
techniques like buffering or timing.
227227

228+
The version also supports an `implementation` namedtuple with the following
229+
fields:
230+
* `name` 'fast_io'
231+
* `variant` `standard`
232+
* `major` 0 Major version no.
233+
* `minor` 100 Minor version no. i.e. version = 0.100
234+
235+
The `variant` field can also contain `lowpower` if running that version.
228236

229237
###### [Contents](./FASTPOLL.md#contents)
230238

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# 1. The MicroPython uasyncio library
22

3-
This GitHub repository consists of the following parts:
3+
This GitHub repository consists of the following parts. Firstly a modified
4+
`fast_io` version of `uasyncio` offering some benefits over the official
5+
version.
6+
7+
Secondly the following resources relevant to users of official or `fast_io`
8+
versions:
49
* [A tutorial](./TUTORIAL.md) An introductory tutorial on asynchronous
510
programming and the use of the uasyncio library (asyncio subset).
611
* [Asynchronous device drivers](./DRIVERS.md). A module providing drivers for
@@ -24,7 +29,7 @@ This GitHub repository consists of the following parts:
2429
* [Under the hood](./UNDER_THE_HOOD.md) A guide to help understand the
2530
`uasyncio` code. For scheduler geeks and those wishing to modify `uasyncio`.
2631

27-
## 1.1 A new "priority" version.
32+
## 1.1 The "fast_io" version.
2833

2934
This repo included `asyncio_priority.py` which is now deprecated. Its primary
3035
purpose was to provide a means of servicing fast hardware devices by means of
@@ -42,6 +47,13 @@ provides an option for I/O scheduling with much reduced latency. It also fixes
4247
the bug. It is hoped that these changes will be accepted into mainstream in due
4348
course.
4449

50+
### 1.1.1 A Pyboard-only low power version
51+
52+
This is documented [here](./lowpower/README.md). In essence a Python file is
53+
placed on the device which configures the `fast_io` version of `uasyncio` to
54+
reduce power consumption at times when it is not busy. This provides a means of
55+
using the library on battery powered projects.
56+
4557
# 2. Version and installation of uasyncio
4658

4759
The documentation and code in this repository are based on `uasyncio` version

fast_io/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
# uasyncio.__init__ fast_io
2+
# fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw
13
import uerrno
24
import uselect as select
35
import usocket as _socket
46
from uasyncio.core import *
57

6-
78
DEBUG = 0
89
log = None
910

fast_io/core.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import utime as time
1+
# uasyncio.core fast_io
2+
# fork: peterhinch/micropython-lib branch: uasyncio-io-fast-and-rw
3+
version = 'fast_io'
4+
try:
5+
import rtc_time as time # Low power timebase using RTC
6+
except ImportError:
7+
import utime as time
28
import utimeq
39
import ucollections
410

@@ -46,7 +52,8 @@ def time(self):
4652

4753
def create_task(self, coro):
4854
# CPython 3.4.2
49-
assert not isinstance(coro, type_genf), 'Generator function is not iterable.' # upy issue #3241
55+
assert not isinstance(coro, type_genf), 'Coroutine arg expected.' # upy issue #3241
56+
# create_task with a callable would work, so above assert only traps the easily-made error
5057
self.call_later_ms(0, coro)
5158
# CPython asyncio incompatibility: we don't return Task object
5259

@@ -158,8 +165,8 @@ def run_forever(self):
158165
continue
159166
elif isinstance(ret, IOWriteDone):
160167
self.remove_writer(arg)
161-
self._call_io(cb, args) # Next call produces StopIteration. Arguably this is not required
162-
continue # as awrite produces no return value.
168+
self._call_io(cb, args) # Next call produces StopIteration: see StreamWriter.aclose
169+
continue
163170
elif isinstance(ret, StopLoop): # e.g. from run_until_complete. run_forever() terminates
164171
return arg
165172
else:
@@ -205,7 +212,7 @@ def run_forever(self):
205212
self.wait(delay)
206213

207214
def run_until_complete(self, coro):
208-
assert not isinstance(coro, type_genf), 'Generator function is not iterable.' # upy issue #3241
215+
assert not isinstance(coro, type_genf), 'Coroutine arg expected.' # upy issue #3241
209216
def _run_and_stop():
210217
yield from coro
211218
yield StopLoop(0)

lowpower/README.md

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# An experimental low power usayncio adaptation
2+
3+
# 1. Introduction
4+
5+
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+
[see docs](http://docs.micropython.org/en/latest/pyboard/library/pyb.html).
9+
10+
Use of `standby` is simple in concept: the application runs and issues
11+
`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
13+
reset. In that respect a `uasyncio` application is no different from any other.
14+
If the application can cope with the fact that execution state is lost during
15+
the delay, it will correctly resume.
16+
17+
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`.
21+
22+
The adaptation trades a reduction in scheduling performance for a substantial
23+
reduction in power consumption.
24+
25+
Some general notes on low power Pyboard applications may be found
26+
[here](https://github.com/peterhinch/micropython-micropower).
27+
28+
# 2. Installation
29+
30+
Ensure that the version of `uasyncio` in this repository is installed and
31+
tested. Copy the file `rtc_time.py` to the device so that it is on `sys.path`.
32+
33+
The test program `lowpower.py` requires a link between pins X1 and X2 to enable
34+
UART 4 to operate via a loopback.
35+
36+
# 3 Low power uasyncio operation
37+
38+
## 3.1 The official uasyncio package
39+
40+
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
43+
be ready to run. It then calls `select.poll`'s `ipoll` method with a timeout
44+
calculated on that basis. This consumes power.
45+
46+
The second issue is that it uses `utime`'s millisecond timing utilities for
47+
timing. This ensures portability across MicroPython platforms. Unfortunately on
48+
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.
51+
52+
## 3.2 The low power adaptation
53+
54+
If running on a Pyboard the version of `uasyncio` in this repo attempts to
55+
import the file `rtc_time.py`. If this succeeds and there is no USB connection
56+
to the board it derives its millisecond timing from the RTC; this continues to
57+
run through `stop`.
58+
59+
To avoid the power drain caused by `select.poll` the user code must issue the
60+
following:
61+
62+
```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
76+
```
77+
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.
81+
82+
### 3.2.1 Consequences of pyb.stop
83+
84+
#### 3.2.1.1 Timing Accuracy
85+
86+
A minor limitation is that the Pyboard RTC cannot resolve times of less than
87+
4ms so there is a theoretical reduction in the accuracy of delays. In practice,
88+
as explained in the [tutorial](../TUTORIAL.md), issuing
89+
90+
```python
91+
await asyncio.sleep_ms(t)
92+
```
93+
94+
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.
97+
98+
#### 3.2.1.2 USB
99+
100+
Programs using `pyb.stop` disable the USB connection to the PC. This is
101+
inconvenient for debugging so `rtc_time.py` detects an active USB connection
102+
and disables power saving. This enables an application to be developed normally
103+
via a USB connected PC. The board can then be disconnected from the PC and run
104+
from a separate power source for power measurements.
105+
106+
Applications can detect which timebase is in use by issuing:
107+
108+
```python
109+
import rtc_time
110+
if rtc_time.use_utime:
111+
# Timebase is utime: either a USB connection exists or not a Pyboard
112+
else:
113+
# Running on RTC timebase with no USB connection
114+
```
115+
116+
# 4. rtc_time.py
117+
118+
This provides the following.
119+
120+
Variable:
121+
* `use_utime` `True` if the `uasyncio` timebase is `utime`, `False` if it is
122+
the RTC.
123+
124+
Functions:
125+
If the timebase is `utime` these are references to the corresponding `utime`
126+
functions. Otherwise they are direct replacements but using the RTC as their
127+
timebase. See the `utime` official documentation for these.
128+
* `ticks_ms`
129+
* `ticks_add`
130+
* `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.
133+
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.
139+
140+
# 5. Application design
141+
142+
Attention to detail is required to minimise power consumption, both in terms of
143+
hardware and code.
144+
145+
## 5.1 Hardware
146+
147+
Hardware issues are covered [here](https://github.com/peterhinch/micropython-micropower).
148+
To summarise an SD card consumes on the order of 150μA. For lowest power
149+
consumption use the onboard flash memory. Peripherals usually consume power
150+
even when not in use: consider switching their power source under program
151+
control.
152+
153+
## 5.2 Application Code
154+
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.
160+
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
163+
will not be scheduled. I/O from interrupt driven devices such as UARTs will be
164+
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.
167+
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
171+
172+
```python
173+
async def foo():
174+
while True:
175+
# Do some processing
176+
await asyncio.sleep(0)
177+
```
178+
179+
will execute (at best) at a rate of 5Hz. And possibly considerably less
180+
frequently depending on the behaviour of competing coros. Likewise
181+
182+
```python
183+
async def bar():
184+
while True:
185+
# Do some processing
186+
await asyncio.sleep_ms(10)
187+
```
188+
189+
the 10ms sleep may be 200ms or longer, again dependent on other application
190+
code.

lowpower/lowpower.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# lowpower.py Demo of using uasyncio to reduce Pyboard power consumption
2+
# Author: Peter Hinch
3+
# Copyright Peter Hinch 2018 Released under the MIT license
4+
5+
# The file rtc_time.py must be on the path for this to work at low power.
6+
7+
import pyb
8+
import uasyncio as asyncio
9+
import rtc_time
10+
11+
# Stop the test after a period
12+
async def killer(duration):
13+
await asyncio.sleep(duration)
14+
15+
# Briefly pulse an LED to save power
16+
async def pulse(led):
17+
led.on()
18+
await asyncio.sleep_ms(200)
19+
led.off()
20+
21+
# Flash an LED forever
22+
async def flash(led, ms):
23+
while True:
24+
await pulse(led)
25+
await asyncio.sleep_ms(ms)
26+
27+
# Periodically send text through UART
28+
async def sender(uart):
29+
swriter = asyncio.StreamWriter(uart, {})
30+
while True:
31+
await swriter.awrite('Hello uart\n')
32+
await asyncio.sleep(1.3)
33+
34+
# Each time a message is received pulse the LED
35+
async def receiver(uart, led):
36+
sreader = asyncio.StreamReader(uart)
37+
while True:
38+
await sreader.readline()
39+
await pulse(led)
40+
41+
def test(duration):
42+
# For lowest power consumption set unused pins as inputs with pullups.
43+
# Note the 4K7 I2C pullups on X9 X10 Y9 Y10.
44+
for pin in [p for p in dir(pyb.Pin.board) if p[0] in 'XY']:
45+
pin_x = pyb.Pin(pin, pyb.Pin.IN, pyb.Pin.PULL_UP)
46+
if rtc_time.use_utime: # Not running in low power mode
47+
pyb.LED(4).on()
48+
uart = pyb.UART(4, 115200)
49+
loop = asyncio.get_event_loop()
50+
loop.create_task(rtc_time.lo_power(100))
51+
loop.create_task(flash(pyb.LED(1), 4000))
52+
loop.create_task(sender(uart))
53+
loop.create_task(receiver(uart, pyb.LED(2)))
54+
loop.run_until_complete(killer(duration))
55+
56+
test(60)

0 commit comments

Comments
 (0)