I’ve written a little bit in the past about how to design a basic STM32 breakout board, and how to write simple software that runs on these kinds of microcontrollers. But let’s be honest: there’s still a bit of a gap between creating a small breakout board to blink an LED, and building hardware / software for a ‘real-world’ application. Personally, I would still want a couple of more experienced engineers to double-check any designs that I wanted to be reliable enough for other people to use, but building more complex applications is a great way to help yourself learn.
So in this post, I’m going to walk through the process of designing a small ‘gameboy’-style handheld with a GPS receiver and microSD card slot, for exploring the outdoors instead of video games. Don’t get me wrong, you could still write games to run on this if you wanted to, and that would be fun, but everyone and their dog has made a Cortex-M-based handheld game console by now; there are plenty of better guides for that, and many of those authors put a lot more time into their designs and firmware than I ever did.
The board design isn’t too complicated, but there are several different parts and it gets easier to make small-but-important mistakes as a design gets larger. It mostly uses peripherals that I’ve talked about previously, but there are a couple of new ones too. The display will be driven over SPI, the speaker uses a DAC, the GPS receiver talks over UART, the battery and light levels will be read using an ADC, and the buttons will be listened to using interrupts. But I haven’t written about the USB or SD card (“MMC”) peripherals, and those will need to go in a future post since I haven’t actually worked them out myself yet. Note that SD cards can technically use either SPI or SD/MMC to communicate, but the microcontroller that I picked has a dedicated SD/MMC peripheral, and I wanted to learn about it.
Anyways, if that sounds interesting, read on and let’s get started!
Step #1: Part Selection
Before you get to the fun parts of designing a circuit board, you have to decide which parts you’re going to use. It’s usually a balancing act of cost considerations, ease of assembly, useful features, and ease of use. When you’re choosing a capacitor or a resistor, it doesn’t matter too much which $0.02 part you use, as long as it has the right values. But when you’re designing a board with several interconnected systems, there are a lot of opportunities for cost savings and over-engineering when you pick parts. I’ll try to focus on functionality and ease of use over making the cheapest design possible, but I will still cut a few corners by using cheap ‘commodity’ modules for some of the more intricate parts such as the display and GPS receiver.
In fact, since this board is primarily intended to be a handheld GPS thingamajig, the display and GPS receiver are some of the first parts that we should pick. Once we’ve got some idea of the ‘important’ parts that we need to design around, we can choose an appropriate microcontroller, power supply, etc. I ended up picking a 2.8″ 240×320-pixel
ILI9341 display with 16 bits of color per pixel and an 18-pin 0.8mm-pitch ‘flex ribbon’ connector. They seemed common among online retailers, they use an extensively-documented display driver, they are available with resistive ‘touch screen’ overlays, and you should be able to find listings for less than $5 per display. A smaller display would probably be a bit easier to work with because it would require much less RAM, but I wanted something that could be visible from up to maybe a meter away. These days, a transflective display would probably be a better choice for outdoor use, but this is an affordable and easy-to-use part.
As for the GPS receiver, I decided to use a pre-assembled u-blox Neo-6M module. I used a blue-colored version of the modules marked, “GY-NEO6MV2”:
In future revisions, I hope to integrate the u-blox module onto the board itself, but that would take some time and the first revision might not work. These modules are a great way to quickly prototype ideas with a ‘known-good’ design, and they also let you check that your connections are correct on a breadboard before you order a PCB. Although if you really try to get the lowest price on already-cheap modules, you may end up with a batch of rejects or non-functional boards. It’s not foolproof, but I try to order from listings that are within 25% of the ‘prevailing price’ for popular items. If a listing has reviews with pictures of the items that people received, that also helps; generic stock images are often re-used for common modules which may have subtle differences across manufacturers and revisions, so you might not always get exactly what you order.
Anyways, with those two parts selected, it looks like we’ll need a microcontroller with at least 150KB of RAM to hold a framebuffer for the display. It’s possible to draw to these displays without using a framebuffer, by selecting different drawing areas and drawing UIs one pixel or rectangle at a time. But it’s frustrating to write code like that, and it’s also slow. If you can fit a framebuffer in RAM, then you can ‘map’ that framebuffer to the display by setting up a DMA channel to run in the background and continually send pixel data in a loop. DMA runs independently of the CPU, so it’s a good way to draw to displays.
Unfortunately, 150KB is a lot of RAM for a microcontroller, so we’ll need a fairly large microcontroller. STM32 chips include a ‘Flexible Static Memory Controller’ which can map external RAM chips to the normal memory address space, but those are only fully available on chips with more than 144 pins, and they target parallel RAM chips with 30-50 pins or more. I don’t really want to deal with that for 150KB of space, so I decided to just use one of their more expensive microcontrollers. At $10-$11 in single quantities, the
STM32L496RG is not exactly cheap, but it won’t break the bank and it comes with 320KB of internal RAM and 1MB of Flash memory. It also includes some fancy peripherals, like full USB OTG support and an MMC interface for talking to SD cards. And with 64 pins, it has enough I/O to manage all of the required interfaces without being unwieldy.
With the microcontroller selected, I decided to use a simple
AP7365 600mA linear voltage regulator, and an
MCP73831 lithium battery charging circuit. Throw in a few buttons and a
PAM8302 audio amplifier, and you got yourself a stew! I picked the
AP7365 regulator because it comes in an easy SOT-25 package and supplies enough current to drive the GPS module, display backlight, microcontroller, and extras. The
MCP73831 is a fairly simple battery charger that I’ve used recently, and the
PAM8302 comes in a small 8-pin package with minimal options or dependencies, which is nice when you only need a single quick and easy audio channel. So overall, these parts are not exactly top-of-the-line, but I think they strike a good balance of cost, performance, and ease of assembly for the hobbyist.
With all of the integrated circuits selected, it’s just a matter of picking out the required supporting components; a speaker to go with the audio amplifier, connectors for the USB and SD card interfaces, mute and on-off switches, plus a handful of resistors and capacitors. You can see the parts that I ended up choosing in the bill of materials on this board’s GitHub repository, but it shouldn’t be hard to make substitutions; the 2.8″ display means that there’ll be plenty of room on the board.
Step #2: Schematic Design
Once the parts are picked out, I like to draw a schematic starting with the recommendations in each part’s datasheet. Usually they’ll include a ‘typical application circuit’ and some sort of section with suggestions for choosing supporting parts. But KiCAD (or your preferred EDA suite) probably won’t include a schematic symbol for parts like “this cheap display that I bought off of eBay”. You’ll have to make your own symbols for parts like this, so if a listing on eBay / TaoBao / etc. doesn’t include images with pinouts and dimensions that you can understand, you probably shouldn’t order from it. And if it does, you should save those images and back them up, because the listing might not exist in a month or two.
You might even want to create schematic symbols for all of the integrated circuits that you use; there are too many parts in the world for anyone to maintain an accurate and comprehensive list, and it doesn’t take long to make a new schematic symbol from a device’s datasheet. Once I have symbols for all of the important parts, I like to arrange them all on a single page to get a feel for how things will connect. I guess this won’t be feasible with larger designs, but it works well enough here:
I’m glossing over the specifics here so that things don’t drag on, but I wrote a quick KiCAD tutorial a little while ago which goes over this general process in more detail. Now, before I place all of the capacitors and resistors and things, I like to assign labels to all of the I/O pins to make sure that there are enough pins and peripherals for all of the parts on the board. Check the microcontroller’s datasheet to make sure that you’re connecting the right pins to the right functions. Some chips, like the ESP32, can assign any peripheral function to any I/O pin. Others, like these STM32s, can’t. And STM32s have another ‘gotcha’ to be aware of at this stage: hardware interrupts for inputs such as buttons are shared between all pins with the same number. So you can’t have a ‘button press’ interrupt listening to pins A2 and B2 at the same time, but A1 and B2 would be fine.
Once all of the pins are labeled, it’s just a matter of adding the right supporting parts and wiring everything together. Again, check the datasheets for recommended application circuits and part values. I’m far from an expert, but I guess you probably get a feel for this over time:
Note that I used resistor and capacitor ‘networks’ or ‘arrays’ to make some simple button-debouncing circuits. These parts stuff several resistors or capacitors of the same value into one small package, which saves a lot of space and reduces the part count (in this case) from 16 to 3. I also used a 6-pin ‘navigation switch’ which includes 5 buttons in a ‘directional pad’ pattern: up / down / left / right / press.
Anyways, once everything is connected it’s time to assign footprints to the schematic symbols. You’ll probably need to create custom footprints here, because things like buttons and ribbon connectors come in all kinds of shapes and sizes. Dimensions and tolerances are usually listed towards the end of most datasheets, and they’ll usually include a recommended PCB pattern too. In cases like the cheap display where you don’t have a proper datasheet, make sure that the listing you buy from includes enough information to make a footprint:
And as a general rule, if you plan on hand-soldering something, you should consider extending the rectangular pads a bit, to make it easier to apply even heat to both the board and the part with a soldering iron. If you’re using KiCAD, look at the difference between the normal and ‘HandSolderable’ footprints for standard surface-mount resistor / capacitor footprints as an example. And when you use fine-pitch parts, long pads can also let you ‘wick’ solder away from the part to avoid bridges between the pins. I’ll also sometimes make pads thinner than the recommendation if it allows soldermask to fit between the pins.
Once you’ve created all the custom footprints you need and assigned a footprint to every part, generate the netlist and let’s move on to…
Step #3: Board Layout
This is actually a fairly simple step for this board, because we’ve got a big 2.8″ display and a few buttons on the same side of the board. Those elements will dictate the size of the board, and the size of the display ensures that we’ll have plenty of space for everything else on the other side. Usually it’s sort of hard to estimate how small you can make a board before assembly and/or routing becomes problematic, but here the job is done for us.
Now we have to arrange the major components. I usually put the microcontroller near the center of the board, and if you angle it at 45 degrees, you can easily access a quarter of the chip’s pins from each quadrant of the board. It also looks cool, like wearing a hat backwards or putting stripes on your car.
Side-access parts like the slide switches and USB / SD card connectors should go near the edges of the board, for obvious reasons. In my first revision of this design, I tried to fit the microUSB port under the space where the TFT’s ribbon cable would fold over, but it didn’t quite fit – I think it’s probably best to leave the space underneath a display clear until you can measure how it folds. And I put the audio amplifier near the pin that it connected to, on the opposite side of the board from the power supply.
Once the main parts are in place, try to arrange the smaller ICs and supporting components so that you minimize the length of traces between parts, and avoid having data lines cross over each other whenever possible. Then you just connect everything with traces and vias; I usually start by creating a ground pour covering both sides of the board, but in this case I carved out a few smaller areas for positive voltages to travel between the power supply and the battery / debug connectors. This is my favorite part, it’s like a puzzle game.
Note that it’s usually a good idea to group all of the power supply parts in one location, and to keep them away from ‘noisy’ high-speed data interfaces. In my case, I ended up putting the SD card connector in between the USB plug / power supply and the battery / on-off switch, which is not ideal. But I put large copper pours over the power traces on the other side of the board, which should minimize resistance across that distance. Use thick traces for power connections whenever you can, especially if they are long.
Also, with differential data interfaces like USB, it’s good practice to ensure that the
- data lines have equal lengths. In KiCAD, this is pretty easy to do. Just make sure that you are in the OpenGL rendering mode, then select ‘Differential Pair’ from the ‘Route’ menu along the top of the window. Click on one of the data lines, and it will draw both traces at the same time with equal length. You can still create vias by pressing
v, and the traces will ‘spread out’ to allow extra room for the drilled holes. I usually use a smaller trace width and via size in this mode, and it seems to work well even if the automatic pathfinding can be a little wonky sometimes. The only restriction is that you have to label the paired data lines in a certain way: they need to have the same name, with one having a
P suffix and the other having a
M suffix. I usually use something like
Once everything is connected, it’s just a matter of adding silkscreen markings. I usually label all of the external pin connections, highlight things like ‘pin #1’ locations and the polarity of diodes if they’re not clear anywhere, and add an OSHW mark if I plan on going to the trouble of putting together a bill of materials and setting up a repository for the design files. The ‘3D render’ option can be a good way to spot-check a board once you think you’re done, and sometimes a close examination can help you make a ground plane more cohesive by moving a via away from a trace, or something like that. But usually I’m too lazy to make those sorts of tiny changes in an early revision like this.
Oh, and once you think you’re done, don’t forget to run the DRC and ‘list unconnected’ checks. They’re good sanity checks which ensure that what you’ve drawn matches the schematic.
Step #Always: Learn From Mistakes
Once you finish a board and order it, there’s a good chance you’ll run into some sort of issue with the first run. Recently, I forgot to connect a microcontroller’s power supply pins to the voltage regulator output in a board’s schematic, so of course it didn’t work – whoops!
But sometimes you’ll get problems that are a little bit harder to catch. It happens, and the best thing to do is learn from them and move on. In the first version of this board that I made, I hoped to fold the display over the side of the board and have the USB connector fit under the ribbon connector. But I left too much room after the footprint, so the display ended up folding onto the USB plug instead of lying flat on the board, and the whole thing didn’t fit together very well.
So in this newer revision, I moved the USB connector to the other side of the board from the display, and shortened it a bit to move the display connector closer to the edge. Hopefully this will fit better, but I won’t get the new boards for another couple of weeks. Still, don’t get discouraged if things don’t work perfectly the first time.
Step #5: Write Test Firmwares
Once you finish designing a board and assembling a first revision, it’s a good idea to write some test firmwares that you can flash to quickly test individual parts of the board. If you prepare tests for every part that you use, you can quickly tell whether a newly-assembled board is ‘good’ or not. I’m pretty new to writing these sorts of test projects, but I started on a few basic ones for this board, which you can find in the board’s GitHub repository.
There is a
fw_lib directory which contains some shared functions that are used by multiple projects, and a
firmware directory which contains the actual test firmwares. Right now there are only a few simple examples which test the display, the speaker, the GPS module, etc. But long-term, I think it might be a good idea to combine these projects into one firmware which uses the display and buttons or touch screen to show a menu with the various tests.
Anyways, however you decide to organize it, I definitely recommend writing some simple test firmwares for new boards that you assemble. Besides helping you check that new revisions work correctly, they can also be good starting points for more fully-featured applications. And if you are easily distracted like me, they can help you get back up to speed quickly when you return to a project after working on something else for awhile.
There are definitely more opportunities for mistakes when you design more complicated boards and applications, but I think it helps if you look at embedded development like building a pyramid. If you want to design a board with a display and a GPS module and a battery charger, try designing smaller projects around each part individually first so that you have working models to refer to for each portion of the design when you want to build something on top of those simple ‘building blocks’.
I still haven’t tested the SD card slot with the chip’s MMC peripheral yet, so I didn’t really follow my own advice there, but I doubt the first revision of this board would have worked if I hadn’t already used most of the individual parts in previous projects. Small parts of the design such as the backlight control or the battery charger are very simple in theory, but when you combine enough of them, the chances of you goofing up at least one of them grows pretty quickly. The more practice that you have with using your parts, the easier it will be to catch small mistakes during the design process.
There are definitely things that I’d like to add in future revisions: mounting holes, a microphone, colorful LEDs, maybe some sort of radio module … but for now, it seems like a promising starting point. My next step will be trying to figure out how to collate and display map data using only ~100KB of RAM, which might not be straightforward. But the hardware design went about as well as I could have expected, and it should be possible for anyone who can manage QFP chips with a 0.5mm pitch to assemble it by hand.
This was also a bit more complicated and less detailed than my previous guides, so suggestions and feedback are even more welcome than usual (if that’s possible). You’ll probably get more out of it if you download the project off of GitHub and take a closer look at the schematic and PCB layout, and please feel free to ask questions or open issues on that repository if you run into any issues!