Last year, I moved to Denver from Massachusetts for a job, and have since grown to love the city and develop a close circle of friends. I'm not usually a Christmas person, but after seeing these 3D greeting cards hidden in redecorated matchboxes, I was inspired to do something small and cute like that for my friends. I also love LEDs and electronics, so hey, why not thrown some microcontrollers in there and see what I can do?
I didn't really have a specific idea or goal when I started, but went through the constraints to help guide me: what's the smallest microcontroller board I can buy? How many LEDs can I fit in here? Are there enough pins to control them? What else can I add?
Eventually, with enough fussing, I realized I could fit an Adafruit Trinket, and 4-6 DotStar LEDs. This worked out well since I had a whole strip of the LEDs laying around, and I received a free 5V Trinket with that order.
I also began to think about what else I could do: blinking LEDs in a box seemed kinda boring. A piezo buzzer would fit and was really cheap (and would be fun to program).
First order of business was to make a prototype: how easily can I blink LEDs, play a tune, and how big a microcontroller would I need? Would I be able to put this, with the LEDs, piezo, and batteries in something the size of a matchbox, or would I need something bigger?
I had never played around with Arduino, and my digital electronics had been almost entirely using Linux-based single-board computers (SBCs) and some fooling around with a SparkCore. While the SparkCore API is "Arduino compatible" I admittedly preferred to clone their "firmware" repo and build my apps from scratch. This actually proved necessary as trying to use some Adafruit Arduino Libraries required either porting the library or implementing missing features in the SparkCore firmware. This sounds tedious but was so much fun and I learned a lot! I was tempted to go this route with the Trinket (setup an AVR buildchain and makefile, pull in libraries as needed, etc...), but I knew I needed to get this done and built quickly for a deadline, so I figured Arduino was a quick way to do so.
I also had an ulterior motive: one of the recipients had shown some interest in digital electronics, but was weary of microcontrollers and programming. As a result, I wanted to lure him in with Arduino and give him the ability to study and tweak it if he liked.
I ordered some of the parts I planned on using to build a breadboard prototype, and luckily got an Adafruit Trinket for free!
Light Up those LEDs!
This was very easy using the Adafruit library. It presents the string of LEDs as a simple array of values that can be set/read, and the LEDs set with a
show() method call.
Some simple iteration yields the ability to "push" the colors to the strip which made lots of pretty colors! However, this was horribly bright, and just incrementing the RGB integer just made it flash red/purple rapidly while slowly fading blue/purple (from GBR pixel order). Waiting long enough for "green" usually just showed me orange, and blinded me along the way. I eventually got tired enough of this that I made a brightness constant that clamped each channel. But, would need a better cycling program. I iterated a few demos and settled on each R, G, and B channel oscillating up and down at different rates between 0 and the clamped "brightness" value and pushes each new color out after a certain delay. It's not a bad display, colors shift gently around, and every few seconds the LEDs dim when they are momentarily in sync, then gently brighten again.
We're Making Music
With the piezo buzzers in hand I started testing out the ability to play notes out of it, to discover that the easy
tone() function from Arduino was not available for the Trinket! Crud (turns out the Trinket uses a different AVR chip that doesn't have the same hardware counters the beefier ones have that the
tone() function uses). However! Someone very knowledgeable with the timer system in this chip had built a compatible function! Yay! Reading the datasheet for this chip to understand the register use, and trying to understand his code (and tweak it a bit as well ;]) was a lot of fun and scratched my "low-level" itch as well.
There were plenty of examples how to play notes, and this is fairly straightforward: each note is a specific frequency and can be played by
tone(), an array of notes (stored in progmem) can then be looped through using a constant for duration of each note. I had to dust off sheet music reading and find "piano" versions of popular songs. I started off with the Final Fantasy battle victory music (simple, easy, short, fun!). This worked great! I made music and it was awesome! I had to make one adjustment that would pause after each note, to make sure each note was differentiated.
My original goal was to make a different song for each person, but then ran into troubles as I started looking at more tunes. First, note duration is not constant. Second, a lot of the songs I wanted to play used chords...
This wasn't too difficult: add a second array that contains the duration for each note. The difficulty here was mostly deciding what was easy and didn't bloat the program beyond the tiny Trinket's size. I initially just doubled the length of the array and encoded it as
long int notes = [1st note len, 1st note pitch, 2nd note len, 2nd note pitch, ....]; but! this was only usable with the short FF victory music. Everything else I encoded was much longer and left me no space for other things.
Luckily, most music is based on fractions "whole, half, quarter, etc" which went as small as a 1/64 note. So I could store these as short ints instead, and move them to a second array and give me much more space. This also worked well as I changed the playTone usage to multiply a constant by duration so I could change the tempo by changing a single constant. Whoo!
The only other space saving measure I wanted to do was some sort of run-length encoding, but I never ended up implementing this.
Chords are notes you play at the same time! However, mixing sound is something I am very unfamiliar with, and I'm aware is a difficult thing to implement in general, let alone on microcontrollers.
However! I was very aware that the old consoles with chip-tune music did not have intense sound mixers either, but faked it with rapid arpeggios (which gives it that nostalgic and awesome reverbating buzzing sound).
I attempted to implement a class of my own that would buffer notes, and rapidly cycle through them using a constant "arpeggio duration."
The first problem I had was encoding chords. I wracked my brain to think of a clever space efficient way to encode them, but ended up brute-forcing it by just using as many note arrays as the largest chord in the sequence. Yuck. I was only able to encode one song, which was shorter, and had small uncomplicated chords (no "play three note chord, but hold one note for the next chord that has two different notes").
I also just couldn't get this to work... The "mixer" class needed to keep track of notes that were playing: new notes could be added and had to be appended to the list, old notes done playing had to be removed, and to arpeggio properly, the notes had to be sorted by pitch. This meant a lot of logic occurring very often. My first attempt yielded arpeggios, but the song ran at a horribly slow tempo as the program was bogged down in loops...
Simplification and optimization of these routines just resulted in garbage noise. Reducing the "arpeggio" duration helped, but this slowed it down again and made it sound off as well. I kept playing with values and durations, but the best I could get was either arpeggio's that were overly distinct, or rapid ones that skipped notes.
I eventually gave up, but it would be fun to revive this...
Sync LEDs to Music!
I was running low on time, and I tried to set the LED color to the pitch of the current note. The first algorithm I thought of set the brightness to the duration of the note and converted pitch to color, but this was horrible and jarring. I attempted to have the duration set the initial brightness and fade as the node played, but this didn't work with the LED routine that pushed new colors out each cycle.
This was a big TODO on my list that never got addressed... the default algorithm was enough however.
Improving the Prototype
Next step was to figure out how much I could fit in the matchbox, if they're be enough LEDs, and how I was going to wire this whole thing together. A quick prototype of this was easy enough and worked well. A vague-booking post prompted some intrigued/confused comments.
I began pricing out the parts needed to make more of these, and how many I could make with how much I was willing to spend. The size of the matchbox increased a bit as I realized I needed to make room for batteries (this also limited how many I could make as I learned there is a limit to how many lithium ion batteries you can ship at once).
I wanted to fit more in to make it more interesting, and then realized there was a way to cheat: infinity mirrors. I finally had a vision of the box playing a chip-tune when opened, sequence the LEDs based on the notes, and the infinity mirror repeating the display off into, well, infinity.
I had thought about the best way to power this beast. Given the average draw of the microcontroller, and of the LEDs, the batteries would only last a matter of days. My instinct was to use a deep sleep mode and awake on button press. However, the LEDs were connected directly to the 5V output from the power converter and would need to be shutdown separately (otherwise, the DotStar will stay powered and still illuminated with the last color pushed).
I quickly designed a battery holder I could carve out from foam board, and careful application of copper tape gave me a sturdy battery holder that put the two batteries in series (giving me the necessary >5 V I needed for the 5V Trinkets...), but also conveniently placed two contacts underneath the board. I was very happy how well this worked out.
I also had failed to think of a good design using copper tape to allow it to trigger when opened, and without the time or know-how to do either of those quickly, I reached into my parts drawer and grabbed a button and a NPN transistor. I wired those both up to the battery and power supply and it worked! You did have to hold the button down to run it, and there was a very slow startup time from the bootloader... but it worked! I figured if I had the time I'd do this properly, but I did not.
To Infinity and Beyond!
I priced out some one-way mirrored acrylic pre-cut into the size I needed, but I procrastinated and wouldn't have had them ship in time to finish this for Christmas. However, a quick trip to the nearest big box hardware store yielded me a big sheet of clear acrylic and some one-way window film. Perfect.
I didn't have a laser cutter, saw, or any means of a workshop; but, I knew I could cut acrylic by slicing a deep enough cut, then snapping it on a corner. This... worked well enough. I had more than enough acrylic, which was good because I made a lot of mistakes here.
Putting it All Together
With Christmas two days away, I made a quick and rough prototype to find it worked! ...well sort of. The mirrored film stuck well to acrylic, but not to the foam board I used to carve out the battery holder. Some use of clear acrylic fingernail coating I picked up helped it stick and make a more uniform film and was "good enough." Cutting the acrylic by hand resulted in some deformations near the cut edges that prevented the film from fully sticking and would start to peel off after so much handling or time.
Also, I discovered that powering the thing was really annoying! Holding the button down was hard, and the battery holder was failing to keep connectivity if I didn't hold it just right. I quickly grabbed the cardboard from the battery packaging itself and made a "reinforced" battery contact. And... thankfully it worked!!! Huzzah! I started mass producing the matchbox cases for these and finally went to sleep...
Christmas Eve! I frantically began to assemble these monstrosities. I tweaked some small things as I went along: wire placement, button placement, conservative use of hot glue. The assembly was going well, and with more care in placing the mirror film, it was sticking better and being a better mirror.
Awaking early on Christmas morning, frantically assembling the final pieces in time I found that with the reinforced battery contacts, the whole assembly was a few mm thicker than the matchbox and the top piece of plastic was sticking up, preventing it from sliding in the outer case. I was already late for the get together... I quickly made a sleeve slighty bigger and jammed those guys in there. It was messy, it was a little ugly, but it was done and [mostly] in time.
I have pictures from nearly every step of the assembly! ...but this post is already long enough, so I shall save them for a separate write-up ;D
Hindsight is 20/20: Postmortem and Lesson's Learned
I will admit this not only is the first project I have made more than 1 of, but also the first project I've taken past a "prototype" stage, and more importantly a hobby project I actually finished.
The last point is very important, and I have to thank the fact I made these for other people, but also had to have them done by a deadline. This led some important consequences for myself:
- I didn't let myself get distracted by challenging problems
- I avoided feature creep
- I accepted imperfections
When I do things for myself, I have no problem wasting time on challenging problems. I find them intellectually stimulating and that makes things fun. However, I never know when to cut my losses and move on, and those problems usually end up depleting my time and motivation. The result is I get burned out quicker and will move onto something completely different. The decision to use Arduino was also a big boon as I got up and running quickly and was able to focus on the project, not the build system. No one who received one of these would notice or even care if I had used a low-level toolchain to build it. The need to be pragmatic and concede certain design choices in the interest of feasibility is of great importance (of course, I fully intend to still setup a low-level AVR build system ;D).
On the same note, given a simple project, I tend to keep adding more and more things. Not being able to implement newer more complicated things will leave me dejected, and an overwhelming project will leave me frustrated as well. Deciding on a feature set, building a prototype, and conceding some features I wanted was essential to actually finishing. While music synced LEDs would have been awesome, it wasn't essential and is something that can be added later with a quick firmware update.
Lastly, I tend to fuss trying to fix every last imperfection, until I again became dejected and give up without ever "finishing" a project, let alone being happy with the final product. Sometimes to finish something you just have to hack it together. A slightly ugly present is better than no present at all.
I was extremely happy with myself for being able to finally overcome these problems. I gave up on features that were too complicated, such as chorded songs or music responsive LEDs, and finished an actual project that I could give to other people.
That being said, if I was to make more, there are some obvious problems I would fix/work on.
The NPN transistor works better when sinking current as opposed to sourcing it: this may have been the cause of some startup bugs that seemed to happen once in a while (would appear like a partial startup, with some LEDs turned on and a stray buzz or two from the piezo). It would have been better connected to ground.
A copper sliding switch would have been very easy. Two tabs on the outer edges of the inside case connected to ground, and a tab in the middle connected to a pin, with a length of copper foil on the inside of the outer sleeve in the middle. Adjust the width of the middle inside-tab to give you more room to open it. I don't know why the other ideas I had made this so complicated...
Deep sleep mode is a better option to overcome the slow bootloader startup time. A callback function that blanks the LEDs would have been fairly easy and a good way to save battery life. Also, not adding the transistor would have made assembly a lot easier!
There was no need for mirrored film on the foam board: as we're reflecting all the light, I could have just used aluminum foil. While I would have to be more careful while assembling the device, this would have been a cheaper and better looking option.