Event Driven Programming#

%serialconnect
Found serial ports: /dev/cu.usbmodem14301, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14301 --baud=115200 
Ready.

Project Idea: Ultra Low-Cost Syring Pump#

Syringe pumps provide a convenient means of metering flows in microfluidic experiments. Conventional laboratory-grade syringe pumps are expensive, on the order of several hundreds to several thousand dollars, which stands in the way of using microfluidics as a platform for low-cost experimentation.

In the last decade there have been many reports of open-source, low-cost syringe pumps. Typically these employ NEMA 17 stepper motors, an Arduino microcontroller, and 3D printed components and off-the-shelf hardware.

Here we explore the possibiity of even lower cost devices based on 28BYJ-48 stepper motors and Raspberry Pi Pico. Would it be possible to build a viable syringe pump for less than, say, $50?

Ideas …

Button#

Polling#

As our first attempt at building a class for button, let’s consider a simple polling loop.

%serialconnect

from machine import Pin
import time

btn = Pin(20, Pin.IN)

start = time.time()
while time.time() - start <= 20:
    print(btn.value(), end="")
    time.sleep(1)
serial exception on close write failed: [Errno 6] Device not configured
Found serial ports: /dev/cu.usbmodem14301, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14301 --baud=115200 
Ready.
111100001110000111111

Issues:

  • How fast should we poll?

  • time.sleep() blocks anything else we may wish to do.

  • How do we handle multiple buttons that may require different sampling rates?

Timer#

Microcontroller boards typically have timers to assist with coding time dependent operations. The Raspberry Pi Pico has a particularly effective implementation offering an unlimited number of timers based on a global microsecond timebase.

%serialconnect

from machine import Pin, Timer
import time

btn = Pin(20, Pin.IN)

def check_btn(timer):
    global btn
    print(btn.value(), end="")

start = time.time()
tim = Timer(freq=1, mode=Timer.PERIODIC, callback=check_btn)

time.sleep(20)
tim.deinit()
Found serial ports: /dev/cu.usbmodem14301, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14301 --baud=115200 

**[ys] <class 'serial.serialutil.SerialException'>
**[ys] device reports readiness to read but returned no data (device disconnected or multiple access on port?)


**[ys] <class 'serial.serialutil.SerialException'>
**[ys] read failed: [Errno 6] Device not configured

Ready.


***OSError [Device not configured]

How could we handle multiple buttons?

from machine import Pin, Timer
import time

btn1 = Pin(20, Pin.IN)
btn2 = Pin(21, Pin.IN)
btn3 = Pin(22, Pin.IN)

def check_btn1(timer):
    global btn1
    print(f"btn1 = {btn1.value()}")
    
def check_btn2(timer):
    global btn2
    print(f"btn2 = {btn2.value()}")
    
def check_btn3(timer):
    global btn3
    print(f"btn3 = {btn3.value()}")

start = time.time()
tim1 = Timer(freq=1, mode=Timer.PERIODIC, callback=check_btn1)
tim2 = Timer(freq=0.5, mode=Timer.PERIODIC, callback=check_btn2)
tim3 = Timer(freq=2, mode=Timer.PERIODIC, callback=check_btn3)
time.sleep(20)
tim1.deinit()
tim2.deinit()
tim3.deinit()
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 0
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 0
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn3 = 1
btn3 = 1
btn1 = 1
btn2 = 1

Obviously this is getting very clumsy.

  • Because buttons must be declared global, each button requires a unique function

  • Buttons that are sampled slowly also respond very slowly, buttons must be held down for up to one full sampling period.

  • There are serious limitations on what can be included in a callback function. Cannot allocate new memory during a callback.

We could use a functional programming technique known as “closures” to create functions

Interrupt Requests and Service Routines: IRQ/ISR#

Microcontrollers often incorporate hardware mechanisms to interrupt normal execution to service “interrupts”. The Raspberry Pi Pico supports a number of different interrupts, and the MicroPython interpreter provides simplified access that makes them relatively easy and safe to use.

Here we set up MicroPython to respond to interrupts on a GPIO pin. The possible triggers include

  • Pin.IRQ_FALLING

  • Pin.IRQ_RISING

  • Pin.IRQ_LOW_VALUE

  • Pin.IRQ_HIGH_VALUE

These triggers can be combined using the Python OR | operator. For many applications, triggering on a falling or rising edge is often the method of choice. Consult the MicroPython documentation for other options.

%serialconnect 

from machine import Pin
import time

btn = Pin(20, Pin.IN)
led = Pin(25, Pin.OUT)

start = time.time()

def btn_isr(_):
    led.toggle()
    
btn.irq(btn_isr, trigger=Pin.IRQ_FALLING)
serial exception on close write failed: [Errno 6] Device not configured
Found serial ports: /dev/cu.usbmodem14401, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14401 --baud=115200 
Ready.

Here’s a second example that adds a buzzer. The buzzer toggles on when the button is pressed, and toggles off when released.

The pin assignments assume use of the Cytron Maker Pi Pico board. If so, be sure the buzzer on/off switch is in the “on” position.

%serialconnect 

from machine import Pin, PWM
import time

btn20 = Pin(20, Pin.IN)
btn21 = Pin(21, Pin.IN)

led = Pin(25, Pin.OUT)
buzzer = PWM(Pin(18, Pin.OUT))

start = time.time()

def btn20_isr(_):
    led.toggle()
    
buzzer_on = False
buzzer.freq(500)
def btn21_isr(_):
    global buzzer_on
    if buzzer_on:
        buzzer.duty_u16(0)
        buzzer_on = False
    else:
        buzzer.duty_u16(1000)
        buzzer_on = True
    
btn20.irq(btn20_isr, trigger=Pin.IRQ_FALLING)
btn21.irq(btn21_isr, trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING)
Found serial ports: /dev/cu.usbmodem14401, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14401 --baud=115200 
Ready.

Capturing data via IRQ/ISR’s#

There are many tricky issues involved with capturing data through the IRQ/ISR mechanism. This is partly due to Python’s mechanism for creating and updating objects. Interrupting Python at precisely the moment new objects are being created can lead to data or memory corruption.

One useful technique is to use IRQ/ISR within the context of an object class.

%serialconnect

from machine import Pin
import time

class Button(object):
    def __init__(self, gpio):
        self.btn = Pin(gpio, Pin.IN)
        # set up IRQ
        self.btn.irq(self.isr, trigger=Pin.IRQ_FALLING)
        # flag and data
        self.pressed = False
        self.time_pressed = time.ticks_ms()
        
    def isr(self, t):
        self.pressed = True
        self.time_pressed = time.ticks_ms()
        
btn = Button(20)
start = time.ticks_ms()
for k in range(10):
    if btn.pressed:
        print(btn.time_pressed - start)
        btn.pressed = False
    time.sleep(1)
Found serial ports: /dev/cu.usbmodem14401, /dev/cu.Bluetooth-Incoming-Port 
Connecting to --port=/dev/cu.usbmodem14401 --baud=115200 
Ready.
.7178
8803