The New Status Light: Getting to blinky

A project log for USB Status Light

A slightly novelty notifier

Stephen HoldawayStephen Holdaway 07/06/2019 at 10:480 Comments

At 10pm on a Thursday evening in early April, it suddenly seemed like a great time to make another version of the status light I've been using for the last few years. Something small, simple and fun. Why not chuck together a schematic and a start a layout before calling it a night!

I'd been eyeing the STM32F070 for another project needing a basic, low-cost controller with a USB peripheral built in, and that seemed to fit the build here too. Sure, an ATTiny85 with V-USB is a little cheaper, but the options the STM32 brings to the table are worth the premium:

Most of these are overkill for a device that controls a handful of addressable LEDs, but the smooth remote debugging experience is probably worth the cost alone. The STM32 does need 3.3V regulation, but the part count is reduced overall compared to the V-USB approach.


There's not a whole lot to this piece of hardware. All of the spare pins on the micro are broken out so this can be used as a mini dev board for other devices that need USB control.

A few notes for next time:

PCB layout

Again not much to see here. The pictured layout is slightly improved over the design that went to the fab - "2.0a" fixes an unnecessarily long ground return path for the crystal, though this hasn't caused any issues in the boards I had made so far...

I'd previously bought all of the components needed for this build, so I sent the board off and moved onto other projects while waiting. The PCB manufacture only took a couple of days, but the standard-post shipping I chose took significantly longer.

Board bring-up

Three weeks later, the PCBs arrived and I spent an evening at the bench soldering. Assembly went fairly smoothly, with the regulator installed and tested first, then the STM32 installed and confirmed accessible over SWD:

The SK6812-mini LEDs went on, and toggling their data pin as fast as possible confirmed they were all working at full brightness. A little NOP wrangling later, the correct time-based data signal could be sent and the LEDs were ready to work. The encoding has slightly different timing than the WS2812(B), but it's the same concept.

The first issue arose when the board wouldn't communicate over USB, with dmesg reporting something along the lines of "device not accepting address". While trying to confirm the crystal was operating at the expected frequency I somehow managed to destroy the oscillator circuit in the chip I was using (just by probing it?), and from then on that chip would hang waiting for the external oscillator to start, even when the STM32 was completely removed from the circuit.

After replacing the crystal and MCU, everything worked correctly and a basic USB HID device was registering correctly with the host:

Basic USB device firmware

A simple USB device is fairly straight forward conceptually - configure a bunch of endpoints that can be polled by the host to read data, or interrupted by the host to write data. Combined with the semi-documented STM32 USB device library + HAL though, I was in for a fun time.

I won't bore you with the gory details, but after three long evenings of reading, digging through the example code and complaining about ST's code quality, and almost bailing to use libopencm3 I had an endpoint that I could send colour data:

Two LED status lights in development
Two status lights assembled and running. I haven't settled on a design for the 3D-printed case yet.

Continuously piping new colours to the device to animate works as a demo, but it's not ideal. The next step is to offload most of the work to the device and provide an interface for programming changes and animations:

The host side of this will be implemented in a Python library, since libusb1 makes the host side super easy:

with usb1.USBContext() as context:
    handle = context.openByVendorIDAndProductID(VENDOR_ID, PRODUCT_ID)

    if handle is None:
        print("Failed to find a device")

    with handle.claimInterface(0):
        handle.interruptWrite(1, bytearray(...))