
Onboard pcb
rober708/BIRD-E
The Bounded Infrared Recording Drone Ecosystem (BIRD-E) is a tethered quadcopter with an attached infrared camera, with livestream functionality and XBOX ground control. Senior design capstone at Purdue ECE.

STM32-based alarm clock and RGB lamp: custom alarms wake you with light and your own songs, configured over USB from the browser or a Python tool
Hoppy clock is an open-source printed circuit board by borkdlabs, from the borkdlabs/hoppy_clock repository on GitHub, published under the MIT licence. It is a 4-layer board of 35 × 48 mm with 61 parts on 57 nets, routed with 342 tracks and 113 vias.
It is built around the STM32L432KCU6, PAM8302AASCR and W25Q128JVSIQ, with the TPS2116DRL, USBLC6-2P6 and ECS-.327-6-34R-C-TR among its other parts. Its parts include 20 capacitors, 15 resistors, 6 ICs, 6 connectors and 2 switches. It belongs with the lEDs and displays boards and the STM32 boards of this library.
STM32-based alarm clock and RGB lamp: custom alarms wake you with light and your own songs, configured over USB from the browser or a Python tool.
💚 Sponsored by PCBWay, who provided the bare PCBs and solder paste stencil for the v0.1.0-alpha boards - see Acknowledgements.
| Top | Bottom |
|---|---|
![]() | ![]() |
Features:
solid, rainbow, sweep, breathe,
wipe) rendered across the onboard LED and any chained via the
WS2812B breakout connector, every one of them with its own fade and optional
flicker.| Manufacturer Part Number | Manufacturer | Description | Quantity | Notes |
|---|---|---|---|---|
| STM32L432KC | STMicroelectronics | 32-bit MCU | 1 | |
| WS2812B | (Various) | PWM Addressable RGB LED | 1 | |
| PAM8302AAS | Diodes Incorporated | Audio Amplifier | 1 | |
| W25Q128JVSIQ | Winbond Electronics | 128 Mbit NOR Memory | 1 | |
| Generic Push Button | 1 |

Drawio file here: hoppy_clock.drawio.

| STM32L432KC | Peripheral | Config | Connection | Notes |
|---|---|---|---|---|
| PA14 | SYS_JTCK-SWCLK | TC2050 SWD Pin 4: SWCLK | ||
| PA13 | SYS_JTMS-SWDIO | TC2050 SWD Pin 2: SWDIO | ||
| PB3 | SYS_JTDO-SWO | TC2050 SWD Pin 2: SWO | ||
TIM2_CH1 | PWM no output | Scheduling | Scheduler timer. | |
TIM6 | TRGO update event | DAC1_OUT1 TRGO. | ||
ADC1 VREFINT | Scan conversion mode | VDDA Sense | Configured in ADC1 rank 1. | |
ADC1_IN17 | Scan conversion mode | Temperature Sensor Channel | Configured in ADC1 rank 2. | |
| PA10 | Reserved | 115200 bps | GPIO Breakout: (ie Qwiic: I2C1_SDA) | Reserved GPIO breakout (PA10). |
| PA9 | Reserved | 115200 bps | GPIO Breakout: (ie Qwiic: I2C1_SCL) | Reserved GPIO breakout (PA9). |
| PA11 | USB_DM | Device (FS) | USB-C D- | |
| PA12 | USB_DP | Device (FS) | USB-C D+ | |
| PA8 | TIM1_CH1 | PWM Generation CH1 | WS2812B-2020 Pin: DIN | DIN pin number depends on IC variant. |
| PA4 | DAC1_OUT1 | PAM8302AAS Input Circuit | ||
| PA1 | GPIO_Output | Hardware pull-down | PAM8302AAS Pin 1: SD | |
| PA3 | QUADSPI_CLK | W25Q128JVSIQ Pin 6: CLK | ||
| PA2 | QUADSPI_BK1_NCS | Hardware pull-up | W25Q128JVSIQ Pin 1: CS | |
| PB1 | QUADSPI_BK1_IO0 | W25Q128JVSIQ Pin 5: IO0 | ||
| PB0 | QUADSPI_BK1_IO1 | W25Q128JVSIQ Pin 2: IO1 | ||
| PA7 | QUADSPI_BK1_IO2 | Hardware pull-up | W25Q128JVSIQ Pin 3: IO2 | Hardware pull-up for potential bringup from SPI single-line. |
| PA6 | QUADSPI_BK1_IO3 | Hardware pull-up | W25Q128JVSIQ Pin 7: IO3 | Hardware pull-up for potential bringup from SPI single-line. |
| PB4 | GPIO_EXTI4 | Hardware pull-up | Generic Push Button Active Low pin |
4 MHz Multi-Speed Internal (MSI), LSE-trimmed
-> Phase-Locked Loop Main (PLL)
-> 80 MHz SYSCLK
-> 80 MHz HCLK
-> 80 MHz APB1 (Maxed) -> 80 MHz APB1 Timer
-> 80 MHz APB2 (Maxed) -> 80 MHz APB2 Timer
-> PLLSAI1 -> 48 MHz USB & ADC clock
32.768 kHz Low Speed External (LSE)
-> 32.768 kHz RTC
-> Disciplines the MSI (MSI PLL mode)Connectors fixed by hardware (PCB traces or the connector itself).
| Connector | Ref | Description |
|---|---|---|
Tag-Connect TC2050 | J1 | SWD programming/debug connector |
USB-C | J2 | USB-C 5 V power & data source |
Backup supply | J3 | 1x2 JST XH (2.5 mm pitch), Pin 1: Backup 5 V, Pin 2: ground |
Qwiic | J4 | 1x4 JST SH, Pin 1: ground, Pin 2: 3.3 V, Pin 3: SDA, Pin 4: SCL |
WS2812B breakout | J5 | 1x3 JST PH, Pin 1: 5 V, Pin 2: DOUT, Pin 3: ground |
Speaker | J6 | 1x2 JST PH, Pin 1: OUT+, Pin 2: OUT- |
User controllable hardware and/or firmware driven inputs.
| Switch/Jumper | Ref | Description |
|---|---|---|
BOOT0 button | SW1 | Push to pull BOOT0 high |
User button | SW2 | Generic 6 mm SMD button with through-hole breakout |
LEDs used to show board status and/or user controllable.
| LED | Mark | Description |
|---|---|---|
WS2812B LED | None | RGB addressable LED |
| Test Point | Ref | Description |
|---|---|---|
TPS2116 ST | TP1 | ST pin from onboard TPS2116 |
By default, the board is powered from the USB-C 5 V source. An onboard TPS2116
priority power mux allows a backup 5 V supply to be connected via the
Backup supply connector (for example, a regulated battery pack output). If the
USB-C supply drops below the mux threshold, the TPS2116 automatically switches
the board over to the backup supply and switches back when USB-C power returns.
The mux status pin (ST) is exposed on the TPS2116 ST test pad and is pulled
low whenever the backup supply is in use, allowing a probe to detect the active
source during development/testing.
TPS2116 4.1 V switchover threshold logic:
PR1 = VIN1 * R_bot / (R_top + R_bot)
= VIN1 * 220k / (680k + 220k)
= VIN1 * 0.2444
VIN1(th) = 1V * (R_top + R_bot) / R_bot
= 1V * (680k + 220k) / 220k
= 4.09 V
Tolerance: +-0.08V
min: (1V - 0.08V) * 4.0909 = 3.76 V
max: (1V + 0.08V) * 4.0909 = 4.42 VExternal LEDs on the WS2812B breakout connector are powered from USB (VBUS)
directly, not the priority power mux in order to prevent excessive battery drain
during a power outage. The single onboard LED is on the priority power mux
supply, so it remains available for minimum operation on battery power.
An 8 ohm, >= 1 W speaker can be connected via the Speaker connector. The
amplifier output is bridge-tied (BTL): both terminals are driven, so neither may
be connected to ground.
The firmware is fixed, all user settings (time, alarms, light looks, the lamp,
sounds and the LED count) live in the W25Q NOR flash and are written over USB at
runtime. Settings survive resets (the clock's time is kept in the STM32 backup
domain), as long as the board stays powered from USB-C or the backup supply. A
full power loss resets the clock (see the clock-unset cue above). A flash wipe
returns the unit to a clean state.
Stored settings carry a format version. A firmware update that changes that format resets them: rather than misread an older image, the board falls back to empty defaults, comes up on its built-in lamp look and needs reconfiguring.
| Action | While idle | While an alarm is ringing |
|---|---|---|
| Short press | Toggle the lamp on/off | (ignored) |
| Long press | Play / stop the button song | Silence the alarm |
The board enumerates as a USB CDC virtual serial port and speaks a small framed
command protocol (firmware/Core/Inc/usb_cmd.h). Two hosts implement it:
| Host | Runs on |
|---|---|
software/main.py | Any OS with Python 3 |
| Web app | Chrome or Edge on desktop, no install |
Both open the same serial port and only one program may hold it at a time.
Connecting: When the clock is idle and off USB it deep-sleeps (STOP2) and deliberately presents as detached, so plugging into a host shows no device at first. To connect:
If the clock is already awake (in use, ringing, an alarm just fired, or the lamp is showing a continuously animating look), it enumerates the moment you plug in, with no press needed. Unplugging or the host going to sleep allows the system to return to deep sleep.
Why a button press? The clock cannot tell a data host (a PC) from a plain USB-C charger or power bank, both simply present 5 V with no reliable way to distinguish them until an enumeration that only a real host answers. Waking and enumerating on every plug-in would spend energy for the majority of the time the port is used only to charge or power the unit and risks staying awake on a battery pack it mistook for a host. Gating USB behind a deliberate button press ties enumeration to a real intent to configure and lets the clock stay in its lowest-power state whenever it is merely being powered. Firmware itself is flashed over SWD (the
TC2050header), independent of this path.
https://borkdlabs.github.io/hoppy_clock/
A single static page that drives the port through the Web Serial API, so there is nothing to install beyond the OS's own CDC driver. It needs Chrome or Edge on desktop (Windows, macOS or Linux); Firefox, Safari and mobile browsers do not implement Web Serial and the page says so rather than half-working.
Press Connect and pick the STM32 virtual COM port (0483:5740). Permission is
granted per site and remembered, so later visits reopen that port on their own.
cd software
python main.py <command> [options] # add -p COM7 (or /dev/ttyACM0) to pick the portNeeds Python 3 with
pyserial(pip install -r software/requirements.txt), MP3 uploads additionally needffmpegon thePATH. See top docstring inmain.pyfor more information.
| Command | Description |
|---|---|
set-time | Sync the RTC to the host's local time |
add-alarm / set-alarm | Add an alarm (e.g. --at 08:00 --days weekdays) / replace all with one |
remove-alarm N / clear-alarms | Delete one alarm by index / delete all |
list-alarms | Show alarms, lights, the lamp and LED count |
set-light | Define a light look (--effect, --fade, --curve, --flicker) |
set-lamp | Choose the on/off lamp idle looks |
set-led-count N | Set the number of chained LEDs |
upload-sound | Store a sound from a WAV/MP3 file or a synthesized tone |
play-sound / stop-sound | Play / stop a stored sound now |
set-button-song | Set which sound the long-press plays |
wipe | Factory-reset the flash (--full also scrubs the audio) |
Run python main.py --help (or <command> --help) for the full option list.
A light look is not a stored animation but a handful of parameters the firmware renders live across the whole chain. Up to 16 are stored; alarms and the two lamp idle states each reference one by id. Every look sets a base colour and a master brightness, then picks an effect:
| Effect | Renders | Period | Spread |
|---|---|---|---|
solid | The whole strip held at one colour | - | - |
rainbow | The HSV hue wheel, cycling | Time per turn | Hue step per LED (0 = strip as one colour) |
sweep | A lit band travelling over darkness | Time per pass | Band width, in LEDs |
breathe | The colour swelling and receding | Time per breath | - |
wipe | The colour painted along from LED 0 | Time to cross | Width of the soft front, in LEDs |
wipe-reverse | The same, from the far end of the chain | Time to cross | Width of the soft front, in LEDs |
solid settles and holds as an idle. rainbow, sweep and breathe loop
until something else is played.
The two wipes run once and then hold, like a solid that arrives along the
strip instead of everywhere at once. Each LED blends from whatever it was
showing when the wipe started, so over a dark strip it fills the strip up LED by
LED, and over another look it paints the new colour across the old one. With
three LEDs and a one-LED front, their levels step through 0 0 0,
128 0 0, 255 0 0, 255 128 0 and on to 255 255 255. A wider spread lets
several LEDs ramp at once for a smoother edge. Because the wipe is already a
transition, it is usually paired with a fade of 0; a lamp-off look of
wipe-reverse at brightness 0 then rolls the light back down the strip.
The same three settings apply to all six alike:
| Setting | Does |
|---|---|
fade | Time this look takes to fade in over whatever is already lit |
curve | Shape of that fade: linear (constant rate) or ease (gentle at both ends) |
flicker | Amplitude of a random brightness dip, redrawn several times a second |
A fade runs on entry only. Every transition uses the fade of the look
arriving, never of the one leaving: playing a look is the only thing that starts
a fade, and by then the previous look is already on its way out. So the fade
on a rainbow decides how that rainbow appears and has no say in what happens
when something later replaces it.
To fade a look both in and out, set the fade on that look and on whatever replaces it. A lamp that eases up and back down over two and a half seconds:
| Look | Effect | Brightness | Fade | Curve |
|---|---|---|---|---|
| Lamp on | rainbow | 200 | 2500 | ease |
| Lamp off | solid | 0 | 2500 | ease |
Give the lamp-off look a fade of 0 instead, and it cuts to black the instant the button is pressed, no matter how long the rainbow's own fade is. A fade of 0 always means "no fade, show this look now".
The look being faded away from is held as a still frame, so a rainbow stops
cycling the moment it is replaced and dims from whichever hues it had reached.
Flicker is a texture rather than a shape, so it is set separately from the
curve, and the two combine freely. Unlike the fade it never ends, which is what
makes a solid warm white read as a candle instead of a lamp.
A continuously animating look keeps the board out of deep sleep. Only a
solid look or a wipe, with flicker 0, ever finishes: once its fade lands
(and a wipe has crossed the strip), it settles on a fixed colour, stops
rendering and lets the MCU drop into STOP2. rainbow, sweep and breathe
loop for as long as they are showing, and any flicker above 0 never stops, so
a look of either kind holds the board in light sleep instead. Two consequences
follow:
solid one.The clock-unset warning blink holds the board awake in the same way, until the time is set.
Two slots hold one sound each, 7.5 MiB apiece, in the same NOR flash as the settings. Alarms reference a slot by id, and the long press plays one.
The source file's own format does not matter. Uploading anything that is not
already raw PCM runs it through ffmpeg, which decodes and resamples it to mono
at the rate and sample format you pick:
ffmpeg -i song.mp3 -ac 1 -ar <rate> -f s16le|u8 -A 320 kbps 44.1 kHz stereo MP3 and a 96 kbps mono one land on the board as the same shape of data, so there is nothing to read off a file to decide with. What gets stored is the raw blob plus the rate and format it was made at, the firmware reads those back at playtime and clocks the DAC accordingly.
So the two settings are a trade between quality and how much fits in a slot:
| Rate | s16 (2 B/sample) | u8 (1 B/sample) |
|---|---|---|
| 8000 | 8m 11s | 16m 23s |
| 16000 (default) | 4m 05s | 8m 11s |
| 22050 | 2m 58s | 5m 56s |
| 32000 | 2m 02s | 4m 05s |
| 48000 (max) | 1m 21s | 2m 43s |
Two things to weigh:
u8 halves the size but adds hiss. Eight-bit quantization is audible on
anything sustained. It suits short effects and tones, not music.In practice leave both on their defaults (s16 at 16000 Hz) and use the trim
option to fit a long track, rather than dropping quality to make it fit. Reach
for u8 or a lower rate only when a long clip matters more than how it sounds.
This flashes new firmware (not settings, those use the USB tool above).
Normally firmware is programmed over SWD (the TC2050 header). Without a
debugger, the STM32L432's built-in USB bootloader (DFU) flashes it over the same
USB-C port.
BOOT0 is sampled only at power-up, so DFU has to be entered on a fresh cold
boot with the button held:
Backup supply
connector. A backup supply keeps the MCU running, so plugging in USB would
not be a cold boot and BOOT0 would never be re-sampled. (With no backup
supply attached, USB is the only source and this is automatic.)BOOT0 button.0483:DF11). Release the BOOT0 button.0x08000000, then restart into it with
your DFU tool/software.webapp/ is plain ES modules with no build step, no bundler and no
dependencies, it is served exactly as it sits in the repository. Web Serial only
runs in a secure context and http://localhost counts as one, so a static
server is enough and no HTTPS setup is needed:
cd webapp
python -m http.server 8000Then open http://localhost:8000 in Chrome or Edge.
| Path | What it is |
|---|---|
index.html, css/style.css | Page and styling |
js/alarms.js | Packed alarm records, mirrors manifest.h |
js/app.js | Tab switching and the wiring behind every card |
js/device.js | Port lifecycle, transactions, manifest read/write |
js/lights.js | Packed light looks, mirrors manifest.h |
js/sounds.js | Sound slots and the decode/encode upload path |
js/protocol.js | Framing and CRC-8, mirrors firmware/Core/Inc/usb_cmd.h |
dev/ | The helpers below, stripped from the published site |
Working without a board. The offline checks exercise the framing and transaction layers against a fake CDC port, covering the happy path, error statuses, command timeouts and a mid-command unplug:
cd webapp/dev
node test-device.mjsFor the UI itself, paste
dev/inject-fake-port.js into the browser
console with the page open. It stubs navigator.serial.requestPort with a clock
whose RTC runs 47 s fast, so the connect flow, the drift readout and the sync
button can all be driven dry. More in webapp/dev/.
A protocol change touches three implementations, keep them in step:
firmware/Core/Inc/usb_cmd.h, software/main.py and webapp/js/
(protocol.js for the framing, alarms.js, lights.js and sounds.js for the
record layouts).
.github/workflows/pages.yaml publishes the app
to GitHub Pages on every push to main touching webapp/. It runs
node --check over each module and the offline checks above, copies webapp/
to the site root minus dev/, then deploys. Pull requests build and test but do
not publish and the workflow can also be started by hand (Actions -> Pages ->
Run workflow).
The repository's Pages source must be set to GitHub Actions (Settings -> Pages -> Build and deployment -> Source).
This project is sponsored by PCBWay, whose PCB manufacturing services are essential in producing high-quality prototypes for its development. Their support ensures reliable boards that meet the project's demands.
Why PCBWay?
PCBWay stands out for their exceptional services and commitment to the community:
Their dedication to professional-grade services and fostering innovation makes PCBWay an invaluable partner in bringing this project to life.
This project uses the following open-source software components:
3-Clause BSD License.
LICENSE.txt.STMicroelectronics are trademarks of their respective owners. Use of these names does not imply any endorsement by the trademark holders.
The PCBWay name and logo are trademarks of PCBWay, reproduced with their permission to acknowledge their sponsorship of this project.
| Qty | Value | Footprint | References |
|---|---|---|---|
| 10 | 100n | Capacitor_SMD:C_0603_1608Metric | C3, C4, C5, C6, C7, C8, C10, C12, C15, C16 |
| 7 | 10k | Resistor_SMD:R_0603_1608Metric | R5, R6, R7, R8, R10, R14, R15 |
| 6 | 1mm | Fiducial:Fiducial_1mm_Mask2mm | FM1, FM2, FM3, FM4, FM5, FM6 |
| 5 | 1u | Capacitor_SMD:C_0603_1608Metric | C1, C2, C11, C19, C20 |
| 3 | M3 | MountingHole:MountingHole_3.2mm_M3 | H1, H2, H3 |
| 2 | 100 | Resistor_SMD:R_0603_1608Metric | R12, R13 |
| 2 | 5k1 | Resistor_SMD:R_0603_1608Metric | R2, R3 |
| 2 | 6p8 | Capacitor_SMD:C_0402_1005Metric | C13, C14 |
| 1 | 10u | Capacitor_SMD:C_0603_1608Metric | C17 |
| 1 | 1k | Resistor_SMD:R_0603_1608Metric | R9 |
| 1 | 1k8 | Resistor_SMD:R_0603_1608Metric | R11 |
| 1 | 2171790001 | common:molex_2171790001_usb_c_16p | J2 |
| 1 | 220k | Resistor_SMD:R_0603_1608Metric | R4 |
| 1 | 32k768 | common:ecs_ecx-34r | Y1 |
| 1 | 4n7 | Capacitor_SMD:C_0603_1608Metric | C18 |
| 1 | 4u7 | Capacitor_SMD:C_0603_1608Metric | C9 |
| 1 | 680k | Resistor_SMD:R_0603_1608Metric | R1 |
| 1 | BOOT0 | switch:swt0310-0430xxgsa | SW1 |
| 1 | Battery Backup | Connector_JST:JST_XH_B2B-XH-A_1x02_P2.50mm_Vertical | J3 |
| 1 | Button | switch:swt0310-0430xxgsa-tht_breakout | SW2 |
| 1 | PAM8302AAS | Package_SO:MSOP-8_3x3mm_P0.65mm | U6 |
| 1 | Power Mux | TestPoint:TestPoint_Pad_D2.0mm | TP1 |
| 1 | Qwiic | Connector_JST:JST_SH_SM04B-SRSS-TB_1x04-1MP_P1.00mm_Horizontal | J4 |
| 1 | STM32L432KCU | stm32l4:QFN-32-1EP_5x5mm_P0.5mm_EP3.45x3.45mm | U4 |
| 1 | Speaker | Connector_JST:JST_PH_S2B-PH-SM4-TB_1x02-1MP_P2.00mm_Horizontal | J6 |
| 1 | TC2050-IDC-NL | tag_connect:tag_connect_tc2050-idc-nl | J1 |
| 1 | TLV77333PDBVR | common:sot-23_5 | U1 |
| 1 | TPS2116DRL | Package_TO_SOT_SMD:SOT-583-8 | U2 |
| 1 | USBLC6-2P6 | Package_TO_SOT_SMD:SOT-666 | U3 |
| 1 | W25Q128JVS | Package_SO:SOIC-8_5.3x5.3mm_P1.27mm | U5 |
| 1 | WS2812B | LED_SMD:LED_WS2812B_PLCC4_5.0x5.0mm_P3.2mm | D1 |
| 1 | WS2812B Breakout | Connector_JST:JST_PH_S3B-PH-SM4-TB_1x03-1MP_P2.00mm_Horizontal | J5 |
It measures 35 × 48 mm and has 4 copper layers, 1.56 mm thick.
61 parts, built around the STM32L432KCU6, PAM8302AASCR, W25Q128JVSIQ and TPS2116DRL. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the borkdlabs/hoppy_clock repository on GitHub, at the commit this page was made from: the board is hardware/hoppy_clock.kicad_pcb and its schematic hardware/hoppy_clock.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository publishes it under the MIT licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).

rober708/BIRD-E
The Bounded Infrared Recording Drone Ecosystem (BIRD-E) is a tethered quadcopter with an attached infrared camera, with livestream functionality and XBOX ground control. Senior design capstone at Purdue ECE.


christrotter/arcboard-stm32-mk2
Next iteration of the stm32f405 board; paradigm shift means new repo.

Twisted-Fields/rp2040-motor-controller
A dual channel brushless motor controller based on the RP2040 microcontroller, designed in Kicad.

skogaby/skogaslider
Firmware, CAD, and PCB files for the skogaslider, an RP2040-based controller to simulate Chunithm's gameplay experience



hazy2go/kagiboy
A hardware wallet that lives in a Game Boy cartridge. Real ROM, Solana + EVM, every signature approved on the Game Boy's own screen.