The Nyan Cat badge demo started as a project for me to familiarize myself with badge firmware source code. It has since evolved into a piece of educational sample code for Supercon badge hackers as well as a fun little app to run on everyone's Supercon badge.
This project page tells the story behind my Nyan Cat demo, part of the 2018 Hackaday Superconference badge. (Everything was written up after the fact, so that's why all the timestamps are after the conference.)
Nyan Cat badge demo project met its goals of:
Giving me hands-on experience with Jaromir Sukuba's badge firmware code
Serve as sample code to guide others in the same code base
Fit within the limited free space left on existing badge firmware
Be a fun little app for people to run on their badge
Thanks to the following people who made this project possible:
I had a great time at 2018 Hackaday Superconference and I think Nyan Cat was a success as part of the conference badge. I’m happy with it, even though I found two minor problems with my Nyan Cat app during the weekend.
The first problem was that it does not tell the badge it is running, which is required to prevent automatic power-down. The auto-sleep feature was added after my code was merged into master, shortly before the badges started getting flashed en masse. I knew this power saving feature was going in but I was busy with badge production. So I didn’t have the chance to add code to keep the badge from going to sleep while Nyan Cat is running. My life was filled with rows and rows of badges.
What this means is that Nyan Cat couldn’t just keep running on a badge forever. The badge will go to sleep and need to be awakened for animation to resume.
Somewhat related to the above, there’s a problem with timer synchronization upon wake. It appears that when the badge is asleep, the main timer still advances at some rate. I believe this is a side effect of loop_badge() in badge.c. Called by timer #5 every 15 milliseconds to check the status of the power button. During this check, the main system timer (running on timer #1) clicks upwards even though the rest of the badge is asleep.
What this means is that, if the badge is running Nyan Cat when it goes into low power mode, the timer will advance even though the animation & music does not. As a result, when the badge wakes up, the loops in charge of animation and timing will frantically try to catch up. It only takes a second or two to get back in sync, but in that brief moment we get a comically distorted kitty running and singing at warp speed.
This second problem can be reproduced by:
Launch Nyan Cat with 'nya' to see and hear dancing singing pop tart cat.
Push the power button to put the badge in low power mode.
Wait about 30 seconds.
Push the button again to wake up the badge.
See and hear cat in hyperdrive for a few seconds before slowing down to normal speed.
I might go back and fix these bugs in the future, but they’re not horrendous embarrassments (and Nyan Cat in hyperdrive is pretty hilarious) so I’m content to leave them as-is for now.
In my pull request for Nyan Cat to go in the Hackaday Superconference 2018 badge, I included a way to launch it by typing ‘nya’ at the main menu. It existed mainly because I felt silly to create a pull request for a feature that was impossible to launch. This was originally intended to be a placeholder and be replaced by something else, but due to time crunch that “something else” never happened. So the final badge firmware flashed to every unit distributed to Supercon attendees had Nyan Cat launched via ‘nya’. It’s not the original intention, but also not the end of the world.
What this meant was that Nyan Cat badge app is now also an unplanned social experiment. By default only people who comb through badge firmware source code will find it. I didn’t think that would be enough, so on Friday I walked around telling people about it while the animation was playing on the badge around my neck. I think I told roughly one dozen people (some of them in groups) by the time Friday evening’s official kickoff party was done. Saturday morning at the official opening ceremony, I saw a person I didn’t recognize with Nyan Cat running on their badge. At that point I felt the knowledge seeding stage was complete, and it was time to let the word-of-mouth propagation take over.
One thing I thought was interesting: I had programmed a mute button to the app so it’s possible to play the animation without the music. Every time I told someone about Nyan Cat, I also mentioned ‘0’ would mute and ‘9’ would resume playback. However, as the weekend went on, I realized not everyone who knew about ‘nya’ knew about the mute button! It’s possible these people found out about ‘nya’ by reading the badge main menu source code (which launched the app) but not the app’s own source code (where the mute code lives.) It was also possible it got lost in the word-of-mouth propagation.
I knew there was a risk that people (including myself) would be annoyed at the music by the end of the weekend. But the sheer number of people packed in a small venue created a noisy enough environment that the music is actually a little difficult to hear over ambient noise. I didn’t encounter any complaints about it being a nuisance. Whew!
There are just a few more touches before the finish line for getting my Nyan Cat project ready for consideration for the Hackaday Superconference 2018 badge. The final space savings came from realizing I only used two out of three voices in music playback, but my song structure carried three values so the unused voice takes up space holding zeroes. Maybe it’s only a few hundred bytes, but that’s still waste I could trim.
Once trimmed, I reviewed my code and realized I was still reading key-presses by reading the raw IO lines instead of using the existing stdio_get() API. This was a mistake made early on, when I thought it would block my animation/music until a key is pressed. Functionally speaking, neglecting to use the API not a terribly huge deal, but if I want the program to serve as example code it should do the right thing.
And finally, code comments. I want this to be something people can read through and understand how to write code for the badge in C using some techniques that are absent from other examples on the badge. This is an important part of the pitch for putting Nyan Cat on the badge – it should be educational in its construction in addition to being entertaining to launch.
After I committed the final code updates and comments, I realized I forgot to update the memory footprint listed by the flag to turn Nyan Cat on/off in a build. Final tally: 84 bytes of data memory, 8468 bytes of program memory. This is over the 8 kilobyte goal I was shooting for, but certainly a tremendous reduction from the original 30+ kilobytes.
I built my Nyan Cat project on the Hackaday Belgrade 2018 badge. Now there’s a slim opening for it to be part of the Hackaday Superconference 2018 badge. The key is to get Nyan Cat skinny enough to fit through that door. Most of the 512 kilobytes of program storage on the badge’s PIC32MX370F512H chip have already been spoken for, leaving only about 16 kilobytes available. I set out to take up less than half of that space. I’m now just over 10 kilobytes, I need to trim another 2+ kilobytes.
The key insight for more savings is realizing that we now store a quarter-scale image of 80×60 pixels and scale up at rendering time. Since they are encoded (and decoded) one scanline at a time, this means no single run length of pixels can be longer than 80. Previously, 12 bits were used to store length because a 320×240 image may have a run of 320 – longer than the maximum 8-bit value of 256. Now I only need 8 bits for run length. (The color palette always had only 14 colors, so it still needed only 4 bits.)
This trims every run in the image, color index + run length, from 16 bits down to 12. This did indeed trim data down to a little over 8.5 kilobytes. But code readability took a hit as a result: the smallest convenient unit in C is in multiple of 8 bits, so working with 12 bit values involve a lot of bit manipulation to pack and unpack that data.
Now I’ve got Nyan Cat animation running while playing a simplified adaptation of the Nyan Cat music on my Hackaday Belgrade 2018 badge. I started writing run-length encoding on Friday, had a crash course in reading sheet music on Saturday, and by end of Sunday I had music and animation loop running together. This is roughly the same timespan that will be available to people putting their own projects on Hackaday Superconference 2018 badge, I just had the opportunity to do it a week ahead of time.
The original intent was twofold: One, to give myself some hands-on familiarity with the badge code base and two, have Nyan Cat project code available for other badge hackers as reference. I originally planned to have Nyan Cat up on Github for people to look at, but with this success my ambition grew: can this musical pop tart cat be part of the default badge firmware?
The greatest constraint on badge firmware is available space. Before I started this exploration, I hacked and slashed a bunch of features to make things lightweight, but if Nyan Cat is to be on the basic badge, it must coexist with all those features I had cut. And there isn’t enough free space for >30 kilobytes of Nyan Cat.
The majority of that space were consumed by run-length encoded animation frames, so that’s an obvious place to start. Code – both on encoding and decoding – most easily accommodates even power-of-two reductions. Nyan Cat is a blocky little kitty and should preserve well in downscaling from full resolution (320×240) to half (160×120), quarter (80×60), and eighth (40×30.) Here they are displayed in their pixel dimensions.
And here they are with pixels scaled up to fill a 320×240 screen, as it would on the badge.
There is visible degradation from full to half resolution, but not terrible. Nyan Cat starts getting pretty blocky at quarter resolution, but still recognizable. Eighth resolution is not usable.
So it looks like quarter scale is the sweet spot to target. This reduces the animation data from over 32 kilobytes to a little over 8 kilobytes. The music data and the program to bring it all together adds roughly another 2 kilobytes. It’s a good start but there is still room for improvement.
The first reference was the “play” button on musescore.com. When playing the song, the web page also highlights the current measure being played. This information may be obvious to experienced musicians, but super helpful to newcomers like myself trying to decipher sheet music. With this highlight tool, I could see which measures are played and the patterns in which they repeat to build the Nyan Cat song.
The second reference was the existing “mario” song on the badge, which showed one way to organize a song in code. Each measure is in its own array, and music playback consists of making calls to play measures in a particular sequence. I built on top of this idea and built another array nyancat_measures around them for convenient indexing. Then a separate array nyancat_sequence was created to track the order in which to play the measures. Playing the song would then be a matter of walking the sequence array in order and play each indexed measure as we go.
And finally – it’s time to put the sights and sounds together! The Nyan Cat badge app runs in a tight infinite loop checking the time via the millis() API. The video playback speed is controlled by one variable time_for_next_frame tracking the time for the next frame, and now music playback will be tracked with time_for_audio_update.
Is this perfect? No.
Unpacking and rendering a frame of animation takes a few tens of milliseconds. Occasionally this overlaps with a desired music start time, causing a note to end up playing a touch too long. The converse is also true – if music processing is underway at a desired animation frame time, there will be a brief animation glitch.
But there was a big problem: neither [amybaldwindev] or I knew how to read music.
We did what anyone would do in this day and age: search for “How to read sheet music” and read the first link. For the purpose of this specific project, I was not concerned with understanding the notes on the staff as I only needed to match them up against the reference chart on the badge music page. The duration of each note, however, proved more challenging. On several occasions we thought a note sounded wrong but it turned out just to be their duration
We started with a piano adaptation, but several attempts at translating it to badge music format failed to create something that sounded like Nyan Cat. Eventually we switched to this version – intended for violin – whose translated result running on the badge sounded like we were on the right track.
I’m quite happy I got the Nyan Cat animation up and running, but the Nyan Cat experience is not complete without the catchy cutesy music. That music was an important reason for choosing Nyan Cat as the project.
Since Nyan Cat is a YouTube sensation, the obvious search for music information started with YouTube. Quite a few people have translated the music into piano and while a lot of notes go by very quickly, the simplified versions of Nyan Cat music seem to require no more than 2 keys at any given time.
This can be translated to fit on the badge, using two voices and one voice to spare. It leaves the door open to reuse the third audio timer for some other task.
It wouldn’t be hard to translate the piano keys from the YouTube video. Tedious, but not hard. Fortunately there are internet resources other than YouTube. A search turned up the web site musescore.com full of songs in sheet music format. Each of which was contributed by an user on the site and, given Nyan Cat’s internet fame, it was not a surprise that there were multiple takes on translating the catch tune into sheet music format.
I’m adapting the Nyan Cat animation to the Hackaday Belgrade 2018 badge (close predecessor to the Hackaday Superconference 2018 badge) and I chose to simplify the animated GIF data to a simple run-length encoding. RLE doesn’t compress as much as GIF, but it makes the decode algorithm simple. A simple algorithm takes up little program memory on the PIC32, and more importantly, it takes up less of my time coding and debugging. The latter is quite important when I’m trying to do something within the timeframe of a single weekend, as most Supercon badge hackers do.
The simplicity paid off when I wrote the code – just a few lines and only a few variables – and ran it on my Belgrade badge. The first test only decoded part of the first frame. I was actually quite surprised when the face of a cartoon cat looked back at me from the badge LCD! It is rare when a chunk of code works immediately on the first try.
After the first partial frame was proven to work, I extended the for() loop to the entire image, then I wrote an infinite loop to cycle through all 12 frames of the animation. I made the design decision for code simplicity and it paid off tremendously. Drastically reducing the time spent on implementation and (quite unexpectedly) completely eliminated the time spent on debugging.
If I had tried to port a GIF decode library I’m certain I would have needed to spend more time getting the code to compile and run on the badge, and it would not have been fun to debug if anything should go wrong.
At first glance, converting the Nyan Cat animated GIF to a run length encoded image doesn’t make a lot of sense: I’d need to decode the GIF at some point anyway, why go through the effort?
The key point is that the conversion process can be done on a desktop PC, with far more powerful and user-friendly tools. Yes, I could have done the work to port a GIF library to the PIC32 chip running the Hackaday Superconference 2018 badge, but on a desktop computer I could fire up Python, “from PIL import Image“, and be off and running manipulating a decoded GIF image sequence.
Once I got over the (small) hump of initial learning, it was easy to write a Python script to do everything I wanted to do to the image:
Downscale from original 400×400 resolution to 320×320
Crop off the top and bottom to obtain a 320×240 image
Perform run-length encoding of resulting image.
Output encoded bytes.
In step 4, the output comes in the form of text: specifically, C source code. Each encoded image is a static C array declaration that I could copy and paste into MPLAB X for storage into PIC32 program memory.
I had hoped the simple GIF would compress well with run length encoding, and the first results were quite promising: a little over 2 kilobytes for the first frame, and there are 12 frames in total implying the neighborhood of 24-28 kilobytes.
The original Nyan Cat GIF takes up 30 kilobytes. Granted, it is higher resolution and different aspect ratio, but this is going to make the PIC32 side programming far FAR easier. Which not only makes it easier for me to write, but also for others to read and understand in a little demo. I’m willing to trade a bit of space for those benefits.