Using a Raspberry Pi Pico with an LCD Screen and SD Card
A universal remote control can eliminate tabletop clutter, and can be used not only for audio and video equipment but also for any appliance that comes with a remote. These two Cornell University students built an easily programmable universal IR remote that could store the remotes of up to 10 different pieces of equipment.
Everyone is doubtless familiar with the classic coffee table chaos of half a dozen TV remotes. Each accomplishes a specific and unique function, but no one knows which one does what. Our project is the solution to decluttering your coffee table! We have designed a universal infrared remote control that can store up to 10 remotes with eight buttons per remote, and can be programmed with any infrared communication protocol. This means that all those remotes sitting on your coffee table can be shoved away in the junk drawer, never to be needed again!
We accomplished this project as the final assignment in our class at Cornell University, “Digital Systems Design Using Microcontrollers” (ECE 4760). We were tasked with coming up with a four-week project that sufficiently utilized the features of the Raspberry Pi Pico we had used all semester. We often found that class projects are discarded after the completion of the course, and we wanted something that we could use later, in our everyday lives; so, we aimed to ensure that this project was designed to be user friendly and convenient to use.
The most crucial feature of our remote that allows it to be legitimately useful is the non-volatile memory of the SD card. This allows the user to have the unpowered remote sitting on a coffee table, and when it is powered on, all the buttons and remotes are still there, ready to be used. Without this non-volatile memory, every time the Raspberry Pi Pico was powered off, it would lose all locally stored memory, except for the code that had been originally flashed onto it. Thus, without the SD card, you would have to reprogram the remote every time you wanted to use it.
OVERVIEW
To program the Universal Infrared Remote (UIR) with all your remote needs, you simply turn it on and plug the USB cable into your computer, activate any serial interface, and press the programming button on the remote. The interface will prompt you to name the remote and button that you want to add. Then you will be prompted to record your existing remote’s infrared signal by pointing that remote at the front of our Universal Infrared Remote, and pressing the desired button on your remote. Once you have recorded the remotes and buttons that you want, you can unplug the USB cable from your computer.
All the devices and remotes will be saved locally on an SD card, so your data will persist through uses and power cycles. If you get a new TV and want to delete the old TV remote data from the device, you simply follow the same instructions as above, but tell the serial interface that you want to delete some data.
Now that all your devices and buttons have been programmed, all you need to do is place the UIR, shown in Figure 1, on your coffee table, and use it as you would any other infrared remote!
HIGH-LEVEL DESIGN: PERIPHERAL COMPONENTS
The Raspberry Pi Pico interacts with five peripheral components at the same time, shown in the block diagram in Figure 2. The first is the LCD display and its included joystick and buttons. This display shows all the information and controls needed to operate the UIR as a remote in daily life. It communicates to the Pico through one of the two built-in SPI channels. The second and third peripherals are the infrared LED and receiver. These two components are the only external communication with outside devices. The receiver is used during the recording process, when programming the remote, and the LED outputs an exact replica of the recorded signal when the output button is pressed. The fourth peripheral is the SD card [1]. The SD card is interfaced to the Pico through a base plate that allows all the necessary SPI and control lines to be accessed easily. The last main component is the battery pack. It consists of two AA batteries in series, to create the needed voltage to power the Pico.

Block diagram showing the five peripheral components with which the Raspberry Pi Pico interacts.
HIGH-LEVEL DESIGN: SERIAL INTERFACE
You can program and delete buttons and remotes via a custom serial interface. To begin utilizing it, you need to connect a micro-USB cable to your computer, open a terminal emulator, such as PuTTY (which is free and open source), and press the button labeled “B” on the UIR. For the set-up of the program, we set the serial interface at a baud rate of 115200, which is what the Raspberry Pi Pico uses by default.
First, the interface asks you whether you want to add or remove a button or remote. You select your choice by entering the number corresponding to your desired choice. To make the user experience as simple as possible, adding a button requires only that you enter the name of the remote that you’re adding it to. If you enter a remote that is not already in the system, the device will create a new remote with that name. Accordingly, there is no “add remote” functionality, because adding a button allows you to create a remote easily, and a remote with no buttons does not make sense.
Next, the interface asks for a remote name, listing the currently assigned remote names for ease of access for reference. To remove a button or remote, the name that you type in must exactly match that of an existing button or remote. To add a button, the interface allows a new name to be typed, and adds the remote if it doesn’t already exist—provided that the remote size does not exceed the capacity of the device (10 remotes maximum, eight buttons per remote). The interface then asks for the name of the button to be added. Once that name is entered it creates that button and pauses everything else in the system to wait for an infrared signal to come into the receiver.
After every request is fulfilled, the system closes out once more, and relinquishes control back to the LCD screen until the button to re-open the interface is pressed again.
HIGH-LEVEL DESIGN: USER INTERFACE
The desire for practicality limited the size of screen and buttons that we could use in our design. We selected an LCD screen and button set that has been adapted to be used specifically with the Raspberry Pi Pico, because of its compact size and preexisting C library [2]. This specific display communicates over SPI, and it uses one of the two SPI channels built into the Pico. Each button has its own GPIO pin, which means that, by the final implementation, we had used almost all the pins of the Pico. It also meant that we did not need any additional hardware to work with the buttons.
The digital interface design was developed to be as clear and streamlined as possible; thus, there are only four main display states. The first display state is the “boot screen” (Figure 3). It lets the user know that the device is running, and it also buys time for the SD card to read all the data before needing to display it.

The initial boot screen shown on the LCD display tells the user that the device is running, and allows the SD card to read all the data before displaying it.
The second screen displayed allows users to navigate through their programmed remotes. A small red bar on the left side of this “Choose a Remote” screen (Figure 4) shows which remote is selected. If there are multiple remotes, the user may scroll to the desired remote using up and down on the joystick. The user can then press in or right on the joystick to progress to the buttons stored in the highlighted remote.
Once the user has selected a remote and advanced to the buttons within that selection, the third screen, “Choose a Button” (Figure 5), allows the user to highlight the desired button with the same technique (using a joystick) as with the “Choose a Remote” screen. If the user wants to broadcast the signal of one of these buttons, the button is highlighted and the “A” button on the top left of the display plate is pressed. If users do not want to broadcast any of these buttons, they may go back to the “Choose a Remote” screen by pressing left on the joystick.

The”Choose a Button” screen on the LCD display lets the user highlight a particular button.
If at any time the users want to program a new button or remote, they may press the “B” button on the top right of the display plate. This displays the fourth screen, “Running Serial Interface” (Figure 6), which prevents the user from interacting with the LCD. All the user input will then be handled by the serial interface on a computer.

The “Running Serial Interface” screen on the LCD display prevents the user from interacting with the LCD while programming a new button or remote.
HIGH-LEVEL DESIGN: DATA STORAGE
All data management with the SD card is abstracted away from the users, so that they can simply enjoy using the remote. Upon powering the device, all the data is read from the SD card to the Pico; the data will never be read from the SD card again while the device is powered. The data is written to the SD card any time the user changes something through the serial interface. This limited data handling ensures that all transactions are as secure as possible, and the risk of data loss is as low as possible.
Since we chose to use an SD card as our non-volatile memory solution, the number of remotes and devices is still limited by the volatile memory capacity on the Raspberry Pi Pico. The only issue presented by our choice to use an SD card is the strict communication requirements. To accomplish this system, we used an existing C library that can handle all FAT32 file transactions with the SD card over SPI. Although SPI mode is a slower method of communicating than SD mode, the Pico only has native support for SPI.
HARDWARE DESIGN
Although this project is electrically simple, all but two of the Pico’s GPIO pins were used. Figure 7 is a schematic diagram showing the five main components of this system and how they are connected to the Pico. The LCD shield is shown with Xs on the pins that are not used by the shield, and the Pico is shown with Xs on the pins that are not used at all. This allows you to see how only GPIO 14 and GPIO 22 are the only two remaining GPIO pins not used and that are not reserved for a special function.
Many of the components were soldered in free space, since we felt like there was not enough strict circuit complexity to require a more sophisticated solution, such as a PCB. This also meant that exchanging circuit components, if anything got fried, was a simple task. We also had originally used socket jumper cables to connect the SD card breakout board to the Pico, but after extensive debugging, we determined that those connections were not stable enough; so we had to solder wires between the Pico and SD card breakout board. This solved the problem of the SD card occasionally losing its SPI connection to the Pico and failing during a read or write sequence.
SOFTWARE DESIGN: IR COMMUNICATIONS
The code for this project is available on Circuit Cellar‘s Article Materials and Resources web page.
For the IR Communications, the key element is precise timing. As a result, our approach was to halt any potential side processes of our system as the IR communications are under way. This is done by only running the IR communications off one core, and shutting off the other core while handling any requests. Once the IR request is completed the other core resumes running, and the device returns to normal functionality.
IR protocols wildly differ from system to system, so the first element to address is how to properly record and store the information about a given IR signal into our system. After some thinking, we realized that there were two key elements that connected all IR signal protocols: precise timings (as noted above), and a binary state of the IR LED.
We decided that the best approach would be not to attempt to decode what the individual IR protocol was and what data the transmission contained. Instead, we would directly mimic the signal by precisely measuring each time the signal is toggled from on to off or vice versa. This then gave us a timing road map of any infrared signal received that told us when we would need to turn the IR LED on or off. This clever solution allows us to avoid limiting the device’s functionality to work only with the IR communication protocols that we had time to program in. Instead, this device works with any IR communication protocol.
To record the timings of the IR signal received, we used an interrupt that was triggered by a change of state on the IR receiver input pin. To ensure that no bit flips are missed, the interrupt service routine (the code that runs when an interrupt is triggered) needs to be a short as possible. We did this by simply adding the system’s clock time to an array that we later process more to clean up the data and make it usable by our IR LED outputting function. We also start a timer on every interrupt triggered. It serves as a timeout trigger that lets the code know when the IR transmission is finished, and we can give control back to the other core.
Once the system detects that the timeout of the system has occurred, it encodes the signal before storing it into its proper button. This means that it takes the difference in time between every two different steps of the signal to make the array a list of wait times, rather than the exact system time when the user happened to record the signal. This encoding process is careful to preserve the first index of the array that dictates the tail of the signal.
When the output button on the remote is pressed, the LCD button state machine sends out the encoded signal associated with the selected button to a different method running on the other core to perform the broadcast. The broadcasting method simply turns on or off the IR LED for the time allotted in each index of the array. This results in an exact replica of the inputted signal, without having to know which specific proprietary IR protocol is used. Once the LED broadcasting is complete, the system simply relinquishes control back to the LCD methods once more, to resume normal remote functionality.
SOFTWARE DESIGN: SERIAL INTERFACE
The serial interface is how you can program new buttons and remotes and remove the old ones, if necessary. The interface was written so that it prompts the user for everything that is needed, and shows all the possible input options, so that no one is confused when using our system. We used a protothread library [3], so that we can easily yield the thread when not in use. This allows us to minimize power consumption because a protothread yield does not use constant CPU cycles as does any loop-oriented yield technique. This was advantageous, in that it should increase the battery life of the device.
While the serial interface is running, the LCD screen handling is not, though the methods are being run on separate cores. We did this to minimize the risk of data losses when recording an IR signal. Although we never actually saw any losses due to this issue, we figured it was better to be safe than sorry.
SOFTWARE DESIGN: LCD INTERFACE
The LCD interface runs entirely on Core 1 of the Pico, and makes use of an LCD library that came with the LCD shield [4]. This greatly simplified the user interface development process, but this was our second LCD shield. The first shield we purchased did not have any libraries written in C, and so we decided to purchase a backup option that did.
The user interface is segmented into two states. The first state refreshes the display and handles button inputs. The second state saves the data to the SD card. These were segmented in this way to ensure that the SD card SPI timings were not interrupted by any context switches or other CPU actions taken.
The state that refreshes the display is made up of a finite state machine and some abstracted display handling methods that allow the data to be generalized. This results in a very readable chunk of code, which can handle a lot of buttons and remotes, and can be easily expanded to handle more, if needed.
SOFTWARE DESIGN: DATA STORAGE
The writing and reading of data to and from the SD card happens over SPI and is configured using an SD card library [5]. This aspect of the project required the most tinkering to get to play nicely with the other components. The SD card Fat32 file system is very finicky and requires a large library to be integrated into normal C code.
To ensure that the data on the SD card is up to date at all times and the display of data on the LCD interface is as fast as possible, the data is only read from the SD card once. This happens during the boot sequence of the remote, and is hidden behind the boot screen. This results in the data being immediately populated on the LCD screen when the display switches to the “Choose a Remote” screen. The data is also only written to the SD card when a change is made to the data through the serial interface. This is because writing to an SD card might accidentally wipe the SD card, if the transaction cannot be fully completed. We therefore tried to access the SD card as few times as possible to minimize risk; this is also why Core 1 does not run the LCD when the transaction is completing.
Data is written to and read from an SD card over SPI as text line by line. The data is stored in a .txt file. Thus, our data had to be stored in an intelligent way to ensure fast reading. We chose to store the data in the following format:
> remote
> “the remote name”
> button
> “the button name”
> signal
> “signal[0]”
> “signal[1]” …
This results in simple file read and write methods that are efficient and can be expanded effectively indefinitely. Also, due to the text file format and this data storage technique, debugging the storage system was as easy as possible.
RESULTS AND CONCLUSION
The final design of our remote, despite being physically made of cardboard and electrical tape, is well polished and functions as well as we ever could have hoped. We were able to ensure that our IR communications were exactly the same as the ones coming from the original remote by using an oscilloscope to capture the output of the original remote and the output of our remote. This gave us confidence that we had successfully designed a truly universal infrared remote. We were then able to demonstrate this in a video showing how we easily controlled an air conditioning unit without the original remote control [6].
We noticed that the resistor we used with our IR LED was too large. This gave our remote an exceptionally short range (only a few feet); but this error was an easy fix after our demonstration. We also observed that fewer and fewer devices these days use an infrared communication system, rather than another wireless technology such as Bluetooth or Wi-Fi. This means that our UIR may have been most useful a decade ago. Nonetheless, it has proved itself to be useful and was an enjoyable project for us.
RESOURCES
Adafruit | www.adafruit.com
Raspberry Pi | www.raspberrypi.com
REFERENCES
[1] SD Card Breakout Board:
https://www.digikey.com/en/products/detail/adafruit-industries-llc/4682/12822319
[2] LCD Screen: https://www.waveshare.com/wiki/Pico-LCD-1.14
[3] Protothread Library: https://people.ece.cornell.edu/land/courses/ece4760/PIC32/index_Protothreads.html
[4] LCD Screen C Library: https://www.waveshare.com/wiki/Pico-LCD-1.14
[5] SD Card C Library: http://elm-chan.org/fsw/ff/00index_e.html
[6] Video of universal IR remote implemented on RP2040:
https://www.youtube.com/watch?v=OaMCaecrE4o
SOURCES
AA Battery Holder:
https://www.amazon.com/dp/B09V7Z4MT7?psc=1&ref=ppx_yo2ov_dt_b_product_details
Infrared LED:
https://www.sparkfun.com/products/9349?_ga=2.201535761.1075059845.1667497389-1261656821.1667497389
Infrared Receiver: https://www.sparkfun.com/products/10266?_ga=2.197351087.1075059845.1667497389-1261656821.1667497389
— ADVERTISMENT—
—Advertise Here—
Sponsor this Article
ABOUT THE AUTHORs
Owen Louis, received a BS in Electrical and Computer Engineering from Cornell University in 2023. He is from Chicago, and currently works as a Quality Engineer at Palantir Inc. in New York City. He loves applying his math, physics, and embedded systems knowledge to real world problems. He has worked on the Baja Racing project team for 4 years, and enjoys the bridge between electronic and mechanical systems.
Liam Sweeney, is a graduate student at Cornell University studying for a Masters of Engineering in Electrical and Computer Engineering. He received his undergraduate degree in Computer Engineering from the University of Massachusetts, Lowell. He loves working in the field of embedded systems, to apply an electrical and programming skill set toward the emerging problem or project. He also enjoys playing electric bass guitar, and has worked as a DJ with his own show on the UMass radio station, WUML.




