Ah, thanks for that - I'd misunderstood you point about blocking threads. My scheduler does enable the user to write blocking device drivers. For a serial device the user would proceed as follows.
There would be a custom written interrupt service routine which would perform actions requiring immediate attention, such as putting characters in a (pre-allocated) buffer and otherwise dealing with the UART hardware. Its final action before terminating would be to call a generic interrupt handler imported from my module.
The generic handler vectors the interrupt to run a callback function on the appropriate Waitfor instance: this simply increments a counter which is a property of the instance. I'm hoping this should be pretty fast but I haven't yet quantified it.
The thread which is to block instantiates a Waitfor object passing it the IRQ number and a timeout value (which can be infinite). When it wants to block it yields the Waitfor instance. It will then consume no resources until the interrupt occurs or the timeout expires.
The scheduler, whenever it gains control, builds a list of the threads which are ready to run. It does this as follows. When a thread submits control to the scheduler it yields a Waitfor object. The scheduler queries each of these and receives a tuple containing three integers. In the case of a Waitfor which is waiting on an interrupt, and where at least one interrupt has occurred, the tuple will contain (No. of interrupts, 0,0). (At this point the Waitfor instance zeroes the counter and herein lies a potential concurrency issue which may be worth discussing).
The scheduler determines which thread to run by finding the tuple with the maximum value, which prioritises threads waiting on interrupts. The priority order is
1. Threads waiting on interrupts
2. Threads waiting on polling functions
3. Those waiting on times (most overdue first). (As discussed, this may change)
The tuple containing the interrupt count is passed back to the thread so it can determine the number of interrupts which have occurred since it last ran.
As far as I can see I've implemented the required functionality you've identified. I do need to use my oscilloscope to quantify the time overhead this mechanism imposes on the interrupt handler as I'm very conscious of performance.
One issue with Micropython is the fact that interrupt handlers can't accept arguments. This means (as far as I can see) that they can't be class members. The way I employ to vector interrupts is using a global vector dictionary (private to the module) indexed by IRQ number and containing function pointers to the relevant instance method. I'm sure you'll agree this isn't ideal. If you can think of a better way, or can influence the relevant devs to consider the issue, it would be good.
Here is an example of some code which tests this. The ISR pulses Y10 and the thread pulses Y9 when it responds and a count is maintained on my LCD display.
Code: Select all
# Test of hardware interrupt. Run oscillator thread and patch output (X7) to input (X8) to test
testpin = pyb.Pin(pyb.Pin.board.Y10, pyb.Pin.OUT_PP)
def mycallback(irqno): # Custom callback pulses a pin when interrupt received
testpin.high() # Should see pulse of 6.8uS on Y10 after each
testpin.low()
intcallback(irqno) # Must run default callback
def irqtest1(mylcd):
outpin = pyb.Pin(pyb.Pin.board.Y9, pyb.Pin.OUT_PP) # Push pull output pin
mypin = pyb.Pin.board.X8
extint = pyb.ExtInt(mypin, pyb.ExtInt.IRQ_FALLING, pyb.Pin.PULL_NONE, mycallback)
wf = Waitfor(irq = extint, forever = True) # On yield the thread will block pending an interrupt
count = 0
while True:
yield wf
outpin.high() # Toggle Y9 to test speed of toggle
outpin.low()
mylcd[0] = "Interrupt count: {:6d}".format(count)
count += 1
# print("Interrupt received {:2d}".format(wf.pinstate))
def oscillator():
outpin = pyb.Pin(pyb.Pin.board.X7, pyb.Pin.OUT_PP) # Push pull output pin
wf = Waitfor()
tim = seconds(0.1)
while True:
outpin.low()
yield wf.retrig(tim)
outpin.high()
yield wf.retrig(tim)
Regards, Pete