V1.02 6th Sept 2014
Author: Peter Hinch
A set of libraries for writing threaded code on the MicroPython board.
There are five libraries
- usched.py The scheduler
- switch.py Support for debounced switches. Uses usched.
- pushbutton.py Pushbutton supports logical value, press, release, long and double click callbacks
- lcdthread.py Support for LCD displays using the Hitachi HD44780 controller chip. Uses usched.
- delay.py A simple retriggerable time delay class
Test/demonstration programs
- ledflash.py Flashes the onboard LED's asynchronously
- roundrobin.py Demonstrates round-robin schedulting.
- irqtest.py Demonstrates a thread which blocks on an interrupt.
- subthread.py Illustrates dynamic creation and deletion of threads.
- lcdtest.py Demonstrates output to an attached LCD display.
- polltest.py A thread which blocks on a user defined polling function
- instrument.py The scheduler's timing functions employed to instrument code
- pushbuttontest.py Demo of pushbutton class
Now uses the new pyb.micros() function rather than tie up a hardware timer. Hence requires a version of MicroPython dated on or after 28th Aug 2014.
There is also a file minified.zip. This includes the above files but run through pyminifier to strip comments and unneccesary spaces. Cryptic. Only recommended if you're running on internal memory and are short of space. Please report any bugs against the standard version as the line numbers won't match otherwise!
The scheduler uses generators and the yield statement to implement lightweight threads. When a thread submits control to the scheduler it yields an object which informs the scheduler of the circumstances in which the thread should resume execution. There are four options.
- A timeout: the thread will be rescheduled after a given time has elapsed.
- Round robin: it will be rescheduled as soon as possible subject to other pending threads getting run.
- Pending a poll function: a user specified function is polled by the scheduler and can cause the thread to be scheduled.
- Wait pending a pin interrupt: thread will reschedule after a pin interrupt has occurred.
The last two options may include a timeout: a maximum time the thread will block pending the specified event.
Documentation
Most of this is in the code comments. Look at the example programs first, then at the libraries themselves for more detail.
Timing
The scheduler's timing is based on pyb.micros(). My use of microsecond timing shouldn't lead the user into hopeless optimism: if you want a delay of 1mS exactly don't issue
yield from wait(0.001)
and expect to get a one millisecond delay. It's a cooperative scheduler. Another thread will be running when the period elapses. Until that thread decides to yield your thread will have no chance of restarting. Even then a higher priority thread such as one blocked on an interrupt may, by then, be pending. So, while the minimum delay will be 1mS the maximum is dependent on the other code you have running. On the Micropython board don't be too surprised to see delays of many milliseconds.
If you want precise timing, especially at millisecond level or better, you'll need to use one of the hardware timers.
Avoid issuing short timeout values. A thread which does so will tend to hog the CPU at the expense of other threads. The well mannered way to yield control in the expectation of restarting soon is to yield a Roundrobin instance. In the absence of higher priority events, such a thread will resume when any other such threads have been scheduled.
Communication
In nontrivial applications threads need to communicate. A well behaved thread periodically yields control to the scheduler: the item yielded is an object which tells the scheduler the conditions under which the thread is to be re-sceduled. The item yielded is unsuitable for use for inter-thread communication which is best achieved by passing a shared mutable object as an argument to a thread on creation. At its simplest this can be a list, as in the example subthread.py. More flexibly a user defined mutable object may be used as in polltest.py. I'm ignoring the idea of globals here!
Concurrency
The more gory aspects of concurrency are largely averted in a simple cooperative scheduler such as this: at any one time one thread has complete control and a data item is not suddenly going to be changed by the activities of another thread. However the Micropython system does enable hardware interrupts, and their handlers pre-emptively take control and run in their own context. Appropriate precautions should be taken communicating between interrupt handlers and other code.
Interrupts
The way in which the scheduler supports pin interrupts is described in irqtest.py In essence the user supplies a callback function. When an interrupt occurs, the default callback runs which increments a counter and runs the user's callback. A thread blocked on this interrupt will be rescheduled by virtue of the scheduler checking this counter.
It's important to be aware that the user's callback runs in the IRQ context and is therefore subject to the Micropython rules on interrupt handlers along with the concurrency issues mentioned above.
Polling
Some hardware such as the accelerometer doesn't support interrupts, and therefore needs to be polled. One option suitable for slow devices is to write a thread which polls the device periodically. A faster and more elegant way is to delegate this activity to the scheduler. The thread then suspends excution pending the result of a user supplied callback function, which is run by the scheduler. From the thread's point of view it blocks pending an event - with an optional timeout available.
Return from yield
The scheduler returns a 3-tuple to all yield statements. In many case this can be ignored but it contains information about why the thread was scheduled such as a count of interrupts which have occurred and whether the return was due to an event or a timeout. Elements are: 0. 0 unless thread returned a Pinblock and one or more interrupts have occured, when it holds a count of interrupts.
- 0 unless thread returned a Poller and the latter has returned an integer, when it holds that value.
- 0 unless thread was waiting on a timer* when it holds no. of uS it is late
- In addition to Timeout instances this includes timeouts applied to Pinblock or Poller objects: this enables the thread to determine whether it was rescheduled because of the event or because of a timeout. If the thread yielded a Roundrobin instance the return tuple will be (0, 0, 0). There is little point in intercepting this.
Initialisation
A thread is created with code like
objSched.add_thread(robin("Thread 1"))
When this code runs a generator object is created and assigned to the scheduler. It's important to note that at this time the thread will run until the first yield statement. It will then suspend execution until the scheduler starts. This enables initialisation code to be run in a well defined order: the order in which the threads are created.