An atmega328p AVR-based LED matrix for marquee messages, in a credit card sized form factor.
This project came from an idea to make a gift for my girlfriend at the time, Caroline, for our one year anniversary. The idea was fairly simple- a small, custom circuit board containing an LED matrix, coin cell battery, and buttons. Upon pushing a button the LEDs would flash to display a message. The message would be a random selection from a list of fun memories that we have together, inside jokes, etc. The circuit board would be the size of a credit card, allowing it to be carried around in a wallet or purse.
The project had the extra benefit of being a great learning experience. For one, it was an excellent opportunity to practice hand-soldering on SMT components. Soldering 0402 packages by hand is no small feat, especially if done without a microscope. On top of that, this was great practice at designing and taping out boards. At work I frequently design 8+ layer RF boards with HDI, etc but the details of actually producing gerbers, fab drawings and doing the manufacturing are handled by other teams. Being able to do that without Microsoft’s support teams is a valuable skill.
Finally, this was a chance to fine tune and try out my new hardware bench setup at home:
All schematics and layout were done in Cadsoft Eagle PCB. I’m used to using Cadence at work, but the yearly license fee of >$100,000 per year is just a little bit outside my budget for personal projects. It’s pretty tough to beat Eagle’s low, low price of free. Plus, there are extensive component libraries available freely online, saving me from having to create symbols and footprints for my parts. The free version of Eagle limits you to one schematic page, 2 layers maximum PCB, and a maximum PCB size. I intend to get the $160 hobbyist license at some point, removing these restrictions, but I can fit this project inside the free version’s constraints.
Conceptually, the circuit design was very simple. I decided to base the board’s MCU on the Arduino Mini. Using an Arduino gave me access to Arduino’s extensive software codebase, saving me from re-implementing blocks like FIFOs and I2C communication. Like the Arduino mini, at the heart of my board is the ATMEGA328p microcontroller. These are pretty amazing devices in terms of how much compute power and memory can be fit into such a small package. Like most microcontrollers these days, the ATMEGA has the option to use an internal RC oscillator as the system clock, trading reduced BOM and complexity for a less accurate clock. Because I’m trying to minimize things and keep it as simple as possible, I opted for the internal RC oscillator. I’ve never used the internal oscillator option before so I was a bit worried about it’s poor accuracy mucking up any I2C communication. To mitigate this I put in the hooks for an external xtal as a backup. Two user buttons for input, plus a reset button make up the input. After sending the board off for fab I realized that I had forgotten debounce caps for all the buttons. I implemented a software debounce just in case, but ended up not needing it. I should have probably used a bit more decoupling on the microcontroller, but everything’s very low speed here so I got away with it.
For power, I chose a CR2032 battery. These run around 3V, so no external power management necessary. The ATMEGA328P runs down to 1.8V (with internal brownout detector disabled), so even as the battery’s voltage droops as it drains there should be no problems. I might be concerned if it was running near maximum clock frequencies, but at 1MHz, no problem.
A typical CR2032 has 235 mAh of juice. A quick power ballpark- assuming that for one button push, the average message lasts 10 seconds, and half of the LEDs are on at any given time, and the LEDs consume 10mA each, the battery should last around 200 messages. Not bad! This of course neglects the power consumed by the MCU and the IO expanders, which can safely be neglected in this context.
In retrospect, with such high currents going into the LEDs (~400mA peak assuming half of the LEDs are on) I should have added some decoupling to the battery itself. These batteries tend to have high ESR, especially as they drain down, so pulling such a high current could have caused the voltage to droop so much that it would cause erratic behavior in the MCU. Fortunately, after some testing I found that this wasn’t the case. Phew!
Given more time I would have loved to implement a low-power mode for the MCU, turning it on upon an interrupt from one of the message buttons. This would have been cool, there would be no need to an on/off switch. Unfortunately I ran out of time and couldn’t add this feature. I went with a simple on/off switch to turn off the device when not being used. This kind of takes away from the experience, but compromises have to be made. The MCU pulls about 0.3mA when in idle mode, so even without sleep mode the battery would last ~30 days, but I deemed this unacceptable. Those dang batteries are expensive. Even cooler, it would have been great to implement a proximity detector, so that if a user walks by the device wakes up and displays a message, but this would even further reduce the standby lifetime. Also, time constraints.
LEDs and driver
The LED display was implemented as a 7 x 11 array. Choosing these dimensions allowed me to use fonts from 5×7 LED matrix displays, which are easily found on the internet. I considered implementing the LED driver using the ‘scan’ technique, where LEDs are flashed sequentially but very quickly, and persistence of vision makes them appear as a coherent image. This technique trades fewer pins (7+11=18 pins vs 7*11=77 pins) for increased code complexity. Because my goal was not to write fancy code but instead to improve my soldering, layout and general EE skills, I opted for the ‘brute force’ method of assigning one pin per LED.
For LED driver, I chose NXP’s PCA9506. Two of these 40-bit I2C I/O expanders fit the need perfectly. Pin strapping options allow for different I2C addresses. The IOs can output up to 50mA per pin, way more than necessary to drive some lowly LEDs.
For LEDs, I went with the Lite-On LTST-C194TBKT , because they’re blue, cheap and have good brightness for their price. The LEDs were driven directly from the output of the IO expander, with a current limiting resistor. Fancy drivers are overkill in this situation.
One downside of choosing a blue LED is high Vf. These LEDs run around 2.9V at 10mA. I used a 10 ohm resistor to set the current. The issue with this setup is that with such a low voltage overhead in the resistor (~0.1V) the LED current and therefore brightness is highly sensitive to process and temperature variation in the LED. It also increases the risk of thermal runaway in the LEDs. Because I was using the cheapest LEDs with wide binning on color and intensity anyways, I decided that some variation due to the driver scheme was acceptable- there’s no way to get around the variation.
I assumed the LEDs all came from the same reel, were from the same production lot, and therefore had little process variation between them. This can oftentimes be a dangerous thing to rely on in analog design, but in this case the only possible consequence is a little variation in LED color and intensity.
Regarding temperature variation, I assumed a few things. First, the LEDs were going to be run at very low power and would have a very small temperature increase over ambient. Also, because they would all be connected to a huge, 1oz copper ground plane, they would be thermally coupled. These assumptions do the double duty of alleviating concerns about temperature variation, as well as concerns about thermal runaway. Check and check.
While at work I’m used to designing boards with up to 12 layers using Cadence tools, I don’t get such luxuries at home. The free version of Eagle limits a board to two layers. This turned out to be most constraining in the area of the IO expanders, especially when fanning out signals to 77 LEDs. Fortunately there were no high speed signals to route, or else it could have been a problem. I2C is pretty forgiving at 100KHz, especially with some stiff pullup resistors on the bus.
The design intent was to have all user-facing features on the top side. This included LEDs, buttons, on/off switch, and the battery. Because this was the cosmetic side, I wanted to minimize routing on the top. Any traces on the top would be covered by soldermask, but still wanted to avoid this as much as possible.
The LEDs were arranged in a 7×11 array on the top side, taking up as much space as possible on the credit card sized board. Having the battery holder, buttons and switch all along the western edge allows the user to hold the board there without obscuring the LEDs.
The bottom side holds the majority of the components. The microcontroller sits along the western edge, along with hooks for external crystal, reset circuitry and button, I2C pullups, and pullups for UI buttons. Also on this side of the board sit 8 pads to solder wires for programming the ATMEGA. In addition to power, ground, and reset, three pads make up a SPI connection for programming the ATMEGA with an arduino bootloader using atmel studio and an AVRISPmkII. The remaining two pads are UART RX and TX. Once a bootloader is programmed, these pads allow the ATMEGA code to be updated inside the arduino environment using an FTDI USB-UART board. In retrospect, these pads should have instead taken the form of a header for the avrispmkii programmer so I wouldn’t have to deal with loose wires. At the very least I could have placed signal names in silk. Lessons for next time.
Moving east on the bottom side of the board are the two TSSOP IO expanders, decoupling, and current limiting resistors for the LEDs. Ideally I would have had decaps closer to the pins of the IO expanders for improved high-frequency decoupling, but compromises have to be made with a two-layer board. I didn’t want any non-LED components on the top side, being the cosmetic face. Plus, the highest frequency on the board is the I2C at 100KHz. Even with the increased inductance from a non-optimal decap placement, my 0.1uF IC decaps and 22uF board-level decaps should be quite effective at 100KHz.
By far the most difficult part of layout was fanning out the 77 signals from the IO expanders. This took countless hours of moving around resistors so that they were placed in a way that enabled routing. My initial goal was to have each current limiting resistor directly below it’s accompanying LED, which would have resulted in a consistent pattern of LEDs and vias on the top side. However, this proved to be impossible in the center of the board, where the IO expander placement and traces to the uC dictated resistor placement. In the end, some of the resistors had to go in odd places just so I could fit them in. It was a struggle getting the layout to match the design rules from my board house, but after some moving around I got it in a functional state.
Because the bottom side of the board was occupied with 77 LED signals both north and south of the IO expanders, the area in between the pins of the IO expanders served as the avenue to get signals into and out of the IO expanders. This is where the (narrow) power plane, I2C and reset signals went.
Most of the top side etch is a ground plane for LED return current. This is certainly not routed ideally. Some traces from resistor to LED had to traverse north-south on the top side due to constraints in resistor placement- this essentially acts as a slot and forces return current from the eastern LEDs to go around it. The added inductance isn’t a problem for a lowly LED, but could be problematic in a higher speed design. In this case, the ground plane impedance is dominated by DC resistance. Ground stitching vias sprinkled where possible should help reduce the impedance of the ground plane. On the west side of the top layer is a power plane, being switched on by S1. The power plane sits right over the uC,conveniently allowing vias to drop down to power the uC where necessary. Three vias are used to drop the power plane to the IO expanders due to the high current running along this path.
On the top side, I also played around with text in different layers. Along the top is “Happy Anniversary Caroline” and the date, both in silkscreen. Along the bottom is “Love, Nathan” in copper, with a soldermask definition. If this were a production board there should never be exposed copper like this due to corrosion issues, but it was fine in this case. I actually like the exposed copper more- the silkscreen is more prone to defects and scratching. Of the ten boards I ordered, 8 of them had defects in the silk layer. Perhaps this is due to the silkscreen being nonstandard black on white soldermask, or perhaps it’s because I went with the cheapest Chinese board house I could find to make the boards.
All Eagle schematic and board files can be downloaded here.
Gerbers can be downloaded here.
For my board house, I chose Seeed Studio’s fusionPCB service. They conveniently provided a design rules file which could be imported to Eagle to check my component spacing, etc. This was super helpful in checking design rules. They also provide an output configuration file which configures Eagle to export gerbers in the format which Seeed Studio expects. The boards cost $32 for ten of them, using 2-layer, 10oz copper with white soldermask. Shipping was an extra $15 for the non-expedited option. Seeed studio has all kinds of options for soldermask, copper weight, layer count, etc. All for a cost of course. I went with the thickest 1.6mm board thickness. I could have used the reduced via inductance of a thinner board, but because there’s no mechanical enclosure I wanted the mechanical strength of a thicker board.
The boards arrived in exactly three weeks. Not bad! As mentioned before, the manufacturing quality is definitely not top-notch. I didn’t notice any issues with the copper or vias themselves, but the silkscreen and soldermask is definitely sketchy at times. The silk doesn’t go well over traces or vias, and scratches off easily. The soldermask apparently has registration issues on some boards. I wasn’t too worried because I was hand-soldering these boards so it’s purely a cosmetic thing, but if these boards went through reflow it may have been an issue.
The boards have 177 components each. I could have gone through an assembly house for pick and place, but that would have been very costly with such a low volume. Plus, one of my primary goals was to hone my SMT soldering skills, especially with the QFN ICs and the 0402 passive parts. This was also a great opportunity to take my new hardware bench setup for a spin.
By far the most difficult part to solder was the microcontroller, a QFN32 package. I initially tried using a soldering iron for this, by placing the package on the tinned pads, then dragging the iron across the edge of the pins with a liberal amount of flux applied. The hope was that the surface tension of the molten solder would pull it into the pads. This proved to be futile though, it’s really difficult to align the package properly and hold it there while soldering. Plus it’s impossible to remove or re-seat the package once soldered. Because of this I burned through a few of my precious boards plus a few of the microcontrollers. Digikey really gouges you on those.
I decided to splurge on a hot air gun for reworking this QFN, plus any other difficult packages in the future. It still took lots of iteration and headache even with the air gun, but I eventually got it to work. One important lesson here- make sure to heat up the part slowly at the beginning, similar to the soldering heat profile shown at the end of datasheets. Heating up the part or board too quickly can cause thermal shock and lead to issues such as broken bond wires and delaminated PCBs.
The second most difficult parts were the TSSOP IO expanders. Having long external pins which could bend made the parts very fragile, plus they were so large that the heat gun couldn’t adequately heat the entire package at once. I soldered these with the iron by tinning the pads, soldering one corner pin of the part, placing a large “bar” of solder over all the pins of one side (shorting them), then using solderwick and flux to remove the excess solder shorting the pads. Afterwards remaining on the board is a small amount of solder connecting the pins to the pads properly. Not sure if this is proper technique for soldering TSSOP packages, but it worked. Here’s a shot of the two IO expanders, one which still has the excess solder used in this technique:
Otherwise, the passive components were pretty straightforward. The smallest component on this board was a 0402. I’ve done 0201s regularly at work, but that’s with expensive microscopes and much nicer soldering equipment than I have at home. I was able to do the 0402s easily with just my magnifying glass. One important lesson here: soldering pins which connect to large copper planes (ground, power, etc) is very difficult because the plane absorbs all the heat, leaving your component too cool to properly reflow the solder. When soldering passives I found it much easier to first solder the side which is a trace, not the plane side if possible. This proved especially difficult when soldering the battery holder, a large piece of metal being soldered to a copper plane. I eventually used a ton of flux, turned my iron way up and got it to work. Thankfully I went with the 50 Watt iron. Another important lesson- remove flux residue as soon as possible. Initially I thought I would just remove it all at the end in one fell swoop, but I found that it becomes much more difficult to remove when left laying around. See above for a picture of that.
Pictures of the back side of the completed board (top side can be seen at the top of this page):
I realize that some of those passives on the back have way too much solder, I improved this in later versions of the board by using a soldering iron with a finer tip.
The software for this project was fairly simple. There are two main features. The “MSG” button, when pushed, displays various scrolling text message. The “<3” button shows a complex animation plus a scrolling message.
The state of the LED display is recorded in an array of 11 bytes, where the lower 7 bits of each byte correspond to a column of LEDs, where LSB is the top LED. A massive 7 x 11 x 3 array called led_map was used as a lookup table to map (row,column) to (io expander, register, bit).
The font array describing the characters was borrowed from this project. It is set up conveniently in that a letter’s index in the array is equal to it’s ascii integer value. This allows me to pass an ascii character into the lookup table as an integer index, which outputs to the LED configuration to display that letter. Here’s an early debug build which shows the various characters:
The code was written and tested in the standard modular fashion. There’s a function for setting a single LED based on XY cordinates, a function for setting a single column of LEDs, showing a singe letter, updating the current display. In almost every case the message is text which scrolls from right to left, although there’s no limitation which forces this to be the case. Using Arduino’s QueueArray library, I implemented this as a FIFO of bytes, where each byte is a column. Here’s another debug build, narcissicism at it’s finest:
In the final design, pushing the MSG button alternates between some 30 different messages. The messages are chosen at “random”, although it’s not actually random. In lieu of a true (or pseudo) random number generator, I used the time (in milliseconds) since the code began. Because there are ~30 messages and the user presses the button much less frequently than once every 30ms, it gives the impression of being random.
The animation corresponding to the “<3” button is a heart which starts as one pixel and expends off the screen, followed by a text message, see below. This was implemented (very slowly and tediously) as a 11 x 13 byte array, where each row in the array is screen state during one of the 13 frames.
It always seems to be the case that projects which seem simple end up having some weird gotchas or other catches. This project was no exception. The big gotcha here was that by default, the Arduino’s compiler places all data variables in RAM for improved performance. With my massive arrays and lookup tables, I quickly burned through the ATMEGA328P’s measly 1K bytes of ram, resulting in strange and unexpected behavior. After extensive searching, I came across the pgmspace library for AVR. The pgmspace library allows large amounts of data to be stored in the flash memory as opposed to the RAM, trading data storage space for code storage space. My code came in below 8KB, under one quarter of the ATMEGA328P’s 32KB, so this was a worthwhile tradeoff. There’s also a small performance hit, pulling variable from the slow nonvolatile storage. PROGMEM is a compiler directive which forces the given variable to be stored in flash memory. the function pgm_read_byte(blah) is a variable lookup that points towards the flash memory, instead of the usual flash. As you’d expect, pgm_read_byte takes in pointers.
I had originally intended to load the arduino bootloader over SPI using atmel studio and the AVRISPmkii, and then program the board using the Arduino environment and a USB-UART connecting to the ATMEGA328P. I was able to successfully load the bootloader, however I was never able to get the arduino IDE to recognize my board as a valid Arduino board. I believe it was an issue with Arduino’s programmers.txt or boards.txt file, but after a while I decided it wasn’t worth it. I ended up compiling the software using Arduino IDE and uploading the .hex file over SPI using Atmel Studio. Less elegant but hey, it works. Plus I get back the code space which is usually taken up by the bootloader.
Full Arduino code can be downloaded here. The actual messages used have been removed, the world is no ready to see those.
In conclusion, this turned out to be a great project. It was a fun gift for Caroline, and I also gained some valuable experience. I was able to make a board from scratch entirely by myself, without the army of support teams which I usually have access to at work. This could come in handy some day. I also gained good experience with hand-soldering SMT components, and bringing my hardware bench setup up to speed. I also got a chance to run the layout tools myself (something which is usually done by CAD engineers), export the gerbers, and work with a board house in China. A great experience, I’m ready to step it up in complexity. Up next: building my own DC bench power supply!