|
| 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. |
0 commit comments