Categories
My Designs Pico Midi

PICO MIDI – Switch and LED

Let’s continue working on Pico MIDI a Raspberry Pi Pico based MIDI pedal. In my previous post I covered how to send MIDI commands. This was quite a long post, especially because it covered how to actually control two pedals.

In that previous post I controlled Boss RC-500 looper and Digitech Whammy. It was a great fun and I’ll continue exploring ways to control RC-500 in this post (I’ll surely come back to Whammy – it’s too much fun of a pedal not to use it more ?).

Today, we are going to add a minimal user interface for the pedal … an LED and a footswitch. It’s going to be a bit basic, but we’ll get to a more meaty stuff too. There’s a Micropython library for concurrent code execution to explore a bit, and also, some extra configuration for my Boss RC-500 looper.

As usual – you can skip directly to the video and come back later for more details if you wish so.

Here’s the ToC:

Updated Schematic

Let’s expand on bare-bones circuit we had last time and add LED indicator and a foot-switch:

Schematic showing updated diagram of Midi pedal with added Switch and LED
Pico MIDI diagram with added LED & Switch

This is nothing revolutionary, just 3 extra components. LED is very simple to use, but we still need to use current limiting resistor. Nowadays we should be using low current LED, so we can use higher value resistor … but whatever is in the drawer will most likely do (and 1K resistor will always work).

I have a red asterisk next to the switch. How we wire it depends on how we detect the switch state change in our code. For my first example, I need to connect it to 3.3V, for my later examples I needed to connect it to ground, but I’ll cover that later.

Controlling LED

This is super simple, I’ll use nearly a “Hello World” example of embedded programming, and it’s the second example in “Getting Started with Pico” – blinking an LED:

from machine import Pin, Timer
led = Pin(15, Pin.OUT)
timer = Timer()
def blink(timer):
    led.toggle()
timer.init(freq=2.5, mode=Timer.PERIODIC, callback=blink)

That’s as simple as it gets. My LED is connected to Pin 15, and I configured it as output. Timer is calling “blink” function with 2.5Hz frequency. Blink just toggles the switch. Simple 🙂

Controlling Switch

Let’s add a switch. And here’s our code controlling both switch and the LED:

from machine import Pin
import time
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_DOWN)
while True:
    if button.value():
        led.toggle()
        time.sleep(0.5)

That looks even simpler than the previous example! The difference is: I connected the switch to pin 14, and configured it to be input. Here’s where the significance of connecting it to 3.3V comes into play. I had to configure this input pin to use internal Pull Down resistor.

Wiring Switch with Pull-Down Resistor

PICO … well RP2040 microcontroller, has two internal resistors – one pull-down and one pull-up. If I include the pull down resistor into the schematic it looks like this:

Excerpt of the schematic showing internal pull-down resistor on the schematic
Our GPIO with pull-down resistor

I deliberately put * for the designator of the resistor because that’s internal to the microcontroller. By using Pin.PULL_DOWN we essentially do the above. The internal resistor is roughly 50K but the value can vary due to manufacturing process.

While the switch is open, the pull-down resistor keeps our pin connected to the ground and thus our program reads low value. When the switch is closed, our pin is driven high.

We could not use pull-up resistor for this because we’d always read high on that pin, right?

Wiring Switch with Pull-Up Resistor

If we wanted to wire the switch up with pull-up resistor, that would look something like this:

Excerpt of the schematic showing internal pull-up resistor on the schematic
Our GPIO with pull-up resistor

And you can see that switch is wired up slightly differently in this case. When the switch is open, the pull up resistor drives our pin high. If our switch is closed, it drives our pin low connecting it to the ground.

How to wire it up? Well, depends on how you wish to use it in code. I’m not sure if one is better than the other to be honest.

Bouncing Act

Now, this is all great, but it’s unlikely that we’ll do things this way. There is nothing wrong with keeping it simple, but things get complex and unwieldy quickly.
Let’s look into debouncing for example. Switches are not perfect and mechanical connectors can “bounce” a bit before settling. Just one of the wonders of the practical world vs theoretical.
Our Pico is super fast so it might interpret this bouncing of the contacts as clicks. Let’s look at the demo code:

from machine import Pin, Timer
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_DOWN)
cnt = 0
on = False
timer = Timer()
def counter(timer):
    print(cnt)
timer.init(freq=1, mode=Timer.PERIODIC, callback=counter)

while True:
    if button.value():
        led.on()
        if not on:
            on = True
            cnt+=1
    else:
        led.off()
        on = False

The above is a very simple demonstration of bouncing. I use “on” variable to indicate that the switch is “on” and only count the transition between button down and up transition.

In the ideal world, I press the button once, I get the counter increased by one, but in practice, due to the switch bounce, we detect more clicks. Video demonstrates that very well.

Debouncing

So what do we do? Well, we do some debouncing. Essentially we do something to prevent falsely counting bounces as actual switch transitions. This can be done through hardware, I surely have an example of this somewhere. But when using microcontroller, we can do that simply in code.

This is so commonly needed that Micropython comes with a simple example of it. I adapted it so I can demonstrate this in the video:

from machine import Pin, Timer
import time
led = Pin(15, Pin.OUT)
button = Pin(14, Pin.IN, Pin.PULL_DOWN)
cnt = 0
on = False
timer = Timer()
def counter(timer):
    print(cnt)
timer.init(freq=1, mode=Timer.PERIODIC, callback=counter)
def wait_pin_change(pin):
    # wait for pin to change value
    # it needs to be stable for a continuous 20ms
    cur_value = pin.value()
    active = 0
    while active < 20:
        if pin.value() != cur_value:
            active += 1
        else:
            active = 0
        time.sleep_ms(1)
while True:
    wait_pin_change(button)
    if button.value():
        led.on()
        if not on:
            cnt+=1
            on = True
    else:
        led.off()
        on = False

Voila. Simple debouncing logic. There are better ways, but this works. We could go on with this, but what about detecting a button click and button long press etc. how do we do this? Well, things get complicated quickly … as I may have mentioned.
At this stage, it is probably better to use a library. We should be able to find a bunch of libraries for switch handling since it’s a pretty common task.

This does not come with Micropython though, you can check out libraries coming out of the box with it here. But there’s a bunch of libraries you can use – there’s a nice collection of quite a few libraries at this awesome micropython git page.

My Library of Choice

I decided to use micropython-async library. No big deal, I just thought I might want to use some concurrency at some stage, and I just liked the way it enables me to write the code. Keep in mind that RP2040 is quite powerful, so it makes sense to try using some concurrency.

We’ll see if that keeps, we will want to react to switch changes very quickly. I’m not sure how deterministic this will end up being, but I like the theory behind it.

If you want to learn more, have a look at Python’s asyncio library documentation. uasyncio library is based on it, and obviously simplified, because it runs on an embedded system with limited RAM and CPU power. uasyncio comes preinstalled with Micropython firmware.

Let’s find an example. We won’t use Switch class, we’ll use Pushbutton class – it has interface that is better suited for what we need. We’ll have to slightly modify it because this is an example for pyboard, but the change is trivial.

from machine import Pin
import uasyncio as asyncio
from primitives import Pushbutton
def toggle(led):
    led.toggle()
async def my_app():
    pin = Pin(14, Pin.IN, Pin.PULL_UP)  # Pushbutton to gnd - we need to rewire our button
    red = Pin(15, Pin.OUT)
    pb = Pushbutton(pin)
    pb.press_func(toggle, (red,))  # Note how function and args are passed
    while True:
        await asyncio.sleep(60)
asyncio.run(my_app())  # Run main application code

Quick explanation of the Code

The code above is super simple compared to the previously used code. Obviously, I’m using a library that deals with debouncing for me and some extra stuff. Let’s go through that quickly.

asyncio.run(my_app()) is starting our application. Reads great, doesn’t it? Essentially – our main app is my_app function. In there – I setup my pins, we’ve seen that before. Since Pushbuton by default expects high to be open position, and low level for closed switch position I needed to rewire the switch a bit.

pb = Pushbutton(pin) defines our pin, our pb now is allowing us to setup a callback when the switch is pressed: pb.press_func(toggle, (red,)). So when the switch is pressed (after being debounced) toggle function is called and “red” is passed to the function. “toggle” function just toggles the led. Simple.

As long as “my_app” function is running, the application is running. That’s why I have that “while True: await asyncio.sleep(60)”. This runs concurrently because “await” construct gives away control and Pushbutton can run its background polling on our pin. These co-routines (I think that’s a proper term) alternately give away control and take control and this way work concurrently. For more detail here’s a good explanation.

Anyway, let’s move on.

Midi Controller Code

Let’s create our very simple Midi controller. All it will do, it will send CC#1 command when button is pressed:

from machine import UART, Pin
import uasyncio as asyncio
from primitives import Pushbutton
cc_1_on = b'\xb0\x01\x7f'
cc_1_off = b'\xb0\x01\x00'
def toggle(led, midi):
    led.toggle()
    #and we toggle midi
    midi.write(bytearray(cc_1_off))
    midi.write(bytearray(cc_1_on))
    
async def my_app():
    pin = Pin(14, Pin.IN, Pin.PULL_UP)  # Pushbutton to gnd - we need to rewire our button
    red = Pin(15, Pin.OUT)
    midi = UART(0, baudrate=31250, bits=8, parity=None, stop=1, tx=Pin(16), rx=Pin(17), invert=0)
    pb = Pushbutton(pin)
    pb.press_func(toggle, (red, midi,))
    while True:
        await asyncio.sleep(60)
asyncio.run(my_app())  # Run main application code

That’s it ?. At least, that’s it if all you want is that your pedal send CC#1 command. Let’s go through the code quickly. It’s pretty similar to the above, I only added midi as we covered in the previous post.

Simple UART config, that’s passed to our “toggle” function and in the toggle function, I toggle LED and also send CC#1 command over midi. That is super simple.

Some additional RC-500 Config

In the video I covered some peculiarities of configuring RC-500. There’s an option to select between Toggle and Momentary switch. But this is configuration mainly geared towards external foot switch (something like FS-5U pedal for example), not MIDI.

In the video I tried out some things, and unfortunately Boss’ documentation for the pedal is not as exhaustive as I would like it to be. In the previous post (link at the top of the page), I covered lots of configuration, how various things work for RC-500 (and also Whammy pedal). But I only scratched the surface.

Turns out, if you want to get more into it, you’ll probably need to experiment a bit. Whammy seems to be straight forward, but RC-500 is a different beast. In the video I cover 3 different options and what do they mean. And there is a ton of options to be sure.

The things I covered in video are:

  • How target: T1 Ply/Stp target works with SRC MODE: Momentary/Toggle. Makes no difference actually, I had to send CC#1 with minimum value, and then CC#1 with maximum value whichever option I chose to toggle between Play and Stop
  • Target: T1 Reverse. SRC MODE: Momentary – if I send CC#1 with min value, the effect is turned off immediately, if I send CC#1 with max value the effect is turned on immediately. If I choose SRC MODE: Toggle, I have to send CC#1 min and then CC#1 max value to toggle between stopping and starting the effect
  • Finally, for Target: T1 Stop … this is interesting, SRC MODE works in a similar fashion as for T1 Reverse, but this only changes how Stop button works! There’s no immediate action. I configured TARGET MIN to be Immediate, and TARGET MAX to be Fade out. When I activated one or the other, what happened was that only when I pressed stop button my choice of what happens was applied. See the video if this is a bit vague

I slightly changed code for this demonstration:

from machine import UART, Pin
import uasyncio as asyncio
from primitives import Pushbutton
cc_1_on = b'\xb0\x01\x7f'
cc_1_off = b'\xb0\x01\x00'
def toggle(led, midi):
    led.toggle()
    if led.value():
        midi.write(bytearray(cc_1_on))
        print('ON')
    else:
        midi.write(bytearray(cc_1_off))
        print('OFF')
    
async def my_app():
    pin = Pin(14, Pin.IN, Pin.PULL_UP)  # Pushbutton to gnd - we need to rewire our button
    red = Pin(15, Pin.OUT)
    midi = UART(0, baudrate=31250, bits=8, parity=None, stop=1, tx=Pin(16), rx=Pin(17), invert=0)
    on = False
    pb = Pushbutton(pin)
    pb.press_func(toggle, (red, midi, ))
    while True:
        await asyncio.sleep(60)
asyncio.run(my_app())  # Run main application code

The only difference from the previous code is that in the “toggle” function I do not send CC#1 min and immediately CC#1 max. I alternate between them and print out what I’m sending over MIDI. In the video you can see what happens when I change RC-500 configuration.

Action

Here’s a (not so) quick demo of what I covered here:

Action time

Anyway, even though this post covered very simple things – LED and Switch are nothing advanced, I did cover a very cool library that seems like it’s very powerful. And on top of that, I covered some extra configuration for our looper and we literally have something that could be useful straight away.

Things will just get more complex from here. I’ll keep on adding features and the code will start getting bigger and bigger. I hope you liked it so far, stay tuned for more.

6 replies on “PICO MIDI – Switch and LED”

Great post. There are not a lot of people out there explaining how you might use a Pico for this type of application. I also like watching you work through the problems in real time. Very cool. I would love to see you create a four-button pedal that could send midi cc data to Ableton Live via a usb port. There are expensive midi pedals such as the Looptimus (and Morningstar) pedals that can achieve this but what the world needs is a SIMPLE way to send a few cc messages to Ableton. Ableton software allows the user to highlight any button on the screen in Midi-Map Mode (such as the “play” loop button on track #1) and let’s you edit what is going to trigger that function by simply sending it a cc while highlighted. So really all that is needed is a few foot switches to make it really easy to control the tracks/loops while you’re playing guitar and singing. Hands free. I know this is BASIC but I’m not a programmer and there’s not any simple or affordable way to do this through a manufactured pedal. Would love to see this! Great website too.

Hi Colby, thanks for that, I’m glad you find it useful.

I had this in mind when I was starting this. I want to support sending commands over USB, but at the moment MicroPython does not support that. I’ll have some time in next couple of months to look into that, but might be a tricky one to do until MicroPython supports it.

Another option is to use C++ for this … not somewhere I want to go 🙂 but I’ll be looking into that as well.

Stay tuned, and if it happens, happy days 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.