Making conference badges, official or unofficial, has become an art form. It can get pretty serious. #badgelife.
But DEFCON-goers aren’t the only people making fancy personalized nametags. Hams often had callsign badges going back as far as I can remember. Most were made of engraved plastic, but, at some point, it became common to put something like a flashing LED on the top of the engraved antenna tower or maybe something blinking Morse code.
Going back to that simpler time, I wanted to see if I could make my own badge out of easily accessible modules. How easy can it be? Let’s find out. Along the way, we’ll talk about multicore programming, critical sections, namespaces, and jamming images into C++ code. I’ll also show you how to hijack the C preprocessor to create a little scripting language to make the badge easier to configure.
Bottom Line Up Front
The photo shows the Pico badge. It has an RP2040 CPU but not a proper Raspberry Pi Pico. The Waveshare RP2040-Plus clone has a battery connector and charger. It also has a reset button, and this one has 16 MB of flash, but you don’t need that much. The LCD is also a Waveshare product. (This just happened to work out. I bought all of this stuff, and I don’t even know anyone at Waveshare.) The only other thing you need is a USB C cable and a battery with an MX 1.25 connector on it with the correct polarity. Hardware done! Time for software.
For the software, you have a lot of options. The Waveshare documentation shows you how to drive the display using standard C code and Micropython. I decided to go a different way and stick with the Arduino tools. [Earlephilhower] has a fantastic board support package for the RP2040. I’ve been using it lately with excellent results. It has a lot of great things baked in, including straightforward support for multicore and more.
The badge is more or less a slide show. You can display images and write text either on a solid background or over an image. Each slide appears for a set amount of time although, within limits, you can speed it up or slow it down. You can see a quick demo in the video below.
Obviously, if you want to customize it, you could grab my code and change it. But that means you need to understand a little about it. Who has time for that? Instead, the code can take a script file that — eventually — turns into C code, but you don’t have to know much about the internals of the badge.
For example, suppose you want to show a jolly wrencher with your name written across it. The script looks like this:
Sbwimg(wrencher,GREEN,BLACK) Stext(TCENTER,TCENTER,"-AL-")
There are commands to draw color and monochrome images, fill the screen with colors, and print using various fonts and colors.
The buttons respond almost instantly because they are run by the RP2040’s second core. This is probably overkill, but it is nearly effortless to do. The four buttons along the left side let you select a few scenes or loops, change the slide show timing, and pause the show.
The joystick up and down gives you finer control over the timing, while the left and right jump to different parts of the slide show. Pushing the joystick toggles the screen on or off. To customize the buttons, you need a little custom C code, but you don’t need a deep understanding of the hardware or software.
The Deets
The software only has four major parts: the display, the button interface, services to the script, and the actual script language processing. All of these are simple once you dig into them.
The graphics library I used is the Arduino GFX library. This library is impressive because it handles many different display types. You must define a bus to tell the library how to talk to the display. In this case, that’s SPI on specific pins. Then, the library itself is abstracted from the communication channel. You can easily use the screen as a text output device or a graphics output device. It even supports u8g2 fonts.
Buttons and Multicore
At first, I just checked each switch as part of the standard loop processing. Why not? But there’s a problem. The very nature of the badge means that most of the time, the processor is executing a delay between “slides.” While that was happening, the keys were not being scanned. You could, of course, break the delay into small parts simply to check the keys in between.
However, the RP2040 has two perfectly fine cores, and one of them normally stays asleep. By default, your program runs on core 0 along with the USB serial port. So why not use that extra core? Programming with multiple cores must be hard, though, right?
Actually, no. If you want two things going on simultaneously, you only have to define setup1()
and loop1()
in your program (see core1.cpp). This is like the standard Arduino setup
and loop
, but it will wake up core 1 and run on it. It is that simple.
The issue, of course, is when you want to communicate between the cores. The RP2040 support package does have a FIFO-style form of interprocess communication, but for this simple job, I went with something easier.
The two cores share one variable: button
. Core 0 reads this variable and resets it to zero. Core 1 marks bits in the variable that correspond to active button pushes. It never resets the bits, so the main code will see a button push whenever it looks, even if the button was already released.
The core 1 code also has provisions for debouncing the switches and only reporting a single event to the main thread per button press. The trick is not to have both CPUs reading and writing that variable simultaneously. In a regular operating system, you’d have the idea of a critical section. In this case, I use the API call to freeze the “other” core for the brief period required to read or write the buttons variable. For example:
rp2040.idleOtherCore(); buttons |= xbtn; // put in any that are set; client will reset them rp2040.resumeOtherCore();
I didn’t want to delay the keys too long, so the scaledelay
routine (in badge.cpp) breaks the delay into 100 millisecond slices. Of course, that means delays have a practical resolution of 100 milliseconds, but that’s no big deal for this application. After each 0.1-second delay, the loop checks for any keypress, and if found, the loop exits.
Core 1 delays for 5 milliseconds in its main loop. This gives the other core a chance to run and means buttons don’t need a long press to work. Going shorter has little value, and you could probably go a little longer, which might economize on battery life.
Script Services
I wanted to provide services to the script so you could customize the badge without much effort. There is a callback function (customize
) that, if provided, can do things like pause the slide show or loop it at different points.
At first, I put these functions in a static class. That way, you had to refer to them explicitly from the customize function. If your code had a function called pause
, for example, it didn’t matter because you needed to call BADGE::pause
to get the pause service.
After a bit, though, I realized there was no real value in having it in a static class, and a better choice would be to use a namespace. The customize
code looks exactly the same either way. You still call BADGE::pause
. However, now you are calling a function in the BADGE
namespace, not a static function in the BADGE
class. A small distinction, but handy. For example, you could do a use to import the pause function or even the whole namespace if you wanted to.
In the original code, I had static variables that were “private” to the service code. These variables are now in a nameless namespace. This is very similar to static: the code in the file can use what’s defined there, but it isn’t visible to anything else.
Compiling the Script
The script is straightforward to implement because I’ve hijacked the C preprocessor. Each script step is an element in an array of structures. Then there are C preprocessor defines that set up that array and each element (see pico-badge.h). For example, this script:
Sbegin
Sclear(RED)
Stext(3,10,"Hello!")
Send
Expands to:
SCRIPT script[]= { { CLEAR, NULL, NULL, RED, 0 }, { TEXT, "Hello!", NULL, 3, 10} }; unsigned script_size=sizeof(script)/sizeof(script[0])
That’s it. The preprocessor does all the work of compiling. Just remember that things are set up when the array initializes. For example, the default script uses tdelay
to hold the base delay between slides. Once the array builds, changing tdelay
doesn’t do anything for you. You’d have to modify the item in the array, which is not much fun.
The Stag
macro lets you set a numeric label (like a Basic line number) that you can later find. The numbers don’t have to be sequential — they are just to identify a position in your script. Some of the script helpers take tags so you don’t have to count (and recount) array elements to refer to a specific part of the script.
Images
If you want images, you must put them in arrays. Some websites can help with this for monochrome or color images. The GIMP can also export to .C or .H files, as seen in the video below.
If you start with the code in script.cpp, you should be able to figure out how to get it all together.
What’s Left?
Not bad for some off-the-shelf hardware. It isn’t going to win any prizes at your next conference, but you can easily customize it and make it your own. If you have one at Supercon, find me at the soldering challenge table and show off your build.
I still need to produce an enclosure for the badge, although, with a 220 mAh battery wedged between the boards, it does pretty well by itself. I used a few zip ties and some super glue to make a makeshift connector for a lanyard. I wouldn’t mind putting a clock display option together, but you’d need a way to set the clock. A battery charge monitor would be nice, too.
If you could find a Pico-like CPU with both the battery charger and wireless, that would open up some possibilities. A great deal of the device capability — like PIOs and the USB port — is going to waste, too. Since the Arduino package supports LittleFS, you could read files from the “disk.” It probably ought to be able to play Life, too.
Hi Al. Does it … ahem …
Aaaa, doest it p..
(deep breath) Can you play Doom on it?
The answer is yes, but your buttocks will hurt ;)
Nice project. Keep’em comming.
(Groan!)
Not really, I’m always on the lookout for Dad Jokes!
DOOM was definitely my question
Thanks Al, I love these practical articles.
nice, very nice
but why this is FAT!
look this https://hackaday.io/project/187468-ello-lc1 ok, meybe add battery and solar panel but not fat device
its yours… Whose?
Typo in title still not corrected.
Nice little project – sent you a pull request just to explicitly add the correct pin definitions for the Pico LCD 1.3 board since they are missing from your existing project! Thanks for sharing.