I always have trouble remembering what is the Zoom keyboard shortcut to mute or unmute my audio, so I end up grabbing the mouse and clicking the button instead. While there isn’t really a problem with clicking, it feels inefficient, and that awkward silence while every other call participant is waiting for me to unmute and start speaking appears to last an eternity.
I thought it would be interesting to use my Raspberry Pi Pico microcontroller board and MicroPython to design a single-key keyboard with the only function to toggle the audio on my video calls. That way, there is no key combination to remember!
Do you want to learn how to build this project? Just follow along!
To build this cool little gadget you will need a few hardware components, described in the following sections.
Raspberry Pi Pico microcontroller
The heart of this device is going to be a Raspberry Pi Pico microntroller. For this project it is recommended that you buy one with headers already soldered, as shown in the picture below.
If you are handy with a soldering iron, you can buy the standard Pico, which comes without header pins, and solder them yourself.
The next component that you will need to acquire is a momentary push button switch. It does not need to be exactly like the one pictured below, but you’d want it to be momentary, which means that after you release it it returns to its original state.
The easiest way to build the circuit is to do it on a breadboard. For this project a small 400 point breadboard is the perfect size.
The connections between the Raspberry Pi Pico and the push button will be made with jumper wires. These are short cables that have pins on the ends. The pins insert in the holes of the breadboard.
To power your Raspberry Pi Pico you will need a USB cable. The connector on the Pico side is a micro-USB (pictured on the right side in the image below). The other end of the cable will plug into your computer, so you may need that to be a regular USB connector (as shown in the image), or maybe a USB-C connector if you have a newer computer.
The final requirement is a Python 3 interpreter installed on your computer. If you don’t have it already, you can grab an installer for your operating system from python.org.
MicroPython vs. CircuitPython
There are two major sources of MicroPython firmware for the Raspberry Pi Pico:
- Official MicroPython builds
- CircuitPython builds from Adafruit
The MicroPython language is the same in both versions (CircuitPython is a fork of MicroPython), but there are some small variations in the functionality that is included in the releases from these two sources. CircuitPython also has an extensive library of installable drivers for many components, so even if you work with the official MicroPython, it is a good idea to keep CircuitPython in mind.
CircuitPython builds come with USB HID support included, while MicroPython builds do not. The HID (Human Interface Devices) part of the USB specification is the one that covers computer peripherals such as mice and keyboards, so for the project we intend to build, CircuitPython makes the most sense.
Installing CircuitPython on your Pico
In this section you are going to work on installing the CircuitPython firmware inside your Raspberry Pi Pico.
Visit the CircuitPython for Pico downloads page and download the latest version of the firmware to your computer. This is a file that ends with a .uf2 file extension.
Now comes the fun part. Take the USB cable and plug the USB or USB-C end into your computer. Do not plug the micro-USB end to your Pico just yet.
Grab your Pico and locate the
Press the button, and while you keep it pressed, plug the micro-USB end of the USB cable to power your device. In a second or two a new disk drive labeled
RPI-RP2 will appear on your computer. You can release the
BOOTSEL button when you see it.
To install CircuitPython, drag and drop the
.uf2 file that you downloaded earlier inside this disk drive. As soon as the file transfers, the drive will disappear and the device will reboot itself. On some computers you may get a warning about the drive being disconnected without properly ejecting it. You can ignore this warning.
After your device reboots and loads the CircuitPython firmware, a new disk drive with the label
CIRCUITPY should appear on your computer.
To verify that your installation was successful, create a file with the name code.py on your computer and enter the following code in it:
import board import digitalio import time led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT led.value = False # blink the LED three times for _ in range(3): # light up the LED led.value = True # wait 0.5 seconds time.sleep(0.5) # turn off the LED led.value = False # wait 0.5 seconds time.sleep(0.5)
This code blinks the LED that is installed on the Pico board three times. While the Pico is still connected to your computer, drag and drop the code.py file to the
CIRCUITPY volume. If there is a code.py file already on the disk first make sure it does not have anything you care about, and then replace it with this version.
The Pico will detect that the code.py file has been updated, will reboot itself, and run the code. You should see the LED, which is near the micro-USB connector, blink three times.
Congratulations, you have successfully installed CircuitPython on your Pico!
Installing CircuitPython libraries
While the low-level support for HID devices is bundled with the CircuitPython build, the HID protocol used by keyboards is fairly complex. Lucky for us, the Adafruit CircuitPython library includes a high-level HID interface that allows an application to emulate a keyboard, a mouse or a gamepad.
The first step to install a library is to download the CircuitPython library bundle to your computer.
Visit the CircuitPython Library Bundle page and download the latest version of the bundle to your computer. There are three downloads associated with the library bundle, all of them .zip files:
- The compiled “Bundle”
- The MicroPython source code for the bundle library
- Example code
What we need for this tutorial is the first of the three downloads, which includes the compiled MicroPython code for all the Adafruit libraries. When you download the bundle zip file, make sure that the version matches the version of CircuitPython that you installed on your Pico.
Use your favorite zip extraction tool to expand the contents of the downloaded bundle file. Inside the top-level folder you will find a lib subdirectory:
Inside lib, you will find a really large number of files and subdirectories. Look for a subdirectory with the name adafruit_hid. This is the library that we need to install on the Pico.
CIRCUITPY disk drive, and create a lib directory (if it doesn’t exist yet). Then drag and drop the adafruit_hid directory inside lib. The structure of your Pico disk should now look like the following:
The .mpy files that you see in the adafruit_hid directory are compiled MicroPython files that are optimized to save space in your device’s file system. As such, these are not files that you can open and view in a text editor. To inspect the source code for this library you have to download the source code bundle.
The CircuitPython REPL
Like the standard Python language, MicroPython and CircuitPython come with a REPL, where you can enter statements and have them evaluated interactively. The CircuitPython REPL can be accessed from your computer while the device is connected with the USB cable.
Identifying the Pico serial device
The Pico device appears as a USB serial port device on your computer, so the first task is to figure out which of possibly several serial ports installed on your computer is the one that is connected to your board.
If you are using a Mac OS computer, the name of the Pico serial device should be /dev/tty.usbmodem<n>, where <n> is a number. To find out what the complete name of your serial device is, connect the Pico to your computer and then look in the /dev directory for files with this pattern:
$ ls /dev/tty.usbmodem* /dev/tty.usbmodem143101
On a Linux computer the device name may vary, but it usually has the format /dev/ttyACM<n>, where
<n> is a number, likely 0. You can use the
lsusb command to list all the USB attached devices. If you can’t identify which device is the one that maps to the Pico, you can run this command two times, once with the Pico plugged in and another one without, then see which device disappeared in the second run.
On a Windows computer the device name will have the format
<n> is a number. You can open the Device Manager in the Control Panel and look under “Ports (COM & LPT)” for the list of serial devices. If you cannot identify which of the devices listed there is your Pico, unplug it from USB and check the list again to find which one is now missing.
Great, now you know what serial device your computer is using to connect to the Pico microcontroller.
The Pico behaves like a standard serial device, so it can be accessed with any serial terminal program available for your operating system. In this article you are going to use a tool called mpfshell, a tool that is designed to connect to and manage MicroPython microcontroller boards.
Open a terminal window in your computer and create a new directory where you will store your Pico project:
mkdir pico-mute cd pico-mute
Create and activate a Python virtual environment. If you are using a Mac or Linux computer, do it as follows:
python3 -m venv venv source venv/bin/activate
On a Windows computer, use the following commands:
python -m venv venv venv\Scripts\activate
Make sure that the Python virtual environment is activated by checking that your command prompt was modified to include a
Next install the
mpfshell tool in your virtual environment:
pip install mpfshell
Make sure the Pico is still connected to your computer with the USB cable, and then execute the following command to connect to it from your computer:
Make sure to replace
<your-pico-serial-device> with the serial device name assigned to your Pico in the command above. If you are doing this on a Linux or Mac computer, you do not need to include the /dev/ prefix. For example, on my Mac computer the command that I use is:
On Windows, assuming the Pico is connected to
COM3, the command would be:
The output of
mpfshell should look as follows:
(venv) $ mpfshell tty.usbmodem143101 Connected to rp2040 ** Micropython File Shell v0.9.2, email@example.com ** -- Running on Python 3.8 using PySerial 3.5 -- mpfs [/]>
The command should end with a new prompt. Type
help in this prompt to see all the commands
mpfs [/]> help Documented commands (type help <topic>): ======================================== EOF cd exec get lcd lpwd md mput mrm put pwd rm cat close exit help lls ls mget mpyc open putc repl
For now, let’s concentrate on the
repl command, which starts an interactive shell on your Pico device:
mpfs [/]> repl > *** Exit REPL with Ctrl+] *** Adafruit CircuitPython 6.2.0 on 2021-04-05; Raspberry Pi Pico with rp2040 >>>
Does this look familiar? If you’ve ever used a Python REPL you will surely recognize the
>>> prompt. You can now enter Python statements, exactly like you would in a Python shell running on your computer. But keep in mind that any statements that you enter in this shell will be executed by the Pico board. Your computer is just acting as a dumb terminal.
To exit the CircuitPython shell, press
Ctrl-] and that will bring you back to the
mpfshell prompt. Then press
Ctrl-D at the
mpfshell prompt to exit back to your terminal.
If all you want to do is access the MicroPython REPL, then you can skip the intermediate step of the
mpfshell prompt by adding
-c repl at the end of the
mpfshell <your-pico-serial-device> -c repl
Building the circuit
We’ve made some good progress on the software front, so now it is time to concentrate on the hardware.
The following diagram shows how to wire the push button to the Pico using two jumper wires. We’ll go over these connections step-by-step to make sure you do it correctly.
In addition to the actual wiring diagram, below you can see the pin layout of the Raspberry Pi Pico. You do not need to fully understand this diagram, but it is good to have it present and use it as a reference.
Positioning the breadboard
To begin, position your breadboard in landscape orientation, with the numbers printed on it increasing from the right to the left. For this project we are going to use the 10 rows of holes, highlighted below:
This part of the breadboard has 30 columns of holes, numbered 1 to 30 starting from the right. The five rows in the upper half are labeled
e, while the five rows in the bottom half are labeled
On any given column, the group of 5 vertical holes (either in the top or in the bottom halves) are all internally connected to each other, and this is what makes it easy to make connections between components, as you will soon see.
Installing the Raspberry Pi Pico on the breadboard
Take the Raspberry Pi Pico with the micro-USB port pointing to the right, and carefully align the header pins with the breadboard holes. You want the right-most top and bottom pins of the Pico to match the
1h holes. Once you have it in the right position, gently press on the board to allow the pins to go in as far as they can.
It is important that the board is vertically centered on the breadboard surface, which should leave the
b rows of holes visible right above it, and the
j rows below it. These two rows of holes will allow us to make connections to the terminals of the Pico board.
Installing the push button
The next step is to insert your push button somewhere in the free part of the breadboard, to the left of the Pico board. For this you need to be very careful and understand how your push button’s connectors work.
Push buttons have two terminals, but many models (like the one I’m using) come with each terminal doubled, so while there are still two actual connections, there are four physical pins.
Before you install the button on the breadboard you need to identify the pairs of pins that go together. If you have the same style of push button, when you orient the button so that there are two pins on the top and two on the bottom, the two left-side pins are connected to one of the terminals, and the two right-side pins are on the other. To connect this button you have to pick one of the pins on the left and one on the right.
For convenience when wiring, the button should be installed on the breadboard in such a way that the two terminals (or the two terminal groups) end up on different columns on the breadboard. In the case of a button with double terminals, you can position the two paired terminals above and below the middle line of the breadboard respectively, as I’ve done in the picture above.
The installation that I used has the button’s connectors on columns 25 and 27 of the breadboard, with the terminals on rows
g. To connect this button you will need a wire in any of the open holes in column 25 (top or bottom, whatever is most convenient) and another in any of the holes in column 27.
Installing the jumper wires
The last wiring step is to connect the Pico to the button with two jumper wires.
Take one of the wires, and plug one of the ends in the hole
1a. This hole is connected to pin 1 of the Pico board, also known as
GP0. This is one of the Pico’s GPIO (General Purpose Input/Output) pins, used for connections to other devices.
Insert the other end of the wire next to one of the button’s connectors. It doesn't really matter which one, it can be any of them. I connected it on the
25a hole, which is connected to the right-side terminals of the push button.
The second wire is connected to the Pico on the bottom side, on the
18j hole, which is connected to pin 23. According to the pin diagram this pin is a “ground” pin, or GND, which means that when anything is connected to it, it attracts electricity towards itself.
Connect the other end of the second wire to the second button terminal. In my case I inserted this end of the wire on the
27j breadboard hole, which is in the same group as the lower left-side button terminal.
Testing the push button
The hardware circuit is now complete, so for the next step you are going to write a short CircuitPython application that prints something when the button is pressed.
Remember the code.py file you wrote at the beginning of this tutorial, which made the LED in the Pico board blink three times? Let’s extend that program to also blink the LED when the button is pressed.
Open code.py on your text editor or IDE and copy the following code to it, replacing the previous contents of the file. The parts of this code that are new are highlighted.
import board import digitalio import time gp0 = digitalio.DigitalInOut(board.GP0) gp0.direction = digitalio.Direction.INPUT gp0.pull = digitalio.Pull.UP led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT led.value = False # blink the LED three times for _ in range(3): # light up the LED led.value = True # wait 0.5 seconds time.sleep(0.5) # turn off the LED led.value = False # wait 0.5 seconds time.sleep(0.5) while True: # block while button is not pressed while gp0.value: pass print("Button pushed!") # light up the LED led.value = True # wait 0.5 seconds time.sleep(0.5) # turn off the LED led.value = False # wait for button to be released while not gp0.value: pass
With this version we are initializing two variables
led. These are digital pins of the Pico board that can function as inputs or outputs. We are using the
direction attribute to indicate that the
gp0 pin is an input pin and the
led pin is an output pin. That means that the expression
gp0.value will give us the state of the
GP0 pin (a
False value), while setting
False will operate the LED.
gp0 pin is also configured as a “pull-up” pin. Pull-up pins have some special wiring (that is implemented internally by the Pico board). These are often used to read buttons or other types of switches. The idea is that a pull-up pin is configured internally to have a charge, which means that it reads as a high or
True value when it is not connected to anything.
In our circuit, the
GP0 pin is connected to a
GND (ground) pin with the button in between. When the button is pressed, the connection between these two will be made, and the charge in the
GP0 pin will drain towards the ground, changing the value of the pin to low or
False. In other words, this pin will have a default value of
True, and only when the button is pressed the value will change to
You’ve seen the for-loop in the first version of this file. This loop blinks the LED three times. While this isn’t necessary for our purposes, I find it useful as a visual signal that the board is actually running the code.
The while-loop that follows starts with an inner while-loop that blocks while
True. This is the high state for the
GP0 pin, which corresponds to the button not being pressed.
As soon as the button is pressed
gp0.value will change to
False and that first inner while-loop will exit. At this point we print a debugging message, we turn the LED on, wait for half a second, and then turn it off again.
The last part of the outer while-loop is another inner while-loop that waits for the
GP0 pin to go back to the high or
True value. The idea here is that we want to prevent duplicate firings of the button, so we want to wait for the user to release the button before we end the loop iteration and go back to the top to wait for another press.
Save code.py locally on your computer, and then drop the file on the
CIRCUITPY disk drive. A moment later the board will reboot and execute the code automatically. You will know when the board is ready because you will see the LED blink three times. If you do not see the LED blink, it could be because CircuitPython is still inside a shell sessio, so it is not rebooting automatically when code.py changes. You can disconnect it from power and connect it again to make sure it runs your code.
After the LED does its blinking dance, try pressing the button to see if the LED reacts to it.
Does the LED blink as a response to a button press? Great! You are ready to move on to the last step of the tutorial. If the LED does not blink, then something isn’t working, so you’ll need to debug the problem.
To debug this application, start a REPL with
mpfshell <your-pico-serial-device> -c repl
Ctrl-D to force the Pico to do a soft-reboot and run code.py from scratch. At this point the code will run while you are connected on the terminal, so if there are any errors they’ll be printed to the screen. This should help you debug any issues.
Emulating a keyboard
We are now very close to having this project completed. The last change we need to make to our code.py file is to emit a key to the computer through the USB connection. The computer will be fooled into thinking the key is coming from an actual USB keyboard.
Here is the final code.py, with the changes from the previous version highlighted:
import board import digitalio import time import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode gp0 = digitalio.DigitalInOut(board.GP0) gp0.direction = digitalio.Direction.INPUT gp0.pull = digitalio.Pull.UP led = digitalio.DigitalInOut(board.LED) led.direction = digitalio.Direction.OUTPUT led.value = False keyboard = Keyboard(usb_hid.devices) # blink the LED three times for _ in range(3): # light up the LED led.value = True # wait 0.5 seconds time.sleep(0.5) # turn off the LED led.value = False # wait 0.5 seconds time.sleep(0.5) while True: # block while button is not pressed while gp0.value: pass print("Button pushed!") # light up the LED led.value = True # send Cmd+Shift+A keyboard.send(Keycode.GUI, Keycode.SHIFT, Keycode.A) # wait 0.5 seconds time.sleep(0.5) # turn off the LED led.value = False # wait for button to be released while not gp0.value: pass
The USB HID support in CircuitPython requires us to import three new packages:
usb_hid: the low-level HID support
adafruit_hid.keyboard.Keyboard: the high-level keyboard support class
adafruit_hid.keycode.Keycode: key constants
In addition to the
led variables, we now add
keyboard, which is initialized as an instance of the
Keyboard class from the Adafruit HID library. This object is what will make the Pico board appear to the computer as a USB keyboard.
To send a key to the computer, we use the
keyboard.send() method. Depending on the use you intend to give this application, the key that you send might need to change. The Zoom application uses the
Cmd+Shift+A keyboard shortcut to toggle the audio on Mac, so this is what I implemented:
keyboard.send(Keycode.GUI, Keycode.SHIFT, Keycode.A)
In CircuitPython the Mac’s Command key is called
For a Windows computer, Zoom uses the
Ctrl-A shortcut, so you would use this instead:
You can change the key combination to be anything you want. And if you want to test this without using Zoom or any other application, you can just make the program output a letter A:
To test the application this way, make sure you open a text editor so that you can see the letter appear as you push the button.
Next steps with the Raspberry Pi Pico
I hope you had fun working on this project, and that this motivates you to continue working with MicroPython and microcontrollers.
Are you interested in more articles about the Raspberry Pi Pico? I’ve also written an introductory tutorial that uses the official MicroPython build instead of CircuitPython.
I’d love to see what you build!
Miguel Grinberg is a Principal Software Engineer for Technical Content at Twilio. Reach out to him at mgrinberg [at] twilio [dot] com if you have a cool project you’d like to share on this blog!