Mark Hofmeister
The best gifts are those that are hand-made. To gain experience in battery management/charging circuitry, SD interfacing, and audio processing/amplifiers, specifically in the context of custom PCBs, I decided to use Christmas as an excuse to make a totally complicated, over-the-top PCB that can read audio data from an SD card, output the data to an audio frontend, and have user-defined volume control. In addition, this assembly is battery-powered and can be charged via USB. This audio panel was gifted in the context of playing soundbites from the movie "Christmas Vacation," which my family adores.
System Requirements & Design
I had quite a bit of autonomy in the design of this system, but there were still tons of constraints within which my work had to function.
Specifically,
-
Tangible, Visual, and Aural User Interface: The user must be able to select a specific soundbite to play, adjust the volume of the audio output, and see an indicator of the volume audio setting. Furthermore, there must be indicators of battery life and battery charging status.
-
Peripherals: The PCB must contain proper interfacing for an SD card, USB High Speed, LiPo battery connection, user-facing power switch connection, volume control potentiometer analog input, digital audio output, and analog audio output.
-
Load-Sharing Battery Charging: The device is battery-powered and must be chargeable through a USB connection. Furthermore, the device should allow load sharing (i.e., the device can be used while charging.)
-
Manufacturable by JLCPCB: Pitt's ECE department (specifically my advisor Dr. Sam Dickerson) was generous enough to pay for the PCBA in exchange for documentation of the process. The ECE department uses JLCPCB for all PCB orders and is experimenting with integrating JLCPCB's assembly service into some class projects. JLCPCB has lightning-fast and cheap assembly services, but a customer must use parts that they have in their library. Therefore, I was restricted to using what was available to me, which required a few workarounds.
-
Resilient to Frequent Use: There are a bunch of interfaces here (SD, USB) that are ESD-prone or allow me to accidentally short lines whilst testing. Therefore, I had to incorporate ESD protection and other protection circuitry.
With these requirements in mind, the top-level system diagram looks as such:
![system-diagram_Mk-I.png](https://static.wixstatic.com/media/3968b4_a93ec95f776d4b5d9b59881b6cbf7c7b~mv2.png/v1/fill/w_913,h_477,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/system-diagram_Mk-I.png)
MCU Selection & Circuit
I decided to use the STM32F103RET6, which has a USB interface, SDIO interface, multiple I2S interfaces, a DAC, and a fair bit of flash memory, and IO. It doesn't have a ton of terribly esoteric hardware configuration options that I don't care about. It's relatively cheap (for an STM32 F series model) and is readily available in JLC's parts library. I chose a LQFP-64 package since these have high reliability rates on pick-and-place machines.
Furthermore, STM32's Cube IDE has support for an SD-card-specific FATFS middleware, simplifying my life tremendously. The MCU schematic looks as such:
![schem-MCU.png](https://static.wixstatic.com/media/3968b4_575f09fdb7f448c8bb99f5cfee9a69b5~mv2.png/v1/fill/w_898,h_657,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/schem-MCU.png)
I included a DIP switch to allow me to put the STM32 in boot mode for programming over USB. I also included a dim yellow LED for debugging. I decoupled each VDD pin on the MCU with a 10nF capacitor and included an additional 2.2uF bulk capacitor for extra charge storage to avoid power line overshoots or sags damaging the MCU or causing brownouts. I further filtered the power supply line through additional capacitors and a ferrite bead for the extra-sensitive analog supply. The rest of the pin assignments include GPIO, UART, ADC input, DAC output and analog amplifier control, I2S output and I2S amplifier control, SWD, USB, shift register pins, and SDIO.
![saturn-load-cap.png](https://static.wixstatic.com/media/3968b4_9d743bc0fd554ddc9d19473390cb2fd6~mv2.png/v1/fill/w_452,h_145,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/saturn-load-cap.png)
I'm using a 12 MHz high-speed external (HSE) crystal to provide a clock source that is fast and accurate enough to drive the USB HS interface. I calculated the load capacitance values by using the Saturn PCB Tool. The STM32F103 datasheet gives package pin parasitic capacitances as 5pF, and I estimated another 2 pF of parasitic capacitance from the PCB layout. The "rule of thumb," then, gives us a requirement of 18pF load capacitances.
LiPO BMS & Power Circuit
I don't mess around with rechargeable battery circuitry, especially LiPo batteries. I only use high-quality batteries, and I'm using 2200mAh Adafruit Lithium batteries with integrated protection circuitry. Initially, I was going to use the MCP73831 single-cell LiPo charger IC, pulled from Adafruit's Micro USB LiPo charger. However, this architecture doesn't allow load sharing, which is a concern. In addition to allowing the device to be used whilst charging, load-sharing prevents potential damage from users accidentally plugging the device in to charge while it is still powered on.
![unusable-BMS.jpg](https://static.wixstatic.com/media/3968b4_89e7849a1f014058b32a71fd44157762~mv2.jpg/v1/fill/w_448,h_336,al_c,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/unusable-BMS.jpg)
![schem-lipo.png](https://static.wixstatic.com/media/3968b4_960e6f9bd8844459ad44a83c4543f279~mv2.png/v1/fill/w_842,h_461,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/schem-lipo.png)
I decided to use another Microchip IC: the MCP73871. This IC is a bit more complicated in configuration (shown in the above shematic) but allows load sharing and tight control of the allowed charging currents. Furthermore, it has pins for 3 status LEDs, making my debugging life easier and giving users more transparency.
I use the VBATT node to power more power-hungry components (like the amplifiers and LEDs,) and then regulate the VBATT node down to a very stable 3.3V VDD rail (shown on the right) for more sensitive components like the MCU, since the LiPo cell voltage can range from 3.7V - 4.2V.
![schem-ldo.png](https://static.wixstatic.com/media/3968b4_1d87c057df9f4bb0b7fc1d7216164bbd~mv2.png/v1/fill/w_453,h_188,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/schem-ldo.png)
interfacing circuits
I used an ejecting SD card connector from JLCPCB. I added a bit of local filtering circuitry on the SD card power rail, along with ESD protection. I'm using a 4-wire-wide SDIO bus. I don't have ESD protection on the data lines, which I wish I'd included.
All lines but the clock line have pull-up resistors, which are necessary for idling, even though this is a push-pull protocol. The clock line has a series resistor to suppress ringing. The JLCPCB component's datasheet states that the card detect line is pulled low upon TF card insertion, so I have a very weak pull-up resistor on the card detect line.
![schem-SD.png](https://static.wixstatic.com/media/3968b4_2113cd228dff4375a1116f18ee23d582~mv2.png/v1/fill/w_557,h_320,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/schem-SD.png)
![schem-SWD.png](https://static.wixstatic.com/media/3968b4_d1b8918ec18b41ff96e98da0a51794bb~mv2.png/v1/fill/w_556,h_329,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/schem-SWD.png)
![schem-USB.png](https://static.wixstatic.com/media/3968b4_40961d683a334870b4432bb096cdc8f7~mv2.png/v1/fill/w_556,h_345,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/schem-USB.png)
I have footprints for both SWD and USB programming. I included ESD protection on all power and data lines, and a decoupling capacitor on the reset line to prevent spurious resets.
I also included series resistors on all SWD lines in the case that I short them whilst probing and debugging. I also connected the SWO net to the incorrect MCU, which is unfortunate.
I'm fairly sure that the USB connector's shield should not be grounded, as this might create a ground loop. However, in the case that this is necessary, I placed an empty 0603 resistor footprint between the shield nodes and ground, which I can fix while debugging.
amplifier circuits
I included 2 amplifiers in this design, each of which can be loaded with a speaker if desired. I did this to allow me to test both amplifiers and choose which to use based on the sound quality.
I chose a STM32 microcontroller with a dedicated I2S output, as I was influenced to use the MAX98357A, which is used on Adafruit's 3W mono I2S amplifier board, which I used in my Bait-Cast-Reel project. I was pleasantly surprised to find that Analog Devices makes an amplifier IC that is less than $10/pop.
I'm operating in mono mode, which is configured by cascading a specific pull-up resistor value with an internal 100k pull-down resistor. This keeps the amp. in mono mode across all battery cell voltages. The gain is controlled by connecting the gain slot to either power or ground directly or through a 100k resistor. The gain can be set to 3, 6, 9, 12, or 15dB. A floating pin defaults to 9dB, which is the PCB's default, but this can be changed by bridging the 0603 resistor footprint and twiddling transistor gates from the MCU. I actually ended up using this hardware gain switching to control the volume of the final product, since changing I2S volume in software proved to be too costly processor-wise.
The output pins have ferrite beads and very small decoupling capacitors to eliminate harmonics from any nonlinearity in the amplifier.
![](https://static.wixstatic.com/media/3968b4_8f42d588993a4693903795475b92d03c~mv2.png/v1/fill/w_951,h_422,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_8f42d588993a4693903795475b92d03c~mv2.png)
The second amplifier takes a raw analog signal and is hooked up to the MCU's DAC. I have series capacitors to eliminate DC from the audio signal and very filtered power on the VBATT rail. There are ferrite beads and decoupling capacitors on the amplifier output lines to filter harmonics, just as on the I2S amplifier.
![](https://static.wixstatic.com/media/3968b4_a312e609234b42d2ab51ab09a912cf68~mv2.png/v1/fill/w_969,h_353,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_a312e609234b42d2ab51ab09a912cf68~mv2.png)
UI circiutry
![](https://static.wixstatic.com/media/3968b4_82113a756af14142ad7299c6a906843e~mv2.png/v1/fill/w_534,h_557,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_82113a756af14142ad7299c6a906843e~mv2.png)
The 10 buttons should be as expected, but I'm using the FSM8JSMASTR from TE Connectivity. I wanted to use this product because they have green user contacts and are the proper height. However, it's not exactly clear which pins connect when the switch is closed. The datasheet seems to insinuate that pins 1 and 3 (on the schematic) connect upon closing, but I'm used to pins 1 & 2 connecting (i.e., the two pairs that are close to each other) connecting upon closing. Therefore, I assumed that pins 1 and 3 close, but included footprints to allow the opposite.
I'm glad I did. Upon receiving the buttons, a continuity test showed that pins 1 and 2 were a complimentary pair. It's a pain to swap 10 resistors on 5 boards, but much better than not including this failsafe and having a disfunctional board.
To save pins, I'm using a shift register to control 9 identical LEDs that are part of the user interface to display the volume level. Each shift register output is connected to a FET-controlled LED, each with a series gate and pull-down resistor to eliminate spurious behavior.
![](https://static.wixstatic.com/media/3968b4_1ba5db26f321433995430da3648ba7db~mv2.png/v1/fill/w_599,h_361,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_1ba5db26f321433995430da3648ba7db~mv2.png)
x 7
PCB layout
This certainly isn't the prettiest layout that I've done. I had to create a board shape to give user-centric button distribution, yet keep peripherals close enough to the MCU so as to not introduce signal degradation over long transmission lines. I'm using a 4-layer, Sig-PWR-GND-Sig stackup.
![](https://static.wixstatic.com/media/3968b4_cc659bea591e4a14aa6fb15ed6bbcbcf~mv2.png/v1/fill/w_890,h_469,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_cc659bea591e4a14aa6fb15ed6bbcbcf~mv2.png)
Front
![](https://static.wixstatic.com/media/3968b4_1ad588328692423ab95e261867a65b3d~mv2.png/v1/fill/w_888,h_460,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_1ad588328692423ab95e261867a65b3d~mv2.png)
Back
This project has 35 test points. I also have DIP switches for enabling boot mode and for changing the LiPo battery charging rate. These thing make my life much easier, especially when cramming for Christmas.
![](https://static.wixstatic.com/media/3968b4_ea9466aa997c46cdb4af6b6426162e49~mv2.png/v1/fill/w_651,h_415,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_ea9466aa997c46cdb4af6b6426162e49~mv2.png)
I routed the USB data lines with Altium's controlled impedance tool and prioritized short, direct routing over a solid ground plane. I also added teardrops to everything for smooth copper transitions, which look very nice.
The 3W amplifiers will be pulling some serious current during peak load, so I made sure to use wide traces/polygons to increase power rail integrity. This is a board where color-coding comes in handy; the nature of a particular trace or polygon is communicated and only requires a quick glance.
![](https://static.wixstatic.com/media/3968b4_81b3e33f9a5141029054924b8562b39b~mv2.png/v1/fill/w_599,h_368,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_81b3e33f9a5141029054924b8562b39b~mv2.png)
![](https://static.wixstatic.com/media/3968b4_0b17756bee374265a476b105d02a1657~mv2.png/v1/fill/w_467,h_356,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_0b17756bee374265a476b105d02a1657~mv2.png)
Though JLCPCB has hooks to allow verification of component placement and rotation and has its tightly controlled parts library in-house, I want to communicate my design intent fully. I'm always sure to include notes of indicators, especially for LEDs. It's also good to include a stackup diagram to indicate exactly what dielectrics and prepregs are needed, as a miscommunication here could lead to my USB communications failing.
![](https://static.wixstatic.com/media/3968b4_10014ecc37c3450e94a602721cf8a84c~mv2.png/v1/fill/w_823,h_478,al_c,q_90,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_10014ecc37c3450e94a602721cf8a84c~mv2.png)
enclosure fabrication
To create a duplicatable gift with a rustic Christmas-esque feel, I opted to laser-cut and piece together an enclosure from 1/8" plywood. I generated a DXF file to create a console box for keyboards from boxes.py and modified it to my specifications. I added fixturing holes for a power button, speaker mount, USB micro extension cable, and user interface components, including battery status LEDs. The PCB is attached to the front face via standoffs, which gives components on the board clearance and controls the extension of the buttons through the holes in the panel.
This was my first time laser-cutting and assembling faces into a snazzy box. I accounted for machine tolerances and laser burn-off, but I overestimated the quality of Pitt's CNC laser, which seems to have a hard time remembering where it is, resulting in misaligned cuts. Luckily, I was able to amputate some pieces and use wood glue to result in a solid final product. The exploded animation below shows how the assembly is constructed.
Firmware
I decided to interface to the SD card through SDIO and use STM32's FAT file system middleware package to handle files on the card. It's an abstraction layer above the HAL SDIO drivers that allows the use of simple functions to read and modify files and directories on a media card like an SD.
![fatfs.png](https://static.wixstatic.com/media/3968b4_0a8f0ac1c372446785539865543c2630~mv2.png/v1/fill/w_301,h_347,al_c,lg_1,q_85,enc_avif,quality_auto/fatfs.png)
![fatfs-2.jpg](https://static.wixstatic.com/media/3968b4_79801f3cf6b343aaaf934e6ce0cb1adf~mv2.jpg/v1/fill/w_336,h_349,al_c,lg_1,q_80,enc_avif,quality_auto/fatfs-2.jpg)
![](https://static.wixstatic.com/media/3968b4_56f97db2d9664340a6d57b8c338e6124~mv2.png/v1/fill/w_705,h_268,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_56f97db2d9664340a6d57b8c338e6124~mv2.png)
I formatted my WAV files with a consistent structure. I used a sampling rate of 44.1kHz, 16-bit resolution, and stereo audio. I downloaded audio clips from YoutUbe and used Audacity to trim, amplify, and re-mix soundbites to my liking.
To unpack and play a WAV file, I used I2S interrupts to play the WAV files chunk-by-chunk, as the files are too large to completely load into flash memory all at once. The playWavFile function opens a file with the passed file name string and parses through the file header to ensure proper audio formatting. Specifically, each file must have the same header format, a compression scalar of 1, 2 channels, 16 bits per sample per channel, and a sample rate of 44.1 kHz.
![](https://static.wixstatic.com/media/3968b4_1369479b81c04f65a1e42d34a5f7dea0~mv2.png/v1/fill/w_387,h_237,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_1369479b81c04f65a1e42d34a5f7dea0~mv2.png)
![](https://static.wixstatic.com/media/3968b4_ac05b21dd9674fb1ac7a3c669bdac6d4~mv2.png/v1/fill/w_562,h_470,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_ac05b21dd9674fb1ac7a3c669bdac6d4~mv2.png)
The HAL I2S interrupt TX function is called, which triggers a global interrupt when a transmission sequence is complete. During transmission, the playWavFile function enters an idling loop until the TX completion interrupt is triggered, at which point sequential data chunks are loaded and transmitted. This continues until there is no data left.
Since the STM32 HAL seemed to have issues running the playWavFile function (which requires I2S interrupts and delay functions) from a GPIO external interrupt callback, so I implemented a play index flag system to determine which file to play. Since I know the names of my soundbite files, I declare these in a global array of character arrays at the start of program execution, the first of which is a very short, silent WAV file. Rather than calling the playWavFile function from the EXTI callback (when a button is pressed,) I simply play this short and silent file until an external interrupt changes the play index flag to a non-zero value, at which point the desired soundbite is played.
The main loop is therefore infinitely calling the playWavFile function, but only outputting real aural data when triggered to do so from GPIO interrupts.
![](https://static.wixstatic.com/media/3968b4_b4f9bcb3a5d9456dbb6b4d813e6feea0~mv2.png/v1/fill/w_396,h_450,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_b4f9bcb3a5d9456dbb6b4d813e6feea0~mv2.png)
![](https://static.wixstatic.com/media/3968b4_1363a113f3414bdcaa4be6feca1b3464~mv2.png/v1/fill/w_184,h_199,al_c,q_85,enc_avif,quality_auto/3968b4_1363a113f3414bdcaa4be6feca1b3464~mv2.png)
On top of all of the SDIO/I2S chatter, I also need to check the volume slider and adjust both the actual volume and the volume bar UI level quickly enough so that it looks continuous. I did so by reading the ADC input at regular intervals determined by a timer, which triggers a global interrupt to shift the CPU's focus toward this task.
![](https://static.wixstatic.com/media/3968b4_614fe2018acf495f9d95ee6023f706b6~mv2.png/v1/fill/w_673,h_351,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_614fe2018acf495f9d95ee6023f706b6~mv2.png)
I noticed that more frequent ADC checking didn't necessarily result in a more "continuous" looking volume bar UI and distorted the audio more. This is because the CPU was spending less time continuously sending data and more time checking the ADC. I settled on a relatively long period of 200ms, which resulted in a continuous-looking volume bar and trivially distorted audio.
Quite a few things are going on at once, as illustrated by the below master diagram.
![](https://static.wixstatic.com/media/3968b4_0e844e181d5c4ac8902d7bae27cb161a~mv2.png/v1/fill/w_942,h_245,al_c,q_85,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_0e844e181d5c4ac8902d7bae27cb161a~mv2.png)
TESTING
![](https://static.wixstatic.com/media/3968b4_35a3c59946794436a26af581bc9909c1~mv2.jpg/v1/fill/w_450,h_600,al_c,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_35a3c59946794436a26af581bc9909c1~mv2.jpg)
It was during testing that I discovered that the STM32 does not like to call I2S TX interrupt functions from GPIO EXTI callback functions, which was proved by I2S waveforms on my oscilloscope. I was smart to liberally sprinkle test points all over my board.
Miraculously, almost all of the hardware was perfect on the first try, which never happens. I did stupidly flip the SD card DATA0 and DATA1 pins on the schematic, so I was doing janky trace surgery in my basement.
![](https://static.wixstatic.com/media/3968b4_0fbc4533078d49b69c7cddf24c1eec50~mv2.jpg/v1/fill/w_484,h_363,al_c,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_0fbc4533078d49b69c7cddf24c1eec50~mv2.jpg)
Before ordering the board, I had assumed that each GPIO pin's external interrupt was unique to just that pin. That's a lie - they're not. I discovered that some button input pins had some conflicting interrupt pin selections, which forced me to do some loop checking (yuck) on the input pins in the main loop.
I decided to use a relatively long 200ms period between ADC checks. Anything shorter than this results in noticably distorted audio and doesn't result in a smoother user experience, as the CPU is devoting more cycles to checking the ADC and less cycles playing audio.
I was crunched for time with Christmas coming up, but in the future, I'd like to use DMA to facilitate the SDIO --> I2S data transactions. This will allow me to recycle this work and do more with the MCU in the future.
Ackowledgements
I drew heavily on external references, and would not have had success in such a short time without these folks:
-
Philip Salmony from Phil's Lab, specifically:
-
STM32 I2S ADC DMA & Double Buffering - Digital Audio Processing with STM32 #4 - Phil's Lab #55
-
Altium STM32 Hardware Design - An Overview in Under 20 Minutes - Phil's Lab #38
-
STM32 + SWD + ST-Link + CubeIDE | Debugging on Custom Hardware Tutorial - Phil's Lab #4
-
STM32 Programming Tutorial for Custom Hardware | SWD, PWM, USB, SPI - Phil's Lab #13
-
-
NuTubeチャンネル
-
STM32 DAC Sine Wave Generation – STM32 DAC DMA Timer Example
-
Pitt ECE Department (Funding,) namely Dr. Samuel Dickerson.
![](https://static.wixstatic.com/media/3968b4_0a2ced75468a444b8ee92d9fd7973180~mv2.jpg/v1/fill/w_117,h_117,al_c,q_80,usm_0.66_1.00_0.01,enc_avif,quality_auto/3968b4_0a2ced75468a444b8ee92d9fd7973180~mv2.jpg)